Last active
March 21, 2018 23:26
-
-
Save mx-moth/68cc4084eae3405eca90b41bf30896c7 to your computer and use it in GitHub Desktop.
Get the next/previous items in a queryset in relation to a reference object.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
Get the next/previous items in a queryset in relation to a reference object. | |
""" | |
from django.db.models import Q | |
def next_in_order(queryset, reference): | |
""" | |
Get the next items in a QuerySet according to its ordering, in relation to | |
the reference object passed in, from 'closest' to 'furthest' item. | |
>>> next_in_order(Integer.objects.order_by('num'), Integer(num=3)) | |
<Queryset [Integer(num=4), Integer(num=5), Integer(num=6), ...]> | |
""" | |
order_suffix = {True: 'gt', False: 'lt'} | |
# Makes a list [('foo', True), ('bar', False), ('baz', True)] | |
# standard_ordering is toggled by queryset.reverse() | |
order_by = [split_order(order, queryset.query.standard_ordering) | |
for order in queryset.query.order_by] | |
# Grab all the values from the reference object | |
model_opts = queryset.model._meta | |
values = {field: model_opts.get_field(field).value_from_object(reference) | |
for field, _ in order_by} | |
# Build up a Q that filters for items greater than the current reference | |
# object, according to the queryset ordering. For an ordering of | |
# ``['foo', '-bar', 'baz']``, it will build a Q of: | |
# | |
# Q(foo__gt=reference.foo) | |
# | Q(foo=reference.foo, bar__lt=reference.bar) | |
# | Q(foo=reference.foo, bar=reference.bar, baz__gt=reference.baz) | |
next_q = Q() | |
for order_items in inits(order_by): | |
(next_field, next_asc) = order_items.pop() | |
# Build up the Q of fields that equal the reference object | |
q_eq = Q(**{field: values[field] for (field, _) in order_items}) | |
# And the Q of the final field, which must be greater than (or less | |
# than when reversed) the reference field | |
next_filter = '{0}__{1}'.format(next_field, order_suffix[next_asc]) | |
q_next = Q(**{next_filter: values[next_field]}) | |
next_q |= (q_eq & q_next) | |
return queryset.filter(next_q) | |
def previous_in_order(queryset, reference): | |
""" | |
Get the previous items in a QuerySet, in relation to the reference object | |
passed in, from 'closest' in order to 'furthest'. | |
>>> previous_in_order(Integer.objects.order_by('num'), Integer(num=6)) | |
<Queryset [Integer(num=5), Integer(num=4), Integer(num=3), ...]> | |
""" | |
return next_in_order(queryset.reverse(), reference) | |
def inits(seq): | |
""" | |
http://hackage.haskell.org/package/base-4.7.0.1/docs/Data-List.html#v:inits | |
>>> inits([1, 2, 3]) | |
[[1], [1, 2], [1, 2, 3]] | |
""" | |
acc = [] | |
for item in seq: | |
acc.append(item) | |
yield acc[:] | |
def split_order(order, standard_ordering): | |
""" | |
Splits a QuerySet order_by parameter in to its component parts: the field | |
name, and if it is ascending (inverted if ``standard_ordering`` is False). | |
>>> split_order('date', True) | |
('date', True) | |
>>> split_order('-date', True) | |
('date', False) | |
>>> split_order('date', False) | |
('date', False) | |
>>> split_order('-date', False) | |
('date', True) | |
""" | |
if order[0] == '-': | |
field = order[1:] | |
asc = not standard_ordering | |
else: | |
field = order | |
asc = standard_ordering | |
return (field, asc) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment