diff --git a/mo/rights.py b/mo/rights.py index 1ab63fc07b9e4a2bd831c1f1567d74d84c9bac15..52ed4f316f086829780929fc6478b09bb17829bb 100644 --- a/mo/rights.py +++ b/mo/rights.py @@ -16,6 +16,8 @@ class Right(Enum): upload_solutions = auto() upload_feedback = auto() edit_points = auto() + edit_users = auto() + edit_orgs = auto() @dataclass @@ -33,6 +35,8 @@ roles: List[Role] = [ Right.assign_rights, Right.edit_place, Right.manage_contest, + Right.edit_users, + Right.edit_orgs, }, ), Role( @@ -42,6 +46,8 @@ roles: List[Role] = [ Right.assign_rights, Right.edit_place, Right.manage_contest, + Right.edit_users, + Right.edit_orgs, }, ), Role( @@ -51,6 +57,7 @@ roles: List[Role] = [ Right.assign_rights, Right.edit_place, Right.manage_contest, + Right.edit_users, }, ), Role( diff --git a/mo/web/org_users.py b/mo/web/org_users.py index 29b2cc20ebf948973bbc4117ab9ec5c7d6c92433..885b630492ca5e2a3edcf94e43f8d0b66001d8a3 100644 --- a/mo/web/org_users.py +++ b/mo/web/org_users.py @@ -1,15 +1,19 @@ -from operator import or_ -from flask import render_template, redirect, url_for, flash, request +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_ +from sqlalchemy.orm import joinedload from typing import Optional, List +from wtforms.validators import Email, Required + import mo import mo.db as db import mo.rights import mo.util +import mo.users from mo.web import app @@ -38,6 +42,9 @@ class UsersFilterForm(PagerForm): @app.route('/org/users/') def org_users(): sess = db.get_session() + rr = mo.rights.Rights(g.user) + rr.get_generic() + can_edit = rr.have_right(mo.rights.Right.edit_users) q = sess.query(db.User).filter_by(is_admin=False, is_org=False) filter = UsersFilterForm(request.args) @@ -115,7 +122,10 @@ def org_users(): q = q.limit(filter.limit.data) users = q.all() - return render_template('org_users.html', users=users, count=count, filter=filter, filter_errors=filter_errors) + return render_template( + 'org_users.html', users=users, count=count, + filter=filter, filter_errors=filter_errors, can_edit=can_edit + ) class OrgsFilterForm(PagerForm): @@ -126,6 +136,9 @@ class OrgsFilterForm(PagerForm): @app.route('/org/users/orgs/') def org_users_orgs(): sess = db.get_session() + rr = mo.rights.Rights(g.user) + rr.get_generic() + can_edit = rr.have_right(mo.rights.Right.edit_orgs) q = sess.query(db.User).filter(or_(db.User.is_admin, db.User.is_org)) filter = OrgsFilterForm(request.args) @@ -147,20 +160,137 @@ def org_users_orgs(): q = q.limit(filter.limit.data) users = q.all() - return render_template('org_users_orgs.html', users=users, count=count, filter=filter, filter_errors=None) + return render_template( + 'org_users_orgs.html', users=users, count=count, + filter=filter, filter_errors=None, can_edit=can_edit, + ) @app.route('/org/user/<int:id>/') def org_user(id: int): - return render_template('not_implemented.html') + sess = db.get_session() + user = sess.query(db.User).get(id) + if not user: + raise werkzeug.exceptions.NotFound() + + rr = mo.rights.Rights(g.user) + rr.get_generic() + if user.is_admin: + can_edit = False + elif user.is_org: + can_edit = rr.have_right(mo.rights.Right.edit_orgs) + else: + can_edit = rr.have_right(mo.rights.Right.edit_users) + + participants = sess.query(db.Participant).filter_by(user_id=user.user_id) + rounds = sess.query(db.Participation).filter_by(user_id=user.user_id) + + return render_template('org_user.html', user=user, can_edit=can_edit, participants=participants, rounds=rounds) + + +class UserEditForm(FlaskForm): + first_name = wtforms.StringField("Jméno", validators=[Required()]) + last_name = wtforms.StringField("Příjmení", validators=[Required()]) + note = wtforms.TextAreaField("Poznámka") + submit = wtforms.SubmitField("Uložit") + +class NewUserForm(UserEditForm): + email = wtforms.StringField("E-mail", validators=[Required()]) + submit = wtforms.SubmitField("Vytvořit") -@app.route('/org/user/<int:id>/edit') + +@app.route('/org/user/<int:id>/edit', methods=("GET", "POST")) def org_user_edit(id: int): - return render_template('not_implemented.html') + sess = db.get_session() + user = mo.users.user_by_uid(id) + if not user: + raise werkzeug.exceptions.NotFound() + + rr = mo.rights.Rights(g.user) + rr.get_generic() + if user.is_admin: + raise werkzeug.exceptions.Forbidden() + elif user.is_org and not rr.have_right(mo.rights.Right.edit_orgs): + raise werkzeug.exceptions.Forbidden() + elif not rr.have_right(mo.rights.Right.edit_users): + raise werkzeug.exceptions.Forbidden() + + form = UserEditForm(obj=user) + if form.validate_on_submit(): + form.populate_obj(user) + + if sess.is_modified(user): + changes = db.get_object_changes(user) + + app.logger.info(f"User {id} modified, changes: {changes}") + mo.util.log( + type=db.LogType.user, + what=id, + details={'action': 'edit', 'changes': changes}, + ) + sess.commit() + flash('Změny uživatele uloženy', 'success') + else: + flash(u'Žádné změny k uložení', 'info') + + return redirect(url_for('org_user', id=id)) + return render_template('org_user_edit.html', user=user, form=form) -@app.route('/org/user/new/', defaults={'type': None}) -@app.route('/org/user/new/<type>/') + +@app.route('/org/user/new/', defaults={'type': None}, methods=('GET', 'POST')) +@app.route('/org/user/new/<type>/', methods=('GET', 'POST')) def org_user_new(type: Optional[str]): - return render_template('not_implemented.html') + sess = db.get_session() + rr = mo.rights.Rights(g.user) + rr.get_generic() + + if type is not None and type != "org": + raise werkzeug.exceptions.BadRequest() + if not rr.have_right(mo.rights.Right.edit_users): + raise werkzeug.exceptions.Forbidden() + if type == 'org' and not rr.have_right(mo.rights.Right.edit_orgs): + raise werkzeug.exceptions.Forbidden() + + form = NewUserForm() + if form.validate_on_submit(): + check = True + + if mo.users.user_by_email(form.email.data) is not None: + flash('Účet s daným emailem již existuje', 'danger') + check = False + + if check: + new_user = db.User() + form.populate_obj(new_user) + new_user.is_org = (type == 'org') + sess.add(new_user) + sess.flush() + + app.logger.info(f"New user created: {db.row2dict(new_user)}") + mo.util.log( + type=db.LogType.user, + what=new_user.user_id, + details={'action': 'new', 'user': db.row2dict(new_user)}, + ) + + sess.commit() + flash('Nový uživatel vytvořen', 'success') + + # Send password (re)set link + token = mo.users.ask_reset_password(new_user) + link = url_for('reset', token=token, _external=True) + db.get_session().commit() + + try: + mo.util.send_password_reset_email(new_user, link) + flash('Email s odkazem pro nastavení hesla odeslán na {}'.format(new_user.email), 'success') + except RuntimeError as e: + app.logger.error('Login: problém při posílání emailu: {}'.format(e)) + flash('Problém při odesílání emailu s odkazem pro nastavení hesla', 'danger') + + return redirect(url_for('org_user', id=new_user.user_id)) + + return render_template('org_user_new.html', form=form) + diff --git a/templates/org_user.html b/templates/org_user.html new file mode 100644 index 0000000000000000000000000000000000000000..fa4d8892961a50a5d0159ac2796ae47894889b23 --- /dev/null +++ b/templates/org_user.html @@ -0,0 +1,88 @@ +{% extends "base.html" %} +{% block body %} +<h2>{% if user.is_org %}Organizátor:{% elif user.is_admin %}Správce:{% else %}Soutěžící:{% endif %} {{ user.first_name }} {{ user.last_name }}</h2> + +<table class=data> +<tr><td>Jméno:</td><td>{{ user.first_name }}</td></tr> +<tr><td>Příjmení:</td><td>{{ user.last_name }}</td></tr> +<tr><td>E-mail:</td><td><a href="mailto:{{ user.email }}">{{ user.email }}</a></td></tr> +{% if user.is_admin %}<tr><td>Administrátor:</td><td>ano</td></tr>{% endif %} +{% if user.is_org %}<tr><td>Organizátor:</td><td>ano</td></tr>{% endif %} +<tr><td>Poznámka:</td><td style="white-space: pre;">{{ user.note }}</td></tr> +</table> + +{% if can_edit %} +<div class="btn-group" role="group" style="margin: 10px 0px;"> + <a class="btn btn-primary" href="{{ url_for('org_user_edit', id=user.user_id) }}">Editovat</a> +</div> +{% endif %} + +{% if user.is_org or user.is_admin %} +<h3>Role</h3> + +<table class="table"> + <thead> + <tr> + <th>Role</th><th>Ročník</th><th>Kategorie</th><th>Kolo</th><th>Oblast</th> + </tr> + </thead> +{% for role in user.roles %} + <tr> + <td>{{ role.role.name }}</td> + <td>{{ role.year or '*' }}</td> + <td>{{ role.category or '*' }}</td> + <td>{{ role.seq or '*' }}</td> + <td>{{ role.place.type_name() + ": " + role.place.name or '*' }}</td> + </tr> +{% endfor %} +</table> +{% endif %} + +<h3>Registrace v ročnících</h3> +{% if participants.count() %} +<table class="table"> + <thead> + <tr> + <th>Ročník</th><th>Škola</th><th>Třída</th><th>Rok narození</th> + </tr> + </thead> +{% for participant in participants %} + <tr> + <td>{{ participant.year }}</td> + <td><a href="{{ url_for('org_place', id=participant.school) }}">{{ participant.school_place.name }}</a></td> + <td>{{ participant.grade }}</td> + <td>{{ participant.birth_year }}</td> + </tr> +{% endfor %} +</table> +{% else %} +<p>Žádná registrace v ročníku</p> +{% endif %} + +<h3>Účast v kolech</h3> +{% if rounds.count() %} +<table class="table"> + <thead> + <tr> + <th>Ročník</th><th>Kategorie</th><th>Kolo</th><th>Místo</th><th>Stav účasti</th> + </tr> + </thead> +{% for round in rounds %} + <tr> + <td>{{ round.contest.round.year }}</td> + <td>{{ round.contest.round.category }}</td> + <td>{{ round.contest.round.seq }}</td> + <td><a href="{{ url_for('org_place', id=round.contest.place_id) }}">{{ round.contest.place.name }}</a> + {% if round.place_id != round.contest.place_id %} + <br>(ale soutěží v <a href="{{ url_for('org_place', id=round.place_id) }}">{{ round.place.name }}</a>) + {% endif %} + </td> + <td>{{ round.state.name }}</td> + </tr> +{% endfor %} +</table> +{% else %} +<p>Žádná účast v kole</p> +{% endif %} + +{% endblock %} diff --git a/templates/org_user_edit.html b/templates/org_user_edit.html new file mode 100644 index 0000000000000000000000000000000000000000..147c2e8cbd08732b16e065d66eb22aa80b9ef5a7 --- /dev/null +++ b/templates/org_user_edit.html @@ -0,0 +1,25 @@ +{% extends "base.html" %} +{% import "bootstrap/wtf.html" as wtf %} +{% block body %} +<h2>{% if user.is_org %}Org:{% elif user.is_admin %}Admin:{% else %}Soutěžící{% endif %} {{ user.first_name }} {{ user.last_name }}</h2> + +<table class=data> +<tr><td>Jméno:</td><td>{{ user.first_name }}</td></tr> +<tr><td>Příjmení:</td><td>{{ user.last_name }}</td></tr> +<tr><td>E-mail:</td><td><a href="mailto:{{ user.email }}">{{ user.email }}</a></td></tr> +{% if user.is_admin %}<tr><td>Administrátor:</td><td>ano</td></tr>{% endif %} +{% if user.is_org %}<tr><td>Organizátor:</td><td>ano</td></tr>{% endif %} +<tr><td>Poznámka:</td><td style="white-space: pre;">{{ user.note }}</td></tr> +</table> + +<a href='{{ url_for('org_user', id=user.user_id) }}'>Zpět na detail</a> + +<hr> + +<h3>Editace údajů</h3> + +<p>Email nelze editovat. Pro jeho změnu kontaktujte někoho ze správců.</p> + +{{ wtf.quick_form(form, form_type='horizontal') }} + +{% endblock %} diff --git a/templates/org_user_new.html b/templates/org_user_new.html new file mode 100644 index 0000000000000000000000000000000000000000..d3529c2b05a6c072b62d9357e7e557abe5bcb235 --- /dev/null +++ b/templates/org_user_new.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} +{% import "bootstrap/wtf.html" as wtf %} +{% block body %} +<h2>Nový {% if is_org %}organizátor{% else %}soutěžící{% endif %}</h2> + +<p>Na zadaný email dorazí email pro nastavení hesla. Email po vytvoření účtu již nelze měnit.</p> + +{{ wtf.quick_form(form, form_type='horizontal') }} + +{% endblock %} diff --git a/templates/org_users_orgs.html b/templates/org_users_orgs.html index a7e54c460c0fc1cf96721af3079e641328847209..97f2cf88985295fab127b2f12168024770322f7c 100644 --- a/templates/org_users_orgs.html +++ b/templates/org_users_orgs.html @@ -38,16 +38,19 @@ <table class="table"> <thead> <tr> - <th>Jméno</th><th>Příjmení</th><th>E-mail</th><th>Akce</th> + <th>Jméno</th><th>Příjmení</th><th>E-mail</th><th>Nejvyšší role</th><th>Akce</th> </tr> </thead> {% for user in users %} <tr> <td>{{ user.first_name }}</td><td>{{ user.last_name }}</td> <td><a href="mailto:{{ user.email }}">{{ user.email }}</a></td> + <td>{% if user.is_admin %}administrátor{% else %} + {%- if user.roles|count > 0 %}{{user.roles[0].role.name}}{% else %}<i>žádná role</i>{% endif -%} + {% endif %}</td> <td class='btn-group'> <a class="btn btn-xs btn-default" href="{{ url_for('org_user', id=user.user_id) }}">Detail</a> - <a class="btn btn-xs btn-default" href="{{ url_for('org_user_edit', id=user.user_id) }}">Edit</a> + {% if can_edit and not user.is_admin %}<a class="btn btn-xs btn-default" href="{{ url_for('org_user_edit', id=user.user_id) }}">Edit</a>{% endif %} </td> </tr> {% endfor %}