Blog Entry 10 years, 10 months ago

Create new content block (promotion) in Django Oscar E-commerce

Standard (or stock) Oscar only provides 6 types of content blocks: Raw HTML, Image, Single Product, Automatic Product List, Hand Picked Product List, Multi Image. Those, in most case, well afford all normal needs of page content creation. But if you need a new special one, with unique styling, you should create a new content block type.

Inside Django Oscar dashboard, there is a part for managing content which contains:

  •     Content blocks
  •     Content blocks by page
  •     Pages
  •     Email template
  •     Reviews

Within entry scope, we only touch the first 3 items. Page actually is Flatpage object, it contains basic fields like: title, url, and content. For more information about Flatpage, visit Django Flatpages. Content block, like its name, is single block of content inside given page. It could be Raw HTML, Linked Image, Image,... 

The third item "Content blocks by page" plays role of linkage between Pages and Content Blocks. What block(s) will stay in what page(s), how they will be positioned.

Standard (or stock) Oscar only provides 6 types of content blocks: Raw HTML, Image, Single Product, Automatic Product List, Hand Picked Product List, Multi Image

Those, in most case, well afford all normal needs of page content creation. But if you need a new special one, with unique styling, you should create a new content block type. 

Note:Inside Oscar, another name of content block is Promotion. And content blocks are objects of models inside oscar.apps.promotions.models.

Steps for making new Promotion/Content Block are:

1. Create new application for deriving oscar.apps.promotions modules:

        __init__.py
        admin.py
        models.py
        conf.py 

2. Inside models.py, we have something like this:

        from django.db import models
        from django.conf import settings
        from django.utils.translation import ugettext_lazy as _
        from oscar.apps.promotions.models import AbstractPromotion
        from oscar.models.fields import ExtendedURLField

        class NewsPromotion(AbstractPromotion):
            """
            Model for news, events, press, or competitions object.
            """
            _type = 'News'
            name = models.CharField(_("Name"), max_length=128)
            description = models.TextField(_("Description"))
            link_url = ExtendedURLField(
                _('Link URL'), blank=True,
                help_text=_('This is where this promotion links to'))
            image = models.ImageField(
                _('Image'), upload_to=settings.OSCAR_PROMOTION_FOLDER,
                max_length=255)
            date_created = models.DateTimeField(auto_now_add=True)

            def __unicode__(self):
                return self.name

            class Meta:
                verbose_name = _("News")
                verbose_name_plural = _("News")

        This looks just like the Image Promotion, but having new description field for additional information

3. admin.py is like other admin.py module, for registering NewsPromotion to be shown in admin section.

4. Inside conf.py, we override the PROMOTION_CLASSES, add new type(s) there:

        from models import NewsPromotion
        from oscar.apps.promotions.conf import *

        def get_promotion_classes():
            return (RawHTML, Image, NewsPromotion, SingleProduct,
                    AutomaticProductList, HandPickedProductList, MultiImage)

        PROMOTION_CLASSES = get_promotion_classes()

5. Now, add the new app into project setting file, inside the INSTALLED_APPS definition. Make sure it stands above the Oscar apps registration:

        INSTALLED_APPS = [
            ...
            'new_promotions',
            'new_dashboard',
            ...
        ]
        from oscar import get_core_apps
        INSTALLED_APPS = INSTALLED_APPS + get_core_apps()

Have you notified the "new_dashboard"? We will have to customize it in order to have new content block shown inside dashboard.

6. Create new application for deriving oscar.apps.dashboard, we name it 'new_dashboard' for example:

        __init__.py
        app.py
        promotions/__init__.py
        promotions/app.py
        promotions/forms.py
        promotions/views.py

7. Because we have new view for NewsPromotion, we must override the app.py module of oscar.apps.dashboard. Short, and simple:

        from oscar.apps.dashboard.app import DashboardApplication as \
            CoreDashboardApp

        from promotions.app import application

        class DashboardApplication(CoreDashboardApp):
            promotions_app = application

        application = DashboardApplication()

8. We also have to override the nested application "oscars.apps.dashboard.promotions". Its app.py is a bit longer for some altered configurations (here, it used the new PROMOTION_CLASSES overrided above)

        from django.conf.urls import patterns, url

        from oscar.apps.dashboard.promotions.app import PromotionsDashboardApplication \
            as CorePromotionsDashboardApplication
        from apps.dashboard.promotions import views
        from apps.promotions.conf import PROMOTION_CLASSES

        class PromotionsDashboardApplication(CorePromotionsDashboardApplication):

            list_view = views.ListView
            page_list = views.PageListView
            page_detail = views.PageDetailView
            create_redirect_view = views.CreateRedirectView
            delete_page_promotion_view = views.DeletePagePromotionView

            for klass in PROMOTION_CLASSES:
                locals()['create_%s_view' % klass.classname()] \
                    = getattr(views, 'Create%sView' % klass.__name__)
                locals()['update_%s_view' % klass.classname()] \
                    = getattr(views, 'Update%sView' % klass.__name__)
                locals()['delete_%s_view' % klass.classname()] \
                    = getattr(views, 'Delete%sView' % klass.__name__)

            def get_urls(self):
                urls = [
                    url(r'^$', self.list_view.as_view(), name='promotion-list'),
                    url(r'^pages/$', self.page_list.as_view(),
                        name='promotion-list-by-page'),
                    url(r'^page/(?P<path>/([\w-]+(/[\w-]+)*/)?)$',
                        self.page_detail.as_view(), name='promotion-list-by-url'),
                    url(r'^create/$',
                        self.create_redirect_view.as_view(),
                        name='promotion-create-redirect'),
                    url(r'^page-promotion/(?P<pk>\d+)/$',
                        self.delete_page_promotion_view.as_view(),
                        name='pagepromotion-delete')]

                for klass in PROMOTION_CLASSES:
                    code = klass.classname()
                    urls += [
                        url(r'create/%s/' % code,
                            getattr(self, 'create_%s_view' % code).as_view(),
                            name='promotion-create-%s' % code),
                        url(r'^update/(?P<ptype>%s)/(?P<pk>\d+)/$' % code,
                            getattr(self, 'update_%s_view' % code).as_view(),
                            name='promotion-update'),
                        url(r'^delete/(?P<ptype>%s)/(?P<pk>\d+)/$' % code,
                            getattr(self, 'delete_%s_view' % code).as_view(),
                            name='promotion-delete')]

                return self.post_process_urls(patterns('', *urls))

        application = PromotionsDashboardApplication()

9. Also the forms.py, where new PROMOTION_CLASSES is used:

        from django.utils.translation import ugettext_lazy as _
        from oscar.apps.dashboard.promotions.forms import *
        from apps.promotions.conf import PROMOTION_CLASSES

        class PromotionTypeSelectForm(forms.Form):
            choices = []
            for klass in PROMOTION_CLASSES:
                choices.append((klass.classname(), klass._meta.verbose_name))
                promotion_type = forms.ChoiceField(choices=tuple(choices),
                                                   label=_("Promotion type"))

10. Finally with promotions app, views.py with new views for NewsPromotions:

        from oscar.apps.dashboard.promotions.views import *
        from apps.promotions.conf import PROMOTION_CLASSES
        from apps.promotions.models import NewsPromotion

        from forms import PromotionTypeSelectForm as SelectForm

        class ListView(generic.TemplateView):
            template_name = 'dashboard/promotions/promotion_list.html'

            def get_context_data(self):
                # Need to load all promotions of all types and chain them together
                # no pagination required for now.
                data = []
                num_promotions = 0
                for klass in PROMOTION_CLASSES:
                    objects = klass.objects.all()
                    num_promotions += objects.count()
                    data.append(objects)
                promotions = itertools.chain(*data)
                ctx = {
                    'num_promotions': num_promotions,
                    'promotions': promotions,
                    'select_form': SelectForm(),
                }
                return ctx

        class CreateRedirectView(generic.RedirectView):
            permanent = True

            def get_redirect_url(self, **kwargs):
                code = self.request.GET.get('promotion_type', None)
                urls = {}
                for klass in PROMOTION_CLASSES:
                    urls[klass.classname()] = reverse('dashboard:promotion-create-%s' %
                                                      klass.classname())
                return urls.get(code, None)

        class CreateNewsPromotionView(CreateView):
            model = NewsPromotion

        class UpdateNewsPromotionView(UpdateView):
            model = NewsPromotion

        class DeleteNewsPromotionView(DeleteView):
            model = NewsPromotion

11. Don't forget to add "new_dashboard" into INSTALLED_APPS inside settings file

12. Almost last, a new template with the same name, 'newspromotion.html', must be present in templates path (here is "promotions/newspromotion.html"):

        {% load staticfiles %}

        <div class="promotion-news">
            {% if promotion.image %}
                {% if promotion.link_url %}
                    <div class="promotion-block widget-image-link">
                        <a href="{{ promotion.link_url }}">
                            <img src="{% static promotion.image.url %}" alt="{{ promotion.name }}" />
                            {% comment %}
                            <h3>{{ promotion.name }} <i class="icon-arrow-right hidden-phone"></i></h3>
                            {% endcomment %}
                        </a>
                    </div>
                {% else %}
                    <img src="{% static promotion.image.url %}" title="{{ promotion.name }}" />
                {% endif %}
            {% endif %}
            <div class="news-info">
                <h4>{{ promotion.name }}</h4>
                <div class="promotion-date">{{ promotion.date_created|date:"F d, Y" }}</div>
                <div class="promotion-description">{{ promotion.description|safe }}</div>
            </div>
        </div>

13. Do the sync or migrate for making new model active.

 

Recent Reads