Get current user in form (formset)

Hi!
I want to fill in the first line of my inline with current user, but don’t really understand how to do so.

Current inline and formset:

class AuthorInlineFormset(BaseGenericInlineFormSet):
    def __init__(self, *args, **kwargs):
        self.request = kwargs.pop("request", None)
        super().__init__(*args, **kwargs)

    def save_new(self, form, commit=True):
        instance = super().save_new(form, commit=False)
        instance.department_at_moment = instance.employee.department
        if self.request and instance.employee == self.request.user:
            instance.has_read = True
        if commit:
            instance.save()
            if form.cleaned_data.get("save_m2m", True):
                form.save_m2m()
        return instance

class AuthorInline(GenericTabularInline):
    model = Author
    extra = 1
    formset = AuthorInlineFormset
    fields = ("employee", "int_affiliation", "role")
    autocomplete_fields = ["employee"]
    classes = (
        "author-inline",
        "basic_author-inline",
    )

The model:

class Author(models.Model):
    content_type: ForeignKey = ForeignKey(
        ContentType, on_delete=models.CASCADE, null=True, blank=True
    )
    object_id: models.PositiveIntegerField = models.PositiveIntegerField()
    content_object = GenericForeignKey("content_type", "object_id")
    employee: ForeignKey = ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name="author",
        verbose_name="Автор",
    )
    department_at_moment: ForeignKey = ForeignKey(
        Department,
        # to_field="name",
        on_delete=models.CASCADE,
        related_name="author",
        verbose_name="Департамент",
        help_text="Департамент, в котором автор был на момент работы",
    )
    int_affiliation: Uint1Field = Uint1Field(
        default=1, verbose_name="Количество аффиляций"
    )
    role: CharField = CharField(
        max_length=20, blank=True, null=True, verbose_name="Роль"
    )
    has_read: BooleanField = BooleanField(
        default=False,
        db_comment="Прочитал ли пользователь объект, в котором его указали",
    )

Welcome @Jrol123 !

First, a side note: When posting code here, please get rid of excess blank lines. Having double or triple spaced lines makes the code extremely difficult to read here. (I’ve taken the liberty of editing your original post to condense it.)

When you create the instance of the formset, you can set initial values - see Formsets | Django documentation | Django. (It’s worth reviewing the entire page.)

We might be able to provide more specific assistance if you post the view in which you are rendering and using this formset.

I’m also not sure I understand why you are using a generic version of these formsets. Can you explain the situation in more detail? (This may simply become more clear once I see how you’re using this.)

1 Like

Okay!
Will check a bit later. Will reply again after i review the link you sent me.

In a mean time, if i recall correctly, i use generic versions because of the generic foreign key of the Author model, but i may be wrong, because i was doing this part when i just started my project and was relying heavily on the Deepseek to guide me at my first steps.

P. S.
Thanks for formatting my code. It looks much better now, and sorry that i didn’t format it myself as i posted it.

P. P. S. Before i leave to check the link, could you explain, please, why does the AuthorInlineFormset.save_existing() doesn’t trigger when i try and change some data in the model, that uses it as inline?
I don’t change nothing on the Inline itself, just some fields on the ‘parent’ model

def save_existing(self, form, instance, commit=True):
        if self.request and instance.employee == self.request.user:
            instance.has_read = True
        if 'employee' in form.changed_data:
            instance.department_at_moment = instance.employee.department
        return super().save_existing(form, instance, commit)

The snippets you’re showing here aren’t enough to answer your questions.

We would need to see the complete view that is using this formset, and if there are other questions concerning the formset, the complete code for that as well.

I don’t see a documented save_existing api on a form or a formset - I wouldn’t suggest trying to use it.

So, i was able to look at the link you send and yeah, that’s almost exactly the thing i was looking for!
The only problem is that it always creates a new form when i open an already created object with its inline.

Any suggestions how to fix it?

class AuthorInlineFormset(BaseGenericInlineFormSet):
    def __init__(self, *args, **kwargs):
        self.request = kwargs.pop("request", None)
        super().__init__(initial=[{"employee": self.request.user}], *args, **kwargs)

    def save_new(self, form, commit=True):
        instance = super().save_new(form, commit=False)
        instance.department_at_moment = instance.employee.department
        if self.request and instance.employee == self.request.user:
            instance.has_read = True
        if commit:
            instance.save()
            if form.cleaned_data.get("save_m2m", True):
                form.save_m2m()
        return instance

P. S.
It all happens in the admin panel.

P. P. S.
I got the save_new and save_existing ideas from BaseModelFormSet.
Although, in its child model, BaseGenericInlineFormSet there’s only save_new.
Any ideas how to work with saving the existing ones?

P. P. P. S.
I realize now that it’s because i keep the ‘extra’ field not equal to zero.
So, now the question is:
How to nullify the ‘extra’ field if the objects are already created?

Well… At the end i created this abomination:

class AuthorInlineFormset(BaseGenericInlineFormSet):
    def __init__(self, *args, **kwargs):
        self.request = kwargs.pop("request", None)
        self.parent_instance = kwargs.get("instance", None)
        if self.parent_instance and self.parent_instance.pk:
                self.extra = 0
        else:
            self.extra = 1
            if self.request and self.request.user.is_authenticated:
                kwargs["initial"] = [{"employee": self.request.user}]
        super().__init__(*args, **kwargs)

    def save_new(self, form, commit=True):
        instance = super().save_new(form, commit=False)
        instance.department_at_moment = instance.employee.department
        if self.request and instance.employee == self.request.user:
            instance.has_read = True
        if commit:
            instance.save()
            if form.cleaned_data.get("save_m2m", True):
                form.save_m2m()
        return instance

With the Inline being:

class AuthorInline(GenericTabularInline):
    model = Author
    extra = 1

    formset = AuthorInlineFormset

    fields = ("employee", "int_affiliation", "role")
    autocomplete_fields = ["employee"]
    
    def get_formset(self, request, obj=None, **kwargs):
        FormSet = super().get_formset(request, obj, **kwargs)

        class RequestFormSet(FormSet):
            def __init__(self, *args, **kwargs):
                kwargs["request"] = request
                super().__init__(*args, **kwargs)

        return RequestFormSet

    classes = (
        "author-inline",
        "basic_author-inline",
    )

I am not sure that i have done everything correctly (the step with the get_formset in AuthorInline for the request is still pretty wild for me, but it works!)

As soon as mr. KenWhitesell checks it (because i really want to understand if i have done everything correctly) i’ll close the topic

In general, I can say that you haven’t, because you’re still using an undocumented (internal) API. (It’s undocumented for a reason, and you have no guarantees that the method will not be changed, renamed, or otherwise altered at any point in time.)

But I can’t comment on more than that without seeing the complete code for the ModelAdmin class using these, and verification that these are the complete inline and formset classes.

These AuthorInline are used in a bunch of different ModelAdmin’s, so i’ll show just one:

@admin.register(Publication)
class PublicationAdmin(admin.ModelAdmin):
    list_display = ("id", "publication_type", "display_authors", "total_affiliation")
    autocomplete_fields = ["grant"]
    change_form_template = "admin/publications/publication/change_form.html"
    inlines = [
        AuthorWithoutRoleInline,
        AuthorOnlyInline,
        AuthorInline,
        ArticleInline,
        MonographInline,
        FieldResearchReportInline,
        CollectionInline,
    ]

    def get_fieldsets(self, request, obj=None):
        """Разделяем поля на две группы: до и после инлайна автора"""
        fieldsets = (
            (
                "Основная информация",
                {
                    "fields": (
                        "publication_type",
                        "grant",
                    ),
                },
            ),
            (
                "Дополнительная информация",
                {
                    "fields": (
                        "year_publication",
                        "quarter_publication",
                        "foreign_work",
                    ),
                },
            ),
        )
        return fieldsets

    def display_authors(self, obj):
        """
        Возвращает строку со всеми авторами, привязанными к этой публикации.
        """
        authors = Author.objects.filter(
            content_type__model="publication", object_id=obj.id
        )
        author_names = [str(author.employee) for author in authors]
        return ", ".join(author_names) if author_names else "Нет авторов"
    display_authors.short_description = "Авторы"

    def save_related(self, request, form, formsets, change):
        """
        Вызывается после сохранения всех инлайнов (авторов).
        """
        super().save_related(request, form, formsets, change)
        form.instance.total_affiliation = form.instance.calculate_total_affiliation()
        form.instance.save(update_fields=["total_affiliation"])

    def get_formsets_with_inlines(self, request, obj=None):
        formsets_with_inlines = super().get_formsets_with_inlines(request, obj)
        if obj:
            filtered_formsets = []
            for formset, inline in formsets_with_inlines:
                if (
                    (
                        isinstance(inline, AuthorWithoutRoleInline)
                        and obj.publication_type in ("collection", "article")
                    )
                    or (
                        isinstance(inline, AuthorInline)
                        and obj.publication_type == "monograph"
                    )
                    or (
                        isinstance(inline, AuthorOnlyInline)
                        and obj.publication_type == "field_research_report"
                    )
                ):
                    filtered_formsets.append((formset, inline))
                if (
                    (
                        isinstance(inline, ArticleInline)
                        and obj.publication_type == "article"
                    )
                    or (
                        isinstance(inline, MonographInline)
                        and obj.publication_type == "monograph"
                    )
                    or (
                        isinstance(inline, FieldResearchReportInline)
                        and obj.publication_type == "field_research_report"
                    )
                    or (
                        isinstance(inline, CollectionInline)
                        and obj.publication_type == "collection"
                    )
                ):
                    filtered_formsets.append((formset, inline))
            return filtered_formsets
        return formsets_with_inlines

    class Media:
        js = (
            "admin/js/jquery.init.js",
            "js/publication_admin.js",
        )
    fields = (
        "publication_type",
        "grant",
        "year_publication",
        "quarter_publication",
        "foreign_work",
    )
    readonly_fields = (
        "total_affiliation",
    )

And yes, the InlineFormset is used in all of the Author*Inline types (they’re different from each other just by the fields, so i don’t see the reason to show all of them, with the exception of AuthorOnlyInline, that additionaly uses that:

def formfield_for_foreignkey(
        self, db_field: ForeignKey, request: HttpRequest, **kwargs: Any
    ) -> ModelChoiceField | None:
        if db_field.name == "employee":
            kwargs["label"] = "составитель"
        return super().formfield_for_foreignkey(db_field, request, **kwargs)

)

If there’s a more correct way to do so, please tell me!

I think this helps me a lot.

To echo this back to try and confirm my understanding:

  • You have a number of different models all sharing AuthorInline - among others.
  • The Author model has a Generic FK to relate it to one of the model types.
    • This implies that if an Author has multiple Publication, there will be multiple instances of Author.
    • Given that Author has FKs to both “content” and employee, Author is effectively the join table between employee and “content”. (Using the term “content” as a generic term of reference to the Publication types.)

Assuming the above is all correct, your original question was:

You have:

Which does appear to me to satisfy the requirement - and looks fine to me. I don’t see anything “wrong” here.

So my only question concerns the use of save_new. As an undocumented API, using it comes with risks regarding future updates to Django. (An undocumented function can be changed in any release without going through the deprecation process.)

Fundamentally, save_new ends up calling save on the form - which is a documented API, I’d be looking at adding this functionality there rather than here.

But I’m also quite pragmatic about such things. The last time this was changed was 7 years ago, and most of it has been stable for more than 10 years - making the likelihood of significant changes quite small.

In the end, it is your decision to make. It’s not a question of “right” vs “wrong” (or “correct” vs “incorrect”), it’s “What are you comfortable with?” It’s a tradeoff. This is probably more straight-forward than the alternatives, but does come with a risk. If you’re comfortable with that, it wouldn’t be my place to recommend against it.

1 Like

Yeah, you’re correct!
The Author is a join table, as one employee may take multiple roles on the single Publication and i need to save the current_department that the employee was at at the moment of creating the Publication.

Okay, so, the save is being called each time the object is being saved (both when it’s a new one and when it already exists).

I’ve done a bit of googling and came out with this:

class AuthorInlineFormset(BaseGenericInlineFormSet):
    def __init__(self, *args, **kwargs):
        self.request = kwargs.pop("request", None)
        self.parent_instance = kwargs.get("instance", None)
        if self.parent_instance and self.parent_instance.pk:
            self.extra = 0
        else:
            self.extra = 1
            if self.request and self.request.user.is_authenticated:
                kwargs["initial"] = [{"employee": self.request.user}]
        super().__init__(*args, **kwargs)
        
    def save(self, commit=True):
        result = super(AuthorInlineFormset, self).save(commit=False)
        for form in self.forms:
            author = form.save(commit=False)
            if author.employee == self.request.user:
                author.has_read = True
            if commit:
                author.save()
                if form.cleaned_data.get("save_m2m", True):
                    form.save_m2m()
        return result

So, i replaced the undocumented save_new with the normal save
I didn’t exactly found in the documentation the part for the overriding the default save, but, as i understand correctly, it first saves but not commits the forms which i then manually commit, correct?

And now i get this error if i try to save the model with the current ‘filled in’ Author:

Django Version: 5.2.7
Python Version: 3.11.14
Installed Applications:
['polymorphic',
 'users',
 'django.contrib.admin',
 'django.contrib.auth',
 'django.contrib.contenttypes',
 'django.contrib.sessions',
 'django.contrib.messages',
 'django.contrib.staticfiles',
 'core',
 'publications',
 'conferences_presentations',
 'reviews',
 'organizational_work',
 'scientific_guidance',
 'teaching_activities',
 'field_study',
 'expertise',
 'admin_autoregister',
 'django_jsonform']
Installed Middleware:
['django.middleware.security.SecurityMiddleware',
 'django.contrib.sessions.middleware.SessionMiddleware',
 'django.middleware.common.CommonMiddleware',
 'django.middleware.csrf.CsrfViewMiddleware',
 'django.contrib.auth.middleware.AuthenticationMiddleware',
 'django.contrib.messages.middleware.MessageMiddleware',
 'django.middleware.clickjacking.XFrameOptionsMiddleware']



Traceback (most recent call last):
  File "/usr/local/lib/python3.11/site-packages/django/core/handlers/exception.py", line 55, in inner
    response = get_response(request)
               ^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/django/core/handlers/base.py", line 197, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/django/contrib/admin/options.py", line 719, in wrapper
    return self.admin_site.admin_view(view)(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/django/utils/decorators.py", line 192, in _view_wrapper
    result = _process_exception(request, e)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/django/utils/decorators.py", line 190, in _view_wrapper
    response = view_func(request, *args, **kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/django/views/decorators/cache.py", line 80, in _view_wrapper
    response = view_func(request, *args, **kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/django/contrib/admin/sites.py", line 246, in inner
    return view(request, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/django/contrib/admin/options.py", line 1984, in add_view
    return self.changeform_view(request, None, form_url, extra_context)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/django/utils/decorators.py", line 48, in _wrapper
    return bound_method(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/django/utils/decorators.py", line 192, in _view_wrapper
    result = _process_exception(request, e)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/django/utils/decorators.py", line 190, in _view_wrapper
    response = view_func(request, *args, **kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/django/contrib/admin/options.py", line 1843, in changeform_view
    return self._changeform_view(request, object_id, form_url, extra_context)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/django/contrib/admin/options.py", line 1895, in _changeform_view
    self.save_related(request, form, formsets, not add)
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/django/contrib/admin/options.py", line 1342, in save_related
    self.save_formset(request, form, formset, change=change)
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/django/contrib/admin/options.py", line 1330, in save_formset
    formset.save()
    ^^^^^^^^^^^^^^
  File "/app/src/core/authorAdmin.py", line 43, in save
    author.department_at_moment = author.employee.department
                                  ^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/django/db/models/fields/related_descriptors.py", line 271, in __get__
    raise self.RelatedObjectDoesNotExist(
    ^

Exception Type: RelatedObjectDoesNotExist at /admin/scientific_guidance/scientificguidance/add/
Exception Value: Author has no employee.

But when i select the other Author in form, everything works perfectly.
Help, please!

P. S. Happy new year!

The problem is that the super().save() returns an empty array, but if i use additional form, with the total count of 2, the super().save() returns an array with the length of 1.

… And now that i started testing it more thoroughly, it doesn’t even save with the save_new method. Not throwing any errors, but it just not creating new Author object.

And yeah, i really don’t understand why it doesn’t save properly. The only clue i have is the next quote from the documentation:

If you use an initial for displaying a formset, you should pass the same initial when processing that formset’s submission so that the formset can detect which forms were changed by the user. For example, you might have something like: ArticleFormSet(request.POST, initial=[…]).

But i don’t get it. Please, help

P. S. I rewrote a save method a bit, both as a workaround and just to straighten things up:

    @property
    def _new_init(self):
        """Is it the first time the formset being initialised"""
        return self.extra == 1

    def save(self, commit=True):
        result = super().save(commit=False)
        print(len(self.forms), len(result))
        if (self._new_init
            and not any(instance.employee == self.request.user for instance in result)
            and len(self.forms) != len(result)
        ):
            result.insert(0, self.model(content_object=self.instance, employee=self.request.user))
        for author in result:
            author.department_at_moment = author.employee.department
            if author.employee == self.request.user:
                author.has_read = True
            if commit:
                author.save()
        return result

Quite honestly, you’re digging into areas that either I’m not familiar with or have never dealt with. (e.g. GFKs in the admin, conditional initialization of inline formsets in the admin)

In the general case, this isn’t the type of view that I would be looking to implement in the admin.

My personal opinion (with no technical basis that I can directly reference), I’m getting the feeling that this is one of those cases where the effort involved in making the admin do what you want ends up involving a lot more work than if you just created the custom views yourself.

If I were doing this in a view, I’d know that I’d have complete control over the form and formset, after the data was POSTed and the formset regenerated. I could then alter that form data before calling save. But adding the admin into the mix feels like it removes one level of control from me.

I am curious enough to keep looking at this myself to see if there’s something I’m missing - but I can’t say when I might find something.

Maybe one of the other members here have some thoughts or ideas to add?

2 Likes

To be honest, maybe you’re right.
But my project is for the internal usage and i thought to only use the admin panel. Well, it seems that i would need to learn now how the views work if i wish to rewrite the save method…
Maybe even override the default admin site, but that’s the other question.

For now, i managed to make it work by making the self.initial_authors in the formset and then, at the save check if it’s an initial opening of the form (with the creation of objects, thus using the extra and initial), then manually creating and adding all the missed authors to the result variable and then just checking and cleaning the data.
Hope it’ll help someone someday, but for now i’ll close this question as the solution, though not the most direct, has been found.
Will select current message as a solution only because of the code.
Big thanks to the @KenWhitesell for help!

class AuthorInlineFormset(BaseGenericInlineFormSet):
    def __init__(self, *args, **kwargs):
        self.request = kwargs.pop("request", None)
        self.parent_instance = kwargs.get("instance", None)
        if self.parent_instance and self.parent_instance.pk:
            self.extra = 0
            self.initial_authors = []
        else:
            self.extra = 1
            self.initial_authors = [
                {
                    "employee": self.request.user,
                    # "content_object": self.parent_instance, # Doesn't help
                }
            ]
        super().__init__(*args, initial=self.initial_authors, **kwargs)

    @property
    def _new_init(self):
        """If formset is initialised for the first time"""
        return self.extra == 1

    # Not the most straightforward approach, but it works. Maybe can be fixed with this. https://docs.djangoproject.com/en/5.2/topics/forms/formsets/#using-initial-data-with-a-formset
    def save(self, commit=True):
        result = super().save(commit=False)
        if self._new_init and len(self.forms) != len(result):
            # [::-1] is used for saving in the same order as authors were displayed in the form (it inflicts the order of authors after the save)
            for initial_author in self.initial_authors[::-1]:
                if not any(
                    instance.employee == initial_author["employee"]
                    for instance in result
                ):
                    result.insert(
                        0,
                        self.model(
                            content_object=self.instance,
                            employee=initial_author["employee"],
                        ),
                    )

        for author in result:
            author.department_at_moment = author.employee.department
            if author.employee == self.request.user:
                author.has_read = True
            if commit:
                author.save()
        return result

class AuthorInline(GenericTabularInline):
    model = Author
    extra = 1
    formset = AuthorInlineFormset
    fields = ("employee", "int_affiliation", "role")
    autocomplete_fields = ["employee"]

    def get_formset(self, request, obj=None, **kwargs):
        FormSet = super().get_formset(request, obj, **kwargs)
        class RequestFormSet(FormSet):
            def __init__(self, *args, **kwargs):
                kwargs["request"] = request
                super().__init__(*args, **kwargs)

        return RequestFormSet
1 Like