diff --git a/apps/course/doc.py b/apps/course/doc.py index 55e929d..dc3acaa 100644 --- a/apps/course/doc.py +++ b/apps/course/doc.py @@ -112,6 +112,56 @@ def doc_courses_lesson(): """ +def doc_courses_lesson_v2(): + return """ +# Scenario +Nested chapter-based lesson list for a course. + +This endpoint returns active chapters for a course, and inside each chapter returns active lessons in priority order. + +Important behavior: +- Pagination is applied on chapters, not individual lessons. +- `lessons` is a nested array under each chapter. +- `permission` indicates whether the current user can access lesson content. +- `is_complated` indicates whether the authenticated user has completed the lesson. +- `quizs` is either `null` or a list of quizzes related to that lesson. + +Request: +`GET /api/course/v2//lessons/` + +Success example: +```json +{ + "count": 2, + "next": null, + "previous": null, + "results": [ + { + "id": 14, + "title": "فصل 1", + "priority": 1, + "is_active": true, + "lessons": [ + { + "id": 52, + "title": "Greetings", + "priority": 1, + "is_active": true, + "permission": true, + "duration": 16, + "content_type": "youtube_link", + "content_file": null, + "video_link": "https://youtu.be/t5Qo7W5a8qQ?si=WKPxmb4G9F0Va_6e", + "is_complated": false, + "quizs": null + } + ] + } + ] +} +``` +""" + def doc_courses_my_courses(): return """ diff --git a/apps/course/management/commands/ensure_course_rooms.py b/apps/course/management/commands/ensure_course_rooms.py new file mode 100644 index 0000000..d29f4b5 --- /dev/null +++ b/apps/course/management/commands/ensure_course_rooms.py @@ -0,0 +1,63 @@ +from django.core.management.base import BaseCommand +from django.db import transaction +from django.db.models import Exists, OuterRef +from django.utils.translation import gettext_lazy as _ + +from apps.chat.models import RoomMessage +from apps.course.models import Course + + +class Command(BaseCommand): + help = _('Ensure every course has a group chat room') + + def add_arguments(self, parser): + parser.add_argument( + '--dry-run', + action='store_true', + dest='dry_run', + help=_('Show courses missing group rooms without creating them'), + ) + + def handle(self, *args, **options): + dry_run = options['dry_run'] + + group_room_exists = RoomMessage.objects.filter( + course=OuterRef('pk'), + room_type=RoomMessage.RoomTypeChoices.GROUP, + ) + + courses_without_group_room = Course.objects.select_related('professor').annotate( + has_group_room=Exists(group_room_exists) + ).filter( + has_group_room=False + ) + + missing_count = courses_without_group_room.count() + + if missing_count == 0: + self.stdout.write(self.style.SUCCESS(_('All courses already have a group room.'))) + return + + self.stdout.write(self.style.WARNING(_(f'Found {missing_count} course(s) without a group room.'))) + + for course in courses_without_group_room: + self.stdout.write(f'- Course #{course.id}: {course.title}') + + if dry_run: + self.stdout.write(self.style.WARNING(_('Dry run completed. No rooms were created.'))) + return + + created_count = 0 + with transaction.atomic(): + for course in courses_without_group_room: + RoomMessage.objects.create( + name=f"{course.title} - Group", + description=f"Group chat for course: {course.title}", + initiator=course.professor, + course=course, + room_type=RoomMessage.RoomTypeChoices.GROUP, + is_locked=course.is_group_chat_locked, + ) + created_count += 1 + + self.stdout.write(self.style.SUCCESS(_(f'Created {created_count} missing group room(s).'))) diff --git a/apps/course/views/lesson.py b/apps/course/views/lesson.py index 60479e1..0a2a068 100644 --- a/apps/course/views/lesson.py +++ b/apps/course/views/lesson.py @@ -189,8 +189,81 @@ class LessonListV2APIView(ListAPIView): authentication_classes = [TokenAuthentication] @swagger_auto_schema( - operation_description="V2: Get a nested list of chapters and their lessons", + operation_description=doc_courses_lesson_v2(), tags=['Imam-Javad - Course (V2)'], + manual_parameters=[ + openapi.Parameter( + 'slug', openapi.IN_PATH, + description="Course slug", + type=openapi.TYPE_STRING, + required=True + ), + openapi.Parameter( + 'page', openapi.IN_QUERY, + description="Page number for chapter pagination", + type=openapi.TYPE_INTEGER, + required=False + ), + ], + responses={ + 200: openapi.Response( + description="Paginated list of active chapters with nested active lessons", + schema=openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={ + 'count': openapi.Schema(type=openapi.TYPE_INTEGER), + 'next': openapi.Schema(type=openapi.TYPE_STRING, format='uri', nullable=True), + 'previous': openapi.Schema(type=openapi.TYPE_STRING, format='uri', nullable=True), + 'results': openapi.Schema( + type=openapi.TYPE_ARRAY, + items=openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={ + 'id': openapi.Schema(type=openapi.TYPE_INTEGER), + 'title': openapi.Schema(type=openapi.TYPE_STRING), + 'priority': openapi.Schema(type=openapi.TYPE_INTEGER), + 'is_active': openapi.Schema(type=openapi.TYPE_BOOLEAN), + 'lessons': openapi.Schema( + type=openapi.TYPE_ARRAY, + items=openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={ + 'id': openapi.Schema(type=openapi.TYPE_INTEGER), + 'title': openapi.Schema(type=openapi.TYPE_STRING), + 'priority': openapi.Schema(type=openapi.TYPE_INTEGER), + 'is_active': openapi.Schema(type=openapi.TYPE_BOOLEAN), + 'permission': openapi.Schema(type=openapi.TYPE_BOOLEAN), + 'duration': openapi.Schema(type=openapi.TYPE_INTEGER, nullable=True), + 'content_type': openapi.Schema(type=openapi.TYPE_STRING, nullable=True), + 'content_file': openapi.Schema(type=openapi.TYPE_STRING, nullable=True), + 'video_link': openapi.Schema(type=openapi.TYPE_STRING, nullable=True), + 'is_complated': openapi.Schema(type=openapi.TYPE_BOOLEAN), + 'quizs': openapi.Schema( + type=openapi.TYPE_ARRAY, + nullable=True, + items=openapi.Schema( + type=openapi.TYPE_OBJECT, + properties={ + 'id': openapi.Schema(type=openapi.TYPE_INTEGER), + 'title': openapi.Schema(type=openapi.TYPE_STRING), + 'description': openapi.Schema(type=openapi.TYPE_STRING, nullable=True), + 'permission': openapi.Schema(type=openapi.TYPE_BOOLEAN), + 'each_question_timing': openapi.Schema(type=openapi.TYPE_INTEGER, nullable=True), + 'is_complated': openapi.Schema(type=openapi.TYPE_BOOLEAN), + } + ) + ), + } + ) + ), + } + ) + ), + } + ) + ), + 404: openapi.Response(description="Course not found"), + } ) def get(self, request, *args, **kwargs): return super().get(request, *args, **kwargs) @@ -208,4 +281,4 @@ class LessonListV2APIView(ListAPIView): ).filter( course=course, is_active=True - ).order_by('priority', 'id') \ No newline at end of file + ).order_by('priority', 'id')