How to get the next object by id in DetailView

Hi,

I’m working with objects from the ObjetArchi model. When I navigate on the page of an object, based on a DetailView generic class based view, I’d like to integrate “Previous object” and “Next object” links that open the page of the previous (or the next) object, based on the id field, by order of creation.

Some ids are missing in the database, but the ascending order is preserved. e.g.: 13, 22, 23, 24, 25, 35, 56, etc.

If object.id +1 (or -1) doesn’t exist, it should iterate until the next (or previous) existing id.

How could I implement this?

Thanks for your hints!

views.py:

class Notice(DetailView):

    model = ObjetArchi
    template_name = 'notices/objetarchi_detail.html'

urls.py:

from django.urls import path
from notices import views
from django.contrib.flatpages import views as flatviews

app_name = 'notices'

urlpatterns = [
    path('', views.NoticesListView.as_view(), name='notices_list'),
    path('notices', views.NoticesListView.as_view(), name='notices'),
    path('notices/<int:pk>', views.Notice.as_view(), name='detail'),
    path('notices/<int:pk>/pdf', views.NoticePdf.as_view(), name='detail_pdf'),
    path('apropos', flatviews.flatpage, {"url": "/apropos/"}, name='apropos'),
]

Assuming you know what your current ID is for the object being viewed, your next ID in sequence is going to be

ObjectArchi.objects.filter(
    id__gt=current_object_id
).order_by('id').only('id').first().id

and the previous will be

ObjectArchi.objects.filter(
    id__lt=current_object_id
).order_by('-id').only('id').first().id`
3 Likes

You can also use subqueries to retrieve previous and next ids as annotations to avoid two extra queries

queryset = ObjetArchi.objects.annotate(
     previous_id=ObjetArchi.objects.filter(
         id__lt=OuterRef("id"),
     ).order_by("-id").values("id")[:1],
     next_id=ObjetArchi.objects.filter(
         id__gt=OuterRef("id"),
     ).order_by("id").values("id")[:1]
).get(id=pk)
4 Likes

Thanks both for your help!

Here’s what I came up with, based on @KenWhitesell’s answer (I could simplify it with annotations as suggested by @charettes). I added two functions to reach the first or the last object in the list.

views.py

class Notice(DetailView):

    model = ObjetArchi
    template_name = 'notices/objetarchi_detail.html'

    def get_first_id(self):
        qs = ObjetArchi.objects.order_by('id').only('id').first()
        if qs:
            return qs.id
        else:
            return None
    
    def get_last_id(self):
        qs = ObjetArchi.objects.order_by('-id').only('id').first()
        if qs:
            return qs.id
        else:
            return None


    def get_next_id(self, current_object_id):
        qs = ObjetArchi.objects.filter(id__gt=current_object_id).order_by('id').only('id').first()
        if qs:
            return qs.id
        else:
            return None
        
    def get_previous_id(self, current_object_id):
        qs = ObjetArchi.objects.filter(id__lt=current_object_id).order_by('-id').only('id').first()
        if qs:
            return qs.id
        else:
            return None

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        current_object = self.object
        current_object_id = current_object.id
        first_object_id = self.get_first_id()
        last_object_id = self.get_last_id()
        next_object_id = self.get_next_id(current_object_id)
        previous_object_id = self.get_previous_id(current_object_id)
        context['last_object_id'] = last_object_id
        context['first_object_id'] = first_object_id
        context['next_object_id'] = next_object_id
        context['previous_object_id'] = previous_object_id
        return context

And in the template:

{% if previous_object_id %}
<li><a href="{{ first_object_id }}">Début</a>
{% else %}
<li disabled>Début</button>
{% endif %}
</li>

{% if previous_object_id %}
<li><a href="{{ previous_object_id }}">Précédente</a>
{% else %}
<li disabled>Précédente</button>
{% endif %}
</li>

{% if next_object_id %}
<li><a href="{{ next_object_id }}">Suivante</a>
{% else %}
<li disabled>Suivante</button>
{% endif %}
</li>

{% if next_object_id %}
<li><a href="{{ last_object_id }}">Fin</a>
{% else %}
<li disabled>Fin</button>
{% endif %}
</li>