2025, Nov 29 18:01

Первые три изображения в Django-шаблоне: forloop.counter0 вместо asset.0

Почему в Django-шаблоне не работает asset.0 и как вывести первые три изображения с помощью forloop.counter0: понятное объяснение и готовый фрагмент кода.

Показать несколько загруженных изображений в чётко разделённых тегах кажется простым — пока шаблон Django не начинает вести себя как индексатор списка. Если переменная цикла — не список, обращения вроде field.0 или field.1 не дадут никакого толка. Ниже — короткий разбор проблемы и аккуратное решение с использованием встроенных метаданных цикла в Django.

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

У нас есть модель загружаемых файлов, привязанных к записи, представление, которое сохраняет запись и помечает загрузки как изображения или видео, и шаблон, пытающийся вывести первые три изображения в отдельные теги.

Слой данных:

class MediaItem(models.Model):
  entry = models.ForeignKey(Entry, on_delete=models.CASCADE, null=True, blank=True)
  blob = models.FileField(upload_to='PostImages/%Y/%m/%d/', null=True, blank=True)
  is_image = models.BooleanField(default=False)
  is_video = models.BooleanField(default=False)

Логика представления:

def feed_screen(request):
  heading = "For You"
  authored_entries = Entry.objects.filter(Q(poster_profile=request.user))
  if request.method == "POST":
      entry_form = EntryForm(request.POST)
      files_form = MediaItemForm(request.POST, request.FILES)
      if entry_form.is_valid() and files_form.is_valid():
          obj = entry_form.save(commit=False)
          obj.poster_profile = request.user
          obj.save()
          entry_form.save_m2m()
          uploaded = request.FILES.getlist('files')
          for payload in uploaded:
              item = MediaItem.objects.create(entry=obj, blob=payload)
              mime, _ = mimetypes.guess_type(item.blob.name)
              if mime and mime.startswith('image/'):
                  item.is_image = True
              elif mime and mime.startswith('video/'):
                  item.is_video = True
              item.save()
          return redirect('site:foryou_view')
  else:
      entry_form = EntryForm()
      files_form = MediaItemForm()
  ctx = {
      'page_title': heading,
      'poster_profile': authored_entries,
      'form': entry_form,
      'form_upload': files_form,
  }
  return render(request, 'foryou.html', ctx)

Неудачная попытка в шаблоне:

{% if poster_profile %}
{% for person in poster_profile %}
    {% if person.mediaitem_set.all|length == 3 %}
        {% for asset in person.mediaitem_set.all %}
            <!-- Здесь должно быть первое изображение -->
            {% if asset.0.is_image == True %}
                <img src="{{ asset.blob.url }}" alt="img"/>
            {% endif %}
            <!-- Здесь должно быть второе изображение -->
            {% if asset.1.is_image == True %}
                <img src="{{ asset.blob.url }}" alt="img"/>
            {% endif %}
            <!-- Здесь должно быть третье изображение -->
            {% if asset.2.is_image == True %}
                <img src="{{ asset.blob.url }}" alt="img"/>
            {% endif %}
        {% endfor %}
    {% endif %}
{% endfor %}
{% endif %}

Почему это не работает

Внутренний цикл перебирает объекты MediaItem. Каждый asset — это один объект, а не список. Попытки обратиться к asset.0, asset.1 или asset.2 предполагают, что переменная цикла индексируется как последовательность, чего нет. В итоге ни одно условие не срабатывает, и ничего не отображается.

В шаблонах Django индексы цикла уже доступны через переменные forloop. Они как раз и предназначены для случаев с «первым», «вторым» и «третьим» элементами.

Решение: используйте счётчики forloop, чтобы выбрать первые три

Правильный подход — продолжать итерироваться по связанным элементам и использовать forloop.counter0 (индекс с нуля), чтобы различать первые три. Затем для каждого из них выводить отдельный тег изображения, если элемент действительно является изображением.

{% for asset in person.mediaitem_set.all %}
  {% if forloop.counter0 == 0 %}
    {% if asset.is_image %}
      <img src="{{ asset.blob.url }}" alt="Image 1">
    {% endif %}
  {% elif forloop.counter0 == 1 %}
    {% if asset.is_image %}
      <img src="{{ asset.blob.url }}" alt="Image 2">
    {% endif %}
  {% elif forloop.counter0 == 2 %}
    {% if asset.is_image %}
      <img src="{{ asset.blob.url }}" alt="Image 3">
    {% endif %}
  {% else %}
    <img src="{{ asset.blob.url }}" alt="Image {{ forloop.counter }}">
  {% endif %}
{% endfor %}

Так сохраняется изначальная задумка: первые три изображения получают отдельные теги, а всё остальное по-прежнему выводится предсказуемо. Проверка asset.is_image гарантирует, что в эти позиции попадут только изображения.

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

Логика шаблонов Django намеренно ограничена и декларативна. Когда объекты ошибочно принимают за списки, возникают трудноуловимые сбои: вместо явной ошибки вы получаете пустой вывод. Понимание метаданных forloop помогает держать шаблоны простыми, читаемыми и предсказуемыми, особенно когда требуется позиционное отображение для галерей, каруселей или лент.

Вывод

Если нужно разместить первое, второе и третье изображения в отдельных тегах, не индексируйте переменную цикла. Используйте forloop.counter0, чтобы ветвиться по позиции, и оставляйте простой фильтр is_image внутри каждой ветки. Это избавляет от хрупких конструкций и делает поведение шаблона очевидным с первого взгляда. И помните: Django уже предоставляет нужные индексы — в шаблоне не требуется индексировать списки.