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.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:
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.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'
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.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 commandpython manage.py createsuperuser
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'
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: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:
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.
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
andmodels.py
, because these are not required for the Oscar's dashboard app.tests.py
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.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.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.There are four different views required to deploy a custom Oscar dashboard. These are:
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 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.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
.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)
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.
For templates let’s first create a directory
/src/templates/dashboard
. In this directory, we must implement three-view templates and one message template: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: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:
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
Previously published at https://mmtechslv.com/tutorials/django-oscar-new-app-part-1/