diff --git a/db/db.ddl b/db/db.ddl index be92253b1738e6992debdced44cf5e55804545fb..7b3dc02526a384c48b54481f366ea20035a5e8e1 100644 --- a/db/db.ddl +++ b/db/db.ddl @@ -404,6 +404,7 @@ CREATE TABLE reg_requests ( -- Statistiky -- Pro každý region spočítáme všechna podřízená místa +-- (každý region je svým vlastním potomkem) CREATE VIEW region_descendants AS WITH RECURSIVE descendant_regions(place_id, descendant) AS ( SELECT place_id, place_id FROM places diff --git a/mo/rights.py b/mo/rights.py index b9724ca31669eae40b3e3492b3d092681037b1fc..fc82662101aee292a4070209dbf22517d93d4a0a 100644 --- a/mo/rights.py +++ b/mo/rights.py @@ -146,6 +146,7 @@ roles: List[Role] = [ rights={ Right.view_contestants, Right.view_submits, + Right.upload_solutions, Right.upload_feedback, Right.edit_points, Right.view_statement, diff --git a/mo/web/org.py b/mo/web/org.py index e27f3587ade8e6c2e6a70b9587400f91585095be..d1b0087277136f9d741d7931e31b144634b16f7e 100644 --- a/mo/web/org.py +++ b/mo/web/org.py @@ -1,8 +1,9 @@ +from collections import defaultdict from dataclasses import dataclass, field from flask import render_template, redirect, url_for, request, flash, g from sqlalchemy import and_, or_, tuple_, not_ from sqlalchemy.orm import aliased, joinedload -from typing import List, Set, Optional, Dict, Tuple +from typing import List, Set, Optional, Tuple, DefaultDict import mo.config as config import mo.db as db @@ -66,19 +67,16 @@ def org_index(): .outerjoin(db.Contest, and_(db.Contest.round_id == db.Round.round_id, db.Contest.place_id == db.UserRole.place_id)) .all()) - # Soutěžní místa, ke kterým máme roli dozoru nebo opravovatele + # Soutěžní místa, ke kterým máme nějakou roli contest_place = aliased(db.Place) place_rcu = (rcu_base .filter(not_(db.Round.state.in_([db.RoundState.preparing, db.RoundState.closed]))) # for performance - .filter(db.Place.level == 4) - .filter(db.Place.level > db.Round.level) - .join(db.RegionDescendant, db.RegionDescendant.descendant == db.Place.place_id) - .join(contest_place, db.RegionDescendant.region == contest_place.place_id) - .filter(contest_place.level == db.Round.level) - .join(db.Contest, and_(db.Contest.round_id == db.Round.round_id, db.Contest.place_id == contest_place.place_id)) + .filter(db.Place.level >= db.Round.level) + .join(db.Contest, db.Contest.round_id == db.Round.round_id) .filter(sess.query(db.Participation) .filter(and_(db.Participation.contest_id == db.Contest.contest_id, - db.Participation.place_id == db.Place.place_id)) + db.Participation.place_id == db.Place.place_id, + db.Participation.place_id != db.Contest.place_id)) .exists()) .all()) @@ -94,7 +92,7 @@ def org_index(): if ct is None and ur.place.level == r.level: continue o = overview[-1] if overview else None - if not (o and o.round == r and o.place == ur.place): + if not (o and o.round == r and o.place == ur.place and o.contest == ct): o = OrgOverview(round=r, place=ur.place, contest=ct) overview.append(o) o.role_set.add(ur.role) @@ -111,38 +109,39 @@ def org_index(): def get_stats(overview: List[OrgOverview]) -> None: sess = db.get_session() - over_by_round_place: Dict[Tuple[int, int], OrgOverview] = {} + requests: DefaultDict[Tuple[int, int], List[OrgOverview]] = defaultdict(list) rcs_for: List[Tuple[int, int]] = [] rps_for: List[Tuple[int, int]] = [] for o in overview: - rp = (o.round.round_id, o.place.place_id) - over_by_round_place[rp] = o if o.contest: o.num_contests = 1 o.contest_states.add(o.contest.state) + rp = (o.round.round_id, o.contest.place_id) else: + rp = (o.round.round_id, o.place.place_id) rcs_for.append(rp) rps_for.append(rp) + requests[rp].append(o) if rcs_for: rcss = (sess.query(db.RegionContestStat) .filter(tuple_(db.RegionContestStat.round_id, db.RegionContestStat.region).in_(rcs_for)) .all()) for rcs in rcss: - o = over_by_round_place[(rcs.round_id, rcs.region)] - o.num_contests += rcs.count - o.contest_states.add(rcs.state) + for o in requests[(rcs.round_id, rcs.region)]: + o.num_contests += rcs.count + o.contest_states.add(rcs.state) if rps_for: rpss = (sess.query(db.RegionParticipantStat) .filter(tuple_(db.RegionParticipantStat.round_id, db.RegionParticipantStat.region).in_(rps_for)) .all()) for rps in rpss: - o = over_by_round_place[(rps.round_id, rps.region)] - if rps.state == db.PartState.active: - o.num_active_pants += rps.count - elif rps.state == db.PartState.registered: - o.num_unconfirmed_pants += rps.count + for o in requests[(rps.round_id, rps.region)]: + if rps.state == db.PartState.active: + o.num_active_pants += rps.count + elif rps.state == db.PartState.registered: + o.num_unconfirmed_pants += rps.count def filter_overview(overview: List[OrgOverview]) -> List[OrgOverview]: diff --git a/mo/web/org_contest.py b/mo/web/org_contest.py index 56d4231beca7af823c32cf2d76f3a607c8753a5e..f9015ea49eff73dcf81bb1f2fb0d90b1fa85a746 100644 --- a/mo/web/org_contest.py +++ b/mo/web/org_contest.py @@ -1861,7 +1861,7 @@ def org_contest_scans(ct_id: int, site_id: Optional[int] = None): round, contest, site = ctx.round, ctx.contest, ctx.site assert contest - if not ctx.rights.can_upload_solutions(): + if not (ctx.rights.can_upload_solutions() or ctx.rights.can_upload_feedback()): raise werkzeug.exceptions.Forbidden() sess = db.get_session() @@ -2108,3 +2108,16 @@ def org_contest_scans_file(ct_id: int, job_id: int, file: str, site_id: Optional return send_file(path) else: raise werkzeug.exceptions.NotFound() + + +@app.route('/org/contest/c/<int:ct_id>/task-statement/zadani.pdf') +@app.route('/org/contest/c/<int:ct_id>/site/<int:site_id>/task-statement/zadani.pdf') +@app.route('/org/contest/r/<int:round_id>/task-statement/zadani.pdf') +def org_task_statement(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) + + 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(ctx.round) diff --git a/mo/web/org_round.py b/mo/web/org_round.py index 799ec890ad3b30bb8b74762a694428a7066e9322..ff7c75198cc140d6d37455d0e19c39b9079c5c88 100644 --- a/mo/web/org_round.py +++ b/mo/web/org_round.py @@ -526,17 +526,6 @@ def _time_crossed(first_field: mo_fields.DateTime, second_field: mo_fields.DateT return first is not None and second is not None and first > second -@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 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(ctx.round) - - class StatementEditForm(FlaskForm): file = flask_wtf.file.FileField("Soubor", render_kw={'autofocus': True}) upload = wtforms.SubmitField('Nahrát') diff --git a/mo/web/templates/org_contest.html b/mo/web/templates/org_contest.html index 9606dfe6640c497d53eb00df9992ff537dff1c2b..b6316dfe0fac158f765622708fc2ba22d57f2adc 100644 --- a/mo/web/templates/org_contest.html +++ b/mo/web/templates/org_contest.html @@ -47,7 +47,7 @@ <tr><td>Zadání<td> {% if round.tasks_file %} {% if can_view_statement %} - <a href='{{ ctx.url_for('org_task_statement', ct_id=None) }}'>stáhnout</a> + <a href='{{ ctx.url_for('org_task_statement') }}'>stáhnout</a> {% elif possible_rights_elsewhere %} viz stránka vaší soutěže/místa {{ rights_elsewhere_info() }} {% else %} diff --git a/mo/web/templates/org_index.html b/mo/web/templates/org_index.html index 53ad47d0ed2dc671afc6040a1668a2e34d1cd1b7..4d75a3ee68a8914e6598fb782973d4fec9676864 100644 --- a/mo/web/templates/org_index.html +++ b/mo/web/templates/org_index.html @@ -15,7 +15,7 @@ <th>Kolo <th>Místo <th>Stav - <th class='has-tip' title='S=soutěže, U=soutěžící účastníci, P=přihlášky čekající na potvrzení'>Statistiky + <th class='has-tip' title='S=soutěže, U=soutěžící účastníci, P=přihlášky čekající na potvrzení. U soutěžních míst se zobrazuje celkový počet účastníků soutěže.'>Statistiky <th>Moje role <th>Odkazy </thead> @@ -42,7 +42,7 @@ {% if o.place == o.contest.place %} <td>{{ o.place.name }} {% else %} - <td><i>{{ o.place.name_locative() }}</i> + <td>{{ o.contest.place.name }} {{ o.place.name_locative() }} {% endif %} {% else %} <td><i>{{ o.place.name_locative() }}</i>