From 8bbe714fa94f5e9e43ec44dc5a3a33e30fb57c24 Mon Sep 17 00:00:00 2001 From: Jouni Kuusisto Date: Thu, 16 Jul 2015 15:41:46 +0300 Subject: [PATCH] Human readable choice labels Extended IndexableField schema to have list of human readable choice labels alongside the actual choice values. Added few tests for choice labels and brief documentation for field choices. --- CHANGES | 5 ++++ documentation/schema.rst | 13 +++++++++ src/resource_api/schema.py | 24 +++++++++++++++-- src/tests/resource_schema_test.py | 44 +++++++++++++++++++++++++++++++ src/tests/schema_test.py | 21 ++++++++++++--- 5 files changed, 102 insertions(+), 5 deletions(-) diff --git a/CHANGES b/CHANGES index f2aedba..62d9248 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,8 @@ +3.2.0 2015-07-20 + + [Jouni Kuusisto ] + * MINOR: Implemented choice labels for IdexableField subclasses. + 3.1.1 2015-03-23 [Anton Berezin ] diff --git a/documentation/schema.rst b/documentation/schema.rst index 47a5660..29e86b1 100644 --- a/documentation/schema.rst +++ b/documentation/schema.rst @@ -1,4 +1,5 @@ .. _schema: +.. py:currentmodule:: resource_api Schema ====== @@ -36,6 +37,18 @@ readonly (bool=False) changeable (bool=False) if True field can be set during creation but cannot be change later on. User's birth date is a valid example. +Field choices +""""""""""""" + +For all fields inherit from :class:`schema.IndexableField`, it's possible to define list of possible +values as choices. When choices are defined for a field field value is only allowed to be set one of these, otherwise +:exc:`errors.ValidationError` is raised. + +To define human readable labels for the value choices, pass a list or tuple of +labels as the *choice_labels* attribute for the field. This is useful for scenarios such as automated UI generation. + +.. autoclass:: resource_api.schema.IndexableField + Primitive fields ---------------- diff --git a/src/resource_api/schema.py b/src/resource_api/schema.py index 7912c4e..523e9cc 100644 --- a/src/resource_api/schema.py +++ b/src/resource_api/schema.py @@ -190,8 +190,21 @@ def get_schema(self): class IndexableField(BaseSimpleField): + """ + Base class for most of the primitive fields. + + choices (list|tuple = None) + Predefined list of possible values for the field. ValidationError will be raised if field value doesn't + match any of the defined values. + choice_labels (list|tuple = None) + List of human readable labels for defined choice values. + The length of the list must match length of the defined choices. + invalid_choices (list|tuple = None) + List of invalid values for the field. ValidationError will be raised if the field value matches any of the + defined values. + """ - def __init__(self, choices=None, invalid_choices=None, **kwargs): + def __init__(self, choices=None, choice_labels=None, invalid_choices=None, **kwargs): super(IndexableField, self).__init__(**kwargs) if choices is not None: @@ -205,6 +218,12 @@ def __init__(self, choices=None, invalid_choices=None, **kwargs): raise DeclarationError("[%d]: %s" % (i, str(e))) choices = tempo + if choice_labels is not None: + if not isinstance(choice_labels, (list, tuple)): + raise DeclarationError("choices has to be a list or tuple") + if choices is None or len(choices) != len(choice_labels): + raise DeclarationError("length of choice_labels has to match with choices.") + if invalid_choices is not None: if not isinstance(invalid_choices, (list, tuple)): raise DeclarationError("invalid_choices has to be a list or tuple") @@ -227,7 +246,7 @@ def __init__(self, choices=None, invalid_choices=None, **kwargs): if inter: raise DeclarationError("these choices are stated as both valid and invalid: %r" % inter) - self.choices, self.invalid_choices = choices, invalid_choices + self.choices, self.choice_labels, self.invalid_choices = choices, choice_labels, invalid_choices def _validate(self, val): super(IndexableField, self)._validate(val) @@ -241,6 +260,7 @@ def _validate(self, val): def get_schema(self): rval = super(IndexableField, self).get_schema() rval["choices"] = self.choices + rval["choice_labels"] = self.choice_labels rval["invalid_choices"] = self.invalid_choices return rval diff --git a/src/tests/resource_schema_test.py b/src/tests/resource_schema_test.py index 6756f0d..247a7f5 100644 --- a/src/tests/resource_schema_test.py +++ b/src/tests/resource_schema_test.py @@ -35,6 +35,7 @@ "default": None, "required": False, "choices": None, + "choice_labels": None, "max_length": None, "invalid_choices": None, "type": "string" @@ -46,6 +47,7 @@ "default": None, "required": False, "choices": None, + "choice_labels": None, "max_length": None, "invalid_choices": None, "type": "string" @@ -71,6 +73,7 @@ "default": None, "required": True, "choices": None, + "choice_labels": None, "max_length": None, "invalid_choices": None, "type": "string" @@ -84,6 +87,7 @@ "default": None, "required": False, "choices": None, + "choice_labels": None, "max_length": None, "invalid_choices": None, "type": "string" @@ -95,6 +99,7 @@ "default": None, "required": False, "choices": None, + "choice_labels": None, "max_length": None, "invalid_choices": None, "type": "string" @@ -117,6 +122,7 @@ "default": None, "required": False, "choices": None, + "choice_labels": None, "max_length": None, "invalid_choices": None, "type": "string" @@ -128,6 +134,7 @@ "default": None, "required": False, "choices": None, + "choice_labels": None, "max_length": None, "invalid_choices": None, "type": "string" @@ -143,6 +150,7 @@ "default": None, "required": True, "choices": None, + "choice_labels": None, "max_length": None, "invalid_choices": None, "type": "string" @@ -155,6 +163,7 @@ "default": None, "required": True, "choices": None, + "choice_labels": None, "max_val": None, "pk": True, "invalid_choices": None, @@ -167,6 +176,7 @@ "default": None, "required": False, "choices": None, + "choice_labels": None, "max_length": None, "invalid_choices": None, "type": "string" @@ -178,6 +188,7 @@ "default": None, "required": False, "choices": None, + "choice_labels": None, "max_length": None, "invalid_choices": None, "type": "string" @@ -198,6 +209,7 @@ "default": None, "required": True, "choices": None, + "choice_labels": None, "max_length": None, "invalid_choices": None, "type": "string" @@ -221,6 +233,7 @@ "default": None, "required": True, "choices": None, + "choice_labels": None, "max_length": None, "invalid_choices": None, "type": "string" @@ -234,6 +247,7 @@ "default": None, "required": False, "choices": None, + "choice_labels": None, "max_length": None, "invalid_choices": None, "type": "string" @@ -245,6 +259,7 @@ "default": None, "required": False, "choices": None, + "choice_labels": None, "max_length": None, "invalid_choices": None, "type": "string" @@ -267,6 +282,7 @@ "default": None, "required": False, "choices": None, + "choice_labels": None, "max_length": None, "invalid_choices": None, "type": "string" @@ -278,6 +294,7 @@ "default": None, "required": False, "choices": None, + "choice_labels": None, "max_length": None, "invalid_choices": None, "type": "string" @@ -300,6 +317,7 @@ "default": None, "required": False, "choices": None, + "choice_labels": None, "max_length": None, "invalid_choices": None, "type": "string" @@ -311,6 +329,7 @@ "default": None, "required": False, "choices": None, + "choice_labels": None, "max_length": None, "invalid_choices": None, "type": "string" @@ -325,6 +344,7 @@ "default": None, "required": True, "choices": None, + "choice_labels": None, "max_val": None, "pk": True, "invalid_choices": None, @@ -337,6 +357,7 @@ "default": None, "required": False, "choices": None, + "choice_labels": None, "max_length": None, "invalid_choices": None, "type": "string" @@ -348,6 +369,7 @@ "default": None, "required": False, "choices": None, + "choice_labels": None, "max_length": None, "invalid_choices": None, "type": "string" @@ -371,6 +393,7 @@ "default": None, "required": True, "choices": None, + "choice_labels": None, "max_length": None, "invalid_choices": None, "type": "string" @@ -394,6 +417,7 @@ "default": None, "required": True, "choices": None, + "choice_labels": None, "max_length": None, "invalid_choices": None, "type": "string" @@ -407,6 +431,7 @@ "default": None, "required": False, "choices": None, + "choice_labels": None, "max_length": None, "invalid_choices": None, "type": "string" @@ -418,6 +443,7 @@ "default": None, "required": False, "choices": None, + "choice_labels": None, "max_length": None, "invalid_choices": None, "type": "string" @@ -440,6 +466,7 @@ "default": None, "required": False, "choices": None, + "choice_labels": None, "max_length": None, "invalid_choices": None, "type": "string" @@ -451,6 +478,7 @@ "default": None, "required": False, "choices": None, + "choice_labels": None, "max_length": None, "invalid_choices": None, "type": "string" @@ -473,6 +501,7 @@ "default": None, "required": False, "choices": None, + "choice_labels": None, "max_length": None, "invalid_choices": None, "type": "string" @@ -484,6 +513,7 @@ "default": None, "required": False, "choices": None, + "choice_labels": None, "max_length": None, "invalid_choices": None, "type": "string" @@ -498,6 +528,7 @@ "default": None, "required": True, "choices": None, + "choice_labels": None, "max_val": None, "pk": True, "invalid_choices": None, @@ -510,6 +541,7 @@ "default": None, "required": False, "choices": None, + "choice_labels": None, "max_length": None, "invalid_choices": None, "type": "string" @@ -521,6 +553,7 @@ "default": None, "required": False, "choices": None, + "choice_labels": None, "max_length": None, "invalid_choices": None, "type": "string" @@ -552,6 +585,7 @@ "default": None, "required": False, "choices": None, + "choice_labels": None, "max_length": None, "invalid_choices": None, "type": "string" @@ -563,6 +597,7 @@ "default": None, "required": False, "choices": None, + "choice_labels": None, "max_length": None, "invalid_choices": None, "type": "string" @@ -588,6 +623,7 @@ "default": None, "required": True, "choices": None, + "choice_labels": None, "max_length": None, "invalid_choices": None, "type": "string" @@ -601,6 +637,7 @@ "default": None, "required": False, "choices": None, + "choice_labels": None, "max_length": None, "invalid_choices": None, "type": "string" @@ -612,6 +649,7 @@ "default": None, "required": False, "choices": None, + "choice_labels": None, "max_length": None, "invalid_choices": None, "type": "string" @@ -634,6 +672,7 @@ "default": None, "required": False, "choices": None, + "choice_labels": None, "max_length": None, "invalid_choices": None, "type": "string" @@ -645,6 +684,7 @@ "default": None, "required": False, "choices": None, + "choice_labels": None, "max_length": None, "invalid_choices": None, "type": "string" @@ -660,6 +700,7 @@ "default": None, "required": True, "choices": None, + "choice_labels": None, "max_length": None, "invalid_choices": None, "type": "string" @@ -672,6 +713,7 @@ "default": None, "required": True, "choices": None, + "choice_labels": None, "max_val": None, "pk": True, "invalid_choices": None, @@ -684,6 +726,7 @@ "default": None, "required": False, "choices": None, + "choice_labels": None, "max_length": None, "invalid_choices": None, "type": "string" @@ -695,6 +738,7 @@ "default": None, "required": False, "choices": None, + "choice_labels": None, "max_length": None, "invalid_choices": None, "type": "string" diff --git a/src/tests/schema_test.py b/src/tests/schema_test.py index 01d6ade..9db397d 100644 --- a/src/tests/schema_test.py +++ b/src/tests/schema_test.py @@ -163,6 +163,18 @@ def test_wrong_invalid_choices(self): # wrong item types self.assertRaises(DeclarationError, self.field_class, invalid_choices=self.nok_choices) + def test_wrong_choice_labels(self): + # not a list (while len() would match) + self.assertRaises(DeclarationError, self.field_class, choices=self.ok_choices, + choice_labels="W" * len(self.ok_choices)) + # wrong number of labels + self.assertRaises(DeclarationError, self.field_class, choices=self.ok_choices, + choice_labels=self.ok_choices + [self.ok_choices[-1]]) + + def test_ok_choice_labels(self): + self.field_class(choices=self.ok_choices, + choice_labels=self.ok_choices) + def test_not_in_choices(self): field = self.field_class(choices=self.ok_choices) self.assertRaises(ValidationError, field.deserialize, self.nok_choice_value) @@ -291,6 +303,7 @@ def test_get_schema(self): 'description': None, 'required': True, 'choices': None, + 'choice_labels': None, 'invalid_choices': None, 'default': None, 'min_length': 16, @@ -367,8 +380,9 @@ def test_schema(self): "type": "dict", "schema": { "two": {'type': 'list', 'description': None, 'required': True, - 'schema': {'type': 'int', 'description': None, 'choices': None, 'default': None, - 'invalid_choices': None, 'max_val': None, 'min_val': None, 'required': True}}, + 'schema': {'type': 'int', 'description': None, 'choices': None, 'choice_labels': None, + 'default': None, 'invalid_choices': None, 'max_val': None, 'min_val': None, + 'required': True}}, "has_additional_fields": True } }) @@ -386,7 +400,8 @@ def test_dict_schema(self): "schema": { "two": {'type': 'list', 'description': None, 'required': True, 'schema': {'type': 'int', 'description': None, 'choices': None, 'default': None, - 'invalid_choices': None, 'max_val': None, 'min_val': None, 'required': True}}, + 'choice_labels': None, 'invalid_choices': None, 'max_val': None, 'min_val': None, + 'required': True}}, "has_additional_fields": True } })