Skip to content

Commit a80c763

Browse files
committed
Update schema and generator
1 parent 6f43eea commit a80c763

File tree

3 files changed

+346
-89
lines changed

3 files changed

+346
-89
lines changed

api/generator.py

+176-1
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,18 @@
2323
# see http://www.gnu.org/licenses/.
2424
#
2525
# ##############################################################################
26-
from rest_framework.schemas.openapi import SchemaGenerator
26+
from collections import OrderedDict
27+
28+
from rest_framework.schemas.openapi import AutoSchema, SchemaGenerator
29+
from rest_framework.serializers import Serializer
30+
31+
from base.models.utils.utils import ChoiceEnum
2732

2833

2934
class AdmissionSchemaGenerator(SchemaGenerator):
3035
def get_schema(self, *args, **kwargs):
3136
schema = super().get_schema(*args, **kwargs)
37+
schema["openapi"] = "3.0.0"
3238
schema["info"]["title"] = "Admission API"
3339
schema["info"]["description"] = "This API delivers data for the Admission project."
3440
schema["info"]["contact"] = {
@@ -63,4 +69,173 @@ def get_schema(self, *args, **kwargs):
6369
"description": "Enter your token in the format **Token <token>**"
6470
}
6571
}
72+
schema['components']['parameters'] = {
73+
"X-User-FirstName": {
74+
"in": "header",
75+
"name": "X-User-FirstName",
76+
"schema": {
77+
"type": "string"
78+
},
79+
"required": False
80+
},
81+
"X-User-LastName": {
82+
"in": "header",
83+
"name": "X-User-LastName",
84+
"schema": {
85+
"type": "string"
86+
},
87+
"required": False
88+
},
89+
"X-User-Email": {
90+
"in": "header",
91+
"name": "X-User-Email",
92+
"schema": {
93+
"type": "string"
94+
},
95+
"required": False
96+
},
97+
"X-User-GlobalID": {
98+
"in": "header",
99+
"name": "X-User-GlobalID",
100+
"schema": {
101+
"type": "string"
102+
},
103+
"required": False
104+
},
105+
"Accept-Language": {
106+
"in": "header",
107+
"name": "Accept-Language",
108+
"description": "The header advertises which languages the client is able to understand, and which "
109+
"locale variant is preferred. (By languages, we mean natural languages, such as "
110+
"English, and not programming languages.)",
111+
"schema": {
112+
"$ref": "#/components/schemas/AcceptedLanguageEnum"
113+
},
114+
"required": False
115+
}
116+
}
117+
schema['components']['responses'] = {
118+
"Unauthorized": {
119+
"description": "Unauthorized",
120+
"content": {
121+
"application/json": {
122+
"schema": {
123+
"$ref": "#/components/schemas/Error"
124+
}
125+
}
126+
}
127+
},
128+
"BadRequest": {
129+
"description": "Bad request",
130+
"content": {
131+
"application/json": {
132+
"schema": {
133+
"$ref": "#/components/schemas/Error"
134+
}
135+
}
136+
}
137+
},
138+
"NotFound": {
139+
"description": "The specified resource was not found",
140+
"content": {
141+
"application/json": {
142+
"schema": {
143+
"$ref": "#/components/schemas/Error"
144+
}
145+
}
146+
}
147+
}
148+
}
149+
schema['components']['schemas']['Error'] = {
150+
"type": "object",
151+
"properties": {
152+
"code": {
153+
"type": "string"
154+
},
155+
"message": {
156+
"type": "string"
157+
}
158+
},
159+
"required": [
160+
"code",
161+
"message"
162+
]
163+
}
164+
schema['components']['schemas']['AcceptedLanguageEnum'] = {
165+
"type": "string",
166+
"enum": [
167+
"en",
168+
"fr-be"
169+
]
170+
}
171+
for path, path_content in schema['paths'].items():
172+
for method, method_content in path_content.items():
173+
method_content['responses'].update({
174+
"400": {
175+
"$ref": "#/components/responses/BadRequest"
176+
},
177+
"401": {
178+
"$ref": "#/components/responses/Unauthorized"
179+
},
180+
"404": {
181+
"$ref": "#/components/responses/NotFound"
182+
}
183+
})
66184
return schema
185+
186+
187+
class DetailedAutoSchema(AutoSchema):
188+
def __init__(self, *args, **kwargs):
189+
super().__init__(*args, **kwargs)
190+
self.enums = {}
191+
192+
def get_request_body(self, path, method):
193+
if method not in ('PUT', 'PATCH', 'POST'):
194+
return {}
195+
196+
self.request_media_types = self.map_parsers(path, method)
197+
198+
serializer = self.get_serializer(path, method, for_response=False)
199+
200+
if not isinstance(serializer, Serializer):
201+
item_schema = {}
202+
else:
203+
item_schema = self._get_reference(serializer)
204+
205+
return {
206+
'content': {
207+
ct: {'schema': item_schema}
208+
for ct in self.request_media_types
209+
}
210+
}
211+
212+
def get_components(self, path, method):
213+
if method.lower() == 'delete':
214+
return {}
215+
216+
components = {}
217+
for with_response in [True, False]:
218+
serializer = self.get_serializer(path, method, for_response=with_response)
219+
if not isinstance(serializer, Serializer):
220+
return {}
221+
component_name = self.get_component_name(serializer)
222+
content = self.map_serializer(serializer)
223+
components[component_name] = content
224+
225+
for enum_name, enum in self.enums.items():
226+
components[enum_name] = enum
227+
228+
return components
229+
230+
def get_serializer(self, path, method, for_response=True):
231+
raise NotImplementedError
232+
233+
def map_choicefield(self, field):
234+
# The only way to retrieve the original enum is to compare choices
235+
for declared_enum in ChoiceEnum.__subclasses__():
236+
if OrderedDict(declared_enum.choices()) == field.choices:
237+
self.enums[declared_enum.__name__] = super().map_choicefield(field)
238+
return {
239+
'$ref': "#/components/responses/{}".format(declared_enum.__name__)
240+
}
241+
return super().map_choicefield(field)

api/views/doctorate.py

+15-43
Original file line numberDiff line numberDiff line change
@@ -26,58 +26,24 @@
2626
from rest_framework import mixins, status
2727
from rest_framework.generics import GenericAPIView, ListCreateAPIView
2828
from rest_framework.response import Response
29-
from rest_framework.schemas.openapi import AutoSchema
30-
from rest_framework.serializers import Serializer
3129

30+
from admission.api.generator import DetailedAutoSchema
3231
from admission.contrib import serializers
32+
from backoffice.settings.rest_framework.common_views import DisplayExceptionsByFieldNameAPIMixin
3333
from ddd.logic.admission.preparation.projet_doctoral.commands import (
3434
CompleterPropositionCommand, GetPropositionCommand,
3535
InitierPropositionCommand,
3636
SearchPropositionsCommand,
3737
)
38+
from ddd.logic.admission.preparation.projet_doctoral.domain.validator.exceptions import (
39+
BureauCDEInconsistantException,
40+
ContratTravailInconsistantException,
41+
InstitutionInconsistanteException,
42+
JustificationRequiseException,
43+
)
3844
from infrastructure.messages_bus import message_bus_instance
3945

4046

41-
class DetailedAutoSchema(AutoSchema):
42-
def get_request_body(self, path, method):
43-
if method not in ('PUT', 'PATCH', 'POST'):
44-
return {}
45-
46-
self.request_media_types = self.map_parsers(path, method)
47-
48-
serializer = self.get_serializer(path, method, for_response=False)
49-
50-
if not isinstance(serializer, Serializer):
51-
item_schema = {}
52-
else:
53-
item_schema = self._get_reference(serializer)
54-
55-
return {
56-
'content': {
57-
ct: {'schema': item_schema}
58-
for ct in self.request_media_types
59-
}
60-
}
61-
62-
def get_components(self, path, method):
63-
if method.lower() == 'delete':
64-
return {}
65-
66-
components = {}
67-
for with_response in [True, False]:
68-
serializer = self.get_serializer(path, method, for_response=with_response)
69-
if not isinstance(serializer, Serializer):
70-
return {}
71-
component_name = self.get_component_name(serializer)
72-
content = self.map_serializer(serializer)
73-
components[component_name] = content
74-
75-
return components
76-
77-
def get_serializer(self, path, method, for_response=True):
78-
raise NotImplementedError
79-
80-
8147
class PropositionListSchema(DetailedAutoSchema):
8248
def get_operation_id_base(self, path, method, action):
8349
return '_proposition' if method == 'POST' else '_propositions'
@@ -90,10 +56,16 @@ def get_serializer(self, path, method, for_response=True):
9056
return serializers.PropositionSearchDTOSerializer()
9157

9258

93-
class PropositionListViewSet(ListCreateAPIView):
59+
class PropositionListViewSet(DisplayExceptionsByFieldNameAPIMixin, ListCreateAPIView):
9460
schema = PropositionListSchema()
9561
pagination_class = None
9662
filter_backends = None
63+
field_name_by_exception = {
64+
JustificationRequiseException: ['justification'],
65+
InstitutionInconsistanteException: ['institution'],
66+
ContratTravailInconsistantException: ['type_contrat_travail'],
67+
BureauCDEInconsistantException: ['bureau_cde'],
68+
}
9769

9870
def list(self, request, **kwargs):
9971
proposition_list = message_bus_instance.invoke(

0 commit comments

Comments
 (0)