Added a slightly tweaked version of timedelta, in order to support handling of TimedeltaFields.
- Legacy-Id: 9109
This commit is contained in:
parent
fed0e17ac1
commit
f94bcd93d7
1
timedelta/VERSION
Normal file
1
timedelta/VERSION
Normal file
|
@ -0,0 +1 @@
|
||||||
|
0.7.3
|
19
timedelta/__init__.py
Normal file
19
timedelta/__init__.py
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import os
|
||||||
|
|
||||||
|
__version__ = open(os.path.join(os.path.dirname(__file__), "VERSION")).read().strip()
|
||||||
|
|
||||||
|
try:
|
||||||
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
except ImportError:
|
||||||
|
ImproperlyConfigured = ImportError
|
||||||
|
|
||||||
|
try:
|
||||||
|
from .fields import TimedeltaField
|
||||||
|
from .helpers import (
|
||||||
|
divide, multiply, modulo,
|
||||||
|
parse, nice_repr,
|
||||||
|
percentage, decimal_percentage,
|
||||||
|
total_seconds
|
||||||
|
)
|
||||||
|
except (ImportError, ImproperlyConfigured):
|
||||||
|
pass
|
120
timedelta/fields.py
Normal file
120
timedelta/fields.py
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
from django.db import models
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.utils import six
|
||||||
|
|
||||||
|
from collections import defaultdict
|
||||||
|
import datetime
|
||||||
|
import six
|
||||||
|
|
||||||
|
from .helpers import parse
|
||||||
|
from .forms import TimedeltaFormField
|
||||||
|
|
||||||
|
# TODO: Figure out why django admin thinks fields of this type have changed every time an object is saved.
|
||||||
|
|
||||||
|
# Define the different column types that different databases can use.
|
||||||
|
COLUMN_TYPES = defaultdict(lambda:"char(20)")
|
||||||
|
COLUMN_TYPES["django.db.backends.postgresql_psycopg2"] = "interval"
|
||||||
|
COLUMN_TYPES["django.contrib.gis.db.backends.postgis"] = "interval"
|
||||||
|
|
||||||
|
class TimedeltaField(six.with_metaclass(models.SubfieldBase, models.Field)):
|
||||||
|
"""
|
||||||
|
Store a datetime.timedelta as an INTERVAL in postgres, or a
|
||||||
|
CHAR(20) in other database backends.
|
||||||
|
"""
|
||||||
|
_south_introspects = True
|
||||||
|
|
||||||
|
description = "A datetime.timedelta object"
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self._min_value = kwargs.pop('min_value', None)
|
||||||
|
|
||||||
|
if isinstance(self._min_value, (int, float)):
|
||||||
|
self._min_value = datetime.timedelta(seconds=self._min_value)
|
||||||
|
|
||||||
|
self._max_value = kwargs.pop('max_value', None)
|
||||||
|
|
||||||
|
if isinstance(self._max_value, (int, float)):
|
||||||
|
self._max_value = datetime.timedelta(seconds=self._max_value)
|
||||||
|
|
||||||
|
super(TimedeltaField, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def to_python(self, value):
|
||||||
|
if (value is None) or isinstance(value, datetime.timedelta):
|
||||||
|
return value
|
||||||
|
if isinstance(value, (int, float)):
|
||||||
|
return datetime.timedelta(seconds=value)
|
||||||
|
if isinstance(value, six.string_types) and value.replace('.','0').isdigit():
|
||||||
|
return datetime.timedelta(seconds=float(value))
|
||||||
|
if value == "":
|
||||||
|
if self.null:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return datetime.timedelta(0)
|
||||||
|
return parse(value)
|
||||||
|
|
||||||
|
def get_prep_value(self, value):
|
||||||
|
if self.null and value == "":
|
||||||
|
return None
|
||||||
|
if (value is None) or isinstance(value, six.string_types):
|
||||||
|
return value
|
||||||
|
return str(value).replace(',', '')
|
||||||
|
|
||||||
|
def get_db_prep_value(self, value, connection=None, prepared=None):
|
||||||
|
return self.get_prep_value(value)
|
||||||
|
|
||||||
|
def formfield(self, *args, **kwargs):
|
||||||
|
defaults = {'form_class':TimedeltaFormField}
|
||||||
|
defaults.update(kwargs)
|
||||||
|
return super(TimedeltaField, self).formfield(*args, **defaults)
|
||||||
|
|
||||||
|
def validate(self, value, model_instance):
|
||||||
|
super(TimedeltaField, self).validate(value, model_instance)
|
||||||
|
if self._min_value is not None:
|
||||||
|
if self._min_value > value:
|
||||||
|
raise ValidationError('Less than minimum allowed value')
|
||||||
|
if self._max_value is not None:
|
||||||
|
if self._max_value < value:
|
||||||
|
raise ValidationError('More than maximum allowed value')
|
||||||
|
|
||||||
|
def value_to_string(self, obj):
|
||||||
|
value = self._get_val_from_obj(obj)
|
||||||
|
return unicode(value)
|
||||||
|
|
||||||
|
def get_default(self):
|
||||||
|
"""
|
||||||
|
Needed to rewrite this, as the parent class turns this value into a
|
||||||
|
unicode string. That sux pretty deep.
|
||||||
|
"""
|
||||||
|
if self.has_default():
|
||||||
|
if callable(self.default):
|
||||||
|
return self.default()
|
||||||
|
return self.get_prep_value(self.default)
|
||||||
|
if not self.empty_strings_allowed or (self.null):
|
||||||
|
return None
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def db_type(self, connection):
|
||||||
|
return COLUMN_TYPES[connection.settings_dict['ENGINE']]
|
||||||
|
|
||||||
|
def deconstruct(self):
|
||||||
|
"""
|
||||||
|
Break down this field into arguments that can be used to reproduce it
|
||||||
|
with Django migrations.
|
||||||
|
|
||||||
|
The thing to to note here is that currently the migration file writer
|
||||||
|
can't serialize timedelta objects so we convert them to a float
|
||||||
|
representation (in seconds) that we can later interpret as a timedelta.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name, path, args, kwargs = super(TimedeltaField, self).deconstruct()
|
||||||
|
|
||||||
|
if isinstance(self._min_value, datetime.timedelta):
|
||||||
|
kwargs['min_value'] = self._min_value.total_seconds()
|
||||||
|
|
||||||
|
if isinstance(self._max_value, datetime.timedelta):
|
||||||
|
kwargs['max_value'] = self._max_value.total_seconds()
|
||||||
|
|
||||||
|
if isinstance(kwargs.get('default'), datetime.timedelta):
|
||||||
|
kwargs['default'] = kwargs['default'].total_seconds()
|
||||||
|
|
||||||
|
return name, path, args, kwargs
|
99
timedelta/forms.py
Normal file
99
timedelta/forms.py
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
from django import forms
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from django.utils import six
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
from .widgets import TimedeltaWidget
|
||||||
|
from .helpers import parse
|
||||||
|
|
||||||
|
class TimedeltaFormField(forms.Field):
|
||||||
|
|
||||||
|
default_error_messages = {
|
||||||
|
'invalid':_('Enter a valid time span: e.g. "3 days, 4 hours, 2 minutes"')
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
defaults = {'widget':TimedeltaWidget}
|
||||||
|
defaults.update(kwargs)
|
||||||
|
super(TimedeltaFormField, self).__init__(*args, **defaults)
|
||||||
|
|
||||||
|
def clean(self, value):
|
||||||
|
"""
|
||||||
|
This doesn't really need to be here: it should be tested in
|
||||||
|
parse()...
|
||||||
|
|
||||||
|
>>> t = TimedeltaFormField()
|
||||||
|
>>> t.clean('1 day')
|
||||||
|
datetime.timedelta(1)
|
||||||
|
>>> t.clean('1 day, 0:00:00')
|
||||||
|
datetime.timedelta(1)
|
||||||
|
>>> t.clean('1 day, 8:42:42.342')
|
||||||
|
datetime.timedelta(1, 31362, 342000)
|
||||||
|
>>> t.clean('3 days, 8:42:42.342161')
|
||||||
|
datetime.timedelta(3, 31362, 342161)
|
||||||
|
>>> try:
|
||||||
|
... t.clean('3 days, 8:42:42.3.42161')
|
||||||
|
... except forms.ValidationError as arg:
|
||||||
|
... six.print_(arg.messages[0])
|
||||||
|
Enter a valid time span: e.g. "3 days, 4 hours, 2 minutes"
|
||||||
|
>>> t.clean('5 day, 8:42:42')
|
||||||
|
datetime.timedelta(5, 31362)
|
||||||
|
>>> t.clean('1 days')
|
||||||
|
datetime.timedelta(1)
|
||||||
|
>>> t.clean('1 second')
|
||||||
|
datetime.timedelta(0, 1)
|
||||||
|
>>> t.clean('1 sec')
|
||||||
|
datetime.timedelta(0, 1)
|
||||||
|
>>> t.clean('10 seconds')
|
||||||
|
datetime.timedelta(0, 10)
|
||||||
|
>>> t.clean('30 seconds')
|
||||||
|
datetime.timedelta(0, 30)
|
||||||
|
>>> t.clean('1 minute, 30 seconds')
|
||||||
|
datetime.timedelta(0, 90)
|
||||||
|
>>> t.clean('2.5 minutes')
|
||||||
|
datetime.timedelta(0, 150)
|
||||||
|
>>> t.clean('2 minutes, 30 seconds')
|
||||||
|
datetime.timedelta(0, 150)
|
||||||
|
>>> t.clean('.5 hours')
|
||||||
|
datetime.timedelta(0, 1800)
|
||||||
|
>>> t.clean('30 minutes')
|
||||||
|
datetime.timedelta(0, 1800)
|
||||||
|
>>> t.clean('1 hour')
|
||||||
|
datetime.timedelta(0, 3600)
|
||||||
|
>>> t.clean('5.5 hours')
|
||||||
|
datetime.timedelta(0, 19800)
|
||||||
|
>>> t.clean('1 day, 1 hour, 30 mins')
|
||||||
|
datetime.timedelta(1, 5400)
|
||||||
|
>>> t.clean('8 min')
|
||||||
|
datetime.timedelta(0, 480)
|
||||||
|
>>> t.clean('3 days, 12 hours')
|
||||||
|
datetime.timedelta(3, 43200)
|
||||||
|
>>> t.clean('3.5 day')
|
||||||
|
datetime.timedelta(3, 43200)
|
||||||
|
>>> t.clean('1 week')
|
||||||
|
datetime.timedelta(7)
|
||||||
|
>>> t.clean('2 weeks, 2 days')
|
||||||
|
datetime.timedelta(16)
|
||||||
|
>>> try:
|
||||||
|
... t.clean(six.u('2 we\xe8k, 2 days'))
|
||||||
|
... except forms.ValidationError as arg:
|
||||||
|
... six.print_(arg.messages[0])
|
||||||
|
Enter a valid time span: e.g. "3 days, 4 hours, 2 minutes"
|
||||||
|
"""
|
||||||
|
|
||||||
|
super(TimedeltaFormField, self).clean(value)
|
||||||
|
if value == '' and not self.required:
|
||||||
|
return ''
|
||||||
|
try:
|
||||||
|
return parse(value)
|
||||||
|
except TypeError:
|
||||||
|
raise forms.ValidationError(self.error_messages['invalid'])
|
||||||
|
|
||||||
|
class TimedeltaChoicesField(TimedeltaFormField):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
choices = kwargs.pop('choices')
|
||||||
|
defaults = {'widget':forms.Select(choices=choices)}
|
||||||
|
defaults.update(kwargs)
|
||||||
|
super(TimedeltaChoicesField, self).__init__(*args, **defaults)
|
554
timedelta/helpers.py
Normal file
554
timedelta/helpers.py
Normal file
|
@ -0,0 +1,554 @@
|
||||||
|
from __future__ import division
|
||||||
|
|
||||||
|
import re
|
||||||
|
import datetime
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from django.utils import six
|
||||||
|
|
||||||
|
STRFDATETIME = re.compile('([dgGhHis])')
|
||||||
|
STRFDATETIME_REPL = lambda x: '%%(%s)s' % x.group()
|
||||||
|
|
||||||
|
def nice_repr(timedelta, display="long", sep=", "):
|
||||||
|
"""
|
||||||
|
Turns a datetime.timedelta object into a nice string repr.
|
||||||
|
|
||||||
|
display can be "sql", "iso8601", "minimal", "short" or "long" [default].
|
||||||
|
|
||||||
|
>>> from datetime import timedelta as td
|
||||||
|
>>> nice_repr(td(days=1, hours=2, minutes=3, seconds=4))
|
||||||
|
'1 day, 2 hours, 3 minutes, 4 seconds'
|
||||||
|
>>> nice_repr(td(days=1, seconds=1), "minimal")
|
||||||
|
'1d, 1s'
|
||||||
|
>>> nice_repr(datetime.timedelta(days=1))
|
||||||
|
'1 day'
|
||||||
|
>>> nice_repr(datetime.timedelta(days=0))
|
||||||
|
'0 seconds'
|
||||||
|
>>> nice_repr(datetime.timedelta(seconds=1))
|
||||||
|
'1 second'
|
||||||
|
>>> nice_repr(datetime.timedelta(seconds=10))
|
||||||
|
'10 seconds'
|
||||||
|
>>> nice_repr(datetime.timedelta(seconds=30))
|
||||||
|
'30 seconds'
|
||||||
|
>>> nice_repr(datetime.timedelta(seconds=60))
|
||||||
|
'1 minute'
|
||||||
|
>>> nice_repr(datetime.timedelta(seconds=150))
|
||||||
|
'2 minutes, 30 seconds'
|
||||||
|
>>> nice_repr(datetime.timedelta(seconds=1800))
|
||||||
|
'30 minutes'
|
||||||
|
>>> nice_repr(datetime.timedelta(seconds=3600))
|
||||||
|
'1 hour'
|
||||||
|
>>> nice_repr(datetime.timedelta(seconds=3601))
|
||||||
|
'1 hour, 1 second'
|
||||||
|
>>> nice_repr(datetime.timedelta(seconds=19800))
|
||||||
|
'5 hours, 30 minutes'
|
||||||
|
>>> nice_repr(datetime.timedelta(seconds=91800))
|
||||||
|
'1 day, 1 hour, 30 minutes'
|
||||||
|
>>> nice_repr(datetime.timedelta(seconds=302400))
|
||||||
|
'3 days, 12 hours'
|
||||||
|
|
||||||
|
Tests for handling zero:
|
||||||
|
>>> nice_repr(td(seconds=0), 'minimal')
|
||||||
|
'0s'
|
||||||
|
>>> nice_repr(td(seconds=0), 'short')
|
||||||
|
'0 sec'
|
||||||
|
>>> nice_repr(td(seconds=0), 'long')
|
||||||
|
'0 seconds'
|
||||||
|
"""
|
||||||
|
|
||||||
|
assert isinstance(timedelta, datetime.timedelta), "First argument must be a timedelta."
|
||||||
|
|
||||||
|
result = []
|
||||||
|
|
||||||
|
weeks = int(timedelta.days / 7)
|
||||||
|
days = timedelta.days % 7
|
||||||
|
hours = int(timedelta.seconds / 3600)
|
||||||
|
minutes = int((timedelta.seconds % 3600) / 60)
|
||||||
|
seconds = timedelta.seconds % 60
|
||||||
|
|
||||||
|
if display == "sql":
|
||||||
|
days += weeks * 7
|
||||||
|
return "%i %02i:%02i:%02i" % (days, hours, minutes, seconds)
|
||||||
|
elif display == "iso8601":
|
||||||
|
return iso8601_repr(timedelta)
|
||||||
|
elif display == 'minimal':
|
||||||
|
words = ["w", "d", "h", "m", "s"]
|
||||||
|
elif display == 'short':
|
||||||
|
words = [" wks", " days", " hrs", " min", " sec"]
|
||||||
|
elif display == 'long':
|
||||||
|
words = [" weeks", " days", " hours", " minutes", " seconds"]
|
||||||
|
else:
|
||||||
|
# Use django template-style formatting.
|
||||||
|
# Valid values are:
|
||||||
|
# d,g,G,h,H,i,s
|
||||||
|
return STRFDATETIME.sub(STRFDATETIME_REPL, display) % {
|
||||||
|
'd': days,
|
||||||
|
'g': hours,
|
||||||
|
'G': hours if hours > 9 else '0%s' % hours,
|
||||||
|
'h': hours,
|
||||||
|
'H': hours if hours > 9 else '0%s' % hours,
|
||||||
|
'i': minutes if minutes > 9 else '0%s' % minutes,
|
||||||
|
's': seconds if seconds > 9 else '0%s' % seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
values = [weeks, days, hours, minutes, seconds]
|
||||||
|
|
||||||
|
for i in range(len(values)):
|
||||||
|
if values[i]:
|
||||||
|
if values[i] == 1 and len(words[i]) > 1:
|
||||||
|
result.append("%i%s" % (values[i], words[i].rstrip('s')))
|
||||||
|
else:
|
||||||
|
result.append("%i%s" % (values[i], words[i]))
|
||||||
|
|
||||||
|
# values with less than one second, which are considered zeroes
|
||||||
|
if len(result) == 0:
|
||||||
|
# display as 0 of the smallest unit
|
||||||
|
result.append('0%s' % (words[-1]))
|
||||||
|
|
||||||
|
return sep.join(result)
|
||||||
|
|
||||||
|
|
||||||
|
def iso8601_repr(timedelta, format=None):
|
||||||
|
"""
|
||||||
|
Represent a timedelta as an ISO8601 duration.
|
||||||
|
http://en.wikipedia.org/wiki/ISO_8601#Durations
|
||||||
|
|
||||||
|
>>> from datetime import timedelta as td
|
||||||
|
>>> iso8601_repr(td(days=1, hours=2, minutes=3, seconds=4))
|
||||||
|
'P1DT2H3M4S'
|
||||||
|
|
||||||
|
>>> iso8601_repr(td(hours=1, minutes=10, seconds=20), 'alt')
|
||||||
|
'PT01:10:20'
|
||||||
|
"""
|
||||||
|
years = int(timedelta.days / 365)
|
||||||
|
weeks = int((timedelta.days % 365) / 7)
|
||||||
|
days = timedelta.days % 7
|
||||||
|
|
||||||
|
hours = int(timedelta.seconds / 3600)
|
||||||
|
minutes = int((timedelta.seconds % 3600) / 60)
|
||||||
|
seconds = timedelta.seconds % 60
|
||||||
|
|
||||||
|
if format == 'alt':
|
||||||
|
if years or weeks or days:
|
||||||
|
raise ValueError('Does not support alt format for durations > 1 day')
|
||||||
|
return 'PT{0:02d}:{1:02d}:{2:02d}'.format(hours, minutes, seconds)
|
||||||
|
|
||||||
|
formatting = (
|
||||||
|
('P', (
|
||||||
|
('Y', years),
|
||||||
|
('W', weeks),
|
||||||
|
('D', days),
|
||||||
|
)),
|
||||||
|
('T', (
|
||||||
|
('H', hours),
|
||||||
|
('M', minutes),
|
||||||
|
('S', seconds),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for category, subcats in formatting:
|
||||||
|
result += category
|
||||||
|
for format, value in subcats:
|
||||||
|
if value:
|
||||||
|
result.append('%d%c' % (value, format))
|
||||||
|
if result[-1] == 'T':
|
||||||
|
result = result[:-1]
|
||||||
|
|
||||||
|
return "".join(result)
|
||||||
|
|
||||||
|
def parse(string):
|
||||||
|
"""
|
||||||
|
Parse a string into a timedelta object.
|
||||||
|
|
||||||
|
>>> parse("1 day")
|
||||||
|
datetime.timedelta(1)
|
||||||
|
>>> parse("2 days")
|
||||||
|
datetime.timedelta(2)
|
||||||
|
>>> parse("1 d")
|
||||||
|
datetime.timedelta(1)
|
||||||
|
>>> parse("1 hour")
|
||||||
|
datetime.timedelta(0, 3600)
|
||||||
|
>>> parse("1 hours")
|
||||||
|
datetime.timedelta(0, 3600)
|
||||||
|
>>> parse("1 hr")
|
||||||
|
datetime.timedelta(0, 3600)
|
||||||
|
>>> parse("1 hrs")
|
||||||
|
datetime.timedelta(0, 3600)
|
||||||
|
>>> parse("1h")
|
||||||
|
datetime.timedelta(0, 3600)
|
||||||
|
>>> parse("1wk")
|
||||||
|
datetime.timedelta(7)
|
||||||
|
>>> parse("1 week")
|
||||||
|
datetime.timedelta(7)
|
||||||
|
>>> parse("1 weeks")
|
||||||
|
datetime.timedelta(7)
|
||||||
|
>>> parse("2 wks")
|
||||||
|
datetime.timedelta(14)
|
||||||
|
>>> parse("1 sec")
|
||||||
|
datetime.timedelta(0, 1)
|
||||||
|
>>> parse("1 secs")
|
||||||
|
datetime.timedelta(0, 1)
|
||||||
|
>>> parse("1 s")
|
||||||
|
datetime.timedelta(0, 1)
|
||||||
|
>>> parse("1 second")
|
||||||
|
datetime.timedelta(0, 1)
|
||||||
|
>>> parse("1 seconds")
|
||||||
|
datetime.timedelta(0, 1)
|
||||||
|
>>> parse("1 minute")
|
||||||
|
datetime.timedelta(0, 60)
|
||||||
|
>>> parse("1 min")
|
||||||
|
datetime.timedelta(0, 60)
|
||||||
|
>>> parse("1 m")
|
||||||
|
datetime.timedelta(0, 60)
|
||||||
|
>>> parse("1 minutes")
|
||||||
|
datetime.timedelta(0, 60)
|
||||||
|
>>> parse("1 mins")
|
||||||
|
datetime.timedelta(0, 60)
|
||||||
|
>>> parse("2 ws")
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
TypeError: '2 ws' is not a valid time interval
|
||||||
|
>>> parse("2 ds")
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
TypeError: '2 ds' is not a valid time interval
|
||||||
|
>>> parse("2 hs")
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
TypeError: '2 hs' is not a valid time interval
|
||||||
|
>>> parse("2 ms")
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
TypeError: '2 ms' is not a valid time interval
|
||||||
|
>>> parse("2 ss")
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
TypeError: '2 ss' is not a valid time interval
|
||||||
|
>>> parse("")
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
TypeError: '' is not a valid time interval
|
||||||
|
>>> parse("1.5 days")
|
||||||
|
datetime.timedelta(1, 43200)
|
||||||
|
>>> parse("3 weeks")
|
||||||
|
datetime.timedelta(21)
|
||||||
|
>>> parse("4.2 hours")
|
||||||
|
datetime.timedelta(0, 15120)
|
||||||
|
>>> parse(".5 hours")
|
||||||
|
datetime.timedelta(0, 1800)
|
||||||
|
>>> parse(" hours")
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
TypeError: 'hours' is not a valid time interval
|
||||||
|
>>> parse("1 hour, 5 mins")
|
||||||
|
datetime.timedelta(0, 3900)
|
||||||
|
|
||||||
|
>>> parse("-2 days")
|
||||||
|
datetime.timedelta(-2)
|
||||||
|
>>> parse("-1 day 0:00:01")
|
||||||
|
datetime.timedelta(-1, 1)
|
||||||
|
>>> parse("-1 day, -1:01:01")
|
||||||
|
datetime.timedelta(-2, 82739)
|
||||||
|
>>> parse("-1 weeks, 2 days, -3 hours, 4 minutes, -5 seconds")
|
||||||
|
datetime.timedelta(-5, 11045)
|
||||||
|
|
||||||
|
>>> parse("0 seconds")
|
||||||
|
datetime.timedelta(0)
|
||||||
|
>>> parse("0 days")
|
||||||
|
datetime.timedelta(0)
|
||||||
|
>>> parse("0 weeks")
|
||||||
|
datetime.timedelta(0)
|
||||||
|
|
||||||
|
>>> zero = datetime.timedelta(0)
|
||||||
|
>>> parse(nice_repr(zero))
|
||||||
|
datetime.timedelta(0)
|
||||||
|
>>> parse(nice_repr(zero, 'minimal'))
|
||||||
|
datetime.timedelta(0)
|
||||||
|
>>> parse(nice_repr(zero, 'short'))
|
||||||
|
datetime.timedelta(0)
|
||||||
|
>>> parse(' 50 days 00:00:00 ')
|
||||||
|
datetime.timedelta(50)
|
||||||
|
"""
|
||||||
|
string = string.strip()
|
||||||
|
|
||||||
|
if string == "":
|
||||||
|
raise TypeError("'%s' is not a valid time interval" % string)
|
||||||
|
# This is the format we get from sometimes Postgres, sqlite,
|
||||||
|
# and from serialization
|
||||||
|
d = re.match(r'^((?P<days>[-+]?\d+) days?,? )?(?P<sign>[-+]?)(?P<hours>\d+):'
|
||||||
|
r'(?P<minutes>\d+)(:(?P<seconds>\d+(\.\d+)?))?$',
|
||||||
|
six.text_type(string))
|
||||||
|
if d:
|
||||||
|
d = d.groupdict(0)
|
||||||
|
if d['sign'] == '-':
|
||||||
|
for k in 'hours', 'minutes', 'seconds':
|
||||||
|
d[k] = '-' + d[k]
|
||||||
|
d.pop('sign', None)
|
||||||
|
else:
|
||||||
|
# This is the more flexible format
|
||||||
|
d = re.match(
|
||||||
|
r'^((?P<weeks>-?((\d*\.\d+)|\d+))\W*w((ee)?(k(s)?)?)(,)?\W*)?'
|
||||||
|
r'((?P<days>-?((\d*\.\d+)|\d+))\W*d(ay(s)?)?(,)?\W*)?'
|
||||||
|
r'((?P<hours>-?((\d*\.\d+)|\d+))\W*h(ou)?(r(s)?)?(,)?\W*)?'
|
||||||
|
r'((?P<minutes>-?((\d*\.\d+)|\d+))\W*m(in(ute)?(s)?)?(,)?\W*)?'
|
||||||
|
r'((?P<seconds>-?((\d*\.\d+)|\d+))\W*s(ec(ond)?(s)?)?)?\W*$',
|
||||||
|
six.text_type(string))
|
||||||
|
if not d:
|
||||||
|
raise TypeError("'%s' is not a valid time interval" % string)
|
||||||
|
d = d.groupdict(0)
|
||||||
|
|
||||||
|
return datetime.timedelta(**dict(( (k, float(v)) for k,v in d.items())))
|
||||||
|
|
||||||
|
|
||||||
|
def divide(obj1, obj2, as_float=False):
|
||||||
|
"""
|
||||||
|
Allows for the division of timedeltas by other timedeltas, or by
|
||||||
|
floats/Decimals
|
||||||
|
|
||||||
|
>>> from datetime import timedelta as td
|
||||||
|
>>> divide(td(1), td(1))
|
||||||
|
1
|
||||||
|
>>> divide(td(2), td(1))
|
||||||
|
2
|
||||||
|
>>> divide(td(32), 16)
|
||||||
|
datetime.timedelta(2)
|
||||||
|
>>> divide(datetime.timedelta(1), datetime.timedelta(hours=6))
|
||||||
|
4
|
||||||
|
>>> divide(datetime.timedelta(2), datetime.timedelta(3))
|
||||||
|
0
|
||||||
|
>>> divide(datetime.timedelta(8), datetime.timedelta(3), as_float=True)
|
||||||
|
2.6666666666666665
|
||||||
|
>>> divide(datetime.timedelta(8), 2.0)
|
||||||
|
datetime.timedelta(4)
|
||||||
|
>>> divide(datetime.timedelta(8), 2, as_float=True)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
AssertionError: as_float=True is inappropriate when dividing timedelta by a number.
|
||||||
|
|
||||||
|
"""
|
||||||
|
assert isinstance(obj1, datetime.timedelta), "First argument must be a timedelta."
|
||||||
|
assert isinstance(obj2, (datetime.timedelta, int, float, Decimal)), "Second argument must be a timedelta or number"
|
||||||
|
|
||||||
|
sec1 = obj1.days * 86400 + obj1.seconds
|
||||||
|
if isinstance(obj2, datetime.timedelta):
|
||||||
|
sec2 = obj2.days * 86400 + obj2.seconds
|
||||||
|
value = sec1 / sec2
|
||||||
|
if as_float:
|
||||||
|
return value
|
||||||
|
return int(value)
|
||||||
|
else:
|
||||||
|
if as_float:
|
||||||
|
assert None, "as_float=True is inappropriate when dividing timedelta by a number."
|
||||||
|
secs = sec1 / obj2
|
||||||
|
if isinstance(secs, Decimal):
|
||||||
|
secs = float(secs)
|
||||||
|
return datetime.timedelta(seconds=secs)
|
||||||
|
|
||||||
|
def modulo(obj1, obj2):
|
||||||
|
"""
|
||||||
|
Allows for remainder division of timedelta by timedelta or integer.
|
||||||
|
|
||||||
|
>>> from datetime import timedelta as td
|
||||||
|
>>> modulo(td(5), td(2))
|
||||||
|
datetime.timedelta(1)
|
||||||
|
>>> modulo(td(6), td(3))
|
||||||
|
datetime.timedelta(0)
|
||||||
|
>>> modulo(td(15), 4 * 3600 * 24)
|
||||||
|
datetime.timedelta(3)
|
||||||
|
|
||||||
|
>>> modulo(5, td(1))
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
AssertionError: First argument must be a timedelta.
|
||||||
|
>>> modulo(td(1), 2.8)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
AssertionError: Second argument must be a timedelta or int.
|
||||||
|
"""
|
||||||
|
assert isinstance(obj1, datetime.timedelta), "First argument must be a timedelta."
|
||||||
|
assert isinstance(obj2, (datetime.timedelta, int)), "Second argument must be a timedelta or int."
|
||||||
|
|
||||||
|
sec1 = obj1.days * 86400 + obj1.seconds
|
||||||
|
if isinstance(obj2, datetime.timedelta):
|
||||||
|
sec2 = obj2.days * 86400 + obj2.seconds
|
||||||
|
return datetime.timedelta(seconds=sec1 % sec2)
|
||||||
|
else:
|
||||||
|
return datetime.timedelta(seconds=(sec1 % obj2))
|
||||||
|
|
||||||
|
def percentage(obj1, obj2):
|
||||||
|
"""
|
||||||
|
What percentage of obj2 is obj1? We want the answer as a float.
|
||||||
|
>>> percentage(datetime.timedelta(4), datetime.timedelta(2))
|
||||||
|
200.0
|
||||||
|
>>> percentage(datetime.timedelta(2), datetime.timedelta(4))
|
||||||
|
50.0
|
||||||
|
"""
|
||||||
|
assert isinstance(obj1, datetime.timedelta), "First argument must be a timedelta."
|
||||||
|
assert isinstance(obj2, datetime.timedelta), "Second argument must be a timedelta."
|
||||||
|
|
||||||
|
return divide(obj1 * 100, obj2, as_float=True)
|
||||||
|
|
||||||
|
def decimal_percentage(obj1, obj2):
|
||||||
|
"""
|
||||||
|
>>> decimal_percentage(datetime.timedelta(4), datetime.timedelta(2))
|
||||||
|
Decimal('200.0')
|
||||||
|
>>> decimal_percentage(datetime.timedelta(2), datetime.timedelta(4))
|
||||||
|
Decimal('50.0')
|
||||||
|
"""
|
||||||
|
return Decimal(str(percentage(obj1, obj2)))
|
||||||
|
|
||||||
|
|
||||||
|
def multiply(obj, val):
|
||||||
|
"""
|
||||||
|
Allows for the multiplication of timedeltas by float values.
|
||||||
|
>>> multiply(datetime.timedelta(seconds=20), 1.5)
|
||||||
|
datetime.timedelta(0, 30)
|
||||||
|
>>> multiply(datetime.timedelta(1), 2.5)
|
||||||
|
datetime.timedelta(2, 43200)
|
||||||
|
>>> multiply(datetime.timedelta(1), 3)
|
||||||
|
datetime.timedelta(3)
|
||||||
|
>>> multiply(datetime.timedelta(1), Decimal("5.5"))
|
||||||
|
datetime.timedelta(5, 43200)
|
||||||
|
>>> multiply(datetime.date.today(), 2.5)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
AssertionError: First argument must be a timedelta.
|
||||||
|
>>> multiply(datetime.timedelta(1), "2")
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
AssertionError: Second argument must be a number.
|
||||||
|
"""
|
||||||
|
|
||||||
|
assert isinstance(obj, datetime.timedelta), "First argument must be a timedelta."
|
||||||
|
assert isinstance(val, (int, float, Decimal)), "Second argument must be a number."
|
||||||
|
|
||||||
|
sec = obj.days * 86400 + obj.seconds
|
||||||
|
sec *= val
|
||||||
|
if isinstance(sec, Decimal):
|
||||||
|
sec = float(sec)
|
||||||
|
return datetime.timedelta(seconds=sec)
|
||||||
|
|
||||||
|
|
||||||
|
def round_to_nearest(obj, timedelta):
|
||||||
|
"""
|
||||||
|
The obj is rounded to the nearest whole number of timedeltas.
|
||||||
|
|
||||||
|
obj can be a timedelta, datetime or time object.
|
||||||
|
|
||||||
|
>>> round_to_nearest(datetime.datetime(2012, 1, 1, 9, 43), datetime.timedelta(1))
|
||||||
|
datetime.datetime(2012, 1, 1, 0, 0)
|
||||||
|
>>> round_to_nearest(datetime.datetime(2012, 1, 1, 9, 43), datetime.timedelta(hours=1))
|
||||||
|
datetime.datetime(2012, 1, 1, 10, 0)
|
||||||
|
>>> round_to_nearest(datetime.datetime(2012, 1, 1, 9, 43), datetime.timedelta(minutes=15))
|
||||||
|
datetime.datetime(2012, 1, 1, 9, 45)
|
||||||
|
>>> round_to_nearest(datetime.datetime(2012, 1, 1, 9, 43), datetime.timedelta(minutes=1))
|
||||||
|
datetime.datetime(2012, 1, 1, 9, 43)
|
||||||
|
|
||||||
|
>>> td = datetime.timedelta(minutes=30)
|
||||||
|
>>> round_to_nearest(datetime.timedelta(minutes=0), td)
|
||||||
|
datetime.timedelta(0)
|
||||||
|
>>> round_to_nearest(datetime.timedelta(minutes=14), td)
|
||||||
|
datetime.timedelta(0)
|
||||||
|
>>> round_to_nearest(datetime.timedelta(minutes=15), td)
|
||||||
|
datetime.timedelta(0, 1800)
|
||||||
|
>>> round_to_nearest(datetime.timedelta(minutes=29), td)
|
||||||
|
datetime.timedelta(0, 1800)
|
||||||
|
>>> round_to_nearest(datetime.timedelta(minutes=30), td)
|
||||||
|
datetime.timedelta(0, 1800)
|
||||||
|
>>> round_to_nearest(datetime.timedelta(minutes=42), td)
|
||||||
|
datetime.timedelta(0, 1800)
|
||||||
|
>>> round_to_nearest(datetime.timedelta(hours=7, minutes=22), td)
|
||||||
|
datetime.timedelta(0, 27000)
|
||||||
|
|
||||||
|
>>> td = datetime.timedelta(minutes=15)
|
||||||
|
>>> round_to_nearest(datetime.timedelta(minutes=0), td)
|
||||||
|
datetime.timedelta(0)
|
||||||
|
>>> round_to_nearest(datetime.timedelta(minutes=14), td)
|
||||||
|
datetime.timedelta(0, 900)
|
||||||
|
>>> round_to_nearest(datetime.timedelta(minutes=15), td)
|
||||||
|
datetime.timedelta(0, 900)
|
||||||
|
>>> round_to_nearest(datetime.timedelta(minutes=29), td)
|
||||||
|
datetime.timedelta(0, 1800)
|
||||||
|
>>> round_to_nearest(datetime.timedelta(minutes=30), td)
|
||||||
|
datetime.timedelta(0, 1800)
|
||||||
|
>>> round_to_nearest(datetime.timedelta(minutes=42), td)
|
||||||
|
datetime.timedelta(0, 2700)
|
||||||
|
>>> round_to_nearest(datetime.timedelta(hours=7, minutes=22), td)
|
||||||
|
datetime.timedelta(0, 26100)
|
||||||
|
|
||||||
|
>>> td = datetime.timedelta(minutes=30)
|
||||||
|
>>> round_to_nearest(datetime.datetime(2010,1,1,9,22), td)
|
||||||
|
datetime.datetime(2010, 1, 1, 9, 30)
|
||||||
|
>>> round_to_nearest(datetime.datetime(2010,1,1,9,32), td)
|
||||||
|
datetime.datetime(2010, 1, 1, 9, 30)
|
||||||
|
>>> round_to_nearest(datetime.datetime(2010,1,1,9,42), td)
|
||||||
|
datetime.datetime(2010, 1, 1, 9, 30)
|
||||||
|
|
||||||
|
>>> round_to_nearest(datetime.time(0,20), td)
|
||||||
|
datetime.time(0, 30)
|
||||||
|
|
||||||
|
TODO: test with tzinfo (non-naive) datetimes/times.
|
||||||
|
"""
|
||||||
|
|
||||||
|
assert isinstance(obj, (datetime.datetime, datetime.timedelta, datetime.time)), "First argument must be datetime, time or timedelta."
|
||||||
|
assert isinstance(timedelta, datetime.timedelta), "Second argument must be a timedelta."
|
||||||
|
|
||||||
|
time_only = False
|
||||||
|
if isinstance(obj, datetime.timedelta):
|
||||||
|
counter = datetime.timedelta(0)
|
||||||
|
elif isinstance(obj, datetime.datetime):
|
||||||
|
counter = datetime.datetime.combine(obj.date(), datetime.time(0, tzinfo=obj.tzinfo))
|
||||||
|
elif isinstance(obj, datetime.time):
|
||||||
|
counter = datetime.datetime.combine(datetime.date.today(), datetime.time(0, tzinfo=obj.tzinfo))
|
||||||
|
obj = datetime.datetime.combine(datetime.date.today(), obj)
|
||||||
|
time_only = True
|
||||||
|
|
||||||
|
diff = abs(obj - counter)
|
||||||
|
while counter < obj:
|
||||||
|
old_diff = diff
|
||||||
|
counter += timedelta
|
||||||
|
diff = abs(obj - counter)
|
||||||
|
|
||||||
|
if counter == obj:
|
||||||
|
result = obj
|
||||||
|
elif diff <= old_diff:
|
||||||
|
result = counter
|
||||||
|
else:
|
||||||
|
result = counter - timedelta
|
||||||
|
|
||||||
|
if time_only:
|
||||||
|
return result.time()
|
||||||
|
else:
|
||||||
|
return result
|
||||||
|
|
||||||
|
def decimal_hours(timedelta, decimal_places=None):
|
||||||
|
"""
|
||||||
|
Return a decimal value of the number of hours that this timedelta
|
||||||
|
object refers to.
|
||||||
|
"""
|
||||||
|
hours = Decimal(timedelta.days*24) + Decimal(timedelta.seconds) / 3600
|
||||||
|
if decimal_places:
|
||||||
|
return hours.quantize(Decimal(str(10**-decimal_places)))
|
||||||
|
return hours
|
||||||
|
|
||||||
|
def week_containing(date):
|
||||||
|
if date.weekday():
|
||||||
|
date -= datetime.timedelta(date.weekday())
|
||||||
|
|
||||||
|
return date, date + datetime.timedelta(6)
|
||||||
|
|
||||||
|
try:
|
||||||
|
datetime.timedelta().total_seconds
|
||||||
|
def total_seconds(timedelta):
|
||||||
|
return timedelta.total_seconds()
|
||||||
|
except AttributeError:
|
||||||
|
def total_seconds(timedelta):
|
||||||
|
"""
|
||||||
|
Python < 2.7 does not have datetime.timedelta.total_seconds
|
||||||
|
"""
|
||||||
|
return timedelta.days * 86400 + timedelta.seconds
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import doctest
|
||||||
|
doctest.testmod()
|
0
timedelta/models.py
Normal file
0
timedelta/models.py
Normal file
0
timedelta/templatetags/__init__.py
Normal file
0
timedelta/templatetags/__init__.py
Normal file
10
timedelta/templatetags/decimal_hours.py
Normal file
10
timedelta/templatetags/decimal_hours.py
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
from django import template
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
from ..helpers import decimal_hours as dh
|
||||||
|
|
||||||
|
@register.filter(name='decimal_hours')
|
||||||
|
def decimal_hours(value, decimal_places=None):
|
||||||
|
if value is None:
|
||||||
|
return value
|
||||||
|
return dh(value, decimal_places)
|
34
timedelta/templatetags/timedelta.py
Normal file
34
timedelta/templatetags/timedelta.py
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
from django import template
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
# Don't really like using relative imports, but no choice here!
|
||||||
|
from ..helpers import parse, nice_repr, iso8601_repr, total_seconds as _total_seconds
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter(name='timedelta')
|
||||||
|
def timedelta(value, display="long"):
|
||||||
|
if value is None:
|
||||||
|
return value
|
||||||
|
if isinstance(value, basestring):
|
||||||
|
value = parse(value)
|
||||||
|
return nice_repr(value, display)
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter(name='iso8601')
|
||||||
|
def iso8601(value):
|
||||||
|
return timedelta(value, display='iso8601')
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter(name='total_seconds')
|
||||||
|
def total_seconds(value):
|
||||||
|
if value is None:
|
||||||
|
return value
|
||||||
|
return _total_seconds(value)
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter(name='total_seconds_sort')
|
||||||
|
def total_seconds(value, places=10):
|
||||||
|
if value is None:
|
||||||
|
return value
|
||||||
|
return ("%0" + str(places) + "i") % _total_seconds(value)
|
||||||
|
|
128
timedelta/tests.py
Normal file
128
timedelta/tests.py
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
from unittest import TestCase
|
||||||
|
import datetime
|
||||||
|
import doctest
|
||||||
|
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.db import models
|
||||||
|
from django.utils import six
|
||||||
|
|
||||||
|
from .fields import TimedeltaField
|
||||||
|
import timedelta.helpers
|
||||||
|
import timedelta.forms
|
||||||
|
import timedelta.widgets
|
||||||
|
|
||||||
|
class MinMaxTestModel(models.Model):
|
||||||
|
min = TimedeltaField(min_value=datetime.timedelta(1))
|
||||||
|
max = TimedeltaField(max_value=datetime.timedelta(1))
|
||||||
|
minmax = TimedeltaField(min_value=datetime.timedelta(1), max_value=datetime.timedelta(7))
|
||||||
|
|
||||||
|
class IntTestModel(models.Model):
|
||||||
|
field = TimedeltaField(min_value=1, max_value=86400)
|
||||||
|
|
||||||
|
class FloatTestModel(models.Model):
|
||||||
|
field = TimedeltaField(min_value=1.0, max_value=86400.0)
|
||||||
|
|
||||||
|
class TimedeltaModelFieldTest(TestCase):
|
||||||
|
def test_validate(self):
|
||||||
|
test = MinMaxTestModel(
|
||||||
|
min=datetime.timedelta(1),
|
||||||
|
max=datetime.timedelta(1),
|
||||||
|
minmax=datetime.timedelta(1)
|
||||||
|
)
|
||||||
|
test.full_clean() # This should have met validation requirements.
|
||||||
|
|
||||||
|
test.min = datetime.timedelta(hours=23)
|
||||||
|
self.assertRaises(ValidationError, test.full_clean)
|
||||||
|
|
||||||
|
test.min = datetime.timedelta(hours=25)
|
||||||
|
test.full_clean()
|
||||||
|
|
||||||
|
test.max = datetime.timedelta(11)
|
||||||
|
self.assertRaises(ValidationError, test.full_clean)
|
||||||
|
|
||||||
|
test.max = datetime.timedelta(hours=20)
|
||||||
|
test.full_clean()
|
||||||
|
|
||||||
|
test.minmax = datetime.timedelta(0)
|
||||||
|
self.assertRaises(ValidationError, test.full_clean)
|
||||||
|
test.minmax = datetime.timedelta(22)
|
||||||
|
self.assertRaises(ValidationError, test.full_clean)
|
||||||
|
test.minmax = datetime.timedelta(6, hours=23, minutes=59, seconds=59)
|
||||||
|
test.full_clean()
|
||||||
|
|
||||||
|
def test_from_int(self):
|
||||||
|
"""
|
||||||
|
Check that integers can be used to define the min_value and max_value
|
||||||
|
arguments, and that when assigned an integer, TimedeltaField converts
|
||||||
|
to timedelta.
|
||||||
|
"""
|
||||||
|
|
||||||
|
test = IntTestModel()
|
||||||
|
|
||||||
|
# valid
|
||||||
|
test.field = 3600
|
||||||
|
self.assertEquals(test.field, datetime.timedelta(seconds=3600))
|
||||||
|
test.full_clean()
|
||||||
|
|
||||||
|
# invalid
|
||||||
|
test.field = 0
|
||||||
|
self.assertRaises(ValidationError, test.full_clean)
|
||||||
|
|
||||||
|
# also invalid
|
||||||
|
test.field = 86401
|
||||||
|
self.assertRaises(ValidationError, test.full_clean)
|
||||||
|
|
||||||
|
def test_from_float(self):
|
||||||
|
"""
|
||||||
|
Check that floats can be used to define the min_value and max_value
|
||||||
|
arguments, and that when assigned a float, TimedeltaField converts
|
||||||
|
to timedelta.
|
||||||
|
"""
|
||||||
|
|
||||||
|
test = FloatTestModel()
|
||||||
|
|
||||||
|
# valid
|
||||||
|
test.field = 3600.0
|
||||||
|
self.assertEquals(test.field, datetime.timedelta(seconds=3600))
|
||||||
|
test.full_clean()
|
||||||
|
|
||||||
|
# invalid
|
||||||
|
test.field = 0.0
|
||||||
|
self.assertRaises(ValidationError, test.full_clean)
|
||||||
|
|
||||||
|
# also invalid
|
||||||
|
test.field = 86401.0
|
||||||
|
self.assertRaises(ValidationError, test.full_clean)
|
||||||
|
|
||||||
|
def test_deconstruct(self):
|
||||||
|
"""
|
||||||
|
Check that the deconstruct() method of TimedeltaField is returning the
|
||||||
|
min_value, max_value and default kwargs as floats.
|
||||||
|
"""
|
||||||
|
|
||||||
|
field = TimedeltaField(
|
||||||
|
min_value=datetime.timedelta(minutes=5),
|
||||||
|
max_value=datetime.timedelta(minutes=15),
|
||||||
|
default=datetime.timedelta(minutes=30),
|
||||||
|
)
|
||||||
|
|
||||||
|
kwargs = field.deconstruct()[3]
|
||||||
|
self.assertEqual(kwargs['default'], 1800.0)
|
||||||
|
self.assertEqual(kwargs['max_value'], 900.0)
|
||||||
|
self.assertEqual(kwargs['min_value'], 300.0)
|
||||||
|
|
||||||
|
def test_load_from_db(self):
|
||||||
|
obj = MinMaxTestModel.objects.create(min='2 days', max='2 minutes', minmax='3 days')
|
||||||
|
self.assertEquals(datetime.timedelta(2), obj.min)
|
||||||
|
self.assertEquals(datetime.timedelta(0, 120), obj.max)
|
||||||
|
self.assertEquals(datetime.timedelta(3), obj.minmax)
|
||||||
|
|
||||||
|
obj = MinMaxTestModel.objects.get()
|
||||||
|
self.assertEquals(datetime.timedelta(2), obj.min)
|
||||||
|
self.assertEquals(datetime.timedelta(0, 120), obj.max)
|
||||||
|
self.assertEquals(datetime.timedelta(3), obj.minmax)
|
||||||
|
|
||||||
|
def load_tests(loader, tests, ignore):
|
||||||
|
tests.addTests(doctest.DocTestSuite(timedelta.helpers))
|
||||||
|
tests.addTests(doctest.DocTestSuite(timedelta.forms))
|
||||||
|
return tests
|
46
timedelta/widgets.py
Normal file
46
timedelta/widgets.py
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from django import forms
|
||||||
|
from django.utils import six
|
||||||
|
|
||||||
|
from .helpers import nice_repr, parse
|
||||||
|
|
||||||
|
class TimedeltaWidget(forms.TextInput):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
return super(TimedeltaWidget, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def render(self, name, value, attrs=None):
|
||||||
|
if value is None:
|
||||||
|
value = ""
|
||||||
|
elif isinstance(value, six.string_types):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if isinstance(value, int):
|
||||||
|
value = datetime.timedelta(seconds=value)
|
||||||
|
value = nice_repr(value)
|
||||||
|
return super(TimedeltaWidget, self).render(name, value, attrs)
|
||||||
|
|
||||||
|
def _has_changed(self, initial, data):
|
||||||
|
"""
|
||||||
|
We need to make sure the objects are of the canonical form, as a
|
||||||
|
string comparison may needlessly fail.
|
||||||
|
"""
|
||||||
|
if initial in ["", None] and data in ["", None]:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if initial in ["", None] or data in ["", None]:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if initial:
|
||||||
|
if not isinstance(initial, datetime.timedelta):
|
||||||
|
initial = parse(initial)
|
||||||
|
|
||||||
|
if not isinstance(data, datetime.timedelta):
|
||||||
|
try:
|
||||||
|
data = parse(data)
|
||||||
|
except TypeError:
|
||||||
|
# initial didn't throw a TypeError, so this must be different
|
||||||
|
# from initial
|
||||||
|
return True
|
||||||
|
|
||||||
|
return initial != data
|
Loading…
Reference in a new issue