diff --git a/djangoproject/coreapp/serializers/KeywordSerializer.py b/djangoproject/coreapp/serializers/KeywordSerializer.py new file mode 100644 index 0000000..9f09570 --- /dev/null +++ b/djangoproject/coreapp/serializers/KeywordSerializer.py @@ -0,0 +1,20 @@ +from drf_writable_nested import UniqueFieldsMixin +from rest_framework import serializers + +from coreapp.models import Keyword + + +class KeywordSerializer(UniqueFieldsMixin, serializers.ModelSerializer): + class Meta: + model = Keyword + fields = ('name',) # '__all__' + + def to_representation(self, value): + """overridden to support returning of plain json array like [key1, key2]""" + return value.name + + def to_internal_value(self, data): + """overridden to also support parsing of plain json array like [key1, key2]""" + if type(data) == str: + return super().to_internal_value(data={'name': data}) + return super().to_internal_value(data) \ No newline at end of file diff --git a/djangoproject/coreapp/serializers/PostCommentSerializers.py b/djangoproject/coreapp/serializers/PostCommentSerializers.py new file mode 100644 index 0000000..e178b76 --- /dev/null +++ b/djangoproject/coreapp/serializers/PostCommentSerializers.py @@ -0,0 +1,49 @@ +from drf_yasg.utils import swagger_serializer_method +from rest_framework import serializers, exceptions +from rest_framework.utils.serializer_helpers import ReturnDict + +from accounts.serializers import UserRefSerializer +from coreapp.models import PostComment, Post +from coreapp.swagger.schema import UserRefSerializerSchema + + +class PostCommentSerializer(serializers.ModelSerializer): + user = serializers.SerializerMethodField() + + class Meta: + model = PostComment + + @swagger_serializer_method(serializer_or_field=UserRefSerializerSchema) + def get_user(self, postcomment) -> ReturnDict: + return UserRefSerializer(postcomment.user, context=self.context).data + + +class PostCommentCreateSerializer(PostCommentSerializer): + post = serializers.PrimaryKeyRelatedField(queryset=Post.approved.all(), write_only=True, required=False) + + class Meta(PostCommentSerializer.Meta): + fields = ('id', 'comment', 'created_at', 'parent', + 'user', 'post') + read_only_fields = ( + 'created_at', 'user',) + + def create(self, validated_data): + post = validated_data.get('post', None) + parent = validated_data.get('parent', None) + if post and parent: + raise exceptions.ValidationError(detail='Only one of `post` or `parent` field can be passed') + elif parent: + post_id = parent.post_id + return PostComment.objects.create(**validated_data, post_id=post_id, user=self.context['request'].user) + elif post: + return PostComment.objects.create(**validated_data, user=self.context['request'].user) + else: + raise exceptions.ValidationError(detail='Either `post` or `parent` field must be passed') + + +class PostCommentUpdateSerializer(PostCommentSerializer): + class Meta(PostCommentSerializer.Meta): + fields = ('id', 'comment', 'created_at', 'parent', + 'user') + read_only_fields = ( + 'created_at', 'user', 'parent',) \ No newline at end of file diff --git a/djangoproject/coreapp/serializers/PostReactionSerializer.py b/djangoproject/coreapp/serializers/PostReactionSerializer.py new file mode 100644 index 0000000..40df4fa --- /dev/null +++ b/djangoproject/coreapp/serializers/PostReactionSerializer.py @@ -0,0 +1,26 @@ +from rest_framework import serializers +from rest_framework_nested.relations import NestedHyperlinkedIdentityField + +from accounts.models import User +from coreapp.consts_db import Reaction +from coreapp.models import Post, PostReaction +from coreapp.serializers.serializer_fields import ChoiceField + + +class PostReactionSerializer(serializers.ModelSerializer): + user = serializers.HyperlinkedRelatedField(queryset=User.objects.all(), + view_name='api:user-detail', + default=serializers.CurrentUserDefault()) + post = serializers.HyperlinkedRelatedField(queryset=Post.approved.all(), view_name='api:post-detail', required=True) + reaction = ChoiceField(choices=Reaction.choices, required=True) + url = NestedHyperlinkedIdentityField(view_name='api:post-reaction-detail', + parent_lookup_kwargs={'post_pk': 'post_id'}, read_only=True, + label="reaction's view url") + + class Meta: + model = PostReaction + fields = ['reaction', 'user', 'post', 'url'] + + def get_unique_together_validators(self): + """disable unique together checks for (user, post) for get_or_create operation in manager.create""" + return [] \ No newline at end of file diff --git a/djangoproject/coreapp/serializers.py b/djangoproject/coreapp/serializers/PostSerializers.py similarity index 70% rename from djangoproject/coreapp/serializers.py rename to djangoproject/coreapp/serializers/PostSerializers.py index d156ed6..81974a2 100644 --- a/djangoproject/coreapp/serializers.py +++ b/djangoproject/coreapp/serializers/PostSerializers.py @@ -1,50 +1,18 @@ +from django.db import transaction +from django.db.models import Count from django.utils import timezone -from drf_writable_nested import UniqueFieldsMixin, NestedUpdateMixin +from drf_writable_nested import NestedUpdateMixin from drf_yasg.utils import swagger_serializer_method from rest_framework import serializers, exceptions from rest_framework.utils.serializer_helpers import ReturnDict -from rest_framework_nested.relations import NestedHyperlinkedIdentityField -from coreapp.models import * from accounts.serializers import UserRefSerializer -from coreapp.serializer_fields import ChoiceField, ImageBase64HybridFileField +from coreapp.consts_db import ApprovalStatus, Reaction +from coreapp.models import Post, PostReaction, Keyword +from coreapp.serializers.KeywordSerializer import KeywordSerializer +from coreapp.serializers.serializer_fields import ChoiceField, ImageBase64HybridFileField from coreapp.swagger.schema import UserRefSerializerSchema -from coreapp.swagger.serializer_fields import * - - -class KeywordSerializer(UniqueFieldsMixin, serializers.ModelSerializer): - class Meta: - model = Keyword - fields = ('name',) # '__all__' - - def to_representation(self, value): - """overridden to support returning of plain json array like [key1, key2]""" - return value.name - - def to_internal_value(self, data): - """overridden to also support parsing of plain json array like [key1, key2]""" - if type(data) == str: - return super().to_internal_value(data={'name': data}) - return super().to_internal_value(data) - - -class PostReactionSerializer(serializers.ModelSerializer): - user = serializers.HyperlinkedRelatedField(queryset=User.objects.all(), - view_name='api:user-detail', - default=serializers.CurrentUserDefault()) - post = serializers.HyperlinkedRelatedField(queryset=Post.approved.all(), view_name='api:post-detail', required=True) - reaction = ChoiceField(choices=Reaction.choices, required=True) - url = NestedHyperlinkedIdentityField(view_name='api:post-reaction-detail', - parent_lookup_kwargs={'post_pk': 'post_id'}, read_only=True, - label="reaction's view url") - - class Meta: - model = PostReaction - fields = ['reaction', 'user', 'post', 'url'] - - def get_unique_together_validators(self): - """disable unique together checks for (user, post) for get_or_create operation in manager.create""" - return [] +from coreapp.swagger.serializer_fields import Post_reaction_counts class PostSerializer(NestedUpdateMixin, serializers.ModelSerializer): @@ -179,36 +147,4 @@ def update(self, instance, validated_data): """update is overridden to set moderator and approval modification time""" instance.moderator = self.context['request'].user instance.approval_at = timezone.now() - return super().update(instance, validated_data) - - -class PostCommentSerializer(serializers.ModelSerializer): - user = serializers.SerializerMethodField() - - class Meta: - model = PostComment - - @swagger_serializer_method(serializer_or_field=UserRefSerializerSchema) - def get_user(self, postcomment) -> ReturnDict: - return UserRefSerializer(postcomment.user, context=self.context).data - - def create(self, validated_data): - return PostComment.objects.create(**validated_data, user=self.context['request'].user) - - -class PostCommentCreateSerializer(PostCommentSerializer): - post = serializers.PrimaryKeyRelatedField(queryset=Post.approved.all(), write_only=True, required=True) - - class Meta(PostCommentSerializer.Meta): - fields = ('id', 'comment', 'created_at', 'parent', - 'user', 'post') - read_only_fields = ( - 'created_at', 'user',) - - -class PostCommentUpdateSerializer(PostCommentSerializer): - class Meta(PostCommentSerializer.Meta): - fields = ('id', 'comment', 'created_at', 'parent', - 'user') - read_only_fields = ( - 'created_at', 'user', 'parent',) + return super().update(instance, validated_data) \ No newline at end of file diff --git a/djangoproject/coreapp/serializer_fields.py b/djangoproject/coreapp/serializers/serializer_fields.py similarity index 100% rename from djangoproject/coreapp/serializer_fields.py rename to djangoproject/coreapp/serializers/serializer_fields.py diff --git a/djangoproject/coreapp/swagger/serializers.py b/djangoproject/coreapp/swagger/serializers.py index 5ac6160..585eedd 100644 --- a/djangoproject/coreapp/swagger/serializers.py +++ b/djangoproject/coreapp/swagger/serializers.py @@ -3,7 +3,7 @@ from coreapp.consts_db import Reaction from coreapp.models import PostReaction -from coreapp.serializer_fields import ChoiceField +from coreapp.serializers.serializer_fields import ChoiceField class PostReactionRequestBodySerializer(serializers.ModelSerializer): diff --git a/djangoproject/coreapp/urls.py b/djangoproject/coreapp/urls.py index f2a8bd0..4637482 100644 --- a/djangoproject/coreapp/urls.py +++ b/djangoproject/coreapp/urls.py @@ -1,19 +1,25 @@ from django.urls import include, path from rest_framework_nested import routers -from coreapp import views +import coreapp.views.PostViewSet +import coreapp.views.UserViewSet +import coreapp.views.PostReactionViewSet +import coreapp.views.PostModerationViewSet +import coreapp.views.CommentViewSet +import coreapp.views.PostCommentViewSet router = routers.SimpleRouter() -router.register(r'post', views.PostViewSet, basename='post') -router.register(r'comment', views.CommentViewSet, basename='comment') -router.register(r'moderation/post', views.PostModerationViewSet, basename='moderation-post') +router.register(r'post', coreapp.views.PostViewSet.PostViewSet, basename='post') +router.register(r'comment', coreapp.views.CommentViewSet.CommentViewSet, basename='comment') +router.register(r'moderation/post', coreapp.views.PostModerationViewSet.PostModerationViewSet, + basename='moderation-post') # router.register(r'keyword', views.KeywordViewSet, basename='keyword') -router.register(r'user', views.UserViewSet, basename='user') +router.register(r'user', coreapp.views.UserViewSet.UserViewSet, basename='user') post_router = routers.NestedSimpleRouter(router, r'post', lookup='post') -post_router.register(r'reaction', views.PostReactionViewSet, basename='post-reaction') -post_router.register(r'comment', views.PostCommentViewSet, basename='post-comment') +post_router.register(r'reaction', coreapp.views.PostReactionViewSet.PostReactionViewSet, basename='post-reaction') +post_router.register(r'comment', coreapp.views.PostCommentViewSet.PostCommentViewSet, basename='post-comment') urlpatterns = [ path('', include(router.urls)), diff --git a/djangoproject/coreapp/views.py b/djangoproject/coreapp/views.py deleted file mode 100644 index 79fd0b8..0000000 --- a/djangoproject/coreapp/views.py +++ /dev/null @@ -1,256 +0,0 @@ -from django.utils.decorators import method_decorator -from drf_yasg import openapi -from drf_yasg.utils import swagger_auto_schema -from filters.mixins import FiltersMixin -from rest_framework import viewsets, status, permissions, mixins -from rest_framework.decorators import action -from rest_framework.response import Response - -from accounts.serializers import UserSerializer -from coreapp.pagination import StandardResultsSetPagination -from coreapp.permissions import IsModerator, IsAuthenticatedCreateOrOwnerModifyOrReadOnly -from coreapp.filters import * -from coreapp.serializers import * -from coreapp.swagger import query_params -from coreapp.swagger.serializers import PostReactionRequestBodySerializer, PostReactionResponseBodySerializer -from coreapp.utils import to_bool -from coreapp.validators import post_query_schema - - -@method_decorator(name='retrieve', - decorator=swagger_auto_schema(operation_summary='Details of a post', - responses={status.HTTP_404_NOT_FOUND: 'Post not found/approved'})) -@method_decorator(name='list', - decorator=swagger_auto_schema(operation_summary='List of posts', - manual_parameters=query_params.POST_LIST_QUERY_PARAMS)) -@method_decorator(name='create', - decorator=swagger_auto_schema(operation_summary='Uploads a new post', - manual_parameters=[query_params.REQUIRED_AUTHORIZATION_PARAMETER])) -@method_decorator(name='update', - decorator=swagger_auto_schema(operation_summary='Modifies a post', - manual_parameters=[query_params.REQUIRED_AUTHORIZATION_PARAMETER], - responses={status.HTTP_404_NOT_FOUND: 'Post not found'})) -class PostViewSet(FiltersMixin, viewsets.ModelViewSet): - """ - API endpoint that allows Post to be created/viewed/edited. - TODO: validation using models - TODO: check timezone - TODO: uploader=me - """ - filter_backends = (PostCategoryFilter, PostSearchFilter, filters.OrderingFilter) - filter_mappings = { - 'uploader': 'user_id__in', - 'violent': 'is_violent', - 'adult': 'is_adult', - 'keyword': 'keywordlist__keyword__name__in', - 'uploaded-before': 'uploaded_at__lt', - 'uploaded-after': 'uploaded_at__gte', - 'uploaded-on': 'uploaded_at__date', - 'template': 'template__isnull', - } - filter_value_transformations = { - 'violent': lambda val: to_bool(val), - 'adult': lambda val: to_bool(val), - 'template': lambda val: to_bool(val), - 'keyword': lambda val: filter(None, val.strip().lower().split(',')), - } - filter_validation_schema = post_query_schema - search_fields = ['caption', 'author__username', 'keywordlist__keyword__name'] - search_fields_mappings = {'caption': 'caption', - 'uploader': 'author__username', - 'keyword': 'keywordlist__keyword__name'} - search_param = 'q' - ordering_fields = ['uploaded_at', 'nviews'] - ordering = ['-uploaded_at'] - - pagination_class = StandardResultsSetPagination - serializer_class = PostSerializer - serializer_classes = { - 'pending': PostModerationSerializer, - } - - permission_classes = (IsAuthenticatedCreateOrOwnerModifyOrReadOnly,) - http_method_names = ['get', 'post', 'put'] - - def get_queryset(self): - if getattr(self, 'swagger_fake_view', False): - return Post.objects.all().first() - if self.action == 'related': - return Post.objects.get_related_posts(post_id=self.kwargs.get('pk', None)).prefetch_related('reactions', - 'user') - return Post.objects.prefetch_related('reactions', 'user').all() - - def get_serializer_class(self): - return self.serializer_classes.get(self.action, self.serializer_class) - - @swagger_auto_schema(method='get', - operation_summary='List of related posts', - operation_description="Returns list of posts made on the same template of post=pk", - responses={status.HTTP_404_NOT_FOUND: 'No such post exists with provided pk'}, - manual_parameters=query_params.POST_LIST_QUERY_PARAMS) - @action(detail=True, methods=['GET'], - url_path='related', url_name='related-posts') - def related(self, request, pk): - try: - return super().list(request, pk=pk) - except Post.DoesNotExist: - raise exceptions.NotFound(detail='No such post exists with id=%s' % pk) - - -class KeywordViewSet(viewsets.ModelViewSet): - """ - API endpoint that allows keywords to be viewed or created but not to be modified. - """ - queryset = Keyword.objects.all() - serializer_class = KeywordSerializer - permission_classes = (permissions.IsAuthenticatedOrReadOnly,) - http_method_names = ['get', 'post'] - - -@method_decorator(name='list', - decorator=swagger_auto_schema(operation_summary="List of users")) -@method_decorator(name='retrieve', - decorator=swagger_auto_schema(operation_summary="Details of a user")) -class UserViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, - viewsets.GenericViewSet): - """ - API endpoint that allows users to be viewed or edited. - """ - queryset = User.objects.all() - serializer_class = UserSerializer - pagination_class = StandardResultsSetPagination - - @swagger_auto_schema(operation_summary="Current user Profile", - manual_parameters=[query_params.REQUIRED_AUTHORIZATION_PARAMETER], - responses={status.HTTP_401_UNAUTHORIZED: 'User is not authenticated'}) - @action(detail=False, methods=['GET'], permission_classes=[permissions.IsAuthenticated], - url_path='current', url_name='current') - def current_user(self, request): - return Response(UserSerializer(request.user, context={'request': request}).data, status=status.HTTP_200_OK) - - -@method_decorator(name='list', - decorator=swagger_auto_schema(operation_summary="All reactions on a post", - operation_description="All reactions on the post with post_id=post_pk", - responses={status.HTTP_404_NOT_FOUND: 'Post not found/approved'})) -@method_decorator(name='retrieve', - decorator=swagger_auto_schema(operation_summary="Details of a reaction on a post", - operation_description='Details of a reaction with reaction_id=id' - ' on the post with post_id=post_pk', - responses={status.HTTP_404_NOT_FOUND: 'Post not found/approved'})) -class PostReactionViewSet(mixins.ListModelMixin, mixins.CreateModelMixin, mixins.RetrieveModelMixin, - viewsets.GenericViewSet): - """ - API endpoint that allows users to react or view reactions on approved posts. - """ - serializer_class = PostReactionSerializer - permission_classes = (permissions.IsAuthenticatedOrReadOnly,) - - def get_queryset(self): - qs = PostReaction.objects.all().of_approved_posts() - if 'post_pk' in self.kwargs: - qs = qs.of_post(self.kwargs['post_pk']) - return qs - - @swagger_auto_schema(method='get', operation_summary="Current user's reaction on the post", - manual_parameters=[query_params.REQUIRED_AUTHORIZATION_PARAMETER], - responses={status.HTTP_404_NOT_FOUND: 'Post/Reaction Not found'}) - @action(detail=False, methods=['GET'], permission_classes=[permissions.IsAuthenticated], - url_path='user', url_name='user') - def user(self, request, post_pk=None): - """ - currently authenticated user's reaction only on post with id=post_pk - """ - try: - post = Post.objects.get(id=post_pk, approval_status=ApprovalStatus.APPROVED) - return Response(PostReactionSerializer(PostReaction.objects.get(post=post, user=request.user), - context={'request': request}).data, - status=status.HTTP_200_OK) - except Post.DoesNotExist: - raise exceptions.NotFound(detail="No such react-able post exists with this id") - except PostReaction.DoesNotExist: - raise exceptions.NotFound(detail="No reaction on the post from this user") - - @swagger_auto_schema(request_body=PostReactionRequestBodySerializer, - operation_summary="Create/Change/Remove current user's reaction on the post by post-ID", - manual_parameters=[query_params.REQUIRED_AUTHORIZATION_PARAMETER], - responses={status.HTTP_201_CREATED: PostReactionResponseBodySerializer, - status.HTTP_401_UNAUTHORIZED: 'User not authorized', - status.HTTP_400_BAD_REQUEST: 'post/user passed in request body', - status.HTTP_404_NOT_FOUND: 'Post Not found'}) - def create(self, request, *args, **kwargs): - is_mutable = True if getattr(request.data, '_mutable', True) else request.data._mutable - if not is_mutable: - request.data._mutable = True - if getattr(request.data, 'user', None) or getattr(request.data, 'post', None): - raise exceptions.ValidationError(detail="Invalid body parameters: post/user cannot be specified") - request.data['reaction'] = str(request.data['reaction']) - request.data['post'] = reverse('api:post-detail', args=[kwargs['post_pk']]) - if not is_mutable: - request.data._mutable = False - return super().create(request, args, kwargs) - - -@method_decorator(name='retrieve', - decorator=swagger_auto_schema(operation_summary='Moderation specific details of a post', - manual_parameters=[ - query_params.REQUIRED_MODERATION_AUTHORIZATION_PARAMETER], - responses={status.HTTP_404_NOT_FOUND: 'Post not found/approved'})) -@method_decorator(name='list', - decorator=swagger_auto_schema(operation_summary='List of posts for moderation', - manual_parameters=[ - query_params.REQUIRED_MODERATION_AUTHORIZATION_PARAMETER])) -@method_decorator(name='update', - decorator=swagger_auto_schema(operation_summary='Update Post Moderation Status/Detail', - manual_parameters=[ - query_params.REQUIRED_MODERATION_AUTHORIZATION_PARAMETER], - responses={status.HTTP_404_NOT_FOUND: 'Post not found'})) -class PostModerationViewSet(mixins.ListModelMixin, mixins.UpdateModelMixin, mixins.RetrieveModelMixin, - viewsets.GenericViewSet): - """ - API endpoint to allow moderators to approve/moderate posts - """ - pagination_class = StandardResultsSetPagination - serializer_class = PostModerationSerializer - permission_classes = (IsModerator,) - queryset = Post.objects.prefetch_related('reactions', 'user', 'moderator').all() - http_method_names = ('get', 'post', 'put') - - -@method_decorator(name='retrieve', - decorator=swagger_auto_schema(operation_summary='Details of a comment', - responses={status.HTTP_404_NOT_FOUND: 'Comment not found'})) -@method_decorator(name='create', - decorator=swagger_auto_schema(operation_summary='New comment on the post', - manual_parameters=[query_params.REQUIRED_AUTHORIZATION_PARAMETER], - responses={status.HTTP_404_NOT_FOUND: 'Post not found/approved'})) -@method_decorator(name='update', - decorator=swagger_auto_schema(operation_summary='Modify an existing User Comment by owner', - manual_parameters=[query_params.REQUIRED_AUTHORIZATION_PARAMETER], - responses={status.HTTP_404_NOT_FOUND: 'Post/Comment not found'})) -class CommentViewSet(mixins.RetrieveModelMixin, mixins.CreateModelMixin, mixins.UpdateModelMixin, - viewsets.GenericViewSet): - serializer_classes = {'retrieve': PostCommentCreateSerializer, - 'create': PostCommentCreateSerializer, - 'update': PostCommentUpdateSerializer, } - permission_classes = (IsAuthenticatedCreateOrOwnerModifyOrReadOnly,) - queryset = PostComment.objects.prefetch_related('user').all() - http_method_names = ('get', 'post', 'put') - - def get_serializer_class(self): - return self.serializer_classes.get(self.action, PostCommentCreateSerializer) - - -class PostCommentViewSet(mixins.ListModelMixin, - viewsets.GenericViewSet): - """ - API endpoint to list comments on an approved posts. - """ - pagination_class = StandardResultsSetPagination - serializer_class = PostCommentCreateSerializer - - def get_queryset(self): - qs = PostComment.objects.all().of_approved_posts() - if 'post_pk' in self.kwargs: - qs = qs.of_post(self.kwargs['post_pk']) - return qs diff --git a/djangoproject/coreapp/views/CommentViewSet.py b/djangoproject/coreapp/views/CommentViewSet.py new file mode 100644 index 0000000..6b9d088 --- /dev/null +++ b/djangoproject/coreapp/views/CommentViewSet.py @@ -0,0 +1,32 @@ +from django.utils.decorators import method_decorator +from drf_yasg.utils import swagger_auto_schema +from rest_framework import status, mixins, viewsets + +from coreapp.models import PostComment +from coreapp.permissions import IsAuthenticatedCreateOrOwnerModifyOrReadOnly +from coreapp.serializers.PostCommentSerializers import PostCommentCreateSerializer, PostCommentUpdateSerializer +from coreapp.swagger import query_params + + +@method_decorator(name='retrieve', + decorator=swagger_auto_schema(operation_summary='Details of a comment', + responses={status.HTTP_404_NOT_FOUND: 'Comment not found'})) +@method_decorator(name='create', + decorator=swagger_auto_schema(operation_summary='New comment on the post', + manual_parameters=[query_params.REQUIRED_AUTHORIZATION_PARAMETER], + responses={status.HTTP_404_NOT_FOUND: 'Post not found/approved'})) +@method_decorator(name='update', + decorator=swagger_auto_schema(operation_summary='Modify an existing User Comment by owner', + manual_parameters=[query_params.REQUIRED_AUTHORIZATION_PARAMETER], + responses={status.HTTP_404_NOT_FOUND: 'Post/Comment not found'})) +class CommentViewSet(mixins.RetrieveModelMixin, mixins.CreateModelMixin, mixins.UpdateModelMixin, + viewsets.GenericViewSet): + serializer_classes = {'retrieve': PostCommentCreateSerializer, + 'create': PostCommentCreateSerializer, + 'update': PostCommentUpdateSerializer, } + permission_classes = (IsAuthenticatedCreateOrOwnerModifyOrReadOnly,) + queryset = PostComment.objects.prefetch_related('user').all() + http_method_names = ('get', 'post', 'put') + + def get_serializer_class(self): + return self.serializer_classes.get(self.action, PostCommentCreateSerializer) \ No newline at end of file diff --git a/djangoproject/coreapp/views/KeywordViewSet.py b/djangoproject/coreapp/views/KeywordViewSet.py new file mode 100644 index 0000000..99f0e41 --- /dev/null +++ b/djangoproject/coreapp/views/KeywordViewSet.py @@ -0,0 +1,14 @@ +from rest_framework import viewsets, permissions + +from coreapp.models import Keyword +from coreapp.serializers.KeywordSerializer import KeywordSerializer + + +class KeywordViewSet(viewsets.ModelViewSet): + """ + API endpoint that allows keywords to be viewed or created but not to be modified. + """ + queryset = Keyword.objects.all() + serializer_class = KeywordSerializer + permission_classes = (permissions.IsAuthenticatedOrReadOnly,) + http_method_names = ['get', 'post'] \ No newline at end of file diff --git a/djangoproject/coreapp/views/PostCommentViewSet.py b/djangoproject/coreapp/views/PostCommentViewSet.py new file mode 100644 index 0000000..b6ad263 --- /dev/null +++ b/djangoproject/coreapp/views/PostCommentViewSet.py @@ -0,0 +1,20 @@ +from rest_framework import mixins, viewsets + +from coreapp.models import PostComment +from coreapp.pagination import StandardResultsSetPagination +from coreapp.serializers.PostCommentSerializers import PostCommentCreateSerializer + + +class PostCommentViewSet(mixins.ListModelMixin, + viewsets.GenericViewSet): + """ + API endpoint to list comments on an approved posts. + """ + pagination_class = StandardResultsSetPagination + serializer_class = PostCommentCreateSerializer + + def get_queryset(self): + qs = PostComment.objects.all().of_approved_posts() + if 'post_pk' in self.kwargs: + qs = qs.of_post(self.kwargs['post_pk']) + return qs \ No newline at end of file diff --git a/djangoproject/coreapp/views/PostModerationViewSet.py b/djangoproject/coreapp/views/PostModerationViewSet.py new file mode 100644 index 0000000..046ad00 --- /dev/null +++ b/djangoproject/coreapp/views/PostModerationViewSet.py @@ -0,0 +1,35 @@ +from django.utils.decorators import method_decorator +from drf_yasg.utils import swagger_auto_schema +from rest_framework import status, mixins, viewsets + +from coreapp.models import Post +from coreapp.pagination import StandardResultsSetPagination +from coreapp.permissions import IsModerator +from coreapp.serializers.PostSerializers import PostModerationSerializer +from coreapp.swagger import query_params + + +@method_decorator(name='retrieve', + decorator=swagger_auto_schema(operation_summary='Moderation specific details of a post', + manual_parameters=[ + query_params.REQUIRED_MODERATION_AUTHORIZATION_PARAMETER], + responses={status.HTTP_404_NOT_FOUND: 'Post not found/approved'})) +@method_decorator(name='list', + decorator=swagger_auto_schema(operation_summary='List of posts for moderation', + manual_parameters=[ + query_params.REQUIRED_MODERATION_AUTHORIZATION_PARAMETER])) +@method_decorator(name='update', + decorator=swagger_auto_schema(operation_summary='Update Post Moderation Status/Detail', + manual_parameters=[ + query_params.REQUIRED_MODERATION_AUTHORIZATION_PARAMETER], + responses={status.HTTP_404_NOT_FOUND: 'Post not found'})) +class PostModerationViewSet(mixins.ListModelMixin, mixins.UpdateModelMixin, mixins.RetrieveModelMixin, + viewsets.GenericViewSet): + """ + API endpoint to allow moderators to approve/moderate posts + """ + pagination_class = StandardResultsSetPagination + serializer_class = PostModerationSerializer + permission_classes = (IsModerator,) + queryset = Post.objects.prefetch_related('reactions', 'user', 'moderator').all() + http_method_names = ('get', 'post', 'put') \ No newline at end of file diff --git a/djangoproject/coreapp/views/PostReactionViewSet.py b/djangoproject/coreapp/views/PostReactionViewSet.py new file mode 100644 index 0000000..ec448a2 --- /dev/null +++ b/djangoproject/coreapp/views/PostReactionViewSet.py @@ -0,0 +1,74 @@ +from django.urls import reverse +from django.utils.decorators import method_decorator +from drf_yasg.utils import swagger_auto_schema +from rest_framework import status, mixins, viewsets, permissions, exceptions +from rest_framework.decorators import action +from rest_framework.response import Response + +from coreapp.consts_db import ApprovalStatus +from coreapp.models import PostReaction, Post +from coreapp.serializers.PostReactionSerializer import PostReactionSerializer +from coreapp.swagger import query_params +from coreapp.swagger.serializers import PostReactionRequestBodySerializer, PostReactionResponseBodySerializer + + +@method_decorator(name='list', + decorator=swagger_auto_schema(operation_summary="All reactions on a post", + operation_description="All reactions on the post with post_id=post_pk", + responses={status.HTTP_404_NOT_FOUND: 'Post not found/approved'})) +@method_decorator(name='retrieve', + decorator=swagger_auto_schema(operation_summary="Details of a reaction on a post", + operation_description='Details of a reaction with reaction_id=id' + ' on the post with post_id=post_pk', + responses={status.HTTP_404_NOT_FOUND: 'Post not found/approved'})) +class PostReactionViewSet(mixins.ListModelMixin, mixins.CreateModelMixin, mixins.RetrieveModelMixin, + viewsets.GenericViewSet): + """ + API endpoint that allows users to react or view reactions on approved posts. + """ + serializer_class = PostReactionSerializer + permission_classes = (permissions.IsAuthenticatedOrReadOnly,) + + def get_queryset(self): + qs = PostReaction.objects.all().of_approved_posts() + if 'post_pk' in self.kwargs: + qs = qs.of_post(self.kwargs['post_pk']) + return qs + + @swagger_auto_schema(method='get', operation_summary="Current user's reaction on the post", + manual_parameters=[query_params.REQUIRED_AUTHORIZATION_PARAMETER], + responses={status.HTTP_404_NOT_FOUND: 'Post/Reaction Not found'}) + @action(detail=False, methods=['GET'], permission_classes=[permissions.IsAuthenticated], + url_path='user', url_name='user') + def user(self, request, post_pk=None): + """ + currently authenticated user's reaction only on post with id=post_pk + """ + try: + post = Post.objects.get(id=post_pk, approval_status=ApprovalStatus.APPROVED) + return Response(PostReactionSerializer(PostReaction.objects.get(post=post, user=request.user), + context={'request': request}).data, + status=status.HTTP_200_OK) + except Post.DoesNotExist: + raise exceptions.NotFound(detail="No such react-able post exists with this id") + except PostReaction.DoesNotExist: + raise exceptions.NotFound(detail="No reaction on the post from this user") + + @swagger_auto_schema(request_body=PostReactionRequestBodySerializer, + operation_summary="Create/Change/Remove current user's reaction on the post by post-ID", + manual_parameters=[query_params.REQUIRED_AUTHORIZATION_PARAMETER], + responses={status.HTTP_201_CREATED: PostReactionResponseBodySerializer, + status.HTTP_401_UNAUTHORIZED: 'User not authorized', + status.HTTP_400_BAD_REQUEST: 'post/user passed in request body', + status.HTTP_404_NOT_FOUND: 'Post Not found'}) + def create(self, request, *args, **kwargs): + is_mutable = True if getattr(request.data, '_mutable', True) else request.data._mutable + if not is_mutable: + request.data._mutable = True + if getattr(request.data, 'user', None) or getattr(request.data, 'post', None): + raise exceptions.ValidationError(detail="Invalid body parameters: post/user cannot be specified") + request.data['reaction'] = str(request.data['reaction']) + request.data['post'] = reverse('api:post-detail', args=[kwargs['post_pk']]) + if not is_mutable: + request.data._mutable = False + return super().create(request, args, kwargs) \ No newline at end of file diff --git a/djangoproject/coreapp/views/PostViewSet.py b/djangoproject/coreapp/views/PostViewSet.py new file mode 100644 index 0000000..eb065e2 --- /dev/null +++ b/djangoproject/coreapp/views/PostViewSet.py @@ -0,0 +1,94 @@ +from django.utils.decorators import method_decorator +from drf_yasg.utils import swagger_auto_schema +from filters.mixins import FiltersMixin +from rest_framework import status, viewsets, filters, exceptions +from rest_framework.decorators import action + +from coreapp.filters import PostCategoryFilter, PostSearchFilter +from coreapp.models import Post +from coreapp.pagination import StandardResultsSetPagination +from coreapp.permissions import IsAuthenticatedCreateOrOwnerModifyOrReadOnly +from coreapp.serializers.PostSerializers import PostSerializer, PostModerationSerializer +from coreapp.swagger import query_params +from coreapp.utils import to_bool +from coreapp.validators import post_query_schema + + +@method_decorator(name='retrieve', + decorator=swagger_auto_schema(operation_summary='Details of a post', + responses={status.HTTP_404_NOT_FOUND: 'Post not found/approved'})) +@method_decorator(name='list', + decorator=swagger_auto_schema(operation_summary='List of posts', + manual_parameters=query_params.POST_LIST_QUERY_PARAMS)) +@method_decorator(name='create', + decorator=swagger_auto_schema(operation_summary='Uploads a new post', + manual_parameters=[query_params.REQUIRED_AUTHORIZATION_PARAMETER])) +@method_decorator(name='update', + decorator=swagger_auto_schema(operation_summary='Modifies a post', + manual_parameters=[query_params.REQUIRED_AUTHORIZATION_PARAMETER], + responses={status.HTTP_404_NOT_FOUND: 'Post not found'})) +class PostViewSet(FiltersMixin, viewsets.ModelViewSet): + """ + API endpoint that allows Post to be created/viewed/edited. + TODO: validation using models + TODO: check timezone + TODO: uploader=me + """ + filter_backends = (PostCategoryFilter, PostSearchFilter, filters.OrderingFilter) + filter_mappings = { + 'uploader': 'user_id__in', + 'violent': 'is_violent', + 'adult': 'is_adult', + 'keyword': 'keywordlist__keyword__name__in', + 'uploaded-before': 'uploaded_at__lt', + 'uploaded-after': 'uploaded_at__gte', + 'uploaded-on': 'uploaded_at__date', + 'template': 'template__isnull', + } + filter_value_transformations = { + 'violent': lambda val: to_bool(val), + 'adult': lambda val: to_bool(val), + 'template': lambda val: to_bool(val), + 'keyword': lambda val: filter(None, val.strip().lower().split(',')), + } + filter_validation_schema = post_query_schema + search_fields = ['caption', 'author__username', 'keywordlist__keyword__name'] + search_fields_mappings = {'caption': 'caption', + 'uploader': 'author__username', + 'keyword': 'keywordlist__keyword__name'} + search_param = 'q' + ordering_fields = ['uploaded_at', 'nviews'] + ordering = ['-uploaded_at'] + + pagination_class = StandardResultsSetPagination + serializer_class = PostSerializer + serializer_classes = { + 'pending': PostModerationSerializer, + } + + permission_classes = (IsAuthenticatedCreateOrOwnerModifyOrReadOnly,) + http_method_names = ['get', 'post', 'put'] + + def get_queryset(self): + if getattr(self, 'swagger_fake_view', False): + return Post.objects.all().first() + if self.action == 'related': + return Post.objects.get_related_posts(post_id=self.kwargs.get('pk', None)).prefetch_related('reactions', + 'user') + return Post.objects.prefetch_related('reactions', 'user').all() + + def get_serializer_class(self): + return self.serializer_classes.get(self.action, self.serializer_class) + + @swagger_auto_schema(method='get', + operation_summary='List of related posts', + operation_description="Returns list of posts made on the same template of post=pk", + responses={status.HTTP_404_NOT_FOUND: 'No such post exists with provided pk'}, + manual_parameters=query_params.POST_LIST_QUERY_PARAMS) + @action(detail=True, methods=['GET'], + url_path='related', url_name='related-posts') + def related(self, request, pk): + try: + return super().list(request, pk=pk) + except Post.DoesNotExist: + raise exceptions.NotFound(detail='No such post exists with id=%s' % pk) \ No newline at end of file diff --git a/djangoproject/coreapp/views/UserViewSet.py b/djangoproject/coreapp/views/UserViewSet.py new file mode 100644 index 0000000..7b64141 --- /dev/null +++ b/djangoproject/coreapp/views/UserViewSet.py @@ -0,0 +1,32 @@ +from django.utils.decorators import method_decorator +from drf_yasg.utils import swagger_auto_schema +from rest_framework import mixins, viewsets, status, permissions +from rest_framework.decorators import action +from rest_framework.response import Response + +from accounts.models import User +from accounts.serializers import UserSerializer +from coreapp.pagination import StandardResultsSetPagination +from coreapp.swagger import query_params + + +@method_decorator(name='list', + decorator=swagger_auto_schema(operation_summary="List of users")) +@method_decorator(name='retrieve', + decorator=swagger_auto_schema(operation_summary="Details of a user")) +class UserViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, + viewsets.GenericViewSet): + """ + API endpoint that allows users to be viewed or edited. + """ + queryset = User.objects.all() + serializer_class = UserSerializer + pagination_class = StandardResultsSetPagination + + @swagger_auto_schema(operation_summary="Current user Profile", + manual_parameters=[query_params.REQUIRED_AUTHORIZATION_PARAMETER], + responses={status.HTTP_401_UNAUTHORIZED: 'User is not authenticated'}) + @action(detail=False, methods=['GET'], permission_classes=[permissions.IsAuthenticated], + url_path='current', url_name='current') + def current_user(self, request): + return Response(UserSerializer(request.user, context={'request': request}).data, status=status.HTTP_200_OK) \ No newline at end of file diff --git a/djangoproject/mememaker/settings.py b/djangoproject/mememaker/settings.py index 4df861b..601d1b1 100644 --- a/djangoproject/mememaker/settings.py +++ b/djangoproject/mememaker/settings.py @@ -159,6 +159,7 @@ CORS_ORIGIN_REGEX_WHITELIST = [ 'http://localhost:8080', ] + STATIC_URL = '/static/' MEDIA_URL = '/media/' MEDIA_ROOT = os.path.join(BASE_DIR, 'media')