Skip to content

Commit

Permalink
Merge pull request #2385 from tulimaki/tune-duplicate-queries
Browse files Browse the repository at this point in the history
Tune duplicate queries
  • Loading branch information
chessbr committed Feb 12, 2021
2 parents d6e8454 + 679b614 commit 453bc30
Show file tree
Hide file tree
Showing 23 changed files with 244 additions and 46 deletions.
2 changes: 1 addition & 1 deletion shuup/admin/forms/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ class PersonContactChoiceWidget(ContactChoiceWidget):

@property
def filter(self):
return json.dumps({"groups": [PersonContact.get_default_group().pk]})
return json.dumps({"groups": [PersonContact().default_group.pk]})


class PackageProductChoiceWidget(ProductChoiceWidget):
Expand Down
35 changes: 26 additions & 9 deletions shuup/core/models/_contacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
from django.db import models
from django.db.models import QuerySet
from django.utils.encoding import python_2_unicode_compatible
from django.utils.functional import cached_property
from django.utils.lru_cache import lru_cache
from django.utils.translation import ugettext_lazy as _
from enumfields import Enum, EnumField
from filer.fields.image import FilerImageField
Expand Down Expand Up @@ -42,6 +44,17 @@
]


@lru_cache()
def get_price_display_options(group):
options = group.price_display_options.for_group_and_shop(group, shop=group.shop)
return (options.to_price_display() or PriceDisplayOptions())


@lru_cache()
def get_groups_ids(group):
return group.groups.values_list("pk", flat=True)


class ContactGroupPriceDisplayQueryset(QuerySet):
def for_group_and_shop(self, group, shop):
obj = self.filter(group=group, shop=shop).first()
Expand Down Expand Up @@ -132,11 +145,7 @@ def set_price_display_options(self, **kwargs):
return self

def get_price_display_options(self):
if self.pk:
options = self.price_display_options.for_group_and_shop(self, shop=self.shop)
if options:
return options.to_price_display()
return PriceDisplayOptions()
return (get_price_display_options(self) if self.pk else PriceDisplayOptions())

def can_delete(self):
return bool(
Expand Down Expand Up @@ -313,15 +322,15 @@ def get_price_display_options(self, **kwargs):
if not group:
groups_with_options = self.groups.with_price_display_options(shop)
if groups_with_options:
default_group = self.get_default_group()
default_group = self.default_group
if groups_with_options.filter(pk=default_group.pk).exists():
group = default_group
else:
# Contact was removed from the default group.
group = groups_with_options.first()

if not group:
group = self.get_default_group()
group = self.default_group

return get_price_display_options_for_group_and_shop(group, shop)

Expand All @@ -346,6 +355,10 @@ def get_default_group(cls):
)
return obj

@cached_property
def default_group(self):
return self.get_default_group()

def add_to_shops(self, registration_shop, shops):
"""
Add contact to multiple shops
Expand Down Expand Up @@ -377,6 +390,10 @@ def in_shop(self, shop, only_registration=False):
return True
return self.registered_in(shop)

@property
def groups_ids(self):
return get_groups_ids(self) if self.pk else [self.default_group.pk]


class CompanyContact(Contact):
default_tax_group_getter = CustomerTaxGroup.get_default_company_group
Expand Down Expand Up @@ -517,7 +534,7 @@ def delete(self, *args, **kwargs):
"AnonymousContacts don't exist in the database, silly."
)

@property
@cached_property
def groups(self):
"""
Contact groups accessor for anonymous contact.
Expand All @@ -535,7 +552,7 @@ def groups(self):
:rtype: django.db.QuerySet
"""
self.get_default_group() # Make sure group exists
self.default_group # Make sure group exists
return ContactGroup.objects.filter(identifier=self.default_contact_group_identifier)


Expand Down
6 changes: 5 additions & 1 deletion shuup/core/models/_product_variation.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,11 @@ def get_all_available_combinations(product):
values_by_variable = defaultdict(list)
values = (
ProductVariationVariableValue.objects.filter(variable__product=product)
.prefetch_related("variable").order_by("ordering")
.prefetch_related(
"translations",
"variable",
"variable__translations"
).order_by("ordering")
)
for val in values:
values_by_variable[val.variable].append(val)
Expand Down
2 changes: 2 additions & 0 deletions shuup/core/models/_products.py
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,8 @@ def get_shop_instance(self, shop, allow_cache=False):
return val

shop_inst = self.shop_products.get(shop_id=shop.id)
shop_inst.product = self
shop_inst.shop = shop
context_cache.set_cached_value(key, shop_inst)
return shop_inst

Expand Down
24 changes: 14 additions & 10 deletions shuup/core/models/_units.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from django.db.models.signals import post_save
from django.utils.encoding import python_2_unicode_compatible
from django.utils.functional import cached_property
from django.utils.lru_cache import lru_cache
from django.utils.translation import pgettext
from django.utils.translation import ugettext_lazy as _
from parler.models import (
Expand All @@ -30,6 +31,18 @@
from ._base import TranslatableShuupModel


@lru_cache()
def get_display_unit(sales_unit):
cache_key = "display_unit:sales_unit_{}_default_display_unit".format(sales_unit.pk)
default_display_unit = cache.get(cache_key)
if default_display_unit is None:
default_display_unit = sales_unit.display_units.filter(default=True).first()
# Set 0 to cache to prevent None values, which will not be a valid cache value
# 0 will be invalid below, hence we prevent another query here
cache.set(cache_key, default_display_unit or 0)
return default_display_unit


# TODO: (2.0) Remove deprecated SalesUnit.short_name
class _ShortNameToSymbol(object):
def __init__(self, *args, **kwargs):
Expand Down Expand Up @@ -107,16 +120,7 @@ def display_unit(self):
:rtype: DisplayUnit
"""
cache_key = "display_unit:sales_unit_{}_default_display_unit".format(self.pk)
default_display_unit = cache.get(cache_key)

