Skip to content
Snippets Groups Projects

Compare revisions

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

Source

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

Target

Select target project
  • mj/mo-submit
1 result
Select Git revision
  • devel
  • fo
  • fo-base
  • honza/add-contestant
  • honza/kolo-vs-soutez
  • honza/mr6
  • honza/mr7
  • honza/mra
  • honza/mrd
  • honza/mrf
  • honza/submit-images
  • jh-stress-test-wip
  • jirka/typing
  • jk/issue-196
  • jk/issue-96
  • master
  • mj/submit-images
  • shorten-schools
18 results
Show changes
Showing
with 176 additions and 64 deletions
......@@ -90,7 +90,7 @@ class Place(wtforms.StringField):
place_loaded: bool = False
place: Optional[db.Place] = None
place_error: str
place_error: str = ""
def load_place(field) -> None:
field.place = None
......@@ -133,8 +133,30 @@ class School(Place):
def load_place(field) -> None:
field.place = None
field.place_error = ""
if field.data:
try:
field.place = mo.users.validate_and_find_school(field.data)
except mo.CheckError as e:
field.place_error = str(e)
class NewPassword(wtforms.PasswordField):
def __init__(self, label="Nové heslo", validators=None, **kwargs):
super().__init__(label, validators, **kwargs)
def pre_validate(field, form):
if field.data:
if not mo.users.validate_password(field.data):
raise wtforms.ValidationError(mo.users.password_help)
class RepeatPassword(wtforms.PasswordField):
"""Pro validaci hledá ve formuláři form.new_passwd a s ním porovnává."""
def __init__(self, label="Zopakujte heslo", validators=None, **kwargs):
super().__init__(label, validators, **kwargs)
def pre_validate(field, form):
if field.data != form.new_passwd.data:
raise wtforms.ValidationError('Hesla se neshodují.')
......@@ -24,6 +24,7 @@ app.jinja_env.trim_blocks = True
# Filtry definované v mo.util_format
app.jinja_env.filters.update(timeformat=util_format.timeformat)
app.jinja_env.filters.update(timeformat_short=util_format.timeformat_short)
app.jinja_env.filters.update(inflected=util_format.inflect_number)
app.jinja_env.filters.update(inflected_by=util_format.inflect_by_number)
app.jinja_env.filters.update(timedelta=util_format.timedelta)
......@@ -93,6 +94,11 @@ def yes_no(a: bool) -> str:
return "ano" if a else "ne"
@app.template_filter()
def jsescape(js: Any) -> str:
return Markup(json_pretty(js))
@app.template_filter()
def json_pretty(js: Any) -> str:
return json.dumps(js, sort_keys=True, indent=4, ensure_ascii=False)
......
......@@ -43,7 +43,8 @@ def get_menu():
name += " [admin]"
items.append(MenuItem(url_for('user_settings'), name, classes=["right"]))
else:
items.append(MenuItem(url_for('login'), "Přihlásit se", active_prefix="/auth/", classes=["right"]))
items.append(MenuItem(url_for('create_acct'), "Založit účet", classes=["right"]))
items.append(MenuItem(url_for('login'), "Přihlásit se", active_prefix="/acct/", classes=["right"]))
active = None
for item in items:
......
......@@ -78,8 +78,8 @@ school_export_columns = (
)
@app.route('/org/export/skoly')
def org_export_skoly():
@app.route('/org/export/schools')
def org_export_schools():
sess = db.get_session()
format = request.args.get('format', 'en_csv')
......
......@@ -442,10 +442,11 @@ def org_contest_import(id: int):
)
@app.route('/org/contest/c/<int:id>/ucastnici', methods=('GET', 'POST'))
@app.route('/org/contest/c/<int:id>/site/<int:site_id>/ucastnici', methods=('GET', 'POST'))
@app.route('/org/contest/c/<int:id>/ucastnici/emails', endpoint="org_contest_list_emails")
@app.route('/org/contest/c/<int:id>/site/<int:site_id>/ucastnici/emails', endpoint="org_contest_list_emails")
# URL je explicitně uvedeno v mo.email.contestant_list_url
@app.route('/org/contest/c/<int:id>/participants', methods=('GET', 'POST'))
@app.route('/org/contest/c/<int:id>/site/<int:site_id>/participants', methods=('GET', 'POST'))
@app.route('/org/contest/c/<int:id>/participants/emails', endpoint="org_contest_list_emails")
@app.route('/org/contest/c/<int:id>/site/<int:site_id>/participants/emails', endpoint="org_contest_list_emails")
def org_contest_list(id: int, site_id: Optional[int] = None):
contest, master_contest, site, rr = get_contest_site_rr(id, site_id, Right.view_contestants)
can_edit = rr.have_right(Right.manage_contest) and request.endpoint != 'org_contest_list_emails'
......@@ -1497,7 +1498,7 @@ def org_contest_advance(contest_id: int):
prev_pion_query = (sess.query(db.Participation)
.filter(db.Participation.contest_id.in_([c.contest_id for c in prev_contests]))
.filter(db.Participation.state.in_((db.PartState.registered, db.PartState.invited, db.PartState.present))))
.filter_by(state=db.PartState.active))
prev_pions = prev_pion_query.all()
if form.boundary.data > 0:
......@@ -1540,7 +1541,7 @@ def org_contest_advance(contest_id: int):
user_id=pp.user_id,
contest_id=contest.contest_id,
place_id=contest.place.place_id,
state=db.PartState.invited,
state=db.PartState.active,
)
.on_conflict_do_nothing()
.returning(db.Participation.contest_id)
......@@ -1637,11 +1638,11 @@ def org_contest_edit(id: int):
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()])
first_name = mo_fields.FirstName(validators=[validators.Optional()])
last_name = mo_fields.LastName(validators=[validators.Optional()])
school = mo_fields.School(validators=[validators.Optional()])
grade = mo_fields.Grade(validators=[validators.Optional()])
birth_year = mo_fields.BirthYear(validators=[validators.Optional()])
participation_place = mo_fields.Place("Kód soutěžního místa")
save = wtforms.SubmitField("Přidat")
......@@ -1650,8 +1651,8 @@ class ParticipantAddForm(FlaskForm):
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'))
@app.route('/org/contest/c/<int:id>/participants/new', methods=('GET', 'POST'))
@app.route('/org/contest/c/<int:id>/site/<int:site_id>/participants/new', 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)
......@@ -1674,6 +1675,8 @@ def org_contest_add_user(id: int, site_id: Optional[int] = None):
db.get_session().commit()
if is_new_user:
flash("Založen nový uživatel.", "info")
token = mo.users.make_activation_token(user)
mo.email.send_new_account_email(user, token)
if is_new_participant:
flash("Založena nová registrace do ročníku.", "info")
if is_new_participation:
......
......@@ -55,7 +55,15 @@ def org_rounds():
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, db.Round.part)
return render_template('org_rounds.html', rounds=rounds)
return render_template('org_rounds.html', rounds=rounds, history=False)
@app.route('/org/contest/history')
def org_rounds_history():
sess = db.get_session()
rounds = sess.query(db.Round).order_by(db.Round.year.desc(), db.Round.category, db.Round.seq, db.Round.part)
return render_template('org_rounds.html', rounds=rounds, history=True)
class TaskDeleteForm(FlaskForm):
......@@ -144,14 +152,26 @@ def add_contest(round: db.Round, form: AddContestForm) -> bool:
)
app.logger.info(f"Soutěž #{contest.contest_id} založena: {db.row2dict(contest)}")
# Přidání soutěže do podkol ve skupině
subrounds = round.master.get_group_rounds()
create_subcontests(round.master, contest)
sess.commit()
flash(f'Založena soutěž {place.name_locative()}', 'success')
return True
# XXX: Používá se i v registraci účastníků
def create_subcontests(master_round: db.Round, master_contest: db.Contest):
if master_round.part == 0:
return
sess = db.get_session()
subrounds = master_round.get_group_rounds()
for subround in subrounds:
subcontest = db.Contest(
round_id=subround.round_id,
master_contest_id=contest.contest_id,
place_id=contest.place_id,
state=state,
master_contest_id=master_contest.contest_id,
place_id=master_contest.place_id,
state=master_contest.state,
)
sess.add(subcontest)
sess.flush()
......@@ -161,11 +181,7 @@ def add_contest(round: db.Round, form: AddContestForm) -> bool:
what=subcontest.contest_id,
details={'action': 'add', 'contest': db.row2dict(subcontest)},
)
app.logger.info(f"Soutěž #{subcontest.contest_id} založena: {db.row2dict(subcontest)}")
sess.commit()
flash(f'Založena soutěž {place.name_locative()}', 'success')
return True
app.logger.info(f"Podsoutěž #{subcontest.contest_id} založena: {db.row2dict(subcontest)}")
@app.route('/org/contest/r/<int:id>/', methods=('GET', 'POST'))
......@@ -446,6 +462,8 @@ class RoundEditForm(FlaskForm):
"Přesnost bodování", choices=db.round_points_step_choices, coerce=decimal.Decimal,
description="Ovlivňuje možnost zadávání nových bodů, již uložené body nezmění"
)
enroll_mode = wtforms.SelectField("Režim přihlašování", choices=db.RoundEnrollMode.choices(), coerce=db.RoundEnrollMode.coerce)
enroll_advert = wtforms.StringField("Popis v přihlášce")
has_messages = wtforms.BooleanField("Zprávičky pro účastníky (aktivuje možnost vytvářet novinky zobrazované účastníkům)")
submit = wtforms.SubmitField('Uložit')
......@@ -483,6 +501,8 @@ def org_round_edit(id: int):
del form.score_winner_limit
del form.score_successful_limit
del form.points_step
# ani nastavení přihlašování
del form.enroll_mode
if form.validate_on_submit():
form.populate_obj(round)
......
......@@ -14,6 +14,7 @@ from wtforms.validators import Required
import mo
import mo.db as db
import mo.email
from mo.rights import Right
import mo.util
import mo.users
......@@ -316,14 +317,15 @@ class ResendInviteForm(FlaskForm):
resend_invite = SubmitField()
def do(self, user: db.User):
token = mo.users.ask_reset_password(user)
if user.last_login_at is None:
token = mo.users.make_activation_token(user)
db.get_session().commit()
if user.last_login_at is None and mo.util.send_new_account_email(user, token):
flash('Uvítací e-mail s odkazem pro nastavení hesla odeslán na {}'.format(user.email), 'success')
elif mo.util.send_password_reset_email(user, token):
flash('E-mail s odkazem pro resetování hesla odeslán na {}'.format(user.email), 'success')
if mo.email.send_new_account_email(user, token):
flash('Uvítací e-mail s odkazem na aktivaci účtu odeslán na {}.'.format(user.email), 'success')
else:
flash('Problém při odesílání e-mailu s odkazem pro nastavení hesla', 'danger')
flash('Problém při odesílání e-mailu s odkazem na aktivaci účtu.', 'danger')
else:
flash('Tento uživatel už má účet aktivovaný.', 'danger')
@app.route('/org/org/<int:id>/', methods=('GET', 'POST'))
......@@ -339,7 +341,7 @@ def org_org(id: int):
can_assign_rights = rr.have_right(Right.assign_rights)
resend_invite_form: Optional[ResendInviteForm] = None
if rr.can_edit_user(user):
if user.last_login_at is None and rr.can_edit_user(user):
resend_invite_form = ResendInviteForm()
if resend_invite_form.resend_invite.data and resend_invite_form.validate_on_submit():
resend_invite_form.do(user)
......@@ -425,7 +427,7 @@ def org_user(id: int):
rr = g.gatekeeper.rights_generic()
resend_invite_form: Optional[ResendInviteForm] = None
if rr.can_edit_user(user):
if user.last_login_at is None and rr.can_edit_user(user):
resend_invite_form = ResendInviteForm()
if resend_invite_form.resend_invite.data and resend_invite_form.validate_on_submit():
resend_invite_form.do(user)
......@@ -567,17 +569,14 @@ def org_user_new():
details={'action': 'new', 'user': db.row2dict(new_user)},
)
token = mo.users.make_activation_token(new_user)
sess.commit()
flash('Nový uživatel vytvořen', 'success')
# Send password (re)set link
token = mo.users.ask_reset_password(new_user)
db.get_session().commit()
if mo.util.send_new_account_email(new_user, token):
flash('E-mail s odkazem pro nastavení hesla odeslán na {}'.format(new_user.email), 'success')
if mo.email.send_new_account_email(new_user, token):
flash('E-mail s odkazem na aktivaci účtu odeslán na {}.'.format(new_user.email), 'success')
else:
flash('Problém při odesílání e-mailu s odkazem pro nastavení hesla', 'danger')
flash('Problém při odesílání e-mailu s odkazem na aktivaci účtu.', 'danger')
if is_org:
return redirect(url_for('org_org', id=new_user.user_id))
......
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Aktivace nového účtu{% endblock %}
{% block body %}
{{ wtf.quick_form(form, form_type='horizontal', button_map={'submit': 'primary'}) }}
{% endblock %}
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Změna e-mailu{% endblock %}
{% block body %}
{{ wtf.quick_form(form, form_type='horizontal', button_map={'submit': 'primary'}) }}
{% endblock %}
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Založení účtu{% endblock %}
{% block body %}
<p>Nejprve vyplňte svou e-mailovou adresu, která také bude sloužit jako přihlašovací jméno.
Na ni vám pošleme ověřovací e-mail.
{{ wtf.quick_form(form, form_type='horizontal', button_map={'submit': 'primary'}) }}
{% endblock %}
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Založení účtu{% endblock %}
{% block body %}
{% if form %}
<p>S údaji o účtu budeme zacházet v souladu se <a href='{{ url_for('doc_gdpr') }}'>zásadami
zpracování osobních údajů</a>.
{{ wtf.quick_form(form, form_type='horizontal', button_map={'submit': 'primary'}) }}
{% else %}
<p>Počkejte prosím, až vám přijde e-mail a klikněte na odkaz v něm uvedený.
{% endif %}
{% endblock %}
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Nastavení nového hesla{% endblock %}
{% block body %}
{{ wtf.quick_form(form, form_type='horizontal', button_map={'submit': 'primary'}) }}
{{ wtf.quick_form(cancel_form, form_type='horizontal') }}
{% endblock %}
......@@ -12,6 +12,8 @@
<p>z <a href='https://www.mff.cuni.cz/'>Matematicko-fyzikální fakulty</a> <a href='https://www.cuni.cz/'>Univerzity Karlovy</a> v Praze.
MFF UK také děkujeme za poskytnutí serveru, kde systém běží.
<h3>Správce systému</h3>
<p>Veškeré připomínky k chodu systému a nápady na další rozvoj
prosím posílejte e-mailem na {{ config.MAIL_CONTACT|mailto }}.
......
......@@ -20,16 +20,16 @@ když přidáte vlastní sloupce s novými názvy, budou se ignorovat.
<h2>Import účastníků</h2>
<p>Definovány jsou tyto sloupce (tučné jsou povinné):
<p>Definovány jsou tyto sloupce (tučné jsou povinné, kurzívou jsou povinné pro zatím nezaregistrované účty):
<table class=data>
<tr><th>Název<th>Obsah
<tr><td><b>email</b><td>E-mailová adresa
<tr><td><b>krestni</b><td>Křestní jméno
<tr><td><b>prijmeni</b><td>Příjmení
<tr><td><b>kod_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><i>krestni</i><td>Křestní jméno
<tr><td><i>prijmeni</i><td>Příjmení
<tr><td><i>kod_skoly</i><td>Kód školy (viz katalog škol na tomto webu)
<tr><td><i>rocnik</i><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><i>rok_naroz</i><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í.
......
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Login{% endblock %}
{% block title %}Přihlášení uživatele{% endblock %}
{% block body %}
<form method="POST" class="form form-horizontal" action="{{ url_for('login') }}">
......@@ -11,6 +11,7 @@
<div class="btn-group col-lg-offset-2">
{{ wtf.form_field(form.submit, class="btn btn-primary") }}
{{ wtf.form_field(form.reset) }}
<a class='btn btn-default' href='{{ url_for('create_acct') }}'>Založit nový účet</a>
</div>
</form>
......
{% extends "base.html" %}
{% block title %}Vítejte{% endblock %}
{% block body %}
<p>Na tomto webu je možné odevzdávat řešení úloh Matematické olympiády.
Momentálně je dostupný pouze uživatelům, kteří získali účet při postupu
do dalšího kola MO.
<p>Pokud účet máte, tak se prosím přihlašte.
<p>Na odevzdávání potřebujete účet. Pokud už ho máte, tak se prosím přihlašte.
V opačném případě si účet založte.
<p>Pokud jste postoupili, ale účet dosud nemáte, zkontrolujte prosím
svou složku na nevyžádanou poštu, zda pozvánka neuvízla tam. Nepomůže-li
to, obraťte se prosím na svého učitele matematiky nebo na správce
tohoto systému (odkaz viz patička).
<p><a class='btn btn-primary' href='{{ url_for('login') }}'>Přihlásit se</a>
<a class='btn btn-primary' href='{{ url_for('create_acct') }}'>Založit nový účet</a>
{% endblock %}
......@@ -11,8 +11,7 @@
{% block body %}
{% if errs %}
{% endif %}
<p>Jméno, škola, ročník a rok narození nejsou povinné pro již registrované účty.</p>
{{ wtf.quick_form(form, form_type='simple', button_map={'save': 'primary'}) }}
......
......@@ -41,9 +41,9 @@
<li><a href='{{ url_for('doc_garant') }}'>Návod pro garanty</a> (může se hodit i ostatním organizátorům)
<li><a href='{{ url_for('static', filename='doc/import-navod.pdf') }}'>Podrobnější návod k importům</a> (PDF)
<li>Export všech škol:
<a href='{{ url_for('org_export_skoly', format='en_csv') }}'>CSV s čárkami</a>,
<a href='{{ url_for('org_export_skoly', format='cs_csv') }}'>CSV se středníky</a>,
<a href='{{ url_for('org_export_skoly', format='tsv') }}'>TSV</a>
<a href='{{ url_for('org_export_schools', format='en_csv') }}'>CSV s čárkami</a>,
<a href='{{ url_for('org_export_schools', format='cs_csv') }}'>CSV se středníky</a>,
<a href='{{ url_for('org_export_schools', format='tsv') }}'>TSV</a>
<li><a href='https://docs.google.com/document/d/1XXk7Od-ZKtfmfNa-9FpFjUqmy0Ekzf2-2q3EpSWyn1w/edit?usp=sharing'>Návod na tvorbu PDF</a>
</ul>
......
......@@ -34,6 +34,8 @@
{% with state=round.ct_state() %}
<tr><td>Stav pro účastníky<td class='rstate-{{state.name}}'>{{ state.friendly_name() }}
{% endwith %}
<tr><td>Režim přihlašování<td>{{ round.enroll_mode.friendly_name() }}
<tr><td>Popis v přihlášce<td>{{ round.enroll_advert }}
</table>
<table class=data style="float: left;">
<thead>
......
......@@ -23,4 +23,10 @@
<td class='rstate-{{r.state.name}}'>{{ r.state.friendly_name() }}
{% endfor %}
</table>
{% if history %}
<p><a class='btn btn-default' href='{{ url_for('org_rounds') }}'>Aktuální ročník</a>
{% else %}
<p><a class='btn btn-default' href='{{ url_for('org_rounds_history') }}'>Všechny ročníky</a>
{% endif %}
{% endblock %}