42 changed files with 2383 additions and 69 deletions
-
26apps/account/admin/professor.py
-
40apps/account/admin/student.py
-
835apps/account/doc.py
-
18apps/account/migrations/0004_user_skill.py
-
1apps/account/models/user.py
-
24apps/account/serializers/user.py
-
35apps/account/views/user.py
-
0apps/chat/__init__.py
-
3apps/chat/admin.py
-
6apps/chat/apps.py
-
35apps/chat/migrations/0001_initial.py
-
0apps/chat/migrations/__init__.py
-
60apps/chat/models.py
-
0apps/chat/tests.py
-
3apps/chat/views.py
-
3apps/course/admin/__init__.py
-
31apps/course/admin/course.py
-
21apps/course/admin/lesson.py
-
33apps/course/admin/participant.py
-
410apps/course/doc.py
-
115apps/course/migrations/0001_initial.py
-
32apps/course/migrations/0002_auto_20241121_2238.py
-
27apps/course/migrations/0003_participant.py
-
42apps/course/migrations/0004_auto_20241122_1913.py
-
3apps/course/models/__init__.py
-
17apps/course/models/course.py
-
33apps/course/models/lesson.py
-
24apps/course/models/participant.py
-
3apps/course/serializers/__init__.py
-
113apps/course/serializers/course.py
-
39apps/course/serializers/lesson.py
-
4apps/course/urls.py
-
4apps/course/views/__init__.py
-
106apps/course/views/course.py
-
28apps/course/views/lesson.py
-
27apps/course/views/participant.py
-
4config/settings/base.py
-
4config/test_auth_middleware.py
-
10templates/admin/auth/user/add_form.html
-
57templates/admin/auth/user/change_password.html
-
124utils/exceptions.py
-
52utils/schema.py
@ -0,0 +1,835 @@ |
|||||
|
def doc_reset(): |
||||
|
return """ |
||||
|
# 🐈 Scenario |
||||
|
🛠️ تنظیم مجدد رمز عبور |
||||
|
|
||||
|
کاربر پس از تأیید کد بازیابی رمز عبور، میتواند رمز عبور جدید خود را تنظیم کند. برای این کار، کاربر باید رمز عبور جدید و تأیید آن را وارد کند. |
||||
|
|
||||
|
بعد از ریکاور و وریفای |
||||
|
به این صفحه برای ریست میآید |
||||
|
که باید با همان توکنی که در وریفای دریافت کرده است را درخواست کند |
||||
|
|
||||
|
(نکته بعد از ریست پسورد توکن ذخیره شده حذف شود و کاربر باید با رمز عبور جدیدی که ست کرده است مجددا لاگین را انجام دهد) |
||||
|
--- |
||||
|
|
||||
|
## 🚀 درخواست API |
||||
|
|
||||
|
### URL: |
||||
|
``` |
||||
|
POST /api/reset-password/ |
||||
|
``` |
||||
|
|
||||
|
### Header: |
||||
|
| کلید | مقدار | |
||||
|
|---------------|---------------------------------| |
||||
|
| Content-Type | application/json | |
||||
|
| Authorization | Bearer <توکن احراز هویت> | |
||||
|
|
||||
|
### Body: |
||||
|
```json |
||||
|
{ |
||||
|
"password": "newstrongpassword", |
||||
|
"password_confirmation": "newstrongpassword" |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 📊 پاسخها |
||||
|
|
||||
|
| کد وضعیت | توضیحات | |
||||
|
|---------------|-----------------------------------------------------------| |
||||
|
| `200` | موفقیتآمیز - رمز عبور با موفقیت تغییر یافت. | |
||||
|
| `400` | درخواست نادرست - مشکلات مربوط به دادههای ارسالی. | |
||||
|
| `401` | عدم احراز هویت - کاربر وارد نشده است یا توکن نامعتبر است. | |
||||
|
| `500` | مشکل موقتی در سرور. | |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 📄 نمونه پاسخ موفقیتآمیز |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"message": "Your password has been changed successfully." |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 📄 نمونه پاسخ خطا |
||||
|
|
||||
|
### رمز عبور و تأیید رمز عبور برابر نیستند: |
||||
|
```json |
||||
|
{ |
||||
|
"status": "error", |
||||
|
"code": "validation_error", |
||||
|
"status_code": 400, |
||||
|
"message": "Passwords do not match." |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### رمز عبور کوتاهتر از 8 کاراکتر است: |
||||
|
```json |
||||
|
{ |
||||
|
"status": "error", |
||||
|
"code": "validation_error", |
||||
|
"status_code": 400, |
||||
|
"message": "Password must be at least 8 characters long." |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### عدم احراز هویت: |
||||
|
```json |
||||
|
{ |
||||
|
"status": "error", |
||||
|
"code": "unauthorized", |
||||
|
"status_code": 401, |
||||
|
"message": "Authentication credentials were not provided or are invalid." |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### مشکل موقتی در سرور: |
||||
|
```json |
||||
|
{ |
||||
|
"status": "error", |
||||
|
"code": "service_unavailable", |
||||
|
"status_code": 500, |
||||
|
"message": "Service temporarily unavailable." |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 💡 نکات مهم: |
||||
|
1. **رمز عبور جدید:** |
||||
|
- باید حداقل 8 کاراکتر باشد و تأیید رمز عبور (`password_confirmation`) باید با رمز عبور اصلی یکسان باشد. |
||||
|
2. **امنیت:** |
||||
|
- کاربر باید توکن احراز هویت معتبر برای تنظیم مجدد رمز عبور ارائه دهد. |
||||
|
3. **توکن احراز هویت:** |
||||
|
- فقط کاربران احراز هویت شده میتوانند رمز عبور خود را تغییر دهند. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 🔧 توضیحات فنی: |
||||
|
|
||||
|
### فرآیند تنظیم مجدد رمز عبور: |
||||
|
1. کاربر باید ابتدا کد بازیابی رمز عبور را تأیید کند. |
||||
|
2. پس از تأیید موفقیتآمیز، کاربر با استفاده از توکن احراز هویت، رمز عبور جدید و تأیید آن را وارد میکند. |
||||
|
3. اگر دادهها معتبر باشند، رمز عبور جدید برای کاربر تنظیم میشود. |
||||
|
4. اگر دادهها نادرست باشند، پیام خطای مناسب به کاربر بازگردانده میشود. |
||||
|
|
||||
|
### ولیدیشنها: |
||||
|
- **رمز عبور:** |
||||
|
- بررسی میشود که رمز عبور حداقل 8 کاراکتر باشد. |
||||
|
- بررسی میشود که رمز عبور و تأیید آن یکسان باشند. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 📄 نمونه درخواست: |
||||
|
|
||||
|
### درخواست کامل: |
||||
|
```json |
||||
|
{ |
||||
|
"password": "mynewpassword", |
||||
|
"password_confirmation": "mynewpassword" |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### پاسخ موفق: |
||||
|
```json |
||||
|
{ |
||||
|
"message": "Your password has been changed successfully." |
||||
|
} |
||||
|
``` |
||||
|
""" |
||||
|
|
||||
|
|
||||
|
|
||||
|
def doc_recover(): |
||||
|
return """ |
||||
|
# 🐈 Scenario |
||||
|
🛠️ بازیابی رمز عبور |
||||
|
|
||||
|
کاربر با وارد کردن ایمیل خود، درخواست بازیابی رمز عبور میدهد. |
||||
|
یک کد تأیید به ایمیل کاربر ارسال میشود تا کاربر بتواند رمز عبور خود را بازیابی کند. |
||||
|
سپس کاربر باید به صفحه وریفای ریدایرکت شود |
||||
|
و بعد از تایید وریفای با توکن داده شده |
||||
|
به صفحه ریست پسورد ریدایرکت میشود تا پسور جدیدی را ست کند |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 🚀 درخواست API |
||||
|
|
||||
|
### URL: |
||||
|
``` |
||||
|
POST /api/recover-password/ |
||||
|
``` |
||||
|
|
||||
|
### Header: |
||||
|
| کلید | مقدار | |
||||
|
|---------------|---------------------------------| |
||||
|
| Content-Type | application/json | |
||||
|
| Authorization | Optional (برای این endpoint نیاز نیست) | |
||||
|
|
||||
|
### Body: |
||||
|
```json |
||||
|
{ |
||||
|
"email": "johndoe@example.com" |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 📊 پاسخها |
||||
|
|
||||
|
| کد وضعیت | توضیحات | |
||||
|
|---------------|-----------------------------------------------------------| |
||||
|
| `202` | موفقیتآمیز - کد بازیابی رمز عبور به ایمیل کاربر ارسال شد. | |
||||
|
| `400` | درخواست نادرست - مشکلات مربوط به دادههای ارسالی. | |
||||
|
| `404` | کاربر یافت نشد. | |
||||
|
| `500` | مشکل موقتی در سرور. | |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 📄 نمونه پاسخ موفقیتآمیز |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"id": 1, |
||||
|
"fullname": "John Doe", |
||||
|
"phone_number": "1234567890", |
||||
|
"email": "johndoe@example.com", |
||||
|
"avatar": null, |
||||
|
"message": "Forgot password code sent" |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 📄 نمونه پاسخ خطا |
||||
|
|
||||
|
### کاربر یافت نشد: |
||||
|
```json |
||||
|
{ |
||||
|
"status": "error", |
||||
|
"code": "not_found", |
||||
|
"status_code": 404, |
||||
|
"message": "User not found." |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### مشکل موقتی در سرور: |
||||
|
```json |
||||
|
{ |
||||
|
"status": "error", |
||||
|
"code": "service_unavailable", |
||||
|
"status_code": 500, |
||||
|
"message": "Service temporarily unavailable." |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 💡 نکات مهم: |
||||
|
1. **کد بازیابی رمز عبور:** |
||||
|
- کد تأیید به ایمیل کاربر ارسال میشود و باید در مرحله بعدی برای بازیابی رمز عبور استفاده شود. |
||||
|
2. **امنیت:** |
||||
|
- کد بازیابی رمز عبور فقط برای مدت محدود اعتبار دارد و بعد از آن منقضی میشود. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 🔧 توضیحات فنی: |
||||
|
|
||||
|
### فرآیند بازیابی رمز عبور: |
||||
|
1. کاربر ایمیل خود را وارد میکند. |
||||
|
2. سیستم بررسی میکند که آیا کاربری با این ایمیل وجود دارد یا خیر. |
||||
|
3. اگر کاربر یافت شود، یک کد تأیید بازیابی رمز عبور به ایمیل کاربر ارسال میشود. |
||||
|
4. کاربر باید این کد را در مرحله بعدی برای تنظیم رمز عبور جدید وارد کند. |
||||
|
|
||||
|
### ولیدیشنها: |
||||
|
- **ایمیل:** |
||||
|
- بررسی میشود که ایمیل وارد شده معتبر باشد. |
||||
|
- اگر کاربری با این ایمیل یافت نشود، پیام خطای مناسب برگردانده میشود. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 📄 نمونه درخواست: |
||||
|
|
||||
|
### درخواست کامل: |
||||
|
```json |
||||
|
{ |
||||
|
"email": "janedoe@example.com" |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### پاسخ موفق: |
||||
|
```json |
||||
|
{ |
||||
|
"id": 2, |
||||
|
"fullname": "Jane Doe", |
||||
|
"phone_number": "0987654321", |
||||
|
"email": "janedoe@example.com", |
||||
|
"avatar": null, |
||||
|
"message": "Forgot password code sent" |
||||
|
} |
||||
|
``` |
||||
|
""" |
||||
|
|
||||
|
|
||||
|
|
||||
|
def doc_login(): |
||||
|
return """ |
||||
|
# 🐈 Scenario |
||||
|
🛠️ ورود به حساب کاربری |
||||
|
|
||||
|
کاربر با وارد کردن ایمیل و رمز عبور خود به سیستم وارد میشود. اگر اعتبارنامهها معتبر باشند، توکن احراز هویت برای دسترسی به دیگر بخشهای سیستم بازگردانده میشود. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 🚀 درخواست API |
||||
|
|
||||
|
### URL: |
||||
|
``` |
||||
|
POST /api/login/ |
||||
|
``` |
||||
|
|
||||
|
### Header: |
||||
|
| کلید | مقدار | |
||||
|
|---------------|---------------------------------| |
||||
|
| Content-Type | application/json | |
||||
|
| Authorization | Optional (برای این endpoint نیاز نیست) | |
||||
|
|
||||
|
### Body: |
||||
|
```json |
||||
|
{ |
||||
|
"email": "johndoe@example.com", |
||||
|
"password": "strongpassword", |
||||
|
"fcm": "fcm_token_optional", |
||||
|
"device_id": "device_id_optional" |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 📊 پاسخها |
||||
|
|
||||
|
| کد وضعیت | توضیحات | |
||||
|
|---------------|-----------------------------------------------------------| |
||||
|
| `201` | موفقیتآمیز - کاربر با موفقیت وارد شد و توکن احراز هویت بازگردانده شد. | |
||||
|
| `400` | درخواست نادرست - مشکلات مربوط به دادههای ارسالی. | |
||||
|
| `404` | کاربر یافت نشد. | |
||||
|
| `500` | مشکل موقتی در سرور. | |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 📄 نمونه پاسخ موفقیتآمیز |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"id": 1, |
||||
|
"fullname": "John Doe", |
||||
|
"email": "johndoe@example.com", |
||||
|
"token": "abc123def456", |
||||
|
"avatar": "https://example.com/avatar.jpg" |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 📄 نمونه پاسخ خطا |
||||
|
|
||||
|
### ورود ناموفق (اطلاعات اشتباه): |
||||
|
```json |
||||
|
{ |
||||
|
"status": "error", |
||||
|
"code": "invalid_credentials", |
||||
|
"status_code": 400, |
||||
|
"message": "Unable to log in with provided credentials." |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### کاربر یافت نشد: |
||||
|
```json |
||||
|
{ |
||||
|
"status": "error", |
||||
|
"code": "not_found", |
||||
|
"status_code": 404, |
||||
|
"message": "User not found." |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### مشکل موقتی در سرور: |
||||
|
```json |
||||
|
{ |
||||
|
"status": "error", |
||||
|
"code": "service_unavailable", |
||||
|
"status_code": 500, |
||||
|
"message": "Service temporarily unavailable." |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 💡 نکات مهم: |
||||
|
1. **رمز عبور:** |
||||
|
- رمز عبور باید صحیح و مطابق با آنچه کاربر هنگام ثبتنام ارائه کرده است، باشد. |
||||
|
2. **توکن احراز هویت:** |
||||
|
- پس از ورود موفقیتآمیز، توکن احراز هویت به کاربر بازگردانده میشود که برای دسترسی به دیگر بخشهای سیستم نیاز است. |
||||
|
3. **اطلاعات دستگاه:** |
||||
|
- `fcm` و `device_id` به عنوان اطلاعات اختیاری برای شناسایی دستگاه ارسال میشوند. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 🔧 توضیحات فنی: |
||||
|
|
||||
|
### فرآیند ورود به حساب کاربری: |
||||
|
1. کاربر ایمیل و رمز عبور خود را وارد میکند. |
||||
|
2. سیستم سعی میکند کاربر را با استفاده از اعتبارنامههای ارائه شده احراز هویت کند. |
||||
|
3. اگر کاربر یافت شود و اعتبارنامهها صحیح باشند، یک توکن احراز هویت ایجاد شده و به کاربر بازگردانده میشود. |
||||
|
4. اگر اعتبارنامه نادرست باشند، پیام خطا برگردانده میشود. |
||||
|
|
||||
|
### ولیدیشنها: |
||||
|
- **ایمیل و رمز عبور:** |
||||
|
- بررسی میشود که ایمیل و رمز عبور وارد شده معتبر باشند. |
||||
|
- اگر کاربر با این ایمیل و رمز عبور یافت نشود، پیام خطای مناسب برگردانده میشود. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 📄 نمونه درخواست: |
||||
|
|
||||
|
### درخواست کامل: |
||||
|
```json |
||||
|
{ |
||||
|
"email": "janedoe@example.com", |
||||
|
"password": "mypassword", |
||||
|
"fcm": "fcm_token_example", |
||||
|
"device_id": "device_id_example" |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### پاسخ موفق: |
||||
|
```json |
||||
|
{ |
||||
|
"id": 2, |
||||
|
"fullname": "Jane Doe", |
||||
|
"email": "janedoe@example.com", |
||||
|
"token": "xyz987uvw654", |
||||
|
"avatar": null |
||||
|
} |
||||
|
``` |
||||
|
""" |
||||
|
|
||||
|
|
||||
|
def doc_verify(): |
||||
|
return """ |
||||
|
# 🐈 Scenario |
||||
|
📅️ تأیید حساب کاربری با کد تأیید |
||||
|
|
||||
|
کاربر پس از ثبتنام، باید با استفاده از کد تأییدی که به ایمیل او ارسال شده است، |
||||
|
حساب کاربری خود را تأیید کند. در این مرحله، کاربر ایمیل و کد تأیید خود را ارسال میکند. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 🚀 درخواست API |
||||
|
|
||||
|
### URL: |
||||
|
``` |
||||
|
POST /api/verify/ |
||||
|
``` |
||||
|
|
||||
|
### Header: |
||||
|
| کلید | مقدار | |
||||
|
|---------------|---------------------------------| |
||||
|
| Content-Type | application/json | |
||||
|
| Authorization | Optional (برای این endpoint نیاز نیست) | |
||||
|
|
||||
|
### Body: |
||||
|
```json |
||||
|
{ |
||||
|
"email": "johndoe@example.com", |
||||
|
"code": "12345" |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 📊 پاسخها |
||||
|
|
||||
|
| کد وضعیت | توضیحات | |
||||
|
|---------------|-----------------------------------------------------------| |
||||
|
| `201` | موفقیتآمیز - کاربر تأیید شد و توکن احراز هویت بازگردانده شد. | |
||||
|
| `400` | درخواست نادرست - مشکلات مربوط به دادههای ارسالی. | |
||||
|
| `404` | کاربر یا کد تأیید یافت نشد. | |
||||
|
| `410` | کد تأیید منقضی شده است. | |
||||
|
| `500` | مشکل موقتی در سرور. | |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 📄 نمونه پاسخ موفقیتآمیز |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"token": "abc123def456", |
||||
|
"user_id": 1, |
||||
|
"phone_number": "1234567890", |
||||
|
"email": "johndoe@example.com", |
||||
|
"fullname": "John Doe", |
||||
|
"avatar": null |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 📄 نمونه پاسخ خطا |
||||
|
|
||||
|
### کد تأیید نادرست: |
||||
|
```json |
||||
|
{ |
||||
|
"status": "error", |
||||
|
"code": "invalid_verification_code", |
||||
|
"status_code": 400, |
||||
|
"message": "The verification code is invalid." |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### کد تأیید منقضی شده است: |
||||
|
```json |
||||
|
{ |
||||
|
"status": "error", |
||||
|
"code": "expired_code", |
||||
|
"status_code": 410, |
||||
|
"message": "The verification code has expired." |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### کاربر یا کد تأیید یافت نشد: |
||||
|
```json |
||||
|
{ |
||||
|
"status": "error", |
||||
|
"code": "not_found", |
||||
|
"status_code": 404, |
||||
|
"message": "Verification data not found or expired." |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### مشکل موقتی در سرور: |
||||
|
```json |
||||
|
{ |
||||
|
"status": "error", |
||||
|
"code": "service_unavailable", |
||||
|
"status_code": 500, |
||||
|
"message": "Service temporarily unavailable." |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 💡 نکات مهم: |
||||
|
1. **کد تأیید:** |
||||
|
- کد تأیید باید دقیقاً با کدی که به ایمیل کاربر ارسال شده مطابقت داشته باشد. |
||||
|
- کد تأیید فقط برای یک مدت محدود اعتبار دارد. |
||||
|
2. **خطاها:** |
||||
|
- اگر کد تأیید نادرست باشد، پیام مناسب بازگردانده میشود. |
||||
|
- اگر کد تأیید منقضی شده باشد، کاربر باید درخواست کد جدید کند. |
||||
|
3. **توکن احراز هویت:** |
||||
|
- پس از تأیید موفقیتآمیز، توکن احراز هویت به کاربر بازگردانده میشود که برای دسترسی به دیگر بخشهای سیستم نیاز است. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### ولیدیشنها: |
||||
|
- **کد تأیید:** |
||||
|
- باید حداکثر 5 کاراکتر باشد. |
||||
|
- اگر کد معتبر نباشد یا منقضی شده باشد، پیام خطای مناسب برگردانده میشود. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 📄 نمونه درخواست: |
||||
|
|
||||
|
### درخواست کامل: |
||||
|
```json |
||||
|
{ |
||||
|
"email": "janedoe@example.com", |
||||
|
"code": "67890" |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### پاسخ موفق: |
||||
|
```json |
||||
|
{ |
||||
|
"token": "xyz987uvw654", |
||||
|
"user_id": 2, |
||||
|
"phone_number": "0987654321", |
||||
|
"email": "janedoe@example.com", |
||||
|
"fullname": "Jane Doe", |
||||
|
"avatar": null |
||||
|
} |
||||
|
``` |
||||
|
""" |
||||
|
|
||||
|
|
||||
|
def doc_register(): |
||||
|
return """ |
||||
|
# 🐈 Scenario |
||||
|
ثبت نام کاربر |
||||
|
|
||||
|
کاربر با وارد کردن اطلاعات مورد نیاز شامل نام کامل، ایمیل، رمز عبور و تأیید رمز عبور درخواست ثبتنام ارسال میکند. پس از ثبت موفق، یک کد تأیید به ایمیل ارسال میشود که برای تکمیل ثبتنام مورد نیاز است. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 🚀 درخواست API |
||||
|
|
||||
|
### URL: |
||||
|
``` |
||||
|
POST /api/register/ |
||||
|
``` |
||||
|
|
||||
|
### Header: |
||||
|
| کلید | مقدار | |
||||
|
|---------------|---------------------------------| |
||||
|
| Content-Type | application/json | |
||||
|
| Authorization | Optional (برای این endpoint نیاز نیست) | |
||||
|
|
||||
|
### Body: |
||||
|
```json |
||||
|
{ |
||||
|
"fullname": "John Doe", |
||||
|
"email": "johndoe@example.com", |
||||
|
"password": "strongpassword", |
||||
|
"password_confirmation": "strongpassword", |
||||
|
"fcm": "fcm_token_optional", |
||||
|
"device_id": "device_id_optional" |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 📊 پاسخها |
||||
|
|
||||
|
| کد وضعیت | توضیحات | |
||||
|
|---------------|-----------------------------------------------------------| |
||||
|
| `202` | موفقیتآمیز - کد تأیید به ایمیل کاربر ارسال شد. | |
||||
|
| `400` | درخواست نادرست - مشکلات مربوط به دادههای ارسالی. | |
||||
|
| `409` | ایمیل قبلاً ثبت شده است. | |
||||
|
| `404` | کاربر یا منبع یافت نشد. | |
||||
|
| `410` | کد تأیید منقضی شده است. | |
||||
|
| `500` | مشکل موقتی در سرور. | |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 📄 نمونه پاسخ موفقیتآمیز |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"user": { |
||||
|
"id": 1, |
||||
|
"fullname": "John Doe", |
||||
|
"email": "johndoe@example.com" |
||||
|
}, |
||||
|
"message": "The otp code was sent to the user's email" |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 📄 نمونه پاسخ خطا |
||||
|
|
||||
|
### ایمیل تکراری: |
||||
|
```json |
||||
|
{ |
||||
|
"status": "error", |
||||
|
"code": "validation_error", |
||||
|
"status_code": 409, |
||||
|
"message": "There were validation errors.", |
||||
|
"errors": [ |
||||
|
{ |
||||
|
"field": "email", |
||||
|
"message": "This email is already registered." |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### رمز عبور و تأیید رمز عبور برابر نیستند: |
||||
|
```json |
||||
|
{ |
||||
|
"status": "error", |
||||
|
"code": "validation_error", |
||||
|
"status_code": 400, |
||||
|
"message": "There were validation errors.", |
||||
|
"errors": [ |
||||
|
{ |
||||
|
"field": "password_confirmation", |
||||
|
"message": "Passwords do not match." |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### رمز عبور کوتاهتر از 8 کاراکتر است: |
||||
|
```json |
||||
|
{ |
||||
|
"status": "error", |
||||
|
"code": "validation_error", |
||||
|
"status_code": 400, |
||||
|
"message": "There were validation errors.", |
||||
|
"errors": [ |
||||
|
{ |
||||
|
"field": "password", |
||||
|
"message": "Password must be at least 8 characters long." |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### درخواست نامعتبر (فیلدهای اجباری): |
||||
|
```json |
||||
|
{ |
||||
|
"status": "error", |
||||
|
"code": "validation_error", |
||||
|
"status_code": 400, |
||||
|
"message": "There were validation errors.", |
||||
|
"errors": [ |
||||
|
{ |
||||
|
"field": "fullname", |
||||
|
"message": "This field is required." |
||||
|
}, |
||||
|
{ |
||||
|
"field": "email", |
||||
|
"message": "This field is required." |
||||
|
}, |
||||
|
{ |
||||
|
"field": "password", |
||||
|
"message": "This field is required." |
||||
|
}, |
||||
|
{ |
||||
|
"field": "password_confirmation", |
||||
|
"message": "This field is required." |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### کاربر یافت نشد: |
||||
|
```json |
||||
|
{ |
||||
|
"status": "error", |
||||
|
"code": "not_found", |
||||
|
"status_code": 404, |
||||
|
"message": "The requested resource was not found." |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### کد تأیید منقضی شده است: |
||||
|
```json |
||||
|
{ |
||||
|
"status": "error", |
||||
|
"code": "expired_code", |
||||
|
"status_code": 410, |
||||
|
"message": "The verification code has expired." |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### مشکل موقتی در سرور: |
||||
|
```json |
||||
|
{ |
||||
|
"status": "error", |
||||
|
"code": "service_unavailable", |
||||
|
"status_code": 500, |
||||
|
"message": "Service temporarily unavailable." |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 💡 نکات مهم: |
||||
|
1. **رمز عبور:** |
||||
|
- باید حداقل 8 کاراکتر باشد. |
||||
|
- رمز عبور و تأیید رمز عبور (`password_confirmation`) باید یکسان باشند. |
||||
|
2. **ایمیل:** |
||||
|
- باید یک آدرس ایمیل معتبر باشد. |
||||
|
- ایمیلهای تکراری مجاز نیستند. |
||||
|
3. **کد OTP:** |
||||
|
- کد تأیید به ایمیل ارسال میشود و برای وریفای کاربر استفاده میشود. |
||||
|
4. **فیلدهای اختیاری:** |
||||
|
- `fcm` و `device_id` در صورت نیاز میتوانند ارسال شوند اما اجباری نیستند. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
### ولیدیشنها: |
||||
|
- **ایمیل:** |
||||
|
- بررسی میشود که در سیستم موجود نباشد. |
||||
|
- اگر موجود باشد، پیام خطای زیر برگردانده میشود: |
||||
|
```json |
||||
|
{ |
||||
|
"status": "error", |
||||
|
"code": "validation_error", |
||||
|
"status_code": 409, |
||||
|
"message": "There were validation errors.", |
||||
|
"errors": [ |
||||
|
{ |
||||
|
"field": "email", |
||||
|
"message": "This email is already registered." |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
``` |
||||
|
- **رمز عبور:** |
||||
|
- بررسی میشود که حداقل 8 کاراکتر باشد: |
||||
|
```json |
||||
|
{ |
||||
|
"status": "error", |
||||
|
"code": "validation_error", |
||||
|
"status_code": 400, |
||||
|
"message": "There were validation errors.", |
||||
|
"errors": [ |
||||
|
{ |
||||
|
"field": "password", |
||||
|
"message": "Password must be at least 8 characters long." |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
``` |
||||
|
- بررسی میشود که با `password_confirmation` یکسان باشد: |
||||
|
```json |
||||
|
{ |
||||
|
"status": "error", |
||||
|
"code": "validation_error", |
||||
|
"status_code": 400, |
||||
|
"message": "There were validation errors.", |
||||
|
"errors": [ |
||||
|
{ |
||||
|
"field": "password_confirmation", |
||||
|
"message": "Passwords do not match." |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 📄 نمونه درخواست: |
||||
|
|
||||
|
### درخواست کامل: |
||||
|
```json |
||||
|
{ |
||||
|
"fullname": "Jane Doe", |
||||
|
"email": "janedoe@example.com", |
||||
|
"password": "securepassword", |
||||
|
"password_confirmation": "securepassword", |
||||
|
"fcm": "fcm_token_example", |
||||
|
"device_id": "device_id_example" |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### پاسخ موفق: |
||||
|
```json |
||||
|
{ |
||||
|
"user": { |
||||
|
"id": 2, |
||||
|
"fullname": "Jane Doe", |
||||
|
"email": "janedoe@example.com" |
||||
|
}, |
||||
|
"message": "The otp code was sent to the user's email" |
||||
|
} |
||||
|
``` |
||||
|
""" |
||||
@ -0,0 +1,18 @@ |
|||||
|
# Generated by Django 3.2.4 on 2024-11-22 10:19 |
||||
|
|
||||
|
from django.db import migrations, models |
||||
|
|
||||
|
|
||||
|
class Migration(migrations.Migration): |
||||
|
|
||||
|
dependencies = [ |
||||
|
('account', '0003_auto_20241120_1741'), |
||||
|
] |
||||
|
|
||||
|
operations = [ |
||||
|
migrations.AddField( |
||||
|
model_name='user', |
||||
|
name='skill', |
||||
|
field=models.CharField(blank=True, max_length=512, null=True), |
||||
|
), |
||||
|
] |
||||
@ -0,0 +1,3 @@ |
|||||
|
from django.contrib import admin |
||||
|
|
||||
|
# Register your models here. |
||||
@ -0,0 +1,6 @@ |
|||||
|
from django.apps import AppConfig |
||||
|
|
||||
|
|
||||
|
class ChatConfig(AppConfig): |
||||
|
default_auto_field = 'django.db.models.BigAutoField' |
||||
|
name = 'apps.chat' |
||||
@ -0,0 +1,35 @@ |
|||||
|
# Generated by Django 3.2.4 on 2024-11-22 19:13 |
||||
|
|
||||
|
from django.conf import settings |
||||
|
from django.db import migrations, models |
||||
|
import django.db.models.deletion |
||||
|
|
||||
|
|
||||
|
class Migration(migrations.Migration): |
||||
|
|
||||
|
initial = True |
||||
|
|
||||
|
dependencies = [ |
||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL), |
||||
|
('course', '0004_auto_20241122_1913'), |
||||
|
] |
||||
|
|
||||
|
operations = [ |
||||
|
migrations.CreateModel( |
||||
|
name='ChatMessage', |
||||
|
fields=[ |
||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
||||
|
('content', models.TextField(verbose_name='Message Content')), |
||||
|
('content_type', models.CharField(choices=[('text', 'Text'), ('file', 'File'), ('audio', 'Audio'), ('image', 'Image')], default='text', max_length=10, verbose_name='Chat Type')), |
||||
|
('content_size', models.PositiveIntegerField(blank=True, null=True, verbose_name='Content Size (bytes)')), |
||||
|
('is_to_professor', models.BooleanField(default=False, verbose_name='Is to Professor')), |
||||
|
('sent_at', models.DateTimeField(auto_now_add=True, verbose_name='Sent At')), |
||||
|
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')), |
||||
|
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='Deleted At')), |
||||
|
('is_deleted', models.BooleanField(default=False, verbose_name='Is deleted')), |
||||
|
('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='course.course', verbose_name='Course')), |
||||
|
('recipient', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='messages_received', to=settings.AUTH_USER_MODEL, verbose_name='Recipient')), |
||||
|
('sender', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages_sent', to=settings.AUTH_USER_MODEL, verbose_name='Sender')), |
||||
|
], |
||||
|
), |
||||
|
] |
||||
@ -0,0 +1,60 @@ |
|||||
|
|
||||
|
from django.db import models |
||||
|
|
||||
|
from apps.account.models import User, ProfessorUser |
||||
|
from apps.course.models import Course |
||||
|
|
||||
|
|
||||
|
class ChatMessage(models.Model): |
||||
|
class ChatTypeChoices(models.TextChoices): |
||||
|
TEXT = 'text', 'Text' |
||||
|
FILE = 'file', 'File' |
||||
|
AUDIO = 'audio', 'Audio' |
||||
|
IMAGE = 'image', 'Image' |
||||
|
|
||||
|
course = models.ForeignKey( |
||||
|
Course, |
||||
|
on_delete=models.CASCADE, |
||||
|
related_name="messages", |
||||
|
verbose_name="Course" |
||||
|
) |
||||
|
sender = models.ForeignKey( |
||||
|
User, |
||||
|
on_delete=models.CASCADE, |
||||
|
related_name="messages_sent", |
||||
|
verbose_name="Sender" |
||||
|
) |
||||
|
recipient = models.ForeignKey( |
||||
|
User, |
||||
|
on_delete=models.CASCADE, |
||||
|
related_name="messages_received", |
||||
|
verbose_name="Recipient", |
||||
|
null=True, |
||||
|
blank=True |
||||
|
|
||||
|
) |
||||
|
content = models.TextField(verbose_name="Message Content") |
||||
|
content_type = models.CharField( |
||||
|
max_length=10, |
||||
|
choices=ChatTypeChoices.choices, |
||||
|
default=ChatTypeChoices.TEXT, |
||||
|
verbose_name="Chat Type" |
||||
|
) |
||||
|
content_size = models.PositiveIntegerField( |
||||
|
verbose_name="Content Size (bytes)", |
||||
|
blank=True, |
||||
|
null=True |
||||
|
) |
||||
|
is_to_professor = models.BooleanField(default=False, verbose_name="Is to Professor") |
||||
|
|
||||
|
sent_at = models.DateTimeField(auto_now_add=True, verbose_name="Sent At") |
||||
|
updated_at = models.DateTimeField(auto_now=True, verbose_name="Updated At") |
||||
|
deleted_at = models.DateTimeField(null=True, blank=True, verbose_name="Deleted At") |
||||
|
is_deleted = models.BooleanField(default=False, verbose_name="Is deleted") |
||||
|
|
||||
|
def __str__(self): |
||||
|
return f"Message from {self.sender} to {self.recipient or 'Group'} in {self.course.title}" |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
@ -0,0 +1,3 @@ |
|||||
|
from django.shortcuts import render |
||||
|
|
||||
|
# Create your views here. |
||||
@ -1,2 +1,3 @@ |
|||||
from .course import * |
from .course import * |
||||
from .lesson import * |
|
||||
|
from .lesson import * |
||||
|
from .participant import * |
||||
@ -0,0 +1,33 @@ |
|||||
|
from django.contrib import admin |
||||
|
|
||||
|
from apps.course.models import Participant |
||||
|
from apps.account.models import StudentUser, User |
||||
|
|
||||
|
|
||||
|
|
||||
|
@admin.register(Participant) |
||||
|
class ParticipantAdmin(admin.ModelAdmin): |
||||
|
list_display = ('student', 'course', 'joined_date') |
||||
|
search_fields = ('student__fullname', 'student__email', 'course__title') |
||||
|
list_filter = ('course', 'joined_date') |
||||
|
ordering = ('-joined_date',) |
||||
|
autocomplete_fields = ['student'] # جستجوی پویا برای فیلد دانشآموز |
||||
|
|
||||
|
def get_readonly_fields(self, request, obj=None): |
||||
|
""" |
||||
|
Make fields readonly if the object already exists. |
||||
|
""" |
||||
|
if obj: |
||||
|
return ['student', 'course', 'joined_date'] |
||||
|
return [] |
||||
|
|
||||
|
|
||||
|
def get_form(self, request, obj=None, **kwargs): |
||||
|
form = super().get_form(request, obj, **kwargs) |
||||
|
if obj is None: # Adding a new participant |
||||
|
# محدود کردن انتخاب دانشآموزان به کاربرانی که از نوع StudentUser هستند |
||||
|
form.base_fields['student'].queryset = StudentUser.objects.filter(user_type=User.UserType.STUDENT) |
||||
|
form.base_fields['student'].widget.can_add_related = True # فعال کردن دکمه اضافه کردن |
||||
|
|
||||
|
return form |
||||
|
|
||||
@ -0,0 +1,410 @@ |
|||||
|
def doc_course_participants(): |
||||
|
return """ |
||||
|
# 🐈 Scenario |
||||
|
🛠️ لیست شرکتکنندگان دوره |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 🚀 درخواست API |
||||
|
|
||||
|
### URL: |
||||
|
``` |
||||
|
GET /api/courses/<slug>/participants/ |
||||
|
``` |
||||
|
|
||||
|
|
||||
|
## 📊 پاسخها |
||||
|
|
||||
|
| کد وضعیت | توضیحات | |
||||
|
|---------------|-----------------------------------------------------------| |
||||
|
| `200` | موفقیتآمیز - لیستی از شرکتکنندگان دوره بازگردانده شد. | |
||||
|
| `404` | دوره یافت نشد. | |
||||
|
| `500` | مشکل موقتی در سرور. | |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
|
||||
|
### پاسخ موفق: |
||||
|
```json |
||||
|
[ |
||||
|
{ |
||||
|
"id": 1, |
||||
|
"fullname": "Ali Rezaei", |
||||
|
"avatar": "https://example.com/avatars/ali_rezaei.jpg", |
||||
|
"email": "ali@example.com", |
||||
|
"phone_number": "+98 912 345 6789", |
||||
|
"info": "Experienced Python Developer", |
||||
|
"skill": "Python, Django, REST API" |
||||
|
} |
||||
|
] |
||||
|
``` |
||||
|
""" |
||||
|
|
||||
|
|
||||
|
def doc_courses_lesson(): |
||||
|
return """ |
||||
|
# 🐈 Scenario |
||||
|
🛠️ لیست درسهای دوره |
||||
|
|
||||
|
این API برای دریافت لیست درسهای یک دوره خاص استفاده میشود. این لیست شامل اطلاعاتی مانند عنوان، اولویت، مدت زمان، نوع محتوا، لینک ویدئو، و وضعیت تکمیل هر درس میباشد. |
||||
|
|
||||
|
(مقدار is_complated مشخص میکند آیا کاربر این درس را گذرانده است |
||||
|
ممکن است درس دارای کوعیز باشد که باید در زیر آ» مانند طرح نمایش داده شود |
||||
|
) |
||||
|
--- |
||||
|
``` |
||||
|
|
||||
|
## 📄 توضیحات مقادیر پاسخ |
||||
|
|
||||
|
| کلید | نوع داده | توضیحات | |
||||
|
|-------------------------|------------|----------------------------------------------------------| |
||||
|
| `id` | Integer | شناسه یکتای درس. | |
||||
|
| `title` | String | عنوان درس. | |
||||
|
| `priority` | Integer | اولویت نمایش درس در لیست دروس. | |
||||
|
| `is_active` | Boolean | آیا درس فعال است یا خیر. | |
||||
|
| `duration` | Integer | مدت زمان درس به دقیقه. | |
||||
|
| `content_type` | String | نوع محتوا (لینک یا فایل). | |
||||
|
| `content_file` | String | فایل مرتبط با درس (در صورت وجود). | |
||||
|
| `video_link` | String | لینک ویدئو برای درس (در صورت آنلاین بودن). | |
||||
|
| `is_complated` | Boolean | آیا کاربر این درس را تکمیل کرده است یا خیر. | |
||||
|
| `quiz` | Object | اطلاعات مرتبط با کوییز درس (در صورت وجود). | |
||||
|
|
||||
|
|
||||
|
### پاسخ موفق: |
||||
|
```json |
||||
|
[ |
||||
|
{ |
||||
|
"id": 1, |
||||
|
"title": "Introduction to Variables", |
||||
|
"duration": 30, |
||||
|
"content_type": "link", |
||||
|
"content_file": null, |
||||
|
"video_link": "https://example.com/videos/variables_intro.mp4", |
||||
|
"is_complated": true, |
||||
|
"quiz": { |
||||
|
"title": "", |
||||
|
"description": "", |
||||
|
"is_complated": "", |
||||
|
} |
||||
|
} |
||||
|
] |
||||
|
``` |
||||
|
""" |
||||
|
|
||||
|
|
||||
|
|
||||
|
def doc_courses_my_courses(): |
||||
|
return """ |
||||
|
# 🐈 Scenario |
||||
|
🛠️ دورههای من |
||||
|
|
||||
|
این API برای دریافت لیست دورههایی است که کاربر در آنها شرکت کرده است. این شامل دورههایی است که به اتمام رسیدهاند یا هنوز در حال تکمیل هستند. |
||||
|
|
||||
|
(برای دوره های تکمیل نشده |
||||
|
?completed=false |
||||
|
دوره های تکمیل شده |
||||
|
?completed=true |
||||
|
) |
||||
|
(برای همه دوره های کاربر بدون هیچ مقداری بفرستید) |
||||
|
(در صفحه هوم هم میتوانید دوره هایی که کاربر شرکت کرده است و هنوز تکمیل نشده است را نمایش دهید) |
||||
|
|
||||
|
|
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 🚀 درخواست API |
||||
|
|
||||
|
### پارامترهای فیلتر |
||||
|
| کلید | نوع داده | توضیحات | |
||||
|
|---------------|-----------|----------------------------------------------------------| |
||||
|
| `completed` | Boolean | اگر `true` باشد، فقط دورههایی که تکمیل شدهاند را بازمیگرداند. | |
||||
|
|
||||
|
|
||||
|
### درخواست کامل: |
||||
|
``` |
||||
|
GET /api/my-courses/?completed=true |
||||
|
``` |
||||
|
""" |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
def doc_course_detail(): |
||||
|
return """ |
||||
|
# 🐈 Scenario |
||||
|
🛠️ جزئیات دوره |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 💡 نکات مهم: |
||||
|
1. **اطلاعات دسترسی (`access`)**: |
||||
|
- این مقدار نشان میدهد که آیا کاربر به این دوره دسترسی دارد یا خیر. |
||||
|
در واقع آیا دانش آموز این دوره است و به درس های این دوره دسترسی دارد |
||||
|
|
||||
|
2. **ویدئو دوره**: |
||||
|
- دورهها میتوانند شامل لینک ویدئو یا فایل ویدئویی باشند که توسط `video_type` مشخص میشود. |
||||
|
3. **تعداد درسهای تکمیلشده**: |
||||
|
- `lessons_complated_count` نشان میدهد که چند درس توسط کاربر تکمیل شده است. |
||||
|
(برای به دست آوردن درصد درس های تکمیل شده دانش اموز تعداد کل درس های دوره را بر اساس درس های تکمیل شده دوره توسط دانش آموز محاسبه کنید) |
||||
|
4. **اطلاعات استاد (`professor`)**: |
||||
|
- اطلاعات استاد شامل نام، تصویر و مهارتها برای آشنایی بیشتر با مربی دوره فراهم شده است. |
||||
|
5. برای دیدن درس ها و فایل ها و گلاسوری api |
||||
|
های جدا در نظر گرفته شده است. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
--- |
||||
|
## 📄 توضیحات مقادیر پاسخ |
||||
|
|
||||
|
| کلید | نوع داده | توضیحات | |
||||
|
|-------------------------|------------|----------------------------------------------------------| |
||||
|
| `id` | Integer | شناسه یکتای دوره. | |
||||
|
| `title` | String | عنوان دوره. | |
||||
|
| `slug` | String | شناسه یکتای دوره که برای URLها استفاده میشود. | |
||||
|
| `category` | Object | اطلاعات دستهبندی دوره شامل نام و شناسه. | |
||||
|
| `access` | Boolean | آیا کاربر به این دوره دسترسی دارد یا خیر. | |
||||
|
| `participant_count` | Integer | تعداد شرکتکنندگان در این دوره. | |
||||
|
| `professor` | Object | اطلاعات استاد شامل نام، تصویر، و مهارتها. | |
||||
|
| `thumbnail` | String | لینک تصویر کوچک دوره. به صورت ابجکت است | |
||||
|
| `video_type` | String | نوع ویدئو (لینک یا فایل). | |
||||
|
| `video_file` | String | لینک فایل ویدئویی در صورت وجود. | |
||||
|
| `video_link` | String | لینک ویدئو در صورت آنلاین بودن محتوا. | |
||||
|
| `is_online` | Boolean | آیا دوره به صورت آنلاین برگزار میشود یا خیر. | |
||||
|
| `level` | String | سطح دوره (beginner, mid, advanced). | |
||||
|
| `duration` | Integer | مدت زمان دوره به ساعت. | |
||||
|
| `lessons_count` | Integer | تعداد درسهای موجود در این دوره. | |
||||
|
| `lessons_complated_count`| Integer | تعداد درسهایی که کاربر تکمیل کرده است. که ممکن است مقدار خالی هم باشد | |
||||
|
| `short_description` | String | توضیح کوتاه در مورد دوره. | |
||||
|
| `status` | String | وضعیت دوره (upcoming, registering, ongoing, finished). | |
||||
|
| `is_free` | Boolean | آیا دوره رایگان است یا خیر. | |
||||
|
| `price` | Decimal | قیمت اصلی دوره در صورت غیر رایگان بودن. | |
||||
|
| `discount_percentage` | Decimal | درصد تخفیف برای دوره. | |
||||
|
| `final_price` | Decimal | قیمت نهایی دوره پس از اعمال تخفیف. | |
||||
|
| `timing` | String | زمانبندی برگزاری دوره (مثلاً ساعتها و روزهای برگزاری).'enum': ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'], | |
||||
|
| `features` | String | ویژگیهای برجسته دوره. | |
||||
|
|
||||
|
--- |
||||
|
## 📄 نمونه پاسخ موفقیتآمیز |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"id": 1, |
||||
|
"title": "Тажвид м", |
||||
|
"slug": "tazhvid-m", |
||||
|
"category": { |
||||
|
"name": "Таджвид Корана", |
||||
|
"slug": "tadzhvid-korana", |
||||
|
"course_count": 25 |
||||
|
}, |
||||
|
"access": true, |
||||
|
"participant_count": 120, |
||||
|
"professor": { |
||||
|
"id": 2, |
||||
|
"fullname": "rezaa", |
||||
|
"avatar": "http://localhost:8000/media/users/avatars/2024/11/test3.jpeg", |
||||
|
"email": "root@admin.com", |
||||
|
"phone_number": "+98 901 203 1023", |
||||
|
"info": "good", |
||||
|
"skill": null |
||||
|
}, |
||||
|
"thumbnail": {}, |
||||
|
"video_type": "video_link", |
||||
|
"video_file": null, |
||||
|
"video_link": "https:222", |
||||
|
"is_online": true, |
||||
|
"level": "beginner", |
||||
|
"duration": 55, |
||||
|
"lessons_count": 2, |
||||
|
"lessons_complated_count": 0, |
||||
|
"short_description": "Таджвид Корана2", |
||||
|
"status": "upcoming", |
||||
|
"is_free": true, |
||||
|
"price": "0.00", |
||||
|
"discount_percentage": 0, |
||||
|
"final_price": "0.00", |
||||
|
"timing": [ |
||||
|
{ |
||||
|
"day": "Monday", |
||||
|
"time": "02:00" |
||||
|
}, |
||||
|
{ |
||||
|
"day": "Friday", |
||||
|
"time": "10:00" |
||||
|
} |
||||
|
], |
||||
|
"features": [ |
||||
|
{ |
||||
|
"title": "good" |
||||
|
}, |
||||
|
{ |
||||
|
"title": "regood" |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
""" |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
def doc_course_list(): |
||||
|
return """ |
||||
|
# 🐈 Scenario |
||||
|
🛠️ لیست دورهها |
||||
|
|
||||
|
این API برای لیست کردن دورهها به همراه اطلاعاتی مانند تعداد شرکتکنندگان، دستهبندی، تصویر کوچک، سطح، مدت زمان و دیگر جزئیات مرتبط استفاده میشود. |
||||
|
|
||||
|
|
||||
|
## 📄 توضیحات مقادیر پاسخ |
||||
|
|
||||
|
| کلید | نوع داده | توضیحات | |
||||
|
|---------------------|------------|----------------------------------------------------------| |
||||
|
| `id` | Integer | شناسه یکتای دوره. | |
||||
|
| `title` | String | عنوان دوره. | |
||||
|
| `slug` | String | شناسه یکتای دوره که برای URLها استفاده میشود. | |
||||
|
| `participant_count` | Integer | تعداد شرکتکنندگانی که در این دوره حضور دارند. | |
||||
|
| `category` | Object | اطلاعات دستهبندی دوره شامل نام و شناسه و اسلاک | |
||||
|
| `thumbnail` | String | لینک تصویر کوچک دوره. | |
||||
|
| `is_online` | Boolean | آیا دوره به صورت آنلاین برگزار میشود یا خیر. | |
||||
|
| `level` | String | سطح دوره (beginner, mid, advanced). | |
||||
|
| `duration` | Integer | مدت زمان دوره به ساعت. | |
||||
|
| `lessons_count` | Integer | تعداد درسهای موجود در این دوره. | |
||||
|
| `short_description` | String | توضیح کوتاه در مورد دوره. | |
||||
|
| `status` | String | وضعیت دوره (upcoming, registering, ongoing, finished). | |
||||
|
| `is_free` | Boolean | آیا دوره رایگان است یا خیر. | |
||||
|
| `price` | Decimal | قیمت اصلی دوره در صورت غیر رایگان بودن. | |
||||
|
| `discount_percentage`| Decimal | درصد تخفیف برای دوره. | |
||||
|
| `final_price` | Decimal | قیمت نهایی دوره پس از اعمال تخفیف. | |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
|
||||
|
### پارامترهای فیلتر و جستجو |
||||
|
| کلید | نوع داده | توضیحات | |
||||
|
|---------------|-----------|----------------------------------------------------------| |
||||
|
| `title` | String | عنوان دوره برای جستجو در لیست دورهها. | |
||||
|
| `category_slug` | String | اسلاگ دستهبندی دوره برای فیلتر کردن دورهها براساس دستهبندی. | |
||||
|
| `status` | String | وضعیت دوره برای فیلتر کردن براساس وضعیت (upcoming, registering, ongoing, finished) | |
||||
|
| `is_free` | Boolean | برای فیلتر کردن دورههای رایگان یا غیررایگان. | |
||||
|
| `is_online` | Boolean | برای فیلتر کردن دورههای آنلاین یا آفلاین. | |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
|
||||
|
|
||||
|
## 📊 پاسخها |
||||
|
|
||||
|
| کد وضعیت | توضیحات | |
||||
|
|---------------|-----------------------------------------------------------| |
||||
|
| `200` | موفقیتآمیز - لیستی از دورهها بازگردانده شد. | |
||||
|
| `500` | مشکل موقتی در سرور. | |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 📄 نمونه پاسخ موفقیتآمیز |
||||
|
|
||||
|
```json |
||||
|
[ |
||||
|
{ |
||||
|
"id": 1, |
||||
|
"title": "Introduction to Python", |
||||
|
"slug": "introduction-to-python", |
||||
|
"participant_count": 120, |
||||
|
"category": { |
||||
|
"name": "Programming", |
||||
|
"slug": "programming" |
||||
|
}, |
||||
|
"thumbnail": {}, |
||||
|
"is_online": true, |
||||
|
"level": "beginner", |
||||
|
"duration": 180, |
||||
|
"lessons_count": 12, |
||||
|
"short_description": "Learn the basics of Python programming.", |
||||
|
"status": "upcoming", |
||||
|
"is_free": false, |
||||
|
"price": 100.0, |
||||
|
"discount_percentage": 20.0, |
||||
|
"final_price": 80.0 |
||||
|
}, |
||||
|
|
||||
|
] |
||||
|
``` |
||||
|
|
||||
|
""" |
||||
|
|
||||
|
|
||||
|
def doc_course_category(): |
||||
|
return """ |
||||
|
# 🐈 Scenario |
||||
|
🛠️ لیست دستهبندیهای دورهها |
||||
|
|
||||
|
این API برای لیست کردن دستهبندیهای دورهها به همراه تعداد دورههای مرتبط با هر دستهبندی استفاده میشود. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 🚀 درخواست API |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 📄 توضیحات مقادیر پاسخ |
||||
|
|
||||
|
| کلید | نوع داده | توضیحات | |
||||
|
|---------------|-----------|----------------------------------------------------------| |
||||
|
| `name` | String | نام دستهبندی دوره. | |
||||
|
| `slug` | String | شناسه یکتای دستهبندی که برای URLها استفاده میشود. | |
||||
|
| `course_count`| Integer | تعداد دورههایی که در این دستهبندی قرار دارند. | |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 📊 پاسخها |
||||
|
|
||||
|
| کد وضعیت | توضیحات | |
||||
|
|---------------|-----------------------------------------------------------| |
||||
|
| `200` | موفقیتآمیز - لیستی از دستهبندیهای دورهها بازگردانده شد. | |
||||
|
| `500` | مشکل موقتی در سرور. | |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 📄 نمونه پاسخ موفقیتآمیز |
||||
|
|
||||
|
```json |
||||
|
[ |
||||
|
{ |
||||
|
"name": "Programming", |
||||
|
"slug": "programming", |
||||
|
"course_count": 12 |
||||
|
}, |
||||
|
{ |
||||
|
"name": "Data Science", |
||||
|
"slug": "data-science", |
||||
|
"course_count": 8 |
||||
|
} |
||||
|
] |
||||
|
``` |
||||
|
|
||||
|
## 📄 نمونه درخواست: |
||||
|
|
||||
|
### درخواست کامل: |
||||
|
``` |
||||
|
GET /api/course-categories/ |
||||
|
``` |
||||
|
|
||||
|
### پاسخ موفق: |
||||
|
```json |
||||
|
[ |
||||
|
{ |
||||
|
"name": "Web Development", |
||||
|
"slug": "web-development", |
||||
|
"course_count": 15 |
||||
|
}, |
||||
|
{ |
||||
|
"name": "Artificial Intelligence", |
||||
|
"slug": "ai", |
||||
|
"course_count": 10 |
||||
|
} |
||||
|
] |
||||
|
``` |
||||
|
""" |
||||
@ -0,0 +1,115 @@ |
|||||
|
# Generated by Django 3.2.4 on 2024-11-21 20:46 |
||||
|
|
||||
|
import apps.course.models.course |
||||
|
import apps.course.models.lesson |
||||
|
from django.conf import settings |
||||
|
from django.db import migrations, models |
||||
|
import django.db.models.deletion |
||||
|
import filer.fields.image |
||||
|
import utils.schema |
||||
|
|
||||
|
|
||||
|
class Migration(migrations.Migration): |
||||
|
|
||||
|
initial = True |
||||
|
|
||||
|
dependencies = [ |
||||
|
('account', '0003_auto_20241120_1741'), |
||||
|
migrations.swappable_dependency(settings.FILER_IMAGE_MODEL), |
||||
|
] |
||||
|
|
||||
|
operations = [ |
||||
|
migrations.CreateModel( |
||||
|
name='Course', |
||||
|
fields=[ |
||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
||||
|
('title', models.CharField(max_length=255, verbose_name='Course Title')), |
||||
|
('slug', models.SlugField(allow_unicode=True, unique=True)), |
||||
|
('video_type', models.CharField(choices=[('video_file', 'Video File'), ('video_link', 'Video Link')], max_length=20, verbose_name='Vedio Type')), |
||||
|
('video_file', models.FileField(blank=True, null=True, upload_to=apps.course.models.course.course_file_upload_to)), |
||||
|
('video_link', models.CharField(blank=True, max_length=500, null=True, verbose_name='Video Link')), |
||||
|
('is_online', models.BooleanField(default=True, verbose_name='Is Online Course')), |
||||
|
('level', models.CharField(choices=[('beginner', 'Beginner'), ('mid', 'Mid Level'), ('advanced', 'Advanced')], max_length=10, verbose_name='Course Level')), |
||||
|
('duration', models.PositiveIntegerField(verbose_name='Duration (in hours)')), |
||||
|
('lessons_count', models.PositiveIntegerField(verbose_name='Number of Lessons')), |
||||
|
('description', models.TextField(verbose_name='Course Description')), |
||||
|
('short_description', models.CharField(blank=True, max_length=500, null=True, verbose_name='Short Description')), |
||||
|
('status', models.CharField(choices=[('inactive', 'Inactive'), ('upcoming', 'Upcoming'), ('registering', 'Registering'), ('ongoing', 'Ongoing'), ('finished', 'Finished')], default='inactive', max_length=15, verbose_name='Course Status')), |
||||
|
('is_free', models.BooleanField(default=True, verbose_name='Is Free')), |
||||
|
('price', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, verbose_name='Course Price')), |
||||
|
('discount_percentage', models.PositiveIntegerField(default=0, verbose_name='Discount Percentage')), |
||||
|
('final_price', models.DecimalField(blank=True, decimal_places=2, default=0.0, help_text='This field is automatically calculated based on the discount percentage.', max_digits=10, verbose_name='Course Final Price')), |
||||
|
('timing', models.JSONField(blank=True, default=utils.schema.default_timing, help_text='The Timing information in JSON format.', null=True, verbose_name='Timing')), |
||||
|
('features', models.JSONField(blank=True, default=dict, null=True, verbose_name='Course features')), |
||||
|
], |
||||
|
options={ |
||||
|
'verbose_name': 'Course', |
||||
|
'verbose_name_plural': 'Courses', |
||||
|
}, |
||||
|
), |
||||
|
migrations.CreateModel( |
||||
|
name='CourseCategory', |
||||
|
fields=[ |
||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
||||
|
('name', models.CharField(max_length=255, verbose_name='Category Name')), |
||||
|
('slug', models.SlugField(max_length=255, unique=True)), |
||||
|
], |
||||
|
), |
||||
|
migrations.CreateModel( |
||||
|
name='Lesson', |
||||
|
fields=[ |
||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
||||
|
('title', models.CharField(max_length=255, verbose_name='Lesson Title')), |
||||
|
('priority', models.IntegerField(blank=True, null=True, verbose_name='Priority')), |
||||
|
('duration', models.PositiveIntegerField(verbose_name='Duration (in minutes)')), |
||||
|
('content_type', models.CharField(choices=[('link', 'Link'), ('file', 'File')], max_length=10, verbose_name='Content Type')), |
||||
|
('content_file', models.FileField(blank=True, null=True, upload_to=apps.course.models.lesson.lesson_file_upload_to)), |
||||
|
('video_link', models.CharField(blank=True, max_length=500, null=True, verbose_name='Video Link')), |
||||
|
('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lessons', to='course.course', verbose_name='Course')), |
||||
|
], |
||||
|
), |
||||
|
migrations.CreateModel( |
||||
|
name='Glossary', |
||||
|
fields=[ |
||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
||||
|
('title', models.CharField(max_length=555, verbose_name='Glossary Title')), |
||||
|
('description', models.TextField(verbose_name='Description')), |
||||
|
('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='glossaries', to='course.course', verbose_name='Course')), |
||||
|
], |
||||
|
options={ |
||||
|
'verbose_name': 'Glossary', |
||||
|
'verbose_name_plural': 'Glossary', |
||||
|
'ordering': ('-id',), |
||||
|
}, |
||||
|
), |
||||
|
migrations.AddField( |
||||
|
model_name='course', |
||||
|
name='category', |
||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='courses', to='course.coursecategory', verbose_name='Category'), |
||||
|
), |
||||
|
migrations.AddField( |
||||
|
model_name='course', |
||||
|
name='professor', |
||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='courses', to='account.professoruser'), |
||||
|
), |
||||
|
migrations.AddField( |
||||
|
model_name='course', |
||||
|
name='thumbnail', |
||||
|
field=filer.fields.image.FilerImageField(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to=settings.FILER_IMAGE_MODEL, verbose_name='thumbnail'), |
||||
|
), |
||||
|
migrations.CreateModel( |
||||
|
name='Attachment', |
||||
|
fields=[ |
||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
||||
|
('title', models.CharField(max_length=255, verbose_name='Attachment Title')), |
||||
|
('file', models.FileField(upload_to=apps.course.models.course.attachment_file_upload_to, verbose_name='Attachment File')), |
||||
|
('file_size', models.PositiveIntegerField(blank=True, null=True, verbose_name='File Size (in bytes)')), |
||||
|
('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='course.course', verbose_name='Course')), |
||||
|
], |
||||
|
options={ |
||||
|
'verbose_name': 'Attachment', |
||||
|
'verbose_name_plural': 'Attachments', |
||||
|
'ordering': ('-id',), |
||||
|
}, |
||||
|
), |
||||
|
] |
||||
@ -0,0 +1,32 @@ |
|||||
|
# Generated by Django 3.2.4 on 2024-11-21 22:38 |
||||
|
|
||||
|
from django.db import migrations, models |
||||
|
import django.db.models.deletion |
||||
|
|
||||
|
|
||||
|
class Migration(migrations.Migration): |
||||
|
|
||||
|
dependencies = [ |
||||
|
('account', '0003_auto_20241120_1741'), |
||||
|
('course', '0001_initial'), |
||||
|
] |
||||
|
|
||||
|
operations = [ |
||||
|
migrations.AddField( |
||||
|
model_name='lesson', |
||||
|
name='is_active', |
||||
|
field=models.BooleanField(default=True, verbose_name='Is Active'), |
||||
|
), |
||||
|
migrations.CreateModel( |
||||
|
name='LessonCompletion', |
||||
|
fields=[ |
||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
||||
|
('completed_at', models.DateTimeField(auto_now_add=True)), |
||||
|
('lesson', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='completions', to='course.lesson')), |
||||
|
('student', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lesson_completions', to='account.studentuser')), |
||||
|
], |
||||
|
options={ |
||||
|
'unique_together': {('student', 'lesson')}, |
||||
|
}, |
||||
|
), |
||||
|
] |
||||
@ -0,0 +1,27 @@ |
|||||
|
# Generated by Django 3.2.4 on 2024-11-21 22:39 |
||||
|
|
||||
|
from django.db import migrations, models |
||||
|
import django.db.models.deletion |
||||
|
|
||||
|
|
||||
|
class Migration(migrations.Migration): |
||||
|
|
||||
|
dependencies = [ |
||||
|
('account', '0003_auto_20241120_1741'), |
||||
|
('course', '0002_auto_20241121_2238'), |
||||
|
] |
||||
|
|
||||
|
operations = [ |
||||
|
migrations.CreateModel( |
||||
|
name='Participant', |
||||
|
fields=[ |
||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
||||
|
('joined_date', models.DateTimeField(auto_now_add=True)), |
||||
|
('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='participants', to='course.course')), |
||||
|
('student', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='participated_courses', to='account.studentuser')), |
||||
|
], |
||||
|
options={ |
||||
|
'unique_together': {('student', 'course')}, |
||||
|
}, |
||||
|
), |
||||
|
] |
||||
@ -0,0 +1,42 @@ |
|||||
|
# Generated by Django 3.2.4 on 2024-11-22 19:13 |
||||
|
|
||||
|
from django.db import migrations, models |
||||
|
import django.utils.timezone |
||||
|
|
||||
|
|
||||
|
class Migration(migrations.Migration): |
||||
|
|
||||
|
dependencies = [ |
||||
|
('course', '0003_participant'), |
||||
|
] |
||||
|
|
||||
|
operations = [ |
||||
|
migrations.AddField( |
||||
|
model_name='course', |
||||
|
name='created_at', |
||||
|
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='Created at'), |
||||
|
preserve_default=False, |
||||
|
), |
||||
|
migrations.AddField( |
||||
|
model_name='course', |
||||
|
name='updated_at', |
||||
|
field=models.DateTimeField(auto_now=True, verbose_name='Updated At'), |
||||
|
), |
||||
|
migrations.AddField( |
||||
|
model_name='lesson', |
||||
|
name='created_at', |
||||
|
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='Created at'), |
||||
|
preserve_default=False, |
||||
|
), |
||||
|
migrations.AddField( |
||||
|
model_name='lesson', |
||||
|
name='updated_at', |
||||
|
field=models.DateTimeField(auto_now=True, verbose_name='Updated At'), |
||||
|
), |
||||
|
migrations.AddField( |
||||
|
model_name='lessoncompletion', |
||||
|
name='created_at', |
||||
|
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='Created at'), |
||||
|
preserve_default=False, |
||||
|
), |
||||
|
] |
||||
@ -1,2 +1,3 @@ |
|||||
from .course import * |
from .course import * |
||||
from .lesson import * |
|
||||
|
from .lesson import * |
||||
|
from .participant import * |
||||
@ -0,0 +1,24 @@ |
|||||
|
|
||||
|
from django.db import models |
||||
|
|
||||
|
|
||||
|
from apps.account.models import StudentUser |
||||
|
from apps.course.models import Course |
||||
|
|
||||
|
|
||||
|
class Participant(models.Model): |
||||
|
student = models.ForeignKey( |
||||
|
StudentUser, |
||||
|
on_delete=models.CASCADE, |
||||
|
related_name='participated_courses' |
||||
|
) |
||||
|
course = models.ForeignKey( |
||||
|
Course, |
||||
|
on_delete=models.CASCADE, |
||||
|
related_name='participants' |
||||
|
) |
||||
|
joined_date = models.DateTimeField(auto_now_add=True) |
||||
|
|
||||
|
class Meta: |
||||
|
unique_together = ('student', 'course') |
||||
|
|
||||
@ -1 +1,2 @@ |
|||||
from .course import * |
|
||||
|
from .course import * |
||||
|
from .lesson import * |
||||
@ -0,0 +1,39 @@ |
|||||
|
|
||||
|
|
||||
|
from rest_framework import serializers |
||||
|
from apps.course.models import Lesson, Participant, LessonCompletion |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
class LessonSerializer(serializers.ModelSerializer): |
||||
|
is_complated = serializers.SerializerMethodField() |
||||
|
quiz = serializers.SerializerMethodField() |
||||
|
|
||||
|
class Meta: |
||||
|
model = Lesson |
||||
|
fields = ['id', 'title', 'priority', 'is_active', 'duration', 'content_type', 'content_file', 'video_link', 'is_complated', 'quiz'] |
||||
|
|
||||
|
def get_is_complated(self, obj): |
||||
|
request = self.context.get('request') |
||||
|
if not request or not request.user.is_authenticated: |
||||
|
return False |
||||
|
user = request.user |
||||
|
is_participant = Participant.objects.filter( |
||||
|
student=user, |
||||
|
course=obj.course |
||||
|
).exists() |
||||
|
|
||||
|
if not is_participant: |
||||
|
return False |
||||
|
|
||||
|
return LessonCompletion.objects.filter( |
||||
|
student=user, |
||||
|
lesson=obj |
||||
|
).exists() |
||||
|
|
||||
|
|
||||
|
def get_quiz(self, obj): |
||||
|
return {} |
||||
@ -1 +1,3 @@ |
|||||
from .course import * |
|
||||
|
from .course import * |
||||
|
from .lesson import * |
||||
|
from .participant import * |
||||
@ -0,0 +1,28 @@ |
|||||
|
from rest_framework.generics import ListAPIView, RetrieveAPIView |
||||
|
|
||||
|
from drf_yasg.utils import swagger_auto_schema |
||||
|
from drf_yasg import openapi |
||||
|
from django.shortcuts import get_object_or_404 |
||||
|
|
||||
|
|
||||
|
from apps.course.serializers import ( |
||||
|
LessonSerializer |
||||
|
) |
||||
|
from apps.course.models import Course, Lesson |
||||
|
from apps.course.doc import * |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
class LessonListView(ListAPIView): |
||||
|
serializer_class = LessonSerializer |
||||
|
queryset = Lesson.objects.filter(is_active=True) |
||||
|
|
||||
|
# doc_courses_lesson |
||||
|
@swagger_auto_schema( |
||||
|
operation_description=doc_courses_lesson(), |
||||
|
) |
||||
|
def get_queryset(self): |
||||
|
course_slug = self.kwargs.get('slug') |
||||
|
course = get_object_or_404(Course, slug=course_slug) |
||||
|
return self.queryset.filter(course=course).order_by('priority','id') |
||||
@ -0,0 +1,27 @@ |
|||||
|
|
||||
|
from rest_framework.generics import ListAPIView |
||||
|
from rest_framework.exceptions import NotFound |
||||
|
from drf_yasg.utils import swagger_auto_schema |
||||
|
from drf_yasg import openapi |
||||
|
|
||||
|
from apps.account.models import StudentUser |
||||
|
from apps.course.models import Participant, Course |
||||
|
from apps.account.serializers import UserProfileSerializer |
||||
|
from apps.course.doc import * |
||||
|
|
||||
|
|
||||
|
|
||||
|
class CourseParticipantsView(ListAPIView): |
||||
|
serializer_class = UserProfileSerializer |
||||
|
|
||||
|
@swagger_auto_schema( |
||||
|
operation_description=doc_course_participants(), |
||||
|
) |
||||
|
def get_queryset(self): |
||||
|
course_slug = self.kwargs.get('slug') |
||||
|
try: |
||||
|
course = Course.objects.get(slug=course_slug) |
||||
|
except Course.DoesNotExist: |
||||
|
raise NotFound("Course not found") |
||||
|
|
||||
|
return StudentUser.objects.filter(participated_courses__course=course) |
||||
@ -0,0 +1,10 @@ |
|||||
|
{% extends "admin/change_form.html" %} |
||||
|
{% load i18n %} |
||||
|
|
||||
|
{% block form_top %} |
||||
|
{% if not is_popup %} |
||||
|
{% comment %} <p>{% translate 'First, enter a username and password. Then, you’ll be able to edit more user options.' %}</p> {% endcomment %} |
||||
|
{% else %} |
||||
|
{% comment %} <p>{% translate "You can add the details" %}</p> {% endcomment %} |
||||
|
{% endif %} |
||||
|
{% endblock %} |
||||
@ -0,0 +1,57 @@ |
|||||
|
{% extends "admin/base_site.html" %} |
||||
|
{% load i18n static %} |
||||
|
{% load admin_urls %} |
||||
|
|
||||
|
{% block extrastyle %}{{ block.super }}<link rel="stylesheet" type="text/css" href="{% static "admin/css/forms.css" %}">{% endblock %} |
||||
|
{% block bodyclass %}{{ block.super }} {{ opts.app_label }}-{{ opts.model_name }} change-form{% endblock %} |
||||
|
{% if not is_popup %} |
||||
|
{% block breadcrumbs %} |
||||
|
<div class="breadcrumbs"> |
||||
|
<a href="{% url 'admin:index' %}">{% translate 'Home' %}</a> |
||||
|
› <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a> |
||||
|
› <a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a> |
||||
|
› <a href="{% url opts|admin_urlname:'change' original.pk|admin_urlquote %}">{{ original|truncatewords:"18" }}</a> |
||||
|
› {% translate 'Change password' %} |
||||
|
</div> |
||||
|
{% endblock %} |
||||
|
{% endif %} |
||||
|
{% block content %}<div id="content-main"> |
||||
|
<form{% if form_url %} action="{{ form_url }}"{% endif %} method="post" id="{{ opts.model_name }}_form">{% csrf_token %}{% block form_top %}{% endblock %} |
||||
|
<input type="text" name="username" value="{{ original.get_username }}" class="hidden"> |
||||
|
<div> |
||||
|
{% if is_popup %}<input type="hidden" name="_popup" value="1">{% endif %} |
||||
|
{% if form.errors %} |
||||
|
<p class="errornote"> |
||||
|
{% if form.errors.items|length == 1 %}{% translate "Please correct the error below." %}{% else %}{% translate "Please correct the errors below." %}{% endif %} |
||||
|
</p> |
||||
|
{% endif %} |
||||
|
|
||||
|
<p>{% blocktranslate with username=original %}Enter a new password for the user <strong>{{ username }}</strong>.{% endblocktranslate %}</p> |
||||
|
|
||||
|
<fieldset class="module aligned"> |
||||
|
|
||||
|
<div class="form-row"> |
||||
|
{{ form.password1.errors }} |
||||
|
{{ form.password1.label_tag }} {{ form.password1 }} |
||||
|
{% if form.password1.help_text %} |
||||
|
<div class="help">{{ form.password1.help_text|safe }}</div> |
||||
|
{% endif %} |
||||
|
</div> |
||||
|
|
||||
|
<div class="form-row"> |
||||
|
{{ form.password2.errors }} |
||||
|
{{ form.password2.label_tag }} {{ form.password2 }} |
||||
|
{% if form.password2.help_text %} |
||||
|
<div class="help">{{ form.password2.help_text|safe }}</div> |
||||
|
{% endif %} |
||||
|
</div> |
||||
|
|
||||
|
</fieldset> |
||||
|
|
||||
|
<div class="submit-row"> |
||||
|
<input type="submit" value="{% translate 'Change password' %}" class="default"> |
||||
|
</div> |
||||
|
|
||||
|
</div> |
||||
|
</form></div> |
||||
|
{% endblock %} |
||||
@ -1,14 +1,52 @@ |
|||||
|
from django.utils.translation import gettext_lazy as _ |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
def default_timing(): |
def default_timing(): |
||||
return { |
return { |
||||
"saturday": "", |
|
||||
"sunday": "", |
|
||||
"monday": "", |
|
||||
"tuesday": "", |
|
||||
"wednesday": "", |
|
||||
"thursday": "", |
|
||||
"friday": "" |
|
||||
|
# "saturday": "", |
||||
|
# "sunday": "", |
||||
|
# "monday": "", |
||||
|
# "tuesday": "", |
||||
|
# "wednesday": "", |
||||
|
# "thursday": "", |
||||
|
# "friday": "" |
||||
|
} |
||||
|
|
||||
|
|
||||
|
def get_weekly_timing_schema(): |
||||
|
return { |
||||
|
'type': "array", |
||||
|
'format': 'table', |
||||
|
'title': ' ', |
||||
|
'items': { |
||||
|
'type': 'object', |
||||
|
'title': str('Weekly Timing'), |
||||
|
'properties': { |
||||
|
'day': { |
||||
|
'type': 'string', |
||||
|
'enum': ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'], |
||||
|
'title': 'Day', |
||||
|
|
||||
|
}, |
||||
|
'time': {'type': 'string', 'format': 'time','title': str('Time')}, |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
def get_course_feature_schema(): |
||||
|
return { |
||||
|
'type': "array", |
||||
|
'format': 'table', |
||||
|
'title': ' ', |
||||
|
'items': { |
||||
|
'type': 'object', |
||||
|
'title': str(_('Course Features')), |
||||
|
'properties': { |
||||
|
'title': {'type': 'string', 'title': str(_('Title'))}, |
||||
|
} |
||||
|
} |
||||
} |
} |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue