From bcc4ebfa81507da96158bfe8393b1188892ddf40 Mon Sep 17 00:00:00 2001 From: sevdog Date: Fri, 24 Jun 2022 10:59:20 +0200 Subject: [PATCH] Allow format duration as ISO-8601 --- docs/api-guide/fields.md | 9 ++++++--- docs/api-guide/settings.md | 9 +++++++++ rest_framework/__init__.py | 1 + rest_framework/fields.py | 13 +++++++++++-- rest_framework/settings.py | 4 +++- tests/test_fields.py | 25 ++++++++++++++++++++++++- 6 files changed, 54 insertions(+), 7 deletions(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 94b6e7c21..614366bc4 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -377,13 +377,16 @@ A Duration representation. Corresponds to `django.db.models.fields.DurationField` The `validated_data` for these fields will contain a `datetime.timedelta` instance. -The representation is a string following this format `'[DD] [HH:[MM:]]ss[.uuuuuu]'`. -**Signature:** `DurationField(max_value=None, min_value=None)` +**Signature:** `DurationField(format=api_settings.DURATION_FORMAT, max_value=None, min_value=None)` +* `format` - A string representing the output format. If not specified, this defaults to the same value as the `DURATION_FORMAT` settings key, which will be `'standard'` unless set. Setting to a format string indicates that `to_representation` return values should be coerced to string output. Format strings are described below. Setting this value to `None` indicates that Python `timedelta` objects should be returned by `to_representation`. In this case the date encoding will be determined by the renderer. * `max_value` Validate that the duration provided is no greater than this value. * `min_value` Validate that the duration provided is no less than this value. +#### `DurationField` format strings +Format strings may either be the special string `'iso-8601'`, which indicates that [ISO 8601][iso8601] style intervals should be used (eg `'P4DT1H15M20S'`), or the special string `'standard'`, which indicates that Django interval format `'[DD] [HH:[MM:]]ss[.uuuuuu]'` should be used (eg: `'4 1:15:20'`). + --- # Choice selection fields @@ -552,7 +555,7 @@ For further examples on `HiddenField` see the [validators](validators.md) docume --- -**Note:** `HiddenField()` does not appear in `partial=True` serializer (when making `PATCH` request). This behavior might change in future, follow updates on [github discussion](https://github.com/encode/django-rest-framework/discussions/8259). +**Note:** `HiddenField()` does not appear in `partial=True` serializer (when making `PATCH` request). This behavior might change in future, follow updates on [github discussion](https://github.com/encode/django-rest-framework/discussions/8259). --- diff --git a/docs/api-guide/settings.md b/docs/api-guide/settings.md index 47e2ce993..5e13a994f 100644 --- a/docs/api-guide/settings.md +++ b/docs/api-guide/settings.md @@ -314,6 +314,15 @@ May be a list including the string `'iso-8601'` or Python [strftime format][strf Default: `['iso-8601']` + +#### DURATION_FORMAT + +A format string that should be used by default for rendering the output of `DurationField` serializer fields. If `None`, then `DurationField` serializer fields will return Python `timedelta` objects, and the duration encoding will be determined by the renderer. + +May be any of `None`, `'iso-8601'` or `'standard'` (the format accepted by `django.utils.dateparse.parse_duration`). + +Default: `'standard'` + --- ## Encodings diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index 636f0c8ad..18238b9fb 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -21,6 +21,7 @@ HTTP_HEADER_ENCODING = 'iso-8859-1' # Default datetime input and output formats ISO_8601 = 'iso-8601' +STD_DURATION = 'standard' class RemovedInDRF316Warning(DeprecationWarning): diff --git a/rest_framework/fields.py b/rest_framework/fields.py index cbc02e2c2..7e0041097 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -24,7 +24,7 @@ from django.utils import timezone from django.utils.dateparse import ( parse_date, parse_datetime, parse_duration, parse_time ) -from django.utils.duration import duration_string +from django.utils.duration import duration_iso_string, duration_string from django.utils.encoding import is_protected_type, smart_str from django.utils.formats import localize_input, sanitize_separators from django.utils.ipv6 import clean_ipv6_address @@ -1351,9 +1351,11 @@ class DurationField(Field): 'overflow': _('The number of days must be between {min_days} and {max_days}.'), } - def __init__(self, **kwargs): + def __init__(self, format=empty, **kwargs): self.max_value = kwargs.pop('max_value', None) self.min_value = kwargs.pop('min_value', None) + if format is not empty: + self.format = format super().__init__(**kwargs) if self.max_value is not None: message = lazy_format(self.error_messages['max_value'], max_value=self.max_value) @@ -1376,6 +1378,13 @@ class DurationField(Field): self.fail('invalid', format='[DD] [HH:[MM:]]ss[.uuuuuu]') def to_representation(self, value): + output_format = getattr(self, 'format', api_settings.DURATION_FORMAT) + + if output_format is None or isinstance(value, str): + return value + + if output_format.lower() == ISO_8601: + return duration_iso_string(value) return duration_string(value) diff --git a/rest_framework/settings.py b/rest_framework/settings.py index b0d7bacec..4b466e044 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -24,7 +24,7 @@ from django.conf import settings from django.core.signals import setting_changed from django.utils.module_loading import import_string -from rest_framework import ISO_8601 +from rest_framework import ISO_8601, STD_DURATION DEFAULTS = { # Base API policies @@ -109,6 +109,8 @@ DEFAULTS = { 'TIME_FORMAT': ISO_8601, 'TIME_INPUT_FORMATS': [ISO_8601], + 'DURATION_FORMAT': STD_DURATION, + # Encoding 'UNICODE_JSON': True, 'COMPACT_JSON': True, diff --git a/tests/test_fields.py b/tests/test_fields.py index 430681763..2ddee002e 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -1782,8 +1782,31 @@ class TestDurationField(FieldValues): field = serializers.DurationField() -# Choice types... +class TestNoOutputFormatDurationField(FieldValues): + """ + Values for `TimeField` with a no output format. + """ + valid_inputs = {} + invalid_inputs = {} + outputs = { + datetime.timedelta(1): datetime.timedelta(1) + } + field = serializers.DurationField(format=None) + +class TestISOOutputFormatDurationField(FieldValues): + """ + Values for `TimeField` with a custom output format. + """ + valid_inputs = {} + invalid_inputs = {} + outputs = { + datetime.timedelta(days=3, hours=8, minutes=32, seconds=1, microseconds=123): 'P3DT08H32M01.000123S' + } + field = serializers.DurationField(format='iso-8601') + + +# Choice types... class TestChoiceField(FieldValues): """ Valid and invalid values for `ChoiceField`.