Django
Online Shop With Django Part III

Online Shop With Django Part III

When a shopping cart is checked out, we need to save an order in the database. Orders will contain information about customers and the products they are buying.

Table of contents

Create orders app

Create a new application for managing customer orders using the following command:

#terminal
python manage.py startapp orders

Edit settings.py file and add the new app.

# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'myshop',
    'cart',
    'orders',
]

Creating order models

We need a model to store the order details and a second model to store items bought, including their price and quantity. Edit the models.py file of the orders app and add the following code to it.

from django.db import models
from myshop.models import Product

# Create your models here.
class Order(models.Model):
    first_name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)
    email = models.EmailField()
    address = models.CharField(max_length=250)
    postal_code = models.CharField(max_length=20)
    city = models.CharField(max_length=100)
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)
    paid = models.BooleanField(default=False)


    class Meta:
        ordering = ['-created']
        indexes = [models.Index(fields=['-created']),]

    def __str__(self):
        return f'Order {self.id}'
    
    def get_total_cost(self):
        return sum(item.get_cost() for item in self.items.all())
    

class OrderItem(models.Model):
    order = models.ForeignKey(Order,related_name='items',on_delete=models.CASCADE)
    product = models.ForeignKey(Product,
                                related_name='order_items',
                                on_delete=models.CASCADE)
    price = models.DecimalField(max_digits=10,decimal_places=2)
    quantity = models.PositiveIntegerField(default=1)

    def __str__(self):
        return str(self.id)
    
    def get_cost(self):
        return self.price * self.quantity

The Order model contains several fields to store customer information and a paid Boolean field, which default to False. Later on, we are going to use this field to differentiate between paid and unpaid orders. We have also defined a get_total_cost() method to obtain the total cost of the items bought in this order.

The OrderItem model allows us to store the product, quantity and price paid for each item. We have defined a get_cost() method that returns the cost of the item by multiplying the item price with the quantity.

Make the migrations

#terminal
python manage.py makemigrations orders
python manage.py migrate

Including order models in the administration site

Let us add the order models to the administration site. Edit the admin.py file of the orders application and add the following code highlighted in bold.

from django.contrib import admin
from .models import Order, OrderItem


class OrderItemInline(admin.TabularInline):
    model = OrderItem
    raw_id_fields = ['product']


@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
    list_display = ['id', 'first_name', 'last_name', 'email',
                    'address', 'postal_code', 'city', 'paid',
                    'created', 'updated']
    list_filter = ['paid', 'created', 'updated']
    inlines = [OrderItemInline]

We use a ModelInline class for the OrderItem model to include it as an inline in the OrderAdmin class. An inline allows us to include a model on the same page as its related model.

Run the server. Visit http://127.0.0.1:8000/admin/orders/order/add/

Creating customer orders

We will use the order models that we created to persist the items contained in the shopping cart when the user finally places an order. A new order will be created following these steps:

  • Present a user with an order form to fill their data
  • Create a newOrder instance with the data entered, and create an associated OrderItem instance for each in the cart.
  • Clear all the cart’s contents and redirect the user to a success page.

Create orders/forms.py to create a form to enter the order details.

from django import forms
from .models import Order

class OrderCreateForm(forms.ModelForm):
    class Meta:
        model = Order
        fields = ['first_name','last_name','email',
                  'address','postal_code','city']
        

This is the form that we are going to use to create new Order objects. Now we need a view to handle the form and create a new order.

Add the following codes to orders/views.py

from django.shortcuts import render
from .models import OrderItem
from .forms import OrderCreateForm
from cart.cart import Cart

# Create your views here.
def order_create(request):
    cart = Cart(request)
    if request.method == 'POST':
        form = OrderCreateForm(request.POST)
        if form.is_valid():
            order = form.save()
            for item in cart:
                OrderItem.objects.create(order=order,
                                         product=item['product'],
                                         price=item['price'],
                                         quantity=item['quantity'])
                
            #Clear the cart
            cart.clear()

        return render(request,'orders/order/created.html',{'order':order})
    else:
        form = OrderCreateForm()

    return render(request,'orders/order/create.html',
                  {'cart':cart,'form':form})

In the order_create view, we obtain the current cart from the session with cart = Cart(request). Depending on the request method, we perform the following tasks:

  • GET request: Instantiates the OrderCreateForm form and renders the orders/order/create.html template.
  • POST request: Validates the data sent in the request. If the data is valid, we create a new order in the database using order = form.save(). We iterate over the cart items and create an OrderItem for each of them. Finally, we clear the cart’s contents and render the template orders/order/created.html.

Create a new file inside the orders application directory and name it urls.py. Add the following code to it:

from django.urls import path
from . import views

app_name = "orders"

urlpatterns = [
    path('create/',views.order_create,name='order_create'),
]

Edit the urls.py of the project.

"""
URL configuration for shop project.

The `urlpatterns` list routes URLs to views. For more information please see:
    https://docs.djangoproject.com/en/4.2/topics/http/urls/
Examples:
Function views
    1. Add an import:  from my_app import views
    2. Add a URL to urlpatterns:  path('', views.home, name='home')
Class-based views
    1. Add an import:  from other_app.views import Home
    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
Including another URLconf
    1. Import the include() function: from django.urls import include, path
    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
"""
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('cart/',include('cart.urls',namespace='cart')),
    path('orders/',include('orders.urls',namespace='orders')),
    path("",include('myshop.urls',namespace='myshop')),
]

if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL,document_root=settings.MEDIA_ROOT)

Edit cart/detail.html template of the cart application and find the following line.

<a href="#" class="button">
            Checkout
        </a>

Replace above code with the following code.

  <a href="{% url "orders:order_create" %}" class="button">
            Checkout
        </a>

Templates for orders

Create the following file structure inside the orders application directory.

Edit orders/order/create.html as follows:

{% extends "shop/base.html" %}
{% block title %}
Checkout
{% endblock %}
{% block content %}
    <h1>
        Checkout
    </h1>
    <div class="order-info">
        <h3>
            Your order
        </h3>
        <ul>
            {% for item in cart %}
            <li>
                {{ item.quantity }}x{{ item.product.name }}
                <span>${{ item.total_price }}</span>
            </li>
            {% endfor %}
        </ul>
        <p>
            Total: $ {{ cart.get_total_price }}
        </p>
    </div>
    <form method="post" class="order-form">
        {{ form.as_p }}
        <p>
            <input type="submit" value="Place order">
        </p>
        {% csrf_token %}
    </form>
    {% endblock %}
    

Edit the orders/order/created.html template and add the following code:

{% extends "shop/base.html" %}
{% block title %}
Thank you
{% endblock %}

{% block content %}
    <h1>
        Thank You
    </h1>
    <p>
        Your order has been successfully completed. Your order number is
        <strong>
            {{ order.id }}
        </strong>
    </p>
{% endblock %}

Run the server and select some items.

Checkout now.

We see on the top right corner, Your cart is empty. To remove it in this stage:

Edit shop/base.html as follow.

{% with total_items=cart|length %}
                {% if total_items > 0 %}
                    Your cart:
                    <a href="{% url "cart:cart_detail" %}">
                        {{ total_items }} item{{ total_items|pluralize }}
                        ${{ cart.get_total_price }}
                    </a>
                {% elif not order %}
                    Your cart is empty 
                {% endif %}

            {% endwith %}

Also visit http://127.0.0.1:8000/admin/orders/order/ to see the order.