From b9682fbfed8e936646e08460eb2d06c0e6cf56d0 Mon Sep 17 00:00:00 2001 From: Martin Mares <mj@ucw.cz> Date: Sun, 3 Jan 2021 19:59:16 +0100 Subject: [PATCH] =?UTF-8?q?Import:=20Rozes=C3=ADl=C3=A1n=C3=AD=20mail?= =?UTF-8?q?=C5=AF=20a=20logov=C3=A1n=C3=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit O commit a logování se stará back-end v mo.imports. Maily novým účastníkům se generují v samostatných transakcích. --- mo/imports.py | 84 ++++++++++++++++++++++++++++++++++++++++--- mo/web/org_contest.py | 27 ++------------ 2 files changed, 82 insertions(+), 29 deletions(-) diff --git a/mo/imports.py b/mo/imports.py index 90e14310..d5b943e7 100644 --- a/mo/imports.py +++ b/mo/imports.py @@ -1,4 +1,5 @@ from dataclasses import dataclass +import logging import io import re from typing import List, Optional, Any, Dict, Callable, Type, TypeVar @@ -6,6 +7,7 @@ from typing import List, Optional, Any, Dict, Callable, Type, TypeVar import mo.csv import mo.db as db import mo.rights +import mo.users import mo.util RowType = TypeVar('RowType', bound=mo.csv.Row) @@ -32,16 +34,25 @@ class ProctorImportRow(mo.csv.Row): class Import: - line_errors: List[str] + # Výsledek importu errors: List[str] + cnt_new_users: int = 0 + cnt_new_participants: int = 0 + cnt_new_participations: int = 0 + cnt_new_roles: int = 0 + + # Interní: Co zrovna importujeme user: db.User round: Optional[db.Round] contest: Optional[db.Contest] + # Interní: Stav importu place_cache: Dict[str, db.Place] school_place_cache: Dict[str, db.Place] rr: Optional[mo.rights.Rights] place_rights_cache: Dict[int, bool] + line_errors: List[str] + new_user_ids: List[int] def __init__(self, user: db.User): self.errors = [] @@ -50,6 +61,7 @@ class Import: self.place_cache = {} self.school_place_cache = {} self.place_rights_cache = {} + self.new_user_ids = [] def error(self, msg: str) -> Any: self.line_errors.append(msg) @@ -163,11 +175,14 @@ class Import: user = db.User(email=email, first_name=krestni, last_name=prijmeni) sess.add(user) sess.flush() # Aby uživatel dostal user_id + logging.info(f'Import: Založen uživatel user=#{user.user_id} email=<{user.email}>') mo.util.log( type=db.LogType.user, what=user.user_id, details={'action': 'import', 'new': db.row2dict(user)}, ) + self.cnt_new_users += 1 + self.new_user_ids.append(user.user_id) return user def find_or_create_participant(self, user: db.User, year: int, school_id: int, birth_year: int, grade: str) -> Optional[db.Participant]: @@ -181,11 +196,13 @@ class Import: else: part = db.Participant(user=user, year=year, school=school_id, birth_year=birth_year, grade=grade) sess.add(part) + logging.info(f'Import: Založen účastník #{user.user_id}') mo.util.log( type=db.LogType.participant, what=user.user_id, details={'action': 'import', 'new': db.row2dict(part)}, ) + self.cnt_new_participants += 1 return part @@ -202,11 +219,13 @@ class Import: if not pions: pion = db.Participation(user=user, contest=contest, place_id=place.place_id, state=db.PartState.registered) sess.add(pion) + logging.info(f'Import: Založena účast user=#{user.user_id} contest=#{contest.contest_id} place=#{place.place_id}') mo.util.log( type=db.LogType.participant, what=user.user_id, details={'action': 'add-to-contest', 'new': db.row2dict(pion)}, ) + self.cnt_new_participations += 1 elif len(pions) == 1: pion = pions[0] if pion.place != place: @@ -292,18 +311,20 @@ class Import: category=round.category, year=round.year, seq=round.seq) sess.add(ur) sess.flush() + logging.info(f'Import: Dozor user=#{user.user_id} place=#{misto.place_id} user_role=#{ur.user_role_id}') mo.util.log( type=db.LogType.user_role, what=ur.user_role_id, details={'action': 'import', 'new': db.row2dict(ur)}, ) + self.cnt_new_roles += 1 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[RowType] = mo.csv.read(file=file, dialect='excel', row_class=row_class) except Exception as e: - return self.error(f'Chybná struktura tabulky {e}') + return self.error(f'Chybná struktura tabulky: {e}') line_num = 2 for row in rows: @@ -316,17 +337,70 @@ class Import: break line_num += 1 - return len(self.errors) == 0 + if self.errors: + logging.info('Import: Rollback') + return False + else: + logging.info(f'Import: Hotovo (users={self.cnt_new_users} p-ants={self.cnt_new_participants} p-ions={self.cnt_new_participations} roles={self.cnt_new_roles})') + return True 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) + logging.info(f'Import: Účastníci ze souboru {path}: uid={self.user.user_id} round=#{round.round_id}') + if not self.generic_import(path, ContestImportRow, Import.import_contest_row): + db.get_session().rollback() + return False + + mo.util.log( + type=db.LogType.contest, + what=contest.contest_id, + details={'action': 'import'} + ) + db.get_session().commit() + + mo.util.log( + type=db.LogType.round, + what=round.round_id, + details={'action': 'import'} + ) + db.get_session().commit() + + self.notify_users() + return True 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) + logging.info(f'Import: Dozor ze souboru {path}: uid={self.user.user_id}') + if not self.generic_import(path, ProctorImportRow, Import.import_proctor_row): + return False + + mo.util.log( + type=db.LogType.round, + what=round.round_id, + details={'action': 'import-proctors'} + ) + db.get_session().commit() + + self.notify_users() + return True + + def notify_users(self): + # Projde všechny uživatele a těm, kteří ještě nemají nastavené heslo, + # ani nepožádali o jeho reset, pošle mail s odkazem na reset. Každého + # uživatele zpracováváme ve zvlášť transakci, aby se po chybě neztratila + # informace o tom, že už jsme nějaké maily rozeslali. + + sess = db.get_session() + for uid in self.new_user_ids: + u = sess.query(db.User).get(uid) + if u and not u.password_hash and not u.reset_at: + token = mo.users.ask_reset_password(u) + sess.commit() + mo.util.send_new_account_email(u, token) + else: + sess.rollback() def generic_template(row_class: Type[RowType]) -> str: diff --git a/mo/web/org_contest.py b/mo/web/org_contest.py index 15047ba1..4488c26c 100644 --- a/mo/web/org_contest.py +++ b/mo/web/org_contest.py @@ -100,17 +100,10 @@ def org_round_import(id: int): 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: Zpracovávám soubor %s pro round=%s, uid=%s', tmp_name, round.round_code(), g.user.user_id) imp = mo.imports.Import(g.user) if imp.import_contest(round, None, tmp_path): - mo.util.log( - type=db.LogType.round, - what=round.round_id, - details={'action': 'import'} - ) - db.get_session().commit() - flash('Účastníci importováni', 'success') + flash(f'Účastníci importováni (založeno {imp.cnt_new_users} uživatelů, {imp.cnt_new_participations} účastí)', 'success') return redirect(url_for('org_round', id=round.round_id)) else: flash('Došlo k chybě při importu (detaily níže)', 'danger') @@ -168,17 +161,10 @@ def org_contest_import(id: int): 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: 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_contest(contest.round, contest, tmp_path): - mo.util.log( - type=db.LogType.contest, - what=contest.contest_id, - details={'action': 'import'} - ) - db.get_session().commit() - flash('Účastníci importováni', 'success') + flash(f'Účastníci importováni (založeno {imp.cnt_new_users} uživatelů, {imp.cnt_new_participations} účastí)', 'success') return redirect(url_for('org_contest', id=contest.contest_id)) else: flash('Došlo k chybě při importu (detaily níže)', 'danger') @@ -293,17 +279,10 @@ def org_proctor_import(id: int): 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') + flash(f'Dozor importován (založeno {imp.cnt_new_users} uživatelů, {imp.cnt_new_roles} rolí)', 'success') return redirect(url_for('org_contest', id=contest.contest_id)) else: flash('Došlo k chybě při importu (detaily níže)', 'danger') -- GitLab