Browse Source

merge conflicts fixed.

master
Mohsen Taba 5 months ago
parent
commit
5c88c959db
  1. 7
      apps/account/admin/__init__.py
  2. 27
      apps/account/admin/professor.py
  3. 35
      apps/account/admin/student.py
  4. 106
      apps/account/admin/user.py
  5. 5
      apps/account/serializers/__init__.py
  6. 62
      apps/account/serializers/user.py
  7. 1
      apps/account/urls.py
  8. 7
      apps/account/views/__init__.py
  9. 11
      apps/account/views/notification.py
  10. 59
      apps/account/views/user.py
  11. 18
      apps/certificate/migrations/0001_initial.py
  12. 30
      apps/chat/migrations/0001_initial.py
  13. 4
      apps/course/admin/__init__.py
  14. 86
      apps/course/admin/course.py
  15. 33
      apps/course/admin/lesson.py
  16. 83
      apps/course/migrations/0001_initial.py
  17. 4
      apps/course/models/__init__.py
  18. 88
      apps/course/models/course.py
  19. 80
      apps/course/models/lesson.py
  20. 7
      apps/course/models/participant.py
  21. 4
      apps/course/serializers/__init__.py
  22. 111
      apps/course/serializers/course.py
  23. 41
      apps/course/serializers/lesson.py
  24. 4
      apps/course/views/__init__.py
  25. 104
      apps/course/views/course.py
  26. 122
      apps/course/views/lesson.py
  27. 9
      apps/course/views/participant.py
  28. 4
      apps/hadis/admin/__init__.py
  29. 219
      apps/hadis/admin/category.py
  30. 162
      apps/hadis/admin/hadis.py
  31. 124
      apps/hadis/management/commands/README.md
  32. 74
      apps/hadis/migrations/0001_initial.py
  33. 4
      apps/hadis/models/__init__.py
  34. 102
      apps/hadis/models/category.py
  35. 87
      apps/hadis/models/hadis.py
  36. 36
      apps/hadis/models/transmitter.py
  37. 4
      apps/hadis/views/__init__.py
  38. 78
      apps/hadis/views/hadis.py
  39. 83
      apps/library/migrations/0001_initial.py
  40. 43
      apps/quiz/admin/participant.py
  41. 50
      apps/quiz/admin/question.py
  42. 57
      apps/quiz/admin/quiz.py
  43. 62
      apps/quiz/migrations/0001_initial.py
  44. 3
      apps/quiz/models/participant.py
  45. 4
      apps/quiz/models/quiz.py
  46. 13
      apps/quiz/serializers/quiz.py
  47. 5
      apps/quiz/views/participant.py
  48. 7
      apps/quiz/views/quiz.py
  49. 20
      apps/transaction/migrations/0001_initial.py
  50. 52
      apps/video/migrations/0001_initial.py
  51. 22
      dynamic_preferences/admin.py
  52. 58
      dynamic_preferences/locale/fa/LC_MESSAGES/django.po
  53. 54
      utils/__init__.py
  54. 10
      utils/json_editor_field.py
  55. 12
      utils/redis.py
  56. 7
      utils/schema.py
  57. 3
      utils/validators.py

7
apps/account/admin/__init__.py

@ -1,9 +1,3 @@
<<<<<<< HEAD
from .user import *
from .professor import *
from .student import *
=======
from unfold.components import BaseComponent, register_component from unfold.components import BaseComponent, register_component
from django.template.loader import render_to_string from django.template.loader import render_to_string
@ -71,4 +65,3 @@ class StudentUserComponent(BaseComponent):
}, },
) )
return context return context
>>>>>>> develop

27
apps/account/admin/professor.py

@ -1,10 +1,5 @@
<<<<<<< HEAD
from django.contrib import admin
from django.contrib.auth.forms import UserChangeForm, UsernameField
=======
# This file is no longer used. All admin classes are now in user.pyfrom django.contrib import admin # This file is no longer used. All admin classes are now in user.pyfrom django.contrib import admin
from django.contrib.auth.forms import UserChangeForm, UsernameField, UserCreationForm from django.contrib.auth.forms import UserChangeForm, UsernameField, UserCreationForm
>>>>>>> develop
from django.contrib.auth.admin import UserAdmin from django.contrib.auth.admin import UserAdmin
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework.authtoken.models import TokenProxy from rest_framework.authtoken.models import TokenProxy
@ -13,29 +8,15 @@ from ajaxdatatable.admin import AjaxDatatable
from django.contrib import admin from django.contrib import admin
from apps.account.models import User from apps.account.models import User
from django import forms from django import forms
<<<<<<< HEAD
from django.contrib import admin
from django.urls import path, reverse
from django.shortcuts import render, redirect
from django.contrib import messages
=======
from django.urls import path, reverse from django.urls import path, reverse
from django.shortcuts import render, redirect from django.shortcuts import render, redirect
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from phonenumber_field.formfields import PhoneNumberField from phonenumber_field.formfields import PhoneNumberField
>>>>>>> develop
from apps.account.models import ProfessorUser from apps.account.models import ProfessorUser
<<<<<<< HEAD
@admin.register(ProfessorUser)
class ProfessorUserAdmin(UserAdmin, AjaxDatatable):
list_display = (
'device_id', 'email', 'fullname', 'user_type','last_login', 'date_joined',
=======
class ProfessorUserCreationForm(UserCreationForm): class ProfessorUserCreationForm(UserCreationForm):
phone_number = PhoneNumberField( phone_number = PhoneNumberField(
help_text="Enter the phone number in international format. Example: +989012023212", help_text="Enter the phone number in international format. Example: +989012023212",
@ -52,7 +33,6 @@ class ProfessorUserAdmin(UserAdmin, AjaxDatatable):
add_form = ProfessorUserCreationForm add_form = ProfessorUserCreationForm
list_display = ( list_display = (
'email', 'fullname', 'last_login', 'date_joined', 'email', 'fullname', 'last_login', 'date_joined',
>>>>>>> develop
) )
ordering = 'last_login', ordering = 'last_login',
readonly_fields = ('date_joined',) readonly_fields = ('date_joined',)
@ -84,12 +64,6 @@ class ProfessorUserAdmin(UserAdmin, AjaxDatatable):
) )
def save_model(self, request, obj, form, change): def save_model(self, request, obj, form, change):
<<<<<<< HEAD
if not change:
obj.set_password(form.cleaned_data['password1'])
obj.user_type = User.UserType.PROFESSOR
super().save_model(request, obj, form, change)
=======
if not change: # Creating a new professor if not change: # Creating a new professor
# Check if a user with this email already exists # Check if a user with this email already exists
email = form.cleaned_data.get('email') email = form.cleaned_data.get('email')
@ -131,7 +105,6 @@ class ProfessorUserAdmin(UserAdmin, AjaxDatatable):
if obj: # Only proceed if obj is not None if obj: # Only proceed if obj is not None
obj.add_role('professor') obj.add_role('professor')
super().save_model(request, obj, form, change) super().save_model(request, obj, form, change)
>>>>>>> develop
@admin.display(description='Phone Number') @admin.display(description='Phone Number')
def _phone_number(self, obj): def _phone_number(self, obj):

35
apps/account/admin/student.py

@ -4,10 +4,7 @@ from django.contrib.auth.admin import UserAdmin
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework.authtoken.models import TokenProxy from rest_framework.authtoken.models import TokenProxy
from ajaxdatatable.admin import AjaxDatatable from ajaxdatatable.admin import AjaxDatatable
<<<<<<< HEAD
=======
from unfold.admin import TabularInline, StackedInline from unfold.admin import TabularInline, StackedInline
>>>>>>> develop
from django.contrib import admin from django.contrib import admin
from apps.account.models import User from apps.account.models import User
@ -19,21 +16,11 @@ from django.contrib import messages
from apps.account.models import StudentUser, User from apps.account.models import StudentUser, User
<<<<<<< HEAD
@admin.register(StudentUser)
class StudentUserAdmin(UserAdmin, AjaxDatatable):
list_display = (
'device_id', 'email', 'fullname', 'user_type','last_login', 'date_joined',
)
=======
@admin.register(StudentUser) @admin.register(StudentUser)
class StudentUserAdmin(UserAdmin, AjaxDatatable): class StudentUserAdmin(UserAdmin, AjaxDatatable):
list_display = ( list_display = (
'device_id', 'email', 'fullname', 'user_type', 'enrolled_courses_count', 'last_login', 'date_joined', 'device_id', 'email', 'fullname', 'user_type', 'enrolled_courses_count', 'last_login', 'date_joined',
) )
>>>>>>> develop
ordering = 'last_login', ordering = 'last_login',
readonly_fields = ('date_joined',) readonly_fields = ('date_joined',)
exclude = ('password', 'user_permissions') exclude = ('password', 'user_permissions')
@ -41,11 +28,6 @@ class StudentUserAdmin(UserAdmin, AjaxDatatable):
(None, { (None, {
'classes': ('wide',), 'classes': ('wide',),
'fields': ('fullname', 'email', 'phone_number',), 'fields': ('fullname', 'email', 'phone_number',),
<<<<<<< HEAD
# 'description': 'Please provide the student details including full name, email, and phone number.',
=======
>>>>>>> develop
}), }),
('other', { ('other', {
'classes': ('wide',), 'classes': ('wide',),
@ -62,25 +44,13 @@ class StudentUserAdmin(UserAdmin, AjaxDatatable):
fieldsets = ( fieldsets = (
(_('Personal info'), {'fields': ('fullname', 'email', 'phone_number', 'avatar',)}), (_('Personal info'), {'fields': ('fullname', 'email', 'phone_number', 'avatar',)}),
(_('Permissions'), { (_('Permissions'), {
<<<<<<< HEAD
'fields': ('is_active', 'is_staff', 'is_superuser', 'groups',),
=======
'fields': ('is_active', 'groups',), 'fields': ('is_active', 'groups',),
>>>>>>> develop
}), }),
(_('Important dates'), {'fields': ('last_login', 'date_joined', 'fcm')}), (_('Important dates'), {'fields': ('last_login', 'date_joined', 'fcm')}),
) )
@admin.display(description='Phone Number') @admin.display(description='Phone Number')
def _phone_number(self, obj): def _phone_number(self, obj):
return obj.phone_number return obj.phone_number
<<<<<<< HEAD
def get_queryset(self, request):
# محدود کردن نمایش فقط دانش‌آموزان
qs = super().get_queryset(request)
return qs.filter(user_type=User.UserType.STUDENT)
=======
@admin.display(description=_('Enrolled Courses')) @admin.display(description=_('Enrolled Courses'))
def enrolled_courses_count(self, obj): def enrolled_courses_count(self, obj):
@ -93,17 +63,12 @@ class StudentUserAdmin(UserAdmin, AjaxDatatable):
# محدود کردن نمایش فقط دانش‌آموزان و بهینه‌سازی query # محدود کردن نمایش فقط دانش‌آموزان و بهینه‌سازی query
qs = super().get_queryset(request) qs = super().get_queryset(request)
return qs.filter(user_type=User.UserType.STUDENT).prefetch_related('participated_courses') return qs.filter(user_type=User.UserType.STUDENT).prefetch_related('participated_courses')
>>>>>>> develop
def save_model(self, request, obj, form, change): def save_model(self, request, obj, form, change):
if not change: if not change:
obj.set_password(form.cleaned_data['password1']) obj.set_password(form.cleaned_data['password1'])
<<<<<<< HEAD
obj.user_type = User.UserType.STUDENT
=======
obj.add_role('student') obj.add_role('student')
>>>>>>> develop
super().save_model(request, obj, form, change) super().save_model(request, obj, form, change)

106
apps/account/admin/user.py

@ -1,108 +1,3 @@
<<<<<<< HEAD
from django.contrib import admin
from django.contrib.auth.forms import UserChangeForm, UsernameField
from django.contrib.auth.admin import UserAdmin
from django.utils.translation import gettext_lazy as _
from rest_framework.authtoken.models import TokenProxy
from ajaxdatatable.admin import AjaxDatatable
from apps.account.models import User, Notification
from django import forms
from django.contrib import admin
from django.urls import path, reverse
from django.shortcuts import render, redirect
from django.contrib import messages
from apps.account.models import ClientUser, AdminUser
@admin.register(Notification)
class NotificationAdmin(AjaxDatatable):
list_display = ('title', 'user', 'is_read', 'created_at')
list_filter = ('is_read', 'created_at')
search_fields = ('title', 'message', 'user__fullname')
list_editable = ('is_read',)
ordering = ('-created_at',)
autocomplete_fields = ['user',]
@admin.register(User)
class UserAdmin(UserAdmin, AjaxDatatable):
list_display = (
'device_id', 'email', 'fullname', 'user_type','last_login', 'device_os', 'date_joined',
)
ordering = '-id',
readonly_fields = ('date_joined',)
exclude = ('password', 'user_permissions')
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': ('email', 'password1', 'password2'),
}),
)
search_fields = (
'email', 'fullname', 'username',
)
fieldsets = (
(_('Personal info'), {'fields': ('fullname', 'email', 'phone_number', 'avatar',)}),
(_('Permissions'), {
'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'password'),
}),
(_('Important dates'), {'fields': ('last_login', 'date_joined', 'fcm')}),
)
def save_model(self, request, obj, form, change):
if not change:
obj.set_password(form.cleaned_data['password1'])
# obj.user_type = User.UserType.CLIENT
super().save_model(request, obj, form, change)
@admin.display(description='Phone Number')
def _phone_number(self, obj):
return obj.phone_number
@admin.register(AdminUser)
class AdminUserAdmin(UserAdmin, AjaxDatatable):
list_display = (
'email', 'fullname', 'user_type','last_login', 'date_joined',
)
ordering = 'last_login',
readonly_fields = ('date_joined',)
exclude = ('password', 'user_permissions')
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': ('email', 'password1', 'password2'),
}),
)
search_fields = (
'email', 'fullname', 'username',
)
fieldsets = (
(_('Personal info'), {'fields': ('fullname', 'email', 'phone_number', 'avatar',)}),
(_('Permissions'), {
'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'password'),
}),
(_('Important dates'), {'fields': ('last_login', 'date_joined', 'fcm')}),
)
def save_model(self, request, obj, form, change):
if not change:
obj.set_password(form.cleaned_data['password1'])
# obj.user_type = User.UserType.CLIENT
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)
=======
from django import forms from django import forms
from django.contrib import admin from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
@ -481,4 +376,3 @@ try:
admin.site.unregister(TokenProxy) admin.site.unregister(TokenProxy)
except admin.sites.NotRegistered: except admin.sites.NotRegistered:
pass pass
>>>>>>> develop

5
apps/account/serializers/__init__.py

@ -1,7 +1,4 @@
from .user import * from .user import *
from .notification import * from .notification import *
<<<<<<< HEAD
=======
from .location_history import *
from .auth import * from .auth import *
>>>>>>> develop
from .location_history import *

62
apps/account/serializers/user.py

@ -1,8 +1,3 @@
<<<<<<< HEAD
=======
>>>>>>> develop
from rest_framework import serializers from rest_framework import serializers
from rest_framework.authtoken.models import Token from rest_framework.authtoken.models import Token
from django.contrib.auth.password_validation import validate_password from django.contrib.auth.password_validation import validate_password
@ -18,17 +13,6 @@ class UserProfileSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True, required=False, validators=[validate_password]) password = serializers.CharField(write_only=True, required=False, validators=[validate_password])
fullname = serializers.CharField(required=False) fullname = serializers.CharField(required=False)
gender = serializers.ChoiceField( gender = serializers.ChoiceField(
<<<<<<< HEAD
choices=User.GenderChoices.choices,
required=False,
help_text="Select the user's gender."
)
fcm = serializers.CharField(required=False, help_text="Firebase Cloud Messaging token.")
class Meta:
model = User
fields = ['id', 'device_id', 'fcm', 'fullname', 'avatar', 'email', 'phone_number', 'password', 'info', 'skill', 'city', 'country', 'birthdate', 'gender']
read_only_fields = ['email', 'info', 'skill', 'device_id']
=======
choices=User.GenderChoices.choices, choices=User.GenderChoices.choices,
required=False, required=False,
help_text="Select the user's gender." help_text="Select the user's gender."
@ -54,7 +38,6 @@ class UserProfileSerializer(serializers.ModelSerializer):
'at_time': last_location.at_time, 'at_time': last_location.at_time,
} }
return None return None
>>>>>>> develop
# def validate_email(self, value): # def validate_email(self, value):
# if User.objects.filter(email=value).exists(): # if User.objects.filter(email=value).exists():
@ -62,36 +45,25 @@ class UserProfileSerializer(serializers.ModelSerializer):
# return value # return value
def update(self, instance, validated_data): def update(self, instance, validated_data):
<<<<<<< HEAD
=======
# Pop the password from the data to handle it separately # Pop the password from the data to handle it separately
password = validated_data.pop('password', None) password = validated_data.pop('password', None)
# Use the default update logic for all other fields # Use the default update logic for all other fields
>>>>>>> develop
for attr, value in validated_data.items(): for attr, value in validated_data.items():
if value is not None: if value is not None:
setattr(instance, attr, value) setattr(instance, attr, value)
<<<<<<< HEAD
=======
# If a new password was provided, hash and set it correctly # If a new password was provided, hash and set it correctly
if password: if password:
instance.set_password(password) instance.set_password(password)
>>>>>>> develop
instance.save() instance.save()
return instance return instance
class UserRegisterSerializer(serializers.ModelSerializer): class UserRegisterSerializer(serializers.ModelSerializer):
<<<<<<< HEAD
fcm = serializers.CharField(required=False)
device_id = serializers.CharField(required=True)
=======
fcm = serializers.CharField(required=False, allow_blank=True, allow_null=True) fcm = serializers.CharField(required=False, allow_blank=True, allow_null=True)
device_id = serializers.CharField(required=False, allow_blank=True, allow_null=True, write_only=True) device_id = serializers.CharField(required=False, allow_blank=True, allow_null=True, write_only=True)
>>>>>>> develop
email = serializers.EmailField() email = serializers.EmailField()
class Meta: class Meta:
@ -100,15 +72,6 @@ class UserRegisterSerializer(serializers.ModelSerializer):
extra_kwargs = { extra_kwargs = {
'fullname': {'required': True,}, 'fullname': {'required': True,},
'email': {'required': True,}, 'email': {'required': True,},
<<<<<<< HEAD
'device_id': {'required': True,},
}
def validate_email(self, value):
if User.objects.filter(email=value).exists():
raise serializers.ValidationError("This email is already registered.")
return value
=======
} }
def create(self, validated_data): def create(self, validated_data):
@ -124,7 +87,6 @@ class UserRegisterSerializer(serializers.ModelSerializer):
if User.objects.filter(email=normalized_email).exists(): if User.objects.filter(email=normalized_email).exists():
raise serializers.ValidationError("This email is already registered.") raise serializers.ValidationError("This email is already registered.")
return normalized_email return normalized_email
>>>>>>> develop
@ -133,16 +95,12 @@ class UserVerifySerializer(serializers.Serializer):
email = serializers.EmailField() email = serializers.EmailField()
device_id = serializers.CharField(max_length=255, required=False) device_id = serializers.CharField(max_length=255, required=False)
<<<<<<< HEAD
=======
def validate_email(self, value): def validate_email(self, value):
""" """
Normalize the email to ensure the Redis key matches correctly. Normalize the email to ensure the Redis key matches correctly.
""" """
return User.objects.normalize_email(value) return User.objects.normalize_email(value)
>>>>>>> develop
class UserLoginSerializer(serializers.Serializer): class UserLoginSerializer(serializers.Serializer):
password = serializers.CharField(write_only=True) password = serializers.CharField(write_only=True)
@ -160,15 +118,12 @@ class UserLoginSerializer(serializers.Serializer):
# data.pop('fcm', None) # data.pop('fcm', None)
# data.pop('device_id', None) # data.pop('device_id', None)
return data return data
<<<<<<< HEAD
=======
def validate_email(self, value): def validate_email(self, value):
""" """
Normalize email for case-insensitive login. Normalize email for case-insensitive login.
""" """
return User.objects.normalize_email(value) return User.objects.normalize_email(value)
>>>>>>> develop
# class UserLoginSerializer(serializers.Serializer): # class UserLoginSerializer(serializers.Serializer):
# password = serializers.CharField(write_only=True) # password = serializers.CharField(write_only=True)
@ -184,19 +139,6 @@ class UserLoginSerializer(serializers.Serializer):
<<<<<<< HEAD
class UserRecoverPasswordSerializer(serializers.ModelSerializer):
email = serializers.EmailField()
class Meta:
model = User
fields = ['email',]
extra_kwargs = {
'email': {'required': True,},
}
=======
# class UserRecoverPasswordSerializer(serializers.ModelSerializer): # class UserRecoverPasswordSerializer(serializers.ModelSerializer):
# email = serializers.EmailField() # email = serializers.EmailField()
@ -220,7 +162,6 @@ class UserRecoverPasswordSerializer(serializers.Serializer):
""" """
return User.objects.normalize_email(value) return User.objects.normalize_email(value)
>>>>>>> develop
class UserResetPasswordSerializer(serializers.ModelSerializer): class UserResetPasswordSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True) password = serializers.CharField(write_only=True)
@ -253,8 +194,6 @@ class UserGuestSerializer(serializers.ModelSerializer):
return data return data
<<<<<<< HEAD
=======
class WebUserGuestSerializer(serializers.ModelSerializer): class WebUserGuestSerializer(serializers.ModelSerializer):
user_agent = serializers.CharField(required=False, allow_null=True, allow_blank=True) user_agent = serializers.CharField(required=False, allow_null=True, allow_blank=True)
@ -277,4 +216,3 @@ class UserFCMSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = User model = User
fields = ['fcm'] fields = ['fcm']
>>>>>>> develop

1
apps/account/urls.py

@ -1,4 +1,3 @@
from django.urls import path, include from django.urls import path, include
from rest_framework.routers import DefaultRouter from rest_framework.routers import DefaultRouter

7
apps/account/views/__init__.py

@ -1,9 +1,4 @@
from .user import * from .user import *
from .notification import * from .notification import *
<<<<<<< HEAD
=======
from .location_history import *
from .auth import * from .auth import *
>>>>>>> develop
from .location_history import*

11
apps/account/views/notification.py

@ -1,17 +1,11 @@
from rest_framework import generics, status from rest_framework import generics, status
from rest_framework.response import Response from rest_framework.response import Response
from drf_yasg.utils import swagger_auto_schema from drf_yasg.utils import swagger_auto_schema
from drf_yasg import openapi from drf_yasg import openapi
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from apps.account.serializers import NotificationSerializer, NotificationSendSerializer from apps.account.serializers import NotificationSerializer, NotificationSendSerializer
<<<<<<< HEAD
from apps.account.models import Notification
# from apps.account.fcm_notification import send_notification
=======
from apps.account.models import Notification, User from apps.account.models import Notification, User
from apps.account.tasks import send_notification from apps.account.tasks import send_notification
>>>>>>> develop
@ -39,7 +33,6 @@ class NotificationListView(generics.ListAPIView):
This API allows you to retrieve a list of notifications based on the authenticated user's type. This API allows you to retrieve a list of notifications based on the authenticated user's type.
If the user is a regular user, their notifications will be fetched from the `Notification` model. If the user is a regular user, their notifications will be fetched from the `Notification` model.
If the user is a merchant, their notifications will be fetched from the `MerchantAccountNotification` model. If the user is a merchant, their notifications will be fetched from the `MerchantAccountNotification` model.
- **Method**: GET - **Method**: GET
- **URL**: /api/notifications/ - **URL**: /api/notifications/
- **Query Parameters**: - **Query Parameters**:
@ -108,8 +101,6 @@ class NotificationReadAllView(generics.GenericAPIView):
<<<<<<< HEAD
=======
class SendNotificationView(generics.GenericAPIView): class SendNotificationView(generics.GenericAPIView):
@swagger_auto_schema( @swagger_auto_schema(
@ -161,5 +152,3 @@ class SendNotificationView(generics.GenericAPIView):
return Response({ return Response({
'error': str(e) 'error': str(e)
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
>>>>>>> develop

59
apps/account/views/user.py

@ -23,12 +23,8 @@ from rest_framework.exceptions import ValidationError
from utils.exceptions import InvaliedCodeVrify, ExpiredCodeException, ServiceUnavailableException from utils.exceptions import InvaliedCodeVrify, ExpiredCodeException, ServiceUnavailableException
from apps.account.models import User from apps.account.models import User
<<<<<<< HEAD
from apps.account.serializers import UserRegisterSerializer, UserProfileSerializer, UserVerifySerializer, UserLoginSerializer, UserRecoverPasswordSerializer, UserResetPasswordSerializer, UserGuestSerializer
=======
from apps.account.serializers import UserRegisterSerializer, UserProfileSerializer, UserVerifySerializer, UserLoginSerializer, UserRecoverPasswordSerializer, UserResetPasswordSerializer, UserGuestSerializer,UserFCMSerializer,WebUserGuestSerializer from apps.account.serializers import UserRegisterSerializer, UserProfileSerializer, UserVerifySerializer, UserLoginSerializer, UserRecoverPasswordSerializer, UserResetPasswordSerializer, UserGuestSerializer,UserFCMSerializer,WebUserGuestSerializer
from apps.account.serializers.user_web import WebUserRegisterSerializer from apps.account.serializers.user_web import WebUserRegisterSerializer
>>>>>>> develop
from utils.redis import RedisManager from utils.redis import RedisManager
from utils.exceptions import AppAPIException from utils.exceptions import AppAPIException
from utils import send_email, is_valid_email from utils import send_email, is_valid_email
@ -117,8 +113,6 @@ class UserGuestView(CreateAPIView):
return obj return obj
<<<<<<< HEAD
=======
class WebUserGuestView(CreateAPIView): class WebUserGuestView(CreateAPIView):
permission_classes = [AllowAny] permission_classes = [AllowAny]
serializer_class = WebUserGuestSerializer serializer_class = WebUserGuestSerializer
@ -215,7 +209,6 @@ class WebUserGuestView(CreateAPIView):
) )
return obj return obj
>>>>>>> develop
class UserRegisterView(CreateAPIView): class UserRegisterView(CreateAPIView):
@ -230,25 +223,16 @@ class UserRegisterView(CreateAPIView):
def post(self, request): def post(self, request):
serializer = self.get_serializer(data=request.data) serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
<<<<<<< HEAD
data = serializer.data
=======
data = serializer.validated_data data = serializer.validated_data
>>>>>>> develop
code = RedisManager.generate_otp_code() code = RedisManager.generate_otp_code()
logger.info(f"phone= {data['email']}") logger.info(f"phone= {data['email']}")
print(f'send {code}/{data["email"]}') print(f'send {code}/{data["email"]}')
phone_number = RedisManager().add_to_redis(code, **data) phone_number = RedisManager().add_to_redis(code, **data)
<<<<<<< HEAD
send_email([data['email']], code)
=======
try: try:
send_email([data['email']], code) send_email([data['email']], code)
except Exception as exp: except Exception as exp:
print(f'-exp-register-->{exp}') print(f'-exp-register-->{exp}')
>>>>>>> develop
return Response( return Response(
data= { data= {
"user": data, "user": data,
@ -290,11 +274,7 @@ class UserVerifyView(CreateAPIView):
code = self.valied_code(data['code'], verify_data['code']) code = self.valied_code(data['code'], verify_data['code'])
del verify_data['code'] del verify_data['code']
user = self.perform_create( user = self.perform_create(
<<<<<<< HEAD
email=serializer.data['email'], device_id=serializer.data['device_id'], **verify_data
=======
email=serializer.data['email'], device_id=serializer.data.get('device_id'), **verify_data email=serializer.data['email'], device_id=serializer.data.get('device_id'), **verify_data
>>>>>>> develop
) )
token, _ = Token.objects.get_or_create(user=user) token, _ = Token.objects.get_or_create(user=user)
return Response(data={ return Response(data={
@ -308,11 +288,8 @@ class UserVerifyView(CreateAPIView):
def valied_code(self, current_code, save_code): def valied_code(self, current_code, save_code):
if (current_code and save_code) and ( current_code != save_code): if (current_code and save_code) and ( current_code != save_code):
<<<<<<< HEAD
=======
if current_code == "11111": if current_code == "11111":
return current_code return current_code
>>>>>>> develop
raise ValidationError({"code": "code notfound"}) raise ValidationError({"code": "code notfound"})
return current_code return current_code
@ -322,27 +299,6 @@ class UserVerifyView(CreateAPIView):
device_id = kwargs.get('device_id') device_id = kwargs.get('device_id')
user = User.objects.filter(email=email).first() user = User.objects.filter(email=email).first()
if user: if user:
<<<<<<< HEAD
if kwargs['password']:
user.is_active = True
user.deletion_date = None
user.device_id = device_id
user.last_login = timezone.now()
user.save()
else:
user = User.objects.filter(device_id=device_id, email__isnull=True).first()
if not user:
user = User.objects.create(**kwargs)
else:
user.email = email
user.fullname = kwargs['fullname']
user.device_id = device_id
user.last_login = timezone.now()
user.is_active = True
user.save()
return user
=======
if kwargs.get('password'): if kwargs.get('password'):
user.is_active = True user.is_active = True
user.deletion_date = None user.deletion_date = None
@ -410,7 +366,6 @@ class WebUserRegisterView(CreateAPIView):
}, },
status=status.HTTP_202_ACCEPTED, status=status.HTTP_202_ACCEPTED,
) )
>>>>>>> develop
class UserLoginView(CreateAPIView): class UserLoginView(CreateAPIView):
@ -503,14 +458,10 @@ class UserRecoverPassword(CreateAPIView):
print(f' send {code}') print(f' send {code}')
phone_number = RedisManager().add_to_redis(code, fullname=str(user.fullname), password='', email=data['email']) phone_number = RedisManager().add_to_redis(code, fullname=str(user.fullname), password='', email=data['email'])
<<<<<<< HEAD
send_email([data['email']], code)
=======
try: try:
send_email([data['email']], code) send_email([data['email']], code)
except Exception as exp: except Exception as exp:
print(f'-exp-register-->{exp}') print(f'-exp-register-->{exp}')
>>>>>>> develop
return Response( return Response(
data= { data= {
@ -518,11 +469,7 @@ class UserRecoverPassword(CreateAPIView):
"fullname": user.fullname, "fullname": user.fullname,
"phone_number": str(user.phone_number) if user.phone_number else None, "phone_number": str(user.phone_number) if user.phone_number else None,
"email": user.email if user.email else None, "email": user.email if user.email else None,
<<<<<<< HEAD
"avatar": user.avatar if user.avatar else None,
=======
"avatar": request.build_absolute_uri(user.avatar.url) if user.avatar else None, "avatar": request.build_absolute_uri(user.avatar.url) if user.avatar else None,
>>>>>>> develop
"message": "Forgot password code sent" "message": "Forgot password code sent"
}, },
status=status.HTTP_202_ACCEPTED, status=status.HTTP_202_ACCEPTED,
@ -575,8 +522,6 @@ class UserDeleteView(APIView):
return Response({"detail": "User does not exist."}, status=status.HTTP_404_NOT_FOUND) return Response({"detail": "User does not exist."}, status=status.HTTP_404_NOT_FOUND)
<<<<<<< HEAD
=======
class UpdateFCMView(GenericAPIView): class UpdateFCMView(GenericAPIView):
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
serializer_class = UserFCMSerializer serializer_class = UserFCMSerializer
@ -592,6 +537,4 @@ class UpdateFCMView(GenericAPIView):
user.fcm = fcm_token user.fcm = fcm_token
user.save() user.save()
return Response({"detail": "FCM token updated successfully."}, status=status.HTTP_200_OK)
>>>>>>> develop
return Response({"detail": "FCM token updated successfully."}, status=status.HTTP_200_OK)

18
apps/certificate/migrations/0001_initial.py

@ -1,16 +1,8 @@
<<<<<<< HEAD
# Generated by Django 3.2.7 on 2024-12-14 08:35
from django.db import migrations, models
import django.db.models.deletion
import filer.fields.file
=======
# Generated by Django 5.1.8 on 2025-04-03 00:05 # Generated by Django 5.1.8 on 2025-04-03 00:05
import django.db.models.deletion import django.db.models.deletion
import filer.fields.file import filer.fields.file
from django.db import migrations, models from django.db import migrations, models
>>>>>>> develop
class Migration(migrations.Migration): class Migration(migrations.Migration):
@ -18,14 +10,8 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
<<<<<<< HEAD
('course', '0005_participant_unread_messages_count'),
('account', '0005_user_city'),
('filer', '0015_auto_20241214_0835'),
=======
('account', '0001_initial'), ('account', '0001_initial'),
('course', '0001_initial'), ('course', '0001_initial'),
>>>>>>> develop
] ]
operations = [ operations = [
@ -33,11 +19,7 @@ class Migration(migrations.Migration):
name='Certificate', name='Certificate',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
<<<<<<< HEAD
('status', models.CharField(choices=[('pending', 'در حال بررسی'), ('approved', 'تایید شده'), ('canceled', 'لغو شده')], default='pending', max_length=10)),
=======
('status', models.CharField(choices=[('pending', 'pending'), ('approved', 'approved'), ('canceled', 'canceled')], default='pending', max_length=10)), ('status', models.CharField(choices=[('pending', 'pending'), ('approved', 'approved'), ('canceled', 'canceled')], default='pending', max_length=10)),
>>>>>>> develop
('created_at', models.DateTimeField(auto_now_add=True)), ('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)), ('updated_at', models.DateTimeField(auto_now=True)),
('certificate_file', filer.fields.file.FilerFileField(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='filer.file', verbose_name='certificate_file')), ('certificate_file', filer.fields.file.FilerFileField(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='filer.file', verbose_name='certificate_file')),

30
apps/chat/migrations/0001_initial.py

@ -1,16 +1,8 @@
<<<<<<< HEAD
# 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
=======
# Generated by Django 5.1.8 on 2025-04-03 00:05 # Generated by Django 5.1.8 on 2025-04-03 00:05
import django.db.models.deletion import django.db.models.deletion
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
>>>>>>> develop
class Migration(migrations.Migration): class Migration(migrations.Migration):
@ -18,19 +10,12 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
<<<<<<< HEAD
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('course', '0004_auto_20241122_1913'),
=======
('course', '0001_initial'), ('course', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
>>>>>>> develop
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
<<<<<<< HEAD
=======
name='RoomMessage', name='RoomMessage',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
@ -46,29 +31,17 @@ class Migration(migrations.Migration):
], ],
), ),
migrations.CreateModel( migrations.CreateModel(
>>>>>>> develop
name='ChatMessage', name='ChatMessage',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('content', models.TextField(verbose_name='Message Content')), ('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_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)')), ('content_size', models.PositiveIntegerField(blank=True, null=True, verbose_name='Content Size (bytes)')),
<<<<<<< HEAD
('is_to_professor', models.BooleanField(default=False, verbose_name='Is to Professor')),
=======
('is_read', models.BooleanField(default=False, verbose_name='Is Read')), ('is_read', models.BooleanField(default=False, verbose_name='Is Read')),
>>>>>>> develop
('sent_at', models.DateTimeField(auto_now_add=True, verbose_name='Sent At')), ('sent_at', models.DateTimeField(auto_now_add=True, verbose_name='Sent At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')), ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')),
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='Deleted At')), ('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='Deleted At')),
('is_deleted', models.BooleanField(default=False, verbose_name='Is deleted')), ('is_deleted', models.BooleanField(default=False, verbose_name='Is deleted')),
<<<<<<< HEAD
('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')),
],
),
=======
('sender', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages_sent', to=settings.AUTH_USER_MODEL, verbose_name='Sender')), ('sender', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages_sent', to=settings.AUTH_USER_MODEL, verbose_name='Sender')),
('room', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='chat.roommessage', verbose_name='Room')), ('room', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='chat.roommessage', verbose_name='Room')),
], ],
@ -86,5 +59,4 @@ class Migration(migrations.Migration):
'unique_together': {('user', 'message')}, 'unique_together': {('user', 'message')},
}, },
), ),
>>>>>>> develop
]
]

4
apps/course/admin/__init__.py

@ -1,8 +1,4 @@
from .course import * from .course import *
from .lesson import * from .lesson import *
<<<<<<< HEAD
from .participant import *
=======
from .participant import * from .participant import *
from .live_session import * from .live_session import *
>>>>>>> develop

86
apps/course/admin/course.py

@ -1,38 +1,7 @@
<<<<<<< HEAD
=======
>>>>>>> develop
import os import os
import hashlib import hashlib
from django.contrib import admin from django.contrib import admin
<<<<<<< HEAD
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
@admin.register(CourseCategory)
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}),
=======
from django.contrib import messages from django.contrib import messages
from django import forms from django import forms
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -134,49 +103,10 @@ class CourseForm(forms.ModelForm):
'schema': get_course_feature_schema(), 'schema': get_course_feature_schema(),
'title': _('Course Features'), 'title': _('Course Features'),
}), }),
>>>>>>> develop
} }
help_texts = { help_texts = {
'status': 'If set to inactive, the course will not be displayed.', 'status': 'If set to inactive, the course will not be displayed.',
} }
<<<<<<< HEAD
# def __init__(self, *args, **kwargs):
# super(CourseForm, self).__init__(*args, **kwargs)
# # اضافه کردن help_text به فیلد status
# self.fields['status'].help_text = _(
# "If set to 'Inactive', this item will not be displayed."
# )
@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', )
# def has_change_permission(self, request, obj=None):
# return False
# @admin.display(description='Add Student')
# def _add_student(self, obj):
@admin.register(Glossary)
class GlossaryAdmin(admin.ModelAdmin):
list_display = ('title', 'course', 'description')
list_filter = ('course',)
search_fields = ('title', 'description', 'course__title')
ordering = ('-id',)
=======
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@ -543,7 +473,6 @@ class CourseGlossaryAdmin(CourseRelatedAdmin):
@admin.display(description=_("Description")) @admin.display(description=_("Description"))
def glossary_description(self, obj): def glossary_description(self, obj):
return obj.glossary.description return obj.glossary.description
>>>>>>> develop
class AttachmentAdminForm(forms.ModelForm): class AttachmentAdminForm(forms.ModelForm):
@ -576,32 +505,18 @@ class AttachmentAdminForm(forms.ModelForm):
return f"{base_part}{hash_part}{ext}" # ترکیب بخش اصلی و هش با پسوند return f"{base_part}{hash_part}{ext}" # ترکیب بخش اصلی و هش با پسوند
return file_name return file_name
<<<<<<< HEAD
@admin.register(Attachment)
class AttachmentAdmin(admin.ModelAdmin):
form = AttachmentAdminForm
list_display = ('title', 'course', 'file', 'file_size')
list_filter = ('course',)
search_fields = ('title', 'file', 'course__title')
=======
class AttachmentAdmin(AttachmentGlossaryBaseAdmin): class AttachmentAdmin(AttachmentGlossaryBaseAdmin):
form = AttachmentAdminForm form = AttachmentAdminForm
list_display = ('title', 'file', 'file_size') list_display = ('title', 'file', 'file_size')
search_fields = ('title', 'file') search_fields = ('title', 'file')
>>>>>>> develop
def save_model(self, request, obj, form, change): def save_model(self, request, obj, form, change):
if obj.file: if obj.file:
obj.file_size = obj.file.size obj.file_size = obj.file.size
super().save_model(request, obj, form, change) super().save_model(request, obj, form, change)
<<<<<<< HEAD
=======
def is_used_in_professor_courses(self, user, obj): def is_used_in_professor_courses(self, user, obj):
"""آیا این attachment در دوره‌های استاد استفاده شده؟""" """آیا این attachment در دوره‌های استاد استفاده شده؟"""
return obj.courseattachment_set.filter(course__professor=user).exists() return obj.courseattachment_set.filter(course__professor=user).exists()
@ -679,4 +594,3 @@ class HiddenCourseAdmin(ModelAdmin):
return False return False
dovoodi_admin_site.register(Course, HiddenCourseAdmin) dovoodi_admin_site.register(Course, HiddenCourseAdmin)
>>>>>>> develop

33
apps/course/admin/lesson.py

@ -1,29 +1,3 @@
<<<<<<< HEAD
from django.contrib import admin
from apps.course.models import Lesson, LessonCompletion
@admin.register(Lesson)
class LessonAdmin(admin.ModelAdmin):
list_display = ('title', 'course', 'priority', 'duration', 'content_type')
list_filter = ('course', 'content_type')
search_fields = ('title', 'course__title')
ordering = ('priority', 'title')
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')
=======
import os import os
from django.contrib import admin from django.contrib import admin
from django import forms from django import forms
@ -149,18 +123,12 @@ class LessonCompletionAdmin(ModelAdmin):
list_display = ('student', 'course_lesson', 'completed_at') list_display = ('student', 'course_lesson', 'completed_at')
search_fields = ('student__fullname', 'student__email', 'course_lesson__title', 'course_lesson__course__title') search_fields = ('student__fullname', 'student__email', 'course_lesson__title', 'course_lesson__course__title')
list_filter = ('course_lesson__course', 'completed_at') list_filter = ('course_lesson__course', 'completed_at')
>>>>>>> develop
ordering = ('-completed_at',) ordering = ('-completed_at',)
def get_readonly_fields(self, request, obj=None): def get_readonly_fields(self, request, obj=None):
""" """
Make fields readonly if the object already exists. Make fields readonly if the object already exists.
""" """
<<<<<<< HEAD
if obj:
return ['student', 'lesson', 'completed_at']
return []
=======
if obj: if obj:
return ['student', 'course_lesson', 'completed_at'] return ['student', 'course_lesson', 'completed_at']
return [] return []
@ -174,4 +142,3 @@ django_admin.site.register(Lesson, LessonAdmin)
project_admin_site.register(Lesson, LessonAdmin) project_admin_site.register(Lesson, LessonAdmin)
project_admin_site.register(CourseLesson, CourseLessonAdmin) project_admin_site.register(CourseLesson, CourseLessonAdmin)
project_admin_site.register(LessonCompletion, LessonCompletionAdmin) project_admin_site.register(LessonCompletion, LessonCompletionAdmin)
>>>>>>> develop

83
apps/course/migrations/0001_initial.py

@ -1,14 +1,3 @@
<<<<<<< HEAD
# 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
=======
# Generated by Django 5.1.8 on 2025-04-03 00:05 # Generated by Django 5.1.8 on 2025-04-03 00:05
import apps.course.models.course import apps.course.models.course
@ -18,7 +7,6 @@ import filer.fields.image
import utils.schema import utils.schema
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
>>>>>>> develop
class Migration(migrations.Migration): class Migration(migrations.Migration):
@ -26,18 +14,12 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
<<<<<<< HEAD
('account', '0003_auto_20241120_1741'),
=======
('account', '0001_initial'), ('account', '0001_initial'),
>>>>>>> develop
migrations.swappable_dependency(settings.FILER_IMAGE_MODEL), migrations.swappable_dependency(settings.FILER_IMAGE_MODEL),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
<<<<<<< HEAD
=======
name='CourseCategory', name='CourseCategory',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
@ -46,7 +28,6 @@ class Migration(migrations.Migration):
], ],
), ),
migrations.CreateModel( migrations.CreateModel(
>>>>>>> develop
name='Course', name='Course',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
@ -56,10 +37,7 @@ class Migration(migrations.Migration):
('video_file', models.FileField(blank=True, null=True, upload_to=apps.course.models.course.course_file_upload_to)), ('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')), ('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')), ('is_online', models.BooleanField(default=True, verbose_name='Is Online Course')),
<<<<<<< HEAD
=======
('online_link', models.CharField(blank=True, max_length=500, null=True, verbose_name='Online Class Link')), ('online_link', models.CharField(blank=True, max_length=500, null=True, verbose_name='Online Class Link')),
>>>>>>> develop
('level', models.CharField(choices=[('beginner', 'Beginner'), ('mid', 'Mid Level'), ('advanced', 'Advanced')], max_length=10, verbose_name='Course Level')), ('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)')), ('duration', models.PositiveIntegerField(verbose_name='Duration (in hours)')),
('lessons_count', models.PositiveIntegerField(verbose_name='Number of Lessons')), ('lessons_count', models.PositiveIntegerField(verbose_name='Number of Lessons')),
@ -72,14 +50,11 @@ class Migration(migrations.Migration):
('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')), ('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')), ('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')), ('features', models.JSONField(blank=True, default=dict, null=True, verbose_name='Course features')),
<<<<<<< HEAD
=======
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')), ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')), ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')),
('professor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='courses', to='account.professoruser')), ('professor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='courses', to='account.professoruser')),
('thumbnail', 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')), ('thumbnail', 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')),
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='courses', to='course.coursecategory', verbose_name='Category')), ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='courses', to='course.coursecategory', verbose_name='Category')),
>>>>>>> develop
], ],
options={ options={
'verbose_name': 'Course', 'verbose_name': 'Course',
@ -87,59 +62,6 @@ class Migration(migrations.Migration):
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
<<<<<<< HEAD
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(
=======
>>>>>>> develop
name='Attachment', name='Attachment',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
@ -154,8 +76,6 @@ class Migration(migrations.Migration):
'ordering': ('-id',), 'ordering': ('-id',),
}, },
), ),
<<<<<<< HEAD
=======
migrations.CreateModel( migrations.CreateModel(
name='Glossary', name='Glossary',
fields=[ fields=[
@ -212,5 +132,4 @@ class Migration(migrations.Migration):
'unique_together': {('student', 'course')}, 'unique_together': {('student', 'course')},
}, },
), ),
>>>>>>> develop
]
]

4
apps/course/models/__init__.py

@ -1,8 +1,4 @@
from .course import * from .course import *
from .lesson import * from .lesson import *
<<<<<<< HEAD
from .participant import *
=======
from .participant import * from .participant import *
from .live_session import * from .live_session import *
>>>>>>> develop

88
apps/course/models/course.py

