59 files
This commit is contained in:
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from calendar import monthrange
|
||||
from datetime import date, datetime
|
||||
from datetime import date, datetime, timedelta
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from homeassistant.util import dt as dt_util
|
||||
@@ -833,6 +833,18 @@ class ChoresMixin:
|
||||
)
|
||||
return None
|
||||
|
||||
# Weekly target (#883): the same cap one level up. Parents completing on
|
||||
# behalf are bound by it too — the quota is the point of the chore, and
|
||||
# the daily limit above already treats them the same way.
|
||||
if self.weekly_target_met(chore, child_id):
|
||||
_LOGGER.debug(
|
||||
"complete_chore no-op: weekly target reached for '%s' (%d/%d this week)",
|
||||
chore.name,
|
||||
self.weekly_completion_count(chore_id, child_id),
|
||||
getattr(chore, "weekly_target", 0),
|
||||
)
|
||||
return None
|
||||
|
||||
# A photo-required chore always goes through parent approval (unless a
|
||||
# parent is completing on behalf), so the evidence gets reviewed.
|
||||
requires_photo = bool(getattr(chore, "require_photo", False))
|
||||
@@ -869,6 +881,7 @@ class ChoresMixin:
|
||||
completed_at=now,
|
||||
approved=auto_approve,
|
||||
points_awarded=effective_points if auto_approve else 0,
|
||||
submitted_points=effective_points,
|
||||
photo_url=photo_url or "",
|
||||
note=note,
|
||||
suggested_points=suggested_points,
|
||||
@@ -972,6 +985,7 @@ class ChoresMixin:
|
||||
approved=True,
|
||||
approved_at=now,
|
||||
points_awarded=0,
|
||||
submitted_points=0,
|
||||
)
|
||||
|
||||
self.storage.add_completion(completion)
|
||||
@@ -1058,6 +1072,7 @@ class ChoresMixin:
|
||||
completed_at=now,
|
||||
approved=not chore.requires_approval,
|
||||
points_awarded=subtask.points if not chore.requires_approval else 0,
|
||||
submitted_points=subtask.points,
|
||||
bonus_subtask_id=bonus_subtask_id,
|
||||
)
|
||||
|
||||
@@ -1093,8 +1108,9 @@ class ChoresMixin:
|
||||
worth (#832) — needed for open-ended submissions, useful for any chore.
|
||||
It replaces the chore's *base* points, so the streak and level
|
||||
multipliers ``_award_points`` applies still ride on top exactly as they
|
||||
would for an ordinary approval. ``None`` means "use the chore's value"
|
||||
and leaves the existing behaviour untouched.
|
||||
would for an ordinary approval. ``None`` means "pay what the submission
|
||||
was worth when it was made", falling back to the chore's value for
|
||||
completions recorded before that was stored.
|
||||
"""
|
||||
completions = self.storage.get_completions()
|
||||
for completion in completions:
|
||||
@@ -1111,7 +1127,15 @@ class ChoresMixin:
|
||||
if chore and child:
|
||||
comp_date = dt_util.as_local(completion.completed_at).date()
|
||||
is_bonus = bool(completion.bonus_subtask_id)
|
||||
if is_bonus:
|
||||
if completion.submitted_points is not None:
|
||||
# Pay what the child was promised when they submitted.
|
||||
# Recalculating here reads the chore's *current* points
|
||||
# — so an edit made while the work sat in the queue
|
||||
# changed the price after the fact — and silently drops
|
||||
# the speed bonus and roulette multiplier that were in
|
||||
# the figure they saw.
|
||||
pts = completion.submitted_points
|
||||
elif is_bonus:
|
||||
subtask = next((b for b in chore.bonus_subtasks if b.id == completion.bonus_subtask_id), None)
|
||||
pts = subtask.points if subtask else 0
|
||||
elif completion.timed_duration_seconds > 0 and chore.task_type == "timed":
|
||||
@@ -1601,6 +1625,34 @@ class ChoresMixin:
|
||||
days_since = (today - last_dt).days
|
||||
return days_since >= window_days
|
||||
|
||||
def weekly_completion_count(self, chore_id: str, child_id: str) -> int:
|
||||
"""How many times ``child_id`` has completed ``chore_id`` this week.
|
||||
|
||||
Monday-anchored, matching the Challenges period convention. Bonus
|
||||
sub-tasks don't count; pending completions do, so a chore can't be
|
||||
submitted a fourth time just because the parent hasn't got round to
|
||||
approving the third yet.
|
||||
"""
|
||||
today = dt_util.as_local(dt_util.now()).date()
|
||||
week_start = today - timedelta(days=today.weekday())
|
||||
count = 0
|
||||
for comp in self._cached_completions_for_chore(chore_id):
|
||||
if comp.child_id != child_id or getattr(comp, "bonus_subtask_id", ""):
|
||||
continue
|
||||
try:
|
||||
if dt_util.as_local(comp.completed_at).date() >= week_start:
|
||||
count += 1
|
||||
except (AttributeError, TypeError, ValueError):
|
||||
continue
|
||||
return count
|
||||
|
||||
def weekly_target_met(self, chore, child_id: str) -> bool:
|
||||
"""True when a weekly-target chore has had its quota filled this week."""
|
||||
target = int(getattr(chore, "weekly_target", 0) or 0)
|
||||
if target <= 0:
|
||||
return False
|
||||
return self.weekly_completion_count(chore.id, child_id) >= target
|
||||
|
||||
def _is_chore_completable_by_child(self, chore, child_id: str) -> bool:
|
||||
"""Whether ``child_id`` may complete ``chore`` right now (card parity).
|
||||
|
||||
@@ -1657,6 +1709,10 @@ class ChoresMixin:
|
||||
continue
|
||||
if done >= limit:
|
||||
continue
|
||||
# Weekly target (#883): filled the week's quota, so nothing is
|
||||
# owed until Monday even on a day the chore is scheduled.
|
||||
if self.weekly_target_met(chore, child_id):
|
||||
continue
|
||||
out.append(chore)
|
||||
return out
|
||||
|
||||
|
||||
Reference in New Issue
Block a user