Reputation: 551
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
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
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
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
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