양파개발자 실바의 블로그

Django Model ViewSet

Django REST Framework의 ModelViewSet을 활용한 예제입니다. EagerLoadingMixin과 ExcelDownloadMixin을 사용하여 효율적인 쿼리와 엑셀 다운로드 기능을 구현합니다.


1. ViewSet 구현

ModelViewSet을 상속받아 구현한 예제입니다.

import logging

from django.db import transaction
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import status, serializers
from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError
from rest_framework.filters import OrderingFilter
from rest_framework.permissions import IsAdminUser
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet

from apps.partner.services.base import get_partner_info
from apps.product.models2.wholesaler import WholeSaler
from baro.consts import HexColor
from baro.mixins import EagerLoadingMixin
from baro.mixins2.xls_download import ExcelDownloadMixin
from baro.permissions import IsPartnerAdmin
from dodo_apps.inven.filters.dodo_inven import DodoInventoryFilter
from dodo_apps.inven.models.dodo_inven import DodoInventory
from dodo_apps.inven.serializers.cms_dodo_inven import (
    CMSDodoInventoryListSerializer,
    CMSDodoInventoryDetailSerializer,
)
from dodo_apps.inven.serializers.dodo_inven import (
    DodoInventoryListSerializer,
    DodoInventoryDetailSerializer,
    DodoInventoryExcelSerializerForERPSyncWholesaler,
    DodoInventoryExcelSerializerForSelfManageWholesaler,
)
from dodo_apps.inven.services.dodo_inven import DodoInventoryService

logger = logging.getLogger(__name__)


class DodoInventoryViewSet(EagerLoadingMixin, ExcelDownloadMixin, ModelViewSet):
    permission_classes = [IsPartnerAdmin]
    queryset = DodoInventory.objects.all()
    queryset_select_related_list = ["product", "wholesaler"]
    filter_backends = [DjangoFilterBackend, OrderingFilter]
    filterset_class = DodoInventoryFilter
    ordering_fields = ["id"]
    ordering = ["-id"]
    wholesaler = None
    excel_download_header_bg_color = HexColor.BLUE_1.code

    def filter_queryset(self, queryset):
        try:
            return super().filter_queryset(queryset)
        except ValueError as ve:
            logger.warning(
                "도도팜 재고 목록 조회: 입력된 필터 정보 오류 - %s",
                self.request.query_params,
            )
            raise serializers.ValidationError("필터링 정보가 잘못 되었습니다")

    def get_queryset(self):
        partner = get_partner_info(self.request.user.id)
        self.wholesaler = partner.whole_saler
        queryset = super().get_queryset()
        return queryset.filter(wholesaler=self.wholesaler)

    def get_serializer_context(self):
        context = super().get_serializer_context()
        context["wholesaler"] = self.wholesaler
        return context

    def get_serializer_class(self):
        if self.action == "list":
            return DodoInventoryListSerializer

        return DodoInventoryDetailSerializer

    def get_excel_download_serializer_class(self):
        order_type_enum = WholeSaler.OrderType.get_member(self.wholesaler.order_type)
        if order_type_enum == WholeSaler.OrderType.EXTERNAL_DB_CONNECT:
            for i in range(10):
                self.set_column_bg_color(i, HexColor.GREY_2.code)
            for i in range(11, 16):
                self.set_column_bg_color(i, HexColor.GREY_2.code)
            for i in [10, 16, 17, 18, 19]:
                self.set_column_bg_color(i, HexColor.YELLO_1.code)
            return DodoInventoryExcelSerializerForERPSyncWholesaler
        elif order_type_enum == WholeSaler.OrderType.INTERNAL_DB_CONNECT:
            for i in range(8):
                self.set_column_bg_color(i, HexColor.GREY_2.code)
            for i in range(8, 19):
                self.set_column_bg_color(i, HexColor.YELLO_1.code)
            return DodoInventoryExcelSerializerForSelfManageWholesaler
        else:
            raise ValidationError(f"주문방식이 {order_type_enum.text} 인 도매는 다운로드가 불가합니다.")

    def list(self, request, *args, **kwargs):
        response = super().list(request, *args, **kwargs)
        DodoInventoryService.set_product_data(response.data["items"])
        return response

    def retrieve(self, request, *args, **kwargs):
        response = super().retrieve(request, *args, **kwargs)
        DodoInventoryService.set_product_data([response.data])
        return response

    @action(detail=False, methods=["put"], url_path="bulk-update")
    def bulk_update(self, request, *args, **kwargs):
        """
        여러개 인벤토리 정보를 한번에 수정할 수 있는 API

        - Request Body = Dict List 형태
        - 전부 다 성공 하거나 전부다 실패하도록 처리
        - 입력된 데이터 순서대로 리턴
        """
        data = request.data
        if not isinstance(data, list):
            raise ValidationError("입력 데이터 형식이 잘못되었습니다.")

        request_data_dic = {item["id"]: item for item in data}
        inventories = self.get_queryset().filter(id__in=request_data_dic.keys())
        if len(inventories) == 0:
            raise ValidationError("업데이트할 재고정보를 찾지 못했습니다.")

        updated_objects = []
        updated_fields = set()
        with transaction.atomic():
            for inven in inventories:
                try:
                    serializer = self.get_serializer(
                        inven, data=request_data_dic[inven.id], partial=True
                    )
                    serializer.is_valid(raise_exception=True)
                    for field, value in serializer.validated_data.items():
                        updated_fields.add(field)
                        setattr(inven, field, value)
                except serializers.ValidationError as ve:
                    ve.detail["id"] = inven.id
                    raise ve

            DodoInventory.objects.bulk_update(inventories, fields=list(updated_fields))
            serializer = self.get_serializer(inventories, many=True)
            updated_objects = serializer.data

        return Response({"data": updated_objects}, status=status.HTTP_200_OK)


class CMSDodoInventoryViewSet(DodoInventoryViewSet):
    permission_classes = [IsAdminUser]

    def get_queryset(self):
        return DodoInventory.objects.all().select_related("product", "wholesaler")

    def get_serializer_class(self):
        if self.action == "list":
            return CMSDodoInventoryListSerializer

        return CMSDodoInventoryDetailSerializer

    def get_excel_download_serializer_class(self):
        return DodoInventoryExcelSerializerForERPSyncWholesaler

    def get_object(self):
        inven = super().get_object()
        self.wholesaler = inven.wholesaler
        return inven