In this chapter we will build a dashboard for Booktime company employees on top of the Django admin interface. We will discuss why and how to do this, along with different types of users in the company.
Configuring the admin interface
Adding admin views
Configuring users and permissions
Creating internal reports
Generating PDFs
Reasons to Use Django admin
In this chapter we are using the Django admin interface to demonstrate how customizable this app is. With only some basic customizations we are already able to manage our products and order data, as we have seen in the previous chapters. We are also already able to filter and search for products and orders in our system.
The Django admin interface comes with a built-in authentication and permission system. You can easily configure multiple users to be able to see and change only parts of your data. This interface also has a built-in log to track who changed what models in the database.
This app allows us to get an adequate state without much effort. In this chapter we will continue building upon the customizations that we already did by creating new views integrated in the admin interface, modifying the permissions of the users, integrating reporting functions, and customizing its look.
All of this is possible by mostly overriding the base classes, although this approach has limits. Given that, when customizing the admin interface, we are always overriding built-in behavior with additional code, it is advisable to not overcustomize it because your code will quickly become hard to read.
Another limitation of this approach is that you cannot fundamentally alter the user flow of the app. Just like the choice between class-based views and function-based views, if you spend more time overriding built-in behavior than codifying your own, customizing the admin interface is not the right approach.
In this chapter we will try to stretch this interface to its limits to achieve support for all the standard operations an e-commerce company should be able to do, or at least for the ones that our fictitious book-selling company requires.
Views in the admin interface
Index view : The initial page, which lists all the Django apps and their models
App list view : The list of models of a single Django app
Change list view : The list of all entries for a Django model
Change view : A view to change a single entity of a Django model
Add view : A view to add a new entity of a Django model
Delete view : A confirmation view to delete a single entity of a Django model
History view : A list of all changes done through Django admin interface of a single entity
Support views : Login, logout, and change password views
Index view: admin/index.html
App list view: admin/app_index.html
Change list view: admin/change_list.html
Change item view: admin/change_form.html
Add item view: admin/change_form.html
Delete item view on one item: admin/delete_confirmation.html
Delete item view on multiple items: admin/delete_selected_confirmation.html
History view: admin/object_history.html
There are many more templates that represent specific sections of the screen of some of these views. I encourage you to explore these templates to understand their structure. You can find them in your Python virtualenv or online on GitHub1.
Django admin interface comes with a built-in set of views, but you can add new views. You can define views both at the top level and at the model level. The new views will inherit all the security checks and URL namespacing of the correspondent admin instance. This makes it possible to add, for instance, all the reporting views to our admin instance, with the proper authorization checks in place.
In addition to all the preceding features, it is possible to have multiple Django admin interfaces running on one site, each with its own customizations. Up to this point we have used django.contrib.admin.site, which is an instance of django.contrib.admin.AdminSite, but nothing stops us from having many instances of it.
Configuring User Types and Permissions for the Company
- Owners
Can see and operate on all useful models
- Central office employees
Can flag orders as paid
Can change order data
Can see reports about the site’s performance
Can manage products and related information
- Dispatch office
Can flag order lines as shipped (or canceled)
Can flag products as out of stock
Owners : Any user for whom the is_superuser field is set to True
Central office employees : Any user belonging to the “Employees” group
Dispatch office : Any user belonging to the “Dispatchers” group
Implementing Multiple admin interfaces for Users
That’s a lot of code! First of all, there are three instances of Django admin, one for each type of user we declared in the previous section. Each instance has a different set of models registered, depending on what is relevant for that type of user.
The Django admin sites will be color-coded. Colors are injected through some custom CSS. The owners’ and central office’s admin interfaces have also some extra views for reporting. The extra views are inserted in three steps: the actual view (orders_per_day), the URL mapping (in get_urls()), and the inclusion in the index template (index()).
Specifically to DispatchersAdminSite , we have special a version of ModelAdmin for Product and Order. DispatchersOrderAdmin overrides the get_queryset() method because the dispatch office only needs to see the orders that have been marked as paid already. On those, they only need to see the shipping address.
For anyone else other than owners, we are also limiting the ability to change the slugs because they are part of the URLs. If they were changed, Google or any other entity that links to our site would have broken links.
In the code we have seen above the index view had some extra template variables. To display those, a new template is required. We will place this file in templates/admin/index.html, and it will be a customization of a built-in admin template.
We now have the three dashboards that we want to give to our internal team. Please go ahead and open the URLs in your browser, after having logged in as a superuser. Bear in mind that the reporting section is not finished yet.
In the next section we will talk more about reporting with the Django ORM. The code was included above (orders_per_day()), but given how important it is, it deserves to be explained in its own section.
Reporting on Orders
When it comes to reporting, SQL queries tend to become a bit more complicated, using aggregate functions, GROUP BY clauses, etc. The purpose of the Django ORM is to map database rows to Model objects. It can be used to do reports, but that is not its primary function. This can lead to some difficult-to-understand ORM expressions, so be warned.
In Django, there are two classes of aggregation: one that acts on all entries in a QuerySet and another that acts on each entry of it. The first uses the aggregate() method , and the second annotate(). Another way to explain it is that aggregate() returns a Python dictionary, while annotate() returns a QuerySet where each entry is annotated with additional information.
There is an exception to this rule, and that is when the annotate() function is used with values(). In that case, annotations are not generated for each item of the QuerySet, but rather on each unique combination of the fields specified in the values() method .
When in doubt, you can see the SQL that the ORM is generating by checking the property query on any QuerySet.
The next few sections present some reports, and break down the ORM queries.
Numbers of Orders Per Day
Creates a temporary/annotated day field, populating it with data based on the date_added field
Uses the new day field as a unit for aggregation
Counts orders for specific days
The preceding query includes two annotate() calls. The first acts on all rows in the order table. The second, instead of acting on all rows, acts on the result of the GROUP BY clause, which is generated by the values() call.
In the template we are using an open source library called Chart.js . Using a charting library for reporting views is a common theme, and you should familiarize yourself with a few charting libraries and the format they require the data to be in.
Viewing the Most Bought Products
We are going to add another view that shows what are the most bought products. Differently from orders_per_day(), we are going to do this with a bit more customization, and with integration tests, to show that you can apply the same concepts of normal views to admin views.
As you can see, we can use forms inside this view. We created a simple form to select how far back we want the report for.
Bulk Updates to Products
In the Django admin interface it is possible to apply actions in bulk. An example of this is deletion: from the change list view we can select multiple items and then select the delete action from the drop-down menu at the top of the table.
These are called “actions,” and it is possible to add custom actions to specific instances of ModelAdmin. We are going to add a couple to mark products as active or inactive.
As you can see, it is a very simple change. The result of this can be seen in the product list page by clicking the drop-down button on the left before the column names, as shown in Figure 3-5.
Printing Order Invoices (As PDFs)
The last thing we are going to tackle is a common occurrence for e-commerce shops: printing invoices. In Django there is no facility to generate PDFs, so we will need to install a third-party library.
There are multiple Python PDF libraries available online; in our case we will choose WeasyPrint . This library allows us to create PDFs out of HTML pages, and that is how we will start here. If you want more flexibility, perhaps you should rely on a different library.
WeasyPrint requires a couple of system libraries installed in the system: Cairo and Pango. They are both used for rendering the document. You can install those with your package manager. You also require the fonts required to render the CSS properly.
This Django view has two rendering modes, HTML and PDF. Both modes use the same invoice.html template, but in the case of PDF, WeasyPrint is used to post-process the output of the templating engine.
When generating PDFs, instead of using the normal render() call, we use the render_to_string() method and store the result in memory. The PDF library will then use this to generate the PDF body, which we will store in a temporary file. In our case, the temporary file will be deleted, but we could persist this in a FileField if we wanted to.
At this point, please go ahead and try to retrieve and view the PDF. If it does not generate correctly, you may have to go on the WeasyPrint forums and work out why. You will find most times that the issue is a missing dependency in your system.
Testing Invoice Generation
The last thing this functionality needs is a test. We want to make sure that, given an order with some specific data, the results for both HTML and PDF versions are exactly what we expect.
Summary
This chapter was a deep dive into the Django admin interface. We saw how customizable this app can be. We also talked about the dangers of pushing this app too far: if the user flow we want to offer is different from the simple create/edit/delete approach, customizing the admin may not be worth it, and instead adding custom views outside of the admin may be better.
We also talked about reporting and how to structure ORM queries for this. Django documentation goes a lot deeper on this, and I encourage you to research it for more advanced queries.
We also covered PDF generation. In our case, this is done only for people in the back office. Some sites offer invoice generation directly available to website users. In that case, it would be easy to adapt the code in this chapter to offer it in a normal (non-admin) view.
In the next chapter we will talk about an extension of Django called Channels, and how we can use this to build a chat page to interact with our customers.