Django get instance in inline form admin
Asked Answered
S

3

9

Have a inline form class:

class ItemColorSelectForm(forms.ModelForm):
    def __init__(self, *args, **kwargs):
        super(ItemColorSelectForm, self).__init__(*args, **kwargs)
        #here i need current object

Inline class:

class ItemColorSelectInline(generic.GenericTabularInline):
    model = ColorSelect
    extra = 1
    form = ItemColorSelectForm

Admin class

class ItemAdmin(admin.ModelAdmin):
    inlines = [ItemColorInline,]

Question: how can a get current object in ItemColorSelectForm.

print kwargs return:

{'auto_id': u'id_%s', 'prefix': u'catalog-colorselect-content_type-object_id-__prefix__', 'empty_permitted': True}
Sherly answered 17/3, 2014 at 22:14 Comment(0)
S
3

Solution: Override the formset method in Inline class

def get_formset(self, request, obj=None, **kwargs):
        InlineForm.obj = obj
        return super(InlineAdmin, self).get_formset(request, obj, **kwargs)
Sherly answered 18/3, 2014 at 11:44 Comment(4)
If you're running in multi-threaded mode, you might run into trouble since InlineForm.obj is shared between threads, so the value could change between setting and getting it.Speckle
That said, it doesn't seem like Django gives us any choice, and there probably aren't many users in the admin at any time, so read this as 'you actually shouldn't do this, but I'm going to use it myself right now'.Speckle
Horrible solution that will break when run multi threaded, for example using uwsgi.Bandwidth
Why assign to class when you can assign to instance? self.instance = objBidarka
B
6

Currently accepted solution is not thread safe. If you care about thread safety, never, ever assign an instance to a static class property.

Thread safe solutions are:

For Django 1.7 < 1.9 (possibly earlier versions, unclear):

from django.utils.functional import cached_property

def get_formset(self, *args, **kwargs):
    FormSet = super(InlineAdmin, self).get_formset(*args, **kwargs)

    class ProxyFormSet(FormSet):
        def __init__(self, *args, **kwargs):
            self.instance = kwargs['instance']
            super(ProxyFormSet, self).__init__(*args, **kwargs)

        @cached_property
        def forms(self):
            kwargs = {'instance': self.instance}
            forms = [self._construct_form(i, **kwargs) 
                    for i in xrange(self.total_form_count())]
            return forms
    return ProxyFormSet

As of Django >= 1.9 it's also possible to pass form_kwargs:

def get_formset(self, *args, **kwargs):
    FormSet = super(InlineAdmin, self).get_formset(*args, **kwargs)

    class ProxyFormSet(FormSet):
        def __init__(self, *args, **kwargs):
            form_kwargs = kwargs.pop('form_kwargs', {})
            form_kwargs['instance'] = kwargs['instance']
            super(ProxyFormSet, self).__init__(
                *args, form_kwargs=form_kwargs, **kwargs)
    return ProxyFormSet

Above solutions will make an instance kwarg available in the model form:

class InlineForm(forms.ModelForm):
    def __init__(self, *args, **kwargs):
        super(InlineForm, self).__init__(*args, **kwargs)
        print('instance', kwargs['instance'])
Bandwidth answered 12/10, 2016 at 14:55 Comment(0)
S
3

Solution: Override the formset method in Inline class

def get_formset(self, request, obj=None, **kwargs):
        InlineForm.obj = obj
        return super(InlineAdmin, self).get_formset(request, obj, **kwargs)
Sherly answered 18/3, 2014 at 11:44 Comment(4)
If you're running in multi-threaded mode, you might run into trouble since InlineForm.obj is shared between threads, so the value could change between setting and getting it.Speckle
That said, it doesn't seem like Django gives us any choice, and there probably aren't many users in the admin at any time, so read this as 'you actually shouldn't do this, but I'm going to use it myself right now'.Speckle
Horrible solution that will break when run multi threaded, for example using uwsgi.Bandwidth
Why assign to class when you can assign to instance? self.instance = objBidarka
G
3

To fix: currently accepted solution not safe in multi-thread mode

Arti's solution works, another better option could be:

Instead of passing the current object id into the inline form,
use the object id to create a inline form field within the get_formset().

# admin.py
class TransactionInline(admin.TabularInline):
    model = Transaction
    form = TransactionInlineForm

    def get_formset(self, request, obj=None, **kwargs):
        # comment Arti's solution
        # TransactionInlineForm.project_id = obj.id

        formset = super().get_formset(request, obj, **kwargs)
        field = formset.form.declared_fields['purchase']
        field.queryset = get_object_or_404(Project, pk=obj.id).products.all()
        return formset
# forms.py
class TransactionInlineForm(ModelForm):
    purchase = ModelChoiceField(queryset=None, label='Purchase', required=False)

So, there is no need to override the __init__() in form anymore, neither the current object.

works in Django 2.1.7

Glowworm answered 9/5, 2019 at 8:49 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.