Django 管理界面未及时调用对象的保存方法

1 投票
4 回答
1968 浏览
提问于 2025-04-18 07:05

我在Django中有两个应用,其中一个应用的模型(ScopeItem)在创建实例时必须同时创建另一个应用的模型实例(Workflow);也就是说,ScopeItem包含它的工作流程。

在命令行中测试时,这个功能运行得很好。创建一个新的ScopeItem会同时创建一个Workflow并将其存储在ScopeItem中。但是在管理后台,我遇到了一个错误,提示workflow属性是必需的。这个属性没有被填入,而模型定义要求它必须设置。不过,我重写的save方法是可以做到这一点的。因此,我的问题是,如何在管理后台检查之前调用save方法?

如果我在管理后台选择一个已有的Workflow实例并保存(成功保存后),我可以看到我的save方法会在之后被调用,这时会创建一个新的Workflow并附加到ScopeItem实例上。只是这个调用的时机太晚了。

我知道我可以允许ScopeItem中的workflow属性为空,或者将ScopeItemWorkflow类合并,以避免在管理后台出现这个问题。不过这两种方法以后都会带来麻烦,我想避免这种“黑科技”。

另外,我不想在save_item中重复代码。仅仅从那里调用save显然是不够的。

这是来自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)

还有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)

scopeitems/admin.py中,我有:

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)

4 个回答

1

@Daniel Roseman的回答是对的,只要你在后台管理界面不需要修改工作流字段。如果你需要修改它,那么你就得在管理表单上写一个自定义的clean()方法。

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)
1

你可以在 workflow 字段上设置 blank=True

你提到不想在 ScopeItem 中允许“空的 workflow 属性”。设置 blank=True 只是为了验证的需要。因此,在后台 workflow 仍然会是 NOT NULL。根据 Django 的文档:

如果一个字段设置了 blank=True,表单验证会允许输入空值

根据你的例子,你应该可以使用:

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

你需要把这个字段从管理员使用的表单中去掉,这样它就不会被验证了。

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

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

admin.site.register(ScopeItem, ScopeItemAdmin)
1

我来回答我自己的问题:

正如@pcoronel所建议的,ScopeItem中的workflow属性必须设置blank=True,这样才能在表单中正常使用。

按照@hellsgate的建议,重写表单的clean方法也是必要的,这样才能创建并保存新的Workflow

为了避免代码重复,我在workflow/models.py中添加了一个函数:

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

这使得ScopeItemAdminForm看起来像这样:

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

另外,我还在scopeitems/models.py中修改了save方法为:

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)

撰写回答