3 changed files with 474 additions and 184 deletions
-
100apps/course/tests/test_live_session_api.py
-
292apps/course/views/course.py
-
266docs/online_class_entry_endpoints_guide.md
@ -5,6 +5,7 @@ from django.core.files.uploadedfile import SimpleUploadedFile |
|||
from django.test import override_settings |
|||
from django.urls import reverse |
|||
from django.utils import timezone |
|||
from dj_language.models import Language |
|||
from rest_framework import status |
|||
from rest_framework.test import APITestCase |
|||
|
|||
@ -15,6 +16,7 @@ from apps.course.models import ( |
|||
CourseLiveSession, |
|||
Participant, |
|||
) |
|||
from apps.course.views.course import get_course_slug_value |
|||
|
|||
|
|||
@override_settings( |
|||
@ -22,9 +24,19 @@ from apps.course.models import ( |
|||
PLUGNMEET_API_KEY='test-key', |
|||
PLUGNMEET_API_SECRET='test-secret', |
|||
MEDIA_ROOT=tempfile.gettempdir(), |
|||
ONLINE_CLASS_FRONTEND_DOMAIN='http://testserver', |
|||
) |
|||
class CourseLiveSessionAPITests(APITestCase): |
|||
def setUp(self): |
|||
Language.objects.update_or_create( |
|||
id=69, |
|||
defaults={ |
|||
'code': 'en', |
|||
'name': 'English', |
|||
'status': True, |
|||
'countries': [], |
|||
}, |
|||
) |
|||
self.professor = ProfessorUser.objects.create( |
|||
email='[email protected]', |
|||
fullname='Professor Sample', |
|||
@ -144,7 +156,12 @@ class CourseLiveSessionAPITests(APITestCase): |
|||
|
|||
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( |
|||
course=self.course, |
|||
subject='Session Live', |
|||
@ -154,7 +171,10 @@ class CourseLiveSessionAPITests(APITestCase): |
|||
Participant.objects.create(course=self.course, student=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) |
|||
|
|||
self.assertEqual(response.status_code, status.HTTP_200_OK) |
|||
@ -164,8 +184,21 @@ class CourseLiveSessionAPITests(APITestCase): |
|||
self.assertTrue(metadata['can_join_live_session']) |
|||
self.assertEqual(metadata['live_session']['room_id'], session.room_id) |
|||
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( |
|||
course=self.course, |
|||
subject='Session Live', |
|||
@ -174,9 +207,68 @@ class CourseLiveSessionAPITests(APITestCase): |
|||
) |
|||
|
|||
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) |
|||
|
|||
self.assertEqual(response.status_code, status.HTTP_200_OK) |
|||
metadata = response.data['metadata'] |
|||
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']) |
|||
@ -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/` |
|||
|
|||
#### کاربرد |
|||
|
|||
این 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/` برای وقتی است که کلاس واقعا شروع شده است |
|||
دیگر مسئولیت کلاینت نیست و در بکند انجام میشود. |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue