Browse Source

views of user page for admin panel added

master
Mohsen Taba 2 months ago
parent
commit
4662755256
  1. 33
      apps/account/serializers/user.py
  2. 7
      apps/account/urls.py
  3. 121
      apps/account/views/user.py
  4. 117
      apps/course/management/commands/redistribute_lessons_to_chapters.py
  5. 2
      entrypoint.sh

33
apps/account/serializers/user.py

@ -220,3 +220,36 @@ class UserFCMSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = User model = User
fields = ['fcm'] fields = ['fcm']
class AdminUserSerializer(serializers.ModelSerializer):
avatar = FileFieldSerializer(required=False)
password = serializers.CharField(write_only=True, required=False, validators=[validate_password])
email = serializers.EmailField(required=True)
class Meta:
model = User
fields = [
'id', 'fullname', 'email', 'phone_number', 'password', 'avatar',
'gender', 'user_type', 'is_active', 'is_staff', 'is_superuser',
'date_joined', 'last_login'
]
read_only_fields = ['id', 'date_joined', 'last_login']
def create(self, validated_data):
password = validated_data.pop('password', None)
user = User(**validated_data)
if password:
user.set_password(password)
else:
user.set_unusable_password()
user.save()
return user
def update(self, instance, validated_data):
password = validated_data.pop('password', None)
user = super().update(instance, validated_data)
if password:
user.set_password(password)
user.save()
return user

7
apps/account/urls.py

