diff --git a/mo/web/org_contest.py b/mo/web/org_contest.py index 26f566e11318e46703842d8c03d08cfafbd2b832..62392153028b741d9ef2e83e59d4612df37fe86f 100644 --- a/mo/web/org_contest.py +++ b/mo/web/org_contest.py @@ -2,10 +2,12 @@ from dataclasses import dataclass from flask import render_template, g, redirect, url_for, flash, request from flask_wtf import FlaskForm import flask_wtf.file +import locale from markupsafe import Markup -from sqlalchemy import func, and_ +from sqlalchemy import func, and_, select 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 werkzeug.exceptions import wtforms @@ -1187,3 +1189,99 @@ def org_contest_user(contest_id: int, user_id: int): paper_link=lambda u, p: mo.web.util.org_paper_link(sc.contest, None, u, p), paper_counts=paper_counts, ) + + +class AdvanceForm(FlaskForm): + boundary = IntegerField('Bodová hranice', description="Postupí všichni účastníci, kteří v minulém kole získali aspoň tolik bodů.", validators=[validators.InputRequired()]) + status = wtforms.HiddenField() + preview = wtforms.SubmitField('Zobrazit návrh') + execute = wtforms.SubmitField('Provést') + + +@app.route('/org/contest/c/<int:contest_id>/advance', methods=('GET', 'POST')) +def org_contest_advance(contest_id: int): + sess = db.get_session() + conn = sess.connection() + contest, rr = get_contest_rr(contest_id, Right.manage_contest) + + round = contest.round + if round.state != db.RoundState.preparing: + flash('Aktuální kolo není ve stavu přípravy', 'danger') + return redirect(url_for('org_contest', id=contest_id)) + + prev_round = sess.query(db.Round).filter_by(year=round.year, category=round.category, seq=round.seq - 1).one_or_none() + if prev_round is None: + flash('Předchozí kolo nenalezeno', 'danger') + return redirect(url_for('org_contest', id=contest_id)) + elif prev_round.state != db.RoundState.closed: + flash('Předchozí kolo dosud nebylo ukončeno', 'danger') + return redirect(url_for('org_contest', id=contest_id)) + elif prev_round.level < round.level: + flash('Předchozí kolo se koná ve vyšší oblasti než toto kolo', 'danger') + return redirect(url_for('org_contest', id=contest_id)) + + prev_contests: List[db.Contest] = [] + accept_by_place_id: Dict[int, int] = {} + reject_by_place_id: Dict[int, int] = {} + + form = AdvanceForm() + if form.validate_on_submit(): + desc_cte = db.place_descendant_cte(contest.place, max_level=prev_round.level) + prev_contests = (sess.query(db.Contest) + .filter(db.Contest.round == prev_round) + .filter(db.Contest.place_id.in_(select([desc_cte]))) + .options(joinedload(db.Contest.place)) + .all()) + prev_contests.sort(key=lambda c: locale.strxfrm(c.place.name or "")) + + accept_by_place_id = {c.place_id: 0 for c in prev_contests} + reject_by_place_id = {c.place_id: 0 for c in prev_contests} + + prev_pion_query = (sess.query(db.Participation) + .filter(db.Participation.contest_id.in_([c.contest_id for c in prev_contests])) + .filter(db.Participation.state.in_((db.PartState.registered, db.PartState.invited, db.PartState.present)))) + prev_pions = prev_pion_query.all() + + if form.boundary.data > 0: + accept_uids = (sess.query(db.Solution.user_id) + .select_from(db.Solution) + .join(db.Task, and_(db.Task.task_id == db.Solution.task_id, db.Task.round == prev_round)) + .filter(db.Solution.user_id.in_(prev_pion_query.with_entities(db.Participation.user_id).subquery())) + .group_by(db.Solution.user_id) + .having(func.sum(db.Solution.points) >= form.boundary.data) + .all()) + accept_uids = [a[0] for a in accept_uids] + else: + accept_uids = None + + want_execute = form.execute.data + for pp in prev_pions: + if accept_uids and pp.user_id not in accept_uids: + reject_by_place_id[pp.place_id] += 1 + continue + accept_by_place_id[pp.place_id] += 1 + + if want_execute: + # ORM neumí ON CONFLICT DO NOTHING, takže musíme o vrstvu níže + ins = pgsql_insert(db.Participation.__table__).values( + user_id=pp.user_id, + contest_id=contest.contest_id, + place_id=contest.place.place_id, + state=db.PartState.invited, + ).on_conflict_do_nothing() + conn.execute(ins) + + if want_execute: + sess.commit() + flash('Provedeno.', 'success') + + return render_template( + 'org_contest_advance.html', + contest=contest, + round=contest.round, + prev_round=prev_round, + prev_contests=prev_contests, + accept_by_place_id=accept_by_place_id, + reject_by_place_id=reject_by_place_id, + form=form, + ) diff --git a/mo/web/templates/org_contest.html b/mo/web/templates/org_contest.html index b91335053dbfcc99440bfdcfb5fb5849af74d90f..46315999d2421c81e6ce88c16de79982645e6574 100644 --- a/mo/web/templates/org_contest.html +++ b/mo/web/templates/org_contest.html @@ -32,11 +32,16 @@ <div class="btn-group"> <a class="btn btn-primary" href='{{ url_for('org_contest_list', id=contest.contest_id, site_id=site_id) }}'>Seznam účastníků</a> + {% if round.state != RoundState.preparing %} <a class="btn btn-primary" href='{{ url_for('org_contest_solutions', id=contest.contest_id, site_id=site_id) }}'>Odevzdaná řešení</a> + {% endif %} {% if not site %} {% if round.state in [RoundState.grading, RoundState.closed] %} <a class="btn btn-primary" href='{{ url_for('org_score', contest_id=contest.contest_id) }}'>Výsledky</a> {% endif %} + {% if round.state == RoundState.preparing and round.seq > 1 %} + <a class="btn btn-primary" href='{{ url_for('org_contest_advance', contest_id=contest.contest_id) }}'>Postup z minulého kola</a> + {% endif %} {% if can_manage %} <a class="btn btn-default" href='{{ url_for('org_contest_import', id=contest.contest_id) }}'>Importovat data</a> {% endif %} diff --git a/mo/web/templates/org_contest_advance.html b/mo/web/templates/org_contest_advance.html new file mode 100644 index 0000000000000000000000000000000000000000..f7a86b84908938777a2f9327a3e063d4fc28c6b7 --- /dev/null +++ b/mo/web/templates/org_contest_advance.html @@ -0,0 +1,41 @@ +{% extends "base.html" %} +{% import "bootstrap/wtf.html" as wtf %} + +{% block title %}Postup z {{ prev_round.round_code() }} ({{ prev_round.name }}) do {{ round.round_code() }} ({{ round.name }}){% endblock %} +{% block breadcrumbs %} +{{ contest_breadcrumbs(round=round, contest=contest, action="Postup") }} +{% endblock %} +{% block body %} + +<form method="POST" class="form form-horizontal" action=""> + {{ form.csrf_token }} + {{ wtf.form_field(form.boundary, form_type='horizontal') }} + <div class="btn-group col-lg-offset-2"> + {{ wtf.form_field(form.preview) }} + {{ wtf.form_field(form.execute, class="btn btn-primary") }} + </div> +</form> + +{% if form.preview.data or form.execute.data %} +<h3>Postup z oblastí</h3> + +<table class='data'> + <thead> + <tr><th>Oblast<th>Postoupilo<th>Nepostoupilo + <tbody> + {% for c in prev_contests %} + <tr> + <td>{{ c.place.name }} + <td>{{ accept_by_place_id[c.place.place_id] }} + <td>{{ reject_by_place_id[c.place.place_id] }} + {% endfor %} + <tfoot> + <tr> + <th>Celkem + <th>{{ accept_by_place_id.values()|sum }} + <th>{{ reject_by_place_id.values()|sum }} + </tfoot> +</table> +{% endif %} + +{% endblock %}