A Guide On Building a Django-Oscar Application with a Dashboard

Written by mmtechslv | Published 2021/03/25
Tech Story Tags: django | django-oscar | dashboard | ecommerce-web-development | web-development | programming | software-development | backend | web-monetization

TLDRvia the TL;DR App

In this tutorial, you are going to learn how to create a new Django app and integrate it into the Oscar e-commerce framework. Particularly, we will create a new sample Django app called 
boutique
 and integrate it to Oscar's default front and dashboard.

Getting ready (Django-Oscar)

First, it is necessary to create a virtual environment to work in. I use pipenv as a virtual environment for its simplicity and ease of use. Create a directory called 
/myoscarapp
, move inside and run the following command:
$ pipenv shell
Then install the django-oscar using pip:
$ pip install django-oscar[sorl-thumbnail]
Now create a brand new Dango project using the following command and rename the created directory to 
src
 for convenience:
$ django-admin startproject myoscarproject
$ mv myoscarproject src
Next, configure Django 
settings.py
 and 
urls.py
 as described in Oscar's corresponding docs. Run 
makemigrations
 and migrate:
$ python manage.py makemigrations
$ python manage.py migrate
Test the website:
$ python manage.py runserver
The following screen should be now available:

Creating “boutique” app for Django-Oscar

The new app is created as usual using the following command:
$ python manage.py startapp boutique
Once again as usual after the app is created, it is necessary to register the app in 
INSTALLED_APPS
 in 
settings.py
 as shown below:
INSTALLED_APPS = [
        ...
        'boutique.apps.BoutiqueConfig',
    ]
Similarly, your 
urls.py
 should look like this:
from django.apps import apps
from django.urls import include, path
from django.contrib import admin
urlpatterns = [
    path('i18n/', include('django.conf.urls.i18n')),
    path('admin/', admin.site.urls),
    #path('dashboard/boutique/', apps.get_app_config('boutique_dashboard').urls),
    path('boutique/', apps.get_app_config('boutique').urls),
    path('', include(apps.get_app_config('oscar').urls[0])),
]
In the code above, line with 
boutique_dashboard
 URL configuration is temporarily commented out and will be turned on when Oscar's dashboard app is forked.

Models for “boutique” app

Create the following model that will represent a single boutique with three fields.
from django.db import models
class Boutique(models.Model):
    name = models.CharField(max_length=255, blank=True, null=True)
    manager = models.CharField(max_length=150, blank=True, null=True)
    city = models.CharField(max_length=150, blank=True, null=True)
class Meta:
        app_label = 'boutique'

App configs for “boutique” app

While the usual Django app’s config Class in 
apps.py
 inherits Django's default
django.apps.AppConfig
class, Oscar app's must inherit
oscar.core.application.OscarConfig
 instead. Your 
apps.py
should look like this:
from oscar.core.application import OscarConfig
from django.urls import path, re_path
from oscar.core.loading import get_class
class BoutiqueConfig(OscarConfig):
    name = 'boutique'
    namespace = 'boutique'
def ready(self):
        super().ready()
        self.boutique_list_view = get_class(
            'boutique.views', 'BoutiqueListView')
        self.boutique_detail_view = get_class(
            'boutique.views', 'BoutiqueDetailView')
def get_urls(self):
        urls = super().get_urls()
        urls += [
            path('', self.boutique_list_view.as_view(), name='index'),
            re_path(r'^view/(?P<pk>\d+)/$',
                    self.boutique_detail_view.as_view(), name='details'),
        ]
        return self.post_process_urls(urls)
It is optional to use 
get_class
 and 
get_model
 when developing your own app but required when overriding Oscar apps. However, I prefer using Oscar's approach in all cases as I previously encountered various errors when importing modules using 
import
 statement.

Admin for “boutique” app

This step is optional and Oscar’s dashboard is sufficient to add, modify and remove 
Boutique
 elements to the database. However, for early testing let's register our model in Django's admin. Add the following code to the
admin.py
 in the app's directory.
from django.contrib import admin
from oscar.core.loading import get_model
Boutique = get_model('boutique', 'Boutique')
class BoutiqueAdmin(admin.ModelAdmin):
    pass
admin.site.register(Boutique, BoutiqueAdmin)
Now that the model is registered in Django’s admin, go on and add few items for testing.
To access Django’s admin you will need to create a super user using command 
python manage.py createsuperuser

Views for “boutique” app

There is nothing special in the implementation of views that will deliver context to the front pages. Following is a working 
views.py
 based on Django's generic class-based views.
from django.views import generic
from oscar.core.loading import get_model
Boutique = get_model('boutique', 'Boutique')
class BoutiqueListView(generic.ListView):
    model = Boutique
    template_name = 'boutique/boutique_list.html'
    context_object_name = 'boutique_list'
class BoutiqueDetailView(generic.DetailView):
    model = Boutique
    template_name = 'boutique/boutique_details.html'
    context_object_name = 'boutique'

Front-end templates for “boutique” views

First and foremost, let’s override Oscar’s navigation template by adding a URL to our 
BoutiqueListView
. First, create a directory called 
oscar
 in
/src/templates
 directory. Any template file with the same relative path Oscar's templates from source code will be overridden by Oscar and become a higher priority template. Because Oscar is developed in a very smart and customizable way, it is very easy to add an element to the original Oscar template navigation. The original template HTML file from Oscar's source code can be found in 
/templates/oscar/partials/nav_primary.html
. Accordingly, we need to create a file
oscar/partials/nav_primary.html
 that will contain the following code:
{% extends "oscar/partials/nav_primary.html" %}
{% load i18n %}
{% block nav_items %}
{{ block.super }}
<li class="nav-item dropdown">
    <a class="nav-link" href="#" role="button">
    {% trans "Boutiques" %}
    </a>
</li>
{% endblock %}
In the code above, we first extend the original Oscar’s template. Then we override the block 
nav_items
 by adding new elements to Oscar's default front-end navigation. After restarting the server, the following front should show up:

Template for list of boutiques

Previously we created a view 
BoutiqueListView
, which is responsible for delivering the context with a list of Boutique instances to the template
boutique/boutique_list.html
. Therefore, we first create an HTML file
/src/templates/boutique/boutique_list.html
. Notice that this template file is not placed under 
/src/templates/oscar
 the directory. This is because we do not override Oscar's template and merely creating a new custom template. However, in our case, it does extend the default Oscar layout template as shown:
{% extends "oscar/layout.html" %}
{% load i18n %}
{% load product_tags %}
{% block title %}
{% trans "Boutiques" %} | {{ block.super }}
{% endblock %}
{% block breadcrumbs %}
    <nav aria-label="breadcrumb">
        <ol class="breadcrumb">
            <li class="breadcrumb-item">
                <a href="{{ homepage_url }}">{% trans "Home" %}</a>
            </li>
            <li class="breadcrumb-item active" aria-current="page">{% trans "Boutiques" %}</li>
        </ol>
    </nav>
{% endblock %}
{% block headertext %}
    {% trans "Boutique" %}
{% endblock %}
{% block content %}
    {% if not boutique_list %}
        <p>{% trans "There are no boutique at the moment." %}</p>
    {% else %}
        {% for boutique in boutique_list %}
        <p>
          <h2><a href="{% url 'boutique:details' boutique.pk %}">{{ boutique.name }}</a></h2>
          The boutique is in: {{ boutique.city }}
        </p> <hr/>
        {% endfor %}
    {% endif %}
{% endblock content %}
The result should look like this:

Template for boutique details

Now that we have a page with a list of our boutique elements let’s add a page where users can view details of any given boutique. Similarly to the listing template, let’s create a new HTML file 
/src/templates/boutique/boutique_details.html
 with the following code:
{% extends "oscar/layout.html" %}
{% load i18n %}
{% load product_tags %}
{% block title %}
{% trans "Boutiques" %} | {{ block.super }}
{% endblock %}
{% block breadcrumbs %}
    <nav aria-label="breadcrumb">
        <ol class="breadcrumb">
            <li class="breadcrumb-item">
                <a href="{{ homepage_url }}">{% trans "Home" %}</a>
            </li>
            <li class="breadcrumb-item" aria-current="page">
              <a href="{% url 'boutique:index' %}">{% trans "Boutiques" %}</a>
            </li>
            <li class="breadcrumb-item active" aria-current="page">{{ boutique.name }}</li>
        </ol>
    </nav>
{% endblock %}
{% block headertext %}
    {% trans "Boutique" %}
{% endblock %}
{% block content %}
    <p>
      <h2>{{ boutique.name }}</h2> <br>
      The boutique is in: {{ boutique.city }} <br>
      The boutique's manager is Mr/Mrs: <strong>{{ boutique.manager }} </strong>
    </p>
{% endblock content %}
The result should look like this:
At this point the app’s model, configs, and front-end templates are ready. Now we can move on to develop an Oscar dashboard for the boutique app.

Creating a Django-Oscar dashboard app to manage boutiques

Let’s create a new app called dashboard inside the boutique app directory:
$ mkdir boutique/dashboard
Then initialize a new Django app using the following command:
$ python manage.py startapp dashboard boutique/dashboard
You can delete 
admin.py
models.py
 and 
tests.py
, because these are not required for the Oscar's dashboard app.
Once again after the dashboard app is created, it is necessary to register the app in 
INSTALLED_APPS
 in 
settings.py
 as shown below:
INSTALLED_APPS = [
    ...
    'boutique.dashboard.apps.DashboardConfig',
]
If you run the server at this moment it will not work as you need to first complete the app configurations.
In the first part, we had a commented-out line in our
myoscarproject/urls.py
. Now that the dashboard app is created we need to uncomment it as shown below:
from django.apps import apps
from django.urls import include, path
from django.contrib import admin
urlpatterns = [
    ...
    path('dashboard/boutique/', apps.get_app_config('boutique_dashboard').urls),
    ...
]
However, at this point label, 
boutique_dashboard
 is not associated with any configuration. Therefore, let's move on and create the Boutique Dashboard Oscar app config.

App configs for the Boutique’s dashboard

Configuration for boutique dashboard app is similar to configs from the first part of this tutorial. With few additions as shown below:
from django.urls import path
from oscar.core.application import OscarDashboardConfig
from oscar.core.loading import get_class
class DashboardConfig(OscarDashboardConfig):
    name = 'boutique.dashboard'
    label = 'boutique_dashboard'
    namespace = 'boutique-dashboard'
    default_permissions = ['is_staff']
    def ready(self):
        self.boutique_list_view = get_class(
            'boutique.dashboard.views', 'DashboardBoutiqueListView')
        self.boutique_create_view = get_class(
            'boutique.dashboard.views', 'DashboardBoutiqueCreateView')
        self.boutique_update_view = get_class(
            'boutique.dashboard.views', 'DashboardBoutiqueUpdateView')
        self.boutique_delete_view = get_class(
            'boutique.dashboard.views', 'DashboardBoutiqueDeleteView')
    def get_urls(self):
        urls = [
            path('', self.boutique_list_view.as_view(), name='boutique-list'),
            path('create/', self.boutique_create_view.as_view(),
                 name='boutique-create'),
            path('update/<int:pk>/', self.boutique_update_view.as_view(),
                 name='boutique-update'),
            path('delete/<int:pk>/', self.boutique_delete_view.as_view(),
                 name='boutique-delete'),
        ]
        return self.post_process_urls(urls)
One important point in this configuration is to change the 
label
parameter. The Django Oscar's default dashboard app conflicts with
DashboardConfig
the Boutique dashboard app. Django's documentation state that:
AppConfig.label defaults to the last component of name.
Therefore, it is necessary to choose a different label
boutique_dashboard
 in order to "tell" Django that this dashboard app is different from Oscar's built-in dashboard app.
Another difference between dashboard app config from primary boutique app config is the 
default_permissions
 parameter. This parameter sets Oscar's dashboard permissions for this dashboard app. Since the Oscar has multiple user permission levels like one that has Fulfilment Parters, setting this parameter 
is_staff
 disables access to this dashboard for any user except c users like super-users.

Forms for the Boutique’s dashboard app

First, it is necessary to create forms for your custom dashboard app. Create a 
forms.py
 file in 
boutique/dashboard
 directory and add the following code:
from django import forms
from django.db.models import Q
from django.utils.translation import gettext_lazy as _
from oscar.core.loading import get_model
Boutique = get_model('boutique', 'Boutique')
class DashboardBoutiqueSearchForm(forms.Form):
    name = forms.CharField(label=_('Boutique name'), required=False)
    city = forms.CharField(label=_('City'), required=False)
    def is_empty(self):
        d = getattr(self, 'cleaned_data', {})
        def empty(key): return not d.get(key, None)
        return empty('name') and empty('city')
    def apply_city_filter(self, qs, value):
        words = value.replace(',', ' ').split()
        q = [Q(city__icontains=word) for word in words]
        return qs.filter(*q)
    def apply_name_filter(self, qs, value):
        return qs.filter(name__icontains=value)
    def apply_filters(self, qs):
        for key, value in self.cleaned_data.items():
            if value:
                qs = getattr(self, 'apply_%s_filter' % key)(qs, value)
        return qs
class DashboardBoutiqueCreateUpdateForm(forms.ModelForm):
    class Meta:
        model = Boutique
        fields = ('name', 'manager', 'city')
In the code above 
DashboardBoutiqueSearchForm
 is a form to filter Boutique instances in the dashboard. We design our form so that it can filter by model's 
city
 and 
name
 fields. The form
DashboardBoutiqueCreateUpdateForm
 is the create and update form required to create or edit a boutique instance. This form inherits Django's default 
forms.ModelForm
 so it is relatively simple to make it work.

Views for the Boutique’s dashboard app

There are four different views required to deploy a custom Oscar dashboard. These are:
  • View to list boutique instances DashboardBoutiqueListView
  • View to create a new boutique instance DashboardBoutiqueCreateView
  • View to update/edit a boutique instance DashboardBoutiqueUpdateView
  • View to delete a boutique instance DashboardBoutiqueDeleteView
Prior to moving on to the views add the following code to the head of a
views.py
 the file of the boutique's dashboard app:
from django.contrib import messages
from django.template.loader import render_to_string
from django.urls import reverse_lazy
from django.utils.translation import gettext
from django.utils.translation import gettext_lazy as _
from django.views import generic
from oscar.core.loading import get_class, get_model
Boutique = get_model('boutique', 'Boutique')
BoutiqueCreateUpdateForm = get_class(
    'boutique.dashboard.forms', 'DashboardBoutiqueCreateUpdateForm')
DashboardBoutiqueSearchForm = get_class(
    'boutique.dashboard.forms', 'DashboardBoutiqueSearchForm')

Listing Boutique instances in the dashboard

Listing boutique instances in a custom dashboard app is no different than any other Django app. The list view inherits Django’s
generic.ListView
as shown in the following code:
class DashboardBoutiqueListView(generic.ListView):
    model = Boutique
    template_name = "dashboard/boutique/boutique_list.html"
    context_object_name = "boutique_list"
    paginate_by = 20
    filterform_class = DashboardBoutiqueSearchForm
    def get_title(self):
        data = getattr(self.filterform, 'cleaned_data', {})
        name = data.get('name', None)
        city = data.get('city', None)
        if name and not city:
            return gettext('Boutiques matching "%s"') % (name)
        elif name and city:
            return gettext('Boutiques matching "%s" near "%s"') % (name, city)
        elif city:
            return gettext('Boutiques near "%s"') % (city)
        else:
            return gettext('Boutiques')
    def get_context_data(self, **kwargs):
        data = super().get_context_data(**kwargs)
        data['filterform'] = self.filterform
        data['queryset_description'] = self.get_title()
        return data
    def get_queryset(self):
        qs = self.model.objects.all()
        self.filterform = self.filterform_class(self.request.GET)
        if self.filterform.is_valid():
            qs = self.filterform.apply_filters(qs)
        return qs
The only non-trivial part of the code above is the additional parameter,
filterform_class
, which is essentially a parameter that is recognized and processed by Oscar's templates.

Creating Boutique instances in the dashboard

Similarly, the view responsible for creating the boutique instances inherits
generic.CreateView
 and is shown in the following code:
class DashboardBoutiqueCreateView(generic.CreateView):
    model = Boutique
    template_name = 'dashboard/boutique/boutique_update.html'
    form_class = BoutiqueCreateUpdateForm
    success_url = reverse_lazy('boutique-dashboard:boutique-list')
    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ctx['title'] = _('Create new boutique')
        return ctx
    def forms_invalid(self, form, inlines):
        messages.error(
            self.request,
            "Your submitted data was not valid - please correct the below errors")
        return super().forms_invalid(form, inlines)
    def forms_valid(self, form, inlines):
        response = super().forms_valid(form, inlines)
        msg = render_to_string('dashboard/boutique/messages/boutique_saved.html',
                               {'boutique': self.object})
        messages.success(self.request, msg, extra_tags='safe')
        return response
In the code above, the parameter
success_url
is assigned to
reverse_lazy
and not reverse because the URL will be evaluated lazily(or when required). Moreover, Oscar uses Django's built-in messages framework to pass success and fail messages to the templates. The messages are handled in corresponding methods
forms_invalid
and
forms_valid
.

Updating Boutique instances in the dashboard

View for updating Boutique instance is very similar to create a view and uses the same template but inherits
generic.UpdateView
 instead.
class DashboardBoutiqueUpdateView(generic.UpdateView):
    model = Boutique
    template_name = "dashboard/boutique/boutique_update.html"
    form_class = BoutiqueCreateUpdateForm
    success_url = reverse_lazy('boutique-dashboard:boutique-list')
    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ctx['title'] = self.object.name
        return ctx
    def forms_invalid(self, form, inlines):
        messages.error(
            self.request,
            "Your submitted data was not valid - please correct the below errors")
        return super().forms_invalid(form, inlines)
    def forms_valid(self, form, inlines):
        msg = render_to_string('dashboard/boutique/messages/boutique_saved.html',
                               {'boutique': self.object})
        messages.success(self.request, msg, extrforms_valida_tags='safe')
        return super().forms_valid(form, inlines)

Deleting Boutique instances from the dashboard

Delete view is rather simple compared to others and it inherits Django’s
generic.DeleteView
 as shown below:
class DashboardBoutiqueDeleteView(generic.DeleteView):
    model = Boutique
    template_name = "dashboard/boutique/boutique_delete.html"
    success_url = reverse_lazy('boutique-dashboard:boutique-list')
Finally, now that views are completed we can move on to templates.

Templates for the Boutique’s dashboard app

For templates let’s first create a directory 
/src/templates/dashboard
. In this directory, we must implement three-view templates and one message template:
  • Template for list view: /dashboard/boutique/boutique_list.html
  • Template for update view: /dashboard/boutique/boutique_update.html
  • Template for delete view: /dashboard/boutique/boutique_delete.html
  • Message template: /dashboard/boutique/messages/boutique_saved.html
Templates are implemented the same way as was described in the first part of this tutorial except that these templates must extend different base layout,
{% extends 'oscar/dashboard/layout.html' %}
. Since templates are long you can find them in the Git repository of this tutorial. After templates are ready the following screen will be available when you go to http://127.0.0.1:8000/dashboard/boutique/ URL:

Adding “Boutiques” navigation item to Django-Oscar’s dashboard

Finally, after Boutiques are ready we need to add a navigation item to Oscar’s dashboard navigation. Luckily, Django-Oscar provides a very easy way to do this. You need to add the following code to the settings but make sure that it comes after importing Oscar’s defaults:
from django.utils.translation import gettext_lazy as _
... # Django's Other Settings
from oscar.defaults import *
OSCAR_DASHBOARD_NAVIGATION.append({
    'label': _('Boutiques'),
    'icon': 'fas fa-store',
    'url_name': 'boutique-dashboard:boutique-list',
})
Once the navigation item is added you will get the following screen when entering your Oscar Dashboard:

Conclusion

At the end of this tutorial, you should be able to create a brand new Django Oscar app with a working dashboard and everything. I hope this tutorial was helpful for the reader and made one’s life easier while learning such an amazing e-commence framework like Django-Oscar.
Source code of this tutorial can be found in my Git repository here

Written by mmtechslv | Coder with passion for bioinformatics and health informatics.
Published by HackerNoon on 2021/03/25