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()
我尝试填充多对多字段。菜单和课程模型之间存在关系。我向 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()