久经考验的 django rest framework 项目模板
[TOC] 感谢 github TOC 生成器:https://ecotrust-canada.github.io/markdown-toc/
- 脚本位置:
code/scripts/generate_crud_code.py
- 脚本的 run 方法:
def run(*args): if len(args) > 0: model_path = args[0] else: model_path = 'generate_crud_code_example.models.Book' code_generator = CrudCodeGenerator(model_path) # 生成代码到临时目录 # code_generator.generate_to_temp_dir() # 生成代码到指定目录 code_generator.generate_to_target_apis_dir( os.path.join(project_root_dir, 'apps/generate_crud_code_example/apis'), 'apps.generate_crud_code_example.apis', is_regenerate=True, )
- 执行完上述脚本后,会在
code/apps/generate_crud_code_example
目录下生成 api 模板代码,根据业务场景再针对性修改即可:. ├── filters │ └── book.py ├── schemas │ └── book.py ├── serializers │ └── book.py └── views └── book.py
- 代码位置:
code/utils/base_class.py
- 比如附带逻辑删除以及创建时间、修改时间的 model:
class LogicDeleteQuerySet(models.QuerySet):
def delete(self): # queryset 的逻辑删除方法
# return self.update(is_deleted=True, deleted_at=timezone.now())
return self.update(deleted_at=timezone.now())
class LogicDeleteManager(models.manager.BaseManager.from_queryset(LogicDeleteQuerySet)):
pass
class LogicDeleteModel(models.Model):
# is_delete = models.BooleanField('删除标记', default=False, editable=False)
deleted_at = models.DateTimeField('删除时间', null=True, db_index=True)
class Meta:
abstract = True
def delete(self, using=None, keep_parents=False): # 单个实例的逻辑删除方法
# self.is_delete = True
self.deleted_at = timezone.now()
# self.save(update_fields=['is_delete', 'deleted_at'])
self.save(update_fields=['deleted_at'])
class NotDeleteManager(LogicDeleteManager):
def get_queryset(self):
# return super().get_queryset().filter(is_delete=False)
return super().get_queryset().filter(deleted_at__isnull=True)
objects = NotDeleteManager()
all_objects = models.Manager() # 需要查找被逻辑删除的数据时使用这个 all_objects
- 代码位置:
code/utils/database.py
- 实际开发中会经常用到这种写法:目的是仅获取查询结果集的 id 集合
上面那个文件中简单封装了这种写法:
target_user_id_set = set(User.objects.all().values_list('id', flat=True))
此时一开始的代码就可以写成:def values_list_ids(queryset: QuerySet) -> QuerySet: return queryset.values_list('id', flat=True) def values_list_ids_set(queryset: QuerySet) -> set: return set(values_list_ids(queryset)) ids_set = values_list_ids_set
target_user_id_set = ids_set(User.objects.all())
- 代码位置:
code/utils/date_and_time.py
- 支持年月日、年月日时分秒、秒级时间戳、毫秒级时间戳转datetime:
def str2datetime(s: str, format='%Y-%m-%d') -> Union[datetime, None]: """ 日期型字符转datetime """ if isinstance(s, datetime): return s try: return datetime.strptime(s, format) except: pass try: return datetime.strptime(s, '%Y-%m-%d %H:%M:%S') except: pass try: return datetime.fromtimestamp(int(s)) except: pass try: return datetime.fromtimestamp(int(s) / 1000) except: pass return None
- 代码位置:
code/utils/excel.py
,转为 BytesIO 后,可以选择保存为文件,或者作为接口响应返回 - 使用 xlwt 的方法:
generate_excel_io
,xlwt 支持保存为 xls、xlsx - 使用 openpyxl 的方法:
generate_excel_io_by_openpyxl
,openpyxl 只支持保存为 xlsx
- 代码位置:
code/utils/image.py
- 修改图片尺寸:resize_image
- 压缩图片并修改尺寸:compress_and_resize_image
- 切图图片白边:crop_image_margin
- 代码位置:
code/utils/middlewares.py
直接使用第三方库:djangorestframework-camel-case
,配置一下在 settings 的 MIDDLEWARE,这个库只会对 body 中的参数进行转换,我写了一个对驼峰形式 get 参数转下划线的中间件:GetParamsCamelCaseMiddleware
CodeMessageDataMiddleware
,这个中间件可能会影响某些第三方库的行为以及异常,比如 drf_extensions 的 cache 装饰器会报错,可以自行适配一下,我适配后的代码在code/utils/drf_extensions/cache/decorators.py
。- 比起强行修改 ViewSet 的响应数据(比如封装一个自己的 ViewSet,实现 code message data 的响应),我更喜欢这种中间件可插拔形式的做法
- 代码位置:
code/utils/mixins.py
class SerializerMixin:
def get_serializer_class(self):
"""
让 ViewSet 支持以下写法,而不是serializer_class(这段代码来自 jumpserver 源码
serializer_classes = {
'default': serializers.AssetUserWriteSerializer,
'list': serializers.AssetUserReadSerializer,
'retrieve': serializers.AssetUserReadSerializer,
}
"""
serializer_class = None
if hasattr(self, 'serializer_classes') and isinstance(self.serializer_classes, dict):
serializer_class = self.serializer_classes.get(self.action, self.serializer_classes.get('default'))
if serializer_class:
return serializer_class
return super().get_serializer_class()
def get_request_serializer(self, *args, **kwargs):
"""
校验请求数据并返回请求serializer
"""
if 'data' not in kwargs:
kwargs['data'] = self.request.data
serializer = self.get_serializer(*args, **kwargs)
serializer.is_valid(raise_exception=True)
return serializer
然后常用的写法长这样:
# user.apis.views.UserViewSet
class UserViewSet(SerializerMixin, PermissionMixin, viewsets.GenericViewSet):
serializer_classes = {
'default': UserMeSerializer,
'me': UserMeSerializer,
'create_or_update_profile': UserProfileCreateOrUpdateSerializer
}
@action(detail=False, methods=['POST'])
def create_or_update_profile(self, request):
req_serializer: UserProfileCreateOrUpdateSerializer = self.get_request_serializer()
user_profile = req_serializer.save()
res_serializer = UserProfileDisplaySerializer(user_profile)
return Response(res_serializer.data)
权限检查同理,也有一个 PermissionMixin
- 代码位置:
code/utils/paginations.py
- 代码位置:
code/utils/request.py
,这个函数需要 nginx 配合,否则会有漏洞(比如请求头的 X_FORWARDED_FOR 是可以客户端伪造的)# nginx.conf proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr;
- 代码位置:
code/utils/response.py
- 使用的是 xlrd 库,返回了 xls 文件,这里存在优化空间,比如返回 xlsx,以及将 xlrd 换成 openpyxl。
- 代码位置:
utils.serializers.QuerySerializer
- 因为 http 的 get 方法存在一些弊端,这里封装了一个 QuerySerializer,具体细节和用法可以读一下代码(个人觉得还不够完善)。
- 代码位置:
code/utils/strings.py
- 字符串、float 类型转 Decimal:
str2decimal
- 下划线转驼峰:
underline_2_hump
- 代码位置:
code/apps/django_socio_grpc_quickstart
- 第三方库:django-socio-grpc
- Dockerfile 文件位置:
code/Dockerfile
- 为了提高镜像构建速度以及节约镜像层开销,建议自己构建一个基础镜像,替换 Dockerfile 中的
YourBaseDockerImage:last
,基础镜像的写法可以参考我的另一个代码仓库:https://github.com/ChouBaoDxs/PublicDockerfile/tree/zjkj/centos-py365
,这个 Dockerfile 使用的是 centos7 以及 python 3.6.5
为了让 Docker 镜像的使用更加灵活,支持部署 django、celery 以及 runscript,通过在 entrypoint.sh
中判断环境变量,决定镜像执行的命令。
#!/usr/bin/env bash
MODE=${MODE:-django}
cmd=""
if [ $MODE = 'django' ]; then
mkdir -p /data/logs/uwsgi
cmd="/usr/local/python3/bin/uwsgi --ini uwsgi.ini"
elif [ $MODE = 'celery_beat' ]; then
cmd="/usr/local/python3/bin/celery beat -A drf_template -l info"
elif [ $MODE = 'celery_worker_queue_default' ]; then
cmd="/usr/local/python3/bin/celery worker -A drf_template -l info -Q default"
elif [ $MODE = 'runscript' ]; then
runscript_name=${runscript_name:-runscript_name}
cmd="/usr/local/python3 manage.py runscript $runscript_name --traceback"
fi