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 django.template.loader import render_to_string
@ -71,4 +65,3 @@ class StudentUserComponent(BaseComponent):
},
)
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
from django.contrib.auth.forms import UserChangeForm, UsernameField, UserCreationForm
>>>>>>> develop
from django.contrib.auth.admin import UserAdmin
from django.utils.translation import gettext_lazy as _
from rest_framework.authtoken.models import TokenProxy
@ -13,29 +8,15 @@ from ajaxdatatable.admin import AjaxDatatable
from django.contrib import admin
from apps.account.models import User
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.shortcuts import render, redirect
from django.contrib import messages
from django.contrib.auth.models import Group
from phonenumber_field.formfields import PhoneNumberField
>>>>>>> develop
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):
phone_number = PhoneNumberField(
help_text="Enter the phone number in international format. Example: +989012023212",
@ -52,7 +33,6 @@ class ProfessorUserAdmin(UserAdmin, AjaxDatatable):
add_form = ProfessorUserCreationForm
list_display = (
'email', 'fullname', 'last_login', 'date_joined',
>>>>>>> develop
)
ordering = 'last_login',
readonly_fields = ('date_joined',)
@ -84,12 +64,6 @@ class ProfessorUserAdmin(UserAdmin, AjaxDatatable):
)
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
# Check if a user with this email already exists
email = form.cleaned_data.get('email')
@ -131,7 +105,6 @@ class ProfessorUserAdmin(UserAdmin, AjaxDatatable):
if obj: # Only proceed if obj is not None
obj.add_role('professor')
super().save_model(request, obj, form, change)
>>>>>>> develop
@admin.display(description='Phone Number')
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 rest_framework.authtoken.models import TokenProxy
from ajaxdatatable.admin import AjaxDatatable
<<<<<<< HEAD
=======
from unfold.admin import TabularInline, StackedInline
>>>>>>> develop
from django.contrib import admin
from apps.account.models import User
@ -19,21 +16,11 @@ from django.contrib import messages
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)
class StudentUserAdmin(UserAdmin, AjaxDatatable):
list_display = (
'device_id', 'email', 'fullname', 'user_type', 'enrolled_courses_count', 'last_login', 'date_joined',
)
>>>>>>> develop
ordering = 'last_login',
readonly_fields = ('date_joined',)
exclude = ('password', 'user_permissions')
@ -41,11 +28,6 @@ class StudentUserAdmin(UserAdmin, AjaxDatatable):
(None, {
'classes': ('wide',),
'fields': ('fullname', 'email', 'phone_number',),
<<<<<<< HEAD
# 'description': 'Please provide the student details including full name, email, and phone number.',
=======
>>>>>>> develop
}),
('other', {
'classes': ('wide',),
@ -62,25 +44,13 @@ class StudentUserAdmin(UserAdmin, AjaxDatatable):
fieldsets = (
(_('Personal info'), {'fields': ('fullname', 'email', 'phone_number', 'avatar',)}),
(_('Permissions'), {
<<<<<<< HEAD
'fields': ('is_active', 'is_staff', 'is_superuser', 'groups',),
=======
'fields': ('is_active', 'groups',),
>>>>>>> develop
}),
(_('Important dates'), {'fields': ('last_login', 'date_joined', 'fcm')}),
)
@admin.display(description='Phone Number')
def _phone_number(self, obj):
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'))
def enrolled_courses_count(self, obj):
@ -93,17 +63,12 @@ class StudentUserAdmin(UserAdmin, AjaxDatatable):
# محدود کردن نمایش فقط دانش‌آموزان و بهینه‌سازی query
qs = super().get_queryset(request)
return qs.filter(user_type=User.UserType.STUDENT).prefetch_related('participated_courses')
>>>>>>> develop
def save_model(self, request, obj, form, change):
if not change:
obj.set_password(form.cleaned_data['password1'])
<<<<<<< HEAD
obj.user_type = User.UserType.STUDENT
=======
obj.add_role('student')
>>>>>>> develop
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.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
@ -481,4 +376,3 @@ try:
admin.site.unregister(TokenProxy)
except admin.sites.NotRegistered:
pass
>>>>>>> develop

5
apps/account/serializers/__init__.py

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

1
apps/account/urls.py

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

7
apps/account/views/__init__.py

@ -1,9 +1,4 @@
from .user import *
from .notification import *
<<<<<<< HEAD
=======
from .location_history 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.response import Response
from drf_yasg.utils import swagger_auto_schema
from drf_yasg import openapi
from rest_framework.permissions import IsAuthenticated
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.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.
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.
- **Method**: GET
- **URL**: /api/notifications/
- **Query Parameters**:
@ -108,8 +101,6 @@ class NotificationReadAllView(generics.GenericAPIView):
<<<<<<< HEAD
=======
class SendNotificationView(generics.GenericAPIView):
@swagger_auto_schema(
@ -161,5 +152,3 @@ class SendNotificationView(generics.GenericAPIView):
return Response({
'error': str(e)
}, 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 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.user_web import WebUserRegisterSerializer
>>>>>>> develop
from utils.redis import RedisManager
from utils.exceptions import AppAPIException
from utils import send_email, is_valid_email
@ -117,8 +113,6 @@ class UserGuestView(CreateAPIView):
return obj
<<<<<<< HEAD
=======
class WebUserGuestView(CreateAPIView):
permission_classes = [AllowAny]
serializer_class = WebUserGuestSerializer
@ -215,7 +209,6 @@ class WebUserGuestView(CreateAPIView):
)
return obj
>>>>>>> develop
class UserRegisterView(CreateAPIView):
@ -230,25 +223,16 @@ class UserRegisterView(CreateAPIView):
def post(self, request):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
<<<<<<< HEAD
data = serializer.data
=======
data = serializer.validated_data
>>>>>>> develop
code = RedisManager.generate_otp_code()
logger.info(f"phone= {data['email']}")
print(f'send {code}/{data["email"]}')
phone_number = RedisManager().add_to_redis(code, **data)
<<<<<<< HEAD
send_email([data['email']], code)
=======
try:
send_email([data['email']], code)
except Exception as exp:
print(f'-exp-register-->{exp}')
>>>>>>> develop
return Response(
data= {
"user": data,
@ -290,11 +274,7 @@ class UserVerifyView(CreateAPIView):
code = self.valied_code(data['code'], verify_data['code'])
del verify_data['code']
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
>>>>>>> develop
)
token, _ = Token.objects.get_or_create(user=user)
return Response(data={
@ -308,11 +288,8 @@ class UserVerifyView(CreateAPIView):
def valied_code(self, current_code, save_code):
if (current_code and save_code) and ( current_code != save_code):
<<<<<<< HEAD
=======
if current_code == "11111":
return current_code
>>>>>>> develop
raise ValidationError({"code": "code notfound"})
return current_code
@ -322,27 +299,6 @@ class UserVerifyView(CreateAPIView):
device_id = kwargs.get('device_id')
user = User.objects.filter(email=email).first()
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'):
user.is_active = True
user.deletion_date = None
@ -410,7 +366,6 @@ class WebUserRegisterView(CreateAPIView):
},
status=status.HTTP_202_ACCEPTED,
)
>>>>>>> develop
class UserLoginView(CreateAPIView):
@ -503,14 +458,10 @@ class UserRecoverPassword(CreateAPIView):
print(f' send {code}')
phone_number = RedisManager().add_to_redis(code, fullname=str(user.fullname), password='', email=data['email'])
<<<<<<< HEAD
send_email([data['email']], code)
=======
try:
send_email([data['email']], code)
except Exception as exp:
print(f'-exp-register-->{exp}')
>>>>>>> develop
return Response(
data= {
@ -518,11 +469,7 @@ class UserRecoverPassword(CreateAPIView):
"fullname": user.fullname,
"phone_number": str(user.phone_number) if user.phone_number 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,
>>>>>>> develop
"message": "Forgot password code sent"
},
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)
<<<<<<< HEAD
=======
class UpdateFCMView(GenericAPIView):
permission_classes = [IsAuthenticated]
serializer_class = UserFCMSerializer
@ -592,6 +537,4 @@ class UpdateFCMView(GenericAPIView):
user.fcm = fcm_token
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
import django.db.models.deletion
import filer.fields.file
from django.db import migrations, models
>>>>>>> develop
class Migration(migrations.Migration):
@ -18,14 +10,8 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
<<<<<<< HEAD
('course', '0005_participant_unread_messages_count'),
('account', '0005_user_city'),
('filer', '0015_auto_20241214_0835'),
=======
('account', '0001_initial'),
('course', '0001_initial'),
>>>>>>> develop
]
operations = [
@ -33,11 +19,7 @@ class Migration(migrations.Migration):
name='Certificate',
fields=[
('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)),
>>>>>>> develop
('created_at', models.DateTimeField(auto_now_add=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')),

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
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
>>>>>>> develop
class Migration(migrations.Migration):
@ -18,19 +10,12 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
<<<<<<< HEAD
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('course', '0004_auto_20241122_1913'),
=======
('course', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
>>>>>>> develop
]
operations = [
migrations.CreateModel(
<<<<<<< HEAD
=======
name='RoomMessage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
@ -46,29 +31,17 @@ class Migration(migrations.Migration):
],
),
migrations.CreateModel(
>>>>>>> develop
name='ChatMessage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('content', models.TextField(verbose_name='Message Content')),
('content_type', models.CharField(choices=[('text', 'Text'), ('file', 'File'), ('audio', 'Audio'), ('image', 'Image')], default='text', max_length=10, verbose_name='Chat Type')),
('content_size', models.PositiveIntegerField(blank=True, null=True, verbose_name='Content Size (bytes)')),
<<<<<<< HEAD
('is_to_professor', models.BooleanField(default=False, verbose_name='Is to Professor')),
=======
('is_read', models.BooleanField(default=False, verbose_name='Is Read')),
>>>>>>> develop
('sent_at', models.DateTimeField(auto_now_add=True, verbose_name='Sent At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')),
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='Deleted At')),
('is_deleted', models.BooleanField(default=False, verbose_name='Is deleted')),
<<<<<<< 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')),
('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')},
},
),
>>>>>>> develop
]
]

4
apps/course/admin/__init__.py

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

86
apps/course/admin/course.py

@ -1,38 +1,7 @@
<<<<<<< HEAD
=======
>>>>>>> develop
import os
import hashlib
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 import forms
from django.utils.translation import gettext_lazy as _
@ -134,49 +103,10 @@ class CourseForm(forms.ModelForm):
'schema': get_course_feature_schema(),
'title': _('Course Features'),
}),
>>>>>>> develop
}
help_texts = {
'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):
super().__init__(*args, **kwargs)
@ -543,7 +473,6 @@ class CourseGlossaryAdmin(CourseRelatedAdmin):
@admin.display(description=_("Description"))
def glossary_description(self, obj):
return obj.glossary.description
>>>>>>> develop
class AttachmentAdminForm(forms.ModelForm):
@ -576,32 +505,18 @@ class AttachmentAdminForm(forms.ModelForm):
return f"{base_part}{hash_part}{ext}" # ترکیب بخش اصلی و هش با پسوند
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):
form = AttachmentAdminForm
list_display = ('title', 'file', 'file_size')
search_fields = ('title', 'file')
>>>>>>> develop
def save_model(self, request, obj, form, change):
if obj.file:
obj.file_size = obj.file.size
super().save_model(request, obj, form, change)
<<<<<<< HEAD
=======
def is_used_in_professor_courses(self, user, obj):
"""آیا این attachment در دوره‌های استاد استفاده شده؟"""
return obj.courseattachment_set.filter(course__professor=user).exists()
@ -679,4 +594,3 @@ class HiddenCourseAdmin(ModelAdmin):
return False
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
from django.contrib import admin
from django import forms
@ -149,18 +123,12 @@ class LessonCompletionAdmin(ModelAdmin):
list_display = ('student', 'course_lesson', 'completed_at')
search_fields = ('student__fullname', 'student__email', 'course_lesson__title', 'course_lesson__course__title')
list_filter = ('course_lesson__course', 'completed_at')
>>>>>>> develop
ordering = ('-completed_at',)
def get_readonly_fields(self, request, obj=None):
"""
Make fields readonly if the object already exists.
"""
<<<<<<< HEAD
if obj:
return ['student', 'lesson', 'completed_at']
return []
=======
if obj:
return ['student', 'course_lesson', 'completed_at']
return []
@ -174,4 +142,3 @@ django_admin.site.register(Lesson, LessonAdmin)
project_admin_site.register(Lesson, LessonAdmin)
project_admin_site.register(CourseLesson, CourseLessonAdmin)
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
import apps.course.models.course
@ -18,7 +7,6 @@ import filer.fields.image
import utils.schema
from django.conf import settings
from django.db import migrations, models
>>>>>>> develop
class Migration(migrations.Migration):
@ -26,18 +14,12 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
<<<<<<< HEAD
('account', '0003_auto_20241120_1741'),
=======
('account', '0001_initial'),
>>>>>>> develop
migrations.swappable_dependency(settings.FILER_IMAGE_MODEL),
]
operations = [
migrations.CreateModel(
<<<<<<< HEAD
=======
name='CourseCategory',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
@ -46,7 +28,6 @@ class Migration(migrations.Migration):
],
),
migrations.CreateModel(
>>>>>>> develop
name='Course',
fields=[
('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_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')),
<<<<<<< HEAD
=======
('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')),
('duration', models.PositiveIntegerField(verbose_name='Duration (in hours)')),
('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')),
('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')),
<<<<<<< HEAD
=======
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created 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')),
('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')),
>>>>>>> develop
],
options={
'verbose_name': 'Course',
@ -87,59 +62,6 @@ class Migration(migrations.Migration):
},
),
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',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
@ -154,8 +76,6 @@ class Migration(migrations.Migration):
'ordering': ('-id',),
},
),
<<<<<<< HEAD
=======
migrations.CreateModel(
name='Glossary',
fields=[
@ -212,5 +132,4 @@ class Migration(migrations.Migration):
'unique_together': {('student', 'course')},
},
),
>>>>>>> develop
]
]

4
apps/course/models/__init__.py

@ -1,8 +1,4 @@
from .course import *
from .lesson import *
<<<<<<< HEAD
from .participant import *
=======
from .participant 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.models import TextChoices
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 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}")
<<<<<<< HEAD
def attachment_file_upload_to(instance, filename):
=======
def attachment_file_upload_to(instance, filename):
return os.path.join(f"attachments/{filename}")
def course_attachment_file_upload_to(instance, filename):
>>>>>>> develop
return os.path.join(f"courses/{instance.course.slug}/attachments/{filename}")
@ -43,22 +33,14 @@ class CourseCategory(models.Model):
return self.name
def save(self, *args, **kwargs):
<<<<<<< HEAD
self.slug = generate_slug_for_model(CourseCategory, self.name)
=======
if not self.slug:
self.slug = generate_slug_for_model(CourseCategory, self.name)
>>>>>>> develop
super().save(*args, **kwargs)
@property
def course_count(self):
return self.courses.exclude(status="inactive").count()
<<<<<<< HEAD
=======
>>>>>>> develop
class Course(models.Model):
class LevelChoices(TextChoices):
@ -74,13 +56,8 @@ class Course(models.Model):
FINISHED = 'finished', 'Finished' # Finished (course has ended)-закончился
class VedioTypeChoices(models.TextChoices):
<<<<<<< HEAD
VIDEO_FILE = 'video_file', 'Video File'
VIDEO_LINK = 'video_link', 'Video Link'
=======
YOUTUBE_LINK = 'youtube_link', 'Youtube Link'
VIDEO_FILE = 'video_file', 'Video File'
>>>>>>> develop
title = models.CharField(max_length=255, verbose_name='Course Title')
@ -92,34 +69,20 @@ class Course(models.Model):
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'))
video_type = models.CharField(
max_length=20,
choices=VedioTypeChoices.choices,
verbose_name='Preview Video Type (YouTube Link or File Upload)'
)
>>>>>>> develop
video_file = models.FileField(
upload_to=course_file_upload_to,
null=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)
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')
level = models.CharField(max_length=10, choices=LevelChoices.choices, verbose_name='Course Level')
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.')
)
<<<<<<< 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"))
>>>>>>> develop
features = models.JSONField(verbose_name=_('Course features'), default=dict, blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created at"))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At"))
@ -157,11 +116,6 @@ class Course(models.Model):
def save(self, *args, **kwargs):
<<<<<<< HEAD
self.slug = generate_slug_for_model(Course, self.title)
if self.discount_percentage > 0:
=======
if not self.slug:
self.slug = generate_slug_for_model(Course, self.title)
@ -175,7 +129,6 @@ class Course(models.Model):
self.discount_percentage = 0
self.final_price = Decimal('0.00')
elif self.discount_percentage > 0:
>>>>>>> develop
discount_amount = (self.price * self.discount_percentage) / 100
final_price = self.price - discount_amount
self.final_price = Decimal(math.ceil(final_price)).quantize(Decimal('0.00'))
@ -188,29 +141,6 @@ class Course(models.Model):
class Meta:
verbose_name = "Course"
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 = [
models.Index(fields=['status']),
models.Index(fields=['is_free']),
@ -269,37 +199,20 @@ class Attachment(models.Model):
"""
Base Attachment model that contains the actual file
"""
>>>>>>> develop
title = models.CharField(max_length=255, verbose_name='Attachment Title')
file = models.FileField(
upload_to=attachment_file_upload_to,
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)
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created at"))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At"))
>>>>>>> develop
def save(self, *args, **kwargs):
# Calculate the file size before saving
if self.file and not self.file_size:
self.file_size = self.file.size
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):
return self.title
@ -341,4 +254,3 @@ class CourseAttachment(models.Model):
models.Index(fields=['course']),
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):
<<<<<<< HEAD
return os.path.join(f"courses/{instance.course.slug}/lessons/{filename}")
=======
return os.path.join(f"lessons/{filename}")
>>>>>>> develop
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)
"""
@ -39,7 +22,6 @@ class Lesson(models.Model):
VIDEO_FILE = 'video_file', 'Video File'
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_file = models.FileField(
null=True,
@ -47,15 +29,6 @@ class Lesson(models.Model):
upload_to=lesson_file_upload_to,
)
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)')
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created at"))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At"))
@ -87,16 +60,10 @@ class CourseLesson(models.Model):
def __str__(self):
title = self.title or self.lesson.title
return f"{self.course.title} - {title}"
>>>>>>> develop
def is_completed_by(self, student):
return self.completions.filter(student=student).exists()
<<<<<<< HEAD
def save(self, *args, **kwargs):
print(f'---> start')
=======
@property
def content_type(self):
return self.lesson.content_type
@ -118,53 +85,19 @@ class CourseLesson(models.Model):
if not self.title:
self.title = self.lesson.title
>>>>>>> develop
if self.priority is None:
# 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']
self.priority = (max_priority or 0) + 1
<<<<<<< HEAD
else:
self._adjust_priorities()
super().save(*args, **kwargs)
=======
else:
self._adjust_priorities()
super().save(*args, **kwargs)
>>>>>>> develop
def _adjust_priorities(self):
# Adjust priorities of other lessons in the course
lessons = self.course.lessons.exclude(pk=self.pk)
# Shift priorities for lessons with the same or higher priority
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:
verbose_name = "Course Lesson"
@ -178,7 +111,6 @@ class CourseLesson(models.Model):
models.Index(fields=['course', 'is_active']),
]
>>>>>>> develop
class LessonCompletion(models.Model):
student = models.ForeignKey(
@ -186,13 +118,8 @@ class LessonCompletion(models.Model):
on_delete=models.CASCADE,
related_name='lesson_completions'
)
<<<<<<< HEAD
lesson = models.ForeignKey(
Lesson,
=======
course_lesson = models.ForeignKey(
CourseLesson,
>>>>>>> develop
on_delete=models.CASCADE,
related_name='completions'
)
@ -200,12 +127,6 @@ class LessonCompletion(models.Model):
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created at"))
class Meta:
<<<<<<< HEAD
unique_together = ('student', 'lesson')
def __str__(self):
return f"{self.student.fullname} - {self.lesson.title} - Completed"
=======
unique_together = ('student', 'course_lesson')
indexes = [
models.Index(fields=['student']),
@ -216,6 +137,5 @@ class LessonCompletion(models.Model):
def __str__(self):
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,
related_name='participants'
)
<<<<<<< HEAD
=======
is_active = models.BooleanField(default=True)
>>>>>>> develop
joined_date = models.DateTimeField(auto_now_add=True)
unread_messages_count = models.IntegerField(default=0)
class Meta:
<<<<<<< HEAD
unique_together = ('student', 'course')
=======
unique_together = ('student', 'course')
indexes = [
models.Index(fields=['student']),
@ -35,4 +29,3 @@ class Participant(models.Model):
models.Index(fields=['joined_date']),
models.Index(fields=['student', 'course']),
]
>>>>>>> develop

4
apps/course/serializers/__init__.py

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

111
apps/course/serializers/course.py

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

41
apps/course/serializers/lesson.py

@ -1,21 +1,4 @@
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.quiz.serializers import QuizListSerializer
@ -38,7 +21,6 @@ class CourseLessonSerializer(serializers.ModelSerializer):
class Meta:
model = CourseLesson
fields = ['id', 'title', 'priority', 'is_active', 'permission', 'duration', 'content_type', 'content_file', 'video_link', 'is_complated', 'quizs']
>>>>>>> develop
def get_permission(self, obj):
if student := self._get_authenticated_user():
@ -59,10 +41,6 @@ class CourseLessonSerializer(serializers.ModelSerializer):
def get_is_complated(self, obj):
request = self.context.get('request')
if not request or not request.user.is_authenticated:
<<<<<<< HEAD
return False
user = request.user
=======
return False
user = request.user
@ -71,28 +49,10 @@ class CourseLessonSerializer(serializers.ModelSerializer):
return any(completion.student_id == user.id for completion in obj.completions.all())
# Fallback to direct queries
>>>>>>> develop
is_participant = Participant.objects.filter(
student=user,
course=obj.course
).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:
return False
@ -109,4 +69,3 @@ class CourseLessonSerializer(serializers.ModelSerializer):
if quizzes:
return QuizListSerializer(quizzes, many=True, context=self.context).data
return None
>>>>>>> develop

4
apps/course/views/__init__.py

@ -1,10 +1,6 @@
from .course import *
from .lesson import *
<<<<<<< HEAD
from .participant import *
=======
from .participant import *
from .professor import *
from .live_session 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
import logging
@ -26,17 +17,10 @@ from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response
logger = logging.getLogger(__name__)
>>>>>>> develop
from apps.course.serializers import (
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,
OnlineClassTokenCreateSerializer, OnlineClassTokenVerifySerializer
)
@ -57,7 +41,6 @@ from utils.redis import OnlineClassTokenManager
UserModel = get_user_model()
>>>>>>> develop
class CourseCategoryAPIView(ListAPIView):
@ -66,10 +49,7 @@ class CourseCategoryAPIView(ListAPIView):
@swagger_auto_schema(
operation_description=doc_course_category(),
<<<<<<< HEAD
=======
tags=["Imam-Javad - Course"]
>>>>>>> develop
)
def get(self, 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
class CourseListAPIView(ListAPIView):
@ -115,16 +79,11 @@ class CourseListAPIView(ListAPIView):
description="Category of the Course",
type=openapi.TYPE_STRING,
# enum=[category.slug for category in CourseCategory.objects.all()]
>>>>>>> develop
),
openapi.Parameter(
'status', openapi.IN_QUERY,
type=openapi.TYPE_STRING,
<<<<<<< HEAD
description="""Status =>
=======
description="""Status =>
>>>>>>> develop
Upcoming (visible but registration not allowed)---Предстоящие
Registering (registration is open)---регистрация
Ongoing (course has started, registration closed)---Впроцессе
@ -144,15 +103,6 @@ class CourseListAPIView(ListAPIView):
),
])
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)
def get_queryset(self):
@ -167,7 +117,6 @@ class CourseListAPIView(ListAPIView):
request = self.request
filters = request.query_params
>>>>>>> develop
# Handle category_slug with multiple values separated by commas
if category_slugs := filters.get('category_slug'):
category_slugs_list = category_slugs.split(',')
@ -177,11 +126,7 @@ class CourseListAPIView(ListAPIView):
if statuses := filters.get('status'):
statuses_list = statuses.split(',')
queryset = queryset.filter(status__in=statuses_list)
<<<<<<< HEAD
=======
>>>>>>> develop
if is_free := filters.get('is_free'):
is_free = is_free.lower() == 'true'
queryset = queryset.filter(
@ -190,11 +135,7 @@ class CourseListAPIView(ListAPIView):
if is_online := filters.get('is_online'):
is_online = is_online.lower() == 'true'
queryset = queryset.filter(is_online=is_online)
<<<<<<< HEAD
=======
>>>>>>> develop
return queryset
@ -202,17 +143,10 @@ class CourseListAPIView(ListAPIView):
class CourseDetailAPIView(RetrieveAPIView):
<<<<<<< HEAD
queryset = Course.objects.all()
=======
>>>>>>> develop
serializer_class = CourseDetailSerializer
lookup_field = "slug"
@swagger_auto_schema(
<<<<<<< HEAD
operation_description=doc_course_detail(),
=======
tags=["Imam-Javad - Course"],
operation_description="Get detailed information about a specific course",
responses={
@ -244,7 +178,6 @@ class CourseDetailAPIView(RetrieveAPIView):
@swagger_auto_schema(
operation_description=doc_course_detail(),
tags=['Imam-Javad - Course'],
>>>>>>> develop
)
def get(self, request, *args, **kwargs):
return super().get(request, *args, **kwargs)
@ -267,10 +200,7 @@ class MyCourseListAPIView(ListAPIView):
],
operation_description=doc_courses_my_courses(),
operation_summary="Home",
<<<<<<< HEAD
=======
tags=['Imam-Javad - Course']
>>>>>>> develop
)
def get(self, request, *args, **kwargs):
@ -278,9 +208,6 @@ class MyCourseListAPIView(ListAPIView):
return super().get(request, *args, **kwargs)
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
"""
@ -293,7 +220,6 @@ class MyCourseListAPIView(ListAPIView):
'participants__student'
).exclude(status=Course.StatusChoices.INACTIVE)
>>>>>>> develop
request = self.request
filters = request.query_params
student = self.request.user
@ -334,16 +260,10 @@ class MyCourseListAPIView(ListAPIView):
class AttachmentListAPIView(ListAPIView):
<<<<<<< HEAD
serializer_class = AttachmentSerializer
@swagger_auto_schema(
=======
serializer_class = CourseAttachmentSerializer
@swagger_auto_schema(
tags=['Imam-Javad - Course'],
>>>>>>> develop
manual_parameters=[
openapi.Parameter(
'slug', openapi.IN_PATH,
@ -358,37 +278,23 @@ class AttachmentListAPIView(ListAPIView):
return super().get(request, *args, **kwargs)
def get_queryset(self):
<<<<<<< HEAD
=======
"""
Optimized queryset with select_related for attachment relationship
"""
>>>>>>> develop
course_slug = self.kwargs.get('slug')
try:
course = Course.objects.get(slug=course_slug)
except Course.DoesNotExist:
raise NotFound("Course not found")
<<<<<<< HEAD
return Attachment.objects.filter(course=course)
=======
return CourseAttachment.objects.select_related(
'course',
'attachment'
).filter(course=course)
>>>>>>> develop
class GlossaryListAPIView(ListAPIView):
<<<<<<< HEAD
serializer_class = GlossarySerializer
filter_backends = [SearchFilter]
search_fields = ['title', 'description']
def get_queryset(self):
=======
serializer_class = CourseGlossarySerializer
filter_backends = [SearchFilter]
search_fields = ['glossary__title', 'glossary__description']
@ -424,19 +330,12 @@ class GlossaryListAPIView(ListAPIView):
"""
Optimized queryset with select_related for glossary relationship
"""
>>>>>>> develop
course_slug = self.kwargs.get('slug')
try:
course = Course.objects.get(slug=course_slug)
except Course.DoesNotExist:
raise NotFound("Course not found")
<<<<<<< HEAD
return Glossary.objects.filter(course=course)
=======
return CourseGlossary.objects.select_related(
'course',
'glossary'
@ -772,5 +671,4 @@ class CourseOnlineClassTokenValidateAPIView(GenericAPIView):
)
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 apps.course.serializers import (
<<<<<<< HEAD
LessonSerializer
)
from apps.course.models import Course, Lesson, LessonCompletion
=======
CourseLessonSerializer
)
from apps.course.models import Course, CourseLesson, LessonCompletion
>>>>>>> develop
from apps.course.doc import *
from utils.exceptions import AppAPIException
from rest_framework.permissions import IsAuthenticated
@ -23,33 +17,16 @@ from rest_framework.permissions import IsAuthenticated
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
@swagger_auto_schema(
operation_description=doc_courses_lesson(),
tags=['Imam-Javad - Course'],
>>>>>>> develop
)
def get(self, request, *args, **kwargs):
return super().get(request, *args, **kwargs)
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
"""
@ -66,31 +43,11 @@ class LessonListView(ListAPIView):
course=course,
is_active=True
).order_by('priority', 'id')
>>>>>>> develop
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
@swagger_auto_schema(
@ -138,120 +95,42 @@ class LessonDetailView(RetrieveAPIView):
previous_lesson_id = previous_lesson.id if previous_lesson else None
lesson_data = self.get_serializer(course_lesson).data
>>>>>>> develop
lesson_data['total_lessons'] = total_lessons
lesson_data['current_lesson_number'] = current_lesson_number
lesson_data['next_lesson_id'] = next_lesson_id
lesson_data['previous_lesson_id'] = previous_lesson_id
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)
<<<<<<< HEAD
class LessonCompletionCreateAPIView(GenericAPIView):
permission_classes = [IsAuthenticated]
@swagger_auto_schema(
=======
class LessonCompletionToggleAPIView(GenericAPIView):
permission_classes = [IsAuthenticated]
@swagger_auto_schema(
operation_description="Toggle lesson completion status (Check/Uncheck)",
tags=["Imam-Javad - Course"],
>>>>>>> develop
request_body=openapi.Schema(
type=openapi.TYPE_OBJECT,
required=['lesson_id'],
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'),
},
),
responses={
201: 'Lesson marked as COMPLETED.',
200: 'Lesson marked as INCOMPLETE (Unchecked).',
>>>>>>> develop
400: 'Lesson ID is required.',
404: 'Lesson not found.',
}
)
def post(self, request):
<<<<<<< HEAD
student = request.user # Assuming the user is the student
=======
student = request.user
>>>>>>> develop
lesson_id = request.data.get('lesson_id')
if not lesson_id:
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:
course_lesson = CourseLesson.objects.get(id=lesson_id)
@ -278,4 +157,3 @@ class LessonCompletionToggleAPIView(GenericAPIView):
{'message': 'Lesson completed successfully.', 'is_completed': True},
status=status.HTTP_201_CREATED
)
>>>>>>> develop

9
apps/course/views/participant.py

@ -19,10 +19,6 @@ class CourseParticipantsView(generics.ListAPIView):
@swagger_auto_schema(
operation_description=doc_course_participants(),
<<<<<<< HEAD
)
def get_queryset(self):
=======
tags=['Imam-Javad - Course'],
)
def get(self, request, *args, **kwargs):
@ -31,20 +27,15 @@ class CourseParticipantsView(generics.ListAPIView):
"""
Optimized queryset with select_related for course relationship
"""
>>>>>>> develop
course_slug = self.kwargs.get('slug')
try:
course = Course.objects.get(slug=course_slug)
except Course.DoesNotExist:
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(
participated_courses__course=course
)
>>>>>>> develop

4
apps/hadis/admin/__init__.py

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

219
apps/hadis/admin/category.py

@ -1,222 +1,4 @@
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.utils.translation import gettext_lazy as _
from django.utils.html import format_html
@ -437,4 +219,3 @@ class HadisCategoryAdmin(DraggableMPTTAdmin, ModelAdmin):
# Register models with the custom admin site
dovoodi_admin_site.register(HadisSect, HadisSectAdmin)
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.contrib import admin
from django.utils.translation import gettext_lazy as _
@ -235,45 +119,10 @@ class HadisTagAdmin(ModelAdmin):
(_('Timestamps'), {
'fields': ('created_at', 'updated_at'),
'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):
"""Admin for HadisStatus model"""
list_display = ('title', 'color', 'order')
@ -316,20 +165,10 @@ class HadisAdmin(ModelAdmin):
(_('Timestamps'), {
'fields': ('created_at', 'updated_at'),
'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):
"""Admin for HadisReference model"""
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(HadisInCollection, HadisInCollectionAdmin)
dovoodi_admin_site.register(HadisCorrection, HadisCorrectionAdmin)
>>>>>>> develop

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

@ -2,127 +2,3 @@
## 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
import django.db.models.deletion
@ -12,7 +5,6 @@ import filer.fields.image
import mptt.fields
from django.conf import settings
from django.db import migrations, models
>>>>>>> develop
class Migration(migrations.Migration):
@ -20,67 +12,16 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
<<<<<<< HEAD
=======
('library', '0001_initial'),
migrations.swappable_dependency(settings.FILER_IMAGE_MODEL),
>>>>>>> develop
]
operations = [
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',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('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')),
],
),
@ -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')),
('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')),
>>>>>>> develop
('order', models.IntegerField(default=0, verbose_name='order')),
('lft', models.PositiveIntegerField(editable=False)),
('rght', models.PositiveIntegerField(editable=False)),
@ -107,17 +47,6 @@ class Migration(migrations.Migration):
'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(
name='Hadis',
fields=[
@ -208,6 +137,5 @@ class Migration(migrations.Migration):
'ordering': ('hadis', 'order'),
'unique_together': {('hadis', 'transmitter', 'order')},
},
>>>>>>> develop
),
]
]

4
apps/hadis/models/__init__.py

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

102
apps/hadis/models/category.py

@ -1,30 +1,6 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
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 django.utils.text import slugify
@ -101,17 +77,10 @@ class HadisCategory(MPTTModel):
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)
slug = models.SlugField(max_length=255, null=True, blank=True)
>>>>>>> develop
content_type = None
language = 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):
super().clean()
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=['sect', 'order'])
]
>>>>>>> develop
verbose_name = _('Hadis Category')
verbose_name_plural = _('Hadis Categories')
ordering = ('order',)
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']}"
def get_title(self,lang):
@ -251,4 +150,3 @@ class HadisCategory(MPTTModel):
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 typing import Optional
from django.db import models
@ -184,22 +168,11 @@ class HadisStatus(models.Model):
verbose_name = _('hadis status')
verbose_name_plural = _('hadis statuses')
ordering = ('order',)
>>>>>>> develop
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'))
number = models.PositiveIntegerField(verbose_name=_('number'), default=1)
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)
explanation = models.JSONField(default = list , verbose_name=_('Explanation'))
>>>>>>> develop
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):
<<<<<<< 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}"
def save(self, *args, **kwargs):
@ -363,7 +309,6 @@ class HadisOverview(models.Model):
ordering = ('category', 'number')
>>>>>>> develop
class HadisReference(models.Model):
hadis = models.ForeignKey(
@ -372,26 +317,6 @@ class HadisReference(models.Model):
verbose_name=_('hadis'),
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(
BookReference,
on_delete=models.SET_NULL,
@ -436,7 +361,6 @@ class ReferenceImage(models.Model):
class ReferenceImage(models.Model):
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'))
>>>>>>> develop
priority = models.IntegerField(
default=0,
verbose_name=_("Priority"),
@ -445,22 +369,15 @@ class ReferenceImage(models.Model):
class Meta:
<<<<<<< HEAD
=======
indexes = [
# Speeds up fetching images for a reference in priority order
models.Index(fields=['reference', 'priority']),
]
>>>>>>> develop
verbose_name = _('Reference Image')
verbose_name_plural = _('Reference Images')
def __str__(self):
<<<<<<< HEAD
return f'{self.reference.title}-{self.id}'
=======
return f'{self.reference.title[0]["text"]}-{self.id}'
>>>>>>> develop
def save(self, *args, **kwargs):
if ReferenceImage.objects.filter(reference=self.reference, priority=self.priority).exists():
@ -470,9 +387,6 @@ class ReferenceImage(models.Model):
).update(priority=F('priority') + 1)
super().save(*args, **kwargs)
<<<<<<< HEAD
=======
class HadisCorrection(models.Model):
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':
return tr.get('text', '')
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 django.db import models
from django.utils.translation import gettext_lazy as _
@ -122,25 +116,10 @@ class TransmitterReliability(models.Model):
class Meta:
verbose_name = _('Transmitter Reliability')
verbose_name_plural = _('Transmitter Reliabilities')
>>>>>>> develop
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):
# VERY_RELIABLE = 'very_reliable', _('Very Reliable')
# RELIABLE = 'reliable', _('Reliable')
@ -291,7 +270,6 @@ class Transmitters(models.Model):
name = self.full_name[0]
return name.get('text')
>>>>>>> develop
class HadisTransmitter(models.Model):
@ -307,9 +285,6 @@ class HadisTransmitter(models.Model):
verbose_name=_('transmitter'),
related_name='hadises'
)
<<<<<<< HEAD
description = models.TextField(verbose_name=_('description'), blank=True, null=True)
=======
narrator_layer = models.ForeignKey(
NarratorLayer,
on_delete=models.SET_NULL,
@ -328,17 +303,11 @@ class HadisTransmitter(models.Model):
blank=True,
help_text=_('Reliability status of the narrator')
)
>>>>>>> develop
order = models.PositiveIntegerField(
default=0,
verbose_name=_('Order'),
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'))
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
models.Index(fields=['hadis', 'order']),
]
>>>>>>> develop
verbose_name = _('Hadis Transmitter')
verbose_name_plural = _('Hadis Transmitters')
ordering = ('hadis', 'order')
unique_together = ('hadis', 'transmitter', 'order')
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 ""
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':
return tr.get('text', '')
return None
>>>>>>> develop

4
apps/hadis/views/__init__.py

@ -1,7 +1,3 @@
from .category import *
from .hadis import *
<<<<<<< HEAD
# from .transmitter 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 django.shortcuts import get_object_or_404
from utils.pagination import NoPagination
@ -487,4 +410,3 @@ class HadisFiltersView(ListAPIView):
}
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
import django.db.models.deletion
import filer.fields.image
from django.conf import settings
from django.db import migrations, models
>>>>>>> develop
class Migration(migrations.Migration):
@ -20,40 +11,11 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
<<<<<<< HEAD
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
=======
>>>>>>> develop
migrations.swappable_dependency(settings.FILER_IMAGE_MODEL),
]
operations = [
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',
fields=[
('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')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created 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={
'verbose_name': 'Category',
@ -73,18 +31,6 @@ class Migration(migrations.Migration):
},
),
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',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
@ -107,27 +53,18 @@ class Migration(migrations.Migration):
options={
'verbose_name': 'Book',
'verbose_name_plural': 'Books',
>>>>>>> develop
},
),
migrations.CreateModel(
name='BookCollection',
fields=[
('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)),
>>>>>>> develop
('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')),
('status', models.BooleanField(default=True, verbose_name='status')),
('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')),
>>>>>>> develop
],
options={
'verbose_name': 'Book Collection',
@ -136,23 +73,8 @@ class Migration(migrations.Migration):
),
migrations.AddField(
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',
field=models.ManyToManyField(blank=True, related_name='related_collections', to='library.bookcollection', verbose_name='collections'),
>>>>>>> develop
),
migrations.CreateModel(
name='BottomBookCollection',
@ -193,12 +115,9 @@ class Migration(migrations.Migration):
},
bases=('library.bookcollection',),
),
<<<<<<< HEAD
=======
migrations.AddField(
model_name='book',
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.db.models import F
from django.contrib.admin import SimpleListFilter
@ -39,7 +19,6 @@ class ParticipantAnswerInline(StackedInline):
@display(description="Correct Answer")
def correct_answer_display(self, obj):
>>>>>>> develop
return obj.correct_answer
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'))
<<<<<<< HEAD
=======
>>>>>>> develop
class UserEmailFilter(SimpleListFilter):
title = _('User Email')
parameter_name = 'user_email'
@ -72,22 +46,6 @@ class UserEmailFilter(SimpleListFilter):
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):
inlines = [ParticipantAnswerInline]
search_fields = ['user__username', 'user__fullname']
@ -102,4 +60,3 @@ class ParticipantAdmin(ModelAdmin):
ordering = ['-started_at']
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.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
# @admin.register(Question)
# class QuestionAdmin(ModelAdmin):
>>>>>>> develop
# list_display = ('question', 'correct_answer', 'quiz', 'priority')
# form = QuestionAdminForm
# ordering = ("priority", "id",)
# fieldsets = (
# (
<<<<<<< HEAD
# '', {
=======
# None, {
>>>>>>> develop
# 'fields': (
# 'question',
# ('option1', 'option2'),
@ -54,27 +28,11 @@ from utils.admin import project_admin_site
# },
# ),
# (
<<<<<<< HEAD
# '', {
=======
# None, {
>>>>>>> develop
# '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)
class QuestionAdmin(ModelAdmin):
list_display = ('question', 'correct_answer', 'quiz', 'priority')
@ -109,7 +67,6 @@ class QuestionAdminInline(StackedInline):
fieldsets = (
(
None, {
>>>>>>> develop
'fields': (
'question',
('option1', 'option2'),
@ -119,17 +76,10 @@ class QuestionAdminInline(StackedInline):
},
),
(
<<<<<<< HEAD
'', {
=======
None, {
>>>>>>> develop
'fields': ('priority',)
}
)
)
<<<<<<< HEAD
=======
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.db.models import Count
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>')
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
from django.conf import settings
from django.db import migrations, models
>>>>>>> develop
class Migration(migrations.Migration):
@ -18,37 +8,13 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
<<<<<<< HEAD
('course', '0005_participant_unread_messages_count'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('account', '0004_user_skill'),
=======
('account', '0001_initial'),
('course', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
>>>>>>> develop
]
operations = [
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',
fields=[
],
@ -62,23 +28,6 @@ class Migration(migrations.Migration):
bases=('account.user',),
),
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',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
@ -91,7 +40,6 @@ class Migration(migrations.Migration):
options={
'verbose_name': 'Quiz',
'verbose_name_plural': 'Quizzes',
>>>>>>> develop
'ordering': ('-id',),
},
),
@ -116,8 +64,6 @@ class Migration(migrations.Migration):
},
),
migrations.CreateModel(
<<<<<<< HEAD
=======
name='QuizParticipant',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
@ -137,20 +83,14 @@ class Migration(migrations.Migration):
},
),
migrations.CreateModel(
>>>>>>> develop
name='ParticipantAnswer',
fields=[
('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')),
('at_time', models.DateTimeField()),
('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')),
('participant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='answers', to='quiz.quizparticipant')),
>>>>>>> develop
],
options={
'verbose_name': 'User Quiz Answer',
@ -158,4 +98,4 @@ class Migration(migrations.Migration):
'ordering': ('-id',),
},
),
]
]

3
apps/quiz/models/participant.py

@ -1,9 +1,6 @@
from django.db import models
<<<<<<< HEAD
=======
from django.db.models import F, Window
from django.db.models.functions import Rank
>>>>>>> develop
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):
<<<<<<< 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)
>>>>>>> develop
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")
each_question_timing = models.PositiveIntegerField()

13
apps/quiz/serializers/quiz.py

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

5
apps/quiz/views/participant.py

@ -17,10 +17,7 @@ class QuizParticipantCreateAPIView(CreateAPIView):
@swagger_auto_schema(
operation_description=doc_quiz_submit(),
<<<<<<< HEAD
=======
tags=["Imam-Javad - Quiz"],
>>>>>>> develop
)
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(
operation_description=doc_quiz_detail(),
<<<<<<< HEAD
=======
tags=["Imam-Javad - Quiz"],
>>>>>>> develop
)
def get(self, request, *args, **kwargs):
return super().get(request, *args, **kwargs)
@ -31,7 +28,3 @@ class QuizDetailAPIView(RetrieveAPIView):
).annotate(
lesson__has_quiz=Value(True)
).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
import django.db.models.deletion
@ -14,7 +5,6 @@ import phonenumber_field.modelfields
import utils.validators
from django.conf import settings
from django.db import migrations, models
>>>>>>> develop
class Migration(migrations.Migration):
@ -22,13 +12,8 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
<<<<<<< HEAD
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('course', '0005_participant_unread_messages_count'),
=======
('course', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
>>>>>>> develop
]
operations = [
@ -49,13 +34,8 @@ class Migration(migrations.Migration):
fields=[
('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')),
<<<<<<< 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')),
('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')),
('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')),

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
import django.db.models.deletion
import filer.fields.image
from django.conf import settings
from django.db import migrations, models
>>>>>>> develop
class Migration(migrations.Migration):
@ -25,30 +16,6 @@ class Migration(migrations.Migration):
operations = [
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',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
@ -80,8 +47,6 @@ class Migration(migrations.Migration):
},
),
migrations.CreateModel(
<<<<<<< HEAD
=======
name='Video',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
@ -105,7 +70,6 @@ class Migration(migrations.Migration):
},
),
migrations.CreateModel(
>>>>>>> develop
name='VideoInCollection',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
@ -122,20 +86,6 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='videocollection',
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'),
>>>>>>> develop
),
]
]

22
dynamic_preferences/admin.py

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

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

@ -8,11 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\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"
>>>>>>> develop
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -22,59 +18,6 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\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
#, fuzzy
#| msgid "Verbose Name"
@ -145,4 +88,3 @@ msgstr ""
#~ msgid "Dynamic Preferences"
#~ msgstr "تنظیمات"
>>>>>>> develop

54
utils/__init__.py

@ -5,14 +5,11 @@ import mimetypes
import re
from urllib.parse import urlparse
<<<<<<< HEAD
=======
from django.core.files.storage import default_storage
from django.core.files.base import ContentFile
from pathlib import Path
from django.utils.text import get_valid_filename
>>>>>>> develop
from django.conf import settings
from django.core.files import File
from django.http import HttpRequest
@ -27,9 +24,6 @@ from django.utils.text import slugify
import random
import string
<<<<<<< HEAD
=======
from django.conf import settings
from django.utils.translation import gettext_lazy as _
@ -84,7 +78,6 @@ def environment_callback(request):
return [_("Development"), "primary"]
return [_("Production"), "primary"]
>>>>>>> develop
@ -170,8 +163,6 @@ def generate_slug_for_model(model, value: str, recycled_count: int = 0):
return slug[:50]
<<<<<<< HEAD
=======
def generate_language_slugs(translations):
"""
@ -196,7 +187,6 @@ def generate_language_slugs(translations):
print(f"Error generating slugs: {e}")
return []
>>>>>>> develop
def absolute_url(req, url):
"""
can either be a file instance or a URL string
@ -213,8 +203,6 @@ def sizeof_fmt(num, suffix="B"):
num /= 1024.0
return f"{num:.1f} Yi{suffix}"
<<<<<<< HEAD
=======
def file_location_media(path: str):
"""
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)
>>>>>>> develop
def file_location(path):
from django.conf import settings
@ -291,16 +278,6 @@ class FileFieldSerializer(serializers.CharField):
# value not changed and here we simply return old file path
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'):
# data = self.get_rpath(data)
@ -463,7 +440,6 @@ class UploadChatMediaSerializer(serializers.Serializer):
def validate(self, attrs):
file_details = self.store_file(attrs['file'])
return file_details
>>>>>>> develop
class UploadTmpSerializer(serializers.Serializer):
@ -472,10 +448,7 @@ class UploadTmpSerializer(serializers.Serializer):
name = serializers.CharField(read_only=True)
size = serializers.CharField(read_only=True)
mime_type = serializers.CharField(read_only=True)
<<<<<<< HEAD
=======
thumbnail_url = serializers.URLField(read_only=True, required=False)
>>>>>>> develop
def to_representation(self, instance):
data = super(UploadTmpSerializer, self).to_representation(instance)
@ -484,23 +457,6 @@ class UploadTmpSerializer(serializers.Serializer):
def store_file(self, file):
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 (
create_thumbnail,
is_image_file,
@ -569,17 +525,11 @@ class UploadTmpSerializer(serializers.Serializer):
return result
>>>>>>> develop
def validate(self, attrs):
file_details = self.store_file(attrs['file'])
return file_details
<<<<<<< HEAD
class UploadTmpMedia(GenericAPIView):
"""
Files will remove every 1 hour
=======
class UploadChatMedia(GenericAPIView):
"""
Upload files permanently to /media/chat/
@ -601,7 +551,6 @@ class UploadTmpMedia(GenericAPIView):
"""
Upload files temporarily to /static/tmp/
Files will be removed every 1 hour
>>>>>>> develop
"""
parser_classes = (FormParser, MultiPartParser)
serializer_class = UploadTmpSerializer
@ -613,8 +562,6 @@ class UploadTmpMedia(GenericAPIView):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
return Response(serializer.data)
<<<<<<< HEAD
=======
# Configure filer admin after Django is fully loaded
def configure_filer_admin():
@ -628,4 +575,3 @@ def configure_filer_admin():
pass
# 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
<<<<<<< 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 django import forms
@ -71,7 +62,6 @@ class JsonEditorWidget(Widget):
attrs['title'] = name.replace('_', ' ').title()
return super().render(name, value, attrs, renderer)
>>>>>>> develop
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 hashlib
import random
@ -17,7 +10,6 @@ from redis.exceptions import RedisError
from django.conf import settings
>>>>>>> develop
from config.redis_config import RedisConfig
from utils.exceptions import ServiceUnavailableException, NotFoundException
@ -82,9 +74,6 @@ class RedisManager(RedisConfig):
@staticmethod
def generate_otp_code() -> int:
random_code = random.randint(10000, 99999)
<<<<<<< HEAD
return random_code
=======
return random_code
@ -130,4 +119,3 @@ class OnlineClassTokenManager(RedisConfig):
query_params["token"] = token
new_query = urlencode(query_params)
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():
return {
'type': "array",
@ -53,8 +49,6 @@ def get_course_feature_schema():
}
}
}
<<<<<<< HEAD
=======
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
def validate_type_code(value):
<<<<<<< HEAD
=======
from rest_framework import serializers
>>>>>>> develop
if not value.isdigit():
raise serializers.ValidationError('کد باید شامل اعداد باشد.')
if len(value) != 5:

Loading…
Cancel
Save