|
1 | 1 | """Support for django rest framework symmetric serialization"""
|
2 | 2 |
|
3 |
| -__all__ = ["EnumField", "EnumFieldMixin"] |
| 3 | +__all__ = ["EnumField", "FlagField", "EnumFieldMixin"] |
4 | 4 |
|
5 | 5 | import inspect
|
6 | 6 | from datetime import date, datetime, time, timedelta
|
7 | 7 | from decimal import Decimal, DecimalException
|
8 |
| -from enum import Enum |
| 8 | +from enum import Enum, Flag |
| 9 | +from functools import reduce |
| 10 | +from operator import or_ |
9 | 11 | from typing import Any, Dict, Optional, Type, Union
|
10 | 12 |
|
11 | 13 | from rest_framework.fields import (
|
|
23 | 25 | from rest_framework.serializers import ModelSerializer
|
24 | 26 | from rest_framework.utils.field_mapping import get_field_kwargs
|
25 | 27 |
|
26 |
| -from django_enum import EnumField as EnumModelField |
| 28 | +from django_enum.fields import EnumField as EnumModelField |
| 29 | +from django_enum.fields import FlagField as FlagModelField |
27 | 30 | from django_enum.utils import (
|
28 | 31 | choices,
|
29 | 32 | decimal_params,
|
@@ -172,6 +175,80 @@ def to_representation(self, value: Any) -> Any:
|
172 | 175 | return getattr(value, "value", value)
|
173 | 176 |
|
174 | 177 |
|
| 178 | +class FlagField(ChoiceField): |
| 179 | + """ |
| 180 | + A djangorestframework serializer field for :class:`~enum.Flag` types. If |
| 181 | + unspecified ModelSerializers will assign :class:`~django_enum.fields.FlagField` |
| 182 | + model field types to `ChoiceField |
| 183 | + <https://www.django-rest-framework.org/api-guide/fields/#choicefield>`_ which will |
| 184 | + not combine composite flag values appropriately. This field will also allow any |
| 185 | + symmetric values to be used (e.g. labels or names instead of values). |
| 186 | +
|
| 187 | + **You should add** :class:`~django_enum.drf.EnumFieldMixin` **to your serializer to |
| 188 | + automatically use this field.** |
| 189 | +
|
| 190 | + :param enum: The type of the flag of the field |
| 191 | + :param strict: If True (default) only values in the flag type |
| 192 | + will be acceptable. If False, no errors will be thrown if other |
| 193 | + values of the same primitive type are used |
| 194 | + :param kwargs: Any other named arguments applicable to a ChoiceField |
| 195 | + will be passed up to the base classes. |
| 196 | + """ |
| 197 | + |
| 198 | + enum: Type[Flag] |
| 199 | + strict: bool = True |
| 200 | + |
| 201 | + def __init__(self, enum: Type[Flag], strict: bool = strict, **kwargs): |
| 202 | + self.enum = enum |
| 203 | + self.strict = strict |
| 204 | + self.choices = kwargs.pop("choices", choices(enum)) |
| 205 | + kwargs.pop("field_name", None) |
| 206 | + kwargs.pop("model_field", None) |
| 207 | + super().__init__(choices=self.choices, **kwargs) |
| 208 | + |
| 209 | + def to_internal_value(self, data: Any) -> Union[Enum, Any]: |
| 210 | + """ |
| 211 | + Transform the *incoming* primitive data into an enum instance. |
| 212 | + We accept a composite flag value or a list of values. If a list, |
| 213 | + each element will be converted to a flag value and then the values |
| 214 | + will be reduced into a composite value with the or operator. |
| 215 | +
|
| 216 | + :return: A composite flag value. |
| 217 | + """ |
| 218 | + if not data: |
| 219 | + if self.allow_null and (data is None or data == ""): |
| 220 | + return None |
| 221 | + return self.enum(0) |
| 222 | + |
| 223 | + if not isinstance(data, self.enum): |
| 224 | + try: |
| 225 | + return self.enum(data) |
| 226 | + except (TypeError, ValueError): |
| 227 | + try: |
| 228 | + if isinstance(data, str): |
| 229 | + return self.enum[data] |
| 230 | + if isinstance(data, (list, tuple)): |
| 231 | + values = [] |
| 232 | + for val in data: |
| 233 | + try: |
| 234 | + values.append(self.enum(val)) |
| 235 | + except (TypeError, ValueError): |
| 236 | + values.append(self.enum[val]) |
| 237 | + return reduce(or_, values) |
| 238 | + except (TypeError, ValueError, KeyError): |
| 239 | + pass |
| 240 | + self.fail("invalid_choice", input=data) |
| 241 | + return data |
| 242 | + |
| 243 | + def to_representation(self, value: Any) -> Any: |
| 244 | + """ |
| 245 | + Transform the *outgoing* enum value into its primitive value. |
| 246 | +
|
| 247 | + :return: The primitive composite value of the flag (most likely an integer). |
| 248 | + """ |
| 249 | + return getattr(value, "value", value) |
| 250 | + |
| 251 | + |
175 | 252 | class EnumFieldMixin(with_typehint(ModelSerializer)): # type: ignore
|
176 | 253 | """
|
177 | 254 | A mixin for ModelSerializers that adds auto-magic support for
|
@@ -204,7 +281,9 @@ class Meta:
|
204 | 281 | :return: A 2-tuple, the first element is the field class, the
|
205 | 282 | second is the kwargs for the field
|
206 | 283 | """
|
207 |
| - field_class = ClassLookupDict({EnumModelField: EnumField})[model_field] |
| 284 | + field_class = ClassLookupDict( |
| 285 | + {FlagModelField: FlagField, EnumModelField: EnumField} |
| 286 | + )[model_field] |
208 | 287 | if field_class:
|
209 | 288 | return field_class, {
|
210 | 289 | "enum": model_field.enum,
|
|
0 commit comments