2025, Oct 31 01:16

Цикличная навигация вперёд/назад в Django DetailView с учётом прав доступа

Как реализовать навигацию вперёд/назад в Django DetailView с учётом прав доступа: цикличные переходы по своим записям на чистых ORM‑запросах, без списков.

Реализовать навигацию «вперёд/назад» в Django DetailView кажется пустяком — пока не вступают в игру права доступа. Если пользователь может просматривать только свои записи, нельзя бездумно шагать по ID или итерироваться по всей таблице. Цель проста: двигаться в пределах разрешённого пользователю поднабора, обеспечить цикличность переходов и не загружать в память тяжёлые структуры вроде связных списков. Ниже — компактный, нативный для ORM подход, который делает именно это.

Постановка задачи

Предположим, вы выводите список сущностей и связываете каждую с её страницей деталей. Маршруты могут выглядеть так, а ссылка на страницу деталей формируется в шаблоне:

urlpatterns = [
    path("records/", views.RecordListView.as_view(), name="records"),
    path("record/<int:pk>/", views.RecordDetailView.as_view(), name="record"),
]
href='{% url "record" obj.id %}'

DetailView элементарен:

class RecordDetailView(LoginRequiredMixin, DetailView):
    model = Record
    template_name = "record.html"

Нужно вывести на странице деталей кнопки перехода к следующей и предыдущей записи, не выходя за рамки данных текущего пользователя и с «закольцовкой» на концах. В шаблоне это может выглядеть так:

<a class='button' href='{% url "record" next_record_id %}'>Next</a>

Недостающее звено — как передать в шаблон next_record_id (и его «предыдущий» аналог), не итерируясь по недоступным данным и не собирая в памяти связный список.

Почему так происходит

Наивный приём «прибавить/убавить ID» не срабатывает, потому что ID не обязаны быть непрерывными в рамках пользовательского набора. Какие‑то записи принадлежат другим пользователям, какие‑то могут вовсе отсутствовать. Загружать полный список при каждом переходе — лишняя нагрузка, особенно когда база может найти соседей «следующий/предыдущий» парой простых, ограниченных запросов. Самый чистый путь — попросить БД о ближайшем большем и меньшем ID внутри пользовательского поднабора и, если их нет, откатываться к первой или последней записи для круговой навигации.

Решение: пусть работу сделает ORM

Выполните два точечных запроса, чтобы получить следующую и предыдущую записи относительно текущей. Если «следующей» в пользовательских данных нет — переходите к минимальному ID. Если нет «предыдущей» — к максимальному. Добавьте оба ID в контекст и подключите их в шаблоне.

class RecordDetailView(LoginRequiredMixin, DetailView):
    model = Record
    template_name = "record.html"
    def get_context_data(self, **kwargs):
        payload = super().get_context_data(**kwargs)
        current = self.get_object()
        nxt = Record.objects.filter(
            user=self.request.user,
            id__gt=current.id
        ).order_by('id').first()
        prv = Record.objects.filter(
            user=self.request.user,
            id__lt=current.id
        ).order_by('-id').first()
        if not nxt:
            nxt = Record.objects.filter(user=self.request.user).order_by('id').first()
        if not prv:
            prv = Record.objects.filter(user=self.request.user).order_by('-id').first()
        payload['next_record_id'] = nxt.id if nxt else None
        payload['prev_record_id'] = prv.id if prv else None
        return payload

А в шаблоне:

{% if prev_record_id %}
    <a class='button' href='{% url "record" prev_record_id %}'>Previous</a>
{% endif %}
{% if next_record_id %}
    <a class='button' href='{% url "record" next_record_id %}'>Next</a>
{% endif %}

Почему это важно

Такой подход не загружает и не поддерживает в памяти никакие последовательности — значит, экономит ресурсы. Он соблюдает видимость на уровне пользователя, ограничивая каждый поиск его записями. Плюс, круговая навигация достигается без специальных структур данных: ORM‑запросы получаются прозрачными, предсказуемыми и удобными в сопровождении.

Выводы

Навигации «вперёд/назад» в Django DetailView не нужны связные списки или последовательности в сессии. Запрашивайте ближайшие больший и меньший ID в пределах данных текущего пользователя и делайте «обход по кругу», когда упираетесь в границы. Держите логику в get_context_data, передавайте вычисленные ID в шаблон и выводите ссылки только если они есть. Результат — простая, безопасная и эффективная навигация по собственным записям пользователя.

Статья основана на вопросе на StackOverflow от Franz Müller и ответе Mahrez BenHamad.