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
|
||||
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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
|
|
@ -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
|
||||
126
lms/app/views.py
126
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)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Loading…
Add table
Reference in a new issue