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
......@@ -24,7 +24,7 @@
<form method=POST class='btn-group' onsubmit='return confirm("Poslat účastníkovi e-mail s odkazem na vytvoření hesla?");'>
{{ resend_invite_form.csrf_token }}
<button class="btn btn-default" type='submit' name='resend_invite' value='yes'>
{% if user.last_login_at %}Resetovat heslo{% else %}Znovu poslat zvací e-mail{% endif %}
Znovu poslat zvací e-mail
</button>
</form>
{% endif %}
......
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Nastavení nového hesla{% endblock %}
{% block body %}
<form method="POST" class="form form-horizontal" action="">
{{ form.csrf_token }}
{{ form.token() }}
{{ wtf.form_field(form.email, form_type='horizontal') }}
{{ wtf.form_field(form.passwd, form_type='horizontal') }}
<div class="btn-group col-lg-offset-2">
{{ wtf.form_field(form.submit, class="btn btn-primary") }}
{{ wtf.form_field(form.cancel) }}
</div>
</form>
{% endblock %}
{% extends "base.html" %}
{% block title %}Uživatel {{ g.user.full_name() }}{% endblock %}
{% block title %}Uživatel {{ user.full_name() }}{% endblock %}
{% block body %}
<h3>Osobní údaje</h3>
<table class=table>
<tr><th>Jméno<td>{{ user.first_name }}
<tr><th>Příjmení<td>{{ user.last_name }}
<tr><th>E-mail<td>{{ user.email }}
{% if pant %}
<tr><th>Škola<td>{{ pant.school_place.name }}
<tr><th>Ročník<td>{{ pant.grade }}
<tr><th>Rok narození<td>{{ pant.birth_year }}
{% endif %}
</table>
<p><a class='btn btn-primary' href='{{ url_for('user_settings_change') }}'>Změnit e-mail nebo heslo</a>
<p>Pokud potřebujete změnit jiné údaje, ozvěte se svému učiteli nebo garantovi.
Neuspějete-li u nich, napište správci OSMO (kontakt viz patička stránky).
{% if user.is_admin or user.is_org %}
<h3>Práva</h3>
{% if g.user.is_admin %}
{% if user.is_admin %}
<p>Správce systému
{% endif %}
{% if g.user.is_org %}
{% if user.is_org %}
<p>Organizátor s následujícími rolemi:
<table class=data>
<table class=table>
<tr>
<th>Role
<th>Oblast
......@@ -25,8 +45,7 @@
{% endfor %}
</table>
{% endif %}
{% if not g.user.is_admin and not g.user.is_org %}
<p>Běžný uživatel
{% endif %}
{% endblock %}
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Změna osobních údajů{% endblock %}
{% block body %}
{{ wtf.quick_form(form, form_type='horizontal', button_map={'submit': 'primary'}) }}
{% endblock %}
......@@ -29,7 +29,18 @@
{% endif %}
{% endif %}
{% if state == RoundState.preparing %}
{% if part_state == PartState.registered %}
<p>
Vaše přihláška do této soutěže <b>dosud nebyla potvrzena organizátory.</b>
Vyčkejte prosím.
{% set state = RoundState.preparing %}
</p>
{% elif part_state == PartState.refused %}
<p>
Vaše přihláška do této soutěže <b>byla odmítnuta organizátory.</b>
{% set state = RoundState.preparing %}
</p>
{% elif state == RoundState.preparing %}
<p>
Soutěžní kolo se <b>připravuje</b>{% if round.ct_tasks_start and round.ct_tasks_start > g.now %},
začne <b>{{ round.ct_tasks_start|time_and_timedelta }}</b>{% endif %}.
......@@ -64,12 +75,12 @@ Pokud si s tvorbou PDF nevíte rady, zkuste se podívat do <a href='https://docs
<p>Soutěž se nachází v neznámém stavu. To by se nemělo stát :)
{% endif %}
{% if state != RoundState.preparing %}
{% if contest.ct_task_statement_available() %}
<p>Můžete si stáhnout <a href='{{ url_for('user_task_statement', id=contest.contest_id) }}'>zadání úloh</a>.
{% endif %}
{% if state != RoundState.preparing %}
<h3>Úlohy</h3>
<table class="table table-bordered table-hover">
......
......@@ -42,4 +42,6 @@
<p>Momentálně se neúčastníte žádného kola MO.
{% endif %}
<p><a class="btn btn-primary" href="{{ url_for('user_join') }}">Přihlásit se do další kategorie</a>
{% endblock %}
{% extends "base.html" %}
{% block title %}Přihláška do MO{% endblock %}
{% block body %}
{% if available_rounds %}
<p>Zde si můžete vybrat, do kterých kategorií olympíády se přihlásíte.
<table class="table">
<thead>
<tr>
<th>Kategorie
<th>Popis
<th>Kolo
<th>Termíny
<th>Odkazy
<tbody>
{% for round in available_rounds %}
<tr>
<td><b>{{ round.category }}</b>
<td>{{ round.enroll_advert }}</b>
<td>{{ round.name }}
<td>{{ round.format_times() }}
{% if round.round_id in pcrs_by_round_id %}
<td>Již přihlášen
{% else %}
<td><a href='{{ url_for('user_join_round', round_id=round.round_id) }}' class='btn btn-xs btn-primary'>Přihlásit se</a>
{% endif %}
{% endfor %}
</table>
{% else %}
<p>V tomto školním roce zatím nejsou otevřené žádné kategorie olympiády.
Zkuste to prosím později.
{% endif %}
{% if pcrs_by_round_id %}
{# Není-li účastník přihlášen v žádné soutěží, user_index přesměrovává zpět na tuto stránku. #}
<p><a class="btn btn-default" href="{{ url_for('user_index') }}">Zpět</a>
{% endif %}
{% endblock %}
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block head %}
<script src='{{ asset_url('js/osmo.js') }}'></script>
<script>
osmo_api_root = {{ url_for('api_root')|jsescape }};
osmo_school = new OSMOSchool();
{% if form.town_query.data %}
osmo_school.prefill_town_query = {{ form.town_query.data|jsescape }};
{% if form.town_list.data %}
osmo_school.prefill_town_list = {{ form.town_list.data|jsescape }};
{% if form.school.data %}
osmo_school.prefill_school = {{ form.school.data|jsescape }};
{% endif %}
{% endif %}
{% endif %}
document.addEventListener('DOMContentLoaded', () => { osmo_school.init() });
</script>
{% endblock %}
{% block title %}Přihláška{% endblock %}
{% block body %}
<h3 style='margin-bottom: 21px'>{{ round.name }} kategorie {{ round.category }} {% if round.enroll_advert %} ({{ round.enroll_advert }}){% endif %}</h3>
<form action="" method="POST" role="form" class="form form-horizontal">
{{ form.csrf_token }}
{% if form.school %}
<div id='town_query-group' class='form-group row required{% if form.school.errors %} has-error{% endif %}'>
<label class='col-sm-2 control-label' for='town_query'>Škola</label>
<div class='col-sm-8'>
<input autofocus="" class="form-control" id="town_query" name="town_query" type="text" value="">
<div id='town_query-help' class='help-block'>
Zadejte prvních pár znaků jména obce a zmáčkněte Hledat.
Pokud se vám nedaří školu najít (například proto, že studujete v zahraničí),
informujte prosím <a href='{{ url_for('doc_about') }}'>správce OSMO</a>.
</div>
</div>
<div class='col-sm-2'>
<button class='btn btn-primary' type='button' onclick='osmo_school.find_town(false)'>
Hledat
</button>
</div>
</div>
<div id='town_list-div' class='form-group row' style='display: none'>
<div class='col-sm-offset-2 col-sm-10'>
<select id='town_list' name='town_list' onchange='osmo_school.town_picked()'>
</select>
</div>
</div>
<div id='school-div' name='school' class='form-group row' style='display: none'>
<div class='col-sm-offset-2 col-sm-10'>
<select id='school' name='school'>
</select>
</div>
</div>
{{ wtf.form_field(form.grade, form_type='horizontal', horizontal_columns=('sm', 2, 10)) }}
{{ wtf.form_field(form.birth_year, form_type='horizontal', horizontal_columns=('sm', 2, 10)) }}
<p>Přihlášením do soutěže udělujete souhlas se <a href='{{ url_for('doc_gdpr') }}'>zpracováním osobních údajů</a>.
U nezletilých účastníků musí přihlášku odeslat zákonný zástupce.
{% else %}
<p>Vaše osobní údaje už známe z ostatních kategorií. Stačí tedy potvrdit přihlášení.
{% endif %}
<div class='form-group'>
<div class='col-sm-12'>
{{ wtf.form_field(form.submit, form_type='inline', button_map={'submit': 'primary'}) }}
<a class="btn btn-default" href="{{ url_for('user_join') }}">Zpět</a>
</div>
</div>
</form>
{% endblock %}
from flask import render_template, jsonify, g, redirect, url_for, flash
from flask import render_template, jsonify, g, redirect, url_for, flash, request
from flask_wtf import FlaskForm
import flask_wtf.file
from sqlalchemy import and_
......@@ -6,24 +6,28 @@ from sqlalchemy.orm import joinedload
from typing import List, Tuple
import werkzeug.exceptions
import wtforms
from wtforms.validators import Required
import mo
import mo.config as config
import mo.email
import mo.db as db
import mo.submit
import mo.util
from mo.util import logger
from mo.util_format import time_and_timedelta
from mo.web import app
import mo.web.fields as mo_fields
import mo.web.org_round
import mo.web.util
@app.route('/user/')
def user_index():
pcrs = load_pcrs()
if getattr(config, 'AUTO_REGISTER_TEST', False) and not any(round.category == 'T' for pion, contest, round in pcrs):
if register_to_test():
pcrs = load_pcrs()
if not pcrs:
return redirect(url_for('user_join'))
return render_template(
'user_index.html',
......@@ -39,41 +43,212 @@ def load_pcrs() -> List[Tuple[db.Participation, db.Contest, db.Round]]:
.filter(db.Participation.user == g.user)
.filter(db.Round.year == mo.current_year)
.options(joinedload(db.Contest.place))
.order_by(db.Round.year.desc(), db.Round.category, db.Round.seq, db.Round.part)
.order_by(db.Round.category, db.Round.seq, db.Round.part)
.all())
@app.route('/user/join/')
def user_join():
available_rounds: List[db.Round] = (
db.get_session().query(db.Round)
.select_from(db.Round)
.filter_by(year=mo.current_year)
.filter(db.Round.enroll_mode.in_([db.RoundEnrollMode.register, db.RoundEnrollMode.confirm]))
.filter_by(state=db.RoundState.running)
.order_by(db.Round.category, db.Round.seq)
.all())
available_rounds = [r for r in available_rounds if not r.is_subround()]
pcrs = load_pcrs()
pcrs_by_round_id = {pcr[1].round_id: pcr for pcr in pcrs}
return render_template(
'user_join_list.html',
available_rounds=available_rounds,
pcrs_by_round_id=pcrs_by_round_id,
)
class JoinRoundForm(FlaskForm):
# Zadávání školy je JS hack implementovaný v šabloně. Fields definují jen rozhraní.
school = mo_fields.School("Škola", validators=[Required()])
town_query = wtforms.HiddenField()
town_list = wtforms.HiddenField()
grade = mo_fields.Grade("Třída", validators=[Required()])
birth_year = mo_fields.BirthYear("Rok narození", validators=[Required()])
submit = wtforms.SubmitField('Přihlásit se')
def register_to_test() -> bool:
@app.route('/user/join/<int:round_id>/', methods=('GET', 'POST'))
def user_join_round(round_id):
sess = db.get_session()
round = sess.query(db.Round).filter_by(year=mo.current_year, category='T', seq=1, part=0).one_or_none()
round = sess.query(db.Round).get(round_id)
if not round:
app.logger.error(f'Nemohu najít kolo {mo.current_year}-T-1')
return False
raise werkzeug.exceptions.NotFound()
if round.level != 0:
app.logger.error(f'Kolo {round.round_code_short()} není na celostátní úrovni')
return False
if (round.year != mo.current_year
or round.part > 1
or round.enroll_mode not in [db.RoundEnrollMode.register, db.RoundEnrollMode.confirm]
or round.state != db.RoundState.running):
flash('Do této kategorie se není možné přihlásit.', 'danger')
return redirect(url_for('user_register'))
pion = (sess.query(db.Participation)
.select_from(db.Participation)
.filter_by(user=g.user)
.join(db.Participation.contest)
.filter(db.Contest.round == round)
.with_for_update()
.one_or_none())
if pion:
flash('Do této kategorie už jste přihlášen.', 'info')
return redirect(url_for('user_join'))
pant = (sess.query(db.Participant)
.filter_by(user=g.user, year=round.year)
.with_for_update()
.one_or_none())
form = JoinRoundForm()
if pant:
del form.school
del form.grade
del form.birth_year
if form.validate_on_submit():
if form.submit.data:
if not pant:
pant = join_create_pant(form)
sess.add(pant)
contest = join_create_contest(round, pant)
join_create_pion(contest)
sess.commit()
join_notify(contest)
msg = 'Přihláška přijata.'
if round.enroll_mode == db.RoundEnrollMode.confirm:
msg += ' Ještě ji musí potvrdit organizátor soutěže.'
flash(msg, 'success')
return redirect(url_for('user_index'))
elif not pant and request.method == 'GET':
# Pokusíme se předvyplnit data z minulých ročníků
prev_pant = (sess.query(db.Participant)
.filter_by(user=g.user)
.options(joinedload(db.Participant.school_place, db.Place.parent_place))
.order_by(db.Participant.year.desc())
.limit(1).one_or_none())
if prev_pant:
form.school.data = f'#{prev_pant.school}'
town = prev_pant.school_place.parent_place
form.town_query.data = town.name
form.town_list.data = str(town.place_id)
form.birth_year.data = prev_pant.birth_year
return render_template(
'user_join_round.html',
round=round,
form=form,
)
def join_create_pant(form: JoinRoundForm) -> db.Participant:
assert form.school.place is not None
pant = db.Participant(user=g.user,
year=mo.current_year,
school_place=form.school.place,
grade=form.grade.data,
birth_year=form.birth_year.data)
logger.info(f'Join: Účastník #{g.user.user_id} se přihlásil do {pant.year}. ročníku')
mo.util.log(
type=db.LogType.participant,
what=g.user.user_id,
details={'action': 'create-participant', 'reason': 'user-join', 'new': db.row2dict(pant)},
)
return pant
contest = sess.query(db.Contest).filter_by(round=round).limit(1).one_or_none()
if not contest:
app.logger.error(f'Kolo {round.round_code_short()} nemá soutěž')
return False
pion = db.Participation(user=g.user, contest=contest, place=contest.place, state=db.PartState.registered)
sess.add(pion)
def join_create_contest(round: db.Round, pant: db.Participant) -> db.Contest:
sess = db.get_session()
place = pant.school_place
if place.level != round.level:
parents = db.get_place_parents(pant.school_place)
places = [p for p in parents if p.level == round.level]
assert len(places) == 1
place = places[0]
# XXX: Z rekurzivního dotazu nedostaneme plnohodnotný db.Place, ale jenom named tuple, tak musíme pracovat s ID.
place_id = place.place_id
assert round.part <= 1
c = (sess.query(db.Contest)
.filter_by(round=round, place_id=place_id)
.with_for_update()
.one_or_none())
if not c:
c = db.Contest(
round=round,
place_id=place_id,
state=db.RoundState.running,
)
sess.add(c)
sess.flush()
c.master = c
logger.info(f'Join: Automaticky založena soutěž #{c.contest_id} {round.round_code()} pro místo #{place_id}')
mo.util.log(
type=db.LogType.contest,
what=c.contest_id,
details={'action': 'created', 'reason': 'user-join'},
)
mo.web.org_round.create_subcontests(round, c)
return c
def join_create_pion(c: db.Contest) -> None:
sess = db.get_session()
if c.round.enroll_mode == db.RoundEnrollMode.register:
state = db.PartState.active
else:
state = db.PartState.registered
p = db.Participation(user=g.user, contest=c, place=c.place, state=state)
sess.add(p)
logger.info(f'Join: Účastník #{g.user.user_id} přihlášen do soutěže #{c.contest_id}')
mo.util.log(
type=db.LogType.participant,
what=g.user.user_id,
details={'action': 'add-to-contest', 'new': db.row2dict(pion)},
details={'action': 'add-to-contest', 'reason': 'user-join', 'new': db.row2dict(p)},
)
sess.commit()
app.logger.info(f'Účastník #{g.user.user_id} automaticky registrován do soutěže #{contest.contest_id}')
return True
def join_notify(c: db.Contest) -> None:
sess = db.get_session()
r = c.round
place = c.place
while place is not None:
uroles = (sess.query(db.UserRole)
.filter(db.UserRole.role.in_((db.RoleType.garant, db.RoleType.garant_kraj, db.RoleType.garant_okres, db.RoleType.garant_skola)))
.filter_by(place_id=place.place_id)
.options(joinedload(db.UserRole.user))
.all())
notify = [ur.user for ur in uroles if ur.applies_to(at=place, year=r.year, cat=r.category, seq=r.seq)]
if notify:
for org in notify:
logger.info(f'Join: Notifikuji orga <{org.email}> pro místo {place.get_code()}')
mo.email.send_join_notify_email(org, g.user, c)
return
place = place.parent_place
def get_contest(id: int) -> db.Contest:
logger.warn('Join: Není komu poslat mail')
def get_contest_pion(id: int, require_reg: bool = True) -> Tuple[db.Contest, db.Participation]:
contest = (db.get_session().query(db.Contest)
.options(joinedload(db.Contest.place),
joinedload(db.Contest.round))
......@@ -82,13 +257,20 @@ def get_contest(id: int) -> db.Contest:
if not contest:
raise werkzeug.exceptions.NotFound()
# FIXME: Kontrolovat nějak pion.state?
pion = (db.get_session().query(db.Participation)
.filter_by(user=g.user, contest_id=contest.master_contest_id)
.one_or_none())
if not pion:
raise werkzeug.exceptions.Forbidden()
if require_reg and pion.state in [db.PartState.registered, db.PartState.refused]:
raise werkzeug.exceptions.Forbidden()
return contest, pion
def get_contest(id: int) -> db.Contest:
contest, _ = get_contest_pion(id)
return contest
......@@ -105,7 +287,7 @@ def get_task(contest: db.Contest, id: int) -> db.Task:
@app.route('/user/contest/<int:id>/')
def user_contest(id: int):
sess = db.get_session()
contest = get_contest(id)
contest, pion = get_contest_pion(id, require_reg=False)
messages = sess.query(db.Message).filter_by(round_id=contest.round_id).order_by(db.Message.created_at).all()
......@@ -121,6 +303,7 @@ def user_contest(id: int):
return render_template(
'user_contest.html',
contest=contest,
part_state=pion.state,
task_sols=task_sols,
messages=messages,
max_submit_size=config.MAX_CONTENT_LENGTH,
......
/*
* JavaScriptové funkce pro OSMO
*/
'use strict';
let osmo_api_root = undefined;
/*** Výběr škol ***/
class OSMOSchool {
constructor() {
this.prefill_town_query = undefined;
this.prefill_town_list = undefined;
this.prefill_school = undefined;
}
find_town_error(msg) {
document.getElementById('town_query-group').classList.add('has-error');
document.getElementById('town_query-help').innerText = msg;
}
async do_find_town(query) {
let resp = undefined;
try {
resp = await fetch(osmo_api_root + 'find-town?q=' + encodeURIComponent(query));
} catch (err) {
console.log('OSMO: Search failed: ' + err);
this.find_town_error('Spojení se serverem selhalo.');
return;
}
if (resp.status !== 200) {
console.log('OSMO: Search status: ' + resp.status);
this.find_town_error('Spojení se serverem selhalo.');
return;
}
const ans = await resp.json();
if (ans.error !== undefined) {
this.find_town_error(ans.error);
return;
}
const list = document.getElementById('town_list');
const opts = list.options;
opts.length = 0;
opts.add(new Option('Vyberte obec ze seznamu', ""));
for (const t of ans.found) {
opts.add(new Option(t[1], t[0]));
}
document.getElementById('town_list-div').style.display = 'block';
document.getElementById('town_query-help').innerText = ans.msg
if (this.prefill_town_list !== undefined) {
list.value = this.prefill_town_list;
this.prefill_town_list = undefined;
this.town_picked();
} else if (ans.found.length == 1) {
list.selectedIndex = 1;
this.town_picked();
}
}
find_town(during_init) {
const query = document.getElementById('town_query').value;
document.getElementById('town_list-div').style.display = 'none';
document.getElementById('school-div').style.display = 'none';
document.getElementById('town_query-help').innerText = 'Hledám...';
if (!during_init) {
document.getElementById('town_query-group').classList.remove('has-error');
}
this.do_find_town(query);
}
town_keydown(event) {
if (event.key === 'Enter') {
event.preventDefault();
this.find_town(false);
}
}
async do_get_schools(town_id) {
let resp = undefined;
try {
resp = await fetch(osmo_api_root + 'get-schools?town=' + encodeURIComponent(town_id));
} catch (err) {
console.log('OSMO: Search failed: ' + err);
return;
}
if (resp.status !== 200) {
console.log('OSMO: Search status: ' + resp.status);
return;
}
const ans = await resp.json();
const list = document.getElementById('school');
const opts = list.options;
list.replaceChildren();
if (ans.zs.length > 0 || ans.ss.length > 0) {
opts.add(new Option('Vyberte školu ze seznamu', ""));
if (ans.zs.length > 0) {
const g = document.createElement('optgroup');
g.label = 'Základní školy'
for (const s of ans.zs) {
g.append(new Option(s.name, '#' + s.id));
}
opts.add(g);
}
if (ans.ss.length > 0) {
const g = document.createElement('optgroup');
g.label = 'Střední školy'
for (const s of ans.ss) {
g.append(new Option(s.name, '#' + s.id));
}
opts.add(g);
}
} else {
opts.add(new Option('V této obci nejsou žádné školy.', ""));
}
document.getElementById('school-div').style.display = 'block';
if (this.prefill_school !== undefined) {
list.value = this.prefill_school;
this.prefill_school = undefined;
}
}
town_picked() {
const town_id = document.getElementById('town_list').value;
document.getElementById('school-div').style.display = 'none';
if (town_id != "") {
this.do_get_schools(town_id);
}
}
init() {
console.log('OSMO: Init schools');
var tq = document.getElementById('town_query');
tq.addEventListener('keydown', (event) => { this.town_keydown(event); });
if (this.prefill_town_query !== undefined) {
tq.value = this.prefill_town_query;
this.prefill_town_query = undefined;
this.find_town(true);
}
}
}