From 3f31c85186ae74ae92977e885189b7e1bfc60d5c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ji=C5=99=C3=AD=20Setni=C4=8Dka?= <setnicka@seznam.cz>
Date: Thu, 4 Mar 2021 01:46:42 +0100
Subject: [PATCH] =?UTF-8?q?Vytv=C3=A1=C5=99en=C3=AD=20a=20ru=C5=A1en=C3=AD?=
=?UTF-8?q?=20podkol=20ze=20str=C3=A1nky=20kola?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Při vytvoření podkola se vytvoří i všechny podsoutěže, při mazání se zase smaží
a při vytvoření nové soutěže se tato vytvoří i v podkolech.
Mazat podkola se dá jen, pokud nemá žádné úlohy (což tranzitivně zaručuje i žádné
řešení, body, ...).
V podkolech se nedají editovat soutěže nebo vytvářet další podkola.
Issue #178
---
mo/web/org_round.py | 152 ++++++++++++++++++++++++++++++--
mo/web/templates/org_round.html | 89 +++++++++++++++++--
static/mo.css | 9 +-
3 files changed, 235 insertions(+), 15 deletions(-)
diff --git a/mo/web/org_round.py b/mo/web/org_round.py
index 8b749771..4ceccd8b 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 f7b53848..29dae766 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 c90f0243..0846ee92 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;
}
--
GitLab