12 changed files with 570 additions and 12 deletions
-
83.zencoder/rules/repo.md
-
51apps/transaction/admin.py
-
3apps/transaction/apps.py
-
1apps/transaction/management/__init__.py
-
1apps/transaction/management/commands/__init__.py
-
201apps/transaction/management/commands/sync_successful_transactions.py
-
16apps/transaction/models.py
-
67apps/transaction/signals.py
-
136apps/transaction/tests.py
-
1apps/transaction/urls.py
-
10apps/transaction/views.py
-
12config/test_auth_middleware.py
@ -0,0 +1,83 @@ |
|||
--- |
|||
description: Repository Information Overview |
|||
alwaysApply: true |
|||
--- |
|||
|
|||
# Imam Javad Backend Information |
|||
|
|||
## Summary |
|||
A Django-based backend application for the Imam Javad platform, providing API services for various features including user accounts, courses, library resources, hadis (religious texts), videos, podcasts, and more. The application is multilingual, supporting English, Persian, and Russian. |
|||
|
|||
## Structure |
|||
- **apps/**: Contains all application modules (account, course, hadis, library, etc.) |
|||
- **config/**: Django project configuration and settings |
|||
- **dynamic_preferences/**: Custom preferences management system |
|||
- **static/**: Static files (CSS, images, media) |
|||
- **templates/**: HTML templates for admin and frontend views |
|||
- **utils/**: Utility functions and helper classes |
|||
- **locale/**: Translation files for multilingual support |
|||
|
|||
## Language & Runtime |
|||
**Language**: Python |
|||
**Version**: 3.9 (as specified in Dockerfile) |
|||
**Framework**: Django 4.2+ |
|||
**Build System**: pip |
|||
**Package Manager**: pip |
|||
|
|||
## Dependencies |
|||
**Main Dependencies**: |
|||
- Django 4.2+ |
|||
- Django REST Framework 3.16.0 |
|||
- Celery 5.2.1 |
|||
- PostgreSQL (psycopg2-binary 2.9.9) |
|||
- Redis 4.3.4 |
|||
- django-unfold 0.54.0 (Admin UI) |
|||
- django-filer 3.3.1 |
|||
- django-dynamic-preferences 1.16.0 |
|||
- django-rosetta 0.9.6 (Translations) |
|||
|
|||
**Development Dependencies**: |
|||
- django-debug-toolbar 4.3.0 |
|||
- django-reset-migrations 0.4.0 |
|||
|
|||
## Build & Installation |
|||
```bash |
|||
# Install dependencies |
|||
pip install -r requirements.txt |
|||
|
|||
# Run migrations |
|||
python manage.py migrate |
|||
|
|||
# Run development server |
|||
python manage.py runserver 0.0.0.0:8000 |
|||
``` |
|||
|
|||
## Docker |
|||
**Dockerfile**: Dockerfile (development), Dockerfile.prod (production) |
|||
**Image**: Python 3.9 |
|||
**Configuration**: Docker Compose with PostgreSQL database |
|||
**Run Command**: |
|||
```bash |
|||
docker-compose up -d |
|||
``` |
|||
|
|||
## Testing |
|||
**Framework**: Django Test |
|||
**Test Location**: Each app has a tests.py file |
|||
**Run Command**: |
|||
```bash |
|||
python manage.py test |
|||
``` |
|||
|
|||
## Main Components |
|||
- **Account**: User authentication and profile management |
|||
- **Course**: Online course management system |
|||
- **Hadis**: Religious text management and API |
|||
- **Library**: Digital book library and collections |
|||
- **Video**: Video content management |
|||
- **Podcast**: Audio content management |
|||
- **Chat**: Messaging functionality |
|||
- **Quiz**: Quiz and assessment system |
|||
- **Transaction**: Payment processing |
|||
- **Certificate**: Course completion certificates |
|||
- **API**: Core API endpoints and documentation |
|||
@ -0,0 +1 @@ |
|||
# Management commands for transaction app |
|||
@ -0,0 +1 @@ |
|||
# Management commands |
|||
@ -0,0 +1,201 @@ |
|||
from django.core.management.base import BaseCommand, CommandError |
|||
from django.db import transaction |
|||
from django.utils import timezone |
|||
from apps.transaction.models import TransactionParticipant |
|||
from apps.course.models import Participant |
|||
from apps.account.models import User |
|||
|
|||
|
|||
class Command(BaseCommand): |
|||
help = 'بررسی تراکنشهای موفق و ایجاد شرکتکنندگان دوره در صورت عدم وجود' |
|||
|
|||
def add_arguments(self, parser): |
|||
parser.add_argument( |
|||
'--dry-run', |
|||
action='store_true', |
|||
help='فقط نمایش تراکنشهایی که نیاز به بروزرسانی دارند بدون اعمال تغییرات', |
|||
) |
|||
parser.add_argument( |
|||
'--transaction-id', |
|||
type=int, |
|||
help='بررسی تراکنش خاص با ID مشخص', |
|||
) |
|||
parser.add_argument( |
|||
'--user-email', |
|||
type=str, |
|||
help='بررسی تراکنشهای کاربر خاص', |
|||
) |
|||
parser.add_argument( |
|||
'--course-slug', |
|||
type=str, |
|||
help='بررسی تراکنشهای دوره خاص', |
|||
) |
|||
|
|||
def handle(self, *args, **options): |
|||
dry_run = options['dry_run'] |
|||
transaction_id = options.get('transaction_id') |
|||
user_email = options.get('user_email') |
|||
course_slug = options.get('course_slug') |
|||
|
|||
# ساخت queryset اولیه |
|||
queryset = TransactionParticipant.objects.filter( |
|||
status=TransactionParticipant.TransactionStatus.SUCCESS, |
|||
is_deleted=False |
|||
).select_related('user', 'course') |
|||
|
|||
# اعمال فیلترهای اضافی |
|||
if transaction_id: |
|||
queryset = queryset.filter(id=transaction_id) |
|||
|
|||
if user_email: |
|||
try: |
|||
user = User.objects.get(email=user_email) |
|||
queryset = queryset.filter(user=user) |
|||
except User.DoesNotExist: |
|||
raise CommandError(f'کاربر با ایمیل {user_email} یافت نشد.') |
|||
|
|||
if course_slug: |
|||
queryset = queryset.filter(course__slug=course_slug) |
|||
|
|||
total_transactions = queryset.count() |
|||
|
|||
if total_transactions == 0: |
|||
self.stdout.write( |
|||
self.style.WARNING('هیچ تراکنش موفقی برای بررسی یافت نشد.') |
|||
) |
|||
return |
|||
|
|||
self.stdout.write( |
|||
self.style.SUCCESS(f'تعداد {total_transactions} تراکنش موفق برای بررسی یافت شد.') |
|||
) |
|||
|
|||
missing_participants = [] |
|||
existing_participants = [] |
|||
errors = [] |
|||
|
|||
# بررسی هر تراکنش |
|||
for trans in queryset: |
|||
try: |
|||
# بررسی وجود participant |
|||
participant_exists = Participant.objects.filter( |
|||
student=trans.user, |
|||
course=trans.course |
|||
).exists() |
|||
|
|||
if not participant_exists: |
|||
missing_participants.append(trans) |
|||
self.stdout.write( |
|||
self.style.WARNING( |
|||
f'❌ تراکنش {trans.id}: کاربر {trans.user.email} در دوره "{trans.course.title}" ثبتنام نشده' |
|||
) |
|||
) |
|||
else: |
|||
existing_participants.append(trans) |
|||
self.stdout.write( |
|||
self.style.SUCCESS( |
|||
f'✅ تراکنش {trans.id}: کاربر {trans.user.email} در دوره "{trans.course.title}" قبلاً ثبتنام شده' |
|||
) |
|||
) |
|||
|
|||
except Exception as e: |
|||
errors.append((trans, str(e))) |
|||
self.stdout.write( |
|||
self.style.ERROR( |
|||
f'⚠️ خطا در بررسی تراکنش {trans.id}: {str(e)}' |
|||
) |
|||
) |
|||
|
|||
# نمایش خلاصه |
|||
self.stdout.write('\n' + '='*50) |
|||
self.stdout.write(f'📊 خلاصه نتایج:') |
|||
self.stdout.write(f' • کل تراکنشهای بررسی شده: {total_transactions}') |
|||
self.stdout.write(f' • شرکتکنندگان موجود: {len(existing_participants)}') |
|||
self.stdout.write(f' • شرکتکنندگان ناموجود: {len(missing_participants)}') |
|||
self.stdout.write(f' • خطاها: {len(errors)}') |
|||
self.stdout.write('='*50 + '\n') |
|||
|
|||
if not missing_participants: |
|||
self.stdout.write( |
|||
self.style.SUCCESS('🎉 همه تراکنشهای موفق دارای شرکتکننده مربوطه هستند!') |
|||
) |
|||
return |
|||
|
|||
if dry_run: |
|||
self.stdout.write( |
|||
self.style.WARNING( |
|||
f'🔍 حالت Dry Run: {len(missing_participants)} شرکتکننده نیاز به ایجاد دارند.' |
|||
) |
|||
) |
|||
self.stdout.write( |
|||
'برای اعمال تغییرات، دستور را بدون --dry-run اجرا کنید.' |
|||
) |
|||
return |
|||
|
|||
# ایجاد شرکتکنندگان ناموجود |
|||
created_count = 0 |
|||
failed_count = 0 |
|||
|
|||
self.stdout.write( |
|||
self.style.SUCCESS(f'🚀 شروع ایجاد {len(missing_participants)} شرکتکننده...') |
|||
) |
|||
|
|||
for trans in missing_participants: |
|||
try: |
|||
with transaction.atomic(): |
|||
# اضافه کردن نقش student اگر وجود نداشته باشد |
|||
if not trans.user.has_role('student'): |
|||
trans.user.add_role('student') |
|||
self.stdout.write( |
|||
f' 👤 نقش student به کاربر {trans.user.email} اضافه شد' |
|||
) |
|||
|
|||
# ایجاد participant |
|||
participant = Participant.objects.create( |
|||
student=trans.user, |
|||
course=trans.course |
|||
) |
|||
|
|||
created_count += 1 |
|||
self.stdout.write( |
|||
self.style.SUCCESS( |
|||
f' ✅ شرکتکننده ایجاد شد: {trans.user.email} در دوره "{trans.course.title}"' |
|||
) |
|||
) |
|||
|
|||
except Exception as e: |
|||
failed_count += 1 |
|||
self.stdout.write( |
|||
self.style.ERROR( |
|||
f' ❌ خطا در ایجاد شرکتکننده برای تراکنش {trans.id}: {str(e)}' |
|||
) |
|||
) |
|||
|
|||
# نمایش نتیجه نهایی |
|||
self.stdout.write('\n' + '='*50) |
|||
self.stdout.write('🏁 نتیجه نهایی:') |
|||
self.stdout.write(f' • شرکتکنندگان ایجاد شده: {created_count}') |
|||
self.stdout.write(f' • شکستها: {failed_count}') |
|||
|
|||
if created_count > 0: |
|||
self.stdout.write( |
|||
self.style.SUCCESS(f'✅ {created_count} شرکتکننده با موفقیت ایجاد شد!') |
|||
) |
|||
|
|||
if failed_count > 0: |
|||
self.stdout.write( |
|||
self.style.ERROR(f'❌ {failed_count} مورد با خطا مواجه شد!') |
|||
) |
|||
|
|||
self.stdout.write('='*50) |
|||
|
|||
def get_transaction_info(self, trans): |
|||
"""اطلاعات کامل تراکنش را برمیگرداند""" |
|||
return { |
|||
'id': trans.id, |
|||
'user_email': trans.user.email, |
|||
'course_title': trans.course.title, |
|||
'course_slug': trans.course.slug, |
|||
'price': trans.price, |
|||
'created_at': trans.created_at, |
|||
'status': trans.status |
|||
} |
|||
@ -0,0 +1,67 @@ |
|||
from django.db.models.signals import post_save, pre_save |
|||
from django.dispatch import receiver |
|||
from django.db import transaction |
|||
|
|||
from apps.transaction.models import TransactionParticipant |
|||
from apps.course.models import Participant |
|||
|
|||
|
|||
@receiver(pre_save, sender=TransactionParticipant) |
|||
def store_previous_status(sender, instance, **kwargs): |
|||
""" |
|||
Store the previous status before saving to compare with new status |
|||
""" |
|||
if instance.pk: |
|||
try: |
|||
previous_instance = TransactionParticipant.objects.get(pk=instance.pk) |
|||
instance._previous_status = previous_instance.status |
|||
except TransactionParticipant.DoesNotExist: |
|||
instance._previous_status = None |
|||
else: |
|||
instance._previous_status = None |
|||
|
|||
|
|||
@receiver(post_save, sender=TransactionParticipant) |
|||
def create_participant_on_success(sender, instance, created, **kwargs): |
|||
""" |
|||
Create course participant when transaction status changes to SUCCESS |
|||
""" |
|||
# اگر تراکنش جدید ایجاد شده و وضعیت آن SUCCESS است |
|||
if created and instance.status == TransactionParticipant.TransactionStatus.SUCCESS: |
|||
create_course_participant(instance) |
|||
|
|||
# اگر تراکنش موجود بوده و وضعیت آن از حالت دیگری به SUCCESS تغییر کرده |
|||
elif not created and hasattr(instance, '_previous_status'): |
|||
if (instance._previous_status != TransactionParticipant.TransactionStatus.SUCCESS and |
|||
instance.status == TransactionParticipant.TransactionStatus.SUCCESS): |
|||
create_course_participant(instance) |
|||
|
|||
|
|||
def create_course_participant(transaction_instance): |
|||
""" |
|||
Create course participant for successful transaction |
|||
""" |
|||
try: |
|||
with transaction.atomic(): |
|||
# بررسی اینکه آیا کاربر نقش student دارد یا نه |
|||
if not transaction_instance.user.has_role('student'): |
|||
transaction_instance.user.add_role('student') |
|||
|
|||
# بررسی اینکه آیا قبلاً participant ایجاد شده یا نه |
|||
existing_participant = Participant.objects.filter( |
|||
student=transaction_instance.user, |
|||
course=transaction_instance.course |
|||
).first() |
|||
|
|||
if not existing_participant: |
|||
# ایجاد participant جدید |
|||
participant = Participant.objects.create( |
|||
student=transaction_instance.user, |
|||
course=transaction_instance.course |
|||
) |
|||
print(f"Course participant created: {participant}") |
|||
else: |
|||
print(f"Course participant already exists: {existing_participant}") |
|||
|
|||
except Exception as e: |
|||
print(f"Error creating course participant: {e}") |
|||
@ -1,3 +1,137 @@ |
|||
from django.test import TestCase |
|||
from django.contrib.auth import get_user_model |
|||
from apps.account.models import StudentUser |
|||
from apps.course.models import Course, CourseCategory, Participant |
|||
from apps.transaction.models import TransactionParticipant |
|||
from apps.account.models import ProfessorUser |
|||
|
|||
# Create your tests here. |
|||
User = get_user_model() |
|||
|
|||
|
|||
class TransactionParticipantSignalTest(TestCase): |
|||
def setUp(self): |
|||
"""تنظیمات اولیه برای تست""" |
|||
# ایجاد کاربر |
|||
self.user = User.objects.create_user( |
|||
email='test@example.com', |
|||
password='testpass123' |
|||
) |
|||
|
|||
# ایجاد استاد |
|||
self.professor = ProfessorUser.objects.create( |
|||
email='professor@example.com', |
|||
password='testpass123' |
|||
) |
|||
|
|||
# ایجاد دستهبندی دوره |
|||
self.category = CourseCategory.objects.create( |
|||
name='Test Category', |
|||
slug='test-category' |
|||
) |
|||
|
|||
# ایجاد دوره |
|||
self.course = Course.objects.create( |
|||
title='Test Course', |
|||
slug='test-course', |
|||
category=self.category, |
|||
professor=self.professor, |
|||
video_type='youtube_link', |
|||
level='beginner', |
|||
duration=10, |
|||
lessons_count=5, |
|||
description='Test course description', |
|||
is_free=False, |
|||
price=100.00, |
|||
final_price=100.00 |
|||
) |
|||
|
|||
def test_participant_created_on_success_status(self): |
|||
"""تست ایجاد participant هنگام تغییر وضعیت به SUCCESS""" |
|||
# ایجاد تراکنش با وضعیت PENDING |
|||
transaction = TransactionParticipant.objects.create( |
|||
user=self.user, |
|||
course=self.course, |
|||
price=100.00, |
|||
status=TransactionParticipant.TransactionStatus.PENDING |
|||
) |
|||
|
|||
# بررسی که participant ایجاد نشده |
|||
self.assertFalse( |
|||
Participant.objects.filter(student=self.user, course=self.course).exists() |
|||
) |
|||
|
|||
# تغییر وضعیت به SUCCESS |
|||
transaction.status = TransactionParticipant.TransactionStatus.SUCCESS |
|||
transaction.save() |
|||
|
|||
# بررسی که participant ایجاد شده |
|||
self.assertTrue( |
|||
Participant.objects.filter(student=self.user, course=self.course).exists() |
|||
) |
|||
|
|||
# بررسی که کاربر نقش student دارد |
|||
self.assertTrue(self.user.has_role('student')) |
|||
|
|||
def test_participant_created_on_direct_success(self): |
|||
"""تست ایجاد participant هنگام ایجاد تراکنش با وضعیت SUCCESS""" |
|||
# ایجاد تراکنش مستقیماً با وضعیت SUCCESS |
|||
transaction = TransactionParticipant.objects.create( |
|||
user=self.user, |
|||
course=self.course, |
|||
price=100.00, |
|||
status=TransactionParticipant.TransactionStatus.SUCCESS |
|||
) |
|||
|
|||
# بررسی که participant ایجاد شده |
|||
self.assertTrue( |
|||
Participant.objects.filter(student=self.user, course=self.course).exists() |
|||
) |
|||
|
|||
# بررسی که کاربر نقش student دارد |
|||
self.assertTrue(self.user.has_role('student')) |
|||
|
|||
def test_no_duplicate_participant(self): |
|||
"""تست عدم ایجاد participant تکراری""" |
|||
# ایجاد participant دستی |
|||
existing_participant = Participant.objects.create( |
|||
student=self.user, |
|||
course=self.course |
|||
) |
|||
|
|||
# ایجاد تراکنش با وضعیت SUCCESS |
|||
transaction = TransactionParticipant.objects.create( |
|||
user=self.user, |
|||
course=self.course, |
|||
price=100.00, |
|||
status=TransactionParticipant.TransactionStatus.SUCCESS |
|||
) |
|||
|
|||
# بررسی که فقط یک participant وجود دارد |
|||
self.assertEqual( |
|||
Participant.objects.filter(student=self.user, course=self.course).count(), |
|||
1 |
|||
) |
|||
|
|||
def test_model_helper_methods(self): |
|||
"""تست متدهای کمکی مدل""" |
|||
# ایجاد تراکنش |
|||
transaction = TransactionParticipant.objects.create( |
|||
user=self.user, |
|||
course=self.course, |
|||
price=100.00, |
|||
status=TransactionParticipant.TransactionStatus.PENDING |
|||
) |
|||
|
|||
# بررسی که participant وجود ندارد |
|||
self.assertFalse(transaction.is_participant_enrolled()) |
|||
self.assertIsNone(transaction.get_participant()) |
|||
|
|||
# ایجاد participant |
|||
participant = Participant.objects.create( |
|||
student=self.user, |
|||
course=self.course |
|||
) |
|||
|
|||
# بررسی که participant وجود دارد |
|||
self.assertTrue(transaction.is_participant_enrolled()) |
|||
self.assertEqual(transaction.get_participant(), participant) |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue