import enum
import re
from dataclasses import dataclass, field
from typing import Union, Generator, Optional
from marshmallow import ValidationError
from timApp.plugin.taskid import TaskId
from tim_common.marshmallow_dataclass import class_schema
[docs]class PointType(enum.Enum):
task = 1
velp = 2
[docs]class PointCountMethod(enum.Enum):
"""Point count method for scoreboard."""
# Counts points by the latest answer per task.
latest = 1
# Counts points by the answer with the most points per task.
max = 2
[docs]class Group:
def __init__(self, name: str, data: str | dict) -> None:
self.name = name
if isinstance(data, str):
self.matchers = {data}
self.point_types = {PointType.task, PointType.velp}
self.min_points: float = 0
self.max_points: float = 1e100
self.expl: str = "{0}: {1:.1f}"
self.link: bool = False
self.linktext: str | None = None
elif isinstance(data, dict):
match_re = data.get("match", name)
# match can be a single regex or a list of regexes
if isinstance(match_re, str):
self.matchers = {match_re}
elif isinstance(match_re, list):
self.matchers = set(match_re)
else:
raise Exception("Unknown type for match.")
point_type = data.get("type", "vt")
self.point_types = set()
if "v" in point_type:
self.point_types.add(PointType.velp)
if "t" in point_type:
self.point_types.add(PointType.task)
self.min_points = data.get("min_points", 0)
self.max_points = data.get("max_points", 1e100)
self.expl = data.get("expl", "{0}: {1:.1f}")
self.link = data.get("link", False)
self.linktext = data.get("linktext", None)
[docs] def check_match(self, task_id: str) -> bool:
try:
return any(
re.fullmatch(regex, task_id.split(".")[1]) is not None
for regex in self.matchers
)
except re.error:
return False
[docs]@dataclass(frozen=True)
class ScoreboardOptions:
groups: list[str] = field(default_factory=list)
[docs]@dataclass(frozen=True)
class CountModel:
best: int | None = None
worst: int | None = None
# TODO: Add all pointsumrule fields under this.
[docs]@dataclass(frozen=True)
class PointSumRuleModel:
count: CountModel = CountModel(best=9999)
scoreboard: ScoreboardOptions = ScoreboardOptions()
include_groupless: bool = False
point_count_method: PointCountMethod = PointCountMethod.latest
PointSumRuleSchema = class_schema(PointSumRuleModel)
[docs]class PointSumRule:
def __init__(self, data: dict) -> None:
try:
self.groups = {k: Group(k, v) for k, v in data["groups"].items()}
except (AttributeError, KeyError):
self.groups = {}
self.scoreboard_error = None
try:
pr: PointSumRuleModel = PointSumRuleSchema().load(data, unknown="EXCLUDE")
except ValidationError as e:
self.scoreboard_error = e
pr = PointSumRuleModel()
if pr.count.best is not None:
self.count_type, self.count_amount = "best", pr.count.best
elif pr.count.worst is not None:
self.count_type, self.count_amount = "worst", pr.count.worst
self.scoreboard = pr.scoreboard
self.include_groupless = pr.include_groupless
self.point_count_method = pr.point_count_method
self.total = data.get("total", None)
self.hide = data.get("hide", None)
self.sort = data.get("sort", True)
self.count_all = data.get("count_all", False)
self.breaklines = data.get("breaklines", False)
self.force = data.get("force", False)
self.linktext = data.get("linktext", "link")
[docs] def find_groups(self, task_id: str) -> Generator[str, None, None]:
for g in self.groups.values():
if g.check_match(task_id):
yield g.name
[docs] def get_groups(self, task_ids: list[TaskId] | None = None) -> dict[str, Group]:
if task_ids is None:
return self.groups
groups = dict(self.groups)
if self.include_groupless:
for id in task_ids:
if all(not g.check_match(id.doc_task) for g in groups.values()):
groups[id.task_name] = Group(id.task_name, id.doc_task)
return groups