Hello, I’ve just started with Django and I’m also new to Python (I have experience in web development with other languages/frameworks).
I started by using the built-in class-based view to list objects (ListView) but I’ve been surprised by the absence of a built-in facility to manage sorting automatically. It is mandatory that it works together with the existing pagination tool.
I don’t want to use an external package to implement such a basic function since Django should be “Batteries-Included”, so I solved writing a Mixin (code and explanation below).
Since I’m a newbie in Django/Python I’d like to hear your opinions about my code.
Thank you for your comments and suggestions!
A simple class to wrap sort field value and direction:
class SortField:
SORT_DIR_ASC = "asc"
SORT_DIR_DESC = "desc"
def __init__(self, sort_by, sort_dir=SORT_DIR_ASC):
self.sort_by = sort_by
self.sort_dir = sort_dir
def __repr__(self):
return f"SortField({self.sort_by}, {self.sort_dir})"
The Mixin:
class SortableListMixin:
default_sort_field = None # override to set default sorting data
q_sort_by = "qSortBy" # query string param name for the field
q_sort_dir = "qSortDir" # query string param name for sort direction (ascending/descending)
def get_default_sort_field(self) -> SortField:
return self.default_sort_field
def get_ordering(self):
sf = self.get_default_sort_field()
orderby = self.request.GET.get(self.q_sort_by, sf.sort_by)
sort_direction = self.request.GET.get(self.q_sort_dir, sf.sort_dir)
# The negative sign in front the value indicates descending order.
if sort_direction == SortField.SORT_DIR_DESC:
orderby = "-" + orderby
return orderby
# keep current sorting url (query string) while paging
# Example:
# <a href="?page={{ page_obj.next_page_number }}&{{ ctx_sort_qstring }}">next</a>
# <a href="?page={{ page_obj.paginator.num_pages }}&{{ ctx_sort_qstring }}">last</a>
def get_context_data(self, **kwargs):
sf = self.get_default_sort_field()
context = super().get_context_data(**kwargs)
sort_by = self.request.GET.get(self.q_sort_by, sf.sort_by)
sort_dir = self.request.GET.get(self.q_sort_dir, sf.sort_dir)
context['ctx_orderby'] = sort_by
context['ctx_sort'] = sort_dir
context['ctx_sort_qstring'] = "&".join(
["=".join([self.q_sort_by, sort_by]), "=".join([self.q_sort_dir, sort_dir])])
return context
ListView exetnsion with MIxin:
class CustomersListView(SortableListMixin, ListView):
template_name = "pages/customers_list.html"
model = Customer
paginate_by = 10
default_sort_field = SortField('lastname')
Interesting part of the template:
<table>
<thead>
<tr>
<th>Lastname
{% if ctx_orderby != "lastname" or ctx_orderby == "lastname" and ctx_sort == "desc" %}
<a href="?qSortBy=lastname&qSortDir=asc">▲</a>
{% endif %}
{% if ctx_orderby == "lastname" and ctx_sort == "asc" %}
<a href="?qSortBy=lastname&qSortDir=desc">▼</a>
{% endif %}
</th>
...omissis...
{% if is_paginated %}
<div class="pagination">
<span class="step-links">
{% if page_obj.has_previous %}
<a href="?page=1&{{ ctx_sort_qstring }}">« first</a>
<a href="?page={{ page_obj.previous_page_number }}&{{ ctx_sort_qstring }}">previous</a>
{% endif %}
<span class="current">
[ Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }} ]
</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}&{{ ctx_sort_qstring }}">next</a>
<a href="?page={{ page_obj.paginator.num_pages }}&{{ ctx_sort_qstring }}">last »</a>
{% endif %}
</span>
</div>
{% endif %}
Basically I used GET query string for passing required values for sorting, then I use them in the “get_ordering” to compute the “order by” clause.
Then, in “get_context_data” I set those values in the context to send them back and reuse in future requests.