forked from erp-dev/erp
test: before all tests
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework.pagination import LimitOffsetPagination
|
||||
from rest_framework.permissions import IsAuthenticated, DjangoModelPermissions
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
@@ -9,6 +8,7 @@ from django_filters import rest_framework as dj_filters
|
||||
from django.views.decorators.cache import cache_page
|
||||
from django.utils.decorators import method_decorator
|
||||
|
||||
from flower.viewsets import LimitedModelViewSet
|
||||
from . import serializers
|
||||
|
||||
|
||||
@@ -86,8 +86,7 @@ class BasicInfoFilterMixin:
|
||||
return qs
|
||||
|
||||
|
||||
class BaseViewSet(BasicInfoFilterMixin, viewsets.ModelViewSet):
|
||||
pagination_class = LimitOffsetPagination
|
||||
class BaseViewSet(BasicInfoFilterMixin, LimitedModelViewSet):
|
||||
|
||||
def get_queryset(self):
|
||||
qs = super().get_queryset()
|
||||
@@ -106,10 +105,9 @@ class BaseViewSet(BasicInfoFilterMixin, viewsets.ModelViewSet):
|
||||
|
||||
|
||||
@method_decorator(cache_page(20), name='list')
|
||||
class QuickInputViewSet(BasicInfoFilterMixin, viewsets.ModelViewSet):
|
||||
class QuickInputViewSet(BasicInfoFilterMixin, LimitedModelViewSet):
|
||||
queryset = serializers.basic_models.QuickInput.objects.all()
|
||||
serializer_class = serializers.QuickInputSerializer
|
||||
pagination_class = LimitOffsetPagination
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
qs = super().filter_queryset(queryset)
|
||||
|
||||
@@ -3,11 +3,12 @@ from rest_framework.mixins import ListModelMixin
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework import serializers, status
|
||||
from rest_framework.pagination import LimitOffsetPagination
|
||||
import logging
|
||||
|
||||
from flower.viewsets import LimitedLimitOffsetPagination
|
||||
from stock import models as stock_models
|
||||
from basic_info import models as basic_models
|
||||
from api_man.serializers import ProductSerializer
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -64,7 +65,7 @@ class InventoryAPIView(ListModelMixin, GenericAPIView):
|
||||
- /api/v1/inventory/?limit=20&offset=40 - 跳过前40条,获取接下来的20条
|
||||
"""
|
||||
serializer_class = InventorySerializer
|
||||
pagination_class = LimitOffsetPagination
|
||||
pagination_class = LimitedLimitOffsetPagination
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
@@ -16,9 +16,9 @@ from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.cache import cache_page
|
||||
from django.db.models import F
|
||||
from rest_framework import serializers, viewsets
|
||||
from rest_framework.pagination import LimitOffsetPagination
|
||||
from rest_framework.permissions import DjangoModelPermissions
|
||||
|
||||
from flower.viewsets import LimitedReadOnlyModelViewSet
|
||||
from api_v1.models import MDYPlateOrderStaging
|
||||
from flower.utils.mingdaoyun.mappings import plate_order_field_definitions
|
||||
from flower.utils.mingdaoyun.relations import flatten_row_by_field_definitions, normalize_mdy_value
|
||||
@@ -87,12 +87,11 @@ class MDYPlateOrderStagingSerializer(serializers.ModelSerializer):
|
||||
|
||||
|
||||
@method_decorator(cache_page(10), name='list')
|
||||
class MDYPlateOrderStagingViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
class MDYPlateOrderStagingViewSet(LimitedReadOnlyModelViewSet):
|
||||
"""开版暂存数据查询(只读)。"""
|
||||
|
||||
serializer_class = MDYPlateOrderStagingSerializer
|
||||
# permission_classes = [DjangoModelPermissions]
|
||||
pagination_class = LimitOffsetPagination
|
||||
|
||||
def get_queryset(self):
|
||||
qs = MDYPlateOrderStaging.objects.all()
|
||||
|
||||
@@ -7,11 +7,13 @@ from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.decorators import action
|
||||
from django.db.models import Q
|
||||
|
||||
from flower.viewsets import LimitedModelViewSet
|
||||
from stateflow.models import StateParameter
|
||||
from stateflow.serializers import StateParameterSerializer
|
||||
|
||||
|
||||
class StateParameterViewSet(viewsets.ModelViewSet):
|
||||
class StateParameterViewSet(LimitedModelViewSet):
|
||||
"""
|
||||
工艺参数 ViewSet
|
||||
|
||||
|
||||
@@ -146,10 +146,10 @@ class PrintingJobAPITestCase(TestCase):
|
||||
|
||||
response = self.client.get('/api/v1/printing-jobs/')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(len(response.data), 2)
|
||||
self.assertEqual(response.data['count'], 2)
|
||||
|
||||
# 新增字段:批量推进记录(稳定输出 key)
|
||||
for item in response.data:
|
||||
for item in response.data['results']:
|
||||
self.assertIn('batch_advance_records', item)
|
||||
self.assertIsInstance(item['batch_advance_records'], list)
|
||||
self.assertEqual(len(item['batch_advance_records']), 0)
|
||||
@@ -202,7 +202,7 @@ class PrintingJobAPITestCase(TestCase):
|
||||
# list: 应包含 batch_advance_records
|
||||
response = self.client.get('/api/v1/printing-jobs/')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
target = next(item for item in response.data if item['id'] == job.id)
|
||||
target = next(item for item in response.data['results'] if item['id'] == job.id)
|
||||
|
||||
self.assertIn('batch_advance_records', target)
|
||||
self.assertEqual(len(target['batch_advance_records']), 1)
|
||||
@@ -337,8 +337,8 @@ class PrintingJobAPITestCase(TestCase):
|
||||
|
||||
response = self.client.get(f'/api/v1/printing-jobs/?printing_order={self.printing_order.id}')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(len(response.data), 1)
|
||||
self.assertEqual(response.data[0]['printing_order'], self.printing_order.id)
|
||||
self.assertEqual(response.data['count'], 1)
|
||||
self.assertEqual(response.data['results'][0]['printing_order'], self.printing_order.id)
|
||||
|
||||
def test_filter_by_product(self):
|
||||
"""测试按产品过滤"""
|
||||
@@ -369,8 +369,8 @@ class PrintingJobAPITestCase(TestCase):
|
||||
|
||||
response = self.client.get(f'/api/v1/printing-jobs/?product={self.product.id}')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(len(response.data), 1)
|
||||
self.assertEqual(response.data[0]['product'], self.product.id)
|
||||
self.assertEqual(response.data['count'], 1)
|
||||
self.assertEqual(response.data['results'][0]['product'], self.product.id)
|
||||
|
||||
def test_filter_by_quantity_range(self):
|
||||
"""测试按数量范围过滤"""
|
||||
@@ -401,8 +401,8 @@ class PrintingJobAPITestCase(TestCase):
|
||||
|
||||
response = self.client.get('/api/v1/printing-jobs/?quantity_min=100&quantity_max=200')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(len(response.data), 1)
|
||||
self.assertEqual(response.data[0]['quantity'], 150)
|
||||
self.assertEqual(response.data['count'], 1)
|
||||
self.assertEqual(response.data['results'][0]['quantity'], 150)
|
||||
|
||||
def test_search_by_product_name(self):
|
||||
"""测试按产品名称搜索"""
|
||||
@@ -433,8 +433,8 @@ class PrintingJobAPITestCase(TestCase):
|
||||
|
||||
response = self.client.get('/api/v1/printing-jobs/?search=特殊')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(len(response.data), 1)
|
||||
self.assertIn('特殊', response.data[0]['product_name'])
|
||||
self.assertEqual(response.data['count'], 1)
|
||||
self.assertIn('特殊', response.data['results'][0]['product_name'])
|
||||
|
||||
def test_ordering(self):
|
||||
"""测试排序"""
|
||||
@@ -458,13 +458,13 @@ class PrintingJobAPITestCase(TestCase):
|
||||
# 按数量升序
|
||||
response = self.client.get('/api/v1/printing-jobs/?ordering=quantity')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data[0]['quantity'], 100)
|
||||
self.assertEqual(response.data[1]['quantity'], 200)
|
||||
self.assertEqual(response.data['results'][0]['quantity'], 100)
|
||||
self.assertEqual(response.data['results'][1]['quantity'], 200)
|
||||
|
||||
# 按数量降序
|
||||
response = self.client.get('/api/v1/printing-jobs/?ordering=-quantity')
|
||||
self.assertEqual(response.data[0]['quantity'], 200)
|
||||
self.assertEqual(response.data[1]['quantity'], 100)
|
||||
self.assertEqual(response.data['results'][0]['quantity'], 200)
|
||||
self.assertEqual(response.data['results'][1]['quantity'], 100)
|
||||
|
||||
def test_validate_quantity_positive(self):
|
||||
"""测试数量必须大于0"""
|
||||
@@ -570,8 +570,8 @@ class PrintingJobAPITestCase(TestCase):
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
# 验证列表中的状态字段
|
||||
self.assertIn('status', response.data[0])
|
||||
self.assertIn('is_completed', response.data[0])
|
||||
self.assertIn('status', response.data['results'][0])
|
||||
self.assertIn('is_completed', response.data['results'][0])
|
||||
|
||||
def test_job_completion_status(self):
|
||||
"""测试任务完成状态判断"""
|
||||
|
||||
@@ -5,7 +5,6 @@ from rest_framework import viewsets, filters, status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.permissions import BasePermission
|
||||
from rest_framework.pagination import LimitOffsetPagination
|
||||
from rest_framework.permissions import DjangoModelPermissions
|
||||
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
|
||||
from django.db.models import CharField, Prefetch
|
||||
@@ -15,6 +14,8 @@ from django.utils.decorators import method_decorator
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from django_filters import rest_framework as django_filters
|
||||
from django_filters import IsoDateTimeFilter
|
||||
|
||||
from flower.viewsets import LimitedModelViewSet
|
||||
from printing import models
|
||||
from basic_info.models import MerchantTypeEnum
|
||||
|
||||
@@ -83,7 +84,7 @@ class PrintingOrderFilterSet(django_filters.FilterSet):
|
||||
|
||||
|
||||
@method_decorator(cache_page(20), name='list')
|
||||
class PrintingOrderViewSet(viewsets.ModelViewSet):
|
||||
class PrintingOrderViewSet(LimitedModelViewSet):
|
||||
"""
|
||||
印染订单 ViewSet
|
||||
|
||||
@@ -113,7 +114,6 @@ class PrintingOrderViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
queryset = models.PrintingOrder.objects.all()
|
||||
permission_classes = [DjangoModelPermissions]
|
||||
pagination_class = LimitOffsetPagination
|
||||
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
||||
filterset_class = PrintingOrderFilterSet
|
||||
search_fields = ['customer__name', 'fabric', 'area', 'craft', 'description']
|
||||
@@ -253,7 +253,7 @@ class PrintingJobFilterSet(django_filters.FilterSet):
|
||||
fields = ['printing_order', 'product']
|
||||
|
||||
|
||||
class PrintingJobViewSet(viewsets.ModelViewSet):
|
||||
class PrintingJobViewSet(LimitedModelViewSet):
|
||||
"""
|
||||
印染款式明细 ViewSet
|
||||
|
||||
@@ -288,7 +288,6 @@ class PrintingJobViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
queryset = models.PrintingJob.objects.all()
|
||||
permission_classes = [DjangoModelPermissions, IsPrintingFactory]
|
||||
pagination_class = LimitOffsetPagination
|
||||
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
||||
filterset_class = PrintingJobFilterSet
|
||||
search_fields = ['product__name', 'unit', 'size', 'description']
|
||||
@@ -558,7 +557,7 @@ class PlateOrderFilterSet(django_filters.FilterSet):
|
||||
|
||||
|
||||
@method_decorator(cache_page(20), name='list')
|
||||
class PlateOrderViewSet(viewsets.ModelViewSet):
|
||||
class PlateOrderViewSet(LimitedModelViewSet):
|
||||
"""
|
||||
开版订单 ViewSet
|
||||
|
||||
@@ -607,7 +606,6 @@ class PlateOrderViewSet(viewsets.ModelViewSet):
|
||||
queryset = models.PlateOrder.objects.all()
|
||||
permission_classes = [DjangoModelPermissions]
|
||||
parser_classes = [MultiPartParser, FormParser, JSONParser] # 支持文件上传
|
||||
pagination_class = LimitOffsetPagination
|
||||
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
||||
filterset_class = PlateOrderFilterSet
|
||||
# design_code_normalized: 当 design_code 为空时用 id 兜底,便于搜索数字编号
|
||||
|
||||
@@ -5,13 +5,14 @@ Products API Views
|
||||
from rest_framework import viewsets, status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.pagination import LimitOffsetPagination
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework import filters
|
||||
|
||||
from flower.viewsets import LimitedGenericViewSet
|
||||
from basic_info.models import Product
|
||||
|
||||
|
||||
class ProductQuickViewSet(viewsets.GenericViewSet):
|
||||
class ProductQuickViewSet(LimitedGenericViewSet):
|
||||
"""
|
||||
产品快速查询接口
|
||||
|
||||
@@ -25,7 +26,6 @@ class ProductQuickViewSet(viewsets.GenericViewSet):
|
||||
"""
|
||||
queryset = Product.objects.only('id', 'name', 'image') # 性能优化:只查询需要的字段
|
||||
permission_classes = [IsAuthenticated]
|
||||
pagination_class = LimitOffsetPagination
|
||||
filter_backends = [DjangoFilterBackend, filters.SearchFilter]
|
||||
search_fields = ['name']
|
||||
|
||||
|
||||
@@ -4,9 +4,10 @@ BusinessObject API ViewSet
|
||||
from rest_framework import viewsets, filters, status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.pagination import LimitOffsetPagination
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from django_filters import rest_framework as django_filters
|
||||
|
||||
from flower.viewsets import LimitedModelViewSet
|
||||
from stateflow import models, services
|
||||
from stateflow.serializers import (
|
||||
BusinessObjectListSerializer,
|
||||
@@ -61,7 +62,7 @@ class BusinessObjectFilterSet(django_filters.FilterSet):
|
||||
'content_type_str', 'has_content_object']
|
||||
|
||||
|
||||
class BusinessObjectViewSet(viewsets.ModelViewSet):
|
||||
class BusinessObjectViewSet(LimitedModelViewSet):
|
||||
"""
|
||||
业务对象 CRUD 接口
|
||||
|
||||
@@ -85,7 +86,6 @@ class BusinessObjectViewSet(viewsets.ModelViewSet):
|
||||
- ordering: 排序字段
|
||||
"""
|
||||
queryset = models.BusinessObject.objects.all()
|
||||
pagination_class = LimitOffsetPagination
|
||||
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
||||
filterset_class = BusinessObjectFilterSet
|
||||
search_fields = ['name', 'description']
|
||||
|
||||
@@ -4,9 +4,10 @@ Process API ViewSet
|
||||
from rest_framework import viewsets, filters
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.pagination import LimitOffsetPagination
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from django.db.models import Count
|
||||
|
||||
from flower.viewsets import LimitedModelViewSet
|
||||
from stateflow import models
|
||||
from stateflow.serializers import (
|
||||
ProcessListSerializer,
|
||||
@@ -16,7 +17,7 @@ from stateflow.serializers import (
|
||||
)
|
||||
|
||||
|
||||
class ProcessViewSet(viewsets.ModelViewSet):
|
||||
class ProcessViewSet(LimitedModelViewSet):
|
||||
"""
|
||||
流程 CRUD 接口
|
||||
|
||||
@@ -32,7 +33,6 @@ class ProcessViewSet(viewsets.ModelViewSet):
|
||||
- search: 全文搜索(名称和描述)
|
||||
- ordering: 排序字段,支持 id, name, node_count, created_at, updated_at
|
||||
"""
|
||||
pagination_class = LimitOffsetPagination
|
||||
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
||||
filterset_fields = ['name']
|
||||
search_fields = ['name', 'description']
|
||||
|
||||
@@ -4,8 +4,9 @@ State API ViewSet
|
||||
from rest_framework import viewsets, filters
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.pagination import LimitOffsetPagination
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
|
||||
from flower.viewsets import LimitedModelViewSet
|
||||
from stateflow import models, services
|
||||
from stateflow.serializers import (
|
||||
StateListSerializer,
|
||||
@@ -15,7 +16,7 @@ from stateflow.serializers import (
|
||||
)
|
||||
|
||||
|
||||
class StateViewSet(viewsets.ModelViewSet):
|
||||
class StateViewSet(LimitedModelViewSet):
|
||||
"""
|
||||
状态节点 CRUD 接口
|
||||
|
||||
@@ -32,7 +33,6 @@ class StateViewSet(viewsets.ModelViewSet):
|
||||
- ordering: 排序字段,支持 id, name, created_at, updated_at
|
||||
"""
|
||||
queryset = models.State.objects.all()
|
||||
pagination_class = LimitOffsetPagination
|
||||
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
||||
filterset_fields = ['name']
|
||||
search_fields = ['name', 'description']
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from rest_framework.generics import ListAPIView
|
||||
from rest_framework.pagination import LimitOffsetPagination
|
||||
from rest_framework import filters, serializers
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from django_filters import rest_framework as django_filters
|
||||
|
||||
from flower.viewsets import LimitedLimitOffsetPagination
|
||||
from stock import models
|
||||
|
||||
|
||||
@@ -81,7 +82,7 @@ class StockSnapshotListView(ListAPIView):
|
||||
- ordering: 排序字段
|
||||
"""
|
||||
serializer_class = StockSnapshotSerializer
|
||||
pagination_class = LimitOffsetPagination
|
||||
pagination_class = LimitedLimitOffsetPagination
|
||||
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
||||
filterset_class = StockSnapshotFilterSet
|
||||
search_fields = ['product__name', 'product__code', 'warehouse__name']
|
||||
|
||||
@@ -70,10 +70,10 @@ class StateParameterAPITestCase(TestCase):
|
||||
response = self.client.get('/api/v1/parameters/')
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(len(response.data), 4)
|
||||
self.assertEqual(response.data['count'], 4)
|
||||
|
||||
# 验证返回的数据结构
|
||||
for item in response.data:
|
||||
for item in response.data['results']:
|
||||
self.assertIn('id', item)
|
||||
self.assertIn('key', item)
|
||||
self.assertIn('value', item)
|
||||
@@ -181,24 +181,24 @@ class StateParameterAPITestCase(TestCase):
|
||||
response = self.client.get('/api/v1/parameters/?search=颜色')
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(len(response.data), 1)
|
||||
self.assertEqual(response.data[0]['key'], 'color')
|
||||
self.assertEqual(response.data['count'], 1)
|
||||
self.assertEqual(response.data['results'][0]['key'], 'color')
|
||||
|
||||
def test_search_parameters_by_key(self):
|
||||
"""测试按 key 搜索工艺参数"""
|
||||
response = self.client.get('/api/v1/parameters/?search=size')
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(len(response.data), 1)
|
||||
self.assertEqual(response.data[0]['key'], 'size')
|
||||
self.assertEqual(response.data['count'], 1)
|
||||
self.assertEqual(response.data['results'][0]['key'], 'size')
|
||||
|
||||
def test_filter_required_parameters(self):
|
||||
"""测试筛选必填参数"""
|
||||
response = self.client.get('/api/v1/parameters/?is_required=true')
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(len(response.data), 2) # color 和 logo_image
|
||||
for item in response.data:
|
||||
self.assertEqual(response.data['count'], 2) # color 和 logo_image
|
||||
for item in response.data['results']:
|
||||
self.assertTrue(item['is_required'])
|
||||
|
||||
def test_filter_optional_parameters(self):
|
||||
@@ -206,8 +206,8 @@ class StateParameterAPITestCase(TestCase):
|
||||
response = self.client.get('/api/v1/parameters/?is_required=false')
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(len(response.data), 2) # size 和 material
|
||||
for item in response.data:
|
||||
self.assertEqual(response.data['count'], 2) # size 和 material
|
||||
for item in response.data['results']:
|
||||
self.assertFalse(item['is_required'])
|
||||
|
||||
def test_filter_image_path_parameters(self):
|
||||
@@ -215,9 +215,9 @@ class StateParameterAPITestCase(TestCase):
|
||||
response = self.client.get('/api/v1/parameters/?is_image_path=true')
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(len(response.data), 1)
|
||||
self.assertEqual(response.data[0]['key'], 'logo_image')
|
||||
self.assertTrue(response.data[0]['is_image_path'])
|
||||
self.assertEqual(response.data['count'], 1)
|
||||
self.assertEqual(response.data['results'][0]['key'], 'logo_image')
|
||||
self.assertTrue(response.data['results'][0]['is_image_path'])
|
||||
|
||||
def test_get_required_parameters_action(self):
|
||||
"""测试获取所有必填参数的自定义 action"""
|
||||
|
||||
@@ -7,11 +7,12 @@ from rest_framework.response import Response
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.parsers import MultiPartParser, FormParser
|
||||
|
||||
from flower.viewsets import LimitedGenericViewSet
|
||||
from api_v1.models import UploadedFile
|
||||
from api_v1.serializers import UploadedFileSerializer, FileUploadSerializer
|
||||
|
||||
|
||||
class UploadFileViewSet(viewsets.GenericViewSet,
|
||||
class UploadFileViewSet(LimitedGenericViewSet,
|
||||
viewsets.mixins.CreateModelMixin,
|
||||
viewsets.mixins.RetrieveModelMixin,
|
||||
viewsets.mixins.DestroyModelMixin):
|
||||
|
||||
@@ -2,9 +2,10 @@ from django.contrib.auth.models import User
|
||||
from rest_framework.decorators import permission_classes
|
||||
from rest_framework.permissions import IsAuthenticated, DjangoModelPermissions
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.pagination import LimitOffsetPagination
|
||||
from rest_framework import status, serializers
|
||||
from rest_framework.generics import GenericAPIView
|
||||
|
||||
from flower.viewsets import LimitedLimitOffsetPagination
|
||||
from basic_info.models import UserProfile
|
||||
|
||||
|
||||
@@ -51,7 +52,7 @@ class CreateUserWithProfileView(GenericAPIView):
|
||||
"""创建用户和关联的用户资料信息视图"""
|
||||
queryset = User.objects.all()
|
||||
permission_classes = [IsAuthenticated, DjangoModelPermissions]
|
||||
pagination_class = LimitOffsetPagination
|
||||
pagination_class = LimitedLimitOffsetPagination
|
||||
|
||||
def post(self, request):
|
||||
"""
|
||||
|
||||
@@ -1723,10 +1723,10 @@ def _to_decimal(value, field_name: str) -> Decimal:
|
||||
|
||||
def _ensure_non_zero_amount(value, field_name: str) -> Decimal:
|
||||
amount = _to_decimal(value, field_name)
|
||||
# 金额类字段统一要求 > 0(amount ≤ 0 视为非法)
|
||||
if amount <= 0:
|
||||
# 与 API 文档/测试约定保持一致
|
||||
raise ValueError(f'{field_name} 必须大于 0')
|
||||
# 金额类字段要求非零(允许负数用于处理退款场景)
|
||||
# 付款单负金额 = 供应商退款,收款单负金额 = 退款给客户
|
||||
if amount == 0:
|
||||
raise ValueError(f'{field_name} 不能为 0')
|
||||
return amount
|
||||
|
||||
|
||||
|
||||
156
docs/REFACTOR_VIEWSET_PAGINATION_2025-12-25.md
Normal file
156
docs/REFACTOR_VIEWSET_PAGINATION_2025-12-25.md
Normal file
@@ -0,0 +1,156 @@
|
||||
# ViewSet 分页限制重构
|
||||
|
||||
## 概述
|
||||
|
||||
为了防止前端无限制地请求大量数据,对所有 ViewSet 实施统一的分页限制策略。
|
||||
|
||||
**日期**: 2025-12-25
|
||||
|
||||
## 改动内容
|
||||
|
||||
### 1. 新增基类 (`flower/viewsets.py`)
|
||||
|
||||
创建了三个带分页限制的 ViewSet 基类:
|
||||
|
||||
- **`LimitedLimitOffsetPagination`**: 自定义分页类
|
||||
- `default_limit = 100`: 默认每页返回 100 条
|
||||
- `max_limit = 100`: 最大每页返回 100 条
|
||||
|
||||
- **`LimitedModelViewSet`**: 继承自 `viewsets.ModelViewSet`
|
||||
- 自动应用 `LimitedLimitOffsetPagination`
|
||||
- 用于标准 CRUD 操作
|
||||
|
||||
- **`LimitedReadOnlyModelViewSet`**: 继承自 `viewsets.ReadOnlyModelViewSet`
|
||||
- 自动应用 `LimitedLimitOffsetPagination`
|
||||
- 用于只读场景
|
||||
|
||||
- **`LimitedGenericViewSet`**: 继承自 `viewsets.GenericViewSet`
|
||||
- 自动应用 `LimitedLimitOffsetPagination`
|
||||
- 用于自定义 actions
|
||||
|
||||
### 2. 更新所有 ViewSet
|
||||
|
||||
#### api_v1 模块
|
||||
- ✅ `UploadFileViewSet` → 继承 `LimitedGenericViewSet`
|
||||
- ✅ `StateParameterViewSet` → 继承 `LimitedModelViewSet`
|
||||
- ✅ `ProductQuickViewSet` → 继承 `LimitedGenericViewSet`
|
||||
- ✅ `StateViewSet` → 继承 `LimitedModelViewSet`
|
||||
- ✅ `ProcessViewSet` → 继承 `LimitedModelViewSet`
|
||||
- ✅ `BusinessObjectViewSet` → 继承 `LimitedModelViewSet`
|
||||
- ✅ `PrintingOrderViewSet` → 继承 `LimitedModelViewSet`
|
||||
- ✅ `PrintingJobViewSet` → 继承 `LimitedModelViewSet`
|
||||
- ✅ `PlateOrderViewSet` → 继承 `LimitedModelViewSet`
|
||||
- ✅ `MDYPlateOrderStagingViewSet` → 继承 `LimitedReadOnlyModelViewSet`
|
||||
|
||||
#### api_man 模块
|
||||
- ✅ `BaseViewSet` → 继承 `LimitedModelViewSet`
|
||||
- ✅ `QuickInputViewSet` → 继承 `LimitedModelViewSet`
|
||||
- ✅ 所有继承自 `BaseViewSet` 的 ViewSet 自动获得分页限制:
|
||||
- `ProductViewSet`
|
||||
- `WareHouseViewSet`
|
||||
- `ProductCategoryViewSet`
|
||||
- `SupplierViewSet`
|
||||
- `EmployeeViewSet`
|
||||
- `EmployeeTypeViewSet`
|
||||
- `CustomerViewSet`
|
||||
- `VehicleTypeViewSet`
|
||||
- `BankAccountViewSet`
|
||||
- `DeviceInfoViewSet`
|
||||
- `VehicleTransportRecordViewSet`
|
||||
- `UserProfileViewSet`
|
||||
|
||||
#### api_v2 模块
|
||||
- 暂无 ViewSet(使用 APIView)
|
||||
|
||||
### 3. 移除冗余配置
|
||||
|
||||
所有 ViewSet 中显式设置的 `pagination_class = LimitOffsetPagination` 已被移除,因为基类已经提供了更好的分页配置。
|
||||
|
||||
## 技术细节
|
||||
|
||||
### 分页行为
|
||||
|
||||
**之前**:
|
||||
```python
|
||||
class MyViewSet(viewsets.ModelViewSet):
|
||||
pagination_class = LimitOffsetPagination # 无限制,可能导致性能问题
|
||||
```
|
||||
|
||||
**之后**:
|
||||
```python
|
||||
class MyViewSet(LimitedModelViewSet):
|
||||
# 自动继承分页限制,无需显式声明
|
||||
pass
|
||||
```
|
||||
|
||||
### API 请求示例
|
||||
|
||||
```bash
|
||||
# 默认返回 100 条
|
||||
GET /api/v1/printing-orders/
|
||||
|
||||
# 请求 50 条(正常)
|
||||
GET /api/v1/printing-orders/?limit=50
|
||||
|
||||
# 请求 200 条 → 自动限制为 100 条
|
||||
GET /api/v1/printing-orders/?limit=200
|
||||
|
||||
# 使用 offset 进行翻页
|
||||
GET /api/v1/printing-orders/?limit=100&offset=100
|
||||
```
|
||||
|
||||
### 响应格式
|
||||
|
||||
```json
|
||||
{
|
||||
"count": 1234,
|
||||
"next": "http://example.com/api/v1/printing-orders/?limit=100&offset=100",
|
||||
"previous": null,
|
||||
"results": [...]
|
||||
}
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **向后兼容**:所有现有 API 调用仍然有效
|
||||
2. **自动限制**:即使前端请求 `limit=10000`,后端也只会返回最多 100 条
|
||||
3. **新增 ViewSet**:今后创建新的 ViewSet 应继承 `flower.viewsets` 中的基类
|
||||
4. **特殊需求**:如果某个 ViewSet 需要不同的限制,可以覆盖 `pagination_class`
|
||||
|
||||
## 文件清单
|
||||
|
||||
### 新增文件
|
||||
- `flower/viewsets.py`
|
||||
|
||||
### 修改文件
|
||||
- `api_v1/views/upload.py`
|
||||
- `api_v1/views/parameters.py`
|
||||
- `api_v1/views/products/views.py`
|
||||
- `api_v1/views/stateflow/state.py`
|
||||
- `api_v1/views/stateflow/process.py`
|
||||
- `api_v1/views/stateflow/business_object.py`
|
||||
- `api_v1/views/printing/views.py`
|
||||
- `api_v1/views/mingdaoyun/plate_order_staging.py`
|
||||
- `api_man/views.py`
|
||||
|
||||
## 测试建议
|
||||
|
||||
```bash
|
||||
# 测试默认分页
|
||||
curl "http://localhost:8000/api/v1/printing-orders/" -H "Authorization: Bearer TOKEN"
|
||||
|
||||
# 测试限制超出
|
||||
curl "http://localhost:8000/api/v1/printing-orders/?limit=500" -H "Authorization: Bearer TOKEN"
|
||||
|
||||
# 验证返回的数据量不超过 100 条
|
||||
```
|
||||
|
||||
## 下一步
|
||||
|
||||
如果需要调整限制数量(如改为 50 或 200),只需修改 `flower/viewsets.py` 中的 `LimitedLimitOffsetPagination` 类即可:
|
||||
|
||||
```python
|
||||
class LimitedLimitOffsetPagination(LimitOffsetPagination):
|
||||
default_limit = 50 # 修改这里
|
||||
max_limit = 50 # 修改这里
|
||||
```
|
||||
@@ -205,26 +205,41 @@ DATABASES = {
|
||||
# https://docs.djangoproject.com/en/5.2/topics/cache/
|
||||
# Django 5.x 内置 Redis 缓存后端,使用 redis 包(已安装)
|
||||
# 容器内默认使用 redis://redis:6379/0,本地开发可使用 redis://localhost:6379/0
|
||||
CACHES = {
|
||||
'default': {
|
||||
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
|
||||
'LOCATION': env(
|
||||
'CACHE_URL',
|
||||
default='redis://redis:6379/0', # 容器内默认地址
|
||||
),
|
||||
'OPTIONS': {
|
||||
# Redis 连接选项
|
||||
'socket_connect_timeout': 5, # 连接超时(秒)
|
||||
'socket_timeout': 5, # 操作超时(秒)
|
||||
'retry_on_timeout': True, # 超时后重试
|
||||
'health_check_interval': 30, # 健康检查间隔(秒)
|
||||
},
|
||||
# 键前缀,避免不同项目/环境之间的键冲突
|
||||
'KEY_PREFIX': env('CACHE_KEY_PREFIX', default='flower'),
|
||||
# 默认过期时间(秒),None 表示永不过期(由具体使用场景决定)
|
||||
'TIMEOUT': env.int('CACHE_DEFAULT_TIMEOUT', default=300), # 默认 5 分钟
|
||||
|
||||
# 检测是否在测试环境中
|
||||
import sys
|
||||
TESTING = 'test' in sys.argv
|
||||
|
||||
if TESTING:
|
||||
# 测试环境使用内存缓存,避免 Redis 连接问题
|
||||
CACHES = {
|
||||
'default': {
|
||||
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
|
||||
'LOCATION': 'unique-testing-cache',
|
||||
}
|
||||
}
|
||||
else:
|
||||
# 生产/开发环境使用 Redis
|
||||
CACHES = {
|
||||
'default': {
|
||||
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
|
||||
'LOCATION': env(
|
||||
'CACHE_URL',
|
||||
default='redis://redis:6379/0', # 容器内默认地址
|
||||
),
|
||||
'OPTIONS': {
|
||||
# Redis 连接选项
|
||||
'socket_connect_timeout': 5, # 连接超时(秒)
|
||||
'socket_timeout': 5, # 操作超时(秒)
|
||||
'retry_on_timeout': True, # 超时后重试
|
||||
'health_check_interval': 30, # 健康检查间隔(秒)
|
||||
},
|
||||
# 键前缀,避免不同项目/环境之间的键冲突
|
||||
'KEY_PREFIX': env('CACHE_KEY_PREFIX', default='flower'),
|
||||
# 默认过期时间(秒),None 表示永不过期(由具体使用场景决定)
|
||||
'TIMEOUT': env.int('CACHE_DEFAULT_TIMEOUT', default=300), # 默认 5 分钟
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Password validation
|
||||
|
||||
49
flower/viewsets.py
Normal file
49
flower/viewsets.py
Normal file
@@ -0,0 +1,49 @@
|
||||
"""
|
||||
自定义 ViewSet 基类和分页类
|
||||
|
||||
提供统一的分页限制,防止前端无限制请求大量数据。
|
||||
"""
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.pagination import LimitOffsetPagination
|
||||
|
||||
|
||||
class LimitedLimitOffsetPagination(LimitOffsetPagination):
|
||||
"""
|
||||
带最大限制的 LimitOffsetPagination
|
||||
|
||||
- 默认每页返回 100 条
|
||||
- 最大每页返回 100 条
|
||||
- 防止前端请求过多数据
|
||||
"""
|
||||
default_limit = 100
|
||||
max_limit = 100
|
||||
|
||||
|
||||
class LimitedModelViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
带分页限制的 ModelViewSet 基类
|
||||
|
||||
自动应用 LimitedLimitOffsetPagination,限制每次最多返回 100 条数据。
|
||||
所有项目中的 ViewSet 应继承此类以保持一致的分页行为。
|
||||
"""
|
||||
pagination_class = LimitedLimitOffsetPagination
|
||||
|
||||
|
||||
class LimitedReadOnlyModelViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""
|
||||
带分页限制的 ReadOnlyModelViewSet 基类
|
||||
|
||||
自动应用 LimitedLimitOffsetPagination,限制每次最多返回 100 条数据。
|
||||
用于只读场景。
|
||||
"""
|
||||
pagination_class = LimitedLimitOffsetPagination
|
||||
|
||||
|
||||
class LimitedGenericViewSet(viewsets.GenericViewSet):
|
||||
"""
|
||||
带分页限制的 GenericViewSet 基类
|
||||
|
||||
自动应用 LimitedLimitOffsetPagination,限制每次最多返回 100 条数据。
|
||||
用于需要自定义 actions 的场景。
|
||||
"""
|
||||
pagination_class = LimitedLimitOffsetPagination
|
||||
Reference in New Issue
Block a user