Skip to content

Commit 613a636

Browse files
authored
Merge pull request #48 from benedicthsieh/sorting-with-duplicates-fix2
Sorting with duplicates, branched from #41
2 parents 1b6fee4 + 2de8bc1 commit 613a636

File tree

3 files changed

+97
-26
lines changed

3 files changed

+97
-26
lines changed

docs/tutorial.rst

Lines changed: 47 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -16,28 +16,28 @@ albums/models.py:
1616
.. code:: python
1717
1818
from django.db import models
19-
20-
19+
20+
2121
class Genre(models.Model):
2222
name = models.CharField('Name', max_length=80)
23-
23+
2424
class Meta:
2525
ordering = ['name']
26-
26+
2727
def __str__(self):
2828
return self.name
29-
30-
29+
30+
3131
class Artist(models.Model):
3232
name = models.CharField('Name', max_length=80)
33-
33+
3434
class Meta:
3535
ordering = ['name']
36-
36+
3737
def __str__(self):
3838
return self.name
39-
40-
39+
40+
4141
class Album(models.Model):
4242
name = models.CharField('Name', max_length=80)
4343
rank = models.PositiveIntegerField('Rank')
@@ -53,10 +53,10 @@ albums/models.py:
5353
verbose_name='Genres',
5454
related_name='albums'
5555
)
56-
56+
5757
class Meta:
5858
ordering = ['name']
59-
59+
6060
def __str__(self):
6161
return self.name
6262
@@ -76,14 +76,14 @@ albums/serializers.py:
7676
'id', 'name',
7777
)
7878
79-
79+
8080
class AlbumSerializer(serializers.ModelSerializer):
8181
artist = ArtistSerializer()
8282
genres = serializers.SerializerMethodField()
83-
83+
8484
def get_genres(self, album):
8585
return ', '.join([str(genre) for genre in album.genres.all()])
86-
86+
8787
class Meta:
8888
model = Album
8989
fields = (
@@ -98,29 +98,29 @@ albums/views.py:
9898
from rest_framework import viewsets
9999
from .models import Album
100100
from .serializers import AlbumSerializer
101-
102-
101+
102+
103103
def index(request):
104104
return render(request, 'albums/albums.html')
105-
106-
105+
106+
107107
class AlbumViewSet(viewsets.ModelViewSet):
108108
queryset = Album.objects.all().order_by('rank')
109109
serializer_class = AlbumSerializer
110110
111111
urls.py:
112112

113113
.. code:: python
114-
114+
115115
from django.conf.urls import url, include
116116
from rest_framework import routers
117117
from albums import views
118-
119-
118+
119+
120120
router = routers.DefaultRouter()
121121
router.register(r'albums', views.AlbumViewSet)
122-
123-
122+
123+
124124
urlpatterns = [
125125
url('^api/', include(router.urls)),
126126
url('', views.index, name='albums')
@@ -229,7 +229,7 @@ In the above example, the 'get_options' method will be called to populate the re
229229
.. important::
230230

231231
To sum up, **the most important things** to remember here are:
232-
232+
233233
- don't forget to add ``?format=datatables`` to your API URL
234234
- you must add a **data-data attribute** or specify the column data property via JS for each columns, the name must **match one of the fields of your DRF serializers**.
235235

@@ -334,7 +334,7 @@ We could also have written that in a more conventional form (without data attrib
334334
{'data': 'year'},
335335
{'data': 'genres', 'name': 'genres.name'},
336336
]
337-
337+
338338
});
339339
});
340340
</script>
@@ -372,3 +372,24 @@ If you need more complex filtering and ordering, you can always implement your o
372372

373373

374374
You can see this code live by running the :doc:`example app <example-app>`.
375+
376+
377+
Handling Duplicates in Sorting
378+
------------------------------
379+
If sorting is done on a single column with more duplicates than the page size it's possible than some rows are never retrieved as we traverse through our datatable. This is because of how order by together with limit and offset works in the database.
380+
381+
As a workaround for this problem we add a second column to sort by in the case of ties.
382+
383+
class AlbumViewSet(viewsets.ModelViewSet):
384+
queryset = Album.objects.all().order_by('year')
385+
serializer_class = AlbumSerializer
386+
datatables_additional_order_by = 'rank'
387+
388+
def get_options(self):
389+
return "options", {
390+
"artist": [{'label': obj.name, 'value': obj.pk} for obj in Artist.objects.all()],
391+
"genre": [{'label': obj.name, 'value': obj.pk} for obj in Genre.objects.all()]
392+
}
393+
394+
class Meta:
395+
datatables_extra_json = ('get_options', )

rest_framework_datatables/filters.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,15 @@ def filter_queryset(self, request, queryset, view):
7474

7575
# order queryset
7676
if len(ordering):
77+
if hasattr(view, 'datatables_additional_order_by'):
78+
additional = view.datatables_additional_order_by
79+
# Django will actually only take the first occurrence if the
80+
# same column is added multiple times in an order_by, but it
81+
# feels cleaner to double check for duplicate anyway.
82+
if not any((o[1:] if o[0] == '-' else o) == additional
83+
for o in ordering):
84+
ordering.append(additional)
85+
7786
queryset = queryset.order_by(*ordering)
7887
return queryset
7988

tests/test_filter.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from albums.models import Album
2+
from albums.serializers import AlbumSerializer
3+
4+
from django.conf.urls import url
5+
from django.test.utils import override_settings
6+
from django.test import TestCase
7+
8+
from rest_framework.generics import ListAPIView
9+
from rest_framework.test import (
10+
APIClient,
11+
)
12+
from rest_framework_datatables.pagination import (
13+
DatatablesLimitOffsetPagination,
14+
)
15+
16+
class TestFilterTestCase(TestCase):
17+
class TestAPIView(ListAPIView):
18+
serializer_class = AlbumSerializer
19+
pagination_class = DatatablesLimitOffsetPagination
20+
datatables_additional_order_by = 'year'
21+
22+
def get_queryset(self):
23+
return Album.objects.all()
24+
25+
fixtures = ['test_data']
26+
27+
def setUp(self):
28+
self.client = APIClient()
29+
30+
@override_settings(ROOT_URLCONF=__name__)
31+
def test_additional_order_by(self):
32+
response = self.client.get('/api/additionalorderby/?format=datatables&draw=1&columns[0][data]=rank&columns[0][name]=&columns[0][searchable]=true&columns[0][orderable]=true&columns[0][search][value]=&columns[0][search][regex]=false&columns[1][data]=artist_name&columns[1][name]=artist.name&columns[1][searchable]=true&columns[1][orderable]=true&columns[1][search][value]=&columns[1][search][regex]=false&columns[2][data]=name&columns[2][name]=&columns[2][searchable]=true&columns[2][orderable]=true&columns[2][search][value]=&columns[2][search][regex]=false&order[0][column]=1&order[0][dir]=desc&start=4&length=1&search[value]=&search[regex]=false')
33+
# Would be "Sgt. Pepper's Lonely Hearts Club Band" without the additional order by
34+
expected = (15, 15, 'Rubber Soul')
35+
result = response.json()
36+
self.assertEquals((result['recordsFiltered'], result['recordsTotal'], result['data'][0]['name']), expected)
37+
38+
39+
urlpatterns = [
40+
url('^api/additionalorderby', TestFilterTestCase.TestAPIView.as_view()),
41+
]

0 commit comments

Comments
 (0)