From db19517b912d1a11ca22b153e2084504041151ea Mon Sep 17 00:00:00 2001 From: Fox Wilson Date: Mon, 30 Nov 2015 23:05:17 -0500 Subject: [PATCH] finish trouble tickets, notification infrastructure, challenge filtering --- app.py | 71 +++++++++++++++++++++++++++++- ctferror.py | 2 + modules/admin.py | 46 ++++++++++++++++++- modules/api.py | 14 +++++- templates/admin/base.html | 3 +- templates/admin/dashboard.html | 8 ++++ templates/admin/ticket_detail.html | 22 +++++++++ templates/admin/tickets.html | 27 ++++++++++++ templates/base.html | 21 ++++++++- templates/challenges.html | 29 ++++++++++-- templates/open_ticket.html | 24 ++++++++++ templates/ticket_detail.html | 22 +++++++++ templates/tickets.html | 26 +++++++++++ utils/decorators.py | 8 ++++ utils/notification.py | 2 + 15 files changed, 315 insertions(+), 10 deletions(-) create mode 100644 templates/admin/ticket_detail.html create mode 100644 templates/admin/tickets.html create mode 100644 templates/open_ticket.html create mode 100644 templates/ticket_detail.html create mode 100644 templates/tickets.html create mode 100644 utils/notification.py diff --git a/app.py b/app.py index 5c8d796..500946e 100644 --- a/app.py +++ b/app.py @@ -1,7 +1,7 @@ from flask import Flask, render_template, session, redirect, url_for, request, g, flash app = Flask(__name__) -from database import Team, TeamAccess, Challenge, ChallengeSolve, ChallengeFailure, ScoreAdjustment, db +from database import Team, TeamAccess, Challenge, ChallengeSolve, ChallengeFailure, ScoreAdjustment, TroubleTicket, TicketComment, Notification, db from datetime import datetime from peewee import fn @@ -29,8 +29,10 @@ def scoreboard_variables(): if "team_id" in session: var["logged_in"] = True var["team"] = g.team + var["notifications"] = Notification.select().where(Notification.team == g.team) else: var["logged_in"] = False + var["notifications"] = [] return var @@ -171,7 +173,8 @@ def dashboard(): def challenges(): chals = Challenge.select().order_by(Challenge.points) solved = Challenge.select().join(ChallengeSolve).where(ChallengeSolve.team == g.team) - return render_template("challenges.html", challenges=chals, solved=solved) + categories = sorted(list({chal.category for chal in chals})) + return render_template("challenges.html", challenges=chals, solved=solved, categories=categories) @app.route('/submit//', methods=["POST"]) @decorators.competition_running_required @@ -184,6 +187,70 @@ def submit(challenge): flash(message) return redirect(url_for('challenges')) +# Trouble tickets + +@app.route('/tickets/') +@decorators.login_required +def team_tickets(): + return render_template("tickets.html", tickets=list(g.team.tickets)) + +@app.route('/tickets/new/', methods=["GET", "POST"]) +@decorators.login_required +def open_ticket(): + if request.method == "GET": + return render_template("open_ticket.html") + elif request.method == "POST": + summary = request.form["summary"] + description = request.form["description"] + opened_at = datetime.now() + ticket = TroubleTicket.create(team=g.team, summary=summary, description=description, opened_at=opened_at) + flash("Ticket #{} opened.".format(ticket.id)) + return redirect(url_for("team_ticket_detail", ticket=ticket.id)) + +@app.route('/tickets//') +@decorators.login_required +def team_ticket_detail(ticket): + try: + ticket = TroubleTicket.get(TroubleTicket.id == ticket) + except TroubleTicket.DoesNotExist: + flash("Couldn't find ticket #{}.".format(ticket)) + return redirect(url_for("team_tickets")) + + if ticket.team != g.team: + flash("That's not your ticket.") + return redirect(url_for("team_tickets")) + + comments = TicketComment.select().where(TicketComment.ticket == ticket) + return render_template("ticket_detail.html", ticket=ticket, comments=comments) + +@app.route('/tickets//comment/', methods=["POST"]) +def team_ticket_comment(ticket): + try: + ticket = TroubleTicket.get(TroubleTicket.id == ticket) + except TroubleTicket.DoesNotExist: + flash("Couldn't find ticket #{}.".format(ticket)) + return redirect(url_for("team_tickets")) + + if ticket.team != g.team: + flash("That's not your ticket.") + return redirect(url_for("team_tickets")) + + if request.form["comment"]: + TicketComment.create(ticket=ticket, comment_by=g.team.name, comment=request.form["comment"], time=datetime.now()) + flash("Comment added.") + + if ticket.active and "resolved" in request.form: + ticket.active = False + ticket.save() + flash("Ticket closed.") + + elif not ticket.active and "resolved" not in request.form: + ticket.active = True + ticket.save() + flash("Ticket re-opened.") + + return redirect(url_for("team_ticket_detail", ticket=ticket.id)) + # Manage Peewee database sessions and Redis @app.before_request diff --git a/ctferror.py b/ctferror.py index d88a2bc..89c1bfe 100644 --- a/ctferror.py +++ b/ctferror.py @@ -6,3 +6,5 @@ FLAG_INCORRECT = (1003, "Incorrect flag.") CAPTCHA_NOT_COMPLETED = (2001, "Please complete the CAPTCHA.") CAPTCHA_INVALID = (2002, "Invalid CAPTCHA response.") + +NOTIFICATION_NOT_YOURS = (3001, "You cannot dismiss notifications that do not belong to you.") diff --git a/modules/admin.py b/modules/admin.py index b9e3e3c..eeeca83 100644 --- a/modules/admin.py +++ b/modules/admin.py @@ -1,8 +1,11 @@ from flask import Blueprint, render_template, request, session, redirect, url_for, flash -from database import AdminUser, Team, Challenge, ChallengeSolve, ChallengeFailure, ScoreAdjustment +from database import AdminUser, Team, Challenge, ChallengeSolve, ChallengeFailure, ScoreAdjustment, TroubleTicket, TicketComment, Notification import utils import utils.admin import utils.scoreboard +from utils.decorators import admin_required +from utils.notification import make_link +from datetime import datetime admin = Blueprint("admin", "admin", url_prefix="/admin") @admin.route("/") @@ -32,15 +35,54 @@ def admin_login(): return render_template("admin/login.html") @admin.route("/dashboard/") +@admin_required def admin_dashboard(): teams = Team.select() solves = ChallengeSolve.select(ChallengeSolve, Challenge).join(Challenge) adjustments = ScoreAdjustment.select() scoredata = utils.scoreboard.get_all_scores(teams, solves, adjustments) lastsolvedata = utils.scoreboard.get_last_solves(teams, solves) - return render_template("admin/dashboard.html", teams=teams, scoredata=scoredata, lastsolvedata=lastsolvedata) + tickets = list(TroubleTicket.select().where(TroubleTicket.active == True)) + return render_template("admin/dashboard.html", teams=teams, scoredata=scoredata, lastsolvedata=lastsolvedata, tickets=tickets) + +@admin.route("/tickets/") +@admin_required +def admin_tickets(): + tickets = list(TroubleTicket.select(TroubleTicket, Team).join(Team)) + return render_template("admin/tickets.html", tickets=tickets) + +@admin.route("/tickets//") +@admin_required +def admin_ticket_detail(ticket): + ticket = TroubleTicket.get(TroubleTicket.id == ticket) + comments = list(TicketComment.select().where(TicketComment.ticket == ticket)) + return render_template("admin/ticket_detail.html", ticket=ticket, comments=comments) + +@admin.route("/tickets//comment/", methods=["POST"]) +@admin_required +def admin_ticket_comment(ticket): + ticket = TroubleTicket.get(TroubleTicket.id == ticket) + if request.form["comment"]: + TicketComment.create(ticket=ticket, comment_by=session["admin"], comment=request.form["comment"], time=datetime.now()) + Notification.create(team=ticket.team, notification="A response has been added for {}.".format(make_link("ticket #{}".format(ticket.id), url_for("team_ticket_detail", ticket=ticket.id)))) + flash("Comment added.") + + if ticket.active and "resolved" in request.form: + ticket.active = False + ticket.save() + Notification.create(team=ticket.team, notification="{} has been marked resolved.".format(make_link("Ticket #{}".format(ticket.id), url_for("team_ticket_detail", ticket=ticket.id)))) + flash("Ticket closed.") + + elif not ticket.active and "resolved" not in request.form: + ticket.active = True + ticket.save() + Notification.create(team=ticket.team, notification="{} has been reopened.".format(make_link("Ticket #{}".format(ticket.id), url_for("team_ticket_detail", ticket=ticket.id)))) + flash("Ticket reopened.") + + return redirect(url_for(".admin_ticket_detail", ticket=ticket.id)) @admin.route("/team//") +@admin_required def admin_show_team(tid): team = Team.get(Team.id == tid) return render_template("admin/team.html", team=team) diff --git a/modules/api.py b/modules/api.py index 41d2a3e..07b4fcc 100644 --- a/modules/api.py +++ b/modules/api.py @@ -1,6 +1,7 @@ from flask import Blueprint, jsonify, g, request -from database import Challenge +from database import Challenge, Notification from utils import decorators, flag +from ctferror import * api = Blueprint("api", "api", url_prefix="/api") @api.route("/submit/.json", methods=["POST"]) @@ -12,3 +13,14 @@ def submit_api(challenge): code, message = flag.submit_flag(g.team, chal, flagval) return jsonify(dict(code=code, message=message)) + +@api.route("/dismiss/.json", methods=["POST"]) +@decorators.login_required +def dismiss_notification(nid): + n = Notification.get(Notification.id == nid) + if g.team != n.team: + code, message = NOTIFICATION_NOT_YOURS + + Notification.delete().where(Notification.id == nid).execute() + code, message = SUCCESS + return jsonify(dict(code=code, message=message)) diff --git a/templates/admin/base.html b/templates/admin/base.html index f7af99d..fe5e49c 100644 --- a/templates/admin/base.html +++ b/templates/admin/base.html @@ -15,11 +15,12 @@ {% if "admin" in session %}