Django forms: editing multiple sets of related objects in one form

I am trying to do something that should be very common: add / edit a bunch of related models in one form. For instance:

Visitor Details: Select destinations and activities: Miami [] - swimming [], clubbing [], sunbathing[] Cancun [] - swimming [], clubbing [], sunbathing[] 

My models are the visitor, destination and activity, and the visitor has the ManyToMany field in Destination through the intermediate VisitorDestination model, which describes in detail the actions that must be performed at the destination (the ManyToMany field in Activity itself).

 Visitor ---->(M2M though VisitorDestination) -------------> Destination | activities ---->(M2M)---> Activity 

Please note that I do not want to enter new destination / activity values , just choose from the ones available in db (but is it absolutely legal to use the M2M fields right?)

For me, this looks like an extremely common situation (many to a large number with additional details that are an FK or M2M field in some other model), and this looks like the most reasonable simulation, but please correct me, Wrong.

I spent several days searching for Django docs / SO / googling, but could not figure out how to deal with it. I tried several approaches:

  • A custom model for Visitor, where I add several selection fields for Destination and Activity. This works fine if the purpose and activity can be selected independently, but here they are correlated , i.e. I want to select one or more actions for each appointment

  • Using inlineformset_factory to create a set of destination / action forms using inlineformset_factory(Destination, Visitor) . This is interrupted because Visitor has an M2M relationship with Destination, not FK.

  • Setting up a simple set of forms using formset_factory , e.g. DestinationActivityFormSet = formset_factory(DestinationActivityForm, extra=2) . But how to develop a DestinationActivityForm ? I haven’t studied it enough, but it doesn’t look very promising: I don’t want to enter an addressee and a list of activities, I need a list of checkboxes with labels set for assignment / actions that I want to select, but formset_factory will return a list of forms with the same labels .

I am a complete newbie with django, so maybe the solution is obvious, but I think the documentation in this area is very weak - if anyone has some indications of usage examples for forms / forms that will also be useful

thanks!

+6
source share
3 answers

In the end, I decided to process several forms in one view, a visitor model form for visitor details, and then a list of custom forms for each destination.

Processing several forms in one representation turned out to be quite simple (at least in this case, when there were no problems with checking the transverse field).

I am still surprised that there is no built-in support for many, many relationships with the intermediate model, and looking around the Internet, I did not find a direct link to it. I will send the code in case it helps anyone.

Custom forms first:

 class VisitorForm(ModelForm): class Meta: model = Visitor exclude = ['destinations'] class VisitorDestinationForm(Form): visited = forms.BooleanField(required=False) activities = forms.MultipleChoiceField(choices = [(obj.pk, obj.name) for obj in Activity.objects.all()], required=False, widget = CheckboxSelectMultipleInline(attrs={'style' : 'display:inline'})) def __init__(self, visitor, destination, visited, *args, **kwargs): super(VisitorDestinationForm, self).__init__(*args, **kwargs) self.destination = destination self.fields['visited'].initial = visited self.fields['visited'].label= destination.destination # load initial choices for activities activities_initial = [] try: visitorDestination_entry = VisitorDestination.objects.get(visitor=visitor, destination=destination) activities = visitorDestination_entry.activities.all() for activity in Activity.objects.all(): if activity in activities: activities_initial.append(activity.pk) except VisitorDestination.DoesNotExist: pass self.fields['activities'].initial = activities_initial 

I customize each form by passing in Visitor and Destination objects (and a visited flag, which is calculated externally for convenience)

I use a boolean field to allow the user to select each destination. The field is called “visited,” however I set a shortcut to the destination so that it displays beautifully.

Actions are handled by the usual MultipleChoiceField (I used a custom widget to display checkboxes in a table, quite simple, but I can post it if someone needs it)

Then a code of the form:

 def edit_visitor(request, pk): visitor_obj = Visitor.objects.get(pk=pk) visitorDestinations = visitor_obj.destinations.all() if request.method == 'POST': visitorForm = VisitorForm(request.POST, instance=visitor_obj) # set up the visitor destination forms destinationForms = [] for destination in Destination.objects.all(): visited = destination in visitorDestinations destinationForms.append(VisitorDestinationForm(visitor_obj, destination, visited, request.POST, prefix=destination.destination)) if visitorForm.is_valid() and all([form.is_valid() for form in destinationForms]): visitor_obj = visitorForm.save() # clear any existing entries, visitor_obj.destinations.clear() for form in destinationForms: if form.cleaned_data['visited']: visitorDestination_entry = VisitorDestination(visitor = visitor_obj, destination=form.destination) visitorDestination_entry.save() for activity_pk in form.cleaned_data['activities']: activity = Activity.objects.get(pk=activity_pk) visitorDestination_entry.activities.add(activity) print 'activities: %s' % visitorDestination_entry.activities.all() visitorDestination_entry.save() success_url = reverse('visitor_detail', kwargs={'pk' : visitor_obj.pk}) return HttpResponseRedirect(success_url) else: visitorForm = VisitorForm(instance=visitor_obj) # set up the visitor destination forms destinationForms = [] for destination in Destination.objects.all(): visited = destination in visitorDestinations destinationForms.append(VisitorDestinationForm(visitor_obj, destination, visited, prefix=destination.destination)) return render_to_response('testapp/edit_visitor.html', {'form': visitorForm, 'destinationForms' : destinationForms, 'visitor' : visitor_obj}, context_instance= RequestContext(request)) 

I just collect my destination forms in a list and pass that list to my template so that it can iterate over them and display. It works well until you forget to pass a different prefix for each of the constructors.

I will leave the question open for a few days if anyone has a cleaner method.

Thanks!

+7
source

So, as you saw, one of the things about inlineformset_factory is that it expects two models - a parent and a child, which has a foreign key to a parent relationship. How do you submit additional data on the fly to the form to receive additional data in the intermediate model?

How I do it using curry:

 from django.utils.functional import curry from my_app.models import ParentModel, ChildModel, SomeOtherModel def some_view(request, child_id, extra_object_id): instance = ChildModel.objects.get(pk=child_id) some_extra_model = SomeOtherModel.objects.get(pk=extra_object_id) MyFormset = inlineformset_factory(ParentModel, ChildModel, form=ChildModelForm) #This is where the object "some_extra_model" gets passed to each form via the #static method MyFormset.form = staticmethod(curry(ChildModelForm, some_extra_model=some_extra_model)) formset = MyFormset(request.POST or None, request.FILES or None, queryset=SomeObject.objects.filter(something=something), instance=instance) 

The form class "ChildModelForm" must have an init override that adds the "some_extra_model" object from the arguments:

 def ChildModelForm(forms.ModelForm): class Meta: model = ChildModel def __init__(self, some_extra_model, *args, **kwargs): super(ChildModelForm, self).__init__(*args, **kwargs) #do something with "some_extra_model" here 

Hope this helps you on the right track.

+1
source

From django 1.9 there is support for passing custom parameters to form forms: https://docs.djangoproject.com/en/1.9/topics/forms/formsets/#passing-custom-parameters-to-formset-forms

Just add form_kwargs to your FormSet as follows:

 from my_app.models import ParentModel, ChildModel, SomeOtherModel def some_view(request, child_id, extra_object_id): instance = ChildModel.objects.get(pk=child_id) some_extra_model = SomeOtherModel.objects.get(pk=extra_object_id) MyFormset = inlineformset_factory(ParentModel, ChildModel, form=ChildModelForm) formset = MyFormset(request.POST or None, request.FILES or None, queryset=SomeObject.objects.filter(something=something), instance=instance, form_kwargs={"some_extra_model": some_extra_model}) 
0
source

Source: https://habr.com/ru/post/902792/


All Articles