From 0935cfcb1f88ce3821cf432ecae57392bfa5d1e8 Mon Sep 17 00:00:00 2001 From: Fazliddin Abdurahimov Date: Wed, 6 Aug 2025 15:13:30 +0500 Subject: [PATCH] add: BaseApiViewMixin created, TODO: switch external endpoints to GenericAPIView and BaseApiViewMixin --- README.MD | 9 ++- core/apps/companies/urls.py | 23 ++++-- core/apps/companies/views/accounts.py | 14 ++-- core/apps/companies/views/companies.py | 88 +++++++++++------------ core/apps/companies/views/folders.py | 46 +++++++----- core/apps/contracts/urls.py | 2 +- core/apps/contracts/views/owners.py | 96 ++++++++++++-------------- core/utils/views.py | 93 +++++++++++++++++++++++++ 8 files changed, 237 insertions(+), 134 deletions(-) create mode 100644 core/utils/views.py diff --git a/README.MD b/README.MD index aabe50e..6319944 100644 --- a/README.MD +++ b/README.MD @@ -60,8 +60,8 @@ GET /companies//contracts # user # partial - - folder: uuid | None - - status: list[str] -GET /companies//folders # user #! not working -POST /companies//folders # user #! not working +GET /companies//folders # user # ok +POST /companies//folders # user # ok GET /companies//accounts # user # ok POST /companies//accounts # user #! TODO @@ -116,10 +116,9 @@ PATCH /contract-owners/ # admin # ok GET /contract-owners//contract # user # ok | full contract data return -POST /contract-owners//files # user # ok +POST /contract-owners//files # user #! not ok +GET /contract-owners//files # user #! not ok | full data return DELETE /contract-owners//files/ # user # ok - -GET /contract-owners//files # user # not ok | full data return POST /contract-owners//files//upload # user # ok diff --git a/core/apps/companies/urls.py b/core/apps/companies/urls.py index f09864f..29570a7 100644 --- a/core/apps/companies/urls.py +++ b/core/apps/companies/urls.py @@ -5,14 +5,23 @@ from . import views router = DefaultRouter() -router.register(r"company-accounts", views.CompanyAccountView, "company-account") # type: ignore -router.register(r"company-folders", views.CompanyFolderView, "company-folders") # type: ignore -router.register(r"companies", views.CompanyView, "companies") # type: ignore -router.register(r"companies", views.CompanyFolderViewSet, "companies-folders") # type: ignore -router.register(r"companies", views.CompanyAccountViewSet, "companies-accounts") # type: ignore -router.register(r"companies", views.CompanyContractViewSet, "companies-contracts") # type: ignore +router.register(r"company-accounts", views.CompanyAccountCrudViewSet, "company-account-view-set") # type: ignore +router.register(r"company-folders", views.CompanyFolderCrudViewSet, "company-folders-view-set") # type: ignore +# router.register(r"company-folders", views.ContractFolderApiView, "folders-contracts-view-set") # type: ignore +router.register(r"companies", views.CompanyCrudViewSet, "companies-view-set") # type: ignore +# router.register(r"companies", views.CompanyAccountView, "companies-accounts-view") # type: ignore -urlpatterns = [ +urlpatterns: list[object] = [ path("", include(router.urls)), # type: ignore + path( + r"companies//folders", + views.CompanyFolderApiView.as_view(), + name="company-folders-api-view" + ), + path( + r"companies//contracts", + views.CompanyContractApiView.as_view(), + name="company-contracts" + ) ] diff --git a/core/apps/companies/views/accounts.py b/core/apps/companies/views/accounts.py index 76287c8..3b62a2d 100644 --- a/core/apps/companies/views/accounts.py +++ b/core/apps/companies/views/accounts.py @@ -1,7 +1,7 @@ -from django_core.mixins import BaseViewSetMixin +from django_core.mixins import BaseViewSetMixin # type: ignore from drf_spectacular.utils import extend_schema -from rest_framework.permissions import IsAdminUser, AllowAny -from rest_framework.viewsets import ModelViewSet +from rest_framework.permissions import IsAdminUser, AllowAny # type: ignore +from rest_framework.viewsets import ModelViewSet # type: ignore from core.apps.companies.models import CompanyAccountModel from core.apps.companies.serializers.accounts import ( @@ -12,9 +12,11 @@ from core.apps.companies.serializers.accounts import ( DestroyCompanyAccountSerializer, ) - +###################################################################### +# Crud +###################################################################### @extend_schema(tags=["CompanyAccount"]) -class CompanyAccountView(BaseViewSetMixin, ModelViewSet): +class CompanyAccountCrudViewSet(BaseViewSetMixin, ModelViewSet): queryset = CompanyAccountModel.objects.all() serializer_class = ListCompanyAccountSerializer permission_classes = [AllowAny] @@ -26,7 +28,7 @@ class CompanyAccountView(BaseViewSetMixin, ModelViewSet): "update": [IsAdminUser], "destroy": [IsAdminUser], } - action_serializer_class = { + action_serializer_class = { # type: ignore "list": ListCompanyAccountSerializer, "retrieve": RetrieveCompanyAccountSerializer, "create": CreateCompanyAccountSerializer, diff --git a/core/apps/companies/views/companies.py b/core/apps/companies/views/companies.py index e8d8ce3..4734450 100644 --- a/core/apps/companies/views/companies.py +++ b/core/apps/companies/views/companies.py @@ -1,5 +1,4 @@ -import uuid - +from typing import Any from django_core.mixins import BaseViewSetMixin # type: ignore from django.contrib.auth import get_user_model @@ -8,10 +7,13 @@ from drf_spectacular.utils import extend_schema from rest_framework.decorators import action # type: ignore from rest_framework.permissions import AllowAny, IsAdminUser # type: ignore from rest_framework.viewsets import ModelViewSet, GenericViewSet # type: ignore +from rest_framework.generics import GenericAPIView # type: ignore from rest_framework.request import HttpRequest # type: ignore from rest_framework.response import Response # type: ignore from rest_framework import status # type: ignore +from rest_framework.generics import get_object_or_404 # type: ignore +from core.utils.views import BaseApiViewMixin from core.apps.companies.permissions import IsCompanyAccount from core.apps.companies.models import ( CompanyModel, @@ -26,7 +28,6 @@ from core.apps.companies.serializers import ( DestroyCompanySerializer, RetrieveCompanyAccountSerializer, - BaseCompanyAccountSerializer, RetrieveCompanyFolderSerializer, CreateFolderForCompanySerializer, @@ -44,10 +45,10 @@ UserModel = get_user_model() ###################################################################### -# View Only For Company CRUD That Belongs To Admin. +# Crud ###################################################################### @extend_schema(tags=["Company"]) -class CompanyView(BaseViewSetMixin, ModelViewSet): +class CompanyCrudViewSet(BaseViewSetMixin, ModelViewSet): queryset = CompanyModel.objects.all() serializer_class = ListCompanySerializer permission_classes = [AllowAny] @@ -71,16 +72,16 @@ class CompanyView(BaseViewSetMixin, ModelViewSet): ###################################################################### # company//contract Views ###################################################################### -class CompanyContractViewSet(BaseViewSetMixin, GenericViewSet): +class CompanyContractApiView(BaseApiViewMixin, GenericAPIView): # type: ignore queryset = CompanyModel.objects.all() - permission_classes = [AllowAny] + permission_classes = [IsCompanyAccount] serializer_class = BaseContractSerializer - action_permission_classes = { - "list_contract": [IsCompanyAccount] + method_permission_classes = { + "get": [IsCompanyAccount], } - action_serializer_class = { - "list_contract": RetrieveContractSerializer + method_serializer_class = { + "get": RetrieveContractSerializer, } #! TODO: status should be added. @@ -88,8 +89,7 @@ class CompanyContractViewSet(BaseViewSetMixin, GenericViewSet): summary="Company Contracts", description="Get List Company Contracts" ) - @action(methods=["GET"], detail=True, url_path="contracts") - def list_contract( + def get( self, request: HttpRequest, *args: object, @@ -112,18 +112,20 @@ class CompanyContractViewSet(BaseViewSetMixin, GenericViewSet): ###################################################################### # company//accounts Views ###################################################################### -class CompanyAccountViewSet(BaseViewSetMixin, GenericViewSet): +class CompanyAccountView(GenericAPIView): queryset = CompanyModel.objects.all() - serializer_class = BaseCompanyAccountSerializer - permission_classes = [AllowAny] + serializer_class = None + permission_classes = [IsCompanyAccount] - action_permission_classes = { - "list_account": [IsCompanyAccount] - } action_serializer_class = { "list_account": RetrieveCompanyAccountSerializer } + def get_serializer_class(self): + if self.request.method == "GET": + return RetrieveCompanyAccountSerializer + return RetrieveCompanyFolderSerializer + @extend_schema( summary="List company accounts", description="List Company Accounts" @@ -144,36 +146,26 @@ class CompanyAccountViewSet(BaseViewSetMixin, GenericViewSet): ###################################################################### # company//folders Views ###################################################################### -class CompanyFolderViewSet(BaseViewSetMixin, GenericViewSet): +class CompanyFolderApiView(GenericAPIView): queryset = CompanyModel.objects.all() - permission_classes = [AllowAny] + permission_classes = [IsCompanyAccount] - action_permission_classes = { - "list_folder": [IsCompanyAccount], - "create_folder": [IsCompanyAccount], - } - action_serializer_class = { # type: ignore - "list_folder": RetrieveCompanyFolderSerializer, - "create_folder": CreateFolderForCompanySerializer, - } + def get_serializer_class(self): # type: ignore + if self.request.method == "POST": + return CreateFolderForCompanySerializer + return RetrieveCompanyFolderSerializer - @extend_schema( - summary="List Company Folders", - description="List Company Folders" - ) - @action(methods=["GET"], detail=True, url_path="folders") - def list_folder(self, pk: uuid.UUID, *args: object, **kwargs: object) -> Response: - folders = CompanyFolderModel.objects.filter(company__id=pk) - ser = self.get_serializer(instance=folders, many=True) # type: ignore - return Response(data=ser.data, status=status.HTTP_200_OK) - - @extend_schema( - summary="Create Folder for company", - description="Create Folder for company", - ) - @action(url_path="folders", detail=True, methods=["POST"]) - def create_folder(self, request: HttpRequest, *args: object, **kwargs: object) -> Response: + @extend_schema(summary="List Company Folders") + def get(self, request: HttpRequest, *args: object, **kwargs: object) -> Response: company = self.get_object() - ser = self.get_serializer(data=data, context={"company": company}) # type: ignore - ser.is_valid(raise_exception=True) - return Response(data=ser.data, status=status.HTTP_201_CREATED) + folders = CompanyFolderModel.objects.filter(company=company) + serializer = self.get_serializer(instance=folders, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + + @extend_schema(summary="Create Folder for Company") + def post(self, request: HttpRequest, *args: object, **kwargs: object): + company = self.get_object() + serializer = self.get_serializer(data=request.data, context={"company_id": company.pk}) + serializer.is_valid(raise_exception=True) + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) diff --git a/core/apps/companies/views/folders.py b/core/apps/companies/views/folders.py index 975ecfb..086c916 100644 --- a/core/apps/companies/views/folders.py +++ b/core/apps/companies/views/folders.py @@ -1,10 +1,12 @@ from typing import cast from django_core.mixins import BaseViewSetMixin # type: ignore +from django.db import transaction from drf_spectacular.utils import extend_schema from rest_framework.decorators import action # type: ignore from rest_framework.permissions import AllowAny, IsAdminUser # type: ignore -from rest_framework.viewsets import ModelViewSet # type: ignore +from rest_framework.viewsets import ModelViewSet, GenericViewSet # type: ignore +from rest_framework.generics import GenericAPIView # type: ignore from rest_framework.request import HttpRequest # type: ignore from rest_framework.response import Response # type: ignore from rest_framework import status # type: ignore @@ -21,10 +23,12 @@ from core.apps.companies.serializers.folders import ( ) +###################################################################### +# Crud +###################################################################### @extend_schema(tags=["CompanyFolder"]) -class CompanyFolderView(BaseViewSetMixin, ModelViewSet): +class CompanyFolderCrudViewSet(BaseViewSetMixin, ModelViewSet): queryset = CompanyFolderModel.objects.all() - serializer_class = ListCompanyFolderSerializer permission_classes = [AllowAny] action_permission_classes = { # type: ignore @@ -33,7 +37,6 @@ class CompanyFolderView(BaseViewSetMixin, ModelViewSet): "create": [IsAdminUser], "update": [IsAdminUser], "destroy": [IsAdminUser], - "create_contract": [IsFolderOwner] } action_serializer_class = { # type: ignore "list": ListCompanyFolderSerializer, @@ -41,24 +44,33 @@ class CompanyFolderView(BaseViewSetMixin, ModelViewSet): "create": CreateCompanyFolderSerializer, "update": UpdateCompanyFolderSerializer, "destroy": DestroyCompanyFolderSerializer, - "create_contract": CreateContractSerializer, } + +###################################################################### +# /contract-folders//contracts +###################################################################### +class ContractFolderApiView(GenericAPIView): + queryset = CompanyFolderModel.objects.all() + permission_classes = [AllowAny] + serializer_class = None + + def get_serializer_class(self): # type: ignore + if self.request.method == "POST": + return CreateContractSerializer + return self.serializer_class + @extend_schema( summary="Create Contract For Folder", description="Create Contract For Folder", ) @action(methods=["POST"], detail=True, url_path="contracts") - def create_contract( - self, - request: HttpRequest, - *args: object, - **kwargs: object - ) -> Response: - ser = cast( - CreateContractSerializer, - self.get_serializer(data=request.data) # type: ignore - ) - ser.is_valid(raise_exception=True) - ser.save() + def create_contract(self, request: HttpRequest, *args: object, **kwargs: object) -> Response: + with transaction.atomic(): + folder = cast(CompanyFolderModel, self.get_object()) + ser = cast(CreateContractSerializer, self.get_serializer(data=request.data)) # type: ignore + ser.is_valid(raise_exception=True) + contract = ser.save() + folder.contracts.add(contract) return Response(ser.data, status.HTTP_201_CREATED) + \ No newline at end of file diff --git a/core/apps/contracts/urls.py b/core/apps/contracts/urls.py index cec9cf8..17e8841 100644 --- a/core/apps/contracts/urls.py +++ b/core/apps/contracts/urls.py @@ -10,7 +10,7 @@ router.register(r"contracts", views.ContractView, "contracts") # type: ignore router.register(r"contracts", views.ContractRelationsViewSet, "contract-relations") # type: ignore router.register(r"contract-file-contents", views.ContractFileContentView, "contract-file-contents") # type: ignore router.register(r"contract-owners", views.ContractOwnerView, "contract-owners") # type: ignore -router.register(r"contract-owners", views.ContractOwnerFileViewSet, "contract-owner-files") # type: ignore +# router.register(r"contract-owners", views.CompanyFolderCrudViewSet, "contract-owner-files") # type: ignore urlpatterns = [ # type: ignore path("", include(router.urls)), # type: ignore diff --git a/core/apps/contracts/views/owners.py b/core/apps/contracts/views/owners.py index c792dc1..e5767ca 100644 --- a/core/apps/contracts/views/owners.py +++ b/core/apps/contracts/views/owners.py @@ -54,59 +54,55 @@ class ContractOwnerView(BaseViewSetMixin, ModelViewSet): } -class ContractOwnerFileViewSet(BaseViewSetMixin, ModelViewSet): - queryset = ContractOwnerModel.objects.all() - serializer_class = RetrieveContractAttachedFileSerializer - permission_classes = [AllowAny] +# class ContractOwnerFileViewSet(BaseViewSetMixin, ModelViewSet): +# queryset = ContractOwnerModel.objects.all() +# serializer_class = RetrieveContractAttachedFileSerializer +# permission_classes = [AllowAny] - # action_permission_classes = { - # "create_file": [AllowAny], - # "list_file": [AllowAny] - # } - action_serializer_class = { # type: ignore - "list_file": RetrieveContractAttachedFileSerializer, - "create_file": CreateContractAttachedFileSerializer, - } +# action_serializer_class = { # type: ignore +# "list_file": RetrieveContractAttachedFileSerializer, +# "create_file": CreateContractAttachedFileSerializer, +# } - @extend_schema( - summary="Contract Files Related to Owner", - description="Contract Files Related to Owner", - ) - @action(url_path="files", methods=["GET"], detail=True) - def list_file( - self, - request: HttpRequest, - *args: object, - **kwargs: object, - ) -> Response: - owner = cast(ContractOwnerModel, self.get_object()) - files = ContractAttachedFileModel.objects.filter( - contract__owners=owner, contents__owner=owner - ).select_related("contents") - serializer = self.get_serializer(instance=files, many=True) - return Response(serializer.data, status.HTTP_200_OK) +# @extend_schema( +# summary="Contract Files Related to Owner", +# description="Contract Files Related to Owner", +# ) +# @action(url_path="files", methods=["GET"], detail=True) +# def list_file( +# self, +# request: HttpRequest, +# *args: object, +# **kwargs: object, +# ) -> Response: +# owner = cast(ContractOwnerModel, self.get_object()) +# files = ContractAttachedFileModel.objects.filter( +# contract__owners=owner, contents__owner=owner +# ).select_related("contents") +# serializer = self.get_serializer(instance=files, many=True) +# return Response(serializer.data, status.HTTP_200_OK) - @extend_schema( - summary="Create Contract Files Related to Owner", - description="Create Contract Files Related to Owner" - ) - @action(url_path="files", methods=["GET"], detail=True) - def create_file( - self, - request: HttpRequest, - *args: object, - **kwargs: object, - ) -> Response: - owner = cast( - ContractOwnerModel, - self.get_queryset().select_related("contract") - ) - if not owner.contract.allow_add_files: - raise PermissionDenied(_("Attaching new files was restricted for this contract.")) - ser = self.get_serializer(data=request.data) - ser.is_valid(raise_exception=True) - ser.save() # type: ignore - return Response(ser.data, status.HTTP_201_CREATED) +# @extend_schema( +# summary="Create Contract Files Related to Owner", +# description="Create Contract Files Related to Owner" +# ) +# @action(url_path="files", methods=["GET"], detail=True) +# def create_file( +# self, +# request: HttpRequest, +# *args: object, +# **kwargs: object, +# ) -> Response: +# owner = cast( +# ContractOwnerModel, +# self.get_queryset().select_related("contract") +# ) +# if not owner.contract.allow_add_files: +# raise PermissionDenied(_("Attaching new files was restricted for this contract.")) +# ser = self.get_serializer(data=request.data) +# ser.is_valid(raise_exception=True) +# ser.save() # type: ignore +# return Response(ser.data, status.HTTP_201_CREATED) class ContractAttachedFileDeleteView(APIView): diff --git a/core/utils/views.py b/core/utils/views.py new file mode 100644 index 0000000..5f0a890 --- /dev/null +++ b/core/utils/views.py @@ -0,0 +1,93 @@ +from typing import Sequence, cast, Type, override + +from rest_framework.request import HttpRequest # type: ignore +from rest_framework.response import Response # type: ignore +from rest_framework.serializers import Serializer # type: ignore +from rest_framework.permissions import BasePermission # type: ignore + + +class BaseApiViewMixin: + """ + A reusable mixin for DRF GenericAPIView-based views, inspired by `BaseViewSetMixin`. + + This mixin provides method-specific serializer and permission class resolution, + and overrides `finalize_response()` to return a standardized response structure. + """ + serializer_class: Type[Serializer] + permission_classes: Sequence[Type[BasePermission]] + + method_serializer_class: dict[str, Type[Serializer]] = {} + method_permission_classes: dict[str, Sequence[Type[BasePermission]]] = {} + + @override + def finalize_response( # type: ignore + self, + request: HttpRequest, + response: Response, + *args: object, + **kwargs: object + ) -> Response: + """ + Finalizes the response by wrapping it in a standardized format. + + - If the status code is 2xx, the response is wrapped as: {"ok": True, "data": ...} + - If the status code is >= 400, the response becomes: {"ok": False, "data": ...} + - If the status code is 204 (No Content), the response is left untouched. + + This behavior is designed to mimic the uniform API structure used in `BaseViewSetMixin`. + + Returns: + Response: A DRF Response object with standardized content structure. + """ + if response.status_code >= 400: + response.data = {"ok": False, "data": response.data} # type: ignore + elif response.status_code == 204: + pass + else: + response.data = {"ok": True, "data": response.data} # type: ignore + return super().finalize_response(request, response, *args, **kwargs) # type: ignore + + @override + def get_serializer_class(self) -> Type[Serializer]: # type: ignore + """ + Returns the serializer class based on the current request method. + + Falls back to the default `serializer_class` if no method-specific + serializer is provided in `method_serializer_class`. + + Returns: + Type[Serializer]: The resolved serializer class. + """ + return self.method_serializer_class.get( + self.__get_request_method(), + self.serializer_class + ) + + @override + def get_permissions(self) -> list[BasePermission]: # type: ignore + """ + Returns a list of permission instances based on the current request method. + + Falls back to the default `permission_classes` if no method-specific + permissions are defined in `method_permission_classes`. + + Returns: + list[BasePermission]: A list of permission instances. + """ + return [ + permission() + for permission in self.method_permission_classes.get( + self.__get_request_method(), self.permission_classes + ) + ] + + def __get_request_method(self) -> str: + """ + Returns the HTTP method of the current request in lowercase form. + + Used internally to resolve method-specific serializers and permissions. + + Returns: + str: The request method (e.g., 'get', 'post', 'put', etc.). + """ + return cast(str, self.request.method).lower() # type: ignore # noqa