5. Products with Different Properties

In the previous examples we have seen that we can model our products according to their physical properties, but what if we want to sell another type of a product with different properties. This is where polymorphism enters the scene.

5.1. Run the Polymorphic Demo

To test this example, set the shell environment variable export DJANGO_SHOP_TUTORIAL=polymorphic, then recreate the database as explained in Create a database for the demo and start the demo server:

./manage.py runserver

5.2. The Polymorphic Product Model

If in addition to Smart Cards we also want to sell Smart Phones, we must declare a new model. Here instead of duplicating all the common fields, we unify them into a common base class named Product. Then that base class shall be extended to become either our known model SmartCard or a new model SmartPhone.

To enable polymorphic models in django-SHOP, we require the application django-polymorphic. Here our models for Smart Cards or Smart Phones will be split up into a generic part and a specialized part. The generic part goes into our new Product model, whereas the specialized parts remain in their models.

You should already start to think about the layout of the list views. Only attributes in model Product will be available for list views displaying Smart Phones side by side with Smart Cards. First we must create a special Model Manager which unifies the query methods for translatable and polymorphic models:

myshop/models/i18n/polymorphic/product.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
from parler.models import TranslatableModelMixin, TranslatedFieldsModel
from parler.fields import TranslatedField
from parler.managers import TranslatableManager, TranslatableQuerySet
from polymorphic.query import PolymorphicQuerySet
from shop.models.product import BaseProductManager, BaseProduct, CMSPageReferenceMixin
from shop.models.defaults.mapping import ProductPage, ProductImage
from ..manufacturer import Manufacturer

class ProductQuerySet(TranslatableQuerySet, PolymorphicQuerySet):
    pass

class ProductManager(BaseProductManager, TranslatableManager):
    queryset_class = ProductQuerySet

        return qs.prefetch_related('translations')


@python_2_unicode_compatible
class Product(CMSPageReferenceMixin, TranslatableModelMixin, BaseProduct):
    # controlling the catalog

The next step is to identify which model attributes qualify for being part of our Product model. Unfortunately, there is no silver bullet for this problem and that’s one of the reason why django-SHOP is shipped without any prepared model for it. If we want to sell both Smart Cards and Smart Phones, then this Product model may do its jobs:

myshop/models/i18n/polymorphic/product.py
1
2
3
4
5
6
7
8
9
    """
    Base class to describe a polymorphic product. Here we declare common fields available in all of
    our different product types. These common fields are also used to build up the view displaying
    a list of all products.
    """
    product_name = models.CharField(max_length=255, verbose_name=_("Product Name"))
    slug = models.SlugField(verbose_name=_("Slug"), unique=True)
    caption = TranslatedField()

5.2.1. Model for Smart Card

The model used to store translated fields is the same as in our last example. The new model for Smart Cards now inherits from Product:

myshop/models/i18n/polymorphic/smartcard.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
from django.db import models
from django.utils.translation import ugettext_lazy as _
from djangocms_text_ckeditor.fields import HTMLField
from shop.money.fields import MoneyField
from parler.models import TranslatedFields


class SmartCard(Product):
    # common product fields
    unit_price = MoneyField(_("Unit price"), decimal_places=3,
                            help_text=_("Net price for this product"))

    # product properties
    CARD_TYPE = (2 * ('{}{}'.format(s, t),)
                 for t in ('SD', 'SDXC', 'SDHC', 'SDHC II') for s in ('', 'micro '))
    card_type = models.CharField(_("Card Type"), choices=CARD_TYPE, max_length=15)
    SPEED = [(str(s), "{} MB/s".format(s)) for s in (4, 20, 30, 40, 48, 80, 95, 280)]
    speed = models.CharField(_("Transfer Speed"), choices=SPEED, max_length=8)
    product_code = models.CharField(_("Product code"), max_length=255, unique=True)
    storage = models.PositiveIntegerField(_("Storage Capacity"),

5.2.2. Model for Smart Phone

The product model for Smart Phones is intentionally a little bit more complicated. Not only does it have a few more attributes, but Smart Phones can be sold with different specifications of internal storage. The latter influences the price and the product code. This is also the reason why we didn’t move the model fields unit_price and products_code into our base class Product, although every product in our shop requires them.

When presenting Smart Phones in our list views, we want to focus on different models, but not on each flavor, ie. its internal storage. Therefore customers will have to differentiate between the concrete Smart Phone variations, whenever they add them to their cart, but not when viewing them in the catalog list. For a customer, it would be very boring to scroll through lists with many similar products, which only differentiate by a few variations.

This means that for some Smart Phone models, there is be more than one Add to cart button.

When modeling, we therefore require two different classes, one for the Smart Phone model and one for each Smart Phone variation.

myshop/models/polymorphic/smartphone.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from shop.money import Money, MoneyMaker
from djangocms_text_ckeditor.fields import HTMLField
from shop.money.fields import MoneyField
from parler.models import TranslatedFields


class SmartPhoneModel(Product):
    """
    A generic smart phone model, which must be concretized by a model `SmartPhone` - see below.
    """
    BATTERY_TYPES = (
        (1, "Lithium Polymer (Li-Poly)"),
        (2, "Lithium Ion (Li-Ion)"),
    )
    WIFI_CONNECTIVITY = (
        (1, "802.11 b/g/n"),
    )
    BLUETOOTH_CONNECTIVITY = (
        (1, "Bluetooth 4.0"),
        (2, "Bluetooth 3.0"),
        (3, "Bluetooth 2.1"),
    )
    battery_type = models.PositiveSmallIntegerField(_("Battery type"), choices=BATTERY_TYPES)
    battery_capacity = models.PositiveIntegerField(_("Capacity"),
                                                   help_text=_("Battery capacity in mAh"))
    ram_storage = models.PositiveIntegerField(_("RAM"), help_text=_("RAM storage in MB"))
    wifi_connectivity = models.PositiveIntegerField(_("WiFi"), choices=WIFI_CONNECTIVITY,
                                                    help_text=_("WiFi Connectivity"))
    bluetooth = models.PositiveIntegerField(_("Bluetooth"), choices=BLUETOOTH_CONNECTIVITY,

Here the method get_price() can only return the minimum, average or maximum price for our product. In this situation, most merchants extol the prices as: Price starting at € 99.50.

The concrete Smart Phone then is modeled as:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
            raise SmartPhoneModel.DoesNotExist(e)


class SmartPhone(models.Model):
    product = models.ForeignKey(SmartPhoneModel, verbose_name=_("Smart-Phone Model"))
    product_code = models.CharField(_("Product code"), max_length=255, unique=True)
    unit_price = MoneyField(_("Unit price"), decimal_places=3,
                            help_text=_("Net price for this product"))
    storage = models.PositiveIntegerField(_("Internal Storage"),
                                          help_text=_("Internal storage in MB"))

To proceed with purchasing, customers need some Cart and Checkout pages.

5.2.3. Model for a generic Commodity

For demo purposes, this polymorphic example adds another kind of Product model, a generic Commodity. Here instead of adding every possible attribute of our product to the model, we try to remain as generic as possible, and instead use a PlaceholderField as provided by djangoCMS.

myshop/models/commodity.py
from cms.models.fields import PlaceholderField
from myshop.models.product import Product

class Commodity(Product):
    # other product fields
    placeholder = PlaceholderField("Commodity Details")

This allows us to add any arbitrary information to our product’s detail page. The only requirement for this to work is, that the rendering template adds a templatetag to render this placeholder.

Since the django-SHOP framework looks in the folder catalog for a template named after its product class, adding this HTML snippet should do the job:

myshop/catalog/commodity-detail.html
{% extends "myshop/pages/default.html" %}
{% load cms_tags %}

<div class="container">
    <div class="row">
        <div class="col-xs-12">
            <h1>{% render_model product "product_name" %}</h1>
            {% render_placeholder product.placeholder %}
        </div>
    </div>
</div>

This detail template extends the default template of our site. Apart from the product’s name (which has added as a convenience), this view remains empty when first viewed. In Edit mode, double clicking on the heading containing the product name, opens the detail editor for our commodity.

After switching into Structure mode, a placeholder named Commodity Details appears. Here we can add as many Cascade plugins as we want, by subdividing our placeholder into rows, columns, images, text blocks, etc. It allows us to edit the detail view of our commodity in whatever layout we like. The drawback using this approach is, that it can lead to inconsistent design and is much more labor intensive, than just editing the product’s attributes together with their appropriate templates.

5.2.3.1. Configure the Placeholder

Since we use this placeholder inside a hard-coded Bootstrap column, we must provide a hint to Cascade about the widths of that column. This has to be done in the settings of the project:

myshop/settings.py
CMS_PLACEHOLDER_CONF = {
  ...
  'Commodity Details': {
    'plugins': ['BootstrapRowPlugin', 'TextPlugin', 'ImagePlugin', 'PicturePlugin'],
    'text_only_plugins': ['TextLinkPlugin'],
    'parent_classes': {'BootstrapRowPlugin': []},
    'require_parent': False,
    'glossary': {
      'breakpoints': ['xs', 'sm', 'md', 'lg'],
      'container_max_widths': {'xs': 750, 'sm': 750, 'md': 970, 'lg': 1170},
      'fluid': False,
      'media_queries': {
        'xs': ['(max-width: 768px)'],
        'sm': ['(min-width: 768px)', '(max-width: 992px)'],
        'md': ['(min-width: 992px)', '(max-width: 1200px)'],
        'lg': ['(min-width: 1200px)'],
      },
    }
  },
  ...
}

This placeholder configuration emulates the Bootstrap column as declared by <div class="col-xs-12">.