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>