37

I'm trying to get the next and previous objects of a comic book issue. Simply changing the id number or filtering through date added is not going to work because I don't add the issues sequentially.

This is how my views are setup and it WORKS for prev_issue and does return the previous object, but it returns the last object for next_issue and I do not know why.

def issue(request, issue_id):
    issue = get_object_or_404(Issue, pk=issue_id)
    title = Title.objects.filter(issue=issue)
    prev_issue = Issue.objects.filter(title=title).filter(number__lt=issue.number)[0:1]
    next_issue = Issue.objects.filter(title=title).filter(number__gt=issue.number)[0:1]
3
  • What are you trying to do with the information of the prev/next issues? Are you intending to link to them on a page, display them in a list, etc? Are the issue_ids sequential once filtered for each title?
    – j_syk
    Commented May 16, 2011 at 18:54
  • just a curious question: are you using a get_absolute_url attribute in your model or just redirecting to the prev_issue.issue_id into the url?
    – j_syk
    Commented May 16, 2011 at 19:14
  • I'm using the get_absolute_url attribute...definitely not the issue_id because I don't sequentially add issues all the time, and that would impractical.
    – AAA
    Commented May 18, 2011 at 18:37

7 Answers 7

60

Add an order_by clause to ensure it orders by number.

next_issue = Issue.objects.filter(title=title, number__gt=issue.number).order_by('number').first()
7
  • 2
    You should do this to the prev_issue query too. Happy to help! Commented May 16, 2011 at 19:38
  • I wonder how many records such a command would retrieve from the database. Just one record or all records that satisfy the filtering conditions (and we just pick one of them)? If it is the latter case, how can we make it more efficient?
    – Randy Tang
    Commented Oct 5, 2014 at 2:38
  • 1
    The slicing syntax ([0:1]) will use "limit" queries if supported by the DB - so it should be fine on MySQL. docs.djangoproject.com/en/dev/topics/db/queries/… Commented Oct 6, 2014 at 15:15
  • 1
    Make sure the 'number' field has an index.
    – sbaechler
    Commented Jan 16, 2015 at 10:10
  • 3
    You don't need to use filter twice, you can do filter(title=title, number__gt=issue.number) Commented Feb 10, 2015 at 21:06
36

I know this is a bit late, but for anyone else, django does have a nicer way to do this, see https://docs.djangoproject.com/en/stable/ref/models/instances/#django.db.models.Model.get_previous_by_FOO

So the answer here would be something something like

next_issue = Issue.get_next_by_number(issue, title=title)

Django managers to do that with a bit of meta class cleaverness.

5
  • 7
    Note that this only works for non-null DateField and DateTimeField. Commented Dec 17, 2015 at 3:57
  • I assume it's done on Django core level beacuse of the difficulties that developer could experience due to possible Date or DateTime equalness. Conditions change to __gte and __lte, which causes additional complexity to resulting query. This built-in are the best choice when dealing with previous and next objects by Date* fields. Commented Jan 14, 2017 at 9:50
  • Coolest solution. Thumbs up (y) Commented Mar 12, 2018 at 12:49
  • 2
    Updated link since the original is broken: docs.djangoproject.com/en/3.1/ref/models/instances/… Commented Feb 5, 2021 at 14:54
  • stackoverflow.com/questions/1931008/…
    – Glycerine
    Commented Nov 22, 2023 at 0:41
8

If it's required to find next and previous objects ordered by field values that can be equal and those fields are not of Date* type, the query gets slightly complex, because:

  • ordering on objects with same values limiting by [:1] will always produce same result for several objects;
  • object can itself be included in resulting set.

Here's are querysets that also take into account the primary keys to produce a correct result (assuming that number parameter from OP is not unique and omitting the title parameter as it's irrelevant for the example):

Previous:

prev_issue = (Issue.objects
    .filter(number__lte=issue.number, id__lt=instance.id)
    .exclude(id=issue.id)
    .order_by('-number', '-id')
    .first())

Next:

next_issue = (Issue.objects
    .filter(number__gte=issue.number, id__gt=instance.id)
    .exclude(id=issue.id)
    .order_by('number', 'id')
    .first())
0
from functools import partial, reduce
from django.db import models


def next_or_prev_instance(instance, qs=None, prev=False, loop=False):

    if not qs:
        qs = instance.__class__.objects.all()

    if prev:
        qs = qs.reverse()
        lookup = 'lt'
    else:
        lookup = 'gt'

    q_list = []
    prev_fields = []

    if qs.query.extra_order_by:
        ordering = qs.query.extra_order_by
    elif qs.query.order_by:
        ordering = qs.query.order_by
    elif qs.query.get_meta().ordering:
        ordering = qs.query.get_meta().ordering
    else:
        ordering = []

    ordering = list(ordering)

    if 'pk' not in ordering and '-pk' not in ordering:
        ordering.append('pk')
        qs = qs.order_by(*ordering)

    for field in ordering:
        if field[0] == '-':
            this_lookup = (lookup == 'gt' and 'lt' or 'gt')
            field = field[1:]
        else:
            this_lookup = lookup
        q_kwargs = dict([(f, get_model_attr(instance, f))
                         for f in prev_fields])
        key = "%s__%s" % (field, this_lookup)
        q_kwargs[key] = get_model_attr(instance, field)
        q_list.append(models.Q(**q_kwargs))
        prev_fields.append(field)
    try:
        return qs.filter(reduce(models.Q.__or__, q_list))[0]
    except IndexError:
        length = qs.count()
        if loop and length > 1:
            return qs[0]
    return None


next_instance = partial(next_or_prev_instance, prev=False)
prev_instance = partial(next_or_prev_instance, prev=True)
0

note that do not use object.get(pk=object.pk + 1) these sorts of things, IntegrityError occurs if object at that pk is deleted, hence always use a query set

for visitors:

''' Useage '''
"""
    # Declare our item
    store = Store.objects.get(pk=pk)
    # Define our models
    stores = Store.objects.all()
    # Ask for the next item
    new_store = get_next_or_prev(stores, store, 'next')
    # If there is a next item
    if new_store:
        # Replace our item with the next one
        store = new_store
"""

''' Function '''
def get_next_or_prev(models, item, direction):
    '''
    Returns the next or previous item of
    a query-set for 'item'.

    'models' is a query-set containing all
    items of which 'item' is a part of.

    direction is 'next' or 'prev'
    
    '''

    getit = False
    if direction == 'prev':
        models = models.reverse()
    for m in models:
        if getit:
            return m
        if item == m:
            getit = True
    if getit:
        # This would happen when the last
        # item made getit True
        return models[0]
    return False

original author

Usage

# you MUST call order by to pass in an order, otherwise QuerySet.reverse will not work
qs = Model.objects.all().order_by('pk')
q = qs[0]
prev = get_next_or_prev(qs, q, 'prev')
next = get_next_or_prev(qs, q, 'next')
0

It works for me

next_post = Blog.objects.filter(date__lte=self.object.date, published=True).exclude(id=self.object.id).order_by('-date').first()
next_post = next_post if next_post else Blog.objects.filter(published=True).first()
previous_post = Blog.objects.filter(date__gte=self.object.date, published=True).exclude(id=self.object.id).order_by('date').first()
previous_post = previous_post if previous_post else Blog.objects.filter(published=True).last()
-3

next_obj_id = int(current_obj_id) + 1 next_obj = Model.objects.filter(id=next_obj_id).first()

prev_obj_id= int(current_obj_id) - 1 prev_obj = Model.objects.filter(id=prev_obj_id).first()

#You have nothing to loose here... This works for me

2
  • The variable current_obj_id does not exist in the OP's code. Commented Jul 13, 2021 at 16:08
  • This also assumes your id's are sequential, which doesn't need to be true from when results come from a filter query.
    – dtasev
    Commented Oct 29, 2021 at 13:48

Not the answer you're looking for? Browse other questions tagged or ask your own question.