Skip to content
Snippets Groups Projects
Commit 9d4d89df authored by Martin Mareš's avatar Martin Mareš
Browse files

Diplomy/UI: Přehled ocenění studentů pro školní garanty

parent 9786c214
No related branches found
No related tags found
1 merge request!137Sazba diplomů
# Web: Certifikáty # Web: Certifikáty
from flask import render_template, g, redirect, url_for, flash from flask import render_template, g, redirect, url_for, flash
from flask.helpers import send_file
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
import flask_wtf.file import flask_wtf.file
import locale
from markupsafe import Markup from markupsafe import Markup
import os import os
import pikepdf
from pikepdf.models.metadata import encode_pdf_date
from sqlalchemy import func, select, and_
from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload
from tempfile import NamedTemporaryFile
from typing import Tuple, Optional, Dict from typing import Tuple, Optional, Dict
import werkzeug.exceptions import werkzeug.exceptions
import wtforms import wtforms
from wtforms import validators from wtforms import validators
import mo import mo
import mo.config as config
import mo.db as db import mo.db as db
import mo.email import mo.email
from mo.jobs.certs import schedule_create_certs, DesignParams, BackgroundType from mo.jobs.certs import schedule_create_certs, DesignParams, BackgroundType
...@@ -257,3 +264,134 @@ def org_cert_file(ct_id: int, cert_type: str, filename: str, user_id: Optional[i ...@@ -257,3 +264,134 @@ def org_cert_file(ct_id: int, cert_type: str, filename: str, user_id: Optional[i
assert ctx.contest and ctx.master_contest assert ctx.contest and ctx.master_contest
return send_certificate(ct_id, cert_type, filename, user_id) return send_certificate(ct_id, cert_type, filename, user_id)
@app.route('/org/contest/school-results/<int:school_id>/c/<int:ct_id>/certificates/<cert_type>/<filename>')
def org_school_results_certs(school_id: int, ct_id: int, cert_type: str, filename: str):
sess = db.get_session()
place = sess.query(db.Place).filter_by(place_id=school_id).one_or_none()
if place is None:
raise werkzeug.exceptions.NotFound()
if not g.gatekeeper.rights_for(place=place).have_right(Right.view_contestants):
raise werkzeug.exceptions.Forbidden()
contest = sess.query(db.Contest).options(joinedload(db.Contest.round)).get(ct_id)
if contest is None or contest.round.year != config.CURRENT_YEAR:
raise werkzeug.exceptions.NotFound()
if contest.state != db.RoundState.closed:
raise werkzeug.exceptions.Forbidden()
try:
ctype: db.CertType = db.CertType.coerce(cert_type)
except ValueError:
raise werkzeug.exceptions.NotFound()
if filename != ctype.file_name(plural=True) + '.pdf':
raise werkzeug.exceptions.NotFound()
cfile = sess.query(db.CertFile).filter_by(cert_set_id=ct_id, type=ctype, approved=True).one_or_none()
if cfile is None:
raise werkzeug.exceptions.NotFound()
users_and_certs = (sess.query(db.User, db.Certificate)
.select_from(db.Certificate)
.join(db.Participant, and_(db.Participant.user_id == db.Certificate.user_id,
db.Participant.year == config.CURRENT_YEAR,
db.Participant.school == school_id))
.join(db.Certificate.user)
.filter(db.Certificate.cert_set_id == ct_id,
db.Certificate.type == ctype)
.all())
users_and_certs.sort(key=lambda x: x[0].sort_key())
try:
file = os.path.join(mo.util.data_dir('certs'), cfile.pdf_file)
if not os.path.isfile(file):
app.logger.error(f'Certifikát {file} je v DB, ale soubor neexistuje')
raise werkzeug.exceptions.NotFound()
with pikepdf.open(file, attempt_recovery=False) as src:
with pikepdf.new() as dst:
for _, cert in users_and_certs:
dst.pages.append(src.pages[cert.page_number - 1])
dst.docinfo['/Title'] = f'Matematická Olympiáda – {ctype.friendly_name(plural=True)}'
dst.docinfo['/Creator'] = 'Odevzdávací Systém Matematické Olympiády'
dst.docinfo['/CreationDate'] = encode_pdf_date(mo.now.astimezone())
tmp_file = NamedTemporaryFile(dir=mo.util.data_dir('tmp'), prefix='cert-')
dst.save(tmp_file.name)
except pikepdf.PdfError as e:
app.logger.error(f'Chyba při zpracování PDF certifikátů: {e}')
raise werkzeug.exceptions.InternalServerError()
return send_file(open(tmp_file.name, 'rb'), mimetype='application/pdf')
@app.route('/org/contest/school-results/<int:school_id>/')
def org_school_results(school_id: int):
sess = db.get_session()
place = sess.query(db.Place).filter_by(place_id=school_id).one_or_none()
if place is None:
raise werkzeug.exceptions.NotFound()
# Úmyslně nekontrolujeme práva ke kategorii
if not g.gatekeeper.rights_for(place=place).have_right(Right.view_contestants):
raise werkzeug.exceptions.Forbidden()
pants_subq = (sess.query(db.Participant.user_id)
.filter_by(year=config.CURRENT_YEAR,
school=school_id)
.subquery())
contest_counts = (sess.query(db.Contest.contest_id, func.count(db.Participation.user_id))
.select_from(db.Contest)
.join(db.Contest.round)
.join(db.Contest.participations)
.filter(db.Contest.contest_id == db.Contest.master_contest_id,
db.Round.year == config.CURRENT_YEAR,
db.Participation.user_id.in_(select(pants_subq)),
db.Participation.state == db.PartState.active)
.group_by(db.Contest.contest_id)
.all())
contest_ids = [cid for cid, _ in contest_counts]
num_pants_by_cid = {cid: cnt for cid, cnt in contest_counts}
contests = (sess.query(db.Contest)
.filter(db.Contest.contest_id.in_(contest_ids))
.options(joinedload(db.Contest.round),
joinedload(db.Contest.place))
.all())
contests.sort(key=lambda c: (c.round.category, c.round.seq, locale.strxfrm(c.place.name))) # part nepotřebujeme, vše jsou hlavní soutěže
cert_counts = (sess.query(db.CertFile.cert_set_id, db.CertFile.type, func.count(db.Certificate.user_id))
.select_from(db.Certificate)
.join(db.Certificate.cert_file)
.filter(db.Certificate.cert_set_id.in_(contest_ids))
.filter(db.Certificate.user_id.in_(select(pants_subq)))
.filter(db.CertFile.approved)
.group_by(db.CertFile.cert_set_id, db.CertFile.type)
.all())
cert_counts_by_cid_type = {(cid, ctype): cnt for cid, ctype, cnt in cert_counts}
contests_with_cert = {cid for cid, ctype, cnt in cert_counts}
return render_template(
'org_school_results.html',
place=place,
contests=contests,
num_pants_by_cid=num_pants_by_cid,
cert_counts_by_cid_type=cert_counts_by_cid_type,
contests_with_cert=contests_with_cert,
)
@app.route('/org/contest/school-results/')
def org_school_results_all():
school_place_ids = set(ur.place_id for ur in g.gatekeeper.roles if ur.role == db.RoleType.garant_skola)
if len(school_place_ids) == 1:
return redirect(url_for('org_school_results', school_id=list(school_place_ids)[0]))
sess = db.get_session()
schools = sess.query(db.Place).filter(db.Place.place_id.in_(school_place_ids)).all()
schools.sort(key=lambda p: locale.strxfrm(p.name))
return render_template('org_school_results_all.html', schools=schools)
{% extends "base.html" %}
{% block title %}
Ocenění studentů {{ place.name }}
{% endblock %}
{% block breadcrumbs %}
<li><a href='{{ url_for('org_school_results_all') }}'>Ocenění studentů</a><li>{{ place.name }}
{% endblock %}
{% block body %}
{% if contests %}
<table class='table table-bordered greyhead vcenter-rows'>
<thead>
<tr>
<th>Kat.
<th>Kolo
<th title='Počet studentů Vaší školy v tomto kole'>Počet
<th>Výsledky
<th>Diplomy
<tbody>
{% for c in contests %}
<tr>
<td>{{ c.round.category }}
<td>{% if c.round.round_type == RoundType.other %}{{ c.round.name }}{% else %}{{ c.round.round_type.friendly_name() }}{% endif %}
{% if c.round.level < 4 %}
({{ c.place.name }})
{% endif %}
<td class=ar>{{ num_pants_by_cid[c.contest_id] }}
{% if c.state == RoundState.closed %}
<td>
{% if c.scoretable_id is not none %}
<a href='{{ url_for('org_score_snapshot', ct_id=c.contest_id, scoretable_id=c.scoretable_id) }}'>Zobrazit</a>
{% else %}
{% endif %}
<td>
{% if c.contest_id in contests_with_cert %}
<ul class=bare>
{% for t in CertType %}
{% set ccnt = cert_counts_by_cid_type[(c.contest_id, t)] %}
{% if ccnt %}
<li><a href='{{ url_for('org_school_results_certs', school_id=place.place_id, ct_id=c.contest_id, cert_type=t.name, filename=t.file_name(plural=True) + '.pdf') }}'>{{ t.friendly_name(plural=True) }}</a> ({{ ccnt }})
{% endif %}
{% endfor %}
</ul>
{% else %}
{% endif %}
{% else %}
<td colspan=2><em>kolo dosud není uzavřeno</em>
{% endif %}
{% endfor %}
</table>
{% else %}
<p><em>Vaši studenti se v letošním ročníku neúčastní žádné soutěže.</em></p>
{% endif %}
{% endblock %}
{% extends "base.html" %}
{% block title %}
Ocenění studentů
{% endblock %}
{% block breadcrumbs %}
<li><a href='{{ url_for('org_school_results_all') }}'>Ocenění studentů</a>
{% endblock %}
{% block body %}
{% if schools %}
<p>
Jste školním garantem pro více škol.
Nejprve si vyberte školu, pro kterou chcete ocenění zobrazit:
</p>
<ul>
{% for s in schools %}
<li><a href='{{ url_for('org_school_results', school_id=s.place_id) }}'>{{ s.name }}</a>
{% endfor %}
</ul>
{% else %}
<p>
Nejste školním garantem pro žádnou školu.
</p>
{% endif %}
{% endblock %}
...@@ -170,6 +170,10 @@ table.greyhead thead tr th { ...@@ -170,6 +170,10 @@ table.greyhead thead tr th {
border: none; border: none;
} }
table.vcenter-rows tr td, table.vcenter-rows tr th {
vertical-align: middle;
}
.ac { .ac {
text-align: center; text-align: center;
} }
...@@ -642,6 +646,14 @@ table.dsn-check-err tr th { ...@@ -642,6 +646,14 @@ table.dsn-check-err tr th {
padding-right: 2ex; padding-right: 2ex;
} }
/* Seznamy bez odrážek, používáme uvnitř tabulek */
ul.bare {
padding: 0;
margin: 0;
list-style-type: none;
}
/* Vzhled pro mobily a úzké displeje */ /* Vzhled pro mobily a úzké displeje */
@media only screen and (max-width: 600px) { @media only screen and (max-width: 600px) {
... ...
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment