Why is a Bad Idea to inherit the Product model in Satchmo (and how to do it right)

I've seen on some articles on the web that one of the ways people is extending the Product model in Satchmo is inheriting from it.

I used it once and it was a Bad Idea, and I will explain why. With a little luck Google will index this and show this to people looking for ways to override Product because currently if one looks at the results it's like that inheriting Product is the only way.

Probably one of the reasons that people do this is because in Django is very common to extend the django.auth User model inheriting it. But this case is different, because:

  • Django doesn't ship with default templates
  • The User model in Django is only used on a few default views where only the User fields have sense.
  • The User mode in Django is not linked to any other Django default models.
  • Inheriting User allows for cleaner implementations when you have lots of different user types and roles; using Profiles you would have to implement it using an intermediate profile which for every type of user links to the real profile.

Inheriting Product is a bad idea in Satchmo because:

  • Satchmo ships with lots of default Templates. Usually most sites will rewrite some of these templates, but...
  • Those templates are generated by lots of default views. Most of those use Product or some object linked with Product, so the templates will receive objects that only have Product fields, even if they were generated using your custom model. So you'll be unable to access any field of your model inside the template, except rewriting the views (and the urls pointing to the views.)
  • There are very important models like Order, OrderItem, CartItem and others linked to the Product model. So you'll have to inherit those two, along with rewriting (again!) the templates and views that use them.
  • You don't gain anything by inheriting Product compared with the Satchmo standard way.

So what is my proposal for extending a Product object? Easy, just use what Satchmo documentation propose.

For example if we want to extend our shop products adding the "code" and "provider" fields we can follow these precise steps:

On your core app models.py, add the extension model (take care to don't call it "Product" or it will clash with Satchmo product model!):

from django.db import models
from django.utils.translation import ugettext_lazy as _

import django.db.models.signals as djangosignals
from product.models import Product
from satchmo_store.contact.models import Organization

class LocalProduct(models.Model):
    product = models.OneToOneField(Product, verbose_name=_('Base Product'))

    code = models.CharField(verbose_name=_('Code'), max_length=32, blank=True, null=True)
    suplier = models.ManyToManyField(Organization, verbose_name=_('Supplier'), blank=True, null=True)

    def _get_subtype(self):  return 'LocalProduct'

    def __unicode__(self):
        if self.code:
            return u"%s [%s]" % (self.product.name, self.code)
        else:
            return u'%s' % self.product.name

    class Admin: pass

    class Meta:
        verbose_name = _('LocalProduct')
        verbose_name_plural = _('LocalProducts')

import config

Add this on your core app config.py (change "localsite" for your app name):

from django.utils.translation import ugettext_lazy as _
from livesettings import config_get

PRODUCT_TYPES = config_get('PRODUCT', 'PRODUCT_TYPES')
PRODUCT_TYPES.add_choice(('localsite::LocalProduct', _('Local Product')))

If you want the LocalProduct to show on the admin automatically linked when you add or edit a product (hint: you do) then add this to you app admin.py, configuring the admin display fields to your taste:

from django.contrib import admin
from product.models import Product
from models import LocalProduct
from livesettings import config_value
from product.admin import ProductAttribute_Inline, Price_Inline, \
    ProductImage_Inline, ProductTranslation_Inline, ProductOptions

class LocalProduct_Inline(admin.StackedInline):
    model = LocalProduct
    extra = 1

class LocalProductOptions(ProductOptions):
    exclude = ('slug', 'sku', 'meta', 'length', 'length_units',
               'width', 'width_units', 'height', 'height_units',
               'weight_units', 'related_items', 'also_purchased',
               'date_added', 'taxable', 'taxClass', 'shipclass',
               'site',)

    fieldsets = (
        (None, {'fields': ('name', 'category', 'description', 
        'short_description', 'weight', 'active', 'featured', 
        'items_in_stock', 'total_sold','ordering',)}),
        )

    list_display = ('name', 'items_in_stock', 'unit_price')
    list_display_links = ('name',)
    search_fields = ['name', 'description', 'short_description']
    inlines = [LocalProduct_Inline, Price_Inline, ProductImage_Inline]
    filter_horizontal = ('category',)

    if config_value('LANGUAGE','SHOW_TRANSLATIONS'):
        inlines.append(ProductTranslation_Inline)

admin.site.unregister(Product)
admin.site.register(Product, LocalProductOptions)

Finally, if you want to have something done when the Product saves, instead of overriding save on an inheriting model save(), use Django signals:

models.py (at the end):

from satchmo_store.shop import get_satchmo_setting, signals
from listeners import *

djangosignals.pre_save.connect(product_pre_save, sender = Product)

listeners.py:

def product_pre_save(sender, instance, **kwargs):
    if instance.pk is None:
        instance.site = Site.objects.get(pk=1)
        instance.taxable = True
        instance.taxClass = TaxClass.objects.get(pk=1)
        instance.date_added = datetime.date.today()
        if not instance.has_full_weight:
            instance.weight = Decimal(0)
            instance.weight_units = 'grs'

And that's it. You have an extended Product without any of the pains. Now if you want to access some of the new fields from a template you just do: product.localproduct.code, without the need to overwrite any Satchmo view or inheriting lots of Product-related objects.





8 comentarios a esta entrada


Hello Juanjo! Nice post, very informative. I'm having some troubles following the instructions: When I run the ./manage.py syncdb I got the error:

SettingNotSet: PRODUCT.PRODUCT_TYPES

Do you know what it could be ?

Thanks.


Mmm, no idea. Didn't happened to me. Are you using the last version from the repository?


I found the error !

My custom product package was named "product" (The same name of satchmo 'product' application). When I changed the package name the problems was solved.

Thanks!


Nice that you found it! I will add a comment about it in the article.


Hey, thanks for the article - really useful, especially the inline stuff. If users want to add a non-custom product, they can just not fill in the custom inline, but is there a way to have a separate form for non-custom products?


Sorry to say that all of your points about why it is a bad idea to inherit the Product model are incorrect.

"Satchmo ships with lots of default Templates. Usually most sites will rewrite some of these templates, but..."

Satchmo's default templates should be no consideration when customizing a store. The default templates aren't even good.

"Those templates are generated by lots of default views. Most of those use Product or some object linked with Product, so the templates will receive objects that only have Product fields, even if they were generated using your custom model. So you'll be unable to access any field of your model inside the template, except rewriting the views (and the urls pointing to the views.)"

The default views in Satchmo are not at all affected by model inheritance. You should study Django a bit more, specifically its handling of related object syntax. The reason inheriting Satchmo's Product model is such a slick solution is that you don't have to do anything to the default views or urls AT ALL. You will modify your templates to display your custom product fields, but not views & urls. If you create a CustomProduct model that subclasses Product, then you access its fields like this:

{{ product.customproduct.attribute }}

Simple. No view modification required. No url modification required. Just template work, which is simple.

"There are very important models like Order, OrderItem, CartItem and others linked to the Product model. So you'll have to inherit those two, along with rewriting (again!) the templates and views that use them."

You're completely wrong here and misleading people altogether. We have a completely custom shop. Every product is a custom product created via model inheritance. Everything related to Order, Cart, OrderItem, CartItem, etc., is stock Satchmo code. We did not have to inherit and rewrite templates, views, and urls for those. This is absolute nonsense. It is because subclassing Product creates a Product instance that you DO NOT have to touch anything related to the cart & order framework.

"You don't gain anything by inheriting Product compared with the Satchmo standard way."

Now, if you want to write a post about how this is your preferred method, that's fine. But don't try to give credence to your preferred way of doing things by erroneously claiming that doing it any other way is wrong and going to require more work. Be honest with your audience.

Your statement of the "Satchmo standard way" doesn't take into account that the way of using ForeignKeys to create custom product models is only the "standard way" because the code was written before Django had robust support for Model inheritance. Trust me that if Satchmo's product app was being written today, it'd be done via Model inheritance.


Hello bob,

I could swear that I tried the product.customproduct.attribute and got an error because it didn't found customproduct on Product class (while I passed a CustomProduct class to the view that added the product to the cart.)

But I could be wrong or could have done something wrong then that made it work bad; I'll try to test again this weekend and if that's the case I'll correct the article (or remove it the article for a placeholder).

My reference to the "standard" way was because at the time I wrote the article it was the way explained on Saatchmo documentation. I think that defining what is documented on the software documentation as standard is honest. What would I gain or care about people using one method or the other? My shop is my shop and I could care less about what peoples preferred way to do it is, this is not religion for god sake (no pun intended.) I just wanted to avoid people losing time like I did.


Hi

I get the below error when I try to do any import from product.admin. e.g. from product.admin import ProductImage_Inline

I go to http://localhost:8000/admin/ and I get this error, I remove that import and everything goes normally!

Caught an exception while rendering: Reverse for 'satchmo_site_settings' with arguments '()' and keyword arguments '{}' not found.

Any idea please?