Build an E-Commerce Site with Wagtail CMS, Bootstrap & Django Framework.
paulsaul621
Posted on February 6, 2022
In this blog, we build a fully functional e-commerce site with Django and Wagtail CMS. I will also be briefly discussing:
- Why choose django framework to build a custom e-commerce website
- Which One is Better For Your Website? Django CMS Vs WordPress
You can check the demo of the final product at the following URL
Why choose Django for e-commerce?
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 and can scale perfectly with business growth. You can rely on Django to handle hundreds/thousands of visitors at a time. It's 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, often weakening 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. 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
Django makes it easy to build custom e-commerce websites. It has a modern, easy-to-use Python Web Framework that encourages rapid development and scalable architecture. Its high level abstraction make it easier to build, connect, configure, and maintain applications. In addition, Django makes it easy to add caching, secure authentication, and URL-agnostic views
Which One is Better For Your E-commerce Website? Django CMS Vs WordPress
Django database API makes it easy to work with databases. Django has a robust and secure authentication system. Django has a built in template engine, which means that you do not need to use a separate template language. Django is a free, open source web framework to! Django also follows the DRY(Don't Repeat Yourself) principle, DRY is the principle of reducing repetition in the code, referring back to a single source—or "snippet"—of reusable code whenever you need it.
Django also removes a lot of overhead. Develiopers do not have to worry about making their programs compatible with different versions of Python, which means they can spend more time about the application.
Wagtail CMS + Snipcart e-commerce
Wagtail is a developer-first Django content management system. Free and open-source, the good-hearted folks at Torchbox developed it. 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.
Below is a screenshot of the shop we are going to build:
Shall we begin!
Pre-requisites
- A Snipcart account.
1. Creating a Wagtail app
Use pip, which is packaged with Python, to install Wagtail and its dependencies:
$ python3 -m venv venv
$ source venv/bin/activate
$ pip install wagtail requests
Open a terminal and launch a new Wagtail site:
$ wagtail start snipcartwagtaildemo
$ cd snipcartwagtaildemo
We have an extra step to complete the Wagtail setup, and it's to install the wagtail.contrib.settings plugin that we'll require later on.
In your new Wagtail project, open the base.py file located in snipcartwaigtaildemo/settings folder. Then, add wagtail.contrib.settings to the INSTALLED_APPS array.
# ./setting/base.py
INSTALLED_APPS = [
...,
'wagtail.contrib.settings'
]
1.1 Models Creation.
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 the required modules:
# ./home/models.py
from django.db import models
from wagtail.core.models import Page
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
from wagtail.contrib.settings.models import BaseSetting, register_setting
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
from django.template.response import TemplateResponse
from django.core.paginator import Paginator
Now add the Product model:
# ./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
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'),
]
Update the homepage class which is responsible for listing products on the homepage only. I will be listing only 6 products on the homepage. In any Wagtail Page, you can override a method name get_context. You can add the data that the view will receive in parameters
class HomePage(Page):
def get_context(self, request):
context = super().get_context(request)
context['products'] = Product.objects.child_of(self).live()[:6]
return context
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')
]
We need to add one more view that will be responsible for showing all products in our online shop, and for paginating the page. I will be paginating on the backend side as loading all the objects on a page is not very efficient. We can however cache our data and load it all on the front end. This will make the pagination seamless, this all depends on the size of your database.
def shop(request):
products = Product.objects.live()
p = Paginator(products, 10)
#shows number of items in page
totalProducts = (p.count)
pageNum = request.GET.get('page', 1)
page1 = p.page(pageNum)
return TemplateResponse(request, 'home/shop.html', {
'products': products,
'dataSaved':page1
})
Finally, add the about us and contact us section views, like so:
def about(request):
return TemplateResponse(request, 'home/about.html', {
})
def contact(request):
return TemplateResponse(request, 'home/contact.html', {
})
2. Adding Snipcart configuration settings
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.py 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:
$ python3 manage.py makemigrations
$ python3 manage.py migrate
Finally, create your first CMS user with the createsuperuser command:
$ python3 manage.py createsuperuser
It will ask for an email address, a username, and to create a password. Don't forget the username and password you picked; you will need them to log into Wagtail's dashboard.
4. Creating products
Fire up your server like so:
$ python3 manage.py runserver
Now open your browser and navigate to http://127.0.0.1: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 Home page and give it a name.
Go back to the Home page menu and click Add child page button under your newly created page.
Once again, you'll be asked to pick a type of page, this time select 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.
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.
Adding new URL routes
I have barely used wagtail and i am not very familiar with how the URL's are handled but i created my own URL for the shop view, for listing all products:
from django.conf import settings
from django.urls import include, path
from django.contrib import admin
from wagtail.admin import urls as wagtailadmin_urls
from wagtail.core import urls as wagtail_urls
from wagtail.documents import urls as wagtaildocs_urls
from search import views as search_views
from home import models as p
urlpatterns = [
path('django-admin/', admin.site.urls),
path('admin/', include(wagtailadmin_urls)),
path('documents/', include(wagtaildocs_urls)),
path('search/', search_views.search, name='search'),
path('shop/', p.shop, name='shop'),
path('about/', p.about, name='about'),
path('contact/', p.contact, name='contact'),
]
if settings.DEBUG:
from django.conf.urls.static import static
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
# Serve static and media files from development server
urlpatterns += staticfiles_urlpatterns()
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns = urlpatterns + [
# For anything not caught by a more specific rule above, hand over to
# Wagtail's page serving mechanism. This should be the last pattern in
# the list:
path("", include(wagtail_urls)),
# Alternatively, if you want Wagtail pages to be served from a subpath
# of your site, rather than the site root:
# path("pages/", include(wagtail_urls)),
]
5. Templating
Let us now proceed with creating our HTML files and modifying our application UI. The template is from this Colorlib URL.
Homepage
On our homepage, we need to display a few items from our database. We already specified 6 items on our homepage view. Create 3 files on the home/templates/home folder:
- home_page.html
- product.html
- shop.html
Before i display the whole code, let me explain a few important bits, starting with the head of the HTML files
{% load static wagtailcore_tags wagtailuserbar %}
{% load wagtailimages_tags %}
<!DOCTYPE html>
<html lang="en">
<head>
<title>Tafari Pharmacy</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
{% load static wagtailsettings_tags %}
{% get_settings %}
{# Global stylesheets #}
<link href="https://fonts.googleapis.com/css?family=Rubik:400,700|Crimson+Text:400,400i" rel="stylesheet">
<link rel="stylesheet" href="{% static 'fonts/icomoon/style.css' %}">
<link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}">
<link rel="stylesheet" href="{% static 'css/magnific-popup.css' %}">
<link rel="stylesheet" href="{% static 'css/jquery-ui.css' %}">
<link rel="stylesheet" href="{% static 'css/owl.carousel.min.css' %}">
<link rel="stylesheet" href="{% static 'css/owl.theme.default.min.css' %}">
<link rel="stylesheet" href="{% static 'css/aos.css' %}">
<link rel="stylesheet" href="{% static 'css/style.css' %}">
{# Snipcart #}
{% if settings.home.SnipcartSettings.api_key %}
<script async src="https://cdn.snipcart.com/themes/v3.2.1/default/snipcart.js"></script>
<div hidden id="snipcart" data-api-key="{{ settings.home.SnipcartSettings.api_key }}"></div>
<link rel="stylesheet" href="https://cdn.snipcart.com/themes/v3.2.1/default/snipcart.css" />
{% endif %}
</head>
We have the reference for the snipcart tags where it detects the API key we added on the settings file and have reference for the snipcart files as cdn files.
<div class="row">
{% for product in products %}
<div class="col-sm-6 col-lg-4 text-center item mb-4">
<a href="{{ product.get_url }}">
{% image product.image fill-370x270 as tmp_image %}
<img src="{{ tmp_image.url }}" alt="Image">
</a>
<h3 class="text-dark"><a href="{{ product.get_url }}">{{ product.title }}</a></h3>
<p class="price">Ksh. {{ product.price }}</p>
</div>
{% endfor %}
</div>
We also have the product part that loops through the products and displays them all. For more details, you can visit the snip cart docs.
The whole code for the homepage is:
{% load static wagtailcore_tags wagtailuserbar %}
{% load wagtailimages_tags %}
<!DOCTYPE html>
<html lang="en">
<head>
<title>Tafari Pharmacy</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
{% load static wagtailsettings_tags %}
{% get_settings %}
{# Global stylesheets #}
<link href="https://fonts.googleapis.com/css?family=Rubik:400,700|Crimson+Text:400,400i" rel="stylesheet">
<link rel="stylesheet" href="{% static 'fonts/icomoon/style.css' %}">
<link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}">
<link rel="stylesheet" href="{% static 'css/magnific-popup.css' %}">
<link rel="stylesheet" href="{% static 'css/jquery-ui.css' %}">
<link rel="stylesheet" href="{% static 'css/owl.carousel.min.css' %}">
<link rel="stylesheet" href="{% static 'css/owl.theme.default.min.css' %}">
<link rel="stylesheet" href="{% static 'css/aos.css' %}">
<link rel="stylesheet" href="{% static 'css/style.css' %}">
{# Snipcart #}
{% if settings.home.SnipcartSettings.api_key %}
<script async src="https://cdn.snipcart.com/themes/v3.2.1/default/snipcart.js"></script>
<div hidden id="snipcart" data-api-key="{{ settings.home.SnipcartSettings.api_key }}"></div>
<link rel="stylesheet" href="https://cdn.snipcart.com/themes/v3.2.1/default/snipcart.css" />
{% endif %}
</head>
<body>
{% wagtailuserbar %}
<div class="site-wrap">
<div class="site-navbar py-2">
<div class="search-wrap">
<div class="container">
<a href="#" class="search-close js-search-close"><span class="icon-close2"></span></a>
<form action="#" method="post">
<input type="text" class="form-control" placeholder="Search keyword and hit enter...">
</form>
</div>
</div>
<div class="container">
<div class="d-flex align-items-center justify-content-between">
<div class="logo">
<div class="site-logo">
<a href="/" class="js-logo-clone">Tafari Pharmacy</a>
</div>
</div>
<div class="main-nav d-none d-lg-block">
<nav class="site-navigation text-right text-md-center" role="navigation">
<ul class="site-menu js-clone-nav d-none d-lg-block">
<li class="active"><a href="/">Home</a></li>
{% if request.path == '/' %}
<li><a href="/shop">Store</a></li>
{% endif %}
<li class="has-children">
<a href="#">Medications</a>
<ul class="dropdown">
<li><a href="#">Supplements</a></li>
<li class="has-children">
<a href="#">Vitamins</a>
<ul class="dropdown">
<li><a href="#">Supplements</a></li>
<li><a href="#">Vitamins</a></li>
<li><a href="#">Diet & Nutrition</a></li>
<li><a href="#">Tea & Coffee</a></li>
</ul>
</li>
<li><a href="#">Diet & Nutrition</a></li>
<li><a href="#">Tea & Coffee</a></li>
</ul>
</li>
<li><a href="/about">About</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
</div>
<div class="icons">
<a href="#" class="icons-btn d-inline-block js-search-open"><span class="icon-search"></span></a>
<a href="" class="icons-btn d-inline-block bag">
<span class="snipcart-checkout icon-shopping-bag"></span>
<span class="snipcart-checkout number snipcart-items-count"></span>
</a>
<a href="#" class="site-menu-toggle js-menu-toggle ml-3 d-inline-block d-lg-none"><span
class="icon-menu"></span></a>
</div>
</div>
</div>
</div>
<div class="site-blocks-cover" style="background-image: url('{% static 'images/hero_1.jpg' %}');">
<div class="container">
<div class="row">
<div class="col-lg-7 mx-auto order-lg-2 align-self-center">
<div class="site-block-cover-content text-center">
<h2 class="sub-title">Effective Medicine, New Medicine Everyday</h2>
<h1>Welcome To Tafari Pharmacy</h1>
<p>
{% if request.path == '/' %}
<a href="#" class="btn btn-primary px-5 py-3">Shop Now</a>
{% endif %}
</p>
</div>
</div>
</div>
</div>
</div>
<div class="site-section">
<div class="container">
<div class="row align-items-stretch section-overlap">
<div class="col-md-6 col-lg-4 mb-4 mb-lg-0">
<div class="banner-wrap bg-primary h-100">
<a href="#" class="h-100">
<h5>Free <br> Delivery</h5>
<p>
Amet sit amet dolor
<strong>Lorem, ipsum dolor sit amet consectetur adipisicing.</strong>
</p>
</a>
</div>
</div>
<div class="col-md-6 col-lg-4 mb-4 mb-lg-0">
<div class="banner-wrap h-100">
<a href="#" class="h-100">
<h5>Season <br> Sale 50% Off</h5>
<p>
Amet sit amet dolor
<strong>Lorem, ipsum dolor sit amet consectetur adipisicing.</strong>
</p>
</a>
</div>
</div>
<div class="col-md-6 col-lg-4 mb-4 mb-lg-0">
<div class="banner-wrap bg-warning h-100">
<a href="#" class="h-100">
<h5>Buy <br> A Gift Card</h5>
<p>
Amet sit amet dolor
<strong>Lorem, ipsum dolor sit amet consectetur adipisicing.</strong>
</p>
</a>
</div>
</div>
</div>
</div>
</div>
<div class="site-section">
<div class="container">
<div class="row">
<div class="title-section text-center col-12">
<h2 class="text-uppercase">Popular Products</h2>
</div>
</div>
<div class="row">
{% for product in products %}
<div class="col-sm-6 col-lg-4 text-center item mb-4">
<a href="{{ product.get_url }}">
{% image product.image fill-370x270 as tmp_image %}
<img src="{{ tmp_image.url }}" alt="Image">
</a>
<h3 class="text-dark"><a href="{{ product.get_url }}">{{ product.title }}</a></h3>
<p class="price">Ksh. {{ product.price }}</p>
</div>
{% endfor %}
</div>
<div class="row mt-5">
<div class="col-12 text-center">
{% if request.path == '/' %}
<a href="/shop" class="btn btn-primary px-4 py-3">View All Products</a>
{% endif %}
</div>
</div>
</div>
</div>
<div class="site-section bg-light">
<div class="container">
<div class="row">
<div class="title-section text-center col-12">
<h2 class="text-uppercase">New Products</h2>
</div>
</div>
<div class="row">
<div class="col-md-12 block-3 products-wrap">
<div class="nonloop-block-3 owl-carousel">
<div class="text-center item mb-4">
<a href="shop-single.html"> <img src="{% static 'images/product_03.png' %}" alt="Image"></a>
<h3 class="text-dark"><a href="shop-single.html">Umcka Cold Care</a></h3>
<p class="price">$120.00</p>
</div>
<div class="text-center item mb-4">
<a href="shop-single.html"> <img src="{% static 'images/product_01.png' %}" alt="Image"></a>
<h3 class="text-dark"><a href="shop-single.html">Umcka Cold Care</a></h3>
<p class="price">$120.00</p>
</div>
<div class="text-center item mb-4">
<a href="shop-single.html"> <img src="{% static 'images/product_02.png' %}" alt="Image"></a>
<h3 class="text-dark"><a href="shop-single.html">Umcka Cold Care</a></h3>
<p class="price">$120.00</p>
</div>
<div class="text-center item mb-4">
<a href="shop-single.html"> <img src="{% static 'images/product_04.png' %}" alt="Image"></a>
<h3 class="text-dark"><a href="shop-single.html">Umcka Cold Care</a></h3>
<p class="price">$120.00</p>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="site-section">
<div class="container">
<div class="row">
<div class="title-section text-center col-12">
<h2 class="text-uppercase">Testimonials</h2>
</div>
</div>
<div class="row">
<div class="col-md-12 block-3 products-wrap">
<div class="nonloop-block-3 no-direction owl-carousel">
<div class="testimony">
<blockquote>
<img src="{% static 'images/person_1.jpg' %}" alt="Image" class="img-fluid w-25 mb-4 rounded-circle">
<p>“Lorem ipsum dolor, sit amet consectetur adipisicing elit. Nemo omnis voluptatem consectetur quam tempore obcaecati maiores voluptate aspernatur iusto eveniet, placeat ab quod tenetur ducimus. Minus ratione sit quaerat unde.”</p>
</blockquote>
<p>— Kelly Holmes</p>
</div>
<div class="testimony">
<blockquote>
<img src="{% static 'images/person_2.jpg' %}" alt="Image" class="img-fluid w-25 mb-4 rounded-circle">
<p>“Lorem ipsum dolor, sit amet consectetur adipisicing elit. Nemo omnis voluptatem consectetur quam tempore
obcaecati maiores voluptate aspernatur iusto eveniet, placeat ab quod tenetur ducimus. Minus ratione sit quaerat
unde.”</p>
</blockquote>
<p>— Rebecca Morando</p>
</div>
<div class="testimony">
<blockquote>
<img src="{% static 'images/person_3.jpg' %}" alt="Image" class="img-fluid w-25 mb-4 rounded-circle">
<p>“Lorem ipsum dolor, sit amet consectetur adipisicing elit. Nemo omnis voluptatem consectetur quam tempore
obcaecati maiores voluptate aspernatur iusto eveniet, placeat ab quod tenetur ducimus. Minus ratione sit quaerat
unde.”</p>
</blockquote>
<p>— Lucas Gallone</p>
</div>
<div class="testimony">
<blockquote>
<img src="{% static 'images/person_4.jpg' %}" alt="Image" class="img-fluid w-25 mb-4 rounded-circle">
<p>“Lorem ipsum dolor, sit amet consectetur adipisicing elit. Nemo omnis voluptatem consectetur quam tempore
obcaecati maiores voluptate aspernatur iusto eveniet, placeat ab quod tenetur ducimus. Minus ratione sit quaerat
unde.”</p>
</blockquote>
<p>— Andrew Neel</p>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="site-section bg-secondary bg-image" style="background-image: url('{% static 'images/bg_2.jpg' %}');">
<div class="container">
<div class="row align-items-stretch">
<div class="col-lg-6 mb-5 mb-lg-0">
<a href="#" class="banner-1 h-100 d-flex" style="background-image: url('{% static 'images/bg_1.jpg' %}');">
<div class="banner-1-inner align-self-center">
<h2>Tafari Pharmacy Products</h2>
<p>Lorem, ipsum dolor sit amet consectetur adipisicing elit. Molestiae ex ad minus rem odio voluptatem.
</p>
</div>
</a>
</div>
<div class="col-lg-6 mb-5 mb-lg-0">
<a href="#" class="banner-1 h-100 d-flex" style="background-image: url('{% static 'images/bg_2.jpg' %}');">
<div class="banner-1-inner ml-auto align-self-center">
<h2>Rated by Experts</h2>
<p>Lorem, ipsum dolor sit amet consectetur adipisicing elit. Molestiae ex ad minus rem odio voluptatem.
</p>
</div>
</a>
</div>
</div>
</div>
</div>
<footer class="site-footer">
<div class="container">
<div class="row">
<div class="col-md-6 col-lg-3 mb-4 mb-lg-0">
<div class="block-7">
<h3 class="footer-heading mb-4">About Us</h3>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Eius quae reiciendis distinctio voluptates
sed dolorum excepturi iure eaque, aut unde.</p>
</div>
</div>
<div class="col-lg-3 mx-auto mb-5 mb-lg-0">
<h3 class="footer-heading mb-4">Quick Links</h3>
<ul class="list-unstyled">
<li><a href="#">Supplements</a></li>
<li><a href="#">Vitamins</a></li>
<li><a href="#">Diet & Nutrition</a></li>
<li><a href="#">Tea & Coffee</a></li>
</ul>
</div>
<div class="col-md-6 col-lg-3">
<div class="block-5 mb-5">
<h3 class="footer-heading mb-4">Contact Info</h3>
<ul class="list-unstyled">
<li class="address">203 Fake St. Mountain View, San Francisco, California, USA</li>
<li class="phone"><a href="tel://23923929210">+2 392 3929 210</a></li>
<li class="email">emailaddress@domain.com</li>
</ul>
</div>
</div>
</div>
<div class="row pt-5 mt-5 text-center">
<div class="col-md-12">
<p>
<!-- Link back to Colorlib can't be removed. Template is licensed under CC BY 3.0. -->
Copyright ©
<script>document.write(new Date().getFullYear());</script> All rights reserved | This template is made
with <i class="icon-heart" aria-hidden="true"></i> by <a href="https://colorlib.com" target="_blank"
class="text-primary">Colorlib</a>
<!-- Link back to Colorlib can't be removed. Template is licensed under CC BY 3.0. -->
</p>
</div>
</div>
</div>
</footer>
</div>
<script src="{% static 'js/jquery-3.3.1.min.js' %}"></script>
<script src="{% static 'js/jquery-ui.js' %}"></script>
<script src="{% static 'js/popper.min.js' %}"></script>
<script src="{% static 'js/bootstrap.min.js' %}"></script>
<script src="{% static 'js/owl.carousel.min.js' %}"></script>
<script src="{% static 'js/jquery.magnific-popup.min.js' %}"></script>
<script src="{% static 'js/aos.js' %}"></script>
<script src="{% static 'js/main.js' %}"></script>
</body>
</html>
All Products Page
On the product.html, add the lines of code below. I didn't use the inbuilt django templates functionality to work in components so we have to repeat the tags we added in the homepage file, for every page we create. Copy paste the code below on the templates/home/shop.html:
{% load static wagtailcore_tags wagtailuserbar %}
{% load wagtailimages_tags %}
<!DOCTYPE html>
<html lang="en">
<head>
<title>Tafari Tafari Pharmacycy — Colorlib Template</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
{% load static wagtailsettings_tags %}
{% get_settings %}
{# Global stylesheets #}
<link href="https://fonts.googleapis.com/css?family=Rubik:400,700|Crimson+Text:400,400i" rel="stylesheet">
<link rel="stylesheet" href="{% static 'fonts/icomoon/style.css' %}">
<link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}">
<link rel="stylesheet" href="{% static 'css/magnific-popup.css' %}">
<link rel="stylesheet" href="{% static 'css/jquery-ui.css' %}">
<link rel="stylesheet" href="{% static 'css/owl.carousel.min.css' %}">
<link rel="stylesheet" href="{% static 'css/owl.theme.default.min.css' %}">
<link rel="stylesheet" href="{% static 'css/aos.css' %}">
<link rel="stylesheet" href="{% static 'css/style.css' %}">
{# Snipcart #}
{% if settings.home.SnipcartSettings.api_key %}
<script async src="https://cdn.snipcart.com/themes/v3.2.1/default/snipcart.js"></script>
<div hidden id="snipcart" data-api-key="{{ settings.home.SnipcartSettings.api_key }}"></div>
<link rel="stylesheet" href="https://cdn.snipcart.com/themes/v3.2.1/default/snipcart.css" />
{% endif %}
</head>
<body>
{% wagtailuserbar %}
<div class="site-wrap">
<div class="site-navbar py-2">
<div class="search-wrap">
<div class="container">
<a href="#" class="search-close js-search-close"><span class="icon-close2"></span></a>
<form action="#" method="post">
<input type="text" class="form-control" placeholder="Search keyword and hit enter...">
</form>
</div>
</div>
<div class="container">
<div class="d-flex align-items-center justify-content-between">
<div class="logo">
<div class="site-logo">
<a href="/" class="js-logo-clone">Tafari Pharmacy</a>
</div>
</div>
<div class="main-nav d-none d-lg-block">
<nav class="site-navigation text-right text-md-center" role="navigation">
<ul class="site-menu js-clone-nav d-none d-lg-block">
<li><a href="/">Home</a></li>
<li class="active"><a href="/shop">Store</a></li>
<li class="has-children">
<a href="#">Medications</a>
<ul class="dropdown">
<li><a href="#">Supplements</a></li>
<li class="has-children">
<a href="#">Vitamins</a>
<ul class="dropdown">
<li><a href="#">Supplements</a></li>
<li><a href="#">Vitamins</a></li>
<li><a href="#">Diet & Nutrition</a></li>
<li><a href="#">Tea & Coffee</a></li>
</ul>
</li>
<li><a href="#">Diet & Nutrition</a></li>
<li><a href="#">Tea & Coffee</a></li>
</ul>
</li>
<li><a href="/about">About</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
</div>
<div class="icons">
<a href="#" class="icons-btn d-inline-block js-search-open"><span class="icon-search"></span></a>
<a href="" class="icons-btn d-inline-block bag">
<span class="snipcart-checkout icon-shopping-bag"></span>
<span class="snipcart-checkout number snipcart-items-count"></span>
</a>
<a href="#" class="site-menu-toggle js-menu-toggle ml-3 d-inline-block d-lg-none"><span
class="icon-menu"></span></a>
</div>
</div>
</div>
</div>
<div class="bg-light py-3">
<div class="container">
<div class="row">
<div class="col-md-12 mb-0"><a href="/">Home</a> <span class="mx-2 mb-0">/</span> <strong class="text-black">Store</strong></div>
</div>
</div>
</div>
<div class="site-section">
<div class="container">
{% for product in dataSaved.object_list %}
<div class="row">
<div class="col-sm-6 col-lg-4 text-center item mb-4">
<a href="{{ product.get_url }}">
{% image product.image fill-370x270 as tmp_image %}
<img src="{{ tmp_image.url }}" alt="Image">
</a>
<h3 class="text-dark"><a href="{{ product.get_url }}">{{ product.title }}</a></h3>
<p class="price">Ksh. {{ product.price }}</p>
</div>
</div>
{% endfor %}
<div class="row mt-5">
<div class="col-md-12 text-center">
<div class="site-block-27">
{% if dataSaved.has_other_pages %}
<ul class="pagination justify-content-center">
{% if dataSaved.has_previous %}
<li class="page-item">
<a class="page-link" href="?page={{ dataSaved.previous_page_number }}" tabindex="-1">Previous</a>
</li>
{% endif %}
{% for i in dataSaved.paginator.page_range %}
{% if dataSaved.number == i %}
<li class="page-item active">
<a class="page-link" href="#">{{ i }}<span class="sr-only">(current)</span></a>
</li>
{% endif %}
{% endfor %}
<li class="page-item">
{% if dataSaved.has_next %}
<a class="page-link" href="?page={{ dataSaved.next_page_number }}">Next</a>
{% endif %}
</li>
</ul>
{% endif %}
</div>
</div>
</div>
</div>
</div>
<div class="site-section bg-secondary bg-image" style="background-image: url('{% static 'images/bg_2.jpg' %}');">
<div class="container">
<div class="row align-items-stretch">
<div class="col-lg-6 mb-5 mb-lg-0">
<a href="#" class="banner-1 h-100 d-flex" style="background-image: url('{% static 'images/bg_1.jpg' %}');">
<div class="banner-1-inner align-self-center">
<h2>Tafari Pharmacy Products</h2>
<p>Lorem, ipsum dolor sit amet consectetur adipisicing elit. Molestiae ex ad minus rem odio voluptatem.
</p>
</div>
</a>
</div>
<div class="col-lg-6 mb-5 mb-lg-0">
<a href="#" class="banner-1 h-100 d-flex" style="background-image: url('{% static 'images/bg_2.jpg' %}');">
<div class="banner-1-inner ml-auto align-self-center">
<h2>Rated by Experts</h2>
<p>Lorem, ipsum dolor sit amet consectetur adipisicing elit. Molestiae ex ad minus rem odio voluptatem.
</p>
</div>
</a>
</div>
</div>
</div>
</div>
<footer class="site-footer">
<div class="container">
<div class="row">
<div class="col-md-6 col-lg-3 mb-4 mb-lg-0">
<div class="block-7">
<h3 class="footer-heading mb-4">About Us</h3>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Eius quae reiciendis distinctio voluptates
sed dolorum excepturi iure eaque, aut unde.</p>
</div>
</div>
<div class="col-lg-3 mx-auto mb-5 mb-lg-0">
<h3 class="footer-heading mb-4">Quick Links</h3>
<ul class="list-unstyled">
<li><a href="#">Supplements</a></li>
<li><a href="#">Vitamins</a></li>
<li><a href="#">Diet & Nutrition</a></li>
<li><a href="#">Tea & Coffee</a></li>
</ul>
</div>
<div class="col-md-6 col-lg-3">
<div class="block-5 mb-5">
<h3 class="footer-heading mb-4">Contact Info</h3>
<ul class="list-unstyled">
<li class="address">203 Fake St. Mountain View, San Francisco, California, USA</li>
<li class="phone"><a href="tel://23923929210">+2 392 3929 210</a></li>
<li class="email">emailaddress@domain.com</li>
</ul>
</div>
</div>
</div>
<div class="row pt-5 mt-5 text-center">
<div class="col-md-12">
<p>
<!-- Link back to Colorlib can't be removed. Template is licensed under CC BY 3.0. -->
Copyright ©
<script>document.write(new Date().getFullYear());</script> All rights reserved | This template is made
with <i class="icon-heart" aria-hidden="true"></i> by <a href="https://colorlib.com" target="_blank"
class="text-primary">Colorlib</a>
<!-- Link back to Colorlib can't be removed. Template is licensed under CC BY 3.0. -->
</p>
</div>
</div>
</div>
</footer>
</div>
<script src="{% static 'js/jquery-3.3.1.min.js' %}"></script>
<script src="{% static 'js/jquery-ui.js' %}"></script>
<script src="{% static 'js/popper.min.js' %}"></script>
<script src="{% static 'js/bootstrap.min.js' %}"></script>
<script src="{% static 'js/owl.carousel.min.js' %}"></script>
<script src="{% static 'js/jquery.magnific-popup.min.js' %}"></script>
<script src="{% static 'js/aos.js' %}"></script>
<script src="{% static 'js/main.js' %}"></script>
<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>
</body>
</html>
The code is the same as the home page code, the only difference is in the shop.html file, we paginate our webpage, and display all products instead of just 6 like in the homepage.
Product details and cart display page
For the products page, we need to display the product when a user clicks on an item, like so:
In order to do this, we need to create a template that will be associated with the Product page model. On the product.html in the home/templates/home folder, add:
{% load static wagtailcore_tags wagtailuserbar %}
{% load wagtailimages_tags %}
<!DOCTYPE html>
<html lang="en">
<head>
<title>Tafari Pharmacy — Colorlib Template</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
{% load static wagtailsettings_tags %}
{% get_settings %}
{# Global stylesheets #}
<link href="https://fonts.googleapis.com/css?family=Rubik:400,700|Crimson+Text:400,400i" rel="stylesheet">
<link rel="stylesheet" href="{% static 'fonts/icomoon/style.css' %}">
<link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}">
<link rel="stylesheet" href="{% static 'css/magnific-popup.css' %}">
<link rel="stylesheet" href="{% static 'css/jquery-ui.css' %}">
<link rel="stylesheet" href="{% static 'css/owl.carousel.min.css' %}">
<link rel="stylesheet" href="{% static 'css/owl.theme.default.min.css' %}">
<link rel="stylesheet" href="{% static 'css/aos.css' %}">
<link rel="stylesheet" href="{% static 'css/style.css' %}">
{# Snipcart #}
{% if settings.home.SnipcartSettings.api_key %}
<script async src="https://cdn.snipcart.com/themes/v3.2.1/default/snipcart.js"></script>
<div hidden id="snipcart" data-api-key="{{ settings.home.SnipcartSettings.api_key }}"></div>
<link rel="stylesheet" href="https://cdn.snipcart.com/themes/v3.2.1/default/snipcart.css" />
{% endif %}
</head>
<body>
{% wagtailuserbar %}
<div class="site-wrap">
<div class="site-navbar py-2">
<div class="search-wrap">
<div class="container">
<a href="#" class="search-close js-search-close"><span class="icon-close2"></span></a>
<form action="#" method="post">
<input type="text" class="form-control" placeholder="Search keyword and hit enter...">
</form>
</div>
</div>
<div class="container">
<div class="d-flex align-items-center justify-content-between">
<div class="logo">
<div class="site-logo">
<a href="/" class="js-logo-clone">Tafari Pharmacy</a>
</div>
</div>
<div class="main-nav d-none d-lg-block">
<nav class="site-navigation text-right text-md-center" role="navigation">
<ul class="site-menu js-clone-nav d-none d-lg-block">
<li><a href="/">Home</a></li>
<li class="active"><a href="/shop">Store</a></li>
<li class="has-children">
<a href="#">Medications</a>
<ul class="dropdown">
<li><a href="#">Supplements</a></li>
<li class="has-children">
<a href="#">Vitamins</a>
<ul class="dropdown">
<li><a href="#">Supplements</a></li>
<li><a href="#">Vitamins</a></li>
<li><a href="#">Diet & Nutrition</a></li>
<li><a href="#">Tea & Coffee</a></li>
</ul>
</li>
<li><a href="#">Diet & Nutrition</a></li>
<li><a href="#">Tea & Coffee</a></li>
</ul>
</li>
<li><a href="/about">About</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
</div>
<div class="icons">
<a href="#" class="icons-btn d-inline-block js-search-open"><span class="icon-search"></span></a>
<a href="" class="icons-btn d-inline-block bag">
<span class="snipcart-checkout icon-shopping-bag"></span>
<span class="snipcart-checkout number snipcart-items-count"></span>
</a>
<a href="#" class="site-menu-toggle js-menu-toggle ml-3 d-inline-block d-lg-none"><span
class="icon-menu"></span></a>
</div>
</div>
</div>
</div>
<div class="bg-light py-3">
<div class="container">
<div class="row">
<div class="col-md-12 mb-0"><a href="/">Home</a> <span class="mx-2 mb-0">/</span> <a
href="/shop">Store</a> <span class="mx-2 mb-0">/</span> <strong class="text-black">{{ page.title }}</strong></div>
</div>
</div>
</div>
<div class="site-section">
<div class="container">
<div class="row">
<div class="col-md-5 mr-auto">
<div class="border text-center">
{% image page.image max-370x270 as temp_image %}
<img src="{{ temp_image.url }}" alt="Image" class="img-fluid p-5">
</div>
</div>
<div class="col-md-6">
<h2 class="text-black">{{ page.title }}</h2>
<p>{{page.short_description}}</p>
<p><del>$95.00</del> <strong class="text-primary h4">Ksh. {{ page.price }}</strong></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 mt-6 py-2 px-4 bg-gradient-to-r from-green-400 to-blue-500 hover:from-pink-500 hover:to-yellow-500 text-white font-bold rounded-full shadow-offset hover:shadow-lg transition duration-300"
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 %}>Add to cart
</button>
<div class="mt-5">
<ul class="nav nav-pills mb-3 custom-pill" id="pills-tab" role="tablist">
<li class="nav-item">
<a class="nav-link active" id="pills-home-tab" data-toggle="pill" href="#pills-home" role="tab"
aria-controls="pills-home" aria-selected="true">Ordering Information</a>
</li>
<li class="nav-item">
<a class="nav-link" id="pills-profile-tab" data-toggle="pill" href="#pills-profile" role="tab"
aria-controls="pills-profile" aria-selected="false">Specifications</a>
</li>
</ul>
<div class="tab-content" id="pills-tabContent">
<div class="tab-pane fade show active" id="pills-home" role="tabpanel" aria-labelledby="pills-home-tab">
<table class="table custom-table">
<thead>
<th>Material</th>
<th>Description</th>
<th>Packaging</th>
</thead>
<tbody>
<tr>
<th scope="row">OTC022401</th>
<td>Pain Management: Acetaminophen PM Extra-Strength Caplets, 500 mg, 100/Bottle</td>
<td>1 BT</td>
</tr>
<tr>
<th scope="row">OTC022401</th>
<td>Pain Management: Acetaminophen PM Extra-Strength Caplets, 500 mg, 100/Bottle</td>
<td>144/CS</td>
</tr>
<tr>
<th scope="row">OTC022401</th>
<td>Pain Management: Acetaminophen PM Extra-Strength Caplets, 500 mg, 100/Bottle</td>
<td>1 EA</td>
</tr>
</tbody>
</table>
</div>
<div class="tab-pane fade" id="pills-profile" role="tabpanel" aria-labelledby="pills-profile-tab">
<table class="table custom-table">
<tbody>
<tr>
<td>HPIS CODE</td>
<td class="bg-light">999_200_40_0</td>
</tr>
<tr>
<td>HEALTHCARE PROVIDERS ONLY</td>
<td class="bg-light">No</td>
</tr>
<tr>
<td>LATEX FREE</td>
<td class="bg-light">Yes, No</td>
</tr>
<tr>
<td>MEDICATION ROUTE</td>
<td class="bg-light">Topical</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="site-section bg-secondary bg-image" style="background-image: url('images/bg_2.jpg');">
<div class="container">
<div class="row align-items-stretch">
<div class="col-lg-6 mb-5 mb-lg-0">
<a href="#" class="banner-1 h-100 d-flex" style="background-image: url('images/bg_1.jpg');">
<div class="banner-1-inner align-self-center">
<h2>Tafari Pharmacy Products</h2>
<p>Lorem, ipsum dolor sit amet consectetur adipisicing elit. Molestiae ex ad minus rem odio voluptatem.
</p>
</div>
</a>
</div>
<div class="col-lg-6 mb-5 mb-lg-0">
<a href="#" class="banner-1 h-100 d-flex" style="background-image: url('images/bg_2.jpg');">
<div class="banner-1-inner ml-auto align-self-center">
<h2>Rated by Experts</h2>
<p>Lorem, ipsum dolor sit amet consectetur adipisicing elit. Molestiae ex ad minus rem odio voluptatem.
</p>
</div>
</a>
</div>
</div>
</div>
</div>
<footer class="site-footer">
<div class="container">
<div class="row">
<div class="col-md-6 col-lg-3 mb-4 mb-lg-0">
<div class="block-7">
<h3 class="footer-heading mb-4">About Us</h3>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Eius quae reiciendis distinctio voluptates
sed dolorum excepturi iure eaque, aut unde.</p>
</div>
</div>
<div class="col-lg-3 mx-auto mb-5 mb-lg-0">
<h3 class="footer-heading mb-4">Quick Links</h3>
<ul class="list-unstyled">
<li><a href="#">Supplements</a></li>
<li><a href="#">Vitamins</a></li>
<li><a href="#">Diet & Nutrition</a></li>
<li><a href="#">Tea & Coffee</a></li>
</ul>
</div>
<div class="col-md-6 col-lg-3">
<div class="block-5 mb-5">
<h3 class="footer-heading mb-4">Contact Info</h3>
<ul class="list-unstyled">
<li class="address">203 Fake St. Mountain View, San Francisco, California, USA</li>
<li class="phone"><a href="tel://23923929210">+2 392 3929 210</a></li>
<li class="email">emailaddress@domain.com</li>
</ul>
</div>
</div>
</div>
<div class="row pt-5 mt-5 text-center">
<div class="col-md-12">
<p>
<!-- Link back to Colorlib can't be removed. Template is licensed under CC BY 3.0. -->
Copyright ©
<script>document.write(new Date().getFullYear());</script> All rights reserved | This template is made
with <i class="icon-heart" aria-hidden="true"></i> by <a href="https://colorlib.com" target="_blank"
class="text-primary">Colorlib</a>
<!-- Link back to Colorlib can't be removed. Template is licensed under CC BY 3.0. -->
</p>
</div>
</div>
</div>
</footer>
</div>
<script src="{% static 'js/jquery-3.3.1.min.js' %}"></script>
<script src="{% static 'js/jquery-ui.js' %}"></script>
<script src="{% static 'js/popper.min.js' %}"></script>
<script src="{% static 'js/bootstrap.min.js' %}"></script>
<script src="{% static 'js/owl.carousel.min.js' %}"></script>
<script src="{% static 'js/jquery.magnific-popup.min.js' %}"></script>
<script src="{% static 'js/aos.js' %}"></script>
<script src="{% static 'js/main.js' %}"></script>
<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>
</body>
</html>
The javascript included above is responsible for updating the Snipcart buy button when a custom field selection is made on the page. This code updates the button data attributes when the select value changes.
Live demo
See the live demo
Final thoughts about the project
Wagtail is simple, intuitive and minimal. I think wagtail is a great headless CMS, and I have enjoyed my little venture with Django, Wagtail CMS, and Bootstrap. LEt me know what you think about Wagtail in the comment section!
Posted on February 6, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
February 6, 2022