Django
Online Shop With Django Part IV

Online Shop With Django Part IV

In this part we will learn to edit the admin site of Django and add an option to exports the orders as csv files. We will also learn to use WeasyPrint to create pdf files dynamically.

Table of contents

Exporting orders to CSV files

Sometimes, we might want to export the information contained in a model to a file so that we can import it into another system. One of the most widely used formats to export/import data is Comma-Separated Value (CSV).

Adding custom actions to the administration site

Django offers a wide range of options to customize the administration site. We are going to modify the object list view to include a custom administration action. We can implement custom administration actions to allow staff users to apply actions to multiple elements at once in the change list view.

We can create a custom action by writing a regular function that receives the following parameters.

  • The current ModelAdmin displayed
  • The current request object as an HttpRequest instance
  • A QuerySet for the objects selected by the user

We are going to create a custom administration action to download a list of orders as a CSV file.

Edit admin.py file of the orders application and add the following code before the OrderAdmin class.

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

import csv
import datetime
from django.http import HttpResponse


def export_to_csv(modelamdin,request,queryset):
    opts = modelamdin.model._meta
    content_disposition = f'attachment; filename={opts.verbose_name}.csv'
    response = HttpResponse(content_type='text/csv')
    response['Content-Disposition'] = content_disposition
    writer = csv.writer(response)
    fields = [field for field in opts.get_fields() if not field.many_to_many and not field.one_to_many]

    #Write a first row with header information
    writer.writerow([field.verbose_name for field in fields])

    #write data rows
    for obj in queryset:
        data_row = []
        for field in fields:
            value = getattr(obj,field.name)
            if isinstance(value,datetime.datetime):
                value = value.strftime('%d/%m/%Y')
            data_row.append(value)

        writer.writerow(data_row)

    return response

export_to_csv.short_description = 'Export to csv'

In the above code, we perform the following task.

  • We create an instance of HttpResponse, specifying the text/csv content type, to tell the browser that the response has to be treated as a CSV file. We also add a Content-Disposition header to indicated that the HTTP response contains an attached file.
  • We create a CSV write object that will write to the response object.
  • We get the model fields dynamically using the get_fields() method of the model’s _meta options. We exclude many-to-many and one-to-many relationships.
  • We write a header row including the field names.
  • We iterate over the given QuerySet and write a row for each object returned by the QuerySet. We take care of formatting datetime objects because the output value for CSV has to be a string.
  • We customize the display name for the action in the actions drop-down element of the administration site by setting a short_description attribute on the function.

We have created a generic administration action that can be added to any ModelAdmin class.

Finally, add the new export_to_csv administration action to the OrderAdmin class, as follows.

Edit orders/admin.py as follows


@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]
    actions = [export_to_csv]

Run the server.

Visit http://127.0.0.1:8000/admin/orders/order/

We can see “Export to csv” as option.

After clicking on go we get a csv file.

Extending the administration site with custom views

We want to implement additional functionalities that are not available in existing administration views or templates. If this is the case, we need to create a custom administration view. With a custom view, we can build any functionality we want; we just have to make sure that only staff users can access our view and that we maintain the administration look and feel by making our template extend an administration template.

Let us create a custom view display information about an order. Edit the orders/views.py file of the orders application.

from django.shortcuts import render,get_object_or_404
from django.contrib.admin.views.decorators import staff_member_required

from .models import OrderItem, Order
from .forms import OrderCreateForm
from cart.cart import Cart

..............................................

@staff_member_required
def admin_order_detail(request,order_id):
    order = get_object_or_404(Order,id=order_id)
    return render(request,'admin/orders/order/detail.html',
                  {'order':order})

The staff_member_required decorator checks that both the is_active and is_staff fields of the user requesting the page are set to True. In this view, we get the Order object with the given ID and render a template to display the order.

Next, edit the orders/urls.py file of the orders application and add the following URL pattern highlighted in bold.

from django.urls import path
from . import views

app_name = "orders"

urlpatterns = [
    path('create/',views.order_create,name='order_create'),
    path('admin/order/<int:order_id>/',views.admin_order_detail,name='admin_order_detail'),
]

Create the following file structure inside the templates directory.

Edit the detail.html as follow.

{% extends "admin/base_site.html" %}
{% block title %}
    Order {{order.id}} {{ block.super }}
{% endblock %}

{% block breadcrumbs %}
    <div class="breadcrumbs">
        <a href="{% url "admin:index"%}">Home</a> &rsaquo;
        <a href="{% url "admin:orders_order_changelist" %}">
            Orders
        </a> &rsaquo;
        <a href="{% url "admin:orders_order_change" order.id %}">
            Order {{ order.id }}
        </a>
        &rsaquo; Detail
    </div>
{% endblock %}

{% block content %}
    <div class="module">
        <h1>Order {{ order.id }}</h1>
        <ul class="object-tools">
            <li>
                <a href="#" onclick="window.print();">
                Print order
                </a>
            </li>
        </ul>
        <table>
            <tr>
                <th>Created</th>
                <td>{{ order.created }}</td>
            </tr>
            <tr>
                <th>Customer</th>
                <td>{{ order.first_name }} {{ order.last_name }}</td>
            </tr>
            <tr>
                <th>E-Mail</th>
                <td><a href="mailto:{{ order.email }}">
                    {{ order.email }}
                </a></td>
            </tr>

            <tr>
                <th>Addrress</th>
                <td>
                    {{ order.address}},{{order.postal_code }} {{ order.city }}
                </td>

            </tr>
            <tr>
                <th>Total amount </th>
                <td>${{ order.get_total_cost }}</td>
            </tr>

            <tr>
                <th>Status</th>
                <td>{%if order.paid %}
                    Paid
                    {% else %}
                    Pending payment
                    {% endif %}
                </td>
            </tr>
        </table>
    </div>

    <div class="module">
        <h2>Items bought</h2>
        <table style="width: 100%;">
            <thread>
                <tr>
                    <th>Product</th>
                    <th>Price</th>
                    <th>Quantity</th>
                    <th>Total</th>
                </tr>

            </thread>
            <tbody>
                {% for item in order.items.all %}
                    <tr class="row{% cycle "1" "2" %}">
                        <td>{{ item.product.name }}</td>
                        <td class="num">${{ item.price }}</td>
                        <td class="num">{{ item.quantity }}</td>
                        <td class="num">${{ item.get_cost }}</td>
                    </tr>
                {% endfor %}

                <tr class="total">
                    <td colspan="3">Total</td>
                    <td class="num">
                        ${{ order.get_total_cost }}
                    </td>
                </tr>
            </tbody>
        </table>
    </div>
{% endblock %}

The above template extends admin/base_site.html template of Django’s administration site, which contains the main HTML structure and CSS styles. We use the blocks defined in the parent template to include our own content. We display information about the order and the items bought.

Add the following code to orders/admin.py

from django.contrib import admin
from django.urls import reverse
from django.utils.safestring import mark_safe

from .models import Order, OrderItem

import csv
import datetime
from django.http import HttpResponse


...........................................................



def order_detail(obj):
    url = reverse('orders:admin_order_detail',args=[obj.id])
    return mark_safe(f'<a href="{url}">View</a>')

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

Run the server and visit http://127.0.0.1:8000/admin/orders/order/

Click on View

Click on Print Order to print.

Generating PDF invoices dynamically

Install WeasyPrint

#terminal
pip install WeasyPrint==56.1

Creating a PDF template

We need an HTML document as input for WeasyPrint. We are going to create an HTML template, render it using Django and pass it to WeasyPrint to generate the PDF file.

Create a new template file inside the templates/orders/order directory of the orders application and name it pdf.html.

Add the following code to it.

<html>
    <body>
        <h1>My Shop</h1>
        <p>
            Invoice no. {{ order.id }}<br>
            <span class="secondary">
                {{ order.created|date:"M d, Y" }}
            </span>
        </p>
        <h3>Bill to</h3>
        <p>
            {{order.first_name }} {{ order.last_name }} <br>
            {{ order.email }} <br>
            {{ order.address }} <br>
            {{ order.postal_code }},{{ order.city }}
        </p>
        <h3>Items bought</h3>
        <table>
            <thead>
                <tr>
                    <th>Product</th>
                    <th>Price</th>
                    <th>Quantity</th>
                    <th>Cost</th>
                </tr>

            </thead>
            <tbody>
                {% for item in order.items.all %}
                <tr class="row{% cycle "1" "2" %}">
                    <td>{{ item.product.name }}</td>
                    <td class="num">${{ item.price }}</td>
                    <td class="num">${{ item.quantity }}</td>
                    <td class="num">${{ item.get_cost }}</td>
                
                </tr>
                    
                {% endfor %}
                <tr class="total">
                    <td colspan="3">Total</td>
                    <td class="num">${{ order.get_total_cost }}</td>
                </tr>
            </tbody>
        </table>
        <span class="{% if order.paid %} paid {% else %} pending {% endif %}">
            {% if order.paid %}
            Paid
            {% else %}
            Pending payment
            {% endif %}
        </span>
    </body>
</html>

This is the template for the PDF invoice. In this template, we display all order details and an HTML <table> element including the products. We also include a message to display whether the order has been paid.

Rendering PDF files

We are going to create a view to generate PDF invoices for existing orders using the administration site. Edit the orders/views.py file inside the orders application directory and add the following code to it.

from django.shortcuts import render,get_object_or_404
from django.contrib.admin.views.decorators import staff_member_required
from django.conf import settings
from django.http import HttpResponse
from django.template.loader import render_to_string

import weasyprint

from .models import OrderItem, Order
from .forms import OrderCreateForm
from cart.cart import Cart


@staff_member_required
def admin_order_pdf(request,order_id):
    order = get_object_or_404(Order,id=order_id)
    html = render_to_string('orders/order/pdf.html',
                            {'order':order})
    response = HttpResponse(content_type='application/pdf')
    response['Content-Disposition'] = f'filename=order_{order.id}.pdf'
    weasyprint.HTML(string=html).write_pdf(response,stylesheets=[weasyprint.CSS(
        settings.STATIC_ROOT / 'css/pdf.css'
    )])
    return response

This is the view to generate a PDF invoice for an order. We use the staff_member_required decorator to make sure only staff users can access this view.

We get the Order object with the given ID and we use the render_to_string() function provided by Django to render orders/order/pdf.html. The rendered HTML is saved in the html variable.

Add the following line of code to settings.

STATIC_ROOT = BASE_DIR / 'static'

Run the following command.

#terminal
python manage.py collectstatic

The collectstatic command copies all static files from our applications into the directory defined in the STATIC_ROOT setting. This allows each application to provide its own static files using a static/ directory containing them.

Edit the orders/urls.py and add the following url pattern.

from django.urls import path
from . import views

app_name = "orders"

urlpatterns = [
    path('create/',views.order_create,name='order_create'),
    
    path('admin/order/<int:order_id>/',views.admin_order_detail,name='admin_order_detail'),
    path('admin/order/<int:order_id>/pdf/',
         views.admin_order_pdf,name='admin_order_pdf'),
]

Edit the orders/admin.py file and add the following code.

from django.contrib import admin
from django.urls import reverse
from django.utils.safestring import mark_safe

from .models import Order, OrderItem

import csv
import datetime
from django.http import HttpResponse


.................................................................
def order_detail(obj):
    url = reverse('orders:admin_order_detail',args=[obj.id])
    return mark_safe(f'<a href="{url}">View</a>')

def order_pdf(obj):
    url = reverse('orders:admin_order_pdf',args=[obj.id])
    return mark_safe(f'<a href="{url}">PDF</a>')
order_detail.short_description = "Invoice"

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

Add the following codes to shop/static/css/pdf.css

body {
    font-family:Helvetica, sans-serif;
    color:#222;
    line-height:1.5;
}

table {
    width:100%;
    border-spacing:0;
    border-collapse: collapse;
    margin:20px 0;
}

table th, table td {
    text-align:left;
    font-size:14px;
    padding:10px;
    margin:0;
}

tbody tr:nth-child(odd) {
    background:#efefef;
}

thead th, tbody tr.total {
    background:#5993bb;
    color:#fff;
    font-weight:bold;
}

h1 {
    margin:0;
}


.secondary {
    color:#bbb;
    margin-bottom:20px;
}

.num {
    text-align:right;
}

.paid, .pending {
    color:#1bae37;
    border:4px solid #1bae37;
    text-transform:uppercase;
    font-weight:bold;
    font-size:22px;
    padding:4px 12px 0px;
    float:right;
    transform: rotate(-15deg);
    margin-right:40px;
}

.pending {
    color:#a82d2d;
    border:4px solid #a82d2d;
}

Run the server and create a new order:

Chceckout

Click on PDF