# -*- coding: utf-8 -*-
# This file is part of Shuup.
#
# Copyright (c) 2012-2021, Shuup Commerce Inc. All rights reserved.
#
# This source code is licensed under the OSL-3.0 license found in the
# LICENSE file in the root directory of this source tree.
import hashlib
import six
from django.core.handlers.wsgi import WSGIRequest
from django.db.models import Q, QuerySet
from parler.managers import TranslatableQuerySet
from shuup.core import cache
try:
from urllib.parse import parse_qs, urlparse
except ImportError: # Py2 fallback
from urlparse import parse_qs, urlparse
HASHABLE_KEYS = ["customer_groups", "customer", "shop"]
GENERIC_CACHE_NAMESPACE_PREFIX = "generic_context_cache"
[docs]def get_cached_value(identifier, item, context, **kwargs):
"""
Get item from context cache by identifier
Accepts optional kwargs parameter `allow_cache` which will skip
fetching the actual cached object. When `allow_cache` is set to
False only cache key for identifier, item, context combination is
returned.
:param identifier: Any
:type identifier: string
:param item: Any
:param context: Any
:type context: dict
:return: Cache key and cached value if allowed
:rtype: tuple(str, object)
"""
allow_cache = True
if "allow_cache" in kwargs:
allow_cache = kwargs.pop("allow_cache")
key = get_cache_key_for_context(identifier, item, context, **kwargs)
if allow_cache is False:
return key, None
return key, cache.get(key)
[docs]def set_cached_value(key, value, timeout=None):
"""
Set value to context cache
:param key: Unique key formed to the context
:param value: Value to cache
:param timeout: Timeout as seconds
:type timeout: int
"""
cache.set(key, value, timeout=timeout)
[docs]def bump_cache_for_shop_product(instance, shop=None):
"""
Bump cache for given shop product
Clear cache for shop product, product linked to it and
all the children.
:param shop_product: shop product object or shop product object id
:type shop_product: shuup.core.models.ShopProduct
"""
from shuup.core.models import Product, ProductPackageLink, ShopProduct
if isinstance(instance, ShopProduct):
shop_product_ids = [instance.pk]
elif isinstance(instance, QuerySet):
shop_product_ids = instance
else:
shop_product_ids = [instance]
# Get all normal products linked to passed
# shop product id
product_ids = Product.objects.filter(shop_products__id__in=shop_product_ids).values_list("id", flat=True)
# Get all affect variation parent ids just in
# case passed shop product ids includes child
# products we need to bump simplings
variation_parent_ids = Product.objects.filter(id__in=product_ids).values_list("variation_parent_id", flat=True)
# Get all packages or products in any package
package_product_ids = ProductPackageLink.objects.filter(
Q(parent_id__in=product_ids) | Q(child_id__in=product_ids)
).values_list("child_id", "parent_id")
# All above querysets should in theory be lazy and executed once
# here
product_ids_to_bump = Product.objects.filter(
Q(id__in=product_ids)
| Q(variation_parent_id__in=product_ids)
| Q(variation_parent_id__in=variation_parent_ids)
| Q(id__in=set(value for pair_of_values in package_product_ids for value in pair_of_values))
).values_list("id", flat=True)
# One extra query should be better what we have now
shop_product_ids_to_bump = ShopProduct.objects.filter(product_id__in=product_ids_to_bump).values_list(
"id", flat=True
)
bump_cache_for_item_ids(shop_product_ids_to_bump, "shuup-shopproduct", ShopProduct, shop)
bump_cache_for_item_ids(product_ids_to_bump, "shuup-product", Product, shop)
[docs]def bump_cache_for_product(product, shop=None):
"""
Bump cache for product
In case shop is not given all the shop products
for the product is bumped.
:param product: product object or product object id or a list of product object id's
:type product: shuup.core.models.Product
:param shop: shop object
:type shop: shuup.core.models.Shop|None
"""
from shuup.core.models import ShopProduct
if not isinstance(product, list):
product_id = product.id if hasattr(product, "id") else product
products = [product_id]
else:
products = product
shop_product_ids = ShopProduct.objects.filter(product_id__in=products).values_list("pk", flat=True)
for shop_product_id in shop_product_ids:
bump_cache_for_shop_product(shop_product_id, shop)
[docs]def bump_cache_for_item_ids(item_ids, namespace, object_class, shop=None):
"""
Bump cache for given item ids
Use this only for non product items. For products
and shop_products use `bump_cache_for_product` and
`bump_cache_for_shop_product` for those.
`shop` parameter is deprecated and not used
:param ids: list of cached object id's
"""
for item_id in item_ids:
cache.bump_version("{}-{}".format(namespace, item_id))
[docs]def bump_cache_for_item(item):
"""
Bump cache for given item
Use this only for non product items. For products
and shop_products use `bump_cache_for_product` and
`bump_cache_for_shop_product` for those.
:param item: Cached object
"""
cache.bump_version(_get_namespace_for_item(item))
[docs]def bump_cache_for_pk(cls, pk):
"""
Bump cache for given class and pk combination
Use this only for non product items. For products
and shop_products use `bump_cache_for_product` and
`bump_cache_for_shop_product` for those.
In case you need to use this to product or shop_product
make sure you also bump related objects like in
`bump_cache_for_shop_product`.
:param cls: Class for cached object
:param pk: pk for cached object
"""
cache.bump_version("%s-%s" % (_get_namespace_prefix(cls), pk))
[docs]def bump_product_signal_handler(sender, instance, **kwargs):
"""
Signal handler for clearing product cache
:param instance: Shuup product
:type instance: shuup.core.models.Product
"""
bump_cache_for_product(instance)
[docs]def bump_shop_product_signal_handler(sender, instance, **kwargs):
"""
Signal handler for clearing shop product cache
:param instance: Shuup shop product
:type instance: shuup.core.models.ShopProduct
"""
bump_cache_for_shop_product(instance)
[docs]def get_cache_key_for_context(identifier, item, context, **kwargs):
namespace = _get_namespace_for_item(item)
items = _get_items_from_context(context)
for k, v in six.iteritems(kwargs):
items[k] = _get_val(v)
if isinstance(context, WSGIRequest):
query_string = urlparse(context.get_full_path()).query
for k, v in six.iteritems(parse_qs(query_string)):
items[k] = _get_val(v)
sorted_items = dict(sorted(items.items(), key=lambda item: item[0]))
key_hash = hashlib.sha1(str(sorted_items).encode("utf-8")).hexdigest()
return "%s:%s_%s" % (namespace, identifier, key_hash)
[docs]def bump_internal_cache():
cache.bump_version("_ctx_cache")
def _get_cached_value_from_context(context, key, value):
cached_value = None
# 1) check whether the value is cached inside the context as an attribute
try:
cache_key = "_ctx_cache_{}".format(key)
cached_value = getattr(context, cache_key)
except AttributeError:
pass
# 2) Check whether the value is cached in general cache
# we can only cache objects that has `pk` attribute
if cached_value is None and hasattr(value, "pk"):
cache_key = "_ctx_cache:{}_{}".format(key, value.pk)
cached_value = cache.get(cache_key)
# 3) Nothing is cached, then read the value itself
if cached_value is None:
if key == "customer" and value:
cached_value = _get_val(value.groups.all())
else:
cached_value = _get_val(value)
# Set the value as attribute of the context
# somethings this will raise AttributeError because the
# context is not a valid object, like a dictionary
try:
cache_key = "_ctx_cache_{}".format(key)
setattr(context, cache_key, cached_value)
except AttributeError:
pass
# cache the value in the general cache
if hasattr(value, "pk"):
cache_key = "_ctx_cache:{}_{}".format(key, value.pk)
cache.set(cache_key, cached_value)
return cached_value
def _get_items_from_context(context): # noqa (C901)
items = {}
def handle_item(context, key, value):
value = _get_cached_value_from_context(context, key, value)
if key == "customer":
key = "customer_groups"
items[key] = value
if hasattr(context, "items"):
for key, value in list(six.iteritems(context)):
if key in HASHABLE_KEYS:
handle_item(context, key, value)
else:
for key in HASHABLE_KEYS:
if hasattr(context, key):
value = getattr(context, key, None)
handle_item(context, key, value)
return items
def _get_val(v):
if isinstance(v, dict):
sorted_items = dict(sorted(v.items(), key=lambda item: item[0]))
return hashlib.sha1(str(frozenset(sorted_items.items())).encode("utf-8")).hexdigest()
if hasattr(v, "pk"):
return v.pk
if isinstance(v, QuerySet) or isinstance(v, TranslatableQuerySet):
return "|".join(list(map(str, v.all().values_list("pk", flat=True))))
if isinstance(v, list):
return "|".join(list(map(str, v)))
return v
def _get_namespace_for_item(item):
return "%s-%s" % (_get_namespace_prefix(item), _get_item_id(item))
def _get_namespace_prefix(item):
if hasattr(item, "_meta"):
model_meta = item._meta
return "%s-%s" % (model_meta.app_label, model_meta.model_name)
return GENERIC_CACHE_NAMESPACE_PREFIX
def _get_item_id(item):
if isinstance(item, int):
return item
item_id = 0
if item:
if isinstance(item, six.string_types):
item_id = item
elif hasattr(item, "pk"):
item_id = item.pk or 0
else:
item_id = item.__class__.lower() if callable(item) else 0
return item_id