diff --git a/mo/db.py b/mo/db.py
index deb67a4012ea97a5427d9c9accda607b68255c73..76c6aa3c1e5ee1b48365d95b95eb248a5e958ebb 100644
--- a/mo/db.py
+++ b/mo/db.py
@@ -416,6 +416,9 @@ class User(Base):
def full_name(self) -> str:
return self.first_name + ' ' + self.last_name
+ def full_email(self) -> str:
+ return f'{self.first_name} {self.last_name} <{self.email}>'
+
def sort_key(self) -> Tuple[str, str, int]:
return (locale.strxfrm(self.last_name), locale.strxfrm(self.first_name), self.user_id)
diff --git a/mo/web/org_contest.py b/mo/web/org_contest.py
index 6f57771b00183fcb951ee0008b49c39383bcd3ef..6bbc463d1287bc56f83a29499f6dd8c1dd30d906 100644
--- a/mo/web/org_contest.py
+++ b/mo/web/org_contest.py
@@ -275,6 +275,9 @@ class ParticipantsFilterForm(PagerForm):
contest_place = mo_fields.Place("Soutěžní oblast", render_kw={'autofocus': True})
participation_state = wtforms.SelectField('Stav účasti', choices=[('*', '*')] + list(db.PartState.choices()), default='*')
+ pant_email = wtforms.BooleanField("E-maily účastníků", default=True)
+ school_email = wtforms.BooleanField("E-maily jejich školních garantů", default=False)
+
# format = wtforms.RadioField(choices=[('', 'Zobrazit'), ('csv', 'Stáhnout vše v CSV'), ('tsv', 'Stáhnout vše v TSV')])
submit = wtforms.SubmitField("Zobrazit")
download_csv = wtforms.SubmitField("↓ CSV")
@@ -545,7 +548,7 @@ def org_import_org(round_id: Optional[int] = None, hier_id: Optional[int] = None
contest=contest,
round=round,
default_place=default_place
- )
+ )
# URL je explicitně uvedeno v mo.email.contestant_list_url
@@ -565,7 +568,8 @@ def org_generic_list(round_id: Optional[int] = None, hier_id: Optional[int] = No
can_edit = rr.have_right(Right.manage_contest) and request.endpoint != 'org_generic_list_emails'
format = request.args.get('format', "")
- filter = ParticipantsFilterForm(formdata=request.args)
+ # XXX: Předáme-li prázdný slovník jako request.args, zresetují se booleovská políčka
+ filter = ParticipantsFilterForm(formdata=request.args if request.args else None)
if request.args:
filter.validate()
@@ -587,13 +591,17 @@ def org_generic_list(round_id: Optional[int] = None, hier_id: Optional[int] = No
if format == "":
table = None
emails = None
+ missing_org_warnings = None
mailto_link = None
if request.endpoint == 'org_generic_list_emails':
if contest:
subj = f'{contest.round.name} {contest.round.category} {contest.place.name_locative()}'
else:
subj = f'{round.name} kategorie {round.category}'
- (emails, mailto_link) = get_contestant_emails(query, mailto_subject=subj)
+ (emails, mailto_link, missing_org_warnings) = \
+ get_contestant_emails(query, mailto_subject=subj,
+ pant_email=filter.pant_email.data,
+ school_email=filter.school_email.data)
count = len(emails)
else:
(count, query) = filter.apply_limits(query, pagesize=50)
@@ -605,9 +613,10 @@ def org_generic_list(round_id: Optional[int] = None, hier_id: Optional[int] = No
contest=contest, round=round, site=ctx.site,
table=table, emails=emails, mailto_link=mailto_link,
filter=filter, count=count, action_form=action_form,
+ missing_org_warnings=missing_org_warnings,
)
else:
- table = make_contestant_table(query, round)
+ table = make_contestant_table(query, round, add_school_orgs=True)
return table.send_as(format, args=request.args)
@@ -618,6 +627,7 @@ contest_list_columns = (
Column(key='email', name='email', title='E-mail'),
Column(key='school', name='skola', title='Škola'),
Column(key='school_code', name='kod_skoly', title='Kód školy'),
+ Column(key='school_orgs', name='skol_garanti', title='E-maily školních garantů'),
Column(key='grade', name='rocnik', title='Ročník'),
Column(key='born_year', name='rok_naroz', title='Rok naroz.'),
Column(key='place_code', name='kod_soutez_mista', title='Sout. místo'),
@@ -665,33 +675,55 @@ def get_contestants_query(
return query
-def make_contestant_table(query: Query, round: db.Round, add_checkbox: bool = False, add_contest_column: bool = False):
+def get_school_orgs(query_result: List[Tuple[db.Participation, db.Participant, db.Contest]]) -> Dict[int, List[db.User]]:
+ school_ids = set(pant.school for _, pant, _ in query_result)
+
+ user_roles = (db.get_session()
+ .query(db.UserRole)
+ .filter_by(role=db.RoleType.garant_skola)
+ .filter(db.UserRole.place_id.in_(school_ids))
+ .options(joinedload(db.UserRole.user))
+ .all())
+
+ out: Dict[int, List[db.User]] = {sid: [] for sid in school_ids}
+
+ for ur in user_roles:
+ out[ur.place_id].append(ur.user)
+
+ return out
+
+
+def make_contestant_table(query: Query, round: db.Round, add_checkbox: bool = False, add_contest_column: bool = False, add_school_orgs: bool = False) -> Table:
ctants = query.all()
+ if add_school_orgs:
+ school_orgs = get_school_orgs(ctants)
+
rows: List[Row] = []
for pion, pant, ct in ctants:
u = pion.user
html_attr = {
'class': 'state-' + pion.state.name
}
- rows.append(Row(
- keys={
- 'sort_key': u.sort_key(),
- 'user_id': u.user_id,
- 'first_name': cell_pion_link(u, pion.contest_id, u.first_name),
- 'last_name': cell_pion_link(u, pion.contest_id, u.last_name),
- 'email': cell_email_link_flags(u),
- 'school': pant.school_place.name,
- 'school_code': cell_place_link(pant.school_place),
- 'grade': pant.grade,
- 'born_year': pant.birth_year,
- 'region_code': cell_contest_link(ct),
- 'place_code': cell_contest_link(ct, pion.place),
- 'status': pion.state.friendly_name(),
- 'checkbox': CellCheckbox('checked', u.user_id, False),
- },
- html_attr=html_attr,
- ))
+ row_keys = {
+ 'sort_key': u.sort_key(),
+ 'user_id': u.user_id,
+ 'first_name': cell_pion_link(u, pion.contest_id, u.first_name),
+ 'last_name': cell_pion_link(u, pion.contest_id, u.last_name),
+ 'email': cell_email_link_flags(u),
+ 'school': pant.school_place.name,
+ 'school_code': cell_place_link(pant.school_place),
+ 'grade': pant.grade,
+ 'born_year': pant.birth_year,
+ 'region_code': cell_contest_link(ct),
+ 'place_code': cell_contest_link(ct, pion.place),
+ 'status': pion.state.friendly_name(),
+ 'checkbox': CellCheckbox('checked', u.user_id, False),
+ }
+ if add_school_orgs:
+ assert pant.school in school_orgs
+ row_keys['school_orgs'] = ', '.join([user.email for user in school_orgs[pant.school]])
+ rows.append(Row(keys=row_keys, html_attr=html_attr))
rows.sort(key=lambda r: r.keys['sort_key'])
@@ -710,15 +742,40 @@ def make_contestant_table(query: Query, round: db.Round, add_checkbox: bool = Fa
)
-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]
+def get_contestant_emails(query: Query,
+ mailto_subject: str,
+ pant_email: bool = True,
+ school_email: bool = True,
+ ) -> Tuple[List[str], str, List[str]]:
+ ctants = query.all()
+
+ user_set: Set[db.User]
+ if pant_email:
+ user_set = set(pion.user for (pion, _, _) in ctants)
+ else:
+ user_set = set()
+
+ org_warning_for: List[db.User] = []
+ if school_email:
+ school_orgs = get_school_orgs(ctants)
+ for pion, pant, _ in ctants:
+ if school_orgs[pant.school]:
+ for u in school_orgs[pant.school]:
+ user_set.add(u)
+ else:
+ org_warning_for.append(pion.user)
+
+ users = sorted(user_set, key=lambda u: u.sort_key())
+ emails = [u.full_email() for u in users]
+ org_warnings = [u.full_name() for u in sorted(org_warning_for, key=lambda u: u.sort_key())]
+
mailto_link = (
- 'mailto:' + urllib.parse.quote(config.MAIL_CONTACT, safe='@')
+ 'mailto:' + urllib.parse.quote(g.user.full_email(), safe='@')
+ '?subject=' + urllib.parse.quote(mailto_subject)
- + '&bcc=' + ','.join([urllib.parse.quote(email, safe='@') for email in emails])
+ + '&bcc=' + ','.join([urllib.parse.quote(u.email, safe='@') for u in users])
)
- return (emails, mailto_link)
+
+ return (emails, mailto_link, org_warnings)
class SubmitForm(FlaskForm):
diff --git a/mo/web/table.py b/mo/web/table.py
index 65fdde48fd4f6f65e15967102760dd351c0283ff..bc6975f587a3f1637b1628d84acb65e33c920667 100644
--- a/mo/web/table.py
+++ b/mo/web/table.py
@@ -6,7 +6,7 @@ import io
from markupsafe import Markup
from typing import Any, Dict, List, Sequence, Optional, Iterable, Union
import urllib.parse
-from werkzeug.datastructures import ImmutableMultiDict
+from werkzeug.datastructures import MultiDict, ImmutableMultiDict
import werkzeug.exceptions
from mo.csv import FileFormat
@@ -206,7 +206,7 @@ class Table:
return Markup("\n".join(tab))
- def get_columns_checkboxes(self, line_prefix: str = "", args: Optional[ImmutableMultiDict] = None) -> Markup:
+ def get_columns_checkboxes(self, line_prefix: str = "", args: Union[None, MultiDict, ImmutableMultiDict] = None) -> Markup:
out = [line_prefix + '<input type="hidden" name="do_column_selection" value="1">']
for c in self.columns:
if c.in_export is None:
@@ -251,7 +251,7 @@ class Table:
yield out.getvalue().encode(fmt.get_charset())
- def send_as(self, format: Union[FileFormat, str], streaming: bool = False, args: Optional[ImmutableMultiDict] = None) -> Response:
+ def send_as(self, format: Union[FileFormat, str], streaming: bool = False, args: Union[None, MultiDict, ImmutableMultiDict] = None) -> Response:
try:
fmt = FileFormat.coerce(format)
except ValueError:
diff --git a/mo/web/templates/org_generic_list.html b/mo/web/templates/org_generic_list.html
index cddfcda9cbc408667feae0aee9b4b18d0841aad1..854df8042880b21271ce943a6ceae728afbec00d 100644
--- a/mo/web/templates/org_generic_list.html
+++ b/mo/web/templates/org_generic_list.html
@@ -27,6 +27,13 @@
{{ wtf.form_field(filter.school, size=8) }}
{{ wtf.form_field(filter.participation_state) }}
</div>
+ {% if not table %}
+ {{ wtf.form_field(filter.pant_email) }}
+
+ {{ wtf.form_field(filter.school_email) }}
+ <div>
+ </div>
+ {% endif %}
<div class="form-row" style="margin-top: 5px;">
<div class="btn-group">
{{ wtf.form_field(filter.submit, class='btn btn-primary') }}
@@ -146,6 +153,11 @@
</p>
{% endif %}
{% else %}
+ {% if missing_org_warnings %}
+ <h3>Školní garant nenalezen pro tyto účastníky:</h3>
+ <pre>{{ missing_org_warnings|join('\n')|escape }}</pre>
+ {% endif %}
+
<h3>E-mailové adresy</h3>
{% if emails %}
diff --git a/mo/web/templates/parts/org_contest_guide.html b/mo/web/templates/parts/org_contest_guide.html
index f8f23ddd13a7dae496fcb431d6458aab7a3930d1..9d425c5879348faa89986fd033ef3103f1dd023e 100644
--- a/mo/web/templates/parts/org_contest_guide.html
+++ b/mo/web/templates/parts/org_contest_guide.html
@@ -27,7 +27,7 @@
{% endif %}
{% if round_type not in [RoundType.domaci, RoundType.skolni] %}
- <li>Rozešlete soutěžícím <a href='{{ ctx.url_for('org_generic_list_emails') }}'>pozvánky</a>.
+ <li>Rozešlete soutěžícím <a href='{{ ctx.url_for('org_generic_list_emails', participation_state='active', pant_email=True, school_email=True) }}'>pozvánky</a>.
{% endif %}
{% if round_type != RoundType.domaci %}