todosrht: Use GraphQL for tracker CRUD operations
Use the GraphQL API for tracker CRUD operations so that GraphQL-native user webhooks will be delivered.
This commit is contained in:
parent
ae7916d7fd
commit
ebb55fc95a
|
@ -1,11 +1,12 @@
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from flask import Blueprint, abort, request
|
from flask import Blueprint, current_app, abort, request
|
||||||
from srht.api import paginated_response
|
from srht.api import paginated_response
|
||||||
from srht.database import db
|
from srht.database import db
|
||||||
|
from srht.graphql import exec_gql
|
||||||
from srht.oauth import oauth, current_token
|
from srht.oauth import oauth, current_token
|
||||||
from srht.validation import Validation, valid_url
|
from srht.validation import Validation, valid_url
|
||||||
from todosrht.access import get_tracker, get_ticket
|
from todosrht.access import get_tracker, get_ticket
|
||||||
from todosrht.tickets import add_comment, submit_ticket
|
from todosrht.tickets import add_comment
|
||||||
from todosrht.tickets import get_participant_for_user, get_participant_for_external
|
from todosrht.tickets import get_participant_for_user, get_participant_for_external
|
||||||
from todosrht.blueprints.api import get_user
|
from todosrht.blueprints.api import get_user
|
||||||
from todosrht.types import Ticket, TicketAccess, TicketStatus, TicketResolution
|
from todosrht.types import Ticket, TicketAccess, TicketStatus, TicketResolution
|
||||||
|
@ -79,17 +80,26 @@ def tracker_tickets_POST(username, tracker_name):
|
||||||
if not valid.ok:
|
if not valid.ok:
|
||||||
return valid.response
|
return valid.response
|
||||||
|
|
||||||
if external_id:
|
input = {
|
||||||
participant = get_participant_for_external(external_id, external_url)
|
"subject": title,
|
||||||
else:
|
"body": desc,
|
||||||
participant = get_participant_for_user(current_token.user)
|
"created": created,
|
||||||
|
"externalId": external_id,
|
||||||
|
"externalUrl": external_url,
|
||||||
|
}
|
||||||
|
|
||||||
ticket = submit_ticket(tracker, participant, title, desc)
|
resp = exec_gql(current_app.site, """
|
||||||
if created:
|
mutation SubmitTicket($trackerId: Int!, $input: SubmitTicketInput!) {
|
||||||
ticket._no_autoupdate = True
|
submitTicket(trackerId: $trackerId, input: $input) {
|
||||||
ticket.created = created
|
id
|
||||||
ticket.updated = created
|
}
|
||||||
db.session.commit()
|
}
|
||||||
|
""", user=user, valid=valid, trackerId=tracker.id, input=input)
|
||||||
|
|
||||||
|
if not valid.ok:
|
||||||
|
return valid.response
|
||||||
|
|
||||||
|
ticket, _ = get_ticket(tracker, resp["submitTicket"]["id"])
|
||||||
|
|
||||||
TrackerWebhook.deliver(TrackerWebhook.Events.ticket_create,
|
TrackerWebhook.deliver(TrackerWebhook.Events.ticket_create,
|
||||||
ticket.to_dict(),
|
ticket.to_dict(),
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
from flask import Blueprint, abort, request
|
from flask import Blueprint, current_app, abort, request
|
||||||
from srht.api import paginated_response
|
from srht.api import paginated_response
|
||||||
from srht.database import db
|
from srht.database import db
|
||||||
|
from srht.graphql import exec_gql
|
||||||
from srht.oauth import oauth, current_token
|
from srht.oauth import oauth, current_token
|
||||||
from srht.validation import Validation
|
from srht.validation import Validation
|
||||||
from todosrht.access import get_tracker
|
from todosrht.access import get_tracker
|
||||||
from todosrht.blueprints.api import get_user
|
from todosrht.blueprints.api import get_user
|
||||||
from todosrht.tickets import get_participant_for_user
|
from todosrht.tickets import get_participant_for_user
|
||||||
from todosrht.types import Label, Tracker, TicketAccess, TicketSubscription
|
from todosrht.types import Label, Tracker, TicketAccess, TicketSubscription
|
||||||
from todosrht.webhooks import UserWebhook, TrackerWebhook
|
from todosrht.webhooks import TrackerWebhook
|
||||||
|
|
||||||
trackers = Blueprint("api_trackers", __name__)
|
trackers = Blueprint("api_trackers", __name__)
|
||||||
|
|
||||||
|
@ -26,24 +27,53 @@ def user_trackers_GET(username):
|
||||||
@oauth("trackers:write")
|
@oauth("trackers:write")
|
||||||
def user_trackers_POST():
|
def user_trackers_POST():
|
||||||
user = current_token.user
|
user = current_token.user
|
||||||
tracker, valid = Tracker.create_from_request(request, user)
|
valid = Validation(request)
|
||||||
|
name = valid.require("name", friendly_name="Name")
|
||||||
|
description = valid.optional("description")
|
||||||
|
visibility = valid.require("visibility")
|
||||||
if not valid.ok:
|
if not valid.ok:
|
||||||
return valid.response
|
return valid.response
|
||||||
db.session.add(tracker)
|
|
||||||
db.session.commit()
|
|
||||||
UserWebhook.deliver(UserWebhook.Events.tracker_create,
|
|
||||||
tracker.to_dict(),
|
|
||||||
UserWebhook.Subscription.user_id == tracker.owner_id)
|
|
||||||
|
|
||||||
# Auto-subscribe the owner
|
resp = exec_gql(current_app.site, """
|
||||||
sub = TicketSubscription()
|
mutation CreateTracker($name: String!, $description: String, $visibility: Visibility!) {
|
||||||
participant = get_participant_for_user(user)
|
createTracker(name: $name, description: $description, visibility: $visibility) {
|
||||||
sub.tracker_id = tracker.id
|
id
|
||||||
sub.participant_id = participant.id
|
created
|
||||||
db.session.add(sub)
|
updated
|
||||||
db.session.commit()
|
owner {
|
||||||
|
canonical_name: canonicalName
|
||||||
|
... on User {
|
||||||
|
name: username
|
||||||
|
}
|
||||||
|
}
|
||||||
|
name
|
||||||
|
description
|
||||||
|
defaultACL {
|
||||||
|
browse
|
||||||
|
submit
|
||||||
|
comment
|
||||||
|
edit
|
||||||
|
triage
|
||||||
|
}
|
||||||
|
visibility
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""", user=user, valid=valid, name=name, description=description, visibility=visibility)
|
||||||
|
|
||||||
return tracker.to_dict(), 201
|
if not valid.ok:
|
||||||
|
return valid.response
|
||||||
|
|
||||||
|
resp = resp["createTracker"]
|
||||||
|
|
||||||
|
# Build default_access list
|
||||||
|
resp["default_access"] = [
|
||||||
|
key for key in [
|
||||||
|
"browse", "submit", "comment", "edit", "triage"
|
||||||
|
] if resp["defaultACL"][key]
|
||||||
|
]
|
||||||
|
del resp["defaultACL"]
|
||||||
|
|
||||||
|
return resp, 201
|
||||||
|
|
||||||
@trackers.route("/api/user/<username>/trackers/<tracker_name>")
|
@trackers.route("/api/user/<username>/trackers/<tracker_name>")
|
||||||
@trackers.route("/api/trackers/<tracker_name>", defaults={"username": None})
|
@trackers.route("/api/trackers/<tracker_name>", defaults={"username": None})
|
||||||
|
@ -93,15 +123,55 @@ def user_tracker_by_name_PUT(username, tracker_name):
|
||||||
abort(404)
|
abort(404)
|
||||||
if tracker.owner_id != current_token.user_id:
|
if tracker.owner_id != current_token.user_id:
|
||||||
abort(401)
|
abort(401)
|
||||||
|
|
||||||
valid = Validation(request)
|
valid = Validation(request)
|
||||||
tracker.update(valid)
|
rewrite = lambda value: None if value == "" else value
|
||||||
|
input = {
|
||||||
|
key: rewrite(valid.source[key]) for key in [
|
||||||
|
"description"
|
||||||
|
] if valid.source.get(key) is not None
|
||||||
|
}
|
||||||
|
|
||||||
|
resp = exec_gql(current_app.site, """
|
||||||
|
mutation UpdateTracker($id: Int!, $input: TrackerInput!) {
|
||||||
|
updateTracker(id: $id, input: $input) {
|
||||||
|
id
|
||||||
|
created
|
||||||
|
updated
|
||||||
|
owner {
|
||||||
|
canonical_name: canonicalName
|
||||||
|
... on User {
|
||||||
|
name: username
|
||||||
|
}
|
||||||
|
}
|
||||||
|
name
|
||||||
|
description
|
||||||
|
defaultACL {
|
||||||
|
browse
|
||||||
|
submit
|
||||||
|
comment
|
||||||
|
edit
|
||||||
|
triage
|
||||||
|
}
|
||||||
|
visibility
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""", user=user, valid=valid, id=tracker.id, input=input)
|
||||||
|
|
||||||
if not valid.ok:
|
if not valid.ok:
|
||||||
return valid.response
|
return valid.response
|
||||||
db.session.commit()
|
|
||||||
UserWebhook.deliver(UserWebhook.Events.tracker_update,
|
resp = resp["updateTracker"]
|
||||||
tracker.to_dict(),
|
|
||||||
UserWebhook.Subscription.user_id == tracker.owner_id)
|
# Build default_access list
|
||||||
return tracker.to_dict()
|
resp["default_access"] = [
|
||||||
|
key for key in [
|
||||||
|
"browse", "submit", "comment", "edit", "triage"
|
||||||
|
] if resp["defaultACL"][key]
|
||||||
|
]
|
||||||
|
del resp["defaultACL"]
|
||||||
|
|
||||||
|
return resp
|
||||||
|
|
||||||
@trackers.route("/api/user/<username>/trackers/<tracker_name>",
|
@trackers.route("/api/user/<username>/trackers/<tracker_name>",
|
||||||
methods=["DELETE"])
|
methods=["DELETE"])
|
||||||
|
@ -115,17 +185,11 @@ def user_tracker_by_name_DELETE(username, tracker_name):
|
||||||
abort(404)
|
abort(404)
|
||||||
if tracker.owner_id != current_token.user_id:
|
if tracker.owner_id != current_token.user_id:
|
||||||
abort(401)
|
abort(401)
|
||||||
# SQLAlchemy shits itself on some of our weird constraints/relationships
|
exec_gql(current_app.site, """
|
||||||
# so fuck it, postgres knows what to do here
|
mutation DeleteTracker($id: Int!) {
|
||||||
tracker_id = tracker.id
|
deleteTracker(id: $id) { id }
|
||||||
owner_id = tracker.owner_id
|
}
|
||||||
assert isinstance(tracker_id, int)
|
""", user=user, id=tracker.id);
|
||||||
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 {}, 204
|
return {}, 204
|
||||||
|
|
||||||
@trackers.route("/api/user/<username>/trackers/<tracker_name>/labels")
|
@trackers.route("/api/user/<username>/trackers/<tracker_name>/labels")
|
||||||
|
|
|
@ -2,13 +2,14 @@ import gzip
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from flask import Blueprint, render_template, request, url_for, abort, redirect
|
from flask import Blueprint, current_app, render_template, request, url_for, abort, redirect
|
||||||
from flask import current_app, send_file
|
from flask import current_app, send_file
|
||||||
from srht.config import get_origin
|
from srht.config import get_origin
|
||||||
from srht.crypto import sign_payload
|
from srht.crypto import sign_payload
|
||||||
from srht.database import db
|
from srht.database import db
|
||||||
from srht.oauth import current_user, loginrequired
|
from srht.oauth import current_user, loginrequired
|
||||||
from srht.flask import date_handler, session
|
from srht.flask import date_handler, session
|
||||||
|
from srht.graphql import exec_gql
|
||||||
from srht.validation import Validation
|
from srht.validation import Validation
|
||||||
from tempfile import NamedTemporaryFile
|
from tempfile import NamedTemporaryFile
|
||||||
from todosrht.access import get_tracker
|
from todosrht.access import get_tracker
|
||||||
|
@ -16,7 +17,6 @@ from todosrht.trackers import get_recent_users
|
||||||
from todosrht.types import Event, EventType, Ticket, TicketAccess, Visibility
|
from todosrht.types import Event, EventType, Ticket, TicketAccess, Visibility
|
||||||
from todosrht.types import ParticipantType, UserAccess, User
|
from todosrht.types import ParticipantType, UserAccess, User
|
||||||
from todosrht.urls import tracker_url
|
from todosrht.urls import tracker_url
|
||||||
from todosrht.webhooks import UserWebhook
|
|
||||||
from todosrht.tracker_import import tracker_import
|
from todosrht.tracker_import import tracker_import
|
||||||
|
|
||||||
settings = Blueprint("settings", __name__)
|
settings = Blueprint("settings", __name__)
|
||||||
|
@ -63,24 +63,33 @@ def details_POST(owner, name):
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
valid = Validation(request)
|
valid = Validation(request)
|
||||||
desc = valid.optional("tracker_desc", default=tracker.description)
|
rewrite = lambda value: None if value == "" else value
|
||||||
vis = valid.require("visibility", cls=Visibility)
|
input = {
|
||||||
valid.expect(not desc or len(desc) < 4096,
|
key: rewrite(valid.source[key]) for key in [
|
||||||
"Must be less than 4096 characters",
|
"description", "visibility",
|
||||||
field="tracker_desc")
|
] if valid.source.get(key) is not None
|
||||||
|
}
|
||||||
|
|
||||||
|
resp = exec_gql(current_app.site, """
|
||||||
|
mutation UpdateTracker($id: Int!, $input: TrackerInput!) {
|
||||||
|
updateTracker(id: $id, input: $input) {
|
||||||
|
name
|
||||||
|
owner {
|
||||||
|
canonicalName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""", valid=valid, id=tracker.id, input=input)
|
||||||
|
|
||||||
if not valid.ok:
|
if not valid.ok:
|
||||||
return render_template("tracker-details.html",
|
return render_template("tracker-details.html",
|
||||||
tracker=tracker, **valid.kwargs), 400
|
tracker=tracker, **valid.kwargs), 400
|
||||||
|
|
||||||
tracker.description = desc
|
resp = resp["updateTracker"]
|
||||||
tracker.visibility = vis
|
|
||||||
|
|
||||||
UserWebhook.deliver(UserWebhook.Events.tracker_update,
|
return redirect(url_for("settings.details_GET",
|
||||||
tracker.to_dict(),
|
owner=resp["owner"]["canonicalName"],
|
||||||
UserWebhook.Subscription.user_id == tracker.owner_id)
|
name=resp["name"]))
|
||||||
|
|
||||||
db.session.commit()
|
|
||||||
return redirect(tracker_url(tracker))
|
|
||||||
|
|
||||||
|
|
||||||
def render_tracker_access(tracker, **kwargs):
|
def render_tracker_access(tracker, **kwargs):
|
||||||
|
@ -111,17 +120,23 @@ def access_POST(owner, name):
|
||||||
|
|
||||||
valid = Validation(request)
|
valid = Validation(request)
|
||||||
access = parse_html_perms('default', valid)
|
access = parse_html_perms('default', valid)
|
||||||
|
input = {
|
||||||
|
perm: ((access & TicketAccess[perm].value) != 0) for perm in [
|
||||||
|
"browse", "submit", "comment", "edit", "triage",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
resp = exec_gql(current_app.site, """
|
||||||
|
mutation updateTrackerACL($id: Int!, $input: ACLInput!) {
|
||||||
|
updateTrackerACL(trackerId: $id, input: $input) {
|
||||||
|
browse
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""", valid=valid, id=tracker.id, input=input)
|
||||||
|
|
||||||
if not valid.ok:
|
if not valid.ok:
|
||||||
return render_tracker_access(tracker, **valid.kwargs), 400
|
return render_tracker_access(tracker, **valid.kwargs), 400
|
||||||
|
|
||||||
tracker.default_access = access
|
|
||||||
|
|
||||||
UserWebhook.deliver(UserWebhook.Events.tracker_update,
|
|
||||||
tracker.to_dict(),
|
|
||||||
UserWebhook.Subscription.user_id == tracker.owner_id)
|
|
||||||
|
|
||||||
db.session.commit()
|
|
||||||
return redirect(tracker_url(tracker))
|
return redirect(tracker_url(tracker))
|
||||||
|
|
||||||
@settings.route("/<owner>/<name>/settings/user-access/create", methods=["POST"])
|
@settings.route("/<owner>/<name>/settings/user-access/create", methods=["POST"])
|
||||||
|
@ -198,20 +213,14 @@ def delete_POST(owner, name):
|
||||||
abort(404)
|
abort(404)
|
||||||
if current_user.id != tracker.owner_id:
|
if current_user.id != tracker.owner_id:
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
|
exec_gql(current_app.site, """
|
||||||
|
mutation DeleteTracker($id: Int!) {
|
||||||
|
deleteTracker(id: $id) { id }
|
||||||
|
}
|
||||||
|
""", id=tracker.id);
|
||||||
|
|
||||||
session["notice"] = f"{tracker.owner}/{tracker.name} was deleted."
|
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_GET"))
|
return redirect(url_for("html.index_GET"))
|
||||||
|
|
||||||
@settings.route("/<owner>/<name>/settings/import-export")
|
@settings.route("/<owner>/<name>/settings/import-export")
|
||||||
|
|
|
@ -1,20 +1,21 @@
|
||||||
from flask import Blueprint, render_template, request, url_for, abort, redirect
|
from flask import Blueprint, current_app, render_template, request, url_for, abort, redirect
|
||||||
from srht.config import cfg
|
from srht.config import cfg
|
||||||
from srht.database import db
|
from srht.database import db
|
||||||
from srht.flask import paginate_query, session
|
from srht.flask import paginate_query, session
|
||||||
|
from srht.graphql import exec_gql
|
||||||
from srht.oauth import current_user, loginrequired
|
from srht.oauth import current_user, loginrequired
|
||||||
from srht.validation import Validation
|
from srht.validation import Validation
|
||||||
from todosrht.access import get_tracker
|
from todosrht.access import get_tracker, get_ticket
|
||||||
from todosrht.color import color_from_hex, color_to_hex, get_text_color
|
from todosrht.color import color_from_hex, color_to_hex, get_text_color
|
||||||
from todosrht.color import valid_hex_color_code
|
from todosrht.color import valid_hex_color_code
|
||||||
from todosrht.filters import render_markup
|
from todosrht.filters import render_markup
|
||||||
from todosrht.search import apply_search
|
from todosrht.search import apply_search
|
||||||
from todosrht.tickets import get_participant_for_user, submit_ticket
|
from todosrht.tickets import get_participant_for_user
|
||||||
from todosrht.types import Event, Label, TicketLabel
|
from todosrht.types import Event, Label, TicketLabel
|
||||||
from todosrht.types import TicketSubscription, Participant
|
from todosrht.types import TicketSubscription, Participant
|
||||||
from todosrht.types import Tracker, Ticket, TicketAccess
|
from todosrht.types import Tracker, Ticket, TicketAccess
|
||||||
from todosrht.urls import tracker_url, ticket_url
|
from todosrht.urls import tracker_url, ticket_url
|
||||||
from todosrht.webhooks import TrackerWebhook, UserWebhook
|
from todosrht.webhooks import TrackerWebhook
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
@ -41,30 +42,37 @@ def create_GET():
|
||||||
@tracker.route("/tracker/create", methods=["POST"])
|
@tracker.route("/tracker/create", methods=["POST"])
|
||||||
@loginrequired
|
@loginrequired
|
||||||
def create_POST():
|
def create_POST():
|
||||||
tracker, valid = Tracker.create_from_request(request, current_user)
|
valid = Validation(request)
|
||||||
|
name = valid.require("name", friendly_name="Name")
|
||||||
|
visibility = valid.require("visibility")
|
||||||
|
description = valid.optional("description")
|
||||||
if not valid.ok:
|
if not valid.ok:
|
||||||
return render_template("tracker-create.html", **valid.kwargs), 400
|
return render_template("tracker-create.html", **valid.kwargs), 400
|
||||||
|
|
||||||
db.session.add(tracker)
|
resp = exec_gql(current_app.site, """
|
||||||
db.session.flush()
|
mutation CreateTracker($name: String!, $description: String, $visibility: Visibility!) {
|
||||||
|
createTracker(name: $name, description: $description, visibility: $visibility) {
|
||||||
|
name
|
||||||
|
owner {
|
||||||
|
canonicalName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""", valid=valid, name=name, description=description, visibility=visibility)
|
||||||
|
|
||||||
UserWebhook.deliver(UserWebhook.Events.tracker_create,
|
if not valid.ok:
|
||||||
tracker.to_dict(),
|
return render_template("tracker-create.html", **valid.kwargs), 400
|
||||||
UserWebhook.Subscription.user_id == tracker.owner_id)
|
|
||||||
|
|
||||||
participant = get_participant_for_user(current_user)
|
resp = resp["createTracker"]
|
||||||
sub = TicketSubscription()
|
|
||||||
sub.tracker_id = tracker.id
|
|
||||||
sub.participant_id = participant.id
|
|
||||||
db.session.add(sub)
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
if "create-configure" in valid:
|
if "create-configure" in valid:
|
||||||
return redirect(url_for("settings.details_GET",
|
return redirect(url_for("settings.details_GET",
|
||||||
owner=current_user.canonical_name,
|
owner=current_user.canonical_name,
|
||||||
name=tracker.name))
|
name=resp["name"]))
|
||||||
|
|
||||||
return redirect(tracker_url(tracker))
|
return redirect(url_for("tracker.tracker_GET",
|
||||||
|
owner=resp["owner"]["canonicalName"],
|
||||||
|
name=resp["name"]))
|
||||||
|
|
||||||
def return_tracker(tracker, access, **kwargs):
|
def return_tracker(tracker, access, **kwargs):
|
||||||
another = session.get("another") or False
|
another = session.get("another") or False
|
||||||
|
@ -183,20 +191,14 @@ def tracker_submit_POST(owner, name):
|
||||||
if not TicketAccess.submit in access:
|
if not TicketAccess.submit in access:
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
|
db.session.commit() # Unlock tracker row
|
||||||
|
|
||||||
valid = Validation(request)
|
valid = Validation(request)
|
||||||
title = valid.require("title", friendly_name="Title")
|
title = valid.require("title", friendly_name="Title")
|
||||||
desc = valid.optional("description")
|
desc = valid.optional("description")
|
||||||
another = valid.optional("another")
|
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) < 16384,
|
|
||||||
"Description must be no more than 16384 characters.",
|
|
||||||
field="description")
|
|
||||||
|
|
||||||
if not valid.ok:
|
if not valid.ok:
|
||||||
db.session.commit() # Unlock tracker row
|
|
||||||
return return_tracker(tracker, access, **valid.kwargs), 400
|
return return_tracker(tracker, access, **valid.kwargs), 400
|
||||||
|
|
||||||
if "preview" in request.form:
|
if "preview" in request.form:
|
||||||
|
@ -204,13 +206,24 @@ def tracker_submit_POST(owner, name):
|
||||||
return return_tracker(tracker, access,
|
return return_tracker(tracker, access,
|
||||||
rendered_preview=preview, **valid.kwargs), 200
|
rendered_preview=preview, **valid.kwargs), 200
|
||||||
|
|
||||||
# TODO: Handle unique constraint failure (contention) and retry?
|
input = {
|
||||||
participant = get_participant_for_user(current_user)
|
"subject": title,
|
||||||
ticket = submit_ticket(tracker, participant, title, desc)
|
"body": desc,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp = exec_gql(current_app.site, """
|
||||||
|
mutation SubmitTicket($trackerId: Int!, $input: SubmitTicketInput!) {
|
||||||
|
submitTicket(trackerId: $trackerId, input: $input) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""", valid=valid, trackerId=tracker.id, input=input)
|
||||||
|
|
||||||
|
if not valid.ok:
|
||||||
|
return return_tracker(tracker, access, **valid.kwargs), 400
|
||||||
|
|
||||||
|
ticket, _ = get_ticket(tracker, resp["submitTicket"]["id"])
|
||||||
|
|
||||||
UserWebhook.deliver(UserWebhook.Events.ticket_create,
|
|
||||||
ticket.to_dict(),
|
|
||||||
UserWebhook.Subscription.user_id == current_user.id)
|
|
||||||
TrackerWebhook.deliver(TrackerWebhook.Events.ticket_create,
|
TrackerWebhook.deliver(TrackerWebhook.Events.ticket_create,
|
||||||
ticket.to_dict(),
|
ticket.to_dict(),
|
||||||
TrackerWebhook.Subscription.tracker_id == tracker.id)
|
TrackerWebhook.Subscription.tracker_id == tracker.id)
|
||||||
|
|
|
@ -6,31 +6,30 @@
|
||||||
<form class="row" method="POST">
|
<form class="row" method="POST">
|
||||||
{{csrf_token()}}
|
{{csrf_token()}}
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="form-group {{valid.cls("tracker_name")}}">
|
<div class="form-group {{valid.cls("name")}}">
|
||||||
<label for="tracker_name">
|
<label for="tracker_name">
|
||||||
Name
|
Name
|
||||||
<span class="text-muted">(you can't edit this)</p>
|
<span class="text-muted">(you can't edit this)</p>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
name="tracker_name"
|
name="name"
|
||||||
id="tracker_name"
|
id="name"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
value="{{ tracker.name }}"
|
value="{{ tracker.name }}"
|
||||||
disabled />
|
disabled />
|
||||||
{{ valid.summary("tracker_name") }}
|
{{ valid.summary("name") }}
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group {{ valid.cls('tracker_desc') }}">
|
<div class="form-group {{valid.cls("description")}}">
|
||||||
<label for="tracker_desc">Description</label>
|
<label for="description">Description</label>
|
||||||
<textarea
|
<textarea
|
||||||
name="tracker_desc"
|
name="description"
|
||||||
id="tracker_desc"
|
id="description"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
rows="5"
|
rows="5"
|
||||||
aria-describedby="tracker_desc-help"
|
|
||||||
placeholder="Markdown supported"
|
placeholder="Markdown supported"
|
||||||
>{{tracker.desc or tracker.description}}</textarea>
|
>{{tracker.description}}</textarea>
|
||||||
{{ valid.summary("tracker_desc") }}
|
{{ valid.summary("description") }}
|
||||||
</div>
|
</div>
|
||||||
{{ valid.summary() }}
|
{{ valid.summary() }}
|
||||||
<span class="pull-right">
|
<span class="pull-right">
|
||||||
|
|
|
@ -45,49 +45,6 @@ class Tracker(Base):
|
||||||
import_in_progress = sa.Column(sa.Boolean,
|
import_in_progress = sa.Column(sa.Boolean,
|
||||||
nullable=False, server_default='f')
|
nullable=False, server_default='f')
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def create_from_request(request, user):
|
|
||||||
valid = Validation(request)
|
|
||||||
name = valid.require("name", friendly_name="Name")
|
|
||||||
visibility = valid.require("visibility", cls=Visibility)
|
|
||||||
desc = valid.optional("description")
|
|
||||||
if not valid.ok:
|
|
||||||
return None, valid
|
|
||||||
|
|
||||||
valid.expect(1 <= len(name) < 256,
|
|
||||||
"Must be between 1 and 255 characters",
|
|
||||||
field="name")
|
|
||||||
valid.expect(not valid.ok or name_re.match(name),
|
|
||||||
"Name must match [A-Za-z0-9._-]+",
|
|
||||||
field="name")
|
|
||||||
valid.expect(not valid.ok or name not in [".", ".."],
|
|
||||||
"Name cannot be '.' or '..'",
|
|
||||||
field="name")
|
|
||||||
valid.expect(not valid.ok or name not in [".git", ".hg"],
|
|
||||||
"Name must not be '.git' or '.hg'",
|
|
||||||
field="name")
|
|
||||||
valid.expect(not desc or len(desc) < 4096,
|
|
||||||
"Must be less than 4096 characters",
|
|
||||||
field="description")
|
|
||||||
if not valid.ok:
|
|
||||||
return None, valid
|
|
||||||
|
|
||||||
tracker = (Tracker.query
|
|
||||||
.filter(Tracker.owner_id == user.id)
|
|
||||||
.filter(Tracker.name.ilike(name.replace('_', '\\_')))
|
|
||||||
).first()
|
|
||||||
valid.expect(not tracker,
|
|
||||||
"A tracker by this name already exists", field="name")
|
|
||||||
if not valid.ok:
|
|
||||||
return None, valid
|
|
||||||
|
|
||||||
tracker = Tracker(owner=user,
|
|
||||||
name=name,
|
|
||||||
description=desc,
|
|
||||||
visibility=visibility)
|
|
||||||
|
|
||||||
return tracker, valid
|
|
||||||
|
|
||||||
def ref(self):
|
def ref(self):
|
||||||
return "{}/{}".format(
|
return "{}/{}".format(
|
||||||
self.owner.canonical_name,
|
self.owner.canonical_name,
|
||||||
|
@ -114,10 +71,3 @@ class Tracker(Base):
|
||||||
"visibility": self.visibility,
|
"visibility": self.visibility,
|
||||||
} if not short else {})
|
} if not short else {})
|
||||||
}
|
}
|
||||||
|
|
||||||
def update(self, valid):
|
|
||||||
desc = valid.optional("description", default=self.description)
|
|
||||||
valid.expect(not desc or len(desc) < 4096,
|
|
||||||
"Must be less than 4096 characters",
|
|
||||||
field="description")
|
|
||||||
self.description = desc
|
|
||||||
|
|
Loading…
Reference in New Issue