djangopython-3.xdjango-formsdjango-viewsdjango-formtools

Changing an image in Django inlineformset_factory puts it to the end of the list


Supposing I am making a "How to" Django webapp where users make posts about how to- do different things like.

You get the idea. I have made the post create view for this.Now when members make the post.They add additional images to the post

Example: "How to" make a rope

Now they have to show images step by step how the rope is made

I am using Django formsets along with my post model to achieve this. Everything is working absolutely fine in create view. no problems. But in update view things break.

The Problem

The problem is when a user wants to EDIT their post and switch image number 2. from their post to a different image. Even though they changed the 2nd image. That image now ends up at the very end of the list. Making the user to re-upload all the images. To bring back the Order. Making my app look buggy.

Example: Lets assume user has the below post

main post Title 
" Some description "
Main Image = Post_image.jpg  

1st Image = A.jpg
   Image Title
   Image description
2nd Image = B.jpg
   Image Title
   Image description
3rd Image = C.jpg
   Image Title
   Image description
4st Image = D.jpg
    Image Title
     Image description
5th Image = E.jpg
     Image Title
     Image description
6th Image = F.img
     Image Title
     Image description

Now if I changed 2nd image B.jpg to b.jpg b.jpg moves to the very end of the list and you have the order as A, C, D, E, F, b

Below are my models:

 class Post(models.Model):
    user = models.ForeignKey(User, related_name='posts')
    created_at = models.DateTimeField(auto_now_add=True)
    title = models.CharField(max_length=250, unique=True)
    slug = models.SlugField(allow_unicode=True, unique=True,max_length=500)
    post_image = models.ImageField()
    message = models.TextField()

class Prep (models.Model): #(Images)
    post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='post_prep')
    image = models.ImageField(upload_to='images/', blank=True, null=True, default='')
    image_title = models.CharField(max_length=100, default='')
    image_description = models.CharField(max_length=250, default='')
    sequence = models.SmallIntegerField() ###########################ADDED THIS

    class Meta:    ###########################ADDED THIS
    unique_together = (('post', 'sequence'),) ###########################ADDED THIS
    ordering = ['sequence']  ###########################ADDED THIS

My post create view

def post_create(request):
    ImageFormSet = modelformset_factory(Prep, fields=('image', 'image_title', 'image_description'), extra=12, max_num=12,
                                        min_num=2)
    if request.method == "POST":
        form = PostForm(request.POST or None, request.FILES or None)
        formset = ImageFormSet(request.POST or None, request.FILES or None)
        if form.is_valid() and formset.is_valid():
            instance = form.save(commit=False)
            instance.user = request.user
            instance.save()
            post_user = request.user
            for index, f in enumerate(formset.cleaned_data): #######CHANGED THIS
                try: ##############CHANGED THIS
                    photo = Prep(sequence=index, post=instance, image=f['image'], 
                             image_title=f['image_title'], image_description=f['image_description'])
                    photo.save()
                except Exception as e:
                    break

            return redirect('posts:single', username=instance.user.username, slug=instance.slug)
    else:
        form = PostForm()
        formset = ImageFormSet(queryset=Prep.objects.none())
    context = {
        'form': form,
        'formset': formset,
    }
    return render(request, 'posts/post_form.html', context)

My post Edit View:

class PostPrepUpdate(LoginRequiredMixin, UpdateView):
    model = Post
    fields = ('title', 'message', 'post_image')
    template_name = 'posts/post_edit.html'
    success_url = reverse_lazy('home')

    def get_context_data(self, **kwargs):
        data = super(PostPrepUpdate, self).get_context_data(**kwargs)
        if self.request.POST:
            data['prep'] = PrepFormSet(self.request.POST, self.request.FILES, instance=self.object)
        else:
            data['prep'] = PrepFormSet(instance=self.object)
        return data

    def form_valid(self, form):
        context = self.get_context_data()
        prep = context['prep']
        with transaction.atomic():
            self.object = form.save()

            if prep.is_valid():
                prep.instance = self.object
                prep.save()
        return super(PostPrepUpdate, self).form_valid(form)

My Forms.py

class PostEditForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = ('title', 'message', 'post_image', 'group', )


class PrepForm(forms.ModelForm): #####################CHANGED THIS
    class Meta:
        model = Prep
        fields = ('image', 'image_title', 'image_description', 'sequence')


PrepFormSet = inlineformset_factory(Post, Prep, form=PrepForm, extra=5, max_num=7, min_num=2)

***Need help fixing this issue. Example if they change Image 2. Then it should stay at Number 2 position and not move to the end of the list


Solution

  • Currently you don't save the order of the images, relying on the fact that they are displayed in the same order as they are created. Adding a field in Prep containing the place of the image in the sequence of images would help:

    class Prep (models.Model):
        # ...
        nr = SmallIntegerField()
        #...
        class Meta:
            unique_together = (('post', 'nr'),)
    

    The unique_together constraint ensures that every number is only used once per post. This also allows reordering of images within a post without deleting and recreating all Prep objects.

    On displaying the post, you'd have to order the Prep objects by nr.


    As for populating the new column, since there's no single default value that makes sense, the easiest approach might be:

    unique_together needs string parameters (it cannot access the fields in the outer class); thanks for catching that.


    On the edit form, you'd want to include the new field so that users can swap the order of two images by swapping their indexes. You just have to provide them a meaningful error message if they use duplicate indexes.

    When creating however, your users don't need to specify the order explicitly as it is implicit in the sequence of the images in the formset. So I'd suggest changing your for loop like this:

    for index, f in enumerate(formset.cleaned_data):
        # ...
        photo = Prep(nr=index,
                     # add all other fields
                    )
    

    Use nr = index + 1 for human-friendly indexes starting with 1. In fact, index or image_index might be a better name for the field than nr.