initial commit
This commit is contained in:
0
utils/__init__.py
Normal file
0
utils/__init__.py
Normal file
BIN
utils/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
utils/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/__init__.cpython-38.pyc
Normal file
BIN
utils/__pycache__/__init__.cpython-38.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/chen_crud.cpython-313.pyc
Normal file
BIN
utils/__pycache__/chen_crud.cpython-313.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/chen_crud.cpython-38.pyc
Normal file
BIN
utils/__pycache__/chen_crud.cpython-38.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/chen_ninja.cpython-313.pyc
Normal file
BIN
utils/__pycache__/chen_ninja.cpython-313.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/chen_ninja.cpython-38.pyc
Normal file
BIN
utils/__pycache__/chen_ninja.cpython-38.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/chen_pagination.cpython-313.pyc
Normal file
BIN
utils/__pycache__/chen_pagination.cpython-313.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/chen_pagination.cpython-38.pyc
Normal file
BIN
utils/__pycache__/chen_pagination.cpython-38.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/chen_response.cpython-313.pyc
Normal file
BIN
utils/__pycache__/chen_response.cpython-313.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/chen_response.cpython-38.pyc
Normal file
BIN
utils/__pycache__/chen_response.cpython-38.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/codes.cpython-313.pyc
Normal file
BIN
utils/__pycache__/codes.cpython-313.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/codes.cpython-38.pyc
Normal file
BIN
utils/__pycache__/codes.cpython-38.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/models.cpython-313.pyc
Normal file
BIN
utils/__pycache__/models.cpython-313.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/models.cpython-38.pyc
Normal file
BIN
utils/__pycache__/models.cpython-38.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/path_utils.cpython-313.pyc
Normal file
BIN
utils/__pycache__/path_utils.cpython-313.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/path_utils.cpython-38.pyc
Normal file
BIN
utils/__pycache__/path_utils.cpython-38.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/util.cpython-313.pyc
Normal file
BIN
utils/__pycache__/util.cpython-313.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/util.cpython-38.pyc
Normal file
BIN
utils/__pycache__/util.cpython-38.pyc
Normal file
Binary file not shown.
BIN
utils/chapter_tools/__pycache__/csx_chapter.cpython-313.pyc
Normal file
BIN
utils/chapter_tools/__pycache__/csx_chapter.cpython-313.pyc
Normal file
Binary file not shown.
BIN
utils/chapter_tools/__pycache__/csx_chapter.cpython-38.pyc
Normal file
BIN
utils/chapter_tools/__pycache__/csx_chapter.cpython-38.pyc
Normal file
Binary file not shown.
18
utils/chapter_tools/csx_chapter.py
Normal file
18
utils/chapter_tools/csx_chapter.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from apps.project.models import Round
|
||||
from utils.util import get_dict_info
|
||||
|
||||
def create_csx_chapter_dict(one_round: Round):
|
||||
"""传入轮次对象,返回测试项类型字典key的数组/测试项key的dict,以便后续使用"""
|
||||
testType_list = []
|
||||
last_chapter_items = {}
|
||||
if one_round:
|
||||
for csx in one_round.rtField.all():
|
||||
if csx.testType not in testType_list:
|
||||
testType_list.append(csx.testType)
|
||||
# 排序需要查字典里面index来排序
|
||||
testType_list.sort(key=lambda x: get_dict_info('testType', x)['index'], reverse=False)
|
||||
for test_type in testType_list:
|
||||
last_chapter_items[test_type] = []
|
||||
for csx in one_round.rtField.all():
|
||||
last_chapter_items[csx.testType].append(csx.key)
|
||||
return testType_list, last_chapter_items
|
||||
92
utils/chen_crud.py
Normal file
92
utils/chen_crud.py
Normal file
@@ -0,0 +1,92 @@
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
# 1.1)这个函数参数1:request、参数2:schema或schema.dict()、参数3:ORM模型
|
||||
def create(request, data, model):
|
||||
if not isinstance(data, dict):
|
||||
data = data.dict()
|
||||
query_set = model.objects.create(**data)
|
||||
return query_set
|
||||
|
||||
# 1.2)新增快捷函数,直接填入data
|
||||
def createWithOutRequestParam(data, model):
|
||||
if not isinstance(data, dict):
|
||||
data = data.dict()
|
||||
query_set = model.objects.create(**data)
|
||||
return query_set
|
||||
|
||||
# 2.1)更新便捷函数,特别注意参数0为request后面有用,参数1:id,参数2:schema,参数3:ORM模型
|
||||
def update(request, id, data, model):
|
||||
dict_data = data.dict()
|
||||
instance = get_object_or_404(model, id=id)
|
||||
for attr, value in dict_data.items():
|
||||
setattr(instance, attr, value)
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
# 2.2)更新便捷函数-无request参数 -> data参数为schema对象
|
||||
def updateWithoutRequestParam(id, data, model):
|
||||
dict_data = data.model_dump(exclude_none=True)
|
||||
instance = get_object_or_404(model, id=id)
|
||||
for attr, value in dict_data.items():
|
||||
if attr != 'id': # 不对id更新
|
||||
setattr(instance, attr, value)
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
# 多个id删除便捷函数,参数1:ids数组,参数2:ORM模型
|
||||
def multi_delete(ids, model):
|
||||
for item in ids:
|
||||
# 删除多对多关系 - project
|
||||
instance = get_object_or_404(model, pk=item)
|
||||
instance.delete()
|
||||
pass
|
||||
|
||||
# project删除,对应problem问题单多对多关系也要删除
|
||||
def multi_delete_project(ids, model):
|
||||
idents = []
|
||||
for item in ids:
|
||||
instance = get_object_or_404(model, pk=item)
|
||||
# (注意:project所属problem全部删除,且关联关系删除)
|
||||
for problem in instance.projField.all():
|
||||
problem.case.clear()
|
||||
idents.append(instance.ident)
|
||||
instance.delete()
|
||||
return idents
|
||||
|
||||
# testDemand多个id删除便捷函数,参数1:ids数组,参数2:ORM模型
|
||||
def multi_delete_testDemand(ids, model):
|
||||
for item in ids:
|
||||
instance = get_object_or_404(model, pk=item)
|
||||
# (多对多删除)case下面的problem关联删除
|
||||
for case in instance.tcField.all():
|
||||
case.caseField.clear()
|
||||
instance.delete()
|
||||
pass
|
||||
|
||||
# dut的删除,需要多对多case-problem
|
||||
def multi_delete_dut(ids, model):
|
||||
for item in ids:
|
||||
instance = get_object_or_404(model, pk=item)
|
||||
# (多对多删除)case下面的problem关联删除
|
||||
for case in instance.ducField.all():
|
||||
case.caseField.clear()
|
||||
instance.delete()
|
||||
pass
|
||||
|
||||
# design的多个id删除便捷函数,参数1:ids数组,参数2:ORM模型
|
||||
def multi_delete_design(ids, model):
|
||||
for item in ids:
|
||||
instance = get_object_or_404(model, pk=item)
|
||||
# (多对多删除)case下面problem关联删除
|
||||
for case in instance.dcField.all():
|
||||
case.caseField.clear()
|
||||
instance.delete()
|
||||
pass
|
||||
|
||||
# 由于case有多对多关系,所以单独提出来删除
|
||||
def multi_delete_case(ids, model):
|
||||
for item in ids:
|
||||
instance = get_object_or_404(model, pk=item)
|
||||
instance.caseField.clear()
|
||||
instance.delete()
|
||||
pass
|
||||
45
utils/chen_ninja.py
Normal file
45
utils/chen_ninja.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from ninja_extra import NinjaExtraAPI
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from typing import Any
|
||||
from utils.codes import HTTP_USER_PASSWORD_ERROR_CODE
|
||||
|
||||
# 重写ninja返回 - 全局统一视图函数返回,如果None则如下返回
|
||||
class ChenNinjaAPI(NinjaExtraAPI):
|
||||
def create_response(
|
||||
self, request: HttpRequest, data: Any, *, status: int = 200, code: int = 200, message: str = "请求成功",
|
||||
temporal_response: HttpResponse = None,
|
||||
) -> HttpResponse:
|
||||
std_data = {
|
||||
"code": code,
|
||||
"data": data,
|
||||
"message": message,
|
||||
"success": True
|
||||
}
|
||||
# 因为extra的APIException会添加到data['detail']里面的,所以做一致性处理转换
|
||||
if std_data['data'] is not None:
|
||||
if 'detail' in std_data['data']:
|
||||
std_data['message'] = std_data['data']['detail']
|
||||
# ~~~~~~~~~~正常异常,status进行通用处理:TODO:后续规划~~~~~~~~~~
|
||||
# 1.当状态码为403时候,给前端提示message
|
||||
if status == 403:
|
||||
std_data['message'] = '您没有权限这样做'
|
||||
# 2.当状态码为401时候,根据APIException处理
|
||||
elif status == 401:
|
||||
if (std_data['data']['detail'] == '找不到指定凭据对应的有效用户' or
|
||||
std_data['data']['detail'] == 'No active account found with the given credentials'):
|
||||
std_data['message'] = '账号或密码错误,如果是内网登录检查密码是否过期...'
|
||||
std_data['data']['code'] = HTTP_USER_PASSWORD_ERROR_CODE # TODO:后续单独以枚举方式定义code
|
||||
else:
|
||||
std_data['message'] = '您的token已过期,请重新登录'
|
||||
# 3.因为前端是获取message,所以这里处理
|
||||
elif status != 200 and std_data['message'] == '请求成功':
|
||||
std_data['message'] = '请求失败,请检查'
|
||||
elif status == 404:
|
||||
std_data['message'] = '未找到相应的内容,请检查参数'
|
||||
elif status == 422:
|
||||
std_data['message'] = '请求的参数或响应数据未通过验证'
|
||||
content = self.renderer.render(request, std_data, response_status=status)
|
||||
content_type = "{}; charset={}".format(
|
||||
self.renderer.media_type, self.renderer.charset
|
||||
)
|
||||
return HttpResponse(content, status=status, content_type=content_type)
|
||||
34
utils/chen_pagination.py
Normal file
34
utils/chen_pagination.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from django.db.models import QuerySet
|
||||
from ninja.pagination import PaginationBase
|
||||
from ninja.types import DictStrAny
|
||||
from ninja_schema import Schema
|
||||
from ninja import Field
|
||||
from typing import List,Any
|
||||
|
||||
# 这个结构是前端要求的,看前端代码可知
|
||||
class Total(Schema):
|
||||
total:int
|
||||
|
||||
class MyPagination(PaginationBase):
|
||||
class Input(Schema):
|
||||
pageSize: int = Field(10, gt=0)
|
||||
page: int = Field(1, gt=-1)
|
||||
|
||||
class Output(Schema):
|
||||
items: List[Any]
|
||||
pageInfo: Total
|
||||
|
||||
def paginate_queryset(
|
||||
self,
|
||||
queryset: QuerySet,
|
||||
pagination: Input,
|
||||
**params: DictStrAny,
|
||||
) -> Any:
|
||||
offset = pagination.pageSize * (pagination.page - 1)
|
||||
limit: int = pagination.pageSize
|
||||
return {
|
||||
"page": offset,
|
||||
"limit": limit,
|
||||
"items": queryset[offset: offset + limit],
|
||||
"pageInfo": {'total':self._items_count(queryset)},
|
||||
} # noqa: E203
|
||||
25
utils/chen_response.py
Normal file
25
utils/chen_response.py
Normal file
@@ -0,0 +1,25 @@
|
||||
import json
|
||||
import datetime
|
||||
from django.http import HttpResponse
|
||||
|
||||
# 返回的时间有可能无法序列化,这里单独处理
|
||||
class DateEncoder(json.JSONEncoder):
|
||||
def default(self, obj):
|
||||
if isinstance(obj, datetime.datetime):
|
||||
return obj.strftime("%Y-%m-%d")
|
||||
else:
|
||||
return json.JSONEncoder.default(self, obj)
|
||||
|
||||
## 重写django的HttpResponse,注意返回的是axios里面的data!!!
|
||||
class ChenResponse(HttpResponse):
|
||||
def __init__(self, data=None, message='success', code=200, *args, **kwargs):
|
||||
if data is None:
|
||||
data = {}
|
||||
std_data = {
|
||||
"code": code,
|
||||
"data": data,
|
||||
"message": message,
|
||||
"success": True
|
||||
}
|
||||
data = json.dumps(std_data, cls=DateEncoder)
|
||||
super().__init__(data, *args, **kwargs)
|
||||
7
utils/codes.py
Normal file
7
utils/codes.py
Normal file
@@ -0,0 +1,7 @@
|
||||
# 确定响应codes的枚举类型
|
||||
# 1.账号账户和密码错误的code -> status:部分401
|
||||
HTTP_USER_PASSWORD_ERROR_CODE: int = 40001
|
||||
# 2.传给后端的数据越界
|
||||
HTTP_INDEX_ERROR: int = 40038
|
||||
# 3.当右键测试项时,如果测试项下面已经有用例了
|
||||
HTTP_EXISTS_CASES:int = 40031
|
||||
BIN
utils/log_util/__pycache__/middleware.cpython-313.pyc
Normal file
BIN
utils/log_util/__pycache__/middleware.cpython-313.pyc
Normal file
Binary file not shown.
BIN
utils/log_util/__pycache__/middleware.cpython-38.pyc
Normal file
BIN
utils/log_util/__pycache__/middleware.cpython-38.pyc
Normal file
Binary file not shown.
BIN
utils/log_util/__pycache__/request_util.cpython-313.pyc
Normal file
BIN
utils/log_util/__pycache__/request_util.cpython-313.pyc
Normal file
Binary file not shown.
BIN
utils/log_util/__pycache__/request_util.cpython-38.pyc
Normal file
BIN
utils/log_util/__pycache__/request_util.cpython-38.pyc
Normal file
Binary file not shown.
90
utils/log_util/middleware.py
Normal file
90
utils/log_util/middleware.py
Normal file
@@ -0,0 +1,90 @@
|
||||
"""
|
||||
日志中间件
|
||||
"""
|
||||
import json
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.utils.deprecation import MiddlewareMixin
|
||||
from apps.system.models import OperationLog
|
||||
from apps.user.models import Users
|
||||
from utils.log_util.request_util import (
|
||||
get_browser,
|
||||
get_os,
|
||||
get_request_data,
|
||||
get_request_ip,
|
||||
get_request_path,
|
||||
)
|
||||
|
||||
class ApiLoggingMiddleware(MiddlewareMixin):
|
||||
"""
|
||||
用于记录API访问日志中间件
|
||||
"""
|
||||
|
||||
def __init__(self, get_response=None):
|
||||
super().__init__(get_response)
|
||||
self.enable = getattr(settings, 'API_LOG_ENABLE', None) or False
|
||||
self.methods = getattr(settings, 'API_LOG_METHODS', None) or set()
|
||||
self.operation_log_id = None
|
||||
|
||||
@classmethod
|
||||
def __handle_request(cls, request):
|
||||
request.request_ip = get_request_ip(request)
|
||||
request.request_data = get_request_data(request)
|
||||
request.request_path = get_request_path(request)
|
||||
|
||||
def __handle_response(self, request, response):
|
||||
# request_data,request_ip由PermissionInterfaceMiddleware中间件中添加的属性
|
||||
body = getattr(request, 'request_data', {})
|
||||
# 请求含有password则用*替换掉(暂时先用于所有接口的password请求参数)
|
||||
if isinstance(body, dict) and body.get('password', ''):
|
||||
body['password'] = '*' * len(body['password'])
|
||||
if not hasattr(response, 'data') or not isinstance(response.data, dict):
|
||||
response.data = {}
|
||||
try:
|
||||
if not response.data and response.content:
|
||||
content = json.loads(response.content.decode())
|
||||
response.data = content if isinstance(content, dict) else {}
|
||||
except Exception:
|
||||
return
|
||||
if getattr(request, 'user', None) is None:
|
||||
return
|
||||
user = request.user
|
||||
if isinstance(user, AnonymousUser):
|
||||
return
|
||||
# 如果操作日志在settings.API_OPERATION_EXCLUDE_START记录的路径中,则不操作【暂时不记录:因为太多了】
|
||||
for path in settings.API_OPERATION_EXCLUDE_START:
|
||||
if request.request_path.startswith(path):
|
||||
return
|
||||
info = {
|
||||
'request_username': user.username if isinstance(user, Users) else user['username'],
|
||||
'request_ip': getattr(request, 'request_ip', 'unknown'),
|
||||
'creator_id': user.id if isinstance(user, Users) else user['id'],
|
||||
'request_method': request.method,
|
||||
'request_path': request.request_path,
|
||||
'request_body': body,
|
||||
'response_code': response.data.get('code'),
|
||||
'request_os': get_os(request),
|
||||
'request_browser': get_browser(request),
|
||||
# 'request_msg': request.session.get('request_msg'),
|
||||
'status': True if response.data.get('code') in [2000, ] else False,
|
||||
'json_result': {"code": response.data.get('code'), "msg": response.data.get('result')},
|
||||
}
|
||||
operation_log, creat = OperationLog.objects.update_or_create(defaults=info, id=self.operation_log_id)
|
||||
if not operation_log.request_modular and settings.API_MODEL_MAP.get(request.request_path, None):
|
||||
operation_log.request_modular = settings.API_MODEL_MAP[request.request_path]
|
||||
operation_log.save()
|
||||
|
||||
def process_request(self, request):
|
||||
self.__handle_request(request)
|
||||
|
||||
def process_response(self, request, response):
|
||||
"""
|
||||
主要请求处理完之后记录
|
||||
:param request:
|
||||
:param response:
|
||||
:return:
|
||||
"""
|
||||
if self.enable:
|
||||
if self.methods == 'ALL' or request.method in self.methods:
|
||||
self.__handle_response(request, response)
|
||||
return response
|
||||
132
utils/log_util/request_util.py
Normal file
132
utils/log_util/request_util.py
Normal file
@@ -0,0 +1,132 @@
|
||||
"""
|
||||
request工具类
|
||||
"""
|
||||
import json
|
||||
from apps.system.models import LoginLog
|
||||
from django.urls.resolvers import ResolverMatch
|
||||
from user_agents import parse
|
||||
|
||||
def get_request_ip(request):
|
||||
"""
|
||||
获取请求IP
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR', '')
|
||||
if x_forwarded_for:
|
||||
ip = x_forwarded_for.split(',')[-1].strip()
|
||||
return ip
|
||||
ip = request.META.get('REMOTE_ADDR', '') or getattr(request, 'request_ip', None)
|
||||
return ip or 'unknown'
|
||||
|
||||
def get_request_data(request):
|
||||
"""
|
||||
获取请求参数
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
request_data = getattr(request, 'request_data', None)
|
||||
if request_data:
|
||||
return request_data
|
||||
data: dict = {**request.GET.dict(), **request.POST.dict()}
|
||||
if not data:
|
||||
try:
|
||||
body = request.body
|
||||
if body:
|
||||
data = json.loads(body)
|
||||
except Exception as e:
|
||||
pass
|
||||
if not isinstance(data, dict):
|
||||
data = {'data': data}
|
||||
return data
|
||||
|
||||
def get_request_path(request, *args, **kwargs):
|
||||
"""
|
||||
获取请求路径
|
||||
:param request:
|
||||
:param args:
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
request_path = getattr(request, 'request_path', None)
|
||||
if request_path:
|
||||
return request_path
|
||||
values = []
|
||||
for arg in args:
|
||||
if len(arg) == 0:
|
||||
continue
|
||||
if isinstance(arg, str):
|
||||
values.append(arg)
|
||||
elif isinstance(arg, (tuple, set, list)):
|
||||
values.extend(arg)
|
||||
elif isinstance(arg, dict):
|
||||
values.extend(arg.values())
|
||||
if len(values) == 0:
|
||||
return request.path
|
||||
path: str = request.path
|
||||
for value in values:
|
||||
path = path.replace('/' + value, '/' + '{id}')
|
||||
return path
|
||||
|
||||
def get_request_canonical_path(request, ):
|
||||
"""
|
||||
获取请求路径
|
||||
:param request:
|
||||
:param args:
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
request_path = getattr(request, 'request_canonical_path', None)
|
||||
if request_path:
|
||||
return request_path
|
||||
path: str = request.path
|
||||
resolver_match: ResolverMatch = request.resolver_match
|
||||
for value in resolver_match.args:
|
||||
path = path.replace(f"/{value}", "/{id}")
|
||||
for key, value in resolver_match.kwargs.items():
|
||||
if key == 'pk':
|
||||
path = path.replace(f"/{value}", f"/{{id}}")
|
||||
continue
|
||||
path = path.replace(f"/{value}", f"/{{{key}}}")
|
||||
|
||||
return path
|
||||
|
||||
def get_browser(request, ):
|
||||
"""
|
||||
获取浏览器名
|
||||
:param request:
|
||||
:param args:
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
ua_string = request.META['HTTP_USER_AGENT']
|
||||
user_agent = parse(ua_string)
|
||||
return user_agent.get_browser()
|
||||
|
||||
def get_os(request, ):
|
||||
"""
|
||||
获取操作系统
|
||||
:param request:
|
||||
:param args:
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
ua_string = request.META['HTTP_USER_AGENT']
|
||||
user_agent = parse(ua_string)
|
||||
return user_agent.get_os()
|
||||
|
||||
def save_login_log(request, user):
|
||||
"""
|
||||
保存登录日志
|
||||
:return:
|
||||
"""
|
||||
ip = get_request_ip(request=request)
|
||||
analysis_data = {
|
||||
'username': user.username,
|
||||
'ip': ip,
|
||||
'agent': str(parse(request.META['HTTP_USER_AGENT'])),
|
||||
'browser': get_browser(request),
|
||||
'os': get_os(request),
|
||||
'creator_id': user.id
|
||||
}
|
||||
LoginLog.objects.create(**analysis_data)
|
||||
46
utils/models.py
Normal file
46
utils/models.py
Normal file
@@ -0,0 +1,46 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from django.apps import apps
|
||||
from django.db import models
|
||||
from cdtestplant_v1 import settings
|
||||
|
||||
class CoreModel(models.Model):
|
||||
"""
|
||||
核心标准抽象模型,可直接继承使用
|
||||
增加审计字段,覆盖字段时,字段名称请勿修改,必须统一审计字段名称
|
||||
"""
|
||||
id = models.BigAutoField(primary_key=True, help_text="Id", verbose_name="Id")
|
||||
remark = models.CharField(max_length=255, verbose_name="描述", null=True, blank=True, help_text="描述")
|
||||
update_datetime = models.DateField(auto_now=True, null=True, blank=True, help_text="修改时间", verbose_name="修改时间")
|
||||
create_datetime = models.DateField(auto_now_add=True, null=True, blank=True, help_text="创建时间",
|
||||
verbose_name="创建时间")
|
||||
sort = models.IntegerField(default=1, null=True, blank=True, verbose_name="显示排序", help_text="显示排序")
|
||||
|
||||
class Meta:
|
||||
abstract = True # 指定为True不会创建表,只会当做父类
|
||||
verbose_name = '核心模型'
|
||||
verbose_name_plural = verbose_name
|
||||
|
||||
def get_all_models_objects(model_name=None):
|
||||
"""
|
||||
获取所有 models 对象
|
||||
:return: {}
|
||||
"""
|
||||
settings.ALL_MODELS_OBJECTS = {}
|
||||
if not settings.ALL_MODELS_OBJECTS:
|
||||
all_models = apps.get_models()
|
||||
for item in list(all_models):
|
||||
table = {
|
||||
"tableName": item._meta.verbose_name,
|
||||
"table": item.__name__,
|
||||
"tableFields": []
|
||||
}
|
||||
for field in item._meta.fields:
|
||||
fields = {
|
||||
"title": field.verbose_name,
|
||||
"field": field.name
|
||||
}
|
||||
table['tableFields'].append(fields)
|
||||
settings.ALL_MODELS_OBJECTS.setdefault(item.__name__, {"table": table, "object": item})
|
||||
if model_name:
|
||||
return settings.ALL_MODELS_OBJECTS[model_name] or {}
|
||||
return settings.ALL_MODELS_OBJECTS or {}
|
||||
11
utils/path_utils.py
Normal file
11
utils/path_utils.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from apps.project.models import Project
|
||||
|
||||
def project_path(id: int):
|
||||
"""
|
||||
:param id:传入project_id获取其ident作为路径
|
||||
:return:
|
||||
"""
|
||||
project = Project.objects.filter(id=id).first()
|
||||
if not project:
|
||||
return
|
||||
return project.ident
|
||||
BIN
utils/smallTools/__pycache__/interfaceTools.cpython-313.pyc
Normal file
BIN
utils/smallTools/__pycache__/interfaceTools.cpython-313.pyc
Normal file
Binary file not shown.
BIN
utils/smallTools/__pycache__/interfaceTools.cpython-38.pyc
Normal file
BIN
utils/smallTools/__pycache__/interfaceTools.cpython-38.pyc
Normal file
Binary file not shown.
18
utils/smallTools/interfaceTools.py
Normal file
18
utils/smallTools/interfaceTools.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""该文件主要是小工具,简化接口处理函数写的代码太多"""
|
||||
from ninja import Schema
|
||||
from django.db.models import QuerySet
|
||||
|
||||
def conditionNoneToBlank(condition: Schema):
|
||||
"""将BaseModel/Schema对象中None变为空字符串"""
|
||||
for attr, value in condition.__dict__.items():
|
||||
if getattr(condition, attr) is None:
|
||||
setattr(condition, attr, '')
|
||||
|
||||
def model_retrieve(condition: Schema, qs: QuerySet, exclude_field) -> QuerySet:
|
||||
"""该函数合并上面功能,传入qs以及排除字段,全部添加__icontains进行查询"""
|
||||
conditionNoneToBlank(condition)
|
||||
search_obj = {}
|
||||
for k, v in condition.dict(exclude_none=True).items():
|
||||
if k not in exclude_field:
|
||||
search_obj["".join([k, "__icontains"])] = v
|
||||
return qs.filter(**search_obj)
|
||||
407
utils/util.py
Normal file
407
utils/util.py
Normal file
@@ -0,0 +1,407 @@
|
||||
from typing import List
|
||||
from apps.dict.models import DictItem
|
||||
from apps.project.models import TestDemand
|
||||
from html.parser import HTMLParser
|
||||
from django.db.models import QuerySet
|
||||
# 导入生成文档检查器log - 单例模式
|
||||
from apps.createSeiTaiDocument.extensions.logger import GenerateLogger
|
||||
|
||||
logger = GenerateLogger(model='字典模块')
|
||||
|
||||
# 传入一个字符串数字以及字典标志code,返回真实的title名字
|
||||
def get_str_dict(a, dict_code):
|
||||
dict_obj = DictItem.objects.filter(dict__code=dict_code, key=a).first()
|
||||
if dict_obj:
|
||||
return dict_obj.title
|
||||
else:
|
||||
# 如果用户没填写内容则记录检查中心log
|
||||
logger.write_warning_log(fragment='字典数据缺失', message=f'字典数据{dict_code}数据缺失,请检查相应数据是否存在')
|
||||
return ''
|
||||
|
||||
# 传入一个字符串数字以及字典标志code,返回字典所属的缩写(show_title)
|
||||
def get_str_abbr(a, dict_code):
|
||||
dict_obj = DictItem.objects.filter(dict__code=dict_code, key=a).first()
|
||||
if dict_obj:
|
||||
return dict_obj.show_title
|
||||
else:
|
||||
logger.write_warning_log(fragment='字典数据缺失',
|
||||
message=f'查询字段数据缩写问题,字典数据{dict_code}数据可能缺失')
|
||||
return ""
|
||||
|
||||
# 传入一个字符串数组(测试项类型),字典标志code,返回(真实title,sort)
|
||||
def get_str_dict_plus(a, dict_code):
|
||||
dict_obj = DictItem.objects.filter(dict__code=dict_code, key=a).first()
|
||||
if dict_obj:
|
||||
return dict_obj.title, dict_obj.sort
|
||||
else:
|
||||
logger.write_warning_log(fragment='字典数据查询错误', message=f'字典{dict_code}未查询到数据,请检查')
|
||||
return "", 1
|
||||
|
||||
# 找到testType字典中的缩写,例如“DC”“FT”
|
||||
def get_testType(a, dict_code):
|
||||
dict_obj = DictItem.objects.filter(dict__code=dict_code, key=a).first()
|
||||
if dict_obj:
|
||||
return dict_obj.show_title
|
||||
else:
|
||||
logger.write_warning_log(fragment='字典数据查询错误',
|
||||
message=f'查询字段数据缩写问题,字典数据{dict_code}数据可能缺失')
|
||||
return ""
|
||||
|
||||
# 标识处理:获取测试需求(测试项的)生成文档的ident(标识)
|
||||
def get_ident(test_item):
|
||||
# key_index = int(test_item.key.split("-")[-1]) + 1
|
||||
# test_index = str(key_index).rjust(3, '0')
|
||||
reveal_ident = "_".join(
|
||||
["XQ", get_testType(test_item.testType, "testType"), test_item.ident])
|
||||
return reveal_ident
|
||||
|
||||
# 标识处理:传入demand的ident以及case,返回case的ident
|
||||
def get_case_ident(demand_ident, case):
|
||||
key_index = int(case.key.split("-")[-1]) + 1
|
||||
test_index = str(key_index).rjust(3, '0')
|
||||
reveal_ident = "_".join([demand_ident.replace("XQ", "YL"), test_index])
|
||||
return reveal_ident
|
||||
|
||||
# 传入字典code,以及字符串数组返回一个数组,每个数组是dict
|
||||
def get_list_dict(dict_code: str, str_list: List[str]):
|
||||
result = []
|
||||
qss = DictItem.objects.filter(dict__code=dict_code)
|
||||
for st in str_list:
|
||||
res = {}
|
||||
for qs in qss:
|
||||
if st == qs.key:
|
||||
res['ident_version'] = qs.title
|
||||
res['doc_name'] = qs.doc_name
|
||||
res['publish_date'] = qs.publish_date
|
||||
res['source'] = qs.source
|
||||
result.append(res)
|
||||
return result
|
||||
|
||||
# 传入字典code,以及字符串数组返回一个列表,每个元素是dict,包含字典item很多信息
|
||||
def get_list_dict_info(dict_code, str_list):
|
||||
"""传入字典的字符串列表,输出每个元素为dict的列表信息"""
|
||||
result = []
|
||||
qss = DictItem.objects.filter(dict__code=dict_code) # type:ignore
|
||||
for st in str_list:
|
||||
res = {}
|
||||
for qs in qss:
|
||||
if st == qs.key:
|
||||
res['title'] = qs.title
|
||||
res['index'] = qs.sort
|
||||
result.append(res)
|
||||
return result
|
||||
|
||||
# 传入字典code,以及单个字典字符串,输出一个dict带信息
|
||||
def get_dict_info(dict_code, item_str):
|
||||
"""传入字典的字符串单个表示,输出dict包含排序index字段"""
|
||||
qss = DictItem.objects.filter(dict__code=dict_code)
|
||||
res = {}
|
||||
for qs in qss:
|
||||
if item_str == qs.key:
|
||||
res['title'] = qs.title
|
||||
res['index'] = qs.sort
|
||||
return res
|
||||
|
||||
# 简单HTML解析器 - 解析富文本的parser - 复杂的使用apps/createDocument/extensions/parse_rich_text.py
|
||||
class MyHTMLParser(HTMLParser):
|
||||
def __init__(self):
|
||||
HTMLParser.__init__(self)
|
||||
self.allStrList = []
|
||||
|
||||
def error(self, message):
|
||||
print("HTML解析器出错,error信息为:", message)
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
if tag == 'img':
|
||||
img_base64 = attrs[0][1]
|
||||
self.allStrList.append(img_base64)
|
||||
|
||||
def handle_endtag(self, tag):
|
||||
pass
|
||||
|
||||
def handle_data(self, data):
|
||||
if data != '\n':
|
||||
self.allStrList.append(data)
|
||||
|
||||
# 不提取图片的HTML解析器
|
||||
class MyHTMLParser_p(HTMLParser):
|
||||
def __init__(self):
|
||||
HTMLParser.__init__(self)
|
||||
self.allStrList = []
|
||||
|
||||
def handle_data(self, data):
|
||||
if data != '\n':
|
||||
self.allStrList.append(data)
|
||||
|
||||
def create_problem_grade_str(problems):
|
||||
"""传入问题qs,生成到文档的字符串:严重问题4个、一般问题10个"""
|
||||
problem_r1_grade_dict = {}
|
||||
for problem in problems:
|
||||
grade_key: str = get_str_dict(problem.grade, 'problemGrade')
|
||||
if grade_key in problem_r1_grade_dict.keys():
|
||||
problem_r1_grade_dict[grade_key] += 1
|
||||
else:
|
||||
problem_r1_grade_dict[grade_key] = 1
|
||||
problem_r1_grade_list = []
|
||||
for key, value in problem_r1_grade_dict.items():
|
||||
problem_r1_grade_list.append("".join([f"{key}问题", f"{value}个"]))
|
||||
return "、".join(problem_r1_grade_list)
|
||||
|
||||
def create_problem_type_str(problems):
|
||||
"""传入问题qs,生成到文档的字符串:文档问题1个,程序问题10个"""
|
||||
problem_r1_type_dict = {}
|
||||
for problem in problems:
|
||||
type_key: str = get_str_dict(problem.type, 'problemType')
|
||||
if type_key in problem_r1_type_dict.keys():
|
||||
problem_r1_type_dict[type_key] += 1
|
||||
else:
|
||||
problem_r1_type_dict[type_key] = 1
|
||||
problem_r1_type_list = []
|
||||
for key, value in problem_r1_type_dict.items():
|
||||
problem_r1_type_list.append("".join([f"{key}", f"{value}个"]))
|
||||
return "、".join(problem_r1_type_list)
|
||||
|
||||
def create_str_testType_list(cases):
|
||||
"""传入用例的qs,生成其测试类型的字符串:例如包含xxx测试5个,xxx测试11个,返回的是元组第二个元素表示测试类型个数"""
|
||||
test_type_set: set = set()
|
||||
for case in cases:
|
||||
demand: TestDemand = case.test
|
||||
test_type_set.add(demand.testType)
|
||||
test_type_list = get_list_dict_info('testType', list(test_type_set))
|
||||
test_type_list.sort(key=lambda x: int(x['index']))
|
||||
round1_testType_list = list(map(lambda x: x['title'], test_type_list))
|
||||
return round1_testType_list, len(test_type_list)
|
||||
|
||||
def util_func(item):
|
||||
"""辅助函数,将下面函数返回的summary_list改造为以需求类型执行统计列表"""
|
||||
transform_item = {'title': item['title'], 'total_count': 0, 'exe_count': 0, 'not_exe_count': 0, 'pass_count': 0,
|
||||
'not_pass_count': 0}
|
||||
for it in item['demand_list']:
|
||||
transform_item['total_count'] += it['total_count']
|
||||
transform_item['exe_count'] += it['exe_count']
|
||||
transform_item['not_exe_count'] += it['not_exe_count']
|
||||
transform_item['pass_count'] += it['pass_count']
|
||||
transform_item['not_pass_count'] += it['not_pass_count']
|
||||
return transform_item
|
||||
|
||||
def create_demand_summary(demands: QuerySet, project_ident: str):
|
||||
"""
|
||||
[
|
||||
{
|
||||
'title':'功能测试_5',
|
||||
'demand_list':[
|
||||
{
|
||||
'name': '测试项1',
|
||||
'total_count': 1,
|
||||
'exe_count': 1,
|
||||
'not_exe_count': 0,
|
||||
'pass_count': 1,
|
||||
'not_pass_count': 0,
|
||||
'conclusion': '通过',
|
||||
'problems': '/'
|
||||
}
|
||||
]
|
||||
},
|
||||
]
|
||||
"""
|
||||
summary_dict = {} # 后面再变为数组
|
||||
# 1.首先根据testType字段生成一个一个dict
|
||||
for demand in demands:
|
||||
# 取出testType -> '4'(后面查字典)
|
||||
test_type_num = demand.testType
|
||||
# 查出需求类型的sort与testType真实名字,组合为一个字符串作为key
|
||||
testTypeStr, testTypeSort = get_str_dict_plus(test_type_num, 'testType')
|
||||
testType_info = testTypeStr + '_' + str(testTypeSort)
|
||||
demand_dict = {
|
||||
'name': demand.name,
|
||||
'total_count': 0,
|
||||
'exe_count': 0,
|
||||
'not_exe_count': 0,
|
||||
'pass_count': 0,
|
||||
'not_pass_count': 0,
|
||||
'conclusion': '通过',
|
||||
'problems': "/"
|
||||
}
|
||||
# 查询当前测试需求下用例
|
||||
demand_problem_list = set()
|
||||
for case in demand.tcField.all():
|
||||
demand_dict['total_count'] += 1
|
||||
# 要遍历下面的测试步骤
|
||||
isPassed = True
|
||||
isExe = True
|
||||
for step in case.step.all():
|
||||
# 所有步骤有一个未通过则是未通过,所有都执行则是已执行
|
||||
if step.passed == '2':
|
||||
isPassed = False
|
||||
isExe = True
|
||||
break
|
||||
if step.passed == '3':
|
||||
isExe = False
|
||||
if isPassed:
|
||||
demand_dict['pass_count'] += 1
|
||||
else:
|
||||
demand_dict['not_pass_count'] += 1
|
||||
demand_dict['conclusion'] = '未通过'
|
||||
if isExe:
|
||||
demand_dict['exe_count'] += 1
|
||||
else:
|
||||
demand_dict['not_exe_count'] += 1
|
||||
# 查询每个用例下面的问题单,将问题单的ident弄出来
|
||||
for problem in case.caseField.all():
|
||||
problem_ident = 'PT_' + project_ident + '_' + problem.ident.rjust(3, '0')
|
||||
demand_problem_list.add(problem_ident)
|
||||
if len(list(demand_problem_list)) > 0:
|
||||
demand_dict['problems'] = "\a".join(list(demand_problem_list))
|
||||
if testType_info in summary_dict.keys():
|
||||
summary_dict[testType_info].append(demand_dict)
|
||||
else:
|
||||
summary_dict[testType_info] = [demand_dict]
|
||||
summary_list = []
|
||||
for key, value in summary_dict.items():
|
||||
one_dict = {
|
||||
'title': key,
|
||||
'demand_list': value
|
||||
}
|
||||
summary_list.append(one_dict)
|
||||
# 根据其排序
|
||||
summary_list.sort(key=lambda x: int(x['title'].split('_')[-1]))
|
||||
# 然后将_5这种替换掉
|
||||
for one in summary_list:
|
||||
one['title'] = one['title'].split("_")[0]
|
||||
# ~~~~还需要返回一个 -> 测试类型的用例执行情况
|
||||
demandType_exe_list = list(map(util_func, summary_list))
|
||||
return summary_list, demandType_exe_list
|
||||
|
||||
def create_problem_table(problems):
|
||||
"""传入问题单qs,输出按问题种类分的问题统计表的数据"""
|
||||
res_list = [{
|
||||
'name': '一般问题',
|
||||
'xq_count': 0,
|
||||
'sj_count': 0,
|
||||
'wd_count': 0,
|
||||
'bm_count': 0,
|
||||
'data_count': 0,
|
||||
'other_count': 0,
|
||||
'closed_count': 0,
|
||||
'non_closed_count': 0,
|
||||
}, {
|
||||
'name': '严重问题',
|
||||
'xq_count': 0,
|
||||
'sj_count': 0,
|
||||
'wd_count': 0,
|
||||
'bm_count': 0,
|
||||
'data_count': 0,
|
||||
'other_count': 0,
|
||||
'closed_count': 0,
|
||||
'non_closed_count': 0,
|
||||
}, {
|
||||
'name': '建议',
|
||||
'xq_count': 0,
|
||||
'sj_count': 0,
|
||||
'wd_count': 0,
|
||||
'bm_count': 0,
|
||||
'data_count': 0,
|
||||
'other_count': 0,
|
||||
'closed_count': 0,
|
||||
'non_closed_count': 0,
|
||||
}, {
|
||||
'name': '重大问题',
|
||||
'xq_count': 0,
|
||||
'sj_count': 0,
|
||||
'wd_count': 0,
|
||||
'bm_count': 0,
|
||||
'data_count': 0,
|
||||
'other_count': 0,
|
||||
'closed_count': 0,
|
||||
'non_closed_count': 0,
|
||||
}]
|
||||
|
||||
for problem in problems:
|
||||
index = int(problem.grade) - 1
|
||||
if problem.type == '1':
|
||||
res_list[index]['other_count'] += 1
|
||||
elif problem.type == '2':
|
||||
res_list[index]['wd_count'] += 1
|
||||
elif problem.type == '3':
|
||||
res_list[index]['bm_count'] += 1
|
||||
elif problem.type == '4':
|
||||
res_list[index]['sj_count'] += 1
|
||||
elif problem.type == '5':
|
||||
res_list[index]['xq_count'] += 1
|
||||
elif problem.type == '6':
|
||||
res_list[index]['data_count'] += 1
|
||||
|
||||
# 是否归零
|
||||
if problem.status == '1':
|
||||
res_list[index]['closed_count'] += 1
|
||||
else:
|
||||
res_list[index]['non_closed_count'] += 1
|
||||
return res_list
|
||||
|
||||
def create_problem_type_table(problems):
|
||||
"""传入问题qs,解析出按测试类型分的问题统计表格"""
|
||||
res_list = [{
|
||||
'name': '一般问题',
|
||||
'wd_count': 0,
|
||||
'jt_count': 0,
|
||||
'dm_count': 0,
|
||||
'dt_count': 0,
|
||||
'data_count': 0,
|
||||
'closed_count': 0,
|
||||
'non_closed_count': 0,
|
||||
}, {
|
||||
'name': '严重问题',
|
||||
'wd_count': 0,
|
||||
'jt_count': 0,
|
||||
'dm_count': 0,
|
||||
'dt_count': 0,
|
||||
'data_count': 0,
|
||||
'closed_count': 0,
|
||||
'non_closed_count': 0,
|
||||
}, {
|
||||
'name': '建议',
|
||||
'wd_count': 0,
|
||||
'jt_count': 0,
|
||||
'dm_count': 0,
|
||||
'dt_count': 0,
|
||||
'data_count': 0,
|
||||
'closed_count': 0,
|
||||
'non_closed_count': 0,
|
||||
}, {
|
||||
'name': '重大问题',
|
||||
'wd_count': 0,
|
||||
'jt_count': 0,
|
||||
'dm_count': 0,
|
||||
'dt_count': 0,
|
||||
'data_count': 0,
|
||||
'closed_count': 0,
|
||||
'non_closed_count': 0,
|
||||
}]
|
||||
|
||||
for problem in problems:
|
||||
index = int(problem.grade) - 1
|
||||
belong_demand: TestDemand = problem.case.all()[0].test
|
||||
if belong_demand.testType == '8': # 属于文档审查问题
|
||||
res_list[index]['wd_count'] += 1
|
||||
elif belong_demand.testType == '15': # 属于静态分析问题
|
||||
res_list[index]['jt_count'] += 1
|
||||
elif belong_demand.testType == '2' or belong_demand.testType == '3': # 属于代码审查和走查
|
||||
res_list[index]['dm_count'] += 1
|
||||
else: # 属于动态发现问题
|
||||
res_list[index]['dt_count'] += 1
|
||||
|
||||
# 是否归零
|
||||
if problem.status == '1':
|
||||
res_list[index]['closed_count'] += 1
|
||||
else:
|
||||
res_list[index]['non_closed_count'] += 1
|
||||
return res_list
|
||||
|
||||
def get_demand_testTypes(demand_qs) -> str:
|
||||
"""传入测试项qs,返回字符串类似于“静态分析、代码审查、功能测试等”"""
|
||||
testType_list = list(demand_qs.values("testType").distinct().order_by('testType'))
|
||||
t_code_list = [item['testType'] for item in testType_list]
|
||||
t_code_list = get_list_dict_info('testType', t_code_list)
|
||||
t_code_list.sort(key=lambda x: int(x['index']))
|
||||
t_str = [item['title'] for item in t_code_list]
|
||||
return '、'.join(t_str) if len(t_str) > 0 else "测试"
|
||||
Reference in New Issue
Block a user