0

If I have models like this:

from django.db import models

class Publisher(models.Model):
    name = models.CharField(max_length=255)

class Book(models.Model):
    publisher = models.ForeignKey('Publisher')
    title = models.CharField(max_length=255)

class BookImage(models.Model):
    book = models.ForeignKey('Book')
    file = models.ImageField(max_length=255)
    title = models.CharField(max_length=255)

I want to make a page that:

  • Lists all the Books for a particular publisher (e.g. Book.objects.filter(publisher=34)).
  • For each Book, displays any existing BookImages.
  • For each Book displays 3 forms for uploading and titling new BookImages.
  • One submit button.

I don't need to edit the details of the Books - the forms are only for BookImages.

I'm getting in a tangle with modelformset_factory and inlineformset_factory and none of it is right... I feel like I'm making things too complicated. Any ideas?

Update:

Here are some things I've tried that head in a sort-of-right direction, but I'm not sure they help:

# Forms for multiple Books for this Publisher
# But I don't think I need forms for the Books in my situation?
my_publisher = Publisher.objects.get(pk=37)
BookFormSet = modelformset_factory(Book, fields=(['title']))
formset = BookFormSet(queryset=Book.objects.filter(publisher=my_publisher))

# Multiple BookImages on one Book:
# Good, but how do I do this for all of a Publisher's Books, and display existing BookImages?
my_book = Book.objects.get(pk=42)
BookImageFormSet = inlineformset_factory(Book, BookImage, fields=('file', 'title'))
formset = BookImageFormSet(instance=my_book)
Phil Gyford
  • 13,432
  • 14
  • 81
  • 143
  • I also find formsets somewhat annoying. When using them I tend to fall back on this simple tutorial and following it through step by stem: https://medium.com/@adandan01/django-inline-formsets-example-mybook-420cc4b6225d. Once you've some code, we might be able to see any issues – HenryM Mar 01 '18 at 18:35
  • I've added a bit of code, but I'm not sure it's very helpful. Thanks for the tutorial. It's somehow harder than most stuff to get my head around formsets. – Phil Gyford Mar 01 '18 at 18:48
  • I've found a good example of what I'm after here https://micropyramid.com/blog/how-to-use-nested-formsets-in-django/ I don't have time to write it up as an answer here right now but will try to do so later. – Phil Gyford Mar 02 '18 at 13:00
  • (I've now done that.) – Phil Gyford Mar 04 '18 at 11:19

2 Answers2

2

I found an example of how to do this in this blog post. Below I've rewritten the example using my Publisher/Book/BookImage models, and generic class-based views, for future reference.

The form also allows the user to edit the titles of the Books, which wasn't what I originally wanted, but this seems easier than not doing it; the inline Book forms require at least one field each, so we may as well include the Book's title.

Also, to see how this worked, I've put together a small Django project using this code, and a little more detail, available on GitHub.

models.py:

from django.db import models

class Publisher(models.Model):
    name = models.CharField(max_length=255)

class Book(models.Model):
    title = models.CharField(max_length=255)
    publisher = models.ForeignKey('Publisher', on_delete=models.CASCADE)

class BookImage(models.Model):
    book = models.ForeignKey('Book', on_delete=models.CASCADE)
    image = models.ImageField(max_length=255)
    alt_text = models.CharField(max_length=255)

forms.py:

from django.forms.models import BaseInlineFormSet, inlineformset_factory
from .models import Publisher, Book, BookImage

# The formset for editing the BookImages that belong to a Book.
BookImageFormset = inlineformset_factory(
                                        Book,
                                        BookImage,
                                        fields=('image', 'alt_text')),
                                        extra=1)

class BaseBooksWithImagesFormset(BaseInlineFormSet):
    """
    The base formset for editing Books belonging to a Publisher, and the
    BookImages belonging to those Books.
    """
    def add_fields(self, form, index):
        super().add_fields(form, index)

        # Save the formset for a Book's Images in a custom `nested` property.
        form.nested = BookImageFormset(
                                instance=form.instance,
                                data=form.data if form.is_bound else None,
                                files=form.files if form.is_bound else None,
                                prefix='bookimage-%s-%s' % (
                                    form.prefix,
                                    BookImageFormset.get_default_prefix()),
                            )

    def is_valid(self):
        "Also validate the `nested` formsets."
        result = super().is_valid()

        if self.is_bound:
            for form in self.forms:
                if hasattr(form, 'nested'):
                    result = result and form.nested.is_valid()

        return result

    def save(self, commit=True):
        "Also save the `nested` formsets."
        result = super().save(commit=commit)

        for form in self.forms:
            if hasattr(form, 'nested'):
                if not self._should_delete_form(form):
                    form.nested.save(commit=commit)

        return result

# This is the formset for the Books belonging to a Publisher and the
# BookImages belonging to those Books.
PublisherBooksWithImagesFormset = inlineformset_factory(
                                        Publisher,
                                        Book,
                                        formset=BaseBooksWithImagesFormset,
                                        fields=('title',),
                                        extra=1)

views.py:

from django.http import HttpResponseRedirect
from django.views.generic import FormView
from django.views.generic.detail import SingleObjectMixin

from .forms import PublisherBooksWithImagesFormset
from .models import Publisher, Book, BookImage

class PublisherUpdateView(SingleObjectMixin, FormView):
    model = Publisher
    success_url = 'publishers/updated/'
    template_name = 'publisher_update.html'

    def get(self, request, *args, **kwargs):
        # The Publisher whose Books we're editing:
        self.object = self.get_object(queryset=Publisher.objects.all())
        return super().get(request, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        # The Publisher whose Books we're editing:
        self.object = self.get_object(queryset=Publisher.objects.all())
        return super().post(request, *args, **kwargs)

    def get_form(self, form_class=None):
        "Use our formset of formsets, and pass in the Publisher object."
        return PublisherBooksWithImagesFormset(
                            **self.get_form_kwargs(), instance=self.object)

    def form_valid(self, form):            
        form.save()
        return HttpResponseRedirect(self.get_success_url())

templates/publisher_update.html:

{% extends 'base.html' %}

{% block content %}

  <form action="" method="post" enctype="multipart/form-data">

    {% for hidden_field in form.hidden_fields %}
      {{ hidden_field.errors }}
      {{ hidden_field }}
    {% endfor %}

    {% csrf_token %}

    {{ form.management_form }}
    {{ form.non_form_errors }}

    {% for book_form in form.forms %}
      {# Output a Book form. #}

      {% for hidden_field in book_form.hidden_fields %}
        {{ hidden_field.errors }}
      {% endfor %}

      <table>
        {{ book_form.as_table }}
      </table>

      {# Check if our `nested` property exists, with BookImage forms in it. #}
      {% if book_form.nested %}
          {{ book_form.nested.management_form }}
          {{ book_form.nested.non_form_errors }}

            {% for bookimage_form in book_form.nested.forms %}
              {# Output the BookImage forms for this Book. #}

              {% for hidden_field in bookimage_form.hidden_fields %}
                {{ hidden_field.errors }}
              {% endfor %}

              <table>
                {{ bookimage_form.as_table }}
              </table>
            {% endfor %}
      {% endif %}

    {% endfor %}

    <input type="submit" value="Update books">
  </form>

{% endblock content %}
Phil Gyford
  • 13,432
  • 14
  • 81
  • 143
0

All formset_factory methods require a form which they can generate multiple times. So, you need to create a form (since you are using Models, you need to create a model form) for BookImage.

forms.py

class BookImageForm(ModelForm):
    class Meta:
        model = BookImage

        # book field wont be generated in the template if it is excluded
        exclude = ['book',] 

views.py

from django.forms.formsets import formset_factory

def your_view(request):
    # I'm just including the formset code which you need, im assuming you have the remaining code in working condition

    # TO HANDLE NORMAL RENDERING OF FORM WHEN USER OPENS THE WEBPAGE
    if request.method == "GET":        
        bookimage_form = BookImageForm()
        bookimage_formset = formset_factory(bookimage_form, max_num=3)
        return render(request, 'index.html', {'bookimage_formset': bookimage_formset})


    # WHEN USER SUBMITS THE FORM
    if request.method == "POST"

        # Consider BookImageFormSet as a placeholder which will be able to contain the formset which will come through POST
        BookImageFormSet = modelformset_factory(BookImage, BookImageForm, max_num=3)

        # bookimage_formset is a variable which stores the Formset of the type BookImageFormSet which in turn is populated by the data received from POST
        bookimage_formset = BookImageFormSet(request.POST)

        # HIDDEN AUTO GENERATED FIELDS ARE CREATED WHEN THE FORMSET IS RENDERED IN A TEMPLATE, THE FOLLOWING VALIDATION CHECKS IF THE VALUE OF THE HIDDEN FIELDS ARE OKAY OR NOT
        if bookimage_formset.is_valid():

            # EACH FORM HAS TO BE INDIVIDUALLY VALIDATED, THIS IS NORMAL FORM VALIDATION. ONLY DIFFERENCE IS THAT THIS IS INSIDE A FOR LOOP
            for bookimage_form in bookimage_formset:

                if bookimage_form.is_valid:
                    bookimage_form.save()
            return HttpResponse("Form saved!")

        return HttpResponseRedirect('/')

PS: You can get the data from request.POST for other models in the same view to handle other data (such as the data for Books)

hulkinBrain
  • 746
  • 8
  • 28
  • Thanks so much for this, and all the clear comments! But I'm not clear how either formset knows which Books to display the forms for (i.e. all Books belonging to a particular Publisher)? – Phil Gyford Mar 01 '18 at 20:29
  • @PhilGyford If you just want to display the bookimages for books, you don't need a formset, you can simply use QuerySet to display information. Formset is required when you want the user to input data. If what you mean is that 'how do I store the book images pertaining to one particular book' you need to get the book_id from request.POST assuming you have a hidden field which contains the id of the book, then you can set the foreign key of the bookimage to that of the book's id – hulkinBrain Mar 02 '18 at 07:25
  • @PhilGyford Also, You can render the formset anywhere and any number of times in your template. It's not a one time render – hulkinBrain Mar 02 '18 at 07:36
  • Perhaps I'm not being clear... I want to list ALL the Books for a Publisher... and for ALL of those Books, display any existing BookImages AND allow the user to upload new BookImages for all of those Books. – Phil Gyford Mar 02 '18 at 10:39
  • @PhilGyford You can refer to this https://stackoverflow.com/questions/6337973/get-multiple-rows-with-one-query-in-django which fetches multiple queries based on multiple IDs (or other field constraints). You can apply the same logic to fetch multiple bookimages for books. Use a for loop to iterate through books and keep adding the fetched bookimages into a list or a json object which you can access in the template using JS/JQuery. – hulkinBrain Mar 02 '18 at 13:47
  • I think you are not clear on the usage of formsets. They are nothing but forms which have been rendered multiple times. They are NOT used display data, but as normal forms (which have been created multiple times) where the user inputs data. As far is was able to understand your problem, the link which I put in my last comment should be able to help you along with the formset code which I have provided in the answer. If not, feel free to ask any further doubts – hulkinBrain Mar 02 '18 at 13:51
  • I think I am clear about formsets :) I don't want to display data with one. I want to use several formsets - every Book on the page would have a formset of BookImages. I posted a link in the comments on the question that solves the problem. – Phil Gyford Mar 02 '18 at 14:11
  • @PhilGyford Okay great job on finding the right stuff on your own man :D Cheers – hulkinBrain Mar 02 '18 at 14:35