Compare commits

...

4 Commits

Author SHA1 Message Date
mashen
ff96867679 feat: 添加前端 MobX Stores
- RegionStore(版块管理、查询、评分、收藏)
- ArticleStore(文章管理、提交、审核、评论、点赞、统计)
- ServiceStore(特色服务管理、提交、审核、评论、点赞、评分、统计)
- InteractionStore(交互功能:评论、评分、点赞、收藏)
- 更新 UserStore(保持原有)
- 更新 AuthStore(保持原有)
- 更新 index.js 导入所有 Stores
2026-04-09 13:45:47 +00:00
mashen
7f5cd49070 feat: 配置所有 apps 的 URL 路由
- User URLs(用户相关)
- Region URLs(版块相关)
- Article URLs(文章相关)
- FeaturedService URLs(特色服务相关)
- Moderation URLs(版主管理相关)
- Interaction URLs(交互功能相关)
- 更新主 URL 配置,整合所有 API 端点
- 添加 JWT 认证端点
2026-04-09 13:44:31 +00:00
mashen
d9c6c8ff59 feat: 添加所有 Django apps 的 ViewSets
- User ViewSet(个人中心、统计、收藏、评分、搜索)
- Region ViewSet(层级查询、树形结构、文章、服务、统计、评分、收藏)
- Article ViewSet(创建、提交、审核、评论、点赞、统计)
- FeaturedService ViewSet(创建、提交、审核、评论、点赞、评分、统计)
- Moderation ViewSets(版主申请、权限、支持、限制)
- Interaction ViewSets(评论、评分、点赞、收藏、AI审核)

完整实现权限控制、审核流程和交互功能
2026-04-09 13:44:13 +00:00
mashen
edec596516 feat: 添加所有 Django apps 的 Serializers
- User Serializers(基础、详情、更新、统计)
- Region Serializers(基础、详情、树形结构)
- Moderation Serializers(版主申请、权限、支持、限制)
- Article Serializers(基础、创建、更新、审核、列表)
- FeaturedService Serializers(基础、创建、更新、审核、列表)
- Interaction Serializers(评论、评分、点赞、收藏)
2026-04-09 13:42:02 +00:00
23 changed files with 2256 additions and 10 deletions

View File

@@ -0,0 +1,65 @@
from rest_framework import serializers
from .models import Article
class ArticleSerializer(serializers.ModelSerializer):
"""Serializer for Article model."""
article_type_display = serializers.CharField(source='get_article_type_display', read_only=True)
status_display = serializers.CharField(source='get_publish_status_display', read_only=True)
moderator_status_display = serializers.CharField(source='get_moderator_status_display', read_only=True)
ai_status_display = serializers.CharField(source='get_ai_status_display', read_only=True)
author_username = serializers.CharField(source='author.username', read_only=True)
region_name = serializers.CharField(source='region.name', read_only=True)
class Meta:
model = Article
fields = ('id', 'title', 'content', 'region', 'region_name', 'article_type', 'article_type_display',
'author', 'author_username', 'moderator_status', 'moderator_status_display',
'moderator_reviewer', 'moderator_reviewed_at', 'moderator_rejection_reason',
'ai_status', 'ai_status_display', 'ai_reviewed_at', 'ai_rejection_reason',
'publish_status', 'status_display', 'published_at', 'created_at', 'updated_at')
read_only_fields = ('id', 'author', 'moderator_reviewer', 'moderator_reviewed_at',
'ai_reviewed_at', 'published_at', 'created_at', 'updated_at')
class ArticleCreateSerializer(serializers.ModelSerializer):
"""Serializer for creating articles."""
class Meta:
model = Article
fields = ('title', 'content', 'region', 'article_type')
def create(self, validated_data):
validated_data['author'] = self.context['request'].user
return super().create(validated_data)
class ArticleUpdateSerializer(serializers.ModelSerializer):
"""Serializer for updating articles."""
class Meta:
model = Article
fields = ('title', 'content', 'article_type')
class ArticleReviewSerializer(serializers.Serializer):
"""Serializer for article review actions."""
action = serializers.ChoiceField(choices=['approve', 'reject'])
reason = serializers.CharField(required=False, allow_blank=True)
class ArticleListSerializer(serializers.ModelSerializer):
"""Simplified serializer for article list."""
article_type_display = serializers.CharField(source='get_article_type_display', read_only=True)
author_username = serializers.CharField(source='author.username', read_only=True)
region_name = serializers.CharField(source='region.name', read_only=True)
class Meta:
model = Article
fields = ('id', 'title', 'article_type', 'article_type_display',
'author', 'author_username', 'region', 'region_name',
'publish_status', 'created_at')
read_only_fields = ('id', 'created_at')

View File

@@ -0,0 +1,10 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import ArticleViewSet
router = DefaultRouter()
router.register(r'articles', ArticleViewSet, basename='article')
urlpatterns = [
path('', include(router.urls)),
]

View File

@@ -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),
})

View File

@@ -0,0 +1,66 @@
from rest_framework import serializers
from .models import FeaturedService
class FeaturedServiceSerializer(serializers.ModelSerializer):
"""Serializer for FeaturedService model."""
category_display = serializers.CharField(source='get_category_display', read_only=True)
status_display = serializers.CharField(source='get_publish_status_display', read_only=True)
moderator_status_display = serializers.CharField(source='get_moderator_status_display', read_only=True)
ai_status_display = serializers.CharField(source='get_ai_status_display', read_only=True)
submitter_username = serializers.CharField(source='submitter.username', read_only=True)
region_name = serializers.CharField(source='region.name', read_only=True)
class Meta:
model = FeaturedService
fields = ('id', 'name', 'description', 'region', 'region_name', 'category', 'category_display',
'address', 'contact', 'image', 'submitter', 'submitter_username',
'moderator_status', 'moderator_status_display', 'moderator_reviewer',
'moderator_reviewed_at', 'moderator_rejection_reason',
'ai_status', 'ai_status_display', 'ai_reviewed_at', 'ai_rejection_reason',
'publish_status', 'status_display', 'published_at', 'created_at', 'updated_at')
read_only_fields = ('id', 'submitter', 'moderator_reviewer', 'moderator_reviewed_at',
'ai_reviewed_at', 'published_at', 'created_at', 'updated_at')
class FeaturedServiceCreateSerializer(serializers.ModelSerializer):
"""Serializer for creating featured services."""
class Meta:
model = FeaturedService
fields = ('name', 'description', 'region', 'category', 'address', 'contact', 'image')
def create(self, validated_data):
validated_data['submitter'] = self.context['request'].user
return super().create(validated_data)
class FeaturedServiceUpdateSerializer(serializers.ModelSerializer):
"""Serializer for updating featured services."""
class Meta:
model = FeaturedService
fields = ('name', 'description', 'category', 'address', 'contact', 'image')
class FeaturedServiceReviewSerializer(serializers.Serializer):
"""Serializer for service review actions."""
action = serializers.ChoiceField(choices=['approve', 'reject'])
reason = serializers.CharField(required=False, allow_blank=True)
class FeaturedServiceListSerializer(serializers.ModelSerializer):
"""Simplified serializer for service list."""
category_display = serializers.CharField(source='get_category_display', read_only=True)
submitter_username = serializers.CharField(source='submitter.username', read_only=True)
region_name = serializers.CharField(source='region.name', read_only=True)
class Meta:
model = FeaturedService
fields = ('id', 'name', 'category', 'category_display', 'image',
'submitter', 'submitter_username', 'region', 'region_name',
'publish_status', 'created_at')
read_only_fields = ('id', 'created_at')

View File

@@ -0,0 +1,10 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import FeaturedServiceViewSet
router = DefaultRouter()
router.register(r'services', FeaturedServiceViewSet, basename='featured_service')
urlpatterns = [
path('', include(router.urls)),
]

View File

@@ -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(),
})

View File

@@ -0,0 +1,117 @@
from rest_framework import serializers
from .models import Comment, Rating, Like, Favorite
class CommentSerializer(serializers.ModelSerializer):
"""Serializer for Comment model."""
target_type_display = serializers.CharField(source='get_target_type_display', read_only=True)
ai_status_display = serializers.CharField(source='get_ai_status_display', read_only=True)
author_username = serializers.CharField(source='author.username', read_only=True)
class Meta:
model = Comment
fields = ('id', 'content', 'target_type', 'target_type_display', 'target_id',
'author', 'author_username', 'ai_status', 'ai_status_display',
'ai_rejection_reason', 'created_at')
read_only_fields = ('id', 'author', 'ai_status', 'ai_rejection_reason', 'created_at')
def create(self, validated_data):
validated_data['author'] = self.context['request'].user
return super().create(validated_data)
class CommentCreateSerializer(serializers.ModelSerializer):
"""Serializer for creating comments."""
class Meta:
model = Comment
fields = ('content', 'target_type', 'target_id')
def create(self, validated_data):
validated_data['author'] = self.context['request'].user
return super().create(validated_data)
class RatingSerializer(serializers.ModelSerializer):
"""Serializer for Rating model."""
target_type_display = serializers.CharField(source='get_target_type_display', read_only=True)
user_username = serializers.CharField(source='user.username', read_only=True)
class Meta:
model = Rating
fields = ('id', 'user', 'user_username', 'target_type', 'target_type_display',
'target_id', 'score', 'created_at')
read_only_fields = ('id', 'user', 'created_at')
def create(self, validated_data):
validated_data['user'] = self.context['request'].user
return super().create(validated_data)
class RatingCreateSerializer(serializers.ModelSerializer):
"""Serializer for creating/updating ratings."""
class Meta:
model = Rating
fields = ('target_type', 'target_id', 'score')
def create(self, validated_data):
validated_data['user'] = self.context['request'].user
# Check if rating already exists
Rating.objects.filter(
user=validated_data['user'],
target_type=validated_data['target_type'],
target_id=validated_data['target_id']
).delete()
return super().create(validated_data)
class LikeSerializer(serializers.ModelSerializer):
"""Serializer for Like model."""
target_type_display = serializers.CharField(source='get_target_type_display', read_only=True)
user_username = serializers.CharField(source='user.username', read_only=True)
class Meta:
model = Like
fields = ('id', 'user', 'user_username', 'target_type', 'target_type_display',
'target_id', 'created_at')
read_only_fields = ('id', 'user', 'created_at')
class FavoriteSerializer(serializers.ModelSerializer):
"""Serializer for Favorite model."""
target_type_display = serializers.CharField(source='get_target_type_display', read_only=True)
user_username = serializers.CharField(source='user.username', read_only=True)
class Meta:
model = Favorite
fields = ('id', 'user', 'user_username', 'target_type', 'target_type_display',
'target_id', 'created_at')
read_only_fields = ('id', 'user', 'created_at')
class FavoriteCreateSerializer(serializers.ModelSerializer):
"""Serializer for creating/removing favorites."""
class Meta:
model = Favorite
fields = ('target_type', 'target_id')
def create(self, validated_data):
validated_data['user'] = self.context['request'].user
# Check if favorite already exists
existing = Favorite.objects.filter(
user=validated_data['user'],
target_type=validated_data['target_type'],
target_id=validated_data['target_id']
).first()
if existing:
existing.delete()
return None # Return None to indicate removal
return super().create(validated_data)

View File

@@ -0,0 +1,18 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import (
CommentViewSet,
RatingViewSet,
LikeViewSet,
FavoriteViewSet
)
router = DefaultRouter()
router.register(r'comments', CommentViewSet, basename='comment')
router.register(r'ratings', RatingViewSet, basename='rating')
router.register(r'likes', LikeViewSet, basename='like')
router.register(r'favorites', FavoriteViewSet, basename='favorite')
urlpatterns = [
path('', include(router.urls)),
]

View File

@@ -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)

View File

@@ -0,0 +1,92 @@
from rest_framework import serializers
from .models import (
ModeratorApplication,
ModeratorPermission,
ModeratorSupport,
PermissionRestriction
)
class ModeratorApplicationSerializer(serializers.ModelSerializer):
"""Serializer for ModeratorApplication model."""
status_display = serializers.CharField(source='get_status_display', read_only=True)
rank_display = serializers.CharField(source='get_rank_display', read_only=True)
applicant_username = serializers.CharField(source='applicant.username', read_only=True)
region_name = serializers.CharField(source='region.name', read_only=True)
region_path = serializers.SerializerMethodField()
is_expired = serializers.BooleanField(read_only=True)
class Meta:
model = ModeratorApplication
fields = ('id', 'applicant', 'applicant_username', 'region', 'region_name', 'region_path',
'support_count', 'deadline', 'status', 'status_display', 'rank',
'is_expired', 'reviewed_by', 'reviewed_at', 'created_at')
read_only_fields = ('id', 'created_at', 'reviewed_by', 'reviewed_at')
def get_region_path(self, obj):
return obj.region.get_full_path()
class ModeratorApplicationCreateSerializer(serializers.ModelSerializer):
"""Serializer for creating moderator applications."""
class Meta:
model = ModeratorApplication
fields = ('region', 'rank', 'deadline')
def create(self, validated_data):
validated_data['applicant'] = self.context['request'].user
return super().create(validated_data)
class ModeratorPermissionSerializer(serializers.ModelSerializer):
"""Serializer for ModeratorPermission model."""
status_display = serializers.CharField(source='get_status_display', read_only=True)
rank_display = serializers.CharField(source='get_rank_display', read_only=True)
moderator_username = serializers.CharField(source='moderator.username', read_only=True)
region_name = serializers.CharField(source='region.name', read_only=True)
is_active = serializers.BooleanField(read_only=True)
class Meta:
model = ModeratorPermission
fields = ('id', 'moderator', 'moderator_username', 'region', 'region_name',
'rank', 'rank_display', 'status', 'status_display', 'is_active',
'restricted_until', 'created_at')
read_only_fields = ('id', 'created_at')
class ModeratorSupportSerializer(serializers.ModelSerializer):
"""Serializer for ModeratorSupport model."""
supporter_username = serializers.CharField(source='supporter.username', read_only=True)
application_region_name = serializers.CharField(source='application.region.name', read_only=True)
class Meta:
model = ModeratorSupport
fields = ('id', 'supporter', 'supporter_username', 'application',
'application_region_name', 'created_at')
read_only_fields = ('id', 'created_at')
def create(self, validated_data):
validated_data['supporter'] = self.context['request'].user
# Increment support count
application = validated_data['application']
application.support_count += 1
application.save()
return super().create(validated_data)
class PermissionRestrictionSerializer(serializers.ModelSerializer):
"""Serializer for PermissionRestriction model."""
restriction_type_display = serializers.CharField(source='get_restriction_type_display', read_only=True)
operator_username = serializers.CharField(source='operator.username', read_only=True)
target_moderator_username = serializers.CharField(source='target_moderator.username', read_only=True)
class Meta:
model = PermissionRestriction
fields = ('id', 'operator', 'operator_username', 'target_moderator', 'target_moderator_username',
'restriction_type', 'restriction_type_display', 'start_time', 'end_time', 'created_at')
read_only_fields = ('id', 'created_at')

View File

@@ -0,0 +1,16 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import (
ModeratorApplicationViewSet,
ModeratorPermissionViewSet,
PermissionRestrictionViewSet
)
router = DefaultRouter()
router.register(r'applications', ModeratorApplicationViewSet, basename='moderator_application')
router.register(r'permissions', ModeratorPermissionViewSet, basename='moderator_permission')
router.register(r'restrictions', PermissionRestrictionViewSet, basename='permission_restriction')
urlpatterns = [
path('moderator/', include(router.urls)),
]

View File

@@ -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)

View File

@@ -0,0 +1,53 @@
from rest_framework import serializers
from .models import Region
class RegionSerializer(serializers.ModelSerializer):
"""Serializer for Region model."""
level_display = serializers.CharField(source='get_level_display', read_only=True)
status_display = serializers.CharField(source='get_status_display', read_only=True)
parent_name = serializers.CharField(source='parent.name', read_only=True, allow_null=True)
children_count = serializers.SerializerMethodField()
class Meta:
model = Region
fields = ('id', 'name', 'level', 'level_display', 'parent', 'parent_name',
'status', 'status_display', 'children_count', 'created_at', 'updated_at')
read_only_fields = ('id', 'created_at', 'updated_at')
def get_children_count(self, obj):
return obj.children.count()
class RegionDetailSerializer(serializers.ModelSerializer):
"""Detailed serializer for Region model."""
level_display = serializers.CharField(source='get_level_display', read_only=True)
status_display = serializers.CharField(source='get_status_display', read_only=True)
parent = RegionSerializer(read_only=True)
children = RegionSerializer(many=True, read_only=True)
full_path = serializers.SerializerMethodField()
class Meta:
model = Region
fields = ('id', 'name', 'level', 'level_display', 'parent', 'children',
'status', 'status_display', 'full_path', 'created_at', 'updated_at')
read_only_fields = ('id', 'created_at', 'updated_at')
def get_full_path(self, obj):
return obj.get_full_path()
class RegionTreeSerializer(serializers.ModelSerializer):
"""Serializer for Region tree structure."""
children = serializers.SerializerMethodField()
class Meta:
model = Region
fields = ('id', 'name', 'level', 'status', 'children')
def get_children(self, obj):
children = obj.get_children()
return RegionTreeSerializer(children, many=True).data

View File

@@ -0,0 +1,10 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import RegionViewSet
router = DefaultRouter()
router.register(r'regions', RegionViewSet, basename='region')
urlpatterns = [
path('', include(router.urls)),
]

View File

@@ -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)

View File

@@ -3,12 +3,12 @@ from .models import User
class UserSerializer(serializers.ModelSerializer): class UserSerializer(serializers.ModelSerializer):
"""Serializer for User model.""" """Serializer for User model (basic info)."""
class Meta: class Meta:
model = User model = User
fields = ('id', 'username', 'email', 'first_name', 'last_name', 'avatar') fields = ('id', 'username', 'email', 'first_name', 'last_name', 'avatar', 'role', 'status')
read_only_fields = ('id',) read_only_fields = ('id', 'role', 'status')
class UserDetailSerializer(serializers.ModelSerializer): class UserDetailSerializer(serializers.ModelSerializer):
@@ -18,3 +18,21 @@ class UserDetailSerializer(serializers.ModelSerializer):
model = User model = User
fields = '__all__' fields = '__all__'
read_only_fields = ('id', 'date_joined', 'last_login') read_only_fields = ('id', 'date_joined', 'last_login')
class UserUpdateSerializer(serializers.ModelSerializer):
"""Serializer for updating user profile."""
class Meta:
model = User
fields = ('first_name', 'last_name', 'avatar')
class UserStatsSerializer(serializers.Serializer):
"""Serializer for user statistics."""
articles_count = serializers.IntegerField()
services_count = serializers.IntegerField()
comments_count = serializers.IntegerField()
likes_count = serializers.IntegerField()
favorites_count = serializers.IntegerField()
ratings_count = serializers.IntegerField()

View File

@@ -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.decorators import action
from rest_framework.response import Response from rest_framework.response import Response
from django.db.models import Count, Q
from .models import User from .models import User
from .serializers import UserSerializer, UserDetailSerializer from .serializers import (
UserSerializer,
UserDetailSerializer,
UserUpdateSerializer,
UserStatsSerializer
)
class UserViewSet(viewsets.ModelViewSet): class UserViewSet(viewsets.ModelViewSet):
@@ -12,12 +18,136 @@ class UserViewSet(viewsets.ModelViewSet):
permission_classes = [permissions.IsAuthenticated] permission_classes = [permissions.IsAuthenticated]
def get_serializer_class(self): 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 return UserDetailSerializer
elif self.action in ['update', 'partial_update'] and self.kwargs.get('pk') == 'me':
return UserUpdateSerializer
return UserSerializer 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']) @action(detail=False, methods=['get'])
def me(self, request): def me(self, request):
"""Get current user.""" """Get current user details."""
serializer = self.get_serializer(request.user) serializer = self.get_serializer(request.user)
return Response(serializer.data) 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)

View File

@@ -6,10 +6,29 @@ from django.contrib import admin
from django.urls import path, include from django.urls import path, include
from django.conf import settings from django.conf import settings
from django.conf.urls.static import static from django.conf.urls.static import static
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
)
from apps.api.views import CustomTokenObtainPairView
urlpatterns = [ urlpatterns = [
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
# Authentication
path('api/auth/login/', CustomTokenObtainPairView.as_view(), name='token_obtain_pair'),
path('api/auth/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
# API endpoints
path('api/', include('apps.users.urls')),
path('api/', include('apps.regions.urls')),
path('api/', include('apps.articles.urls')),
path('api/', include('apps.featured_services.urls')),
path('api/', include('apps.moderation.urls')),
path('api/', include('apps.interactions.urls')),
path('api/', include('apps.api.urls')), path('api/', include('apps.api.urls')),
# GraphQL
path('graphql/', include('apps.api.graphql_urls')), path('graphql/', include('apps.api.graphql_urls')),
] ]

View File

@@ -6,14 +6,22 @@ import App from './App';
import './styles/global'; import './styles/global';
// Import stores // Import stores
import UserStore from './stores/UserStore';
import AuthStore from './stores/AuthStore'; import AuthStore from './stores/AuthStore';
import UserStore from './stores/UserStore';
import RegionStore from './stores/RegionStore';
import ArticleStore from './stores/ArticleStore';
import ServiceStore from './stores/ServiceStore';
import InteractionStore from './stores/InteractionStore';
const root = ReactDOM.createRoot(document.getElementById('root')); const root = ReactDOM.createRoot(document.getElementById('root'));
const stores = { const stores = {
userStore: new UserStore(),
authStore: new AuthStore(), authStore: new AuthStore(),
userStore: new UserStore(),
regionStore: new RegionStore(),
articleStore: new ArticleStore(),
serviceStore: new ServiceStore(),
interactionStore: new InteractionStore(),
}; };
root.render( root.render(

View File

@@ -0,0 +1,152 @@
import { makeAutoObservable } from 'mobx';
import api from '../services/api';
class ArticleStore {
articles = [];
currentArticle = null;
loading = false;
error = null;
constructor() {
makeAutoObservable(this);
}
async fetchArticles(params = {}) {
this.loading = true;
this.error = null;
try {
const response = await api.get('/api/articles/', { params });
this.articles = response.data.results || response.data;
} catch (error) {
this.error = error.response?.data || 'Failed to fetch articles';
} finally {
this.loading = false;
}
}
async fetchArticle(id) {
this.loading = true;
this.error = null;
try {
const response = await api.get(`/api/articles/${id}/`);
this.currentArticle = response.data;
} catch (error) {
this.error = error.response?.data || 'Failed to fetch article';
} finally {
this.loading = false;
}
}
async createArticle(data) {
this.loading = true;
this.error = null;
try {
const response = await api.post('/api/articles/', data);
return { success: true, article: response.data };
} catch (error) {
this.error = error.response?.data || 'Failed to create article';
return { success: false, error: this.error };
} finally {
this.loading = false;
}
}
async updateArticle(id, data) {
this.loading = true;
this.error = null;
try {
const response = await api.put(`/api/articles/${id}/`, data);
return { success: true, article: response.data };
} catch (error) {
this.error = error.response?.data || 'Failed to update article';
return { success: false, error: this.error };
} finally {
this.loading = false;
}
}
async deleteArticle(id) {
try {
await api.delete(`/api/articles/${id}/`);
return { success: true };
} catch (error) {
return {
success: false,
error: error.response?.data || 'Failed to delete article',
};
}
}
async submitArticle(id) {
try {
await api.post(`/api/articles/${id}/submit/`);
return { success: true };
} catch (error) {
return {
success: false,
error: error.response?.data || 'Failed to submit article',
};
}
}
async approveArticle(id, reason = '') {
try {
await api.post(`/api/articles/${id}/approve/`, { action: 'approve', reason });
return { success: true };
} catch (error) {
return {
success: false,
error: error.response?.data || 'Failed to approve article',
};
}
}
async rejectArticle(id, reason) {
try {
await api.post(`/api/articles/${id}/reject/`, { action: 'reject', reason });
return { success: true };
} catch (error) {
return {
success: false,
error: error.response?.data || 'Failed to reject article',
};
}
}
async likeArticle(id) {
try {
const response = await api.post(`/api/articles/${id}/like/`);
return response.data;
} catch (error) {
return null;
}
}
async fetchArticleComments(id) {
try {
const response = await api.get(`/api/articles/${id}/comments/`);
return response.data;
} catch (error) {
return [];
}
}
async fetchArticleStats(id) {
try {
const response = await api.get(`/api/articles/${id}/stats/`);
return response.data;
} catch (error) {
return null;
}
}
clearCurrentArticle() {
this.currentArticle = null;
}
}
export default ArticleStore;

View File

@@ -0,0 +1,164 @@
import { makeAutoObservable } from 'mobx';
import api from '../services/api';
class InteractionStore {
comments = [];
ratings = [];
likes = [];
favorites = [];
loading = false;
error = null;
constructor() {
makeAutoObservable(this);
}
// Comments
async createComment(targetType, targetId, content) {
try {
const response = await api.post('/api/comments/', {
target_type: targetType,
target_id: targetId,
content,
});
return { success: true, comment: response.data };
} catch (error) {
return {
success: false,
error: error.response?.data || 'Failed to create comment',
};
}
}
async fetchComments(targetType, targetId) {
try {
const response = await api.get('/api/comments/', {
params: { target_type: targetType, target_id: targetId },
});
this.comments = response.data.results || response.data;
} catch (error) {
this.error = error.response?.data || 'Failed to fetch comments';
}
}
async approveComment(commentId) {
try {
await api.post(`/api/comments/${commentId}/approve_ai/`);
return { success: true };
} catch (error) {
return {
success: false,
error: error.response?.data || 'Failed to approve comment',
};
}
}
async rejectComment(commentId, reason) {
try {
await api.post(`/api/comments/${commentId}/reject_ai/`, { reason });
return { success: true };
} catch (error) {
return {
success: false,
error: error.response?.data || 'Failed to reject comment',
};
}
}
// Ratings
async createRating(targetType, targetId, score) {
try {
const response = await api.post('/api/ratings/', {
target_type: targetType,
target_id: targetId,
score,
});
return { success: true, rating: response.data };
} catch (error) {
return {
success: false,
error: error.response?.data || 'Failed to create rating',
};
}
}
async fetchRatings(params = {}) {
try {
const response = await api.get('/api/ratings/', { params });
this.ratings = response.data.results || response.data;
} catch (error) {
this.error = error.response?.data || 'Failed to fetch ratings';
}
}
async fetchMyRatings() {
try {
const response = await api.get('/api/ratings/my_ratings/');
this.ratings = response.data;
} catch (error) {
this.error = error.response?.data || 'Failed to fetch my ratings';
}
}
// Likes
async toggleLike(targetType, targetId) {
try {
const response = await api.post('/api/likes/toggle/', {
target_type: targetType,
target_id: targetId,
});
return response.data;
} catch (error) {
return null;
}
}
async fetchMyLikes() {
try {
const response = await api.get('/api/likes/my_likes/');
this.likes = response.data;
} catch (error) {
this.error = error.response?.data || 'Failed to fetch my likes';
}
}
// Favorites
async toggleFavorite(targetType, targetId) {
try {
const response = await api.post('/api/favorites/toggle/', {
target_type: targetType,
target_id: targetId,
});
return response.data;
} catch (error) {
return null;
}
}
async fetchMyFavorites() {
try {
const response = await api.get('/api/favorites/my_favorites/');
this.favorites = response.data;
} catch (error) {
this.error = error.response?.data || 'Failed to fetch my favorites';
}
}
clearComments() {
this.comments = [];
}
clearRatings() {
this.ratings = [];
}
clearLikes() {
this.likes = [];
}
clearFavorites() {
this.favorites = [];
}
}
export default InteractionStore;

View File

@@ -0,0 +1,139 @@
import { makeAutoObservable } from 'mobx';
import api from '../services/api';
class RegionStore {
regions = [];
currentRegion = null;
loading = false;
error = null;
constructor() {
makeAutoObservable(this);
}
async fetchRegions() {
this.loading = true;
this.error = null;
try {
const response = await api.get('/api/regions/');
this.regions = response.data;
} catch (error) {
this.error = error.response?.data || 'Failed to fetch regions';
} finally {
this.loading = false;
}
}
async fetchRegion(id) {
this.loading = true;
this.error = null;
try {
const response = await api.get(`/api/regions/${id}/`);
this.currentRegion = response.data;
} catch (error) {
this.error = error.response?.data || 'Failed to fetch region';
} finally {
this.loading = false;
}
}
async fetchProvinces() {
this.loading = true;
this.error = null;
try {
const response = await api.get('/api/regions/provinces/');
this.regions = response.data;
} catch (error) {
this.error = error.response?.data || 'Failed to fetch provinces';
} finally {
this.loading = false;
}
}
async fetchChildren(parentId) {
this.loading = true;
this.error = null;
try {
const response = await api.get(`/api/regions/${parentId}/children/`);
return response.data;
} catch (error) {
this.error = error.response?.data || 'Failed to fetch children';
return [];
} finally {
this.loading = false;
}
}
async fetchRegionArticles(regionId) {
this.loading = true;
this.error = null;
try {
const response = await api.get(`/api/regions/${regionId}/articles/`);
return response.data;
} catch (error) {
this.error = error.response?.data || 'Failed to fetch region articles';
return [];
} finally {
this.loading = false;
}
}
async fetchRegionServices(regionId) {
this.loading = true;
this.error = null;
try {
const response = await api.get(`/api/regions/${regionId}/services/`);
return response.data;
} catch (error) {
this.error = error.response?.data || 'Failed to fetch region services';
return [];
} finally {
this.loading = false;
}
}
async rateRegion(regionId, score) {
try {
await api.post(`/api/regions/${regionId}/rate/`, { score });
return { success: true };
} catch (error) {
return {
success: false,
error: error.response?.data || 'Failed to rate region',
};
}
}
async getRegionRating(regionId) {
try {
const response = await api.get(`/api/regions/${regionId}/my_rating/`);
return response.data.score;
} catch (error) {
return null;
}
}
async favoriteRegion(regionId) {
try {
await api.post(`/api/regions/${regionId}/favorite/`);
return { success: true };
} catch (error) {
return {
success: false,
error: error.response?.data || 'Failed to favorite region',
};
}
}
clearCurrentRegion() {
this.currentRegion = null;
}
}
export default RegionStore;

View File

@@ -0,0 +1,164 @@
import { makeAutoObservable } from 'mobx';
import api from '../services/api';
class ServiceStore {
services = [];
currentService = null;
loading = false;
error = null;
constructor() {
makeAutoObservable(this);
}
async fetchServices(params = {}) {
this.loading = true;
this.error = null;
try {
const response = await api.get('/api/services/', { params });
this.services = response.data.results || response.data;
} catch (error) {
this.error = error.response?.data || 'Failed to fetch services';
} finally {
this.loading = false;
}
}
async fetchService(id) {
this.loading = true;
this.error = null;
try {
const response = await api.get(`/api/services/${id}/`);
this.currentService = response.data;
} catch (error) {
this.error = error.response?.data || 'Failed to fetch service';
} finally {
this.loading = false;
}
}
async createService(data) {
this.loading = true;
this.error = null;
try {
const response = await api.post('/api/services/', data);
return { success: true, service: response.data };
} catch (error) {
this.error = error.response?.data || 'Failed to create service';
return { success: false, error: this.error };
} finally {
this.loading = false;
}
}
async updateService(id, data) {
this.loading = true;
this.error = null;
try {
const response = await api.put(`/api/services/${id}/`, data);
return { success: true, service: response.data };
} catch (error) {
this.error = error.response?.data || 'Failed to update service';
return { success: false, error: this.error };
} finally {
this.loading = false;
}
}
async deleteService(id) {
try {
await api.delete(`/api/services/${id}/`);
return { success: true };
} catch (error) {
return {
success: false,
error: error.response?.data || 'Failed to delete service',
};
}
}
async submitService(id) {
try {
await api.post(`/api/services/${id}/submit/`);
return { success: true };
} catch (error) {
return {
success: false,
error: error.response?.data || 'Failed to submit service',
};
}
}
async approveService(id, reason = '') {
try {
await api.post(`/api/services/${id}/approve/`, { action: 'approve', reason });
return { success: true };
} catch (error) {
return {
success: false,
error: error.response?.data || 'Failed to approve service',
};
}
}
async rejectService(id, reason) {
try {
await api.post(`/api/services/${id}/reject/`, { action: 'reject', reason });
return { success: true };
} catch (error) {
return {
success: false,
error: error.response?.data || 'Failed to reject service',
};
}
}
async likeService(id) {
try {
const response = await api.post(`/api/services/${id}/like/`);
return response.data;
} catch (error) {
return null;
}
}
async rateService(id, score) {
try {
await api.post(`/api/services/${id}/rate/`, { score });
return { success: true };
} catch (error) {
return {
success: false,
error: error.response?.data || 'Failed to rate service',
};
}
}
async fetchServiceComments(id) {
try {
const response = await api.get(`/api/services/${id}/comments/`);
return response.data;
} catch (error) {
return [];
}
}
async fetchServiceStats(id) {
try {
const response = await api.get(`/api/services/${id}/stats/`);
return response.data;
} catch (error) {
return null;
}
}
clearCurrentService() {
this.currentService = null;
}
}
export default ServiceStore;