Repository: saleor/saleor

Issue:
Fix crop sizes for product images and adjust placeholder sizes
Crop sizes in settings for product images should be changed according to new design and should support retina screens. Changes need to be applied in Django templates and in product item React component.
Also placeholder sizes should be adjusted to new design.


Discussion:

Resolving patch:
diff --git a/saleor/graphql/api.py b/saleor/graphql/api.py
--- a/saleor/graphql/api.py
+++ b/saleor/graphql/api.py
@@ -39,7 +39,10 @@ class ProductAvailabilityType(graphene.ObjectType):
 
 class ProductType(DjangoObjectType):
     url = graphene.String()
-    thumbnail_url = graphene.String()
+    thumbnail_url = graphene.String(
+        size=graphene.Argument(
+            graphene.String,
+            description="The size of a thumbnail, for example 255x255"))
     images = graphene.List(lambda: ProductImageType)
     variants = graphene.List(lambda: ProductVariantType)
     availability = graphene.Field(lambda: ProductAvailabilityType)
@@ -50,7 +53,10 @@ class Meta:
         interfaces = (relay.Node, DjangoPkInterface)
 
     def resolve_thumbnail_url(self, args, context, info):
-        return product_first_image(self, '400x400')
+        size = args.get('size')
+        if not size:
+            size = '255x255'
+        return product_first_image(self, size)
 
     def resolve_images(self, args, context, info):
         return self.images.all()
diff --git a/saleor/product/templatetags/product_images.py b/saleor/product/templatetags/product_images.py
--- a/saleor/product/templatetags/product_images.py
+++ b/saleor/product/templatetags/product_images.py
@@ -1,4 +1,5 @@
 import logging
+import re
 import warnings
 
 from django import template
@@ -18,13 +19,34 @@ def get_available_sizes():
             all_sizes.add(size)
     return all_sizes
 
+
 AVAILABLE_SIZES = get_available_sizes()
 
 
+def choose_placeholder(size=''):
+    # type: (str) -> str
+    """Assign placeholder at least as big as provided size if possible.
+    When size is bigger than available, return the biggest.
+    If size is invalid or not provided, return DEFAULT_PLACEHOLDER"""
+    placeholder = settings.DEFAULT_PLACEHOLDER
+    parsed_sizes = re.match(r'(\d+)x(\d+)', size)
+    available_sizes = sorted(settings.PLACEHOLDER_IMAGES.keys())
+    if parsed_sizes and available_sizes:
+        # check for placeholder equal or bigger than requested picture
+        x_size, y_size = parsed_sizes.groups()
+        max_size = max([int(x_size), int(y_size)])
+        bigger_or_eq = list(filter(lambda x: x >= max_size, available_sizes))
+        if len(bigger_or_eq) > 0:
+            placeholder = settings.PLACEHOLDER_IMAGES[bigger_or_eq[0]]
+        else:
+            placeholder = settings.PLACEHOLDER_IMAGES[available_sizes[-1]]
+    return placeholder
+
+
 @register.simple_tag()
 def get_thumbnail(instance, size, method='crop'):
+    size_name = '%s__%s' % (method, size)
     if instance:
-        size_name = '%s__%s' % (method, size)
         if (size_name not in AVAILABLE_SIZES and not
                 settings.VERSATILEIMAGEFIELD_SETTINGS['create_images_on_demand']):
             msg = ('Thumbnail size %s is not defined in settings '
@@ -40,7 +62,7 @@ def get_thumbnail(instance, size, method='crop'):
                              extra={'instance': instance, 'size': size})
         else:
             return thumbnail.url
-    return static('images/product-image-placeholder.png')
+    return static(choose_placeholder(size))
 
 
 @register.simple_tag()
