Django-rest-framework: DurationField with choices

Created on 21 Aug 2016  路  10Comments  路  Source: encode/django-rest-framework

I might be missing something, but there seems to be some inconsistencies with using choices for a DurationField. Without specifying a DRF DurationField in the serializer, it serializes out as a float, but does not accept those float values back on save. When you specify a DRF DurationField, it serializes out according to the docs, but does not add any choices in an OPTIONS request.

djangorestframework==3.4.5, python2.7 with Postgres

Steps to reproduce

Create a model with a duration field that has choices:

class MyModel(models.Model):
    EMAIL_FREQUENCY_CHOICES = (
        (datetime.timedelta(days=1), 'Daily'),
        (datetime.timedelta(weeks=1), 'Weekly'),
        (datetime.timedelta(weeks=4), 'Monthly'),
    )

    email_frequency = models.DurationField(blank=True, null=True, choices=EMAIL_FREQUENCY_CHOICES)

and a corresponding model serializer (and a viewset, omitted):

class MyModelSerializer(serializers.ModelSerializer):
    class Meta:
        model = MyModel
        fields = ('__all__')

Then make an OPTIONS request to see the serialized choices:

"...",
"email_frequency": {
    "...",
    "choices": [
        { "display_name": "Daily", "value": "86400.0" },
        { "display_name": "Weekly", "value": "604800.0" },
        { "display_name": "Monthly", "value": "2419200.0" }
    ]
},

Try to POST or PUT with one of the float values for the field. I get an error:

"email_frequency": [
    "'604800.0' is not a valid choice."
]

If you change the serializer to use a DRF DurationField:

class MyModelSerializer(serializers.ModelSerializer):
    class Meta:
        model = MyModel
        fields = ('__all__')
    email_frequency = serializers.DurationField(allow_null=True)

It serializes out the value on a GET according to the docs: 1 00:00:00, but then when calling OPTIONS, nothing appears in the choices array for the field.

I'm not sure if this is a bug or if I'm supposed to be using a ChoiceField.

Expected behavior

I would assume DRF would pick up on choices for a native DurationField, serialize per the docs as [DD] [HH:[MM:]]ss[.uuuuuu], and provide the choices in the same format on OPTIONS.

Actual behavior

Serializers give out floats, choices in OPTIONS are floats, but the serializers won't accept the float values.

Specifying a DRF DurationField in the serializer doesn't pick up on the choices in OPTIONS.

Bug

Most helpful comment

