From bb22b8d791597d57ba376c1954a1e83be22a23f0 Mon Sep 17 00:00:00 2001 From: Drew DeVault Date: Wed, 13 Sep 2017 07:39:40 -0400 Subject: [PATCH] Flesh out schema and update templates/create --- todosrht/app.py | 26 +++- todosrht/blueprints/auth.py | 2 +- todosrht/blueprints/tracker.py | 93 +++++++++++++ todosrht/decorators.py | 15 ++ todosrht/templates/tracker-create.html | 51 +++++++ todosrht/templates/tracker.html | 185 +++++++++++++++++++++++++ todosrht/types/__init__.py | 4 +- todosrht/types/ticket.py | 40 +++--- todosrht/types/ticketassignee.py | 24 ++++ todosrht/types/ticketauditentry.py | 95 ------------- todosrht/types/ticketcomment.py | 19 --- todosrht/types/ticketsubscription.py | 22 +++ 12 files changed, 434 insertions(+), 142 deletions(-) create mode 100644 todosrht/blueprints/tracker.py create mode 100644 todosrht/decorators.py create mode 100644 todosrht/templates/tracker-create.html create mode 100644 todosrht/templates/tracker.html create mode 100644 todosrht/types/ticketassignee.py delete mode 100644 todosrht/types/ticketauditentry.py delete mode 100644 todosrht/types/ticketcomment.py create mode 100644 todosrht/types/ticketsubscription.py diff --git a/todosrht/app.py b/todosrht/app.py index 70fda07..52cc89f 100644 --- a/todosrht/app.py +++ b/todosrht/app.py @@ -1,5 +1,6 @@ from flask import render_template, request from flask_login import LoginManager, current_user +from jinja2 import Markup import locale import urllib @@ -27,23 +28,40 @@ try: except: pass +def oauth_url(return_to): + return "{}/oauth/authorize?client_id={}&scopes=profile&state={}".format( + meta_sr_ht, meta_client_id, urllib.parse.quote_plus(return_to)) + from todosrht.blueprints.html import html from todosrht.blueprints.auth import auth +from todosrht.blueprints.tracker import tracker app.register_blueprint(html) app.register_blueprint(auth) +app.register_blueprint(tracker) meta_sr_ht = cfg("network", "meta") meta_client_id = cfg("meta.sr.ht", "oauth-client-id") -def oauth_url(return_to): - return "{}/oauth/authorize?client_id={}&scopes=profile&state={}".format( - meta_sr_ht, meta_client_id, urllib.parse.quote_plus(return_to)) +def tracker_name(tracker): + split = tracker.name.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) + for i, p in enumerate(parts) + ]) + "/" + 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 + if current_user else None, + "format_tracker_name": tracker_name } diff --git a/todosrht/blueprints/auth.py b/todosrht/blueprints/auth.py index 0d969a6..73e48eb 100644 --- a/todosrht/blueprints/auth.py +++ b/todosrht/blueprints/auth.py @@ -69,7 +69,7 @@ def oauth_callback(): user.oauth_token_scopes = scopes db.session.commit() - login_user(user) + login_user(user, remember=True) if not state or not state.startswith("/"): return redirect("/") else: diff --git a/todosrht/blueprints/tracker.py b/todosrht/blueprints/tracker.py new file mode 100644 index 0000000..2b7eab8 --- /dev/null +++ b/todosrht/blueprints/tracker.py @@ -0,0 +1,93 @@ +import re +import string +from flask import Blueprint, render_template, request, url_for, abort, redirect +from flask_login import current_user +from todosrht.decorators import loginrequired +from todosrht.types import Tracker, User +from srht.validation import Validation +from srht.database import db + +tracker = Blueprint("tracker", __name__) + +name_re = re.compile(r"^([a-z][a-z0-9_.-]*/?)+$") + +@tracker.route("/tracker/create") +@loginrequired +def create_GET(): + return render_template("tracker-create.html") + +@tracker.route("/tracker/create", methods=["POST"]) +@loginrequired +def create_POST(): + valid = Validation(request) + name = valid.require("tracker_name", friendly_name="Name") + desc = valid.optional("tracker_desc") + if not valid.ok: + return render_template("tracker-create.html", **valid.kwargs), 400 + + valid.expect(2 < len(name) < 256, + "Must be between 2 and 256 characters", + field="tracker_name") + valid.expect(not valid.ok or name[0] in string.ascii_lowercase, + "Must begin with a lowercase letter", field="tracker_name") + valid.expect(not valid.ok or name_re.match(name), + "Only lowercase alphanumeric characters or -./", + field="tracker_name") + valid.expect(not desc or len(desc) < 4096, + "Must be less than 4096 characters", + field="tracker_desc") + if not valid.ok: + return render_template("tracker-create.html", **valid.kwargs), 400 + + tracker = (Tracker.query + .filter(Tracker.owner_id == current_user.id) + .filter(Tracker.name == name) + ).first() + valid.expect(not tracker, + "A tracker by this name already exists", + field="tracker_name") + if not valid.ok: + return render_template("tracker-create.html", **valid.kwargs), 400 + + tracker = Tracker() + tracker.owner_id = current_user.id + tracker.name = name + tracker.description = desc + db.session.add(tracker) + db.session.commit() + + if "create-configure" in valid: + return redirect(url_for(".tracker_configure_GET", + owner=current_user.username, + name=name)) + + return redirect(url_for(".tracker_GET", + owner="~" + current_user.username, + name=name)) + +@tracker.route("//") +def tracker_GET(owner, name): + if owner.startswith("~"): + owner = User.query.filter(User.username == owner[1:]).first() + if not owner: + abort(404) + print(name) + tracker = (Tracker.query + .filter(Tracker.owner_id == owner.id) + .filter(Tracker.name == name.lower()) + ).first() + if not tracker: + abort(404) + else: + abort(404) # TODO + return render_template("tracker.html", tracker=tracker) + +@tracker.route("///configure") +@loginrequired +def tracker_configure_GET(owner, name): + pass + +@tracker.route("///submit") +@loginrequired +def tracker_submit_GET(owner, name): + pass diff --git a/todosrht/decorators.py b/todosrht/decorators.py new file mode 100644 index 0000000..0a1e041 --- /dev/null +++ b/todosrht/decorators.py @@ -0,0 +1,15 @@ +from flask import redirect, request, abort +from flask_login import current_user +from functools import wraps +from todosrht.app import oauth_url + +import urllib + +def loginrequired(f): + @wraps(f) + def wrapper(*args, **kwargs): + if not current_user: + return redirect(oauth_url(request.url)) + else: + return f(*args, **kwargs) + return wrapper diff --git a/todosrht/templates/tracker-create.html b/todosrht/templates/tracker-create.html new file mode 100644 index 0000000..d37785b --- /dev/null +++ b/todosrht/templates/tracker-create.html @@ -0,0 +1,51 @@ +{% extends "layout.html" %} +{% block content %} +
+
+
+

