217 files
This commit is contained in:
@@ -52,6 +52,7 @@ from ..helpers.task_fields import (
|
||||
EARLIEST_COMPLETION_RANGE,
|
||||
INTERVAL_ANCHORS,
|
||||
INTERVAL_DAYS_RANGE,
|
||||
REQUIRABLE_COMPLETION_FIELDS,
|
||||
ROTATION_STRATEGY_VALUES,
|
||||
TASK_PRIORITIES,
|
||||
WARNING_DAYS_RANGE,
|
||||
@@ -94,6 +95,7 @@ TASK_UPDATE_FIELD_MAP = {
|
||||
"documentation_url": "documentation_url",
|
||||
"responsible_user_id": "responsible_user_id",
|
||||
"assignee_pool": "assignee_pool",
|
||||
"required_completion_fields": "required_completion_fields",
|
||||
"rotation_strategy": "rotation_strategy",
|
||||
"entity_slug": "entity_slug",
|
||||
"custom_icon": "custom_icon",
|
||||
@@ -140,6 +142,7 @@ TASK_UPDATE_FIELD_MAP = {
|
||||
vol.Optional("assignee_pool"): vol.Any(
|
||||
vol.All([vol.All(str, vol.Length(max=MAX_META_LENGTH))], vol.Length(max=MAX_ASSIGNEE_POOL)), None
|
||||
),
|
||||
vol.Optional("required_completion_fields"): vol.Any([vol.In(REQUIRABLE_COMPLETION_FIELDS)], None),
|
||||
vol.Optional("rotation_strategy"): vol.Any(vol.In(ROTATION_STRATEGY_VALUES), None),
|
||||
vol.Optional("entity_slug"): vol.Any(vol.All(str, vol.Length(max=MAX_ENTITY_SLUG_LENGTH)), None),
|
||||
vol.Optional("custom_icon"): vol.Any(vol.All(str, vol.Length(max=MAX_ICON_LENGTH)), None),
|
||||
@@ -283,6 +286,12 @@ async def ws_create_task(
|
||||
task_data["assignee_pool"] = sanitize_assignee_pool(msg["assignee_pool"])
|
||||
if msg.get("rotation_strategy"):
|
||||
task_data["rotation_strategy"] = msg["rotation_strategy"]
|
||||
if msg.get("required_completion_fields"):
|
||||
from ..helpers.completion_requirements import sanitize_required_completion_fields
|
||||
|
||||
task_data["required_completion_fields"] = sanitize_required_completion_fields(
|
||||
msg["required_completion_fields"]
|
||||
)
|
||||
from ..helpers.sanitize import seed_rotation_assignee
|
||||
|
||||
seed_rotation_assignee(task_data)
|
||||
@@ -313,8 +322,13 @@ async def ws_create_task(
|
||||
if msg.get("consumes_parts") is not None:
|
||||
from ..const import CONF_PARTS
|
||||
from ..helpers.parts import sanitize_consumes_parts
|
||||
from . import foreign_part_resolver
|
||||
|
||||
links = sanitize_consumes_parts(msg["consumes_parts"], set(entry.data.get(CONF_PARTS) or {}))
|
||||
links = sanitize_consumes_parts(
|
||||
msg["consumes_parts"],
|
||||
set(entry.data.get(CONF_PARTS) or {}),
|
||||
foreign_part_ids=foreign_part_resolver(hass),
|
||||
)
|
||||
if links:
|
||||
task_data["consumes_parts"] = links
|
||||
if msg.get("checklist"):
|
||||
@@ -392,6 +406,7 @@ async def ws_create_task(
|
||||
vol.Optional("assignee_pool"): vol.Any(
|
||||
vol.All([vol.All(str, vol.Length(max=MAX_META_LENGTH))], vol.Length(max=MAX_ASSIGNEE_POOL)), None
|
||||
),
|
||||
vol.Optional("required_completion_fields"): vol.Any([vol.In(REQUIRABLE_COMPLETION_FIELDS)], None),
|
||||
vol.Optional("rotation_strategy"): vol.Any(vol.In(ROTATION_STRATEGY_VALUES), None),
|
||||
vol.Optional("entity_slug"): vol.Any(vol.All(str, vol.Length(max=MAX_ENTITY_SLUG_LENGTH)), None),
|
||||
vol.Optional("custom_icon"): vol.Any(vol.All(str, vol.Length(max=MAX_ICON_LENGTH)), None),
|
||||
@@ -500,6 +515,22 @@ async def ws_update_task(
|
||||
if msg_key in msg:
|
||||
task[data_key] = msg[msg_key]
|
||||
|
||||
# The loop above copies values verbatim, which is wrong for part links:
|
||||
# `task/create` validates them and `task/update` did not, so an edit could
|
||||
# persist a link to a part — or, since #111, to an OBJECT — that does not
|
||||
# exist. Nothing complained: the consume path simply skipped it. Sanitize
|
||||
# here so both write paths agree.
|
||||
if "consumes_parts" in msg:
|
||||
from ..const import CONF_PARTS
|
||||
from ..helpers.parts import sanitize_consumes_parts
|
||||
from . import foreign_part_resolver
|
||||
|
||||
task["consumes_parts"] = sanitize_consumes_parts(
|
||||
msg["consumes_parts"],
|
||||
set(entry.data.get(CONF_PARTS) or {}),
|
||||
foreign_part_ids=foreign_part_resolver(hass),
|
||||
)
|
||||
|
||||
# Recurrence resolution: an explicit nested `schedule` wins (calendar kinds
|
||||
# and kind-switches). Otherwise rebuild from the flat view ONLY when a real
|
||||
# legacy recurrence signal is present — a flat interval/due field, or a legacy
|
||||
@@ -532,9 +563,18 @@ async def ws_update_task(
|
||||
# schedule; drop the stray flat schedule_type so normalize stays clean.
|
||||
task.pop("schedule_type", None)
|
||||
|
||||
# Validate/cap newly-applied v1.3.0 fields. cap_task_fields runs the
|
||||
# full task sanitize (caches, lengths, action shape) so update-path
|
||||
# behaves identically to create-path.
|
||||
# Validate/cap the newly-applied fields. NOTE: this deliberately does NOT
|
||||
# call cap_task_fields() — the `task/update` schema above already
|
||||
# length/range-caps every scalar field the sanitiser covers (name,
|
||||
# task_type, schedule_type, interval_days, warning_days, notes,
|
||||
# documentation_url, priority, checklist, labels, …), which is the
|
||||
# WS-boundary contract documented in helpers/sanitize. What the schema
|
||||
# *can't* express is the shape of the loose dicts and the dedup/trim/seed
|
||||
# rules on the list fields — exactly the sub-helpers called below. Adding
|
||||
# cap_task_fields here would be a no-op on everything else and would swap
|
||||
# the WS layer's reject-at-boundary semantics for silent clamping.
|
||||
# (The service path in tasks_persist.py DOES call cap_task_fields: those
|
||||
# functions are reachable directly from Python, not only behind a schema.)
|
||||
from ..helpers.sanitize import (
|
||||
cap_action_field,
|
||||
cap_quick_complete_defaults_field,
|
||||
@@ -549,6 +589,10 @@ async def ws_update_task(
|
||||
task["labels"] = sanitize_labels(task["labels"])
|
||||
if "assignee_pool" in task:
|
||||
task["assignee_pool"] = sanitize_assignee_pool(task["assignee_pool"])
|
||||
if "required_completion_fields" in task:
|
||||
from ..helpers.completion_requirements import sanitize_required_completion_fields
|
||||
|
||||
task["required_completion_fields"] = sanitize_required_completion_fields(task["required_completion_fields"])
|
||||
seed_rotation_assignee(task)
|
||||
|
||||
# Clear stale trigger runtime in Store only when trigger fundamentally changes
|
||||
|
||||
Reference in New Issue
Block a user