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. 123
      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:
model = User
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.region_info import RegionInfoView
admin_router = DefaultRouter()
admin_router.register(r'users', views.AdminUserViewSet, basename='admin-users')
urlpatterns = [
path('location-info/', IPGeolocationAPIView.as_view(), name='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('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)),
]

123
apps/account/views/user.py

@ -23,7 +23,7 @@ from rest_framework.exceptions import ValidationError
from utils.exceptions import InvaliedCodeVrify, ExpiredCodeException, ServiceUnavailableException
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 utils.redis import RedisManager
from utils.exceptions import AppAPIException
@ -555,4 +555,123 @@ class UpdateFCMView(GenericAPIView):
user.fcm = fcm_token
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 migrate
# python manage.py seed_images
python manage.py compilemessages
# python manage.py compilemessages
python manage.py collectstatic --noinput
# Seed Russian data (only runs once, skips if data exists)
# python manage.py seed_corrections

Loading…
Cancel
Save