From be2c84af7787a53e6b52c308a99c038f435a46be Mon Sep 17 00:00:00 2001 From: mohsentaba Date: Thu, 28 May 2026 11:30:06 +0330 Subject: [PATCH] transactions views and serializers added for admin panel --- apps/transaction/serializers.py | 243 +++++++++++++++++++------------- apps/transaction/urls.py | 11 +- apps/transaction/views.py | 51 +++++++ 3 files changed, 205 insertions(+), 100 deletions(-) diff --git a/apps/transaction/serializers.py b/apps/transaction/serializers.py index f3c91f7..e243f1b 100644 --- a/apps/transaction/serializers.py +++ b/apps/transaction/serializers.py @@ -1,96 +1,147 @@ - -from rest_framework import serializers - -from apps.transaction.models import TransactionParticipant, ParticipantInfo, TransactionReceipt -from apps.course.serializers import CourseDetailSerializer -from utils import FileFieldSerializer - - - - -class ParticipantInfoSerializer(serializers.ModelSerializer): - phone_number = serializers.CharField(max_length=30) - - class Meta: - model = ParticipantInfo - fields = ['fullname', 'email', 'phone_number', 'gender', 'birthdate'] - - def validate_phone_number(self, value): - return value - - -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() - receipts = serializers.SerializerMethodField() - - class Meta: - model = TransactionParticipant - fields = ['id', 'course', 'status', 'price', 'receipts', 'created_at', 'updated_at'] - - def get_course(self, obj): - return CourseDetailSerializer(obj.course, context=self.context).data - - def get_receipts(self, obj): - receipts = obj.receipts.all() - return TransactionReceiptSerializer(receipts, many=True, context=self.context).data - - -class TransactionReceiptSerializer(serializers.ModelSerializer): - """ - Serializer for uploading payment receipts - Uses FileFieldSerializer to handle pre-uploaded files from /upload-tmp-media/ - """ - file = FileFieldSerializer() - - class Meta: - model = TransactionReceipt - fields = ['id', 'file', 'description', 'uploaded_at'] - read_only_fields = ['id', 'uploaded_at'] - - -class UploadReceiptsSerializer(serializers.Serializer): - """ - Serializer for uploading multiple receipt files for a transaction. - Files should be pre-uploaded using /upload-tmp-media/ endpoint, - then their URLs should be sent here. - """ - files = serializers.ListField( - child=FileFieldSerializer(), - allow_empty=False, - max_length=10, - help_text="List of file URLs (max 10 files) - files should be pre-uploaded via /upload-tmp-media/" - ) - description = serializers.CharField( - required=False, - allow_blank=True, - max_length=1000, - help_text="Optional description for the receipts" - ) - - def validate_files(self, files): - """ - Validate uploaded file URLs - """ - if len(files) > 10: - raise serializers.ValidationError("You can upload a maximum of 10 files.") - - return files \ No newline at end of file + +from rest_framework import serializers + +from apps.transaction.models import TransactionParticipant, ParticipantInfo, TransactionReceipt +from apps.course.serializers import CourseDetailSerializer +from utils import FileFieldSerializer + + + + +class ParticipantInfoSerializer(serializers.ModelSerializer): + phone_number = serializers.CharField(max_length=30) + + class Meta: + model = ParticipantInfo + fields = ['fullname', 'email', 'phone_number', 'gender', 'birthdate'] + + def validate_phone_number(self, value): + return value + + +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() + receipts = serializers.SerializerMethodField() + + class Meta: + model = TransactionParticipant + fields = ['id', 'course', 'status', 'price', 'receipts', 'created_at', 'updated_at'] + + def get_course(self, obj): + return CourseDetailSerializer(obj.course, context=self.context).data + + def get_receipts(self, obj): + receipts = obj.receipts.all() + return TransactionReceiptSerializer(receipts, many=True, context=self.context).data + + +class TransactionReceiptSerializer(serializers.ModelSerializer): + """ + Serializer for uploading payment receipts + Uses FileFieldSerializer to handle pre-uploaded files from /upload-tmp-media/ + """ + file = FileFieldSerializer() + + class Meta: + model = TransactionReceipt + fields = ['id', 'file', 'description', 'uploaded_at'] + read_only_fields = ['id', 'uploaded_at'] + + +class UploadReceiptsSerializer(serializers.Serializer): + """ + Serializer for uploading multiple receipt files for a transaction. + Files should be pre-uploaded using /upload-tmp-media/ endpoint, + then their URLs should be sent here. + """ + files = serializers.ListField( + child=FileFieldSerializer(), + allow_empty=False, + max_length=10, + help_text="List of file URLs (max 10 files) - files should be pre-uploaded via /upload-tmp-media/" + ) + description = serializers.CharField( + required=False, + allow_blank=True, + max_length=1000, + help_text="Optional description for the receipts" + ) + + def validate_files(self, files): + """ + Validate uploaded file URLs + """ + if len(files) > 10: + raise serializers.ValidationError("You can upload a maximum of 10 files.") + + return files + + +# ============================================================================= +# Admin Panel Serializers +# ============================================================================= + +class AdminTransactionReceiptSerializer(serializers.ModelSerializer): + file = serializers.SerializerMethodField() + + class Meta: + model = TransactionReceipt + fields = ['id', 'file', 'description', 'uploaded_at'] + + def get_file(self, obj): + request = self.context.get('request') + if obj.file: + if request: + return request.build_absolute_uri(obj.file.url) + return obj.file.url + return None + + +class AdminParticipantInfoSerializer(serializers.ModelSerializer): + phone_number = serializers.CharField(read_only=True) + + class Meta: + model = ParticipantInfo + fields = ['id', 'fullname', 'email', 'phone_number', 'gender', 'birthdate'] + + +class AdminTransactionSerializer(serializers.ModelSerializer): + user_name = serializers.CharField(source='user.fullname', read_only=True) + user_email = serializers.CharField(source='user.email', read_only=True) + course_title = serializers.CharField(source='course.title', read_only=True) + receipts = AdminTransactionReceiptSerializer(many=True, read_only=True) + participant_infos = AdminParticipantInfoSerializer(many=True, read_only=True) + + class Meta: + model = TransactionParticipant + fields = [ + 'id', 'user', 'user_name', 'user_email', + 'course', 'course_title', + 'price', 'status', 'payment_method', + 'participant_infos', 'receipts', + 'created_at', 'updated_at' + ] + read_only_fields = [ + 'id', 'user', 'user_name', 'user_email', + 'course', 'course_title', 'price', 'payment_method', + 'participant_infos', 'receipts', 'created_at', 'updated_at' + ] \ No newline at end of file diff --git a/apps/transaction/urls.py b/apps/transaction/urls.py index 7614de9..bd99d98 100644 --- a/apps/transaction/urls.py +++ b/apps/transaction/urls.py @@ -1,16 +1,19 @@ -from django.urls import path, re_path +from django.urls import path, re_path, include +from rest_framework.routers import DefaultRouter from . import views +from .views import AdminTransactionViewSet - +router = DefaultRouter() +router.register(r'admin/transactions', AdminTransactionViewSet, basename='admin-transactions') urlpatterns = [ + path('', include(router.urls)), re_path(r'(?P[\w-]+)/join/$', views.TransactionParticipantCreateView.as_view(), name='transaction-participant-create'), path('list/', views.TransactiontListView.as_view(), name='transaction-list'), path('/delete/', views.SoftDeleteTransactionParticipantView.as_view(), name='soft-delete-transaction-participant'), path('/receipts/upload/', views.UploadTransactionReceiptsView.as_view(), name='upload-transaction-receipts'), path('/receipts/', views.TransactionReceiptsListView.as_view(), name='transaction-receipts-list'), -] - \ No newline at end of file +] \ No newline at end of file diff --git a/apps/transaction/views.py b/apps/transaction/views.py index adde1d1..ce68e00 100644 --- a/apps/transaction/views.py +++ b/apps/transaction/views.py @@ -410,3 +410,54 @@ class TransactionReceiptsListView(generics.ListAPIView): ) return TransactionReceipt.objects.filter(transaction=transaction) + + +# ============================================================================= +# Admin Panel ViewSet +# ============================================================================= + +from rest_framework.viewsets import ModelViewSet +from rest_framework.permissions import IsAdminUser +from apps.transaction.serializers import AdminTransactionSerializer +from django.db.models import Q + +class AdminTransactionViewSet(ModelViewSet): + """ + Admin-only viewset for viewing and updating transactions. + Only GET (list/retrieve) and PATCH (update status) are allowed. + """ + queryset = TransactionParticipant.objects.all() + serializer_class = AdminTransactionSerializer + permission_classes = [IsAuthenticated, IsAdminUser] + authentication_classes = [TokenAuthentication] + http_method_names = ['get', 'patch', 'head', 'options'] + + def get_queryset(self): + queryset = TransactionParticipant.objects.all().select_related('user', 'course').prefetch_related('receipts', 'participant_infos') + + # Search + search_query = self.request.query_params.get('search', None) + if search_query: + queryset = queryset.filter( + Q(user__fullname__icontains=search_query) | + Q(user__email__icontains=search_query) | + Q(course__title__icontains=search_query) + ) + + # Status filter + status_param = self.request.query_params.get('status', None) + if status_param: + queryset = queryset.filter(status=status_param) + + # Course filter + course_id = self.request.query_params.get('course', None) + if course_id: + queryset = queryset.filter(course_id=course_id) + + # Payment method filter + payment_method = self.request.query_params.get('payment_method', None) + if payment_method: + queryset = queryset.filter(payment_method=payment_method) + + return queryset.order_by('-created_at') +