commonground-api-common 1.12.2__py3-none-any.whl → 2.4.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- commonground_api_common-1.12.2.data/scripts/patch_content_types → commonground_api_common-2.4.1.data/scripts/generate_schema +2 -4
- {commonground_api_common-1.12.2.dist-info → commonground_api_common-2.4.1.dist-info}/METADATA +47 -40
- {commonground_api_common-1.12.2.dist-info → commonground_api_common-2.4.1.dist-info}/RECORD +47 -52
- {commonground_api_common-1.12.2.dist-info → commonground_api_common-2.4.1.dist-info}/WHEEL +1 -1
- vng_api_common/__init__.py +1 -1
- vng_api_common/admin.py +1 -20
- vng_api_common/api/views.py +1 -0
- vng_api_common/apps.py +44 -26
- vng_api_common/audittrails/utils.py +44 -0
- vng_api_common/authorizations/admin.py +1 -1
- vng_api_common/authorizations/middleware.py +244 -0
- vng_api_common/authorizations/migrations/0016_remove_authorizationsconfig_api_root_and_more.py +76 -0
- vng_api_common/authorizations/models.py +62 -3
- vng_api_common/authorizations/utils.py +17 -0
- vng_api_common/authorizations/validators.py +5 -11
- vng_api_common/caching/etags.py +2 -1
- vng_api_common/client.py +61 -29
- vng_api_common/conf/api.py +33 -48
- vng_api_common/contrib/setup_configuration/models.py +32 -0
- vng_api_common/contrib/setup_configuration/steps.py +46 -0
- vng_api_common/extensions/file.py +26 -0
- vng_api_common/extensions/gegevensgroep.py +16 -0
- vng_api_common/extensions/geojson.py +270 -0
- vng_api_common/extensions/hyperlink.py +37 -0
- vng_api_common/extensions/polymorphic.py +68 -0
- vng_api_common/extensions/query.py +20 -0
- vng_api_common/filters.py +0 -1
- vng_api_common/generators.py +12 -113
- vng_api_common/middleware.py +1 -227
- vng_api_common/migrations/0006_delete_apicredential.py +120 -0
- vng_api_common/mocks.py +4 -1
- vng_api_common/models.py +10 -111
- vng_api_common/notifications/api/views.py +8 -8
- vng_api_common/notifications/handlers.py +8 -3
- vng_api_common/notifications/migrations/0011_remove_subscription_config_and_more.py +23 -0
- vng_api_common/oas.py +6 -10
- vng_api_common/pagination.py +10 -0
- vng_api_common/routers.py +3 -3
- vng_api_common/schema.py +414 -158
- vng_api_common/tests/schema.py +13 -0
- vng_api_common/utils.py +0 -22
- vng_api_common/validators.py +111 -113
- vng_api_common/views.py +35 -20
- commonground_api_common-1.12.2.data/scripts/generate_schema +0 -39
- commonground_api_common-1.12.2.data/scripts/use_external_components +0 -16
- vng_api_common/inspectors/cache.py +0 -57
- vng_api_common/inspectors/fields.py +0 -126
- vng_api_common/inspectors/files.py +0 -121
- vng_api_common/inspectors/geojson.py +0 -360
- vng_api_common/inspectors/polymorphic.py +0 -72
- vng_api_common/inspectors/query.py +0 -96
- vng_api_common/inspectors/utils.py +0 -40
- vng_api_common/inspectors/view.py +0 -547
- vng_api_common/management/commands/generate_autorisaties.py +0 -43
- vng_api_common/management/commands/generate_notificaties.py +0 -40
- vng_api_common/management/commands/generate_swagger.py +0 -197
- vng_api_common/management/commands/patch_error_contenttypes.py +0 -61
- vng_api_common/management/commands/use_external_components.py +0 -94
- vng_api_common/notifications/constants.py +0 -3
- vng_api_common/notifications/models.py +0 -97
- vng_api_common/templates/vng_api_common/api_schema_to_markdown_table.md +0 -16
- vng_api_common/templates/vng_api_common/autorisaties.md +0 -15
- vng_api_common/templates/vng_api_common/notificaties.md +0 -24
- {commonground_api_common-1.12.2.dist-info → commonground_api_common-2.4.1.dist-info}/top_level.txt +0 -0
- /vng_api_common/{inspectors → contrib}/__init__.py +0 -0
- /vng_api_common/{management → contrib/setup_configuration}/__init__.py +0 -0
- /vng_api_common/{management/commands → extensions}/__init__.py +0 -0
@@ -1,547 +0,0 @@
|
|
1
|
-
import inspect
|
2
|
-
import logging
|
3
|
-
from collections import OrderedDict
|
4
|
-
from itertools import chain
|
5
|
-
from typing import Optional, Tuple, Union
|
6
|
-
|
7
|
-
from django.apps import apps
|
8
|
-
from django.conf import settings
|
9
|
-
from django.utils.translation import gettext, gettext_lazy as _
|
10
|
-
|
11
|
-
from drf_yasg import openapi
|
12
|
-
from drf_yasg.inspectors import SwaggerAutoSchema
|
13
|
-
from drf_yasg.utils import get_consumes
|
14
|
-
from rest_framework import exceptions, serializers, status, viewsets
|
15
|
-
|
16
|
-
from ..constants import HEADER_AUDIT, HEADER_LOGRECORD_ID, VERSION_HEADER
|
17
|
-
from ..exceptions import Conflict, Gone, PreconditionFailed
|
18
|
-
from ..geo import GeoMixin
|
19
|
-
from ..permissions import BaseAuthRequired, get_required_scopes
|
20
|
-
from ..search import is_search_view
|
21
|
-
from ..serializers import (
|
22
|
-
FoutSerializer,
|
23
|
-
ValidatieFoutSerializer,
|
24
|
-
add_choice_values_help_text,
|
25
|
-
)
|
26
|
-
from .cache import CACHE_REQUEST_HEADERS, get_cache_headers, has_cache_header
|
27
|
-
|
28
|
-
logger = logging.getLogger(__name__)
|
29
|
-
|
30
|
-
TYPE_TO_FIELDMAPPING = {
|
31
|
-
openapi.TYPE_INTEGER: serializers.IntegerField,
|
32
|
-
openapi.TYPE_NUMBER: serializers.FloatField,
|
33
|
-
openapi.TYPE_STRING: serializers.CharField,
|
34
|
-
openapi.TYPE_BOOLEAN: serializers.BooleanField,
|
35
|
-
openapi.TYPE_ARRAY: serializers.ListField,
|
36
|
-
}
|
37
|
-
|
38
|
-
COMMON_ERRORS = [
|
39
|
-
exceptions.AuthenticationFailed,
|
40
|
-
exceptions.NotAuthenticated,
|
41
|
-
exceptions.PermissionDenied,
|
42
|
-
exceptions.NotAcceptable,
|
43
|
-
Conflict,
|
44
|
-
Gone,
|
45
|
-
exceptions.UnsupportedMediaType,
|
46
|
-
exceptions.Throttled,
|
47
|
-
exceptions.APIException,
|
48
|
-
]
|
49
|
-
|
50
|
-
DEFAULT_ACTION_ERRORS = {
|
51
|
-
"create": COMMON_ERRORS + [exceptions.ParseError, exceptions.ValidationError],
|
52
|
-
"list": COMMON_ERRORS,
|
53
|
-
"retrieve": COMMON_ERRORS + [exceptions.NotFound],
|
54
|
-
"update": COMMON_ERRORS
|
55
|
-
+ [exceptions.ParseError, exceptions.ValidationError, exceptions.NotFound],
|
56
|
-
"partial_update": COMMON_ERRORS
|
57
|
-
+ [exceptions.ParseError, exceptions.ValidationError, exceptions.NotFound],
|
58
|
-
"destroy": COMMON_ERRORS + [exceptions.NotFound],
|
59
|
-
}
|
60
|
-
|
61
|
-
HTTP_STATUS_CODE_TITLES = {
|
62
|
-
status.HTTP_100_CONTINUE: "Continue",
|
63
|
-
status.HTTP_101_SWITCHING_PROTOCOLS: "Switching protocols",
|
64
|
-
status.HTTP_200_OK: "OK",
|
65
|
-
status.HTTP_201_CREATED: "Created",
|
66
|
-
status.HTTP_202_ACCEPTED: "Accepted",
|
67
|
-
status.HTTP_203_NON_AUTHORITATIVE_INFORMATION: "Non authoritative information",
|
68
|
-
status.HTTP_204_NO_CONTENT: "No content",
|
69
|
-
status.HTTP_205_RESET_CONTENT: "Reset content",
|
70
|
-
status.HTTP_206_PARTIAL_CONTENT: "Partial content",
|
71
|
-
status.HTTP_207_MULTI_STATUS: "Multi status",
|
72
|
-
status.HTTP_300_MULTIPLE_CHOICES: "Multiple choices",
|
73
|
-
status.HTTP_301_MOVED_PERMANENTLY: "Moved permanently",
|
74
|
-
status.HTTP_302_FOUND: "Found",
|
75
|
-
status.HTTP_303_SEE_OTHER: "See other",
|
76
|
-
status.HTTP_304_NOT_MODIFIED: "Not modified",
|
77
|
-
status.HTTP_305_USE_PROXY: "Use proxy",
|
78
|
-
status.HTTP_306_RESERVED: "Reserved",
|
79
|
-
status.HTTP_307_TEMPORARY_REDIRECT: "Temporary redirect",
|
80
|
-
status.HTTP_400_BAD_REQUEST: "Bad request",
|
81
|
-
status.HTTP_401_UNAUTHORIZED: "Unauthorized",
|
82
|
-
status.HTTP_402_PAYMENT_REQUIRED: "Payment required",
|
83
|
-
status.HTTP_403_FORBIDDEN: "Forbidden",
|
84
|
-
status.HTTP_404_NOT_FOUND: "Not found",
|
85
|
-
status.HTTP_405_METHOD_NOT_ALLOWED: "Method not allowed",
|
86
|
-
status.HTTP_406_NOT_ACCEPTABLE: "Not acceptable",
|
87
|
-
status.HTTP_407_PROXY_AUTHENTICATION_REQUIRED: "Proxy authentication required",
|
88
|
-
status.HTTP_408_REQUEST_TIMEOUT: "Request timeout",
|
89
|
-
status.HTTP_409_CONFLICT: "Conflict",
|
90
|
-
status.HTTP_410_GONE: "Gone",
|
91
|
-
status.HTTP_411_LENGTH_REQUIRED: "Length required",
|
92
|
-
status.HTTP_412_PRECONDITION_FAILED: "Precondition failed",
|
93
|
-
status.HTTP_413_REQUEST_ENTITY_TOO_LARGE: "Request entity too large",
|
94
|
-
status.HTTP_414_REQUEST_URI_TOO_LONG: "Request uri too long",
|
95
|
-
status.HTTP_415_UNSUPPORTED_MEDIA_TYPE: "Unsupported media type",
|
96
|
-
status.HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE: "Requested range not satisfiable",
|
97
|
-
status.HTTP_417_EXPECTATION_FAILED: "Expectation failed",
|
98
|
-
status.HTTP_422_UNPROCESSABLE_ENTITY: "Unprocessable entity",
|
99
|
-
status.HTTP_423_LOCKED: "Locked",
|
100
|
-
status.HTTP_424_FAILED_DEPENDENCY: "Failed dependency",
|
101
|
-
status.HTTP_428_PRECONDITION_REQUIRED: "Precondition required",
|
102
|
-
status.HTTP_429_TOO_MANY_REQUESTS: "Too many requests",
|
103
|
-
status.HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE: "Request header fields too large",
|
104
|
-
status.HTTP_451_UNAVAILABLE_FOR_LEGAL_REASONS: "Unavailable for legal reasons",
|
105
|
-
status.HTTP_500_INTERNAL_SERVER_ERROR: "Internal server error",
|
106
|
-
status.HTTP_501_NOT_IMPLEMENTED: "Not implemented",
|
107
|
-
status.HTTP_502_BAD_GATEWAY: "Bad gateway",
|
108
|
-
status.HTTP_503_SERVICE_UNAVAILABLE: "Service unavailable",
|
109
|
-
status.HTTP_504_GATEWAY_TIMEOUT: "Gateway timeout",
|
110
|
-
status.HTTP_505_HTTP_VERSION_NOT_SUPPORTED: "HTTP version not supported",
|
111
|
-
status.HTTP_507_INSUFFICIENT_STORAGE: "Insufficient storage",
|
112
|
-
status.HTTP_511_NETWORK_AUTHENTICATION_REQUIRED: "Network authentication required",
|
113
|
-
}
|
114
|
-
|
115
|
-
AUDIT_TRAIL_ENABLED = apps.is_installed("vng_api_common.audittrails")
|
116
|
-
|
117
|
-
AUDIT_REQUEST_HEADERS = [
|
118
|
-
openapi.Parameter(
|
119
|
-
name=HEADER_LOGRECORD_ID,
|
120
|
-
type=openapi.TYPE_STRING,
|
121
|
-
in_=openapi.IN_HEADER,
|
122
|
-
required=False,
|
123
|
-
description=gettext(
|
124
|
-
"Identifier of the request, traceable throughout the network"
|
125
|
-
),
|
126
|
-
),
|
127
|
-
openapi.Parameter(
|
128
|
-
name=HEADER_AUDIT,
|
129
|
-
type=openapi.TYPE_STRING,
|
130
|
-
in_=openapi.IN_HEADER,
|
131
|
-
required=False,
|
132
|
-
description=gettext("Explanation why the request is done"),
|
133
|
-
),
|
134
|
-
]
|
135
|
-
|
136
|
-
|
137
|
-
def response_header(description: str, type: str, format: str = None) -> OrderedDict:
|
138
|
-
header = OrderedDict(
|
139
|
-
(("schema", OrderedDict((("type", type),))), ("description", description))
|
140
|
-
)
|
141
|
-
if format is not None:
|
142
|
-
header["schema"]["format"] = format
|
143
|
-
return header
|
144
|
-
|
145
|
-
|
146
|
-
version_header = response_header(
|
147
|
-
"Geeft een specifieke API-versie aan in de context van een specifieke aanroep. Voorbeeld: 1.2.1.",
|
148
|
-
type=openapi.TYPE_STRING,
|
149
|
-
)
|
150
|
-
|
151
|
-
location_header = response_header(
|
152
|
-
"URL waar de resource leeft.", type=openapi.TYPE_STRING, format=openapi.FORMAT_URI
|
153
|
-
)
|
154
|
-
|
155
|
-
|
156
|
-
def _view_supports_audittrail(view: viewsets.ViewSet) -> bool:
|
157
|
-
if not AUDIT_TRAIL_ENABLED:
|
158
|
-
return False
|
159
|
-
|
160
|
-
if not hasattr(view, "action"):
|
161
|
-
logger.debug("Could not determine view action for view %r", view)
|
162
|
-
return False
|
163
|
-
|
164
|
-
# local imports, since you get errors if you try to import non-installed app
|
165
|
-
# models
|
166
|
-
from ..audittrails.viewsets import AuditTrailMixin
|
167
|
-
|
168
|
-
relevant_bases = [
|
169
|
-
base for base in view.__class__.__bases__ if issubclass(base, AuditTrailMixin)
|
170
|
-
]
|
171
|
-
if not relevant_bases:
|
172
|
-
return False
|
173
|
-
|
174
|
-
# check if the view action is listed in any of the audit trail mixins
|
175
|
-
action = view.action
|
176
|
-
if action == "partial_update": # partial update is self.update(partial=True)
|
177
|
-
action = "update"
|
178
|
-
|
179
|
-
# if the current view action is not provided by any of the audit trail
|
180
|
-
# related bases, then it's not audit trail enabled
|
181
|
-
action_in_audit_bases = any(
|
182
|
-
action in dict(inspect.getmembers(base)) for base in relevant_bases
|
183
|
-
)
|
184
|
-
|
185
|
-
return action_in_audit_bases
|
186
|
-
|
187
|
-
|
188
|
-
class ResponseRef(openapi._Ref):
|
189
|
-
def __init__(self, resolver, response_name, ignore_unresolved=False):
|
190
|
-
"""
|
191
|
-
Adds a reference to a named Response defined in the ``#/responses/`` object.
|
192
|
-
"""
|
193
|
-
assert "responses" in resolver.scopes
|
194
|
-
super().__init__(
|
195
|
-
resolver, response_name, "responses", openapi.Response, ignore_unresolved
|
196
|
-
)
|
197
|
-
|
198
|
-
|
199
|
-
class AutoSchema(SwaggerAutoSchema):
|
200
|
-
@property
|
201
|
-
def model(self):
|
202
|
-
if hasattr(self.view, "queryset") and self.view.queryset is not None:
|
203
|
-
return self.view.queryset.model
|
204
|
-
|
205
|
-
if hasattr(self.view, "get_queryset"):
|
206
|
-
qs = self.view.get_queryset()
|
207
|
-
return qs.model
|
208
|
-
return None
|
209
|
-
|
210
|
-
@property
|
211
|
-
def _is_search_view(self):
|
212
|
-
return is_search_view(self.view)
|
213
|
-
|
214
|
-
def get_operation_id(self, operation_keys=None) -> str:
|
215
|
-
"""
|
216
|
-
Simply return the model name as lowercase string, postfixed with the operation name.
|
217
|
-
"""
|
218
|
-
operation_keys = operation_keys or self.operation_keys
|
219
|
-
|
220
|
-
operation_id = self.overrides.get("operation_id", "")
|
221
|
-
if operation_id:
|
222
|
-
return operation_id
|
223
|
-
|
224
|
-
action = operation_keys[-1]
|
225
|
-
if self.model is not None:
|
226
|
-
model_name = self.model._meta.model_name
|
227
|
-
return f"{model_name}_{action}"
|
228
|
-
else:
|
229
|
-
operation_id = "_".join(operation_keys)
|
230
|
-
return operation_id
|
231
|
-
|
232
|
-
def should_page(self):
|
233
|
-
if self._is_search_view:
|
234
|
-
return hasattr(self.view, "paginator")
|
235
|
-
return super().should_page()
|
236
|
-
|
237
|
-
def get_request_serializer(self):
|
238
|
-
if not self._is_search_view:
|
239
|
-
return super().get_request_serializer()
|
240
|
-
|
241
|
-
Base = self.view.get_search_input_serializer_class()
|
242
|
-
|
243
|
-
filter_fields = []
|
244
|
-
for filter_backend in self.view.filter_backends:
|
245
|
-
filter_fields += (
|
246
|
-
self.probe_inspectors(
|
247
|
-
self.filter_inspectors, "get_filter_parameters", filter_backend()
|
248
|
-
)
|
249
|
-
or []
|
250
|
-
)
|
251
|
-
|
252
|
-
filters = {}
|
253
|
-
for parameter in filter_fields:
|
254
|
-
help_text = parameter.description
|
255
|
-
# we can't get the verbose_label back from the enum, so the inspector
|
256
|
-
# in vng_api_common.inspectors.fields leaves a filter field reference behind
|
257
|
-
_filter_field = getattr(parameter, "_filter_field", None)
|
258
|
-
choices = getattr(_filter_field, "extra", {}).get("choices", [])
|
259
|
-
if choices:
|
260
|
-
FieldClass = serializers.ChoiceField
|
261
|
-
extra = {"choices": choices}
|
262
|
-
value_display_mapping = add_choice_values_help_text(choices)
|
263
|
-
help_text += f"\n\n{value_display_mapping}"
|
264
|
-
else:
|
265
|
-
FieldClass = TYPE_TO_FIELDMAPPING[parameter.type]
|
266
|
-
extra = {}
|
267
|
-
|
268
|
-
filters[parameter.name] = FieldClass(
|
269
|
-
help_text=help_text, required=parameter.required, **extra
|
270
|
-
)
|
271
|
-
|
272
|
-
SearchSerializer = type(Base.__name__, (Base,), filters)
|
273
|
-
return SearchSerializer()
|
274
|
-
|
275
|
-
def _get_search_responses(self):
|
276
|
-
response_status = status.HTTP_200_OK
|
277
|
-
response_schema = self.serializer_to_schema(self.get_view_serializer())
|
278
|
-
schema = openapi.Schema(type=openapi.TYPE_ARRAY, items=response_schema)
|
279
|
-
if self.should_page():
|
280
|
-
schema = self.get_paginated_response(schema) or schema
|
281
|
-
return OrderedDict({str(response_status): schema})
|
282
|
-
|
283
|
-
def register_error_responses(self):
|
284
|
-
ref_responses = self.components.with_scope("responses")
|
285
|
-
|
286
|
-
if not ref_responses.keys():
|
287
|
-
# general errors
|
288
|
-
general_classes = list(chain(*DEFAULT_ACTION_ERRORS.values()))
|
289
|
-
# add geo and validation errors
|
290
|
-
exception_classes = general_classes + [
|
291
|
-
PreconditionFailed,
|
292
|
-
exceptions.ValidationError,
|
293
|
-
]
|
294
|
-
status_codes = sorted({e.status_code for e in exception_classes})
|
295
|
-
|
296
|
-
fout_schema = self.serializer_to_schema(FoutSerializer())
|
297
|
-
validation_fout_schema = self.serializer_to_schema(
|
298
|
-
ValidatieFoutSerializer()
|
299
|
-
)
|
300
|
-
for status_code in status_codes:
|
301
|
-
schema = (
|
302
|
-
validation_fout_schema
|
303
|
-
if status_code == exceptions.ValidationError.status_code
|
304
|
-
else fout_schema
|
305
|
-
)
|
306
|
-
response = openapi.Response(
|
307
|
-
description=HTTP_STATUS_CODE_TITLES.get(status_code, ""),
|
308
|
-
schema=schema,
|
309
|
-
)
|
310
|
-
self.set_response_headers(str(status_code), response)
|
311
|
-
ref_responses.set(str(status_code), response)
|
312
|
-
|
313
|
-
def _get_error_responses(self) -> OrderedDict:
|
314
|
-
"""
|
315
|
-
Add the appropriate possible error responses to the schema.
|
316
|
-
|
317
|
-
E.g. - we know that HTTP 400 on a POST/PATCH/PUT leads to validation
|
318
|
-
errors, 403 to Permission Denied etc.
|
319
|
-
"""
|
320
|
-
# only supports viewsets
|
321
|
-
if not hasattr(self.view, "action"):
|
322
|
-
return OrderedDict()
|
323
|
-
|
324
|
-
self.register_error_responses()
|
325
|
-
|
326
|
-
action = self.view.action
|
327
|
-
if (
|
328
|
-
action not in DEFAULT_ACTION_ERRORS and self._is_search_view
|
329
|
-
): # similar to a CREATE
|
330
|
-
action = "create"
|
331
|
-
|
332
|
-
# general errors
|
333
|
-
general_klasses = DEFAULT_ACTION_ERRORS.get(action)
|
334
|
-
if general_klasses is None:
|
335
|
-
logger.debug("Unknown action %s, no default error responses added")
|
336
|
-
return OrderedDict()
|
337
|
-
|
338
|
-
exception_klasses = general_klasses[:]
|
339
|
-
# add geo and validation errors
|
340
|
-
has_validation_errors = self.get_filter_parameters() or any(
|
341
|
-
issubclass(klass, exceptions.ValidationError) for klass in exception_klasses
|
342
|
-
)
|
343
|
-
if has_validation_errors:
|
344
|
-
exception_klasses.append(exceptions.ValidationError)
|
345
|
-
|
346
|
-
if isinstance(self.view, GeoMixin):
|
347
|
-
exception_klasses.append(PreconditionFailed)
|
348
|
-
|
349
|
-
status_codes = sorted({e.status_code for e in exception_klasses})
|
350
|
-
|
351
|
-
return OrderedDict(
|
352
|
-
[
|
353
|
-
(status_code, ResponseRef(self.components, str(status_code)))
|
354
|
-
for status_code in status_codes
|
355
|
-
]
|
356
|
-
)
|
357
|
-
|
358
|
-
def get_default_responses(self) -> OrderedDict:
|
359
|
-
if self._is_search_view:
|
360
|
-
responses = self._get_search_responses()
|
361
|
-
serializer = self.get_view_serializer()
|
362
|
-
else:
|
363
|
-
responses = super().get_default_responses()
|
364
|
-
serializer = self.get_request_serializer() or self.get_view_serializer()
|
365
|
-
|
366
|
-
# inject any headers
|
367
|
-
_responses = OrderedDict()
|
368
|
-
custom_headers = OrderedDict()
|
369
|
-
for status_, schema in responses.items():
|
370
|
-
if serializer is not None:
|
371
|
-
custom_headers = (
|
372
|
-
self.probe_inspectors(
|
373
|
-
self.field_inspectors,
|
374
|
-
"get_response_headers",
|
375
|
-
serializer,
|
376
|
-
{"field_inspectors": self.field_inspectors},
|
377
|
-
status=status_,
|
378
|
-
)
|
379
|
-
or OrderedDict()
|
380
|
-
)
|
381
|
-
|
382
|
-
# add the cache headers, if applicable
|
383
|
-
for header, header_schema in get_cache_headers(self.view).items():
|
384
|
-
custom_headers[header] = header_schema
|
385
|
-
|
386
|
-
assert isinstance(schema, openapi.Schema.OR_REF) or schema == ""
|
387
|
-
response = openapi.Response(
|
388
|
-
description=HTTP_STATUS_CODE_TITLES.get(int(status_), ""),
|
389
|
-
schema=schema or None,
|
390
|
-
headers=custom_headers,
|
391
|
-
)
|
392
|
-
_responses[status_] = response
|
393
|
-
|
394
|
-
for status_code, response in self._get_error_responses().items():
|
395
|
-
_responses[status_code] = response
|
396
|
-
|
397
|
-
return _responses
|
398
|
-
|
399
|
-
@staticmethod
|
400
|
-
def set_response_headers(
|
401
|
-
status_code: str, response: Union[openapi.Response, ResponseRef]
|
402
|
-
):
|
403
|
-
if not isinstance(response, openapi.Response):
|
404
|
-
return
|
405
|
-
|
406
|
-
response.setdefault("headers", OrderedDict())
|
407
|
-
response["headers"][VERSION_HEADER] = version_header
|
408
|
-
|
409
|
-
if status_code == "201":
|
410
|
-
response["headers"]["Location"] = location_header
|
411
|
-
|
412
|
-
def get_response_schemas(self, response_serializers):
|
413
|
-
# parent class doesn't support responses as ref objects,
|
414
|
-
# so we temporary remove them
|
415
|
-
ref_responses = OrderedDict()
|
416
|
-
for status_code, serializer in response_serializers.copy().items():
|
417
|
-
if isinstance(serializer, ResponseRef):
|
418
|
-
ref_responses[str(status_code)] = response_serializers.pop(status_code)
|
419
|
-
|
420
|
-
responses = super().get_response_schemas(response_serializers)
|
421
|
-
|
422
|
-
# and add them again
|
423
|
-
responses.update(ref_responses)
|
424
|
-
responses = OrderedDict(sorted(responses.items()))
|
425
|
-
|
426
|
-
# add the Api-Version headers
|
427
|
-
for status_code, response in responses.items():
|
428
|
-
self.set_response_headers(status_code, response)
|
429
|
-
|
430
|
-
return responses
|
431
|
-
|
432
|
-
def get_request_content_type_header(self) -> Optional[openapi.Parameter]:
|
433
|
-
if self.method not in ["POST", "PUT", "PATCH"]:
|
434
|
-
return None
|
435
|
-
|
436
|
-
consumes = get_consumes(self.get_parser_classes())
|
437
|
-
return openapi.Parameter(
|
438
|
-
name="Content-Type",
|
439
|
-
in_=openapi.IN_HEADER,
|
440
|
-
type=openapi.TYPE_STRING,
|
441
|
-
required=True,
|
442
|
-
enum=consumes,
|
443
|
-
description=_("Content type of the request body."),
|
444
|
-
)
|
445
|
-
|
446
|
-
def add_manual_parameters(self, parameters):
|
447
|
-
base = super().add_manual_parameters(parameters)
|
448
|
-
|
449
|
-
content_type = self.get_request_content_type_header()
|
450
|
-
if content_type is not None:
|
451
|
-
base = [content_type] + base
|
452
|
-
|
453
|
-
if self._is_search_view:
|
454
|
-
serializer = self.get_request_serializer()
|
455
|
-
else:
|
456
|
-
serializer = self.get_request_serializer() or self.get_view_serializer()
|
457
|
-
|
458
|
-
extra = []
|
459
|
-
if serializer is not None:
|
460
|
-
extra = (
|
461
|
-
self.probe_inspectors(
|
462
|
-
self.field_inspectors,
|
463
|
-
"get_request_header_parameters",
|
464
|
-
serializer,
|
465
|
-
{"field_inspectors": self.field_inspectors},
|
466
|
-
)
|
467
|
-
or []
|
468
|
-
)
|
469
|
-
result = base + extra
|
470
|
-
|
471
|
-
if has_cache_header(self.view):
|
472
|
-
result += CACHE_REQUEST_HEADERS
|
473
|
-
|
474
|
-
if _view_supports_audittrail(self.view):
|
475
|
-
result += AUDIT_REQUEST_HEADERS
|
476
|
-
|
477
|
-
return result
|
478
|
-
|
479
|
-
def get_security(self):
|
480
|
-
"""Return a list of security requirements for this operation.
|
481
|
-
|
482
|
-
Returning an empty list marks the endpoint as unauthenticated (i.e. removes all accepted
|
483
|
-
authentication schemes). Returning ``None`` will inherit the top-level secuirty requirements.
|
484
|
-
|
485
|
-
:return: security requirements
|
486
|
-
:rtype: list[dict[str,list[str]]]"""
|
487
|
-
permissions = self.view.get_permissions()
|
488
|
-
scope_permissions = [
|
489
|
-
perm for perm in permissions if isinstance(perm, BaseAuthRequired)
|
490
|
-
]
|
491
|
-
|
492
|
-
if not scope_permissions:
|
493
|
-
return super().get_security()
|
494
|
-
|
495
|
-
if len(permissions) != len(scope_permissions):
|
496
|
-
logger.warning(
|
497
|
-
"Can't represent all permissions in OAS for path %s and method %s",
|
498
|
-
self.path,
|
499
|
-
self.method,
|
500
|
-
)
|
501
|
-
|
502
|
-
required_scopes = []
|
503
|
-
for perm in scope_permissions:
|
504
|
-
scopes = get_required_scopes(self.request, self.view)
|
505
|
-
if scopes is None:
|
506
|
-
continue
|
507
|
-
required_scopes.append(scopes)
|
508
|
-
|
509
|
-
if not required_scopes:
|
510
|
-
return None # use global security
|
511
|
-
|
512
|
-
scopes = [str(scope) for scope in sorted(required_scopes)]
|
513
|
-
|
514
|
-
# operation level security
|
515
|
-
return [{settings.SECURITY_DEFINITION_NAME: scopes}]
|
516
|
-
|
517
|
-
# all of these break if you accept method HEAD because the view.action is None
|
518
|
-
def is_list_view(self) -> bool:
|
519
|
-
if self.method == "HEAD":
|
520
|
-
return False
|
521
|
-
return super().is_list_view()
|
522
|
-
|
523
|
-
def get_summary_and_description(self) -> Tuple[str, str]:
|
524
|
-
if self.method != "HEAD":
|
525
|
-
return super().get_summary_and_description()
|
526
|
-
|
527
|
-
default_description = _(
|
528
|
-
"De headers voor een specifiek(e) {model_name} opvragen"
|
529
|
-
).format(model_name=self.model._meta.model_name.upper())
|
530
|
-
default_summary = _(
|
531
|
-
"Vraag de headers op die je bij een GET request zou krijgen."
|
532
|
-
)
|
533
|
-
|
534
|
-
description = self.overrides.get("operation_description", default_description)
|
535
|
-
summary = self.overrides.get("operation_summary", default_summary)
|
536
|
-
return description, summary
|
537
|
-
|
538
|
-
# patch around drf-yasg not taking overrides into account
|
539
|
-
# TODO: contribute back in PR
|
540
|
-
def get_produces(self) -> list:
|
541
|
-
produces = super().get_produces()
|
542
|
-
return self.overrides.get("produces", produces)
|
543
|
-
|
544
|
-
|
545
|
-
# translations aren't picked up/defined in DRF, so we need to hook them up here
|
546
|
-
_("A page number within the paginated result set.")
|
547
|
-
_("Number of results to return per page.")
|
@@ -1,43 +0,0 @@
|
|
1
|
-
import logging
|
2
|
-
import os
|
3
|
-
|
4
|
-
from django.conf import settings
|
5
|
-
from django.core.management import BaseCommand
|
6
|
-
from django.template.loader import render_to_string
|
7
|
-
|
8
|
-
from ...scopes import SCOPE_REGISTRY
|
9
|
-
|
10
|
-
|
11
|
-
class Command(BaseCommand):
|
12
|
-
"""
|
13
|
-
Generate a markdown file documenting the auth scopes of the component
|
14
|
-
"""
|
15
|
-
|
16
|
-
def add_arguments(self, parser):
|
17
|
-
super().add_arguments(parser)
|
18
|
-
|
19
|
-
parser.add_argument(
|
20
|
-
"--output-file",
|
21
|
-
dest="output_file",
|
22
|
-
default=None,
|
23
|
-
help="Name of the output file",
|
24
|
-
)
|
25
|
-
|
26
|
-
def handle(self, output_file, *args, **options):
|
27
|
-
scopes = sorted(
|
28
|
-
(scope for scope in SCOPE_REGISTRY if not scope.children),
|
29
|
-
key=lambda s: s.label,
|
30
|
-
)
|
31
|
-
|
32
|
-
template = "vng_api_common/autorisaties.md"
|
33
|
-
markdown = render_to_string(
|
34
|
-
template,
|
35
|
-
context={
|
36
|
-
"scopes": scopes,
|
37
|
-
"project_name": settings.PROJECT_NAME,
|
38
|
-
"site_title": settings.SITE_TITLE,
|
39
|
-
},
|
40
|
-
)
|
41
|
-
|
42
|
-
with open(output_file, "w") as f:
|
43
|
-
f.write(markdown)
|
@@ -1,40 +0,0 @@
|
|
1
|
-
import logging
|
2
|
-
import os
|
3
|
-
|
4
|
-
from django.conf import settings
|
5
|
-
from django.core.management import BaseCommand
|
6
|
-
from django.template.loader import render_to_string
|
7
|
-
|
8
|
-
from notifications_api_common.kanalen import KANAAL_REGISTRY
|
9
|
-
|
10
|
-
|
11
|
-
class Command(BaseCommand):
|
12
|
-
"""
|
13
|
-
Generate a markdown file documenting the notification channels of the component
|
14
|
-
"""
|
15
|
-
|
16
|
-
def add_arguments(self, parser):
|
17
|
-
super().add_arguments(parser)
|
18
|
-
|
19
|
-
parser.add_argument(
|
20
|
-
"--output-file",
|
21
|
-
dest="output_file",
|
22
|
-
default=None,
|
23
|
-
help="Name of the output file",
|
24
|
-
)
|
25
|
-
|
26
|
-
def handle(self, output_file, *args, **options):
|
27
|
-
kanalen = sorted(KANAAL_REGISTRY, key=lambda s: s.label)
|
28
|
-
|
29
|
-
template = "vng_api_common/notificaties.md"
|
30
|
-
markdown = render_to_string(
|
31
|
-
template,
|
32
|
-
context={
|
33
|
-
"kanalen": kanalen,
|
34
|
-
"project_name": settings.PROJECT_NAME,
|
35
|
-
"site_title": settings.SITE_TITLE,
|
36
|
-
},
|
37
|
-
)
|
38
|
-
|
39
|
-
with open(output_file, "w") as f:
|
40
|
-
f.write(markdown)
|