diff --git a/saleor/settings.py b/saleor/settings.py
--- a/saleor/settings.py
+++ b/saleor/settings.py
@@ -303,11 +303,12 @@ def get_host():
 
 VERSATILEIMAGEFIELD_RENDITION_KEY_SETS = {
     'defaults': [
-        ('list_view', 'crop__100x100'),
-        ('dashboard', 'crop__400x400'),
-        ('product_page_mobile', 'crop__680x680'),
-        ('product_page_big', 'crop__750x750'),
-        ('product_page_thumb', 'crop__280x280')]}
+        ('product_gallery', 'crop__540x540'),
+        ('product_gallery_2x', 'crop__1080x1080'),
+        ('product_small', 'crop__60x60'),
+        ('product_small_2x', 'crop__120x120'),
+        ('product_list', 'crop__255x255'),
+        ('product_list_2x', 'crop__510x510')]}
 
 VERSATILEIMAGEFIELD_SETTINGS = {
     # Images should be pre-generated on Production environment
@@ -315,6 +316,16 @@ def get_host():
         os.environ.get('CREATE_IMAGES_ON_DEMAND', 'True')),
 }
 
+PLACEHOLDER_IMAGES = {
+    60: 'images/placeholder60x60.png',
+    120: 'images/placeholder120x120.png',
+    255: 'images/placeholder255x255.png',
+    540: 'images/placeholder540x540.png',
+    1080: 'images/placeholder1080x1080.png'
+}
+
+DEFAULT_PLACEHOLDER = 'images/placeholder255x255.png'
+
 WEBPACK_LOADER = {
     'DEFAULT': {
         'CACHE': not DEBUG,


Test patch:
diff --git a/tests/test_product_tags.py b/tests/test_product_tags.py
--- a/tests/test_product_tags.py
+++ b/tests/test_product_tags.py
@@ -1,8 +1,7 @@
-from mock import Mock
 from django.contrib.staticfiles.templatetags.staticfiles import static
+from mock import Mock
 from saleor.product.templatetags.product_images import (
-    get_thumbnail, product_first_image)
-
+    choose_placeholder, get_thumbnail,product_first_image)
 
 def test_get_thumbnail():
     instance = Mock()
@@ -16,9 +15,13 @@ def test_get_thumbnail():
     assert thumb == thumbnail_value.url
 
 
-def test_get_thumbnail_no_instance():
+def test_get_thumbnail_no_instance(monkeypatch):
+    """When no instance, function should return placeholder"""
+    monkeypatch.setattr(
+        'saleor.product.templatetags.product_images.choose_placeholder',
+        lambda x: 'placeholder')
     output = get_thumbnail(instance=None, size='10x10', method='crop')
-    assert output == static('images/product-image-placeholder.png')
+    assert output == static('placeholder')
 
 
 def test_product_first_image():
@@ -31,3 +34,28 @@ def test_product_first_image():
     mock_product = Mock(images=mock_queryset)
     out = product_first_image(mock_product, '10x10', method='crop')
     assert out == 'crop.jpg'
+
+
+def test_choose_placeholder(settings):
+    settings.PLACEHOLDER_IMAGES = {
+        10: '10_placeholder',
+        20: '20_placeholder',
+        30: '30_placeholder'}
+
+    settings.DEFAULT_PLACEHOLDER = 'default_placeholder'
+
+    # wrong or no size returns default
+    assert choose_placeholder('wrong') == settings.DEFAULT_PLACEHOLDER
+    assert choose_placeholder() == settings.DEFAULT_PLACEHOLDER
+
+    # exact size
+    assert choose_placeholder('10x10') == settings.PLACEHOLDER_IMAGES[10]
+
+    # when exact not found, choose bigger available
+    assert choose_placeholder('15x15') == settings.PLACEHOLDER_IMAGES[20]
+
+    # like previous, but only one side bigger
+    assert choose_placeholder('10x15') == settings.PLACEHOLDER_IMAGES[20]
+
+    # when too big requested, choose the biggest available
+    assert choose_placeholder('1500x1500') == settings.PLACEHOLDER_IMAGES[30]