Django管理后台:如何格式化只读字段?
我有一个模型,叫做 Director
,里面有两个日期字段,还有两个子类(代码在下面)。我想为每个 Director 创建一个管理页面,显示对应的子类实例,而不是 Director
实例;这部分相对简单(我为每个子类创建了一个内联,给主模型管理器提供一个排除了所有字段的表单,并且主模型管理器只请求那些有对应实例的内联的表单集 - 代码在下面;不过这种方法有一个未解决的问题,我在下面提到,但这不是这个问题的重点)。
我遇到的问题是,我想调整显示给用户的值,其中一个是在只读字段中显示,另一个则不是。我想把一个特殊的值(date(1,1,1)
)改成字符串 "On incorporation"
。
只读字段中的日期格式对解析不太友好,我希望减少对 JavaScript 的不必要依赖,所以我更倾向于使用服务器端的解决方案。
下面的代码显示了我想要的表单,除了日期值没有被调整,而且在保存时出现了一个多余的“请纠正下面的错误”消息,尽管实际上没有错误,所有字段都正确保存。
我的问题是:我该如何拦截要在页面上显示的值,无论是在只读字段中,还是在表单字段中,并将它们更改为我选择的字符串?
模型(目前为止的内容):
class Director(models.Model, Specializable):
date_of_appointment = models.DateField()
date_ceased_to_act = models.DateField(blank=True,null=True)
class DirectorsIndividual(Director):
pass
class DirectorsCorporate(Director):
pass
管理代码:
class DirectorAdmin(EnhancedAdmin):
fields = ()
## def formfield_for_dbfield(self, db_field, **kwargs):
## return None
def queryset(self, request):
""" Directors for all companies which are incorporated by the current user's organisation """
individual = Individual.for_user(request.user)
return Director.objects.filter(company__incorporation_ticket__ordered_by__in = Organisation.all_organisations_for_which_individual_authorised_to_incorporate(individual))
class form(forms.ModelForm):
# have this return no html - that way only inlines are shown
class Meta:
fields = ()
pass
def is_valid(self):
self._errors = {}
return True
class DirectorsIndividualInline(admin.StackedInline):
model = DirectorsIndividual
fk_name = 'director_ptr'
extra = 0
readonly_fields = ('deferred_on','company','date_of_appointment',)
can_delete = False
def get_readonly_fields(self, request, obj=None):
if obj and obj.company and not obj.company.is_submitted(): return self.readonly_fields # allow editing of fields listed in else
else:
return itertools.chain(self.readonly_fields, ('individual', 'is_secretary'))
def has_delete_permission(self, request, obj=None):
return obj and ((obj.company and not obj.company.is_submitted()) or not obj.company)
class form(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(forms.ModelForm, self).__init__(*args, **kwargs)
self.fields['surrogate_for'].required = False
self.fields['representative_for'].required = False
if self.instance:
obj = self.instance
for field in (f for f in type(obj)._meta.fields if type(f) == fields.DateField):
val = field.value_from_object(obj)
assert (type(val) in (datetime.date, type(None),))
# assert field.name != 'date_of_appointment'
if val == inc_consts.EARLIEST_DATE:
self.initial[field.name] = "On incorporation"
def is_valid(self):
self._errors = {}
return True
class DirectorsCorporateInline(admin.StackedInline):
model = DirectorsCorporate
fk_name = 'director_ptr'
extra = 0
can_delete = False
class form(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(forms.ModelForm, self).__init__(*args, **kwargs)
if True:
for k in self.fields:
self.fields[k].required = False
def is_valid(self):
self._errors = {}
return True
inlines = (DirectorsIndividualInline,DirectorsCorporateInline)
def get_inlines(self, request, obj=None):
return (inline for inline in (self.inline_instances)
if inline.model.objects.filter(**{(inline.fk_name or self.model._meta.object_name.lower()) : obj }))
def get_formsets(self, request, obj=None):
""" only return formset for inlines for which there exists an object """
return (inline.get_formset(request, obj) for inline in self.get_inlines(request, obj))
我意识到 DirectorsCorporateInline
和 DirectorsIndividualInline
之间存在不对称;这是因为我正在测试一个包含 DirectorsIndividual
实例的实例。上面的代码提到了模型中未显示的字段,因为它们与日期问题无关;应该可以在不更改那些字段的情况下,使它们与多余的错误问题无关(虽然我知道这对那个问题的帮助较小,但我想把这个问题主要集中在一个问题上)。 EnhancedAdmin
是一个 ModelAdmin
的子类,做了一些小的修改,这些修改应该不会影响结果。额外的代码可以根据合理的请求提供,但我不想用无关的代码来混淆问题。
为了完整起见:我在使用 Python 2.7.2 的 Django 1.3.1。
4 个回答
正如@drjimbob(还有在#django上的carljm)所建议的,解决办法是给模型创建一个成员函数或属性,比如:
class Director(models.Model, Specializable):
date_of_appointment = models.DateField()
date_ceased_to_act = models.DateField(blank=True,null=True)
#def date_formatter and def _date_format_factory omitted
date_of_appointment_formatted = lambda self: self.date_formatter(getattr(self, 'date_of_appointment'))
date_ceased_to_act_formatted = _date_format_factory(None, 'date_ceased_to_act') #for some reason, I get a 'classmethod/staticmethod object is not callable' error if I decorate _date_format_factory
date_of_appointment_formatted.short_description = u'Date of appointment'
注意这里的 date_of_appointment_formatted.short_description
- ModelAdmin
会把 short_description
当作 readonly_field
的标签。
为了让这些属性和模型字段一起工作,需要一个自定义表单:
class DirectorInlineForm(EnhancedModelForm):
from django.utils import formats
date_ceased_to_act_formatted = forms.DateField(required = False, widget = admin.widgets.AdminDateWidget,
label = u'Date officer\'s appointment terminated',
input_formats = formats.get_format('DATE_INPUT_FORMATS') + (Director.on_incorporation,))
class Meta:
model = Director # Note that model declaration is necessary for this to work with additional fields declared
def __init__(self, *args, **kwargs):
super(DirectorInlineForm, self).__init__(*args, **kwargs)
# set initial values from model of declared fields
if self.instance:
self.initial['date_ceased_to_act_formatted'] = self.instance.date_ceased_to_act_formatted
def save(self, commit = True):
# save logic for magic formatted fields
if self._raw_value('date_ceased_to_act_formatted') == Director.on_incorporation:
sval = Director.on_incorporation
else: sval = self.cleaned_data['date_ceased_to_act_formatted']
self.instance.date_ceased_to_act_formatted = sval
return super(forms.ModelForm, self).save(commit)
ModelForm
需要一个自定义字段来显示这个属性;还需要一个自定义的 __init__
方法来从属性中设置字段的初始值,以及一个自定义的保存方法,用来从表单字段设置模型属性。
在我的例子中,保存方法还需要考虑到一个特殊值,因为 DateField
是如何处理这个特殊值的。你也可以把这段代码放到一个自定义字段里。
最简单的方法是通过在 ModelAdmin
中定义一个自定义的回调函数来实现。假设这个字段叫做 my_datetime
:
from django.contrib import admin
from django.utils.formats import localize
class MyModelAdmin(admin.ModelAdmin):
readonly_fields = ('my_datetime_localized',)
def my_datetime_localized(self, obj):
return localize(obj.my_datetime)
my_datetime_localized.short_description = 'Date / time'
注意:如果 settings.USE_L10N
设置为 True
,那么这个日期时间会显示为查看者的本地时间,这通常是你想要的。如果你想把 USE_L10N
保持为 False
,那么你可以像这样覆盖它的行为: return localize(obj.my_datetime, use_l10n=True)
。
定义一个你的 Director
类的成员函数,让它按照你想要的方式显示只读字段。
class Director(models.Model, Specializable):
date_of_appointment = models.DateField()
date_ceased_to_act = models.DateField(blank=True,null=True)
def date_of_appointment_str(self):
if self.date_of_appointment == datetime.date(1,1,1):
return "On incorporation"
else:
return "%s" % (self.date_of_appointment) # format as you wish
然后只需将 'date_of_appointment_str'
添加到你在管理界面中的 readonly_fields
列表里。
补充说明:我应该提到这是一个快速的解决方案。一个更稳妥的办法是把 models.DateField
进行子类化,创建一个 MyCustomDateField
,它的功能和 DateField
一样,但当值为 date(1,1,1)
时,它会显示为“在公司成立时”,或者当用户保存“在公司成立时”时,它会把值保存为 date(1,1,1)
。这样可以确保你在这个字段类型出现的任何地方都能重复使用这个功能。不过,如果这个字段只在一个地方使用,这样做可能就有些多余了。
你可能需要一些类似的东西(这个还没测试过;你可能还需要额外修改你的表单中的 DateField
和/或其他东西;例如,如果你使用 django-south,你需要添加自定义的检查规则)。
class MyCustomDateField(models.DateField):
date_111_str = 'On incorporation'
def value_to_string(self, obj):
val = self._get_val_from_obj(obj)
if val is None:
data = ''
elif val.year == val.day == val.month == 1:
data = date_111_str
else:
data = datetime_safe.new_date(val).strftime("%Y-%m-%d")
return data
def get_prep_value(self, value):
if value == date_111_str:
value = datetime.date(1,1,1)
return super(MyCustomDateField,self).get_prep_value(self, value)