Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nested Formsets #6

Closed
blanchardjeremy opened this issue Dec 16, 2011 · 10 comments
Closed

Nested Formsets #6

blanchardjeremy opened this issue Dec 16, 2011 · 10 comments

Comments

@blanchardjeremy
Copy link

Is it possible to do nested formsets with django-extra-views? I'm thinking of something along these lines: http://yergler.net/blog/2009/09/27/nested-formsets-with-django/ ... If it is possible, how would I do it?

A secondary question: Is it possible to have a formset with nested forms? So I have a formset of Survey objects and I want each to also render/save/update a related model of

@AndrewIngram
Copy link
Owner

I've not tried it, but it should work in theory. I've tried to mimic the API of Django's existing class-based views as closely as possible. So if you've defined a custom formset class as in your example you can do something like:

EditBlockBuildingsView(InlineFormSetView):
    formset_class = BaseBuildingFormset
    model = models.Block
    inline_model = models.Building
    extra = 1
    template_name = 'rentals/edit_buildings.html'

In the template context you should have 'object' and 'formset' to play with.

@blanchardjeremy
Copy link
Author

So you're saying I would still have to code up BaseBuildingFormset?

I was hoping there was a way to have to use inline formsets and have an "inline inline" or a "formset of formsets" just using the code that already exists. Do you think that would work or would it still require all the custom code that blog talks about?

@blanchardjeremy
Copy link
Author

Closing this because I ended up figuring it out. Basically, the answer is "yes, I do have to code up a custom BaseBuildingFormset to make this work."

@oppianmatt
Copy link

@auzigog mind showing the rest of us how to do it?

@pymarco
Copy link

pymarco commented Jan 25, 2017

@oppianmatt I have opened a StackOverflow question requesting an explanation on how to achieve this.

@yerycs
Copy link

yerycs commented Jul 29, 2021

Hi @blanchardjeremy
Could you provide BaseBuildingFormset code snippet here?

@blanchardjeremy
Copy link
Author

blanchardjeremy commented Jul 29, 2021

@yerycs Unfortunately, I can't. Haven't engaged with Django in a decade. Sorry!

@yerycs
Copy link

yerycs commented Jul 29, 2021

I see, that's okay. @blanchardjeremy

@egkennedy93
Copy link

Hey guys,

I was able to figure this out! I used https://github.com/philgyford/django-nested-inline-formsets-example/tree/main for heavy inspiration.

In my example, I'm building a golf score tracking app.

TeeTime
-- GolfRound
------- GolfRoundScore

This may not be the most efficient way, but it was a way I was able to replicate the github listed above.

Two major areas of trouble I ran into.
1.) the formset used to build the custom BaseInlineFormSet has to be a default django inlineformset_factory, not the one from extras.
2.) I HAD to create a form_class for the formview. If I didn't override the default, the nested fields wouldn't be accessible in the template.

models..py

class TeeTime(models.Model):
    updated = models.DateTimeField(auto_now=True)
    timestamp = models.DateTimeField(auto_now_add=True)
    activity = models.ForeignKey(GolfActivity, on_delete=models.CASCADE, related_name='event_teetime')
    tee_time = models.TimeField(auto_now=False, auto_now_add=False, blank=True)
    players = models.ManyToManyField(GolfTripMember, blank=True, through='TeeTimePlayers')

    def __str__(self):
        return "{}-{}".format(self.activity, self.tee_time)

class GolfRound(models.Model):
    tee_time = models.ForeignKey(TeeTime, on_delete=models.CASCADE, related_name = 'round')
    golfer =  models.ForeignKey(GolfTripMember, on_delete=models.CASCADE, related_name = 'round_player')
    gross_score = models.IntegerField(null=True)
    net_score = models.IntegerField(null=True)
    par_3 = models.BooleanField(null=True, default=False)
    complete = models.BooleanField(null=True, default=False)
    active = models.BooleanField(null=True, default=False)

class GolfRoundHole(models.Model):
    round = models.ForeignKey(GolfRound, on_delete=models.CASCADE, related_name='hole_score')
    name = models.IntegerField(choices=INDEX, default = INDEX[0][0])
    par = models.IntegerField(null=True)
    netscore = models.IntegerField(null=True, blank=True)
    grossscore = models.IntegerField(null=True, blank=True)
forms.py

######Helper functions##########
def is_empty_form(form):
    """
    A form is considered empty if it passes its validation,
    but doesn't have any data.

    This is primarily used in formsets, when you want to
    validate if an individual form is empty (extra_form).
    """
    if form.is_valid() and not form.cleaned_data:
        return True
    else:
        # Either the form has errors (isn't valid) or
        # it doesn't have errors and contains data.
        return False


def is_form_persisted(form):
    """
    Does the form have a model instance attached and it's not being added?
    e.g. The form is about an existing Book whose data is being edited.
    """
    if form.instance and not form.instance._state.adding:
        return True
    else:
        # Either the form has no instance attached or
        # it has an instance that is being added.
        return False
    


GolfRoundHoleFormset = inlineformset_factory(
    GolfRound, GolfRoundHole, fields=("__all__"), extra=0
)    


class BaseRoundWithHolesFormset(BaseInlineFormSet):
    
    def add_fields(self, form, index):
        super().add_fields(form, index)
        # Save the formset for a Book's Images in the nested property.
        form.nested = GolfRoundHoleFormset(
            instance=form.instance,
            data=form.data if form.is_bound else None,
            files=form.files if form.is_bound else None,

        )
        # print(form.nested)
        print(form.instance)    
    
    def is_valid(self):
        """
        Also validate the nested formsets.
        """
        result = super().is_valid()

        if self.is_bound:
            for form in self.forms:
                if hasattr(form, "nested"):
                    result = result and form.nested.is_valid()

        return result
    
    def clean(self):
        """
        If a parent form has no data, but its nested forms do, we should
        return an error, because we can't save the parent.
        For example, if the Book form is empty, but there are Images.
        """
        super().clean()

        for form in self.forms:
            if not hasattr(form, "nested") or self._should_delete_form(form):
                continue

            if self._is_adding_nested_inlines_to_empty_form(form):
                form.add_error(
                    field=None,
                    error=_(
                        "You are trying to add image(s) to a book which "
                        "does not yet exist. Please add information "
                        "about the book and choose the image file(s) again."
                    ),
                )

    def save(self, commit=True):
        """
        Also save the nested formsets.
        """
        result = super().save(commit=commit)

        for form in self.forms:
            if hasattr(form, "nested"):
                if not self._should_delete_form(form):
                    form.nested.save(commit=commit)

        return result
    
    def _is_adding_nested_inlines_to_empty_form(self, form):
        """
        Are we trying to add data in nested inlines to a form that has no data?
        e.g. Adding Images to a new Book whose data we haven't entered?
        """
        if not hasattr(form, "nested"):
            # A basic form; it has no nested forms to check.
            return False

        if is_form_persisted(form):
            # We're editing (not adding) an existing model.
            return False

        if not is_empty_form(form):
            # The form has errors, or it contains valid data.
            return False

        # All the inline forms that aren't being deleted:
        non_deleted_forms = set(form.nested.forms).difference(
            set(form.nested.deleted_forms)
        )

        # At this point we know that the "form" is empty.
        # In all the inline forms that aren't being deleted, are there any that
        # contain data? Return True if so.
        return any(not is_empty_form(nested_form) for nested_form in non_deleted_forms)
    

class TeeTimeRoundHoleForm(forms.ModelForm):
    
    class Meta():
        model = TeeTime
        fields = '__all__'
views.py

class GolfRoundScoreView(InlineFormSetView):
    model = TeeTime
    inline_model = GolfRound
    fields = '__all__'
    formset_class = BaseRoundWithHolesFormset
    form_class = TeeTimeRoundHoleForm
    factory_kwargs = {'extra': 0}
    template_name = 'golf_trip/golfround_formset.html'

Red is GolfRound
Blue is GolfRoundHole
image

I'm going to try and do this for the UpdateInlinesView, as I would really like to use that. Will update if there are differences there

@egkennedy93
Copy link

Got this to work for UpdateView. I just had to play around with the forms and formsets for the view:

views.py

class GolfRoundInLine(InlineFormSetFactory):
    model = GolfRound
    form_class = HoleForm
    formset_class = BaseRoundWithHolesFormset

class GolfRoundScoreView(UpdateWithInlinesView):
    model = TeeTime
    inlines = [GolfRoundInLine]
    
    form_class = TeeTimeRoundHoleForm
    factory_kwargs = {'extra': 0}
    template_name = 'golf_trip/golfround_formset.html'

    def get_success_url(self):
        return self.object.get_absolute_url()




forms.py

class HoleForm(forms.ModelForm):

    class Meta():
        model = GolfRound
        fields = '__all__'

class TeeTimeRoundHoleForm(forms.ModelForm):
    hcp_index = forms.DecimalField(decimal_places=1, max_digits=3)
    golfer = golferSelectField(to_field_name="pk", queryset=GolfTripMember.objects.all())

    
    class Meta():
        model = TeeTime
        fields = '__all__'
        widgets = {
            'net_score': forms.TextInput(attrs={'class': 'td-score'}),
            'gross_score': forms.TextInput(attrs={'class': 'td-score'}),
        }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants