25

That question might look similar to this one, but it's not...

I have a model structure like :

class Customer(models.Model):
    ....

class CustomerCompany(models.Model):
    customer = models.ForeignKey(Customer)
    type = models.SmallIntegerField(....)

I am using InlineModels, and have two types of CustomerCompany.type. So I define two different inlines for the CustomerCompany and override InlineModelAdmin.queryset

class CustomerAdmin(admin.ModelAdmin):
    inlines=[CustomerCompanyType1Inline, CustomerCompanyType2Inline]


class CustomerCompanyType1Inline(admin.TabularInline):
    model = CustomerCompany
    def queryset(self, request):
        return super(CustomerCompanyType1Inline, self).queryset(request).filter(type=1)

class CustomerCompanyType2Inline(admin.TabularInline):
    model = CustomerCompany
    def queryset(self, request):
        return super(CustomerCompanyType2Inline, self).queryset(request).filter(type=2)
    

All is nice and good up to here, but for adding new records for InlineModelAdmin, I still need to display type field of CustomerCompany on the AdminForm, since I can not override save method of an InlineModelAdmin like:

class CustomerCompanyType2Inline(admin.TabularInline):
    model = CustomerCompany
    def queryset(self, request):
        return super(CustomerCompanyType2Inline, self).queryset(request).filter(type=2)
    #Following override do not work
    def save_model(self, request, obj, form, change):
        obj.type=2
        obj.save()

Using a signal is also not a solution since my signal sender will be the same Model, so I can not detect which InlineModelAdmin send it and what the type must be...

Is there a way that will let me set type field before save?

djvg
  • 11,722
  • 5
  • 72
  • 103
Mp0int
  • 18,172
  • 15
  • 83
  • 114

4 Answers4

39

Alasdair's answer isn't wrong, but it has a few sore points that could cause problems. First, by looping through the formset using form as the variable name, you actually override the value passed into the method for form. It's not a huge deal, but since you can do the save without commit right from the formset, it's better to do it that way. Second, the all important formset.save_m2m() was left out of the answer. The actual Django docs recommend the following:

def save_formset(self, request, form, formset, change):
    instances = formset.save(commit=False)
    for instance in instances:
        # Do something with `instance`
        instance.save()
    formset.save_m2m()

The problem you're going to run into is that the save_formset method must go on the parent ModelAdmin rather than the inlines, and from there, there's no way to know which inline is actually being utilized. If you have an obj with two "types" and all the fields are the same, then you should be using proxy models and you can actually override the save method of each to set the appropriate type automatically.

class CustomerCompanyType1(CustomerCompany):
    class Meta:
       proxy = True

    def save(self, *args, **kwargs):
        self.type = 1
        super(CustomerCompanyType1, self).save(*args, **kwargs)

class CustomerCompanyType2(CustomerCompany):
    class Meta:
       proxy = True

    def save(self, *args, **kwargs):
        self.type = 2
        super(CustomerCompanyType2, self).save(*args, **kwargs)

Then, you don't need to do anything special at all with your inlines. Just change your existing inline admin classes to use their appropriate proxy model, and everything will sort itself out.

Chris Pratt
  • 232,153
  • 36
  • 385
  • 444
  • +1 good approach to use proxy models. I updated my answer to fix the most obvious mistakes you mentioned. That still leaves the problem of working out which inline the formset represents. – Alasdair Nov 28 '11 at 18:20
  • Actually, the proxy model approach removes the need for overriding `save_formset`. The proxies themselves have overridden `save` methods that know how to save as the right type. So, you then just use the inlines without worrying about it. – Chris Pratt Nov 28 '11 at 19:07
  • My comment wasn't clear -- I was agreeing that even after fixing the issues in `save_formset` (`save_m2m` etc), there was still the issue of which inline you're saving. I understood that the proxy model approach avoids that :) – Alasdair Nov 28 '11 at 19:24
  • Thank you, somehow i totally forgot `ProxyModel`, and the fact that its usage will solve my problem... I will try it in a short time... Thanks alot – Mp0int Dec 01 '11 at 08:50
  • I'd recommend against using Proxy models if you are planning to use permissions. As of today, [there is a terrible unfixed bug with permissions](http://code.djangoproject.com/ticket/11154) that transforms the proxy approach to an unbelievable pain. – Ivan Kharlamov Jan 11 '12 at 07:50
  • There's no consensus on that "bug" yet. I for one am on the side of those that think this isn't a bug at all. Proxy models *should* have their own permissions. There's all kinds of use cases where you may not want a user to edit the parent model but *will* allow them to edit the proxy model. You can only do that with two different permission sets. In fact, I'm using this already pretty extensively in my app. As far as I'm concerned, it's a feature. – Chris Pratt Jan 11 '12 at 15:07
  • Can you somehow unlink your answer from the cited answer please? I.e., provide the full code instead of citing another answer, etc. – EugZol Jan 19 '21 at 01:09
7

There's a save_formset method which you could override. You'd have to work out which inline the formset represents somehow.

def save_formset(self, request, form, formset, change):
    instances = formset.save(commit=False)
    for instance in instances:
        # Do something with `instance`
        instance.save()
    formset.save_m2m()
Wtower
  • 18,848
  • 11
  • 103
  • 80
Alasdair
  • 298,606
  • 55
  • 578
  • 516
4

Other answers are right when it comes to using save_formset. They are missing a way to check what model is currently saved though. To do that, you can just:

if formset.model == CustomerCompany:
    # actions for specific model

Which would make the save_formset function look like: (assuming you just want to override save for the specific model(s))

def save_formset(self, request, form, formset, change):
    for obj in formset.deleted_objects:
    obj.delete()

    # if it's not the model we want to change
    # just call the default function
    if formset.model != CustomerCompany:
        return super(CustomerAdmin, self).save_formset(request, form, formset, change)

    # if it is, do our custom stuff
    instances = formset.save(commit=False)
    for instance in instances:
        instance.type = 2
        instance.save()
    formset.save_m2m()
C.K.
  • 4,348
  • 29
  • 43
chross
  • 61
  • 1
2

For the cases where you need to take an action if the registry is new, you need to do it before saving the formset.

    def save_formset(self, request, form, formset, change):
        for form in formset:
            model = type(form.instance)
            if change and hasattr(model, "created_by"):
                # craeted_by will not appear in the form dictionary because
                # is read_only, but we can anyway set it directly at the yet-
                # to-be-saved instance.
                form.instance.created_by = request.user
        super().save_formset(request, form, formset, change)

In this case I'm also applying it when the model contains a "created_by" field (because this is for a mixin that I'm using in many places, not for a specific model).

Just remove the and hasattr(model, "created_by") part if you don't need it.

Some other properties that might be interesting for you when messing with formsets:

        """
        The interesting fields to play with are:
        for form in formset:
            print("Instance str representation:", form.instance)
            print("Instance dict:", form.instance.__dict__)
            print("Initial for ID field:", form["id"].initial)
            print("Has changed:", form.has_changed())

        form["id"].initial will be None if it's a new entry.
        """

I hope my digging helps someone else! ^^

Community
  • 1
  • 1
Pere Picornell
  • 874
  • 1
  • 8
  • 15