diff --git a/mo/users.py b/mo/users.py index 96bc328b51bfbe0b9957584f88458e7d537f6c00..b57c6e0557861c16c31f7ac9afb63089d2d94aeb 100644 --- a/mo/users.py +++ b/mo/users.py @@ -7,6 +7,7 @@ import email.errors import email.headerregistry import re import secrets +from sqlalchemy.dialects.postgresql import insert as pgsql_insert from typing import Optional, Tuple import mo @@ -96,40 +97,57 @@ def change_user_to_org(user, reason: str): def find_or_create_user(email: str, krestni: Optional[str], prijmeni: Optional[str], is_org: bool, reason: str, allow_change_user_to_org=False) -> Tuple[db.User, bool, bool]: sess = db.get_session() - user = sess.query(db.User).with_for_update().filter_by(email=email).one_or_none() - is_new = user is None + user = sess.query(db.User).filter_by(email=email).one_or_none() + is_new = False is_change_user_to_org = False + if user is None: # HACK: Podmínku je nutné zapsat znovu místo užití is_new, jinak si s tím mypy neporadí if not krestni or not prijmeni: raise mo.CheckError('Osoba s daným e-mailem zatím neexistuje, je nutné uvést její jméno.') - user = db.User(email=email, first_name=krestni, last_name=prijmeni, is_org=is_org) - sess.add(user) - sess.flush() # Aby uživatel dostal user_id - logger.info(f'{reason.title()}: Založen uživatel user=#{user.user_id} email=<{user.email}>') - mo.util.log( - type=db.LogType.user, - what=user.user_id, - details={'action': 'create-user', 'reason': reason, 'new': db.row2dict(user)}, + + res = sess.connection().execute( + pgsql_insert(db.User.__table__) + .values( + email=email, + first_name=krestni, + last_name=prijmeni, + is_org=is_org, + ) + .on_conflict_do_nothing() + .returning(db.User.user_id) ) - else: - if (krestni and user.first_name != krestni) or (prijmeni and user.last_name != prijmeni): - raise mo.CheckError(f'Osoba již registrována s odlišným jménem {user.full_name()}') - if (user.is_admin or user.is_org) != is_org: - if is_org: - if allow_change_user_to_org: - change_user_to_org(user, reason) - is_change_user_to_org = True - else: - raise CheckErrorOrgIsUser('Nelze předefinovat účastníka na organizátora.') + + user = sess.query(db.User).filter_by(email=email).one() + + if res.fetchall(): + is_new = True + logger.info(f'{reason.title()}: Založen uživatel user=#{user.user_id} email=<{user.email}>') + mo.util.log( + type=db.LogType.user, + what=user.user_id, + details={'action': 'create-user', 'reason': reason, 'new': db.row2dict(user)}, + ) + + if (krestni and user.first_name != krestni) or (prijmeni and user.last_name != prijmeni): + raise mo.CheckError(f'Osoba již registrována s odlišným jménem {user.full_name()}') + if (user.is_admin or user.is_org) != is_org: + if is_org: + if allow_change_user_to_org: + change_user_to_org(user, reason) + is_change_user_to_org = True else: - raise mo.CheckError('Nelze předefinovat organizátora na účastníka.') + raise CheckErrorOrgIsUser('Nelze předefinovat účastníka na organizátora.') + else: + raise mo.CheckError('Nelze předefinovat organizátora na účastníka.') + return user, is_new, is_change_user_to_org def find_or_create_participant(user: db.User, year: int, school_id: Optional[int], birth_year: Optional[int], grade: Optional[str], reason: str) -> Tuple[db.Participant, bool]: sess = db.get_session() part = sess.query(db.Participant).get((user.user_id, year)) - is_new = part is None + is_new = False + if part is None: prev_part = sess.query(db.Participant).filter_by(user_id=user.user_id).order_by(db.Participant.year.desc()).limit(1).one_or_none() if not school_id: @@ -144,19 +162,37 @@ def find_or_create_participant(user: db.User, year: int, school_id: Optional[int raise mo.CheckError('Osoba s daným e-mailem zatím není zaregistrovaná do ročníku, je nutné uvést rok narození.') if not grade: raise mo.CheckError('Osoba s daným e-mailem zatím není zaregistrovaná do ročníku, je nutné uvést ročník.') - part = db.Participant(user=user, year=year, school=school_id, birth_year=birth_year, grade=grade) - sess.add(part) - logger.info(f'{reason.title()}: Založen účastník #{user.user_id}') - mo.util.log( - type=db.LogType.participant, - what=user.user_id, - details={'action': 'create-participant', 'reason': reason, 'new': db.row2dict(part)}, + + res = sess.connection().execute( + pgsql_insert(db.Participant.__table__) + .values( + user_id=user.user_id, + year=year, + school=school_id, + birth_year=birth_year, + grade=grade, + ) + .on_conflict_do_nothing() + .returning(db.Participant.user_id) ) - else: - if ((school_id and part.school != school_id) - or (grade and part.grade != grade) - or (birth_year and part.birth_year != birth_year)): - raise mo.CheckError('Účastník již zaregistrován s odlišnou školou/ročníkem/rokem narození') + + part = sess.query(db.Participant).get((user.user_id, year)) + assert part is not None + + if res.fetchall(): + is_new = True + logger.info(f'{reason.title()}: Založen účastník #{user.user_id}') + mo.util.log( + type=db.LogType.participant, + what=user.user_id, + details={'action': 'create-participant', 'reason': reason, 'new': db.row2dict(part)}, + ) + + if ((school_id and part.school != school_id) + or (grade and part.grade != grade) + or (birth_year and part.birth_year != birth_year)): + raise mo.CheckError('Účastník již zaregistrován s odlišnou školou/ročníkem/rokem narození') + return part, is_new @@ -165,27 +201,47 @@ def find_or_create_participation(user: db.User, contest: db.Contest, place: Opti place = contest.place sess = db.get_session() - pions = (sess.query(db.Participation) - .filter_by(user=user) - .filter(db.Participation.contest.has(db.Contest.round == contest.round)) - .all()) + pion = None + is_new = False + retry = False + + while pion is None: + pions = (sess.query(db.Participation) + .filter_by(user=user) + .filter(db.Participation.contest.has(db.Contest.round == contest.round)) + .all()) + + if len(pions) == 0: + assert not retry + retry = True + res = sess.connection().execute( + pgsql_insert(db.Participation.__table__) + .values( + user_id=user.user_id, + contest_id=contest.contest_id, + place_id=place.place_id, + state=db.PartState.active, + ) + .on_conflict_do_nothing() + .returning(db.Participation.user_id) + ) + if res.fetchall(): + is_new = True + elif len(pions) == 1: + pion = pions[0] + else: + raise mo.CheckError('Již se tohoto kola účastní ve více oblastech, což by nemělo být možné') + + if pion.place != place: + raise mo.CheckError(f'Již se tohoto kola účastní v {contest.round.get_level().name_locative("jiném", "jiné", "jiném")} ({pion.place.get_code()})') - is_new = pions == [] if is_new: - pion = db.Participation(user=user, contest=contest, place_id=place.place_id, state=db.PartState.active) - sess.add(pion) logger.info(f'{reason.title()}: 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', 'reason': reason, 'new': db.row2dict(pion)}, ) - elif len(pions) == 1: - pion = pions[0] - if pion.place != place: - raise mo.CheckError(f'Již se tohoto kola účastní v {contest.round.get_level().name_locative("jiném", "jiné", "jiném")} ({pion.place.get_code()})') - else: - raise mo.CheckError('Již se tohoto kola účastní ve více oblastech, což by nemělo být možné') return pion, is_new