Browse Source
Add API documentation templates, authentication UI, and pagination utility
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
43 changed files with 6791 additions and 4046 deletions
-
34apps/api/views.py
-
15apps/api/views/__init__.py
-
31apps/api/views/api_views.py
-
462apps/api/views/documentation.py
-
74apps/api/views/swagger_views.py
-
237apps/hadis/admin/category.py
-
370apps/hadis/admin/hadis.py
-
66apps/hadis/admin/transmitter.py
-
452apps/hadis/doc.py
-
341apps/hadis/docs.py
-
217apps/hadis/migrations/0002_hadissect_hadisstatus_alter_hadis_options_and_more.py
-
106apps/hadis/models/category.py
-
97apps/hadis/models/hadis.py
-
52apps/hadis/models/transmitter.py
-
127apps/hadis/serializers.py
-
2apps/hadis/serializers/__init__.py
-
102apps/hadis/serializers/category.py
-
144apps/hadis/serializers/hadis.py
-
2343apps/hadis/templates/admin/category_index.html
-
42apps/hadis/templates/admin/hadiscategory/change_form.html
-
153apps/hadis/templates/admin/hadisowerview_change_form.html
-
7apps/hadis/templates/admin/widgets/color_radio.html
-
9apps/hadis/templates/admin/widgets/color_radio_option.html
-
16apps/hadis/urls.py
-
380apps/hadis/views/category.py
-
85apps/hadis/views/hadis.py
-
63config/enhanced_auth_middleware.py
-
85config/settings/base.py
-
38config/urls.py
-
252docs/API_Documentation_System_README.md
-
1433docs/Custom_Swagger_API_Documentation_Implementation_Guide.md
-
169scripts/README.md
-
246scripts/clear_hadis_data.py
-
1087scripts/seed_hadis_data.py
-
BINscripts/seed_images/book1.png
-
BINscripts/seed_images/book2.png
-
BINscripts/seed_images/book3.png
-
BINscripts/seed_images/book4.png
-
BINscripts/test.xmind
-
711templates/api/documentation.html
-
402templates/swagger/auth.html
-
354templates/swagger/ui.html
-
23utils/pagination.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 |
|||
@ -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', |
|||
] |
|||
@ -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) |
|||
@ -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) |
|||
} |
|||
} |
|||
] |
|||
} |
|||
} |
|||
@ -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') |
|||
@ -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() |
|||
) |
|||
) |
|||
|
|||
# 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) |
|||
fieldsets = ( |
|||
(None, { |
|||
'fields': ('parent', 'sect', 'source_type', 'title', 'order') |
|||
}), |
|||
(_('Files'), { |
|||
'fields': ('xmind_file',), |
|||
'classes': ('collapse',) |
|||
}), |
|||
) |
|||
|
|||
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) |
|||
@ -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) |
|||
@ -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) |
|||
@ -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." |
|||
} |
|||
) |
|||
@ -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") |
|||
} |
|||
) |
|||
@ -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', |
|||
), |
|||
] |
|||
@ -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 |
|||
@ -0,0 +1,2 @@ |
|||
from .category import * |
|||
from .hadis import * |
|||
@ -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], |
|||
} |
|||
@ -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
File diff suppressed because it is too large
View File
@ -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 %} |
|||
@ -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 %} |
|||
@ -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 %} |
|||
@ -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 %} |
|||
@ -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'), |
|||
] |
|||
@ -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}') |
|||
|
|||
# Join all parts with a separator |
|||
key = ':'.join(key_parts) |
|||
|
|||
return key |
|||
|
|||
@classmethod |
|||
def invalidate_cache(cls, source_type=None): |
|||
""" |
|||
Invalidate the category tree cache. |
|||
|
|||
Args: |
|||
source_type: Optional source_type to invalidate specific cache. |
|||
If None, invalidates all category tree caches. |
|||
""" |
|||
if source_type: |
|||
# Invalidate specific tree cache |
|||
tree_cache_key = cls().get_cache_key(source_type) |
|||
cache.delete(tree_cache_key) |
|||
|
|||
# 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. |
|||
from rest_framework.response import Response |
|||
from django.shortcuts import get_object_or_404 |
|||
from utils.pagination import NoPagination |
|||
|
|||
Args: |
|||
c: The HadisCategory instance |
|||
from ..models import HadisSect, HadisCategory |
|||
from ..serializers import HadisCategorySectListSerializer, HadisCategoryTreeSerializer |
|||
from ..docs import hadis_sect_list_swagger, hadis_category_tree_swagger |
|||
|
|||
Returns: |
|||
Dictionary representation of the category with proper tree structure |
|||
""" |
|||
# Get the level of this category |
|||
level = c.level_p |
|||
|
|||
# Determine source_type and category_type based on level |
|||
source_type = None |
|||
category_type = None |
|||
class HadisSectListView(ListAPIView): |
|||
""" |
|||
API view to list all HadisSects grouped by sect_type (shia/sunni) |
|||
""" |
|||
queryset = HadisSect.objects.filter(is_active=True).order_by('order') |
|||
serializer_class = HadisCategorySectListSerializer |
|||
pagination_class = NoPagination |
|||
|
|||
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 |
|||
@hadis_sect_list_swagger |
|||
def get(self, request, *args, **kwargs): |
|||
return self.list(request, *args, **kwargs) |
|||
|
|||
# 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', []) |
|||
def list(self, request, *args, **kwargs): |
|||
queryset = self.get_queryset() |
|||
response = super().list(request, *args, **kwargs) |
|||
lang = request.LANGUAGE_CODE |
|||
|
|||
# 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], |
|||
# Group sects by type |
|||
grouped_data = { |
|||
'shia': [], |
|||
'sunni': [] |
|||
} |
|||
|
|||
def get_pagination_cache_key(self, source_type=None, page=1, page_size=None): |
|||
""" |
|||
Generate a cache key for paginated results. |
|||
|
|||
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 |
|||
""" |
|||
# 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}') |
|||
for sect in queryset: |
|||
sect_data = { |
|||
'id': sect.id, |
|||
'title': sect.title, |
|||
'seo_field': None |
|||
} |
|||
|
|||
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 response with count and results |
|||
response_data = { |
|||
'count': queryset.count(), |
|||
'results': grouped_data |
|||
} |
|||
|
|||
# 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}') |
|||
return Response(response_data) |
|||
|
|||
# Join all parts with a separator |
|||
key = ':'.join(key_parts) |
|||
|
|||
return key |
|||
class HadisCategoryTreeView(ListAPIView): |
|||
""" |
|||
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) |
|||
@ -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) |
|||
|
|||
def get_object(self): |
|||
hadis_id = self.kwargs.get('pk') |
|||
queryset = Hadis.objects.filter(id=hadis_id) |
|||
return self.retrieve(request, *args, **kwargs) |
|||
|
|||
# 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 |
|||
|
|||
@ -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 |
|||
@ -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
File diff suppressed because it is too large
View File
@ -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 присутствуют |
|||
|
|||
## Примечания |
|||
|
|||
- Скрипт создает данные на русском языке |
|||
- Используются реалистичные исламские термины и имена |
|||
- Данные подходят для демонстрации и тестирования |
|||
- Можно безопасно запускать несколько раз |
|||
- Поддерживается частичная очистка данных |
|||
@ -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
File diff suppressed because it is too large
View File
|
After Width: 509 | Height: 679 | Size: 806 KiB |
|
After Width: 510 | Height: 679 | Size: 772 KiB |
|
After Width: 507 | Height: 675 | Size: 804 KiB |
|
After Width: 216 | Height: 320 | Size: 184 KiB |
@ -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> |
|||
@ -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> |
|||
@ -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> |
|||
@ -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, |
|||
}) |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue