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:
parent
45a1813465
commit
20473818a9
12 changed files with 4621 additions and 469 deletions
3915
engine/core/static/js/rapidoc-min.js
vendored
Normal file
3915
engine/core/static/js/rapidoc-min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
63
engine/core/templates/rapidoc.html
Normal file
63
engine/core/templates/rapidoc.html
Normal 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>
|
||||
|
|
@ -30,13 +30,10 @@ from django.utils.translation import gettext_lazy as _
|
|||
from django.views.decorators.cache import cache_page
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.decorators.vary import vary_on_headers
|
||||
from django.views.generic import TemplateView
|
||||
from django_ratelimit.decorators import ratelimit
|
||||
from drf_spectacular.utils import extend_schema_view
|
||||
from drf_spectacular.views import (
|
||||
SpectacularAPIView,
|
||||
SpectacularRedocView,
|
||||
SpectacularSwaggerView,
|
||||
)
|
||||
from drf_spectacular.views import SpectacularAPIView
|
||||
from graphene_file_upload.django import FileUploadGraphQLView
|
||||
from rest_framework import status
|
||||
from rest_framework.permissions import AllowAny
|
||||
|
|
@ -133,19 +130,13 @@ class CustomSpectacularAPIView(SpectacularAPIView):
|
|||
return super().get(request, *args, **kwargs)
|
||||
|
||||
|
||||
class CustomSwaggerView(SpectacularSwaggerView):
|
||||
def get_context_data(self, **kwargs):
|
||||
# noinspection PyUnresolvedReferences
|
||||
context = super().get_context_data(**kwargs) # ty: ignore[unresolved-attribute]
|
||||
context["script_url"] = self.request.build_absolute_uri()
|
||||
return context
|
||||
class RapiDocView(TemplateView):
|
||||
template_name = "rapidoc.html"
|
||||
|
||||
|
||||
class CustomRedocView(SpectacularRedocView):
|
||||
def get_context_data(self, **kwargs):
|
||||
# noinspection PyUnresolvedReferences
|
||||
context = super().get_context_data(**kwargs) # ty: ignore[unresolved-attribute]
|
||||
context["script_url"] = self.request.build_absolute_uri()
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["title"] = settings.SPECTACULAR_SETTINGS.get("TITLE", "API")
|
||||
context["schema_url"] = self.request.build_absolute_uri("/docs/schema/")
|
||||
return context
|
||||
|
||||
|
||||
|
|
|
|||
253
engine/vibes_auth/docs/drf/emailing.py
Normal file
253
engine/vibes_auth/docs/drf/emailing.py
Normal 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."
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@ from engine.vibes_auth.serializers import (
|
|||
TOKEN_OBTAIN_SCHEMA = {
|
||||
"post": extend_schema(
|
||||
tags=[
|
||||
"vibesAuth",
|
||||
"Auth",
|
||||
],
|
||||
summary=_("obtain a token pair"),
|
||||
description=_("obtain a token pair (refresh and access) for authentication."),
|
||||
|
|
@ -36,7 +36,7 @@ TOKEN_OBTAIN_SCHEMA = {
|
|||
TOKEN_REFRESH_SCHEMA = {
|
||||
"post": extend_schema(
|
||||
tags=[
|
||||
"vibesAuth",
|
||||
"Auth",
|
||||
],
|
||||
summary=_("refresh a token pair"),
|
||||
description=_("refresh a token pair (refresh and access)."),
|
||||
|
|
@ -59,7 +59,7 @@ TOKEN_REFRESH_SCHEMA = {
|
|||
TOKEN_VERIFY_SCHEMA = {
|
||||
"post": extend_schema(
|
||||
tags=[
|
||||
"vibesAuth",
|
||||
"Auth",
|
||||
],
|
||||
summary=_("verify a token"),
|
||||
description=_("Verify a token (refresh or access)."),
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ from engine.vibes_auth.serializers import (
|
|||
USER_SCHEMA = {
|
||||
"create": extend_schema(
|
||||
tags=[
|
||||
"vibesAuth",
|
||||
"Auth",
|
||||
],
|
||||
summary=_("create a new user"),
|
||||
request=UserSerializer,
|
||||
|
|
@ -22,14 +22,14 @@ USER_SCHEMA = {
|
|||
),
|
||||
"retrieve": extend_schema(
|
||||
tags=[
|
||||
"vibesAuth",
|
||||
"Auth",
|
||||
],
|
||||
summary=_("retrieve a user's details"),
|
||||
responses={status.HTTP_200_OK: UserSerializer, **BASE_ERRORS},
|
||||
),
|
||||
"update": extend_schema(
|
||||
tags=[
|
||||
"vibesAuth",
|
||||
"Auth",
|
||||
],
|
||||
summary=_("update a user's details"),
|
||||
request=UserSerializer,
|
||||
|
|
@ -37,7 +37,7 @@ USER_SCHEMA = {
|
|||
),
|
||||
"partial_update": extend_schema(
|
||||
tags=[
|
||||
"vibesAuth",
|
||||
"Auth",
|
||||
],
|
||||
summary=_("partially update a user's details"),
|
||||
request=UserSerializer,
|
||||
|
|
@ -45,14 +45,14 @@ USER_SCHEMA = {
|
|||
),
|
||||
"destroy": extend_schema(
|
||||
tags=[
|
||||
"vibesAuth",
|
||||
"Auth",
|
||||
],
|
||||
summary=_("delete a user"),
|
||||
responses={status.HTTP_204_NO_CONTENT: {}, **BASE_ERRORS},
|
||||
),
|
||||
"reset_password": extend_schema(
|
||||
tags=[
|
||||
"vibesAuth",
|
||||
"Auth",
|
||||
],
|
||||
summary=_("reset a user's password by sending a reset password email"),
|
||||
request=ResetPasswordSerializer,
|
||||
|
|
@ -60,7 +60,7 @@ USER_SCHEMA = {
|
|||
),
|
||||
"upload_avatar": extend_schema(
|
||||
tags=[
|
||||
"vibesAuth",
|
||||
"Auth",
|
||||
],
|
||||
summary=_("handle avatar upload for a user"),
|
||||
request={
|
||||
|
|
@ -78,7 +78,7 @@ USER_SCHEMA = {
|
|||
),
|
||||
"confirm_password_reset": extend_schema(
|
||||
tags=[
|
||||
"vibesAuth",
|
||||
"Auth",
|
||||
],
|
||||
summary=_("confirm a user's password reset"),
|
||||
request=ConfirmPasswordResetSerializer,
|
||||
|
|
@ -90,7 +90,7 @@ USER_SCHEMA = {
|
|||
),
|
||||
"activate": extend_schema(
|
||||
tags=[
|
||||
"vibesAuth",
|
||||
"Auth",
|
||||
],
|
||||
summary=_("activate a user's account"),
|
||||
request=ActivateEmailSerializer,
|
||||
|
|
|
|||
|
|
@ -1,15 +1,17 @@
|
|||
from uuid import UUID
|
||||
|
||||
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.permissions import AllowAny
|
||||
from rest_framework.response import Response
|
||||
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
|
||||
|
||||
|
||||
@extend_schema_view(**UNSUBSCRIBE_SCHEMA)
|
||||
class UnsubscribeView(APIView):
|
||||
"""
|
||||
Public endpoint for one-click unsubscribe from email campaigns.
|
||||
|
|
@ -20,44 +22,10 @@ class UnsubscribeView(APIView):
|
|||
permission_classes = [AllowAny]
|
||||
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):
|
||||
"""Handle GET request for unsubscribe (email link click)."""
|
||||
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):
|
||||
"""Handle POST request for one-click unsubscribe (RFC 8058)."""
|
||||
return self._process_unsubscribe(request)
|
||||
|
|
@ -103,6 +71,7 @@ class UnsubscribeView(APIView):
|
|||
)
|
||||
|
||||
|
||||
@extend_schema_view(**TRACKING_SCHEMA)
|
||||
class TrackingView(APIView):
|
||||
"""
|
||||
Endpoint for tracking email opens and clicks.
|
||||
|
|
@ -113,22 +82,6 @@ class TrackingView(APIView):
|
|||
permission_classes = [AllowAny]
|
||||
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):
|
||||
"""Track email open via tracking pixel."""
|
||||
from django.utils import timezone
|
||||
|
|
|
|||
|
|
@ -11,10 +11,10 @@ dependencies = [
|
|||
"channels==4.3.2",
|
||||
"channels-redis==4.3.0",
|
||||
"colorlog==6.10.1",
|
||||
"coverage==7.13.2",
|
||||
"coverage==7.13.3",
|
||||
"click==8.3.1",
|
||||
"cryptography==46.0.3",
|
||||
"django==5.2.9",
|
||||
"cryptography==46.0.4",
|
||||
"django==5.2.11",
|
||||
"django-cacheops==7.2",
|
||||
"django-constance==4.3.4",
|
||||
"django-cors-headers==4.9.0",
|
||||
|
|
@ -22,7 +22,7 @@ dependencies = [
|
|||
"django-elasticsearch-dsl==8.2",
|
||||
"django-extensions==4.1",
|
||||
"django-filter==25.2",
|
||||
"django-health-check==3.20.8",
|
||||
"django-health-check==3.23.3",
|
||||
"django-import-export[all]==4.4.0",
|
||||
"django-json-widget==2.1.1",
|
||||
"django-model-utils==5.0.0",
|
||||
|
|
@ -33,16 +33,16 @@ dependencies = [
|
|||
"django-redis==6.0.0",
|
||||
"django-ratelimit==4.1.0",
|
||||
"django-storages==1.14.6",
|
||||
"django-unfold==0.76.0",
|
||||
"django-unfold==0.78.1",
|
||||
"django-widget-tweaks==1.5.1",
|
||||
"djangorestframework==3.16.1",
|
||||
"djangorestframework-recursive==0.1.2",
|
||||
"djangorestframework-simplejwt[crypto]==5.5.1",
|
||||
"djangorestframework-xml==2.0.0",
|
||||
"djangorestframework-yaml==2.0.0",
|
||||
"djangoql==0.18.1",
|
||||
"djangoql==0.19.1",
|
||||
"docutils==0.22.4",
|
||||
"drf-spectacular[sidecar]==0.29.0",
|
||||
"drf-spectacular==0.29.0",
|
||||
"drf-spectacular-websocket==1.3.1",
|
||||
"drf-orjson-renderer==1.8.0",
|
||||
"elasticsearch-dsl==8.18.0",
|
||||
|
|
@ -53,17 +53,17 @@ dependencies = [
|
|||
"httpx==0.28.1",
|
||||
"paramiko==4.0.0",
|
||||
"pillow==12.1.0",
|
||||
"pip==25.3",
|
||||
"pip==26.0.1",
|
||||
"polib==1.2.0",
|
||||
"PyJWT==2.10.1",
|
||||
"PyJWT==2.11.0",
|
||||
"pytest==9.0.2",
|
||||
"pytest-django==4.11.1",
|
||||
"python-slugify==8.0.4",
|
||||
"psutil==7.2.1",
|
||||
"psycopg[binary]==3.2.9",
|
||||
"psutil==7.2.2",
|
||||
"psycopg[binary]==3.3.2",
|
||||
"redis==7.1.0",
|
||||
"requests==2.32.5",
|
||||
"sentry-sdk[django,celery,opentelemetry]==2.50.0",
|
||||
"sentry-sdk[django,celery,opentelemetry]==2.52.0",
|
||||
"six==1.17.0",
|
||||
"swapper==1.4.0",
|
||||
"uvicorn==0.40.0",
|
||||
|
|
@ -79,20 +79,20 @@ worker = [
|
|||
"django-celery-results==2.6.0",
|
||||
]
|
||||
linting = [
|
||||
"ty==0.0.13",
|
||||
"ruff==0.14.14",
|
||||
"ty==0.0.15",
|
||||
"ruff==0.15.0",
|
||||
"celery-types==0.24.0",
|
||||
"django-stubs==5.2.9",
|
||||
"djangorestframework-stubs==3.16.7",
|
||||
"djangorestframework-stubs==3.16.8",
|
||||
"types-requests==2.32.4.20260107",
|
||||
"types-redis==4.6.0.20241004",
|
||||
"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-docutils==0.22.3.20251115",
|
||||
"types-six==1.17.0.20251009",
|
||||
]
|
||||
openai = ["openai==2.15.0"]
|
||||
openai = ["openai==2.16.0"]
|
||||
jupyter = ["jupyter==1.1.1"]
|
||||
|
||||
[tool.uv]
|
||||
|
|
|
|||
|
|
@ -117,21 +117,13 @@ SPECTACULAR_SETTINGS = {
|
|||
"TITLE": f"{PROJECT_NAME} API",
|
||||
"DESCRIPTION": SPECTACULAR_DESCRIPTION,
|
||||
"VERSION": SCHON_VERSION, # noqa: F405
|
||||
"TOS": "https://schon.wiseless.xyz/terms-of-service",
|
||||
"SWAGGER_UI_DIST": "SIDECAR",
|
||||
"TOS": "https://schon.fureunoir.com/terms-of-service",
|
||||
"CAMELIZE_NAMES": True,
|
||||
"POSTPROCESSING_HOOKS": [
|
||||
"schon.utils.renderers.camelize_serializer_fields",
|
||||
"drf_spectacular.hooks.postprocess_schema_enums",
|
||||
],
|
||||
"REDOC_DIST": "SIDECAR",
|
||||
"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": [
|
||||
{
|
||||
"url": f"https://api.{BASE_DOMAIN}/",
|
||||
|
|
|
|||
|
|
@ -116,14 +116,9 @@ UNFOLD: dict[str, Any] = {
|
|||
"link": reverse_lazy("core:sitemap-index"),
|
||||
},
|
||||
{
|
||||
"title": "Swagger",
|
||||
"icon": "integration_instructions",
|
||||
"link": reverse_lazy("swagger-ui-platform"),
|
||||
},
|
||||
{
|
||||
"title": "Redoc",
|
||||
"icon": "integration_instructions",
|
||||
"link": reverse_lazy("redoc-ui-platform"),
|
||||
"title": "API Docs",
|
||||
"icon": "api",
|
||||
"link": reverse_lazy("rapidoc-platform"),
|
||||
},
|
||||
{
|
||||
"title": "GraphQL",
|
||||
|
|
|
|||
|
|
@ -7,9 +7,8 @@ from django.views.decorators.csrf import csrf_exempt
|
|||
from engine.core.graphene.schema import schema
|
||||
from engine.core.views import (
|
||||
CustomGraphQLView,
|
||||
CustomRedocView,
|
||||
CustomSpectacularAPIView,
|
||||
CustomSwaggerView,
|
||||
RapiDocView,
|
||||
favicon_view,
|
||||
index,
|
||||
)
|
||||
|
|
@ -55,19 +54,14 @@ urlpatterns = [
|
|||
### DOCUMENTATION URLS ###
|
||||
path(
|
||||
r"docs/",
|
||||
RapiDocView.as_view(),
|
||||
name="rapidoc-platform",
|
||||
),
|
||||
path(
|
||||
r"docs/schema/",
|
||||
CustomSpectacularAPIView.as_view(urlconf="schon.urls"),
|
||||
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 ###
|
||||
path(
|
||||
r"b2b/",
|
||||
|
|
|
|||
Loading…
Reference in a new issue