189 files
This commit is contained in:
1 parent
79fefc0a7c
commit
303886f212
189 files changed
+21402
-3514
No files matched your search
@@ -17,6 +17,11 @@ from ..const import (
|
||||
MAX_META_LENGTH,
|
||||
MAX_TEXT_LENGTH,
|
||||
)
|
||||
from ..helpers.completion_photos import (
|
||||
MAX_COMPLETION_PHOTOS,
|
||||
history_photo_ids,
|
||||
normalize_photo_doc_ids,
|
||||
)
|
||||
from ..helpers.permissions import require_write
|
||||
from . import (
|
||||
_get_runtime_data,
|
||||
@@ -30,8 +35,9 @@ from . import (
|
||||
# browser between read and write — timestamp is more stable. If multiple
|
||||
# entries share a timestamp (rare), the first match is patched.
|
||||
#
|
||||
# Patchable fields: timestamp, notes, cost, duration, completed_by and — since
|
||||
# #130 — used_parts (stock reconciled by the per-part delta). Anything else
|
||||
# Patchable fields: timestamp, notes, cost, duration, completed_by, since
|
||||
# #130 used_parts (stock reconciled by the per-part delta) and since #161
|
||||
# photo_doc_ids (add/remove completion photos after the fact). Anything else
|
||||
# (type, trigger_value, checklist_state, feedback) is intentionally read-only —
|
||||
# those carry semantic meaning that shouldn't be silently rewritten.
|
||||
#
|
||||
@@ -53,6 +59,15 @@ from . import (
|
||||
vol.Optional("cost"): vol.Any(vol.All(vol.Coerce(float), vol.Range(min=0, max=MAX_COST)), None),
|
||||
vol.Optional("duration"): vol.Any(vol.All(vol.Coerce(int), vol.Range(min=0, max=MAX_DURATION_MINUTES)), None),
|
||||
vol.Optional("completed_by"): vol.Any(vol.All(str, vol.Length(max=MAX_META_LENGTH)), None),
|
||||
# Recorded readings (#161 phase 2): a typo among nine meter values is
|
||||
# likely, and nothing downstream depends on the entry being frozen.
|
||||
# The scalar patches like the other fields; the slot map REPLACES the
|
||||
# snapshot (None value = that meter unread), ids validated in the handler.
|
||||
vol.Optional("reading_value"): vol.Any(vol.All(vol.Coerce(float), vol.Range(min=-1e12, max=1e12)), None),
|
||||
vol.Optional("reading_values"): vol.Any(
|
||||
{str: vol.Any(vol.All(vol.Coerce(float), vol.Range(min=-1e12, max=1e12)), None)},
|
||||
None,
|
||||
),
|
||||
# #130: edit the entry's part consumption. The stock is reconciled by
|
||||
# the per-part DELTA against the entry's previous used_parts; None (or
|
||||
# []) clears the consumption and returns the old quantities to stock.
|
||||
@@ -71,6 +86,14 @@ from . import (
|
||||
),
|
||||
None,
|
||||
),
|
||||
# #161: the entry's completion photos. Replaces the whole list; None
|
||||
# (or []) detaches every photo from the entry. The documents
|
||||
# themselves are never deleted here — they stay in the object's
|
||||
# documents, the entry merely stops pointing at them.
|
||||
vol.Optional("photo_doc_ids"): vol.Any(
|
||||
vol.All([vol.All(str, vol.Length(max=MAX_ID_LENGTH))], vol.Length(max=MAX_COMPLETION_PHOTOS)),
|
||||
None,
|
||||
),
|
||||
}
|
||||
)
|
||||
@require_write
|
||||
@@ -137,9 +160,20 @@ async def ws_update_history_entry(
|
||||
|
||||
patched = dict(history[target_index])
|
||||
|
||||
# #161 phase 2: "never both" — the scalar cannot be set on an entry that
|
||||
# carries a slot snapshot, nor on a task that records slots.
|
||||
if msg.get("reading_value") is not None:
|
||||
from . import _get_merged_tasks
|
||||
|
||||
if patched.get("reading_values") or (_get_merged_tasks(entry).get(task_id) or {}).get("readings"):
|
||||
connection.send_error(
|
||||
msg["id"], "invalid_input", "This entry records named readings — patch reading_values instead"
|
||||
)
|
||||
return
|
||||
|
||||
# Apply patch — explicit None means "clear field" (drop the key entirely
|
||||
# so the dict stays minimal); explicit value sets it.
|
||||
PATCHABLE = ("timestamp", "notes", "cost", "duration", "completed_by")
|
||||
PATCHABLE = ("timestamp", "notes", "cost", "duration", "completed_by", "reading_value")
|
||||
for field in PATCHABLE:
|
||||
if field not in msg:
|
||||
continue
|
||||
@@ -149,6 +183,31 @@ async def ws_update_history_entry(
|
||||
else:
|
||||
patched[field] = value
|
||||
|
||||
# #161 phase 2: the slot snapshot. Ids resolve against the task's current
|
||||
# slots OR the entry's own snapshot (a slot deleted since the completion
|
||||
# stays editable); anything else is refused rather than guessed.
|
||||
if "reading_values" in msg:
|
||||
from ..helpers.reading_slots import history_reading_values, resolve_reading_values
|
||||
from . import _get_merged_tasks
|
||||
|
||||
slot_task = _get_merged_tasks(entry).get(task_id) or {}
|
||||
try:
|
||||
new_values = resolve_reading_values(
|
||||
slot_task.get("readings") or [],
|
||||
msg["reading_values"],
|
||||
keep=history_reading_values(patched),
|
||||
default_unit=slot_task.get("reading_unit"),
|
||||
)
|
||||
except ValueError as err:
|
||||
connection.send_error(msg["id"], "invalid_input", str(err))
|
||||
return
|
||||
if new_values:
|
||||
patched["reading_values"] = new_values
|
||||
# The snapshot supersedes a scalar from the pre-slot era.
|
||||
patched.pop("reading_value", None)
|
||||
else:
|
||||
patched.pop("reading_values", None)
|
||||
|
||||
# #130: part consumption on the entry. The stock is adjusted by the
|
||||
# per-part delta between the stored and the submitted selection, so
|
||||
# corrections and backfills keep the shelf honest. Best-effort like the
|
||||
@@ -171,6 +230,23 @@ async def ws_update_history_entry(
|
||||
else:
|
||||
patched.pop("used_parts", None)
|
||||
|
||||
# #161: completion photos on the entry. A new id is linked to the task
|
||||
# like a live completion does (best-effort); a removed id keeps its
|
||||
# document and its links — only the entry forgets it. The legacy
|
||||
# scalar is folded into the list the moment the entry is edited.
|
||||
if "photo_doc_ids" in msg:
|
||||
old_photos = history_photo_ids(patched)
|
||||
new_photos = normalize_photo_doc_ids(msg["photo_doc_ids"])
|
||||
patched.pop("photo_doc_id", None)
|
||||
if new_photos:
|
||||
patched["photo_doc_ids"] = new_photos
|
||||
else:
|
||||
patched.pop("photo_doc_ids", None)
|
||||
if rd and rd.coordinator:
|
||||
for doc_id in new_photos:
|
||||
if doc_id not in old_photos:
|
||||
await rd.coordinator._link_completion_photo(doc_id, task_id)
|
||||
|
||||
history[target_index] = patched
|
||||
store.set_history(task_id, history)
|
||||
|
||||
|
||||
Reference in new issue
Block a user