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 %}