Skip to content

Commit

Permalink
Work on a version of this field that uses python timedeltas internall…
Browse files Browse the repository at this point in the history
…y to store interval
  • Loading branch information
poswald committed Mar 27, 2010
1 parent 1f9fa5a commit 892b6e9
Show file tree
Hide file tree
Showing 7 changed files with 181 additions and 44 deletions.
52 changes: 46 additions & 6 deletions README.rst
Expand Up @@ -32,8 +32,8 @@ and report back any issues.
Years and Months
----------------

You will need to uncomment two lines in timestring.py to support years and months.

You will need to uncomment two lines in timestring.py to support years and months. This causes a
loss of precision because the number of days in a month is not exact. This has not been extensively tested.

Usage
-----
Expand All @@ -47,24 +47,64 @@ In models.py::
duration = DurationField()
...

In your forms::

from durationfield.forms import DurationField as FDurationField
class MyForm(forms.ModelForm):
duration = FDurationField()

Note that database queries still need to treat the values as integers. If you are using things like
aggregates, you will need to explicitly convert them to timedeltas yourself:

timedelta(microseconds=list.aggregate(sum=Sum('duration'))['sum'])

You can also make a template filter to render out duration values. Create a file called duration.py::

myapp/templatetags/__init__.py
myapp/templatetags/duration.py

And put this code in it::

from django import template
from durationfield.utils.timestring import from_timedelta
register = template.Library()
def duration(value, arg=None):
if not value:
return u""
return u"%s" % from_timedelta(value)
register.filter('duration', duration)
Then in your HTML template::


{% load duration %}

....
<span>{{object.duration|duration}}</span>


Example
-------

Enter the time into the textbox in the following format::
1y 7m 6w 3d 18h 30min 23s 10ms 150us
6w 3d 18h 30min 23s 10ms 150us

This is interpreted as::
1 year 7 months 6 weeks 3 days 18 hours 30 minutes 23 seconds 1 milliseconds 150 microseconds
6 weeks 3 days 18 hours 30 minutes 23 seconds 1 milliseconds 150 microseconds

In your application it will be represented as a python datetime::
datetime.timedelta(624, 6155, 805126)
45 days, 18:30:23.010150

This will be stored into the database as a 'bigint' with the value of::
53919755833350
3954623010150


81 changes: 70 additions & 11 deletions durationfield/db/models/fields/duration.py
@@ -1,33 +1,92 @@
# -*- coding: utf-8 -*-
from datetime import timedelta
from django.core import exceptions
from django.db.models.fields import Field
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import smart_str, smart_unicode

from durationfield.utils.timestring import to_timedelta
from durationfield.forms.fields import DurationField as FDurationField
from django.db.models.fields import DecimalField

class DurationField(DecimalField):
class DurationField(Field):
"""
A duration field is used
"""
description = _("A duration of time")

default_error_messages = {
'invalid': _("This value must be in \"w d h min s ms us\" format."),
'unknown_type': _("The value's type could not be converted"),
}

description = "A duration of time"
__metaclass__ = models.SubfieldBase

def __init__(self, *args, **kwargs):
super(DurationField, self).__init__(*args, **kwargs)
self.max_digits, self.decimal_places = 20, 6
#self.max_digits, self.decimal_places = 20, 6

def get_internal_type(self):
return "IntegerField"
return "DurationField"

def db_type(self):
# Django 1.1.X does not support multiple db's and therefore does not
# call db_type passing in the connection string.
"""
Returns the database column data type for this field, for the provided connection.
Django 1.1.X does not support multiple db's and therefore does not pass in the db
connection string. Called by Django only when the framework constructs the table
"""
return "bigint"

def get_db_prep_save(self, value):
def get_db_prep_value(self, value):
"""
Returns field's value prepared for interacting with the database backend. In our case this is
an integer representing the number of microseconds elapsed.
"""
if value is None:
return None # db NULL
if isinstance(value, int) or isinstance(value, long):
value = timedelta(microseconds=value)
return value.days * 24 * 3600 * 1000000 + value.seconds * 1000000 + value.microseconds

def get_db_prep_save(self, value):
return self.get_db_prep_value(value)

def value_to_string(self, obj):
value = self._get_val_from_obj(obj)
return smart_unicode(from_timedelta(value))

def to_python(self, value):
return value
"""
Converts the input value into the timedelta Python data type, raising
django.core.exceptions.ValidationError if the data can't be converted.
Returns the converted value as a timedelta.
"""

# Note that value may be coming from the database column or a serializer so we should
# handle a timedelta, string or an integer
if value is None:
return value

if isinstance(value, timedelta):
return value

if isinstance(value, int) or isinstance(value, long):
return timedelta(microseconds=value)

# Try to parse the value
str = smart_str(value)
if isinstance(str, basestring):
try:
return to_timedelta(value)
except ValueError:
raise exceptions.ValidationError(self.default_error_messages['invalid'])

raise exceptions.ValidationError(self.default_error_messages['unknown_type'])

def formfield(self, **kwargs):
defaults = {'form_class': FDurationField}
defaults.update(kwargs)
return super(DurationField, self).formfield(**defaults)



def formfield(self, form_class=FDurationField, **kwargs):
return super(DurationField, self).formfield(form_class, **kwargs)
1 change: 1 addition & 0 deletions durationfield/forms/__init__.py
@@ -0,0 +1 @@
from fields import *
32 changes: 9 additions & 23 deletions durationfield/forms/fields.py
@@ -1,47 +1,33 @@
from django.forms.fields import Field
from django.core.exceptions import ValidationError
from durationfield.utils.timestring import to_timedelta
from django.forms import ValidationError
from django.utils.translation import ugettext_lazy as _
from widgets import DurationInput

from durationfield.forms.widgets import DurationInput
from durationfield.utils.timestring import to_timedelta

class DurationField(Field):
widget = DurationInput

default_error_messages = {
'invalid': u'Enter a valid duration.',
'max_value': _(u'Ensure this value is less than or equal to %(limit_value)s.'),
'min_value': _(u'Ensure this value is greater than or equal to %(limit_value)s.'),
'invalid': _('Enter a valid duration.'),
}

def __init__(self, min_value=None, max_value=None, *args, **kwargs):
def __init__(self, *args, **kwargs):
super(DurationField, self).__init__(*args, **kwargs)

if max_value is not None:
self.validators.append(validators.MaxValueValidator(max_value))
if min_value is not None:
self.validators.append(validators.MinValueValidator(min_value))

def clean(self, value):
"""
Validates max_value and min_value.
Returns a datetime.timedelta object.
"""
super(DurationField, self).clean(value)
try:
return to_timedelta(value)
except ValueError, e:
raise ValidationError(e)

if self.max_value is not None and value > self.max_value:
raise ValidationError(self.error_messages['max_value'] % {'max': self.max_value})

if self.min_value is not None and value < self.min_value:
raise ValidationError(self.error_messages['min_value'] % {'min': self.min_value})

return value
raise ValidationError(self.default_error_messages['invalid'])

def to_python(self, value):
try:
return to_timedelta(value)
except ValueError, e:
raise ValidationError(e)
raise ValidationError(self.default_error_messages['invalid'])

8 changes: 6 additions & 2 deletions durationfield/forms/widgets.py
Expand Up @@ -20,12 +20,16 @@

class DurationInput(TextInput):
def render(self, name, value, attrs=None):
if value is None: value = ''
if value is None: value = u''
final_attrs = self.build_attrs(attrs, type=self.input_type, name=name)
if value != '':
if value != u'':
# Only add the 'value' attribute if a value is non-empty.
if isinstance(value, timedelta):
value = from_timedelta(value)

if isinstance(value, int) or isinstance(value, long): # Database backends serving different types
value = from_timedelta(timedelta(microseconds=value))

final_attrs['value'] = force_unicode(formats.localize_input(value))
return mark_safe(u'<input%s />' % flatatt(final_attrs))

48 changes: 46 additions & 2 deletions durationfield/tests/tests.py
Expand Up @@ -8,8 +8,8 @@ class DurationFieldTests(TestCase):

def setUp(self):
self.test_data = (
[u"8h", 28800000000L],
[u"6w 3d 18h 30min 23s 10ms 150us", 3954623010150L],
[u"8h", timedelta(microseconds=28800000000L)],
[u"6w 3d 18h 30min 23s 10ms 150us", timedelta(microseconds=3954623010150L)],
)

self.month_year_test_data = (
Expand Down Expand Up @@ -55,6 +55,50 @@ def testDefaultValue(self):
self.assertEquals(None, model_test.duration_field)
model_test.delete()

def testApplicationType(self):
"""
Timedeltas should be returned to the applciation
"""
model_test = TestModel()
td = timedelta(microseconds=1234567890)
model_test.duration_field = td
model_test.save()
model_test = TestModel.objects.get(id__exact=model_test.id)
self.assertEquals(td, model_test.duration_field)
model_test.delete()

# Test with strings
model_test = TestModel()
td = "8d"
model_test.duration_field = td
model_test.save()
model_test = TestModel.objects.get(id__exact=model_test.id)
self.assertEquals(timedelta(days=8), model_test.duration_field)
model_test.delete()

# Test with long
model_test = TestModel()
td = 28800000000L
model_test.duration_field = td
model_test.save()
model_test = TestModel.objects.get(id__exact=model_test.id)
self.assertEquals(timedelta(microseconds=td), model_test.duration_field)
model_test.delete()

def testForm(self):
model_test = TestModel()
model_test.duration_field = timestring.to_timedelta("3d 8h 56s")
model_test.save()
model_test = TestModel.objects.get(id__exact=1)

#form = DurationField()
#self.assertContains("input", str)









Expand Down
3 changes: 3 additions & 0 deletions durationfield/utils/timestring.py
Expand Up @@ -23,6 +23,9 @@
))

def to_timedelta(value):
"""
returns a timedelta parsed from value
"""
chunks = []
for b in value.lower().split():
for index, char in enumerate(b):
Expand Down

0 comments on commit 892b6e9

Please sign in to comment.