Some checks failed
Backend (courses app): - courses/serializers.py: Course, Module, Lesson, Page, Enrollment, PageProgress serializers - courses/views.py: CourseListView, CourseDetailView, CourseEnrollView, CourseMyProgressView, MyEnrollmentsView, PageDetailView - courses/urls.py: expose all 6 endpoints under /api/v1/courses/ Frontend: - courses.html + courses.js: course catalog with "Meine Kurse" and "Alle Kurse" sections, enroll button with toast feedback, progress bars - course-player.html + course-player.js: full-screen player with collapsible sidebar nav (module → lesson → page tree), page content renderer (text/video/embed), dwell gate, prev/next navigation, enrollment CTA, completion screen - courses.css: course grid, player layout, sidebar nav, progress bar, dwell gate - Add Kurse nav link to meetings.html, attendance.html - Update index.html hero and workflow grid to include Kurse Co-Authored-By: Paperclip <noreply@paperclip.ing>
186 lines
7.0 KiB
Python
186 lines
7.0 KiB
Python
from django.db.models import Count, Prefetch
|
|
from django.shortcuts import get_object_or_404
|
|
from rest_framework import generics, status
|
|
from rest_framework.permissions import IsAuthenticated
|
|
from rest_framework.response import Response
|
|
from rest_framework.views import APIView
|
|
|
|
from tracking.models import Enrollment, PageProgress
|
|
|
|
from .models import Course, Lesson, Module, Page
|
|
from .serializers import (
|
|
CourseSummarySerializer,
|
|
CourseDetailSerializer,
|
|
EnrollmentSerializer,
|
|
PageDetailSerializer,
|
|
PageProgressSerializer,
|
|
)
|
|
|
|
|
|
def _attach_enrollment(course, user):
|
|
"""Attach _my_enrollment (with _progress_percent) to a Course instance."""
|
|
if not user.is_authenticated:
|
|
course._my_enrollment = None
|
|
return
|
|
try:
|
|
enrollment = Enrollment.objects.get(user=user, course=course)
|
|
except Enrollment.DoesNotExist:
|
|
course._my_enrollment = None
|
|
return
|
|
total = Page.objects.filter(lesson__module__course=course, is_required=True).count()
|
|
completed = PageProgress.objects.filter(enrollment=enrollment, is_complete=True).count()
|
|
enrollment._progress_percent = (completed * 100 // total) if total > 0 else 0
|
|
course._my_enrollment = enrollment
|
|
|
|
|
|
class CourseListView(generics.ListAPIView):
|
|
"""List all published courses with the current user's enrollment status."""
|
|
permission_classes = [IsAuthenticated]
|
|
serializer_class = CourseSummarySerializer
|
|
|
|
def get_queryset(self):
|
|
return Course.objects.filter(is_published=True).annotate(
|
|
module_count=Count("modules", distinct=True)
|
|
)
|
|
|
|
def list(self, request, *args, **kwargs):
|
|
queryset = self.get_queryset()
|
|
user = request.user
|
|
|
|
enrollments_by_course = {
|
|
e.course_id: e
|
|
for e in Enrollment.objects.filter(user=user, course__in=queryset)
|
|
}
|
|
|
|
if enrollments_by_course:
|
|
completed_counts = {
|
|
p["enrollment_id"]: p["completed"]
|
|
for p in PageProgress.objects.filter(
|
|
enrollment__in=enrollments_by_course.values(), is_complete=True
|
|
).values("enrollment_id").annotate(completed=Count("id"))
|
|
}
|
|
page_counts = {
|
|
c.id: Page.objects.filter(
|
|
lesson__module__course=c, is_required=True
|
|
).count()
|
|
for c in queryset
|
|
if c.id in enrollments_by_course
|
|
}
|
|
|
|
for course in queryset:
|
|
enrollment = enrollments_by_course.get(course.id)
|
|
if enrollment:
|
|
total = page_counts.get(course.id, 0) if enrollments_by_course else 0
|
|
completed = completed_counts.get(enrollment.id, 0) if enrollments_by_course else 0
|
|
enrollment._progress_percent = (completed * 100 // total) if total > 0 else 0
|
|
course._my_enrollment = enrollment
|
|
else:
|
|
course._my_enrollment = None
|
|
|
|
return Response(self.get_serializer(queryset, many=True).data)
|
|
|
|
|
|
class MyEnrollmentsView(generics.ListAPIView):
|
|
"""List all courses the current user is enrolled in, with progress."""
|
|
permission_classes = [IsAuthenticated]
|
|
serializer_class = EnrollmentSerializer
|
|
|
|
def get_queryset(self):
|
|
return Enrollment.objects.filter(user=self.request.user).select_related("course")
|
|
|
|
def list(self, request, *args, **kwargs):
|
|
queryset = self.get_queryset()
|
|
completed_counts = {
|
|
p["enrollment_id"]: p["completed"]
|
|
for p in PageProgress.objects.filter(
|
|
enrollment__in=queryset, is_complete=True
|
|
).values("enrollment_id").annotate(completed=Count("id"))
|
|
}
|
|
for enrollment in queryset:
|
|
total = Page.objects.filter(
|
|
lesson__module__course=enrollment.course, is_required=True
|
|
).count()
|
|
completed = completed_counts.get(enrollment.id, 0)
|
|
enrollment._progress_percent = (completed * 100 // total) if total > 0 else 0
|
|
|
|
return Response(self.get_serializer(queryset, many=True).data)
|
|
|
|
|
|
class CourseDetailView(generics.RetrieveAPIView):
|
|
"""Course detail with full module → lesson → page structure."""
|
|
permission_classes = [IsAuthenticated]
|
|
serializer_class = CourseDetailSerializer
|
|
lookup_field = "slug"
|
|
|
|
def get_queryset(self):
|
|
return Course.objects.filter(is_published=True).prefetch_related(
|
|
Prefetch(
|
|
"modules",
|
|
queryset=Module.objects.prefetch_related(
|
|
Prefetch(
|
|
"lessons",
|
|
queryset=Lesson.objects.prefetch_related(
|
|
Prefetch("pages", queryset=Page.objects.order_by("order"))
|
|
).order_by("order"),
|
|
)
|
|
).order_by("order"),
|
|
)
|
|
)
|
|
|
|
def get_object(self):
|
|
obj = super().get_object()
|
|
_attach_enrollment(obj, self.request.user)
|
|
return obj
|
|
|
|
|
|
class CourseEnrollView(APIView):
|
|
"""Enroll the current user in a course."""
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def post(self, request, slug):
|
|
course = get_object_or_404(Course, slug=slug, is_published=True)
|
|
user = request.user
|
|
org_id = user.role_bindings.values_list("org_id", flat=True).first()
|
|
if org_id is None:
|
|
return Response(
|
|
{"detail": "Kein Org-Kontext für diesen Benutzer gefunden."},
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
)
|
|
enrollment, created = Enrollment.objects.get_or_create(
|
|
user=user,
|
|
course=course,
|
|
defaults={"org_id": org_id},
|
|
)
|
|
enrollment._progress_percent = 0
|
|
s = EnrollmentSerializer(enrollment)
|
|
return Response(s.data, status=status.HTTP_201_CREATED if created else status.HTTP_200_OK)
|
|
|
|
|
|
class CourseMyProgressView(APIView):
|
|
"""Page-level progress for the current user in a specific course."""
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def get(self, request, slug):
|
|
course = get_object_or_404(Course, slug=slug, is_published=True)
|
|
try:
|
|
enrollment = Enrollment.objects.get(user=request.user, course=course)
|
|
except Enrollment.DoesNotExist:
|
|
return Response([], status=status.HTTP_200_OK)
|
|
progresses = PageProgress.objects.filter(enrollment=enrollment).select_related("page")
|
|
return Response(PageProgressSerializer(progresses, many=True).data)
|
|
|
|
|
|
class PageDetailView(APIView):
|
|
"""Full page content. Requires enrollment in the parent course."""
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def get(self, request, slug, page_id):
|
|
course = get_object_or_404(Course, slug=slug, is_published=True)
|
|
page = get_object_or_404(Page, id=page_id, lesson__module__course=course)
|
|
if not Enrollment.objects.filter(user=request.user, course=course).exists():
|
|
return Response(
|
|
{"detail": "Nicht eingeschrieben."},
|
|
status=status.HTTP_403_FORBIDDEN,
|
|
)
|
|
return Response(PageDetailSerializer(page).data)
|