Browse Source

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
mortezaei 10 months ago
parent
commit
fd54052c2a
  1. 80
      CLAUDE.md
  2. 10
      apps/account/admin/professor.py
  3. 2
      apps/account/admin/student.py
  4. 158
      apps/account/management/commands/migrate_user_roles.py
  5. 13
      apps/account/manager.py
  6. 114
      apps/account/middleware/admin_access.py
  7. 90
      apps/account/models/user.py
  8. 9
      apps/account/serializers/user.py
  9. 240
      apps/account/tests/test_multiple_roles.py
  10. 3
      apps/certificate/admin.py
  11. 1
      apps/certificate/migrations/0001_initial.py
  12. 1
      apps/chat/management/__init__.py
  13. 62
      apps/chat/management/commands/README.md
  14. 1
      apps/chat/management/commands/__init__.py
  15. 79
      apps/chat/management/commands/clear_chat_data.py
  16. 40
      apps/course/admin/course.py
  17. 7
      apps/course/admin/lesson.py
  18. 181
      apps/course/admin/professor_base.py
  19. 6
      apps/course/serializers/course.py
  20. 216
      apps/course/tests/test_multiple_roles_api.py
  21. 56
      apps/hadis/management/commands/README.md
  22. 98
      apps/hadis/management/commands/seed_hadis_data.py
  23. 152
      apps/hadis/management/commands/test_safe_seeding.py
  24. 15
      apps/quiz/admin/quiz.py
  25. 7
      apps/transaction/views.py
  26. 3
      config/settings/base.py
  27. 252
      docs/API_Documentation_System_README.md
  28. 252
      scripts/optimize_hadis_transmitters.py
  29. 28
      utils/__init__.py

80
CLAUDE.md

@ -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)

10
apps/account/admin/professor.py

@ -75,8 +75,8 @@ class ProfessorUserAdmin(UserAdmin, AjaxDatatable):
messages.error(request, f"A professor with the email {email} already exists.")
return
# If user exists but is not a professor, convert them to professor
existing_user.user_type = User.UserType.PROFESSOR
# اضافه کردن نقش professor بدون حذف نقش‌های قبلی
existing_user.add_role('professor')
# Update user fields from form data
existing_user.fullname = form.cleaned_data.get('fullname')
@ -92,10 +92,6 @@ class ProfessorUserAdmin(UserAdmin, AjaxDatatable):
# Save the user
existing_user.save()
# Add to professor group
professor_group, _ = Group.objects.get_or_create(name="Professor Group")
existing_user.groups.add(professor_group)
# Show success message
messages.success(request, f"The user with email {email} has been converted to a professor.")
@ -107,7 +103,7 @@ class ProfessorUserAdmin(UserAdmin, AjaxDatatable):
obj.set_password(form.cleaned_data['password1'])
if obj: # Only proceed if obj is not None
obj.user_type = User.UserType.PROFESSOR
obj.add_role('professor')
super().save_model(request, obj, form, change)
@admin.display(description='Phone Number')

2
apps/account/admin/student.py

@ -63,7 +63,7 @@ class StudentUserAdmin(UserAdmin, AjaxDatatable):
def save_model(self, request, obj, form, change):
if not change:
obj.set_password(form.cleaned_data['password1'])
obj.user_type = User.UserType.STUDENT
obj.add_role('student')
super().save_model(request, obj, form, change)

158
apps/account/management/commands/migrate_user_roles.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}')

13
apps/account/manager.py

@ -42,6 +42,7 @@ class UserManager(BaseUserManager):
def change_user_type(self, user, new_user_type):
"""تغییر نوع کاربر - deprecated، از add_role استفاده کنید"""
group_name = f"{new_user_type.capitalize()} Group"
if user.user_type != new_user_type and not user.groups.filter(name=group_name).exists():
@ -50,7 +51,17 @@ class UserManager(BaseUserManager):
user.groups.add(new_group)
user.save()
return user
return None
return None
def add_user_role(self, user, role_name):
"""اضافه کردن نقش جدید به کاربر بدون حذف نقش‌های قبلی"""
user.add_role(role_name)
return user
def remove_user_role(self, user, role_name):
"""حذف نقش خاص از کاربر"""
user.remove_role(role_name)
return user

114
apps/account/middleware/admin_access.py

