rgov
rgov

Reputation: 4369

Overriding Django's DurationField display

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

Answers (2)

Conor
Conor

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

rgov
rgov

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

Related Questions