@ -7,6 +7,9 @@ from apps.account import views
from apps.geolocation_package.views.geolocation import IPGeolocationAPIView, ReverseGeolocationAPIView from apps.geolocation_package.views.geolocation import IPGeolocationAPIView, ReverseGeolocationAPIView
from apps.geolocation_package.views.region_info import RegionInfoView from apps.geolocation_package.views.region_info import RegionInfoView
admin_router = DefaultRouter()
admin_router.register(r'users', views.AdminUserViewSet, basename='admin-users')
urlpatterns = [ urlpatterns = [
path('location-info/', IPGeolocationAPIView.as_view(), name='location-info'), path('location-info/', IPGeolocationAPIView.as_view(), name='location-info'),
path('location-info/reverse/', ReverseGeolocationAPIView.as_view(), name='reverse-location-info'), path('location-info/reverse/', ReverseGeolocationAPIView.as_view(), name='reverse-location-info'),
@ -44,4 +47,8 @@ urlpatterns = [
path('profile/delete/', views.UserDeleteView.as_view(), name='user-delete'), path('profile/delete/', views.UserDeleteView.as_view(), name='user-delete'),
path('update-fcm/', views.UpdateFCMView.as_view(), name='update-fcm'), path('update-fcm/', views.UpdateFCMView.as_view(), name='update-fcm'),
#admin panel :
path('admin/login/', views.AdminLoginView.as_view(), name='admin-login'),
path('admin/', include(admin_router.urls)),
] ]

121
apps/account/views/user.py

@ -23,7 +23,7 @@ from rest_framework.exceptions import ValidationError
from utils.exceptions import InvaliedCodeVrify, ExpiredCodeException, ServiceUnavailableException from utils.exceptions import InvaliedCodeVrify, ExpiredCodeException, ServiceUnavailableException
from apps.account.models import User from apps.account.models import User
from apps.account.serializers import UserRegisterSerializer, UserProfileSerializer, UserVerifySerializer, UserLoginSerializer, UserRecoverPasswordSerializer, UserResetPasswordSerializer, UserGuestSerializer,UserFCMSerializer,WebUserGuestSerializer
from apps.account.serializers import UserRegisterSerializer, UserProfileSerializer, UserVerifySerializer, UserLoginSerializer, UserRecoverPasswordSerializer, UserResetPasswordSerializer, UserGuestSerializer,UserFCMSerializer,WebUserGuestSerializer, AdminUserSerializer
from apps.account.serializers.user_web import WebUserRegisterSerializer from apps.account.serializers.user_web import WebUserRegisterSerializer
from utils.redis import RedisManager from utils.redis import RedisManager
from utils.exceptions import AppAPIException from utils.exceptions import AppAPIException
@ -556,3 +556,122 @@ class UpdateFCMView(GenericAPIView):
user.save() user.save()
return Response({"detail": "FCM token updated successfully."}, status=status.HTTP_200_OK) return Response({"detail": "FCM token updated successfully."}, status=status.HTTP_200_OK)
class AdminLoginView(CreateAPIView):
permission_classes = [AllowAny]
serializer_class = UserLoginSerializer
@swagger_auto_schema(
operation_description="Login specifically for Admin Panel users",
request_body=UserLoginSerializer,
)
def post(self, request, *args, **kwargs):
return self.create(request, *args, **kwargs)
def get_client_ip(self):
request = self.request
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
return ip
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
data = serializer.data
email = request.data['email']
try:
user_obj = User.objects.get(email=email)
except User.DoesNotExist:
raise ValidationError({"email": "No admin found with that information."})
# --- THE CRUCIAL ADMIN CHECK ---
# Adjust 'is_staff' or 'is_superuser' based on how you define admins in Django
if not (user_obj.is_staff or user_obj.is_superuser):
raise AuthenticationFailed("No admin found with that information.")
user = authenticate(request, username=email, password=data['password'])
if not user:
raise ValidationError({"password": "Password is incorrect"})
user_timezone = serializer.validated_data.pop('timezone', None)
user.last_login = timezone.now()
user.is_active = True
user.save()
token, created = Token.objects.get_or_create(user=user)
# Log the history
user.login_history.create(
ip=self.get_client_ip(),
timezone=user_timezone,
device_os='web_admin'
)
return Response({
"id": user.id,
"fullname": user.fullname,
"email": user.email,
"token": token.key,
"user_type": user.user_type_based_on_groups,
"avatar": request.build_absolute_uri(user.avatar.url) if user.avatar else None,
}, status=status.HTTP_201_CREATED)
from rest_framework.viewsets import ModelViewSet
from rest_framework.filters import SearchFilter, OrderingFilter
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.permissions import IsAdminUser
class AdminUserViewSet(ModelViewSet):
"""
Admin-only endpoint for full CRUD operations on Users.
Includes searching, filtering, and pagination.
"""
serializer_class = AdminUserSerializer
permission_classes = [IsAuthenticated, IsAdminUser] # Strictly admin only
authentication_classes = [TokenAuthentication]
filter_backends = [] # Disable default backend filtering to use custom manual filtering
def get_queryset(self):
queryset = User.objects.filter(is_active=True, email__isnull=False).exclude(email='')
# Handle Search
search_query = self.request.query_params.get('search', None)
if search_query:
queryset = queryset.filter(
Q(fullname__icontains=search_query) |
Q(email__icontains=search_query) |
Q(phone_number__icontains=search_query)
)
# Handle is_active filter
is_active_param = self.request.query_params.get('is_active', None)
if is_active_param is not None:
if is_active_param.lower() == 'true':
queryset = queryset.filter(is_active=True)
elif is_active_param.lower() == 'false':
queryset = queryset.filter(is_active=False)
# Handle gender filter
gender_param = self.request.query_params.get('gender', None)
if gender_param:
queryset = queryset.filter(gender=gender_param)
# Handle Ordering
ordering_param = self.request.query_params.get('ordering', '-date_joined')
allowed_orderings = [
'date_joined', '-date_joined',
'last_login', '-last_login',
'fullname', '-fullname'
]
if ordering_param in allowed_orderings:
queryset = queryset.order_by(ordering_param)
else:
queryset = queryset.order_by('-date_joined')
return queryset.distinct()

117
apps/course/management/commands/redistribute_lessons_to_chapters.py

@ -0,0 +1,117 @@
import random
import sys
from django.core.management.base import BaseCommand
from django.db import transaction
from apps.course.models import Course, CourseChapter, CourseLesson
class Command(BaseCommand):
help = 'Redistributes course lessons into multiple chapters randomly (maintaining order) for courses with >= 2 lessons.'
def add_arguments(self, parser):
parser.add_argument(
'--dry-run',
action='store_true',
help='Simulates the redistribution without modifying the database.',
)
parser.add_argument(
'--max-chapters',
type=int,
default=5,
help='Maximum number of chapters to create for a course (default: 5).',
)
def handle(self, *args, **options):
# Reconfigure stdout to UTF-8 to prevent UnicodeEncodeError on Windows terminals
try:
sys.stdout.reconfigure(encoding='utf-8')
except AttributeError:
pass
dry_run = options['dry_run']
max_chapters = options['max_chapters']
if dry_run:
self.stdout.write(self.style.WARNING("⚠️ DRY RUN MODE: No database changes will be saved."))
self.stdout.write(self.style.WARNING("Starting lesson redistribution..."))
courses = Course.objects.all()
courses_updated = 0
total_chapters_created = 0
total_lessons_redistributed = 0
# Wrap everything in an atomic transaction to ensure safety
with transaction.atomic():
for course in courses:
lessons = list(course.lessons.all().order_by('priority'))
n = len(lessons)
if n < 2:
self.stdout.write(self.style.NOTICE(
f"Skipping course: '{course.title}' (ID: {course.id}) - Only has {n} lesson(s)."
))
continue
# Number of chapters C must be between 2 and min(N, max_chapters)
c = random.randint(2, min(n, max_chapters))
self.stdout.write(self.style.MIGRATE_HEADING(
f"\nReorganizing Course: '{course.title}' (ID: {course.id})"
f"\n Total lessons: {n} | Splitting into {c} chapters..."
))
# Partition the ordered lessons list into C slices
# We randomly pick C-1 unique split boundaries between 1 and n-1
split_points = sorted(random.sample(range(1, n), c - 1))
partitioned_lessons = []
last_idx = 0
for sp in split_points:
partitioned_lessons.append(lessons[last_idx:sp])
last_idx = sp
partitioned_lessons.append(lessons[last_idx:])
if not dry_run:
# 1. Unlink lessons from chapters first to prevent CASCADE deletion.
# Using .update() bypasses the save() method and avoids AttributeErrors
# caused by _adjust_priorities when chapter is None.
course.lessons.all().update(chapter=None)
# 2. Delete all existing chapters for this course
course.chapters.all().delete()
# 3. Create the new chapters and assign the sliced lessons
for i, chapter_lessons in enumerate(partitioned_lessons):
chapter_title = f"فصل {i+1}"
priority = i + 1
self.stdout.write(f" 📂 Chapter '{chapter_title}' (priority {priority}) contains:")
for idx, lesson in enumerate(chapter_lessons):
self.stdout.write(f" - [{idx + 1}] {lesson.title} (Original ID: {lesson.id})")
if not dry_run:
# Create the new chapter
chapter = CourseChapter.objects.create(
course=course,
title=chapter_title,
priority=priority
)
# Assign and save each lesson within this chapter
for idx, lesson in enumerate(chapter_lessons):
lesson.chapter = chapter
lesson.priority = idx + 1
lesson.save()
total_chapters_created += 1
total_lessons_redistributed += len(chapter_lessons)
courses_updated += 1
if dry_run:
self.stdout.write(self.style.WARNING("\n⚠️ Dry run complete. No database changes were made."))
else:
self.stdout.write(self.style.SUCCESS(
f"\n🎉 Successfully redistributed {total_lessons_redistributed} lessons "
f"across {total_chapters_created} chapters in {courses_updated} courses!"
))

2
entrypoint.sh

@ -3,7 +3,7 @@ sleep 20
python manage.py makemigrations python manage.py makemigrations
python manage.py migrate python manage.py migrate
# python manage.py seed_images # python manage.py seed_images
python manage.py compilemessages
# python manage.py compilemessages
python manage.py collectstatic --noinput python manage.py collectstatic --noinput
# Seed Russian data (only runs once, skips if data exists) # Seed Russian data (only runs once, skips if data exists)
# python manage.py seed_corrections # python manage.py seed_corrections

Loading…
Cancel
Save