1
1
#-*- coding: utf-8 -*-
2
2
3
-
4
3
try :
5
4
from collections .abc import Sequence
6
5
except ImportError :
7
6
from collections import Sequence
8
7
9
8
from django .core .paginator import EmptyPage
10
- from django .db .models import QuerySet
9
+ from django .db .models import QuerySet , Q
11
10
12
11
__all__ = [
13
12
'SeekPaginator' ,
17
16
'PREV_PAGE' ]
18
17
19
18
20
- NEXT_PAGE = 1
21
- PREV_PAGE = 2
19
+ NEXT_PAGE , PREV_PAGE , DESC , ASC = range (1 , 5 )
22
20
23
21
24
22
class _NoPk :
@@ -33,52 +31,73 @@ def __repr__(self):
33
31
# the first page
34
32
_NO_PK = _NoPk ()
35
33
34
+ # XXX simplify things by removing the pk parameter,
35
+ # and requiring it as last field/value; we should
36
+ # also validate there is a single unique=True field,
37
+ # and it's the last one; this is a breaking chance, though
36
38
37
- class SeekPaginator (object ):
38
39
40
+ class SeekPaginator :
39
41
def __init__ (self , query_set , per_page , lookup_field ):
40
42
assert isinstance (query_set , QuerySet ), 'QuerySet expected'
41
43
assert isinstance (per_page , int ), 'Int expected'
42
- assert isinstance (lookup_field , str ), 'String expected'
44
+ # assert isinstance(lookup_field, str), 'String expected'
43
45
self .query_set = query_set
44
46
self .per_page = per_page
45
- self .is_desc = lookup_field .startswith ('-' )
46
- self .is_asc = not self .is_desc
47
- self .lookup_field = lookup_field .lstrip ('-' )
47
+ if isinstance (lookup_field , str ):
48
+ lookup_field = (lookup_field ,)
49
+ self .lookup_fields = lookup_field
50
+
51
+ @property
52
+ def fields (self ):
53
+ return tuple (f .lstrip ('-' ) for f in self .lookup_fields )
54
+
55
+ @property
56
+ def fields_direction (self ):
57
+ d = {True : DESC , False : ASC }
58
+ return tuple (
59
+ (f .lstrip ('-' ), d [f .startswith ('-' )])
60
+ for f in self .lookup_fields )
48
61
49
62
def prepare_order (self , has_pk , move_to ):
50
- pk_sort = 'pk'
51
- lookup_sort = self .lookup_field
52
- if ((self .is_desc and move_to == NEXT_PAGE ) or
53
- (self .is_asc and move_to == PREV_PAGE )):
54
- pk_sort = '-%s' % pk_sort
55
- lookup_sort = '-%s' % lookup_sort
63
+ fields = list (self .fields_direction )
56
64
if has_pk :
57
- return [lookup_sort , pk_sort ]
58
- return [lookup_sort ]
59
-
60
- def prepare_lookup (self , value , pk , move_to ):
61
- lookup_include = '%s__gt' % self .lookup_field
62
- lookup_exclude_pk = 'pk__lte'
63
- if ((self .is_desc and move_to == NEXT_PAGE ) or
64
- (self .is_asc and move_to == PREV_PAGE )):
65
- lookup_include = '%s__lt' % self .lookup_field
66
- lookup_exclude_pk = 'pk__gte'
67
- lookup_exclude = None
68
- if pk is not _NO_PK :
69
- lookup_include = "%se" % lookup_include
70
- lookup_exclude = {self .lookup_field : value , lookup_exclude_pk : pk }
71
- lookup_filter = {lookup_include : value }
72
- return lookup_filter , lookup_exclude
65
+ fields .append (
66
+ ('pk' , fields [- 1 ][1 ]))
67
+ result = []
68
+ for f , d in fields :
69
+ if ((d == DESC and move_to == NEXT_PAGE ) or
70
+ (d == ASC and move_to == PREV_PAGE )):
71
+ f = '-%s' % f
72
+ result .append (f )
73
+ return result
74
+
75
+ # q = X<=? & ~(X=? & ~(Y<?))
76
+ def _apply_filter (self , i , fields , values , move_to ):
77
+ assert i < len (fields )
78
+ f , d = fields [i ]
79
+ v = values [i ]
80
+ lf = '%s__gt' % f
81
+ if ((d == DESC and move_to == NEXT_PAGE ) or
82
+ (d == ASC and move_to == PREV_PAGE )):
83
+ lf = '%s__lt' % f
84
+ if len (fields ) == 1 :
85
+ return Q (** {lf : v })
86
+ if i + 1 == len (fields ):
87
+ return Q (** {lf : v })
88
+ q = self ._apply_filter (i + 1 , fields , values , move_to )
89
+ return Q (** {lf + 'e' : v }) & ~ (Q (** {f : v }) & ~ q )
73
90
74
91
def apply_filter (self , value , pk , move_to ):
75
- query_set = self .query_set
76
- lookup_filter , lookup_exclude = self .prepare_lookup (
77
- value = value , pk = pk , move_to = move_to )
78
- query_set = query_set .filter (** lookup_filter )
79
- if lookup_exclude :
80
- query_set = query_set .exclude (** lookup_exclude )
81
- return query_set
92
+ assert len (value ) == len (self .lookup_fields )
93
+ fields = list (self .fields_direction )
94
+ values = list (value )
95
+ if pk is not _NO_PK :
96
+ values .append (pk )
97
+ fields .append (
98
+ ('pk' , fields [- 1 ][1 ]))
99
+ q = self ._apply_filter (0 , fields , values , move_to )
100
+ return self .query_set .filter (q )
82
101
83
102
def seek (self , value , pk , move_to ):
84
103
"""
@@ -92,9 +111,36 @@ def seek(self, value, pk, move_to):
92
111
AND NOT (date = ? AND id >= ?)
93
112
ORDER BY date DESC, id DESC
94
113
114
+ Multi field lookup. Note how it produces nesting,
115
+ and how I removed it using boolean logic simplification::
116
+
117
+ X <= ?
118
+ AND NOT (X = ? AND (date <= ? AND NOT (date = ? AND id >= ?)))
119
+ <--->
120
+ X <= ?
121
+ AND (NOT X = ? OR NOT date <= ? OR (date = ? AND id >= ?))
122
+ <--->
123
+ X <= ?
124
+ AND (NOT X = ? OR NOT date <= ? OR date = ?)
125
+ AND (NOT X = ? OR NOT date <= ? OR id >= ?)
126
+
127
+ A * ~(B * (C * ~(D * F)))
128
+ -> (D + ~B + ~C) * (F + ~B + ~C) * A
129
+ A * ~(B * (C * ~(D * (F * ~(G * H)))))
130
+ -> (D + ~B + ~C) * (F + ~B + ~C) * (~B + ~C + ~G + ~H) * A
131
+ A * ~(B * (C * ~(D * (F * ~(G * (X * ~(Y * Z)))))))
132
+ -> (D + ~B + ~C) * (F + ~B + ~C) * (Y + ~B + ~C + ~G + ~X) * (Z + ~B + ~C + ~G + ~X) * A
133
+
134
+ Addendum::
135
+
136
+ X <= ?
137
+ AND NOT (X = ? AND NOT (date <= ? AND NOT (date = ? AND id >= ?)))
138
+
95
139
"""
96
140
query_set = self .query_set
97
141
if value is not None :
142
+ if not isinstance (value , (tuple , list )):
143
+ value = (value ,)
98
144
query_set = self .apply_filter (
99
145
value = value , pk = pk , move_to = move_to )
100
146
query_set = query_set .order_by (
@@ -125,7 +171,6 @@ def page(self, value, pk=_NO_PK, move_to=NEXT_PAGE):
125
171
126
172
127
173
class SeekPage (Sequence ):
128
-
129
174
def __init__ (self , query_set , key , move_to , paginator ):
130
175
self ._query_set = query_set
131
176
self ._key = key
@@ -163,8 +208,11 @@ def _some_seek(self, direction):
163
208
pk = _NO_PK
164
209
if self ._key ['pk' ] is not _NO_PK :
165
210
pk = last .pk
211
+ values = tuple (
212
+ getattr (last , f )
213
+ for f in self .paginator .fields )
166
214
return self .paginator .seek (
167
- value = getattr ( last , self . paginator . lookup_field ) ,
215
+ value = values ,
168
216
pk = pk ,
169
217
move_to = direction )
170
218
@@ -214,10 +262,10 @@ def prev_pages_left(self, limit=None):
214
262
def _some_page (self , index ):
215
263
if not self .object_list :
216
264
return {}
217
- key = {
218
- 'value' : getattr (
219
- self .object_list [ index ],
220
- self . paginator . lookup_field ) }
265
+ values = tuple (
266
+ getattr (self . object_list [ index ], f )
267
+ for f in self .paginator . fields )
268
+ key = { 'value' : values }
221
269
if self ._key ['pk' ] is not _NO_PK :
222
270
key ['pk' ] = self .object_list [index ].pk
223
271
return key
0 commit comments