Search code examples
djangoformsdjango-formsinline-formsetdjango-file-upload

Using a Django FileField in an inline formset


having issues getting the file to upload in my app. A User submits a report and can add attachments (through a foreign key relationship). I've got the inline form showing up and will work if I leave it blank, but when I try to upload a file then submit the form I get a 500 error. The base report is made, but the attachment that's getting inlined doesn't get saved.

forms.py

class ReportForm(forms.ModelForm):
    accidentDate = forms.DateField(widget=SelectDateWidget(
                                   empty_label=("Choose Year",
                                                 "Choose Month",
                                                 "Choose Day"),
                                   ),
                                   label=(u'Accident Date'),
                                   initial=timezone.now())
    claimNumber = forms.CharField(max_length=50,
                                  label=(u'Claim Number'),
                                  required=True)
    damageEstimate = forms.DecimalField(max_digits=6, decimal_places=2,
                                        label=(u'Damage Estimate'))


    description = forms.CharField(widget=forms.Textarea(attrs={'rows': 5,
                                                               'cols': 80}
                                                        ),
                                  label=(u'Description'),
                                  required=True)
    drivable = forms.BooleanField(label=(u'Drivable'), 
                                  required=False)
    receivedCheck = forms.CharField(max_length=30, label=(u'Who Received Check'))
    reported = forms.BooleanField(label=(u'Reported to Insurance Company'), 
                                  required=False)
    reportedDate = forms.DateField(widget=SelectDateWidget(
                                    empty_label=("Choose Year",
                                                 "Choose Month",
                                                 "Choose Day"),
                                ),
                               initial=timezone.now(),
                               label=(u'Date Reported to Insurance'))
    repairsScheduled = forms.DateField(widget=SelectDateWidget(
                                    empty_label=("Choose Year",
                                                 "Choose Month",
                                                 "Choose Day"),
                                ),
                               initial=timezone.now(),
                               label=(u'Scheduled Repair Date'))
    repairsCompleted = forms.DateField(widget=SelectDateWidget(
                                    empty_label=("Choose Year",
                                                 "Choose Month",
                                                 "Choose Day"),
                                ),
                               initial=timezone.now(),
                               label=(u'Repair Completion Date'))
    repairsPaid = forms.BooleanField(label=(u'Repairs Paid'), 
                                  required=False)
    subrogationReceived = forms.BooleanField(label=(u'Subrogation Received'), 
                                  required=False)
    subrogationDate = forms.DateField(widget=SelectDateWidget(
                                    empty_label=("Choose Year",
                                                 "Choose Month",
                                                 "Choose Day"),
                                ),
                               initial=timezone.now(),
                               label=('Subrogation Date'))

class Meta:
    model = Report
    exclude = ('driver',)


ReportAttachmentFormSet = inlineformset_factory(Report,  # parent form
                                                    ReportAttachment,  # inline-form
                                                    fields=['title', 'attachment'], # inline-form fields
                                                    # labels for the fields
                                                    labels={
                                                        'title': (u'Attachment Name'),
                                                        'attachment': (u'File'),
                                                    },
                                                    # help texts for the fields
                                                    help_texts={
                                                        'title': None,
                                                        'attachment': None,
                                                    },
                                                    # set to false because cant' delete an non-exsitant instance
                                                    can_delete=False,
                                                    # how many inline-forms are sent to the template by default
                                                    extra=1)

relevant views.py CreateView

class ReportCreateView(CreateView):
    model = Report
    form_class = ReportForm
    object = None

    def get(self, request, *args, **kwargs):
        """
        Handles GET requests and instantiates blank versions of the form
        and its inline formsets.
        """
        self.object = None
        form_class = self.get_form_class()
        form = self.get_form(form_class)
        report_attachment_form = ReportAttachmentFormSet()
        return self.render_to_response(
                  self.get_context_data(form=form,
                                        report_attachment_form=report_attachment_form,
                                        )
                                     )

    def post(self, request, *args, **kwargs):
        """
        Handles POST requests, instantiating a form instance and its inline
        formsets with the passed POST variables and then checking them for
        validity.
        """
        self.object = None
        form_class = self.get_form_class()
        form = self.get_form(form_class)
        report_attachment_form = ReportAttachmentFormSet(self.request.POST, self.request.FILES)
        if form.is_valid() and report_attachment_form.is_valid():
            return self.form_valid(form, report_attachment_form)
        else:
            return self.form_invalid(form, report_attachment_form)

    def form_valid(self, form, report_attachment_form):
        """
        Called if all forms are valid. Creates Report instance along with the
        associated ReportAttachment instances then redirects to success url
        Args:
            form: Report Form
            report_attachment_form: Report attachment Form

        Returns: an HttpResponse to success url

        """
        self.object = form.save(commit=False)
        # pre-processing for Report instance here...

        self.object.driver = Profile.objects.get(user=self.request.user)
        self.object.save()

        # saving ReportAttachment Instances
        report_attachments = report_attachment_form.save(commit=False)
        for ra in report_attachments:
            #  change the ReportAttachment instance values here
            #  ra.some_field = some_value
            ra.save()

        return HttpResponseRedirect(self.get_success_url())

    def form_invalid(self, form, report_attachment_form):
        """
        Called if a form is invalid. Re-renders the context data with the
        data-filled forms and errors.

        Args:
            form: Report Form
        report_attachment_form: Report Attachment Form
        """
        return self.render_to_response(
                 self.get_context_data(form=form,
                                       report_attachment_form=report_attachment_form
                                       )
        )

report_form.html

{% block body %}
<p>
<form action="" method="post" enctype="multipart/form-data">
    {% csrf_token %}
    <fieldset>
            {{ form.non_field_errors }}
            {% for field in form %}
            <div class="row">
                <div class="col-md-3">{% bootstrap_label field.label %}</div>
                <div class="col-md-8">{% bootstrap_field field show_label=False %}</div>
            </div>
            {% endfor %}
    </fieldset>
    <fieldset>
            <legend>Attachments</legend>
            {{ report_attachment_form.management_form }}
            {{ report_attachment_form.non_form_errors }}
                            {% for form in report_attachment_form %}
                    <div class="inline {{ report_attachment_form.prefix }}">
                    {% for field in form.visible_fields %}
                            <div class="row">
                                    <div class="col-md-3">{% bootstrap_label field.label %}</div>
                                    <div class="col-md-8">{% bootstrap_field field show_label=False %}</div>
                            </div>
                    {% endfor %}
                    </div>
            {% endfor %}
    </fieldset>
    <div class="row">
            <div class="col-md-1">
                    <input type="submit" class="btn btn-groppus-primary bordered" value="Submit" />
            </div>
    </div>
    </form>
</p>
{% endblock %}

{% block scripts %}
    <script src="{% static 'formset/jquery.formset.js' %}"></script>
    <script type="text/javascript">
            $(function() {
                    $(".inline.{{ report_attachment_form.prefix }}").formset({
                            prefix: "{{ report_attachment_form.prefix }}", // The form prefix for your django formset
                            addCssClass: "btn btn-block btn-primary bordered inline-form-add", // CSS class applied to the add link
                            deleteCssClass: "btn btn-block btn-primary bordered", // CSS class applied to the delete link
                            addText: 'Add another attachment', // Text for the add link
                            deleteText: 'Remove attachment above', // Text for the delete link
                            formCssClass: 'inline-form' // CSS class applied to each form in a formset
                    })
            });
    </script>
{% endblock %}

Having a hard time figuring this one out since it doesn't give me any sort of traceback to deal with, I included the enc-type in the form, and I'm passing request.FILES in the views.py file. I've got file uploads working in normal forms, but inline is proving to be trouble.

Let me know if you need me to clarify anything, any help is appreciated :)

UPDATE: Got a traceback by making the CreateView csrf exempt using @method_decorator(csrf_exempt, name='dispatch')

which gives me a ValueError: save() prohibited to prevent data loss due to unsaved related object 'report'. From the trace I can see that my file is in fact making it into memory, and the issue is here:traceback

Will continue updating as I progress, seems like this method of saving FK objects worked flawlessly pre-1.8 so if I'm lucky it's a quick fix to get the object to save right.


Solution

  • All I needed to do was pass the instance of the Report Form I was working with along to the formset, nice and easy. So in my CreateView's post method, I changed the declaration of report_attachment_form to report_attachment_form = ReportAttachmentFormSet(self.request.POST, self.request.FILES, instance=form.instance) and we're good as gold.