diff --git a/apps/account/admin/__init__.py b/apps/account/admin/__init__.py index fc61670..ac7abff 100644 --- a/apps/account/admin/__init__.py +++ b/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 diff --git a/apps/account/admin/professor.py b/apps/account/admin/professor.py index 40e515f..82fecc0 100644 --- a/apps/account/admin/professor.py +++ b/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): diff --git a/apps/account/admin/student.py b/apps/account/admin/student.py index a56b91c..a7cc389 100644 --- a/apps/account/admin/student.py +++ b/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) diff --git a/apps/account/admin/user.py b/apps/account/admin/user.py index d81c5cd..49f7f6a 100644 --- a/apps/account/admin/user.py +++ b/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 diff --git a/apps/account/serializers/__init__.py b/apps/account/serializers/__init__.py index 5d28da6..f6a8ca6 100644 --- a/apps/account/serializers/__init__.py +++ b/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 * diff --git a/apps/account/serializers/user.py b/apps/account/serializers/user.py index 738e393..1c0a605 100644 --- a/apps/account/serializers/user.py +++ b/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 diff --git a/apps/account/urls.py b/apps/account/urls.py index 4517911..5e450c1 100644 --- a/apps/account/urls.py +++ b/apps/account/urls.py @@ -1,4 +1,3 @@ - from django.urls import path, include from rest_framework.routers import DefaultRouter diff --git a/apps/account/views/__init__.py b/apps/account/views/__init__.py index d25e678..4266590 100644 --- a/apps/account/views/__init__.py +++ b/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* diff --git a/apps/account/views/notification.py b/apps/account/views/notification.py index ba8bac9..fa20834 100644 --- a/apps/account/views/notification.py +++ b/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 diff --git a/apps/account/views/user.py b/apps/account/views/user.py index 515d56d..5f7bf05 100644 --- a/apps/account/views/user.py +++ b/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) \ No newline at end of file diff --git a/apps/certificate/migrations/0001_initial.py b/apps/certificate/migrations/0001_initial.py index dac1abc..cc3af2b 100644 --- a/apps/certificate/migrations/0001_initial.py +++ b/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')), diff --git a/apps/chat/migrations/0001_initial.py b/apps/chat/migrations/0001_initial.py index ee278cc..6460652 100644 --- a/apps/chat/migrations/0001_initial.py +++ b/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 - ] + ] \ No newline at end of file diff --git a/apps/course/admin/__init__.py b/apps/course/admin/__init__.py index b42b347..bff68d7 100644 --- a/apps/course/admin/__init__.py +++ b/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 diff --git a/apps/course/admin/course.py b/apps/course/admin/course.py index dbcb794..8fbcf21 100644 --- a/apps/course/admin/course.py +++ b/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 diff --git a/apps/course/admin/lesson.py b/apps/course/admin/lesson.py index 2094009..a244412 100644 --- a/apps/course/admin/lesson.py +++ b/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 diff --git a/apps/course/migrations/0001_initial.py b/apps/course/migrations/0001_initial.py index 0098592..2b829ce 100644 --- a/apps/course/migrations/0001_initial.py +++ b/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 - ] + ] \ No newline at end of file diff --git a/apps/course/models/__init__.py b/apps/course/models/__init__.py index b42b347..bff68d7 100644 --- a/apps/course/models/__init__.py +++ b/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 diff --git a/apps/course/models/course.py b/apps/course/models/course.py index 06eb96a..1a47a8e 100644 --- a/apps/course/models/course.py +++ b/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 diff --git a/apps/course/models/lesson.py b/apps/course/models/lesson.py index 0f0ec96..c3f12cb 100644 --- a/apps/course/models/lesson.py +++ b/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 \ No newline at end of file diff --git a/apps/course/models/participant.py b/apps/course/models/participant.py index b6c7061..80947f3 100644 --- a/apps/course/models/participant.py +++ b/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 diff --git a/apps/course/serializers/__init__.py b/apps/course/serializers/__init__.py index affa72b..cb5e526 100644 --- a/apps/course/serializers/__init__.py +++ b/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 diff --git a/apps/course/serializers/course.py b/apps/course/serializers/course.py index ae0e1e8..76237ff 100644 --- a/apps/course/serializers/course.py +++ b/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'] \ No newline at end of file diff --git a/apps/course/serializers/lesson.py b/apps/course/serializers/lesson.py index 5e66e14..dd8c972 100644 --- a/apps/course/serializers/lesson.py +++ b/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 diff --git a/apps/course/views/__init__.py b/apps/course/views/__init__.py index 23ff190..70ef373 100644 --- a/apps/course/views/__init__.py +++ b/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 diff --git a/apps/course/views/course.py b/apps/course/views/course.py index 5a29484..4961c5e 100644 --- a/apps/course/views/course.py +++ b/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}") \ No newline at end of file diff --git a/apps/course/views/lesson.py b/apps/course/views/lesson.py index 39ea533..7261040 100644 --- a/apps/course/views/lesson.py +++ b/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 diff --git a/apps/course/views/participant.py b/apps/course/views/participant.py index 3188d82..b591f76 100644 --- a/apps/course/views/participant.py +++ b/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 diff --git a/apps/hadis/admin/__init__.py b/apps/hadis/admin/__init__.py index 7074a60..143131e 100644 --- a/apps/hadis/admin/__init__.py +++ b/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 diff --git a/apps/hadis/admin/category.py b/apps/hadis/admin/category.py index 656bc8d..461d300 100644 --- a/apps/hadis/admin/category.py +++ b/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 diff --git a/apps/hadis/admin/hadis.py b/apps/hadis/admin/hadis.py index 977e19d..4acc62f 100644 --- a/apps/hadis/admin/hadis.py +++ b/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 diff --git a/apps/hadis/management/commands/README.md b/apps/hadis/management/commands/README.md index d8f4a22..8e83cbb 100644 --- a/apps/hadis/management/commands/README.md +++ b/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) diff --git a/apps/hadis/migrations/0001_initial.py b/apps/hadis/migrations/0001_initial.py index 8c95bbe..6f16586 100644 --- a/apps/hadis/migrations/0001_initial.py +++ b/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 ), - ] + ] \ No newline at end of file diff --git a/apps/hadis/models/__init__.py b/apps/hadis/models/__init__.py index 7074a60..143131e 100644 --- a/apps/hadis/models/__init__.py +++ b/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 diff --git a/apps/hadis/models/category.py b/apps/hadis/models/category.py index 26b05da..3bbce65 100644 --- a/apps/hadis/models/category.py +++ b/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 diff --git a/apps/hadis/models/hadis.py b/apps/hadis/models/hadis.py index 36d692b..54f439a 100644 --- a/apps/hadis/models/hadis.py +++ b/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 diff --git a/apps/hadis/models/transmitter.py b/apps/hadis/models/transmitter.py index 6159ae6..6abb658 100644 --- a/apps/hadis/models/transmitter.py +++ b/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 diff --git a/apps/hadis/views/__init__.py b/apps/hadis/views/__init__.py index 4fe1bee..a05e2cd 100644 --- a/apps/hadis/views/__init__.py +++ b/apps/hadis/views/__init__.py @@ -1,7 +1,3 @@ from .category import * from .hadis import * -<<<<<<< HEAD -# from .transmitter import * -======= from .info import * ->>>>>>> develop diff --git a/apps/hadis/views/hadis.py b/apps/hadis/views/hadis.py index 04a6d3d..12bdf75 100644 --- a/apps/hadis/views/hadis.py +++ b/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 diff --git a/apps/library/migrations/0001_initial.py b/apps/library/migrations/0001_initial.py index a9264ca..151cd03 100644 --- a/apps/library/migrations/0001_initial.py +++ b/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 - ] + ] \ No newline at end of file diff --git a/apps/quiz/admin/participant.py b/apps/quiz/admin/participant.py index 9f9d2d1..b5fa008 100644 --- a/apps/quiz/admin/participant.py +++ b/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 diff --git a/apps/quiz/admin/question.py b/apps/quiz/admin/question.py index 8135229..2036866 100644 --- a/apps/quiz/admin/question.py +++ b/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 diff --git a/apps/quiz/admin/quiz.py b/apps/quiz/admin/quiz.py index a17bbe1..95d7cd5 100644 --- a/apps/quiz/admin/quiz.py +++ b/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("Active") - - return mark_safe("Inactive") - - @admin.display(description='Questions', ordering='questions_count') - def _questions(self, obj): - return mark_safe(f"Questions: {obj.questions_count}") - - 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'Questions: {obj.questions_count}') project_admin_site.register(Quiz, QuizAdmin) ->>>>>>> develop diff --git a/apps/quiz/migrations/0001_initial.py b/apps/quiz/migrations/0001_initial.py index 229ee40..baf658e 100644 --- a/apps/quiz/migrations/0001_initial.py +++ b/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',), }, ), - ] + ] \ No newline at end of file diff --git a/apps/quiz/models/participant.py b/apps/quiz/models/participant.py index c796d07..3400977 100644 --- a/apps/quiz/models/participant.py +++ b/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 diff --git a/apps/quiz/models/quiz.py b/apps/quiz/models/quiz.py index 21cf049..1be1190 100644 --- a/apps/quiz/models/quiz.py +++ b/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() diff --git a/apps/quiz/serializers/quiz.py b/apps/quiz/serializers/quiz.py index 5a3538c..6156e64 100644 --- a/apps/quiz/serializers/quiz.py +++ b/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 diff --git a/apps/quiz/views/participant.py b/apps/quiz/views/participant.py index 1de79d9..dbe6bc3 100644 --- a/apps/quiz/views/participant.py +++ b/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) \ No newline at end of file diff --git a/apps/quiz/views/quiz.py b/apps/quiz/views/quiz.py index 336314e..5dd8a00 100644 --- a/apps/quiz/views/quiz.py +++ b/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() - - - - diff --git a/apps/transaction/migrations/0001_initial.py b/apps/transaction/migrations/0001_initial.py index 899a98b..6fc1605 100644 --- a/apps/transaction/migrations/0001_initial.py +++ b/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')), diff --git a/apps/video/migrations/0001_initial.py b/apps/video/migrations/0001_initial.py index 95cd5a1..d63e4c5 100644 --- a/apps/video/migrations/0001_initial.py +++ b/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 ), - ] + ] \ No newline at end of file diff --git a/dynamic_preferences/admin.py b/dynamic_preferences/admin.py index 21dd326..369a0e8 100644 --- a/dynamic_preferences/admin.py +++ b/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 diff --git a/dynamic_preferences/locale/fa/LC_MESSAGES/django.po b/dynamic_preferences/locale/fa/LC_MESSAGES/django.po index 58e4376..dff9266 100644 --- a/dynamic_preferences/locale/fa/LC_MESSAGES/django.po +++ b/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 \n" "Language-Team: LANGUAGE \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 diff --git a/utils/__init__.py b/utils/__init__.py index dc91cb3..4ad546c 100644 --- a/utils/__init__.py +++ b/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 diff --git a/utils/json_editor_field.py b/utils/json_editor_field.py index 5242f90..b907505 100644 --- a/utils/json_editor_field.py +++ b/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): diff --git a/utils/redis.py b/utils/redis.py index d34a636..7295973 100644 --- a/utils/redis.py +++ b/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 diff --git a/utils/schema.py b/utils/schema.py index e4ba409..4183b56 100644 --- a/utils/schema.py +++ b/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 diff --git a/utils/validators.py b/utils/validators.py index ac831f8..62cf2e0 100644 --- a/utils/validators.py +++ b/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: