Skip to content

Instantly share code, notes, and snippets.

@mx-moth
Last active March 21, 2018 23:26
Show Gist options
  • Save mx-moth/68cc4084eae3405eca90b41bf30896c7 to your computer and use it in GitHub Desktop.
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.
"""
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