From e3331b7022ec1fb63a94dffe37cedb831ffe360f Mon Sep 17 00:00:00 2001 From: Martin Mares <mj@ucw.cz> Date: Fri, 19 Feb 2021 18:11:03 +0100 Subject: [PATCH] =?UTF-8?q?Import:=20Bodov=C3=A1n=C3=AD=20um=C3=AD=20i=20z?= =?UTF-8?q?akl=C3=A1dat/odstra=C5=88ovat=20=C5=99e=C5=A1en=C3=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mo/imports.py | 87 +++++++++++++++---- mo/web/org_contest.py | 7 +- .../templates/org_generic_batch_points.html | 3 +- 3 files changed, 74 insertions(+), 23 deletions(-) diff --git a/mo/imports.py b/mo/imports.py index 0648efb3..1922896b 100644 --- a/mo/imports.py +++ b/mo/imports.py @@ -4,7 +4,7 @@ import io import re from sqlalchemy import and_ 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 from mo.csv import FileFormat, MissingHeaderError @@ -42,6 +42,8 @@ class Import: cnt_new_participations: int = 0 cnt_new_roles: int = 0 cnt_set_points: int = 0 + cnt_add_sols: int = 0 + cnt_del_sols: int = 0 # Veřejné vlastnosti importu template_basename: str = "sablona" @@ -50,7 +52,8 @@ class Import: user: db.User round: Optional[db.Round] 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 row_class: Type[mo.csv.Row] row_example: mo.csv.Row @@ -202,9 +205,13 @@ class Import: self.new_user_ids.append(user.user_id) 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 == "": - 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: pts = int(points_str) @@ -329,6 +336,8 @@ class Import: ('p-ions', self.cnt_new_participations), ('roles', self.cnt_new_roles), ('points', self.cnt_set_points), + ('add-sols', self.cnt_add_sols), + ('del-sols', self.cnt_del_sols), ]: if val > 0: args.append(f'{key}={val}') @@ -628,14 +637,16 @@ class PointsImport(Import): if (len(self.errors) > num_prev_errs or user_id is None or krestni is None - or prijmeni is None): + or prijmeni is None + or body is None): return assert self.round is not None assert self.task is not None + task_id = self.task.task_id 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() if not pion_sols: return self.error('Soutěžící nenalezen v tomto kole') @@ -656,30 +667,66 @@ class PointsImport(Import): return self.error('Neodpovídá ID a jméno soutěžícího') if sol is None: - return self.error('Tento soutěžící úlohu neodevzdal') + if body == 'X': + return + if not self.allow_add_del: + 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: - sol.points = body + points = body if isinstance(body, int) else None + if sol.points != points: + sol.points = points sess.add(db.PointsHistory( task=self.task, participant_id=user_id, user=self.user, points_at=mo.now, - points=body, + points=points, )) self.cnt_set_points += 1 def get_template(self) -> str: rows = [] for pion, sol in sorted(self._pion_sol_query().all(), key=lambda pair: pair[0].user.sort_key()): - if sol is not None: - user = pion.user - rows.append(PointsImportRow( - user_id=user.user_id, - krestni=user.first_name, - prijmeni=user.last_name, - body=sol.points, - )) + if sol is None: + pts = 'X' + elif sol.points is None: + pts = '?' + else: + pts = str(sol.points) + user = pion.user + rows.append(PointsImportRow( + user_id=user.user_id, + krestni=user.first_name, + prijmeni=user.last_name, + body=pts, + )) out = io.StringIO() mo.csv.write(file=out, fmt=self.fmt, row_class=self.row_class, rows=rows) @@ -691,7 +738,8 @@ def create_import(user: db.User, fmt: FileFormat, round: Optional[db.Round] = None, contest: Optional[db.Contest] = None, - task: Optional[db.Task] = None): + task: Optional[db.Task] = None, + allow_add_del: bool = False): imp: Import if type == ImportType.participants: imp = ContestImport() @@ -708,6 +756,7 @@ def create_import(user: db.User, imp.round = round imp.contest = contest imp.task = task + imp.allow_add_del = allow_add_del imp.fmt = fmt imp.gatekeeper = mo.rights.Gatekeeper(user) imp.setup() diff --git a/mo/web/org_contest.py b/mo/web/org_contest.py index 839b3435..8b0d5edd 100644 --- a/mo/web/org_contest.py +++ b/mo/web/org_contest.py @@ -15,7 +15,7 @@ import wtforms import mo from mo.csv import FileFormat import mo.db as db -from mo.imports import ImportType, create_import +from mo.imports import ImportType, Import, create_import import mo.jobs.submit from mo.rights import Right, Rights import mo.util @@ -1161,6 +1161,7 @@ class BatchPointsForm(FlaskForm): choices=FileFormat.choices(), coerce=FileFormat.coerce, default=FileFormat.cs_csv, ) + add_del_sols = wtforms.BooleanField('Zakládat / mazat řešení', description='Xyzzy') submit = wtforms.SubmitField('Nahrát body') get_template = wtforms.SubmitField('Stáhnout šablonu') @@ -1172,7 +1173,7 @@ def generic_batch_points(round: db.Round, contest: Optional[db.Contest], task: d errs = [] if form.validate_on_submit(): 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.file.data is not None: file = form.file.data.stream @@ -1182,7 +1183,7 @@ def generic_batch_points(round: db.Round, contest: Optional[db.Contest], task: d if imp.cnt_rows == 0: flash('Soubor neobsahoval žádné řádky s daty', 'danger') 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: return redirect(url_for('org_contest', id=contest.contest_id)) else: diff --git a/mo/web/templates/org_generic_batch_points.html b/mo/web/templates/org_generic_batch_points.html index 26246028..646b7647 100644 --- a/mo/web/templates/org_generic_batch_points.html +++ b/mo/web/templates/org_generic_batch_points.html @@ -19,7 +19,8 @@ {% endif %} <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'}) }} -- GitLab