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 1375 additions and 1125 deletions
from flask import request
from flask.json import jsonify
from sqlalchemy import func
from sqlalchemy.orm import joinedload
import werkzeug.exceptions
import mo.db as db
from mo.util_format import inflect_with_number
from mo.web import app
@app.route('/api/')
def api_root():
"""Slouží jako prefix pro konstrukci URL v JavaScriptu."""
raise werkzeug.exceptions.NotFound()
@app.route('/api/find-town')
def api_find_town():
query = request.args.get('q')
if query is None or len(query) < 2:
return jsonify(error='Zadejte alespoň 2 znaky jména obce.')
elif '%' in query:
return jsonify(error='Nepovolené znaky ve jménu obce.')
else:
max_places = 50
places = (db.get_session().query(db.Place)
.filter_by(level=3)
.filter(func.lower(db.f_unaccent(db.Place.name)).like(func.lower(db.f_unaccent(query + '%'))))
.options(joinedload(db.Place.parent_place))
.order_by(db.Place.name, db.Place.place_id)
.limit(max_places)
.all())
if not places:
return jsonify(error='Nenalezena žádná obec.')
# XXX: Nemůže se stát, že nastane přesná shoda a k tomu příliš mnoho nepřesných?
if len(places) >= max_places:
return jsonify(error='Nalezeno příliš mnoho obcí. Zadejte prosím více znaků jména.')
res = []
for p in places:
name = p.name
if p.name != p.parent_place.name:
name += f' (okres {p.parent_place.name})'
res.append([p.place_id, name])
msg = inflect_with_number(len(res), 'Nalezena %s obec.', 'Nalezeny %s obce.', 'Nalezeno %s obcí.')
return jsonify(found=res, msg=msg)
@app.route('/api/get-schools')
def api_get_schools():
town = request.args.get('town')
if town is None or not town.isnumeric():
raise werkzeug.exceptions.BadRequest()
town_id = int(town)
places = (db.get_session().query(db.Place)
.filter_by(level=4, type=db.PlaceType.school, parent=town_id)
.options(joinedload(db.Place.school))
.order_by(db.Place.name)
.all())
zs = []
ss = []
for p in places:
s = {
'id': p.place_id,
'name': p.name,
}
if p.school.is_zs:
zs.append(s)
if p.school.is_ss:
ss.append(s)
return jsonify(zs=zs, ss=ss)
import datetime
from flask import render_template, request, g, redirect, url_for, session
from flask.helpers import flash
from flask_wtf import FlaskForm
import werkzeug.exceptions
import wtforms
from wtforms.fields.html5 import EmailField
import wtforms.validators as validators
from sqlalchemy.orm import joinedload
from typing import Optional
import mo.util
import mo.db as db
import mo.rights
import mo.users
from mo.web import app, NeedLoginError
class LoginForm(FlaskForm):
next = wtforms.HiddenField()
email = EmailField('E-mail', validators=[validators.DataRequired()])
passwd = wtforms.PasswordField('Heslo')
submit = wtforms.SubmitField('Přihlásit se')
reset = wtforms.SubmitField('Zapomenuté heslo')
def login_and_redirect(user: db.User, url: Optional[str] = None):
session.clear()
session['uid'] = user.user_id
if not url:
if user.is_admin or user.is_org:
url = url_for('org_index')
else:
url = url_for('index')
else:
url = request.script_root + url
return redirect(url)
@app.route('/auth/login', methods=('GET', 'POST'))
def login():
form = LoginForm(email=request.args.get('email'))
if not form.validate_on_submit():
return render_template('login.html', form=form, error=None)
email = form.email.data
user = mo.users.user_by_email(email)
if not user:
app.logger.error('Login: Neznámý uživatel <%s>', email)
flash('Neznámý uživatel', 'danger')
elif form.reset.data:
app.logger.info('Login: Požadavek na reset hesla pro <%s>', email)
min_time_between_resets = datetime.timedelta(minutes=1)
now = datetime.datetime.now().astimezone()
if (user.reset_at is not None
and now - user.reset_at < min_time_between_resets):
flash('Poslední požadavek na obnovení hesla byl odeslán příliš nedávno', 'danger')
else:
token = mo.users.ask_reset_password(user)
db.get_session().commit()
mo.util.send_password_reset_email(user, token)
flash('Na uvedenou adresu byl odeslán e-mail s odkazem na obnovu hesla', 'success')
elif not form.passwd.data or not mo.users.check_password(user, form.passwd.data):
app.logger.error('Login: Špatné heslo pro uživatele <%s>', email)
flash('Chybné heslo', 'danger')
else:
if user.is_admin:
typ = ' (admin)'
elif user.is_org:
typ = ' (org)'
elif user.is_test:
typ = ' (test)'
else:
typ = ""
app.logger.info('Login: Přihlásil se uživatel #%s <%s>%s', user.user_id, email, typ)
mo.users.login(user)
db.get_session().commit()
return login_and_redirect(user, url=form.next.data)
return render_template('login.html', form=form)
@app.route('/auth/logout', methods=('POST',))
def logout():
session.clear()
return redirect(url_for('index'))
@app.route('/auth/incarnate/<int:id>', methods=('POST',))
def incarnate(id):
if not g.user.is_admin:
raise werkzeug.exceptions.Forbidden()
new_user = db.get_session().query(db.User).get(id)
if not new_user:
raise werkzeug.exceptions.NotFound()
app.logger.info('Login: Uživatel #%s se převtělil na #%s', g.user.user_id, new_user.user_id)
return login_and_redirect(new_user)
@app.route('/user/settings')
def user_settings():
sess = db.get_session()
roles = []
if g.user:
roles = (sess.query(db.UserRole)
.filter_by(user_id=g.user.user_id)
.options(joinedload(db.UserRole.place))
.all())
return render_template('settings.html', roles=roles, roles_by_type=mo.rights.roles_by_type)
@app.errorhandler(NeedLoginError)
def handle_need_login(e):
form = LoginForm()
form.next.data = request.path
return render_template('login.html', form=form), e.code
class ResetForm(FlaskForm):
email = EmailField('E-mail', description='Účet pro který se nastavuje nové heslo', render_kw={"disabled": "disabled"})
token = wtforms.HiddenField()
passwd = wtforms.PasswordField('Nové heslo', description='Heslo musí mít alespoň 8 znaků. Doporučujeme kombinovat velká a malá písmena a číslice.')
submit = wtforms.SubmitField('Nastavit heslo')
cancel = wtforms.SubmitField('Zrušit obnovu hesla')
@app.route('/auth/reset', methods=('GET', 'POST'))
def reset():
token = request.args.get('token')
if not token:
flash('Žádný token pro resetování hesla', 'danger')
return redirect(url_for('login'))
user = mo.users.check_reset_password(token)
if not user:
flash('Neplatný požadavek na obnovu hesla', 'danger')
return redirect(url_for('login'))
form = ResetForm(token=token, email=user.email)
ok = form.validate_on_submit()
if not ok:
return render_template('reset.html', form=form)
if form.cancel.data:
mo.users.cancel_reset_password(user)
app.logger.info('Login: Zrušen reset hesla pro uživatele <%s>', user.email)
db.get_session().commit()
flash('Obnova hesla zrušena', 'warning')
return redirect(url_for('login'))
elif len(form.passwd.data) < 8:
flash('Heslo musí být aspoň 8 znaků dlouhé', 'danger')
return render_template('reset.html', form=form)
else:
mo.users.do_reset_password(user)
mo.users.set_password(user, form.passwd.data)
app.logger.info('Login: Reset hesla pro uživatele <%s>', user.email)
mo.util.log(
type=db.LogType.user,
what=user.user_id,
details={'action': 'reset-passwd'},
)
mo.users.login(user)
app.logger.info('Login: Přihlásil se uživatel <%s> po resetování hesla', user.email)
db.get_session().commit()
flash('Nastavení nového hesla a přihlášení do systému proběhlo úspěšně', 'success')
return login_and_redirect(user)
@app.errorhandler(werkzeug.exceptions.Forbidden)
def handle_forbidden(e):
return render_template('forbidden.html')
import decimal
from typing import Optional
import wtforms
from wtforms.fields.html5 import EmailField
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 Decimal(wtforms.DecimalField):
"""Upravený DecimalField, který formátuje číslo podle jeho skutečného počtu
desetinných míst a zadané `places` používá jen jako maximální počet desetinných míst."""
def _value(self):
if self.data is not None:
# Spočítání počtu desetinných míst, zbytek necháme na původní implementaci
max_places = self.places
self.places = 0
d = decimal.Decimal(1)
while self.data % d != 0 and self.places < max_places:
self.places += 1
d /= 10
return super(Decimal, self)._value()
class IntList(wtforms.StringField):
list = None
def __init__(self, label="", validators=None, **kwargs):
super().__init__(label, validators, **kwargs)
def pre_validate(field, form):
field.list = None
if field.data:
try:
field.list = mo.util.parse_int_list(field.data)
except mo.CheckError as e:
raise wtforms.ValidationError(str(e))
class Points(Decimal):
def __init__(self, label="Body", validators=None, **kwargs):
super().__init__(label, validators, **kwargs)
class Email(EmailField):
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)
self.render_kw = {"placeholder": "Kód"}
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
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í.')
class DateTime(wtforms.DateTimeField):
def __init__(self, label, format='%Y-%m-%d %H:%M', description='Ve formátu 2000-01-01 12:34', **kwargs):
super().__init__(label, format=format, description=description, **kwargs)
def process_data(self, valuelist):
super().process_data(valuelist)
if self.data is not None:
self.data = self.data.astimezone()
def process_formdata(self, valuelist):
super().process_formdata(valuelist)
if self.data is not None:
self.data = self.data.astimezone()
......@@ -9,9 +9,9 @@ import urllib.parse
import mo.config as config
import mo.db as db
from mo.rights import Right
import mo.util_format as util_format
from mo.web import app
from mo.web.org_contest import contest_breadcrumbs
from mo.web.org_place import place_breadcrumbs
from mo.web.util import user_html_flags
......@@ -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)
......@@ -46,10 +47,10 @@ app.jinja_env.globals.update(JobState=db.JobState)
# Další typy:
app.jinja_env.globals.update(Markup=Markup)
app.jinja_env.globals.update(Right=Right)
# Vlastní pomocné funkce
app.jinja_env.globals.update(contest_breadcrumbs=contest_breadcrumbs)
app.jinja_env.globals.update(place_breadcrumbs=place_breadcrumbs)
# Funkce asset_url se přidává v mo.ext.assets
......@@ -59,6 +60,7 @@ def user_link(u: db.User) -> Markup:
return Markup('<a href="{url}">{name}{test}</a>').format(url=user_url(u), name=u.full_name(), test=" (test)" if u.is_test else "")
@app.template_filter()
def user_url(u: db.User) -> str:
if u.is_admin or u.is_org:
return url_for('org_org', id=u.user_id)
......@@ -68,7 +70,7 @@ def user_url(u: db.User) -> str:
@app.template_filter()
def pion_link(u: db.User, contest_id: int) -> Markup:
url = url_for('org_contest_user', contest_id=contest_id, user_id=u.user_id)
url = url_for('org_contest_user', ct_id=contest_id, user_id=u.user_id)
return Markup('<a href="{url}">{name}{test}</a>').format(url=url, name=u.full_name(), test=" (test)" if u.is_test else "")
......@@ -93,6 +95,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:
......
from dataclasses import dataclass, field
from flask import render_template, redirect, url_for, request, flash, g
from sqlalchemy import and_, or_
from sqlalchemy.orm import aliased, joinedload
from typing import List, Set, Dict
from typing import List, Set, Optional
import mo.config as config
import mo.db as db
import mo.rights
import mo.users
......@@ -11,6 +13,15 @@ from mo.web.jinja import user_url
from mo.web.table import Table, Row, Column
@dataclass
class OrgOverview:
round: db.Round
place: db.Place
contest: Optional[db.Contest]
role_set: Set[db.RoleType] = field(default_factory=set)
role_list: List[db.RoleType] = field(default_factory=list)
@app.route('/org/')
def org_index():
if 'place' in request.args:
......@@ -32,36 +43,33 @@ def org_index():
flash('ID uživatele musí být číslo', 'danger')
sess = db.get_session()
ctr = (sess.query(db.Contest, db.UserRole)
.select_from(db.UserRole, db.Round, db.Contest)
.filter(and_(db.UserRole.user_id == g.user.user_id,
rcu = (sess.query(db.Round, db.Contest, db.UserRole)
.select_from(db.UserRole)
.join(db.Place)
.join(db.Round, and_(db.UserRole.user_id == g.user.user_id,
or_(db.UserRole.category == None, db.UserRole.category == db.Round.category),
or_(db.UserRole.year == None, db.UserRole.year == db.Round.year),
or_(db.UserRole.seq == None, db.UserRole.seq == db.Round.seq),
db.Round.year == mo.current_year,
db.Contest.round_id == db.Round.round_id,
db.Contest.place_id == db.UserRole.place_id))
.options(joinedload(db.Contest.place))
db.Place.level <= db.Round.level))
.outerjoin(db.Contest, and_(db.Contest.round_id == db.Round.round_id, db.Contest.place_id == db.UserRole.place_id))
.filter(db.Round.year == config.CURRENT_YEAR)
.options(joinedload(db.UserRole.place))
.order_by(db.Round.level, db.Round.category, db.Round.seq, db.Round.part,
db.Contest.place_id, db.Contest.contest_id)
.all())
# Pokud máme pro jednu soutěž více rolí, zkombinujeme je
contests: List[db.Contest] = []
contest_role_sets: Dict[db.Contest, Set[db.RoleType]] = {}
for ct, ur in ctr:
if len(contests) == 0 or contests[-1] != ct:
contests.append(ct)
contest_role_sets[ct.contest_id] = set()
contest_role_sets[ct.contest_id].add(ur.role)
overview: List[OrgOverview] = []
for r, ct, ur in rcu:
o = overview[-1] if overview else None
if not (o and o.round == r and o.place == ur.place):
o = OrgOverview(round=r, place=ur.place, contest=ct)
overview.append(o)
o.role_set.add(ur.role)
# Role pro každou soutěž setřídíme podle důležitosti
contest_roles: Dict[db.Contest, List[db.RoleType]] = {
ct_id: sorted(list(contest_role_sets[ct_id]), key=lambda r: mo.rights.role_order_by_type[r])
for ct_id in contest_role_sets.keys()
}
for o in overview:
o.role_list = sorted(o.role_set, key=lambda r: mo.rights.role_order_by_type[r])
return render_template('org_index.html', contests=contests, contest_roles=contest_roles, role_type_names=db.role_type_names)
return render_template('org_index.html', overview=overview, role_type_names=db.role_type_names)
school_export_columns = (
......@@ -78,8 +86,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')
......
from dataclasses import dataclass
from flask import render_template, g, redirect, url_for, flash, request
from flask_wtf import FlaskForm
import flask_wtf.file
......@@ -8,10 +7,13 @@ from sqlalchemy import func, and_, select
from sqlalchemy.orm import joinedload, aliased
from sqlalchemy.orm.query import Query
from sqlalchemy.dialects.postgresql import insert as pgsql_insert
from typing import Any, List, Tuple, Optional, Sequence, Dict
import sqlalchemy.sql.schema
from typing import Any, List, Tuple, Optional, Dict
import urllib.parse
import werkzeug.exceptions
import wtforms
import wtforms.validators as validators
from wtforms.widgets.html5 import NumberInput
import mo
from mo.csv import FileFormat
......@@ -19,38 +21,196 @@ import mo.config as config
import mo.db as db
from mo.imports import ImportType, create_import
import mo.jobs.submit
from mo.rights import Right, ContestRights
from mo.rights import Right, RoundRights
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
import wtforms.validators as validators
from wtforms.widgets.html5 import NumberInput
from mo.web.util import PagerForm
from mo.web.table import CellCheckbox, Table, Row, Column, cell_pion_link, cell_place_link, cell_email_link_flags
class ImportForm(FlaskForm):
file = flask_wtf.file.FileField("Soubor", render_kw={'autofocus': True})
typ = wtforms.SelectField(
"Typ dat",
choices=[(x.name, x.friendly_name()) for x in (ImportType.participants, ImportType.proctors, ImportType.judges)],
coerce=ImportType.coerce,
default=ImportType.participants,
)
fmt = wtforms.SelectField(
"Formát souboru",
choices=FileFormat.choices(), coerce=FileFormat.coerce,
default=FileFormat.cs_csv,
class Context:
# Kolo máme vždy
round: db.Round
master_round: db.Round
# Vypočtená práva: Může být implementováno i jako ContestRights
rights: RoundRights
# Můžeme mít vybranou soutěž a místo
# Pro nedělená kola platí contest == master_contest.
# Operace s účastníky by měly probíhat vždy přes master_contest.
contest: Optional[db.Contest] = None
master_contest: Optional[db.Contest] = None
site: Optional[db.Place] = None
# Můžeme se omezit na soutěže v dané oblasti
hier_place: Optional[db.Place] = None
# Účastník a úloha
pion: Optional[db.Participation] = None
user: Optional[db.User] = None
task: Optional[db.Task] = None
# IDčka, jak je předáváme do URL
round_id: Optional[int] = None
ct_id: Optional[int] = None
site_id: Optional[int] = None
hier_id: Optional[int] = None
user_id: Optional[int] = None
task_id: Optional[int] = None
def url_for(self, endpoint: str, **kwargs):
a = {}
round_id = kwargs.get('round_id', self.round_id)
ct_id = kwargs.get('ct_id', self.ct_id)
if ct_id is not None:
a['ct_id'] = ct_id
else:
assert round_id is not None
a['round_id'] = round_id
for arg in ('site_id', 'hier_id', 'user_id', 'task_id'):
val = getattr(self, arg)
if val is not None:
a[arg] = val
for arg, val in kwargs.items():
a[arg] = val
return url_for(endpoint, **a)
def url_home(self):
if self.ct_id:
return url_for('org_contest', ct_id=self.ct_id)
else:
return url_for('org_round', round_id=self.round_id, hier_id=self.hier_id)
def breadcrumbs(self, table: bool = False, action: Optional[str] = None) -> Markup:
elements = [(url_for('org_rounds'), 'Soutěže')]
elements.append((url_for('org_round', round_id=self.round_id), self.round.round_code()))
if self.hier_place:
parents = g.gatekeeper.get_ancestors(self.hier_place)
for p in parents[1:]:
elements.append((url_for('org_round', round_id=self.round_id, hier_id=p.place_id), p.name or '???'))
if self.contest:
if self.round.level >= 2:
parents = g.gatekeeper.get_ancestors(self.contest.place)
for i in range(1, len(parents) - 1):
p = parents[i]
if p.level >= 3:
break
elements.append((url_for('org_round', round_id=self.round_id, hier_id=p.place_id), db.Place.get_code(p)))
elements.append((url_for('org_contest', ct_id=self.ct_id), self.contest.place.name or '???'))
if self.site:
elements.append((url_for('org_contest', ct_id=self.ct_id, site_id=self.site_id), f"soutěžní místo {self.site.name}"))
if self.task:
elements.append((
url_for('org_contest_task', ct_id=self.ct_id, site_id=self.site_id, task_id=self.task_id) if self.contest
else url_for('org_round_task_edit', round_id=self.round_id, task_id=self.task_id),
f"{self.task.code} {self.task.name}"
))
if self.user:
elements.append((url_for('org_contest_user', ct_id=self.ct_id, user_id=self.user_id), self.user.full_name()))
if table:
if self.contest:
elements.append((url_for('org_generic_list', ct_id=self.ct_id, site=self.site_id), "Seznam účastníků"))
else:
elements.append((url_for('org_generic_list', round_id=self.round_id), "Seznam účastníků"))
if action:
elements.append(('', action))
return Markup(
"\n".join([f"<li><a href='{url}'>{name}</a>" for url, name in elements[:-1]])
+ "<li>" + elements[-1][1]
)
submit = wtforms.SubmitField('Importovat')
get_template = wtforms.SubmitField('Stáhnout šablonu')
def get_context(round_id: Optional[int] = None,
ct_id: Optional[int] = None,
site_id: Optional[int] = None,
hier_id: Optional[int] = None,
user_id: Optional[int] = None,
task_id: Optional[int] = None,
right_needed: Optional[Right] = None,
) -> Context:
ctx = Context()
ctx.round_id = round_id
ctx.ct_id = ct_id
ctx.site_id = site_id
ctx.hier_id = hier_id
ctx.user_id = user_id
ctx.task_id = task_id
sess = db.get_session()
if site_id is not None:
assert ct_id is not None
ctx.site = db.get_session().query(db.Place).get(site_id)
if not ctx.site:
raise werkzeug.exceptions.NotFound()
if hier_id is not None:
assert ct_id is None
ctx.hier_place = db.get_session().query(db.Place).get(hier_id)
if not ctx.hier_place:
raise werkzeug.exceptions.NotFound()
if ct_id is not None:
ctx.contest = (db.get_session().query(db.Contest)
.options(joinedload(db.Contest.place),
joinedload(db.Contest.round),
joinedload(db.Contest.master).joinedload(db.Contest.round))
.get(ct_id))
if not ctx.contest:
raise werkzeug.exceptions.NotFound()
ctx.master_contest = ctx.contest.master
ctx.round, ctx.master_round = ctx.contest.round, ctx.master_contest.round
if round_id is not None and ctx.round.round_id != round_id:
raise werkzeug.exceptions.NotFound()
ctx.round_id = ctx.round.round_id
ctx.rights = g.gatekeeper.rights_for_contest(ctx.contest, ctx.site)
else:
ctx.round = sess.query(db.Round).options(joinedload(db.Round.master)).get(round_id)
if not ctx.round:
raise werkzeug.exceptions.NotFound()
if hier_id is not None and ctx.hier_place.level > ctx.round.level:
raise werkzeug.exceptions.NotFound()
ctx.master_round = ctx.round.master
ctx.rights = g.gatekeeper.rights_for_round(ctx.round, for_place=ctx.hier_place)
# Zkontrolujeme, zda se účastník opravdu účastní soutěže
if user_id is not None:
assert ctx.master_contest is not None
ctx.pion = (sess.query(db.Participation)
.filter_by(user_id=user_id, contest_id=ctx.master_contest.contest_id)
.options(joinedload(db.Participation.place),
joinedload(db.Participation.user))
.one_or_none())
if not ctx.pion:
raise werkzeug.exceptions.NotFound()
ctx.user = ctx.pion.user
# A zda soutěží na zadaném soutěžním místě, je-li určeno
if site_id is not None and site_id != ctx.pion.place_id:
raise werkzeug.exceptions.NotFound()
# Najdeme úlohu a ověříme, že je součástí kola
if task_id is not None:
ctx.task = sess.query(db.Task).get(task_id)
if not ctx.task or ctx.task.round != ctx.round:
raise werkzeug.exceptions.NotFound()
if not (right_needed is None or ctx.rights.have_right(right_needed)):
raise werkzeug.exceptions.Forbidden()
return ctx
class ParticipantsFilterForm(PagerForm):
school = wtforms.StringField("Škola")
participation_place = wtforms.StringField("Soutěžní místo", render_kw={'autofocus': True})
contest_place = wtforms.StringField("Soutěžní oblast", render_kw={'autofocus': True})
school = mo_fields.School()
participation_place = mo_fields.Place("Soutěžní místo", render_kw={'autofocus': True})
contest_place = mo_fields.Place("Soutěžní oblast", render_kw={'autofocus': True})
participation_state = wtforms.SelectField('Stav účasti', choices=[('*', '*')] + list(db.PartState.choices()), default='*')
# format = wtforms.RadioField(choices=[('', 'Zobrazit'), ('csv', 'Stáhnout vše v CSV'), ('tsv', 'Stáhnout vše v TSV')])
......@@ -58,32 +218,6 @@ class ParticipantsFilterForm(PagerForm):
download_csv = wtforms.SubmitField("↓ CSV")
download_tsv = wtforms.SubmitField("↓ TSV")
# Výstupní hodnoty filtru, None při nepoužitém filtru, prázdná db hodnota při
# nepovedené filtraci (neexistující místo a podobně)
f_school: Optional[db.Place] = None
f_participation_place: Optional[db.Place] = None
f_contest_place: Optional[db.Place] = None
f_participation_state: Optional[db.PartState] = None
def validate(self):
if self.school.data:
self.f_school = db.get_place_by_code(self.school.data)
if not self.f_school:
flash(f"Zadaná škola '{self.school.data}' neexistuje", "danger")
self.f_school = db.School()
if self.participation_place.data:
self.f_participation_place = db.get_place_by_code(self.participation_place.data)
if not self.f_participation_place:
flash(f"Zadané soutěžní místo '{self.participation_place.data}' neexistuje", "danger")
self.f_participation_place = db.Place()
if self.contest_place.data:
self.f_contest_place = db.get_place_by_code(self.contest_place.data)
if not self.f_contest_place:
flash(f"Zadaná soutěžní oblast '{self.contest_place.data}' neexistuje", "danger")
self.f_contest_place = db.Place()
self.f_participation_state = None if self.participation_state.data == '*' else self.participation_state.data
class ParticipantsActionForm(FlaskForm):
action_on = wtforms.RadioField(
......@@ -96,12 +230,12 @@ class ParticipantsActionForm(FlaskForm):
participation_state = wtforms.SelectField('Stav účasti', choices=db.PartState.choices(), coerce=db.PartState.coerce)
set_participation_state = wtforms.SubmitField("Nastavit stav účasti")
participation_place = wtforms.StringField(
participation_place = mo_fields.Place(
'Soutěžní místo', description='Zadejte kód místa'
)
set_participation_place = wtforms.SubmitField("Nastavit soutěžní místo")
contest_place = wtforms.StringField(
contest_place = mo_fields.Place(
'Soutěžní oblast',
description='Musí existovat soutěž v dané oblasti pro stejné kolo. Oblast zadejte pomocí kódu.'
)
......@@ -122,15 +256,9 @@ class ParticipantsActionForm(FlaskForm):
if self.set_participation_state.data:
pass
elif self.set_participation_place.data:
participation_place = db.get_place_by_code(self.participation_place.data)
if not participation_place:
flash('Nenalezeno místo s daným kódem', 'danger')
return False
participation_place = self.participation_place.place
elif self.set_contest.data:
contest_place = db.get_place_by_code(self.contest_place.data)
if not contest_place:
flash("Nepovedlo se najít "+round.get_level().name_accusative("zadaný", "zadanou", "zadané"), 'danger')
return False
contest_place = self.contest_place.place
# Contest hledáme vždy v master kole, abychom náhodou nepřesunuli účastníky do soutěže v podkole
contest = sess.query(db.Contest).filter_by(round_id=round.master_round_id, place_id=contest_place.place_id).one_or_none()
if not contest:
......@@ -152,20 +280,7 @@ class ParticipantsActionForm(FlaskForm):
flash('Data v checkboxech nelze převést na čísla, kontaktujte správce', 'danger')
return False
# Check all participations if we can edit them
ctants: List[Tuple[db.Participation, Any, Any]] = query.all()
for pion, _, _ in ctants:
u = pion.user
if self.action_on.data == 'checked' and u.user_id not in user_ids:
continue
rr = g.gatekeeper.rights_for_contest(pion.contest)
if not rr.have_right(Right.manage_contest):
flash(
f"Nemáte právo ke správě soutěže v kole {round.round_code_short()} {pion.contest.place.name_locative()} "
+ f"(účastník {u.full_name()}). Žádná akce nebyla provedena.", 'danger'
)
return False
count = 0
unchanged = 0
for pion, _, _ in ctants:
......@@ -186,6 +301,7 @@ class ParticipantsActionForm(FlaskForm):
if self.set_participation_state.data:
pion.state = self.participation_state.data
elif self.set_participation_place.data:
assert participation_place
pion.place_id = participation_place.place_id
elif self.set_contest.data:
pion.contest_id = contest.contest_id
......@@ -215,12 +331,14 @@ class ParticipantsActionForm(FlaskForm):
'success'
)
elif self.set_participation_place.data:
assert participation_place
flash(
f'Nastaveno soutěžní místo {participation_place.name} '
+ inflect_number(count, 'účastníkovi', 'účastníkům', 'účastníkům'),
'success'
)
elif self.set_contest.data:
assert contest_place
flash(
inflect_number(count, 'účastník přesunut', 'účastníci přesunuti', 'účastníků přesunuto')
+ f' do soutěže {contest_place.name_locative()}',
......@@ -236,109 +354,19 @@ class ParticipantsActionForm(FlaskForm):
return True
def get_contest(id: int) -> Tuple[db.Contest, db.Contest]:
"""Vrací contest a master_contest pro zadané contest_id.
Pro nedělená kola platí contest == master_contest.
Operace s účastníky by měly probíhat vždy přes master_contest."""
contest = (db.get_session().query(db.Contest)
.options(joinedload(db.Contest.place),
joinedload(db.Contest.round),
joinedload(db.Contest.master).joinedload(db.Contest.round))
.get(id))
if not contest:
raise werkzeug.exceptions.NotFound()
return contest, contest.master
def get_contest_rr(id: int, right_needed: Optional[Right] = None) -> Tuple[db.Contest, db.Contest, ContestRights]:
"""Vrací contest, master_contest a ContestRights objekt pro zadané contest_id.
Pro nedělená kola platí contest == master_contest.
Operace s účastníky by měly probíhat vždy přes master_contest."""
contest, master_contest = get_contest(id)
rr = g.gatekeeper.rights_for_contest(contest)
if not (right_needed is None or rr.have_right(right_needed)):
raise werkzeug.exceptions.Forbidden()
return contest, master_contest, rr
def get_contest_site_rr(id: int, site_id: Optional[int], right_needed: Optional[Right] = None) -> Tuple[db.Contest, db.Contest, Optional[db.Place], ContestRights]:
"""Vrací contest, master_contest, optional site a ContestRights objekt pro zadané contest_id a site_id.
Pro nedělená kola platí contest == master_contest.
Operace s účastníky by měly probíhat vždy přes master_contest."""
if site_id is None:
contest, master_contest, rr = get_contest_rr(id, right_needed)
return contest, master_contest, None, rr
contest, master_contest = get_contest(id)
site = db.get_session().query(db.Place).get(site_id)
if not site:
raise werkzeug.exceptions.NotFound()
rr = g.gatekeeper.rights_for_contest(contest, site)
if not (right_needed is None or rr.have_right(right_needed)):
raise werkzeug.exceptions.Forbidden()
return contest, master_contest, site, rr
def contest_breadcrumbs(
round: Optional[db.Round] = None, contest: Optional[db.Contest] = None,
site: Optional[db.Place] = None, task: Optional[db.Task] = None,
user: Optional[db.User] = None, action: Optional[str] = None,
table: Optional[bool] = False
) -> Markup:
elements = [(url_for('org_rounds'), 'Soutěže')]
round_id = None
if round:
round_id = round.round_id
elements.append((url_for('org_round', id=round_id), round.round_code()))
ct_id = None
if contest:
ct_id = contest.contest_id
elements.append((url_for('org_contest', id=ct_id), contest.place.name))
site_id = None
if site:
site_id = site.place_id
elements.append((url_for('org_contest', id=ct_id, site_id=site_id), f"soutěžní místo {site.name}"))
if task:
task_id = task.task_id
elements.append((
url_for('org_contest_task', contest_id=ct_id, site_id=site_id, task_id=task_id) if ct_id
else url_for('org_round_task_edit', id=round_id, task_id=task_id),
f"{task.code} {task.name}"
))
if user:
user_id = user.user_id
elements.append((url_for('org_contest_user', contest_id=ct_id, user_id=user_id), user.full_name()))
if table:
if contest:
elements.append((url_for('org_contest_list', id=ct_id, site=site_id), "Seznam účastníků"))
else:
elements.append((url_for('org_round_list', id=round_id), "Seznam účastníků"))
if action:
elements.append(('', action))
return Markup(
"\n".join([f"<li><a href='{url}'>{name}</a>" for url, name in elements[:-1]])
+ "<li>" + elements[-1][1]
)
@app.route('/org/contest/c/<int:id>')
@app.route('/org/contest/c/<int:id>/site/<int:site_id>/')
def org_contest(id: int, site_id: Optional[int] = None):
@app.route('/org/contest/c/<int:ct_id>/')
@app.route('/org/contest/c/<int:ct_id>/site/<int:site_id>/')
def org_contest(ct_id: int, site_id: Optional[int] = None):
sess = db.get_session()
contest, master_contest, site, rr = get_contest_site_rr(id, site_id, None)
round = contest.round
tasks_subq = sess.query(db.Task.task_id).filter_by(round=contest.round)
pions_subq = sess.query(db.Participation.user_id).filter_by(contest=master_contest)
if site:
pions_subq = pions_subq.filter_by(place=site)
ctx = get_context(ct_id=ct_id, site_id=site_id)
contest = ctx.contest
assert contest
rights = ctx.rights
tasks_subq = sess.query(db.Task.task_id).filter_by(round=ctx.round)
pions_subq = sess.query(db.Participation.user_id).filter_by(contest=ctx.master_contest)
if ctx.site:
pions_subq = pions_subq.filter_by(place=ctx.site)
sol_counts_q = (
sess.query(db.Solution.task_id, func.count(db.Solution.task_id))
.filter(
......@@ -351,7 +379,7 @@ def org_contest(id: int, site_id: Optional[int] = None):
for task_id, count in sol_counts_q.group_by(db.Solution.task_id).all():
sol_counts[task_id] = count
tasks = sess.query(db.Task).filter_by(round=round).all()
tasks = sess.query(db.Task).filter_by(round=ctx.round).all()
tasks.sort(key=lambda t: t.code)
for task in tasks:
task.sol_count = sol_counts[task.task_id] if task.task_id in sol_counts else 0
......@@ -362,7 +390,7 @@ def org_contest(id: int, site_id: Optional[int] = None):
sess.query(db.Place, func.count('*'))
.select_from(db.Participation).join(db.Place)
.group_by(db.Place)
.filter(db.Participation.contest == master_contest).all()
.filter(db.Participation.contest == ctx.master_contest).all()
)
group_contests = contest.get_group_contests(True)
......@@ -370,27 +398,50 @@ def org_contest(id: int, site_id: Optional[int] = None):
return render_template(
'org_contest.html',
contest=contest, group_contests=group_contests, site=site,
rights=sorted(rr.rights, key=lambda r: r.name),
roles=[r.friendly_name() for r in rr.get_roles()],
can_manage=rr.have_right(Right.manage_contest),
can_upload=rr.can_upload_feedback(),
can_edit_points=rr.can_edit_points(),
can_create_solutions=rr.can_upload_feedback() or rr.can_upload_solutions(),
can_view_statement=rr.can_view_statement(),
ctx=ctx, rights=rights,
round=ctx.round, contest=contest, site=ctx.site,
group_contests=group_contests,
rights_list=sorted(rights.rights, key=lambda r: r.name),
roles=[r.friendly_name() for r in rights.get_roles()],
tasks=tasks, places_counts=places_counts,
)
def generic_import(round: db.Round, master_round: db.Round, contest: Optional[db.Contest], master_contest: Optional[db.Contest]):
"""Společná funkce pro importování do soutěží a kol"""
@app.route('/doc/import')
def doc_import():
return render_template('doc_import.html')
class ImportForm(FlaskForm):
file = flask_wtf.file.FileField("Soubor", render_kw={'autofocus': True})
typ = wtforms.SelectField(
"Typ dat",
choices=[(x.name, x.friendly_name()) for x in (ImportType.participants, ImportType.proctors, ImportType.judges)],
coerce=ImportType.coerce,
default=ImportType.participants,
)
fmt = wtforms.SelectField(
"Formát souboru",
choices=FileFormat.choices(), coerce=FileFormat.coerce,
default=FileFormat.cs_csv,
)
submit = wtforms.SubmitField('Importovat')
get_template = wtforms.SubmitField('Stáhnout šablonu')
@app.route('/org/contest/c/<int:ct_id>/import', methods=('GET', 'POST'))
@app.route('/org/contest/r/<int:round_id>/import', methods=('GET', 'POST'))
@app.route('/org/contest/r/<int:round_id>/h/<int:hier_id>/import', methods=('GET', 'POST'))
def org_generic_import(round_id: Optional[int] = None, hier_id: Optional[int] = None, ct_id: Optional[int] = None):
ctx = get_context(round_id=round_id, hier_id=hier_id, ct_id=ct_id, right_needed=Right.manage_contest)
round, contest = ctx.master_round, ctx.master_contest
form = ImportForm()
errs = []
warnings = []
if form.validate_on_submit():
fmt = form.fmt.data
imp = create_import(user=g.user, type=form.typ.data, fmt=fmt, round=master_round, contest=master_contest)
imp = create_import(user=g.user, type=form.typ.data, fmt=fmt, round=round, contest=contest, only_region=ctx.hier_place)
if form.submit.data:
if form.file.data is not None:
file = form.file.data.stream
......@@ -401,10 +452,7 @@ def generic_import(round: db.Round, master_round: db.Round, contest: Optional[db
flash('Soubor neobsahoval žádné řádky s daty', 'danger')
else:
flash(f'Importováno ({imp.cnt_rows} řádků, založeno {imp.cnt_new_users} uživatelů, {imp.cnt_new_participations} účastí, {imp.cnt_new_roles} rolí)', 'success')
if contest is not None:
return redirect(url_for('org_contest', id=contest.contest_id))
else:
return redirect(url_for('org_round', id=round.round_id))
return redirect(ctx.url_home())
else:
errs = imp.errors
warnings = imp.warnings
......@@ -419,6 +467,7 @@ def generic_import(round: db.Round, master_round: db.Round, contest: Optional[db
return render_template(
'org_generic_import.html',
ctx=ctx,
contest=contest,
round=round,
form=form,
......@@ -427,43 +476,39 @@ def generic_import(round: db.Round, master_round: db.Round, contest: Optional[db
)
@app.route('/doc/import')
def doc_import():
return render_template('doc_import.html')
@app.route('/org/contest/c/<int:id>/import', methods=('GET', 'POST'))
def org_contest_import(id: int):
contest, master_contest, rr = get_contest_rr(id, Right.manage_contest)
return generic_import(
round=contest.round, master_round=master_contest.round,
contest=contest, master_contest=master_contest
)
@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")
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'
# URL je explicitně uvedeno v mo.email.contestant_list_url
@app.route('/org/contest/c/<int:ct_id>/participants', methods=('GET', 'POST'))
@app.route('/org/contest/c/<int:ct_id>/site/<int:site_id>/participants', methods=('GET', 'POST'))
@app.route('/org/contest/c/<int:ct_id>/participants/emails', endpoint="org_generic_list_emails")
@app.route('/org/contest/c/<int:ct_id>/site/<int:site_id>/participants/emails', endpoint="org_generic_list_emails")
@app.route('/org/contest/r/<int:round_id>/participants', methods=('GET', 'POST'))
@app.route('/org/contest/r/<int:round_id>/participants/emails', endpoint="org_generic_list_emails")
@app.route('/org/contest/r/<int:round_id>/h/<int:hier_id>/participants', methods=('GET', 'POST'))
@app.route('/org/contest/r/<int:round_id>/h/<int:hier_id>/participants/emails', endpoint="org_generic_list_emails")
def org_generic_list(round_id: Optional[int] = None, hier_id: Optional[int] = None,
ct_id: Optional[int] = None, site_id: Optional[int] = None):
ctx = get_context(round_id=round_id, hier_id=hier_id, ct_id=ct_id, site_id=site_id, right_needed=Right.view_contestants)
round, contest = ctx.master_round, ctx.master_contest
rr = ctx.rights
can_edit = rr.have_right(Right.manage_contest) and request.endpoint != 'org_generic_list_emails'
format = request.args.get('format', "")
filter = ParticipantsFilterForm(request.args)
filter = ParticipantsFilterForm(formdata=request.args)
if request.args:
filter.validate()
query = get_contestants_query(
round=master_contest.round, contest=master_contest, site=site,
school=filter.f_school,
# contest_place=filter.f_contest_place,
participation_place=filter.f_participation_place,
participation_state=filter.f_participation_state,
ctx=ctx,
school=filter.school.place,
contest_place=filter.contest_place.place,
participation_place=filter.participation_place.place,
participation_state=mo.util.star_is_none(filter.participation_state.data),
)
action_form = None
if can_edit:
action_form = ParticipantsActionForm()
if action_form.do_action(round=contest.round, query=query):
if action_form.do_action(round=round, query=query):
# Action happened, redirect
return redirect(request.url)
......@@ -471,23 +516,26 @@ def org_contest_list(id: int, site_id: Optional[int] = None):
table = None
emails = None
mailto_link = None
if request.endpoint == 'org_contest_list_emails':
(emails, mailto_link) = get_contestant_emails(query,
mailto_subject=f'{contest.round.name} {contest.round.category} {contest.place.name_locative()}')
if request.endpoint == 'org_generic_list_emails':
if contest:
subj = f'{contest.round.name} {contest.round.category} {contest.place.name_locative()}'
else:
subj = f'{round.name} kategorie {round.category}'
(emails, mailto_link) = get_contestant_emails(query, mailto_subject=subj)
count = len(emails)
else:
# (count, query) = filter.apply_limits(query, pagesize=50)
count = db.get_count(query)
table = make_contestant_table(query, master_contest.round, add_checkbox=can_edit)
(count, query) = filter.apply_limits(query, pagesize=50)
table = make_contestant_table(query, round, add_contest_column=(contest is None), add_checkbox=can_edit)
return render_template(
'org_contest_list.html',
contest=contest, site=site,
'org_generic_list.html',
ctx=ctx,
contest=contest, round=round, site=ctx.site,
table=table, emails=emails, mailto_link=mailto_link,
filter=filter, count=count, action_form=action_form,
)
else:
table = make_contestant_table(query, master_contest.round, is_export=True)
table = make_contestant_table(query, round, is_export=True)
return table.send_as(format)
......@@ -505,8 +553,7 @@ contest_list_columns = (
def get_contestants_query(
round: db.Round, contest: Optional[db.Contest] = None,
site: Optional[db.Place] = None,
ctx: Context,
contest_place: Optional[db.Place] = None,
participation_place: Optional[db.Place] = None,
participation_state: Optional[db.PartState] = None,
......@@ -516,15 +563,18 @@ def get_contestants_query(
.select_from(db.Participation)
.join(db.Participant, db.Participant.user_id == db.Participation.user_id)
.join(db.User, db.User.user_id == db.Participation.user_id)
.filter(db.Participant.year == round.year))
if contest:
query = query.join(db.Contest, db.Contest.contest_id == contest.contest_id)
.join(db.Contest)
.filter(db.Participant.year == ctx.round.year)
.filter(db.Participation.contest_id == db.Contest.contest_id))
if ctx.contest:
query = query.filter(db.Contest.contest_id == ctx.contest.contest_id)
if ctx.site:
query = query.filter(db.Participation.place_id == ctx.site.place_id)
else:
query = query.filter(db.Contest.round == round)
query = query.filter(db.Contest.round == ctx.round)
if ctx.hier_place:
query = db.filter_place_nth_parent(query, db.Contest.place_id, ctx.round.level - ctx.hier_place.level, ctx.hier_place.place_id)
query = query.options(joinedload(db.Contest.place))
query = query.filter(db.Participation.contest_id == db.Contest.contest_id)
if site:
query = query.filter(db.Participation.place_id == site.place_id)
if contest_place:
query = query.filter(db.Contest.place_id == contest_place.place_id)
if participation_place:
......@@ -599,95 +649,10 @@ def get_contestant_emails(query: Query, mailto_subject: str = '[OSMO] Zpráva pr
return (emails, mailto_link)
@dataclass
class SolutionContext:
contest: db.Contest
master_contest: db.Contest
round: db.Round
master_round: db.Round
pion: Optional[db.Participation]
user: Optional[db.User]
task: Optional[db.Task]
site: Optional[db.Place]
allow_view: bool
allow_upload_solutions: bool
allow_upload_feedback: bool
allow_create_solutions: bool
allow_edit_points: bool
def get_solution_context(contest_id: int, user_id: Optional[int], task_id: Optional[int], site_id: Optional[int]) -> SolutionContext:
sess = db.get_session()
# Nejprve zjistíme, zda existuje soutěž
contest, master_contest = get_contest(contest_id)
round = contest.round
master_round = master_contest.round
# Najdeme úlohu a ověříme, že je součástí soutěže
if task_id is not None:
task = sess.query(db.Task).get(task_id)
if not task or task.round != round:
raise werkzeug.exceptions.NotFound()
else:
task = None
site = None
user = None
if user_id is not None:
# Zkontrolujeme, zda se účastník opravdu účastní soutěže
pion = (sess.query(db.Participation)
.filter_by(user_id=user_id, contest_id=master_contest.contest_id)
.options(joinedload(db.Participation.place),
joinedload(db.Participation.user))
.one_or_none())
if not pion:
raise werkzeug.exceptions.NotFound()
user = pion.user
# A zda soutěží na zadaném soutěžním místě, je-li určeno
if site_id is not None and site_id != pion.place_id:
raise werkzeug.exceptions.NotFound()
# Pokud je uvedeno soutěžní místo, hledáme práva k němu, jinak k soutěži
if site_id is not None:
site = pion.place
else:
pion = None
if site_id is not None:
site = sess.query(db.Place).get(site_id)
if not site:
raise werkzeug.exceptions.NotFound()
rr = g.gatekeeper.rights_for_contest(contest, site)
allow_view = rr.have_right(Right.view_submits)
if not allow_view:
raise werkzeug.exceptions.Forbidden()
allow_upload_solutions = rr.can_upload_solutions()
allow_upload_feedback = rr.can_upload_feedback()
return SolutionContext(
contest=contest, master_contest=master_contest,
round=round, master_round=master_round,
pion=pion,
user=user,
task=task,
site=site,
# XXX: Potřebujeme tohle všechno? Nechceme spíš vracet rr a nechat každého, ať na něm volá metody?
allow_view=allow_view,
allow_upload_solutions=allow_upload_solutions,
allow_upload_feedback=allow_upload_feedback,
allow_create_solutions=allow_upload_solutions or allow_upload_feedback,
allow_edit_points=rr.can_edit_points(),
)
class SubmitForm(FlaskForm):
note = wtforms.TextAreaField("Poznámka pro účastníka", description="Viditelná účastníkovi po uzavření kola", render_kw={'autofocus': True})
org_note = wtforms.TextAreaField("Interní poznámka", description="Viditelná jen organizátorům")
points = MODecimalField('Body', description="Účastník po uzavření kola uvidí jen naposledy zadané body", validators=[validators.Optional()])
points = mo_fields.Points(description="Účastník po uzavření kola uvidí jen naposledy zadané body", validators=[validators.Optional()])
submit = wtforms.SubmitField('Uložit')
file = flask_wtf.file.FileField("Soubor")
......@@ -703,15 +668,15 @@ class SetFinalForm(FlaskForm):
submit_final = wtforms.SubmitField("Prohlásit za finální")
@app.route('/org/contest/c/<int:contest_id>/submit/<int:user_id>/<int:task_id>/', methods=('GET', 'POST'))
@app.route('/org/contest/c/<int:contest_id>/site/<int:site_id>/submit/<int:user_id>/<int:task_id>/', methods=('GET', 'POST'))
def org_submit_list(contest_id: int, user_id: int, task_id: int, site_id: Optional[int] = None):
sc = get_solution_context(contest_id, user_id, task_id, site_id)
assert sc.user is not None
assert sc.task is not None
@app.route('/org/contest/c/<int:ct_id>/submit/<int:user_id>/<int:task_id>/', methods=('GET', 'POST'))
@app.route('/org/contest/c/<int:ct_id>/site/<int:site_id>/submit/<int:user_id>/<int:task_id>/', methods=('GET', 'POST'))
def org_submit_list(ct_id: int, user_id: int, task_id: int, site_id: Optional[int] = None):
ctx = get_context(ct_id=ct_id, site_id=site_id, user_id=user_id, task_id=task_id, right_needed=Right.view_submits)
assert ctx.contest and ctx.user and ctx.task
rights = ctx.rights
sess = db.get_session()
self_url = url_for('org_submit_list', contest_id=contest_id, user_id=user_id, task_id=task_id, site_id=site_id)
self_url = ctx.url_for('org_submit_list')
# Najdeme řešení úlohy (nemusí existovat)
sol = (sess.query(db.Solution)
......@@ -719,7 +684,7 @@ def org_submit_list(contest_id: int, user_id: int, task_id: int, site_id: Option
.one_or_none())
set_final_form: Optional[SetFinalForm] = None
if sol and sc.allow_upload_feedback:
if sol and rights.can_upload_feedback():
set_final_form = SetFinalForm()
if set_final_form.validate_on_submit() and set_final_form.submit_final.data:
is_submit = set_final_form.type.data == "submit"
......@@ -766,7 +731,7 @@ def org_submit_list(contest_id: int, user_id: int, task_id: int, site_id: Option
return redirect(self_url)
form = SubmitForm(obj=sol)
form.points.widget = NumberInput(min=0, max=sc.task.max_points, step=sc.master_round.points_step) # min a max v HTML
form.points.widget = NumberInput(min=0, max=ctx.task.max_points, step=ctx.master_round.points_step) # min a max v HTML
if form.validate_on_submit():
if sol and form.delete.data:
if sol.final_submit or sol.final_feedback:
......@@ -776,14 +741,14 @@ def org_submit_list(contest_id: int, user_id: int, task_id: int, site_id: Option
sess.delete(sol)
mo.util.log(
type=db.LogType.participant,
what=sc.user.user_id,
what=ctx.user.user_id,
details={
'action': 'solution-removed',
'task': task_id,
},
)
sess.commit()
app.logger.info(f"Řešení úlohy {sc.task.code} od účastníka {sc.user.user_id} smazáno")
app.logger.info(f"Řešení úlohy {ctx.task.code} od účastníka {ctx.user.user_id} smazáno")
return redirect(self_url)
points = form.points.data
......@@ -792,28 +757,28 @@ def org_submit_list(contest_id: int, user_id: int, task_id: int, site_id: Option
flash('Schází soubor k nahrání, žádné změny nebyly uloženy', 'danger')
return redirect(self_url)
if points:
error = mo.util.check_points(points, for_task=sc.task, for_round=sc.round)
error = mo.util.check_points(points, for_task=ctx.task, for_round=ctx.round)
if error:
flash(error, 'danger')
return redirect(self_url)
if not sol and (sc.allow_edit_points or sc.allow_upload_solutions or sc.allow_upload_feedback):
if not sol and (rights.can_edit_points() or rights.can_upload_solutions() or rights.can_upload_feedback()):
flash('Řešení založeno', 'success')
sol = db.Solution(task=sc.task, user=sc.user)
sol = db.Solution(task=ctx.task, user=ctx.user)
sess.add(sol)
mo.util.log(
type=db.LogType.participant,
what=sc.user.user_id,
what=ctx.user.user_id,
details={
'action': 'solution-created',
'task': task_id,
},
)
sess.commit()
app.logger.info(f"Řešení úlohy {sc.task.code} od účastníka {sc.user.user_id} založeno")
app.logger.info(f"Řešení úlohy {ctx.task.code} od účastníka {ctx.user.user_id} založeno")
# Edit sol and points
if sol and sc.allow_edit_points:
if sol and rights.can_edit_points():
# Sol edit
sol.note = form.note.data
sol.org_note = form.org_note.data
......@@ -823,7 +788,7 @@ def org_submit_list(contest_id: int, user_id: int, task_id: int, site_id: Option
if points != sol.points:
sol.points = points
sess.add(db.PointsHistory(
task=sc.task,
task=ctx.task,
participant=sol.user,
user=g.user,
points_at=mo.now,
......@@ -835,7 +800,7 @@ def org_submit_list(contest_id: int, user_id: int, task_id: int, site_id: Option
changes = db.get_object_changes(sol)
mo.util.log(
type=db.LogType.participant,
what=sc.user.user_id,
what=ctx.user.user_id,
details={
'action': 'solution-edit',
'task': task_id,
......@@ -843,22 +808,22 @@ def org_submit_list(contest_id: int, user_id: int, task_id: int, site_id: Option
},
)
sess.commit()
app.logger.info(f"Řešení úlohy {sc.task.code} od účastníka {sc.user.user_id} modifikováno, změny: {changes}")
app.logger.info(f"Řešení úlohy {ctx.task.code} od účastníka {ctx.user.user_id} modifikováno, změny: {changes}")
if (form.submit_sol.data and sc.allow_upload_solutions) or (form.submit_fb.data and sc.allow_upload_feedback):
if (form.submit_sol.data and rights.can_upload_solutions()) or (form.submit_fb.data and rights.can_upload_feedback()):
file = form.file.data.stream
if sc.allow_upload_solutions and form.submit_sol.data:
if rights.can_upload_solutions() and form.submit_sol.data:
type = db.PaperType.solution
elif sc.allow_upload_feedback and form.submit_fb.data:
elif rights.can_upload_feedback() and form.submit_fb.data:
type = db.PaperType.feedback
else:
raise werkzeug.exceptions.Forbidden()
assert sc.task is not None and sc.user is not None
paper = db.Paper(task=sc.task, for_user_obj=sc.user, uploaded_by_obj=g.user, type=type, note=form.file_note.data)
assert ctx.task is not None and ctx.user is not None
paper = db.Paper(task=ctx.task, for_user_obj=ctx.user, uploaded_by_obj=g.user, type=type, note=form.file_note.data)
submitter = mo.submit.Submitter()
self_url = url_for('org_submit_list', contest_id=contest_id, user_id=user_id, task_id=task_id, site_id=site_id)
self_url = url_for('org_submit_list', ct_id=ct_id, user_id=user_id, task_id=task_id, site_id=site_id)
try:
submitter.submit_paper(paper, file.name)
......@@ -888,7 +853,7 @@ def org_submit_list(contest_id: int, user_id: int, task_id: int, site_id: Option
return redirect(self_url)
papers = (sess.query(db.Paper)
.filter_by(for_user_obj=sc.user, task=sc.task)
.filter_by(for_user_obj=ctx.user, task=ctx.task)
.options(joinedload(db.Paper.uploaded_by_obj))
.order_by(db.Paper.uploaded_at.desc())
.all())
......@@ -897,21 +862,21 @@ def org_submit_list(contest_id: int, user_id: int, task_id: int, site_id: Option
fb_papers = [p for p in papers if p.type == db.PaperType.feedback]
points_history = (sess.query(db.PointsHistory)
.filter_by(task=sc.task, participant=sc.user)
.filter_by(task=ctx.task, participant=ctx.user)
.options(joinedload(db.PointsHistory.user))
.order_by(db.PointsHistory.points_at.desc())
.all())
return render_template(
'org_submit_list.html',
sc=sc,
ctx=ctx, rights=rights,
solution=sol,
sol_papers=sol_papers,
fb_papers=fb_papers,
points_history=points_history,
for_site=(site_id is not None),
paper_link=lambda p: mo.web.util.org_paper_link(sc.contest, sc.site, sc.user, p),
orig_paper_link=lambda p: mo.web.util.org_paper_link(sc.contest, sc.site, sc.user, p, orig=True),
paper_link=lambda p: mo.web.util.org_paper_link(ctx.contest, ctx.site, ctx.user, p),
orig_paper_link=lambda p: mo.web.util.org_paper_link(ctx.contest, ctx.site, ctx.user, p, orig=True),
form=form,
set_final_form=set_final_form,
)
......@@ -923,11 +888,11 @@ class SubmitEditForm(FlaskForm):
submit = wtforms.SubmitField("Uložit")
@app.route('/org/contest/c/<int:contest_id>/paper/<int:paper_id>/<filename>', endpoint='org_submit_paper')
@app.route('/org/contest/c/<int:contest_id>/site/<int:site_id>/paper/<int:paper_id>/<filename>', endpoint='org_submit_paper')
@app.route('/org/contest/c/<int:contest_id>/paper/orig/<int:paper_id>/<filename>', endpoint='org_submit_paper_orig')
@app.route('/org/contest/c/<int:contest_id>/site/<int:site_id>/paper/orig/<int:paper_id>/<filename>', endpoint='org_submit_paper_orig')
def org_submit_paper(contest_id: int, paper_id: int, filename: str, site_id: Optional[int] = None):
@app.route('/org/contest/c/<int:ct_id>/paper/<int:paper_id>/<filename>', endpoint='org_submit_paper')
@app.route('/org/contest/c/<int:ct_id>/site/<int:site_id>/paper/<int:paper_id>/<filename>', endpoint='org_submit_paper')
@app.route('/org/contest/c/<int:ct_id>/paper/orig/<int:paper_id>/<filename>', endpoint='org_submit_paper_orig')
@app.route('/org/contest/c/<int:ct_id>/site/<int:site_id>/paper/orig/<int:paper_id>/<filename>', endpoint='org_submit_paper_orig')
def org_submit_paper(ct_id: int, paper_id: int, filename: str, site_id: Optional[int] = None):
paper = (db.get_session().query(db.Paper)
.options(joinedload(db.Paper.task)) # pro task_paper_filename()
.get(paper_id))
......@@ -937,7 +902,7 @@ def org_submit_paper(contest_id: int, paper_id: int, filename: str, site_id: Opt
if not filename.endswith('.pdf'):
raise werkzeug.exceptions.NotFound()
get_solution_context(contest_id, paper.for_user, paper.for_task, site_id)
get_context(ct_id=ct_id, user_id=paper.for_user, task_id=paper.for_task, site_id=site_id, right_needed=Right.view_submits)
return mo.web.util.send_task_paper(paper, (request.endpoint == 'org_submit_paper_orig'))
......@@ -970,25 +935,25 @@ class TaskCreateForm(FlaskForm):
submit = wtforms.SubmitField("Založit označená řešení")
@app.route('/org/contest/c/<int:contest_id>/task/<int:task_id>/')
@app.route('/org/contest/c/<int:contest_id>/site/<int:site_id>/task/<int:task_id>/')
@app.route('/org/contest/c/<int:contest_id>/task/<int:task_id>/points', methods=('GET', 'POST'), endpoint="org_contest_task_points")
@app.route('/org/contest/c/<int:contest_id>/task/<int:task_id>/create', methods=('GET', 'POST'), endpoint="org_contest_task_create")
@app.route('/org/contest/c/<int:contest_id>/site/<int:site_id>/task/<int:task_id>/create', methods=('GET', 'POST'), endpoint="org_contest_task_create")
def org_contest_task(contest_id: int, task_id: int, site_id: Optional[int] = None):
sc = get_solution_context(contest_id, None, task_id, site_id)
assert sc.task is not None
@app.route('/org/contest/c/<int:ct_id>/task/<int:task_id>/')
@app.route('/org/contest/c/<int:ct_id>/site/<int:site_id>/task/<int:task_id>/')
@app.route('/org/contest/c/<int:ct_id>/task/<int:task_id>/points', methods=('GET', 'POST'), endpoint="org_contest_task_points")
@app.route('/org/contest/c/<int:ct_id>/task/<int:task_id>/create', methods=('GET', 'POST'), endpoint="org_contest_task_create")
@app.route('/org/contest/c/<int:ct_id>/site/<int:site_id>/task/<int:task_id>/create', methods=('GET', 'POST'), endpoint="org_contest_task_create")
def org_contest_task(ct_id: int, task_id: int, site_id: Optional[int] = None):
ctx = get_context(ct_id=ct_id, site_id=site_id, task_id=task_id, right_needed=Right.view_submits)
assert ctx.contest and ctx.task
action_create = request.endpoint == "org_contest_task_create"
action_points = request.endpoint == "org_contest_task_points"
if action_create and not sc.allow_create_solutions:
if action_create and not ctx.rights.can_create_solutions():
raise werkzeug.exceptions.Forbidden()
if action_points and not sc.allow_edit_points:
if action_points and not ctx.rights.can_edit_points():
raise werkzeug.exceptions.Forbidden()
sess = db.get_session()
q = get_solutions_query(sc.task, for_contest=sc.master_contest, for_site=sc.site)
q = get_solutions_query(ctx.task, for_contest=ctx.master_contest, for_site=ctx.site)
rows: List[Tuple[db.Participation, db.Solution]] = q.all()
rows.sort(key=lambda r: r[0].user.sort_key())
......@@ -1005,7 +970,7 @@ def org_contest_task(contest_id: int, task_id: int, site_id: Optional[int] = Non
if not request.form.get(f"create_sol_{pion.user_id}"):
continue # nikdo nežádá o vytvoření
sol = db.Solution(task=sc.task, user=pion.user)
sol = db.Solution(task=ctx.task, user=pion.user)
sess.add(sol)
mo.util.log(
type=db.LogType.participant,
......@@ -1015,7 +980,7 @@ def org_contest_task(contest_id: int, task_id: int, site_id: Optional[int] = Non
'task': task_id,
},
)
app.logger.info(f"Řešení úlohy {sc.task.code} od účastníka {pion.user_id} založeno")
app.logger.info(f"Řešení úlohy {ctx.task.code} od účastníka {pion.user_id} založeno")
new_sol_count += 1
if new_sol_count > 0:
......@@ -1025,7 +990,7 @@ def org_contest_task(contest_id: int, task_id: int, site_id: Optional[int] = Non
"success")
else:
flash("Žádné změny k uložení", "info")
return redirect(url_for('org_contest_task', contest_id=contest_id, task_id=task_id, site_id=site_id))
return redirect(ctx.url_for('org_contest_task'))
if action_points:
points_form = TaskPointsForm()
......@@ -1036,7 +1001,7 @@ def org_contest_task(contest_id: int, task_id: int, site_id: Optional[int] = Non
if sol is None:
continue
points, error = mo.util.parse_points(request.form.get(f"points_{sol.user_id}"), for_task=sc.task, for_round=sc.round)
points, error = mo.util.parse_points(request.form.get(f"points_{sol.user_id}"), for_task=ctx.task, for_round=ctx.round)
if error:
flash(f'{sol.user.first_name} {sol.user.last_name}: {error}', 'danger')
ok = False
......@@ -1045,7 +1010,7 @@ def org_contest_task(contest_id: int, task_id: int, site_id: Optional[int] = Non
# Save points
sol.points = points
sess.add(db.PointsHistory(
task=sc.task,
task=ctx.task,
participant=sol.user,
user=g.user,
points_at=mo.now,
......@@ -1058,13 +1023,13 @@ def org_contest_task(contest_id: int, task_id: int, site_id: Optional[int] = Non
flash("Změněny body u " + inflect_number(count, "řešení", "řešení", "řešení"), "success")
else:
flash("Žádné změny k uložení", "info")
return redirect(url_for('org_contest_task', contest_id=contest_id, task_id=task_id))
return redirect(ctx.url_for('org_contest_task'))
# Count papers for each solution
paper_counts = {}
for user_id, type, count in (
db.get_session().query(db.Paper.for_user, db.Paper.type, func.count(db.Paper.type))
.filter_by(task=sc.task)
.filter_by(task=ctx.task)
.group_by(db.Paper.for_user, db.Paper.type)
.all()
):
......@@ -1072,8 +1037,10 @@ def org_contest_task(contest_id: int, task_id: int, site_id: Optional[int] = Non
return render_template(
"org_contest_task.html",
sc=sc, rows=rows, paper_counts=paper_counts,
paper_link=lambda u, p: mo.web.util.org_paper_link(sc.contest, sc.site, u, p),
ctx=ctx, rights=ctx.rights,
round=ctx.round, contest=ctx.contest,
rows=rows, paper_counts=paper_counts,
paper_link=lambda u, p: mo.web.util.org_paper_link(ctx.contest, ctx.site, u, p),
points_form=points_form, create_form=create_form, request_form=request.form,
)
......@@ -1082,33 +1049,34 @@ class ContestSolutionsEditForm(FlaskForm):
submit = wtforms.SubmitField("Založit označená řešení")
@app.route('/org/contest/c/<int:id>/solutions', methods=('GET', 'POST'))
@app.route('/org/contest/c/<int:id>/site/<int:site_id>/solutions', methods=('GET', 'POST'))
@app.route('/org/contest/c/<int:id>/solutions/edit', methods=('GET', 'POST'), endpoint="org_contest_solutions_edit")
@app.route('/org/contest/c/<int:id>/site/<int:site_id>/solutions/edit', methods=('GET', 'POST'), endpoint="org_contest_solutions_edit")
def org_contest_solutions(id: int, site_id: Optional[int] = None):
sc = get_solution_context(id, None, None, site_id)
@app.route('/org/contest/c/<int:ct_id>/solutions', methods=('GET', 'POST'))
@app.route('/org/contest/c/<int:ct_id>/site/<int:site_id>/solutions', methods=('GET', 'POST'))
@app.route('/org/contest/c/<int:ct_id>/solutions/edit', methods=('GET', 'POST'), endpoint="org_contest_solutions_edit")
@app.route('/org/contest/c/<int:ct_id>/site/<int:site_id>/solutions/edit', methods=('GET', 'POST'), endpoint="org_contest_solutions_edit")
def org_contest_solutions(ct_id: int, site_id: Optional[int] = None):
sess = db.get_session()
ctx = get_context(ct_id=ct_id, site_id=site_id, right_needed=Right.view_submits)
assert ctx.contest
edit_action = request.endpoint == "org_contest_solutions_edit"
if edit_action and not sc.allow_create_solutions:
if edit_action and not ctx.rights.can_create_solutions():
raise werkzeug.exceptions.Forbidden()
pions_subq = sess.query(db.Participation.user_id).filter_by(contest=sc.master_contest)
if sc.site:
pions_subq = pions_subq.filter_by(place=sc.site)
pions_subq = sess.query(db.Participation.user_id).filter_by(contest=ctx.master_contest)
if ctx.site:
pions_subq = pions_subq.filter_by(place=ctx.site)
pions_subq = pions_subq.subquery()
pions = (sess.query(db.Participation)
.filter(
db.Participation.contest == sc.master_contest,
db.Participation.contest == ctx.master_contest,
db.Participation.user_id.in_(pions_subq),
).options(joinedload(db.Participation.user))
.all())
pions.sort(key=lambda p: p.user.sort_key())
tasks_subq = sess.query(db.Task.task_id).filter_by(round=sc.round).subquery()
tasks_subq = sess.query(db.Task.task_id).filter_by(round=ctx.round).subquery()
tasks = (sess.query(db.Task)
.filter_by(round=sc.round)
.filter_by(round=ctx.round)
.order_by(db.Task.code)
.all())
......@@ -1168,19 +1136,20 @@ def org_contest_solutions(id: int, site_id: Optional[int] = None):
"success")
else:
flash("Žádné změny k uložení", "info")
return redirect(url_for('org_contest_solutions', id=id, site_id=site_id))
return redirect(ctx.url_for('org_contest_solutions'))
return render_template(
'org_contest_solutions.html',
contest=sc.contest, site=sc.site, sc=sc,
ctx=ctx,
contest=ctx.contest, site=ctx.site, rights=ctx.rights,
pions=pions, tasks=tasks, tasks_sols=task_sols, paper_counts=paper_counts,
paper_link=lambda u, p: mo.web.util.org_paper_link(sc.contest, sc.site, u, p),
paper_link=lambda u, p: mo.web.util.org_paper_link(ctx.contest, ctx.site, u, p),
edit_form=edit_form,
)
class DownloadSubmitsForm(FlaskForm):
min_points = wtforms.IntegerField(
min_points = mo_fields.Points(
'Minimální počet bodů v kole', render_kw={'autofocus': True},
description='Je-li uveden, řešení účastníků, kteří ve výsledcích celého kola dostali méně bodů, se nestahují.',
validators=[validators.Optional()]
......@@ -1229,23 +1198,30 @@ def download_submits(form: DownloadSubmitsForm, round: db.Round, sol_query, pion
return True
def generic_batch_download(round: db.Round, contest: Optional[db.Contest], site: Optional[db.Place], task: db.Task):
"""Společná funkce pro download submitů/feedbacku do soutěží a kol."""
@app.route('/org/contest/c/<int:ct_id>/task/<int:task_id>/download', methods=('GET', 'POST'))
@app.route('/org/contest/c/<int:ct_id>/site/<int:site_id>/task/<int:task_id>/download', methods=('GET', 'POST'))
@app.route('/org/contest/r/<int:round_id>/task/<int:task_id>/download', methods=('GET', 'POST'))
@app.route('/org/contest/r/<int:round_id>/h/<int:hier_id>/task/<int:task_id>/download', methods=('GET', 'POST'))
def org_generic_batch_download(task_id: int, round_id: Optional[int] = None, hier_id: Optional[int] = None, ct_id: Optional[int] = None, site_id: Optional[int] = None):
sess = db.get_session()
ctx = get_context(round_id=round_id, hier_id=hier_id, ct_id=ct_id, site_id=site_id, task_id=task_id, right_needed=Right.view_submits)
round, hier_place, contest, site, task = ctx.round, ctx.hier_place, ctx.contest, ctx.site, ctx.task
assert task
pion_query = sess.query(db.Participation.user_id).select_from(db.Participation)
if contest is not None:
if contest:
pion_query = pion_query.filter_by(contest_id=contest.master_contest_id)
if site is not None:
if site:
pion_query = pion_query.filter_by(place=site)
else:
pion_query = pion_query.join(db.Contest).filter(db.Contest.round_id == round.master_round_id)
if hier_place:
pion_query = db.filter_place_nth_parent(pion_query, db.Contest.place_id, round.level - hier_place.level, hier_place.place_id)
sol_query = (sess.query(db.Solution)
.select_from(db.Solution)
.filter(db.Solution.task == task))
if contest is not None:
if contest or hier_place:
sol_query = sol_query.filter(db.Solution.user_id.in_(pion_query.subquery()))
form = DownloadSubmitsForm()
......@@ -1255,6 +1231,8 @@ def generic_batch_download(round: db.Round, contest: Optional[db.Contest], site:
subj = f'{subj} ({site.name})'
elif contest is not None:
subj = f'{subj} ({contest.place.name})'
elif hier_place is not None:
subj = f'{subj} ({hier_place.name})'
if download_submits(form, round, sol_query, pion_query, subj, contest is None):
return redirect(url_for('org_jobs'))
......@@ -1273,7 +1251,8 @@ def generic_batch_download(round: db.Round, contest: Optional[db.Contest], site:
return render_template(
'org_generic_batch_download.html',
round=round, contest=contest, site=site, task=task,
ctx=ctx,
task=task,
submit_count=submit_count,
pion_count=pion_count,
sol_count=sol_count, fb_count=fb_count,
......@@ -1287,13 +1266,18 @@ class UploadSubmitsForm(FlaskForm):
submit = wtforms.SubmitField('Odeslat')
def generic_batch_upload(round: db.Round, contest: Optional[db.Contest], site: Optional[db.Place], task: db.Task,
offer_upload_solutions: bool, offer_upload_feedback: bool):
"""Společná funkce pro upload feedbacku do soutěží a kol."""
@app.route('/org/contest/c/<int:ct_id>/task/<int:task_id>/upload', methods=('GET', 'POST'))
@app.route('/org/contest/c/<int:ct_id>/site/<int:site_id>/task/<int:task_id>/upload', methods=('GET', 'POST'))
@app.route('/org/contest/r/<int:round_id>/task/<int:task_id>/upload', methods=('GET', 'POST'))
@app.route('/org/contest/r/<int:round_id>/h/<int:hier_id>/task/<int:task_id>/upload', methods=('GET', 'POST'))
def org_generic_batch_upload(task_id: int, round_id: Optional[int] = None, hier_id: Optional[int] = None, ct_id: Optional[int] = None, site_id: Optional[int] = None):
ctx = get_context(round_id=round_id, hier_id=hier_id, ct_id=ct_id, site_id=site_id, task_id=task_id)
round, hier_place, contest, site, task = ctx.round, ctx.hier_place, ctx.contest, ctx.site, ctx.task
assert task
# Základní kontrola, zda vůbec chceme akci spustit.
# Základní kontrola, zda vůbec chceme akci spustit. Zbytek se kontrole uvnitř jobu.
# Zatím neumíme dávkově nahrávat řešení.
if not offer_upload_feedback:
if not ctx.rights.offer_upload_feedback():
raise werkzeug.exceptions.Forbidden()
request.custom_max_content_length = mo.config.MAX_BATCH_CONTENT_LENGTH
......@@ -1303,35 +1287,18 @@ def generic_batch_upload(round: db.Round, contest: Optional[db.Contest], site: O
file = form.file.data.stream
mo.jobs.submit.schedule_upload_feedback(round, file.name, f'Nahrání opravených řešení {round.round_code()}',
for_user=g.user,
only_contest=contest, only_site=site, only_task=task)
only_contest=contest, only_site=site, only_region=hier_place, only_task=task)
return redirect(url_for('org_jobs'))
return render_template(
'org_generic_batch_upload.html',
round=round, contest=contest, site=site, task=task,
ctx=ctx,
task=task,
max_size=mo.config.MAX_BATCH_CONTENT_LENGTH,
form=form,
)
@app.route('/org/contest/c/<int:contest_id>/task/<int:task_id>/download', methods=('GET', 'POST'))
@app.route('/org/contest/c/<int:contest_id>/site/<int:site_id>/task/<int:task_id>/download', methods=('GET', 'POST'))
def org_contest_task_download(contest_id: int, task_id: int, site_id: Optional[int] = None):
sc = get_solution_context(contest_id, None, task_id, site_id)
assert sc.task is not None
return generic_batch_download(round=sc.round, contest=sc.contest, site=sc.site, task=sc.task)
@app.route('/org/contest/c/<int:contest_id>/task/<int:task_id>/upload', methods=('GET', 'POST'))
@app.route('/org/contest/c/<int:contest_id>/site/<int:site_id>/task/<int:task_id>/upload', methods=('GET', 'POST'))
def org_contest_task_upload(contest_id: int, task_id: int, site_id: Optional[int] = None):
sc = get_solution_context(contest_id, None, task_id, site_id)
assert sc.task is not None
return generic_batch_upload(round=sc.round, contest=sc.contest, site=sc.site, task=sc.task,
offer_upload_solutions=sc.allow_upload_solutions,
offer_upload_feedback=sc.allow_upload_feedback)
class BatchPointsForm(FlaskForm):
file = flask_wtf.file.FileField("Soubor", render_kw={'autofocus': True})
fmt = wtforms.SelectField(
......@@ -1344,15 +1311,22 @@ class BatchPointsForm(FlaskForm):
get_template = wtforms.SubmitField('Stáhnout šablonu')
def generic_batch_points(round: db.Round, contest: Optional[db.Contest], task: db.Task):
"""Společná funkce pro download/upload bodů do soutěží a kol."""
@app.route('/org/contest/c/<int:ct_id>/task/<int:task_id>/batch-points', methods=('GET', 'POST'))
@app.route('/org/contest/r/<int:round_id>/task/<int:task_id>/batch-points', methods=('GET', 'POST'))
@app.route('/org/contest/r/<int:round_id>/h/<int:hier_id>/task/<int:task_id>/batch-points', methods=('GET', 'POST'))
def org_generic_batch_points(task_id: int, round_id: Optional[int] = None, hier_id: Optional[int] = None, ct_id: Optional[int] = None):
ctx = get_context(round_id=round_id, hier_id=hier_id, ct_id=ct_id, task_id=task_id)
round, hier_place, contest, task = ctx.round, ctx.hier_place, ctx.contest, ctx.task
if not ctx.rights.can_edit_points():
raise werkzeug.exceptions.Forbidden()
form = BatchPointsForm()
errs = []
warnings = []
if form.validate_on_submit():
fmt = form.fmt.data
imp = create_import(user=g.user, type=ImportType.points, fmt=fmt, round=round, contest=contest, task=task, allow_add_del=form.add_del_sols.data)
imp = create_import(user=g.user, type=ImportType.points, fmt=fmt, round=round, only_region=hier_place, contest=contest, task=task, allow_add_del=form.add_del_sols.data)
if form.submit.data:
if form.file.data is not None:
file = form.file.data.stream
......@@ -1363,10 +1337,7 @@ def generic_batch_points(round: db.Round, contest: Optional[db.Contest], task: d
flash('Soubor neobsahoval žádné řádky s daty', 'danger')
else:
flash(f'Importováno ({imp.cnt_rows} řádků, {imp.cnt_set_points} řešení přebodováno, {imp.cnt_add_sols} založeno a {imp.cnt_del_sols} smazáno)', 'success')
if contest is not None:
return redirect(url_for('org_contest', id=contest.contest_id))
else:
return redirect(url_for('org_round', id=round.round_id))
return redirect(ctx.url_home())
else:
errs = imp.errors
warnings = imp.warnings
......@@ -1381,6 +1352,7 @@ def generic_batch_points(round: db.Round, contest: Optional[db.Contest], task: d
return render_template(
'org_generic_batch_points.html',
ctx=ctx,
round=round, contest=contest, task=task,
form=form,
errs=errs,
......@@ -1388,37 +1360,27 @@ def generic_batch_points(round: db.Round, contest: Optional[db.Contest], task: d
)
@app.route('/org/contest/c/<int:contest_id>/task/<int:task_id>/batch-points', methods=('GET', 'POST'))
def org_contest_task_batch_points(contest_id: int, task_id: int):
sc = get_solution_context(contest_id, None, task_id, None)
assert sc.task is not None
if not sc.allow_edit_points:
raise werkzeug.exceptions.Forbidden()
return generic_batch_points(round=sc.round, contest=sc.contest, task=sc.task)
@app.route('/org/contest/c/<int:contest_id>/user/<int:user_id>')
def org_contest_user(contest_id: int, user_id: int):
sc = get_solution_context(contest_id, user_id, None, None)
@app.route('/org/contest/c/<int:ct_id>/user/<int:user_id>')
def org_contest_user(ct_id: int, user_id: int):
ctx = get_context(ct_id=ct_id, user_id=user_id, right_needed=Right.view_contestants)
assert ctx.contest
sess = db.get_session()
pant = sess.query(db.Participant).filter_by(
user_id=user_id, year=sc.round.year
user_id=user_id, year=ctx.round.year
).options(joinedload(db.Participant.school_place)).one_or_none()
if not pant:
raise werkzeug.exceptions.NotFound()
task_sols = sess.query(db.Task, db.Solution).select_from(db.Task).outerjoin(
db.Solution, and_(db.Solution.task_id == db.Task.task_id, db.Solution.user == sc.user)
).filter(db.Task.round == sc.round).options(
db.Solution, and_(db.Solution.task_id == db.Task.task_id, db.Solution.user == ctx.user)
).filter(db.Task.round == ctx.round).options(
joinedload(db.Solution.final_submit_obj),
joinedload(db.Solution.final_feedback_obj)
).order_by(db.Task.code).all()
# Count papers for each task and solution
tasks_subq = sess.query(db.Task.task_id).filter_by(round=sc.round).subquery()
tasks_subq = sess.query(db.Task.task_id).filter_by(round=ctx.round).subquery()
paper_counts = {}
for task_id, type, count in (
db.get_session().query(db.Paper.for_task, db.Paper.type, func.count(db.Paper.type))
......@@ -1432,14 +1394,15 @@ def org_contest_user(contest_id: int, user_id: int):
return render_template(
'org_contest_user.html',
sc=sc, pant=pant, task_sols=task_sols,
paper_link=lambda u, p: mo.web.util.org_paper_link(sc.contest, None, u, p),
ctx=ctx, rights=ctx.rights,
pant=pant, task_sols=task_sols,
paper_link=lambda u, p: mo.web.util.org_paper_link(ctx.contest, None, u, p),
paper_counts=paper_counts,
)
class AdvanceForm(FlaskForm):
boundary = MODecimalField(
boundary = mo_fields.Points(
'Bodová hranice', render_kw={'autofocus': True},
description="Postoupí všichni účastníci, kteří v minulém kole získali aspoň tolik bodů.",
validators=[validators.InputRequired()]
......@@ -1449,16 +1412,18 @@ class AdvanceForm(FlaskForm):
execute = wtforms.SubmitField('Provést')
@app.route('/org/contest/c/<int:contest_id>/advance', methods=('GET', 'POST'))
def org_contest_advance(contest_id: int):
@app.route('/org/contest/c/<int:ct_id>/advance', methods=('GET', 'POST'))
def org_contest_advance(ct_id: int):
sess = db.get_session()
conn = sess.connection()
contest, master_contest, rr = get_contest_rr(contest_id, Right.manage_contest)
ctx = get_context(ct_id=ct_id, right_needed=Right.manage_contest)
contest, master_contest = ctx.contest, ctx.master_contest
round = ctx.round
assert contest and master_contest
def redirect_back():
return redirect(url_for('org_contest', id=contest_id))
return redirect(ctx.url_for('org_contest'))
round = contest.round
if round.state != db.RoundState.preparing:
flash('Aktuální kolo není ve stavu přípravy', 'danger')
return redirect_back()
......@@ -1496,7 +1461,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:
......@@ -1539,7 +1504,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)
......@@ -1548,7 +1513,7 @@ def org_contest_advance(contest_id: int):
if inserted:
# Opravdu došlo ke vložení
really_inserted += 1
app.logger.info(f'Postup: Založena účast user=#{pp.user_id} contest=#{contest_id} place=#{contest.place_id}')
app.logger.info(f'Postup: Založena účast user=#{pp.user_id} contest=#{ct_id} place=#{contest.place_id}')
mo.util.log(
type=db.LogType.participant,
what=pp.user_id,
......@@ -1573,6 +1538,7 @@ def org_contest_advance(contest_id: int):
return render_template(
'org_contest_advance.html',
ctx=ctx,
contest=contest,
round=contest.round,
prev_round=prev_round,
......@@ -1590,11 +1556,13 @@ class ContestEditForm(FlaskForm):
submit = wtforms.SubmitField('Uložit')
@app.route('/org/contest/c/<int:id>/edit', methods=('GET', 'POST'))
def org_contest_edit(id: int):
@app.route('/org/contest/c/<int:ct_id>/edit', methods=('GET', 'POST'))
def org_contest_edit(ct_id: int):
sess = db.get_session()
contest, _, rr = get_contest_rr(id, Right.manage_contest)
round = contest.round
ctx = get_context(ct_id=ct_id, right_needed=Right.manage_contest)
contest = ctx.contest
round = ctx.round
assert contest and round
form = ContestEditForm(obj=contest)
if round.state != db.RoundState.delegate:
......@@ -1609,25 +1577,85 @@ def org_contest_edit(id: int):
if 'state' in changes and round.state != db.RoundState.delegate:
flash("Nastavení kola neumožňuje měnit stav soutěže", "danger")
return redirect(url_for('org_contest', id=id))
return redirect(url_for('org_contest', ct_id=ct_id))
app.logger.info(f"Contest #{id} modified, changes: {changes}")
app.logger.info(f"Contest #{ct_id} modified, changes: {changes}")
mo.util.log(
type=db.LogType.contest,
what=id,
what=ct_id,
details={'action': 'edit', 'changes': changes},
)
sess.commit()
flash('Změny soutěže 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_contest', id=id))
return redirect(ctx.url_for('org_contest'))
return render_template(
'org_contest_edit.html',
ctx=ctx,
round=round,
contest=contest,
form=form,
)
class ParticipantAddForm(FlaskForm):
email = mo_fields.Email(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")
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:ct_id>/participants/new', methods=('GET', 'POST'))
@app.route('/org/contest/c/<int:ct_id>/site/<int:site_id>/participants/new', methods=('GET', 'POST'))
def org_contest_add_user(ct_id: int, site_id: Optional[int] = None):
ctx = get_context(ct_id=ct_id, site_id=site_id, right_needed=Right.manage_contest)
contest = ctx.master_contest
assert 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(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")
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:
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(ctx.url_for('org_generic_list'))
return render_template(
'org_contest_add_user.html',
ctx=ctx,
contest=contest, round=ctx.master_round, site=ctx.site,
form=form
)
......@@ -13,6 +13,7 @@ import mo.imports
import mo.rights
import mo.util
from mo.web import app
import mo.web.fields as mo_fields
import wtforms.validators as validators
......@@ -80,8 +81,8 @@ class PlaceSchoolEditForm(PlaceEditForm):
def place_breadcrumbs(place: db.Place, action: Optional[str] = None) -> Markup:
elements = []
parents: List[db.Place] = reversed(g.gatekeeper.get_parents(place))
for parent in parents:
ancestors: List[db.Place] = g.gatekeeper.get_ancestors(place)
for parent in ancestors:
elements.append((url_for('org_place', id=parent.place_id), parent.name))
if action:
elements.append(('', action))
......@@ -177,14 +178,14 @@ def org_place_edit(id: int):
class PlaceMoveForm(FlaskForm):
code = wtforms.StringField(validators=[validators.DataRequired()], render_kw={'autofocus': True})
new_parent = mo_fields.Place(validators=[validators.DataRequired()], render_kw={'autofocus': True})
submit = wtforms.SubmitField('Najít místo')
reset = wtforms.HiddenField()
move = wtforms.HiddenField()
class PlaceMoveConfirmForm(FlaskForm):
code = wtforms.HiddenField()
new_parent = mo_fields.Place(widget = wtforms.widgets.HiddenInput())
reset = wtforms.SubmitField('Zrušit')
move = wtforms.SubmitField('Přesunout')
......@@ -206,15 +207,15 @@ def org_place_move(id: int):
form = PlaceMoveForm()
form_confirm = None
if form.validate_on_submit():
if not form.validate_on_submit():
if form.new_parent.place_error:
search_error = form.new_parent.place_error
else:
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(g.gatekeeper.get_parents(new_parent))
new_parent = form.new_parent.place
new_parents = g.gatekeeper.get_ancestors(new_parent)
(_, levels) = db.place_type_names_and_levels[place.type]
rr = g.gatekeeper.rights_for(new_parent)
......@@ -241,7 +242,9 @@ def org_place_move(id: int):
else:
# OK but not confirmed yet, display the confirm form
form_confirm = PlaceMoveConfirmForm()
form_confirm.code.data = form.code.data
form_confirm.new_parent.data = form.new_parent.data
# tady se používá hnusný trik, že políčko new_parents z PlaceMoveConfirmForm se
# parsuje jako new_parents z PlaceMoveForm
return render_template(
'org_place_move.html',
......@@ -367,7 +370,7 @@ def org_place_rights(id: int):
if not place:
raise werkzeug.exceptions.NotFound()
parent_ids = [p.place_id for p in g.gatekeeper.get_parents(place)]
parent_ids = [p.place_id for p in g.gatekeeper.get_ancestors(place)]
roles = (sess.query(db.UserRole)
.filter(db.UserRole.place_id.in_(parent_ids))
.options(joinedload(db.UserRole.user))
......
from dataclasses import dataclass, field
import decimal
from flask import render_template, g, redirect, url_for, flash, request
from flask import render_template, g, redirect, flash, request
import locale
import flask_wtf.file
from flask_wtf.form import FlaskForm
......@@ -8,54 +9,38 @@ from bleach.sanitizer import ALLOWED_TAGS
import markdown
import os
from sqlalchemy import func
from sqlalchemy.orm import joinedload
from sqlalchemy.orm import joinedload, aliased
from sqlalchemy.sql.functions import coalesce
from typing import Optional, Tuple
from typing import Optional, List, Dict, Tuple, Set
import werkzeug.exceptions
import wtforms
from wtforms import validators, ValidationError
from wtforms.widgets.html5 import NumberInput
import mo
import mo.config as config
import mo.db as db
import mo.imports
from mo.rights import Right, RoundRights
from mo.rights import Right
import mo.util
from mo.web import app
from mo.web.util import MODecimalField
from mo.web.org_contest import ParticipantsActionForm, ParticipantsFilterForm, get_contestant_emails, get_contestants_query, make_contestant_table, \
generic_import, generic_batch_download, generic_batch_upload, generic_batch_points
import mo.web.fields as mo_fields
from mo.web.org_contest import get_context
def get_round_rr(id: int, right_needed: Optional[Right], any_place: bool) -> Tuple[db.Round, db.Round, RoundRights]:
"""Vrací round, master_round a Rights objekt pro zadané round_id.
Pro nedělená kola platí round == master_round.
Operace s účastníky by měly probíhat vždy přes master_round."""
round = db.get_session().query(db.Round).options(joinedload(db.Round.master)).get(id)
if not round:
raise werkzeug.exceptions.NotFound()
rr = g.gatekeeper.rights_for_round(round, any_place)
if not (right_needed is None or rr.have_right(right_needed)):
raise werkzeug.exceptions.Forbidden()
return round, round.master, rr
@app.route('/org/contest/')
def org_rounds():
sess = db.get_session()
def get_task(round: db.Round, task_id: int) -> db.Task:
task = db.get_session().query(db.Task).get(task_id)
if not task or task.round_id != round.round_id:
raise werkzeug.exceptions.NotFound()
return task
rounds = sess.query(db.Round).filter_by(year=config.CURRENT_YEAR).order_by(db.Round.year, db.Round.category, db.Round.seq, db.Round.part)
return render_template('org_rounds.html', rounds=rounds, history=False)
@app.route('/org/contest/')
def org_rounds():
@app.route('/org/contest/history')
def org_rounds_history():
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)
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):
......@@ -96,7 +81,7 @@ def delete_task(round_id: int, form: TaskDeleteForm) -> bool:
class AddContestForm(FlaskForm):
place_code = wtforms.StringField('Nová soutěž v oblasti:', validators=[validators.Required()])
place = mo_fields.Place('Nová soutěž v oblasti:', validators=[validators.Required()])
create_contest = wtforms.SubmitField('Založit')
......@@ -104,10 +89,7 @@ def add_contest(round: db.Round, form: AddContestForm) -> bool:
if not (request.method == 'POST' and 'create_contest' in request.form and form.validate_on_submit()):
return False
place = db.get_place_by_code(form.place_code.data)
if place is None:
flash(f'Místo s kódem {form.place_code.data} neexistuje', 'danger')
return False
place: db.Place = form.place.place
if place.level != round.level:
flash(f'{place.type_name().title()} {place.name} není {round.get_level().name}', 'danger')
......@@ -144,14 +126,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,78 +155,125 @@ 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)}")
app.logger.info(f"Podsoutěž #{subcontest.contest_id} založena: {db.row2dict(subcontest)}")
sess.commit()
flash(f'Založena soutěž {place.name_locative()}', 'success')
return True
@dataclass
class ContestStat:
region: db.Place
contest: Optional[db.Contest] = None
num_contests: int = 0
contest_states: Set[db.RoundState] = field(default_factory=set)
num_active_pants: int = 0
num_unconfirmed_pants: int = 0
@app.route('/org/contest/r/<int:id>/', methods=('GET', 'POST'))
def org_round(id: int):
def region_stats(round: db.Round, region: db.Place) -> List[ContestStat]:
stats: Dict[int, ContestStat] = {}
sess = db.get_session()
round, _, rr = get_round_rr(id, None, True)
can_manage_round = rr.have_right(Right.manage_round)
can_manage_contestants = rr.have_right(Right.manage_contest)
participants_count = sess.query(
db.Participation.contest_id,
func.count(db.Participation.user_id).label('count')
).group_by(db.Participation.contest_id).subquery()
# účastníci jsou jen pod master contesty
contests_counts = (sess.query(
db.Contest,
coalesce(participants_count.c.count, 0)
).outerjoin(participants_count, db.Contest.master_contest_id == participants_count.c.contest_id)
.filter(db.Contest.round == round)
.options(joinedload(db.Contest.place))
.all())
contests_counts.sort(key=lambda c: locale.strxfrm(c[0].place.name))
if region.level > round.level:
return []
if (region.level >= round.level - 1
or region.level == 2 and round.level == 4):
# List individual contests
q = sess.query(db.Contest).filter_by(round=round)
q = db.filter_place_nth_parent(q, db.Contest.place_id, round.level - region.level, region.place_id)
q = q.options(joinedload(db.Contest.place))
for c in q.all():
s = ContestStat(region=c.place, contest=c, num_contests=1)
stats[c.place.place_id] = s
have_contests = True
else:
# List sub-regions
regs = sess.query(db.Place).filter(db.Place.parent_place == region).all()
for r in regs:
s = ContestStat(region=r)
stats[r.place_id] = s
have_contests = False
region_ids = [s.region.place_id for s in stats.values()]
if not have_contests:
rcs = (sess.query(db.RegionContestStat)
.filter_by(round=round)
.filter(db.RegionContestStat.region.in_(region_ids))
.all())
for r in rcs:
stats[r.region].num_contests += r.count
stats[r.region].contest_states.add(r.state)
sol_counts_q = (
sess.query(db.Solution.task_id, func.count(db.Solution.task_id))
.filter(db.Solution.task_id.in_(
sess.query(db.Task.task_id).filter_by(round=round)
))
rs = (sess.query(db.RegionParticipantStat)
.filter_by(round_id=round.master_round_id)
.filter(db.RegionParticipantStat.region.in_(region_ids))
.all())
for r in rs:
if r.state == db.PartState.active:
stats[r.region].num_active_pants = r.count
elif r.state == db.PartState.registered:
stats[r.region].num_unconfirmed_pants = r.count
out = list(stats.values())
out.sort(key=lambda s: locale.strxfrm(s.region.name or ""))
return out
def region_totals(region: db.Place, stats: List[ContestStat]) -> ContestStat:
return ContestStat(
region=region,
num_contests=sum(s.num_contests for s in stats),
num_active_pants=sum(s.num_active_pants for s in stats),
num_unconfirmed_pants=sum(s.num_unconfirmed_pants for s in stats),
)
sol_counts = {}
for task_id, count in sol_counts_q.group_by(db.Solution.task_id).all():
sol_counts[task_id] = count
def task_stats(round: db.Round, region: db.Place) -> List[Tuple[db.Task, int]]:
sess = db.get_session()
tasks = sess.query(db.Task).filter_by(round=round).all()
tasks.sort(key=lambda t: t.code)
for task in tasks:
task.sol_count = sol_counts[task.task_id] if task.task_id in sol_counts else 0
ts = (sess.query(db.RegionTaskStat)
.filter_by(round=round, region=region.place_id)
.all())
count_by_id = {s.task_id: s.count for s in ts}
return [(t, count_by_id.get(t.task_id, 0)) for t in tasks]
@app.route('/org/contest/r/<int:round_id>/', methods=('GET', 'POST'))
@app.route('/org/contest/r/<int:round_id>/h/<int:hier_id>', methods=('GET', 'POST'))
def org_round(round_id: int, hier_id: Optional[int] = None):
ctx = get_context(round_id=round_id, hier_id=hier_id)
round = ctx.round
rights = ctx.rights
form_delete_task = TaskDeleteForm()
if can_manage_round and delete_task(id, form_delete_task):
return redirect(url_for('org_round', id=id))
if rights.have_right(Right.manage_round) and delete_task(round_id, form_delete_task):
return redirect(ctx.url_for('org_round'))
form_add_contest = AddContestForm()
form_add_contest.place_code.label.text = "Nová soutěž " + round.get_level().in_name()
form_add_contest.place.label.text = "Nová soutěž " + round.get_level().in_name()
if add_contest(round, form_add_contest):
return redirect(url_for('org_round', id=id))
return redirect(ctx.url_for('org_round'))
group_rounds = round.get_group_rounds(True)
group_rounds.sort(key=lambda r: r.round_code())
region = ctx.hier_place or db.get_root_place()
reg_stats = region_stats(round, region)
reg_total = region_totals(region, reg_stats)
task_info = task_stats(round, region)
return render_template(
'org_round.html',
ctx=ctx, rights=rights,
round=round, group_rounds=group_rounds,
roles=[r.friendly_name() for r in rr.get_roles()],
contests_counts=contests_counts,
tasks=tasks, form_delete_task=form_delete_task,
roles=[r.friendly_name() for r in rights.get_roles()],
reg_stats=reg_stats, reg_total=reg_total,
task_info=task_info,
form_delete_task=form_delete_task,
form_add_contest=form_add_contest,
can_manage_round=can_manage_round,
can_manage_contestants=can_manage_contestants,
can_handle_submits=rr.have_right(Right.view_submits),
can_upload=rr.offer_upload_feedback(),
can_view_statement=rr.can_view_statement(),
can_add_contest=g.gatekeeper.rights_generic().have_right(Right.add_contest),
statement_exists=mo.web.util.task_statement_exists(round),
)
......@@ -243,7 +284,7 @@ class TaskEditForm(FlaskForm):
validators.Regexp(r'^[A-Za-z0-9-]+$', message="Kód úlohy smí obsahovat jen nediakritická písmena, čísla a znak -"),
], render_kw={'autofocus': True})
name = wtforms.StringField('Název úlohy')
max_points = MODecimalField(
max_points = mo_fields.Points(
'Maximum bodů', validators=[validators.Optional(), validators.NumberRange(min=0)],
description="Při nastavení maxima nelze udělit více bodů, pro zrušení uložte prázdnou hodnotu",
)
......@@ -254,18 +295,18 @@ class TaskEditForm(FlaskForm):
self.max_points.widget = NumberInput(min=0, step=points_step)
@app.route('/org/contest/r/<int:id>/task/new', methods=('GET', 'POST'))
def org_round_task_new(id: int):
@app.route('/org/contest/r/<int:round_id>/task/new', methods=('GET', 'POST'))
def org_round_task_new(round_id: int):
sess = db.get_session()
round, master_round, _ = get_round_rr(id, Right.manage_round, True)
ctx = get_context(round_id=round_id, right_needed=Right.manage_round)
form = TaskEditForm(master_round.points_step)
form = TaskEditForm(ctx.master_round.points_step)
if form.validate_on_submit():
task = db.Task()
task.round = round
task.round = ctx.round
form.populate_obj(task)
if sess.query(db.Task).filter_by(round_id=id, code=task.code).first():
if sess.query(db.Task).filter_by(round_id=round_id, code=task.code).first():
flash('Úloha se stejným kódem již v tomto kole existuje', 'danger')
else:
sess.add(task)
......@@ -278,28 +319,25 @@ def org_round_task_new(id: int):
sess.commit()
app.logger.info(f"Úloha {task.code} ({task.task_id}) přidána: {db.row2dict(task)}")
flash('Nová úloha přidána', 'success')
return redirect(url_for('org_round', id=id))
return redirect(ctx.url_for('org_round'))
return render_template(
'org_round_task_edit.html',
round=round, task=None, form=form,
ctx=ctx, form=form,
)
@app.route('/org/contest/r/<int:id>/task/<int:task_id>/edit', methods=('GET', 'POST'))
def org_round_task_edit(id: int, task_id: int):
@app.route('/org/contest/r/<int:round_id>/task/<int:task_id>/edit', methods=('GET', 'POST'))
def org_round_task_edit(round_id: int, task_id: int):
sess = db.get_session()
round, master_round, _ = get_round_rr(id, Right.manage_round, True)
ctx = get_context(round_id=round_id, task_id=task_id, right_needed=Right.manage_round)
task = ctx.task
assert task
task = sess.query(db.Task).get(task_id)
# FIXME: Check contest!
if not task:
raise werkzeug.exceptions.NotFound()
form = TaskEditForm(master_round.points_step, obj=task)
form = TaskEditForm(ctx.master_round.points_step, obj=task)
if form.validate_on_submit():
if sess.query(db.Task).filter(
db.Task.task_id != task_id, db.Task.round_id == id, db.Task.code == form.code.data
db.Task.task_id != task_id, db.Task.round_id == round_id, db.Task.code == form.code.data
).first():
flash('Úloha se stejným kódem již v tomto kole existuje', 'danger')
else:
......@@ -316,109 +354,16 @@ def org_round_task_edit(id: int, task_id: int):
app.logger.info(f"Úloha {task.code} ({task_id}) modifikována, změny: {changes}")
flash('Změny úlohy 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_round', id=id))
return redirect(ctx.url_for('org_round', task_id=None))
return render_template(
'org_round_task_edit.html',
round=round, task=task, form=form,
ctx=ctx, form=form,
)
@app.route('/org/contest/r/<int:round_id>/task/<int:task_id>/download', methods=('GET', 'POST'))
def org_round_task_download(round_id: int, task_id: int):
round, _, _ = get_round_rr(round_id, Right.view_submits, False)
task = get_task(round, task_id)
return generic_batch_download(round=round, contest=None, site=None, task=task)
@app.route('/org/contest/r/<int:round_id>/task/<int:task_id>/upload', methods=('GET', 'POST'))
def org_round_task_upload(round_id: int, task_id: int):
round, _, rr = get_round_rr(round_id, Right.view_submits, False)
task = get_task(round, task_id)
return generic_batch_upload(round=round, contest=None, site=None, task=task,
offer_upload_solutions=rr.offer_upload_solutions(),
offer_upload_feedback=rr.offer_upload_feedback())
@app.route('/org/contest/r/<int:round_id>/task/<int:task_id>/batch-points', methods=('GET', 'POST'))
def org_round_task_batch_points(round_id: int, task_id: int):
round, _, _ = get_round_rr(round_id, Right.edit_points, True)
task = get_task(round, task_id)
return generic_batch_points(round=round, contest=None, task=task)
@app.route('/org/contest/r/<int:id>/list', methods=('GET', 'POST'))
@app.route('/org/contest/r/<int:id>/list/emails', endpoint="org_round_list_emails")
def org_round_list(id: int):
round, master_round, rr = get_round_rr(id, Right.view_contestants, True)
can_edit = rr.have_right(Right.manage_round) and request.endpoint != 'org_round_list_emails'
format = request.args.get('format', "")
filter = ParticipantsFilterForm(request.args)
filter.validate()
query = get_contestants_query(
round=master_round,
school=filter.f_school,
contest_place=filter.f_contest_place,
participation_place=filter.f_participation_place,
participation_state=filter.f_participation_state,
)
action_form = None
if can_edit:
action_form = ParticipantsActionForm()
if action_form.do_action(round=master_round, query=query):
# Action happened, redirect
return redirect(request.url)
if format == "":
table = None
emails = None
mailto_link = None
if request.endpoint == 'org_round_list_emails':
(emails, mailto_link) = get_contestant_emails(query,
mailto_subject=f'{round.name} kategorie {round.category}')
count = len(emails)
else:
(count, query) = filter.apply_limits(query, pagesize=50)
# count = db.get_count(query)
table = make_contestant_table(query, round, add_contest_column=True, add_checkbox=True)
return render_template(
'org_round_list.html',
round=round,
table=table, emails=emails, mailto_link=mailto_link,
filter=filter, count=count, action_form=action_form,
)
else:
table = make_contestant_table(query, round, is_export=True)
return table.send_as(format)
@app.route('/org/contest/r/<int:id>/import', methods=('GET', 'POST'))
def org_round_import(id: int):
round, master_round, rr = get_round_rr(id, Right.manage_contest, True)
return generic_import(round, master_round, None, None)
class MODateTimeField(wtforms.DateTimeField):
def __init__(self, label, format='%Y-%m-%d %H:%M', description='Ve formátu 2000-01-01 12:34', **kwargs):
super().__init__(label, format=format, description=description, **kwargs)
def process_data(self, valuelist):
super().process_data(valuelist)
if self.data is not None:
self.data = self.data.astimezone()
def process_formdata(self, valuelist):
super().process_formdata(valuelist)
if self.data is not None:
self.data = self.data.astimezone()
class RoundEditForm(FlaskForm):
_for_round: Optional[db.Round] = None
......@@ -429,16 +374,16 @@ class RoundEditForm(FlaskForm):
)
# Only the desktop Firefox does not support datetime-local field nowadays,
# other browsers does provide date and time picker UI :(
ct_tasks_start = MODateTimeField("Čas zveřejnění úloh pro účastníky", validators=[validators.Optional()])
pr_tasks_start = MODateTimeField("Čas zveřejnění úloh pro dozor", validators=[validators.Optional()])
ct_submit_end = MODateTimeField("Konec odevzdávání pro účastníky", validators=[validators.Optional()])
pr_submit_end = MODateTimeField("Konec odevzdávání pro dozor", validators=[validators.Optional()])
ct_tasks_start = mo_fields.DateTime("Čas zveřejnění úloh pro účastníky", validators=[validators.Optional()])
pr_tasks_start = mo_fields.DateTime("Čas zveřejnění úloh pro dozor", validators=[validators.Optional()])
ct_submit_end = mo_fields.DateTime("Konec odevzdávání pro účastníky", validators=[validators.Optional()])
pr_submit_end = mo_fields.DateTime("Konec odevzdávání pro dozor", validators=[validators.Optional()])
score_mode = wtforms.SelectField("Výsledková listina", choices=db.RoundScoreMode.choices(), coerce=db.RoundScoreMode.coerce)
score_winner_limit = MODecimalField(
score_winner_limit = mo_fields.Points(
"Hranice bodů pro vítěze", validators=[validators.Optional(), validators.NumberRange(min=0)],
description="Řešitelé s alespoň tolika body budou označeni za vítěze, prázdná hodnota = žádné neoznačovat",
)
score_successful_limit = MODecimalField(
score_successful_limit = mo_fields.Points(
"Hranice bodů pro úspěšné řešitele", validators=[validators.Optional(), validators.NumberRange(min=0)],
description="Řešitelé s alespoň tolika body budou označeni za úspěšné řešitele, prázdná hodnota = žádné neoznačovat",
)
......@@ -446,6 +391,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')
......@@ -458,11 +405,23 @@ class RoundEditForm(FlaskForm):
if num_tasks == 0:
raise ValidationError('Nejsou-li definovány žádné úlohy, stav musí být "připravuje se"')
def abstract_validate_time_order(self, field):
if field.data is not None:
if any([i.data is not None and i.data > field.data for i in [self.ct_tasks_start, self.pr_tasks_start]]):
raise ValidationError('Soutěž nesmí skončit dříve než začne.')
def validate_ct_submit_end(self, field):
self.abstract_validate_time_order(field)
def validate_pr_submit_end(self, field):
self.abstract_validate_time_order(field)
@app.route('/org/contest/r/<int:id>/edit', methods=('GET', 'POST'))
def org_round_edit(id: int):
@app.route('/org/contest/r/<int:round_id>/edit', methods=('GET', 'POST'))
def org_round_edit(round_id: int):
sess = db.get_session()
round, _, rr = get_round_rr(id, Right.manage_round, True)
ctx = get_context(round_id=round_id, right_needed=Right.manage_round)
round = ctx.round
form = RoundEditForm(obj=round)
form._for_round = round
......@@ -472,16 +431,18 @@ 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)
if sess.is_modified(round):
changes = db.get_object_changes(round)
app.logger.info(f"Round #{id} modified, changes: {changes}")
app.logger.info(f"Round #{round_id} modified, changes: {changes}")
mo.util.log(
type=db.LogType.round,
what=id,
what=round_id,
details={'action': 'edit', 'changes': changes},
)
......@@ -500,26 +461,27 @@ def org_round_edit(id: int):
sess.commit()
flash('Změny kola 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_round', id=id))
return redirect(ctx.url_for('org_round'))
return render_template(
'org_round_edit.html',
ctx=ctx,
round=round,
form=form,
)
@app.route('/org/contest/r/<int:id>/task-statement/zadani.pdf')
def org_task_statement(id: int):
round, _, rr = get_round_rr(id, None, True)
@app.route('/org/contest/r/<int:round_id>/task-statement/zadani.pdf')
def org_task_statement(round_id: int):
ctx = get_context(round_id=round_id)
if not rr.can_view_statement():
if not ctx.rights.can_view_statement():
app.logger.warn(f'Organizátor #{g.user.user_id} chce zadání, na které nemá právo')
raise werkzeug.exceptions.Forbidden()
return mo.web.util.send_task_statement(round)
return mo.web.util.send_task_statement(ctx.round)
class StatementEditForm(FlaskForm):
......@@ -528,18 +490,19 @@ class StatementEditForm(FlaskForm):
delete = wtforms.SubmitField('Smazat')
@app.route('/org/contest/r/<int:id>/task-statement/edit', methods=('GET', 'POST'))
def org_edit_statement(id: int):
@app.route('/org/contest/r/<int:round_id>/task-statement/edit', methods=('GET', 'POST'))
def org_edit_statement(round_id: int):
sess = db.get_session()
round, _, rr = get_round_rr(id, Right.manage_round, True)
ctx = get_context(round_id=round_id, right_needed=Right.manage_round)
round = ctx.round
def log_changes():
if sess.is_modified(round):
changes = db.get_object_changes(round)
app.logger.info(f"Kolo #{id} změněno, změny: {changes}")
app.logger.info(f"Kolo #{round_id} změněno, změny: {changes}")
mo.util.log(
type=db.LogType.round,
what=id,
what=round_id,
details={'action': 'edit', 'changes': changes},
)
......@@ -560,7 +523,7 @@ def org_edit_statement(id: int):
log_changes()
sess.commit()
flash('Zadání nahráno', 'success')
return redirect(url_for('org_round', id=id))
return redirect(ctx.url_for('org_round'))
else:
flash('Vyberte si prosím soubor', 'danger')
if form.delete.data:
......@@ -568,10 +531,11 @@ def org_edit_statement(id: int):
log_changes()
sess.commit()
flash('Zadání smazáno', 'success')
return redirect(url_for('org_round', id=id))
return redirect(ctx.url_for('org_round'))
return render_template(
'org_edit_statement.html',
ctx=ctx,
round=round,
form=form,
)
......@@ -593,38 +557,39 @@ class MessageRemoveForm(FlaskForm):
message_remove = wtforms.SubmitField()
@app.route('/org/contest/r/<int:id>/messages/', methods=('GET', 'POST'))
def org_round_messages(id: int):
@app.route('/org/contest/r/<int:round_id>/messages/', methods=('GET', 'POST'))
def org_round_messages(round_id: int):
sess = db.get_session()
round, _, rr = get_round_rr(id, None, True)
ctx = get_context(round_id=round_id)
round = ctx.round
if not round.has_messages:
flash('Toto kolo nemá aktivní zprávičky pro účastníky, aktivujte je v nastavení kola', 'warning')
return redirect(url_for('org_round', id=id))
return redirect(ctx.url_for('org_round'))
messages = sess.query(db.Message).filter_by(round_id=id).order_by(db.Message.created_at).all()
messages = sess.query(db.Message).filter_by(round_id=round_id).order_by(db.Message.created_at).all()
add_form: Optional[MessageAddForm] = None
remove_form: Optional[MessageRemoveForm] = None
preview: Optional[db.Message] = None
if rr.have_right(Right.manage_round):
if ctx.rights.have_right(Right.manage_round):
add_form = MessageAddForm()
remove_form = MessageRemoveForm()
if remove_form.validate_on_submit() and remove_form.message_remove.data:
msg = sess.query(db.Message).get(remove_form.message_id.data)
if not msg or msg.round_id != id:
if not msg or msg.round_id != round_id:
raise werkzeug.exceptions.NotFound()
sess.delete(msg)
sess.commit()
app.logger.info(f"Zprávička pro kolo {id} odstraněna: {db.row2dict(msg)}")
app.logger.info(f"Zprávička pro kolo {round_id} odstraněna: {db.row2dict(msg)}")
flash('Zprávička odstraněna', 'success')
return redirect(url_for('org_round_messages', id=id))
return redirect(ctx.url_for('org_round_messages'))
if add_form.validate_on_submit():
msg = db.Message(
round_id=id,
round_id=round_id,
created_by=g.user.user_id,
created_at=mo.now,
)
......@@ -639,14 +604,15 @@ def org_round_messages(id: int):
elif add_form.submit.data:
sess.add(msg)
sess.commit()
app.logger.info(f"Vložena nová zprávička pro kolo {id}: {db.row2dict(msg)}")
app.logger.info(f"Vložena nová zprávička pro kolo {round_id}: {db.row2dict(msg)}")
flash('Zprávička úspěšně vložena', 'success')
return redirect(url_for('org_round_messages', id=id))
return redirect(ctx.url_for('org_round_messages'))
return render_template(
'org_round_messages.html',
round=round, rr=rr, messages=messages,
ctx=ctx,
round=round, messages=messages,
add_form=add_form, remove_form=remove_form,
preview=preview,
)
......@@ -9,6 +9,7 @@ import mo.db as db
from mo.rights import Right
from mo.score import Score
from mo.web import app
from mo.web.org_contest import get_context
from mo.web.table import Cell, CellLink, Column, Row, Table, cell_pion_link
from mo.util_format import format_decimal
......@@ -42,11 +43,13 @@ class SolPointsCell(Cell):
contest_id: int
user: db.User
sol: Optional[db.Solution]
link_to_paper: bool
def __init__(self, contest_id: int, user: db.User, sol: Optional[db.Solution]):
def __init__(self, contest_id: int, user: db.User, sol: Optional[db.Solution], link_to_paper: bool):
self.contest_id = contest_id
self.user = user
self.sol = sol
self.link_to_paper = link_to_paper
def __str__(self) -> str:
if not self.sol:
......@@ -63,7 +66,9 @@ class SolPointsCell(Cell):
else:
points = format_decimal(self.sol.points)
if self.sol.final_feedback_obj:
if not self.link_to_paper:
return f'<td>{points}'
elif self.sol.final_feedback_obj:
url = mo.web.util.org_paper_link(self.contest_id, None, self.user, self.sol.final_feedback_obj)
return f'<td><a href="{url}" title="Zobrazit finální opravu">{points}</a>'
elif self.sol.final_submit_obj:
......@@ -74,34 +79,18 @@ class SolPointsCell(Cell):
@app.route('/org/contest/r/<int:round_id>/score')
@app.route('/org/contest/c/<int:contest_id>/score')
def org_score(round_id: Optional[int] = None, contest_id: Optional[int] = None):
if round_id is None and contest_id is None:
raise werkzeug.exceptions.BadRequest()
if round_id is not None and contest_id is not None:
raise werkzeug.exceptions.BadRequest()
@app.route('/org/contest/c/<int:ct_id>/score')
def org_score(round_id: Optional[int] = None, ct_id: Optional[int] = None):
ctx = get_context(round_id=round_id, ct_id=ct_id)
contest = ctx.contest
round = ctx.round
format = request.args.get('format', "")
sess = db.get_session()
if round_id:
contest = None
round = sess.query(db.Round).options(
joinedload(db.Round.master)
).get(round_id)
if not round:
raise werkzeug.exceptions.NotFound()
rr = g.gatekeeper.rights_for_round(round, True)
else:
contest = sess.query(db.Contest).options(
joinedload(db.Contest.round).joinedload(db.Round.master)
).get(contest_id)
if not contest:
raise werkzeug.exceptions.NotFound()
round = contest.round
rr = g.gatekeeper.rights_for_contest(contest)
if not rr.have_right(Right.view_submits):
if not ctx.rights.have_right(Right.view_contestants):
raise werkzeug.exceptions.Forbidden()
can_view_submits = ctx.rights.have_right(Right.view_submits)
score = Score(round.master, contest)
tasks = score.get_tasks()
......@@ -127,7 +116,7 @@ def org_score(round_id: Optional[int] = None, contest_id: Optional[int] = None):
columns.append(Column(key='participant', name='ucastnik', title='Účastník'))
if is_export:
columns.append(Column(key='email', name='email'))
if not contest_id:
if not ct_id:
columns.append(Column(key='contest', name='oblast', title=round.get_level().name.title()))
if is_export:
columns.append(Column(key='pion_place', name='soutezni_misto'))
......@@ -140,12 +129,12 @@ def org_score(round_id: Optional[int] = None, contest_id: Optional[int] = None):
if contest:
local_ct_id = subcontest_id_map[(task.round_id, contest.master_contest_id)]
title = '<a href="{}">{}</a>'.format(
url_for('org_contest_task', contest_id=local_ct_id, task_id=task.task_id),
url_for('org_contest_task', ct_id=local_ct_id, task_id=task.task_id),
task.code
)
if rr.can_edit_points():
if ctx.rights.can_edit_points():
title += ' <a href="{}" title="Editovat body" class="icon">✎</a>'.format(
url_for('org_contest_task_points', contest_id=local_ct_id, task_id=task.task_id),
url_for('org_contest_task_points', ct_id=local_ct_id, task_id=task.task_id),
)
columns.append(Column(key=f'task_{task.task_id}', name=task.code, title=title))
columns.append(Column(key='total_points', name='celkove_body', title='Celkové body'))
......@@ -177,7 +166,7 @@ def org_score(round_id: Optional[int] = None, contest_id: Optional[int] = None):
'user': user,
'email': user.email,
'participant': cell_pion_link(user, local_pion_ct_id, user.full_name()),
'contest': CellLink(pion.contest.place.name or "?", url_for('org_contest', id=pion.contest_id)),
'contest': CellLink(pion.contest.place.name or "?", url_for('org_contest', ct_id=pion.contest_id)),
'pion_place': pion.place.name,
'school': CellLink(school.name or "?", url_for('org_place', id=school.place_id)),
'grade': pant.grade,
......@@ -190,7 +179,8 @@ def org_score(round_id: Optional[int] = None, contest_id: Optional[int] = None):
for task in tasks:
local_sol_ct_id = subcontest_id_map[(task.round_id, pion.contest_id)]
row.keys[f'task_{task.task_id}'] = SolPointsCell(
contest_id=local_sol_ct_id, user=user, sol=sols.get(task.task_id)
contest_id=local_sol_ct_id, user=user, sol=sols.get(task.task_id),
link_to_paper=can_view_submits
)
if result.winner:
row.html_attr = {"class": "winner", "title": "Vítěz"}
......@@ -214,6 +204,7 @@ def org_score(round_id: Optional[int] = None, contest_id: Optional[int] = None):
if format == "":
return render_template(
'org_score.html',
ctx=ctx,
contest=contest, round=round, tasks=tasks,
table=table, messages=messages,
group_rounds=group_rounds,
......
from typing import Optional
from typing import Optional, List
from flask import render_template, g, redirect, url_for, flash, request
from flask_wtf import FlaskForm
import werkzeug.exceptions
import wtforms
from sqlalchemy import or_
import flask_sqlalchemy
from sqlalchemy.orm import joinedload, subqueryload
from wtforms import validators
......@@ -13,10 +14,12 @@ 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
from mo.web import app
import mo.web.fields as mo_fields
from mo.web.util import PagerForm
......@@ -26,57 +29,25 @@ class UsersFilterForm(PagerForm):
search_email = wtforms.TextField("E-mail")
# participants
year = wtforms.IntegerField("Ročník")
school_code = wtforms.StringField("Škola")
year = mo_fields.OptionalInt("Ročník")
school = mo_fields.School()
# rounds->participations
round_year = wtforms.IntegerField("Ročník")
round_year = mo_fields.OptionalInt("Ročník")
round_category = wtforms.SelectField("Kategorie")
round_seq = wtforms.SelectField("Kolo")
contest_site_code = wtforms.StringField("Soutěžní oblast")
contest_site = mo_fields.Place("Soutěžní oblast")
participation_state = wtforms.SelectField('Účast', choices=[('*', '*')] + list(db.PartState.choices()))
submit = wtforms.SubmitField("Filtrovat")
# Výstupní hodnoty filtru, None při nepoužitém filtru, prázdná db hodnota při
# nepovedené filtraci (neexistující místo a podobně)
f_search_name: Optional[str] = None
f_search_email: Optional[str] = None
f_year: Optional[int] = None
f_school: Optional[db.Place] = None
f_round_category: Optional[str] = None
f_round_seq: Optional[int] = None
f_contest_site: Optional[db.Place] = None
f_participation_state: Optional[db.PartState] = None
def __init__(self, formdata, **kwargs):
super().__init__(formdata=formdata, **kwargs)
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.round_category.choices = ['*'] + sorted(db.get_categories())
self.round_seq.choices = ['*'] + sorted(db.get_seqs())
def validate(self):
self.f_search_name = f"%{self.search_name.data}%" if self.search_name.data else None
self.f_search_email = f"%{self.search_email.data}%" if self.search_email.data else None
self.f_year = self.year.data
self.f_round_year = self.round_year.data
if self.school_code.data:
self.f_school = db.get_place_by_code(self.school_code.data)
if not self.f_school:
flash(f"Zadaná škola '{self.school_code.data}' neexistuje", "danger")
self.f_school = db.Place()
if self.contest_site_code.data:
self.f_contest_site = db.get_place_by_code(self.contest_site_code.data)
if not self.f_contest_site:
flash(f"Zadaná soutěžní oblast '{self.contest_site_code.data}' neexistuje", "danger")
self.f_contest_site = db.Place()
self.f_round_category = None if self.round_category.data == '*' else self.round_category.data
self.f_round_seq = None if self.round_seq.data == '*' else self.round_seq.data
self.f_participation_state = None if self.participation_state.data == '*' else self.participation_state.data
@app.route('/org/user/')
@app.route('/org/user/', methods=('GET', 'POST'))
def org_users():
sess = db.get_session()
rr = g.gatekeeper.rights_generic()
......@@ -84,32 +55,33 @@ def org_users():
q = sess.query(db.User).filter_by(is_admin=False, is_org=False).options(
subqueryload(db.User.participants).joinedload(db.Participant.school_place)
)
filter = UsersFilterForm(request.args)
filter = UsersFilterForm(formdata=request.args)
if request.args:
filter.validate()
if filter.f_search_name:
if filter.search_name.data:
q = q.filter(or_(
db.User.first_name.ilike(filter.f_search_name),
db.User.last_name.ilike(filter.f_search_name)
db.User.first_name.ilike(f"%{filter.search_name.data}%"),
db.User.last_name .ilike(f"%{filter.search_name.data}%")
))
if filter.f_search_email:
q = q.filter(db.User.email.ilike(filter.f_search_email))
if filter.search_email.data:
q = q.filter(db.User.email.ilike(f"%{filter.search_email.data}%"))
if filter.f_year or filter.f_school:
if filter.year.data or filter.school.place:
participant_filter = sess.query(db.Participant.user_id)
if filter.f_year:
participant_filter = participant_filter.filter_by(year=filter.f_year)
if filter.f_school:
participant_filter = participant_filter.filter_by(school=filter.f_school.place_id)
if filter.year.data:
participant_filter = participant_filter.filter_by(year=filter.year.data)
if filter.school.place:
participant_filter = participant_filter.filter_by(school=filter.school.place.place_id)
q = q.filter(db.User.user_id.in_(participant_filter))
round_filter = sess.query(db.Round.round_id)
round_filter_apply = False
if filter.f_round_year:
round_filter = round_filter.filter_by(year=filter.f_round_year)
if filter.round_year.data:
round_filter = round_filter.filter_by(year=filter.round_year.data)
round_filter_apply = True
if filter.f_round_category:
round_filter = round_filter.filter_by(category=filter.f_round_category)
if filter.round_category.data and filter.round_category.data != "*":
round_filter = round_filter.filter_by(category=filter.round_category.data)
round_filter_apply = True
if filter.round_seq.data and filter.round_seq.data != "*":
round_filter = round_filter.filter_by(seq=filter.round_seq.data)
......@@ -120,8 +92,8 @@ def org_users():
if round_filter_apply:
contest_filter = contest_filter.filter(db.Contest.round_id.in_(round_filter))
contest_filter_apply = True
if filter.f_contest_site:
contest_filter = contest_filter.filter_by(place_id=filter.f_contest_site.place_id)
if filter.contest_site.place:
contest_filter = contest_filter.filter_by(place_id=filter.contest_site.place.place_id)
contest_filter_apply = True
participation_filter = sess.query(db.Participation.user_id)
......@@ -129,8 +101,8 @@ def org_users():
if contest_filter_apply:
participation_filter = participation_filter.filter(db.Participation.contest_id.in_(contest_filter))
participation_filter_apply = True
if filter.f_participation_state:
participation_filter = participation_filter.filter_by(state=filter.f_participation_state)
if filter.participation_state.data and filter.participation_state.data != '*':
participation_filter = participation_filter.filter_by(state=filter.participation_state.data)
participation_filter_apply = True
if participation_filter_apply:
......@@ -153,44 +125,101 @@ class OrgsFilterForm(PagerForm):
search_name = wtforms.TextField("Jméno/příjmení", render_kw={'autofocus': True})
search_email = wtforms.TextField("E-mail")
# TODO: filtering by roles?
submit = wtforms.SubmitField("Filtrovat")
# Výstupní hodnoty filtru, None při nepoužitém filtru, prázdná db hodnota při
# nepovedené filtraci (neexistující místo a podobně)
f_search_name: Optional[str] = None
f_search_email: Optional[str] = None
search_role = wtforms.SelectMultipleField('Role', choices=db.RoleType.choices(), coerce=db.RoleType.coerce, validators=[validators.Optional()])
search_right_for_place = mo_fields.Place('Právo pro oblast', validators=[validators.Optional()])
search_in_place = mo_fields.Place('V oblasti', validators=[validators.Optional()])
search_place_level = wtforms.SelectMultipleField("Úroveň oblasti", choices=[(i.level, i.name) for i in db.place_levels], validators=[validators.Optional()], coerce=int)
search_year = mo_fields.IntList('Ročník', validators=[validators.Optional()])
search_category = wtforms.StringField("Kategorie", validators=[validators.Optional()])
search_seq = mo_fields.IntList("Kolo", validators=[validators.Optional()])
def validate(self):
self.f_search_name = f"%{self.search_name.data}%" if self.search_name.data else None
self.f_search_email = f"%{self.search_email.data}%" if self.search_email.data else None
submit = wtforms.SubmitField("Filtrovat")
show_role_filter = wtforms.SubmitField("Zobrazit filtrování dle rolí")
hide_role_filter = wtforms.SubmitField("Skrýt filtrování dle rolí")
is_role_filter = wtforms.HiddenField(default="") # "" -> skrýt. "yes" -> zobrazit
def prepare_role_filter(self):
if self.show_role_filter.data:
self.is_role_filter.data = "yes"
if self.hide_role_filter.data:
self.is_role_filter.data = ""
if self.is_role_filter.data:
del self.show_role_filter
else:
del self.hide_role_filter
del self.search_role
del self.search_right_for_place
del self.search_in_place
del self.search_place_level
del self.search_year
del self.search_category
del self.search_seq
@app.route('/org/org/')
@app.route('/org/org/', methods=('GET', 'POST'))
def org_orgs():
sess = db.get_session()
rr = g.gatekeeper.rights_generic()
q = sess.query(db.User).filter(or_(db.User.is_admin, db.User.is_org)).options(
subqueryload(db.User.roles).joinedload(db.UserRole.place)
)
filter = OrgsFilterForm(request.args)
filter = OrgsFilterForm(formdata=request.args)
if request.args:
filter.validate()
filter.prepare_role_filter()
if filter.f_search_name:
if filter.search_name.data:
q = q.filter(or_(
db.User.first_name.ilike(filter.f_search_name),
db.User.last_name.ilike(filter.f_search_name)
db.User.first_name.ilike(f"%{filter.search_name.data}%"),
db.User.last_name .ilike(f"%{filter.search_name.data}%")
))
if filter.f_search_email:
q = q.filter(db.User.email.ilike(filter.f_search_email))
if filter.search_email.data:
q = q.filter(db.User.email.ilike(f"%{filter.search_email.data}%"))
def query_filter_role(qr: flask_sqlalchemy.BaseQuery) -> flask_sqlalchemy.BaseQuery:
if filter.search_role.data:
qr = qr.filter(db.UserRole.role.in_(filter.search_role.data))
if filter.search_category.data:
qr = qr.filter(or_(db.UserRole.category.in_(filter.search_category.data.split(",")), db.UserRole.category == None))
if filter.search_seq.list:
qr = qr.filter(or_(db.UserRole.seq.in_(filter.search_seq.list), db.UserRole.seq == None))
if filter.search_year.list:
qr = qr.filter(or_(db.UserRole.year.in_(filter.search_year.list), db.UserRole.year == None))
pass
if filter.search_in_place.place is not None:
qr = qr.filter(db.UserRole.place_id.in_(db.place_descendant_cte(filter.search_in_place.place)))
if filter.search_right_for_place.place is not None:
qr = qr.filter(db.UserRole.place_id.in_([x.place_id for x in db.get_place_ancestors(filter.search_right_for_place.place)]))
# Po n>3 hodinách v mo.db jsem dospěl k závěru, že to hezčeji neumím (neumím vyrobit place_parents_cte)
if filter.search_place_level.data:
qr = qr.filter(db.UserRole.place_id.in_(
sess.query(db.Place.place_id).filter(db.Place.level.in_(filter.search_place_level.data))
))
print(qr)
return qr
if filter.is_role_filter.data:
qr = sess.query(db.UserRole.user_id)
qr = query_filter_role(qr)
q = q.filter(db.User.user_id.in_(qr))
q = q.order_by(db.User.user_id)
(count, q) = filter.apply_limits(q, pagesize=50)
users = q.all()
marked_roles_id: Set[int] = set()
if filter.is_role_filter.data:
qmr = sess.query(db.UserRole.user_role_id).filter(db.UserRole.user_id.in_([i.user_id for i in users]))
qmr = query_filter_role(qmr)
marked_roles_id = set([i[0] for i in qmr.all()])
return render_template(
'org_orgs.html', users=users, count=count,
filter=filter,
marked_roles_id=marked_roles_id,
can_edit=rr.have_right(Right.edit_orgs),
can_add=rr.have_right(Right.add_orgs),
)
......@@ -198,7 +227,7 @@ def org_orgs():
class FormAddRole(FlaskForm):
role = wtforms.SelectField('Role', choices=db.RoleType.choices(), coerce=db.RoleType.coerce, render_kw={'autofocus': True})
place_code = wtforms.StringField('Oblast')
place = mo_fields.Place()
year = wtforms.IntegerField('Ročník', validators=[validators.Optional()])
category = wtforms.StringField("Kategorie", validators=[validators.Length(max=2)], filters=[lambda x: x or None])
seq = wtforms.IntegerField("Kolo", validators=[validators.Optional()])
......@@ -215,14 +244,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'))
......@@ -238,7 +268,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)
......@@ -257,14 +287,7 @@ def org_org(id: int):
new_role.assigned_by = g.user.user_id
ok = True
place_code = form_add_role.place_code.data
if place_code:
place = db.get_place_by_code(place_code)
if not place:
role_errors.append("Nepovedlo se nalézt místo podle kódu")
ok = False
else:
new_role.place = place
new_role.place = form_add_role.place.place
if not g.gatekeeper.can_set_role(new_role):
role_errors.append(f'Roli "{new_role}" nelze přidělit, není podmnožinou žádné vaší role')
......@@ -324,7 +347,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)
......@@ -351,19 +374,14 @@ def org_user(id: int):
class UserEditForm(FlaskForm):
first_name = wtforms.StringField("Jméno", validators=[Required()], render_kw={'autofocus': True})
last_name = wtforms.StringField("Příjmení", validators=[Required()])
email = wtforms.StringField("E-mail", validators=[Required()])
first_name = mo_fields.FirstName(validators=[Required()], render_kw={'autofocus': True})
last_name = mo_fields.LastName(validators=[Required()])
email = mo_fields.Email(validators=[Required()])
note = wtforms.TextAreaField("Poznámka")
is_test = wtforms.BooleanField("Testovací účet")
allow_duplicate_name = wtforms.BooleanField("Přidat účet s duplicitním jménem")
submit = wtforms.SubmitField("Uložit")
def validate_email(form, field):
try:
field.data = mo.users.normalize_email(field.data)
except mo.CheckError as e:
raise wtforms.ValidationError(str(e))
@app.route('/org/org/<int:id>/edit', methods=("GET", "POST"), endpoint="org_org_edit")
@app.route('/org/user/<int:id>/edit', methods=("GET", "POST"))
......@@ -385,6 +403,7 @@ def org_user_edit(id: int):
raise werkzeug.exceptions.Forbidden()
form = UserEditForm(obj=user)
del form.allow_duplicate_name
if (user.is_org or user.is_admin) and not g.user.is_admin:
# emaily u organizátorů může editovat jen správce
del form.email
......@@ -411,7 +430,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))
......@@ -433,6 +452,7 @@ def org_user_new():
form = UserEditForm()
form.submit.label.text = 'Vytvořit'
is_duplicate_name = False
if form.validate_on_submit():
check = True
......@@ -440,6 +460,15 @@ def org_user_new():
flash('Účet s daným e-mailem již existuje', 'danger')
check = False
if is_org:
if (mo.db.get_session().query(db.User)
.filter_by(first_name=form.first_name.data, last_name=form.last_name.data, is_org=True)
.first() is not None):
is_duplicate_name = True
if not form.allow_duplicate_name.data:
flash('Organizátor s daným jménem již existuje. V případě, že se nejedná o chybu, zaškrtněte políčko ve formuláři.', 'danger')
check = False
if check:
new_user = db.User()
form.populate_obj(new_user)
......@@ -454,20 +483,63 @@ 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))
return redirect(url_for('org_user', id=new_user.user_id))
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)
......@@ -210,7 +210,7 @@ def cell_user_link(user: db.User, text: str) -> CellLink:
def cell_pion_link(user: db.User, contest_id: int, text: str) -> CellLink:
return CellLink(text, url_for('org_contest_user', contest_id=contest_id, user_id=user.user_id))
return CellLink(text, url_for('org_contest_user', ct_id=contest_id, user_id=user.user_id))
def cell_place_link(place: db.Place, text: str) -> CellLink:
......
{% 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>
......