From 09d66b26d35d6c1d425628cec21d64c63c9c273e Mon Sep 17 00:00:00 2001 From: Drew DeVault Date: Wed, 13 Sep 2017 22:09:32 -0400 Subject: [PATCH] Implement initial ticket detail page --- scss/main.scss | 10 ++++ todosrht/app.py | 32 +++++++++-- todosrht/blueprints/tracker.py | 72 ++++++++++++++++++++++-- todosrht/templates/ticket.html | 99 +++++++++++++++++++++++++++++++++ todosrht/templates/tracker.html | 10 +++- todosrht/types/__init__.py | 1 + todosrht/types/ticket.py | 2 +- todosrht/types/ticketaccess.py | 1 + todosrht/types/ticketcomment.py | 17 ++++++ todosrht/types/ticketstatus.py | 1 - todosrht/types/tracker.py | 2 + 11 files changed, 233 insertions(+), 14 deletions(-) create mode 100644 todosrht/templates/ticket.html create mode 100644 todosrht/types/ticketcomment.py diff --git a/scss/main.scss b/scss/main.scss index 9acd9cd..ef330ea 100644 --- a/scss/main.scss +++ b/scss/main.scss @@ -1 +1,11 @@ @import "base"; + +.ellipsis { + overflow-x: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +select { + padding: 0.1rem; +} diff --git a/todosrht/app.py b/todosrht/app.py index 52cc89f..0ff4920 100644 --- a/todosrht/app.py +++ b/todosrht/app.py @@ -8,7 +8,7 @@ from srht.config import cfg, cfgi, load_config load_config("todo") from srht.database import DbSession db = DbSession(cfg("sr.ht", "connection-string")) -from todosrht.types import User +from todosrht.types import User, TicketAccess, TicketStatus, TicketResolution db.init() from srht.flask import SrhtFlask @@ -43,13 +43,19 @@ app.register_blueprint(tracker) meta_sr_ht = cfg("network", "meta") meta_client_id = cfg("meta.sr.ht", "oauth-client-id") -def tracker_name(tracker): +def tracker_name(tracker, full=False): split = tracker.name.split("/") + user = "~" + tracker.owner.username + if full: + return Markup( + "/".join([ + "{}".format(user, "/".join(split[:i + 1]), p) + for i, p in enumerate(split) + ])) name = split[-1] if len(name) == 0: return name parts = split[:-1] - user = "~" + tracker.owner.username return Markup( "/".join([ "{}".format(user, "/".join(parts[:i + 1]), p) @@ -57,11 +63,29 @@ def tracker_name(tracker): ]) + "/" + name ) +def render_status(ticket, access): + if TicketAccess.edit in access: + return Markup( + "" + ) + else: + return "{}".format(ticket.status.name) + @app.context_processor def inject(): return { "oauth_url": oauth_url(request.full_path), "current_user": User.query.filter(User.id == current_user.id).first() \ if current_user else None, - "format_tracker_name": tracker_name + "format_tracker_name": tracker_name, + "render_status": render_status, + "TicketAccess": TicketAccess, + "TicketStatus": TicketStatus, + "TicketResolution": TicketResolution } diff --git a/todosrht/blueprints/tracker.py b/todosrht/blueprints/tracker.py index ad3a89b..e33f87d 100644 --- a/todosrht/blueprints/tracker.py +++ b/todosrht/blueprints/tracker.py @@ -4,7 +4,8 @@ from flask import Blueprint, render_template, request, url_for, abort, redirect from flask import session from flask_login import current_user from todosrht.decorators import loginrequired -from todosrht.types import Tracker, User, Ticket, TicketStatus +from todosrht.types import Tracker, User, Ticket, TicketStatus, TicketAccess +from todosrht.types import TicketComment from srht.validation import Validation from srht.database import db @@ -131,20 +132,20 @@ def tracker_configure_GET(owner, name): @tracker.route("///submit", methods=["POST"]) @loginrequired -def tracker_submit_GET(owner, name): +def tracker_submit_POST(owner, name): tracker = get_tracker(owner, name) if not tracker: abort(404) valid = Validation(request) title = valid.require("title", friendly_name="Title") - desc = valid.require("description", friendly_name="Description") + desc = valid.optional("description") another = valid.optional("another") 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) < 2048, + valid.expect(not desc or len(desc) < 16384, "Description must be no more than 16384 characters.", field="description") @@ -171,6 +172,67 @@ def tracker_submit_GET(owner, name): name=name, ticket_id=ticket.id)) +def get_access(tracker, ticket): + # TODO: flesh out + if current_user and current_user.id == tracker.owner_id: + return TicketAccess.all + elif current_user and current_user.id == ticket.submitter_id: + return ticket.submitter_perms or tracker.default_submitter_perms + elif current_user: + return ticket.user_perms or tracker.default_user_perms + return ticket.anonymous_perms or tracker.default_anonymous_perms + @tracker.route("///") def ticket_GET(owner, name, ticket_id): - pass + tracker = get_tracker(owner, name) + if not tracker: + abort(404) + ticket = Ticket.query.get(ticket_id) + if not ticket: + abort(404) + access = get_access(tracker, ticket) + if not TicketAccess.browse in access: + abort(404) + return render_template("ticket.html", + tracker=tracker, + ticket=ticket, + access=access) + +@tracker.route("////comment", methods=["POST"]) +@loginrequired +def ticket_comment_POST(owner, name, ticket_id): + tracker = get_tracker(owner, name) + if not tracker: + abort(404) + ticket = Ticket.query.get(ticket_id) + if not ticket: + abort(404) + access = get_access(tracker, ticket) + if not TicketAccess.browse in access: + abort(404) + + valid = Validation(request) + text = valid.require("comment", friendly_name="Comment") + + valid.expect(not text or 3 < len(text) < 16384, + "Comment must be between 3 and 16384 characters.") + + if not valid.ok: + return render_template("ticket.html", + tracker=tracker, + ticket=ticket, + access=access, + **valid.kwargs) + + comment = TicketComment() + comment.text = text + # TODO: anonymous comments (when configured appropriately) + comment.submitter_id = current_user.id + comment.ticket_id = ticket.id + db.session.add(comment) + db.session.commit() + + return redirect(url_for(".ticket_GET", + owner="~" + tracker.owner.username, + name=tracker.name, + ticket_id=ticket.id) + "#comment-" + str(comment.id)) diff --git a/todosrht/templates/ticket.html b/todosrht/templates/ticket.html new file mode 100644 index 0000000..75c138f --- /dev/null +++ b/todosrht/templates/ticket.html @@ -0,0 +1,99 @@ +{% extends "layout.html" %} +{% block body %} +
+
+
+

+ {{ format_tracker_name(tracker, full=True) }}/#{{ticket.id}}: + {{ticket.title}} +

+
+
+
+
+ {% if ticket.description %} +

Description

+ {{ ticket.description | md }} + {% endif %} + {% if TicketAccess.edit in access %} + Edit + {% endif %} +

+ Details +

+
+
Status
+
+ {{ ticket.status.name.upper() }} +
+
Submitter
+
~{{ ticket.submitter.username }}
+
Submitted
+
{{ ticket.created | date }}
+
Updated
+
{{ ticket.updated | date }}
+
User Agent
+
+ {{ ticket.user_agent }} +
+
+
+
+ {% for comment in ticket.comments %} +

+ ~{{ comment.submitter.username }} + + edit + delete + {{ comment.created | date }} + +

+ {{ comment.text | md }} + {% endfor %} + {% if TicketAccess.comment in access %} +

Add comment

+
+
+ + {{valid.summary("comment")}} +
+ + {% if TicketAccess.edit in access %} + + + {% endif %} +
+ {% else %} + {% if not ticket.comments %} +

It's a bit quiet in here.

+ {% endif %} + {% endif %} +
+
+
+{% endblock %} diff --git a/todosrht/templates/tracker.html b/todosrht/templates/tracker.html index 441407d..11af571 100644 --- a/todosrht/templates/tracker.html +++ b/todosrht/templates/tracker.html @@ -11,7 +11,7 @@ {{ tracker.description | md }}

