Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for custom Product models #57

Closed
wants to merge 16 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions cartridge/shop/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,14 @@

from django.contrib import admin
from django.db.models import ImageField
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.utils.translation import ugettext_lazy as _

from mezzanine.conf import settings
from mezzanine.core.admin import DisplayableAdmin, TabularDynamicInlineAdmin
from mezzanine.pages.admin import PageAdmin
from mezzanine.utils.urls import admin_url

from cartridge.shop.fields import MoneyField
from cartridge.shop.forms import ProductAdminForm, ProductVariationAdminForm
Expand Down Expand Up @@ -156,13 +159,55 @@ class Media:
form = ProductAdminForm
fieldsets = product_fieldsets

def __init__(self, *args, **kwargs):
"""
For ``Product`` subclasses that are registered with an Admin class
that doesn't implement fieldsets, add any extra model fields
to this instance's fieldsets. This mimics Django's behaviour of
adding all model fields when no fieldsets are defined on the
Admin class.
"""

super(ProductAdmin, self).__init__(*args, **kwargs)

# Test that the fieldsets don't differ from ProductAdmin's.
if (self.model is not Product and
self.fieldsets == ProductAdmin.fieldsets):

# Make a copy so that we aren't modifying other Admin
# classes' fieldsets.
self.fieldsets = deepcopy(self.fieldsets)

# Insert each field between the publishing fields and nav
# fields. Do so in reverse order to retain the order of
# the model's fields.
for field in reversed(self.model._meta.fields):
check_fields = [f.name for f in Product._meta.fields]
check_fields.append("product_ptr")
try:
check_fields.extend(self.exclude)
except (AttributeError, TypeError):
pass
try:
check_fields.extend(self.form.Meta.exclude)
except (AttributeError, TypeError):
pass
if field.name not in check_fields and field.editable:
self.fieldsets[0][1]["fields"].insert(3, field.name)

def save_model(self, request, obj, form, change):
"""
Store the product object for creating variations in save_formset.
"""
super(ProductAdmin, self).save_model(request, obj, form, change)
self._product = obj

def in_menu(self):
"""
Hide subclasses from the admin menu.
"""
return self.model is Product

def save_formset(self, request, form, formset, change):
"""

Expand Down Expand Up @@ -229,6 +274,21 @@ def save_formset(self, request, form, formset, change):
# variation to the product.
self._product.copy_default_variation()

def change_view(self, request, object_id, extra_context=None):
"""
As in Mezzanine's ``Page`` model, check ``product.get_content_model()``
for a subclass and redirect to its admin change view.
"""
if self.model is Product:
product = get_object_or_404(Product, pk=object_id)
content_model = product.get_content_model()
if content_model is not None:
change_url = admin_url(content_model.__class__, "change",
content_model.id)
return HttpResponseRedirect(change_url)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's where the redirection happens.

return super(ProductAdmin, self).change_view(request, object_id,
extra_context=extra_context)


class ProductOptionAdmin(admin.ModelAdmin):
ordering = ("type", "name")
Expand Down
38 changes: 35 additions & 3 deletions cartridge/shop/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,27 @@ def copy_price_fields_to(self, obj_to):
obj_to.save()


class Product(Displayable, Priced, RichText, AdminThumbMixin):
class BaseProduct(Displayable):
"""
Exists solely to store ``DisplayableManager`` as the main manager.
If it's defined on ``Product``, a concrete model, then each
``Product`` subclass loses the custom manager.
"""

objects = DisplayableManager()

class Meta:
abstract = True


class Product(BaseProduct, Priced, RichText, AdminThumbMixin):
"""
Container model for a product that stores information common to
all of its variations such as the product's title and description.
"""

content_model = models.CharField(editable=False, max_length=50, null=True)

available = models.BooleanField(_("Available for purchase"),
default=False)
image = CharField(_("Image"), max_length=100, blank=True, null=True)
Expand All @@ -106,14 +121,29 @@ class Product(Displayable, Priced, RichText, AdminThumbMixin):
verbose_name=_("Upsell products"), blank=True)
rating = RatingField(verbose_name=_("Rating"))

objects = DisplayableManager()

admin_thumb_field = "image"

class Meta:
verbose_name = _("Product")
verbose_name_plural = _("Products")

@classmethod
def get_content_models(cls):
"""
Return all ``Product`` subclasses.
"""
is_product_subclass = lambda cls: issubclass(cls, Product)
cmp = lambda a, b: (int(b is Product) - int(a is Product) or
a._meta.verbose_name < b._meta.verbose_name)
return sorted(filter(is_product_subclass, models.get_models()), cmp)

def get_content_model(self):
"""
Provides a generic method of retrieving the instance of the custom
product's model, if there is one.
"""
return getattr(self, self.content_model, None)

def save(self, *args, **kwargs):
"""
Copies the price fields to the default variation when
Expand All @@ -125,6 +155,8 @@ def save(self, *args, **kwargs):
if updating and not settings.SHOP_USE_VARIATIONS:
default = self.variations.get(default=True)
self.copy_price_fields_to(default)
else:
self.content_model = self._meta.object_name.lower()

@models.permalink
def get_absolute_url(self):
Expand Down
26 changes: 26 additions & 0 deletions cartridge/shop/templates/admin/shop/product/change_list.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{% extends "admin/change_list.html" %}
{% load shop_tags pages_tags i18n adminmedia %}

{% block extrahead %}
{{ block.super }}

<script src="{{ STATIC_URL }}mezzanine/js/jquery-ui-1.8.14.custom.min.js"></script>
<script src="{{ STATIC_URL }}mezzanine/js/admin/page_tree.js"></script>
{% endblock %}

{% block object-tools-items %}
<li>
<div id="addlist-primary">
<select class="addlist">
<option value="">{% trans "Add" %} ...</option>
{% models_for_products as product_models %}
{% for model in product_models %}
{% set_model_permissions model %}
{% if model.perms.add %}
<option value="{{ model.add_url }}">{{ model.name }}</option>
{% endif %}
{% endfor %}
</select>
</div>
</li>
{% endblock %}
25 changes: 24 additions & 1 deletion cartridge/shop/templatetags/shop_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@
import locale
import platform

from django import template
from django.core.urlresolvers import NoReverseMatch

from mezzanine import template
from mezzanine.utils.urls import admin_url

from cartridge.shop.models import Product
from cartridge.shop.utils import set_locale


Expand Down Expand Up @@ -79,3 +83,22 @@ def order_totals_text(context):
Text version of order_totals.
"""
return _order_totals(context)


@register.as_tag
def models_for_products(*args):
"""
Create a select list containing each of the models that subclass the
``Product`` model, plus the ``Product`` model itself.
"""
product_models = []
for model in Product.get_content_models():
try:
admin_add_url = admin_url(model, "add")
except NoReverseMatch:
continue
else:
setattr(model, "name", model._meta.verbose_name)
setattr(model, "add_url", admin_add_url)
product_models.append(model)
return product_models
9 changes: 8 additions & 1 deletion cartridge/shop/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,14 @@ def product(request, slug, template="shop/product.html"):
for_user=request.user),
"add_product_form": add_product_form
}
return render(request, template, context)

templates = []
# Check for a template matching the page's content model.
if product.content_model is not None:
templates.append(u"shop/products/%s.html" % product.content_model)
templates.append(template)

return render(request, templates, context)


@never_cache
Expand Down