Flesh out schema and update templates/create

This commit is contained in:
Drew DeVault 2017-09-13 07:39:40 -04:00
parent f2885f8777
commit bb22b8d791
12 changed files with 434 additions and 142 deletions

View File

@ -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([
"<a href='/{}/{}'>{}</a>".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
}

View File

@ -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:

View File

@ -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("/<owner>/<path:name>")
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("/<owner>/<path:name>/configure")
@loginrequired
def tracker_configure_GET(owner, name):
pass
@tracker.route("/<owner>/<path:name>/submit")
@loginrequired
def tracker_submit_GET(owner, name):
pass

15
todosrht/decorators.py Normal file
View File

@ -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

View File

@ -0,0 +1,51 @@
{% extends "layout.html" %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-6">
<h2>Create new tracker</h2>
<form method="POST" action="/tracker/create">
<div class="form-group {{valid.cls("tracker_name")}}">
<label for="tracker_name">Name</label>
<input
type="text"
name="tracker_name"
id="tracker_name"
class="form-control"
value="{{ tracker_name or "" }}"
aria-describedby="tracker_name-help" />
{{valid.summary("tracker_name")}}
<p id="tracker_name-help" class="form-text text-muted">
Use slashes to nest trackers (i.e. my-project/linux)
</p>
</div>
<div class="form-group {{valid.cls("tracker_desc")}}">
<label for="tracker_desc">Description</label>
<textarea
name="tracker_desc"
id="tracker_desc"
class="form-control"
value="{{ tracker_desc or "" }}"
rows="5"
aria-describedby="tracker_desc-help">{{tracker_desc or ""}}</textarea>
<p id="tracker_desc-help" class="form-text text-muted">
Markdown supported
</p>
{{valid.summary("tracker_desc")}}
</div>
{{valid.summary()}}
<button
type="submit"
class="btn btn-default"
name="create"
>Create</button>
<button
type="submit"
class="btn btn-default"
name="create-configure"
>Create &amp; configure</button>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,185 @@
{% extends "layout.html" %}
{% block body %}
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<h2>{{ format_tracker_name(tracker) }}</h2>
</div>
</div>
<div class="row">
<div class="col-md-4">
{{ tracker.description | md }}
<h3 style="margin-top: 1rem">Submit ticket</h3>
<form method="POST" action="{{
url_for(".tracker_submit_GET",
owner=tracker.owner.username,
name=tracker.name
)
}}">
<div class="form-group">
<label for="title">Title</label>
<input
type="text"
class="form-control"
id="title"
name="title" />
</div>
<div class="form-group">
<label for="description">Description</label>
<textarea
class="form-control"
id="description"
name="description"
placeholder="Markdown supported"
rows="5"></textarea>
</div>
<button
type="submit"
class="btn btn-default"
>Submit</button>
<label class="form-check-label" style="margin-left: 0.5rem">
<input
class="form-check-input"
type="checkbox"
name="another"
style="position: relative; top: 2px;"
{% if another %}
checked
{% endif %}> Submit another?
</label>
</form>
</div>
<div class="col-md-8">
<form>
<input
name="search"
type="text"
placeholder="Search tickets... status:closed order:updated submitter:me"
class="form-control"
value="{{ search if search else "" }}" />
</form>
<ul class="nav nav-tabs">
<li class="nav-item">
<a
class="nav-link active"
href="#">open tickets</a>
</li>
<li class="nav-item">
<a
class="nav-link"
href="#">closed tickets</a>
</li>
<li class="nav-item">
<a
class="nav-link"
href="#">board owner</a>
</li>
<li class="nav-item">
<a
class="nav-link"
href="#">can add</a>
</li>
<li class="nav-item">
<a
class="nav-link"
href="#">arbitrary tabs</a>
</li>
<li class="nav-item">
<a
class="nav-link"
href="#">users can also</a>
</li>
<li class="nav-item">
<a
class="nav-link"
href="#">add personal ones</a>
</li>
</ul>
<table class="table table-striped">
<thead>
<tr>
<th></th>
<th>Title</th>
<th>Updated</th>
<th>Submitter</th>
</tr>
</thead>
<tbody>
{% for n in range(0, 3) %}
<tr>
<td><a href="#">#1289</a></td>
<td>Won't start weston-terminal if WLC_XWAYLAND != 0</td>
<td>3 days ago</td>
<td><a href="#">johalun</a></td>
</tr>
<tr>
<td><a href="#">#1287</a></td>
<td>Variable prefix of another variable</td>
<td>5 days ago</td>
<td><a href="#">emersion</a></td>
</tr>
<tr>
<td><a href="#">#1286</a></td>
<td>Remove titlebar in tabbed mode too</td>
<td>6 days ago</td>
<td><a href="#">ormung</a></td>
</tr>
<tr>
<td><a href="#">#1284</a></td>
<td>Memory leaks in swaybar</td>
<td>11 days ago</td>
<td><a href="#">4e554c4c</a></td>
</tr>
<tr>
<td><a href="#">#1278</a></td>
<td>Chromium (Aura) context menus do not hold window focus.</td>
<td>14 days ago</td>
<td><a href="#">Zach-Button</a></td>
</tr>
<tr>
<td><a href="#">#1266</a></td>
<td>Inconsistent Caps Lock behavior</td>
<td>19 days ago</td>
<td><a href="#">zasma</a></td>
</tr>
<tr>
<td><a href="#">#1260</a></td>
<td>Sway on Void</td>
<td>23 days ago</td>
<td><a href="#">julio641742</a></td>
</tr>
<tr>
<td><a href="#">#1245</a></td>
<td>swaygrab appears to interpret spaces in filename</td>
<td>Jun 19</td>
<td><a href="#">louisch</a></td>
</tr>
<tr>
<td><a href="#">#1230</a></td>
<td>Build failure - gcc: fatal error: cannot specify -o with -c, -S, or -E with multiple files</td>
<td>May 30</td>
<td><a href="#">ng-0</a></td>
</tr>
<tr>
<td><a href="#">#1229</a></td>
<td>sway floating &gt; spawns at the top left corner instead of at the middle of the screen</td>
<td>May 25</td>
<td><a href="#">narutowindy</a></td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="row">
<div class="col-md-4">
</div>
<div class="col-md-4 text-centered">
[ 1 / 23 ]
</div>
<div class="col-md-4 text-right">
<strong><a href="#">[next]</a></strong>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -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

View File

@ -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"""

View File

@ -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))

View File

@ -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])

View File

@ -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"""

View File

@ -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))