How to upload multiple images with flags to Django

Question:

I am building a web page where a blog author can write content and upload images.
Using an image field, I am allowing the author to upload multiple images and with some Javascript logic, I am displaying the images before upload. Under each image, I have a checkbox that indicates if that is the main image of the post (e.g. the first to show in the slideshow and used as thumbnail of the post itself).

On this page I am showing two forms with a shared submit button and it works fine.

My problems begin when I try to save the image and also indicate if it is the main one.

My images model has multiple helpful methods, thumbnail property for Django admin and a save method that will set all images for a post to false, before editing/adding a new main_image. I had to include a commit argument logic because of my experiments. Please ignore it. The model includes 3 fields: the image, the main flag and a foreign key to the post as follows:


class PostImages(models.Model):
    """
    Post images for the blog app.
    """
    image = models.ImageField(
        verbose_name=_("Изображение"),
        upload_to='blog/images/',
        blank=False,
        null=False,
        default='blog/images/default.jpg'
    )
    post = models.ForeignKey(
        to=Post,
        on_delete=models.CASCADE,
        verbose_name=_('Статия')
    )
    main_image = models.BooleanField(
        default=False,
        verbose_name=_('Основно изображение')
    )

    class Meta:
        verbose_name = _('Изображение')
        verbose_name_plural = _('Изображения')
        permissions = [
            ('can_manage_post_image', _('Може да управлява изображенията на публикациите')),
            ('can_add_post_image', _('Може да добавя изображения към публикациите')),
        ]

    def __str__(self):
        return f"{self.post.title} - {self.image}"

    def get_absolute_url(self):
        return f"/blog/post/image/{self.id}"

    def get_edit_url(self):
        return f"/blog/post/image/{self.id}/edit"

    def get_delete_url(self):
        return f"/blog/post/image/{self.id}/delete"

    def get_image(self):
        return mark_safe(f'<img src="{self.image.url}" width="100" height="100" />')

    def get_image_url(self):
        return self.image.url

    @property
    def thumbnail_preview(self):
        if self.image:
            return mark_safe('<img src="{}" width="400" height="auto" />'.format(self.image.url))
        return ''

    def save(self, *args, **kwargs):
        if self.main_image and kwargs.get("commit", True):
            PostImages.objects.filter(post=self.post).update(main_image=False)
        kwargs.pop("commit", None)
        super().save(*args, **kwargs)

I am also using a Model form. In the model form I do not ask for the post, because it is not yet created. My idea was to fill the post in the save phase following a process of:

  1. save post
  2. save images
    For me this is a logical move, but knowing how much time it costed me so far -> probably not the best move.
class PostImagesForm(forms.ModelForm):
    """
    Form for uploading images for posts.
    """
    image = forms.ImageField(
        label='Снимка',
        widget=forms.ClearableFileInput(
            attrs={
                'class': 'form-control',
                'multiple': True,
            }
        )
    )

    main_image = forms.BooleanField(
        label='Основна снимка',
        required=False,
        widget=forms.CheckboxInput(
            # attrs={
            #     'class': 'form-control',
            # }
        )
    )

    class Meta:
        model = PostImages
        fields = ['image', 'main_image']

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['image'].required = False

    def save(self, commit=True):
        """
        Save the form and return the created or edited post.
        """
        post_image = super().save(commit=False)
        post_image.save(commit=commit)
        return post_image

    def clean(self):
        """
        Clean the form and return the cleaned data.
        """
        cleaned_data = super().clean()
        return cleaned_data

So far, so …. good I assume.

So here are the questions:

  • How do I tell Django that I am uploading a pair of fields (file and checkbox)?
  • How to pass the post instance to the image?
  • Why is my request files empty?
  • How am I supposed to group the image-checkbox pairs? Is it something inside the html??

I tried to use the form itself, but it misses the post instance.

I tried to iterate through the files but they are empty.

I tried to iterate through the form fields, but I do not see all uploaded files.

Asked By: NFSpeedy

||

Answers:

If someone ever needs this:

I found multiple problems that I think might be useful to share.

  1. I am using multiple forms under one submit button. At some point I played myself and deleted the enctype="multipart/form-data" from my form tag. (oops)
  2. My javascript had a strange behaviour because it was pumping the image as "data:sjdfhsdkjbfksd" in my identifier for the checkbox. So again a rookie mistake. I detected it when I started slapping console.log() to everything an anything.
  3. If you are like me (main language is Python and JS is something accidentally known), remember – there is a fantastic debugger for JS in the browser, under sources.
  4. JS scopes are strange and they are not like in Python. While using the debugger, you will see that the loops doing stuff and calling event functions will always do everything outside event functions and then move to your event function. You can pass stuff by reassigning in var statements, but be ready to be screamed at by a JS guy.
Answered By: NFSpeedy