Skip to content
Snippets Groups Projects
Commit b3d00a25 authored by Jiří Setnička's avatar Jiří Setnička
Browse files

Výpis participants - filtrování + provádění akcí

Podporované akce:
* nastavení stavu účasti
* nastavení soutěžního místa
* přesun do jiné soutěže ve stejném kole (podle místa)
* smazání záznamu o účasti
parent fa3b063f
No related branches found
No related tags found
1 merge request!7Filtrování a provádění hromadných akcí na participants
...@@ -5,6 +5,7 @@ import locale ...@@ -5,6 +5,7 @@ import locale
import os import os
import secrets import secrets
from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload
from sqlalchemy.orm.query import Query
from typing import List, Tuple, Optional, Sequence from typing import List, Tuple, Optional, Sequence
import werkzeug.exceptions import werkzeug.exceptions
import wtforms import wtforms
...@@ -16,7 +17,8 @@ import mo.imports ...@@ -16,7 +17,8 @@ import mo.imports
import mo.rights import mo.rights
import mo.util import mo.util
from mo.web import app from mo.web import app
from mo.web.table import Table, Column, cell_place_link, cell_user_link, cell_email_link from mo.web.util import PagerForm
from mo.web.table import CellCheckbox, Table, Column, cell_place_link, cell_user_link, cell_email_link
import wtforms.validators as validators import wtforms.validators as validators
...@@ -25,6 +27,132 @@ class ImportForm(FlaskForm): ...@@ -25,6 +27,132 @@ class ImportForm(FlaskForm):
submit = wtforms.SubmitField('Importovat') submit = wtforms.SubmitField('Importovat')
class ParticipantsFilterForm(PagerForm):
school = wtforms.StringField("Škola")
participation_place = wtforms.StringField("Soutěžní místo")
contest_place = wtforms.StringField("Soutěžní oblast")
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')])
submit = wtforms.SubmitField("Zobrazit")
download_csv = wtforms.SubmitField("↓ CSV")
download_tsv = wtforms.SubmitField("↓ TSV")
class ParticipantsActionForm(FlaskForm):
action_on = wtforms.RadioField(
"Provést akci na", validators=[validators.DataRequired()],
choices=[('all', 'všech vyfiltrovaných soutěžících'), ('checked', 'označených soutěžících')]
# checkboxes are handled not through FlaskForm, see below
)
participation_state = wtforms.SelectField('Stav účasti', choices=list(db.PartState.choices()))
set_participation_state = wtforms.SubmitField("Nastavit stav účasti")
participation_place = wtforms.StringField(
'Soutěžní místo', description='Zadejte kód nebo ID ve tvaru <code>#123</code>'
)
set_participation_place = wtforms.SubmitField("Nastavit soutěžní místo")
contest_place = wtforms.StringField(
'Soutěžní oblast',
description='Musí existovat soutěž v dané oblasti pro stejné kolo. Oblast zadejte pomocí kódu nebo ID ve tvaru <code>#123</code>.'
)
set_contest = wtforms.SubmitField("Přesunout do jiné soutěžní oblasti")
remove_participation = wtforms.SubmitField("Smazat záznam o účasti")
def do_action(self, round: db.Round, rights: mo.rights.Rights, query: Query) -> bool:
"""Do participation modification on partipations from given query
(possibly filtered by checkboxes). Expects that rights for round/contest
are checked before calling this function, `rights` param are used only
for checking that we can move participation to another contest."""
if not self.validate_on_submit():
return False
sess = db.get_session()
# Check that operation is valid
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
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 zadanou soutěžní oblast", 'danger')
return False
contest = sess.query(db.Contest).filter_by(round_id=round.round_id, place_id=contest_place.place_id).one_or_none()
if not contest:
flash(f"Nepovedlo se najít soutěž v kole {round.round_code()} v oblasti {contest_place.name}", 'danger')
return False
rights.get_for_contest(contest)
if not rights.have_right(mo.rights.Right.manage_contest):
flash(f"Nemáte právo ke správě soutěže v kole {round.round_code()} v oblasti {contest_place.name}, nelze do ní přesunout soutěžící", 'danger')
elif self.remove_participation.data:
pass
else:
flash('Neznámá operace', 'danger')
return False
try:
user_ids = list(map(int, request.form.getlist('checked')))
except ValueError:
flash('Data v checkboxech nelze převést na čísla, kontaktujte správce', 'danger')
return False
count = 0
ctants = query.all()
for pion, _, _ in ctants:
u = pion.user
if self.action_on.data == 'checked' and u.user_id not in user_ids:
continue
if self.remove_participation.data:
sess.delete(pion)
app.logger.info(f"Participation of user {u.user_id} in contest {pion.contest} removed")
mo.util.log(
type=db.LogType.participant,
what=u.user_id,
details={'action': 'participation-removed', 'participation': db.row2dict(pion)},
)
else:
if self.set_participation_state.data:
pion.state = self.participation_state.data
elif self.set_participation_place.data:
pion.place = participation_place
elif self.set_contest.data:
pion.contest = contest
changes = db.get_object_changes(pion)
app.logger.info(f"Participation of user {u.user_id} modified, changes: {changes}")
mo.util.log(
type=db.LogType.participant,
what=u.user_id,
details={'action': 'participation-changed', 'changes': changes},
)
sess.flush()
count += 1
sess.commit()
if count == 0:
flash('Žádní vybraní soutěžící', 'warning')
elif self.set_participation_state.data:
flash(f'Nastaven stav {db.part_state_names[self.participation_state.data]} {count} řešitelům', 'success')
elif self.set_participation_place.data:
flash(f'Nastaveno soutěžní místo {participation_place.name} {count} řešitelům', 'success')
elif self.set_contest.data:
flash(f'{count} řešitelů přesunuto do soutěže v oblasti {contest_place.name}', 'success')
elif self.remove_participation.data:
flash(f'Odstraněno {count} soutěžících z této soutěže', 'success')
return True
@app.route('/org/contest/') @app.route('/org/contest/')
def org_contest_root(): def org_contest_root():
sess = db.get_session() sess = db.get_session()
...@@ -73,20 +201,39 @@ def org_round(id: int): ...@@ -73,20 +201,39 @@ def org_round(id: int):
) )
@app.route('/org/contest/r/<int:id>/list') @app.route('/org/contest/r/<int:id>/list', methods=('GET', 'POST'))
def org_round_list(id: int): def org_round_list(id: int):
round, rr = get_round_rr(id, mo.rights.Right.manage_contest) round, rr = get_round_rr(id, mo.rights.Right.manage_contest)
format = request.args.get('format', "") format = request.args.get('format', "")
table = make_contestant_table(round, None) filter = ParticipantsFilterForm(request.args)
filter.validate()
query = get_contestants_query(
round=round,
school=db.get_place_by_code(filter.school.data),
contest_place=db.get_place_by_code(filter.contest_place.data),
participation_place=db.get_place_by_code(filter.participation_place.data),
participation_state=None if filter.participation_state.data == '*' else filter.participation_state.data
)
action_form = ParticipantsActionForm()
if action_form.do_action(round=round, rights=rr, query=query):
# Action happened, redirect
return redirect(request.url)
(count, query) = filter.apply_limits(query, pagesize=50)
# count = query.count()
if format == "": if format == "":
table = make_contestant_table(query, add_contest_column=True, add_checkbox=True)
return render_template( return render_template(
'org_round_list.html', 'org_round_list.html',
round=round, round=round,
table=table, table=table,
filter=filter, count=count, action_form=action_form,
) )
else: else:
table = make_contestant_table(query)
return table.send_as(format) return table.send_as(format)
...@@ -191,20 +338,39 @@ def org_contest_import_template(): ...@@ -191,20 +338,39 @@ def org_contest_import_template():
return resp return resp
@app.route('/org/contest/c/<int:id>/ucastnici') @app.route('/org/contest/c/<int:id>/ucastnici', methods=('GET', 'POST'))
def org_contest_list(id: int): def org_contest_list(id: int):
contest, rr = get_contest_rr(id, mo.rights.Right.manage_contest) contest, rr = get_contest_rr(id, mo.rights.Right.manage_contest)
format = request.args.get('format', "") format = request.args.get('format', "")
table = make_contestant_table(contest.round, contest) filter = ParticipantsFilterForm(request.args)
filter.validate()
query = get_contestants_query(
round=contest.round, contest=contest,
school=db.get_place_by_code(filter.school.data),
# contest_place=db.get_place_by_code(filter.contest_place.data),
participation_place=db.get_place_by_code(filter.participation_place.data),
participation_state=None if filter.participation_state.data == '*' else filter.participation_state.data
)
action_form = ParticipantsActionForm()
if action_form.do_action(round=contest.round, rights=rr, query=query):
# Action happened, redirect
return redirect(request.url)
# (count, query) = filter.apply_limits(query, pagesize=50)
count = query.count()
if format == "": if format == "":
table = make_contestant_table(query, add_checkbox=True)
return render_template( return render_template(
'org_contest_list.html', 'org_contest_list.html',
contest=contest, contest=contest,
table=table, table=table,
filter=filter, count=count, action_form=action_form,
) )
else: else:
table = make_contestant_table(query)
return table.send_as(format) return table.send_as(format)
...@@ -221,7 +387,12 @@ contest_list_columns = ( ...@@ -221,7 +387,12 @@ contest_list_columns = (
) )
def make_contestant_table(round: db.Round, contest: Optional[db.Contest]) -> Table: def get_contestants_query(
round: db.Round, contest: Optional[db.Contest] = None,
contest_place: Optional[db.Place] = None,
participation_place: Optional[db.Place] = None,
participation_state: Optional[db.PartState] = None,
school: Optional[db.Place] = None) -> Query:
query = (db.get_session() query = (db.get_session()
.query(db.Participation, db.Participant, db.Contest) .query(db.Participation, db.Participant, db.Contest)
.select_from(db.Participation) .select_from(db.Participation)
...@@ -233,10 +404,22 @@ def make_contestant_table(round: db.Round, contest: Optional[db.Contest]) -> Tab ...@@ -233,10 +404,22 @@ def make_contestant_table(round: db.Round, contest: Optional[db.Contest]) -> Tab
query = query.filter(db.Contest.round == round) query = query.filter(db.Contest.round == round)
query = query.options(joinedload(db.Contest.place)) query = query.options(joinedload(db.Contest.place))
query = query.filter(db.Participation.contest_id == db.Contest.contest_id) query = query.filter(db.Participation.contest_id == db.Contest.contest_id)
if contest_place:
query = query.filter(db.Contest.place_id == contest_place.place_id)
if participation_place:
query = query.filter(db.Participation.place_id == participation_place.place_id)
if school:
query = query.filter(db.Participant.school == school.place_id)
if participation_state:
query = query.filter(db.Participation.state == participation_state)
query = query.options(joinedload(db.Participation.user), query = query.options(joinedload(db.Participation.user),
joinedload(db.Participation.place), joinedload(db.Participation.place),
joinedload(db.Participant.school_place)) joinedload(db.Participant.school_place))
return query
def make_contestant_table(query: Query, add_checkbox: bool = False, add_contest_column: bool = False):
ctants = query.all() ctants = query.all()
rows: List[dict] = [] rows: List[dict] = []
...@@ -254,18 +437,22 @@ def make_contestant_table(round: db.Round, contest: Optional[db.Contest]) -> Tab ...@@ -254,18 +437,22 @@ def make_contestant_table(round: db.Round, contest: Optional[db.Contest]) -> Tab
'region_code': cell_place_link(ct.place, ct.place.get_code()), 'region_code': cell_place_link(ct.place, ct.place.get_code()),
'place_code': cell_place_link(pion.place, pion.place.get_code()), 'place_code': cell_place_link(pion.place, pion.place.get_code()),
'status': pion.state.friendly_name(), 'status': pion.state.friendly_name(),
'checkbox': CellCheckbox('checked', u.user_id, False),
}) })
rows.sort(key=lambda r: r['sort_key']) rows.sort(key=lambda r: r['sort_key'])
cols: Sequence[Column] = contest_list_columns cols: Sequence[Column] = contest_list_columns
if not contest: if add_checkbox:
cols = [Column(key='checkbox', name=' ', title=' ')] + list(cols)
if add_contest_column:
cols = list(cols) + [Column(key='region_code', name='kod_oblasti', title='Oblast')] cols = list(cols) + [Column(key='region_code', name='kod_oblasti', title='Oblast')]
return Table( return Table(
columns=cols, columns=cols,
rows=rows, rows=rows,
filename='ucastnici', filename='ucastnici',
show_downlink=False, # downlinks are in filter
) )
......
{% extends "base.html" %} {% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block body %} {% block body %}
<h2>Účastníci {{ contest.round.round_code() }}: {{ contest.place.name }}</h2> <h2>Účastníci {{ contest.round.round_code() }}: {{ contest.place.name }}</h2>
{{ table.to_html() }} <form action="" method="GET" class="form form-inline" role="form">
{{ wtf.form_field(filter.participation_place, placeholder='Kód / #ID', size=8) }}
{{ wtf.form_field(filter.school, placeholder='Kód / #ID', size=8) }}
{{ wtf.form_field(filter.participation_state) }}
<div class="btn-group">
{{ wtf.form_field(filter.submit, class='btn btn-primary') }}
<button class="btn btn-default" name="format" value="csv" title="Stáhnout celý výsledek v CSV">↓ CSV</button>
<button class="btn btn-default" name="format" value="tsv" title="Stáhnout celý výsledek v TSV">↓ TSV</button>
</div>
</form>
<p>Celkem <b>{{count}} nalezených soutěžících</b>.</p>
{% include 'parts/org_participants_table_actions.html' %}
{% endblock %} {% endblock %}
{% extends "base.html" %} {% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block body %} {% block body %}
<h2>Účastníci kola {{ round.round_code() }}</h2> <h2>Účastníci kola {{ round.round_code() }}</h2>
{{ table.to_html() }} <form action="" method="GET" class="form form-inline" role="form">
{{ wtf.form_field(filter.contest_place, placeholder='Kód / #ID', size=8) }}
{{ wtf.form_field(filter.participation_place, placeholder='Kód / #ID', size=8) }}
{{ wtf.form_field(filter.school, placeholder='Kód / #ID', size=8) }}
{{ wtf.form_field(filter.participation_state) }}
<div class="btn-group">
{{ wtf.form_field(filter.submit, class='btn btn-primary') }}
<button class="btn btn-default" name="format" value="csv" title="Stáhnout celý výsledek v CSV">↓ CSV</button>
<button class="btn btn-default" name="format" value="tsv" title="Stáhnout celý výsledek v TSV">↓ TSV</button>
</div>
<div style="float: right">
Stránka {{ filter.offset.data // filter.limit.data + 1}} z {{ (count / filter.limit.data)|round(0, 'ceil')|int }}:
<div class="btn-group">
{% if filter.offset.data > 0 %}
{{ wtf.form_field(filter.previous) }}
{% else %}
<button class="btn" disabled>Předchozí</button>
{% endif %}
{% if count > filter.offset.data + filter.limit.data %}
{{ wtf.form_field(filter.next) }}
{% else %}
<button class="btn" disabled>Další</button>
{% endif %}
</div>
<input type="hidden" name="offset" value="{{filter.offset.data}}">
<input type="hidden" name="limit" value="{{filter.limit.data}}">
</div>
</form>
{% set max = filter.offset.data + filter.limit.data if filter.offset.data + filter.limit.data < count else count %}
<p>Zobrazuji záznamy <b>{{filter.offset.data + 1}}</b><b>{{ max }}</b> z <b>{{count}} nalezených soutěžících</b>.</p>
{% include 'parts/org_participants_table_actions.html' %}
{% endblock %} {% endblock %}
<form action="" method="POST" class="form form-horizontal" role="form">
{{ table.to_html() }}
{{ action_form.csrf_token }}
<h3>Provést akci</h3>
<div class="form-group">
<label class="col-form-label col-sm-2">Provést akci na:</label>
<div class="col-sm-10">
<div class="radio">
<label>
<input id="action_on-0" name="action_on" type="radio" value="all" required{% if action_form.action_on.data == 'all' %} checked{% endif %}>
všech vyfiltrovaných soutěžících
</label>
</div>
<div class="radio">
<label>
<input id="action_on-1" name="action_on" type="radio" value="checked" required{% if action_form.action_on.data == 'all' %} checked{% endif %}>
označených soutěžících
</label>
</div>
</div>
</div>
<hr>
{{ wtf.form_field(action_form.participation_state, form_type='horizontal', horizontal_columns=('sm', 2, 10)) }}
{{ wtf.form_field(action_form.set_participation_state, form_type='horizontal', class='btn btn-primary', horizontal_columns=('sm', 2, 10)) }}
<hr>
{{ wtf.form_field(action_form.participation_place, form_type='horizontal', horizontal_columns=('sm', 2, 10)) }}
{{ wtf.form_field(action_form.set_participation_place, form_type='horizontal', class='btn btn-primary', horizontal_columns=('sm', 2, 10)) }}
<hr>
{{ wtf.form_field(action_form.contest_place, form_type='horizontal', horizontal_columns=('sm', 2, 10)) }}
{{ wtf.form_field(action_form.set_contest, form_type='horizontal', class='btn btn-primary', horizontal_columns=('sm', 2, 10)) }}
<hr>
{{ wtf.form_field(action_form.remove_participation, form_type='horizontal', class='btn btn-danger', horizontal_columns=('sm', 2, 10)) }}
</form>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment