3. Modeling a simple product

As a simple example, this tutorial uses Smart Cards as its first product. As emphasized earlier, django-SHOP is not shipped with ready to use product models. Instead the merchant must declare these models based on the products properties. Lets have a look ar a model describing a typical Smart Card:

myshop/models/smartcard.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
30
31
32
33
from djangocms_text_ckeditor.fields import HTMLField

from shop.money.fields import MoneyField
from shop.models.product import BaseProduct, BaseProductManager, CMSPageReferenceMixin
from shop.models.defaults.mapping import ProductPage, ProductImage
from shop.models.defaults.order import Order

from ..manufacturer import Manufacturer
class SmartCard(CMSPageReferenceMixin, BaseProduct):
    # common product fields
    product_name = models.CharField(
        _("Product Name"),
        max_length=255
    )

    slug = models.SlugField(_("Slug"))

    unit_price = MoneyField(
        _("Unit price"),
        decimal_places=3,
        help_text=_("Net price for this product"),
    )

    caption = HTMLField(
        _("Caption"),
        configuration='CKEDITOR_SETTINGS_CAPTION',
        help_text=_("Short description used in the catalog's list view of products."),
    )

    images = models.ManyToManyField(
        'filer.Image',
        through=ProductImage,
    )

Here our model SmartCard inherits directly from BaseProduct, which is a stub class, hence the most common fields, such as product_name, slug and unit_price must be added to our product here. Later on we will see why these fields, even though required by each product, can not be part of our abstract model BaseProduct.

Additionally a smart card has some product specific properties:

 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
30
31
    # product properties
    manufacturer = models.ForeignKey(
        Manufacturer,
        verbose_name=_("Manufacturer")
    )

    card_type = models.CharField(
        _("Card Type"),
        choices=(2 * ('{}{}'.format(s, t),)
                 for t in ('SD', 'SDXC', 'SDHC', 'SDHC II') for s in ('', 'micro ')),
        max_length=15,
    )

    speed = models.CharField(
        _("Transfer Speed"),
        choices=((str(s), "{} MB/s".format(s))
                 for s in (4, 20, 30, 40, 48, 80, 95, 280)),
        max_length=8,
    )

    product_code = models.CharField(
        _("Product code"),
        max_length=255,
        unique=True,
    )

    storage = models.PositiveIntegerField(
        _("Storage Capacity"),
        help_text=_("Storage capacity in GB"),
    )

these class attributes depend heavily on the data sheet of the product to sell.

Finally we also want to position our products into categories and sort them:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
    # controlling the catalog
    order = models.PositiveIntegerField(
        _("Sort by"),
        db_index=True,
    )

    cms_pages = models.ManyToManyField(
        'cms.Page',
        through=ProductPage,
        help_text=_("Choose list view this product shall appear on."),
    )

The field order is used to keep track on the sequence of our products while rendering a list view.

The field cms_pages specifies on which pages of the CMS a product shall appear.

Note

If categories do not require to keep any technical properties, it often is completely sufficient to use CMS pages as their surrogates.

Finally images is another many-to-many relation, allowing to associate none, one or more images to a product.

Both fields cms_pages and images must use the through parameter. This is because we have two many-to-many mapping tables which are part of the merchant’s project rather than the django-SHOP application. The first of those mapping tables has foreign keys onto the models cms.Page and myshop.SmartCard. The second table has foreign keys onto the models filer.Image and myshop.SmartCard again. Since the model myshop.SmartCard has been declared by the merchant himself, he also is responsible for managing those many-to-many mapping tables.

Additionally each product model requires these attributes:

  • A model field or property method named product_name: It must returns the product’s name in its natural language.
  • A method get_price(request): Returns the product price. This can depend on the given region, which is available through the request object.
  • A method get_absolute_url(): Returns the canonical URL of a product.
  • The object attribute must be of type BaseProductManager or derived from thereof.

These product model attributes are optional, but highly recommended:

  • A model field or property method named product_code: It shall returns a language independent product code or article number.
  • A property method sample_image: It shall returns a sample image for the given product.

3.1. Add Model myshop.SmartCard to Django Admin

For reasons just explained, it is the responsibility of the project to manage the many-to-many relations between its CMS pages and the images on one side, and the product on the other side. Therefore we can’t use the built-in admin widget FilteredSelectMultiple for these relations.

Instead django-SHOP is shipped with a special mixin class CMSPageAsCategoryMixin, which handles the relation between CMS pages and the product. This however implies that the field used to specify this relation is named cms_pages.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
from adminsortable2.admin import SortableAdminMixin
from shop.admin.product import CMSPageAsCategoryMixin, ProductImageInline
from myshop.models import SmartCard


@admin.register(SmartCard)
class SmartCardAdmin(SortableAdminMixin, CMSPageAsCategoryMixin, admin.ModelAdmin):
    fieldsets = (
        (None, {
            'fields': ('product_name', 'slug', 'product_code', 'unit_price', 'active', 'description',),
        }),
        (_("Properties"), {
            'fields': ('manufacturer', 'storage', 'card_type', 'speed',)
        }),
    )
    inlines = (ProductImageInline,)
    prepopulated_fields = {'slug': ('product_name',)}
    list_display = ('product_name', 'product_code', 'unit_price', 'active',)
    search_fields = ('product_name',)

For images, the admin class must use a special inline class named ProductImageInline. This is because the merchant might want to arrange the order of the images and therefore a simple SelectMultiple widget won’t do this job here.

Extend our simple product to support other natural languages by Modeling a Multilingual Product.