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