Browse Source

Add API documentation templates, authentication UI, and pagination utility

- Created a new HTML template for API documentation with a responsive design and interactive elements.
- Added an authentication page for Swagger UI, allowing users to enter their API token.
- Implemented a custom Swagger UI template with a fixed authentication banner and user information display.
- Introduced a pagination utility class to return all items without pagination, enhancing data retrieval flexibility.
- Added seed images and test files for future development and testing purposes.
master
mortezaei 11 months ago
parent
commit
7452d3ba3d
  1. 34
      apps/api/views.py
  2. 15
      apps/api/views/__init__.py
  3. 31
      apps/api/views/api_views.py
  4. 462
      apps/api/views/documentation.py
  5. 74
      apps/api/views/swagger_views.py
  6. 235
      apps/hadis/admin/category.py
  7. 370
      apps/hadis/admin/hadis.py
  8. 66
      apps/hadis/admin/transmitter.py
  9. 452
      apps/hadis/doc.py
  10. 341
      apps/hadis/docs.py
  11. 217
      apps/hadis/migrations/0002_hadissect_hadisstatus_alter_hadis_options_and_more.py
  12. 106
      apps/hadis/models/category.py
  13. 97
      apps/hadis/models/hadis.py
  14. 52
      apps/hadis/models/transmitter.py
  15. 127
      apps/hadis/serializers.py
  16. 2
      apps/hadis/serializers/__init__.py
  17. 102
      apps/hadis/serializers/category.py
  18. 144
      apps/hadis/serializers/hadis.py
  19. 2343
      apps/hadis/templates/admin/category_index.html
  20. 42
      apps/hadis/templates/admin/hadiscategory/change_form.html
  21. 153
      apps/hadis/templates/admin/hadisowerview_change_form.html
  22. 7
      apps/hadis/templates/admin/widgets/color_radio.html
  23. 9
      apps/hadis/templates/admin/widgets/color_radio_option.html
  24. 16
      apps/hadis/urls.py
  25. 370
      apps/hadis/views/category.py
  26. 85
      apps/hadis/views/hadis.py
  27. 63
      config/enhanced_auth_middleware.py
  28. 85
      config/settings/base.py
  29. 38
      config/urls.py
  30. 252
      docs/API_Documentation_System_README.md
  31. 1433
      docs/Custom_Swagger_API_Documentation_Implementation_Guide.md
  32. 169
      scripts/README.md
  33. 246
      scripts/clear_hadis_data.py
  34. 1087
      scripts/seed_hadis_data.py
  35. BIN
      scripts/seed_images/book1.png
  36. BIN
      scripts/seed_images/book2.png
  37. BIN
      scripts/seed_images/book3.png
  38. BIN
      scripts/seed_images/book4.png
  39. BIN
      scripts/test.xmind
  40. 711
      templates/api/documentation.html
  41. 402
      templates/swagger/auth.html
  42. 354
      templates/swagger/ui.html
  43. 23
      utils/pagination.py

34
apps/api/views.py

@ -1,32 +1,2 @@
import random
from rest_framework.generics import GenericAPIView
from rest_framework.response import Response
from rest_framework import serializers
from rest_framework.authtoken.models import Token
from apps.account.models import User
class HomeSerializer(serializers.Serializer):
token = serializers.CharField()
from utils.countries import countries
# test class generate token
class HomeView(GenericAPIView):
serializer_class = HomeSerializer
def get(self, request):
# Get build_number from headers
build_number = request.META.get('HTTP_BUILD_NUMBER')
# Print the build_number
print(f"Build Number: {build_number}")
return Response({'token': "ok", 'build_number': build_number})
class CountryView(GenericAPIView):
def get(self, request):
return Response(countries, status=200)
# Legacy views - moved to views/api_views.py for better organization
from .views.api_views import HomeView, CountryView

15
apps/api/views/__init__.py

@ -0,0 +1,15 @@
# API Views Package
# This package contains all API-related views organized by functionality
from .api_views import HomeView, CountryView
from .documentation import CustomAPIDocumentationView
from .swagger_views import CustomSwaggerView, SwaggerTokenAuthView, clear_swagger_auth
__all__ = [
'HomeView',
'CountryView',
'CustomAPIDocumentationView',
'CustomSwaggerView',
'SwaggerTokenAuthView',
'clear_swagger_auth',
]

31
apps/api/views/api_views.py

@ -0,0 +1,31 @@
import random
from rest_framework.generics import GenericAPIView
from rest_framework.response import Response
from rest_framework import serializers
from rest_framework.authtoken.models import Token
from apps.account.models import User
class HomeSerializer(serializers.Serializer):
token = serializers.CharField()
from utils.countries import countries
# test class generate token
class HomeView(GenericAPIView):
serializer_class = HomeSerializer
def get(self, request):
# Get build_number from headers
build_number = request.META.get('HTTP_BUILD_NUMBER')
# Print the build_number
print(f"Build Number: {build_number}")
return Response({'token': "ok", 'build_number': build_number})
class CountryView(GenericAPIView):
def get(self, request):
return Response(countries, status=200)

462
apps/api/views/documentation.py

@ -0,0 +1,462 @@
import json
from django.shortcuts import render
from django.views import View
from django.contrib.admin.views.decorators import staff_member_required
from django.utils.decorators import method_decorator
@method_decorator(staff_member_required, name='dispatch')
class CustomAPIDocumentationView(View):
"""
Custom API Documentation view with collapsible sidebar navigation
Requires admin login to access
"""
def get(self, request):
api_structure = self._get_api_structure()
context = {
'api_structure': api_structure,
'request': request,
'title': 'Imam Javad API Documentation',
'description': 'Comprehensive API documentation with interactive examples for the Imam Javad project',
}
return render(request, 'api/documentation.html', context)
def _get_api_structure(self):
"""
Define the API structure for the Imam Javad project with all apps and endpoints
"""
return {
'account': {
'name': 'Account Management',
'description': 'User authentication, registration, and profile management',
'endpoints': [
{
'name': 'User Registration',
'method': 'POST',
'url': '/api/account/register/',
'description': 'Register a new user account with email verification',
'parameters': [
{'name': 'email', 'type': 'string', 'description': 'User email address', 'required': True},
{'name': 'password', 'type': 'string', 'description': 'User password', 'required': True},
{'name': 'password_confirm', 'type': 'string', 'description': 'Password confirmation', 'required': True},
{'name': 'fullname', 'type': 'string', 'description': 'User full name', 'required': True},
],
'response_examples': {
'success': json.dumps({
"message": "Registration successful. Please check your email for verification code.",
"user_id": 123,
"email": "user@example.com"
}, indent=2),
'error': json.dumps({
"error": "Email already exists",
"details": "A user with this email address already exists."
}, indent=2)
}
},
{
'name': 'Email Verification',
'method': 'POST',
'url': '/api/account/verify/',
'description': 'Verify user email with verification code',
'parameters': [
{'name': 'email', 'type': 'string', 'description': 'User email address', 'required': True},
{'name': 'code', 'type': 'string', 'description': 'Verification code from email', 'required': True},
],
'response_examples': {
'success': json.dumps({
"message": "Email verified successfully",
"token": "abc123def456...",
"user": {
"id": 123,
"email": "user@example.com",
"fullname": "John Doe",
"is_verified": True
}
}, indent=2)
}
},
{
'name': 'User Login',
'method': 'POST',
'url': '/api/account/login/',
'description': 'Authenticate user and get access token',
'parameters': [
{'name': 'email', 'type': 'string', 'description': 'User email address', 'required': True},
{'name': 'password', 'type': 'string', 'description': 'User password', 'required': True},
{'name': 'fcm', 'type': 'string', 'description': 'FCM token for notifications', 'required': False},
{'name': 'device_id', 'type': 'string', 'description': 'Device identifier', 'required': False},
],
'response_examples': {
'success': json.dumps({
"token": "abc123def456...",
"user": {
"id": 123,
"email": "user@example.com",
"fullname": "John Doe",
"is_verified": True,
"profile_image": None
}
}, indent=2)
}
},
{
'name': 'User Profile',
'method': 'GET',
'url': '/api/account/profile/',
'description': 'Get current user profile information',
'parameters': [
{'name': 'Authorization', 'type': 'header', 'description': 'Token <user_token>', 'required': True},
],
'response_examples': {
'success': json.dumps({
"id": 123,
"email": "user@example.com",
"fullname": "John Doe",
"phone": "+989123456789",
"profile_image": "https://example.com/media/profiles/user.jpg",
"is_verified": True,
"date_joined": "2024-01-15T10:30:00Z"
}, indent=2)
}
}
]
},
'courses': {
'name': 'Course Management',
'description': 'Educational courses, lessons, and learning progress',
'endpoints': [
{
'name': 'Course Categories',
'method': 'GET',
'url': '/api/courses/categories/',
'description': 'Get list of all course categories',
'parameters': [],
'response_examples': {
'success': json.dumps({
"count": 5,
"results": [
{
"id": 1,
"title": "Islamic Studies",
"description": "Courses related to Islamic knowledge",
"image": "https://example.com/media/categories/islamic.jpg",
"courses_count": 12
},
{
"id": 2,
"title": "Arabic Language",
"description": "Arabic language learning courses",
"image": "https://example.com/media/categories/arabic.jpg",
"courses_count": 8
}
]
}, indent=2)
}
},
{
'name': 'Course List',
'method': 'GET',
'url': '/api/courses/',
'description': 'Get paginated list of courses with filtering options',
'parameters': [
{'name': 'category', 'type': 'integer', 'description': 'Filter by category ID', 'required': False},
{'name': 'search', 'type': 'string', 'description': 'Search in course titles', 'required': False},
{'name': 'page', 'type': 'integer', 'description': 'Page number for pagination', 'required': False},
],
'response_examples': {
'success': json.dumps({
"count": 25,
"next": "http://example.com/api/courses/?page=2",
"previous": None,
"results": [
{
"id": 1,
"title": "Introduction to Islamic Jurisprudence",
"slug": "intro-islamic-jurisprudence",
"category": {
"id": 1,
"title": "Islamic Studies"
},
"professor": {
"id": 1,
"name": "Dr. Ahmad Hassan",
"bio": "Expert in Islamic Law"
},
"thumbnail": "https://example.com/media/courses/course1.jpg",
"duration": "8 weeks",
"lessons_count": 24,
"participants_count": 156,
"price": "50.00",
"is_free": False
}
]
}, indent=2)
}
}
]
},
'hadis': {
'name': 'Hadis Collection',
'description': 'Islamic hadis texts organized by categories and sects',
'endpoints': [
{
'name': 'Hadis Sects',
'method': 'GET',
'url': '/api/hadis/categories/',
'description': 'Get list of hadis sects grouped by type (Shia/Sunni)',
'parameters': [],
'response_examples': {
'success': json.dumps({
"count": 4,
"results": {
"shia": [
{
"id": 1,
"title": "Twelver Shia",
"seo_field": None
}
],
"sunni": [
{
"id": 3,
"title": "Hanafi",
"seo_field": None
}
]
}
}, indent=2)
}
},
{
'name': 'Hadis Categories',
'method': 'GET',
'url': '/api/hadis/categories/<int:sect_id>/',
'description': 'Get hadis categories tree structure by sect ID',
'parameters': [
{'name': 'sect_id', 'type': 'integer', 'description': 'Hadis sect ID', 'required': True},
],
'response_examples': {
'success': json.dumps({
"count": 10,
"results": {
"quran": [
{
"id": 1,
"title": "Quranic Interpretations",
"order": 1,
"children": [
{
"id": 2,
"title": "Tafsir al-Mizan",
"order": 1,
"hadis_count": 45
}
]
}
],
"hadith": [
{
"id": 10,
"title": "Prophetic Traditions",
"order": 1,
"children": []
}
]
}
}, indent=2)
}
}
]
},
'library': {
'name': 'Digital Library',
'description': 'Books, documents, and downloadable resources',
'endpoints': [
{
'name': 'Book Categories',
'method': 'GET',
'url': '/api/library/categories/',
'description': 'Get list of book categories',
'parameters': [],
'response_examples': {
'success': json.dumps({
"count": 8,
"results": [
{
"id": 1,
"title": "Islamic Jurisprudence",
"description": "Books on Islamic law and jurisprudence",
"books_count": 45
}
]
}, indent=2)
}
},
{
'name': 'Book List',
'method': 'GET',
'url': '/api/library/books/',
'description': 'Get paginated list of books',
'parameters': [
{'name': 'category', 'type': 'integer', 'description': 'Filter by category ID', 'required': False},
{'name': 'search', 'type': 'string', 'description': 'Search in book titles and authors', 'required': False},
],
'response_examples': {
'success': json.dumps({
"count": 120,
"results": [
{
"id": 1,
"title": "Al-Kafi",
"author": "Muhammad ibn Ya'qub al-Kulayni",
"description": "One of the most important Shia hadith collections",
"cover_image": "https://example.com/media/books/alkafi.jpg",
"file_size": "15.2 MB",
"pages": 1200,
"language": "Arabic",
"download_count": 2456
}
]
}, indent=2)
}
}
]
},
'videos': {
'name': 'Video Content',
'description': 'Educational and religious video content',
'endpoints': [
{
'name': 'Video Categories',
'method': 'GET',
'url': '/api/videos/categories/',
'description': 'Get list of video categories',
'parameters': [],
'response_examples': {
'success': json.dumps({
"count": 6,
"results": [
{
"id": 1,
"title": "Lectures",
"description": "Educational lectures and talks",
"videos_count": 89
}
]
}, indent=2)
}
},
{
'name': 'Video List',
'method': 'GET',
'url': '/api/videos/list/',
'description': 'Get paginated list of videos',
'parameters': [
{'name': 'category', 'type': 'integer', 'description': 'Filter by category ID', 'required': False},
{'name': 'search', 'type': 'string', 'description': 'Search in video titles', 'required': False},
],
'response_examples': {
'success': json.dumps({
"count": 156,
"results": [
{
"id": 1,
"title": "Introduction to Islamic Philosophy",
"slug": "intro-islamic-philosophy",
"description": "A comprehensive introduction to Islamic philosophical thought",
"thumbnail": "https://example.com/media/videos/thumb1.jpg",
"duration": "45:30",
"views_count": 1234,
"speaker": "Dr. Ali Rezaei",
"upload_date": "2024-01-15"
}
]
}, indent=2)
}
}
]
},
'podcast': {
'name': 'Podcast Platform',
'description': 'Audio content and podcast episodes',
'endpoints': [
{
'name': 'Podcast Categories',
'method': 'GET',
'url': '/api/podcast/categories/',
'description': 'Get list of podcast categories',
'parameters': [],
'response_examples': {
'success': json.dumps({
"count": 4,
"results": [
{
"id": 1,
"title": "Religious Discussions",
"description": "Discussions on religious topics",
"podcasts_count": 23
}
]
}, indent=2)
}
}
]
},
'quiz': {
'name': 'Quiz System',
'description': 'Interactive quizzes and assessments',
'endpoints': [
{
'name': 'Quiz Detail',
'method': 'GET',
'url': '/api/quiz/<int:quiz_id>/',
'description': 'Get quiz details and questions',
'parameters': [
{'name': 'quiz_id', 'type': 'integer', 'description': 'Quiz ID', 'required': True},
],
'response_examples': {
'success': json.dumps({
"id": 1,
"title": "Islamic History Quiz",
"description": "Test your knowledge of Islamic history",
"each_question_timing": 30,
"questions": [
{
"id": 1,
"question": "When was the Battle of Badr fought?",
"options": [
{"id": 1, "text": "624 CE"},
{"id": 2, "text": "625 CE"},
{"id": 3, "text": "626 CE"},
{"id": 4, "text": "627 CE"}
]
}
]
}, indent=2)
}
}
]
},
'bookmarks': {
'name': 'Bookmarks & Ratings',
'description': 'User bookmarks and content ratings',
'endpoints': [
{
'name': 'Add Bookmark',
'method': 'POST',
'url': '/api/bookmarks/add/',
'description': 'Add content to user bookmarks',
'parameters': [
{'name': 'content_type', 'type': 'string', 'description': 'Type of content (course, video, etc.)', 'required': True},
{'name': 'object_id', 'type': 'integer', 'description': 'ID of the content object', 'required': True},
],
'response_examples': {
'success': json.dumps({
"message": "Bookmark added successfully",
"bookmark_id": 123
}, indent=2)
}
}
]
}
}

74
apps/api/views/swagger_views.py

@ -0,0 +1,74 @@
from django.shortcuts import render, redirect
from django.views import View
from django.contrib import messages
from django.contrib.admin.views.decorators import staff_member_required
from django.utils.decorators import method_decorator
from rest_framework.authtoken.models import Token
@method_decorator(staff_member_required, name='dispatch')
class CustomSwaggerView(View):
"""
Custom Swagger UI view with authentication banner
Requires admin login to access
"""
def get(self, request):
context = {
'swagger_spec_url': '/en/swagger.json', # Adjust based on your URL structure
'request': request,
}
return render(request, 'swagger/ui.html', context)
@method_decorator(staff_member_required, name='dispatch')
class SwaggerTokenAuthView(View):
"""
Token authentication management for Swagger
"""
def get(self, request):
context = {
'current_token': request.session.get('swagger_token'),
'user_info': request.session.get('swagger_user_info'),
}
return render(request, 'swagger/auth.html', context)
def post(self, request):
token = request.POST.get('token', '').strip()
if not token or len(token) != 40:
messages.error(request, 'Token must be exactly 40 characters long')
return redirect('swagger-token-auth')
try:
token_obj = Token.objects.get(key=token)
user = token_obj.user
if not user.is_active:
messages.error(request, 'User account is not active')
return redirect('swagger-token-auth')
request.session['swagger_token'] = token
request.session['swagger_user_info'] = {
'id': user.id,
'email': user.email,
'fullname': getattr(user, 'fullname', user.email),
'is_staff': user.is_staff,
'is_superuser': user.is_superuser,
'user_type': 'User'
}
messages.success(request, f'Successfully authenticated as {user.email}')
return redirect('schema-swagger-ui')
except Token.DoesNotExist:
messages.error(request, 'Invalid token')
return redirect('swagger-token-auth')
@staff_member_required
def clear_swagger_auth(request):
"""Clear swagger authentication from session"""
if 'swagger_token' in request.session:
del request.session['swagger_token']
if 'swagger_user_info' in request.session:
del request.session['swagger_user_info']
messages.success(request, 'Successfully logged out from Swagger')
return redirect('swagger-token-auth')

235
apps/hadis/admin/category.py

