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
Showing
with 410 additions and 99 deletions
......@@ -56,7 +56,7 @@ class PlaceEditForm(FlaskForm):
)
code = wtforms.StringField(
'Kód', filters=[lambda x: x or None], # may be NULL in db
description="Při nevyplnění se použije #číslo"
description="Na místo se lze odkazovat kódem z písmen a číslic."
)
type = wtforms.SelectField(
'Typ', choices=db.PlaceType.choices(), coerce=db.PlaceType.coerce
......@@ -112,10 +112,13 @@ def org_place_edit(id: int):
school = sess.query(db.School).get(place.place_id)
# Pass school data as additional dict (data is used after obj)
form = PlaceSchoolEditForm(obj=place, data=db.row2dict(school))
form.name.description = ('Název školy tak, jak se má objevovat ve výsledkové listině. Viz '
+ Markup('<a href="' + url_for('doc_garant') + '#kodskoly">pojmenovací konvence</a>.'))
else:
form = PlaceEditForm(obj=place)
school = None
form.code.description += f' Kromě zadaného kódu funguje též #{id}.'
form.type.choices = db.PlaceType.choices(level=place.level)
if form.validate_on_submit():
......
......@@ -284,6 +284,7 @@ class TaskEditForm(FlaskForm):
validators.Regexp(r'^[A-Za-z0-9-]+$', message="Kód úlohy smí obsahovat jen nediakritická písmena, čísla a znak -"),
], render_kw={'autofocus': True})
name = wtforms.StringField('Název úlohy')
type = wtforms.SelectField('Typ úlohy', choices=db.TaskType.choices(), coerce=db.TaskType.coerce)
max_points = mo_fields.Points(
'Maximum bodů', validators=[validators.Optional(), validators.NumberRange(min=0)],
description="Při nastavení maxima nelze udělit více bodů, pro zrušení uložte prázdnou hodnotu",
......@@ -368,6 +369,9 @@ class RoundEditForm(FlaskForm):
_for_round: Optional[db.Round] = None
name = wtforms.StringField("Název", render_kw={'autofocus': True})
code = wtforms.StringField("Kód",
description="Kód kola používaný v kódech úloh ('1', 'S' apod.). Není-li vyplněn, použije se pořadí kola v kategorii.",
)
state = wtforms.SelectField(
"Stav kola", choices=db.RoundState.choices(), coerce=db.RoundState.coerce,
description="Stav soutěží ve všech oblastech kola. Pokud zvolíme 'po oblastech', každá soutěž si svůj stav určuje sama.",
......
from typing import Optional, List
from typing import Optional, Set
from flask import render_template, g, redirect, url_for, flash, request
from flask_wtf import FlaskForm
import werkzeug.exceptions
import wtforms
from sqlalchemy import or_
from sqlalchemy import or_, select
import flask_sqlalchemy
from sqlalchemy.orm import joinedload, subqueryload
......@@ -15,11 +15,13 @@ from wtforms.validators import Required
import mo
import mo.db as db
import mo.email
from mo.imports import GlobalOrgsImport
from mo.rights import Right
import mo.util
import mo.users
from mo.web import app
import mo.web.fields as mo_fields
from mo.web.imports import ImportForm, generic_import_page
from mo.web.util import PagerForm
......@@ -52,6 +54,11 @@ def org_users():
sess = db.get_session()
rr = g.gatekeeper.rights_generic()
if rr.have_right(Right.view_all_users):
schools = None
else:
schools = rr.get_user_schools(Right.view_school_users)
q = sess.query(db.User).filter_by(is_admin=False, is_org=False).options(
subqueryload(db.User.participants).joinedload(db.Participant.school_place)
)
......@@ -108,6 +115,9 @@ def org_users():
if participation_filter_apply:
q = q.filter(db.User.user_id.in_(participation_filter))
if schools is not None:
q = rr.restrict_user_query(q, schools)
# print(str(q))
(count, q) = filter.apply_limits(q, pagesize=50)
users = q.all()
......@@ -115,8 +125,9 @@ def org_users():
return render_template(
'org_users.html', users=users, count=count,
filter=filter,
can_edit=rr.have_right(Right.edit_users),
can_edit=rr.have_right(Right.edit_all_users) or rr.have_right(Right.edit_school_users),
can_add=rr.have_right(Right.add_users),
is_restricted=(schools is not None),
)
......@@ -188,7 +199,7 @@ def org_orgs():
qr = qr.filter(or_(db.UserRole.year.in_(filter.search_year.list), db.UserRole.year == None))
pass
if filter.search_in_place.place is not None:
qr = qr.filter(db.UserRole.place_id.in_(db.place_descendant_cte(filter.search_in_place.place)))
qr = qr.filter(db.UserRole.place_id.in_(select([db.place_descendant_cte(filter.search_in_place.place)])))
if filter.search_right_for_place.place is not None:
qr = qr.filter(db.UserRole.place_id.in_([x.place_id for x in db.get_place_ancestors(filter.search_right_for_place.place)]))
# Po n>3 hodinách v mo.db jsem dospěl k závěru, že to hezčeji neumím (neumím vyrobit place_parents_cte)
......@@ -196,7 +207,6 @@ def org_orgs():
qr = qr.filter(db.UserRole.place_id.in_(
sess.query(db.Place.place_id).filter(db.Place.level.in_(filter.search_place_level.data))
))
print(qr)
return qr
if filter.is_role_filter.data:
......@@ -230,7 +240,7 @@ class FormAddRole(FlaskForm):
place = mo_fields.Place()
year = wtforms.IntegerField('Ročník', validators=[validators.Optional()])
category = wtforms.StringField("Kategorie", validators=[validators.Length(max=2)], filters=[lambda x: x or None])
seq = wtforms.IntegerField("Kolo", validators=[validators.Optional()])
seq = wtforms.IntegerField("Kolo", render_kw={"placeholder": "Pořadí kola v kategorii"}, validators=[validators.Optional()])
submit = wtforms.SubmitField('Přidat roli')
......@@ -255,6 +265,10 @@ class ResendInviteForm(FlaskForm):
flash('Tento uživatel už má účet aktivovaný.', 'danger')
class UpgradeToOrgForm(FlaskForm):
upgrade = SubmitField()
@app.route('/org/org/<int:id>/', methods=('GET', 'POST'))
def org_org(id: int):
sess = db.get_session()
......@@ -265,6 +279,8 @@ def org_org(id: int):
raise werkzeug.exceptions.NotFound()
rr = g.gatekeeper.rights_generic()
if not rr.can_view_user(user):
raise werkzeug.exceptions.Forbidden()
can_assign_rights = rr.have_right(Right.assign_rights)
resend_invite_form: Optional[ResendInviteForm] = None
......@@ -283,13 +299,16 @@ def org_org(id: int):
form_add_role.populate_obj(new_role)
new_role.user_id = id
new_role.place = db.get_root_place()
assert form_add_role.place
new_role.place = form_add_role.place.place or db.get_root_place()
new_role.assigned_by = g.user.user_id
ok = True
new_role.place = form_add_role.place.place
if not g.gatekeeper.can_set_role(new_role):
if not new_role.is_legal():
role_errors.append('Tato kombinace role a místa není povolena')
ok = False
elif not g.gatekeeper.can_set_role(new_role):
role_errors.append(f'Roli "{new_role}" nelze přidělit, není podmnožinou žádné vaší role')
ok = False
......@@ -345,6 +364,10 @@ def org_user(id: int):
return redirect(url_for('org_org', id=id))
rr = g.gatekeeper.rights_generic()
can_edit = rr.can_edit_user(user)
can_view = can_edit or rr.can_view_user(user) # Zkratka, abychom se vyhnuli drahému dotazu
if not can_view:
raise werkzeug.exceptions.Forbidden()
resend_invite_form: Optional[ResendInviteForm] = None
if user.last_login_at is None and rr.can_edit_user(user):
......@@ -353,6 +376,19 @@ def org_user(id: int):
resend_invite_form.do(user)
return redirect(url_for('org_user', id=id))
upgrade_form: Optional[UpgradeToOrgForm] = None
if rr.can_edit_user:
upgrade_form = UpgradeToOrgForm()
if upgrade_form.upgrade.data and upgrade_form.validate_on_submit():
try:
mo.users.change_user_to_org(user, reason='web')
sess.commit()
flash('Účet změněn na organizátorský.', 'success')
return redirect(url_for('org_org', id=user.user_id))
except mo.CheckError as e:
flash(str(e), 'danger')
return redirect(url_for('org_user', id=user.user_id))
participants = sess.query(db.Participant).filter_by(user_id=user.user_id)
participations = (
sess.query(db.Participation, db.Contest, db.Round)
......@@ -366,10 +402,11 @@ def org_user(id: int):
)
return render_template(
'org_user.html', user=user, can_edit=rr.can_edit_user(user),
'org_user.html', user=user, can_edit=can_edit,
can_incarnate=g.user.is_admin,
participants=participants, participations=participations,
resend_invite_form=resend_invite_form,
upgrade_form=upgrade_form,
)
......@@ -380,6 +417,7 @@ class UserEditForm(FlaskForm):
note = wtforms.TextAreaField("Poznámka")
is_test = wtforms.BooleanField("Testovací účet")
allow_duplicate_name = wtforms.BooleanField("Přidat účet s duplicitním jménem")
allow_change_user_to_org = wtforms.BooleanField("Povolit převedení účastníka na organizátora")
submit = wtforms.SubmitField("Uložit")
......@@ -404,6 +442,7 @@ def org_user_edit(id: int):
form = UserEditForm(obj=user)
del form.allow_duplicate_name
del form.allow_change_user_to_org
if (user.is_org or user.is_admin) and not g.user.is_admin:
# emaily u organizátorů může editovat jen správce
del form.email
......@@ -453,11 +492,34 @@ def org_user_new():
form = UserEditForm()
form.submit.label.text = 'Vytvořit'
is_duplicate_name = False
allow_change_user_to_org_show_field = False
if form.validate_on_submit():
check = True
if mo.users.user_by_email(form.email.data) is not None:
flash('Účet s daným e-mailem již existuje', 'danger')
old_user = mo.users.user_by_email(form.email.data)
if old_user is not None:
if is_org and not old_user.is_org:
allow_change_user_to_org_show_field = True
if form.allow_change_user_to_org.data:
try:
mo.users.find_or_create_user(
email=form.email.data,
krestni=form.first_name.data,
prijmeni=form.last_name.data,
is_org=True,
reason="web",
allow_change_user_to_org=True)
except mo.CheckError as e:
flash(str(e), 'danger')
check = False
if check:
mo.db.get_session().commit()
return redirect(url_for('org_org', id=old_user.user_id))
if check:
flash('Účet s daným e-mailem již existuje. Převedení účastníka na organizátora můžete povolit ve formuláři.', 'danger')
check = False
if check:
flash('Účet s daným e-mailem již existuje.', 'danger')
check = False
if is_org:
......@@ -496,8 +558,11 @@ def org_user_new():
return redirect(url_for('org_org', id=new_user.user_id))
return redirect(url_for('org_user', id=new_user.user_id))
if not is_duplicate_name:
if not is_duplicate_name and not form.allow_duplicate_name.data:
del form.allow_duplicate_name
if not (is_org and allow_change_user_to_org_show_field):
del form.allow_change_user_to_org
return render_template('org_user_new.html', form=form, is_org=is_org)
......@@ -543,3 +608,26 @@ def org_user_participant_edit(user_id: int, year: int):
return redirect(url_for('org_user', id=user_id))
return render_template('org_user_participant_edit.html', user=user, year=year, form=form)
class GlobalOrgsImportForm(ImportForm):
allow_change_user_to_org = wtforms.BooleanField("Povolit převádění účastníků na organizátory")
default_place = mo_fields.Place("Výchozí oblast (není-li v souboru uvedena)")
default_cat = wtforms.StringField("Výchozí kategorie (není-li v souboru uvedena)")
default_code = wtforms.StringField("Výchozí kód kola (není-li v souboru uveden)")
only_this_year = wtforms.BooleanField("Omezit práva na aktuální ročník", default=True)
@app.route('/org/org/import', methods=('GET', 'POST'))
def org_orgs_import():
form = GlobalOrgsImportForm()
imp = None
if form.validate_on_submit():
imp = GlobalOrgsImport(
g.user,
default_place=form.default_place.place,
default_cat=form.default_cat.data,
default_code=form.default_code.data,
year=mo.config.CURRENT_YEAR if form.only_this_year.data else None
)
return generic_import_page(form, imp, url_for('org_orgs_import'), template='org_global_orgs_import.html')
......@@ -41,30 +41,37 @@ když přidáte vlastní sloupce s novými názvy, budou se ignorovat.
rozporu mezi importovanými údaji a již známými import selže a je nutné provést
editaci ručně.
<h2>Import dozoru</h2>
<h2>Import organizátorů</h2>
<p>Definovány jsou tyto sloupce (tučné jsou povinné):
<p>Definovány jsou tyto sloupce (tučné jsou povinné, kurzívou jsou povinné pro zatím nezaregistrované účty):
<table class=data>
<tr><th>Název<th>Obsah
<tr><td><b>email</b><td>E-mailová adresa
<tr><td><b>krestni</b><td>Křestní jméno
<tr><td><b>prijmeni</b><td>Příjmení
<tr><td><b>kod_mista</b><td>Kód soutěžního místa (viz katalog škol na tomto webu)
<tr><td><i>krestni</i><td>Křestní jméno
<tr><td><i>prijmeni</i><td>Příjmení
<tr><td>kod_oblasti<td>Pokud neimportujete do konkrétní soutěže,
můžete uvést kód oblasti, na kterou bude mít organizátor omezená práva.
V opačném případě bude mít práva ke všem oblastem.
<tr><td><b>role</b><td>Jedna z následujících rolí:
garant,
garant_kraj,
garant_okres,
garant_skola,
dozor,
opravovatel,
pozorovatel.
</table>
<h2>Import opravovatelů</h2>
<p>Definovány jsou tyto sloupce (tučné jsou povinné):
V obecném importu organizátorů krom výše uvedených existuje ještě:
<table class=data>
<tr><th>Název<th>Obsah
<tr><td><b>email</b><td>E-mailová adresa
<tr><td><b>krestni</b><td>Křestní jméno
<tr><td><b>prijmeni</b><td>Příjmení
<tr><td>kod_oblasti<td>Pokud neimportujete do konkrétní soutěže, ale do celého kola,
můžete uvést kód oblasti, ve které opravovatel pracuje.
V opačném případě bude mít práva ke všem oblastem.
<tr><td>kategorie<td>Omezí práva na příslušnou kategorii.
Kategorie Z funguje pro všechny kategorie pro základní školy a kategorie S pro všechny kategorie A, B, C.
<tr><td>kolo<td>Omezí práva organizátora pouze do příslušného kola.
Je nutné současně určit i kategorii a nelze kombinovat s importem bez omezení na aktuální ročník.
Kolo se zadává pomocí kódu (například 1, S, 2).
</table>
{% endblock %}
{% extends "base.html" %}
{% block title %}Dokumentace{% endblock %}
{% block body %}
<ul>
<li><a href='{{ url_for('doc_about') }}'>O aplikaci</a>
<li><a href='{{ url_for('doc_gdpr') }}'>Zpracování osobních údajů</a>
<li><a href='https://docs.google.com/document/d/1XXk7Od-ZKtfmfNa-9FpFjUqmy0Ekzf2-2q3EpSWyn1w/edit?usp=sharing'>Návod na tvorbu PDF</a>
</ul>
{% if g.user.is_org or g.user.is_admin %}
<h3>Pro organizátory</h3>
<ul>
<li><a href='{{ url_for('doc_org') }}'>Obecný popis systému pro organizátory</a>
<li><a href='{{ url_for('doc_import') }}'>Importování</a>
<li><a href='{{ url_for('static', filename='doc/skolni-garanti.pdf') }}'>Podrobnější návod pro školní garanty</a> (PDF)
<li><a href='{{ url_for('static', filename='doc/import-navod.pdf') }}'>Podrobnější návod k importům</a> (PDF)
<li><a href='{{ url_for('static', filename='doc/zakladani-skolnich-garantu.pdf') }}'>Zakládání školních garantů</a> (PDF)
<li><a href='{{ url_for('static', filename='doc/import-skolnich-garantu.pdf') }}'>Import školních garantů</a> (PDF)
</ul>
<h3>Exporty</h3>
<ul>
<li>Export všech škol:
<a href='{{ url_for('org_export_schools', format='en_csv') }}'>CSV s čárkami</a>,
<a href='{{ url_for('org_export_schools', format='cs_csv') }}'>CSV se středníky</a>,
<a href='{{ url_for('org_export_schools', format='tsv') }}'>TSV</a>
</ul>
{% endif %}
{% endblock %}
{% extends "base.html" %}
{% block title %}Návod pro garanty{% endblock %}
{% block title %}Popis systému pro organizátory{% endblock %}
{% block body %}
<p><b>Varování:</b> Systém je ve vývoji. Spoustu věcí ještě neumí. Kdybyste
narazili na jakoukoliv chybu, dejte prosím vědět správcům (viz odkaz v patičce
každé stránky). Stejně tak kdybyste neuměli nějaká data upravit.
<h3>Účty a role</h3>
<p>Každý <b>účet</b> v systému je jednoznačně identifikovaný e-mailovou
......@@ -20,18 +16,13 @@ oblasti (kraje, okresy, školy), kategorie či kola soutěže.
účty účastníkům a registrovat je do soutěže, zakládat účty
organizátorům a přidělovat jim role, spravovat odevzdaná řešení
<li><b>krajský garant</b> – má právo spravovat soutěž ve svém kraji
(systém dovolí tuto roli přidělit i na jiné úrovni než kraj,
ale nedělejte to prosím)
<li><b>okresní garant</b> – má právo spravovat soutěž ve svém okrese
(systém dovolí tuto roli přidělit i na jiné úrovni než okres,
ale nedělejte to prosím)
<li><b>školní garant</b> – má právo spravovat soutěž na své škole
(systém dovolí tuto roli přidělit i na jiné úrovni než škola,
ale nedělejte to prosím)
<li><b>dozor</b> – může v rámci nějakého soutěžního místa (to je třeba škola)
odevzdávat řešení za účastníky, a to i po oficiálním konci soutěže
<li><b>opravovatel</b> – může si prohlížet a stahovat odevzdaná řešení,
nahrávat opravená řešení a udělovat body a komentáře.
<li><b>pozorovatel</b> – vidí všechno, co garant, ale nemůže nic upravovat.
</ul>
<p>Role se přidělují takto:
......@@ -44,6 +35,11 @@ oblasti (kraje, okresy, školy), kategorie či kola soutěže.
i formulář na přidělování rolí.
</ul>
<p>Může se stát, že budoucí organizátor už má účastnický účet (je to bývalý účastník
nebo si pomocí registrace účet založil sám). Pak je potřeba účet nejdřív převést
na organizátorský. To jde udělat vyhledáním uživatele mezi účastníky a zmáčknutím
tlačítka pro převod účtu.
<h3>Hierarchie míst</h3>
<p>Jednotlivé oblasti a školy, kde se soutěží, jsou zařazeny do hierarchie <b>míst</b>.
......@@ -64,6 +60,21 @@ kód, použije se interní ve tvaru <tt>#</tt><i>číslo</i>.
<p>Místa od obce níže mohou editovat garanti, vyšší oblasti pouze správce.
<h4 id=kodskoly>Pojmenování škol</h4>
<p>Oficiální názvy škol zkracujeme do systematické podoby, kterou uvádíme ve výsledkových listinách.
Držte se prosím následujících konvencí:
<ul>
<li><b>G Turnov</b> &ndash; jediné gymnázium ve městě
<li><b>ZŠ Česká Lípa, Šluknovská</b> &ndash; pokud je škol ve městě více, rozlišujeme je ulicí;
číslo domu obvykle vynecháváme
<li><b>SPŠ stavební, Liberec</b> &ndash; u odborných škol uvádíme obor
<li><b>G Matyáše Lercha, Brno</b> &ndash; pokud má škola netriviální název, použijeme ho
<li><b>Moravské G Brno</b> &ndash; u názvů končících typem školy neoddělujeme město čárkou
<li><b>G Nad Štolou, Praha 7</b> &ndash; v Praze uvádíme i městský obvod
</ul>
<h3>Soutěžní kola</h3>
<p>Olympiáda se skládá ze <b>soutěžních kol</b>. Každé kolo má tyto vlastnosti:
......@@ -72,6 +83,7 @@ kód, použije se interní ve tvaru <tt>#</tt><i>číslo</i>.
<li><b>ročník MO</b> (nyní 70)
<li><b>kategorie</b> (A, P, Z5, &hellip;)
<li><b>pořadí</b> v rámci kategorie (1, 2, &hellip;)
<li><b>kód</b> v rámci kategorie (zobrazuje se místo pořadí, třeba „S“ pro školní kolo)
<li><b>část</b> – u běžných kol 0, jinak viz níže
<li><b>úroveň</b> v hierarchii míst, na které se odehrává (to může být celá republika,
kraj, okres apod.). Odpovídá tomu, pro jaké oblasti se sestavují samostatné
......@@ -88,17 +100,17 @@ Např. krajské kolo má samostatnou soutěž v každém kraji.
<p>Soutěžní kolo se nachází v jednom z následujících stavů:
<ul>
<li>připravuje se – kolo je zatím přístupné jenom organizátorům;
<li><b>připravuje se</b> – kolo je zatím přístupné jenom organizátorům;
účastníci vidí jen, že kolo existuje (pokud jsou do něj pozvaní),
a termín začátku soutěže.
<li>běží – účastníkům je dostupné zadání (po zadaném čase) a mohou odevzdávat,
<li><b>běží</b> – účastníkům je dostupné zadání (po zadaném čase) a mohou odevzdávat,
dozor také může odevzdávat. Opravovatelé si mohou průběžně prohlížet
odevzdané úlohy, ale ještě nemohou nic měnit.
<li>opravuje se – opravovatelé si mohou stahovat finální verzi řešení, nahrávat opravená
<li><b>opravuje se</b> – opravovatelé si mohou stahovat finální verzi řešení, nahrávat opravená
řešení a vyplňovat body a poznámky.
<li>ukončeno – opravená řešení, body a poznámky jsou dostupné účastníkům (vše pouze
<li><b>ukončeno</b> – opravená řešení, body a poznámky jsou dostupné účastníkům (vše pouze
ve finální verzi), opravovatelé už nemohou nic měnit.
<li>po oblastech – soutěž v každé oblasti si může nastavit svůj stav (viz níže)
<li><b>po oblastech</b> – soutěž v každé oblasti si může nastavit svůj stav (viz níže)
</ul>
<p>Omezení daná stavem soutěže neplatí pro garanty, ti mohou vždy všechno.
......@@ -119,7 +131,24 @@ Každá oblast nyní bude ve stavu <em>běží</em> (zdědila předchozí nastav
oblasti může podle potřeby přepínat do dalších stavů. Až budou všechny oblasti ve stavu <em>ukončeno</em>,
celostátní garant kolo také přepne do <em>ukončeno.</em>
<h3>Účastníci</h3>
<h3>Registrace</h3>
<p>Účastníci si mohou sami založit účet a pak se pomocí něj přihlásit do domácího kola.
Přesněji řečeno každé kolo může mít nastaven jeden ze tří režimů registrace:
<ul>
<li><b>účastníci sami</b> &ndash; účastník se registruje sám; používáme v kategorii P
<li><b>potvrzení organizátorem</b> &ndash; účastník se registruje sám, ale organizátor
musí registraci potvrdit (převést účast ze stavu „přihlášený“ do „soutěží“);
používáme v ostatních kategoriích
<li><b>jen organizátoři</b> &ndash; účastníky přihlašuje organizátor
</ul>
<p>Do vyšších kol obvykle účastníky nepřihlašujeme přímo, ale používáme
tlačítko „Postup z minulého kola“ na stránce soutěže.
<h3>Import účastníku</h3>
<p>Garanti mohou přihlašovat účastníky do soutěže <b>importem</b> souboru ve formátu CSV.
Tento soubor můžete vyrobit v Excelu či jiném tabulkovém kalkulátoru a pak do CSV exportovat.
......@@ -130,7 +159,7 @@ Nadřazení garanti také mohou importovat více oblastí najednou na stránce k
pak musí být uveden i sloupec s kódem oblasti).
<p>Kód školy najdete u školy v hierarchii míst (záložka „Místa“). Také si můžete stáhnout
seznam všech škol ve formátu CSV a prohlížet si ho v Excelu (ten je v záložce „Domů“).
seznam všech škol ve formátu CSV a prohlížet si ho v Excelu (ten je v záložce „Návod“).
Náš seznam obsahuje všechny školy z Rejstříku škol Ministerstva školství. Je ale možné,
že v něm některé školy chybí. V takovém případě školu založte tlačítkem „Přidat nové podřízené
místo“ na stránce obce. Kdyby chyběla i obec, založte ji stejným způsobem na stránce okresu.
......@@ -153,26 +182,13 @@ místo pak může mít svůj <b>dozor</b> (viz popis rolí). Typické situace js
<p>Pokud importujete účastníka, který dosud neměl založen účet, účet se automaticky vytvoří
a účastníkovi se pošle e-mail s odkazem na nastavení hesla.
<h3>Dozor</h3>
<h3>Import organizátorů</h3>
<p>Osoby vykonávající dozor na soutěžních místech jde také hromadně importovat.
<p>Účty a role organizátorů je také možné zakládat hromadně pomocí importu.
Funguje to podobně jako import účastníků, opět je k dispozici <a href='{{ url_for('doc_import') }}'>popis formátu</a>.
<p>Dozírajícím se automaticky založí organizátorské účty (pokud je ještě nemají) a přidělí
se jim dozorová role k příslušnému kolu a soutěžnímu místu.
<p>Dozor se ke svému soutěžnímu místu dostane přes Soutěž » výběr kola » výběr soutěže »
výběr soutěžního místa. Pak si může prohlížet seznam účastníků a odevzdané úlohy
a také za účastníky odevzdávat.
<h3>Opravovatelé</h3>
<p>Opravovatelé si mohou prohlížet účastnická řešení, nahrávat do systému jejich opravené verze
a udělovat body a poznámky.
<p>Také je možné stáhnout si najednou všechna účastnická řešení jako jeden ZIP, do řešení
dopsat poznámky a pak je zase jako ZIP nahrát zpět. Přitom je nutné zachovat jména souborů.
<p>Podobně jako dozor, i opravovatele můžete importovat.
<p>Importovat organizátory jde do konkrétní soutěže (na stránce soutěže, přidělí se role omezené
na tuto soutěž), do konkrétního kola (na stránce kola) nebo obecně (v záložce „Organizátoři“,
tak lze zakládat neomezené organizátorské role, což se hodí například pro garanty).
{% endblock %}
{% extends "base.html" %}
{% block title %}Vítejte{% endblock %}
{% block body %}
<p>Na tomto webu je možné odevzdávat řešení úloh Matematické olympiády.
<p>Prostřednictvím tohoto webu se mohou soutěžící přihlásit do Matematické
olympiády. Také zde najdou své výsledky a opravené protokoly z vyšších kol
soutěže a výsledkové listiny před jejich zveřejněním na stránkách MO.
V případě distanční soutěže zde mohou odevzdávat svá řešení.
<p>Na odevzdávání potřebujete účet. Pokud už ho máte, tak se prosím přihlašte.
<p>Organizátoři mohou prostřednictvím tohoto webu spravovat soutěž ve své oblasti.
<p>Nejdříve potřebujete účet. Pokud už ho máte, tak se prosím přihlašte.
V opačném případě si účet založte.
<p><a class='btn btn-primary' href='{{ url_for('login') }}'>Přihlásit se</a>
......
......@@ -60,11 +60,13 @@
{% if state in [RoundState.grading, RoundState.closed] %}
<a class="btn btn-primary" href='{{ ctx.url_for('org_score') }}'>Výsledky</a>
{% endif %}
<a class="btn btn-default" href='{{ ctx.url_for('org_contest_protocols') }}'>Protokoly</a>
{% if state == RoundState.preparing and round.seq > 1 %}
<a class="btn btn-primary" href='{{ ctx.url_for('org_contest_advance') }}'>Postup z minulého kola</a>
{% endif %}
{% if can_manage %}
<a class="btn btn-default" href='{{ ctx.url_for('org_generic_import') }}'>Importovat data</a>
<a class="btn btn-default" href='{{ ctx.url_for('org_import_user') }}'>Importovat účastníky</a>
<a class="btn btn-default" href='{{ ctx.url_for('org_import_org') }}'>Importovat organizátory</a>
{% endif %}
{% if can_manage and not site %}
<a class="btn btn-default" href='{{ ctx.url_for('org_contest_edit') }}'>Nastavení</a>
......@@ -119,8 +121,9 @@
<tr>
<th>Kód
<th>Název
<th>Odevzdaná řešení
<th>Maximum bodů
<th>Typ
<th>Odevzdáno
<th>Max. bodů
<th>Jednotlivé akce
<th>Dávkové operace
</tr>
......@@ -129,6 +132,7 @@
<tr>
<td>{{ task.code }}
<td>{{ task.name }}
<td>{{ task.type.friendly_name() }}
<td>{{ task.sol_count }}
<td>{{ task.max_points|decimal|none_value('–') }}
<td><div class="btn-group">
......
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}
Protokoly pro {{ ctx.round.name|lower }} kategorie {{ ctx.round.category }}
{% endblock %}
{% block breadcrumbs %}
{{ ctx.breadcrumbs(action="Protokoly") }}
{% endblock %}
{% block body %}
<p>Zde je možné vytvořit PDF s formuláři protokolů pro všechny soutěžící. Každý
formulář je opatřen unikátním QR kódem. FIXME: Dovysvětlit.
<h3>Formuláře protokolů</h3>
{% macro field(f) %}
{{ wtf.form_field(f, form_type='horizontal', horizontal_columns=('lg', 3, 7), button_map={'gen_protos': 'primary', 'process_scans': 'primary'}) }}
{% endmacro %}
<form action="" method=POST class="form form-horizontal" role="form">
{{ gen_form.csrf_token }}
{% if gen_task_fields %}
<div class='form-group'>
<label class='control-label col-lg-3' for='{{ gen_task_fields[0].id }}'>Úlohy</label>
<div class='col-lg-7'>
{% for f in gen_task_fields %}
{{ wtf.form_field(f) }}
{% endfor %}
</div>
</div>
{% endif %}
{{ field(gen_form.num_universal) }}
{{ field(gen_form.num_blank) }}
{{ field(gen_form.gen_protos) }}
</form>
<h3>Zpracování scanů</h3>
<form action="" method=POST class="form form-horizontal" role="form" enctype='multipart/form-data'>
{{ proc_form.csrf_token }}
{% if proc_task_fields %}
<div class='form-group'>
<label class='control-label col-lg-3' for='{{ proc_task_fields[0].id }}'>Úlohy</label>
<div class='col-lg-7'>
{% for f in proc_task_fields %}
{{ wtf.form_field(f) }}
{% endfor %}
</div>
</div>
{% endif %}
{{ field(proc_form.files) }}
{{ field(proc_form.process_scans) }}
</form>
{% endblock %}
{% extends "org_generic_import.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}
Import soutěžících {% if contest or round %}do {% if contest %}soutěže {{ contest.place.name_locative() }}{% else %}kola {{ round.round_code() }}{% endif %}{% endif %}
{% endblock %}
{% block breadcrumbs %}
{{ ctx.breadcrumbs(action="Import soutěžících") }}
{% endblock %}
{% block import_info %}
{% if not contest %}
<p><em>Pozor, zde se importuje do více soutěží najednou, takže je nutné uvádět
kód oblasti. Nechcete raději importovat do konkrétní oblasti?</em></p>
{% endif %}
{% if default_place %}
<p>Výchozí oblastí tohoto importu je: <a href='{{ url_for('org_place', id=default_place.place_id) }}'>{{ default_place.name }}</a>.</p>
{% endif %}
{% endblock %}
{% block import_form %}
{% endblock %}
......@@ -2,13 +2,11 @@
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}
Import dat do {% if contest %}soutěže {{ contest.place.name_locative() }}{% else %}kola {{ round.round_code() }}{% endif %}
{% endblock %}
{% block breadcrumbs %}
{{ ctx.breadcrumbs(action="Import dat") }}
Import dat {% if contest or round %}do {% if contest %}soutěže {{ contest.place.name_locative() }}{% else %}kola {{ round.round_code() }}{% endif %}{% endif %}
{% endblock %}
{% block body %}
{% block import_errs %}
{% if warnings %}
<h3>Varování při importu</h3>
......@@ -28,15 +26,24 @@ Import dat do {% if contest %}soutěže {{ contest.place.name_locative() }}{% el
{% endfor %}
</div>
{% endif %}
{% endblock %}
<p>Zde je možné importovat účastníky soutěže, dozor na soutěžních místech a opravovatele.
Detaily fungování importu najdete v <a href='{{ url_for('doc_import') }}'>dokumentaci</a>.
{% block import_info %}{% endblock %}
{% if not contest %}
<p><em>Pozor, zde se importuje do více soutěží najednou, takže je nutné uvádět
kód oblasti. Nechcete raději importovat do konkrétní oblasti?</em>
{% endif %}
{% block import_help %}
<p>Detaily fungování importu najdete v <a href='{{ url_for('doc_import') }}'>dokumentaci</a>.
{% endblock %}
<form action="" method="post" class="form" enctype="multipart/form-data" role="form">
{{ form.csrf_token }}
{{ wtf.form_field(form.file) }}
{{ wtf.form_field(form.fmt) }}
{% block import_form %}{% endblock %}
<div class="btn-group">
{{ wtf.form_field(form.submit, class='btn btn-primary') }}
{{ wtf.form_field(form.get_template, class='btn btn-default') }}
</div>
</form>
{{ wtf.quick_form(form, form_type='simple', button_map={'submit': 'primary'}) }}
{% endblock %}
......@@ -69,6 +69,7 @@
{% if table %}
{% if action_form %}
<form action="" method="POST" class="form form-horizontal" role="form">
{{ action_form.submit_no_action }}
{% endif %}
{{ table.to_html() }}
......
{% extends "org_generic_import.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}
Import organizátorů
{% endblock %}
{% block import_info %}
<p><em>Toto je obecný import organzátorů. Ten se hodí pro importování „dlouhodobých“ organizátorských rolí,
které nejsou vázané na konkrétní soutěž. U každého organizátora můžete určit kategorii, kolo
i oblast jeho působnosti. Pokud chcete raději importovat organizátory konkrétní soutěže, jde to jednodušeji
přes stránky kola, soutěže, případně soutěžního místa.</em>
{% endblock %}
{% block import_form %}
{{ wtf.form_field(form.default_place) }}
{{ wtf.form_field(form.default_cat) }}
{{ wtf.form_field(form.default_code) }}
{{ wtf.form_field(form.only_this_year) }}
{% if imp.cnt_change_user_to_org %}
{{ wtf.form_field(form.allow_change_user_to_org) }}
{% endif %}
{% endblock %}
{% extends "base.html" %}
{% block title %}Organizátorské rozhraní{% endblock %}
{% block title %}Přístup pro organizátory{% endblock %}
{% block body %}
{% if overview %}
......@@ -45,19 +45,11 @@
{% endfor %}
</table>
{% endif %}
{% else %}
<h3>Různé</h3>
<p>Momentálně neorganizujete žádnou soutěž.
<ul>
<li><a href='{{ url_for('doc_garant') }}'>Návod pro garanty</a> (může se hodit i ostatním organizátorům)
<li><a href='{{ url_for('static', filename='doc/import-navod.pdf') }}'>Podrobnější návod k importům</a> (PDF)
<li>Export všech škol:
<a href='{{ url_for('org_export_schools', format='en_csv') }}'>CSV s čárkami</a>,
<a href='{{ url_for('org_export_schools', format='cs_csv') }}'>CSV se středníky</a>,
<a href='{{ url_for('org_export_schools', format='tsv') }}'>TSV</a>
<li><a href='https://docs.google.com/document/d/1XXk7Od-ZKtfmfNa-9FpFjUqmy0Ekzf2-2q3EpSWyn1w/edit?usp=sharing'>Návod na tvorbu PDF</a>
</ul>
{% endif %}
<h3>Rychlé hledání</h3>
......
......@@ -44,7 +44,8 @@
{% if can_assign_rights %}
<h4>Přidělení nové role</h4>
<p>Můžete přidělit jen roli, která je podmnožinou nějaké vaší role (včetně omezení na oblast, kolo, &hellip;).</p>
<p>Pokud roli omezíte na kategorii <code>Z</code> bude fungovat pro všechny kategorie začínající na <code>Z</code>.
<p>Pokud roli omezíte na kategorii <code>Z</code>, bude fungovat pro všechny kategorie začínající na <code>Z</code>.
Podobně <code>S</code> znamená všechny středoškolské kategorie <code>A</code>, <code>B</code>, <code>C</code>.
{% if role_errors %}
<div class="alert alert-danger" role="alert">
{{ role_errors|join(Markup("<br>")) }}
......@@ -65,7 +66,7 @@
<table class="data full">
<thead>
<tr>
<th>Role<th>Oblast<th>Ročník<th>Kategorie<th>Kolo<th>Přidělil<th>Akce
<th>Role<th>Oblast<th>Ročník<th>Kategorie<th class='has-tip' title='Pořadí kola v kategorii'>Kolo<th>Přidělil<th>Akce
</tr>
</thead>
{% for role in user.roles %}
......
......@@ -3,7 +3,10 @@
{% block title %}Organizátoři{% endblock %}
{% block body %}
{% if can_add %}
<a class="pull-right btn btn-primary" style="margin-top: -40px;" href="{{ url_for('org_org_new') }}">Nový organizátor</a>
<div class="btn-group pull-right" style="margin-top: -40px;">
<a class="btn btn-primary" href="{{ url_for('org_org_new') }}">Nový organizátor</a>
<a class="btn btn-default" href="{{ url_for('org_orgs_import') }}">Importovat organizátory</a>
</div>
{% endif %}
<div class="form-frame">
......
{% extends "org_generic_import.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}
Import organizátorů {% if contest or round %}{% if contest %}soutěže {{ contest.place.name_locative() }}{% else %}kola {{ round.round_code() }}{% endif %}{% endif %}
{% endblock %}
{% block breadcrumbs %}
{{ ctx.breadcrumbs(action="Import organizátorů") }}
{% endblock %}
{% block import_info %}
{% if not contest %}
<p><em>Zde můžete importovat organizátory do více soutěží najednou, takže je nutné uvádět kód oblasti.
Nechcete raději importovat do konkrétní oblasti na stránce soutěže?</em></p>
{% else %}
<p><em>Zde můžete importovat organizátory soutěže. Dostanou organizátorskou roli
omezenou na tuto konkrétní soutěž.</em></p>
{% endif %}
{% if default_place %}
<p>Výchozí oblastí tohoto importu je: <a href='{{ url_for('org_place', id=default_place.place_id) }}'>{{ default_place.name }}</a>.</p>
{% endif %}
{% endblock %}
{% block import_form %}
{% if imp.cnt_change_user_to_org %}
{{ wtf.form_field(form.allow_change_user_to_org) }}
{% endif %}
{% endblock %}
......@@ -11,7 +11,7 @@
<th>Jméno
<th>Roč.
<th>Kat.
<th>Kolo
<th class='has-tip' title='Pořadí kola v kategorii'>Kolo
<th>Zdroj
</thead>
{% for role in roles %}
......
......@@ -28,7 +28,7 @@
</thead>
<tr><td>Ročník<td>{{ round.year }}
<tr><td>Kategorie<td>{{ round.category }}
<tr><td>Pořadí<td>{{ round.seq }}
<tr><td>Pořadí / kód<td>{{ round.seq }} / {{ round.code if round.code else round.seq }}
{% if round.part > 0 %}<tr><td>Část<td>{{ round.part_code() }}{% endif %}
<tr><td>Oblast<td>{{ round.get_level().name }}
<tr><td>Vaše role<td>{% if g.user.is_admin %}správce{% elif roles %}{{ roles|join(", ") }}{% else %}–{% endif %}
......@@ -92,7 +92,8 @@
<a class="btn btn-primary" href='{{ ctx.url_for('org_score') }}'>Výsledky</a>
{% endif %}
{% if can_manage_contest %}
<a class="btn btn-default" href='{{ ctx.url_for('org_generic_import') }}'>Importovat data</a>
<a class="btn btn-default" href='{{ ctx.url_for('org_import_user') }}'>Importovat účastníky</a>
<a class="btn btn-default" href='{{ ctx.url_for('org_import_org') }}'>Importovat organizátory</a>
{% endif %}
{% if can_manage_round %}
<a class="btn btn-default" href='{{ ctx.url_for('org_round_edit') }}'>Nastavení a termíny</a>
......@@ -173,6 +174,7 @@
<tr>
<th>Kód
<th>Název
<th>Typ
<th>Odevzdaná řešení
<th>Maximum bodů
{% if can_manage_round %}<th>Akce{% endif %}
......@@ -183,6 +185,7 @@
<tr>
<td>{{ task.code }}
<td>{{ task.name }}
<td>{{ task.type.friendly_name() }}
<td>{{ sol_count }}
<td>{{ task.max_points|decimal|none_value('–') }}
{% if can_manage_round %}
......
......@@ -28,6 +28,14 @@
</button>
</form>
{% endif %}
{% if upgrade_form %}
<form method=POST class='btn-group' onsubmit='return confirm("Změnit účastnický účet na organizátorský?");'>
{{ upgrade_form.csrf_token }}
<button class="btn btn-default" type='submit' name='upgrade' value='yes'>
Změnit na organizátora
</button>
</form>
{% endif %}
{% if g.user.is_admin %}
<a class="btn btn-default" href="{{ log_url('user', user.user_id) }}">Historie</a>
{% endif %}
......