import abc
import six
from shuup.core.models import Basket
from shuup.core.utils.users import real_user_or_none
from shuup.utils.importing import cached_load
[docs]
class BasketCompatibilityError(Exception):
pass
[docs]
class BasketStorage(six.with_metaclass(abc.ABCMeta)):
[docs]
def load(self, basket):
"""
Load the given basket's data dictionary from the storage.
:type basket: shuup.core.basket.objects.BaseBasket
:rtype: dict
:raises:
`BasketCompatibilityError` if basket loaded from the storage
is not compatible with the requested basket.
"""
stored_basket = self._load_stored_basket(basket)
if not stored_basket:
return {}
if stored_basket.shop_id != basket.shop.id:
msg = (
"Error! Cannot load basket of a different Shop ("
f"{type(stored_basket).__name__} id={stored_basket.id!r} with Shop={stored_basket.shop_id}, Dest. Basket Shop={basket.shop.id})"
)
raise BasketCompatibilityError(msg)
price_units_diff = _price_units_diff(stored_basket, basket.shop)
if price_units_diff:
msg = f"Error! {type(stored_basket).__name__} {stored_basket.id!r}: Price unit mismatch with Shop ({price_units_diff})"
raise BasketCompatibilityError(msg)
return stored_basket.data or {}
@abc.abstractmethod
def _load_stored_basket(self, basket):
"""
Load stored basket for the given basket from the storage.
The returned object should have ``id``, ``shop_id``,
``currency``, ``prices_include_tax`` and ``data`` attributes.
:type basket: shuup.core.basket.objects.BaseBasket
:return: Stored basket or None
"""
pass
[docs]
@abc.abstractmethod
def save(self, basket, data): # pragma: no cover
"""
Save the given data dictionary into the storage for the given basket.
:type basket: shuup.core.basket.objects.BaseBasket
:type data: dict
:rtype str:
:return: The unique identifier of the basket just created
"""
pass
[docs]
@abc.abstractmethod
def delete(self, basket): # pragma: no cover
"""
Delete the basket from storage.
:type basket: shuup.core.basket.objects.BaseBasket
"""
pass
[docs]
def finalize(self, basket):
"""
Mark the basket as "finalized" (i.e. completed) in the storage.
The actual semantics of what finalization does are up to each backend.
:type basket: shuup.core.basket.objects.BaseBasket
"""
self.delete(basket=basket)
[docs]
def basket_exists(self, key, shop):
"""
Check if basket exists in the storage.
For example this is used from API to check whether the basket
actually exists for certain shop when accessed with key.
:type key: str
:type shop: shuup.core.models.Shop
"""
return False
[docs]
class BaseDatabaseBasketStorage(BasketStorage):
model = Basket
[docs]
def get_basket_kwargs(self, basket):
return {}
[docs]
def save(self, basket, data):
"""
:type basket: shuup.core.basket.objects.BaseBasket
"""
stored_basket = self._load_stored_basket(basket)
stored_basket.data = data
stored_basket.taxless_total_price = basket.taxless_total_price_or_none
stored_basket.taxful_total_price = basket.taxful_total_price_or_none
stored_basket.product_count = basket.smart_product_count
stored_basket.customer = basket.customer or None
stored_basket.orderer = basket.orderer or None
stored_basket.creator = real_user_or_none(basket.creator)
if hasattr(self.model, "supplier") and hasattr(basket, "supplier"):
stored_basket.supplier = basket.supplier
stored_basket.class_spec = f"{basket.__class__.__module__}.{basket.__class__.__name__}"
stored_basket.save()
stored_basket.products.set(set(basket.product_ids))
return stored_basket
[docs]
def delete(self, basket):
stored_basket = self._load_stored_basket(basket)
if stored_basket and stored_basket.pk:
stored_basket.deleted = True
stored_basket.save()
[docs]
def finalize(self, basket):
stored_basket = self._load_stored_basket(basket)
if stored_basket and stored_basket.pk:
stored_basket.deleted = True
stored_basket.finished = True
stored_basket.save()
def _load_stored_basket(self, basket):
basket_get_kwargs = self.get_basket_kwargs(basket)
stored_basket = None
if basket_get_kwargs:
stored_basket = self.model.objects.filter(deleted=False, **basket_get_kwargs).first()
if not stored_basket:
stored_basket = self.model(
shop=basket.shop,
currency=basket.currency,
prices_include_tax=basket.prices_include_tax,
)
return stored_basket
[docs]
def basket_exists(self, key, shop):
return self.model.objects.filter(key=key, shop=shop).exists()
[docs]
class DatabaseBasketStorage(BaseDatabaseBasketStorage):
[docs]
def get_basket_kwargs(self, basket):
if basket.key:
return {"key": basket.key}
return {}
def _price_units_diff(x, y):
diff = []
if x.currency != y.currency:
diff.append(f"currency: {x.currency!r} vs {y.currency!r}")
if x.prices_include_tax != y.prices_include_tax:
diff.append(f"includes_tax: {x.prices_include_tax!r} vs {y.prices_include_tax!r}")
return ", ".join(diff)
[docs]
def get_storage():
"""
Retrieve a basket storage object.
:return: A basket storage object.
:rtype: BasketStorage
"""
storage_class = cached_load("SHUUP_BASKET_STORAGE_CLASS_SPEC")
return storage_class()