if default_display_unit is None:
default_display_unit = self.display_units.filter(default=True).first()
# Set 0 to cache to prevent None values, which will not be a valid cache value
# 0 will be invalid below, hence we prevent another query here
cache.set(cache_key, default_display_unit or 0)

return default_display_unit or SalesUnitAsDisplayUnit(self)
return (get_display_unit(self) if self.pk else None) or SalesUnitAsDisplayUnit(self)


class SalesUnitTranslation(_ShortNameToSymbol, TranslatedFieldsModel):
Expand Down
30 changes: 28 additions & 2 deletions shuup/core/signal_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,14 @@
from django.dispatch import receiver

from shuup.core.models import (
Category, CompanyContact, ContactGroup, PersonContact, Product, Shop,
ShopProduct, Supplier, Tax, TaxClass
Category, CompanyContact, ContactGroup, ContactGroupPriceDisplay,
DisplayUnit, PersonContact, Product, Shop, ShopProduct, Supplier, Tax,
TaxClass
)
from shuup.core.models._contacts import (
get_groups_ids, get_price_display_options
)
from shuup.core.models._units import get_display_unit
from shuup.core.order_creator.signals import order_creator_finished
from shuup.core.signals import context_cache_item_bumped, order_changed
from shuup.core.utils import context_cache
Expand Down Expand Up @@ -66,6 +71,7 @@ def handle_supplier_post_save(sender, instance, **kwargs):

def handle_contact_post_save(sender, instance, **kwargs):
bump_internal_cache()
get_groups_ids.cache_clear()


@receiver(order_creator_finished)
Expand All @@ -81,6 +87,14 @@ def on_order_changed(sender, order, **kwargs):
line.supplier.module.update_stock(line.product_id)


def handle_contact_group_price_display_post_save(sender, instance, **kwargs):
get_price_display_options.cache_clear()


def handle_display_unit_post_save(sender, instance, **kwargs):
get_display_unit.cache_clear()


# connect signals to bump caches on Product and ShopProduct change
m2m_changed.connect(
handle_shop_product_post_save,
Expand Down Expand Up @@ -118,4 +132,16 @@ def on_order_changed(sender, order, **kwargs):
dispatch_uid="contact_group:change_members"
)

post_save.connect(
handle_contact_group_price_display_post_save,
sender=ContactGroupPriceDisplay,
dispatch_uid="shuup_contact_group_price_display_bump"
)

post_save.connect(
handle_display_unit_post_save,
sender=DisplayUnit,
dispatch_uid="shuup_display_unit_bump"
)

connection_created.connect(extend_sqlite_functions)
2 changes: 1 addition & 1 deletion shuup/discounts/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def get_potential_discounts_for_product(context, product, available_only=True):
product_id = product if isinstance(product, six.integer_types) else product.pk

category_ids = list(Category.objects.filter(shop_products__product_id=product_id).values_list("id", flat=True))
group_ids = list(context.customer.groups.values_list("id", flat=True))
group_ids = list(context.customer.groups_ids)

# Product condition is always applied
condition_query = (Q(product__isnull=True) | Q(product_id=product_id))
Expand Down
10 changes: 8 additions & 2 deletions shuup/front/utils/product.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,11 +162,17 @@ def get_orderable_variation_children(product, request, variation_variables, supp
orderable_variation_children = OrderedDict()
orderable = 0

shop = request.shop
product_queryset = product.variation_children.visible(
shop=request.shop, customer=request.customer
shop=shop, customer=request.customer
).values_list("pk", flat=True)
all_combinations = list(product.get_all_available_combinations())
for shop_product in ShopProduct.objects.filter(shop=request.shop, product__id__in=product_queryset):
for shop_product in (
ShopProduct.objects.filter(
shop=shop, product__id__in=product_queryset
).select_related("product").prefetch_related("suppliers")
):
shop_product.shop = shop # To avoid query on orderability checks
combo_data = first(
combo for combo in all_combinations if combo["result_product_pk"] == shop_product.product.id
)
Expand Down
4 changes: 3 additions & 1 deletion shuup/front/views/category.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ def get_context_data(context, request, category, product_filters):
shop=request.shop
).filter(
**product_filters
).filter(get_query_filters(request, category, data=data))
).filter(
get_query_filters(request, category, data=data)
).prefetch_related("sales_unit", "sales_unit__translations")

products = get_product_queryset(products, request, category, data).distinct()
products = post_filter_products(request, category, products, data)
Expand Down
1 change: 1 addition & 0 deletions shuup/gdpr/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,4 @@ class AppConfig(shuup.apps.AppConfig):
def ready(self):
# connect receivers
import shuup.gdpr.receivers # noqa: F401
import shuup.gdpr.signal_handlers # noqa: F401
14 changes: 10 additions & 4 deletions shuup/gdpr/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from django.conf import settings
from django.db import models
from django.utils.encoding import python_2_unicode_compatible
from django.utils.lru_cache import lru_cache
from django.utils.translation import activate, get_language
from django.utils.translation import ugettext_lazy as _
from parler.models import TranslatableModel, TranslatedFields
Expand All @@ -21,6 +22,14 @@
GDPR_ANONYMIZE_TASK_TYPE_IDENTIFIER = "gdpr_anonymize"


@lru_cache()
def get_setting(shop):
instance, created = GDPRSettings.objects.get_or_create(shop=shop)
if created or not instance.safe_translation_getter("cookie_banner_content"):
instance.set_default_content()
return instance


@python_2_unicode_compatible
class GDPRSettings(TranslatableModel):
shop = models.OneToOneField("shuup.Shop", related_name="gdpr_settings", on_delete=models.CASCADE)
Expand Down Expand Up @@ -87,10 +96,7 @@ def set_default_content(self):

@classmethod
def get_for_shop(cls, shop):
instance, created = cls.objects.get_or_create(shop=shop)
if created or not instance.safe_translation_getter("cookie_banner_content"):
instance.set_default_content()
return instance
return get_setting(shop)


@python_2_unicode_compatible
Expand Down
21 changes: 21 additions & 0 deletions shuup/gdpr/signal_handlers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# This file is part of Shuup.
#
# Copyright (c) 2012-2021, Shoop Commerce Ltd. 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.
from django.db.models.signals import post_save

from .models import GDPRSettings, get_setting


def handle_settings_post_save(sender, instance, **kwargs):
get_setting.cache_clear()


post_save.connect(
handle_settings_post_save,
sender=GDPRSettings,
dispatch_uid="shuup_gdpr:handle_settings_post_save"
)
6 changes: 3 additions & 3 deletions shuup/xtheme/_theme.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ def __init__(self, theme_settings=None, shop=None):
raise ValueError(_("Theme identifiers must match."))

self._theme_settings = theme_settings
self._shop = theme_settings.shop
self._shop = shop or theme_settings.shop

elif shop:
from shuup.xtheme.models import ThemeSettings
Expand Down Expand Up @@ -462,7 +462,7 @@ def get_theme_by_identifier(identifier, shop):
shop=shop
)[0]

return theme_cls(theme_settings=theme_settings)
return theme_cls(theme_settings=theme_settings, shop=shop)

return None # No such thing.

Expand Down Expand Up @@ -515,7 +515,7 @@ def _get_current_theme(shop):
if theme_settings:
theme_cls = get_identifier_to_object_map("xtheme").get(theme_settings.theme_identifier)
if theme_cls is not None:
theme = theme_cls(theme_settings=theme_settings)
theme = theme_cls(theme_settings=theme_settings, shop=shop)
else:
log.warn("Warning! The active theme %r is currently not installed.", theme_settings.theme_identifier)

Expand Down
5 changes: 3 additions & 2 deletions shuup/xtheme/admin_module/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def get_menu_entries(self, request): # doccov: ignore
]

def get_help_blocks(self, request, kind):
theme = get_current_theme(request.shop)
theme = getattr(request, "theme", None) or get_current_theme(request.shop)
if kind == "quicklink" and theme:
yield SimpleHelpBlock(
text=_("Customize the look and feel of your shop"),
Expand All @@ -83,7 +83,8 @@ def get_notifications(self, request):

if engine and isinstance(engine, Jinja2): # The engine is what we expect...
if isinstance(engine.env, XthemeEnvironment): # ... and it's capable of loading themes...
if not get_current_theme(request.shop): # ... but there's no theme active?!
if not (getattr(request, "theme", None) or get_current_theme(request.shop)):
# ... but there's no theme active?!
# Panic!
yield Notification(
text=_("No theme is active. Click here to activate one."),
Expand Down
4 changes: 2 additions & 2 deletions shuup/xtheme/editing.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ def can_edit(context):
if not (request and could_edit(request) and may_inject(context)):
return False

return bool(get_current_theme(request.shop))
return bool(getattr(request, "theme", None) or get_current_theme(request.shop))


def add_edit_resources(context):
Expand All @@ -102,7 +102,7 @@ def add_edit_resources(context):

from .rendering import get_view_config # avoid circular import
view_config = get_view_config(context)
theme = get_current_theme(request.shop)
theme = getattr(request, "theme", None) or get_current_theme(request.shop)
add_resource(context, "body_end", InlineScriptResource.from_vars("XthemeEditorConfig", {
"commandUrl": command_url,
"editUrl": edit_url,
Expand Down
Loading

0 comments on commit 453bc30

Please sign in to comment.