Split settings into separate blueprint

This commit is contained in:
Drew DeVault 2019-11-04 16:10:41 -05:00
parent 8983bccf90
commit 2fb6bc8d13
6 changed files with 219 additions and 206 deletions

View File

@ -0,0 +1,208 @@
from flask import Blueprint, render_template, request, url_for, abort, redirect
from flask_login import current_user
from todosrht.access import get_tracker
from todosrht.trackers import get_recent_users
from todosrht.types import UserAccess, User
from todosrht.types import Ticket, TicketAccess
from todosrht.urls import tracker_url
from todosrht.webhooks import UserWebhook
from srht.database import db
from srht.flask import loginrequired, session
from srht.validation import Validation
settings = Blueprint("settings", __name__)
def parse_html_perms(short, valid):
result = 0
for sub_perm in TicketAccess:
new_perm = valid.optional("perm_{}_{}".format(short, sub_perm.name))
if new_perm:
result |= int(new_perm)
return result
access_help_map={
TicketAccess.browse:
"Permission to view tickets",
TicketAccess.submit:
"Permission to submit tickets",
TicketAccess.comment:
"Permission to comment on tickets",
TicketAccess.edit:
"Permission to edit tickets",
TicketAccess.triage:
"Permission to resolve, re-open, or label tickets",
}
@settings.route("/<owner>/<name>/settings/details")
@loginrequired
def details_GET(owner, name):
tracker, access = get_tracker(owner, name)
if not tracker:
abort(404)
if current_user.id != tracker.owner_id:
abort(403)
return render_template("tracker-details.html",
view="details", tracker=tracker)
@settings.route("/<owner>/<name>/settings/details", methods=["POST"])
@loginrequired
def details_POST(owner, name):
tracker, access = get_tracker(owner, name)
if not tracker:
abort(404)
if current_user.id != tracker.owner_id:
abort(403)
valid = Validation(request)
desc = valid.optional("tracker_desc", default=tracker.description)
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-details.html",
tracker=tracker, **valid.kwargs), 400
tracker.description = desc
UserWebhook.deliver(UserWebhook.Events.tracker_update,
tracker.to_dict(),
UserWebhook.Subscription.user_id == tracker.owner_id)
db.session.commit()
return redirect(tracker_url(tracker))
def render_tracker_access(tracker, **kwargs):
recent_users = get_recent_users(tracker)
return render_template("tracker-access.html",
view="access", tracker=tracker, access_type_list=TicketAccess,
access_help_map=access_help_map, recent_users=recent_users, **kwargs)
@settings.route("/<owner>/<name>/settings/access")
@loginrequired
def access_GET(owner, name):
tracker, access = get_tracker(owner, name)
if not tracker:
abort(404)
if current_user.id != tracker.owner_id:
abort(403)
return render_tracker_access(tracker)
@settings.route("/<owner>/<name>/settings/access", methods=["POST"])
@loginrequired
def access_POST(owner, name):
tracker, access = get_tracker(owner, name)
if not tracker:
abort(404)
if current_user.id != tracker.owner_id:
abort(403)
valid = Validation(request)
perm_anon = parse_html_perms('anon', valid)
perm_user = parse_html_perms('user', valid)
perm_submit = parse_html_perms('submit', valid)
if not valid.ok:
return render_tracker_access(tracker, **valid.kwargs), 400
tracker.default_anonymous_perms = perm_anon
tracker.default_user_perms = perm_user
tracker.default_submitter_perms = perm_submit
UserWebhook.deliver(UserWebhook.Events.tracker_update,
tracker.to_dict(),
UserWebhook.Subscription.user_id == tracker.owner_id)
db.session.commit()
return redirect(tracker_url(tracker))
@settings.route("/<owner>/<name>/settings/user-access/create", methods=["POST"])
@loginrequired
def user_access_create_POST(owner, name):
tracker, access = get_tracker(owner, name)
if not tracker:
abort(404)
if current_user.id != tracker.owner_id:
abort(403)
valid = Validation(request)
username = valid.require("username")
permissions = parse_html_perms("user_access", valid)
if not valid.ok:
return render_tracker_access(tracker, **valid.kwargs), 400
username = username.lstrip("~")
user = User.query.filter_by(username=username).one_or_none()
valid.expect(user, "User not found.", field="username")
if not valid.ok:
return render_tracker_access(tracker, **valid.kwargs), 400
existing = UserAccess.query.filter_by(user=user, tracker=tracker).count()
valid.expect(user != tracker.owner,
"Cannot override tracker owner's permissions.", field="username")
valid.expect(existing == 0,
"This user already has custom permissions assigned.", field="username")
if not valid.ok:
return render_tracker_access(tracker, **valid.kwargs), 400
ua = UserAccess(tracker=tracker, user=user, permissions=permissions)
db.session.add(ua)
db.session.commit()
return redirect(url_for("settings.access_GET",
owner=tracker.owner.canonical_name,
name=name))
@settings.route("/<owner>/<name>/settings/user-access/<user_id>/delete",
methods=["POST"])
@loginrequired
def user_access_delete_POST(owner, name, user_id):
tracker, access = get_tracker(owner, name)
if not tracker:
abort(404)
if current_user.id != tracker.owner_id:
abort(403)
UserAccess.query.filter_by(user_id=user_id, tracker_id=tracker.id).delete()
db.session.commit()
return redirect(url_for("settings.access_GET",
owner=tracker.owner.canonical_name,
name=name))
@settings.route("/<owner>/<name>/settings/delete")
@loginrequired
def delete_GET(owner, name):
tracker, access = get_tracker(owner, name)
if not tracker:
abort(404)
if current_user.id != tracker.owner_id:
abort(403)
return render_template("tracker-delete.html",
view="delete", tracker=tracker)
@settings.route("/<owner>/<name>/settings/delete", methods=["POST"])
@loginrequired
def delete_POST(owner, name):
tracker, access = get_tracker(owner, name)
if not tracker:
abort(404)
if current_user.id != tracker.owner_id:
abort(403)
session["notice"] = f"{tracker.owner}/{tracker.name} was deleted."
# SQLAlchemy shits itself on some of our weird constraints/relationships
# so fuck it, postgres knows what to do here
tracker_id = tracker.id
owner_id = tracker.owner_id
assert isinstance(tracker_id, int)
db.session.expunge_all()
db.engine.execute(f"DELETE FROM tracker WHERE id = {tracker_id};")
db.session.commit()
UserWebhook.deliver(UserWebhook.Events.tracker_delete,
{ "id": tracker_id },
UserWebhook.Subscription.user_id == owner_id)
return redirect(url_for("html.index"))