@ -1,222 +1,53 @@
from django.contrib import admin
from django.utils.translation import gettext_lazy as _
from django.http import JsonResponse
from django.urls import path
from django.db import models
from django.contrib import messages
from django.http import HttpResponseRedirect
from django.urls import reverse
from unfold.admin import ModelAdmin
from unfold.decorators import display
from dj_category.admin import BaseCategoryAdmin
from ajaxdatatable.admin import AjaxDatatable
from django.db.models import Case, When, Value
from django.utils.html import format_html
from unfold.admin import ModelAdmin
from unfold.decorators import display, action
from mptt.admin import DraggableMPTTAdmin
from apps.hadis.models import HadisCategory
from utils.admin import project_admin_site
from ..models import HadisSect, HadisCategory
@admin.register(HadisCategory)
class HadisCategoryAdmin(BaseCategoryAdmin, ModelAdmin):
change_form_template = 'admin/hadiscategory/change_form.html'
change_list_template = 'admin/category_index.html'
class HadisSectAdmin(ModelAdmin):
"""Admin for HadisSect model"""
list_display = ('sect_type', 'title', 'is_active', 'order')
list_filter = ('sect_type', 'is_active')
search_fields = ('title',)
ordering = ('order',)
fieldsets = (
(None, {
'fields': ('name', 'source_type', 'category_type', 'parent', 'is_active', 'order'),
'classes': ('unfold-fieldset',),
'fields': ('sect_type', 'title', 'is_active', 'order')
}),
)
search_fields = ['name']
list_display = ['name', 'source_type_badge', 'category_type', 'parent', 'is_active', 'order']
list_filter = ['source_type', 'category_type', 'is_active']
@display(description=_("Source Type"))
def source_type_badge(self, obj):
badge_classes = {
'quran': 'unfold-badge unfold-badge--success',
'hadith': 'unfold-badge unfold-badge--info',
'book': 'unfold-badge unfold-badge--warning',
# Add more source types as needed
}
badge_class = badge_classes.get(obj.source_type, 'unfold-badge')
return format_html('<span class="{}">{}</span>', badge_class, obj.get_source_type_display())
def get_form(self, request, obj=None, **kwargs):
form = super().get_form(request, obj, **kwargs)
return form
def get_urls(self):
urls = super().get_urls()
custom_urls = [
path('categories-ajax/hadiscategory/', self.admin_site.admin_view(self.ajax_categories), name='hadiscategory_ajax_categories'),
]
return custom_urls + urls
def get_categories_groupby_language(self, request=None, selected_values=(), is_multiple=False):
return super().get_categories(request, selected_values, is_multiple)
def ajax_update(self, request):
data = request.POST
src_node = self.model.objects.get(pk=int(data['srcNode']))
other_node = self.model.objects.get(pk=int(data['otherNode']))
if src_node.slug in self.base_categories or other_node.slug in self.base_categories:
return JsonResponse({'data': _('This item can not be modified')}, status=401)
mode = data['hitMode']
if mode == 'over':
src_node.move_to(other_node, 'first-child')
elif mode == 'after':
src_node.move_to(other_node, 'right')
elif mode == 'before':
src_node.move_to(other_node, 'left')
return JsonResponse({'data': 'ok'}, safe=False)
def get_categories(self, request=None, selected_values=(), is_multiple=False):
"""
Override the get_categories method to filter by source_type if provided in the request
"""
categories = super().get_categories(request, selected_values, is_multiple)
# If request has source_type parameter, filter the categories
if request and request.GET.get('source_type'):
source_type = request.GET.get('source_type')
# Filter the categories by source_type
filtered_categories = []
for category in categories:
# If it's a dictionary (serialized category)
if isinstance(category, dict) and category.get('source_type') == source_type:
filtered_categories.append(category)
# If it's a model instance
elif hasattr(category, 'source_type') and getattr(category, 'source_type') == source_type:
filtered_categories.append(category)
return filtered_categories
return categories
def ajax_categories(self, request):
"""
Handle AJAX request for categories with source_type filtering and search
"""
# Get source_type from request
source_type = request.GET.get('source_type')
# Get node_id if provided (for single node data)
node_id = request.GET.get('node_id')
# Get search term if provided
search = request.GET.get('search')
# Get parent level filter if provided
parent_level = request.GET.get('parent_level')
class HadisCategoryAdmin(DraggableMPTTAdmin, ModelAdmin):
"""Admin for HadisCategory model with MPTT tree support"""
list_display = ('indented_title', 'sect', 'source_type', 'order')
list_display_links = ('indented_title',)
list_filter = ('sect', 'source_type')
search_fields = ('title',)
ordering = ('tree_id', 'lft')
if node_id:
# Return data for a specific node
try:
node = self.model.objects.get(pk=int(node_id))
return JsonResponse({
'id': node.id,
'source_type': node.source_type,
'category_type': node.category_type,
'parent': node.parent_id,
'level': node.level_p # Add the level_p property
})
except self.model.DoesNotExist:
return JsonResponse({'error': 'Node not found'}, status=404)
# Get all categories
queryset = self.model.objects.all()
# Annotate queryset with level_p
queryset = queryset.annotate(
level_pp=Case(
When(parent=None, then=Value(1)),
When(parent__isnull=False, parent__parent=None, then=Value(2)),
default=Value(3),
output_field=models.IntegerField()
)
fieldsets = (
(None, {
'fields': ('parent', 'sect', 'source_type', 'title', 'order')
}),
(_('Files'), {
'fields': ('xmind_file',),
'classes': ('collapse',)
}),
)
# Filter by source_type if provided
if source_type:
queryset = queryset.filter(source_type=source_type)
# Filter by search term if provided
if search:
queryset = queryset.filter(name__icontains=search)
# Filter by parent_level if provided
if parent_level and parent_level.isdigit():
# Convert to integer
level = int(parent_level)
# Filter categories by level_p
queryset = queryset.filter(level_pp=level)
# Convert queryset to list of dictionaries for JSON response
categories = []
for category in queryset:
categories.append({
'key': category.id,
'title': category.name,
'parent': category.parent_id,
'source_type': category.source_type,
'category_type': category.category_type,
'level': category.level_p,
# Add data property to store additional information
'data': {
'parent': category.parent_id,
'level': category.level_p
}
})
return JsonResponse(categories, safe=False)
def save_model(self, request, obj, form, change):
# Get the level choice from the form data
level_choice = request.POST.get('level_choice_hidden')
# Get the parent from AJAX selection if provided
ajax_parent = request.POST.get('ajax_parent')
if ajax_parent and ajax_parent.isdigit():
# Set the parent for the object
try:
parent_category = self.model.objects.get(pk=int(ajax_parent))
obj.parent = parent_category
except self.model.DoesNotExist:
pass
# Let the parent class handle the save
super().save_model(request, obj, form, change)
# Add a message to trigger tree reload via JavaScript
messages.success(request, _("Category saved successfully. Tree will be reloaded."),
extra_tags='unfold-message unfold-message--success')
# Set a flag in the request to redirect back to the category index page
request._category_saved = True
def response_add(self, request, obj, post_url_continue=None):
"""
Override to redirect back to the category index page after adding a new category
"""
if hasattr(request, '_category_saved') and request._category_saved:
return HttpResponseRedirect(reverse('admin:hadis_hadiscategory_changelist'))
return super().response_add(request, obj, post_url_continue)
def response_change(self, request, obj):
"""
Override to redirect back to the category index page after editing a category
"""
if hasattr(request, '_category_saved') and request._category_saved:
return HttpResponseRedirect(reverse('admin:hadis_hadiscategory_changelist'))
return super().response_change(request, obj)
def indented_title(self, instance):
"""Display indented title for tree structure"""
return f"{'' * instance.level} {instance.title}"
indented_title.short_description = _('Title')
# Register with project_admin_site if needed
# project_admin_site.register(HadisCategory, HadisCategoryAdmin)
# Register models with the custom admin site
project_admin_site.register(HadisSect, HadisSectAdmin)
project_admin_site.register(HadisCategory, HadisCategoryAdmin)

370
apps/hadis/admin/hadis.py

@ -1,161 +1,209 @@
# from django.contrib import admin
# from django.utils.translation import gettext_lazy as _
# from dj_category.admin import BaseCategoryAdmin
# from ajaxdatatable.admin import AjaxDatatable
# from django.http import JsonResponse
# from django.urls import path
# from django.db.models import Q
# from django.utils.safestring import mark_safe
# from django.forms.widgets import RadioSelect
# from apps.hadis.models import *
# from django import forms
# from utils.json_editor_field import JsonEditorWidget
# # Define color choices
# COLOR_CHOICES = [
# ('red', _('Red')),
# ('blue', _('Blue')),
# ('green', _('Green')),
# ('yellow', _('Yellow')),
# ('orange', _('Orange')),
# ('purple', _('Purple')),
# ('pink', _('Pink')),
# ('brown', _('Brown')),
# ('gray', _('Gray')),
# ('black', _('Black')),
# ]
# class ColorRadioSelect(RadioSelect):
# template_name = 'admin/widgets/color_radio.html'
# option_template_name = 'admin/widgets/color_radio_option.html'
# def get_links_schema():
# return {
# 'type': "array",
# 'format': 'table',
# 'title': ' ',
# 'items': {
# 'type': 'object',
# 'title': str(_('Link')),
# 'properties': {
# 'text': {'type': 'string', "format": "textarea",'title': str(_('text'))},
# 'link': {'type': 'string', "format": "textarea", 'title': str(_('link'))},
# }
# }
# }
# class HadisOverviewForm(forms.ModelForm):
# status_color = forms.ChoiceField(
# choices=COLOR_CHOICES,
# widget=ColorRadioSelect(),
# required=False
# )
# class Meta:
# model = HadisOverview
# fields = '__all__'
# widgets = {
# 'links': JsonEditorWidget(attrs={'schema': get_links_schema}),
# }
# @admin.register(HadisTag)
# class HadisTagAdmin(AjaxDatatable):
# list_display = ['title', 'status']
# search_fields = ['title']
# class ReferenceImageInline(admin.TabularInline):
# model = ReferenceImage
# extra = 1
# verbose_name_plural = _('Reference Images')
# fields = ('thumbnail', 'priority')
# @admin.register(HadisReference)
# class HadisReferenceAdmin(AjaxDatatable):
# list_display = ['hadis', 'book', 'created_at']
# list_filter = ['book']
# search_fields = ['hadis__title', 'hadis__number', 'description']
# autocomplete_fields = ['hadis', 'book']
# readonly_fields = ['created_at']
# inlines = [ReferenceImageInline]
# fieldsets = (
# (None, {
# 'fields': ('hadis', 'book', 'description')
# }),
# )
# @admin.register(HadisOverview)
# class HadisOverviewAdmin(AjaxDatatable):
# change_form_template = 'admin/hadisowerview_change_form.html'
# form = HadisOverviewForm
# ordering = ['hadis__number']
# list_display = ['hadis', 'status', 'created_at']
# search_fields = ['hadis__title', 'hadis__number', 'status_text',]
# autocomplete_fields = ['hadis', 'tags']
# fieldsets = (
# (None, {
# 'fields': ('hadis', 'status', 'status_color', 'status_text')
# }),
# (_('Reference Information'), {
# 'fields': ('address', 'share_link',),
# }),
# (_('Additional Information'), {
# 'fields': ('links', 'tags', 'created_at'),
# 'classes': ('collapse',),
# }),
# )
# class HadisOverviewInline(admin.StackedInline):
# change_form_template = 'admin/hadisowerview_change_form.html'
# form = HadisOverviewForm
# model = HadisOverview
# autocomplete_fields = ['tags', ]
# can_delete = False
# verbose_name_plural = _('Hadis Overview')
# fieldsets = (
# (None, {
# 'fields': ('status', 'status_color', 'status_text', 'address', 'share_link', 'links', 'tags',),
# }),
# )
# extra = 1
# min_num = 1
# @admin.register(Hadis)
# class HadisAdmin(AjaxDatatable):
# # form = HadisForm
# list_display = ['number', 'title', 'category', 'status', 'created_at']
# list_filter = ['status', 'category']
# search_fields = ['title', 'text', 'number']
# readonly_fields = ['created_at', 'updated_at']
# autocomplete_fields = ['category']
# inlines = [HadisOverviewInline]
# fieldsets = (
# (None, {
# 'fields': ('number', 'title', 'category', 'status')
# }),
# (_('Content'), {
# 'fields': ('text', 'translation'),
# 'classes': ('collapse',),
# }),
# )
# def get_form(self, request, obj=None, **kwargs):
# form = super().get_form(request, obj, **kwargs)
# if obj is None:
# form.base_fields['category'].widget.can_add_related = False
# return form
from django import forms
from django.contrib import admin
from django.utils.translation import gettext_lazy as _
from unfold.admin import ModelAdmin, TabularInline
from unfold.contrib.forms.widgets import WysiwygWidget
from unfold.decorators import display, action
from utils.json_editor_field import JsonEditorWidget
import json
from utils.admin import project_admin_site
from ..models import (
Hadis, HadisReference, HadisTag, HadisStatus, ReferenceImage
)
# Custom Forms for JSON Fields
class HadisAdminForm(forms.ModelForm):
"""Custom form for Hadis with JSON editor widgets"""
class Meta:
model = Hadis
fields = '__all__'
widgets = {
'explanation': WysiwygWidget(),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Schema for translation JSON field
translation_schema = {
"type": "array",
"title": "Translations",
"items": {
"type": "object",
"title": "Translation",
"properties": {
"language_code": {
"type": "string",
"title": "Language Code",
"enum": ["en", "fa", "ar", "ur"],
"options": {
"enum_titles": ["English", "Persian", "Arabic", "Urdu"]
}
},
"title": {
"type": "string",
"title": "Translation Text"
}
},
"required": ["language_code", "title"]
}
}
# Schema for links JSON field (array of objects with title and link)
links_schema = {
"type": "array",
"title": "Links",
"items": {
"type": "object",
"title": "Link",
"properties": {
"title": {
"type": "string",
"title": "Link Title"
},
"link": {
"type": "string",
"title": "URL",
"format": "uri"
}
},
"required": ["title", "link"]
}
}
# Apply JSON editor widgets
self.fields['translation'].widget = JsonEditorWidget(attrs={
'schema': json.dumps(translation_schema),
'title': 'Translations'
})
self.fields['links'].widget = JsonEditorWidget(attrs={
'schema': json.dumps(links_schema),
'title': 'Links'
})
# Inline Admin Classes
class ReferenceImageInline(TabularInline):
"""Inline for ReferenceImage in HadisReference admin"""
model = ReferenceImage
extra = 0
fields = ('thumbnail', 'priority')
ordering = ('priority',)
class HadisReferenceInline(TabularInline):
"""Inline for HadisReference in Hadis admin"""
model = HadisReference
extra = 0
fields = ('book', 'description')
readonly_fields = ('created_at',)
# Main Admin Classes
class HadisTagAdmin(ModelAdmin):
"""Admin for HadisTag model"""
list_display = ('title', 'status', 'created_at')
list_filter = ('status', 'created_at')
search_fields = ('title',)
readonly_fields = ('created_at', 'updated_at')
fieldsets = (
(None, {
'fields': ('title', 'status')
}),
(_('Timestamps'), {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',)
}),
)
class HadisStatusAdmin(ModelAdmin):
"""Admin for HadisStatus model"""
list_display = ('title', 'color', 'order')
list_filter = ('color',)
search_fields = ('title',)
ordering = ('order',)
fieldsets = (
(None, {
'fields': ('title', 'color', 'order')
}),
)
class HadisAdmin(ModelAdmin):
"""Admin for Hadis model"""
form = HadisAdminForm
list_display = ('number', 'title', 'category', 'status', 'hadis_status', 'created_at')
list_filter = ('status', 'hadis_status', 'category', 'created_at')
search_fields = ('title', 'text', 'category__title')
readonly_fields = ('created_at', 'updated_at', 'share_link')
ordering = ('category', 'number')
inlines = [HadisReferenceInline]
filter_horizontal = ('tags',)
fieldsets = (
(None, {
'fields': ('category', 'number', 'title', 'status')
}),
(_('Content'), {
'fields': ('text', 'translation', 'explanation')
}),
(_('Status & Classification'), {
'fields': ('hadis_status', 'hadis_status_text', 'tags')
}),
(_('Additional Information'), {
'fields': ('address', 'links', 'share_link'),
'classes': ('collapse',)
}),
(_('Timestamps'), {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',)
}),
)
class HadisReferenceAdmin(ModelAdmin):
"""Admin for HadisReference model"""
list_display = ('hadis', 'book', 'created_at')
list_filter = ('created_at', 'book')
search_fields = ('hadis__title', 'book__title', 'description')
readonly_fields = ('created_at',)
inlines = [ReferenceImageInline]
fieldsets = (
(None, {
'fields': ('hadis', 'book', 'description')
}),
(_('Timestamps'), {
'fields': ('created_at',),
'classes': ('collapse',)
}),
)
class ReferenceImageAdmin(ModelAdmin):
"""Admin for ReferenceImage model"""
list_display = ('reference', 'thumbnail', 'priority')
list_filter = ('priority',)
search_fields = ('reference__hadis__title', 'reference__book__title')
ordering = ('reference', 'priority')
fieldsets = (
(None, {
'fields': ('reference', 'thumbnail', 'priority')
}),
)
# Register models with the custom admin site
project_admin_site.register(HadisTag, HadisTagAdmin)
project_admin_site.register(HadisStatus, HadisStatusAdmin)
project_admin_site.register(Hadis, HadisAdmin)
project_admin_site.register(HadisReference, HadisReferenceAdmin)
project_admin_site.register(ReferenceImage, ReferenceImageAdmin)

66
apps/hadis/admin/transmitter.py

@ -0,0 +1,66 @@
from django.contrib import admin
from django.utils.translation import gettext_lazy as _
from unfold.admin import ModelAdmin, TabularInline
from unfold.decorators import display, action
from utils.admin import project_admin_site
from ..models import Transmitters, HadisTransmitter
class HadisTransmitterInline(TabularInline):
"""Inline for HadisTransmitter in Transmitters admin"""
model = HadisTransmitter
extra = 0
fields = ('hadis', 'order', 'status', 'is_gap')
readonly_fields = ('created_at',)
class TransmittersAdmin(ModelAdmin):
"""Admin for Transmitters model"""
list_display = ('full_name', 'birth_year_hijri', 'death_year_hijri')
list_filter = ('birth_year_hijri', 'death_year_hijri')
search_fields = ('full_name', 'description')
readonly_fields = ('created_at', 'updated_at')
inlines = [HadisTransmitterInline]
fieldsets = (
(None, {
'fields': ('full_name', 'birth_year_hijri', 'death_year_hijri')
}),
(_('Additional Information'), {
'fields': ('description', 'thumbnail'),
'classes': ('collapse',)
}),
(_('Timestamps'), {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',)
}),
)
class HadisTransmitterAdmin(ModelAdmin):
"""Admin for HadisTransmitter model"""
list_display = ('hadis', 'transmitter', 'order', 'status', 'is_gap', 'created_at')
list_filter = ('status', 'is_gap', 'created_at')
search_fields = ('hadis__title', 'transmitter__full_name')
readonly_fields = ('created_at',)
ordering = ('hadis', 'order')
fieldsets = (
(None, {
'fields': ('hadis', 'transmitter', 'order')
}),
(_('Status & Gap Information'), {
'fields': ('status', 'is_gap'),
'description': _('Use "Is Gap" to mark missing links in the transmission chain')
}),
(_('Timestamps'), {
'fields': ('created_at',),
'classes': ('collapse',)
}),
)
# Register models with the custom admin site
project_admin_site.register(Transmitters, TransmittersAdmin)
project_admin_site.register(HadisTransmitter, HadisTransmitterAdmin)

452
apps/hadis/doc.py

@ -1,452 +0,0 @@
"""
Swagger documentation for the Hadis API endpoints.
This module provides Swagger documentation for the Hadis API endpoints using drf-yasg.
It defines the request parameters, response schemas, and decorators for the views.
"""
from drf_yasg import openapi
from drf_yasg.utils import swagger_auto_schema
from apps.hadis.models import HadisCategory
# Parameter definitions
source_type_param = openapi.Parameter(
'source_type',
openapi.IN_QUERY,
description="Filter categories by source type (shia or sunni)",
type=openapi.TYPE_STRING,
enum=[HadisCategory.SourceType.SHIA, HadisCategory.SourceType.SUNNI],
required=False
)
# Response schemas
tag_schema = openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'id': openapi.Schema(
type=openapi.TYPE_INTEGER,
description="Unique identifier for the tag"
),
'title': openapi.Schema(
type=openapi.TYPE_STRING,
description="Title of the tag"
)
},
required=['id', 'title']
)
category_schema = openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'id': openapi.Schema(
type=openapi.TYPE_INTEGER,
description="Unique identifier for the category"
),
'name': openapi.Schema(
type=openapi.TYPE_STRING,
description="Name of the category"
),
'hadis_count': openapi.Schema(
type=openapi.TYPE_INTEGER,
description="Number of hadis items in this category"
),
'source_type': openapi.Schema(
type=openapi.TYPE_STRING,
enum=[HadisCategory.SourceType.SHIA, HadisCategory.SourceType.SUNNI],
description="Source type of the category (shia or sunni)"
),
'category_type': openapi.Schema(
type=openapi.TYPE_STRING,
enum=[HadisCategory.ContentType.QURAN, HadisCategory.ContentType.HADITH],
description="Content type of the category (quran or hadith)",
nullable=True
),
'children': openapi.Schema(
type=openapi.TYPE_ARRAY,
items=openapi.Schema(type=openapi.TYPE_OBJECT), # Recursive reference
description="List of child categories"
)
},
required=['id', 'name', 'hadis_count', 'source_type', 'children']
)
categories_response = openapi.Response(
description="Tree structure of hadis categories",
schema=openapi.Schema(
type=openapi.TYPE_ARRAY,
items=category_schema
)
)
hadis_schema = openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'number': openapi.Schema(
type=openapi.TYPE_INTEGER,
description="Unique number identifier for the hadis"
),
'title': openapi.Schema(
type=openapi.TYPE_STRING,
description="Title of the hadis"
),
'text': openapi.Schema(
type=openapi.TYPE_STRING,
description="Original text of the hadis"
),
'translation': openapi.Schema(
type=openapi.TYPE_STRING,
description="Translation of the hadis text"
),
'tags': openapi.Schema(
type=openapi.TYPE_ARRAY,
items=tag_schema,
description="List of tags associated with this hadis"
)
},
required=['number', 'title', 'text', 'translation', 'tags']
)
hadis_list_response = openapi.Response(
description="List of hadis items in the specified category",
schema=openapi.Schema(
type=openapi.TYPE_ARRAY,
items=hadis_schema
)
)
# Reference image schema
reference_image_schema = openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'id': openapi.Schema(
type=openapi.TYPE_INTEGER,
description="Unique identifier for the reference image"
),
'thumbnail': openapi.Schema(
type=openapi.TYPE_INTEGER,
description="ID of the thumbnail image",
nullable=True
),
'priority': openapi.Schema(
type=openapi.TYPE_INTEGER,
description="Priority of the image (lower values mean higher priority)"
)
},
required=['id', 'priority']
)
# Hadis reference schema
hadis_reference_schema = openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'id': openapi.Schema(
type=openapi.TYPE_INTEGER,
description="Unique identifier for the hadis reference"
),
'book': openapi.Schema(
type=openapi.TYPE_INTEGER,
description="ID of the referenced book",
nullable=True
),
'description': openapi.Schema(
type=openapi.TYPE_STRING,
description="Description of the reference",
nullable=True
),
'created_at': openapi.Schema(
type=openapi.TYPE_STRING,
format=openapi.FORMAT_DATETIME,
description="Creation timestamp"
),
'images': openapi.Schema(
type=openapi.TYPE_ARRAY,
items=reference_image_schema,
description="List of reference images"
)
},
required=['id', 'created_at', 'images']
)
# Hadis overview schema
hadis_overview_schema = openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'status': openapi.Schema(
type=openapi.TYPE_STRING,
description="Status of the hadis"
),
'status_color': openapi.Schema(
type=openapi.TYPE_STRING,
description="Display color for the status"
),
'status_text': openapi.Schema(
type=openapi.TYPE_STRING,
description="Descriptive text for the status",
nullable=True
),
'address': openapi.Schema(
type=openapi.TYPE_STRING,
description="Address information",
nullable=True
),
'links': openapi.Schema(
type=openapi.TYPE_OBJECT,
description="Related links"
),
'tags': openapi.Schema(
type=openapi.TYPE_ARRAY,
items=tag_schema,
description="List of tags associated with this hadis"
),
'share_link': openapi.Schema(
type=openapi.TYPE_STRING,
description="Link for sharing the hadis",
nullable=True
),
'created_at': openapi.Schema(
type=openapi.TYPE_STRING,
format=openapi.FORMAT_DATETIME,
description="Creation timestamp"
)
},
required=['status', 'status_color', 'tags', 'created_at']
)
# Hadis detail schema
hadis_detail_schema = openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'id': openapi.Schema(
type=openapi.TYPE_INTEGER,
description="Unique identifier for the hadis"
),
'number': openapi.Schema(
type=openapi.TYPE_INTEGER,
description="Unique number identifier for the hadis"
),
'title': openapi.Schema(
type=openapi.TYPE_STRING,
description="Title of the hadis"
),
'text': openapi.Schema(
type=openapi.TYPE_STRING,
description="Original text of the hadis"
),
'translation': openapi.Schema(
type=openapi.TYPE_STRING,
description="Translation of the hadis text"
),
'status': openapi.Schema(
type=openapi.TYPE_BOOLEAN,
description="Visibility status of the hadis"
),
'created_at': openapi.Schema(
type=openapi.TYPE_STRING,
format=openapi.FORMAT_DATETIME,
description="Creation timestamp"
),
'updated_at': openapi.Schema(
type=openapi.TYPE_STRING,
format=openapi.FORMAT_DATETIME,
description="Last update timestamp"
),
'overview': hadis_overview_schema,
'first_reference': hadis_reference_schema
},
required=['id', 'number', 'title', 'text', 'translation', 'status', 'created_at', 'updated_at', 'overview']
)
hadis_detail_response = openapi.Response(
description="Detailed information about a specific hadis",
schema=hadis_detail_schema
)
# Transmitter schema
transmitter_schema = openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'id': openapi.Schema(
type=openapi.TYPE_INTEGER,
description="Unique identifier for the transmitter"
),
'full_name': openapi.Schema(
type=openapi.TYPE_STRING,
description="Full name of the transmitter"
),
'birth_year_hijri': openapi.Schema(
type=openapi.TYPE_INTEGER,
description="Birth year in Hijri calendar"
),
'death_year_hijri': openapi.Schema(
type=openapi.TYPE_INTEGER,
description="Death year in Hijri calendar"
),
'description': openapi.Schema(
type=openapi.TYPE_STRING,
description="Description of the transmitter",
nullable=True
),
'status': openapi.Schema(
type=openapi.TYPE_STRING,
description="Status of the transmitter"
),
'status_color': openapi.Schema(
type=openapi.TYPE_STRING,
description="Display color for the status"
),
'thumbnail': openapi.Schema(
type=openapi.TYPE_OBJECT,
description="Thumbnail image information",
nullable=True
)
},
required=['id', 'full_name', 'birth_year_hijri', 'death_year_hijri', 'status', 'status_color']
)
# Hadis transmitter schema
hadis_transmitter_schema = openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'id': openapi.Schema(
type=openapi.TYPE_INTEGER,
description="Unique identifier for the hadis transmitter relation"
),
'transmitter': transmitter_schema,
'description': openapi.Schema(
type=openapi.TYPE_STRING,
description="Description of the transmitter's role in this hadis",
nullable=True
),
'order': openapi.Schema(
type=openapi.TYPE_INTEGER,
description="Order in the chain of transmission"
),
'created_at': openapi.Schema(
type=openapi.TYPE_STRING,
format=openapi.FORMAT_DATETIME,
description="Creation timestamp"
)
},
required=['id', 'transmitter', 'order', 'created_at']
)
# Update hadis detail schema to include transmitters
hadis_detail_schema = openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'id': openapi.Schema(
type=openapi.TYPE_INTEGER,
description="Unique identifier for the hadis"
),
'number': openapi.Schema(
type=openapi.TYPE_INTEGER,
description="Unique number identifier for the hadis"
),
'title': openapi.Schema(
type=openapi.TYPE_STRING,
description="Title of the hadis"
),
'text': openapi.Schema(
type=openapi.TYPE_STRING,
description="Original text of the hadis"
),
'translation': openapi.Schema(
type=openapi.TYPE_STRING,
description="Translation of the hadis text"
),
'status': openapi.Schema(
type=openapi.TYPE_BOOLEAN,
description="Visibility status of the hadis"
),
'created_at': openapi.Schema(
type=openapi.TYPE_STRING,
format=openapi.FORMAT_DATETIME,
description="Creation timestamp"
),
'updated_at': openapi.Schema(
type=openapi.TYPE_STRING,
format=openapi.FORMAT_DATETIME,
description="Last update timestamp"
),
'overview': hadis_overview_schema,
'first_reference': hadis_reference_schema,
'transmitters': openapi.Schema(
type=openapi.TYPE_ARRAY,
items=hadis_transmitter_schema,
description="List of transmitters for this hadis"
)
},
required=['id', 'number', 'title', 'text', 'translation', 'status', 'created_at', 'updated_at', 'overview']
)
hadis_detail_response = openapi.Response(
description="Detailed information about a specific hadis",
schema=hadis_detail_schema
)
# Swagger decorators for views
category_list_swagger = swagger_auto_schema(
operation_id="list_hadis_categories",
operation_description="""
Retrieve a hierarchical tree structure of hadis categories.
This endpoint returns all hadis categories in a tree structure, with parent categories
containing their child categories. Each category includes its ID, name, source type,
category type, and the count of hadis items it contains.
The response can be filtered by source type (shia or sunni) using the query parameter.
If no source type is specified, all categories are returned.
""",
operation_summary="List Hadis Categories",
tags=["Hadis"],
manual_parameters=[source_type_param],
responses={
200: categories_response,
401: "Authentication credentials were not provided or are invalid.",
500: "Internal server error occurred."
}
)
category_hadis_list_swagger = swagger_auto_schema(
operation_id="list_hadis_in_category",
operation_description="""
Retrieve a list of hadis items belonging to a specific category.
This endpoint returns all hadis items that belong to the specified category.
Each hadis item includes its number, title, original text, translation, and associated tags.
The category is specified by its ID in the URL path.
""",
operation_summary="List Hadis Items in Category",
tags=["Hadis"],
responses={
200: hadis_list_response,
401: "Authentication credentials were not provided or are invalid.",
404: "The specified category does not exist.",
500: "Internal server error occurred."
}
)
hadis_detail_swagger = swagger_auto_schema(
operation_id="get_hadis_detail",
operation_description="""
Retrieve detailed information about a specific hadis.
This endpoint returns comprehensive information about a hadis, including:
- Basic hadis details (number, title, text, translation)
- HadisOverview information (status, tags, etc.)
- The first HadisReference with its ReferenceImages
- List of Transmitters in order of transmission chain
The hadis is specified by its ID in the URL path.
""",
operation_summary="Get Hadis Detail",
tags=["Hadis"],
responses={
200: hadis_detail_response,
401: "Authentication credentials were not provided or are invalid.",
404: "The specified hadis does not exist.",
500: "Internal server error occurred."
}
)

341
apps/hadis/docs.py

@ -0,0 +1,341 @@
from drf_yasg.utils import swagger_auto_schema
from drf_yasg import openapi
from rest_framework import status
# Swagger documentation for HadisSectListView
hadis_sect_list_swagger = swagger_auto_schema(
operation_description="Get list of all active Hadis sects grouped by sect type (Shia/Sunni)",
operation_summary="List Hadis Sects",
tags=['Hadis'],
responses={
status.HTTP_200_OK: openapi.Response(
description="List of hadis sects grouped by type with count",
examples={
"application/json": {
"count": 4,
"results": {
"shia": [
{
"id": 1,
"title": "Twelver Shia",
"seo_field": None
},
{
"id": 2,
"title": "Ismaili Shia",
"seo_field": None
}
],
"sunni": [
{
"id": 3,
"title": "Hanafi",
"seo_field": None
},
{
"id": 4,
"title": "Maliki",
"seo_field": None
}
]
}
}
}
),
status.HTTP_500_INTERNAL_SERVER_ERROR: openapi.Response(
description="Internal server error"
)
}
)
# Swagger documentation for HadisCategoryTreeView
hadis_category_tree_swagger = swagger_auto_schema(
operation_description="Get hierarchical tree structure of Hadis categories for a specific sect, grouped by source type (Quran/Hadith)",
operation_summary="Get Hadis Category Tree by Sect",
tags=['Hadis'],
manual_parameters=[
openapi.Parameter(
'sect_id',
openapi.IN_PATH,
description="ID of the Hadis sect",
type=openapi.TYPE_INTEGER,
required=True
)
],
responses={
status.HTTP_200_OK: openapi.Response(
description="Hierarchical tree structure of categories with total count",
examples={
"application/json": {
"count": 6,
"results": {
"quran": [
{
"id": 1,
"name": "Tafsir",
"hadis_count": 150,
"has_hadis": False,
"order": 1,
"xmind_file": "http://example.com/media/xmind/tafsir.xmind",
"has_xmind_file": True,
"children": [
{
"id": 2,
"name": "Surah Al-Fatiha",
"hadis_count": 25,
"has_hadis": True,
"order": 1,
"xmind_file": None,
"has_xmind_file": False,
"children": []
},
{
"id": 3,
"name": "Surah Al-Baqarah",
"hadis_count": 125,
"has_hadis": True,
"order": 2,
"xmind_file": "http://example.com/media/xmind/baqarah.xmind",
"has_xmind_file": True,
"children": []
}
]
}
],
"hadith": [
{
"id": 4,
"name": "Sahih Bukhari",
"hadis_count": 300,
"has_hadis": False,
"order": 1,
"xmind_file": "http://example.com/media/xmind/bukhari.xmind",
"has_xmind_file": True,
"children": [
{
"id": 5,
"name": "Book of Faith",
"hadis_count": 50,
"has_hadis": True,
"order": 1,
"xmind_file": None,
"has_xmind_file": False,
"children": []
},
{
"id": 6,
"name": "Book of Prayer",
"hadis_count": 250,
"has_hadis": True,
"order": 2,
"xmind_file": "http://example.com/media/xmind/prayer.xmind",
"has_xmind_file": True,
"children": []
}
]
}
]
}
}
}
),
status.HTTP_404_NOT_FOUND: openapi.Response(
description="Sect not found"
),
status.HTTP_500_INTERNAL_SERVER_ERROR: openapi.Response(
description="Internal server error"
)
}
)
# Swagger documentation for HadisListView
hadis_list_swagger = swagger_auto_schema(
operation_description="Get paginated list of Hadis for a specific category with translations based on request language",
operation_summary="List Hadis by Category",
tags=['Hadis'],
manual_parameters=[
openapi.Parameter(
'category_id',
openapi.IN_PATH,
description="ID of the Hadis category",
type=openapi.TYPE_INTEGER,
required=True
),
openapi.Parameter(
'page',
openapi.IN_QUERY,
description="Page number for pagination",
type=openapi.TYPE_INTEGER,
required=False
),
openapi.Parameter(
'Accept-Language',
openapi.IN_HEADER,
description="Language code for translations (en, fa, ar, ur)",
type=openapi.TYPE_STRING,
required=False,
default='en'
)
],
responses={
status.HTTP_200_OK: openapi.Response(
description="Paginated list of hadis",
examples={
"application/json": {
"count": 150,
"next": "http://example.com/api/hadis/category/1/hadis/?page=2",
"previous": None,
"results": [
{
"id": 1,
"number": 1,
"title": "The first hadis about faith",
"category": {
"id": 1,
"title": "Book of Faith"
},
"translation": "This is the English translation of the hadis"
},
{
"id": 2,
"number": 2,
"title": "The second hadis about prayer",
"category": {
"id": 1,
"title": "Book of Faith"
},
"translation": "This is the English translation of the second hadis"
}
]
}
}
),
status.HTTP_404_NOT_FOUND: openapi.Response(
description="Category not found"
),
status.HTTP_500_INTERNAL_SERVER_ERROR: openapi.Response(
description="Internal server error"
)
}
)
hadis_detail_swagger = swagger_auto_schema(
operation_summary="Get Hadis Details",
operation_description="Retrieve detailed information about a specific hadis including status, tags, transmitters, and references",
tags=['Hadis'],
responses={
200: openapi.Response(
description="Hadis details retrieved successfully",
schema=openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'id': openapi.Schema(type=openapi.TYPE_INTEGER, description='Hadis ID'),
'number': openapi.Schema(type=openapi.TYPE_INTEGER, description='Hadis number'),
'title': openapi.Schema(type=openapi.TYPE_STRING, description='Hadis title'),
'text': openapi.Schema(type=openapi.TYPE_STRING, description='Arabic text of hadis'),
'translation': openapi.Schema(type=openapi.TYPE_STRING, description='Translation in request language'),
'explanation': openapi.Schema(type=openapi.TYPE_STRING, description='Detailed explanation'),
'address': openapi.Schema(type=openapi.TYPE_STRING, description='Source address'),
'hadis_status_text': openapi.Schema(type=openapi.TYPE_STRING, description='Status description'),
'links': openapi.Schema(
type=openapi.TYPE_ARRAY,
items=openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'title': openapi.Schema(type=openapi.TYPE_STRING),
'link': openapi.Schema(type=openapi.TYPE_STRING)
}
),
description='Related links'
),
'status': openapi.Schema(type=openapi.TYPE_BOOLEAN, description='Active status'),
'category': openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'id': openapi.Schema(type=openapi.TYPE_INTEGER),
'title': openapi.Schema(type=openapi.TYPE_STRING),
'category_type': openapi.Schema(type=openapi.TYPE_STRING)
}
),
'hadis_status': openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'id': openapi.Schema(type=openapi.TYPE_INTEGER),
'title': openapi.Schema(type=openapi.TYPE_STRING),
'color': openapi.Schema(type=openapi.TYPE_STRING)
}
),
'tags': openapi.Schema(
type=openapi.TYPE_ARRAY,
items=openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'id': openapi.Schema(type=openapi.TYPE_INTEGER),
'title': openapi.Schema(type=openapi.TYPE_STRING)
}
)
),
'transmitters': openapi.Schema(
type=openapi.TYPE_ARRAY,
items=openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'id': openapi.Schema(type=openapi.TYPE_INTEGER),
'order': openapi.Schema(type=openapi.TYPE_INTEGER),
'is_gap': openapi.Schema(type=openapi.TYPE_BOOLEAN),
'transmitter': openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'id': openapi.Schema(type=openapi.TYPE_INTEGER),
'full_name': openapi.Schema(type=openapi.TYPE_STRING),
'birth_year_hijri': openapi.Schema(type=openapi.TYPE_INTEGER),
'death_year_hijri': openapi.Schema(type=openapi.TYPE_INTEGER),
'description': openapi.Schema(type=openapi.TYPE_STRING)
}
)
}
)
),
'references': openapi.Schema(
type=openapi.TYPE_ARRAY,
items=openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'id': openapi.Schema(type=openapi.TYPE_INTEGER),
'page_number': openapi.Schema(type=openapi.TYPE_STRING),
'hadis_number_in_book': openapi.Schema(type=openapi.TYPE_STRING),
'description': openapi.Schema(type=openapi.TYPE_STRING),
'book': openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'id': openapi.Schema(type=openapi.TYPE_INTEGER),
'title': openapi.Schema(type=openapi.TYPE_STRING),
'summary_title': openapi.Schema(type=openapi.TYPE_STRING),
'publisher': openapi.Schema(type=openapi.TYPE_STRING),
'year_of_publication': openapi.Schema(type=openapi.TYPE_STRING)
}
),
'images': openapi.Schema(
type=openapi.TYPE_ARRAY,
items=openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'id': openapi.Schema(type=openapi.TYPE_INTEGER),
'thumbnail': openapi.Schema(type=openapi.TYPE_STRING),
'priority': openapi.Schema(type=openapi.TYPE_INTEGER)
}
)
)
}
)
)
}
)
),
404: openapi.Response(description="Hadis not found")
}
)

217
apps/hadis/migrations/0002_hadissect_hadisstatus_alter_hadis_options_and_more.py

@ -0,0 +1,217 @@
# Generated by Django 5.1.8 on 2025-07-04 11:34
import django.db.models.deletion
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('hadis', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='HadisSect',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('sect_type', models.CharField(choices=[('shia', 'Shia'), ('sunni', 'Sunni')], max_length=10, unique=True, verbose_name='Sect Name')),
('title', models.CharField(max_length=256, verbose_name='Name')),
('is_active', models.BooleanField(default=True, verbose_name='Is Active')),
('order', models.IntegerField(default=0, verbose_name='order')),
],
options={
'verbose_name': 'Hadis Sect',
'verbose_name_plural': 'Hadis Sects',
'ordering': ('order',),
},
),
migrations.CreateModel(
name='HadisStatus',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=119, verbose_name='title')),
('color', models.CharField(choices=[('red', 'Red'), ('green', 'Green'), ('blue', 'Blue'), ('yellow', 'Yellow'), ('orange', 'Orange'), ('purple', 'Purple'), ('gray', 'Gray')], max_length=20, verbose_name='color')),
('order', models.IntegerField(default=0, verbose_name='order')),
],
options={
'verbose_name': 'hadis status',
'verbose_name_plural': 'hadis statuses',
'ordering': ('order',),
},
),
migrations.AlterModelOptions(
name='hadis',
options={'ordering': ('category', 'number'), 'verbose_name': 'hadis', 'verbose_name_plural': 'hadises'},
),
migrations.AlterModelOptions(
name='transmitters',
options={'ordering': ('full_name',), 'verbose_name': 'Transmitter', 'verbose_name_plural': 'Transmitters'},
),
migrations.RemoveField(
model_name='hadiscategory',
name='category_type',
),
migrations.RemoveField(
model_name='hadiscategory',
name='created_at',
),
migrations.RemoveField(
model_name='hadiscategory',
name='is_active',
),
migrations.RemoveField(
model_name='hadiscategory',
name='name',
),
migrations.RemoveField(
model_name='hadistransmitter',
name='description',
),
migrations.RemoveField(
model_name='transmitters',
name='status',
),
migrations.RemoveField(
model_name='transmitters',
name='status_color',
),
migrations.AddField(
model_name='hadis',
name='address',
field=models.TextField(blank=True, null=True, verbose_name='address'),
),
migrations.AddField(
model_name='hadis',
name='explanation',
field=models.TextField(blank=True, null=True, verbose_name='explanation'),
),
migrations.AddField(
model_name='hadis',
name='hadis_status_text',
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='hadis status text'),
),
migrations.AddField(
model_name='hadis',
name='links',
field=models.JSONField(blank=True, default=dict, null=True, verbose_name='links'),
),
migrations.AddField(
model_name='hadis',
name='share_link',
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='share link'),
),
migrations.AddField(
model_name='hadis',
name='tags',
field=models.ManyToManyField(blank=True, related_name='hadis_overview', to='hadis.hadistag', verbose_name='tags'),
),
migrations.AddField(
model_name='hadiscategory',
name='title',
field=models.CharField(default='Default Category', max_length=256, verbose_name='Title'),
preserve_default=False,
),
migrations.AddField(
model_name='hadiscategory',
name='xmind_file',
field=models.FileField(blank=True, null=True, upload_to='hadis/xmind_files/', verbose_name='xmind file'),
),
migrations.AddField(
model_name='hadistag',
name='created_at',
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='created at'),
preserve_default=False,
),
migrations.AddField(
model_name='hadistag',
name='updated_at',
field=models.DateTimeField(auto_now=True, verbose_name='updated at'),
),
migrations.AddField(
model_name='hadistransmitter',
name='is_gap',
field=models.BooleanField(default=False, help_text='Check this if this represents a gap in the transmission chain', verbose_name='Is Gap'),
),
migrations.AddField(
model_name='transmitters',
name='created_at',
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='created at'),
preserve_default=False,
),
migrations.AddField(
model_name='transmitters',
name='updated_at',
field=models.DateTimeField(auto_now=True, verbose_name='updated at'),
),
migrations.AlterField(
model_name='hadis',
name='category',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='hadis.hadiscategory', verbose_name='category'),
),
migrations.AlterField(
model_name='hadis',
name='number',
field=models.PositiveIntegerField(default=1, verbose_name='number'),
),
migrations.AlterField(
model_name='hadis',
name='title',
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='title'),
),
migrations.AlterField(
model_name='hadis',
name='translation',
field=models.JSONField(default=list, verbose_name='translation'),
),
migrations.AlterField(
model_name='hadiscategory',
name='source_type',
field=models.CharField(choices=[('quran', 'Quran'), ('hadith', 'Hadith')], max_length=10, verbose_name='Source Type'),
),
migrations.AlterField(
model_name='hadistransmitter',
name='transmitter',
field=models.ForeignKey(blank=True, help_text='Leave empty if this represents a gap in the chain', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='hadises', to='hadis.transmitters', verbose_name='transmitter'),
),
migrations.AlterField(
model_name='referenceimage',
name='thumbnail',
field=models.ImageField(blank=True, null=True, upload_to='hadis/reference_images/', verbose_name='thumbnail'),
),
migrations.AlterField(
model_name='transmitters',
name='birth_year_hijri',
field=models.IntegerField(blank=True, null=True, verbose_name='Birth Year (Hijri)'),
),
migrations.AlterField(
model_name='transmitters',
name='death_year_hijri',
field=models.IntegerField(blank=True, null=True, verbose_name='Death Year (Hijri)'),
),
migrations.AlterField(
model_name='transmitters',
name='full_name',
field=models.CharField(max_length=255, verbose_name='full name'),
),
migrations.AddField(
model_name='hadiscategory',
name='sect',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.PROTECT, to='hadis.hadissect', verbose_name='Sect'),
preserve_default=False,
),
migrations.AddField(
model_name='hadis',
name='hadis_status',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='hadis.hadisstatus', verbose_name='hadis status'),
),
migrations.AddField(
model_name='hadistransmitter',
name='status',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='transmitters', to='hadis.hadisstatus', verbose_name='status'),
),
migrations.DeleteModel(
name='HadisOverview',
),
]

106
apps/hadis/models/category.py

@ -1,34 +1,45 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ValidationError
from dj_category.models import BaseCategoryAbstract
from mptt.models import MPTTModel, TreeForeignKey
class HadisCategory(BaseCategoryAbstract):
class SourceType(models.TextChoices):
class HadisSect(models.Model):
class SectType(models.TextChoices):
SHIA = 'shia', _('Shia')
SUNNI = 'sunni', _('Sunni')
class ContentType(models.TextChoices):
sect_type = models.CharField(max_length=10, choices=SectType.choices, unique=True, verbose_name=_('Sect Name'))
title = models.CharField(max_length=256, verbose_name=_('Name'))
is_active = models.BooleanField(default=True, verbose_name=_('Is Active'))
order = models.IntegerField(default=0, verbose_name=_('order'))
def __str__(self):
return f"{self.sect_type}: {self.title}"
class Meta:
verbose_name = _('Hadis Sect')
verbose_name_plural = _('Hadis Sects')
ordering = ('order',)
class HadisCategory(MPTTModel):
class SourceType(models.TextChoices):
QURAN = 'quran', _('Quran')
HADITH = 'hadith', _('Hadith')
class LevelChoices(models.IntegerChoices):
LEVEL_1 = 1, _('Level 1 (Root)')
LEVEL_2 = 2, _('Level 2 (Child)')
LEVEL_3 = 3, _('Level 3 (Grandchild)')
source_type = models.CharField(max_length=10, choices=SourceType.choices, default=SourceType.SHIA, verbose_name=_('Source Type'), blank=True)
category_type = models.CharField(max_length=10, choices=ContentType.choices, verbose_name=_('Category Content Type'), blank=True, null=True)
name = models.CharField(max_length=355, verbose_name=_('name'))
parent = TreeForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, related_name='children')
sect = models.ForeignKey(HadisSect, on_delete=models.PROTECT, verbose_name=_('Sect'), null=False, blank=False)
source_type = models.CharField(max_length=10, choices=SourceType.choices, verbose_name=_('Source Type'))
title = models.CharField(max_length=256, verbose_name=_('Title'))
order = models.IntegerField(default=0, verbose_name=_('order'))
xmind_file = models.FileField(upload_to='hadis/xmind_files/', verbose_name=_('xmind file'), null=True, blank=True)
slug = None
content_type = None
language = None
language_id = None
# This field is not stored in the database, it's only used for the form
level_choice = None
class Meta:
verbose_name = _('Hadis Category')
@ -36,70 +47,5 @@ class HadisCategory(BaseCategoryAbstract):
ordering = ('order',)
def __str__(self):
return f'<{str(self.level_p)}>{self.name}'
def __repr__(self):
return f'<{str(self.level_p)}>{self.name}'
def clean(self):
super().clean()
# Skip validation for new objects that haven't been saved yet
# This allows the admin form to set these values properly
if self.pk is None:
return
# For existing objects, apply the validation rules
if self.level_p == 1 and self.category_type:
raise ValidationError(_("Level 1 cannot have content type"))
if self.level_p == 2 and not self.category_type:
raise ValidationError(_("Level 2 must have content type"))
if self.level_p == 3 and (self.source_type or self.category_type):
raise ValidationError(_("Level 3 cannot have source/content type"))
def save(self, *args, **kwargs):
self.clean()
# Get the level from the parent structure
level = self.level_p
# Apply level-specific logic
# if level == 2 and self.parent:
# For level 2, inherit source_type from parent
# self.source_type = self.parent.source_type
# elif level == 3:
# For level 3, inherit both from parent
# if self.parent and self.parent.parent:
# self.source_type = self.parent.source_type
# self.category_type = self.parent.category_type
# Call the parent class's save method
super().save(*args, **kwargs)
@property
def level_p(self):
if not self.parent:
return 1
elif not self.parent.parent:
return 2
else:
return 3
def get_level_info(self):
info = {
'level': self.level_p,
'source_type': None,
'category_type': None,
}
if self.level_p == 1:
info['source_type'] = self.source_type
elif self.level_p == 2:
info['source_type'] = self.parent.source_type
info['category_type'] = self.category_type
return info
return f"{self.sect.sect_type}: {self.source_type} - {self.title}"

97
apps/hadis/models/hadis.py

@ -1,58 +1,96 @@
from django.db import models
from django.db.models import F
from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ValidationError
from filer.fields.image import FilerImageField
from django.conf import settings
class HadisTag(models.Model):
title = models.CharField(max_length=355, verbose_name=_('title'))
status = models.BooleanField(default=True, verbose_name=_('status'))
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at'))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at'))
def __str__(self):
return f"{self.title}"
class HadisStatus(models.Model):
class ColorChoices(models.TextChoices):
RED = 'red', _('Red')
GREEN = 'green', _('Green')
BLUE = 'blue', _('Blue')
YELLOW = 'yellow', _('Yellow')
ORANGE = 'orange', _('Orange')
PURPLE = 'purple', _('Purple')
GRAY = 'gray', _('Gray')
title = models.CharField(max_length=119, verbose_name=_('title'))
color = models.CharField(max_length=20, choices=ColorChoices.choices, verbose_name=_('color'))
order = models.IntegerField(default=0, verbose_name=_('order'))
def __str__(self):
return self.title
class Meta:
verbose_name = _('hadis status')
verbose_name_plural = _('hadis statuses')
ordering = ('order',)
class Hadis(models.Model):
number = models.PositiveIntegerField(verbose_name=_('number'), unique=True)
title = models.CharField(max_length=355, verbose_name=_('title'))
category = models.ForeignKey("hadis.HadisCategory", on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_('category'))
number = models.PositiveIntegerField(verbose_name=_('number'), default=1)
title = models.CharField(max_length=255, verbose_name=_('title'), null=True, blank=True)
text = models.TextField(verbose_name=_('text'))
translation = models.TextField(verbose_name=_('translation'), blank=True, default='')
translation = models.JSONField(verbose_name=_('translation'), default=list)
status = models.BooleanField(default=True, verbose_name=_('visibility'))
category = models.ForeignKey("hadis.HadisCategory", null=True, on_delete=models.SET_NULL, verbose_name=_('category'), )
hadis_status = models.ForeignKey(HadisStatus, on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_('hadis status'))
hadis_status_text = models.CharField(max_length=255, verbose_name=_('hadis status text'), null=True, blank=True)
address = models.TextField(verbose_name=_('address'), null=True, blank=True)
links = models.JSONField(verbose_name=_('links'), null=True, blank=True, default=dict)
tags = models.ManyToManyField("HadisTag", related_name="hadis_overview", verbose_name=_('tags'), blank=True)
share_link = models.CharField(max_length=255, verbose_name=_('share link'), null=True, blank=True)
explanation = models.TextField(verbose_name=_('explanation'), null=True, blank=True)
status = models.BooleanField(default=True, verbose_name=_('visibility'))
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at'))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at'))
def __str__(self):
return f"<{self.number}> {self.title[:32]}"
return f"{self.number} - {self.title}" if self.title else f"Hadis {self.number}"
def get_translation(self, lang):
"""
Get translation for a specific language
"""
if not self.translation or not isinstance(self.translation, list):
return None
@property
def get_tags(self):
return self.tags.all().order_by('hadistagrelation__priority')
for tr in self.translation:
if isinstance(tr, dict) and tr.get('language_code') == lang:
return tr.get('title', '')
for tr in self.translation:
if isinstance(tr, dict) and tr.get('language_code') == 'en':
return tr.get('title', '')
return None
def save(self, *args, **kwargs):
# ساخت share_link قبل از ذخیره
if not self.share_link:
self.share_link = f"{settings.SITE_DOMAIN}/hadis/{self.id}"
super().save(*args, **kwargs)
class Meta:
verbose_name = _('hadis')
verbose_name_plural = _('hadises')
ordering = ('category', 'number')
class HadisOverview(models.Model):
hadis = models.OneToOneField(Hadis, on_delete=models.CASCADE, primary_key=True)
status = models.CharField(max_length=50, verbose_name=_('status'))
status_color = models.CharField(max_length=25, verbose_name=_('Display Status Color'))
status_text = models.TextField(verbose_name=_('Status Text'), null=True, blank=True)
address = models.TextField(verbose_name=_('address'), null=True, blank=True)
links = models.JSONField(verbose_name=_('title'), null=True, blank=True, default=dict)
tags = models.ManyToManyField("HadisTag", related_name="hadises", verbose_name=_('tags'), blank=True)
share_link = models.CharField(max_length=255, verbose_name=_('share link'), null=True, blank=True)
explanation = models.TextField(verbose_name=_('explanation'), null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at'))
class HadisReference(models.Model):
hadis = models.ForeignKey(
@ -71,14 +109,11 @@ class HadisReference(models.Model):
unique_together = ('hadis', 'book')
def __str__(self):
return f'{self.hadis.number}-{self.book.title}'
return f'{self.hadis.number}-{self.book.title if self.book else "No Book"}'
class ReferenceImage(models.Model):
reference = models.ForeignKey(HadisReference, verbose_name="Hadis Reference", on_delete=models.CASCADE)
thumbnail = FilerImageField(
related_name='+', on_delete=models.PROTECT, null=True, blank=True,
verbose_name=_('thumbnail')
)
thumbnail = models.ImageField(upload_to='hadis/reference_images/', null=True, blank=True, verbose_name=_('thumbnail'))
priority = models.IntegerField(
default=0,
verbose_name=_("Priority"),

52
apps/hadis/models/transmitter.py

@ -2,27 +2,37 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ValidationError
from filer.fields.image import FilerImageField
class Transmitters(models.Model):
full_name = models.CharField(max_length=255)
birth_year_hijri = models.IntegerField(verbose_name="Birth Year (Hijri)")
death_year_hijri = models.IntegerField(verbose_name="Death Year (Hijri)")
description = models.TextField(blank=True, null=True, verbose_name="Description")
status = models.CharField(max_length=50, verbose_name=_('status'))
status_color = models.CharField(max_length=25, verbose_name=_('Display Status Color'))
thumbnail = FilerImageField(related_name="+", on_delete=models.CASCADE, help_text=_(
'image allowed'
), null=True, blank=True)
full_name = models.CharField(max_length=255, verbose_name=_('full name'))
birth_year_hijri = models.IntegerField(verbose_name=_("Birth Year (Hijri)"), null=True, blank=True)
death_year_hijri = models.IntegerField(verbose_name=_("Death Year (Hijri)"), null=True, blank=True)
description = models.TextField(blank=True, null=True, verbose_name=_("Description"))
thumbnail = FilerImageField(
related_name="+",
on_delete=models.CASCADE,
help_text=_('image allowed'),
null=True,
blank=True
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at'))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at'))
class Meta:
verbose_name = _('Transmitter')
verbose_name_plural = _('Transmitters')
ordering = ('full_name',)
def __str__(self):
return self.full_name
class HadisTransmitter(models.Model):
hadis = models.ForeignKey(
"hadis.Hadis",
on_delete=models.CASCADE,
@ -33,14 +43,30 @@ class HadisTransmitter(models.Model):
Transmitters,
on_delete=models.CASCADE,
verbose_name=_('transmitter'),
related_name='hadises'
related_name='hadises',
null=True,
blank=True,
help_text=_('Leave empty if this represents a gap in the chain')
)
status = models.ForeignKey(
"hadis.HadisStatus",
on_delete=models.SET_NULL,
verbose_name=_('status'),
related_name='transmitters',
null=True,
blank=True
)
description = models.TextField(verbose_name=_('description'), blank=True, null=True)
order = models.PositiveIntegerField(
default=0,
verbose_name=_('Order'),
help_text=_('Order in the chain of transmission')
)
is_gap = models.BooleanField(
default=False,
verbose_name=_('Is Gap'),
help_text=_('Check this if this represents a gap in the transmission chain')
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at'))
class Meta:
@ -50,4 +76,4 @@ class HadisTransmitter(models.Model):
unique_together = ('hadis', 'transmitter', 'order')
def __str__(self):
return f'{self.hadis.number} - {self.transmitter.full_name} ({self.order})'
return f'{self.hadis.number} - {self.transmitter.full_name if self.transmitter else "Gap"} ({self.order})'

127
apps/hadis/serializers.py

@ -1,127 +0,0 @@
from rest_framework import serializers
from utils import get_thumbs
from apps.hadis.models import *
class HadisCategorySerializer(serializers.ModelSerializer):
children = serializers.SerializerMethodField('get_children')
name = serializers.SerializerMethodField()
hadis_count = serializers.SerializerMethodField()
source_type = serializers.CharField(read_only=True)
def get_children(self, obj):
return [self.to_dict(cat) for cat in obj.get_children()]
def to_dict(self, c):
children = c.get_children()
return {
'id': c.id,
'name': c.name,
'hadis_count': c.hadis_count,
'source_type': c.source_type,
'category_type': c.category_type,
'children': [] if not children else [self.to_dict(i) for i in children],
}
class Meta:
model = HadisCategory
fields = ['id', 'name', 'hadis_count', 'source_type','children']
class HadisTagSerializer(serializers.ModelSerializer):
class Meta:
model = HadisTag
fields = ('id', 'title')
class HadisSerializer(serializers.ModelSerializer):
class Meta:
model = Hadis
fields = ('number', 'title', 'text', 'translation',)
class ReferenceImageSerializer(serializers.ModelSerializer):
thumbnail = serializers.SerializerMethodField()
class Meta:
model = ReferenceImage
fields = ('id', 'thumbnail', 'priority')
def get_thumbnail(self, obj):
return get_thumbs(obj.thumbnail, self.context.get('request'))
class HadisReferenceSerializer(serializers.ModelSerializer):
images = serializers.SerializerMethodField()
class Meta:
model = HadisReference
fields = ('id', 'book', 'description', 'created_at', 'images')
def get_images(self, obj):
return ReferenceImageSerializer(
obj.referenceimage_set.all(),
many=True,
context=self.context
).data
class TransmittersSerializer(serializers.ModelSerializer):
thumbnail = serializers.SerializerMethodField()
class Meta:
model = Transmitters
fields = ('id', 'full_name', 'birth_year_hijri', 'death_year_hijri',
'description', 'status', 'status_color', 'thumbnail')
def get_thumbnail(self, obj):
return get_thumbs(obj.thumbnail, self.context.get('request'))
class HadisTransmitterSerializer(serializers.ModelSerializer):
transmitter = serializers.SerializerMethodField()
class Meta:
model = HadisTransmitter
fields = ('id', 'transmitter', 'description', 'order', 'created_at')
def get_transmitter(self, obj):
return TransmittersSerializer(
obj.transmitter,
context=self.context
).data
class HadisOverviewSerializer(serializers.ModelSerializer):
tags = serializers.SerializerMethodField()
class Meta:
model = HadisOverview
fields = ('status', 'status_color', 'status_text', 'address', 'links', 'tags', 'share_link', 'explanation', 'created_at')
def get_tags(self, obj):
return HadisTagSerializer(
obj.tags.all(),
many=True,
context=self.context
).data
class HadisDetailSerializer(serializers.ModelSerializer):
overview = HadisOverviewSerializer(source='hadisoverview', read_only=True)
reference = serializers.SerializerMethodField()
transmitters = HadisTransmitterSerializer(many=True, read_only=True)
class Meta:
model = Hadis
fields = ('id', 'number', 'title', 'text', 'translation', 'status',
'created_at', 'updated_at', 'overview', 'reference', 'transmitters')
def get_reference(self, obj):
reference = obj.references.first()
if reference:
return HadisReferenceSerializer(reference, context=self.context).data
return None

2
apps/hadis/serializers/__init__.py

@ -0,0 +1,2 @@
from .category import *
from .hadis import *

102
apps/hadis/serializers/category.py

@ -0,0 +1,102 @@
from rest_framework import serializers
from django.utils.translation import gettext_lazy as _
from ..models import HadisSect, HadisCategory, Hadis
class HadisCategorySectListSerializer(serializers.ModelSerializer):
"""Serializer for HadisSect list with grouped response"""
class Meta:
model = HadisSect
fields = ['id', 'title']
def to_representation(self, instance):
# This method will be overridden in the view to create the grouped response
return super().to_representation(instance)
class HadisCategoryTreeSerializer(serializers.ModelSerializer):
"""Serializer for HadisCategory tree structure"""
class Meta:
model = HadisCategory
fields = ['id', 'title', 'source_type']
def get_name(self, obj):
"""Get category name based on request language"""
request = self.context.get('request')
language_code = getattr(request, 'LANGUAGE_CODE', 'en')
return obj.get_translation(language_code) if hasattr(obj, 'get_translation') else obj.title
def get_children(self, obj):
"""Get active children categories that have children or hadis"""
children = obj.get_children().filter(sect=obj.sect).order_by('order')
# Filter children that have either children or hadis
filtered_children = []
for child in children:
has_children = child.get_children().filter(sect=obj.sect).exists()
has_hadis = Hadis.objects.filter(category=child, status=True).exists()
if has_children or has_hadis:
filtered_children.append(child)
return [self.to_dict(cat) for cat in filtered_children]
def get_hadis_count(self, obj):
"""Get total hadis count including children categories"""
# Get direct hadis count
direct_count = Hadis.objects.filter(category=obj, status=True).count()
# Get hadis count from all descendants
descendants = obj.get_descendants().filter(sect=obj.sect)
descendant_count = 0
for descendant in descendants:
descendant_count += Hadis.objects.filter(category=descendant, status=True).count()
return direct_count + descendant_count
def get_has_hadis(self, obj):
"""Check if category can have hadis (no active children) and has hadis"""
# Check if category has active children
has_active_children = obj.get_children().filter(sect=obj.sect).exists()
# If has active children, cannot have hadis
if has_active_children:
return False
# If no active children, check if has hadis
return Hadis.objects.filter(category=obj, status=True).exists()
def get_xmind_file(self, obj):
"""Get absolute URL for xmind file"""
if obj.xmind_file:
request = self.context.get('request')
if request:
return request.build_absolute_uri(obj.xmind_file.url)
return obj.xmind_file.url
return None
def get_has_xmind_file(self, obj):
"""Check if category has xmind file"""
return bool(obj.xmind_file)
def to_dict(self, c):
"""Convert category to dictionary"""
children = c.get_children().filter(sect=c.sect).order_by('order')
# Filter children that have either children or hadis
filtered_children = []
for child in children:
has_children = child.get_children().filter(sect=c.sect).exists()
has_hadis = Hadis.objects.filter(category=child, status=True).exists()
if has_children or has_hadis:
filtered_children.append(child)
return {
'id': c.id,
'name': self.get_name(c),
'hadis_count': self.get_hadis_count(c),
'has_hadis': self.get_has_hadis(c),
'order': c.order,
'xmind_file': self.get_xmind_file(c),
'has_xmind_file': self.get_has_xmind_file(c),
'children': [] if not filtered_children else [self.to_dict(i) for i in filtered_children],
}

144
apps/hadis/serializers/hadis.py

@ -0,0 +1,144 @@
from rest_framework import serializers
from django.utils.translation import gettext_lazy as _
from ..models import (
Hadis, HadisStatus, HadisTag, HadisTransmitter,
HadisReference, ReferenceImage, Transmitters
)
from apps.library.serializers import BookSerializer
class HadisListSerializer(serializers.ModelSerializer):
"""Serializer for Hadis list"""
category = serializers.SerializerMethodField()
translation = serializers.SerializerMethodField()
class Meta:
model = Hadis
fields = ['id', 'number', 'title', 'category', 'translation']
def get_category(self, obj):
"""Get category id and title"""
if obj.category:
return {
'id': obj.category.id,
'title': obj.category.title
}
return None
def get_translation(self, obj):
"""Get translation based on request language"""
request = self.context.get('request')
language_code = getattr(request, 'LANGUAGE_CODE', 'en')
return obj.get_translation(language_code)
class HadisStatusSerializer(serializers.ModelSerializer):
"""Serializer for HadisStatus"""
class Meta:
model = HadisStatus
fields = ['id', 'title', 'color']
class HadisTagSerializer(serializers.ModelSerializer):
"""Serializer for HadisTag"""
class Meta:
model = HadisTag
fields = ['id', 'title']
class TransmitterSerializer(serializers.ModelSerializer):
"""Serializer for Transmitters"""
class Meta:
model = Transmitters
fields = [
'id', 'full_name', 'birth_year_hijri', 'death_year_hijri',
'description'
]
class HadisTransmitterSerializer(serializers.ModelSerializer):
"""Serializer for HadisTransmitter with transmitter details"""
transmitter = TransmitterSerializer(read_only=True)
class Meta:
model = HadisTransmitter
fields = [
'id', 'transmitter', 'order', 'is_gap'
]
class ReferenceImageSerializer(serializers.ModelSerializer):
"""Serializer for ReferenceImage"""
thumbnail = serializers.SerializerMethodField()
class Meta:
model = ReferenceImage
fields = ['id', 'thumbnail', 'priority']
def get_thumbnail(self, obj):
"""Get thumbnail URL"""
if obj.image:
request = self.context.get('request')
if request:
return request.build_absolute_uri(obj.image.url)
return obj.image.url
return None
class HadisReferenceSerializer(serializers.ModelSerializer):
"""Serializer for HadisReference with book and images"""
book = BookSerializer(read_only=True)
images = ReferenceImageSerializer(many=True, read_only=True)
class Meta:
model = HadisReference
fields = [
'id', 'book', 'page_number', 'hadis_number_in_book',
'description', 'images'
]
class HadisDetailSerializer(serializers.ModelSerializer):
"""Detailed serializer for Hadis with all related objects"""
hadis_status = HadisStatusSerializer(read_only=True)
tags = HadisTagSerializer(many=True, read_only=True)
transmitters = HadisTransmitterSerializer(
source='hadistransmitter_set',
many=True,
read_only=True
)
references = HadisReferenceSerializer(
source='hadisreference_set',
many=True,
read_only=True
)
category = serializers.SerializerMethodField()
translation = serializers.SerializerMethodField()
class Meta:
model = Hadis
fields = [
'id', 'number', 'title', 'text', 'translation', 'explanation',
'address', 'hadis_status_text', 'links', 'status',
'category', 'hadis_status', 'tags', 'transmitters', 'references'
]
def get_category(self, obj):
"""Get category details"""
if obj.category:
return {
'id': obj.category.id,
'title': obj.category.title,
'category_type': obj.category.category_type
}
return None
def get_translation(self, obj):
"""Get translation based on request language"""
request = self.context.get('request')
language_code = getattr(request, 'LANGUAGE_CODE', 'en')
return obj.get_translation(language_code)

2343
apps/hadis/templates/admin/category_index.html
File diff suppressed because it is too large
View File

42
apps/hadis/templates/admin/hadiscategory/change_form.html

@ -1,42 +0,0 @@
{% extends 'admin/category_index.html' %}
{% load i18n admin_urls %}
{% block extrahead %}
{{ block.super }}
<style>
.bg-success {
background-color: #4CAF50 !important;
}
.bg-success:hover {
background-color: #45a049 !important;
}
</style>
{% endblock %}
{% block scripts %}
{{ block.super }}
<script>
$(document).ready(function() {
// Add the button after the page is loaded
setTimeout(function() {
// Find the history button in the header
var $historyBtn = $(".historylink.float-right.btn");
// Create the Add Category button with the same styling as the History button
var addUrl = "{{ request.path }}".replace(/\/\d+\/change\/$/, "/add/");
var $addCategoryBtn = $('<a href="' + addUrl + '" class="float-right btn bg-success legitRipple mr-3 ml-3">' +
'{% trans "Add Category" %} <i class="icon-plus-circle2"></i></a>');
// Insert the Add Category button before the History button
if ($historyBtn.length) {
$historyBtn.before($addCategoryBtn);
} else {
// If history button not found, add to the btns-head div
$(".btns-head .dtr-inline").append($addCategoryBtn);
}
console.log("Add Category button added");
}, 1000); // Increased timeout to ensure DOM is fully loaded
});
</script>
{% endblock %}

153
apps/hadis/templates/admin/hadisowerview_change_form.html

@ -1,153 +0,0 @@
{% extends "admin/change_form.html" %}
{% load i18n %}
{% load static %}
{% block submit_buttons_bottom %}
{{ block.super }}
<button type="submit" class="btn-block mt-3 btn bg-indigo-600 legitRipple" name="_save_and_next">
{% translate 'Save And Edit Next Hadis' %}
<i class="mi-border-color ml-2"></i>
</button>
<button type="submit" class="btn-block mt-3 btn bg-indigo-600 legitRipple" name="_save_and_prev">
{% translate 'Save And Edit Previus Hadis' %}
<i class="mi-border-color ml-2"></i>
</button>
<button type="submit" class="btn-block mt-3 btn bg-indigo-600 legitRipple" name="_save_and_random">
{% translate 'Save And Edit Random' %}
<i class="mi-border-color ml-2"></i>
</button>
{% endblock %}
{% block scripts %}
{{ block.super }}
<style>
.color-picker-container {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin: 15px 0;
justify-content: flex-start;
max-width: 600px;
}
.color-option {
flex: 0 0 auto;
}
.color-radio-label {
cursor: pointer;
margin: 0;
padding: 0;
}
.color-option-container {
display: flex;
flex-direction: column;
align-items: center;
transition: all 0.2s ease;
padding: 8px;
border-radius: 8px;
}
.color-option-container:hover {
background-color: rgba(0, 0, 0, 0.05);
transform: translateY(-2px);
}
.color-preview {
display: block;
width: 36px;
height: 36px;
border-radius: 50%;
border: 2px solid #e0e0e0;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
transition: all 0.2s ease;
position: relative;
}
.color-name {
display: block;
font-size: 12px;
margin-top: 6px;
color: #555;
font-weight: 500;
transition: all 0.2s ease;
}
input[type="radio"] {
display: none;
}
input[type="radio"]:checked + .color-option-container {
background-color: rgba(0, 0, 0, 0.05);
}
input[type="radio"]:checked + .color-option-container .color-preview {
border: 2px solid #333;
box-shadow: 0 0 0 2px rgba(0,0,0,0.2);
transform: scale(1.1);
}
input[type="radio"]:checked + .color-option-container .color-preview:after {
content: '✓';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
text-shadow: 0 0 2px rgba(0,0,0,0.7);
font-size: 18px;
font-weight: bold;
}
input[type="radio"]:checked + .color-option-container .color-name {
color: #000;
font-weight: 600;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Get all color radio options
const colorOptions = document.querySelectorAll('.color-option input[type="radio"]');
// Check if there's a pre-selected value
const statusColorField = document.querySelector('input[name$="-status_color"]');
const preSelectedValue = statusColorField ? statusColorField.value : null;
colorOptions.forEach(function(radio) {
// Add click event to the color container
const colorContainer = radio.nextElementSibling;
colorContainer.addEventListener('click', function() {
radio.checked = true;
// Trigger change event to update any listeners
const event = new Event('change', { bubbles: true });
radio.dispatchEvent(event);
});
// If this radio's value matches the pre-selected value, check it
if (preSelectedValue && radio.value === preSelectedValue) {
radio.checked = true;
// Scroll to make the selected color visible
setTimeout(() => {
radio.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}, 300);
}
});
// If no color is selected but there's a value in the hidden field, try to match it
if (preSelectedValue && !document.querySelector('.color-option input[type="radio"]:checked')) {
// Try to find a color that matches (case insensitive)
const matchingRadio = Array.from(colorOptions).find(radio =>
radio.value.toLowerCase() === preSelectedValue.toLowerCase()
);
if (matchingRadio) {
matchingRadio.checked = true;
}
}
});
</script>
{% endblock %}

7
apps/hadis/templates/admin/widgets/color_radio.html

@ -1,7 +0,0 @@
{% for group, options, index in widget.optgroups %}
{% for option in options %}
<div class="color-option" style="display: inline-block; margin: 5px;">
{% include option.template_name with widget=option %}
</div>
{% endfor %}
{% endfor %}

9
apps/hadis/templates/admin/widgets/color_radio_option.html

@ -1,9 +0,0 @@
{% if widget.wrap_label %}
<label{% if widget.attrs.id %} for="{{ widget.attrs.id }}"{% endif %}>
{% endif %}
<input type="{{ widget.type }}" name="{{ widget.name }}"{% if widget.value != None %} value="{{ widget.value }}"{% endif %}{% include "django/forms/widgets/attrs.html" %}>
<span class="color-preview" style="display: inline-block; width: 30px; height: 30px; background-color: {{ widget.value }}; border: 1px solid #ccc; border-radius: 5px;"></span>
<span class="color-name">{{ widget.label }}</span>
{% if widget.wrap_label %}
</label>
{% endif %}

16
apps/hadis/urls.py

@ -1,12 +1,16 @@
from django.urls import path, include
from . import views
from django.urls import path
from .views.category import HadisSectListView, HadisCategoryTreeView
from .views.hadis import HadisListView, HadisDetailView
urlpatterns = [
path('categories/', views.CategoryListView.as_view(), name='category-list'),
path('categories/<int:pk>/hadis/', views.CategoryHadisListView.as_view(), name='category-hadis-list'),
path('<int:pk>/', views.HadisDetailView.as_view(), name='hadis-detail'),
# Hadis Sect endpoints
path('categories/', HadisSectListView.as_view(), name='hadis-sect-list'),
# Hadis Category endpoints
path('categories/<int:sect_id>/', HadisCategoryTreeView.as_view(), name='hadis-category-tree'),
# Hadis endpoints
path('<int:category_id>/hadis/', HadisListView.as_view(), name='hadis-list'),
path('hadis/<int:hadis_id>/', HadisDetailView.as_view(), name='hadis-detail'),
]

370
apps/hadis/views/category.py

@ -1,301 +1,119 @@
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from django.db.models import Subquery, Count, F, OuterRef, Q, Prefetch, Case, When, Value, IntegerField
from rest_framework.pagination import PageNumberPagination
from rest_framework.generics import ListAPIView
from django.core.cache import cache
from django.conf import settings
import hashlib
import json
from apps.hadis.models import *
from apps.hadis.serializers import *
from apps.hadis.doc import category_list_swagger, category_hadis_list_swagger
class CategoryPagination(PageNumberPagination):
page_size = 10
page_size_query_param = 'page_size'
max_page_size = 100
class CategoryListView(ListAPIView):
serializer_class = HadisCategorySerializer
permission_classes = (IsAuthenticated,)
pagination_class = CategoryPagination
# Cache timeout in seconds (1 hour)
CACHE_TIMEOUT = 60 * 60
def get_cache_key(self, source_type=None):
"""
Generate a unique cache key based on the view name and filter parameters.
Args:
source_type: Optional source_type filter parameter
Returns:
A unique cache key string
"""
# Base key with the view name
key_parts = ['category_tree']
# Add filter parameters to make the key specific
if source_type:
key_parts.append(f'source_type:{source_type}')
from rest_framework.response import Response
from django.shortcuts import get_object_or_404
from utils.pagination import NoPagination
# Join all parts with a separator
key = ':'.join(key_parts)
from ..models import HadisSect, HadisCategory
from ..serializers import HadisCategorySectListSerializer, HadisCategoryTreeSerializer
from ..docs import hadis_sect_list_swagger, hadis_category_tree_swagger
return key
@classmethod
def invalidate_cache(cls, source_type=None):
class HadisSectListView(ListAPIView):
"""
Invalidate the category tree cache.
Args:
source_type: Optional source_type to invalidate specific cache.
If None, invalidates all category tree caches.
API view to list all HadisSects grouped by sect_type (shia/sunni)
"""
if source_type:
# Invalidate specific tree cache
tree_cache_key = cls().get_cache_key(source_type)
cache.delete(tree_cache_key)
queryset = HadisSect.objects.filter(is_active=True).order_by('order')
serializer_class = HadisCategorySectListSerializer
pagination_class = NoPagination
# Invalidate all paginated caches for this source_type
paginated_pattern = f'category_tree_paginated:source_type:{source_type}*'
paginated_keys = cache.keys(paginated_pattern)
if paginated_keys:
cache.delete_many(paginated_keys)
else:
# Invalidate all category tree caches (both full trees and paginated results)
# This uses cache key pattern matching if supported by the cache backend
# For Redis, we can use wildcards
all_cache_keys = cache.keys('category_tree*')
if all_cache_keys:
cache.delete_many(all_cache_keys)
else:
# Fallback: delete specific known keys
for st in [HadisCategory.SourceType.SHIA, HadisCategory.SourceType.SUNNI]:
# Delete tree cache
tree_cache_key = cls().get_cache_key(st)
cache.delete(tree_cache_key)
# Try to delete paginated caches
try:
paginated_pattern = f'category_tree_paginated:source_type:{st}*'
paginated_keys = cache.keys(paginated_pattern)
if paginated_keys:
cache.delete_many(paginated_keys)
except:
pass
# Also delete the default keys (no source_type)
cache.delete(cls().get_cache_key())
try:
default_paginated_keys = cache.keys('category_tree_paginated:page:*')
if default_paginated_keys:
cache.delete_many(default_paginated_keys)
except:
pass
def get_children(self, obj):
return [self.to_dict(cat) for cat in obj.get_children()]
def to_dict(self, c):
"""
Convert a category to a dictionary with proper tree structure based on level.
Args:
c: The HadisCategory instance
@hadis_sect_list_swagger
def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)
Returns:
Dictionary representation of the category with proper tree structure
"""
# Get the level of this category
level = c.level_p
def list(self, request, *args, **kwargs):
queryset = self.get_queryset()
response = super().list(request, *args, **kwargs)
lang = request.LANGUAGE_CODE
# Determine source_type and category_type based on level
source_type = None
category_type = None
# Group sects by type
grouped_data = {
'shia': [],
'sunni': []
}
if level == 1:
# Level 1 (Root) - Has its own source_type
source_type = c.source_type
category_type = None
elif level == 2:
# Level 2 (Child) - Inherits source_type from parent, has own category_type
if c.parent:
source_type = c.parent.source_type
else:
source_type = c.source_type
category_type = c.category_type
elif level == 3:
# Level 3 (Grandchild) - Inherits source_type from grandparent, category_type from parent
if c.parent and c.parent.parent:
source_type = c.parent.parent.source_type
category_type = c.parent.category_type
else:
source_type = c.source_type
category_type = c.category_type
for sect in queryset:
sect_data = {
'id': sect.id,
'title': sect.title,
'seo_field': None
}
# Get direct children - use getattr to handle both model instances and cached trees
if hasattr(c, 'get_children'):
# For model instances
children = c.get_children()
else:
# For cached trees
children = getattr(c, 'children', [])
if sect.sect_type == HadisSect.SectType.SHIA:
grouped_data['shia'].append(sect_data)
elif sect.sect_type == HadisSect.SectType.SUNNI:
grouped_data['sunni'].append(sect_data)
# Create the dictionary representation
return {
'id': c.id,
'name': c.name,
'hadis_count': getattr(c, 'hadis_count', 0),
'source_type': source_type,
'category_type': category_type,
'children': [] if not children else [self.to_dict(child) for child in children],
# Create response with count and results
response_data = {
'count': queryset.count(),
'results': grouped_data
}
def get_pagination_cache_key(self, source_type=None, page=1, page_size=None):
"""
Generate a cache key for paginated results.
return Response(response_data)
Args:
source_type: Optional source_type filter
page: Page number
page_size: Number of items per page
Returns:
A unique cache key for the paginated results
class HadisCategoryTreeView(ListAPIView):
"""
# Base key with the view name
key_parts = ['category_tree_paginated']
# Add filter parameters
if source_type:
key_parts.append(f'source_type:{source_type}')
# Add pagination parameters
key_parts.append(f'page:{page}')
if page_size:
key_parts.append(f'page_size:{page_size}')
else:
key_parts.append(f'page_size:{self.pagination_class.page_size}')
# Join all parts with a separator
key = ':'.join(key_parts)
return key
API view to get HadisCategory tree structure by sect_id
Returns categories grouped by source_type (quran/hadith)
"""
serializer_class = HadisCategoryTreeSerializer
pagination_class = NoPagination
@category_list_swagger
@hadis_category_tree_swagger
def get(self, request, *args, **kwargs):
from mptt.templatetags.mptt_tags import cache_tree_children
# Get source_type filter from query params
source_type = request.query_params.get('source_type', None)
# Get pagination parameters
page = request.query_params.get('page', 1)
page_size = request.query_params.get('page_size', self.pagination_class.page_size)
# Try to get paginated response from cache first
pagination_cache_key = self.get_pagination_cache_key(source_type, page, page_size)
cached_response = cache.get(pagination_cache_key)
if cached_response:
return Response(cached_response)
# Generate a unique cache key for the full tree
tree_cache_key = self.get_cache_key(source_type)
# Try to get the tree from cache first
tree = cache.get(tree_cache_key)
# If not in cache, build the tree
if tree is None:
# Build filter query
filter_query = Q(is_active=True)
if source_type and source_type in [HadisCategory.SourceType.SHIA, HadisCategory.SourceType.SUNNI]:
filter_query &= Q(source_type=source_type)
# Get ALL categories with hadis count - this is important to include all levels
queryset = HadisCategory.objects.filter(filter_query).select_related(
'parent', 'parent__parent' # Prefetch parent relationships for efficient access
).annotate(
hadis_count=Count('hadis'),
)
# Use cache_tree_children to build the full tree structure
# This will properly set up the parent-child relationships for the entire tree
all_categories = cache_tree_children(queryset)
# Filter to get only level 1 (root) categories as the starting point for our tree
root_categories = [category for category in all_categories if category.parent is None]
# Build the tree
tree = []
for c in root_categories:
# Convert to dictionary with proper tree structure based on level
tdata = self.to_dict(c)
# Calculate total hadis_count including all children recursively
def calculate_total_hadis_count(node):
total = node['hadis_count']
for child in node['children']:
total += calculate_total_hadis_count(child)
return total
# Update the hadis_count to include all children
tdata['hadis_count'] = calculate_total_hadis_count(tdata)
# Add to the result tree
tree.append(tdata)
# Store the tree in cache
cache.set(tree_cache_key, tree, self.CACHE_TIMEOUT)
# Apply pagination only to the root categories (level 1)
page_obj = self.paginate_queryset(tree)
if page_obj is not None:
# Get paginated response
response = self.get_paginated_response(page_obj)
# Cache the paginated response
cache.set(pagination_cache_key, response.data, self.CACHE_TIMEOUT)
return response
# If pagination is not applied, return the full tree
return Response(tree)
return self.list(request, *args, **kwargs)
def get_queryset(self):
"""
Get the base queryset for the serializer.
This is used by DRF's default list() method if we don't override get().
Note: This method is not used directly in our implementation since we override get(),
but it's kept for completeness and API compatibility.
"""
source_type = self.request.query_params.get('source_type', None)
# Build filter query
filter_query = Q(is_active=True)
if source_type and source_type in [HadisCategory.SourceType.SHIA, HadisCategory.SourceType.SUNNI]:
filter_query &= Q(source_type=source_type)
sect_id = self.kwargs.get('sect_id')
sect = get_object_or_404(HadisSect, id=sect_id, is_active=True)
# Get root categories (no parent) for this sect
return HadisCategory.objects.filter(
sect=sect,
parent__isnull=True
).order_by('order')
def list(self, request, *args, **kwargs):
queryset = self.get_queryset()
# Group categories by source_type
grouped_data = {
'quran': [],
'hadith': []
}
# Get ALL categories with proper prefetching for efficiency
queryset = HadisCategory.objects.filter(filter_query).select_related(
'parent', 'parent__parent'
).prefetch_related(
'children', 'children__children' # Prefetch two levels of children
).annotate(
hadis_count=Count('hadis'),
# Create serializer instance for to_dict method
serializer_instance = HadisCategoryTreeSerializer(context={'request': request})
for category in queryset:
category_data = serializer_instance.to_dict(category)
if category.source_type == HadisCategory.SourceType.QURAN:
grouped_data['quran'].append(category_data)
elif category.source_type == HadisCategory.SourceType.HADITH:
grouped_data['hadith'].append(category_data)
# Calculate total count including all descendants recursively
def count_objects_recursive(data_list):
"""Count all objects including children recursively"""
count = 0
for item in data_list:
count += 1 # Count current item
if 'children' in item and item['children']:
count += count_objects_recursive(item['children']) # Count children recursively
return count
# Calculate total count from grouped data
total_count = (
count_objects_recursive(grouped_data['quran']) +
count_objects_recursive(grouped_data['hadith'])
)
# Filter to only return root categories (level 1)
queryset = queryset.filter(parent=None)
# Create response with count and results
response_data = {
'count': total_count,
'results': grouped_data
}
return queryset
return Response(response_data)

85
apps/hadis/views/hadis.py

@ -1,75 +1,50 @@
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from django.db.models import Subquery, Count, F, OuterRef, Q, Prefetch
from rest_framework.generics import ListAPIView, RetrieveAPIView
from django.shortcuts import get_object_or_404
from ..models import HadisCategory, Hadis
from ..serializers import HadisListSerializer, HadisDetailSerializer
from ..docs import hadis_list_swagger, hadis_detail_swagger
from apps.hadis.models import *
from apps.hadis.serializers import *
from apps.hadis.doc import category_list_swagger, category_hadis_list_swagger, hadis_detail_swagger
class HadisListView(ListAPIView):
"""
API view to list Hadis by category_id
"""
serializer_class = HadisListSerializer
class CategoryHadisListView(ListAPIView):
serializer_class = HadisSerializer
permission_classes = (IsAuthenticated,)
@category_hadis_list_swagger
@hadis_list_swagger
def get(self, request, *args, **kwargs):
return super().get(request, *args, **kwargs)
return self.list(request, *args, **kwargs)
def get_queryset(self):
categories = HadisCategory.objects.filter(id=self.kwargs['pk']).order_by('-order')
category_id = self.kwargs.get('category_id')
if not HadisCategory.objects.filter(id=category_id).exists():
return Hadis.objects.none()
return Hadis.objects.filter(
Q(category__in=categories),
status=True,
).prefetch_related(
'category',
)
category_id=category_id,
status=True
).order_by('number')
class HadisDetailView(RetrieveAPIView):
"""
API endpoint to retrieve detailed information about a specific hadis.
Returns:
- Hadis details (number, title, text, translation)
- HadisOverview information (status, tags, etc.)
- First HadisReference with its ReferenceImages
- List of Transmitters
API view to retrieve detailed Hadis information by hadis_id
"""
serializer_class = HadisDetailSerializer
permission_classes = (IsAuthenticated,)
lookup_field = 'id'
lookup_url_kwarg = 'hadis_id'
@hadis_detail_swagger
def get(self, request, *args, **kwargs):
return super().get(request, *args, **kwargs)
return self.retrieve(request, *args, **kwargs)
def get_object(self):
hadis_id = self.kwargs.get('pk')
queryset = Hadis.objects.filter(id=hadis_id)
# Prefetch related data to optimize queries
queryset = queryset.prefetch_related(
'hadisoverview',
'hadisoverview__tags',
Prefetch(
'references',
queryset=HadisReference.objects.prefetch_related(
'referenceimage_set',
'book'
)
),
Prefetch(
'transmitters',
queryset=HadisTransmitter.objects.select_related('transmitter').order_by('order')
)
def get_queryset(self):
return Hadis.objects.filter(status=True).select_related(
'category', 'hadis_status'
).prefetch_related(
'tags',
'hadistransmitter_set__transmitter',
'hadisreference_set__book',
'hadisreference_set__images'
)
return get_object_or_404(queryset, id=hadis_id)
def get_serializer_context(self):
context = super().get_serializer_context()
context.update({'request': self.request})
return context

63
config/enhanced_auth_middleware.py

@ -0,0 +1,63 @@
from rest_framework.authtoken.models import Token
from django.contrib.auth import get_user_model
from django.shortcuts import redirect
from django.urls import reverse
from django.contrib import messages
User = get_user_model()
def enhanced_auth_middleware(get_response):
"""
Enhanced middleware for API authentication with admin restriction
Handles custom documentation system authentication
"""
def middleware(request):
# Define protected paths that require staff access
protected_paths = ["/swagger", "/redoc", "/docs"]
is_protected_path = any(path in request.path for path in protected_paths)
if is_protected_path:
# Check if user is authenticated and is staff
if request.user.is_authenticated and request.user.is_staff:
# Handle swagger token authentication from session
if 'swagger_token' in request.session:
token = request.session['swagger_token']
# Validate the token still exists and is valid
try:
token_obj = Token.objects.get(key=token)
if token_obj.user.is_active:
request.META['HTTP_AUTHORIZATION'] = f"Token {token}"
else:
# Token user is inactive, clear session
del request.session['swagger_token']
if 'swagger_user_info' in request.session:
del request.session['swagger_user_info']
except Token.DoesNotExist:
# Token doesn't exist, clear session
del request.session['swagger_token']
if 'swagger_user_info' in request.session:
del request.session['swagger_user_info']
# If no swagger token in session, provide default admin token for basic access
elif not request.META.get('HTTP_AUTHORIZATION'):
# Create or get token for the current admin user
token, _ = Token.objects.get_or_create(user=request.user)
request.META['HTTP_AUTHORIZATION'] = f"Token {token.key}"
else:
# User is not authenticated or not staff
# For swagger-auth paths, allow access (they handle their own auth)
if '/swagger-auth/' not in request.path:
# Redirect to admin login for other protected paths
messages.warning(request, 'You must be logged in as a staff member to access API documentation.')
return redirect(f"{reverse('admin:login')}?next={request.path}")
# For non-protected API paths, handle normal authentication
elif "/admin/" not in request.path and request.META.get('HTTP_AUTHORIZATION') is None:
if request.user.is_authenticated and request.user.is_staff:
token, _ = Token.objects.get_or_create(user=request.user)
request.META['HTTP_AUTHORIZATION'] = f"Token {token.key}"
return get_response(request)
return middleware

85
config/settings/base.py

@ -125,7 +125,7 @@ MIDDLEWARE = [
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'config.language_code_middleware.language_middleware',
'config.test_auth_middleware.test_auth_middleware',
'config.enhanced_auth_middleware.enhanced_auth_middleware',
]
ROOT_URLCONF = 'config.urls'
@ -270,18 +270,9 @@ FILER_ENABLE_LOGGING = True
FILER_DEBUG = True
ADMIN_TITLE = 'Imam Javad App'
ADMIN_INDEX_TITLE = 'Imam Javad Administration'
SITE_DOMAIN = "https://imamjavad.nwhco.ir"
# Dictionary with phone number ranges and corresponding countries
# If a country is in this dictionary, it indicates that the project's OTP service supports that country
SERVICE_OTP_COUNTRU_API_KEY = {
"Iran": "https://console.melipayamak.com/api/send/simple/33213d78f1234e99b81f94eefda77e45"
}
SERVICE_OTP_COUNTRY_PHONE_RANGE = {
"98": "Iran",
"+98": "Iran"
}
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.0/howto/static-files/
@ -290,19 +281,12 @@ SERVICE_OTP_COUNTRY_PHONE_RANGE = {
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
DEFAULT_SHOW_CITY_GUIDE_CITY = 'mashhad'
FILE_UPLOAD_HANDLERS = [
'django.core.files.uploadhandler.TemporaryFileUploadHandler',
]
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.gmail.com'
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_HOST_USER = 'aliabdolahi.171@gmail.com'
EMAIL_HOST_PASSWORD = 'rkxb nnhx iave fxxt'
######################################################################
# Sessions
######################################################################
@ -768,6 +752,53 @@ UNFOLD = {
# },
]
},
{
"title": _("Hadis"),
"collapsible": True,
"separator": True,
"items": [
{
"title": _("Hadis Sects"),
"icon": "account_tree",
"link": reverse_lazy("admin:hadis_hadissect_changelist"),
},
{
"title": _("Hadis Categories"),
"icon": "category",
"link": reverse_lazy("admin:hadis_hadiscategory_changelist"),
},
{
"title": _("Hadis"),
"icon": "format_quote",
"link": reverse_lazy("admin:hadis_hadis_changelist"),
},
{
"title": _("Hadis References"),
"icon": "link",
"link": reverse_lazy("admin:hadis_hadisreference_changelist"),
},
{
"title": _("Hadis Tags"),
"icon": "label",
"link": reverse_lazy("admin:hadis_hadistag_changelist"),
},
{
"title": _("Hadis Status"),
"icon": "flag",
"link": reverse_lazy("admin:hadis_hadisstatus_changelist"),
},
{
"title": _("Transmitters"),
"icon": "person",
"link": reverse_lazy("admin:hadis_transmitters_changelist"),
},
{
"title": _("Hadis Transmitters"),
"icon": "group",
"link": reverse_lazy("admin:hadis_hadistransmitter_changelist"),
},
]
},
{
"title": "",
"items": [
@ -785,24 +816,6 @@ UNFOLD = {
# "SCRIPTS": [
# lambda request: static("js/scripts.js"),
# ],
# {
# "title": _("Hadis"),
# "collapsible": True,
# "separator": True,
# "items": [
# {
# "title": _("Hadis Categories"),
# "icon": "category",
# "link": reverse_lazy("admin:hadis_hadiscategory_changelist"),
# },
# # {
# # "title": _("Hadis"),
# # "icon": "format_quote",
# # "link": reverse_lazy("admin:hadis_hadis_changelist"),
# # },
# ]
# },
],
},
}

38
config/urls.py

@ -19,6 +19,7 @@ from django.urls import path, include, re_path
from django.conf import settings
from django.conf.urls.static import static
from django.conf.urls.i18n import i18n_patterns
from django.contrib.admin.views.decorators import staff_member_required
from utils import UploadTmpMedia
from django.http import JsonResponse
from django.shortcuts import render
@ -35,19 +36,24 @@ from drf_yasg import openapi
from rest_framework import permissions
import requests
from filer import views
# Import custom API views
from apps.api.views import CustomAPIDocumentationView, CustomSwaggerView, SwaggerTokenAuthView, clear_swagger_auth
# Restricted schema view for admin users only
schema_view = get_schema_view(
openapi.Info(
title="Snippets API",
title="Imam Javad API",
default_version='v1',
description="Project API Documentation",
terms_of_service="https://www.google.com/policies/terms/",
contact=openapi.Contact(email="nwhco.com"),
license=openapi.License(name="BSD License"),
description="Comprehensive API documentation for the Imam Javad educational platform",
contact=openapi.Contact(email="contact@imamjavad.com"),
license=openapi.License(name="MIT License"),
),
public=True,
permission_classes=(permissions.AllowAny,),
public=False,
permission_classes=(permissions.IsAdminUser,),
)
def oneapi_translate(request):
dist_lang = request.GET.get('dist_lang')
q = request.GET.get('q')
@ -94,11 +100,23 @@ urlpatterns = [
path('admin/filer/', include('filer.urls')),
]
# Protected swagger URL patterns
swagger_urlpatterns = [
path('swagger-auth/', SwaggerTokenAuthView.as_view(), name='swagger-token-auth'),
path('swagger-auth/clear/', clear_swagger_auth, name='clear-swagger-auth'),
re_path(r'^swagger(?P<format>\.json|\.yaml)$',
staff_member_required(schema_view.without_ui(cache_timeout=0)),
name='schema-json'),
path('swagger/', CustomSwaggerView.as_view(), name='schema-swagger-ui'),
re_path(r'^redoc/$',
staff_member_required(schema_view.with_ui('redoc', cache_timeout=0)),
name='schema-redoc'),
]
urlpatterns+= i18n_patterns(
path("admin/", project_admin_site.urls),
re_path(r'^swagger(?P<format>\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'),
re_path(r'^swagger/$', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
re_path(r'^redoc/$', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'),
path('docs/', CustomAPIDocumentationView.as_view(), name='docs-index'),
*swagger_urlpatterns,
path('admin/filer/', include('filer.urls')),
)

252
docs/API_Documentation_System_README.md

@ -0,0 +1,252 @@
# Imam Javad API Documentation System
## Overview
This project implements a comprehensive custom API documentation system that replaces the default Swagger UI with a beautiful, secure, and user-friendly interface. The system is designed specifically for the Imam Javad educational platform and includes advanced authentication, responsive design, and professional styling.
## Features
### 🔐 Security & Access Control
- **Admin-only access**: All documentation endpoints require staff member authentication
- **Token-based authentication**: Secure API token management for testing endpoints
- **Session management**: Persistent authentication state across documentation systems
- **Automatic redirects**: Unauthorized users are redirected to admin login
### 🎨 Custom Documentation Interface
- **Responsive sidebar navigation**: Collapsible app sections with smooth animations
- **Interactive endpoint explorer**: Click to navigate and highlight specific endpoints
- **Beautiful JSON viewer**: Syntax-highlighted response examples with Prism.js
- **Mobile-friendly design**: Optimized for all screen sizes
- **Professional styling**: Modern gradient backgrounds and smooth transitions
### 🔧 Enhanced Swagger UI
- **Fixed authentication banner**: Always-visible user info and token management
- **Custom branding**: Imam Javad themed colors and styling
- **Token injection**: Automatic authorization header injection for API testing
- **Integrated navigation**: Seamless links between documentation systems
## System Architecture
### File Structure
```
apps/api/views/
├── __init__.py
├── documentation.py # Custom documentation view
├── swagger_views.py # Enhanced Swagger views with auth
└── api_views.py # Original API views
templates/
├── api/
│ └── documentation.html # Main documentation template
└── swagger/
├── ui.html # Custom Swagger UI template
└── auth.html # Token authentication template
config/
├── urls.py # Updated URL configuration
└── enhanced_auth_middleware.py # Custom authentication middleware
```
### URL Endpoints
#### Documentation System
- `/en/docs/` - Main API documentation interface
- `/en/swagger/` - Enhanced Swagger UI with authentication
- `/en/swagger-auth/` - Token authentication management
- `/en/swagger-auth/clear/` - Clear authentication session
- `/en/redoc/` - Protected ReDoc interface
#### API Structure
The documentation covers all major app endpoints:
- **Account Management** (`/api/account/`) - User auth, registration, profiles
- **Course System** (`/api/courses/`) - Educational courses and lessons
- **Hadis Collection** (`/api/hadis/`) - Islamic hadis texts and categories
- **Digital Library** (`/api/library/`) - Books and downloadable resources
- **Video Content** (`/api/videos/`) - Educational video content
- **Podcast Platform** (`/api/podcast/`) - Audio content and episodes
- **Quiz System** (`/api/quiz/`) - Interactive assessments
- **Bookmarks & Ratings** (`/api/bookmarks/`) - User content management
## Setup Instructions
### 1. Authentication Setup
The system automatically creates middleware that handles authentication for protected paths. Admin users get automatic token generation for API access.
### 2. Admin User Creation
```python
# Create admin user (already done in implementation)
from apps.account.models import User
from rest_framework.authtoken.models import Token
admin_user = User.objects.create(
email='admin@imamjavad.com',
fullname='Admin User',
is_staff=True,
is_superuser=True,
user_type=User.UserType.SUPER_ADMIN
)
admin_user.set_password('admin123')
admin_user.save()
# Get admin token for API testing
token, _ = Token.objects.get_or_create(user=admin_user)
print(f"Admin token: {token.key}")
```
### 3. Accessing the Documentation
1. **Login to Admin Panel**: Visit `/en/admin/` and login with admin credentials
2. **Access Documentation**: Navigate to `/en/docs/` for the main documentation
3. **Use Swagger UI**: Visit `/en/swagger/` for interactive API testing
4. **Manage Tokens**: Use `/en/swagger-auth/` for token authentication
## Usage Guide
### Main Documentation Interface
1. **Sidebar Navigation**:
- Click on app names to expand/collapse endpoint lists
- Click on specific endpoints to scroll to their documentation
- Mobile users can toggle sidebar with the hamburger menu
2. **Endpoint Documentation**:
- Each endpoint shows HTTP method, URL, and description
- Parameters table with types and requirements
- Interactive response examples with syntax highlighting
- Tabbed interface for different response types
3. **Action Buttons**:
- "Swagger UI" button links to interactive testing interface
- "ReDoc" button provides alternative documentation view
### Swagger UI Interface
1. **Authentication Banner**:
- Shows current user information and authentication status
- Provides quick access to token management
- Links to main documentation
2. **Token Management**:
- Enter 40-character API tokens for testing
- Automatic token injection into API requests
- Session persistence across page reloads
3. **API Testing**:
- All endpoints automatically include authentication headers
- Interactive request/response testing
- Real-time API exploration
## Customization
### Adding New Endpoints
Update the `_get_api_structure()` method in `apps/api/views/documentation.py`:
```python
def _get_api_structure(self):
return {
'new_app': {
'name': 'New App Name',
'description': 'App description',
'endpoints': [
{
'name': 'Endpoint Name',
'method': 'GET',
'url': '/api/new-app/endpoint/',
'description': 'Endpoint description',
'parameters': [...],
'response_examples': {...}
}
]
}
}
```
### Styling Customization
Modify CSS variables in `templates/api/documentation.html`:
```css
:root {
--primary-color: #2c3e50;
--secondary-color: #3498db;
--success-color: #27ae60;
/* Add your custom colors */
}
```
### Branding Updates
- Update project name in templates and views
- Modify color schemes and gradients
- Add custom logos and icons
- Update contact information and licensing
## Security Considerations
### Access Control
- All documentation endpoints require `@staff_member_required` decorator
- Middleware automatically handles authentication for protected paths
- Session-based token management with validation
- Automatic cleanup of invalid tokens
### Token Security
- 40-character Django REST framework tokens
- Session storage with server-side validation
- Automatic token refresh and cleanup
- User activity tracking and session management
## Troubleshooting
### Common Issues
1. **403 Forbidden on Documentation Pages**
- Ensure user has `is_staff=True`
- Check middleware configuration
- Verify admin login session
2. **Token Authentication Not Working**
- Verify token is exactly 40 characters
- Check token exists in database
- Ensure user account is active
3. **Responsive Design Issues**
- Clear browser cache
- Check viewport meta tag
- Test on different screen sizes
### Debug Mode
Enable Django debug mode to see detailed error messages:
```python
DEBUG = True # in settings
```
## Performance Optimization
### Caching
- Static assets are cached with appropriate headers
- JSON responses use browser caching
- Template fragments can be cached for better performance
### Mobile Optimization
- Responsive images and media queries
- Touch-friendly interface elements
- Optimized loading for mobile networks
## Contributing
When adding new features or endpoints:
1. Update the API structure in `documentation.py`
2. Add appropriate response examples
3. Test on multiple screen sizes
4. Ensure security requirements are met
5. Update this documentation
## License
This documentation system is part of the Imam Javad educational platform and follows the project's MIT License.
---
**Admin Credentials for Testing:**
- Email: `admin@imamjavad.com`
- Password: `admin123`
- API Token: `632a324083da7c224361fc61eb5882633c1c575b`

1433
docs/Custom_Swagger_API_Documentation_Implementation_Guide.md
File diff suppressed because it is too large
View File

169
scripts/README.md

@ -0,0 +1,169 @@
# Скрипты для заполнения данными приложения Hadis
Этот каталог содержит скрипты для создания и управления тестовыми данными для приложения Hadis.
## Файлы
### Основные скрипты
- **`seed_hadis_data.py`** - Основной скрипт для создания тестовых данных
- **`clear_hadis_data.py`** - Скрипт для очистки созданных данных
- **`README.md`** - Этот файл с документацией
### Ресурсы
- **`seed_images/`** - Каталог с изображениями для обложек книг
- `book1.png`, `book2.png`, `book3.png`, `book4.png`
- **`test.xmind`** - Файл XMind для категорий хадисов
## Использование
### Создание тестовых данных
```bash
# Создать данные без очистки существующих
python scripts/seed_hadis_data.py
# Создать данные с очисткой существующих (ОСТОРОЖНО!)
python scripts/seed_hadis_data.py --clear
# Создать данные без очистки (явно)
python scripts/seed_hadis_data.py --no-clear
```
### Очистка данных
```bash
# Очистить все данные хадисов и связанные данные библиотеки
python scripts/clear_hadis_data.py
# Очистить только данные хадисов, оставить библиотеку
python scripts/clear_hadis_data.py --hadis-only
# Принудительная очистка без подтверждения
python scripts/clear_hadis_data.py --force
```
## Создаваемые данные
### Модели Hadis
1. **HadisSect** (Секты)
- Шииты-двунадесятники
- Сунниты
2. **HadisStatus** (Статусы хадисов)
- Достоверный, Хороший, Слабый, Выдуманный, и др.
3. **HadisTag** (Теги)
- Поклонение, Молитва, Пост, Хадж, Закят, и др.
4. **HadisCategory** (Категории) - Иерархическая структура
- **Коран**: Толкование Корана, Аяты предписаний, и др.
- **Хадисы**: Книга молитвы, Книга поста, Книга хаджа, и др.
5. **Transmitters** (Передатчики)
- Известные мухаддисы и имамы
6. **Hadis** (Хадисы)
- Реалистичные тексты хадисов на русском языке
- Переводы на персидском и английском
- Объяснения и комментарии
7. **HadisTransmitter** (Цепочки передачи)
- Цепочки передатчиков для каждого хадиса
- Включая пропуски в цепочках
8. **HadisReference** (Ссылки)
- Связи хадисов с книгами
9. **ReferenceImage** (Изображения ссылок)
- Изображения для ссылок на источники
### Модели Library
1. **Book** (Книги)
- Аль-Кафи, Сахих аль-Бухари, и др.
- С обложками из seed_images
2. **Category** (Категории библиотеки)
- Книги хадисов, Книги фикха, и др.
3. **BookCollection** (Коллекции книг)
- Шиитские книги хадисов, Суннитские книги хадисов
## Особенности
### Реалистичные данные
- Все тексты на русском языке
- Аутентичные названия книг и имена передатчиков
- Правильная иерархия категорий
- Реалистичные цепочки передачи
### Связи между моделями
- Правильные foreign key связи
- Many-to-many отношения для тегов
- Иерархические структуры (MPTT) для категорий
### Файлы и изображения
- XMind файлы для категорий
- Изображения обложек для книг
- Изображения для ссылок
### Безопасность
- Транзакционная безопасность
- Возможность отката при ошибках
- Подтверждение перед удалением данных
## Структура данных
```
HadisSect (2 записи)
├── HadisCategory (иерархическая структура)
│ ├── Quran categories (4 основные + дочерние)
│ └── Hadith categories (7 основных + дочерние)
├── Hadis (2-4 хадиса на категорию)
│ ├── HadisTransmitter (цепочки 3-6 передатчиков)
│ ├── HadisReference (1-3 ссылки на книги)
│ └── ReferenceImage (изображения для ссылок)
├── HadisStatus (7 статусов)
├── HadisTag (30+ тегов)
└── Transmitters (10 известных передатчиков)
Library Models:
├── Book (4 книги с обложками)
├── Category (5 категорий)
└── BookCollection (3 коллекции)
```
## Тестирование API
После создания данных можно тестировать API:
```bash
# Список сект
curl -X GET "http://localhost:8000/api/hadis/sects/"
# Категории по секте
curl -X GET "http://localhost:8000/api/hadis/sect/1/categories/"
# Хадисы по категории
curl -X GET "http://localhost:8000/api/hadis/category/1/hadis/"
```
## Требования
- Django проект настроен и работает
- Все зависимости установлены
- База данных мигрирована
- Файлы seed_images и test.xmind присутствуют
## Примечания
- Скрипт создает данные на русском языке
- Используются реалистичные исламские термины и имена
- Данные подходят для демонстрации и тестирования
- Можно безопасно запускать несколько раз
- Поддерживается частичная очистка данных

246
scripts/clear_hadis_data.py

@ -0,0 +1,246 @@
#!/usr/bin/env python3
"""
Script to clear existing hadis data created by seeding scripts.
This script safely removes all hadis-related data while preserving
other application data.
"""
import os
import sys
import django
from pathlib import Path
from django.db import transaction
# Setup Django environment
BASE_DIR = Path(__file__).resolve().parent.parent
sys.path.append(str(BASE_DIR))
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
django.setup()
# Import models after Django setup
from apps.hadis.models import (
HadisSect, HadisCategory, HadisStatus, HadisTag, Hadis,
Transmitters, HadisTransmitter, HadisReference, ReferenceImage
)
from apps.library.models import Book, Category as LibraryCategory, BookCollection
class HadisDataCleaner:
"""Class to safely clear hadis data"""
def __init__(self):
pass
def show_current_data(self):
"""Show current data counts"""
print("=== ТЕКУЩИЕ ДАННЫЕ ===")
print(f"HadisSect: {HadisSect.objects.count()}")
print(f"HadisCategory: {HadisCategory.objects.count()}")
print(f"HadisStatus: {HadisStatus.objects.count()}")
print(f"HadisTag: {HadisTag.objects.count()}")
print(f"Hadis: {Hadis.objects.count()}")
print(f"Transmitters: {Transmitters.objects.count()}")
print(f"HadisTransmitter: {HadisTransmitter.objects.count()}")
print(f"HadisReference: {HadisReference.objects.count()}")
print(f"ReferenceImage: {ReferenceImage.objects.count()}")
print(f"Books: {Book.objects.count()}")
print(f"Library Categories: {LibraryCategory.objects.count()}")
print(f"Book Collections: {BookCollection.objects.count()}")
# Show sample data
print("\n=== ОБРАЗЦЫ ДАННЫХ ===")
if HadisSect.objects.exists():
print("HadisSect samples:")
for sect in HadisSect.objects.all()[:3]:
print(f" - {sect.title}")
if HadisStatus.objects.exists():
print("HadisStatus samples:")
for status in HadisStatus.objects.all()[:3]:
print(f" - {status.title}")
if HadisTag.objects.exists():
print("HadisTag samples:")
for tag in HadisTag.objects.all()[:5]:
print(f" - {tag.title}")
if Transmitters.objects.exists():
print("Transmitters samples:")
for trans in Transmitters.objects.all()[:3]:
print(f" - {trans.full_name}")
if Book.objects.exists():
print("Books samples:")
for book in Book.objects.all()[:3]:
print(f" - {book.title}")
@transaction.atomic
def clear_all_hadis_data(self):
"""Clear all hadis-related data"""
print("\n=== ОЧИСТКА ДАННЫХ ХАДИСОВ ===")
# Clear in reverse dependency order
print("Удаление ReferenceImage...")
count = ReferenceImage.objects.count()
ReferenceImage.objects.all().delete()
print(f" Удалено {count} записей ReferenceImage")
print("Удаление HadisReference...")
count = HadisReference.objects.count()
HadisReference.objects.all().delete()
print(f" Удалено {count} записей HadisReference")
print("Удаление HadisTransmitter...")
count = HadisTransmitter.objects.count()
HadisTransmitter.objects.all().delete()
print(f" Удалено {count} записей HadisTransmitter")
print("Удаление Hadis...")
count = Hadis.objects.count()
Hadis.objects.all().delete()
print(f" Удалено {count} записей Hadis")
print("Удаление HadisCategory...")
count = HadisCategory.objects.count()
HadisCategory.objects.all().delete()
print(f" Удалено {count} записей HadisCategory")
print("Удаление HadisSect...")
count = HadisSect.objects.count()
HadisSect.objects.all().delete()
print(f" Удалено {count} записей HadisSect")
print("Удаление HadisStatus...")
count = HadisStatus.objects.count()
HadisStatus.objects.all().delete()
print(f" Удалено {count} записей HadisStatus")
print("Удаление HadisTag...")
count = HadisTag.objects.count()
HadisTag.objects.all().delete()
print(f" Удалено {count} записей HadisTag")
print("Удаление Transmitters...")
count = Transmitters.objects.count()
Transmitters.objects.all().delete()
print(f" Удалено {count} записей Transmitters")
@transaction.atomic
def clear_library_data(self):
"""Clear library data that was created by seeding"""
print("\n=== ОЧИСТКА ДАННЫХ БИБЛИОТЕКИ ===")
# Only clear books that seem to be created by seeding script
# (based on Russian titles or specific patterns)
russian_book_titles = [
'Аль-Кафи', 'Сахих аль-Бухари',
'Ман ля яхдуруху аль-факих', 'Сунан Абу Дауд'
]
books_to_delete = Book.objects.filter(title__in=russian_book_titles)
count = books_to_delete.count()
if count > 0:
books_to_delete.delete()
print(f" Удалено {count} книг с русскими названиями")
else:
print(" Книги с русскими названиями не найдены")
# Clear library categories with Russian names
russian_categories = [
'Книги хадисов', 'Книги фикха', 'Книги толкования',
'Книги нравственности', 'Исторические книги'
]
categories_to_delete = LibraryCategory.objects.filter(title__in=russian_categories)
count = categories_to_delete.count()
if count > 0:
categories_to_delete.delete()
print(f" Удалено {count} категорий библиотеки с русскими названиями")
else:
print(" Категории библиотеки с русскими названиями не найдены")
# Clear book collections with Russian names
russian_collections = [
'Шиитские книги хадисов', 'Суннитские книги хадисов',
'Сборник книг по фикху'
]
collections_to_delete = BookCollection.objects.filter(title__in=russian_collections)
count = collections_to_delete.count()
if count > 0:
collections_to_delete.delete()
print(f" Удалено {count} коллекций книг с русскими названиями")
else:
print(" Коллекции книг с русскими названиями не найдены")
def run_cleanup(self, include_library=True):
"""Main method to run cleanup"""
print("=" * 60)
print("ОЧИСТКА ДАННЫХ ХАДИСОВ")
print("=" * 60)
try:
# Show current state
self.show_current_data()
# Clear hadis data
self.clear_all_hadis_data()
# Clear library data if requested
if include_library:
self.clear_library_data()
# Show final state
print("\n=== ФИНАЛЬНОЕ СОСТОЯНИЕ ===")
self.show_current_data()
print("\n✅ Очистка завершена успешно!")
except Exception as e:
print(f"\n❌ Ошибка при очистке: {e}")
print("Откат транзакции...")
raise
def main():
"""Main function to run the cleanup script"""
import argparse
parser = argparse.ArgumentParser(description='Clear hadis data from database')
parser.add_argument(
'--hadis-only',
action='store_true',
help='Clear only hadis data, keep library data'
)
parser.add_argument(
'--force',
action='store_true',
help='Skip confirmation prompt'
)
args = parser.parse_args()
include_library = not args.hadis_only
if not args.force:
print("Это удалит все данные хадисов из базы данных.")
if include_library:
print("Также будут удалены связанные данные библиотеки.")
response = input("Вы уверены? (да/нет): ")
if response.lower() not in ['да', 'yes', 'y']:
print("Очистка отменена.")
return
try:
cleaner = HadisDataCleaner()
cleaner.run_cleanup(include_library=include_library)
except Exception as e:
print(f"\n❌ Очистка не удалась: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == '__main__':
main()

1087
scripts/seed_hadis_data.py
File diff suppressed because it is too large
View File

BIN
scripts/seed_images/book1.png

After

Width: 509  |  Height: 679  |  Size: 806 KiB

BIN
scripts/seed_images/book2.png

After

Width: 510  |  Height: 679  |  Size: 772 KiB

BIN
scripts/seed_images/book3.png

After

Width: 507  |  Height: 675  |  Size: 804 KiB

BIN
scripts/seed_images/book4.png

After

Width: 216  |  Height: 320  |  Size: 184 KiB

BIN
scripts/test.xmind

711
templates/api/documentation.html

@ -0,0 +1,711 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ title }}</title>
<!-- External Dependencies -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<style>
:root {
--primary-color: #2c3e50;
--secondary-color: #3498db;
--success-color: #27ae60;
--warning-color: #f39c12;
--danger-color: #e74c3c;
--light-bg: #f8f9fa;
--dark-bg: #2c3e50;
--sidebar-width: 300px;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: var(--light-bg);
margin: 0;
padding: 0;
}
/* Main Container */
.documentation-container {
display: flex;
min-height: 100vh;
}
/* Sidebar Styles */
.sidebar {
width: var(--sidebar-width);
background: linear-gradient(135deg, var(--dark-bg) 0%, #34495e 100%);
color: white;
position: fixed;
height: 100vh;
overflow-y: auto;
z-index: 1000;
box-shadow: 2px 0 10px rgba(0,0,0,0.1);
}
.sidebar-header {
padding: 20px;
background: rgba(0,0,0,0.2);
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.sidebar-header h3 {
margin: 0;
font-size: 1.2rem;
font-weight: 600;
}
.sidebar-header p {
margin: 5px 0 0 0;
font-size: 0.9rem;
opacity: 0.8;
}
/* App Navigation */
.app-list {
list-style: none;
padding: 0;
margin: 0;
}
.app-item {
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.app-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 15px 20px;
cursor: pointer;
transition: all 0.3s ease;
background: transparent;
}
.app-header:hover {
background: rgba(255,255,255,0.1);
}
.app-header.active {
background: var(--secondary-color);
}
.app-name {
font-weight: 600;
font-size: 1rem;
}
.app-description {
font-size: 0.8rem;
opacity: 0.7;
margin-top: 2px;
}
.toggle-icon {
transition: transform 0.3s ease;
}
.toggle-icon.rotated {
transform: rotate(90deg);
}
/* Endpoints List */
.endpoints-list {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease;
background: rgba(0,0,0,0.2);
}
.endpoints-list.expanded {
max-height: 500px;
}
.endpoint-item {
padding: 10px 20px 10px 40px;
cursor: pointer;
transition: all 0.3s ease;
border-left: 3px solid transparent;
}
.endpoint-item:hover {
background: rgba(255,255,255,0.1);
border-left-color: var(--success-color);
}
.endpoint-item.active {
background: var(--success-color);
border-left-color: white;
}
/* HTTP Method Badges */
.endpoint-method {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 0.7rem;
font-weight: bold;
margin-right: 8px;
}
.method-get { background: var(--success-color); }
.method-post { background: var(--secondary-color); }
.method-put { background: var(--warning-color); }
.method-delete { background: var(--danger-color); }
.method-patch { background: #9b59b6; }
/* Main Content Area */
.main-content {
margin-left: var(--sidebar-width);
flex: 1;
padding: 30px;
background: white;
}
/* Content Header */
.content-header {
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid var(--light-bg);
}
.header-top {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.content-header h1 {
color: var(--dark-bg);
margin-bottom: 10px;
font-weight: 700;
}
.content-header p {
color: #666;
font-size: 1.1rem;
margin: 0;
}
/* Action Buttons */
.action-buttons {
display: flex;
gap: 10px;
}
.btn-swagger {
background: #ff6b35;
color: white;
padding: 12px 20px;
border-radius: 8px;
text-decoration: none;
font-weight: 600;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.3s ease;
}
.btn-swagger:hover {
background: #e55a2b;
color: white;
text-decoration: none;
}
.btn-redoc {
background: #39b982;
color: white;
padding: 12px 20px;
border-radius: 8px;
text-decoration: none;
font-weight: 600;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.3s ease;
}
.btn-redoc:hover {
background: #2d8f66;
color: white;
text-decoration: none;
}
/* Mobile Toggle Button */
.mobile-toggle {
display: none;
position: fixed;
top: 20px;
left: 20px;
z-index: 1001;
background: var(--secondary-color);
color: white;
border: none;
padding: 10px;
border-radius: 6px;
cursor: pointer;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
}
@media (max-width: 768px) {
.mobile-toggle {
display: block;
}
.sidebar {
transform: translateX(-100%);
transition: transform 0.3s ease;
}
.sidebar.mobile-open {
transform: translateX(0);
}
.main-content {
margin-left: 0;
padding: 20px 15px;
}
.header-top {
flex-direction: column;
align-items: flex-start;
gap: 15px;
}
.action-buttons {
flex-direction: column;
width: 100%;
}
.btn-swagger,
.btn-redoc {
justify-content: center;
width: 100%;
}
}
/* Endpoint Documentation Styles */
.endpoint-section {
margin-bottom: 40px;
padding: 25px;
background: white;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
border: 1px solid #e9ecef;
}
.endpoint-header {
display: flex;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #e9ecef;
flex-wrap: wrap;
gap: 15px;
}
.endpoint-title {
font-size: 1.5rem;
font-weight: 600;
color: var(--dark-bg);
margin: 0;
}
.endpoint-url {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
background: var(--light-bg);
padding: 8px 12px;
border-radius: 6px;
font-size: 0.9rem;
color: var(--dark-bg);
border: 1px solid #dee2e6;
}
.endpoint-description {
color: #666;
margin-bottom: 25px;
line-height: 1.6;
font-size: 1rem;
}
/* Parameters Section */
.parameters-section {
margin-bottom: 25px;
}
.section-title {
font-size: 1.2rem;
font-weight: 600;
color: var(--dark-bg);
margin-bottom: 15px;
display: flex;
align-items: center;
}
.section-title i {
margin-right: 8px;
color: var(--secondary-color);
}
.parameters-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 20px;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
}
.parameters-table th,
.parameters-table td {
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid #e9ecef;
}
.parameters-table th {
background: var(--light-bg);
font-weight: 600;
color: var(--dark-bg);
font-size: 0.9rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.parameters-table tbody tr:hover {
background: #f8f9fa;
}
.param-name {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-weight: 600;
color: var(--secondary-color);
background: #f8f9fa;
padding: 4px 8px;
border-radius: 4px;
}
.param-type {
background: #e3f2fd;
color: #1976d2;
padding: 4px 8px;
border-radius: 4px;
font-size: 0.8rem;
font-weight: 500;
text-transform: uppercase;
}
.param-required {
background: #ffebee;
color: #c62828;
padding: 4px 8px;
border-radius: 4px;
font-size: 0.8rem;
font-weight: 500;
text-transform: uppercase;
}
.param-optional {
background: #e8f5e8;
color: #2e7d32;
padding: 4px 8px;
border-radius: 4px;
font-size: 0.8rem;
font-weight: 500;
text-transform: uppercase;
}
/* Response Examples Section */
.response-section {
margin-top: 25px;
}
.response-tabs {
display: flex;
margin-bottom: 15px;
border-bottom: 1px solid #e9ecef;
gap: 5px;
}
.response-tab {
padding: 10px 20px;
cursor: pointer;
border: none;
background: none;
color: #666;
font-weight: 500;
transition: all 0.3s ease;
border-radius: 6px 6px 0 0;
position: relative;
}
.response-tab:hover {
background: #f8f9fa;
color: var(--secondary-color);
}
.response-tab.active {
color: var(--secondary-color);
background: #f8f9fa;
border-bottom: 2px solid var(--secondary-color);
}
.json-viewer {
background: #1e1e1e;
border-radius: 8px;
padding: 20px;
margin-top: 15px;
overflow-x: auto;
border: 1px solid #e9ecef;
}
.json-viewer pre {
margin: 0;
color: #d4d4d4;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 0.9rem;
line-height: 1.5;
}
</style>
</head>
<body>
<!-- Mobile Toggle Button -->
<button class="mobile-toggle" onclick="toggleSidebar()">
<i class="fas fa-bars"></i>
</button>
<div class="documentation-container">
<!-- Sidebar Navigation -->
<div class="sidebar" id="sidebar">
<div class="sidebar-header">
<h3><i class="fas fa-book"></i> API Documentation</h3>
<p>{{ description }}</p>
</div>
<ul class="app-list">
{% for app_key, app_data in api_structure.items %}
<li class="app-item">
<div class="app-header" onclick="toggleEndpoints('{{ app_key }}-endpoints')">
<div>
<div class="app-name">{{ app_data.name }}</div>
<div class="app-description">{{ app_data.description }}</div>
</div>
<i class="fas fa-chevron-right toggle-icon" id="{{ app_key }}-icon"></i>
</div>
<ul class="endpoints-list" id="{{ app_key }}-endpoints">
{% for endpoint in app_data.endpoints %}
<li class="endpoint-item" onclick="scrollToEndpoint('{{ app_key }}-{{ forloop.counter0 }}')">
<span class="endpoint-method method-{{ endpoint.method|lower }}">{{ endpoint.method }}</span>
{{ endpoint.name }}
</li>
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>
</div>
<!-- Main Content -->
<div class="main-content">
<div class="content-header">
<div class="header-top">
<div>
<h1>{{ title }}</h1>
<p>{{ description }}</p>
</div>
<div class="action-buttons">
<a href="{% url 'schema-swagger-ui' %}" class="btn-swagger">
<i class="fas fa-code"></i>
Swagger UI
</a>
<a href="{% url 'schema-redoc' %}" class="btn-redoc">
<i class="fas fa-book"></i>
ReDoc
</a>
</div>
</div>
</div>
<!-- API Endpoints Documentation -->
{% for app_key, app_data in api_structure.items %}
{% for endpoint in app_data.endpoints %}
<div class="endpoint-section" id="{{ app_key }}-{{ forloop.counter0 }}">
<div class="endpoint-header">
<h2 class="endpoint-title">{{ endpoint.name }}</h2>
<span class="endpoint-method method-{{ endpoint.method|lower }}">{{ endpoint.method }}</span>
<code class="endpoint-url">{{ endpoint.url }}</code>
</div>
<p class="endpoint-description">{{ endpoint.description }}</p>
{% if endpoint.parameters %}
<div class="parameters-section">
<h3 class="section-title">
<i class="fas fa-cogs"></i>
Parameters
</h3>
<table class="parameters-table">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>
<tbody>
{% for param in endpoint.parameters %}
<tr>
<td><code class="param-name">{{ param.name }}</code></td>
<td><span class="param-type">{{ param.type }}</span></td>
<td>
{% if param.required %}
<span class="param-required">Required</span>
{% else %}
<span class="param-optional">Optional</span>
{% endif %}
</td>
<td>{{ param.description }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
<div class="response-section">
<h3 class="section-title">
<i class="fas fa-code"></i>
Response Examples
</h3>
<div class="response-tabs">
{% for response_type, response_data in endpoint.response_examples.items %}
<button class="response-tab {% if forloop.first %}active{% endif %}"
onclick="showResponse('{{ app_key }}-{{ forloop.parentloop.counter0 }}', '{{ response_type }}')">
{{ response_type|title }} Response
</button>
{% endfor %}
</div>
{% for response_type, response_data in endpoint.response_examples.items %}
<div class="json-viewer" id="{{ app_key }}-{{ forloop.parentloop.counter0 }}-{{ response_type }}"
style="{% if not forloop.first %}display: none;{% endif %}">
<pre><code class="language-json">{{ response_data|safe }}</code></pre>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
{% endfor %}
</div>
</div>
<!-- JavaScript for Functionality -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-core.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/plugins/autoloader/prism-autoloader.min.js"></script>
<script>
// Toggle sidebar for mobile
function toggleSidebar() {
const sidebar = document.getElementById('sidebar');
sidebar.classList.toggle('mobile-open');
}
// Toggle endpoints list
function toggleEndpoints(endpointsId) {
const endpointsList = document.getElementById(endpointsId);
const icon = document.getElementById(endpointsId.replace('-endpoints', '-icon'));
const header = icon.closest('.app-header');
endpointsList.classList.toggle('expanded');
icon.classList.toggle('rotated');
header.classList.toggle('active');
}
// Scroll to endpoint section
function scrollToEndpoint(endpointId) {
const element = document.getElementById(endpointId);
if (element) {
element.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
// Highlight the endpoint temporarily
element.style.boxShadow = '0 0 20px rgba(52, 152, 219, 0.3)';
setTimeout(() => {
element.style.boxShadow = '0 4px 20px rgba(0,0,0,0.08)';
}, 2000);
}
// Close sidebar on mobile after selection
if (window.innerWidth <= 768) {
document.getElementById('sidebar').classList.remove('mobile-open');
}
}
// Show response example
function showResponse(endpointId, responseType) {
// Hide all response examples for this endpoint
const allResponses = document.querySelectorAll(`[id^="${endpointId}-"]`);
allResponses.forEach(response => {
if (response.classList.contains('json-viewer')) {
response.style.display = 'none';
}
});
// Show selected response
const selectedResponse = document.getElementById(`${endpointId}-${responseType}`);
if (selectedResponse) {
selectedResponse.style.display = 'block';
}
// Update tab active state
const tabs = document.querySelectorAll('.response-tab');
tabs.forEach(tab => tab.classList.remove('active'));
event.target.classList.add('active');
}
// Format JSON in response examples
document.addEventListener('DOMContentLoaded', function() {
const jsonViewers = document.querySelectorAll('.json-viewer pre code');
jsonViewers.forEach(viewer => {
try {
const jsonText = viewer.textContent;
const jsonObj = JSON.parse(jsonText);
const formattedJson = JSON.stringify(jsonObj, null, 2);
viewer.textContent = formattedJson;
} catch (e) {
// If it's not valid JSON, leave it as is
console.log('Not valid JSON:', e);
}
});
// Initialize Prism.js for syntax highlighting
if (typeof Prism !== 'undefined') {
Prism.highlightAll();
}
});
// Close sidebar when clicking outside on mobile
document.addEventListener('click', function(event) {
if (window.innerWidth <= 768) {
const sidebar = document.getElementById('sidebar');
const mobileToggle = document.querySelector('.mobile-toggle');
if (!sidebar.contains(event.target) && !mobileToggle.contains(event.target)) {
sidebar.classList.remove('mobile-open');
}
}
});
</script>
</body>
</html>

402
templates/swagger/auth.html

@ -0,0 +1,402 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Swagger Authentication - Imam Javad API</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<style>
:root {
--primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--success-color: #28a745;
--warning-color: #f39c12;
--danger-color: #e74c3c;
--info-color: #17a2b8;
}
body {
background: var(--primary-gradient);
min-height: 100vh;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.auth-container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.auth-card {
background: white;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0,0,0,0.1);
overflow: hidden;
max-width: 500px;
width: 100%;
}
.auth-header {
background: var(--primary-gradient);
color: white;
padding: 30px;
text-align: center;
}
.auth-header h1 {
margin: 0 0 10px 0;
font-size: 1.8rem;
font-weight: 600;
}
.auth-header p {
margin: 0;
opacity: 0.9;
font-size: 0.95rem;
}
.auth-body {
padding: 30px;
}
.form-group {
margin-bottom: 20px;
}
.form-label {
font-weight: 600;
color: #2c3e50;
margin-bottom: 8px;
display: block;
}
.form-control {
border: 2px solid #e9ecef;
border-radius: 8px;
padding: 12px 16px;
font-size: 0.95rem;
transition: all 0.3s ease;
}
.form-control:focus {
border-color: #667eea;
box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25);
}
.btn-auth {
background: var(--primary-gradient);
border: none;
color: white;
padding: 12px 24px;
border-radius: 8px;
font-weight: 600;
font-size: 0.95rem;
width: 100%;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.btn-auth:hover {
background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%);
color: white;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
}
.btn-secondary {
background: #6c757d;
border: none;
color: white;
padding: 10px 20px;
border-radius: 6px;
font-weight: 500;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 6px;
transition: all 0.3s ease;
}
.btn-secondary:hover {
background: #5a6268;
color: white;
text-decoration: none;
}
.user-info-card {
background: #f8f9fa;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.user-info-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 15px;
}
.user-avatar {
width: 48px;
height: 48px;
border-radius: 50%;
background: var(--primary-gradient);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 1.2rem;
}
.user-details h4 {
margin: 0;
color: #2c3e50;
font-size: 1.1rem;
}
.user-details p {
margin: 0;
color: #6c757d;
font-size: 0.9rem;
}
.user-badges {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.badge {
padding: 4px 8px;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 500;
}
.badge-staff {
background: var(--info-color);
color: white;
}
.badge-superuser {
background: var(--warning-color);
color: white;
}
.help-section {
background: #e3f2fd;
border-radius: 8px;
padding: 20px;
margin-top: 20px;
}
.help-section h5 {
color: #1976d2;
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 8px;
}
.help-section p {
margin: 0;
color: #1565c0;
font-size: 0.9rem;
line-height: 1.5;
}
.navigation-links {
display: flex;
gap: 10px;
justify-content: center;
margin-top: 20px;
flex-wrap: wrap;
}
.alert {
border-radius: 8px;
border: none;
padding: 12px 16px;
margin-bottom: 20px;
}
.alert-success {
background: #d4edda;
color: #155724;
}
.alert-danger {
background: #f8d7da;
color: #721c24;
}
.alert-warning {
background: #fff3cd;
color: #856404;
}
@media (max-width: 576px) {
.auth-container {
padding: 10px;
}
.auth-header {
padding: 20px;
}
.auth-body {
padding: 20px;
}
.navigation-links {
flex-direction: column;
}
.btn-secondary {
width: 100%;
justify-content: center;
}
}
</style>
</head>
<body>
<div class="auth-container">
<div class="auth-card">
<div class="auth-header">
<h1><i class="fas fa-key"></i> API Authentication</h1>
<p>Enter your API token to access Swagger UI</p>
</div>
<div class="auth-body">
<!-- Display Messages -->
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }}">
{{ message }}
</div>
{% endfor %}
{% endif %}
<!-- Current User Info (if authenticated) -->
{% if user_info %}
<div class="user-info-card">
<div class="user-info-header">
<div class="user-avatar">
{{ user_info.fullname|first|upper }}
</div>
<div class="user-details">
<h4>{{ user_info.fullname }}</h4>
<p>{{ user_info.email }}</p>
</div>
</div>
<div class="user-badges">
{% if user_info.is_staff %}
<span class="badge badge-staff">Staff</span>
{% endif %}
{% if user_info.is_superuser %}
<span class="badge badge-superuser">Superuser</span>
{% endif %}
<span class="badge bg-success text-white">Authenticated</span>
</div>
</div>
{% endif %}
<!-- Token Authentication Form -->
<form method="post">
{% csrf_token %}
<div class="form-group">
<label for="token" class="form-label">
<i class="fas fa-key"></i> API Token
</label>
<input
type="text"
class="form-control"
id="token"
name="token"
placeholder="Enter your 40-character API token"
value="{{ current_token|default:'' }}"
maxlength="40"
required
>
<small class="form-text text-muted">
Token must be exactly 40 characters long
</small>
</div>
<button type="submit" class="btn-auth">
<i class="fas fa-sign-in-alt"></i>
Authenticate
</button>
</form>
<!-- Help Section -->
<div class="help-section">
<h5>
<i class="fas fa-question-circle"></i>
How to get your API token?
</h5>
<p>
Your API token can be found in your user profile or generated through the Django admin panel.
Contact your system administrator if you need assistance obtaining your token.
</p>
</div>
<!-- Navigation Links -->
<div class="navigation-links">
<a href="{% url 'docs-index' %}" class="btn-secondary">
<i class="fas fa-book"></i>
Documentation
</a>
{% if current_token %}
<a href="{% url 'schema-swagger-ui' %}" class="btn-secondary">
<i class="fas fa-code"></i>
Swagger UI
</a>
<a href="{% url 'clear-swagger-auth' %}" class="btn-secondary">
<i class="fas fa-sign-out-alt"></i>
Clear Token
</a>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Bootstrap JavaScript -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
// Auto-focus on token input
document.addEventListener('DOMContentLoaded', function() {
const tokenInput = document.getElementById('token');
if (tokenInput && !tokenInput.value) {
tokenInput.focus();
}
});
// Token validation
document.getElementById('token').addEventListener('input', function(e) {
const token = e.target.value;
const submitBtn = document.querySelector('.btn-auth');
if (token.length === 40) {
submitBtn.style.background = 'linear-gradient(135deg, #28a745 0%, #20c997 100%)';
submitBtn.innerHTML = '<i class="fas fa-check"></i> Ready to Authenticate';
} else {
submitBtn.style.background = 'var(--primary-gradient)';
submitBtn.innerHTML = '<i class="fas fa-sign-in-alt"></i> Authenticate';
}
});
</script>
</body>
</html>

354
templates/swagger/ui.html

@ -0,0 +1,354 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Imam Javad API - Swagger UI</title>
<!-- Swagger UI CSS -->
<link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@4.15.5/swagger-ui.css" />
<style>
:root {
--primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--success-color: #28a745;
--warning-color: #f39c12;
--danger-color: #e74c3c;
--info-color: #17a2b8;
}
/* Fixed Authentication Banner */
.auth-banner {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 9999;
background: var(--primary-gradient);
backdrop-filter: blur(10px);
box-shadow: 0 2px 20px rgba(0,0,0,0.1);
padding: 15px 20px;
color: white;
}
.auth-content {
display: flex;
justify-content: space-between;
align-items: center;
max-width: 1200px;
margin: 0 auto;
}
.auth-info {
display: flex;
align-items: center;
gap: 20px;
}
.user-info {
display: flex;
align-items: center;
gap: 10px;
}
.user-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
background: rgba(255,255,255,0.2);
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
}
.user-details h4 {
margin: 0;
font-size: 0.9rem;
font-weight: 600;
}
.user-details p {
margin: 0;
font-size: 0.8rem;
opacity: 0.8;
}
.auth-status {
display: flex;
align-items: center;
gap: 8px;
background: rgba(255,255,255,0.1);
padding: 8px 12px;
border-radius: 20px;
font-size: 0.8rem;
}
.status-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--success-color);
}
.auth-actions {
display: flex;
gap: 10px;
}
.auth-btn {
padding: 8px 16px;
border: none;
border-radius: 6px;
font-size: 0.8rem;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
text-decoration: none;
display: flex;
align-items: center;
gap: 6px;
}
.btn-primary {
background: rgba(255,255,255,0.2);
color: white;
border: 1px solid rgba(255,255,255,0.3);
}
.btn-primary:hover {
background: rgba(255,255,255,0.3);
color: white;
text-decoration: none;
}
.btn-secondary {
background: transparent;
color: white;
border: 1px solid rgba(255,255,255,0.3);
}
.btn-secondary:hover {
background: rgba(255,255,255,0.1);
color: white;
text-decoration: none;
}
/* Main Content Adjustment */
body {
padding-top: 80px;
}
/* Swagger UI Customizations */
.swagger-ui .topbar {
display: none;
}
.swagger-ui .info {
margin: 20px 0;
}
.swagger-ui .info .title {
color: #2c3e50;
font-size: 2rem;
margin-bottom: 10px;
}
/* Mobile Responsive */
@media (max-width: 768px) {
body {
padding-top: 120px;
}
.auth-content {
flex-direction: column;
gap: 15px;
align-items: flex-start;
}
.auth-info {
flex-direction: column;
gap: 10px;
align-items: flex-start;
width: 100%;
}
.auth-actions {
width: 100%;
justify-content: flex-end;
}
.auth-btn {
font-size: 0.7rem;
padding: 6px 12px;
}
}
@media (max-width: 480px) {
body {
padding-top: 140px;
}
.auth-banner {
padding: 10px 15px;
}
.user-details h4 {
font-size: 0.8rem;
}
.user-details p {
font-size: 0.7rem;
}
}
/* Loading Animation */
.loading-spinner {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid rgba(255,255,255,0.3);
border-radius: 50%;
border-top-color: white;
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Custom Swagger Styles */
.swagger-ui .scheme-container {
background: #f8f9fa;
border-radius: 8px;
padding: 15px;
margin: 20px 0;
}
.swagger-ui .btn.authorize {
background: var(--primary-gradient);
border: none;
color: white;
}
.swagger-ui .btn.authorize:hover {
background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%);
}
</style>
</head>
<body>
<!-- Fixed Authentication Banner -->
<div class="auth-banner">
<div class="auth-content">
<div class="auth-info">
{% if request.session.swagger_user_info %}
<div class="user-info">
<div class="user-avatar">
{{ request.session.swagger_user_info.fullname|first|upper }}
</div>
<div class="user-details">
<h4>{{ request.session.swagger_user_info.fullname }}</h4>
<p>{{ request.session.swagger_user_info.email }}</p>
</div>
</div>
<div class="auth-status">
<div class="status-indicator"></div>
<span>Authenticated</span>
</div>
{% else %}
<div class="auth-status">
<div class="status-indicator" style="background: var(--warning-color);"></div>
<span>Not Authenticated</span>
</div>
{% endif %}
</div>
<div class="auth-actions">
<a href="{% url 'docs-index' %}" class="auth-btn btn-secondary">
<i class="fas fa-book"></i>
Documentation
</a>
{% if request.session.swagger_user_info %}
<a href="{% url 'clear-swagger-auth' %}" class="auth-btn btn-primary">
<i class="fas fa-sign-out-alt"></i>
Logout
</a>
{% else %}
<a href="{% url 'swagger-token-auth' %}" class="auth-btn btn-primary">
<i class="fas fa-key"></i>
Authenticate
</a>
{% endif %}
</div>
</div>
</div>
<!-- Swagger UI Container -->
<div id="swagger-ui"></div>
<!-- Swagger UI JavaScript -->
<script src="https://unpkg.com/swagger-ui-dist@4.15.5/swagger-ui-bundle.js"></script>
<script src="https://unpkg.com/swagger-ui-dist@4.15.5/swagger-ui-standalone-preset.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/js/all.min.js"></script>
<script>
window.onload = function() {
const ui = SwaggerUIBundle({
url: '{{ swagger_spec_url }}',
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout",
requestInterceptor: function(request) {
// Add authorization header if token exists
{% if request.session.swagger_token %}
request.headers['Authorization'] = 'Token {{ request.session.swagger_token }}';
{% endif %}
return request;
},
responseInterceptor: function(response) {
// Handle authentication errors
if (response.status === 401) {
console.log('Authentication required');
// Could redirect to auth page or show message
}
return response;
}
});
// Custom styling after UI loads
setTimeout(function() {
// Hide default topbar
const topbar = document.querySelector('.swagger-ui .topbar');
if (topbar) {
topbar.style.display = 'none';
}
// Add custom info section
const infoSection = document.querySelector('.swagger-ui .info');
if (infoSection && !document.querySelector('.custom-info-added')) {
const customInfo = document.createElement('div');
customInfo.className = 'custom-info-added';
customInfo.innerHTML = `
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white; padding: 20px; border-radius: 8px; margin-bottom: 20px;">
<h2 style="margin: 0 0 10px 0;">Imam Javad API</h2>
<p style="margin: 0; opacity: 0.9;">
Comprehensive API documentation for the Imam Javad educational platform.
This API provides access to courses, hadis collections, library resources, and more.
</p>
</div>
`;
infoSection.parentNode.insertBefore(customInfo, infoSection);
}
}, 1000);
};
</script>
</body>
</html>

23
utils/pagination.py

@ -0,0 +1,23 @@
from rest_framework.pagination import PageNumberPagination
from rest_framework.response import Response
class NoPagination(PageNumberPagination):
def paginate_queryset(self, queryset, request, view=None):
# Override to return all items instead of paginated ones
self.count = len(queryset)
self.request = request
self.page = None
self.page_size = len(queryset)
return list(queryset)
def get_paginated_response(self, data):
# Keep the structure but include all results
return Response({
'count': self.count,
'next': None, # No next page
'previous': None, # No previous page
'results': data,
})
Loading…
Cancel
Save