In recent versions of DRF (I'm pretty sure it used to work), a field with choices other than a text field fails validation, even for simple types like decimal.
(if this should be considered a separate issue, please tell me and I'll create a new one).

Internal Server Error: /api/v1/crm/events/11/
Traceback (most recent call last):
  File "/home/emma/dev/levit_intanet/venv/lib/python3.5/site-packages/django/core/handlers/exception.py", line 39, in inner
    response = get_response(request)
  File "/home/emma/dev/levit_intanet/venv/lib/python3.5/site-packages/django/core/handlers/base.py", line 249, in _legacy_get_response
    response = self._get_response(request)
  File "/home/emma/dev/levit_intanet/venv/lib/python3.5/site-packages/django/core/handlers/base.py", line 187, in _get_response
    response = self.process_exception_by_middleware(e, request)
  File "/home/emma/dev/levit_intanet/venv/lib/python3.5/site-packages/django/core/handlers/base.py", line 185, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/usr/lib/python3.5/contextlib.py", line 30, in inner
    return func(*args, **kwds)
  File "/home/emma/dev/levit_intanet/venv/lib/python3.5/site-packages/django/views/decorators/csrf.py", line 58, in wrapped_view
    return view_func(*args, **kwargs)
  File "/home/emma/dev/levit_intanet/venv/lib/python3.5/site-packages/rest_framework/viewsets.py", line 86, in view
    return self.dispatch(request, *args, **kwargs)
  File "/home/emma/dev/levit_intanet/venv/lib/python3.5/site-packages/rest_framework/views.py", line 489, in dispatch
    response = self.handle_exception(exc)
  File "/home/emma/dev/levit_intanet/venv/lib/python3.5/site-packages/rest_framework/views.py", line 449, in handle_exception
    self.raise_uncaught_exception(exc)
  File "/home/emma/dev/levit_intanet/venv/lib/python3.5/site-packages/rest_framework/views.py", line 486, in dispatch
    response = handler(request, *args, **kwargs)
  File "/home/emma/dev/levit_intanet/venv/lib/python3.5/site-packages/rest_framework/mixins.py", line 69, in update
    serializer.is_valid(raise_exception=True)
  File "/home/emma/dev/levit_intanet/venv/lib/python3.5/site-packages/rest_framework/serializers.py", line 237, in is_valid
    self._validated_data = self.run_validation(self.initial_data)
  File "/home/emma/dev/levit_intanet/venv/lib/python3.5/site-packages/rest_framework/serializers.py", line 432, in run_validation
    value = self.to_internal_value(data)
  File "/home/emma/dev/levit_intanet/venv/lib/python3.5/site-packages/rest_framework/serializers.py", line 462, in to_internal_value
    validated_value = field.run_validation(primitive_value)
  File "/home/emma/dev/levit_intanet/venv/lib/python3.5/site-packages/rest_framework/fields.py", line 526, in run_validation
    self.run_validators(value)
  File "/home/emma/dev/levit_intanet/venv/lib/python3.5/site-packages/rest_framework/fields.py", line 540, in run_validators
    validator(value)
  File "/home/emma/dev/levit_intanet/venv/lib/python3.5/site-packages/django/core/validators.py", line 417, in __call__
    digit_tuple, exponent = value.as_tuple()[1:]
AttributeError: 'int' object has no attribute 'as_tuple'

All 10 comments

Okay. This issue will come down to ChoiceField not having a type itself, so it ends up being suitable for any primitive types (strings/integers/floats) but not necessarily for an input that ends up being coerced into a more complex type.

To work around this, you'd either need to subclass ChoiceField, overriding possible to_internal_value and/or to_representation in a DurationChoiceField class. Or else live with using DurationField, and not having the choices presented in OPTIONS requests.

We _might_ be able to deal with this by having a child field that can be attached to a ChoiceField, which handles coercing the choice into the underlying value, and serializing an internal value back into a primitive representation.

Any reason there isn't a TypedChoiceField?

No particular reason, no. First time this has been raised. That's a possibility.

Thanks for the options @tomchristie, I appreciate your quick responses to the issues :)

If I can figure out how to make a DRF TypedChoiceField, I'll send you a pull request.

In recent versions of DRF (I'm pretty sure it used to work), a field with choices other than a text field fails validation, even for simple types like decimal.
(if this should be considered a separate issue, please tell me and I'll create a new one).

Internal Server Error: /api/v1/crm/events/11/
Traceback (most recent call last):
  File "/home/emma/dev/levit_intanet/venv/lib/python3.5/site-packages/django/core/handlers/exception.py", line 39, in inner
    response = get_response(request)
  File "/home/emma/dev/levit_intanet/venv/lib/python3.5/site-packages/django/core/handlers/base.py", line 249, in _legacy_get_response
    response = self._get_response(request)
  File "/home/emma/dev/levit_intanet/venv/lib/python3.5/site-packages/django/core/handlers/base.py", line 187, in _get_response
    response = self.process_exception_by_middleware(e, request)
  File "/home/emma/dev/levit_intanet/venv/lib/python3.5/site-packages/django/core/handlers/base.py", line 185, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/usr/lib/python3.5/contextlib.py", line 30, in inner
    return func(*args, **kwds)
  File "/home/emma/dev/levit_intanet/venv/lib/python3.5/site-packages/django/views/decorators/csrf.py", line 58, in wrapped_view
    return view_func(*args, **kwargs)
  File "/home/emma/dev/levit_intanet/venv/lib/python3.5/site-packages/rest_framework/viewsets.py", line 86, in view
    return self.dispatch(request, *args, **kwargs)
  File "/home/emma/dev/levit_intanet/venv/lib/python3.5/site-packages/rest_framework/views.py", line 489, in dispatch
    response = self.handle_exception(exc)
  File "/home/emma/dev/levit_intanet/venv/lib/python3.5/site-packages/rest_framework/views.py", line 449, in handle_exception
    self.raise_uncaught_exception(exc)
  File "/home/emma/dev/levit_intanet/venv/lib/python3.5/site-packages/rest_framework/views.py", line 486, in dispatch
    response = handler(request, *args, **kwargs)
  File "/home/emma/dev/levit_intanet/venv/lib/python3.5/site-packages/rest_framework/mixins.py", line 69, in update
    serializer.is_valid(raise_exception=True)
  File "/home/emma/dev/levit_intanet/venv/lib/python3.5/site-packages/rest_framework/serializers.py", line 237, in is_valid
    self._validated_data = self.run_validation(self.initial_data)
  File "/home/emma/dev/levit_intanet/venv/lib/python3.5/site-packages/rest_framework/serializers.py", line 432, in run_validation
    value = self.to_internal_value(data)
  File "/home/emma/dev/levit_intanet/venv/lib/python3.5/site-packages/rest_framework/serializers.py", line 462, in to_internal_value
    validated_value = field.run_validation(primitive_value)
  File "/home/emma/dev/levit_intanet/venv/lib/python3.5/site-packages/rest_framework/fields.py", line 526, in run_validation
    self.run_validators(value)
  File "/home/emma/dev/levit_intanet/venv/lib/python3.5/site-packages/rest_framework/fields.py", line 540, in run_validators
    validator(value)
  File "/home/emma/dev/levit_intanet/venv/lib/python3.5/site-packages/django/core/validators.py", line 417, in __call__
    digit_tuple, exponent = value.as_tuple()[1:]
AttributeError: 'int' object has no attribute 'as_tuple'

I'm going to de-milestone this for now. I'd be really happy to see a PR making this work with TypedChoiceField (assuming that's the needed solution.)

I am going to tackle this.

Based on this PR https://github.com/django/django/pull/12449 Django is going to allow Dictionaries in choices & introduced Enum, what should be the best move to resolve this issue? any pointer?

In recent versions of DRF (I'm pretty sure it used to work), a field with choices other than a text field fails validation, even for simple types like decimal.
(if this should be considered a separate issue, please tell me and I'll create a new one).

Internal Server Error: /api/v1/crm/events/11/
Traceback (most recent call last):
  File "/home/emma/dev/levit_intanet/venv/lib/python3.5/site-packages/django/core/handlers/exception.py", line 39, in inner
    response = get_response(request)
  File "/home/emma/dev/levit_intanet/venv/lib/python3.5/site-packages/django/core/handlers/base.py", line 249, in _legacy_get_response
    response = self._get_response(request)
  File "/home/emma/dev/levit_intanet/venv/lib/python3.5/site-packages/django/core/handlers/base.py", line 187, in _get_response
    response = self.process_exception_by_middleware(e, request)
  File "/home/emma/dev/levit_intanet/venv/lib/python3.5/site-packages/django/core/handlers/base.py", line 185, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/usr/lib/python3.5/contextlib.py", line 30, in inner
    return func(*args, **kwds)
  File "/home/emma/dev/levit_intanet/venv/lib/python3.5/site-packages/django/views/decorators/csrf.py", line 58, in wrapped_view
    return view_func(*args, **kwargs)
  File "/home/emma/dev/levit_intanet/venv/lib/python3.5/site-packages/rest_framework/viewsets.py", line 86, in view
    return self.dispatch(request, *args, **kwargs)
  File "/home/emma/dev/levit_intanet/venv/lib/python3.5/site-packages/rest_framework/views.py", line 489, in dispatch
    response = self.handle_exception(exc)
  File "/home/emma/dev/levit_intanet/venv/lib/python3.5/site-packages/rest_framework/views.py", line 449, in handle_exception
    self.raise_uncaught_exception(exc)
  File "/home/emma/dev/levit_intanet/venv/lib/python3.5/site-packages/rest_framework/views.py", line 486, in dispatch
    response = handler(request, *args, **kwargs)
  File "/home/emma/dev/levit_intanet/venv/lib/python3.5/site-packages/rest_framework/mixins.py", line 69, in update
    serializer.is_valid(raise_exception=True)
  File "/home/emma/dev/levit_intanet/venv/lib/python3.5/site-packages/rest_framework/serializers.py", line 237, in is_valid
    self._validated_data = self.run_validation(self.initial_data)
  File "/home/emma/dev/levit_intanet/venv/lib/python3.5/site-packages/rest_framework/serializers.py", line 432, in run_validation
    value = self.to_internal_value(data)
  File "/home/emma/dev/levit_intanet/venv/lib/python3.5/site-packages/rest_framework/serializers.py", line 462, in to_internal_value
    validated_value = field.run_validation(primitive_value)
  File "/home/emma/dev/levit_intanet/venv/lib/python3.5/site-packages/rest_framework/fields.py", line 526, in run_validation
    self.run_validators(value)
  File "/home/emma/dev/levit_intanet/venv/lib/python3.5/site-packages/rest_framework/fields.py", line 540, in run_validators
    validator(value)
  File "/home/emma/dev/levit_intanet/venv/lib/python3.5/site-packages/django/core/validators.py", line 417, in __call__
    digit_tuple, exponent = value.as_tuple()[1:]
AttributeError: 'int' object has no attribute 'as_tuple'

can you share your recent thoughts on this?

already completed item still open

Was this page helpful?
0 / 5 - 0 ratings