inlineformset_factory create new objects and edit objects after created inlineformset_factory create new objects and edit objects after created django django

inlineformset_factory create new objects and edit objects after created


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

Here's my approach:

models.py

from django.db import modelsclass 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 ModelFormfrom django.forms.models import inlineformset_factoryfrom crispy_forms.helper import FormHelperfrom crispy_forms.layout import Layout, Fieldsetfrom .models import Author, Bookclass 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 helperclass 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 CreateViewfrom django.http import HttpResponseRedirectfrom .forms import AuthorForm, BookFormset, BookFormHelperfrom .models import Book, Authorclass 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, urlfrom django.views.generic import TemplateViewfrom library.views import AuthorCreateViewurlpatterns = 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_factoryfeature and Django generic Class-Based Views
  • I'm assumming django-crispy-forms is installed, and it's properlyconfigured.
  • 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.


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,    })


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 parentBookInlineFormSet = 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 parentBookInlineFormSet = 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.