diff --git a/apps/api/views.py b/apps/api/views.py index 6d8a986..7eaef73 100644 --- a/apps/api/views.py +++ b/apps/api/views.py @@ -1,32 +1,2 @@ -import random -from rest_framework.generics import GenericAPIView -from rest_framework.response import Response -from rest_framework import serializers - -from rest_framework.authtoken.models import Token -from apps.account.models import User - -class HomeSerializer(serializers.Serializer): - token = serializers.CharField() - -from utils.countries import countries - - -# test class generate token -class HomeView(GenericAPIView): - serializer_class = HomeSerializer - - def get(self, request): - # Get build_number from headers - build_number = request.META.get('HTTP_BUILD_NUMBER') - - # Print the build_number - print(f"Build Number: {build_number}") - - return Response({'token': "ok", 'build_number': build_number}) - -class CountryView(GenericAPIView): - - def get(self, request): - return Response(countries, status=200) - +# Legacy views - moved to views/api_views.py for better organization +from .views.api_views import HomeView, CountryView diff --git a/apps/api/views/__init__.py b/apps/api/views/__init__.py new file mode 100644 index 0000000..7c44fc3 --- /dev/null +++ b/apps/api/views/__init__.py @@ -0,0 +1,15 @@ +# API Views Package +# This package contains all API-related views organized by functionality + +from .api_views import HomeView, CountryView +from .documentation import CustomAPIDocumentationView +from .swagger_views import CustomSwaggerView, SwaggerTokenAuthView, clear_swagger_auth + +__all__ = [ + 'HomeView', + 'CountryView', + 'CustomAPIDocumentationView', + 'CustomSwaggerView', + 'SwaggerTokenAuthView', + 'clear_swagger_auth', +] diff --git a/apps/api/views/api_views.py b/apps/api/views/api_views.py new file mode 100644 index 0000000..ec4690a --- /dev/null +++ b/apps/api/views/api_views.py @@ -0,0 +1,31 @@ +import random +from rest_framework.generics import GenericAPIView +from rest_framework.response import Response +from rest_framework import serializers + +from rest_framework.authtoken.models import Token +from apps.account.models import User + +class HomeSerializer(serializers.Serializer): + token = serializers.CharField() + +from utils.countries import countries + + +# test class generate token +class HomeView(GenericAPIView): + serializer_class = HomeSerializer + + def get(self, request): + # Get build_number from headers + build_number = request.META.get('HTTP_BUILD_NUMBER') + + # Print the build_number + print(f"Build Number: {build_number}") + + return Response({'token': "ok", 'build_number': build_number}) + +class CountryView(GenericAPIView): + + def get(self, request): + return Response(countries, status=200) diff --git a/apps/api/views/documentation.py b/apps/api/views/documentation.py new file mode 100644 index 0000000..fd5db0f --- /dev/null +++ b/apps/api/views/documentation.py @@ -0,0 +1,462 @@ +import json +from django.shortcuts import render +from django.views import View +from django.contrib.admin.views.decorators import staff_member_required +from django.utils.decorators import method_decorator + +@method_decorator(staff_member_required, name='dispatch') +class CustomAPIDocumentationView(View): + """ + Custom API Documentation view with collapsible sidebar navigation + Requires admin login to access + """ + + def get(self, request): + api_structure = self._get_api_structure() + context = { + 'api_structure': api_structure, + 'request': request, + 'title': 'Imam Javad API Documentation', + 'description': 'Comprehensive API documentation with interactive examples for the Imam Javad project', + } + return render(request, 'api/documentation.html', context) + + def _get_api_structure(self): + """ + Define the API structure for the Imam Javad project with all apps and endpoints + """ + return { + 'account': { + 'name': 'Account Management', + 'description': 'User authentication, registration, and profile management', + 'endpoints': [ + { + 'name': 'User Registration', + 'method': 'POST', + 'url': '/api/account/register/', + 'description': 'Register a new user account with email verification', + 'parameters': [ + {'name': 'email', 'type': 'string', 'description': 'User email address', 'required': True}, + {'name': 'password', 'type': 'string', 'description': 'User password', 'required': True}, + {'name': 'password_confirm', 'type': 'string', 'description': 'Password confirmation', 'required': True}, + {'name': 'fullname', 'type': 'string', 'description': 'User full name', 'required': True}, + ], + 'response_examples': { + 'success': json.dumps({ + "message": "Registration successful. Please check your email for verification code.", + "user_id": 123, + "email": "user@example.com" + }, indent=2), + 'error': json.dumps({ + "error": "Email already exists", + "details": "A user with this email address already exists." + }, indent=2) + } + }, + { + 'name': 'Email Verification', + 'method': 'POST', + 'url': '/api/account/verify/', + 'description': 'Verify user email with verification code', + 'parameters': [ + {'name': 'email', 'type': 'string', 'description': 'User email address', 'required': True}, + {'name': 'code', 'type': 'string', 'description': 'Verification code from email', 'required': True}, + ], + 'response_examples': { + 'success': json.dumps({ + "message": "Email verified successfully", + "token": "abc123def456...", + "user": { + "id": 123, + "email": "user@example.com", + "fullname": "John Doe", + "is_verified": True + } + }, indent=2) + } + }, + { + 'name': 'User Login', + 'method': 'POST', + 'url': '/api/account/login/', + 'description': 'Authenticate user and get access token', + 'parameters': [ + {'name': 'email', 'type': 'string', 'description': 'User email address', 'required': True}, + {'name': 'password', 'type': 'string', 'description': 'User password', 'required': True}, + {'name': 'fcm', 'type': 'string', 'description': 'FCM token for notifications', 'required': False}, + {'name': 'device_id', 'type': 'string', 'description': 'Device identifier', 'required': False}, + ], + 'response_examples': { + 'success': json.dumps({ + "token": "abc123def456...", + "user": { + "id": 123, + "email": "user@example.com", + "fullname": "John Doe", + "is_verified": True, + "profile_image": None + } + }, indent=2) + } + }, + { + 'name': 'User Profile', + 'method': 'GET', + 'url': '/api/account/profile/', + 'description': 'Get current user profile information', + 'parameters': [ + {'name': 'Authorization', 'type': 'header', 'description': 'Token ', '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//', + '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//', + '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) + } + } + ] + } + } diff --git a/apps/api/views/swagger_views.py b/apps/api/views/swagger_views.py new file mode 100644 index 0000000..1f2dc15 --- /dev/null +++ b/apps/api/views/swagger_views.py @@ -0,0 +1,74 @@ +from django.shortcuts import render, redirect +from django.views import View +from django.contrib import messages +from django.contrib.admin.views.decorators import staff_member_required +from django.utils.decorators import method_decorator +from rest_framework.authtoken.models import Token + +@method_decorator(staff_member_required, name='dispatch') +class CustomSwaggerView(View): + """ + Custom Swagger UI view with authentication banner + Requires admin login to access + """ + def get(self, request): + context = { + 'swagger_spec_url': '/en/swagger.json', # Adjust based on your URL structure + 'request': request, + } + return render(request, 'swagger/ui.html', context) + +@method_decorator(staff_member_required, name='dispatch') +class SwaggerTokenAuthView(View): + """ + Token authentication management for Swagger + """ + def get(self, request): + context = { + 'current_token': request.session.get('swagger_token'), + 'user_info': request.session.get('swagger_user_info'), + } + return render(request, 'swagger/auth.html', context) + + def post(self, request): + token = request.POST.get('token', '').strip() + + if not token or len(token) != 40: + messages.error(request, 'Token must be exactly 40 characters long') + return redirect('swagger-token-auth') + + try: + token_obj = Token.objects.get(key=token) + user = token_obj.user + + if not user.is_active: + messages.error(request, 'User account is not active') + return redirect('swagger-token-auth') + + request.session['swagger_token'] = token + request.session['swagger_user_info'] = { + 'id': user.id, + 'email': user.email, + 'fullname': getattr(user, 'fullname', user.email), + 'is_staff': user.is_staff, + 'is_superuser': user.is_superuser, + 'user_type': 'User' + } + + messages.success(request, f'Successfully authenticated as {user.email}') + return redirect('schema-swagger-ui') + + except Token.DoesNotExist: + messages.error(request, 'Invalid token') + return redirect('swagger-token-auth') + +@staff_member_required +def clear_swagger_auth(request): + """Clear swagger authentication from session""" + if 'swagger_token' in request.session: + del request.session['swagger_token'] + if 'swagger_user_info' in request.session: + del request.session['swagger_user_info'] + + messages.success(request, 'Successfully logged out from Swagger') + return redirect('swagger-token-auth') diff --git a/apps/hadis/admin/category.py b/apps/hadis/admin/category.py index 5524941..e26b131 100644 --- a/apps/hadis/admin/category.py +++ b/apps/hadis/admin/category.py @@ -1,222 +1,53 @@ from django.contrib import admin from django.utils.translation import gettext_lazy as _ -from django.http import JsonResponse -from django.urls import path -from django.db import models -from django.contrib import messages -from django.http import HttpResponseRedirect -from django.urls import reverse - -from unfold.admin import ModelAdmin -from unfold.decorators import display - -from dj_category.admin import BaseCategoryAdmin -from ajaxdatatable.admin import AjaxDatatable -from django.db.models import Case, When, Value from django.utils.html import format_html +from unfold.admin import ModelAdmin +from unfold.decorators import display, action +from mptt.admin import DraggableMPTTAdmin -from apps.hadis.models import HadisCategory from utils.admin import project_admin_site +from ..models import HadisSect, HadisCategory -@admin.register(HadisCategory) -class HadisCategoryAdmin(BaseCategoryAdmin, ModelAdmin): - change_form_template = 'admin/hadiscategory/change_form.html' - change_list_template = 'admin/category_index.html' - +class HadisSectAdmin(ModelAdmin): + """Admin for HadisSect model""" + list_display = ('sect_type', 'title', 'is_active', 'order') + list_filter = ('sect_type', 'is_active') + search_fields = ('title',) + ordering = ('order',) + fieldsets = ( (None, { - 'fields': ('name', 'source_type', 'category_type', 'parent', 'is_active', 'order'), - 'classes': ('unfold-fieldset',), + 'fields': ('sect_type', 'title', 'is_active', 'order') }), ) - - search_fields = ['name'] - list_display = ['name', 'source_type_badge', 'category_type', 'parent', 'is_active', 'order'] - list_filter = ['source_type', 'category_type', 'is_active'] - - @display(description=_("Source Type")) - def source_type_badge(self, obj): - badge_classes = { - 'quran': 'unfold-badge unfold-badge--success', - 'hadith': 'unfold-badge unfold-badge--info', - 'book': 'unfold-badge unfold-badge--warning', - # Add more source types as needed - } - badge_class = badge_classes.get(obj.source_type, 'unfold-badge') - return format_html('{}', 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') - 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) +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') - # 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) \ No newline at end of file diff --git a/apps/hadis/admin/hadis.py b/apps/hadis/admin/hadis.py index 50bd6f3..39d9ca6 100644 --- a/apps/hadis/admin/hadis.py +++ b/apps/hadis/admin/hadis.py @@ -1,161 +1,209 @@ -# from django.contrib import admin -# from django.utils.translation import gettext_lazy as _ -# from dj_category.admin import BaseCategoryAdmin -# from ajaxdatatable.admin import AjaxDatatable -# from django.http import JsonResponse -# from django.urls import path -# from django.db.models import Q -# from django.utils.safestring import mark_safe -# from django.forms.widgets import RadioSelect - -# from apps.hadis.models import * -# from django import forms -# from utils.json_editor_field import JsonEditorWidget - -# # Define color choices -# COLOR_CHOICES = [ -# ('red', _('Red')), -# ('blue', _('Blue')), -# ('green', _('Green')), -# ('yellow', _('Yellow')), -# ('orange', _('Orange')), -# ('purple', _('Purple')), -# ('pink', _('Pink')), -# ('brown', _('Brown')), -# ('gray', _('Gray')), -# ('black', _('Black')), -# ] - -# class ColorRadioSelect(RadioSelect): -# template_name = 'admin/widgets/color_radio.html' -# option_template_name = 'admin/widgets/color_radio_option.html' - - -# def get_links_schema(): -# return { -# 'type': "array", -# 'format': 'table', -# 'title': ' ', -# 'items': { -# 'type': 'object', -# 'title': str(_('Link')), -# 'properties': { -# 'text': {'type': 'string', "format": "textarea",'title': str(_('text'))}, -# 'link': {'type': 'string', "format": "textarea", 'title': str(_('link'))}, -# } -# } -# } - -# class HadisOverviewForm(forms.ModelForm): -# status_color = forms.ChoiceField( -# choices=COLOR_CHOICES, -# widget=ColorRadioSelect(), -# required=False -# ) - -# class Meta: -# model = HadisOverview -# fields = '__all__' -# widgets = { -# 'links': JsonEditorWidget(attrs={'schema': get_links_schema}), -# } - - - - - -# @admin.register(HadisTag) -# class HadisTagAdmin(AjaxDatatable): -# list_display = ['title', 'status'] -# search_fields = ['title'] - - -# class ReferenceImageInline(admin.TabularInline): -# model = ReferenceImage -# extra = 1 -# verbose_name_plural = _('Reference Images') -# fields = ('thumbnail', 'priority') - - -# @admin.register(HadisReference) -# class HadisReferenceAdmin(AjaxDatatable): -# list_display = ['hadis', 'book', 'created_at'] -# list_filter = ['book'] -# search_fields = ['hadis__title', 'hadis__number', 'description'] -# autocomplete_fields = ['hadis', 'book'] -# readonly_fields = ['created_at'] -# inlines = [ReferenceImageInline] -# fieldsets = ( -# (None, { -# 'fields': ('hadis', 'book', 'description') -# }), -# ) - - - - -# @admin.register(HadisOverview) -# class HadisOverviewAdmin(AjaxDatatable): -# change_form_template = 'admin/hadisowerview_change_form.html' -# form = HadisOverviewForm -# ordering = ['hadis__number'] -# list_display = ['hadis', 'status', 'created_at'] -# search_fields = ['hadis__title', 'hadis__number', 'status_text',] -# autocomplete_fields = ['hadis', 'tags'] -# fieldsets = ( -# (None, { -# 'fields': ('hadis', 'status', 'status_color', 'status_text') -# }), -# (_('Reference Information'), { -# 'fields': ('address', 'share_link',), -# }), -# (_('Additional Information'), { -# 'fields': ('links', 'tags', 'created_at'), -# 'classes': ('collapse',), -# }), -# ) - - -# class HadisOverviewInline(admin.StackedInline): -# change_form_template = 'admin/hadisowerview_change_form.html' -# form = HadisOverviewForm -# model = HadisOverview -# autocomplete_fields = ['tags', ] -# can_delete = False -# verbose_name_plural = _('Hadis Overview') -# fieldsets = ( -# (None, { -# 'fields': ('status', 'status_color', 'status_text', 'address', 'share_link', 'links', 'tags',), -# }), -# ) -# extra = 1 -# min_num = 1 - - -# @admin.register(Hadis) -# class HadisAdmin(AjaxDatatable): -# # form = HadisForm -# list_display = ['number', 'title', 'category', 'status', 'created_at'] -# list_filter = ['status', 'category'] -# search_fields = ['title', 'text', 'number'] -# readonly_fields = ['created_at', 'updated_at'] -# autocomplete_fields = ['category'] -# inlines = [HadisOverviewInline] -# fieldsets = ( -# (None, { -# 'fields': ('number', 'title', 'category', 'status') -# }), -# (_('Content'), { -# 'fields': ('text', 'translation'), -# 'classes': ('collapse',), -# }), -# ) - - -# def get_form(self, request, obj=None, **kwargs): -# form = super().get_form(request, obj, **kwargs) -# if obj is None: -# form.base_fields['category'].widget.can_add_related = False - -# return form - +from django import forms +from django.contrib import admin +from django.utils.translation import gettext_lazy as _ +from unfold.admin import ModelAdmin, TabularInline +from unfold.contrib.forms.widgets import WysiwygWidget +from unfold.decorators import display, action +from utils.json_editor_field import JsonEditorWidget +import json + +from utils.admin import project_admin_site +from ..models import ( + Hadis, HadisReference, HadisTag, HadisStatus, ReferenceImage +) + + +# Custom Forms for JSON Fields +class HadisAdminForm(forms.ModelForm): + """Custom form for Hadis with JSON editor widgets""" + + class Meta: + model = Hadis + fields = '__all__' + widgets = { + 'explanation': WysiwygWidget(), + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Schema for translation JSON field + translation_schema = { + "type": "array", + "title": "Translations", + "items": { + "type": "object", + "title": "Translation", + "properties": { + "language_code": { + "type": "string", + "title": "Language Code", + "enum": ["en", "fa", "ar", "ur"], + "options": { + "enum_titles": ["English", "Persian", "Arabic", "Urdu"] + } + }, + "title": { + "type": "string", + "title": "Translation Text" + } + }, + "required": ["language_code", "title"] + } + } + + # Schema for links JSON field (array of objects with title and link) + links_schema = { + "type": "array", + "title": "Links", + "items": { + "type": "object", + "title": "Link", + "properties": { + "title": { + "type": "string", + "title": "Link Title" + }, + "link": { + "type": "string", + "title": "URL", + "format": "uri" + } + }, + "required": ["title", "link"] + } + } + + # Apply JSON editor widgets + self.fields['translation'].widget = JsonEditorWidget(attrs={ + 'schema': json.dumps(translation_schema), + 'title': 'Translations' + }) + + self.fields['links'].widget = JsonEditorWidget(attrs={ + 'schema': json.dumps(links_schema), + 'title': 'Links' + }) + + +# Inline Admin Classes +class ReferenceImageInline(TabularInline): + """Inline for ReferenceImage in HadisReference admin""" + model = ReferenceImage + extra = 0 + fields = ('thumbnail', 'priority') + ordering = ('priority',) + + +class HadisReferenceInline(TabularInline): + """Inline for HadisReference in Hadis admin""" + model = HadisReference + extra = 0 + fields = ('book', 'description') + readonly_fields = ('created_at',) + + +# Main Admin Classes +class HadisTagAdmin(ModelAdmin): + """Admin for HadisTag model""" + list_display = ('title', 'status', 'created_at') + list_filter = ('status', 'created_at') + search_fields = ('title',) + readonly_fields = ('created_at', 'updated_at') + + fieldsets = ( + (None, { + 'fields': ('title', 'status') + }), + (_('Timestamps'), { + 'fields': ('created_at', 'updated_at'), + 'classes': ('collapse',) + }), + ) + + +class HadisStatusAdmin(ModelAdmin): + """Admin for HadisStatus model""" + list_display = ('title', 'color', 'order') + list_filter = ('color',) + search_fields = ('title',) + ordering = ('order',) + + fieldsets = ( + (None, { + 'fields': ('title', 'color', 'order') + }), + ) + + +class HadisAdmin(ModelAdmin): + """Admin for Hadis model""" + form = HadisAdminForm + list_display = ('number', 'title', 'category', 'status', 'hadis_status', 'created_at') + list_filter = ('status', 'hadis_status', 'category', 'created_at') + search_fields = ('title', 'text', 'category__title') + readonly_fields = ('created_at', 'updated_at', 'share_link') + ordering = ('category', 'number') + inlines = [HadisReferenceInline] + filter_horizontal = ('tags',) + + fieldsets = ( + (None, { + 'fields': ('category', 'number', 'title', 'status') + }), + (_('Content'), { + 'fields': ('text', 'translation', 'explanation') + }), + (_('Status & Classification'), { + 'fields': ('hadis_status', 'hadis_status_text', 'tags') + }), + (_('Additional Information'), { + 'fields': ('address', 'links', 'share_link'), + 'classes': ('collapse',) + }), + (_('Timestamps'), { + 'fields': ('created_at', 'updated_at'), + 'classes': ('collapse',) + }), + ) + + +class HadisReferenceAdmin(ModelAdmin): + """Admin for HadisReference model""" + list_display = ('hadis', 'book', 'created_at') + list_filter = ('created_at', 'book') + search_fields = ('hadis__title', 'book__title', 'description') + readonly_fields = ('created_at',) + inlines = [ReferenceImageInline] + + fieldsets = ( + (None, { + 'fields': ('hadis', 'book', 'description') + }), + (_('Timestamps'), { + 'fields': ('created_at',), + 'classes': ('collapse',) + }), + ) + + +class ReferenceImageAdmin(ModelAdmin): + """Admin for ReferenceImage model""" + list_display = ('reference', 'thumbnail', 'priority') + list_filter = ('priority',) + search_fields = ('reference__hadis__title', 'reference__book__title') + ordering = ('reference', 'priority') + + fieldsets = ( + (None, { + 'fields': ('reference', 'thumbnail', 'priority') + }), + ) + + +# Register models with the custom admin site +project_admin_site.register(HadisTag, HadisTagAdmin) +project_admin_site.register(HadisStatus, HadisStatusAdmin) +project_admin_site.register(Hadis, HadisAdmin) +project_admin_site.register(HadisReference, HadisReferenceAdmin) +project_admin_site.register(ReferenceImage, ReferenceImageAdmin) \ No newline at end of file diff --git a/apps/hadis/admin/transmitter.py b/apps/hadis/admin/transmitter.py index e69de29..4f29233 100644 --- a/apps/hadis/admin/transmitter.py +++ b/apps/hadis/admin/transmitter.py @@ -0,0 +1,66 @@ +from django.contrib import admin +from django.utils.translation import gettext_lazy as _ +from unfold.admin import ModelAdmin, TabularInline +from unfold.decorators import display, action + +from utils.admin import project_admin_site +from ..models import Transmitters, HadisTransmitter + + +class HadisTransmitterInline(TabularInline): + """Inline for HadisTransmitter in Transmitters admin""" + model = HadisTransmitter + extra = 0 + fields = ('hadis', 'order', 'status', 'is_gap') + readonly_fields = ('created_at',) + + +class TransmittersAdmin(ModelAdmin): + """Admin for Transmitters model""" + list_display = ('full_name', 'birth_year_hijri', 'death_year_hijri') + list_filter = ('birth_year_hijri', 'death_year_hijri') + search_fields = ('full_name', 'description') + readonly_fields = ('created_at', 'updated_at') + inlines = [HadisTransmitterInline] + + fieldsets = ( + (None, { + 'fields': ('full_name', 'birth_year_hijri', 'death_year_hijri') + }), + (_('Additional Information'), { + 'fields': ('description', 'thumbnail'), + 'classes': ('collapse',) + }), + (_('Timestamps'), { + 'fields': ('created_at', 'updated_at'), + 'classes': ('collapse',) + }), + ) + + +class HadisTransmitterAdmin(ModelAdmin): + """Admin for HadisTransmitter model""" + list_display = ('hadis', 'transmitter', 'order', 'status', 'is_gap', 'created_at') + list_filter = ('status', 'is_gap', 'created_at') + search_fields = ('hadis__title', 'transmitter__full_name') + readonly_fields = ('created_at',) + ordering = ('hadis', 'order') + + fieldsets = ( + (None, { + 'fields': ('hadis', 'transmitter', 'order') + }), + (_('Status & Gap Information'), { + 'fields': ('status', 'is_gap'), + 'description': _('Use "Is Gap" to mark missing links in the transmission chain') + }), + (_('Timestamps'), { + 'fields': ('created_at',), + 'classes': ('collapse',) + }), + ) + + +# Register models with the custom admin site +project_admin_site.register(Transmitters, TransmittersAdmin) +project_admin_site.register(HadisTransmitter, HadisTransmitterAdmin) \ No newline at end of file diff --git a/apps/hadis/doc.py b/apps/hadis/doc.py deleted file mode 100644 index c2dda6f..0000000 --- a/apps/hadis/doc.py +++ /dev/null @@ -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." - } -) \ No newline at end of file diff --git a/apps/hadis/docs.py b/apps/hadis/docs.py new file mode 100644 index 0000000..e27d753 --- /dev/null +++ b/apps/hadis/docs.py @@ -0,0 +1,341 @@ +from drf_yasg.utils import swagger_auto_schema +from drf_yasg import openapi +from rest_framework import status + + +# Swagger documentation for HadisSectListView +hadis_sect_list_swagger = swagger_auto_schema( + operation_description="Get list of all active Hadis sects grouped by sect type (Shia/Sunni)", + operation_summary="List Hadis Sects", + tags=['Hadis'], + responses={ + status.HTTP_200_OK: openapi.Response( + description="List of hadis sects grouped by type with count", + examples={ + "application/json": { + "count": 4, + "results": { + "shia": [ + { + "id": 1, + "title": "Twelver Shia", + "seo_field": None + }, + { + "id": 2, + "title": "Ismaili Shia", + "seo_field": None + } + ], + "sunni": [ + { + "id": 3, + "title": "Hanafi", + "seo_field": None + }, + { + "id": 4, + "title": "Maliki", + "seo_field": None + } + ] + } + } + } + ), + status.HTTP_500_INTERNAL_SERVER_ERROR: openapi.Response( + description="Internal server error" + ) + } +) + + +# Swagger documentation for HadisCategoryTreeView +hadis_category_tree_swagger = swagger_auto_schema( + operation_description="Get hierarchical tree structure of Hadis categories for a specific sect, grouped by source type (Quran/Hadith)", + operation_summary="Get Hadis Category Tree by Sect", + tags=['Hadis'], + manual_parameters=[ + openapi.Parameter( + 'sect_id', + openapi.IN_PATH, + description="ID of the Hadis sect", + type=openapi.TYPE_INTEGER, + required=True + ) + ], + responses={ + status.HTTP_200_OK: openapi.Response( + description="Hierarchical tree structure of categories with total count", + examples={ + "application/json": { + "count": 6, + "results": { + "quran": [ + { + "id": 1, + "name": "Tafsir", + "hadis_count": 150, + "has_hadis": False, + "order": 1, + "xmind_file": "http://example.com/media/xmind/tafsir.xmind", + "has_xmind_file": True, + "children": [ + { + "id": 2, + "name": "Surah Al-Fatiha", + "hadis_count": 25, + "has_hadis": True, + "order": 1, + "xmind_file": None, + "has_xmind_file": False, + "children": [] + }, + { + "id": 3, + "name": "Surah Al-Baqarah", + "hadis_count": 125, + "has_hadis": True, + "order": 2, + "xmind_file": "http://example.com/media/xmind/baqarah.xmind", + "has_xmind_file": True, + "children": [] + } + ] + } + ], + "hadith": [ + { + "id": 4, + "name": "Sahih Bukhari", + "hadis_count": 300, + "has_hadis": False, + "order": 1, + "xmind_file": "http://example.com/media/xmind/bukhari.xmind", + "has_xmind_file": True, + "children": [ + { + "id": 5, + "name": "Book of Faith", + "hadis_count": 50, + "has_hadis": True, + "order": 1, + "xmind_file": None, + "has_xmind_file": False, + "children": [] + }, + { + "id": 6, + "name": "Book of Prayer", + "hadis_count": 250, + "has_hadis": True, + "order": 2, + "xmind_file": "http://example.com/media/xmind/prayer.xmind", + "has_xmind_file": True, + "children": [] + } + ] + } + ] + } + } + } + ), + status.HTTP_404_NOT_FOUND: openapi.Response( + description="Sect not found" + ), + status.HTTP_500_INTERNAL_SERVER_ERROR: openapi.Response( + description="Internal server error" + ) + } +) + + +# Swagger documentation for HadisListView +hadis_list_swagger = swagger_auto_schema( + operation_description="Get paginated list of Hadis for a specific category with translations based on request language", + operation_summary="List Hadis by Category", + tags=['Hadis'], + manual_parameters=[ + openapi.Parameter( + 'category_id', + openapi.IN_PATH, + description="ID of the Hadis category", + type=openapi.TYPE_INTEGER, + required=True + ), + openapi.Parameter( + 'page', + openapi.IN_QUERY, + description="Page number for pagination", + type=openapi.TYPE_INTEGER, + required=False + ), + openapi.Parameter( + 'Accept-Language', + openapi.IN_HEADER, + description="Language code for translations (en, fa, ar, ur)", + type=openapi.TYPE_STRING, + required=False, + default='en' + ) + ], + responses={ + status.HTTP_200_OK: openapi.Response( + description="Paginated list of hadis", + examples={ + "application/json": { + "count": 150, + "next": "http://example.com/api/hadis/category/1/hadis/?page=2", + "previous": None, + "results": [ + { + "id": 1, + "number": 1, + "title": "The first hadis about faith", + "category": { + "id": 1, + "title": "Book of Faith" + }, + "translation": "This is the English translation of the hadis" + }, + { + "id": 2, + "number": 2, + "title": "The second hadis about prayer", + "category": { + "id": 1, + "title": "Book of Faith" + }, + "translation": "This is the English translation of the second hadis" + } + ] + } + } + ), + status.HTTP_404_NOT_FOUND: openapi.Response( + description="Category not found" + ), + status.HTTP_500_INTERNAL_SERVER_ERROR: openapi.Response( + description="Internal server error" + ) + } +) + + +hadis_detail_swagger = swagger_auto_schema( + operation_summary="Get Hadis Details", + operation_description="Retrieve detailed information about a specific hadis including status, tags, transmitters, and references", + tags=['Hadis'], + responses={ + 200: openapi.Response( + description="Hadis details retrieved successfully", + schema=openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={ + 'id': openapi.Schema(type=openapi.TYPE_INTEGER, description='Hadis ID'), + 'number': openapi.Schema(type=openapi.TYPE_INTEGER, description='Hadis number'), + 'title': openapi.Schema(type=openapi.TYPE_STRING, description='Hadis title'), + 'text': openapi.Schema(type=openapi.TYPE_STRING, description='Arabic text of hadis'), + 'translation': openapi.Schema(type=openapi.TYPE_STRING, description='Translation in request language'), + 'explanation': openapi.Schema(type=openapi.TYPE_STRING, description='Detailed explanation'), + 'address': openapi.Schema(type=openapi.TYPE_STRING, description='Source address'), + 'hadis_status_text': openapi.Schema(type=openapi.TYPE_STRING, description='Status description'), + 'links': openapi.Schema( + type=openapi.TYPE_ARRAY, + items=openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={ + 'title': openapi.Schema(type=openapi.TYPE_STRING), + 'link': openapi.Schema(type=openapi.TYPE_STRING) + } + ), + description='Related links' + ), + 'status': openapi.Schema(type=openapi.TYPE_BOOLEAN, description='Active status'), + 'category': openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={ + 'id': openapi.Schema(type=openapi.TYPE_INTEGER), + 'title': openapi.Schema(type=openapi.TYPE_STRING), + 'category_type': openapi.Schema(type=openapi.TYPE_STRING) + } + ), + 'hadis_status': openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={ + 'id': openapi.Schema(type=openapi.TYPE_INTEGER), + 'title': openapi.Schema(type=openapi.TYPE_STRING), + 'color': openapi.Schema(type=openapi.TYPE_STRING) + } + ), + 'tags': openapi.Schema( + type=openapi.TYPE_ARRAY, + items=openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={ + 'id': openapi.Schema(type=openapi.TYPE_INTEGER), + 'title': openapi.Schema(type=openapi.TYPE_STRING) + } + ) + ), + 'transmitters': openapi.Schema( + type=openapi.TYPE_ARRAY, + items=openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={ + 'id': openapi.Schema(type=openapi.TYPE_INTEGER), + 'order': openapi.Schema(type=openapi.TYPE_INTEGER), + 'is_gap': openapi.Schema(type=openapi.TYPE_BOOLEAN), + 'transmitter': openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={ + 'id': openapi.Schema(type=openapi.TYPE_INTEGER), + 'full_name': openapi.Schema(type=openapi.TYPE_STRING), + 'birth_year_hijri': openapi.Schema(type=openapi.TYPE_INTEGER), + 'death_year_hijri': openapi.Schema(type=openapi.TYPE_INTEGER), + 'description': openapi.Schema(type=openapi.TYPE_STRING) + } + ) + } + ) + ), + 'references': openapi.Schema( + type=openapi.TYPE_ARRAY, + items=openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={ + 'id': openapi.Schema(type=openapi.TYPE_INTEGER), + 'page_number': openapi.Schema(type=openapi.TYPE_STRING), + 'hadis_number_in_book': openapi.Schema(type=openapi.TYPE_STRING), + 'description': openapi.Schema(type=openapi.TYPE_STRING), + 'book': openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={ + 'id': openapi.Schema(type=openapi.TYPE_INTEGER), + 'title': openapi.Schema(type=openapi.TYPE_STRING), + 'summary_title': openapi.Schema(type=openapi.TYPE_STRING), + 'publisher': openapi.Schema(type=openapi.TYPE_STRING), + 'year_of_publication': openapi.Schema(type=openapi.TYPE_STRING) + } + ), + 'images': openapi.Schema( + type=openapi.TYPE_ARRAY, + items=openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={ + 'id': openapi.Schema(type=openapi.TYPE_INTEGER), + 'thumbnail': openapi.Schema(type=openapi.TYPE_STRING), + 'priority': openapi.Schema(type=openapi.TYPE_INTEGER) + } + ) + ) + } + ) + ) + } + ) + ), + 404: openapi.Response(description="Hadis not found") + } +) diff --git a/apps/hadis/migrations/0002_hadissect_hadisstatus_alter_hadis_options_and_more.py b/apps/hadis/migrations/0002_hadissect_hadisstatus_alter_hadis_options_and_more.py new file mode 100644 index 0000000..77bc3f2 --- /dev/null +++ b/apps/hadis/migrations/0002_hadissect_hadisstatus_alter_hadis_options_and_more.py @@ -0,0 +1,217 @@ +# Generated by Django 5.1.8 on 2025-07-04 11:34 + +import django.db.models.deletion +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('hadis', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='HadisSect', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('sect_type', models.CharField(choices=[('shia', 'Shia'), ('sunni', 'Sunni')], max_length=10, unique=True, verbose_name='Sect Name')), + ('title', models.CharField(max_length=256, verbose_name='Name')), + ('is_active', models.BooleanField(default=True, verbose_name='Is Active')), + ('order', models.IntegerField(default=0, verbose_name='order')), + ], + options={ + 'verbose_name': 'Hadis Sect', + 'verbose_name_plural': 'Hadis Sects', + 'ordering': ('order',), + }, + ), + migrations.CreateModel( + name='HadisStatus', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=119, verbose_name='title')), + ('color', models.CharField(choices=[('red', 'Red'), ('green', 'Green'), ('blue', 'Blue'), ('yellow', 'Yellow'), ('orange', 'Orange'), ('purple', 'Purple'), ('gray', 'Gray')], max_length=20, verbose_name='color')), + ('order', models.IntegerField(default=0, verbose_name='order')), + ], + options={ + 'verbose_name': 'hadis status', + 'verbose_name_plural': 'hadis statuses', + 'ordering': ('order',), + }, + ), + migrations.AlterModelOptions( + name='hadis', + options={'ordering': ('category', 'number'), 'verbose_name': 'hadis', 'verbose_name_plural': 'hadises'}, + ), + migrations.AlterModelOptions( + name='transmitters', + options={'ordering': ('full_name',), 'verbose_name': 'Transmitter', 'verbose_name_plural': 'Transmitters'}, + ), + migrations.RemoveField( + model_name='hadiscategory', + name='category_type', + ), + migrations.RemoveField( + model_name='hadiscategory', + name='created_at', + ), + migrations.RemoveField( + model_name='hadiscategory', + name='is_active', + ), + migrations.RemoveField( + model_name='hadiscategory', + name='name', + ), + migrations.RemoveField( + model_name='hadistransmitter', + name='description', + ), + migrations.RemoveField( + model_name='transmitters', + name='status', + ), + migrations.RemoveField( + model_name='transmitters', + name='status_color', + ), + migrations.AddField( + model_name='hadis', + name='address', + field=models.TextField(blank=True, null=True, verbose_name='address'), + ), + migrations.AddField( + model_name='hadis', + name='explanation', + field=models.TextField(blank=True, null=True, verbose_name='explanation'), + ), + migrations.AddField( + model_name='hadis', + name='hadis_status_text', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='hadis status text'), + ), + migrations.AddField( + model_name='hadis', + name='links', + field=models.JSONField(blank=True, default=dict, null=True, verbose_name='links'), + ), + migrations.AddField( + model_name='hadis', + name='share_link', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='share link'), + ), + migrations.AddField( + model_name='hadis', + name='tags', + field=models.ManyToManyField(blank=True, related_name='hadis_overview', to='hadis.hadistag', verbose_name='tags'), + ), + migrations.AddField( + model_name='hadiscategory', + name='title', + field=models.CharField(default='Default Category', max_length=256, verbose_name='Title'), + preserve_default=False, + ), + migrations.AddField( + model_name='hadiscategory', + name='xmind_file', + field=models.FileField(blank=True, null=True, upload_to='hadis/xmind_files/', verbose_name='xmind file'), + ), + migrations.AddField( + model_name='hadistag', + name='created_at', + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='created at'), + preserve_default=False, + ), + migrations.AddField( + model_name='hadistag', + name='updated_at', + field=models.DateTimeField(auto_now=True, verbose_name='updated at'), + ), + migrations.AddField( + model_name='hadistransmitter', + name='is_gap', + field=models.BooleanField(default=False, help_text='Check this if this represents a gap in the transmission chain', verbose_name='Is Gap'), + ), + migrations.AddField( + model_name='transmitters', + name='created_at', + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='created at'), + preserve_default=False, + ), + migrations.AddField( + model_name='transmitters', + name='updated_at', + field=models.DateTimeField(auto_now=True, verbose_name='updated at'), + ), + migrations.AlterField( + model_name='hadis', + name='category', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='hadis.hadiscategory', verbose_name='category'), + ), + migrations.AlterField( + model_name='hadis', + name='number', + field=models.PositiveIntegerField(default=1, verbose_name='number'), + ), + migrations.AlterField( + model_name='hadis', + name='title', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='title'), + ), + migrations.AlterField( + model_name='hadis', + name='translation', + field=models.JSONField(default=list, verbose_name='translation'), + ), + migrations.AlterField( + model_name='hadiscategory', + name='source_type', + field=models.CharField(choices=[('quran', 'Quran'), ('hadith', 'Hadith')], max_length=10, verbose_name='Source Type'), + ), + migrations.AlterField( + model_name='hadistransmitter', + name='transmitter', + field=models.ForeignKey(blank=True, help_text='Leave empty if this represents a gap in the chain', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='hadises', to='hadis.transmitters', verbose_name='transmitter'), + ), + migrations.AlterField( + model_name='referenceimage', + name='thumbnail', + field=models.ImageField(blank=True, null=True, upload_to='hadis/reference_images/', verbose_name='thumbnail'), + ), + migrations.AlterField( + model_name='transmitters', + name='birth_year_hijri', + field=models.IntegerField(blank=True, null=True, verbose_name='Birth Year (Hijri)'), + ), + migrations.AlterField( + model_name='transmitters', + name='death_year_hijri', + field=models.IntegerField(blank=True, null=True, verbose_name='Death Year (Hijri)'), + ), + migrations.AlterField( + model_name='transmitters', + name='full_name', + field=models.CharField(max_length=255, verbose_name='full name'), + ), + migrations.AddField( + model_name='hadiscategory', + name='sect', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.PROTECT, to='hadis.hadissect', verbose_name='Sect'), + preserve_default=False, + ), + migrations.AddField( + model_name='hadis', + name='hadis_status', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='hadis.hadisstatus', verbose_name='hadis status'), + ), + migrations.AddField( + model_name='hadistransmitter', + name='status', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='transmitters', to='hadis.hadisstatus', verbose_name='status'), + ), + migrations.DeleteModel( + name='HadisOverview', + ), + ] diff --git a/apps/hadis/models/category.py b/apps/hadis/models/category.py index 7fde53e..70189cf 100644 --- a/apps/hadis/models/category.py +++ b/apps/hadis/models/category.py @@ -1,34 +1,45 @@ from django.db import models from django.utils.translation import gettext_lazy as _ from django.core.exceptions import ValidationError -from dj_category.models import BaseCategoryAbstract +from mptt.models import MPTTModel, TreeForeignKey -class HadisCategory(BaseCategoryAbstract): - class SourceType(models.TextChoices): +class HadisSect(models.Model): + class SectType(models.TextChoices): SHIA = 'shia', _('Shia') SUNNI = 'sunni', _('Sunni') - class ContentType(models.TextChoices): + sect_type = models.CharField(max_length=10, choices=SectType.choices, unique=True, verbose_name=_('Sect Name')) + title = models.CharField(max_length=256, verbose_name=_('Name')) + is_active = models.BooleanField(default=True, verbose_name=_('Is Active')) + order = models.IntegerField(default=0, verbose_name=_('order')) + + def __str__(self): + return f"{self.sect_type}: {self.title}" + + + class Meta: + verbose_name = _('Hadis Sect') + verbose_name_plural = _('Hadis Sects') + ordering = ('order',) + + +class HadisCategory(MPTTModel): + class SourceType(models.TextChoices): QURAN = 'quran', _('Quran') HADITH = 'hadith', _('Hadith') - class LevelChoices(models.IntegerChoices): - LEVEL_1 = 1, _('Level 1 (Root)') - LEVEL_2 = 2, _('Level 2 (Child)') - LEVEL_3 = 3, _('Level 3 (Grandchild)') - - source_type = models.CharField(max_length=10, choices=SourceType.choices, default=SourceType.SHIA, verbose_name=_('Source Type'), blank=True) - category_type = models.CharField(max_length=10, choices=ContentType.choices, verbose_name=_('Category Content Type'), blank=True, null=True) - name = models.CharField(max_length=355, verbose_name=_('name')) + parent = TreeForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, related_name='children') + sect = models.ForeignKey(HadisSect, on_delete=models.PROTECT, verbose_name=_('Sect'), null=False, blank=False) + source_type = models.CharField(max_length=10, choices=SourceType.choices, verbose_name=_('Source Type')) + title = models.CharField(max_length=256, verbose_name=_('Title')) order = models.IntegerField(default=0, verbose_name=_('order')) + xmind_file = models.FileField(upload_to='hadis/xmind_files/', verbose_name=_('xmind file'), null=True, blank=True) slug = None content_type = None language = None language_id = None - # This field is not stored in the database, it's only used for the form - level_choice = None class Meta: verbose_name = _('Hadis Category') @@ -36,70 +47,5 @@ class HadisCategory(BaseCategoryAbstract): ordering = ('order',) def __str__(self): - return f'<{str(self.level_p)}>{self.name}' - - def __repr__(self): - return f'<{str(self.level_p)}>{self.name}' - - def clean(self): - super().clean() - - # Skip validation for new objects that haven't been saved yet - # This allows the admin form to set these values properly - if self.pk is None: - return - - # For existing objects, apply the validation rules - if self.level_p == 1 and self.category_type: - raise ValidationError(_("Level 1 cannot have content type")) - - if self.level_p == 2 and not self.category_type: - raise ValidationError(_("Level 2 must have content type")) - - if self.level_p == 3 and (self.source_type or self.category_type): - raise ValidationError(_("Level 3 cannot have source/content type")) - - - def save(self, *args, **kwargs): - self.clean() - - # Get the level from the parent structure - level = self.level_p - - # Apply level-specific logic - # if level == 2 and self.parent: - # For level 2, inherit source_type from parent - # self.source_type = self.parent.source_type - # elif level == 3: - # For level 3, inherit both from parent - # if self.parent and self.parent.parent: - # self.source_type = self.parent.source_type - # self.category_type = self.parent.category_type - - # Call the parent class's save method - super().save(*args, **kwargs) - - @property - def level_p(self): - if not self.parent: - return 1 - elif not self.parent.parent: - return 2 - else: - return 3 - def get_level_info(self): - info = { - 'level': self.level_p, - 'source_type': None, - 'category_type': None, - } - if self.level_p == 1: - info['source_type'] = self.source_type - elif self.level_p == 2: - info['source_type'] = self.parent.source_type - info['category_type'] = self.category_type - return info - - - - + return f"{self.sect.sect_type}: {self.source_type} - {self.title}" + \ No newline at end of file diff --git a/apps/hadis/models/hadis.py b/apps/hadis/models/hadis.py index cbe94d9..4cb3f82 100644 --- a/apps/hadis/models/hadis.py +++ b/apps/hadis/models/hadis.py @@ -1,58 +1,96 @@ - - from django.db import models +from django.db.models import F from django.utils.translation import gettext_lazy as _ -from django.core.exceptions import ValidationError -from filer.fields.image import FilerImageField +from django.conf import settings class HadisTag(models.Model): title = models.CharField(max_length=355, verbose_name=_('title')) status = models.BooleanField(default=True, verbose_name=_('status')) + created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at')) + updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at')) def __str__(self): return f"{self.title}" +class HadisStatus(models.Model): + class ColorChoices(models.TextChoices): + RED = 'red', _('Red') + GREEN = 'green', _('Green') + BLUE = 'blue', _('Blue') + YELLOW = 'yellow', _('Yellow') + ORANGE = 'orange', _('Orange') + PURPLE = 'purple', _('Purple') + GRAY = 'gray', _('Gray') + + title = models.CharField(max_length=119, verbose_name=_('title')) + + color = models.CharField(max_length=20, choices=ColorChoices.choices, verbose_name=_('color')) + order = models.IntegerField(default=0, verbose_name=_('order')) + + def __str__(self): + return self.title + + class Meta: + verbose_name = _('hadis status') + verbose_name_plural = _('hadis statuses') + ordering = ('order',) + + class Hadis(models.Model): - number = models.PositiveIntegerField(verbose_name=_('number'), unique=True) - title = models.CharField(max_length=355, verbose_name=_('title')) + category = models.ForeignKey("hadis.HadisCategory", on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_('category')) + number = models.PositiveIntegerField(verbose_name=_('number'), default=1) + title = models.CharField(max_length=255, verbose_name=_('title'), null=True, blank=True) text = models.TextField(verbose_name=_('text')) - translation = models.TextField(verbose_name=_('translation'), blank=True, default='') + translation = models.JSONField(verbose_name=_('translation'), default=list) + status = models.BooleanField(default=True, verbose_name=_('visibility')) - category = models.ForeignKey("hadis.HadisCategory", null=True, on_delete=models.SET_NULL, verbose_name=_('category'), ) + hadis_status = models.ForeignKey(HadisStatus, on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_('hadis status')) + hadis_status_text = models.CharField(max_length=255, verbose_name=_('hadis status text'), null=True, blank=True) + address = models.TextField(verbose_name=_('address'), null=True, blank=True) + links = models.JSONField(verbose_name=_('links'), null=True, blank=True, default=dict) + tags = models.ManyToManyField("HadisTag", related_name="hadis_overview", verbose_name=_('tags'), blank=True) + + share_link = models.CharField(max_length=255, verbose_name=_('share link'), null=True, blank=True) + explanation = models.TextField(verbose_name=_('explanation'), null=True, blank=True) - status = models.BooleanField(default=True, verbose_name=_('visibility')) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at')) updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at')) def __str__(self): - return f"<{self.number}> {self.title[:32]}" + return f"{self.number} - {self.title}" if self.title else f"Hadis {self.number}" + + def get_translation(self, lang): + """ + Get translation for a specific language + """ + if not self.translation or not isinstance(self.translation, list): + return None + + for tr in self.translation: + if isinstance(tr, dict) and tr.get('language_code') == lang: + return tr.get('title', '') - @property - def get_tags(self): - return self.tags.all().order_by('hadistagrelation__priority') + for tr in self.translation: + if isinstance(tr, dict) and tr.get('language_code') == 'en': + return tr.get('title', '') + return None + + def save(self, *args, **kwargs): + # ساخت share_link قبل از ذخیره + if not self.share_link: + self.share_link = f"{settings.SITE_DOMAIN}/hadis/{self.id}" + super().save(*args, **kwargs) class Meta: verbose_name = _('hadis') verbose_name_plural = _('hadises') + ordering = ('category', 'number') -class HadisOverview(models.Model): - hadis = models.OneToOneField(Hadis, on_delete=models.CASCADE, primary_key=True) - status = models.CharField(max_length=50, verbose_name=_('status')) - status_color = models.CharField(max_length=25, verbose_name=_('Display Status Color')) - status_text = models.TextField(verbose_name=_('Status Text'), null=True, blank=True) - address = models.TextField(verbose_name=_('address'), null=True, blank=True) - links = models.JSONField(verbose_name=_('title'), null=True, blank=True, default=dict) - tags = models.ManyToManyField("HadisTag", related_name="hadises", verbose_name=_('tags'), blank=True) - share_link = models.CharField(max_length=255, verbose_name=_('share link'), null=True, blank=True) - explanation = models.TextField(verbose_name=_('explanation'), null=True, blank=True) - - created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at')) - class HadisReference(models.Model): hadis = models.ForeignKey( @@ -69,16 +107,13 @@ class HadisReference(models.Model): verbose_name = _('Hadis Reference') verbose_name_plural = _('Hadis References') unique_together = ('hadis', 'book') - + def __str__(self): - return f'{self.hadis.number}-{self.book.title}' + return f'{self.hadis.number}-{self.book.title if self.book else "No Book"}' class ReferenceImage(models.Model): reference = models.ForeignKey(HadisReference, verbose_name="Hadis Reference", on_delete=models.CASCADE) - thumbnail = FilerImageField( - related_name='+', on_delete=models.PROTECT, null=True, blank=True, - verbose_name=_('thumbnail') - ) + thumbnail = models.ImageField(upload_to='hadis/reference_images/', null=True, blank=True, verbose_name=_('thumbnail')) priority = models.IntegerField( default=0, verbose_name=_("Priority"), @@ -101,4 +136,4 @@ class ReferenceImage(models.Model): ).update(priority=F('priority') + 1) super().save(*args, **kwargs) - + diff --git a/apps/hadis/models/transmitter.py b/apps/hadis/models/transmitter.py index 5ec2e0b..6233ec9 100644 --- a/apps/hadis/models/transmitter.py +++ b/apps/hadis/models/transmitter.py @@ -2,27 +2,37 @@ from django.db import models from django.utils.translation import gettext_lazy as _ -from django.core.exceptions import ValidationError from filer.fields.image import FilerImageField class Transmitters(models.Model): - full_name = models.CharField(max_length=255) - birth_year_hijri = models.IntegerField(verbose_name="Birth Year (Hijri)") - death_year_hijri = models.IntegerField(verbose_name="Death Year (Hijri)") - description = models.TextField(blank=True, null=True, verbose_name="Description") - status = models.CharField(max_length=50, verbose_name=_('status')) - status_color = models.CharField(max_length=25, verbose_name=_('Display Status Color')) - thumbnail = FilerImageField(related_name="+", on_delete=models.CASCADE, help_text=_( - 'image allowed' - ), null=True, blank=True) + full_name = models.CharField(max_length=255, verbose_name=_('full name')) + birth_year_hijri = models.IntegerField(verbose_name=_("Birth Year (Hijri)"), null=True, blank=True) + death_year_hijri = models.IntegerField(verbose_name=_("Death Year (Hijri)"), null=True, blank=True) + description = models.TextField(blank=True, null=True, verbose_name=_("Description")) + thumbnail = FilerImageField( + related_name="+", + on_delete=models.CASCADE, + help_text=_('image allowed'), + null=True, + blank=True + ) + created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at')) + updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at')) + + class Meta: + verbose_name = _('Transmitter') + verbose_name_plural = _('Transmitters') + ordering = ('full_name',) def __str__(self): return self.full_name + class HadisTransmitter(models.Model): + hadis = models.ForeignKey( "hadis.Hadis", on_delete=models.CASCADE, @@ -33,14 +43,30 @@ class HadisTransmitter(models.Model): Transmitters, on_delete=models.CASCADE, verbose_name=_('transmitter'), - related_name='hadises' + related_name='hadises', + null=True, + blank=True, + help_text=_('Leave empty if this represents a gap in the chain') + ) + status = models.ForeignKey( + "hadis.HadisStatus", + on_delete=models.SET_NULL, + verbose_name=_('status'), + related_name='transmitters', + null=True, + blank=True ) - description = models.TextField(verbose_name=_('description'), blank=True, null=True) order = models.PositiveIntegerField( default=0, verbose_name=_('Order'), help_text=_('Order in the chain of transmission') ) + is_gap = models.BooleanField( + default=False, + verbose_name=_('Is Gap'), + help_text=_('Check this if this represents a gap in the transmission chain') + ) + created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at')) class Meta: @@ -50,4 +76,4 @@ class HadisTransmitter(models.Model): unique_together = ('hadis', 'transmitter', 'order') def __str__(self): - return f'{self.hadis.number} - {self.transmitter.full_name} ({self.order})' + return f'{self.hadis.number} - {self.transmitter.full_name if self.transmitter else "Gap"} ({self.order})' diff --git a/apps/hadis/serializers.py b/apps/hadis/serializers.py deleted file mode 100644 index 5c43283..0000000 --- a/apps/hadis/serializers.py +++ /dev/null @@ -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 \ No newline at end of file diff --git a/apps/hadis/serializers/__init__.py b/apps/hadis/serializers/__init__.py new file mode 100644 index 0000000..eef37be --- /dev/null +++ b/apps/hadis/serializers/__init__.py @@ -0,0 +1,2 @@ +from .category import * +from .hadis import * \ No newline at end of file diff --git a/apps/hadis/serializers/category.py b/apps/hadis/serializers/category.py new file mode 100644 index 0000000..e186dc6 --- /dev/null +++ b/apps/hadis/serializers/category.py @@ -0,0 +1,102 @@ +from rest_framework import serializers +from django.utils.translation import gettext_lazy as _ + +from ..models import HadisSect, HadisCategory, Hadis + + +class HadisCategorySectListSerializer(serializers.ModelSerializer): + """Serializer for HadisSect list with grouped response""" + + class Meta: + model = HadisSect + fields = ['id', 'title'] + + def to_representation(self, instance): + # This method will be overridden in the view to create the grouped response + return super().to_representation(instance) + + +class HadisCategoryTreeSerializer(serializers.ModelSerializer): + """Serializer for HadisCategory tree structure""" + + class Meta: + model = HadisCategory + fields = ['id', 'title', 'source_type'] + + def get_name(self, obj): + """Get category name based on request language""" + request = self.context.get('request') + language_code = getattr(request, 'LANGUAGE_CODE', 'en') + return obj.get_translation(language_code) if hasattr(obj, 'get_translation') else obj.title + + def get_children(self, obj): + """Get active children categories that have children or hadis""" + children = obj.get_children().filter(sect=obj.sect).order_by('order') + # Filter children that have either children or hadis + filtered_children = [] + for child in children: + has_children = child.get_children().filter(sect=obj.sect).exists() + has_hadis = Hadis.objects.filter(category=child, status=True).exists() + if has_children or has_hadis: + filtered_children.append(child) + return [self.to_dict(cat) for cat in filtered_children] + + def get_hadis_count(self, obj): + """Get total hadis count including children categories""" + # Get direct hadis count + direct_count = Hadis.objects.filter(category=obj, status=True).count() + + # Get hadis count from all descendants + descendants = obj.get_descendants().filter(sect=obj.sect) + descendant_count = 0 + for descendant in descendants: + descendant_count += Hadis.objects.filter(category=descendant, status=True).count() + + return direct_count + descendant_count + + def get_has_hadis(self, obj): + """Check if category can have hadis (no active children) and has hadis""" + # Check if category has active children + has_active_children = obj.get_children().filter(sect=obj.sect).exists() + + # If has active children, cannot have hadis + if has_active_children: + return False + + # If no active children, check if has hadis + return Hadis.objects.filter(category=obj, status=True).exists() + + def get_xmind_file(self, obj): + """Get absolute URL for xmind file""" + if obj.xmind_file: + request = self.context.get('request') + if request: + return request.build_absolute_uri(obj.xmind_file.url) + return obj.xmind_file.url + return None + + def get_has_xmind_file(self, obj): + """Check if category has xmind file""" + return bool(obj.xmind_file) + + def to_dict(self, c): + """Convert category to dictionary""" + children = c.get_children().filter(sect=c.sect).order_by('order') + # Filter children that have either children or hadis + filtered_children = [] + for child in children: + has_children = child.get_children().filter(sect=c.sect).exists() + has_hadis = Hadis.objects.filter(category=child, status=True).exists() + if has_children or has_hadis: + filtered_children.append(child) + + return { + 'id': c.id, + 'name': self.get_name(c), + 'hadis_count': self.get_hadis_count(c), + 'has_hadis': self.get_has_hadis(c), + 'order': c.order, + 'xmind_file': self.get_xmind_file(c), + 'has_xmind_file': self.get_has_xmind_file(c), + 'children': [] if not filtered_children else [self.to_dict(i) for i in filtered_children], + } \ No newline at end of file diff --git a/apps/hadis/serializers/hadis.py b/apps/hadis/serializers/hadis.py new file mode 100644 index 0000000..238357d --- /dev/null +++ b/apps/hadis/serializers/hadis.py @@ -0,0 +1,144 @@ +from rest_framework import serializers +from django.utils.translation import gettext_lazy as _ + +from ..models import ( + Hadis, HadisStatus, HadisTag, HadisTransmitter, + HadisReference, ReferenceImage, Transmitters +) +from apps.library.serializers import BookSerializer + + +class HadisListSerializer(serializers.ModelSerializer): + """Serializer for Hadis list""" + category = serializers.SerializerMethodField() + translation = serializers.SerializerMethodField() + + class Meta: + model = Hadis + fields = ['id', 'number', 'title', 'category', 'translation'] + + def get_category(self, obj): + """Get category id and title""" + if obj.category: + return { + 'id': obj.category.id, + 'title': obj.category.title + } + return None + + def get_translation(self, obj): + """Get translation based on request language""" + request = self.context.get('request') + language_code = getattr(request, 'LANGUAGE_CODE', 'en') + return obj.get_translation(language_code) + + +class HadisStatusSerializer(serializers.ModelSerializer): + """Serializer for HadisStatus""" + + class Meta: + model = HadisStatus + fields = ['id', 'title', 'color'] + + +class HadisTagSerializer(serializers.ModelSerializer): + """Serializer for HadisTag""" + + class Meta: + model = HadisTag + fields = ['id', 'title'] + + +class TransmitterSerializer(serializers.ModelSerializer): + """Serializer for Transmitters""" + + class Meta: + model = Transmitters + fields = [ + 'id', 'full_name', 'birth_year_hijri', 'death_year_hijri', + 'description' + ] + + +class HadisTransmitterSerializer(serializers.ModelSerializer): + """Serializer for HadisTransmitter with transmitter details""" + transmitter = TransmitterSerializer(read_only=True) + + class Meta: + model = HadisTransmitter + fields = [ + 'id', 'transmitter', 'order', 'is_gap' + ] + + +class ReferenceImageSerializer(serializers.ModelSerializer): + """Serializer for ReferenceImage""" + thumbnail = serializers.SerializerMethodField() + + class Meta: + model = ReferenceImage + fields = ['id', 'thumbnail', 'priority'] + + def get_thumbnail(self, obj): + """Get thumbnail URL""" + if obj.image: + request = self.context.get('request') + if request: + return request.build_absolute_uri(obj.image.url) + return obj.image.url + return None + + +class HadisReferenceSerializer(serializers.ModelSerializer): + """Serializer for HadisReference with book and images""" + book = BookSerializer(read_only=True) + images = ReferenceImageSerializer(many=True, read_only=True) + + class Meta: + model = HadisReference + fields = [ + 'id', 'book', 'page_number', 'hadis_number_in_book', + 'description', 'images' + ] + + +class HadisDetailSerializer(serializers.ModelSerializer): + """Detailed serializer for Hadis with all related objects""" + hadis_status = HadisStatusSerializer(read_only=True) + tags = HadisTagSerializer(many=True, read_only=True) + transmitters = HadisTransmitterSerializer( + source='hadistransmitter_set', + many=True, + read_only=True + ) + references = HadisReferenceSerializer( + source='hadisreference_set', + many=True, + read_only=True + ) + category = serializers.SerializerMethodField() + translation = serializers.SerializerMethodField() + + class Meta: + model = Hadis + fields = [ + 'id', 'number', 'title', 'text', 'translation', 'explanation', + 'address', 'hadis_status_text', 'links', 'status', + 'category', 'hadis_status', 'tags', 'transmitters', 'references' + ] + + def get_category(self, obj): + """Get category details""" + if obj.category: + return { + 'id': obj.category.id, + 'title': obj.category.title, + 'category_type': obj.category.category_type + } + return None + + def get_translation(self, obj): + """Get translation based on request language""" + request = self.context.get('request') + language_code = getattr(request, 'LANGUAGE_CODE', 'en') + return obj.get_translation(language_code) diff --git a/apps/hadis/templates/admin/category_index.html b/apps/hadis/templates/admin/category_index.html deleted file mode 100644 index a3653c0..0000000 --- a/apps/hadis/templates/admin/category_index.html +++ /dev/null @@ -1,2343 +0,0 @@ -{% extends 'admin/change_form.html' %} -{% load i18n static admin_modify mptt_tags %} - -{% block content %} -
-
-
- -
-
-
{% trans "Category Tree Editor" %}
-
-
-

- {% trans "Make your category and sort it by drag and drop . and try to edit items by double click." %} -

- - - - - -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
{% csrf_token %} - {% block form_top %}{% endblock %} -
- {% block field_sets %} - - - -
-
- - - -
-
-
- - {% trans "Level 1 categories represent source types: Shia or Sunni" %} -
- -
-
-
- - {% trans "Level 2 categories are children of Shia/Sunni with content type: Quran or Hadith" %} -
- -
-
-
- - {% trans "Level 3 categories are children of Quran or Hadith categories" %} -
- -
-
-
-
- - -
-
-
- - - - - {% trans "Select a parent category or leave empty for top-level category" %} - -
-
-
- - {% for fieldset in adminform %} - {% include "admin/includes/fieldset.html" with fullwidth="" %} - {% endfor %} - - {% endblock %} -
- {% include "admin/submit_line.html" with show_save_and_continue=True show_delete_link=True %} -
-
-
-
-
-
- - - -
-{% endblock %} - -{% block scripts %} - {{ block.super }} - - - - - - - - - - - - - -{% endblock %} diff --git a/apps/hadis/templates/admin/hadiscategory/change_form.html b/apps/hadis/templates/admin/hadiscategory/change_form.html deleted file mode 100644 index 40424b1..0000000 --- a/apps/hadis/templates/admin/hadiscategory/change_form.html +++ /dev/null @@ -1,42 +0,0 @@ -{% extends 'admin/category_index.html' %} -{% load i18n admin_urls %} - -{% block extrahead %} -{{ block.super }} - -{% endblock %} - -{% block scripts %} -{{ block.super }} - -{% endblock %} \ No newline at end of file diff --git a/apps/hadis/templates/admin/hadisowerview_change_form.html b/apps/hadis/templates/admin/hadisowerview_change_form.html deleted file mode 100644 index 0ccd13b..0000000 --- a/apps/hadis/templates/admin/hadisowerview_change_form.html +++ /dev/null @@ -1,153 +0,0 @@ -{% extends "admin/change_form.html" %} -{% load i18n %} -{% load static %} - -{% block submit_buttons_bottom %} - {{ block.super }} - - - -{% endblock %} - -{% block scripts %} - {{ block.super }} - - - -{% endblock %} diff --git a/apps/hadis/templates/admin/widgets/color_radio.html b/apps/hadis/templates/admin/widgets/color_radio.html deleted file mode 100644 index aa7786a..0000000 --- a/apps/hadis/templates/admin/widgets/color_radio.html +++ /dev/null @@ -1,7 +0,0 @@ -{% for group, options, index in widget.optgroups %} - {% for option in options %} -
- {% include option.template_name with widget=option %} -
- {% endfor %} -{% endfor %} \ No newline at end of file diff --git a/apps/hadis/templates/admin/widgets/color_radio_option.html b/apps/hadis/templates/admin/widgets/color_radio_option.html deleted file mode 100644 index 7897608..0000000 --- a/apps/hadis/templates/admin/widgets/color_radio_option.html +++ /dev/null @@ -1,9 +0,0 @@ -{% if widget.wrap_label %} - -{% endif %} - - - {{ widget.label }} -{% if widget.wrap_label %} - -{% endif %} \ No newline at end of file diff --git a/apps/hadis/urls.py b/apps/hadis/urls.py index bf75802..715d5c9 100644 --- a/apps/hadis/urls.py +++ b/apps/hadis/urls.py @@ -1,12 +1,16 @@ -from django.urls import path, include -from . import views +from django.urls import path +from .views.category import HadisSectListView, HadisCategoryTreeView +from .views.hadis import HadisListView, HadisDetailView urlpatterns = [ - path('categories/', views.CategoryListView.as_view(), name='category-list'), + # Hadis Sect endpoints + path('categories/', HadisSectListView.as_view(), name='hadis-sect-list'), - path('categories//hadis/', views.CategoryHadisListView.as_view(), name='category-hadis-list'), - path('/', views.HadisDetailView.as_view(), name='hadis-detail'), + # Hadis Category endpoints + path('categories//', HadisCategoryTreeView.as_view(), name='hadis-category-tree'), - + # Hadis endpoints + path('/hadis/', HadisListView.as_view(), name='hadis-list'), + path('hadis//', HadisDetailView.as_view(), name='hadis-detail'), ] \ No newline at end of file diff --git a/apps/hadis/views/category.py b/apps/hadis/views/category.py index df35e82..7caf4dd 100644 --- a/apps/hadis/views/category.py +++ b/apps/hadis/views/category.py @@ -1,301 +1,119 @@ -from rest_framework.permissions import IsAuthenticated -from rest_framework.response import Response -from django.db.models import Subquery, Count, F, OuterRef, Q, Prefetch, Case, When, Value, IntegerField -from rest_framework.pagination import PageNumberPagination from rest_framework.generics import ListAPIView -from django.core.cache import cache -from django.conf import settings -import hashlib -import json - - -from apps.hadis.models import * -from apps.hadis.serializers import * -from apps.hadis.doc import category_list_swagger, category_hadis_list_swagger - - -class CategoryPagination(PageNumberPagination): - page_size = 10 - page_size_query_param = 'page_size' - max_page_size = 100 - - -class CategoryListView(ListAPIView): - serializer_class = HadisCategorySerializer - permission_classes = (IsAuthenticated,) - pagination_class = CategoryPagination - # Cache timeout in seconds (1 hour) - CACHE_TIMEOUT = 60 * 60 - - def get_cache_key(self, source_type=None): - """ - Generate a unique cache key based on the view name and filter parameters. - - Args: - source_type: Optional source_type filter parameter - - Returns: - A unique cache key string - """ - # Base key with the view name - key_parts = ['category_tree'] - - # Add filter parameters to make the key specific - if source_type: - key_parts.append(f'source_type:{source_type}') - - # 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) \ No newline at end of file diff --git a/apps/hadis/views/hadis.py b/apps/hadis/views/hadis.py index f0b285e..9d8bffe 100644 --- a/apps/hadis/views/hadis.py +++ b/apps/hadis/views/hadis.py @@ -1,75 +1,50 @@ -from rest_framework.permissions import IsAuthenticated -from rest_framework.response import Response -from django.db.models import Subquery, Count, F, OuterRef, Q, Prefetch from rest_framework.generics import ListAPIView, RetrieveAPIView from django.shortcuts import get_object_or_404 +from ..models import HadisCategory, Hadis +from ..serializers import HadisListSerializer, HadisDetailSerializer +from ..docs import hadis_list_swagger, hadis_detail_swagger -from apps.hadis.models import * -from apps.hadis.serializers import * -from apps.hadis.doc import category_list_swagger, category_hadis_list_swagger, hadis_detail_swagger +class HadisListView(ListAPIView): + """ + API view to list Hadis by category_id + """ + serializer_class = HadisListSerializer - -class CategoryHadisListView(ListAPIView): - serializer_class = HadisSerializer - permission_classes = (IsAuthenticated,) - - @category_hadis_list_swagger + @hadis_list_swagger def get(self, request, *args, **kwargs): - return super().get(request, *args, **kwargs) + return self.list(request, *args, **kwargs) + def get_queryset(self): - categories = HadisCategory.objects.filter(id=self.kwargs['pk']).order_by('-order') + category_id = self.kwargs.get('category_id') + if not HadisCategory.objects.filter(id=category_id).exists(): + return Hadis.objects.none() + return Hadis.objects.filter( - Q(category__in=categories), - status=True, - ).prefetch_related( - 'category', - ) + category_id=category_id, + status=True + ).order_by('number') class HadisDetailView(RetrieveAPIView): """ - API endpoint to retrieve detailed information about a specific hadis. - - Returns: - - Hadis details (number, title, text, translation) - - HadisOverview information (status, tags, etc.) - - First HadisReference with its ReferenceImages - - List of Transmitters + API view to retrieve detailed Hadis information by hadis_id """ serializer_class = HadisDetailSerializer - permission_classes = (IsAuthenticated,) + lookup_field = 'id' + lookup_url_kwarg = 'hadis_id' @hadis_detail_swagger def get(self, request, *args, **kwargs): - return super().get(request, *args, **kwargs) - - 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 - diff --git a/config/enhanced_auth_middleware.py b/config/enhanced_auth_middleware.py new file mode 100644 index 0000000..b64a952 --- /dev/null +++ b/config/enhanced_auth_middleware.py @@ -0,0 +1,63 @@ +from rest_framework.authtoken.models import Token +from django.contrib.auth import get_user_model +from django.shortcuts import redirect +from django.urls import reverse +from django.contrib import messages + +User = get_user_model() + +def enhanced_auth_middleware(get_response): + """ + Enhanced middleware for API authentication with admin restriction + Handles custom documentation system authentication + """ + def middleware(request): + # Define protected paths that require staff access + protected_paths = ["/swagger", "/redoc", "/docs"] + is_protected_path = any(path in request.path for path in protected_paths) + + if is_protected_path: + # Check if user is authenticated and is staff + if request.user.is_authenticated and request.user.is_staff: + # Handle swagger token authentication from session + if 'swagger_token' in request.session: + token = request.session['swagger_token'] + # Validate the token still exists and is valid + try: + token_obj = Token.objects.get(key=token) + if token_obj.user.is_active: + request.META['HTTP_AUTHORIZATION'] = f"Token {token}" + else: + # Token user is inactive, clear session + del request.session['swagger_token'] + if 'swagger_user_info' in request.session: + del request.session['swagger_user_info'] + except Token.DoesNotExist: + # Token doesn't exist, clear session + del request.session['swagger_token'] + if 'swagger_user_info' in request.session: + del request.session['swagger_user_info'] + + # If no swagger token in session, provide default admin token for basic access + elif not request.META.get('HTTP_AUTHORIZATION'): + # Create or get token for the current admin user + token, _ = Token.objects.get_or_create(user=request.user) + request.META['HTTP_AUTHORIZATION'] = f"Token {token.key}" + + else: + # User is not authenticated or not staff + # For swagger-auth paths, allow access (they handle their own auth) + if '/swagger-auth/' not in request.path: + # Redirect to admin login for other protected paths + messages.warning(request, 'You must be logged in as a staff member to access API documentation.') + return redirect(f"{reverse('admin:login')}?next={request.path}") + + # For non-protected API paths, handle normal authentication + elif "/admin/" not in request.path and request.META.get('HTTP_AUTHORIZATION') is None: + if request.user.is_authenticated and request.user.is_staff: + token, _ = Token.objects.get_or_create(user=request.user) + request.META['HTTP_AUTHORIZATION'] = f"Token {token.key}" + + return get_response(request) + + return middleware diff --git a/config/settings/base.py b/config/settings/base.py index b9b73a4..9f1613b 100755 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -125,7 +125,7 @@ MIDDLEWARE = [ 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'config.language_code_middleware.language_middleware', - 'config.test_auth_middleware.test_auth_middleware', + 'config.enhanced_auth_middleware.enhanced_auth_middleware', ] ROOT_URLCONF = 'config.urls' @@ -270,18 +270,9 @@ FILER_ENABLE_LOGGING = True FILER_DEBUG = True ADMIN_TITLE = 'Imam Javad App' ADMIN_INDEX_TITLE = 'Imam Javad Administration' +SITE_DOMAIN = "https://imamjavad.nwhco.ir" -# Dictionary with phone number ranges and corresponding countries -# If a country is in this dictionary, it indicates that the project's OTP service supports that country -SERVICE_OTP_COUNTRU_API_KEY = { - "Iran": "https://console.melipayamak.com/api/send/simple/33213d78f1234e99b81f94eefda77e45" -} -SERVICE_OTP_COUNTRY_PHONE_RANGE = { - "98": "Iran", - "+98": "Iran" -} - # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/5.0/howto/static-files/ @@ -290,19 +281,12 @@ SERVICE_OTP_COUNTRY_PHONE_RANGE = { # https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' -DEFAULT_SHOW_CITY_GUIDE_CITY = 'mashhad' FILE_UPLOAD_HANDLERS = [ 'django.core.files.uploadhandler.TemporaryFileUploadHandler', ] -EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' -EMAIL_HOST = 'smtp.gmail.com' -EMAIL_PORT = 587 -EMAIL_USE_TLS = True -EMAIL_HOST_USER = 'aliabdolahi.171@gmail.com' -EMAIL_HOST_PASSWORD = 'rkxb nnhx iave fxxt' ###################################################################### # Sessions ###################################################################### @@ -768,12 +752,59 @@ UNFOLD = { # }, ] }, + { + "title": _("Hadis"), + "collapsible": True, + "separator": True, + "items": [ + { + "title": _("Hadis Sects"), + "icon": "account_tree", + "link": reverse_lazy("admin:hadis_hadissect_changelist"), + }, + { + "title": _("Hadis Categories"), + "icon": "category", + "link": reverse_lazy("admin:hadis_hadiscategory_changelist"), + }, + { + "title": _("Hadis"), + "icon": "format_quote", + "link": reverse_lazy("admin:hadis_hadis_changelist"), + }, + { + "title": _("Hadis References"), + "icon": "link", + "link": reverse_lazy("admin:hadis_hadisreference_changelist"), + }, + { + "title": _("Hadis Tags"), + "icon": "label", + "link": reverse_lazy("admin:hadis_hadistag_changelist"), + }, + { + "title": _("Hadis Status"), + "icon": "flag", + "link": reverse_lazy("admin:hadis_hadisstatus_changelist"), + }, + { + "title": _("Transmitters"), + "icon": "person", + "link": reverse_lazy("admin:hadis_transmitters_changelist"), + }, + { + "title": _("Hadis Transmitters"), + "icon": "group", + "link": reverse_lazy("admin:hadis_hadistransmitter_changelist"), + }, + ] + }, { "title": "", "items": [ { "title": _("Global Preferences"), - "icon": "settings", + "icon": "settings", "link": reverse_lazy("admin:dynamic_preferences_globalpreferencemodel_changelist"), }, # You can add more preference sections here @@ -785,24 +816,6 @@ UNFOLD = { # "SCRIPTS": [ # lambda request: static("js/scripts.js"), # ], - - # { - # "title": _("Hadis"), - # "collapsible": True, - # "separator": True, - # "items": [ - # { - # "title": _("Hadis Categories"), - # "icon": "category", - # "link": reverse_lazy("admin:hadis_hadiscategory_changelist"), - # }, - # # { - # # "title": _("Hadis"), - # # "icon": "format_quote", - # # "link": reverse_lazy("admin:hadis_hadis_changelist"), - # # }, - # ] - # }, ], }, } diff --git a/config/urls.py b/config/urls.py index c859f21..7959ba0 100644 --- a/config/urls.py +++ b/config/urls.py @@ -19,6 +19,7 @@ from django.urls import path, include, re_path from django.conf import settings from django.conf.urls.static import static from django.conf.urls.i18n import i18n_patterns +from django.contrib.admin.views.decorators import staff_member_required from utils import UploadTmpMedia from django.http import JsonResponse from django.shortcuts import render @@ -35,19 +36,24 @@ from drf_yasg import openapi from rest_framework import permissions import requests from filer import views + +# Import custom API views +from apps.api.views import CustomAPIDocumentationView, CustomSwaggerView, SwaggerTokenAuthView, clear_swagger_auth + +# Restricted schema view for admin users only schema_view = get_schema_view( openapi.Info( - title="Snippets API", + title="Imam Javad API", default_version='v1', - description="Project API Documentation", - terms_of_service="https://www.google.com/policies/terms/", - contact=openapi.Contact(email="nwhco.com"), - license=openapi.License(name="BSD License"), + description="Comprehensive API documentation for the Imam Javad educational platform", + contact=openapi.Contact(email="contact@imamjavad.com"), + license=openapi.License(name="MIT License"), ), - public=True, - permission_classes=(permissions.AllowAny,), + public=False, + permission_classes=(permissions.IsAdminUser,), ) + def oneapi_translate(request): dist_lang = request.GET.get('dist_lang') q = request.GET.get('q') @@ -94,11 +100,23 @@ urlpatterns = [ path('admin/filer/', include('filer.urls')), ] +# Protected swagger URL patterns +swagger_urlpatterns = [ + path('swagger-auth/', SwaggerTokenAuthView.as_view(), name='swagger-token-auth'), + path('swagger-auth/clear/', clear_swagger_auth, name='clear-swagger-auth'), + re_path(r'^swagger(?P\.json|\.yaml)$', + staff_member_required(schema_view.without_ui(cache_timeout=0)), + name='schema-json'), + path('swagger/', CustomSwaggerView.as_view(), name='schema-swagger-ui'), + re_path(r'^redoc/$', + staff_member_required(schema_view.with_ui('redoc', cache_timeout=0)), + name='schema-redoc'), +] + urlpatterns+= i18n_patterns( path("admin/", project_admin_site.urls), - re_path(r'^swagger(?P\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'), - re_path(r'^swagger/$', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), - re_path(r'^redoc/$', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'), + path('docs/', CustomAPIDocumentationView.as_view(), name='docs-index'), + *swagger_urlpatterns, path('admin/filer/', include('filer.urls')), ) diff --git a/docs/API_Documentation_System_README.md b/docs/API_Documentation_System_README.md new file mode 100644 index 0000000..1e76b2a --- /dev/null +++ b/docs/API_Documentation_System_README.md @@ -0,0 +1,252 @@ +# Imam Javad API Documentation System + +## Overview + +This project implements a comprehensive custom API documentation system that replaces the default Swagger UI with a beautiful, secure, and user-friendly interface. The system is designed specifically for the Imam Javad educational platform and includes advanced authentication, responsive design, and professional styling. + +## Features + +### 🔐 Security & Access Control +- **Admin-only access**: All documentation endpoints require staff member authentication +- **Token-based authentication**: Secure API token management for testing endpoints +- **Session management**: Persistent authentication state across documentation systems +- **Automatic redirects**: Unauthorized users are redirected to admin login + +### 🎨 Custom Documentation Interface +- **Responsive sidebar navigation**: Collapsible app sections with smooth animations +- **Interactive endpoint explorer**: Click to navigate and highlight specific endpoints +- **Beautiful JSON viewer**: Syntax-highlighted response examples with Prism.js +- **Mobile-friendly design**: Optimized for all screen sizes +- **Professional styling**: Modern gradient backgrounds and smooth transitions + +### 🔧 Enhanced Swagger UI +- **Fixed authentication banner**: Always-visible user info and token management +- **Custom branding**: Imam Javad themed colors and styling +- **Token injection**: Automatic authorization header injection for API testing +- **Integrated navigation**: Seamless links between documentation systems + +## System Architecture + +### File Structure +``` +apps/api/views/ +├── __init__.py +├── documentation.py # Custom documentation view +├── swagger_views.py # Enhanced Swagger views with auth +└── api_views.py # Original API views + +templates/ +├── api/ +│ └── documentation.html # Main documentation template +└── swagger/ + ├── ui.html # Custom Swagger UI template + └── auth.html # Token authentication template + +config/ +├── urls.py # Updated URL configuration +└── enhanced_auth_middleware.py # Custom authentication middleware +``` + +### URL Endpoints + +#### Documentation System +- `/en/docs/` - Main API documentation interface +- `/en/swagger/` - Enhanced Swagger UI with authentication +- `/en/swagger-auth/` - Token authentication management +- `/en/swagger-auth/clear/` - Clear authentication session +- `/en/redoc/` - Protected ReDoc interface + +#### API Structure +The documentation covers all major app endpoints: +- **Account Management** (`/api/account/`) - User auth, registration, profiles +- **Course System** (`/api/courses/`) - Educational courses and lessons +- **Hadis Collection** (`/api/hadis/`) - Islamic hadis texts and categories +- **Digital Library** (`/api/library/`) - Books and downloadable resources +- **Video Content** (`/api/videos/`) - Educational video content +- **Podcast Platform** (`/api/podcast/`) - Audio content and episodes +- **Quiz System** (`/api/quiz/`) - Interactive assessments +- **Bookmarks & Ratings** (`/api/bookmarks/`) - User content management + +## Setup Instructions + +### 1. Authentication Setup +The system automatically creates middleware that handles authentication for protected paths. Admin users get automatic token generation for API access. + +### 2. Admin User Creation +```python +# Create admin user (already done in implementation) +from apps.account.models import User +from rest_framework.authtoken.models import Token + +admin_user = User.objects.create( + email='admin@imamjavad.com', + fullname='Admin User', + is_staff=True, + is_superuser=True, + user_type=User.UserType.SUPER_ADMIN +) +admin_user.set_password('admin123') +admin_user.save() + +# Get admin token for API testing +token, _ = Token.objects.get_or_create(user=admin_user) +print(f"Admin token: {token.key}") +``` + +### 3. Accessing the Documentation + +1. **Login to Admin Panel**: Visit `/en/admin/` and login with admin credentials +2. **Access Documentation**: Navigate to `/en/docs/` for the main documentation +3. **Use Swagger UI**: Visit `/en/swagger/` for interactive API testing +4. **Manage Tokens**: Use `/en/swagger-auth/` for token authentication + +## Usage Guide + +### Main Documentation Interface + +1. **Sidebar Navigation**: + - Click on app names to expand/collapse endpoint lists + - Click on specific endpoints to scroll to their documentation + - Mobile users can toggle sidebar with the hamburger menu + +2. **Endpoint Documentation**: + - Each endpoint shows HTTP method, URL, and description + - Parameters table with types and requirements + - Interactive response examples with syntax highlighting + - Tabbed interface for different response types + +3. **Action Buttons**: + - "Swagger UI" button links to interactive testing interface + - "ReDoc" button provides alternative documentation view + +### Swagger UI Interface + +1. **Authentication Banner**: + - Shows current user information and authentication status + - Provides quick access to token management + - Links to main documentation + +2. **Token Management**: + - Enter 40-character API tokens for testing + - Automatic token injection into API requests + - Session persistence across page reloads + +3. **API Testing**: + - All endpoints automatically include authentication headers + - Interactive request/response testing + - Real-time API exploration + +## Customization + +### Adding New Endpoints +Update the `_get_api_structure()` method in `apps/api/views/documentation.py`: + +```python +def _get_api_structure(self): + return { + 'new_app': { + 'name': 'New App Name', + 'description': 'App description', + 'endpoints': [ + { + 'name': 'Endpoint Name', + 'method': 'GET', + 'url': '/api/new-app/endpoint/', + 'description': 'Endpoint description', + 'parameters': [...], + 'response_examples': {...} + } + ] + } + } +``` + +### Styling Customization +Modify CSS variables in `templates/api/documentation.html`: + +```css +:root { + --primary-color: #2c3e50; + --secondary-color: #3498db; + --success-color: #27ae60; + /* Add your custom colors */ +} +``` + +### Branding Updates +- Update project name in templates and views +- Modify color schemes and gradients +- Add custom logos and icons +- Update contact information and licensing + +## Security Considerations + +### Access Control +- All documentation endpoints require `@staff_member_required` decorator +- Middleware automatically handles authentication for protected paths +- Session-based token management with validation +- Automatic cleanup of invalid tokens + +### Token Security +- 40-character Django REST framework tokens +- Session storage with server-side validation +- Automatic token refresh and cleanup +- User activity tracking and session management + +## Troubleshooting + +### Common Issues + +1. **403 Forbidden on Documentation Pages** + - Ensure user has `is_staff=True` + - Check middleware configuration + - Verify admin login session + +2. **Token Authentication Not Working** + - Verify token is exactly 40 characters + - Check token exists in database + - Ensure user account is active + +3. **Responsive Design Issues** + - Clear browser cache + - Check viewport meta tag + - Test on different screen sizes + +### Debug Mode +Enable Django debug mode to see detailed error messages: +```python +DEBUG = True # in settings +``` + +## Performance Optimization + +### Caching +- Static assets are cached with appropriate headers +- JSON responses use browser caching +- Template fragments can be cached for better performance + +### Mobile Optimization +- Responsive images and media queries +- Touch-friendly interface elements +- Optimized loading for mobile networks + +## Contributing + +When adding new features or endpoints: + +1. Update the API structure in `documentation.py` +2. Add appropriate response examples +3. Test on multiple screen sizes +4. Ensure security requirements are met +5. Update this documentation + +## License + +This documentation system is part of the Imam Javad educational platform and follows the project's MIT License. + +--- + +**Admin Credentials for Testing:** +- Email: `admin@imamjavad.com` +- Password: `admin123` +- API Token: `632a324083da7c224361fc61eb5882633c1c575b` diff --git a/docs/Custom_Swagger_API_Documentation_Implementation_Guide.md b/docs/Custom_Swagger_API_Documentation_Implementation_Guide.md new file mode 100644 index 0000000..c771eac --- /dev/null +++ b/docs/Custom_Swagger_API_Documentation_Implementation_Guide.md @@ -0,0 +1,1433 @@ +# Complete Prompt for Custom Swagger & API Documentation System Implementation + +## Overview +Implement a comprehensive custom API documentation system that replaces the default Swagger UI with a custom documentation interface, includes advanced authentication, and provides a beautiful user experience. The system should be restricted to admin users only. + +## Core Requirements + +### 1. Custom API Documentation System +- Create a custom API documentation view that overrides existing Swagger configuration +- Implement a collapsible sidebar navigation where each Django app is displayed as a main item +- When clicking on an app name, show/hide a list of API endpoints for that specific app underneath +- When clicking on a specific endpoint, scroll to that section and display detailed API documentation including descriptions and response examples +- Display response examples using a beautiful JSON editor/viewer with syntax highlighting +- All content must be in English +- The documentation should be responsive and work well on different screen sizes + +### 2. Custom Swagger UI with Authentication +- Override the existing Swagger setup with a custom implementation +- Create a custom Swagger UI template with authentication banner +- Implement token-based authentication system with 40-character Django tokens +- Add session management for storing authentication tokens +- Display user information and authentication status in a fixed header +- Provide token management interface (login, logout, change token) + +### 3. Security & Access Control +- Restrict access to documentation, Swagger UI, and ReDoc to admin users only +- Users must be logged into the Django admin panel to access any documentation +- Implement `@staff_member_required` decorators on all documentation views +- Update middleware to handle authentication for API endpoints +- Redirect unauthorized users to admin login page + +### 4. UI/UX Requirements +- Fixed header in Swagger UI that stays visible during scrolling +- Responsive design that works on mobile, tablet, and desktop +- Beautiful gradient backgrounds and modern styling +- Smooth animations and transitions +- Professional color scheme and typography +- Integration buttons between different documentation systems + +## Technical Implementation + +### File Structure to Create: + +``` +apps/api/views/ +├── __init__.py +├── documentation.py +├── swagger_views.py +└── api_views.py + +templates/ +├── api/ +│ └── documentation.html +├── swagger/ +│ ├── ui.html +│ └── auth.html +└── admin/ + └── login_required.html +``` + +### 1. Custom Documentation View (`apps/api/views/documentation.py`) + +```python +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': 'Your Project API Documentation', + 'description': 'Comprehensive API documentation with interactive examples', + } + return render(request, 'api/documentation.html', context) + + def _get_api_structure(self): + """ + Define your API structure here with apps, endpoints, parameters, and response examples + """ + return { + 'app_name': { + 'name': 'App Display Name', + 'description': 'App description', + 'endpoints': [ + { + 'name': 'Endpoint Name', + 'method': 'GET', + 'url': '/api/endpoint/', + 'description': 'Endpoint description', + 'parameters': [ + {'name': 'param', 'type': 'string', 'description': 'Parameter description', 'required': True} + ], + 'response_examples': { + 'success': json.dumps({"example": "response"}, indent=2) + } + } + ] + } + } +``` + +### 2. Custom Swagger Views (`apps/api/views/swagger_views.py`) + +```python +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') +``` + +### 3. Update URLs (`config/urls.py`) + +```python +from django.contrib.admin.views.decorators import staff_member_required +from drf_yasg.views import get_schema_view +from drf_yasg import openapi +from rest_framework import permissions +from apps.api.views import CustomAPIDocumentationView, CustomSwaggerView, SwaggerTokenAuthView, clear_swagger_auth + +# Restricted schema view +schema_view = get_schema_view( + openapi.Info( + title="Your Project API", + default_version='v1', + description="Your Project API Documentation", + contact=openapi.Contact(email="admin@yourproject.com"), + license=openapi.License(name="Your License"), + ), + public=False, + permission_classes=(permissions.IsAdminUser,), +) + +# Protected swagger URL patterns +swagger_urlpatterns = [ + path('swagger-auth/', SwaggerTokenAuthView.as_view(), name='swagger-token-auth'), + path('swagger-auth/clear/', clear_swagger_auth, name='clear-swagger-auth'), + re_path(r'^swagger(?P\.json|\.yaml)$', + staff_member_required(schema_view.without_ui(cache_timeout=0)), + name='schema-json'), + path('swagger/', CustomSwaggerView.as_view(), name='schema-swagger-ui'), + re_path(r'^redoc/$', + staff_member_required(schema_view.with_ui('redoc', cache_timeout=0)), + name='schema-redoc'), +] + +urlpatterns += i18n_patterns( + path("admin/", admin.site.urls), # Adjust based on your admin setup + path('docs/', CustomAPIDocumentationView.as_view(), name='docs-index'), + *swagger_urlpatterns, +) +``` + +### 4. Enhanced Middleware (Update existing or create new) + +```python +from rest_framework.authtoken.models import Token +from django.contrib.auth import get_user_model + +User = get_user_model() + +def enhanced_auth_middleware(get_response): + """ + Enhanced middleware for API authentication with admin restriction + """ + def middleware(request): + protected_paths = ["/swagger", "/redoc", "/docs"] + is_protected_path = any(path in request.path for path in protected_paths) + + if is_protected_path: + if request.user.is_authenticated and request.user.is_staff: + # Provide API token for authenticated staff users + if 'swagger_token' in request.session: + token = request.session['swagger_token'] + request.META['HTTP_AUTHORIZATION'] = f"Token {token}" + elif not request.META.get('HTTP_AUTHORIZATION'): + # Fallback to default admin user token + admin_user = User.objects.filter(is_staff=True).first() + if admin_user: + token, _ = Token.objects.get_or_create(user=admin_user) + request.META['HTTP_AUTHORIZATION'] = f"Token {token.key}" + + return get_response(request) + + return middleware +``` + +### 5. Templates + +#### Custom Documentation Template (`templates/api/documentation.html`) +- Responsive sidebar with collapsible app sections +- Beautiful JSON viewer with Prism.js syntax highlighting +- Smooth scrolling and animations +- Links to Swagger UI and ReDoc +- Mobile-friendly design + +#### Custom Swagger UI Template (`templates/swagger/ui.html`) +- Fixed header with authentication banner +- User information display +- Token management buttons +- Links to custom documentation +- Responsive design with proper mobile handling +- Backdrop blur effects + +#### Authentication Template (`templates/swagger/auth.html`) +- Beautiful login form for token authentication +- User information display +- Token validation and management +- Help section and instructions + +### 6. Key Features to Implement + +#### Fixed Header in Swagger UI: +```css +.auth-banner { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 9999; + backdrop-filter: blur(10px); +} + +body { + padding-top: 80px; /* Adjust based on header height */ +} + +@media (max-width: 768px) { + body { + padding-top: 100px; /* More space for mobile */ + } +} +``` + +#### Responsive Design: +- Mobile-first approach +- Collapsible navigation +- Touch-friendly buttons +- Proper spacing and typography + +#### Security Implementation: +- `@staff_member_required` decorators on all views +- Session-based token management +- Automatic redirect to admin login +- Protected schema endpoints + +### 7. Styling Guidelines + +#### Color Scheme: +- Primary: `#667eea` to `#764ba2` (gradient) +- Success: `#28a745` +- Warning: `#f39c12` +- Danger: `#e74c3c` +- Background: `#f8f9fa` + +#### Typography: +- Font Family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif +- Responsive font sizes +- Proper line heights and spacing + +#### Components: +- Rounded corners (6-12px border-radius) +- Subtle shadows and gradients +- Smooth transitions (0.3s ease) +- Hover effects and animations + +### 8. Integration Points + +#### With Existing Admin: +- Use existing admin authentication +- Maintain admin session +- Integrate with admin navigation if needed + +#### With API System: +- Discover endpoints automatically +- Generate documentation from views +- Include proper parameter documentation +- Provide realistic response examples + +### 9. Testing Checklist + +- [ ] Unauthorized users redirected to admin login +- [ ] Authorized staff users can access all documentation +- [ ] Fixed header works properly on all screen sizes +- [ ] Token authentication system functions correctly +- [ ] All links between documentation systems work +- [ ] Mobile responsiveness is maintained +- [ ] JSON syntax highlighting works +- [ ] Collapsible navigation functions properly + +### 10. Customization Notes + +- Replace "Your Project" with actual project name +- Update API structure in `_get_api_structure()` method +- Adjust URL patterns based on your project structure +- Customize color scheme and branding +- Add your specific API endpoints and documentation +- Update contact information and licensing + +## Template Implementation Details + +### Custom Documentation Template Structure (`templates/api/documentation.html`) + +This template should create a beautiful, responsive API documentation interface with the following structure and appearance: + +#### Visual Layout: +``` +┌─────────────────────────────────────────────────────────────┐ +│ Header with Title & Actions │ +├──────────────┬──────────────────────────────────────────────┤ +│ Sidebar │ Main Content │ +│ │ │ +│ ┌─ App 1 │ ┌─ Endpoint Documentation │ +│ │ └─ GET /api │ │ Method + URL │ +│ │ └─ POST / │ │ Description │ +│ │ │ │ Parameters Table │ +│ ┌─ App 2 │ │ Response Examples (JSON) │ +│ │ └─ GET / │ └─────────────────────────────────────────│ +│ │ └─ PUT / │ │ +│ │ ┌─ Next Endpoint Documentation │ +│ │ │ ... │ +└──────────────┴──────────────────────────────────────────────┘ +``` + +#### Complete Template Implementation: + +```html + + + + + + {{ title }} + + + + + + + + + + + + +
+ + + + +
+
+
+
+

{{ title }}

+

{{ description }}

+
+ +
+
+ + + {% for app_key, app_data in api_structure.items %} + {% for endpoint in app_data.endpoints %} +
+
+

{{ endpoint.name }}

+ {{ endpoint.method }} + {{ endpoint.url }} +
+ +

{{ endpoint.description }}

+ + {% if endpoint.parameters %} +
+

+ + Parameters +

+ + + + + + + + + + + {% for param in endpoint.parameters %} + + + + + + + {% endfor %} + +
NameTypeRequiredDescription
{{ param.name }}{{ param.type }} + {% if param.required %} + Required + {% else %} + Optional + {% endif %} + {{ param.description }}
+
+ {% endif %} + +
+

+ + Response Examples +

+ +
+ {% for response_type, response_data in endpoint.response_examples.items %} + + {% endfor %} +
+ + {% for response_type, response_data in endpoint.response_examples.items %} +
+
{{ response_data|safe }}
+
+ {% endfor %} +
+
+ {% endfor %} + {% endfor %} +
+
+ + + + + + + + + +``` + +#### Additional CSS Styles for Complete Functionality + +Add these styles to complete the documentation template: + +```css +/* 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; +} + +/* 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; +} + +/* Mobile Responsive Design */ +.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; + } + + .endpoint-header { + flex-direction: column; + align-items: flex-start; + gap: 10px; + } + + .action-buttons { + flex-direction: column; + width: 100%; + } + + .btn-swagger, + .btn-redoc { + justify-content: center; + width: 100%; + } + + .parameters-table { + font-size: 0.8rem; + } + + .parameters-table th, + .parameters-table td { + padding: 8px 10px; + } + + .response-tabs { + flex-wrap: wrap; + } + + .response-tab { + padding: 8px 15px; + font-size: 0.9rem; + } +} + +@media (max-width: 480px) { + .main-content { + padding: 15px 10px; + } + + .endpoint-section { + padding: 15px; + } + + .endpoint-title { + font-size: 1.2rem; + } + + .parameters-table { + display: block; + overflow-x: auto; + white-space: nowrap; + } +} + +/* Scrollbar Styling */ +.sidebar::-webkit-scrollbar { + width: 6px; +} + +.sidebar::-webkit-scrollbar-track { + background: rgba(255,255,255,0.1); +} + +.sidebar::-webkit-scrollbar-thumb { + background: rgba(255,255,255,0.3); + border-radius: 3px; +} + +.sidebar::-webkit-scrollbar-thumb:hover { + background: rgba(255,255,255,0.5); +} + +/* Loading Animation */ +.loading { + display: flex; + justify-content: center; + align-items: center; + padding: 40px; +} + +.spinner { + width: 40px; + height: 40px; + border: 4px solid #f3f3f3; + border-top: 4px solid var(--secondary-color); + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +/* Smooth Animations */ +* { + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +/* Focus States for Accessibility */ +.endpoint-item:focus, +.app-header:focus, +.response-tab:focus { + outline: 2px solid var(--secondary-color); + outline-offset: 2px; +} +``` + +#### Template Features & Appearance: + +##### 🎨 **Visual Design Elements:** + +1. **Color Scheme:** + - Primary: Dark blue-gray (`#2c3e50`) + - Secondary: Bright blue (`#3498db`) + - Success: Green (`#27ae60`) + - Warning: Orange (`#f39c12`) + - Danger: Red (`#e74c3c`) + +2. **Layout Structure:** + - **Fixed sidebar** (300px width) with dark gradient background + - **Main content area** with white background and proper spacing + - **Responsive design** that collapses sidebar on mobile + +3. **Typography:** + - Primary font: 'Segoe UI' family for readability + - Monospace font for code elements + - Proper font weights and sizes for hierarchy + +##### 📱 **Responsive Behavior:** + +1. **Desktop (>768px):** + - Fixed sidebar navigation + - Full-width main content + - Horizontal action buttons + +2. **Tablet (768px-480px):** + - Collapsible sidebar with overlay + - Mobile toggle button + - Stacked action buttons + +3. **Mobile (<480px):** + - Full-width content + - Compact spacing + - Horizontal scrolling for tables + +##### 🎯 **Interactive Elements:** + +1. **Sidebar Navigation:** + - Collapsible app sections with smooth animations + - Hover effects with color transitions + - Active state indicators + - Chevron icons that rotate on expand/collapse + +2. **Endpoint Documentation:** + - Smooth scrolling to sections + - Temporary highlight effect on navigation + - Tabbed response examples + - Syntax-highlighted JSON with Prism.js + +3. **Parameters Table:** + - Hover effects on rows + - Color-coded parameter types + - Required/Optional badges + - Responsive table design + +##### 🔧 **Functional Components:** + +1. **HTTP Method Badges:** + ```css + .method-get { background: #27ae60; } /* Green */ + .method-post { background: #3498db; } /* Blue */ + .method-put { background: #f39c12; } /* Orange */ + .method-delete { background: #e74c3c; } /* Red */ + .method-patch { background: #9b59b6; } /* Purple */ + ``` + +2. **JSON Response Viewer:** + - Dark theme for better code readability + - Syntax highlighting with Prism.js + - Proper indentation and formatting + - Copy-friendly monospace font + +3. **Navigation Features:** + - Smooth scroll behavior + - Active section highlighting + - Mobile-friendly touch targets + - Keyboard accessibility support + +##### 📋 **Content Structure:** + +Each endpoint section includes: + +1. **Header Section:** + - Endpoint name (large, bold) + - HTTP method badge + - URL path in code format + +2. **Description:** + - Clear, readable explanation + - Proper line height and spacing + +3. **Parameters Table:** + - Name (monospace, highlighted) + - Type (color-coded badge) + - Required/Optional status + - Description + +4. **Response Examples:** + - Tabbed interface for different response types + - JSON syntax highlighting + - Dark theme for code readability + +##### 🎨 **Animation & Transitions:** + +1. **Smooth Transitions:** + - 0.3s cubic-bezier easing for all elements + - Hover effects on interactive elements + - Smooth sidebar collapse/expand + +2. **Visual Feedback:** + - Temporary highlight on section navigation + - Loading spinners for dynamic content + - Focus states for accessibility + +3. **Mobile Interactions:** + - Touch-friendly button sizes + - Swipe-friendly sidebar overlay + - Responsive touch targets + +This template creates a professional, modern API documentation interface that rivals commercial documentation platforms while maintaining full customization control and integration with your Django project. + +### Custom Swagger UI Template Structure + +```html + + + + + + + + + +
+ +
+ + +
+ + + + + +``` + +## Implementation Steps + +### Step 1: Create View Structure +1. Create `apps/api/views/` directory +2. Implement `documentation.py` with custom documentation view +3. Implement `swagger_views.py` with authentication views +4. Update `__init__.py` to export views + +### Step 2: Create Templates +1. Create `templates/api/documentation.html` with responsive design +2. Create `templates/swagger/ui.html` with fixed header +3. Create `templates/swagger/auth.html` for token management +4. Ensure all templates are mobile-responsive + +### Step 3: Update URL Configuration +1. Import required decorators and views +2. Update schema_view configuration for admin-only access +3. Create protected swagger_urlpatterns +4. Add documentation routes to main urlpatterns + +### Step 4: Enhance Security +1. Add `@staff_member_required` decorators to all views +2. Update middleware for token handling +3. Protect all documentation endpoints +4. Test unauthorized access redirects + +### Step 5: Customize and Test +1. Update API structure with your project's endpoints +2. Customize branding and colors +3. Test responsive design on different devices +4. Verify authentication flow works correctly + +## Final Notes + +This implementation provides: +- **Complete security**: Only admin users can access documentation +- **Beautiful UI**: Modern, responsive design with smooth animations +- **Token management**: Full authentication system for API testing +- **Integration**: Seamless connection between different documentation systems +- **Customizable**: Easy to adapt for any Django project + +The system maintains the professional look while providing all the functionality needed for comprehensive API documentation and testing. + +This implementation can be adapted to any Django project by: +1. Updating the project-specific details (names, URLs, branding) +2. Customizing the API structure in the documentation view +3. Adjusting the authentication system if needed +4. Modifying the styling to match your project's design system diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..42c8bf3 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,169 @@ +# Скрипты для заполнения данными приложения Hadis + +Этот каталог содержит скрипты для создания и управления тестовыми данными для приложения Hadis. + +## Файлы + +### Основные скрипты + +- **`seed_hadis_data.py`** - Основной скрипт для создания тестовых данных +- **`clear_hadis_data.py`** - Скрипт для очистки созданных данных +- **`README.md`** - Этот файл с документацией + +### Ресурсы + +- **`seed_images/`** - Каталог с изображениями для обложек книг + - `book1.png`, `book2.png`, `book3.png`, `book4.png` +- **`test.xmind`** - Файл XMind для категорий хадисов + +## Использование + +### Создание тестовых данных + +```bash +# Создать данные без очистки существующих +python scripts/seed_hadis_data.py + +# Создать данные с очисткой существующих (ОСТОРОЖНО!) +python scripts/seed_hadis_data.py --clear + +# Создать данные без очистки (явно) +python scripts/seed_hadis_data.py --no-clear +``` + +### Очистка данных + +```bash +# Очистить все данные хадисов и связанные данные библиотеки +python scripts/clear_hadis_data.py + +# Очистить только данные хадисов, оставить библиотеку +python scripts/clear_hadis_data.py --hadis-only + +# Принудительная очистка без подтверждения +python scripts/clear_hadis_data.py --force +``` + +## Создаваемые данные + +### Модели Hadis + +1. **HadisSect** (Секты) + - Шииты-двунадесятники + - Сунниты + +2. **HadisStatus** (Статусы хадисов) + - Достоверный, Хороший, Слабый, Выдуманный, и др. + +3. **HadisTag** (Теги) + - Поклонение, Молитва, Пост, Хадж, Закят, и др. + +4. **HadisCategory** (Категории) - Иерархическая структура + - **Коран**: Толкование Корана, Аяты предписаний, и др. + - **Хадисы**: Книга молитвы, Книга поста, Книга хаджа, и др. + +5. **Transmitters** (Передатчики) + - Известные мухаддисы и имамы + +6. **Hadis** (Хадисы) + - Реалистичные тексты хадисов на русском языке + - Переводы на персидском и английском + - Объяснения и комментарии + +7. **HadisTransmitter** (Цепочки передачи) + - Цепочки передатчиков для каждого хадиса + - Включая пропуски в цепочках + +8. **HadisReference** (Ссылки) + - Связи хадисов с книгами + +9. **ReferenceImage** (Изображения ссылок) + - Изображения для ссылок на источники + +### Модели Library + +1. **Book** (Книги) + - Аль-Кафи, Сахих аль-Бухари, и др. + - С обложками из seed_images + +2. **Category** (Категории библиотеки) + - Книги хадисов, Книги фикха, и др. + +3. **BookCollection** (Коллекции книг) + - Шиитские книги хадисов, Суннитские книги хадисов + +## Особенности + +### Реалистичные данные +- Все тексты на русском языке +- Аутентичные названия книг и имена передатчиков +- Правильная иерархия категорий +- Реалистичные цепочки передачи + +### Связи между моделями +- Правильные foreign key связи +- Many-to-many отношения для тегов +- Иерархические структуры (MPTT) для категорий + +### Файлы и изображения +- XMind файлы для категорий +- Изображения обложек для книг +- Изображения для ссылок + +### Безопасность +- Транзакционная безопасность +- Возможность отката при ошибках +- Подтверждение перед удалением данных + +## Структура данных + +``` +HadisSect (2 записи) +├── HadisCategory (иерархическая структура) +│ ├── Quran categories (4 основные + дочерние) +│ └── Hadith categories (7 основных + дочерние) +│ +├── Hadis (2-4 хадиса на категорию) +│ ├── HadisTransmitter (цепочки 3-6 передатчиков) +│ ├── HadisReference (1-3 ссылки на книги) +│ └── ReferenceImage (изображения для ссылок) +│ +├── HadisStatus (7 статусов) +├── HadisTag (30+ тегов) +└── Transmitters (10 известных передатчиков) + +Library Models: +├── Book (4 книги с обложками) +├── Category (5 категорий) +└── BookCollection (3 коллекции) +``` + +## Тестирование API + +После создания данных можно тестировать API: + +```bash +# Список сект +curl -X GET "http://localhost:8000/api/hadis/sects/" + +# Категории по секте +curl -X GET "http://localhost:8000/api/hadis/sect/1/categories/" + +# Хадисы по категории +curl -X GET "http://localhost:8000/api/hadis/category/1/hadis/" +``` + +## Требования + +- Django проект настроен и работает +- Все зависимости установлены +- База данных мигрирована +- Файлы seed_images и test.xmind присутствуют + +## Примечания + +- Скрипт создает данные на русском языке +- Используются реалистичные исламские термины и имена +- Данные подходят для демонстрации и тестирования +- Можно безопасно запускать несколько раз +- Поддерживается частичная очистка данных diff --git a/scripts/clear_hadis_data.py b/scripts/clear_hadis_data.py new file mode 100644 index 0000000..10d727d --- /dev/null +++ b/scripts/clear_hadis_data.py @@ -0,0 +1,246 @@ +#!/usr/bin/env python3 +""" +Script to clear existing hadis data created by seeding scripts. +This script safely removes all hadis-related data while preserving +other application data. +""" + +import os +import sys +import django +from pathlib import Path +from django.db import transaction + +# Setup Django environment +BASE_DIR = Path(__file__).resolve().parent.parent +sys.path.append(str(BASE_DIR)) +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') +django.setup() + +# Import models after Django setup +from apps.hadis.models import ( + HadisSect, HadisCategory, HadisStatus, HadisTag, Hadis, + Transmitters, HadisTransmitter, HadisReference, ReferenceImage +) +from apps.library.models import Book, Category as LibraryCategory, BookCollection + + +class HadisDataCleaner: + """Class to safely clear hadis data""" + + def __init__(self): + pass + + def show_current_data(self): + """Show current data counts""" + print("=== ТЕКУЩИЕ ДАННЫЕ ===") + print(f"HadisSect: {HadisSect.objects.count()}") + print(f"HadisCategory: {HadisCategory.objects.count()}") + print(f"HadisStatus: {HadisStatus.objects.count()}") + print(f"HadisTag: {HadisTag.objects.count()}") + print(f"Hadis: {Hadis.objects.count()}") + print(f"Transmitters: {Transmitters.objects.count()}") + print(f"HadisTransmitter: {HadisTransmitter.objects.count()}") + print(f"HadisReference: {HadisReference.objects.count()}") + print(f"ReferenceImage: {ReferenceImage.objects.count()}") + print(f"Books: {Book.objects.count()}") + print(f"Library Categories: {LibraryCategory.objects.count()}") + print(f"Book Collections: {BookCollection.objects.count()}") + + # Show sample data + print("\n=== ОБРАЗЦЫ ДАННЫХ ===") + if HadisSect.objects.exists(): + print("HadisSect samples:") + for sect in HadisSect.objects.all()[:3]: + print(f" - {sect.title}") + + if HadisStatus.objects.exists(): + print("HadisStatus samples:") + for status in HadisStatus.objects.all()[:3]: + print(f" - {status.title}") + + if HadisTag.objects.exists(): + print("HadisTag samples:") + for tag in HadisTag.objects.all()[:5]: + print(f" - {tag.title}") + + if Transmitters.objects.exists(): + print("Transmitters samples:") + for trans in Transmitters.objects.all()[:3]: + print(f" - {trans.full_name}") + + if Book.objects.exists(): + print("Books samples:") + for book in Book.objects.all()[:3]: + print(f" - {book.title}") + + @transaction.atomic + def clear_all_hadis_data(self): + """Clear all hadis-related data""" + print("\n=== ОЧИСТКА ДАННЫХ ХАДИСОВ ===") + + # Clear in reverse dependency order + print("Удаление ReferenceImage...") + count = ReferenceImage.objects.count() + ReferenceImage.objects.all().delete() + print(f" Удалено {count} записей ReferenceImage") + + print("Удаление HadisReference...") + count = HadisReference.objects.count() + HadisReference.objects.all().delete() + print(f" Удалено {count} записей HadisReference") + + print("Удаление HadisTransmitter...") + count = HadisTransmitter.objects.count() + HadisTransmitter.objects.all().delete() + print(f" Удалено {count} записей HadisTransmitter") + + print("Удаление Hadis...") + count = Hadis.objects.count() + Hadis.objects.all().delete() + print(f" Удалено {count} записей Hadis") + + print("Удаление HadisCategory...") + count = HadisCategory.objects.count() + HadisCategory.objects.all().delete() + print(f" Удалено {count} записей HadisCategory") + + print("Удаление HadisSect...") + count = HadisSect.objects.count() + HadisSect.objects.all().delete() + print(f" Удалено {count} записей HadisSect") + + print("Удаление HadisStatus...") + count = HadisStatus.objects.count() + HadisStatus.objects.all().delete() + print(f" Удалено {count} записей HadisStatus") + + print("Удаление HadisTag...") + count = HadisTag.objects.count() + HadisTag.objects.all().delete() + print(f" Удалено {count} записей HadisTag") + + print("Удаление Transmitters...") + count = Transmitters.objects.count() + Transmitters.objects.all().delete() + print(f" Удалено {count} записей Transmitters") + + @transaction.atomic + def clear_library_data(self): + """Clear library data that was created by seeding""" + print("\n=== ОЧИСТКА ДАННЫХ БИБЛИОТЕКИ ===") + + # Only clear books that seem to be created by seeding script + # (based on Russian titles or specific patterns) + russian_book_titles = [ + 'Аль-Кафи', 'Сахих аль-Бухари', + 'Ман ля яхдуруху аль-факих', 'Сунан Абу Дауд' + ] + + books_to_delete = Book.objects.filter(title__in=russian_book_titles) + count = books_to_delete.count() + if count > 0: + books_to_delete.delete() + print(f" Удалено {count} книг с русскими названиями") + else: + print(" Книги с русскими названиями не найдены") + + # Clear library categories with Russian names + russian_categories = [ + 'Книги хадисов', 'Книги фикха', 'Книги толкования', + 'Книги нравственности', 'Исторические книги' + ] + + categories_to_delete = LibraryCategory.objects.filter(title__in=russian_categories) + count = categories_to_delete.count() + if count > 0: + categories_to_delete.delete() + print(f" Удалено {count} категорий библиотеки с русскими названиями") + else: + print(" Категории библиотеки с русскими названиями не найдены") + + # Clear book collections with Russian names + russian_collections = [ + 'Шиитские книги хадисов', 'Суннитские книги хадисов', + 'Сборник книг по фикху' + ] + + collections_to_delete = BookCollection.objects.filter(title__in=russian_collections) + count = collections_to_delete.count() + if count > 0: + collections_to_delete.delete() + print(f" Удалено {count} коллекций книг с русскими названиями") + else: + print(" Коллекции книг с русскими названиями не найдены") + + def run_cleanup(self, include_library=True): + """Main method to run cleanup""" + print("=" * 60) + print("ОЧИСТКА ДАННЫХ ХАДИСОВ") + print("=" * 60) + + try: + # Show current state + self.show_current_data() + + # Clear hadis data + self.clear_all_hadis_data() + + # Clear library data if requested + if include_library: + self.clear_library_data() + + # Show final state + print("\n=== ФИНАЛЬНОЕ СОСТОЯНИЕ ===") + self.show_current_data() + + print("\n✅ Очистка завершена успешно!") + + except Exception as e: + print(f"\n❌ Ошибка при очистке: {e}") + print("Откат транзакции...") + raise + + +def main(): + """Main function to run the cleanup script""" + import argparse + + parser = argparse.ArgumentParser(description='Clear hadis data from database') + parser.add_argument( + '--hadis-only', + action='store_true', + help='Clear only hadis data, keep library data' + ) + parser.add_argument( + '--force', + action='store_true', + help='Skip confirmation prompt' + ) + + args = parser.parse_args() + + include_library = not args.hadis_only + + if not args.force: + print("Это удалит все данные хадисов из базы данных.") + if include_library: + print("Также будут удалены связанные данные библиотеки.") + response = input("Вы уверены? (да/нет): ") + if response.lower() not in ['да', 'yes', 'y']: + print("Очистка отменена.") + return + + try: + cleaner = HadisDataCleaner() + cleaner.run_cleanup(include_library=include_library) + + except Exception as e: + print(f"\n❌ Очистка не удалась: {e}") + import traceback + traceback.print_exc() + sys.exit(1) + + +if __name__ == '__main__': + main() diff --git a/scripts/seed_hadis_data.py b/scripts/seed_hadis_data.py new file mode 100644 index 0000000..5dac553 --- /dev/null +++ b/scripts/seed_hadis_data.py @@ -0,0 +1,1087 @@ +#!/usr/bin/env python3 +""" +Comprehensive data seeding script for Hadis app models. +This script creates realistic sample records for all Hadis app models +while maintaining proper relationships and business domain logic. +""" + +import os +import sys +import django +from pathlib import Path +from django.core.files import File +from django.core.files.base import ContentFile +from django.db import transaction +import random + +# 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 HadisDataSeeder: + """Main seeder class for Hadis app data""" + + def __init__(self): + self.script_dir = Path(__file__).parent + self.seed_images_dir = self.script_dir / 'seed_images' + self.xmind_file_path = self.script_dir / 'test.xmind' + + # Verify required files exist + if not self.seed_images_dir.exists(): + raise FileNotFoundError(f"Seed images directory not found: {self.seed_images_dir}") + if not self.xmind_file_path.exists(): + raise FileNotFoundError(f"XMind file not found: {self.xmind_file_path}") + + # Get available images + self.image_files = list(self.seed_images_dir.glob('*.png')) + if not self.image_files: + raise FileNotFoundError("No PNG images found in seed_images directory") + + print(f"Found {len(self.image_files)} seed images") + print(f"XMind file: {self.xmind_file_path}") + + def clear_existing_data(self): + """Clear existing hadis data (optional - for clean seeding)""" + print("Clearing existing hadis data...") + + # Clear in reverse dependency order + ReferenceImage.objects.all().delete() + HadisReference.objects.all().delete() + HadisTransmitter.objects.all().delete() + Hadis.objects.all().delete() + HadisCategory.objects.all().delete() + HadisSect.objects.all().delete() + HadisStatus.objects.all().delete() + HadisTag.objects.all().delete() + Transmitters.objects.all().delete() + + print("Existing data cleared.") + + def seed_hadis_statuses(self): + """Create HadisStatus records""" + print("Creating Hadis Statuses...") + + statuses_data = [ + {'title': 'Достоверный', 'color': 'green', 'order': 1}, + {'title': 'Хороший', 'color': 'blue', 'order': 2}, + {'title': 'Слабый', 'color': 'yellow', 'order': 3}, + {'title': 'Выдуманный', 'color': 'red', 'order': 4}, + {'title': 'Прерванный', 'color': 'orange', 'order': 5}, + {'title': 'Разорванный', 'color': 'purple', 'order': 6}, + {'title': 'Неизвестный', 'color': 'gray', 'order': 7}, + ] + + statuses = [] + for data in statuses_data: + status, created = HadisStatus.objects.get_or_create( + title=data['title'], + defaults=data + ) + statuses.append(status) + if created: + print(f" Created status: {status.title}") + + return statuses + + def seed_hadis_tags(self): + """Create HadisTag records""" + print("Creating Hadis Tags...") + + tags_data = [ + 'Поклонение', 'Молитва', 'Пост', 'Хадж', 'Закят', 'Хумс', + 'Нравственность', 'Терпение', 'Благодарность', 'Упование', 'Богобоязненность', 'Справедливость', + 'Фикх', 'Предписания', 'Дозволенное', 'Запретное', 'Желательное', 'Нежелательное', + 'Толкование', 'Коран', 'Аяты', 'Сура', 'Чтение', + 'Имамат', 'Власть', 'Непорочные', 'Семья Пророка', + 'Мольба', 'Поминание', 'Прощение', 'Восхваление', 'Единобожие' + ] + + tags = [] + for tag_title in tags_data: + tag, created = HadisTag.objects.get_or_create( + title=tag_title, + defaults={'status': True} + ) + tags.append(tag) + if created: + print(f" Created tag: {tag.title}") + + return tags + + def seed_hadis_sects(self): + """Create HadisSect records""" + print("Creating Hadis Sects...") + + sects_data = [ + {'sect_type': 'shia', 'title': 'Шииты-двунадесятники', 'is_active': True, 'order': 1}, + {'sect_type': 'sunni', 'title': 'Сунниты', 'is_active': True, 'order': 2}, + ] + + sects = [] + for data in sects_data: + sect, created = HadisSect.objects.get_or_create( + sect_type=data['sect_type'], + defaults=data + ) + sects.append(sect) + if created: + print(f" Created sect: {sect.title}") + + return sects + + def assign_xmind_file(self, category): + """Assign XMind file to category""" + try: + with open(self.xmind_file_path, 'rb') as f: + file_content = f.read() + + # Create unique filename for each category + filename = f"category_{category.id}_{category.title[:20]}.xmind" + category.xmind_file.save( + filename, + ContentFile(file_content), + save=True + ) + return True + except Exception as e: + print(f" Warning: Could not assign XMind file to {category.title}: {e}") + return False + + def seed_hadis_categories(self, sects): + """Create HadisCategory records with hierarchical structure""" + print("Creating Hadis Categories...") + + categories = [] + + for sect in sects: + print(f" Creating categories for {sect.title}...") + + # Quran categories + quran_categories_data = [ + {'title': 'Толкование Корана', 'order': 1}, + {'title': 'Аяты предписаний', 'order': 2}, + {'title': 'Рассказы Корана', 'order': 3}, + {'title': 'Достоинства сур', 'order': 4}, + {'title': 'Чудеса Корана', 'order': 5}, + {'title': 'Коранические науки', 'order': 6}, + ] + + for cat_data in quran_categories_data: + category = HadisCategory.objects.create( + sect=sect, + source_type='quran', + title=cat_data['title'], + order=cat_data['order'] + ) + categories.append(category) + + # Assign XMind file to some categories + if random.choice([True, False]): + self.assign_xmind_file(category) + + print(f" Created Quran category: {category.title}") + + # Create child categories for main categories + if cat_data['title'] == 'Толкование Корана': + child_categories = [ + {'title': 'Толкование суры Аль-Фатиха', 'order': 1}, + {'title': 'Толкование суры Аль-Бакара', 'order': 2}, + {'title': 'Толкование суры Аль Имран', 'order': 3}, + {'title': 'Толкование суры Ан-Ниса', 'order': 4}, + {'title': 'Толкование суры Аль-Маида', 'order': 5}, + ] + elif cat_data['title'] == 'Аяты предписаний': + child_categories = [ + {'title': 'Аяты о молитве', 'order': 1}, + {'title': 'Аяты о посте', 'order': 2}, + {'title': 'Аяты о закяте', 'order': 3}, + {'title': 'Аяты о хадже', 'order': 4}, + ] + elif cat_data['title'] == 'Рассказы Корана': + child_categories = [ + {'title': 'История пророков', 'order': 1}, + {'title': 'Рассказы о праведниках', 'order': 2}, + {'title': 'Уроки из истории', 'order': 3}, + ] + elif cat_data['title'] == 'Достоинства сур': + child_categories = [ + {'title': 'Достоинства суры Аль-Фатиха', 'order': 1}, + {'title': 'Достоинства суры Аль-Бакара', 'order': 2}, + {'title': 'Достоинства суры Йа-Син', 'order': 3}, + {'title': 'Достоинства суры Аль-Мульк', 'order': 4}, + ] + + for child_data in child_categories: + child_category = HadisCategory.objects.create( + parent=category, + sect=sect, + source_type='quran', + title=child_data['title'], + order=child_data['order'] + ) + categories.append(child_category) + print(f" Created child category: {child_category.title}") + + # Hadith categories + hadith_categories_data = [ + {'title': 'Книга очищения', 'order': 1}, + {'title': 'Книга молитвы', 'order': 2}, + {'title': 'Книга поста', 'order': 3}, + {'title': 'Книга хаджа', 'order': 4}, + {'title': 'Книга закята', 'order': 5}, + {'title': 'Книга брака', 'order': 6}, + {'title': 'Книга нравственности', 'order': 7}, + {'title': 'Книга торговли', 'order': 8}, + {'title': 'Книга джихада', 'order': 9}, + {'title': 'Книга судопроизводства', 'order': 10}, + ] + + for cat_data in hadith_categories_data: + category = HadisCategory.objects.create( + sect=sect, + source_type='hadith', + title=cat_data['title'], + order=cat_data['order'] + ) + categories.append(category) + + # Assign XMind file to some categories + if random.choice([True, False]): + self.assign_xmind_file(category) + + print(f" Created Hadith category: {category.title}") + + # Create child categories for main categories + if cat_data['title'] == 'Книга очищения': + child_categories = [ + {'title': 'Омовение', 'order': 1}, + {'title': 'Полное омовение', 'order': 2}, + {'title': 'Сухое омовение', 'order': 3}, + {'title': 'Нечистоты', 'order': 4}, + ] + elif cat_data['title'] == 'Книга молитвы': + child_categories = [ + {'title': 'Времена молитвы', 'order': 1}, + {'title': 'Кибла', 'order': 2}, + {'title': 'Азан и икама', 'order': 3}, + {'title': 'Коллективная молитва', 'order': 4}, + {'title': 'Пятничная молитва', 'order': 5}, + ] + elif cat_data['title'] == 'Книга поста': + child_categories = [ + {'title': 'Пост в Рамадан', 'order': 1}, + {'title': 'Добровольный пост', 'order': 2}, + {'title': 'Нарушители поста', 'order': 3}, + {'title': 'Ночь предопределения', 'order': 4}, + ] + elif cat_data['title'] == 'Книга хаджа': + child_categories = [ + {'title': 'Обряды хаджа', 'order': 1}, + {'title': 'Умра', 'order': 2}, + {'title': 'Запреты ихрама', 'order': 3}, + ] + elif cat_data['title'] == 'Книга нравственности': + child_categories = [ + {'title': 'Терпение и благодарность', 'order': 1}, + {'title': 'Справедливость и честность', 'order': 2}, + {'title': 'Знание и мудрость', 'order': 3}, + {'title': 'Дружба и братство', 'order': 4}, + ] + + for child_data in child_categories: + child_category = HadisCategory.objects.create( + parent=category, + sect=sect, + source_type='hadith', + title=child_data['title'], + order=child_data['order'] + ) + categories.append(child_category) + print(f" Created child category: {child_category.title}") + + return categories + + def seed_library_data(self): + """Create library data (books, categories, collections) for references""" + print("Creating Library data...") + + # Create library categories + lib_categories_data = [ + 'Книги хадисов', 'Книги фикха', 'Книги толкования', 'Книги нравственности', 'Исторические книги' + ] + + lib_categories = [] + for cat_title in lib_categories_data: + category, created = LibraryCategory.objects.get_or_create( + title=cat_title, + defaults={'status': True} + ) + lib_categories.append(category) + if created: + print(f" Created library category: {category.title}") + + # Create book collections + collections_data = [ + {'title': 'Шиитские книги хадисов', 'display_position': 'pinned'}, + {'title': 'Суннитские книги хадисов', 'display_position': 'middle'}, + {'title': 'Сборник книг по фикху', 'display_position': 'middle'}, + ] + + collections = [] + for coll_data in collections_data: + collection, created = BookCollection.objects.get_or_create( + title=coll_data['title'], + defaults={ + 'summary': f'Коллекция {coll_data["title"]}', + 'display_position': coll_data['display_position'], + 'status': True, + 'order': len(collections) + 1 + } + ) + collections.append(collection) + if created: + print(f" Created collection: {collection.title}") + + # Create books with cover images + books_data = [ + { + 'title': 'Аль-Кафи', + 'summary_title': 'Книга Аль-Кафи шейха Кулейни', + 'summary': 'Одна из важнейших книг хадисов шиитов', + 'description': 'Книга Аль-Кафи, написанная Мухаммадом ибн Якубом Кулейни, является одной из четырех достоверных книг хадисов шиитов.', + 'publisher': 'Дар аль-Кутуб аль-Исламийя', + 'year_of_publication': '1407', + 'isbn': '978-964-372-001-1', + 'pages_count': '2847', + 'file_type': 'pdf' + }, + { + 'title': 'Сахих аль-Бухари', + 'summary_title': 'Сахих аль-Бухари имама Бухари', + 'summary': 'Самая достоверная книга хадисов суннитов', + 'description': 'Сахих аль-Бухари, написанный Мухаммадом ибн Исмаилом Бухари, является самой достоверной книгой хадисов у суннитов.', + 'publisher': 'Дар Тук ан-Наджа', + 'year_of_publication': '1422', + 'isbn': '978-964-372-002-2', + 'pages_count': '1896', + 'file_type': 'pdf' + }, + { + 'title': 'Ман ля яхдуруху аль-факих', + 'summary_title': 'Ман ля яхдуруху аль-факих шейха Садука', + 'summary': 'Важная книга по фикху и хадисам шиитов', + 'description': 'Книга Ман ля яхдуруху аль-факих, написанная шейхом Садуком, является одной из четырех книг шиитов.', + 'publisher': 'Муассаса ан-Нашр аль-Ислами', + 'year_of_publication': '1413', + 'isbn': '978-964-372-003-3', + 'pages_count': '1524', + 'file_type': 'pdf' + }, + { + 'title': 'Сунан Абу Дауд', + 'summary_title': 'Сунан Абу Дауд имама Абу Дауда', + 'summary': 'Одна из шести книг суннитов', + 'description': 'Сунан Абу Дауд, написанная Сулейманом ибн Ашасом Сиджистани, является одной из шести книг суннитов.', + 'publisher': 'Аль-Мактаба аль-Асрийя', + 'year_of_publication': '1430', + 'isbn': '978-964-372-004-4', + 'pages_count': '1342', + 'file_type': 'pdf' + }, + ] + + books = [] + for i, book_data in enumerate(books_data): + # Get random image for book cover + image_file = random.choice(self.image_files) + + book, created = Book.objects.get_or_create( + title=book_data['title'], + defaults=book_data + ) + + if created: + # Assign cover image + try: + with open(image_file, 'rb') as f: + book.thumbnail.save( + f"book_cover_{book.id}.png", + File(f), + save=True + ) + print(f" Created book: {book.title} with cover image") + except Exception as e: + print(f" Created book: {book.title} (no cover image: {e})") + + # Assign to categories and collections + if lib_categories: + book.categories.add(random.choice(lib_categories)) + if collections: + book.collections.add(random.choice(collections)) + + books.append(book) + + return books, lib_categories, collections + + def seed_transmitters(self): + """Create Transmitters records""" + print("Creating Transmitters...") + + transmitters_data = [ + { + 'full_name': 'Мухаммад ибн Якуб Кулейни', + 'birth_year_hijri': 250, + 'death_year_hijri': 329, + 'description': 'Шейх Кулейни, автор книги Аль-Кафи и один из великих мухаддисов шиитов' + }, + { + 'full_name': 'Мухаммад ибн Али ибн Бабавейх (Шейх Садук)', + 'birth_year_hijri': 306, + 'death_year_hijri': 381, + 'description': 'Шейх Садук, автор книги Ман ля яхдуруху аль-факих' + }, + { + 'full_name': 'Мухаммад ибн аль-Хасан ат-Туси', + 'birth_year_hijri': 385, + 'death_year_hijri': 460, + 'description': 'Шейх Туси, автор книг Тахзиб аль-Ахкам и аль-Истибсар' + }, + { + 'full_name': 'Мухаммад ибн Исмаил аль-Бухари', + 'birth_year_hijri': 194, + 'death_year_hijri': 256, + 'description': 'Имам Бухари, автор Сахих аль-Бухари' + }, + { + 'full_name': 'Муслим ибн аль-Хаджжадж ан-Нишапури', + 'birth_year_hijri': 206, + 'death_year_hijri': 261, + 'description': 'Имам Муслим, автор Сахих Муслим' + }, + { + 'full_name': 'Абу Дауд ас-Сиджистани', + 'birth_year_hijri': 202, + 'death_year_hijri': 275, + 'description': 'Имам Абу Дауд, автор Сунан Абу Дауд' + }, + { + 'full_name': 'Джафар ибн Мухаммад ас-Садик', + 'birth_year_hijri': 83, + 'death_year_hijri': 148, + 'description': 'Имам Джафар Садик (мир ему), шестой имам шиитов' + }, + { + 'full_name': 'Мухаммад ибн Али аль-Бакир', + 'birth_year_hijri': 57, + 'death_year_hijri': 114, + 'description': 'Имам Мухаммад Бакир (мир ему), пятый имам шиитов' + }, + { + 'full_name': 'Али ибн аль-Хусейн ас-Саджжад', + 'birth_year_hijri': 38, + 'death_year_hijri': 95, + 'description': 'Имам Али ибн аль-Хусейн (мир ему), четвертый имам шиитов' + }, + { + 'full_name': 'Мухаммад ибн Муслим', + 'birth_year_hijri': 70, + 'death_year_hijri': 150, + 'description': 'Мухаммад ибн Муслим, из сподвижников имама Бакира и имама Садика (мир им)' + }, + ] + + transmitters = [] + for trans_data in transmitters_data: + transmitter, created = Transmitters.objects.get_or_create( + full_name=trans_data['full_name'], + defaults=trans_data + ) + transmitters.append(transmitter) + if created: + print(f" Created transmitter: {transmitter.full_name}") + + return transmitters + + def seed_hadis_records(self, categories, statuses, tags, transmitters, books): + """Create Hadis records with proper relationships - only for leaf categories""" + print("Creating Hadis records...") + + # Get only leaf categories (categories without children) + leaf_categories = [] + for category in categories: + if not category.get_children().exists(): + leaf_categories.append(category) + + print(f"Found {len(leaf_categories)} leaf categories for hadis creation") + + # Comprehensive hadis samples with longer texts + hadis_samples = { + 'prayer': [ + { + 'title': 'Достоинство молитвы и ее место в религии', + 'text': '''قال رسول الله صلى الله عليه وآله: الصلاة عمود الدين، إن قبلت قبل ما سواها، وإن ردت رد ما سواها. وهي أول ما يحاسب عليه العبد يوم القيامة، فإن صلحت صلح سائر عمله، وإن فسدت فسد سائر عمله. + +والصلاة معراج المؤمن، وهي قربان كل تقي، وهي حب الله تعالى. من أحبها وأقامها في أوقاتها وحافظ على حدودها رفعه الله إلى درجة الأبرار. ومن استخف بها وضيعها وتركها فقد استخف بدين الله، ولا نصيب له في الإسلام. + +إن الله تعالى فرض خمس صلوات في اليوم والليلة، وجعل لكل صلاة وقتاً معلوماً، فمن صلاها في وقتها وأتم ركوعها وسجودها وخشوعها، كانت له نوراً وبرهاناً ونجاة يوم القيامة.''', + 'translation': [ + {'language_code': 'ru', 'title': '''Сказал Посланник Аллаха (да благословит Аллах его и его семейство): Молитва - столп религии, если она принята, то принято и остальное, а если отвергнута, то отвергнуто и остальное. Это первое, за что будет спрошен раб в День Воскресения, и если она будет правильной, то правильными будут и остальные его дела, а если испорчена, то испорчены и остальные его дела. + +Молитва - это вознесение верующего, она - приношение каждого богобоязненного, она - любовь Всевышнего Аллаха. Кто полюбил ее и совершал ее в установленные времена и соблюдал ее границы, того Аллах возвысит до степени праведников. А кто пренебрег ею, потерял ее и оставил, тот пренебрег религией Аллаха, и нет ему доли в Исламе. + +Поистине, Всевышний Аллах предписал пять молитв в сутки и установил для каждой молитвы определенное время. Кто совершал их в свое время и завершал их поклоны, земные поклоны и смирение, для того они станут светом, доказательством и спасением в День Воскресения.'''}, + {'language_code': 'fa', 'title': '''رسول خدا فرمود: نماز ستون دین است، اگر پذیرفته شود غیر آن نیز پذیرفته می‌شود و اگر رد شود غیر آن نیز رد می‌شود. و این اولین چیزی است که بنده در روز قیامت از آن بازخواست می‌شود، پس اگر درست باشد تمام اعمالش درست است و اگر فاسد باشد تمام اعمالش فاسد است. + +نماز معراج مؤمن است و قربانی هر پرهیزکار و محبت خداوند متعال است. هر کس آن را دوست بدارد و در اوقاتش برپا دارد و حدودش را نگه دارد، خداوند او را به درجه نیکان بالا می‌برد. و هر کس آن را سبک بشمارد و ضایع کند و ترک کند، دین خدا را سبک شمرده و بهره‌ای در اسلام ندارد. + +خداوند متعال پنج نماز در شبانه‌روز واجب کرده و برای هر نماز وقت معینی قرار داده، پس هر کس آن‌ها را در وقتشان بخواند و رکوع و سجود و خشوعشان را کامل کند، برایش نور و برهان و نجات در روز قیامت خواهد بود.'''}, + {'language_code': 'en', 'title': '''The Messenger of Allah said: Prayer is the pillar of religion, if it is accepted, other deeds are accepted, and if it is rejected, other deeds are rejected. It is the first thing for which a servant will be held accountable on the Day of Judgment, and if it is sound, all his other deeds will be sound, and if it is corrupted, all his other deeds will be corrupted. + +Prayer is the ascension of the believer, it is the offering of every God-fearing person, and it is the love of Allah the Almighty. Whoever loves it and establishes it at its times and maintains its boundaries, Allah will raise him to the rank of the righteous. And whoever takes it lightly, wastes it and abandons it, has taken Allah's religion lightly, and has no share in Islam. + +Indeed, Allah the Almighty has prescribed five prayers in a day and night, and has appointed a specific time for each prayer. Whoever prays them at their time and completes their bowing, prostration and humility, they will be light, proof and salvation for him on the Day of Judgment.'''} + ], + 'explanation': '''Этот обширный хадис представляет собой фундаментальное учение о молитве в Исламе. Он раскрывает несколько ключевых аспектов: + +Во-первых, молитва описывается как "столп религии" (عمود الدين), что указывает на ее центральную роль в исламской вере. Это метафора подчеркивает, что как здание не может стоять без столпов, так и религиозная жизнь мусульманина не может быть полноценной без молитвы. + +Во-вторых, хадис устанавливает принцип, согласно которому принятие или отвержение молитвы Аллахом определяет судьбу всех остальных деяний верующего. Это подчеркивает качественный аспект молитвы - важна не только ее форма, но и искренность, концентрация и правильное выполнение. + +В-третьих, молитва представлена как "معراج المؤمن" (вознесение верующего), что отсылает к ночному путешествию Пророка (мир ему) и подчеркивает духовное измерение молитвы как средства приближения к Всевышнему. + +Хадис также подчеркивает важность своевременного совершения молитв и соблюдения их внешних и внутренних условий, обещая великую награду тем, кто относится к молитве с должным вниманием и уважением.''' + }, + { + 'title': 'Времена молитвы', + 'text': 'عن أبي عبد الله عليه السلام قال: إن لكل صلاة وقتين، وأول الوقت أفضل من آخره.', + 'translation': [ + {'language_code': 'ru', 'title': 'От Абу Абдуллаха (мир ему) сказал: Поистине, у каждой молитвы два времени, и первое время лучше последнего.'}, + {'language_code': 'fa', 'title': 'از امام صادق علیه السلام: هر نمازی دو وقت دارد و اول وقت بهتر از آخر آن است.'}, + {'language_code': 'en', 'title': 'From Imam Sadiq: Every prayer has two times, and the first time is better than the last.'} + ], + 'explanation': 'Этот хадис касается времен молитвы и достоинства совершения молитвы в первое время.' + }, + { + 'title': 'Коллективная молитва и ее великая награда', + 'text': '''قال رسول الله صلى الله عليه وآله: صلاة الجماعة تفضل صلاة الفذ بسبع وعشرين درجة. وما من رجل يؤم قوماً إلا غفر له ما تقدم من ذنبه وما تأخر، وما من رجل يصلي خلف إمام إلا كتب له مثل أجر الإمام من غير أن ينقص من أجر الإمام شيء. + +وإن الملائكة لتصلي على الصف الأول، وإن الشيطان ليفر من صوت الأذان، وإن الله تعالى يباهي بالمصلين ملائكته. ومن صلى في جماعة أربعين يوماً لا تفوته التكبيرة الأولى كتب له براءتان: براءة من النار وبراءة من النفاق. + +فاحرصوا على صلاة الجماعة، فإنها من شعائر الإسلام العظيمة، وهي سبب للألفة والمحبة بين المؤمنين، وفيها تظهر وحدة الأمة وقوتها.''', + 'translation': [ + {'language_code': 'ru', 'title': '''Сказал Посланник Аллаха (да благословит Аллах его и его семейство): Коллективная молитва превосходит индивидуальную молитву на двадцать семь степеней. И нет человека, который бы руководил молитвой людей, кроме как ему прощаются его прошлые и будущие грехи, и нет человека, который молился бы за имамом, кроме как ему записывается такая же награда, как имаму, не уменьшая награды имама ни на что. + +Поистине, ангелы молятся за первый ряд, и шайтан убегает от звука азана, и Всевышний Аллах гордится молящимися перед Своими ангелами. И кто молился в коллективе сорок дней, не пропуская первый такбир, тому записываются два освобождения: освобождение от огня и освобождение от лицемерия. + +Так стремитесь к коллективной молитве, ибо она из великих обрядов Ислама, и она причина единства и любви между верующими, и в ней проявляется единство уммы и ее сила.'''}, + {'language_code': 'fa', 'title': '''رسول خدا فرمود: نماز جماعت بر نماز فردی بیست و هفت درجه برتری دارد. و هیچ مردی نیست که امامت قومی کند مگر اینکه گناهان گذشته و آینده‌اش آمرزیده می‌شود، و هیچ مردی نیست که پشت سر امام نماز بخواند مگر اینکه مثل پاداش امام برایش نوشته می‌شود بدون اینکه از پاداش امام چیزی کم شود. + +همانا فرشتگان بر صف اول درود می‌فرستند و شیطان از صدای اذان فرار می‌کند و خداوند متعال به نمازگزاران در برابر فرشتگانش فخر می‌کند. و هر کس چهل روز در جماعت نماز بخواند که تکبیر اول را از دست ندهد، دو برائت برایش نوشته می‌شود: برائت از آتش و برائت از نفاق. + +پس بر نماز جماعت حریص باشید که از شعائر بزرگ اسلام است و سبب الفت و محبت میان مؤمنان است و در آن وحدت امت و قدرتش ظاهر می‌شود.'''}, + {'language_code': 'en', 'title': '''The Messenger of Allah said: Congregational prayer is superior to individual prayer by twenty-seven degrees. There is no man who leads people in prayer except that his past and future sins are forgiven, and there is no man who prays behind an imam except that the same reward as the imam is written for him without diminishing anything from the imam's reward. + +Indeed, the angels pray for the first row, and Satan flees from the sound of the call to prayer, and Allah the Almighty boasts of those who pray to His angels. And whoever prays in congregation for forty days without missing the first takbir, two clearances are written for him: clearance from the Fire and clearance from hypocrisy. + +So be keen on congregational prayer, for it is one of the great rituals of Islam, and it is a cause of harmony and love among believers, and in it the unity and strength of the ummah is manifested.'''} + ], + 'explanation': '''Этот развернутый хадис раскрывает множественные аспекты коллективной молитвы в Исламе и ее огромное духовное значение. + +Прежде всего, хадис устанавливает количественное превосходство коллективной молитвы над индивидуальной - в двадцать семь раз. Это число не случайно и подчеркивает особую ценность единения верующих в поклонении. + +Особое внимание уделяется роли имама и участников коллективной молитвы. Имам получает прощение грехов, что подчеркивает ответственность руководства общиной в молитве. Участники же получают равную награду с имамом, что демонстрирует справедливость божественного воздаяния. + +Хадис также упоминает о духовных реалиях, невидимых человеческому глазу: молитвы ангелов за первый ряд, бегство шайтана от азана, и гордость Всевышнего Своими поклоняющимися рабами. + +Особая награда обещана тем, кто постоянно участвует в коллективной молитве в течение сорока дней - это освобождение от огня и от лицемерия, что указывает на очищающую силу регулярного коллективного поклонения. + +Наконец, хадис подчеркивает социальный аспект коллективной молитвы как средства укрепления единства мусульманской общины.''' + }, + { + 'title': 'Времена молитв и их сохранение', + 'text': '''عن أبي عبد الله عليه السلام قال: إن لكل صلاة وقتين، وأول الوقت أفضل من آخره إلا في العشاء الآخرة، فإن أفضل وقتها إذا ذهب ثلث الليل. وقال: من صلى في أول الوقت فقد أدرك فضل الوقت، ومن صلى في آخر الوقت فقد أدرك الوقت. + +وحافظوا على الصلوات والصلاة الوسطى وقوموا لله قانتين. والصلاة الوسطى هي صلاة الظهر، وهي أول صلاة صلاها رسول الله صلى الله عليه وآله، وهي وسط النهار. + +إن الله تعالى جعل لكل صلاة علامات يعرف بها وقتها: فالفجر عند طلوع الفجر الصادق، والظهر عند زوال الشمس، والعصر عند صيرورة ظل كل شيء مثله، والمغرب عند غروب الشمس، والعشاء عند ذهاب الشفق الأحمر.''', + 'translation': [ + {'language_code': 'ru', 'title': '''От Абу Абдуллаха (мир ему): Поистине, у каждой молитвы два времени, и первое время лучше последнего, кроме ночной молитвы (иша), ибо лучшее время для нее - когда пройдет треть ночи. И сказал: кто помолился в первое время, тот достиг достоинства времени, а кто помолился в последнее время, тот достиг времени. + +Соблюдайте молитвы и среднюю молитву, и стойте перед Аллахом смиренно. Средняя молитва - это полуденная молитва, и это первая молитва, которую совершил Посланник Аллаха (да благословит Аллах его и его семейство), и она в середине дня. + +Поистине, Всевышний Аллах установил для каждой молитвы признаки, по которым узнается ее время: утренняя - при появлении истинного рассвета, полуденная - при прохождении солнца через зенит, послеполуденная - когда тень каждой вещи становится равной ей, вечерняя - при заходе солнца, ночная - при исчезновении красной зари.'''}, + {'language_code': 'fa', 'title': '''از امام صادق علیه السلام: همانا هر نمازی دو وقت دارد و اول وقت بهتر از آخر آن است مگر نماز عشاء که بهترین وقتش زمانی است که یک سوم شب بگذرد. و فرمود: هر کس در اول وقت نماز بخواند فضیلت وقت را درک کرده و هر کس در آخر وقت بخواند وقت را درک کرده است. + +بر نمازها و نماز وسطی محافظت کنید و برای خدا فروتنانه بایستید. نماز وسطی همان نماز ظهر است و این اولین نمازی است که رسول خدا صلی الله علیه و آله خواند و در وسط روز است. + +خداوند متعال برای هر نماز علامت‌هایی قرار داده که وقتش با آن شناخته می‌شود: نماز صبح هنگام طلوع فجر صادق، نماز ظهر هنگام زوال آفتاب، نماز عصر هنگامی که سایه هر چیز مثل خودش شود، نماز مغرب هنگام غروب آفتاب، نماز عشاء هنگام رفتن شفق سرخ.'''}, + {'language_code': 'en', 'title': '''From Abu Abdullah (peace be upon him): Indeed, every prayer has two times, and the first time is better than the last, except for the night prayer (Isha), for its best time is when a third of the night has passed. And he said: whoever prays at the first time has achieved the virtue of time, and whoever prays at the last time has achieved the time. + +Maintain the prayers and the middle prayer, and stand before Allah humbly. The middle prayer is the noon prayer, and it is the first prayer that the Messenger of Allah (may Allah bless him and his family) prayed, and it is in the middle of the day. + +Indeed, Allah the Almighty has established signs for each prayer by which its time is known: the dawn prayer at the appearance of true dawn, the noon prayer when the sun passes through the zenith, the afternoon prayer when the shadow of everything becomes equal to it, the evening prayer at sunset, the night prayer when the red twilight disappears.'''} + ], + 'explanation': '''Этот хадис представляет собой подробное руководство по временам молитв и их правильному соблюдению, что является одним из важнейших аспектов исламского поклонения. + +Хадис начинается с установления общего принципа о том, что каждая молитва имеет два времени - предпочтительное (первое) и допустимое (до конца времени). Это дает верующим гибкость в совершении молитв, учитывая различные жизненные обстоятельства, но при этом поощряет стремление к совершенству. + +Особое исключение делается для ночной молитвы (иша), для которой предпочтительное время наступает после прохождения трети ночи. Это связано с тем, что в это время достигается большее духовное сосредоточение и меньше мирских отвлечений. + +Хадис цитирует коранический аят о "средней молитве" и идентифицирует ее как полуденную молитву (зухр), подчеркивая ее особое значение как первой молитвы, установленной Пророком. + +Наконец, хадис предоставляет практическое руководство по определению времен молитв через естественные признаки, что было особенно важно в эпоху до появления точных часов. Эти признаки основаны на движении солнца и изменении освещения, что делает их универсально применимыми в любой географической точке.''' + } + ], + 'fasting': [ + { + 'title': 'Достоинство поста и его духовные плоды', + 'text': '''قال النبي صلى الله عليه وآله: الصوم جنة من النار، وهو زكاة البدن، وصوم شهر الصبر وثلاثة أيام من كل شهر يذهبن وحر الصدر ووساوس الشيطان. ومن صام يوماً في سبيل الله باعد الله وجهه عن النار سبعين خريفاً. + +إن الصائم في عبادة وإن كان نائماً على فراشه، وإن دعاءه مستجاب حتى يفطر، وإن الملائكة تستغفر له حتى يفطر. وللصائم فرحتان: فرحة عند فطره، وفرحة عند لقاء ربه. + +يا معشر الشباب، من استطاع منكم الباءة فليتزوج، ومن لم يستطع فعليه بالصوم فإنه له وجاء. والصوم يكسر الشهوة ويطهر النفس ويقرب إلى الله تعالى.''', + 'translation': [ + {'language_code': 'ru', 'title': '''Сказал Пророк (да благословит Аллах его и его семейство): Пост - щит от огня, и он закят тела, и пост месяца терпения и трех дней каждого месяца устраняют жар груди и наущения шайтана. И кто постился день на пути Аллаха, Аллах отдалит его лицо от огня на семьдесят осеней. + +Поистине, постящийся находится в поклонении, даже если он спит на своей постели, и его мольба принимается до тех пор, пока он не разговеется, и ангелы просят прощения для него до тех пор, пока он не разговеется. И у постящегося две радости: радость при разговении и радость при встрече со своим Господом. + +О молодежь! Кто из вас способен жениться, пусть женится, а кто не способен, пусть постится, ибо это для него защита. Пост ломает страсть, очищает душу и приближает к Всевышнему Аллаху.'''}, + {'language_code': 'fa', 'title': '''پیامبر فرمود: روزه سپری است از آتش جهنم و زکات بدن است و روزه ماه صبر و سه روز از هر ماه، حرارت سینه و وسوسه‌های شیطان را می‌برد. و هر کس روزی در راه خدا روزه بگیرد، خداوند صورتش را از آتش به اندازه هفتاد پاییز دور می‌کند. + +روزه‌دار در عبادت است اگرچه بر بسترش خوابیده باشد و دعایش تا افطار مستجاب است و فرشتگان تا افطار برایش استغفار می‌کنند. و روزه‌دار را دو شادی است: شادی هنگام افطار و شادی هنگام ملاقات پروردگارش. + +ای جوانان! هر کس از شما توانایی ازدواج دارد باید ازدواج کند و هر کس نتواند باید روزه بگیرد که برایش محافظ است. روزه شهوت را می‌شکند و نفس را پاک می‌کند و به خداوند متعال نزدیک می‌کند.'''}, + {'language_code': 'en', 'title': '''The Prophet said: Fasting is a shield from the Fire, and it is the zakat of the body, and fasting the month of patience and three days of each month removes the heat of the chest and the whispers of Satan. And whoever fasts a day in the way of Allah, Allah will distance his face from the Fire by seventy autumns. + +Indeed, the fasting person is in worship even if he is sleeping on his bed, and his supplication is answered until he breaks his fast, and the angels seek forgiveness for him until he breaks his fast. And the fasting person has two joys: joy when he breaks his fast, and joy when he meets his Lord. + +O young people! Whoever among you is able to marry, let him marry, and whoever is not able, let him fast, for it is a protection for him. Fasting breaks desire, purifies the soul, and brings one closer to Allah the Almighty.'''} + ], + 'explanation': '''Этот многогранный хадис раскрывает глубокие духовные и практические аспекты поста в Исламе, представляя его как комплексную систему духовного и физического очищения. + +Хадис начинается с определения поста как "щита от огня" (جنة من النار), что указывает на его защитную функцию от грехов и их последствий. Понятие "закят тела" (زكاة البدن) представляет пост как форму очищения физического тела, аналогичную тому, как закят очищает имущество. + +Особое внимание уделяется посту "месяца терпения" (شهر الصبر) - Рамадана, и дополнительным постам трех дней каждого месяца. Эти посты описываются как средство устранения "жара груди" - метафоры внутреннего беспокойства, гнева и негативных эмоций. + +Хадис подчеркивает непрерывность духовного состояния постящегося - даже во сне он остается в состоянии поклонения. Это указывает на то, что пост - это не просто воздержание от еды и питья, но целостное духовное состояние. + +Упоминание о двух радостях постящегося - при разговении и при встрече с Аллахом - показывает как временные, так и вечные плоды поста. + +Наконец, хадис связывает пост с контролем над страстями, особенно рекомендуя его молодым людям как средство духовной защиты и самодисциплины.''' + }, + { + 'title': 'Пост в месяц Рамадан', + 'text': 'عن أبي عبد الله عليه السلام قال: من صام شهر رمضان إيماناً واحتساباً غفر له ما تقدم من ذنبه.', + 'translation': [ + {'language_code': 'ru', 'title': 'От Абу Абдуллаха (мир ему): Кто постился в месяц Рамадан с верой и надеждой на награду, тому прощены его прошлые грехи.'}, + {'language_code': 'fa', 'title': 'از امام صادق علیه السلام: هر کس ماه رمضان را با ایمان و امید به پاداش روزه بگیرد، گناهان گذشته‌اش آمرزیده می‌شود.'}, + {'language_code': 'en', 'title': 'From Abu Abdullah: Whoever fasts the month of Ramadan with faith and hope for reward, his past sins are forgiven.'} + ], + 'explanation': 'Этот хадис говорит о великой награде за пост в месяц Рамадан.' + } + ], + 'ethics': [ + { + 'title': 'Благородный нрав', + 'text': 'قال رسول الله صلى الله عليه وآله: إنما بعثت لأتمم مكارم الأخلاق.', + 'translation': [ + {'language_code': 'ru', 'title': 'Сказал Посланник Аллаха (да благословит Аллах его и его семейство): Поистине, я послан, чтобы довести до совершенства благородные нравы.'}, + {'language_code': 'fa', 'title': 'رسول خدا فرمود: من فقط برای تکمیل مکارم اخلاق مبعوث شده‌ام.'}, + {'language_code': 'en', 'title': 'The Messenger of Allah said: I was sent only to perfect noble character.'} + ], + 'explanation': 'Этот хадис показывает важность нравственности в исламской религии.' + }, + { + 'title': 'Терпение', + 'text': 'عن أمير المؤمنين عليه السلام: الصبر صبران: صبر على ما تكره، وصبر عما تحب.', + 'translation': [ + {'language_code': 'ru', 'title': 'От Повелителя верующих (мир ему): Терпение бывает двух видов: терпение к тому, что ты не любишь, и терпение от того, что ты любишь.'}, + {'language_code': 'fa', 'title': 'از امیرمؤمنان علیه السلام: صبر دو گونه است: صبر بر آنچه دوست نداری و صبر از آنچه دوست داری.'}, + {'language_code': 'en', 'title': 'From Amir al-Muminin: Patience is of two types: patience with what you dislike, and patience from what you love.'} + ], + 'explanation': 'Этот хадис описывает виды терпения.' + }, + { + 'title': 'Справедливость', + 'text': 'قال رسول الله صلى الله عليه وآله: العدل أساس الملك.', + 'translation': [ + {'language_code': 'ru', 'title': 'Сказал Посланник Аллаха (да благословит Аллах его и его семейство): Справедливость - основа власти.'}, + {'language_code': 'fa', 'title': 'رسول خدا فرمود: عدالت اساس حکومت است.'}, + {'language_code': 'en', 'title': 'The Messenger of Allah said: Justice is the foundation of governance.'} + ], + 'explanation': 'Этот хадис подчеркивает важность справедливости в управлении.' + }, + { + 'title': 'Знание', + 'text': 'قال النبي صلى الله عليه وآله: اطلبوا العلم من المهد إلى اللحد.', + 'translation': [ + {'language_code': 'ru', 'title': 'Сказал Пророк (да благословит Аллах его и его семейство): Ищите знания от колыбели до могилы.'}, + {'language_code': 'fa', 'title': 'پیامبر فرمود: علم را از گهواره تا گور بجویید.'}, + {'language_code': 'en', 'title': 'The Prophet said: Seek knowledge from the cradle to the grave.'} + ], + 'explanation': 'Этот хадис призывает к постоянному поиску знаний на протяжении всей жизни.' + } + ], + 'quran': [ + { + 'title': 'Достоинство чтения Корана', + 'text': 'قال النبي صلى الله عليه وآله: اقرؤوا القرآن فإنه يأتي يوم القيامة شفيعاً لأصحابه.', + 'translation': [ + {'language_code': 'ru', 'title': 'Сказал Пророк (да благословит Аллах его и его семейство): Читайте Коран, ибо он придет в День Воскресения заступником за своих сподвижников.'}, + {'language_code': 'fa', 'title': 'پیامبر فرمود: قرآن بخوانید که در روز قیامت شفیع اصحابش خواهد بود.'}, + {'language_code': 'en', 'title': 'The Prophet said: Read the Quran, for it will come on the Day of Judgment as an intercessor for its companions.'} + ], + 'explanation': 'Этот хадис описывает достоинство чтения Священного Корана.' + }, + { + 'title': 'Изучение Корана', + 'text': 'عن أبي عبد الله عليه السلام قال: خيركم من تعلم القرآن وعلمه.', + 'translation': [ + {'language_code': 'ru', 'title': 'От Абу Абдуллаха (мир ему): Лучший из вас тот, кто изучает Коран и обучает ему.'}, + {'language_code': 'fa', 'title': 'از امام صادق علیه السلام: بهترین شما کسی است که قرآن بیاموزد و آن را تعلیم دهد.'}, + {'language_code': 'en', 'title': 'From Abu Abdullah: The best of you is the one who learns the Quran and teaches it.'} + ], + 'explanation': 'Этот хадис подчеркивает важность изучения и обучения Корану.' + }, + { + 'title': 'Размышление над Кораном', + 'text': 'قال أمير المؤمنين عليه السلام: تدبروا آيات القرآن واعتبروا به.', + 'translation': [ + {'language_code': 'ru', 'title': 'Сказал Повелитель верующих (мир ему): Размышляйте над аятами Корана и извлекайте из него уроки.'}, + {'language_code': 'fa', 'title': 'امیرمؤمنان علیه السلام فرمود: در آیات قرآن تدبر کنید و از آن عبرت بگیرید.'}, + {'language_code': 'en', 'title': 'Amir al-Muminin said: Contemplate the verses of the Quran and take lessons from it.'} + ], + 'explanation': 'Этот хадис призывает к глубокому размышлению над аятами Корана.' + } + ] + } + + hadis_records = [] + hadis_number = 1 + + # Create hadis for each leaf category (10 hadis per category) + for category in leaf_categories: + # Determine hadis type based on category title + hadis_type = 'ethics' # default + if 'молитв' in category.title.lower() or 'намаз' in category.title.lower(): + hadis_type = 'prayer' + elif 'пост' in category.title.lower() or 'روزه' in category.title: + hadis_type = 'fasting' + elif 'нравственност' in category.title.lower() or 'اخلاق' in category.title: + hadis_type = 'ethics' + elif 'коран' in category.title.lower() or 'толкован' in category.title.lower(): + hadis_type = 'quran' + + # Create exactly 10 hadis per leaf category + available_samples = hadis_samples.get(hadis_type, hadis_samples['ethics']) + + for i in range(10): + sample = random.choice(available_samples) + + # Generate varied links based on hadis type and number + links = [] + if hadis_type == 'prayer': + links = [ + {'title': 'Книги о молитве', 'link': f'https://islamicbooks.ru/prayer/{hadis_number}'}, + {'title': 'Времена молитв', 'link': 'https://prayertimes.ru'}, + {'title': 'Руководство по молитве', 'link': 'https://salah-guide.ru'} + ] + elif hadis_type == 'fasting': + links = [ + {'title': 'Книги о посте', 'link': f'https://islamicbooks.ru/fasting/{hadis_number}'}, + {'title': 'Календарь поста', 'link': 'https://ramadan-calendar.ru'}, + {'title': 'Правила поста', 'link': 'https://fasting-rules.ru'} + ] + elif hadis_type == 'quran': + links = [ + {'title': 'Толкование Корана', 'link': f'https://quran-tafsir.ru/hadis/{hadis_number}'}, + {'title': 'Текст Корана', 'link': 'https://quran-text.ru'}, + {'title': 'Аудио Коран', 'link': 'https://quran-audio.ru'} + ] + else: # ethics + links = [ + {'title': 'Исламская этика', 'link': f'https://islamic-ethics.ru/hadis/{hadis_number}'}, + {'title': 'Нравственные учения', 'link': 'https://moral-teachings.ru'}, + {'title': 'Духовное развитие', 'link': 'https://spiritual-growth.ru'} + ] + + hadis = Hadis.objects.create( + category=category, + number=hadis_number, + title=sample['title'], + text=sample['text'], + translation=sample['translation'], + status=True, + hadis_status=random.choice(statuses), + hadis_status_text=f"Приведен в достоверных книгах", + address=f"Книга {category.title}, хадис {hadis_number}", + explanation=sample.get('explanation', ''), + links=links + ) + + # Add random tags + selected_tags = random.sample(tags, random.randint(2, 5)) + hadis.tags.set(selected_tags) + + hadis_records.append(hadis) + hadis_number += 1 + + print(f" Created hadis #{hadis.number}: {hadis.title} in {category.title}") + + return hadis_records + + def seed_hadis_transmitters(self, hadis_records, transmitters, statuses): + """Create HadisTransmitter records (transmission chains)""" + print("Creating Hadis Transmitters (chains)...") + + transmitter_chains = [] + + for hadis in hadis_records: + # Create a transmission chain of 3-6 transmitters + chain_length = random.randint(3, 6) + selected_transmitters = random.sample(transmitters, min(chain_length, len(transmitters))) + + for order, transmitter in enumerate(selected_transmitters, 1): + # Occasionally create gaps in the chain + is_gap = random.choice([False, False, False, True]) # 25% chance of gap + + chain = HadisTransmitter.objects.create( + hadis=hadis, + transmitter=transmitter if not is_gap else None, + status=random.choice(statuses), + is_gap=is_gap, + gap_type=random.choice(['unknown', 'missing', 'disputed', 'weak']) if is_gap else None, + order=order + ) + + transmitter_chains.append(chain) + + if is_gap: + print(f" Added gap in chain for hadis #{hadis.number} at position {order}") + else: + print(f" Added transmitter {transmitter.full_name} to hadis #{hadis.number}") + + return transmitter_chains + + def seed_hadis_references(self, hadis_records, books): + """Create HadisReference records""" + print("Creating Hadis References...") + + references = [] + + for hadis in hadis_records: + # Each hadis can have 1-3 references + num_refs = random.randint(1, 3) + selected_books = random.sample(books, min(num_refs, len(books))) + + for book in selected_books: + try: + reference = HadisReference.objects.create( + hadis=hadis, + book=book, + description=f"Источник хадиса номер {hadis.number} в книге {book.title}" + ) + references.append(reference) + print(f" Created reference: Hadis #{hadis.number} -> {book.title}") + except Exception as e: + # Skip if reference already exists (unique_together constraint) + pass + + return references + + def seed_reference_images(self, references): + """Create ReferenceImage records""" + print("Creating Reference Images...") + + reference_images = [] + + for reference in references: + # 50% chance to add images to reference + if random.choice([True, False]): + # Add 1-2 images per reference + num_images = random.randint(1, 2) + + for i in range(num_images): + image_file = random.choice(self.image_files) + + ref_image = ReferenceImage.objects.create( + reference=reference, + priority=i + ) + + try: + with open(image_file, 'rb') as f: + ref_image.thumbnail.save( + f"ref_image_{ref_image.id}.png", + File(f), + save=True + ) + reference_images.append(ref_image) + print(f" Added image to reference: {reference.hadis.title}") + except Exception as e: + print(f" Warning: Could not add image to reference: {e}") + ref_image.delete() + + return reference_images + + @transaction.atomic + def run_seeding(self, clear_existing=False): + """Main method to run all seeding operations""" + print("=" * 60) + print("STARTING HADIS DATA SEEDING") + print("=" * 60) + + try: + if clear_existing: + self.clear_existing_data() + + # Step 1: Create basic lookup data + print("\n" + "=" * 40) + print("STEP 1: Creating basic lookup data") + print("=" * 40) + + statuses = self.seed_hadis_statuses() + tags = self.seed_hadis_tags() + sects = self.seed_hadis_sects() + + # Step 2: Create hierarchical categories + print("\n" + "=" * 40) + print("STEP 2: Creating hierarchical categories") + print("=" * 40) + + categories = self.seed_hadis_categories(sects) + + # Step 3: Create library data for references + print("\n" + "=" * 40) + print("STEP 3: Creating library data") + print("=" * 40) + + books, lib_categories, collections = self.seed_library_data() + + # Step 4: Create transmitters + print("\n" + "=" * 40) + print("STEP 4: Creating transmitters") + print("=" * 40) + + transmitters = self.seed_transmitters() + + # Step 5: Create hadis records + print("\n" + "=" * 40) + print("STEP 5: Creating hadis records") + print("=" * 40) + + hadis_records = self.seed_hadis_records(categories, statuses, tags, transmitters, books) + + # Step 6: Create transmission chains + print("\n" + "=" * 40) + print("STEP 6: Creating transmission chains") + print("=" * 40) + + transmitter_chains = self.seed_hadis_transmitters(hadis_records, transmitters, statuses) + + # Step 7: Create references + print("\n" + "=" * 40) + print("STEP 7: Creating references") + print("=" * 40) + + references = self.seed_hadis_references(hadis_records, books) + + # Step 8: Create reference images + print("\n" + "=" * 40) + print("STEP 8: Creating reference images") + print("=" * 40) + + reference_images = self.seed_reference_images(references) + + # Summary + print("\n" + "=" * 60) + print("SEEDING COMPLETED SUCCESSFULLY!") + print("=" * 60) + print(f"Created:") + print(f" - {len(sects)} Hadis Sects") + print(f" - {len(categories)} Hadis Categories") + print(f" - {len(statuses)} Hadis Statuses") + print(f" - {len(tags)} Hadis Tags") + print(f" - {len(transmitters)} Transmitters") + print(f" - {len(hadis_records)} Hadis Records") + print(f" - {len(transmitter_chains)} Transmitter Chain Links") + print(f" - {len(books)} Books") + print(f" - {len(references)} Hadis References") + print(f" - {len(reference_images)} Reference Images") + print("=" * 60) + + return { + 'sects': sects, + 'categories': categories, + 'statuses': statuses, + 'tags': tags, + 'transmitters': transmitters, + 'hadis_records': hadis_records, + 'transmitter_chains': transmitter_chains, + 'books': books, + 'references': references, + 'reference_images': reference_images + } + + except Exception as e: + print(f"\nERROR during seeding: {e}") + print("Rolling back transaction...") + raise + + +def main(): + """Main function to run the seeding script""" + import argparse + + parser = argparse.ArgumentParser(description='Seed Hadis app with sample data') + parser.add_argument( + '--clear', + action='store_true', + help='Clear existing data before seeding' + ) + parser.add_argument( + '--no-clear', + action='store_true', + help='Do not clear existing data (default)' + ) + + args = parser.parse_args() + + # Default to not clearing unless explicitly requested + clear_existing = args.clear and not args.no_clear + + if clear_existing: + response = input("This will DELETE all existing Hadis data. Are you sure? (yes/no): ") + if response.lower() != 'yes': + print("Seeding cancelled.") + return + + try: + seeder = HadisDataSeeder() + result = seeder.run_seeding(clear_existing=clear_existing) + + print("\n✅ Seeding completed successfully!") + print("You can now test the APIs:") + print(" - GET /api/hadis/sects/") + print(" - GET /api/hadis/sect/{sect_id}/categories/") + print(" - GET /api/hadis/category/{category_id}/hadis/") + + except Exception as e: + print(f"\n❌ Seeding failed: {e}") + import traceback + traceback.print_exc() + sys.exit(1) + + +if __name__ == '__main__': + main() diff --git a/scripts/seed_images/book1.png b/scripts/seed_images/book1.png new file mode 100644 index 0000000..1aca516 Binary files /dev/null and b/scripts/seed_images/book1.png differ diff --git a/scripts/seed_images/book2.png b/scripts/seed_images/book2.png new file mode 100644 index 0000000..e1be487 Binary files /dev/null and b/scripts/seed_images/book2.png differ diff --git a/scripts/seed_images/book3.png b/scripts/seed_images/book3.png new file mode 100644 index 0000000..87d6ff9 Binary files /dev/null and b/scripts/seed_images/book3.png differ diff --git a/scripts/seed_images/book4.png b/scripts/seed_images/book4.png new file mode 100644 index 0000000..8788993 Binary files /dev/null and b/scripts/seed_images/book4.png differ diff --git a/scripts/test.xmind b/scripts/test.xmind new file mode 100755 index 0000000..f53fb0d Binary files /dev/null and b/scripts/test.xmind differ diff --git a/templates/api/documentation.html b/templates/api/documentation.html new file mode 100644 index 0000000..f933cae --- /dev/null +++ b/templates/api/documentation.html @@ -0,0 +1,711 @@ + + + + + + {{ title }} + + + + + + + + + + + + +
+ + + + +
+
+
+
+

{{ title }}

+

{{ description }}

+
+ +
+
+ + + {% for app_key, app_data in api_structure.items %} + {% for endpoint in app_data.endpoints %} +
+
+

{{ endpoint.name }}

+ {{ endpoint.method }} + {{ endpoint.url }} +
+ +

{{ endpoint.description }}

+ + {% if endpoint.parameters %} +
+

+ + Parameters +

+ + + + + + + + + + + {% for param in endpoint.parameters %} + + + + + + + {% endfor %} + +
NameTypeRequiredDescription
{{ param.name }}{{ param.type }} + {% if param.required %} + Required + {% else %} + Optional + {% endif %} + {{ param.description }}
+
+ {% endif %} + +
+

+ + Response Examples +

+ +
+ {% for response_type, response_data in endpoint.response_examples.items %} + + {% endfor %} +
+ + {% for response_type, response_data in endpoint.response_examples.items %} +
+
{{ response_data|safe }}
+
+ {% endfor %} +
+
+ {% endfor %} + {% endfor %} +
+
+ + + + + + + + + diff --git a/templates/swagger/auth.html b/templates/swagger/auth.html new file mode 100644 index 0000000..a87d382 --- /dev/null +++ b/templates/swagger/auth.html @@ -0,0 +1,402 @@ + + + + + + Swagger Authentication - Imam Javad API + + + + + + + + +
+
+
+

API Authentication

+

Enter your API token to access Swagger UI

+
+ +
+ + {% if messages %} + {% for message in messages %} +
+ {{ message }} +
+ {% endfor %} + {% endif %} + + + {% if user_info %} + + {% endif %} + + +
+ {% csrf_token %} +
+ + + + Token must be exactly 40 characters long + +
+ + +
+ + +
+
+ + How to get your API token? +
+

+ 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. +

+
+ + + +
+
+
+ + + + + + + diff --git a/templates/swagger/ui.html b/templates/swagger/ui.html new file mode 100644 index 0000000..afb9535 --- /dev/null +++ b/templates/swagger/ui.html @@ -0,0 +1,354 @@ + + + + + + Imam Javad API - Swagger UI + + + + + + + + +
+
+
+ {% if request.session.swagger_user_info %} + +
+
+ Authenticated +
+ {% else %} +
+
+ Not Authenticated +
+ {% endif %} +
+ +
+ + + Documentation + + {% if request.session.swagger_user_info %} + + + Logout + + {% else %} + + + Authenticate + + {% endif %} +
+
+
+ + +
+ + + + + + + + + diff --git a/utils/pagination.py b/utils/pagination.py new file mode 100644 index 0000000..10b254b --- /dev/null +++ b/utils/pagination.py @@ -0,0 +1,23 @@ + + +from rest_framework.pagination import PageNumberPagination +from rest_framework.response import Response + + +class NoPagination(PageNumberPagination): + def paginate_queryset(self, queryset, request, view=None): + # Override to return all items instead of paginated ones + self.count = len(queryset) + self.request = request + self.page = None + self.page_size = len(queryset) + return list(queryset) + + def get_paginated_response(self, data): + # Keep the structure but include all results + return Response({ + 'count': self.count, + 'next': None, # No next page + 'previous': None, # No previous page + 'results': data, + }) \ No newline at end of file