mirror of
https://github.com/barkeser2002/offline-db.git
synced 2026-09-25 05:20:06 +03:00
- Added `bio` field to User model and migrations - Created `UserProfileUpdateSerializer` with RegexValidator (XSS) and Bleach (HTML sanitization) - Updated `ExternalSourceSerializer` to require `https://` or `magnet:` schemes - Replaced insecure extension checking with true MIME validation via `python-magic` in serializers (Subtitles, Images, Banners) - Fixed NextUI floating label overlap bug on magnet link input with `labelPlacement="outside"` and a placeholder. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> Co-authored-by: Senpai-YoloBot <41898282+github-actions[bot]@users.noreply.github.com>
147 lines
6.0 KiB
Python
147 lines
6.0 KiB
Python
from rest_framework import generics, permissions, status, viewsets, mixins
|
|
from rest_framework.views import APIView
|
|
from rest_framework.response import Response
|
|
from rest_framework.pagination import PageNumberPagination
|
|
from rest_framework.throttling import ScopedRateThrottle, UserRateThrottle, AnonRateThrottle
|
|
from rest_framework.decorators import action
|
|
from rest_framework_simplejwt.views import TokenObtainPairView
|
|
from django.shortcuts import get_object_or_404
|
|
from .models import Notification, UserBadge, WatchLog, Badge
|
|
from .serializers import NotificationSerializer, UserBadgeSerializer, WatchLogSerializer, UserProfileUpdateSerializer
|
|
|
|
class LoginThrottle(AnonRateThrottle):
|
|
scope = 'login'
|
|
|
|
class CustomTokenObtainPairView(TokenObtainPairView):
|
|
throttle_classes = [LoginThrottle]
|
|
|
|
class WatchLogCreateThrottle(UserRateThrottle):
|
|
scope = 'watchlog'
|
|
def allow_request(self, request, view):
|
|
if request.method != 'POST':
|
|
return True
|
|
return super().allow_request(request, view)
|
|
|
|
class StandardResultsSetPagination(PageNumberPagination):
|
|
page_size = 20
|
|
page_size_query_param = 'page_size'
|
|
max_page_size = 100
|
|
|
|
class NotificationViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
|
|
serializer_class = NotificationSerializer
|
|
permission_classes = [permissions.IsAuthenticated]
|
|
pagination_class = StandardResultsSetPagination
|
|
throttle_classes = [ScopedRateThrottle]
|
|
throttle_scope = 'notifications'
|
|
|
|
def get_queryset(self):
|
|
queryset = Notification.objects.filter(user=self.request.user).order_by('-created_at')
|
|
is_read_param = self.request.query_params.get('is_read')
|
|
|
|
if is_read_param is not None:
|
|
if is_read_param.lower() in ['true', '1', 'yes']:
|
|
queryset = queryset.filter(is_read=True)
|
|
elif is_read_param.lower() in ['false', '0', 'no']:
|
|
queryset = queryset.filter(is_read=False)
|
|
return queryset
|
|
|
|
@action(detail=False, methods=['get'])
|
|
def unread_count(self, request):
|
|
count = Notification.objects.filter(user=request.user, is_read=False).count()
|
|
return Response({'count': count})
|
|
|
|
@action(detail=False, methods=['post'])
|
|
def mark_all_read(self, request):
|
|
Notification.objects.filter(user=request.user, is_read=False).update(is_read=True)
|
|
return Response({'status': 'all marked as read'})
|
|
|
|
@action(detail=True, methods=['post'])
|
|
def mark_read(self, request, pk=None):
|
|
notification = self.get_object()
|
|
notification.is_read = True
|
|
notification.save()
|
|
return Response({'status': 'marked as read'})
|
|
|
|
@action(detail=False, methods=['post'], url_path='bulk-update')
|
|
def bulk_update_status(self, request):
|
|
"""
|
|
Bulk update is_read status for a list of notification IDs.
|
|
Expected payload:
|
|
{
|
|
"notification_ids": [1, 2, 3],
|
|
"is_read": true
|
|
}
|
|
"""
|
|
notification_ids = request.data.get('notification_ids', [])
|
|
is_read = request.data.get('is_read')
|
|
|
|
if not isinstance(notification_ids, list) or is_read is None:
|
|
return Response(
|
|
{"error": "Invalid payload. 'notification_ids' (list) and 'is_read' (boolean) are required."},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
if not notification_ids:
|
|
return Response({"status": "no notifications updated", "updated_count": 0})
|
|
|
|
# Ensure users only update their own notifications
|
|
updated_count = Notification.objects.filter(
|
|
user=request.user,
|
|
id__in=notification_ids
|
|
).update(is_read=is_read)
|
|
|
|
return Response({
|
|
"status": "success",
|
|
"updated_count": updated_count
|
|
})
|
|
|
|
class UserBadgeViewSet(viewsets.ReadOnlyModelViewSet):
|
|
serializer_class = UserBadgeSerializer
|
|
permission_classes = [permissions.IsAuthenticated]
|
|
|
|
def get_queryset(self):
|
|
return UserBadge.objects.filter(user=self.request.user).select_related('badge')
|
|
|
|
class WatchLogViewSet(mixins.CreateModelMixin, mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
|
|
serializer_class = WatchLogSerializer
|
|
permission_classes = [permissions.IsAuthenticated]
|
|
throttle_classes = [WatchLogCreateThrottle]
|
|
|
|
def get_queryset(self):
|
|
return WatchLog.objects.filter(user=self.request.user).order_by('-watched_at')
|
|
|
|
def perform_create(self, serializer):
|
|
serializer.save(user=self.request.user)
|
|
# Note: Badge checks are handled automatically via post_save signal in users.signals
|
|
|
|
class UserProfileAPIView(APIView):
|
|
permission_classes = [permissions.IsAuthenticated]
|
|
|
|
def get(self, request):
|
|
user = request.user
|
|
badges = UserBadge.objects.filter(user=user).select_related('badge')
|
|
# Optimization: WatchLogSerializer only serializes the 'episode' field (ID representation),
|
|
# so select_related('episode__season__anime') causes an unnecessary DB join
|
|
history = WatchLog.objects.filter(user=user).order_by('-watched_at')[:10]
|
|
|
|
# Note: In a real app, create a ProfileSerializer.
|
|
# Here constructing ad-hoc response for speed as per migration plan.
|
|
return Response({
|
|
'id': user.id,
|
|
'username': user.username,
|
|
'email': user.email,
|
|
'bio': getattr(user, 'bio', ''),
|
|
'is_premium': getattr(user, 'is_premium', False),
|
|
'date_joined': user.date_joined,
|
|
'badges': UserBadgeSerializer(badges, many=True).data,
|
|
'recent_history': WatchLogSerializer(history, many=True).data
|
|
})
|
|
|
|
def patch(self, request):
|
|
user = request.user
|
|
serializer = UserProfileUpdateSerializer(user, data=request.data, partial=True)
|
|
if serializer.is_valid():
|
|
serializer.save()
|
|
return Response(serializer.data)
|
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|