Skip to content

Commit 8450fdc

Browse files
Merge pull request #1844 from IFRCGo/feature/enum-fetch-types
Add global endpoint for fetching enums
2 parents d1d2c8a + 887ca36 commit 8450fdc

File tree

8 files changed

+130
-95
lines changed

8 files changed

+130
-95
lines changed

api/drf_views.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from pytz import utc
33
from rest_framework.status import HTTP_201_CREATED, HTTP_200_OK
44
from rest_framework.generics import GenericAPIView, CreateAPIView, UpdateAPIView
5+
from rest_framework.views import APIView
56
from rest_framework.response import Response
67
from rest_framework.authentication import TokenAuthentication
78
from rest_framework.permissions import IsAuthenticated
@@ -14,8 +15,10 @@
1415
from django.db.models import Prefetch, Count, Q, OuterRef
1516
from django.utils import timezone
1617
from django.utils.translation import get_language as django_get_language
18+
from drf_spectacular.utils import extend_schema
1719

1820
from main.utils import is_tableau
21+
from main.enums import GlobalEnumSerializer, get_enum_values
1922
from main.translation import TRANSLATOR_ORIGINAL_LANGUAGE_FIELD_NAME
2023
from deployments.models import Personnel
2124
from databank.serializers import CountryOverviewSerializer
@@ -1194,3 +1197,16 @@ class UsersViewset(viewsets.ReadOnlyModelViewSet):
11941197

11951198
def get_queryset(self):
11961199
return User.objects.filter(is_active=True)
1200+
1201+
1202+
class GlobalEnumView(APIView):
1203+
"""
1204+
Provide a single endpoint to fetch enum metadata
1205+
"""
1206+
1207+
@extend_schema(responses=GlobalEnumSerializer)
1208+
def get(self, _):
1209+
"""
1210+
Return a list of all enums.
1211+
"""
1212+
return Response(get_enum_values())

api/enums.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from . import models
2+
3+
enum_register = {
4+
'region_name': models.RegionName,
5+
'country_type': models.CountryType,
6+
'visibility_choices': models.VisibilityChoices,
7+
'visibility_char_choices': models.VisibilityCharChoices,
8+
'position_type': models.PositionType,
9+
'tab_number': models.TabNumber,
10+
'alert_level': models.AlertLevel,
11+
'appeal_type': models.AppealType,
12+
'appeal_status': models.AppealStatus,
13+
'request_choices': models.RequestChoices,
14+
'episource_choices': models.EPISourceChoices,
15+
'field_report_status': models.FieldReport.Status,
16+
'field_report_recent_affected': models.FieldReport.RecentAffected,
17+
'action_org': models.ActionOrg,
18+
'action_type': models.ActionType,
19+
'action_category': models.ActionCategory,
20+
'profile_org_types': models.Profile.OrgTypes,
21+
}

api/test_views.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -565,3 +565,10 @@ def test_district_deprecation(self):
565565
country.save()
566566
response = self.client.get('/api/v2/district/').json()
567567
self.assertEqual(response['count'], 2)
568+
569+
570+
class GlobalEnumEndpointTest(APITestCase):
571+
def test_200_response(self):
572+
response = self.client.get('/api/v2/global-enums/')
573+
self.assert_200(response)
574+
self.assertIsNotNone(response.json())

dref/enums.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from . import models
2+
3+
enum_register = {
4+
'national_society_action_title': models.NationalSocietyAction.Title,
5+
'identified_need_title': models.IdentifiedNeed.Title,
6+
'planned_intervention_title': models.PlannedIntervention.Title,
7+
'dref_dref_type': models.Dref.DrefType,
8+
'dref_onset_type': models.Dref.OnsetType,
9+
'dref_disaster_category': models.Dref.DisasterCategory,
10+
'dref_status': models.Dref.Status,
11+
}

dref/models.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
@reversion.register()
1818
class NationalSocietyAction(models.Model):
19-
# NOTE: Replace `TextChoices` to `models.TextChoices` after upgrade to Django version 3
2019
class Title(models.TextChoices):
2120
NATIONAL_SOCIETY_READINESS = "national_society_readiness", _("National Society Readiness")
2221
ASSESSMENT = "assessment", _("Assessment")

flash_update/enums.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from . import models
2+
3+
enum_register = {
4+
'flash_update_flash_share_with': models.FlashUpdate.FlashShareWith,
5+
}

main/enums.py

Lines changed: 68 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,94 +1,68 @@
1-
# Note: This is already implemented in the newer version of Django and need to use that instead of this only upgrade is done.
2-
# Source: https://github.com/django/django/blob/main/django/db/models/enums.py
3-
4-
import enum
5-
from types import DynamicClassAttribute
6-
7-
from django.utils.functional import Promise
8-
9-
__all__ = ['Choices', 'IntegerChoices', 'TextChoices']
10-
11-
12-
class ChoicesMeta(enum.EnumMeta):
13-
"""A metaclass for creating a enum choices."""
14-
15-
def __new__(metacls, classname, bases, classdict, **kwds):
16-
labels = []
17-
for key in classdict._member_names:
18-
value = classdict[key]
19-
if (
20-
isinstance(value, (list, tuple)) and
21-
len(value) > 1 and
22-
isinstance(value[-1], (Promise, str))
23-
):
24-
*value, label = value
25-
value = tuple(value)
26-
else:
27-
label = key.replace('_', ' ').title()
28-
labels.append(label)
29-
# Use dict.__setitem__() to suppress defenses against double
30-
# assignment in enum's classdict.
31-
dict.__setitem__(classdict, key, value)
32-
cls = super().__new__(metacls, classname, bases, classdict, **kwds)
33-
for member, label in zip(cls.__members__.values(), labels):
34-
member._label_ = label
35-
return enum.unique(cls)
36-
37-
def __contains__(cls, member):
38-
if not isinstance(member, enum.Enum):
39-
# Allow non-enums to match against member values.
40-
return any(x.value == member for x in cls)
41-
return super().__contains__(member)
42-
43-
@property
44-
def names(cls):
45-
empty = ['__empty__'] if hasattr(cls, '__empty__') else []
46-
return empty + [member.name for member in cls]
47-
48-
@property
49-
def choices(cls):
50-
empty = [(None, cls.__empty__)] if hasattr(cls, '__empty__') else []
51-
return empty + [(member.value, member.label) for member in cls]
52-
53-
@property
54-
def labels(cls):
55-
return [label for _, label in cls.choices]
56-
57-
@property
58-
def values(cls):
59-
return [value for value, _ in cls.choices]
60-
61-
62-
class Choices(enum.Enum, metaclass=ChoicesMeta):
63-
"""Class for creating enumerated choices."""
64-
65-
@DynamicClassAttribute
66-
def label(self):
67-
return self._label_
68-
69-
@property
70-
def do_not_call_in_templates(self):
71-
return True
72-
73-
def __str__(self):
74-
"""
75-
Use value when cast to str, so that Choices set as model instance
76-
attributes are rendered as expected in templates and similar contexts.
77-
"""
78-
return str(self.value)
79-
80-
# A similar format was proposed for Python 3.10.
81-
def __repr__(self):
82-
return f'{self.__class__.__qualname__}.{self._name_}'
83-
84-
85-
class IntegerChoices(int, Choices):
86-
"""Class for creating enumerated integer choices."""
87-
pass
88-
89-
90-
class TextChoices(str, Choices):
91-
"""Class for creating enumerated string choices."""
92-
93-
def _generate_next_value_(name, start, count, last_values):
94-
return name
1+
from rest_framework import serializers
2+
3+
from dref import enums as dref_enums
4+
from api import enums as api_enums
5+
from flash_update import enums as flash_update_enums
6+
7+
8+
apps_enum_register = [
9+
('dref', dref_enums.enum_register),
10+
('api', api_enums.enum_register),
11+
('flash_update', flash_update_enums.enum_register),
12+
]
13+
14+
15+
def underscore_to_camel(text):
16+
return text.replace('_', ' ').title().replace(' ', '')
17+
18+
19+
def generate_global_enum_register():
20+
enum_map = {}
21+
enum_names = set()
22+
for app_prefix, app_enum_register in apps_enum_register:
23+
for enum_field, enum in app_enum_register.items():
24+
# Change field dref_national_society_action_title -> DrefNationalSocietyActionTitle
25+
_enum_field = f'{app_prefix}_{enum_field}'
26+
enum_name = f'{underscore_to_camel(_enum_field)}EnumSerializer'
27+
if enum_name in enum_names:
28+
raise Exception(f'Duplicate enum_names found for {enum_name} in {enum_names}')
29+
enum_names.add(enum_name)
30+
enum_map[_enum_field] = (enum_name, enum)
31+
return enum_map
32+
33+
34+
global_enum_registers = generate_global_enum_register()
35+
36+
37+
def generate_enum_global_serializer(name):
38+
def _get_enum_key_value_serializer(enum, enum_name):
39+
return type(
40+
enum_name,
41+
(serializers.Serializer,),
42+
{
43+
'key': serializers.ChoiceField(enum.choices),
44+
'value': serializers.CharField(),
45+
},
46+
)
47+
48+
fields = {}
49+
for enum_field, (enum_name, enum) in global_enum_registers.items():
50+
fields[enum_field] = _get_enum_key_value_serializer(enum, enum_name)(many=True, required=False)
51+
return type(name, (serializers.Serializer,), fields)
52+
53+
54+
GlobalEnumSerializer = generate_enum_global_serializer('GlobalEnumSerializer')
55+
56+
57+
def get_enum_values():
58+
enum_data = {
59+
enum_field: [
60+
{
61+
'key': key,
62+
'value': value,
63+
}
64+
for key, value in enum.choices
65+
]
66+
for enum_field, (_, enum) in global_enum_registers.items()
67+
}
68+
return GlobalEnumSerializer(enum_data).data

main/urls.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,8 @@
237237
url(r"^server-error-for-devs", DummyHttpStatusError.as_view()),
238238
url(r"^exception-error-for-devs", DummyExceptionError.as_view()),
239239
path("i18n/", include("django.conf.urls.i18n")),
240+
# Enums
241+
url(r"^api/v2/global-enums/", api_views.GlobalEnumView.as_view(), name="global_enums"),
240242
# Docs
241243
path("docs/", SpectacularRedocView.as_view(url_name='schema'), name='redoc'),
242244
path("api-docs/", SpectacularAPIView.as_view(), name='schema'),

0 commit comments

Comments
 (0)