From e5930c2cbf00c71075e13abe213b20fe848ee62f Mon Sep 17 00:00:00 2001 From: Ahmed Nagi <144544047+mindfreq@users.noreply.github.com> Date: Sun, 19 Jan 2025 12:08:03 +0000 Subject: [PATCH] u --- config/settings/base.py | 2 +- lms/accounts/validation_error.py | 13 +- lms/app/admin.py | 6 +- ...iz_alter_enrollment_student_delete_quiz.py | 29 ++++ lms/app/models.py | 18 +-- lms/app/serializers.py | 23 ++-- lms/app/urls.py | 6 +- lms/app/views.py | 126 ++++++++++-------- 8 files changed, 131 insertions(+), 92 deletions(-) create mode 100644 lms/app/migrations/0002_lesson_quiz_alter_enrollment_student_delete_quiz.py diff --git a/config/settings/base.py b/config/settings/base.py index f0997bc..501b774 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -390,7 +390,7 @@ REST_AUTH = { from datetime import timedelta SIMPLE_JWT = { 'ACCESS_TOKEN_LIFETIME': timedelta(hours=5), - 'REFRESH_TOKEN_LIFETIME': timedelta(days=30), + 'REFRESH_TOKEN_LIFETIME': timedelta(days=15), 'ROTATE_REFRESH_TOKENS': True, 'BLACKLIST_AFTER_ROTATION': True, 'ALGORITHM': 'HS256', diff --git a/lms/accounts/validation_error.py b/lms/accounts/validation_error.py index f016ee0..1e4656b 100644 --- a/lms/accounts/validation_error.py +++ b/lms/accounts/validation_error.py @@ -1,4 +1,15 @@ from rest_framework.exceptions import APIException +from rest_framework.response import Response +from rest_framework import status + + +class CustomSuccessResponse(Response): + def __init__(self, detail=None, code=status.HTTP_200_OK): + data = {"success": True} + if detail is not None: + data["detail"] = detail + super().__init__(data, status=code) + class CustomValidationError(APIException): @@ -10,4 +21,4 @@ class CustomValidationError(APIException): if detail is not None: self.detail = detail if code is not None: - self.status_code = code \ No newline at end of file + self.status_code = code diff --git a/lms/app/admin.py b/lms/app/admin.py index 70cceae..6867131 100644 --- a/lms/app/admin.py +++ b/lms/app/admin.py @@ -26,11 +26,7 @@ class EnrollmentAdmin(admin.ModelAdmin): search_fields = ('student__username', 'course__title') list_filter = ('enrolled_at', 'completed') -@admin.register(Quiz) -class QuizAdmin(admin.ModelAdmin): - list_display = ('title', 'module') - search_fields = ('title', 'module__title') - list_filter = ('module',) + @admin.register(Certificate) class CertificateAdmin(admin.ModelAdmin): diff --git a/lms/app/migrations/0002_lesson_quiz_alter_enrollment_student_delete_quiz.py b/lms/app/migrations/0002_lesson_quiz_alter_enrollment_student_delete_quiz.py new file mode 100644 index 0000000..4909bbd --- /dev/null +++ b/lms/app/migrations/0002_lesson_quiz_alter_enrollment_student_delete_quiz.py @@ -0,0 +1,29 @@ +# Generated by Django 5.0.10 on 2025-01-19 09:26 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('app', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='lesson', + name='quiz', + field=models.JSONField(null=True), + ), + migrations.AlterField( + model_name='enrollment', + name='student', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='students_enrollments', to=settings.AUTH_USER_MODEL, verbose_name='Student'), + ), + migrations.DeleteModel( + name='Quiz', + ), + ] diff --git a/lms/app/models.py b/lms/app/models.py index da58c03..f1582ec 100644 --- a/lms/app/models.py +++ b/lms/app/models.py @@ -49,7 +49,7 @@ class Lesson(models.Model): module = models.ForeignKey(Module, on_delete=models.CASCADE, related_name='lessons', verbose_name="Module") file = models.FileField(upload_to='lesson_files/', null=True, blank=True, verbose_name="Attached File") created_by = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True, verbose_name="Created By") - #order = models.IntegerField(null=True, blank=True) + quiz = models.JSONField(null=True) def str(self): return self.title @@ -57,7 +57,7 @@ class Lesson(models.Model): # Table for enrollments (Enrollment) class Enrollment(models.Model): id = models.UUIDField(primary_key=True, default=uuid4, editable=False) - student = models.ForeignKey(User, on_delete=models.CASCADE, related_name='enrollments', verbose_name="Student") + student = models.ForeignKey(User, on_delete=models.CASCADE, related_name='students_enrollments', verbose_name="Student") course = models.ForeignKey(Course, on_delete=models.CASCADE, related_name='enrollments', verbose_name="Course") enrolled_at = models.DateTimeField(auto_now_add=True, verbose_name="Enrollment Date") completed = models.BooleanField(default=False, verbose_name="Completed") @@ -65,20 +65,6 @@ class Enrollment(models.Model): def str(self): return f"{self.student.username} - {self.course.title}" -# Table for quizzes (Quiz) -class Quiz(models.Model): - id = models.UUIDField(primary_key=True, default=uuid4, editable=False) - title = models.CharField(max_length=255, verbose_name="Quiz Title") - module = models.ForeignKey(Module, on_delete=models.CASCADE, related_name='quiz', verbose_name="Module") - questions = models.JSONField(verbose_name="Questions", null=True) # Stores questions as a JSON list - - def str(self): - return self.title - - - def str(self): - return f"{self.student.username} - {self.quiz.title}" - # Table for certificates (Certificate) class Certificate(models.Model): id = models.UUIDField(primary_key=True, default=uuid4, editable=False) diff --git a/lms/app/serializers.py b/lms/app/serializers.py index 8428be6..1796dad 100644 --- a/lms/app/serializers.py +++ b/lms/app/serializers.py @@ -12,11 +12,15 @@ from dj_rest_auth.registration.serializers import RegisterSerializer class CourseSerializer(serializers.ModelSerializer): owner_name = serializers.CharField(source='owner.username', read_only=True) + students_in_course = serializers.SerializerMethodField() class Meta: model = Course - fields = ['id', 'title', 'description', 'is_paid', 'price', 'image', 'owner_name', 'created_at', 'updated_at'] + fields = ['id', 'title', 'description', 'is_paid', 'price', 'image', 'owner_name', 'students_in_course', 'created_at', 'updated_at'] read_only_fields = ['created_at', 'updated_at'] + def get_students_in_course(self, obj): + return Enrollment.objects.filter(course=obj).values('student').distinct().count() + class LessonSerializer(serializers.ModelSerializer): @@ -64,15 +68,16 @@ class EnrollmentSerializer(serializers.ModelSerializer): "rating": course.rating, } - - -class QuizSerializer(serializers.ModelSerializer): - class Meta: - model = Quiz - fields = ['id', 'title', 'module', 'questions'] - + class CertificateSerializer(serializers.ModelSerializer): class Meta: model = Certificate - fields = ['student', 'course', 'issued_at', 'certificate_file'] \ No newline at end of file + fields = ['student', 'course', 'issued_at', 'certificate_file'] + + +class PrivateEnrollmentSerializer(serializers.ModelSerializer): + class Meta: + model = Enrollment + fields = ['id', 'course', 'student', 'enrolled_at'] + read_only_fields = ['enrolled_at'] \ No newline at end of file diff --git a/lms/app/urls.py b/lms/app/urls.py index 4651c76..c860954 100644 --- a/lms/app/urls.py +++ b/lms/app/urls.py @@ -7,7 +7,9 @@ router.register(r'courses', CourseViewSet, basename='course') router.register(r'modules', ModuleViewSet, basename='modules') router.register(r'lessons', LessonViewSet, basename='lessons') router.register(r'enrollment', EnrollmentViewSet, basename='enrollment') -router.register(r'quiz', QuizViewSet, basename='quiz') # router.register(r'certificate', CertificateViewSet, basename='certificate') -urlpatterns = router.urls \ No newline at end of file +urlpatterns = [ + path('private-enrollment/', PrivateEnrollment.as_view()), + +] + router.urls \ No newline at end of file diff --git a/lms/app/views.py b/lms/app/views.py index 2e594d5..02f1e48 100644 --- a/lms/app/views.py +++ b/lms/app/views.py @@ -4,12 +4,17 @@ from .models import * from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet from rest_framework.response import Response from rest_framework import status +from rest_framework.views import APIView from rest_framework.permissions import IsAuthenticated, BasePermission from .permissions import IsOwnerOrReadOnly, IsAdmin from rest_framework.decorators import action from rest_framework.exceptions import PermissionDenied +from django.contrib.auth import get_user_model +from accounts.validation_error import CustomSuccessResponse, CustomValidationError +User = get_user_model() + class CourseViewSet(ModelViewSet): """ A ViewSet for viewing and editing Course instances. @@ -22,6 +27,7 @@ class CourseViewSet(ModelViewSet): """ Save the post data when creating a new course. """ + serializer.save(owner=self.request.user) @action(detail=False, methods=['get'], url_path='my-courses', url_name='my_courses') @@ -32,10 +38,16 @@ class CourseViewSet(ModelViewSet): my_courses = Course.objects.filter(owner=request.user) - # Serialize the data + total_students = Enrollment.objects.filter(course__in=my_courses).values('student').distinct().count() + + # Serialize the data serializer = self.get_serializer(my_courses, many=True) + response_data = { + "total_students": total_students, # Add the total count of students + "courses": serializer.data # Include detailed courses data + } - return Response(serializer.data) + return Response(response_data) @@ -227,9 +239,13 @@ class EnrollmentViewSet(ModelViewSet): except Course.DoesNotExist: return Response({"detail": "Course not found"}, status=status.HTTP_404_NOT_FOUND) + if course.is_paid: + return Response({"detail": "This is paid"}, status=status.HTTP_404_NOT_FOUND) + if Enrollment.objects.filter(student=request.user, course=course).exists(): return Response({"detail": "You are already subscribed to this course."}, status=status.HTTP_404_NOT_FOUND) - elif course.owner == request.user: + + if course.owner == request.user: return Response({"detail": "You can't enroll in your course"}, status=status.HTTP_404_NOT_FOUND) # Create a new enrollment @@ -237,61 +253,7 @@ class EnrollmentViewSet(ModelViewSet): serializer = self.get_serializer(enrollment) return Response(serializer.data, status=status.HTTP_201_CREATED) - - -class QuizViewSet(ModelViewSet): - queryset = Quiz.objects.all() - serializer_class = QuizSerializer - permission_classes = [IsAuthenticated] - def create(self, request, *args, **kwargs): - - # Ensure the user is an owner - if request.user.role != 'owner': - return Response({"detail": "Only owners can create quizzes"}, status=status.HTTP_403_FORBIDDEN) - # Get course data from the request - moduleId = request.data.get('module') - # Check if the course exists - try: - module = Module.objects.get(id=moduleId) - except Course.DoesNotExist: - return Response({"detail": "Course not found"}, status=status.HTTP_404_NOT_FOUND) - # Ensure the current owner is the course owner - if module.course.owner != request.user: - return Response({"detail": "You can only create quizzes for your own courses"}, status=status.HTTP_403_FORBIDDEN) - # Create a new quiz - quiz = Quiz.objects.create(module=module) - serializer = self.get_serializer(quiz) - return Response(serializer.data, status=status.HTTP_201_CREATED) - - - def update(self, request, *args, **kwargs): - # Ensure the user is an owner - if request.user.role != 'owner': - return Response({"detail": "Only owners can update quizzes"}, status=status.HTTP_403_FORBIDDEN) - # Get the quiz object to update - quiz = self.get_object() - # Ensure the current owner is the course owner - if quiz.module.course.owner != request.user: - return Response({"detail": "You can only update quizzes for your own courses"}, status=status.HTTP_403_FORBIDDEN) - # Update the quiz - serializer = self.get_serializer(quiz, data=request.data, partial=True) - serializer.is_valid(raise_exception=True) - serializer.save() - return Response(serializer.data, status=status.HTTP_200_OK) - def destroy(self, request, *args, **kwargs): - - # Ensure the user is an owner - if request.user.role != 'owner': - return Response({"detail": "Only owners can delete quizzes"}, status=status.HTTP_403_FORBIDDEN) - # Get the quiz object to delete - quiz = self.get_object() - # Ensure the current owner is the course owner - if quiz.module.course.owner != request.user: - return Response({"detail": "You can only delete quizzes for your own courses"}, status=status.HTTP_403_FORBIDDEN) - # Delete the quiz - quiz.delete() - return Response({"detail": "Quiz deleted successfully"}, status=status.HTTP_204_NO_CONTENT) - + # class CertificateViewSet(ModelViewSet): # queryset = Certificate.objects.all() @@ -329,4 +291,52 @@ class QuizViewSet(ModelViewSet): # certificate = Certificate.objects.create(course=course, student=student) # serializer = self.get_serializer(certificate) # return Response(serializer.data, status=status.HTTP_201_CREATED) + + + +class PrivateEnrollment(APIView): + def post(self, request): + course_id = request.data.get('course') + student_email = request.data.get('student_email').strip() + + # Check if the course and student exists + course = Course.objects.filter(id=course_id).first() + student = User.objects.filter(email=student_email).first() + + if not course: + raise CustomValidationError("Course not found", code=status.HTTP_404_NOT_FOUND) + + if not student: + raise CustomValidationError("User not found", code=status.HTTP_404_NOT_FOUND) + + if student_email == request.user.email: + raise CustomValidationError("You can't add yourself", code=status.HTTP_400_BAD_REQUEST) + + if Enrollment.objects.filter(student__email=student_email).exists(): + raise CustomValidationError("This user already exists", code=status.HTTP_400_BAD_REQUEST) + + # Check if the course is a paid course + if not course.is_paid: + raise CustomValidationError("Course is not paid", code=status.HTTP_400_BAD_REQUEST) + + # Allow only the course owner to enroll students + if course.owner != request.user: + raise CustomValidationError("You do not have permission to enroll students in this course", + code=status.HTTP_403_FORBIDDEN) + + # Validate the data before saving + enrollment_data = { + 'course': course.id, + 'student': student.id + } + serializer = PrivateEnrollmentSerializer(data=enrollment_data) + if serializer.is_valid(): + serializer.save() + return CustomSuccessResponse(f"Student {student.full_name} has been added", code=status.HTTP_201_CREATED) + + return CustomValidationError(serializer.errors, code=status.HTTP_400_BAD_REQUEST) + + + + \ No newline at end of file