Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master' into pr1312
Browse files Browse the repository at this point in the history
  • Loading branch information
tfranzel committed Nov 30, 2024
2 parents 8c9c7d7 + caf707d commit 40be03d
Show file tree
Hide file tree
Showing 10 changed files with 126 additions and 131 deletions.
7 changes: 5 additions & 2 deletions drf_spectacular/contrib/django_oauth_toolkit.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,11 @@ def get_security_definition(self, auto_schema):
flows[flow_type]['tokenUrl'] = spectacular_settings.OAUTH2_TOKEN_URL
if spectacular_settings.OAUTH2_REFRESH_URL:
flows[flow_type]['refreshUrl'] = spectacular_settings.OAUTH2_REFRESH_URL
scope_backend = get_scopes_backend()
flows[flow_type]['scopes'] = scope_backend.get_all_scopes()
if spectacular_settings.OAUTH2_SCOPES:
flows[flow_type]['scopes'] = spectacular_settings.OAUTH2_SCOPES
else:
scope_backend = get_scopes_backend()
flows[flow_type]['scopes'] = scope_backend.get_all_scopes()

return {
'type': 'oauth2',
Expand Down
15 changes: 11 additions & 4 deletions drf_spectacular/openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,14 +221,18 @@ def _process_override_parameters(self, direction='request'):
parameter = force_instance(parameter)
mapped = self._map_serializer(parameter, 'request')
for property_name, property_schema in mapped['properties'].items():
field = parameter.fields.get(property_name)
try:
# be graceful when serializer might be non-DRF (via extension).
field = parameter.fields.get(property_name)
except Exception:
field = None
result[property_name, OpenApiParameter.QUERY] = build_parameter_type(
name=property_name,
schema=property_schema,
description=property_schema.pop('description', None),
location=OpenApiParameter.QUERY,
allow_blank=getattr(field, 'allow_blank', True),
required=field.required,
required=bool(property_name in mapped.get('required', [])),
)
else:
warn(f'could not resolve parameter annotation {parameter}. Skipping.')
Expand Down Expand Up @@ -695,7 +699,10 @@ def _map_serializer_field(self, field, direction, bypass_extensions=False):
# read_only fields do not have a Manager by design. go around and get field
# from parent. also avoid calling Manager. __bool__ as it might be customized
# to hit the database.
if getattr(field, 'queryset', None) is not None:
if not is_slug and getattr(field, 'pk_field') is not None:
schema = self._map_serializer_field(field.pk_field, direction)
return append_meta(schema, meta)
elif getattr(field, 'queryset', None) is not None:
if is_slug:
model = field.queryset.model
source = [field.slug_field]
Expand All @@ -714,7 +721,7 @@ def _map_serializer_field(self, field, direction, bypass_extensions=False):
f'Could not derive type for under-specified {field.__class__.__name__} '
f'"{field.field_name}". The serializer has no associated model (Meta class) '
f'and this particular field has no type without a model association. Consider '
f'changing the field or adding a Meta class. defaulting to string.'
f'changing the field or adding a Meta class. Defaulting to string.'
)
return append_meta(build_basic_type(OpenApiTypes.STR), meta)

Expand Down
11 changes: 7 additions & 4 deletions drf_spectacular/plumbing.py
Original file line number Diff line number Diff line change
Expand Up @@ -420,8 +420,13 @@ def build_parameter_type(
def build_choice_field(field) -> _SchemaType:
choices = list(OrderedDict.fromkeys(field.choices)) # preserve order and remove duplicates

if all(isinstance(choice, bool) for choice in choices):
type: Optional[str] = 'boolean'
if field.allow_blank and '' not in choices:
choices.append('')

if not choices:
type = None
elif all(isinstance(choice, bool) for choice in choices):
type = 'boolean'
elif all(isinstance(choice, int) for choice in choices):
type = 'integer'
elif all(isinstance(choice, (int, float, Decimal)) for choice in choices): # `number` includes `integer`
Expand All @@ -432,8 +437,6 @@ def build_choice_field(field) -> _SchemaType:
else:
type = None

if field.allow_blank and '' not in choices:
choices.append('')
if field.allow_null and None not in choices:
choices.append(None)

Expand Down
40 changes: 40 additions & 0 deletions tests/contrib/test_oauth_toolkit.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,43 @@ def test_oauth2_toolkit_scopes_backend(no_warnings):
assert 'implicit' in oauth2['flows']
flow = oauth2['flows']['implicit']
assert 'test_backend_scope' in flow['scopes']


@mock.patch(
'drf_spectacular.settings.spectacular_settings.OAUTH2_SCOPES',
{"read": "Read scope", "burn": "Burn scope"},
)
@mock.patch(
'drf_spectacular.settings.spectacular_settings.OAUTH2_FLOWS',
['implicit']
)
@mock.patch(
'drf_spectacular.settings.spectacular_settings.OAUTH2_REFRESH_URL',
'http://127.0.0.1:8000/o/refresh'
)
@mock.patch(
'drf_spectacular.settings.spectacular_settings.OAUTH2_AUTHORIZATION_URL',
'http://127.0.0.1:8000/o/authorize'
)
@mock.patch(
'oauth2_provider.settings.oauth2_settings.SCOPES',
{"read": "Reading scope", "write": "Writing scope", "extra_scope": "Extra Scope"},
)
@mock.patch(
'oauth2_provider.settings.oauth2_settings.DEFAULT_SCOPES',
["read", "write"]
)
@pytest.mark.contrib('oauth2_provider')
def test_oauth2_toolkit_custom_scopes(no_warnings):
router = routers.SimpleRouter()
router.register('TokenHasReadWriteScope', TokenHasReadWriteScopeViewset, basename="x1")

urlpatterns = [
*router.urls,
path('o/', include('oauth2_provider.urls', namespace='oauth2_provider')),
]
schema = generate_schema(None, patterns=urlpatterns)

assert schema['components']['securitySchemes']['oauth2']['flows']['implicit']['scopes'] == {
'burn': 'Burn scope', 'read': 'Read scope'
}
19 changes: 19 additions & 0 deletions tests/contrib/test_pydantic.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,22 @@ def post(self, request):

schema = generate_schema('/x', view=XAPIView)
assert_schema(schema, 'tests/contrib/test_pydantic.yml')


@pytest.mark.contrib('pydantic')
@pytest.mark.skipif(sys.version_info < (3, 7), reason='python 3.7+ is required by package')
def test_pydantic_as_query_parameters(no_warnings):
class QueryParams(BaseModel):
foo: int
bar: str

class XAPIView(APIView):
@extend_schema(responses=str, parameters=[QueryParams])
def get(self, request):
pass # pragma: no cover

schema = generate_schema('/x', view=XAPIView)
assert schema['paths']['/x']['get']['parameters'] == [
{'in': 'query', 'name': 'bar', 'required': True, 'schema': {'title': 'Bar', 'type': 'string'}},
{'in': 'query', 'name': 'foo', 'required': True, 'schema': {'title': 'Foo', 'type': 'integer'}}
]
4 changes: 3 additions & 1 deletion tests/contrib/test_rest_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@

transforms = [
# User model first_name differences
lambda x: re.sub(r'(first_name:\n *type: string\n *maxLength:) 30', r'\g<1> 150', x, re.M),
lambda x: re.sub(r'(first_name:\n *type: string\n *maxLength:) 30', r'\g<1> 150', x),
# Ignore descriptions as it varies too much between versions
lambda x: re.sub(r'description: \|-\n[\S\s\r\n]+?tags:', r'tags:', x),
]


Expand Down
57 changes: 0 additions & 57 deletions tests/contrib/test_rest_auth.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,6 @@ paths:
/rest-auth/login/:
post:
operationId: rest_auth_login_create
description: |-
Check the credentials and return the REST Token
if the credentials are valid and authenticated.
Calls Django Auth login method to register User ID
in Django session framework
Accept the following POST parameters: username, password
Return the REST Framework Token Object's key.
tags:
- rest-auth
requestBody:
Expand Down Expand Up @@ -42,11 +34,6 @@ paths:
/rest-auth/logout/:
post:
operationId: rest_auth_logout_create
description: |-
Calls Django logout method and delete the Token object
assigned to the current User object.
Accepts/Returns nothing.
tags:
- rest-auth
security:
Expand All @@ -63,11 +50,6 @@ paths:
/rest-auth/password/change/:
post:
operationId: rest_auth_password_change_create
description: |-
Calls Django Auth SetPasswordForm save method.
Accepts the following POST parameters: new_password1, new_password2
Returns the success/fail message.
tags:
- rest-auth
requestBody:
Expand Down Expand Up @@ -95,11 +77,6 @@ paths:
/rest-auth/password/reset/:
post:
operationId: rest_auth_password_reset_create
description: |-
Calls Django Auth PasswordResetForm save method.
Accepts the following POST parameters: email
Returns the success/fail message.
tags:
- rest-auth
requestBody:
Expand Down Expand Up @@ -128,13 +105,6 @@ paths:
/rest-auth/password/reset/confirm/:
post:
operationId: rest_auth_password_reset_confirm_create
description: |-
Password reset e-mail link is confirmed, therefore
this resets the user's password.
Accepts the following POST parameters: token, uid,
new_password1, new_password2
Returns the success/fail message.
tags:
- rest-auth
requestBody:
Expand Down Expand Up @@ -246,15 +216,6 @@ paths:
/rest-auth/user/:
get:
operationId: rest_auth_user_retrieve
description: |-
Reads and updates UserModel fields
Accepts GET, PUT, PATCH methods.
Default accepted fields: username, first_name, last_name
Default display fields: pk, username, email, first_name, last_name
Read-only fields: pk, email
Returns UserModel fields.
tags:
- rest-auth
security:
Expand All @@ -269,15 +230,6 @@ paths:
description: ''
put:
operationId: rest_auth_user_update
description: |-
Reads and updates UserModel fields
Accepts GET, PUT, PATCH methods.
Default accepted fields: username, first_name, last_name
Default display fields: pk, username, email, first_name, last_name
Read-only fields: pk, email
Returns UserModel fields.
tags:
- rest-auth
requestBody:
Expand All @@ -304,15 +256,6 @@ paths:
description: ''
patch:
operationId: rest_auth_user_partial_update
description: |-
Reads and updates UserModel fields
Accepts GET, PUT, PATCH methods.
Default accepted fields: username, first_name, last_name
Default display fields: pk, username, email, first_name, last_name
Read-only fields: pk, email
Returns UserModel fields.
tags:
- rest-auth
requestBody:
Expand Down
Loading

0 comments on commit 40be03d

Please sign in to comment.