@ -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')

90
apps/account/models/user.py

@ -102,6 +102,96 @@ class User(AbstractUser):
return self.UserType.PROFESSOR
else:
return self.UserType.CLIENT
@property
def primary_role(self):
"""نقش اصلی کاربر بر اساس اولویت"""
if self.groups.filter(name="Professor Group").exists():
return self.UserType.PROFESSOR
elif self.groups.filter(name="Student Group").exists():
return self.UserType.STUDENT
elif self.groups.filter(name="Admin Group").exists():
return self.UserType.ADMIN
elif self.groups.filter(name="Super Admin Group").exists():
return self.UserType.SUPER_ADMIN
else:
return self.UserType.CLIENT
def has_role(self, role_name):
"""چک کردن داشتن نقش خاص"""
if isinstance(role_name, str):
# اگر نام نقش به صورت string داده شده
group_name = f"{role_name.capitalize()} Group"
else:
# اگر از enum استفاده شده
group_name = f"{role_name.value.capitalize()} Group"
return self.groups.filter(name=group_name).exists()
def add_role(self, role_name):
"""اضافه کردن نقش جدید بدون حذف نقش‌های قبلی"""
from django.contrib.auth.models import Group
if isinstance(role_name, str):
group_name = f"{role_name.capitalize()} Group"
else:
group_name = f"{role_name.value.capitalize()} Group"
group, created = Group.objects.get_or_create(name=group_name)
self.groups.add(group)
# بروزرسانی user_type اگر نقش جدید اولویت بالاتری دارد
if role_name in ['professor', self.UserType.PROFESSOR] and self.user_type != self.UserType.PROFESSOR:
self.user_type = self.UserType.PROFESSOR
self.save()
elif role_name in ['student', self.UserType.STUDENT] and self.user_type == self.UserType.CLIENT:
self.user_type = self.UserType.STUDENT
self.save()
def remove_role(self, role_name):
"""حذف نقش خاص"""
from django.contrib.auth.models import Group
if isinstance(role_name, str):
group_name = f"{role_name.capitalize()} Group"
else:
group_name = f"{role_name.value.capitalize()} Group"
try:
group = Group.objects.get(name=group_name)
self.groups.remove(group)
# بروزرسانی user_type بر اساس نقش‌های باقی‌مانده
self.user_type = self.primary_role
self.save()
except Group.DoesNotExist:
pass
def get_all_roles(self):
"""دریافت لیست تمام نقش‌های کاربر"""
return [group.name.replace(' Group', '').lower()
for group in self.groups.all()]
def can_teach_course(self):
"""آیا می‌تواند دوره تدریس کند؟"""
# اولویت اول: staff یا admin
if self.is_staff or self.has_role('admin') or self.has_role('super_admin'):
return True
# اولویت دوم: professor
return self.has_role('professor')
def can_enroll_course(self):
"""آیا می‌تواند در دوره ثبت‌نام کند؟"""
return True # همه می‌توانند دانش‌آموز باشند
def can_manage_course(self, course=None):
"""آیا می‌تواند دوره خاصی را مدیریت کند؟"""
# اولویت اول: staff یا admin - دسترسی کامل
if self.is_staff or self.has_role('admin') or self.has_role('super_admin'):
return True
# اولویت دوم: professor - فقط دوره‌های خودش
if course and self.has_role('professor'):
return course.professor == self
return False
class Meta:

9
apps/account/serializers/user.py

@ -15,15 +15,16 @@ class UserProfileSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True, required=False, validators=[validate_password])
fullname = serializers.CharField(required=False)
gender = serializers.ChoiceField(
choices=User.GenderChoices.choices,
required=False,
help_text="Select the user's gender."
choices=User.GenderChoices.choices,
required=False,
help_text="Select the user's gender."
)
fcm = serializers.CharField(required=False, help_text="Firebase Cloud Messaging token.")
class Meta:
model = User
fields = ['id', 'device_id', 'fcm', 'fullname', 'avatar', 'email', 'phone_number', 'password', 'info', 'skill', 'city', 'country', 'birthdate', 'gender']
read_only_fields = ['email', 'info', 'skill', 'device_id']
read_only_fields = ['email', 'info', 'skill', 'device_id']
# def validate_email(self, value):
# if User.objects.filter(email=value).exists():

