Skip to content
Snippets Groups Projects
Commit b453a88e authored by Martin Mareš's avatar Martin Mareš Committed by Jan Prachař
Browse files

Hierarchie: Kola a statistiky

parent 365d9313
Branches
No related tags found
No related merge requests found
...@@ -739,6 +739,7 @@ class RegionContestStat(Base): ...@@ -739,6 +739,7 @@ class RegionContestStat(Base):
round_id = Column(Integer, ForeignKey('rounds.round_id'), primary_key=True) round_id = Column(Integer, ForeignKey('rounds.round_id'), primary_key=True)
region = Column(Integer, ForeignKey('places.place_id'), primary_key=True) region = Column(Integer, ForeignKey('places.place_id'), primary_key=True)
state = Column(Enum(RoundState, name='round_state'), primary_key=True)
count = Column(Integer, nullable=False) count = Column(Integer, nullable=False)
round = relationship('Round') round = relationship('Round')
......
...@@ -97,6 +97,14 @@ class Context: ...@@ -97,6 +97,14 @@ class Context:
for p in parents[1:]: for p in parents[1:]:
elements.append((url_for('org_round', round_id=self.round_id, hier_id=p.place_id), p.name or '???')) elements.append((url_for('org_round', round_id=self.round_id, hier_id=p.place_id), p.name or '???'))
if self.contest: if self.contest:
if self.round.level >= 2:
parents = g.gatekeeper.get_parents(self.contest.place)
parents = sorted(parents, key=lambda p: p.level)
for i in range(1, len(parents) - 1):
p = parents[i]
if p.level >= 3:
break
elements.append((url_for('org_round', round_id=self.round_id, hier_id=p.place_id), db.Place.get_code(p)))
elements.append((url_for('org_contest', ct_id=self.ct_id), self.contest.place.name or '???')) elements.append((url_for('org_contest', ct_id=self.ct_id), self.contest.place.name or '???'))
if self.site: 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}")) elements.append((url_for('org_contest', ct_id=self.ct_id, site_id=self.site_id), f"soutěžní místo {self.site.name}"))
......
from dataclasses import dataclass, field
import decimal import decimal
from flask import render_template, g, redirect, flash, request from flask import render_template, g, redirect, flash, request
import locale import locale
...@@ -8,9 +9,9 @@ from bleach.sanitizer import ALLOWED_TAGS ...@@ -8,9 +9,9 @@ from bleach.sanitizer import ALLOWED_TAGS
import markdown import markdown
import os import os
from sqlalchemy import func from sqlalchemy import func
from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload, aliased
from sqlalchemy.sql.functions import coalesce from sqlalchemy.sql.functions import coalesce
from typing import Optional from typing import Optional, List, Dict, Tuple, Set
import werkzeug.exceptions import werkzeug.exceptions
import wtforms import wtforms
from wtforms import validators, ValidationError from wtforms import validators, ValidationError
...@@ -182,44 +183,98 @@ def create_subcontests(master_round: db.Round, master_contest: db.Contest): ...@@ -182,44 +183,98 @@ 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.logger.info(f"Podsoutěž #{subcontest.contest_id} založena: {db.row2dict(subcontest)}")
@app.route('/org/contest/r/<int:round_id>/', methods=('GET', 'POST')) @dataclass
def org_round(round_id: int): class ContestStat:
sess = db.get_session() region: db.Place
ctx = get_context(round_id=round_id) contest: Optional[db.Contest] = None
round = ctx.round num_contests: int = 0
rights = ctx.rights contest_states: Set[db.RoundState] = field(default_factory=set)
lowest_state: db.RoundState = db.RoundState.closed
num_active_pants: int = 0
num_unconfirmed_pants: int = 0
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
contests_counts = (sess.query(
db.Contest,
coalesce(participants_count.c.count, 0)
).outerjoin(participants_count, db.Contest.master_contest_id == participants_count.c.contest_id)
.filter(db.Contest.round == round)
.options(joinedload(db.Contest.place))
.all())
contests_counts.sort(key=lambda c: locale.strxfrm(c[0].place.name)) def region_stats(round: db.Round, region: db.Place) -> List[ContestStat]:
stats: Dict[int, ContestStat] = {}
sess = db.get_session()
sol_counts_q = ( if region.level > round.level:
sess.query(db.Solution.task_id, func.count(db.Solution.task_id)) return []
.filter(db.Solution.task_id.in_(
sess.query(db.Task.task_id).filter_by(round=round) if (region.level >= round.level - 1
)) or region.level == 2 and round.level == 4):
# List individual contests
q = sess.query(db.Contest).filter_by(round=round)
q = db.filter_place_nth_parent(q, db.Contest.place_id, round.level - region.level, region.place_id)
q = q.options(joinedload(db.Contest.place))
for c in q.all():
s = ContestStat(region=c.place, contest=c, num_contests=1)
stats[c.place.place_id] = s
have_contests = True
else:
# List sub-regions
regs = sess.query(db.Place).filter(db.Place.parent_place == region).all()
for r in regs:
s = ContestStat(region=r)
stats[r.place_id] = s
have_contests = False
region_ids = [s.region.place_id for s in stats.values()]
if not have_contests:
rcs = (sess.query(db.RegionContestStat)
.filter_by(round=round)
.filter(db.RegionContestStat.region.in_(region_ids))
.all())
for r in rcs:
stats[r.region].num_contests += r.count
stats[r.region].contest_states.add(r.state)
if r.state > stats[r.region].lowest_state:
stats[r.region].lowest_state = r.state
rs = (sess.query(db.RegionParticipantStat)
.filter_by(round_id=round.master_round_id)
.filter(db.RegionParticipantStat.region.in_(region_ids))
.all())
for r in rs:
if r.state == db.PartState.active:
stats[r.region].num_active_pants = r.count
elif r.state == db.PartState.registered:
stats[r.region].num_unconfirmed_pants = r.count
out = list(stats.values())
out.sort(key=lambda s: locale.strxfrm(s.region.name or ""))
return out
def region_totals(region: db.Place, stats: List[ContestStat]) -> ContestStat:
return ContestStat(
region=region,
num_contests=sum(s.num_contests for s in stats),
num_active_pants=sum(s.num_active_pants for s in stats),
num_unconfirmed_pants=sum(s.num_unconfirmed_pants for s in stats),
) )
sol_counts = {}
for task_id, count in sol_counts_q.group_by(db.Solution.task_id).all():
sol_counts[task_id] = count
def task_stats(round: db.Round, region: db.Place) -> List[Tuple[db.Task, int]]:
sess = db.get_session()
tasks = sess.query(db.Task).filter_by(round=round).all() tasks = sess.query(db.Task).filter_by(round=round).all()
tasks.sort(key=lambda t: [int(u) for u in t.code.lstrip('ABCDEFG').split('-')]) tasks.sort(key=lambda t: [int(u) for u in t.code.lstrip('ABCDEFG').split('-')])
for task in tasks:
task.sol_count = sol_counts[task.task_id] if task.task_id in sol_counts else 0 ts = (sess.query(db.RegionTaskStat)
.filter_by(round=round, region=region.place_id)
.all())
count_by_id = {s.task_id: s.count for s in ts}
return [(t, count_by_id.get(t.task_id, 0)) for t in tasks]
@app.route('/org/contest/r/<int:round_id>/', methods=('GET', 'POST'))
@app.route('/org/contest/r/<int:round_id>/h/<int:hier_id>', methods=('GET', 'POST'))
def org_round(round_id: int, hier_id: Optional[int] = None):
ctx = get_context(round_id=round_id, hier_id=hier_id)
round = ctx.round
rights = ctx.rights
form_delete_task = TaskDeleteForm() form_delete_task = TaskDeleteForm()
if rights.have_right(Right.manage_round) and delete_task(round_id, form_delete_task): if rights.have_right(Right.manage_round) and delete_task(round_id, form_delete_task):
...@@ -233,13 +288,19 @@ def org_round(round_id: int): ...@@ -233,13 +288,19 @@ def org_round(round_id: int):
group_rounds = round.get_group_rounds(True) group_rounds = round.get_group_rounds(True)
group_rounds.sort(key=lambda r: r.round_code()) group_rounds.sort(key=lambda r: r.round_code())
region = ctx.hier_place or db.get_root_place()
reg_stats = region_stats(round, region)
reg_total = region_totals(region, reg_stats)
task_info = task_stats(round, region)
return render_template( return render_template(
'org_round.html', 'org_round.html',
ctx=ctx, rights=rights, ctx=ctx, rights=rights,
round=round, group_rounds=group_rounds, round=round, group_rounds=group_rounds,
roles=[r.friendly_name() for r in rights.get_roles()], roles=[r.friendly_name() for r in rights.get_roles()],
contests_counts=contests_counts, reg_stats=reg_stats, reg_total=reg_total,
tasks=tasks, form_delete_task=form_delete_task, task_info=task_info,
form_delete_task=form_delete_task,
form_add_contest=form_add_contest, form_add_contest=form_add_contest,
statement_exists=mo.web.util.task_statement_exists(round), statement_exists=mo.web.util.task_statement_exists(round),
) )
......
{% extends "base.html" %} {% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %} {% import "bootstrap/wtf.html" as wtf %}
{% set can_manage_round = rights.have_right(Right.manage_round) %} {% set in_hier = ctx.hier_id != None %}
{% set can_manage_round = rights.have_right(Right.manage_round) and not in_hier %}
{% set can_manage_contest = rights.have_right(Right.manage_contest) %} {% set can_manage_contest = rights.have_right(Right.manage_contest) %}
{% set can_view_contestants = rights.have_right(Right.view_contestants) %} {% set can_view_contestants = rights.have_right(Right.view_contestants) %}
{% set can_handle_submits = rights.have_right(Right.view_submits) %} {% set can_handle_submits = rights.have_right(Right.view_submits) %}
...@@ -8,7 +9,9 @@ ...@@ -8,7 +9,9 @@
{% set can_view_statement = rights.can_view_statement() %} {% set can_view_statement = rights.can_view_statement() %}
{% set can_add_contest = g.gatekeeper.rights_generic().have_right(Right.add_contest) %} {% set can_add_contest = g.gatekeeper.rights_generic().have_right(Right.add_contest) %}
{% block title %}{{ round.name }} kategorie {{ round.category }}{% endblock %} {% block title %}
{{ round.name }} kategorie {{ round.category }}{% if in_hier %}: {{ ctx.hier_place.name }}{% endif %}
{% endblock %}
{% block breadcrumbs %} {% block breadcrumbs %}
{{ ctx.breadcrumbs() }} {{ ctx.breadcrumbs() }}
{% endblock %} {% endblock %}
...@@ -64,7 +67,7 @@ ...@@ -64,7 +67,7 @@
{% if can_view_contestants %} {% if can_view_contestants %}
<a class="btn btn-primary" href='{{ ctx.url_for('org_generic_list') }}'>Seznam účastníků</a> <a class="btn btn-primary" href='{{ ctx.url_for('org_generic_list') }}'>Seznam účastníků</a>
{% endif %} {% endif %}
{% if (round.state in [RoundState.grading, RoundState.delegate] and can_view_submits) or round.state == RoundState.closed %} {% if (can_view_contestants and round.state in [RoundState.grading, RoundState.delegate] or round.state == RoundState.closed) and not in_hier %}
<a class="btn btn-primary" href='{{ url_for('org_score', round_id=round.round_id) }}'>Výsledky</a> <a class="btn btn-primary" href='{{ url_for('org_score', round_id=round.round_id) }}'>Výsledky</a>
{% endif %} {% endif %}
{% if can_manage_contest %} {% if can_manage_contest %}
...@@ -77,27 +80,33 @@ ...@@ -77,27 +80,33 @@
{% if round.has_messages %} {% if round.has_messages %}
<a class="btn btn-default" href='{{ ctx.url_for('org_round_messages') }}'>Zprávičky</a> <a class="btn btn-default" href='{{ ctx.url_for('org_round_messages') }}'>Zprávičky</a>
{% endif %} {% endif %}
{% if g.user.is_admin %} {% if g.user.is_admin and not in_hier %}
<a class="btn btn-default" href='{{ log_url('round', round.round_id) }}'>Historie</a> <a class="btn btn-default" href='{{ log_url('round', round.round_id) }}'>Historie</a>
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}
<h3>Soutěže</h3> <h3>Soutěže</h3>
{% if contests_counts %} {% if reg_total.num_contests %}
{% set show_contests = reg_stats[0].contest != None %}
<table class="table table-bordered table-condensed table-auto"> <table class="table table-bordered table-condensed table-auto">
<thead> <thead>
<tr> <tr>
{% if show_contests %}
<th>{{ round.get_level().name|capitalize }} <th>{{ round.get_level().name|capitalize }}
<th>Stav <th>Stav
{% else %}
<th>{{ reg_stats[0].region.type_name()|capitalize }}
<th>Počet soutěží
<th>Stavy soutěží
{% endif %}
<th>Počet účastníků <th>Počet účastníků
<th>Počet přihlášek
</tr> </tr>
</thead> </thead>
{% for (c, count) in contests_counts %} {% for rs in reg_stats %}
<tr> {% with state=rs.contest.state if show_contests else rs.lowest_state %}
<td><a href='{{ url_for('org_contest', ct_id=c.contest_id) }}'>{{ c.place.name }}</a> <tr
{% with state=c.state %}
<td
{% if state == RoundState.preparing %} {% if state == RoundState.preparing %}
class="warning" class="warning"
{% elif state == RoundState.running %} {% elif state == RoundState.running %}
...@@ -105,15 +114,31 @@ ...@@ -105,15 +114,31 @@
{% elif state == RoundState.grading %} {% elif state == RoundState.grading %}
class="info" class="info"
{% endif %}> {% endif %}>
{{ state.friendly_name() }}
{% endwith %} {% endwith %}
<td>{{ count }} {% if show_contests %}
<td><a href='{{ url_for('org_contest', ct_id=rs.contest.contest_id) }}'>{{ rs.region.name }}</a>
{% with state=rs.contest.state %}
<td>{{ state.friendly_name() }}
{% endwith %}
{% else %}
<td><a href='{{ ctx.url_for('org_round', hier_id=rs.region.place_id) }}'>{{ rs.region.name }}</a>
<td>{{ rs.num_contests }}
<td>{% for s in rs.contest_states %}<span class="joiner">{{ s.friendly_name() }}</span>{% endfor %}
{% endif %}
<td>{{ rs.num_active_pants }}
<td>{{ rs.num_unconfirmed_pants }}
{% endfor %} {% endfor %}
<tfoot> <tfoot>
<tr> <tr>
<th>Celkem <th>Celkem
{% if show_contests %}
<th> <th>
<th>{{ contests_counts|sum(attribute=1) }} {% else %}
<th>{{ reg_total.num_contests }}
<th>
{% endif %}
<th>{{ reg_total.num_active_pants }}
<th>{{ reg_total.num_unconfirmed_pants }}
</tr> </tr>
</tfoot> </tfoot>
</table> </table>
...@@ -130,7 +155,7 @@ ...@@ -130,7 +155,7 @@
{% endif %} {% endif %}
<h3>Úlohy</h3> <h3>Úlohy</h3>
{% if tasks %} {% if task_info %}
<table class="table table-bordered"> <table class="table table-bordered">
<thead> <thead>
<tr> <tr>
...@@ -142,11 +167,11 @@ ...@@ -142,11 +167,11 @@
{% if can_view_submits or can_upload %}<th>Dávkové operace{% endif %} {% if can_view_submits or can_upload %}<th>Dávkové operace{% endif %}
</tr> </tr>
</thead> </thead>
{% for task in tasks %} {% for task, sol_count in task_info %}
<tr> <tr>
<td>{{ task.code }} <td>{{ task.code }}
<td>{{ task.name }} <td>{{ task.name }}
<td>{{ task.sol_count }} <td>{{ sol_count }}
<td>{{ task.max_points|decimal|none_value('–') }} <td>{{ task.max_points|decimal|none_value('–') }}
{% if can_manage_round %} {% if can_manage_round %}
<td> <td>
......
...@@ -96,6 +96,10 @@ footer { ...@@ -96,6 +96,10 @@ footer {
color: #737373; color: #737373;
} }
.joiner + .joiner::before {
content: ", ";
}
span.unknown { span.unknown {
font-weight: bold; font-weight: bold;
color: red; color: red;
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment