Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 13311582ea | |||
| 9a89d8ccb0 | |||
| 16f67cd8c2 | |||
| 99fa7c25ca | |||
| 97e70d9d16 | |||
| ee086a6eec | |||
| 4af38c970a | |||
| 95337f85ad | |||
| 1352c2a23b | |||
| 8578b93eba | |||
| 212891b1b8 | |||
| ab6444a32e | |||
| 2e0f9a19a9 | |||
| cda808fe11 | |||
| e6219ab8b7 |
+3
-6
@@ -4,12 +4,13 @@ python:
|
||||
- '3.5'
|
||||
- '3.6'
|
||||
- '3.7'
|
||||
- '3.8-dev'
|
||||
|
||||
dist: xenial
|
||||
|
||||
cache: pip
|
||||
|
||||
jobs:
|
||||
matrix:
|
||||
include:
|
||||
- python: '3.6'
|
||||
env: TOXENV=docs
|
||||
@@ -38,6 +39,7 @@ jobs:
|
||||
allow_failures:
|
||||
- env: TOXENV=lint
|
||||
- env: TOXENV=djmaster
|
||||
- python: '3.8-dev'
|
||||
|
||||
fast_finish: true
|
||||
|
||||
@@ -59,11 +61,6 @@ after_success:
|
||||
codecov
|
||||
fi
|
||||
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- /^v?\d+\.\d+(\.\d+)?(-?\S+)?$/
|
||||
|
||||
stages:
|
||||
- test
|
||||
- name: publish
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
Contributing
|
||||
############
|
||||
|
||||
Contributions are always welcome and appreciated! Here are some ways you can contribut.
|
||||
Contributions are always welcome and appreciated! Here are some ways you can contribute.
|
||||
|
||||
******
|
||||
Issues
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ Generate **real** Swagger/OpenAPI 2.0 specifications from a Django Rest Framewor
|
||||
|
||||
Compatible with
|
||||
|
||||
- **Django Rest Framework**: 3.8, 3.9
|
||||
- **Django Rest Framework**: 3.8, 3.9, 3.10
|
||||
- **Django**: 1.11, 2.1, 2.2
|
||||
- **Python**: 2.7, 3.5, 3.6, 3.7
|
||||
|
||||
|
||||
+12
-1
@@ -2,6 +2,17 @@
|
||||
Changelog
|
||||
#########
|
||||
|
||||
**********
|
||||
**1.17.0**
|
||||
**********
|
||||
|
||||
*Release date: Oct 03, 2019*
|
||||
|
||||
- **ADDED:** added `JSONFieldInspector` for `JSONField` support (:pr:`417`)
|
||||
- **IMPROVED:** updated ``swagger-ui`` to version 3.23.11
|
||||
- **IMPROVED:** updated ``ReDoc`` to version 2.0.0-rc.14 (:issue:`398`)
|
||||
- **FIXED:** fixed a type hint support issue (:pr:`428`, :issue:`450`)
|
||||
- **FIXED:** fixed packaging issue caused by a missing requirement (:issue:`412`)
|
||||
|
||||
**********
|
||||
**1.16.1**
|
||||
@@ -9,8 +20,8 @@ Changelog
|
||||
|
||||
*Release date: Jul 16, 2019*
|
||||
|
||||
- **IMPROVED:** better enum type detection for nested `ChoiceField`\ s (:pr:`400`)
|
||||
- **FIXED:** fixed DRF 3.10 compatibility (:pr:`408`, :issue:`410`, :issue:`411`)
|
||||
- **IMPROVED:** better enum type detection for nested `ChoiceField`s (:pr:`400`)
|
||||
|
||||
**********
|
||||
**1.16.0**
|
||||
|
||||
@@ -45,6 +45,8 @@ some properties of the generated :class:`.Operation`. For example, in a ``ViewSe
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
|
||||
@swagger_auto_schema(operation_description="partial_update description override", responses={404: 'slug not found'})
|
||||
def partial_update(self, request, *args, **kwargs):
|
||||
"""partial_update method docstring"""
|
||||
@@ -376,7 +378,7 @@ A second example, of a :class:`~.inspectors.FieldInspector` that removes the ``t
|
||||
|
||||
|
||||
class AnotherSerializer(serializers.ModelSerializer):
|
||||
chilf = OneSerializer()
|
||||
child = OneSerializer()
|
||||
|
||||
class Meta:
|
||||
model = SomeParentModel
|
||||
|
||||
+2
-1
@@ -85,6 +85,7 @@ to this list.
|
||||
:class:`'drf_yasg.inspectors.ChoiceFieldInspector' <.inspectors.ChoiceFieldInspector>`, |br| \
|
||||
:class:`'drf_yasg.inspectors.FileFieldInspector' <.inspectors.FileFieldInspector>`, |br| \
|
||||
:class:`'drf_yasg.inspectors.DictFieldInspector' <.inspectors.DictFieldInspector>`, |br| \
|
||||
:class:`'drf_yasg.inspectors.JSONFieldInspector' <.inspectors.JSONFieldInspector>`, |br| \
|
||||
:class:`'drf_yasg.inspectors.HiddenFieldInspector' <.inspectors.HiddenFieldInspector>`, |br| \
|
||||
:class:`'drf_yasg.inspectors.RecursiveFieldInspector' <.inspectors.RecursiveFieldInspector>`, |br| \
|
||||
:class:`'drf_yasg.inspectors.SerializerMethodFieldInspector' <.inspectors.SerializerMethodFieldInspector>`, |br| \
|
||||
@@ -454,7 +455,7 @@ FETCH_SCHEMA_WITH_QUERY
|
||||
|
||||
Fetch the OpenAPI document using the query parameters passed to the ReDoc page request.
|
||||
|
||||
**Default**: :python:`'True` |br|
|
||||
**Default**: :python:`True` |br|
|
||||
*Maps to parameter*: -
|
||||
|
||||
|
||||
|
||||
Generated
+260
-246
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "drf-yasg",
|
||||
"dependencies": {
|
||||
"redoc": "^2.0.0-rc.8-1",
|
||||
"swagger-ui-dist": "^3.22.3"
|
||||
"redoc": "^2.0.0-rc.14",
|
||||
"swagger-ui-dist": "^3.23.11"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -4,6 +4,7 @@ ruamel.yaml>=0.15.34
|
||||
inflection>=0.3.1
|
||||
six>=1.10.0
|
||||
uritemplate>=3.0.0
|
||||
packaging
|
||||
|
||||
djangorestframework>=3.8
|
||||
Django>=1.11.7
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
# test project requirements
|
||||
Pillow>=4.3.0
|
||||
django-cors-headers>=2.1.0
|
||||
django-filter>=1.1.0,<2.0; python_version == "2.7"
|
||||
django-filter>=1.1.0; python_version >= "3.5"
|
||||
#djangorestframework-camel-case>=0.2.0
|
||||
@@ -9,5 +8,3 @@ django-filter>=1.1.0; python_version >= "3.5"
|
||||
djangorestframework-recursive>=0.1.2
|
||||
dj-database-url>=0.4.2
|
||||
user_agents>=1.1.0
|
||||
# django-oauth-toolkit 1.2 does not support Django 1.11
|
||||
django-oauth-toolkit>=1.1.0,<1.2.0
|
||||
|
||||
@@ -12,6 +12,7 @@ SWAGGER_DEFAULTS = {
|
||||
'drf_yasg.inspectors.ChoiceFieldInspector',
|
||||
'drf_yasg.inspectors.FileFieldInspector',
|
||||
'drf_yasg.inspectors.DictFieldInspector',
|
||||
'drf_yasg.inspectors.JSONFieldInspector',
|
||||
'drf_yasg.inspectors.HiddenFieldInspector',
|
||||
'drf_yasg.inspectors.RelatedFieldInspector',
|
||||
'drf_yasg.inspectors.SerializerMethodFieldInspector',
|
||||
|
||||
+10
-10
@@ -3,24 +3,16 @@ import logging
|
||||
import re
|
||||
from collections import OrderedDict, defaultdict
|
||||
|
||||
import uritemplate
|
||||
import rest_framework
|
||||
import uritemplate
|
||||
from coreapi.compat import urlparse
|
||||
from packaging.version import Version
|
||||
from rest_framework import versioning
|
||||
from rest_framework.compat import URLPattern, URLResolver, get_original_route
|
||||
from rest_framework.schemas.generators import EndpointEnumerator as _EndpointEnumerator
|
||||
from rest_framework.schemas.generators import endpoint_ordering, get_pk_name
|
||||
|
||||
from rest_framework.settings import api_settings
|
||||
|
||||
from packaging.version import Version
|
||||
if Version(rest_framework.__version__) < Version('3.10'):
|
||||
from rest_framework.schemas.generators import SchemaGenerator
|
||||
from rest_framework.schemas.inspectors import get_pk_description
|
||||
else:
|
||||
from rest_framework.schemas import SchemaGenerator
|
||||
from rest_framework.schemas.utils import get_pk_description
|
||||
|
||||
from . import openapi
|
||||
from .app_settings import swagger_settings
|
||||
from .errors import SwaggerGenerationError
|
||||
@@ -28,6 +20,14 @@ from .inspectors.field import get_basic_type_info, get_queryset_field, get_query
|
||||
from .openapi import ReferenceResolver, SwaggerDict
|
||||
from .utils import force_real_str, get_consumes, get_produces
|
||||
|
||||
if Version(rest_framework.__version__) < Version('3.10'):
|
||||
from rest_framework.schemas.generators import SchemaGenerator
|
||||
from rest_framework.schemas.inspectors import get_pk_description
|
||||
else:
|
||||
from rest_framework.schemas import SchemaGenerator
|
||||
from rest_framework.schemas.utils import get_pk_description
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
PATH_PARAMETER_RE = re.compile(r'{(?P<parameter>\w+)}')
|
||||
|
||||
@@ -4,8 +4,8 @@ from .base import (
|
||||
)
|
||||
from .field import (
|
||||
CamelCaseJSONFilter, ChoiceFieldInspector, DictFieldInspector, FileFieldInspector, HiddenFieldInspector,
|
||||
InlineSerializerInspector, RecursiveFieldInspector, ReferencingSerializerInspector, RelatedFieldInspector,
|
||||
SerializerMethodFieldInspector, SimpleFieldInspector, StringDefaultFieldInspector
|
||||
InlineSerializerInspector, JSONFieldInspector, RecursiveFieldInspector, ReferencingSerializerInspector,
|
||||
RelatedFieldInspector, SerializerMethodFieldInspector, SimpleFieldInspector, StringDefaultFieldInspector
|
||||
)
|
||||
from .query import CoreAPICompatInspector, DjangoRestResponsePagination
|
||||
from .view import SwaggerAutoSchema
|
||||
@@ -24,7 +24,7 @@ __all__ = [
|
||||
|
||||
# field inspectors
|
||||
'InlineSerializerInspector', 'RecursiveFieldInspector', 'ReferencingSerializerInspector', 'RelatedFieldInspector',
|
||||
'SimpleFieldInspector', 'FileFieldInspector', 'ChoiceFieldInspector', 'DictFieldInspector',
|
||||
'SimpleFieldInspector', 'FileFieldInspector', 'ChoiceFieldInspector', 'DictFieldInspector', 'JSONFieldInspector',
|
||||
'StringDefaultFieldInspector', 'CamelCaseJSONFilter', 'HiddenFieldInspector', 'SerializerMethodFieldInspector',
|
||||
|
||||
# view inspectors
|
||||
|
||||
@@ -610,6 +610,8 @@ class SerializerMethodFieldInspector(FieldInspector):
|
||||
# look for Python 3.5+ style type hinting of the return value
|
||||
hint_class = inspect_signature(method).return_annotation
|
||||
|
||||
if not inspect.isclass(hint_class) and hasattr(hint_class, '__args__'):
|
||||
hint_class = hint_class.__args__[0]
|
||||
if inspect.isclass(hint_class) and not issubclass(hint_class, inspect._empty):
|
||||
type_info = get_basic_type_info_from_hint(hint_class)
|
||||
|
||||
@@ -745,11 +747,23 @@ class HiddenFieldInspector(FieldInspector):
|
||||
return NotHandled
|
||||
|
||||
|
||||
class JSONFieldInspector(FieldInspector):
|
||||
"""Provides conversion for ``JSONField``."""
|
||||
|
||||
def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs):
|
||||
SwaggerType, ChildSwaggerType = self._get_partial_types(field, swagger_object_type, use_references, **kwargs)
|
||||
|
||||
if isinstance(field, serializers.JSONField) and swagger_object_type == openapi.Schema:
|
||||
return SwaggerType(type=openapi.TYPE_OBJECT)
|
||||
|
||||
return NotHandled
|
||||
|
||||
|
||||
class StringDefaultFieldInspector(FieldInspector):
|
||||
"""For otherwise unhandled fields, return them as plain :data:`.TYPE_STRING` objects."""
|
||||
|
||||
def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs): # pragma: no cover
|
||||
# TODO unhandled fields: TimeField JSONField
|
||||
# TODO unhandled fields: TimeField
|
||||
SwaggerType, ChildSwaggerType = self._get_partial_types(field, swagger_object_type, use_references, **kwargs)
|
||||
return SwaggerType(type=openapi.TYPE_STRING)
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import six
|
||||
|
||||
from django.shortcuts import resolve_url
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.encoding import force_str
|
||||
from django.utils.functional import Promise
|
||||
from rest_framework.renderers import BaseRenderer, JSONRenderer, TemplateHTMLRenderer
|
||||
from rest_framework.utils import encoders, json
|
||||
@@ -124,7 +124,7 @@ class SwaggerUIRenderer(_UIRenderer):
|
||||
swagger_ui_settings = self.get_swagger_ui_settings()
|
||||
|
||||
request = renderer_context.get('request', None)
|
||||
oauth_redirect_url = force_text(swagger_ui_settings.get('oauth2RedirectUrl', ''))
|
||||
oauth_redirect_url = force_str(swagger_ui_settings.get('oauth2RedirectUrl', ''))
|
||||
if request and oauth_redirect_url:
|
||||
swagger_ui_settings['oauth2RedirectUrl'] = request.build_absolute_uri(oauth_redirect_url)
|
||||
|
||||
|
||||
+2
-2
File diff suppressed because one or more lines are too long
+41
-19
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -5,7 +5,7 @@ from collections import OrderedDict
|
||||
from decimal import Decimal
|
||||
|
||||
from django.db import models
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.encoding import force_str
|
||||
from rest_framework import serializers, status
|
||||
from rest_framework.mixins import DestroyModelMixin, ListModelMixin, RetrieveModelMixin, UpdateModelMixin
|
||||
from rest_framework.parsers import FileUploadParser
|
||||
@@ -434,7 +434,7 @@ def force_real_str(s, encoding='utf-8', strings_only=False, errors='strict'):
|
||||
Fix for https://github.com/axnsan12/drf-yasg/issues/159
|
||||
"""
|
||||
if s is not None:
|
||||
s = force_text(s, encoding, strings_only, errors)
|
||||
s = force_str(s, encoding, strings_only, errors)
|
||||
if type(s) != str:
|
||||
s = '' + s
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import warnings
|
||||
from functools import wraps
|
||||
from functools import WRAPPER_ASSIGNMENTS, wraps
|
||||
|
||||
from django.utils.cache import add_never_cache_headers
|
||||
from django.utils.decorators import available_attrs
|
||||
from django.views.decorators.cache import cache_page
|
||||
from django.views.decorators.vary import vary_on_headers
|
||||
from rest_framework import exceptions
|
||||
@@ -30,7 +29,7 @@ def deferred_never_cache(view_func):
|
||||
never be cached.
|
||||
"""
|
||||
|
||||
@wraps(view_func, assigned=available_attrs(view_func))
|
||||
@wraps(view_func, assigned=WRAPPER_ASSIGNMENTS)
|
||||
def _wrapped_view_func(request, *args, **kwargs):
|
||||
response = view_func(request, *args, **kwargs)
|
||||
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import rest_framework
|
||||
from decimal import Decimal
|
||||
|
||||
import rest_framework
|
||||
from django.contrib.auth import get_user_model
|
||||
from packaging.version import Version
|
||||
from rest_framework import serializers
|
||||
|
||||
from snippets.models import LANGUAGE_CHOICES, STYLE_CHOICES, Snippet, SnippetViewer
|
||||
|
||||
if Version(rest_framework.__version__) < Version('3.10'):
|
||||
from rest_framework.compat import MaxLengthValidator, MinValueValidator
|
||||
else:
|
||||
from django.core.validators import MaxLengthValidator, MinValueValidator
|
||||
from rest_framework import serializers
|
||||
|
||||
from snippets.models import LANGUAGE_CHOICES, STYLE_CHOICES, Snippet, SnippetViewer
|
||||
|
||||
|
||||
class LanguageSerializer(serializers.Serializer):
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import json
|
||||
import sys
|
||||
from collections import OrderedDict
|
||||
|
||||
import pytest
|
||||
@@ -17,6 +18,11 @@ from drf_yasg.errors import SwaggerGenerationError
|
||||
from drf_yasg.generators import OpenAPISchemaGenerator
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
|
||||
try:
|
||||
import typing
|
||||
except ImportError:
|
||||
typing = None
|
||||
|
||||
|
||||
def test_schema_is_valid(swagger, codec_yaml):
|
||||
codec_yaml.encode(swagger)
|
||||
@@ -273,3 +279,58 @@ def test_nested_choice_in_array_field(choices, field, expected_type):
|
||||
swagger = generator.get_schema(None, True)
|
||||
property_schema = swagger['definitions']['Array']['properties']['array']['items']
|
||||
assert property_schema == openapi.Schema(title='Array', type=expected_type, enum=choices)
|
||||
|
||||
|
||||
def test_json_field():
|
||||
class TestJSONFieldSerializer(serializers.Serializer):
|
||||
json = serializers.JSONField()
|
||||
|
||||
class JSONViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = TestJSONFieldSerializer
|
||||
|
||||
router = routers.DefaultRouter()
|
||||
router.register(r'jsons', JSONViewSet, **_basename_or_base_name('jsons'))
|
||||
|
||||
generator = OpenAPISchemaGenerator(
|
||||
info=openapi.Info(title='Test json field generator', default_version='v1'),
|
||||
patterns=router.urls
|
||||
)
|
||||
|
||||
swagger = generator.get_schema(None, True)
|
||||
property_schema = swagger["definitions"]["TestJSONField"]["properties"]["json"]
|
||||
assert property_schema == openapi.Schema(title='Json', type=openapi.TYPE_OBJECT)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('py_type, expected_type', [
|
||||
(str, openapi.TYPE_STRING),
|
||||
(int, openapi.TYPE_INTEGER),
|
||||
(float, openapi.TYPE_NUMBER),
|
||||
(bool, openapi.TYPE_BOOLEAN),
|
||||
])
|
||||
@pytest.mark.skipif(typing is None or sys.version_info.major < 3, reason="typing not supported")
|
||||
def test_optional_return_type(py_type, expected_type):
|
||||
|
||||
class OptionalMethodSerializer(serializers.Serializer):
|
||||
x = serializers.SerializerMethodField()
|
||||
|
||||
def get_x(self, instance):
|
||||
pass
|
||||
|
||||
# Add the type annotation here in order to avoid a SyntaxError in py27
|
||||
get_x.__annotations__["return"] = typing.Optional[py_type]
|
||||
|
||||
class OptionalMethodViewSet(viewsets.ViewSet):
|
||||
@swagger_auto_schema(responses={200: openapi.Response("OK", OptionalMethodSerializer)})
|
||||
def retrieve(self, request, pk=None):
|
||||
return Response({'optional': None})
|
||||
|
||||
router = routers.DefaultRouter()
|
||||
router.register(r'optional', OptionalMethodViewSet, **_basename_or_base_name('optional'))
|
||||
|
||||
generator = OpenAPISchemaGenerator(
|
||||
info=openapi.Info(title='Test optional parameter', default_version='v1'),
|
||||
patterns=router.urls
|
||||
)
|
||||
swagger = generator.get_schema(None, True)
|
||||
property_schema = swagger["definitions"]["OptionalMethod"]["properties"]["x"]
|
||||
assert property_schema == openapi.Schema(title='X', type=expected_type, readOnly=True)
|
||||
|
||||
@@ -8,9 +8,10 @@ envlist =
|
||||
py27-django111-drf39-typing,
|
||||
py27-django111-drf{38,39},
|
||||
py{35,36}-django{111,21,22}-drf{38,39},
|
||||
py37-django{21,22}-drf{38,39},
|
||||
py37-django{21,22}-drf310,
|
||||
py37-django{21,22}-drf{38,39,310},
|
||||
py38-django22-drf310,
|
||||
djmaster, lint, docs
|
||||
skip_missing_interpreters = true
|
||||
|
||||
[testenv:.package]
|
||||
# no additional dependencies besides PEP 517
|
||||
@@ -19,8 +20,17 @@ deps =
|
||||
[testenv]
|
||||
deps =
|
||||
django111: Django>=1.11,<2.0
|
||||
django111: django-cors-headers>=2.1.0
|
||||
django111: django-oauth-toolkit>=1.1.0,<1.2.0
|
||||
|
||||
django21: Django>=2.1,<2.2
|
||||
django21: django-cors-headers>=2.1.0
|
||||
django21: django-oauth-toolkit>=1.2.0
|
||||
|
||||
django22: Django>=2.2,<2.3
|
||||
django22: django-cors-headers>=2.1.0
|
||||
django22: django-oauth-toolkit>=1.2.0
|
||||
|
||||
|
||||
drf38: djangorestframework>=3.8,<3.9
|
||||
drf39: djangorestframework>=3.9,<3.10
|
||||
@@ -28,9 +38,12 @@ deps =
|
||||
|
||||
typing: typing>=3.6.6
|
||||
|
||||
# test with the latest build of django-rest-framework to get early warning of compatibility issues
|
||||
djmaster: https://github.com/encode/django-rest-framework/archive/master.tar.gz
|
||||
# test with the latest builds of Django and django-rest-framework
|
||||
# to get early warning of compatibility issues
|
||||
djmaster: https://github.com/django/django/archive/master.tar.gz
|
||||
djmaster: https://github.com/ottoyiu/django-cors-headers/archive/master.tar.gz
|
||||
djmaster: https://github.com/encode/django-rest-framework/archive/master.tar.gz
|
||||
djmaster: django-oauth-toolkit>=1.2.0
|
||||
|
||||
# other dependencies
|
||||
-r requirements/validation.txt
|
||||
@@ -77,5 +90,5 @@ known_standard_library =
|
||||
known_third_party =
|
||||
coreapi,coreschema,datadiff,dj_database_url,django,django_filters,djangorestframework_camel_case,
|
||||
rest_framework_recursive,flex,gunicorn,inflection,pytest,rest_framework,ruamel,setuptools_scm,
|
||||
swagger_spec_validator,uritemplate,user_agents,whitenoise,oauth2_provider
|
||||
swagger_spec_validator,uritemplate,user_agents,whitenoise,oauth2_provider,packaging
|
||||
known_first_party = drf_yasg,testproj,articles,people,snippets,todo,users,urlconfs
|
||||
|
||||
Reference in New Issue
Block a user