From d9c6c8ff59a9c267cfccc17ccb9830e0810aaf38 Mon Sep 17 00:00:00 2001 From: mashen Date: Thu, 9 Apr 2026 13:44:13 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=89=80=E6=9C=89=20?= =?UTF-8?q?Django=20apps=20=E7=9A=84=20ViewSets?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - User ViewSet(个人中心、统计、收藏、评分、搜索) - Region ViewSet(层级查询、树形结构、文章、服务、统计、评分、收藏) - Article ViewSet(创建、提交、审核、评论、点赞、统计) - FeaturedService ViewSet(创建、提交、审核、评论、点赞、评分、统计) - Moderation ViewSets(版主申请、权限、支持、限制) - Interaction ViewSets(评论、评分、点赞、收藏、AI审核) 完整实现权限控制、审核流程和交互功能 --- backend/apps/articles/views.py | 204 +++++++++++++++++++++ backend/apps/featured_services/views.py | 227 ++++++++++++++++++++++++ backend/apps/interactions/views.py | 226 +++++++++++++++++++++++ backend/apps/moderation/views.py | 197 ++++++++++++++++++++ backend/apps/regions/views.py | 141 +++++++++++++++ backend/apps/users/views.py | 138 +++++++++++++- 6 files changed, 1129 insertions(+), 4 deletions(-) create mode 100644 backend/apps/articles/views.py create mode 100644 backend/apps/featured_services/views.py create mode 100644 backend/apps/interactions/views.py create mode 100644 backend/apps/moderation/views.py create mode 100644 backend/apps/regions/views.py diff --git a/backend/apps/articles/views.py b/backend/apps/articles/views.py new file mode 100644 index 0000000..6eefa77 --- /dev/null +++ b/backend/apps/articles/views.py @@ -0,0 +1,204 @@ +from rest_framework import viewsets, permissions, status, filters +from rest_framework.decorators import action +from rest_framework.response import Response +from django.utils import timezone +from django.db.models import Q +from .models import Article +from .serializers import ( + ArticleSerializer, + ArticleCreateSerializer, + ArticleUpdateSerializer, + ArticleReviewSerializer, + ArticleListSerializer +) + + +class ArticleViewSet(viewsets.ModelViewSet): + """ViewSet for Article model.""" + + permission_classes = [permissions.IsAuthenticatedOrReadOnly] + filter_backends = [filters.SearchFilter, filters.OrderingFilter, filters.DjangoFilterBackend] + search_fields = ['title', 'content'] + filterset_fields = ['article_type', 'region', 'publish_status'] + ordering_fields = ['created_at', 'updated_at', 'published_at'] + ordering = ['-created_at'] + + def get_queryset(self): + queryset = Article.objects.select_related('author', 'region', 'moderator_reviewer') + + # Only show published articles to non-authenticated users + if not self.request.user.is_authenticated: + return queryset.filter(publish_status='published') + + # Show all for admins + if self.request.user.is_admin(): + return queryset + + # Show own articles + published articles for regular users + return queryset.filter( + Q(author=self.request.user) | + Q(publish_status='published') + ).distinct() + + def get_serializer_class(self): + if self.action == 'create': + return ArticleCreateSerializer + elif self.action in ['update', 'partial_update']: + return ArticleUpdateSerializer + elif self.action == 'list': + return ArticleListSerializer + elif self.action in ['approve', 'reject', 'submit']: + return ArticleReviewSerializer + return ArticleSerializer + + def perform_create(self, serializer): + serializer.save(author=self.request.user) + + def perform_update(self, serializer): + # Only allow updating own articles or by admin + if (not self.request.user.is_admin() and + str(serializer.instance.author.id) != str(self.request.user.id)): + from rest_framework.exceptions import PermissionDenied + raise PermissionDenied("You can only update your own articles") + serializer.save() + + def perform_destroy(self, instance): + # Only allow deleting own articles or by admin + if (not self.request.user.is_admin() and + str(instance.author.id) != str(self.request.user.id)): + from rest_framework.exceptions import PermissionDenied + raise PermissionDenied("You can only delete your own articles") + instance.delete() + + @action(detail=True, methods=['post']) + def submit(self, request, pk=None): + """Submit article for review.""" + article = self.get_object() + if article.author != request.user: + return Response( + {'detail': 'You can only submit your own articles'}, + status=status.HTTP_403_FORBIDDEN + ) + article.submit_for_review() + return Response({'message': 'Article submitted for review'}) + + @action(detail=True, methods=['post']) + def approve(self, request, pk=None): + """Approve article (moderator only).""" + article = self.get_object() + serializer = self.get_serializer(data=request.data) + + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + if not request.user.is_moderator(): + return Response( + {'detail': 'Only moderators can approve articles'}, + status=status.HTTP_403_FORBIDDEN + ) + + # Check if moderator has permission for this region + from apps.moderation.models import ModeratorPermission + has_permission = ModeratorPermission.objects.filter( + moderator=request.user, + region=article.region, + status='active' + ).exists() + + if not has_permission: + return Response( + {'detail': 'You do not have permission to approve articles in this region'}, + status=status.HTTP_403_FORBIDDEN + ) + + article.approve_moderator( + reviewer=request.user, + reason=serializer.validated_data.get('reason', '') + ) + return Response({'message': 'Article approved'}) + + @action(detail=True, methods=['post']) + def reject(self, request, pk=None): + """Reject article (moderator only).""" + article = self.get_object() + serializer = self.get_serializer(data=request.data) + + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + if not request.user.is_moderator(): + return Response( + {'detail': 'Only moderators can reject articles'}, + status=status.HTTP_403_FORBIDDEN + ) + + # Check if moderator has permission for this region + from apps.moderation.models import ModeratorPermission + has_permission = ModeratorPermission.objects.filter( + moderator=request.user, + region=article.region, + status='active' + ).exists() + + if not has_permission: + return Response( + {'detail': 'You do not have permission to reject articles in this region'}, + status=status.HTTP_403_FORBIDDEN + ) + + article.reject_moderator( + reviewer=request.user, + reason=serializer.validated_data.get('reason', 'Required reason') + ) + return Response({'message': 'Article rejected'}) + + @action(detail=True, methods=['get']) + def comments(self, request, pk=None): + """Get comments for an article.""" + article = self.get_object() + from apps.interactions.serializers import CommentSerializer + from apps.interactions.models import Comment + comments = Comment.objects.filter( + target_type='article', + target_id=article.id, + ai_status='approved' + ) + serializer = CommentSerializer(comments, many=True) + return Response(serializer.data) + + @action(detail=True, methods=['post']) + def like(self, request, pk=None): + """Like or unlike an article.""" + article = self.get_object() + from apps.interactions.models import Like + + like, created = Like.objects.get_or_create( + user=request.user, + target_type='article', + target_id=article.id + ) + + if not created: + like.delete() + return Response({'message': 'Unliked', 'liked': False}) + + return Response({'message': 'Liked', 'liked': True}) + + @action(detail=True, methods=['get']) + def stats(self, request, pk=None): + """Get article statistics.""" + article = self.get_object() + from apps.interactions.models import Like, Comment, Rating + + return Response({ + 'likes_count': Like.objects.filter( + target_type='article', + target_id=article.id + ).count(), + 'comments_count': Comment.objects.filter( + target_type='article', + target_id=article.id, + ai_status='approved' + ).count(), + 'views_count': getattr(article, 'views_count', 0), + }) \ No newline at end of file diff --git a/backend/apps/featured_services/views.py b/backend/apps/featured_services/views.py new file mode 100644 index 0000000..ab6f3c4 --- /dev/null +++ b/backend/apps/featured_services/views.py @@ -0,0 +1,227 @@ +from rest_framework import viewsets, permissions, status, filters +from rest_framework.decorators import action +from rest_framework.response import Response +from django.db.models import Q +from .models import FeaturedService +from .serializers import ( + FeaturedServiceSerializer, + FeaturedServiceCreateSerializer, + FeaturedServiceUpdateSerializer, + FeaturedServiceReviewSerializer, + FeaturedServiceListSerializer +) + + +class FeaturedServiceViewSet(viewsets.ModelViewSet): + """ViewSet for FeaturedService model.""" + + permission_classes = [permissions.IsAuthenticatedOrReadOnly] + filter_backends = [filters.SearchFilter, filters.OrderingFilter, filters.DjangoFilterBackend] + search_fields = ['name', 'description'] + filterset_fields = ['category', 'region', 'publish_status'] + ordering_fields = ['created_at', 'updated_at', 'published_at'] + ordering = ['-created_at'] + + def get_queryset(self): + queryset = FeaturedService.objects.select_related('submitter', 'region', 'moderator_reviewer') + + # Only show published services to non-authenticated users + if not self.request.user.is_authenticated: + return queryset.filter(publish_status='published') + + # Show all for admins + if self.request.user.is_admin(): + return queryset + + # Show own services + published services for regular users + return queryset.filter( + Q(submitter=self.request.user) | + Q(publish_status='published') + ).distinct() + + def get_serializer_class(self): + if self.action == 'create': + return FeaturedServiceCreateSerializer + elif self.action in ['update', 'partial_update']: + return FeaturedServiceUpdateSerializer + elif self.action == 'list': + return FeaturedServiceListSerializer + elif self.action in ['approve', 'reject', 'submit']: + return FeaturedServiceReviewSerializer + return FeaturedServiceSerializer + + def perform_create(self, serializer): + serializer.save(submitter=self.request.user) + + def perform_update(self, serializer): + # Only allow updating own services or by admin + if (not self.request.user.is_admin() and + str(serializer.instance.submitter.id) != str(self.request.user.id)): + from rest_framework.exceptions import PermissionDenied + raise PermissionDenied("You can only update your own services") + serializer.save() + + def perform_destroy(self, instance): + # Only allow deleting own services or by admin + if (not self.request.user.is_admin() and + str(instance.submitter.id) != str(self.request.user.id)): + from rest_framework.exceptions import PermissionDenied + raise PermissionDenied("You can only delete your own services") + instance.delete() + + @action(detail=True, methods=['post']) + def submit(self, request, pk=None): + """Submit service for review.""" + service = self.get_object() + if service.submitter != request.user: + return Response( + {'detail': 'You can only submit your own services'}, + status=status.HTTP_403_FORBIDDEN + ) + service.submit_for_review() + return Response({'message': 'Service submitted for review'}) + + @action(detail=True, methods=['post']) + def approve(self, request, pk=None): + """Approve service (moderator only).""" + service = self.get_object() + serializer = self.get_serializer(data=request.data) + + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + if not request.user.is_moderator(): + return Response( + {'detail': 'Only moderators can approve services'}, + status=status.HTTP_403_FORBIDDEN + ) + + # Check if moderator has permission for this region + from apps.moderation.models import ModeratorPermission + has_permission = ModeratorPermission.objects.filter( + moderator=request.user, + region=service.region, + status='active' + ).exists() + + if not has_permission: + return Response( + {'detail': 'You do not have permission to approve services in this region'}, + status=status.HTTP_403_FORBIDDEN + ) + + service.approve_moderator( + reviewer=request.user, + reason=serializer.validated_data.get('reason', '') + ) + return Response({'message': 'Service approved'}) + + @action(detail=True, methods=['post']) + def reject(self, request, pk=None): + """Reject service (moderator only).""" + service = self.get_object() + serializer = self.get_serializer(data=request.data) + + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + if not request.user.is_moderator(): + return Response( + {'detail': 'Only moderators can reject services'}, + status=status.HTTP_403_FORBIDDEN + ) + + # Check if moderator has permission for this region + from apps.moderation.models import ModeratorPermission + has_permission = ModeratorPermission.objects.filter( + moderator=request.user, + region=service.region, + status='active' + ).exists() + + if not has_permission: + return Response( + {'detail': 'You do not have permission to reject services in this region'}, + status=status.HTTP_403_FORBIDDEN + ) + + service.reject_moderator( + reviewer=request.user, + reason=serializer.validated_data.get('reason', 'Required reason') + ) + return Response({'message': 'Service rejected'}) + + @action(detail=True, methods=['get']) + def comments(self, request, pk=None): + """Get comments for a service.""" + service = self.get_object() + from apps.interactions.serializers import CommentSerializer + from apps.interactions.models import Comment + comments = Comment.objects.filter( + target_type='service', + target_id=service.id, + ai_status='approved' + ) + serializer = CommentSerializer(comments, many=True) + return Response(serializer.data) + + @action(detail=True, methods=['post']) + def like(self, request, pk=None): + """Like or unlike a service.""" + service = self.get_object() + from apps.interactions.models import Like + + like, created = Like.objects.get_or_create( + user=request.user, + target_type='service', + target_id=service.id + ) + + if not created: + like.delete() + return Response({'message': 'Unliked', 'liked': False}) + + return Response({'message': 'Liked', 'liked': True}) + + @action(detail=True, methods=['post']) + def rate(self, request, pk=None): + """Rate a service.""" + service = self.get_object() + from apps.interactions.serializers import RatingCreateSerializer + + serializer = RatingCreateSerializer(data=request.data) + if serializer.is_valid(): + serializer.save(target_type='service', target_id=service.id) + return Response({'message': 'Rating saved'}) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + @action(detail=True, methods=['get']) + def stats(self, request, pk=None): + """Get service statistics.""" + service = self.get_object() + from apps.interactions.models import Like, Comment, Rating + + likes_count = Like.objects.filter( + target_type='service', + target_id=service.id + ).count() + + comments_count = Comment.objects.filter( + target_type='service', + target_id=service.id, + ai_status='approved' + ).count() + + ratings = Rating.objects.filter( + target_type='service', + target_id=service.id + ) + + avg_rating = ratings.aggregate(avg=models.Avg('score'))['avg'] or 0 + + return Response({ + 'likes_count': likes_count, + 'comments_count': comments_count, + 'avg_rating': round(avg_rating, 1), + 'ratings_count': ratings.count(), + }) \ No newline at end of file diff --git a/backend/apps/interactions/views.py b/backend/apps/interactions/views.py new file mode 100644 index 0000000..6db26c5 --- /dev/null +++ b/backend/apps/interactions/views.py @@ -0,0 +1,226 @@ +from rest_framework import viewsets, permissions, status, filters +from rest_framework.decorators import action +from rest_framework.response import Response +from .models import Comment, Rating, Like, Favorite +from .serializers import ( + CommentSerializer, + CommentCreateSerializer, + RatingSerializer, + RatingCreateSerializer, + LikeSerializer, + FavoriteSerializer, + FavoriteCreateSerializer +) + + +class CommentViewSet(viewsets.ModelViewSet): + """ViewSet for Comment model.""" + + queryset = Comment.objects.select_related('author') + permission_classes = [permissions.IsAuthenticatedOrReadOnly] + filter_backends = [filters.SearchFilter, filters.OrderingFilter, filters.DjangoFilterBackend] + search_fields = ['content'] + filterset_fields = ['target_type', 'target_id', 'ai_status'] + ordering_fields = ['created_at'] + ordering = ['-created_at'] + + def get_queryset(self): + # Only show approved comments + if not self.request.user.is_authenticated: + return self.queryset.filter(ai_status='approved') + + # Admins see all + if self.request.user.is_admin(): + return self.queryset + + # Regular users see approved + their own + return self.queryset.filter( + Q(ai_status='approved') | + Q(author=self.request.user) + ).distinct() + + def get_serializer_class(self): + if self.action == 'create': + return CommentCreateSerializer + return CommentSerializer + + def perform_create(self, serializer): + serializer.save() + + def perform_update(self, serializer): + # Only allow updating own comments + if str(serializer.instance.author.id) != str(self.request.user.id): + from rest_framework.exceptions import PermissionDenied + raise PermissionDenied("You can only update your own comments") + serializer.save() + + def perform_destroy(self, instance): + # Only allow deleting own comments or by admin + if (not self.request.user.is_admin() and + str(instance.author.id) != str(self.request.user.id)): + from rest_framework.exceptions import PermissionDenied + raise PermissionDenied("You can only delete your own comments") + instance.delete() + + @action(detail=True, methods=['post']) + def approve_ai(self, request, pk=None): + """Approve comment by AI (simulated).""" + if not request.user.is_ai_auditor(): + return Response( + {'detail': 'Only AI auditors can approve comments'}, + status=status.HTTP_403_FORBIDDEN + ) + + comment = self.get_object() + comment.approve_ai() + return Response({'message': 'Comment approved by AI'}) + + @action(detail=True, methods=['post']) + def reject_ai(self, request, pk=None): + """Reject comment by AI (simulated).""" + if not request.user.is_ai_auditor(): + return Response( + {'detail': 'Only AI auditors can reject comments'}, + status=status.HTTP_403_FORBIDDEN + ) + + comment = self.get_object() + reason = request.data.get('reason', 'Content violates guidelines') + comment.reject_ai(reason) + return Response({'message': 'Comment rejected by AI'}) + + +class RatingViewSet(viewsets.ModelViewSet): + """ViewSet for Rating model.""" + + queryset = Rating.objects.select_related('user') + serializer_class = RatingSerializer + permission_classes = [permissions.IsAuthenticatedOrReadOnly] + filter_backends = [filters.SearchFilter, filters.OrderingFilter, filters.DjangoFilterBackend] + filterset_fields = ['target_type', 'target_id', 'user'] + ordering_fields = ['created_at'] + ordering = ['-created_at'] + + def get_queryset(self): + # Admins see all + if self.request.user.is_admin(): + return self.queryset + + # Regular users see their own ratings + return self.queryset.filter(user=self.request.user) + + def get_serializer_class(self): + if self.action == 'create': + return RatingCreateSerializer + return RatingSerializer + + def perform_create(self, serializer): + serializer.save() + + def perform_destroy(self, instance): + # Only allow deleting own ratings + if str(instance.user.id) != str(self.request.user.id): + from rest_framework.exceptions import PermissionDenied + raise PermissionDenied("You can only delete your own ratings") + instance.delete() + + @action(detail=False, methods=['get']) + def my_ratings(self, request): + """Get current user's ratings.""" + ratings = Rating.objects.filter(user=request.user).select_related() + serializer = self.get_serializer(ratings, many=True) + return Response(serializer.data) + + +class LikeViewSet(viewsets.ModelViewSet): + """ViewSet for Like model.""" + + queryset = Like.objects.select_related('user') + serializer_class = LikeSerializer + permission_classes = [permissions.IsAuthenticated] + filter_backends = [filters.OrderingFilter, filters.DjangoFilterBackend] + filterset_fields = ['target_type', 'target_id', 'user'] + ordering_fields = ['created_at'] + ordering = ['-created_at'] + + def get_queryset(self): + # Admins see all + if self.request.user.is_admin(): + return self.queryset + + # Regular users see their own likes + return self.queryset.filter(user=self.request.user) + + @action(detail=False, methods=['post']) + def toggle(self, request): + """Toggle like on a target.""" + target_type = request.data.get('target_type') + target_id = request.data.get('target_id') + + if not target_type or not target_id: + return Response( + {'detail': 'target_type and target_id are required'}, + status=status.HTTP_400_BAD_REQUEST + ) + + like, created = Like.objects.get_or_create( + user=request.user, + target_type=target_type, + target_id=target_id + ) + + if not created: + like.delete() + return Response({'message': 'Unliked', 'liked': False}) + + return Response({'message': 'Liked', 'liked': True}) + + @action(detail=False, methods=['get']) + def my_likes(self, request): + """Get current user's likes.""" + likes = Like.objects.filter(user=request.user).select_related() + serializer = self.get_serializer(likes, many=True) + return Response(serializer.data) + + +class FavoriteViewSet(viewsets.ModelViewSet): + """ViewSet for Favorite model.""" + + queryset = Favorite.objects.select_related('user') + serializer_class = FavoriteSerializer + permission_classes = [permissions.IsAuthenticated] + filter_backends = [filters.OrderingFilter, filters.DjangoFilterBackend] + filterset_fields = ['target_type', 'target_id', 'user'] + ordering_fields = ['created_at'] + ordering = ['-created_at'] + + def get_queryset(self): + # Admins see all + if self.request.user.is_admin(): + return self.queryset + + # Regular users see their own favorites + return self.queryset.filter(user=self.request.user) + + def get_serializer_class(self): + if self.action == 'create': + return FavoriteCreateSerializer + return FavoriteSerializer + + @action(detail=False, methods=['post']) + def toggle(self, request): + """Toggle favorite on a target.""" + serializer = FavoriteCreateSerializer(data=request.data) + if serializer.is_valid(): + result = serializer.save() + if result is None: + return Response({'message': 'Unfavorited', 'favorited': False}) + return Response({'message': 'Favorited', 'favorited': True}) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + @action(detail=False, methods=['get']) + def my_favorites(self, request): + """Get current user's favorites.""" + favorites = Favorite.objects.filter(user=request.user).select_related() + serializer = FavoriteSerializer(favorites, many=True) + return Response(serializer.data) \ No newline at end of file diff --git a/backend/apps/moderation/views.py b/backend/apps/moderation/views.py new file mode 100644 index 0000000..4798a5c --- /dev/null +++ b/backend/apps/moderation/views.py @@ -0,0 +1,197 @@ +from rest_framework import viewsets, permissions, status, filters +from rest_framework.decorators import action +from rest_framework.response import Response +from django.db.models import Q +from .models import ( + ModeratorApplication, + ModeratorPermission, + ModeratorSupport, + PermissionRestriction +) +from .serializers import ( + ModeratorApplicationSerializer, + ModeratorApplicationCreateSerializer, + ModeratorPermissionSerializer, + ModeratorSupportSerializer, + PermissionRestrictionSerializer +) + + +class ModeratorApplicationViewSet(viewsets.ModelViewSet): + """ViewSet for ModeratorApplication model.""" + + permission_classes = [permissions.IsAuthenticated] + filter_backends = [filters.SearchFilter, filters.OrderingFilter, filters.DjangoFilterBackend] + search_fields = ['applicant__username', 'region__name'] + filterset_fields = ['status', 'rank', 'region'] + ordering_fields = ['created_at', 'deadline'] + ordering = ['-created_at'] + + def get_queryset(self): + queryset = ModeratorApplication.objects.select_related('applicant', 'region', 'reviewed_by') + + # Admins see all + if self.request.user.is_admin(): + return queryset + + # Regular users see their own applications + return queryset.filter(applicant=self.request.user) + + def get_serializer_class(self): + if self.action == 'create': + return ModeratorApplicationCreateSerializer + return ModeratorApplicationSerializer + + def perform_create(self, serializer): + serializer.save() + + @action(detail=True, methods=['post']) + def support(self, request, pk=None): + """Support a moderator application.""" + application = self.get_object() + + # Check if application is still pending + if application.status != 'pending': + return Response( + {'detail': 'Can only support pending applications'}, + status=status.HTTP_400_BAD_REQUEST + ) + + # Check if already supported + if ModeratorSupport.objects.filter( + supporter=request.user, + application=application + ).exists(): + return Response( + {'detail': 'Already supported this application'}, + status=status.HTTP_400_BAD_REQUEST + ) + + serializer = ModeratorSupportSerializer(data={'application': application.id}) + if serializer.is_valid(): + serializer.save() + return Response({'message': 'Application supported', 'support_count': application.support_count}) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + @action(detail=True, methods=['post']) + def approve(self, request, pk=None): + """Approve moderator application (admin only).""" + if not request.user.is_admin(): + return Response( + {'detail': 'Only admins can approve applications'}, + status=status.HTTP_403_FORBIDDEN + ) + + application = self.get_object() + + if application.status != 'pending': + return Response( + {'detail': 'Can only approve pending applications'}, + status=status.HTTP_400_BAD_REQUEST + ) + + # Check if has enough support + if not application.has_enough_support(): + return Response( + {'detail': 'Not enough support votes'}, + status=status.HTTP_400_BAD_REQUEST + ) + + # Create moderator permission + from .models import ModeratorPermission + ModeratorPermission.objects.create( + moderator=application.applicant, + region=application.region, + rank=application.rank, + status='active' + ) + + # Update application status + application.status = 'approved' + application.reviewed_by = request.user + application.reviewed_at = timezone.now() + application.save() + + return Response({'message': 'Application approved, moderator permissions granted'}) + + @action(detail=True, methods=['post']) + def reject(self, request, pk=None): + """Reject moderator application (admin only).""" + if not request.user.is_admin(): + return Response( + {'detail': 'Only admins can reject applications'}, + status=status.HTTP_403_FORBIDDEN + ) + + application = self.get_object() + + if application.status != 'pending': + return Response( + {'detail': 'Can only reject pending applications'}, + status=status.HTTP_400_BAD_REQUEST + ) + + application.status = 'rejected' + application.reviewed_by = request.user + application.reviewed_at = timezone.now() + application.save() + + return Response({'message': 'Application rejected'}) + + @action(detail=False, methods=['get']) + def my_applications(self, request): + """Get current user's applications.""" + applications = ModeratorApplication.objects.filter( + applicant=request.user + ).select_related('region') + serializer = self.get_serializer(applications, many=True) + return Response(serializer.data) + + +class ModeratorPermissionViewSet(viewsets.ReadOnlyModelViewSet): + """ViewSet for ModeratorPermission model (read-only).""" + + queryset = ModeratorPermission.objects.select_related('moderator', 'region') + serializer_class = ModeratorPermissionSerializer + permission_classes = [permissions.IsAuthenticated] + + def get_queryset(self): + # Admins see all + if self.request.user.is_admin(): + return self.queryset + + # Moderators see their own permissions + return self.queryset.filter(moderator=self.request.user) + + @action(detail=False, methods=['get']) + def my_permissions(self, request): + """Get current user's moderator permissions.""" + permissions = ModeratorPermission.objects.filter( + moderator=request.user, + status='active' + ).select_related('region') + serializer = self.get_serializer(permissions, many=True) + return Response(serializer.data) + + +class PermissionRestrictionViewSet(viewsets.ModelViewSet): + """ViewSet for PermissionRestriction model.""" + + queryset = PermissionRestriction.objects.select_related('operator', 'target_moderator') + serializer_class = PermissionRestrictionSerializer + permission_classes = [permissions.IsAuthenticated] + + def get_queryset(self): + # Admins see all + if self.request.user.is_admin(): + return self.queryset + + # Moderators see restrictions on themselves + return self.queryset.filter(target_moderator=self.request.user) + + def perform_create(self, serializer): + # Only admins can create restrictions + if not self.request.user.is_admin(): + from rest_framework.exceptions import PermissionDenied + raise PermissionDenied("Only admins can create restrictions") + serializer.save(operator=self.request.user) \ No newline at end of file diff --git a/backend/apps/regions/views.py b/backend/apps/regions/views.py new file mode 100644 index 0000000..d5b659c --- /dev/null +++ b/backend/apps/regions/views.py @@ -0,0 +1,141 @@ +from rest_framework import viewsets, permissions, filters +from rest_framework.decorators import action +from rest_framework.response import Response +from django.shortcuts import get_object_or_404 +from .models import Region +from .serializers import ( + RegionSerializer, + RegionDetailSerializer, + RegionTreeSerializer +) +from apps.interactions.models import Rating, Favorite + + +class RegionViewSet(viewsets.ModelViewSet): + """ViewSet for Region model.""" + + queryset = Region.objects.filter(status='active') + permission_classes = [permissions.IsAuthenticatedOrReadOnly] + filter_backends = [filters.SearchFilter, filters.OrderingFilter] + search_fields = ['name'] + ordering_fields = ['name', 'level', 'created_at'] + ordering = ['level', 'name'] + + def get_serializer_class(self): + if self.action == 'retrieve': + return RegionDetailSerializer + elif self.action == 'tree': + return RegionTreeSerializer + return RegionSerializer + + def perform_create(self, serializer): + # Only admin can create regions + if not self.request.user.is_admin(): + from rest_framework.exceptions import PermissionDenied + raise PermissionDenied("Only admins can create regions") + serializer.save() + + def perform_update(self, serializer): + # Only admin can update regions + if not self.request.user.is_admin(): + from rest_framework.exceptions import PermissionDenied + raise PermissionDenied("Only admins can update regions") + serializer.save() + + @action(detail=False, methods=['get']) + def provinces(self, request): + """Get all provinces (top-level regions).""" + provinces = self.queryset.filter(parent__isnull=True) + serializer = self.get_serializer(provinces, many=True) + return Response(serializer.data) + + @action(detail=True, methods=['get']) + def children(self, request, pk=None): + """Get children of a region.""" + region = self.get_object() + children = region.get_children() + serializer = self.get_serializer(children, many=True) + return Response(serializer.data) + + @action(detail=True, methods=['get']) + def path(self, request, pk=None): + """Get full path of a region.""" + region = self.get_object() + path = [] + current = region + while current: + serializer = self.get_serializer(current) + path.insert(0, serializer.data) + current = current.parent + return Response(path) + + @action(detail=False, methods=['get']) + def tree(self, request): + """Get region tree structure.""" + root_regions = self.queryset.filter(parent__isnull=True) + serializer = RegionTreeSerializer(root_regions, many=True) + return Response(serializer.data) + + @action(detail=True, methods=['get']) + def articles(self, request, pk=None): + """Get articles for a region.""" + region = self.get_object() + articles = region.articles.filter(publish_status='published') + from apps.articles.serializers import ArticleListSerializer + serializer = ArticleListSerializer(articles, many=True) + return Response(serializer.data) + + @action(detail=True, methods=['get']) + def services(self, request, pk=None): + """Get featured services for a region.""" + region = self.get_object() + services = region.featured_services.filter(publish_status='published') + from apps.featured_services.serializers import FeaturedServiceListSerializer + serializer = FeaturedServiceListSerializer(services, many=True) + return Response(serializer.data) + + @action(detail=True, methods=['get']) + def stats(self, request, pk=None): + """Get statistics for a region.""" + region = self.get_object() + return Response({ + 'articles_count': region.articles.filter(publish_status='published').count(), + 'services_count': region.featured_services.filter(publish_status='published').count(), + 'children_count': region.children.count(), + }) + + @action(detail=True, methods=['post']) + def rate(self, request, pk=None): + """Rate a region.""" + region = self.get_object() + serializer = RatingCreateSerializer(data=request.data) + if serializer.is_valid(): + serializer.save(target_type='region', target_id=region.id) + return Response({'message': 'Rating saved'}, status=201) + return Response(serializer.errors, status=400) + + @action(detail=True, methods=['get']) + def my_rating(self, request, pk=None): + """Get user's rating for a region.""" + region = self.get_object() + try: + rating = Rating.objects.get( + user=request.user, + target_type='region', + target_id=region.id + ) + return Response({'score': rating.score}) + except Rating.DoesNotExist: + return Response({'score': None}) + + @action(detail=True, methods=['post']) + def favorite(self, request, pk=None): + """Favorite or unfavorite a region.""" + region = self.get_object() + serializer = FavoriteCreateSerializer(data=request.data) + if serializer.is_valid(): + result = serializer.save(target_type='region', target_id=region.id) + if result is None: + return Response({'message': 'Unfavorited'}, status=200) + return Response({'message': 'Favorited'}, status=201) + return Response(serializer.errors, status=400) \ No newline at end of file diff --git a/backend/apps/users/views.py b/backend/apps/users/views.py index 7fb83cf..1fcf567 100644 --- a/backend/apps/users/views.py +++ b/backend/apps/users/views.py @@ -1,8 +1,14 @@ -from rest_framework import viewsets, permissions +from rest_framework import viewsets, permissions, status from rest_framework.decorators import action from rest_framework.response import Response +from django.db.models import Count, Q from .models import User -from .serializers import UserSerializer, UserDetailSerializer +from .serializers import ( + UserSerializer, + UserDetailSerializer, + UserUpdateSerializer, + UserStatsSerializer +) class UserViewSet(viewsets.ModelViewSet): @@ -12,12 +18,136 @@ class UserViewSet(viewsets.ModelViewSet): permission_classes = [permissions.IsAuthenticated] def get_serializer_class(self): - if self.action in ['retrieve', 'update', 'partial_update']: + if self.action == 'retrieve' and self.kwargs.get('pk') == 'me': return UserDetailSerializer + elif self.action in ['update', 'partial_update'] and self.kwargs.get('pk') == 'me': + return UserUpdateSerializer return UserSerializer + def get_queryset(self): + # Only admins can see all users + if self.request.user.is_admin(): + return User.objects.all() + # Regular users can only see themselves + return User.objects.filter(id=self.request.user.id) + + def list(self, request, *args, **kwargs): + """Only admins can list all users.""" + if not request.user.is_admin(): + return Response( + {'detail': 'You do not have permission to perform this action.'}, + status=status.HTTP_403_FORBIDDEN + ) + return super().list(request, *args, **kwargs) + + def retrieve(self, request, *args, **kwargs): + """Get user details (me for current user).""" + if kwargs.get('pk') == 'me': + self.kwargs['pk'] = request.user.id + return super().retrieve(request, *args, **kwargs) + + def update(self, request, *args, **kwargs): + """Update user details (only me for regular users).""" + if kwargs.get('pk') == 'me': + self.kwargs['pk'] = request.user.id + elif not request.user.is_admin() and str(kwargs.get('pk')) != str(request.user.id): + return Response( + {'detail': 'You can only update your own profile.'}, + status=status.HTTP_403_FORBIDDEN + ) + return super().update(request, *args, **kwargs) + @action(detail=False, methods=['get']) def me(self, request): - """Get current user.""" + """Get current user details.""" serializer = self.get_serializer(request.user) + return Response(serializer.data) + + @action(detail=True, methods=['get']) + def stats(self, request, pk=None): + """Get user statistics.""" + if pk == 'me': + pk = request.user.id + user = self.get_object() + + if str(user.id) != str(request.user.id) and not request.user.is_admin(): + return Response( + {'detail': 'You do not have permission to view this user\'s stats.'}, + status=status.HTTP_403_FORBIDDEN + ) + + from apps.articles.models import Article + from apps.featured_services.models import FeaturedService + from apps.interactions.models import Comment, Like, Favorite, Rating + + return Response({ + 'articles_count': Article.objects.filter(author=user).count(), + 'services_count': FeaturedService.objects.filter(submitter=user).count(), + 'comments_count': Comment.objects.filter(author=user).count(), + 'likes_count': Like.objects.filter(user=user).count(), + 'favorites_count': Favorite.objects.filter(user=user).count(), + 'ratings_count': Rating.objects.filter(user=user).count(), + }) + + @action(detail=True, methods=['get']) + def favorites(self, request, pk=None): + """Get user's favorites.""" + if pk == 'me': + pk = request.user.id + user = self.get_object() + + if str(user.id) != str(request.user.id): + return Response( + {'detail': 'You can only view your own favorites.'}, + status=status.HTTP_403_FORBIDDEN + ) + + from apps.interactions.serializers import FavoriteSerializer + favorites = Favorite.objects.filter(user=user).select_related() + serializer = FavoriteSerializer(favorites, many=True) + return Response(serializer.data) + + @action(detail=True, methods=['get']) + def ratings(self, request, pk=None): + """Get user's ratings.""" + if pk == 'me': + pk = request.user.id + user = self.get_object() + + if str(user.id) != str(request.user.id): + return Response( + {'detail': 'You can only view your own ratings.'}, + status=status.HTTP_403_FORBIDDEN + ) + + from apps.interactions.serializers import RatingSerializer + ratings = Rating.objects.filter(user=user).select_related() + serializer = RatingSerializer(ratings, many=True) + return Response(serializer.data) + + @action(detail=False, methods=['get']) + def search(self, request): + """Search users (admin only).""" + if not request.user.is_admin(): + return Response( + {'detail': 'Only admins can search users.'}, + status=status.HTTP_403_FORBIDDEN + ) + + query = request.query_params.get('q', '') + if query: + users = User.objects.filter( + Q(username__icontains=query) | + Q(email__icontains=query) | + Q(first_name__icontains=query) + ) + else: + users = User.objects.all() + + page = self.paginate_queryset(users) + if page is not None: + serializer = self.get_serializer(page, many=True) + return self.get_paginated_response(serializer.data) + + serializer = self.get_serializer(users, many=True) return Response(serializer.data) \ No newline at end of file