Add simple ticket API
This commit is contained in:
parent
785574871c
commit
d6986aed72
|
@ -2,6 +2,7 @@ import factory
|
|||
from datetime import datetime, timedelta
|
||||
from factory.fuzzy import FuzzyText
|
||||
from srht.database import db
|
||||
from srht.validation import Validation
|
||||
from todosrht.types import Tracker, User, Ticket
|
||||
|
||||
future_datetime = datetime.now() + timedelta(days=10)
|
||||
|
@ -21,8 +22,8 @@ class UserFactory(factory.alchemy.SQLAlchemyModelFactory):
|
|||
|
||||
|
||||
class TrackerFactory(factory.alchemy.SQLAlchemyModelFactory):
|
||||
owner = factory.SubFactory(UserFactory)
|
||||
name = factory.Sequence(lambda n: f"tracker{n}")
|
||||
user = factory.SubFactory(UserFactory)
|
||||
valid = factory.Sequence(lambda n: Validation({ "name": f"tracker{n}" }))
|
||||
|
||||
class Meta:
|
||||
model = Tracker
|
||||
|
|
|
@ -58,45 +58,41 @@ def test_ticket_comment(mailbox):
|
|||
assert ticket.resolution == TicketResolution.unresolved
|
||||
|
||||
# Comment without status change
|
||||
comment = add_comment(user, ticket, text="how do you do, i")
|
||||
event = add_comment(user, ticket, text="how do you do, i")
|
||||
|
||||
# Submitter gets automatically subscribed
|
||||
assert TicketSubscription.query.filter_by(ticket=ticket, user=user).first()
|
||||
|
||||
assert comment.submitter == user
|
||||
assert comment.ticket == ticket
|
||||
assert comment.text == "how do you do, i"
|
||||
assert event.comment.submitter == user
|
||||
assert event.comment.ticket == ticket
|
||||
assert event.comment.text == "how do you do, i"
|
||||
|
||||
assert ticket.status == TicketStatus.reported
|
||||
assert ticket.resolution == TicketResolution.unresolved
|
||||
assert len(ticket.comments) == 1
|
||||
assert len(ticket.events) == 1
|
||||
|
||||
event = ticket.events[0]
|
||||
assert event.ticket == ticket
|
||||
assert event.comment == comment
|
||||
assert event.event_type == EventType.comment
|
||||
|
||||
assert len(mailbox) == 3
|
||||
assert_notifications_sent(comment.text)
|
||||
assert_notifications_sent(event.comment.text)
|
||||
assert_event_notifications_created(event)
|
||||
|
||||
# Comment and resolve issue
|
||||
comment = add_comment(user, ticket, text="see you've met my",
|
||||
event = add_comment(user, ticket, text="see you've met my",
|
||||
resolve=True, resolution=TicketResolution.fixed)
|
||||
|
||||
assert comment.submitter == user
|
||||
assert comment.ticket == ticket
|
||||
assert comment.text == "see you've met my"
|
||||
assert event.comment.submitter == user
|
||||
assert event.comment.ticket == ticket
|
||||
assert event.comment.text == "see you've met my"
|
||||
|
||||
assert ticket.status == TicketStatus.resolved
|
||||
assert ticket.resolution == TicketResolution.fixed
|
||||
assert len(ticket.comments) == 2
|
||||
assert len(ticket.events) == 2
|
||||
|
||||
event = ticket.events[1]
|
||||
assert event.ticket == ticket
|
||||
assert event.comment == comment
|
||||
assert event.event_type == EventType.status_change | EventType.comment
|
||||
assert event.old_status == TicketStatus.reported
|
||||
assert event.new_status == TicketStatus.resolved
|
||||
|
@ -108,31 +104,27 @@ def test_ticket_comment(mailbox):
|
|||
assert_event_notifications_created(event)
|
||||
|
||||
# Comment and reopen issue
|
||||
comment = add_comment(user, ticket, text="faithful handyman", reopen=True)
|
||||
event = add_comment(user, ticket, text="faithful handyman", reopen=True)
|
||||
|
||||
assert comment.submitter == user
|
||||
assert comment.ticket == ticket
|
||||
assert comment.text == "faithful handyman"
|
||||
assert event.comment.submitter == user
|
||||
assert event.comment.ticket == ticket
|
||||
assert event.comment.text == "faithful handyman"
|
||||
|
||||
assert ticket.status == TicketStatus.reported
|
||||
assert ticket.resolution == TicketResolution.fixed
|
||||
assert len(ticket.comments) == 3
|
||||
assert len(ticket.events) == 3
|
||||
|
||||
event = ticket.events[2]
|
||||
assert event.ticket == ticket
|
||||
assert event.comment == comment
|
||||
|
||||
assert len(mailbox) == 9
|
||||
assert_notifications_sent(comment.text)
|
||||
assert_notifications_sent(event.comment.text)
|
||||
assert_event_notifications_created(event)
|
||||
|
||||
# Resolve without commenting
|
||||
comment = add_comment(user, ticket,
|
||||
event = add_comment(user, ticket,
|
||||
resolve=True, resolution=TicketResolution.wont_fix)
|
||||
|
||||
assert comment is None
|
||||
|
||||
assert ticket.status == TicketStatus.resolved
|
||||
assert ticket.resolution == TicketResolution.wont_fix
|
||||
assert len(ticket.comments) == 3
|
||||
|
@ -140,25 +132,20 @@ def test_ticket_comment(mailbox):
|
|||
|
||||
event = ticket.events[3]
|
||||
assert event.ticket == ticket
|
||||
assert event.comment == comment
|
||||
|
||||
assert len(mailbox) == 12
|
||||
assert_notifications_sent("Ticket resolved: wont_fix")
|
||||
assert_event_notifications_created(event)
|
||||
|
||||
# Reopen without commenting
|
||||
comment = add_comment(user, ticket, reopen=True)
|
||||
|
||||
assert comment is None
|
||||
event = add_comment(user, ticket, reopen=True)
|
||||
|
||||
assert ticket.status == TicketStatus.reported
|
||||
assert ticket.resolution == TicketResolution.wont_fix
|
||||
assert len(ticket.comments) == 3
|
||||
assert len(ticket.events) == 5
|
||||
|
||||
event = ticket.events[4]
|
||||
assert event.ticket == ticket
|
||||
assert event.comment == comment
|
||||
|
||||
assert len(mailbox) == 15
|
||||
assert_notifications_sent()
|
||||
|
@ -234,7 +221,7 @@ def test_notifications_and_events(mailbox):
|
|||
f"and {u2.canonical_name} "
|
||||
f"also mentioning tickets #{t1.scoped_id}, and #{t2.scoped_id} and #999999"
|
||||
)
|
||||
comment = add_comment(commenter, ticket, text)
|
||||
event = add_comment(commenter, ticket, text)
|
||||
|
||||
assert len(mailbox) == 2
|
||||
|
||||
|
@ -265,11 +252,11 @@ def test_notifications_and_events(mailbox):
|
|||
u1_mention = u1_events.pop()
|
||||
u2_mention = u2_events.pop()
|
||||
|
||||
assert u1_mention.comment == comment
|
||||
assert u1_mention.comment == event.comment
|
||||
assert u1_mention.from_ticket == ticket
|
||||
assert u1_mention.by_user == commenter
|
||||
|
||||
assert u2_mention.comment == comment
|
||||
assert u2_mention.comment == event.comment
|
||||
assert u2_mention.from_ticket == ticket
|
||||
assert u2_mention.by_user == commenter
|
||||
|
||||
|
@ -280,11 +267,11 @@ def test_notifications_and_events(mailbox):
|
|||
t1_mention = t1.events[0]
|
||||
t2_mention = t2.events[0]
|
||||
|
||||
assert t1_mention.comment == comment
|
||||
assert t1_mention.comment == event.comment
|
||||
assert t1_mention.from_ticket == ticket
|
||||
assert t1_mention.by_user == commenter
|
||||
|
||||
assert t2_mention.comment == comment
|
||||
assert t2_mention.comment == event.comment
|
||||
assert t2_mention.from_ticket == ticket
|
||||
assert t2_mention.by_user == commenter
|
||||
|
||||
|
|
|
@ -134,9 +134,9 @@ class MailHandler:
|
|||
print("Rejected, invalid comment length")
|
||||
return "550 Comment must be between 3 and 16384 characters."
|
||||
|
||||
comment = add_comment(sender, ticket, text=body,
|
||||
event = add_comment(sender, ticket, text=body,
|
||||
resolution=resolution, resolve=resolve, reopen=reopen)
|
||||
print(f"Added comment to {comment.ticket.ref()}")
|
||||
print(f"Added comment to {ticket.ref()}")
|
||||
return "250 Message accepted for delivery"
|
||||
|
||||
async def handle_DATA(self, server, session, envelope):
|
||||
|
|
|
@ -15,10 +15,13 @@ def get_user(username):
|
|||
|
||||
def register_api(app):
|
||||
from todosrht.blueprints.api.trackers import trackers
|
||||
from todosrht.blueprints.api.tickets import tickets
|
||||
|
||||
trackers = csrf_bypass(trackers)
|
||||
tickets = csrf_bypass(tickets)
|
||||
|
||||
app.register_blueprint(trackers)
|
||||
app.register_blueprint(tickets)
|
||||
|
||||
@app.route("/api/version")
|
||||
def version():
|
||||
|
|
|
@ -0,0 +1,103 @@
|
|||
from flask import Blueprint, abort, request
|
||||
from srht.api import paginated_response
|
||||
from srht.database import db
|
||||
from srht.oauth import oauth, current_token
|
||||
from srht.validation import Validation
|
||||
from todosrht.access import get_tracker, get_ticket
|
||||
from todosrht.tickets import submit_ticket, add_comment
|
||||
from todosrht.blueprints.api import get_user
|
||||
from todosrht.types import Ticket, TicketAccess, TicketStatus, TicketResolution
|
||||
|
||||
tickets = Blueprint("api.tickets", __name__)
|
||||
|
||||
@tickets.route("/api/user/<username>/trackers/<tracker_name>/tickets")
|
||||
@tickets.route("/api/trackers/<tracker_name>/tickets",
|
||||
defaults={"username": None})
|
||||
@oauth("tickets:read")
|
||||
def tracker_tickets_GET(username, tracker_name):
|
||||
user = get_user(username)
|
||||
tracker, access = get_tracker(user, tracker_name, user=current_token.user)
|
||||
if not tracker:
|
||||
abort(404)
|
||||
if not TicketAccess.browse in access:
|
||||
abort(401)
|
||||
tickets = (Ticket.query
|
||||
.filter(Ticket.tracker_id == tracker.id)
|
||||
.order_by(Ticket.scoped_id.desc()))
|
||||
return paginated_response(Ticket.scoped_id, tickets)
|
||||
|
||||
@tickets.route("/api/user/<username>/trackers/<tracker_name>/tickets",
|
||||
methods=["POST"])
|
||||
@tickets.route("/api/trackers/<tracker_name>/tickets",
|
||||
defaults={"username": None}, methods=["POST"])
|
||||
@oauth("tickets:write")
|
||||
def tracker_tickets_POST(username, tracker_name):
|
||||
user = get_user(username)
|
||||
tracker, access = get_tracker(user, tracker_name, user=current_token.user)
|
||||
if not tracker:
|
||||
abort(404)
|
||||
if not TicketAccess.submit in access:
|
||||
abort(401)
|
||||
|
||||
valid = Validation(request)
|
||||
title = valid.require("title")
|
||||
desc = valid.require("description")
|
||||
valid.expect(not title or 3 <= len(title) <= 2048,
|
||||
"Title must be between 3 and 2048 characters.",
|
||||
field="title")
|
||||
valid.expect(not desc or len(desc) < 16384,
|
||||
"Description must be no more than 16384 characters.",
|
||||
field="description")
|
||||
if not valid.ok:
|
||||
return valid.response
|
||||
|
||||
ticket = submit_ticket(tracker, current_token.user, title, desc)
|
||||
return ticket.to_dict(), 201
|
||||
|
||||
@tickets.route("/api/user/<username>/trackers/<tracker_name>/tickets/<ticket_id>")
|
||||
@tickets.route("/api/trackers/<tracker_name>/tickets/<ticket_id>",
|
||||
defaults={"username": None})
|
||||
@oauth("tickets:read")
|
||||
def tracker_ticket_by_id_GET(username, tracker_name, ticket_id):
|
||||
user = get_user(username)
|
||||
tracker, _ = get_tracker(user, tracker_name, user=current_token.user)
|
||||
if not tracker:
|
||||
abort(404)
|
||||
ticket, access = get_ticket(tracker, ticket_id, user=current_token.user)
|
||||
if not TicketAccess.browse in access:
|
||||
abort(401)
|
||||
return ticket.to_dict()
|
||||
|
||||
@tickets.route("/api/user/<username>/trackers/<tracker_name>/tickets/<ticket_id>",
|
||||
methods=["PUT"])
|
||||
@tickets.route("/api/trackers/<tracker_name>/tickets/<ticket_id>",
|
||||
defaults={"username": None}, methods=["PUT"])
|
||||
@oauth("tickets:write")
|
||||
def tracker_ticket_by_id_PUT(username, tracker_name, ticket_id):
|
||||
user = get_user(username)
|
||||
tracker, _ = get_tracker(user, tracker_name, user=current_token.user)
|
||||
if not tracker:
|
||||
abort(404)
|
||||
ticket, access = get_ticket(tracker, ticket_id, user=current_token.user)
|
||||
|
||||
required_access = TicketAccess.none
|
||||
valid = Validation(request)
|
||||
comment = resolution = None
|
||||
resolve = reopen = False
|
||||
if "comment" in valid:
|
||||
comment = valid.optional("comment")
|
||||
valid.expect(not comment or 3 <= len(comment) <= 16384,
|
||||
"Comment must be between 3 and 16384 characters.",
|
||||
field="comment")
|
||||
if "status" in valid:
|
||||
status = valid.optional("status",
|
||||
cls=TicketStatus, default=valid.status)
|
||||
if status != ticket.status:
|
||||
if status != TicketStatus.open:
|
||||
resolve = True
|
||||
resolution = valid.require("resolution", cls=TicketResolution)
|
||||
else:
|
||||
reopen = True
|
||||
|
||||
event = add_comment(user, ticket, comment, resolve, resolution, reopen)
|
||||
return event.to_dict()
|
|
@ -150,10 +150,10 @@ def ticket_comment_POST(owner, name, ticket_id):
|
|||
ctx = get_ticket_context(ticket, tracker, access)
|
||||
return render_template("ticket.html", **ctx, **valid.kwargs)
|
||||
|
||||
comment = add_comment(current_user, ticket,
|
||||
event = add_comment(current_user, ticket,
|
||||
text=text, resolve=resolve, resolution=resolution, reopen=reopen)
|
||||
|
||||
return redirect(ticket_url(ticket, comment))
|
||||
return redirect(ticket_url(ticket, event.comment))
|
||||
|
||||
@ticket.route("/<owner>/<name>/<int:ticket_id>/edit")
|
||||
@loginrequired
|
||||
|
|
|
@ -249,7 +249,7 @@ def add_comment(user, ticket,
|
|||
ticket.tracker.updated = datetime.utcnow()
|
||||
db.session.commit()
|
||||
|
||||
return comment
|
||||
return event
|
||||
|
||||
def mark_seen(ticket, user):
|
||||
"""Mark the ticket as seen by user."""
|
||||
|
|
|
@ -60,6 +60,28 @@ class Event(Base):
|
|||
def __repr__(self):
|
||||
return '<Event {}>'.format(self.id)
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"created": self.created,
|
||||
"event_type": [t.name for t in EventType if t in self.event_type],
|
||||
"old_status": self.old_status.name,
|
||||
"old_resolution": self.old_resolution.name,
|
||||
"new_status": self.new_status.name,
|
||||
"new_resolution": self.new_resolution.name,
|
||||
"user": self.user.to_dict(short=True)
|
||||
if self.user else None,
|
||||
"ticket": self.ticket.to_dict(short=True)
|
||||
if self.ticket else None,
|
||||
"comment": self.comment.to_dict(short=True)
|
||||
if self.comment else None,
|
||||
"label": self.label.name if self.label else None,
|
||||
"by_user": self.by_user.to_dict(short=True)
|
||||
if self.by_user else None,
|
||||
"from_ticket": self.from_ticket.to_dict(short=True)
|
||||
if self.from_ticket else None,
|
||||
}
|
||||
|
||||
class EventNotification(Base):
|
||||
__tablename__ = 'event_notification'
|
||||
id = sa.Column(sa.Integer, primary_key=True)
|
||||
|
|
|
@ -77,3 +77,32 @@ class Ticket(Base):
|
|||
|
||||
def __repr__(self):
|
||||
return f"<Ticket {self.id}>"
|
||||
|
||||
def to_dict(self, short=False):
|
||||
def permissions(w):
|
||||
return [p.name for p in TicketAccess
|
||||
if p in w and p not in [TicketAccess.none, TicketAccess.all]]
|
||||
return {
|
||||
"id": self.scoped_id,
|
||||
"ref": self.ref(),
|
||||
"tracker": self.tracker.to_dict(short=True),
|
||||
**({
|
||||
"title": self.title,
|
||||
"created": self.created,
|
||||
"updated": self.updated,
|
||||
"submitter": self.submitter.to_dict(short=True),
|
||||
"description": self.description,
|
||||
"status": self.status.name,
|
||||
"resolution": self.resolution.name,
|
||||
"permissions": {
|
||||
"anonymous": permissions(self.anonymous_perms)
|
||||
if self.anonymous_perms else None,
|
||||
"submitter": permissions(self.submitter_perms)
|
||||
if self.submitter_perms else None,
|
||||
"user": permissions(self.user_perms)
|
||||
if self.user_perms else None,
|
||||
},
|
||||
"labels": [l.name for l in self.labels],
|
||||
"assignees": [u.to_dict(short=True) for u in self.assigned_users],
|
||||
} if not short else {}),
|
||||
}
|
||||
|
|
|
@ -20,3 +20,14 @@ class TicketComment(Base):
|
|||
backref=sa.orm.backref("comments", cascade="all, delete-orphan"))
|
||||
|
||||
text = sa.Column(sa.Unicode(16384))
|
||||
|
||||
def to_dict(self, short=False):
|
||||
return {
|
||||
"id": self.id,
|
||||
"created": self.created,
|
||||
"submitter": self.submitter.to_dict(short=True),
|
||||
"text": self.text,
|
||||
**({
|
||||
"ticket": self.ticket.to_dict(short=True),
|
||||
} if not short else {})
|
||||
}
|
||||
|
|
|
@ -84,28 +84,30 @@ class Tracker(Base):
|
|||
return
|
||||
|
||||
self.owner_id = user.id
|
||||
self.owner = user
|
||||
self.name = name
|
||||
self.description = desc
|
||||
|
||||
def __repr__(self):
|
||||
return '<Tracker {} {}>'.format(self.id, self.name)
|
||||
|
||||
def to_dict(self):
|
||||
def to_dict(self, short=False):
|
||||
def permissions(w):
|
||||
return [p.name for p in TicketAccess
|
||||
if p in w and p not in [TicketAccess.none, TicketAccess.all]]
|
||||
return {
|
||||
"id": self.id,
|
||||
"owner": self.owner.to_dict(short=True),
|
||||
"created": self.created,
|
||||
"updated": self.updated,
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
"default_permissions": {
|
||||
"anonymous": permissions(self.default_anonymous_perms),
|
||||
"submitter": permissions(self.default_submitter_perms),
|
||||
"user": permissions(self.default_user_perms),
|
||||
},
|
||||
**({
|
||||
"description": self.description,
|
||||
"default_permissions": {
|
||||
"anonymous": permissions(self.default_anonymous_perms),
|
||||
"submitter": permissions(self.default_submitter_perms),
|
||||
"user": permissions(self.default_user_perms),
|
||||
},
|
||||
} if not short else {})
|
||||
}
|
||||
|
||||
def update(self, valid):
|
||||
|
|
Loading…
Reference in New Issue