I have a model, Director
with two DateFields, and two subclasses (code below). I am trying to create an admin page for each Director which shows the corresponding subclass instance, and not the Director
instance; this part is mostly easy (I create an inline for each subclass, give the main ModelAdmin a form with all fields excluded, and have the main ModelAdmin only request formsets from the inlines which have a corresponding instance - the code; there is an unresolved issue with this approach, which I note below, but is not the focus of this question).
The problem I have is that I want to massage the values displayed to the user, one of which is shown in a readonly field, one of which is not. The processing is that I want to change a magic value (date(1,1,1)
) to the string "On incorporation"
.
Dates in readonly fields aren't rendered in a format very friendly to parsing, and I would like to reduce unnecessary dependence on javascript, so I would very much prefer a server-side solution.
The code below displays the forms as I want them, except that date values are not massaged at all, and when saving, there is a spurious "Please correct the error below" message, even though there are no errors, and all fields are saved correctly.
My question is: how do I intercept the values to be rendered on the page, both in readonly fields, and in forms fields, and alter them to display a string of my choosing?
The models (so far as material):
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
The admin code:
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))
I realise that there is an asymmetry between DirectorsCorporateInline
and DirectorsIndividualInline
; that is because I am testing on an instance with a DirectorsIndividual
instance. The code above refers to model fields not shown in the models, because they are not material to the dates issue; it should be possible to render them immaterial for the spurious error issue without altering those fields (although I realise it is less helpful for that issue, I want to keep this question mostly focused on one issue). EnhancedAdmin
is a ModelAdmin
subclass with some minor alterations which shouldn't be of consequence. Extra code can be shown on reasoned request, but I don't want to confuse with irrelevant code.
For completeness: I am using django 1.3.1 on python 2.7.2.
I would massage field values with javascript. You can override the admin templates, and attach your javascript code into the
{% block extrahead %}
block ( some more info from the django book). Put your magic massage function example into.ready()
(if you use jQuery).I hope this will work for you, because I would like to do something similar, but haven't implemented yet. :)
Define a member function of your
Director
class that renders the readonly_field as you want.and then just add
'date_of_appointment_str'
to your list ofreadonly_fields
in the admin.EDIT: I should add that this is one quick solution. A more robust solution is to subclass
models.DateField
into aMyCustomDateField
that acts like aDateField
except that when the value isdate(1,1,1)
it renders as "On incorporation" or when a user saves "On incorporation" it saves the value asdate(1,1,1)
. This would ensure that you can reuse this functionality everywhere this field type shows up. However, if it only shows up in one place; this may be overkill.You'd need something like (this is untested; you may need to additionally alter your the forms
DateField
and/or other things; e.g., if you use django-south you'll have to add custom introspection rules).The easiest way is to do it by defining a custom callback in the
ModelAdmin
. Let's say the field is calledmy_datetime
:Note: if
settings.USE_L10N
isTrue
, this will display the datetime in the local time of the viewer, which is probably what you want. If you want to keepUSE_L10N
asFalse
then you can override its behaviour like so:return localize(obj.my_datetime, use_l10n=True)
.As @drjimbob (and carljm on #django) suggested, the solution is to create a member function or property on the model, e.g.:
Note the
date_of_appointment_formatted.short_description
- theModelAdmin
will use theshort_description
as the label for areadonly_field
.To get the properties working with model fields, a custom form is needed:
The
ModelForm
needs a custom field to display the property; a custom__init__
to set the initial value for the field from the property, and a custom save, to set the model property from the form field.In my example, the save also has to be aware of the magic value, because of how
DateField
handles the magic value. You could push that code into a custom field instead.