E-Commerce for Django Developers (with Wagtail Tutorial)

In a rush? Skip to list of tools, Wagtail tutorial or live demo

"D-J-A-N-G-O. The D is silent."

Man, I love that line. So badass.

But the eponymous character from Quentin Tarantino's masterpiece isn't the only badass Django in town.

So is the popular Python framework of the same name.

Today, I'm leaving the realm of JavaScript frameworks for a quick venture into Django e-commerce.

django

In this post, I'll answer legitimate questions you might have when starting a new e-commerce project, such as:

Is Python the right language for my project? And Django the right framework? Which tools or plugins should I use?

Then, I'll show you our homemade recipe for Django-powered e-commerce success with a step-by-step Wagtail CMS tutorial:

  • Creating a new Wagtail site.
  • Adding Snipcart configuration settings.
  • Generating database migrations.
  • Creating new products for your Django store.
  • Crafting an e-commerce template.

Let's start with the basics.

The State of Python

One of the main reasons to pick Django as a framework is its Python foundation.

A general purpose, dynamic programming language, Python was developed by ex-Googler Guido van Rossum in the late 80's. A fan of Monthy Python, he took one-half of the name to baptize his programming project.

monty-python

How can you not be fan?

He wasn't joking though. To say that Python has become "popular" is an understatement.

Today, it's used by hundreds of thousands of developers all over the world. As StackOverflow puts it:

The term “fastest-growing” can be hard to define precisely, but we make the case that Python has a solid claim to being the fastest-growing major programming language.

A few reasons explain the Python love:

  • Its grammatical readability is awesome.
  • It has a fast learning curve for newcomers.
  • It boasts a long-lasting, solid ecosystem of libraries & community
  • It's now the standard language for data science & machine learning.
  • It powers great dev tools like Pelican, a neat static blog generator.
  • Reddit is written in Python. ;)

What about the Django framework?

Django is an open source, high-level Python web framework. Its emphasis on reusable components makes it faster for developers to build web apps on top of Python. It presents itself as a web framework for perfectionists with deadlines.

django-framework

Now maintained by the Django Software Foundation, it was originally written by two brilliant Lawrence Journal-World developers. Oh, and while Python draws its name from comedy icons, Django got his from a versatile guitar legend: Django Reinhardt!

As a full-stack framework, it overshadows pretty much any alternative tool out there. It's fast, fully loaded, secure, scalable & versatile. All characteristics you'll probably want to apply to your e-commerce setup!

Why use Django for e-commerce?

While you can do a lot with Django, let's keep the focus on what it brings to e-commerce and the different tools available to put together an online store.

First, here are some Django features to consider if you're looking for the right framework to build a shop.

Scalability

Django is perfect for e-commerce startups, as it's a good fit for small websites, but also has scales perfectly with business growth. You can rely on Django to handle hundreds/thousands of visitors at a time. Its built with independent components you can unplug or replace depending on your needs at any specific time.

Security

With e-commerce, you want to make sure merchants and clients alike feel safe through your shopping experience. Django prevents a whole lot of common security mistakes which often are what weakens traditional PHP CMSs. For instance, Django hides your site's source code from direct viewing on the web by dynamically generating web pages.

Feature-rich

Compared to most frameworks, Django comes with way more features out-of-the-box. It allows you to build an app right off the bat. Perfect for supporting your online store with functionalities such as user authentification, content management or RSS feed. If something seems to be missing, you can count on Django's community and plugins ecosystem to extend your app!

SEO-friendly

SEO is paramount for any online business. While other frameworks don't natively play well with search engines (mainly JavaScript frameworks, like Vue or React, at least Django advocates best practices for SEO. Human-readable URLs and sitemap features are sure to please any marketing team.

Oh and also, it's fast. Which is always great for both customer experience and SEO.

Reliable

It has been crowd-tested for a while now, and the community surrounding it is widely supportive. It's continuously updated by active developers; maybe you'll even find yourself contributing. ;)

Django e-commerce tools

There are a few noteworthy e-commerce solutions in the Python/Django ecosystem:

  • Oscar — Domain-driven e-commerce for Django, open-source.
  • Saleor — An e-commerce storefront written in Python, open-source.
  • Django-SHOP — A Django based shop system.
  • Shuup — A single and multi-vendor application.

You can explore more options through this extensive list of the best e-commerce packages.

Now let me present you another cool stack for a complete and custom e-commerce setup with Django.

Wagtail CMS + Snipcart e-commerce setup

wagtail-tutorial

Wagtail is a developer-first Django content management system. Free and open source, it was developed by the good-hearted folks at Torchbox. It's elegant, flexible, and, IMHO, kicks ass.

In the following Wagtail tutorial, the CMS will be in charge of creating and managing products that users will then be able to buy through a shopping cart.

By the end of it, you'll have a solid Django-powered e-commerce site up and running.

Let's get practical!

Django e-commerce tutorial with Wagtail CMS

django-wagtail-ecommerce-tutorial

To continue with the movie references and because we're working in Python, I'll craft a Slytherin demo shop! Let's see how it goes.

Pre-requisites

1. Creating a new Wagtail e-commerce site

Make sure you have Wagtail installed. If not, refer to their installation documentation.

Open a terminal and launch a new Wagtail site:

wagtail start snipcartwagtaildemo
cd snipcartwagtaildemo

1.1 Models definition

The first thing you need to do is create your Page models. Wagtail uses these Django models to generate a page type.

Open the models.py file located in the home folder of your product. This is where you'll define all your custom models.

Create two different models:

  • Product: defines the product you're selling.
  • ProductCustomField: defines a single product custom field.

Let's begin by importing required modules:

# ./home/models.py
from django.db import models

from modelcluster.fields import ParentalKey

from wagtail.core.models import Page, Orderable
from wagtail.admin.edit_handlers import FieldPanel, MultiFieldPanel, InlinePanel
from wagtail.images.edit_handlers import ImageChooserPanel

Now add the Product model:

# ./home/models.py

class Product(Page):
    sku = models.CharField(max_length=255)
    short_description = models.TextField(blank=True, null=True)
    price = models.DecimalField(decimal_places=2, max_digits=10)
    image = models.ForeignKey(
        'wagtailimages.Image',
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name='+'
    )

    content_panels = Page.content_panels + [
        FieldPanel('sku'),
        FieldPanel('price'),
        ImageChooserPanel('image'),
        FieldPanel('short_description'),
        InlinePanel('custom_fields', label='Custom fields'),
    ]

And ProductCustomField:

# ./home/models.py

class ProductCustomField(Orderable):
    product = ParentalKey(Product, on_delete=models.CASCADE, related_name='custom_fields')
    name = models.CharField(max_length=255)
    options = models.CharField(max_length=500, null=True, blank=True)

    panels = [
        FieldPanel('name'),
        FieldPanel('options')
    ]

2. Adding Snipcart configuration settings

If you need more help for this part, refer to our documentation here and here.

Let's make sure you can update the Snipcart API key directly from Wagtail's dashboard.

You'll need to add site settings to do so.

Site settings are special fields that you can add to your models file. They'll appear in the Wagtail Settings section of the dashboard.

Import this module:

# ./home/models.py

from wagtail.contrib.settings.models import BaseSetting, register_setting

Then add these:

# ./home/models.py

@register_setting
class SnipcartSettings(BaseSetting):
    api_key = models.CharField(
        max_length=255,
        help_text='Your Snipcart public API key'
    )

3. Database migrations

Now that your models are created, you'll need to generate database migrations and run them.

In your terminal, use the makemigrations command:

manage.py makemigrations

You should see the following output:

Migrations for 'home':
  home\migrations\0003_product_productcustomfield_snipcartsettings.py
    - Create model Product
    - Create model ProductCustomField
    - Create model SnipcartSettings

Once the migrations are generated, apply them on your database with the migrate command:

manage.py migrate

It will take a couple of seconds; Wagtail will set up its own database schema along with the models you just defined.

Finally, create your first CMS user with the createsuperuser command:

manage.py createsuperuser

Don't forget the username and password you picked; you will need them to log into Wagtail's dashboard.

4. Creating products

Start by firing up your dev server with the Django dev server command:

manage.py runserver

Now open your browser and navigate to: http://localhost:8000/admin. Use the credentials you set up earlier to log in.

Select the Home page in Wagtail's menu. Then click on the Add child page button.

You'll be asked to pick a type of page, select Product.

new-product

Enter the product details, then publish your new product:

publish-product

You can create as many products as you wish.

4.1 Adding Snipcart API key

Remember the SnipcartSettings class you created? You'll be able to configure your API key by expanding the Settings menu and going to Snipcart settings.

snipcart-settings

Open Snipcart's dashboard and get your public API key (Test or Live), go back to Wagtail and paste it in the API key field.

Save your settings.

5. Templating

Your backend is now ready, your API key is configured, and your first products are created. Time to start building the site.

For this demo, I decided to use Spectre.css CSS framework. It's straightforward and lightweight.

Open the base.html file located in snipcartwaigtaildemo/templates

You'll need to add references for Spectree.css and Snipcart. Add these lines in the head of your document:

<!-- ./snipcartwagtaildemo/templates/base.html -->

{# Global stylesheets #}

<link rel="stylesheet" href="https://unpkg.com/spectre.css/dist/spectre.min.css">
<link rel="stylesheet" href="https://unpkg.com/spectre.css/dist/spectre-exp.min.css">
<link rel="stylesheet" href="https://unpkg.com/spectre.css/dist/spectre-icons.min.css">

{# Snipcart #}

{% if settings.home.SnipcartSettings.api_key %}
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.2/jquery.min.js"></script>
    <script src="https://cdn.snipcart.com/scripts/2.0/snipcart.js" id="snipcart" data-api-key="{{ settings.home.SnipcartSettings.api_key }}"></script>
    <link href="https://cdn.snipcart.com/themes/2.0/base/snipcart.min.css" type="text/css" rel="stylesheet" />
{% endif %}

The Snipcart API key that you configured previously is available via:

settings.home.SnipcartSettings.api_key

Then, add the navbar and some other Spectre.css layout elements.

Replace the content of the whole body by these lines:

<!-- ./snipcartwagtaildemo/templates/base.html -->
{% wagtailuserbar %}

<div class="container grid-lg">
    <header class="navbar">

        <section class="navbar-section">
            <a href="/" class="navbar-brand mr-2">
                Products
            </a>
        </section>

        <!-- Snipcart summary and View cart button -->
        <section class="navbar-section snipcart-summary">
            <div class="input-group input-line">
                <a href="" class="btn btn-primary snipcart-checkout">
                    <i class="icon icon-apps"></i>
                    View cart (<span class="snipcart-total-items">0</span>)
                </a>
            </div>
        </section>

    </header>
</div>

<div class="container grid-lg">
    {% block content %}{% endblock %}
</div>

{# Global javascript #}
<script type="text/javascript" src="{% static 'js/snipcartwagtaildemo.js' %}"></script>

{% block extra_js %}
    {# Override this in templates to add extra javascript #}
{% endblock %}

5.1 Listing products

The first template you need is your index, where products will be listed.

You'll have to make your products available in your home page context. In any Wagtail Page, you can override a method name get_context.

You can add the data that the view will receive in parameters. In my case, I want to set the products context variable.

Open the models.py file in home folder and update the HomePage class:

# ./home/models.py

class HomePage(Page):
    def get_context(self, request):
        context = super().get_context(request)

        context['products'] = Product.objects.child_of(self).live()

        return context

Then, open the home_page.html file located in the home/templates/home folder.

Let's create a simple page that will show each product image with a link to the product details.

<!-- /.home/templates/home/home_template.html -->
{% extends "base.html" %}
{% load wagtailimages_tags %}

{% block content %}
    <h1>
        Welcome to our store
    </h1>

    <div class="columns">
        {% for product in products %}
            <div class="column col-6">
                <div class="card">
                    <div class="card-image">
                        {% image product.image fill-1000x200 as tmp_image %}
                        <img src="{{ tmp_image.url }}" alt="" class="img-responsive">
                    </div>
                    <div class="card-header">
                        <a href="{{ product.get_url }}" class="btn btn-primary float-right">
                            <i class="icon icon-plus"></i>
                        </a>
                        <div class="card-title h5">
                            {{ product.title }}
                        </div>
                    </div>
                    <div class="card-body">
                        {{ product.description }}
                    </div>
                </div>
            </div>
        {% endfor %}
    </div>
{% endblock %}

5.2 Product details

The last template is the one showing individual product details along with the Snipcart buy button.

Also, it would be nice to be able to select product options directly on this page, before adding it to the cart. So I'll add a way to choose all custom fields with options directly in the template.

Before writing some HTML, you have to update the view context. Django templates don't give us 100% access to all Python methods and objects, so things like splitting a string do not work very well.

I decided to override the get_context method again. Maybe there's a better way to do that—let me know in the comments below! ;)

Open models.py from the home folder and add this method in the Product class:

# ./home/models.py

class Product(Page):

    def get_context(self, request):
        context = super().get_context(request)
        fields = []
        for f in self.custom_fields.get_object_list():
            if f.options:
                f.options_array = f.options.split('|')
                fields.append(f)
            else:
                fields.append(f)

        context['custom_fields'] = fields

        return context

A custom_fields array will be available in the product.html template.

Create a file named product.html in home/templates/home folder. This is the template that will be associated with the Product page model.

<!-- ./home/templates/home/product.html -->
{% extends "base.html" %}
{% load wagtailimages_tags %}

{% block content %}
    <div class="container grid-lg">
        <div class="columns">
            <div class="column col-4">
                {% image page.image max-300x300 as temp_image %}
                <img src="{{ temp_image.url }}" alt="" />
            </div>
            <div class="column col-8">
                <h1>
                    {{ page.title }}
                </h1>

                <p>
                    {{ page.short_description }}
                </p>

                <p>
                    {% for f in custom_fields %}
                        {% if f.options_array|length > 0 %}
                            <div class="form-group">
                                <label class="form-label" for="{{ f.name|lower }}">
                                    {{ f.name }}
                                </label>
                                <select class="form-select custom-field-select" id="{{ f.name|lower }}" data-field="{{ forloop.counter }}">
                                    {% for opt in f.options_array %}
                                        <option>
                                            {{ opt }}
                                        </option>
                                    {% endfor %}
                                </select>
                            </div>
                        {% endif %}
                    {% endfor %}
                </p>

                <button class="snipcart-add-item btn btn-primary"
                    data-item-name="{{ page.title }}"
                    data-item-id="{{ page.sku }}"
                    data-item-url="{{ page.get_full_url }}"
                    data-item-price="{{ page.price }}"
                    data-item-description="{{ page.short_description}}"
                    data-item-image="{{ temp_image.url }}"
                    {% for f in custom_fields %}
                        data-item-custom{{forloop.counter}}-name="{{f.name}}"
                        data-item-custom{{forloop.counter}}-options="{{f.options}}"
                    {% endfor %}>
                    <i class="icon icon-plus"></i>
                    Add to cart
                </button>
            </div>
        </div>
    </div>
{% endblock %}

Then, add some JavaScript to update Snipcart buy button when a custom field selection is made on the page.

Add the following script snippet before the endblock statement:

<script>
    document.addEventListener('DOMContentLoaded', function() {
        document.querySelector('.custom-field-select').onchange = function(event) {
            if (event.target.dataset.field) {
                document.querySelector('.snipcart-add-item')
                    .dataset['itemCustom' + event.target.dataset.field + 'Value'] = event.target.value;
            }
        };
    },false);
</script>

This code updates the button data attributes when the select value changes.

If you click on the + button beside any product, you should see its details:

product-details

You now have pretty strong foundations to start your e-commerce project using Django and Wagtail!

These frameworks are very powerful; you could quickly add some search functionalities, product suggestions, reviews, etc.

Live demo & GitHub repo

django-wagtail-demo-store

See the live demo

See the GitHub repo

Closing thoughts

I really enjoyed working with Wagtail; it's simple and intuitive. I have to say that their documentation feels incomplete at times though. At first, I wanted to make some changes on how routing would work and haven't found anything in their docs about that.

I didn't have Python installed on my laptop at the start, so setting up everything and having this demo up and running took me about a day, including hosting of the demo. I figure it would be way faster for avid Python developers!

For further exploration, I think Wagtail could be a great headless CMS, especially with their built-in API. I think it could be cool to leverage it and strapping it to tools like Nuxt or Gatsby to handle the front end.

Maybe another time! ;)


If you've enjoyed this post, please take a second to share it on Twitter. Got comments, questions? Hit the section below!

Suggested posts: