【问题标题】:Displaying a series of inline forms for a queryset of Django models为 Django 模型的查询集显示一系列内联表单
【发布时间】:2018-03-01 18:00:20
【问题描述】:

如果我有这样的模型:

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)

我想制作一个页面:

  • 列出特定出版商的所有图书(例如Book.objects.filter(publisher=34))。
  • 对于每本书,显示所有现有的 BookImages。
  • 每本书显示 3 个表单,用于上传和命名新的 BookImage。
  • 一个提交按钮。

我不需要编辑书籍的详细信息 - 表单仅适用于 BookImages。

我正在与modelformset_factoryinlineformset_factory 纠缠不清,但没有一个是正确的……我觉得我把事情弄得太复杂了。有什么想法吗?

更新:

以下是我在正确的方向上尝试过的一些方法,但我不确定它们是否有帮助:

# 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)

【问题讨论】:

  • 我还发现表单集有些烦人。使用它们时,我倾向于使用这个简单的教程,并逐步遵循它:medium.com/@adandan01/…。一旦你有一些代码,我们可能会发现任何问题
  • 我添加了一些代码,但我不确定它是否很有帮助。感谢您的教程。了解表单集比大多数东西都难。
  • 我在micropyramid.com/blog/how-to-use-nested-formsets-in-django 找到了一个很好的例子,我现在没有时间在这里写它作为答案,但稍后会尝试这样做。
  • (我现在已经做到了。)

标签: django


【解决方案1】:

我找到了一个如何做到这一点的示例in this blog post。下面我使用我的 Publisher/Book/BookImage 模型和基于类的通用视图重写了示例,以供将来参考。

表单还允许用户编辑书籍的标题,这不是我最初想要的,但这似乎比更容易;内联 Book 表单每个都需要至少一个字段,因此我们也可以包含 Book 的标题。

另外,为了了解它是如何工作的,我已经使用此代码和更多细节组合了一个小型 Django 项目,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 %}

【讨论】:

    【解决方案2】:

    所有 formset_factory 方法都需要一个可以多次生成的表单。所以,你需要为BookImage创建一个表单(因为你使用的是Models,所以你需要创建一个模型表单)。

    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:你可以从 request.POST 中获取数据给同一个视图中的其他模型来处理其他数据(比如 Books 的数据)

    【讨论】:

    • 非常感谢,感谢所有清晰的 cmets!但我不清楚这两个表单集如何知道要显示哪些书籍的表单(即属于特定出版商的所有书籍)?
    • @PhilGyford 如果您只想显示书籍的书籍图像,则不需要表单集,您可以简单地使用 QuerySet 来显示信息。当您希望用户输入数据时,Formset 是必需的。如果您的意思是“我如何存储与一本书有关的图书图像”,您需要从 request.POST 中获取 book_id,假设您有一个包含图书 id 的隐藏字段,那么您可以设置外部bookimage 的 key 到 book id 的 key
    • @PhilGyford 此外,您可以在模板中的任意位置和任意次数呈现表单集。这不是一次性渲染
    • 也许我不是很清楚...我想列出出版商的所有书籍...对于所有这些书籍,显示任何现有的 BookImages 并允许用户上传新的 BookImages对于所有这些书籍。
    • @PhilGyford 你可以参考这个stackoverflow.com/questions/6337973/…,它基于多个ID(或其他字段约束)获取多个查询。您可以应用相同的逻辑来获取书籍的多个书籍图像。使用 for 循环遍历书籍并继续将获取的书籍图像添加到列表或 json 对象中,您可以使用 JS/JQuery 在模板中访问。
    猜你喜欢
    • 2019-04-23
    • 1970-01-01
    • 2011-10-05
    • 2018-07-11
    • 1970-01-01
    • 1970-01-01
    • 2011-11-19
    • 2012-03-15
    • 1970-01-01
    相关资源
    最近更新 更多