I am trying to create a Django model field that represents a duration with days, hours, minutes and seconds text input fields in the HTML, and stores the duration in the db using the ical format (RFC5545).
(this is related to my question on How to create an ical duration field in Django?)
Here is my approach:
Thanks bakkal and Pol. Below is what I came up with.
from django.db import models
from icalendar.prop import vDuration
from django.forms.widgets import MultiWidget
from django.forms import TextInput, IntegerField
from django.forms.util import flatatt
from django.forms.fields import MultiValueField
from django.utils.encoding import force_unicode
from django.utils.safestring import mark_safe
from django.utils.text import capfirst
from django.utils.translation import ugettext_lazy as _
from django.core import validators
from datetime import timedelta
def is_int(s):
try:
int(s)
return True
except ValueError:
return False
class Widget_LabelInputField(TextInput):
"""
Input widget with label
"""
input_type="numbers"
def __init__(self, labelCaption, attrs=None):
self.labelCaption = labelCaption
super(Widget_LabelInputField, self).__init__(attrs)
def _format_value(self, value):
if is_int(value):
return value
return '0'
def render(self, name, value, attrs=None):
if value is None:
value = '0'
final_attrs = self.build_attrs(attrs, type=self.input_type, name=name)
if value != '':
# Only add the 'value' attribute if a value is non-empty.
final_attrs['value'] = force_unicode(self._format_value(value))
if (self.labelCaption):
typeString = self.labelCaption + ': '
else:
typeString = ''
return mark_safe(u'' + typeString + '<input%s style=\'width: 30px; margin-right: 20px\'/>' % flatatt(final_attrs))
class Widget_DurationField(MultiWidget):
"""
A Widget that splits duration input into two <input type="text"> boxes.
"""
def __init__(self, attrs=None):
widgets = (Widget_LabelInputField(labelCaption='days', attrs=attrs),
Widget_LabelInputField(labelCaption='hours', attrs=attrs),
Widget_LabelInputField(labelCaption='minutes', attrs=attrs),
Widget_LabelInputField(labelCaption='seconds', attrs=attrs)
)
super(Widget_DurationField, self).__init__(widgets, attrs)
def decompress(self, value):
if value:
duration = vDuration.from_ical(value)
return [str(duration.days), str(duration.seconds // 3600), str(duration.seconds % 3600 // 60), str(duration.seconds % 60)]
return [None, None, None, None]
class Forms_DurationField(MultiValueField):
widget = Widget_DurationField
default_error_messages = {
'invalid_day': _(u'Enter a valid day.'),
'invalid_hour': _(u'Enter a valid hour.'),
'invalid_minute': _(u'Enter a valid minute.'),
'invalid_second': _(u'Enter a valid second.')
}
def __init__(self, *args, **kwargs):
errors = self.default_error_messages.copy()
if 'error_messages' in kwargs:
errors.update(kwargs['error_messages'])
fields = (
IntegerField(min_value=-9999, max_value=9999,
error_messages={'invalid': errors['invalid_day']},),
IntegerField(min_value=-9999, max_value=9999,
error_messages={'invalid': errors['invalid_hour']},),
IntegerField(min_value=-9999, max_value=9999,
error_messages={'invalid': errors['invalid_minute']},),
IntegerField(min_value=-9999, max_value=9999,
error_messages={'invalid': errors['invalid_second']},),
)
super(Forms_DurationField, self).__init__(fields, *args, **kwargs)
def compress(self, data_list):
if data_list:
if data_list[0] in validators.EMPTY_VALUES:
raise ValidationError(self.error_messages['invalid_day'])
if data_list[1] in validators.EMPTY_VALUES:
raise ValidationError(self.error_messages['invalid_hour'])
if data_list[2] in validators.EMPTY_VALUES:
raise ValidationError(self.error_messages['invalid_minute'])
if data_list[3] in validators.EMPTY_VALUES:
raise ValidationError(self.error_messages['invalid_second'])
return vDuration(timedelta(days=data_list[0],hours=data_list[1],minutes=data_list[2],seconds=data_list[3]))
return None
class Model_DurationField(models.Field):
description = "Duration"
def __init__(self, *args, **kwargs):
super(Model_DurationField, self).__init__(*args, **kwargs)
def db_type(self, connection):
return 'varchar(255)'
def get_internal_type(self):
return "Model_DurationField"
def to_python(self, value):
if isinstance(value, vDuration) or value is None:
return value
return vDuration.from_ical(value)
def get_prep_value(self, value):
return value.to_ical()
def formfield(self, **kwargs):
defaults = {
'form_class': Forms_DurationField,
'required': not self.blank,
'label': capfirst(self.verbose_name),
'help_text': self.help_text}
defaults.update(kwargs)
return super(Model_DurationField, self).formfield(**defaults)
It works in the following Model:
class TestModel(models.Model):
ID = models.CharField(max_length=255)
start = models.DateTimeField(null=True)
#duration = models.CharField(max_length=255,null=True) commented out
otherDuration = duration.Model_DurationField(null=True)
but not in this one:
class TestModel(models.Model):
ID = models.CharField(max_length=255)
start = models.DateTimeField(null=True)
duration = models.CharField(max_length=255,null=True) # not commented out
otherDuration = duration.Model_DurationField(null=True)
I get the following error:
File "/somepath/models.py", line 5, in TestModel
otherDuration = duration.Model_DurationField(null=True)
AttributeError: 'CharField' object has no attribute 'Model_DurationField'
That puzzles me... it seems that python considers my field to be an attribute of the previous field, but only if it is a CharField. Any ideas?
I was stupid. The problem was that I named the file where the model was defined duration.py, so there was a naming conflict with the "duration" field. I renamed the file and it worked.