Submit ticket

closed tickets + {% if len(tickets) %} @@ -90,18 +91,21 @@ {% endfor %}
#{{ticket.id}} {{ ticket.title }} {{ ticket.updated | date }} {{ ticket.submitter.username }}
+ {% else %} +
No tickets found for this search criteria.
+ {% endif %} {% if total_pages > 1 %}
diff --git a/todosrht/types/__init__.py b/todosrht/types/__init__.py index 9a86538..4142229 100644 --- a/todosrht/types/__init__.py +++ b/todosrht/types/__init__.py @@ -5,3 +5,4 @@ from .ticketstatus import TicketStatus, TicketResolution from .tracker import Tracker from .ticket import Ticket from .ticketsubscription import TicketSubscription +from .ticketcomment import TicketComment diff --git a/todosrht/types/ticket.py b/todosrht/types/ticket.py index 123bd95..8db5e7e 100644 --- a/todosrht/types/ticket.py +++ b/todosrht/types/ticket.py @@ -20,7 +20,7 @@ class Ticket(Base): submitter = sa.orm.relationship("User", backref=sa.orm.backref("submitted")) title = sa.Column(sa.Unicode(2048), nullable=False) - description = sa.Column(sa.Unicode(16384), nullable=False) + description = sa.Column(sa.Unicode(16384)) user_agent = sa.Column(sa.Unicode(2048)) status = sa.Column(FlagType(TicketStatus), diff --git a/todosrht/types/ticketaccess.py b/todosrht/types/ticketaccess.py index d84e42b..e02fd87 100644 --- a/todosrht/types/ticketaccess.py +++ b/todosrht/types/ticketaccess.py @@ -7,3 +7,4 @@ class TicketAccess(IntFlag): comment = 4 edit = 8 triage = 16 + all = browse | submit | comment | edit | triage diff --git a/todosrht/types/ticketcomment.py b/todosrht/types/ticketcomment.py new file mode 100644 index 0000000..ca50309 --- /dev/null +++ b/todosrht/types/ticketcomment.py @@ -0,0 +1,17 @@ +import sqlalchemy as sa +from srht.database import Base +from todosrht.types import FlagType, TicketAccess, TicketStatus, TicketResolution + +class TicketComment(Base): + __tablename__ = 'ticket_comment' + id = sa.Column(sa.Integer, primary_key=True) + created = sa.Column(sa.DateTime, nullable=False) + updated = sa.Column(sa.DateTime, nullable=False) + + submitter_id = sa.Column(sa.Integer, sa.ForeignKey("user.id"), nullable=False) + submitter = sa.orm.relationship("User") + + ticket_id = sa.Column(sa.Integer, sa.ForeignKey("ticket.id"), nullable=False) + ticket = sa.orm.relationship("Ticket", backref=sa.orm.backref("comments")) + + text = sa.Column(sa.Unicode(16384)) diff --git a/todosrht/types/ticketstatus.py b/todosrht/types/ticketstatus.py index 21c4586..11eb7f5 100644 --- a/todosrht/types/ticketstatus.py +++ b/todosrht/types/ticketstatus.py @@ -6,7 +6,6 @@ class TicketStatus(IntFlag): in_progress = 2 pending = 4 resolved = 8 - shipped = 16 class TicketResolution(IntFlag): unresolved = 0 diff --git a/todosrht/types/tracker.py b/todosrht/types/tracker.py index eef7fbc..d4bae43 100644 --- a/todosrht/types/tracker.py +++ b/todosrht/types/tracker.py @@ -18,6 +18,8 @@ class Tracker(Base): description = sa.Column(sa.Unicode(8192)) """Markdown""" + min_desc_length = sa.Column(sa.Integer, nullable=False, default=0) + enable_ticket_status = sa.Column(FlagType(TicketStatus), nullable=False, default=TicketStatus.resolved)