Browse Source

online class new logic for redirect paths

master
Mohsen Taba 1 month ago
parent
commit
5fa44fbc80
  1. 100
      apps/course/tests/test_live_session_api.py
  2. 292
      apps/course/views/course.py
  3. 266
      docs/online_class_entry_endpoints_guide.md

100
apps/course/tests/test_live_session_api.py

@ -5,6 +5,7 @@ from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import override_settings from django.test import override_settings
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from dj_language.models import Language
from rest_framework import status from rest_framework import status
from rest_framework.test import APITestCase from rest_framework.test import APITestCase
@ -15,6 +16,7 @@ from apps.course.models import (
CourseLiveSession, CourseLiveSession,
Participant, Participant,
) )
from apps.course.views.course import get_course_slug_value
@override_settings( @override_settings(
@ -22,9 +24,19 @@ from apps.course.models import (
PLUGNMEET_API_KEY='test-key', PLUGNMEET_API_KEY='test-key',
PLUGNMEET_API_SECRET='test-secret', PLUGNMEET_API_SECRET='test-secret',
MEDIA_ROOT=tempfile.gettempdir(), MEDIA_ROOT=tempfile.gettempdir(),
ONLINE_CLASS_FRONTEND_DOMAIN='http://testserver',
) )
class CourseLiveSessionAPITests(APITestCase): class CourseLiveSessionAPITests(APITestCase):
def setUp(self): def setUp(self):
Language.objects.update_or_create(
id=69,
defaults={
'code': 'en',
'name': 'English',
'status': True,
'countries': [],
},
)
self.professor = ProfessorUser.objects.create( self.professor = ProfessorUser.objects.create(
email='prof@example.com', email='prof@example.com',
fullname='Professor Sample', fullname='Professor Sample',
@ -144,7 +156,12 @@ class CourseLiveSessionAPITests(APITestCase):
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test_validate_metadata_includes_active_room_for_student(self):
@mock.patch('apps.course.views.course.PlugNMeetClient')
def test_validate_metadata_includes_active_room_for_student(self, mock_client_cls):
mock_client = mock_client_cls.return_value
mock_client.is_room_active.return_value = {'status': True, 'msg': 'room is active', 'isActive': True}
mock_client.get_join_token.return_value = {'token': 'joined-student-token'}
session = CourseLiveSession.objects.create( session = CourseLiveSession.objects.create(
course=self.course, course=self.course,
subject='Session Live', subject='Session Live',
@ -154,7 +171,10 @@ class CourseLiveSessionAPITests(APITestCase):
Participant.objects.create(course=self.course, student=self.student) Participant.objects.create(course=self.course, student=self.student)
self.client.force_authenticate(user=self.student) self.client.force_authenticate(user=self.student)
url = reverse('course-online-validate', kwargs={'slug': self.course.slug})
url = reverse(
'course-online-validate',
kwargs={'slug': get_course_slug_value(self.course)},
)
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
@ -164,8 +184,21 @@ class CourseLiveSessionAPITests(APITestCase):
self.assertTrue(metadata['can_join_live_session']) self.assertTrue(metadata['can_join_live_session'])
self.assertEqual(metadata['live_session']['room_id'], session.room_id) self.assertEqual(metadata['live_session']['room_id'], session.room_id)
self.assertIsNotNone(metadata['live_session']['started_at']) self.assertIsNotNone(metadata['live_session']['started_at'])
self.assertEqual(
response.data['redirect_path'],
'http://testserver/?access_token=joined-student-token'
)
self.assertEqual(
metadata['redirect_path'],
'http://testserver/?access_token=joined-student-token'
)
@mock.patch('apps.course.views.course.PlugNMeetClient')
def test_validate_metadata_for_professor_hides_creation_when_online(self, mock_client_cls):
mock_client = mock_client_cls.return_value
mock_client.is_room_active.return_value = {'status': True, 'msg': 'room is active', 'isActive': True}
mock_client.get_join_token.return_value = {'token': 'joined-prof-token'}
def test_validate_metadata_for_professor_hides_creation_when_online(self):
CourseLiveSession.objects.create( CourseLiveSession.objects.create(
course=self.course, course=self.course,
subject='Session Live', subject='Session Live',
@ -174,9 +207,68 @@ class CourseLiveSessionAPITests(APITestCase):
) )
self.client.force_authenticate(user=self.professor) self.client.force_authenticate(user=self.professor)
url = reverse('course-online-validate', kwargs={'slug': self.course.slug})
url = reverse(
'course-online-validate',
kwargs={'slug': get_course_slug_value(self.course)},
)
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
metadata = response.data['metadata'] metadata = response.data['metadata']
self.assertFalse(metadata['can_create_live_session']) self.assertFalse(metadata['can_create_live_session'])
self.assertEqual(
response.data['redirect_path'],
'http://testserver/?access_token=joined-prof-token'
)
@mock.patch('apps.course.views.course.jwt.encode', return_value='teacher-access-token')
@mock.patch('apps.course.views.course.PlugNMeetClient')
def test_validate_returns_direct_access_token_for_professor_when_class_is_not_online(
self,
mock_client_cls,
_mock_jwt_encode,
):
mock_client = mock_client_cls.return_value
mock_client.create_room.return_value = {'status': 'success'}
self.client.force_authenticate(user=self.professor)
url = reverse(
'course-online-validate',
kwargs={'slug': get_course_slug_value(self.course)},
)
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
metadata = response.data['metadata']
self.assertFalse(metadata['is_online'])
self.assertTrue(metadata['can_create_live_session'])
self.assertEqual(
response.data['redirect_path'],
'http://testserver/?access_token=teacher-access-token',
)
self.assertEqual(metadata['redirect_path'], response.data['redirect_path'])
self.assertTrue(
CourseLiveSession.objects.filter(course=self.course, ended_at__isnull=True).exists()
)
@mock.patch(
'apps.course.views.course.CourseOnlineClassTokenValidateAPIView._generate_entry_token',
return_value='waiting-token',
)
def test_validate_returns_waiting_redirect_when_class_is_not_online(self, _mock_generate_entry_token):
Participant.objects.create(course=self.course, student=self.student)
self.client.force_authenticate(user=self.student)
url = reverse(
'course-online-validate',
kwargs={'slug': get_course_slug_value(self.course)},
)
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
metadata = response.data['metadata']
self.assertFalse(metadata['is_online'])
self.assertFalse(metadata['can_join_live_session'])
self.assertEqual(response.data['redirect_path'], 'http://testserver/?token=waiting-token&slug=sample-course')
self.assertIn('&slug=sample-course', response.data['redirect_path'])
self.assertEqual(metadata['redirect_path'], response.data['redirect_path'])

292
apps/course/views/course.py

@ -1,8 +1,9 @@
from django.conf import settings
import logging import logging
from typing import Optional
from django.conf import settings from django.conf import settings
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.core.exceptions import ImproperlyConfigured
from django.db.models import Count, Q, F from django.db.models import Count, Q, F
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.utils import timezone from django.utils import timezone
@ -43,6 +44,8 @@ from apps.course.services.plugnmeet import PlugNMeetClient, PlugNMeetError
from apps.account.serializers import UserProfileSerializer from apps.account.serializers import UserProfileSerializer
from utils.exceptions import AppAPIException from utils.exceptions import AppAPIException
from utils.redis import OnlineClassTokenManager from utils.redis import OnlineClassTokenManager
import time
import jwt
UserModel = get_user_model() UserModel = get_user_model()
@ -61,6 +64,17 @@ def get_course_slug_value(course: Course) -> str:
return str(slug_data or '') return str(slug_data or '')
def get_online_class_frontend_base() -> str:
base = getattr(
settings,
"ONLINE_CLASS_FRONTEND_DOMAIN",
getattr(settings, "SITE_DOMAIN", ""),
).rstrip("/")
if base and not base.startswith(('http://', 'https://')):
base = f"https://{base}"
return base
class CourseCategoryAPIView(ListAPIView): class CourseCategoryAPIView(ListAPIView):
queryset = CourseCategory.objects.all() queryset = CourseCategory.objects.all()
serializer_class = CourseCategorySerializer serializer_class = CourseCategorySerializer
@ -459,7 +473,7 @@ class CourseOnlineClassTokenAPIView(GenericAPIView):
}) })
# ساخت URL ثابت با token و course slug # ساخت URL ثابت با token و course slug
frontend_base = getattr(settings, "ONLINE_CLASS_FRONTEND_DOMAIN", getattr(settings, "SITE_DOMAIN", "")).rstrip("/")
frontend_base = get_online_class_frontend_base()
course_slug = get_course_slug_value(course) course_slug = get_course_slug_value(course)
entry_url = f"{frontend_base}/?token={token}&slug={course_slug}" entry_url = f"{frontend_base}/?token={token}&slug={course_slug}"
@ -544,6 +558,8 @@ class CourseOnlineClassTokenValidateAPIView(GenericAPIView):
{'user_id': user.id if user.is_authenticated else None, 'extra': {}, 'generated_at': timezone.now().isoformat()}, {'user_id': user.id if user.is_authenticated else None, 'extra': {}, 'generated_at': timezone.now().isoformat()},
user=user, user=user,
) )
redirect_path = self._resolve_redirect_path(request, course, user, metadata)
metadata['redirect_path'] = redirect_path
print(f"[Online Validate GET] Success - metadata={metadata}") print(f"[Online Validate GET] Success - metadata={metadata}")
logger.info(f"[Online Validate GET] Success - user_id={user.id if user.is_authenticated else 'anonymous'} course={slug} can_create={metadata.get('can_create_live_session')} can_join={metadata.get('can_join_live_session')}") logger.info(f"[Online Validate GET] Success - user_id={user.id if user.is_authenticated else 'anonymous'} course={slug} can_create={metadata.get('can_create_live_session')} can_join={metadata.get('can_join_live_session')}")
@ -552,6 +568,7 @@ class CourseOnlineClassTokenValidateAPIView(GenericAPIView):
'course': course_data, 'course': course_data,
'user': user_data, 'user': user_data,
'metadata': metadata, 'metadata': metadata,
'redirect_path': redirect_path,
}, status=status.HTTP_200_OK) }, status=status.HTTP_200_OK)
@swagger_auto_schema( @swagger_auto_schema(
@ -624,6 +641,8 @@ class CourseOnlineClassTokenValidateAPIView(GenericAPIView):
course_data = CourseDetailSerializer(course, context={'request': request}).data course_data = CourseDetailSerializer(course, context={'request': request}).data
user_data = UserProfileSerializer(user, context={'request': request}).data user_data = UserProfileSerializer(user, context={'request': request}).data
metadata = self._build_metadata(course, payload, user=user) metadata = self._build_metadata(course, payload, user=user)
redirect_path = self._resolve_redirect_path(request, course, user, metadata, token_value=token_value)
metadata['redirect_path'] = redirect_path
print(f"[Online Validate POST] Success - metadata={metadata}") print(f"[Online Validate POST] Success - metadata={metadata}")
logger.info(f"[Online Validate POST] Success - user_id={user_id} course={course.slug} can_create={metadata.get('can_create_live_session')} can_join={metadata.get('can_join_live_session')}") logger.info(f"[Online Validate POST] Success - user_id={user_id} course={course.slug} can_create={metadata.get('can_create_live_session')} can_join={metadata.get('can_join_live_session')}")
@ -632,6 +651,7 @@ class CourseOnlineClassTokenValidateAPIView(GenericAPIView):
'course': course_data, 'course': course_data,
'user': user_data, 'user': user_data,
'metadata': metadata, 'metadata': metadata,
'redirect_path': redirect_path,
}, status=status.HTTP_200_OK) }, status=status.HTTP_200_OK)
def _build_metadata(self, course: Course, payload: dict, user=None) -> dict: def _build_metadata(self, course: Course, payload: dict, user=None) -> dict:
@ -658,12 +678,278 @@ class CourseOnlineClassTokenValidateAPIView(GenericAPIView):
'scheduled_times': timing_data, 'scheduled_times': timing_data,
'generated_at': payload.get('generated_at'), 'generated_at': payload.get('generated_at'),
'validated_at': timezone.now().isoformat(), 'validated_at': timezone.now().isoformat(),
'redirect_path': payload.get('redirect_path'),
'redirect_path': None,
} }
metadata.update(live_context) metadata.update(live_context)
return metadata return metadata
def _resolve_redirect_path(self, request, course: Course, user, metadata: dict, token_value: str = None) -> Optional[str]:
if not user or not user.is_authenticated:
return None
if metadata.get('is_online') and metadata.get('can_join_live_session'):
redirect_path = self._build_live_redirect_path(request, course, user)
if redirect_path:
return redirect_path
if metadata.get('can_create_live_session'):
redirect_path = self._build_professor_direct_redirect_path(course, user)
if redirect_path:
return redirect_path
if self._user_has_course_access(user, course):
return self._build_waiting_redirect_path(course, user, token_value=token_value)
return None
def _build_waiting_redirect_path(self, course: Course, user, token_value: str = None) -> str:
course_slug = get_course_slug_value(course)
token = token_value or self._generate_entry_token(course, user)
frontend_base = get_online_class_frontend_base()
return f"{frontend_base}/?token={token}&slug={course_slug}"
def _build_professor_direct_redirect_path(self, course: Course, user) -> Optional[str]:
try:
session = self._ensure_professor_live_session(course)
except (ImproperlyConfigured, PlugNMeetError, Exception) as exc:
logger.warning(
"[Online Validate] Failed to create professor live session - course=%s user_id=%s error=%s",
course.slug,
user.id,
str(exc),
)
return None
access_token = self._build_professor_access_token(user, session.room_id)
frontend_base = get_online_class_frontend_base()
return f"{frontend_base}/?access_token={access_token}"
def _ensure_professor_live_session(self, course: Course) -> CourseLiveSession:
session = (
CourseLiveSession.objects.filter(course=course, ended_at__isnull=True)
.order_by('-started_at', '-id')
.first()
)
if session and session.room_id:
return session
subject = self._get_live_session_subject(course)
room_id = f"room-{course.id}-{int(time.time())}"
session = CourseLiveSession.objects.create(
course=course,
room_id=room_id,
subject=subject,
started_at=timezone.now(),
)
client = PlugNMeetClient()
client.create_room({
'room_id': room_id,
'empty_timeout': 90,
'metadata': self._build_live_room_metadata(subject),
})
return session
def _build_professor_access_token(self, user, room_id: str) -> str:
token_payload = {
"room_id": room_id,
"user_info": {
"name": user.get_full_name() or user.email or user.username or f"user-{user.id}",
"user_id": str(user.id),
"is_admin": True,
"is_hidden": False,
},
}
return jwt.encode(
{
"iss": settings.PLUGNMEET_API_KEY,
"exp": int(time.time()) + 3600,
"sub": str(user.id),
**token_payload,
},
settings.PLUGNMEET_API_SECRET,
algorithm="HS256",
)
@staticmethod
def _get_live_session_subject(course: Course) -> str:
title = ""
course_title = course.title
if isinstance(course_title, list):
for tr in course_title:
if isinstance(tr, dict) and tr.get('language_code') == 'en':
val = tr.get('title') or tr.get('text') or tr.get('value') or tr.get('name')
if val:
title = str(val).strip()
break
if not title:
for tr in course_title:
if isinstance(tr, dict) and tr.get('language_code') == 'ru':
val = tr.get('title') or tr.get('text') or tr.get('value') or tr.get('name')
if val:
title = str(val).strip()
break
if not title:
title = extract_text_from_json(course_title)
if not title:
title = "Course"
return f"{title} Live Session"
def _build_live_room_metadata(self, subject: str) -> dict:
return {
'room_title': subject,
'webhook_url': self._get_plugnmeet_webhook_url(),
'default_lock_settings': {
'lock_microphone': True,
'lock_webcam': True,
'lock_screen_sharing': True,
'lock_whiteboard': True,
'lock_shared_notepad': False,
'lock_chat': False,
'lock_chat_send_message': False,
'lock_chat_file_share': False,
'lock_private_chat': False,
},
'room_features': {
'allow_webcams': True,
'mute_on_start': True,
'allow_screen_sharing': True,
'allow_recording': True,
'allow_rtmp': False,
'allow_view_other_webcams': True,
'allow_view_other_participants_list': True,
'admin_only_webcams': False,
'allow_polls': True,
'room_duration': 0,
'chat_features': {
'allow_chat': True,
'allow_file_upload': True,
},
'shared_note_pad_features': {
'allowed_shared_note_pad': True,
},
'whiteboard_features': {
'allowed_whiteboard': True,
},
'breakout_room_features': {
'is_allow': True,
'allowed_number_rooms': 6,
},
'waiting_room_features': {
'is_active': False,
},
'recording_features': {
'is_allow': True,
'is_allow_cloud': True,
'is_allow_local': False,
'enable_auto_cloud_recording': False,
'only_record_admin_webcams': False,
},
},
}
@staticmethod
def _get_plugnmeet_webhook_url() -> str:
base = getattr(settings, 'SITE_DOMAIN', '').rstrip('/')
if not base:
raise ImproperlyConfigured('SITE_DOMAIN must be configured for PlugNMeet webhook delivery.')
if not base.startswith(('http://', 'https://')):
base = f"https://{base}"
return f"{base}/api/courses/plugnmeet/webhook/"
def _build_live_redirect_path(self, request, course: Course, user) -> Optional[str]:
session = (
CourseLiveSession.objects.filter(course=course, ended_at__isnull=True)
.order_by('-started_at', '-id')
.first()
)
if not session or not session.room_id:
return None
is_admin = bool(user.can_manage_course(course))
user_info = {
'user_id': str(user.id),
'name': user.get_full_name() or user.email or user.username or f"user-{user.id}",
'is_admin': is_admin,
}
user_metadata = {}
profile_pic = self._build_profile_url(request, user)
if profile_pic:
user_metadata['profilePic'] = profile_pic
if not is_admin:
user_metadata['lock_settings'] = {
'lock_microphone': True,
'lock_screen_sharing': True,
'lock_webcam': True,
'lock_whiteboard': True,
'lock_shared_notepad': False,
'lock_chat': False,
'lock_chat_send_message': False,
'lock_chat_file_share': False,
'lock_private_chat': False,
}
else:
user_metadata['is_hidden'] = False
if user_metadata:
user_info['user_metadata'] = user_metadata
try:
client = PlugNMeetClient()
response = client.get_join_token({
'room_id': session.room_id,
'user_info': user_info,
})
except (PlugNMeetError, Exception) as exc:
logger.warning(
"[Online Validate] Failed to generate direct access token - course=%s user_id=%s error=%s",
course.slug,
user.id,
str(exc),
)
return None
access_token = response.get('token')
if not access_token:
return None
frontend_base = get_online_class_frontend_base()
return f"{frontend_base}/?access_token={access_token}"
def _generate_entry_token(self, course: Course, user) -> str:
manager = OnlineClassTokenManager()
user_token, _ = Token.objects.get_or_create(user=user)
identifier = f"{user.id}:{user_token.key[:8]}"
token = manager.generate_token(course_id=course.id, user_identifier=identifier)
manager.store_token(token, {
'course_id': course.id,
'user_id': user.id,
'user_token': user_token.key,
'course_slug': get_course_slug_value(course),
'extra': {
'professor_in_class': False,
},
})
return token
@staticmethod
def _user_has_course_access(user, course: Course) -> bool:
if not user or not user.is_authenticated:
return False
if user.is_staff or user.can_manage_course(course):
return True
return Participant.objects.filter(course=course, student=user).exists()
@staticmethod
def _build_profile_url(request, user):
avatar = getattr(user, 'avatar', None)
if avatar and getattr(avatar, 'url', None):
return request.build_absolute_uri(avatar.url)
return None
def _build_live_session_context(self, course: Course) -> dict: def _build_live_session_context(self, course: Course) -> dict:
""" """
Build live session context with real-time PlugNMeet verification. Build live session context with real-time PlugNMeet verification.

266
docs/online_class_entry_endpoints_guide.md

@ -1,237 +1,149 @@
# راهنمای استفاده از endpoint های ورود به کلاس آنلاین
# راهنمای جدید ورود به کلاس آنلاین
این فایل برای تیم‌های `frontend` و `flutter` نوشته شده و فقط توضیح می‌دهد:
این فایل برای تیم‌های `frontend` و `flutter` نوشته شده و توضیح می‌دهد که از این به بعد
برای ورود کاربر به کلاس آنلاین، endpoint اصلی فقط `validate` است.
- هر endpoint چه کاری انجام می‌دهد
- در چه شرایطی باید از آن استفاده شود
- ترتیب درست استفاده از endpoint ها چیست
## هدف تغییر
این راهنما وارد کدنویسی React یا Flutter نمی‌شود و فقط منطق استفاده را توضیح می‌دهد.
قبلاً کلاینت برای ورود به کلاس باید بین چند endpoint تصمیم می‌گرفت:
## هدف کلی
- `online/validate`
- `online/token`
- `online/room/token`
کاربر برای ورود به کلاس آنلاین دو حالت دارد:
این کار باعث می‌شد منطق تصمیم‌گیری، ساخت token و تشخیص مسیر redirect در کلاینت پخش شود.
1. کلاس هنوز توسط استاد شروع نشده است
2. کلاس از قبل شروع شده و room فعال است
الان این تصمیم‌گیری به بکند منتقل شده است.
رفتار درست فرانت باید بر اساس همین دو حالت تعیین شود.
## endpoint اصلی
## اصل مهم
فرانت نباید با درخواست اشتباه باعث ساختن کلاس توسط دانشجو شود.
بنابراین:
- اگر کلاس شروع نشده باشد، کاربر باید وارد `waiting page` شود
- اگر کلاس شروع شده باشد، کاربر باید مستقیم `join token` واقعی بگیرد و وارد کلاس شود
## endpoint ها
### 1. بررسی وضعیت کلاس
#### Endpoint
### بررسی وضعیت کلاس و گرفتن مسیر نهایی ورود
`GET /api/courses/<course-slug>/online/validate/` `GET /api/courses/<course-slug>/online/validate/`
#### کاربرد
این endpoint برای تصمیم‌گیری اولیه فرانت است.
با این endpoint می‌توان فهمید:
- آیا کلاس الان آنلاین است یا نه
- آیا کاربر اجازه ورود به کلاس را دارد یا نه
- آیا کاربر استاد است و می‌تواند کلاس را شروع کند یا نه
#### خروجی مهم
فیلدهای مهم در `metadata`:
- `is_online`
- `can_join_live_session`
- `can_create_live_session`
- `has_finished`
#### زمان استفاده
این endpoint باید قبل از تصمیم نهایی برای ورود به کلاس صدا زده شود.
#### تصمیم‌گیری بر اساس پاسخ
- اگر `is_online = true` و `can_join_live_session = true`
فرانت باید مستقیم به سراغ گرفتن `join token` واقعی برود
- اگر `is_online = false`
فرانت نباید مستقیم سراغ `room/token` برود و باید از flow صفحه انتظار استفاده کند
- اگر `can_create_live_session = true`
کاربر استاد است و فرانت باید مستقیم به سراغ گرفتن `join token` واقعی برود
---
### 2. ساخت لینک ورود موقت برای waiting page
#### Endpoint
`POST /api/courses/<course_id>/online/token/`
## کاری که این endpoint انجام می‌دهد
#### کاربرد
این endpoint حالا همه این کارها را یکجا انجام می‌دهد:
این endpoint برای ورود به waiting flow استفاده می‌شود.
- وضعیت کلاس را بررسی می‌کند
- مشخص می‌کند کاربر اجازه ورود دارد یا نه
- اگر کلاس شروع شده باشد، `access_token` مستقیم کلاس را می‌سازد
- اگر کلاس شروع نشده باشد، `temporary token` صفحه انتظار را می‌سازد
- مسیر نهایی redirect را در `redirect_path` برمی‌گرداند
این endpoint:
## منطق پاسخ
- یک `temporary token` می‌سازد
- یک URL ورود به `conference_client` برمی‌گرداند
### اگر کلاس آنلاین باشد
#### چه زمانی باید استفاده شود
بکند:
فقط وقتی که کلاس هنوز شروع نشده است.
- `join token` واقعی PlugNMeet را می‌سازد
- کاربر را باید به مسیر مستقیم کلاس هدایت کرد
#### چه زمانی نباید استفاده شود
نمونه:
اگر کلاس از قبل شروع شده و room فعال است، نباید این endpoint مسیر اصلی ورود باشد.
`https://meet.example.com/?access_token=<access_token>`
در آن حالت باید مستقیم `join token` واقعی گرفته شود.
### اگر کلاس هنوز آنلاین نشده باشد
#### ورودی
بکند:
- `course_id` در URL
- هدر احراز هویت کاربر
- `temporary token` صفحه انتظار را می‌سازد
- کاربر را باید به flow انتظار/پری‌جوین هدایت کرد
بدنه عملا می‌تواند خالی باشد.
نمونه:
#### خروجی
`https://meet.example.com/?token=<temporary_token>&slug=<course_slug>`
پاسخ شامل این فیلدهاست:
### اگر کلاس هنوز آنلاین نشده باشد و کاربر استاد باشد
- `token`
- `url`
- `expires_in`
بکند:
- room را همان لحظه می‌سازد
- `access_token` مستقیم ورود به کلاس را برمی‌گرداند
- استاد مستقیم وارد خود کلاس می‌شود
### 3. گرفتن join token واقعی برای ورود مستقیم به کلاس
نمونه:
#### Endpoint
`https://meet.example.com/?access_token=<access_token>`
`POST /api/courses/online/room/token/`
## فیلد مهم جدید
#### کاربرد
در پاسخ این endpoint، فیلد `redirect_path` برگردانده می‌شود.
این endpoint `access_token` واقعی PlugNMeet را می‌سازد.
کلاینت باید فقط از همین فیلد برای navigation استفاده کند.
با این token کاربر مستقیم وارد کلاس می‌شود.
## ساختار پاسخ
#### چه زمانی باید استفاده شود
پاسخ همچنان شامل این بخش‌هاست:
وقتی که کلاس از قبل شروع شده و room فعال است.
- `course`
- `user`
- `metadata`
- `redirect_path`
#### چه زمانی نباید استفاده شود
فیلد `metadata.redirect_path` هم با همین مقدار نهایی پر می‌شود تا سازگاری قبلی حفظ شود.
وقتی هنوز کلاس شروع نشده است.
## رفتار پیشنهادی کلاینت
در آن حالت این endpoint مسیر مناسب ورود نیست و باید از waiting flow استفاده شود.
### سناریوی استاندارد
#### ورودی
1. کاربر روی دکمه ورود به کلاس می‌زند
2. کلاینت فقط `online/validate` را صدا می‌زند
3. اگر `redirect_path` مقدار داشت:
- کاربر به همان مسیر هدایت می‌شود
4. اگر `redirect_path` مقدار نداشت:
- یعنی کاربر اجازه ورود ندارد یا هنوز شرایط ورود برای او مهیا نیست
بدنه:
## معنی فیلدهای مهم metadata
`course_slug`
به همراه هدر احراز هویت کاربر
#### خروجی
پاسخ شامل:
- `room_id`
- `token`
#### رفتار درست بعد از دریافت پاسخ
کاربر باید مستقیم به `conference_client` با `access_token` هدایت شود.
مثال:
`https://meet.imamjavad.online/?access_token=<access_token>`
## ترتیب درست استفاده در فرانت
### سناریو 1: کاربر روی دکمه ورود به کلاس می‌زند
ترتیب درست:
1. فرانت وضعیت کلاس را با `online/validate` بررسی کند
2. اگر کلاس فعال بود:
- `online/room/token/`
- هدایت مستقیم با `access_token`
3. اگر کلاس فعال نبود:
- `online/token/`
- هدایت کاربر به waiting page
## منطق تصمیم‌گیری پیشنهادی
- `is_online = true`
مسیر درست: ورود مستقیم به کلاس
- `is_online = false`
مسیر درست: waiting page
## رفتار داخل conference_client
`conference_client` الان دو ورودی را می‌فهمد:
### حالت اول: ورود مستقیم
اگر URL شامل این باشد:
- `is_online`
- `can_join_live_session`
- `can_create_live_session`
- `has_finished`
- `redirect_path`
- `access_token`
## نکته مهم برای صفحه انتظار
کاربر مستقیم وارد flow اصلی کلاس می‌شود.
اگر `redirect_path` از نوع `?token=...&slug=...` باشد:
### حالت دوم: waiting flow
- کاربر وارد `conference_client` می‌شود
- صفحه انتظار یا pre-join نمایش داده می‌شود
- اگر استاد باشد، می‌تواند از همان flow کلاس را شروع کند
- اگر دانشجو باشد، منتظر شروع کلاس می‌ماند
اگر URL شامل این باشد:
اگر کاربر استاد باشد و کلاس هنوز شروع نشده باشد:
- `token`
- `slug`
- `redirect_path` باید مستقیم از نوع `?access_token=...` باشد
- کلاینت نباید استاد را به waiting page بفرستد
کاربر وارد waiting/pre-join flow می‌شود.
## نکته مهم برای ورود مستقیم
در این حالت:
اگر `redirect_path` از نوع `?access_token=...` باشد:
1. temporary token به auth token واقعی تبدیل می‌شود
2. وضعیت کلاس از Django خوانده می‌شود
3. اگر کلاس هنوز شروع نشده باشد، waiting page نمایش داده می‌شود
4. اگر کلاس شروع شده باشد، دکمه ورود به کلاس نمایش داده می‌شود
5. اگر کاربر استاد باشد و کلاس شروع نشده باشد، دکمه شروع کلاس نمایش داده می‌شود
- کاربر مستقیم وارد کلاس می‌شود
- دیگر نیازی نیست کلاینت جداگانه `room/token` را صدا بزند
## آپدیت خودکار وضعیت
## وضعیت endpoint های قبلی
در waiting flow، وضعیت کلاس به صورت polling دوره‌ای از Django بررسی می‌شود.
endpoint های زیر هنوز ممکن است برای سازگاری یا استفاده داخلی موجود باشند:
بنابراین:
- `POST /api/courses/<course_id>/online/token/`
- `POST /api/courses/online/room/token/`
- اگر استاد کلاس را شروع کند، صفحه انتظار به‌روزرسانی می‌شود
- اگر استاد کلاس را ببندد، وضعیت صفحه دوباره به حالت انتظار/پایان بازمی‌گردد
اما برای flow اصلی ورود کاربر، کلاینت جدید نباید روی آن‌ها تصمیم‌گیری انجام دهد.
## جمع‌بندی نهایی ## جمع‌بندی نهایی
### برای فرانت اصلی سایت یا اپ
- ابتدا همیشه `online/validate`
- اگر کلاس فعال بود: `online/room/token/`
- اگر کلاس فعال نبود: `online/token/`
### برای conference_client
برای ورود کاربر به کلاس:
- اگر `access_token` داشت: ورود مستقیم
- اگر `token + slug` داشت: waiting flow
- فقط `online/validate` را صدا بزنید
- فقط `redirect_path` را بخوانید
- بر اساس آن redirect کنید
### قاعده مهم
یعنی منطق انتخاب بین:
`online/token/` برای قبل از شروع کلاس است
- صفحه انتظار
- ورود مستقیم به کلاس
`online/room/token/` برای وقتی است که کلاس واقعا شروع شده است
دیگر مسئولیت کلاینت نیست و در بکند انجام می‌شود.
Loading…
Cancel
Save