diff --git a/mo/web/org_round.py b/mo/web/org_round.py index 979093b19910c10f96cda26db941ac362ffd935c..4ac98010b07a3e1d8e43c550a4c416fd05c3f410 100644 --- a/mo/web/org_round.py +++ b/mo/web/org_round.py @@ -1,6 +1,9 @@ from flask import render_template, g, redirect, url_for, flash, request import locale from flask_wtf.form import FlaskForm +import bleach +from bleach.sanitizer import ALLOWED_TAGS +import markdown from sqlalchemy import func from sqlalchemy.orm import joinedload from sqlalchemy.sql.functions import coalesce @@ -428,6 +431,7 @@ class RoundEditForm(FlaskForm): "Hranice bodů pro úspěšné řešitele", validators=[validators.Optional()], description="Řešitelé s alespoň tolika body budou označeni za úspěšné řešitele, prázdná hodnota = žádné neoznačovat", ) + has_messages = wtforms.BooleanField("Zprávičky pro účastníky (aktivuje možnost vytvářet novinky zobrazované účastníkům)") submit = wtforms.SubmitField('Uložit') def validate_state(self, field): @@ -489,3 +493,78 @@ def org_task_statement(id: int): raise werkzeug.exceptions.Forbidden() return mo.web.util.send_task_statement(round) + + +class MessageAddForm(FlaskForm): + title = wtforms.StringField('Nadpis', validators=[validators.Required()]) + markdown = wtforms.TextAreaField( + 'Text novinky', description='Zprávičky lze formátovat pomocí Markdownu', + validators=[validators.Required()], + render_kw={'rows': 10}, + ) + submit = wtforms.SubmitField(label='Vložit zprávičku') + preview = wtforms.SubmitField(label='Zobrazit náhled') + + +class MessageRemoveForm(FlaskForm): + message_id = wtforms.IntegerField(validators=[validators.Required()]) + message_remove = wtforms.SubmitField() + + +@app.route('/org/contest/r/<int:id>/messages/', methods=('GET', 'POST')) +def org_round_messages(id: int): + sess = db.get_session() + round, _, rr = get_round_rr(id, None, True) + + if not round.has_messages: + flash('Toto kolo nemá aktivní zprávičky pro účastníky, aktivujte je v nastavení kola', 'warning') + return redirect(url_for('org_round', id=id)) + + messages = sess.query(db.Message).filter_by(round_id=id).order_by(db.Message.created_at).all() + + add_form: Optional[MessageAddForm] = None + remove_form: Optional[MessageRemoveForm] = None + preview: Optional[db.Message] = None + if rr.have_right(Right.manage_round): + add_form = MessageAddForm() + remove_form = MessageRemoveForm() + + if remove_form.validate_on_submit() and remove_form.message_remove.data: + msg = sess.query(db.Message).get(remove_form.message_id.data) + if not msg or msg.round_id != id: + raise werkzeug.exceptions.NotFound() + sess.delete(msg) + sess.commit() + app.logger.info(f"Zprávička pro kolo {id} odstraněna: {db.row2dict(msg)}") + + flash('Zprávička odstraněna', 'success') + return redirect(url_for('org_round_messages', id=id)) + + if add_form.validate_on_submit(): + msg = db.Message( + round_id=id, + created_by=g.user.user_id, + created_at=mo.now, + ) + add_form.populate_obj(msg) + msg.html = bleach.clean( + markdown.markdown(msg.markdown), + tags=ALLOWED_TAGS+['p'] + ) + + if add_form.preview.data: + preview = msg + elif add_form.submit.data: + sess.add(msg) + sess.commit() + app.logger.info(f"Vložena nová zprávička pro kolo {id}: {db.row2dict(msg)}") + + flash('Zprávička úspěšně vložena', 'success') + return redirect(url_for('org_round_messages', id=id)) + + return render_template( + 'org_round_messages.html', + round=round, rr=rr, messages=messages, + add_form=add_form, remove_form=remove_form, + preview=preview, + ) diff --git a/mo/web/templates/org_round.html b/mo/web/templates/org_round.html index 6f4a2a560f656e64f0036bb4ca0582659fe636ab..6c5705dc35258985d0b6b711ac38ae08ee7e4e2e 100644 --- a/mo/web/templates/org_round.html +++ b/mo/web/templates/org_round.html @@ -58,6 +58,9 @@ {% if can_manage_round %} <a class="btn btn-default" href='{{ url_for('org_round_edit', id=round.round_id) }}'>Editovat nastavení kola</a> {% endif %} + {% if round.has_messages %} + <a class="btn btn-default" href='{{ url_for('org_round_messages', id=round.round_id) }}'>Zprávičky</a> + {% endif %} {% if g.user.is_admin %} <a class="btn btn-default" href='{{ log_url('round', round.round_id) }}'>Historie</a> {% endif %} diff --git a/mo/web/templates/org_round_messages.html b/mo/web/templates/org_round_messages.html new file mode 100644 index 0000000000000000000000000000000000000000..1be2a7367b9db1ef420ee6ad65f7a19d925270f4 --- /dev/null +++ b/mo/web/templates/org_round_messages.html @@ -0,0 +1,34 @@ +{% extends "base.html" %} +{% import "bootstrap/wtf.html" as wtf %} + +{% block title %}{{ round.name }} {{ round.round_code() }} – zprávičky{% endblock %} +{% block breadcrumbs %} +{{ contest_breadcrumbs(round=round, action='Zprávičky') }} +{% endblock %} + +{% block body %} + +{% if messages %} +{% with message_remove_form=remove_form %} + {% include "parts/messages.html" %} +{% endwith %} +{% else %}<p><i>Žádné zprávičky k vypsání.</i></p>{% endif %} + +{% if add_form %} + <h3>Nová zprávička</h3> + <div class="form-frame"> + {{ wtf.quick_form(add_form, button_map={'submit': 'primary', 'preview': 'default'}) }} + </div> + {% if preview %} + <div class="form-frame"> + <b>Náhled zprávičky:</b> + {% with messages = [preview] %} + {% include "parts/messages.html" %} + {% endwith %} + </div> + {% endif %} +{% else %} + <p><i>Nemáte právo k přidávání nových zpráviček.</i></p> +{% endif %} + +{% endblock %} diff --git a/mo/web/templates/parts/messages.html b/mo/web/templates/parts/messages.html new file mode 100644 index 0000000000000000000000000000000000000000..d90436f4af4b3bcb363f896936d3ba7ab1249099 --- /dev/null +++ b/mo/web/templates/parts/messages.html @@ -0,0 +1,14 @@ +{% for msg in messages %} +<div class="message"> + <span class="msg-title">{{ msg.title }}</span> + <span class="msg-date">{{ msg.created_at|time_and_timedelta }}</span> + <div class="msg-text">{{ msg.html|safe }}</div> + {% if message_remove_form %} + <form method="POST" onsubmit="return confirm('Opravdu nenávratně smazat?');" style="float: right; margin-top: -20px;"> + {{ message_remove_form.csrf_token }} + <input type="hidden" name="message_id" value="{{ msg.message_id }}"> + <input type="submit" name="message_remove" class="btn btn-xs btn-danger" value="Smazat"> + </form> + {% endif %} +</div> +{% endfor %} diff --git a/static/mo.css b/static/mo.css index 9826de39d25461d82bfbf4df2e5c60e7bfc90c22..dec13006ac04a3a4b4393624fa95db3c9aeca644 100644 --- a/static/mo.css +++ b/static/mo.css @@ -382,3 +382,24 @@ div.alert + div.alert { .collapsible input[type="checkbox"].toggle:checked ~ .collapsible-inner { max-height: 100vh; } + +div.message { + padding: 5px 10px; + margin-bottom: 5px; + border-radius: 5px; + border: solid 1px #f8d99b; + color:#46381f; + background-color:#fff8d5; +} +div.message.new { + background-color: #ffdede; + border: solid 1px #ff5e5e; +} +div.message .msg-title { + font-weight: bold; +} +div.message .msg-date { + float: right; + font-style: italic; + color: #777; +}