27 changed files with 635 additions and 28 deletions
-
26apps/account/manager.py
-
18apps/account/migrations/0005_user_city.py
-
1apps/account/models/user.py
-
2apps/account/serializers/user.py
-
2apps/chat/admin.py
-
2apps/course/admin/participant.py
-
5apps/course/models/participant.py
-
3apps/course/serializers/__init__.py
-
17apps/course/serializers/participant.py
-
1apps/course/urls.py
-
42apps/course/views/participant.py
-
0apps/transaction/__init__.py
-
37apps/transaction/admin.py
-
6apps/transaction/apps.py
-
44apps/transaction/migrations/0001_initial.py
-
0apps/transaction/migrations/__init__.py
-
50apps/transaction/models.py
-
44apps/transaction/serializers.py
-
3apps/transaction/tests.py
-
13apps/transaction/urls.py
-
77apps/transaction/views.py
-
3config/settings/base.py
-
3config/urls.py
-
166dynamic_preferences/dynamic_preferences_registry.py
-
17dynamic_preferences/serializers.py
-
20dynamic_preferences/urls.py
-
61dynamic_preferences/views.py
@ -0,0 +1,18 @@ |
|||||
|
# Generated by Django 3.2.4 on 2024-11-30 23:53 |
||||
|
|
||||
|
from django.db import migrations, models |
||||
|
|
||||
|
|
||||
|
class Migration(migrations.Migration): |
||||
|
|
||||
|
dependencies = [ |
||||
|
('account', '0004_user_skill'), |
||||
|
] |
||||
|
|
||||
|
operations = [ |
||||
|
migrations.AddField( |
||||
|
model_name='user', |
||||
|
name='city', |
||||
|
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='City'), |
||||
|
), |
||||
|
] |
||||
@ -1,2 +1,3 @@ |
|||||
from .course import * |
from .course import * |
||||
from .lesson import * |
|
||||
|
from .lesson import * |
||||
|
from .participant import * |
||||
@ -0,0 +1,17 @@ |
|||||
|
from rest_framework import serializers |
||||
|
|
||||
|
|
||||
|
from apps.course.models import Lesson, Participant, LessonCompletion |
||||
|
from apps.account.models import StudentUser, User |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
class ParticipantSerializer(serializers.ModelSerializer): |
||||
|
email = serializers.EmailField(required=True) |
||||
|
gender = serializers.ChoiceField(choices=User.GenderChoices.choices, required=True) |
||||
|
|
||||
|
|
||||
|
class Meta: |
||||
|
model = StudentUser |
||||
|
fields = ['fullname' , 'phone_number', 'gender', 'email', 'birthdate'] |
||||
@ -0,0 +1,37 @@ |
|||||
|
from django.contrib import admin |
||||
|
|
||||
|
from apps.transaction.models import TransactionParticipant, ParticipantInfo |
||||
|
from django.utils.translation import gettext_lazy as _ |
||||
|
|
||||
|
|
||||
|
|
||||
|
class ParticipantInfoInline(admin.StackedInline): |
||||
|
model = ParticipantInfo |
||||
|
extra = 1 |
||||
|
fields = ['fullname', 'email', 'phone_number', 'gender', 'birthdate'] |
||||
|
# readonly_fields = ['email', 'phone_number'] |
||||
|
classes = ['collapse'] |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
@admin.register(TransactionParticipant) |
||||
|
class TransactionParticipantAdmin(admin.ModelAdmin): |
||||
|
list_display = ('user', 'course', 'is_paid', 'price', 'created_at', 'updated_at') |
||||
|
list_filter = ('is_paid', 'course', 'created_at') |
||||
|
search_fields = ('user__email', 'course__title') |
||||
|
readonly_fields = ['user', 'course', 'price', 'created_at', 'updated_at'] |
||||
|
inlines = [ParticipantInfoInline] |
||||
|
ordering = ('-created_at',) |
||||
|
list_filter = ('is_paid', 'course', 'created_at') |
||||
|
|
||||
|
fieldsets = ( |
||||
|
(None, { |
||||
|
'fields': ('user', 'course', 'is_paid', 'price') |
||||
|
}), |
||||
|
(_('Timestamps'), { |
||||
|
'fields': ('created_at', 'updated_at'), |
||||
|
'classes': ('collapse',) |
||||
|
}), |
||||
|
) |
||||
|
|
||||
@ -0,0 +1,6 @@ |
|||||
|
from django.apps import AppConfig |
||||
|
|
||||
|
|
||||
|
class TransactionConfig(AppConfig): |
||||
|
default_auto_field = 'django.db.models.BigAutoField' |
||||
|
name = 'apps.transaction' |
||||
@ -0,0 +1,44 @@ |
|||||
|
# Generated by Django 3.2.4 on 2024-11-30 22:25 |
||||
|
|
||||
|
from django.conf import settings |
||||
|
from django.db import migrations, models |
||||
|
import django.db.models.deletion |
||||
|
import phonenumber_field.modelfields |
||||
|
import utils.validators |
||||
|
|
||||
|
|
||||
|
class Migration(migrations.Migration): |
||||
|
|
||||
|
initial = True |
||||
|
|
||||
|
dependencies = [ |
||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL), |
||||
|
('course', '0005_participant_unread_messages_count'), |
||||
|
] |
||||
|
|
||||
|
operations = [ |
||||
|
migrations.CreateModel( |
||||
|
name='TransactionParticipant', |
||||
|
fields=[ |
||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
||||
|
('is_paid', models.BooleanField(default=False, help_text='Indicates whether the payment has been completed or not', verbose_name='Payment Status')), |
||||
|
('price', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='Transaction Price')), |
||||
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), |
||||
|
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')), |
||||
|
('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='course_transactions', to='course.course')), |
||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='transactions', to=settings.AUTH_USER_MODEL)), |
||||
|
], |
||||
|
), |
||||
|
migrations.CreateModel( |
||||
|
name='ParticipantInfo', |
||||
|
fields=[ |
||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
||||
|
('fullname', models.CharField(help_text='Enter the full name of the user.', max_length=255, verbose_name='Full Name')), |
||||
|
('email', models.EmailField(help_text="Enter the user's email address.", max_length=254, unique=True, verbose_name='Email Address')), |
||||
|
('phone_number', phonenumber_field.modelfields.PhoneNumberField(blank=True, max_length=128, null=True, region=None, unique=True, validators=[utils.validators.validate_possible_number], verbose_name='phone')), |
||||
|
('gender', models.CharField(blank=True, choices=[('male', 'Male'), ('female', 'Female')], help_text="Select the user's gender.", max_length=20, null=True, verbose_name='Gender')), |
||||
|
('birthdate', models.DateField(blank=True, null=True, verbose_name='birthdate')), |
||||
|
('transaction_participant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='participant_infos', to='transaction.transactionparticipant', verbose_name='Transaction Participant')), |
||||
|
], |
||||
|
), |
||||
|
] |
||||
@ -0,0 +1,50 @@ |
|||||
|
from django.db import models |
||||
|
|
||||
|
from django.utils.translation import gettext_lazy as _ |
||||
|
|
||||
|
from apps.account.models import StudentUser, User |
||||
|
from apps.course.models import Course |
||||
|
from phonenumber_field.modelfields import PhoneNumberField |
||||
|
from utils.validators import validate_possible_number |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
class TransactionParticipant(models.Model): |
||||
|
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='transactions') |
||||
|
course = models.ForeignKey(Course, on_delete=models.CASCADE, related_name='course_transactions') |
||||
|
is_paid = models.BooleanField(default=False, verbose_name='Payment Status', help_text='Indicates whether the payment has been completed or not') |
||||
|
price = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='Transaction Price') |
||||
|
|
||||
|
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created at")) |
||||
|
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated at")) |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
class ParticipantInfo(models.Model): |
||||
|
class GenderChoices(models.TextChoices): |
||||
|
MALE = 'male', 'Male' |
||||
|
FEMALE = 'female', 'Female' |
||||
|
|
||||
|
transaction_participant = models.ForeignKey( |
||||
|
TransactionParticipant, |
||||
|
on_delete=models.CASCADE, |
||||
|
related_name='participant_infos', |
||||
|
verbose_name="Transaction Participant" |
||||
|
) |
||||
|
fullname = models.CharField(max_length=255, verbose_name="Full Name", help_text="Enter the full name of the user.") |
||||
|
email = models.EmailField(unique=True, verbose_name="Email Address", help_text="Enter the user's email address.") |
||||
|
phone_number = PhoneNumberField(unique=True, validators=[validate_possible_number], null=True, blank=True, verbose_name=_('phone')) |
||||
|
gender = models.CharField( |
||||
|
max_length=20, choices=GenderChoices.choices, null=True, blank=True, verbose_name=_('Gender'), help_text="Select the user's gender." |
||||
|
) |
||||
|
birthdate = models.DateField(verbose_name=_('birthdate'), null=True, blank=True) |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
@ -0,0 +1,44 @@ |
|||||
|
|
||||
|
from rest_framework import serializers |
||||
|
|
||||
|
from apps.transaction.models import TransactionParticipant, ParticipantInfo |
||||
|
from apps.course.serializers import CourseDetailSerializer |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
class ParticipantInfoSerializer(serializers.ModelSerializer): |
||||
|
class Meta: |
||||
|
model = ParticipantInfo |
||||
|
fields = ['fullname', 'email', 'phone_number', 'gender', 'birthdate'] |
||||
|
|
||||
|
|
||||
|
class TransactionParticipantSerializer(serializers.ModelSerializer): |
||||
|
participant_infos = ParticipantInfoSerializer(many=True) |
||||
|
|
||||
|
class Meta: |
||||
|
model = TransactionParticipant |
||||
|
fields = ['participant_infos'] |
||||
|
|
||||
|
|
||||
|
def create(self, validated_data): |
||||
|
participant_infos_data = validated_data.pop('participant_infos', []) |
||||
|
transaction_participant = TransactionParticipant.objects.create(**validated_data) |
||||
|
|
||||
|
for participant_info_data in participant_infos_data: |
||||
|
ParticipantInfo.objects.create(transaction_participant=transaction_participant, **participant_info_data) |
||||
|
|
||||
|
return transaction_participant |
||||
|
|
||||
|
|
||||
|
|
||||
|
class TransactionListSerializer(serializers.ModelSerializer): |
||||
|
course = serializers.SerializerMethodField() |
||||
|
|
||||
|
class Meta: |
||||
|
model = TransactionParticipant |
||||
|
fields = ['course', 'is_paid', 'price', 'created_at', 'updated_at'] |
||||
|
|
||||
|
def get_course(self, obj): |
||||
|
return CourseDetailSerializer(obj.course, context=self.context).data |
||||
|
|
||||
@ -0,0 +1,3 @@ |
|||||
|
from django.test import TestCase |
||||
|
|
||||
|
# Create your tests here. |
||||
@ -0,0 +1,13 @@ |
|||||
|
|
||||
|
from django.urls import path |
||||
|
|
||||
|
from . import views |
||||
|
|
||||
|
|
||||
|
|
||||
|
urlpatterns = [ |
||||
|
path('<slug:slug>/join/', views.TransactionParticipantCreateView.as_view(), name='transaction-participant-create'), |
||||
|
path('list/', views.TransactiontListView.as_view(), name='transaction-list'), |
||||
|
|
||||
|
|
||||
|
] |
||||
@ -0,0 +1,77 @@ |
|||||
|
from rest_framework import generics, status |
||||
|
from rest_framework.permissions import IsAuthenticated |
||||
|
from rest_framework.response import Response |
||||
|
|
||||
|
from apps.course.models import Participant, Course |
||||
|
from apps.transaction.models import TransactionParticipant |
||||
|
from apps.transaction.serializers import TransactionParticipantSerializer, TransactionListSerializer |
||||
|
from utils.exceptions import AppAPIException |
||||
|
from apps.account.models import User |
||||
|
|
||||
|
|
||||
|
|
||||
|
class TransactionParticipantCreateView(generics.CreateAPIView): |
||||
|
queryset = TransactionParticipant.objects.all() |
||||
|
serializer_class = TransactionParticipantSerializer |
||||
|
permission_classes = [IsAuthenticated] |
||||
|
|
||||
|
|
||||
|
def create(self, request, *args, **kwargs): |
||||
|
user = request.user |
||||
|
course_slug = self.kwargs.get('slug') # Get the slug from the URL |
||||
|
try: |
||||
|
course = Course.objects.get(slug=course_slug) # Retrieve the Course object |
||||
|
except Course.DoesNotExist: |
||||
|
raise AppAPIException({'message': "Course not found"}) # Handle course not found |
||||
|
|
||||
|
participant_infos = request.data.get('participant_infos', []) |
||||
|
print(f'1---> {participant_infos}') |
||||
|
print(f'2---> {len(participant_infos)}') |
||||
|
|
||||
|
serializer = self.get_serializer(data=request.data) |
||||
|
serializer.is_valid(raise_exception=True) |
||||
|
|
||||
|
if len(participant_infos) == 1 and (course.final_price == 0 or course.is_free): |
||||
|
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) |
||||
|
|
||||
|
participant, created = Participant.objects.get_or_create( |
||||
|
student=user, |
||||
|
course=course |
||||
|
) |
||||
|
return Response({ |
||||
|
'message': 'Transaction Participant created successfully.', |
||||
|
'participant_id': participant.id, |
||||
|
'participant_infos': serializer.data['participant_infos'] |
||||
|
}, status=status.HTTP_201_CREATED) |
||||
|
|
||||
|
|
||||
|
|
||||
|
transaction_participant = serializer.save(user=user, course=course, price=course.final_price) |
||||
|
print(f'---> {type(transaction_participant)}/ {transaction_participant}') |
||||
|
return Response({ |
||||
|
'message': 'Transaction Participant created successfully.', |
||||
|
'transaction_id': transaction_participant.id, |
||||
|
'participant_infos': serializer.data['participant_infos'] |
||||
|
}, status=status.HTTP_201_CREATED) |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
class TransactiontListView(generics.ListAPIView): |
||||
|
queryset = TransactionParticipant.objects.all() # یا هر فیلتر که بخواهید اضافه کنید |
||||
|
serializer_class = TransactionListSerializer |
||||
|
permission_classes = [IsAuthenticated] # برای دسترسی کاربران احراز هویت شده |
||||
|
|
||||
|
|
||||
|
def get_queryset(self): |
||||
|
queryset = super().get_queryset() |
||||
|
queryset = queryset.filter(user=self.request.user) |
||||
|
return queryset |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue