u
This commit is contained in:
parent
1bf4e86d4c
commit
e5930c2cbf
8 changed files with 131 additions and 92 deletions
|
|
@ -390,7 +390,7 @@ REST_AUTH = {
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
SIMPLE_JWT = {
|
SIMPLE_JWT = {
|
||||||
'ACCESS_TOKEN_LIFETIME': timedelta(hours=5),
|
'ACCESS_TOKEN_LIFETIME': timedelta(hours=5),
|
||||||
'REFRESH_TOKEN_LIFETIME': timedelta(days=30),
|
'REFRESH_TOKEN_LIFETIME': timedelta(days=15),
|
||||||
'ROTATE_REFRESH_TOKENS': True,
|
'ROTATE_REFRESH_TOKENS': True,
|
||||||
'BLACKLIST_AFTER_ROTATION': True,
|
'BLACKLIST_AFTER_ROTATION': True,
|
||||||
'ALGORITHM': 'HS256',
|
'ALGORITHM': 'HS256',
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,15 @@
|
||||||
from rest_framework.exceptions import APIException
|
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):
|
class CustomValidationError(APIException):
|
||||||
|
|
|
||||||
|
|
@ -26,11 +26,7 @@ class EnrollmentAdmin(admin.ModelAdmin):
|
||||||
search_fields = ('student__username', 'course__title')
|
search_fields = ('student__username', 'course__title')
|
||||||
list_filter = ('enrolled_at', 'completed')
|
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)
|
@admin.register(Certificate)
|
||||||
class CertificateAdmin(admin.ModelAdmin):
|
class CertificateAdmin(admin.ModelAdmin):
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -49,7 +49,7 @@ class Lesson(models.Model):
|
||||||
module = models.ForeignKey(Module, on_delete=models.CASCADE, related_name='lessons', verbose_name="Module")
|
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")
|
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")
|
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):
|
def str(self):
|
||||||
return self.title
|
return self.title
|
||||||
|
|
@ -57,7 +57,7 @@ class Lesson(models.Model):
|
||||||
# Table for enrollments (Enrollment)
|
# Table for enrollments (Enrollment)
|
||||||
class Enrollment(models.Model):
|
class Enrollment(models.Model):
|
||||||
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
|
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")
|
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")
|
enrolled_at = models.DateTimeField(auto_now_add=True, verbose_name="Enrollment Date")
|
||||||
completed = models.BooleanField(default=False, verbose_name="Completed")
|
completed = models.BooleanField(default=False, verbose_name="Completed")
|
||||||
|
|
@ -65,20 +65,6 @@ class Enrollment(models.Model):
|
||||||
def str(self):
|
def str(self):
|
||||||
return f"{self.student.username} - {self.course.title}"
|
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)
|
# Table for certificates (Certificate)
|
||||||
class Certificate(models.Model):
|
class Certificate(models.Model):
|
||||||
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
|
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,15 @@ from dj_rest_auth.registration.serializers import RegisterSerializer
|
||||||
|
|
||||||
class CourseSerializer(serializers.ModelSerializer):
|
class CourseSerializer(serializers.ModelSerializer):
|
||||||
owner_name = serializers.CharField(source='owner.username', read_only=True)
|
owner_name = serializers.CharField(source='owner.username', read_only=True)
|
||||||
|
students_in_course = serializers.SerializerMethodField()
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Course
|
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']
|
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):
|
class LessonSerializer(serializers.ModelSerializer):
|
||||||
|
|
@ -66,13 +70,14 @@ class EnrollmentSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class QuizSerializer(serializers.ModelSerializer):
|
|
||||||
class Meta:
|
|
||||||
model = Quiz
|
|
||||||
fields = ['id', 'title', 'module', 'questions']
|
|
||||||
|
|
||||||
|
|
||||||
class CertificateSerializer(serializers.ModelSerializer):
|
class CertificateSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Certificate
|
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']
|
||||||
|
|
@ -7,7 +7,9 @@ router.register(r'courses', CourseViewSet, basename='course')
|
||||||
router.register(r'modules', ModuleViewSet, basename='modules')
|
router.register(r'modules', ModuleViewSet, basename='modules')
|
||||||
router.register(r'lessons', LessonViewSet, basename='lessons')
|
router.register(r'lessons', LessonViewSet, basename='lessons')
|
||||||
router.register(r'enrollment', EnrollmentViewSet, basename='enrollment')
|
router.register(r'enrollment', EnrollmentViewSet, basename='enrollment')
|
||||||
router.register(r'quiz', QuizViewSet, basename='quiz')
|
|
||||||
# router.register(r'certificate', CertificateViewSet, basename='certificate')
|
# router.register(r'certificate', CertificateViewSet, basename='certificate')
|
||||||
|
|
||||||
urlpatterns = router.urls
|
urlpatterns = [
|
||||||
|
path('private-enrollment/', PrivateEnrollment.as_view()),
|
||||||
|
|
||||||
|
] + router.urls
|
||||||
122
lms/app/views.py
122
lms/app/views.py
|
|
@ -4,12 +4,17 @@ from .models import *
|
||||||
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
|
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
from rest_framework.views import APIView
|
||||||
from rest_framework.permissions import IsAuthenticated, BasePermission
|
from rest_framework.permissions import IsAuthenticated, BasePermission
|
||||||
from .permissions import IsOwnerOrReadOnly, IsAdmin
|
from .permissions import IsOwnerOrReadOnly, IsAdmin
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.exceptions import PermissionDenied
|
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):
|
class CourseViewSet(ModelViewSet):
|
||||||
"""
|
"""
|
||||||
A ViewSet for viewing and editing Course instances.
|
A ViewSet for viewing and editing Course instances.
|
||||||
|
|
@ -22,6 +27,7 @@ class CourseViewSet(ModelViewSet):
|
||||||
"""
|
"""
|
||||||
Save the post data when creating a new course.
|
Save the post data when creating a new course.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
serializer.save(owner=self.request.user)
|
serializer.save(owner=self.request.user)
|
||||||
|
|
||||||
@action(detail=False, methods=['get'], url_path='my-courses', url_name='my_courses')
|
@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)
|
my_courses = Course.objects.filter(owner=request.user)
|
||||||
|
|
||||||
|
total_students = Enrollment.objects.filter(course__in=my_courses).values('student').distinct().count()
|
||||||
|
|
||||||
# Serialize the data
|
# Serialize the data
|
||||||
serializer = self.get_serializer(my_courses, many=True)
|
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:
|
except Course.DoesNotExist:
|
||||||
return Response({"detail": "Course not found"}, status=status.HTTP_404_NOT_FOUND)
|
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():
|
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)
|
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)
|
return Response({"detail": "You can't enroll in your course"}, status=status.HTTP_404_NOT_FOUND)
|
||||||
|
|
||||||
# Create a new enrollment
|
# Create a new enrollment
|
||||||
|
|
@ -239,60 +255,6 @@ class EnrollmentViewSet(ModelViewSet):
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
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):
|
# class CertificateViewSet(ModelViewSet):
|
||||||
# queryset = Certificate.objects.all()
|
# queryset = Certificate.objects.all()
|
||||||
# serializer_class = CertificateSerializer
|
# serializer_class = CertificateSerializer
|
||||||
|
|
@ -330,3 +292,51 @@ class QuizViewSet(ModelViewSet):
|
||||||
# serializer = self.get_serializer(certificate)
|
# serializer = self.get_serializer(certificate)
|
||||||
# return Response(serializer.data, status=status.HTTP_201_CREATED)
|
# 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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Loading…
Add table
Reference in a new issue