Browse Source
Refactor API Documentation System and optimize Hadis data scripts
Refactor API Documentation System and optimize Hadis data scripts
- Removed the API documentation README file as it is no longer needed. - Added a new script to optimize Hadis transmitter chains, ensuring a maximum of 5 transmitters and exactly one gap. - Enhanced the Hadis data seeding script for better performance with batch operations and duplicate checks. - Updated utility functions to streamline thumbnail generation and improve code readability.master
29 changed files with 1825 additions and 351 deletions
-
80CLAUDE.md
-
10apps/account/admin/professor.py
-
2apps/account/admin/student.py
-
158apps/account/management/commands/migrate_user_roles.py
-
13apps/account/manager.py
-
114apps/account/middleware/admin_access.py
-
90apps/account/models/user.py
-
9apps/account/serializers/user.py
-
240apps/account/tests/test_multiple_roles.py
-
3apps/certificate/admin.py
-
1apps/certificate/migrations/0001_initial.py
-
1apps/chat/management/__init__.py
-
62apps/chat/management/commands/README.md
-
1apps/chat/management/commands/__init__.py
-
79apps/chat/management/commands/clear_chat_data.py
-
40apps/course/admin/course.py
-
7apps/course/admin/lesson.py
-
181apps/course/admin/professor_base.py
-
6apps/course/serializers/course.py
-
216apps/course/tests/test_multiple_roles_api.py
-
56apps/hadis/management/commands/README.md
-
98apps/hadis/management/commands/seed_hadis_data.py
-
152apps/hadis/management/commands/test_safe_seeding.py
-
15apps/quiz/admin/quiz.py
-
7apps/transaction/views.py
-
3config/settings/base.py
-
252docs/API_Documentation_System_README.md
-
252scripts/optimize_hadis_transmitters.py
-
28utils/__init__.py
@ -0,0 +1,80 @@ |
|||||
|
# CodeViz Research Context |
||||
|
|
||||
|
> **Note**: This file contains research context from CodeViz. Most recent contexts are at the bottom. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## Research Query |
||||
|
|
||||
|
در مورد چت میشه بگی |
||||
|
|
||||
|
*Session: 169492aff6d1e2bbd34a3c87fd82786e | Generated: 7/22/2025, 4:26:02 PM* |
||||
|
|
||||
|
### Analysis Summary |
||||
|
|
||||
|
# Chat Functionality Overview |
||||
|
|
||||
|
The chat functionality in this codebase is primarily handled by the **`chat`** application, located at [apps/chat/](apps/chat/). This application is responsible for managing chat-related data models, administrative interfaces, and potentially views for handling chat interactions. |
||||
|
|
||||
|
## High-Level Architecture |
||||
|
|
||||
|
The **`chat`** application is a self-contained Django application designed to manage real-time or asynchronous chat features. It integrates with the main project through its models, views, and potentially URL configurations. |
||||
|
|
||||
|
## Mid-Level Components |
||||
|
|
||||
|
The **`chat`** application consists of several key components: |
||||
|
|
||||
|
### **`apps.py`** - Application Configuration |
||||
|
This file defines the configuration for the chat application, including its name. |
||||
|
- **Purpose**: Configures the Django application. |
||||
|
- **Internal Parts**: Contains the `ChatConfig` class. |
||||
|
- **External Relationships**: Registered in the project's settings to enable the `chat` application. |
||||
|
- [ChatConfig](apps/chat/apps.py) |
||||
|
|
||||
|
### **`models.py`** - Data Models |
||||
|
This file defines the database models for chat-related entities. These models represent the structure of the data stored in the database for chat messages, conversations, or related concepts. |
||||
|
- **Purpose**: Defines the data structures for chat entities. |
||||
|
- **Internal Parts**: Contains Django model classes (e.g., `Message`, `Conversation`, `ChatRoom`, etc., depending on implementation). |
||||
|
- **External Relationships**: Interacts with the database to store and retrieve chat data. These models are likely related to user models from the `account` app. |
||||
|
- [models.py](apps/chat/models.py) |
||||
|
|
||||
|
### **`admin.py`** - Admin Interface |
||||
|
This file registers the chat models with the Django administrative interface, allowing superusers to manage chat data directly from the admin panel. |
||||
|
- **Purpose**: Provides an administrative interface for managing chat data. |
||||
|
- **Internal Parts**: Contains `admin.site.register()` calls for chat models. |
||||
|
- **External Relationships**: Integrates with Django's built-in admin site. |
||||
|
- [admin.py](apps/chat/admin.py) |
||||
|
|
||||
|
### **`views.py`** - Views and Logic |
||||
|
This file would contain the logic for handling requests related to chat, such as sending messages, retrieving chat history, or managing chat rooms. |
||||
|
- **Purpose**: Implements the business logic and presentation for chat features. |
||||
|
- **Internal Parts**: Contains Django views (function-based or class-based) that handle HTTP requests. |
||||
|
- **External Relationships**: Interacts with the models to perform CRUD operations on chat data and potentially renders templates or returns API responses. |
||||
|
- [views.py](apps/chat/views.py) |
||||
|
|
||||
|
### **`migrations/`** - Database Migrations |
||||
|
This directory contains the database migration files generated by Django, which manage changes to the chat application's database schema. |
||||
|
- **Purpose**: Manages database schema evolution for chat models. |
||||
|
- **Internal Parts**: Contains numbered migration files (e.g., `0001_initial.py`). |
||||
|
- **External Relationships**: Applied by Django's ORM to update the database schema. |
||||
|
- [migrations/](apps/chat/migrations/) |
||||
|
|
||||
|
### **`management/`** - Custom Management Commands |
||||
|
This directory might contain custom Django management commands specific to the chat application, such as commands for cleaning up old messages or seeding chat data. |
||||
|
- **Purpose**: Provides custom command-line utilities for chat-related tasks. |
||||
|
- **Internal Parts**: Contains Python files defining custom commands. |
||||
|
- **External Relationships**: Executed via `python manage.py <command_name>`. |
||||
|
- [management/](apps/chat/management/) |
||||
|
|
||||
|
### **`templates/`** - HTML Templates |
||||
|
This directory would hold any HTML templates used by the chat application's views for rendering web pages. |
||||
|
- **Purpose**: Stores HTML templates for rendering chat-related user interfaces. |
||||
|
- **Internal Parts**: Contains `.html` files. |
||||
|
- **External Relationships**: Used by Django views to render dynamic content. |
||||
|
- [templates/](apps/chat/templates/) |
||||
|
|
||||
|
## Integration with Project URLs |
||||
|
|
||||
|
The chat application's URLs are likely included in the main project's URL configuration, typically found in [config/urls.py](config/urls.py). This file acts as the central routing mechanism for the entire application, directing requests to the appropriate views within the `chat` app or other applications. |
||||
|
- [urls.py](config/urls.py) |
||||
|
|
||||
@ -0,0 +1,158 @@ |
|||||
|
""" |
||||
|
Management command برای migration دادههای موجود به سیستم نقشهای چندگانه |
||||
|
""" |
||||
|
from django.core.management.base import BaseCommand |
||||
|
from django.contrib.auth.models import Group |
||||
|
from apps.account.models import User |
||||
|
from apps.course.models import Course, Participant |
||||
|
|
||||
|
|
||||
|
class Command(BaseCommand): |
||||
|
help = 'Migrate existing user data to multiple roles system' |
||||
|
|
||||
|
def add_arguments(self, parser): |
||||
|
parser.add_argument( |
||||
|
'--dry-run', |
||||
|
action='store_true', |
||||
|
help='Show what would be done without making changes', |
||||
|
) |
||||
|
|
||||
|
def handle(self, *args, **options): |
||||
|
dry_run = options['dry_run'] |
||||
|
|
||||
|
if dry_run: |
||||
|
self.stdout.write( |
||||
|
self.style.WARNING('DRY RUN MODE - No changes will be made') |
||||
|
) |
||||
|
|
||||
|
# اطمینان از وجود گروهها |
||||
|
self.ensure_groups_exist(dry_run) |
||||
|
|
||||
|
# Migration کاربران بر اساس user_type فعلی |
||||
|
self.migrate_user_types(dry_run) |
||||
|
|
||||
|
# Migration کاربرانی که هم استاد و هم دانشآموز هستند |
||||
|
self.migrate_professor_students(dry_run) |
||||
|
|
||||
|
self.stdout.write( |
||||
|
self.style.SUCCESS('Migration completed successfully!') |
||||
|
) |
||||
|
|
||||
|
def ensure_groups_exist(self, dry_run): |
||||
|
"""اطمینان از وجود گروههای مورد نیاز""" |
||||
|
groups = [ |
||||
|
"Professor Group", |
||||
|
"Student Group", |
||||
|
"Client Group", |
||||
|
"Admin Group", |
||||
|
"Super Admin Group" |
||||
|
] |
||||
|
|
||||
|
for group_name in groups: |
||||
|
if dry_run: |
||||
|
exists = Group.objects.filter(name=group_name).exists() |
||||
|
if not exists: |
||||
|
self.stdout.write(f'Would create group: {group_name}') |
||||
|
else: |
||||
|
group, created = Group.objects.get_or_create(name=group_name) |
||||
|
if created: |
||||
|
self.stdout.write(f'Created group: {group_name}') |
||||
|
|
||||
|
def migrate_user_types(self, dry_run): |
||||
|
"""Migration کاربران بر اساس user_type فعلی""" |
||||
|
users = User.objects.all() |
||||
|
|
||||
|
for user in users: |
||||
|
# چک کنیم که آیا کاربر قبلاً در گروه مناسب است یا خیر |
||||
|
expected_group_name = f"{user.user_type.capitalize()} Group" |
||||
|
|
||||
|
if not user.groups.filter(name=expected_group_name).exists(): |
||||
|
if dry_run: |
||||
|
self.stdout.write( |
||||
|
f'Would add user {user.email} to group {expected_group_name}' |
||||
|
) |
||||
|
else: |
||||
|
try: |
||||
|
group = Group.objects.get(name=expected_group_name) |
||||
|
user.groups.add(group) |
||||
|
self.stdout.write( |
||||
|
f'Added user {user.email} to group {expected_group_name}' |
||||
|
) |
||||
|
except Group.DoesNotExist: |
||||
|
self.stdout.write( |
||||
|
self.style.ERROR(f'Group {expected_group_name} does not exist') |
||||
|
) |
||||
|
|
||||
|
def migrate_professor_students(self, dry_run): |
||||
|
"""شناسایی و migration کاربرانی که هم استاد و هم دانشآموز هستند""" |
||||
|
# کاربرانی که دوره ساختهاند (استاد هستند) |
||||
|
professors = User.objects.filter(courses__isnull=False).distinct() |
||||
|
|
||||
|
# کاربرانی که در دوره شرکت کردهاند (دانشآموز هستند) |
||||
|
students = User.objects.filter(participated_courses__isnull=False).distinct() |
||||
|
|
||||
|
# کاربرانی که هم استاد و هم دانشآموز هستند |
||||
|
professor_students = professors.filter( |
||||
|
id__in=students.values_list('id', flat=True) |
||||
|
) |
||||
|
|
||||
|
self.stdout.write( |
||||
|
f'Found {professor_students.count()} users who are both professors and students' |
||||
|
) |
||||
|
|
||||
|
for user in professor_students: |
||||
|
# اطمینان از اینکه در هر دو گروه هستند |
||||
|
professor_group_exists = user.groups.filter(name="Professor Group").exists() |
||||
|
student_group_exists = user.groups.filter(name="Student Group").exists() |
||||
|
|
||||
|
if not professor_group_exists: |
||||
|
if dry_run: |
||||
|
self.stdout.write( |
||||
|
f'Would add professor role to user {user.email}' |
||||
|
) |
||||
|
else: |
||||
|
user.add_role('professor') |
||||
|
self.stdout.write( |
||||
|
f'Added professor role to user {user.email}' |
||||
|
) |
||||
|
|
||||
|
if not student_group_exists: |
||||
|
if dry_run: |
||||
|
self.stdout.write( |
||||
|
f'Would add student role to user {user.email}' |
||||
|
) |
||||
|
else: |
||||
|
user.add_role('student') |
||||
|
self.stdout.write( |
||||
|
f'Added student role to user {user.email}' |
||||
|
) |
||||
|
|
||||
|
# نمایش آمار |
||||
|
courses_taught = Course.objects.filter(professor=user).count() |
||||
|
courses_enrolled = Participant.objects.filter(student=user).count() |
||||
|
|
||||
|
self.stdout.write( |
||||
|
f' User {user.email}: teaches {courses_taught} courses, ' |
||||
|
f'enrolled in {courses_enrolled} courses' |
||||
|
) |
||||
|
|
||||
|
def get_user_statistics(self): |
||||
|
"""نمایش آمار کاربران""" |
||||
|
total_users = User.objects.count() |
||||
|
professors = User.objects.filter(groups__name="Professor Group").count() |
||||
|
students = User.objects.filter(groups__name="Student Group").count() |
||||
|
clients = User.objects.filter(groups__name="Client Group").count() |
||||
|
|
||||
|
# کاربرانی که چندین نقش دارند |
||||
|
multi_role_users = User.objects.filter( |
||||
|
groups__name__in=["Professor Group", "Student Group"] |
||||
|
).annotate( |
||||
|
role_count=models.Count('groups') |
||||
|
).filter(role_count__gt=1).count() |
||||
|
|
||||
|
self.stdout.write('\n--- User Statistics ---') |
||||
|
self.stdout.write(f'Total users: {total_users}') |
||||
|
self.stdout.write(f'Professors: {professors}') |
||||
|
self.stdout.write(f'Students: {students}') |
||||
|
self.stdout.write(f'Clients: {clients}') |
||||
|
self.stdout.write(f'Multi-role users: {multi_role_users}') |
||||
@ -0,0 +1,114 @@ |
|||||
|
""" |
||||
|
Middleware برای محدود کردن دسترسی به admin panel |
||||
|
""" |
||||
|
from django.shortcuts import redirect |
||||
|
from django.urls import reverse |
||||
|
from django.contrib import messages |
||||
|
from django.utils.translation import gettext_lazy as _ |
||||
|
|
||||
|
|
||||
|
class AdminAccessMiddleware: |
||||
|
"""Middleware برای کنترل دسترسی به admin panel""" |
||||
|
|
||||
|
def __init__(self, get_response): |
||||
|
self.get_response = get_response |
||||
|
|
||||
|
# مدلهایی که استادان نباید به آنها دسترسی داشته باشند |
||||
|
self.restricted_models = [ |
||||
|
'user', |
||||
|
'professoruser', |
||||
|
'studentuser', |
||||
|
'clientuser', |
||||
|
'transaction', |
||||
|
'transactionparticipant', |
||||
|
'book', |
||||
|
'bookcollection', |
||||
|
'article', |
||||
|
'podcast', |
||||
|
'chat', |
||||
|
'roommessage', |
||||
|
'hadis', |
||||
|
'hadiscategory', |
||||
|
'globalpreference', |
||||
|
'coursecategory', |
||||
|
] |
||||
|
|
||||
|
# URL patterns که استادان نباید به آنها دسترسی داشته باشند |
||||
|
self.restricted_urls = [ |
||||
|
'/admin/account/', |
||||
|
'/admin/transaction/', |
||||
|
'/admin/library/', |
||||
|
'/admin/article/', |
||||
|
'/admin/podcast/', |
||||
|
'/admin/chat/', |
||||
|
'/admin/hadis/', |
||||
|
'/admin/dynamic_preferences/', |
||||
|
'/admin/course/coursecategory/', |
||||
|
] |
||||
|
|
||||
|
def __call__(self, request): |
||||
|
# بررسی دسترسی قبل از پردازش request |
||||
|
if self.should_restrict_access(request): |
||||
|
return self.handle_restricted_access(request) |
||||
|
|
||||
|
response = self.get_response(request) |
||||
|
return response |
||||
|
|
||||
|
def should_restrict_access(self, request): |
||||
|
"""آیا باید دسترسی محدود شود؟""" |
||||
|
# فقط برای admin URLs |
||||
|
if not request.path.startswith('/admin/'): |
||||
|
return False |
||||
|
|
||||
|
# اولویت اول: staff یا admin - دسترسی کامل بدون محدودیت |
||||
|
if (request.user.is_authenticated and |
||||
|
(request.user.is_staff or |
||||
|
request.user.has_role('admin') or |
||||
|
request.user.has_role('super_admin'))): |
||||
|
return False |
||||
|
|
||||
|
# اگر کاربر احراز هویت نشده، دسترسی ندارد |
||||
|
if not request.user.is_authenticated: |
||||
|
return True |
||||
|
|
||||
|
# اگر کاربر استاد نیست، دسترسی ندارد |
||||
|
if not (request.user.is_authenticated and request.user.has_role('professor')): |
||||
|
return True |
||||
|
|
||||
|
# برای استادان: بررسی URL های محدود شده |
||||
|
for restricted_url in self.restricted_urls: |
||||
|
if request.path.startswith(restricted_url): |
||||
|
return True |
||||
|
|
||||
|
# برای استادان: بررسی مدلهای محدود شده |
||||
|
path_parts = request.path.strip('/').split('/') |
||||
|
if len(path_parts) >= 3: # admin/app/model/ |
||||
|
app_name = path_parts[1] |
||||
|
model_name = path_parts[2] |
||||
|
|
||||
|
if model_name in self.restricted_models: |
||||
|
return True |
||||
|
|
||||
|
return False |
||||
|
|
||||
|
def handle_restricted_access(self, request): |
||||
|
"""مدیریت دسترسی محدود شده""" |
||||
|
if not request.user.is_authenticated: |
||||
|
return redirect('admin:login') |
||||
|
|
||||
|
# اگر کاربر استاد است، در همان admin panel میماند |
||||
|
if request.user.is_authenticated and request.user.has_role('professor'): |
||||
|
# فقط پیام میدهیم که دسترسی محدود است |
||||
|
messages.info( |
||||
|
request, |
||||
|
_('You have limited access as a professor.') |
||||
|
) |
||||
|
# به صفحه اصلی admin هدایت میکنیم |
||||
|
return redirect('/admin/') |
||||
|
|
||||
|
# سایر کاربران |
||||
|
messages.error( |
||||
|
request, |
||||
|
_('You do not have permission to access this page.') |
||||
|
) |
||||
|
return redirect('admin:login') |
||||
@ -0,0 +1,240 @@ |
|||||
|
""" |
||||
|
تستهای سیستم نقشهای چندگانه |
||||
|
""" |
||||
|
from django.test import TestCase |
||||
|
from django.contrib.auth.models import Group |
||||
|
from apps.account.models import User |
||||
|
from apps.course.models import Course, CourseCategory, Participant |
||||
|
from apps.transaction.models import TransactionParticipant |
||||
|
|
||||
|
|
||||
|
class MultipleRolesTestCase(TestCase): |
||||
|
def setUp(self): |
||||
|
"""راهاندازی دادههای تست""" |
||||
|
# ایجاد گروهها |
||||
|
self.professor_group = Group.objects.create(name="Professor Group") |
||||
|
self.student_group = Group.objects.create(name="Student Group") |
||||
|
self.client_group = Group.objects.create(name="Client Group") |
||||
|
|
||||
|
# ایجاد کاربر |
||||
|
self.user = User.objects.create_user( |
||||
|
email='test@example.com', |
||||
|
fullname='Test User', |
||||
|
password='testpass123' |
||||
|
) |
||||
|
# حذف language برای جلوگیری از خطای foreign key |
||||
|
self.user.language = None |
||||
|
self.user.save() |
||||
|
|
||||
|
# ایجاد دستهبندی دوره |
||||
|
self.category = CourseCategory.objects.create( |
||||
|
name='Test Category', |
||||
|
slug='test-category' |
||||
|
) |
||||
|
|
||||
|
def test_user_can_have_multiple_roles(self): |
||||
|
"""تست اینکه کاربر میتواند چندین نقش داشته باشد""" |
||||
|
# اضافه کردن نقش professor |
||||
|
self.user.add_role('professor') |
||||
|
self.assertTrue(self.user.has_role('professor')) |
||||
|
self.assertEqual(self.user.primary_role, User.UserType.PROFESSOR) |
||||
|
|
||||
|
# اضافه کردن نقش student |
||||
|
self.user.add_role('student') |
||||
|
self.assertTrue(self.user.has_role('student')) |
||||
|
self.assertTrue(self.user.has_role('professor')) # نقش قبلی حفظ شده |
||||
|
|
||||
|
# نقش اصلی باید professor باشد (اولویت بالاتر) |
||||
|
self.assertEqual(self.user.primary_role, User.UserType.PROFESSOR) |
||||
|
|
||||
|
# لیست تمام نقشها |
||||
|
roles = self.user.get_all_roles() |
||||
|
self.assertIn('professor', roles) |
||||
|
self.assertIn('student', roles) |
||||
|
|
||||
|
def test_remove_role(self): |
||||
|
"""تست حذف نقش""" |
||||
|
# اضافه کردن دو نقش |
||||
|
self.user.add_role('professor') |
||||
|
self.user.add_role('student') |
||||
|
|
||||
|
# حذف نقش professor |
||||
|
self.user.remove_role('professor') |
||||
|
self.assertFalse(self.user.has_role('professor')) |
||||
|
self.assertTrue(self.user.has_role('student')) |
||||
|
|
||||
|
# نقش اصلی باید student شود |
||||
|
self.assertEqual(self.user.primary_role, User.UserType.STUDENT) |
||||
|
|
||||
|
def test_course_creation_and_enrollment(self): |
||||
|
"""تست ایجاد دوره و ثبتنام در دوره دیگر""" |
||||
|
# کاربر نقش professor میگیرد |
||||
|
self.user.add_role('professor') |
||||
|
|
||||
|
# ایجاد دوره |
||||
|
course1 = Course.objects.create( |
||||
|
title='Test Course 1', |
||||
|
slug='test-course-1', |
||||
|
category=self.category, |
||||
|
professor=self.user, |
||||
|
level='beginner', |
||||
|
duration=10, |
||||
|
lessons_count=5, |
||||
|
description='Test description' |
||||
|
) |
||||
|
|
||||
|
# بررسی اینکه کاربر میتواند دوره را مدیریت کند |
||||
|
self.assertTrue(self.user.can_manage_course(course1)) |
||||
|
|
||||
|
# کاربر دیگری دوره دیگری میسازد |
||||
|
other_user = User.objects.create_user( |
||||
|
email='other@example.com', |
||||
|
fullname='Other User', |
||||
|
password='testpass123' |
||||
|
) |
||||
|
other_user.language = None |
||||
|
other_user.save() |
||||
|
other_user.add_role('professor') |
||||
|
|
||||
|
course2 = Course.objects.create( |
||||
|
title='Test Course 2', |
||||
|
slug='test-course-2', |
||||
|
category=self.category, |
||||
|
professor=other_user, |
||||
|
level='beginner', |
||||
|
duration=10, |
||||
|
lessons_count=5, |
||||
|
description='Test description 2' |
||||
|
) |
||||
|
|
||||
|
# کاربر اول در دوره دوم شرکت میکند |
||||
|
self.user.add_role('student') |
||||
|
participant = Participant.objects.create( |
||||
|
student=self.user, |
||||
|
course=course2 |
||||
|
) |
||||
|
|
||||
|
# بررسی نقشها |
||||
|
self.assertTrue(self.user.has_role('professor')) # هنوز استاد است |
||||
|
self.assertTrue(self.user.has_role('student')) # و دانشآموز هم هست |
||||
|
|
||||
|
# بررسی دسترسیها |
||||
|
self.assertTrue(self.user.can_manage_course(course1)) # دوره خودش |
||||
|
self.assertFalse(self.user.can_manage_course(course2)) # دوره دیگری |
||||
|
|
||||
|
def test_transaction_preserves_professor_role(self): |
||||
|
"""تست اینکه transaction نقش professor را حفظ میکند""" |
||||
|
# کاربر استاد میشود |
||||
|
self.user.add_role('professor') |
||||
|
|
||||
|
# ایجاد دوره |
||||
|
course = Course.objects.create( |
||||
|
title='Test Course', |
||||
|
slug='test-course', |
||||
|
category=self.category, |
||||
|
professor=self.user, |
||||
|
level='beginner', |
||||
|
duration=10, |
||||
|
lessons_count=5, |
||||
|
description='Test description', |
||||
|
is_free=True |
||||
|
) |
||||
|
|
||||
|
# شبیهسازی transaction (کاربر در دورهای شرکت میکند) |
||||
|
if not self.user.has_role('student'): |
||||
|
self.user.add_role('student') |
||||
|
|
||||
|
# بررسی اینکه هر دو نقش حفظ شدهاند |
||||
|
self.assertTrue(self.user.has_role('professor')) |
||||
|
self.assertTrue(self.user.has_role('student')) |
||||
|
|
||||
|
# نقش اصلی باید professor باشد |
||||
|
self.assertEqual(self.user.primary_role, User.UserType.PROFESSOR) |
||||
|
|
||||
|
def test_permissions(self): |
||||
|
"""تست دسترسیها""" |
||||
|
# کاربر بدون نقش خاص |
||||
|
self.assertFalse(self.user.can_teach_course()) |
||||
|
self.assertTrue(self.user.can_enroll_course()) |
||||
|
|
||||
|
# اضافه کردن نقش professor |
||||
|
self.user.add_role('professor') |
||||
|
self.assertTrue(self.user.can_teach_course()) |
||||
|
self.assertTrue(self.user.can_enroll_course()) |
||||
|
|
||||
|
# حذف نقش professor |
||||
|
self.user.remove_role('professor') |
||||
|
self.assertFalse(self.user.can_teach_course()) |
||||
|
self.assertTrue(self.user.can_enroll_course()) |
||||
|
|
||||
|
def test_user_type_based_on_groups_compatibility(self): |
||||
|
"""تست سازگاری با property قدیمی""" |
||||
|
# اضافه کردن نقش student |
||||
|
self.user.add_role('student') |
||||
|
self.user.refresh_from_db() # بروزرسانی از دیتابیس |
||||
|
self.assertEqual(self.user.user_type_based_on_groups, User.UserType.STUDENT) |
||||
|
|
||||
|
# اضافه کردن نقش professor |
||||
|
self.user.add_role('professor') |
||||
|
self.user.refresh_from_db() # بروزرسانی از دیتابیس |
||||
|
# property قدیمی بر اساس اولویت کار میکند - student اول چک میشود |
||||
|
# پس باید student برگرداند نه professor |
||||
|
self.assertEqual(self.user.user_type_based_on_groups, User.UserType.STUDENT) |
||||
|
|
||||
|
# حذف نقش student |
||||
|
self.user.remove_role('student') |
||||
|
self.user.refresh_from_db() |
||||
|
self.assertEqual(self.user.user_type_based_on_groups, User.UserType.PROFESSOR) |
||||
|
|
||||
|
# حذف همه نقشها |
||||
|
self.user.remove_role('professor') |
||||
|
self.user.refresh_from_db() |
||||
|
self.assertEqual(self.user.user_type_based_on_groups, User.UserType.CLIENT) |
||||
|
|
||||
|
def test_admin_priority_over_professor(self): |
||||
|
"""تست اولویت admin بر professor""" |
||||
|
# کاربر هم admin و هم professor است |
||||
|
self.user.add_role('admin') |
||||
|
self.user.add_role('professor') |
||||
|
self.user.is_staff = True |
||||
|
self.user.save() |
||||
|
|
||||
|
# ایجاد دوره |
||||
|
course = Course.objects.create( |
||||
|
title='Test Course', |
||||
|
slug='test-course', |
||||
|
category=self.category, |
||||
|
professor=self.user, |
||||
|
level='beginner', |
||||
|
duration=10, |
||||
|
lessons_count=5, |
||||
|
description='Test description' |
||||
|
) |
||||
|
|
||||
|
# admin باید دسترسی کامل داشته باشد |
||||
|
self.assertTrue(self.user.can_manage_course(course)) |
||||
|
self.assertTrue(self.user.can_teach_course()) |
||||
|
|
||||
|
# حتی اگر دوره متعلق به کس دیگری باشد |
||||
|
other_user = User.objects.create_user( |
||||
|
email='other@example.com', |
||||
|
fullname='Other User', |
||||
|
password='testpass123' |
||||
|
) |
||||
|
other_user.language = None |
||||
|
other_user.save() |
||||
|
other_user.add_role('professor') |
||||
|
|
||||
|
other_course = Course.objects.create( |
||||
|
title='Other Course', |
||||
|
slug='other-course', |
||||
|
category=self.category, |
||||
|
professor=other_user, |
||||
|
level='beginner', |
||||
|
duration=10, |
||||
|
lessons_count=5, |
||||
|
description='Other description' |
||||
|
) |
||||
|
|
||||
|
# admin باید به دوره دیگران هم دسترسی داشته باشد |
||||
|
self.assertTrue(self.user.can_manage_course(other_course)) |
||||
@ -0,0 +1 @@ |
|||||
|
|
||||
@ -0,0 +1,62 @@ |
|||||
|
# Chat Management Commands |
||||
|
|
||||
|
## clear_chat_data |
||||
|
|
||||
|
این management command برای پاک کردن دادههای چت طراحی شده است و دو حالت کاری دارد: |
||||
|
|
||||
|
### حالت پیشفرض (محافظت از رومهای کورس) |
||||
|
در این حالت: |
||||
|
- همه پیامها (ChatMessage) حذف میشوند |
||||
|
- همه وضعیتهای خواندن پیام (MessageReadStatus) حذف میشوند |
||||
|
- رومهایی که مربوط به کورس نیستند (course=null) حذف میشوند |
||||
|
- رومهایی که مربوط به کورس هستند حفظ میشوند اما پیامهایشان حذف میشود |
||||
|
- تعداد پیامهای خوانده نشده رومهای کورس صفر میشود |
||||
|
|
||||
|
### حالت حذف کامل |
||||
|
در این حالت همه دادههای چت شامل رومهای کورس نیز حذف میشوند. |
||||
|
|
||||
|
## استفاده |
||||
|
|
||||
|
### حالت پیشفرض (محافظت از رومهای کورس) |
||||
|
```bash |
||||
|
# با تأیید کاربر |
||||
|
python manage.py clear_chat_data |
||||
|
|
||||
|
# بدون تأیید کاربر |
||||
|
python manage.py clear_chat_data --force |
||||
|
``` |
||||
|
|
||||
|
### حذف کامل همه دادهها |
||||
|
```bash |
||||
|
# با تأیید کاربر |
||||
|
python manage.py clear_chat_data --all-rooms |
||||
|
|
||||
|
# بدون تأیید کاربر |
||||
|
python manage.py clear_chat_data --all-rooms --force |
||||
|
``` |
||||
|
|
||||
|
## پارامترها |
||||
|
|
||||
|
- `--force`: اجرای دستور بدون درخواست تأیید از کاربر |
||||
|
- `--all-rooms`: حذف همه رومها شامل رومهای مربوط به کورس |
||||
|
|
||||
|
## نکات مهم |
||||
|
|
||||
|
1. **ایمنی**: دستور در یک transaction اجرا میشود تا در صورت خطا، تغییرات rollback شوند |
||||
|
2. **گزارشدهی**: دستور تعداد رکوردهای حذف شده را نمایش میدهد |
||||
|
3. **محافظت از دادههای کورس**: در حالت پیشفرض، رومهای مربوط به کورس حفظ میشوند |
||||
|
4. **بازنشانی شمارنده**: تعداد پیامهای خوانده نشده رومهای کورس به صفر تنظیم میشود |
||||
|
|
||||
|
## مثال خروجی |
||||
|
|
||||
|
``` |
||||
|
Found: |
||||
|
- 150 messages |
||||
|
- 75 read statuses |
||||
|
- 10 total rooms (3 course rooms, 7 non-course rooms) |
||||
|
✓ Deleted 75 MessageReadStatus records |
||||
|
✓ Deleted 150 ChatMessage records |
||||
|
✓ Deleted 7 non-course RoomMessage records |
||||
|
✓ Reset unread_messages_count for 3 course rooms |
||||
|
Chat data clearing completed successfully! |
||||
|
``` |
||||
@ -0,0 +1 @@ |
|||||
|
|
||||
@ -0,0 +1,79 @@ |
|||||
|
from django.core.management.base import BaseCommand |
||||
|
from django.db import transaction |
||||
|
from django.utils.translation import gettext_lazy as _ |
||||
|
|
||||
|
from apps.chat.models import RoomMessage, ChatMessage, MessageReadStatus |
||||
|
|
||||
|
|
||||
|
class Command(BaseCommand): |
||||
|
help = 'Clear chat data: all rooms, messages and read statuses, but preserve course-related rooms' |
||||
|
|
||||
|
def add_arguments(self, parser): |
||||
|
parser.add_argument( |
||||
|
'--force', |
||||
|
action='store_true', |
||||
|
dest='force', |
||||
|
help=_('Force deletion without confirmation'), |
||||
|
) |
||||
|
parser.add_argument( |
||||
|
'--all-rooms', |
||||
|
action='store_true', |
||||
|
dest='all_rooms', |
||||
|
help=_('Delete ALL rooms including course-related rooms'), |
||||
|
) |
||||
|
|
||||
|
def handle(self, *args, **options): |
||||
|
force = options['force'] |
||||
|
all_rooms = options['all_rooms'] |
||||
|
|
||||
|
if not force: |
||||
|
if all_rooms: |
||||
|
confirm = input(_('This will delete ALL chat data including course rooms. Are you sure? (yes/no): ')) |
||||
|
else: |
||||
|
confirm = input(_('This will delete all messages and read statuses, and non-course rooms. Course rooms will be preserved but their messages will be deleted. Are you sure? (yes/no): ')) |
||||
|
|
||||
|
if confirm.lower() != 'yes': |
||||
|
self.stdout.write(self.style.WARNING(_('Operation cancelled.'))) |
||||
|
return |
||||
|
|
||||
|
try: |
||||
|
with transaction.atomic(): |
||||
|
# Count existing data |
||||
|
total_messages = ChatMessage.objects.count() |
||||
|
total_read_statuses = MessageReadStatus.objects.count() |
||||
|
total_rooms = RoomMessage.objects.count() |
||||
|
course_rooms = RoomMessage.objects.filter(course__isnull=False).count() |
||||
|
non_course_rooms = RoomMessage.objects.filter(course__isnull=True).count() |
||||
|
|
||||
|
self.stdout.write(self.style.WARNING(f'Found:')) |
||||
|
self.stdout.write(f' - {total_messages} messages') |
||||
|
self.stdout.write(f' - {total_read_statuses} read statuses') |
||||
|
self.stdout.write(f' - {total_rooms} total rooms ({course_rooms} course rooms, {non_course_rooms} non-course rooms)') |
||||
|
|
||||
|
# Step 1: Delete all MessageReadStatus records |
||||
|
deleted_read_statuses = MessageReadStatus.objects.all().delete()[0] |
||||
|
self.stdout.write(self.style.SUCCESS(f'✓ Deleted {deleted_read_statuses} MessageReadStatus records')) |
||||
|
|
||||
|
# Step 2: Delete all ChatMessage records |
||||
|
deleted_messages = ChatMessage.objects.all().delete()[0] |
||||
|
self.stdout.write(self.style.SUCCESS(f'✓ Deleted {deleted_messages} ChatMessage records')) |
||||
|
|
||||
|
# Step 3: Handle rooms based on options |
||||
|
if all_rooms: |
||||
|
# Delete ALL rooms |
||||
|
deleted_rooms = RoomMessage.objects.all().delete()[0] |
||||
|
self.stdout.write(self.style.SUCCESS(f'✓ Deleted {deleted_rooms} RoomMessage records (including course rooms)')) |
||||
|
else: |
||||
|
# Delete only non-course rooms (rooms without course relationship) |
||||
|
deleted_non_course_rooms = RoomMessage.objects.filter(course__isnull=True).delete()[0] |
||||
|
self.stdout.write(self.style.SUCCESS(f'✓ Deleted {deleted_non_course_rooms} non-course RoomMessage records')) |
||||
|
|
||||
|
# Reset unread_messages_count for course rooms |
||||
|
course_rooms_updated = RoomMessage.objects.filter(course__isnull=False).update(unread_messages_count=0) |
||||
|
self.stdout.write(self.style.SUCCESS(f'✓ Reset unread_messages_count for {course_rooms_updated} course rooms')) |
||||
|
|
||||
|
self.stdout.write(self.style.SUCCESS(_('Chat data clearing completed successfully!'))) |
||||
|
|
||||
|
except Exception as e: |
||||
|
self.stdout.write(self.style.ERROR(f'Error occurred: {str(e)}')) |
||||
|
raise |
||||
@ -0,0 +1,181 @@ |
|||||
|
""" |
||||
|
Base admin classes برای استادان |
||||
|
""" |
||||
|
from django.contrib import admin |
||||
|
from django.contrib.admin import ModelAdmin |
||||
|
from django.utils.translation import gettext_lazy as _ |
||||
|
from unfold.admin import ModelAdmin as UnfoldModelAdmin |
||||
|
|
||||
|
|
||||
|
class ProfessorBaseAdmin(UnfoldModelAdmin): |
||||
|
"""Base admin class برای استادان""" |
||||
|
|
||||
|
def has_module_permission(self, request): |
||||
|
"""آیا کاربر میتواند این ماژول را ببیند؟""" |
||||
|
# چک کردن احراز هویت |
||||
|
if not request.user.is_authenticated: |
||||
|
return False |
||||
|
|
||||
|
# اولویت اول: staff یا admin |
||||
|
if request.user.is_staff or request.user.has_role('admin') or request.user.has_role('super_admin'): |
||||
|
return True |
||||
|
# اولویت دوم: professor |
||||
|
return request.user.has_role('professor') |
||||
|
|
||||
|
def has_view_permission(self, request, obj=None): |
||||
|
"""آیا میتواند مشاهده کند؟""" |
||||
|
# چک کردن احراز هویت |
||||
|
if not request.user.is_authenticated: |
||||
|
return False |
||||
|
|
||||
|
# اولویت اول: staff یا admin - دسترسی کامل |
||||
|
if request.user.is_staff or request.user.has_role('admin') or request.user.has_role('super_admin'): |
||||
|
return True |
||||
|
# اولویت دوم: professor - دسترسی محدود |
||||
|
if request.user.has_role('professor'): |
||||
|
if obj is None: |
||||
|
return True |
||||
|
return self.can_access_object(request.user, obj) |
||||
|
return False |
||||
|
|
||||
|
def has_add_permission(self, request): |
||||
|
"""آیا میتواند اضافه کند؟""" |
||||
|
# چک کردن احراز هویت |
||||
|
if not request.user.is_authenticated: |
||||
|
return False |
||||
|
|
||||
|
# اولویت اول: staff یا admin - دسترسی کامل |
||||
|
if request.user.is_staff or request.user.has_role('admin') or request.user.has_role('super_admin'): |
||||
|
return True |
||||
|
# اولویت دوم: professor |
||||
|
return request.user.has_role('professor') |
||||
|
|
||||
|
def has_change_permission(self, request, obj=None): |
||||
|
"""آیا میتواند تغییر دهد؟""" |
||||
|
# چک کردن احراز هویت |
||||
|
if not request.user.is_authenticated: |
||||
|
return False |
||||
|
|
||||
|
# اولویت اول: staff یا admin - دسترسی کامل |
||||
|
if request.user.is_staff or request.user.has_role('admin') or request.user.has_role('super_admin'): |
||||
|
return True |
||||
|
# اولویت دوم: professor - دسترسی محدود |
||||
|
if request.user.has_role('professor'): |
||||
|
if obj is None: |
||||
|
return True |
||||
|
return self.can_access_object(request.user, obj) |
||||
|
return False |
||||
|
|
||||
|
def has_delete_permission(self, request, obj=None): |
||||
|
"""آیا میتواند حذف کند؟""" |
||||
|
# چک کردن احراز هویت |
||||
|
if not request.user.is_authenticated: |
||||
|
return False |
||||
|
|
||||
|
# اولویت اول: staff یا admin - دسترسی کامل |
||||
|
if request.user.is_staff or request.user.has_role('admin') or request.user.has_role('super_admin'): |
||||
|
return True |
||||
|
# اولویت دوم: professor - دسترسی محدود |
||||
|
if request.user.has_role('professor'): |
||||
|
if obj is None: |
||||
|
return True |
||||
|
return self.can_access_object(request.user, obj) |
||||
|
return False |
||||
|
|
||||
|
def can_access_object(self, user, obj): |
||||
|
"""آیا کاربر میتواند به این object دسترسی داشته باشد؟""" |
||||
|
# این method باید در subclass ها override شود |
||||
|
return True |
||||
|
|
||||
|
def get_queryset(self, request): |
||||
|
"""فیلتر کردن queryset بر اساس دسترسی کاربر""" |
||||
|
qs = super().get_queryset(request) |
||||
|
|
||||
|
# چک کردن احراز هویت |
||||
|
if not request.user.is_authenticated: |
||||
|
return qs.none() |
||||
|
|
||||
|
# اولویت اول: staff یا admin - دسترسی کامل |
||||
|
if request.user.is_staff or request.user.has_role('admin') or request.user.has_role('super_admin'): |
||||
|
return qs |
||||
|
# اولویت دوم: professor - دسترسی محدود |
||||
|
if request.user.has_role('professor'): |
||||
|
return self.filter_queryset_for_professor(request, qs) |
||||
|
return qs.none() |
||||
|
|
||||
|
def filter_queryset_for_professor(self, request, queryset): |
||||
|
"""فیلتر کردن queryset برای استاد""" |
||||
|
# این method باید در subclass ها override شود |
||||
|
return queryset |
||||
|
|
||||
|
|
||||
|
class CourseRelatedAdmin(ProfessorBaseAdmin): |
||||
|
"""Base admin برای مدلهایی که به Course مرتبط هستند""" |
||||
|
|
||||
|
def can_access_object(self, user, obj): |
||||
|
"""چک کردن دسترسی بر اساس Course""" |
||||
|
course = self.get_course_from_object(obj) |
||||
|
if course: |
||||
|
return user.can_manage_course(course) |
||||
|
return False |
||||
|
|
||||
|
def filter_queryset_for_professor(self, request, queryset): |
||||
|
"""فیلتر کردن بر اساس دورههای استاد""" |
||||
|
return queryset.filter(course__professor=request.user) |
||||
|
|
||||
|
def get_course_from_object(self, obj): |
||||
|
"""دریافت Course از object""" |
||||
|
# این method باید در subclass ها override شود |
||||
|
if hasattr(obj, 'course'): |
||||
|
return obj.course |
||||
|
return None |
||||
|
|
||||
|
|
||||
|
class DirectCourseAdmin(ProfessorBaseAdmin): |
||||
|
"""Admin برای خود مدل Course""" |
||||
|
|
||||
|
def can_access_object(self, user, obj): |
||||
|
"""چک کردن دسترسی به Course""" |
||||
|
return user.can_manage_course(obj) |
||||
|
|
||||
|
def filter_queryset_for_professor(self, request, queryset): |
||||
|
"""فقط دورههای خود استاد""" |
||||
|
return queryset.filter(professor=request.user) |
||||
|
|
||||
|
|
||||
|
class AttachmentGlossaryBaseAdmin(ProfessorBaseAdmin): |
||||
|
"""Base admin برای Attachment و Glossary""" |
||||
|
|
||||
|
def can_access_object(self, user, obj): |
||||
|
"""چک کردن دسترسی - فقط اگر در دورههای استاد استفاده شده""" |
||||
|
# چک کنیم که آیا این attachment/glossary در دورههای استاد استفاده شده |
||||
|
return self.is_used_in_professor_courses(user, obj) |
||||
|
|
||||
|
def filter_queryset_for_professor(self, request, queryset): |
||||
|
"""فیلتر کردن بر اساس استفاده در دورههای استاد""" |
||||
|
return self.filter_by_professor_usage(request.user, queryset) |
||||
|
|
||||
|
def is_used_in_professor_courses(self, user, obj): |
||||
|
"""آیا در دورههای استاد استفاده شده؟""" |
||||
|
# باید در subclass ها پیادهسازی شود |
||||
|
return True |
||||
|
|
||||
|
def filter_by_professor_usage(self, user, queryset): |
||||
|
"""فیلتر کردن بر اساس استفاده در دورههای استاد""" |
||||
|
# باید در subclass ها پیادهسازی شود |
||||
|
return queryset |
||||
|
|
||||
|
|
||||
|
class CertificateBaseAdmin(ProfessorBaseAdmin): |
||||
|
"""Base admin برای Certificate""" |
||||
|
|
||||
|
def can_access_object(self, user, obj): |
||||
|
"""چک کردن دسترسی به Certificate""" |
||||
|
# فقط certificate های دانشآموزان دورههای خودش |
||||
|
if hasattr(obj, 'course') and obj.course: |
||||
|
return user.can_manage_course(obj.course) |
||||
|
return False |
||||
|
|
||||
|
def filter_queryset_for_professor(self, request, queryset): |
||||
|
"""فقط certificate های دانشآموزان دورههای استاد""" |
||||
|
return queryset.filter(course__professor=request.user) |
||||
@ -0,0 +1,216 @@ |
|||||
|
""" |
||||
|
تستهای API برای سیستم نقشهای چندگانه |
||||
|
""" |
||||
|
from django.test import TestCase |
||||
|
from django.urls import reverse |
||||
|
from rest_framework.test import APIClient |
||||
|
from rest_framework import status |
||||
|
from django.contrib.auth.models import Group |
||||
|
from apps.account.models import User |
||||
|
from apps.course.models import Course, CourseCategory, Participant |
||||
|
from apps.transaction.models import TransactionParticipant |
||||
|
|
||||
|
|
||||
|
class MultipleRolesAPITestCase(TestCase): |
||||
|
def setUp(self): |
||||
|
"""راهاندازی دادههای تست""" |
||||
|
# ایجاد گروهها |
||||
|
Group.objects.create(name="Professor Group") |
||||
|
Group.objects.create(name="Student Group") |
||||
|
Group.objects.create(name="Client Group") |
||||
|
|
||||
|
# ایجاد کاربر |
||||
|
self.user = User.objects.create_user( |
||||
|
email='test@example.com', |
||||
|
fullname='Test User', |
||||
|
password='testpass123' |
||||
|
) |
||||
|
|
||||
|
# ایجاد دستهبندی دوره |
||||
|
self.category = CourseCategory.objects.create( |
||||
|
name='Test Category', |
||||
|
slug='test-category' |
||||
|
) |
||||
|
|
||||
|
# راهاندازی API client |
||||
|
self.client = APIClient() |
||||
|
self.client.force_authenticate(user=self.user) |
||||
|
|
||||
|
def test_user_profile_basic_functionality(self): |
||||
|
"""تست عملکرد اصلی profile کاربر""" |
||||
|
# اضافه کردن نقشها |
||||
|
self.user.add_role('professor') |
||||
|
self.user.add_role('student') |
||||
|
|
||||
|
# تست متدهای جدید User model |
||||
|
self.assertTrue(self.user.has_role('professor')) |
||||
|
self.assertTrue(self.user.has_role('student')) |
||||
|
|
||||
|
roles = self.user.get_all_roles() |
||||
|
self.assertIn('professor', roles) |
||||
|
self.assertIn('student', roles) |
||||
|
|
||||
|
# نقش اصلی باید professor باشد (اولویت بالاتر) |
||||
|
self.assertEqual(self.user.primary_role, User.UserType.PROFESSOR) |
||||
|
|
||||
|
def test_course_access_for_professor(self): |
||||
|
"""تست دسترسی استاد به دوره خودش""" |
||||
|
# کاربر استاد میشود و دوره میسازد |
||||
|
self.user.add_role('professor') |
||||
|
|
||||
|
course = Course.objects.create( |
||||
|
title='Test Course', |
||||
|
slug='test-course', |
||||
|
category=self.category, |
||||
|
professor=self.user, |
||||
|
level='beginner', |
||||
|
duration=10, |
||||
|
lessons_count=5, |
||||
|
description='Test description' |
||||
|
) |
||||
|
|
||||
|
# تست serializer |
||||
|
from apps.course.serializers import CourseDetailSerializer |
||||
|
|
||||
|
# شبیهسازی request context |
||||
|
from django.test import RequestFactory |
||||
|
factory = RequestFactory() |
||||
|
request = factory.get('/') |
||||
|
request.user = self.user |
||||
|
|
||||
|
serializer = CourseDetailSerializer(course, context={'request': request}) |
||||
|
data = serializer.data |
||||
|
|
||||
|
# استاد باید دسترسی داشته باشد |
||||
|
self.assertTrue(data['access']) |
||||
|
|
||||
|
def test_course_enrollment_preserves_professor_role(self): |
||||
|
"""تست اینکه ثبتنام در دوره نقش professor را حفظ میکند""" |
||||
|
# کاربر استاد میشود |
||||
|
self.user.add_role('professor') |
||||
|
|
||||
|
# کاربر دیگری دوره میسازد |
||||
|
other_user = User.objects.create_user( |
||||
|
email='other@example.com', |
||||
|
fullname='Other User', |
||||
|
password='testpass123' |
||||
|
) |
||||
|
other_user.add_role('professor') |
||||
|
|
||||
|
course = Course.objects.create( |
||||
|
title='Test Course', |
||||
|
slug='test-course', |
||||
|
category=self.category, |
||||
|
professor=other_user, |
||||
|
level='beginner', |
||||
|
duration=10, |
||||
|
lessons_count=5, |
||||
|
description='Test description', |
||||
|
is_free=True |
||||
|
) |
||||
|
|
||||
|
# شبیهسازی transaction |
||||
|
transaction_data = { |
||||
|
'participant_infos': [{'email': self.user.email}] |
||||
|
} |
||||
|
|
||||
|
# شبیهسازی منطق transaction |
||||
|
if not self.user.has_role('student'): |
||||
|
self.user.add_role('student') |
||||
|
|
||||
|
Participant.objects.create( |
||||
|
student=self.user, |
||||
|
course=course |
||||
|
) |
||||
|
|
||||
|
# بررسی اینکه هر دو نقش حفظ شدهاند |
||||
|
self.assertTrue(self.user.has_role('professor')) |
||||
|
self.assertTrue(self.user.has_role('student')) |
||||
|
|
||||
|
# بررسی اینکه کاربر میتواند دوره خودش را مدیریت کند |
||||
|
own_course = Course.objects.create( |
||||
|
title='Own Course', |
||||
|
slug='own-course', |
||||
|
category=self.category, |
||||
|
professor=self.user, |
||||
|
level='beginner', |
||||
|
duration=10, |
||||
|
lessons_count=5, |
||||
|
description='Own course description' |
||||
|
) |
||||
|
|
||||
|
self.assertTrue(self.user.can_manage_course(own_course)) |
||||
|
self.assertFalse(self.user.can_manage_course(course)) # دوره دیگری |
||||
|
|
||||
|
def test_course_access_for_professor_student(self): |
||||
|
"""تست دسترسی دوره برای کاربری که هم استاد و هم دانشآموز است""" |
||||
|
# کاربر استاد میشود |
||||
|
self.user.add_role('professor') |
||||
|
|
||||
|
# دوره خودش |
||||
|
own_course = Course.objects.create( |
||||
|
title='Own Course', |
||||
|
slug='own-course', |
||||
|
category=self.category, |
||||
|
professor=self.user, |
||||
|
level='beginner', |
||||
|
duration=10, |
||||
|
lessons_count=5, |
||||
|
description='Own course description' |
||||
|
) |
||||
|
|
||||
|
# دوره دیگری |
||||
|
other_user = User.objects.create_user( |
||||
|
email='other@example.com', |
||||
|
fullname='Other User', |
||||
|
password='testpass123' |
||||
|
) |
||||
|
other_user.add_role('professor') |
||||
|
|
||||
|
other_course = Course.objects.create( |
||||
|
title='Other Course', |
||||
|
slug='other-course', |
||||
|
category=self.category, |
||||
|
professor=other_user, |
||||
|
level='beginner', |
||||
|
duration=10, |
||||
|
lessons_count=5, |
||||
|
description='Other course description' |
||||
|
) |
||||
|
|
||||
|
# کاربر در دوره دیگری شرکت میکند |
||||
|
self.user.add_role('student') |
||||
|
Participant.objects.create( |
||||
|
student=self.user, |
||||
|
course=other_course |
||||
|
) |
||||
|
|
||||
|
# تست دسترسیها |
||||
|
from apps.course.serializers import CourseDetailSerializer |
||||
|
from django.test import RequestFactory |
||||
|
|
||||
|
factory = RequestFactory() |
||||
|
request = factory.get('/') |
||||
|
request.user = self.user |
||||
|
|
||||
|
# دسترسی به دوره خودش |
||||
|
serializer = CourseDetailSerializer(own_course, context={'request': request}) |
||||
|
data = serializer.data |
||||
|
self.assertTrue(data['access']) |
||||
|
|
||||
|
# دسترسی به دوره دیگری (به عنوان participant) |
||||
|
serializer = CourseDetailSerializer(other_course, context={'request': request}) |
||||
|
data = serializer.data |
||||
|
self.assertTrue(data['access']) |
||||
|
|
||||
|
def test_backward_compatibility(self): |
||||
|
"""تست سازگاری با کدهای قدیمی""" |
||||
|
# property قدیمی باید همچنان کار کند |
||||
|
self.user.add_role('student') |
||||
|
self.assertEqual(self.user.user_type_based_on_groups, User.UserType.STUDENT) |
||||
|
|
||||
|
self.user.add_role('professor') |
||||
|
self.assertEqual(self.user.user_type_based_on_groups, User.UserType.PROFESSOR) |
||||
|
|
||||
|
# user_type field باید بروزرسانی شود |
||||
|
self.assertEqual(self.user.user_type, User.UserType.PROFESSOR) |
||||
@ -0,0 +1,152 @@ |
|||||
|
""" |
||||
|
Test safe seeding with lock detection and retry logic |
||||
|
""" |
||||
|
|
||||
|
import time |
||||
|
from django.core.management.base import BaseCommand |
||||
|
from django.db import connection |
||||
|
from django.db.utils import OperationalError, IntegrityError |
||||
|
from apps.hadis.models import HadisSect, HadisStatus, HadisTag |
||||
|
|
||||
|
|
||||
|
class Command(BaseCommand): |
||||
|
help = 'Test safe seeding with lock detection' |
||||
|
|
||||
|
def __init__(self, *args, **kwargs): |
||||
|
super().__init__(*args, **kwargs) |
||||
|
self.retry_delay = 1 # seconds |
||||
|
self.max_retries = 3 |
||||
|
|
||||
|
def handle(self, **options): |
||||
|
self.stdout.write("🧪 Testing safe seeding with lock detection...") |
||||
|
|
||||
|
# Check database status |
||||
|
self.check_database_locks() |
||||
|
|
||||
|
# Test creating a few records |
||||
|
self.test_sect_creation() |
||||
|
self.test_status_creation() |
||||
|
self.test_tag_creation() |
||||
|
|
||||
|
self.stdout.write(self.style.SUCCESS("✅ All tests completed successfully!")) |
||||
|
|
||||
|
def safe_execute_with_retry(self, operation_name, operation_func, *args, **kwargs): |
||||
|
"""Execute database operation with retry logic for handling locks""" |
||||
|
for attempt in range(self.max_retries): |
||||
|
try: |
||||
|
self.stdout.write(f" Attempting {operation_name} (attempt {attempt + 1}/{self.max_retries})") |
||||
|
result = operation_func(*args, **kwargs) |
||||
|
self.stdout.write(f" ✓ {operation_name} completed successfully") |
||||
|
return result |
||||
|
|
||||
|
except OperationalError as e: |
||||
|
error_msg = str(e).lower() |
||||
|
if 'database is locked' in error_msg or 'deadlock' in error_msg: |
||||
|
self.stdout.write( |
||||
|
self.style.WARNING( |
||||
|
f" ⚠ Database lock detected in {operation_name}: {str(e)}" |
||||
|
) |
||||
|
) |
||||
|
if attempt < self.max_retries - 1: |
||||
|
self.stdout.write(f" ⏳ Waiting {self.retry_delay} seconds before retry...") |
||||
|
time.sleep(self.retry_delay) |
||||
|
self.retry_delay = min(self.retry_delay * 1.5, 5) |
||||
|
else: |
||||
|
self.stdout.write( |
||||
|
self.style.ERROR(f" ❌ Max retries reached for {operation_name}") |
||||
|
) |
||||
|
raise |
||||
|
else: |
||||
|
self.stdout.write( |
||||
|
self.style.ERROR(f" ❌ Non-lock error in {operation_name}: {str(e)}") |
||||
|
) |
||||
|
raise |
||||
|
|
||||
|
except IntegrityError as e: |
||||
|
if 'unique' in str(e).lower() or 'duplicate' in str(e).lower(): |
||||
|
self.stdout.write( |
||||
|
self.style.WARNING(f" ⚠ Record already exists in {operation_name}: {str(e)}") |
||||
|
) |
||||
|
return None |
||||
|
else: |
||||
|
self.stdout.write( |
||||
|
self.style.ERROR(f" ❌ Integrity error in {operation_name}: {str(e)}") |
||||
|
) |
||||
|
raise |
||||
|
|
||||
|
except Exception as e: |
||||
|
self.stdout.write( |
||||
|
self.style.ERROR(f" ❌ Unexpected error in {operation_name}: {str(e)}") |
||||
|
) |
||||
|
raise |
||||
|
|
||||
|
def check_database_locks(self): |
||||
|
"""Check for existing database locks""" |
||||
|
try: |
||||
|
with connection.cursor() as cursor: |
||||
|
cursor.execute("SELECT 1;") |
||||
|
cursor.fetchone() |
||||
|
self.stdout.write("✓ Database connection is working") |
||||
|
|
||||
|
except Exception as e: |
||||
|
self.stdout.write( |
||||
|
self.style.WARNING(f"Could not check database: {str(e)}") |
||||
|
) |
||||
|
|
||||
|
def create_test_sect(self): |
||||
|
"""Create a test sect""" |
||||
|
sect, created = HadisSect.objects.get_or_create( |
||||
|
sect_type='test', |
||||
|
defaults={ |
||||
|
'title': 'Test Sect', |
||||
|
'is_active': True, |
||||
|
'order': 999 |
||||
|
} |
||||
|
) |
||||
|
if created: |
||||
|
self.stdout.write(" ✅ Created test sect") |
||||
|
else: |
||||
|
self.stdout.write(" ✓ Test sect already exists") |
||||
|
return sect |
||||
|
|
||||
|
def create_test_status(self): |
||||
|
"""Create a test status""" |
||||
|
status, created = HadisStatus.objects.get_or_create( |
||||
|
title='Test Status', |
||||
|
defaults={ |
||||
|
'color': 'blue', |
||||
|
'order': 999 |
||||
|
} |
||||
|
) |
||||
|
if created: |
||||
|
self.stdout.write(" ✅ Created test status") |
||||
|
else: |
||||
|
self.stdout.write(" ✓ Test status already exists") |
||||
|
return status |
||||
|
|
||||
|
def create_test_tag(self): |
||||
|
"""Create a test tag""" |
||||
|
tag, created = HadisTag.objects.get_or_create( |
||||
|
title='Test Tag', |
||||
|
defaults={'status': True} |
||||
|
) |
||||
|
if created: |
||||
|
self.stdout.write(" ✅ Created test tag") |
||||
|
else: |
||||
|
self.stdout.write(" ✓ Test tag already exists") |
||||
|
return tag |
||||
|
|
||||
|
def test_sect_creation(self): |
||||
|
"""Test sect creation with retry logic""" |
||||
|
self.stdout.write("🕌 Testing sect creation...") |
||||
|
self.safe_execute_with_retry("Create test sect", self.create_test_sect) |
||||
|
|
||||
|
def test_status_creation(self): |
||||
|
"""Test status creation with retry logic""" |
||||
|
self.stdout.write("📊 Testing status creation...") |
||||
|
self.safe_execute_with_retry("Create test status", self.create_test_status) |
||||
|
|
||||
|
def test_tag_creation(self): |
||||
|
"""Test tag creation with retry logic""" |
||||
|
self.stdout.write("🏷️ Testing tag creation...") |
||||
|
self.safe_execute_with_retry("Create test tag", self.create_test_tag) |
||||
@ -1,252 +0,0 @@ |
|||||
# 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` |
|
||||
@ -0,0 +1,252 @@ |
|||||
|
#!/usr/bin/env python3 |
||||
|
""" |
||||
|
Script to optimize Hadis Transmitter chains: |
||||
|
1. Limit each hadis to maximum 5 transmitter chain links |
||||
|
2. Remove excess transmitters if more than 5 |
||||
|
3. Ensure exactly one transmitter has is_gap=True (minimum 1, maximum 1) |
||||
|
""" |
||||
|
|
||||
|
import os |
||||
|
import sys |
||||
|
import django |
||||
|
from pathlib import Path |
||||
|
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 Hadis, HadisTransmitter |
||||
|
from django.db import transaction |
||||
|
from django.db.models import Count |
||||
|
|
||||
|
|
||||
|
class HadisTransmitterOptimizer: |
||||
|
"""Optimizer for Hadis Transmitter chains""" |
||||
|
|
||||
|
def __init__(self): |
||||
|
self.max_transmitters = 5 |
||||
|
self.required_gaps = 1 # Exactly one gap required |
||||
|
|
||||
|
def optimize_all_hadis(self): |
||||
|
"""Optimize transmitter chains for all hadis records""" |
||||
|
print("🔧 شروع بهینهسازی زنجیره راویان احادیث...") |
||||
|
print("=" * 60) |
||||
|
|
||||
|
# Get all hadis with transmitters |
||||
|
hadis_with_transmitters = Hadis.objects.annotate( |
||||
|
transmitter_count=Count('transmitters') |
||||
|
).filter(transmitter_count__gt=0) |
||||
|
|
||||
|
total_hadis = hadis_with_transmitters.count() |
||||
|
|
||||
|
print(f"📊 تعداد کل احادیث با راوی: {total_hadis}") |
||||
|
print(f"⚙️ حداکثر راوی در هر حدیث: {self.max_transmitters}") |
||||
|
print(f"🔗 تعداد گپ مورد نیاز: دقیقاً {self.required_gaps} گپ") |
||||
|
print("-" * 60) |
||||
|
|
||||
|
optimized_count = 0 |
||||
|
removed_transmitters = 0 |
||||
|
|
||||
|
with transaction.atomic(): |
||||
|
for i, hadis in enumerate(hadis_with_transmitters, 1): |
||||
|
hadis_title = hadis.title[:30] if hadis.title else f"حدیث {hadis.number}" |
||||
|
print(f"\n[{i}/{total_hadis}] پردازش حدیث #{hadis.number}: {hadis_title}...") |
||||
|
|
||||
|
result = self.optimize_hadis_transmitters(hadis) |
||||
|
if result['optimized']: |
||||
|
optimized_count += 1 |
||||
|
removed_transmitters += result['removed_count'] |
||||
|
|
||||
|
# Progress indicator |
||||
|
if i % 25 == 0: |
||||
|
print(f"📈 پیشرفت: {i}/{total_hadis} ({(i/total_hadis)*100:.1f}%)") |
||||
|
|
||||
|
print("\n" + "=" * 60) |
||||
|
print("✅ بهینهسازی کامل شد!") |
||||
|
print(f"📊 آمار:") |
||||
|
print(f" - تعداد کل احادیث پردازش شده: {total_hadis}") |
||||
|
print(f" - تعداد احادیث بهینهسازی شده: {optimized_count}") |
||||
|
print(f" - تعداد راویان حذف شده: {removed_transmitters}") |
||||
|
print(f" - نرخ موفقیت: {(optimized_count/total_hadis)*100:.1f}%") |
||||
|
|
||||
|
return { |
||||
|
'total_processed': total_hadis, |
||||
|
'optimized_count': optimized_count, |
||||
|
'removed_transmitters': removed_transmitters |
||||
|
} |
||||
|
|
||||
|
def optimize_hadis_transmitters(self, hadis): |
||||
|
"""Optimize transmitter chain for a single hadis""" |
||||
|
# Get all transmitters for this hadis, ordered by order field |
||||
|
transmitters = list(hadis.transmitters.all().order_by('order')) |
||||
|
original_count = len(transmitters) |
||||
|
|
||||
|
print(f" 📋 تعداد راویان اصلی: {original_count}") |
||||
|
|
||||
|
needs_modification = False |
||||
|
|
||||
|
# 1. Check if more than 5 transmitters |
||||
|
if original_count > self.max_transmitters: |
||||
|
needs_modification = True |
||||
|
# Keep only first 5 transmitters (ordered by 'order' field) |
||||
|
transmitters_to_keep = transmitters[:self.max_transmitters] |
||||
|
transmitters_to_delete = transmitters[self.max_transmitters:] |
||||
|
|
||||
|
# Delete excess transmitters |
||||
|
removed_count = len(transmitters_to_delete) |
||||
|
for transmitter in transmitters_to_delete: |
||||
|
transmitter_name = transmitter.transmitter.full_name if transmitter.transmitter else 'گپ' |
||||
|
print(f" �️ حذف راوی: {transmitter_name} (ترتیب: {transmitter.order})") |
||||
|
transmitter.delete() |
||||
|
|
||||
|
transmitters = transmitters_to_keep |
||||
|
print(f" ✂️ تعداد راویان از {original_count} به {len(transmitters)} کاهش یافت") |
||||
|
else: |
||||
|
removed_count = 0 |
||||
|
|
||||
|
# 2. Ensure exactly one transmitter has is_gap=True |
||||
|
gap_transmitters = [t for t in transmitters if t.is_gap] |
||||
|
gap_count = len(gap_transmitters) |
||||
|
|
||||
|
if gap_count == 0: |
||||
|
# No gap transmitter, set one randomly |
||||
|
if transmitters: |
||||
|
random_transmitter = random.choice(transmitters) |
||||
|
random_transmitter.is_gap = True |
||||
|
random_transmitter.save() |
||||
|
needs_modification = True |
||||
|
print(f" 🔗 گپ به راوی ترتیب {random_transmitter.order} اضافه شد") |
||||
|
|
||||
|
elif gap_count > 1: |
||||
|
# Multiple gap transmitters, keep only one |
||||
|
transmitter_to_keep_gap = gap_transmitters[0] |
||||
|
for transmitter in gap_transmitters[1:]: |
||||
|
transmitter.is_gap = False |
||||
|
transmitter.save() |
||||
|
needs_modification = True |
||||
|
print(f" � گپ از {gap_count-1} راوی حذف شد، فقط راوی ترتیب {transmitter_to_keep_gap.order} گپ باقی ماند") |
||||
|
|
||||
|
# Reorder transmitters to ensure proper sequence |
||||
|
if needs_modification: |
||||
|
self._reorder_transmitters(transmitters) |
||||
|
|
||||
|
final_gap_count = sum(1 for t in transmitters if t.is_gap) |
||||
|
|
||||
|
if needs_modification: |
||||
|
print(f" ✅ بهینهسازی شد: {original_count} -> {len(transmitters)} راوی") |
||||
|
print(f" 🔗 تعداد گپ: {final_gap_count}") |
||||
|
else: |
||||
|
print(f" ✅ قبلاً بهینه بود (گپ: {final_gap_count})") |
||||
|
|
||||
|
return {'optimized': needs_modification, 'removed_count': removed_count} |
||||
|
|
||||
|
def _reorder_transmitters(self, transmitters): |
||||
|
"""Reorder transmitters with proper order values""" |
||||
|
for i, transmitter in enumerate(transmitters, 1): |
||||
|
transmitter.order = i |
||||
|
transmitter.save() |
||||
|
|
||||
|
|
||||
|
|
||||
|
def get_statistics(self): |
||||
|
"""Get current statistics about transmitter chains""" |
||||
|
print("\n📊 آمار فعلی زنجیره راویان:") |
||||
|
print("-" * 50) |
||||
|
|
||||
|
# Total hadis with transmitters |
||||
|
hadis_with_transmitters = Hadis.objects.annotate( |
||||
|
transmitter_count=Count('transmitters') |
||||
|
).filter(transmitter_count__gt=0) |
||||
|
|
||||
|
total_hadis = hadis_with_transmitters.count() |
||||
|
|
||||
|
# Transmitter count distribution |
||||
|
chain_lengths = {} |
||||
|
gap_distributions = {} |
||||
|
|
||||
|
for hadis in hadis_with_transmitters: |
||||
|
transmitter_count = hadis.transmitter_count |
||||
|
gap_count = hadis.transmitters.filter(is_gap=True).count() |
||||
|
|
||||
|
chain_lengths[transmitter_count] = chain_lengths.get(transmitter_count, 0) + 1 |
||||
|
gap_distributions[gap_count] = gap_distributions.get(gap_count, 0) + 1 |
||||
|
|
||||
|
print(f"تعداد کل احادیث با راوی: {total_hadis}") |
||||
|
print("\nتوزیع طول زنجیره:") |
||||
|
for length in sorted(chain_lengths.keys()): |
||||
|
count = chain_lengths[length] |
||||
|
percentage = (count / total_hadis) * 100 if total_hadis > 0 else 0 |
||||
|
print(f" {length} راوی: {count} حدیث ({percentage:.1f}%)") |
||||
|
|
||||
|
print("\nتوزیع گپ:") |
||||
|
for gaps in sorted(gap_distributions.keys()): |
||||
|
count = gap_distributions[gaps] |
||||
|
percentage = (count / total_hadis) * 100 if total_hadis > 0 else 0 |
||||
|
print(f" {gaps} گپ: {count} حدیث ({percentage:.1f}%)") |
||||
|
|
||||
|
# Identify problematic hadis |
||||
|
problematic = 0 |
||||
|
for hadis in hadis_with_transmitters: |
||||
|
transmitter_count = hadis.transmitter_count |
||||
|
gap_count = hadis.transmitters.filter(is_gap=True).count() |
||||
|
|
||||
|
if transmitter_count > self.max_transmitters or gap_count != self.required_gaps: |
||||
|
problematic += 1 |
||||
|
|
||||
|
print(f"\nاحادیث مشکلدار (نیاز به بهینهسازی): {problematic}") |
||||
|
if total_hadis > 0: |
||||
|
print(f"درصد نیاز به بهینهسازی: {(problematic/total_hadis)*100:.1f}%") |
||||
|
|
||||
|
|
||||
|
def main(): |
||||
|
"""Main function""" |
||||
|
import argparse |
||||
|
|
||||
|
parser = argparse.ArgumentParser(description='بهینهسازی زنجیره راویان احادیث') |
||||
|
parser.add_argument('--stats-only', action='store_true', help='فقط نمایش آمار، بدون بهینهسازی') |
||||
|
parser.add_argument('--dry-run', action='store_true', help='نمایش تغییرات بدون اعمال آنها') |
||||
|
|
||||
|
args = parser.parse_args() |
||||
|
|
||||
|
optimizer = HadisTransmitterOptimizer() |
||||
|
|
||||
|
if args.stats_only: |
||||
|
optimizer.get_statistics() |
||||
|
else: |
||||
|
# Show current statistics |
||||
|
optimizer.get_statistics() |
||||
|
|
||||
|
if not args.dry_run: |
||||
|
print("\n" + "="*60) |
||||
|
confirm = input("🚨 این عملیات زنجیره راویان را تغییر خواهد داد. ادامه میدهید؟ (yes/no): ").strip().lower() |
||||
|
|
||||
|
if confirm == 'yes': |
||||
|
optimizer.optimize_all_hadis() |
||||
|
print(f"\n🎉 بهینهسازی با موفقیت کامل شد!") |
||||
|
|
||||
|
# Show final statistics |
||||
|
print("\n" + "="*60) |
||||
|
print("📊 آمار نهایی:") |
||||
|
optimizer.get_statistics() |
||||
|
else: |
||||
|
print("❌ عملیات لغو شد.") |
||||
|
else: |
||||
|
print("\n🔍 حالت آزمایشی - هیچ تغییری اعمال نخواهد شد") |
||||
|
print("برای اجرای واقعی، بدون --dry-run اجرا کنید") |
||||
|
|
||||
|
|
||||
|
if __name__ == "__main__": |
||||
|
try: |
||||
|
main() |
||||
|
except KeyboardInterrupt: |
||||
|
print("\n❌ عملیات توسط کاربر لغو شد.") |
||||
|
sys.exit(1) |
||||
|
except Exception as e: |
||||
|
print(f"\n💥 خطا: {e}") |
||||
|
import traceback |
||||
|
traceback.print_exc() |
||||
|
sys.exit(1) |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue