diff options
| author | Marc Tamlyn <marc.tamlyn@gmail.com> | 2014-07-24 13:57:24 +0100 |
|---|---|---|
| committer | Marc Tamlyn <marc.tamlyn@gmail.com> | 2014-12-20 18:28:29 +0000 |
| commit | 57554442fe3e209c135e15dda4ea45123e579e58 (patch) | |
| tree | 0ef2cb0e3048d13b82e4c7e81192df6124556a44 /tests | |
| parent | a3d96bee36040975ded8e3bf02e33e48d06f1f16 (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.py | 1 | ||||
| -rw-r--r-- | tests/expressions/tests.py | 73 | ||||
| -rw-r--r-- | tests/forms_tests/tests/test_fields.py | 37 | ||||
| -rw-r--r-- | tests/model_fields/models.py | 4 | ||||
| -rw-r--r-- | tests/model_fields/test_durationfield.py | 67 | ||||
| -rw-r--r-- | tests/utils_tests/test_dateparse.py | 44 | ||||
| -rw-r--r-- | tests/utils_tests/test_duration.py | 43 |
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) |
