4

I've just created a forms.models.BaseInlineFormSet to override the default formset for a TabularInline model. I need to evaluate the user's group in formset validation (clean) because some groups must write a number inside a range (0,20).

I'm using django admin to autogenerate the interface.

I've tried getting the request and the user from the kwargs in the init method, but I couldn't get the reference.

This is what I have now:

class OrderInlineFormset(forms.models.BaseInlineFormSet):
    def __init__(self, *args, **kwargs):
        self.user = kwargs.pop('user')
        super(OrderInlineFormset, self).__init__(*args, **kwargs)

    def clean(self):
        # get forms that actually have valid data
        count = 0
        for form in self.forms:
            try:
                if form.cleaned_data:
                    count += 1
                    if self.user.groups.filter(name='Seller').count() == 1:
                        if form.cleaned_data['discount'] > 20:
                            raise forms.ValidationError('Not authorized to specify a discount greater than 20%')
            except AttributeError:
                # annoyingly, if a subform is invalid Django explicity raises
                # an AttributeError for cleaned_data
                pass
        if count < 1:
            raise forms.ValidationError('You need to specify at least one item')

class OrderItemInline(admin.TabularInline):
    model = OrderItem
    formset = OrderInlineFormset

Then I use it as inlines = [OrderItemInline,] in my ModelAdmin.

Unfortunatly self.user is always None so I cannot compare the user group and the filter is not applied. I need to filter it because other groups should be able to specify any discount percent.

How can I do? If you also need the ModelAdmin code I'll publish it (I just avoided to copy the whole code to avoid confusions).

Ale A
  • 349
  • 5
  • 16

2 Answers2

7

Well, I recognise my code there in your question, so I guess I'd better try and answer it. But I would say first of all that that snippet is really only for validating a minimum number of forms within the formset. Your use case is different - you want to check something within each form. That should be done with validation at the level of the form, not the formset.

That said, the trouble is not actually with the code you've posted, but with the fact that that's only part of it. Obviously, if you want to get the user from the kwargs when the form or formset is initialized, you need to ensure that the user is actually passed into that initialization - which it isn't, by default.

Unfortunately, Django's admin doesn't really give you a proper hook to intercept the initialization itself. But you can cheat by overriding the get_form function and using functools.partial to wrap the form class with the request argument (this code is reasonably untested, but should work):

from functools import partial

class OrderForm(forms.ModelForm):
    def __init__(self, *args, **kwargs):
        self.user = kwargs.pop('user')
        super(OrderForm, self).__init__(*args, **kwargs)

    def clean(self)
         if self.user.groups.filter(name='Seller').count() == 1:
             if self.cleaned_data['discount'] > 20:
                 raise forms.ValidationError('Not authorized to specify a discount greater than 20%')
         return self.cleaned_data

class MyAdmin(admin.ModelAdmin):
    form = OrderForm

    def get_form(self, request, obj=None, **kwargs):
        form_class = super(MyAdmin, self).get_form(request, obj, **kwargs)
        return functools.partial(form_class, user=request.user)
Daniel Roseman
  • 588,541
  • 66
  • 880
  • 895
  • 3
    Sorry to dig up such an old post, but I stumbled upon this solution and when implementing I get: 'functools.partial' object has no attribute 'base_fields' – Matthew Feb 24 '16 at 21:53
  • @Matthew you cannot use anymore partial trick with newer django versions, since it assumes the result of get_form to be a class (that has base_fields) – sherpya Apr 04 '23 at 21:54
2

Here's another option without using partials. First override the get_formset method in your TabularInline class.

Assign request.user or what ever extra varaibles you need to be available in the formset as in example below:

class OrderItemInline(admin.TabularInline):
    model = OrderItem
    formset = OrderInlineFormset

    def get_formset(self, request, obj=None, **kwargs):  
       formset = super(OrderProductsInline, self).get_formset(request, obj, **kwargs)
       formset.user = request.user
       return formset

Now the user is available in the formset as self.user

class OrderInlineFormset(forms.models.BaseInlineFormSet):

    def clean(self):
      print(self.user) # is available here 
bhaskarc
  • 9,269
  • 10
  • 65
  • 86