Django:使用 modelformset_factory 填充多对多字段

Django: populating many to many field using modelformset_factory

我尝试填充多对多字段。菜单和课程模型之间存在关系。我向 M2M Table 添加了一些自定义字段,因此我可以存储课程的顺序及其类型(即开胃菜、开胃菜等)。

为了获得动态网络表单,我使用 modelformset_factory 和 python-formset-js-improved pip 包 (https://pypi.org/project/django-formset-js-improved/)。

按照我 views.py 中的逻辑,在此处 进行了解释,我 运行 出现了以下错误。此错误适用于所有关键字参数。

menu.menu_item.add(course_position=position,
                                   course_type=menu_item_form['course_type'],
                                   course=course)

TypeError: add() got an unexpected keyword argument 'course_position'

我做错了什么?以下是 models.py、forms.py、views.py 和 html 文件的摘录

编辑 我按照文档并尝试相应地填充 m2m table。 https://docs.djangoproject.com/en/4.0/topics/db/models/#intermediary-manytomany

我尝试将值添加到模型中使用,但我不知道如何访问 menu_item_form 的 course_type 字段,因为我无法验证表单:

menu_item = MenuItem(menu=menu, 
                     course=course, 
                     course_position=position, 
                     course_type=menu_item_form.fields['course_type'])
    
menu_item.save()

models.py

class Course(models.Model):
    # Individual name of a course (i.e. "Natschis Spezial Fondue")
    creator = models.ForeignKey(User, related_name='creator_id_course', on_delete=models.PROTECT)

    course_name = models.CharField(max_length=100)
    course_description = models.CharField(max_length=1000)
    course_price = models.DecimalField(max_digits=7, decimal_places=2, null=True)
    course_tags = models.ManyToManyField(CourseTag)

    private = models.BooleanField(default=False, verbose_name=_('private item'))
    active = models.BooleanField(default=True)
    deleted = models.BooleanField(default=False)

    objects = CourseManager()

    def __str__(self):
        return self.course_name


class Menu(models.Model):
    class Status(models.TextChoices):
        ACTIVE = 'a', _('active')
        SUSPENDED = 's', _('suspended')
        DELETED = 'd', _('deleted')

    # assemble a menu from different courses --> "Movie"
    creator = models.ForeignKey(User, related_name='creator_id_menu', on_delete=models.PROTECT)

    menu_name = models.CharField(max_length=100, default='', verbose_name=_('Menu Name'), help_text=_('i.e. saturday night fajita night'))
    menu_description = models.TextField(max_length=1000, default='', verbose_name='Menu Description', help_text=_('this menu will blow your mind...'))
    menu_duration_minutes = models.IntegerField(default=30, blank=True, null=True)

    menu_item = models.ManyToManyField(Course, through='MenuItem', blank=True)  # collects all courses related to this menu

    created_at = models.DateTimeField(auto_now_add=True, verbose_name='Creation Date')
    updated_at = models.DateTimeField(auto_now=True, verbose_name='Updated on')

    private = models.BooleanField(default=False, verbose_name=_('private'))
    status = models.CharField(max_length=1, default='a', choices=Status.choices, verbose_name=_('menu status'))

    objects = MenuManager()

    def __str__(self):
        return self.menu_name


class MenuItem(models.Model):
    class CourseType(models.TextChoices):
        APPETIZER = '10', _('appetizer')
        STARTER = '20', _('starter')
        MAIN = '30', _('main course')
        DESSERT = '40', _('dessert')

        SOUP = '21', _('soup')
        SALAD = '22', _('salad')

        PRIMO = '31', _('primo')
        PASTA = '32', _('pasta')

        SECONDO = '33', _('secondo')
        FISH = '34',  _('fish')
        MEAT = '35', _('meat')

        PIZZA = '36', _('pizza')
        HAMBURGER = '37', _('hamburger')

        CHEESE = '41', _('cheese')
        FRUITS = '42', _('fruits')
        CAKES = '43', _('cakes')
        ICE_CREAM = '44', _('ice cream')
        DIGESTIVE = '45', _('digestive')

    menu = models.ForeignKey(Menu, on_delete=models.CASCADE, verbose_name=_('menu name'))
    course = models.ForeignKey(Course, on_delete=models.PROTECT, verbose_name=_('course name'))
    course_type = models.CharField(choices=CourseType.choices, max_length=3, verbose_name=_('course type'))
    course_position = models.PositiveSmallIntegerField(verbose_name=_('course position'))

forms.py

class MenuItemForm(forms.ModelForm):
    course_type = forms.ChoiceField(choices=MenuItem.CourseType.choices)

    class Meta:
        model = MenuItem
        fields = '__all__'


MenuItemFormset = modelformset_factory(MenuItem,
                                       form=MenuItemForm,
                                       extra=1)

class CourseForm(forms.ModelForm):

    class Meta:
        model = Course
        fields = ['course_name', 'course_description']

    class Media(object):
        # todo: can this be deleted? used for
        js = formset_media_js + (
            # Other form media here
        )


CourseFormset = modelformset_factory(Course,
                                     form=CourseForm,
                                     extra=1)

views.py

def create_menu_with_courses(request):
    context = {}
    user = request.user

    menu_form = MenuCreationForm(user, request.POST or None)
    course_formset = CourseFormset(request.POST or None, queryset=Course.objects.none(), prefix='course')

    menu_item_formset = MenuItemFormset(request.POST or None, queryset=MenuItem.objects.none(), prefix='menu-item')

    print(f"debug: {request.POST}")

    if request.method == 'POST':
        current_user = request.user

        if all([menu_form.is_valid(), course_formset.is_valid()]):
            print("menu & course_formset is valid")

            menu = menu_form.save(commit=False)
            menu.creator = current_user
            menu.save()

            for (position, course_form), menu_item_form in zip(enumerate(course_formset), menu_item_formset):
                course = course_form.save(commit=False)
                course.creator = request.user
                course.save()

                menu.menu_item.add(course_position=position,
                                   course_type=menu_item_form.course_type,
                                   course=course)

                menu.save()

            messages.success(request, f'Well done! Your menu "{menu}" was successfully created!')

            return redirect('menu-list')

        else:
            print(menu_form.errors, course_formset.errors)

    context['menu_form'] = menu_form
    context['course_formset'] = course_formset
    context['menu_item_formset'] = menu_item_formset

    return render(request, 'menus/create_menu.html', context)

html

             <div id="formset" data-formset-prefix="{{ course_formset.prefix }}">
                    {{ course_formset.media }}
                    {{ course_formset.management_form }}
                    {{ menu_item_formset.management_form }}

                        <div id="formset-body" data-formset-body>
                            <!-- New forms will be inserted in here -->
                            <div data-formset-form>
                                {% for course_form in course_formset %}
                                    {% for menu_item_form in menu_item_formset %}
                                        <div class="form-floating">
                                            {{ menu_item_form.id }}
                                            {% render_field menu_item_form.course_type class+="form-select" aria-label="Floating label select" %}

                                            {{ course_form.id }}
                                            <label for="input{{ course_form.course_name.label }}" class+="form-label">{{ course_form.course_name.label }}</label>
                                            {% render_field course_form.course_name class+="form-control" %}

                                            {% for error in course_form.course_name.errors %}
                                                <p>{{ error }}</p>
                                            {% endfor %}

                                            <label for="input{{ course_form.course_description.label }}" class="form-label">{{ course_form.course_description.label }}</label>
                                            {% render_field course_form.course_description class+="form-control" rows="3" id="input{{ course_form.course_description.label }}" %}

                                            <button class="btn btn-outline-primary btn-block my-3 type="button" data-formset-move-up-button>Move up</button>
                                            <button class="btn btn-outline-primary btn-block my-3 type="button" data-formset-move-down-button>Move down</button>
                                            <button class="btn btn-outline-primary btn-block my-3 type="button" data-formset-delete-button>Delete form</button>
                                        </div>
                                    {% endfor %}
                                {% endfor %}
                            </div>
                        </div>

                    <!-- The empty form template. By wrapping this in a <script> tag, the
                    __prefix__ placeholder can easily be replaced in both attributes and
                    any scripts -->
                    <script type="form-template" data-formset-empty-form>
                        {% escapescript %}
                            <div data-formset-form>
                                <!-- Course Formset-->
                                <div class="form-floating">
                                    <!---
                                    {% render_field menu_item_formset.empty_form.course_type class+="form-select" aria-label="Floating label select"%}
                                    -->
                                    {% render_field menu_item_formset.empty_form.course_type class+="form-select" aria-label="Floating label select" %}

                                    <label for="input{{ course_formset.empty_form.course_name.label }}" class+="form-label">{{ course_formset.empty_form.course_name.label }}</label>
                                    {% render_field course_formset.empty_form.course_name class+="form-control" %}
                                        {% for error in course_formset.empty_form.course_name.errors %}
                                            <p>{{ error }}</p>
                                        {% endfor %}

                                    <label for="input{{ course_formset.empty_form.course_description.label }}" class="form-label">{{ course_formset.empty_form.course_description.label }}</label>
                                    {% render_field course_formset.empty_form.course_description class+="form-control" rows="3" id="input{{ course_formset.empty_form.course_description.label }}" %}
                                </div>

                                <button class="btn btn-outline-primary btn-block my-3 type="button" data-formset-move-up-button>Move up</button>
                                <button class="btn btn-outline-primary btn-block my-3 type="button" data-formset-move-down-button>Move down</button>
                                <button class="btn btn-outline-primary btn-block my-3 type="button" data-formset-delete-button>Delete form</button>
                            </div>
                        {% endescapescript %}
                    </script>

                    <!-- This button will add a new form when clicked -->
                    <input class="btn btn-outline-primary btn-block my-3 type="button" value="Add new" data-formset-add>


                    <script>jQuery(function($) {
                        $("#formset").formset({
                            animateForms: true,
                            reorderMode: 'dom',
                        });
                    });</script>

                </div>

基于这个线程(Set form field value before is_valid())我解决了我的问题。

我将字段 course_type 添加到 course_formset。我从 request.POST 本身检索了数据。

menu_item = MenuItem(menu=menu, course=course, course_position=position, course_type=request.POST[f'course-{position}-course_type'])
menu_item.save()