Skip to content
Snippets Groups Projects
Commit 754adc40 authored by Martin Mareš's avatar Martin Mareš
Browse files

Hierarchie: Kola a statistiky

parent 081abf58
Branches
No related tags found
1 merge request!95Reforma orgovského rozhraní ke kolům a soutěžím
...@@ -718,6 +718,7 @@ class RegionContestStat(Base): ...@@ -718,6 +718,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')
......
...@@ -93,6 +93,14 @@ class Context: ...@@ -93,6 +93,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
...@@ -157,44 +158,95 @@ def create_subcontests(master_round: db.Round, master_contest: db.Contest): ...@@ -157,44 +158,95 @@ 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:
region: db.Place
contest: Optional[db.Contest] = None
num_contests: int = 0
contest_states: Set[db.RoundState] = field(default_factory=set)
num_active_pants: int = 0
num_unconfirmed_pants: int = 0
def region_stats(round: db.Round, region: db.Place) -> List[ContestStat]:
stats: Dict[int, ContestStat] = {}
sess = db.get_session() sess = db.get_session()
ctx = get_context(round_id=round_id)
round = ctx.round
rights = ctx.rights
participants_count = sess.query( if region.level > round.level:
db.Participation.contest_id, return []
func.count(db.Participation.user_id).label('count')
).group_by(db.Participation.contest_id).subquery() if (region.level >= round.level - 1
or region.level == 2 and round.level == 4):
# Účastníci jsou jen pod master contesty # List individual contests
contests_counts = (sess.query( q = sess.query(db.Contest).filter_by(round=round)
db.Contest, q = db.filter_place_nth_parent(q, db.Contest.place_id, round.level - region.level, region.place_id)
coalesce(participants_count.c.count, 0) q = q.options(joinedload(db.Contest.place))
).outerjoin(participants_count, db.Contest.master_contest_id == participants_count.c.contest_id) for c in q.all():
.filter(db.Contest.round == round) s = ContestStat(region=c.place, contest=c, num_contests=1)
.options(joinedload(db.Contest.place)) 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()) .all())
for r in rcs:
stats[r.region].num_contests += r.count
stats[r.region].contest_states.add(r.state)
contests_counts.sort(key=lambda c: locale.strxfrm(c[0].place.name)) rs = (sess.query(db.RegionParticipantStat)
.filter_by(round_id=round.master_round_id)
sol_counts_q = ( .filter(db.RegionParticipantStat.region.in_(region_ids))
sess.query(db.Solution.task_id, func.count(db.Solution.task_id)) .all())
.filter(db.Solution.task_id.in_( for r in rs:
sess.query(db.Task.task_id).filter_by(round=round) 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: t.code) 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 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):
...@@ -208,13 +260,19 @@ def org_round(round_id: int): ...@@ -208,13 +260,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,13 @@ ...@@ -8,7 +9,13 @@
{% 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 }} {{ round.round_code() }}{% endblock %} {% block title %}
{% if in_hier %}
{{ round.round_code() }}: {{ ctx.hier_place.name }}
{% else %}
{{ round.name }} {{ round.round_code() }}
{% endif %}
{% endblock %}
{% block breadcrumbs %} {% block breadcrumbs %}
{{ ctx.breadcrumbs() }} {{ ctx.breadcrumbs() }}
{% endblock %} {% endblock %}
...@@ -17,7 +24,7 @@ ...@@ -17,7 +24,7 @@
<table class=data style="float: left; margin-right: 10px;"> <table class=data style="float: left; margin-right: 10px;">
<thead> <thead>
<tr><th colspan=2>Parametry kola <i>(nelze editovat)</i> <tr><th colspan=2>Parametry kola{% if can_manage_round %} <i>(nelze editovat)</i>{% endif %}
</thead> </thead>
<tr><td>Ročník<td>{{ round.year }} <tr><td>Ročník<td>{{ round.year }}
<tr><td>Kategorie<td>{{ round.category }} <tr><td>Kategorie<td>{{ round.category }}
...@@ -81,7 +88,7 @@ ...@@ -81,7 +88,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 can_view_contestants and round.state in [RoundState.grading, RoundState.closed, RoundState.delegate] %} {% if can_view_contestants and round.state in [RoundState.grading, RoundState.closed, RoundState.delegate] and not in_hier %}
<a class="btn btn-primary" href='{{ ctx.url_for('org_score') }}'>Výsledky</a> <a class="btn btn-primary" href='{{ ctx.url_for('org_score') }}'>Výsledky</a>
{% endif %} {% endif %}
{% if can_manage_contest %} {% if can_manage_contest %}
...@@ -94,35 +101,56 @@ ...@@ -94,35 +101,56 @@
{% 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=data> <table class=data>
<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> <tr>
<td><a href='{{ url_for('org_contest', ct_id=c.contest_id) }}'>{{ c.place.name }}</a> {% if show_contests %}
{% with state=c.state %} <td><a href='{{ url_for('org_contest', ct_id=rs.contest.contest_id) }}'>{{ rs.region.name }}</a>
{% with state=rs.contest.state %}
<td class='rstate-{{state.name}}'>{{ state.friendly_name() }} <td class='rstate-{{state.name}}'>{{ state.friendly_name() }}
{% endwith %} {% endwith %}
<td>{{ count }} {% 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='rstate-{{s.name}}'>{{ 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>
...@@ -139,7 +167,7 @@ ...@@ -139,7 +167,7 @@
{% endif %} {% endif %}
<h3>Úlohy</h3> <h3>Úlohy</h3>
{% if tasks %} {% if task_info %}
<table class=data> <table class=data>
<thead> <thead>
<tr> <tr>
...@@ -151,11 +179,11 @@ ...@@ -151,11 +179,11 @@
{% if can_handle_submits or can_upload %}<th>Dávkové operace{% endif %} {% if can_handle_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><div class="btn-group"> <td><div class="btn-group">
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment