diff --git a/mo/db.py b/mo/db.py index 2ecf598842c882bc43caedc891a6eaf24ff85cc8..f331c08a1e0ebd02fe779e8a5652f01450bf5793 100644 --- a/mo/db.py +++ b/mo/db.py @@ -129,6 +129,9 @@ class Place(Base): def get_code(self): return self.code or '#' + str(self.place_id) + def name_or_id(self): + return self.name or '#' + str(self.place_id) + def can_have_child(self): return len(PlaceType.choices(level=self.level + 1)) > 0 diff --git a/mo/web/org.py b/mo/web/org.py index 377f6ab34ef03b416f75c114c107405cb4ef6502..66d2e12fea8295dee2ba5d2c51b23c9d85b16048 100644 --- a/mo/web/org.py +++ b/mo/web/org.py @@ -1,6 +1,7 @@ from collections import defaultdict from dataclasses import dataclass, field from flask import render_template, redirect, url_for, request, flash, g +import re from sqlalchemy import and_, or_, tuple_, not_ from sqlalchemy.orm import aliased, joinedload from typing import List, Set, Optional, Tuple, DefaultDict @@ -39,15 +40,11 @@ def org_index(): else: flash(f'Místo s kódem {code} neexistuje', 'danger') - if 'uid' in request.args: - try: - uid = int(request.args['uid']) - user = mo.users.user_by_uid(uid) - if user is not None: - return redirect(user_url(user)) - flash(f'Uživatel s ID {uid} neexistuje', 'danger') - except ValueError: - flash('ID uživatele musí být číslo', 'danger') + # Univerzální hledátko pro správce + if 'search' in request.args: + search_result = magic_search(request.args['search']) + if search_result is not None: + return search_result # Soutěže, ke kterým máme nějakou roli sess = db.get_session() @@ -220,6 +217,35 @@ school_export_columns = ( ) +def magic_search(query: str): + search = re.fullmatch(r'([cpru])(\d{1,9})', request.args['search']) + if not search: + flash('Chybná syntaxe dotazu', 'danger') + return None + what, id = search[1], int(search[2]) + sess = db.get_session() + + if what == 'c': + contest = sess.query(db.Contest).get(id) + if contest is not None: + return redirect(url_for('org_contest', ct_id=id)) + elif what == 'p': + place = sess.query(db.Place).get(id) + if place is not None: + return redirect(url_for('org_place', id=id)) + elif what == 'r': + round = sess.query(db.Round).get(id) + if round is not None: + return redirect(url_for('org_round', round_id=id)) + elif what == 'u': + user = mo.users.user_by_uid(id) + if user is not None: + return redirect(user_url(user)) + + flash('Nenalezeno', 'danger') + return None + + @app.route('/org/export/schools') def org_export_schools(): sess = db.get_session() diff --git a/mo/web/org_contest.py b/mo/web/org_contest.py index 552a569e0d0166feee1183de3b9367fe1fec764a..e81e6af9597b557348488a1b527ef3882ba9b336 100644 --- a/mo/web/org_contest.py +++ b/mo/web/org_contest.py @@ -1012,12 +1012,6 @@ def org_submit_list(ct_id: int, user_id: int, task_id: int, site_id: Optional[in ) -class SubmitEditForm(FlaskForm): - note = wtforms.TextAreaField("Poznámka pro účastníka", description="Viditelná účastníkovi po uzavření kola", render_kw={"rows": 8, 'autofocus': True}) - org_note = wtforms.TextAreaField("Interní poznámka", description="Viditelná jen organizátorům", render_kw={"rows": 8}) - submit = wtforms.SubmitField("Uložit") - - @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') diff --git a/mo/web/org_place.py b/mo/web/org_place.py index a04ca1885318c67824bc10a462612a42ff34e12c..a37e46bd16d191ea64427ed5d555912c4e7cb4f8 100644 --- a/mo/web/org_place.py +++ b/mo/web/org_place.py @@ -2,6 +2,7 @@ from flask import render_template, g, redirect, url_for, flash, request from flask_wtf import FlaskForm import locale from markupsafe import Markup +from sqlalchemy import func, and_ from sqlalchemy.orm import joinedload from typing import List, Optional import werkzeug.exceptions @@ -17,6 +18,11 @@ import mo.web.fields as mo_fields import wtforms.validators as validators +class PlaceSearchForm(FlaskForm): + query = mo_fields.String(render_kw={'autofocus': True, 'placeholder': 'Kód nebo části názvu'}) + submit = wtforms.SubmitField('Hledat') + + @app.route('/org/place/<int:id>/') def org_place(id: int): sess = db.get_session() @@ -25,6 +31,44 @@ def org_place(id: int): if not place: raise werkzeug.exceptions.NotFound() + # Formulář nemá side-efekty, takže to může být GET bez CSRF. + search_form = PlaceSearchForm(request.args, meta={'csrf': False}) + found_places = None + search_failed = False + search_limited = False + if 'submit' in request.args and search_form.validate(): + query = search_form.query.data + query_words = query.split() + + if len(query_words) == 1: + found = db.get_place_by_code(query_words[0]) + if found is not None: + flash('Nalezeno toto místo', 'info') + return redirect(url_for('org_place', id=found.place_id)) + + if len(query_words) > 0 and '%' not in query: + max_places = 100 + place_q = (sess.query(db.Place) + .filter(db.Place.place_id != place.place_id)) + for qw in query_words: + place_q = place_q.filter(func.lower(db.f_unaccent(db.Place.name)).like(func.lower(db.f_unaccent(f'%{qw}%')))) + if place.level > 0: + place_q = place_q.join(db.RegionDescendant, and_(db.RegionDescendant.region == place.place_id, + db.RegionDescendant.descendant == db.Place.place_id)) + found_places = (place_q + .options(joinedload(db.Place.parent_place)) + .order_by(db.Place.level, db.Place.name, db.Place.place_id) + .limit(max_places) + .all()) + + if not found_places: + search_failed = True + if len(found_places) == 1: + flash('Nalezeno toto místo', 'info') + return redirect(url_for('org_place', id=found_places[0].place_id)) + else: + search_limited = len(found_places) >= max_places + if place.type == db.PlaceType.school: school = sess.query(db.School).get(place.place_id) else: @@ -33,19 +77,13 @@ def org_place(id: int): 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 + search_form=search_form, + found_places=found_places, search_failed=search_failed, search_limited=search_limited, ) @@ -232,7 +270,7 @@ def org_place_move(id: int): 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' + 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 nepovolenou úroveň' elif new_parent.place_id == place.parent: search_error = 'Žádná změna, místo je zde již umístěno' elif form.move.data: @@ -372,8 +410,8 @@ def org_place_root(): return redirect(url_for('org_place', id=root.place_id)) -@app.route('/org/place/<int:id>/rights') -def org_place_rights(id: int): +@app.route('/org/place/<int:id>/roles') +def org_place_roles(id: int): sess = db.get_session() place = sess.query(db.Place).get(id) @@ -383,14 +421,38 @@ def org_place_rights(id: int): 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)) + .options(joinedload(db.UserRole.user), + joinedload(db.UserRole.place), + joinedload(db.UserRole.assigned_by_user)) .all()) roles.sort(key=lambda r: (mo.rights.role_order_by_type[r.role], r.user.sort_key())) + assigned_roles = [r for r in roles if r.place_id == id] + inherited_roles = [r for r in roles if r.place_id != id] + 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 + 'org_place_roles.html', place=place, rights=rights, + assigned_roles=assigned_roles, inherited_roles=inherited_roles, + roles_by_type=mo.rights.roles_by_type, ) + + +@app.route('/org/place/<int:id>/contests') +def org_place_contests(id: int): + sess = db.get_session() + + place = sess.query(db.Place).get(id) + if not place: + raise werkzeug.exceptions.NotFound() + + 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_contests.html', place=place, contests=contests) diff --git a/mo/web/templates/org_index.html b/mo/web/templates/org_index.html index a5962c432b06e1753b9f6c039ab227b24e6365ae..177a3b86edeed21a35c905d7e4c7f1115002cf2b 100644 --- a/mo/web/templates/org_index.html +++ b/mo/web/templates/org_index.html @@ -80,17 +80,15 @@ {% endif %} -<h3>Rychlé hledání</h3> - -<form method=GET action="" class='form form-inline' role=form> - <input class='form-control' name=place placeholder='Kód místa'></input> - <input class='btn btn-primary' type="submit" value='Vyhledat'> -</form> {% if g.user.is_admin %} + +<h3>Univerzální hledátko</h3> + <form method=GET action="" class='form form-inline' role=form> - <input class='form-control' name=uid placeholder='ID uživatele'></input> + <input class='form-control' name=search placeholder='cID pID rID uID' autofocus></input> <input class='btn btn-primary' type="submit" value='Vyhledat'> </form> + {% endif %} {% endblock %} diff --git a/mo/web/templates/org_place.html b/mo/web/templates/org_place.html index d7b4a0e0a94fba10369d661eaf50d6a05ecb298b..14d21ac3a52d75a76c1be715a9a032c0ccbf4810 100644 --- a/mo/web/templates/org_place.html +++ b/mo/web/templates/org_place.html @@ -1,4 +1,5 @@ {% extends "base.html" %} +{% import "bootstrap/wtf.html" as wtf %} {% block title %}{{ place.type_name().title() }}: {{ place.name }}{% endblock %} {% block breadcrumbs %} {{ place_breadcrumbs(place) }} @@ -28,12 +29,48 @@ </form> <a class="btn btn-default" href="{{ url_for('org_place_move', id=place.place_id) }}">Přesunout</a> {% endif %} - <a class="btn btn-default" href='{{ url_for('org_place_rights', id=place.place_id) }}'>Přístupová práva</a> + <a class="btn btn-default" href='{{ url_for('org_place_contests', id=place.place_id) }}'>Soutěže</a> + <a class="btn btn-default" href='{{ url_for('org_place_roles', id=place.place_id) }}'>Organizátoři</a> {% if g.user.is_admin %} <a class="btn btn-default" href="{{ log_url('place', place.place_id) }}">Historie</a> {% endif %} </div> +<h3>Vyhledávání míst{% if place.level > 0 %} v této oblasti{% endif %}</h3> + +{% if search_failed %} +<div class='alert alert-danger' role='alert'> + Žádné vyhovující místo nenalezeno. +</div> +{% endif %} + +{% if search_limited %} +<div class='alert alert-warning' role='alert'> + Nalezeno příliš mnoho míst, zobrazeno jen prvních {{ found_places|length }}. +</div> +{% endif %} + +{{ wtf.quick_form(search_form, method='GET', form_type='inline', button_map={'submit': 'primary'}) }} + +{% if found_places %} + <table class=data> + <thead><tr> + <th>Kód + <th>Typ + <th>Název + </thead> + {% for p in found_places %} + <tr> + <td>{{ p.get_code() }} + <td>{{ p.type_name() }} + <td><a href='{{ url_for('org_place', id=p.place_id) }}'>{{ p.name_or_id() }}</a> + {% if p.parent_place.level > 0 %} + ({{ p.parent_place.type_name() }} {{ p.parent_place.name_or_id() }}) + {% endif %} + {% endfor %} + </table> +{% endif %} + {% if place.can_have_child() %} <h3>Podřízená místa</h3> {% if children %} @@ -41,14 +78,14 @@ <table class=data> <thead><tr> <th>Kód - <th>Název <th>Typ + <th>Název </thead> {% for child in children %} <tr> <td>{{ child.get_code() }} - <td><a href='{{ url_for('org_place', id=child.place_id) }}'>{{ child.name or "#" + child.place_id|string }}</a> <td>{{ child.type_name() }} + <td><a href='{{ url_for('org_place', id=child.place_id) }}'>{{ child.name_or_id() }}</a> {% endfor %} </table> {% endif %} @@ -57,28 +94,4 @@ {% endif %} {% endif %} -<h3>Soutěže</h3> - -{% if not contests %} -<p>K tomuto místu nejsou přidružené žádné soutěže. -{% else %} -<table class=data> - <thead><tr> - <th>ID - <th>Ročník - <th>Kat. - <th>Název - <th>Stav - </thead> - {% for c in contests %} - <tr> - {% set r = c.round %} - <td><a href='{{ url_for('org_contest', ct_id=c.contest_id) }}'>{{ r.round_code() }}</a> - <td>{{ r.year }} - <td>{{ r.category }} - <td>{{ r.name }} - <td class='rstate-{{c.state.name}}'>{{ c.state.friendly_name() }} - {% endfor %} -</table> -{% endif %} {% endblock %} diff --git a/mo/web/templates/org_place_contests.html b/mo/web/templates/org_place_contests.html new file mode 100644 index 0000000000000000000000000000000000000000..00af0b43f4471235132a94c37bfa1731e9d3980c --- /dev/null +++ b/mo/web/templates/org_place_contests.html @@ -0,0 +1,32 @@ +{% extends "base.html" %} +{% block title %}{{ place.type_name().title() }}: {{ place.name }} – soutěže{% endblock %} +{% block breadcrumbs %} +{{ place_breadcrumbs(place, action="Soutěže") }} +{% endblock %} + +{% block body %} + +{% if not contests %} +<p>V tomto místě se nekonají žádné soutěže. +{% else %} +<table class=data> + <thead><tr> + <th>ID + <th>Ročník + <th>Kat. + <th>Název + <th>Stav + </thead> + {% for c in contests %} + <tr> + {% set r = c.round %} + <td><a href='{{ url_for('org_contest', ct_id=c.contest_id) }}'>{{ r.round_code() }}</a> + <td>{{ r.year }} + <td>{{ r.category }} + <td>{{ r.name }} + <td class='rstate-{{c.state.name}}'>{{ c.state.friendly_name() }} + {% endfor %} +</table> +{% endif %} + +{% endblock %} diff --git a/mo/web/templates/org_place_rights.html b/mo/web/templates/org_place_rights.html deleted file mode 100644 index 22505de047e20e161449d80c7fefc3007b757205..0000000000000000000000000000000000000000 --- a/mo/web/templates/org_place_rights.html +++ /dev/null @@ -1,40 +0,0 @@ -{% extends "base.html" %} -{% block title %}{{ place.type_name().title() }}: {{ place.name }} – role{% endblock %} -{% block breadcrumbs %} -{{ place_breadcrumbs(place, action="Role") }} -{% endblock %} - -{% block body %} - <table class=data> - <thead><tr> - <th>Role - <th>Jméno - <th>Roč. - <th>Kat. - <th class='has-tip' title='Pořadí kola v kategorii'>Kolo - <th>Zdroj - </thead> - {% for role in roles %} - <tr> - <td>{{ roles_by_type[role.role].name }} - <td>{{ role.user|user_link }} - <td>{{ role.year or '–' }} - <td>{{ role.category or '–' }} - <td>{{ role.seq or '–' }} - <td>{% if role.place_id == place.place_id %}přiděleno{% else %}<a href='{{ url_for('org_place_rights', id=role.place_id) }}'>zděděno</a>{% endif %} - {% endfor %} - </table> - -<!-- -Odvozená práva: -{% if g.user.is_admin %} - admin -{% elif rights %} - {% for r in rights %} - {{ r.name }} - {% endfor %} -{% else %} - žádná -{% endif %} ---> -{% endblock %} diff --git a/mo/web/templates/org_place_roles.html b/mo/web/templates/org_place_roles.html new file mode 100644 index 0000000000000000000000000000000000000000..dfaaa739eebd66b05b367598e5a7c035fbdbeb64 --- /dev/null +++ b/mo/web/templates/org_place_roles.html @@ -0,0 +1,58 @@ +{% extends "base.html" %} +{% block title %}{{ place.type_name().title() }}: {{ place.name }} – organizátoři{% endblock %} +{% block breadcrumbs %} +{{ place_breadcrumbs(place, action="Organizátoři") }} +{% endblock %} + +{% macro show_roles(title, roles, inherited) %} + <h3>{{ title }}</h3> + {% if roles %} + <table class=data> + <thead><tr> + <th>Role + <th>Jméno + <th>Roč. + <th>Kat. + <th class='has-tip' title='Pořadí kola v kategorii'>Kolo + {% if inherited %} + <th>Zděděno z + {% else %} + <th>Přidělil + {% endif %} + </thead> + {% for role in roles %} + <tr> + <td>{{ roles_by_type[role.role].name }} + <td>{{ role.user|user_link }} + <td>{{ role.year or '–' }} + <td>{{ role.category or '–' }} + <td>{{ role.seq or '–' }} + {% if inherited %} + <td><a href='{{ url_for('org_place_roles', id=role.place_id) }}'>{{ role.place.type_name() }} {{ role.place.name_or_id() }}</a> + {% else %} + <td>{% if role.assigned_by_user %}{{ role.assigned_by_user|user_link }}{% else %}systém{% endif %} + {% endif %} + {% endfor %} + </table> + {% else %} + <em>Žádné.</em> + {% endif %} +{% endmacro %} + +{% block body %} +{{ show_roles("Přidělené role", assigned_roles, False) }} +{{ show_roles("Zděděné role", inherited_roles, True) }} + +<!-- +Odvozená práva: +{% if g.user.is_admin %} + admin +{% elif rights %} + {% for r in rights %} + {{ r.name }} + {% endfor %} +{% else %} + žádná +{% endif %} +--> +{% endblock %}