Browse Source

init project add models

master
alireza 2 years ago
parent
commit
738aa6aa8a
  1. 26
      apps/account/admin/professor.py
  2. 40
      apps/account/admin/student.py
  3. 835
      apps/account/doc.py
  4. 18
      apps/account/migrations/0004_user_skill.py
  5. 1
      apps/account/models/user.py
  6. 24
      apps/account/serializers/user.py
  7. 35
      apps/account/views/user.py
  8. 0
      apps/chat/__init__.py
  9. 3
      apps/chat/admin.py
  10. 6
      apps/chat/apps.py
  11. 35
      apps/chat/migrations/0001_initial.py
  12. 0
      apps/chat/migrations/__init__.py
  13. 60
      apps/chat/models.py
  14. 0
      apps/chat/tests.py
  15. 3
      apps/chat/views.py
  16. 3
      apps/course/admin/__init__.py
  17. 31
      apps/course/admin/course.py
  18. 21
      apps/course/admin/lesson.py
  19. 33
      apps/course/admin/participant.py
  20. 410
      apps/course/doc.py
  21. 115
      apps/course/migrations/0001_initial.py
  22. 32
      apps/course/migrations/0002_auto_20241121_2238.py
  23. 27
      apps/course/migrations/0003_participant.py
  24. 42
      apps/course/migrations/0004_auto_20241122_1913.py
  25. 3
      apps/course/models/__init__.py
  26. 17
      apps/course/models/course.py
  27. 33
      apps/course/models/lesson.py
  28. 24
      apps/course/models/participant.py
  29. 3
      apps/course/serializers/__init__.py
  30. 113
      apps/course/serializers/course.py
  31. 39
      apps/course/serializers/lesson.py
  32. 4
      apps/course/urls.py
  33. 4
      apps/course/views/__init__.py
  34. 106
      apps/course/views/course.py
  35. 28
      apps/course/views/lesson.py
  36. 27
      apps/course/views/participant.py
  37. 4
      config/settings/base.py
  38. 4
      config/test_auth_middleware.py
  39. 10
      templates/admin/auth/user/add_form.html
  40. 57
      templates/admin/auth/user/change_password.html
  41. 124
      utils/exceptions.py
  42. 52
      utils/schema.py

26
apps/account/admin/professor.py

@ -28,16 +28,25 @@ class ProfessorUserAdmin(UserAdmin, AjaxDatatable):
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': ('email', 'password1', 'password2'),
'fields': ('fullname', 'email', 'phone_number',),
}),
('other', {
'classes': ('wide',),
'fields': ('avatar', 'info', 'skill'),
}),
('Password', {
'classes': ('wide',),
'fields': ('password1', 'password2'),
}),
)
search_fields = (
'email', 'fullname', 'username',
'email', 'fullname',
)
fieldsets = (
(_('Personal info'), {'fields': ('fullname', 'email', 'phone_number', 'avatar',)}),
(_('Personal info'), {'fields': ('fullname', 'email', 'phone_number', 'avatar', 'info', 'skill')}),
(_('Permissions'), {
'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'password'),
'fields': ('is_active', 'groups', 'password'),
}),
(_('Important dates'), {'fields': ('last_login', 'date_joined', 'fcm')}),
)
@ -53,4 +62,11 @@ class ProfessorUserAdmin(UserAdmin, AjaxDatatable):
return obj.phone_number
# admin.site.unregister(TokenProxy)
def get_readonly_fields(self, request, obj=None):
"""
Restrict the ability to modify groups to superusers only.
"""
readonly = list(self.readonly_fields)
if not request.user.is_superuser:
readonly.append('groups')
return readonly

40
apps/account/admin/student.py