View File

@ -7,9 +7,8 @@ from todosrht.search import apply_search
from todosrht.tickets import get_last_seen_times, get_comment_counts
from todosrht.tickets import get_participant_for_user, submit_ticket
from todosrht.trackers import get_recent_users
from todosrht.types import Event, UserAccess
from todosrht.types import Label, TicketLabel
from todosrht.types import TicketSubscription, User, Participant
from todosrht.types import Event, Label, TicketLabel
from todosrht.types import TicketSubscription, Participant
from todosrht.types import Tracker, Ticket, TicketAccess
from todosrht.urls import tracker_url, ticket_url
from todosrht.webhooks import TrackerWebhook, UserWebhook
@ -52,7 +51,7 @@ def create_POST():
db.session.commit()
if "create-configure" in valid:
return redirect(url_for(".settings_details_GET",
return redirect(url_for(".details_GET",
owner=current_user.username,
name=tracker.name))
@ -162,201 +161,6 @@ def disable_notifications(owner, name):
db.session.commit()
return redirect(tracker_url(tracker))
def parse_html_perms(short, valid):
result = 0
for sub_perm in TicketAccess:
new_perm = valid.optional("perm_{}_{}".format(short, sub_perm.name))
if new_perm:
result |= int(new_perm)
return result
access_help_map={
TicketAccess.browse:
"Permission to view tickets",
TicketAccess.submit:
"Permission to submit tickets",
TicketAccess.comment:
"Permission to comment on tickets",
TicketAccess.edit:
"Permission to edit tickets",
TicketAccess.triage:
"Permission to resolve, re-open, or label tickets",
}
@tracker.route("/<owner>/<name>/settings/details")
@loginrequired
def settings_details_GET(owner, name):
tracker, access = get_tracker(owner, name)
if not tracker:
abort(404)
if current_user.id != tracker.owner_id:
abort(403)
return render_template("tracker-details.html",
view="details", tracker=tracker)
@tracker.route("/<owner>/<name>/settings/details", methods=["POST"])
@loginrequired
def settings_details_POST(owner, name):
tracker, access = get_tracker(owner, name)
if not tracker:
abort(404)
if current_user.id != tracker.owner_id:
abort(403)
valid = Validation(request)
desc = valid.optional("tracker_desc", default=tracker.description)
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-details.html",
tracker=tracker, **valid.kwargs), 400
tracker.description = desc
UserWebhook.deliver(UserWebhook.Events.tracker_update,
tracker.to_dict(),
UserWebhook.Subscription.user_id == tracker.owner_id)
db.session.commit()
return redirect(tracker_url(tracker))
def render_tracker_access(tracker, **kwargs):
recent_users = get_recent_users(tracker)
return render_template("tracker-access.html",
view="access", tracker=tracker, access_type_list=TicketAccess,
access_help_map=access_help_map, recent_users=recent_users, **kwargs)
@tracker.route("/<owner>/<name>/settings/access")
@loginrequired
def settings_access_GET(owner, name):
tracker, access = get_tracker(owner, name)
if not tracker:
abort(404)
if current_user.id != tracker.owner_id:
abort(403)
return render_tracker_access(tracker)
@tracker.route("/<owner>/<name>/settings/access", methods=["POST"])
@loginrequired
def settings_access_POST(owner, name):
tracker, access = get_tracker(owner, name)
if not tracker:
abort(404)
if current_user.id != tracker.owner_id:
abort(403)
valid = Validation(request)
perm_anon = parse_html_perms('anon', valid)
perm_user = parse_html_perms('user', valid)
perm_submit = parse_html_perms('submit', valid)
if not valid.ok:
return render_tracker_access(tracker, **valid.kwargs), 400
tracker.default_anonymous_perms = perm_anon
tracker.default_user_perms = perm_user
tracker.default_submitter_perms = perm_submit
UserWebhook.deliver(UserWebhook.Events.tracker_update,
tracker.to_dict(),
UserWebhook.Subscription.user_id == tracker.owner_id)
db.session.commit()
return redirect(tracker_url(tracker))
@tracker.route("/<owner>/<name>/settings/user-access/create", methods=["POST"])
@loginrequired
def settings_user_access_create_POST(owner, name):
tracker, access = get_tracker(owner, name)
if not tracker:
abort(404)
if current_user.id != tracker.owner_id:
abort(403)
valid = Validation(request)
username = valid.require("username")
permissions = parse_html_perms("user_access", valid)
if not valid.ok:
return render_tracker_access(tracker, **valid.kwargs), 400
username = username.lstrip("~")
user = User.query.filter_by(username=username).one_or_none()
valid.expect(user, "User not found.", field="username")
if not valid.ok:
return render_tracker_access(tracker, **valid.kwargs), 400
existing = UserAccess.query.filter_by(user=user, tracker=tracker).count()
valid.expect(user != tracker.owner,
"Cannot override tracker owner's permissions.", field="username")
valid.expect(existing == 0,
"This user already has custom permissions assigned.", field="username")
if not valid.ok:
return render_tracker_access(tracker, **valid.kwargs), 400
ua = UserAccess(tracker=tracker, user=user, permissions=permissions)
db.session.add(ua)
db.session.commit()
return redirect(url_for("tracker.settings_access_GET",
owner=tracker.owner.canonical_name,
name=name))
@tracker.route("/<owner>/<name>/settings/user-access/<user_id>/delete",
methods=["POST"])
@loginrequired
def settings_user_access_delete_POST(owner, name, user_id):
tracker, access = get_tracker(owner, name)
if not tracker:
abort(404)
if current_user.id != tracker.owner_id:
abort(403)
UserAccess.query.filter_by(user_id=user_id, tracker_id=tracker.id).delete()
db.session.commit()
return redirect(url_for("tracker.settings_access_GET",
owner=tracker.owner.canonical_name,
name=name))
@tracker.route("/<owner>/<name>/settings/delete")
@loginrequired
def settings_delete_GET(owner, name):
tracker, access = get_tracker(owner, name)
if not tracker:
abort(404)
if current_user.id != tracker.owner_id:
abort(403)
return render_template("tracker-delete.html",
view="delete", tracker=tracker)
@tracker.route("/<owner>/<name>/settings/delete", methods=["POST"])
@loginrequired
def settings_delete_POST(owner, name):
tracker, access = get_tracker(owner, name)
if not tracker:
abort(404)
if current_user.id != tracker.owner_id:
abort(403)
session["notice"] = f"{tracker.owner}/{tracker.name} was deleted."
# SQLAlchemy shits itself on some of our weird constraints/relationships
# so fuck it, postgres knows what to do here
tracker_id = tracker.id
owner_id = tracker.owner_id
assert isinstance(tracker_id, int)
db.session.expunge_all()
db.engine.execute(f"DELETE FROM tracker WHERE id = {tracker_id};")
db.session.commit()
UserWebhook.deliver(UserWebhook.Events.tracker_delete,
{ "id": tracker_id },
UserWebhook.Subscription.user_id == owner_id)
return redirect(url_for("html.index"))
@tracker.route("/<owner>/<name>/submit", methods=["POST"])
@loginrequired
def tracker_submit_POST(owner, name):
@ -412,7 +216,6 @@ def tracker_labels_GET(owner, name):
return render_template("tracker-labels.html",
tracker=tracker, access=access, is_owner=is_owner)
def validate_label(request):
valid = Validation(request)
name = valid.require("name")

View File

@ -18,11 +18,13 @@ class TodoApp(SrhtFlask):
from todosrht.blueprints.html import html
from todosrht.blueprints.tracker import tracker
from todosrht.blueprints.ticket import ticket
from todosrht.blueprints.settings import settings
register_api(self)
self.register_blueprint(html)
self.register_blueprint(tracker)
self.register_blueprint(ticket)
self.register_blueprint(settings)
self.add_template_filter(filters.label_badge)
self.add_template_filter(filters.render_comment)

View File

@ -18,17 +18,17 @@
>{{icon("caret-left")}} back</a>
</li>
<li class="nav-item">
{{link(url_for(".settings_details_GET",
{{link(url_for("settings.details_GET",
owner=tracker.owner.canonical_name,
name=tracker.name), "details")}}
</li>
<li class="nav-item">
{{link(url_for(".settings_access_GET",
{{link(url_for("settings.access_GET",
owner=tracker.owner.canonical_name,
name=tracker.name), "access")}}
</li>
<li class="nav-item">
{{link(url_for(".settings_delete_GET",
{{link(url_for("settings.delete_GET",
owner=tracker.owner.canonical_name,
name=tracker.name), "delete")}}
</li>

View File

@ -96,7 +96,7 @@
<form
method="POST"
class="form-horizontal"
action="{{ url_for(".settings_user_access_create_POST",
action="{{ url_for("settings.user_access_create_POST",
owner=tracker.owner.canonical_name,
name=tracker.name) }}"
>
@ -168,7 +168,7 @@
</td>
<td>
<form
action="{{ url_for(".settings_user_access_delete_POST",
action="{{ url_for("settings.user_access_delete_POST",
owner=tracker.owner.canonical_name,
name=tracker.name,
user_id=user_access.user_id) }}"

View File

@ -38,7 +38,7 @@
</li>
{% if current_user and current_user.id == tracker.owner_id %}
<li class="nav-item d-none d-sm-block">
<a class="nav-link" href="{{url_for(".settings_details_GET",
<a class="nav-link" href="{{url_for("settings.details_GET",
owner=tracker.owner.canonical_name,
name=tracker.name)}}"
>settings</a>