summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorMarc Tamlyn <marc.tamlyn@gmail.com>2014-07-24 13:57:24 +0100
committerMarc Tamlyn <marc.tamlyn@gmail.com>2014-12-20 18:28:29 +0000
commit57554442fe3e209c135e15dda4ea45123e579e58 (patch)
tree0ef2cb0e3048d13b82e4c7e81192df6124556a44 /tests
parenta3d96bee36040975ded8e3bf02e33e48d06f1f16 (diff)
Fixed #2443 -- Added DurationField.
A field for storing periods of time - modeled in Python by timedelta. It is stored in the native interval data type on PostgreSQL and as a bigint of microseconds on other backends. Also includes significant changes to the internals of time related maths in expressions, including the removal of DateModifierNode. Thanks to Tim and Josh in particular for reviews.
Diffstat (limited to 'tests')
-rw-r--r--tests/expressions/models.py1
-rw-r--r--tests/expressions/tests.py73
-rw-r--r--tests/forms_tests/tests/test_fields.py37
-rw-r--r--tests/model_fields/models.py4
-rw-r--r--tests/model_fields/test_durationfield.py67
-rw-r--r--tests/utils_tests/test_dateparse.py44
-rw-r--r--tests/utils_tests/test_duration.py43
7 files changed, 221 insertions, 48 deletions
diff --git a/tests/expressions/models.py b/tests/expressions/models.py
index 3a25e0862e..53eb54ec48 100644
--- a/tests/expressions/models.py
+++ b/tests/expressions/models.py
@@ -47,6 +47,7 @@ class Experiment(models.Model):
name = models.CharField(max_length=24)
assigned = models.DateField()
completed = models.DateField()
+ estimated_time = models.DurationField()
start = models.DateTimeField()
end = models.DateTimeField()
diff --git a/tests/expressions/tests.py b/tests/expressions/tests.py
index 6949dbf43a..0e9bd57e91 100644
--- a/tests/expressions/tests.py
+++ b/tests/expressions/tests.py
@@ -4,7 +4,7 @@ from copy import deepcopy
import datetime
from django.core.exceptions import FieldError
-from django.db import connection, transaction
+from django.db import connection, transaction, DatabaseError
from django.db.models import F
from django.test import TestCase, skipIfDBFeature, skipUnlessDBFeature
from django.test.utils import Approximate
@@ -602,7 +602,7 @@ class FTimeDeltaTests(TestCase):
# e0: started same day as assigned, zero duration
end = stime + delta0
e0 = Experiment.objects.create(name='e0', assigned=sday, start=stime,
- end=end, completed=end.date())
+ end=end, completed=end.date(), estimated_time=delta0)
self.deltas.append(delta0)
self.delays.append(e0.start -
datetime.datetime.combine(e0.assigned, midnight))
@@ -617,7 +617,7 @@ class FTimeDeltaTests(TestCase):
delay = datetime.timedelta(1)
end = stime + delay + delta1
e1 = Experiment.objects.create(name='e1', assigned=sday,
- start=stime + delay, end=end, completed=end.date())
+ start=stime + delay, end=end, completed=end.date(), estimated_time=delta1)
self.deltas.append(delta1)
self.delays.append(e1.start -
datetime.datetime.combine(e1.assigned, midnight))
@@ -627,7 +627,7 @@ class FTimeDeltaTests(TestCase):
end = stime + delta2
e2 = Experiment.objects.create(name='e2',
assigned=sday - datetime.timedelta(3), start=stime, end=end,
- completed=end.date())
+ completed=end.date(), estimated_time=datetime.timedelta(hours=1))
self.deltas.append(delta2)
self.delays.append(e2.start -
datetime.datetime.combine(e2.assigned, midnight))
@@ -637,7 +637,7 @@ class FTimeDeltaTests(TestCase):
delay = datetime.timedelta(4)
end = stime + delay + delta3
e3 = Experiment.objects.create(name='e3',
- assigned=sday, start=stime + delay, end=end, completed=end.date())
+ assigned=sday, start=stime + delay, end=end, completed=end.date(), estimated_time=delta3)
self.deltas.append(delta3)
self.delays.append(e3.start -
datetime.datetime.combine(e3.assigned, midnight))
@@ -647,7 +647,7 @@ class FTimeDeltaTests(TestCase):
end = stime + delta4
e4 = Experiment.objects.create(name='e4',
assigned=sday - datetime.timedelta(10), start=stime, end=end,
- completed=end.date())
+ completed=end.date(), estimated_time=delta4 - datetime.timedelta(1))
self.deltas.append(delta4)
self.delays.append(e4.start -
datetime.datetime.combine(e4.assigned, midnight))
@@ -676,6 +676,10 @@ class FTimeDeltaTests(TestCase):
self.assertEqual(test_set, self.expnames[:i])
test_set = [e.name for e in
+ Experiment.objects.filter(end__lt=delta + F('start'))]
+ self.assertEqual(test_set, self.expnames[:i])
+
+ test_set = [e.name for e in
Experiment.objects.filter(end__lte=F('start') + delta)]
self.assertEqual(test_set, self.expnames[:i + 1])
@@ -756,42 +760,29 @@ class FTimeDeltaTests(TestCase):
self.assertEqual(expected_ends, new_ends)
self.assertEqual(expected_durations, new_durations)
- def test_delta_invalid_op_mult(self):
- raised = False
- try:
- repr(Experiment.objects.filter(end__lt=F('start') * self.deltas[0]))
- except TypeError:
- raised = True
- self.assertTrue(raised, "TypeError not raised on attempt to multiply datetime by timedelta.")
+ def test_invalid_operator(self):
+ with self.assertRaises(DatabaseError):
+ list(Experiment.objects.filter(start=F('start') * datetime.timedelta(0)))
+
+ def test_durationfield_add(self):
+ zeros = [e.name for e in
+ Experiment.objects.filter(start=F('start') + F('estimated_time'))]
+ self.assertEqual(zeros, ['e0'])
- def test_delta_invalid_op_div(self):
- raised = False
- try:
- repr(Experiment.objects.filter(end__lt=F('start') / self.deltas[0]))
- except TypeError:
- raised = True
- self.assertTrue(raised, "TypeError not raised on attempt to divide datetime by timedelta.")
+ end_less = [e.name for e in
+ Experiment.objects.filter(end__lt=F('start') + F('estimated_time'))]
+ self.assertEqual(end_less, ['e2'])
- def test_delta_invalid_op_mod(self):
- raised = False
- try:
- repr(Experiment.objects.filter(end__lt=F('start') % self.deltas[0]))
- except TypeError:
- raised = True
- self.assertTrue(raised, "TypeError not raised on attempt to modulo divide datetime by timedelta.")
+ delta_math = [e.name for e in
+ Experiment.objects.filter(end__gte=F('start') + F('estimated_time') + datetime.timedelta(hours=1))]
+ self.assertEqual(delta_math, ['e4'])
- def test_delta_invalid_op_and(self):
- raised = False
- try:
- repr(Experiment.objects.filter(end__lt=F('start').bitand(self.deltas[0])))
- except TypeError:
- raised = True
- self.assertTrue(raised, "TypeError not raised on attempt to binary and a datetime with a timedelta.")
+ @skipUnlessDBFeature("has_native_duration_field")
+ def test_date_subtraction(self):
+ under_estimate = [e.name for e in
+ Experiment.objects.filter(estimated_time__gt=F('end') - F('start'))]
+ self.assertEqual(under_estimate, ['e2'])
- def test_delta_invalid_op_or(self):
- raised = False
- try:
- repr(Experiment.objects.filter(end__lt=F('start').bitor(self.deltas[0])))
- except TypeError:
- raised = True
- self.assertTrue(raised, "TypeError not raised on attempt to binary or a datetime with a timedelta.")
+ over_estimate = [e.name for e in
+ Experiment.objects.filter(estimated_time__lt=F('end') - F('start'))]
+ self.assertEqual(over_estimate, ['e4'])
diff --git a/tests/forms_tests/tests/test_fields.py b/tests/forms_tests/tests/test_fields.py
index 3caf902349..b2f0869c47 100644
--- a/tests/forms_tests/tests/test_fields.py
+++ b/tests/forms_tests/tests/test_fields.py
@@ -43,11 +43,12 @@ except ImportError:
from django.core.files.uploadedfile import SimpleUploadedFile
from django.forms import (
BooleanField, CharField, ChoiceField, ComboField, DateField, DateTimeField,
- DecimalField, EmailField, Field, FileField, FilePathField, FloatField,
- Form, forms, HiddenInput, ImageField, IntegerField, MultipleChoiceField,
- NullBooleanField, NumberInput, PasswordInput, RadioSelect, RegexField,
- SplitDateTimeField, TextInput, Textarea, TimeField, TypedChoiceField,
- TypedMultipleChoiceField, URLField, UUIDField, ValidationError, Widget,
+ DecimalField, DurationField, EmailField, Field, FileField, FilePathField,
+ FloatField, Form, forms, HiddenInput, ImageField, IntegerField,
+ MultipleChoiceField, NullBooleanField, NumberInput, PasswordInput,
+ RadioSelect, RegexField, SplitDateTimeField, TextInput, Textarea,
+ TimeField, TypedChoiceField, TypedMultipleChoiceField, URLField, UUIDField,
+ ValidationError, Widget,
)
from django.test import SimpleTestCase
from django.utils import formats
@@ -611,6 +612,32 @@ class FieldsTests(SimpleTestCase):
# RegexField ##################################################################
+ def test_durationfield_1(self):
+ f = DurationField()
+ self.assertEqual(datetime.timedelta(seconds=30), f.clean('30'))
+ self.assertEqual(
+ datetime.timedelta(minutes=15, seconds=30),
+ f.clean('15:30')
+ )
+ self.assertEqual(
+ datetime.timedelta(hours=1, minutes=15, seconds=30),
+ f.clean('1:15:30')
+ )
+ self.assertEqual(
+ datetime.timedelta(
+ days=1, hours=1, minutes=15, seconds=30, milliseconds=300),
+ f.clean('1 1:15:30.3')
+ )
+
+ def test_durationfield_2(self):
+ class DurationForm(Form):
+ duration = DurationField(initial=datetime.timedelta(hours=1))
+ f = DurationForm()
+ self.assertHTMLEqual(
+ '<input id="id_duration" type="text" name="duration" value="01:00:00">',
+ str(f['duration'])
+ )
+
def test_regexfield_1(self):
f = RegexField('^[0-9][A-F][0-9]$')
self.assertEqual('2A2', f.clean('2A2'))
diff --git a/tests/model_fields/models.py b/tests/model_fields/models.py
index bb75208905..e9e287f04f 100644
--- a/tests/model_fields/models.py
+++ b/tests/model_fields/models.py
@@ -121,6 +121,10 @@ class DateTimeModel(models.Model):
t = models.TimeField()
+class DurationModel(models.Model):
+ field = models.DurationField()
+
+
class PrimaryKeyCharModel(models.Model):
string = models.CharField(max_length=10, primary_key=True)
diff --git a/tests/model_fields/test_durationfield.py b/tests/model_fields/test_durationfield.py
new file mode 100644
index 0000000000..fc2c22af61
--- /dev/null
+++ b/tests/model_fields/test_durationfield.py
@@ -0,0 +1,67 @@
+import datetime
+import json
+
+from django.core import exceptions, serializers
+from django.db import models
+from django.test import TestCase
+
+from .models import DurationModel
+
+
+class TestSaveLoad(TestCase):
+
+ def test_simple_roundtrip(self):
+ duration = datetime.timedelta(days=123, seconds=123, microseconds=123)
+ DurationModel.objects.create(field=duration)
+ loaded = DurationModel.objects.get()
+ self.assertEqual(loaded.field, duration)
+
+
+class TestQuerying(TestCase):
+
+ @classmethod
+ def setUpTestData(cls):
+ cls.objs = [
+ DurationModel.objects.create(field=datetime.timedelta(days=1)),
+ DurationModel.objects.create(field=datetime.timedelta(seconds=1)),
+ DurationModel.objects.create(field=datetime.timedelta(seconds=-1)),
+ ]
+
+ def test_exact(self):
+ self.assertSequenceEqual(
+ DurationModel.objects.filter(field=datetime.timedelta(days=1)),
+ [self.objs[0]]
+ )
+
+ def test_gt(self):
+ self.assertSequenceEqual(
+ DurationModel.objects.filter(field__gt=datetime.timedelta(days=0)),
+ [self.objs[0], self.objs[1]]
+ )
+
+
+class TestSerialization(TestCase):
+ test_data = '[{"fields": {"field": "1 01:00:00"}, "model": "model_fields.durationmodel", "pk": null}]'
+
+ def test_dumping(self):
+ instance = DurationModel(field=datetime.timedelta(days=1, hours=1))
+ data = serializers.serialize('json', [instance])
+ self.assertEqual(json.loads(data), json.loads(self.test_data))
+
+ def test_loading(self):
+ instance = list(serializers.deserialize('json', self.test_data))[0].object
+ self.assertEqual(instance.field, datetime.timedelta(days=1, hours=1))
+
+
+class TestValidation(TestCase):
+
+ def test_invalid_string(self):
+ field = models.DurationField()
+ with self.assertRaises(exceptions.ValidationError) as cm:
+ field.clean('not a datetime', None)
+ self.assertEqual(cm.exception.code, 'invalid')
+ self.assertEqual(
+ cm.exception.message % cm.exception.params,
+ "'not a datetime' value has an invalid format. "
+ "It must be in [DD] [HH:[MM:]]ss[.uuuuuu] format."
+ )
diff --git a/tests/utils_tests/test_dateparse.py b/tests/utils_tests/test_dateparse.py
index cdf91c039e..a224e3b174 100644
--- a/tests/utils_tests/test_dateparse.py
+++ b/tests/utils_tests/test_dateparse.py
@@ -1,9 +1,9 @@
from __future__ import unicode_literals
-from datetime import date, time, datetime
+from datetime import date, time, datetime, timedelta
import unittest
-from django.utils.dateparse import parse_date, parse_time, parse_datetime
+from django.utils.dateparse import parse_date, parse_time, parse_datetime, parse_duration
from django.utils.timezone import get_fixed_timezone
@@ -46,3 +46,43 @@ class DateParseTests(unittest.TestCase):
# Invalid inputs
self.assertEqual(parse_datetime('20120423091500'), None)
self.assertRaises(ValueError, parse_datetime, '2012-04-56T09:15:90')
+
+
+class DurationParseTests(unittest.TestCase):
+ def test_seconds(self):
+ self.assertEqual(parse_duration('30'), timedelta(seconds=30))
+
+ def test_minutes_seconds(self):
+ self.assertEqual(parse_duration('15:30'), timedelta(minutes=15, seconds=30))
+ self.assertEqual(parse_duration('5:30'), timedelta(minutes=5, seconds=30))
+
+ def test_hours_minutes_seconds(self):
+ self.assertEqual(parse_duration('10:15:30'), timedelta(hours=10, minutes=15, seconds=30))
+ self.assertEqual(parse_duration('1:15:30'), timedelta(hours=1, minutes=15, seconds=30))
+ self.assertEqual(parse_duration('100:200:300'), timedelta(hours=100, minutes=200, seconds=300))
+
+ def test_days(self):
+ self.assertEqual(parse_duration('4 15:30'), timedelta(days=4, minutes=15, seconds=30))
+ self.assertEqual(parse_duration('4 10:15:30'), timedelta(days=4, hours=10, minutes=15, seconds=30))
+
+ def test_fractions_of_seconds(self):
+ self.assertEqual(parse_duration('15:30.1'), timedelta(minutes=15, seconds=30, milliseconds=100))
+ self.assertEqual(parse_duration('15:30.01'), timedelta(minutes=15, seconds=30, milliseconds=10))
+ self.assertEqual(parse_duration('15:30.001'), timedelta(minutes=15, seconds=30, milliseconds=1))
+ self.assertEqual(parse_duration('15:30.0001'), timedelta(minutes=15, seconds=30, microseconds=100))
+ self.assertEqual(parse_duration('15:30.00001'), timedelta(minutes=15, seconds=30, microseconds=10))
+ self.assertEqual(parse_duration('15:30.000001'), timedelta(minutes=15, seconds=30, microseconds=1))
+
+ def test_negative(self):
+ self.assertEqual(parse_duration('-4 15:30'), timedelta(days=-4, minutes=15, seconds=30))
+
+ def test_iso_8601(self):
+ self.assertEqual(parse_duration('P4Y'), None)
+ self.assertEqual(parse_duration('P4M'), None)
+ self.assertEqual(parse_duration('P4W'), None)
+ self.assertEqual(parse_duration('P4D'), timedelta(days=4))
+ self.assertEqual(parse_duration('P0.5D'), timedelta(hours=12))
+ self.assertEqual(parse_duration('PT5H'), timedelta(hours=5))
+ self.assertEqual(parse_duration('PT5M'), timedelta(minutes=5))
+ self.assertEqual(parse_duration('PT5S'), timedelta(seconds=5))
+ self.assertEqual(parse_duration('PT0.000005S'), timedelta(microseconds=5))
diff --git a/tests/utils_tests/test_duration.py b/tests/utils_tests/test_duration.py
new file mode 100644
index 0000000000..559d2ef16f
--- /dev/null
+++ b/tests/utils_tests/test_duration.py
@@ -0,0 +1,43 @@
+import datetime
+import unittest
+
+from django.utils.dateparse import parse_duration
+from django.utils.duration import duration_string
+
+
+class TestDurationString(unittest.TestCase):
+
+ def test_simple(self):
+ duration = datetime.timedelta(hours=1, minutes=3, seconds=5)
+ self.assertEqual(duration_string(duration), '01:03:05')
+
+ def test_days(self):
+ duration = datetime.timedelta(days=1, hours=1, minutes=3, seconds=5)
+ self.assertEqual(duration_string(duration), '1 01:03:05')
+
+ def test_microseconds(self):
+ duration = datetime.timedelta(hours=1, minutes=3, seconds=5, microseconds=12345)
+ self.assertEqual(duration_string(duration), '01:03:05.012345')
+
+ def test_negative(self):
+ duration = datetime.timedelta(days=-1, hours=1, minutes=3, seconds=5)
+ self.assertEqual(duration_string(duration), '-1 01:03:05')
+
+
+class TestParseDurationRoundtrip(unittest.TestCase):
+
+ def test_simple(self):
+ duration = datetime.timedelta(hours=1, minutes=3, seconds=5)
+ self.assertEqual(parse_duration(duration_string(duration)), duration)
+
+ def test_days(self):
+ duration = datetime.timedelta(days=1, hours=1, minutes=3, seconds=5)
+ self.assertEqual(parse_duration(duration_string(duration)), duration)
+
+ def test_microseconds(self):
+ duration = datetime.timedelta(hours=1, minutes=3, seconds=5, microseconds=12345)
+ self.assertEqual(parse_duration(duration_string(duration)), duration)
+
+ def test_negative(self):
+ duration = datetime.timedelta(days=-1, hours=1, minutes=3, seconds=5)
+ self.assertEqual(parse_duration(duration_string(duration)), duration)