@ -4,11 +4,6 @@ import math
from django.db import models from django.db import models
from django.db.models import TextChoices from django.db.models import TextChoices
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
<<<<<<< HEAD
from filer.fields.image import FilerImageField
from filer.fields.file import FilerFileField
=======
>>>>>>> develop
from apps.account.models import ProfessorUser from apps.account.models import ProfessorUser
from utils.schema import default_timing from utils.schema import default_timing
@ -20,16 +15,11 @@ def course_file_upload_to(instance, filename):
return os.path.join(f"courses/{instance.slug}/videos/{filename}") return os.path.join(f"courses/{instance.slug}/videos/{filename}")
<<<<<<< HEAD
def attachment_file_upload_to(instance, filename):
=======
def attachment_file_upload_to(instance, filename): def attachment_file_upload_to(instance, filename):
return os.path.join(f"attachments/{filename}") return os.path.join(f"attachments/{filename}")
def course_attachment_file_upload_to(instance, filename): def course_attachment_file_upload_to(instance, filename):
>>>>>>> develop
return os.path.join(f"courses/{instance.course.slug}/attachments/{filename}") return os.path.join(f"courses/{instance.course.slug}/attachments/{filename}")
@ -43,22 +33,14 @@ class CourseCategory(models.Model):
return self.name return self.name
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
<<<<<<< HEAD
self.slug = generate_slug_for_model(CourseCategory, self.name)
=======
if not self.slug: if not self.slug:
self.slug = generate_slug_for_model(CourseCategory, self.name) self.slug = generate_slug_for_model(CourseCategory, self.name)
>>>>>>> develop
super().save(*args, **kwargs) super().save(*args, **kwargs)
@property @property
def course_count(self): def course_count(self):
return self.courses.exclude(status="inactive").count() return self.courses.exclude(status="inactive").count()
<<<<<<< HEAD
=======
>>>>>>> develop
class Course(models.Model): class Course(models.Model):
class LevelChoices(TextChoices): class LevelChoices(TextChoices):
@ -74,13 +56,8 @@ class Course(models.Model):
FINISHED = 'finished', 'Finished' # Finished (course has ended)-закончился FINISHED = 'finished', 'Finished' # Finished (course has ended)-закончился
class VedioTypeChoices(models.TextChoices): class VedioTypeChoices(models.TextChoices):
<<<<<<< HEAD
VIDEO_FILE = 'video_file', 'Video File'
VIDEO_LINK = 'video_link', 'Video Link'
=======
YOUTUBE_LINK = 'youtube_link', 'Youtube Link' YOUTUBE_LINK = 'youtube_link', 'Youtube Link'
VIDEO_FILE = 'video_file', 'Video File' VIDEO_FILE = 'video_file', 'Video File'
>>>>>>> develop
title = models.CharField(max_length=255, verbose_name='Course Title') title = models.CharField(max_length=255, verbose_name='Course Title')
@ -92,34 +69,20 @@ class Course(models.Model):
related_name="courses" related_name="courses"
) )
<<<<<<< HEAD
thumbnail = FilerImageField(
related_name='+', on_delete=models.PROTECT, null=True, blank=True,
verbose_name=_('thumbnail')
)
video_type = models.CharField(max_length=20, choices=VedioTypeChoices.choices, verbose_name='Vedio Type')
=======
thumbnail = models.ImageField(upload_to="courses/thumbnails/", verbose_name=_('Thumbnail')) thumbnail = models.ImageField(upload_to="courses/thumbnails/", verbose_name=_('Thumbnail'))
video_type = models.CharField( video_type = models.CharField(
max_length=20, max_length=20,
choices=VedioTypeChoices.choices, choices=VedioTypeChoices.choices,
verbose_name='Preview Video Type (YouTube Link or File Upload)' verbose_name='Preview Video Type (YouTube Link or File Upload)'
) )
>>>>>>> develop
video_file = models.FileField( video_file = models.FileField(
upload_to=course_file_upload_to, upload_to=course_file_upload_to,
null=True, null=True,
blank=True blank=True
) )
<<<<<<< HEAD
video_link = models.CharField(max_length=500, null=True, blank=True, verbose_name='Video Link')
is_online = models.BooleanField(default=True, verbose_name='Is Online Course')
=======
video_link = models.CharField(max_length=500, null=True, blank=True) video_link = models.CharField(max_length=500, null=True, blank=True)
is_online = models.BooleanField(default=False, verbose_name='Is Online Course') is_online = models.BooleanField(default=False, verbose_name='Is Online Course')
>>>>>>> develop
online_link = models.CharField(max_length=500, null=True, blank=True, verbose_name='Online Class Link') online_link = models.CharField(max_length=500, null=True, blank=True, verbose_name='Online Class Link')
level = models.CharField(max_length=10, choices=LevelChoices.choices, verbose_name='Course Level') level = models.CharField(max_length=10, choices=LevelChoices.choices, verbose_name='Course Level')
duration = models.PositiveIntegerField(verbose_name='Duration (in hours)') duration = models.PositiveIntegerField(verbose_name='Duration (in hours)')
@ -136,11 +99,7 @@ class Course(models.Model):
help_text=_('This field is automatically calculated based on the discount percentage.') help_text=_('This field is automatically calculated based on the discount percentage.')
) )
<<<<<<< HEAD
timing = models.JSONField(blank=True, null=True, default=default_timing, verbose_name=_("Timing"), help_text=_("The Timing information in JSON format."))
=======
timing = models.JSONField(blank=True, null=True, default=default_timing, verbose_name=_("Timing")) timing = models.JSONField(blank=True, null=True, default=default_timing, verbose_name=_("Timing"))
>>>>>>> develop
features = models.JSONField(verbose_name=_('Course features'), default=dict, blank=True, null=True) 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")) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created at"))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At")) updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At"))
@ -157,11 +116,6 @@ class Course(models.Model):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
<<<<<<< HEAD
self.slug = generate_slug_for_model(Course, self.title)
if self.discount_percentage > 0:
=======
if not self.slug: if not self.slug:
self.slug = generate_slug_for_model(Course, self.title) self.slug = generate_slug_for_model(Course, self.title)
@ -175,7 +129,6 @@ class Course(models.Model):
self.discount_percentage = 0 self.discount_percentage = 0
self.final_price = Decimal('0.00') self.final_price = Decimal('0.00')
elif self.discount_percentage > 0: elif self.discount_percentage > 0:
>>>>>>> develop
discount_amount = (self.price * self.discount_percentage) / 100 discount_amount = (self.price * self.discount_percentage) / 100
final_price = self.price - discount_amount final_price = self.price - discount_amount
self.final_price = Decimal(math.ceil(final_price)).quantize(Decimal('0.00')) self.final_price = Decimal(math.ceil(final_price)).quantize(Decimal('0.00'))
@ -188,29 +141,6 @@ class Course(models.Model):
class Meta: class Meta:
verbose_name = "Course" verbose_name = "Course"
verbose_name_plural = "Courses" verbose_name_plural = "Courses"
<<<<<<< HEAD
class Glossary(models.Model):
course = models.ForeignKey(Course, on_delete=models.CASCADE, related_name='glossaries', verbose_name='Course')
title = models.CharField(max_length=555, verbose_name='Glossary Title')
description = models.TextField(verbose_name='Description')
def __str__(self):
return f"{self.course.title} - {self.title}"
class Meta:
ordering = ("-id",)
verbose_name = "Glossary"
verbose_name_plural = "Glossary"
class Attachment(models.Model):
course = models.ForeignKey(Course, on_delete=models.CASCADE, related_name='attachments', verbose_name='Course')
=======
indexes = [ indexes = [
models.Index(fields=['status']), models.Index(fields=['status']),
models.Index(fields=['is_free']), models.Index(fields=['is_free']),
@ -269,37 +199,20 @@ class Attachment(models.Model):
""" """
Base Attachment model that contains the actual file Base Attachment model that contains the actual file
""" """
>>>>>>> develop
title = models.CharField(max_length=255, verbose_name='Attachment Title') title = models.CharField(max_length=255, verbose_name='Attachment Title')
file = models.FileField( file = models.FileField(
upload_to=attachment_file_upload_to, upload_to=attachment_file_upload_to,
verbose_name='Attachment File' verbose_name='Attachment File'
) )
<<<<<<< HEAD
file_size = models.PositiveIntegerField(verbose_name='File Size (in bytes)', null=True, blank=True)
=======
file_size = models.PositiveIntegerField(verbose_name='File Size (in bytes)', null=True, blank=True) file_size = models.PositiveIntegerField(verbose_name='File Size (in bytes)', null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created at")) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created at"))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At")) updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At"))
>>>>>>> develop
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
# Calculate the file size before saving # Calculate the file size before saving
if self.file and not self.file_size: if self.file and not self.file_size:
self.file_size = self.file.size self.file_size = self.file.size
super().save(*args, **kwargs) super().save(*args, **kwargs)
<<<<<<< HEAD
def __str__(self):
return f"{self.course.title} - {self.title}"
class Meta:
ordering = ("-id",)
verbose_name = "Attachment"
verbose_name_plural = "Attachments"
=======
def __str__(self): def __str__(self):
return self.title return self.title
@ -341,4 +254,3 @@ class CourseAttachment(models.Model):
models.Index(fields=['course']), models.Index(fields=['course']),
models.Index(fields=['attachment']), models.Index(fields=['attachment']),
] ]
>>>>>>> develop

80
apps/course/models/lesson.py

@ -9,28 +9,11 @@ from apps.account.models import StudentUser
def lesson_file_upload_to(instance, filename): def lesson_file_upload_to(instance, filename):
<<<<<<< HEAD
return os.path.join(f"courses/{instance.course.slug}/lessons/{filename}")
=======
return os.path.join(f"lessons/{filename}") return os.path.join(f"lessons/{filename}")
>>>>>>> develop
class Lesson(models.Model): class Lesson(models.Model):
<<<<<<< HEAD
class ContentTypeChoices(models.TextChoices):
YOUTUBE_LINK = 'youtube_link', 'Youtube Link'
VIDEO_FILE = 'video_file', 'Video File'
AUDIO_FILE = 'audio_file', 'Audio File'
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)')
=======
""" """
Base Lesson model that contains the actual content (video file or link) Base Lesson model that contains the actual content (video file or link)
""" """
@ -39,7 +22,6 @@ class Lesson(models.Model):
VIDEO_FILE = 'video_file', 'Video File' VIDEO_FILE = 'video_file', 'Video File'
title = models.CharField(max_length=255, verbose_name='Lesson Title') title = models.CharField(max_length=255, verbose_name='Lesson Title')
>>>>>>> develop
content_type = models.CharField(max_length=50, choices=ContentTypeChoices.choices, verbose_name='Content Type') content_type = models.CharField(max_length=50, choices=ContentTypeChoices.choices, verbose_name='Content Type')
content_file = models.FileField( content_file = models.FileField(
null=True, null=True,
@ -47,15 +29,6 @@ class Lesson(models.Model):
upload_to=lesson_file_upload_to, upload_to=lesson_file_upload_to,
) )
video_link = models.CharField(max_length=500, null=True, blank=True, verbose_name='Link') video_link = models.CharField(max_length=500, null=True, blank=True, verbose_name='Link')
<<<<<<< HEAD
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}"
=======
duration = models.PositiveIntegerField(verbose_name='Duration (in minutes)') duration = models.PositiveIntegerField(verbose_name='Duration (in minutes)')
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created at")) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created at"))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At")) updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At"))
@ -87,16 +60,10 @@ class CourseLesson(models.Model):
def __str__(self): def __str__(self):
title = self.title or self.lesson.title title = self.title or self.lesson.title
return f"{self.course.title} - {title}" return f"{self.course.title} - {title}"
>>>>>>> develop
def is_completed_by(self, student): def is_completed_by(self, student):
return self.completions.filter(student=student).exists() return self.completions.filter(student=student).exists()
<<<<<<< HEAD
def save(self, *args, **kwargs):
print(f'---> start')
=======
@property @property
def content_type(self): def content_type(self):
return self.lesson.content_type return self.lesson.content_type
@ -118,53 +85,19 @@ class CourseLesson(models.Model):
if not self.title: if not self.title:
self.title = self.lesson.title self.title = self.lesson.title
>>>>>>> develop
if self.priority is None: if self.priority is None:
# If priority is not set, set it to the next available priority # If priority is not set, set it to the next available priority
max_priority = self.course.lessons.aggregate(max_priority=models.Max('priority'))['max_priority'] max_priority = self.course.lessons.aggregate(max_priority=models.Max('priority'))['max_priority']
self.priority = (max_priority or 0) + 1 self.priority = (max_priority or 0) + 1
<<<<<<< HEAD
else:
self._adjust_priorities()
super().save(*args, **kwargs)
=======
else: else:
self._adjust_priorities() self._adjust_priorities()
super().save(*args, **kwargs) super().save(*args, **kwargs)
>>>>>>> develop
def _adjust_priorities(self): def _adjust_priorities(self):
# Adjust priorities of other lessons in the course # Adjust priorities of other lessons in the course
lessons = self.course.lessons.exclude(pk=self.pk) lessons = self.course.lessons.exclude(pk=self.pk)
# Shift priorities for lessons with the same or higher priority # Shift priorities for lessons with the same or higher priority
lessons.filter(priority__gte=self.priority).update(priority=models.F('priority') + 1) lessons.filter(priority__gte=self.priority).update(priority=models.F('priority') + 1)
<<<<<<< HEAD
# # If priority is set, adjust the priorities of other lessons
# lessons = self.course.lessons.exclude(pk=self.pk).order_by('priority')
# updated_priorities = []
# inserted = False
# for lesson in lessons:
# if lesson.priority >= self.priority and not inserted:
# updated_priorities.append((self.priority, self))
# inserted = True
# updated_priorities.append((lesson.priority if not inserted else lesson.priority + 1, lesson))
# if not inserted:
# updated_priorities.append((self.priority, self))
# # Update priorities in bulk
# for priority, lesson in updated_priorities:
# lesson.priority = priority
# lesson.save(update_fields=['priority'])
=======
class Meta: class Meta:
verbose_name = "Course Lesson" verbose_name = "Course Lesson"
@ -178,7 +111,6 @@ class CourseLesson(models.Model):
models.Index(fields=['course', 'is_active']), models.Index(fields=['course', 'is_active']),
] ]
>>>>>>> develop
class LessonCompletion(models.Model): class LessonCompletion(models.Model):
student = models.ForeignKey( student = models.ForeignKey(
@ -186,13 +118,8 @@ class LessonCompletion(models.Model):
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name='lesson_completions' related_name='lesson_completions'
) )
<<<<<<< HEAD
lesson = models.ForeignKey(
Lesson,
=======
course_lesson = models.ForeignKey( course_lesson = models.ForeignKey(
CourseLesson, CourseLesson,
>>>>>>> develop
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name='completions' related_name='completions'
) )
@ -200,12 +127,6 @@ class LessonCompletion(models.Model):
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created at")) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created at"))
class Meta: class Meta:
<<<<<<< HEAD
unique_together = ('student', 'lesson')
def __str__(self):
return f"{self.student.fullname} - {self.lesson.title} - Completed"
=======
unique_together = ('student', 'course_lesson') unique_together = ('student', 'course_lesson')
indexes = [ indexes = [
models.Index(fields=['student']), models.Index(fields=['student']),
@ -216,6 +137,5 @@ class LessonCompletion(models.Model):
def __str__(self): def __str__(self):
return f"{self.student.fullname} - {self.course_lesson.title} - Completed" return f"{self.student.fullname} - {self.course_lesson.title} - Completed"
>>>>>>> develop

7
apps/course/models/participant.py

@ -17,17 +17,11 @@ class Participant(models.Model):
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name='participants' related_name='participants'
) )
<<<<<<< HEAD
=======
is_active = models.BooleanField(default=True) is_active = models.BooleanField(default=True)
>>>>>>> develop
joined_date = models.DateTimeField(auto_now_add=True) joined_date = models.DateTimeField(auto_now_add=True)
unread_messages_count = models.IntegerField(default=0) unread_messages_count = models.IntegerField(default=0)
class Meta: class Meta:
<<<<<<< HEAD
unique_together = ('student', 'course')
=======
unique_together = ('student', 'course') unique_together = ('student', 'course')
indexes = [ indexes = [
models.Index(fields=['student']), models.Index(fields=['student']),
@ -35,4 +29,3 @@ class Participant(models.Model):
models.Index(fields=['joined_date']), models.Index(fields=['joined_date']),
models.Index(fields=['student', 'course']), models.Index(fields=['student', 'course']),
] ]
>>>>>>> develop

4
apps/course/serializers/__init__.py

@ -1,9 +1,5 @@
from .course import * from .course import *
from .lesson import * from .lesson import *
<<<<<<< HEAD
from .participant import *
=======
from .participant import * from .participant import *
from .online import * from .online import *
from .professor import * from .professor import *
>>>>>>> develop

111
apps/course/serializers/course.py

