Implement initial ticket detail page
This commit is contained in:
parent
2c6b094d49
commit
09d66b26d3
|
@ -1 +1,11 @@
|
|||
@import "base";
|
||||
|
||||
.ellipsis {
|
||||
overflow-x: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
select {
|
||||
padding: 0.1rem;
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ from srht.config import cfg, cfgi, load_config
|
|||
load_config("todo")
|
||||
from srht.database import DbSession
|
||||
db = DbSession(cfg("sr.ht", "connection-string"))
|
||||
from todosrht.types import User
|
||||
from todosrht.types import User, TicketAccess, TicketStatus, TicketResolution
|
||||
db.init()
|
||||
|
||||
from srht.flask import SrhtFlask
|
||||
|
@ -43,13 +43,19 @@ app.register_blueprint(tracker)
|
|||
meta_sr_ht = cfg("network", "meta")
|
||||
meta_client_id = cfg("meta.sr.ht", "oauth-client-id")
|
||||
|
||||
def tracker_name(tracker):
|
||||
def tracker_name(tracker, full=False):
|
||||
split = tracker.name.split("/")
|
||||
user = "~" + tracker.owner.username
|
||||
if full:
|
||||
return Markup(
|
||||
"/".join([
|
||||
"<a href='/{}/{}'>{}</a>".format(user, "/".join(split[:i + 1]), p)
|
||||
for i, p in enumerate(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)
|
||||
|
@ -57,11 +63,29 @@ def tracker_name(tracker):
|
|||
]) + "/" + name
|
||||
)
|
||||
|
||||
def render_status(ticket, access):
|
||||
if TicketAccess.edit in access:
|
||||
return Markup(
|
||||
"<select name='status'>" +
|
||||
"".join([
|
||||
"<option value='{0}' {1}>{0}</option>".format(s.name,
|
||||
"selected" if ticket.status == s else "")
|
||||
for s in TicketStatus
|
||||
]) +
|
||||
"</select>"
|
||||
)
|
||||
else:
|
||||
return "<span>{}</span>".format(ticket.status.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,
|
||||
"format_tracker_name": tracker_name
|
||||
"format_tracker_name": tracker_name,
|
||||
"render_status": render_status,
|
||||
"TicketAccess": TicketAccess,
|
||||
"TicketStatus": TicketStatus,
|
||||
"TicketResolution": TicketResolution
|
||||
}
|
||||
|
|
|
@ -4,7 +4,8 @@ from flask import Blueprint, render_template, request, url_for, abort, redirect
|
|||
from flask import session
|
||||
from flask_login import current_user
|
||||
from todosrht.decorators import loginrequired
|
||||
from todosrht.types import Tracker, User, Ticket, TicketStatus
|
||||
from todosrht.types import Tracker, User, Ticket, TicketStatus, TicketAccess
|
||||
from todosrht.types import TicketComment
|
||||
from srht.validation import Validation
|
||||
from srht.database import db
|
||||
|
||||
|
@ -131,20 +132,20 @@ def tracker_configure_GET(owner, name):
|
|||
|
||||
@tracker.route("/<owner>/<path:name>/submit", methods=["POST"])
|
||||
@loginrequired
|
||||
def tracker_submit_GET(owner, name):
|
||||
def tracker_submit_POST(owner, name):
|
||||
tracker = get_tracker(owner, name)
|
||||
if not tracker:
|
||||
abort(404)
|
||||
|
||||
valid = Validation(request)
|
||||
title = valid.require("title", friendly_name="Title")
|
||||
desc = valid.require("description", friendly_name="Description")
|
||||
desc = valid.optional("description")
|
||||
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) < 2048,
|
||||
valid.expect(not desc or len(desc) < 16384,
|
||||
"Description must be no more than 16384 characters.",
|
||||
field="description")
|
||||
|
||||
|
@ -171,6 +172,67 @@ def tracker_submit_GET(owner, name):
|
|||
name=name,
|
||||
ticket_id=ticket.id))
|
||||
|
||||
def get_access(tracker, ticket):
|
||||
# TODO: flesh out
|
||||
if current_user and current_user.id == tracker.owner_id:
|
||||
return TicketAccess.all
|
||||
elif current_user and current_user.id == ticket.submitter_id:
|
||||
return ticket.submitter_perms or tracker.default_submitter_perms
|
||||
elif current_user:
|
||||
return ticket.user_perms or tracker.default_user_perms
|
||||
return ticket.anonymous_perms or tracker.default_anonymous_perms
|
||||
|
||||
@tracker.route("/<owner>/<path:name>/<int:ticket_id>")
|
||||
def ticket_GET(owner, name, ticket_id):
|
||||
pass
|
||||
tracker = get_tracker(owner, name)
|
||||
if not tracker:
|
||||
abort(404)
|
||||
ticket = Ticket.query.get(ticket_id)
|
||||
if not ticket:
|
||||
abort(404)
|
||||
access = get_access(tracker, ticket)
|
||||
if not TicketAccess.browse in access:
|
||||
abort(404)
|
||||
return render_template("ticket.html",
|
||||
tracker=tracker,
|
||||
ticket=ticket,
|
||||
access=access)
|
||||
|
||||
@tracker.route("/<owner>/<path:name>/<int:ticket_id>/comment", methods=["POST"])
|
||||
@loginrequired
|
||||
def ticket_comment_POST(owner, name, ticket_id):
|
||||
tracker = get_tracker(owner, name)
|
||||
if not tracker:
|
||||
abort(404)
|
||||
ticket = Ticket.query.get(ticket_id)
|
||||
if not ticket:
|
||||
abort(404)
|
||||
access = get_access(tracker, ticket)
|
||||
if not TicketAccess.browse in access:
|
||||
abort(404)
|
||||
|
||||
valid = Validation(request)
|
||||
text = valid.require("comment", friendly_name="Comment")
|
||||
|
||||
valid.expect(not text or 3 < len(text) < 16384,
|
||||
"Comment must be between 3 and 16384 characters.")
|
||||
|
||||
if not valid.ok:
|
||||
return render_template("ticket.html",
|
||||
tracker=tracker,
|
||||
ticket=ticket,
|
||||
access=access,
|
||||
**valid.kwargs)
|
||||
|
||||
comment = TicketComment()
|
||||
comment.text = text
|
||||
# TODO: anonymous comments (when configured appropriately)
|
||||
comment.submitter_id = current_user.id
|
||||
comment.ticket_id = ticket.id
|
||||
db.session.add(comment)
|
||||
db.session.commit()
|
||||
|
||||
return redirect(url_for(".ticket_GET",
|
||||
owner="~" + tracker.owner.username,
|
||||
name=tracker.name,
|
||||
ticket_id=ticket.id) + "#comment-" + str(comment.id))
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
{% extends "layout.html" %}
|
||||
{% block body %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h2>
|
||||
{{ format_tracker_name(tracker, full=True) }}/#{{ticket.id}}:
|
||||
{{ticket.title}}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<form class="col-md-6">
|
||||
{% if ticket.description %}
|
||||
<h3>Description</h3>
|
||||
{{ ticket.description | md }}
|
||||
{% endif %}
|
||||
{% if TicketAccess.edit in access %}
|
||||
<a href="#" class="btn btn-default pull-right">Edit</a>
|
||||
{% endif %}
|
||||
<h3>
|
||||
Details
|
||||
</h3>
|
||||
<dl class="row">
|
||||
<dt class="col-md-3">Status</dt>
|
||||
<dd class="col-md-9">
|
||||
<span class="text-danger">{{ ticket.status.name.upper() }}</span>
|
||||
</dd>
|
||||
<dt class="col-md-3">Submitter</dt>
|
||||
<dd class="col-md-9"><a href="#">~{{ ticket.submitter.username }}</a></dd>
|
||||
<dt class="col-md-3">Submitted</dt>
|
||||
<dd class="col-md-9">{{ ticket.created | date }}</dd>
|
||||
<dt class="col-md-3">Updated</dt>
|
||||
<dd class="col-md-9">{{ ticket.updated | date }}</dd>
|
||||
<dt class="col-md-3">User Agent</dt>
|
||||
<dd class="col-md-9 ellipsis" title="{{ ticket.user_agent }}">
|
||||
{{ ticket.user_agent }}
|
||||
</dd>
|
||||
</dl>
|
||||
</form>
|
||||
<div class="col-md-6">
|
||||
{% for comment in ticket.comments %}
|
||||
<h4>
|
||||
<a href="#">~{{ comment.submitter.username }}</a>
|
||||
<span class="pull-right">
|
||||
<small><a href="#">edit</a></small>
|
||||
<small><a href="#">delete</a></small>
|
||||
<small>{{ comment.created | date }}</small>
|
||||
</span>
|
||||
</h4>
|
||||
{{ comment.text | md }}
|
||||
{% endfor %}
|
||||
{% if TicketAccess.comment in access %}
|
||||
<h3 style="margin-top: 1rem">Add comment</h3>
|
||||
<form method="POST" action="{{
|
||||
url_for(".ticket_comment_POST",
|
||||
owner="~" + tracker.owner.username,
|
||||
name=tracker.name,
|
||||
ticket_id=ticket.id
|
||||
)
|
||||
}}">
|
||||
<div class="form-group {{ valid.cls("comment") }}">
|
||||
<textarea
|
||||
class="form-control"
|
||||
id="comment"
|
||||
name="comment"
|
||||
placeholder="Markdown supported"
|
||||
maxlength="16384"
|
||||
rows="5">{{ comment or "" }}</textarea>
|
||||
{{valid.summary("comment")}}
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-default"
|
||||
>Comment</button>
|
||||
{% if TicketAccess.edit in access %}
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-default"
|
||||
name="resolve"
|
||||
>Resolve</button>
|
||||
<select name="resolution">
|
||||
{% for r in TicketResolution %}
|
||||
{% if r.name != "unresolved" %}
|
||||
<option value="{{ r.name }}">{{ r.name }}</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% endif %}
|
||||
</form>
|
||||
{% else %}
|
||||
{% if not ticket.comments %}
|
||||
<p>It's a bit quiet in here.</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -11,7 +11,7 @@
|
|||
{{ tracker.description | md }}
|
||||
<h3 style="margin-top: 1rem">Submit ticket</h3>
|
||||
<form method="POST" action="{{
|
||||
url_for(".tracker_submit_GET",
|
||||
url_for(".tracker_submit_POST",
|
||||
owner="~" + tracker.owner.username,
|
||||
name=tracker.name
|
||||
)
|
||||
|
@ -76,6 +76,7 @@
|
|||
href="#">closed tickets</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% if len(tickets) %}
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
|
@ -90,18 +91,21 @@
|
|||
<tr>
|
||||
<td><a href="{{url_for(".ticket_GET",
|
||||
owner="~" + tracker.owner.username,
|
||||
name=name,
|
||||
name=tracker.name,
|
||||
ticket_id=ticket.id)}}">#{{ticket.id}}</a></td>
|
||||
<td>{{ ticket.title }}</td>
|
||||
<td>{{ ticket.updated | date }}</td>
|
||||
<td><a href="{{url_for(".ticket_GET",
|
||||
owner="~" + tracker.owner.username,
|
||||
name=name,
|
||||
name=tracker.name,
|
||||
ticket_id=ticket.id)}}">{{ ticket.submitter.username }}</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="alert alert-info">No tickets found for this search criteria.</div>
|
||||
{% endif %}
|
||||
{% if total_pages > 1 %}
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
|
|
|
@ -5,3 +5,4 @@ from .ticketstatus import TicketStatus, TicketResolution
|
|||
from .tracker import Tracker
|
||||
from .ticket import Ticket
|
||||
from .ticketsubscription import TicketSubscription
|
||||
from .ticketcomment import TicketComment
|
||||
|
|
|
@ -20,7 +20,7 @@ class Ticket(Base):
|
|||
submitter = sa.orm.relationship("User", backref=sa.orm.backref("submitted"))
|
||||
|
||||
title = sa.Column(sa.Unicode(2048), nullable=False)
|
||||
description = sa.Column(sa.Unicode(16384), nullable=False)
|
||||
description = sa.Column(sa.Unicode(16384))
|
||||
user_agent = sa.Column(sa.Unicode(2048))
|
||||
|
||||
status = sa.Column(FlagType(TicketStatus),
|
||||
|
|
|
@ -7,3 +7,4 @@ class TicketAccess(IntFlag):
|
|||
comment = 4
|
||||
edit = 8
|
||||
triage = 16
|
||||
all = browse | submit | comment | edit | triage
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
import sqlalchemy as sa
|
||||
from srht.database import Base
|
||||
from todosrht.types import FlagType, TicketAccess, TicketStatus, TicketResolution
|
||||
|
||||
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)
|
||||
|
||||
submitter_id = sa.Column(sa.Integer, sa.ForeignKey("user.id"), nullable=False)
|
||||
submitter = sa.orm.relationship("User")
|
||||
|
||||
ticket_id = sa.Column(sa.Integer, sa.ForeignKey("ticket.id"), nullable=False)
|
||||
ticket = sa.orm.relationship("Ticket", backref=sa.orm.backref("comments"))
|
||||
|
||||
text = sa.Column(sa.Unicode(16384))
|
|
@ -6,7 +6,6 @@ class TicketStatus(IntFlag):
|
|||
in_progress = 2
|
||||
pending = 4
|
||||
resolved = 8
|
||||
shipped = 16
|
||||
|
||||
class TicketResolution(IntFlag):
|
||||
unresolved = 0
|
||||
|
|
|
@ -18,6 +18,8 @@ class Tracker(Base):
|
|||
description = sa.Column(sa.Unicode(8192))
|
||||
"""Markdown"""
|
||||
|
||||
min_desc_length = sa.Column(sa.Integer, nullable=False, default=0)
|
||||
|
||||
enable_ticket_status = sa.Column(FlagType(TicketStatus),
|
||||
nullable=False,
|
||||
default=TicketStatus.resolved)
|
||||
|
|
Loading…
Reference in New Issue