Repository: saleor/saleor

Issue:
Variants displayed in product add to cart form even when product type has no variants
### What I'm trying to achieve

...Smaller form for products with no variants

### Steps to reproduce the problem

1. Create a product type with no variants
2. Create a product based on that product type
3. Open product details page in the consumer website

### What I expected to happen

...Variants field is not shown when product has no variants

### What happened instead/how it failed

...Variants field is displayed with SKU and price

(Please include a stack trace if this problem results in a crash.)
![capture](https://user-images.githubusercontent.com/6353787/34457534-47bc27fc-edab-11e7-9197-5ffff5d033cd.JPG)

Mostly I'm wondering if this is expected behaviour? Displaying a product form containing only one option showing the SKU seems a bit odd for the enduser. In my app, I am probably going to set the product variant field to hidden in the product variant form for products with no variants.


Discussion:

Resolving patch:
diff --git a/saleor/product/forms.py b/saleor/product/forms.py
--- a/saleor/product/forms.py
+++ b/saleor/product/forms.py
@@ -11,6 +11,9 @@
 class VariantChoiceField(forms.ModelChoiceField):
     discounts = None
 
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
     def label_from_instance(self, obj):
         variant_label = smart_text(obj)
         label = pgettext_lazy(
@@ -20,6 +23,20 @@ def label_from_instance(self, obj):
                 'price': gross(obj.get_price(discounts=self.discounts))}
         return label
 
+    def update_field_data(self, variants, cart):
+        """ Function initializing fields custom data """
+        self.queryset = variants
+        self.discounts = cart.discounts
+        self.empty_label = None
+        images_map = {variant.pk: [vi.image.image.url
+                                   for vi in variant.variant_images.all()]
+                      for variant in variants.all()}
+        self.widget.attrs['data-images'] = json.dumps(images_map)
+        # Don't display select input if there are less than two variants
+        if self.queryset.count() < 2:
+            self.widget = forms.HiddenInput(
+                {'value': variants.all()[0].pk})
+
 
 class ProductForm(AddToCartForm):
     variant = VariantChoiceField(queryset=None)
@@ -27,14 +44,7 @@ class ProductForm(AddToCartForm):
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
         variant_field = self.fields['variant']
-        variant_field.queryset = self.product.variants
-        variant_field.discounts = self.cart.discounts
-        variant_field.empty_label = None
-        images_map = {variant.pk: [vi.image.image.url
-                                   for vi in variant.variant_images.all()]
-                      for variant in self.product.variants.all()}
-        variant_field.widget.attrs['data-images'] = json.dumps(images_map)
+        variant_field.update_field_data(self.product.variants, self.cart)
 
     def get_variant(self, cleaned_data):
         return cleaned_data.get('variant')
-
diff --git a/saleor/product/views.py b/saleor/product/views.py
--- a/saleor/product/views.py
+++ b/saleor/product/views.py
@@ -63,6 +63,7 @@ def product_details(request, slug, product_id, form=None):
     variant_picker_data = get_variant_picker_data(
         product, request.discounts, request.currency)
     product_attributes = get_product_attributes_data(product)
+    # show_variant_picker determines if variant picker is used or select input
     show_variant_picker = all([v.attributes for v in product.variants.all()])
     json_ld_data = product_json_ld(product, availability, product_attributes)
     return TemplateResponse(


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,6 +1,6 @@
 from decimal import Decimal
 from io import BytesIO
-from unittest.mock import MagicMock
+from unittest.mock import MagicMock, Mock
 
 import pytest
 from django.contrib.auth.models import AnonymousUser, Group, Permission
@@ -9,6 +9,7 @@
 from django.core.files.uploadedfile import SimpleUploadedFile
 from django.utils.encoding import smart_text
 from PIL import Image
+from prices import Price
 
 from saleor.cart import utils
 from saleor.cart.models import Cart
@@ -294,7 +295,7 @@ def product_list(product_class, default_category):
 
     return [product_1, product_2, product_3]
 
-from prices import Price
+
 @pytest.fixture
 def order_list(admin_user, billing_address):
     data = {
diff --git a/tests/dashboard/test_product.py b/tests/dashboard/test_product.py
--- a/tests/dashboard/test_product.py
+++ b/tests/dashboard/test_product.py
@@ -1,9 +1,12 @@
 from io import BytesIO
 import json
 
+from unittest.mock import Mock, MagicMock
+
 from PIL import Image
 from django.conf import settings
 from django.core.files.uploadedfile import SimpleUploadedFile
+from django.forms import HiddenInput
 from django.urls import reverse
 from django.utils.encoding import smart_text
 import pytest
@@ -11,6 +14,7 @@
 from saleor.dashboard.product import ProductBulkAction
 from saleor.dashboard.product.forms import (
     ProductBulkUpdate, ProductClassForm, ProductForm)
+from saleor.product.forms import VariantChoiceField
 from saleor.product.models import (
     AttributeChoiceValue, Product, ProductAttribute, ProductClass,
     ProductImage, ProductVariant, Stock, StockLocation)
@@ -640,3 +644,13 @@ def test_product_select_classes_by_ajax(admin_client, product_class):
     assert response.status_code == 200
     assert resp_decoded.get('redirectUrl') == reverse(
         'dashboard:product-add', kwargs={'class_pk': product_class.pk})
+
+
+def test_hide_field_in_variant_choice_field_form():
+    form = VariantChoiceField(Mock)
+    variants, cart = MagicMock(), MagicMock()
+    variants.count.return_value = 1
+    variants.all()[0].pk = 'test'
+    form.update_field_data(variants, cart)
+    assert isinstance(form.widget, HiddenInput)
+    assert form.widget.attrs.get('value') == 'test'