Pre-populate an inline FormSet?

后端 未结 10 1512
自闭症患者
自闭症患者 2020-11-30 18:25

I\'m working on an attendance entry form for a band. My idea is to have a section of the form to enter event information for a performance or rehearsal. Here\'s the model

相关标签:
10条回答
  • 2020-11-30 18:56

    I spent a fair amount of time trying to come up with a solution that I could re-use across sites. James' post contained the key piece of wisdom of extending BaseInlineFormSet but strategically invoking calls against BaseFormSet.

    The solution below is broken into two pieces: a AdminInline and a BaseInlineFormSet.

    1. The InlineAdmin dynamically generates an initial value based on the exposed request object.
    2. It uses currying to expose the initial values to a custom BaseInlineFormSet through keyword arguments passed to the constructor.
    3. The BaseInlineFormSet constructor pops the initial values off the list of keyword arguments and constructs normally.
    4. The last piece is overriding the form construction process by changing the maximum total number of forms and using the BaseFormSet._construct_form and BaseFormSet._construct_forms methods

    Here are some concrete snippets using the OP's classes. I've tested this against Django 1.2.3. I highly recommend keeping the formset and admin documentation handy while developing.

    admin.py

    from django.utils.functional import curry
    from django.contrib import admin
    from example_app.forms import *
    from example_app.models import *
    
    class AttendanceInline(admin.TabularInline):
        model           = Attendance
        formset         = AttendanceFormSet
        extra           = 5
    
        def get_formset(self, request, obj=None, **kwargs):
            """
            Pre-populating formset using GET params
            """
            initial = []
            if request.method == "GET":
                #
                # Populate initial based on request
                #
                initial.append({
                    'foo': 'bar',
                })
            formset = super(AttendanceInline, self).get_formset(request, obj, **kwargs)
            formset.__init__ = curry(formset.__init__, initial=initial)
            return formset
    

    forms.py

    from django.forms import formsets
    from django.forms.models import BaseInlineFormSet
    
    class BaseAttendanceFormSet(BaseInlineFormSet):
        def __init__(self, *args, **kwargs):
            """
            Grabs the curried initial values and stores them into a 'private'
            variable. Note: the use of self.__initial is important, using
            self.initial or self._initial will be erased by a parent class
            """
            self.__initial = kwargs.pop('initial', [])
            super(BaseAttendanceFormSet, self).__init__(*args, **kwargs)
    
        def total_form_count(self):
            return len(self.__initial) + self.extra
    
        def _construct_forms(self):
            return formsets.BaseFormSet._construct_forms(self)
    
        def _construct_form(self, i, **kwargs):
            if self.__initial:
                try:
                    kwargs['initial'] = self.__initial[i]
                except IndexError:
                    pass
            return formsets.BaseFormSet._construct_form(self, i, **kwargs)
    
    AttendanceFormSet = formsets.formset_factory(AttendanceForm, formset=BaseAttendanceFormSet)
    
    0 讨论(0)
  • 2020-11-30 19:03

    Django 1.4 and higher supports providing initial values.

    In terms of the original question, the following would work:

    class AttendanceFormSet(models.BaseInlineFormSet):
        def __init__(self, *args, **kwargs):
            super(AttendanceFormSet, self).__init__(*args, **kwargs)
            # Check that the data doesn't already exist
            if not kwargs['instance'].member_id_set.filter(# some criteria):
                initial = []
                initial.append({}) # Fill in with some data
                self.initial = initial
                # Make enough extra formsets to hold initial forms
                self.extra += len(initial)
    

    If you find that the forms are being populated but not being save then you may need to customize your model form. An easy way is to pass a tag in the initial data and look for it in the form init:

    class AttendanceForm(forms.ModelForm):
        def __init__(self, *args, **kwargs):
            super(AttendanceForm, self).__init__(*args, **kwargs)
            # If the form was prepopulated from default data (and has the
            # appropriate tag set), then manually set the changed data
            # so later model saving code is activated when calling
            # has_changed().
            initial = kwargs.get('initial')
            if initial:
                self._changed_data = initial.copy()
    
        class Meta:
            model = Attendance
    
    0 讨论(0)
  • 2020-11-30 19:05

    So, you're not going to like the answer, partly because I'm not yet done writing the code and partly because it's a lot of work.

    What you need to do, as I discovered when I ran into this myself, is:

    1. Spend a lot of time reading through the formset and model-formset code to get a feel for how it all works (not helped by the fact that some of the functionality lives on the formset classes, and some of it lives in factory functions which spit them out). You will need this knowledge in the later steps.
    2. Write your own formset class which subclasses from BaseInlineFormSet and accepts initial. The really tricky bit here is that you must override __init__(), and you must make sure that it calls up to BaseFormSet.__init__() rather than using the direct parent or grandparent __init__() (since those are BaseInlineFormSet and BaseModelFormSet, respectively, and neither of them can handle initial data).
    3. Write your own subclass of the appropriate admin inline class (in my case it was TabularInline) and override its get_formset method to return the result of inlineformset_factory() using your custom formset class.
    4. On the actual ModelAdmin subclass for the model with the inline, override add_view and change_view, and replicate most of the code, but with one big change: build the initial data your formset will need, and pass it to your custom formset (which will be returned by your ModelAdmin's get_formsets() method).

    I've had a few productive chats with Brian and Joseph about improving this for future Django releases; at the moment, the way the model formsets work just make this more trouble than it's usually worth, but with a bit of API cleanup I think it could be made extremely easy.

    0 讨论(0)
  • 2020-11-30 19:05

    You can override empty_form getter on a formset. Here is an example on how do I deal with this in conjunction with django admin:

    class MyFormSet(forms.models.BaseInlineFormSet):
        model = MyModel
    
        @property
        def empty_form(self):
            initial = {}
            if self.parent_obj:
                initial['name'] = self.parent_obj.default_child_name
            form = self.form(
                auto_id=self.auto_id,
                prefix=self.add_prefix('__prefix__'),
                empty_permitted=True, initial=initial
            )
            self.add_fields(form, None)
            return form    
    
    class MyModelInline(admin.StackedInline):
        model = MyModel
        formset = MyFormSet
    
        def get_formset(self, request, obj=None, **kwargs):    
            formset = super(HostsSpaceInline, self).get_formset(request, obj, **kwargs)
            formset.parent_obj = obj
            return formset
    
    0 讨论(0)
  • 2020-11-30 19:07

    Here is how I solved the problem. There's a bit of a trade-off in creating and deleting the records, but the code is clean...

    def manage_event(request, event_id):
        """
        Add a boolean field 'record_saved' (default to False) to the Event model
        Edit an existing Event record or, if the record does not exist:
        - create and save a new Event record
        - create and save Attendance records for each Member
        Clean up any unsaved records each time you're using this view
        """
        # delete any "unsaved" Event records (cascading into Attendance records)
        Event.objects.filter(record_saved=False).delete()
        try:
            my_event = Event.objects.get(pk=int(event_id))
        except Event.DoesNotExist:
            # create a new Event record
            my_event = Event.objects.create()
            # create an Attendance object for each Member with the currect Event id
            for m in Members.objects.get.all():
                Attendance.objects.create(event_id=my_event.id, member_id=m.id)
        AttendanceFormSet = inlineformset_factory(Event, Attendance, 
                                            can_delete=False, 
                                            extra=0, 
                                            form=AttendanceForm)
        if request.method == "POST":
            form = EventForm(request.POST, request.FILES, instance=my_event)
            formset = AttendanceFormSet(request.POST, request.FILES, 
                                            instance=my_event)
            if formset.is_valid() and form.is_valid():
                # set record_saved to True before saving
                e = form.save(commit=False)
                e.record_saved=True
                e.save()
                formset.save()
                return HttpResponseRedirect('/')
        else:
            form = EventForm(instance=my_event)
            formset = OptieFormSet(instance=my_event)
        return render_to_response("edit_event.html", {
                                "form":form, 
                                "formset": formset,
                                }, 
                                context_instance=RequestContext(request))
    
    0 讨论(0)
  • 2020-11-30 19:08

    I came accross the same problem.

    You can do it through JavaScript, make a simple JS that makes an ajax call for all the band memebers, and populates the form.

    This solution lacks DRY principle, because you need to write this for every inline form you have.

    0 讨论(0)
提交回复
热议问题