跳转至

DjangoRestFramework笔记

说明:在笔记中,DjangoRestFramework将被简写为DRF,ForeignKey简写为Fk,ManyToManyField简写为M2M。

Serializer相关

serializer中将图片、文件等对象的相对路径url变成绝对路径url

在Serializer初始化时向其传递content参数中需要包含request对象即可,既:serializer = AccountSerializer(queryset, context={'request': request})

可以简单的写成 serializer = AccountSerializer(queryset, context=self.get_serializer_context()),以下是get_serializer_context函数的源码:

def get_serializer_context(self):
    """
    Extra context provided to the serializer class.
    """
    return {
        'request': self.request,
        'format': self.format_kwarg,
        'view': self
    }

添加ForeignKey、ManytoMany时注意事项

前端在添加ForeignKey时,直接为requsetBody添加ID即可;

添加ManytoMany时,不是在requsetBody中为参数m2m_name添加[1,2,3],而是依次向m2m_name中添加1、2、3,既连续添加三次。

ajax传递数组

使用ajax从前端像后端传递数组是比较常规的需求,在前端ajax需要设置traditional: true参数。DRF后端使用request.data.getlist('key')方法获取该数组。

有时候DRF对应使用不同的前端,如android中的okhttp或者vue中的&http或者python中的requests模块,会出现request.data是Dict而不是QueryDict的时候,需要使用方法兼容:

def get_list(_dict, key):
    """
    usage: get_list(request.data, 'key')
    :param _dict: QueryDict or dict
    :param key: list 键名
    :return: _dict中key对应的list
    """
    if isinstance(_dict, QueryDict):
        return _dict.getlist(key)
    else:
        return _dict.get(key, [])用法为get_list(request.data, 'key')

jQuery ajax的traditional参数的作用

Serializers嵌套新增FK或者M2M

RDF2.0以前有此功能,3.0声明中放弃此功能,应为此功能对于用户来说逻辑处理区别大,要支持嵌套创建、更新需要强制重写create、update方法。

方法一: 用 ast.literal_eval() 将str解析称为dict再对外键进行序列化,将序列化得到的ID赋给外键。略繁琐,赋值时要设置request.data._mutable=True来保证request.data可被修改,需要有事务逻辑,当被嵌套的serializer数据不合法时,需要删除先前创建的待嵌套对象,浪费数据库开销。

方法二: 使用user.name作为参数名直接进行序列化,缺点是更新时需要一个field一个field的去写,比较麻烦(也可用for循环配合setattr方法使用)。

方法三: 手写具体的create\update方法,示例:

class EscapeRecipeModifySerializer(WritableNestedModelSerializer):
    crafts = EscapeRecipeCraftModifySerializer(many=True, required=False, label='特殊工艺')

def create(self, validated_data):
    crafts_data = validated_data.pop('crafts')
    escape_recipe = EscapeRecipe.objects.create(**validated_data)

    # 保存原始配方
    crafts = []
    for craft_data in crafts_data:
        craft = OriginalRecipe(**craft_data)
        craft.save()
        crafts.append(craft)

    escape_recipe.original_recipes.add(*crafts)
    return escape_recipe

class Meta:
    model = EscapeRecipe
    fields = ('original_recipe', 'medicine', 'dosage', 'crafts')
方法四:使用drf-writable-nested库,需注意客户端和单元测试时数据格式必须为application/json。用requests库请求时,用requests.post(url=xxx,json={},headers={}),是json={}而不是data={},可参考drf-writable-nested嵌套新增更新。

创建、更新时使用的Serializer中FK\O2O\M2M不可展开

创建使用的serializer和更新使用的serializer,如果包含外键、O2O、M2M的话,不可将外键、O2O、M2M展开成详情,不然无法进行创建或者更新。

Serializer 一次序列化多个对象 VS 多次序列一个对象

