diff --git a/apps/api/permissions.py b/apps/api/permissions.py index 3540a1e..c4d8baa 100644 --- a/apps/api/permissions.py +++ b/apps/api/permissions.py @@ -1,60 +1,71 @@ -from rest_framework import permissions -from rest_framework.authtoken.models import Token -from django.contrib.auth.models import AnonymousUser - - -class SwaggerTokenPermission(permissions.BasePermission): - """ - Custom permission for Swagger that allows access to authenticated users via token - or admin users via session authentication - """ - - def has_permission(self, request, view): - # Check if user is admin (for session-based access) - if request.user and request.user.is_authenticated and request.user.is_staff: - return True - - # Check for token in session (from our custom auth system) - swagger_token = request.session.get('swagger_token') - if swagger_token: - try: - token_obj = Token.objects.get(key=swagger_token) - if token_obj.user.is_active: - return True - except Token.DoesNotExist: - pass - - # Check for Authorization header - auth_header = request.META.get('HTTP_AUTHORIZATION', '') - if auth_header.startswith('Token '): - token = auth_header.split(' ')[1] - try: - token_obj = Token.objects.get(key=token) - if token_obj.user.is_active: - return True - except Token.DoesNotExist: - pass - - return False - - -class IsAdminOrSwaggerToken(permissions.BasePermission): - """ - Permission that allows access to admin users or users with valid swagger token - """ - - def has_permission(self, request, view): - # Allow admin users - if request.user and request.user.is_authenticated and request.user.is_staff: - return True - - # Check swagger token in session - swagger_token = request.session.get('swagger_token') - if swagger_token: - try: - token_obj = Token.objects.get(key=swagger_token) - return token_obj.user.is_active - except Token.DoesNotExist: - pass - - return False \ No newline at end of file +from rest_framework import permissions +from rest_framework.authtoken.models import Token +from django.contrib.auth.models import AnonymousUser + + +class SwaggerTokenPermission(permissions.BasePermission): + """ + Custom permission for Swagger that allows access to authenticated users via token + or admin users via session authentication + """ + + def has_permission(self, request, view): + # Check if user is admin (for session-based access) + if request.user and request.user.is_authenticated and request.user.is_staff: + return True + + # Check for token in session (from our custom auth system) + swagger_token = request.session.get('swagger_token') + if swagger_token: + try: + token_obj = Token.objects.get(key=swagger_token) + if token_obj.user.is_active: + return True + except Token.DoesNotExist: + pass + + # Check for Authorization header + auth_header = request.META.get('HTTP_AUTHORIZATION', '') + if auth_header.startswith('Token '): + token = auth_header.split(' ')[1] + try: + token_obj = Token.objects.get(key=token) + if token_obj.user.is_active: + return True + except Token.DoesNotExist: + pass + + return False + + +class IsAdminOrSwaggerToken(permissions.BasePermission): + """ + Permission that allows access to admin users or users with valid swagger token + """ + + def has_permission(self, request, view): + # Allow admin users + if request.user and request.user.is_authenticated and request.user.is_staff: + return True + + # Check swagger token in session + swagger_token = request.session.get('swagger_token') + if swagger_token: + try: + token_obj = Token.objects.get(key=swagger_token) + return token_obj.user.is_active + except Token.DoesNotExist: + pass + + return False + + +class IsProfessorUser(permissions.BasePermission): + """ + Permission that allows access only to professors or admin users. + """ + + def has_permission(self, request, view): + if not (request.user and request.user.is_authenticated): + return False + return request.user.user_type == 'professor' or request.user.is_staff \ No newline at end of file diff --git a/apps/api/views/__init__.py b/apps/api/views/__init__.py index eea387a..1a5e3e4 100644 --- a/apps/api/views/__init__.py +++ b/apps/api/views/__init__.py @@ -5,6 +5,7 @@ from .api_views import HomeView, CountryView, CommentListAPIView from .documentation import CustomAPIDocumentationView from .swagger_views import CustomSwaggerView, SwaggerTokenAuthView, clear_swagger_auth from .admin_dashboard import AdminDashboardStatsView +from .professor_dashboard import ProfessorDashboardStatsView __all__ = [ 'HomeView', 'CountryView', @@ -13,5 +14,6 @@ __all__ = [ 'CustomSwaggerView', 'SwaggerTokenAuthView', 'clear_swagger_auth', - 'AdminDashboardStatsView' + 'AdminDashboardStatsView', + 'ProfessorDashboardStatsView' ] diff --git a/apps/api/views/professor_dashboard.py b/apps/api/views/professor_dashboard.py new file mode 100644 index 0000000..cad6fe0 --- /dev/null +++ b/apps/api/views/professor_dashboard.py @@ -0,0 +1,139 @@ +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework.permissions import IsAuthenticated +from rest_framework.authentication import TokenAuthentication +from django.utils import timezone +from datetime import timedelta +from django.db.models import Count, Q +from django.db.models.functions import TruncDate + +from apps.account.models import User +from apps.course.models import Course, Participant +from apps.course.models.course import extract_text_from_json +from apps.quiz.models import Quiz, QuizParticipant +from apps.chat.models import ChatMessage, RoomMessage +from apps.api.permissions import IsProfessorUser + + +class ProfessorDashboardStatsView(APIView): + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated, IsProfessorUser] + + def get(self, request): + now = timezone.now() + thirty_days_ago = now - timedelta(days=30) + + # --------------------------------------------------------- + # 1. CARDS & BASIC STATS + # --------------------------------------------------------- + # Total courses taught by this professor + courses_count = Course.objects.filter(professor=request.user).count() + + # Total unique active students enrolled in this professor's courses + students_count = Participant.objects.filter( + course__professor=request.user, + is_active=True + ).values('student').distinct().count() + + # Total quizzes created in this professor's courses + quizzes_count = Quiz.objects.filter(course__professor=request.user).count() + + # Chat rooms count (group rooms for professor's courses + private chats with professor) + chat_rooms_count = RoomMessage.objects.filter( + Q(course__professor=request.user) | + (Q(room_type=RoomMessage.RoomTypeChoices.PRIVATE) & (Q(initiator=request.user) | Q(recipient=request.user))) + ).distinct().count() + + # --------------------------------------------------------- + # 2. CHARTS DATA + # --------------------------------------------------------- + # Chat Volume Trend (30 days) + professor_rooms = RoomMessage.objects.filter( + Q(course__professor=request.user) | + (Q(room_type=RoomMessage.RoomTypeChoices.PRIVATE) & (Q(initiator=request.user) | Q(recipient=request.user))) + ) + chat_volume = ChatMessage.objects.filter( + room__in=professor_rooms, + sent_at__gte=thirty_days_ago + ).annotate( + date=TruncDate('sent_at') + ).values('date').annotate(count=Count('id')).order_by('date') + + chat_chart = [{"date": item['date'].strftime('%Y-%m-%d'), "messages": item['count']} for item in chat_volume] + + # Course Efficacy: Completed vs Enrolled (Bar Chart for Top 5 Courses) + top_courses_qs = Course.objects.filter(professor=request.user).annotate( + participant_count=Count('participants') + ).order_by('-participant_count')[:5] + + course_efficacy = [] + for course in top_courses_qs: + enrolled = course.participant_count + completed = Participant.objects.filter( + course=course, + student__lesson_completions__course_lesson__course=course + ).distinct().count() + title_text = extract_text_from_json(course.title) + course_efficacy.append({ + "title": title_text[:20] + "..." if len(title_text) > 20 else title_text, + "enrolled": enrolled, + "completed": completed + }) + + # Quiz Participation statistics (all quizzes of the professor) + quizzes = Quiz.objects.filter(course__professor=request.user) + quizzes_stats = [] + for quiz in quizzes: + course = quiz.course + if not course: + continue + total_enrolled = Participant.objects.filter(course=course, is_active=True).count() + participated = QuizParticipant.objects.filter(quiz=quiz).values('user').distinct().count() + not_participated = max(0, total_enrolled - participated) + quizzes_stats.append({ + "id": quiz.id, + "title": quiz.title, + "course_title": extract_text_from_json(course.title), + "participated": participated, + "not_participated": not_participated, + }) + + # --------------------------------------------------------- + # 3. LISTS & LEADERBOARDS + # --------------------------------------------------------- + # Top Courses List + top_courses_list = [ + { + "id": c.id, + "title": extract_text_from_json(c.title), + "participants": c.participant_count + } for c in top_courses_qs + ] + + # Leaderboard (Top 5 Performing Students in professor's quizzes) + top_students = QuizParticipant.objects.filter( + quiz__course__professor=request.user + ).select_related('user').order_by('-total_score', 'total_timing')[:5] + + leaderboard = [ + {"id": s.user.id, "name": s.user.fullname, "score": s.total_score, "time": s.total_timing} + for s in top_students + ] + + return Response({ + "cards": { + "courses_count": courses_count, + "students_count": students_count, + "quizzes_count": quizzes_count, + "chat_rooms_count": chat_rooms_count, + }, + "charts": { + "chat_volume": chat_chart, + "course_efficacy": course_efficacy, + "quizzes_stats": quizzes_stats + }, + "lists": { + "top_courses": top_courses_list, + "leaderboard": leaderboard + } + }) diff --git a/config/urls.py b/config/urls.py index 9fbdd54..13a4766 100644 --- a/config/urls.py +++ b/config/urls.py @@ -38,7 +38,7 @@ import requests from filer import views # Import custom API views -from apps.api.views import CustomAPIDocumentationView, CustomSwaggerView, SwaggerTokenAuthView, clear_swagger_auth ,admin_dashboard +from apps.api.views import CustomAPIDocumentationView, CustomSwaggerView, SwaggerTokenAuthView, clear_swagger_auth ,admin_dashboard, ProfessorDashboardStatsView from apps.api.permissions import IsAdminOrSwaggerToken from apps.api.decorators import swagger_auth_required @@ -85,6 +85,7 @@ api_patterns = [ path('calendar/', include('apps.dobodbi_calendar.urls')), path('blog/', include('apps.blog.urls')), path('admin/dashboard-stats/' , admin_dashboard.AdminDashboardStatsView.as_view()), + path('professor/dashboard-stats/' , ProfessorDashboardStatsView.as_view()), path('settings/', include('dynamic_preferences.urls')),