rfindeis
rfindeis

Reputation: 551

Django admin does not call object's save method early enough

I have two apps in Django where one app's model (ScopeItem) on its instance creation must create an instance of the other app's model as well (Workflow); i.e. the ScopeItem contains it's workflow.

This works nicely when tried from the shell. Creating a new ScopeItem creates a Workflow and stores it in the ScopeItem. In admin I get an error, that the workflow attribute is required. The attribute is not filled in and the model definition requires it to be set. The overwritten save method though does this. Hence my question is, how to call save before the check in admin happens?

If I pick an existing Workflow instance in admin and save (successfully then), then I can see that my save method is called later and a new Workflow is created and attached to the ScopeItem instance. It is just called too late.

I am aware that I could allow empty workflow attributes in a ScopeItem or merge the ScopeItem and the Workflow class to avoid the issue with admin. Both would cause trouble later though and I like to avoid such hacks.

Also I do not want to duplicate code in save_item. Just calling save from there apparently does not cut it.

Here is the code from scopeitems/models.py:

class ScopeItem(models.Model):
    title = models.CharField(max_length=64)
    description = models.CharField(max_length=4000, null=True)
    workflow = models.ForeignKey(Workflow)

    def save(self, *args, **kwargs):
        if not self.id:
            workflow = Workflow(
                description='ScopeItem %s workflow' % self.title,
                status=Workflow.PENDING)
            workflow.save()
            self.workflow = workflow
        super(ScopeItem, self).save(*args, **kwargs)

And workflow/models.py:

from django.utils.timezone import now

class Workflow(models.Model):
    PENDING = 0
    APPROVED = 1
    CANCELLED = 2
    STATUS_CHOICES = (
        (PENDING, 'Pending'),
        (APPROVED, 'Done'),
        (CANCELLED, 'Cancelled'),
    )
    description = models.CharField(max_length=4000)
    status = models.IntegerField(choices=STATUS_CHOICES)
    approval_date = models.DateTimeField('date approved', null=True)
    creation_date = models.DateTimeField('date created')
    update_date = models.DateTimeField('date updated')

    def save(self, *args, **kwargs):
        if not self.id:
            self.creation_date = now()
        self.update_date = now()
        super(Workflow, self).save(*args, **kwargs)

In scopeitems/admin.py I have:

from django.contrib import admin

from .models import ScopeItem
from workflow.models import Workflow


class ScopeItemAdmin(admin.ModelAdmin):
    list_display = ('title', 'description', 'status')
    list_filter = ('workflow__status', )
    search_fields = ['title', 'description']

    def save_model(self, request, obj, form, change):
        obj.save()

    def status(self, obj):
        return Workflow.STATUS_CHOICES[obj.workflow.status][1]

admin.site.register(ScopeItem, ScopeItemAdmin)

Upvotes: 1

Views: 2133

Answers (4)

rfindeis
rfindeis

Reputation: 551

Answering my own question:

As @pcoronel suggested, the workflow attribute in ScopeItem must have blank=True set to get out of the form in the first place.

Overwriting the form's clean method as suggested by @hellsgate was also needed to create and store the new Workflow.

To prevent code duplication I added a function to workflow/models.py:

def create_workflow(title="N/A"):
    workflow = Workflow(
        description='ScopeItem %s workflow' % title,
        status=Workflow.PENDING)
    workflow.save()
    return workflow

This makes the ScopeItemAdminForm look like this:

class ScopeItemAdminForm(forms.ModelForm):
    class Meta:
        model = ScopeItem

    def clean(self):
        cleaned_data = super(ScopeItemAdminForm, self).clean()
        cleaned_data['workflow'] = create_workflow(cleaned_data['title'])
        return cleaned_data

Additionally I changed the save method in scopeitems/models.py to:

def save(self, *args, **kwargs):
    if not self.id:
        if not self.workflow:
            self.workflow = create_workflow(self.title)
    super(ScopeItem, self).save(*args, **kwargs)

Upvotes: 1

hellsgate
hellsgate

Reputation: 6005

@Daniel Roseman's answer is correct as long as you don't need to edit the workflow field in admin at any time. If you do need to edit it then you'll need to write a custom clean() method on the admin form.

forms.py

class ScopeItemAdminForm(forms.ModelForm):
    class Meta:
        model = ScopeItem

    def clean(self):
        cleaned_data = super(ScopeItemAdminForm, self).clean()
        if 'pk' not in self.instance:
            workflow = Workflow(
                description='ScopeItem %s workflow' % self.title,
                status=Workflow.PENDING)
            workflow.save()
            self.workflow = workflow
        return cleaned_data

admin.py

class ScopeItemAdmin(admin.ModelAdmin):
    form = ScopeItemAdminForm
    ...

admin.site.register(ScopeItem, ScopeItemAdmin)

Upvotes: 1

Daniel Roseman
Daniel Roseman

Reputation: 599600

You need to exclude the field from the form used in the admin, so that it won't be validated.

class ScopeItemForm(forms.ModelForm):
    class Meta:
        exclude = ('workflow',)
        model = ScopeItem

class ScopeItemAdmin(admin.ModelAdmin):
    form = ScopeItemForm
    ...

admin.site.register(ScopeItem, ScopeItemAdmin)

Upvotes: 1

pcoronel
pcoronel

Reputation: 3971

You could set the field blank=True on workflow.

You said you don't want to allow "empty workflow attributes in a ScopeItem." Setting blank=True is purely validation-related. Thus, on the backend workflow will still be NOT NULL. From the Django docs:

If a field has blank=True, form validation will allow entry of an empty value.

Referring to your example you should be able to use:

workflow = models.ForeignKey(Workflow, blank=True)

Upvotes: 1

Related Questions