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 11

......@@ -132,6 +132,13 @@ def get_root_place():
return get_session().query(Place).filter_by(parent=None).one()
def get_place_by_id(place_id: int, fetch_school: bool = False) -> Place:
q = get_session().query(Place)
if fetch_school:
q = q.options(joinedload(Place.school))
return q.filter_by(place_id=place_id).one()
def get_place_by_code(code: str, fetch_school: bool = False) -> Optional[Place]:
code = code.strip()
if code == "":
......
......@@ -149,21 +149,16 @@ class Import:
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:
return self.error(f'Škola s kódem "{kod}" nenalezena'+
('. Nechybí vám # na začátku?' if re.fullmatch(r'\d+', kod) else ''))
if place.type != db.PlaceType.school:
return self.error(f'Kód školy "{kod}" neodpovídá škole')
try:
place = mo.users.validate_and_find_school(kod)
except mo.CheckError as e:
return self.error(str(e))
self.school_place_cache[kod] = place
return place
def parse_grade(self, rocnik: str, school: Optional[db.School]) -> Optional[str]:
......@@ -174,48 +169,30 @@ class Import:
# lidé připisují všechny možné i nemožné znaky, které vypadají jako apostrof :)
rocnik = re.sub('[\'"\u00b4\u2019]', "", rocnik)
if (not re.fullmatch(r'\d(/\d)?', rocnik)):
return self.error(f'Ročník má neplatný formát, musí to být buď číslice, nebo číslice/číslice')
if (not school.is_zs and re.fullmatch(r'\d', rocnik)):
return self.error(f'Ročník pro střední školu ({school.place.name}) zapisujte ve formátu číslice/číslice')
if (not school.is_ss and re.fullmatch(r'\d/\d', rocnik)):
return self.error(f'Ročník pro základní školu ({school.place.name}) zapisujte jako číslici 1–9')
return rocnik
try:
return mo.users.normalize_grade(rocnik, school)
except mo.CheckError as e:
return self.error(str(e))
def parse_born(self, rok: str) -> Optional[int]:
if not re.fullmatch(r'\d{4}', rok):
return self.error('Rok narození musí být čtyřciferné číslo')
r = int(rok)
if r < 2000 or r > 2099:
return self.error('Rok narození musí být v intervalu [2000,2099]')
try:
mo.users.validate_born_year(r)
except mo.CheckError as e:
return self.error(str(e))
return r
def find_or_create_user(self, email: str, krestni: str, prijmeni: str, is_org: bool) -> 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:
return self.error(f'Osoba již registrována s odlišným jménem {user.full_name()}')
if (user.is_admin or user.is_org) != is_org:
if is_org:
return self.error('Nelze předefinovat účastníka na organizátora')
else:
return self.error('Nelze předefinovat organizátora na účastníka')
else:
user = db.User(email=email, first_name=krestni, last_name=prijmeni, is_org=is_org)
sess.add(user)
sess.flush() # Aby uživatel dostal user_id
logger.info(f'Import: Založen uživatel user=#{user.user_id} email=<{user.email}>')
mo.util.log(
type=db.LogType.user,
what=user.user_id,
details={'action': 'import', 'new': db.row2dict(user)},
)
try:
user, is_new = mo.users.find_or_create_user(email, krestni, prijmeni, is_org, reason='import')
except mo.CheckError as e:
return self.error(str(e))
if is_new:
self.cnt_new_users += 1
self.new_user_ids.append(user.user_id)
return user
......@@ -235,53 +212,21 @@ class Import:
return pts
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):
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)
logger.info(f'Import: Založen účastník #{user.user_id}')
mo.util.log(
type=db.LogType.participant,
what=user.user_id,
details={'action': 'import', 'new': db.row2dict(part)},
)
try:
part, is_new = mo.users.find_or_create_participant(user, year, school_id, birth_year, grade, reason='import')
except mo.CheckError as e:
return self.error(str(e))
if is_new:
self.cnt_new_participants += 1
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
sess = db.get_session()
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.invited)
sess.add(pion)
logger.info(f'Import: Založena účast user=#{user.user_id} contest=#{contest.contest_id} place=#{place.place_id}')
mo.util.log(
type=db.LogType.participant,
what=user.user_id,
details={'action': 'add-to-contest', 'new': db.row2dict(pion)},
)
try:
pion, is_new = mo.users.find_or_create_participation(user, contest, place, reason='import')
except mo.CheckError as e:
return self.error(str(e))
if is_new:
self.cnt_new_participations += 1
elif len(pions) == 1:
pion = pions[0]
if pion.place != place:
return self.error(f'Již se tohoto kola účastní v {contest.round.get_level().name_locative("jiném", "jiné", "jiném")} ({pion.place.get_code()})')
else:
return self.error('Již se tohoto kola účastní ve vice oblastech, což by nemělo být možné')
return pion
def obtain_contest(self, oblast: Optional[db.Place], allow_none: bool = False):
......
......@@ -5,14 +5,126 @@ import datetime
import email.errors
import email.headerregistry
import re
from typing import Optional
from typing import Optional, Tuple
import mo
import mo.db as db
import mo.util
from mo.util import logger
import mo.tokens
def normalize_grade(rocnik: str, school: db.School) -> str:
""" Aktuálně provádí jen kontrolu formátu. """
if not re.fullmatch(r'\d(/\d)?', rocnik):
raise mo.CheckError('Ročník má neplatný formát, musí to být buď číslice, nebo číslice/číslice')
if not school.is_zs and re.fullmatch(r'\d', rocnik):
raise mo.CheckError(f'Ročník pro střední školu ({school.place.name}) zapisujte ve formátu číslice/číslice')
if not school.is_ss and re.fullmatch(r'\d/\d', rocnik):
raise mo.CheckError(f'Ročník pro základní školu ({school.place.name}) zapisujte jako číslici 1–9')
return rocnik
def validate_born_year(r: int) -> None:
if r < 2000 or r > 2099:
raise mo.CheckError('Rok narození musí být v intervalu [2000,2099]')
def validate_and_find_school(kod: str) -> db.Place:
if kod == "":
raise mo.CheckError('Škola je povinná')
place = db.get_place_by_code(kod, fetch_school=True)
if not place:
raise mo.CheckError(f'Škola s kódem "{kod}" nenalezena' +
('. Nechybí vám # na začátku?' if re.fullmatch(r'\d+', kod) else ''))
if place.type != db.PlaceType.school:
raise mo.CheckError(f'Kód školy "{kod}" neodpovídá škole')
return place
def find_or_create_user(email: str, krestni: str, prijmeni: str, is_org: bool, reason: str) -> Tuple[db.User, bool]:
sess = db.get_session()
user = sess.query(db.User).filter_by(email=email).one_or_none()
is_new = user is None
if user is None: # HACK: Podmínku je nutné zapsat znovu místo užití is_new, jinak si s tím mypy neporadí
user = db.User(email=email, first_name=krestni, last_name=prijmeni, is_org=is_org)
sess.add(user)
sess.flush() # Aby uživatel dostal user_id
logger.info(f'{reason.title()}: Založen uživatel user=#{user.user_id} email=<{user.email}>')
mo.util.log(
type=db.LogType.user,
what=user.user_id,
details={'action': 'create-user', 'reason': reason, 'new': db.row2dict(user)},
)
else:
if user.first_name != krestni or user.last_name != prijmeni:
raise mo.CheckError(f'Osoba již registrována s odlišným jménem {user.full_name()}')
if (user.is_admin or user.is_org) != is_org:
if is_org:
raise mo.CheckError('Nelze předefinovat účastníka na organizátora')
else:
raise mo.CheckError('Nelze předefinovat organizátora na účastníka')
return user, is_new
def find_or_create_participant(user: db.User, year: int, school_id: int, birth_year: int, grade: str, reason: str) -> Tuple[db.Participant, bool]:
sess = db.get_session()
part = sess.query(db.Participant).get((user.user_id, year))
is_new = part is None
if part is None:
part = db.Participant(user=user, year=year, school=school_id, birth_year=birth_year, grade=grade)
sess.add(part)
logger.info(f'{reason.title()}: Založen účastník #{user.user_id}')
mo.util.log(
type=db.LogType.participant,
what=user.user_id,
details={'action': 'create-participant', 'reason': reason, 'new': db.row2dict(part)},
)
else:
if (part.school != school_id
or part.grade != grade
or part.birth_year != birth_year):
raise mo.CheckError('Účastník již zaregistrován s odlišnou školou/ročníkem/rokem narození')
return part, is_new
def find_or_create_participation(user: db.User, contest: db.Contest, place: Optional[db.Place], reason: str) -> Tuple[db.Participation, bool]:
if place is None:
place = contest.place
sess = db.get_session()
pions = (sess.query(db.Participation)
.filter_by(user=user)
.filter(db.Participation.contest.has(db.Contest.round == contest.round))
.all())
is_new = pions == []
if is_new:
pion = db.Participation(user=user, contest=contest, place_id=place.place_id, state=db.PartState.invited)
sess.add(pion)
logger.info(f'{reason.title()}: Založena účast user=#{user.user_id} contest=#{contest.contest_id} place=#{place.place_id}')
mo.util.log(
type=db.LogType.participant,
what=user.user_id,
details={'action': 'add-to-contest', 'reason': reason, 'new': db.row2dict(pion)},
)
elif len(pions) == 1:
pion = pions[0]
if pion.place != place:
raise mo.CheckError(f'Již se tohoto kola účastní v {contest.round.get_level().name_locative("jiném", "jiné", "jiném")} ({pion.place.get_code()})')
else:
raise mo.CheckError('Již se tohoto kola účastní ve více oblastech, což by nemělo být možné')
return pion, is_new
def normalize_email(addr: str) -> str:
if not re.fullmatch(r'.+@.+', addr):
raise mo.CheckError('V e-mailové adrese chybí zavináč')
......
from typing import Optional
import wtforms
from wtforms.widgets.html5 import NumberInput
import mo
import mo.users
import mo.db as db
class OptionalInt(wtforms.IntegerField):
widget = NumberInput()
def process_formdata(self, valuelist):
self.data = None
if valuelist:
if valuelist[0]:
try:
self.data = int(valuelist[0])
except ValueError:
raise wtforms.ValidationError('Nejedná se o číslo.')
class Email(wtforms.StringField):
def __init__(self, label="E-mail", validators=None, **kwargs):
super().__init__(label, validators, **kwargs)
def pre_validate(field, form):
if field.data:
try:
field.data = mo.users.normalize_email(field.data)
except mo.CheckError as e:
raise wtforms.ValidationError(str(e))
class Grade(wtforms.StringField):
"""Pro validaci hledá ve formuláři form.school a podle ní rozlišuje SŠ a ZŠ """
default_description = "Pro základní školy je to číslo od 1 do 9, pro <var>k</var>-tý ročník <var>r</var>-leté střední školy má formát <var>k</var>/<var>r</var>."
validate_grade = True
def __init__(self, label="Ročník", validators=None, description=default_description, **kwargs):
super().__init__(label, validators, description=description, **kwargs)
def pre_validate(field, form):
if field.data:
if field.validate_grade:
school_place = form.school.get_place()
if school_place is not None:
try:
field.data = mo.users.normalize_grade(field.data, school_place.school)
except mo.CheckError as e:
raise wtforms.ValidationError(str(e))
class BirthYear(OptionalInt):
def __init__(self, label="Rok narození", validators=None, **kwargs):
super().__init__(label, validators, **kwargs)
def pre_validate(field, form):
if field.data is not None:
r: int = field.data
try:
mo.users.validate_born_year(r)
except mo.CheckError as e:
raise wtforms.ValidationError(str(e))
class Name(wtforms.StringField):
def pre_validate(field, form):
# XXX: Tato kontrola úmyslně není striktní, aby prošla i jména jako 'de Beer'
if field.data:
if field.data == field.data.lower():
raise wtforms.ValidationError('Ve jméně nejsou velká písmena.')
if field.data == field.data.upper():
raise wtforms.ValidationError('Ve jméně nejsou malá písmena.')
class FirstName(Name):
def __init__(self, label="Jméno", validators=None, **kwargs):
super().__init__(label, validators, **kwargs)
class LastName(Name):
def __init__(self, label="Příjmení", validators=None, **kwargs):
super().__init__(label, validators, **kwargs)
class Place(wtforms.StringField):
def __init__(self, label="Místo", validators=None, **kwargs):
super().__init__(label, validators, **kwargs)
place_loaded: bool = False
place: Optional[db.Place] = None
place_error: str
def load_place(field) -> None:
field.place = None
field.place_error = ""
if field.data:
field.place = db.get_place_by_code(field.data)
if field.place is None:
field.place_error = "Zadané místo nenalezeno."
def get_place(field) -> Optional[db.Place]:
""" Kešuje výsledek v field.place"""
if not field.place_loaded:
field.place_loaded = True
field.load_place()
return field.place
def pre_validate(field, form):
if field.get_place() is None and field.place_error:
raise wtforms.ValidationError(field.place_error)
def get_place_id(field) -> int:
p = field.get_place()
if p is None:
return 0
return p.place_id
def populate_obj(field, obj, name):
setattr(obj, name, field.get_place_id())
def process_data(field, obj: Optional[int]):
if obj is not None:
field.data = db.get_place_by_id(obj).get_code()
else:
field.data = ""
class School(Place):
def __init__(self, label="Škola", validators=None, **kwargs):
super().__init__(label, validators, **kwargs)
def load_place(field) -> None:
field.place = None
if field.data:
try:
field.place = mo.users.validate_and_find_school(field.data)
except mo.CheckError as e:
field.place_error = str(e)
......@@ -23,6 +23,7 @@ from mo.rights import Right, ContestRights
import mo.util
from mo.util_format import inflect_number, inflect_by_number
from mo.web import app
import mo.web.fields as mo_fields
import mo.web.util
from mo.web.util import MODecimalField, PagerForm
from mo.web.table import CellCheckbox, Table, Row, Column, cell_pion_link, cell_place_link, cell_email_link, cell_email_link_flags
......@@ -1631,3 +1632,58 @@ def org_contest_edit(id: int):
contest=contest,
form=form,
)
class ParticipantAddForm(FlaskForm):
email = mo_fields.Email(validators=[validators.Required()])
first_name = mo_fields.FirstName(validators=[validators.Required()])
last_name = mo_fields.LastName(validators=[validators.Required()])
school = mo_fields.School(validators=[validators.Required()])
grade = mo_fields.Grade(validators=[validators.Required()])
birth_year = mo_fields.BirthYear(validators=[validators.Required()])
participation_place = mo_fields.Place("Kód soutěžního místa")
save = wtforms.SubmitField("Přidat")
def set_descriptions(self, contest: db.Contest):
self.school.description = f'Kód školy najdete v <a href="{url_for("org_place", id=contest.place.place_id)}">katalogu míst</a>.'
self.participation_place.description = f'Pokud účastník soutěží někde jinde než {contest.place.name_locative()}, vyplňte <a href="{url_for("org_place", id=contest.place.place_id)}">kód místa</a>. Dozor na tomto místě pak může za účastníka odevzdávat řešení.'
@app.route('/org/contest/c/<int:id>/ucastnici/pridat', methods=('GET', 'POST'))
@app.route('/org/contest/c/<int:id>/site/<int:site_id>/ucastnici/pridat', methods=('GET', 'POST'))
def org_contest_add_user(id: int, site_id: Optional[int] = None):
contest, master_contest, site, rr = get_contest_site_rr(id, site_id, right_needed=Right.manage_contest)
form = ParticipantAddForm()
if site_id is not None:
if not form.is_submitted():
form.participation_place.process_data(site_id)
form.participation_place.render_kw = {"readonly": True}
form.set_descriptions(master_contest)
if form.validate_on_submit():
try:
user, is_new_user = mo.users.find_or_create_user(form.email.data, form.first_name.data, form.last_name.data, False, reason='web')
participant, is_new_participant = mo.users.find_or_create_participant(user, contest.round.year, form.school.get_place_id(), form.birth_year.data, form.grade.data, reason='web')
participation, is_new_participation = mo.users.find_or_create_participation(user, contest, form.participation_place.get_place(), reason='web')
except mo.CheckError as e:
db.get_session().rollback()
flash(f"{e}", "danger")
else:
db.get_session().commit()
if is_new_user:
flash("Založen nový uživatel.", "info")
if is_new_participant:
flash("Založena nová registrace do ročníku.", "info")
if is_new_participation:
flash("Uživatel přihlášen do soutěže.", "info")
else:
flash("Žádná změna. Uživatel už byl přihlášen.", "info")
return redirect(url_for('org_contest_list', id=id, site_id=site_id))
return render_template(
'org_contest_add_user.html',
contest=master_contest, site=site,
form=form
)
......@@ -18,6 +18,7 @@ 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.util import PagerForm
......@@ -513,7 +514,7 @@ def org_user_edit(id: int):
sess.commit()
flash('Změny uživatele uloženy', 'success')
else:
flash(u'Žádné změny k uložení', 'info')
flash('Žádné změny k uložení', 'info')
return redirect(url_for('org_user', id=id))
......@@ -585,3 +586,47 @@ def org_user_new():
if not is_duplicate_name:
del form.allow_duplicate_name
return render_template('org_user_new.html', form=form, is_org=is_org)
class ParticipantEditForm(FlaskForm):
school = mo_fields.School("Škola", validators=[Required()], render_kw={'autofocus': True})
grade = mo_fields.Grade("Třída", validators=[Required()])
birth_year = mo_fields.BirthYear("Rok narození", validators=[Required()])
submit = wtforms.SubmitField("Uložit")
@app.route('/org/user/<int:user_id>/participant/<int:year>/edit', methods=('GET', 'POST'))
def org_user_participant_edit(user_id: int, year: int):
sess = db.get_session()
user = mo.users.user_by_uid(user_id)
if not user:
raise werkzeug.exceptions.NotFound()
rr = g.gatekeeper.rights_generic()
if not rr.can_edit_user(user):
raise werkzeug.exceptions.Forbidden()
participant = sess.query(db.Participant).filter_by(user_id=user.user_id).filter_by(year=year).one_or_none()
if participant is None:
raise werkzeug.exceptions.NotFound()
form = ParticipantEditForm(obj=participant)
if form.validate_on_submit():
form.populate_obj(participant)
if sess.is_modified(participant):
changes = db.get_object_changes(participant)
app.logger.info(f"Participant id {id} year {year} modified, changes: {changes}")
mo.util.log(
type=db.LogType.participant,
what=user_id,
details={'action': 'edit-participant', 'year': year, 'changes': changes},
)
sess.commit()
flash('Změny registrace uloženy', 'success')
else:
flash('Žádné změny k uložení', 'info')
return redirect(url_for('org_user', id=user_id))
return render_template('org_user_participant_edit.html', user=user, year=year, form=form)
......@@ -49,6 +49,9 @@
{% if state != RoundState.preparing %}
<a class="btn btn-primary" href='{{ url_for('org_contest_solutions', id=contest.contest_id, site_id=site_id) }}'>Odevzdaná řešení</a>
{% endif %}
{% if can_manage and site %}
<a class="btn btn-default" href="{{ url_for('org_contest_add_user', id=contest.contest_id, site_id=site_id) }}">Přidat účastníka</a>
{% endif %}
{% if not site %}
{% if state in [RoundState.grading, RoundState.closed] %}
<a class="btn btn-primary" href='{{ url_for('org_score', contest_id=contest.contest_id) }}'>Výsledky</a>
......@@ -73,25 +76,37 @@
{% if places_counts %}
<table class=data>
<thead>
<tr><th>Místo<th>Počet účastníků
<tr><th>Místo<th>Počet účastníků<th>Akce
</thead>
{% for (place, count) in places_counts %}
<tr>
<td><a href="{{ url_for('org_contest', id=contest.contest_id, site_id=place.place_id) }}">{{ place.name }}</a>
<td>{{ count }}
<td><div class="btn-group">
<a class="btn btn-xs btn-primary" href="{{ url_for('org_contest', id=contest.contest_id, site_id=place.place_id) }}">Detail</a>
{% if can_manage %}
<a class="btn btn-xs btn-default" href="{{ url_for('org_contest_add_user', id=contest.contest_id, site_id=place.place_id) }}">Přidat účastníka</a>
</div>
{% endif %}
</tr>
{% endfor %}
<tfoot>
<tr>
<th>Celkem
<th>{{ places_counts|sum(attribute=1) }}
<th>
</tr>
</tfoot>
</table>
{% else %}
<i>Žádní účastníci a žádná soutěžní místa.</i>
<p><i>Žádní účastníci a žádná soutěžní místa.</i></p>
{% endif %}
{% endif %}
<div class="btn-group">
{% if can_manage and not site %}
<a class="btn btn-default" href='{{ url_for('org_contest_add_user', id=contest.contest_id) }}'>Přidat účastníka</a>
{% endif %}
</div>
<h3>Úlohy</h3>
{% if tasks %}
......@@ -134,7 +149,7 @@
{% endfor %}
</table>
{% else %}
<p>Zatím nebyly přidány žádné úlohy.</p>
<p><i>Zatím nebyly přidány žádné úlohy.</i></p>
{% endif %}
<!--
......
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% set round = contest.round %}
{% block title %}
{{ round.round_code() }}: Přidat účastníka {% if site %}do soutěžního místa {{ site.name }}{% else %}do oblasti {{ contest.place.name }}{% endif %}
{% endblock %}
{% block breadcrumbs %}
{{ contest_breadcrumbs(round=round, contest=contest, site=site, action="Přidat účastníka") }}
{% endblock %}
{% block body %}
{% if errs %}
{% endif %}
{{ wtf.quick_form(form, form_type='simple', button_map={'save': 'primary'}) }}
{% endblock %}
......@@ -43,7 +43,7 @@
<table class="data full">
<thead>
<tr>
<th>Ročník<th>Škola<th>Třída<th>Rok narození
<th>Ročník<th>Škola<th>Třída<th>Rok narození<th>Akce
</tr>
</thead>
{% for participant in participants %}
......@@ -52,6 +52,7 @@
<td><a href="{{ url_for('org_place', id=participant.school) }}">{{ participant.school_place.name }}</a>
<td>{{ participant.grade }}
<td>{{ participant.birth_year }}
<td><a class="btn btn-xs btn-primary" href="{{ url_for('org_user_participant_edit', user_id=user.user_id, year=participant.year) }}">Editovat</a>
</tr>
{% endfor %}
</table>
......
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Editace registrace soutěžícího {{ user.full_name() }} v {{ year }}. ročníku{% endblock %}
{% block body %}
{{ wtf.quick_form(form, form_type='horizontal', button_map={'submit': 'primary'}) }}
{% endblock %}
......@@ -4,7 +4,10 @@
{{ table.to_html() }}
<a class="btn btn-primary pull-right"
{% if contest %}
<a class="btn btn-primary" href="{{ url_for('org_contest_add_user', id=contest.contest_id, site_id=site.place_id if site else None) }}">Přidat účastníka</a>
{% endif %}
<a class="btn btn-default"
title="Zobrazí emailové adresy ve snadno zkopírovatelném formátu"
href="{{ url_for('org_contest_list_emails', id=id, site_id=site_id, **request.args) if contest else url_for('org_round_list_emails', id=id, **request.args) }}">
Vypsat e-mailové adresy
......