DecimalField

This commit is contained in:
Tom Christie 2013-04-26 13:59:06 +01:00
commit d985aec3c9
6 changed files with 263 additions and 0 deletions

View File

@ -248,6 +248,12 @@ A floating point representation.
Corresponds to `django.db.models.fields.FloatField`. Corresponds to `django.db.models.fields.FloatField`.
## DecimalField
A decimal representation.
Corresponds to `django.db.models.fields.DecimalField`.
## FileField ## FileField
A file representation. Performs Django's standard FileField validation. A file representation. Performs Django's standard FileField validation.

View File

@ -118,6 +118,12 @@ And would have the following entry in the urlconf:
Usage of the old-style attributes continues to be supported, but will raise a `PendingDeprecationWarning`. Usage of the old-style attributes continues to be supported, but will raise a `PendingDeprecationWarning`.
## DecimalField
2.3 introduces a `DecimalField` serializer field, which returns `Decimal` instances.
For most cases APIs using model fields will behave as previously, however if you are using a custom renderer, not provided by REST framework, then you may now need to add support for rendering `Decimal` instances to your renderer implmentation.
--- ---
# Other notes # Other notes

View File

@ -38,6 +38,20 @@ You can determine your currently installed version using `pip freeze`:
--- ---
## 2.3.x series
### 2.3.0
* ViewSets and Routers.
* ModelSerializers support reverse relations in 'fields' option.
* HyperLinkedModelSerializers support 'id' field in 'fields' option.
* Cleaner generic views.
* DecimalField support.
**Note**: See the [2.3 announcement][2.3-announcement] for full details.
---
## 2.2.x series ## 2.2.x series
### 2.2.7 ### 2.2.7
@ -458,6 +472,7 @@ This change will not affect user code, so long as it's following the recommended
[django-deprecation-policy]: https://docs.djangoproject.com/en/dev/internals/release-process/#internal-release-deprecation-policy [django-deprecation-policy]: https://docs.djangoproject.com/en/dev/internals/release-process/#internal-release-deprecation-policy
[defusedxml-announce]: http://blog.python.org/2013/02/announcing-defusedxml-fixes-for-xml.html [defusedxml-announce]: http://blog.python.org/2013/02/announcing-defusedxml-fixes-for-xml.html
[2.2-announcement]: 2.2-announcement.md [2.2-announcement]: 2.2-announcement.md
[2.3-announcement]: 2.3-announcement.md
[743]: https://github.com/tomchristie/django-rest-framework/pull/743 [743]: https://github.com/tomchristie/django-rest-framework/pull/743
[staticfiles14]: https://docs.djangoproject.com/en/1.4/howto/static-files/#with-a-template-tag [staticfiles14]: https://docs.djangoproject.com/en/1.4/howto/static-files/#with-a-template-tag
[staticfiles13]: https://docs.djangoproject.com/en/1.3/howto/static-files/#with-a-template-tag [staticfiles13]: https://docs.djangoproject.com/en/1.3/howto/static-files/#with-a-template-tag

View File

@ -7,6 +7,7 @@ from __future__ import unicode_literals
import copy import copy
import datetime import datetime
from decimal import Decimal, DecimalException
import inspect import inspect
import re import re
import warnings import warnings
@ -726,6 +727,75 @@ class FloatField(WritableField):
raise ValidationError(msg) raise ValidationError(msg)
class DecimalField(WritableField):
type_name = 'DecimalField'
form_field_class = forms.DecimalField
default_error_messages = {
'invalid': _('Enter a number.'),
'max_value': _('Ensure this value is less than or equal to %(limit_value)s.'),
'min_value': _('Ensure this value is greater than or equal to %(limit_value)s.'),
'max_digits': _('Ensure that there are no more than %s digits in total.'),
'max_decimal_places': _('Ensure that there are no more than %s decimal places.'),
'max_whole_digits': _('Ensure that there are no more than %s digits before the decimal point.')
}
def __init__(self, max_value=None, min_value=None, max_digits=None, decimal_places=None, *args, **kwargs):
self.max_value, self.min_value = max_value, min_value
self.max_digits, self.decimal_places = max_digits, decimal_places
super(DecimalField, self).__init__(*args, **kwargs)
if max_value is not None:
self.validators.append(validators.MaxValueValidator(max_value))
if min_value is not None:
self.validators.append(validators.MinValueValidator(min_value))
def from_native(self, value):
"""
Validates that the input is a decimal number. Returns a Decimal
instance. Returns None for empty values. Ensures that there are no more
than max_digits in the number, and no more than decimal_places digits
after the decimal point.
"""
if value in validators.EMPTY_VALUES:
return None
value = smart_text(value).strip()
try:
value = Decimal(value)
except DecimalException:
raise ValidationError(self.error_messages['invalid'])
return value
def validate(self, value):
super(DecimalField, self).validate(value)
if value in validators.EMPTY_VALUES:
return
# Check for NaN, Inf and -Inf values. We can't compare directly for NaN,
# since it is never equal to itself. However, NaN is the only value that
# isn't equal to itself, so we can use this to identify NaN
if value != value or value == Decimal("Inf") or value == Decimal("-Inf"):
raise ValidationError(self.error_messages['invalid'])
sign, digittuple, exponent = value.as_tuple()
decimals = abs(exponent)
# digittuple doesn't include any leading zeros.
digits = len(digittuple)
if decimals > digits:
# We have leading zeros up to or past the decimal point. Count
# everything past the decimal point as a digit. We do not count
# 0 before the decimal point as a digit since that would mean
# we would not allow max_digits = decimal_places.
digits = decimals
whole_digits = digits - decimals
if self.max_digits is not None and digits > self.max_digits:
raise ValidationError(self.error_messages['max_digits'] % self.max_digits)
if self.decimal_places is not None and decimals > self.decimal_places:
raise ValidationError(self.error_messages['max_decimal_places'] % self.decimal_places)
if self.max_digits is not None and self.decimal_places is not None and whole_digits > (self.max_digits - self.decimal_places):
raise ValidationError(self.error_messages['max_whole_digits'] % (self.max_digits - self.decimal_places))
return value
class FileField(WritableField): class FileField(WritableField):
use_files = True use_files = True
type_name = 'FileField' type_name = 'FileField'

View File

@ -560,6 +560,7 @@ class ModelSerializer(Serializer):
models.DateTimeField: DateTimeField, models.DateTimeField: DateTimeField,
models.DateField: DateField, models.DateField: DateField,
models.TimeField: TimeField, models.TimeField: TimeField,
models.DecimalField: DecimalField,
models.EmailField: EmailField, models.EmailField: EmailField,
models.CharField: CharField, models.CharField: CharField,
models.URLField: URLField, models.URLField: URLField,

View File

@ -3,12 +3,14 @@ General serializer field tests.
""" """
from __future__ import unicode_literals from __future__ import unicode_literals
import datetime import datetime
from decimal import Decimal
from django.db import models from django.db import models
from django.test import TestCase from django.test import TestCase
from django.core import validators from django.core import validators
from rest_framework import serializers from rest_framework import serializers
from rest_framework.serializers import Serializer
class TimestampedModel(models.Model): class TimestampedModel(models.Model):
@ -481,3 +483,166 @@ class TimeFieldTest(TestCase):
self.assertEqual('04 - 00 [000000]', result_1) self.assertEqual('04 - 00 [000000]', result_1)
self.assertEqual('04 - 59 [000000]', result_2) self.assertEqual('04 - 59 [000000]', result_2)
self.assertEqual('04 - 59 [000200]', result_3) self.assertEqual('04 - 59 [000200]', result_3)
class DecimalFieldTest(TestCase):
"""
Tests for the DecimalField from_native() and to_native() behavior
"""
def test_from_native_string(self):
"""
Make sure from_native() accepts string values
"""
f = serializers.DecimalField()
result_1 = f.from_native('9000')
result_2 = f.from_native('1.00000001')
self.assertEqual(Decimal('9000'), result_1)
self.assertEqual(Decimal('1.00000001'), result_2)
def test_from_native_invalid_string(self):
"""
Make sure from_native() raises ValidationError on passing invalid string
"""
f = serializers.DecimalField()
try:
f.from_native('123.45.6')
except validators.ValidationError as e:
self.assertEqual(e.messages, ["Enter a number."])
else:
self.fail("ValidationError was not properly raised")
def test_from_native_integer(self):
"""
Make sure from_native() accepts integer values
"""
f = serializers.DecimalField()
result = f.from_native(9000)
self.assertEqual(Decimal('9000'), result)
def test_from_native_float(self):
"""
Make sure from_native() accepts float values
"""
f = serializers.DecimalField()
result = f.from_native(1.00000001)
self.assertEqual(Decimal('1.00000001'), result)
def test_from_native_empty(self):
"""
Make sure from_native() returns None on empty param.
"""
f = serializers.DecimalField()
result = f.from_native('')
self.assertEqual(result, None)
def test_from_native_none(self):
"""
Make sure from_native() returns None on None param.
"""
f = serializers.DecimalField()
result = f.from_native(None)
self.assertEqual(result, None)
def test_to_native(self):
"""
Make sure to_native() returns Decimal as string.
"""
f = serializers.DecimalField()
result_1 = f.to_native(Decimal('9000'))
result_2 = f.to_native(Decimal('1.00000001'))
self.assertEqual(Decimal('9000'), result_1)
self.assertEqual(Decimal('1.00000001'), result_2)
def test_to_native_none(self):
"""
Make sure from_native() returns None on None param.
"""
f = serializers.DecimalField(required=False)
self.assertEqual(None, f.to_native(None))
def test_valid_serialization(self):
"""
Make sure the serializer works correctly
"""
class DecimalSerializer(Serializer):
decimal_field = serializers.DecimalField(max_value=9010,
min_value=9000,
max_digits=6,
decimal_places=2)
self.assertTrue(DecimalSerializer(data={'decimal_field': '9001'}).is_valid())
self.assertTrue(DecimalSerializer(data={'decimal_field': '9001.2'}).is_valid())
self.assertTrue(DecimalSerializer(data={'decimal_field': '9001.23'}).is_valid())
self.assertFalse(DecimalSerializer(data={'decimal_field': '8000'}).is_valid())
self.assertFalse(DecimalSerializer(data={'decimal_field': '9900'}).is_valid())
self.assertFalse(DecimalSerializer(data={'decimal_field': '9001.234'}).is_valid())
def test_raise_max_value(self):
"""
Make sure max_value violations raises ValidationError
"""
class DecimalSerializer(Serializer):
decimal_field = serializers.DecimalField(max_value=100)
s = DecimalSerializer(data={'decimal_field': '123'})
self.assertFalse(s.is_valid())
self.assertEqual(s.errors, {'decimal_field': ['Ensure this value is less than or equal to 100.']})
def test_raise_min_value(self):
"""
Make sure min_value violations raises ValidationError
"""
class DecimalSerializer(Serializer):
decimal_field = serializers.DecimalField(min_value=100)
s = DecimalSerializer(data={'decimal_field': '99'})
self.assertFalse(s.is_valid())
self.assertEqual(s.errors, {'decimal_field': ['Ensure this value is greater than or equal to 100.']})
def test_raise_max_digits(self):
"""
Make sure max_digits violations raises ValidationError
"""
class DecimalSerializer(Serializer):
decimal_field = serializers.DecimalField(max_digits=5)
s = DecimalSerializer(data={'decimal_field': '123.456'})
self.assertFalse(s.is_valid())
self.assertEqual(s.errors, {'decimal_field': ['Ensure that there are no more than 5 digits in total.']})
def test_raise_max_decimal_places(self):
"""
Make sure max_decimal_places violations raises ValidationError
"""
class DecimalSerializer(Serializer):
decimal_field = serializers.DecimalField(decimal_places=3)
s = DecimalSerializer(data={'decimal_field': '123.4567'})
self.assertFalse(s.is_valid())
self.assertEqual(s.errors, {'decimal_field': ['Ensure that there are no more than 3 decimal places.']})
def test_raise_max_whole_digits(self):
"""
Make sure max_whole_digits violations raises ValidationError
"""
class DecimalSerializer(Serializer):
decimal_field = serializers.DecimalField(max_digits=4, decimal_places=3)
s = DecimalSerializer(data={'decimal_field': '12345.6'})
self.assertFalse(s.is_valid())
self.assertEqual(s.errors, {'decimal_field': ['Ensure that there are no more than 4 digits in total.']})