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')
......
This diff is collapsed.
......@@ -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))
......
This diff is collapsed.
......@@ -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,
......
This diff is collapsed.
......@@ -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>
......