django forms: editing multiple sets of related objects in a single form

前端 未结 3 454
天涯浪人
天涯浪人 2020-12-29 15:30

I\'m trying to do something that should be very common: add/edit a bunch of related models in a single form. For example:

Visitor Details:
Select destination         


        
相关标签:
3条回答
  • 2020-12-29 15:37

    So, as you've seen, one of the things about inlineformset_factory is that it expects two models - a parent, and child, which has a foreign key relationship to the parent. How do you pass extra data on the fly to the form, for extra data in the intermediary model?

    How I do this is by 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" would need to 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 that helps get you on the right track.

    0 讨论(0)
  • 2020-12-29 15:39

    In the end I opted for processing multiple forms within the same view, a Visitor model form for the visitor details, then a list of custom forms for each of the destinations.

    Processing multiple forms in the same view turned out to be simple enough (at least in this case, where there were no cross-field validation issues).

    I'm still surprised there is no built-in support for many to many relationships with an intermediary model, and looking around in the web I found no direct reference to it. I'll post the code in case it helps anyone.

    First the custom forms:

    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 a Visitor and Destination objects (and a 'visited' flag which is calculated outside for convenience)

    I use a boolean field to allow the user to select each destination. The field is called 'visited', however I set the label to the destination so it gets nicely displayed.

    The activities get handled by the usual MultipleChoiceField (I used I customized widget to get the checkboxes to display on a table, pretty simple but can post it if somebody needs that)

    Then the view code:

    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 simply collect my destination forms in a list and pass this list to my template, so that it can iterate over them and display them. It works well as long as you don't forget to pass a different prefix for each one in the constructor

    I'll leave the question open for a few days in case some one has a cleaner method.

    Thanks!

    0 讨论(0)
  • 2020-12-29 15:46

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

    Just add form_kwargs to your FormSet init like this :

    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 讨论(0)
提交回复
热议问题