f213/education-backend

View on GitHub
src/apps/homework/api/viewsets.py

Summary

Maintainability
A
0 mins
Test Coverage
A
100%
from typing import Any

from django.db.models import QuerySet
from django.utils.functional import cached_property
from drf_spectacular.utils import extend_schema
from rest_framework.exceptions import MethodNotAllowed
from rest_framework.generics import get_object_or_404
from rest_framework.permissions import IsAuthenticated
from rest_framework.request import Request
from rest_framework.response import Response

from apps.homework.api.filtersets import AnswerFilterSet
from apps.homework.api.permissions import (
    MayChangeAnswerOnlyForLimitedTime,
    MayChangeAnswerOnlyWithoutDescendants,
    ShouldBeAuthorOrReadOnly,
    ShouldHavePurchasedQuestionCoursePermission,
)
from apps.homework.api.serializers import (
    AnswerCreateSerializer,
    AnswerDetailedSerializer,
    ReactionCreateSerializer,
    ReactionDetailedSerializer,
)
from apps.homework.models import Answer, AnswerAccessLogEntry
from apps.homework.models.answer import AnswerQuerySet
from apps.homework.models.reaction import Reaction
from apps.homework.services import ReactionCreator
from core.api.mixins import DisablePaginationWithQueryParamMixin
from core.viewsets import AppViewSet, CreateDeleteAppViewSet


class AnswerViewSet(DisablePaginationWithQueryParamMixin, AppViewSet):
    queryset = Answer.objects.for_viewset()
    serializer_class = AnswerDetailedSerializer
    serializer_action_classes = {
        "create": AnswerCreateSerializer,
        "partial_update": AnswerCreateSerializer,
    }

    lookup_field = "slug"
    permission_classes = [
        IsAuthenticated
        & ShouldHavePurchasedQuestionCoursePermission
        & ShouldBeAuthorOrReadOnly
        & MayChangeAnswerOnlyForLimitedTime
        & MayChangeAnswerOnlyWithoutDescendants,
    ]
    filterset_class = AnswerFilterSet

    def create(self, request: Request, *args: Any, **kwargs: dict[str, Any]) -> Response:
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        answer = serializer.save()
        answer = self.get_queryset().get(pk=answer.pk)

        Serializer = self.get_serializer_class(action="retrieve")
        return Response(Serializer(answer).data, status=201)

    def update(self, request: Request, *args: Any, **kwargs: dict[str, Any]) -> Response:
        if not kwargs.get("partial", False):
            raise MethodNotAllowed("Please use patch")

        response = super().update(request, *args, **kwargs)

        answer = self.get_object()
        answer.refresh_from_db()
        Serializer = self.get_serializer_class(action="retrieve")
        response.data = Serializer(answer).data

        return response

    def get_queryset(self) -> AnswerQuerySet:
        queryset = super().get_queryset()

        queryset = self.limit_queryset_to_user(queryset)  # type: ignore
        queryset = self.limit_queryset_for_list(queryset)

        return queryset.with_children_count().order_by("created").prefetch_reactions()

    def limit_queryset_to_user(self, queryset: AnswerQuerySet) -> AnswerQuerySet:
        if self.action != "retrieve":
            return queryset.allowed_for_user(self.request.user)  # type: ignore

        return queryset

    def limit_queryset_for_list(self, queryset: AnswerQuerySet) -> AnswerQuerySet:
        if self.action == "list":
            return queryset.root_only()

        return queryset

    def get_object(self) -> Answer:
        """Write a log entry for each answer from another user that is retrieved"""
        instance = super().get_object()

        self.write_log_entry(answer=instance)

        return instance

    def write_log_entry(self, answer: Answer) -> None:
        if not self.request.user.has_perm("homework.see_all_answers"):
            if answer.author != self.request.user:
                AnswerAccessLogEntry.objects.get_or_create(
                    user=self.request.user,
                    answer=answer,
                )


class ReactionViewSet(CreateDeleteAppViewSet):
    queryset = Reaction.objects.for_viewset()
    serializer_class = ReactionDetailedSerializer
    serializer_action_classes = {
        "create": ReactionCreateSerializer,
    }
    permission_classes = [IsAuthenticated & ShouldBeAuthorOrReadOnly]

    lookup_field = "slug"

    @extend_schema(responses=ReactionDetailedSerializer)
    def create(self, request: Request, *args: Any, **kwargs: dict[str, Any]) -> Response:
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        data = serializer.validated_data.copy()
        reaction = ReactionCreator(emoji=data.get("emoji"), slug=data.get("slug"), author=self.request.user, answer=self.answer)()  # type: ignore

        Serializer = self.get_serializer_class(action="retrieve")
        return Response(Serializer(reaction).data, status=201)

    def get_queryset(self) -> QuerySet[Reaction]:
        return super().get_queryset().filter(answer=self.answer)

    @cached_property
    def answer(self) -> Answer:
        return get_object_or_404(Answer, slug=self.kwargs.get("answer_slug"))