Skip to content
Snippets Groups Projects
Commit 3f31c851 authored by Jiří Setnička's avatar Jiří Setnička
Browse files

Vytváření a rušení podkol ze stránky kola

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
parent 8b79d794
No related branches found
No related tags found
1 merge request!38Draft: Dělená kola
This commit is part of merge request !38. Comments created here will be created in the context of that merge request.
...@@ -49,8 +49,12 @@ def get_task(round: db.Round, task_id: int) -> db.Task: ...@@ -49,8 +49,12 @@ def get_task(round: db.Round, task_id: int) -> db.Task:
def org_rounds(): def org_rounds():
sess = db.get_session() 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) 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)
return render_template('org_rounds.html', rounds=rounds, level_names=mo.db.place_level_names) 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): class TaskDeleteForm(FlaskForm):
...@@ -90,6 +94,42 @@ def delete_task(round_id: int, form: TaskDeleteForm) -> bool: ...@@ -90,6 +94,42 @@ def delete_task(round_id: int, form: TaskDeleteForm) -> bool:
return False 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): class AddContestForm(FlaskForm):
place_code = wtforms.StringField('Nová soutěž v oblasti:', validators=[validators.Required()]) place_code = wtforms.StringField('Nová soutěž v oblasti:', validators=[validators.Required()])
create_contest = wtforms.SubmitField('Založit') create_contest = wtforms.SubmitField('Založit')
...@@ -121,12 +161,25 @@ def add_contest(round: db.Round, form: AddContestForm) -> bool: ...@@ -121,12 +161,25 @@ def add_contest(round: db.Round, form: AddContestForm) -> bool:
sess.add(contest) sess.add(contest)
sess.flush() sess.flush()
contest.master_contest_id = contest.contest_id
sess.add(contest)
sess.flush()
mo.util.log( mo.util.log(
type=db.LogType.contest, type=db.LogType.contest,
what=contest.contest_id, what=contest.contest_id,
details={'action': 'add', 'contest': db.row2dict(contest)}, 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)}") app.logger.info(f"Soutěž #{contest.contest_id} založena: {db.row2dict(contest)}")
sess.commit() sess.commit()
...@@ -134,11 +187,65 @@ def add_contest(round: db.Round, form: AddContestForm) -> bool: ...@@ -134,11 +187,65 @@ def add_contest(round: db.Round, form: AddContestForm) -> bool:
return True 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')) @app.route('/org/contest/r/<int:id>/', methods=('GET', 'POST'))
def org_round(id: int): def org_round(id: int):
sess = db.get_session() sess = db.get_session()
round, rr = get_round_rr(id, None, True) 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_round = rr.have_right(Right.manage_round)
can_manage_contestants = rr.have_right(Right.manage_contest) can_manage_contestants = rr.have_right(Right.manage_contest)
...@@ -150,7 +257,7 @@ def org_round(id: int): ...@@ -150,7 +257,7 @@ def org_round(id: int):
contests_counts = (sess.query( contests_counts = (sess.query(
db.Contest, db.Contest,
coalesce(participants_count.c.count, 0) 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) .filter(db.Contest.round == round)
.options(joinedload(db.Contest.place)) .options(joinedload(db.Contest.place))
.all()) .all())
...@@ -177,17 +284,33 @@ def org_round(id: int): ...@@ -177,17 +284,33 @@ def org_round(id: int):
if can_manage_round and delete_task(id, form_delete_task): if can_manage_round and delete_task(id, form_delete_task):
return redirect(url_for('org_round', id=id)) 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() form_add_contest = AddContestForm()
if add_contest(round, form_add_contest): if add_contest(round, form_add_contest):
return redirect(url_for('org_round', id=id)) 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( return render_template(
'org_round.html', 'org_round.html',
round=round, round=round, subrounds=subrounds,
roles=[r.friendly_name() for r in rr.get_roles()], roles=[r.friendly_name() for r in rr.get_roles()],
contests_counts=contests_counts, contests_counts=contests_counts,
tasks=tasks, form_delete_task=form_delete_task, tasks=tasks, form_delete_task=form_delete_task,
form_add_contest=form_add_contest, 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, level_names=mo.db.place_level_names,
can_manage_round=can_manage_round, can_manage_round=can_manage_round,
can_manage_contestants=can_manage_contestants, can_manage_contestants=can_manage_contestants,
...@@ -357,7 +480,8 @@ class MODateTimeField(wtforms.DateTimeField): ...@@ -357,7 +480,8 @@ class MODateTimeField(wtforms.DateTimeField):
self.data = self.data.astimezone() 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) state = wtforms.SelectField("Stav kola", choices=db.RoundState.choices(), coerce=db.RoundState.coerce)
# Only the desktop Firefox does not support datetime-local field nowadays, # Only the desktop Firefox does not support datetime-local field nowadays,
# other browsers does provide date and time picker UI :( # other browsers does provide date and time picker UI :(
...@@ -366,6 +490,12 @@ class RoundEditForm(FlaskForm): ...@@ -366,6 +490,12 @@ class RoundEditForm(FlaskForm):
pr_tasks_start = MODateTimeField("Čas zveřejnění úloh pro dozor", validators=[validators.Optional()]) 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()]) 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()]) 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_mode = wtforms.SelectField("Výsledková listina", choices=db.RoundScoreMode.choices(), coerce=db.RoundScoreMode.coerce)
score_winner_limit = IntegerField( score_winner_limit = IntegerField(
"Hranice bodů pro vítěze", validators=[validators.Optional()], "Hranice bodů pro vítěze", validators=[validators.Optional()],
...@@ -383,7 +513,11 @@ def org_round_edit(id: int): ...@@ -383,7 +513,11 @@ def org_round_edit(id: int):
sess = db.get_session() sess = db.get_session()
round, rr = get_round_rr(id, Right.manage_round, True) round, rr = get_round_rr(id, Right.manage_round, True)
if round.is_subround():
form = SubroundEditForm(obj=round)
else:
form = RoundEditForm(obj=round) form = RoundEditForm(obj=round)
if form.validate_on_submit(): if form.validate_on_submit():
form.populate_obj(round) form.populate_obj(round)
......
{% extends "base.html" %} {% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %} {% 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 %} {% block breadcrumbs %}
{{ contest_breadcrumbs(round=round) }} {{ contest_breadcrumbs(round=round) }}
{% endblock %} {% endblock %}
...@@ -12,10 +12,14 @@ ...@@ -12,10 +12,14 @@
<tr><td>Ročník<td>{{ round.year }} <tr><td>Ročník<td>{{ round.year }}
<tr><td>Kategorie<td>{{ round.category }} <tr><td>Kategorie<td>{{ round.category }}
<tr><td>Pořadí<td>{{ round.seq }} <tr><td>Pořadí<td>{{ round.seq }}
<tr><td>Název<td>{{ round.name }}
<tr><td>Oblast<td>{{ level_names[round.level] }} <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 %} <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 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>Účastníci odevzdávají do<td>{{ round.ct_submit_end|timeformat }}
<tr><td>Dozor vidí zadání od<td>{{ round.pr_tasks_start|timeformat }} <tr><td>Dozor vidí zadání od<td>{{ round.pr_tasks_start|timeformat }}
...@@ -30,12 +34,20 @@ ...@@ -30,12 +34,20 @@
{% else %} {% else %}
{% endif %} {% endif %}
{% if not round.is_subround() %}
<tr><td>Výsledková listina<td>{{ round.score_mode.friendly_name() }} <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 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>')) }} <tr><td>Hranice bodů pro úspěšné řešitele<td>{{ round.score_successful_limit|none_value(Markup('<i>nenastaveno</i>')) }}
{% endif %}
</table> </table>
<div class="btn-group"> <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> <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] %} {% 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> <a class="btn btn-primary" href='{{ url_for('org_score', round_id=round.round_id) }}'>Výsledky</a>
...@@ -43,6 +55,7 @@ ...@@ -43,6 +55,7 @@
{% if can_manage_contestants %} {% if can_manage_contestants %}
<a class="btn btn-default" href='{{ url_for('org_round_import', id=round.round_id) }}'>Importovat data</a> <a class="btn btn-default" href='{{ url_for('org_round_import', id=round.round_id) }}'>Importovat data</a>
{% endif %} {% endif %}
{% endif %}
{% if can_manage_round %} {% if can_manage_round %}
<a class="btn btn-default" href='{{ url_for('org_round_edit', id=round.round_id) }}'>Editovat nastavení kola</a> <a class="btn btn-default" href='{{ url_for('org_round_edit', id=round.round_id) }}'>Editovat nastavení kola</a>
{% endif %} {% endif %}
...@@ -50,8 +63,71 @@ ...@@ -50,8 +63,71 @@
<a class="btn btn-default" href='{{ log_url('round', round.round_id) }}'>Historie</a> <a class="btn btn-default" href='{{ log_url('round', round.round_id) }}'>Historie</a>
{% endif %} {% endif %}
</div> </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> <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 %} {% if contests_counts %}
<table class=data> <table class=data>
<thead> <thead>
...@@ -76,14 +152,16 @@ ...@@ -76,14 +152,16 @@
<p>Zatím nebyly založeny v žádné oblasti. <p>Zatím nebyly založeny v žádné oblasti.
{% endif %} {% endif %}
{% if can_add_contest %} {% if not round.is_subround() and can_add_contest %}
<form action="" method="POST" class="form-inline"> <form action="" method="POST" class="form-inline">
{{ form_add_contest.csrf_token() }} {{ form_add_contest.csrf_token() }}
{{ wtf.form_field(form_add_contest.place_code) }} {{ wtf.form_field(form_add_contest.place_code) }}
{{ wtf.form_field(form_add_contest.create_contest) }} {{ wtf.form_field(form_add_contest.create_contest) }}
</form> </form>
{% endif %} {% endif %}
</div>
<div class="box-frame">
<h3>Úlohy</h3> <h3>Úlohy</h3>
{% if tasks %} {% if tasks %}
<table class=data> <table class=data>
...@@ -117,7 +195,7 @@ ...@@ -117,7 +195,7 @@
</div> </div>
{% endif %} {% endif %}
{% if can_handle_submits or can_upload %} {% if can_handle_submits or can_upload %}
<td><dic class="btn-group"> <td><div class="btn-group">
{% if can_handle_submits %} {% 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> <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 %} {% endif %}
...@@ -138,5 +216,6 @@ ...@@ -138,5 +216,6 @@
{% if can_manage_round %} {% 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> <a class="btn btn-primary right-float" href="{{ url_for('org_round_task_new', id=round.round_id) }}">Nová úloha</a>
{% endif %} {% endif %}
</div>
{% endblock %} {% endblock %}
...@@ -171,12 +171,19 @@ nav#main-menu a.active { ...@@ -171,12 +171,19 @@ nav#main-menu a.active {
color:red; color:red;
} }
.form-frame { .form-frame, .box-frame {
padding: 10px; padding: 10px;
border: 1px #ddd solid; border: 1px #ddd solid;
border-radius: 4px 4px; border-radius: 4px 4px;
} }
.box-frame {
margin: 10px 0px;
}
.box-frame > h3 {
margin-top: 5px;
}
.checked_toggle input.toggle:checked ~ .checked_hide { .checked_toggle input.toggle:checked ~ .checked_hide {
display: none; display: none;
} }
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment