diff --git a/apps/certificate/admin.py b/apps/certificate/admin.py index 92943e1..4ac3883 100644 --- a/apps/certificate/admin.py +++ b/apps/certificate/admin.py @@ -1,4 +1,5 @@ from django.contrib import admin +from django.db.models import Q from django.utils.translation import gettext_lazy as _ from django.utils.html import format_html @@ -6,15 +7,40 @@ from unfold.admin import ModelAdmin from unfold.decorators import display from apps.certificate.models import Certificate +from apps.course.models import Course +from apps.course.models.course import extract_text_from_json from utils.admin import project_admin_site from apps.course.admin.professor_base import CertificateBaseAdmin + +def find_matching_course_ids(search_term: str): + normalized = (search_term or "").strip().lower() + if not normalized: + return [] + + matching_ids = list( + Course.objects.filter( + Q(title__icontains=search_term) | Q(slug__icontains=search_term) + ).values_list("id", flat=True) + ) + + if matching_ids: + return matching_ids + + for course in Course.objects.all().only("id", "title", "slug"): + title_text = extract_text_from_json(course.title).lower() + slug_text = extract_text_from_json(course.slug).lower() + if normalized in title_text or normalized in slug_text: + matching_ids.append(course.id) + + return matching_ids + @admin.register(Certificate) class CertificateAdmin(CertificateBaseAdmin): list_display = ['student', 'course', 'certificate_status', 'created_at'] list_filter = ['status', 'created_at'] - search_fields = ['id', 'student__username', 'student__email', 'course__title'] + search_fields = ['id', 'student__username', 'student__email'] readonly_fields = ['created_at', 'updated_at'] autocomplete_fields = ['student',] fieldsets = ( @@ -42,4 +68,14 @@ class CertificateAdmin(CertificateBaseAdmin): def get_queryset(self, request): queryset = super().get_queryset(request) return queryset -project_admin_site.register(Certificate, CertificateAdmin) \ No newline at end of file + + def get_search_results(self, request, queryset, search_term): + queryset, use_distinct = super().get_search_results(request, queryset, search_term) + matching_course_ids = find_matching_course_ids(search_term) + + if matching_course_ids: + queryset = queryset | self.model.objects.filter(course_id__in=matching_course_ids) + use_distinct = True + + return queryset, use_distinct +project_admin_site.register(Certificate, CertificateAdmin) diff --git a/apps/certificate/models.py b/apps/certificate/models.py index 9a321ee..b31eed2 100644 --- a/apps/certificate/models.py +++ b/apps/certificate/models.py @@ -3,6 +3,7 @@ from django.db import models from django.utils.translation import gettext_lazy as _ from filer.fields.file import FilerFileField from apps.course.models import Course +from apps.course.models.course import extract_text_from_json from apps.account.models import StudentUser @@ -23,6 +24,7 @@ class Certificate(models.Model): updated_at = models.DateTimeField(auto_now=True) def __str__(self): - return f"Certificate {self.student.fullname} - {self.course.title}" + course_title = extract_text_from_json(self.course.title) if self.course_id else "" + return f"Certificate {self.student.fullname} - {course_title}" + - \ No newline at end of file diff --git a/apps/certificate/views.py b/apps/certificate/views.py index 3ab67f1..14f2dac 100644 --- a/apps/certificate/views.py +++ b/apps/certificate/views.py @@ -2,12 +2,38 @@ from rest_framework import generics, permissions from rest_framework.authentication import TokenAuthentication from drf_yasg.utils import swagger_auto_schema from drf_yasg import openapi +from django.db.models import Q from apps.certificate.models import Certificate from apps.certificate.serializers import CertificateRequestSerializer, CertificateSerializer +from apps.course.models import Course +from apps.course.models.course import extract_text_from_json from utils.pagination import StandardResultsSetPagination +def find_matching_course_ids(search_term: str): + normalized = (search_term or "").strip().lower() + if not normalized: + return [] + + matching_ids = list( + Course.objects.filter( + Q(title__icontains=search_term) | Q(slug__icontains=search_term) + ).values_list("id", flat=True) + ) + + if matching_ids: + return matching_ids + + for course in Course.objects.all().only("id", "title", "slug"): + title_text = extract_text_from_json(course.title).lower() + slug_text = extract_text_from_json(course.slug).lower() + if normalized in title_text or normalized in slug_text: + matching_ids.append(course.id) + + return matching_ids + + class CertificateRequestView(generics.CreateAPIView): queryset = Certificate.objects.all() @@ -82,10 +108,11 @@ class AdminCertificateViewSet(ModelViewSet): # Search query search_query = self.request.query_params.get('search', None) if search_query: + matching_course_ids = find_matching_course_ids(search_query) queryset = queryset.filter( Q(student__fullname__icontains=search_query) | Q(student__email__icontains=search_query) | - Q(course__title__icontains=search_query) + Q(course_id__in=matching_course_ids) ) # Filters @@ -115,4 +142,4 @@ class AdminCertificateViewSet(ModelViewSet): return queryset.order_by('-created_at') - \ No newline at end of file + diff --git a/apps/quiz/admin/quiz.py b/apps/quiz/admin/quiz.py index db0dde2..33ac9b2 100644 --- a/apps/quiz/admin/quiz.py +++ b/apps/quiz/admin/quiz.py @@ -1,4 +1,5 @@ from django.contrib import admin, messages +from django.db.models import Q from django.db.models import Count from django.utils.safestring import mark_safe from django.urls import reverse @@ -9,6 +10,8 @@ from unfold.admin import ModelAdmin from unfold.decorators import display, action from apps.course.models import CourseLesson +from apps.course.models import Course +from apps.course.models.course import extract_text_from_json from apps.quiz.models import Quiz from apps.quiz.admin.question import QuestionAdminInline from apps.quiz.admin.participant import QuizParticipantInline @@ -16,7 +19,7 @@ from utils.admin import project_admin_site, admin_url_generator class QuizAdmin(ModelAdmin): - search_fields = ['title', 'lesson__title'] + search_fields = ['title', 'description'] list_display = ['title', 'description', 'lesson', 'each_question_timing', 'status_display', 'questions_display'] list_filter = ['each_question_timing', 'status'] inlines = [QuestionAdminInline, QuizParticipantInline] @@ -41,6 +44,43 @@ class QuizAdmin(ModelAdmin): return queryset.filter(lesson__course__professor=request.user) return queryset.none() + + def _find_matching_course_ids(self, search_term): + normalized = (search_term or "").strip().lower() + if not normalized: + return [] + + matching_ids = list( + Course.objects.filter( + Q(title__icontains=search_term) | Q(slug__icontains=search_term) + ).values_list("id", flat=True) + ) + + if matching_ids: + return matching_ids + + for course in Course.objects.all().only("id", "title", "slug"): + title_text = extract_text_from_json(course.title).lower() + slug_text = extract_text_from_json(course.slug).lower() + if normalized in title_text or normalized in slug_text: + matching_ids.append(course.id) + + return matching_ids + + def _find_matching_lesson_ids(self, search_term): + normalized = (search_term or "").strip().lower() + if not normalized: + return [] + + matching_ids = [] + for lesson in CourseLesson.objects.select_related("lesson").all(): + lesson_title = extract_text_from_json( + lesson.title or (lesson.lesson.title if lesson.lesson else []) + ).lower() + if normalized in lesson_title: + matching_ids.append(lesson.id) + + return matching_ids def get_exclude(self, request, obj=None): if not obj: # اگر obj وجود ندارد یعنی صفحه Add است @@ -72,6 +112,19 @@ class QuizAdmin(ModelAdmin): form.base_fields['lesson'].widget.can_add_related = False return form + + def get_search_results(self, request, queryset, search_term): + queryset, use_distinct = super().get_search_results(request, queryset, search_term) + matching_course_ids = self._find_matching_course_ids(search_term) + matching_lesson_ids = self._find_matching_lesson_ids(search_term) + + if matching_course_ids or matching_lesson_ids: + queryset = queryset | self.model.objects.filter( + Q(course_id__in=matching_course_ids) | Q(lesson_id__in=matching_lesson_ids) + ) + use_distinct = True + + return queryset, use_distinct @display(description=_('Status'), ordering='status') def status_display(self, obj): @@ -119,4 +172,4 @@ class QuizAdmin(ModelAdmin): messages.error(request, _("Course not found for this quiz.")) return redirect(request.META.get('HTTP_REFERER', '/')) -project_admin_site.register(Quiz, QuizAdmin) \ No newline at end of file +project_admin_site.register(Quiz, QuizAdmin) diff --git a/apps/quiz/views/admin.py b/apps/quiz/views/admin.py index 0d77448..96faba0 100644 --- a/apps/quiz/views/admin.py +++ b/apps/quiz/views/admin.py @@ -10,6 +10,7 @@ from utils.pagination import StandardResultsSetPagination from apps.quiz.models import Quiz, Question, QuizParticipant from apps.course.models import Course, CourseLesson, Participant +from apps.course.models.course import extract_text_from_json from apps.account.models import User from apps.account.permissions import IsPanelUser @@ -28,6 +29,45 @@ def is_professor(request): return getattr(request.user, 'user_type', None) == 'professor' +def find_matching_course_ids(search_term: str): + normalized = (search_term or "").strip().lower() + if not normalized: + return [] + + matching_ids = list( + Course.objects.filter( + Q(title__icontains=search_term) | Q(slug__icontains=search_term) + ).values_list("id", flat=True) + ) + + if matching_ids: + return matching_ids + + for course in Course.objects.all().only("id", "title", "slug"): + title_text = extract_text_from_json(course.title).lower() + slug_text = extract_text_from_json(course.slug).lower() + if normalized in title_text or normalized in slug_text: + matching_ids.append(course.id) + + return matching_ids + + +def find_matching_lesson_ids(search_term: str): + normalized = (search_term or "").strip().lower() + if not normalized: + return [] + + matching_ids = [] + for lesson in CourseLesson.objects.select_related("lesson").all(): + lesson_title = extract_text_from_json( + lesson.title or (lesson.lesson.title if lesson.lesson else []) + ).lower() + if normalized in lesson_title: + matching_ids.append(lesson.id) + + return matching_ids + + class AdminQuizViewSet(ModelViewSet): """ Admin CRUD ViewSet for Quiz management. @@ -52,9 +92,13 @@ class AdminQuizViewSet(ModelViewSet): # Handle Search search_query = self.request.query_params.get('search', None) if search_query: + matching_course_ids = find_matching_course_ids(search_query) + matching_lesson_ids = find_matching_lesson_ids(search_query) queryset = queryset.filter( Q(title__icontains=search_query) | - Q(description__icontains=search_query) + Q(description__icontains=search_query) | + Q(course_id__in=matching_course_ids) | + Q(lesson_id__in=matching_lesson_ids) ) # Handle Course Filter diff --git a/apps/transaction/views.py b/apps/transaction/views.py index 181a85d..7bff327 100644 --- a/apps/transaction/views.py +++ b/apps/transaction/views.py @@ -3,7 +3,9 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView from rest_framework.authentication import TokenAuthentication +from django.db.models import Q from apps.course.models import Participant, Course +from apps.course.models.course import extract_text_from_json from apps.transaction.models import TransactionParticipant, TransactionReceipt from apps.transaction.serializers import ( TransactionParticipantSerializer, @@ -26,6 +28,32 @@ from apps.transaction.doc import ( from utils.ip_helper import get_client_ip, get_country_code +def find_course_by_multilingual_identifier(identifier: str): + normalized = (identifier or "").strip() + if not normalized: + return None + + course = Course.objects.filter(slug__contains=[{"title": normalized}]).first() + if course: + return course + + course = Course.objects.filter( + Q(title__icontains=normalized) | + Q(slug__icontains=normalized) + ).first() + if course: + return course + + normalized_lower = normalized.lower() + for item in Course.objects.all().only("id", "title", "slug"): + title_text = extract_text_from_json(item.title).lower() + slug_text = extract_text_from_json(item.slug).lower() + if normalized_lower in title_text or normalized_lower in slug_text: + return item + + return None + + class TransactionParticipantCreateView(generics.CreateAPIView): queryset = TransactionParticipant.objects.all() @@ -118,9 +146,8 @@ class TransactionParticipantCreateView(generics.CreateAPIView): course_slug = self.kwargs.get('slug') # 1. Retrieve Course - try: - course = Course.objects.get(slug=course_slug) - except Course.DoesNotExist: + course = find_course_by_multilingual_identifier(course_slug) + if not course: raise AppAPIException({'message': "Course not found"}) participant_infos = request.data.get('participant_infos', []) @@ -450,10 +477,25 @@ class AdminTransactionViewSet(ModelViewSet): # Search search_query = self.request.query_params.get('search', None) if search_query: + matching_course_ids = list( + Course.objects.filter( + Q(title__icontains=search_query) | + Q(slug__icontains=search_query) + ).values_list("id", flat=True) + ) + if not matching_course_ids: + normalized_search = search_query.lower().strip() + matching_course_ids = [ + course.id + for course in Course.objects.all().only("id", "title", "slug") + if normalized_search in extract_text_from_json(course.title).lower() + or normalized_search in extract_text_from_json(course.slug).lower() + ] + queryset = queryset.filter( Q(user__fullname__icontains=search_query) | Q(user__email__icontains=search_query) | - Q(course__title__icontains=search_query) + Q(course_id__in=matching_course_ids) ) # Status filter