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

Target

Select target project
  • mj/mo-submit
1 result
Select Git revision
Show changes
Commits on Source (10)
......@@ -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,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]:
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
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)
)
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)},
)
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:
......@@ -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.')
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,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í.')
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)
sess.flush() # Kvůli logování
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)
)
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)},
)
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í')
return part, is_new
......@@ -166,28 +201,47 @@ def find_or_create_participation(user: db.User, contest: db.Contest, place: Opti
place = contest.place
sess = db.get_session()
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())
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:
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}')
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
......
......@@ -193,6 +193,10 @@ def user_settings_personal():
sess.commit()
flash('Heslo změněno.', 'success')
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)
if rr:
rr.user_id = user.user_id
......@@ -436,6 +440,7 @@ class Reg2:
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.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: {
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:
email = mo.users.normalize_email(rr.email) # Pro jistotu
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
app.logger.info(f'Reg2: Účet s e-mailem {email} začal během registrace existovat')
self.status = RegStatus.already_exists
return False
user = db.User(
email=email,
first_name=first_name,
last_name=last_name,
)
mo.users.set_password(user, passwd)
mo.users.login(user)
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()
self.user = user
return True
def change_email(self):
def change_email(self) -> bool:
sess = db.get_session()
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
app.logger.info(f'Reg2: Uživatel #{user.user_id} si změnil email na <{user.email}>')
......@@ -543,6 +548,7 @@ class Reg2:
self.rr.used_at = mo.now
sess.commit()
return True
def change_passwd(self, new_passwd: str):
sess = db.get_session()
......@@ -617,13 +623,15 @@ def confirm_email():
form = ConfirmEmailForm()
if form.validate_on_submit():
if form.submit.data:
reg2.change_email()
if reg2.change_email():
flash('E-mail změněn.', 'success')
return redirect(url_for('user_settings'))
elif form.cancel.data:
reg2.spend_request()
flash('Požadavek na změnu e-mailu zrušen.', 'success')
return redirect(url_for('user_settings'))
reg2.flash_message()
form.orig_email.data = reg2.rr.user.email
form.new_email.data = reg2.rr.email
......
......@@ -368,7 +368,7 @@ class ParticipantsActionForm(FlaskForm):
if self.remove_participation.data:
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(
type=db.LogType.participant,
what=u.user_id,
......@@ -383,10 +383,11 @@ class ParticipantsActionForm(FlaskForm):
pion.place_id = participation_place.place_id
elif self.set_contest.data:
pion.contest_id = assert_not_none(contest).contest_id
pion.place_id = assert_not_none(contest_place).place_id
if sess.is_modified(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(
type=db.LogType.participant,
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
bylo možné vyplnit body. Pokud nějaké řešení založíte omylem, lze toto prázdné řešení smazat v jeho detailu.
{% else %}
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
konkrétní úlohu. Symbol <span class="icon">🗐</span> značí, že existuje více verzí dostupných v detailu.
{% endif %}
......
......@@ -7,6 +7,15 @@
{% endblock %}
{% 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.
Maximální možná velikost ZIPu je {{ max_size|data_size }}.
......
......@@ -5,6 +5,7 @@ import flask_wtf.file
import hashlib
import hmac
from sqlalchemy import and_
from sqlalchemy.dialects.postgresql import insert as pgsql_insert
from sqlalchemy.orm import joinedload
from typing import List, Tuple, Optional
import werkzeug.exceptions
......@@ -221,7 +222,6 @@ def join_create_pion(c: db.Contest) -> None:
state = db.PartState.registered
p = db.Participation(user=g.user, contest=c, place=c.place, state=state)
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}')
mo.util.log(
......@@ -410,22 +410,25 @@ def user_contest_task(contest_id: int, task_id: int):
flash(f'Chyba: {e}', 'danger')
return redirect(url_for('user_contest_task', contest_id=contest_id, task_id=task_id))
is_broken = paper.is_broken()
sess.add(paper)
sess.flush()
# FIXME: Bylo by hezké použít INSERT ... ON CONFLICT UPDATE
# (SQLAlchemy to umí, ale ne přes ORM, jen core rozhraním)
sol = (sess.query(db.Solution)
.filter_by(task=task, user=g.user)
.with_for_update()
.one_or_none())
if sol is None:
sol = db.Solution(task=task, user=g.user)
sess.add(sol)
sol.final_submit_obj = paper
sess.connection().execute(
pgsql_insert(db.Solution.__table__)
.values(
task_id=task.task_id,
user_id=g.user.user_id,
final_submit=paper.paper_id,
)
.on_conflict_do_update(
constraint='solutions_pkey',
set_={'final_submit': paper.paper_id},
)
)
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. ' +
'Zkontrolujte prosím, že se na vašem počítači zobrazuje správně.',
'warning')
......