feat(emailing): add OpenAPI schemas for unsubscribe and tracking endpoints

Includes detailed OpenAPI schemas for unsubscribe (GET and POST) and tracking pixel (GET) endpoints, supporting email compatibility and event tracking. Added support for RFC 8058-compliant one-click unsubscribe functionality and transparent image-based email tracking.
This commit is contained in:
Egor Pavlovich Gorbunov 2026-02-05 19:30:53 +03:00
parent 45a1813465
commit 20473818a9
12 changed files with 4621 additions and 469 deletions

3915
engine/core/static/js/rapidoc-min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,63 @@
<!DOCTYPE html>
<html lang="en">
<head>
{% load tz static %}
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ title }} - API Documentation</title>
<link rel="icon" type="image/png" href="{% static 'favicon.ico' %}">
<script type="module" src="{% static 'js/rapidoc-min.js' %}"></script>
<style>
body {
margin: 0;
padding: 0;
}
rapi-doc {
--primary-color: #5c7182;
--bg-color: #fafafa;
--text-color: #22282d;
--nav-bg-color: #ffffff;
--nav-text-color: #22282d;
--nav-hover-bg-color: #e2e7ea;
--nav-accent-color: #5c7182;
}
@media (prefers-color-scheme: dark) {
rapi-doc {
--bg-color: #22282d;
--text-color: #e2e7ea;
--nav-bg-color: #353d44;
--nav-text-color: #e2e7ea;
--nav-hover-bg-color: #44515d;
}
}
</style>
</head>
<body>
<rapi-doc
spec-url="{{ schema_url }}"
heading-text="{{ title }}"
theme="dark"
render-style="read"
schema-style="table"
show-header="false"
show-info="true"
allow-authentication="true"
allow-server-selection="true"
allow-try="true"
allow-spec-url-load="false"
allow-spec-file-load="false"
show-method-in-nav-bar="as-colored-block"
nav-bg-color="#ffffff"
nav-text-color="#22282d"
nav-hover-bg-color="#e2e7ea"
nav-accent-color="#5c7182"
primary-color="#5c7182"
regular-font="system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif"
mono-font="ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace"
load-fonts="false"
sort-endpoints-by="path"
>
<img slot="logo" src="{% static 'favicon.png' %}" alt="{{ title }}" style="max-height: 40px; max-width: 150px;">
</rapi-doc>
</body>
</html>

View file

@ -30,13 +30,10 @@ from django.utils.translation import gettext_lazy as _
from django.views.decorators.cache import cache_page from django.views.decorators.cache import cache_page
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.vary import vary_on_headers from django.views.decorators.vary import vary_on_headers
from django.views.generic import TemplateView
from django_ratelimit.decorators import ratelimit from django_ratelimit.decorators import ratelimit
from drf_spectacular.utils import extend_schema_view from drf_spectacular.utils import extend_schema_view
from drf_spectacular.views import ( from drf_spectacular.views import SpectacularAPIView
SpectacularAPIView,
SpectacularRedocView,
SpectacularSwaggerView,
)
from graphene_file_upload.django import FileUploadGraphQLView from graphene_file_upload.django import FileUploadGraphQLView
from rest_framework import status from rest_framework import status
from rest_framework.permissions import AllowAny from rest_framework.permissions import AllowAny
@ -133,19 +130,13 @@ class CustomSpectacularAPIView(SpectacularAPIView):
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
class CustomSwaggerView(SpectacularSwaggerView): class RapiDocView(TemplateView):
def get_context_data(self, **kwargs): template_name = "rapidoc.html"
# noinspection PyUnresolvedReferences
context = super().get_context_data(**kwargs) # ty: ignore[unresolved-attribute]
context["script_url"] = self.request.build_absolute_uri()
return context
class CustomRedocView(SpectacularRedocView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
# noinspection PyUnresolvedReferences context = super().get_context_data(**kwargs)
context = super().get_context_data(**kwargs) # ty: ignore[unresolved-attribute] context["title"] = settings.SPECTACULAR_SETTINGS.get("TITLE", "API")
context["script_url"] = self.request.build_absolute_uri() context["schema_url"] = self.request.build_absolute_uri("/docs/schema/")
return context return context

View file

@ -0,0 +1,253 @@
from django.utils.translation import gettext_lazy as _
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import (
OpenApiExample,
OpenApiParameter,
OpenApiResponse,
extend_schema,
inline_serializer,
)
from rest_framework import serializers, status
from engine.core.docs.drf import error
_unsubscribe_token_param = OpenApiParameter(
name="token",
location=OpenApiParameter.QUERY,
description=_(
"UUID token for unsubscribing. This token is unique per user and is included "
"in the unsubscribe link of every campaign email. The token remains constant "
"for each user unless regenerated."
),
required=True,
type=str,
examples=[
OpenApiExample(
name="Valid token",
value="550e8400-e29b-41d4-a716-446655440000",
description="A valid UUID v4 unsubscribe token",
),
],
)
_unsubscribe_success_response = inline_serializer(
name="UnsubscribeSuccessResponse",
fields={
"detail": serializers.CharField(
default=_("You have been successfully unsubscribed from our emails.")
),
},
)
_unsubscribe_already_response = inline_serializer(
name="UnsubscribeAlreadyResponse",
fields={
"detail": serializers.CharField(default=_("You are already unsubscribed.")),
},
)
UNSUBSCRIBE_GET_SCHEMA = extend_schema(
tags=["emailing"],
operation_id="emailing_unsubscribe_get",
summary=_("Unsubscribe from email campaigns"),
description=_(
"Unsubscribe a user from all marketing email campaigns using their unique "
"unsubscribe token.\n\n"
"This endpoint is designed for email client compatibility where clicking a link "
"triggers a GET request. The user will no longer receive promotional emails "
"after successful unsubscription.\n\n"
"**Note:** Transactional emails (order confirmations, password resets, etc.) "
"are not affected by this setting."
),
parameters=[_unsubscribe_token_param],
responses={
status.HTTP_200_OK: OpenApiResponse(
response=_unsubscribe_success_response,
description=_("Successfully unsubscribed from email campaigns."),
examples=[
OpenApiExample(
name="Unsubscribed",
value={
"detail": "You have been successfully unsubscribed from our emails."
},
),
OpenApiExample(
name="Already unsubscribed",
value={"detail": "You are already unsubscribed."},
),
],
),
status.HTTP_400_BAD_REQUEST: OpenApiResponse(
response=error,
description=_("Invalid or missing unsubscribe token."),
examples=[
OpenApiExample(
name="Missing token",
value={"detail": "Unsubscribe token is required."},
),
OpenApiExample(
name="Invalid format",
value={"detail": "Invalid unsubscribe token format."},
),
],
),
status.HTTP_404_NOT_FOUND: OpenApiResponse(
response=error,
description=_("User associated with the token was not found."),
examples=[
OpenApiExample(
name="User not found",
value={"detail": "User not found."},
),
],
),
},
examples=[
OpenApiExample(
name="Unsubscribe request",
description="Example unsubscribe request with token",
value=None,
request_only=True,
),
],
)
UNSUBSCRIBE_POST_SCHEMA = extend_schema(
tags=["emailing"],
operation_id="emailing_unsubscribe_post",
summary=_("One-Click Unsubscribe (RFC 8058)"),
description=_(
"RFC 8058 compliant one-click unsubscribe endpoint for email campaigns.\n\n"
"This endpoint supports the List-Unsubscribe-Post header mechanism defined in "
"RFC 8058, which allows email clients to unsubscribe users with a single click "
"without leaving the email application.\n\n"
"The token can be provided either as a query parameter or in the request body.\n\n"
"**Standards Compliance:**\n"
"- RFC 8058: Signaling One-Click Functionality for List Email Headers\n"
"- RFC 2369: The Use of URLs as Meta-Syntax for Core Mail List Commands\n\n"
"**Note:** Transactional emails are not affected by this setting."
),
parameters=[_unsubscribe_token_param],
request=inline_serializer(
name="UnsubscribeRequest",
fields={
"token": serializers.UUIDField(
required=False,
help_text=_(
"Unsubscribe token (alternative to query parameter). "
"Can be omitted if token is provided in URL."
),
),
},
),
responses={
status.HTTP_200_OK: OpenApiResponse(
response=_unsubscribe_success_response,
description=_("Successfully unsubscribed from email campaigns."),
examples=[
OpenApiExample(
name="Unsubscribed",
value={
"detail": "You have been successfully unsubscribed from our emails."
},
),
OpenApiExample(
name="Already unsubscribed",
value={"detail": "You are already unsubscribed."},
),
],
),
status.HTTP_400_BAD_REQUEST: OpenApiResponse(
response=error,
description=_("Invalid or missing unsubscribe token."),
examples=[
OpenApiExample(
name="Missing token",
value={"detail": "Unsubscribe token is required."},
),
OpenApiExample(
name="Invalid format",
value={"detail": "Invalid unsubscribe token format."},
),
],
),
status.HTTP_404_NOT_FOUND: OpenApiResponse(
response=error,
description=_("User associated with the token was not found."),
examples=[
OpenApiExample(
name="User not found",
value={"detail": "User not found."},
),
],
),
},
external_docs={
"description": "RFC 8058 - Signaling One-Click Functionality",
"url": "https://datatracker.ietf.org/doc/html/rfc8058",
},
)
UNSUBSCRIBE_SCHEMA = {
"get": UNSUBSCRIBE_GET_SCHEMA,
"post": UNSUBSCRIBE_POST_SCHEMA,
}
TRACKING_SCHEMA = {
"get": extend_schema(
tags=["emailing"],
operation_id="emailing_tracking_pixel",
summary=_("Track email open event"),
description=_(
"Records when a campaign email is opened by the recipient.\n\n"
"This endpoint is called automatically when the tracking pixel (1x1 transparent GIF) "
"embedded in the email is loaded by the recipient's email client.\n\n"
"**How it works:**\n"
"1. Each campaign email contains a unique tracking pixel URL with a `tid` parameter\n"
"2. When the email is opened and images are loaded, this endpoint is called\n"
"3. The recipient's status is updated to 'opened' and the timestamp is recorded\n"
"4. The campaign's aggregate opened count is updated\n\n"
"**Privacy considerations:**\n"
"- Only the first open is recorded (subsequent opens are ignored)\n"
"- No personal information beyond the tracking ID is logged\n"
"- Users who disable image loading will not trigger this event\n\n"
"**Response:**\n"
"Returns a 1x1 transparent GIF image regardless of whether tracking succeeded, "
"to ensure consistent behavior and prevent information leakage."
),
parameters=[
OpenApiParameter(
name="tid",
location=OpenApiParameter.QUERY,
description=_(
"Tracking ID (UUID) unique to each campaign-recipient combination. "
"This ID links the open event to a specific recipient and campaign."
),
required=True,
type=str,
examples=[
OpenApiExample(
name="Valid tracking ID",
value="123e4567-e89b-12d3-a456-426614174000",
description="A valid UUID v4 tracking identifier",
),
],
),
],
responses={
status.HTTP_200_OK: OpenApiResponse(
response=OpenApiTypes.BINARY,
description=_(
"1x1 transparent GIF image. Always returned regardless of tracking status "
"to maintain consistent behavior."
),
),
status.HTTP_404_NOT_FOUND: OpenApiResponse(
description=_(
"Returned when no tracking ID is provided. Note: Invalid tracking IDs "
"still return 200 with the GIF to prevent enumeration attacks."
),
),
},
),
}

View file

@ -13,7 +13,7 @@ from engine.vibes_auth.serializers import (
TOKEN_OBTAIN_SCHEMA = { TOKEN_OBTAIN_SCHEMA = {
"post": extend_schema( "post": extend_schema(
tags=[ tags=[
"vibesAuth", "Auth",
], ],
summary=_("obtain a token pair"), summary=_("obtain a token pair"),
description=_("obtain a token pair (refresh and access) for authentication."), description=_("obtain a token pair (refresh and access) for authentication."),
@ -36,7 +36,7 @@ TOKEN_OBTAIN_SCHEMA = {
TOKEN_REFRESH_SCHEMA = { TOKEN_REFRESH_SCHEMA = {
"post": extend_schema( "post": extend_schema(
tags=[ tags=[
"vibesAuth", "Auth",
], ],
summary=_("refresh a token pair"), summary=_("refresh a token pair"),
description=_("refresh a token pair (refresh and access)."), description=_("refresh a token pair (refresh and access)."),
@ -59,7 +59,7 @@ TOKEN_REFRESH_SCHEMA = {
TOKEN_VERIFY_SCHEMA = { TOKEN_VERIFY_SCHEMA = {
"post": extend_schema( "post": extend_schema(
tags=[ tags=[
"vibesAuth", "Auth",
], ],
summary=_("verify a token"), summary=_("verify a token"),
description=_("Verify a token (refresh or access)."), description=_("Verify a token (refresh or access)."),

View file

@ -14,7 +14,7 @@ from engine.vibes_auth.serializers import (
USER_SCHEMA = { USER_SCHEMA = {
"create": extend_schema( "create": extend_schema(
tags=[ tags=[
"vibesAuth", "Auth",
], ],
summary=_("create a new user"), summary=_("create a new user"),
request=UserSerializer, request=UserSerializer,
@ -22,14 +22,14 @@ USER_SCHEMA = {
), ),
"retrieve": extend_schema( "retrieve": extend_schema(
tags=[ tags=[
"vibesAuth", "Auth",
], ],
summary=_("retrieve a user's details"), summary=_("retrieve a user's details"),
responses={status.HTTP_200_OK: UserSerializer, **BASE_ERRORS}, responses={status.HTTP_200_OK: UserSerializer, **BASE_ERRORS},
), ),
"update": extend_schema( "update": extend_schema(
tags=[ tags=[
"vibesAuth", "Auth",
], ],
summary=_("update a user's details"), summary=_("update a user's details"),
request=UserSerializer, request=UserSerializer,
@ -37,7 +37,7 @@ USER_SCHEMA = {
), ),
"partial_update": extend_schema( "partial_update": extend_schema(
tags=[ tags=[
"vibesAuth", "Auth",
], ],
summary=_("partially update a user's details"), summary=_("partially update a user's details"),
request=UserSerializer, request=UserSerializer,
@ -45,14 +45,14 @@ USER_SCHEMA = {
), ),
"destroy": extend_schema( "destroy": extend_schema(
tags=[ tags=[
"vibesAuth", "Auth",
], ],
summary=_("delete a user"), summary=_("delete a user"),
responses={status.HTTP_204_NO_CONTENT: {}, **BASE_ERRORS}, responses={status.HTTP_204_NO_CONTENT: {}, **BASE_ERRORS},
), ),
"reset_password": extend_schema( "reset_password": extend_schema(
tags=[ tags=[
"vibesAuth", "Auth",
], ],
summary=_("reset a user's password by sending a reset password email"), summary=_("reset a user's password by sending a reset password email"),
request=ResetPasswordSerializer, request=ResetPasswordSerializer,
@ -60,7 +60,7 @@ USER_SCHEMA = {
), ),
"upload_avatar": extend_schema( "upload_avatar": extend_schema(
tags=[ tags=[
"vibesAuth", "Auth",
], ],
summary=_("handle avatar upload for a user"), summary=_("handle avatar upload for a user"),
request={ request={
@ -78,7 +78,7 @@ USER_SCHEMA = {
), ),
"confirm_password_reset": extend_schema( "confirm_password_reset": extend_schema(
tags=[ tags=[
"vibesAuth", "Auth",
], ],
summary=_("confirm a user's password reset"), summary=_("confirm a user's password reset"),
request=ConfirmPasswordResetSerializer, request=ConfirmPasswordResetSerializer,
@ -90,7 +90,7 @@ USER_SCHEMA = {
), ),
"activate": extend_schema( "activate": extend_schema(
tags=[ tags=[
"vibesAuth", "Auth",
], ],
summary=_("activate a user's account"), summary=_("activate a user's account"),
request=ActivateEmailSerializer, request=ActivateEmailSerializer,

View file

@ -1,15 +1,17 @@
from uuid import UUID from uuid import UUID
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from drf_spectacular.utils import OpenApiParameter, extend_schema from drf_spectacular.utils import extend_schema_view
from rest_framework import status from rest_framework import status
from rest_framework.permissions import AllowAny from rest_framework.permissions import AllowAny
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from engine.vibes_auth.docs.drf.emailing import TRACKING_SCHEMA, UNSUBSCRIBE_SCHEMA
from engine.vibes_auth.models import User from engine.vibes_auth.models import User
@extend_schema_view(**UNSUBSCRIBE_SCHEMA)
class UnsubscribeView(APIView): class UnsubscribeView(APIView):
""" """
Public endpoint for one-click unsubscribe from email campaigns. Public endpoint for one-click unsubscribe from email campaigns.
@ -20,44 +22,10 @@ class UnsubscribeView(APIView):
permission_classes = [AllowAny] permission_classes = [AllowAny]
authentication_classes = [] authentication_classes = []
@extend_schema(
summary="Unsubscribe from email campaigns",
description="Unsubscribe a user from email campaigns using their unsubscribe token.",
parameters=[
OpenApiParameter(
name="token",
description="Unsubscribe token from the email",
required=True,
type=str,
),
],
responses={
200: {"description": "Successfully unsubscribed"},
400: {"description": "Invalid or missing token"},
404: {"description": "User not found"},
},
)
def get(self, request): def get(self, request):
"""Handle GET request for unsubscribe (email link click).""" """Handle GET request for unsubscribe (email link click)."""
return self._process_unsubscribe(request) return self._process_unsubscribe(request)
@extend_schema(
summary="Unsubscribe from email campaigns (One-Click)",
description="RFC 8058 compliant one-click unsubscribe endpoint.",
parameters=[
OpenApiParameter(
name="token",
description="Unsubscribe token from the email",
required=True,
type=str,
),
],
responses={
200: {"description": "Successfully unsubscribed"},
400: {"description": "Invalid or missing token"},
404: {"description": "User not found"},
},
)
def post(self, request): def post(self, request):
"""Handle POST request for one-click unsubscribe (RFC 8058).""" """Handle POST request for one-click unsubscribe (RFC 8058)."""
return self._process_unsubscribe(request) return self._process_unsubscribe(request)
@ -103,6 +71,7 @@ class UnsubscribeView(APIView):
) )
@extend_schema_view(**TRACKING_SCHEMA)
class TrackingView(APIView): class TrackingView(APIView):
""" """
Endpoint for tracking email opens and clicks. Endpoint for tracking email opens and clicks.
@ -113,22 +82,6 @@ class TrackingView(APIView):
permission_classes = [AllowAny] permission_classes = [AllowAny]
authentication_classes = [] authentication_classes = []
@extend_schema(
summary="Track email open",
description="Track when a campaign email is opened.",
parameters=[
OpenApiParameter(
name="tid",
description="Tracking ID from the email",
required=True,
type=str,
),
],
responses={
200: {"description": "Tracking recorded"},
404: {"description": "Invalid tracking ID"},
},
)
def get(self, request): def get(self, request):
"""Track email open via tracking pixel.""" """Track email open via tracking pixel."""
from django.utils import timezone from django.utils import timezone

View file

@ -11,10 +11,10 @@ dependencies = [
"channels==4.3.2", "channels==4.3.2",
"channels-redis==4.3.0", "channels-redis==4.3.0",
"colorlog==6.10.1", "colorlog==6.10.1",
"coverage==7.13.2", "coverage==7.13.3",
"click==8.3.1", "click==8.3.1",
"cryptography==46.0.3", "cryptography==46.0.4",
"django==5.2.9", "django==5.2.11",
"django-cacheops==7.2", "django-cacheops==7.2",
"django-constance==4.3.4", "django-constance==4.3.4",
"django-cors-headers==4.9.0", "django-cors-headers==4.9.0",
@ -22,7 +22,7 @@ dependencies = [
"django-elasticsearch-dsl==8.2", "django-elasticsearch-dsl==8.2",
"django-extensions==4.1", "django-extensions==4.1",
"django-filter==25.2", "django-filter==25.2",
"django-health-check==3.20.8", "django-health-check==3.23.3",
"django-import-export[all]==4.4.0", "django-import-export[all]==4.4.0",
"django-json-widget==2.1.1", "django-json-widget==2.1.1",
"django-model-utils==5.0.0", "django-model-utils==5.0.0",
@ -33,16 +33,16 @@ dependencies = [
"django-redis==6.0.0", "django-redis==6.0.0",
"django-ratelimit==4.1.0", "django-ratelimit==4.1.0",
"django-storages==1.14.6", "django-storages==1.14.6",
"django-unfold==0.76.0", "django-unfold==0.78.1",
"django-widget-tweaks==1.5.1", "django-widget-tweaks==1.5.1",
"djangorestframework==3.16.1", "djangorestframework==3.16.1",
"djangorestframework-recursive==0.1.2", "djangorestframework-recursive==0.1.2",
"djangorestframework-simplejwt[crypto]==5.5.1", "djangorestframework-simplejwt[crypto]==5.5.1",
"djangorestframework-xml==2.0.0", "djangorestframework-xml==2.0.0",
"djangorestframework-yaml==2.0.0", "djangorestframework-yaml==2.0.0",
"djangoql==0.18.1", "djangoql==0.19.1",
"docutils==0.22.4", "docutils==0.22.4",
"drf-spectacular[sidecar]==0.29.0", "drf-spectacular==0.29.0",
"drf-spectacular-websocket==1.3.1", "drf-spectacular-websocket==1.3.1",
"drf-orjson-renderer==1.8.0", "drf-orjson-renderer==1.8.0",
"elasticsearch-dsl==8.18.0", "elasticsearch-dsl==8.18.0",
@ -53,17 +53,17 @@ dependencies = [
"httpx==0.28.1", "httpx==0.28.1",
"paramiko==4.0.0", "paramiko==4.0.0",
"pillow==12.1.0", "pillow==12.1.0",
"pip==25.3", "pip==26.0.1",
"polib==1.2.0", "polib==1.2.0",
"PyJWT==2.10.1", "PyJWT==2.11.0",
"pytest==9.0.2", "pytest==9.0.2",
"pytest-django==4.11.1", "pytest-django==4.11.1",
"python-slugify==8.0.4", "python-slugify==8.0.4",
"psutil==7.2.1", "psutil==7.2.2",
"psycopg[binary]==3.2.9", "psycopg[binary]==3.3.2",
"redis==7.1.0", "redis==7.1.0",
"requests==2.32.5", "requests==2.32.5",
"sentry-sdk[django,celery,opentelemetry]==2.50.0", "sentry-sdk[django,celery,opentelemetry]==2.52.0",
"six==1.17.0", "six==1.17.0",
"swapper==1.4.0", "swapper==1.4.0",
"uvicorn==0.40.0", "uvicorn==0.40.0",
@ -79,20 +79,20 @@ worker = [
"django-celery-results==2.6.0", "django-celery-results==2.6.0",
] ]
linting = [ linting = [
"ty==0.0.13", "ty==0.0.15",
"ruff==0.14.14", "ruff==0.15.0",
"celery-types==0.24.0", "celery-types==0.24.0",
"django-stubs==5.2.9", "django-stubs==5.2.9",
"djangorestframework-stubs==3.16.7", "djangorestframework-stubs==3.16.8",
"types-requests==2.32.4.20260107", "types-requests==2.32.4.20260107",
"types-redis==4.6.0.20241004", "types-redis==4.6.0.20241004",
"types-paramiko==4.0.0.20250822", "types-paramiko==4.0.0.20250822",
"types-psutil==7.2.1.20260116", "types-psutil==7.2.2.20260130",
"types-pillow==10.2.0.20240822", "types-pillow==10.2.0.20240822",
"types-docutils==0.22.3.20251115", "types-docutils==0.22.3.20251115",
"types-six==1.17.0.20251009", "types-six==1.17.0.20251009",
] ]
openai = ["openai==2.15.0"] openai = ["openai==2.16.0"]
jupyter = ["jupyter==1.1.1"] jupyter = ["jupyter==1.1.1"]
[tool.uv] [tool.uv]

View file

@ -117,21 +117,13 @@ SPECTACULAR_SETTINGS = {
"TITLE": f"{PROJECT_NAME} API", "TITLE": f"{PROJECT_NAME} API",
"DESCRIPTION": SPECTACULAR_DESCRIPTION, "DESCRIPTION": SPECTACULAR_DESCRIPTION,
"VERSION": SCHON_VERSION, # noqa: F405 "VERSION": SCHON_VERSION, # noqa: F405
"TOS": "https://schon.wiseless.xyz/terms-of-service", "TOS": "https://schon.fureunoir.com/terms-of-service",
"SWAGGER_UI_DIST": "SIDECAR",
"CAMELIZE_NAMES": True, "CAMELIZE_NAMES": True,
"POSTPROCESSING_HOOKS": [ "POSTPROCESSING_HOOKS": [
"schon.utils.renderers.camelize_serializer_fields", "schon.utils.renderers.camelize_serializer_fields",
"drf_spectacular.hooks.postprocess_schema_enums", "drf_spectacular.hooks.postprocess_schema_enums",
], ],
"REDOC_DIST": "SIDECAR",
"ENABLE_DJANGO_DEPLOY_CHECK": not DEBUG, # noqa: F405 "ENABLE_DJANGO_DEPLOY_CHECK": not DEBUG, # noqa: F405
"SWAGGER_UI_FAVICON_HREF": r"/static/favicon.png",
"SWAGGER_UI_SETTINGS": {
"connectSocket": False,
"socketMaxMessages": 8,
"socketMessagesInitialOpened": False,
},
"SERVERS": [ "SERVERS": [
{ {
"url": f"https://api.{BASE_DOMAIN}/", "url": f"https://api.{BASE_DOMAIN}/",

View file

@ -116,14 +116,9 @@ UNFOLD: dict[str, Any] = {
"link": reverse_lazy("core:sitemap-index"), "link": reverse_lazy("core:sitemap-index"),
}, },
{ {
"title": "Swagger", "title": "API Docs",
"icon": "integration_instructions", "icon": "api",
"link": reverse_lazy("swagger-ui-platform"), "link": reverse_lazy("rapidoc-platform"),
},
{
"title": "Redoc",
"icon": "integration_instructions",
"link": reverse_lazy("redoc-ui-platform"),
}, },
{ {
"title": "GraphQL", "title": "GraphQL",

View file

@ -7,9 +7,8 @@ from django.views.decorators.csrf import csrf_exempt
from engine.core.graphene.schema import schema from engine.core.graphene.schema import schema
from engine.core.views import ( from engine.core.views import (
CustomGraphQLView, CustomGraphQLView,
CustomRedocView,
CustomSpectacularAPIView, CustomSpectacularAPIView,
CustomSwaggerView, RapiDocView,
favicon_view, favicon_view,
index, index,
) )
@ -55,19 +54,14 @@ urlpatterns = [
### DOCUMENTATION URLS ### ### DOCUMENTATION URLS ###
path( path(
r"docs/", r"docs/",
RapiDocView.as_view(),
name="rapidoc-platform",
),
path(
r"docs/schema/",
CustomSpectacularAPIView.as_view(urlconf="schon.urls"), CustomSpectacularAPIView.as_view(urlconf="schon.urls"),
name="schema-platform", name="schema-platform",
), ),
path(
r"docs/swagger/",
CustomSwaggerView.as_view(url_name="schema-platform"),
name="swagger-ui-platform",
),
path(
r"docs/redoc/",
CustomRedocView.as_view(url_name="schema-platform"),
name="redoc-ui-platform",
),
### ENGINE APPS URLS ### ### ENGINE APPS URLS ###
path( path(
r"b2b/", r"b2b/",

684
uv.lock

File diff suppressed because it is too large Load diff