Monday, 5 April 2021

Django: form with fields for creating related objects

I have several models similar to these (example from Django docs):

from django.db import models

class Person(models.Model):
    name = models.CharField(max_length=128)

    def __str__(self):
        return self.name

class Group(models.Model):
    name = models.CharField(max_length=128)
    members = models.ManyToManyField(Person, through='Membership')

    def __str__(self):
        return self.name

class Membership(models.Model):
    person = models.ForeignKey(Person, on_delete=models.CASCADE)
    group = models.ForeignKey(Group, on_delete=models.CASCADE)
    date_joined = models.DateField()
    invite_reason = models.CharField(max_length=64)

And I have HTML form in which (if we continue the analogy with the example from the docs) there are three separate input fields for:

  • entering the name of the Person;
  • specifying the date_joined;
  • specifying the invite_reason

And button "Add member". The user fills in all these fields and clicks on this button. JS code adds hidden input fields (filled in with the entered values) to the html page (so that all entered data will be included in the POST-request data). And also JS adds an span-element to the page with information about the added member with a button "Delete" next to it. Then three visible fields are cleared so that you can enter data for the next person. It looks like this:

enter image description here

Now I see two ways to handle this situation on the Django side:

  1. Put all the logic for working with memberships in the form:
class MembershipForm(forms.ModelForm):
    class Meta:
        model = Membership
        fields = ('person', 'date_joined', 'invite_reason')


class GroupForm(forms.ModelForm):
    class Meta:
        model = Recipe
        fields = ('name',)

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.memberships = self.instance.membership_set.all()

    def clean(self):
        super().clean()
        self.memberships = []

        memberships_data = self.get_memberships_data(self.data) 
        for name, date_joined, invite_reason in memberships_data:
            data = {
                'person': name,
                'date_joined': date_joined,
                'invite_reason': invite_reason
            }
            membership_form = MembershipForm(data)
            if membership_form.is_valid():
                self.memberships.append(membership_form.instance)
            else:
                membership_errors = sum(membership_form.errors.values(), [])
                self.add_error(None, membership_errors)

        # There may be other checks here as well. 
        # For example, checks for the presence of at least one group member

    def _save_m2m(self):
        super()._save_m2m()
        self.instance.members.clear()
        self.instance.membership_set.set(self.memberships, bulk=False)
  1. Use inlineformset_factory (example for CreateView)
MembershipFormSet = forms.inlineformset_factory(Group, Membership, fields=('person', 'date_joined', 'invite_reason'))

class CreateGroupView(CreateView):
    model = Group
    ...

    def get(self, request, *args, **kwargs):
        self.object = None
        form_class = self.get_form_class()
        form = self.get_form(form_class)
        membership_formset = MembershipFormSet()
        return self.render_to_response(
            self.get_context_data(
                form=form,
                membership_formset=membership_formset
            )
        )

    def post(self, request, *args, **kwargs):
        self.object = None
        form_class = self.get_form_class()
        form = self.get_form(form_class)
        membership_formset = MembershipFormSet(self.request.POST)
        if (form.is_valid() and membership_formset.is_valid()):
            return self.form_valid(form, membership_formset)
        else:
            return self.form_invalid(form, membership_formset)

    def form_valid(self, form, membership_formset):
        self.object = form.save(commit=False)
        self.object.author = self.request.user
        self.object.save()
        membership_formset.instance = self.object
        membership_formset.save()
        return HttpResponseRedirect(self.get_success_url())

    def form_invalid(self, form, membership_formset):
        return self.render_to_response(
            self.get_context_data(
                form=form,
                membership_formset=membership_formset
            )
        )

In this case, all the logic for processing forms will be in view-functions (or View classes). Also, this approach assumes a certain format of hidden input fields, which allows the formset to retrieve data without additional settings. And it imposes additional restrictions on the js code (in terms of removing items from the list). However, this approach seems like a cleaner solution to me.

I would like to hear the opinion of experienced developers which approach would be most suitable here. Or both the first and the second are bad, but may be there is another, more appropriate for this situation.

If the problem is not clear, I am ready to answer any clarifying questions

Thanks for any help.



from Django: form with fields for creating related objects

No comments:

Post a Comment