Skip to content

Commit f94bcd9

Browse files
committed
Added a slightly tweaked version of timedelta, in order to support handling of TimedeltaFields.
- Legacy-Id: 9109
1 parent fed0e17 commit f94bcd9

11 files changed

Lines changed: 1011 additions & 0 deletions

File tree

timedelta/VERSION

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
0.7.3

timedelta/__init__.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import os
2+
3+
__version__ = open(os.path.join(os.path.dirname(__file__), "VERSION")).read().strip()
4+
5+
try:
6+
from django.core.exceptions import ImproperlyConfigured
7+
except ImportError:
8+
ImproperlyConfigured = ImportError
9+
10+
try:
11+
from .fields import TimedeltaField
12+
from .helpers import (
13+
divide, multiply, modulo,
14+
parse, nice_repr,
15+
percentage, decimal_percentage,
16+
total_seconds
17+
)
18+
except (ImportError, ImproperlyConfigured):
19+
pass

timedelta/fields.py

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
from django.db import models
2+
from django.core.exceptions import ValidationError
3+
from django.utils import six
4+
5+
from collections import defaultdict
6+
import datetime
7+
import six
8+
9+
from .helpers import parse
10+
from .forms import TimedeltaFormField
11+
12+
# TODO: Figure out why django admin thinks fields of this type have changed every time an object is saved.
13+
14+
# Define the different column types that different databases can use.
15+
COLUMN_TYPES = defaultdict(lambda:"char(20)")
16+
COLUMN_TYPES["django.db.backends.postgresql_psycopg2"] = "interval"
17+
COLUMN_TYPES["django.contrib.gis.db.backends.postgis"] = "interval"
18+
19+
class TimedeltaField(six.with_metaclass(models.SubfieldBase, models.Field)):
20+
"""
21+
Store a datetime.timedelta as an INTERVAL in postgres, or a
22+
CHAR(20) in other database backends.
23+
"""
24+
_south_introspects = True
25+
26+
description = "A datetime.timedelta object"
27+
28+
def __init__(self, *args, **kwargs):
29+
self._min_value = kwargs.pop('min_value', None)
30+
31+
if isinstance(self._min_value, (int, float)):
32+
self._min_value = datetime.timedelta(seconds=self._min_value)
33+
34+
self._max_value = kwargs.pop('max_value', None)
35+
36+
if isinstance(self._max_value, (int, float)):
37+
self._max_value = datetime.timedelta(seconds=self._max_value)
38+
39+
super(TimedeltaField, self).__init__(*args, **kwargs)
40+
41+
def to_python(self, value):
42+
if (value is None) or isinstance(value, datetime.timedelta):
43+
return value
44+
if isinstance(value, (int, float)):
45+
return datetime.timedelta(seconds=value)
46+
if isinstance(value, six.string_types) and value.replace('.','0').isdigit():
47+
return datetime.timedelta(seconds=float(value))
48+
if value == "":
49+
if self.null:
50+
return None
51+
else:
52+
return datetime.timedelta(0)
53+
return parse(value)
54+
55+
def get_prep_value(self, value):
56+
if self.null and value == "":
57+
return None
58+
if (value is None) or isinstance(value, six.string_types):
59+
return value
60+
return str(value).replace(',', '')
61+
62+
def get_db_prep_value(self, value, connection=None, prepared=None):
63+
return self.get_prep_value(value)
64+
65+
def formfield(self, *args, **kwargs):
66+
defaults = {'form_class':TimedeltaFormField}
67+
defaults.update(kwargs)
68+
return super(TimedeltaField, self).formfield(*args, **defaults)
69+
70+
def validate(self, value, model_instance):
71+
super(TimedeltaField, self).validate(value, model_instance)
72+
if self._min_value is not None:
73+
if self._min_value > value:
74+
raise ValidationError('Less than minimum allowed value')
75+
if self._max_value is not None:
76+
if self._max_value < value:
77+
raise ValidationError('More than maximum allowed value')
78+
79+
def value_to_string(self, obj):
80+
value = self._get_val_from_obj(obj)
81+
return unicode(value)
82+
83+
def get_default(self):
84+
"""
85+
Needed to rewrite this, as the parent class turns this value into a
86+
unicode string. That sux pretty deep.
87+
"""
88+
if self.has_default():
89+
if callable(self.default):
90+
return self.default()
91+
return self.get_prep_value(self.default)
92+
if not self.empty_strings_allowed or (self.null):
93+
return None
94+
return ""
95+
96+
def db_type(self, connection):
97+
return COLUMN_TYPES[connection.settings_dict['ENGINE']]
98+
99+
def deconstruct(self):
100+
"""
101+
Break down this field into arguments that can be used to reproduce it
102+
with Django migrations.
103+
104+
The thing to to note here is that currently the migration file writer
105+
can't serialize timedelta objects so we convert them to a float
106+
representation (in seconds) that we can later interpret as a timedelta.
107+
"""
108+
109+
name, path, args, kwargs = super(TimedeltaField, self).deconstruct()
110+
111+
if isinstance(self._min_value, datetime.timedelta):
112+
kwargs['min_value'] = self._min_value.total_seconds()
113+
114+
if isinstance(self._max_value, datetime.timedelta):
115+
kwargs['max_value'] = self._max_value.total_seconds()
116+
117+
if isinstance(kwargs.get('default'), datetime.timedelta):
118+
kwargs['default'] = kwargs['default'].total_seconds()
119+
120+
return name, path, args, kwargs

timedelta/forms.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
from django import forms
2+
from django.utils.translation import ugettext_lazy as _
3+
from django.utils import six
4+
5+
import datetime
6+
from collections import defaultdict
7+
8+
from .widgets import TimedeltaWidget
9+
from .helpers import parse
10+
11+
class TimedeltaFormField(forms.Field):
12+
13+
default_error_messages = {
14+
'invalid':_('Enter a valid time span: e.g. "3 days, 4 hours, 2 minutes"')
15+
}
16+
17+
def __init__(self, *args, **kwargs):
18+
defaults = {'widget':TimedeltaWidget}
19+
defaults.update(kwargs)
20+
super(TimedeltaFormField, self).__init__(*args, **defaults)
21+
22+
def clean(self, value):
23+
"""
24+
This doesn't really need to be here: it should be tested in
25+
parse()...
26+
27+
>>> t = TimedeltaFormField()
28+
>>> t.clean('1 day')
29+
datetime.timedelta(1)
30+
>>> t.clean('1 day, 0:00:00')
31+
datetime.timedelta(1)
32+
>>> t.clean('1 day, 8:42:42.342')
33+
datetime.timedelta(1, 31362, 342000)
34+
>>> t.clean('3 days, 8:42:42.342161')
35+
datetime.timedelta(3, 31362, 342161)
36+
>>> try:
37+
... t.clean('3 days, 8:42:42.3.42161')
38+
... except forms.ValidationError as arg:
39+
... six.print_(arg.messages[0])
40+
Enter a valid time span: e.g. "3 days, 4 hours, 2 minutes"
41+
>>> t.clean('5 day, 8:42:42')
42+
datetime.timedelta(5, 31362)
43+
>>> t.clean('1 days')
44+
datetime.timedelta(1)
45+
>>> t.clean('1 second')
46+
datetime.timedelta(0, 1)
47+
>>> t.clean('1 sec')
48+
datetime.timedelta(0, 1)
49+
>>> t.clean('10 seconds')
50+
datetime.timedelta(0, 10)
51+
>>> t.clean('30 seconds')
52+
datetime.timedelta(0, 30)
53+
>>> t.clean('1 minute, 30 seconds')
54+
datetime.timedelta(0, 90)
55+
>>> t.clean('2.5 minutes')
56+
datetime.timedelta(0, 150)
57+
>>> t.clean('2 minutes, 30 seconds')
58+
datetime.timedelta(0, 150)
59+
>>> t.clean('.5 hours')
60+
datetime.timedelta(0, 1800)
61+
>>> t.clean('30 minutes')
62+
datetime.timedelta(0, 1800)
63+
>>> t.clean('1 hour')
64+
datetime.timedelta(0, 3600)
65+
>>> t.clean('5.5 hours')
66+
datetime.timedelta(0, 19800)
67+
>>> t.clean('1 day, 1 hour, 30 mins')
68+
datetime.timedelta(1, 5400)
69+
>>> t.clean('8 min')
70+
datetime.timedelta(0, 480)
71+
>>> t.clean('3 days, 12 hours')
72+
datetime.timedelta(3, 43200)
73+
>>> t.clean('3.5 day')
74+
datetime.timedelta(3, 43200)
75+
>>> t.clean('1 week')
76+
datetime.timedelta(7)
77+
>>> t.clean('2 weeks, 2 days')
78+
datetime.timedelta(16)
79+
>>> try:
80+
... t.clean(six.u('2 we\xe8k, 2 days'))
81+
... except forms.ValidationError as arg:
82+
... six.print_(arg.messages[0])
83+
Enter a valid time span: e.g. "3 days, 4 hours, 2 minutes"
84+
"""
85+
86+
super(TimedeltaFormField, self).clean(value)
87+
if value == '' and not self.required:
88+
return ''
89+
try:
90+
return parse(value)
91+
except TypeError:
92+
raise forms.ValidationError(self.error_messages['invalid'])
93+
94+
class TimedeltaChoicesField(TimedeltaFormField):
95+
def __init__(self, *args, **kwargs):
96+
choices = kwargs.pop('choices')
97+
defaults = {'widget':forms.Select(choices=choices)}
98+
defaults.update(kwargs)
99+
super(TimedeltaChoicesField, self).__init__(*args, **defaults)

0 commit comments

Comments
 (0)