Create new tracker

+
+
+ + + {{valid.summary("tracker_name")}} +

+ Use slashes to nest trackers (i.e. my-project/linux) +

+
+
+ + +

+ Markdown supported +

+ {{valid.summary("tracker_desc")}} +
+ {{valid.summary()}} + + +
+
+
+
+{% endblock %} diff --git a/todosrht/templates/tracker.html b/todosrht/templates/tracker.html new file mode 100644 index 0000000..7b9bcaf --- /dev/null +++ b/todosrht/templates/tracker.html @@ -0,0 +1,185 @@ +{% extends "layout.html" %} +{% block body %} +
+
+
+

{{ format_tracker_name(tracker) }}

+
+
+
+
+ {{ tracker.description | md }} +

Submit ticket

+
+
+ + +
+
+ + +
+ + +
+
+
+
+ +
+ + + + + + + + + + + + {% for n in range(0, 3) %} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {% endfor %} + +
TitleUpdatedSubmitter
#1289Won't start weston-terminal if WLC_XWAYLAND != 03 days agojohalun
#1287Variable prefix of another variable5 days agoemersion
#1286Remove titlebar in tabbed mode too6 days agoormung
#1284Memory leaks in swaybar11 days ago4e554c4c
#1278Chromium (Aura) context menus do not hold window focus.14 days agoZach-Button
#1266Inconsistent Caps Lock behavior19 days agozasma
#1260Sway on Void23 days agojulio641742
#1245swaygrab appears to interpret spaces in filenameJun 19louisch
#1230Build failure - gcc: fatal error: cannot specify -o with -c, -S, or -E with multiple filesMay 30ng-0
#1229sway floating > spawns at the top left corner instead of at the middle of the screenMay 25narutowindy
+
+
+
+
+ [ 1 / 23 ] +
+
+ [next] +
+
+
+
+
+{% endblock %} diff --git a/todosrht/types/__init__.py b/todosrht/types/__init__.py index bf1f103..c3fe249 100644 --- a/todosrht/types/__init__.py +++ b/todosrht/types/__init__.py @@ -2,7 +2,5 @@ from .flagtype import FlagType from .user import User from .ticketaccess import TicketAccess from .tracker import Tracker -from .ticketfield import TicketFieldType, TicketField -from .ticketfieldvalue import TicketFieldValue from .ticket import Ticket -from .ticketauditentry import AuditFieldType, PermissionsTarget, TicketAuditEntry +from .ticketsubscription import TicketSubscription diff --git a/todosrht/types/ticket.py b/todosrht/types/ticket.py index 9b69fe7..6756981 100644 --- a/todosrht/types/ticket.py +++ b/todosrht/types/ticket.py @@ -1,40 +1,40 @@ import sqlalchemy as sa from srht.database import Base -from todosrht.types import TicketAccess, FlagType +from todosrht.types import FlagType, TicketAccess +from enum import Enum class Ticket(Base): - """ - Represents a ticket filed in the system. The default permissions are - inherited from the tracker configuration, but may be edited to i.e. - - - Give an arbitrary edit/view/whatever access - - Remove a specific user's permission to edit - - Allow the public to comment on an otherwise uncommentable issue - - Lock an issue from further discussion from non-contributors - - etc - """ __tablename__ = 'ticket' id = sa.Column(sa.Integer, primary_key=True) created = sa.Column(sa.DateTime, nullable=False) updated = sa.Column(sa.DateTime, nullable=False) - ticket_id = sa.Column(sa.Integer, index=True) - """The ID specific to this tracker, appears in URLs etc""" - name = sa.Column(sa.Unicode(2048), nullable=False) tracker_id = sa.Column(sa.Integer, sa.ForeignKey("tracker.id"), nullable=False) tracker = sa.orm.relationship("Tracker", backref=sa.orm.backref("tickets")) submitter_id = sa.Column(sa.Integer, sa.ForeignKey("user.id"), nullable=False) - submitter = sa.orm.relationship("User", backref=sa.orm.backref("tickets")) + submitter = sa.orm.relationship("User", backref=sa.orm.backref("submitted")) - default_user_perms = sa.Column(FlagType(TicketAccess), nullable=False) + title = sa.Column(sa.Unicode(2048), nullable=False) + description = sa.Column(sa.Unicode(16384), nullable=False) + user_agent = sa.Column(sa.Unicode(2048)) + + user_perms = sa.Column(FlagType(TicketAccess), + nullable=False, + default=TicketAccess.browse + TicketAccess.submit + TicketAccess.comment) """Permissions given to any logged in user""" - default_submitter_perms = sa.Column(FlagType(TicketAccess), nullable=False) - """Permissions granted to the ticket submitter""" + submitter_perms = sa.Column(FlagType(TicketAccess), + nullable=False, + default=TicketAccess.browse + TicketAccess.edit + TicketAccess.comment) + """Permissions granted to submitters for their own tickets""" - default_committer_perms = sa.Column(FlagType(TicketAccess), nullable=False) + committer_perms = sa.Column(FlagType(TicketAccess), + nullable=False, + default=TicketAccess.browse + TicketAccess.submit + TicketAccess.comment) """Permissions granted to people who have authored commits in the linked git repo""" - default_anonymous_perms = sa.Column(FlagType(TicketAccess), nullable=False) + anonymous_perms = sa.Column(FlagType(TicketAccess), + nullable=False, + default=TicketAccess.browse) """Permissions granted to anonymous (non-logged in) users""" diff --git a/todosrht/types/ticketassignee.py b/todosrht/types/ticketassignee.py new file mode 100644 index 0000000..8ba15c1 --- /dev/null +++ b/todosrht/types/ticketassignee.py @@ -0,0 +1,24 @@ +import sqlalchemy as sa +from srht.database import Base +from enum import Enum + +class TicketAssignee(Base): + __tablename__ = 'ticket_assignee' + id = sa.Column(sa.Integer, primary_key=True) + created = sa.Column(sa.DateTime, nullable=False) + updated = sa.Column(sa.DateTime, nullable=False) + + ticket_id = sa.Column(sa.Integer, sa.ForeignKey("ticket.id"), nullable=False) + ticket = sa.orm.relationship("Ticket", backref=sa.orm.backref("assignees")) + + assignee_id = sa.Column(sa.Integer, sa.ForeignKey("user.id"), nullable=False) + assignee = sa.orm.relationship("User", + backref=sa.orm.backref("assigned"), + foreign_keys="assignee_id") + + assigner_id = sa.Column(sa.Integer, sa.ForeignKey("user.id"), nullable=False) + assigner = sa.orm.relationship("User", + backref=sa.orm.backref("assigned"), + foreign_keys="assignee_id") + + role = sa.Column(sa.Unicode(256)) diff --git a/todosrht/types/ticketauditentry.py b/todosrht/types/ticketauditentry.py deleted file mode 100644 index c6e6f9d..0000000 --- a/todosrht/types/ticketauditentry.py +++ /dev/null @@ -1,95 +0,0 @@ -import sqlalchemy as sa -import sqlalchemy_utils as sau -from todosrht.types import FlagType, TicketAccess -from srht.database import Base -from enum import Enum - -class AuditFieldType(Enum): - """Describes what kind of field was updated in an audit log event""" - name = "name" - permissions = "permissions" - tracker = "tracker" - custom_field = "custom_field" - custom_event = "custom_event" - -class PermissionsTarget(Enum): - """Describes the target of an update to ticket permissions""" - anonymous = "anonymous" - logged_in = "logged_in" - submitted = "submitter" - committer = "committer" - user = "user" - """A specific named user""" - -class TicketAuditEntry(Base): - """ - Records an event that has occured to a ticket. The field_type tells you - what kind of field was affected, which is used to disambiguate the affected - columns in the database. - - AuditFieldType.name is used when the ticket is renamed. old_name and - new_name are valid for these events. - - AuditFieldType.permissions is used permissions are changed. old_permissions - and new_permissions are valid for these events, as well as - permissions_target, which describes what kind of user was affected by the - change. If permissions_target == PermissionsTarget.user, a specific user's - permissions were edited and permissions_user is valid. - - AuditFieldType.tracker is used when a ticket is moved between trackers. - old_tracker and new_tracker are valid for this event. - - AuditFieldType.custom_field is when a custom field is edited. - old_custom_value and new_custom_value are valid for this event. - - AuditFieldType.custom_event is used for events submitted through the API - (i.e. build status updates). oauth_client and custom_text are valid for - this event. - """ - __tablename__ = 'ticket_audit_entry' - id = sa.Column(sa.Integer, primary_key=True) - created = sa.Column(sa.DateTime, nullable=False) - ticket_id = sa.Column(sa.Integer, sa.ForeignKey("ticket.id"), nullable=False) - ticket = sa.orm.relationship("Ticket", - backref=sa.orm.backref("audit_log")) - field_type = sa.Column(sau.ChoiceType(AuditFieldType), nullable=False) - user_id = sa.Column(sa.Integer, sa.ForeignKey("user.id"), nullable=False) - user = sa.orm.relationship("User", - foreign_keys=[user_id]) - """The user who executed the change""" - ticket_field_id = sa.Column(sa.Integer, sa.ForeignKey("ticket_field.id")) - ticket_field = sa.orm.relationship("TicketField") - #oauth_client_id = sa.Column(sa.Integer, sa.ForeignKey("oauth_client.id")) - #oauth_client = sa.orm.relationship("OAuthClient") - - custom_text = sa.Column(sa.Unicode(4096)) - """Markdown, typically used for custom events submitted via API""" - - old_name = sa.Column(sa.Unicode(2048)) - new_name = sa.Column(sa.Unicode(2048)) - - old_permissions = sa.Column(FlagType(TicketAccess)) - new_permissions = sa.Column(FlagType(TicketAccess)) - permissions_target = sa.Column(sau.ChoiceType(PermissionsTarget)) - permissions_user_id = sa.Column(sa.Integer, sa.ForeignKey("user.id")) - permissions_user = sa.orm.relationship("User", - foreign_keys=[permissions_user_id]) - - old_tracker_id = sa.Column(sa.Integer, - sa.ForeignKey("tracker.id")) - old_tracker = sa.orm.relationship("Tracker", - foreign_keys=[old_tracker_id]) - new_tracker_id = sa.Column(sa.Integer, - sa.ForeignKey("tracker.id")) - new_tracker = sa.orm.relationship("Tracker", - foreign_keys=[new_tracker_id]) - - old_custom_value_id = sa.Column(sa.Integer, - sa.ForeignKey("ticket_field_value.id")) - old_custom_value = sa.orm.relationship("TicketFieldValue", - foreign_keys=[old_custom_value_id]) - - new_custom_value_id = sa.Column(sa.Integer, - sa.ForeignKey("ticket_field_value.id")) - new_custom_value = sa.orm.relationship("TicketFieldValue", - foreign_keys=[new_custom_value_id]) diff --git a/todosrht/types/ticketcomment.py b/todosrht/types/ticketcomment.py deleted file mode 100644 index 960f130..0000000 --- a/todosrht/types/ticketcomment.py +++ /dev/null @@ -1,19 +0,0 @@ -import sqlalchemy as sa -from srht.database import Base - -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) - - ticket_id = sa.Column(sa.Integer, sa.ForeignKey("ticket.id"), nullable=False) - ticket = sa.orm.relationship("Ticket", backref=sa.orm.backref("fields")) - - user_id = sa.Column(sa.Integer, sa.ForeignKey("user.id"), nullable=False) - user = sa.orm.relationship("User", backref=sa.orm.backref("comments")) - - text = sa.Column(sa.Unicode(16384), nullable=False) - """Markdown""" - visible = sa.Column(sa.Boolean, nullable=False, default=True) - """Deleted comments stay in the system, but are removed from the listing""" diff --git a/todosrht/types/ticketsubscription.py b/todosrht/types/ticketsubscription.py new file mode 100644 index 0000000..2109550 --- /dev/null +++ b/todosrht/types/ticketsubscription.py @@ -0,0 +1,22 @@ +import sqlalchemy as sa +import sqlalchemy_utils as sau +from srht.database import Base +from todosrht.types import FlagType +from enum import Enum + +class TicketSubscription(Base): + """One of user, email, or webhook will be valid. The rest will be null.""" + __tablename__ = 'ticket_subscription' + id = sa.Column(sa.Integer, primary_key=True) + created = sa.Column(sa.DateTime, nullable=False) + updated = sa.Column(sa.DateTime, nullable=False) + + ticket_id = sa.Column(sa.Integer, sa.ForeignKey("ticket.id"), nullable=False) + ticket = sa.orm.relationship("Ticket", backref=sa.orm.backref("subscriptions")) + + user_id = sa.Column(sa.Integer, sa.ForeignKey("user.id")) + user = sa.orm.relationship("User", backref=sa.orm.backref("subscriptions")) + + email = sa.Column(sa.Unicode(512)) + + webhook = sa.Column(sa.Unicode(1024))