18

In the django docs, there's an example of using inlineformset_factory to edit already created objects

https://docs.djangoproject.com/en/dev/topics/forms/modelforms/#using-an-inline-formset-in-a-view

I changed the example to be this way:

def manage_books(request):
    author = Author()
    BookInlineFormSet = inlineformset_factory(Author, Book, fields=('title',))
    if request.method == "POST":
        formset = BookInlineFormSet(request.POST, request.FILES, instance=author)
        if formset.is_valid():
            formset.save()
            return HttpResponseRedirect(author.get_absolute_url())
    else:
        formset = BookInlineFormSet(instance=author)
    return render_to_response("manage_books.html", {
        "formset": formset,
    })

With the above, it renders only the inline model without the parent model.

To create a new object, say Author, with multiple Books associated to, using inlineformset_factory, what's the approach?

An example using the above Author Book model from django docs will be helpful. The django docs only provided example of how to edit already created object using inlineformset_factory but not to create new one

KhoPhi
  • 9,660
  • 17
  • 77
  • 128
  • Take a look at this answer [http://stackoverflow.com/questions/1113047/creating-a-model-and-related-models-with-inline-formsets](http://stackoverflow.com/questions/1113047/creating-a-model-and-related-models-with-inline-formsets) – onyeka Apr 21 '15 at 10:06
  • @onyeka I've been there already. Followed the steps. Still renders without the parent model. I've tried the exact example in the django docs to see if I could edit an already existing objects. I still only get the Book model rendered. Tried both in views and shell, still spits out only the Book model. That answer was from 2011. I guess lots have changed since then even with django docs being a bit ambiguous on that inlineformset_factory – KhoPhi Apr 21 '15 at 10:20

5 Answers5

16

I've done that using Django Class-Based Views.

Here's my approach:

models.py

from django.db import models

class Author(models.Model):
    name = models.CharField(max_length=100)


class Book(models.Model):
    author = models.ForeignKey(Author)
    title = models.CharField(max_length=100)

forms.py

from django.forms import ModelForm
from django.forms.models import inlineformset_factory

from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Fieldset

from .models import Author, Book

class AuthorForm(ModelForm):
    class Meta:
        model = Author
        fields = ('name', )

    @property
    def helper(self):
        helper = FormHelper()
        helper.form_tag = False # This is crucial.

        helper.layout = Layout(
            Fieldset('Create new author', 'name'),
        )

        return helper


class BookFormHelper(FormHelper):
    def __init__(self, *args, **kwargs):
        super(BookFormHelper, self).__init__(*args, **kwargs)
        self.form_tag = False
        self.layout = Layout(
            Fieldset("Add author's book", 'title'),
        )


BookFormset = inlineformset_factory(
    Author,
    Book,
    fields=('title', ),
    extra=2,
    can_delete=False,
)

views.py

from django.views.generic import CreateView
from django.http import HttpResponseRedirect

from .forms import AuthorForm, BookFormset, BookFormHelper
from .models import Book, Author

class AuthorCreateView(CreateView):
    form_class = AuthorForm
    template_name = 'library/manage_books.html'
    model = Author
    success_url = '/'

    def get(self, request, *args, **kwargs):
        self.object = None
        form_class = self.get_form_class()
        form = self.get_form(form_class)
        book_form = BookFormset()
        book_formhelper = BookFormHelper()

        return self.render_to_response(
            self.get_context_data(form=form, book_form=book_form)
        )

    def post(self, request, *args, **kwargs):
        self.object = None
        form_class = self.get_form_class()
        form = self.get_form(form_class)
        book_form = BookFormset(self.request.POST)

        if (form.is_valid() and book_form.is_valid()):
            return self.form_valid(form, book_form)

        return self.form_invalid(form, book_form)

    def form_valid(self, form, book_form):
        """
        Called if all forms are valid. Creates a Author instance along
        with associated books and then redirects to a success page.
        """
        self.object = form.save()
        book_form.instance = self.object
        book_form.save()

        return HttpResponseRedirect(self.get_success_url())

    def form_invalid(self, form, book_form):
        """
        Called if whether a form is invalid. Re-renders the context
        data with the data-filled forms and errors.
        """
        return self.render_to_response(
            self.get_context_data(form=form, book_form=book_form)
        )

    def get_context_data(self, **kwargs):
        """ Add formset and formhelper to the context_data. """
        ctx = super(AuthorCreateView, self).get_context_data(**kwargs)
        book_formhelper = BookFormHelper()

        if self.request.POST:
            ctx['form'] = AuthorForm(self.request.POST)
            ctx['book_form'] = BookFormset(self.request.POST)
            ctx['book_formhelper'] = book_formhelper
        else:
            ctx['form'] = AuthorForm()
            ctx['book_form'] = BookFormset()
            ctx['book_formhelper'] = book_formhelper

        return ctx

urls.py

from django.conf.urls import patterns, url
from django.views.generic import TemplateView

from library.views import AuthorCreateView

urlpatterns = patterns('',
    url(r'^author/manage$', AuthorCreateView.as_view(), name='handle-books'),
    url(r'^$', TemplateView.as_view(template_name='home.html'), name='home'),
)

manage_books.html

{% load crispy_forms_tags %}

<head>
  <!-- Latest compiled and minified CSS -->
  <link rel="stylesheet"
    href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css">
</head>

<div class='container'>
  <form method='post'>
    {% crispy form %}
    {{ book_form.management_form }}
    {{ book_form.non_form_errors }}

    {% crispy book_form book_formhelper %}
    <input class='btn btn-primary' type='submit' value='Save'>
  </form>
<div>

Notice:

  • This is a simple runable example that use the inlineformset_factory feature and Django generic Class-Based Views
  • I'm assumming django-crispy-forms is installed, and it's properly configured.
  • Code repository is hosted at: https://bitbucket.org/slackmart/library_example

I know it's more code that the showed solutions, but start to using Django Class-Based Views is great.

slackmart
  • 4,754
  • 3
  • 25
  • 39
  • 1
    No. Its fine. I like the cbv approach too. I think eventually, that should provide the shortest, simplest way to doing this inline formset kinda thing. Thanks for the input. Currently using the function based approach, but my next try at inlineformsets, will give the cbv approach a try. – KhoPhi Feb 05 '16 at 12:27
11

I didn't read your question properly at first. You need to also render the the form for the parent model. I haven't tested this, I'm going off what I've done before and the previously linked answer, but it should work.

UPDATE

If you're using the view to both and edit, you should check for an Author ID first. If there's no ID, it'll render both forms as a new instance, whereas with an ID it'll, fill them with the existing data. Then you can check if there was a POST request.

def manage_books(request, id):

    if id:
        author = Author.objects.get(pk=author_id)  # if this is an edit form, replace the author instance with the existing one
    else: 
        author = Author()
    author_form = AuthorModelForm(instance=author) # setup a form for the parent

    BookInlineFormSet = inlineformset_factory(Author, Book, fields=('title',))
    formset = BookInlineFormSet(instance=author)

    if request.method == "POST":
        author_form = AuthorModelForm(request.POST)

        if id: 
            author_form = AuthorModelForm(request.POST, instance=author)

        formset = BookInlineFormSet(request.POST, request.FILES)

        if author_form.is_valid():
            created_author = author_form.save(commit=False)
            formset = BookInlineFormSet(request.POST, request.FILES, instance=created_author)

            if formset.is_valid():
                created_author.save()
                formset.save()
                return HttpResponseRedirect(created_author.get_absolute_url())

    return render_to_response("manage_books.html", {
        "author_form": author_form,
        "formset": formset,
    })
onyeka
  • 1,517
  • 13
  • 18
  • Just tried. First of all, there is a typo at it should be else: author_form = AuthorModelForm() I tried, and still no rendering of the parent model. In IRC, a guy suggested using modelform_factory. Any idea abou that? – KhoPhi Apr 21 '15 at 11:38
  • I edited my answer. There should be an instance for the main form, sorry. I just tried this code myself (without the POST part, however), and it renders both forms. – onyeka Apr 21 '15 at 12:27
  • When I save the above snippet, I get the error: save() got an unexpected keyword argument 'instance' referencing: formset.save(instance=created_author) as the culprit. – KhoPhi Apr 23 '15 at 19:42
  • Ah, sorry. AGAIN (what is my deal). I messed it up. Try the edit. – onyeka Apr 23 '15 at 20:48
  • Finally, Working! In fact, the way the formset thing is, its easy to miss something. I couldn't notice that though. Thanks – KhoPhi Apr 23 '15 at 21:26
  • Now to the editing part of the created object, the info here doesn't work, obviously: https://docs.djangoproject.com/en/1.8/topics/forms/modelforms/#using-an-inline-formset-in-a-view . I passed in the instances, and the Author prepopulates in the form, but the Book doesn't. My changes made here: http://pastebin.com/XzVpXmLN – KhoPhi Apr 24 '15 at 00:06
  • Not sure what your error is at this point, but just by looking at it, I'm pretty sure you'll need to rearrange the code to check what form it should render BEFORE the `if request == POST secton.` – onyeka Apr 26 '15 at 09:54
  • I just again tried your snippet. Works, but half-way. At a url like say: /edit/1 I get the author form with all the books as inlines nicely. Upon submission, it redirects to just /edit/ which throws a 404, page not found. Any reason why? – KhoPhi May 12 '15 at 17:35
  • hm, it shouldn't, seeing as the redirect url says to go to the absolute url. Does the form save? – onyeka May 13 '15 at 10:11
  • It doesnt save. It simply redirects to /edit/ without saving. The form in template also has the action="." Which is supposed to send the post to the same view but it doesn't. – KhoPhi May 13 '15 at 10:17
  • Did you change anything? Because some comments above you said it was working. – onyeka May 13 '15 at 10:20
  • The comments above was for the **saving** of the object. The *editing* of an object, I've not had success with. I'm close as in the object I want to edit comes up for editing but it doesn't do the update – KhoPhi May 13 '15 at 10:23
  • Well, you need to check if there's an id, e.g `edit/3` and if there is, attach that instance to the form if it was a post. Please see edit above. – onyeka May 14 '15 at 10:33
  • What was causing the form posting back to /edit/ instead of /edit/1 was because I was using action='.' on the form. I changed it to constructing the url using the {% url 'edit_author' author_id %} where author_id was passed into template via the view function. See my post below for what I eventually found to work, from views to templates. Thanks for your extensive assistance. Really appreciate. – KhoPhi May 17 '15 at 17:39
  • This solution from @onyeka is the **best** aproach on what was asked by @KhoPhi "an example of using inlineformset_factory to edit". Since it's an **edit** the `else` clause is not required (in the **POST** segment). Can't be any cleaner. Moreover, here no 3rd party solutions are needed like `django-crispy-forms`. – Love Putin Not War Apr 28 '20 at 04:04
3

I am posting my final solutions, as per extensive assistant given by Onyeka.

Below I post the Add and Edit solutions of using inlineformset_factory of Django using the Author and Book example found in the Django Docs.

First, the Adding of Author object, with 3 extras of Book object to be appended.

Obviously, this goes into your views.py

def add_author(request):
    '''This function creates a brand new Author object with related Book objects using inlineformset_factory'''
    author = Author()
    author_form = AuthorModelForm(instance=author) # setup a form for the parent
BookInlineFormSet = inlineformset_factory(Author, Book, fields=('title',))

if request.method == "POST":
    author_form = AuthorModelForm(request.POST)
    formset = BookInlineFormSet(request.POST, request.FILES)

    if author_form.is_valid():
        created_author = author_form.save(commit=False)
        formset = BookInlineFormSet(request.POST, request.FILES, instance=created_author)

        if formset.is_valid():
            created_author.save()
            formset.save()
            return HttpResponseRedirect(created_author.get_absolute_url())
else:
    author_form = AuthorModelForm(instance=author)
    formset = BookInlineFormSet()

return render(request, "add_author.html", {
    "author_form": author_form,
    "formset": formset,
})


def edit_author(request, author_id):
    '''This function edits an Author object and its related Book objects using inlineformset_factory'''
    if id:
        author = Author.objects.get(pk=author_id)  # if this is an edit form, replace the author instance with the existing one
    else:
        author = Author()
    author_form = AuthorModelForm(instance=author) # setup a form for the parent
BookInlineFormSet = inlineformset_factory(Author, Book, fields=('title',))
formset = BookInlineFormSet(instance=author)

if request.method == "POST":
    author_form = AuthorModelForm(request.POST)

    if id:
        author_form = AuthorModelForm(request.POST, instance=author)

    formset = BookInlineFormSet(request.POST, request.FILES)

    if author_form.is_valid():
        created_author = author_form.save(commit=False)
        formset = BookInlineFormSet(request.POST, request.FILES, instance=created_author)

        if formset.is_valid():
            created_author.save()
            formset.save()
            return HttpResponseRedirect(created_author.get_absolute_url())

return render(request, "edit_author.html", {
    "author_id": author_id, # This author_id is referenced 
                            # in template for constructing the posting url via {% url %} tag
    "author_form": author_form,
    "formset": formset,
})

This part goes into your urls.py, assuming views have been imported, and urlpatterns constructed already.

...
    url(r'^add/book/$', views.add_author, name='add_author'),
    url(r'^edit/(?P<author_id>[\d]+)$', views.edit_author, name='edit_author'),
...

Now to the templates part. The edit Author object template (edit_author.html) looks like this (no styling applied)

<form action="{% url 'edit_book' author_id %}" method="POST" >
<!-- See above: We're using the author_id that was passed to template via views render of the edit_author(...) function -->
{% csrf_token %} <!-- You're dealing with forms. csrf_token must come -->
{{ author_form.as_p }}
{{ formset.as_p }}
<input type="submit" value="submit">
</form>

To add a brand new Author object via template (add_author.html):

<form action="." method="POST" >{% csrf_token %}
{{ author_form.as_p }}
{{ formset.as_p }}
<input type="submit" value="submit">
</form>

NOTE:

Using the action='.' might appear to be a cheap way of constructing the url, whereby the form posts the form data to the same page. With this example, using the action='.' for the edit_author.html template always got the form posted to /edit/ instead of /edit/1 or /edit/2

Constructing the url using the {% url 'edit_author' author_id %} ensures the form always posts to the right url. Failing to do use the {% url %} cost me lots of hours and trouble.

Big thanks to Onyeka.

KhoPhi
  • 9,660
  • 17
  • 77
  • 128
  • You might like to **revisit** your contention on : **Using the action='.' might appear to be a cheap way of constructing the url, whereby the form posts the form data to the same page. With this example, using the action='.' for the edit_author.html template always got the form posted to /edit/ instead of /edit/1 or /edit/2**. It's suspect. – Love Putin Not War Apr 28 '20 at 04:15
1

i did exactly what you are trying : https://github.com/yakoub/django_training/tree/master/article

you need to create a separate form using the prefix attribute . then when you save you need to iterate over all books and associate them with the author you just created .

yakoub abaya
  • 69
  • 1
  • 1
  • 6
  • Couldn't get yours to work. I get a blank screen without any errors whatsoever. I cloned your repo but couldn't find your manage.py. Where's it? – KhoPhi Apr 21 '15 at 12:21
  • i don't understand why you need manage.py ? if you get blank screen then turn debugging on and see which error you receive . – yakoub abaya Apr 22 '15 at 13:01
0

This is my first django inline_formset view for create a invoice with list of invoice_item_set.

In models.py there are three models

  1. Customer it has customer data like name, mobile_no, his_address etc..
  2. Invoice it has invoice data like customer_primary_key(required), delivery_address, billed_date etc.. total of the invoice item can be achieved by getting all "invoiceitem_set.all()" as items and from that sum of all add(items.item_subtotal)
  3. InvoiceItem it has invoiceitem data like invoice_primary_key(required), item_name, quantity, price etc.. the total is calculated before the model is saves

models.py

class Customer(models.Model):
    pass

class Invoice(models.Model):
    customer_id = models.ForeignKey(Customer, on_delete=models.PROTECT)          # many - to - on relationship 
    invoice_id = models.CharField(....) 
    bill_note = models.TextField(....)
    cash_pay = models.DecimalField(....)
    upi_pay = models.DecimalField(....)

    @property
    def total_amount(self):
        bill_total = 0
        items = self.invoiceitem_set.all()
        for item in items:
            bill_total += item.item_subtotal
        return bill_total

class InvoiceItem(models.Model):
    invoice = models.ForeignKey(Invoice)  # many - to - one relationship 
    item_name = models.CharField(....)
    item_quantity = models.DecimalField(....)
    item_price = models.DecimalField(....)
    item_subtotal = models.DecimalField(....)
    
    def save(self, *args, **kwargs):
        self.item_subtotal = self.item_quantity * self.item_price
        super(InvoiceItem, self).save(*args, **kwargs)

views.py (CreateView)

from django.db import transaction
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpResponse, HttpResponseRedirect



class InvoiceCreateView(LoginRequiredMixin, CreateView):
    model = Invoice
    form_class = InvoiceForm
    template_name = 'billingsite/create_invoice.html' 

    def get_context_data(self, **kwargs):
        context = super(InvoiceCreateView, self).get_context_data(**kwargs)
        context['custom_title'] = "New Invoice"
        temp = dict()   
        temp['customer_id'] = 0
        
        if self.request.POST:
            customer_id = int(self.request.POST.get('customer_id')) or False   # custom clean method.
            
            if customer_id:
                customer_object = Customer.objects.get(pk=customer_id)
                invoice_object = Invoice.objects.filter(customer_id=customer_object).order_by('-created_time').first()
                temp = {
                    "customer_id": customer_id, "mobile_no": customer_object.mobile_no,
                    "searched_mobile_no": customer_object.raw_mobile_no,
                    "customer_name": customer_object.name, "gst_no": customer_object.goods_tax_id,
                    "pre_bal": customer_object.pending_balance, "purchased_date": "No Bills",
                    "created_date": customer_object.created_date.strftime(CUSTOM_DATE_FORMAT)
                    }

            context['formset'] = InvoiceFormSet(self.request.POST)

        else:
            context['formset'] = InvoiceFormSet()

        context['temp'] = temp
        return context

    def post(self, request, *args, **kwargs):
        self.object = None
        context = self.get_context_data()
        customer_id = int(self.request.POST.get('customer_id'))   # custom clean method.

        if customer_id and customer_id != 0:
            customer_object = Customer.objects.get(pk=customer_id)
            form_class = self.get_form_class()
            form = self.get_form(form_class)
            formsets = context['formset']

            with transaction.atomic():
                form.instance.customer_id = customer_object
                form.save(commit=False)

                if form.is_valid() and formsets.is_valid():
                    self.object = form.save()
                    messages.success(self.request, f'Invoice is Submitted.')
                    return self.form_valid(form, formsets)
                else:
                    return self.form_invalid(form, formsets)
            return reverse_lazy('InvoiceList')
        
        return self.render_to_response(context)

    def form_valid(self, form, formsets):
        formsets = formsets.save(commit=False)
        for formset in formsets:
            formset.invoice = self.object
            formset.save()
        return HttpResponseRedirect(self.get_success_url(self.object.pk))

    def form_invalid(self, form, formsets):
        return self.render_to_response(
            self.get_context_data(form=form, formset=formsets))

    def get_success_url(self, pk):
        return reverse_lazy('ViewInvoice', kwargs={'pk': pk})

urls.py

urlpatterns = [
    path('invoice/create/', views.InvoiceCreateView.as_view(), name='AddInvoice'),
]

forms.py

class InvoiceItemForm(forms.ModelForm):
    item_name = forms.CharField(label=_('Product Name'))
    item_subtotal = forms.IntegerField(required=False, label=_('Sub Total'))

    class Meta:
        model = InvoiceItem
        fields = ['item_name', 'item_quantity', 'item_price', 'item_subtotal']
        exclude = ()
        widgets = {
            "item_quantity": widgets.NumberInput(attrs={'step': '0.25'}),
            "item_price": widgets.NumberInput(attrs={'step': '0.25'})
        }


    def __init__(self, *args, **kwargs):
        super(InvoiceItemForm, self).__init__(*args, **kwargs)
        self.fields['item_name'].widget.attrs['placeholder'] = 'Enter the food name'
        self.fields['item_quantity'].widget.attrs['placeholder'] = 'Pieces'
        self.fields['item_price'].widget.attrs['placeholder'] = 'in ₹' 
        self.fields['item_subtotal'].widget.attrs['readonly'] = True
        self.fields['item_subtotal'].widget.attrs['tabindex'] = -1
        
        for field in self.fields.values():
            field.widget.attrs['class'] = 'form-control'



class InlineFormSet(forms.BaseInlineFormSet):

    def __init__(self, *args, **kwargs):
        super(InlineFormSet, self).__init__(*args, **kwargs)
        for form in self.forms:
            form.empty_permitted = False



InvoiceFormSet = forms.inlineformset_factory(
    Invoice, InvoiceItem, fields=('__all__'), 
    form=InvoiceItemForm, formset = InlineFormSet,
    extra=0, min_num=1, can_delete=True
)

create_invoice.html

<fieldset>
    <div class="text-dark py-4 table-responsive">
        <div class="inline-formset inline-group" id="{{ formset.prefix }}-group" data-inline-type="tabular" 
        data-inline-formset="{  
                                &quot;name&quot;: &quot;#{{ formset.prefix }}&quot;, 
                                &quot;options&quot;: {
                                    &quot;prefix&quot;: &quot;{{ formset.prefix }}&quot;, 
                                    &quot;addText&quot;: &quot;Add+&quot;, 
                                    &quot;deleteText&quot;: &quot;<i class='bi bi-x'></i>&quot;, 
                                    &quot;formCssClass&quot;: &quot;dynamic-{{ formset.prefix }}&quot;, 
                                }
                            }">
            {% csrf_token %}
            
            <div class="tabular inline-related">
                {{ formset.management_form }}
                <table id="invoice-table" class="as-table table table-xl table-hover caption">
                    <div class="d-block invalid-feedback">{{ formset.non_form_errors }}</div>
                    <caption>Add list of items.</caption>
                    {% for form in formset.forms %}
                        {% if forloop.first %}
                            <thead class="text-light">
                                <tr class="text-center">
                                    <th scope="col">#</th>
                                    {% for field in form.visible_fields %}
                                        <th scope="col">{{ field.label|capfirst }}</th>
                                    {% endfor %}
                                </tr>
                            </thead>
                            <tbody>
                        {% endif %}
                        <tr scope="row" class="form-row" id="{{ formset.prefix }}-{{ forloop.counter0 }}">
                            <th class="original">
                                <div class="index">{{ forloop.counter1 }}</div>
                                {% for hidden in form.hidden_fields %}
                                    {{ hidden }}
                                {% endfor %}
                            </th>
                            {% for field in form.visible_fields %}
                                <td class="field-{{ field.name }}">
                                    {% if field.name != "DELETE" %}
                                        {% if field.errors %}
                                            {{ field|addCls:"is-invalid" }}
                                        {% else %}
                                            {{ field }}
                                        {% endif %}
                                        {% if field.errors %}
                                            <div class="invalid-feedback">{{ field.errors }}</div>
                                        {% endif %}
                                    {% else %}
                                        {{ field }}
                                    {% endif %}
                                </td>
                            {% endfor %}
                        </tr>
                    {% endfor %}
                        <tr scope="row" class="form-row empty-row" id="{{ formset.prefix }}-empty">
                            <th class="original">
                                <div class="index">__prefix__</div>
                                {% for field in formset.empty_form.hidden_fields %}
                                    {{ field }}
                                {% endfor %}
                            </th>
                            {% for field in formset.empty_form.visible_fields %}
                                <td class="field-{{ field.name }}">
                                    {% if field.name != "DELETE" %}
                                        {{ field }}
                                    {% endif %}
                                </td>
                            {% endfor %}
                        </tr>
                    </tbody>
                </table>
            </div>
        </div>
    </div>
</fieldset>