Repository: shuup/shuup

Issue:
Admin: product visibility mass action doesn't bump caches
After products are cached in xtheme plugins (like highlight) and products are hided through mass action in product admin, the storefront just doesn't change. Looks like we are missing to bump caches.


Discussion:

Resolving patch:
diff --git a/shuup/admin/modules/orders/mass_actions.py b/shuup/admin/modules/orders/mass_actions.py
--- a/shuup/admin/modules/orders/mass_actions.py
+++ b/shuup/admin/modules/orders/mass_actions.py
@@ -14,6 +14,7 @@
 from django.utils.translation import ugettext
 from six import BytesIO
 
+from shuup.admin.shop_provider import get_shop
 from shuup.admin.utils.picotable import (
     PicotableFileMassAction, PicotableMassAction
 )
@@ -28,9 +29,11 @@ class CancelOrderAction(PicotableMassAction):
     identifier = "mass_action_order_cancel"
 
     def process(self, request, ids):
-        query = Q(id__in=ids)
+        shop = get_shop(request)
         if isinstance(ids, six.string_types) and ids == "all":
-            query = Q()
+            query = Q(shop=shop)
+        else:
+            query = Q(id__in=ids, shop=shop)
         for order in Order.objects.filter(query):
             if not order.can_set_canceled():
                 continue
diff --git a/shuup/admin/modules/products/mass_actions.py b/shuup/admin/modules/products/mass_actions.py
--- a/shuup/admin/modules/products/mass_actions.py
+++ b/shuup/admin/modules/products/mass_actions.py
@@ -14,10 +14,12 @@
 
 from shuup.admin.modules.products.views.list import ProductListView
 from shuup.admin.modules.settings.view_settings import ViewSettings
+from shuup.admin.shop_provider import get_shop
 from shuup.admin.utils.picotable import (
     PicotableFileMassAction, PicotableMassAction, PicotableRedirectMassAction
 )
 from shuup.core.models import ShopProduct, ShopProductVisibility
+from shuup.core.utils import context_cache
 
 
 class VisibleMassAction(PicotableMassAction):
@@ -25,10 +27,16 @@ class VisibleMassAction(PicotableMassAction):
     identifier = "mass_action_product_visible"
 
     def process(self, request, ids):
-        query = Q(product__pk__in=ids)
+        shop = get_shop(request)
+
         if isinstance(ids, string_types) and ids == "all":
-            query = Q()
+            query = Q(shop=shop)
+        else:
+            query = Q(product__pk__in=ids, shop=shop)
+
         ShopProduct.objects.filter(query).update(visibility=ShopProductVisibility.ALWAYS_VISIBLE)
+        for shop_product in ShopProduct.objects.filter(query).iterator():
+            context_cache.bump_cache_for_shop_product(shop_product)
 
 
 class InvisibleMassAction(PicotableMassAction):
@@ -36,10 +44,15 @@ class InvisibleMassAction(PicotableMassAction):
     identifier = "mass_action_product_invisible"
 
     def process(self, request, ids):
-        query = Q(product__pk__in=ids)
+        shop = get_shop(request)
         if isinstance(ids, string_types) and ids == "all":
-            query = Q()
+            query = Q(shop=shop)
+        else:
+            query = Q(product__pk__in=ids, shop=shop)
+
         ShopProduct.objects.filter(query).update(visibility=ShopProductVisibility.NOT_VISIBLE)
+        for shop_product in ShopProduct.objects.filter(query).iterator():
+            context_cache.bump_cache_for_shop_product(shop_product)
 
 
 class FileResponseAction(PicotableFileMassAction):
@@ -47,9 +60,11 @@ class FileResponseAction(PicotableFileMassAction):
     identifier = "mass_action_product_simple_csv"
 
     def process(self, request, ids):
-        query = Q(id__in=ids)
+        shop = get_shop(request)
         if isinstance(ids, string_types) and ids == "all":
-            query = Q()
+            query = Q(shop=shop)
+        else:
+            query = Q(product__pk__in=ids, shop=shop)
         view_settings = ViewSettings(ShopProduct, ProductListView.default_columns, ProductListView)
         response = HttpResponse(content_type='text/csv')
         response['Content-Disposition'] = 'attachment; filename="products.csv"'


Test patch:
diff --git a/shuup_tests/admin/test_mass_actions.py b/shuup_tests/admin/test_mass_actions.py
--- a/shuup_tests/admin/test_mass_actions.py
+++ b/shuup_tests/admin/test_mass_actions.py
@@ -4,13 +4,18 @@
 #
 # 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 csv
+import csv, mock
 
 import pytest
 from django.http import HttpResponse
+from django.core.cache import cache
 from shuup.admin.utils.picotable import PicotableMassAction, PicotableFileMassAction
-from shuup.core.models import Product
-from shuup.testing.factories import get_default_shop, get_default_supplier, create_product
+from shuup.admin.modules.products.mass_actions import InvisibleMassAction, VisibleMassAction
+
+from shuup.core.models import Product, Shop, ShopProduct, ShopStatus, ShopProductVisibility
+from shuup.core.utils import context_cache
+
+from shuup.testing.factories import get_default_shop, get_default_supplier, create_product, get_default_currency, get_default_product
 from shuup.testing.utils import apply_request_middleware
 from shuup_tests.utils import printable_gibberish
 
@@ -48,3 +53,57 @@ def test_mass_actions(rf, admin_user):
 
     mass_action_response = TestPicotableFileMassAction().process(request, ids)
     assert mass_action_response["Content-disposition"] == 'attachment; filename="products.csv"'
+
+
+@pytest.mark.django_db
+def test_mass_action_cache(rf, admin_user):
+    shop = get_default_shop()
+    cache.clear()
+    product = create_product(printable_gibberish(), shop=shop, default_price=50)
+    product2 = create_product(printable_gibberish(), shop=shop, default_price=100)
+    set_bump_cache_for_shop_product = mock.Mock(wraps=context_cache.bump_cache_for_shop_product)
+
+    def bump_cache_for_shop_product(item):
+        return set_bump_cache_for_shop_product(item)
+
+    request = apply_request_middleware(rf.get("/"), user=admin_user, shop=shop)
+
+    with mock.patch.object(context_cache, "bump_cache_for_shop_product", new=bump_cache_for_shop_product):
+        assert set_bump_cache_for_shop_product.call_count == 0
+        InvisibleMassAction().process(request, [product.id, product2.id])
+        assert set_bump_cache_for_shop_product.call_count == 2
+
+        VisibleMassAction().process(request, [product.id, product2.id])
+        assert set_bump_cache_for_shop_product.call_count == 4
+
+
+@pytest.mark.django_db
+def test_mass_action_multishop(rf, admin_user):
+    def create_shop(name):
+        return Shop.objects.create(
+            name="foobar",
+            identifier=name,
+            status=ShopStatus.ENABLED,
+            public_name=name,
+            currency=get_default_currency().code
+        )
+    shop_one = get_default_shop()
+    shop_two = create_shop("foobar")
+    product = get_default_product()
+    shop_product_one = product.get_shop_instance(shop_one)
+    shop_product_two = ShopProduct.objects.create(shop=shop_two, product=product)
+    shop_product_two.save()
+    assert shop_product_one.visibility == ShopProductVisibility.ALWAYS_VISIBLE
+    assert shop_product_two.visibility == ShopProductVisibility.ALWAYS_VISIBLE
+    request = apply_request_middleware(rf.get("/"), user=admin_user, shop=shop_one)
+    InvisibleMassAction().process(request, 'all')
+    shop_product_one.refresh_from_db()
+    shop_product_two.refresh_from_db()
+    assert shop_product_one.visibility == ShopProductVisibility.NOT_VISIBLE
+    assert shop_product_two.visibility == ShopProductVisibility.ALWAYS_VISIBLE
+    request = apply_request_middleware(rf.get("/"), user=admin_user, shop=shop_one)
+    VisibleMassAction().process(request, 'all')
+    shop_product_one.refresh_from_db()
+    shop_product_two.refresh_from_db()
+    assert shop_product_two.visibility == ShopProductVisibility.ALWAYS_VISIBLE
+    assert shop_product_one.visibility == ShopProductVisibility.ALWAYS_VISIBLE