Select Git revision
org_round.py
-
Jiří Kalvoda authoredJiří Kalvoda authored
org_place.py 13.75 KiB
from flask import render_template, g, redirect, url_for, flash, request
from flask_wtf import FlaskForm
import locale
from markupsafe import Markup
from sqlalchemy.orm import joinedload
from typing import List, Optional
import werkzeug.exceptions
import wtforms
import mo
import mo.db as db
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
@app.route('/org/place/<int:id>/')
def org_place(id: int):
sess = db.get_session()
place = sess.query(db.Place).get(id)
if not place:
raise werkzeug.exceptions.NotFound()
if place.type == db.PlaceType.school:
school = sess.query(db.School).get(place.place_id)
else:
school = None
children = sorted(place.children, key=lambda p: locale.strxfrm(p.name))
rr = g.gatekeeper.rights_for(place)
contests = (sess.query(db.Contest)
.options(joinedload(db.Contest.round))
.filter_by(place=place)
.all())
contests.sort(key=lambda c: (-c.round.year, c.round.category, c.round.seq, c.round.part))
return render_template(
'org_place.html', place=place, school=school,
can_edit=rr.can_edit_place(place),
can_add_child=rr.can_add_place_child(place),
children=children,
contests=contests
)
class PlaceEditForm(FlaskForm):
name = wtforms.StringField(
'Název', render_kw={'autofocus': True},
validators=[validators.DataRequired()]
)
code = wtforms.StringField(
'Kód', filters=[lambda x: x or None], # may be NULL in db
description="Na místo se lze odkazovat kódem z písmen a číslic."
)
type = wtforms.SelectField(
'Typ', choices=db.PlaceType.choices(), coerce=db.PlaceType.coerce
)
nuts = wtforms.StringField(
'NUTS', filters=[lambda x: x or None], # may be NULL in db
description="Pro okresy a výše"
)
note = wtforms.StringField('Poznámka')
submit = wtforms.SubmitField('Uložit')
class PlaceSchoolEditForm(PlaceEditForm):
red_izo = wtforms.StringField('RED_IZO')
ico = wtforms.StringField('IČO')
official_name = wtforms.StringField('Oficiální název')
address = wtforms.StringField('Adresa')
is_zs = wtforms.BooleanField('ZŠ')
is_ss = wtforms.BooleanField('SŠ')
submit = wtforms.SubmitField('Uložit')
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:
elements.append((url_for('org_place', id=parent.place_id), parent.name))
if action:
elements.append(('', action))
if len(elements) == 0:
return ""
return Markup(
"\n".join([f"<li><a href='{url}'>{name}</a>" for url, name in elements[:-1]])
+ "<li>" + elements[-1][1]
)
@app.route('/org/place/<int:id>/edit', methods=('GET', 'POST'))
def org_place_edit(id: int):
sess = db.get_session()
place = sess.query(db.Place).get(id)
if not place:
raise werkzeug.exceptions.NotFound()
rr = g.gatekeeper.rights_for(place)
if not rr.can_edit_place(place):
raise werkzeug.exceptions.Forbidden()
if place.type == db.PlaceType.school:
school = sess.query(db.School).get(place.place_id)
# Pass school data as additional dict (data is used after obj)
form = PlaceSchoolEditForm(obj=place, data=db.row2dict(school))
form.name.description = ('Název školy tak, jak se má objevovat ve výsledkové listině. Viz '
+ Markup('<a href="' + url_for('doc_garant') + '#kodskoly">pojmenovací konvence</a>.'))
else:
form = PlaceEditForm(obj=place)
school = None
form.code.description += f' Kromě zadaného kódu funguje též #{id}.'
form.type.choices = db.PlaceType.choices(level=place.level)
if form.validate_on_submit():
form.populate_obj(place)
if school:
form.populate_obj(school)
msg = 'Změny místa uloženy'
redirectURL = url_for('org_place', id=id)
if sess.is_modified(place) or school and sess.is_modified(school):
placeChanges = db.get_object_changes(place)
schoolChanges = {}
if school:
if request.form.get('type') != 'school':
# School record removed
mo.util.log(
type=db.LogType.place,
what=school.place_id,
details={'action': 'school-delete', 'school': db.row2dict(school)},
)
app.logger.info(f"Deleting school record for place {place.place_id}")
db.get_session().delete(school)
msg = 'Změny místa uloženy, záznam o škole smazán'
else:
schoolChanges = db.get_object_changes(school)
elif request.form.get('type') == 'school':
# School record created
new_school = db.School()
new_school.place_id = place.place_id
mo.util.log(
type=db.LogType.place,
what=new_school.place_id,
details={'action': 'school-add'},
)
app.logger.info(f"Creating new school for place {place.place_id}")
db.get_session().add(new_school)
# Take org directly to the school edit to fill the data
msg = 'Záznam o škole vytvořen, vyplňte prosím všechna data'
redirectURL = url_for('org_place_edit', id=id)
changes = {**placeChanges, **schoolChanges}
app.logger.info(f"Place {id} modified, changes: {changes}")
mo.util.log(
type=db.LogType.place,
what=id,
details={'action': 'edit', 'changes': changes},
)
db.get_session().commit()
flash(msg, 'success')
else:
flash(u'Žádné změny k uložení', 'info')
return redirect(redirectURL)
return render_template(
'org_place_edit.html', place=place, school=school,
form=form,
)
class PlaceMoveForm(FlaskForm):
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):
new_parent = mo_fields.Place(widget = wtforms.widgets.HiddenInput())
reset = wtforms.SubmitField('Zrušit')
move = wtforms.SubmitField('Přesunout')
@app.route('/org/place/<int:id>/move', methods=('GET', 'POST'))
def org_place_move(id: int):
sess = db.get_session()
# Tests: can move only existing places that we can edit
place = sess.query(db.Place).get(id)
if not place:
raise werkzeug.exceptions.NotFound()
rr = g.gatekeeper.rights_for(place)
if not rr.can_edit_place(place):
raise werkzeug.exceptions.Forbidden()
new_parents = None
search_error = None
form = PlaceMoveForm()
form_confirm = None
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 = form.new_parent.place
new_parents = reversed(g.gatekeeper.get_parents(new_parent))
(_, levels) = db.place_type_names_and_levels[place.type]
rr = g.gatekeeper.rights_for(new_parent)
if not rr.can_edit_place(new_parent):
search_error = 'Nemáte právo k editaci vybraného nadřazeného místa, přesun nelze uskutečnit'
elif (new_parent.level + 1) not in levels:
search_error = f'Toto místo ({place.type_name()}) nelze přemístit pod vybrané místo ({new_parent.type_name()}), dostalo by se na nepovolený level'
elif new_parent.place_id == place.parent:
search_error = 'Žádná změna, místo je zde již umístěno'
elif form.move.data:
# Everything is OK, if submitted with 'move' do the move
place.parent = new_parent.place_id
place.level = new_parent.level + 1
changes = db.get_object_changes(place)
mo.util.log(
type=db.LogType.place,
what=id,
details={'action': 'move', 'changes': changes},
)
app.logger.info(f"Place {id} moved, changes: {changes}")
db.get_session().commit()
flash('Místo úspěšně přesunuto', 'success')
return redirect(url_for('org_place', id=id))
else:
# OK but not confirmed yet, display the confirm form
form_confirm = PlaceMoveConfirmForm()
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',
place=place, form=form, form_confirm=form_confirm, search_error=search_error,
new_parents=new_parents
)
@app.route('/org/place/<int:id>/delete', methods=('POST',))
def org_place_delete(id: int):
sess = db.get_session()
# Tests: can delete only existing places that we can edit
place = sess.query(db.Place).get(id)
if not place:
raise werkzeug.exceptions.NotFound()
rr = g.gatekeeper.rights_for(place)
if not rr.can_edit_place(place):
raise werkzeug.exceptions.Forbidden()
# Cannot delete place with children
if place.children:
flash("Nelze smazat místo s podřízenými místy", "danger")
return redirect(url_for('org_place', id=id))
# Cannot delete place with contests
if db.get_count(sess.query(db.Contest).filter_by(place_id=id)) > 0:
flash("Nelze smazat místo ke kterému se váže nějaká soutěž ", "danger")
return redirect(url_for('org_place', id=id))
if place.type == db.PlaceType.school:
school = sess.query(db.School).get(place.place_id)
mo.util.log(
type=db.LogType.place,
what=school.place_id,
details={'action': 'school-delete', 'school': db.row2dict(school)},
)
app.logger.info(f"Deleting school record for place {id}")
db.get_session().delete(school)
mo.util.log(
type=db.LogType.place,
what=id,
details={'action': 'delete', 'place': db.row2dict(place)},
)
app.logger.info(f"Deleting place {id}")
parent = place.parent
db.get_session().delete(place)
db.get_session().commit()
flash("Místo smazáno", "success")
return redirect(url_for('org_place', id=parent))
@app.route('/org/place/<int:id>/new-child', methods=('GET', 'POST'))
def org_place_new_child(id: int):
sess = db.get_session()
# Tests: can add new child only under existing places that we can edit
parent_place = sess.query(db.Place).get(id)
if not parent_place:
raise werkzeug.exceptions.NotFound()
rr = g.gatekeeper.rights_for(parent_place)
if not rr.can_edit_place(parent_place):
raise werkzeug.exceptions.Forbidden()
if not parent_place.can_have_child():
raise werkzeug.exceptions.Forbidden()
form = PlaceEditForm()
form.type.choices = db.PlaceType.choices(level=parent_place.level + 1)
if form.validate_on_submit():
new_place = db.Place()
form.populate_obj(new_place)
new_place.parent = parent_place.place_id
new_place.level = parent_place.level + 1
sess.add(new_place)
sess.flush()
app.logger.info(f"New place created: {db.row2dict(new_place)}")
mo.util.log(
type=db.LogType.place,
what=new_place.place_id,
details={'action': 'new', 'place': db.row2dict(new_place)},
)
redirect_url = url_for('org_place', id=new_place.place_id)
msg = 'Nové místo uloženo'
if new_place.type == db.PlaceType.school:
new_school = db.School()
new_school.place_id = new_place.place_id
mo.util.log(
type=db.LogType.place,
what=new_school.place_id,
details={'action': 'school-add'},
)
app.logger.info(f"Creating new school for place {new_place.place_id}")
sess.add(new_school)
# Take org directly to the school edit to fill the data
msg = 'Záznam o škole vytvořen, vyplňte prosím všechna data'
redirect_url = url_for('org_place_edit', id=new_place.place_id)
sess.commit()
flash(msg, 'success')
return redirect(redirect_url)
return render_template('org_place_new.html', parent_place=parent_place, form=form)
@app.route('/org/place/')
def org_place_root():
root = db.get_root_place()
return redirect(url_for('org_place', id=root.place_id))
@app.route('/org/place/<int:id>/rights')
def org_place_rights(id: int):
sess = db.get_session()
place = sess.query(db.Place).get(id)
if not place:
raise werkzeug.exceptions.NotFound()
parent_ids = [p.place_id for p in g.gatekeeper.get_parents(place)]
roles = (sess.query(db.UserRole)
.filter(db.UserRole.place_id.in_(parent_ids))
.options(joinedload(db.UserRole.user))
.all())
roles.sort(key=lambda r: (mo.rights.role_order_by_type[r.role], r.user.sort_key()))
rr = g.gatekeeper.rights_for(place)
rights = sorted(rr.rights, key=lambda r: r.name)
return render_template(
'org_place_rights.html', place=place, rights=rights,
roles=roles, roles_by_type=mo.rights.roles_by_type
)