Reputation: 4369
My model has a DurationField which is editable in Django Admin. I don't like how Django (inheriting its behavior from Python) displays negative durations, so I've tried to monkey patch it:
test = lambda: duration.duration_string(datetime.timedelta(seconds=-5)) == \
'-00:00:05'
if not test():
_duration_string = duration.duration_string
def duration_string(duration):
if duration.days < 0:
return '-' + _duration_string(-duration)
return _duration_string(duration)
duration.duration_string = duration_string
assert test()
This code gets run as part of my AppConfig.ready()
method.
However, in Admin, the field still displays values formatted the default way. Is there some other way to customize how a DurationField's value is rendered in Admin?
At @Mehak's suggestion I tried the solution in this question. In fact, I tried just making a custom field that just bombs the program:
class CustomDurationField(models.DurationField):
def value_to_string(self, obj):
raise Exception()
def __str__(self):
raise Exception()
No exception is raised when viewing or editing the field, after making and applying the migration of course.
Upvotes: 1
Views: 667
Reputation: 395
Your answer also provides for customizing the input value (what's going in as the value). For example, if you wanted the input to be minutes instead of seconds, you could do:
def prepare_value(self, value):
if isinstance(value, datetime.timedelta):
val = value * 60
return val
In order to customize how the output is formatted for the post record in the admin, I did some interesting monkeypatching in admin.py - the reason being that duration_string had no formatting effect after deployment.
@admin.register(Post) # "Post" is the name of my database model
class PostAdmin(admin.ModelAdmin):
exclude = ('database_field') # hide this existing database field by excluding it, what you named the field replaces "database_field"
readonly_fields = ('database_field_readonly') # display new field as readonly
@admin.display(description="Estimated completion time")
def database_field_readonly(self, obj):
str_output = str(obj.database_field) # replace "database_field" with real existing database field name
str_output_list = str_output.split(':')
modify_hour_string = str_output_list[0] # this portion of text will include hours and any days
if(len(modify_hour_string) == 1): # if there were any days, the length of this portion will be greater than one
hour_num = modify_hour_string[0] # there were no days, we take the charAt 0 to get hours
else:
hour_num = modify_hour_string[len(modify_hour_string)-1] #capture the hour number (the number to the left of the colon which is len(string) - 1)
if hour_num == "1": #if hours is one, print "hour"
str_choice_hr = " hour "
else:
str_choice_hr = " hours "
if str_output_list[1] == "01": #if minutes is one, print "minute"
str_choice_min = " minute"
else:
str_choice_min = " minutes"
formatted_str = str_output_list[0] + str_choice_hr + str_output_list[1] + str_choice_min # any days will be automatically shown because of the ISO time format
return formatted_str
Any days, minutes, and hours will now be outputted like: "5 days, 2 hours 10 minutes"
Upvotes: 1
Reputation: 4369
This answer led me on the right track. After performing the monkey patch, I had to define a custom model field that uses a custom form field...
import datetime
from django import forms
from django.db import models
from django.utils import duration
class CustomDurationFormField(forms.DurationField):
def prepare_value(self, value):
if isinstance(value, datetime.timedelta):
return duration.duration_string(value)
return value
class CustomDurationField(models.DurationField):
def formfield(self, **kwargs):
return super().formfield(**{
'form_class': CustomDurationFormField,
**kwargs,
})
If you don't want to monkey patch Django's django.utils.duration.duration_string
, then you would just change CustomDurationFormField.prepare_value
to call a separately defined version of it.
I'm not entirely sure why it requires so much effort to do this, but there it is.
Upvotes: 2