djangodjango-forms

Django: Adding a field which allows multiple images


forms.py

class SellerForm(forms.ModelForm):
    username = forms.CharField()

    class Meta:
        model = Seller
        fields = [
            'username',
            'first_name',
            'last_name',
            'country',
            'image_of_seller',
            'city',
            'date_of_birth',
            'describe_yourself'
        ]
    
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['username'].initial = self.instance.seller.username
        
    @transaction.atomic
    def save(self, commit=True):
        seller = super().save(commit=False)
        user = seller.seller
        self.instance.seller.username = self.cleaned_data['username']
        
        if commit:
            seller.save()
            user.save()
        
        return seller

views.py

class SellerUpdateView(UpdateView):
    model = Seller
    form_class = SellerForm
    template_name = 'core/seller_update_form.html'
    
    def get_object(self, queryset=None):
        return self.request.user.seller

models.py

class Seller(models.Model):
    seller = models.OneToOneField(User, on_delete=models.CASCADE)
    first_name = models.CharField(max_length=50, null=False, blank=False)
    last_name = models.CharField(max_length=50, default=False)
    country = models.CharField(max_length=50, null=False, blank=False)
    identification = models.ImageField(null=False, blank=False)
    image_of_seller = models.ImageField(null=False, blank=True)  # I want this to accept multiple files, which are images.
    city = models.CharField(max_length=30)
    date_of_birth = models.DateField(null=False, blank=False)
    describe_yourself = models.TextField(null=False, blank=False)
    slug = models.SlugField(unique=True, blank=True)
    
    def __str__(self):
        return self.seller.username
    
    def save(self, *args, **kwargs):
        self.slug = slugify(self.seller.username)
        super().save(*args, **kwargs)

I know I can create another model with a ForeignKey to the Seller model, then have it inline while creating ModelAdmin. But is there a way to have this for the form instead? I want the user to be able to update their pictures.

UPDATE

I am thinking of something that behaves like StackedInline, like the example below, but instead rendering it not in admin, but in the SellerForm.

class Seller(models.Model):
    # Changed from seller to user, just for readability. 
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    first_name = models.CharField(max_length=50, null=False, blank=False)
    last_name = models.CharField(max_length=50, default=False)
    country = models.CharField(max_length=50, null=False, blank=False)
    identification = models.ImageField(null=False, blank=False)
    city = models.CharField(max_length=30)
    date_of_birth = models.DateField(null=False, blank=False)
    describe_yourself = models.TextField(null=False, blank=False)
    slug = models.SlugField(unique=True, blank=True)
    age = models.IntegerField(default=0)

    @property
    def calculate_age(self):
        today = datetime.date.today()
        year = today.strftime("%Y")
        current_year = int(year)
        user_year = int(self.date_of_birth.year)
        age = current_year - user_year
        self.age = age 
        return age
    
    def __str__(self):
        return self.seller.username
    
    def save(self, *args, **kwargs):
        self.calculate_age
        self.slug = slugify(self.seller.username)
        super().save(*args, **kwargs)


class ImageOfSeller(models.Model):
    seller = models.ForeignKey(Seller, on_delete=models.CASCADE)
    image = models.ImageField()

admin.py

class ImageSellerInline(admin.StackedInline):
    model = ImageOfSeller 
    can_delete = True 
    verbose_name_plural = 'seller_image'
    extra = 0
    
class SellerAdmin(admin.ModelAdmin):
    inlines = [
        ImageSellerInline,
    ]

admin.site.register(Seller, SellerAdmin)

Solution

  • First, I created my own field instead, which allows me to upload multiple files: MultipleFileField.

    from django.forms import ClearableFileInput, FileField 
    
    class MultipleFileInput(ClearableFileInput):
        allow_multiple_selected = True
    
    class MultipleFileField(FileField):
        def __init__(self, *args, **kwargs):
            kwargs.setdefault("widget", MultipleFileInput())
            super().__init__(*args, **kwargs)
    
        def clean(self, data, initial=None):
            single_file_clean = super().clean
            if isinstance(data, (list, tuple)):
                result = [single_file_clean(d, initial) for d in data]
            else:
                result = single_file_clean(data, initial)
            return result
    

    Then, I added validate_image_file_extension, which validates if the image is an actual image, using the default_validators attribute.

    from django.forms import ClearableFileInput, FileField 
    from django.core.validators import validate_image_file_extension
    
    class MultipleFileInput(ClearableFileInput):
        allow_multiple_selected = True
    
    class MultipleFileField(FileField):
        default_validators = [validate_image_file_extension]
    
        def __init__(self, *args, **kwargs):
            kwargs.setdefault("widget", MultipleFileInput())
            super().__init__(*args, **kwargs)
    
        def clean(self, data, initial=None):
            single_file_clean = super().clean
            if isinstance(data, (list, tuple)):
                result = [single_file_clean(d, initial) for d in data]
            else:
                result = single_file_clean(data, initial)
            return result
    

    Then, I use my field, MultipleFileField, for my form, something like this:

    image_of_seller = MultipleFileField()
    

    I then use cleaned_data on MultipleFileField in my form, something like this:

    def clean(self):
        # other stuff ...
        self.image_of_seller = self.cleaned_data.get('image_of_seller')
    

    Then, I use another model, which has a ForeignKey to my User model, and create objects based on the files.

    Example

    for file in images:
        ImageSeller.objects.create(
            seller=seller_user,
            image=file,
        )
    

    UPDATE

    Use bulk_create for the example above