From 008b614011f66b60005ea10559f41a0c080739c3 Mon Sep 17 00:00:00 2001 From: Martin Mares <mj@ucw.cz> Date: Sun, 3 Jan 2021 00:42:35 +0100 Subject: [PATCH] Import dozoru MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Zobecnil jsem modul na importování, ale ještě se ne úplně pěkně opakují kusy webového rozhraní. Až budeme přidávat další importy, bude to chtít vyřešit. --- mo/imports.py | 92 +++++++++++++++++++++--- mo/web/org_contest.py | 42 +++++++++++ mo/web/templates/org_contest.html | 1 + mo/web/templates/org_proctor_import.html | 24 +++++++ 4 files changed, 148 insertions(+), 11 deletions(-) create mode 100644 mo/web/templates/org_proctor_import.html diff --git a/mo/imports.py b/mo/imports.py index 645ba2b2..90e14310 100644 --- a/mo/imports.py +++ b/mo/imports.py @@ -1,13 +1,15 @@ from dataclasses import dataclass import io import re -from typing import List, Optional, Any, Dict +from typing import List, Optional, Any, Dict, Callable, Type, TypeVar import mo.csv import mo.db as db import mo.rights import mo.util +RowType = TypeVar('RowType', bound=mo.csv.Row) + @dataclass class ContestImportRow(mo.csv.Row): @@ -21,12 +23,21 @@ class ContestImportRow(mo.csv.Row): kod_oblasti: str = "" +@dataclass +class ProctorImportRow(mo.csv.Row): + email: str = "" + krestni: str = "" + prijmeni: str = "" + kod_mista: str = "" + + class Import: line_errors: List[str] errors: List[str] user: db.User + round: Optional[db.Round] + contest: Optional[db.Contest] - round: db.Round place_cache: Dict[str, db.Place] school_place_cache: Dict[str, db.Place] rr: Optional[mo.rights.Rights] @@ -81,6 +92,7 @@ class Import: self.rr = mo.rights.Rights(self.user) round = self.round + assert round is not None self.rr.get_for(place, round.year, round.category, round.seq) result = self.rr.have_right(mo.rights.Right.manage_contest) self.place_rights_cache[place.place_id] = result @@ -146,7 +158,7 @@ class Import: user = sess.query(db.User).filter_by(email=email).one_or_none() if user: if user.first_name != krestni or user.last_name != prijmeni: - return self.error(f'Účastník již registrován s odlišným jménem {user.first_name} {user.last_name}') + return self.error(f'Osoba již registrována s odlišným jménem {user.first_name} {user.last_name}') else: user = db.User(email=email, first_name=krestni, last_name=prijmeni) sess.add(user) @@ -204,7 +216,7 @@ class Import: return pion - def import_contest_row(self, contest: Optional[db.Contest], r: ContestImportRow): + def import_contest_row(self, r: ContestImportRow): num_prev_errs = len(self.errors) email = self.parse_email(r.email) krestni = self.parse_name(r.krestni) @@ -232,7 +244,8 @@ class Import: if part is None: return - if contest: + if self.contest: + contest = self.contest if oblast is not None and oblast.place_id != contest.place.place_id: return self.error('Účastník soutěží mimo oblast, do které se importuje') else: @@ -244,19 +257,58 @@ class Import: self.find_or_create_participation(user, contest, misto) - def import_contest(self, round: db.Round, contest: Optional[db.Contest], path: str) -> bool: - self.round = round + def import_proctor_row(self, r: ProctorImportRow): + num_prev_errs = len(self.errors) + email = self.parse_email(r.email) + krestni = self.parse_name(r.krestni) + prijmeni = self.parse_name(r.prijmeni) + misto = self.parse_opt_place(r.kod_mista) + + if misto is None: + return self.error('Kód místa je povinné uvést') + + if (len(self.errors) > num_prev_errs + or email is None + or krestni is None + or prijmeni is None): + return + user = self.find_or_create_user(email, krestni, prijmeni) + if user is None: + return + + sess = db.get_session() + round = self.round + assert round is not None + + if (sess.query(db.UserRole) + .filter_by(user=user, place=misto, role=db.RoleType.dozor, + category=round.category, year=round.year, seq=round.seq) + .with_for_update() + .first()): + pass + else: + ur = db.UserRole(user=user, place=misto, role=db.RoleType.dozor, + category=round.category, year=round.year, seq=round.seq) + sess.add(ur) + sess.flush() + mo.util.log( + type=db.LogType.user_role, + what=ur.user_role_id, + details={'action': 'import', 'new': db.row2dict(ur)}, + ) + + def generic_import(self, path: str, row_class: Type[RowType], process_row: Callable[['Import', RowType], None]) -> bool: try: with open(path) as file: - rows: List[ContestImportRow] = mo.csv.read(file=file, dialect='excel', row_class=ContestImportRow) + rows: List[RowType] = mo.csv.read(file=file, dialect='excel', row_class=row_class) except Exception as e: return self.error(f'Chybná struktura tabulky {e}') line_num = 2 for row in rows: self.line_errors = [] - self.import_contest_row(contest, row) + process_row(self, row) for err in self.line_errors: self.errors.append(f"Řádek {line_num}: {err}") if len(self.errors) >= 100: @@ -266,8 +318,26 @@ class Import: return len(self.errors) == 0 + def import_contest(self, round: db.Round, contest: Optional[db.Contest], path: str) -> bool: + self.round = round + self.contest = contest + return self.generic_import(path, ContestImportRow, Import.import_contest_row) -def contest_template() -> str: + def import_proctors(self, round: db.Round, path: str) -> bool: + self.round = round + self.contest = None + return self.generic_import(path, ProctorImportRow, Import.import_proctor_row) + + +def generic_template(row_class: Type[RowType]) -> str: out = io.StringIO() - mo.csv.write(file=out, dialect='excel', row_class=ContestImportRow, rows=[]) + mo.csv.write(file=out, dialect='excel', row_class=row_class, rows=[]) return out.getvalue() + + +def contest_template() -> str: + return generic_template(ContestImportRow) + + +def proctor_template() -> str: + return generic_template(ProctorImportRow) diff --git a/mo/web/org_contest.py b/mo/web/org_contest.py index 0df2acea..15047ba1 100644 --- a/mo/web/org_contest.py +++ b/mo/web/org_contest.py @@ -281,3 +281,45 @@ def make_contestant_table(round: db.Round, contest: Optional[db.Contest]) -> Tab rows=rows, filename='ucastnici', ) + + +@app.route('/org/contest/c/<int:id>/proctor-import', methods=('GET', 'POST')) +def org_proctor_import(id: int): + contest, rr = get_contest_rr(id, mo.rights.Right.manage_contest) + + form = ImportForm() + errs = [] + if form.validate_on_submit(): + tmp_name = secrets.token_hex(16) + '.csv' + tmp_path = os.path.join(app.instance_path, 'imports', tmp_name) + form.file.data.save(tmp_path) + app.logger.info('Import dozoru: Zpracovávám soubor %s pro contest_id=%s, uid=%s', tmp_name, contest.contest_id, g.user.user_id) + + imp = mo.imports.Import(g.user) + if imp.import_proctors(contest.round, tmp_path): + mo.util.log( + type=db.LogType.contest, + what=contest.contest_id, + details={'action': 'import-proctors'} + ) + db.get_session().commit() + flash('Dozor importován', 'success') + return redirect(url_for('org_contest', id=contest.contest_id)) + else: + flash('Došlo k chybě při importu (detaily níže)', 'danger') + errs = imp.errors + + return render_template( + 'org_proctor_import.html', + contest=contest, + form=form, + errs=errs, + ) + + +@app.route('/org/contest/import/sablona-dozor.csv') +def org_proctor_import_template(): + out = mo.imports.proctor_template() + resp = app.make_response(out) + resp.content_type = 'text/csv; charset=utf=8' + return resp diff --git a/mo/web/templates/org_contest.html b/mo/web/templates/org_contest.html index e899dce8..88d7b83d 100644 --- a/mo/web/templates/org_contest.html +++ b/mo/web/templates/org_contest.html @@ -13,6 +13,7 @@ {% if can_manage %} <p><a href='{{ url_for('org_contest_import', id=contest.contest_id) }}'>Importovat účastníky</a> <p><a href='{{ url_for('org_contest_list', id=contest.contest_id) }}'>Seznam účastníků</a> +<p><a href='{{ url_for('org_proctor_import', id=contest.contest_id) }}'>Importovat dozor</a> {% endif %} <h3>Vaše práva k této soutěži</h3> diff --git a/mo/web/templates/org_proctor_import.html b/mo/web/templates/org_proctor_import.html new file mode 100644 index 00000000..d4dd0095 --- /dev/null +++ b/mo/web/templates/org_proctor_import.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} +{% import "bootstrap/wtf.html" as wtf %} +{% block body %} +<h2>Soutěž {{ contest.round.round_code() }}: {{ contest.place.name }}</h2> + +<a href='{{ url_for('org_contest', id=contest.contest_id) }}'>Zpět na soutěž</a> + +{% if errs %} +<h3>Chyby při importu</h3> +<pre> +{% for e in errs %} +{{ e }} +{% endfor %} +</pre> +{% endif %} + +<h3>Import dozoru</h3> + +<p>Dozor na jednotlivých soutěžních místech můžete importovat ve <a href='{{ url_for('org_contest_import_help') }}'>formátu CSV</a> (FIXME) +podle <a href='{{ url_for('org_proctor_import_template') }}'>šablony</a>. + +{{ wtf.quick_form(form, form_type='horizontal') }} + +{% endblock %} -- GitLab