From 2f6179ccfe92dbcaeb2dcbfee3cb6a3fe667f4a1 Mon Sep 17 00:00:00 2001 From: vladkhard Date: Wed, 18 Oct 2017 12:05:30 +0300 Subject: [PATCH 1/2] change Item.quantity to DecimalType --- openregistry/api/models/ocds.py | 32 +++++++++++++++++++++++++++++--- openregistry/api/tests/models.py | 27 +++++++++++++++++++++++++-- 2 files changed, 54 insertions(+), 5 deletions(-) diff --git a/openregistry/api/models/ocds.py b/openregistry/api/models/ocds.py index bdb0f36..5683898 100644 --- a/openregistry/api/models/ocds.py +++ b/openregistry/api/models/ocds.py @@ -2,11 +2,14 @@ from uuid import uuid4 from schematics.types import (StringType, FloatType, URLType, IntType, - BooleanType, BaseType, EmailType, MD5Type) -from schematics.exceptions import ValidationError + BooleanType, BaseType, EmailType, MD5Type, + DecimalType as BaseDecimalType) +from schematics.exceptions import ValidationError, ConversionError from schematics.types.compound import ModelType, ListType from schematics.types.serializable import serializable +from decimal import Decimal, InvalidOperation, ROUND_HALF_UP + from openregistry.api.constants import (DEFAULT_CURRENCY, DEFAULT_ITEM_CLASSIFICATION, ITEM_CLASSIFICATIONS, DOCUMENT_TYPES, IDENTIFIER_CODES, DEBTOR_TYPES @@ -147,6 +150,29 @@ class Identifier(Model): uri = URLType() # A URI to identify the organization. +class DecimalType(BaseDecimalType): + + def __init__(self, precision=-3, **kwargs): + self.precision = Decimal("1E{:d}".format(precision)) + super(DecimalType, self).__init__(**kwargs) + + def to_primitive(self, value, context=None): + return value + + def to_native(self, value, context=None): + try: + value = Decimal(value).quantize(self.precision, rounding=ROUND_HALF_UP).normalize() + except (TypeError, InvalidOperation): + raise ConversionError(self.messages['number_coerce'].format(value)) + + if self.min_value is not None and value < self.min_value: + raise ConversionError(self.messages['number_min'].format(value)) + if self.max_value is not None and self.max_value < value: + raise ConversionError(self.messages['number_max'].format(value)) + + return value + + class Item(Model): """A good, service, or work to be contracted.""" id = StringType(required=True, min_length=1, default=lambda: uuid4().hex) @@ -156,7 +182,7 @@ class Item(Model): classification = ModelType(ItemClassification) additionalClassifications = ListType(ModelType(Classification), default=list()) unit = ModelType(Unit) # Description of the unit which the good comes in e.g. hours, kilograms - quantity = IntType() # The number of units required + quantity = DecimalType() # The number of units required address = ModelType(Address) location = ModelType(Location) diff --git a/openregistry/api/tests/models.py b/openregistry/api/tests/models.py index 17cf8b6..d12bfe5 100644 --- a/openregistry/api/tests/models.py +++ b/openregistry/api/tests/models.py @@ -3,6 +3,7 @@ import mock from datetime import datetime, timedelta from schematics.exceptions import ConversionError, ValidationError, ModelValidationError +from decimal import Decimal from openregistry.api.utils import get_now @@ -12,7 +13,7 @@ from openregistry.api.models.ocds import ( Organization, ContactPoint, Identifier, Address, Item, Location, Unit, Value, ItemClassification, Classification, - Period, PeriodEndRequired, Document + Period, PeriodEndRequired, Document, DecimalType ) @@ -45,6 +46,28 @@ def test_IsoDateTimeType_model(self): with self.assertRaises(ConversionError): dt.to_native(dt.to_primitive(date)) + def test_DecimalType_model(self): + number = '5.001' + + dt = DecimalType() + + value = dt.to_primitive(number) + self.assertEqual(Decimal(number), dt.to_native(number)) + self.assertEqual(Decimal(number), dt.to_native(value)) + + for number in (None, '5,5'): + with self.assertRaisesRegexp(ConversionError, u"Number '{}' failed to convert to a decimal.".format(number)): + dt.to_native(number) + + dt = DecimalType(precision=-3, min_value=Decimal('0'), max_value=Decimal('10.0')) + + self.assertEqual(Decimal('0.111'), dt.to_native('0.11111')) + self.assertEqual(Decimal('0.556'), dt.to_native('0.55555')) + + for number in ('-1.0', '11.0'): + with self.assertRaises(ConversionError): + dt.to_native(dt.to_primitive(number)) + def test_HashType_model(self): from uuid import uuid4 @@ -263,7 +286,7 @@ def test_Item_model(self): "name": u"item", "code": u"39513200-3" }, - "quantity": 5, + "quantity": Decimal('5.001'), "address": { "countryName": u"Україна", "postalCode": "79000", From 183f65344593c4fdcab71da0a54e4fe47a1286e6 Mon Sep 17 00:00:00 2001 From: vladkhard Date: Thu, 19 Oct 2017 13:12:36 +0300 Subject: [PATCH 2/2] fix DecimalType model test --- openregistry/api/tests/models.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/openregistry/api/tests/models.py b/openregistry/api/tests/models.py index d12bfe5..c5ea3e4 100644 --- a/openregistry/api/tests/models.py +++ b/openregistry/api/tests/models.py @@ -56,7 +56,7 @@ def test_DecimalType_model(self): self.assertEqual(Decimal(number), dt.to_native(value)) for number in (None, '5,5'): - with self.assertRaisesRegexp(ConversionError, u"Number '{}' failed to convert to a decimal.".format(number)): + with self.assertRaisesRegexp(ConversionError, dt.messages['number_coerce'].format(number)): dt.to_native(number) dt = DecimalType(precision=-3, min_value=Decimal('0'), max_value=Decimal('10.0')) @@ -64,9 +64,12 @@ def test_DecimalType_model(self): self.assertEqual(Decimal('0.111'), dt.to_native('0.11111')) self.assertEqual(Decimal('0.556'), dt.to_native('0.55555')) - for number in ('-1.0', '11.0'): - with self.assertRaises(ConversionError): - dt.to_native(dt.to_primitive(number)) + number = '-1.1' + with self.assertRaisesRegexp(ConversionError, dt.messages['number_min'].format(number)): + dt.to_native(dt.to_primitive(number)) + number = '11.1' + with self.assertRaisesRegexp(ConversionError, dt.messages['number_max'].format(number)): + dt.to_native(dt.to_primitive(number)) def test_HashType_model(self): from uuid import uuid4