Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CONTRIBUTORS.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ Karim A. (Directeur) <directeur@gmail.com>
Eric Florenzano <floguy@gmail.com>
Stéphane Raimbault <stephane.raimbault@webstack.fr>
S. Kossouho <artscoop93@gmail.com>
Joffrey M. <joffrey.mander@polyconseil.fr>
3 changes: 2 additions & 1 deletion testproj/README
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
3 pip install ..
4. ./manage.py migrate
5. ./manage.py loaddata secretfiles
6. ./manage.py runserver
6. ./manage.py collectstatic
7. ./manage.py runserver

To run tests: ./manage.py test
1 change: 1 addition & 0 deletions testproj/testproj/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.1/howto/static-files/

STATIC_ROOT = "static/"
STATIC_URL = "/static/"

DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
11 changes: 11 additions & 0 deletions testproj/testproj/testapp/fixtures/secretfiles.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,16 @@
"is_secret": false,
"filename": "0-w00t.crt"
}
},
{
"pk": 5,
"model": "testapp.secretfile",
"fields": {
"created_on":"2014-01-09T23:01:30Z",
"size": null,
"order": 1,
"is_secret": true,
"filename": "nulls_size"
}
}
]
2 changes: 2 additions & 0 deletions testproj/testproj/testapp/jinja2/env.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from jinja2.environment import Environment

