Search code examples
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.

  • "How to" make a rope
  • "How to" make an earthen pot
  • "How to" learn to ride a bike

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

  • This has Post Title = How to make a rope
  • Post description = "Some description"
  • Post Image = Main Image

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

  • Image 1: Do this 1st
  • Image 2: Do this 2nd

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:

    • Add the nr field without the unique_together constraint and with null=True first; migrate the changes.
    • After migration, loop over the existing Prep objects of each Post in the current order and assign them ascending numbers.
    • After that remove null=True, add unique_together and migrate again.

    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.