Repository: saleor/saleor

Issue:
Introduce attribute based variant picker
Variant picker should allow to select variants by attributes for example color and size.
Variant data should be exposed by an API endpoint or by view context.

![image](https://cloud.githubusercontent.com/assets/1754812/21272090/05aa81d2-c3be-11e6-9346-a11ab42b044a.png)

For MVP purposes I suggest following approach:
- Create React component that renders radio group for each variant attribute
- Component should update price automatically 
- When product is unavailable or particular variant combination does not exist we should display information that product is unavailable.


Discussion:

Resolving patch:
diff --git a/saleor/product/models/utils.py b/saleor/product/models/utils.py
--- a/saleor/product/models/utils.py
+++ b/saleor/product/models/utils.py
@@ -1,9 +1,10 @@
 from django.utils.encoding import smart_text
 
-def get_attributes_display_map(variant, attributes):
+
+def get_attributes_display_map(obj, attributes):
     display = {}
     for attribute in attributes:
-        value = variant.get_attribute(attribute.pk)
+        value = obj.attributes.get(smart_text(attribute.pk))
         if value:
             choices = {smart_text(a.pk): a for a in attribute.values.all()}
             attr = choices.get(value)
diff --git a/saleor/product/utils.py b/saleor/product/utils.py
--- a/saleor/product/utils.py
+++ b/saleor/product/utils.py
@@ -3,6 +3,7 @@
 from ..cart.decorators import get_cart_from_request
 from ..core.utils import to_local_currency
 from .forms import get_form_class_for_product
+from .models.utils import get_attributes_display_map
 from .models import Product
 
 
@@ -90,3 +91,34 @@ def products_for_cart(user):
     products = products.prefetch_related(
         'variants', 'variants__variant_images__image')
     return products
+
+
+def get_variant_picker_data(variants, attributes):
+    data = {'attributes': [], 'variants': []}
+    for attribute in attributes:
+        data['attributes'].append({
+            'pk': attribute.pk,
+            'display': attribute.display,
+            'name': attribute.name,
+            'values': [{'pk': value.pk, 'display': value.display}
+                       for value in attribute.values.all()]
+        })
+    for variant in variants:
+        price = variant.get_price_per_item()
+        variant_data = {
+            'id': variant.id,
+            'price': float(price.gross),
+            'currency': price.currency,
+            'attributes': variant.attributes
+        }
+        data['variants'].append(variant_data)
+    return data
+
+
+def get_product_attributes_data(product):
+    attributes = product.product_class.product_attributes.prefetch_related(
+        'values')
+    attributes_map = {attribute.pk: attribute for attribute in attributes}
+    values_map = get_attributes_display_map(product, attributes)
+    return {attributes_map.get(attr_pk): value_obj
+            for (attr_pk, value_obj) in values_map.items()}
diff --git a/saleor/product/views.py b/saleor/product/views.py
--- a/saleor/product/views.py
+++ b/saleor/product/views.py
@@ -1,9 +1,11 @@
 from __future__ import unicode_literals
 
 import datetime
+import json
+
 from django.conf import settings
 from django.core.urlresolvers import reverse
-from django.http import HttpResponsePermanentRedirect
+from django.http import HttpResponsePermanentRedirect, JsonResponse
 from django.shortcuts import get_object_or_404, redirect
 from django.template.response import TemplateResponse
 
@@ -13,7 +15,8 @@
 from .utils import (products_with_details, products_for_cart,
                     products_with_availability,
                     handle_cart_form, get_availability,
-                    get_product_images)
+                    get_product_images, get_variant_picker_data,
+                    get_product_attributes_data)
 
 
 def product_details(request, slug, product_id, form=None):
@@ -55,21 +58,27 @@ def product_details(request, slug, product_id, form=None):
         product.available_on is None or product.available_on <= today)
     if form is None:
         form = handle_cart_form(request, product, create_cart=False)[0]
-
     availability = get_availability(product, discounts=request.discounts,
                                     local_currency=request.currency)
-
     template_name = 'product/details_%s.html' % (
         type(product).__name__.lower(),)
     templates = [template_name, 'product/details.html']
     product_images = get_product_images(product)
+    variant_picker_data = get_variant_picker_data(
+        product.variants.all(),
+        product.product_class.variant_attributes.prefetch_related('values'))
+    product_attributes = get_product_attributes_data(product)
+    show_variant_picker = all([v.attributes for v in product.variants.all()])
     return TemplateResponse(
         request, templates,
         {'is_visible': is_visible,
          'form': form,
          'availability': availability,
+         'product': product,
+         'product_attributes': product_attributes,
          'product_images': product_images,
-         'product': product})
+         'show_variant_picker': show_variant_picker,
+         'variant_picker_data': json.dumps(variant_picker_data)})
 
 
 def product_add_to_cart(request, slug, product_id):
@@ -83,9 +92,15 @@ def product_add_to_cart(request, slug, product_id):
     form, cart = handle_cart_form(request, product, create_cart=True)
     if form.is_valid():
         form.save()
-        response = redirect('cart:index')
+        if request.is_ajax():
+            response = JsonResponse({'next': reverse('cart:index')}, status=200)
+        else:
+            response = redirect('cart:index')
     else:
-        response = product_details(request, slug, product_id, form)
+        if request.is_ajax():
+            response = JsonResponse({'error': form.errors}, status=400)
+        else:
+            response = product_details(request, slug, product_id, form)
     if not request.user.is_authenticated():
         set_cart_cookie(cart, response)
     return response


Test patch: