diff --git a/mo/db.py b/mo/db.py index bb45a9d863246107868e7c43d61741717dc66936..c1de5014635fe58d8c326daed91d22dcad52c7cc 100644 --- a/mo/db.py +++ b/mo/db.py @@ -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 == "": diff --git a/mo/imports.py b/mo/imports.py index 51aa689b4e9a77adbbfc85124a688f0e0e552507..dfd5e5213f8729333d7cb8d3e40db38f56972866 100644 --- a/mo/imports.py +++ b/mo/imports.py @@ -16,6 +16,8 @@ import mo.util from mo.util import logger from mo.util_format import format_decimal +reason = "import" + class ImportType(db.MOEnum): participants = auto() @@ -149,21 +151,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 +171,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=reason) + 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 +214,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=reason) + 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=reason) + 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): diff --git a/mo/users.py b/mo/users.py index 503276570d83c2962f82a7b67b8b28be1192d716..bb9c43b4b19158f29ab1761437cd03dc8389c7e7 100644 --- a/mo/users.py +++ b/mo/users.py @@ -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 = "undef-reason") -> 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 = "undef-reason") -> 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 = "undef-reason") -> 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áč') diff --git a/mo/web/fields.py b/mo/web/fields.py new file mode 100644 index 0000000000000000000000000000000000000000..e6f21da098528f367c161d9421dc366ee78b8135 --- /dev/null +++ b/mo/web/fields.py @@ -0,0 +1,140 @@ +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) diff --git a/mo/web/org_contest.py b/mo/web/org_contest.py index 0e70af2bbfa2aff3366157d811603a3b3e1fb049..40e13cd3a12d6723507cec963c679f96f9930d9e 100644 --- a/mo/web/org_contest.py +++ b/mo/web/org_contest.py @@ -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,60 @@ 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) + + reason = "form-add-participation" + + 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=reason) + 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=reason) + participation, is_new_participation = mo.users.find_or_create_participation(user, contest, form.participation_place.get_place(), reason=reason) + 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 + ) diff --git a/mo/web/org_users.py b/mo/web/org_users.py index db44ea01379bbdde91ecfeb5d973cfb09d7975a9..e885537a033c07024aed303dd034d15903535b95 100644 --- a/mo/web/org_users.py +++ b/mo/web/org_users.py @@ -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) diff --git a/mo/web/templates/org_contest.html b/mo/web/templates/org_contest.html index 36eb3b62772f8e11c69e1632c2796e9c2bfc361b..f78ea9c2fbaaf0635885c1abef6ba9f3a06d6c0a 100644 --- a/mo/web/templates/org_contest.html +++ b/mo/web/templates/org_contest.html @@ -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 %} <!-- diff --git a/mo/web/templates/org_contest_add_user.html b/mo/web/templates/org_contest_add_user.html new file mode 100644 index 0000000000000000000000000000000000000000..297d6ce7b40fb6c007f9b8fe18a7d587ec67e6c7 --- /dev/null +++ b/mo/web/templates/org_contest_add_user.html @@ -0,0 +1,19 @@ +{% 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 %} diff --git a/mo/web/templates/org_user.html b/mo/web/templates/org_user.html index e76644b20ea3f1dd6eb8292f942428556b15c25b..966d4b94591f593b6f012aca424ba3a5ddaabc6b 100644 --- a/mo/web/templates/org_user.html +++ b/mo/web/templates/org_user.html @@ -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> diff --git a/mo/web/templates/org_user_participant_edit.html b/mo/web/templates/org_user_participant_edit.html new file mode 100644 index 0000000000000000000000000000000000000000..a4245de62fb650d49beda17948a7fc6f5af16c23 --- /dev/null +++ b/mo/web/templates/org_user_participant_edit.html @@ -0,0 +1,8 @@ +{% 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 %} diff --git a/mo/web/templates/parts/org_participants_table_actions.html b/mo/web/templates/parts/org_participants_table_actions.html index 24eb85aa361049b900d3a1672d8637673e50cfda..0eec0d02a253fa76728b4d2623192de5c44acd3e 100644 --- a/mo/web/templates/parts/org_participants_table_actions.html +++ b/mo/web/templates/parts/org_participants_table_actions.html @@ -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