Source code for tim_common.markupmodels

from copy import copy
from dataclasses import dataclass, field, fields, is_dataclass
from datetime import datetime, timezone
from typing import Any, Mapping, NewType

import marshmallow
from marshmallow import missing, pre_load

from tim_common.utils import Missing


[docs]@dataclass class PointsRule:
[docs] class Meta: unknown = "EXCLUDE" # Plugins may have custom rules - TIM can ignore them.
maxPoints: str | int | float | None | Missing = missing allowUserMin: int | float | None | Missing = missing allowUserMax: int | float | None | Missing = missing multiplier: int | float | None | Missing = missing penalties: dict[str, float] | None | Missing = missing
[docs]class PluginDateTimeField(marshmallow.fields.Field): def _serialize( self, value: Any, attr: str, obj: Any, **kwargs: dict[str, Any] ) -> Any: raise NotImplementedError def _deserialize( self, value: Any, attr: str | None, data: Mapping[str, Any] | None, **kwargs: dict[str, Any], ) -> datetime: d = None if isinstance(value, datetime): d = value elif isinstance(value, str): try: d = datetime.strptime(value, "%Y-%m-%d %H:%M:%S") except ValueError: raise self.make_error("validator_failed") if d: if d.tzinfo is None: d = d.replace(tzinfo=timezone.utc) return d raise self.make_error("validator_failed")
PluginDateTime = NewType("PluginDateTime", datetime) PluginDateTime._marshmallow_field = PluginDateTimeField # type: ignore
[docs]class HiddenFieldsMixin:
[docs] @pre_load def process_minus(self, data: Any, **_: dict[str, Any]) -> Any: if isinstance(data, dict): data = copy(data) # Don't modify the original. hidden_keys = { k[1:] for k in data.keys() if isinstance(k, str) and k.startswith("-") } for k in hidden_keys: data[k] = data.pop(f"-{k}") data["hidden_keys"] = hidden_keys return data
[docs]@dataclass class KnownMarkupFields(HiddenFieldsMixin): """Represents the plugin markup fields that are known and used by TIM.""" accessDuration: int | None | Missing = missing accessEndText: str | None | Missing = missing anonymous: bool | None | Missing = missing answerLimit: int | None | Missing = missing automd: bool | None | Missing = missing buttonNewTask: str | None | Missing = missing cache: bool | None | Missing = missing deadline: PluginDateTime | datetime | None | Missing = missing fields: list[str] | None | Missing = missing floatHeader: str | None | Missing = missing floatSize: tuple[int, int] | None | Missing = missing header: str | None | Missing = missing headerText: str | None | Missing = missing hideBrowser: bool | Missing | None = missing initNewAnswer: str | None | Missing = missing lazy: bool | Missing = missing maxHeight: str | None | Missing = field( metadata={"data_key": "max-height"}, default=missing ) minHeight: str | None | Missing = field( metadata={"data_key": "min-height"}, default=missing ) pointsRule: PointsRule | None | Missing = missing pointsText: str | None | Missing = missing postprogram: str | Missing = missing postoutput: str | Missing = missing showPoints: bool | None | Missing = missing starttime: PluginDateTime | datetime | None | Missing = missing showInView: bool | Missing = missing stem: str | None | Missing = missing triesText: str | None | Missing = missing useCurrentUser: bool | None | Missing = missing texafterprint: str | None | Missing = missing texbeforeprint: str | None | Missing = missing texprint: str | None | Missing = missing
[docs] def show_points(self) -> bool: if isinstance(self.showPoints, bool): return self.showPoints return True
[docs] def tries_text(self) -> str: if isinstance(self.triesText, str): return self.triesText return "Tries left:"
[docs] def points_text(self) -> str: if isinstance(self.pointsText, str): return self.pointsText return "Points:"
[docs]def asdict_skip_missing(obj: Any) -> dict[str, Any]: result = [] for f in fields(obj): v = getattr(obj, f.name) if v is missing: continue if f.metadata.get("missing", False): continue value = asdict_skip_missing(v) if is_dataclass(v) else v result.append((f.name, value)) return dict(result)
[docs]def list_not_missing_fields(inst: Any) -> list: return list(((k, v) for k, v in asdict_skip_missing(inst).items()))
[docs]@dataclass class UndoInfo: button: str | None | Missing = missing title: str | None | Missing = missing confirmation: str | None | Missing = missing confirmationTitle: str | None | Missing = missing
[docs]@dataclass class GenericMarkupModel(KnownMarkupFields): """Specifies which fields the editor can use in the plugin markup. This base class defines some fields that are applicable to all plugins. The difference to KnownMarkupFields is that this class should define fields that are not used by TIM. TODO: Some fields here should be moved to KnownMarkupFields. """ hidden_keys: list[str] | Missing = missing """Meta field that keeps track which markup fields were hidden (that is, prefixed with "-"). Hidden keys are never sent to browser. """ button: str | None | Missing = missing buttonText: str | None | Missing = missing allowUnsavedLeave: bool | Missing | None = missing disableUnchanged: bool | Missing | None = missing footer: str | Missing = missing forceBrowser: bool | Missing | None = missing globalField: bool | Missing | None = missing readonly: bool | Missing | None = missing lang: str | None | Missing = missing resetText: str | Missing | None = missing connectionErrorMessage: str | Missing = missing undo: UndoInfo | Missing | None = missing
[docs] def get_visible_data(self) -> dict: assert isinstance(self.hidden_keys, list) return { k: v for k, v in list_not_missing_fields(self) if k not in self.hidden_keys and k != "hidden_keys" }