Browse Source

docs: add repo information and update transaction admin

master
mortezaei 9 months ago
parent
commit
18c98ff3a6
  1. 83
      .zencoder/rules/repo.md
  2. 51
      apps/transaction/admin.py
  3. 3
      apps/transaction/apps.py
  4. 1
      apps/transaction/management/__init__.py
  5. 1
      apps/transaction/management/commands/__init__.py
  6. 201
      apps/transaction/management/commands/sync_successful_transactions.py
  7. 16
      apps/transaction/models.py
  8. 67
      apps/transaction/signals.py
  9. 136
      apps/transaction/tests.py
  10. 1
      apps/transaction/urls.py
  11. 10
      apps/transaction/views.py
  12. 12
      config/test_auth_middleware.py

83
.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

51
apps/transaction/admin.py

@ -1,11 +1,13 @@
from django.contrib import admin from django.contrib import admin
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.utils.html import format_html from django.utils.html import format_html
from django.contrib import messages
from unfold.admin import ModelAdmin, StackedInline from unfold.admin import ModelAdmin, StackedInline
from unfold.decorators import display from unfold.decorators import display
from apps.transaction.models import TransactionParticipant, ParticipantInfo from apps.transaction.models import TransactionParticipant, ParticipantInfo
from apps.course.models import Participant
from utils.admin import project_admin_site from utils.admin import project_admin_site
@ -21,7 +23,7 @@ class ParticipantInfoInline(StackedInline):
@admin.register(TransactionParticipant) @admin.register(TransactionParticipant)
class TransactionParticipantAdmin(ModelAdmin): 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') list_filter = ('status', 'course', 'created_at')
search_fields = ('user__email', 'course__title') search_fields = ('user__email', 'course__title')
readonly_fields = [ 'created_at', 'updated_at'] readonly_fields = [ 'created_at', 'updated_at']
@ -52,6 +54,53 @@ class TransactionParticipantAdmin(ModelAdmin):
def price_display(self, obj): def price_display(self, obj):
return format_html('<span class="unfold-badge unfold-badge--info">${}</span>', obj.price) return format_html('<span class="unfold-badge unfold-badge--info">${}</span>', 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('<span class="unfold-badge unfold-badge--success">✓ Enrolled</span>')
else:
return format_html('<span class="unfold-badge unfold-badge--warning">⚠ Not Enrolled</span>')
else:
return format_html('<span class="unfold-badge unfold-badge--secondary">- Not Applicable</span>')
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): def get_queryset(self, request):
# Filter out deleted transactions # Filter out deleted transactions
return super().get_queryset(request).filter(is_deleted=False) return super().get_queryset(request).filter(is_deleted=False)

3
apps/transaction/apps.py

@ -4,3 +4,6 @@ from django.apps import AppConfig
class TransactionConfig(AppConfig): class TransactionConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField' default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.transaction' name = 'apps.transaction'
def ready(self):
import apps.transaction.signals

1
apps/transaction/management/__init__.py

@ -0,0 +1 @@
# Management commands for transaction app

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

@ -0,0 +1 @@
# Management commands

201
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
}

16
apps/transaction/models.py

@ -31,6 +31,22 @@ class TransactionParticipant(models.Model):
def __str__(self): def __str__(self):
return f"{self.user.email} - {self.course.title} ({self.status})" 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()

67
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}")

136
apps/transaction/tests.py

@ -1,3 +1,137 @@
from django.test import TestCase 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)

1
apps/transaction/urls.py

@ -12,3 +12,4 @@ urlpatterns = [
] ]

10
apps/transaction/views.py

@ -40,10 +40,12 @@ class TransactionParticipantCreateView(generics.CreateAPIView):
if not user.has_role('student'): if not user.has_role('student'):
user.add_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 statis = TransactionParticipant.TransactionStatus.SUCCESS

12
config/test_auth_middleware.py

@ -16,12 +16,12 @@ def test_auth_middleware(get_response):
request.META['HTTP_AUTHORIZATION'] = "Token " + token.key 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() # user = User.objects.filter(email="muhammadamin.ghorbani@gmail.com").first()
# if user: # if user:

Loading…
Cancel
Save