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

Merge branch 'mj/vysledky-pdf' into 'devel'

Sazba výsledkovek TeXem

See merge request !118
parents 898179c0 a2390783
Branches
No related tags found
1 merge request!118Sazba výsledkovek TeXem
#!/usr/bin/env python3
import argparse
import mo.db as db
import mo.jobs.score
from mo.score import Score
from mo.util import init_standalone
import os
import shutil
from sqlalchemy.orm import joinedload
parser = argparse.ArgumentParser(description='Testuje generování výsledkových listin v PDF')
parser.add_argument('--contest', type=int, metavar='ID', help='soutěž')
parser.add_argument('--year', type=int, metavar='ID', help='ročník')
args = parser.parse_args()
init_standalone()
sess = db.get_session()
ctq = sess.query(db.Contest).join(db.Contest.round)
if args.contest is not None:
ctq = ctq.filter(db.Contest.contest_id == args.contest)
else:
ctq = ctq.filter(db.Round.round_type != db.RoundType.other)
if args.year is not None:
ctq = ctq.filter(db.Round.year == args.year)
contests = ctq.options(joinedload(db.Contest.round)).all()
for ct in contests:
r = ct.round
print(f'#{ct.contest_id}: {r.round_code_short()} ({ct.place.get_code()})')
score = Score(r.master, ct)
results = score.get_sorted_results()
for severity, msg in score.get_messages():
if severity != 'info':
print(f'\t{severity}: {msg}')
temp_dir = f'tmp/score/{r.round_code_short()}/{ct.place.get_code()}'
shutil.rmtree(temp_dir, ignore_errors=True)
os.makedirs(temp_dir, exist_ok=True)
pdf = mo.jobs.score.make_score_pdf(temp_dir, score, results)
final = f'{temp_dir}.pdf'
try:
os.unlink(final)
except FileNotFoundError:
pass
os.link(pdf, final)
print(f'\t=> {final}')
...@@ -164,6 +164,7 @@ CREATE TABLE contests ( ...@@ -164,6 +164,7 @@ CREATE TABLE contests (
place_id int NOT NULL REFERENCES places(place_id), place_id int NOT NULL REFERENCES places(place_id),
state round_state NOT NULL DEFAULT 'preparing', -- používá se, pokud round.state='delegate', jinak kopíruje round.state state round_state NOT NULL DEFAULT 'preparing', -- používá se, pokud round.state='delegate', jinak kopíruje round.state
scoretable_id int DEFAULT NULL, -- odkaz na snapshot představující oficiální výsledkovou listinu soutěže scoretable_id int DEFAULT NULL, -- odkaz na snapshot představující oficiální výsledkovou listinu soutěže
tex_hacks text NOT NULL DEFAULT '', -- speciální nastavení pro sazbu výsledkovky
UNIQUE (round_id, place_id) UNIQUE (round_id, place_id)
); );
......
SET ROLE 'mo_osmo';
ALTER TABLE contests ADD COLUMN
tex_hacks text NOT NULL DEFAULT '';
...@@ -13,4 +13,7 @@ python-lsp-server ...@@ -13,4 +13,7 @@ python-lsp-server
rope rope
sqlacodegen sqlacodegen
sqlalchemy-stubs sqlalchemy-stubs
types-python-dateutil
types-requests
whatthepatch
yapf yapf
...@@ -217,6 +217,9 @@ class RoundType(MOEnum): ...@@ -217,6 +217,9 @@ class RoundType(MOEnum):
def friendly_name(self) -> str: def friendly_name(self) -> str:
return round_type_names[self] return round_type_names[self]
def friendly_name_genitive(self) -> str:
return round_type_names_genitive[self]
def letter(self) -> Optional[str]: def letter(self) -> Optional[str]:
"""Písmena se používají při exportu výsledkovky na web MO""" """Písmena se používají při exportu výsledkovky na web MO"""
return round_type_letters[self] return round_type_letters[self]
...@@ -231,6 +234,15 @@ round_type_names = { ...@@ -231,6 +234,15 @@ round_type_names = {
RoundType.other: 'jiné', RoundType.other: 'jiné',
} }
round_type_names_genitive = {
RoundType.domaci: 'domácího kola',
RoundType.skolni: 'školního kola',
RoundType.okresni: 'okresního kola',
RoundType.krajske: 'krajského kola',
RoundType.ustredni: 'ústředního kola',
RoundType.other: 'jiného kola',
}
round_type_letters = { round_type_letters = {
RoundType.domaci: 'D', RoundType.domaci: 'D',
RoundType.skolni: 'S', RoundType.skolni: 'S',
...@@ -439,6 +451,7 @@ class Contest(Base): ...@@ -439,6 +451,7 @@ class Contest(Base):
place_id = Column(Integer, ForeignKey('places.place_id'), nullable=False) place_id = Column(Integer, ForeignKey('places.place_id'), nullable=False)
state = Column(Enum(RoundState, name='round_state'), nullable=False, server_default=text("'preparing'::round_state")) state = Column(Enum(RoundState, name='round_state'), nullable=False, server_default=text("'preparing'::round_state"))
scoretable_id = Column(Integer, ForeignKey('score_tables.scoretable_id'), nullable=True) scoretable_id = Column(Integer, ForeignKey('score_tables.scoretable_id'), nullable=True)
tex_hacks = Column(Text, nullable=False, server_default=text("''::text"))
master = relationship('Contest', primaryjoin='Contest.master_contest_id == Contest.contest_id', remote_side='Contest.contest_id', post_update=True) master = relationship('Contest', primaryjoin='Contest.master_contest_id == Contest.contest_id', remote_side='Contest.contest_id', post_update=True)
place = relationship('Place') place = relationship('Place')
......
...@@ -18,7 +18,7 @@ import mo ...@@ -18,7 +18,7 @@ import mo
import mo.config as config import mo.config as config
import mo.db as db import mo.db as db
from mo.jobs import TheJob, job_handler from mo.jobs import TheJob, job_handler
from mo.util import logger, part_path from mo.util import logger, part_path, tex_arg
import mo.util_format import mo.util_format
...@@ -54,14 +54,6 @@ def schedule_create_protocols(contest: db.Contest, site: Optional[db.Place], for ...@@ -54,14 +54,6 @@ def schedule_create_protocols(contest: db.Contest, site: Optional[db.Place], for
the_job.submit() the_job.submit()
def tex_arg(s: str) -> str:
# Primitivní escapování do TeXu. Nesnaží se ani tak o věrnou intepretaci všech znaků,
# jako o zabránění pádu TeXu kvůli divným znakům.
s = re.sub(r'[\\{}#$%^~]', '?', s)
s = re.sub(r'([&_])', r'\\\1', s)
return '{' + s + '}'
def _get_user_id_query(contest: db.Contest, site_id: Optional[int]) -> Query: def _get_user_id_query(contest: db.Contest, site_id: Optional[int]) -> Query:
q = db.get_session().query(db.Participation.user_id).filter( q = db.get_session().query(db.Participation.user_id).filter(
db.Participation.contest == contest, db.Participation.contest == contest,
......
...@@ -3,20 +3,22 @@ ...@@ -3,20 +3,22 @@
import decimal import decimal
import json import json
import os import os
import subprocess import re
import requests import requests
from typing import Any, Dict, Iterable, Optional import subprocess
from typing import Any, Dict, Iterable, Optional, List
import mo import mo
import mo.config as config import mo.config as config
import mo.db as db import mo.db as db
from mo.jobs import TheJob, job_handler from mo.jobs import TheJob, job_handler
from mo.score import Score from mo.score import Score, ScoreResult
from mo.util import tex_arg, assert_not_none
from mo.util_format import format_decimal from mo.util_format import format_decimal
# #
# Job snapshot_score: Vytvoří kopii aktuální výsledkovky (TODO: a pdf k ní) # Job snapshot_score: Vytvoří kopii aktuální výsledkovky
# #
# Vstupní JSON: # Vstupní JSON:
# { 'contest_id': int, # { 'contest_id': int,
...@@ -45,17 +47,95 @@ class OrderKeyEncoder(json.JSONEncoder): ...@@ -45,17 +47,95 @@ class OrderKeyEncoder(json.JSONEncoder):
return super().encode(o) return super().encode(o)
def make_snapshot_score_pdf(the_job: TheJob, score: db.ScoreTable) -> str: def order_to_tex(result: ScoreResult) -> str:
job = the_job.job if not result.show_order:
contest = score.contest return ""
round = contest.round order = result.order
if order.continuation:
return ""
elif order.span > 1:
return f'{order.place}.--{order.place + order.span -1}.'
else:
return f'{order.place}.'
temp_dir = job.dir_path()
tex_src = os.path.join(temp_dir, 'score.tex') def parse_tex_hacks(tex_hacks: str) -> Dict[str, str]:
hacks = {}
fields = re.split(r'(\w+)={([^}]*)}', tex_hacks)
for i, f in enumerate(fields):
if i % 3 == 0:
if f.strip() != "":
raise RuntimeError('Chyba při parsování tex_hacks')
elif i % 3 == 1:
hacks[f] = fields[i + 1]
return hacks
def format_hacks(tex_hacks: str) -> str:
# Nemůžeme uživatelům dovolit předávat TeXu libovolné příkazy,
# protože bychom jim například zpřístupnili celý filesystem.
lines = []
for k, v in parse_tex_hacks(tex_hacks).items():
lines.append('\\hack' + k + tex_arg(v) + '\n')
return "".join(lines)
def make_score_pdf(temp_dir: str, score: Score, results: List[ScoreResult]) -> str:
# Pozor, toto se volá i z pomocných skriptů
contest = assert_not_none(score.contest)
round = contest.round
tasks = score.get_tasks()
tex_src = os.path.join(temp_dir, 'score.tex')
with open(tex_src, 'w') as f: with open(tex_src, 'w') as f:
f.write('Zde bude výsledkovka\\bye\n') f.write('\\input vysledky.tex\n\n')
hlavicka = f'Výsledková listina {round.round_type.friendly_name_genitive()} {round.year}. ročníku MO kategorie {round.category}'
f.write('\\def\\hlavicka' + tex_arg(hlavicka) + '\n')
if round.level > 0 and contest.place.name:
f.write('\\def\\misto' + tex_arg(contest.place.name) + '\n')
f.write(format_hacks(contest.tex_hacks))
f.write('\n')
last_type = ""
f.write('\\scoretable{\n')
for result in score.get_sorted_results():
if result.winner:
type = 'winner'
elif result.successful:
type = 'successful'
else:
type = 'normal'
if type != last_type:
f.write('\\section' + tex_arg(type) + '\n')
last_type = type
cols = [
order_to_tex(result),
result.user.full_name(),
result.pant.grade,
result.pant.school_place.name,
]
f.write('\\row' + "".join(map(tex_arg, cols)))
f.write('{')
sols = result.get_sols_map()
for task in tasks:
sol = sols.get(task.task_id)
if sol is not None and sol.points is not None:
pts = assert_not_none(format_decimal(sol.points))
else:
pts = '\\n'
f.write('\\p{' + pts + '}')
f.write('}')
f.write(tex_arg(format_decimal(result.get_total_points())) + '\n')
f.write('}\n')
f.write('\n\\bye\n')
env = dict(os.environ) env = dict(os.environ)
env['TEXINPUTS'] = mo.util.part_path('tex') + '//:' env['TEXINPUTS'] = mo.util.part_path('tex') + '//:'
...@@ -69,10 +149,17 @@ def make_snapshot_score_pdf(the_job: TheJob, score: db.ScoreTable) -> str: ...@@ -69,10 +149,17 @@ def make_snapshot_score_pdf(the_job: TheJob, score: db.ScoreTable) -> str:
stdout=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
) )
dir = str(contest.contest_id)
return os.path.join(temp_dir, 'score.pdf')
def make_snapshot_score_pdf(the_job: TheJob, contest: db.Contest, score: Score, results: List[ScoreResult]) -> str:
temp_dir = the_job.job.dir_path()
temp_pdf = make_score_pdf(temp_dir, score, results)
dir = contest.round.round_code_short()
full_dir = os.path.join(mo.util.data_dir('score'), dir) full_dir = os.path.join(mo.util.data_dir('score'), dir)
os.makedirs(full_dir, exist_ok=True) os.makedirs(full_dir, exist_ok=True)
full_name = mo.util.link_to_dir(os.path.join(temp_dir, 'score.pdf'), full_dir, suffix='.pdf') full_name = mo.util.link_to_dir(temp_pdf, full_dir, suffix='.pdf')
return os.path.join(dir, os.path.basename(full_name)) return os.path.join(dir, os.path.basename(full_name))
...@@ -96,9 +183,9 @@ def handle_snapshot_score(the_job: TheJob): ...@@ -96,9 +183,9 @@ def handle_snapshot_score(the_job: TheJob):
snapshot_rows = [] snapshot_rows = []
for result in results: for result in results:
order: Optional[dict] = None order: Optional[dict] = None
if result.successful or not score.want_successful: if result.show_order:
order = result.order.__dict__ order = result.order.__dict__
snapshot_row = { snapshot_row: Dict[str, Any] = {
'order': order, 'order': order,
'winner': result.winner, 'winner': result.winner,
'successful': result.successful, 'successful': result.successful,
...@@ -125,7 +212,7 @@ def handle_snapshot_score(the_job: TheJob): ...@@ -125,7 +212,7 @@ def handle_snapshot_score(the_job: TheJob):
} }
pdf_file = make_snapshot_score_pdf(the_job, score) pdf_file = make_snapshot_score_pdf(the_job, contest, score, results)
score_table = db.ScoreTable( score_table = db.ScoreTable(
contest_id=ct_id, contest_id=ct_id,
...@@ -175,6 +262,7 @@ def api_params(contest: db.Contest) -> Dict[str, Any]: ...@@ -175,6 +262,7 @@ def api_params(contest: db.Contest) -> Dict[str, Any]:
'region': contest.place.nuts, 'region': contest.place.nuts,
} }
def schedule_export_score_to_mo_web(score_table: db.ScoreTable) -> None: def schedule_export_score_to_mo_web(score_table: db.ScoreTable) -> None:
the_job = TheJob() the_job = TheJob()
job = the_job.create(db.JobType.export_score_to_mo_web, db.get_system_user()) job = the_job.create(db.JobType.export_score_to_mo_web, db.get_system_user())
......
...@@ -32,6 +32,7 @@ class Right(Enum): ...@@ -32,6 +32,7 @@ class Right(Enum):
edit_school_users = auto() # Editovat uživatele ze své školy (jen garant_skola) edit_school_users = auto() # Editovat uživatele ze své školy (jen garant_skola)
add_orgs = auto() add_orgs = auto()
edit_orgs = auto() edit_orgs = auto()
edit_tex_hacks = auto() # Nastavovat hacky pro sazbu výsledkovek TeXem
@dataclass @dataclass
...@@ -67,6 +68,7 @@ roles: List[Role] = [ ...@@ -67,6 +68,7 @@ roles: List[Role] = [
Right.edit_all_users, Right.edit_all_users,
Right.add_orgs, Right.add_orgs,
Right.edit_orgs, Right.edit_orgs,
Right.edit_tex_hacks,
}, },
), ),
Role( Role(
......
...@@ -21,6 +21,10 @@ class ScoreOrder: ...@@ -21,6 +21,10 @@ class ScoreOrder:
self.span = span self.span = span
self.continuation = continuation self.continuation = continuation
def __repr__(self) -> str:
cont = '/cont' if self.continuation else ""
return f'ScoreOrder({self.place}+{self.span}{cont})'
class ScoreResult: class ScoreResult:
user: db.User user: db.User
...@@ -29,6 +33,7 @@ class ScoreResult: ...@@ -29,6 +33,7 @@ class ScoreResult:
order: ScoreOrder order: ScoreOrder
successful: bool successful: bool
winner: bool winner: bool
show_order: bool
# Řešení jednotlivých kol (pro některá řazení je potřeba znát i výsledky # Řešení jednotlivých kol (pro některá řazení je potřeba znát i výsledky
# z předcházejících kol). První index je krok (0 = toto kolo, 1 = předcházející, ...) # z předcházejících kol). První index je krok (0 = toto kolo, 1 = předcházející, ...)
...@@ -49,6 +54,7 @@ class ScoreResult: ...@@ -49,6 +54,7 @@ class ScoreResult:
self.order = ScoreResult._null_score_order self.order = ScoreResult._null_score_order
self.winner = False self.winner = False
self.successful = False self.successful = False
self.show_order = True
self._order_key = [] self._order_key = []
def get_sols(self) -> List[db.Solution]: def get_sols(self) -> List[db.Solution]:
...@@ -64,6 +70,10 @@ class ScoreResult: ...@@ -64,6 +70,10 @@ class ScoreResult:
sum += sol.points sum += sol.points
return sum return sum
def __repr__(self) -> str:
hide = "" if self.show_order else '(hidden)'
return f'ScoreResult(user=#{self.user.user_id} order={self.order}{hide} winner={self.winner} succ={self.successful})'
class ScoreTask: class ScoreTask:
task: db.Task task: db.Task
...@@ -232,6 +242,8 @@ class Score: ...@@ -232,6 +242,8 @@ class Score:
total_points = result.get_total_points() total_points = result.get_total_points()
result.winner = self.want_winners and total_points >= self.round.score_winner_limit result.winner = self.want_winners and total_points >= self.round.score_winner_limit
result.successful = self.want_successful and total_points >= self.round.score_successful_limit result.successful = self.want_successful and total_points >= self.round.score_successful_limit
if self.want_successful and not result.successful:
result.show_order = False
def _load_prev_round(self, step: int) -> bool: def _load_prev_round(self, step: int) -> bool:
"""Načtení úloh a řešení předchozího kola, pokud takové existuje.""" """Načtení úloh a řešení předchozího kola, pokud takové existuje."""
...@@ -375,11 +387,12 @@ class Score: ...@@ -375,11 +387,12 @@ class Score:
points_from_max = list(sorted(sol_points.values())) points_from_max = list(sorted(sol_points.values()))
points_from_difficult = [sol_points[task_id] for task_id in tasks_by_difficulty] points_from_difficult = [sol_points[task_id] for task_id in tasks_by_difficulty]
if result.successful or not self.want_successful: if result.show_order:
# Primárně podle počtu získaných bodů, sekundárně podle bodů od maxima, terciárně podle bodů od nejobtížnější # Primárně podle počtu získaných bodů, sekundárně podle bodů od maxima, terciárně podle bodů od nejobtížnější
result._order_key.append((total_points, points_from_max, points_from_difficult)) result._order_key.append((total_points, points_from_max, points_from_difficult))
else: else:
# Neúspěšné řešitele třídíme podle počtu získaných bodů, sekundárně podle jména, jednoznačně podle user_id # Neúspěšné řešitele bez uvedeného pořadí třídíme podle počtu získaných bodů, sekundárně podle jména,
# jednoznačně podle user_id
result._order_key.append((total_points, result.user.name_sort_key(), result.user.user_id)) result._order_key.append((total_points, result.user.name_sort_key(), result.user.user_id))
# Otestujeme, jestli teď existují sdílená místa # Otestujeme, jestli teď existují sdílená místa
......
% Obecná makra pro sazbu věcí okolo MO
\newbox\ellipsisbox
\setbox\ellipsisbox=\hbox{\bf~\dots~~}
\directlua{
function cut_box(box_nr, max_w)
local box = tex.box[box_nr]
% nodetree.analyze(box)
local n
local total_w = 0
local last_visible
for n in node.traverse(box.head) do
local w, h, d = node.dimensions(n, n.next)
total_w = total_w + w
if total_w > max_w then
local new = node.copy_list(box.head, last_visible.next)
tex.box[box_nr] = node.hpack(new)
% nodetree.analyze(tex.box[box_nr])
return
end
if n.id == 0 or n.id == 2 or n.id == 29 then % hlist, rule, glyph
last_visible = n
end
end
end
}
\def\limitedbox#1#2{%
\setbox0=\hbox{#2}%
\ifdim \wd0 > #1\relax
\dimen0=\dimexpr #1 - \wd\ellipsisbox\relax
\directlua{cut_box(0, tex.dimen[0])}%
\setbox0=\hbox{\box0\copy\ellipsisbox}%
\fi
\box0
}
\input ltluatex.tex \input ltluatex.tex
\input luatex85.sty \input luatex85.sty
\input ucwmac2.tex \input ucwmac2.tex
\input mo-lib.tex
\setmargins{15mm} \setmargins{15mm}
\setuppage \setuppage
...@@ -23,42 +24,6 @@ ...@@ -23,42 +24,6 @@
\def\kolo{TODO} \def\kolo{TODO}
\def\kat{TODO} \def\kat{TODO}
\newbox\ellipsisbox
\setbox\ellipsisbox=\hbox{\bf~\dots~~}
\directlua{
function cut_box(box_nr, max_w)
local box = tex.box[box_nr]
% nodetree.analyze(box)
local n
local total_w = 0
local last_visible
for n in node.traverse(box.head) do
local w, h, d = node.dimensions(n, n.next)
total_w = total_w + w
if total_w > max_w then
local new = node.copy_list(box.head, last_visible.next)
tex.box[box_nr] = node.hpack(new)
% nodetree.analyze(tex.box[box_nr])
return
end
if n.id == 0 or n.id == 2 or n.id == 29 then % hlist, rule, glyph
last_visible = n
end
end
end
}
\def\limitedbox#1#2{%
\setbox0=\hbox{#2}%
\ifdim \wd0 > #1\relax
\dimen0=\dimexpr #1 - \wd\ellipsisbox\relax
\directlua{cut_box(0, tex.dimen[0])}%
\setbox0=\hbox{\box0\copy\ellipsisbox}%
\fi
\box0
}
\def\field#1#2{\hbox to #1{\limitedbox{#1}{#2}\hss}} \def\field#1#2{\hbox to #1{\limitedbox{#1}{#2}\hss}}
\def\fillin#1{\smash{\lower 2pt\hbox to #1{\hrulefill}}} \def\fillin#1{\smash{\lower 2pt\hbox to #1{\hrulefill}}}
......
\input ltluatex.tex
\input luatex85.sty
\input ucwmac2.tex
\input mo-lib.tex
\setmargins{15mm}
\setuppage
\nopagenumbers
\raggedbottom
\ucwmodule{luaofs}
\settextsize{12}
\ofsdeclarefamily [Pagella] {%
\loadtextfam qplr;%
qplb;%
qpli;%
qplbi;;%
}
\def\MSfeat#1{:mode=node;script=latn;+tlig}
\registertfm qplr - file:texgyrepagella-regular.otf\MSfeat{}
\registertfm qplb - file:texgyrepagella-bold.otf\MSfeat{}
\registertfm qpli - file:texgyrepagella-italic.otf\MSfeat{}
\registertfm qplbi - file:texgyrepagella-bolditalic.otf\MSfeat{}
\setfonts[Pagella/12]
\uselanguage{czech}
\frenchspacing
% Základní údaje o soutěži
\def\hlavicka{TODO}
\def\misto{}
\def\podhlavicka{}
% Řádky tabulky
\newtoks\rows
\def\scoretable#1{
\heading
\rows={#1}
\findpointswidth
\tryone
}
\def\tryone{
% První pokus: vše na plnou šířku, široké mezery mezi sloupci
\colskip=20pt
\pointswidth=\dimexpr\rawpointswidth + 6pt\relax
\trytable
\ifdim\wd0 < \hsize
\finaltable
\else
\trytwo
\fi
}
\def\trytwo{
% Druhý pokus: vše na plnou šířku, ale zúžíme mezery mezi sloupci
\colskip=10pt
\pointswidth=\dimexpr\rawpointswidth + 4pt\relax
\trytable
\ifdim\wd0 < \hsize
\finaltable
\else
\trythree
\fi
}
\def\trythree{
% Třetí pokus: zkracujeme názvy škol
% Nejdřív si změříme, jak bude tabulka široká bez nich
{
\let\row=\rownoschools
\trytable
\global\schoolwd=\dimexpr\hsize - \wd0\relax
}
\let\row=\rowshortschools
\ifdim\schoolwd < 30pt
% Lepší je nechat tabulku trochu přetéci než zkrátit školy absurdně
\schoolwd=30pt
\fi
\finaltable
}
%%% Hlavička %%%
\def\heading{
\centerline{\bf\hlavicka}
\ifx\misto\empty\else
\medskip
\centerline{\bf\misto}
\fi
\ifx\podhlavicka\empty\else
\medskip
\centerline{\bf\podhlavicka}
\fi
\bigskip
\hrule
\bigskip
}
%%% Oddíly výsledkovky %%%
\def\section#1{%
\noalign{\goodbreak\bigskip}
\multispan6\hfil\bf \csname sec#1\endcsname\hfil\cr
\noalign{\nobreak\medskip}
}
\def\secwinner{Vítězové}
\def\secsuccessful{Úspěšní řešitelé}
\def\secnormal{Ostatní účastníci}
%%% Hlavní tabulka %%%
% Mezera mezi sloupci
\newdimen\colskip
\def\cs{\hskip\colskip}
% Pružná mezera na levém a pravém okraji tabulky
% (uplatní se pouze, pokud sázíme finální tabulku roztaženou na \hsize)
\newskip\borderskip
\borderskip=0pt plus 1fil minus 1fil
\def\maintable#1{%
{\tabskip=\borderskip
\halign#1{%
\tabskip=0pt
##\hfil\cs&% pořadí
\it ##\hfil\cs&% jméno
\hfil##\hfil\cs&% ročník
##\hfil\cs&% škola
##\p{}&% body za úlohy (a pak mezera na šířku počtu bodů)
\hfil##% celkové body
\tabskip=\borderskip\cr
\the\rows
}}
}
% Volá se \row{1.}{Pokusný Králík}{3/4}{Gymnázium, Praha 8, U Libeňského zámku 1}{\p{10}\p{8}\p{\n}}{5}
% #1 #2 #3 #4 #5 #6
\def\row#1#2#3#4#5#6{#1&#2&#3&#4&#5&#6\cr}
\def\rownoschools#1#2#3#4#5#6{#1&#2&#3&&#5&#6\cr}
\newdimen\schoolwd
\def\rowshortschools#1#2#3#4#5#6{#1&#2&#3&\limitedbox{\schoolwd}{#4}&#5&#6\cr}
\def\p#1{\hbox to \pointswidth{\hfil #1}}
\def\n{--}
\def\trytable{
\setbox0=\vbox{\maintable{}}
}
\def\finaltable{
\maintable{to \hsize}
}
%%% Stanovení maximální šířky počtu bodů %%%
\newdimen\rawpointswidth
\newdimen\pointswidth
\def\findpointswidth{{
\let\section=\ignoreone
\let\row=\fpwrow
\let\p=\fpwp
\setbox0=\vbox{\halign{\hfil ##\hfil\cr\the\rows}}
\global\rawpointswidth=\wd0
}}
\def\ignoreone#1{}
\def\fpwrow#1#2#3#4#5#6{#5}
\def\fpwp#1{\hfil #1\cr}
%%% Hacky %%%
\def\hackpodhlavicka#1{\def\podhlavicka{#1}}
...@@ -202,3 +202,16 @@ T = TypeVar('T') ...@@ -202,3 +202,16 @@ T = TypeVar('T')
def assert_not_none(x: Optional[T]) -> T: def assert_not_none(x: Optional[T]) -> T:
assert x is not None assert x is not None
return x return x
def tex_arg(s: Any) -> str:
# Primitivní escapování do TeXu. Nesnaží se ani tak o věrnou intepretaci všech znaků,
# jako o zabránění pádu TeXu kvůli divným znakům.
if s is None:
s = ""
elif type(s) != str:
s = str(s)
s = re.sub(r'[\\{}#$%^~]', '?', s)
s = re.sub(r'([&_])', r'\\\1', s)
s = re.sub(r' - ', r' -- ', s)
return '{' + s + '}'
...@@ -1557,6 +1557,8 @@ class ContestEditForm(FlaskForm): ...@@ -1557,6 +1557,8 @@ class ContestEditForm(FlaskForm):
state = wtforms.SelectField("Stav soutěže", state = wtforms.SelectField("Stav soutěže",
choices=[ch for ch in db.RoundState.choices() if ch[0] != 'delegate'], choices=[ch for ch in db.RoundState.choices() if ch[0] != 'delegate'],
coerce=db.RoundState.coerce) coerce=db.RoundState.coerce)
tex_hacks = mo_fields.String("Nastavení sazby",
description="Speciální nastavení sazby výsledkové listiny (klíč={hodnota}).")
submit = wtforms.SubmitField('Uložit') submit = wtforms.SubmitField('Uložit')
force_submit = wtforms.SubmitField('Uložit s chybami') force_submit = wtforms.SubmitField('Uložit s chybami')
...@@ -1575,6 +1577,9 @@ def org_contest_edit(ct_id: int): ...@@ -1575,6 +1577,9 @@ def org_contest_edit(ct_id: int):
form.state.render_kw = {'disabled': ""} form.state.render_kw = {'disabled': ""}
form.state.description = 'Nastavení kola neumožňuje měnit stav soutěže.' form.state.description = 'Nastavení kola neumožňuje měnit stav soutěže.'
if not ctx.rights.have_right(Right.edit_tex_hacks):
del form.tex_hacks
do_submit = False do_submit = False
errors = [] errors = []
offer_force_submit = False offer_force_submit = False
......
...@@ -183,7 +183,7 @@ def org_score(round_id: Optional[int] = None, hier_id: Optional[int] = None, ct_ ...@@ -183,7 +183,7 @@ def org_score(round_id: Optional[int] = None, hier_id: Optional[int] = None, ct_
local_pion_ct_id = subcontest_id_map[(round.round_id, pion.contest_id)] local_pion_ct_id = subcontest_id_map[(round.round_id, pion.contest_id)]
order_cell: Union[Cell, str] order_cell: Union[Cell, str]
if result.successful or not score.want_successful: if result.show_order:
order_cell = OrderCell(result.order.place, result.order.span, result.order.continuation) order_cell = OrderCell(result.order.place, result.order.span, result.order.continuation)
else: else:
order_cell = "" order_cell = ""
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment