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

Import: Rozesílání mailů a logování

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.
parent a5fc75e0
No related branches found
No related tags found
No related merge requests found
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:
......
......@@ -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')
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment