diff --git a/mo/points.py b/mo/points.py new file mode 100644 index 0000000000000000000000000000000000000000..f97099a1f0acca6367f962d7dac361a15738c81f --- /dev/null +++ b/mo/points.py @@ -0,0 +1,154 @@ +from dataclasses import dataclass, field +from decimal import Decimal, InvalidOperation +from enum import auto +from typing import Optional, Tuple, List + +import mo.db as db +import mo.rights +import mo.util +from mo.util import assert_not_none +import mo.util_format + + +@dataclass +class GenPoints: + have_solution: bool = True + is_empty: bool = False + points: Optional[Decimal] = None + error: Optional[str] = None + + @staticmethod + def parse( + inp: Optional[str], + for_task: Optional[db.Task] = None, + for_round: Optional[db.Round] = None, + ) -> 'GenPoints': + """Zobecnění mo.util.parse_points(). Naparsuje generalizované body používané při editaci.""" + + inp = inp.upper() if inp is not None else None + gp = GenPoints() + + if inp is None or inp == 'X': + gp.have_solution = False + return gp + + if inp == "" or inp == '?': + pass + elif inp == 'P': + gp.is_empty = True + gp.points = Decimal(0) + else: + try: + gp.points = Decimal(inp.replace(',', '.')) + except InvalidOperation: + gp.error = f"Hodnota '{inp}' není číslo ani X nebo P" + return gp + gp.error = mo.util.check_points(gp.points, for_task, for_round) + return gp + + +class SolActionError(RuntimeError): + pass + + +@dataclass +class SolAction: + task: db.Task + user: db.User + sol: Optional[db.Solution] + gp: GenPoints + reason: str + rights: mo.rights.RoundRights + to_log: List[str] + allow_add_del: bool = True + did_add_or_del: bool = False + + def add_or_del(self) -> Tuple[bool, bool]: + if (self.sol is not None) == self.gp.have_solution: + return False, False + + sess = db.get_session() + self.did_add_or_del = True + + if self.sol is None: + if not self.allow_add_del: + raise SolActionError('Tento soutěžící úlohu neodevzdal') + if not (self.rights.can_upload_solutions() or self.rights.can_upload_feedback()): + raise SolActionError('Nemáte právo na zakládání nových řešení, můžete jen upravovat body') + self.sol = db.Solution(user=self.user, task=self.task) + sess.add(self.sol) + self.to_log.append(f'Založeno řešení user=#{self.user.user_id} task=#{self.task.task_id}') + mo.util.log( + type=db.LogType.participant, + what=self.user.user_id, + details={'action': 'solution-created', 'task': self.task.task_id, 'reason': self.reason}, + ) + return True, False + else: + if not self.allow_add_del: + raise SolActionError('Tento soutěžící úlohu odevzdal') + if self.sol.final_submit is not None or self.sol.final_feedback is not None: + raise SolActionError('Nelze smazat řešení, ke kterému existují odevzdané soubory') + if not self.rights.can_upload_solutions(): + raise SolActionError('Nemáte právo na mazání řešení') + self.to_log.append(f'Smazáno řešení user=#{self.user.user_id} task=#{self.task.task_id}') + mo.util.log( + type=db.LogType.participant, + what=self.user.user_id, + details={'action': 'solution-removed', 'task': self.task.task_id, 'reason': self.reason}, + ) + sess.delete(self.sol) + self.sol = None + return False, True + + def set_points(self) -> bool: + sol = self.sol + gp = self.gp + + if sol is None: + return False + if gp.is_empty == sol.is_empty and gp.points == sol.points: + return False + + if not self.rights.can_edit_points(): + raise SolActionError('Nemáte právo hodnotit řešení') + + sol.points = gp.points + sol.is_empty = gp.is_empty + sess = db.get_session() + sess.add(db.PointsHistory( + task=self.task, + participant_id=self.user.user_id, + user=mo.util.current_log_user, + points_at=mo.now, + points=gp.points, + is_empty=gp.is_empty, + )) + return True + + def log_changes(self): + sess = db.get_session() + if self.sol and not self.did_add_or_del and sess.is_modified(self.sol): + changes = db.get_object_changes(self.sol) + mo.util.log( + type=db.LogType.participant, + what=self.user.user_id, + details={ + 'action': 'solution-edit', + 'task': self.task.task_id, + 'changes': changes, + 'reason': self.reason, + }, + ) + self.to_log.append(f"Řešení user=#{self.user.user_id} task=#{self.task.task_id} modifikováno, změny: {changes}") + + +def format_sol_editable_points(s: Optional[db.Solution], none_is_qmark: bool = False) -> str: + if s is None: + return 'X' + elif s.is_empty: + return 'P' + elif s.points is None: + return '?' if none_is_qmark else "" + else: + return assert_not_none(mo.util_format.format_decimal(s.points)) diff --git a/mo/util.py b/mo/util.py index 38ef66f0555899709bebb404699edff4131e811e..0874e3a7768c18d4d9351f52b896f4c856317ab3 100644 --- a/mo/util.py +++ b/mo/util.py @@ -4,7 +4,6 @@ from dataclasses import dataclass import datetime import decimal import dateutil.tz -from enum import auto import locale import logging import os @@ -155,34 +154,6 @@ def parse_points( return points, check_points(points, for_task, for_round) -class GPAction(db.MOEnum): - no_solution = auto() - no_points = auto() - is_empty = auto() - has_points = auto() - - -def parse_gen_points( - gen_points: Optional[str], for_task: Optional[db.Task] = None, for_round: Optional[db.Round] = None, -) -> Tuple[GPAction, Optional[decimal.Decimal], Optional[str]]: - """Zobecnění parse_points(). Naparsuje generalizované body používané při editaci. - Vrátí typ hodnocení (GPAction), body (decimal.Decimal nebo None) a případný error.""" - gen_points = gen_points.upper() if gen_points is not None else None - if gen_points is None or gen_points == 'X': - return GPAction.no_solution, None, None - elif gen_points == "" or gen_points == '?': - # Řešení má existovat, ale nemá přidělené body - return GPAction.no_points, None, None - elif gen_points == 'P': - return GPAction.is_empty, decimal.Decimal(0), None - else: - try: - points = decimal.Decimal(gen_points.replace(',', '.')) - except decimal.InvalidOperation: - return "", decimal.Decimal(0), f"Hodnota '{gen_points}' není číslo ani X" - return GPAction.points, points, check_points(points, for_task, for_round) - - def check_points(points: decimal.Decimal, for_task: Optional[db.Task] = None, for_round: Optional[db.Round] = None) -> Optional[str]: """Zkontroluje body. Pokud je vše ok, tak vrátí None, jinak vrátí text chyby.""" if points < 0: