59 files
This commit is contained in:
@@ -54,6 +54,24 @@ def parse_datetime(value: str | datetime | None) -> datetime | None:
|
||||
return None
|
||||
|
||||
|
||||
def parse_optional_points(value: Any) -> int | None:
|
||||
"""Parse a stored points figure, or None for "not recorded".
|
||||
|
||||
Used for fields that snapshot a value at the moment it applied. ``None``
|
||||
marks a record written before the field existed, so callers can fall back
|
||||
to recalculating; anything unusable is treated the same way rather than
|
||||
being trusted as a number.
|
||||
"""
|
||||
if value is None or isinstance(value, bool):
|
||||
return None
|
||||
try:
|
||||
points = int(value)
|
||||
except (ValueError, TypeError, OverflowError):
|
||||
_LOGGER.warning("Ignoring unusable stored points value %r", value)
|
||||
return None
|
||||
return None if points < 0 else points
|
||||
|
||||
|
||||
def format_datetime(dt: datetime | None) -> str | None:
|
||||
"""Format a datetime as ISO string with timezone info.
|
||||
|
||||
@@ -260,6 +278,12 @@ class Chore:
|
||||
time_category: str = "anytime" # morning, afternoon, evening, night, anytime
|
||||
claim_allowance_minutes: int = 0 # Grace minutes past period end during which the chore stays claimable; 0 = no grace. Night chores still cap at midnight.
|
||||
daily_limit: int = 1
|
||||
# Weekly target (#883): "do this N times this week, on whichever days you
|
||||
# like" — instrument practice, reading, exercise. 0 = off. Monday-anchored
|
||||
# to match Challenges. It caps the week the way daily_limit caps the day,
|
||||
# and counts pending completions too, so a slow approval never hands the
|
||||
# child a spare go.
|
||||
weekly_target: int = 0
|
||||
completion_sound: str = "coin" # Sound to play on completion
|
||||
# Optional picture for the chore. Text-free pre-reader mode (#683) needs
|
||||
# one per chore; everything else falls back to the time-of-day icon.
|
||||
@@ -375,6 +399,7 @@ class Chore:
|
||||
time_category=data.get("time_category", "anytime"),
|
||||
claim_allowance_minutes=max(0, int(data.get("claim_allowance_minutes", 0) or 0)),
|
||||
daily_limit=data.get("daily_limit", 1),
|
||||
weekly_target=int(data.get("weekly_target", 0) or 0),
|
||||
completion_sound=data.get("completion_sound", "coin"),
|
||||
icon=str(data.get("icon", "") or ""),
|
||||
image_url=str(data.get("image_url", "") or ""),
|
||||
@@ -442,6 +467,7 @@ class Chore:
|
||||
"time_category": self.time_category,
|
||||
"claim_allowance_minutes": self.claim_allowance_minutes,
|
||||
"daily_limit": self.daily_limit,
|
||||
"weekly_target": self.weekly_target,
|
||||
"completion_sound": self.completion_sound,
|
||||
"icon": self.icon,
|
||||
"image_url": self.image_url,
|
||||
@@ -721,6 +747,10 @@ class ChoreCompletion:
|
||||
note: str = "" # child's own description of the work (open-ended chores, #832)
|
||||
suggested_points: int = 0 # what the child reckons it was worth (#832)
|
||||
id: str = field(default_factory=generate_id)
|
||||
# The award this submission was worth when it was made, before the streak
|
||||
# and weekend multipliers that _award_points adds on top. None on records
|
||||
# written before the field existed.
|
||||
submitted_points: int | None = None
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> ChoreCompletion:
|
||||
@@ -741,6 +771,7 @@ class ChoreCompletion:
|
||||
note=data.get("note", ""),
|
||||
suggested_points=int(data.get("suggested_points", 0) or 0),
|
||||
id=data.get("id") or generate_id(),
|
||||
submitted_points=parse_optional_points(data.get("submitted_points")),
|
||||
)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
@@ -758,6 +789,7 @@ class ChoreCompletion:
|
||||
"note": self.note,
|
||||
"suggested_points": self.suggested_points,
|
||||
"id": self.id,
|
||||
"submitted_points": self.submitted_points,
|
||||
}
|
||||
|
||||
|
||||
@@ -820,6 +852,9 @@ class RewardClaim:
|
||||
approved: bool = False
|
||||
approved_at: datetime | None = None
|
||||
id: str = field(default_factory=generate_id)
|
||||
# What this purchase actually cost, recorded when it was approved. None on
|
||||
# a pending claim, and on claims approved before the field existed.
|
||||
approved_cost: int | None = None
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> RewardClaim:
|
||||
@@ -834,6 +869,7 @@ class RewardClaim:
|
||||
approved=data.get("approved", False),
|
||||
approved_at=approved_at,
|
||||
id=data.get("id") or generate_id(),
|
||||
approved_cost=parse_optional_points(data.get("approved_cost")),
|
||||
)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
@@ -845,6 +881,7 @@ class RewardClaim:
|
||||
"approved": self.approved,
|
||||
"approved_at": format_datetime(self.approved_at),
|
||||
"id": self.id,
|
||||
"approved_cost": self.approved_cost,
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user