4. Product Models

Products can vary wildly, and modeling them is not always trivial. Some products are salable in pieces, while others are continues. Trying to define a set of product models, capable for describing all such scenarios is impossible –

4.1. Describe Products by customizing the Model

DjangoSHOP requires to describe products instead of prescribing prefabricated models.

All in all, we know best how our products should be modelled!

4.1.1. E-commerce solutions, claiming to be plug-and-play, usually use one of these (anti-)patterns

Either, they offer a field for every possible variation, or they use the Entity Attribute Value (EAV) pattern to add meta-data for each of our models. This at a first glance seems to be easy. But both approaches are unwieldy and have serious drawbacks. They both apply a different “physical schema” – the way data is stored, rather than a “logical schema” – the way users and applications require that data. As soon as we have to combine our e-commerce solution with some Enterprise Resource Planning (ERP) software, additional back-and-forward conversion routines have to be added.

4.1.2. In django-SHOP, the physical representation of a product always maps to its logical

Django-SHOP‘s approach to this problem is to have a minimal set of models. These abstract models are stubs providing to subclass the physical models. Hence the logical representation of the product conforms to their physical one. Moreover, it is even possible to represent various types of products by subclassing polymorphically from an abstract base model. Thanks to Django’s Object Relational Mapper, modeling the logical representation for a set of products, together with an administration backend, becomes almost effortless.

Therefore the base class to model a product is a stub which contains only these three fields:

The timestamps for created_at and updated_at; these are self-explanatory.

A boolean field active, used to signalize the products availability.

The attentive reader may wonder, why there not even fields for the most basic requirements of each sellable article, there is no product name, no price field and no product code.

The reason for this is, that django-SHOP does not impose any fields, which might require a different implementation for the merchants use case. However, for a sellable commodity some information is fundamental and required. But its up to him how to implement these fields:

The product’s name must be implemented as a model field or as a property method, but both must be declared as product_name. Use a method implementation for composed and translatable names, otherwise use a database model field with that name.

The product’s price must be implemented as a method declared as get_price(request) which accepts the request object. This gives the merchant the ability to vary the price and/or its currency depending on the geographic location, the customers login status, the browsers user-agent, or whatever else.

An optional, but highly recommended field is the products item number, declared as product_code. It shall return a unique and language independent identifier for each product, to be identifiable. In most cases the product code is implemented by the product model itself, but in some circumstances it may be implemented by the product’s variant. The SmartPhone from the demo code is one such example.

The example section of django-SHOP contains a few models which can be copied and adopted to the specific needs of the merchants products. Let’s have a look at a few use-cases:

4.2. Case study: Smart-Phones

There are many smart-phone models with different equipment. All the features are the same, except for the built-in storage. How shall we describe such a model?

In that model, the product’s name shall not be translatable, not even on a multi-lingual site, since smart-phones have international names used everywhere. Smart-phones models have dimensions, an operating system, a display type and other features.

But smart-phone have different equipment, namely the built-in storage, and depending on that, they have different prices and a unique product code. Therefore our product models consists of two classes, the generic smart phone model and the concrete flavor of that model.

Therefore we would model our smart-phones using a database model similar to the following one:

from shop.models.product import BaseProductManager, BaseProduct
from shop.money import Money

class SmartPhoneModel(BaseProduct):
    product_name = models.CharField(max_length=255,
        verbose_name=_("Product Name"))
    slug = models.SlugField(verbose_name=_("Slug"))
    description = HTMLField(help_text=_("Detailed description."))
    manufacturer = models.ForeignKey(Manufacturer,
        verbose_name=_("Manufacturer"))
    screen_size = models.DecimalField(_("Screen size"),
        max_digits=4, decimal_places=2)
    # other fields to map the specification sheet

    objects = BaseProductManager()
    lookup_fields = ('product_name__icontains',)

    def get_price(request):
        aggregate = self.smartphone_set.aggregate(models.Min('unit_price'))
        return Money(aggregate['unit_price__min'])

class SmartPhone(models.Model):
    product_model = models.ForeignKey(SmartPhoneModel)
    product_code = models.CharField(_("Product code"),
        max_length=255, unique=True)
    unit_price = MoneyField(_("Unit price"))
    storage = models.PositiveIntegerField(_("Internal Storage"))

Lets go into the details of these classes. The model fields are self-explanatory. Something to note here is, that each product requires a field product_name. This alternatively can also be implemented as property.

Another mandatory attribute for each product is the ProductManager class. It must inheriting from BaseProductManager, and adds some methods to generate some special querysets.

Finally, the attribute lookup_fields contains a list or tuple of lookup fields. These are required by the administration backend, and used when the site editor has to search for certain products. Since the framework does not impose which fields are used to distinguish between products, we must give some hints.

Each product also requires a method implemented as get_price(request). This must return the unit price using one of the available Money Types.

4.2.1. Add multilingual support

Adding multilingual support to an existing product is quite easy and straight forward. To achieve this django-SHOP uses the app django-parler which provides Django model translations without nasty hacks. All we have to do, is to replace the ProductManager with one capable of handling translations:

class ProductQuerySet(TranslatableQuerySet, PolymorphicQuerySet):
    pass

class ProductManager(BaseProductManager, TranslatableManager):
    queryset_class = ProductQuerySet

The next step is to locate the model fields, which shall be available in different languages. In our use-case thats only the product’s description:

class SmartPhoneModel(BaseProduct, TranslatableModel):
    # other field remain unchanged
    description = TranslatedField()

class ProductTranslation(TranslatedFieldsModel):
    master = models.ForeignKey(SmartPhoneModel, related_name='translations', null=True)
    description = HTMLField(help_text=_("Some more detailed description."))

    class Meta:
        unique_together = [('language_code', 'master')]

This simple change now allows us to offer the shop’s assortment in different natural languages.

4.2.2. Add Polymorphic Support

If besides smart phones we also want to sell cables, pipes or smart cards, we must split our product models into a common- and a specialized part. That said, we must separate the information every product requires from the information specific to a certain product type. Say, in addition to smart phones, we also want to sell smart cards. First we declare a generic Product model, which is a common base class of both, SmartPhone and SmartCard:

class Product(BaseProduct, TranslatableModel):
    product_name = models.CharField(max_length=255, verbose_name=_("Product Name"))
    slug = models.SlugField(verbose_name=_("Slug"), unique=True)
    description = TranslatedField()

    objects = ProductManager()
    lookup_fields = ('product_name__icontains',)

Next we only add the product specific attributes to the class models derived from Product:

class SmartPhoneModel(Product):
    manufacturer = models.ForeignKey(Manufacturer, verbose_name=_("Manufacturer"))
    screen_size = models.DecimalField(_("Screen size"), max_digits=4, decimal_places=2)
    battery_type = models.PositiveSmallIntegerField(_("Battery type"), choices=BATTERY_TYPES)
    battery_capacity = models.PositiveIntegerField(help_text=_("Battery capacity in mAh"))
    ram_storage = models.PositiveIntegerField(help_text=_("RAM storage in MB"))
    # and many more attributes as found on the data sheet

class SmartPhone(models.Model):
    product_model = models.ForeignKey(SmartPhoneModel)
    product_code = models.CharField(_("Product code"), max_length=255, unique=True)
    unit_price = MoneyField(_("Unit price"))
    storage = models.PositiveIntegerField(_("Internal Storage"))

class SmartCard(Product):
    product_code = models.CharField(_("Product code"), max_length=255, unique=True)
    storage = models.PositiveIntegerField(help_text=_("Storage capacity in GB"))
    unit_price = MoneyField(_("Unit price"))
    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)

If MyShop would sell the iPhone5 with 16GB and 32GB storage as independent products, then we could unify the classes SmartPhoneModel and SmartPhone and move the attributes product_code and unit_price into the class Product. This would simplify some programming aspects, but would require the merchant to add a lot of information twice. Therefore we remain with the model layout presented here.

4.3. Caveat using a ManyToManyField with existing models

Sometimes we may need to use a ManyToManyField for models which are handled by other apps in our project. This for example could be an attribute files referring the model filer.FilerFileField from the library django-filer. Here Django would try to create a mapping table, where the foreign key to our product model can not be resolved properly, because while bootstrapping the application, our Product model is still considered to be deferred.

Therefore, we have to create our own mapping model and refer to it using the through parameter, as shown in this example:

from six import with_metaclass
from django.db import models
from filer.fields.file import FilerFileField
from shop.models import deferred
from shop.models.product import BaseProductManager, BaseProduct

class ProductFile(with_metaclass(deferred.ForeignKeyBuilder, models.Model)):
    file = FilerFileField()
    product = deferred.ForeignKey(BaseProduct)

class Product(BaseProduct):
    # other fields
    files = models.ManyToManyField('filer.File', through=ProductFile)

    objects = ProductManager()

Note

Do not use this example for creating a many-to-many field to FilerImageField. Instead use shop.models.related.BaseProductImage which is a base class for this kind of mapping. Just import and materialize it, in your own project.