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):
"""Serializer for User model."""
"""Serializer for User model (basic info)."""
class Meta:
model = User
fields = ('id', 'username', 'email', 'first_name', 'last_name', 'avatar')
read_only_fields = ('id',)
fields = ('id', 'username', 'email', 'first_name', 'last_name', 'avatar', 'role', 'status')
read_only_fields = ('id', 'role', 'status')
class UserDetailSerializer(serializers.ModelSerializer):
@@ -17,4 +17,22 @@ class UserDetailSerializer(serializers.ModelSerializer):
class Meta:
model = User
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.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)

View File

@@ -6,10 +6,29 @@ from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
)
from apps.api.views import CustomTokenObtainPairView
urlpatterns = [
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')),
# GraphQL
path('graphql/', include('apps.api.graphql_urls')),
]

View File

@@ -6,14 +6,22 @@ import App from './App';
import './styles/global';
// Import stores
import UserStore from './stores/UserStore';
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 stores = {
userStore: new UserStore(),
authStore: new AuthStore(),
userStore: new UserStore(),
regionStore: new RegionStore(),
articleStore: new ArticleStore(),
serviceStore: new ServiceStore(),
interactionStore: new InteractionStore(),
};
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;