240
apps/account/tests/test_multiple_roles.py

@ -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))

3
apps/certificate/admin.py

@ -8,9 +8,10 @@ from unfold.decorators import display
from apps.certificate.models import Certificate
from utils.admin import project_admin_site
from apps.course.admin.professor_base import CertificateBaseAdmin
@admin.register(Certificate)
class CertificateAdmin(ModelAdmin):
class CertificateAdmin(CertificateBaseAdmin):
list_display = ['student', 'course', 'certificate_status', 'created_at']
list_filter = ['status', 'created_at']
search_fields = ['id', 'student__username', 'student__email', 'course__title']

1
apps/certificate/migrations/0001_initial.py

@ -12,7 +12,6 @@ class Migration(migrations.Migration):
dependencies = [
('account', '0001_initial'),
('course', '0001_initial'),
('filer', '0017_image__transparent'),
]
operations = [

1
apps/chat/management/__init__.py

@ -0,0 +1 @@

62
apps/chat/management/commands/README.md

@ -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!
```

1
apps/chat/management/commands/__init__.py

@ -0,0 +1 @@

79
apps/chat/management/commands/clear_chat_data.py

@ -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

40
apps/course/admin/course.py

@ -28,6 +28,7 @@ from unfold.widgets import (
UnfoldAdminSplitDateTimeWidget,
UnfoldAdminTextInputWidget,
)
from .professor_base import DirectCourseAdmin, CourseRelatedAdmin, AttachmentGlossaryBaseAdmin
from unfold.contrib.forms.widgets import ArrayWidget
from django.contrib.postgres.fields import ArrayField
@ -226,7 +227,7 @@ class AddStudentForm(forms.Form):
)
class CourseAdmin(ModelAdmin):
class CourseAdmin(DirectCourseAdmin):
form = CourseForm
inlines = [CourseLessonInline, CourseAttachmentInline, CourseGlossaryInline, ParticipantInline]
list_display = ('display_header', 'category', 'display_professor', 'status', 'display_price', 'is_online')
@ -359,13 +360,12 @@ class CourseAdmin(ModelAdmin):
def has_is_course_professor_permission(self, request, object_id=None):
try:
if request.user.is_staff:
return True
course = self.get_object(request, object_id)
# Check if the current user is the professor of this course
return course and hasattr(request.user, 'professor') and course.professor_id == request.user.id
# Check if the current user can manage this course
return course and request.user.can_manage_course(course)
except Exception as e:
print(e)
return False
@ -396,13 +396,17 @@ class CourseAdmin(ModelAdmin):
if Participant.objects.filter(student=student, course=course).exists():
messages.warning(request, _(f"Student {student.fullname} is already enrolled in this course"))
else:
# اطمینان از اینکه کاربر نقش student دارد
if not student.has_role('student'):
student.add_role('student')
# Create a new participant
Participant.objects.create(
student=student,
course=course,
)
messages.success(
request,
request,
_(f"Student {student.fullname} has been successfully added to {course.title}")
)
@ -422,13 +426,21 @@ class CourseAdmin(ModelAdmin):
)
class GlossaryAdmin(ModelAdmin):
class GlossaryAdmin(AttachmentGlossaryBaseAdmin):
list_display = ('title', 'description')
search_fields = ('title', 'description')
ordering = ('-id',)
def is_used_in_professor_courses(self, user, obj):
"""آیا این glossary در دوره‌های استاد استفاده شده؟"""
return obj.courseglossary_set.filter(course__professor=user).exists()
def filter_by_professor_usage(self, user, queryset):
"""فیلتر کردن glossary ها بر اساس استفاده در دوره‌های استاد"""
return queryset.filter(courseglossary__course__professor=user).distinct()
class CourseGlossaryAdmin(ModelAdmin):
class CourseGlossaryAdmin(CourseRelatedAdmin):
list_display = ('course', 'glossary_title', 'glossary_description')
list_filter = ('course',)
search_fields = ('glossary__title', 'glossary__description', 'course__title')
@ -476,7 +488,7 @@ class AttachmentAdminForm(forms.ModelForm):
return file_name
class AttachmentAdmin(ModelAdmin):
class AttachmentAdmin(AttachmentGlossaryBaseAdmin):
form = AttachmentAdminForm
list_display = ('title', 'file', 'file_size')
search_fields = ('title', 'file')
@ -486,8 +498,16 @@ class AttachmentAdmin(ModelAdmin):
obj.file_size = obj.file.size
super().save_model(request, obj, form, change)
def is_used_in_professor_courses(self, user, obj):
"""آیا این attachment در دوره‌های استاد استفاده شده؟"""
return obj.courseattachment_set.filter(course__professor=user).exists()
class CourseAttachmentAdmin(ModelAdmin):
def filter_by_professor_usage(self, user, queryset):
"""فیلتر کردن attachment ها بر اساس استفاده در دوره‌های استاد"""
return queryset.filter(courseattachment__course__professor=user).distinct()
class CourseAttachmentAdmin(CourseRelatedAdmin):
list_display = ('course', 'attachment_title', 'attachment_file', 'attachment_file_size')
list_filter = ('course',)
search_fields = ('attachment__title', 'course__title')
@ -514,3 +534,5 @@ project_admin_site.register(CourseGlossary, CourseGlossaryAdmin)
project_admin_site.register(Attachment, AttachmentAdmin)
project_admin_site.register(CourseAttachment, CourseAttachmentAdmin)
project_admin_site.register(Participant, ParticipantAdmin)
# مدل‌های ProfessorUser و StudentUser قبلاً در admin های مربوطه ثبت شده‌اند

7
apps/course/admin/lesson.py

@ -17,6 +17,7 @@ from unfold.widgets import (
)
from utils.admin import project_admin_site
from .professor_base import CourseRelatedAdmin
from apps.course.models.lesson import Lesson, CourseLesson, LessonCompletion
from unfold.admin import ModelAdmin, StackedInline, TabularInline
@ -88,7 +89,7 @@ class LessonAdmin(ModelAdmin):
)
class CourseLessonAdmin(ModelAdmin):
class CourseLessonAdmin(CourseRelatedAdmin):
form = CourseLessonForm
list_display = ('title', 'course', 'display_duration', 'is_active', 'priority')
list_filter = (
@ -136,4 +137,6 @@ class LessonCompletionAdmin(ModelAdmin):
# Register with the project admin site
project_admin_site.register(Lesson, LessonAdmin)
project_admin_site.register(CourseLesson, CourseLessonAdmin)
project_admin_site.register(LessonCompletion, LessonCompletionAdmin)
project_admin_site.register(LessonCompletion, LessonCompletionAdmin)
# Lesson قبلاً ثبت شده است

181
apps/course/admin/professor_base.py

@ -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)

6
apps/course/serializers/course.py

@ -99,7 +99,7 @@ class CourseDetailSerializer(serializers.ModelSerializer):
price = serializers.SerializerMethodField()
discount_percentage = serializers.SerializerMethodField()
final_price = serializers.SerializerMethodField()
is_free = serializers.SerializerMethodField()
is_free = serializers.SerializerMethodField()
class Meta:
model = Course
@ -341,6 +341,10 @@ class MyCourseListSerializer(serializers.ModelSerializer):
def _is_participant(self, student, course):
"""Helper method to check if a student is a participant in the given course."""
# اگر کاربر استاد دوره است، دسترسی کامل دارد
if course.professor == student:
return True
# در غیر این صورت چک می‌کنیم که آیا participant است یا خیر
return Participant.objects.filter(student=student, course=course).exists()
def _get_authenticated_user(self):

216
apps/course/tests/test_multiple_roles_api.py

@ -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)

56
apps/hadis/management/commands/README.md

@ -2,7 +2,11 @@
## seed_hadis_data
<<<<<<< HEAD
This management command seeds comprehensive data for all Hadis app models with realistic sample records while maintaining proper relationships and business domain logic.
=======
This management command seeds comprehensive data for all Hadis app models with realistic sample records while maintaining proper relationships and business domain logic. **Enhanced with lock detection and retry logic to prevent database locks.**
>>>>>>> 932fb17 (Refactor API Documentation System and optimize Hadis data scripts)
### Usage
@ -47,12 +51,25 @@ python manage.py seed_hadis_data --clear --images-dir scripts/seed_images --xmin
- The XMind file is optional but recommended for category mind maps
- All models must be properly migrated before running
<<<<<<< HEAD
### Performance
The command uses optimized batch operations to create data efficiently:
- Bulk create/update operations for categories
- Checks for existing records to avoid duplicates
- Progress reporting for large datasets
=======
### Performance & Lock Prevention
The command uses advanced techniques to prevent database locks and ensure reliable execution:
- **Lock Detection**: Automatically detects database locks and deadlocks
- **Retry Logic**: Retries failed operations with exponential backoff (up to 5 attempts)
- **Step-by-step Processing**: Creates records individually with small delays to prevent locks
- **Batch Processing**: Processes tags in small batches to avoid overwhelming the database
- **No Large Transactions**: Avoids wrapping everything in atomic transactions that can cause locks
- **Progress Reporting**: Detailed progress with emoji indicators and clear status messages
- **Error Handling**: Graceful handling of duplicate records and constraint violations
>>>>>>> 932fb17 (Refactor API Documentation System and optimize Hadis data scripts)
### Example Output
@ -70,3 +87,42 @@ Creating Hadis Categories...
...
Successfully seeded all Hadis data!
```
<<<<<<< HEAD
=======
## test_safe_seeding
A simple test command to verify that the lock detection and retry logic is working properly.
### Usage
```bash
# Test the safe seeding functionality
python manage.py test_safe_seeding
```
### What it tests
- Database connectivity
- Lock detection mechanisms
- Retry logic for failed operations
- Creation of test records (sect, status, tag)
## Additional Commands
### fix_sects
Fixes any issues with sect creation by using simple English titles.
```bash
python manage.py fix_sects
```
### seed_basic_data
Creates only the essential basic data (statuses, tags, sects) without the full dataset.
```bash
python manage.py seed_basic_data [--clear]
```
>>>>>>> 932fb17 (Refactor API Documentation System and optimize Hadis data scripts)

98
apps/hadis/management/commands/seed_hadis_data.py

@ -227,13 +227,13 @@ class Command(BaseCommand):
self.stdout.write("Creating Hadis Statuses...")
statuses_data = [
{'title': 'Authentic', 'color': 'green', 'order': 1},
{'title': 'Good', 'color': 'blue', 'order': 2},
{'title': 'Weak', 'color': 'yellow', 'order': 3},
{'title': 'Fabricated', 'color': 'red', 'order': 4},
{'title': 'Interrupted', 'color': 'orange', 'order': 5},
{'title': 'Broken', 'color': 'purple', 'order': 6},
{'title': 'Unknown', 'color': 'gray', 'order': 7},
{'title': 'Достоверный', 'color': 'green', 'order': 1},
{'title': 'Хороший', 'color': 'blue', 'order': 2},
{'title': 'Слабый', 'color': 'yellow', 'order': 3},
{'title': 'Выдуманный', 'color': 'red', 'order': 4},
{'title': 'Прерванный', 'color': 'orange', 'order': 5},
{'title': 'Разорванный', 'color': 'purple', 'order': 6},
{'title': 'Неизвестный', 'color': 'gray', 'order': 7},
]
statuses = []
@ -273,12 +273,12 @@ class Command(BaseCommand):
self.stdout.write("Creating Hadis Tags...")
tags_data = [
'Worship', 'Prayer', 'Fasting', 'Hajj', 'Zakat', 'Khums',
'Ethics', 'Patience', 'Gratitude', 'Trust', 'Piety', 'Justice',
'Fiqh', 'Rulings', 'Halal', 'Haram', 'Mustahab', 'Makruh',
'Interpretation', 'Quran', 'Verses', 'Surah', 'Recitation',
'Imamate', 'Authority', 'Infallibles', 'Prophets Family',
'Supplication', 'Remembrance', 'Forgiveness', 'Praise', 'Monotheism'
'Поклонение', 'Молитва', 'Пост', 'Хадж', 'Закят', 'Хумс',
'Нравственность', 'Терпение', 'Благодарность', 'Доверие', 'Богобоязненность', 'Справедливость',
'Фикх', 'Постановления', 'Халяль', 'Харам', 'Мустахаб', 'Макрух',
'Толкование', 'Коран', 'Аяты', 'Сура', 'Чтение',
'Имамат', 'Власть', 'Непорочные', 'Семья Пророка',
'Мольба', 'Поминание', 'Прощение', 'Восхваление', 'Единобожие'
]
tags = []
@ -338,8 +338,8 @@ class Command(BaseCommand):
self.stdout.write("Creating Hadis Sects...")
sects_data = [
{'sect_type': 'shia', 'title': 'Shia Twelvers', 'is_active': True, 'order': 1},
{'sect_type': 'sunni', 'title': 'Sunni', 'is_active': True, 'order': 2},
{'sect_type': 'shia', 'title': 'Шииты-двунадесятники', 'is_active': True, 'order': 1},
{'sect_type': 'sunni', 'title': 'Сунниты', 'is_active': True, 'order': 2},
]
sects = []
@ -426,12 +426,12 @@ class Command(BaseCommand):
# Quran categories - create one by one to avoid MPTT issues
quran_categories_data = [
{'title': 'Quran Interpretation', 'order': 1},
{'title': 'Verses of Rulings', 'order': 2},
{'title': 'Quran Stories', 'order': 3},
{'title': 'Virtues of Surahs', 'order': 4},
{'title': 'Quran Miracles', 'order': 5},
{'title': 'Quranic Sciences', 'order': 6},
{'title': 'Толкование Корана', 'order': 1},
{'title': 'Аяты постановлений', 'order': 2},
{'title': 'Коранические истории', 'order': 3},
{'title': 'Достоинства сур', 'order': 4},
{'title': 'Чудеса Корана', 'order': 5},
{'title': 'Коранические науки', 'order': 6},
]
# Create main Quran categories one by one
@ -456,22 +456,22 @@ class Command(BaseCommand):
for parent_category in quran_parent_categories:
child_categories_data = []
if parent_category.title == 'Quran Interpretation':
if parent_category.title == 'Толкование Корана':
child_categories_data = [
{'title': 'Surah Al-Fatiha Interpretation', 'order': 1},
{'title': 'Surah Al-Baqara Interpretation', 'order': 2},
{'title': 'Surah Al Imran Interpretation', 'order': 3},
{'title': 'Толкование суры Аль-Фатиха', 'order': 1},
{'title': 'Толкование суры Аль-Бакара', 'order': 2},
{'title': 'Толкование суры Аль Имран', 'order': 3},
]
elif parent_category.title == 'Verses of Rulings':
elif parent_category.title == 'Аяты постановлений':
child_categories_data = [
{'title': 'Prayer Verses', 'order': 1},
{'title': 'Fasting Verses', 'order': 2},
{'title': 'Zakat Verses', 'order': 3},
{'title': 'Аяты о молитве', 'order': 1},
{'title': 'Аяты о посте', 'order': 2},
{'title': 'Аяты о закяте', 'order': 3},
]
elif parent_category.title == 'Quran Stories':
elif parent_category.title == 'Коранические истории':
child_categories_data = [
{'title': 'Prophets Stories', 'order': 1},
{'title': 'Righteous People Stories', 'order': 2},
{'title': 'Истории пророков', 'order': 1},
{'title': 'Истории праведников', 'order': 2},
]
# Create child categories one by one
@ -496,12 +496,12 @@ class Command(BaseCommand):
# Hadith categories - create one by one
self.stdout.write(" 📚 Creating Hadith categories...")
hadith_categories_data = [
{'title': 'Book of Purification', 'order': 1},
{'title': 'Book of Prayer', 'order': 2},
{'title': 'Book of Fasting', 'order': 3},
{'title': 'Book of Hajj', 'order': 4},
{'title': 'Book of Zakat', 'order': 5},
{'title': 'Book of Ethics', 'order': 6},
{'title': 'Книга очищения', 'order': 1},
{'title': 'Книга молитвы', 'order': 2},
{'title': 'Книга поста', 'order': 3},
{'title': 'Книга хаджа', 'order': 4},
{'title': 'Книга закята', 'order': 5},
{'title': 'Книга нравственности', 'order': 6},
]
# Create main Hadith categories one by one
@ -526,22 +526,22 @@ class Command(BaseCommand):
for parent_category in hadith_parent_categories:
child_categories_data = []
if parent_category.title == 'Book of Purification':
if parent_category.title == 'Книга очищения':
child_categories_data = [
{'title': 'Ablution', 'order': 1},
{'title': 'Full Bath', 'order': 2},
{'title': 'Dry Ablution', 'order': 3},
{'title': 'Омовение', 'order': 1},
{'title': 'Полное омовение', 'order': 2},
{'title': 'Сухое омовение', 'order': 3},
]
elif parent_category.title == 'Book of Prayer':
elif parent_category.title == 'Книга молитвы':
child_categories_data = [
{'title': 'Prayer Times', 'order': 1},
{'title': 'Qibla Direction', 'order': 2},
{'title': 'Congregational Prayer', 'order': 3},
{'title': 'Времена молитв', 'order': 1},
{'title': 'Направление киблы', 'order': 2},
{'title': 'Коллективная молитва', 'order': 3},
]
elif parent_category.title == 'Book of Ethics':
elif parent_category.title == 'Книга нравственности':
child_categories_data = [
{'title': 'Patience and Gratitude', 'order': 1},
{'title': 'Justice and Honesty', 'order': 2},
{'title': 'Терпение и благодарность', 'order': 1},
{'title': 'Справедливость и честность', 'order': 2},
]
# Create child categories one by one

152
apps/hadis/management/commands/test_safe_seeding.py

@ -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)

15
apps/quiz/admin/quiz.py

@ -25,11 +25,18 @@ class QuizAdmin(ModelAdmin):
queryset = super().get_queryset(request).annotate(
questions_count=Count('questions')
)
if request.user.groups.filter(name="Professor Group").exists():
# اولویت اول: staff یا admin - دسترسی کامل
if (request.user.is_staff or
request.user.has_role('admin') or
request.user.has_role('super_admin')):
return queryset
# اولویت دوم: professor - فقط کوئیزهای دوره‌های خود
if request.user.has_role('professor'):
return queryset.filter(lesson__course__professor=request.user)
return queryset
return queryset.none()
def get_form(self, request, obj=None, **kwargs):
form = super().get_form(request, obj, **kwargs)

7
apps/transaction/views.py

@ -35,9 +35,10 @@ class TransactionParticipantCreateView(generics.CreateAPIView):
participant = participant_infos[0]
if participant.get('email') != user.email:
raise AppAPIException({'message': "The email must be for the requesting user"})
if user.user_type != User.UserType.STUDENT:
user = User.objects.change_user_type(user, User.UserType.STUDENT)
# به جای تغییر user_type، فقط نقش student را اضافه می‌کنیم
if not user.has_role('student'):
user.add_role('student')
participant, created = Participant.objects.get_or_create(
student=user,

3
config/settings/base.py

@ -121,11 +121,12 @@ MIDDLEWARE = [
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
# "django.contrib.auth.middleware.LoginRequiredMiddleware",
# "django.contrib.auth.middleware.LoginRequiredMiddleware",
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'config.language_code_middleware.language_middleware',
'config.enhanced_auth_middleware.enhanced_auth_middleware',
'apps.account.middleware.admin_access.AdminAccessMiddleware',
]
ROOT_URLCONF = 'config.urls'

252
docs/API_Documentation_System_README.md

@ -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`

252
scripts/optimize_hadis_transmitters.py

@ -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)

28
utils/__init__.py

@ -42,26 +42,24 @@ def get_thumbs(obj, request: HttpRequest = None) -> dict:
return {}
try:
from easy_thumbnails.files import get_thumbnailer
# تعریف سه سایز ثابت
sizes = ['sm', 'md', 'lg']
thumbnail_object = {}
thumbs = qs_thumbs()
# print(f'--> {thumbs}')
# بررسی نوع فیلد و استفاده از روش مناسب
if hasattr(obj, 'easy_thumbnails_thumbnailer'):
# برای فیلدهای FilerImageField
thumbnailer = obj.easy_thumbnails_thumbnailer
# گرفتن URL اصلی تصویر
if hasattr(obj, 'url'):
original_url = obj.url
else:
# برای فیلدهای ImageField معمولی
thumbnailer = get_thumbnailer(obj)
return {}
for thumb in thumbs:
url = thumbnailer.get_thumbnail(thumb.as_dict).url
# برای هر سه سایز، همان URL اصلی را برگردان
for size in sizes:
if request:
url = request.build_absolute_uri(url)
thumbnail_object[thumb.name] = url
url = request.build_absolute_uri(original_url)
else:
url = original_url
thumbnail_object[size] = url
return thumbnail_object

Loading…
Cancel
Save