Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions custom_components/taskmate/coord_assignments.py
Original file line number Diff line number Diff line change
Expand Up @@ -673,8 +673,9 @@ def _is_rotation_done_today_uncached(self, chore) -> bool:
completed_bonus_ids_today.add(bonus_id)
continue
# Parent completions count toward the quota. A `__parent__`
# sentinel covers the whole rotation for today.
if comp.child_id in pool or comp.child_id == "__parent__":
# sentinel covers the whole rotation for today — unless it was
# for one child only (#1096), which a rotation never carries.
if comp.child_id in pool or (comp.child_id == "__parent__" and not getattr(comp, "for_child_id", "")):
completions_today += 1
# first_come is a single-winner race: clamp any mis-configured quota to 1.
if getattr(chore, "assignment_mode", "everyone") == "first_come":
Expand Down
37 changes: 30 additions & 7 deletions custom_components/taskmate/coord_chores.py
Original file line number Diff line number Diff line change
Expand Up @@ -882,7 +882,7 @@ async def async_complete_chore(
continue
if (
comp.chore_id == chore_id
and self._counts_toward_child_quota(chore, comp.child_id, child_id)
and self._counts_toward_child_quota(chore, comp.child_id, child_id, getattr(comp, "for_child_id", ""))
and not comp.bonus_subtask_id
):
comp_dt = comp.completed_at
Expand Down Expand Up @@ -1350,8 +1350,12 @@ def week_progress_on(self, day: date) -> dict[str, dict[str, int]]:
progress.setdefault(child.id, {})[chore.id] = count
return progress

async def async_parent_complete_chore(self, chore_id: str) -> ChoreCompletion:
"""Mark a chore as completed by the parent — zero points, advances recurrence."""
async def async_parent_complete_chore(self, chore_id: str, child_id: str | None = None) -> ChoreCompletion:
"""Mark a chore as completed by the parent — zero points, advances recurrence.

``child_id`` dismisses it for that one child only (#1096); without it
the chore is dismissed for every child it's assigned to.
"""
chore = self.get_chore(chore_id)
if not chore:
raise ValueError(f"Chore {chore_id} not found")
Expand All @@ -1372,6 +1376,16 @@ async def async_parent_complete_chore(self, chore_id: str) -> ChoreCompletion:
if not child_ids:
raise ValueError(f"Chore '{chore.name}' has no children to suppress")

if child_id:
# Only an everyone-mode chore gives each child their own copy; a
# rotation or race has one shared slot, so "done for one child"
# means nothing there.
if getattr(chore, "assignment_mode", "everyone") != "everyone":
raise ValueError(f"Chore '{chore.name}' is shared by a rotation and can only be done for everyone")
if child_id not in child_ids:
raise ValueError(f"Chore '{chore.name}' is not assigned to that child")
child_ids = [child_id]

completion = ChoreCompletion(
chore_id=chore_id,
child_id="__parent__",
Expand All @@ -1380,6 +1394,7 @@ async def async_parent_complete_chore(self, chore_id: str) -> ChoreCompletion:
approved_at=now,
points_awarded=0,
submitted_points=0,
for_child_id=child_id or "",
)

self.storage.add_completion(completion)
Expand Down Expand Up @@ -2390,17 +2405,21 @@ def _reverse_weekly_target_bonus(self, completion, child) -> None:
)

@staticmethod
def _counts_toward_child_quota(chore, completion_child_id: str, child_id: str) -> bool:
def _counts_toward_child_quota(chore, completion_child_id: str, child_id: str, for_child_id: str = "") -> bool:
"""Whether a completion counts toward ``child_id``'s daily limit for ``chore``.

A parent completion (``__parent__``) closes the chore for the whole pool,
so it counts for every child — the same rule the child card applies (#1082).
first_come is the exception: there the parent completion fills the shared
race quota instead (``_is_rotation_done_today``), and nobody won it.
A parent completion with ``for_child_id`` set closes it for that one
child only (#1096).
"""
if completion_child_id == child_id:
return True
return completion_child_id == "__parent__" and getattr(chore, "assignment_mode", "everyone") != "first_come"
if completion_child_id != "__parent__" or getattr(chore, "assignment_mode", "everyone") == "first_come":
return False
return not for_child_id or for_child_id == child_id

def _weekly_target_refund_owed(self, completion, child) -> int:
"""What ``_reverse_weekly_target_bonus`` would take back, without doing it."""
Expand Down Expand Up @@ -2469,7 +2488,9 @@ def get_due_chores_for_child(self, child_id: str) -> list:
limit = getattr(chore, "daily_limit", 1) or 1
done = 0
for c in completions:
if c.chore_id != chore.id or not self._counts_toward_child_quota(chore, c.child_id, child_id):
if c.chore_id != chore.id or not self._counts_toward_child_quota(
chore, c.child_id, child_id, getattr(c, "for_child_id", "")
):
continue
if getattr(c, "bonus_subtask_id", ""):
continue
Expand Down Expand Up @@ -2517,7 +2538,9 @@ def get_today_board(self) -> dict:
continue
except (AttributeError, TypeError, ValueError):
continue
done_today.setdefault((c.child_id, c.chore_id), []).append(bool(c.approved))
# A parent completion for one child (#1096) is that child's alone.
owner = getattr(c, "for_child_id", "") or c.child_id
done_today.setdefault((owner, c.chore_id), []).append(bool(c.approved))
chores = self.storage.get_chores()
for child in self.storage.get_children():
items = []
Expand Down
6 changes: 6 additions & 0 deletions custom_components/taskmate/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1175,6 +1175,9 @@ class ChoreCompletion:
# True when nobody reviewed it in time and the auto-approve timeout
# approved it instead.
auto_approved: bool = False
# Set on a parent completion (child_id "__parent__") that dismisses the
# chore for this one child only (#1096). Empty = the whole pool.
for_child_id: str = ""

@classmethod
def from_dict(cls, data: dict[str, Any]) -> ChoreCompletion:
Expand Down Expand Up @@ -1202,6 +1205,7 @@ def from_dict(cls, data: dict[str, Any]) -> ChoreCompletion:
backdated=data.get("backdated") is True,
quantity=_clamp_int(data.get("quantity"), 0, MAX_QUANTITY),
auto_approved=data.get("auto_approved") is True,
for_child_id=str(data.get("for_child_id", "") or ""),
)

def to_dict(self) -> dict[str, Any]:
Expand Down Expand Up @@ -1234,6 +1238,8 @@ def to_dict(self) -> dict[str, Any]:
data["quantity"] = self.quantity
if self.auto_approved:
data["auto_approved"] = True
if self.for_child_id:
data["for_child_id"] = self.for_child_id
return data


Expand Down
4 changes: 4 additions & 0 deletions custom_components/taskmate/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,9 @@ def _build_todays_completions(common: dict) -> list[dict]:
rec["timed_duration_seconds"] = timed_secs
if getattr(comp, "quantity", 0):
rec["quantity"] = comp.quantity
# Parent did it for one child only (#1096); absent = the whole pool.
if getattr(comp, "for_child_id", ""):
rec["for_child_id"] = comp.for_child_id
# Child undo (#918): child_undo_pending / child_undo_until, present only
# on a completion the child may still take back, so the attribute
# stays small (and empty whenever the window is 0).
Expand Down Expand Up @@ -800,6 +803,7 @@ def _build_recent_completions(common: dict, limit: int = 35) -> list[dict]:
else str(comp.completed_at),
# Star rating the parent gave at approval (#927) — only when rated.
**({"rating": comp.quality_rating} if getattr(comp, "quality_rating", 0) else {}),
**({"for_child_id": comp.for_child_id} if getattr(comp, "for_child_id", "") else {}),
}
)
return out
Expand Down
4 changes: 3 additions & 1 deletion custom_components/taskmate/websocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -2507,12 +2507,14 @@ async def _ws_reject_reward(hass, connection, msg, coordinator):
{
vol.Required("type"): WS_PARENT_COMPLETE_CHORE,
vol.Required("chore_id"): str,
# Done for this one child only (#1096); omitted = everyone assigned.
vol.Optional("child_id"): str,
}
)
@websocket_api.async_response
@_admin_only
async def _ws_parent_complete_chore(hass, connection, msg, coordinator):
await coordinator.async_parent_complete_chore(msg["chore_id"])
await coordinator.async_parent_complete_chore(msg["chore_id"], msg.get("child_id") or None)
connection.send_result(msg["id"], {"ok": True})


Expand Down
3 changes: 3 additions & 0 deletions custom_components/taskmate/www/locales/da.json
Original file line number Diff line number Diff line change
Expand Up @@ -1337,6 +1337,9 @@
"panel.parent_complete_failed": "Mislykkedes: {error}",
"panel.parent_complete_success": "Markeret som klaret af forælder",
"panel.parent_complete_tooltip": "Forælder klarede den (ingen point)",
"panel.parent_complete_everyone": "Forælder klarede den for alle (ingen point)",
"panel.parent_complete_for_child": "Forælder klarede den for {name} (ingen point)",
"panel.parent_complete_for_child_confirm": "Markér denne opgave som klaret kun for {name}? Der gives ingen point.",
"panel.penalty_apply_text": "Vælg hvilket barn der skal have straffen (trækker {points} point).",
"panel.penalty_empty_description": "Tilføj en straf for at trække point for dårlig opførsel.",
"panel.penalty_empty_title": "Ingen straffe endnu",
Expand Down
3 changes: 3 additions & 0 deletions custom_components/taskmate/www/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -1285,6 +1285,9 @@
"panel.parent_complete_failed": "Fehlgeschlagen: {error}",
"panel.parent_complete_success": "Als von Eltern erledigt markiert",
"panel.parent_complete_tooltip": "Eltern haben es erledigt (keine Punkte)",
"panel.parent_complete_everyone": "Eltern haben es für alle erledigt (keine Punkte)",
"panel.parent_complete_for_child": "Eltern haben es für {name} erledigt (keine Punkte)",
"panel.parent_complete_for_child_confirm": "Diese Aufgabe nur für {name} als erledigt markieren? Es werden keine Punkte vergeben.",
"panel.penalty_apply_text": "Wähle aus, welchem Kind diese Strafe zugewiesen wird ({points} Punkte werden abgezogen).",
"panel.penalty_empty_description": "Füge eine Strafe hinzu, um Punkte für Fehlverhalten abzuziehen.",
"panel.penalty_empty_title": "Noch keine Strafen",
Expand Down
3 changes: 3 additions & 0 deletions custom_components/taskmate/www/locales/en-GB.json
Original file line number Diff line number Diff line change
Expand Up @@ -1337,6 +1337,9 @@
"panel.parent_complete_failed": "Failed: {error}",
"panel.parent_complete_success": "Marked as parent completed",
"panel.parent_complete_tooltip": "Parent did it (no points)",
"panel.parent_complete_everyone": "Parent did it for everyone (no points)",
"panel.parent_complete_for_child": "Parent did it for {name} (no points)",
"panel.parent_complete_for_child_confirm": "Mark this chore as done for {name} only? No points will be awarded.",
"panel.penalty_apply_text": "Choose which child to apply this penalty to (will deduct {points} points).",
"panel.penalty_empty_description": "Add a penalty to deduct points for misbehaviour.",
"panel.penalty_empty_title": "No penalties yet",
Expand Down
3 changes: 3 additions & 0 deletions custom_components/taskmate/www/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1337,6 +1337,9 @@
"panel.parent_complete_failed": "Failed: {error}",
"panel.parent_complete_success": "Marked as parent completed",
"panel.parent_complete_tooltip": "Parent did it (no points)",
"panel.parent_complete_everyone": "Parent did it for everyone (no points)",
"panel.parent_complete_for_child": "Parent did it for {name} (no points)",
"panel.parent_complete_for_child_confirm": "Mark this chore as done for {name} only? No points will be awarded.",
"panel.penalty_apply_text": "Choose which child to apply this penalty to (will deduct {points} points).",
"panel.penalty_empty_description": "Add a penalty to deduct points for misbehaviour.",
"panel.penalty_empty_title": "No penalties yet",
Expand Down
3 changes: 3 additions & 0 deletions custom_components/taskmate/www/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -1285,6 +1285,9 @@
"panel.parent_complete_failed": "Échec : {error}",
"panel.parent_complete_success": "Marqué comme complété par le parent",
"panel.parent_complete_tooltip": "Fait par le parent (pas de points)",
"panel.parent_complete_everyone": "Fait par le parent pour tous (pas de points)",
"panel.parent_complete_for_child": "Fait par le parent pour {name} (pas de points)",
"panel.parent_complete_for_child_confirm": "Marquer cette tâche comme faite pour {name} uniquement ? Aucun point ne sera attribué.",
"panel.penalty_apply_text": "Choisissez l'enfant à qui appliquer cette pénalité ({points} points seront déduits).",
"panel.penalty_empty_description": "Ajoutez une pénalité pour retirer des points en cas de mauvais comportement.",
"panel.penalty_empty_title": "Aucune pénalité",
Expand Down
3 changes: 3 additions & 0 deletions custom_components/taskmate/www/locales/nb.json
Original file line number Diff line number Diff line change
Expand Up @@ -1285,6 +1285,9 @@
"panel.parent_complete_failed": "Feilet: {error}",
"panel.parent_complete_success": "Markert som fullført av forelder",
"panel.parent_complete_tooltip": "Forelder gjorde det (ingen poeng)",
"panel.parent_complete_everyone": "Forelder gjorde det for alle (ingen poeng)",
"panel.parent_complete_for_child": "Forelder gjorde det for {name} (ingen poeng)",
"panel.parent_complete_for_child_confirm": "Merke denne oppgaven som gjort bare for {name}? Ingen poeng blir gitt.",
"panel.penalty_apply_text": "Velg hvilket barn som skal få denne straffen (trekker {points} poeng).",
"panel.penalty_empty_description": "Legg til en straff for å trekke poeng ved dårlig oppførsel.",
"panel.penalty_empty_title": "Ingen straffer ennå",
Expand Down
3 changes: 3 additions & 0 deletions custom_components/taskmate/www/locales/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -1337,6 +1337,9 @@
"panel.parent_complete_failed": "Mislukt: {error}",
"panel.parent_complete_success": "Als door ouder gedaan gemarkeerd",
"panel.parent_complete_tooltip": "Ouder deed het (geen punten)",
"panel.parent_complete_everyone": "Ouder deed het voor iedereen (geen punten)",
"panel.parent_complete_for_child": "Ouder deed het voor {name} (geen punten)",
"panel.parent_complete_for_child_confirm": "Deze taak alleen voor {name} als gedaan markeren? Er worden geen punten gegeven.",
"panel.penalty_apply_text": "Kies welk kind deze puntenaftrek krijgt (er gaan {points} punten af).",
"panel.penalty_empty_description": "Voeg puntenaftrek toe om punten af te trekken bij wangedrag.",
"panel.penalty_empty_title": "Nog geen puntenaftrek",
Expand Down
3 changes: 3 additions & 0 deletions custom_components/taskmate/www/locales/nn.json
Original file line number Diff line number Diff line change
Expand Up @@ -1285,6 +1285,9 @@
"panel.parent_complete_failed": "Feila: {error}",
"panel.parent_complete_success": "Merkt som fullført av forelder",
"panel.parent_complete_tooltip": "Forelder gjorde det (ingen poeng)",
"panel.parent_complete_everyone": "Forelder gjorde det for alle (ingen poeng)",
"panel.parent_complete_for_child": "Forelder gjorde det for {name} (ingen poeng)",
"panel.parent_complete_for_child_confirm": "Merke denne oppgåva som gjord berre for {name}? Ingen poeng blir gjevne.",
"panel.penalty_apply_text": "Vel kva born som skal få denne straffa (trekk {points} poeng).",
"panel.penalty_empty_description": "Legg til ei straff for å trekke poeng for dårleg oppførsel.",
"panel.penalty_empty_title": "Ingen straffar enno",
Expand Down
3 changes: 3 additions & 0 deletions custom_components/taskmate/www/locales/pl.json
Original file line number Diff line number Diff line change
Expand Up @@ -1337,6 +1337,9 @@
"panel.parent_complete_failed": "Niepowodzenie: {error}",
"panel.parent_complete_success": "Oznaczono jako wykonane przez rodzica",
"panel.parent_complete_tooltip": "Rodzic to zrobił (bez punktów)",
"panel.parent_complete_everyone": "Rodzic zrobił to za wszystkich (bez punktów)",
"panel.parent_complete_for_child": "Rodzic zrobił to za {name} (bez punktów)",
"panel.parent_complete_for_child_confirm": "Oznaczyć to zadanie jako wykonane tylko dla {name}? Punkty nie zostaną przyznane.",
"panel.penalty_apply_text": "Wybierz dziecko, na które nałożyć tę karę (odejmie {points} pkt).",
"panel.penalty_empty_description": "Dodaj karę, aby odejmować punkty za złe zachowanie.",
"panel.penalty_empty_title": "Brak kar",
Expand Down
3 changes: 3 additions & 0 deletions custom_components/taskmate/www/locales/pt-BR.json
Original file line number Diff line number Diff line change
Expand Up @@ -1285,6 +1285,9 @@
"panel.parent_complete_failed": "Falhou: {error}",
"panel.parent_complete_success": "Marcado como concluído pelo responsável",
"panel.parent_complete_tooltip": "Responsável fez (sem pontos)",
"panel.parent_complete_everyone": "Responsável fez por todos (sem pontos)",
"panel.parent_complete_for_child": "Responsável fez por {name} (sem pontos)",
"panel.parent_complete_for_child_confirm": "Marcar esta tarefa como feita só para {name}? Nenhum ponto será concedido.",
"panel.penalty_apply_text": "Escolha qual criança receberá esta penalidade ({points} pontos serão deduzidos).",
"panel.penalty_empty_description": "Adicione uma penalidade para deduzir pontos por mau comportamento.",
"panel.penalty_empty_title": "Nenhuma penalidade ainda",
Expand Down
3 changes: 3 additions & 0 deletions custom_components/taskmate/www/locales/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -1290,6 +1290,9 @@
"panel.parent_complete_failed": "Falhou: {error}",
"panel.parent_complete_success": "Marcado como concluído pelo responsável",
"panel.parent_complete_tooltip": "Responsável fez (sem pontos)",
"panel.parent_complete_everyone": "Responsável fez por todos (sem pontos)",
"panel.parent_complete_for_child": "Responsável fez por {name} (sem pontos)",
"panel.parent_complete_for_child_confirm": "Marcar esta tarefa como feita só para {name}? Não serão atribuídos pontos.",
"panel.penalty_apply_text": "Escolha a criança a penalizar (será deduzido {points} pontos).",
"panel.penalty_empty_description": "Adicione uma penalização para deduzir pontos por mau comportamento.",
"panel.penalty_empty_title": "Ainda sem penalizações",
Expand Down
Loading
Loading