Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • devel
  • fo
  • fo-base
  • honza/add-contestant
  • honza/kolo-vs-soutez
  • honza/mr6
  • honza/mr7
  • honza/mra
  • honza/mrd
  • honza/mrf
  • honza/submit-images
  • jh-stress-test-wip
  • jirka/typing
  • jk/issue-196
  • jk/issue-96
  • master
  • mj/submit-images
  • shorten-schools
18 results

Target

Select target project
  • mj/mo-submit
1 result
Select Git revision
  • devel
  • fo
  • fo-base
  • honza/add-contestant
  • honza/kolo-vs-soutez
  • honza/mr6
  • honza/mr7
  • honza/mra
  • honza/mrd
  • honza/mrf
  • honza/submit-images
  • jh-stress-test-wip
  • jirka/typing
  • jk/issue-196
  • jk/issue-96
  • master
  • mj/submit-images
  • shorten-schools
18 results
Show changes
Commits on Source (10)
...@@ -7,6 +7,7 @@ import email.errors ...@@ -7,6 +7,7 @@ import email.errors
import email.headerregistry import email.headerregistry
import re import re
import secrets import secrets
from sqlalchemy.dialects.postgresql import insert as pgsql_insert
from typing import Optional, Tuple from typing import Optional, Tuple
import mo import mo
...@@ -96,22 +97,37 @@ def change_user_to_org(user, reason: str): ...@@ -96,22 +97,37 @@ 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]: 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() sess = db.get_session()
user = sess.query(db.User).with_for_update().filter_by(email=email).one_or_none() user = sess.query(db.User).filter_by(email=email).one_or_none()
is_new = user is None is_new = False
is_change_user_to_org = 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 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: 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.') 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) res = sess.connection().execute(
sess.flush() # Aby uživatel dostal user_id 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)
)
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}>') logger.info(f'{reason.title()}: Založen uživatel user=#{user.user_id} email=<{user.email}>')
mo.util.log( mo.util.log(
type=db.LogType.user, type=db.LogType.user,
what=user.user_id, what=user.user_id,
details={'action': 'create-user', 'reason': reason, 'new': db.row2dict(user)}, details={'action': 'create-user', 'reason': reason, 'new': db.row2dict(user)},
) )
else:
if (krestni and user.first_name != krestni) or (prijmeni and user.last_name != prijmeni): 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()}') 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 (user.is_admin or user.is_org) != is_org:
...@@ -123,13 +139,15 @@ def find_or_create_user(email: str, krestni: Optional[str], prijmeni: Optional[s ...@@ -123,13 +139,15 @@ def find_or_create_user(email: str, krestni: Optional[str], prijmeni: Optional[s
raise CheckErrorOrgIsUser('Nelze předefinovat účastníka na organizátora.') raise CheckErrorOrgIsUser('Nelze předefinovat účastníka na organizátora.')
else: else:
raise mo.CheckError('Nelze předefinovat organizátora na účastníka.') raise mo.CheckError('Nelze předefinovat organizátora na účastníka.')
return user, is_new, is_change_user_to_org 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]: 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() sess = db.get_session()
part = sess.query(db.Participant).get((user.user_id, year)) part = sess.query(db.Participant).get((user.user_id, year))
is_new = part is None is_new = False
if part is None: 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() 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: if not school_id:
...@@ -144,20 +162,37 @@ def find_or_create_participant(user: db.User, year: int, school_id: Optional[int ...@@ -144,20 +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í.') 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: 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.') 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) res = sess.connection().execute(
sess.flush() # Kvůli logování 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)
)
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}') logger.info(f'{reason.title()}: Založen účastník #{user.user_id}')
mo.util.log( mo.util.log(
type=db.LogType.participant, type=db.LogType.participant,
what=user.user_id, what=user.user_id,
details={'action': 'create-participant', 'reason': reason, 'new': db.row2dict(part)}, details={'action': 'create-participant', 'reason': reason, 'new': db.row2dict(part)},
) )
else:
if ((school_id and part.school != school_id) if ((school_id and part.school != school_id)
or (grade and part.grade != grade) or (grade and part.grade != grade)
or (birth_year and part.birth_year != birth_year)): 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í') raise mo.CheckError('Účastník již zaregistrován s odlišnou školou/ročníkem/rokem narození')
return part, is_new return part, is_new
...@@ -166,28 +201,47 @@ def find_or_create_participation(user: db.User, contest: db.Contest, place: Opti ...@@ -166,28 +201,47 @@ def find_or_create_participation(user: db.User, contest: db.Contest, place: Opti
place = contest.place place = contest.place
sess = db.get_session() sess = db.get_session()
pion = None
is_new = False
retry = False
while pion is None:
pions = (sess.query(db.Participation) pions = (sess.query(db.Participation)
.filter_by(user=user) .filter_by(user=user)
.filter(db.Participation.contest.has(db.Contest.round == contest.round)) .filter(db.Participation.contest.has(db.Contest.round == contest.round))
.all()) .all())
is_new = pions == [] 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()})')
if is_new: if is_new:
pion = db.Participation(user=user, contest=contest, place_id=place.place_id, state=db.PartState.active)
sess.add(pion)
sess.flush() # Kvůli logování
logger.info(f'{reason.title()}: Založena účast user=#{user.user_id} contest=#{contest.contest_id} place=#{place.place_id}') logger.info(f'{reason.title()}: Založena účast user=#{user.user_id} contest=#{contest.contest_id} place=#{place.place_id}')
mo.util.log( mo.util.log(
type=db.LogType.participant, type=db.LogType.participant,
what=user.user_id, what=user.user_id,
details={'action': 'add-to-contest', 'reason': reason, 'new': db.row2dict(pion)}, 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 return pion, is_new
......
...@@ -193,6 +193,10 @@ def user_settings_personal(): ...@@ -193,6 +193,10 @@ def user_settings_personal():
sess.commit() sess.commit()
flash('Heslo změněno.', 'success') flash('Heslo změněno.', 'success')
if form.email.data != user.email: if form.email.data != user.email:
if mo.users.user_by_email(form.email.data) is not None:
flash('Tuto e-mailovou adresu už používá jiný uživatel.', 'danger')
ok = False
else:
rr = mo.users.new_reg_request(db.RegReqType.change_email, request.remote_addr) rr = mo.users.new_reg_request(db.RegReqType.change_email, request.remote_addr)
if rr: if rr:
rr.user_id = user.user_id rr.user_id = user.user_id
...@@ -436,6 +440,7 @@ class Reg2: ...@@ -436,6 +440,7 @@ class Reg2:
RegStatus.new: 'Chybný potvrzovací kód. Zkontrolujte, že jste odkaz z e-mailu zkopírovali správně.', RegStatus.new: 'Chybný potvrzovací kód. Zkontrolujte, že jste odkaz z e-mailu zkopírovali správně.',
RegStatus.expired: 'Vypršela platnost potvrzovacího kódu, požádejte prosím o změnu e-mailu znovu.', RegStatus.expired: 'Vypršela platnost potvrzovacího kódu, požádejte prosím o změnu e-mailu znovu.',
RegStatus.already_spent: 'Tento odkaz na potvrzení změny e-mailu byl již využit.', RegStatus.already_spent: 'Tento odkaz na potvrzení změny e-mailu byl již využit.',
RegStatus.already_exists: 'Tuto adresu už použivá jiný účet.',
}, },
db.RegReqType.reset_passwd: { db.RegReqType.reset_passwd: {
RegStatus.new: 'Chybný kód pro obnovení hesla. Zkontrolujte, že jste odkaz z e-mailu zkopírovali správně.', RegStatus.new: 'Chybný kód pro obnovení hesla. Zkontrolujte, že jste odkaz z e-mailu zkopírovali správně.',
...@@ -498,37 +503,37 @@ class Reg2: ...@@ -498,37 +503,37 @@ class Reg2:
email = mo.users.normalize_email(rr.email) # Pro jistotu email = mo.users.normalize_email(rr.email) # Pro jistotu
sess = db.get_session() sess = db.get_session()
if db.get_session().query(db.User).with_for_update().filter_by(email=email).one_or_none(): try:
user, is_new, _ = mo.users.find_or_create_user(email, first_name, last_name, is_org=False, reason='register')
except mo.CheckError as e:
app.logger.info(f'Reg2: Založení účtu {email} selhalo: {e}')
self.status = RegStatus.already_exists
return False
if not is_new:
# Účet mohl začít existovat mezi 1. a 2. krokem registrace # Účet mohl začít existovat mezi 1. a 2. krokem registrace
app.logger.info(f'Reg2: Účet s e-mailem {email} začal během registrace existovat') app.logger.info(f'Reg2: Účet s e-mailem {email} začal během registrace existovat')
self.status = RegStatus.already_exists self.status = RegStatus.already_exists
return False return False
user = db.User(
email=email,
first_name=first_name,
last_name=last_name,
)
mo.users.set_password(user, passwd) mo.users.set_password(user, passwd)
mo.users.login(user)
rr.used_at = mo.now rr.used_at = mo.now
sess.add(user)
sess.flush()
app.logger.info(f'Reg2: Založen uživatel user=#{user.user_id} email=<{user.email}>')
mo.util.log(
type=db.LogType.user,
what=user.user_id,
details={'action': 'register', 'new': db.row2dict(user)},
)
sess.commit() sess.commit()
self.user = user self.user = user
return True return True
def change_email(self): def change_email(self) -> bool:
sess = db.get_session() sess = db.get_session()
user = self.rr.user user = self.rr.user
if mo.users.user_by_email(self.rr.email) is not None:
app.logger.info(f'Reg2: Uživatel #{user.user_id} si chce změnit email na <{user.email}>, ale už je použitý jiným účtem.')
self.status = RegStatus.already_exists
return False
# Tady je krátké okénko, kdy může nastat race condition. Chytí ji integritní omezení v DB a vznikne výjimka.
user.email = self.rr.email user.email = self.rr.email
app.logger.info(f'Reg2: Uživatel #{user.user_id} si změnil email na <{user.email}>') app.logger.info(f'Reg2: Uživatel #{user.user_id} si změnil email na <{user.email}>')
...@@ -543,6 +548,7 @@ class Reg2: ...@@ -543,6 +548,7 @@ class Reg2:
self.rr.used_at = mo.now self.rr.used_at = mo.now
sess.commit() sess.commit()
return True
def change_passwd(self, new_passwd: str): def change_passwd(self, new_passwd: str):
sess = db.get_session() sess = db.get_session()
...@@ -617,13 +623,15 @@ def confirm_email(): ...@@ -617,13 +623,15 @@ def confirm_email():
form = ConfirmEmailForm() form = ConfirmEmailForm()
if form.validate_on_submit(): if form.validate_on_submit():
if form.submit.data: if form.submit.data:
reg2.change_email() if reg2.change_email():
flash('E-mail změněn.', 'success') flash('E-mail změněn.', 'success')
return redirect(url_for('user_settings'))
elif form.cancel.data: elif form.cancel.data:
reg2.spend_request() reg2.spend_request()
flash('Požadavek na změnu e-mailu zrušen.', 'success') flash('Požadavek na změnu e-mailu zrušen.', 'success')
return redirect(url_for('user_settings')) return redirect(url_for('user_settings'))
reg2.flash_message()
form.orig_email.data = reg2.rr.user.email form.orig_email.data = reg2.rr.user.email
form.new_email.data = reg2.rr.email form.new_email.data = reg2.rr.email
......
...@@ -368,7 +368,7 @@ class ParticipantsActionForm(FlaskForm): ...@@ -368,7 +368,7 @@ class ParticipantsActionForm(FlaskForm):
if self.remove_participation.data: if self.remove_participation.data:
sess.delete(pion) sess.delete(pion)
app.logger.info(f"Účast uživatele #{u.user_id} v soutěži #{pion.contest} zrušena") app.logger.info(f"Web: Zrušena účast uživatele #{u.user_id} v soutěži #{pion.contest.contest_id}")
mo.util.log( mo.util.log(
type=db.LogType.participant, type=db.LogType.participant,
what=u.user_id, what=u.user_id,
...@@ -383,10 +383,11 @@ class ParticipantsActionForm(FlaskForm): ...@@ -383,10 +383,11 @@ class ParticipantsActionForm(FlaskForm):
pion.place_id = participation_place.place_id pion.place_id = participation_place.place_id
elif self.set_contest.data: elif self.set_contest.data:
pion.contest_id = assert_not_none(contest).contest_id pion.contest_id = assert_not_none(contest).contest_id
pion.place_id = assert_not_none(contest_place).place_id
if sess.is_modified(pion): if sess.is_modified(pion):
changes = db.get_object_changes(pion) changes = db.get_object_changes(pion)
app.logger.info(f"Účast uživatele #{u.user_id} upravena, změny: {changes}") app.logger.info(f"Web: Upravena účast uživatele #{u.user_id} v soutěži #{pion.contest.contest_id}, změny: {changes}")
mo.util.log( mo.util.log(
type=db.LogType.participant, type=db.LogType.participant,
what=u.user_id, what=u.user_id,
......
...@@ -31,6 +31,7 @@ To se hodí, pokud se nechystáte do systému nahrávat soubory řešení, ale j ...@@ -31,6 +31,7 @@ To se hodí, pokud se nechystáte do systému nahrávat soubory řešení, ale j
bylo možné vyplnit body. Pokud nějaké řešení založíte omylem, lze toto prázdné řešení smazat v jeho detailu. bylo možné vyplnit body. Pokud nějaké řešení založíte omylem, lze toto prázdné řešení smazat v jeho detailu.
{% else %} {% else %}
Všechna odevzdání od účastníka k úloze můžete vidět po kliknutí na ikonku <span class="icon">🔍</span>. Všechna odevzdání od účastníka k úloze můžete vidět po kliknutí na ikonku <span class="icon">🔍</span>.
Tamtéž můžete nahrávat skeny jednotlivých řešení.
Odkazem v záhlaví se lze dostat na detailní výpis odevzdání všech uživatelů pro Odkazem v záhlaví se lze dostat na detailní výpis odevzdání všech uživatelů pro
konkrétní úlohu. Symbol <span class="icon">🗐</span> značí, že existuje více verzí dostupných v detailu. konkrétní úlohu. Symbol <span class="icon">🗐</span> značí, že existuje více verzí dostupných v detailu.
{% endif %} {% endif %}
......
...@@ -7,6 +7,15 @@ ...@@ -7,6 +7,15 @@
{% endblock %} {% endblock %}
{% block body %} {% block body %}
{% if ctx.contest %}
<p><em>Tato funkce se používá, pokud jste si stáhli soubory účastnických řešení a opravovali
je elektronicky. Pokud opravená řešení skenujete z papíru, nahrávejte je přes
<a href='{{ ctx.url_for('org_contest_solutions') }}'>tabulku odevzdaných řešení</a>.
Pokud skenujete předtištěné protokoly, použijte
<a href='{{ ctx.url_for('org_contest_scans') }}'>zpracování skenů</a>.
</em>
{% endif %}
<p>Zde můžete najednou nahrát více opravených řešení zabalených do souboru typu ZIP. <p>Zde můžete najednou nahrát více opravených řešení zabalených do souboru typu ZIP.
Maximální možná velikost ZIPu je {{ max_size|data_size }}. Maximální možná velikost ZIPu je {{ max_size|data_size }}.
......
...@@ -5,6 +5,7 @@ import flask_wtf.file ...@@ -5,6 +5,7 @@ import flask_wtf.file
import hashlib import hashlib
import hmac import hmac
from sqlalchemy import and_ from sqlalchemy import and_
from sqlalchemy.dialects.postgresql import insert as pgsql_insert
from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload
from typing import List, Tuple, Optional from typing import List, Tuple, Optional
import werkzeug.exceptions import werkzeug.exceptions
...@@ -221,7 +222,6 @@ def join_create_pion(c: db.Contest) -> None: ...@@ -221,7 +222,6 @@ def join_create_pion(c: db.Contest) -> None:
state = db.PartState.registered state = db.PartState.registered
p = db.Participation(user=g.user, contest=c, place=c.place, state=state) p = db.Participation(user=g.user, contest=c, place=c.place, state=state)
sess.add(p) sess.add(p)
sess.flush() # Kvůli logování
logger.info(f'Join: Účastník #{g.user.user_id} přihlášen do soutěže #{c.contest_id}') logger.info(f'Join: Účastník #{g.user.user_id} přihlášen do soutěže #{c.contest_id}')
mo.util.log( mo.util.log(
...@@ -410,22 +410,25 @@ def user_contest_task(contest_id: int, task_id: int): ...@@ -410,22 +410,25 @@ def user_contest_task(contest_id: int, task_id: int):
flash(f'Chyba: {e}', 'danger') flash(f'Chyba: {e}', 'danger')
return redirect(url_for('user_contest_task', contest_id=contest_id, task_id=task_id)) return redirect(url_for('user_contest_task', contest_id=contest_id, task_id=task_id))
is_broken = paper.is_broken()
sess.add(paper) sess.add(paper)
sess.flush()
# FIXME: Bylo by hezké použít INSERT ... ON CONFLICT UPDATE sess.connection().execute(
# (SQLAlchemy to umí, ale ne přes ORM, jen core rozhraním) pgsql_insert(db.Solution.__table__)
sol = (sess.query(db.Solution) .values(
.filter_by(task=task, user=g.user) task_id=task.task_id,
.with_for_update() user_id=g.user.user_id,
.one_or_none()) final_submit=paper.paper_id,
if sol is None: )
sol = db.Solution(task=task, user=g.user) .on_conflict_do_update(
sess.add(sol) constraint='solutions_pkey',
sol.final_submit_obj = paper set_={'final_submit': paper.paper_id},
)
)
sess.commit() sess.commit()
if paper.is_broken(): if is_broken:
flash('Soubor není korektní PDF, ale přesto jsme ho přijali a pokusíme se ho zpracovat. ' + flash('Soubor není korektní PDF, ale přesto jsme ho přijali a pokusíme se ho zpracovat. ' +
'Zkontrolujte prosím, že se na vašem počítači zobrazuje správně.', 'Zkontrolujte prosím, že se na vašem počítači zobrazuje správně.',
'warning') 'warning')
......