From 18c98ff3a623c066f40ba680bbcb19e453674bfa Mon Sep 17 00:00:00 2001 From: mortezaei Date: Mon, 11 Aug 2025 18:37:35 +0330 Subject: [PATCH] docs: add repo information and update transaction admin --- .zencoder/rules/repo.md | 83 ++++++++ apps/transaction/admin.py | 51 ++++- apps/transaction/apps.py | 3 + apps/transaction/management/__init__.py | 1 + .../management/commands/__init__.py | 1 + .../commands/sync_successful_transactions.py | 201 ++++++++++++++++++ apps/transaction/models.py | 16 ++ apps/transaction/signals.py | 67 ++++++ apps/transaction/tests.py | 136 +++++++++++- apps/transaction/urls.py | 1 + apps/transaction/views.py | 10 +- config/test_auth_middleware.py | 12 +- 12 files changed, 570 insertions(+), 12 deletions(-) create mode 100644 .zencoder/rules/repo.md create mode 100644 apps/transaction/management/__init__.py create mode 100644 apps/transaction/management/commands/__init__.py create mode 100644 apps/transaction/management/commands/sync_successful_transactions.py create mode 100644 apps/transaction/signals.py diff --git a/.zencoder/rules/repo.md b/.zencoder/rules/repo.md new file mode 100644 index 0000000..03382f3 --- /dev/null +++ b/.zencoder/rules/repo.md @@ -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 \ No newline at end of file diff --git a/apps/transaction/admin.py b/apps/transaction/admin.py index 10e5ca8..ff73c78 100644 --- a/apps/transaction/admin.py +++ b/apps/transaction/admin.py @@ -1,11 +1,13 @@ from django.contrib import admin from django.utils.translation import gettext_lazy as _ from django.utils.html import format_html +from django.contrib import messages from unfold.admin import ModelAdmin, StackedInline from unfold.decorators import display from apps.transaction.models import TransactionParticipant, ParticipantInfo +from apps.course.models import Participant from utils.admin import project_admin_site @@ -21,7 +23,7 @@ class ParticipantInfoInline(StackedInline): @admin.register(TransactionParticipant) class TransactionParticipantAdmin(ModelAdmin): - list_display = ('user', 'course', 'payment_status', 'price_display', 'created_at', 'updated_at') + list_display = ('user', 'course', 'payment_status', 'price_display', 'participant_status', 'created_at', 'updated_at') list_filter = ('status', 'course', 'created_at') search_fields = ('user__email', 'course__title') readonly_fields = [ 'created_at', 'updated_at'] @@ -52,6 +54,53 @@ class TransactionParticipantAdmin(ModelAdmin): def price_display(self, obj): return format_html('${}', obj.price) + @display(description=_("Course Participant Status")) + def participant_status(self, obj): + """نمایش وضعیت شرکت‌کننده در دوره""" + if obj.status == TransactionParticipant.TransactionStatus.SUCCESS: + participant_exists = Participant.objects.filter( + student=obj.user, + course=obj.course + ).exists() + if participant_exists: + return format_html('✓ Enrolled') + else: + return format_html('⚠ Not Enrolled') + else: + return format_html('- Not Applicable') + + def save_model(self, request, obj, form, change): + """Override save_model to show messages when participant is created""" + if change: + # Store the old status before saving + old_obj = TransactionParticipant.objects.get(pk=obj.pk) + old_status = old_obj.status + + # Save the object + super().save_model(request, obj, form, change) + + # Check if status changed to SUCCESS + if (old_status != TransactionParticipant.TransactionStatus.SUCCESS and + obj.status == TransactionParticipant.TransactionStatus.SUCCESS): + + participant_exists = Participant.objects.filter( + student=obj.user, + course=obj.course + ).exists() + + if participant_exists: + messages.success( + request, + f"Transaction status updated to SUCCESS. User {obj.user.email} is now enrolled in course '{obj.course.title}'." + ) + else: + messages.warning( + request, + f"Transaction status updated to SUCCESS, but there was an issue enrolling user {obj.user.email} in course '{obj.course.title}'. Please check the logs." + ) + else: + super().save_model(request, obj, form, change) + def get_queryset(self, request): # Filter out deleted transactions return super().get_queryset(request).filter(is_deleted=False) diff --git a/apps/transaction/apps.py b/apps/transaction/apps.py index 8cf8565..d10da14 100644 --- a/apps/transaction/apps.py +++ b/apps/transaction/apps.py @@ -4,3 +4,6 @@ from django.apps import AppConfig class TransactionConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'apps.transaction' + + def ready(self): + import apps.transaction.signals diff --git a/apps/transaction/management/__init__.py b/apps/transaction/management/__init__.py new file mode 100644 index 0000000..11942f5 --- /dev/null +++ b/apps/transaction/management/__init__.py @@ -0,0 +1 @@ +# Management commands for transaction app \ No newline at end of file diff --git a/apps/transaction/management/commands/__init__.py b/apps/transaction/management/commands/__init__.py new file mode 100644 index 0000000..0b1b20d --- /dev/null +++ b/apps/transaction/management/commands/__init__.py @@ -0,0 +1 @@ +# Management commands \ No newline at end of file diff --git a/apps/transaction/management/commands/sync_successful_transactions.py b/apps/transaction/management/commands/sync_successful_transactions.py new file mode 100644 index 0000000..cddae07 --- /dev/null +++ b/apps/transaction/management/commands/sync_successful_transactions.py @@ -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 + } \ No newline at end of file diff --git a/apps/transaction/models.py b/apps/transaction/models.py index f500e54..e31d002 100644 --- a/apps/transaction/models.py +++ b/apps/transaction/models.py @@ -31,6 +31,22 @@ class TransactionParticipant(models.Model): def __str__(self): return f"{self.user.email} - {self.course.title} ({self.status})" + def is_participant_enrolled(self): + """بررسی اینکه آیا کاربر در دوره ثبت‌نام شده یا نه""" + from apps.course.models import Participant + return Participant.objects.filter( + student=self.user, + course=self.course + ).exists() + + def get_participant(self): + """دریافت شرکت‌کننده دوره اگر وجود داشته باشد""" + from apps.course.models import Participant + return Participant.objects.filter( + student=self.user, + course=self.course + ).first() + diff --git a/apps/transaction/signals.py b/apps/transaction/signals.py new file mode 100644 index 0000000..cf870f9 --- /dev/null +++ b/apps/transaction/signals.py @@ -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}") \ No newline at end of file diff --git a/apps/transaction/tests.py b/apps/transaction/tests.py index 7ce503c..08f1014 100644 --- a/apps/transaction/tests.py +++ b/apps/transaction/tests.py @@ -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) diff --git a/apps/transaction/urls.py b/apps/transaction/urls.py index babecbb..a6d1022 100644 --- a/apps/transaction/urls.py +++ b/apps/transaction/urls.py @@ -12,3 +12,4 @@ urlpatterns = [ ] + \ No newline at end of file diff --git a/apps/transaction/views.py b/apps/transaction/views.py index fc85c98..99274a4 100644 --- a/apps/transaction/views.py +++ b/apps/transaction/views.py @@ -40,10 +40,12 @@ class TransactionParticipantCreateView(generics.CreateAPIView): if not user.has_role('student'): user.add_role('student') - participant, created = Participant.objects.get_or_create( - student=user, - course=course - ) + # فیلتر کردن برای چک کردن وجود participant + existing_participant = Participant.objects.filter(student=user, course=course).first() + if existing_participant: + participant = existing_participant + else: + participant = Participant.objects.create(student=user, course=course) statis = TransactionParticipant.TransactionStatus.SUCCESS diff --git a/config/test_auth_middleware.py b/config/test_auth_middleware.py index 1cdcf3c..583f2cf 100644 --- a/config/test_auth_middleware.py +++ b/config/test_auth_middleware.py @@ -16,12 +16,12 @@ def test_auth_middleware(get_response): request.META['HTTP_AUTHORIZATION'] = "Token " + token.key - if "/swagger" in request.path or "/redoc" in request.path: - if not request.META.get('HTTP_AUTHORIZATION'): - user = User.objects.filter(is_staff=True, email="admin@gmail.com").first() - if user: - t, _ = Token.objects.get_or_create(user=user) - request.META['HTTP_AUTHORIZATION'] = f"Token {t}" + # if "/swagger" in request.path or "/redoc" in request.path: + # if not request.META.get('HTTP_AUTHORIZATION'): + # user = User.objects.filter(is_staff=True, email="admin@gmail.com").first() + # if user: + # t, _ = Token.objects.get_or_create(user=user) + # request.META['HTTP_AUTHORIZATION'] = f"Token {t}" # user = User.objects.filter(email="muhammadamin.ghorbani@gmail.com").first() # if user: