24

I am using the formfield_for_manytomany given in django documentation. But inside that function I need to get the current parent object being edited.

def formfield_for_manytomany(self, db_field, request, **kwargs):
    if db_field.name == "car":
        kwargs["queryset"] = Cars.objects.filter(owner=person)
    return super(myModel, self).formfield_for_manytomany(db_field, request, **kwargs)

How can I get the person being edited?

arulmr
  • 8,620
  • 9
  • 54
  • 69
user192082107
  • 1,277
  • 4
  • 15
  • 19

3 Answers3

25

If the person cannot be easily got from request, you may need to manually pass it by overriding ModelAdmin.get_form() or InlineModelAdmin.get_formset():

from functools import partial

class MyModelAdmin(admin.ModelAdmin):
    def get_form(self, request, obj=None, **kwargs):
        kwargs['formfield_callback'] = partial(self.formfield_for_dbfield, request=request, obj=obj)
        return super(MyModelAdmin, self).get_form(request, obj, **kwargs)

    def formfield_for_dbfield(self, db_field, **kwargs):
        person = kwargs.pop('obj', None)
        formfield = super(MyModelAdmin, self).formfield_for_dbfield(db_field, **kwargs)
        if db_field.name == "car" and person:
            formfield.queryset = Cars.objects.filter(owner=person)
        return formfield 

# or its inline
class MyInlineModelAdmin(admin.StackedInline):
    def get_formset(self, request, obj=None, **kwargs):
        kwargs['formfield_callback'] = partial(self.formfield_for_dbfield, request=request, obj=obj)
        return super(MyInlineModelAdmin, self).get_formset(request, obj, **kwargs)

    def formfield_for_dbfield(self, db_field, **kwargs):
        person = kwargs.pop('obj', None)
        formfield = super(MyInlineModelAdmin, self).formfield_for_dbfield(db_field, **kwargs)
        if db_field.name == "car" and person:
            formfield.queryset = Cars.objects.filter(owner=person)
        return formfield 

Or

class MyModelAdmin(admin.ModelAdmin):
    def get_form(self, request, obj=None, **kwargs):
        kwargs['formfield_callback'] = partial(self.formfield_for_dbfield, request=request, obj=obj)
        return super(MyModelAdmin, self).get_form(request, obj, **kwargs)

    def formfield_for_dbfield(self, db_field, **kwargs):
        if db_field.name != "car":
            kwargs.pop('obj', None)
        return super(MyModelAdmin, self).formfield_for_dbfield(db_field, **kwargs)

    def formfield_for_manytomany(self, db_field, request=None, **kwargs):
        person = kwargs.pop('obj', None)
        if db_field.name == "car" and person:
            kwargs['queryset'] = Cars.objects.filter(owner=person)
        return super(MyModelAdmin, self).formfield_for_manytomany(db_field, request, **kwargs)
okm
  • 23,575
  • 5
  • 83
  • 90
  • 2
    i don't get it. what is `formfield_callback` and what is `partial` – user192082107 Feb 19 '13 at 06:43
  • The callback is the behind-scene magic to invoke `formfield_for_manytomany`, we need to manually pass it the `obj`, in order to get `obj` from `kwargs` inside `formfield_for_manytomany`. The `partial` part makes a high-order function which, when being called as `formfield_callback(some_more_arguments)`, behaves like `self.formfield_for_dbfield(request, some_more_arguments)`, you could check [the doc](http://docs.python.org/2/library/functools.html#functools.partial) and [the post](http://stackoverflow.com/questions/3252228/python-why-is-functools-partial-necessary) – okm Feb 19 '13 at 06:49
  • Everything is going above my head. i have two questiosn 1. From here i can get more info about `formfield_callback` . is that build in django or its custom 2. how cna i know what are the things or fields or functions avaiable inside kwargs. i how do you know that obj is in kwargs – user192082107 Feb 19 '13 at 06:57
  • @user2082226 1. formfield_callback is a bit hard-coded in Django w/o passing the `obj`, but we could customize it easily, [check the code](https://github.com/django/django/blob/stable/1.5.x/django/contrib/admin/options.py#L462). 2. `kwargs` is filled by Django form to generate form field, thus it could contain possible things that the field accepts: `widget` and `queryset` for example. Here we do mix the arguments having different targets, thus remember to name it unique (`obj` here`). – okm Feb 19 '13 at 07:14
  • 1
    i get this error `__init__() got an unexpected keyword argument obj` – user192082107 Feb 19 '13 at 07:15
  • @user2082226 Oops, then continue to my latest comment: we also need to remove the `obj` from kwargs being used for other fields. It affects other fields that does not expect such arguments. – okm Feb 19 '13 at 07:16
  • i didn't get it. what chnages i need to do – user192082107 Feb 19 '13 at 07:27
  • @user2082226 I've fixed the code. The key point is to only pass extra `obj` to `formfield_for_manytomany` that we could handle it correctly there; Or like what's in the fixed code, pop the `obj` and modify the db_field when certain condition is met. – okm Feb 19 '13 at 07:31
  • Its not working , the person object is coming as None. aslo u need have `request` as 3d argument in many to many – user192082107 Feb 19 '13 at 08:03
  • @user2082226 fixed, what about now? `obj` should be there for `change_view`, could you check that? – okm Feb 19 '13 at 08:17
  • @user2082226 hmm, it's a bit weird. Could you paste your admin code in your question? P.S. I've fixed another bug, check the edit – okm Feb 19 '13 at 08:41
  • ok. Your first part of code is working but i was trying your second method where you have three functions defined. that is not working. actually i already had stackedinline so that was not the issue. so my problem is solved by your two solution in first part but the code with three functions is not working. i just want to know what is wrong with that. thanks for your help – user192082107 Feb 19 '13 at 08:52
  • can you please tell me which function is calling `formfield_callback` and where. i am not able to find where any function is using that – user192082107 Feb 19 '13 at 09:01
  • @user2082226 I see, fine! The second method is targeting for `ModelAdmin`, you need to change `get_form` to `get_formset` to make it work for `InlineModeAdmin`. The `formfield_callback` is called by [`modelform_factory`](https://github.com/django/django/blob/stable/1.5.x/django/forms/models.py#L377) or [`inlineformset_factory`](https://github.com/django/django/blob/stable/1.5.x/django/forms/models.py#L829) and then finally by [`fields_for_model`](https://github.com/django/django/blob/stable/1.5.x/django/forms/models.py#L134) – okm Feb 19 '13 at 09:14
  • I am not able to find , how did that `formfield_callback` transports our object inside `formfielf_manytomany` – user192082107 Feb 19 '13 at 09:30
  • @user2082226 the `formfield_callback` itself carries the `obj`: we've already passed the `obj` to `partial(...)` inside `get_formset()`. Thus when it's invoked, `obj` is there inside `kwargs` – okm Feb 19 '13 at 11:45
5

I discovered a cleaner way without all of the hacking around in get_formset() / get_formsets() / formfield_for_dbfield(). Use Django's Request object (which you have access to) to retrieve the request.path_info, then retrieve the PK from the args in the resolve match. Example:

from django.contrib import admin
from django.core.urlresolvers import resolve
from app.models import YourParentModel, YourInlineModel


class YourInlineModelInline(admin.StackedInline):
    model = YourInlineModel

    def get_parent_object_from_request(self, request):
        """
        Returns the parent object from the request or None.

        Note that this only works for Inlines, because the `parent_model`
        is not available in the regular admin.ModelAdmin as an attribute.
        """
        resolved = resolve(request.path_info)
        if resolved.args:
            return self.parent_model.objects.get(pk=resolved.args[0])
        return None

    def has_add_permission(self, request):
        parent = self.get_parent_object_from_request(request)
        if parent and parent.is_active is True:
            return False
        return super(YourInlineModelInline, self).has_add_permission(request)

    def get_formset(self, request, *args, **kwargs):
        """
        Using the get_formset example from above to override the QuerySet.
        """
        def formfield_callback(field, **kwargs):
            formfield = field.formfield(**kwargs)
            if field.name == 'car':
                formfield.queryset = self.parent_model.objects.filter(
                    owner=self.get_parent_object_from_request(request)
                )
            return formfield

        if self.get_parent_object_from_request(request) is not None:
            kwargs['formfield_callback'] = formfield_callback

        return super(YourInlineModelInline, self).get_formset(*args, **kwargs)


@admin.register(YourParentModel)
class YourParentModelAdmin(admin.ModelAdmin):
    inlines = [YourInlineModelInline]
Michael B
  • 5,148
  • 1
  • 28
  • 32
  • 1
    Not enough time for proper editing, so I'll leave here some notes to make this work under Django 2.0: `from django.urls import resolve`, `pk=resolved.kwargs['object_id']`, for ModelAdmin `self.parent_model` -> `self.model` – Vanni Totaro Dec 13 '17 at 19:00
  • I feel like this is the better, less complicated solution. I did something similar to this, but didn't use the resolve() function. I just did something like `object_id = request.path.split("/")[4]` knowing that the ID would always be the 4th parameter in the request URL. – Chris Hubbard Apr 03 '19 at 17:03
4

To access the model instance of the InlineModelAdmin's parent ModelAdmin, I have used this hack in the past:

class PersonAdmin(ModelAdmin):
    def get_formsets(self, request, obj=None, *args, **kwargs):
        for inline in self.inline_instances:
            inline._parent_instance = obj
            yield inline.get_formset(request, obj)

class CarInline(TabularInline):
    _parent_instance = None

    def get_formset(self, *args, **kwargs):
        def formfield_callback(field, **kwargs):
            formfield = field.formfield(**kwargs)
            if field.name == 'car':
                formfield.queryset = Cars.objects.filter(owner=self._parent_instance)
            return formfield

        if self._parent_instance is not None:
            kwargs['formfield_callback'] = formfield_callback
        return super(CarInline, self).get_formset(*args,
                                                                 **kwargs)
Rune Kaagaard
  • 6,643
  • 2
  • 38
  • 29