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
tob.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
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:
nr
field without the unique_together
constraint and with null=True
first; migrate the changes.Prep
objects of each Post
in the current order and assign them ascending numbers. 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
.