Removed all use of timedeltafield by converting old migrations to reference compatible types.

- Legacy-Id: 12825
This commit is contained in:
Henrik Levkowetz 2017-02-12 17:10:06 +00:00
parent 7f607c51d2
commit 350ea9efcf
15 changed files with 6 additions and 1211 deletions

View file

@ -3,8 +3,6 @@ from __future__ import unicode_literals
from django.db import models, migrations
import datetime
import ietf.meeting.timedeltafield
class Migration(migrations.Migration):
@ -112,7 +110,7 @@ class Migration(migrations.Migration):
('attendees', models.IntegerField(null=True, blank=True)),
('agenda_note', models.CharField(max_length=255, blank=True)),
('requested', models.DateTimeField(default=datetime.datetime.now)),
('requested_duration', ietf.meeting.timedeltafield.TimedeltaField(default=0)),
('requested_duration', models.IntegerField(default=0)),
('comments', models.TextField(blank=True)),
('scheduled', models.DateTimeField(null=True, blank=True)),
('modified', models.DateTimeField(default=datetime.datetime.now)),
@ -141,7 +139,7 @@ class Migration(migrations.Migration):
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('name', models.CharField(max_length=255)),
('time', models.DateTimeField()),
('duration', ietf.meeting.timedeltafield.TimedeltaField()),
('duration', models.IntegerField()),
('show_location', models.BooleanField(default=True, help_text=b'Show location in agenda')),
('modified', models.DateTimeField(default=datetime.datetime.now)),
('location', models.ForeignKey(blank=True, to='meeting.Room', null=True)),

View file

@ -2,8 +2,6 @@
from __future__ import unicode_literals
from django.db import models, migrations
import timedelta.fields
class Migration(migrations.Migration):
@ -27,13 +25,13 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='meeting',
name='idsubmit_cutoff_time_utc',
field=timedelta.fields.TimedeltaField(default=86399.0, help_text=b'The time of day (UTC) after which submission will be closed. Use for example 23 hours, 59 minutes, 59 seconds.'),
field=models.CharField(max_length=20, default='86399.0', help_text=b'The time of day (UTC) after which submission will be closed. Use for example 23 hours, 59 minutes, 59 seconds.'),
preserve_default=True,
),
migrations.AddField(
model_name='meeting',
name='idsubmit_cutoff_warning_days',
field=timedelta.fields.TimedeltaField(default=1814400.0, help_text=b'How long before the 00 cutoff to start showing cutoff warnings. Use for example 21 days or 3 weeks.'),
field=models.CharField(max_length=20, default='1814400.0', help_text=b'How long before the 00 cutoff to start showing cutoff warnings. Use for example 21 days or 3 weeks.'),
preserve_default=True,
),
]

View file

@ -2,8 +2,6 @@
from __future__ import unicode_literals
from django.db import models, migrations
import timedelta.fields
class Migration(migrations.Migration):
@ -27,13 +25,13 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='meeting',
name='idsubmit_cutoff_time_utc',
field=timedelta.fields.TimedeltaField(default=86399.0, help_text=b'The time of day (UTC) after which submission will be closed. Use for example 23 hours, 59 minutes, 59 seconds.', blank=True),
field=models.CharField(max_length=20, default='86399.0', help_text=b'The time of day (UTC) after which submission will be closed. Use for example 23 hours, 59 minutes, 59 seconds.', blank=True),
preserve_default=True,
),
migrations.AlterField(
model_name='meeting',
name='idsubmit_cutoff_warning_days',
field=timedelta.fields.TimedeltaField(default=1814400.0, help_text=b'How long before the 00 cutoff to start showing cutoff warnings. Use for example 21 days or 3 weeks.', blank=True),
field=models.CharField(max_length=20, default='1814400.0', help_text=b'How long before the 00 cutoff to start showing cutoff warnings. Use for example 21 days or 3 weeks.', blank=True),
preserve_default=True,
),
]

View file

@ -1,188 +0,0 @@
# -*- coding: iso-8859-1 -*-
# $Id: TimedeltaField.py 1787 2011-04-20 07:09:57Z tguettler $
# $HeadURL: svn+ssh://svnserver/svn/djangotools/trunk/dbfields/TimedeltaField.py $
# from http://djangosnippets.org/snippets/1060/ with some fixes
# Python
import datetime
# Django
import django
from django import forms
from django.db import models
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
SECS_PER_DAY=3600*24
class TimedeltaField(models.Field):
u'''
Store Python's datetime.timedelta in an integer column.
Most database systems only support 32 bit integers by default.
'''
__metaclass__ = models.SubfieldBase
empty_strings_allowed = False
def __init__(self, *args, **kwargs):
super(TimedeltaField, self).__init__(*args, **kwargs)
def to_python(self, value):
if (value is None) or isinstance(value, datetime.timedelta):
return value
try:
# else try to convert to int (e.g. from string)
value = int(value)
except (TypeError, ValueError):
raise django.core.exceptions.ValidationError(
_("This value must be an integer or a datetime.timedelta."))
return datetime.timedelta(seconds=value)
def get_internal_type(self):
return 'IntegerField'
def get_db_prep_lookup(self, lookup_type, value, connection=None, prepared=False):
raise NotImplementedError() # SQL WHERE
def get_db_prep_save(self, value, connection=None, prepared=False):
if (value is None) or isinstance(value, int):
return value
return SECS_PER_DAY*value.days+value.seconds
def formfield(self, *args, **kwargs):
defaults={'form_class': TimedeltaFormField}
defaults.update(kwargs)
return super(TimedeltaField, self).formfield(*args, **defaults)
def value_to_string(self, obj):
value = self._get_val_from_obj(obj)
return self.get_db_prep_value(value) # pylint: disable=no-value-for-parameter
class TimedeltaFormField(forms.Field):
default_error_messages = {
'invalid': _(u'Enter a whole number.'),
}
def __init__(self, *args, **kwargs):
defaults={'widget': TimedeltaWidget}
defaults.update(kwargs)
super(TimedeltaFormField, self).__init__(*args, **defaults)
def clean(self, value):
# value comes from Timedelta.Widget.value_from_datadict(): tuple of strings
super(TimedeltaFormField, self).clean(value)
assert len(value)==len(self.widget.inputs), (value, self.widget.inputs)
i=0
for value, multiply in zip(value, self.widget.multiply):
try:
i+=int(value)*multiply
except (ValueError, TypeError):
raise forms.ValidationError(self.error_messages['invalid'])
return i
class TimedeltaWidget(forms.Widget):
INPUTS=['days', 'hours', 'minutes', 'seconds']
MULTIPLY=[60*60*24, 60*60, 60, 1]
def __init__(self, attrs=None):
self.widgets=[]
if not attrs:
attrs={}
inputs=attrs.get('inputs', self.INPUTS)
multiply=[]
for input in inputs:
assert input in self.INPUTS, (input, self.INPUT)
self.widgets.append(forms.TextInput(attrs=attrs))
multiply.append(self.MULTIPLY[self.INPUTS.index(input)])
self.inputs=inputs
self.multiply=multiply
super(TimedeltaWidget, self).__init__(attrs)
def render(self, name, value, attrs):
if value is None:
values=[0 for i in self.inputs]
elif isinstance(value, datetime.timedelta):
values=split_seconds(value.days*SECS_PER_DAY+value.seconds, self.inputs, self.multiply)
elif isinstance(value, int):
# initial data from model
values=split_seconds(value, self.inputs, self.multiply)
else:
assert isinstance(value, tuple), (value, type(value))
assert len(value)==len(self.inputs), (value, self.inputs)
values=value
id=attrs.pop('id')
assert not attrs, attrs
rendered=[]
for input, widget, val in zip(self.inputs, self.widgets, values):
rendered.append(u'%s %s' % (_(input), widget.render('%s_%s' % (name, input), val)))
return mark_safe('<div id="%s">%s</div>' % (id, ' '.join(rendered)))
def value_from_datadict(self, data, files, name):
# Don't throw ValidationError here, just return a tuple of strings.
ret=[]
for input, multi in zip(self.inputs, self.multiply):
ret.append(data.get('%s_%s' % (name, input), 0))
return tuple(ret)
def _has_changed(self, initial_value, data_value):
# data_value comes from value_from_datadict(): A tuple of strings.
if initial_value is None:
return bool(set(data_value)!=set([u'0']))
assert isinstance(initial_value, datetime.timedelta), initial_value
initial=tuple([unicode(i) for i in split_seconds(initial_value.days*SECS_PER_DAY+initial_value.seconds, self.inputs, self.multiply)])
assert len(initial)==len(data_value), (initial, data_value)
return bool(initial!=data_value)
def main():
assert split_seconds(1000000)==[11, 13, 46, 40]
field=TimedeltaField()
td=datetime.timedelta(days=10, seconds=11)
s=field.get_db_prep_save(td)
assert isinstance(s, int), (s, type(s))
td_again=field.to_python(s)
assert td==td_again, (td, td_again)
td=datetime.timedelta(seconds=11)
s=field.get_db_prep_save(td)
td_again=field.to_python(s)
assert td==td_again, (td, td_again)
field=TimedeltaFormField()
assert field.widget._has_changed(datetime.timedelta(seconds=0), (u'0', u'0', u'0', u'0',)) is False
assert field.widget._has_changed(None, (u'0', u'0', u'0', u'0',)) is False
assert field.widget._has_changed(None, (u'0', u'0')) is False
assert field.widget._has_changed(datetime.timedelta(days=1, hours=2, minutes=3, seconds=4), (u'1', u'2', u'3', u'4',)) is False
for secs, soll, kwargs in [
(100, [0, 0, 1, 40], dict()),
(100, ['0days', '0hours', '1minutes', '40seconds'], dict(with_unit=True)),
(100, ['1minutes', '40seconds'], dict(with_unit=True, remove_leading_zeros=True)),
(100000, ['1days', '3hours'], dict(inputs=['days', 'hours'], with_unit=True, remove_leading_zeros=True)),
]:
ist=split_seconds(secs, **kwargs)
if ist!=soll:
raise Exception('geg=%s soll=%s ist=%s kwargs=%s' % (secs, soll, ist, kwargs))
print "unittest OK"
def split_seconds(secs, inputs=TimedeltaWidget.INPUTS, multiply=TimedeltaWidget.MULTIPLY,
with_unit=False, remove_leading_zeros=False):
ret=[]
assert len(inputs)<=len(multiply), (inputs, multiply)
for input, multi in zip(inputs, multiply):
count, secs = divmod(secs, multi)
if remove_leading_zeros and not ret and not count:
continue
if with_unit:
ret.append('%s%s' % (count, input))
else:
ret.append(count)
return ret
if __name__=='__main__':
main()

View file

@ -1 +0,0 @@
0.7.3

View file

@ -1,19 +0,0 @@
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

View file

@ -1,120 +0,0 @@
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

View file

@ -1,99 +0,0 @@
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)

View file

@ -1,554 +0,0 @@
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()

View file

View file

@ -1,10 +0,0 @@
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)

View file

@ -1,34 +0,0 @@
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)

View file

@ -1,128 +0,0 @@
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

View file

@ -1,46 +0,0 @@
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