Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • devel
  • fo
  • fo-base
  • honza/add-contestant
  • honza/kolo-vs-soutez
  • honza/mr6
  • honza/mr7
  • honza/mra
  • honza/mrd
  • honza/mrf
  • honza/submit-images
  • jh-stress-test-wip
  • jirka/typing
  • jk/issue-196
  • jk/issue-96
  • master
  • mj/submit-images
  • shorten-schools
18 results

Target

Select target project
  • mj/mo-submit
1 result
Select Git revision
  • devel
  • fo
  • fo-base
  • honza/add-contestant
  • honza/kolo-vs-soutez
  • honza/mr6
  • honza/mr7
  • honza/mra
  • honza/mrd
  • honza/mrf
  • honza/submit-images
  • jh-stress-test-wip
  • jirka/typing
  • jk/issue-196
  • jk/issue-96
  • master
  • mj/submit-images
  • shorten-schools
18 results
Show changes
# Implementace jobů na práci s protokoly
from PIL import Image
from dataclasses import dataclass
import multiprocessing
import os
import poppler
import pyzbar.pyzbar as pyzbar
import re
from sqlalchemy import delete
from sqlalchemy.orm import joinedload
from sqlalchemy.orm.query import Query
import subprocess
from typing import List, Optional
import mo
import mo.config as config
import mo.db as db
from mo.jobs import TheJob, job_handler
from mo.util import logger, part_path
import mo.util_format
#
# Job create_protocols: Vygeneruje formuláře protokolů
#
# Vstupní JSON:
# { 'contest_id': ID contestu,
# 'site_id': ID soutěžního místa nebo none,
# 'task_ids': [task_id, ...],
# 'num_universal': počet papírů s univerzalní hlavičkou,
# 'num_blank': pocet pokračovacích papírů,
# }
#
# Výstupní JSON:
# null
#
def schedule_create_protocols(contest: db.Contest, site: Optional[db.Place], for_user: db.User, tasks: List[db.Task], num_universal: int, num_blank: int):
place = site or contest.place
the_job = TheJob()
job = the_job.create(db.JobType.create_protocols, for_user)
job.description = f'Formuláře protokolů {contest.round.round_code_short()} {place.name}'
job.in_json = {
'contest_id': contest.contest_id,
'site_id': site.place_id if site else None,
'task_ids': [t.task_id for t in tasks],
'num_universal': num_universal,
'num_blank': num_blank,
}
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:
q = db.get_session().query(db.Participation.user_id).filter_by(contest=contest, state=db.PartState.active)
if site_id is not None:
q = q.filter_by(place_id=site_id)
return q
def _get_pants(contest: db.Contest, site_id: Optional[int]) -> List[db.Participant]:
user_id_subq = _get_user_id_query(contest, site_id).subquery()
pants = (db.get_session().query(db.Participant)
.options(joinedload(db.Participant.user), joinedload(db.Participant.school_place))
.filter_by(year=config.CURRENT_YEAR)
.filter(db.Participant.user_id.in_(user_id_subq))
.all())
pants.sort(key=lambda p: p.user.sort_key())
return pants
@job_handler(db.JobType.create_protocols)
def handle_create_protocols(the_job: TheJob):
job = the_job.job
assert job.in_json is not None
contest_id: int = job.in_json['contest_id'] # type: ignore
site_id: int = job.in_json['site_id'] # type: ignore
task_ids: List[int] = job.in_json['task_ids'] # type: ignore
num_universal: int = job.in_json['num_universal'] # type: ignore
num_blank: int = job.in_json['num_blank'] # type: ignore
sess = db.get_session()
contest = sess.query(db.Contest).options(joinedload(db.Contest.round)).get(contest_id)
assert contest is not None
round = contest.round
pants = _get_pants(contest, site_id)
tasks = sess.query(db.Task).filter_by(round=round).filter(db.Task.task_id.in_(task_ids)).order_by(db.Task.code).all()
pages = []
for p in pants:
for t in tasks:
args = [
':'.join(['MO', round.round_code_short(), t.code, str(p.user_id)]),
p.user.full_name(),
p.grade,
p.school_place.name or '???',
t.code,
]
pages.append('\\proto' + "".join([tex_arg(x) for x in args]))
for _ in range(num_universal):
pages.append('\\universal')
for _ in range(num_blank):
pages.append('\\blank')
if not pages:
the_job.error("Nebyly vyžádány žádné protokoly")
return
temp_dir = job.dir_path()
logger.debug('Job: Vytvářím protokoly v %s (%s listů)', temp_dir, len(pages))
tex_src = os.path.join(temp_dir, 'protokoly.tex')
with open(tex_src, 'w') as f:
f.write('\\input protokol.tex\n\n')
kolo = f'{round.name} {round.year}. ročníku Matematické olympiády'
kat = f'Kategorie {round.category}'
if round.level > 0:
kat += ', ' + contest.place.name
f.write('\\def\\kolo' + tex_arg(kolo) + '\n\n')
f.write('\\def\\kat' + tex_arg(kat) + '\n\n')
for p in pages:
f.write(p + '\n')
f.write('\n\\bye\n')
env = dict(os.environ)
env['TEXINPUTS'] = part_path('tex') + '//:'
subprocess.run(
['luatex', '--interaction=errorstopmode', 'protokoly.tex'],
check=True,
cwd=temp_dir,
env=env,
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
job.out_file = 'protokoly.pdf'
job.result = 'Celkem ' + mo.util_format.inflect_number(len(pages), 'list', 'listy', 'listů')
#
# Job process_scans: Zpracuje nascanované protokoly
#
# Vstupní JSON:
# { 'contest_id': ID contestu,
# 'site_id': ID soutěžního místa nebo none,
# 'task_ids': [task_id, ...],
# 'in_files': [názvy vstupních souborů]
# }
#
# Výstupní JSON:
# null
#
# Výstupn soubory:
# p-{file_nr:02d}-{page_nr:04d}-(full|small).png
#
def schedule_process_scans(contest: db.Contest, site: Optional[db.Place], for_user: db.User, tasks: List[db.Task], in_file_names: List[str]):
place = site or contest.place
the_job = TheJob()
job = the_job.create(db.JobType.process_scans, for_user)
job.description = f'Zpracování scanů {contest.round.round_code_short()} {place.name}'
in_files = []
num_files = 0
for ifn in in_file_names:
num_files += 1
in_name = f'input-{num_files:03d}.pdf'
the_job.attach_file(ifn, in_name)
in_files.append(in_name)
assert in_files
job.in_json = {
'contest_id': contest.contest_id,
'site_id': site.place_id if site else None,
'task_ids': [t.task_id for t in tasks],
'in_files': in_files,
}
the_job.submit()
@dataclass
class ScanJobArgs:
in_path: str
out_prefix: str
@dataclass
class ScanJobPage:
code: Optional[str]
@job_handler(db.JobType.process_scans)
def handle_process_scans(the_job: TheJob):
job = the_job.job
assert job.in_json is not None
contest_id = job.in_json['contest_id'] # type: ignore
site_id = job.in_json['site_id'] # type: ignore
task_ids = job.in_json['task_ids'] # type: ignore
in_files: List[str] = job.in_json['in_files'] # type: ignore
sess = db.get_session()
contest = sess.query(db.Contest).options(joinedload(db.Contest.round)).get(contest_id)
assert contest is not None
round = contest.round
round_code = round.round_code_short()
user_ids = set(u[0] for u in _get_user_id_query(contest, site_id).all())
tasks = sess.query(db.Task).filter(db.Task.task_id.in_(task_ids)).all()
tasks_by_code = {t.code: t for t in tasks}
# Jelikož se plánujeme zamyslet na dlouhou dobu, uzavřeme databázovou session.
sess.commit()
with multiprocessing.Pool(1) as pool:
args = [ScanJobArgs(in_path=job.file_path(fn),
out_prefix=job.file_path(f'p-{fi:02d}'))
for fi, fn in enumerate(in_files)]
results = pool.map(_process_scan_file, args)
def _parse_code(pr: ScanJobPage, sp: db.ScanPage) -> Optional[str]:
if pr.code is None:
return None
fields = pr.code.split(':')
if fields[0] != 'MO':
return 'Neznámý prefix'
if len(fields) == 2:
if fields[1] == '*':
# Univerzální hlavička úlohy
sp.seq_id = db.SCAN_PAGE_FIX
return None
if fields[1] == '+':
# Pokračovací papír s kódem
sp.seq_id = db.SCAN_PAGE_CONTINUE
return None
elif len(fields) == 4:
if not fields[3].isnumeric():
return 'User ID není číslo'
user_id = int(fields[3])
if fields[1] != round_code:
return 'Nesouhlasí kód kola'
if fields[2] not in tasks_by_code:
return 'Neznámá úloha'
if user_id not in user_ids:
return 'Neznámý účastník'
sp.user_id = user_id
sp.task_id = tasks_by_code[fields[2]].task_id
sp.seq_id = 0
return None
return 'Neznamý formát kódu'
# Pokud jsme job spustili podruhé (ruční retry), chceme smazat všechny záznamy v scan_pages.
# Pozor, nesynchronizujeme ORM, ale nevadí to, protože v této chvíli mame čerstvou session.
conn = sess.connection()
conn.execute(delete(db.ScanPage.__table__).where(db.ScanPage.job_id == job.job_id))
num_pages = 0
for fi, fn in enumerate(in_files):
for pi, pr in enumerate(results[fi]):
sp = db.ScanPage(
job_id=job.job_id,
file_nr=fi,
page_nr=pi,
seq_id=db.SCAN_PAGE_FIX,
)
err = _parse_code(pr, sp)
if err is not None:
logger.debug(f'Scan: {fi}/{pi} ({pr.code}): {err}')
sp.seq_id = db.SCAN_PAGE_UFO
sess.add(sp)
num_pages += 1
job.result = 'Celkem ' + mo.util_format.inflect_number(num_pages, 'strana', 'strany', 'stran')
the_job.expires_in_minutes = config.JOB_EXPIRATION_LONG
def _process_scan_file(args: ScanJobArgs) -> List[ScanJobPage]:
# Zpracuje jeden soubor se scany. Běží v odděleném procesu.
# FIXME: Ošetření chyb
logger.debug(f'Scan: Analyzuji soubor {args.in_path}')
pdf = poppler.load_from_file(args.in_path)
renderer = poppler.PageRenderer()
renderer.set_render_hint(poppler.RenderHint.antialiasing, True)
renderer.set_render_hint(poppler.RenderHint.text_antialiasing, True)
dpi = 300
output = []
for page_nr in range(pdf.pages):
page = pdf.create_page(page_nr)
page_img = renderer.render_page(page, xres=dpi, yres=dpi)
full_img = Image.frombytes(
"RGBA",
(page_img.width, page_img.height),
page_img.data,
"raw",
str(page_img.format),
)
del page_img
full_img = full_img.convert('L') # Grayscale
full_size = full_img.size
codes = pyzbar.decode(full_img, symbols=[pyzbar.ZBarSymbol.QRCODE])
codes = [c for c in codes if c.type == 'QRCODE' and c.data.startswith(b'MO:')]
qr = None
if codes:
if len(codes) > 1:
logger.warning(f'Scan: Strana #{page_nr} obsahuje více QR kódů')
code = codes[0]
qr = code.data.decode('US-ASCII')
# FIXME: Tady by se dala podle kódu otočit stránka
output.append(ScanJobPage(code=qr))
full_img.save(f'{args.out_prefix}-{page_nr:04d}-full.png')
# FIXME: Potřebujeme vytvářet miniaturu?
small_img = full_img.resize((full_size[0] // 4, full_size[1] // 4))
small_img.save(f'{args.out_prefix}-{page_nr:04d}-small.png')
logger.debug(f'Scan: Strana #{page_nr}: {qr}')
return output
......@@ -13,37 +13,41 @@ import werkzeug.utils
import zipfile
import mo.db as db
from mo.jobs import TheJob, job_handler, job_file_path
from mo.jobs import TheJob, job_handler
from mo.submit import Submitter, SubmitException
from mo.util import logger, data_dir
from mo.util_format import inflect_number, inflect_by_number, data_size
def schedule_download_submits(paper_ids: List[int], description: str, for_user: db.User, want_subdirs: bool):
#
# Job download_submits: Zazipuje vybrané papíry
#
# Vstupní JSON:
# { 'papers': [ seznam paper_id ke stažení ],
# 'want_feedback': true/false,
# 'out_name': jméno výstupního souboru bez přípony,
# }
#
# Výstupní JSON:
# null
#
def schedule_download_submits(paper_ids: List[int], description: str, for_user: db.User, want_subdirs: bool, out_name: str):
the_job = TheJob()
job = the_job.create(db.JobType.download_submits, for_user)
job.description = description
job.in_json = {'papers': paper_ids, 'want_subdirs': want_subdirs}
job.in_json = {'papers': paper_ids, 'want_subdirs': want_subdirs, 'out_name': out_name}
the_job.submit()
@job_handler(db.JobType.download_submits)
def handle_download_submits(the_job: TheJob):
"""Zazipuje zadané papíry.
Vstupní JSON:
{ 'papers': [ seznam paper_id ke stažení ],
'want_feedback': true/false,
}
Výstupní JSON:
null
"""
job = the_job.job
assert job.in_json is not None
ids: List[int] = job.in_json['papers'] # type: ignore
want_subdirs: bool = job.in_json['want_subdirs'] # type: ignore
out_name: str = job.in_json['out_name'] # type: ignore
sess = db.get_session()
papers = (sess.query(db.Paper, db.User, db.Task.code, db.Place)
......@@ -57,11 +61,13 @@ def handle_download_submits(the_job: TheJob):
.all())
papers.sort(key=lambda p: (p[1].sort_key(), p[2]))
temp_file = NamedTemporaryFile(suffix='.zip', dir=data_dir('tmp'), mode='w+b')
logger.debug('Job: Vytvářím archiv %s', temp_file.name)
out_name = werkzeug.utils.secure_filename(out_name + '.zip')
out_path = job.file_path(out_name)
out_file = open(out_path, mode='w+b')
logger.debug('Job: Vytvářím archiv %s', out_path)
cnt = 0
with zipfile.ZipFile(temp_file, mode='w') as zip:
with zipfile.ZipFile(out_file, mode='w') as zip:
for p, u, task_code, place in papers:
cnt += 1
full_name = u.full_name()
......@@ -76,10 +82,25 @@ def handle_download_submits(the_job: TheJob):
zip.write(filename=os.path.join(data_dir('submits'), p.file_name or p.orig_file_name),
arcname=fn)
job.out_file = the_job.attach_file(temp_file.name, '.zip')
out_size = temp_file.tell()
job.out_file = out_name
out_size = out_file.tell()
job.result = 'Celkem ' + inflect_number(cnt, 'soubor', 'soubory', 'souborů') + ', ' + data_size(out_size)
temp_file.close()
out_file.close()
#
# Job upload_feedback: Uploaduje opravená řešení
#
# Vstupní JSON:
# { 'round_id': <id>,
# 'only_task_id': <id_or_null>,
# 'only_contest_id': <id_or_null>,
# 'only_site_id': <id_or_null>,
# }
#
# Výstupní JSON:
# null
#
def schedule_upload_feedback(round: db.Round, tmp_file: str, description: str, for_user: db.User,
......@@ -97,7 +118,7 @@ def schedule_upload_feedback(round: db.Round, tmp_file: str, description: str, f
'only_region_id': only_region.place_id if only_region is not None else None,
'only_task_id': only_task.task_id if only_task is not None else None,
}
job.in_file = the_job.attach_file(tmp_file, '.zip')
job.in_file = the_job.attach_file(tmp_file, 'upload.zip')
the_job.submit()
......@@ -148,19 +169,6 @@ def parse_feedback_name(name: str) -> Optional[UploadFeedback]:
@job_handler(db.JobType.upload_feedback)
def handle_upload_feedback(the_job: TheJob):
"""Uploaduje opravená řešení.
Vstupní JSON:
{ 'round_id': <id>,
'only_task_id': <id_or_null>,
'only_contest_id': <id_or_null>,
'only_site_id': <id_or_null>,
}
Výstupní JSON:
null
"""
job = the_job.job
assert job.in_file is not None
in_json = job.in_json
......@@ -280,7 +288,7 @@ def handle_upload_feedback(the_job: TheJob):
return False
cnt_good = 0
parse_zip(job_file_path(job.in_file))
parse_zip(job.file_path(job.in_file))
if not the_job.errors:
resolve_tasks(files)
......
......@@ -2,9 +2,12 @@
from enum import Enum, auto
from dataclasses import dataclass
from sqlalchemy import or_
from sqlalchemy.orm.query import Query
from typing import Set, List, Dict, Tuple, Optional
import mo
import mo.config as config
import mo.db as db
......@@ -23,7 +26,10 @@ class Right(Enum):
edit_points = auto() # Přidělovat body ve stavu "grading"
view_statement = auto() # Prohlížet zadání, pokud je dostupné pro dozor
add_users = auto()
edit_users = auto()
view_all_users = auto() # Prohlížet všechny uživatele
view_school_users = auto() # Prohlížet uživatele ze své školy (jen garant_skola)
edit_all_users = auto() # Editovat všechny účastníky
edit_school_users = auto() # Editovat uživatele ze své školy (jen garant_skola)
add_orgs = auto()
edit_orgs = auto()
......@@ -57,7 +63,8 @@ roles: List[Role] = [
Right.upload_submits,
Right.edit_points,
Right.add_users,
Right.edit_users,
Right.view_all_users,
Right.edit_all_users,
Right.add_orgs,
Right.edit_orgs,
},
......@@ -75,7 +82,8 @@ roles: List[Role] = [
Right.edit_points,
Right.view_statement,
Right.add_users,
Right.edit_users,
Right.view_all_users,
Right.edit_all_users,
Right.add_orgs,
Right.edit_orgs,
},
......@@ -93,7 +101,8 @@ roles: List[Role] = [
Right.edit_points,
Right.view_statement,
Right.add_users,
Right.edit_users,
Right.view_all_users,
Right.edit_all_users,
Right.add_orgs,
Right.edit_orgs,
},
......@@ -101,8 +110,6 @@ roles: List[Role] = [
Role(
role=db.RoleType.garant_skola,
rights={
# FIXME: Až se pořádně rozjedou školní kola, asi chceme školním správcům omezit
# práva na editaci uživatelů. Viz issue #66.
Right.assign_rights,
Right.edit_place,
Right.manage_contest,
......@@ -112,7 +119,8 @@ roles: List[Role] = [
Right.edit_points,
Right.view_statement,
Right.add_users,
Right.edit_users,
Right.view_school_users,
Right.edit_school_users,
Right.add_orgs,
Right.edit_orgs,
},
......@@ -136,6 +144,15 @@ roles: List[Role] = [
Right.view_statement,
},
),
Role(
role=db.RoleType.pozorovatel,
rights={
Right.view_contestants,
Right.view_submits,
Right.view_statement,
Right.view_all_users,
},
),
]
......@@ -191,12 +208,69 @@ class Rights:
# Práva na práci s uživateli
def can_view_user(self, user: db.User) -> bool:
if user.is_admin or user.is_org:
return True
elif self.have_right(Right.view_all_users):
return True
elif self.have_right(Right.view_school_users):
schools = self.get_user_schools(Right.view_school_users)
if schools:
q = db.get_session().query(db.User).filter_by(user_id=user.user_id)
q = self.restrict_user_query(q, schools)
if q.first():
return True
return False
else:
return False
def can_edit_user(self, user: db.User) -> bool:
if user.is_admin:
return self.user.is_admin # only admins can edit admins
elif user.is_org:
return self.have_right(Right.edit_orgs)
return self.have_right(Right.edit_users)
elif self.have_right(Right.edit_all_users):
return True
elif self.have_right(Right.edit_school_users):
schools = self.get_user_schools(Right.edit_school_users)
if schools:
q = db.get_session().query(db.User).filter_by(user_id=user.user_id)
q = self.restrict_user_query(q, schools)
if q.first():
return True
return False
else:
return False
def get_user_schools(self, right: Right) -> Set[db.Place]:
"""Vrátí seznam škol, kde má organizátor právo spravovat uživatele."""
places: Set[db.Place] = set()
for role in self.user_roles:
r = roles_by_type[role.role]
if right in r.rights:
places.add(role.place)
return places
def restrict_user_query(self, q: Query, schools: Set[db.Place]) -> Query:
"""Přidá k dotazu na hledání uživatelů podmínku na školy z dané množiny."""
sess = db.get_session()
school_ids = {s.place_id for s in schools}
q = q.filter(or_(
db.User.user_id.in_(
sess.query(db.Participant.user_id)
.filter(db.Participant.school.in_(school_ids))
.filter(db.Participant.year >= config.CURRENT_YEAR - 1)
),
db.User.user_id.in_(
sess.query(db.Participation.user_id)
.select_from(db.Participation)
.join(db.Contest)
.join(db.Round)
.filter(or_(db.Contest.place_id.in_(school_ids), db.Participation.place_id.in_(school_ids)))
.filter(db.Round.year == config.CURRENT_YEAR)
)
))
return q
class RoundRights(Rights):
......@@ -293,6 +367,7 @@ class Gatekeeper:
roles: List[db.UserRole]
parent_cache: Dict[int, List[db.Place]]
rights_cache: Dict[Tuple[Optional[int], Optional[int], Optional[str], Optional[int], Optional[db.RoleType]], Rights]
root_place: Optional[db.Place]
def __init__(self, user: db.User):
self.user = user
......@@ -300,6 +375,12 @@ class Gatekeeper:
assert user.is_org or user.is_admin
self.parent_cache = {}
self.rights_cache = {}
self.root_place = None
def get_root_place(self) -> db.Place:
if not self.root_place:
self.root_place = db.get_root_place()
return self.root_place
def get_ancestors(self, place: db.Place) -> List[db.Place]:
pid = place.place_id
......@@ -319,7 +400,7 @@ class Gatekeeper:
"""Posbírá role a práva, která se vztahují k danému místu (možno i tranzitivně) a soutěži.
Pokud place=None, omezení role na místo se nebere v úvahu.
Pokud year==None, vyhovují role s libovolným ročníkem; pokud year=0, vyhovují jen ty s neuvedeným ročníkem.
Podobně cat a seq.
Podobně cat a seq (u cat vyhoví jen ty s neuvedenou kategorií, když cat="").
Pokud min_role!=None, tak se uvažují jen role, které jsou v hierarchii alespoň na úrovni min_role."""
cache_key = (place.place_id if place is not None else None, year, cat, seq, min_role)
......@@ -360,7 +441,7 @@ class Gatekeeper:
elif for_place:
place = for_place
else:
place = db.get_root_place()
place = self.get_root_place()
rights = RoundRights()
rights.round = round
rights._clone_from(self.rights_for(
......
File added
\input ltluatex.tex
\input luatex85.sty
\input ucwmac2.tex
\setmargins{15mm}
\setuppage
\nopagenumbers
\ucwmodule{luaofs}
\settextsize{12}
\baselineskip=18pt
\uselanguage{czech}
\frenchspacing
\newbox\logobox
\setbox\logobox=\putimage{width 21mm}{mo-logo.epdf}
\input qrcode.tex
\qrset{height=23mm, level=H, tight, silent}
\newbox\codebox
\def\kolo{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\fillin#1{\smash{\lower 2pt\hbox to #1{\hrulefill}}}
% \proto{kód}{jméno}{třída}{škola}{příklad}
\def\proto#1#2#3#4#5{
\setbox\codebox=\hbox{\qrcode{#1}}
\line{%
\vhang{\copy\logobox}%
\qquad
\vhanglines{\baselineskip=14pt\vskip -5pt\hbox{\bf\kolo}\hbox{\bf\kat}}%
\hfil
\smash{\vhang{\box\codebox}}%
}
\medskip
\prevdepth=0pt
\leftline{%
\field{0.63\hsize}{Jméno: {\bf #2}}%
Třída: {\bf #3}%
}
\leftline{%
\field{0.63\hsize}{Škola: {\bf #4}}%
Příklad: {\bf #5}%
}
\leftline{%
\field{0.3\hsize}{List {\bf 1} ze \fillin{10mm}}%
\field{0.33\hsize}{Hodnotil:}%
Bodů:
}
\bigskip
\hrule
\vfill\eject
}
\def\universal{\proto{MO:*}{}{}{}{}}
\def\blank{%
\setbox\codebox=\hbox{\qrcode[height=15mm]{MO:+}}
\line{%
\field{0.63\hsize}{Jméno:}%
\field{0.2\hsize}{Třída:}%
\hss
\raise\ht\strutbox\hbox{\smash{\vhang{\box\codebox}}}
}
\leftline{%
\field{0.63\hsize}{List \fillin{10mm} ze \fillin{10mm}}%
\field{0.2\hsize}{Příklad:}%
}
\bigskip
\nointerlineskip
\hbox to 0.85\hsize{\hrulefill}
\vfill\eject
}
This diff is collapsed.
\input protokol.tex
\def\kolo{Krajské kolo 70. ročníku Matematické olympiády}
\def\kat{Kategorie P, Zlínský kraj}
\proto{MO:70-P-III-1:12345}{Pokusný Králík}{4/4}{Gymnázium Na Paloučku, Králíky}{P-III-1}
\proto{MO:70-P-III-2:12345}{Pokusný Králík}{4/4}{Gymnázium Na Paloučku, Králíky}{P-III-2}
\proto{MO:70-P-III-3:12345}{Pokusný Králík}{4/4}{Gymnázium Na Paloučku, Králíky}{P-III-3}
\proto{MO:70-P-III-4:12345}{Pokusný Králík}{4/4}{Gymnázium Na Paloučku, Králíky}{P-III-4}
\proto{MO:70-P-III-4:12345}{Pokusný Králík}{4/4}{MŠ, ZŠ a SŠ pro sluchově postižené, Valašské Meziříčí}{P-III-4}
\universal
\blank
\bye
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
......@@ -89,6 +89,11 @@ def data_dir(name: str) -> str:
return os.path.join(config.DATA_DIR, name)
def part_path(name: str) -> str:
"""Vrátí cestu k datovém souboru, který se instaluje jako součást pythoních modulů."""
return os.path.normpath(os.path.join(__file__, "..", name))
def link_to_dir(src: str, dest_dir: str, prefix: str = "", suffix: str = "") -> str:
"""Vytvoří hardlink na zdrojový soubor pod unikátním jménem v cílovém adresáři."""
......
This diff is collapsed.
from flask import render_template
from mo.web import app
@app.route('/doc/')
def doc_index():
return render_template('doc_index.html')
@app.route('/doc/organizatori')
def doc_org():
return render_template('doc_org.html')
@app.route('/doc/gdpr')
def doc_gdpr():
return render_template('doc_gdpr.html')
@app.route('/doc/about')
def doc_about():
return render_template('doc_about.html')
@app.route('/doc/import')
def doc_import():
return render_template('doc_import.html')
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.