Online Shop With Django Part I
We will learn how to create a web application for online shop with Django.
In this first part we will do basic installation. Then create models for product and catalog. Then register the models to admin site. After the we will build views and templates. At the end of this part we will add css.
Table of Contents
- Basic Installation
- Creating a product and catalog models
- Registering catalog models on the administration site
- Building catalog views
- Creating catalog templates
- Adding css
Basic Installation
#Terminal
mkdir Django_OnlineShop
cd Django_OnlineShop
python3 -m venv venv
sourch venv/bin/activate
pip3 install django==4.2.1
django-admin startproject shop
cd shop
django-admin startapp myshop
python manage.py migrate
python manage.py runserver
Visit http://127.0.0.1:8000/
Creating a product and catalog models
The catalog of our shop will consists of products that are organized into different categories. Each product will have a name, an optional description, an optional image, a price and its availability.
Edit the my_shop/model.py of the shop application as follows:
from django.db import models
# Create your models here.
class Category(models.Model):
name = models.CharField(max_length=200)
slug = models.SlugField(max_length=200,unique=True)
class Meta:
ordering = ['name']
indexes = [models.Index(fields=['name']),]
verbose_name = 'category'
verbose_name_plural = 'categories'
def __str__(self):
return self.name
class Product(models.Model):
category = models.ForeignKey(Category,related_name='products',on_delete=models.CASCADE)
name = models.CharField(max_length=200)
slug = models.SlugField(max_length=200)
image = models.ImageField(upload_to='products/%Y/%m/%d',blank=True)
description = models.TextField(blank=True)
price = models.DecimalField(max_digits=10,decimal_places=2)
available = models.BooleanField(default=True)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['name']
indexes = [models.Index(fields=['id','slug']),
models.Index(fields=['name']),
models.Index(fields=['-created']),
]
def __str__(self):
return self.name
These are the Category and Product models. The Category model consists of a name field and a unique slug field (unique implies the creation of an index). In the Meta class of the Category model, we have defined an index for the name field.
The Product model fields are as follows:
- category : A ForeginKey to the Category model. This is one-to-many relationship: a product belongs to one category and a category contains multiple products.
- name: The name of the product.
- slug: The slug for this product to build beautiful URLs.
- image: An optional product image.
- description: An optional description of the product.
- price: This field uses Python’s decimal. Decimal type to store a fixed-precision decimal number.
- available: A Boolean value that indicates whether the product is available or not. It will be used to enable/disable the product in the catalog.
- created : This filed stores when the object was created.
- updated: This field stores when the object was last updated.
In the Meta class of the product model, we have defined a multiple-field index for the id and slug fields. Both fields are indexed together to improve performance for queries that utilize the two fields.
We plan to query products by both id and slug. We have added an index for the name and an index for the created field. We have used a hyphen before the field name to define the index with a descending order.
Since we are going to deal with images in our models we need to install Pillow library.
#terminal
pip install Pillow
Now, make migrations.
#Terminal
python manage.py makemigrations
python manage.py migrate
Registering catalog models on the administration site
Edit the myshop/admin.py file to add our models to the administration site.
from django.contrib import admin
from .models import Category, Product
# Register your models here.
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
list_display = ['name','slug']
prepopulated_fields = {'slug':('name',)}
@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
list_display = ['name','slug','price','available','created','updated']
list_filter = ['available','created','updated']
list_editable = ['price','available']
prepopulated_fields = {'slug':('name',)}
Remember that we use the prepopulated_fields attribute to specify fields where the value is automatically set using the value of other fields.
We use the list_editable attribute in the ProductAdmin class to set the fields that can be edited from the list display page of the administration site. This will allow us to edit multiple rows at once. Any field in list_editable must also be listed in the list_display attribute, since only the fields displayed can be edited.
Create the super user.
python manage.py createsuperuser
Visit http://127.0.0.1:8000/admin/
Login.
Building catalog views
In order to display the product catalog, we need to create a view to list all the products or filter products by a given category. Edit the myshop/views.py file of the shop application and add the following code.
from django.shortcuts import render,get_object_or_404
from .models import Category,Product
# Create your views here.
def product_list(request,category_slug=None):
category = None
categories = Category.objects.all()
products = Product.objects.filter(availabe=True)
if category_slug:
category = get_object_or_404(Category,
slug=category_slug)
products = products.filter(category = category)
return render(request,'shop/product/list.html',
{'category':category,
'categories':categories,
'products':products})
In above code, we filter the QuerySet with available=True to retrieve only available products. We use an optional category_slug parameter to optionally filter products by a given category.
We also need to have a view for single product. Add the following function to myshop/views.py.
def product_detail(request,id,slug):
product = get_object_or_404(Product,id=id,slug=slug,available=True)
return render(request,'shop/product/detail.html',{'product':product})
The product_detail view expects the id and slug parameters in order to retrieve the Product instance. We can get this instance just through the ID, since it’s unique. However, we include the slug in the URL to build SEO-friendly URLs for products.
After building the product list and detail views, we have to define URL patterns for them. Create myshop/urls.py and add the following code to it.
from django.urls import path
from . import views
app_name = 'myshop'
urlpatterns = [
path('',views.product_list,name="product_list"),
path("<slug:category_slug>/",views.product_list,name='product_list_by_category'),
path("<int:id>/<slug:slug>/",views.product_detail,name="product_detail"),
]
Edit the urls.py file of the shop project to include the urls of our myshop app.
from django.contrib import admin
from django.urls import path,include
urlpatterns = [
path('admin/', admin.site.urls),
path("",include('myshop.urls',namespace='myshop')),
]
Import reverse() function and add a get_absolute_url() method to the Category and Product models by editing myshop/models.py.
from django.db import models
from django.urls import reverse
# Create your models here.
class Category(models.Model):
name = models.CharField(max_length=200)
slug = models.SlugField(max_length=200,unique=True)
class Meta:
ordering = ['name']
indexes = [models.Index(fields=['name']),]
verbose_name = 'category'
verbose_name_plural = 'categories'
def get_absolute_url(self):
return reverse("myshop:product_list_by_category",args=[self.slug])
def __str__(self):
return self.name
class Product(models.Model):
category = models.ForeignKey(Category,related_name='products',on_delete=models.CASCADE)
name = models.CharField(max_length=200)
slug = models.SlugField(max_length=200)
image = models.ImageField(upload_to='products/%Y/%m/%d',blank=True)
description = models.TextField(blank=True)
price = models.DecimalField(max_digits=10,decimal_places=2)
available = models.BooleanField(default=True)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['name']
indexes = [models.Index(fields=['id','slug']),
models.Index(fields=['name']),
models.Index(fields=['-created']),
]
def get_absolute_url(self):
return reverse('myshop:product_detail',args=[self.id,self.slug])
def __str__(self):
return self.name
Creating catalog templates
Create following files and folders inside the myshop app.
Add the following lines of codes to base.html.
{% load static %}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>
{% block title %}
My Shop
{% endblock %}
</title>
<link href="{% static "css/base.css"%}" rel="stylesheet">
</head>
<div id="header">
<a href="/" class="logo">My shop</a>
</div>
<div id="subheader">
<div class="cart">
Your cart is empty
</div>
</div>
<div id="content">
{% block content %}
{% endblock %}
</div>
</html>
Now add following lines of code to shop/product/list.html.
{% extends "shop/base.html" %}
{% load static %}
{% block title %}
{% if category %}{{ category.name }}
{% else %}
Products
{% endif %}
{% endblock %}
{% block content %}
<div id="sidebar">
<h3>
Categories
</h3>
<ul>
<li {% if not category %}class="selected"{% endif %}>
<a href="{% url "myshop:product_list" %}">
All
</a>
</li>
{% for c in categories %}
<li {% if category.slug == c.slug %}class="selected" {% endif %}>
<a href="{{c.get_absolute_url }}">
{{ c.name}}
</a>
</li>
{% endfor %}
</ul>
</div>
<div id="main" class="product_list">
<h1>
{% if category %}{{category.name}}
{%else%}
Products{% endif %}
</h1>
{% for product in products %}
<div class="item">
<a href="{{product.get_absolute_url}}">
{% if product.image %}
<img src="{{ product.image.url}}">
{% else %}
<img src="{%static "img/no_image.png"%}">
{% endif %}
</a>
<a href="{{product.get_absolute_url}}">
{{product.name}}
</a>
<br>
${{product.price}}
</div>
{% endfor %}
</div>
{% endblock %}
This is the product list template. It extends the shop/base.html.
Edit the settings.py file of shop and add the following settings.
MEDIA_URL = "media/"
MEDIA_ROOT = BASE_DIR / "media"
MEDIA_URL is the base URL that servers media files uploaded by users. MEDIA_ROOT is the local path where these files reside, which we build dynamically prepending the BASE_DIR varialbe.
For Django to serve the uploaded media files using the development server, edit the main urls.py file of shop and add the following code.
from django.contrib import admin
from django.urls import path,include
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path('admin/', admin.site.urls),
path("",include('myshop.urls',namespace='myshop')),
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL,document_root=settings.MEDIA_ROOT)
Edit the shop product/detail.html
{% extends "shop/base.html" %}
{% load static %}
{% block title %}
{{ product.name }}
{% endblock %}
{% block content %}
<div class="product-detail">
{% if product.image%}
<img src="{{ product.image.url }}">
{% else %}
<img src="{% static "img/no_image.png"%}">
{% endif %}
<h1>{{ product.name }}</h1>
<h2>
<a href="{{ product.category.get_absolute_url}}">
{{ product.category }}
</a>
</h2>
<p class="price">
${{ product.price }}
</p>
{{ product.description|linebreaks }}
</div>
{% endblock %}
Adding css
Create static/css/base.css
@import url(//fonts.googleapis.com/css?family=Ubuntu);
body {
margin:0;
padding:0;
font-family:helvetica, sans-serif;
}
h1, h2, h3, h4, h5 , h6 {
font-family: 'Ubuntu', sans-serif; font-weight:normal;
}
a {
text-decoration:none;
color:#5993bb;
}
a:hover {
color:#77b6e2;
}
a.button, input[type="submit"] {
color:#fff;
background:#5993bb;
border-radius:4px;
padding:10px 20px;
font-size:14px;
border:0;
}
a.button.light {
color:#666;
background:#ededed;
}
.text-left {
text-align:left;
}
.text-right {
text-align:right;
}
table {
width:100%;
border-spacing:0;
border-collapse: collapse;
}
table th, table td {
text-align:left;
font-size:14px;
padding:10px;
margin:0;
}
tbody tr:nth-child(odd) {
background:#efefef;
}
tbody tr.subtotal {
background:#ccc;
}
thead th, tbody tr.total {
background:#5993bb;
color:#fff;
font-weight:bold;
}
table.cart {
margin-bottom:40px;
}
table.cart img {
width:180px;
}
tbody td.num {
text-align:right;
}
td input[type="submit"] {
font-size:12px;
padding:4px 10px;
}
form p label, form p input {
float:left;
clear:both;
}
form p label {
font-size:14px;
padding-bottom:4px;
}
form p input {
width:300px;
font-size:13px;
margin-bottom:10px;
padding:10px 12px;
border:0;
background:#efefef;
color:#666;
border-radius:4px;
}
ul.errorlist {
float:left;
color:#ff0000;
clear:both;
font-size:13px;
margin:0 0 0 10px;
padding:10px 0;
}
#header {
padding:10px 100px;
font-size:18px;
overflow:auto;
}
#subheader {
background:#ececec;
color:#444;
padding:15px 100px;
overflow:auto;
}
#header .logo {
font-family: 'Ubuntu', sans-serif;
float:left;
color:#333;
font-size:22px;
margin-right:10%;
}
#subheader .cart {
float:right;
padding-top:4px;
}
#content {
padding:0 100px;
overflow:hidden;
}
#sidebar {
width:220px;
padding:30px 20px 0 0;
float:left;
}
#sidebar ul{
margin:0;
padding:0;
}
#sidebar ul li {
margin:0;
list-style:none;
padding:10px 14px;
}
#sidebar ul li a {
display:block;
}
#sidebar ul li.selected {
background:#5993bb;
border-radius:4px;
}
#sidebar ul li.selected a {
color:#fff;
}
#main {
float:left;
width: 96%;
margin-right:-220px;
overflow:hidden;
}
.product-list .item {
width:25%;
padding:10px 20px 10px 0;
float:left;
text-align:center;
line-height:1.2;
}
.product-list .item img {
width:100%;
margin-bottom:8px;
}
.product-detail {
text-align:justify;
}
.product-detail .price {
color:#333;
font-size:28px;
font-weight:bold;
}
.product-detail img {
width:40%;
float:left;
padding:0 20px 20px 0;
}
.product-detail h1 {
margin-bottom:0;
}
.product-detail h2 {
margin-top:10px;
}
.order-form {
float:left;
}
.order-info {
float:right;
width:300px;
background:#efefef;
padding:10px 20px;
color:#333;
border-bottom:4px solid #5993bb;
}
.order-info h3 {
color:#5993bb;
}
.order-info ul li {
margin:0;
padding:0;
font-size:14px;
}
.order-info p {
font-weight:bold;
float:right;
}
.order-info span {
float:right;
}
.neg {
color:#dc1616;
}
.languages {
float:right;
padding-top:4px;
font-size:14px;
}
.languages ul {
list-style:none;
margin:0;
padding:0;
float:left;
}
.languages ul li {
float:left;
margin:0 0 0 10px;
font-size:14px;
}
.languages p {
float:left;
margin:0;
}
.recommendations {
clear:both;
}
.recommendations div {
float:left;
}
.recommendations img {
width:200px
}
.recommendations p {
clear:both;
}
.recommendations.cart {
width:60%;
float:left;
overflow:auto;
}
.recommendations.cart h3 {
margin-top:0;
}
.recommendations.cart .item {
margin-right:10px;
}
.recommendations.cart img {
width:120px;
}
/* braintree hosted fields */
form div.field {
font-size:13px;
color:#666;
width:300px;
height:22px;
margin-bottom:10px;
padding:6px 12px;
border:0;
background:#efefef;
color:#666;
border-radius:4px;
}