from django.template.defaultfilters import yesno
from django.templatetags.static import static
from webstack_django_sorting.jinja2_globals import (
sorting_anchor,
sort_queryset,
Expand All @@ -13,3 +14,4 @@ def __init__(self, **kwargs):
self.filters["yesno"] = yesno
self.globals["sorting_anchor"] = sorting_anchor
self.globals["sort_queryset"] = sort_queryset
self.globals["static"] = static
32 changes: 1 addition & 31 deletions testproj/testproj/testapp/jinja2/secret_list.jinja2
Original file line number Diff line number Diff line change
@@ -1,36 +1,6 @@
<html>
<head>
<style>
body {
font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
line-height: 1.42857143;
color: #333;
}

table {
border-spacing: 0;
border-collapse: collapse;
}

thead>tr>th {
vertical-align: bottom;
border-bottom: 2px solid #ddd;
text-align: left;
}

thead>tr>th>a {
text-decoration: none;
}

thead>tr>th>a:visited {
color: inherit;
}

tr>th, tr>td {
padding: 8px;
line-height: 1.42857143;
}
</style>
<link rel="stylesheet" href="{{ static('css/style.css') }}"/>
</head>
<body>
<h2>List of files</h2>
Expand Down
34 changes: 2 additions & 32 deletions testproj/testproj/testapp/templates/secret_list.html
Original file line number Diff line number Diff line change
@@ -1,38 +1,8 @@
{% load sorting_tags %}
{% load static sorting_tags %}

<html>
<head>
<style>
body {
font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
line-height: 1.42857143;
color: #333;
}

table {
border-spacing: 0;
border-collapse: collapse;
}

thead>tr>th {
vertical-align: bottom;
border-bottom: 2px solid #ddd;
text-align: left;
}

thead>tr>th>a {
text-decoration: none;
}

thead>tr>th>a:visited {
color: inherit;
}

tr>th, tr>td {
padding: 8px;
line-height: 1.42857143;
}
</style>
<link rel="stylesheet" href="{% static 'css/style.css' %}"/>
</head>
<body>
<h2>List of files</h2>
Expand Down
35 changes: 35 additions & 0 deletions testproj/testproj/testapp/templates/secret_list_nulls_first.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{% load static sorting_tags %}

<html>
<head>
<link rel="stylesheet" href="{% static 'css/style.css' %}"/>
</head>
<body>
<h2>List of files</h2>
{% autosort secret_files nulls_first=True %}
<table>
<thead>
<tr>
<th>{% anchor id "ID" %}</th>
<th>{% anchor filename "Filename" %}</th>
<th>{% anchor created_on "Date" %}</th>
<th>{% anchor size "Size" %}</th>
<th>{% anchor order "Order" %}</th>
<th>{% anchor is_secret "Secret?" %}</th>
</tr>
</thead>
<tbody>
{% for secret_file in secret_files %}
<tr>
<td>{{ secret_file.id }}</td>
<td>{{ secret_file.filename }}</td>
<td>{{ secret_file.created_on }}</td>
<td>{{ secret_file.size }}</td>
<td>{{ secret_file.order }}</td>
<td>{{ secret_file.is_secret|yesno }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</body>
</html>
35 changes: 35 additions & 0 deletions testproj/testproj/testapp/templates/secret_list_nulls_last.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{% load static sorting_tags %}

<html>
<head>
<link rel="stylesheet" href="{% static 'css/style.css' %}"/>
</head>
<body>
<h2>List of files</h2>
{% autosort secret_files nulls_last=True %}
<table>
<thead>
<tr>
<th>{% anchor id "ID" %}</th>
<th>{% anchor filename "Filename" %}</th>
<th>{% anchor created_on "Date" %}</th>
<th>{% anchor size "Size" %}</th>
<th>{% anchor order "Order" %}</th>
<th>{% anchor is_secret "Secret?" %}</th>
</tr>
</thead>
<tbody>
{% for secret_file in secret_files %}
<tr>
<td>{{ secret_file.id }}</td>
<td>{{ secret_file.filename }}</td>
<td>{{ secret_file.created_on }}</td>
<td>{{ secret_file.size }}</td>
<td>{{ secret_file.order }}</td>
<td>{{ secret_file.is_secret|yesno }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</body>
</html>
75 changes: 73 additions & 2 deletions testproj/testproj/testapp/tests.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import django.template as django_template
from django.template.engine import Engine
from django.template.response import SimpleTemplateResponse
from django.urls import reverse
from django.test import TestCase, Client
from django.test import TestCase

from . import models


class IndexTest(TestCase):

def setUp(self):
self.client = Client()
self.url = reverse("secret_list")

models.SecretFile.objects.create(filename="foo.txt", order=1, size=1024)
models.SecretFile.objects.create(filename="bar.txt", order=2, size=512)

Expand All @@ -34,3 +38,70 @@ def test_sorting_argument(self):
# Nothing wrong happens with invalid sort argument
response = self.client.get(self.url, {"sort": "NOT EXISTING"})
self.assertContains(response, "foo.txt")


class NullsTestCase(TestCase):
def setUp(self):
self.nulls_first_url = reverse("nulls_first")
self.nulls_last_url = reverse("nulls_last")

models.SecretFile.objects.create(filename="foo.txt", order=1, size=1024)
models.SecretFile.objects.create(filename="bar.txt", order=2, size=512)

def test_sorting_nulls_first(self):
""" Verify None sorted field_name is in firsts places when sorting in asc and desc order """

models.SecretFile.objects.create(filename=None, order=3, size=512)
# asc order
values = ["<SecretFile: #3 None>", "<SecretFile: #2 bar.txt>", "<SecretFile: #1 foo.txt>"]
response = self.client.get(
self.nulls_first_url,
{"sort": "filename", "nulls_first": True, "dir": "asc"}
)
self.assertQuerysetEqual(list(response.context["secret_files"]), values)

# desc order
values = ["<SecretFile: #3 None>", "<SecretFile: #1 foo.txt>", "<SecretFile: #2 bar.txt>"]
response = self.client.get(
self.nulls_first_url,
{"sort": "filename", "nulls_first": True, "dir": "desc"}
)
self.assertQuerysetEqual(list(response.context["secret_files"]), values)

def test_sorting_nulls_last(self):
""" Verify None sorted field_name is in lasts places when sorting in asc and desc order """

models.SecretFile.objects.create(filename=None, order=3, size=512)
# asc order
values = ["<SecretFile: #2 bar.txt>", "<SecretFile: #1 foo.txt>", "<SecretFile: #3 None>"]
response = self.client.get(
self.nulls_last_url,
{"sort": "filename", "nulls_last": True, "dir": "asc"}
)
self.assertQuerysetEqual(list(response.context["secret_files"]), values)

# desc order
values = ["<SecretFile: #1 foo.txt>", "<SecretFile: #2 bar.txt>", "<SecretFile: #3 None>"]
response = self.client.get(
self.nulls_last_url,
{"sort": "filename", "nulls_last": True, "dir": "desc"}
)
self.assertQuerysetEqual(list(response.context["secret_files"]), values)

def test_sorting_nulls_first_and_last(self):
""" Verify nulls_first and nulls_last autosort params can't be used at the same time """

engine = Engine(
libraries={'sorting_tags': 'webstack_django_sorting.templatetags.sorting_tags'},
context_processors=['django.template.context_processors.request'],
)
with self.assertRaises(django_template.TemplateSyntaxError) as exc:
template = engine.from_string("""
{% load sorting_tags %}
{% autosort secret_files nulls_first=True nulls_last=True %}
""")
response = SimpleTemplateResponse(
template,
context={'secret_files': models.SecretFile.objects.all()}
)
self.assertIn("Can't set nulls_first and nulls_last simultaneously.", exc.exception.args)
12 changes: 12 additions & 0 deletions testproj/testproj/testapp/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,15 @@ def secret_list_jinja2(request):
return render(
request, "secret_list.jinja2", {"secret_files": models.SecretFile.objects.all()}
)


def secret_list_nulls_first(request):
return render(
request, "secret_list_nulls_first.html", {"secret_files": models.SecretFile.objects.all()}
)


def secret_list_nulls_last(request):
return render(
request, "secret_list_nulls_last.html", {"secret_files": models.SecretFile.objects.all()}
)
6 changes: 5 additions & 1 deletion testproj/testproj/urls.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
from django.contrib import admin
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
from django.urls import path

from .testapp import views


urlpatterns = [
path("", views.secret_list, name="secret_list"),
path("nulls_first", views.secret_list_nulls_first, name="nulls_first"),
path("nulls_last", views.secret_list_nulls_last, name="nulls_last"),
path("jinja2", views.secret_list_jinja2, name="secret_list_jinja2"),
path("admin/", admin.site.urls),
]
] + staticfiles_urlpatterns()
46 changes: 26 additions & 20 deletions webstack_django_sorting/common.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""
Common to Django tags (sorting_tags) and Jinja2 globals (jinja2_globals)
"""
from django.db.models import F

from operator import attrgetter

from .settings import SORT_DIRECTIONS
Expand All @@ -11,7 +13,9 @@ def render_sort_anchor(request, field_name, title):
sort_by = get_params.get("sort", None)
if sort_by == field_name:
# Render anchor link to next direction
current_direction = SORT_DIRECTIONS[get_params.get("dir", "")]
current_direction = SORT_DIRECTIONS.get(
get_params.get("dir", ""), SORT_DIRECTIONS[""]
)
icon = current_direction["icon"]
next_direction_code = current_direction["next"]
else:
Expand Down Expand Up @@ -49,30 +53,32 @@ def need_python_sorting(queryset, order_by):
return field not in field_names


def sort_queryset(queryset, order_by):
def sort_queryset(queryset, order_by, null_ordering):
"""order_by is an Django ORM order_by argument"""

if not order_by:
return queryset

# The field name can be prefixed by the minus sign and we need to
# extract this information if we want to sort on simple object
# attributes
if order_by[0] == "-":
if len(order_by) == 1:
# Prefix without field name
raise ValueError

reverse = True
name = order_by[1:]
else:
reverse = False
name = order_by

if need_python_sorting(queryset, order_by):
# Fallback on pure Python sorting (much slower on large data)

# The field name can be prefixed by the minus sign and we need to
# extract this information if we want to sort on simple object
# attributes (non-model fields)
if order_by[0] == "-":
if len(order_by) == 1:
# Prefix without field name
raise ValueError

reverse = True
name = order_by[1:]
else:
reverse = False
name = order_by
if hasattr(queryset[0], name):
return sorted(queryset, key=attrgetter(name), reverse=reverse)
else:
raise AttributeError
else:
return queryset.order_by(order_by)
raise AttributeError
ordering_exp = (
F(name).desc if reverse else F(name).asc
)(**null_ordering)
return queryset.order_by(ordering_exp)
Loading