@ -13,7 +13,7 @@ from django.urls import path, reverse
from django.shortcuts import render, redirect
from django.contrib import messages
from apps.account.models import StudentUser
from apps.account.models import StudentUser, User
@ -28,7 +28,17 @@ class StudentUserAdmin(UserAdmin, AjaxDatatable):
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': ('email', 'password1', 'password2'),
'fields': ('fullname', 'email', 'phone_number',),
# 'description': 'Please provide the student details including full name, email, and phone number.',
}),
('other', {
'classes': ('wide',),
'fields': ('avatar', 'info'),
}),
('Password', {
'classes': ('wide',),
'fields': ('password1', 'password2'),
}),
)
search_fields = (
@ -37,20 +47,32 @@ class StudentUserAdmin(UserAdmin, AjaxDatatable):
fieldsets = (
(_('Personal info'), {'fields': ('fullname', 'email', 'phone_number', 'avatar',)}),
(_('Permissions'), {
'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'password'),
'fields': ('is_active', 'is_staff', 'is_superuser', 'groups',),
}),
(_('Important dates'), {'fields': ('last_login', 'date_joined', 'fcm')}),
)
@admin.display(description='Phone Number')
def _phone_number(self, obj):
return obj.phone_number
def get_queryset(self, request):
# محدود کردن نمایش فقط دانش‌آموزان
qs = super().get_queryset(request)
return qs.filter(user_type=User.UserType.STUDENT)
def save_model(self, request, obj, form, change):
if not change:
obj.set_password(form.cleaned_data['password1'])
obj.user_type = User.UserType.PROFESSOR
obj.user_type = User.UserType.STUDENT
super().save_model(request, obj, form, change)
@admin.display(description='Phone Number')
def _phone_number(self, obj):
return obj.phone_number
# admin.site.unregister(TokenProxy)
def has_add_permission(self, request):
if '_popup' in request.GET and request.GET['_popup'] == '1': # بررسی وجود _popup در پارامترهای GET
return True
return False
def has_delete_permission(self, request, obj=None):
return False

835
apps/account/doc.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"
}
```
"""

18
apps/account/migrations/0004_user_skill.py

@ -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),
),
]

1
apps/account/models/user.py

@ -49,6 +49,7 @@ class User(AbstractUser):
is_active = models.BooleanField(default=True, verbose_name="Active", help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.")
deleted_at = models.DateTimeField(null=True, blank=True)
info = models.TextField(verbose_name="Info", null=True, blank=True)
skill = models.CharField(max_length=512, null=True, blank=True)
objects = UserManager()

24
apps/account/serializers/user.py

@ -15,7 +15,7 @@ class UserProfileSerializer(serializers.ModelSerializer):
fullname = serializers.CharField(required=False)
class Meta:
model = User
fields = ['id', 'fullname', 'avatar', 'email', 'phone_number', 'password', 'info']
fields = ['id', 'fullname', 'avatar', 'email', 'phone_number', 'password', 'info', 'skill']
read_only_fields = ['email', 'info']
# def validate_email(self, value):
@ -54,24 +54,26 @@ class UserRegisterSerializer(serializers.ModelSerializer):
def validate_email(self, value):
if User.objects.filter(email=value).exists():
raise serializers.ValidationError("This email is already registered.")
raise serializers.ValidationError({"email": "This email is already registered."})
return value
def validate(self, data):
password = data.get('password')
password_confirmation = data.get('password_confirmation')
errors = {}
if password and password_confirmation and password != password_confirmation:
raise serializers.ValidationError("Passwords do not match.")
raise serializers.ValidationError({"password_confirmation": "Passwords do not match."})
if len(password) < 8:
raise serializers.ValidationError("Password must be at least 8 characters long.")
raise serializers.ValidationError({"password": "Password must be at least 8 characters long."})
# If there are any errors, raise ValidationError
data.pop('password_confirmation', None)
data.pop('fcm', None)
data.pop('device_id', None)
return data
class UserVerifySerializer(serializers.ModelSerializer):
code = serializers.CharField(max_length=5, validators=[validate_type_code])
@ -136,17 +138,19 @@ class UserResetPasswordSerializer(serializers.ModelSerializer):
'password_confirmation': {'required': True,},
}
def validate(self, data):
password = data.get('password')
password_confirmation = data.get('password_confirmation')
errors = {}
if password and password_confirmation and password != password_confirmation:
raise serializers.ValidationError("Passwords do not match.")
raise serializers.ValidationError({"password_confirmation": "Passwords do not match."})
if len(password) < 8:
raise serializers.ValidationError("Password must be at least 8 characters long.")
raise serializers.ValidationError({"password": "Password must be at least 8 characters long."})
# If there are any errors, raise ValidationError
data.pop('password_confirmation', None)
return data

35
apps/account/views/user.py

@ -23,9 +23,11 @@ from utils.exceptions import InvaliedCodeVrify, ExpiredCodeException, ServiceUna
from apps.account.models import User
from apps.account.serializers import UserRegisterSerializer, UserProfileSerializer, UserVerifySerializer, UserLoginSerializer, UserRecoverPasswordSerializer, UserResetPasswordSerializer
from utils.redis import RedisManager
from utils.exceptions import AppAPIException
from utils import send_email, is_valid_email
from config.settings import base as settings
from apps.account.permissions import IsActiveUser
from apps.account.doc import *
logger = logging.getLogger(__name__)
@ -39,8 +41,8 @@ class UserRegisterView(CreateAPIView):
@swagger_auto_schema(
operation_description=doc_register(),
request_body=UserRegisterSerializer,
responses={201: 'User registered successfully', 400: 'Bad request'}
)
def post(self, request):
serializer = self.get_serializer(data=request.data)
@ -68,6 +70,13 @@ class UserVerifyView(CreateAPIView):
permission_classes = [AllowAny]
serializer_class = UserVerifySerializer
@swagger_auto_schema(
operation_description=doc_verify(),
request_body=UserVerifySerializer,
)
def post(self, request, *args, **kwargs):
return super().post(request, *args, **kwargs)
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
@ -77,7 +86,7 @@ class UserVerifyView(CreateAPIView):
if not verify_data:
raise ExpiredCodeException("Verification data not found or expired.")
except (ServiceUnavailableException) as e:
return Response({"detail": str(e)}, status=e.status_code)
return AppAPIException({"message": str(e)}, status_code=e.status_code)
except ExpiredCodeException:
raise ExpiredCodeException("The verification code has expired.")
@ -126,6 +135,14 @@ class UserVerifyView(CreateAPIView):
class UserLoginView(CreateAPIView):
permission_classes = [AllowAny]
serializer_class = UserLoginSerializer
@swagger_auto_schema(
operation_description=doc_login(),
request_body=UserLoginSerializer,
)
def post(self, request, *args, **kwargs):
return super().post(request, *args, **kwargs)
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
@ -133,7 +150,7 @@ class UserLoginView(CreateAPIView):
data = serializer.data
user = authenticate(request, username=request.data['email'], password=data['password'])
if not user:
raise AuthenticationFailed(_('Unable to log in with provided credentials.'))
raise ValidationError({"email": "Unable to log in with provided credentials."})
user.last_login = timezone.now()
user.is_active = True
user.save
@ -169,7 +186,11 @@ class UserUpdateView(UpdateAPIView):
class UserRecoverPassword(CreateAPIView):
serializer_class = UserRecoverPasswordSerializer
@swagger_auto_schema(
operation_description=doc_recover(),
request_body=UserRecoverPasswordSerializer,
)
def post(self, request):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
@ -198,6 +219,10 @@ class UserResetPassword(CreateAPIView):
serializer_class = UserResetPasswordSerializer
permission_classes = [IsAuthenticated]
@swagger_auto_schema(
operation_description=doc_reset(),
request_body=UserResetPasswordSerializer,
)
def post(self, request, *args, **kwargs):
# Get the logged-in user
user = request.user
@ -223,7 +248,7 @@ class UserDeleteView(APIView):
try:
user = request.user
if user.email == "admin@gmail.com":
return Response({"detail": "admin"}, status=status.HTTP_204_NO_CONTENT)
raise AppAPIException({"message": "Unable to log in with provided credentials."}, status_code=status.HTTP_204_NO_CONTENT)
user.soft_delete()
if t := Token.objects.filter(user=user).first():

0
apps/chat/__init__.py

3
apps/chat/admin.py

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
apps/chat/apps.py

@ -0,0 +1,6 @@
from django.apps import AppConfig
class ChatConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.chat'

35
apps/chat/migrations/0001_initial.py

@ -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
apps/chat/migrations/__init__.py

60
apps/chat/models.py

@ -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
apps/account/tests.py → apps/chat/tests.py

3
apps/chat/views.py

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

3
apps/course/admin/__init__.py

@ -1,2 +1,3 @@
from .course import *
from .lesson import *
from .lesson import *
from .participant import *

31
apps/course/admin/course.py

@ -1,8 +1,14 @@
import os
import hashlib
from django.contrib import admin
from django import forms
from ajaxdatatable.admin import AjaxDatatable
from utils.json_editor_field import JsonEditorWidget
from apps.course.models import Course, Glossary, Attachment, CourseCategory
from utils.schema import get_weekly_timing_schema, get_course_feature_schema
@ -11,16 +17,35 @@ class CourseCategoryAdmin(admin.ModelAdmin):
list_display = ('name', 'slug')
search_fields = ('name',)
exclude = ('slug', )
class CourseForm(forms.ModelForm):
class Meta:
model = Course
fields = '__all__'
# exclude = ('slug',)
widgets = {
'timing': JsonEditorWidget(attrs={'schema': get_weekly_timing_schema}),
'features': JsonEditorWidget(attrs={'schema': get_course_feature_schema}),
}
@admin.register(Course)
class CourseAdmin(AjaxDatatable):
form = CourseForm
list_display = ('title', 'category', 'level', 'status', 'final_price', 'is_online')
list_filter = ('status', 'level', 'is_online', 'is_free', 'category')
search_fields = ('title', 'description')
exclude = ('slug', )
# @admin.display(description='Add Student')
# def _add_student(self, obj):
@ -35,10 +60,6 @@ class GlossaryAdmin(admin.ModelAdmin):
from django import forms
import hashlib
import os
class AttachmentAdminForm(forms.ModelForm):
class Meta:

21
apps/course/admin/lesson.py

@ -1,6 +1,6 @@
from django.contrib import admin
from apps.course.models import Lesson
from apps.course.models import Lesson, LessonCompletion
@ -15,4 +15,19 @@ class LessonAdmin(admin.ModelAdmin):
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.order_by('priority')
@admin.register(LessonCompletion)
class LessonCompletionAdmin(admin.ModelAdmin):
list_display = ('student', 'lesson', 'completed_at')
search_fields = ('student__fullname', 'student__email', 'lesson__title', 'lesson__course__title')
list_filter = ('lesson__course', 'completed_at')
ordering = ('-completed_at',)
def get_readonly_fields(self, request, obj=None):
"""
Make fields readonly if the object already exists.
"""
if obj:
return ['student', 'lesson', 'completed_at']
return []

33
apps/course/admin/participant.py

@ -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

410
apps/course/doc.py

@ -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
}
]
```
"""

115
apps/course/migrations/0001_initial.py

@ -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',),
},
),
]

32
apps/course/migrations/0002_auto_20241121_2238.py

@ -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')},
},
),
]

27
apps/course/migrations/0003_participant.py

@ -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')},
},
),
]

42
apps/course/migrations/0004_auto_20241122_1913.py

@ -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,
),
]

3
apps/course/models/__init__.py

@ -1,2 +1,3 @@
from .course import *
from .lesson import *
from .lesson import *
from .participant import *

17
apps/course/models/course.py

@ -50,10 +50,10 @@ class Course(models.Model):
class StatusChoices(TextChoices):
INACTIVE = 'inactive', 'Inactive' # Not Active (does not show)
UPCOMING = 'upcoming', 'Upcoming' # Upcoming (visible but registration not allowed)
REGISTERING = 'registering', 'Registering' # Registering (registration is open)
ONGOING = 'ongoing', 'Ongoing' # Ongoing (course has started, registration closed)
FINISHED = 'finished', 'Finished' # Finished (course has ended)
UPCOMING = 'upcoming', 'Upcoming' # Upcoming (visible but registration not allowed)-Предстоящие
REGISTERING = 'registering', 'Registering' # Registering (registration is open)-регистрация
ONGOING = 'ongoing', 'Ongoing' # Ongoing (course has started, registration closed)-В процессе
FINISHED = 'finished', 'Finished' # Finished (course has ended)-закончился
class VedioTypeChoices(models.TextChoices):
VIDEO_FILE = 'video_file', 'Video File'
@ -100,10 +100,19 @@ class Course(models.Model):
timing = models.JSONField(blank=True, null=True, default=default_timing, verbose_name=_("Timing"), help_text=_("The Timing information in JSON format."))
features = models.JSONField(verbose_name=_('Course features'), default=dict, blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created at"))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At"))
def __str__(self):
return self.title
def get_completed_lessons_count(self, student):
return self.lessons.filter(completions__student=student).count()
def is_student_participant(self, student):
return self.participants.filter(student=student).exists()
def save(self, *args, **kwargs):
self.slug = generate_slug_for_model(Course, self.title)

33
apps/course/models/lesson.py

@ -1,9 +1,11 @@
import os
from django.db import models
from django.utils.translation import gettext_lazy as _
from filer.fields.image import FilerImageField
from filer.fields.file import FilerFileField
from apps.account.models import StudentUser
def lesson_file_upload_to(instance, filename):
@ -20,6 +22,7 @@ class Lesson(models.Model):
course = models.ForeignKey("course.Course", on_delete=models.CASCADE, related_name='lessons', verbose_name='Course')
title = models.CharField(max_length=255, verbose_name='Lesson Title')
priority = models.IntegerField(null=True, blank=True, verbose_name='Priority')
is_active = models.BooleanField(default=True, verbose_name=_('Is Active'))
duration = models.PositiveIntegerField(verbose_name='Duration (in minutes)')
content_type = models.CharField(max_length=10, choices=ContentTypeChoices.choices, verbose_name='Content Type')
content_file = models.FileField(
@ -29,6 +32,36 @@ class Lesson(models.Model):
)
video_link = models.CharField(max_length=500, null=True, blank=True, verbose_name='Video Link')
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created at"))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At"))
def __str__(self):
return f"{self.course.title} - {self.title}"
def is_completed_by(self, student):
return self.completions.filter(student=student).exists()
class LessonCompletion(models.Model):
student = models.ForeignKey(
StudentUser,
on_delete=models.CASCADE,
related_name='lesson_completions'
)
lesson = models.ForeignKey(
Lesson,
on_delete=models.CASCADE,
related_name='completions'
)
completed_at = models.DateTimeField(auto_now_add=True)
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created at"))
class Meta:
unique_together = ('student', 'lesson')
def __str__(self):
return f"{self.student.fullname} - {self.lesson.title} - Completed"

24
apps/course/models/participant.py

@ -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')

3
apps/course/serializers/__init__.py

@ -1 +1,2 @@
from .course import *
from .course import *
from .lesson import *

113
apps/course/serializers/course.py

@ -1,9 +1,9 @@
from rest_framework import serializers
from dj_filer.admin import get_thumbs
from apps.course.models import Course, CourseCategory, Attachment, Glossary
from apps.account.serializers import UserProfileSerializer
from apps.course.models import Course, CourseCategory, Attachment, Glossary, LessonCompletion, Participant
from apps.account.serializers import UserProfileSerializer
@ -15,15 +15,15 @@ class CourseCategorySerializer(serializers.ModelSerializer):
fields = ['name', 'slug', 'course_count']
def get_course_count(self, obj):
# return obj.course_count
return 25
return obj.course_count
class CourseListSerializer(serializers.ModelSerializer):
category = CourseCategorySerializer()
thumbnail = serializers.SerializerMethodField()
participant_count = serializers.SerializerMethodField()
lessons_count = serializers.SerializerMethodField()
class Meta:
model = Course
fields = [
@ -49,8 +49,14 @@ class CourseListSerializer(serializers.ModelSerializer):
return get_thumbs(obj.thumbnail, self.context.get('request'))
def get_participant_count(self, obj):
return 120
return obj.participants.count()
def get_lessons_count(self, obj):
lessons_count = obj.lessons.filter(is_active=True).count()
return max(lessons_count, obj.lessons_count)
class CourseDetailSerializer(serializers.ModelSerializer):
@ -58,7 +64,10 @@ class CourseDetailSerializer(serializers.ModelSerializer):
professor = UserProfileSerializer()
thumbnail = serializers.SerializerMethodField()
participant_count = serializers.SerializerMethodField()
access = serializers.SerializerMethodField()
lessons_complated_count = serializers.SerializerMethodField()
lessons_count = serializers.SerializerMethodField()
class Meta:
model = Course
fields = [
@ -66,6 +75,7 @@ class CourseDetailSerializer(serializers.ModelSerializer):
'title',
'slug',
'category',
'access',
'participant_count',
'professor',
'thumbnail',
@ -76,6 +86,7 @@ class CourseDetailSerializer(serializers.ModelSerializer):
'level',
'duration',
'lessons_count',
'lessons_complated_count',
'short_description',
'status',
'is_free',
@ -86,13 +97,99 @@ class CourseDetailSerializer(serializers.ModelSerializer):
'features',
]
def get_access(self, obj):
if student := self._get_authenticated_user():
if not self._is_participant(student, obj):
return False
return True
return False
def get_is_professor(self, obj):
if professor := self._get_authenticated_user():
return obj.professor == professor
return False
def get_lessons_count(self, obj):
lessons_count = obj.lessons.filter(is_active=True).count()
return max(lessons_count, obj.lessons_count)
def get_lessons_complated_count(self, obj):
if student := self._get_authenticated_user():
if not self._is_participant(student, obj):
return None
return self._get_completed_lessons_count(student, obj)
return None
def _is_participant(self, student, course):
"""Helper method to check if a student is a participant in the given course."""
return Participant.objects.filter(student=student, course=course).exists()
def _get_authenticated_user(self):
"""Helper method to retrieve the authenticated user from the context."""
request = self.context.get('request')
return request.user if request and request.user.is_authenticated else None
def _get_completed_lessons_count(self, student, course):
"""Helper method to count completed lessons for the student in the given course."""
return LessonCompletion.objects.filter(
student=student,
lesson__course=course
).count()
def get_thumbnail(self, obj):
return get_thumbs(obj.thumbnail, self.context.get('request'))
def get_participant_count(self, obj):
return 120
return obj.participants.count()
class MyCourseListSerializer(serializers.ModelSerializer):
category = CourseCategorySerializer()
thumbnail = serializers.SerializerMethodField()
lessons_complated_count = serializers.SerializerMethodField()
class Meta:
model = Course
fields = [
'id',
'title',
'slug',
'category',
'thumbnail',
'lessons_count',
'lessons_complated_count',
'short_description',
'status',
]
def get_thumbnail(self, obj):
return get_thumbs(obj.thumbnail, self.context.get('request'))
def get_lessons_complated_count(self, obj):
if student := self._get_authenticated_user():
if not self._is_participant(student, obj):
return None
return self._get_completed_lessons_count(student, obj)
return None
def _is_participant(self, student, course):
"""Helper method to check if a student is a participant in the given course."""
return Participant.objects.filter(student=student, course=course).exists()
def _get_authenticated_user(self):
"""Helper method to retrieve the authenticated user from the context."""
request = self.context.get('request')
return request.user if request and request.user.is_authenticated else None
def _get_completed_lessons_count(self, student, course):
"""Helper method to count completed lessons for the student in the given course."""
return LessonCompletion.objects.filter(
student=student,
lesson__course=course
).count()
class AttachmentSerializer(serializers.ModelSerializer):

39
apps/course/serializers/lesson.py

@ -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 {}

4
apps/course/urls.py

@ -8,8 +8,12 @@ from . import views
urlpatterns = [
path('categories/', views.CourseCategoryAPIView.as_view(), name='course-categories'),
path('', views.CourseListAPIView.as_view(), name='course-list'),
path('my-courses/', views.MyCourseListAPIView.as_view(), name='course-my-courses-list'),
path('<slug:slug>/', views.CourseDetailAPIView.as_view(), name='course-detail'),
path('<slug:slug>/attachments/', views.AttachmentListAPIView.as_view(), name='course-attachment-list'),
path('<slug:slug>/glossaries/', views.GlossaryListAPIView.as_view(), name='course-glossary-list'),
path('<slug:slug>/lessons/', views.LessonListView.as_view(), name='course-lesson-list'),
path('<slug:slug>/participants/', views.CourseParticipantsView.as_view(), name='course-participant-list'),
]

4
apps/course/views/__init__.py

@ -1 +1,3 @@
from .course import *
from .course import *
from .lesson import *
from .participant import *

106
apps/course/views/course.py

@ -1,37 +1,69 @@
from rest_framework.generics import ListAPIView, RetrieveAPIView
from django.db.models import Count, Q, F
from drf_yasg.utils import swagger_auto_schema
from drf_yasg import openapi
from rest_framework.exceptions import NotFound
from rest_framework.permissions import IsAuthenticated
from rest_framework.filters import SearchFilter
from apps.course.serializers import (
CourseListSerializer, CourseCategorySerializer, CourseDetailSerializer,
AttachmentSerializer, GlossarySerializer
AttachmentSerializer, GlossarySerializer, MyCourseListSerializer
)
from apps.course.models import Course, CourseCategory, Attachment, Glossary
from apps.course.models import Course, CourseCategory, Attachment, Glossary, Participant
from apps.course.doc import *
class CourseCategoryAPIView(ListAPIView):
queryset = CourseCategory.objects.all()
serializer_class = CourseCategorySerializer
@swagger_auto_schema(
operation_description=doc_course_category(),
)
def get(self, request, *args, **kwargs):
return super().get(request, *args, **kwargs)
class CourseListAPIView(ListAPIView):
queryset = Course.objects.all().exclude(status=Course.StatusChoices.INACTIVE)
serializer_class = CourseListSerializer
# filterset_fields = ['category__slug',]
filter_backends = [SearchFilter]
search_fields = ['title']
@swagger_auto_schema(manual_parameters=[
@swagger_auto_schema(
operation_description=doc_course_list(),
manual_parameters=[
openapi.Parameter(
'category_slug', openapi.IN_QUERY,
description="Category of the Course",
type=openapi.TYPE_STRING,
enum=[category.slug for category in CourseCategory.objects.all()]
),
openapi.Parameter(
'status', openapi.IN_QUERY,
type=openapi.TYPE_STRING,
description="""Status =>
Upcoming (visible but registration not allowed)---Предстоящие
Registering (registration is open)---регистрация
Ongoing (course has started, registration closed)---Впроцессе
Finished (course has ended)---закончился
""",
enum=[status for status in ['upcoming', 'registering', 'ongoing', 'finished']]
),
openapi.Parameter(
'is_free', openapi.IN_QUERY,
description="Ценообразование is_free <bool>",
type=openapi.TYPE_BOOLEAN,
),
openapi.Parameter(
'is_online', openapi.IN_QUERY,
description="Статус участия is_online <bool>",
type=openapi.TYPE_BOOLEAN,
),
])
def get(self, request, *args, **kwargs):
return super().get(request, *args, **kwargs)
@ -42,7 +74,18 @@ class CourseListAPIView(ListAPIView):
filters = request.query_params
if category := filters.get('category_slug'):
queryset = queryset.filter(category__slug=category)
if status := filters.get('status'):
queryset = queryset.filter(status=status)
if is_free := filters.get('is_free'):
is_free = is_free.lower() == 'true'
queryset = queryset.filter(
models.Q(is_free=is_free) | models.Q(price=0) if is_free else models.Q(is_free=False, price__gt=0)
)
if is_online := filters.get('is_online'):
is_online = is_online.lower() == 'true'
queryset = queryset.filter(is_online=is_online)
return queryset
@ -52,9 +95,51 @@ class CourseDetailAPIView(RetrieveAPIView):
queryset = Course.objects.all()
serializer_class = CourseDetailSerializer
lookup_field = "slug"
@swagger_auto_schema(
operation_description=doc_course_detail(),
)
def get(self, request, *args, **kwargs):
return super().get(request, *args, **kwargs)
class MyCourseListAPIView(ListAPIView):
serializer_class = MyCourseListSerializer
permission_classes = [IsAuthenticated]
@swagger_auto_schema(manual_parameters=[
openapi.Parameter(
'completed', openapi.IN_QUERY,
description="мои курсы completed <bool> true",
type=openapi.TYPE_BOOLEAN,
),
],
operation_description=doc_courses_my_courses(),
operation_summary="Home",
)
def get(self, request, *args, **kwargs):
return super().get(request, *args, **kwargs)
def get_queryset(self):
queryset = Course.objects.all()
request = self.request
filters = request.query_params
student = self.request.user
qs = queryset.filter(participants__student=student)
completed_only = filters.get('completed', '').lower() == 'true'
if completed_only:
queryset = queryset.annotate(
total_lessons=Count('lessons', distinct=True),
completed_lessons=Count(
'lessons__completions',
filter=Q(lessons__completions__student=student),
distinct=True
)
).filter(total_lessons=F('completed_lessons'))
return queryset
class AttachmentListAPIView(ListAPIView):
@ -87,6 +172,8 @@ class AttachmentListAPIView(ListAPIView):
class GlossaryListAPIView(ListAPIView):
serializer_class = GlossarySerializer
filter_backends = [SearchFilter]
search_fields = ['title', 'description']
def get_queryset(self):
course_slug = self.kwargs.get('slug')
@ -95,4 +182,7 @@ class GlossaryListAPIView(ListAPIView):
except Course.DoesNotExist:
raise NotFound("Course not found")
return Glossary.objects.filter(course=course)
return Glossary.objects.filter(course=course)

28
apps/course/views/lesson.py

@ -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')

27
apps/course/views/participant.py

@ -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)

4
config/settings/base.py

@ -43,6 +43,7 @@ LOCAL_APPS = [
'apps.account.apps.AccountConfig',
'apps.api.apps.ApiConfig',
'apps.course.apps.CourseConfig',
'apps.chat.apps.ChatConfig',
]
THIRD_PARTY_APPS = [
@ -243,7 +244,8 @@ REST_FRAMEWORK = {
'rest_framework.authentication.TokenAuthentication',
# 'rest_framework.authentication.SessionAuthentication',
],
'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema' # or OpenAPISchema if using drf_yasg
'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema',
'EXCEPTION_HANDLER': 'utils.exceptions.exception_handler',
}

4
config/test_auth_middleware.py

@ -23,6 +23,10 @@ def test_auth_middleware(get_response):
t, _ = Token.objects.get_or_create(user=user)
request.META['HTTP_AUTHORIZATION'] = f"Token {t}"
user = User.objects.filter(email="mortezaei2324@gmail.com").first()
if user:
t, _ = Token.objects.get_or_create(user=user)
request.META['HTTP_AUTHORIZATION'] = f"Token {t}"
return get_response(request)

10
templates/admin/auth/user/add_form.html

@ -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 %}

57
templates/admin/auth/user/change_password.html

@ -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>
&rsaquo; <a href="{% url 'admin:app_list' app_label=opts.app_label %}">{{ opts.app_config.verbose_name }}</a>
&rsaquo; <a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>
&rsaquo; <a href="{% url opts|admin_urlname:'change' original.pk|admin_urlquote %}">{{ original|truncatewords:"18" }}</a>
&rsaquo; {% 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 %}

124
utils/exceptions.py

@ -2,9 +2,133 @@
from rest_framework.exceptions import APIException
from rest_framework import status
from rest_framework.views import exception_handler as drf_exception_handler
from rest_framework.response import Response
from rest_framework.exceptions import ValidationError
def error_response(error_code, message, details=None, status_code=status.HTTP_400_BAD_REQUEST):
# print(f'>>>> {error_code}')
response_data = {
"status": "error",
"code": error_code,
"message": message,
"details": details or {}
}
return Response(response_data, status=status_code)
def exception_handler(exc, context):
# Call REST framework's default exception handler first,
# to get the standard error response.
response = drf_exception_handler(exc, context)
# قالب‌بندی جدید خطاها
formatted_errors = []
if response is not None:
# تعیین نوع خطاها
print(f'>>> {type(exc)}/ {exc}')
if isinstance(exc, ValidationError):
error_code = "validation_error"
message = "There were validation errors."
if isinstance(response.data, list):
for error in response.data:
formatted_errors.append({
"field": None, # No specific field when the error is a list
"message": error
})
else:
# فرمت کردن خطاها به فرمت جدید
print(f'>>>>>> {response.data}')
for field, errors in response.data.items():
if isinstance(errors, list):
for error in errors:
# print()
# print(f'>>>> {field}')
formatted_errors.append({
"field": field,
"message": error
})
else:
formatted_errors.append({
"field": field,
"message": errors
})
seen_fields = set()
for i in range(len(formatted_errors) - 1, -1, -1):
field = formatted_errors[i]['field']
if field in seen_fields:
# اگر فیلد تکراری بود، آن را حذف کن
del formatted_errors[i]
else:
# اولین باری که فیلد دیده شده، آن را ثبت کن
seen_fields.add(field)
elif isinstance(exc, AppAPIException):
error_code = "app_api_error"
message = "An error occurred while processing the request."
for field, errors in response.data.items():
if isinstance(errors, list):
for error in errors:
formatted_errors.append({
"message": error
})
else:
formatted_errors.append({
"message": errors
})
else:
error_code = "server_error"
message = "An error occurred."
# فرمت کردن خطاها به فرمت جدید
for field, errors in response.data.items():
if isinstance(errors, list):
for error in errors:
formatted_errors.append({
"message": error
})
else:
formatted_errors.append({
"message": errors
})
# تنظیم ساختار جدید برای پاسخ خطا
response.data = {
"status": "error",
"code": error_code,
"status_code": response.status_code, # Adding the status code
"message": message,
"errors": formatted_errors
}
return response
class AppAPIException(APIException):
status_code = status.HTTP_400_BAD_REQUEST
default_detail = 'An error occurred while processing the request.'
def __init__(self, detail=None, status_code=None):
if detail is None:
detail = self.default_detail
if status_code is None:
status_code = self.default_code
else:
self.status_code = status_code
super().__init__(detail, status_code)
class ExpiredCodeException(APIException):
status_code = status.HTTP_410_GONE

52
utils/schema.py

@ -1,14 +1,52 @@
from django.utils.translation import gettext_lazy as _
def default_timing():
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'))},
}
}
}
Loading…
Cancel
Save