This commit is contained in:
Ahmed Nagi 2025-01-19 12:08:03 +00:00
parent 1bf4e86d4c
commit e5930c2cbf
8 changed files with 131 additions and 92 deletions

View file

@ -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',

View file

@ -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
self.status_code = code

View file

@ -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):

View file

@ -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',
),
]

View file

@ -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)

View file

@ -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']
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']

View file

@ -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
urlpatterns = [
path('private-enrollment/', PrivateEnrollment.as_view()),
] + router.urls

View file

@ -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)