I try to populate a many-to-many field. The Relationship exists between the Menus and Course Model. I added some custom fields to the M2M Table, so I can store the order of the courses and their type (i.e. Starter, Appetizer, etc.).
To get a dynamic webform, I use modelformset_factory with the python-formset-js-improved pip package (https://pypi.org/project/django-formset-js-improved/).
following the logic in my views.py, explained here Populate a ManyToManyField, I run into the following error. This error applies to all keyword arguments.
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'
What did I get wrong? below are extracts from models.py, forms.py, views.py and the html file
EDIT I followed the documentation and tried to populate the m2m table accordingly. https://docs.djangoproject.com/en/4.0/topics/db/models/#intermediary-manytomany
I tried to add the values to the model using, but I don’t know how to access the course_type field of the menu_item_form, as I cannot validate the form:
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>
Advertisement
Answer
Based on this thread (Set form field value before is_valid()) I solved my problem.
I added the field course_type
to the course_formset. I retrieved the data from request.POST itself.
menu_item = MenuItem(menu=menu, course=course, course_position=position, course_type=request.POST[f'course-{position}-course_type']) menu_item.save()