Skip to content
Snippets Groups Projects
Select Git revision
  • c60ca2419197bea72217fb002c980b253319b613
  • devel default
  • master
  • fo
  • jirka/typing
  • fo-base
  • mj/submit-images
  • jk/issue-96
  • jk/issue-196
  • honza/add-contestant
  • honza/mr7
  • honza/mrf
  • honza/mrd
  • honza/mra
  • honza/mr6
  • honza/submit-images
  • honza/kolo-vs-soutez
  • jh-stress-test-wip
  • shorten-schools
19 results

org_contest.py

Blame
  • 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,
        )