Question

I'm using a formset to upload images and link them to a model using a manytomany relationship. I display the formset below the status form so an user can fill the status field + add an image then click « save » or « save and add another image ».

In the last case, it should display the status form prefilled, then within the formset a block with the usual stuff to deal with an uploaded file (a link to the image, a checkbox to delete and a file input to upload a different image) and finally a blank file input to upload a 2nd image. Right now, I have 2 blank file input.

The formset knows there is already 1 image uploaded but it doesn't display anything to manage it. Is this a normal behavior of ImageField in formset or here is something wrong in the way I provide formset data ?

Here is the code:

# Models
class Image(models.Model):
    image = ImageField(upload_to='uploads/status/images/',)


class Status(models.Model):
    author = models.ForeignKey(User,)
    images = models.ManyToManyField(Image, blank=True, null=True)
    body = models.TextField()

# Forms
class StatusForm(forms.ModelForm):
    class Meta:
        model = Status
        fields = ['body',]


class ImageForm(forms.ModelForm):
    class Meta:
        model = Image
        fields = ['image',]

# View
@csrf_protect
def form(request, id=None, template_name='status/form.html'):
    if id:
        status = get_object_or_404(Status, pk=id)
        images = status.images.all().values()
    else:
        status = Status(author=request.user)
        images = None

    if request.method == 'POST':
        form = StatusForm(request.POST, instance=status)
        formset = ImageFormSet(request.POST, request.FILES, initial=images)

        if form.is_valid():
            try:
                status = form.save()

                if formset.is_valid():
                    try:
                        for form in formset:
                            image = form.save()
                            status.images.add(image)
                            status.save()

                        if request.POST.get('_add_image', None):
                            return HttpResponseRedirect(reverse('status_edit', args=[status.id]))
                        else:
                            messages.success(request, 'Status saved')
                            return HttpResponseRedirect(request.POST.get('next', '/'))
                    except:
                        messages.error(request, 'Technical error')
            except:
                messages.error(request, 'Technical error')
    else:
        form = StatusForm(instance=status)
        formset = ImageFormSet(initial=images)

    return render_to_response(template_name, {
        'form': form,
        'formset': formset,
        'next': request.GET.get('next', '/'),
    }, context_instance=RequestContext(request))

Template:

<form action="" method="post" enctype="multipart/form-data">
    {% csrf_token %}
    {{ form.non_field_errors }}
    <input type="hidden" name="next" value="{{ next }}" />
    {{ form.body }}
    {{ form.body.errors }}
    <hr />
    {{ formset }}
    <hr />
    <div class="pull-right">
        <input name="_add_image" class="btn" type="submit" value="Add another image">
        <input name="_complete" class="btn btn-primary" type="submit" value="Save">
    </div>
</form>
Was it helpful?

Solution

It's "normal" for a M2M relationship. As they are currently designed, inline formsets require that there be a foreign key back to the main object being edited. The only place that exists is on the join table (the "through" model in Django parlance) for the M2M relationship. That model itself usually just consists of two foreign keys, one to each side of the relationship.

I've toyed with the idea of creating a custom form that includes all the fields on the model you want to edit along with the normal select field to choose an existing one, and then logic to create a new instance from the fields in the form and assign that to the only real field, the foreign key. However, it's a lot of code to write, most of which is unique and specific to each usage scenario, and it's brittle (you have to be cognizant to always keep the form and the model in sync). I eventually abandoned the idea. Perhaps, one day, Django will provide some mechanism for this, but for now, it's best to just deal.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top