Serializer通过data = Serializer(queryset, many=True).data极端耗费时间,能一次获取的data不要通过多次获取。宁愿获取到data的list,再去交叉对比list里面的数据也不要为了使用orm方便,在筛选出对应的queryset后再多次去做queryset的序列化,这样耗费的时间将成倍增加,1次序列化1个对象的时间和序列化1000个对象的时间差距并不大,而序列化1次和序列化1000次的时间差距巨大,一次序列化1000个耗时是序列化1000次(每次一个)耗时的五分之一。

import django.apps
from school.serializers import PermissionSerializer
permission_list = []
permissions = request.user.user_permissions
for model in django.apps.apps.get_models():
    content_type = ContentType.objects.get_for_model(model)
    model_permissions = permissions.filter(content_type=content_type)
    serializer = PermissionSerializer(model_permissions, many=True)
    permission_list.append({'name': model._meta.verbose_name,
                            'content_type': content_type.id,
                            'model': content_type.model,
                            'permissions': serializer.data})

# permission_list = sorted(permission_list, key=lambda x: int(x['content_type']))
data = {'id': user.id,
        'token': token.access_token,
        'name': user.get_full_name(),
        'permission_list': permission_list}
return success_response(data)

使用orm筛选后多次对queryset序列化 耗时: 0.20399999618530273

# 获取根据模型得到的权限列表
permission_list = []
perm_data = PermissionSerializer(request.user.user_permissions, many=True).data
for model in django.apps.apps.get_models():
    content_type = ContentType.objects.get_for_model(model)
    model_permissions = [data for data in perm_data if data['content_type'] == content_type.id]
    permission_list.append({'name': model._meta.verbose_name, 'content_type': content_type.id,
                            'model': content_type.model, 'permissions': model_permissions})

data = {'id': user.id,
        'token': token.access_token,
        'name': user.get_full_name(),
        'permission_list': sorted(permission_list, key=lambda x: x['content_type'])}
return success_response(data)

一次序列化得到list再对比 耗时:0.04799985885620117

性能对比:

@list_route(methods=['GET'])
def test(self, request):
    users = request.user
    lists = []
    for x in range(0, 1000):
        lists.append(users)
    start_time = time.time()
    data1 = UserListSerializer(lists, many=True, context=self.get_serializer_context()).data
    end_time = time.time()
    print(end_time-start_time)

    start_time = time.time()
    for user in lists:
        data = UserListSerializer(user, context=self.get_serializer_context()).data
    end_time = time.time()
    print(end_time - start_time)
    return success_response('')

平均耗时为:0.1s 0.5s

设置字段选填、必填

extra_kwargs = {'pre_student': {'required': True}}
extra_kwargs = {'username': {'required': False}}

将序列化错误转换成可读语言

# 将serializer中的错误解析为交互更友好的形式
class HumanizationSerializerErrorsMixin(object):
    # humanization_serializer_errors
        def humanize_errors(self, serializer=None, errors=None):
            """
            将serializer中的错误解析为交互更友好的形式
            :param serializer: 序列化类
            :param errors: 对应错误
            :return: 交互友好的错误提示
            """
            humanization_errors = []
            if serializer is None:
                serializer = self
            if errors is None:
                errors = serializer.errors

            if isinstance(errors, list):
                for error in errors:
                    humanization_errors.append(str(error))
            elif isinstance(errors, ErrorDetail):
                humanization_errors.append(str(errors))
            else:
                fields = serializer.get_fields()
                for key, values in errors.items():
                    try:
                        field = fields[key]
                        # field中有model则使用model
                        verbose_name = field.label if field.label else key
                        # 解析子错误-递归
                        if isinstance(field, serializers.Serializer):
                            values = self.humanize_errors(field, errors=values)
                        elif isinstance(field, serializers.ListSerializer):
                            values = [self.humanize_errors(field.child, errors=x) for x in values]
                    except KeyError:
                        if isinstance(serializer, ModelSerializer):
                            model = serializer.Meta.model
                            verbose_name = model._meta.verbose_name
                        else:
                            verbose_name = key
                    errors_text = values if isinstance(values, str) else ' '.join([str(x) for x in values])
                    humanization_errors.append('{}:{}'.format(str(verbose_name), errors_text))
            return '; '.join(humanization_errors)

查看DRF自动生成的Serializer内容

python from user.serializers import UserSerializer serializer = UserSerializer() print(repr(serializer))

BaseSerializer如何判断save方法当前是新增还是更新

有成员变量instance,没有改instance时save方法调用create方法,有该instance时调用update方法

if self.instance is not None:
    self.instance = self.update(self.instance, validated_data)
    assert self.instance is not None, (
        '`update()` did not return an object instance.'
    )
else:
    self.instance = self.create(validated_data)
    assert self.instance is not None, (
        '`create()` did not return an object instance.'
    )

vaildate指定错误对应的字段方法

raise serializers.ValidationError({'discountcouponrecord': ['仅可使用该学员领取的优惠券']})

vaildate调用model中clean方法

DRF3.0后,ModelSerializer的validate默认不会再调用model的clean方法,声明见此。需要手动调用model的clean方法:

def validate(self, data):
    instance = self.Meta.model(**data)
    instance.clean()
    return data

重写ModelSerializer

  1. 将None序列化为 '' 而不是 'null'

class ModelSerializer(serializers.ModelSerializer):
    def to_representation(self, instance):
        """
              Object instance -> Dict of primitive data types.
        """
        ret = OrderedDict()
        fields = [field for field in self.fields.values() if not field.write_only]

        for field in fields:
            try:
                key = field.get_attribute(instance)
            except SkipField:
                continue
            if key is not None:
                value = field.to_representation(key)
                # 子对象中有对象为空 依旧序列化
                # if value is None:
                #     # Do not serialize empty objects
                #     print('empty objects')
                #     continue
                # 子对象中有列表为空 依旧序列化 eg:Moment->photos为空依旧要序列化
                # if isinstance(value, list) and not value:
                #     # Do not serialize empty lists
                #     print('empty lists')
                #     continue
                ret[field.field_name] = value
                # print(field.field_name, value)
            else:
                # value None to '' rather tan 'null'
                # print(field.field_name, field.to_representation(key), '有空值')
                ret[field.field_name] = ''

        # 为serializers中动态添加的context赋值输出
        for field in self.context:
            # context defaults to including 'request', 'view' and 'format' keys.
            if field not in ['request', 'view', 'format']:
                ret[field] = self.context[field]
        return ret
2. 新增时model设置了null=True的字段也要求必填(和form一致)

class ModelSerializer(serializers.ModelSerializer):
    def build_field(self, field_name, info, model_class, nested_depth):
        """
        Return a two tuple of (cls, kwargs) to build a serializer field with.

        重新该方法以保证model中Field在null=True的情况下依旧是必填项
        field_kwargs中required参数原始判断依据
        (build_field->build_standard_field \ build_relational_field->get_field_kwargs\get_relation_kwargs):
            if model_field.has_default() or model_field.blank or model_field.null:
                kwargs['required'] = False
        更新后依据:
            if model_field.has_default() or model_field.blank:
                field_kwargs['required'] = False
            else:
                field_kwargs['required'] = True
        """
        if field_name in info.fields_and_pk:
            # 普通field 部分
            model_field = info.fields_and_pk[field_name]
            field_class, field_kwargs = self.build_standard_field(field_name, model_field)
            field_kwargs['required'] = False if (model_field.has_default() or model_field.blank) else True
            return field_class, field_kwargs

        elif field_name in info.relations:
            # FK\M2M 部分
            relation_info = info.relations[field_name]

            if not nested_depth:
                field_class, field_kwargs = self.build_relational_field(field_name, relation_info)
                model_field, related_model, to_many, to_field, has_through_model, reverse = relation_info
                field_kwargs['required'] = False if (model_field.has_default() or model_field.blank) else True
                return field_class, field_kwargs
            else:
                return self.build_nested_field(field_name, relation_info, nested_depth)

        elif hasattr(model_class, field_name):
            return self.build_property_field(field_name, model_class)

        elif field_name == self.url_field_name:
            return self.build_url_field(field_name, model_class)

        return self.build_unknown_field(field_name, model_class)

DynamicFieldsModelSerializer

# 动态获取Fields
class DynamicFieldsModelSerializer(ModelSerializer):
    """
    A ModelSerializer that takes an additional `fields` argument that
    controls which fields should be displayed.
    """

    def __init__(self, *args, **kwargs):
        # Don't pass the 'fields' arg up to the superclass
        fields = kwargs.pop('fields', None)
        exclude = kwargs.pop('exclude', None)

        # Instantiate the superclass normally
        super(DynamicFieldsModelSerializer, self).__init__(*args, **kwargs)

        if fields is not None:
            # Drop any fields that are not specified in the `fields` argument.
            allowed = set(fields)
            existing = set(self.fields.keys())
            for field_name in existing - allowed:
                self.fields.pop(field_name)

        if exclude is not None:
            not_allowed = set(exclude)
            for exclude_name in not_allowed:
                self.fields.pop(exclude_name)
用法:

DynamicFieldsModelSerializer(data=data, fields=('name'))
DynamicFieldsModelSerializer(data=data, exclude=('name'))

DictField使用

from rest_framework.serializers import DictField
from django.utils import six


class MyDictField(DictField):
    def to_internal_value(self, data):
        """
        Dicts of native values <- Dicts of primitive datatypes.
        """
        # if html.is_html_input(data):
        #    data = html.parse_html_dict(data)

        if not isinstance(data, dict):
            self.fail('not_a_dict', input_type=type(data).__name__)
        return {
            six.text_type(key): self.child.run_validation(value)
            for key, value in data.items()
        }


class FavoritesModifySerializer(ModelSerializer):
    data_params = MyDictField(child=serializers.CharField())
    ...

    def create(self, validated_data):
        print(validated_data)
        data_dic = validated_data.pop("data_params")
        print(data_dic)
        return super(FavoritesModifySerializer, self).create(validated_data)

PostMan中发送key:data_params.data value:呵呵,控制台输出:

{'user': , 'cover': , 'name': '1', 'posts': [], 'data_params': {'data': '呵呵'}, 'categor
y': 1}
{'data': '呵呵'}

重写to_representation方法

def to_representation(self, instance):
    serialized_data = super(SerializerClass, self).to_representation(instance)
    serialized_data['override_field'] = 'some data'
    return serialized_data

source

name = serializers.CharField(source='user.full_name')

自定义unique_together提示信息

方法一:

class MedicineEscapeModifySerializer(ModelSerializer):
    class Meta:
        model = MedicineEscape
        fields = ('hospital', 'medicine', 'medicine_form', 'hospital_medicine_name', 'hospital_medicine_no', 'remarks')
        validators = [
            serializers.UniqueTogetherValidator(
                queryset=model.objects.all(),
                fields=('hospital', 'hospital_medicine_name'),
                message='医院不可重复关联同一医院药品名称;'
            ),
            serializers.UniqueTogetherValidator(
                queryset=model.objects.all(),
                fields=('hospital', 'hospital_medicine_no'),
                message='医院不可重复关联同一医院药品编码;'
            )
        ]

方法二:

 class MedicineEscapeModifySerializer(ModelSerializer):
    class Meta:
        model = MedicineEscape
        fields = ('hospital', 'medicine', 'medicine_form', 'hospital_medicine_name', 'hospital_medicine_no', 'remarks')

        def get_unique_together_validators(self):
            validators = super(MedicineEscapeModifySerializer, self).get_unique_together_validators()
            print(self.initial_data)
            for validator in validators:
                if validator.fields == ('hospital', 'hospital_medicine_name'):
                    validator.message = '医院不可重复关联同一医院药品名称;'
                if validator.fields == ('hospital', 'hospital_medicine_no'):
                    validator.message = '医院不可重复关联同一医院药品编码;'
            return validators
只对admin、form有效, 对serializer无效的方法:

class MedicineEscape(models.Model):
    ....
    def unique_error_message(self, model_class, unique_check):
    """只对admin、form有效 对serializer无效"""
    if model_class == type(self) and unique_check == ('hospital', 'hospital_medicine_name'):
        return '{}不可重复关联{}'.format(self.hospital.name, self.hospital_medicine_name)
    else:
        return super(MedicineEscape, self).unique_error_message(model_class, unique_check)

View相关

获取url参数

请求url为http://192.168.1.108/discount_coupon/?name=xxx

方法一: name = self.request.query_params.get('name')

方法二: name = self.request.GET.get('name')

str转dict方法

import ast

def literal_eval(data):
    """
    :param data: str或者dict
    :return: dict or None
    """
    if isinstance(data, dict):
        return data
    else:
        try:
            return ast.literal_eval(data)
        except (SyntaxError, ValueError):
            return None

get_list适配

from django.http import QueryDict

def get_list(_dict, key):
    """
    :param _dict: QueryDict or dict
    :param key: list 键名
    :return: _dict中key对应的list
    """
    if isinstance(_dict, QueryDict):
        return _dict.getlist(key)
    else:
        return _dict[key]

用法:get_list(request.data, 'ids')

客户端向服务器发起请求时,服务器接收到的request.data可能是dict也可能是QueryDict,不同类型时取list时方法不同。前端使用Ajax时需要设置traditional: true,traditional为false,即jquery会深度序列化参数对象,以适应如PHP和Ruby on Rails框架,但DRF无法处理,我们可以通过设置traditional为true阻止深度序列化。

向request.data中添加数据

  1. data = request.data.copy() 开销大,不友好

  2. request.data._mutable = True 推荐使用,设置可修改标志位True

queryset懒加载

class UserList(ModelViewSet):
    queryset = User.objects.all()

queryset对象只会在程序初始化的时候去数据库取一次值,之后使用的结果均为缓存值,如程序启动时queryset内有【用户1】,后面新增了【用户2】,再次去取该queryset对象依旧只有【用户1】。

想要获取到实时的数据库queryset,需要调用queryset.all()方法来重新至数据库读取数据。ModelViewSet的默认get_queryset方法如下:

def get_queryset(self):
    """
    Get the list of items for this view.
    This must be an iterable, and may be a queryset.
    Defaults to using `self.queryset`.

    This method should always be used rather than accessing `self.queryset`
    directly, as `self.queryset` gets evaluated only once, and those results
    are cached for all subsequent requests.

    You may want to override this if you need to provide different
    querysets depending on the incoming request.

    (Eg. return a list of items that is specific to the user)
    """
    assert self.queryset is not None, (
        "'%s' should either include a `queryset` attribute, "
        "or override the `get_queryset()` method."
        % self.__class__.__name__
    )

    queryset = self.queryset
    if isinstance(queryset, QuerySet):
        # Ensure queryset is re-evaluated on each request.
        queryset = queryset.all()
    return queryset
可见:queryset = queryset.all() # Ensure queryset is re-evaluated on each request.

DRF view doc

Auth相关

开启SessionAuthentication时403

DRF在开启SessionAuthentication时会导致权限设置成AllowAny时依旧会出现403返回,详情为CSRF Failed: CSRF token missing or incorrent。

删除SessionAuthentication只使用TokenAuthentication即可,但是Django默认使用session进行认证,这将导致登陆后台成功后无法浏览DRF的可视化API(browsable api),

使用chrome插件ModHeader即可将浏览器请求加入token头部浏览可视化API来解决此问题。

列表相关

多重条件过滤、排序、搜索

settings.py

REST_FRAMEWORK = {
    'DEFAULT_FILTER_BACKENDS': (
        # 多重条件过滤
        'django_filters.rest_framework.DjangoFilterBackend',
        # 排序
        'rest_framework.filters.OrderingFilter',
        # 搜索
        'rest_framework.filters.SearchFilter'
    ),
    'ORDERING_PARAM': 'ordering',
    'SEARCH_PARAM': 'search',
}

filters.py

from django_filters.rest_framework import FilterSet

class UserFilter(FilterSet):
    class Meta:
        model = User
        fields = {
            'username': ['exact', 'icontains'],
            'tel': ['exact', 'icontains', 'isnull']
        }

views.py

class EmailViewSet(viewsets.ModelViewSet):
    queryset = Email.objects.all()
    serializer_class = EmailCreateSerializer
    permission_classes = (IsAuthenticated,)
    filter_class = UserFilter
    ordering_fields = '__all__'
    search_fields = ('name', )

用法:

过滤:http://192.168.1.108/email/?name=GG&id=2

排序:http://192.168.1.108/email/?ordering=-name,id

搜索:http://192.168.1.108/email/?search=GG

order_by函数会按照传递传输的顺序进行排序,如传递(name, id),意思为在name相同的情况下按照id自增的顺序进行排列,后面的条件只有在前面的条件相同的情况下才会生效。

分页相关

动态设置分页中每页个数 动态开关分页

# 分页
class Pagination(PageNumberPagination):
    # django_paginator_class = DjangoPaginator
    page_number = 1
    page_size_query_param = 'page_size'
    max_page_size = 100

    def paginate_queryset(self, queryset, request, view=None):
        """
        使用page_size参数限制每页条数
        超出页码范围返回第一页
        最后一页页码用last表示
        """
        page_size = self.get_page_size(request)
        if not page_size:
            return None

        paginator = self.django_paginator_class(queryset, page_size)
        self.page_number = request.query_params.get(self.page_query_param, 1)
        if self.page_number in self.last_page_strings:
            self.page_number = paginator.num_pages

        try:
            self.page = paginator.page(self.page_number)
        except InvalidPage:
            # 非法页码返回第一页
            self.page_number = '1'
            self.page = paginator.page(self.page_number)

        if paginator.num_pages > 1 and self.template is not None:
            # The browsable API should display pagination controls.
            self.display_page_controls = True

        self.request = request
        return list(self.page)

    def get_page_size(self, request):
        """
        page_size > 0 使用新page_size
        page_size = 0 时不分页
        page_size < 0 时使用默认page_size
        """
        if self.page_size_query_param:
            page_size = min(int(request.query_params.get(self.page_size_query_param, self.page_size)),
                            self.max_page_size)
            if page_size > 0:
                return page_size
            elif page_size == 0:
                return None
            else:
                pass
        return self.page_size
用法:

每页5条:get https://www.example.com/post/?page_size=5

不开启分页:get https://www.example.com/post/?page_size=0

重写分页返回结果

class Pagination(PageNumberPagination):
    # django_paginator_class = DjangoPaginator

    def get_paginated_response(self, data):
        return Response(OrderedDict([
            ('count', self.page.paginator.count),
            ('num_pages', self.page.paginator.num_pages),
            ('next', self.get_next_link()),
            ('previous', self.get_previous_link()),
            ('results', data)
        ]))

Django1.11.1 Paginator新增UnorderedObjectListWarning检测

Q:Django1.11.1 Paginator新增UnorderedObjectListWarning检测,要求DRF的queryset或者get_queryset之后进行排序 以防止多次分页得到不同的结果

A:

方法一:

from django.core.paginator import Paginator

# 屏蔽UnorderedObjectListWarning
class DjangoPaginator(Paginator):
    def _check_object_list_is_ordered(self):
        """
        Warn if self.object_list is unordered (typically a QuerySet).
        Pagination may yield inconsistent results with an unordered
        """
        pass

# 分页
class Pagination(PageNumberPagination):
    # django_paginator_class = DjangoPaginator

方法二:

没有重写get_queryset方法的ModelViewSet(添加了order_by):

queryset = MessageTemplate.objects.all().order_by('id')

重写了get_queryset方法的ModelViewSet(添加了order_by):

def get_queryset(self):
    queryset = MessageTemplate.objects.filter(Q(publisher=self.request.user, is_private=True) |
                                              Q(is_private=False)).order_by('id')

​ return queryset

权限相关

ViewSet model权限

class DRFModelPermissions(permissions.DjangoModelPermissions):
    """
    Similar to `DjangoModelPermissions`, but adding 'view' permissions.
    """
    perms_map = {
        'GET': ['%(app_label)s.view_%(model_name)s'],
        'OPTIONS': ['%(app_label)s.view_%(model_name)s'],
        'HEAD': ['%(app_label)s.view_%(model_name)s'],
        'POST': ['%(app_label)s.add_%(model_name)s'],
        'PUT': ['%(app_label)s.change_%(model_name)s'],
        'PATCH': ['%(app_label)s.change_%(model_name)s'],
        'DELETE': ['%(app_label)s.delete_%(model_name)s'],
    }

使用list_router和detail_router时,会根据访问的方法类型要求对应的权限。

ViewSet model权限 指定model

class DRFCertainModelPermissions(DRFModelPermissions):
    """
    Certain model with permission_model in ViewSet for DRFModelPermissions
    """

    def get_model(self, view):
        if not hasattr(view, 'permission_model'):
            raise ImproperlyConfigured('permission_model must be override in ViewSet which used '
                                       'DRFCertainModelPermissions.')
        return getattr(view, 'permission_model')

    def has_permission(self, request, view):
        # Workaround to ensure DjangoModelPermissions are not applied
        # to the root view when using DefaultRouter.
        if getattr(view, '_ignore_model_permissions', False):
            return True

        if not request.user or (
                not request.user.is_authenticated and self.authenticated_users_only):
            return False

        perms = self.get_required_permissions(request.method, self.get_model(view))
        return request.user.has_perms(perms)
用法:

class LabelPrinterViewSet(ModelViewSet):
    queryset = LabelPrinter.objects.all()
    permission_classes = (DRFCertainModelPermissions,)
    permission_model = Label

则可以让LabelPrinterViewSet的CRUD对应使用Label的CRUD权限。

ViewSet model权限 假删除

假删除时,删除实际上为作废,调整删除需要的权限为修改权限即可。

class DRFModelDeleteAsChangePermissions(permissions.DjangoModelPermissions):
    """
    Similar to `DjangoModelPermissions`, but adding 'view' permissions.
    Delete action use same permissions as PUT/PATCH action.
    """
    perms_map = {
        'GET': ['%(app_label)s.view_%(model_name)s'],
        'OPTIONS': ['%(app_label)s.view_%(model_name)s'],
        'HEAD': ['%(app_label)s.view_%(model_name)s'],
        'POST': ['%(app_label)s.add_%(model_name)s'],
        'PUT': ['%(app_label)s.change_%(model_name)s'],
        'PATCH': ['%(app_label)s.change_%(model_name)s'],
        'DELETE': ['%(app_label)s.change_%(model_name)s'],
    }

ViewSet中permission_classes检查从左到右依次进行,需要将前提权限类放在最前面。

permission_classes = (DRFModelPermissions, IsAuthenticated,)
permission_classes = (IsAuthenticated, DRFModelPermissions, )

前一种情况会导致未登陆的用户可以访问到所有接口,后一种情况才能够真正限制用户必须登陆并且具有模型权限。因为IsAuthenticated要求用户登陆,DRFModelPermissions判断登陆用户是否有对应的模型权限,其前置条件为要求用户为登陆用户。

动态权限检查

方法1(自己写的 不推荐 会导致所有接口调用前多一次不必要的权限对象是类还是实例的判断):

 class ModelViewSet: 
     ...
     def get_permissions(self):
        """
        Instantiates and returns the list of permissions that this view requires.
        兼容实例或类
        """
        permissions = []
        for permission in self.permission_classes:
            if isinstance(permission, type):
                # 类
                permissions.append(permission())
            else:
                # 实例
                permissions.append(permission)
        return permissions
permissions.py:

class DynamicPermsClass(permissions.BasePermission):
    """
    动态权限检查 需要调整ModelViewSet中get_permissions方法兼容
    usage: DynamicPermsClass(['app_label.action_model', ])
    """
    permissions = []

    def __init__(self, *args, **kwargs):
        self.permissions = args[0] if len(args) > 0 else []
        super(DynamicPermsClass, self).__init__()

    def has_permission(self, request, view):
        print('has_permission', self.permissions)
        if request.user and request.user.is_authenticated and request.user.has_perms(self.permissions):
            return True
        else:
            return False
views.py:

@action(methods=['GET'], detail=False, serializer_class=None,
        permission_classes=[DynamicPermsClass(['demo.view_demo']), ])
def demo(self, request, *args, **kwargs):

方法2(由django-rest-framework-rules改造而来):

mixins.py:

class PermissionRequiredMixin(object):
    """
    源于django-rest-framework-rules 用于自定义的action方法
    usage:
    @action(methods=['GET'], detail=False, permission_required='prescription.view_patient') or
    @action(methods=['GET'], detail=False, permission_required=['prescription.view_patient', 'prescription.view_doctor'])
    """
    permission_required = None

    def get_permission_required(self):
        if self.permission_required is None:
            # This prevents a misconfiguration of the view into which the mixin
            # is mixed. If the mixin is used, at least one permission should be
            # required.
            raise ImproperlyConfigured(
                '{0} is missing the permission_required attribute. Define '
                '{0}.permission_required, or override '
                '{0}.get_permission_required().'.format(self.__class__.__name__)
            )
        if isinstance(self.permission_required, str):
            perms = (self.permission_required,)
        else:
            perms = self.permission_required
        return perms

    def check_permissions(self, request):
        if request.user.has_perms(self.get_permission_required()):
            return super(PermissionRequiredMixin, self).check_permissions(request)
        else:
            self.permission_denied(request, message='您没有执行该操作的权限')
decorators.py:

def permission_required(permissions):
    """
    源自源于django-rest-framework-rules 用于create、update、destroy 等内置方法
    usage:
    @permission_required('demo.add_demo')
    def create(self, request, *args, **kwargs)
    """

    def decorator(view):

        def wrapped_view(self, request, *args, **kwargs):
            if isinstance(permissions, str):
                perms = (permissions,)
            else:
                perms = permissions

            if not request.user.has_perms(perms):
                # raises a permission denied exception causing a 403 response
                self.permission_denied(request, message='您没有执行该操作的权限(特殊权限检查未通过)')
            return view(self, request, *args, **kwargs)

        return wrapped_view

    return decorator

文档生成

安装

需要安装coreapi库用于文档生成。注释语法使用markdown语法,需要安装markdown库。

from rest_framework.documentation import include_docs_urls

urlpatterns = [
    ...
    url(r'^docs/', include_docs_urls(title='My API title'))
]

注释方法

class UserViewSet(viewsets.ModelViewSet):
    """
    retrieve:
    Return the given user.

    list:
    Return a list of all the existing users.

    create:
    Create a new user instance.
    """

@list_route(methods=['POST'], permission_classes=[AllowAny], serializer_class=None)
def refresh_token(self, request):
    """
     刷新令牌

【接收】token: 令牌


【返回】200 新令牌 400-1 数据格式错误 400-2 令牌错误
    """

个性化

表格描述改为serializer中field的label,并加入类型,为ChoiceField时列出选项内容: 修改rest_framework/schema/inspectors.py中的field_to_schema方法:

description = '【{}{}'.format(field.__class__.__name__, force_text(field.label) if field.label else '')
# ChoiceField 列出选项内容
if isinstance(field, ChoiceField):
    description += ' 【选项】'
    for key, value in field.choices.items():
        description += ' {}-{}'.format(key, value)

左侧导航修改为中文 修改 rest_framework/schema/generators.py SchemaGenerator类中的get_keys方法:

# 修改user->用户
named_path_components = [
    component for component in subpath.strip('/').split('/') if '{' not in component
]
named_path_components[0] = view.queryset.model._meta.verbose_name.title()

加入自定义方法支持 修改 rest_framework/schema/generators.py中的is_custom_action方法为:

def is_custom_action(action):
    return action not in {
        'retrieve', 'list', 'create', 'update', 'partial_update', 'destroy', 'bulk_update', 'bulk_destroy'
    }

最终生成文档效果:

Documenting your API

其他

区分路径类型

以/开头的路径是绝对路径,否则会默认取相对路径。

Router默认base_name

以viewset中的queryset参数作为默认base_name;