diff --git a/mo/rights.py b/mo/rights.py index e7873c327954b593fdfcdb7d1fbe96222f3ff13b..b052390dc014cbd972d8666ad26761efd0320133 100644 --- a/mo/rights.py +++ b/mo/rights.py @@ -198,31 +198,6 @@ class Rights: return self.have_right(Right.edit_orgs) return self.have_right(Right.edit_users) - # Interní rozhodovaní o dostupnosti zadání - - def _check_view_statement(self, round: db.Round): - if round.tasks_file is None: - return False - - if self.have_right(Right.manage_round): - # Správce kola může vždy všechno - return True - - # Pokud už soutěž skončila, přístup k zadání má každý org. - # XXX: Rozhodujeme podle stavu kola, nikoliv soutěže! - if round.state in [db.RoundState.grading, db.RoundState.closed]: - return True - - # Od stanoveného času vidí zadání orgové s právem view_statement. - if (self.have_right(Right.view_statement) - and round.state != db.RoundState.preparing - and round.pr_tasks_start is not None - and mo.now >= round.pr_tasks_start): - return True - - # Ve zbylých případech jsme konzervativní a zadání neukazujeme - return False - class RoundRights(Rights): """Práva ke kolu.""" @@ -237,8 +212,11 @@ class RoundRights(Rights): # Metody offer_* testují, zda se má v UI nabízet příslušná funkce. # Skutečnou kontrolu práv dělá až implementace funkce podle stavu soutěže. + def _get_state(self) -> db.RoundState: + return self.round.state + def _is_active(self) -> bool: - return self.round.state not in [db.RoundState.preparing, db.RoundState.closed] + return self._get_state() not in [db.RoundState.preparing, db.RoundState.closed] def offer_upload_solutions(self) -> bool: return (self.have_right(Right.upload_submits) @@ -252,11 +230,47 @@ class RoundRights(Rights): return (self.have_right(Right.manage_contest) or (self.have_right(Right.edit_points) and self._is_active())) - def can_view_statement(self) -> bool: - return self._check_view_statement(self.round) + def can_upload_solutions(self) -> bool: + return (self.have_right(Right.upload_submits) + or self.have_right(Right.upload_solutions) and self._get_state() == db.RoundState.running) + + def can_upload_feedback(self) -> bool: + return (self.have_right(Right.upload_submits) + or self.have_right(Right.upload_feedback) and self._get_state() == db.RoundState.grading) + + def can_edit_points(self) -> bool: + return (self.have_right(Right.edit_points) and self._get_state() == db.RoundState.grading + or self.have_right(Right.manage_contest)) + + def can_create_solutions(self) -> bool: + return self.can_upload_solutions() or self.can_upload_feedback() + + def can_view_statement(self): + round = self.round + if round.tasks_file is None: + return False + + if self.have_right(Right.manage_round): + # Správce kola může vždy všechno + return True + + # Pokud už soutěž skončila, přístup k zadání má každý org. + # XXX: Rozhodujeme podle stavu kola, nikoliv soutěže! + if round.state in [db.RoundState.grading, db.RoundState.closed]: + return True + + # Od stanoveného času vidí zadání orgové s právem view_statement. + if (self.have_right(Right.view_statement) + and round.state != db.RoundState.preparing + and round.pr_tasks_start is not None + and mo.now >= round.pr_tasks_start): + return True + + # Ve zbylých případech jsme konzervativní a zadání neukazujeme + return False -class ContestRights(Rights): +class ContestRights(RoundRights): """Práva k soutěži.""" contest: db.Contest @@ -264,22 +278,10 @@ class ContestRights(Rights): def __repr__(self): ros = " ".join([r.role.name for r in self.user_roles]) ris = " ".join([r.name for r in self.rights]) - return f"ContestRights(uid={self.user.user_id} is_admin={self.user.is_admin} contest=#{self.contest.contest_id} roles=<{ros}> rights=<{ris}>)" - - def can_upload_solutions(self) -> bool: - return (self.have_right(Right.upload_submits) - or self.have_right(Right.upload_solutions) and self.contest.state == db.RoundState.running) - - def can_upload_feedback(self) -> bool: - return (self.have_right(Right.upload_submits) - or self.have_right(Right.upload_feedback) and self.contest.state == db.RoundState.grading) - - def can_edit_points(self) -> bool: - return (self.have_right(Right.edit_points) and self.contest.state == db.RoundState.grading - or self.have_right(Right.manage_contest)) + return f"ContestRights(uid={self.user.user_id} is_admin={self.user.is_admin} round=#{self.round.round_id} contest=#{self.contest.contest_id} roles=<{ros}> rights=<{ris}>)" - def can_view_statement(self) -> bool: - return self._check_view_statement(self.contest.round) + def _get_state(self) -> db.RoundState: + return self.contest.state class Gatekeeper: @@ -347,9 +349,11 @@ class Gatekeeper: """Posbírá role a práva, ale ignoruje omezení rolí na místa a soutěže. Hodí se pro práva k editaci uživatelů apod.""" return self.rights_for() - def rights_for_round(self, round: db.Round, any_place: bool) -> RoundRights: + def rights_for_round(self, round: db.Round, any_place: bool = False, for_place: Optional[db.Place] = None) -> RoundRights: if any_place: place = None + elif for_place: + place = for_place else: place = db.get_root_place() rights = RoundRights() @@ -364,6 +368,7 @@ class Gatekeeper: def rights_for_contest(self, contest: db.Contest, site: Optional[db.Place] = None) -> ContestRights: rights = ContestRights() + rights.round = contest.round rights.contest = contest rights._clone_from(self.rights_for( place=site or contest.place, diff --git a/mo/web/jinja.py b/mo/web/jinja.py index c81543bad49d165a1fd15c3582881a363bd5fd18..04cb8b0c66dc5aeac382aa43f86c59639a642749 100644 --- a/mo/web/jinja.py +++ b/mo/web/jinja.py @@ -9,9 +9,9 @@ import urllib.parse import mo.config as config import mo.db as db +from mo.rights import Right import mo.util_format as util_format from mo.web import app -from mo.web.org_contest import contest_breadcrumbs from mo.web.org_place import place_breadcrumbs from mo.web.util import user_html_flags @@ -47,10 +47,10 @@ app.jinja_env.globals.update(JobState=db.JobState) # Další typy: app.jinja_env.globals.update(Markup=Markup) +app.jinja_env.globals.update(Right=Right) # Vlastní pomocné funkce -app.jinja_env.globals.update(contest_breadcrumbs=contest_breadcrumbs) app.jinja_env.globals.update(place_breadcrumbs=place_breadcrumbs) # Funkce asset_url se přidává v mo.ext.assets @@ -60,6 +60,7 @@ def user_link(u: db.User) -> Markup: return Markup('<a href="{url}">{name}{test}</a>').format(url=user_url(u), name=u.full_name(), test=" (test)" if u.is_test else "") +@app.template_filter() def user_url(u: db.User) -> str: if u.is_admin or u.is_org: return url_for('org_org', id=u.user_id) @@ -69,7 +70,7 @@ def user_url(u: db.User) -> str: @app.template_filter() def pion_link(u: db.User, contest_id: int) -> Markup: - url = url_for('org_contest_user', contest_id=contest_id, user_id=u.user_id) + url = url_for('org_contest_user', ct_id=contest_id, user_id=u.user_id) return Markup('<a href="{url}">{name}{test}</a>').format(url=url, name=u.full_name(), test=" (test)" if u.is_test else "") diff --git a/mo/web/org_contest.py b/mo/web/org_contest.py index 0a17429c8203b2b150d4357ebb562b8ca3db7910..1e130206cf08a386a69bb015a502e68ff5888859 100644 --- a/mo/web/org_contest.py +++ b/mo/web/org_contest.py @@ -1,4 +1,3 @@ -from dataclasses import dataclass from flask import render_template, g, redirect, url_for, flash, request from flask_wtf import FlaskForm import flask_wtf.file @@ -8,10 +7,12 @@ from sqlalchemy import func, and_, select from sqlalchemy.orm import joinedload, aliased from sqlalchemy.orm.query import Query from sqlalchemy.dialects.postgresql import insert as pgsql_insert -from typing import Any, List, Tuple, Optional, Sequence, Dict +from typing import Any, List, Tuple, Optional, Dict import urllib.parse import werkzeug.exceptions import wtforms +import wtforms.validators as validators +from wtforms.widgets.html5 import NumberInput import mo from mo.csv import FileFormat @@ -19,33 +20,184 @@ import mo.config as config import mo.db as db from mo.imports import ImportType, create_import import mo.jobs.submit -from mo.rights import Right, ContestRights +from mo.rights import Right, RoundRights import mo.util from mo.util_format import inflect_number, inflect_by_number from mo.web import app import mo.web.fields as mo_fields import mo.web.util from mo.web.util import PagerForm -from mo.web.table import CellCheckbox, Table, Row, Column, cell_pion_link, cell_place_link, cell_email_link, cell_email_link_flags -import wtforms.validators as validators -from wtforms.widgets.html5 import NumberInput +from mo.web.table import CellCheckbox, Table, Row, Column, cell_pion_link, cell_place_link, cell_email_link_flags -class ImportForm(FlaskForm): - file = flask_wtf.file.FileField("Soubor", render_kw={'autofocus': True}) - typ = wtforms.SelectField( - "Typ dat", - choices=[(x.name, x.friendly_name()) for x in (ImportType.participants, ImportType.proctors, ImportType.judges)], - coerce=ImportType.coerce, - default=ImportType.participants, - ) - fmt = wtforms.SelectField( - "Formát souboru", - choices=FileFormat.choices(), coerce=FileFormat.coerce, - default=FileFormat.cs_csv, - ) - submit = wtforms.SubmitField('Importovat') - get_template = wtforms.SubmitField('Stáhnout šablonu') +class Context: + # Kolo máme vždy + round: db.Round + master_round: db.Round + + # Vypočtená práva: Může být implementováno i jako ContestRights + rights: RoundRights + + # Můžeme mít vybranou soutěž a místo + # Pro nedělená kola platí contest == master_contest. + # Operace s účastníky by měly probíhat vždy přes master_contest. + contest: Optional[db.Contest] = None + master_contest: Optional[db.Contest] = None + site: Optional[db.Place] = None + + # Můžeme se omezit na soutěže v dané oblasti + hier_place: Optional[db.Place] = None + + # Účastník a úloha + pion: Optional[db.Participation] = None + user: Optional[db.User] = None + task: Optional[db.Task] = None + + # IDčka, jak je předáváme do URL + round_id: Optional[int] = None + ct_id: Optional[int] = None + site_id: Optional[int] = None + hier_id: Optional[int] = None + user_id: Optional[int] = None + task_id: Optional[int] = None + + def url_for(self, endpoint: str, **kwargs): + a = {} + round_id = kwargs.get('round_id', self.round_id) + ct_id = kwargs.get('ct_id', self.ct_id) + if ct_id is not None: + a['ct_id'] = ct_id + else: + assert round_id is not None + a['round_id'] = round_id + for arg in ('site_id', 'hier_id', 'user_id', 'task_id'): + val = getattr(self, arg) + if val is not None: + a[arg] = val + for arg, val in kwargs.items(): + a[arg] = val + return url_for(endpoint, **a) + + def url_home(self): + if self.ct_id: + return url_for('org_contest', ct_id=self.ct_id) + else: + return url_for('org_round', round_id=self.round_id, hier_id=self.hier_id) + + def breadcrumbs(self, table: bool = False, action: Optional[str] = None) -> Markup: + elements = [(url_for('org_rounds'), 'Soutěže')] + elements.append((url_for('org_round', round_id=self.round_id), self.round.round_code())) + if self.hier_place: + parents = g.gatekeeper.get_parents(self.hier_place) + parents = sorted(parents, key=lambda p: p.level) + for p in parents[1:]: + elements.append((url_for('org_round', round_id=self.round_id, hier_id=p.place_id), p.name or '???')) + if self.contest: + elements.append((url_for('org_contest', ct_id=self.ct_id), self.contest.place.name or '???')) + if self.site: + elements.append((url_for('org_contest', ct_id=self.ct_id, site_id=self.site_id), f"soutěžní místo {self.site.name}")) + if self.task: + elements.append(( + url_for('org_contest_task', ct_id=self.ct_id, site_id=self.site_id, task_id=self.task_id) if self.contest + else url_for('org_round_task_edit', round_id=self.round_id, task_id=self.task_id), + f"{self.task.code} {self.task.name}" + )) + if self.user: + elements.append((url_for('org_contest_user', ct_id=self.ct_id, user_id=self.user_id), self.user.full_name())) + if table: + if self.contest: + elements.append((url_for('org_generic_list', ct_id=self.ct_id, site=self.site_id), "Seznam účastníků")) + else: + elements.append((url_for('org_generic_list', round_id=self.round_id), "Seznam účastníků")) + if action: + elements.append(('', action)) + + return Markup( + "\n".join([f"<li><a href='{url}'>{name}</a>" for url, name in elements[:-1]]) + + "<li>" + elements[-1][1] + ) + + +def get_context(round_id: Optional[int] = None, + ct_id: Optional[int] = None, + site_id: Optional[int] = None, + hier_id: Optional[int] = None, + user_id: Optional[int] = None, + task_id: Optional[int] = None, + right_needed: Optional[Right] = None, + ) -> Context: + + ctx = Context() + ctx.round_id = round_id + ctx.ct_id = ct_id + ctx.site_id = site_id + ctx.hier_id = hier_id + ctx.user_id = user_id + ctx.task_id = task_id + + sess = db.get_session() + + if site_id is not None: + assert ct_id is not None + ctx.site = db.get_session().query(db.Place).get(site_id) + if not ctx.site: + raise werkzeug.exceptions.NotFound() + + if hier_id is not None: + assert ct_id is None + ctx.hier_place = db.get_session().query(db.Place).get(hier_id) + if not ctx.hier_place: + raise werkzeug.exceptions.NotFound() + + if ct_id is not None: + ctx.contest = (db.get_session().query(db.Contest) + .options(joinedload(db.Contest.place), + joinedload(db.Contest.round), + joinedload(db.Contest.master).joinedload(db.Contest.round)) + .get(ct_id)) + if not ctx.contest: + raise werkzeug.exceptions.NotFound() + ctx.master_contest = ctx.contest.master + ctx.round, ctx.master_round = ctx.contest.round, ctx.master_contest.round + if round_id is not None and ctx.round.round_id != round_id: + raise werkzeug.exceptions.NotFound() + ctx.round_id = ctx.round.round_id + ctx.rights = g.gatekeeper.rights_for_contest(ctx.contest, ctx.site) + else: + ctx.round = sess.query(db.Round).options(joinedload(db.Round.master)).get(round_id) + if not ctx.round: + raise werkzeug.exceptions.NotFound() + if hier_id is not None and ctx.hier_place.level > ctx.round.level: + raise werkzeug.exceptions.NotFound() + ctx.master_round = ctx.round.master + ctx.rights = g.gatekeeper.rights_for_round(ctx.round, for_place=ctx.hier_place) + + # Zkontrolujeme, zda se účastník opravdu účastní soutěže + if user_id is not None: + assert ctx.master_contest is not None + ctx.pion = (sess.query(db.Participation) + .filter_by(user_id=user_id, contest_id=ctx.master_contest.contest_id) + .options(joinedload(db.Participation.place), + joinedload(db.Participation.user)) + .one_or_none()) + if not ctx.pion: + raise werkzeug.exceptions.NotFound() + ctx.user = ctx.pion.user + + # A zda soutěží na zadaném soutěžním místě, je-li určeno + if site_id is not None and site_id != ctx.pion.place_id: + raise werkzeug.exceptions.NotFound() + + # Najdeme úlohu a ověříme, že je součástí kola + if task_id is not None: + ctx.task = sess.query(db.Task).get(task_id) + if not ctx.task or ctx.task.round != ctx.round: + raise werkzeug.exceptions.NotFound() + + if not (right_needed is None or ctx.rights.have_right(right_needed)): + raise werkzeug.exceptions.Forbidden() + + return ctx class ParticipantsFilterForm(PagerForm): @@ -59,6 +211,7 @@ class ParticipantsFilterForm(PagerForm): download_csv = wtforms.SubmitField("↓ CSV") download_tsv = wtforms.SubmitField("↓ TSV") + class ParticipantsActionForm(FlaskForm): action_on = wtforms.RadioField( "Provést akci na", validators=[validators.DataRequired()], @@ -120,20 +273,7 @@ class ParticipantsActionForm(FlaskForm): flash('Data v checkboxech nelze převést na čísla, kontaktujte správce', 'danger') return False - # Check all participations if we can edit them ctants: List[Tuple[db.Participation, Any, Any]] = query.all() - for pion, _, _ in ctants: - u = pion.user - if self.action_on.data == 'checked' and u.user_id not in user_ids: - continue - rr = g.gatekeeper.rights_for_contest(pion.contest) - if not rr.have_right(Right.manage_contest): - flash( - f"Nemáte právo ke správě soutěže v kole {round.round_code_short()} {pion.contest.place.name_locative()} " - + f"(účastník {u.full_name()}). Žádná akce nebyla provedena.", 'danger' - ) - return False - count = 0 unchanged = 0 for pion, _, _ in ctants: @@ -154,6 +294,7 @@ class ParticipantsActionForm(FlaskForm): if self.set_participation_state.data: pion.state = self.participation_state.data elif self.set_participation_place.data: + assert participation_place pion.place_id = participation_place.place_id elif self.set_contest.data: pion.contest_id = contest.contest_id @@ -183,12 +324,14 @@ class ParticipantsActionForm(FlaskForm): 'success' ) elif self.set_participation_place.data: + assert participation_place flash( f'Nastaveno soutěžní místo {participation_place.name} ' + inflect_number(count, 'účastníkovi', 'účastníkům', 'účastníkům'), 'success' ) elif self.set_contest.data: + assert contest_place flash( inflect_number(count, 'účastník přesunut', 'účastníci přesunuti', 'účastníků přesunuto') + f' do soutěže {contest_place.name_locative()}', @@ -204,109 +347,19 @@ class ParticipantsActionForm(FlaskForm): return True -def get_contest(id: int) -> Tuple[db.Contest, db.Contest]: - """Vrací contest a master_contest pro zadané contest_id. - Pro nedělená kola platí contest == master_contest. - Operace s účastníky by měly probíhat vždy přes master_contest.""" - contest = (db.get_session().query(db.Contest) - .options(joinedload(db.Contest.place), - joinedload(db.Contest.round), - joinedload(db.Contest.master).joinedload(db.Contest.round)) - .get(id)) - if not contest: - raise werkzeug.exceptions.NotFound() - return contest, contest.master - - -def get_contest_rr(id: int, right_needed: Optional[Right] = None) -> Tuple[db.Contest, db.Contest, ContestRights]: - """Vrací contest, master_contest a ContestRights objekt pro zadané contest_id. - Pro nedělená kola platí contest == master_contest. - Operace s účastníky by měly probíhat vždy přes master_contest.""" - contest, master_contest = get_contest(id) - - rr = g.gatekeeper.rights_for_contest(contest) - - if not (right_needed is None or rr.have_right(right_needed)): - raise werkzeug.exceptions.Forbidden() - - return contest, master_contest, rr - - -def get_contest_site_rr(id: int, site_id: Optional[int], right_needed: Optional[Right] = None) -> Tuple[db.Contest, db.Contest, Optional[db.Place], ContestRights]: - """Vrací contest, master_contest, optional site a ContestRights objekt pro zadané contest_id a site_id. - Pro nedělená kola platí contest == master_contest. - Operace s účastníky by měly probíhat vždy přes master_contest.""" - if site_id is None: - contest, master_contest, rr = get_contest_rr(id, right_needed) - return contest, master_contest, None, rr - - contest, master_contest = get_contest(id) - site = db.get_session().query(db.Place).get(site_id) - if not site: - raise werkzeug.exceptions.NotFound() - - rr = g.gatekeeper.rights_for_contest(contest, site) - - if not (right_needed is None or rr.have_right(right_needed)): - raise werkzeug.exceptions.Forbidden() - - return contest, master_contest, site, rr - - -def contest_breadcrumbs( - round: Optional[db.Round] = None, contest: Optional[db.Contest] = None, - site: Optional[db.Place] = None, task: Optional[db.Task] = None, - user: Optional[db.User] = None, action: Optional[str] = None, - table: Optional[bool] = False -) -> Markup: - elements = [(url_for('org_rounds'), 'Soutěže')] - round_id = None - if round: - round_id = round.round_id - elements.append((url_for('org_round', id=round_id), round.round_code())) - ct_id = None - if contest: - ct_id = contest.contest_id - elements.append((url_for('org_contest', id=ct_id), contest.place.name)) - site_id = None - if site: - site_id = site.place_id - elements.append((url_for('org_contest', id=ct_id, site_id=site_id), f"soutěžní místo {site.name}")) - if task: - task_id = task.task_id - elements.append(( - url_for('org_contest_task', contest_id=ct_id, site_id=site_id, task_id=task_id) if ct_id - else url_for('org_round_task_edit', id=round_id, task_id=task_id), - f"{task.code} {task.name}" - )) - if user: - user_id = user.user_id - elements.append((url_for('org_contest_user', contest_id=ct_id, user_id=user_id), user.full_name())) - if table: - if contest: - elements.append((url_for('org_contest_list', id=ct_id, site=site_id), "Seznam účastníků")) - else: - elements.append((url_for('org_round_list', id=round_id), "Seznam účastníků")) - if action: - elements.append(('', action)) - - return Markup( - "\n".join([f"<li><a href='{url}'>{name}</a>" for url, name in elements[:-1]]) - + "<li>" + elements[-1][1] - ) - - -@app.route('/org/contest/c/<int:id>') -@app.route('/org/contest/c/<int:id>/site/<int:site_id>/') -def org_contest(id: int, site_id: Optional[int] = None): +@app.route('/org/contest/c/<int:ct_id>/') +@app.route('/org/contest/c/<int:ct_id>/site/<int:site_id>/') +def org_contest(ct_id: int, site_id: Optional[int] = None): sess = db.get_session() - contest, master_contest, site, rr = get_contest_site_rr(id, site_id, None) - round = contest.round - - tasks_subq = sess.query(db.Task.task_id).filter_by(round=contest.round) - pions_subq = sess.query(db.Participation.user_id).filter_by(contest=master_contest) - if site: - pions_subq = pions_subq.filter_by(place=site) + ctx = get_context(ct_id=ct_id, site_id=site_id) + contest = ctx.contest + assert contest + rights = ctx.rights + + tasks_subq = sess.query(db.Task.task_id).filter_by(round=ctx.round) + pions_subq = sess.query(db.Participation.user_id).filter_by(contest=ctx.master_contest) + if ctx.site: + pions_subq = pions_subq.filter_by(place=ctx.site) sol_counts_q = ( sess.query(db.Solution.task_id, func.count(db.Solution.task_id)) .filter( @@ -319,7 +372,7 @@ def org_contest(id: int, site_id: Optional[int] = None): 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 = sess.query(db.Task).filter_by(round=ctx.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 @@ -330,7 +383,7 @@ def org_contest(id: int, site_id: Optional[int] = None): sess.query(db.Place, func.count('*')) .select_from(db.Participation).join(db.Place) .group_by(db.Place) - .filter(db.Participation.contest == master_contest).all() + .filter(db.Participation.contest == ctx.master_contest).all() ) group_contests = contest.get_group_contests(True) @@ -338,27 +391,49 @@ def org_contest(id: int, site_id: Optional[int] = None): return render_template( 'org_contest.html', - contest=contest, group_contests=group_contests, site=site, - rights=sorted(rr.rights, key=lambda r: r.name), - roles=[r.friendly_name() for r in rr.get_roles()], - can_manage=rr.have_right(Right.manage_contest), - can_upload=rr.can_upload_feedback(), - can_edit_points=rr.can_edit_points(), - can_create_solutions=rr.can_upload_feedback() or rr.can_upload_solutions(), - can_view_statement=rr.can_view_statement(), + ctx=ctx, rights=rights, + round=ctx.round, contest=contest, site=ctx.site, + group_contests=group_contests, + rights_list=sorted(rights.rights, key=lambda r: r.name), + roles=[r.friendly_name() for r in rights.get_roles()], tasks=tasks, places_counts=places_counts, ) -def generic_import(round: db.Round, master_round: db.Round, contest: Optional[db.Contest], master_contest: Optional[db.Contest]): - """Společná funkce pro importování do soutěží a kol""" +@app.route('/doc/import') +def doc_import(): + return render_template('doc_import.html') + + +class ImportForm(FlaskForm): + file = flask_wtf.file.FileField("Soubor", render_kw={'autofocus': True}) + typ = wtforms.SelectField( + "Typ dat", + choices=[(x.name, x.friendly_name()) for x in (ImportType.participants, ImportType.proctors, ImportType.judges)], + coerce=ImportType.coerce, + default=ImportType.participants, + ) + fmt = wtforms.SelectField( + "Formát souboru", + choices=FileFormat.choices(), coerce=FileFormat.coerce, + default=FileFormat.cs_csv, + ) + submit = wtforms.SubmitField('Importovat') + get_template = wtforms.SubmitField('Stáhnout šablonu') + + +@app.route('/org/contest/c/<int:ct_id>/import', methods=('GET', 'POST')) +@app.route('/org/contest/r/<int:round_id>/import', methods=('GET', 'POST')) +def org_generic_import(round_id: Optional[int] = None, ct_id: Optional[int] = None): + ctx = get_context(round_id=round_id, ct_id=ct_id, right_needed=Right.manage_contest) + round, contest = ctx.master_round, ctx.master_contest form = ImportForm() errs = [] warnings = [] if form.validate_on_submit(): fmt = form.fmt.data - imp = create_import(user=g.user, type=form.typ.data, fmt=fmt, round=master_round, contest=master_contest) + imp = create_import(user=g.user, type=form.typ.data, fmt=fmt, round=round, contest=contest) if form.submit.data: if form.file.data is not None: file = form.file.data.stream @@ -369,10 +444,7 @@ def generic_import(round: db.Round, master_round: db.Round, contest: Optional[db flash('Soubor neobsahoval žádné řádky s daty', 'danger') else: flash(f'Importováno ({imp.cnt_rows} řádků, založeno {imp.cnt_new_users} uživatelů, {imp.cnt_new_participations} účastí, {imp.cnt_new_roles} rolí)', 'success') - if contest is not None: - return redirect(url_for('org_contest', id=contest.contest_id)) - else: - return redirect(url_for('org_round', id=round.round_id)) + return redirect(ctx.url_home()) else: errs = imp.errors warnings = imp.warnings @@ -387,6 +459,7 @@ def generic_import(round: db.Round, master_round: db.Round, contest: Optional[db return render_template( 'org_generic_import.html', + ctx=ctx, contest=contest, round=round, form=form, @@ -395,28 +468,18 @@ def generic_import(round: db.Round, master_round: db.Round, contest: Optional[db ) -@app.route('/doc/import') -def doc_import(): - return render_template('doc_import.html') - - -@app.route('/org/contest/c/<int:id>/import', methods=('GET', 'POST')) -def org_contest_import(id: int): - contest, master_contest, rr = get_contest_rr(id, Right.manage_contest) - return generic_import( - round=contest.round, master_round=master_contest.round, - contest=contest, master_contest=master_contest - ) - - # URL je explicitně uvedeno v mo.email.contestant_list_url -@app.route('/org/contest/c/<int:id>/participants', methods=('GET', 'POST')) -@app.route('/org/contest/c/<int:id>/site/<int:site_id>/participants', methods=('GET', 'POST')) -@app.route('/org/contest/c/<int:id>/participants/emails', endpoint="org_contest_list_emails") -@app.route('/org/contest/c/<int:id>/site/<int:site_id>/participants/emails', endpoint="org_contest_list_emails") -def org_contest_list(id: int, site_id: Optional[int] = None): - contest, master_contest, site, rr = get_contest_site_rr(id, site_id, Right.view_contestants) - can_edit = rr.have_right(Right.manage_contest) and request.endpoint != 'org_contest_list_emails' +@app.route('/org/contest/c/<int:ct_id>/participants', methods=('GET', 'POST')) +@app.route('/org/contest/c/<int:ct_id>/site/<int:site_id>/participants', methods=('GET', 'POST')) +@app.route('/org/contest/c/<int:ct_id>/participants/emails', endpoint="org_generic_list_emails") +@app.route('/org/contest/c/<int:ct_id>/site/<int:site_id>/participants/emails', endpoint="org_generic_list_emails") +@app.route('/org/contest/r/<int:round_id>/participants', methods=('GET', 'POST')) +@app.route('/org/contest/r/<int:round_id>/participants/emails', endpoint="org_generic_list_emails") +def org_generic_list(round_id: Optional[int] = None, ct_id: Optional[int] = None, site_id: Optional[int] = None): + ctx = get_context(round_id=round_id, ct_id=ct_id, site_id=site_id, right_needed=Right.view_contestants) + round, contest = ctx.master_round, ctx.master_contest + rr = ctx.rights + can_edit = rr.have_right(Right.manage_contest) and request.endpoint != 'org_generic_list_emails' format = request.args.get('format', "") filter = ParticipantsFilterForm(formdata=request.args) @@ -424,7 +487,7 @@ def org_contest_list(id: int, site_id: Optional[int] = None): filter.validate() query = get_contestants_query( - round=master_contest.round, contest=master_contest, site=site, + round=round, contest=contest, site=ctx.site, school=filter.school.place, contest_place=filter.contest_place.place, participation_place=filter.participation_place.place, @@ -434,7 +497,7 @@ def org_contest_list(id: int, site_id: Optional[int] = None): action_form = None if can_edit: action_form = ParticipantsActionForm() - if action_form.do_action(round=contest.round, query=query): + if action_form.do_action(round=round, query=query): # Action happened, redirect return redirect(request.url) @@ -442,23 +505,26 @@ def org_contest_list(id: int, site_id: Optional[int] = None): table = None emails = None mailto_link = None - if request.endpoint == 'org_contest_list_emails': - (emails, mailto_link) = get_contestant_emails(query, - mailto_subject=f'{contest.round.name} {contest.round.category} {contest.place.name_locative()}') + if request.endpoint == 'org_generic_list_emails': + if contest: + subj = f'{contest.round.name} {contest.round.category} {contest.place.name_locative()}' + else: + subj = f'{round.name} kategorie {round.category}' + (emails, mailto_link) = get_contestant_emails(query, mailto_subject=subj) count = len(emails) else: - # (count, query) = filter.apply_limits(query, pagesize=50) - count = db.get_count(query) - table = make_contestant_table(query, master_contest.round, add_checkbox=can_edit) + (count, query) = filter.apply_limits(query, pagesize=50) + table = make_contestant_table(query, round, add_contest_column=(contest is None), add_checkbox=can_edit) return render_template( - 'org_contest_list.html', - contest=contest, site=site, + 'org_generic_list.html', + ctx=ctx, + contest=contest, round=round, site=ctx.site, table=table, emails=emails, mailto_link=mailto_link, filter=filter, count=count, action_form=action_form, ) else: - table = make_contestant_table(query, master_contest.round, is_export=True) + table = make_contestant_table(query, round, is_export=True) return table.send_as(format) @@ -570,91 +636,6 @@ def get_contestant_emails(query: Query, mailto_subject: str = '[OSMO] Zpráva pr return (emails, mailto_link) -@dataclass -class SolutionContext: - contest: db.Contest - master_contest: db.Contest - round: db.Round - master_round: db.Round - pion: Optional[db.Participation] - user: Optional[db.User] - task: Optional[db.Task] - site: Optional[db.Place] - allow_view: bool - allow_upload_solutions: bool - allow_upload_feedback: bool - allow_create_solutions: bool - allow_edit_points: bool - - -def get_solution_context(contest_id: int, user_id: Optional[int], task_id: Optional[int], site_id: Optional[int]) -> SolutionContext: - sess = db.get_session() - - # Nejprve zjistíme, zda existuje soutěž - contest, master_contest = get_contest(contest_id) - round = contest.round - master_round = master_contest.round - - # Najdeme úlohu a ověříme, že je součástí soutěže - if task_id is not None: - task = sess.query(db.Task).get(task_id) - if not task or task.round != round: - raise werkzeug.exceptions.NotFound() - else: - task = None - - site = None - user = None - if user_id is not None: - # Zkontrolujeme, zda se účastník opravdu účastní soutěže - pion = (sess.query(db.Participation) - .filter_by(user_id=user_id, contest_id=master_contest.contest_id) - .options(joinedload(db.Participation.place), - joinedload(db.Participation.user)) - .one_or_none()) - if not pion: - raise werkzeug.exceptions.NotFound() - user = pion.user - - # A zda soutěží na zadaném soutěžním místě, je-li určeno - if site_id is not None and site_id != pion.place_id: - raise werkzeug.exceptions.NotFound() - - # Pokud je uvedeno soutěžní místo, hledáme práva k němu, jinak k soutěži - if site_id is not None: - site = pion.place - else: - pion = None - - if site_id is not None: - site = sess.query(db.Place).get(site_id) - if not site: - raise werkzeug.exceptions.NotFound() - - rr = g.gatekeeper.rights_for_contest(contest, site) - - allow_view = rr.have_right(Right.view_submits) - if not allow_view: - raise werkzeug.exceptions.Forbidden() - - allow_upload_solutions = rr.can_upload_solutions() - allow_upload_feedback = rr.can_upload_feedback() - return SolutionContext( - contest=contest, master_contest=master_contest, - round=round, master_round=master_round, - pion=pion, - user=user, - task=task, - site=site, - # XXX: Potřebujeme tohle všechno? Nechceme spíš vracet rr a nechat každého, ať na něm volá metody? - allow_view=allow_view, - allow_upload_solutions=allow_upload_solutions, - allow_upload_feedback=allow_upload_feedback, - allow_create_solutions=allow_upload_solutions or allow_upload_feedback, - allow_edit_points=rr.can_edit_points(), - ) - - class SubmitForm(FlaskForm): note = wtforms.TextAreaField("Poznámka pro účastníka", description="Viditelná účastníkovi po uzavření kola", render_kw={'autofocus': True}) org_note = wtforms.TextAreaField("Interní poznámka", description="Viditelná jen organizátorům") @@ -674,15 +655,15 @@ class SetFinalForm(FlaskForm): submit_final = wtforms.SubmitField("Prohlásit za finální") -@app.route('/org/contest/c/<int:contest_id>/submit/<int:user_id>/<int:task_id>/', methods=('GET', 'POST')) -@app.route('/org/contest/c/<int:contest_id>/site/<int:site_id>/submit/<int:user_id>/<int:task_id>/', methods=('GET', 'POST')) -def org_submit_list(contest_id: int, user_id: int, task_id: int, site_id: Optional[int] = None): - sc = get_solution_context(contest_id, user_id, task_id, site_id) - assert sc.user is not None - assert sc.task is not None +@app.route('/org/contest/c/<int:ct_id>/submit/<int:user_id>/<int:task_id>/', methods=('GET', 'POST')) +@app.route('/org/contest/c/<int:ct_id>/site/<int:site_id>/submit/<int:user_id>/<int:task_id>/', methods=('GET', 'POST')) +def org_submit_list(ct_id: int, user_id: int, task_id: int, site_id: Optional[int] = None): + ctx = get_context(ct_id=ct_id, site_id=site_id, user_id=user_id, task_id=task_id, right_needed=Right.view_submits) + assert ctx.contest and ctx.user and ctx.task + rights = ctx.rights sess = db.get_session() - self_url = url_for('org_submit_list', contest_id=contest_id, user_id=user_id, task_id=task_id, site_id=site_id) + self_url = ctx.url_for('org_submit_list') # Najdeme řešení úlohy (nemusí existovat) sol = (sess.query(db.Solution) @@ -690,7 +671,7 @@ def org_submit_list(contest_id: int, user_id: int, task_id: int, site_id: Option .one_or_none()) set_final_form: Optional[SetFinalForm] = None - if sol and sc.allow_upload_feedback: + if sol and rights.can_upload_feedback(): set_final_form = SetFinalForm() if set_final_form.validate_on_submit() and set_final_form.submit_final.data: is_submit = set_final_form.type.data == "submit" @@ -737,7 +718,7 @@ def org_submit_list(contest_id: int, user_id: int, task_id: int, site_id: Option return redirect(self_url) form = SubmitForm(obj=sol) - form.points.widget = NumberInput(min=0, max=sc.task.max_points, step=sc.master_round.points_step) # min a max v HTML + form.points.widget = NumberInput(min=0, max=ctx.task.max_points, step=ctx.master_round.points_step) # min a max v HTML if form.validate_on_submit(): if sol and form.delete.data: if sol.final_submit or sol.final_feedback: @@ -747,14 +728,14 @@ def org_submit_list(contest_id: int, user_id: int, task_id: int, site_id: Option sess.delete(sol) mo.util.log( type=db.LogType.participant, - what=sc.user.user_id, + what=ctx.user.user_id, details={ 'action': 'solution-removed', 'task': task_id, }, ) sess.commit() - app.logger.info(f"Řešení úlohy {sc.task.code} od účastníka {sc.user.user_id} smazáno") + app.logger.info(f"Řešení úlohy {ctx.task.code} od účastníka {ctx.user.user_id} smazáno") return redirect(self_url) points = form.points.data @@ -763,28 +744,28 @@ def org_submit_list(contest_id: int, user_id: int, task_id: int, site_id: Option flash('Schází soubor k nahrání, žádné změny nebyly uloženy', 'danger') return redirect(self_url) if points: - error = mo.util.check_points(points, for_task=sc.task, for_round=sc.round) + error = mo.util.check_points(points, for_task=ctx.task, for_round=ctx.round) if error: flash(error, 'danger') return redirect(self_url) - if not sol and (sc.allow_edit_points or sc.allow_upload_solutions or sc.allow_upload_feedback): + if not sol and (rights.can_edit_points() or rights.can_upload_solutions() or rights.can_upload_feedback()): flash('Řešení založeno', 'success') - sol = db.Solution(task=sc.task, user=sc.user) + sol = db.Solution(task=ctx.task, user=ctx.user) sess.add(sol) mo.util.log( type=db.LogType.participant, - what=sc.user.user_id, + what=ctx.user.user_id, details={ 'action': 'solution-created', 'task': task_id, }, ) sess.commit() - app.logger.info(f"Řešení úlohy {sc.task.code} od účastníka {sc.user.user_id} založeno") + app.logger.info(f"Řešení úlohy {ctx.task.code} od účastníka {ctx.user.user_id} založeno") # Edit sol and points - if sol and sc.allow_edit_points: + if sol and rights.can_edit_points(): # Sol edit sol.note = form.note.data sol.org_note = form.org_note.data @@ -794,7 +775,7 @@ def org_submit_list(contest_id: int, user_id: int, task_id: int, site_id: Option if points != sol.points: sol.points = points sess.add(db.PointsHistory( - task=sc.task, + task=ctx.task, participant=sol.user, user=g.user, points_at=mo.now, @@ -806,7 +787,7 @@ def org_submit_list(contest_id: int, user_id: int, task_id: int, site_id: Option changes = db.get_object_changes(sol) mo.util.log( type=db.LogType.participant, - what=sc.user.user_id, + what=ctx.user.user_id, details={ 'action': 'solution-edit', 'task': task_id, @@ -814,22 +795,22 @@ def org_submit_list(contest_id: int, user_id: int, task_id: int, site_id: Option }, ) sess.commit() - app.logger.info(f"Řešení úlohy {sc.task.code} od účastníka {sc.user.user_id} modifikováno, změny: {changes}") + app.logger.info(f"Řešení úlohy {ctx.task.code} od účastníka {ctx.user.user_id} modifikováno, změny: {changes}") - if (form.submit_sol.data and sc.allow_upload_solutions) or (form.submit_fb.data and sc.allow_upload_feedback): + if (form.submit_sol.data and rights.can_upload_solutions()) or (form.submit_fb.data and rights.can_upload_feedback()): file = form.file.data.stream - if sc.allow_upload_solutions and form.submit_sol.data: + if rights.can_upload_solutions() and form.submit_sol.data: type = db.PaperType.solution - elif sc.allow_upload_feedback and form.submit_fb.data: + elif rights.can_upload_feedback() and form.submit_fb.data: type = db.PaperType.feedback else: raise werkzeug.exceptions.Forbidden() - assert sc.task is not None and sc.user is not None - paper = db.Paper(task=sc.task, for_user_obj=sc.user, uploaded_by_obj=g.user, type=type, note=form.file_note.data) + assert ctx.task is not None and ctx.user is not None + paper = db.Paper(task=ctx.task, for_user_obj=ctx.user, uploaded_by_obj=g.user, type=type, note=form.file_note.data) submitter = mo.submit.Submitter() - self_url = url_for('org_submit_list', contest_id=contest_id, user_id=user_id, task_id=task_id, site_id=site_id) + self_url = url_for('org_submit_list', ct_id=ct_id, user_id=user_id, task_id=task_id, site_id=site_id) try: submitter.submit_paper(paper, file.name) @@ -859,7 +840,7 @@ def org_submit_list(contest_id: int, user_id: int, task_id: int, site_id: Option return redirect(self_url) papers = (sess.query(db.Paper) - .filter_by(for_user_obj=sc.user, task=sc.task) + .filter_by(for_user_obj=ctx.user, task=ctx.task) .options(joinedload(db.Paper.uploaded_by_obj)) .order_by(db.Paper.uploaded_at.desc()) .all()) @@ -868,21 +849,21 @@ def org_submit_list(contest_id: int, user_id: int, task_id: int, site_id: Option fb_papers = [p for p in papers if p.type == db.PaperType.feedback] points_history = (sess.query(db.PointsHistory) - .filter_by(task=sc.task, participant=sc.user) + .filter_by(task=ctx.task, participant=ctx.user) .options(joinedload(db.PointsHistory.user)) .order_by(db.PointsHistory.points_at.desc()) .all()) return render_template( 'org_submit_list.html', - sc=sc, + ctx=ctx, rights=rights, solution=sol, sol_papers=sol_papers, fb_papers=fb_papers, points_history=points_history, for_site=(site_id is not None), - paper_link=lambda p: mo.web.util.org_paper_link(sc.contest, sc.site, sc.user, p), - orig_paper_link=lambda p: mo.web.util.org_paper_link(sc.contest, sc.site, sc.user, p, orig=True), + paper_link=lambda p: mo.web.util.org_paper_link(ctx.contest, ctx.site, ctx.user, p), + orig_paper_link=lambda p: mo.web.util.org_paper_link(ctx.contest, ctx.site, ctx.user, p, orig=True), form=form, set_final_form=set_final_form, ) @@ -894,11 +875,11 @@ class SubmitEditForm(FlaskForm): submit = wtforms.SubmitField("Uložit") -@app.route('/org/contest/c/<int:contest_id>/paper/<int:paper_id>/<filename>', endpoint='org_submit_paper') -@app.route('/org/contest/c/<int:contest_id>/site/<int:site_id>/paper/<int:paper_id>/<filename>', endpoint='org_submit_paper') -@app.route('/org/contest/c/<int:contest_id>/paper/orig/<int:paper_id>/<filename>', endpoint='org_submit_paper_orig') -@app.route('/org/contest/c/<int:contest_id>/site/<int:site_id>/paper/orig/<int:paper_id>/<filename>', endpoint='org_submit_paper_orig') -def org_submit_paper(contest_id: int, paper_id: int, filename: str, site_id: Optional[int] = None): +@app.route('/org/contest/c/<int:ct_id>/paper/<int:paper_id>/<filename>', endpoint='org_submit_paper') +@app.route('/org/contest/c/<int:ct_id>/site/<int:site_id>/paper/<int:paper_id>/<filename>', endpoint='org_submit_paper') +@app.route('/org/contest/c/<int:ct_id>/paper/orig/<int:paper_id>/<filename>', endpoint='org_submit_paper_orig') +@app.route('/org/contest/c/<int:ct_id>/site/<int:site_id>/paper/orig/<int:paper_id>/<filename>', endpoint='org_submit_paper_orig') +def org_submit_paper(ct_id: int, paper_id: int, filename: str, site_id: Optional[int] = None): paper = (db.get_session().query(db.Paper) .options(joinedload(db.Paper.task)) # pro task_paper_filename() .get(paper_id)) @@ -908,7 +889,7 @@ def org_submit_paper(contest_id: int, paper_id: int, filename: str, site_id: Opt if not filename.endswith('.pdf'): raise werkzeug.exceptions.NotFound() - get_solution_context(contest_id, paper.for_user, paper.for_task, site_id) + get_context(ct_id=ct_id, user_id=paper.for_user, task_id=paper.for_task, site_id=site_id, right_needed=Right.view_submits) return mo.web.util.send_task_paper(paper, (request.endpoint == 'org_submit_paper_orig')) @@ -941,25 +922,25 @@ class TaskCreateForm(FlaskForm): submit = wtforms.SubmitField("Založit označená řešení") -@app.route('/org/contest/c/<int:contest_id>/task/<int:task_id>/') -@app.route('/org/contest/c/<int:contest_id>/site/<int:site_id>/task/<int:task_id>/') -@app.route('/org/contest/c/<int:contest_id>/task/<int:task_id>/points', methods=('GET', 'POST'), endpoint="org_contest_task_points") -@app.route('/org/contest/c/<int:contest_id>/task/<int:task_id>/create', methods=('GET', 'POST'), endpoint="org_contest_task_create") -@app.route('/org/contest/c/<int:contest_id>/site/<int:site_id>/task/<int:task_id>/create', methods=('GET', 'POST'), endpoint="org_contest_task_create") -def org_contest_task(contest_id: int, task_id: int, site_id: Optional[int] = None): - sc = get_solution_context(contest_id, None, task_id, site_id) - assert sc.task is not None +@app.route('/org/contest/c/<int:ct_id>/task/<int:task_id>/') +@app.route('/org/contest/c/<int:ct_id>/site/<int:site_id>/task/<int:task_id>/') +@app.route('/org/contest/c/<int:ct_id>/task/<int:task_id>/points', methods=('GET', 'POST'), endpoint="org_contest_task_points") +@app.route('/org/contest/c/<int:ct_id>/task/<int:task_id>/create', methods=('GET', 'POST'), endpoint="org_contest_task_create") +@app.route('/org/contest/c/<int:ct_id>/site/<int:site_id>/task/<int:task_id>/create', methods=('GET', 'POST'), endpoint="org_contest_task_create") +def org_contest_task(ct_id: int, task_id: int, site_id: Optional[int] = None): + ctx = get_context(ct_id=ct_id, site_id=site_id, task_id=task_id, right_needed=Right.view_submits) + assert ctx.contest and ctx.task action_create = request.endpoint == "org_contest_task_create" action_points = request.endpoint == "org_contest_task_points" - if action_create and not sc.allow_create_solutions: + if action_create and not ctx.rights.can_create_solutions(): raise werkzeug.exceptions.Forbidden() - if action_points and not sc.allow_edit_points: + if action_points and not ctx.rights.can_edit_points(): raise werkzeug.exceptions.Forbidden() sess = db.get_session() - q = get_solutions_query(sc.task, for_contest=sc.master_contest, for_site=sc.site) + q = get_solutions_query(ctx.task, for_contest=ctx.master_contest, for_site=ctx.site) rows: List[Tuple[db.Participation, db.Solution]] = q.all() rows.sort(key=lambda r: r[0].user.sort_key()) @@ -976,7 +957,7 @@ def org_contest_task(contest_id: int, task_id: int, site_id: Optional[int] = Non if not request.form.get(f"create_sol_{pion.user_id}"): continue # nikdo nežádá o vytvoření - sol = db.Solution(task=sc.task, user=pion.user) + sol = db.Solution(task=ctx.task, user=pion.user) sess.add(sol) mo.util.log( type=db.LogType.participant, @@ -986,7 +967,7 @@ def org_contest_task(contest_id: int, task_id: int, site_id: Optional[int] = Non 'task': task_id, }, ) - app.logger.info(f"Řešení úlohy {sc.task.code} od účastníka {pion.user_id} založeno") + app.logger.info(f"Řešení úlohy {ctx.task.code} od účastníka {pion.user_id} založeno") new_sol_count += 1 if new_sol_count > 0: @@ -996,7 +977,7 @@ def org_contest_task(contest_id: int, task_id: int, site_id: Optional[int] = Non "success") else: flash("Žádné změny k uložení", "info") - return redirect(url_for('org_contest_task', contest_id=contest_id, task_id=task_id, site_id=site_id)) + return redirect(ctx.url_for('org_contest_task')) if action_points: points_form = TaskPointsForm() @@ -1007,7 +988,7 @@ def org_contest_task(contest_id: int, task_id: int, site_id: Optional[int] = Non if sol is None: continue - points, error = mo.util.parse_points(request.form.get(f"points_{sol.user_id}"), for_task=sc.task, for_round=sc.round) + points, error = mo.util.parse_points(request.form.get(f"points_{sol.user_id}"), for_task=ctx.task, for_round=ctx.round) if error: flash(f'{sol.user.first_name} {sol.user.last_name}: {error}', 'danger') ok = False @@ -1016,7 +997,7 @@ def org_contest_task(contest_id: int, task_id: int, site_id: Optional[int] = Non # Save points sol.points = points sess.add(db.PointsHistory( - task=sc.task, + task=ctx.task, participant=sol.user, user=g.user, points_at=mo.now, @@ -1029,13 +1010,13 @@ def org_contest_task(contest_id: int, task_id: int, site_id: Optional[int] = Non flash("Změněny body u " + inflect_number(count, "řešení", "řešení", "řešení"), "success") else: flash("Žádné změny k uložení", "info") - return redirect(url_for('org_contest_task', contest_id=contest_id, task_id=task_id)) + return redirect(ctx.url_for('org_contest_task')) # Count papers for each solution paper_counts = {} for user_id, type, count in ( db.get_session().query(db.Paper.for_user, db.Paper.type, func.count(db.Paper.type)) - .filter_by(task=sc.task) + .filter_by(task=ctx.task) .group_by(db.Paper.for_user, db.Paper.type) .all() ): @@ -1043,8 +1024,10 @@ def org_contest_task(contest_id: int, task_id: int, site_id: Optional[int] = Non return render_template( "org_contest_task.html", - sc=sc, rows=rows, paper_counts=paper_counts, - paper_link=lambda u, p: mo.web.util.org_paper_link(sc.contest, sc.site, u, p), + ctx=ctx, rights=ctx.rights, + round=ctx.round, contest=ctx.contest, + rows=rows, paper_counts=paper_counts, + paper_link=lambda u, p: mo.web.util.org_paper_link(ctx.contest, ctx.site, u, p), points_form=points_form, create_form=create_form, request_form=request.form, ) @@ -1053,33 +1036,34 @@ class ContestSolutionsEditForm(FlaskForm): submit = wtforms.SubmitField("Založit označená řešení") -@app.route('/org/contest/c/<int:id>/solutions', methods=('GET', 'POST')) -@app.route('/org/contest/c/<int:id>/site/<int:site_id>/solutions', methods=('GET', 'POST')) -@app.route('/org/contest/c/<int:id>/solutions/edit', methods=('GET', 'POST'), endpoint="org_contest_solutions_edit") -@app.route('/org/contest/c/<int:id>/site/<int:site_id>/solutions/edit', methods=('GET', 'POST'), endpoint="org_contest_solutions_edit") -def org_contest_solutions(id: int, site_id: Optional[int] = None): - sc = get_solution_context(id, None, None, site_id) +@app.route('/org/contest/c/<int:ct_id>/solutions', methods=('GET', 'POST')) +@app.route('/org/contest/c/<int:ct_id>/site/<int:site_id>/solutions', methods=('GET', 'POST')) +@app.route('/org/contest/c/<int:ct_id>/solutions/edit', methods=('GET', 'POST'), endpoint="org_contest_solutions_edit") +@app.route('/org/contest/c/<int:ct_id>/site/<int:site_id>/solutions/edit', methods=('GET', 'POST'), endpoint="org_contest_solutions_edit") +def org_contest_solutions(ct_id: int, site_id: Optional[int] = None): sess = db.get_session() + ctx = get_context(ct_id=ct_id, site_id=site_id, right_needed=Right.view_submits) + assert ctx.contest edit_action = request.endpoint == "org_contest_solutions_edit" - if edit_action and not sc.allow_create_solutions: + if edit_action and not ctx.rights.can_create_solutions(): raise werkzeug.exceptions.Forbidden() - pions_subq = sess.query(db.Participation.user_id).filter_by(contest=sc.master_contest) - if sc.site: - pions_subq = pions_subq.filter_by(place=sc.site) + pions_subq = sess.query(db.Participation.user_id).filter_by(contest=ctx.master_contest) + if ctx.site: + pions_subq = pions_subq.filter_by(place=ctx.site) pions_subq = pions_subq.subquery() pions = (sess.query(db.Participation) .filter( - db.Participation.contest == sc.master_contest, + db.Participation.contest == ctx.master_contest, db.Participation.user_id.in_(pions_subq), ).options(joinedload(db.Participation.user)) .all()) pions.sort(key=lambda p: p.user.sort_key()) - tasks_subq = sess.query(db.Task.task_id).filter_by(round=sc.round).subquery() + tasks_subq = sess.query(db.Task.task_id).filter_by(round=ctx.round).subquery() tasks = (sess.query(db.Task) - .filter_by(round=sc.round) + .filter_by(round=ctx.round) .order_by(db.Task.code) .all()) @@ -1139,13 +1123,14 @@ def org_contest_solutions(id: int, site_id: Optional[int] = None): "success") else: flash("Žádné změny k uložení", "info") - return redirect(url_for('org_contest_solutions', id=id, site_id=site_id)) + return redirect(ctx.url_for('org_contest_solutions')) return render_template( 'org_contest_solutions.html', - contest=sc.contest, site=sc.site, sc=sc, + ctx=ctx, + contest=ctx.contest, site=ctx.site, rights=ctx.rights, pions=pions, tasks=tasks, tasks_sols=task_sols, paper_counts=paper_counts, - paper_link=lambda u, p: mo.web.util.org_paper_link(sc.contest, sc.site, u, p), + paper_link=lambda u, p: mo.web.util.org_paper_link(ctx.contest, ctx.site, u, p), edit_form=edit_form, ) @@ -1200,15 +1185,19 @@ def download_submits(form: DownloadSubmitsForm, round: db.Round, sol_query, pion return True -def generic_batch_download(round: db.Round, contest: Optional[db.Contest], site: Optional[db.Place], task: db.Task): - """Společná funkce pro download submitů/feedbacku do soutěží a kol.""" - +@app.route('/org/contest/c/<int:ct_id>/task/<int:task_id>/download', methods=('GET', 'POST')) +@app.route('/org/contest/c/<int:ct_id>/site/<int:site_id>/task/<int:task_id>/download', methods=('GET', 'POST')) +@app.route('/org/contest/r/<int:round_id>/task/<int:task_id>/download', methods=('GET', 'POST')) +def org_generic_batch_download(task_id: int, round_id: Optional[int] = None, ct_id: Optional[int] = None, site_id: Optional[int] = None): sess = db.get_session() + ctx = get_context(round_id=round_id, ct_id=ct_id, site_id=site_id, task_id=task_id, right_needed=Right.view_submits) + round, contest, site, task = ctx.round, ctx.contest, ctx.site, ctx.task + assert task pion_query = sess.query(db.Participation.user_id).select_from(db.Participation) - if contest is not None: + if contest: pion_query = pion_query.filter_by(contest_id=contest.master_contest_id) - if site is not None: + if site: pion_query = pion_query.filter_by(place=site) else: pion_query = pion_query.join(db.Contest).filter(db.Contest.round_id == round.master_round_id) @@ -1244,6 +1233,7 @@ def generic_batch_download(round: db.Round, contest: Optional[db.Contest], site: return render_template( 'org_generic_batch_download.html', + ctx=ctx, round=round, contest=contest, site=site, task=task, submit_count=submit_count, pion_count=pion_count, @@ -1258,13 +1248,17 @@ class UploadSubmitsForm(FlaskForm): submit = wtforms.SubmitField('Odeslat') -def generic_batch_upload(round: db.Round, contest: Optional[db.Contest], site: Optional[db.Place], task: db.Task, - offer_upload_solutions: bool, offer_upload_feedback: bool): - """Společná funkce pro upload feedbacku do soutěží a kol.""" +@app.route('/org/contest/c/<int:ct_id>/task/<int:task_id>/upload', methods=('GET', 'POST')) +@app.route('/org/contest/c/<int:ct_id>/site/<int:site_id>/task/<int:task_id>/upload', methods=('GET', 'POST')) +@app.route('/org/contest/r/<int:round_id>/task/<int:task_id>/upload', methods=('GET', 'POST')) +def org_generic_batch_upload(task_id: int, round_id: Optional[int] = None, ct_id: Optional[int] = None, site_id: Optional[int] = None): + ctx = get_context(round_id=round_id, ct_id=ct_id, site_id=site_id, task_id=task_id) + round, contest, site, task = ctx.round, ctx.contest, ctx.site, ctx.task + assert task - # Základní kontrola, zda vůbec chceme akci spustit. + # Základní kontrola, zda vůbec chceme akci spustit. Zbytek se kontrole uvnitř jobu. # Zatím neumíme dávkově nahrávat řešení. - if not offer_upload_feedback: + if not ctx.rights.offer_upload_feedback(): raise werkzeug.exceptions.Forbidden() request.custom_max_content_length = mo.config.MAX_BATCH_CONTENT_LENGTH @@ -1279,30 +1273,13 @@ def generic_batch_upload(round: db.Round, contest: Optional[db.Contest], site: O return render_template( 'org_generic_batch_upload.html', + ctx=ctx, round=round, contest=contest, site=site, task=task, max_size=mo.config.MAX_BATCH_CONTENT_LENGTH, form=form, ) -@app.route('/org/contest/c/<int:contest_id>/task/<int:task_id>/download', methods=('GET', 'POST')) -@app.route('/org/contest/c/<int:contest_id>/site/<int:site_id>/task/<int:task_id>/download', methods=('GET', 'POST')) -def org_contest_task_download(contest_id: int, task_id: int, site_id: Optional[int] = None): - sc = get_solution_context(contest_id, None, task_id, site_id) - assert sc.task is not None - return generic_batch_download(round=sc.round, contest=sc.contest, site=sc.site, task=sc.task) - - -@app.route('/org/contest/c/<int:contest_id>/task/<int:task_id>/upload', methods=('GET', 'POST')) -@app.route('/org/contest/c/<int:contest_id>/site/<int:site_id>/task/<int:task_id>/upload', methods=('GET', 'POST')) -def org_contest_task_upload(contest_id: int, task_id: int, site_id: Optional[int] = None): - sc = get_solution_context(contest_id, None, task_id, site_id) - assert sc.task is not None - return generic_batch_upload(round=sc.round, contest=sc.contest, site=sc.site, task=sc.task, - offer_upload_solutions=sc.allow_upload_solutions, - offer_upload_feedback=sc.allow_upload_feedback) - - class BatchPointsForm(FlaskForm): file = flask_wtf.file.FileField("Soubor", render_kw={'autofocus': True}) fmt = wtforms.SelectField( @@ -1315,8 +1292,14 @@ class BatchPointsForm(FlaskForm): get_template = wtforms.SubmitField('Stáhnout šablonu') -def generic_batch_points(round: db.Round, contest: Optional[db.Contest], task: db.Task): - """Společná funkce pro download/upload bodů do soutěží a kol.""" +@app.route('/org/contest/c/<int:ct_id>/task/<int:task_id>/batch-points', methods=('GET', 'POST')) +@app.route('/org/contest/r/<int:round_id>/task/<int:task_id>/batch-points', methods=('GET', 'POST')) +def org_generic_batch_points(task_id: int, round_id: Optional[int] = None, ct_id: Optional[int] = None): + ctx = get_context(round_id=round_id, ct_id=ct_id, task_id=task_id) + round, contest, task = ctx.round, ctx.contest, ctx.task + + if not ctx.rights.can_edit_points(): + raise werkzeug.exceptions.Forbidden() form = BatchPointsForm() errs = [] @@ -1334,10 +1317,7 @@ def generic_batch_points(round: db.Round, contest: Optional[db.Contest], task: d flash('Soubor neobsahoval žádné řádky s daty', 'danger') else: flash(f'Importováno ({imp.cnt_rows} řádků, {imp.cnt_set_points} řešení přebodováno, {imp.cnt_add_sols} založeno a {imp.cnt_del_sols} smazáno)', 'success') - if contest is not None: - return redirect(url_for('org_contest', id=contest.contest_id)) - else: - return redirect(url_for('org_round', id=round.round_id)) + return redirect(ctx.url_home()) else: errs = imp.errors warnings = imp.warnings @@ -1352,6 +1332,7 @@ def generic_batch_points(round: db.Round, contest: Optional[db.Contest], task: d return render_template( 'org_generic_batch_points.html', + ctx=ctx, round=round, contest=contest, task=task, form=form, errs=errs, @@ -1359,37 +1340,27 @@ def generic_batch_points(round: db.Round, contest: Optional[db.Contest], task: d ) -@app.route('/org/contest/c/<int:contest_id>/task/<int:task_id>/batch-points', methods=('GET', 'POST')) -def org_contest_task_batch_points(contest_id: int, task_id: int): - sc = get_solution_context(contest_id, None, task_id, None) - assert sc.task is not None - - if not sc.allow_edit_points: - raise werkzeug.exceptions.Forbidden() - - return generic_batch_points(round=sc.round, contest=sc.contest, task=sc.task) - - -@app.route('/org/contest/c/<int:contest_id>/user/<int:user_id>') -def org_contest_user(contest_id: int, user_id: int): - sc = get_solution_context(contest_id, user_id, None, None) +@app.route('/org/contest/c/<int:ct_id>/user/<int:user_id>') +def org_contest_user(ct_id: int, user_id: int): + ctx = get_context(ct_id=ct_id, user_id=user_id, right_needed=Right.view_contestants) + assert ctx.contest sess = db.get_session() pant = sess.query(db.Participant).filter_by( - user_id=user_id, year=sc.round.year + user_id=user_id, year=ctx.round.year ).options(joinedload(db.Participant.school_place)).one_or_none() if not pant: raise werkzeug.exceptions.NotFound() task_sols = sess.query(db.Task, db.Solution).select_from(db.Task).outerjoin( - db.Solution, and_(db.Solution.task_id == db.Task.task_id, db.Solution.user == sc.user) - ).filter(db.Task.round == sc.round).options( + db.Solution, and_(db.Solution.task_id == db.Task.task_id, db.Solution.user == ctx.user) + ).filter(db.Task.round == ctx.round).options( joinedload(db.Solution.final_submit_obj), joinedload(db.Solution.final_feedback_obj) ).order_by(db.Task.code).all() # Count papers for each task and solution - tasks_subq = sess.query(db.Task.task_id).filter_by(round=sc.round).subquery() + tasks_subq = sess.query(db.Task.task_id).filter_by(round=ctx.round).subquery() paper_counts = {} for task_id, type, count in ( db.get_session().query(db.Paper.for_task, db.Paper.type, func.count(db.Paper.type)) @@ -1403,8 +1374,9 @@ def org_contest_user(contest_id: int, user_id: int): return render_template( 'org_contest_user.html', - sc=sc, pant=pant, task_sols=task_sols, - paper_link=lambda u, p: mo.web.util.org_paper_link(sc.contest, None, u, p), + ctx=ctx, rights=ctx.rights, + pant=pant, task_sols=task_sols, + paper_link=lambda u, p: mo.web.util.org_paper_link(ctx.contest, None, u, p), paper_counts=paper_counts, ) @@ -1420,16 +1392,18 @@ class AdvanceForm(FlaskForm): execute = wtforms.SubmitField('Provést') -@app.route('/org/contest/c/<int:contest_id>/advance', methods=('GET', 'POST')) -def org_contest_advance(contest_id: int): +@app.route('/org/contest/c/<int:ct_id>/advance', methods=('GET', 'POST')) +def org_contest_advance(ct_id: int): sess = db.get_session() conn = sess.connection() - contest, master_contest, rr = get_contest_rr(contest_id, Right.manage_contest) + ctx = get_context(ct_id=ct_id, right_needed=Right.manage_contest) + contest, master_contest = ctx.contest, ctx.master_contest + round = ctx.round + assert contest and master_contest def redirect_back(): - return redirect(url_for('org_contest', id=contest_id)) + return redirect(ctx.url_for('org_contest')) - round = contest.round if round.state != db.RoundState.preparing: flash('Aktuální kolo není ve stavu přípravy', 'danger') return redirect_back() @@ -1519,7 +1493,7 @@ def org_contest_advance(contest_id: int): if inserted: # Opravdu došlo ke vložení really_inserted += 1 - app.logger.info(f'Postup: Založena účast user=#{pp.user_id} contest=#{contest_id} place=#{contest.place_id}') + app.logger.info(f'Postup: Založena účast user=#{pp.user_id} contest=#{ct_id} place=#{contest.place_id}') mo.util.log( type=db.LogType.participant, what=pp.user_id, @@ -1544,6 +1518,7 @@ def org_contest_advance(contest_id: int): return render_template( 'org_contest_advance.html', + ctx=ctx, contest=contest, round=contest.round, prev_round=prev_round, @@ -1561,11 +1536,13 @@ class ContestEditForm(FlaskForm): submit = wtforms.SubmitField('Uložit') -@app.route('/org/contest/c/<int:id>/edit', methods=('GET', 'POST')) -def org_contest_edit(id: int): +@app.route('/org/contest/c/<int:ct_id>/edit', methods=('GET', 'POST')) +def org_contest_edit(ct_id: int): sess = db.get_session() - contest, _, rr = get_contest_rr(id, Right.manage_contest) - round = contest.round + ctx = get_context(ct_id=ct_id, right_needed=Right.manage_contest) + contest = ctx.contest + round = ctx.round + assert contest and round form = ContestEditForm(obj=contest) if round.state != db.RoundState.delegate: @@ -1580,31 +1557,31 @@ def org_contest_edit(id: int): if 'state' in changes and round.state != db.RoundState.delegate: flash("Nastavení kola neumožňuje měnit stav soutěže", "danger") - return redirect(url_for('org_contest', id=id)) + return redirect(url_for('org_contest', ct_id=ct_id)) - app.logger.info(f"Contest #{id} modified, changes: {changes}") + app.logger.info(f"Contest #{ct_id} modified, changes: {changes}") mo.util.log( type=db.LogType.contest, - what=id, + what=ct_id, details={'action': 'edit', 'changes': changes}, ) sess.commit() flash('Změny soutěže uloženy', 'success') else: - flash(u'Žádné změny k uložení', 'info') + flash('Žádné změny k uložení', 'info') - return redirect(url_for('org_contest', id=id)) + return redirect(ctx.url_for('org_contest')) return render_template( 'org_contest_edit.html', + ctx=ctx, round=round, contest=contest, form=form, ) - class ParticipantAddForm(FlaskForm): email = mo_fields.Email(validators=[validators.Required()]) first_name = mo_fields.FirstName(validators=[validators.Optional()]) @@ -1620,17 +1597,19 @@ class ParticipantAddForm(FlaskForm): self.participation_place.description = f'Pokud účastník soutěží někde jinde než {contest.place.name_locative()}, vyplňte <a href="{url_for("org_place", id=contest.place.place_id)}">kód místa</a>. Dozor na tomto místě pak může za účastníka odevzdávat řešení.' -@app.route('/org/contest/c/<int:id>/participants/new', methods=('GET', 'POST')) -@app.route('/org/contest/c/<int:id>/site/<int:site_id>/participants/new', methods=('GET', 'POST')) -def org_contest_add_user(id: int, site_id: Optional[int] = None): - contest, master_contest, site, rr = get_contest_site_rr(id, site_id, right_needed=Right.manage_contest) +@app.route('/org/contest/c/<int:ct_id>/participants/new', methods=('GET', 'POST')) +@app.route('/org/contest/c/<int:ct_id>/site/<int:site_id>/participants/new', methods=('GET', 'POST')) +def org_contest_add_user(ct_id: int, site_id: Optional[int] = None): + ctx = get_context(ct_id=ct_id, site_id=site_id, right_needed=Right.manage_contest) + contest = ctx.master_contest + assert contest form = ParticipantAddForm() if site_id is not None: if not form.is_submitted(): form.participation_place.process_data(site_id) form.participation_place.render_kw = {"readonly": True} - form.set_descriptions(master_contest) + form.set_descriptions(contest) if form.validate_on_submit(): try: @@ -1652,10 +1631,11 @@ def org_contest_add_user(id: int, site_id: Optional[int] = None): flash("Uživatel přihlášen do soutěže.", "info") else: flash("Žádná změna. Uživatel už byl přihlášen.", "info") - return redirect(url_for('org_contest_list', id=id, site_id=site_id)) + return redirect(ctx.url_for('org_generic_list')) return render_template( 'org_contest_add_user.html', - contest=master_contest, site=site, + ctx=ctx, + contest=contest, round=ctx.master_round, site=ctx.site, form=form ) diff --git a/mo/web/org_round.py b/mo/web/org_round.py index 587821c920575090d5133294189a80db888588e7..79bbe48b96f78a028fec1546d0c51f24fa941554 100644 --- a/mo/web/org_round.py +++ b/mo/web/org_round.py @@ -1,5 +1,5 @@ import decimal -from flask import render_template, g, redirect, url_for, flash, request +from flask import render_template, g, redirect, flash, request import locale import flask_wtf.file from flask_wtf.form import FlaskForm @@ -10,7 +10,7 @@ import os from sqlalchemy import func from sqlalchemy.orm import joinedload from sqlalchemy.sql.functions import coalesce -from typing import Optional, Tuple +from typing import Optional import werkzeug.exceptions import wtforms from wtforms import validators, ValidationError @@ -19,35 +19,11 @@ from wtforms.widgets.html5 import NumberInput import mo.config as config import mo.db as db import mo.imports -from mo.rights import Right, RoundRights +from mo.rights import Right import mo.util from mo.web import app import mo.web.fields as mo_fields -from mo.web.org_contest import ParticipantsActionForm, ParticipantsFilterForm, get_contestant_emails, get_contestants_query, make_contestant_table, \ - generic_import, generic_batch_download, generic_batch_upload, generic_batch_points - - -def get_round_rr(id: int, right_needed: Optional[Right], any_place: bool) -> Tuple[db.Round, db.Round, RoundRights]: - """Vrací round, master_round a Rights objekt pro zadané round_id. - Pro nedělená kola platí round == master_round. - Operace s účastníky by měly probíhat vždy přes master_round.""" - round = db.get_session().query(db.Round).options(joinedload(db.Round.master)).get(id) - if not round: - raise werkzeug.exceptions.NotFound() - - rr = g.gatekeeper.rights_for_round(round, any_place) - - if not (right_needed is None or rr.have_right(right_needed)): - raise werkzeug.exceptions.Forbidden() - - return round, round.master, rr - - -def get_task(round: db.Round, task_id: int) -> db.Task: - task = db.get_session().query(db.Task).get(task_id) - if not task or task.round_id != round.round_id: - raise werkzeug.exceptions.NotFound() - return task +from mo.web.org_contest import get_context @app.route('/org/contest/') @@ -181,20 +157,19 @@ def create_subcontests(master_round: db.Round, master_contest: db.Contest): app.logger.info(f"Podsoutěž #{subcontest.contest_id} založena: {db.row2dict(subcontest)}") -@app.route('/org/contest/r/<int:id>/', methods=('GET', 'POST')) -def org_round(id: int): +@app.route('/org/contest/r/<int:round_id>/', methods=('GET', 'POST')) +def org_round(round_id: int): sess = db.get_session() - round, _, rr = get_round_rr(id, None, True) - - can_manage_round = rr.have_right(Right.manage_round) - can_manage_contestants = rr.have_right(Right.manage_contest) + ctx = get_context(round_id=round_id) + round = ctx.round + rights = ctx.rights participants_count = sess.query( db.Participation.contest_id, func.count(db.Participation.user_id).label('count') ).group_by(db.Participation.contest_id).subquery() - # účastníci jsou jen pod master contesty + # Účastníci jsou jen pod master contesty contests_counts = (sess.query( db.Contest, coalesce(participants_count.c.count, 0) @@ -222,30 +197,25 @@ def org_round(id: int): task.sol_count = sol_counts[task.task_id] if task.task_id in sol_counts else 0 form_delete_task = TaskDeleteForm() - if can_manage_round and delete_task(id, form_delete_task): - return redirect(url_for('org_round', id=id)) + if rights.have_right(Right.manage_round) and delete_task(round_id, form_delete_task): + return redirect(ctx.url_for('org_round')) form_add_contest = AddContestForm() form_add_contest.place.label.text = "Nová soutěž " + round.get_level().in_name() if add_contest(round, form_add_contest): - return redirect(url_for('org_round', id=id)) + return redirect(ctx.url_for('org_round')) group_rounds = round.get_group_rounds(True) group_rounds.sort(key=lambda r: r.round_code()) return render_template( 'org_round.html', + ctx=ctx, rights=rights, round=round, group_rounds=group_rounds, - roles=[r.friendly_name() for r in rr.get_roles()], + roles=[r.friendly_name() for r in rights.get_roles()], contests_counts=contests_counts, tasks=tasks, form_delete_task=form_delete_task, form_add_contest=form_add_contest, - can_manage_round=can_manage_round, - can_manage_contestants=can_manage_contestants, - can_handle_submits=rr.have_right(Right.view_submits), - can_upload=rr.offer_upload_feedback(), - can_view_statement=rr.can_view_statement(), - can_add_contest=g.gatekeeper.rights_generic().have_right(Right.add_contest), statement_exists=mo.web.util.task_statement_exists(round), ) @@ -267,18 +237,18 @@ class TaskEditForm(FlaskForm): self.max_points.widget = NumberInput(min=0, step=points_step) -@app.route('/org/contest/r/<int:id>/task/new', methods=('GET', 'POST')) -def org_round_task_new(id: int): +@app.route('/org/contest/r/<int:round_id>/task/new', methods=('GET', 'POST')) +def org_round_task_new(round_id: int): sess = db.get_session() - round, master_round, _ = get_round_rr(id, Right.manage_round, True) + ctx = get_context(round_id=round_id, right_needed=Right.manage_round) - form = TaskEditForm(master_round.points_step) + form = TaskEditForm(ctx.master_round.points_step) if form.validate_on_submit(): task = db.Task() - task.round = round + task.round = ctx.round form.populate_obj(task) - if sess.query(db.Task).filter_by(round_id=id, code=task.code).first(): + if sess.query(db.Task).filter_by(round_id=round_id, code=task.code).first(): flash('Úloha se stejným kódem již v tomto kole existuje', 'danger') else: sess.add(task) @@ -291,28 +261,25 @@ def org_round_task_new(id: int): 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 redirect(ctx.url_for('org_round')) return render_template( 'org_round_task_edit.html', - round=round, task=None, form=form, + ctx=ctx, 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): +@app.route('/org/contest/r/<int:round_id>/task/<int:task_id>/edit', methods=('GET', 'POST')) +def org_round_task_edit(round_id: int, task_id: int): sess = db.get_session() - round, master_round, _ = get_round_rr(id, Right.manage_round, True) - - task = sess.query(db.Task).get(task_id) - # FIXME: Check contest! - if not task: - raise werkzeug.exceptions.NotFound() + ctx = get_context(round_id=round_id, task_id=task_id, right_needed=Right.manage_round) + task = ctx.task + assert task - form = TaskEditForm(master_round.points_step, obj=task) + form = TaskEditForm(ctx.master_round.points_step, 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 + db.Task.task_id != task_id, db.Task.round_id == round_id, db.Task.code == form.code.data ).first(): flash('Úloha se stejným kódem již v tomto kole existuje', 'danger') else: @@ -329,93 +296,16 @@ def org_round_task_edit(id: int, task_id: int): 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') + flash('Žádné změny k uložení', 'info') - return redirect(url_for('org_round', id=id)) + return redirect(ctx.url_for('org_round', task_id=None)) return render_template( 'org_round_task_edit.html', - round=round, task=task, form=form, + ctx=ctx, form=form, ) -@app.route('/org/contest/r/<int:round_id>/task/<int:task_id>/download', methods=('GET', 'POST')) -def org_round_task_download(round_id: int, task_id: int): - round, _, _ = get_round_rr(round_id, Right.view_submits, False) - task = get_task(round, task_id) - return generic_batch_download(round=round, contest=None, site=None, task=task) - - -@app.route('/org/contest/r/<int:round_id>/task/<int:task_id>/upload', methods=('GET', 'POST')) -def org_round_task_upload(round_id: int, task_id: int): - round, _, rr = get_round_rr(round_id, Right.view_submits, False) - task = get_task(round, task_id) - return generic_batch_upload(round=round, contest=None, site=None, task=task, - offer_upload_solutions=rr.offer_upload_solutions(), - offer_upload_feedback=rr.offer_upload_feedback()) - - -@app.route('/org/contest/r/<int:round_id>/task/<int:task_id>/batch-points', methods=('GET', 'POST')) -def org_round_task_batch_points(round_id: int, task_id: int): - round, _, _ = get_round_rr(round_id, Right.edit_points, True) - task = get_task(round, task_id) - return generic_batch_points(round=round, contest=None, task=task) - - -@app.route('/org/contest/r/<int:id>/list', methods=('GET', 'POST')) -@app.route('/org/contest/r/<int:id>/list/emails', endpoint="org_round_list_emails") -def org_round_list(id: int): - round, master_round, rr = get_round_rr(id, Right.view_contestants, True) - can_edit = rr.have_right(Right.manage_round) and request.endpoint != 'org_round_list_emails' - format = request.args.get('format', "") - - filter = ParticipantsFilterForm(formdata=request.args) - filter.validate() - query = get_contestants_query( - round=master_round, - school=filter.school.place, - contest_place=filter.contest_place.place, - participation_place=filter.participation_place.place, - participation_state=mo.util.star_is_none(filter.participation_state.data), - ) - - action_form = None - if can_edit: - action_form = ParticipantsActionForm() - if action_form.do_action(round=master_round, query=query): - # Action happened, redirect - return redirect(request.url) - - if format == "": - table = None - emails = None - mailto_link = None - if request.endpoint == 'org_round_list_emails': - (emails, mailto_link) = get_contestant_emails(query, - mailto_subject=f'{round.name} kategorie {round.category}') - count = len(emails) - else: - (count, query) = filter.apply_limits(query, pagesize=50) - # count = db.get_count(query) - table = make_contestant_table(query, round, add_contest_column=True, add_checkbox=True) - - return render_template( - 'org_round_list.html', - round=round, - table=table, emails=emails, mailto_link=mailto_link, - filter=filter, count=count, action_form=action_form, - ) - else: - table = make_contestant_table(query, round, is_export=True) - return table.send_as(format) - - -@app.route('/org/contest/r/<int:id>/import', methods=('GET', 'POST')) -def org_round_import(id: int): - round, master_round, rr = get_round_rr(id, Right.manage_contest, True) - return generic_import(round, master_round, None, None) - - class RoundEditForm(FlaskForm): _for_round: Optional[db.Round] = None @@ -469,10 +359,11 @@ class RoundEditForm(FlaskForm): self.abstract_validate_time_order(field) -@app.route('/org/contest/r/<int:id>/edit', methods=('GET', 'POST')) -def org_round_edit(id: int): +@app.route('/org/contest/r/<int:round_id>/edit', methods=('GET', 'POST')) +def org_round_edit(round_id: int): sess = db.get_session() - round, _, rr = get_round_rr(id, Right.manage_round, True) + ctx = get_context(round_id=round_id, right_needed=Right.manage_round) + round = ctx.round form = RoundEditForm(obj=round) form._for_round = round @@ -490,10 +381,10 @@ def org_round_edit(id: int): if sess.is_modified(round): changes = db.get_object_changes(round) - app.logger.info(f"Round #{id} modified, changes: {changes}") + app.logger.info(f"Round #{round_id} modified, changes: {changes}") mo.util.log( type=db.LogType.round, - what=id, + what=round_id, details={'action': 'edit', 'changes': changes}, ) @@ -512,26 +403,27 @@ def org_round_edit(id: int): sess.commit() flash('Změny kola uloženy', 'success') else: - flash(u'Žádné změny k uložení', 'info') + flash('Žádné změny k uložení', 'info') - return redirect(url_for('org_round', id=id)) + return redirect(ctx.url_for('org_round')) return render_template( 'org_round_edit.html', + ctx=ctx, round=round, form=form, ) -@app.route('/org/contest/r/<int:id>/task-statement/zadani.pdf') -def org_task_statement(id: int): - round, _, rr = get_round_rr(id, None, True) +@app.route('/org/contest/r/<int:round_id>/task-statement/zadani.pdf') +def org_task_statement(round_id: int): + ctx = get_context(round_id=round_id) - if not rr.can_view_statement(): + if not ctx.rights.can_view_statement(): app.logger.warn(f'Organizátor #{g.user.user_id} chce zadání, na které nemá právo') raise werkzeug.exceptions.Forbidden() - return mo.web.util.send_task_statement(round) + return mo.web.util.send_task_statement(ctx.round) class StatementEditForm(FlaskForm): @@ -540,18 +432,19 @@ class StatementEditForm(FlaskForm): delete = wtforms.SubmitField('Smazat') -@app.route('/org/contest/r/<int:id>/task-statement/edit', methods=('GET', 'POST')) -def org_edit_statement(id: int): +@app.route('/org/contest/r/<int:round_id>/task-statement/edit', methods=('GET', 'POST')) +def org_edit_statement(round_id: int): sess = db.get_session() - round, _, rr = get_round_rr(id, Right.manage_round, True) + ctx = get_context(round_id=round_id, right_needed=Right.manage_round) + round = ctx.round def log_changes(): if sess.is_modified(round): changes = db.get_object_changes(round) - app.logger.info(f"Kolo #{id} změněno, změny: {changes}") + app.logger.info(f"Kolo #{round_id} změněno, změny: {changes}") mo.util.log( type=db.LogType.round, - what=id, + what=round_id, details={'action': 'edit', 'changes': changes}, ) @@ -572,7 +465,7 @@ def org_edit_statement(id: int): log_changes() sess.commit() flash('Zadání nahráno', 'success') - return redirect(url_for('org_round', id=id)) + return redirect(ctx.url_for('org_round')) else: flash('Vyberte si prosím soubor', 'danger') if form.delete.data: @@ -580,10 +473,11 @@ def org_edit_statement(id: int): log_changes() sess.commit() flash('Zadání smazáno', 'success') - return redirect(url_for('org_round', id=id)) + return redirect(ctx.url_for('org_round')) return render_template( 'org_edit_statement.html', + ctx=ctx, round=round, form=form, ) @@ -605,38 +499,39 @@ class MessageRemoveForm(FlaskForm): message_remove = wtforms.SubmitField() -@app.route('/org/contest/r/<int:id>/messages/', methods=('GET', 'POST')) -def org_round_messages(id: int): +@app.route('/org/contest/r/<int:round_id>/messages/', methods=('GET', 'POST')) +def org_round_messages(round_id: int): sess = db.get_session() - round, _, rr = get_round_rr(id, None, True) + ctx = get_context(round_id=round_id) + round = ctx.round if not round.has_messages: flash('Toto kolo nemá aktivní zprávičky pro účastníky, aktivujte je v nastavení kola', 'warning') - return redirect(url_for('org_round', id=id)) + return redirect(ctx.url_for('org_round')) - messages = sess.query(db.Message).filter_by(round_id=id).order_by(db.Message.created_at).all() + messages = sess.query(db.Message).filter_by(round_id=round_id).order_by(db.Message.created_at).all() add_form: Optional[MessageAddForm] = None remove_form: Optional[MessageRemoveForm] = None preview: Optional[db.Message] = None - if rr.have_right(Right.manage_round): + if ctx.rights.have_right(Right.manage_round): add_form = MessageAddForm() remove_form = MessageRemoveForm() if remove_form.validate_on_submit() and remove_form.message_remove.data: msg = sess.query(db.Message).get(remove_form.message_id.data) - if not msg or msg.round_id != id: + if not msg or msg.round_id != round_id: raise werkzeug.exceptions.NotFound() sess.delete(msg) sess.commit() - app.logger.info(f"Zprávička pro kolo {id} odstraněna: {db.row2dict(msg)}") + app.logger.info(f"Zprávička pro kolo {round_id} odstraněna: {db.row2dict(msg)}") flash('Zprávička odstraněna', 'success') - return redirect(url_for('org_round_messages', id=id)) + return redirect(ctx.url_for('org_round_messages')) if add_form.validate_on_submit(): msg = db.Message( - round_id=id, + round_id=round_id, created_by=g.user.user_id, created_at=mo.now, ) @@ -651,14 +546,15 @@ def org_round_messages(id: int): elif add_form.submit.data: sess.add(msg) sess.commit() - app.logger.info(f"Vložena nová zprávička pro kolo {id}: {db.row2dict(msg)}") + app.logger.info(f"Vložena nová zprávička pro kolo {round_id}: {db.row2dict(msg)}") flash('Zprávička úspěšně vložena', 'success') - return redirect(url_for('org_round_messages', id=id)) + return redirect(ctx.url_for('org_round_messages')) return render_template( 'org_round_messages.html', - round=round, rr=rr, messages=messages, + ctx=ctx, + round=round, messages=messages, add_form=add_form, remove_form=remove_form, preview=preview, ) diff --git a/mo/web/org_score.py b/mo/web/org_score.py index 62932ab90a2714e90624d266ab31a5be971d14e4..331f9ddfd69b559f6698255b5ebba3a90059e05f 100644 --- a/mo/web/org_score.py +++ b/mo/web/org_score.py @@ -9,6 +9,7 @@ import mo.db as db from mo.rights import Right from mo.score import Score from mo.web import app +from mo.web.org_contest import get_context from mo.web.table import Cell, CellLink, Column, Row, Table, cell_pion_link from mo.util_format import format_decimal @@ -74,33 +75,17 @@ class SolPointsCell(Cell): @app.route('/org/contest/r/<int:round_id>/score') -@app.route('/org/contest/c/<int:contest_id>/score') -def org_score(round_id: Optional[int] = None, contest_id: Optional[int] = None): - if round_id is None and contest_id is None: - raise werkzeug.exceptions.BadRequest() - if round_id is not None and contest_id is not None: - raise werkzeug.exceptions.BadRequest() +@app.route('/org/contest/c/<int:ct_id>/score') +def org_score(round_id: Optional[int] = None, ct_id: Optional[int] = None): + ctx = get_context(round_id=round_id, ct_id=ct_id) + contest = ctx.contest + round = ctx.round format = request.args.get('format', "") sess = db.get_session() - if round_id: - contest = None - round = sess.query(db.Round).options( - joinedload(db.Round.master) - ).get(round_id) - if not round: - raise werkzeug.exceptions.NotFound() - rr = g.gatekeeper.rights_for_round(round, True) - else: - contest = sess.query(db.Contest).options( - joinedload(db.Contest.round).joinedload(db.Round.master) - ).get(contest_id) - if not contest: - raise werkzeug.exceptions.NotFound() - round = contest.round - rr = g.gatekeeper.rights_for_contest(contest) - - if not rr.have_right(Right.view_submits): + + # FIXME + if not ctx.rights.have_right(Right.view_submits): raise werkzeug.exceptions.Forbidden() score = Score(round.master, contest) @@ -127,7 +112,7 @@ def org_score(round_id: Optional[int] = None, contest_id: Optional[int] = None): columns.append(Column(key='participant', name='ucastnik', title='Účastník')) if is_export: columns.append(Column(key='email', name='email')) - if not contest_id: + if not ct_id: columns.append(Column(key='contest', name='oblast', title=round.get_level().name.title())) if is_export: columns.append(Column(key='pion_place', name='soutezni_misto')) @@ -140,12 +125,12 @@ def org_score(round_id: Optional[int] = None, contest_id: Optional[int] = None): if contest: local_ct_id = subcontest_id_map[(task.round_id, contest.master_contest_id)] title = '<a href="{}">{}</a>'.format( - url_for('org_contest_task', contest_id=local_ct_id, task_id=task.task_id), + url_for('org_contest_task', ct_id=local_ct_id, task_id=task.task_id), task.code ) - if rr.can_edit_points(): + if ctx.rights.can_edit_points(): title += ' <a href="{}" title="Editovat body" class="icon">✎</a>'.format( - url_for('org_contest_task_points', contest_id=local_ct_id, task_id=task.task_id), + url_for('org_contest_task_points', ct_id=local_ct_id, task_id=task.task_id), ) columns.append(Column(key=f'task_{task.task_id}', name=task.code, title=title)) columns.append(Column(key='total_points', name='celkove_body', title='Celkové body')) @@ -177,7 +162,7 @@ def org_score(round_id: Optional[int] = None, contest_id: Optional[int] = None): 'user': user, 'email': user.email, 'participant': cell_pion_link(user, local_pion_ct_id, user.full_name()), - 'contest': CellLink(pion.contest.place.name or "?", url_for('org_contest', id=pion.contest_id)), + 'contest': CellLink(pion.contest.place.name or "?", url_for('org_contest', ct_id=pion.contest_id)), 'pion_place': pion.place.name, 'school': CellLink(school.name or "?", url_for('org_place', id=school.place_id)), 'grade': pant.grade, @@ -214,6 +199,7 @@ def org_score(round_id: Optional[int] = None, contest_id: Optional[int] = None): if format == "": return render_template( 'org_score.html', + ctx=ctx, contest=contest, round=round, tasks=tasks, table=table, messages=messages, group_rounds=group_rounds, diff --git a/mo/web/table.py b/mo/web/table.py index 72cbbcd14fdf0c7c501ccdb610dcbc6d52ec3543..37d61d86bc30c78683bf59f5d4cd4b38f8ee2000 100644 --- a/mo/web/table.py +++ b/mo/web/table.py @@ -210,7 +210,7 @@ def cell_user_link(user: db.User, text: str) -> CellLink: def cell_pion_link(user: db.User, contest_id: int, text: str) -> CellLink: - return CellLink(text, url_for('org_contest_user', contest_id=contest_id, user_id=user.user_id)) + return CellLink(text, url_for('org_contest_user', ct_id=contest_id, user_id=user.user_id)) def cell_place_link(place: db.Place, text: str) -> CellLink: diff --git a/mo/web/templates/org_contest.html b/mo/web/templates/org_contest.html index f78ea9c2fbaaf0635885c1abef6ba9f3a06d6c0a..0042d9ede74b752ee18fabeee5c614b2f87fbf78 100644 --- a/mo/web/templates/org_contest.html +++ b/mo/web/templates/org_contest.html @@ -2,13 +2,17 @@ {% set round = contest.round %} {% set state = contest.state %} {% set ct_state = contest.ct_state() %} -{% set site_id = site.place_id if site else None %} +{% set can_manage = rights.have_right(Right.manage_contest) %} +{% set can_upload = rights.can_upload_feedback() %} +{% set can_edit_points = rights.can_edit_points() %} +{% set can_create_solutions = rights.can_upload_feedback() or rights.can_upload_solutions() %} +{% set can_view_statement = rights.can_view_statement() %} {% block title %} {{ round.round_code() }}: {% if site %}soutěžní místo {{ site.name }}{% else %}{{ contest.place.name }}{% endif %} {% endblock %} {% block breadcrumbs %} -{{ contest_breadcrumbs(round=round, contest=contest, site=site) }} +{{ ctx.breadcrumbs() }} {% endblock %} {% block body %} @@ -27,7 +31,7 @@ {% if group_contests|length > 1 %} <tr><td>Soutěže ve skupině kol:<td> {% for c in group_contests %} - {% if c == contest %}<i>{% else %}<a href="{{ url_for('org_contest', id=c.contest_id) }}">{% endif %} + {% if c == contest %}<i>{% else %}<a href="{{ url_for('org_contest', ct_id=c.contest_id) }}">{% endif %} {{ c.round.round_code() }}: {% if site %}soutěžní místo {{ site.name }}{% else %}{{ contest.place.name }}{% endif %} {% if c == contest %} (tato soutěž)</i>{% else %}</a>{% endif %}<br> {% endfor %} @@ -35,7 +39,7 @@ <tr><td>Zadání<td> {% if round.tasks_file %} {% if can_view_statement %} - <a href='{{ url_for('org_task_statement', id=round.round_id) }}'>stáhnout</a> + <a href='{{ ctx.url_for('org_task_statement', ct_id=None) }}'>stáhnout</a> {% else %} není dostupné {% endif %} @@ -45,28 +49,28 @@ </table> <div class="btn-group"> - <a class="btn btn-primary" href='{{ url_for('org_contest_list', id=contest.contest_id, site_id=site_id) }}'>Seznam účastníků</a> + <a class="btn btn-primary" href='{{ ctx.url_for('org_generic_list') }}'>Seznam účastníků</a> {% if state != RoundState.preparing %} - <a class="btn btn-primary" href='{{ url_for('org_contest_solutions', id=contest.contest_id, site_id=site_id) }}'>Odevzdaná řešení</a> + <a class="btn btn-primary" href='{{ ctx.url_for('org_contest_solutions') }}'>Odevzdaná řešení</a> {% endif %} {% if can_manage and site %} - <a class="btn btn-default" href="{{ url_for('org_contest_add_user', id=contest.contest_id, site_id=site_id) }}">Přidat účastníka</a> + <a class="btn btn-default" href="{{ ctx.url_for('org_contest_add_user') }}">Přidat účastníka</a> {% endif %} {% if not site %} {% if state in [RoundState.grading, RoundState.closed] %} - <a class="btn btn-primary" href='{{ url_for('org_score', contest_id=contest.contest_id) }}'>Výsledky</a> + <a class="btn btn-primary" href='{{ ctx.url_for('org_score') }}'>Výsledky</a> {% endif %} {% if state == RoundState.preparing and round.seq > 1 %} - <a class="btn btn-primary" href='{{ url_for('org_contest_advance', contest_id=contest.contest_id) }}'>Postup z minulého kola</a> + <a class="btn btn-primary" href='{{ ctx.url_for('org_contest_advance') }}'>Postup z minulého kola</a> {% endif %} {% if can_manage %} - <a class="btn btn-default" href='{{ url_for('org_contest_import', id=contest.contest_id) }}'>Importovat data</a> + <a class="btn btn-default" href='{{ ctx.url_for('org_generic_import') }}'>Importovat data</a> {% endif %} {% if can_manage and not site %} - <a class="btn btn-default" href='{{ url_for('org_contest_edit', id=contest.contest_id) }}'>Nastavení</a> + <a class="btn btn-default" href='{{ ctx.url_for('org_contest_edit') }}'>Nastavení</a> {% endif %} {% if g.user.is_admin %} - <a class="btn btn-default" href="{{ log_url('contest', contest.contest_id) }}">Historie</a> + <a class="btn btn-default" href="{{ log_url('contest', ctx.ct_id) }}">Historie</a> {% endif %} {% endif %} </div> @@ -80,12 +84,12 @@ </thead> {% for (place, count) in places_counts %} <tr> - <td><a href="{{ url_for('org_contest', id=contest.contest_id, site_id=place.place_id) }}">{{ place.name }}</a> + <td><a href="{{ ctx.url_for('org_contest', site_id=place.place_id) }}">{{ place.name }}</a> <td>{{ count }} <td><div class="btn-group"> - <a class="btn btn-xs btn-primary" href="{{ url_for('org_contest', id=contest.contest_id, site_id=place.place_id) }}">Detail</a> + <a class="btn btn-xs btn-primary" href="{{ ctx.url_for('org_contest', site_id=place.place_id) }}">Detail</a> {% if can_manage %} - <a class="btn btn-xs btn-default" href="{{ url_for('org_contest_add_user', id=contest.contest_id, site_id=place.place_id) }}">Přidat účastníka</a> + <a class="btn btn-xs btn-default" href="{{ ctx.url_for('org_contest_add_user', site_id=place.place_id) }}">Přidat účastníka</a> </div> {% endif %} </tr> @@ -104,7 +108,7 @@ {% endif %} <div class="btn-group"> {% if can_manage and not site %} - <a class="btn btn-default" href='{{ url_for('org_contest_add_user', id=contest.contest_id) }}'>Přidat účastníka</a> + <a class="btn btn-default" href='{{ ctx.url_for('org_contest_add_user') }}'>Přidat účastníka</a> {% endif %} </div> @@ -128,21 +132,21 @@ <td>{{ task.sol_count }} <td>{{ task.max_points|decimal|none_value('–') }} <td><div class="btn-group"> - <a class="btn btn-xs btn-primary" href="{{ url_for('org_contest_task', contest_id=contest.contest_id, task_id=task.task_id, site_id=site_id) }}">Odevzdaná řešení</a> + <a class="btn btn-xs btn-primary" href="{{ ctx.url_for('org_contest_task', task_id=task.task_id) }}">Odevzdaná řešení</a> {% if not site and can_edit_points %} - <a class="btn btn-xs btn-default" href="{{ url_for('org_contest_task_points', contest_id=contest.contest_id, task_id=task.task_id) }}">Zadat body</a> + <a class="btn btn-xs btn-default" href="{{ ctx.url_for('org_contest_task_points', task_id=task.task_id) }}">Zadat body</a> {% endif %} {% if can_create_solutions %} - <a class="btn btn-xs btn-default" href="{{ url_for('org_contest_task_create', contest_id=contest.contest_id, task_id=task.task_id, site_id=site_id) }}">Založit řešení</a> + <a class="btn btn-xs btn-default" href="{{ ctx.url_for('org_contest_task_create', task_id=task.task_id) }}">Založit řešení</a> {% endif %} </div> <td><div class="btn-group"> - <a class="btn btn-xs btn-primary" href="{{ url_for('org_contest_task_download', contest_id=contest.contest_id, task_id=task.task_id, site_id=site_id) }}">Stáhnout ZIP</a> + <a class="btn btn-xs btn-primary" href="{{ ctx.url_for('org_generic_batch_download', task_id=task.task_id) }}">Stáhnout ZIP</a> {% if can_upload %} - <a class='btn btn-xs btn-default' href="{{ url_for('org_contest_task_upload', contest_id=contest.contest_id, task_id=task.task_id, site_id=site_id) }}">Nahrát ZIP</a> + <a class='btn btn-xs btn-default' href="{{ ctx.url_for('org_generic_batch_upload', task_id=task.task_id) }}">Nahrát ZIP</a> {% endif %} {% if not site and can_edit_points %} - <a class="btn btn-xs btn-default" href="{{ url_for('org_contest_task_batch_points', contest_id=contest.contest_id, task_id=task.task_id) }}">Nahrát body</a> + <a class="btn btn-xs btn-default" href="{{ ctx.url_for('org_generic_batch_points', task_id=task.task_id) }}">Nahrát body</a> {% endif %} </div> </tr> @@ -156,8 +160,8 @@ Práva k {% if site %}soutěžními místu{% else %}soutěži{% endif %}: {% if g.user.is_admin %} admin -{% elif rights %} - {% for r in rights %} +{% elif rights_list %} + {% for r in rights_list %} {{ r.name }} {% endfor %} {% else %} diff --git a/mo/web/templates/org_contest_add_user.html b/mo/web/templates/org_contest_add_user.html index d544dbef1c6d75fe4f0bfc8b93788a3fb0a6cf33..fe181f2379b20d2247f56a900b49dcae74bbf22c 100644 --- a/mo/web/templates/org_contest_add_user.html +++ b/mo/web/templates/org_contest_add_user.html @@ -1,12 +1,11 @@ {% extends "base.html" %} {% import "bootstrap/wtf.html" as wtf %} -{% set round = contest.round %} {% block title %} {{ round.round_code() }}: Přidat účastníka {% if site %}do soutěžního místa {{ site.name }}{% else %}do oblasti {{ contest.place.name }}{% endif %} {% endblock %} {% block breadcrumbs %} -{{ contest_breadcrumbs(round=round, contest=contest, site=site, action="Přidat účastníka") }} +{{ ctx.breadcrumbs(action="Přidat účastníka") }} {% endblock %} {% block body %} diff --git a/mo/web/templates/org_contest_advance.html b/mo/web/templates/org_contest_advance.html index 2094c11ac51a31f771947e5a2dc0c862ffba0127..b6f22bdac3ba06ec6063ef3b93acc4a210e05552 100644 --- a/mo/web/templates/org_contest_advance.html +++ b/mo/web/templates/org_contest_advance.html @@ -3,7 +3,7 @@ {% block title %}Postup z {{ prev_round.round_code() }} ({{ prev_round.name }}) do {{ round.round_code() }} ({{ round.name }}){% endblock %} {% block breadcrumbs %} -{{ contest_breadcrumbs(round=round, contest=contest, action="Postup") }} +{{ ctx.breadcrumbs(action="Postup") }} {% endblock %} {% block body %} @@ -25,10 +25,10 @@ <tbody> {% for c in prev_contests %} <tr> - <td><a href='{{ url_for('org_contest', id=c.contest_id) }}'>{{ c.place.name }}</a> + <td><a href='{{ url_for('org_contest', ct_id=c.contest_id) }}'>{{ c.place.name }}</a> <td>{{ accept_by_place_id[c.place.place_id] }} <td>{{ reject_by_place_id[c.place.place_id] }} - <td><a class='btn btn-warning btn-xs' href='{{ url_for('org_score', contest_id=c.contest_id) }}'>Výsledková listina</a> + <td><a class='btn btn-warning btn-xs' href='{{ url_for('org_score', ct_id=c.contest_id) }}'>Výsledková listina</a> {% endfor %} <tfoot> <tr> diff --git a/mo/web/templates/org_contest_edit.html b/mo/web/templates/org_contest_edit.html index 508d75fd58a9e51030aa0f60d625ed459405463c..39e4c0fb729dba67e6d6786387dbf5f43873b232 100644 --- a/mo/web/templates/org_contest_edit.html +++ b/mo/web/templates/org_contest_edit.html @@ -3,7 +3,7 @@ {% block title %}Editace soutěže {{ round.round_code() }}: {{ contest.place.name }}{% endblock %} {% block breadcrumbs %} -{{ contest_breadcrumbs(round=round, contest=contest, action="Editace") }} +{{ ctx.breadcrumbs(action="Editace") }} {% endblock %} {% block body %} diff --git a/mo/web/templates/org_contest_list.html b/mo/web/templates/org_contest_list.html deleted file mode 100644 index e8b01899f8b27ce3ed879e8259d7adde140be7a7..0000000000000000000000000000000000000000 --- a/mo/web/templates/org_contest_list.html +++ /dev/null @@ -1,44 +0,0 @@ -{% extends "base.html" %} -{% import "bootstrap/wtf.html" as wtf %} -{% set round = contest.round %} - -{% block title %} -Seznam účastníků {% if site %}v soutěžním místě {{ site.name }}{% else %}{{ contest.place.name_locative() }}{% endif %} -{% endblock %} -{% block breadcrumbs %} -{{ contest_breadcrumbs(round=round, contest=contest, site=site, action="Seznam účastníků" if table else "E-maily", table=False if table else True) }} -{% endblock %} -{% set id = contest.contest_id %} -{% set site_id = site.place_id if site else None %} - -{% block body %} -<div class="form-frame"> -<form action="" method="GET" class="form form-inline" role="form"> - <div class="form-row"> - {% if not site %} - {{ wtf.form_field(filter.participation_place, size=8) }} - {% endif %} - {{ wtf.form_field(filter.school, size=8) }} - {{ wtf.form_field(filter.participation_state) }} - <div class="btn-group"> - {{ wtf.form_field(filter.submit, class='btn btn-primary') }} - {% if table %} - <button class="btn btn-default" name="format" value="cs_csv" title="Stáhnout celý výsledek v CSV">↓ CSV</button> - <button class="btn btn-default" name="format" value="tsv" title="Stáhnout celý výsledek v TSV">↓ TSV</button> - {% endif %} - </div> - {% if not site %} - </div> - <div class="form-row" style="margin-top: 5px;"> - {% endif %} - Celkem <b>{{count|inflected('nalezený účastník', 'nalezení účastníci', 'nalezených účastníků')}}</b>. - </div> -</form> -</div> - -{% if table %} - {% include 'parts/org_participants_table_actions.html' %} -{% else %} - {% include 'parts/org_participants_emails.html' %} -{% endif %} -{% endblock %} diff --git a/mo/web/templates/org_contest_solutions.html b/mo/web/templates/org_contest_solutions.html index 7df83f66e70e2cf897b4b8c8fd0e841f129dbb05..20bbd1ac58d7511eff040d90e3335d7327df6967 100644 --- a/mo/web/templates/org_contest_solutions.html +++ b/mo/web/templates/org_contest_solutions.html @@ -1,20 +1,19 @@ {% extends "base.html" %} {% import "bootstrap/wtf.html" as wtf %} {% set round = contest.round %} -{% set site_id = site.place_id if site else None %} {% block title %} {{ "Založení řešení" if edit_form else "Tabulka řešení" }} {% if site %}soutěžního místa {{ site.name }}{% else %}{{ contest.place.name_locative() }}{% endif %} {% endblock %} {% block breadcrumbs %} -{{ contest_breadcrumbs(round=round, contest=contest, site=site, action="Založení řešení" if edit_form else "Tabulka řešení") }} +{{ ctx.breadcrumbs(action="Založení řešení" if edit_form else "Tabulka řešení") }} {% endblock %} {% block pretitle %} {% if contest.state in [RoundState.grading, RoundState.closed] %} <div class="btn-group pull-right"> - <a class="btn btn-default" href="{{ url_for('org_score', contest_id=contest.contest_id) }}">Výsledky {{ round.get_level().name_genitive() }}</a> - <a class="btn btn-default" href="{{ url_for('org_score', round_id=round.round_id) }}">Výsledky kola</a> + <a class="btn btn-default" href="{{ ctx.url_for('org_score') }}">Výsledky {{ round.get_level().name_genitive() }}</a> + <a class="btn btn-default" href="{{ ctx.url_for('org_score', ct_id=None) }}">Výsledky kola</a> </div> {% endif %} {% endblock %} @@ -48,9 +47,9 @@ konkrétní úlohu. Symbol <span class="icon">🗐</span> značí, že existuje <th rowspan=2>Účastník <th rowspan=2>Stav účasti</th> {% for task in tasks %}<th colspan=4> - <a href="{{ url_for('org_contest_task', contest_id=contest.contest_id, site_id=site_id, task_id=task.task_id) }}">{{ task.code }}</a> - {% if sc.allow_edit_points %} - <a title="Editovat body" href="{{ url_for('org_contest_task_points', contest_id=contest.contest_id, task_id=task.task_id) }}" class="icon pull-right">✎</a> + <a href="{{ ctx.url_for('org_contest_task', task_id=task.task_id) }}">{{ task.code }}</a> + {% if rights.can_edit_points() %} + <a title="Editovat body" href="{{ ctx.url_for('org_contest_task_points', task_id=task.task_id) }}" class="icon pull-right">✎</a> {% endif %} {% endfor %} <th rowspan=2>Body celkem @@ -61,7 +60,7 @@ konkrétní úlohu. Symbol <span class="icon">🗐</span> značí, že existuje </thead> {% for pion in pions %} {% set u = pion.user %} - <tr class="state-{{ pion.state.name }}> + <tr class="state-{{ pion.state.name }}"> <th>{{ u|pion_link(contest.contest_id) }} <td>{{ pion.state.friendly_name() }} {% set sum_points = [] %} @@ -109,7 +108,7 @@ konkrétní úlohu. Symbol <span class="icon">🗐</span> značí, že existuje {% else %}–{% endif %} <td> {% endif %} - <a class="btn btn-xs btn-link icon" title="Detail řešení" href="{{ url_for('org_submit_list', contest_id=contest.contest_id, user_id=u.user_id, task_id=task.task_id, site_id=site_id) }}">🔍</a> + <a class="btn btn-xs btn-link icon" title="Detail řešení" href="{{ ctx.url_for('org_submit_list', user_id=u.user_id, task_id=task.task_id) }}">🔍</a> {% endfor %} <th>{{ sum_points|sum|decimal }}</th> </tr> @@ -118,9 +117,9 @@ konkrétní úlohu. Symbol <span class="icon">🗐</span> značí, že existuje <tr><td><td> {% for task in tasks %} <td colspan=4><div class='btn-group'> - <a class='btn btn-xs btn-primary' href="{{ url_for('org_contest_task_download', contest_id=contest.contest_id, site_id=site_id, task_id=task.task_id) }}">Stáhnout</a> - {% if sc.allow_upload_feedback %} - <a class='btn btn-xs btn-primary' href="{{ url_for('org_contest_task_upload', contest_id=contest.contest_id, site_id=site_id, task_id=task.task_id) }}">Nahrát</a> + <a class='btn btn-xs btn-primary' href="{{ ctx.url_for('org_generic_batch_download', task_id=task.task_id) }}">Stáhnout</a> + {% if rights.can_upload_feedback() %} + <a class='btn btn-xs btn-primary' href="{{ ctx.url_for('org_generic_batch_upload', task_id=task.task_id) }}">Nahrát</a> {% endif %} </div> {% endfor %} @@ -130,13 +129,13 @@ konkrétní úlohu. Symbol <span class="icon">🗐</span> značí, že existuje {% if edit_form %} <div class='btn-group'> {{ wtf.form_field(edit_form.submit, class="btn btn-primary") }} - <a class="btn btn-default" href="{{ url_for('org_contest_solutions', id=contest.contest_id, site_id=site_id) }}">Zrušit</a> + <a class="btn btn-default" href="{{ ctx.url_for('org_contest_solutions') }}">Zrušit</a> </div> </form> {% else %} <div class='btn-group'> - {% if sc.allow_create_solutions %} - <a class="btn btn-primary" href="{{ url_for('org_contest_solutions_edit', id=contest.contest_id, site_id=site_id) }}">Založit řešení</a> + {% if rights.can_create_solutions() %} + <a class="btn btn-primary" href="{{ ctx.url_for('org_contest_solutions_edit') }}">Založit řešení</a> {% endif %} </div> {% endif %} diff --git a/mo/web/templates/org_contest_task.html b/mo/web/templates/org_contest_task.html index 9e8c65c00adbdfc977971e546976447ac78193b9..96fc3f4c974a7939d619b9713194257aa161bfc2 100644 --- a/mo/web/templates/org_contest_task.html +++ b/mo/web/templates/org_contest_task.html @@ -1,23 +1,20 @@ {% extends "base.html" %} {% import "bootstrap/wtf.html" as wtf %} -{% set contest = sc.contest %} -{% set ct_id = contest.contest_id %} -{% set round = sc.round %} -{% set site = sc.site %} -{% set site_id = site.place_id if site else None %} -{% set task = sc.task %} +{% set allow_edit_points=rights.can_edit_points() %} +{% set allow_upload_solutions=rights.can_upload_solutions() %} +{% set allow_upload_feedback=rights.can_upload_feedback() %} -{% block title %}{{ "Zadávání bodů" if points_form else "Založení řešení" if create_form else "Odevzdaná řešení" }} úlohy {{ task.code }} {{ task.name }}{% endblock %} +{% block title %}{{ "Zadávání bodů" if points_form else "Založení řešení" if create_form else "Odevzdaná řešení" }} úlohy {{ ctx.task.code }} {{ ctx.task.name }}{% endblock %} {% block breadcrumbs %} -{{ contest_breadcrumbs(round=round, contest=contest, site=site, task=task, action="Zadávání bodů" if points_form else "Založení řešení" if create_form else None) }} +{{ ctx.breadcrumbs(action="Zadávání bodů" if points_form else "Založení řešení" if create_form else None) }} {% endblock %} {% block pretitle %} <div class="btn-group pull-right"> - <a class="btn btn-default" href="{{ url_for('org_contest_solutions', id=ct_id, site_id=site_id) }}">Všechny úlohy</a> - {% if contest.state in [RoundState.grading, RoundState.closed] %} - <a class="btn btn-default" href="{{ url_for('org_score', contest_id=ct_id) }}">Výsledky {{ round.get_level().name_genitive() }}</a> - <a class="btn btn-default" href="{{ url_for('org_score', round_id=round.round_id) }}">Výsledky kola</a> + <a class="btn btn-default" href="{{ ctx.url_for('org_contest_solutions', task_id=None) }}">Všechny úlohy</a> + {% if ctx.contest.state in [RoundState.grading, RoundState.closed] %} + <a class="btn btn-default" href="{{ ctx.url_for('org_score', task_id=None) }}">Výsledky {{ ctx.round.get_level().name_genitive() }}</a> + <a class="btn btn-default" href="{{ ctx.url_for('org_score', ct_id=None, task_id=None) }}">Výsledky kola</a> {% endif %} </div> {% endblock %} @@ -29,26 +26,26 @@ <form class="form" method="POST"> {{ form.csrf_token }} {% endif %} -{% with for_user=None, for_task=task, rows=rows %} +{% with for_user=None, for_task=ctx.task, rows=rows %} {% include "parts/org_solution_table.html" %} {% endwith %} {% if form %} <div class='btn-group'> {{ wtf.form_field(form.submit, class="btn btn-primary" ) }} - <a class="btn btn-default" href="{{ url_for('org_contest_task', contest_id=ct_id, task_id=task.task_id, site_id=site_id) }}">Zrušit</a> + <a class="btn btn-default" href="{{ ctx.url_for('org_contest_task') }}">Zrušit</a> </div> </form> {% else %} <div class='btn-group'> - <a class='btn btn-primary' href="{{ url_for('org_contest_task_download', contest_id=ct_id, site_id=site_id, task_id=task.task_id) }}">Stáhnout řešení</a> - {% if sc.allow_upload_feedback %} - <a class='btn btn-primary' href="{{ url_for('org_contest_task_upload', contest_id=ct_id, site_id=site_id, task_id=task.task_id) }}">Nahrát opravená řešení</a> + <a class='btn btn-primary' href="{{ ctx.url_for('org_generic_batch_download') }}">Stáhnout řešení</a> + {% if allow_upload_feedback %} + <a class='btn btn-primary' href="{{ ctx.url_for('org_generic_batch_upload') }}">Nahrát opravená řešení</a> {% endif %} - {% if sc.allow_create_solutions %} - <a class="btn btn-primary" href="{{ url_for('org_contest_task_create', contest_id=ct_id, task_id=task.task_id, site_id=site_id) }}">Založit řešení</a> + {% if allow_create_solutions %} + <a class="btn btn-primary" href="{{ ctx.url_for('org_contest_task_create') }}">Založit řešení</a> {% endif %} - {% if not site and sc.allow_edit_points %} - <a class="btn btn-primary" href="{{ url_for('org_contest_task_points', contest_id=ct_id, task_id=task.task_id) }}">Zadat body</a> + {% if not ctx.site and allow_edit_points %} + <a class="btn btn-primary" href="{{ ctx.url_for('org_contest_task_points') }}">Zadat body</a> {% endif %} </div> {% endif %} diff --git a/mo/web/templates/org_contest_user.html b/mo/web/templates/org_contest_user.html index d37540fb4adf55594f1b70a185cfd2171da08811..a77e15c32ef175475d481e5728e65f53559d45b3 100644 --- a/mo/web/templates/org_contest_user.html +++ b/mo/web/templates/org_contest_user.html @@ -1,13 +1,13 @@ {% extends "base.html" %} {% import "bootstrap/wtf.html" as wtf %} -{% set contest = sc.contest %} +{% set contest = ctx.contest %} {% set ct_id = contest.contest_id %} -{% set round = sc.round %} -{% set user = sc.user %} +{% set round = ctx.round %} +{% set user = ctx.user %} {% block title %}{{ round.round_code() }}: účastník {{ user.full_name() }}{% endblock %} {% block breadcrumbs %} -{{ contest_breadcrumbs(round=round, contest=contest, user=user) }} +{{ ctx.breadcrumbs() }} {% endblock %} {% block body %} @@ -15,14 +15,14 @@ <h4>Rychlé odkazy</h4> Soutěžní kolo: <div class="btn-group"> - <a class="btn btn-default" href="{{ url_for('org_round_list', id=round.round_id) }}">Účastníci</a> - <a class="btn btn-default" href="{{ url_for('org_score', round_id=round.round_id) }}">Výsledky</a> + <a class="btn btn-default" href="{{ ctx.url_for('org_generic_list', ct_id=None) }}">Účastníci</a> + <a class="btn btn-default" href="{{ ctx.url_for('org_score', ct_id=None) }}">Výsledky</a> </div> <br>{{ round.get_level().name|capitalize }}: <div class="btn-group"> - <a class="btn btn-default" href="{{ url_for('org_contest_solutions', id=ct_id) }}">Tabulka řešení</a> - <a class="btn btn-default" href="{{ url_for('org_contest_list', id=ct_id) }}">Účastníci</a> - <a class="btn btn-default" href="{{ url_for('org_score', contest_id=ct_id) }}">Výsledky</a> + <a class="btn btn-default" href="{{ ctx.url_for('org_contest_solutions') }}">Tabulka řešení</a> + <a class="btn btn-default" href="{{ ctx.url_for('org_generic_list') }}">Účastníci</a> + <a class="btn btn-default" href="{{ ctx.url_for('org_score') }}">Výsledky</a> </div> </div> @@ -38,14 +38,14 @@ <tr><td>Rok narození:<td>{{ pant.birth_year }} <tr><td>Poznámka:<td style="white-space: pre-line;">{{ user.note }} <thead> - <tr><th colspan='2'>Účast v kole + <tr><th colspan='2'>Účast v kole </thead> - <tr><td>{{ round.get_level().name|capitalize }}:<td><a href='{{ url_for('org_contest', id=ct_id) }}'>{{ contest.place.name }}</a> - <tr><td>Soutěžní místo:<td><a href='{{ url_for('org_contest', id=ct_id, site_id=sc.pion.place_id) }}'>{{ sc.pion.place.name }}</a> - <tr><td>Stav účasti:<td>{{ sc.pion.state.friendly_name() }} + <tr><td>{{ round.get_level().name|capitalize }}:<td><a href='{{ ctx.url_for('org_contest') }}'>{{ contest.place.name }}</a> + <tr><td>Soutěžní místo:<td><a href='{{ ctx.url_for('org_contest', site_id=ctx.pion.place_id) }}'>{{ ctx.pion.place.name }}</a> + <tr><td>Stav účasti:<td>{{ ctx.pion.state.friendly_name() }} </table> -<a class="btn btn-default" href="{{ url_for('org_user', id=user.user_id) }}">Detail uživatele</a> +<a class="btn btn-default" href="{{ user|user_url }}">Detail uživatele</a> {% include "parts/org_submit_warning.html" %} diff --git a/mo/web/templates/org_edit_statement.html b/mo/web/templates/org_edit_statement.html index 3f5a3cf06d560096db40821cc50e27b9ac22cfb6..2c303e6b3238c2e99c8a3508ad470dffa67d2ad6 100644 --- a/mo/web/templates/org_edit_statement.html +++ b/mo/web/templates/org_edit_statement.html @@ -3,7 +3,7 @@ {% block title %}Zadání kola {{ round.round_code() }}{% endblock %} {% block breadcrumbs %} -{{ contest_breadcrumbs(round=round, action="Zadáni") }} +{{ ctx.breadcrumbs(action="Zadáni") }} {% endblock %} {% block body %} diff --git a/mo/web/templates/org_generic_batch_download.html b/mo/web/templates/org_generic_batch_download.html index b2248bed84cdc41a3a6973c9f2a5c180ceaa577f..8f6fb370e1519afc53d694a7f2ef27a2914e72c7 100644 --- a/mo/web/templates/org_generic_batch_download.html +++ b/mo/web/templates/org_generic_batch_download.html @@ -3,7 +3,7 @@ {% block title %}Stažení řešení úlohy {{ task.code }} {{ task.name }}{% endblock %} {% block breadcrumbs %} -{{ contest_breadcrumbs(round=round, contest=contest, site=site, task=task, action="Stažení řešení") }} +{{ ctx.breadcrumbs(action="Stažení řešení") }} {% endblock %} {% block body %} @@ -15,7 +15,7 @@ <p>Zde si můžete stáhnout všechna řešení této úlohy {% if contest %} - {{ contest.place.name_locative() }}. + {{ contest.place.name_locative() }}. {% else %} ze všech oblastí tohoto kola. {% endif %} diff --git a/mo/web/templates/org_generic_batch_points.html b/mo/web/templates/org_generic_batch_points.html index 074f4265deee32e6383d322e8bfc8b86ae49091b..aaa82898ab73e787598fc4ab529f2faa5a8a0c14 100644 --- a/mo/web/templates/org_generic_batch_points.html +++ b/mo/web/templates/org_generic_batch_points.html @@ -3,7 +3,7 @@ {% block title %}Dávkové bodování úlohy {{ task.code }} {{ task.name }}{% endblock %} {% block breadcrumbs %} -{{ contest_breadcrumbs(round=round, contest=contest, task=task, action="Dávkové bodování") }} +{{ ctx.breadcrumbs(action="Dávkové bodování") }} {% endblock %} {% block body %} diff --git a/mo/web/templates/org_generic_batch_upload.html b/mo/web/templates/org_generic_batch_upload.html index 99d78527a97bdd4893dcdfa6a4ac71c3bb1498a8..e085c0ae9b36bd13e7f49918284a9abcf778fca3 100644 --- a/mo/web/templates/org_generic_batch_upload.html +++ b/mo/web/templates/org_generic_batch_upload.html @@ -1,10 +1,9 @@ {% extends "base.html" %} {% import "bootstrap/wtf.html" as wtf %} -{% set site_id = site.place_id if site else None %} {% block title %}Nahrání opravených řešení úlohy {{ task.code }} {{ task.name }}{% endblock %} {% block breadcrumbs %} -{{ contest_breadcrumbs(round=round, contest=contest, site=site, task=task, action="Nahrání opravených řešení") }} +{{ ctx.breadcrumbs(action="Nahrání opravených řešení") }} {% endblock %} {% block body %} diff --git a/mo/web/templates/org_generic_import.html b/mo/web/templates/org_generic_import.html index 295a450726147a5b15b08460303250a439a23526..2d9c95049172ec5b0badb5a406a004b7853b303e 100644 --- a/mo/web/templates/org_generic_import.html +++ b/mo/web/templates/org_generic_import.html @@ -5,7 +5,7 @@ Import dat do {% if contest %}soutěže {{ contest.place.name_locative() }}{% else %}kola {{ round.round_code() }}{% endif %} {% endblock %} {% block breadcrumbs %} -{{ contest_breadcrumbs(round=round, contest=contest, action="Import dat") }} +{{ ctx.breadcrumbs(action="Import dat") }} {% endblock %} {% block body %} diff --git a/mo/web/templates/org_generic_list.html b/mo/web/templates/org_generic_list.html new file mode 100644 index 0000000000000000000000000000000000000000..702955b979fed8bba9fd7f047c890b417270f688 --- /dev/null +++ b/mo/web/templates/org_generic_list.html @@ -0,0 +1,162 @@ +{% extends "base.html" %} +{% import "bootstrap/wtf.html" as wtf %} + +{% block title %} + {% if contest %} + Seznam účastníků {% if site %}v soutěžním místě {{ site.name }}{% else %}{{ contest.place.name_locative() }}{% endif %} + {% else %} + Seznam účastníků kola {{ round.round_code() }} + {% endif %} +{% endblock %} +{% block breadcrumbs %} +{{ ctx.breadcrumbs(action="Seznam účastníků" if table else "E-maily", table=False if table else True) }} +{% endblock %} +{% set id = contest.contest_id %} +{% set site_id = site.place_id if site else None %} + +{% block body %} +<div class="form-frame"> +<form action="" method="GET" class="form form-inline" role="form"> + <div class="form-row"> + {% if not contest %} + {{ wtf.form_field(filter.contest_place, placeholder='Kód', size=8) }} + {% endif %} + {% if not site %} + {{ wtf.form_field(filter.participation_place, size=8) }} + {% endif %} + {{ wtf.form_field(filter.school, size=8) }} + {{ wtf.form_field(filter.participation_state) }} + </div> + <div class="form-row" style="margin-top: 5px;"> + <div class="btn-group"> + {{ wtf.form_field(filter.submit, class='btn btn-primary') }} + {% if table %} + <button class="btn btn-default" name="format" value="cs_csv" title="Stáhnout celý výsledek v CSV">↓ CSV</button> + <button class="btn btn-default" name="format" value="tsv" title="Stáhnout celý výsledek v TSV">↓ TSV</button> + {% endif %} + </div> + {% if table %} + <div style="float: right"> + Stránka {{ filter.offset.data // filter.limit.data + 1}} z {{ (count / filter.limit.data)|round(0, 'ceil')|int }}: + <div class="btn-group"> + {% if filter.offset.data > 0 %} + {{ wtf.form_field(filter.previous) }} + {% else %} + <button class="btn" disabled>Předchozí</button> + {% endif %} + {% if count > filter.offset.data + filter.limit.data %} + {{ wtf.form_field(filter.next) }} + {% else %} + <button class="btn" disabled>Další</button> + {% endif %} + </div> + <input type="hidden" name="offset" value="{{filter.offset.data}}"> + <input type="hidden" name="limit" value="{{filter.limit.data}}"> + </div> + {% set max = filter.offset.data + filter.limit.data if filter.offset.data + filter.limit.data < count else count %} + {% if count > 0 %} + Zobrazuji záznamy <b>{{filter.offset.data + 1}}</b> až <b>{{ max }}</b> z <b>{{count}} nalezených účastníků</b>. + {% else %} + <b>Nebyly nalezeny žádné záznamy účastníků.</b> + {% endif %} + {% else %} + Celkem <b>{{count|inflected('nalezený účastník', 'nalezení účastníci', 'nalezených účastníků')}}</b>. + {% endif %} + </div> +</form> +</div> + +{% if table %} + {% if action_form %} + <form action="" method="POST" class="form form-horizontal" role="form"> + {% endif %} + + {{ table.to_html() }} + + {% if contest %} + <a class="btn btn-primary" href="{{ url_for('org_contest_add_user', ct_id=contest.contest_id, site_id=site.place_id if site else None) }}">Přidat účastníka</a> + {% endif %} + <a class="btn btn-default" + title="Zobrazí emailové adresy ve snadno zkopírovatelném formátu" + href="{{ ctx.url_for('org_generic_list_emails', **request.args) }}"> + Vypsat e-mailové adresy + </a> + + {% if action_form %} + {{ action_form.csrf_token }} + <h3>Provést akci</h3> + <div class="form-frame"> + <div class="form-group"> + <label class="control-label col-sm-2">Provést akci na:</label> + <div class="col-sm-5 radio"> + <label> + <input id="action_on-0" name="action_on" type="radio" value="all" required{% if action_form.action_on.data == 'all' %} checked{% endif %}> + všech vyfiltrovaných účastnících + </label> + </div><div class="col-sm-5 radio"> + <label> + <input id="action_on-1" name="action_on" type="radio" value="checked" required{% if action_form.action_on.data == 'checked' %} checked{% endif %}> + pouze zaškrtnutých účastnících + </label> + </div> + </div> + <div class="form-group"> + <label class="control-label col-sm-2" for="participation_state">Stav účasti</label> + <div class="col-sm-6">{{ wtf.form_field(action_form.participation_state, form_type='inline') }}</div> + <div class="col-sm-4">{{ wtf.form_field(action_form.set_participation_state, form_type='inline', class='btn btn-primary') }}</div> + </div> + <div class="form-group"> + <label class="control-label col-sm-2" for="participation_place">Soutěžní místo</label> + <div class="col-sm-6">{{ wtf.form_field(action_form.participation_place, form_type='inline', placeholder='Kód místa') }}</div> + <div class="col-sm-4">{{ wtf.form_field(action_form.set_participation_place, form_type='inline', class='btn btn-primary') }}</div> + </div> + <div class="form-group"> + <label class="control-label col-sm-2" for="contest_place"> + {{ round.get_level().name|capitalize }} + </label> + <div class="col-sm-6"> + {{ wtf.form_field(action_form.contest_place, form_type='inline', placeholder='Kód místa') }} + <p class="help-block"> + {{ round.get_level().name_locative("V tomto", "V této", "V tomto") }} musí existovat soutěž pro {{ round.name|lower }} kategorie {{ round.category }}. + </p> + </div> + <div class="col-sm-4">{{ wtf.form_field(action_form.set_contest, form_type='inline', class='btn btn-primary', value='Přesunout do ' + round.get_level().name_genitive('jiného', 'jiné', 'jiného')) }}</div> + </div> + <div class="form-group"> + <label class="control-label col-sm-2" for="contest_place">Smazání účasti</label> + <div class="col-sm-6"><p class="help-block">Dojde ke smazání účasti v tomto kole, ne účastníka z ročníku.</p></div> + <div class="col-sm-4">{{ wtf.form_field(action_form.remove_participation, form_type='inline', class='btn btn-danger') }}</div> + </div> + </div> + </form> + {% else %} + <p> + <i>Nemáte právo k editaci účastníků v {{ round.get_level().name_locative("v tomto", "v této", "v tomto") }}.</i> + </p> + {% endif %} +{% else %} + <h3>E-mailové adresy</h3> + + {% if emails %} + <pre>{{ emails|join('\n')|escape }}</pre> + <textarea id="emails-textarea" style="display: none">{{ emails|join('\n')|escape }}</textarea> + + <p> + <a class="btn btn-primary" href="{{ mailto_link }}">Vytvořit e-mail pro {{ count|inflected("adresáta", "adresáty", "adresátů") }}</a> + <button class="btn btn-default" id="copy-emails">Zkopírovat všechny adresy do schránky</button> + <script type="text/javascript"> + var ta = document.getElementById('emails-textarea'); + document.getElementById('copy-emails').addEventListener('click', function () { + ta.style.display = 'block'; + ta.select(); + document.execCommand('copy', false); + ta.style.display = 'none'; + }); + </script> + </p> + + <p>E-mailové adresy účastníků prosím vkládejte do pole pro <b>skrytou kopii (Bcc)</b>, ať si navzájem nevidí své e-maily.</p> + + {% else %}<i>Žádné e-mailové adresy k vypsání.</i>{% endif %} +{% endif %} +{% endblock %} diff --git a/mo/web/templates/org_index.html b/mo/web/templates/org_index.html index e4a5b0f5011b8458a24583502c2dadb683287f0e..40cc4eacb2298cb199fb09df95f22ec764d01544 100644 --- a/mo/web/templates/org_index.html +++ b/mo/web/templates/org_index.html @@ -22,14 +22,14 @@ {% endif %} <tr> - <td><a href='{{ url_for('org_contest', id=c.contest_id) }}'>{{ c.round.round_code() }}</a> + <td><a href='{{ url_for('org_contest', ct_id=c.contest_id) }}'>{{ c.round.round_code() }}</a> <td class="text-center"><b>{{ c.round.category }}</b> <td>{{ c.round.name }} <td>{{ c.place.name }} <td class="rstate-{{c.state.name}}">{{ c.state.friendly_name() }} <td>{% for r in contest_roles[c.contest_id] %}{{ role_type_names[r] }}{% if not loop.last %}<br>{% endif %}{% endfor %} <td> - <a class="btn btn-xs btn-primary" href='{{ url_for('org_contest', id=c.contest_id) }}'>Detail</a> + <a class="btn btn-xs btn-primary" href='{{ url_for('org_contest', ct_id=c.contest_id) }}'>Detail</a> {% endfor %} </table> diff --git a/mo/web/templates/org_place.html b/mo/web/templates/org_place.html index 94f8df8e77ba0934a3043812c5f5f33ef4d133c2..0baf84755988aa855b3253980d56623bfbfe4afb 100644 --- a/mo/web/templates/org_place.html +++ b/mo/web/templates/org_place.html @@ -72,7 +72,7 @@ {% for c in contests %} <tr> {% set r = c.round %} - <td><a href='{{ url_for('org_contest', id=c.contest_id) }}'>{{ r.round_code() }}</a> + <td><a href='{{ url_for('org_contest', ct_id=c.contest_id) }}'>{{ r.round_code() }}</a> <td>{{ r.year }} <td>{{ r.category }} <td>{{ r.name }} diff --git a/mo/web/templates/org_round.html b/mo/web/templates/org_round.html index bd91f5a844a660e1917ff0ee02e7c3be2ccdf16e..a39e611f450043aa40208d0710e9f6c8de9cfb42 100644 --- a/mo/web/templates/org_round.html +++ b/mo/web/templates/org_round.html @@ -1,9 +1,16 @@ {% extends "base.html" %} {% import "bootstrap/wtf.html" as wtf %} +{% set can_manage_round = rights.have_right(Right.manage_round) %} +{% set can_manage_contest = rights.have_right(Right.manage_contest) %} +{% set can_view_contestants = rights.have_right(Right.view_contestants) %} +{% set can_handle_submits = rights.have_right(Right.view_submits) %} +{% set can_upload = rights.can_upload_feedback() %} +{% set can_view_statement = rights.can_view_statement() %} +{% set can_add_contest = g.gatekeeper.rights_generic().have_right(Right.add_contest) %} {% block title %}{{ round.name }} {{ round.round_code() }}{% endblock %} {% block breadcrumbs %} -{{ contest_breadcrumbs(round=round) }} +{{ ctx.breadcrumbs() }} {% endblock %} {% block body %} @@ -22,7 +29,7 @@ <tr><td>Skupina kol:<td> {% for r in group_rounds %} {% if r == round %}<i>{{ r.name }} {{ r.round_code() }} (toto kolo)</i><br> - {% else %}<a href="{{ url_for('org_round', id=r.round_id) }}">{{ r.name }} {{ r.round_code() }}</a><br> + {% else %}<a href="{{ url_for('org_round', round_id=r.round_id) }}">{{ r.name }} {{ r.round_code() }}</a><br> {% endif %} {% endfor %} {% endif %} @@ -50,7 +57,7 @@ {% if not statement_exists %} <span class=error>soubor neexistuje</span> {% elif can_view_statement %} - <a href='{{ url_for('org_task_statement', id=round.round_id) }}'>stáhnout</a> + <a href='{{ ctx.url_for('org_task_statement') }}'>stáhnout</a> {% else %} není dostupné {% endif %} @@ -59,7 +66,7 @@ {% endif %} <thead> <tr><th colspan=2>Nastavení výsledkové listiny{% if round.is_subround() %} - <i>(přejato z <a href="{{ url_for('org_round', id=round.master.round_id) }}">{{ round.master.round_code() }}</a>)</i> + <i>(přejato z <a href="{{ url_for('org_round', round_id=round.master.round_id) }}">{{ round.master.round_code() }}</a>)</i> {% endif %} </thead> <tr><td>Výsledková listina<td>{{ round.master.score_mode.friendly_name() }} @@ -69,25 +76,29 @@ </table> <div style="clear: both;"></div> +{% if can_view_contestants or can_manage_contest or can_manage_round or round.has_messages %} <div class="btn-group"> - <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, RoundState.delegate] %} - <a class="btn btn-primary" href='{{ url_for('org_score', round_id=round.round_id) }}'>Výsledky</a> + {% if can_view_contestants %} + <a class="btn btn-primary" href='{{ ctx.url_for('org_generic_list') }}'>Seznam účastníků</a> {% endif %} - {% if can_manage_contestants %} - <a class="btn btn-default" href='{{ url_for('org_round_import', id=round.round_id) }}'>Importovat data</a> + {% if can_view_contestants and round.state in [RoundState.grading, RoundState.closed, RoundState.delegate] %} + <a class="btn btn-primary" href='{{ ctx.url_for('org_score') }}'>Výsledky</a> + {% endif %} + {% if can_manage_contest %} + <a class="btn btn-default" href='{{ ctx.url_for('org_generic_import') }}'>Importovat data</a> {% endif %} {% if can_manage_round %} - <a class="btn btn-default" href='{{ url_for('org_round_edit', id=round.round_id) }}'>Nastavení a termíny</a> - <a class="btn btn-default" href='{{ url_for('org_edit_statement', id=round.round_id) }}'>Zadání</a> + <a class="btn btn-default" href='{{ ctx.url_for('org_round_edit') }}'>Nastavení a termíny</a> + <a class="btn btn-default" href='{{ ctx.url_for('org_edit_statement') }}'>Zadání</a> {% endif %} {% if round.has_messages %} - <a class="btn btn-default" href='{{ url_for('org_round_messages', id=round.round_id) }}'>Zprávičky</a> + <a class="btn btn-default" href='{{ ctx.url_for('org_round_messages') }}'>Zprávičky</a> {% endif %} {% if g.user.is_admin %} <a class="btn btn-default" href='{{ log_url('round', round.round_id) }}'>Historie</a> {% endif %} </div> +{% endif %} <h3>Soutěže</h3> {% if contests_counts %} @@ -101,7 +112,7 @@ </thead> {% for (c, count) in contests_counts %} <tr> - <td><a href='{{ url_for('org_contest', id=c.contest_id) }}'>{{ c.place.name }}</a> + <td><a href='{{ url_for('org_contest', ct_id=c.contest_id) }}'>{{ c.place.name }}</a> {% with state=c.state %} <td class='rstate-{{state.name}}'>{{ state.friendly_name() }} {% endwith %} @@ -148,7 +159,7 @@ <td>{{ task.max_points|decimal|none_value('–') }} {% if can_manage_round %} <td><div class="btn-group"> - <a class="btn btn-xs btn-primary" href="{{ url_for('org_round_task_edit', id=round.round_id, task_id=task.task_id) }}">Editovat</a> + <a class="btn btn-xs btn-primary" href="{{ ctx.url_for('org_round_task_edit', task_id=task.task_id) }}">Editovat</a> {% if task.sol_count == 0 %} <form action="" method="POST" onsubmit="return confirm('Opravdu nenávratně smazat?')" class="btn-group"> {{ form_delete_task.csrf_token() }} @@ -164,13 +175,13 @@ {% if can_handle_submits or can_upload %} <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> + <a class="btn btn-xs btn-primary" href="{{ ctx.url_for('org_generic_batch_download', task_id=task.task_id) }}">Stáhnout ZIP</a> {% endif %} {% if can_upload %} - <a class="btn btn-xs btn-primary" href="{{ url_for('org_round_task_upload', round_id=round.round_id, task_id=task.task_id) }}">Nahrát ZIP</a> + <a class="btn btn-xs btn-primary" href="{{ ctx.url_for('org_generic_batch_upload', task_id=task.task_id) }}">Nahrát ZIP</a> {% endif %} {% if can_upload %} - <a class="btn btn-xs btn-default" href="{{ url_for('org_round_task_batch_points', round_id=round.round_id, task_id=task.task_id) }}">Nahrát body</a> + <a class="btn btn-xs btn-default" href="{{ ctx.url_for('org_generic_batch_points', task_id=task.task_id) }}">Nahrát body</a> {% endif %} </div> {% endif %} @@ -181,7 +192,7 @@ <p>Zatím nebyly přidány žádné úlohy.</p> {% endif %} {% 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="{{ ctx.url_for('org_round_task_new') }}">Nová úloha</a> {% endif %} {% endblock %} diff --git a/mo/web/templates/org_round_edit.html b/mo/web/templates/org_round_edit.html index 8e9ea08798933dd26db84422c3460f7743f338ff..0e42ebd87aba8af08e40e202dfaccb731dba5d33 100644 --- a/mo/web/templates/org_round_edit.html +++ b/mo/web/templates/org_round_edit.html @@ -3,7 +3,7 @@ {% block title %}Editace kola {{ round.round_code() }}{% endblock %} {% block breadcrumbs %} -{{ contest_breadcrumbs(round=round, action="Editace") }} +{{ ctx.breadcrumbs(action="Editace") }} {% endblock %} {% block body %} diff --git a/mo/web/templates/org_round_list.html b/mo/web/templates/org_round_list.html deleted file mode 100644 index 4dbdcf32b3daec2c0ded2ae58d7a419bae2b4da3..0000000000000000000000000000000000000000 --- a/mo/web/templates/org_round_list.html +++ /dev/null @@ -1,67 +0,0 @@ -{% extends "base.html" %} -{% import "bootstrap/wtf.html" as wtf %} - -{% block title %}Seznam účastníků kola {{ round.round_code() }}{% endblock %} -{% block breadcrumbs %} -{{ contest_breadcrumbs(round=round, action="Seznam účastníků" if table else "E-maily", table=False if table else True) }} -{% endblock %} -{% set id = round.round_id %} - -{% block body %} -<div class="form-frame"> -<form action="" method="GET" class="form form-inline" role="form"> - <div class="form-row"> - {{ wtf.form_field(filter.contest_place, size=8) }} - {{ wtf.form_field(filter.participation_place, size=8) }} - {{ wtf.form_field(filter.school, size=8) }} - {{ wtf.form_field(filter.participation_state) }} - </div> - <div class="form-row" style="margin-top: 5px;"> - <div class="btn-group"> - {{ wtf.form_field(filter.submit, class='btn btn-primary') }} - {% if table %} - <button class="btn btn-default" name="format" value="cs_csv" title="Stáhnout celý výsledek v CSV">↓ CSV</button> - <button class="btn btn-default" name="format" value="tsv" title="Stáhnout celý výsledek v TSV">↓ TSV</button> - {% endif %} - </div> - {% if table %} - <div style="float: right"> - Stránka {{ filter.offset.data // filter.limit.data + 1}} z {{ (count / filter.limit.data)|round(0, 'ceil')|int }}: - <div class="btn-group"> - {% if filter.offset.data > 0 %} - {{ wtf.form_field(filter.previous) }} - {% else %} - <button class="btn" disabled>Předchozí</button> - {% endif %} - {% if count > filter.offset.data + filter.limit.data %} - {{ wtf.form_field(filter.next) }} - {% else %} - <button class="btn" disabled>Další</button> - {% endif %} - </div> - <input type="hidden" name="offset" value="{{filter.offset.data}}"> - <input type="hidden" name="limit" value="{{filter.limit.data}}"> - </div> - {% set max = filter.offset.data + filter.limit.data if filter.offset.data + filter.limit.data < count else count %} - {% if count > 0 %} - Zobrazuji záznamy <b>{{filter.offset.data + 1}}</b> až <b>{{ max }}</b> z <b>{{count}} nalezených účastníků</b>. - {% else %} - <b>Nebyly nalezeny žádné záznamy účastníků.</b> - {% endif %} - {% else %} - Celkem <b>{{count|inflected('nalezený účastník', 'nalezení účastníci', 'nalezených účastníků')}}</b>. - {% endif %} - </div> -</form> -</div> - -{% if table %} - {% include 'parts/org_participants_table_actions.html' %} - {% if form_actions %} - <br> - <i>Upozornění: Můžete editovat jen účastníky soutěžící v oblastech, ke kterým máte právo.</i> - {% endif %} -{% else %} - {% include 'parts/org_participants_emails.html' %} -{% endif %} -{% endblock %} diff --git a/mo/web/templates/org_round_messages.html b/mo/web/templates/org_round_messages.html index 1be2a7367b9db1ef420ee6ad65f7a19d925270f4..d5e9ec3a7a648cdcd9878dfd7b3d80787c962a17 100644 --- a/mo/web/templates/org_round_messages.html +++ b/mo/web/templates/org_round_messages.html @@ -3,7 +3,7 @@ {% block title %}{{ round.name }} {{ round.round_code() }} – zprávičky{% endblock %} {% block breadcrumbs %} -{{ contest_breadcrumbs(round=round, action='Zprávičky') }} +{{ ctx.breadcrumbs(action='Zprávičky') }} {% endblock %} {% block body %} diff --git a/mo/web/templates/org_round_task_edit.html b/mo/web/templates/org_round_task_edit.html index fa26bbeacbf9a3f3df0c50101e627903b64c33b5..a43fbdb88b969bedf6e7160a19391dc76666b396 100644 --- a/mo/web/templates/org_round_task_edit.html +++ b/mo/web/templates/org_round_task_edit.html @@ -5,7 +5,7 @@ {% if task %}Editace úlohy {{ task.code }} {{ task.name }}{% else %}Nová úloha{% endif %} {% endblock %} {% block breadcrumbs %} -{{ contest_breadcrumbs(round=round, task=task, action="Nová úloha" if not task else None) }} +{{ ctx.breadcrumbs(action="Nová úloha" if not ctx.task else None) }} {% endblock %} {% block body %} diff --git a/mo/web/templates/org_rounds.html b/mo/web/templates/org_rounds.html index d4b415dc7e863a0df32fd04f885522194ae67780..b37c89c4c10c8245bfd9f6204f5f2b308ce80657 100644 --- a/mo/web/templates/org_rounds.html +++ b/mo/web/templates/org_rounds.html @@ -14,7 +14,7 @@ </thead> {% for r in rounds %} <tr> - <td><a href='{{ url_for('org_round', id=r.round_id) }}'>{{ r.round_code() }}</a> + <td><a href='{{ url_for('org_round', round_id=r.round_id) }}'>{{ r.round_code() }}</a> <td>{{ r.year }} <td>{{ r.category }} <td>{{ r.seq }}{{ r.part_code() }} diff --git a/mo/web/templates/org_score.html b/mo/web/templates/org_score.html index 8e0b6e98dc2997afa4f9b8c26d2e3e0d98ebe1df..0513cf4d27055f070c286c67d7a94396ca72a7ff 100644 --- a/mo/web/templates/org_score.html +++ b/mo/web/templates/org_score.html @@ -4,14 +4,14 @@ {{ round.round_code() }}: Výsledky pro {{ round.name|lower }} kategorie {{ round.category }}{% if contest %} {{ contest.place.name_locative() }}{% endif %} {% endblock %} {% block breadcrumbs %} -{{ contest_breadcrumbs(round=round, contest=contest, action="Výsledky oblasti" if contest else "Výsledky kola") }} +{{ ctx.breadcrumbs(action="Výsledky oblasti" if contest else "Výsledky kola") }} {% endblock %} {% block pretitle %} <div class="btn-group pull-right"> {% if contest %} - <a class="btn btn-default" href="{{ url_for('org_contest_solutions', id=contest.contest_id) }}">Odevzdaná řešení</a> - <a class="btn btn-default" href="{{ url_for('org_score', round_id=round.round_id) }}">Výsledky kola</a> + <a class="btn btn-default" href="{{ ctx.url_for('org_contest_solutions') }}">Odevzdaná řešení</a> + <a class="btn btn-default" href="{{ ctx.url_for('org_score', ct_id=None) }}">Výsledky kola</a> {% endif %} </div> {% endblock %} @@ -42,7 +42,7 @@ {% if group_rounds|length > 1 %} <p>Toto je <b>sdílená výsledková listina</b> pro několik kol: -{% for r in group_rounds %}{% if loop.index > 1 %}, {% endif %}<a href="{{ url_for('org_round', id=r.round_id) }}">{{ r.round_code() }} {{ r.name }}</a>{% endfor %}. +{% for r in group_rounds %}{% if loop.index > 1 %}, {% endif %}<a href="{{ url_for('org_round', round_id=r.round_id) }}">{{ r.round_code() }} {{ r.name }}</a>{% endfor %}. Jsou v ní započítány body ze všech úloh těchto kol.</p> {% endif %} diff --git a/mo/web/templates/org_submit_list.html b/mo/web/templates/org_submit_list.html index ff445d3ee105f6eeae8c395321ade119702729ed..09a62ca21fea47eea2cc23b480251453d4c50195 100644 --- a/mo/web/templates/org_submit_list.html +++ b/mo/web/templates/org_submit_list.html @@ -1,20 +1,23 @@ {% extends "base.html" %} {% import "bootstrap/wtf.html" as wtf %} -{% block title %}{{ sc.user.full_name() }} – řešení úlohy {{ sc.task.code }} {{ sc.task.name }}{% endblock %} +{% set allow_edit_points=rights.can_edit_points() %} +{% set allow_upload_solutions=rights.can_upload_solutions() %} +{% set allow_upload_feedback=rights.can_upload_feedback() %} + +{% block title %}{{ ctx.user.full_name() }} – řešení úlohy {{ ctx.task.code }} {{ ctx.task.name }}{% endblock %} {% block breadcrumbs %} -{{ contest_breadcrumbs(round=sc.round, contest=sc.contest, site=sc.site, task=sc.task, user=sc.user, action="Detail řešení") }} +{{ ctx.breadcrumbs(action="Detail řešení") }} {% endblock %} {% block body %} -{% set site_id = sc.site.place_id if sc.site else None %} <table class="data"> - <tr><th>Účastník<td>{{ sc.user|pion_link(sc.contest.contest_id) }}{{ sc.user|user_flags }} - <tr><th>Úloha<td><a href='{{ url_for('org_contest_task', contest_id=sc.contest.contest_id, site_id=site_id, task_id=sc.task.task_id) }}'>{{ sc.task.code }} {{ sc.task.name }}</a> + <tr><th>Účastník<td>{{ ctx.user|pion_link(ctx.contest.contest_id) }}{{ ctx.user|user_flags }} + <tr><th>Úloha<td><a href='{{ ctx.url_for('org_contest_task') }}'>{{ ctx.task.code }} {{ ctx.task.name }}</a> {% if solution %} <tr><th>Body<td> {{ solution.points|decimal|none_value(Markup('<span class="unknown">?</span>')) }} - {% if sc.task.max_points is not none %}<span class="hint"> / {{ sc.task.max_points|decimal }}</span>{% endif %} + {% if ctx.task.max_points is not none %}<span class="hint"> / {{ ctx.task.max_points|decimal }}</span>{% endif %} <tr title="Viditelná účastníkovi po uzavření kola"> <th>Poznámka k řešení:<td style="white-space: pre-line;">{{ solution.note|or_dash }}</td> <tr title="Viditelná jen organizátorům"> @@ -63,12 +66,12 @@ Existuje více než jedna verze řešení, finální je podbarvená. {% set active_sol_id = None %} {% endif %} {% for p in sol_papers %} - {% set late = p.check_deadline(sc.round) %} + {% set late = p.check_deadline(ctx.round) %} <tr{% if p.paper_id == active_sol_id %} class='sol-active'{% endif %}> <td{% if late %} class='sol-warn'{% endif %}>{{ p.uploaded_at|timeformat }} <td>{% if p.is_broken() %}nekorektní PDF{% else %}{{ p.pages|or_dash }}{% endif %} <td>{{ p.bytes|or_dash }} - <td>{% if p.uploaded_by_obj == sc.user %}<i>účastník</i>{% else %}{{ p.uploaded_by_obj|user_link }}{% endif %} + <td>{% if p.uploaded_by_obj == ctx.user %}<i>účastník</i>{% else %}{{ p.uploaded_by_obj|user_link }}{% endif %} <td>{% if late %}<span class='sol-warn'>({{ late }})</span> {% endif %}{{ p.note }} {% if p.is_fixed() %}Automaticky opravené nekorektní PDF.{% endif %} <td><div class="btn-group"> @@ -165,21 +168,21 @@ Existuje více než jedna verze oprav, finální je podbarvená. </div> {% else %} -<p>Žádné odevzdané řešení. {% if form and sc.allow_edit_points %}Můžete ho založit pomocí formuláře níže.{% endif %} +<p>Žádné odevzdané řešení. {% if form and allow_edit_points %}Můžete ho založit pomocí formuláře níže.{% endif %} {% endif %} -{% if form and (sc.allow_edit_points or sc.allow_upload_feedback or sc.allow_upload_solutions) %} +{% if form and (allow_edit_points or allow_upload_feedback or allow_upload_solutions) %} <form method="post" class="form-horizontal" enctype="multipart/form-data"> <div class="form-frame"> {{ form.csrf_token }} {% set action = 'Uložit' if solution else 'Založit řešení' %} - {% if sc.allow_edit_points %} + {% if allow_edit_points %} {% if solution %} <h3 style="margin-top: 10px;">Hodnocení řešení</h3> {% else %} <h3>Založit řešení</h3> <p><i>Můžete rovnou vyplnit i poznámky a přidělené body - {%- if sc.allow_upload_feedback or sc.allow_upload_solutions %}, případně rovnou nahrát i soubor řešení nebo opravy{% endif %}. + {%- if allow_upload_feedback or allow_upload_solutions %}, případně rovnou nahrát i soubor řešení nebo opravy{% endif %}. </i></p> {% endif %} {{ wtf.form_field(form.note, form_type='horizontal', horizontal_columns=('sm', 2, 10), rows=4)}} @@ -187,13 +190,13 @@ Existuje více než jedna verze oprav, finální je podbarvená. {{ wtf.form_field(form.points, form_type='horizontal', horizontal_columns=('sm', 2, 10) )}} {{ wtf.form_field( form.submit, form_type='horizontal', class='btn btn-primary', horizontal_columns=('sm', 2, 10), - value=action + (' bez nahrání souboru' if sc.allow_upload_feedback or sc.allow_upload_solutions else '') + value=action + (' bez nahrání souboru' if allow_upload_feedback or allow_upload_solutions else '') )}} {% endif %} - {% if sc.allow_upload_feedback or sc.allow_upload_solutions %} + {% if allow_upload_feedback or allow_upload_solutions %} {% if solution %} <h3>Nahrání souboru</h3> - {% if sc.allow_edit_points %} + {% if allow_edit_points %} <p><i>Lze najednou editovat řešení (například zadat body) i nahrát soubor, použijte tlačítka na spodku formuláře.</i></p> {% endif %} {% else %} @@ -203,10 +206,10 @@ Existuje více než jedna verze oprav, finální je podbarvená. {{ wtf.form_field(form.file_note, form_type='horizontal', horizontal_columns=('sm', 2, 10)) }} <div class="form-group"> <div class="btn btn-group col-sm-offset-2"> - {% if sc.allow_upload_solutions %} + {% if allow_upload_solutions %} {{ wtf.form_field(form.submit_sol, class='btn btn-primary', value=action + ' a nahrát soubor jako řešení' )}} {% endif %} - {% if sc.allow_upload_feedback %} + {% if allow_upload_feedback %} {{ wtf.form_field(form.submit_fb, class='btn btn-success', value=action + ' a nahrát soubor jako opravu' )}} {% endif %} </div> @@ -214,7 +217,7 @@ Existuje více než jedna verze oprav, finální je podbarvená. {% endif %} </div> -{% if solution and not solution.final_submit and not solution.final_feedback and sc.allow_create_solutions %} +{% if solution and not solution.final_submit and not solution.final_feedback and allow_create_solutions %} <div class="form-frame"> <h3 style="margin-top: 10px;">Smazání řešení</h3> <p>Toto řešení zatím neobsahuje žádný soubor. Pokud bylo přidáno omylem, můžete ho smazat.</p> diff --git a/mo/web/templates/org_user.html b/mo/web/templates/org_user.html index 05463d5fedf7d748cda5aef70109032cae7decbb..9595d8566dcee426aad7afa1425c7ab4ee103145 100644 --- a/mo/web/templates/org_user.html +++ b/mo/web/templates/org_user.html @@ -79,9 +79,9 @@ {% endif %} <td>{{ pion.state.friendly_name() }} <td><div class="btn-group"> - <a class="btn btn-xs btn-primary" href="{{ url_for('org_contest_user', contest_id=contest.contest_id, user_id=user.user_id) }}">Odevzdané úlohy</a> - <a class="btn btn-xs btn-default" href="{{ url_for('org_contest', id=contest.contest_id) }}">Stránka soutěže</a> - <a class="btn btn-xs btn-default" href="{{ url_for('org_round', id=round.round_id) }}">Stránka kola</a> + <a class="btn btn-xs btn-primary" href="{{ url_for('org_contest_user', ct_id=contest.contest_id, user_id=user.user_id) }}">Odevzdané úlohy</a> + <a class="btn btn-xs btn-default" href="{{ url_for('org_contest', ct_id=contest.contest_id) }}">Stránka soutěže</a> + <a class="btn btn-xs btn-default" href="{{ url_for('org_round', round_id=round.round_id) }}">Stránka kola</a> {% if g.user.is_admin %} <a class="btn btn-xs btn-default" href="{{ log_url('participant', user.user_id) }}">Historie</a> {% endif %} diff --git a/mo/web/templates/parts/org_participants_emails.html b/mo/web/templates/parts/org_participants_emails.html deleted file mode 100644 index b84c2102ec95312adcf1c168346399c3e71bb3a1..0000000000000000000000000000000000000000 --- a/mo/web/templates/parts/org_participants_emails.html +++ /dev/null @@ -1,23 +0,0 @@ -<h3>E-mailové adresy</h3> - -{% if emails %} -<pre>{{ emails|join('\n')|escape }}</pre> -<textarea id="emails-textarea" style="display: none">{{ emails|join('\n')|escape }}</textarea> - -<p> -<a class="btn btn-primary" href="{{ mailto_link }}">Vytvořit e-mail pro {{ count|inflected("adresáta", "adresáty", "adresátů") }}</a> -<button class="btn btn-default" id="copy-emails">Zkopírovat všechny adresy do schránky</button> -<script type="text/javascript"> - var ta = document.getElementById('emails-textarea'); - document.getElementById('copy-emails').addEventListener('click', function () { - ta.style.display = 'block'; - ta.select(); - document.execCommand('copy', false); - ta.style.display = 'none'; - }); -</script> -</p> - -<p>E-mailové adresy účastníků prosím vkládejte do pole pro <b>skrytou kopii (Bcc)</b>, ať si navzájem nevidí své e-maily.</p> - -{% else %}<i>Žádné e-mailové adresy k vypsání.</i>{% endif %} diff --git a/mo/web/templates/parts/org_participants_table_actions.html b/mo/web/templates/parts/org_participants_table_actions.html deleted file mode 100644 index 0eec0d02a253fa76728b4d2623192de5c44acd3e..0000000000000000000000000000000000000000 --- a/mo/web/templates/parts/org_participants_table_actions.html +++ /dev/null @@ -1,67 +0,0 @@ -{% if action_form %} -<form action="" method="POST" class="form form-horizontal" role="form"> -{% endif %} - - {{ table.to_html() }} - - {% if contest %} - <a class="btn btn-primary" href="{{ url_for('org_contest_add_user', id=contest.contest_id, site_id=site.place_id if site else None) }}">Přidat účastníka</a> - {% endif %} - <a class="btn btn-default" - title="Zobrazí emailové adresy ve snadno zkopírovatelném formátu" - href="{{ url_for('org_contest_list_emails', id=id, site_id=site_id, **request.args) if contest else url_for('org_round_list_emails', id=id, **request.args) }}"> - Vypsat e-mailové adresy - </a> - -{% if action_form %} - {{ action_form.csrf_token }} - <h3>Provést akci</h3> - <div class="form-frame"> - <div class="form-group"> - <label class="control-label col-sm-2">Provést akci na:</label> - <div class="col-sm-5 radio"> - <label> - <input id="action_on-0" name="action_on" type="radio" value="all" required{% if action_form.action_on.data == 'all' %} checked{% endif %}> - všech vyfiltrovaných účastnících - </label> - </div><div class="col-sm-5 radio"> - <label> - <input id="action_on-1" name="action_on" type="radio" value="checked" required{% if action_form.action_on.data == 'checked' %} checked{% endif %}> - pouze zaškrtnutých účastnících - </label> - </div> - </div> - <div class="form-group"> - <label class="control-label col-sm-2" for="participation_state">Stav účasti</label> - <div class="col-sm-6">{{ wtf.form_field(action_form.participation_state, form_type='inline') }}</div> - <div class="col-sm-4">{{ wtf.form_field(action_form.set_participation_state, form_type='inline', class='btn btn-primary') }}</div> - </div> - <div class="form-group"> - <label class="control-label col-sm-2" for="participation_place">Soutěžní místo</label> - <div class="col-sm-6">{{ wtf.form_field(action_form.participation_place, form_type='inline', placeholder='Kód místa') }}</div> - <div class="col-sm-4">{{ wtf.form_field(action_form.set_participation_place, form_type='inline', class='btn btn-primary') }}</div> - </div> - <div class="form-group"> - <label class="control-label col-sm-2" for="contest_place"> - {{ round.get_level().name|capitalize }} - </label> - <div class="col-sm-6"> - {{ wtf.form_field(action_form.contest_place, form_type='inline', placeholder='Kód místa') }} - <p class="help-block"> - {{ round.get_level().name_locative("V tomto", "V této", "V tomto") }} musí existovat soutěž pro {{ round.name|lower }} kategorie {{ round.category }}. - </p> - </div> - <div class="col-sm-4">{{ wtf.form_field(action_form.set_contest, form_type='inline', class='btn btn-primary', value='Přesunout do ' + round.get_level().name_genitive('jiného', 'jiné', 'jiného')) }}</div> - </div> - <div class="form-group"> - <label class="control-label col-sm-2" for="contest_place">Smazání účasti</label> - <div class="col-sm-6"><p class="help-block">Dojde ke smazání účasti v tomto kole, ne účastníka z ročníku.</p></div> - <div class="col-sm-4">{{ wtf.form_field(action_form.remove_participation, form_type='inline', class='btn btn-danger') }}</div> - </div> - </div> -</form> -{% else %} -<p> -<i>Nemáte právo k editaci účastníků v {{ round.get_level().name_locative("v tomto", "v této", "v tomto") }}.</i> -</p> -{% endif %} diff --git a/mo/web/templates/parts/org_solution_table.html b/mo/web/templates/parts/org_solution_table.html index c8a85be235225cd49e2808b69129e8d1d39e77c4..90ac16ebdd9af009eccb4b4c0d70a784d6012f00 100644 --- a/mo/web/templates/parts/org_solution_table.html +++ b/mo/web/templates/parts/org_solution_table.html @@ -5,9 +5,9 @@ To se hodí, pokud se nechystáte do systému nahrávat soubory řešení, ale j bylo možné vyplnit body. Pokud nějaké řešení založíte omylem, lze toto prázdné řešení smazat v jeho detailu. {% else %} Historii všech odevzdání, oprav a bodů pro každé řešení naleznete v jeho detailu. -{% if sc.allow_upload_feedback or sc.allow_edit_points %}Tamtéž můžete odevzdávat nové verze a změnit, které řešení/oprava je -finální (ve výchozím stavu poslední nahrané).{% elif sc.allow_upload_solutions %}Tamtéž můžete odevzdat nové řešení.{% endif %} -{% if for_task and sc.allow_create_solutions %} Hromadně založit řešení pro více řešitelů můžete pomocí tlačítek pod tabulkou.{% endif %} +{% if rights.can_upload_feedback() or ctx.can_edit_points() %}Tamtéž můžete odevzdávat nové verze a změnit, které řešení/oprava je +finální (ve výchozím stavu poslední nahrané).{% elif rights.can_upload_solutions() %}Tamtéž můžete odevzdat nové řešení.{% endif %} +{% if for_task and rights.can_create_solutions() %} Hromadně založit řešení pro více řešitelů můžete pomocí tlačítek pod tabulkou.{% endif %} {% endif %} </i></p> @@ -24,8 +24,8 @@ finální (ve výchozím stavu poslední nahrané).{% elif sc.allow_upload_solut <th>Finální oprava <th>Poznámky <th>Přidělené body - {% if not for_user and not site and sc.allow_edit_points and not points_form %} - <a title="Editovat body" href="{{ url_for('org_contest_task_points', contest_id=contest.contest_id, task_id=task.task_id) }}" class="icon pull-right">✎</a> + {% if not for_user and not site and rights.can_edit_points() and not points_form %} + <a title="Editovat body" href="{{ ctx.url_for('org_contest_task_points') }}" class="icon pull-right">✎</a> {% endif %} <th>Akce </tr> @@ -40,7 +40,7 @@ finální (ve výchozím stavu poslední nahrané).{% elif sc.allow_upload_solut <tr> {% endif %} <td>{% if for_user %} - <a href='{{ url_for('org_contest_task', contest_id=ct_id, task_id=task.task_id) }}'>{{ task.code }} {{ task.name }}</a> + <a href='{{ ctx.url_for('org_contest_task', task_id=task.task_id) }}'>{{ task.code }} {{ task.name }}</a> {% else %} {{ u|pion_link(contest.contest_id) }}{{ u|user_flags }}</a> {% endif %} @@ -105,7 +105,7 @@ finální (ve výchozím stavu poslední nahrané).{% elif sc.allow_upload_solut {% else %}–{% endif %} {% endif %} <td><div class="btn-group"> - <a class="btn btn-xs btn-primary" href="{{ url_for('org_submit_list', contest_id=ct_id, user_id=u.user_id, task_id=task.task_id, site_id=site_id) }}">Detail</a> + <a class="btn btn-xs btn-primary" href="{{ ctx.url_for('org_submit_list', user_id=u.user_id, task_id=task.task_id, site_id=ctx.site_id) }}">Detail</a> </div> </tr> {% endfor %} diff --git a/mo/web/templates/parts/org_submit_warning.html b/mo/web/templates/parts/org_submit_warning.html index a405c308c06102e832a7fd710f446db35f7fc772..4095ff3138946450dea1d9587a21bfc06122b659 100644 --- a/mo/web/templates/parts/org_submit_warning.html +++ b/mo/web/templates/parts/org_submit_warning.html @@ -1,10 +1,10 @@ -{% if not sc.allow_upload_solutions and sc.round.state == RoundState.running %} +{% if not ctx.rights.can_upload_solutions() and ctx.round.state == RoundState.running %} <p class='alert alert-warning'> Soutěž stále běží. Odevzdané úlohy se ještě mohou měnit. </p> {% endif %} -{% if not sc.allow_upload_feedback and sc.round.state == RoundState.grading %} +{% if not ctx.rights.can_upload_feedback() and ctx.round.state == RoundState.grading %} <p class='alert alert-warning'> Opravování stále běží. Opravené úlohy a body se ještě mohou měnit. </p> diff --git a/mo/web/util.py b/mo/web/util.py index d6c0eff2142b3b8ce73409d07690c586d60bdd70..1be8eb8027ee958df4216f8bde64904d9e1f2938 100644 --- a/mo/web/util.py +++ b/mo/web/util.py @@ -88,7 +88,7 @@ def org_paper_link(contest_or_id: Union[db.Contest, int], contest_or_id = contest_or_id.contest_id return url_for('org_submit_paper' if not orig else 'org_submit_paper_orig', - contest_id=contest_or_id, + ct_id=contest_or_id, paper_id=paper.paper_id, site_id=site.place_id if site else None, filename=_task_paper_filename(user, paper))