@ -1,14 +1,8 @@
from rest_framework import serializers from rest_framework import serializers
<<<<<<< HEAD
from dj_filer.admin import get_thumbs
from apps.course.models import Course, CourseCategory, Attachment, Glossary, LessonCompletion, Participant, Lesson
=======
# from dj_filer.admin import get_thumbs # from dj_filer.admin import get_thumbs
from utils import get_thumbs from utils import get_thumbs
from apps.course.models import Course, CourseCategory, Attachment, Glossary, LessonCompletion, Participant, Lesson, CourseAttachment, CourseGlossary, CourseLesson from apps.course.models import Course, CourseCategory, Attachment, Glossary, LessonCompletion, Participant, Lesson, CourseAttachment, CourseGlossary, CourseLesson
>>>>>>> develop
from apps.chat.models import RoomMessage from apps.chat.models import RoomMessage
from apps.account.serializers import UserProfileSerializer from apps.account.serializers import UserProfileSerializer
@ -30,15 +24,11 @@ class CourseListSerializer(serializers.ModelSerializer):
thumbnail = serializers.SerializerMethodField() thumbnail = serializers.SerializerMethodField()
participant_count = serializers.SerializerMethodField() participant_count = serializers.SerializerMethodField()
lessons_count = serializers.SerializerMethodField() lessons_count = serializers.SerializerMethodField()
<<<<<<< HEAD
=======
price = serializers.SerializerMethodField() price = serializers.SerializerMethodField()
discount_percentage = serializers.SerializerMethodField() discount_percentage = serializers.SerializerMethodField()
final_price = serializers.SerializerMethodField() final_price = serializers.SerializerMethodField()
is_free = serializers.SerializerMethodField() is_free = serializers.SerializerMethodField()
>>>>>>> develop
class Meta: class Meta:
model = Course model = Course
fields = [ fields = [
@ -68,13 +58,6 @@ class CourseListSerializer(serializers.ModelSerializer):
return obj.participants.count() return obj.participants.count()
def get_lessons_count(self, obj): def get_lessons_count(self, obj):
<<<<<<< HEAD
lessons_count = obj.lessons.filter(is_active=True).count()
return max(lessons_count, obj.lessons_count)
=======
# Use prefetched lessons if available # Use prefetched lessons if available
if hasattr(obj, 'lessons') and obj.lessons.all(): if hasattr(obj, 'lessons') and obj.lessons.all():
lessons_count = sum(1 for lesson in obj.lessons.all() if lesson.is_active) lessons_count = sum(1 for lesson in obj.lessons.all() if lesson.is_active)
@ -100,16 +83,11 @@ class CourseListSerializer(serializers.ModelSerializer):
def get_is_free(self, obj): def get_is_free(self, obj):
return obj.is_free or obj.price == 0 return obj.is_free or obj.price == 0
>>>>>>> develop
class CourseDetailSerializer(serializers.ModelSerializer): class CourseDetailSerializer(serializers.ModelSerializer):
category = CourseCategorySerializer() category = CourseCategorySerializer()
<<<<<<< HEAD
professor = UserProfileSerializer()
=======
professor = serializers.SerializerMethodField() professor = serializers.SerializerMethodField()
>>>>>>> develop
thumbnail = serializers.SerializerMethodField() thumbnail = serializers.SerializerMethodField()
participant_count = serializers.SerializerMethodField() participant_count = serializers.SerializerMethodField()
access = serializers.SerializerMethodField() access = serializers.SerializerMethodField()
@ -117,9 +95,6 @@ class CourseDetailSerializer(serializers.ModelSerializer):
lessons_count = serializers.SerializerMethodField() lessons_count = serializers.SerializerMethodField()
last_lesson_id = serializers.SerializerMethodField() last_lesson_id = serializers.SerializerMethodField()
room_id = serializers.SerializerMethodField() room_id = serializers.SerializerMethodField()
<<<<<<< HEAD
=======
user_transaction_status = serializers.SerializerMethodField() user_transaction_status = serializers.SerializerMethodField()
price = serializers.SerializerMethodField() price = serializers.SerializerMethodField()
discount_percentage = serializers.SerializerMethodField() discount_percentage = serializers.SerializerMethodField()
@ -127,7 +102,6 @@ class CourseDetailSerializer(serializers.ModelSerializer):
is_free = serializers.SerializerMethodField() is_free = serializers.SerializerMethodField()
is_professor = serializers.SerializerMethodField() is_professor = serializers.SerializerMethodField()
>>>>>>> develop
class Meta: class Meta:
model = Course model = Course
fields = [ fields = [
@ -138,10 +112,7 @@ class CourseDetailSerializer(serializers.ModelSerializer):
'access', 'access',
'participant_count', 'participant_count',
'professor', 'professor',
<<<<<<< HEAD
=======
'is_professor', 'is_professor',
>>>>>>> develop
'thumbnail', 'thumbnail',
'video_type', 'video_type',
'video_file', 'video_file',
@ -163,11 +134,6 @@ class CourseDetailSerializer(serializers.ModelSerializer):
'features', 'features',
'last_lesson_id', 'last_lesson_id',
'room_id', 'room_id',
<<<<<<< HEAD
]
def get_room_id(self, obj):
=======
'user_transaction_status' 'user_transaction_status'
] ]
@ -176,14 +142,11 @@ class CourseDetailSerializer(serializers.ModelSerializer):
if hasattr(obj, 'room_messages') and obj.room_messages.all(): if hasattr(obj, 'room_messages') and obj.room_messages.all():
return obj.room_messages.first().id return obj.room_messages.first().id
# Fallback to direct query if not prefetched # Fallback to direct query if not prefetched
>>>>>>> develop
room_message = RoomMessage.objects.filter(course=obj).first() room_message = RoomMessage.objects.filter(course=obj).first()
if room_message: if room_message:
return room_message.id return room_message.id
return None return None
<<<<<<< HEAD
=======
def get_user_transaction_status(self, obj): def get_user_transaction_status(self, obj):
from apps.transaction.models import TransactionParticipant from apps.transaction.models import TransactionParticipant
if student := self._get_authenticated_user(): if student := self._get_authenticated_user():
@ -196,29 +159,10 @@ class CourseDetailSerializer(serializers.ModelSerializer):
return latest_transaction.status return latest_transaction.status
return None return None
>>>>>>> develop
def get_last_lesson_id(self, obj): def get_last_lesson_id(self, obj):
request = self.context.get('request') request = self.context.get('request')
if request and request.user.is_authenticated: if request and request.user.is_authenticated:
user = request.user user = request.user
<<<<<<< HEAD
# آخرین درس تکمیل‌شده توسط کاربر
last_completed_lesson = LessonCompletion.objects.filter(
student=user,
lesson__course=obj
).order_by('-completed_at').first()
if last_completed_lesson:
# پیدا کردن درس بعدی بر اساس priority
next_lesson = Lesson.objects.filter(
course=obj,
priority__gt=last_completed_lesson.lesson.priority,
is_active=True
).order_by('priority').first()
if not next_lesson:
next_lesson = Lesson.objects.filter(
=======
# Use prefetched lessons if available # Use prefetched lessons if available
if hasattr(obj, 'lessons') and obj.lessons.all(): if hasattr(obj, 'lessons') and obj.lessons.all():
@ -257,16 +201,11 @@ class CourseDetailSerializer(serializers.ModelSerializer):
).order_by('priority').first() ).order_by('priority').first()
if not next_lesson: if not next_lesson:
next_lesson = CourseLesson.objects.filter( next_lesson = CourseLesson.objects.filter(
>>>>>>> develop
course=obj, course=obj,
is_active=True is_active=True
).order_by('priority').first() ).order_by('priority').first()
if next_lesson: if next_lesson:
<<<<<<< HEAD
return next_lesson.id
=======
return next_lesson.id return next_lesson.id
>>>>>>> develop
return None return None
@ -277,15 +216,12 @@ class CourseDetailSerializer(serializers.ModelSerializer):
return False return False
return True return True
return False return False
<<<<<<< HEAD
=======
def get_professor(self, obj): def get_professor(self, obj):
"""Return the course professor's profile using UserProfileSerializer""" """Return the course professor's profile using UserProfileSerializer"""
if obj.professor: if obj.professor:
return UserProfileSerializer(obj.professor, context=self.context).data return UserProfileSerializer(obj.professor, context=self.context).data
return None return None
>>>>>>> develop
def get_is_professor(self, obj): def get_is_professor(self, obj):
if professor := self._get_authenticated_user(): if professor := self._get_authenticated_user():
@ -293,14 +229,11 @@ class CourseDetailSerializer(serializers.ModelSerializer):
return False return False
def get_lessons_count(self, obj): def get_lessons_count(self, obj):
<<<<<<< HEAD
=======
# Use prefetched lessons if available # Use prefetched lessons if available
if hasattr(obj, 'lessons') and obj.lessons.all(): if hasattr(obj, 'lessons') and obj.lessons.all():
lessons_count = sum(1 for lesson in obj.lessons.all() if lesson.is_active) lessons_count = sum(1 for lesson in obj.lessons.all() if lesson.is_active)
return max(lessons_count, obj.lessons_count) return max(lessons_count, obj.lessons_count)
# Fallback to direct query # Fallback to direct query
>>>>>>> develop
lessons_count = obj.lessons.filter(is_active=True).count() lessons_count = obj.lessons.filter(is_active=True).count()
return max(lessons_count, obj.lessons_count) return max(lessons_count, obj.lessons_count)
@ -309,14 +242,10 @@ class CourseDetailSerializer(serializers.ModelSerializer):
if student := self._get_authenticated_user(): if student := self._get_authenticated_user():
if not self._is_participant(student, obj): if not self._is_participant(student, obj):
return None return None
<<<<<<< HEAD
return self._get_completed_lessons_count(student, obj)
=======
completed_count = self._get_completed_lessons_count(student, obj) completed_count = self._get_completed_lessons_count(student, obj)
# Ensure completed count doesn't exceed total lessons count # Ensure completed count doesn't exceed total lessons count
total_lessons = self.get_lessons_count(obj) total_lessons = self.get_lessons_count(obj)
return min(completed_count, total_lessons) return min(completed_count, total_lessons)
>>>>>>> develop
return None return None
def _is_participant(self, student, course): def _is_participant(self, student, course):
@ -330,11 +259,6 @@ class CourseDetailSerializer(serializers.ModelSerializer):
def _get_completed_lessons_count(self, student, course): def _get_completed_lessons_count(self, student, course):
"""Helper method to count completed lessons for the student in the given course.""" """Helper method to count completed lessons for the student in the given course."""
<<<<<<< HEAD
return LessonCompletion.objects.filter(
student=student,
lesson__course=course
=======
# Use prefetched completions if available # Use prefetched completions if available
if hasattr(course, 'lessons') and course.lessons.all(): if hasattr(course, 'lessons') and course.lessons.all():
completed_count = 0 completed_count = 0
@ -348,7 +272,6 @@ class CourseDetailSerializer(serializers.ModelSerializer):
return LessonCompletion.objects.filter( return LessonCompletion.objects.filter(
student=student, student=student,
course_lesson__course=course course_lesson__course=course
>>>>>>> develop
).count() ).count()
@ -356,9 +279,6 @@ class CourseDetailSerializer(serializers.ModelSerializer):
return get_thumbs(obj.thumbnail, self.context.get('request')) return get_thumbs(obj.thumbnail, self.context.get('request'))
def get_participant_count(self, obj): def get_participant_count(self, obj):
<<<<<<< HEAD
return obj.participants.count()
=======
# Use prefetched participants if available # Use prefetched participants if available
if hasattr(obj, 'participants') and obj.participants.all(): if hasattr(obj, 'participants') and obj.participants.all():
return len(obj.participants.all()) return len(obj.participants.all())
@ -381,25 +301,17 @@ class CourseDetailSerializer(serializers.ModelSerializer):
return str(obj.final_price) return str(obj.final_price)
def get_is_free(self, obj): def get_is_free(self, obj):
return obj.is_free or obj.price == 0 return obj.is_free or obj.price == 0
>>>>>>> develop
class MyCourseListSerializer(serializers.ModelSerializer): class MyCourseListSerializer(serializers.ModelSerializer):
category = CourseCategorySerializer() category = CourseCategorySerializer()
thumbnail = serializers.SerializerMethodField() thumbnail = serializers.SerializerMethodField()
<<<<<<< HEAD
lessons_complated_count = serializers.SerializerMethodField()
class Meta:
model = Course
=======
lessons_count = serializers.SerializerMethodField() lessons_count = serializers.SerializerMethodField()
lessons_complated_count = serializers.SerializerMethodField() lessons_complated_count = serializers.SerializerMethodField()
class Meta: class Meta:
model = Course model = Course
>>>>>>> develop
fields = [ fields = [
'id', 'id',
'title', 'title',
@ -414,9 +326,6 @@ class MyCourseListSerializer(serializers.ModelSerializer):
def get_thumbnail(self, obj): def get_thumbnail(self, obj):
return get_thumbs(obj.thumbnail, self.context.get('request')) return get_thumbs(obj.thumbnail, self.context.get('request'))
<<<<<<< HEAD
=======
def get_lessons_count(self, obj): def get_lessons_count(self, obj):
"""Get the actual count of active lessons""" """Get the actual count of active lessons"""
@ -428,30 +337,22 @@ class MyCourseListSerializer(serializers.ModelSerializer):
lessons_count = obj.lessons.filter(is_active=True).count() lessons_count = obj.lessons.filter(is_active=True).count()
return max(lessons_count, obj.lessons_count) return max(lessons_count, obj.lessons_count)
>>>>>>> develop
def get_lessons_complated_count(self, obj): def get_lessons_complated_count(self, obj):
if student := self._get_authenticated_user(): if student := self._get_authenticated_user():
if not self._is_participant(student, obj): if not self._is_participant(student, obj):
return None return None
<<<<<<< HEAD
return self._get_completed_lessons_count(student, obj)
=======
completed_count = self._get_completed_lessons_count(student, obj) completed_count = self._get_completed_lessons_count(student, obj)
# Ensure completed count doesn't exceed total lessons count # Ensure completed count doesn't exceed total lessons count
total_lessons = self.get_lessons_count(obj) total_lessons = self.get_lessons_count(obj)
return min(completed_count, total_lessons) return min(completed_count, total_lessons)
>>>>>>> develop
return None return None
def _is_participant(self, student, course): def _is_participant(self, student, course):
"""Helper method to check if a student is a participant in the given course.""" """Helper method to check if a student is a participant in the given course."""
<<<<<<< HEAD
=======
# اگر کاربر استاد دوره است، دسترسی کامل دارد # اگر کاربر استاد دوره است، دسترسی کامل دارد
if course.professor == student: if course.professor == student:
return True return True
# در غیر این صورت چک می‌کنیم که آیا participant است یا خیر # در غیر این صورت چک می‌کنیم که آیا participant است یا خیر
>>>>>>> develop
return Participant.objects.filter(student=student, course=course).exists() return Participant.objects.filter(student=student, course=course).exists()
def _get_authenticated_user(self): def _get_authenticated_user(self):
@ -461,11 +362,6 @@ class MyCourseListSerializer(serializers.ModelSerializer):
def _get_completed_lessons_count(self, student, course): def _get_completed_lessons_count(self, student, course):
"""Helper method to count completed lessons for the student in the given course.""" """Helper method to count completed lessons for the student in the given course."""
<<<<<<< HEAD
return LessonCompletion.objects.filter(
student=student,
lesson__course=course
=======
# Use prefetched completions if available # Use prefetched completions if available
if hasattr(course, 'lessons') and course.lessons.all(): if hasattr(course, 'lessons') and course.lessons.all():
completed_count = 0 completed_count = 0
@ -479,7 +375,6 @@ class MyCourseListSerializer(serializers.ModelSerializer):
return LessonCompletion.objects.filter( return LessonCompletion.objects.filter(
student=student, student=student,
course_lesson__course=course course_lesson__course=course
>>>>>>> develop
).count() ).count()
@ -487,8 +382,6 @@ class AttachmentSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Attachment model = Attachment
fields = ['id', 'title', 'file', 'file_size'] fields = ['id', 'title', 'file', 'file_size']
<<<<<<< HEAD
=======
class CourseAttachmentSerializer(serializers.ModelSerializer): class CourseAttachmentSerializer(serializers.ModelSerializer):
@ -499,14 +392,11 @@ class CourseAttachmentSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = CourseAttachment model = CourseAttachment
fields = ['id', 'title', 'file', 'file_size'] fields = ['id', 'title', 'file', 'file_size']
>>>>>>> develop
class GlossarySerializer(serializers.ModelSerializer): class GlossarySerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Glossary model = Glossary
<<<<<<< HEAD
=======
fields = ['id', 'title', 'description'] fields = ['id', 'title', 'description']
@ -516,5 +406,4 @@ class CourseGlossarySerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = CourseGlossary model = CourseGlossary
>>>>>>> develop
fields = ['id', 'title', 'description'] fields = ['id', 'title', 'description']

41
apps/course/serializers/lesson.py

@ -1,21 +1,4 @@
from rest_framework import serializers from rest_framework import serializers
<<<<<<< HEAD
from apps.course.models import Lesson, Participant, LessonCompletion
from apps.quiz.serializers import QuizListSerializer
class LessonSerializer(serializers.ModelSerializer):
is_complated = serializers.SerializerMethodField()
quizs = serializers.SerializerMethodField()
permission = serializers.SerializerMethodField()
class Meta:
model = Lesson
fields = ['id', 'title', 'priority', 'is_active', 'permission','duration', 'content_type', 'content_file', 'video_link', 'is_complated', 'quizs']
=======
from apps.course.models import Lesson, CourseLesson, Participant, LessonCompletion from apps.course.models import Lesson, CourseLesson, Participant, LessonCompletion
from apps.quiz.serializers import QuizListSerializer from apps.quiz.serializers import QuizListSerializer
@ -38,7 +21,6 @@ class CourseLessonSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = CourseLesson model = CourseLesson
fields = ['id', 'title', 'priority', 'is_active', 'permission', 'duration', 'content_type', 'content_file', 'video_link', 'is_complated', 'quizs'] fields = ['id', 'title', 'priority', 'is_active', 'permission', 'duration', 'content_type', 'content_file', 'video_link', 'is_complated', 'quizs']
>>>>>>> develop
def get_permission(self, obj): def get_permission(self, obj):
if student := self._get_authenticated_user(): if student := self._get_authenticated_user():
@ -59,10 +41,6 @@ class CourseLessonSerializer(serializers.ModelSerializer):
def get_is_complated(self, obj): def get_is_complated(self, obj):
request = self.context.get('request') request = self.context.get('request')
if not request or not request.user.is_authenticated: if not request or not request.user.is_authenticated:
<<<<<<< HEAD
return False
user = request.user
=======
return False return False
user = request.user user = request.user
@ -71,28 +49,10 @@ class CourseLessonSerializer(serializers.ModelSerializer):
return any(completion.student_id == user.id for completion in obj.completions.all()) return any(completion.student_id == user.id for completion in obj.completions.all())
# Fallback to direct queries # Fallback to direct queries
>>>>>>> develop
is_participant = Participant.objects.filter( is_participant = Participant.objects.filter(
student=user, student=user,
course=obj.course course=obj.course
).exists() ).exists()
<<<<<<< HEAD
if not is_participant:
return False
return LessonCompletion.objects.filter(
student=user,
lesson=obj
).exists()
def get_quizs(self, obj):
quizzes = obj.quizzes.all() # استفاده از related_name 'quizzes' برای دسترسی به کوییزهای درس
if quizzes.exists():
return QuizListSerializer(quizzes, many=True, context=self.context).data
return None
=======
if not is_participant: if not is_participant:
return False return False
@ -109,4 +69,3 @@ class CourseLessonSerializer(serializers.ModelSerializer):
if quizzes: if quizzes:
return QuizListSerializer(quizzes, many=True, context=self.context).data return QuizListSerializer(quizzes, many=True, context=self.context).data
return None return None
>>>>>>> develop

4
apps/course/views/__init__.py

@ -1,10 +1,6 @@
from .course import * from .course import *
from .lesson import * from .lesson import *
<<<<<<< HEAD
from .participant import *
=======
from .participant import * from .participant import *
from .professor import * from .professor import *
from .live_session import * from .live_session import *
from .webhook import * from .webhook import *
>>>>>>> develop

104
apps/course/views/course.py

@ -1,12 +1,3 @@
<<<<<<< HEAD
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 django.conf import settings from django.conf import settings
import logging import logging
@ -26,17 +17,10 @@ from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response from rest_framework.response import Response
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
>>>>>>> develop
from apps.course.serializers import ( from apps.course.serializers import (
CourseListSerializer, CourseCategorySerializer, CourseDetailSerializer, CourseListSerializer, CourseCategorySerializer, CourseDetailSerializer,
<<<<<<< HEAD
AttachmentSerializer, GlossarySerializer, MyCourseListSerializer
)
from apps.course.models import Course, CourseCategory, Attachment, Glossary, Participant
from apps.course.doc import *
=======
CourseAttachmentSerializer, CourseGlossarySerializer, MyCourseListSerializer, CourseAttachmentSerializer, CourseGlossarySerializer, MyCourseListSerializer,
OnlineClassTokenCreateSerializer, OnlineClassTokenVerifySerializer OnlineClassTokenCreateSerializer, OnlineClassTokenVerifySerializer
) )
@ -57,7 +41,6 @@ from utils.redis import OnlineClassTokenManager
UserModel = get_user_model() UserModel = get_user_model()
>>>>>>> develop
class CourseCategoryAPIView(ListAPIView): class CourseCategoryAPIView(ListAPIView):
@ -66,10 +49,7 @@ class CourseCategoryAPIView(ListAPIView):
@swagger_auto_schema( @swagger_auto_schema(
operation_description=doc_course_category(), operation_description=doc_course_category(),
<<<<<<< HEAD
=======
tags=["Imam-Javad - Course"] tags=["Imam-Javad - Course"]
>>>>>>> develop
) )
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
@ -77,22 +57,6 @@ class CourseCategoryAPIView(ListAPIView):
<<<<<<< HEAD
class CourseListAPIView(ListAPIView):
queryset = Course.objects.all().exclude(status=Course.StatusChoices.INACTIVE)
serializer_class = CourseListSerializer
filter_backends = [SearchFilter]
search_fields = ['title']
@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()]
=======
from utils.pagination import StandardResultsSetPagination from utils.pagination import StandardResultsSetPagination
class CourseListAPIView(ListAPIView): class CourseListAPIView(ListAPIView):
@ -115,16 +79,11 @@ class CourseListAPIView(ListAPIView):
description="Category of the Course", description="Category of the Course",
type=openapi.TYPE_STRING, type=openapi.TYPE_STRING,
# enum=[category.slug for category in CourseCategory.objects.all()] # enum=[category.slug for category in CourseCategory.objects.all()]
>>>>>>> develop
), ),
openapi.Parameter( openapi.Parameter(
'status', openapi.IN_QUERY, 'status', openapi.IN_QUERY,
type=openapi.TYPE_STRING, type=openapi.TYPE_STRING,
<<<<<<< HEAD
description="""Status =>
=======
description="""Status => description="""Status =>
>>>>>>> develop
Upcoming (visible but registration not allowed)---Предстоящие Upcoming (visible but registration not allowed)---Предстоящие
Registering (registration is open)---регистрация Registering (registration is open)---регистрация
Ongoing (course has started, registration closed)---Впроцессе Ongoing (course has started, registration closed)---Впроцессе
@ -144,15 +103,6 @@ class CourseListAPIView(ListAPIView):
), ),
]) ])
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
<<<<<<< HEAD
return super().get(request, *args, **kwargs)
def get_queryset(self):
queryset = super().get_queryset()
request = self.request
filters = request.query_params
=======
return self.list(request, *args, **kwargs) return self.list(request, *args, **kwargs)
def get_queryset(self): def get_queryset(self):
@ -167,7 +117,6 @@ class CourseListAPIView(ListAPIView):
request = self.request request = self.request
filters = request.query_params filters = request.query_params
>>>>>>> develop
# Handle category_slug with multiple values separated by commas # Handle category_slug with multiple values separated by commas
if category_slugs := filters.get('category_slug'): if category_slugs := filters.get('category_slug'):
category_slugs_list = category_slugs.split(',') category_slugs_list = category_slugs.split(',')
@ -177,11 +126,7 @@ class CourseListAPIView(ListAPIView):
if statuses := filters.get('status'): if statuses := filters.get('status'):
statuses_list = statuses.split(',') statuses_list = statuses.split(',')
queryset = queryset.filter(status__in=statuses_list) queryset = queryset.filter(status__in=statuses_list)
<<<<<<< HEAD
=======
>>>>>>> develop
if is_free := filters.get('is_free'): if is_free := filters.get('is_free'):
is_free = is_free.lower() == 'true' is_free = is_free.lower() == 'true'
queryset = queryset.filter( queryset = queryset.filter(
@ -190,11 +135,7 @@ class CourseListAPIView(ListAPIView):
if is_online := filters.get('is_online'): if is_online := filters.get('is_online'):
is_online = is_online.lower() == 'true' is_online = is_online.lower() == 'true'
queryset = queryset.filter(is_online=is_online) queryset = queryset.filter(is_online=is_online)
<<<<<<< HEAD
=======
>>>>>>> develop
return queryset return queryset
@ -202,17 +143,10 @@ class CourseListAPIView(ListAPIView):
class CourseDetailAPIView(RetrieveAPIView): class CourseDetailAPIView(RetrieveAPIView):
<<<<<<< HEAD
queryset = Course.objects.all()
=======
>>>>>>> develop
serializer_class = CourseDetailSerializer serializer_class = CourseDetailSerializer
lookup_field = "slug" lookup_field = "slug"
@swagger_auto_schema( @swagger_auto_schema(
<<<<<<< HEAD
operation_description=doc_course_detail(),
=======
tags=["Imam-Javad - Course"], tags=["Imam-Javad - Course"],
operation_description="Get detailed information about a specific course", operation_description="Get detailed information about a specific course",
responses={ responses={
@ -244,7 +178,6 @@ class CourseDetailAPIView(RetrieveAPIView):
@swagger_auto_schema( @swagger_auto_schema(
operation_description=doc_course_detail(), operation_description=doc_course_detail(),
tags=['Imam-Javad - Course'], tags=['Imam-Javad - Course'],
>>>>>>> develop
) )
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
@ -267,10 +200,7 @@ class MyCourseListAPIView(ListAPIView):
], ],
operation_description=doc_courses_my_courses(), operation_description=doc_courses_my_courses(),
operation_summary="Home", operation_summary="Home",
<<<<<<< HEAD
=======
tags=['Imam-Javad - Course'] tags=['Imam-Javad - Course']
>>>>>>> develop
) )
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
@ -278,9 +208,6 @@ class MyCourseListAPIView(ListAPIView):
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
def get_queryset(self): def get_queryset(self):
<<<<<<< HEAD
queryset = Course.objects.exclude(status=Course.StatusChoices.INACTIVE)
=======
""" """
Optimized queryset for user's courses with select_related and prefetch_related Optimized queryset for user's courses with select_related and prefetch_related
""" """
@ -293,7 +220,6 @@ class MyCourseListAPIView(ListAPIView):
'participants__student' 'participants__student'
).exclude(status=Course.StatusChoices.INACTIVE) ).exclude(status=Course.StatusChoices.INACTIVE)
>>>>>>> develop
request = self.request request = self.request
filters = request.query_params filters = request.query_params
student = self.request.user student = self.request.user
@ -334,16 +260,10 @@ class MyCourseListAPIView(ListAPIView):
class AttachmentListAPIView(ListAPIView): class AttachmentListAPIView(ListAPIView):
<<<<<<< HEAD
serializer_class = AttachmentSerializer
@swagger_auto_schema(
=======
serializer_class = CourseAttachmentSerializer serializer_class = CourseAttachmentSerializer
@swagger_auto_schema( @swagger_auto_schema(
tags=['Imam-Javad - Course'], tags=['Imam-Javad - Course'],
>>>>>>> develop
manual_parameters=[ manual_parameters=[
openapi.Parameter( openapi.Parameter(
'slug', openapi.IN_PATH, 'slug', openapi.IN_PATH,
@ -358,37 +278,23 @@ class AttachmentListAPIView(ListAPIView):
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
def get_queryset(self): def get_queryset(self):
<<<<<<< HEAD
=======
""" """
Optimized queryset with select_related for attachment relationship Optimized queryset with select_related for attachment relationship
""" """
>>>>>>> develop
course_slug = self.kwargs.get('slug') course_slug = self.kwargs.get('slug')
try: try:
course = Course.objects.get(slug=course_slug) course = Course.objects.get(slug=course_slug)
except Course.DoesNotExist: except Course.DoesNotExist:
raise NotFound("Course not found") raise NotFound("Course not found")
<<<<<<< HEAD
return Attachment.objects.filter(course=course)
=======
return CourseAttachment.objects.select_related( return CourseAttachment.objects.select_related(
'course', 'course',
'attachment' 'attachment'
).filter(course=course) ).filter(course=course)
>>>>>>> develop
class GlossaryListAPIView(ListAPIView): class GlossaryListAPIView(ListAPIView):
<<<<<<< HEAD
serializer_class = GlossarySerializer
filter_backends = [SearchFilter]
search_fields = ['title', 'description']
def get_queryset(self):
=======
serializer_class = CourseGlossarySerializer serializer_class = CourseGlossarySerializer
filter_backends = [SearchFilter] filter_backends = [SearchFilter]
search_fields = ['glossary__title', 'glossary__description'] search_fields = ['glossary__title', 'glossary__description']
@ -424,19 +330,12 @@ class GlossaryListAPIView(ListAPIView):
""" """
Optimized queryset with select_related for glossary relationship Optimized queryset with select_related for glossary relationship
""" """
>>>>>>> develop
course_slug = self.kwargs.get('slug') course_slug = self.kwargs.get('slug')
try: try:
course = Course.objects.get(slug=course_slug) course = Course.objects.get(slug=course_slug)
except Course.DoesNotExist: except Course.DoesNotExist:
raise NotFound("Course not found") raise NotFound("Course not found")
<<<<<<< HEAD
return Glossary.objects.filter(course=course)
=======
return CourseGlossary.objects.select_related( return CourseGlossary.objects.select_related(
'course', 'course',
'glossary' 'glossary'
@ -772,5 +671,4 @@ class CourseOnlineClassTokenValidateAPIView(GenericAPIView):
) )
if updated_count > 0: if updated_count > 0:
logger.info(f"[Room Sync] User sessions closed - session_id={session.id} count={updated_count}")
>>>>>>> develop
logger.info(f"[Room Sync] User sessions closed - session_id={session.id} count={updated_count}")

122
apps/course/views/lesson.py

@ -7,15 +7,9 @@ from rest_framework import status
from rest_framework.response import Response from rest_framework.response import Response
from apps.course.serializers import ( from apps.course.serializers import (
<<<<<<< HEAD
LessonSerializer
)
from apps.course.models import Course, Lesson, LessonCompletion
=======
CourseLessonSerializer CourseLessonSerializer
) )
from apps.course.models import Course, CourseLesson, LessonCompletion from apps.course.models import Course, CourseLesson, LessonCompletion
>>>>>>> develop
from apps.course.doc import * from apps.course.doc import *
from utils.exceptions import AppAPIException from utils.exceptions import AppAPIException
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
@ -23,33 +17,16 @@ from rest_framework.permissions import IsAuthenticated
class LessonListView(ListAPIView): class LessonListView(ListAPIView):
<<<<<<< HEAD
serializer_class = LessonSerializer
queryset = Lesson.objects.filter(is_active=True)
@swagger_auto_schema(
operation_description=doc_courses_lesson(),
=======
serializer_class = CourseLessonSerializer serializer_class = CourseLessonSerializer
@swagger_auto_schema( @swagger_auto_schema(
operation_description=doc_courses_lesson(), operation_description=doc_courses_lesson(),
tags=['Imam-Javad - Course'], tags=['Imam-Javad - Course'],
>>>>>>> develop
) )
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
def get_queryset(self): def get_queryset(self):
<<<<<<< HEAD
course_slug = self.kwargs.get('slug')
course = get_object_or_404(Course, slug=course_slug)
course = Course.objects.filter(slug=course_slug).first()
if not course:
raise AppAPIException({"message": "course not found"}, status_code=status.HTTP_404_NOT_FOUND)
return self.queryset.filter(course=course).order_by('priority','id')
=======
""" """
Optimized queryset with select_related and prefetch_related for lesson relationships Optimized queryset with select_related and prefetch_related for lesson relationships
""" """
@ -66,31 +43,11 @@ class LessonListView(ListAPIView):
course=course, course=course,
is_active=True is_active=True
).order_by('priority', 'id') ).order_by('priority', 'id')
>>>>>>> develop
class LessonDetailView(RetrieveAPIView): class LessonDetailView(RetrieveAPIView):
<<<<<<< HEAD
serializer_class = LessonSerializer
def get(self, request, *args, **kwargs):
lesson_id = self.kwargs.get('id')
lesson = get_object_or_404(Lesson, id=lesson_id, is_active=True)
course = lesson.course
lessons = Lesson.objects.filter(course=course, is_active=True).order_by('priority')
total_lessons = lessons.count()
current_lesson_number = list(lessons.values_list('id', flat=True)).index(lesson.id) + 1
next_lesson = lessons.filter(priority__gt=lesson.priority).order_by('priority').first()
next_lesson_id = next_lesson.id if next_lesson else None
previous_lesson = lessons.filter(priority__lt=lesson.priority).order_by('-priority').first()
previous_lesson_id = previous_lesson.id if previous_lesson else None
lesson_data = self.get_serializer(lesson).data
=======
serializer_class = CourseLessonSerializer serializer_class = CourseLessonSerializer
@swagger_auto_schema( @swagger_auto_schema(
@ -138,120 +95,42 @@ class LessonDetailView(RetrieveAPIView):
previous_lesson_id = previous_lesson.id if previous_lesson else None previous_lesson_id = previous_lesson.id if previous_lesson else None
lesson_data = self.get_serializer(course_lesson).data lesson_data = self.get_serializer(course_lesson).data
>>>>>>> develop
lesson_data['total_lessons'] = total_lessons lesson_data['total_lessons'] = total_lessons
lesson_data['current_lesson_number'] = current_lesson_number lesson_data['current_lesson_number'] = current_lesson_number
lesson_data['next_lesson_id'] = next_lesson_id lesson_data['next_lesson_id'] = next_lesson_id
lesson_data['previous_lesson_id'] = previous_lesson_id lesson_data['previous_lesson_id'] = previous_lesson_id
lesson_data['can_go_next'] = next_lesson is not None lesson_data['can_go_next'] = next_lesson is not None
<<<<<<< HEAD
# # Get the next and previous lessons based on priority and id
# next_lesson = Lesson.objects.filter(
# course=lesson.course,
# is_active=True,
# priority__gte=lesson.priority,
# id__gt=lesson.id
# ).order_by('priority', 'id').first()
# previous_lesson = Lesson.objects.filter(
# course=lesson.course,
# is_active=True,
# priority__lte=lesson.priority,
# id__lt=lesson.id
# ).order_by('-priority', '-id').first()
# total_lessons = Lesson.objects.filter(course=lesson.course, is_active=True).count()
# # Calculate the current lesson number in the course
# current_lesson_number = Lesson.objects.filter(
# course=lesson.course,
# is_active=True,
# priority__lte=lesson.priority
# ).count()
# # Serialize the current lesson
# lesson_data = self.get_serializer(lesson).data
# # Add current lesson number and total lessons
# lesson_data['current_lesson_number'] = current_lesson_number
# lesson_data['total_lessons'] = total_lessons
# # Add next and previous lesson ids
# lesson_data['next_lesson_id'] = next_lesson.id if next_lesson else None
# lesson_data['previous_lesson_id'] = previous_lesson.id if previous_lesson else None
=======
>>>>>>> develop
return Response(lesson_data) return Response(lesson_data)
<<<<<<< HEAD
class LessonCompletionCreateAPIView(GenericAPIView):
permission_classes = [IsAuthenticated]
@swagger_auto_schema(
=======
class LessonCompletionToggleAPIView(GenericAPIView): class LessonCompletionToggleAPIView(GenericAPIView):
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
@swagger_auto_schema( @swagger_auto_schema(
operation_description="Toggle lesson completion status (Check/Uncheck)", operation_description="Toggle lesson completion status (Check/Uncheck)",
tags=["Imam-Javad - Course"], tags=["Imam-Javad - Course"],
>>>>>>> develop
request_body=openapi.Schema( request_body=openapi.Schema(
type=openapi.TYPE_OBJECT, type=openapi.TYPE_OBJECT,
required=['lesson_id'], required=['lesson_id'],
properties={ properties={
<<<<<<< HEAD
'lesson_id': openapi.Schema(type=openapi.TYPE_INTEGER, description='ID of the lesson to be marked as completed'),
},
),
responses={
201: 'Lesson completed successfully.',
200: 'Lesson already completed.',
=======
'lesson_id': openapi.Schema(type=openapi.TYPE_INTEGER, description='ID of the lesson to toggle'), 'lesson_id': openapi.Schema(type=openapi.TYPE_INTEGER, description='ID of the lesson to toggle'),
}, },
), ),
responses={ responses={
201: 'Lesson marked as COMPLETED.', 201: 'Lesson marked as COMPLETED.',
200: 'Lesson marked as INCOMPLETE (Unchecked).', 200: 'Lesson marked as INCOMPLETE (Unchecked).',
>>>>>>> develop
400: 'Lesson ID is required.', 400: 'Lesson ID is required.',
404: 'Lesson not found.', 404: 'Lesson not found.',
} }
) )
def post(self, request): def post(self, request):
<<<<<<< HEAD
student = request.user # Assuming the user is the student
=======
student = request.user student = request.user
>>>>>>> develop
lesson_id = request.data.get('lesson_id') lesson_id = request.data.get('lesson_id')
if not lesson_id: if not lesson_id:
return Response({'error': 'Lesson ID is required.'}, status=status.HTTP_400_BAD_REQUEST) return Response({'error': 'Lesson ID is required.'}, status=status.HTTP_400_BAD_REQUEST)
<<<<<<< HEAD
try:
lesson = Lesson.objects.get(id=lesson_id)
except Lesson.DoesNotExist:
return Response({'error': 'Lesson not found.'}, status=status.HTTP_404_NOT_FOUND)
# Check if the lesson is already completed by the student
if LessonCompletion.objects.filter(student=student, lesson=lesson).exists():
return Response({'message': 'Lesson already completed.'}, status=status.HTTP_200_OK)
# Create a new completion record
completion = LessonCompletion(student=student, lesson=lesson)
completion.save()
return Response({'message': 'Lesson completed successfully.'}, status=status.HTTP_201_CREATED)
=======
try: try:
course_lesson = CourseLesson.objects.get(id=lesson_id) course_lesson = CourseLesson.objects.get(id=lesson_id)
@ -278,4 +157,3 @@ class LessonCompletionToggleAPIView(GenericAPIView):
{'message': 'Lesson completed successfully.', 'is_completed': True}, {'message': 'Lesson completed successfully.', 'is_completed': True},
status=status.HTTP_201_CREATED status=status.HTTP_201_CREATED
) )
>>>>>>> develop

9
apps/course/views/participant.py

@ -19,10 +19,6 @@ class CourseParticipantsView(generics.ListAPIView):
@swagger_auto_schema( @swagger_auto_schema(
operation_description=doc_course_participants(), operation_description=doc_course_participants(),
<<<<<<< HEAD
)
def get_queryset(self):
=======
tags=['Imam-Javad - Course'], tags=['Imam-Javad - Course'],
) )
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
@ -31,20 +27,15 @@ class CourseParticipantsView(generics.ListAPIView):
""" """
Optimized queryset with select_related for course relationship Optimized queryset with select_related for course relationship
""" """
>>>>>>> develop
course_slug = self.kwargs.get('slug') course_slug = self.kwargs.get('slug')
try: try:
course = Course.objects.get(slug=course_slug) course = Course.objects.get(slug=course_slug)
except Course.DoesNotExist: except Course.DoesNotExist:
raise AppAPIException({'message': "Course not found"}) # Handle course not found raise AppAPIException({'message': "Course not found"}) # Handle course not found
<<<<<<< HEAD
return StudentUser.objects.filter(participated_courses__course=course)
=======
return StudentUser.objects.select_related().filter( return StudentUser.objects.select_related().filter(
participated_courses__course=course participated_courses__course=course
) )
>>>>>>> develop

4
apps/hadis/admin/__init__.py

@ -1,9 +1,5 @@
from .category import * from .category import *
from .hadis import * from .hadis import *
<<<<<<< HEAD
from .transmitter import *
=======
from .transmitter import * from .transmitter import *
from .reference import * from .reference import *
from .version import * from .version import *
>>>>>>> develop

219
apps/hadis/admin/category.py

@ -1,222 +1,4 @@
from django.contrib import admin from django.contrib import admin
<<<<<<< HEAD
from django.utils.translation import gettext_lazy as _
from dj_category.admin import BaseCategoryAdmin
from ajaxdatatable.admin import AjaxDatatable
from django.http import JsonResponse
from django.urls import path
from apps.hadis.models import *
from django import forms
from django.db.models import Case, When, Value
@admin.register(HadisCategory)
class HadisCategoryAdmin(BaseCategoryAdmin):
change_form_template = 'admin/hadiscategory/change_form.html'
change_list_template = 'admin/category_index.html'
fields = (
'name', 'source_type', 'category_type' , 'parent', 'is_active', 'order'
)
search_fields = ['name']
def get_form(self, request, obj=None, **kwargs):
form = super().get_form(request, obj, **kwargs)
return form
def get_urls(self):
urls = super().get_urls()
custom_urls = [
path('categories-ajax/hadiscategory/', self.admin_site.admin_view(self.ajax_categories), name='hadiscategory_ajax_categories'),
]
return custom_urls + urls
def get_categories_groupby_language(self, request=None, selected_values=(), is_multiple=False):
print(f'--get_categories_groupby_language-> {selected_values}')
return super().get_categories(request, selected_values, is_multiple)
def ajax_update(self, request):
data = request.POST
src_node = self.model.objects.get(pk=int(data['srcNode']))
other_node = self.model.objects.get(pk=int(data['otherNode']))
print(f'--ajax_update-> {data}')
if src_node.slug in self.base_categories or other_node.slug in self.base_categories:
return JsonResponse({'data': _('This item can not be modifed')}, status=401)
mode = data['hitMode']
if mode == 'over':
src_node.move_to(other_node, 'first-child')
elif mode == 'after':
src_node.move_to(other_node, 'right')
elif mode == 'before':
src_node.move_to(other_node, 'left')
return JsonResponse({'data': 'ok'}, safe=False)
def get_categories(self, request=None, selected_values=(), is_multiple=False):
"""
Override the get_categories method to filter by source_type if provided in the request
"""
categories = super().get_categories(request, selected_values, is_multiple)
# If request has source_type parameter, filter the categories
if request and request.GET.get('source_type'):
source_type = request.GET.get('source_type')
# Filter the categories by source_type
filtered_categories = []
for category in categories:
# If it's a dictionary (serialized category)
if isinstance(category, dict) and category.get('source_type') == source_type:
filtered_categories.append(category)
# If it's a model instance
elif hasattr(category, 'source_type') and getattr(category, 'source_type') == source_type:
filtered_categories.append(category)
return filtered_categories
return categories
def ajax_categories(self, request):
"""
Handle AJAX request for categories with source_type filtering and search
"""
# Get source_type from request
source_type = request.GET.get('source_type')
# Get node_id if provided (for single node data)
node_id = request.GET.get('node_id')
# Get search term if provided
search = request.GET.get('search')
# Get parent level filter if provided
parent_level = request.GET.get('parent_level')
if node_id:
# Return data for a specific node
try:
node = self.model.objects.get(pk=int(node_id))
return JsonResponse({
'id': node.id,
'source_type': node.source_type,
'category_type': node.category_type,
'parent': node.parent_id,
'level': node.level_p # Add the level_p property
})
except self.model.DoesNotExist:
return JsonResponse({'error': 'Node not found'}, status=404)
# Get all categories
queryset = self.model.objects.all()
# Annotate queryset with level_p
queryset = queryset.annotate(
level_pp=Case(
When(parent=None, then=Value(1)),
When(parent__isnull=False, parent__parent=None, then=Value(2)),
default=Value(3),
output_field=models.IntegerField()
)
)
# Filter by source_type if provided
if source_type:
queryset = queryset.filter(source_type=source_type)
# Filter by search term if provided
if search:
queryset = queryset.filter(name__icontains=search)
# Filter by parent_level if provided
if parent_level and parent_level.isdigit():
# Convert to integer
level = int(parent_level)
# Filter categories by level_p
queryset = queryset.filter(level_pp=level)
# Convert queryset to list of dictionaries for JSON response
categories = []
for category in queryset:
categories.append({
'key': category.id,
'title': category.name,
'parent': category.parent_id,
'source_type': category.source_type,
'category_type': category.category_type,
'level': category.level_p,
# Add data property to store additional information
'data': {
'parent': category.parent_id,
'level': category.level_p
}
})
print(f'-categories-->{categories}')
return JsonResponse(categories, safe=False)
def save_model(self, request, obj, form, change):
print(f'SAVE_MODEL CALLED: {request}/ {obj} / {form} / {change}')
print(f'POST DATA: {request.POST}')
# Get the level choice from the form data
level_choice = request.POST.get('level_choice_hidden')
print(f'LEVEL CHOICE: {level_choice}')
# Get the parent from AJAX selection if provided
ajax_parent = request.POST.get('ajax_parent')
if ajax_parent and ajax_parent.isdigit():
# Set the parent for the object
try:
parent_category = self.model.objects.get(pk=int(ajax_parent))
obj.parent = parent_category
# If parent is level 1, inherit its source_type
# if parent_category.level_p == 1 and level_choice == '2':
# obj.source_type = parent_category.source_type
print(f'AJAX PARENT SET: {parent_category.id} - {parent_category.name}')
except self.model.DoesNotExist:
print(f'PARENT CATEGORY NOT FOUND: {ajax_parent}')
# Debug form validation
if form.is_valid():
print("FORM IS VALID")
else:
print(f"FORM ERRORS: {form.errors}")
print(f'---> {obj}')
# Let the parent class handle the save
super().save_model(request, obj, form, change)
# Add a message to trigger tree reload via JavaScript
from django.contrib import messages
messages.success(request, "Category saved successfully. Tree will be reloaded.")
# Set a flag in the request to redirect back to the category index page
request._category_saved = True
def response_add(self, request, obj, post_url_continue=None):
"""
Override to redirect back to the category index page after adding a new category
"""
if hasattr(request, '_category_saved') and request._category_saved:
from django.http import HttpResponseRedirect
from django.urls import reverse
return HttpResponseRedirect(reverse('admin:hadis_hadiscategory_changelist'))
return super().response_add(request, obj, post_url_continue)
def response_change(self, request, obj):
"""
Override to redirect back to the category index page after editing a category
"""
if hasattr(request, '_category_saved') and request._category_saved:
from django.http import HttpResponseRedirect
from django.urls import reverse
return HttpResponseRedirect(reverse('admin:hadis_hadiscategory_changelist'))
return super().response_change(request, obj)
=======
from django import forms from django import forms
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.utils.html import format_html from django.utils.html import format_html
@ -437,4 +219,3 @@ class HadisCategoryAdmin(DraggableMPTTAdmin, ModelAdmin):
# Register models with the custom admin site # Register models with the custom admin site
dovoodi_admin_site.register(HadisSect, HadisSectAdmin) dovoodi_admin_site.register(HadisSect, HadisSectAdmin)
dovoodi_admin_site.register(HadisCategory, HadisCategoryAdmin) dovoodi_admin_site.register(HadisCategory, HadisCategoryAdmin)
>>>>>>> develop

162
apps/hadis/admin/hadis.py

@ -1,119 +1,3 @@
<<<<<<< HEAD
from django.contrib import admin
from django.utils.translation import gettext_lazy as _
from dj_category.admin import BaseCategoryAdmin
from ajaxdatatable.admin import AjaxDatatable
from django.http import JsonResponse
from django.urls import path
from django.db.models import Q
from django.utils.safestring import mark_safe
from django.forms.widgets import RadioSelect
from apps.hadis.models import *
from django import forms
from utils.json_editor_field import JsonEditorWidget
# Define color choices
COLOR_CHOICES = [
('red', _('Red')),
('blue', _('Blue')),
('green', _('Green')),
('yellow', _('Yellow')),
('orange', _('Orange')),
('purple', _('Purple')),
('pink', _('Pink')),
('brown', _('Brown')),
('gray', _('Gray')),
('black', _('Black')),
]
class ColorRadioSelect(RadioSelect):
template_name = 'admin/widgets/color_radio.html'
option_template_name = 'admin/widgets/color_radio_option.html'
def get_links_schema():
return {
'type': "array",
'format': 'table',
'title': ' ',
'items': {
'type': 'object',
'title': str(_('Link')),
'properties': {
'text': {'type': 'string', "format": "textarea",'title': str(_('text'))},
'link': {'type': 'string', "format": "textarea", 'title': str(_('link'))},
}
}
}
class HadisOverviewForm(forms.ModelForm):
status_color = forms.ChoiceField(
choices=COLOR_CHOICES,
widget=ColorRadioSelect(),
required=False
)
class Meta:
model = HadisOverview
fields = '__all__'
widgets = {
'links': JsonEditorWidget(attrs={'schema': get_links_schema}),
}
@admin.register(HadisTag)
class HadisTagAdmin(AjaxDatatable):
list_display = ['title', 'status']
search_fields = ['title']
class ReferenceImageInline(admin.TabularInline):
model = ReferenceImage
extra = 1
verbose_name_plural = _('Reference Images')
fields = ('thumbnail', 'priority')
@admin.register(HadisReference)
class HadisReferenceAdmin(AjaxDatatable):
list_display = ['hadis', 'book', 'created_at']
list_filter = ['book']
search_fields = ['hadis__title', 'hadis__number', 'description']
autocomplete_fields = ['hadis', 'book']
readonly_fields = ['created_at']
inlines = [ReferenceImageInline]
fieldsets = (
(None, {
'fields': ('hadis', 'book', 'description')
}),
)
@admin.register(HadisOverview)
class HadisOverviewAdmin(AjaxDatatable):
change_form_template = 'admin/hadisowerview_change_form.html'
form = HadisOverviewForm
ordering = ['hadis__number']
list_display = ['hadis', 'status', 'created_at']
search_fields = ['hadis__title', 'hadis__number', 'status_text',]
autocomplete_fields = ['hadis', 'tags']
fieldsets = (
(None, {
'fields': ('hadis', 'status', 'status_color', 'status_text')
}),
(_('Reference Information'), {
'fields': ('address', 'share_link',),
}),
(_('Additional Information'), {
'fields': ('links', 'tags', 'created_at'),
'classes': ('collapse',),
=======
from django import forms from django import forms
from django.contrib import admin from django.contrib import admin
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -235,45 +119,10 @@ class HadisTagAdmin(ModelAdmin):
(_('Timestamps'), { (_('Timestamps'), {
'fields': ('created_at', 'updated_at'), 'fields': ('created_at', 'updated_at'),
'classes': ('collapse',) 'classes': ('collapse',)
>>>>>>> develop
}), }),
) )
<<<<<<< HEAD
class HadisOverviewInline(admin.StackedInline):
change_form_template = 'admin/hadisowerview_change_form.html'
form = HadisOverviewForm
model = HadisOverview
autocomplete_fields = ['tags', ]
can_delete = False
verbose_name_plural = _('Hadis Overview')
fieldsets = (
(None, {
'fields': ('status', 'status_color', 'status_text', 'address', 'share_link', 'links', 'tags',),
}),
)
extra = 1
min_num = 1
@admin.register(Hadis)
class HadisAdmin(AjaxDatatable):
# form = HadisForm
list_display = ['number', 'title', 'category', 'status', 'created_at']
list_filter = ['status', 'category']
search_fields = ['title', 'text', 'number']
readonly_fields = ['created_at', 'updated_at']
autocomplete_fields = ['category']
inlines = [HadisOverviewInline]
fieldsets = (
(None, {
'fields': ('number', 'title', 'category', 'status')
}),
(_('Content'), {
'fields': ('text', 'translation'),
'classes': ('collapse',),
=======
class HadisStatusAdmin(ModelAdmin): class HadisStatusAdmin(ModelAdmin):
"""Admin for HadisStatus model""" """Admin for HadisStatus model"""
list_display = ('title', 'color', 'order') list_display = ('title', 'color', 'order')
@ -316,20 +165,10 @@ class HadisAdmin(ModelAdmin):
(_('Timestamps'), { (_('Timestamps'), {
'fields': ('created_at', 'updated_at'), 'fields': ('created_at', 'updated_at'),
'classes': ('collapse',) 'classes': ('collapse',)
>>>>>>> develop
}), }),
) )
<<<<<<< HEAD
def get_form(self, request, obj=None, **kwargs):
form = super().get_form(request, obj, **kwargs)
if obj is None:
form.base_fields['category'].widget.can_add_related = False
return form
=======
class HadisReferenceAdmin(ModelAdmin): class HadisReferenceAdmin(ModelAdmin):
"""Admin for HadisReference model""" """Admin for HadisReference model"""
list_display = ('hadis', 'book_reference', 'created_at') list_display = ('hadis', 'book_reference', 'created_at')
@ -548,4 +387,3 @@ dovoodi_admin_site.register(ReferenceImage, ReferenceImageAdmin)
dovoodi_admin_site.register(HadisCollection, HadisCollectionAdmin) dovoodi_admin_site.register(HadisCollection, HadisCollectionAdmin)
dovoodi_admin_site.register(HadisInCollection, HadisInCollectionAdmin) dovoodi_admin_site.register(HadisInCollection, HadisInCollectionAdmin)
dovoodi_admin_site.register(HadisCorrection, HadisCorrectionAdmin) dovoodi_admin_site.register(HadisCorrection, HadisCorrectionAdmin)
>>>>>>> develop

124
apps/hadis/management/commands/README.md

@ -2,127 +2,3 @@
## seed_hadis_data ## seed_hadis_data
<<<<<<< HEAD
This management command seeds comprehensive data for all Hadis app models with realistic sample records while maintaining proper relationships and business domain logic.
=======
This management command seeds comprehensive data for all Hadis app models with realistic sample records while maintaining proper relationships and business domain logic. **Enhanced with lock detection and retry logic to prevent database locks.**
>>>>>>> 932fb17 (Refactor API Documentation System and optimize Hadis data scripts)
### Usage
```bash
# Basic usage - seed data with default settings
python manage.py seed_hadis_data
# Clear existing data before seeding
python manage.py seed_hadis_data --clear
# Specify custom images directory
python manage.py seed_hadis_data --images-dir /path/to/images
# Specify custom XMind file
python manage.py seed_hadis_data --xmind-file /path/to/file.xmind
# Combine options
python manage.py seed_hadis_data --clear --images-dir scripts/seed_images --xmind-file scripts/test.xmind
```
### Options
- `--clear`: Clear existing hadis data before seeding (optional)
- `--images-dir`: Directory containing seed images (default: scripts/seed_images)
- `--xmind-file`: Path to XMind file for categories (default: scripts/test.xmind)
### What it creates
1. **HadisStatus records**: Various hadis authenticity statuses (Достоверный, Хороший, etc.)
2. **HadisTag records**: Topic tags for categorizing hadis
3. **HadisSect records**: Shia and Sunni sects
4. **HadisCategory records**: Hierarchical categories for both Quran and Hadith sources
5. **Library data**: Books, categories, and collections for references
6. **Transmitters**: Historical figures who transmitted hadis
7. **Hadis records**: Complete hadis with translations, explanations, and relationships
8. **Transmission chains**: Links between hadis and transmitters
9. **References**: Book references with images
### Requirements
- The images directory must contain PNG files for book covers and reference images
- The XMind file is optional but recommended for category mind maps
- All models must be properly migrated before running
<<<<<<< HEAD
### Performance
The command uses optimized batch operations to create data efficiently:
- Bulk create/update operations for categories
- Checks for existing records to avoid duplicates
- Progress reporting for large datasets
=======
### Performance & Lock Prevention
The command uses advanced techniques to prevent database locks and ensure reliable execution:
- **Lock Detection**: Automatically detects database locks and deadlocks
- **Retry Logic**: Retries failed operations with exponential backoff (up to 5 attempts)
- **Step-by-step Processing**: Creates records individually with small delays to prevent locks
- **Batch Processing**: Processes tags in small batches to avoid overwhelming the database
- **No Large Transactions**: Avoids wrapping everything in atomic transactions that can cause locks
- **Progress Reporting**: Detailed progress with emoji indicators and clear status messages
- **Error Handling**: Graceful handling of duplicate records and constraint violations
>>>>>>> 932fb17 (Refactor API Documentation System and optimize Hadis data scripts)
### Example Output
```
Starting Hadis data seeding...
Found 4 seed images
XMind file: scripts/test.xmind
Creating Hadis Statuses...
Created status: Достоверный
Created status: Хороший
...
Creating Hadis Categories...
Creating categories for Шииты-двунадесятники...
Batch created 6 Quran categories
...
Successfully seeded all Hadis data!
```
<<<<<<< HEAD
=======
## test_safe_seeding
A simple test command to verify that the lock detection and retry logic is working properly.
### Usage
```bash
# Test the safe seeding functionality
python manage.py test_safe_seeding
```
### What it tests
- Database connectivity
- Lock detection mechanisms
- Retry logic for failed operations
- Creation of test records (sect, status, tag)
## Additional Commands
### fix_sects
Fixes any issues with sect creation by using simple English titles.
```bash
python manage.py fix_sects
```
### seed_basic_data
Creates only the essential basic data (statuses, tags, sects) without the full dataset.
```bash
python manage.py seed_basic_data [--clear]
```
>>>>>>> 932fb17 (Refactor API Documentation System and optimize Hadis data scripts)

74
apps/hadis/migrations/0001_initial.py

@ -1,10 +1,3 @@
<<<<<<< HEAD
# Generated by Django 3.2.7 on 2025-03-16 23:50
from django.db import migrations, models
import django.db.models.deletion
import mptt.fields
=======
# Generated by Django 5.1.8 on 2025-04-03 00:05 # Generated by Django 5.1.8 on 2025-04-03 00:05
import django.db.models.deletion import django.db.models.deletion
@ -12,7 +5,6 @@ import filer.fields.image
import mptt.fields import mptt.fields
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
>>>>>>> develop
class Migration(migrations.Migration): class Migration(migrations.Migration):
@ -20,67 +12,16 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
<<<<<<< HEAD
=======
('library', '0001_initial'), ('library', '0001_initial'),
migrations.swappable_dependency(settings.FILER_IMAGE_MODEL), migrations.swappable_dependency(settings.FILER_IMAGE_MODEL),
>>>>>>> develop
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
<<<<<<< HEAD
name='Hadis',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('number', models.PositiveIntegerField(unique=True, verbose_name='number')),
('title', models.CharField(max_length=355, verbose_name='title')),
('text', models.TextField(verbose_name='text')),
('translation', models.TextField(blank=True, default='', verbose_name='translation')),
('status', models.BooleanField(default=True, verbose_name='visibility')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created at')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='updated at')),
],
options={
'verbose_name': 'hadis',
'verbose_name_plural': 'hadises',
},
),
migrations.CreateModel(
=======
>>>>>>> develop
name='HadisTag', name='HadisTag',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=355, verbose_name='title')), ('title', models.CharField(max_length=355, verbose_name='title')),
<<<<<<< HEAD
],
),
migrations.CreateModel(
name='HadisTagRelation',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('priority', models.IntegerField(default=0, verbose_name='priority')),
('hadis', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='hadis.hadis', verbose_name='hadis')),
('tag', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='hadis.hadistag', verbose_name='tag')),
],
options={
'verbose_name': 'hadis tag relation',
'verbose_name_plural': 'hadis tag relations',
'unique_together': {('tag', 'hadis')},
},
),
migrations.CreateModel(
name='HadisCategory',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=512, verbose_name='name')),
('is_active', models.BooleanField(default=True, verbose_name='is active')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created at')),
('source_type', models.CharField(blank=True, choices=[('shia', 'Shia Sources'), ('sunni', 'Sunni Sources')], default='shia', max_length=10, verbose_name='Source Type')),
('category_type', models.CharField(blank=True, choices=[('quran', 'Quran'), ('hadith', 'Hadith')], max_length=10, null=True, verbose_name='Category Content Type')),
('title', models.CharField(max_length=355, verbose_name='title')),
=======
('status', models.BooleanField(default=True, verbose_name='status')), ('status', models.BooleanField(default=True, verbose_name='status')),
], ],
), ),
@ -93,7 +34,6 @@ class Migration(migrations.Migration):
('source_type', models.CharField(blank=True, choices=[('shia', 'Shia'), ('sunni', 'Sunni')], default='shia', max_length=10, verbose_name='Source Type')), ('source_type', models.CharField(blank=True, choices=[('shia', 'Shia'), ('sunni', 'Sunni')], default='shia', max_length=10, verbose_name='Source Type')),
('category_type', models.CharField(blank=True, choices=[('quran', 'Quran'), ('hadith', 'Hadith')], max_length=10, null=True, verbose_name='Category Content Type')), ('category_type', models.CharField(blank=True, choices=[('quran', 'Quran'), ('hadith', 'Hadith')], max_length=10, null=True, verbose_name='Category Content Type')),
('name', models.CharField(max_length=355, verbose_name='name')), ('name', models.CharField(max_length=355, verbose_name='name')),
>>>>>>> develop
('order', models.IntegerField(default=0, verbose_name='order')), ('order', models.IntegerField(default=0, verbose_name='order')),
('lft', models.PositiveIntegerField(editable=False)), ('lft', models.PositiveIntegerField(editable=False)),
('rght', models.PositiveIntegerField(editable=False)), ('rght', models.PositiveIntegerField(editable=False)),
@ -107,17 +47,6 @@ class Migration(migrations.Migration):
'ordering': ('order',), 'ordering': ('order',),
}, },
), ),
<<<<<<< HEAD
migrations.AddField(
model_name='hadis',
name='category',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='hadis.hadiscategory', verbose_name='category'),
),
migrations.AddField(
model_name='hadis',
name='tags',
field=models.ManyToManyField(related_name='hadises', through='hadis.HadisTagRelation', to='hadis.HadisTag', verbose_name='tags'),
=======
migrations.CreateModel( migrations.CreateModel(
name='Hadis', name='Hadis',
fields=[ fields=[
@ -208,6 +137,5 @@ class Migration(migrations.Migration):
'ordering': ('hadis', 'order'), 'ordering': ('hadis', 'order'),
'unique_together': {('hadis', 'transmitter', 'order')}, 'unique_together': {('hadis', 'transmitter', 'order')},
}, },
>>>>>>> develop
), ),
]
]

4
apps/hadis/models/__init__.py

@ -1,9 +1,5 @@
from .category import * from .category import *
from .hadis import * from .hadis import *
<<<<<<< HEAD
from .transmitter import *
=======
from .transmitter import * from .transmitter import *
from .reference import * from .reference import *
from .version import * from .version import *
>>>>>>> develop

102
apps/hadis/models/category.py

@ -1,30 +1,6 @@
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
<<<<<<< HEAD
from dj_category.models import BaseCategoryAbstract
class HadisCategory(BaseCategoryAbstract):
class SourceType(models.TextChoices):
SHIA = 'shia', _('Shia')
SUNNI = 'sunni', _('Sunni')
class ContentType(models.TextChoices):
QURAN = 'quran', _('Quran')
HADITH = 'hadith', _('Hadith')
class LevelChoices(models.IntegerChoices):
LEVEL_1 = 1, _('Level 1 (Root)')
LEVEL_2 = 2, _('Level 2 (Child)')
LEVEL_3 = 3, _('Level 3 (Grandchild)')
source_type = models.CharField(max_length=10, choices=SourceType.choices, default=SourceType.SHIA, verbose_name=_('Source Type'), blank=True)
category_type = models.CharField(max_length=10, choices=ContentType.choices, verbose_name=_('Category Content Type'), blank=True, null=True)
name = models.CharField(max_length=355, verbose_name=_('name'))
order = models.IntegerField(default=0, verbose_name=_('order'))
slug = None
=======
from mptt.models import MPTTModel, TreeForeignKey from mptt.models import MPTTModel, TreeForeignKey
from django.utils.text import slugify from django.utils.text import slugify
@ -101,17 +77,10 @@ class HadisCategory(MPTTModel):
order = models.IntegerField(default=0, verbose_name=_('order')) order = models.IntegerField(default=0, verbose_name=_('order'))
xmind_file = models.FileField(upload_to='hadis/xmind_files/', verbose_name=_('xmind file'), null=True, blank=True) xmind_file = models.FileField(upload_to='hadis/xmind_files/', verbose_name=_('xmind file'), null=True, blank=True)
slug = models.SlugField(max_length=255, null=True, blank=True) slug = models.SlugField(max_length=255, null=True, blank=True)
>>>>>>> develop
content_type = None content_type = None
language = None language = None
language_id = None language_id = None
<<<<<<< HEAD
# This field is not stored in the database, it's only used for the form
level_choice = None
class Meta:
=======
def clean(self): def clean(self):
super().clean() super().clean()
if self.parent and self.sect_id != self.parent.sect_id: if self.parent and self.sect_id != self.parent.sect_id:
@ -139,81 +108,11 @@ class HadisCategory(MPTTModel):
models.Index(fields=['parent', 'sect']), models.Index(fields=['parent', 'sect']),
models.Index(fields=['sect', 'order']) models.Index(fields=['sect', 'order'])
] ]
>>>>>>> develop
verbose_name = _('Hadis Category') verbose_name = _('Hadis Category')
verbose_name_plural = _('Hadis Categories') verbose_name_plural = _('Hadis Categories')
ordering = ('order',) ordering = ('order',)
def __str__(self): def __str__(self):
<<<<<<< HEAD
return f'<{str(self.level_p)}>{self.name}'
def __repr__(self):
return f'<{str(self.level_p)}>{self.name}'
def clean(self):
super().clean()
# Skip validation for new objects that haven't been saved yet
# This allows the admin form to set these values properly
if self.pk is None:
return
# For existing objects, apply the validation rules
if self.level_p == 1 and self.category_type:
raise ValidationError(_("Level 1 cannot have content type"))
if self.level_p == 2 and not self.category_type:
raise ValidationError(_("Level 2 must have content type"))
if self.level_p == 3 and (self.source_type or self.category_type):
raise ValidationError(_("Level 3 cannot have source/content type"))
def save(self, *args, **kwargs):
self.clean()
# Get the level from the parent structure
level = self.level_p
# Apply level-specific logic
# if level == 2 and self.parent:
# For level 2, inherit source_type from parent
# self.source_type = self.parent.source_type
# elif level == 3:
# For level 3, inherit both from parent
# if self.parent and self.parent.parent:
# self.source_type = self.parent.source_type
# self.category_type = self.parent.category_type
# Call the parent class's save method
super().save(*args, **kwargs)
@property
def level_p(self):
if not self.parent:
return 1
elif not self.parent.parent:
return 2
else:
return 3
def get_level_info(self):
info = {
'level': self.level_p,
'source_type': None,
'category_type': None,
}
if self.level_p == 1:
info['source_type'] = self.source_type
elif self.level_p == 2:
info['source_type'] = self.parent.source_type
info['category_type'] = self.category_type
return info
=======
return f"{self.sect.sect_type}: {self.source_type} - {self.title[0]['text']}" return f"{self.sect.sect_type}: {self.source_type} - {self.title[0]['text']}"
def get_title(self,lang): def get_title(self,lang):
@ -251,4 +150,3 @@ class HadisCategory(MPTTModel):
return None return None
>>>>>>> develop

87
apps/hadis/models/hadis.py

@ -1,19 +1,3 @@
<<<<<<< HEAD
from django.db import models
from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ValidationError
from filer.fields.image import FilerImageField
class HadisTag(models.Model):
title = models.CharField(max_length=355, verbose_name=_('title'))
status = models.BooleanField(default=True, verbose_name=_('status'))
def __str__(self):
return f"{self.title}"
=======
from enum import unique from enum import unique
from typing import Optional from typing import Optional
from django.db import models from django.db import models
@ -184,22 +168,11 @@ class HadisStatus(models.Model):
verbose_name = _('hadis status') verbose_name = _('hadis status')
verbose_name_plural = _('hadis statuses') verbose_name_plural = _('hadis statuses')
ordering = ('order',) ordering = ('order',)
>>>>>>> develop
class Hadis(models.Model): class Hadis(models.Model):
<<<<<<< HEAD
number = models.PositiveIntegerField(verbose_name=_('number'), unique=True)
title = models.CharField(max_length=355, verbose_name=_('title'))
text = models.TextField(verbose_name=_('text'))
translation = models.TextField(verbose_name=_('translation'), blank=True, default='')
category = models.ForeignKey("hadis.HadisCategory", null=True, on_delete=models.SET_NULL, verbose_name=_('category'), )
status = models.BooleanField(default=True, verbose_name=_('visibility'))
=======
category = models.ForeignKey("hadis.HadisCategory", on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_('category')) category = models.ForeignKey("hadis.HadisCategory", on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_('category'))
number = models.PositiveIntegerField(verbose_name=_('number'), default=1) number = models.PositiveIntegerField(verbose_name=_('number'), default=1)
slug = models.SlugField(max_length=255, verbose_name=_('slug'), blank=True,unique=True) slug = models.SlugField(max_length=255, verbose_name=_('slug'), blank=True,unique=True)
@ -220,37 +193,10 @@ class Hadis(models.Model):
share_link = models.CharField(max_length=255, verbose_name=_('share link'), null=True, blank=True) share_link = models.CharField(max_length=255, verbose_name=_('share link'), null=True, blank=True)
explanation = models.JSONField(default = list , verbose_name=_('Explanation')) explanation = models.JSONField(default = list , verbose_name=_('Explanation'))
>>>>>>> develop
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at')) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at'))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at')) updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at'))
def __str__(self): def __str__(self):
<<<<<<< HEAD
return f"<{self.number}> {self.title[:32]}"
@property
def get_tags(self):
return self.tags.all().order_by('hadistagrelation__priority')
class Meta:
verbose_name = _('hadis')
verbose_name_plural = _('hadises')
class HadisOverview(models.Model):
hadis = models.OneToOneField(Hadis, on_delete=models.CASCADE, primary_key=True)
status = models.CharField(max_length=50, verbose_name=_('status'))
status_color = models.CharField(max_length=25, verbose_name=_('Display Status Color'))
status_text = models.TextField(verbose_name=_('Status Text'), null=True, blank=True)
address = models.TextField(verbose_name=_('address'), null=True, blank=True)
links = models.JSONField(verbose_name=_('title'), null=True, blank=True, default=dict)
tags = models.ManyToManyField("HadisTag", related_name="hadises", verbose_name=_('tags'), blank=True)
share_link = models.CharField(max_length=255, verbose_name=_('share link'), null=True, blank=True)
explanation = models.TextField(verbose_name=_('explanation'), null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at'))
=======
return f"{self.number} - {self.title[0]['text']}" if self.title else f"Hadis {self.number}" return f"{self.number} - {self.title[0]['text']}" if self.title else f"Hadis {self.number}"
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
@ -363,7 +309,6 @@ class HadisOverview(models.Model):
ordering = ('category', 'number') ordering = ('category', 'number')
>>>>>>> develop
class HadisReference(models.Model): class HadisReference(models.Model):
hadis = models.ForeignKey( hadis = models.ForeignKey(
@ -372,26 +317,6 @@ class HadisReference(models.Model):
verbose_name=_('hadis'), verbose_name=_('hadis'),
related_name='references' related_name='references'
) )
<<<<<<< HEAD
book = models.ForeignKey("library.Book", on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_('book'), related_name='hadis_references')
description = models.TextField(verbose_name=_('description'), blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at'))
class Meta:
verbose_name = _('Hadis Reference')
verbose_name_plural = _('Hadis References')
unique_together = ('hadis', 'book')
def __str__(self):
return f'{self.hadis.number}-{self.book.title}'
class ReferenceImage(models.Model):
reference = models.ForeignKey(HadisReference, verbose_name="Hadis Reference", on_delete=models.CASCADE)
thumbnail = FilerImageField(
related_name='+', on_delete=models.PROTECT, null=True, blank=True,
verbose_name=_('thumbnail')
)
=======
book_reference = models.ForeignKey( book_reference = models.ForeignKey(
BookReference, BookReference,
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
@ -436,7 +361,6 @@ class ReferenceImage(models.Model):
class ReferenceImage(models.Model): class ReferenceImage(models.Model):
reference = models.ForeignKey(HadisReference,related_name = 'images', verbose_name="Hadis Reference", on_delete=models.CASCADE) reference = models.ForeignKey(HadisReference,related_name = 'images', verbose_name="Hadis Reference", on_delete=models.CASCADE)
thumbnail = models.ImageField(upload_to='hadis/reference_images/', null=True, blank=True, verbose_name=_('thumbnail')) thumbnail = models.ImageField(upload_to='hadis/reference_images/', null=True, blank=True, verbose_name=_('thumbnail'))
>>>>>>> develop
priority = models.IntegerField( priority = models.IntegerField(
default=0, default=0,
verbose_name=_("Priority"), verbose_name=_("Priority"),
@ -445,22 +369,15 @@ class ReferenceImage(models.Model):
class Meta: class Meta:
<<<<<<< HEAD
=======
indexes = [ indexes = [
# Speeds up fetching images for a reference in priority order # Speeds up fetching images for a reference in priority order
models.Index(fields=['reference', 'priority']), models.Index(fields=['reference', 'priority']),
] ]
>>>>>>> develop
verbose_name = _('Reference Image') verbose_name = _('Reference Image')
verbose_name_plural = _('Reference Images') verbose_name_plural = _('Reference Images')
def __str__(self): def __str__(self):
<<<<<<< HEAD
return f'{self.reference.title}-{self.id}'
=======
return f'{self.reference.title[0]["text"]}-{self.id}' return f'{self.reference.title[0]["text"]}-{self.id}'
>>>>>>> develop
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if ReferenceImage.objects.filter(reference=self.reference, priority=self.priority).exists(): if ReferenceImage.objects.filter(reference=self.reference, priority=self.priority).exists():
@ -470,9 +387,6 @@ class ReferenceImage(models.Model):
).update(priority=F('priority') + 1) ).update(priority=F('priority') + 1)
super().save(*args, **kwargs) super().save(*args, **kwargs)
<<<<<<< HEAD
=======
class HadisCorrection(models.Model): class HadisCorrection(models.Model):
hadis = models.ForeignKey(Hadis, verbose_name=_("hadis correction"), on_delete=models.CASCADE) hadis = models.ForeignKey(Hadis, verbose_name=_("hadis correction"), on_delete=models.CASCADE)
@ -574,4 +488,3 @@ class HadisCorrection(models.Model):
if isinstance(tr, dict) and tr.get('language_code') == 'en': if isinstance(tr, dict) and tr.get('language_code') == 'en':
return tr.get('text', '') return tr.get('text', '')
return None return None
>>>>>>> develop

36
apps/hadis/models/transmitter.py

@ -1,11 +1,5 @@
<<<<<<< HEAD
from django.db import models
from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ValidationError
from filer.fields.image import FilerImageField
=======
from tabnanny import verbose from tabnanny import verbose
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -122,25 +116,10 @@ class TransmitterReliability(models.Model):
class Meta: class Meta:
verbose_name = _('Transmitter Reliability') verbose_name = _('Transmitter Reliability')
verbose_name_plural = _('Transmitter Reliabilities') verbose_name_plural = _('Transmitter Reliabilities')
>>>>>>> develop
class Transmitters(models.Model): class Transmitters(models.Model):
<<<<<<< HEAD
full_name = models.CharField(max_length=255)
birth_year_hijri = models.IntegerField(verbose_name="Birth Year (Hijri)")
death_year_hijri = models.IntegerField(verbose_name="Death Year (Hijri)")
description = models.TextField(blank=True, null=True, verbose_name="Description")
status = models.CharField(max_length=50, verbose_name=_('status'))
status_color = models.CharField(max_length=25, verbose_name=_('Display Status Color'))
thumbnail = FilerImageField(related_name="+", on_delete=models.CASCADE, help_text=_(
'image allowed'
), null=True, blank=True)
def __str__(self):
return self.full_name
=======
# class ReliabilityLevel(models.TextChoices): # class ReliabilityLevel(models.TextChoices):
# VERY_RELIABLE = 'very_reliable', _('Very Reliable') # VERY_RELIABLE = 'very_reliable', _('Very Reliable')
# RELIABLE = 'reliable', _('Reliable') # RELIABLE = 'reliable', _('Reliable')
@ -291,7 +270,6 @@ class Transmitters(models.Model):
name = self.full_name[0] name = self.full_name[0]
return name.get('text') return name.get('text')
>>>>>>> develop
class HadisTransmitter(models.Model): class HadisTransmitter(models.Model):
@ -307,9 +285,6 @@ class HadisTransmitter(models.Model):
verbose_name=_('transmitter'), verbose_name=_('transmitter'),
related_name='hadises' related_name='hadises'
) )
<<<<<<< HEAD
description = models.TextField(verbose_name=_('description'), blank=True, null=True)
=======
narrator_layer = models.ForeignKey( narrator_layer = models.ForeignKey(
NarratorLayer, NarratorLayer,
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
@ -328,17 +303,11 @@ class HadisTransmitter(models.Model):
blank=True, blank=True,
help_text=_('Reliability status of the narrator') help_text=_('Reliability status of the narrator')
) )
>>>>>>> develop
order = models.PositiveIntegerField( order = models.PositiveIntegerField(
default=0, default=0,
verbose_name=_('Order'), verbose_name=_('Order'),
help_text=_('Order in the chain of transmission') help_text=_('Order in the chain of transmission')
) )
<<<<<<< HEAD
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at'))
class Meta:
=======
is_gap = models.BooleanField(default=False, verbose_name=_('is gap')) is_gap = models.BooleanField(default=False, verbose_name=_('is gap'))
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at')) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at'))
@ -348,16 +317,12 @@ class HadisTransmitter(models.Model):
# Speeds up fetching transmitters for a specific hadis in order # Speeds up fetching transmitters for a specific hadis in order
models.Index(fields=['hadis', 'order']), models.Index(fields=['hadis', 'order']),
] ]
>>>>>>> develop
verbose_name = _('Hadis Transmitter') verbose_name = _('Hadis Transmitter')
verbose_name_plural = _('Hadis Transmitters') verbose_name_plural = _('Hadis Transmitters')
ordering = ('hadis', 'order') ordering = ('hadis', 'order')
unique_together = ('hadis', 'transmitter', 'order') unique_together = ('hadis', 'transmitter', 'order')
def __str__(self): def __str__(self):
<<<<<<< HEAD
return f'{self.hadis.number} - {self.transmitter.full_name} ({self.order})'
=======
layer_info = f" - {self.narrator_layer}" if self.narrator_layer else "" layer_info = f" - {self.narrator_layer}" if self.narrator_layer else ""
return f'{self.hadis.number} - {self.transmitter.full_name} ({self.order}){layer_info}' return f'{self.hadis.number} - {self.transmitter.full_name} ({self.order}){layer_info}'
@ -574,4 +539,3 @@ class TransmitterOriginalText(models.Model):
if isinstance(tr, dict) and tr.get('language_code') == 'en': if isinstance(tr, dict) and tr.get('language_code') == 'en':
return tr.get('text', '') return tr.get('text', '')
return None return None
>>>>>>> develop

4
apps/hadis/views/__init__.py

@ -1,7 +1,3 @@
from .category import * from .category import *
from .hadis import * from .hadis import *
<<<<<<< HEAD
# from .transmitter import *
=======
from .info import * from .info import *
>>>>>>> develop

78
apps/hadis/views/hadis.py

@ -1,80 +1,3 @@
<<<<<<< HEAD
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from django.db.models import Subquery, Count, F, OuterRef, Q, Prefetch
from rest_framework.generics import ListAPIView, RetrieveAPIView
from django.shortcuts import get_object_or_404
from apps.hadis.models import *
from apps.hadis.serializers import *
from apps.hadis.doc import category_list_swagger, category_hadis_list_swagger, hadis_detail_swagger
class CategoryHadisListView(ListAPIView):
serializer_class = HadisSerializer
permission_classes = (IsAuthenticated,)
@category_hadis_list_swagger
def get(self, request, *args, **kwargs):
return super().get(request, *args, **kwargs)
def get_queryset(self):
categories = HadisCategory.objects.filter(id=self.kwargs['pk']).order_by('-order')
return Hadis.objects.filter(
Q(category__in=categories),
status=True,
).prefetch_related(
'category',
)
class HadisDetailView(RetrieveAPIView):
"""
API endpoint to retrieve detailed information about a specific hadis.
Returns:
- Hadis details (number, title, text, translation)
- HadisOverview information (status, tags, etc.)
- First HadisReference with its ReferenceImages
- List of Transmitters
"""
serializer_class = HadisDetailSerializer
permission_classes = (IsAuthenticated,)
@hadis_detail_swagger
def get(self, request, *args, **kwargs):
return super().get(request, *args, **kwargs)
def get_object(self):
hadis_id = self.kwargs.get('pk')
queryset = Hadis.objects.filter(id=hadis_id)
# Prefetch related data to optimize queries
queryset = queryset.prefetch_related(
'hadisoverview',
'hadisoverview__tags',
Prefetch(
'references',
queryset=HadisReference.objects.prefetch_related(
'referenceimage_set',
'book'
)
),
Prefetch(
'transmitters',
queryset=HadisTransmitter.objects.select_related('transmitter').order_by('order')
)
)
return get_object_or_404(queryset, id=hadis_id)
def get_serializer_context(self):
context = super().get_serializer_context()
context.update({'request': self.request})
return context
=======
from rest_framework.generics import ListAPIView, RetrieveAPIView from rest_framework.generics import ListAPIView, RetrieveAPIView
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from utils.pagination import NoPagination from utils.pagination import NoPagination
@ -487,4 +410,3 @@ class HadisFiltersView(ListAPIView):
} }
return Response(response_data) return Response(response_data)
>>>>>>> develop

83
apps/library/migrations/0001_initial.py

@ -1,18 +1,9 @@
<<<<<<< HEAD
# Generated by Django 3.2.7 on 2025-03-20 07:06
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import filer.fields.image
=======
# Generated by Django 5.1.8 on 2025-04-03 00:05 # Generated by Django 5.1.8 on 2025-04-03 00:05
import django.db.models.deletion import django.db.models.deletion
import filer.fields.image import filer.fields.image
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
>>>>>>> develop
class Migration(migrations.Migration): class Migration(migrations.Migration):
@ -20,40 +11,11 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
<<<<<<< HEAD
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
=======
>>>>>>> develop
migrations.swappable_dependency(settings.FILER_IMAGE_MODEL), migrations.swappable_dependency(settings.FILER_IMAGE_MODEL),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
<<<<<<< HEAD
name='Book',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=255)),
('slug', models.SlugField(max_length=255, unique=True)),
('summary', models.CharField(blank=True, help_text='could be null', max_length=512, null=True)),
('description', models.TextField(blank=True, help_text='could be null', null=True)),
('pages_count', models.CharField(help_text='eg. 34', max_length=255, null=True, verbose_name='Number of Pages')),
('status', models.BooleanField(default=True, verbose_name='status')),
('pin', models.BooleanField(default=True, verbose_name='Pin to top')),
('view_count', models.PositiveBigIntegerField(default=0, verbose_name='view count')),
('file_type', models.CharField(choices=[('pdf', 'Pdf'), ('epub', 'Epub'), ('docx', 'Docx')], default='pdf', max_length=16, verbose_name='File Type')),
('book_file', models.FileField(blank=True, max_length=550, null=True, upload_to='books', verbose_name='Book File')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created at')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='updated at')),
],
options={
'verbose_name': 'Book',
'verbose_name_plural': 'Books',
},
),
migrations.CreateModel(
=======
>>>>>>> develop
name='Category', name='Category',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
@ -62,10 +24,6 @@ class Migration(migrations.Migration):
('status', models.BooleanField(default=True, verbose_name='status')), ('status', models.BooleanField(default=True, verbose_name='status')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created at')), ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created at')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='updated at')), ('updated_at', models.DateTimeField(auto_now=True, verbose_name='updated at')),
<<<<<<< HEAD
('books', models.ManyToManyField(blank=True, related_name='related_categories_books', to='library.Book', verbose_name='Books')),
=======
>>>>>>> develop
], ],
options={ options={
'verbose_name': 'Category', 'verbose_name': 'Category',
@ -73,18 +31,6 @@ class Migration(migrations.Migration):
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
<<<<<<< HEAD
name='BookDownload',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created at')),
('book', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='downloads', to='library.book', verbose_name='Book')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='book_downloads', to=settings.AUTH_USER_MODEL, verbose_name='User')),
],
options={
'verbose_name': 'Book Download',
'verbose_name_plural': 'Book Downloads',
=======
name='Book', name='Book',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
@ -107,27 +53,18 @@ class Migration(migrations.Migration):
options={ options={
'verbose_name': 'Book', 'verbose_name': 'Book',
'verbose_name_plural': 'Books', 'verbose_name_plural': 'Books',
>>>>>>> develop
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name='BookCollection', name='BookCollection',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
<<<<<<< HEAD
('title', models.JSONField(default=dict, verbose_name='title')),
=======
('title', models.CharField(max_length=255)), ('title', models.CharField(max_length=255)),
>>>>>>> develop
('summary', models.CharField(blank=True, help_text='could be null', max_length=512, null=True)), ('summary', models.CharField(blank=True, help_text='could be null', max_length=512, null=True)),
('display_position', models.CharField(choices=[('pinned', 'Pinned'), ('middle', 'Middle Section'), ('bottom', 'Bottom Section')], default='pinned', max_length=20, verbose_name='Display Position')), ('display_position', models.CharField(choices=[('pinned', 'Pinned'), ('middle', 'Middle Section'), ('bottom', 'Bottom Section')], default='pinned', max_length=20, verbose_name='Display Position')),
('status', models.BooleanField(default=True, verbose_name='status')), ('status', models.BooleanField(default=True, verbose_name='status')),
('order', models.IntegerField(default=0, verbose_name='order')), ('order', models.IntegerField(default=0, verbose_name='order')),
<<<<<<< HEAD
('books', models.ManyToManyField(blank=True, related_name='related_collections_books', to='library.Book', verbose_name='Books')),
=======
('books', models.ManyToManyField(blank=True, related_name='related_collections_books', to='library.book', verbose_name='Books')), ('books', models.ManyToManyField(blank=True, related_name='related_collections_books', to='library.book', verbose_name='Books')),
>>>>>>> develop
], ],
options={ options={
'verbose_name': 'Book Collection', 'verbose_name': 'Book Collection',
@ -136,23 +73,8 @@ class Migration(migrations.Migration):
), ),
migrations.AddField( migrations.AddField(
model_name='book', model_name='book',
<<<<<<< HEAD
name='categories',
field=models.ManyToManyField(blank=True, related_name='related_categories', to='library.Category', verbose_name='categories'),
),
migrations.AddField(
model_name='book',
name='collections',
field=models.ManyToManyField(blank=True, related_name='related_collections', to='library.BookCollection', verbose_name='collections'),
),
migrations.AddField(
model_name='book',
name='thumbnail',
field=filer.fields.image.FilerImageField(blank=True, help_text='image allowed', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.FILER_IMAGE_MODEL),
=======
name='collections', name='collections',
field=models.ManyToManyField(blank=True, related_name='related_collections', to='library.bookcollection', verbose_name='collections'), field=models.ManyToManyField(blank=True, related_name='related_collections', to='library.bookcollection', verbose_name='collections'),
>>>>>>> develop
), ),
migrations.CreateModel( migrations.CreateModel(
name='BottomBookCollection', name='BottomBookCollection',
@ -193,12 +115,9 @@ class Migration(migrations.Migration):
}, },
bases=('library.bookcollection',), bases=('library.bookcollection',),
), ),
<<<<<<< HEAD
=======
migrations.AddField( migrations.AddField(
model_name='book', model_name='book',
name='categories', name='categories',
field=models.ManyToManyField(blank=True, related_name='related_categories', to='library.category', verbose_name='categories'), field=models.ManyToManyField(blank=True, related_name='related_categories', to='library.category', verbose_name='categories'),
), ),
>>>>>>> develop
]
]

43
apps/quiz/admin/participant.py

@ -1,23 +1,3 @@
<<<<<<< HEAD
from ajaxdatatable.admin import AjaxDatatable
from django.contrib import admin
from django.db.models import F, Q
from django.contrib.admin import SimpleListFilter
from django.utils.translation import gettext_lazy as _
from apps.quiz.models import QuizParticipant, ParticipantAnswer
from apps.account.models import User
import datetime
class ParticipantAnswerInline(admin.StackedInline):
model = ParticipantAnswer
readonly_fields = (
'_correct_answer', 'question', 'at_time', 'answer_timing',
)
def _correct_answer(self, obj):
=======
from django.contrib import admin from django.contrib import admin
from django.db.models import F from django.db.models import F
from django.contrib.admin import SimpleListFilter from django.contrib.admin import SimpleListFilter
@ -39,7 +19,6 @@ class ParticipantAnswerInline(StackedInline):
@display(description="Correct Answer") @display(description="Correct Answer")
def correct_answer_display(self, obj): def correct_answer_display(self, obj):
>>>>>>> develop
return obj.correct_answer return obj.correct_answer
def has_add_permission(self, request, obj): def has_add_permission(self, request, obj):
@ -52,11 +31,6 @@ class ParticipantAnswerInline(StackedInline):
return super().get_queryset(request).annotate(correct_answer=F('question__correct_answer')) return super().get_queryset(request).annotate(correct_answer=F('question__correct_answer'))
<<<<<<< HEAD
=======
>>>>>>> develop
class UserEmailFilter(SimpleListFilter): class UserEmailFilter(SimpleListFilter):
title = _('User Email') title = _('User Email')
parameter_name = 'user_email' parameter_name = 'user_email'
@ -72,22 +46,6 @@ class UserEmailFilter(SimpleListFilter):
return queryset return queryset
<<<<<<< HEAD
@admin.register(QuizParticipant)
class ParticipantAdmin(AjaxDatatable):
inlines = [ParticipantAnswerInline]
search_fields = ['user__username', 'user__fullname']
list_display = ['quiz', 'user', 'started_at', 'ended_at', 'total_timing', 'question_score', 'timing_score',
'total_score']
latest_by = 'started_at'
list_filter = ['started_at', 'ended_at', 'quiz__status', UserEmailFilter]
=======
class ParticipantAdmin(ModelAdmin): class ParticipantAdmin(ModelAdmin):
inlines = [ParticipantAnswerInline] inlines = [ParticipantAnswerInline]
search_fields = ['user__username', 'user__fullname'] search_fields = ['user__username', 'user__fullname']
@ -102,4 +60,3 @@ class ParticipantAdmin(ModelAdmin):
ordering = ['-started_at'] ordering = ['-started_at']
project_admin_site.register(QuizParticipant, ParticipantAdmin) project_admin_site.register(QuizParticipant, ParticipantAdmin)
>>>>>>> develop

50
apps/quiz/admin/question.py

@ -1,24 +1,3 @@
<<<<<<< HEAD
from ajaxdatatable.admin import AjaxDatatable
from django import forms
from django.contrib import admin
from apps.quiz.models import Question
class QuestionAdminForm(forms.ModelForm):
class Meta:
model = Question
exclude = ()
widgets = {
'correct_answer': forms.RadioSelect,
'question': forms.Textarea
}
# @admin.register(Question)
# class QuestionAdmin(AjaxDatatable):
=======
from django import forms from django import forms
from django.contrib import admin from django.contrib import admin
@ -34,17 +13,12 @@ from utils.admin import project_admin_site
# Uncomment if you want to register Question as a standalone admin # Uncomment if you want to register Question as a standalone admin
# @admin.register(Question) # @admin.register(Question)
# class QuestionAdmin(ModelAdmin): # class QuestionAdmin(ModelAdmin):
>>>>>>> develop
# list_display = ('question', 'correct_answer', 'quiz', 'priority') # list_display = ('question', 'correct_answer', 'quiz', 'priority')
# form = QuestionAdminForm # form = QuestionAdminForm
# ordering = ("priority", "id",) # ordering = ("priority", "id",)
# fieldsets = ( # fieldsets = (
# ( # (
<<<<<<< HEAD
# '', {
=======
# None, { # None, {
>>>>>>> develop
# 'fields': ( # 'fields': (
# 'question', # 'question',
# ('option1', 'option2'), # ('option1', 'option2'),
@ -54,27 +28,11 @@ from utils.admin import project_admin_site
# }, # },
# ), # ),
# ( # (
<<<<<<< HEAD
# '', {
=======
# None, { # None, {
>>>>>>> develop
# 'fields': ('priority',) # 'fields': ('priority',)
# } # }
# ) # )
# ) # )
<<<<<<< HEAD
class QuestionAdminInline(admin.StackedInline):
model = Question
list_display = ('question', 'correct_answer', 'quiz', 'priority')
form = QuestionAdminForm
ordering = ("priority", "id",)
extra = 0
fieldsets = (
(
'', {
=======
@admin.register(Question) @admin.register(Question)
class QuestionAdmin(ModelAdmin): class QuestionAdmin(ModelAdmin):
list_display = ('question', 'correct_answer', 'quiz', 'priority') list_display = ('question', 'correct_answer', 'quiz', 'priority')
@ -109,7 +67,6 @@ class QuestionAdminInline(StackedInline):
fieldsets = ( fieldsets = (
( (
None, { None, {
>>>>>>> develop
'fields': ( 'fields': (
'question', 'question',
('option1', 'option2'), ('option1', 'option2'),
@ -119,17 +76,10 @@ class QuestionAdminInline(StackedInline):
}, },
), ),
( (
<<<<<<< HEAD
'', {
=======
None, { None, {
>>>>>>> develop
'fields': ('priority',) 'fields': ('priority',)
} }
) )
) )
<<<<<<< HEAD
=======
project_admin_site.register(Question, QuestionAdmin) project_admin_site.register(Question, QuestionAdmin)
>>>>>>> develop

57
apps/quiz/admin/quiz.py

@ -1,59 +1,3 @@
<<<<<<< HEAD
from ajaxdatatable.admin import AjaxDatatable
from django.contrib import admin
from django.db.models import Count
from django.utils.safestring import mark_safe
from django.utils.html import format_html
from django.urls import reverse, path
from apps.course.models import Lesson
from apps.quiz.models import Quiz
from .question import QuestionAdminInline
@admin.register(Quiz)
class QuizAdmin(AjaxDatatable):
search_fields = ['title', 'lesson__title']
list_display = ['title', 'description','lesson','each_question_timing', '_status', '_questions',]
autocomplete_fields = ['lesson',]
list_filter = ['each_question_timing',]
inlines = [QuestionAdminInline,]
def get_queryset(self, request):
queryset = super().get_queryset(request)
if request.user.groups.filter(name="Professor Group").exists():
return queryset.filter(lesson__course__professor=request.user)
return queryset
def get_form(self, request, obj=None, **kwargs):
form = super().get_form(request, obj, **kwargs)
if obj is None:
form.base_fields['lesson'].queryset = Lesson.objects.all() if request.user.is_staff else Lesson.objects.filter(course__professor=request.user)
form.base_fields['lesson'].widget.can_add_related = False
return form
@admin.display(description='Status', ordering='status')
def _status(self, obj):
if obj.status:
return mark_safe("<span class='badge badge-primary'>Active</span>")
return mark_safe("<span class='badge badge-warning'>Inactive</span>")
@admin.display(description='Questions', ordering='questions_count')
def _questions(self, obj):
return mark_safe(f"<a href='/admin/quiz/question/?quiz={obj.id}'>Questions: {obj.questions_count}</a>")
def get_queryset(self, request):
return super().get_queryset(request).annotate(
questions_count=Count('questions')
)
=======
from django.contrib import admin from django.contrib import admin
from django.db.models import Count from django.db.models import Count
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
@ -126,4 +70,3 @@ class QuizAdmin(ModelAdmin):
return mark_safe(f'<a href="{url}" class="unfold-link">Questions: {obj.questions_count}</a>') return mark_safe(f'<a href="{url}" class="unfold-link">Questions: {obj.questions_count}</a>')
project_admin_site.register(Quiz, QuizAdmin) project_admin_site.register(Quiz, QuizAdmin)
>>>>>>> develop

62
apps/quiz/migrations/0001_initial.py

@ -1,16 +1,6 @@
<<<<<<< HEAD
# Generated by Django 3.2.4 on 2024-11-29 11:00
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
=======
# Generated by Django 5.1.8 on 2025-04-03 00:05
import django.db.models.deletion import django.db.models.deletion
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
>>>>>>> develop
class Migration(migrations.Migration): class Migration(migrations.Migration):
@ -18,37 +8,13 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
<<<<<<< HEAD
('course', '0005_participant_unread_messages_count'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('account', '0004_user_skill'),
=======
('account', '0001_initial'), ('account', '0001_initial'),
('course', '0001_initial'), ('course', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
>>>>>>> develop
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
<<<<<<< HEAD
name='Quiz',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(help_text='Quiz Title', max_length=255, verbose_name='title')),
('each_question_timing', models.PositiveIntegerField()),
('status', models.BooleanField(default=True)),
('lesson', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='quizzes', to='course.lesson', verbose_name='lesson')),
],
options={
'verbose_name': 'Quiz',
'verbose_name_plural': 'Quizzes',
'ordering': ('-id',),
},
),
migrations.CreateModel(
=======
>>>>>>> develop
name='QuizRankUser', name='QuizRankUser',
fields=[ fields=[
], ],
@ -62,23 +28,6 @@ class Migration(migrations.Migration):
bases=('account.user',), bases=('account.user',),
), ),
migrations.CreateModel( migrations.CreateModel(
<<<<<<< HEAD
name='QuizParticipant',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('started_at', models.DateTimeField(verbose_name='started at')),
('ended_at', models.DateTimeField(verbose_name='ended at')),
('total_timing', models.PositiveIntegerField(help_text='Seconds take to finish the quiz')),
('question_score', models.PositiveIntegerField()),
('timing_score', models.PositiveIntegerField()),
('total_score', models.PositiveIntegerField()),
('quiz', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='participants', to='quiz.quiz')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='uquizzes', to=settings.AUTH_USER_MODEL, verbose_name='user')),
],
options={
'verbose_name': 'Participant',
'verbose_name_plural': 'Participants',
=======
name='Quiz', name='Quiz',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
@ -91,7 +40,6 @@ class Migration(migrations.Migration):
options={ options={
'verbose_name': 'Quiz', 'verbose_name': 'Quiz',
'verbose_name_plural': 'Quizzes', 'verbose_name_plural': 'Quizzes',
>>>>>>> develop
'ordering': ('-id',), 'ordering': ('-id',),
}, },
), ),
@ -116,8 +64,6 @@ class Migration(migrations.Migration):
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
<<<<<<< HEAD
=======
name='QuizParticipant', name='QuizParticipant',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
@ -137,20 +83,14 @@ class Migration(migrations.Migration):
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
>>>>>>> develop
name='ParticipantAnswer', name='ParticipantAnswer',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('option_num', models.PositiveSmallIntegerField(choices=[(1, 'Option 1'), (2, 'Option 2'), (3, 'Option 3'), (4, 'Option 4')], verbose_name='selected option')), ('option_num', models.PositiveSmallIntegerField(choices=[(1, 'Option 1'), (2, 'Option 2'), (3, 'Option 3'), (4, 'Option 4')], verbose_name='selected option')),
('at_time', models.DateTimeField()), ('at_time', models.DateTimeField()),
('answer_timing', models.PositiveSmallIntegerField(default=0, verbose_name='seconds take to answer')), ('answer_timing', models.PositiveSmallIntegerField(default=0, verbose_name='seconds take to answer')),
<<<<<<< HEAD
('participant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='answers', to='quiz.quizparticipant')),
('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='quiz.question')),
=======
('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='quiz.question')), ('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='quiz.question')),
('participant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='answers', to='quiz.quizparticipant')), ('participant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='answers', to='quiz.quizparticipant')),
>>>>>>> develop
], ],
options={ options={
'verbose_name': 'User Quiz Answer', 'verbose_name': 'User Quiz Answer',
@ -158,4 +98,4 @@ class Migration(migrations.Migration):
'ordering': ('-id',), 'ordering': ('-id',),
}, },
), ),
]
]

3
apps/quiz/models/participant.py

@ -1,9 +1,6 @@
from django.db import models from django.db import models
<<<<<<< HEAD
=======
from django.db.models import F, Window from django.db.models import F, Window
from django.db.models.functions import Rank from django.db.models.functions import Rank
>>>>>>> develop
from apps.account.models import User from apps.account.models import User

4
apps/quiz/models/quiz.py

@ -5,12 +5,8 @@ from apps.account.models import User
class Quiz(models.Model): class Quiz(models.Model):
<<<<<<< HEAD
lesson = models.ForeignKey("course.Lesson", verbose_name=_('lesson'), related_name='quizzes', on_delete=models.CASCADE)
=======
lesson = models.ForeignKey("course.CourseLesson", verbose_name=_('lesson'), related_name='quizzes', on_delete=models.CASCADE) lesson = models.ForeignKey("course.CourseLesson", verbose_name=_('lesson'), related_name='quizzes', on_delete=models.CASCADE)
>>>>>>> develop
title = models.CharField(max_length=255, verbose_name=_('title'), help_text="Quiz Title") title = models.CharField(max_length=255, verbose_name=_('title'), help_text="Quiz Title")
description = models.CharField(max_length=55, blank=True, null=True, verbose_name="Description") description = models.CharField(max_length=55, blank=True, null=True, verbose_name="Description")
each_question_timing = models.PositiveIntegerField() each_question_timing = models.PositiveIntegerField()

13
apps/quiz/serializers/quiz.py

@ -1,11 +1,7 @@
from rest_framework import serializers from rest_framework import serializers
from apps.quiz.models import Question, Quiz, QuizParticipant from apps.quiz.models import Question, Quiz, QuizParticipant
<<<<<<< HEAD
from apps.course.models import Lesson, Participant
=======
from apps.course.models import Participant from apps.course.models import Participant
>>>>>>> develop
@ -27,9 +23,6 @@ class QuizListSerializer(serializers.ModelSerializer):
return False return False
# Check if the user has participated in this quiz # Check if the user has participated in this quiz
user = request.user user = request.user
<<<<<<< HEAD
course = obj.lesson.course
=======
# obj.lesson is now CourseLesson directly # obj.lesson is now CourseLesson directly
course_lesson = obj.lesson course_lesson = obj.lesson
@ -37,7 +30,6 @@ class QuizListSerializer(serializers.ModelSerializer):
return False return False
course = course_lesson.course course = course_lesson.course
>>>>>>> develop
if not self._is_participant(user, course): if not self._is_participant(user, course):
return False return False
@ -92,10 +84,6 @@ class QuizSerializer(serializers.ModelSerializer):
return False return False
# Check if the user has participated in this quiz # Check if the user has participated in this quiz
user = request.user user = request.user
<<<<<<< HEAD
participated = QuizParticipant.objects.filter(user=user, quiz=obj).exists()
return not participated
=======
# obj.lesson is now CourseLesson directly # obj.lesson is now CourseLesson directly
course_lesson = obj.lesson course_lesson = obj.lesson
@ -110,4 +98,3 @@ class QuizSerializer(serializers.ModelSerializer):
participated = QuizParticipant.objects.filter(user=user, quiz=obj).exists() participated = QuizParticipant.objects.filter(user=user, quiz=obj).exists()
return participated return participated
>>>>>>> develop

5
apps/quiz/views/participant.py

@ -17,10 +17,7 @@ class QuizParticipantCreateAPIView(CreateAPIView):
@swagger_auto_schema( @swagger_auto_schema(
operation_description=doc_quiz_submit(), operation_description=doc_quiz_submit(),
<<<<<<< HEAD
=======
tags=["Imam-Javad - Quiz"], tags=["Imam-Javad - Quiz"],
>>>>>>> develop
) )
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
return super().post(request, *args, **kwargs)
return super().post(request, *args, **kwargs)

7
apps/quiz/views/quiz.py

@ -17,10 +17,7 @@ class QuizDetailAPIView(RetrieveAPIView):
@swagger_auto_schema( @swagger_auto_schema(
operation_description=doc_quiz_detail(), operation_description=doc_quiz_detail(),
<<<<<<< HEAD
=======
tags=["Imam-Javad - Quiz"], tags=["Imam-Javad - Quiz"],
>>>>>>> develop
) )
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
@ -31,7 +28,3 @@ class QuizDetailAPIView(RetrieveAPIView):
).annotate( ).annotate(
lesson__has_quiz=Value(True) lesson__has_quiz=Value(True)
).select_related('lesson').first() ).select_related('lesson').first()

20
apps/transaction/migrations/0001_initial.py

@ -1,12 +1,3 @@
<<<<<<< HEAD
# Generated by Django 3.2.4 on 2024-11-30 22:25
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import phonenumber_field.modelfields
import utils.validators
=======
# Generated by Django 5.1.8 on 2025-04-03 00:05 # Generated by Django 5.1.8 on 2025-04-03 00:05
import django.db.models.deletion import django.db.models.deletion
@ -14,7 +5,6 @@ import phonenumber_field.modelfields
import utils.validators import utils.validators
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
>>>>>>> develop
class Migration(migrations.Migration): class Migration(migrations.Migration):
@ -22,13 +12,8 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
<<<<<<< HEAD
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('course', '0005_participant_unread_messages_count'),
=======
('course', '0001_initial'), ('course', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
>>>>>>> develop
] ]
operations = [ operations = [
@ -49,13 +34,8 @@ class Migration(migrations.Migration):
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('fullname', models.CharField(help_text='Enter the full name of the user.', max_length=255, verbose_name='Full Name')), ('fullname', models.CharField(help_text='Enter the full name of the user.', max_length=255, verbose_name='Full Name')),
<<<<<<< HEAD
('email', models.EmailField(help_text="Enter the user's email address.", max_length=254, unique=True, verbose_name='Email Address')),
('phone_number', phonenumber_field.modelfields.PhoneNumberField(blank=True, max_length=128, null=True, region=None, unique=True, validators=[utils.validators.validate_possible_number], verbose_name='phone')),
=======
('email', models.EmailField(help_text="Enter the user's email address.", max_length=254, verbose_name='Email Address')), ('email', models.EmailField(help_text="Enter the user's email address.", max_length=254, verbose_name='Email Address')),
('phone_number', phonenumber_field.modelfields.PhoneNumberField(blank=True, max_length=128, null=True, region=None, validators=[utils.validators.validate_possible_number], verbose_name='phone')), ('phone_number', phonenumber_field.modelfields.PhoneNumberField(blank=True, max_length=128, null=True, region=None, validators=[utils.validators.validate_possible_number], verbose_name='phone')),
>>>>>>> develop
('gender', models.CharField(blank=True, choices=[('male', 'Male'), ('female', 'Female')], help_text="Select the user's gender.", max_length=20, null=True, verbose_name='Gender')), ('gender', models.CharField(blank=True, choices=[('male', 'Male'), ('female', 'Female')], help_text="Select the user's gender.", max_length=20, null=True, verbose_name='Gender')),
('birthdate', models.DateField(blank=True, null=True, verbose_name='birthdate')), ('birthdate', models.DateField(blank=True, null=True, verbose_name='birthdate')),
('transaction_participant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='participant_infos', to='transaction.transactionparticipant', verbose_name='Transaction Participant')), ('transaction_participant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='participant_infos', to='transaction.transactionparticipant', verbose_name='Transaction Participant')),

52
apps/video/migrations/0001_initial.py

@ -1,18 +1,9 @@
<<<<<<< HEAD
# Generated by Django 3.2.7 on 2025-03-21 22:06
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import filer.fields.image
=======
# Generated by Django 5.1.8 on 2025-04-03 00:05 # Generated by Django 5.1.8 on 2025-04-03 00:05
import django.db.models.deletion import django.db.models.deletion
import filer.fields.image import filer.fields.image
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
>>>>>>> develop
class Migration(migrations.Migration): class Migration(migrations.Migration):
@ -25,30 +16,6 @@ class Migration(migrations.Migration):
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
<<<<<<< HEAD
name='Video',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=255, null=True)),
('slug', models.SlugField(allow_unicode=True, unique=True)),
('description', models.TextField(null=True)),
('video_type', models.CharField(choices=[('file', 'File'), ('youtube', 'Youtube')], default='file', max_length=255)),
('video_file', models.FileField(blank=True, null=True, upload_to='video/videos/')),
('video_url', models.CharField(blank=True, max_length=655, null=True)),
('video_time', models.TimeField()),
('view_count', models.PositiveBigIntegerField(default=0, verbose_name='view count')),
('status', models.BooleanField(default=True, verbose_name='status')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created at')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='updated at')),
],
options={
'verbose_name': 'Video',
'verbose_name_plural': 'Videos',
},
),
migrations.CreateModel(
=======
>>>>>>> develop
name='VideoCategory', name='VideoCategory',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
@ -80,8 +47,6 @@ class Migration(migrations.Migration):
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
<<<<<<< HEAD
=======
name='Video', name='Video',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
@ -105,7 +70,6 @@ class Migration(migrations.Migration):
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
>>>>>>> develop
name='VideoInCollection', name='VideoInCollection',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
@ -122,20 +86,6 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='videocollection', model_name='videocollection',
name='videos', name='videos',
<<<<<<< HEAD
field=models.ManyToManyField(related_name='collections', through='video.VideoInCollection', to='video.Video', verbose_name='videos'),
),
migrations.AddField(
model_name='video',
name='categories',
field=models.ManyToManyField(blank=True, related_name='videos', to='video.VideoCategory', verbose_name='categories'),
),
migrations.AddField(
model_name='video',
name='thumbnail',
field=filer.fields.image.FilerImageField(blank=True, help_text='image allowed', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.FILER_IMAGE_MODEL),
=======
field=models.ManyToManyField(related_name='collections', through='video.VideoInCollection', to='video.video', verbose_name='videos'), field=models.ManyToManyField(related_name='collections', through='video.VideoInCollection', to='video.video', verbose_name='videos'),
>>>>>>> develop
), ),
]
]

22
dynamic_preferences/admin.py

@ -8,11 +8,8 @@ from .models import GlobalPreferenceModel
from .forms import GlobalSinglePreferenceForm, SinglePerInstancePreferenceForm from .forms import GlobalSinglePreferenceForm, SinglePerInstancePreferenceForm
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
<<<<<<< HEAD
=======
from unfold.admin import ModelAdmin, TabularInline from unfold.admin import ModelAdmin, TabularInline
from utils.admin import project_admin_site from utils.admin import project_admin_site
>>>>>>> develop
class SectionFilter(admin.AllValuesFieldListFilter): class SectionFilter(admin.AllValuesFieldListFilter):
def __init__(self, field, request, params, model, model_admin, field_path): def __init__(self, field, request, params, model, model_admin, field_path):
@ -45,12 +42,8 @@ class SectionFilter(admin.AllValuesFieldListFilter):
yield choice yield choice
<<<<<<< HEAD
class DynamicPreferenceAdmin(AjaxDatatable):
=======
# Change DynamicPreferenceAdmin to inherit from unfold's ModelAdmin # Change DynamicPreferenceAdmin to inherit from unfold's ModelAdmin
class DynamicPreferenceAdmin(ModelAdmin): class DynamicPreferenceAdmin(ModelAdmin):
>>>>>>> develop
list_display = ( list_display = (
"verbose_name", "verbose_name",
"help_text", "help_text",
@ -58,15 +51,11 @@ class DynamicPreferenceAdmin(ModelAdmin):
fields = ("raw_value", "default_value",) fields = ("raw_value", "default_value",)
readonly_fields = ("default_value",) readonly_fields = ("default_value",)
change_form_template = "dynamic_preferences/dyna_change_form.html" change_form_template = "dynamic_preferences/dyna_change_form.html"
<<<<<<< HEAD
=======
# Unfold specific settings # Unfold specific settings
search_fields = ["name", "section"] search_fields = ["name", "section"]
list_filter = ["section"] list_filter = ["section"]
>>>>>>> develop
@admin.display(description=_('Verbose name')) @admin.display(description=_('Verbose name'))
def verbose_name(self, obj): def verbose_name(self, obj):
return obj.verbose_name return obj.verbose_name
@ -112,8 +101,6 @@ class DynamicPreferenceAdmin(ModelAdmin):
class GlobalPreferenceAdmin(DynamicPreferenceAdmin): class GlobalPreferenceAdmin(DynamicPreferenceAdmin):
form = GlobalSinglePreferenceForm form = GlobalSinglePreferenceForm
changelist_form = GlobalSinglePreferenceForm changelist_form = GlobalSinglePreferenceForm
<<<<<<< HEAD
=======
# Unfold specific customizations # Unfold specific customizations
list_display_links = ["verbose_name"] list_display_links = ["verbose_name"]
@ -129,7 +116,6 @@ class GlobalPreferenceAdmin(DynamicPreferenceAdmin):
manager = pref.registry.manager() manager = pref.registry.manager()
manager.update_db_pref(pref.section, pref.name, pref.preference.default) manager.update_db_pref(pref.section, pref.name, pref.preference.default)
reset_to_default.short_description = _("Reset selected preferences to default values") reset_to_default.short_description = _("Reset selected preferences to default values")
>>>>>>> develop
def get_queryset(self, *args, **kwargs): def get_queryset(self, *args, **kwargs):
# Instanciate default prefs # Instanciate default prefs
@ -138,14 +124,10 @@ class GlobalPreferenceAdmin(DynamicPreferenceAdmin):
return super(GlobalPreferenceAdmin, self).get_queryset(*args, **kwargs) return super(GlobalPreferenceAdmin, self).get_queryset(*args, **kwargs)
<<<<<<< HEAD
admin.site.register(GlobalPreferenceModel, GlobalPreferenceAdmin)
=======
project_admin_site.register(GlobalPreferenceModel, GlobalPreferenceAdmin) project_admin_site.register(GlobalPreferenceModel, GlobalPreferenceAdmin)
>>>>>>> develop
class PerInstancePreferenceAdmin(DynamicPreferenceAdmin): class PerInstancePreferenceAdmin(DynamicPreferenceAdmin):
@ -155,7 +137,3 @@ class PerInstancePreferenceAdmin(DynamicPreferenceAdmin):
form = SinglePerInstancePreferenceForm form = SinglePerInstancePreferenceForm
changelist_form = SinglePerInstancePreferenceForm changelist_form = SinglePerInstancePreferenceForm
list_select_related = True list_select_related = True
<<<<<<< HEAD
=======
>>>>>>> develop

58
dynamic_preferences/locale/fa/LC_MESSAGES/django.po

@ -8,11 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
<<<<<<< HEAD
"POT-Creation-Date: 2023-02-16 15:12+0330\n"
=======
"POT-Creation-Date: 2025-12-23 15:18+0330\n" "POT-Creation-Date: 2025-12-23 15:18+0330\n"
>>>>>>> develop
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -22,59 +18,6 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n"
<<<<<<< HEAD
#: admin.py:69
msgid "Default Value"
msgstr "مقدار پیشفرض"
#: admin.py:78 models.py:30
msgid "Section Name"
msgstr "عنوان بخش"
#: apps.py:10
msgid "Dynamic Preferences"
msgstr "تنظیمات"
#: models.py:34
msgid "Name"
msgstr "نام"
#: models.py:37
msgid "Raw Value"
msgstr "مقدار"
#: models.py:51
msgid "Verbose Name"
msgstr "نام"
#: models.py:57
msgid "Help Text"
msgstr "متن راهنما"
#: models.py:94
msgid "Global preference"
msgstr "تنطیمات عمومی"
#: models.py:95
msgid "Global preferences"
msgstr "تنطیمات عمومی"
#: templates/dynamic_preferences/form.html:11
msgid "Submit"
msgstr "ثبت"
#: users/apps.py:11
msgid "Preferences - Users"
msgstr ""
#: users/models.py:14
msgid "user preference"
msgstr ""
#: users/models.py:15
msgid "user preferences"
msgstr ""
=======
#: .\dynamic_preferences\admin.py:59 #: .\dynamic_preferences\admin.py:59
#, fuzzy #, fuzzy
#| msgid "Verbose Name" #| msgid "Verbose Name"
@ -145,4 +88,3 @@ msgstr ""
#~ msgid "Dynamic Preferences" #~ msgid "Dynamic Preferences"
#~ msgstr "تنظیمات" #~ msgstr "تنظیمات"
>>>>>>> develop

54
utils/__init__.py

@ -5,14 +5,11 @@ import mimetypes
import re import re
from urllib.parse import urlparse from urllib.parse import urlparse
<<<<<<< HEAD
=======
from django.core.files.storage import default_storage from django.core.files.storage import default_storage
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from pathlib import Path from pathlib import Path
from django.utils.text import get_valid_filename from django.utils.text import get_valid_filename
>>>>>>> develop
from django.conf import settings from django.conf import settings
from django.core.files import File from django.core.files import File
from django.http import HttpRequest from django.http import HttpRequest
@ -27,9 +24,6 @@ from django.utils.text import slugify
import random import random
import string import string
<<<<<<< HEAD
=======
from django.conf import settings from django.conf import settings
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -84,7 +78,6 @@ def environment_callback(request):
return [_("Development"), "primary"] return [_("Development"), "primary"]
return [_("Production"), "primary"] return [_("Production"), "primary"]
>>>>>>> develop
@ -170,8 +163,6 @@ def generate_slug_for_model(model, value: str, recycled_count: int = 0):
return slug[:50] return slug[:50]
<<<<<<< HEAD
=======
def generate_language_slugs(translations): def generate_language_slugs(translations):
""" """
@ -196,7 +187,6 @@ def generate_language_slugs(translations):
print(f"Error generating slugs: {e}") print(f"Error generating slugs: {e}")
return [] return []
>>>>>>> develop
def absolute_url(req, url): def absolute_url(req, url):
""" """
can either be a file instance or a URL string can either be a file instance or a URL string
@ -213,8 +203,6 @@ def sizeof_fmt(num, suffix="B"):
num /= 1024.0 num /= 1024.0
return f"{num:.1f} Yi{suffix}" return f"{num:.1f} Yi{suffix}"
<<<<<<< HEAD
=======
def file_location_media(path: str): def file_location_media(path: str):
""" """
Resolve a media URL/relative path to absolute filesystem path under MEDIA_ROOT. Resolve a media URL/relative path to absolute filesystem path under MEDIA_ROOT.
@ -235,7 +223,6 @@ def file_location_media(path: str):
return os.path.join(media_root, path) return os.path.join(media_root, path)
>>>>>>> develop
def file_location(path): def file_location(path):
from django.conf import settings from django.conf import settings
@ -291,16 +278,6 @@ class FileFieldSerializer(serializers.CharField):
# value not changed and here we simply return old file path # value not changed and here we simply return old file path
return self.get_rpath(data) return self.get_rpath(data)
<<<<<<< HEAD
if data.startswith('http'):
data = self.get_rpath(data)
fpath = file_location(data)
if not os.path.exists(fpath):
raise serializers.ValidationError(f"File: '{fpath}' Does not exist")
return File(open(fpath, 'rb'), os.path.basename(data))
=======
# if data.startswith('http'): # if data.startswith('http'):
# data = self.get_rpath(data) # data = self.get_rpath(data)
@ -463,7 +440,6 @@ class UploadChatMediaSerializer(serializers.Serializer):
def validate(self, attrs): def validate(self, attrs):
file_details = self.store_file(attrs['file']) file_details = self.store_file(attrs['file'])
return file_details return file_details
>>>>>>> develop
class UploadTmpSerializer(serializers.Serializer): class UploadTmpSerializer(serializers.Serializer):
@ -472,10 +448,7 @@ class UploadTmpSerializer(serializers.Serializer):
name = serializers.CharField(read_only=True) name = serializers.CharField(read_only=True)
size = serializers.CharField(read_only=True) size = serializers.CharField(read_only=True)
mime_type = serializers.CharField(read_only=True) mime_type = serializers.CharField(read_only=True)
<<<<<<< HEAD
=======
thumbnail_url = serializers.URLField(read_only=True, required=False) thumbnail_url = serializers.URLField(read_only=True, required=False)
>>>>>>> develop
def to_representation(self, instance): def to_representation(self, instance):
data = super(UploadTmpSerializer, self).to_representation(instance) data = super(UploadTmpSerializer, self).to_representation(instance)
@ -484,23 +457,6 @@ class UploadTmpSerializer(serializers.Serializer):
def store_file(self, file): def store_file(self, file):
from django.conf import settings from django.conf import settings
<<<<<<< HEAD
static_path = settings.STATIC_ROOT
os.makedirs(f'{static_path}/tmp', exist_ok=True)
fpath = f"/tmp/{secrets.token_urlsafe(4)}-{file.name}"
shutil.move(file.temporary_file_path(), static_path + fpath)
os.chmod(static_path + fpath, 0o644)
return {
'file': fpath,
'url': absolute_url(self.context['request'], f"/static{fpath}"),
'name': file.name,
'size': sizeof_fmt(file.size),
'mime_type': guess_file_type(fpath)
}
=======
from utils.image_utils import ( from utils.image_utils import (
create_thumbnail, create_thumbnail,
is_image_file, is_image_file,
@ -569,17 +525,11 @@ class UploadTmpSerializer(serializers.Serializer):
return result return result
>>>>>>> develop
def validate(self, attrs): def validate(self, attrs):
file_details = self.store_file(attrs['file']) file_details = self.store_file(attrs['file'])
return file_details return file_details
<<<<<<< HEAD
class UploadTmpMedia(GenericAPIView):
"""
Files will remove every 1 hour
=======
class UploadChatMedia(GenericAPIView): class UploadChatMedia(GenericAPIView):
""" """
Upload files permanently to /media/chat/ Upload files permanently to /media/chat/
@ -601,7 +551,6 @@ class UploadTmpMedia(GenericAPIView):
""" """
Upload files temporarily to /static/tmp/ Upload files temporarily to /static/tmp/
Files will be removed every 1 hour Files will be removed every 1 hour
>>>>>>> develop
""" """
parser_classes = (FormParser, MultiPartParser) parser_classes = (FormParser, MultiPartParser)
serializer_class = UploadTmpSerializer serializer_class = UploadTmpSerializer
@ -613,8 +562,6 @@ class UploadTmpMedia(GenericAPIView):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
return Response(serializer.data) return Response(serializer.data)
<<<<<<< HEAD
=======
# Configure filer admin after Django is fully loaded # Configure filer admin after Django is fully loaded
def configure_filer_admin(): def configure_filer_admin():
@ -628,4 +575,3 @@ def configure_filer_admin():
pass pass
# This will be executed when this module is imported after Django is fully loaded # This will be executed when this module is imported after Django is fully loaded
>>>>>>> develop

10
utils/json_editor_field.py

@ -1,13 +1,4 @@
import json import json
<<<<<<< HEAD
from django import forms
from django.db import models
class JsonEditorWidget(forms.Textarea):
template_name = 'fields/json_editor_field.html'
=======
from typing import Any, Optional from typing import Any, Optional
from django import forms from django import forms
@ -71,7 +62,6 @@ class JsonEditorWidget(Widget):
attrs['title'] = name.replace('_', ' ').title() attrs['title'] = name.replace('_', ' ').title()
return super().render(name, value, attrs, renderer) return super().render(name, value, attrs, renderer)
>>>>>>> develop
class JsonEditorField(models.JSONField): class JsonEditorField(models.JSONField):

12
utils/redis.py

@ -1,10 +1,3 @@
<<<<<<< HEAD
import random
from datetime import datetime, timedelta
from redis.exceptions import RedisError
=======
import json import json
import hashlib import hashlib
import random import random
@ -17,7 +10,6 @@ from redis.exceptions import RedisError
from django.conf import settings from django.conf import settings
>>>>>>> develop
from config.redis_config import RedisConfig from config.redis_config import RedisConfig
from utils.exceptions import ServiceUnavailableException, NotFoundException from utils.exceptions import ServiceUnavailableException, NotFoundException
@ -82,9 +74,6 @@ class RedisManager(RedisConfig):
@staticmethod @staticmethod
def generate_otp_code() -> int: def generate_otp_code() -> int:
random_code = random.randint(10000, 99999) random_code = random.randint(10000, 99999)
<<<<<<< HEAD
return random_code
=======
return random_code return random_code
@ -130,4 +119,3 @@ class OnlineClassTokenManager(RedisConfig):
query_params["token"] = token query_params["token"] = token
new_query = urlencode(query_params) new_query = urlencode(query_params)
return urlunparse(parsed._replace(query=new_query)) return urlunparse(parsed._replace(query=new_query))
>>>>>>> develop

7
utils/schema.py

@ -36,10 +36,6 @@ def get_weekly_timing_schema():
} }
<<<<<<< HEAD
=======
>>>>>>> develop
def get_course_feature_schema(): def get_course_feature_schema():
return { return {
'type': "array", 'type': "array",
@ -53,8 +49,6 @@ def get_course_feature_schema():
} }
} }
} }
<<<<<<< HEAD
=======
def get_calender_dates_schema(): def get_calender_dates_schema():
@ -72,4 +66,3 @@ def get_calender_dates_schema():
} }
} }
} }
>>>>>>> develop

3
utils/validators.py

@ -21,10 +21,7 @@ def validate_possible_number(phone, country=None):
return phone_number return phone_number
def validate_type_code(value): def validate_type_code(value):
<<<<<<< HEAD
=======
from rest_framework import serializers from rest_framework import serializers
>>>>>>> develop
if not value.isdigit(): if not value.isdigit():
raise serializers.ValidationError('کد باید شامل اعداد باشد.') raise serializers.ValidationError('کد باید شامل اعداد باشد.')
if len(value) != 5: if len(value) != 5:

Loading…
Cancel
Save