Django set formset with current user

Version

Django 3.2.7

Context

I have a form that contains multiple inlineformset_factory formset.

For all my models, they all have a meta field call created_by .

Main form is using a model call Person, with many formsets, for example, PersonEmailFormSet and PersonImageFormSet

Problem

Only the main form Person’s created_by is filled by current user. For all the formsets (emailfs, imagefs), how can I set their created_by with current user?

Code

Here is my views and my 7 different failed approaches.

# home models.py
# base abstract model that all my models inherit
from urllib.parse import urlparse

from django.contrib.auth.models import User
from django.db import models


class Base(models.Model):
    created_at = models.DateTimeField(
        auto_now_add=True,
    )
    updated_at = models.DateTimeField(
        auto_now=True,
    )
    created_by = models.ForeignKey(
        User,
        editable=False,
        null=True,
        on_delete=models.SET_NULL,
        related_name="%(class)s_created_by",
    )
    updated_by = models.ForeignKey(
        User,
        editable=False,
        null=True,
        on_delete=models.SET_NULL,
        related_name="%(class)s_updated_by",
    )

    class Meta:
        abstract = True
        ordering = ["-pk"]
# personimage models.py
from django.db import models
from django.utils.translation import gettext_lazy as _
from imagekit.models import ProcessedImageField

from home.models import Base
from person.models import Person


# Create your models here.
class PersonImage(Base):
    person = models.ForeignKey(
        Person,
        null=True,
        on_delete=models.SET_NULL,
        related_name="person_image",
    )
    image = ProcessedImageField(
        format="WEBP",
        upload_to=update_person_image_name,
    )

    class Meta:
        db_table = "person_image"

    def __str__(self):
        return str(self.image)
# personimage forms.py
from django import forms
from django.utils.translation import gettext_lazy as _

from home.forms import MetadataFormWidget
from person.models import Person
from useruploadfile.models import PersonImage


class PersonImageForm(forms.ModelForm):
    class Meta(MetadataFormWidget):
        model = PersonImage
        exclude = ("version_number",)
        widgets = MetadataFormWidget.widgets.copy()
        person_image_widgets = {
            "image": forms.ClearableFileInput(
                attrs={"multiple": False},
            ),
        }
        widgets |= person_image_widgets

    def __init__(self, *args, **kwargs):
        # For method 6, 7 attempts
        # self.created_by = created_by
        super().__init__(*args, **kwargs)


PersonImageFormSet = forms.models.inlineformset_factory(
    Person,
    PersonImage,
    form=PersonImageForm,
    can_delete=False,
    error_messages={
        "image": {
            "required": _(
                "Please provide image for us to ensure that this person is unique"
            )
        }
    },
    extra=0,
    min_num=1,
    validate_min=True,
)
# Person views.py
class PersonCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView):
    ...
    model = Person
    form_class = PersonForm

    def get_context_data(self, **kwargs):
        context = super(PersonCreateView, self).get_context_data(**kwargs)
        if self.request.POST:
            context["emailfs"] = PersonEmailFormSet(
                self.request.POST, self.request.FILES
            )
            context["imagefs"] = PersonImageFormSet(
                self.request.POST, self.request.FILES
            )
            # Method 6
            # context["imagefs"] = PersonImageFormSet(
            #     self.request.POST, self.request.FILES, user=self.request.user
            # )
            # Method 7
            # context["imagefs"] = PersonImageFormSet(
            #     self.request.POST, self.request.FILES, form_kwargs={'created_by': self.request.user}
            # )
        else:
            context["emailfs"] = PersonEmailFormSet()
            context["imagefs"] = PersonImageFormSet()
        return context


    def form_valid(self, form):
        form.instance.created_by = self.request.user
        context = self.get_context_data()
        emailfs = context["emailfs"]
        imagefs = context["imagefs"]
        personfs = [emailfs, imagefs]
        if (
            form.is_valid()
            and all(fs.is_valid() for fs in personfs)
        ):
            self.object = form.save()
            for fs in personfs:
                # Method 1
                # fs.created_by = self.request.user
                # Method 2
                # self.created_by = self.request.user
                fs.instance = self.object
                # Method 3
                # fs.created_by = self.request.user
                # Method 4
                # fs.instance.created_by = self.request.user
                # Method 5
                # for f in fs.forms:
                #     f.user = self.request.user
                #     f.save()
                fs.save()
        else:
            return self.form_invalid(form)
        return super(PersonCreateView, self).form_valid(form)

We’ll need to see more - at least the Models behind “PersonEmailFormSet” (or ImageFormSet) and the formset definitions.

In general, you need to iterate through the individual forms. See Overriding clean on a model formset.

I just updated with the PersonImage model + formset definitions.

I don’t use the Formset clean method since they are used as inlineformset_factory, not sure if it works / recommended.

The only place that contains the save logic is under Person views.py CreateView.

Ok, I don’t see a created_by field in the PersonImage model. Nothing you do in the form for it is going to be saved.

A formset is just a collection of forms. Each form within a formset is a form, is handled and managed like any other form.

The PersonImage inherit the Base abstract model. All my models inherit Base abstract models.

So it has created_at, updated_at, created_by_id and updated_by_id columns in database. When I try to add, I am using created_by

Got it.

Ok, when your formsets are submitted, you need to follow the pattern as described in the docs I linked in my previous response.

Not sure where I screw up, after I update the PersonImage ModelForm by adding the clean method like the doc, it would throw error AttributeError: 'PersonImageForm' object has no attribute 'forms'

class PersonImageForm(forms.ModelForm):

    class Meta(MetadataFormWidget):
        model = PersonImage
        exclude = ("version_number",)
        widgets = MetadataFormWidget.widgets.copy()
        person_image_widgets = {
            "image": forms.ClearableFileInput(
                attrs={"multiple": False},
            ),
        }
        widgets |= person_image_widgets

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
    
    # This portion return this error
    # AttributeError: 'PersonImageForm' object has no attribute 'forms'
    def clean(self):
        super().clean()
        for form in self.forms:
            form.instance.created_by = self.request.user

That’s because PersonImageForm is a form. it’s not a set of forms. The formset is the set of forms.

Alright, so it means I need to change my PersonImageFormSet from inlineformset_factory to BaseInlineFormSet in order to define the clean method?

PersonImageFormSet = forms.models.inlineformset_factory(
    Person,
    PersonImage,
    form=PersonImageForm,
    can_delete=False,
    error_messages={
        "image": {
            "required": _(
                "Please provide image for us to ensure that this person is unique"
            )
        }
    },
    extra=0,
    min_num=1,
    validate_min=True,
)

Not quite - as documented in the examples at Overriding methods on an InlineFormSet, you define a custom formset class that inherits from BaseInlineFormSet, then pass it as the formset parameter in your call to inlineformset_factory.

Thanks for pointing me to the right direction. Finally understand that you can have both form and formset under inlineformset_factory.