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

Commits on Source 6

......@@ -17,7 +17,7 @@ bin/create-user mj@ucw.cz Martin Mareš --admin --passwd brum
bin/create-user medved@ucw.cz Baltasis Lokys --org --passwd brum
bin/add-role --email medved@ucw.cz --role garant --cat P
bin/add-role --email medved@ucw.cz --role garant --cat A --place PB
bin/add-role --email medved@ucw.cz --role garant_kraj --cat A --place S
bin/create-round -y 70 -c P -s 1 -n 'Školní kolo' -l 1
bin/create-round -y 70 -c P -s 2 -n 'Krajské kolo' -l 1
......
......@@ -2,6 +2,7 @@
autopep8
future
ipython
jedi<0.18.0,>=0.17.2
mypy
pycodestyle
pydocstyle
......
from dataclasses import dataclass
import io
import re
from typing import List, Optional
from typing import List, Optional, Any, Dict
import mo.csv
import mo.db as db
import mo.rights
import mo.util
......@@ -16,20 +17,42 @@ class ContestImportRow(mo.csv.Row):
kod_skoly: str = ""
rocnik: str = ""
rok_naroz: str = ""
def parse_email(email: str, errs: List[str]) -> Optional[str]:
kod_mista: str = ""
kod_oblasti: str = ""
class Import:
line_errors: List[str]
errors: List[str]
user: db.User
round: db.Round
place_cache: Dict[str, db.Place]
school_place_cache: Dict[str, db.Place]
rr: Optional[mo.rights.Rights]
place_rights_cache: Dict[int, bool]
def __init__(self, user: db.User):
self.errors = []
self.user = user
self.rr = None
self.place_cache = {}
self.school_place_cache = {}
self.place_rights_cache = {}
def error(self, msg: str) -> Any:
self.line_errors.append(msg)
return None # Kdyby bylo otypováno správně jako -> None, při volání by si mypy stěžoval
def parse_email(self, email: str) -> Optional[str]:
if email == "":
errs.append('Chybí e-mailová adresa')
return None
return self.error('Chybí e-mailová adresa')
if not re.fullmatch(r'.+@.+', email):
errs.append('Chybný formát e-mailové adresy')
return None
return self.error('Chybný formát e-mailové adresy')
if not re.fullmatch(r'[!-~]+', email):
errs.append('E-mailová adresa obsahuje znaky mimo ASCII')
return None
return self.error('E-mailová adresa obsahuje znaky mimo ASCII')
# XXX: Striktně vzato, tohle není korektní, protože některé domény mohou
# mít case-sensitive levou stranu adresy. Ale i na nich se prakticky nevyskytují
......@@ -37,42 +60,68 @@ def parse_email(email: str, errs: List[str]) -> Optional[str]:
# a malá písmena zaměňují. Menší zlo tedy je normalizovat na malá písmena.
return email.lower()
def parse_name(name: str, errs: List[str]) -> Optional[str]:
def parse_name(self, name: str) -> Optional[str]:
if name == "":
errs.append('Jméno nesmí být prázdné')
return None
return self.error('Jméno nesmí být prázdné')
# XXX: Tato kontrola úmyslně není striktní, aby prošla i jména jako 'de Beer'
if name == name.lower():
errs.append('Ve jméně nejsou velká písmena')
return None
return self.error('Ve jméně nejsou velká písmena')
if name == name.upper():
errs.append('Ve jméně nejsou malá písmena')
return None
return self.error('Ve jméně nejsou malá písmena')
return name
def check_rights(self, place: db.Place) -> bool:
if place.place_id in self.place_rights_cache:
return self.place_rights_cache[place.place_id]
def parse_school(kod: str, errs: List[str]) -> Optional[db.Place]:
if self.rr is None:
self.rr = mo.rights.Rights(self.user)
round = self.round
self.rr.get_for(place, round.year, round.category, round.seq)
result = self.rr.have_right(mo.rights.Right.manage_contest)
self.place_rights_cache[place.place_id] = result
return result
def parse_opt_place(self, kod: str) -> Optional[db.Place]:
if kod == "":
errs.append('Škola je povinná')
return None
if kod in self.place_cache:
return self.place_cache[kod]
place = db.get_place_by_code(kod)
if not place:
return self.error(f'Místo "{kod}" nenalezeno')
if not self.check_rights(place):
return self.error(f'K místu "{kod}" nemáte práva na správu soutěže')
self.place_cache[kod] = place
return place
def parse_school(self, kod: str) -> Optional[db.Place]:
if kod == "":
return self.error('Škola je povinná')
if kod in self.school_place_cache:
return self.school_place_cache[kod]
place = db.get_place_by_code(kod, fetch_school=True)
if not place:
errs.append('Škola nenalezena')
return None
return self.error(f'Škola "{kod}" nenalezena')
if place.type != db.PlaceType.school:
errs.append('Kód školy neodpovídá škole')
return None
return self.error(f'Kód školy "{kod}" neodpovídá škole')
self.school_place_cache[kod] = place
return place
def parse_grade(rocnik: str, school: Optional[db.School], errs: List[str]) -> Optional[str]:
def parse_grade(self, rocnik: str, school: Optional[db.School]) -> Optional[str]:
if not school:
return None
......@@ -80,30 +129,24 @@ def parse_grade(rocnik: str, school: Optional[db.School], errs: List[str]) -> Op
or school.is_zs and re.fullmatch(r'\d', rocnik)):
return rocnik
errs.append('Ročník neodpovídá typu školy: pro základní je to číslice, pro střední číslice/číslice')
return None
return self.error('Ročník neodpovídá typu školy: pro základní je to číslice, pro střední číslice/číslice')
def parse_born(rok: str, errs: List[str]) -> Optional[int]:
def parse_born(self, rok: str) -> Optional[int]:
if not re.fullmatch(r'\d{4}', rok):
errs.append('Rok narození musí být čtyřciferné číslo')
return None
return self.error('Rok narození musí být čtyřciferné číslo')
r = int(rok)
if r < 2000 or r > 2099:
errs.append('Rok narození musí být v intervalu [2000,2099]')
return None
return self.error('Rok narození musí být v intervalu [2000,2099]')
return r
def find_or_create_user(email: str, krestni: str, prijmeni: str, errs: List[str]) -> Optional[db.User]:
def find_or_create_user(self, email: str, krestni: str, prijmeni: str) -> Optional[db.User]:
sess = db.get_session()
user = sess.query(db.User).filter_by(email=email).one_or_none()
if user:
if user.first_name != krestni or user.last_name != prijmeni:
errs.append(f'Účastník již registrován s odlišným jménem {user.first_name} {user.last_name}')
return None
return self.error(f'Účastník již registrován s odlišným jménem {user.first_name} {user.last_name}')
else:
user = db.User(email=email, first_name=krestni, last_name=prijmeni)
sess.add(user)
......@@ -115,16 +158,14 @@ def find_or_create_user(email: str, krestni: str, prijmeni: str, errs: List[str]
)
return user
def find_or_create_participant(user: db.User, year: int, school_id: int, birth_year: int, grade: str, errs: List[str]) -> Optional[db.Participant]:
def find_or_create_participant(self, user: db.User, year: int, school_id: int, birth_year: int, grade: str) -> Optional[db.Participant]:
sess = db.get_session()
part = sess.query(db.Participant).get((user.user_id, year))
if part:
if (part.school != school_id
or part.grade != grade
or part.birth_year != birth_year):
errs.append('Účastník již zaregistrován s odlišnou školou/ročníkem/rokem narození')
return None
return self.error('Účastník již zaregistrován s odlišnou školou/ročníkem/rokem narození')
else:
part = db.Participant(user=user, year=year, school=school_id, birth_year=birth_year, grade=grade)
sess.add(part)
......@@ -136,72 +177,94 @@ def find_or_create_participant(user: db.User, year: int, school_id: int, birth_y
return part
def find_or_create_participation(self, user: db.User, contest: db.Contest, place: Optional[db.Place]) -> Optional[db.Participation]:
if place is None:
place = contest.place
def find_or_create_participation(user: db.User, contest: db.Contest, place_id: int) -> Optional[db.Participation]:
sess = db.get_session()
pion = sess.query(db.Participation).get((user.user_id, contest.contest_id))
if not pion:
pion = db.Participation(user=user, contest=contest, place_id=place_id, state=db.PartState.registered)
pions = (sess.query(db.Participation)
.filter_by(user=user)
.filter(db.Participation.contest.has(db.Contest.round == contest.round))
.all())
if not pions:
pion = db.Participation(user=user, contest=contest, place_id=place.place_id, state=db.PartState.registered)
sess.add(pion)
mo.util.log(
type=db.LogType.participant,
what=user.user_id,
details={'action': 'add-to-contest', 'new': db.row2dict(pion)},
)
elif len(pions) == 1:
pion = pions[0]
if pion.place != place:
return self.error('Již se tohoto kola účastní v jiné oblasti')
else:
return self.error('Již se tohoto kola účastní ve vice oblastech, což by nemělo být možné')
# FIXME: Kontrolovat účast v tomtéž kole, ale jiném contestu
return pion
def import_contest_row(contest: db.Contest, r: ContestImportRow) -> List[str]:
errs: List[str] = []
email = parse_email(r.email, errs)
krestni = parse_name(r.krestni, errs)
prijmeni = parse_name(r.prijmeni, errs)
school_place = parse_school(r.kod_skoly, errs)
rocnik = parse_grade(r.rocnik, (school_place.school if school_place else None), errs)
rok_naroz = parse_born(r.rok_naroz, errs)
if (errs
def import_contest_row(self, contest: Optional[db.Contest], r: ContestImportRow):
num_prev_errs = len(self.errors)
email = self.parse_email(r.email)
krestni = self.parse_name(r.krestni)
prijmeni = self.parse_name(r.prijmeni)
school_place = self.parse_school(r.kod_skoly)
rocnik = self.parse_grade(r.rocnik, (school_place.school if school_place else None))
rok_naroz = self.parse_born(r.rok_naroz)
misto = self.parse_opt_place(r.kod_mista)
oblast = self.parse_opt_place(r.kod_oblasti)
if (len(self.errors) > num_prev_errs
or email is None
or krestni is None
or prijmeni is None
or school_place is None
or rocnik is None
or rok_naroz is None):
return errs
return
user = find_or_create_user(email, krestni, prijmeni, errs)
user = self.find_or_create_user(email, krestni, prijmeni)
if user is None:
return errs
return
part = find_or_create_participant(user, mo.current_year, school_place.place_id, rok_naroz, rocnik, errs)
part = self.find_or_create_participant(user, mo.current_year, school_place.place_id, rok_naroz, rocnik)
if part is None:
return errs
return
find_or_create_participation(user, contest, contest.place.place_id)
if contest:
if oblast is not None and oblast.place_id != contest.place.place_id:
return self.error('Účastník soutěží mimo oblast, do které se importuje')
else:
if oblast is None:
return self.error('Je nutné uvést oblast')
contest = db.get_session().query(db.Contest).filter_by(round=self.round, place=oblast).one_or_none()
if contest is None:
return self.error('V uvedené oblasti toto kolo neprobíhá')
return errs
self.find_or_create_participation(user, contest, misto)
def import_contest(self, round: db.Round, contest: Optional[db.Contest], path: str) -> bool:
self.round = round
def import_contest(contest: db.Contest, path: str) -> List[str]:
try:
with open(path) as file:
rows: List[ContestImportRow] = mo.csv.read(file=file, dialect='excel', row_class=ContestImportRow)
except Exception as e:
return [f'Chybná struktura tabulky {e}']
return self.error(f'Chybná struktura tabulky {e}')
errs: List[str] = []
line_num = 2
for row in rows:
row_errs = import_contest_row(contest, row)
for err in row_errs:
errs.append(f"Řádek {line_num}: {err}")
if len(errs) >= 100:
errs.append('Import přerušen pro příliš mnoho chyb')
self.line_errors = []
self.import_contest_row(contest, row)
for err in self.line_errors:
self.errors.append(f"Řádek {line_num}: {err}")
if len(self.errors) >= 100:
self.errors.append('Import přerušen pro příliš mnoho chyb')
break
line_num += 1
return errs
return len(self.errors) == 0
def contest_template() -> str:
......
......@@ -63,5 +63,7 @@ app.before_request(init_request)
# Většina webu je v samostatných modulech
import mo.web.main
import mo.web.org
import mo.web.org_contest
import mo.web.org_place
import mo.web.org_users
import mo.web.menu
from flask import render_template, g, redirect, url_for, flash, request
from flask_wtf import FlaskForm
import flask_wtf.file
import locale
import os
import secrets
from sqlalchemy.orm import joinedload
from typing import List, Tuple, Optional
import werkzeug.exceptions
import wtforms
from flask import render_template, g, redirect, url_for
import mo
import mo.csv
import mo.db as db
import mo.imports
import mo.rights
import mo.util
from mo.web import app
from mo.web.table import Table, Column, cell_place_link, cell_user_link
import wtforms.validators as validators
@app.route('/org/')
def org_index():
return render_template('org_index.html')
@app.route('/org/place/<int:id>/')
def org_place(id: int):
sess = db.get_session()
place = sess.query(db.Place).get(id)
if not place:
raise werkzeug.exceptions.NotFound()
if place.type == db.PlaceType.school:
school = sess.query(db.School).get(place.place_id)
else:
school = None
children = sorted(place.children, key=lambda p: locale.strxfrm(p.name))
rr = mo.rights.Rights(g.user)
rr.get_for(place)
return render_template(
'org_place.html', place=place, school=school,
rights=sorted(rr.current_rights, key=lambda r: r. name),
can_edit=rr.can_edit_place(place),
children=children
)
class PlaceEditForm(FlaskForm):
name = wtforms.StringField(
'Název',
validators=[validators.DataRequired()]
)
code = wtforms.StringField(
'Kód', filters=[lambda x: x or None], # may be NULL in db
description="Při nevyplnění se použije ID místa"
)
type = wtforms.SelectField(
'Typ', choices=db.PlaceType.choices(), coerce=db.PlaceType.coerce
)
nuts = wtforms.StringField(
'NUTS', filters=[lambda x: x or None], # may be NULL in db
description="Pro okresy a výše"
)
note = wtforms.StringField('Poznámka')
submit = wtforms.SubmitField('Uložit')
class PlaceSchoolEditForm(PlaceEditForm):
red_izo = wtforms.StringField('RED_IZO')
ico = wtforms.StringField('IČO')
official_name = wtforms.StringField('Oficiální název')
address = wtforms.StringField('Adresa')
is_zs = wtforms.BooleanField('')
is_ss = wtforms.BooleanField('')
submit = wtforms.SubmitField('Uložit')
@app.route('/org/place/<int:id>/edit', methods=('GET', 'POST'))
def org_place_edit(id: int):
sess = db.get_session()
place = sess.query(db.Place).get(id)
if not place:
raise werkzeug.exceptions.NotFound()
rr = mo.rights.Rights(g.user)
rr.get_for(place)
if not rr.can_edit_place(place):
raise werkzeug.exceptions.Forbidden()
if place.type == db.PlaceType.school:
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))
else:
form = PlaceEditForm(obj=place)
school = None
form.type.choices = db.PlaceType.choices(level=place.level)
if form.validate_on_submit():
form.populate_obj(place)
if school:
form.populate_obj(school)
msg = 'Změny místa uloženy'
redirectURL = url_for('org_place', id=id)
if sess.is_modified(place) or school and sess.is_modified(school):
placeChanges = db.get_object_changes(place)
schoolChanges = {}
if school:
if request.form.get('type') != 'school':
# School record removed
mo.util.log(
type=db.LogType.place,
what=school.place_id,
details={'action': 'school-delete', 'school': db.row2dict(school)},
)
app.logger.info(f"Deleting school record for place {place.place_id}")
db.get_session().delete(school)
msg = 'Změny místa uloženy, záznam o škole smazán'
else:
schoolChanges = db.get_object_changes(school)
elif request.form.get('type') == 'school':
# School record created
new_school = db.School()
new_school.place_id = place.place_id
mo.util.log(
type=db.LogType.place,
what=new_school.place_id,
details={'action': 'school-add'},
)
app.logger.info(f"Creating new school for place {place.place_id}")
db.get_session().add(new_school)
# Take org directly to the school edit to fill the data
msg = 'Záznam o škole vytvořen, vyplňte prosím všechna data'
redirectURL = url_for('org_place_edit', id=id)
changes = {**placeChanges, **schoolChanges}
app.logger.info(f"Place {id} modified, changes: {changes}")
mo.util.log(
type=db.LogType.place,
what=id,
details={'action': 'edit', 'changes': changes},
)
db.get_session().commit()
flash(msg, 'success')
else:
flash(u'Žádné změny k uložení', 'info')
return redirect(redirectURL)
parents = reversed(db.get_place_parents(place)[1:]) # without place itself and from the top
return render_template(
'org_place_edit.html', place=place, school=school,
parents=parents, form=form
)
class PlaceMoveForm(FlaskForm):
code = wtforms.StringField(validators=[validators.DataRequired()])
submit = wtforms.SubmitField('Najít místo')
reset = wtforms.HiddenField()
move = wtforms.HiddenField()
class PlaceMoveConfirmForm(FlaskForm):
code = wtforms.HiddenField()
reset = wtforms.SubmitField('Zrušit')
move = wtforms.SubmitField('Přesunout')
@app.route('/org/place/<int:id>/move', methods=('GET', 'POST'))
def org_place_move(id: int):
sess = db.get_session()
# Tests: can move only existing places that we can edit
place = sess.query(db.Place).get(id)
if not place:
raise werkzeug.exceptions.NotFound()
rr = mo.rights.Rights(g.user)
rr.get_for(place)
if not rr.can_edit_place(place):
raise werkzeug.exceptions.Forbidden()
parents = reversed(db.get_place_parents(place)[1:]) # without place itself and from the top
new_parents = None
search_error = None
form = PlaceMoveForm()
form_confirm = None
if form.validate_on_submit():
if form.reset.data:
return redirect(url_for('org_place_move', id=id))
new_parent = db.get_place_by_code(form.code.data)
if not new_parent:
search_error = 'Místo s tímto kódem se nepovedlo nalézt'
else:
new_parents = reversed(db.get_place_parents(new_parent))
(_, levels) = db.place_type_names_and_levels[place.type]
rr.get_for(new_parent)
if not rr.can_edit_place(new_parent):
search_error = 'Nemáte právo k editaci vybraného nadřazeného místa, přesun nelze uskutečnit'
elif (new_parent.level + 1) not in levels:
search_error = f'Toto místo ({place.type_name()}) nelze přemístit pod vybrané místo ({new_parent.type_name()}), dostalo by se na nepovolený level'
elif new_parent.place_id == place.parent:
search_error = 'Žádná změna, místo je zde již umístěno'
elif form.move.data:
# Everything is OK, if submitted with 'move' do the move
place.parent = new_parent.place_id
place.level = new_parent.level + 1
changes = db.get_object_changes(place)
mo.util.log(
type=db.LogType.place,
what=id,
details={'action': 'move', 'changes': changes},
)
app.logger.info(f"Place {id} moved, changes: {changes}")
db.get_session().commit()
flash('Místo úspěšně přesunuto', 'success')
return redirect(url_for('org_place', id=id))
else:
# OK but not confirmed yet, display the confirm form
form_confirm = PlaceMoveConfirmForm()
form_confirm.code.data = form.code.data
return render_template(
'org_place_move.html',
place=place, form=form, form_confirm=form_confirm, search_error=search_error,
parents=parents, new_parents=new_parents
)
@app.route('/org/place/<int:id>/delete', methods=('POST',))
def org_place_delete(id: int):
sess = db.get_session()
# Tests: can delete only existing places that we can edit
place = sess.query(db.Place).get(id)
if not place:
raise werkzeug.exceptions.NotFound()
rr = mo.rights.Rights(g.user)
rr.get_for(place)
if not rr.can_edit_place(place):
raise werkzeug.exceptions.Forbidden()
# Cannot delete place with children
if place.children:
flash("Nelze smazat místo s podřízenými místy", "danger")
return redirect(url_for('org_place', id=id))
# Cannot delete place with contests
if sess.query(db.Contest).filter_by(place_id=id).count() > 0:
flash("Nelze smazat místo ke kterému se váže nějaká soutěž ", "danger")
return redirect(url_for('org_place', id=id))
if place.type == db.PlaceType.school:
school = sess.query(db.School).get(place.place_id)
mo.util.log(
type=db.LogType.place,
what=school.place_id,
details={'action': 'school-delete', 'school': db.row2dict(school)},
)
app.logger.info(f"Deleting school record for place {id}")
db.get_session().delete(school)
mo.util.log(
type=db.LogType.place,
what=id,
details={'action': 'delete', 'place': db.row2dict(place)},
)
app.logger.info(f"Deleting place {id}")
parent = place.parent
db.get_session().delete(place)
db.get_session().commit()
flash("Místo smazáno", "success")
return redirect(url_for('org_place', id=parent))
@app.route('/org/place/<int:id>/new-child', methods=('GET', 'POST'))
def org_place_new_child(id: int):
sess = db.get_session()
# Tests: can add new child only under existing places that we can edit
parent_place = sess.query(db.Place).get(id)
if not parent_place:
raise werkzeug.exceptions.NotFound()
rr = mo.rights.Rights(g.user)
rr.get_for(parent_place)
if not rr.can_edit_place(parent_place):
raise werkzeug.exceptions.Forbidden()
if not parent_place.can_have_child():
raise werkzeug.exceptions.Forbidden()
form = PlaceEditForm()
form.type.choices = db.PlaceType.choices(level=parent_place.level + 1)
if form.validate_on_submit():
new_place = db.Place()
form.populate_obj(new_place)
new_place.parent = parent_place.place_id
new_place.level = parent_place.level + 1
sess.add(new_place)
sess.flush()
app.logger.info(f"New place created: {db.row2dict(new_place)}")
mo.util.log(
type=db.LogType.place,
what=new_place.place_id,
details={'action': 'new', 'place': db.row2dict(new_place)},
)
redirect_url = url_for('org_place', id=new_place.place_id)
msg = 'Nové místo uloženo'
if new_place.type == db.PlaceType.school:
new_school = db.School()
new_school.place_id = new_place.place_id
mo.util.log(
type=db.LogType.place,
what=new_school.place_id,
details={'action': 'school-add'},
)
app.logger.info(f"Creating new school for place {new_place.place_id}")
sess.add(new_school)
# Take org directly to the school edit to fill the data
msg = 'Záznam o škole vytvořen, vyplňte prosím všechna data'
redirect_url = url_for('org_place_edit', id=new_place.place_id)
sess.commit()
flash(msg, 'success')
return redirect(redirect_url)
parents = reversed(db.get_place_parents(parent_place))
return render_template('org_place_new.html', parents=parents, form=form)
@app.route('/org/place/')
def org_place_root():
root = db.get_root_place()
return redirect(url_for('org_place', id=root.place_id))
@app.route('/org/place/<int:id>/rights')
def org_place_rights(id: int):
sess = db.get_session()
place = sess.query(db.Place).get(id)
if not place:
raise werkzeug.exceptions.NotFound()
parent_ids = [p.place_id for p in db.get_place_parents(place)]
roles = (sess.query(db.UserRole)
.filter(db.UserRole.place_id.in_(parent_ids))
.options(joinedload(db.UserRole.user))
.all())
rr = mo.rights.Rights(g.user)
rr.get_for(place)
return render_template(
'org_place_rights.html', place=place, rights=rr.current_rights,
roles=roles, roles_by_type=mo.rights.roles_by_type
)
@app.route('/org/contest/')
def org_contest_root():
sess = db.get_session()
rounds = sess.query(db.Round).filter_by(year=mo.current_year).order_by(db.Round.year, db.Round.category, db.Round.seq)
return render_template('org_contest_root.html', rounds=rounds, level_names=mo.db.place_level_names)
def get_round(id: int) -> db.Round:
round = db.get_session().query(db.Round).get(id)
if not round:
raise werkzeug.exceptions.NotFound()
return round
@app.route('/org/contest/r/<int:id>/')
def org_contest_round(id: int):
sess = db.get_session()
round = get_round(id)
contests = (sess.query(db.Contest)
.filter_by(round=round)
.options(joinedload(db.Contest.place))
.all())
contests.sort(key=lambda c: locale.strxfrm(c.place.name))
return render_template('org_contest_round.html', round=round, contests=contests, level_names=mo.db.place_level_names)
def get_contest(id: int) -> db.Contest:
contest = (db.get_session().query(db.Contest)
.options(joinedload(db.Contest.place),
joinedload(db.Contest.round))
.get(id))
if not contest:
raise werkzeug.exceptions.NotFound()
return contest
def get_contest_rr(id: int, right_needed: Optional[mo.rights.Right]) -> Tuple[db.Contest, mo.rights.Rights]:
contest = get_contest(id)
rr = mo.rights.Rights(g.user)
rr.get_for_contest(contest)
if not (right_needed is None or rr.have_right(right_needed)):
raise werkzeug.exceptions.Forbidden()
return contest, rr
@app.route('/org/contest/c/<int:id>')
def org_contest(id: int):
contest, rr = get_contest_rr(id, None)
return render_template(
'org_contest.html',
contest=contest,
rights=sorted(rr.current_rights, key=lambda r: r. name),
can_manage=rr.have_right(mo.rights.Right.manage_contest),
)
class ContestImportForm(FlaskForm):
file = flask_wtf.file.FileField("Soubor", validators=[flask_wtf.file.FileRequired()])
submit = wtforms.SubmitField('Importovat')
@app.route('/org/contest/c/<int:id>/import', methods=('GET', 'POST'))
def org_contest_import(id: int):
contest, rr = get_contest_rr(id, mo.rights.Right.manage_contest)
form = ContestImportForm()
errs = []
if form.validate_on_submit():
tmp_name = secrets.token_hex(16) + '.csv'
tmp_path = os.path.join(app.instance_path, 'imports', tmp_name)
form.file.data.save(tmp_path)
app.logger.info('Import: Zpracovávám soubor %s pro contest_id=%s, uid=%s', tmp_name, contest.contest_id, g.user.user_id)
errs = mo.imports.import_contest(contest, tmp_path)
if not errs:
mo.util.log(
type=db.LogType.contest,
what=contest.contest_id,
details={'action': 'import'}
)
db.get_session().commit()
flash('Účastníci importováni', 'success')
return redirect(url_for('org_contest', id=contest.contest_id))
else:
flash('Došlo k chybě při importu (detaily níže)', 'danger')
return render_template(
'org_contest_import.html',
contest=contest,
form=form,
errs=errs,
)
@app.route('/org/contest/import/help.html')
def org_contest_import_help():
return render_template('org_contest_import_help.html')
@app.route('/org/contest/import/sablona.csv')
def org_contest_import_template():
out = mo.imports.contest_template()
resp = app.make_response(out)
resp.content_type = 'text/csv; charset=utf=8'
return resp
contest_list_columns = (
Column(key='first_name', name='krestni', title='Křestní jméno'),
Column(key='last_name', name='prijmeni', title='Příjmení'),
Column(key='email', name='email', title='E-mail'),
Column(key='school', name='skola', title='Škola'),
Column(key='school_code', name='kod_skoly', title='Kód školy'),
Column(key='grade', name='rocnik', title='Ročník'),
Column(key='born_year', name='rok_naroz', title='Rok naroz.'),
Column(key='place_code', name='kod_soutez_mista', title='Sout. místo'),
Column(key='status', name='stav', title='Stav'),
)
@app.route('/org/contest/c/<int:id>/ucastnici')
def org_contest_list(id: int):
contest, rr = get_contest_rr(id, mo.rights.Right.manage_contest)
format = request.args.get('format', "")
ctants = (db.get_session()
.query(db.Participation, db.Participant)
.select_from(db.Participation)
.join(db.Participant, db.Participant.user_id == db.Participation.user_id)
.options(joinedload(db.Participation.user),
joinedload(db.Participation.place),
joinedload(db.Participant.school_place))
.filter(db.Participation.contest == contest)
.filter(db.Participant.year == contest.round.year)
.all())
rows: List[dict] = []
for pion, pant in ctants:
rows.append({
'first_name': pion.user.first_name,
'last_name': pion.user.last_name,
'email': cell_user_link(pion.user, pion.user.email),
'school': pant.school_place.name,
'school_code': cell_place_link(pant.school_place, pant.school_place.get_code()),
'grade': pant.grade,
'born_year': pant.birth_year,
'place_code': pion.place.get_code(),
'status': pion.state.name,
})
rows.sort(key=lambda r: (locale.strxfrm(r['last_name']), locale.strxfrm(r['first_name'])))
table = Table(
columns=contest_list_columns,
rows=rows,
filename='ucastnici',
)
if format == "":
return render_template(
'org_contest_list.html',
contest=contest,
table=table,
)
else:
return table.send_as(format)
from flask import render_template, g, redirect, url_for, flash, request
from flask_wtf import FlaskForm
import flask_wtf.file
import locale
import os
import secrets
from sqlalchemy.orm import joinedload
from typing import List, Tuple, Optional
import werkzeug.exceptions
import wtforms
import mo
import mo.csv
import mo.db as db
import mo.imports
import mo.rights
import mo.util
from mo.web import app
from mo.web.table import Table, Column, cell_place_link, cell_user_link
import wtforms.validators as validators
class ImportForm(FlaskForm):
file = flask_wtf.file.FileField("Soubor", validators=[flask_wtf.file.FileRequired()])
submit = wtforms.SubmitField('Importovat')
@app.route('/org/contest/')
def org_contest_root():
sess = db.get_session()
rounds = sess.query(db.Round).filter_by(year=mo.current_year).order_by(db.Round.year, db.Round.category, db.Round.seq)
return render_template('org_contest_root.html', rounds=rounds, level_names=mo.db.place_level_names)
def get_round(id: int) -> db.Round:
round = db.get_session().query(db.Round).get(id)
if not round:
raise werkzeug.exceptions.NotFound()
return round
def get_round_rr(id: int, right_needed: Optional[mo.rights.Right]) -> Tuple[db.Round, mo.rights.Rights]:
round = get_round(id)
rr = mo.rights.Rights(g.user)
rr.get_for_round(round)
if not (right_needed is None or rr.have_right(right_needed)):
raise werkzeug.exceptions.Forbidden()
return round, rr
@app.route('/org/contest/r/<int:id>/')
def org_round(id: int):
sess = db.get_session()
round, rr = get_round_rr(id, None)
contests = (sess.query(db.Contest)
.filter_by(round=round)
.options(joinedload(db.Contest.place))
.all())
contests.sort(key=lambda c: locale.strxfrm(c.place.name))
return render_template(
'org_round.html',
round=round,
contests=contests,
level_names=mo.db.place_level_names,
can_manage=rr.have_right(mo.rights.Right.manage_contest),
)
@app.route('/org/contest/r/<int:id>/list')
def org_round_list(id: int):
return render_template('not_implemented.html')
@app.route('/org/contest/r/<int:id>/import', methods=('GET', 'POST'))
def org_round_import(id: int):
round, rr = get_round_rr(id, mo.rights.Right.manage_contest)
form = ImportForm()
errs = []
if form.validate_on_submit():
tmp_name = secrets.token_hex(16) + '.csv'
tmp_path = os.path.join(app.instance_path, 'imports', tmp_name)
form.file.data.save(tmp_path)
app.logger.info('Import: Zpracovávám soubor %s pro round=%s, uid=%s', tmp_name, round.round_code(), g.user.user_id)
imp = mo.imports.Import(g.user)
if imp.import_contest(round, None, tmp_path):
mo.util.log(
type=db.LogType.round,
what=round.round_id,
details={'action': 'import'}
)
db.get_session().commit()
flash('Účastníci importováni', 'success')
return redirect(url_for('org_round', id=round.round_id))
else:
flash('Došlo k chybě při importu (detaily níže)', 'danger')
errs = imp.errors
return render_template(
'org_round_import.html',
round=round,
form=form,
errs=errs,
)
def get_contest(id: int) -> db.Contest:
contest = (db.get_session().query(db.Contest)
.options(joinedload(db.Contest.place),
joinedload(db.Contest.round))
.get(id))
if not contest:
raise werkzeug.exceptions.NotFound()
return contest
def get_contest_rr(id: int, right_needed: Optional[mo.rights.Right]) -> Tuple[db.Contest, mo.rights.Rights]:
contest = get_contest(id)
rr = mo.rights.Rights(g.user)
rr.get_for_contest(contest)
if not (right_needed is None or rr.have_right(right_needed)):
raise werkzeug.exceptions.Forbidden()
return contest, rr
@app.route('/org/contest/c/<int:id>')
def org_contest(id: int):
contest, rr = get_contest_rr(id, None)
return render_template(
'org_contest.html',
contest=contest,
rights=sorted(rr.current_rights, key=lambda r: r. name),
can_manage=rr.have_right(mo.rights.Right.manage_contest),
)
@app.route('/org/contest/c/<int:id>/import', methods=('GET', 'POST'))
def org_contest_import(id: int):
contest, rr = get_contest_rr(id, mo.rights.Right.manage_contest)
form = ImportForm()
errs = []
if form.validate_on_submit():
tmp_name = secrets.token_hex(16) + '.csv'
tmp_path = os.path.join(app.instance_path, 'imports', tmp_name)
form.file.data.save(tmp_path)
app.logger.info('Import: Zpracovávám soubor %s pro contest_id=%s, uid=%s', tmp_name, contest.contest_id, g.user.user_id)
imp = mo.imports.Import(g.user)
if imp.import_contest(contest.round, contest, tmp_path):
mo.util.log(
type=db.LogType.contest,
what=contest.contest_id,
details={'action': 'import'}
)
db.get_session().commit()
flash('Účastníci importováni', 'success')
return redirect(url_for('org_contest', id=contest.contest_id))
else:
flash('Došlo k chybě při importu (detaily níže)', 'danger')
errs = imp.errors
return render_template(
'org_contest_import.html',
contest=contest,
form=form,
errs=errs,
)
@app.route('/org/contest/import/help.html')
def org_contest_import_help():
return render_template('org_contest_import_help.html')
@app.route('/org/contest/import/sablona.csv')
def org_contest_import_template():
out = mo.imports.contest_template()
resp = app.make_response(out)
resp.content_type = 'text/csv; charset=utf=8'
return resp
contest_list_columns = (
Column(key='first_name', name='krestni', title='Křestní jméno'),
Column(key='last_name', name='prijmeni', title='Příjmení'),
Column(key='email', name='email', title='E-mail'),
Column(key='school', name='skola', title='Škola'),
Column(key='school_code', name='kod_skoly', title='Kód školy'),
Column(key='grade', name='rocnik', title='Ročník'),
Column(key='born_year', name='rok_naroz', title='Rok naroz.'),
Column(key='place_code', name='kod_soutez_mista', title='Sout. místo'),
Column(key='status', name='stav', title='Stav'),
)
@app.route('/org/contest/c/<int:id>/ucastnici')
def org_contest_list(id: int):
contest, rr = get_contest_rr(id, mo.rights.Right.manage_contest)
format = request.args.get('format', "")
ctants = (db.get_session()
.query(db.Participation, db.Participant)
.select_from(db.Participation)
.join(db.Participant, db.Participant.user_id == db.Participation.user_id)
.options(joinedload(db.Participation.user),
joinedload(db.Participation.place),
joinedload(db.Participant.school_place))
.filter(db.Participation.contest == contest)
.filter(db.Participant.year == contest.round.year)
.all())
rows: List[dict] = []
for pion, pant in ctants:
rows.append({
'first_name': pion.user.first_name,
'last_name': pion.user.last_name,
'email': cell_user_link(pion.user, pion.user.email),
'school': pant.school_place.name,
'school_code': cell_place_link(pant.school_place, pant.school_place.get_code()),
'grade': pant.grade,
'born_year': pant.birth_year,
'place_code': pion.place.get_code(),
'status': pion.state.name,
})
rows.sort(key=lambda r: (locale.strxfrm(r['last_name']), locale.strxfrm(r['first_name'])))
table = Table(
columns=contest_list_columns,
rows=rows,
filename='ucastnici',
)
if format == "":
return render_template(
'org_contest_list.html',
contest=contest,
table=table,
)
else:
return table.send_as(format)
from flask import render_template, g, redirect, url_for, flash, request
from flask_wtf import FlaskForm
import locale
from sqlalchemy.orm import joinedload
from typing import List, Tuple, Optional
import werkzeug.exceptions
import wtforms
import mo
import mo.csv
import mo.db as db
import mo.imports
import mo.rights
import mo.util
from mo.web import app
import wtforms.validators as validators
@app.route('/org/place/<int:id>/')
def org_place(id: int):
sess = db.get_session()
place = sess.query(db.Place).get(id)
if not place:
raise werkzeug.exceptions.NotFound()
if place.type == db.PlaceType.school:
school = sess.query(db.School).get(place.place_id)
else:
school = None
children = sorted(place.children, key=lambda p: locale.strxfrm(p.name))
rr = mo.rights.Rights(g.user)
rr.get_for(place)
return render_template(
'org_place.html', place=place, school=school,
rights=sorted(rr.current_rights, key=lambda r: r. name),
can_edit=rr.can_edit_place(place),
children=children
)
class PlaceEditForm(FlaskForm):
name = wtforms.StringField(
'Název',
validators=[validators.DataRequired()]
)
code = wtforms.StringField(
'Kód', filters=[lambda x: x or None], # may be NULL in db
description="Při nevyplnění se použije ID místa"
)
type = wtforms.SelectField(
'Typ', choices=db.PlaceType.choices(), coerce=db.PlaceType.coerce
)
nuts = wtforms.StringField(
'NUTS', filters=[lambda x: x or None], # may be NULL in db
description="Pro okresy a výše"
)
note = wtforms.StringField('Poznámka')
submit = wtforms.SubmitField('Uložit')
class PlaceSchoolEditForm(PlaceEditForm):
red_izo = wtforms.StringField('RED_IZO')
ico = wtforms.StringField('IČO')
official_name = wtforms.StringField('Oficiální název')
address = wtforms.StringField('Adresa')
is_zs = wtforms.BooleanField('')
is_ss = wtforms.BooleanField('')
submit = wtforms.SubmitField('Uložit')
@app.route('/org/place/<int:id>/edit', methods=('GET', 'POST'))
def org_place_edit(id: int):
sess = db.get_session()
place = sess.query(db.Place).get(id)
if not place:
raise werkzeug.exceptions.NotFound()
rr = mo.rights.Rights(g.user)
rr.get_for(place)
if not rr.can_edit_place(place):
raise werkzeug.exceptions.Forbidden()
if place.type == db.PlaceType.school:
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))
else:
form = PlaceEditForm(obj=place)
school = None
form.type.choices = db.PlaceType.choices(level=place.level)
if form.validate_on_submit():
form.populate_obj(place)
if school:
form.populate_obj(school)
msg = 'Změny místa uloženy'
redirectURL = url_for('org_place', id=id)
if sess.is_modified(place) or school and sess.is_modified(school):
placeChanges = db.get_object_changes(place)
schoolChanges = {}
if school:
if request.form.get('type') != 'school':
# School record removed
mo.util.log(
type=db.LogType.place,
what=school.place_id,
details={'action': 'school-delete', 'school': db.row2dict(school)},
)
app.logger.info(f"Deleting school record for place {place.place_id}")
db.get_session().delete(school)
msg = 'Změny místa uloženy, záznam o škole smazán'
else:
schoolChanges = db.get_object_changes(school)
elif request.form.get('type') == 'school':
# School record created
new_school = db.School()
new_school.place_id = place.place_id
mo.util.log(
type=db.LogType.place,
what=new_school.place_id,
details={'action': 'school-add'},
)
app.logger.info(f"Creating new school for place {place.place_id}")
db.get_session().add(new_school)
# Take org directly to the school edit to fill the data
msg = 'Záznam o škole vytvořen, vyplňte prosím všechna data'
redirectURL = url_for('org_place_edit', id=id)
changes = {**placeChanges, **schoolChanges}
app.logger.info(f"Place {id} modified, changes: {changes}")
mo.util.log(
type=db.LogType.place,
what=id,
details={'action': 'edit', 'changes': changes},
)
db.get_session().commit()
flash(msg, 'success')
else:
flash(u'Žádné změny k uložení', 'info')
return redirect(redirectURL)
parents = reversed(db.get_place_parents(place)[1:]) # without place itself and from the top
return render_template(
'org_place_edit.html', place=place, school=school,
parents=parents, form=form
)
class PlaceMoveForm(FlaskForm):
code = wtforms.StringField(validators=[validators.DataRequired()])
submit = wtforms.SubmitField('Najít místo')
reset = wtforms.HiddenField()
move = wtforms.HiddenField()
class PlaceMoveConfirmForm(FlaskForm):
code = wtforms.HiddenField()
reset = wtforms.SubmitField('Zrušit')
move = wtforms.SubmitField('Přesunout')
@app.route('/org/place/<int:id>/move', methods=('GET', 'POST'))
def org_place_move(id: int):
sess = db.get_session()
# Tests: can move only existing places that we can edit
place = sess.query(db.Place).get(id)
if not place:
raise werkzeug.exceptions.NotFound()
rr = mo.rights.Rights(g.user)
rr.get_for(place)
if not rr.can_edit_place(place):
raise werkzeug.exceptions.Forbidden()
parents = reversed(db.get_place_parents(place)[1:]) # without place itself and from the top
new_parents = None
search_error = None
form = PlaceMoveForm()
form_confirm = None
if form.validate_on_submit():
if form.reset.data:
return redirect(url_for('org_place_move', id=id))
new_parent = db.get_place_by_code(form.code.data)
if not new_parent:
search_error = 'Místo s tímto kódem se nepovedlo nalézt'
else:
new_parents = reversed(db.get_place_parents(new_parent))
(_, levels) = db.place_type_names_and_levels[place.type]
rr.get_for(new_parent)
if not rr.can_edit_place(new_parent):
search_error = 'Nemáte právo k editaci vybraného nadřazeného místa, přesun nelze uskutečnit'
elif (new_parent.level + 1) not in levels:
search_error = f'Toto místo ({place.type_name()}) nelze přemístit pod vybrané místo ({new_parent.type_name()}), dostalo by se na nepovolený level'
elif new_parent.place_id == place.parent:
search_error = 'Žádná změna, místo je zde již umístěno'
elif form.move.data:
# Everything is OK, if submitted with 'move' do the move
place.parent = new_parent.place_id
place.level = new_parent.level + 1
changes = db.get_object_changes(place)
mo.util.log(
type=db.LogType.place,
what=id,
details={'action': 'move', 'changes': changes},
)
app.logger.info(f"Place {id} moved, changes: {changes}")
db.get_session().commit()
flash('Místo úspěšně přesunuto', 'success')
return redirect(url_for('org_place', id=id))
else:
# OK but not confirmed yet, display the confirm form
form_confirm = PlaceMoveConfirmForm()
form_confirm.code.data = form.code.data
return render_template(
'org_place_move.html',
place=place, form=form, form_confirm=form_confirm, search_error=search_error,
parents=parents, new_parents=new_parents
)
@app.route('/org/place/<int:id>/delete', methods=('POST',))
def org_place_delete(id: int):
sess = db.get_session()
# Tests: can delete only existing places that we can edit
place = sess.query(db.Place).get(id)
if not place:
raise werkzeug.exceptions.NotFound()
rr = mo.rights.Rights(g.user)
rr.get_for(place)
if not rr.can_edit_place(place):
raise werkzeug.exceptions.Forbidden()
# Cannot delete place with children
if place.children:
flash("Nelze smazat místo s podřízenými místy", "danger")
return redirect(url_for('org_place', id=id))
# Cannot delete place with contests
if sess.query(db.Contest).filter_by(place_id=id).count() > 0:
flash("Nelze smazat místo ke kterému se váže nějaká soutěž ", "danger")
return redirect(url_for('org_place', id=id))
if place.type == db.PlaceType.school:
school = sess.query(db.School).get(place.place_id)
mo.util.log(
type=db.LogType.place,
what=school.place_id,
details={'action': 'school-delete', 'school': db.row2dict(school)},
)
app.logger.info(f"Deleting school record for place {id}")
db.get_session().delete(school)
mo.util.log(
type=db.LogType.place,
what=id,
details={'action': 'delete', 'place': db.row2dict(place)},
)
app.logger.info(f"Deleting place {id}")
parent = place.parent
db.get_session().delete(place)
db.get_session().commit()
flash("Místo smazáno", "success")
return redirect(url_for('org_place', id=parent))
@app.route('/org/place/<int:id>/new-child', methods=('GET', 'POST'))
def org_place_new_child(id: int):
sess = db.get_session()
# Tests: can add new child only under existing places that we can edit
parent_place = sess.query(db.Place).get(id)
if not parent_place:
raise werkzeug.exceptions.NotFound()
rr = mo.rights.Rights(g.user)
rr.get_for(parent_place)
if not rr.can_edit_place(parent_place):
raise werkzeug.exceptions.Forbidden()
if not parent_place.can_have_child():
raise werkzeug.exceptions.Forbidden()
form = PlaceEditForm()
form.type.choices = db.PlaceType.choices(level=parent_place.level + 1)
if form.validate_on_submit():
new_place = db.Place()
form.populate_obj(new_place)
new_place.parent = parent_place.place_id
new_place.level = parent_place.level + 1
sess.add(new_place)
sess.flush()
app.logger.info(f"New place created: {db.row2dict(new_place)}")
mo.util.log(
type=db.LogType.place,
what=new_place.place_id,
details={'action': 'new', 'place': db.row2dict(new_place)},
)
redirect_url = url_for('org_place', id=new_place.place_id)
msg = 'Nové místo uloženo'
if new_place.type == db.PlaceType.school:
new_school = db.School()
new_school.place_id = new_place.place_id
mo.util.log(
type=db.LogType.place,
what=new_school.place_id,
details={'action': 'school-add'},
)
app.logger.info(f"Creating new school for place {new_place.place_id}")
sess.add(new_school)
# Take org directly to the school edit to fill the data
msg = 'Záznam o škole vytvořen, vyplňte prosím všechna data'
redirect_url = url_for('org_place_edit', id=new_place.place_id)
sess.commit()
flash(msg, 'success')
return redirect(redirect_url)
parents = reversed(db.get_place_parents(parent_place))
return render_template('org_place_new.html', parents=parents, form=form)
@app.route('/org/place/')
def org_place_root():
root = db.get_root_place()
return redirect(url_for('org_place', id=root.place_id))
@app.route('/org/place/<int:id>/rights')
def org_place_rights(id: int):
sess = db.get_session()
place = sess.query(db.Place).get(id)
if not place:
raise werkzeug.exceptions.NotFound()
parent_ids = [p.place_id for p in db.get_place_parents(place)]
roles = (sess.query(db.UserRole)
.filter(db.UserRole.place_id.in_(parent_ids))
.options(joinedload(db.UserRole.user))
.all())
rr = mo.rights.Rights(g.user)
rr.get_for(place)
return render_template(
'org_place_rights.html', place=place, rights=rr.current_rights,
roles=roles, roles_by_type=mo.rights.roles_by_type
)
......@@ -11,16 +11,25 @@ nebo LibreOffice Calc) umí v tomto formátu ukládat.
<p>První řádek tabulky obsahuje názvy sloupců. Ty najdete v šabloně, kterou si
můžete stáhnout ze stránky importu. Názvy neměňte.
<p>Definovány jsou tyto sloupce:
<p>Definovány jsou tyto sloupce (tučné jsou povinné):
<table class=data>
<tr><th>Název<th>Obsah
<tr><td>email<td>E-mailová adresa
<tr><td>krestni<td>Křestní jméno
<tr><td>prijmeni<td>Příjmení
<tr><td>kod_skoly<td>Kód školy (viz katalog škol na tomto webu)
<tr><td>rocnik<td>Navštěvovaný ročník (třída). Pro základní školy je to číslo od 1 do 9, pro <i>k</i>-tý ročník <i>r</i>-leté střední školy má formát <i>k</i>/<i>r</i>.
<tr><td>rok_naroz<td>Rok narození
<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_skoly</b><td>Kód školy (viz katalog škol na tomto webu)
<tr><td><b>rocnik</b><td>Navštěvovaný ročník (třída). Pro základní školy je to číslo od 1 do 9, pro <i>k</i>-tý ročník <i>r</i>-leté střední školy má formát <i>k</i>/<i>r</i>.
<tr><td><b>rok_naroz</b><td>Rok narození
<tr><td>kod_mista<td>Pokud účastník soutěží někde jinde, je zde uveden kód oblasti, školy,
nebo speciálního soutěžního místa, kde se soutěž koná. Dozor na soutěžním místě
má pak právo odevzdávat za účastníka řešení.
<tr><td>kod_oblasti<td>Pokud neimportujete do konkrétní soutěže, ale do celého kola,
je nutné uvést kód oblasti, ve které účastník soutěží.
</table>
<p>Importovaní účastníci se přidají ke stávajícím. Duplicity se ignorují. V případě
rozporu mezi importovanými údaji a již známými import selže a je nutné provést
editaci ručně.
{% endblock %}
......@@ -13,7 +13,7 @@
</thead>
{% for r in rounds %}
<tr>
<td><a href='{{ url_for('org_contest_round', id=r.round_id) }}'>{{ r.round_code() }}</a>
<td><a href='{{ url_for('org_round', id=r.round_id) }}'>{{ r.round_code() }}</a>
<td>{{ r.year }}
<td>{{ r.category }}
<td>{{ r.seq }}
......
......@@ -10,6 +10,11 @@
<tr><td>Oblast<td>{{ level_names[round.level] }}
</table>
{% if can_manage %}
<p><a href='{{ url_for('org_round_import', id=round.round_id) }}'>Importovat účastníky</a>
<p><a href='{{ url_for('org_round_list', id=round.round_id) }}'>Seznam účastníků</a>
{% endif %}
<h3>Soutěže</h3>
{% if contests %}
<table class=data>
......
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block body %}
<h2>Kolo {{ round.round_code() }}</h2>
<a href='{{ url_for('org_round', id=round.round_id) }}'>Zpět na kolo</a>
{% if errs %}
<h3>Chyby při importu</h3>
<pre>
{% for e in errs %}
{{ e }}
{% endfor %}
</pre>
{% endif %}
<h3>Import účastníků</h3>
<p>Účastníky kola můžete importovat ve <a href='{{ url_for('org_contest_import_help') }}'>formátu CSV</a>
podle <a href='{{ url_for('org_contest_import_template') }}'>šablony</a>.
{{ wtf.quick_form(form, form_type='horizontal') }}
{% endblock %}