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
59 changes: 59 additions & 0 deletions src/viur/shop/skeletons/_bones.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import typing as t

from viur.core.bones import RelationalBone, RelationalUpdateLevel
from viur.core.skeleton import SkeletonInstance

from ..globals import SHOP_LOGGER

logger = SHOP_LOGGER.getChild(__name__)


class SnapshotRelationalBone(RelationalBone):
"""A :class:`RelationalBone` that keeps its cached ``refKeys`` copy in sync
while the owning skeleton is still editable, and freezes that copy once the
owning skeleton is frozen.

Background
----------
viur-core only offers a *static* :class:`RelationalUpdateLevel`, and neither
value fits an order/cart address:

- ``Always`` keeps the cached copy in sync with the referenced entity, but a
completed (frozen) order would then change retroactively whenever the
referenced address gets edited later.
- ``OnValueAssignment`` freezes the copy at assignment time, but then never
reflects edits made to the referenced entity during an ongoing checkout
(e.g. the customer adds a birthdate in a later step or corrects the
address) -- the stale copy is what gets persisted with the order.

This bone combines both: it is configured with ``updateLevel=Always`` so the
copy is kept in sync -- both by the ``update_relations`` task (triggered when
the referenced entity changes) and by explicit :meth:`refresh` calls -- as
long as the owning skeleton is *not* frozen, and turns :meth:`refresh` into a
no-op once it *is* frozen, preserving the snapshot captured at freeze time.

:param is_frozen: Callable deciding whether the owning skeleton is frozen and
its snapshot must be preserved. Defaults to reading the boolean
``is_frozen`` bone.
"""

def __init__(
self,
*args: t.Any,
is_frozen: t.Callable[[SkeletonInstance], bool] = lambda skel: bool(skel["is_frozen"]),
updateLevel: RelationalUpdateLevel = RelationalUpdateLevel.Always,
**kwargs: t.Any,
) -> None:
# The live-sync behaviour relies on updateLevel=Always
if updateLevel != RelationalUpdateLevel.Always:
raise ValueError(f"{type(self).__name__} only accepts RelationalUpdateLevel.Always")
super().__init__(updateLevel=updateLevel, *args, **kwargs)
self.is_frozen = is_frozen

def refresh(self, skel: SkeletonInstance, name: str) -> None:
"""Refresh the cached copy -- unless the owning skeleton is frozen, in
which case the snapshot captured at freeze time is preserved."""
if self.is_frozen(skel):
logger.debug(f"Skipping refresh of frozen {name!r} on {skel['key']!r}")
return
super().refresh(skel, name)
10 changes: 7 additions & 3 deletions src/viur/shop/skeletons/cart.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from viur.core.skeleton import SkeletonInstance
from viur.shop.types import *

from ._bones import SnapshotRelationalBone
from .vat import VatIncludedSkel
from ..globals import SHOP_INSTANCE, SHOP_LOGGER
from ..skeletons.article import ArticleAbstractSkel
Expand Down Expand Up @@ -320,14 +321,14 @@ class CartNodeSkel(TreeSkel):
defaultValue=0,
)

shipping_address = RelationalBone(
shipping_address = SnapshotRelationalBone(
kind="{{viur_shop_modulename}}_address",
module="{{viur_shop_modulename}}/address",
# keep shipping address persistent:
updateLevel=RelationalUpdateLevel.OnValueAssignment,
refKeys={
"*",
},
# keep the copy in sync while the cart is open, freeze it once frozen
# (default is_frozen reads the CartNodeSkel.is_frozen bone):
)

customer_comment = TextBone(
Expand Down Expand Up @@ -412,6 +413,9 @@ def refresh_shipping_address(cls, skel: SkeletonInstance) -> SkeletonInstance:
Due to race-condition and timing issues, the dest values are not always
set correctly. This refresh fixes this.
"""
# Sub-node / total sub-skeletons don't carry the shipping_address bone.
if "shipping_address" not in skel:
return skel
try:
skel.shipping_address.refresh(skel, skel.shipping_address.name)
except Exception as exc:
Expand Down
7 changes: 4 additions & 3 deletions src/viur/shop/skeletons/order.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from viur.core.bones import *
from viur.core.skeleton import Skeleton, SkeletonInstance
from viur.shop.types import *
from ._bones import SnapshotRelationalBone
from ..globals import SHOP_INSTANCE, SHOP_LOGGER

logger = SHOP_LOGGER.getChild(__name__)
Expand All @@ -19,16 +20,16 @@ def get_payment_providers() -> dict[str, str | translate]:
class OrderSkel(Skeleton):
kindName = "{{viur_shop_modulename}}_order"

billing_address = RelationalBone(
billing_address = SnapshotRelationalBone(
kind="{{viur_shop_modulename}}_address",
module="{{viur_shop_modulename}}/address",
searchable=True,
# keep billing address persistent:
updateLevel=RelationalUpdateLevel.OnValueAssignment,
# keep all fields of the billing address as a copy:
refKeys={
"*",
},
# keep the copy in sync while the order is open, freeze it once ordered:
is_frozen=lambda skel: bool(skel["is_ordered"]),
)

customer = RelationalBone(
Expand Down
Loading