Skip to content
Snippets Groups Projects
Commit 7905b6e5 authored by Jiří Setnička's avatar Jiří Setnička
Browse files

Výpis emailových adres účastníků pro snadné posílání hromadných emailů

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
parent 0d1838ae
No related branches found
No related tags found
No related merge requests found
This commit is part of merge request !54. Comments created here will be created in the context of that merge request.
...@@ -9,11 +9,13 @@ from sqlalchemy.orm import joinedload, aliased ...@@ -9,11 +9,13 @@ from sqlalchemy.orm import joinedload, aliased
from sqlalchemy.orm.query import Query from sqlalchemy.orm.query import Query
from sqlalchemy.dialects.postgresql import insert as pgsql_insert from sqlalchemy.dialects.postgresql import insert as pgsql_insert
from typing import Any, List, Tuple, Optional, Sequence, Dict from typing import Any, List, Tuple, Optional, Sequence, Dict
import urllib.parse
import werkzeug.exceptions import werkzeug.exceptions
import wtforms import wtforms
import mo import mo
from mo.csv import FileFormat from mo.csv import FileFormat
import mo.config as config
import mo.db as db import mo.db as db
from mo.imports import ImportType, create_import from mo.imports import ImportType, create_import
import mo.jobs.submit import mo.jobs.submit
...@@ -435,9 +437,11 @@ def org_contest_import(id: int): ...@@ -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>/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>/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): 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) 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', "") format = request.args.get('format', "")
filter = ParticipantsFilterForm(request.args) filter = ParticipantsFilterForm(request.args)
...@@ -458,14 +462,27 @@ def org_contest_list(id: int, site_id: Optional[int] = None): ...@@ -458,14 +462,27 @@ def org_contest_list(id: int, site_id: Optional[int] = None):
return redirect(request.url) return redirect(request.url)
if format == "": if format == "":
table = None
emails = None
mailto_link = None
if request.endpoint == 'org_contest_list_emails':
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('[OSMO] Zpráva pro účastníky')
+ '&bcc=' + ','.join([urllib.parse.quote(email, safe='@') for email in emails])
)
count = len(users)
else:
# (count, query) = filter.apply_limits(query, pagesize=50) # (count, query) = filter.apply_limits(query, pagesize=50)
count = db.get_count(query) 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( return render_template(
'org_contest_list.html', 'org_contest_list.html',
contest=contest, site=site, contest=contest, site=site,
table=table, table=table, emails=emails, mailto_link=mailto_link,
filter=filter, count=count, action_form=action_form, filter=filter, count=count, action_form=action_form,
) )
else: else:
... ...
......
...@@ -5,12 +5,14 @@ from sqlalchemy import func ...@@ -5,12 +5,14 @@ from sqlalchemy import func
from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload
from sqlalchemy.sql.functions import coalesce from sqlalchemy.sql.functions import coalesce
from typing import Optional, Tuple from typing import Optional, Tuple
import urllib.parse
import werkzeug.exceptions import werkzeug.exceptions
import wtforms import wtforms
from wtforms import validators, ValidationError from wtforms import validators, ValidationError
from wtforms.fields.html5 import IntegerField from wtforms.fields.html5 import IntegerField
import mo import mo
import mo.config as config
import mo.db as db import mo.db as db
import mo.imports import mo.imports
from mo.rights import Right, RoundRights from mo.rights import Right, RoundRights
...@@ -338,8 +340,10 @@ def org_round_task_batch_points(round_id: int, task_id: int): ...@@ -338,8 +340,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', methods=('GET', 'POST'))
@app.route('/org/contest/r/<int:id>/list/emails', endpoint="org_round_list_emails")
def org_round_list(id: int): 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', "") format = request.args.get('format', "")
filter = ParticipantsFilterForm(request.args) filter = ParticipantsFilterForm(request.args)
...@@ -352,20 +356,36 @@ def org_round_list(id: int): ...@@ -352,20 +356,36 @@ def org_round_list(id: int):
participation_state=filter.f_participation_state, participation_state=filter.f_participation_state,
) )
action_form = None
if can_edit:
action_form = ParticipantsActionForm() action_form = ParticipantsActionForm()
if action_form.do_action(round=master_round, query=query): if action_form.do_action(round=master_round, query=query):
# Action happened, redirect # Action happened, redirect
return redirect(request.url) return redirect(request.url)
if format == "": if format == "":
table = None
emails = None
mailto_link = None
if request.endpoint == 'org_round_list_emails':
users = [pion.user for (pion, _, _) in query.all()]
emails = [f'{u.first_name} {u.last_name} <{u.email}>' for u in users]
emails.append("můj@supeč---email.cz")
mailto_link = (
'mailto:' + urllib.parse.quote(config.MAIL_CONTACT, safe='@')
+ '?subject=' + urllib.parse.quote('[OSMO] Zpráva pro účastníky')
+ '&bcc=' + ','.join([urllib.parse.quote(email, safe='@') for email in emails])
)
count = len(users)
else:
(count, query) = filter.apply_limits(query, pagesize=50) (count, query) = filter.apply_limits(query, pagesize=50)
# count = db.get_count(query) # 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( return render_template(
'org_round_list.html', 'org_round_list.html',
round=round, round=round,
table=table, table=table, emails=emails, mailto_link=mailto_link,
filter=filter, count=count, action_form=action_form, filter=filter, count=count, action_form=action_form,
) )
else: else:
... ...
......
...@@ -8,6 +8,8 @@ Seznam účastníků {% if site %}soutěžního místa {{ site.name }}{% else %} ...@@ -8,6 +8,8 @@ Seznam účastníků {% if site %}soutěžního místa {{ site.name }}{% else %}
{% block breadcrumbs %} {% block breadcrumbs %}
{{ contest_breadcrumbs(round=round, contest=contest, site=site, action="Seznam účastníků") }} {{ contest_breadcrumbs(round=round, contest=contest, site=site, action="Seznam účastníků") }}
{% endblock %} {% endblock %}
{% set id = contest.contest_id %}
{% set site_id = site.place_id if site else None %}
{% block body %} {% block body %}
<div class="form-frame"> <div class="form-frame">
...@@ -20,8 +22,10 @@ Seznam účastníků {% if site %}soutěžního místa {{ site.name }}{% else %} ...@@ -20,8 +22,10 @@ Seznam účastníků {% if site %}soutěžního místa {{ site.name }}{% else %}
{{ wtf.form_field(filter.participation_state) }} {{ wtf.form_field(filter.participation_state) }}
<div class="btn-group"> <div class="btn-group">
{{ wtf.form_field(filter.submit, class='btn btn-primary') }} {{ 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="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> <button class="btn btn-default" name="format" value="tsv" title="Stáhnout celý výsledek v TSV">↓ TSV</button>
{% endif %}
</div> </div>
{% if not site %} {% if not site %}
</div> </div>
... ...
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
{% block breadcrumbs %} {% block breadcrumbs %}
{{ contest_breadcrumbs(round=round, action="Seznam účastníků") }} {{ contest_breadcrumbs(round=round, action="Seznam účastníků") }}
{% endblock %} {% endblock %}
{% set id = round.round_id %}
{% block body %} {% block body %}
<div class="form-frame"> <div class="form-frame">
...@@ -18,10 +19,12 @@ ...@@ -18,10 +19,12 @@
<div class="form-row" style="margin-top: 5px;"> <div class="form-row" style="margin-top: 5px;">
<div class="btn-group"> <div class="btn-group">
{{ wtf.form_field(filter.submit, class='btn btn-primary') }} {{ 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="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> <button class="btn btn-default" name="format" value="tsv" title="Stáhnout celý výsledek v TSV">↓ TSV</button>
{% endif %}
</div> </div>
{% if table %}
<div style="float: right"> <div style="float: right">
Stránka {{ filter.offset.data // filter.limit.data + 1}} z {{ (count / filter.limit.data)|round(0, 'ceil')|int }}: Stránka {{ filter.offset.data // filter.limit.data + 1}} z {{ (count / filter.limit.data)|round(0, 'ceil')|int }}:
<div class="btn-group"> <div class="btn-group">
...@@ -41,6 +44,9 @@ ...@@ -41,6 +44,9 @@
</div> </div>
{% set max = filter.offset.data + filter.limit.data if filter.offset.data + filter.limit.data < count else count %} {% 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><b>{{ max }}</b> z <b>{{count}} nalezených účastníků</b>. Zobrazuji záznamy <b>{{filter.offset.data + 1}}</b><b>{{ max }}</b> z <b>{{count}} nalezených účastníků</b>.
{% else %}
Celkem {{count}} nalezených účastníků.
{% endif %}
</div> </div>
</form> </form>
</div> </div>
... ...
......
{% if action_form %} {% if action_form %}
<form action="" method="POST" class="form form-horizontal" role="form"> <form action="" method="POST" class="form form-horizontal" role="form">
{% endif %}
{% if table %}
{{ table.to_html() }} {{ 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>
{% endif %}
{% if action_form %}
{{ action_form.csrf_token }} {{ action_form.csrf_token }}
<h3>Provést akci</h3> <h3>Provést akci</h3>
<div class="form-frame"> <div class="form-frame">
...@@ -44,9 +55,28 @@ ...@@ -44,9 +55,28 @@
</div> </div>
</div> </div>
</form> </form>
{% else %} {% elif table %}
{{ table.to_html() }} <p><i>Nemáte právo k editaci účastníků v této oblasti.</i></p>
<p> {% elif emails %}
<i>Nemáte právo k editaci účastníků v této oblasti.</i> <a class="btn btn-default pull-right" style="margin-top: 15px;"
</p> 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>Emailové adresy</h3>
<div class="form-frame">
<p>Pokud máte emailového klienta, který umí <code>mailto:</code> odkazy, tak vám následující tlačítko předvyplní nový email:
  • V jiných částech OSMO píšeme "e-mail" s pomlčkou. To by bylo dobré sjednotit.

    Taktéž bych říkal spíš "odkazy typu mailto:".

  • Author Maintainer

    Sjednoceno a změněno.

  • Jiří Setnička @setnicka

    changed this line in version 2 of the diff

    ·

    changed this line in version 2 of the diff

    Toggle commit list
  • Please register or sign in to reply
<a class="btn btn-primary" href="{{ mailto_link }}">Vytvořit email ({{ count|inflected("adresát", "adresáti", "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();">
{% for email in emails %}
{{ email|escape }}
{% endfor %}
</textarea></code>
</div>
<script type="text/javascript">
document.getElementById('emails-textarea').style.height = (document.getElementById('emails-textarea').scrollHeight + 5) + "px";
</script>
{% endif %} {% endif %}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment