Swagger UI and Redoc lay every description out left-to-right, because they were
built for English. Our text is Persian, and in an LTR block the sentence-final
full stop jumps to the start of the line, table columns come out reversed, and
every English word inside a Persian sentence lands in the wrong place. The
screenshot of /api/docs/ showed all three.
This is not fixable by rewriting the text, because the base direction of the
block is what is wrong. Both views now render through a thin template of ours
that extends the drf-spectacular one via {{ block.super }} and adds scoped CSS:
right-to-left for the blocks that carry rendered Markdown, and an explicit
left-to-right override for code and pre, so that "Authorization: Bearer <token>"
and every JSON sample stay readable. Table cells align right, list bullets move
to the right margin, and the API title keeps its LTR treatment since it is
English.
Writing the description in English instead would have fixed only the header. The
per-endpoint descriptions are Persian across the whole codebase and were broken
the same way, so the shell fixes the entire page rather than one block of it,
and keeps the repo's convention of Persian documentation.
The upstream templates are untouched, so a drf-spectacular upgrade cannot be
broken by this.
Two tests cover it, because this failure is invisible to every other kind of
check: nothing errors, nothing 500s, the page simply reads wrong to a human.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
94 lines
4.9 KiB
Python
94 lines
4.9 KiB
Python
"""
|
||
مسیرهای اصلی پروژهی Tadrisino — فاز ۱ (۴ از ۱۱ اپ)
|
||
|
||
هر اپ زیر پیشوند /api/ خودش mount شده. مسیرهای هر endpoint دقیقاً طبق
|
||
سند «Endpointهای API» طراحیشده تا مقایسه با Swagger راحت باشه.
|
||
"""
|
||
import logging
|
||
|
||
from django.contrib import admin
|
||
from django.http import JsonResponse
|
||
from django.urls import include, path
|
||
from django.views import defaults as django_default_views
|
||
from drf_spectacular.utils import extend_schema
|
||
|
||
from config.health import HealthView
|
||
from drf_spectacular.views import (
|
||
SpectacularAPIView,
|
||
SpectacularRedocView,
|
||
SpectacularSwaggerView,
|
||
)
|
||
|
||
|
||
@extend_schema(exclude=True)
|
||
class HealthViewSlashAlias(HealthView):
|
||
"""نسخهی اسلشدارِ `/api/health`، فقط برای پایشِ بیرونی — بیرون از اسکیما."""
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
def handler404(request, exception=None):
|
||
"""برای مسیرهای /api/ همیشه JSON برمیگردونه؛ بقیه رفتارِ پیشفرضِ جنگو."""
|
||
if request.path.startswith('/api/'):
|
||
return JsonResponse({'detail': 'یافت نشد.'}, status=404)
|
||
return django_default_views.page_not_found(request, exception)
|
||
|
||
|
||
def handler500(request):
|
||
"""شبکهی ایمنیِ نهایی برای exceptionی که حتی به config/exceptions.py هم
|
||
نرسیده (مثلاً در خودِ middleware/URL resolution). traceback کامل رو
|
||
config/middleware.py قبلاً لاگ کرده؛ اینجا فقط یه پاسخِ JSON میده."""
|
||
logger.error('unhandled_500 path=%s', request.path)
|
||
if request.path.startswith('/api/'):
|
||
return JsonResponse(
|
||
{'detail': 'خطای غیرمنتظرهای رخ داد؛ لطفاً بعداً دوباره تلاش کنید.'}, status=500,
|
||
)
|
||
return django_default_views.server_error(request)
|
||
|
||
|
||
urlpatterns = [
|
||
path('admin/', admin.site.urls),
|
||
|
||
# بررسیِ سلامتِ نسخهی مستقرشده — بدونِ احراز، برای پایشِ بیرونی.
|
||
#
|
||
# هر دو شکل ثبت میشود، با و بدونِ اسلشِ آخر. دلیلش تجربهی واقعی است:
|
||
# پایشِ سرور `/api/health/` صدا میزد و چون فقط شکلِ بیاسلش ثبت بود،
|
||
# کسی روی سرور همین فایل را دستی ویرایش کرد تا کار کند — و از آن لحظه
|
||
# هر `git pull` بعدی روی همین فایل گیر میکرد. یک مسیرِ اضافه ارزانش
|
||
# است در برابرِ یک فایلِ دستکاریشده روی پروداکشن.
|
||
path('api/health', HealthView.as_view(), name='health'),
|
||
# همان ویو، فقط برای پایشی که با اسلش صدا میزند. از اسکیما بیرون است تا
|
||
# دو مسیرِ یکسان operationIdهای health_retrieve و health_retrieve_2 نسازند.
|
||
#
|
||
# ⚠️ زیرکلاس لازم است، نه exclude روی خودِ HealthView. extend_schema روی
|
||
# یک کلاس همان کلاس را تغییر میدهد، و چون هر دو مسیر به یک کلاس اشاره
|
||
# میکردند، `/api/health` هم با خودش از اسکیما بیرون میافتاد — یعنی
|
||
# اندپوینتِ سلامت اصلاً در Swagger دیده نمیشد.
|
||
path('api/health/', HealthViewSlashAlias.as_view(), name='health-slash'),
|
||
|
||
# OpenAPI schema + Swagger UI + Redoc
|
||
path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
|
||
# قالبِ خودمان فقط یک پوستهی راستبهچپ روی قالبِ drf-spectacular است؛
|
||
# بدونش متنِ فارسی در یک بلوکِ LTR رندر میشود و نقطهی جمله و ترتیبِ
|
||
# ستونهای جدول جابهجا میشوند. توضیح در خودِ قالب.
|
||
path('api/docs/', SpectacularSwaggerView.as_view(
|
||
url_name='schema', template_name='tadrisino/swagger_ui.html'), name='swagger-ui'),
|
||
path('api/redoc/', SpectacularRedocView.as_view(
|
||
url_name='schema', template_name='tadrisino/redoc.html'), name='redoc'),
|
||
|
||
# همه زیرِ /api/ جمع میشوند؛ هر اپ مسیرهای خودش را نگه میدارد، پس
|
||
# ترتیبِ این فهرست اهمیتی ندارد و تداخلی بینشان نیست.
|
||
path('api/', include('accounts.urls')),
|
||
path('api/', include('content.urls')),
|
||
path('api/', include('progress.urls')),
|
||
path('api/', include('ai_tutor.urls')),
|
||
path('api/', include('rag.urls')),
|
||
path('api/', include('subscription.urls')),
|
||
path('api/', include('payment.urls')),
|
||
path('api/', include('discount.urls')),
|
||
path('api/', include('notifications.urls')),
|
||
path('api/', include('affiliate.urls')),
|
||
path('api/', include('support.urls')),
|
||
path('api/', include('family.urls')),
|
||
]
|