diff --git a/mo/web/org_contest.py b/mo/web/org_contest.py index 5e1f08961d0ade56a28d7e0ce6658d009998106b..a41ecdf6f2c1be4660daed2d3d861621286d7ad9 100644 --- a/mo/web/org_contest.py +++ b/mo/web/org_contest.py @@ -8,7 +8,7 @@ import json import locale import magic from markupsafe import Markup -from sqlalchemy import func, and_, select +from sqlalchemy import func, and_, select, not_ from sqlalchemy.orm import joinedload, aliased from sqlalchemy.orm.query import Query from sqlalchemy.dialects.postgresql import insert as pgsql_insert @@ -1558,6 +1558,7 @@ class ContestEditForm(FlaskForm): choices=[ch for ch in db.RoundState.choices() if ch[0] != 'delegate'], coerce=db.RoundState.coerce) submit = wtforms.SubmitField('Uložit') + force_submit = wtforms.SubmitField('Uložit s chybami') @app.route('/org/contest/c/<int:ct_id>/edit', methods=('GET', 'POST')) @@ -1573,7 +1574,21 @@ def org_contest_edit(ct_id: int): form.state.render_kw = {'disabled': ""} form.state.description = 'Nastavení kola neumožňuje měnit stav soutěže.' - if form.validate_on_submit(): + do_submit = False + errors = [] + offer_force_submit = False + + if form.submit.data or form.force_submit.data: + do_submit = form.validate_on_submit() + if do_submit: + errors = check_contest_state(round, contest, form.state.data) + if errors and not form.force_submit.data: + do_submit = False + offer_force_submit = True + else: + errors = check_contest_state(round, contest, form.state.data) + + if do_submit: form.populate_obj(contest) if sess.is_modified(contest): @@ -1597,15 +1612,66 @@ def org_contest_edit(ct_id: int): return redirect(ctx.url_for('org_contest')) + if not offer_force_submit: + del form.force_submit + return render_template( 'org_contest_edit.html', ctx=ctx, round=round, contest=contest, form=form, + errors=errors, + offer_force_submit=offer_force_submit, ) +def check_contest_state(round: db.Round, contest: Optional[db.Contest], state: db.RoundState) -> List[str]: + """Kontrola stavu soutěží (buď zadané nebo všech v kole) při přepínání stavu.""" + sess = db.get_session() + errors = [] + + contests_query = (sess.query(db.Contest) + .filter_by(round=round) + .options(joinedload(db.Contest.place))) + if contest is not None: + contests_query = contests_query.filter_by(contest_id=contest.contest_id) + + def add_ct_errors(cts: List[db.Contest], msg: str) -> None: + if len(cts) > 100: + errors.append(f'{msg} ({len(ct_no_score)} soutěží).') + else: + for c in cts: + errors.append(f'{msg} {c.place.name_locative()}.') + + if state in [db.RoundState.graded, db.RoundState.closed]: + ct_no_points = (contests_query + .filter((sess.query(db.Solution) + .join(db.Task, and_(db.Task.task_id == db.Solution.task_id, + db.Task.round_id == db.Contest.round_id)) + .join(db.Participation, and_(db.Participation.user_id == db.Solution.user_id, + db.Participation.contest_id == db.Contest.contest_id, + db.Participation.state == db.PartState.active)) + .join(db.User, and_(db.User.user_id == db.Participation.user_id, + not_(db.User.is_test))) + .filter(db.Solution.points == None)) + .exists()) + .all()) + add_ct_errors(ct_no_points, 'Chybí body') + + if not round.is_subround() and state == db.RoundState.closed: + ct_no_score = (contests_query + .filter_by(scoretable_id=None) + .filter((sess.query(db.Participation) + .filter(db.Participation.contest_id == db.Contest.contest_id) + .filter_by(state=db.PartState.active)) + .exists()) + .all()) + add_ct_errors(ct_no_score, 'Chybí oficiální výsledková listina') + + return errors + + class ParticipantAddForm(FlaskForm): email = mo_fields.Email(validators=[validators.Required()]) first_name = mo_fields.FirstName(validators=[validators.Optional()]) diff --git a/mo/web/org_round.py b/mo/web/org_round.py index e6fa946d32ebd1382b2857cb1a47961035a241bf..ba0b140e9f87cb2afad6cf47f05a4b1e95eb60d6 100644 --- a/mo/web/org_round.py +++ b/mo/web/org_round.py @@ -23,7 +23,7 @@ import mo.util from mo.util_format import inflect_with_number from mo.web import app import mo.web.fields as mo_fields -from mo.web.org_contest import get_context, get_prev_round +from mo.web.org_contest import get_context, get_prev_round, check_contest_state @app.route('/org/contest/') @@ -374,8 +374,6 @@ def org_round_task_edit(round_id: int, task_id: int): class RoundEditForm(FlaskForm): - _for_round: Optional[db.Round] = None - name = mo_fields.String("Název", render_kw={'autofocus': True}) code = mo_fields.String("Kód", description="Kód kola používaný v kódech úloh ('1', 'S' apod.). Není-li vyplněn, použije se pořadí kola v kategorii.", @@ -407,26 +405,7 @@ class RoundEditForm(FlaskForm): enroll_advert = mo_fields.String("Popis v přihlášce") 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): - if field.data != db.RoundState.preparing: - if self.ct_tasks_start.data is None: - raise ValidationError('Není-li nastaven času začátku soutěže, stav musí být "připravuje se"') - if self._for_round is not None: - num_tasks = db.get_session().query(db.Task).filter_by(round=self._for_round).count() - if num_tasks == 0: - raise ValidationError('Nejsou-li definovány žádné úlohy, stav musí být "připravuje se"') - - def abstract_validate_time_order(self, field): - if field.data is not None: - if any([i.data is not None and i.data > field.data for i in [self.ct_tasks_start, self.pr_tasks_start]]): - raise ValidationError('Soutěž nesmí skončit dříve než začne.') - - def validate_ct_submit_end(self, field): - self.abstract_validate_time_order(field) - - def validate_pr_submit_end(self, field): - self.abstract_validate_time_order(field) + force_submit = wtforms.SubmitField('Uložit s chybami') @app.route('/org/contest/r/<int:round_id>/edit', methods=('GET', 'POST')) @@ -436,7 +415,6 @@ def org_round_edit(round_id: int): round = ctx.round form = RoundEditForm(obj=round) - form._for_round = round if round.is_subround(): # podkolo nemá nastavení výsledkové listiny del form.score_mode @@ -445,7 +423,22 @@ def org_round_edit(round_id: int): del form.points_step # ani nastavení přihlašování del form.enroll_mode - if form.validate_on_submit(): + + do_submit = False + errors = [] + offer_force_submit = False + + if form.submit.data or form.force_submit.data: + do_submit = form.validate_on_submit() + if do_submit: + errors = check_round_settings(round, form) + if errors and not form.force_submit.data: + do_submit = False + offer_force_submit = True + else: + errors = check_round_settings(round, form) + + if do_submit: form.populate_obj(round) if round.code == "": round.code = str(round.seq) @@ -479,14 +472,48 @@ def org_round_edit(round_id: int): return redirect(ctx.url_for('org_round')) + if not offer_force_submit: + del form.force_submit + return render_template( 'org_round_edit.html', ctx=ctx, round=round, form=form, + errors=errors, + offer_force_submit=offer_force_submit, ) +def check_round_settings(round: db.Round, form: RoundEditForm) -> List[str]: + state = form.state.data + errors = [] + sess = db.get_session() + + if state != db.RoundState.preparing: + if form.ct_tasks_start.data is None: + errors.append('Není nastaven čas začátku soutěže, a přitom stav není „připravuje se“.') + num_tasks = sess.query(db.Task).filter_by(round=round).count() + if num_tasks == 0: + errors.append('Nejsou definovány žádné úlohy, a přitom stav není „připravuje se“.') + + if _time_crossed(form.pr_tasks_start, form.ct_tasks_start): + errors.append('Dozor má úlohy k dispozici později než účastníci.') + if _time_crossed(form.ct_tasks_start, form.ct_submit_end): + errors.append('Soutěž pro účastníky skončí dřív než začne.') + if _time_crossed(form.ct_tasks_start, form.pr_submit_end): + errors.append('Odevzdávání pro dozor skončí dřív než soutěž začne.') + + errors.extend(check_contest_state(round, None, state)) + return errors + + +def _time_crossed(first_field: mo_fields.DateTime, second_field: mo_fields.DateTime) -> bool: + first = first_field.data + second = second_field.data + return first is not None and second is not None and first > second + + @app.route('/org/contest/r/<int:round_id>/task-statement/zadani.pdf') def org_task_statement(round_id: int): ctx = get_context(round_id=round_id) diff --git a/mo/web/templates/org_contest_edit.html b/mo/web/templates/org_contest_edit.html index 39e4c0fb729dba67e6d6786387dbf5f43873b232..1a3a62f1b3c935e7ee053a6cebea4f71367112a7 100644 --- a/mo/web/templates/org_contest_edit.html +++ b/mo/web/templates/org_contest_edit.html @@ -7,6 +7,21 @@ {% endblock %} {% block body %} -{{ wtf.quick_form(form, form_type='horizontal', button_map={'submit': 'primary'}) }} +{% if errors %} +<div class='alert alert-danger'> +<p>V nastavení soutěže byly nalezeny následující problémy: +<ul> + {% for e in errors %} + <li>{{ e }} + {% endfor %} +</ul> +<p>Chyby prosím opravte. +{% if offer_force_submit %} +Pokud přesto chcete nastavení použít, použijte tlačíko „Uložit s chybami“. +{% endif %} +</div> +{% endif %} + +{{ wtf.quick_form(form, form_type='horizontal', button_map={'submit': 'primary', 'force_submit': 'danger'}) }} {% endblock %} diff --git a/mo/web/templates/org_round_edit.html b/mo/web/templates/org_round_edit.html index 0e42ebd87aba8af08e40e202dfaccb731dba5d33..642fc31b4874d858ba365f91490bc9c1f89b5251 100644 --- a/mo/web/templates/org_round_edit.html +++ b/mo/web/templates/org_round_edit.html @@ -7,6 +7,21 @@ {% endblock %} {% block body %} -{{ wtf.quick_form(form, form_type='horizontal', button_map={'submit': 'primary'}) }} +{% if errors %} +<div class='alert alert-danger'> +<p>V nastavení kola byly nalezeny následující problémy: +<ul> + {% for e in errors %} + <li>{{ e }} + {% endfor %} +</ul> +<p>Chyby prosím opravte. +{% if offer_force_submit %} +Pokud přesto chcete nastavení použít, použijte tlačíko „Uložit s chybami“. +{% endif %} +</div> +{% endif %} + +{{ wtf.quick_form(form, form_type='horizontal', button_map={'submit': 'primary', 'force_submit': 'danger'}) }} {% endblock %}