From d89c6f798cfa5ec689ac7d3e9d7fd947fe8412e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Setni=C4=8Dka?= <setnicka@seznam.cz> Date: Sun, 21 Mar 2021 01:32:50 +0100 Subject: [PATCH] =?UTF-8?q?V=C3=BDpis=20emailov=C3=BDch=20adres=20=C3=BA?= =?UTF-8?q?=C4=8Dastn=C3=ADk=C5=AF=20pro=20snadn=C3=A9=20pos=C3=ADl=C3=A1n?= =?UTF-8?q?=C3=AD=20hromadn=C3=BDch=20email=C5=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Z tabulek účastníků kola/soutěže se lze prokliknout na vypsání všech jejich emailů, což vede na stránku se stejným filtrem, ale vypisující všechny emaily bez ohledu na stránkování. Mailto tlačítko a seznam v textarea, včetně jmen účastníků. Issue #99 --- mo/web/org_contest.py | 32 ++++++++++++++++--- mo/web/org_round.py | 31 ++++++++++++------ mo/web/templates/org_contest_list.html | 11 +++++-- mo/web/templates/org_round_list.html | 22 +++++++++---- .../parts/org_participants_emails.html | 19 +++++++++++ .../parts/org_participants_table_actions.html | 10 +++++- 6 files changed, 101 insertions(+), 24 deletions(-) create mode 100644 mo/web/templates/parts/org_participants_emails.html diff --git a/mo/web/org_contest.py b/mo/web/org_contest.py index fb69c52d..76854a79 100644 --- a/mo/web/org_contest.py +++ b/mo/web/org_contest.py @@ -9,11 +9,13 @@ from sqlalchemy.orm import joinedload, aliased from sqlalchemy.orm.query import Query from sqlalchemy.dialects.postgresql import insert as pgsql_insert from typing import Any, List, Tuple, Optional, Sequence, Dict +import urllib.parse import werkzeug.exceptions import wtforms import mo from mo.csv import FileFormat +import mo.config as config import mo.db as db from mo.imports import ImportType, create_import import mo.jobs.submit @@ -435,9 +437,11 @@ def org_contest_import(id: int): @app.route('/org/contest/c/<int:id>/ucastnici', methods=('GET', 'POST')) @app.route('/org/contest/c/<int:id>/site/<int:site_id>/ucastnici', methods=('GET', 'POST')) +@app.route('/org/contest/c/<int:id>/ucastnici/emails', endpoint="org_contest_list_emails") +@app.route('/org/contest/c/<int:id>/site/<int:site_id>/ucastnici/emails', endpoint="org_contest_list_emails") def org_contest_list(id: int, site_id: Optional[int] = None): contest, master_contest, site, rr = get_contest_site_rr(id, site_id, Right.view_contestants) - can_edit = rr.have_right(Right.manage_contest) + can_edit = rr.have_right(Right.manage_contest) and request.endpoint != 'org_contest_list_emails' format = request.args.get('format', "") filter = ParticipantsFilterForm(request.args) @@ -458,14 +462,21 @@ def org_contest_list(id: int, site_id: Optional[int] = None): return redirect(request.url) if format == "": - # (count, query) = filter.apply_limits(query, pagesize=50) - count = db.get_count(query) + table = None + emails = None + mailto_link = None + if request.endpoint == 'org_contest_list_emails': + (emails, mailto_link) = get_contestant_emails(query) + count = len(emails) + else: + # (count, query) = filter.apply_limits(query, pagesize=50) + count = db.get_count(query) + table = make_contestant_table(query, add_checkbox=can_edit) - table = make_contestant_table(query, add_checkbox=can_edit) return render_template( 'org_contest_list.html', contest=contest, site=site, - table=table, + table=table, emails=emails, mailto_link=mailto_link, filter=filter, count=count, action_form=action_form, ) else: @@ -570,6 +581,17 @@ def make_contestant_table(query: Query, add_checkbox: bool = False, add_contest_ ) +def get_contestant_emails(query: Query, mailto_subject: str = '[OSMO] Zpráva pro účastníky') -> Tuple[List[str], str]: + users = [pion.user for (pion, _, _) in query.all()] + emails = [f'{u.first_name} {u.last_name} <{u.email}>' for u in users] + mailto_link = ( + 'mailto:' + urllib.parse.quote(config.MAIL_CONTACT, safe='@') + + '?subject=' + urllib.parse.quote(mailto_subject) + + '&bcc=' + ','.join([urllib.parse.quote(email, safe='@') for email in emails]) + ) + return (emails, mailto_link) + + @dataclass class SolutionContext: contest: db.Contest diff --git a/mo/web/org_round.py b/mo/web/org_round.py index 766193be..979093b1 100644 --- a/mo/web/org_round.py +++ b/mo/web/org_round.py @@ -16,7 +16,7 @@ import mo.imports from mo.rights import Right, RoundRights import mo.util from mo.web import app -from mo.web.org_contest import ParticipantsActionForm, ParticipantsFilterForm, get_contestants_query, make_contestant_table, \ +from mo.web.org_contest import ParticipantsActionForm, ParticipantsFilterForm, get_contestant_emails, get_contestants_query, make_contestant_table, \ generic_import, generic_batch_download, generic_batch_upload, generic_batch_points @@ -338,8 +338,10 @@ def org_round_task_batch_points(round_id: int, task_id: int): @app.route('/org/contest/r/<int:id>/list', methods=('GET', 'POST')) +@app.route('/org/contest/r/<int:id>/list/emails', endpoint="org_round_list_emails") def org_round_list(id: int): - round, master_round, r = get_round_rr(id, Right.view_contestants, True) + round, master_round, rr = get_round_rr(id, Right.view_contestants, True) + can_edit = rr.have_right(Right.manage_round) and request.endpoint != 'org_round_list_emails' format = request.args.get('format', "") filter = ParticipantsFilterForm(request.args) @@ -352,20 +354,29 @@ def org_round_list(id: int): participation_state=filter.f_participation_state, ) - action_form = ParticipantsActionForm() - if action_form.do_action(round=master_round, query=query): - # Action happened, redirect - return redirect(request.url) + action_form = None + if can_edit: + action_form = ParticipantsActionForm() + if action_form.do_action(round=master_round, query=query): + # Action happened, redirect + return redirect(request.url) if format == "": - (count, query) = filter.apply_limits(query, pagesize=50) - # count = db.get_count(query) + table = None + emails = None + mailto_link = None + if request.endpoint == 'org_round_list_emails': + (emails, mailto_link) = get_contestant_emails(query) + count = len(emails) + else: + (count, query) = filter.apply_limits(query, pagesize=50) + # count = db.get_count(query) + table = make_contestant_table(query, add_contest_column=True, add_checkbox=True) - table = make_contestant_table(query, add_contest_column=True, add_checkbox=True) return render_template( 'org_round_list.html', round=round, - table=table, + table=table, emails=emails, mailto_link=mailto_link, filter=filter, count=count, action_form=action_form, ) else: diff --git a/mo/web/templates/org_contest_list.html b/mo/web/templates/org_contest_list.html index 3f817038..3e28dd3a 100644 --- a/mo/web/templates/org_contest_list.html +++ b/mo/web/templates/org_contest_list.html @@ -8,6 +8,8 @@ Seznam účastníků {% if site %}soutěžního místa {{ site.name }}{% else %} {% block breadcrumbs %} {{ contest_breadcrumbs(round=round, contest=contest, site=site, action="Seznam účastníků") }} {% endblock %} +{% set id = contest.contest_id %} +{% set site_id = site.place_id if site else None %} {% block body %} <div class="form-frame"> @@ -20,8 +22,10 @@ Seznam účastníků {% if site %}soutěžního místa {{ site.name }}{% else %} {{ wtf.form_field(filter.participation_state) }} <div class="btn-group"> {{ wtf.form_field(filter.submit, class='btn btn-primary') }} + {% if table %} <button class="btn btn-default" name="format" value="cs_csv" title="Stáhnout celý výsledek v CSV">↓ CSV</button> <button class="btn btn-default" name="format" value="tsv" title="Stáhnout celý výsledek v TSV">↓ TSV</button> + {% endif %} </div> {% if not site %} </div> @@ -32,6 +36,9 @@ Seznam účastníků {% if site %}soutěžního místa {{ site.name }}{% else %} </form> </div> -{% include 'parts/org_participants_table_actions.html' %} - +{% if table %} + {% include 'parts/org_participants_table_actions.html' %} +{% else %} + {% include 'parts/org_participants_emails.html' %} +{% endif %} {% endblock %} diff --git a/mo/web/templates/org_round_list.html b/mo/web/templates/org_round_list.html index 417006a9..69b4ca3f 100644 --- a/mo/web/templates/org_round_list.html +++ b/mo/web/templates/org_round_list.html @@ -5,6 +5,7 @@ {% block breadcrumbs %} {{ contest_breadcrumbs(round=round, action="Seznam účastníků") }} {% endblock %} +{% set id = round.round_id %} {% block body %} <div class="form-frame"> @@ -18,10 +19,12 @@ <div class="form-row" style="margin-top: 5px;"> <div class="btn-group"> {{ wtf.form_field(filter.submit, class='btn btn-primary') }} + {% if table %} <button class="btn btn-default" name="format" value="cs_csv" title="Stáhnout celý výsledek v CSV">↓ CSV</button> <button class="btn btn-default" name="format" value="tsv" title="Stáhnout celý výsledek v TSV">↓ TSV</button> + {% endif %} </div> - + {% if table %} <div style="float: right"> Stránka {{ filter.offset.data // filter.limit.data + 1}} z {{ (count / filter.limit.data)|round(0, 'ceil')|int }}: <div class="btn-group"> @@ -41,13 +44,20 @@ </div> {% set max = filter.offset.data + filter.limit.data if filter.offset.data + filter.limit.data < count else count %} Zobrazuji záznamy <b>{{filter.offset.data + 1}}</b> až <b>{{ max }}</b> z <b>{{count}} nalezených účastníků</b>. + {% else %} + Celkem <b>{{count|inflected('nalezený účastník', 'nalezení účastníci', 'nalezených účastníků')}}</b>. + {% endif %} </div> </form> </div> -{% include 'parts/org_participants_table_actions.html' %} - -<br> -<i>Upozornění: Můžete editovat jen účastníky soutěžící v oblastech, ke kterým máte právo.</i> - +{% if table %} + {% include 'parts/org_participants_table_actions.html' %} + {% if form_actions %} + <br> + <i>Upozornění: Můžete editovat jen účastníky soutěžící v oblastech, ke kterým máte právo.</i> + {% endif %} +{% else %} + {% include 'parts/org_participants_emails.html' %} +{% endif %} {% endblock %} diff --git a/mo/web/templates/parts/org_participants_emails.html b/mo/web/templates/parts/org_participants_emails.html new file mode 100644 index 00000000..3e9ca863 --- /dev/null +++ b/mo/web/templates/parts/org_participants_emails.html @@ -0,0 +1,19 @@ +<a class="btn btn-default pull-right" style="margin-top: 15px;" + href="{{ url_for('org_contest_list', id=id, site_id=site_id, **request.args) if contest else url_for('org_round_list', id=id, **request.args) }}"> + Zpět na tabulku účastníků +</a> + +<h3>E-mailové adresy</h3> +<div class="form-frame"> +{% if emails %} +<p>Pokud máte e-mailového klienta, který umí odkazy typu <code>mailto:</code>, tak vám následující tlačítko předvyplní nový email: +<a class="btn btn-primary" href="{{ mailto_link }}">Vytvořit email pro {{ count|inflected("adresáta", "adresáty", "adresátů") }}</a> + +<p>Emailové adresy si také můžete zkopírovat z následujícího pole. Prosím posílejte jako <b>skrytou kopii</b>, ať účastníci nevidí navzájem své emaily.</p> +<code><textarea id="emails-textarea" class="form-control" readonly style="resize: none;" onclick="this.focus(); this.select();"> +{{ emails|join('\n')|escape }}</textarea></code> +{% else %}<i>Žádné emailové adresy k vypsání.</i>{% endif %} +</div> +<script type="text/javascript"> +document.getElementById('emails-textarea').style.height = (document.getElementById('emails-textarea').scrollHeight + 5) + "px"; +</script> diff --git a/mo/web/templates/parts/org_participants_table_actions.html b/mo/web/templates/parts/org_participants_table_actions.html index dc98b929..9838f834 100644 --- a/mo/web/templates/parts/org_participants_table_actions.html +++ b/mo/web/templates/parts/org_participants_table_actions.html @@ -1,7 +1,16 @@ {% if action_form %} <form action="" method="POST" class="form form-horizontal" role="form"> +{% endif %} + {{ table.to_html() }} + <a class="btn btn-primary pull-right" + title="Zobrazí emailové adresy ve snadno zkopírovatelném formátu" + href="{{ url_for('org_contest_list_emails', id=id, site_id=site_id, **request.args) if contest else url_for('org_round_list_emails', id=id, **request.args) }}"> + Vypsat emailové adresy + </a> + +{% if action_form %} {{ action_form.csrf_token }} <h3>Provést akci</h3> <div class="form-frame"> @@ -45,7 +54,6 @@ </div> </form> {% else %} -{{ table.to_html() }} <p> <i>Nemáte právo k editaci účastníků v této oblasti.</i> </p> -- GitLab