Select Git revision
-
Martin Mareš authoredMartin Mareš authored
org_round.py 11.11 KiB
from typing import Optional, Tuple
from flask import render_template, g, redirect, url_for, flash, request
import locale
import os
import secrets
from flask_wtf.form import FlaskForm
from sqlalchemy import func
from sqlalchemy.orm import joinedload
import werkzeug.exceptions
import wtforms
from wtforms import validators
import mo
import mo.csv
import mo.db as db
import mo.imports
import mo.rights
import mo.util
from mo.web import app
from mo.web.org_contest import ImportForm, ParticipantsActionForm, ParticipantsFilterForm, get_contestants_query, make_contestant_table
def get_round(id: int) -> db.Round:
round = db.get_session().query(db.Round).get(id)
if not round:
raise werkzeug.exceptions.NotFound()
return round
def get_round_rr(id: int, right_needed: Optional[mo.rights.Right]) -> Tuple[db.Round, mo.rights.Rights]:
round = get_round(id)
rr = mo.rights.Rights(g.user)
rr.get_for_round(round)
if not (right_needed is None or rr.have_right(right_needed)):
raise werkzeug.exceptions.Forbidden()
return round, rr
@app.route('/org/contest/')
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)
class TaskDeleteForm(FlaskForm):
delete_task_id = wtforms.IntegerField()
delete = wtforms.SubmitField('Smazat úlohu')
@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)
can_manage_round = rr.have_right(mo.rights.Right.manage_round)
can_manage_contestants = rr.have_right(mo.rights.Right.manage_contest)
contests = (sess.query(db.Contest)
.filter_by(round=round)
.options(joinedload(db.Contest.place))
.all())
contests.sort(key=lambda c: locale.strxfrm(c.place.name))
sol_counts_q = (
sess.query(db.Solution.task_id, func.count(db.Solution.task_id))
.filter(db.Solution.task_id.in_(
sess.query(db.Task.task_id).filter_by(round=round)
))
)
sol_counts = {}
for task_id, count in sol_counts_q.group_by(db.Solution.task_id).all():
sol_counts[task_id] = count
tasks = sess.query(db.Task).filter_by(round=round).all()
tasks.sort(key=lambda t: t.code)
for task in tasks:
task.sol_count = sol_counts[task.task_id] if task.task_id in sol_counts else 0
form_delete_task = TaskDeleteForm()
if can_manage_contestants and form_delete_task.validate_on_submit():
delete_task = sess.query(db.Task).filter_by(
round_id=id, task_id=form_delete_task.delete_task_id.data
).first()
if not delete_task:
flash('Úloha s daným ID v tomto kole neexistuje', 'danger')
elif sess.query(db.Solution).filter_by(task_id=delete_task.task_id).first() is not None:
flash(f'Úlohu {delete_task.code} nelze smazat, existují řešení vázající se na ní', 'danger')
elif sess.query(db.Paper).filter_by(for_task=delete_task.task_id).first() is not None:
flash(f'Úlohu {delete_task.code} nelze smazat, existují papíry vázající se na ní', 'danger')
elif sess.query(db.PointsHistory).filter_by(task_id=delete_task.task_id).first() is not None:
flash(f'Úlohu {delete_task.code} nelze smazat, existují přidělené body vázající se na ní', 'danger')
else:
sess.delete(delete_task)
mo.util.log(
type=db.LogType.task,
what=delete_task.task_id,
details={'action': 'delete', 'task': db.row2dict(delete_task)},
)
app.logger.info(f"Úloha {delete_task.code} ({delete_task.task_id}) smazána: {db.row2dict(delete_task)}")
sess.commit()
flash(f'Úloha {delete_task.code} úspěšně smazána', 'success')
return redirect(url_for('org_round', id=id))
return render_template(
'org_round.html',
round=round,
contests=contests,
tasks=tasks, form_delete_task=form_delete_task,
level_names=mo.db.place_level_names,
can_manage_round=can_manage_round,
can_manage_contestants=can_manage_contestants,
)
class TaskEditForm(FlaskForm):
code = wtforms.StringField('Kód úlohy')
name = wtforms.StringField('Název úlohy')
submit = wtforms.SubmitField('Uložit')
@app.route('/org/contest/r/<int:id>/task/new', methods=('GET', 'POST'))
def org_round_task_new(id: int):
sess = db.get_session()
round, rr = get_round_rr(id, mo.rights.Right.manage_round)
form = TaskEditForm()
if form.validate_on_submit():
task = db.Task()
task.round = round
form.populate_obj(task)
if sess.query(db.Task).filter_by(round_id=id, code=task.code).first():
flash('Úloha se stejným kódem již v tomto kole existuje', 'danger')
else:
sess.add(task)
sess.flush()
mo.util.log(
type=db.LogType.task,
what=task.task_id,
details={'action': 'add', 'task': db.row2dict(task)},
)
sess.commit()
app.logger.info(f"Úloha {task.code} ({task.task_id}) přidána: {db.row2dict(task)}")
flash('Nová úloha přidána', 'success')
return redirect(url_for('org_round', id=id))
return render_template(
'org_round_task_edit.html',
round=round, task=None, form=form,
)
@app.route('/org/contest/r/<int:id>/task/<int:task_id>/edit', methods=('GET', 'POST'))
def org_round_task_edit(id: int, task_id: int):
sess = db.get_session()
round, rr = get_round_rr(id, mo.rights.Right.manage_round)
task = sess.query(db.Task).get(task_id)
if not task:
raise werkzeug.exceptions.NotFound()
form = TaskEditForm(obj=task)
if form.validate_on_submit():
if sess.query(db.Task).filter(
db.Task.task_id != task_id, db.Task.round_id == id, db.Task.code == form.code.data
).first():
flash('Úloha se stejným kódem již v tomto kole existuje', 'danger')
else:
form.populate_obj(task)
if sess.is_modified(task):
changes = db.get_object_changes(task)
mo.util.log(
type=db.LogType.task,
what=task_id,
details={'action': 'edit', 'changes': changes},
)
sess.commit()
app.logger.info(f"Úloha {task.code} ({task_id}) modifikována, změny: {changes}")
flash('Změny úlohy uloženy', 'success')
else:
flash(u'Žádné změny k uložení', 'info')
return redirect(url_for('org_round', id=id))
return render_template(
'org_round_task_edit.html',
round=round, task=task, form=form,
)
@app.route('/org/contest/r/<int:id>/list', methods=('GET', 'POST'))
def org_round_list(id: int):
round, rr = get_round_rr(id, mo.rights.Right.manage_contest)
format = request.args.get('format', "")
filter = ParticipantsFilterForm(request.args)
filter.validate()
query = get_contestants_query(
round=round,
school=db.get_place_by_code(filter.school.data),
contest_place=db.get_place_by_code(filter.contest_place.data),
participation_place=db.get_place_by_code(filter.participation_place.data),
participation_state=None if filter.participation_state.data == '*' else filter.participation_state.data
)
action_form = ParticipantsActionForm()
if action_form.do_action(round=round, rights=rr, query=query):
# Action happened, redirect
return redirect(request.url)
(count, query) = filter.apply_limits(query, pagesize=50)
# count = query.count()
if format == "":
table = make_contestant_table(query, add_contest_column=True, add_checkbox=True)
return render_template(
'org_round_list.html',
round=round,
table=table,
filter=filter, count=count, action_form=action_form,
)
else:
table = make_contestant_table(query)
return table.send_as(format)
@app.route('/org/contest/r/<int:id>/import', methods=('GET', 'POST'))
def org_round_import(id: int):
round, rr = get_round_rr(id, mo.rights.Right.manage_contest)
form = ImportForm()
errs = []
if form.validate_on_submit():
tmp_name = secrets.token_hex(16) + '.csv'
tmp_path = os.path.join(app.instance_path, 'imports', tmp_name)
form.file.data.save(tmp_path)
imp = mo.imports.Import(g.user)
if imp.import_contest(round, None, tmp_path):
flash(f'Účastníci importováni ({imp.cnt_rows} řádků, založeno {imp.cnt_new_users} uživatelů, {imp.cnt_new_participations} účastí)', 'success')
return redirect(url_for('org_round', id=round.round_id))
else:
flash('Došlo k chybě při importu (detaily níže)', 'danger')
errs = imp.errors
return render_template(
'org_round_import.html',
round=round,
form=form,
errs=errs,
)
class RoundEditForm(FlaskForm):
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 :(
tasks_file = wtforms.StringField("Soubor se zadáním", description="Cesta k ručně uploadovanému souboru", filters=[lambda x: x or None])
ct_tasks_start = wtforms.DateTimeField(
"Čas zveřejnění úloh", validators=[validators.Optional()],
description="Ve formátu 2020-01-01 00:00:00"
)
ct_submit_end = wtforms.DateTimeField(
"Konec odevzdávání pro účastníky", validators=[validators.Optional()],
description="Ve formátu 2020-01-01 00:00:00"
)
pr_submit_end = wtforms.DateTimeField(
"Konec odevzdávání pro dozor", validators=[validators.Optional()],
description="Ve formátu 2020-01-01 00:00:00"
)
submit = wtforms.SubmitField('Uložit')
@app.route('/org/contest/r/<int:id>/edit', methods=('GET', 'POST'))
def org_round_edit(id: int):
sess = db.get_session()
round, rr = get_round_rr(id, mo.rights.Right.manage_round)
form = RoundEditForm(obj=round)
if form.validate_on_submit():
form.populate_obj(round)
if sess.is_modified(round):
changes = db.get_object_changes(round)
app.logger.info(f"Round {id} modified, changes: {changes}")
mo.util.log(
type=db.LogType.round,
what=id,
details={'action': 'edit', 'changes': changes},
)
sess.commit()
flash('Změny kola uloženy', 'success')
else:
flash(u'Žádné změny k uložení', 'info')
return redirect(url_for('org_round', id=id))
return render_template(
'org_round_edit.html',
round=round,
form=form,
)