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

Import: Bodování umí i zakládat/odstraňovat řešení

parent d60c8905
Branches
No related tags found
1 merge request!32Dávkové bodování
This commit is part of merge request !32. Comments created here will be created in the context of that merge request.
...@@ -4,7 +4,7 @@ import io ...@@ -4,7 +4,7 @@ import io
import re import re
from sqlalchemy import and_ from sqlalchemy import and_
from sqlalchemy.orm import joinedload, Query from sqlalchemy.orm import joinedload, Query
from typing import List, Optional, Any, Dict, Type from typing import List, Optional, Any, Dict, Type, Union
import mo.csv import mo.csv
from mo.csv import FileFormat, MissingHeaderError from mo.csv import FileFormat, MissingHeaderError
...@@ -42,6 +42,8 @@ class Import: ...@@ -42,6 +42,8 @@ class Import:
cnt_new_participations: int = 0 cnt_new_participations: int = 0
cnt_new_roles: int = 0 cnt_new_roles: int = 0
cnt_set_points: int = 0 cnt_set_points: int = 0
cnt_add_sols: int = 0
cnt_del_sols: int = 0
# Veřejné vlastnosti importu # Veřejné vlastnosti importu
template_basename: str = "sablona" template_basename: str = "sablona"
...@@ -50,7 +52,8 @@ class Import: ...@@ -50,7 +52,8 @@ class Import:
user: db.User user: db.User
round: Optional[db.Round] round: Optional[db.Round]
contest: Optional[db.Contest] contest: Optional[db.Contest]
task: Optional[db.Task] task: Optional[db.Task] # pro Import bodů
allow_add_del: bool # pro Import bodů: je povoleno zakládat/mazat řešení
fmt: FileFormat fmt: FileFormat
row_class: Type[mo.csv.Row] row_class: Type[mo.csv.Row]
row_example: mo.csv.Row row_example: mo.csv.Row
...@@ -202,9 +205,13 @@ class Import: ...@@ -202,9 +205,13 @@ class Import:
self.new_user_ids.append(user.user_id) self.new_user_ids.append(user.user_id)
return user return user
def parse_points(self, points_str: str) -> Optional[int]: def parse_points(self, points_str: str) -> Union[int, str, None]:
if points_str == "": if points_str == "":
return None return self.error('Body musí být vyplněny')
points_str = points_str.upper()
if points_str in ['X', '?']:
return points_str
try: try:
pts = int(points_str) pts = int(points_str)
...@@ -329,6 +336,8 @@ class Import: ...@@ -329,6 +336,8 @@ class Import:
('p-ions', self.cnt_new_participations), ('p-ions', self.cnt_new_participations),
('roles', self.cnt_new_roles), ('roles', self.cnt_new_roles),
('points', self.cnt_set_points), ('points', self.cnt_set_points),
('add-sols', self.cnt_add_sols),
('del-sols', self.cnt_del_sols),
]: ]:
if val > 0: if val > 0:
args.append(f'{key}={val}') args.append(f'{key}={val}')
...@@ -628,14 +637,16 @@ class PointsImport(Import): ...@@ -628,14 +637,16 @@ class PointsImport(Import):
if (len(self.errors) > num_prev_errs if (len(self.errors) > num_prev_errs
or user_id is None or user_id is None
or krestni is None or krestni is None
or prijmeni is None): or prijmeni is None
or body is None):
return return
assert self.round is not None assert self.round is not None
assert self.task is not None assert self.task is not None
task_id = self.task.task_id
sess = db.get_session() sess = db.get_session()
query = self._pion_sol_query().filter(db.Solution.user_id == user_id) query = self._pion_sol_query().filter(db.Participation.user_id == user_id)
pion_sols = query.all() pion_sols = query.all()
if not pion_sols: if not pion_sols:
return self.error('Soutěžící nenalezen v tomto kole') return self.error('Soutěžící nenalezen v tomto kole')
...@@ -656,29 +667,65 @@ class PointsImport(Import): ...@@ -656,29 +667,65 @@ class PointsImport(Import):
return self.error('Neodpovídá ID a jméno soutěžícího') return self.error('Neodpovídá ID a jméno soutěžícího')
if sol is None: if sol is None:
if body == 'X':
return
if not self.allow_add_del:
return self.error('Tento soutěžící úlohu neodevzdal') return self.error('Tento soutěžící úlohu neodevzdal')
if not rights.can_upload_solutions(round):
return self.error('Nemáte právo na zakládání nových řešení')
sol = db.Solution(user_id=user_id, task_id=task_id)
sess.add(sol)
logger.info(f'Import: Založeno řešení user=#{user_id} task=#{task_id}')
mo.util.log(
type=db.LogType.participant,
what=user_id,
details={'action': 'solution-created', 'task': task_id},
)
self.cnt_add_sols += 1
elif body == 'X':
if not self.allow_add_del:
return self.error('Tento soutěžící úlohu odevzdal')
if sol.final_submit is not None or sol.final_feedback is not None:
return self.error('Nelze smazat řešení, ke kterému existují odevzdané soubory')
if not rights.can_upload_solutions(round):
return self.error('Nemáte právo na mazání řešení')
logger.info(f'Import: Smazáno řešení user=#{user_id} task=#{task_id}')
mo.util.log(
type=db.LogType.participant,
what=user_id,
details={'action': 'solution-removed', 'task': task_id},
)
self.cnt_del_sols += 1
sess.delete(sol)
return
if sol.points != body: points = body if isinstance(body, int) else None
sol.points = body if sol.points != points:
sol.points = points
sess.add(db.PointsHistory( sess.add(db.PointsHistory(
task=self.task, task=self.task,
participant_id=user_id, participant_id=user_id,
user=self.user, user=self.user,
points_at=mo.now, points_at=mo.now,
points=body, points=points,
)) ))
self.cnt_set_points += 1 self.cnt_set_points += 1
def get_template(self) -> str: def get_template(self) -> str:
rows = [] rows = []
for pion, sol in sorted(self._pion_sol_query().all(), key=lambda pair: pair[0].user.sort_key()): for pion, sol in sorted(self._pion_sol_query().all(), key=lambda pair: pair[0].user.sort_key()):
if sol is not None: if sol is None:
pts = 'X'
elif sol.points is None:
pts = '?'
else:
pts = str(sol.points)
user = pion.user user = pion.user
rows.append(PointsImportRow( rows.append(PointsImportRow(
user_id=user.user_id, user_id=user.user_id,
krestni=user.first_name, krestni=user.first_name,
prijmeni=user.last_name, prijmeni=user.last_name,
body=sol.points, body=pts,
)) ))
out = io.StringIO() out = io.StringIO()
...@@ -691,7 +738,8 @@ def create_import(user: db.User, ...@@ -691,7 +738,8 @@ def create_import(user: db.User,
fmt: FileFormat, fmt: FileFormat,
round: Optional[db.Round] = None, round: Optional[db.Round] = None,
contest: Optional[db.Contest] = None, contest: Optional[db.Contest] = None,
task: Optional[db.Task] = None): task: Optional[db.Task] = None,
allow_add_del: bool = False):
imp: Import imp: Import
if type == ImportType.participants: if type == ImportType.participants:
imp = ContestImport() imp = ContestImport()
...@@ -708,6 +756,7 @@ def create_import(user: db.User, ...@@ -708,6 +756,7 @@ def create_import(user: db.User,
imp.round = round imp.round = round
imp.contest = contest imp.contest = contest
imp.task = task imp.task = task
imp.allow_add_del = allow_add_del
imp.fmt = fmt imp.fmt = fmt
imp.gatekeeper = mo.rights.Gatekeeper(user) imp.gatekeeper = mo.rights.Gatekeeper(user)
imp.setup() imp.setup()
......
...@@ -15,7 +15,7 @@ import wtforms ...@@ -15,7 +15,7 @@ import wtforms
import mo import mo
from mo.csv import FileFormat from mo.csv import FileFormat
import mo.db as db import mo.db as db
from mo.imports import ImportType, create_import from mo.imports import ImportType, Import, create_import
import mo.jobs.submit import mo.jobs.submit
from mo.rights import Right, Rights from mo.rights import Right, Rights
import mo.util import mo.util
...@@ -1161,6 +1161,7 @@ class BatchPointsForm(FlaskForm): ...@@ -1161,6 +1161,7 @@ class BatchPointsForm(FlaskForm):
choices=FileFormat.choices(), coerce=FileFormat.coerce, choices=FileFormat.choices(), coerce=FileFormat.coerce,
default=FileFormat.cs_csv, default=FileFormat.cs_csv,
) )
add_del_sols = wtforms.BooleanField('Zakládat / mazat řešení', description='Xyzzy')
submit = wtforms.SubmitField('Nahrát body') submit = wtforms.SubmitField('Nahrát body')
get_template = wtforms.SubmitField('Stáhnout šablonu') get_template = wtforms.SubmitField('Stáhnout šablonu')
...@@ -1172,7 +1173,7 @@ def generic_batch_points(round: db.Round, contest: Optional[db.Contest], task: d ...@@ -1172,7 +1173,7 @@ def generic_batch_points(round: db.Round, contest: Optional[db.Contest], task: d
errs = [] errs = []
if form.validate_on_submit(): if form.validate_on_submit():
fmt = form.fmt.data fmt = form.fmt.data
imp = create_import(user=g.user, type=ImportType.points, fmt=fmt, round=round, contest=contest, task=task) imp = create_import(user=g.user, type=ImportType.points, fmt=fmt, round=round, contest=contest, task=task, allow_add_del=form.add_del_sols.data)
if form.submit.data: if form.submit.data:
if form.file.data is not None: if form.file.data is not None:
file = form.file.data.stream file = form.file.data.stream
...@@ -1182,7 +1183,7 @@ def generic_batch_points(round: db.Round, contest: Optional[db.Contest], task: d ...@@ -1182,7 +1183,7 @@ def generic_batch_points(round: db.Round, contest: Optional[db.Contest], task: d
if imp.cnt_rows == 0: if imp.cnt_rows == 0:
flash('Soubor neobsahoval žádné řádky s daty', 'danger') flash('Soubor neobsahoval žádné řádky s daty', 'danger')
else: else:
flash(f'Importováno ({imp.cnt_rows} řádků, změněny body u {imp.cnt_set_points} řešení)', 'success') flash(f'Importováno ({imp.cnt_rows} řádků, {imp.cnt_set_points} řešení přebodováno, {imp.cnt_add_sols} založeno a {imp.cnt_del_sols} smazáno)', 'success')
if contest is not None: if contest is not None:
return redirect(url_for('org_contest', id=contest.contest_id)) return redirect(url_for('org_contest', id=contest.contest_id))
else: else:
......
...@@ -19,7 +19,8 @@ ...@@ -19,7 +19,8 @@
{% endif %} {% endif %}
<p>Zde si můžete stáhnout bodovací formulář v zadaném formátu a pak ho nahrát zpět <p>Zde si můžete stáhnout bodovací formulář v zadaném formátu a pak ho nahrát zpět
s vyplněnými body. s vyplněnými body. "<code>?</code>" místo bodů značí dosud neobodované řešení,
"<code>X</code>" značí řešení neodevzdané.
{{ wtf.quick_form(form, form_type='simple', button_map={'submit': 'primary'}) }} {{ wtf.quick_form(form, form_type='simple', button_map={'submit': 'primary'}) }}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment