diff --git a/mo/web/org_round.py b/mo/web/org_round.py index 8b749771c77899a3387f766d0dd8808b36a1385e..4ceccd8bfdaa46f9b96103a5344e238bb8e52ced 100644 --- a/mo/web/org_round.py +++ b/mo/web/org_round.py @@ -49,8 +49,12 @@ def get_task(round: db.Round, task_id: int) -> db.Task: def org_rounds(): sess = db.get_session() - rounds = sess.query(db.Round).filter_by(year=mo.current_year).order_by(db.Round.year, db.Round.category, db.Round.seq) - return render_template('org_rounds.html', rounds=rounds, level_names=mo.db.place_level_names) + rounds = sess.query(db.Round).filter_by(year=mo.current_year).order_by(db.Round.year, db.Round.category, db.Round.seq, db.Round.round_id) + subround_counts = {round.round_id: 0 for round in rounds} + for round in rounds: + if round.is_subround(): + subround_counts[round.master_round_id] += 1 + return render_template('org_rounds.html', rounds=rounds, subround_counts=subround_counts, level_names=mo.db.place_level_names) class TaskDeleteForm(FlaskForm): @@ -90,6 +94,42 @@ def delete_task(round_id: int, form: TaskDeleteForm) -> bool: return False +class SubroundDeleteForm(FlaskForm): + delete_subround_id = wtforms.IntegerField() + delete_subround = wtforms.SubmitField('Smazat podkolo') + + +def delete_subround(round_id: int, form: SubroundDeleteForm) -> bool: + if not (request.method == 'POST' and 'delete_subround_id' in request.form and form.validate_on_submit()): + return False + + sess = db.get_session() + delete_round = sess.query(db.Round).get(form.delete_subround_id.data) + + if not delete_round: + flash('Podkolo s daným ID neexistuje', 'danger') + elif delete_round.master_round_id != round_id: + flash('Toto není podkolo tohoto kola!', 'danger') + elif sess.query(db.Task).filter_by(round_id=delete_round.round_id).first() is not None: + flash(f'Podkolo {delete_task.name} nelze smazat, existují úlohy na něj vázané', 'danger') + else: + # Smažeme všechny vytvořené podsoutěže, bez úloh by na ně nemělo být nic navázané + sess.query(db.Contest).filter_by(round_id=delete_round.round_id).delete() + + sess.delete(delete_round) + mo.util.log( + type=db.LogType.round, + what=delete_round.round_id, + details={'action': 'delete', 'round': db.row2dict(delete_round)}, + ) + app.logger.info(f"Podkolo {delete_round.round_id} kola {round_id} smazáno: {db.row2dict(delete_round)}") + sess.commit() + flash('Podkolo úspěšně smazáno', 'success') + return True + + return False + + class AddContestForm(FlaskForm): place_code = wtforms.StringField('Nová soutěž v oblasti:', validators=[validators.Required()]) create_contest = wtforms.SubmitField('Založit') @@ -121,12 +161,25 @@ def add_contest(round: db.Round, form: AddContestForm) -> bool: sess.add(contest) sess.flush() + contest.master_contest_id = contest.contest_id + sess.add(contest) + sess.flush() mo.util.log( type=db.LogType.contest, what=contest.contest_id, details={'action': 'add', 'contest': db.row2dict(contest)}, ) + + # Přidání soutěže do všech podkol + subrounds = sess.query(db.Round).filter_by(master=round).all() + for subround in subrounds: + sess.add(db.Contest( + round_id=subround.round_id, + master_contest_id=contest.contest_id, + place_id=contest.place_id, + )) + app.logger.info(f"Soutěž #{contest.contest_id} založena: {db.row2dict(contest)}") sess.commit() @@ -134,11 +187,65 @@ def add_contest(round: db.Round, form: AddContestForm) -> bool: return True +class AddSubroundForm(FlaskForm): + name = wtforms.StringField('Název podkola:', validators=[validators.Required()]) + create_subround = wtforms.SubmitField('Založit') + + +def add_subround(round: db.Round, form: AddSubroundForm) -> bool: + if not (request.method == 'POST' and 'create_subround' in request.form and form.validate_on_submit()): + return False + + subround = db.Round( + master_round_id=round.round_id, + year=round.year, + category=round.category, + seq=round.seq, + level=round.level, + name=form.name.data, + state=db.RoundState.preparing, + ) + + sess = db.get_session() + sess.add(subround) + sess.flush() + mo.util.log( + type=db.LogType.round, + what=subround.round_id, + details={'action': 'add', 'round': db.row2dict(subround)}, + ) + + # Založíme podsoutěže kopií soutěží z hlavního kola + contests = sess.query(db.Contest).filter_by(round_id=round.round_id).all() + for contest in contests: + sess.add(db.Contest( + round_id=subround.round_id, + master_contest_id=contest.contest_id, + place_id=contest.place_id, + )) + + app.logger.info(f"Kolo #{subround.round_id} založeno jako podkolo {round.round_id}: {db.row2dict(subround)}") + sess.commit() + + flash('Podkolo založeno', 'success') + return True + + @app.route('/org/contest/r/<int:id>/', methods=('GET', 'POST')) def org_round(id: int): sess = db.get_session() round, rr = get_round_rr(id, None, True) + tasks_counts = sess.query( + db.Task.round_id, func.count(db.Task.round_id).label('count') + ).group_by(db.Task.round_id).subquery() + print(tasks_counts) + subrounds = sess.query( + db.Round, coalesce(tasks_counts.c.count, 0) + ).outerjoin( + tasks_counts, tasks_counts.c.round_id == db.Round.round_id + ).filter(db.Round.master == round).all() + can_manage_round = rr.have_right(Right.manage_round) can_manage_contestants = rr.have_right(Right.manage_contest) @@ -150,7 +257,7 @@ def org_round(id: int): contests_counts = (sess.query( db.Contest, coalesce(participants_count.c.count, 0) - ).outerjoin(participants_count) + ).outerjoin(participants_count, db.Contest.master_contest_id == participants_count.c.contest_id) .filter(db.Contest.round == round) .options(joinedload(db.Contest.place)) .all()) @@ -177,17 +284,33 @@ def org_round(id: int): if can_manage_round and delete_task(id, form_delete_task): return redirect(url_for('org_round', id=id)) - form_add_contest = AddContestForm() - if add_contest(round, form_add_contest): - return redirect(url_for('org_round', id=id)) + form_delete_subround: Optional[SubroundDeleteForm] = None + form_add_contest: Optional[AddContestForm] = None + form_add_subround: Optional[AddSubroundForm] = None + if not round.is_subround(): + # V podkole zakazujeme operace s dalšími podkoly nebo s contesty + + form_delete_subround = SubroundDeleteForm() + if can_manage_round and delete_subround(id, form_delete_subround): + return redirect(url_for('org_round', id=id)) + + form_add_contest = AddContestForm() + if add_contest(round, form_add_contest): + return redirect(url_for('org_round', id=id)) + + form_add_subround = AddSubroundForm() + if can_manage_round and add_subround(round, form_add_subround): + return redirect(url_for('org_round', id=id)) return render_template( 'org_round.html', - round=round, + round=round, subrounds=subrounds, roles=[r.friendly_name() for r in rr.get_roles()], contests_counts=contests_counts, tasks=tasks, form_delete_task=form_delete_task, form_add_contest=form_add_contest, + form_add_subround=form_add_subround, + form_delete_subround=form_delete_subround, level_names=mo.db.place_level_names, can_manage_round=can_manage_round, can_manage_contestants=can_manage_contestants, @@ -357,7 +480,8 @@ class MODateTimeField(wtforms.DateTimeField): self.data = self.data.astimezone() -class RoundEditForm(FlaskForm): +class SubroundEditForm(FlaskForm): + name = wtforms.StringField("Název") state = wtforms.SelectField("Stav kola", choices=db.RoundState.choices(), coerce=db.RoundState.coerce) # Only the desktop Firefox does not support datetime-local field nowadays, # other browsers does provide date and time picker UI :( @@ -366,6 +490,12 @@ class RoundEditForm(FlaskForm): pr_tasks_start = MODateTimeField("Čas zveřejnění úloh pro dozor", validators=[validators.Optional()]) ct_submit_end = MODateTimeField("Konec odevzdávání pro účastníky", validators=[validators.Optional()]) pr_submit_end = MODateTimeField("Konec odevzdávání pro dozor", validators=[validators.Optional()]) + + submit = wtforms.SubmitField('Uložit') + + +class RoundEditForm(SubroundEditForm): + # SubroundEditForm + ... score_mode = wtforms.SelectField("Výsledková listina", choices=db.RoundScoreMode.choices(), coerce=db.RoundScoreMode.coerce) score_winner_limit = IntegerField( "Hranice bodů pro vítěze", validators=[validators.Optional()], @@ -383,7 +513,11 @@ def org_round_edit(id: int): sess = db.get_session() round, rr = get_round_rr(id, Right.manage_round, True) - form = RoundEditForm(obj=round) + if round.is_subround(): + form = SubroundEditForm(obj=round) + else: + form = RoundEditForm(obj=round) + if form.validate_on_submit(): form.populate_obj(round) diff --git a/mo/web/templates/org_round.html b/mo/web/templates/org_round.html index f7b53848f33b9c106bb9d3876358288c51afb2b0..29dae766b25bbd0e908ce415766c740c48fda580 100644 --- a/mo/web/templates/org_round.html +++ b/mo/web/templates/org_round.html @@ -1,7 +1,7 @@ {% extends "base.html" %} {% import "bootstrap/wtf.html" as wtf %} -{% block title %}{{ round.name }} {{ round.round_code() }}{% endblock %} +{% block title %}{{ round.master.name }} {{ round.master.round_code() }}{% if round.is_subround() %} (podkolo {{ round.name }}){% endif %}{% endblock %} {% block breadcrumbs %} {{ contest_breadcrumbs(round=round) }} {% endblock %} @@ -12,10 +12,14 @@ <tr><td>Ročník<td>{{ round.year }} <tr><td>Kategorie<td>{{ round.category }} <tr><td>Pořadí<td>{{ round.seq }} - <tr><td>Název<td>{{ round.name }} <tr><td>Oblast<td>{{ level_names[round.level] }} - <tr><td>Stav<td class='rstate-{{round.state.name}}'>{{ round.state.friendly_name() }} <tr><td>Vaše role<td>{% if roles %}{{ roles|join(", ") }}{% else %}–{% endif %} + {% if round.is_subround() %} + <tr><td>Nadřazené kolo:<td><a href="{{ url_for('org_round', id=round.master_round_id) }}">{{ round.master.name }} {{ round.master.round_code() }} </a> + {% endif %} + <tr><th colspan=2><h4>Nastavení kola:</h4> + <tr><td>Název<td>{{ round.name }} + <tr><td>Stav<td class='rstate-{{round.state.name}}'>{{ round.state.friendly_name() }} <tr><td>Účastníci vidí zadání od<td>{{ round.ct_tasks_start|timeformat }} <tr><td>Účastníci odevzdávají do<td>{{ round.ct_submit_end|timeformat }} <tr><td>Dozor vidí zadání od<td>{{ round.pr_tasks_start|timeformat }} @@ -30,12 +34,20 @@ {% else %} – {% endif %} + {% if not round.is_subround() %} <tr><td>Výsledková listina<td>{{ round.score_mode.friendly_name() }} <tr><td>Hranice bodů pro vítěze<td>{{ round.score_winner_limit|none_value(Markup('<i>nenastaveno</i>')) }} <tr><td>Hranice bodů pro úspěšné řešitele<td>{{ round.score_successful_limit|none_value(Markup('<i>nenastaveno</i>')) }} + {% endif %} </table> <div class="btn-group"> +{% if round.is_subround() %} + <a class="btn btn-primary" href='{{ url_for('org_round_list', id=round.master_round_id) }}'>Seznam účastníků nadřazeného kola</a> + {% if round.master.state in [RoundState.grading, RoundState.closed] %} + <a class="btn btn-primary" href='{{ url_for('org_score', round_id=round.master_round_id) }}'>Výsledky nadřazeného kola</a> + {% endif %} +{% else %} <a class="btn btn-primary" href='{{ url_for('org_round_list', id=round.round_id) }}'>Seznam účastníků</a> {% if round.state in [RoundState.grading, RoundState.closed] %} <a class="btn btn-primary" href='{{ url_for('org_score', round_id=round.round_id) }}'>Výsledky</a> @@ -43,6 +55,7 @@ {% if can_manage_contestants %} <a class="btn btn-default" href='{{ url_for('org_round_import', id=round.round_id) }}'>Importovat data</a> {% endif %} +{% endif %} {% if can_manage_round %} <a class="btn btn-default" href='{{ url_for('org_round_edit', id=round.round_id) }}'>Editovat nastavení kola</a> {% endif %} @@ -50,8 +63,71 @@ <a class="btn btn-default" href='{{ log_url('round', round.round_id) }}'>Historie</a> {% endif %} </div> +<br><br> +{% if not round.is_subround() %} +<div class="box-frame"> +<h3>Podkola</h3> +{% if subrounds and subrounds|length > 1 %} +<table class=data> + <thead> + <tr> + <th rowspan=2>Podkolo + <th rowspan=2>Počet úloh + <th rowspan=2>Stav + <th colspan=2>Účastníci + <th colspan=2>Dozor + <th rowspan=2>Akce + <tr> + <th>Zadání od + <th>Odevzdávání do + <th>Zadání od + <th>Odevzdávání do + </thead> + {% for (subround, tasks_count) in subrounds %} + <tr> + <td>{% if subround == round %} + <b>Toto kolo: {{ subround.name }}</b> + {% else %} + <a href="{{ url_for('org_round', id=subround.round_id) }}">{{ subround.name }}</a> + {% endif %} + <td>{{ tasks_count }} + <td class='rstate-{{subround.state.name}}'>{{ subround.state.friendly_name() }} + <td>{{ subround.ct_tasks_start|timeformat }} + <td>{{ subround.ct_submit_end|timeformat }} + <td>{{ subround.pr_tasks_start|timeformat }} + <td>{{ subround.pr_submit_end|timeformat }} + <td><div class="btn-group"> + {% if subround != round %} + <a class="btn btn-xs btn-primary" href="{{ url_for('org_round', id=subround.round_id) }}">Detail</a> + {% if can_manage_round and tasks_count == 0 %} + <form action="" method="POST" onsubmit="return confirm('Opravdu nenávratně smazat?')" class="btn-group"> + {{ form_delete_subround.csrf_token() }} + <input type="hidden" name="delete_subround_id" value="{{ subround.round_id }}"> + <button type="submit" class="btn btn-xs btn-danger">Smazat podkolo</button> + </form> + {% endif %} + {% endif %} + </div> + {% endfor %} +</table> +{% else %} +<p><i>Žádná podkola nebyla založena, vše se koná pod hlavním kolem.</i></p> +{% endif %} + +{% if can_manage_round %} +<form action="" method="POST" class="form-inline"> + {{ form_add_subround.csrf_token() }} + {{ wtf.form_field(form_add_subround.name) }} + {{ wtf.form_field(form_add_subround.create_subround) }} +</form> +{% endif %} +</div> +{% endif %} + +<div class="box-frame"> <h3>Soutěže</h3> +{% if round.is_subround() %}<p><i>Soutěže jsou synchronizovány s nadřazeným kolem.</i></p>{% endif %} {% if contests_counts %} <table class=data> <thead> @@ -76,14 +152,16 @@ <p>Zatím nebyly založeny v žádné oblasti. {% endif %} -{% if can_add_contest %} +{% if not round.is_subround() and can_add_contest %} <form action="" method="POST" class="form-inline"> {{ form_add_contest.csrf_token() }} {{ wtf.form_field(form_add_contest.place_code) }} {{ wtf.form_field(form_add_contest.create_contest) }} </form> {% endif %} +</div> +<div class="box-frame"> <h3>Úlohy</h3> {% if tasks %} <table class=data> @@ -117,7 +195,7 @@ </div> {% endif %} {% if can_handle_submits or can_upload %} - <td><dic class="btn-group"> + <td><div class="btn-group"> {% if can_handle_submits %} <a class="btn btn-xs btn-primary" href="{{ url_for('org_round_task_download', round_id=round.round_id, task_id=task.task_id) }}">Stáhnout ZIP</a> {% endif %} @@ -138,5 +216,6 @@ {% if can_manage_round %} <a class="btn btn-primary right-float" href="{{ url_for('org_round_task_new', id=round.round_id) }}">Nová úloha</a> {% endif %} +</div> {% endblock %} diff --git a/static/mo.css b/static/mo.css index c90f0243777b1901eae4212b67a6ceb0045ee9b9..0846ee92b9d3f70d7dc66649801bfa3c0ffbc3b2 100644 --- a/static/mo.css +++ b/static/mo.css @@ -171,12 +171,19 @@ nav#main-menu a.active { color:red; } -.form-frame { +.form-frame, .box-frame { padding: 10px; border: 1px #ddd solid; border-radius: 4px 4px; } +.box-frame { + margin: 10px 0px; +} +.box-frame > h3 { + margin-top: 5px; +} + .checked_toggle input.toggle:checked ~ .checked_hide { display: none; }