Can't save InMemoryUploadedFile to S3 in Django Admin

Question:

I’m using the django-storage package, and trying to upload multiple images at once. So I overwritten the add_view and save_model methods in ModelAdmin, in order to remove the original image field and use a custom one (with a multiple flag in the input tag) given in the template HTML:

MODELS.PY

class Media(AbstractCreatedUpdatedDateMixin):
    uuid = models.UUIDField(unique=True, default=uuid4, editable=False, db_index=True)
    user = models.ForeignKey(User, related_name="uploaded_media", on_delete=models.CASCADE)
    title = models.CharField(max_length=255)
    image = models.ImageField(upload_to=uuid_directory_path)


ADMIN.PY

class MediaModelAdmin(admin.ModelAdmin):
    def add_view(self, request, form_url='', extra_context=None):
        self.exclude = ('image', "is_approved")
        extra_context = extra_context or {}
        extra_context['show_save_and_add_another'] = False
        extra_context['show_save_and_continue'] = False
        return super().add_view(request, form_url, extra_context)

    def save_model(self, request, obj, form, change):
        for file in request.FILES.values():
            obj.user = User.objects.filter(id=request.POST.get("user")).first()
            obj.title = request.POST.get("title")
            obj.image.save(file.name, file.file)
            obj.save()

It uploads correctly to S3, but it doesn’t save the instance and throws this error:

TypeError at /admin/media/media/add/
expected string or bytes-like object

I’m not sure what is wrong here, maybe the fact that the upload is not done yet so the DB transaction is rolled back, but I can’t figure out what do to.

Asked By: andrepz

||

Answers:

Try to change this

obj.image.save(file.name, file.file)

to

obj.image.save(file.name, file)
Answered By: Olga Riabukha

After some time I found out the issue. Instead of using for file in request.FILES.values() or even setting this in the save_model I’ve created a separate Form (code below) and overwrited the self.form attribute in the add_view method of the ModelAdmin class.

--------------
admin.py
--------------
...

class MediaModelAdmin(admin.ModelAdmin):
...

    def add_view(self, request, form_url='', extra_context=None):
        self.form = BulkUploadMediaAdminValidationForm
        return super().add_view(request, form_url, extra_context)
...

--------------
forms.py
--------------
...

class BulkUploadMediaAdminValidationForm(forms.ModelForm):
    user = forms.ModelChoiceField(User.objects.filter(is_active=True))
    title = forms.CharField(max_length=255, required=True)
    category = forms.ChoiceField(choices=ContentType.choices(), required=True)
    location = forms.PointField(widget=LocationWidget(), label=u"Location (lng, lat)", required=True)
    image = forms.ImageField(
        widget=PreviewMultipleFileInput(attrs={"multiple": True}), # this is forms.ClearableFileInput with customized CSS and JS to show file thumbnails
        label=_(u"Image - select multiple with Ctrl or Shift"),
    )

    class Meta:
        model = Media
        fields = [
            "user",
            "title",
            "category",
            "location",
            "image",
            "taken_at",
        ]

    def save(self, *args, **kwargs):
        data_dict = self.cleaned_data.copy()
        data_dict["is_approved"] = timezone.now()
        del data_dict["image"]
        instance = None
        for f in self.files.getlist("image"):
            instance = Media(**data_dict)
            instance.image.save(f.name, f.file)
            instance.save()
        return instance

    def save_m2m(self):
        pass

The main issue here was trying to access request.FILES.values(). I’m not sure why but if you try to access a list of files it’ll return just a single file.
You need to use self.files.getlist("image") in order to correctly retrieve an array of the files.

This happened in Django 3.2, so I’m not sure if it’s fixed in newer versions.

Answered By: andrepz