api/graph: Implement ticket deletion
This commit is contained in:
parent
5650249985
commit
7d65b3653c
|
@ -164,6 +164,7 @@ enum WebhookEvent {
|
|||
TRACKER_DELETED @access(scope: TRACKERS, kind: RO)
|
||||
TICKET_CREATED @access(scope: TICKETS, kind: RO)
|
||||
TICKET_UPDATE @access(scope: TICKETS, kind: RO)
|
||||
TICKET_DELETED @access(scope: TICKETS, kind: RO)
|
||||
LABEL_CREATED @access(scope: TRACKERS, kind: RO)
|
||||
LABEL_UPDATE @access(scope: TRACKERS, kind: RO)
|
||||
LABEL_DELETED @access(scope: TRACKERS, kind: RO)
|
||||
|
@ -263,6 +264,15 @@ type TicketEvent implements WebhookPayload {
|
|||
ticket: Ticket!
|
||||
}
|
||||
|
||||
type TicketDeletedEvent implements WebhookPayload {
|
||||
uuid: String!
|
||||
event: WebhookEvent!
|
||||
date: Time!
|
||||
|
||||
trackerId: Int!
|
||||
ticketId: Int!
|
||||
}
|
||||
|
||||
type EventCreated implements WebhookPayload {
|
||||
uuid: String!
|
||||
event: WebhookEvent!
|
||||
|
@ -904,6 +914,9 @@ type Mutation {
|
|||
submitTicket(trackerId: Int!,
|
||||
input: SubmitTicketInput!): Ticket! @access(scope: TICKETS, kind: RW)
|
||||
|
||||
"Deletes a ticket."
|
||||
deleteTicket(trackerId: Int!, ticketId: Int!): Ticket! @access(scope: TICKETS, kind: RW)
|
||||
|
||||
# Creates a new ticket from an incoming email. (For internal use only)
|
||||
submitTicketEmail(trackerId: Int!,
|
||||
input: SubmitTicketEmailInput!): Ticket! @internal
|
||||
|
|
|
@ -835,6 +835,44 @@ func (r *mutationResolver) SubmitTicket(ctx context.Context, trackerID int, inpu
|
|||
return &ticket, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) DeleteTicket(ctx context.Context, trackerID int, ticketID int) (*model.Ticket, error) {
|
||||
tracker, err := loaders.ForContext(ctx).TrackersByID.Load(trackerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if tracker == nil {
|
||||
return nil, fmt.Errorf("No tracker by ID %d found for this user", trackerID)
|
||||
}
|
||||
if !tracker.CanEdit() {
|
||||
return nil, fmt.Errorf("Access denied")
|
||||
}
|
||||
|
||||
var ticket model.Ticket
|
||||
if err := database.WithTx(ctx, nil, func(tx *sql.Tx) error {
|
||||
row := tx.QueryRowContext(ctx, `
|
||||
DELETE FROM ticket tk
|
||||
WHERE tk.tracker_id = $1 AND tk.scoped_id = $2
|
||||
RETURNING
|
||||
id, scoped_id, submitter_id, tracker_id, created, updated,
|
||||
title, description, authenticity, status, resolution
|
||||
`, trackerID, ticketID)
|
||||
if err := row.Scan(&ticket.PKID, &ticket.ID, &ticket.SubmitterID,
|
||||
&ticket.TrackerID, &ticket.Created, &ticket.Updated, &ticket.Subject,
|
||||
&ticket.Body, &ticket.RawAuthenticity, &ticket.RawStatus,
|
||||
&ticket.RawResolution); err != nil {
|
||||
return err
|
||||
}
|
||||
webhooks.DeliverTrackerTicketDeletedEvent(ctx, ticket.TrackerID, &ticket)
|
||||
webhooks.DeliverTicketDeletedEvent(ctx, ticket.PKID, &ticket)
|
||||
return nil
|
||||
}); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, errors.New("No such ticket")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &ticket, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SubmitTicketEmail(ctx context.Context, trackerID int, input model.SubmitTicketEmailInput) (*model.Ticket, error) {
|
||||
validation := valid.New(ctx)
|
||||
validation.Expect(len(input.Subject) <= 2048,
|
||||
|
@ -2089,7 +2127,8 @@ func (r *mutationResolver) CreateTrackerWebhook(ctx context.Context, trackerID i
|
|||
model.WebhookEventLabelCreated, model.WebhookEventLabelUpdate,
|
||||
model.WebhookEventLabelDeleted:
|
||||
access = "TRACKERS"
|
||||
case model.WebhookEventTicketCreated, model.WebhookEventTicketUpdate:
|
||||
case model.WebhookEventTicketCreated, model.WebhookEventTicketUpdate,
|
||||
model.WebhookEventTicketDeleted:
|
||||
access = "TICKETS"
|
||||
case model.WebhookEventEventCreated:
|
||||
access = "EVENTS"
|
||||
|
@ -2196,7 +2235,7 @@ func (r *mutationResolver) CreateTicketWebhook(ctx context.Context, trackerID in
|
|||
// manual fuckery
|
||||
var access string
|
||||
switch ev {
|
||||
case model.WebhookEventTicketUpdate:
|
||||
case model.WebhookEventTicketUpdate, model.WebhookEventTicketDeleted:
|
||||
access = "TICKETS"
|
||||
case model.WebhookEventEventCreated:
|
||||
access = "EVENTS"
|
||||
|
|
|
@ -108,6 +108,19 @@ func DeliverTrackerTicketEvent(ctx context.Context,
|
|||
deliverTrackerWebhook(ctx, trackerID, event, &payload, payloadUUID)
|
||||
}
|
||||
|
||||
func DeliverTrackerTicketDeletedEvent(ctx context.Context, trackerID int, ticket *model.Ticket) {
|
||||
event := model.WebhookEventTicketDeleted
|
||||
payloadUUID := uuid.New()
|
||||
payload := model.TicketDeletedEvent{
|
||||
UUID: payloadUUID.String(),
|
||||
Event: event,
|
||||
Date: time.Now().UTC(),
|
||||
TrackerID: ticket.TrackerID,
|
||||
TicketID: ticket.ID,
|
||||
}
|
||||
deliverTrackerWebhook(ctx, trackerID, event, &payload, payloadUUID)
|
||||
}
|
||||
|
||||
func DeliverTrackerEventCreated(ctx context.Context, trackerID int, newEvent *model.Event) {
|
||||
event := model.WebhookEventEventCreated
|
||||
payloadUUID := uuid.New()
|
||||
|
@ -132,6 +145,19 @@ func DeliverTicketEvent(ctx context.Context,
|
|||
deliverTicketWebhook(ctx, ticketID, event, &payload, payloadUUID)
|
||||
}
|
||||
|
||||
func DeliverTicketDeletedEvent(ctx context.Context, ticketID int, ticket *model.Ticket) {
|
||||
event := model.WebhookEventTicketDeleted
|
||||
payloadUUID := uuid.New()
|
||||
payload := model.TicketDeletedEvent{
|
||||
UUID: payloadUUID.String(),
|
||||
Event: event,
|
||||
Date: time.Now().UTC(),
|
||||
TrackerID: ticket.TrackerID,
|
||||
TicketID: ticket.ID,
|
||||
}
|
||||
deliverTicketWebhook(ctx, ticketID, event, &payload, payloadUUID)
|
||||
}
|
||||
|
||||
func DeliverTicketEventCreated(ctx context.Context, ticketID int, newEvent *model.Event) {
|
||||
event := model.WebhookEventEventCreated
|
||||
payloadUUID := uuid.New()
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
"""Add TICKET_DELETED webhook event
|
||||
|
||||
Revision ID: e1e2e901be0c
|
||||
Revises: 7c117bc9e99b
|
||||
Create Date: 2022-05-03 11:47:11.143292
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'e1e2e901be0c'
|
||||
down_revision = '7c117bc9e99b'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.execute("""
|
||||
ALTER TYPE tracker_webhook_event ADD VALUE 'TICKET_DELETED';
|
||||
ALTER TYPE ticket_webhook_event ADD VALUE 'TICKET_DELETED';
|
||||
""")
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.execute("""
|
||||
ALTER TYPE tracker_webhook_event RENAME TO tracker_webhook_event_old;
|
||||
CREATE TYPE tracker_webhook_event AS ENUM (
|
||||
'TRACKER_UPDATE',
|
||||
'TRACKER_DELETED',
|
||||
'TICKET_CREATED',
|
||||
'TICKET_UPDATE',
|
||||
'LABEL_CREATED',
|
||||
'LABEL_UPDATE',
|
||||
'LABEL_DELETED',
|
||||
'EVENT_CREATED'
|
||||
);
|
||||
ALTER TABLE gql_tracker_wh_sub ALTER COLUMN events TYPE varchar[];
|
||||
ALTER TABLE gql_tracker_wh_sub ALTER COLUMN events TYPE tracker_webhook_event[] USING events::tracker_webhook_event[];
|
||||
ALTER TABLE gql_tracker_wh_delivery ALTER COLUMN event TYPE varchar;
|
||||
ALTER TABLE gql_tracker_wh_delivery ALTER COLUMN event TYPE tracker_webhook_event USING event::tracker_webhook_event;
|
||||
DROP TYPE tracker_webhook_event_old;
|
||||
|
||||
ALTER TYPE ticket_webhook_event RENAME TO ticket_webhook_event_old;
|
||||
CREATE TYPE ticket_webhook_event AS ENUM (
|
||||
'TICKET_UPDATE',
|
||||
'EVENT_CREATED'
|
||||
);
|
||||
ALTER TABLE gql_ticket_wh_sub ALTER COLUMN events TYPE varchar[];
|
||||
ALTER TABLE gql_ticket_wh_sub ALTER COLUMN events TYPE ticket_webhook_event[] USING events::ticket_webhook_event[];
|
||||
ALTER TABLE gql_ticket_wh_delivery ALTER COLUMN event TYPE varchar;
|
||||
ALTER TABLE gql_ticket_wh_delivery ALTER COLUMN event TYPE ticket_webhook_event USING event::ticket_webhook_event;
|
||||
DROP TYPE ticket_webhook_event_old;
|
||||
""")
|
|
@ -3,6 +3,7 @@ from datetime import datetime
|
|||
from flask import Blueprint, current_app, render_template, request, abort, redirect
|
||||
from srht.config import cfg
|
||||
from srht.database import db
|
||||
from srht.flask import session
|
||||
from srht.graphql import exec_gql
|
||||
from srht.oauth import current_user, loginrequired
|
||||
from srht.validation import Validation
|
||||
|
@ -16,7 +17,7 @@ from todosrht.types import Event, EventType, Label, TicketLabel
|
|||
from todosrht.types import TicketAccess, TicketResolution, ParticipantType
|
||||
from todosrht.types import TicketComment, TicketAuthenticity
|
||||
from todosrht.types import TicketSubscription, User, Participant
|
||||
from todosrht.urls import ticket_url
|
||||
from todosrht.urls import tracker_url, ticket_url
|
||||
from urllib.parse import quote
|
||||
|
||||
|
||||
|
@ -348,6 +349,39 @@ def ticket_edit_POST(owner, name, ticket_id):
|
|||
|
||||
return redirect(ticket_url(ticket))
|
||||
|
||||
@ticket.route("/<owner>/<name>/<int:ticket_id>/delete")
|
||||
@loginrequired
|
||||
def ticket_delete_GET(owner, name, ticket_id):
|
||||
tracker, _ = get_tracker(owner, name)
|
||||
if not tracker:
|
||||
abort(404)
|
||||
ticket, access = get_ticket(tracker, ticket_id)
|
||||
if not ticket:
|
||||
abort(404)
|
||||
if not TicketAccess.edit in access:
|
||||
abort(401)
|
||||
|
||||
return render_template("ticket-delete.html",
|
||||
view="delete", tracker=tracker, ticket=ticket, access=access)
|
||||
|
||||
@ticket.route("/<owner>/<name>/<int:ticket_id>/delete", methods=["POST"])
|
||||
@loginrequired
|
||||
def ticket_delete_POST(owner, name, ticket_id):
|
||||
tracker, _ = get_tracker(owner, name)
|
||||
if not tracker:
|
||||
abort(404)
|
||||
|
||||
exec_gql(current_app.site, """
|
||||
mutation DeleteTicket($trackerId: Int!, $ticketId: Int!) {
|
||||
deleteTicket(trackerId: $trackerId, ticketId: $ticketId) {
|
||||
id
|
||||
}
|
||||
}
|
||||
""", trackerId=tracker.id, ticketId=ticket_id)
|
||||
|
||||
session["notice"] = f"{owner}/{name}#{ticket_id} was deleted."
|
||||
return redirect(tracker_url(tracker))
|
||||
|
||||
@ticket.route("/<owner>/<name>/<int:ticket_id>/add_label", methods=["POST"])
|
||||
@loginrequired
|
||||
def ticket_add_label(owner, name, ticket_id):
|
||||
|
|
|
@ -132,6 +132,7 @@ def tracker_GET(owner, name):
|
|||
kwargs = {
|
||||
"title": request.args.get("title"),
|
||||
"description": request.args.get("description"),
|
||||
"notice": session.pop("notice", None),
|
||||
}
|
||||
|
||||
return return_tracker(tracker, access, **kwargs)
|
||||
|
|
|
@ -37,6 +37,7 @@ class TodoApp(SrhtFlask):
|
|||
self.add_template_filter(urls.participant_url)
|
||||
self.add_template_filter(urls.ticket_assign_url)
|
||||
self.add_template_filter(urls.ticket_edit_url)
|
||||
self.add_template_filter(urls.ticket_delete_url)
|
||||
self.add_template_filter(urls.ticket_unassign_url)
|
||||
self.add_template_filter(urls.ticket_url)
|
||||
self.add_template_filter(urls.tracker_labels_url)
|
||||
|
|
|
@ -29,6 +29,10 @@
|
|||
<a href="{{ ticket|ticket_edit_url }}"
|
||||
class="nav-link active">edit</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="{{ ticket|ticket_delete_url }}"
|
||||
class="nav-link">delete</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
{% extends "layout.html" %}
|
||||
{% block title %}
|
||||
<title>
|
||||
Delete {{ ticket.ref() }}: {{ ticket.title }}
|
||||
—
|
||||
{{ cfg("sr.ht", "site-name") }} todo
|
||||
</title>
|
||||
{% endblock %}
|
||||
{% block body %}
|
||||
<div class="container">
|
||||
<h2 class="ticket-title">
|
||||
<div>
|
||||
<a href="{{ tracker.owner|user_url }}">{{ tracker.owner }}</a>/<wbr
|
||||
><a href="{{ tracker|tracker_url }}">{{ tracker.name }}</a><wbr
|
||||
>#{{ ticket.scoped_id }}<spanclass="d-none d-md-inline">: </span>
|
||||
</div>
|
||||
<div id="title-field">
|
||||
{{ticket.title}}
|
||||
</div>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="header-tabbed">
|
||||
<div class="container">
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<a href="{{ ticket|ticket_url }}" class="nav-link">view</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="{{ ticket|ticket_edit_url }}"
|
||||
class="nav-link">edit</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="{{ ticket|ticket_delete_url }}"
|
||||
class="nav-link active">delete</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p>
|
||||
This will permanently delete this ticket and all of its comments. This
|
||||
cannot be undone.
|
||||
</p>
|
||||
<form method="POST">
|
||||
{{csrf_token()}}
|
||||
<button type="submit" class="btn btn-danger">
|
||||
Proceed and delete {{icon("caret-right")}}
|
||||
</button>
|
||||
<a
|
||||
href="/{{ tracker.owner }}/{{ tracker.name }}"
|
||||
class="btn btn-default"
|
||||
>Nevermind</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -43,6 +43,10 @@
|
|||
<a href="{{ ticket|ticket_edit_url }}"
|
||||
class="nav-link">edit</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="{{ ticket|ticket_delete_url }}"
|
||||
class="nav-link">delete</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="flex-grow-1 d-none d-md-block"></li>
|
||||
<li class="nav-item">
|
||||
|
|
|
@ -193,6 +193,11 @@
|
|||
An import operation is currently in progress.
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if notice %}
|
||||
<div class="alert alert-success">
|
||||
{{ notice }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if TicketAccess.browse not in access and TicketAccess.submit in access %}
|
||||
<div class="alert alert-warning">
|
||||
You do not have permission to view tickets on this tracker unless you
|
||||
|
|
|
@ -34,6 +34,12 @@ def ticket_edit_url(ticket):
|
|||
name=ticket.tracker.name,
|
||||
ticket_id=ticket.scoped_id)
|
||||
|
||||
def ticket_delete_url(ticket):
|
||||
return url_for("ticket.ticket_delete_GET",
|
||||
owner=ticket.tracker.owner.canonical_name,
|
||||
name=ticket.tracker.name,
|
||||
ticket_id=ticket.scoped_id)
|
||||
|
||||
def ticket_assign_url(ticket):
|
||||
return url_for("ticket.ticket_assign",
|
||||
owner=ticket.tracker.owner.canonical_name,
|
||||
|
|
Loading…
Reference in New Issue