Ver Fonte

添加用户组

刘凡 há 2 anos atrás
commit
ad634f3564
100 ficheiros alterados com 3376 adições e 0 exclusões
  1. 3 0
      .gitignore
  2. 8 0
      .idea/.gitignore
  3. 34 0
      .idea/TestLaboratory.iml
  4. 34 0
      .idea/inspectionProfiles/Project_Default.xml
  5. 6 0
      .idea/inspectionProfiles/profiles_settings.xml
  6. 7 0
      .idea/misc.xml
  7. 8 0
      .idea/modules.xml
  8. 6 0
      .idea/sqldialects.xml
  9. 8 0
      .idea/vcs.xml
  10. 0 0
      Config/__init__.py
  11. 7 0
      Config/database_mysql.cnf
  12. 13 0
      Dockerfile
  13. 37 0
      README.md
  14. 3 0
      TestLaboratory/__init__.py
  15. 16 0
      TestLaboratory/asgi.py
  16. 283 0
      TestLaboratory/settings.py
  17. 28 0
      TestLaboratory/urls.py
  18. 16 0
      TestLaboratory/wsgi.py
  19. BIN
      api2.0.xlsx
  20. 0 0
      apps/__init__.py
  21. 0 0
      apps/file/__init__.py
  22. 3 0
      apps/file/admin.py
  23. 6 0
      apps/file/apps.py
  24. 30 0
      apps/file/migrations/0001_initial.py
  25. 18 0
      apps/file/migrations/0002_alter_filemanager_id.py
  26. 0 0
      apps/file/migrations/__init__.py
  27. 24 0
      apps/file/models.py
  28. 3 0
      apps/file/tests.py
  29. 7 0
      apps/file/urls.py
  30. 0 0
      apps/file/views/__init__.py
  31. 128 0
      apps/file/views/fileview.py
  32. 0 0
      apps/log/__init__.py
  33. 3 0
      apps/log/admin.py
  34. 6 0
      apps/log/apps.py
  35. 0 0
      apps/log/middleware/__init__.py
  36. 33 0
      apps/log/middleware/loginfo.py
  37. 35 0
      apps/log/migrations/0001_initial.py
  38. 18 0
      apps/log/migrations/0002_alter_log_id.py
  39. 0 0
      apps/log/migrations/__init__.py
  40. 77 0
      apps/log/models.py
  41. 3 0
      apps/log/tests.py
  42. 6 0
      apps/log/urls.py
  43. 0 0
      apps/log/views/__init__.py
  44. 47 0
      apps/log/views/loglistview.py
  45. 0 0
      apps/plan/__init__.py
  46. 3 0
      apps/plan/admin.py
  47. 6 0
      apps/plan/apps.py
  48. 0 0
      apps/plan/dao/__init__.py
  49. 0 0
      apps/plan/dao/plandao.py
  50. 38 0
      apps/plan/migrations/0001_initial.py
  51. 18 0
      apps/plan/migrations/0002_alter_testplan_id.py
  52. 0 0
      apps/plan/migrations/__init__.py
  53. 68 0
      apps/plan/models.py
  54. 3 0
      apps/plan/tests.py
  55. 9 0
      apps/plan/urls.py
  56. 0 0
      apps/plan/views/__init__.py
  57. 168 0
      apps/plan/views/planlistview.py
  58. 146 0
      apps/plan/views/planview.py
  59. 0 0
      apps/software/__init__.py
  60. 3 0
      apps/software/admin.py
  61. 6 0
      apps/software/apps.py
  62. 49 0
      apps/software/migrations/0001_initial.py
  63. 18 0
      apps/software/migrations/0002_alter_software_name.py
  64. 0 0
      apps/software/migrations/__init__.py
  65. 95 0
      apps/software/models.py
  66. 3 0
      apps/software/tests.py
  67. 0 0
      apps/software/tests/__init__.py
  68. 0 0
      apps/software/tests/softwaretest.py
  69. 16 0
      apps/software/urls.py
  70. 0 0
      apps/software/views/__init__.py
  71. 77 0
      apps/software/views/report.py
  72. 126 0
      apps/software/views/softwarelistview.py
  73. 132 0
      apps/software/views/softwareview.py
  74. 50 0
      apps/software/views/versionlistview.py
  75. 304 0
      apps/software/views/versionview.py
  76. 0 0
      apps/task/__init__.py
  77. 3 0
      apps/task/admin.py
  78. 6 0
      apps/task/apps.py
  79. 74 0
      apps/task/migrations/0001_initial.py
  80. 18 0
      apps/task/migrations/0002_alter_testtask_id.py
  81. 29 0
      apps/task/migrations/0003_auto_20220626_1630.py
  82. 0 0
      apps/task/migrations/__init__.py
  83. 69 0
      apps/task/models.py
  84. 3 0
      apps/task/tests.py
  85. 10 0
      apps/task/urls.py
  86. 0 0
      apps/task/views/__init__.py
  87. 259 0
      apps/task/views/caseview.py
  88. 147 0
      apps/task/views/tasklistview.py
  89. 200 0
      apps/task/views/taskview.py
  90. 0 0
      apps/user/__init__.py
  91. 3 0
      apps/user/admin.py
  92. 6 0
      apps/user/apps.py
  93. 0 0
      apps/user/middleware/__init__.py
  94. 26 0
      apps/user/middleware/authentication.py
  95. 48 0
      apps/user/middleware/rolecontrol.py
  96. 68 0
      apps/user/migrations/0001_initial.py
  97. 33 0
      apps/user/migrations/0002_auto_20220619_1043.py
  98. 0 0
      apps/user/migrations/__init__.py
  99. 69 0
      apps/user/models.py
  100. 3 0
      apps/user/tests.py

+ 3 - 0
.gitignore

@@ -0,0 +1,3 @@
+.DS_Store
+*.pyc
+__pycache__

+ 8 - 0
.idea/.gitignore

@@ -0,0 +1,8 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
+# Editor-based HTTP Client requests
+/httpRequests/

+ 34 - 0
.idea/TestLaboratory.iml

@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="PYTHON_MODULE" version="4">
+  <component name="FacetManager">
+    <facet type="django" name="Django">
+      <configuration>
+        <option name="rootFolder" value="$MODULE_DIR$" />
+        <option name="settingsModule" value="TestLaboratory/settings.py" />
+        <option name="manageScript" value="$MODULE_DIR$/manage.py" />
+        <option name="environment" value="&lt;map/&gt;" />
+        <option name="doNotUseTestRunner" value="false" />
+        <option name="trackFilePattern" value="migrations" />
+      </configuration>
+    </facet>
+  </component>
+  <component name="NewModuleRootManager">
+    <content url="file://$MODULE_DIR$">
+      <sourceFolder url="file://$MODULE_DIR$" isTestSource="false" />
+    </content>
+    <orderEntry type="jdk" jdkName="Python 3.9 (base)" jdkType="Python SDK" />
+    <orderEntry type="sourceFolder" forTests="false" />
+  </component>
+  <component name="PackageRequirementsSettings">
+    <option name="versionSpecifier" value="Strong equality (==x.y.z)" />
+    <option name="removeUnused" value="true" />
+    <option name="modifyBaseFiles" value="true" />
+  </component>
+  <component name="PyDocumentationSettings">
+    <option name="format" value="PLAIN" />
+    <option name="myDocStringFormat" value="Plain" />
+  </component>
+  <component name="TemplatesService">
+    <option name="TEMPLATE_CONFIGURATION" value="Django" />
+  </component>
+</module>

+ 34 - 0
.idea/inspectionProfiles/Project_Default.xml

@@ -0,0 +1,34 @@
+<component name="InspectionProjectProfileManager">
+  <profile version="1.0">
+    <option name="myName" value="Project Default" />
+    <inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
+    <inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
+      <option name="ignoredPackages">
+        <value>
+          <list size="3">
+            <item index="0" class="java.lang.String" itemvalue="pymysql" />
+            <item index="1" class="java.lang.String" itemvalue="pyjwt" />
+            <item index="2" class="java.lang.String" itemvalue="django-cors-headers" />
+          </list>
+        </value>
+      </option>
+    </inspection_tool>
+    <inspection_tool class="PyPep8Inspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
+      <option name="ignoredErrors">
+        <list>
+          <option value="E722" />
+        </list>
+      </option>
+    </inspection_tool>
+    <inspection_tool class="PyPep8NamingInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
+      <option name="ignoredErrors">
+        <list>
+          <option value="N802" />
+          <option value="N806" />
+          <option value="N801" />
+        </list>
+      </option>
+    </inspection_tool>
+    <inspection_tool class="Stylelint" enabled="true" level="ERROR" enabled_by_default="true" />
+  </profile>
+</component>

+ 6 - 0
.idea/inspectionProfiles/profiles_settings.xml

@@ -0,0 +1,6 @@
+<component name="InspectionProjectProfileManager">
+  <settings>
+    <option name="USE_PROJECT_PROFILE" value="false" />
+    <version value="1.0" />
+  </settings>
+</component>

+ 7 - 0
.idea/misc.xml

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.9 (base)" project-jdk-type="Python SDK" />
+  <component name="PyCharmProfessionalAdvertiser">
+    <option name="shown" value="true" />
+  </component>
+</project>

+ 8 - 0
.idea/modules.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ProjectModuleManager">
+    <modules>
+      <module fileurl="file://$PROJECT_DIR$/.idea/TestLaboratory.iml" filepath="$PROJECT_DIR$/.idea/TestLaboratory.iml" />
+    </modules>
+  </component>
+</project>

+ 6 - 0
.idea/sqldialects.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="SqlDialectMappings">
+    <file url="file://$PROJECT_DIR$/mysql-init/TestLaboratory.sql" dialect="MariaDB" />
+  </component>
+</project>

+ 8 - 0
.idea/vcs.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="VcsDirectoryMappings">
+    <mapping directory="$PROJECT_DIR$/../../../.." vcs="Git" />
+    <mapping directory="$PROJECT_DIR$/../.." vcs="Git" />
+    <mapping directory="$PROJECT_DIR$/.." vcs="Git" />
+  </component>
+</project>

+ 0 - 0
Config/__init__.py


+ 7 - 0
Config/database_mysql.cnf

@@ -0,0 +1,7 @@
+[client]
+host = 127.0.0.1
+port = 3306
+database = TestLaboratory
+user = root
+password = liufan3344
+default-character-set = utf8

+ 13 - 0
Dockerfile

@@ -0,0 +1,13 @@
+From python:3.9
+
+MAINTAINER Liufan<1649750212@qq.com>
+ENV PYTHONUNBUFFERED 1
+
+COPY . /code
+WORKDIR /code
+
+RUN pip install -r requirements.txt -i https://pypi.douban.com/simple
+
+EXPOSE 8003
+
+ENTRYPOINT ["/bin/sh","startup.sh"]

+ 37 - 0
README.md

@@ -0,0 +1,37 @@
+# lims测试实验室管理平台
+
+## 作者:刘凡
+
+## 系统地址
+
+http://127.0.0.1:8002 **(本地)**
+
+http://172.26.0.13:8002 **(局域网)**
+
+## 用户操作手册
+
+- 详见 **附件1**
+
+## 源代码地址
+
+- /home/dell/lims/TestLaboratory
+
+## 系统部署安装
+
+### step1:安装mysql镜像
+
+```shell
+docker pull mysql:8.0.26
+```
+
+### step2: 部署前后端镜像
+
+```shell
+docker-compose up -d
+```
+
+## 系统运行环境
+
+- linux系统
+- 浏览器建议使用最新的chrome(老版本浏览器可能出现文件无法下载的问题)
+- docker 2.0

+ 3 - 0
TestLaboratory/__init__.py

@@ -0,0 +1,3 @@
+import pymysql
+pymysql.version_info = (1, 4, 13, "final", 0)
+pymysql.install_as_MySQLdb()

+ 16 - 0
TestLaboratory/asgi.py

@@ -0,0 +1,16 @@
+"""
+ASGI config for TestLaboratory project.
+
+It exposes the ASGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/
+"""
+
+import os
+
+from django.core.asgi import get_asgi_application
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'TestLaboratory.settings')
+
+application = get_asgi_application()

+ 283 - 0
TestLaboratory/settings.py

@@ -0,0 +1,283 @@
+"""
+Django settings for TestLaboratory project.
+
+Generated by 'django-admin startproject' using Django 3.2.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/3.2/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/3.2/ref/settings/
+"""
+import datetime
+from pathlib import Path
+import sys
+import os
+
+# Build paths inside the project like this: BASE_DIR / 'subdir'.
+# import apps.user.middleware.authentication
+
+BASE_DIR = Path(__file__).resolve().parent.parent
+sys.path.insert(0, os.path.join(BASE_DIR, 'apps'))
+
+MEDIA_ROOT = os.path.join(BASE_DIR, 'static')
+MEDIA_URL = '/media/'
+
+SOFTWARE_ROOT = "static/software/"
+PLAN_ROOT = "static/plan-statement/"
+TASK_ROOT = 'static/task-statement/'
+CASE_TEMPLATE_ROOT = 'static/case-template/测试用例模版.xlsx'
+CASE_FILE_ROOT = 'static/case-file/'
+CASE_FILE_EXPORT = 'static/case-file-export/'
+HTTP_HEAD = 'http://172.26.0.14:8003/'
+
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = 'django-insecure-zo64fvv02msf-se7!dek5*w$17#3nh6zta#!i=79bt9d#f88@i'
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = True
+
+ALLOWED_HOSTS = ['*']
+# AUTH_USER_MODEL = "user.User"
+# AUTHENTICATION_BACKENDS = (
+#     'user.middleware.authentication.CustomBackend',
+# )
+# Application definition
+
+INSTALLED_APPS = [
+    'simpleui',
+    'django.contrib.admin',
+    'django.contrib.auth',
+    'django.contrib.contenttypes',
+    'django.contrib.sessions',
+    'django.contrib.messages',
+    'django.contrib.staticfiles',
+    'rest_framework',
+    'corsheaders',
+    'user',
+    'software',
+    'plan',
+    'apps.task',
+    'file',
+    'log'
+]
+
+MIDDLEWARE = [
+    'django.middleware.security.SecurityMiddleware',
+    'django.contrib.sessions.middleware.SessionMiddleware',
+    'django.middleware.common.CommonMiddleware',
+    # 'django.middleware.csrf.CsrfViewMiddleware',
+    'django.contrib.auth.middleware.AuthenticationMiddleware',
+    'django.contrib.messages.middleware.MessageMiddleware',
+    'django.middleware.clickjacking.XFrameOptionsMiddleware',
+    'corsheaders.middleware.CorsMiddleware',
+    'django.middleware.common.CommonMiddleware',
+]
+
+ROOT_URLCONF = 'TestLaboratory.urls'
+
+TEMPLATES = [
+    {
+        'BACKEND': 'django.template.backends.django.DjangoTemplates',
+        'DIRS': [os.path.join(BASE_DIR, 'templates')],
+        'APP_DIRS': True,
+        'OPTIONS': {
+            'context_processors': [
+                'django.template.context_processors.debug',
+                'django.template.context_processors.request',
+                'django.contrib.auth.context_processors.auth',
+                'django.contrib.messages.context_processors.messages',
+            ],
+        },
+    },
+]
+
+WSGI_APPLICATION = 'TestLaboratory.wsgi.application'
+
+# Database
+# https://docs.djangoproject.com/en/3.2/ref/settings/#databases
+
+DATABASES = {
+    'default': {
+        'ENGINE': 'django.db.backends.mysql',
+        'OPTIONS': {
+            'read_default_file': 'Config/database_mysql.cnf',
+        },
+    }
+}
+
+# Password validation
+# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators
+
+AUTH_PASSWORD_VALIDATORS = [
+    {
+        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
+    },
+    {
+        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
+    },
+    {
+        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
+    },
+    {
+        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
+    },
+]
+
+REST_FRAMEWORK = {
+    'DEFAULT_PERMISSION_CLASSES': (
+        'rest_framework.permissions.IsAuthenticated'
+    ),
+    'DEFAULT_AUTHENTICATION_CLASSES': (
+        'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
+        'rest_framework.authentication.BasicAuthentication',
+        'rest_framework.authentication.SessionAuthentication'
+    )
+}
+
+JWT_AUTH = {
+    # 设置token有效时间
+    'JWT_EXPIRATION_DELTA': datetime.timedelta(seconds=60 * 60 * 2)
+}
+
+# Internationalization
+# https://docs.djangoproject.com/en/3.2/topics/i18n/
+
+LANGUAGE_CODE = 'en-us'
+
+TIME_ZONE = 'Asia/Shanghai'
+
+USE_I18N = True
+
+USE_L10N = True
+
+USE_TZ = False
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/3.2/howto/static-files/
+
+STATIC_URL = '/static/'
+STATICFILES_DIRS = [
+    os.path.join(BASE_DIR, "static"),
+]
+
+# Default primary key field type
+# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
+
+DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
+
+REST_FRAMEWORK = {
+    "DEFAULT_AUTHENTICATION_CLASSES": ["apps.user.middleware.authentication.JwtAutentication"]
+}
+
+CORS_ALLOW_CREDENTIALS = True
+CORS_ORIGIN_ALLOW_ALL = True
+
+CORS_ALLOW_METHODS = (
+    'GET',
+    'POST',
+    'PUT',
+    'PATCH',
+    'DELETE',
+    'OPTIONS'
+)
+
+CORS_ALLOW_HEADERS = (
+    'x-requested-with',
+    'content-type',
+    'accept',
+    'origin',
+    'authorization',
+    'x-csrftoken'
+)
+
+import time
+
+cur_path = os.path.dirname(os.path.realpath(__file__))  # log_path是存放日志的路径
+log_path = os.path.join(os.path.dirname(cur_path), 'logs')
+if not os.path.exists(log_path): os.mkdir(log_path)  # 如果不存在这个logs文件夹,就自动创建一个
+
+LOGGING = {
+    'version': 1,
+    'disable_existing_loggers': False,
+    'formatters': {
+        # 日志格式
+        'standard': {
+            'format': '[%(levelname)s] [%(asctime)s] [%(filename)s:%('
+                      'lineno)d] [%(module)s:%(funcName)s] '
+                      '- %(message)s'},
+        'simple': {  # 简单格式
+            'format': '%(levelname)s %(message)s'
+        },
+    },
+    # 过滤
+    'filters': {
+        'require_debug_true': {
+            '()': 'django.utils.log.RequireDebugTrue',  # 过滤掉调试信息
+        },
+    },
+    # 定义具体处理日志的方式
+    'handlers': {
+        # 默认记录所有日志
+        'default': {
+            'level': 'INFO',
+            'class': 'logging.handlers.RotatingFileHandler',
+            'filename': os.path.join(log_path, 'all-{}.log'.format(
+                time.strftime('%Y-%m-%d'))),
+            'maxBytes': 1024 * 1024 * 300,  # 文件大小
+            'backupCount': 10,  # 备份数
+            'formatter': 'standard',  # 输出格式
+            'encoding': 'utf-8',  # 设置默认编码,否则打印出来汉字乱码
+        },
+        # 输出错误日志
+        'error': {
+            'level': 'ERROR',
+            'class': 'logging.handlers.RotatingFileHandler',
+            'filename': os.path.join(log_path, 'error-{}.log'.format(
+                time.strftime('%Y-%m-%d'))),
+            'maxBytes': 1024 * 1024 * 5,  # 文件大小
+            'backupCount': 5,  # 备份数
+            'formatter': 'standard',  # 输出格式
+            'encoding': 'utf-8',  # 设置默认编码
+        },
+        # 控制台输出
+        'console': {
+            'level': 'DEBUG',
+            'class': 'logging.StreamHandler',
+            'formatter': 'standard'
+        },
+        #输出info日志
+        'info': {
+            'level': 'INFO',
+            'class': 'logging.handlers.RotatingFileHandler',
+            'filename': os.path.join(log_path, 'info-{}.log'.format(
+                time.strftime('%Y-%m-%d'))),
+            'maxBytes': 1024 * 1024 * 5,
+            'backupCount': 5,
+            'formatter': 'standard',
+            'encoding': 'utf-8',  # 设置默认编码
+        },
+    },
+    # 配置用哪几种 handlers 来处理日志
+    'loggers': {
+        # 类型 为 django 处理所有类型的日志, 默认调用
+        'django': {
+            'handlers': ['default', 'console'],
+            'level': 'INFO',
+            'propagate': False
+        },
+        # log 调用时需要当作参数传入
+        'log': {
+            'handlers': ['error', 'info', 'console', 'default'],
+            'level': 'INFO',
+            'propagate': True
+        },
+    }
+}
+"""
+python manage.py runserver 172.16.100.180:8000   
+"""

+ 28 - 0
TestLaboratory/urls.py

@@ -0,0 +1,28 @@
+"""TestLaboratory URL Configuration
+
+The `urlpatterns` list routes URLs to views. For more information please see:
+    https://docs.djangoproject.com/en/3.2/topics/http/urls/
+Examples:
+Function views
+    1. Add an import:  from my_app import views
+    2. Add a URL to urlpatterns:  path('', views.home, name='home')
+Class-based views
+    1. Add an import:  from other_app.views import Home
+    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
+Including another URLconf
+    1. Import the include() function: from django.urls import include, path
+    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
+"""
+from django.conf.urls.static import static
+from django.urls import path, include
+
+from TestLaboratory import settings
+
+urlpatterns = [
+    path('api/v1/users/', include('user.urls')),
+    path('api/v1/software/', include('software.urls')),
+    path('api/v1/test-plans/', include('plan.urls')),
+    path('api/v1/test-tasks/', include('task.urls')),
+    path('api/v1/log-info/', include('log.urls')),
+    path('api/v1/files/', include('file.urls'))
+] + static(settings.STATIC_URL, document_root=settings.STATICFILES_DIRS)

+ 16 - 0
TestLaboratory/wsgi.py

@@ -0,0 +1,16 @@
+"""
+WSGI config for TestLaboratory project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/
+"""
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'TestLaboratory.settings')
+
+application = get_wsgi_application()

BIN
api2.0.xlsx


+ 0 - 0
apps/__init__.py


+ 0 - 0
apps/file/__init__.py


+ 3 - 0
apps/file/admin.py

@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.

+ 6 - 0
apps/file/apps.py

@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class FileConfig(AppConfig):
+    default_auto_field = 'django.db.models.BigAutoField'
+    name = 'apps.file'

+ 30 - 0
apps/file/migrations/0001_initial.py

@@ -0,0 +1,30 @@
+# Generated by Django 3.2 on 2021-07-17 20:05
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    initial = True
+
+    dependencies = [
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='FileManager',
+            fields=[
+                ('id', models.CharField(default='TestLaboratory_V1_File_1', max_length=30, primary_key=True, serialize=False, unique=True, verbose_name='文件id')),
+                ('path', models.FilePathField(max_length=200, verbose_name='文件路径')),
+                ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
+                ('update_time', models.DateTimeField(auto_now=True, verbose_name='修改时间')),
+                ('delete', models.BooleanField(default=False)),
+                ('category', models.CharField(max_length=20, verbose_name='文件类型')),
+            ],
+            options={
+                'verbose_name': '文件管理',
+                'verbose_name_plural': '文件管理',
+                'db_table': 'file_manager',
+            },
+        ),
+    ]

+ 18 - 0
apps/file/migrations/0002_alter_filemanager_id.py

@@ -0,0 +1,18 @@
+# Generated by Django 3.2.4 on 2022-06-19 10:43
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('file', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='filemanager',
+            name='id',
+            field=models.CharField(default='TestLaboratory_V1_File_1', max_length=128, primary_key=True, serialize=False, unique=True, verbose_name='文件id'),
+        ),
+    ]

+ 0 - 0
apps/file/migrations/__init__.py


+ 24 - 0
apps/file/models.py

@@ -0,0 +1,24 @@
+from django.db import models
+
+
+# Create your models here.
+class FileManager(models.Model):
+    id = models.CharField(max_length=128, unique=True, verbose_name='文件id', primary_key=True,
+                          default='TestLaboratory_V1_File_1')
+    path = models.FilePathField(max_length=200, verbose_name='文件路径')
+    create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
+    update_time = models.DateTimeField(auto_now=True, verbose_name='修改时间')
+    delete = models.BooleanField(default=False)
+    # category: software, report, plan_statement, task_statement, case_file
+    category = models.CharField(max_length=20, verbose_name='文件类型')
+
+    def __str__(self):
+        return self.path
+
+    class Meta:
+        verbose_name = '文件管理'
+        verbose_name_plural = verbose_name
+        db_table = 'file_manager'
+
+
+

+ 3 - 0
apps/file/tests.py

@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.

+ 7 - 0
apps/file/urls.py

@@ -0,0 +1,7 @@
+from django.urls import path
+
+from .views.fileview import FileView
+
+urlpatterns = [
+    path('', FileView.as_view()),
+]

+ 0 - 0
apps/file/views/__init__.py


+ 128 - 0
apps/file/views/fileview.py

@@ -0,0 +1,128 @@
+import datetime
+import os
+
+from django.http import HttpResponse
+from rest_framework.views import APIView
+
+from TestLaboratory.settings import PLAN_ROOT, TASK_ROOT
+from apps.file.models import FileManager
+from apps.log.models import get_log, gen_log
+from apps.plan.models import TestPlan
+from apps.task.models import TestTask
+from apps.user.middleware.rolecontrol import RoleControl
+from utils.util_add_id import gen_next_id
+
+import logging
+logger = logging.getLogger('django')
+
+
+class FileView(APIView):
+    # create file
+    @staticmethod
+    @RoleControl
+    def post(request, *args, **kwargs):
+        file_type = request.POST.get('type')
+        plan_id = request.POST.get('plan_id')
+        task_id = request.POST.get('task_id')
+        files = request.FILES.getlist('files')
+
+        model_object = None
+        path_head = None
+        if file_type == 'plan_statement':
+            model_object = TestPlan.objects.filter(id=plan_id, delete=False)
+            if not model_object:
+                logger.error("测试计划已删除或不存在")
+                return HttpResponse(status=404, content='测试计划已删除或不存在')
+            model_object = model_object[0]
+            path_head = PLAN_ROOT
+
+        elif file_type == 'task_statement':
+            model_object = TestTask.objects.filter(id=task_id, delete=False)
+            if not model_object:
+                logger.error("测试任务已删除或不存在")
+                return HttpResponse(status=404, content='测试任务已删除或不存在')
+            model_object = model_object[0]
+            path_head = TASK_ROOT
+
+        # step7:创建文件的数据库字段
+        fids = ''
+        file_paths = []
+        for file in files:
+            try:
+                create_time = update_time = datetime.datetime.now()
+                file_dir = path_head + "/" + model_object.id + "/" + str(create_time)
+                if not os.path.exists(file_dir):
+                    os.makedirs(file_dir)
+                file_path = file_dir + "/" + file.name
+                with open(file_path, 'wb') as f:
+                    f.write(file.read())
+                f.close()
+            except:
+                logger.error("文件上传失败")
+                return HttpResponse(status=500, content="文件上传失败")
+
+            if not FileManager.objects.last():
+                id_file = "TestLaboratory_V1_File_1"
+            else:
+                id_file = gen_next_id(FileManager.objects.order_by('create_time').last().id)
+
+            try:
+                file_new = FileManager.objects.create(id=id_file, category=file_type,
+                                                      path=model_object.id+ "/" + str(create_time) + "/" + file.name,
+                                                      create_time=create_time,
+                                                      update_time=update_time)
+            except:
+                logger.error("文件路径写入数据库失败")
+                return HttpResponse(status=500, content="文件路径写入数据库失败")
+            file_paths.append(PLAN_ROOT + file_new.path)
+            fids += id_file + "&&"
+        fids = fids[:-2]
+        model_object.statement_file += ("&&" + fids)
+        model_object.save()
+        executor, action, method = get_log(request)
+        gen_log(action, "说明文档", model_object.title, method, executor)
+        return HttpResponse(status=200, content='新建文件成功')
+
+    # create file
+    @staticmethod
+    @RoleControl
+    def delete(request, *args, **kwargs):
+        file_id = request.GET.get('file_id')
+        file_type = request.GET.get('type')
+        plan_id = request.GET.get('plan_id')
+        task_id = request.GET.get('task_id')
+
+        model_object = None
+        if file_type == 'plan_statement':
+            model_object = TestPlan.objects.filter(id=plan_id, delete=False)
+            if not model_object:
+                logger.error("测试计划已删除或不存在")
+                return HttpResponse(status=404, content='测试计划已删除或不存在')
+            model_object = model_object[0]
+
+        elif file_type == 'task_statement':
+            model_object = TestTask.objects.filter(id=task_id, delete=False)
+            if not model_object:
+                logger.error("测试任务已删除或不存在")
+                return HttpResponse(status=404, content='测试任务已删除或不存在')
+            model_object = model_object[0]
+
+        file = FileManager.objects.filter(id=file_id, delete=False)
+        if not file:
+            logger.error("文件已删除不存在")
+            return HttpResponse(status=404, content='文件已删除不存在')
+        file = file[0]
+        file_list = model_object.statement_file.split('&&')
+        if file.id not in file_list:
+            logger.error("该文件不存在于测试计划或测试任务中,数据库错误")
+            return HttpResponse(status=404, content='该文件不存在于测试计划或测试任务中,数据库错误')
+        file_list.remove(file.id)
+        file_str = ''
+        for file_name in file_list:
+            file_str += file_name + "&&"
+        file_str = file_str[:-2]
+        model_object.statement_file = file_str
+        model_object.save()
+        executor, action, method = get_log(request)
+        gen_log(action, "说明文档", file.path.split('/')[-1], method, executor)
+        return HttpResponse(status=200, content='删除成功')

+ 0 - 0
apps/log/__init__.py


+ 3 - 0
apps/log/admin.py

@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.

+ 6 - 0
apps/log/apps.py

@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class LogInfoConfig(AppConfig):
+    default_auto_field = 'django.db.models.BigAutoField'
+    name = 'apps.log'

+ 0 - 0
apps/log/middleware/__init__.py


+ 33 - 0
apps/log/middleware/loginfo.py

@@ -0,0 +1,33 @@
+import re
+
+import jwt
+from django.http import HttpResponse
+
+from TestLaboratory.settings import SECRET_KEY
+from apps.user.models import User, Role
+
+
+class LogInfo(object):
+    """使用类装饰器来权限校验"""
+    def __init__(self, func):
+        self.func = func
+
+    def __call__(self, request, *args, **kwargs):
+        token = request.META.get('HTTP_ACCESSTOKEN')
+        SALT = SECRET_KEY
+
+        payload = jwt.decode(token, SALT, True)
+
+        user = User.objects.get(username=payload['username'])
+
+        role = Role.objects.get(id="TestLaboratory_V1_Role_"+str(user.identify+1))
+        permission = role.permissions.filter(method=request.method)
+
+        log = None
+        for per in permission:
+            if re.search(per.url, request.path[:-1]):
+                log = per
+                break
+
+        return self.func(request, *args, **kwargs)
+

+ 35 - 0
apps/log/migrations/0001_initial.py

@@ -0,0 +1,35 @@
+# Generated by Django 3.2 on 2021-07-17 20:05
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    initial = True
+
+    dependencies = [
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='Log',
+            fields=[
+                ('id', models.CharField(default='TestLaboratory_V1_Log_1', max_length=50, primary_key=True, serialize=False, verbose_name='日志id')),
+                ('action', models.CharField(max_length=100, verbose_name='操作名称')),
+                ('entity_class', models.CharField(max_length=50, verbose_name='实体类型')),
+                ('entity_name', models.CharField(max_length=50, null=True, verbose_name='实体名称')),
+                ('method', models.CharField(max_length=30, verbose_name='操作方法')),
+                ('executor_id', models.CharField(max_length=50, verbose_name='操作者id')),
+                ('executor_username', models.CharField(max_length=50, verbose_name='操作者名称')),
+                ('executor_identify', models.IntegerField(verbose_name='操作者身份')),
+                ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
+                ('update_time', models.DateTimeField(auto_now=True, verbose_name='修改时间')),
+                ('delete', models.BooleanField(default=False)),
+            ],
+            options={
+                'verbose_name': '日志',
+                'verbose_name_plural': '日志',
+                'db_table': 'log',
+            },
+        ),
+    ]

+ 18 - 0
apps/log/migrations/0002_alter_log_id.py

@@ -0,0 +1,18 @@
+# Generated by Django 3.2.4 on 2022-06-19 10:43
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('log', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='log',
+            name='id',
+            field=models.CharField(default='TestLaboratory_V1_Log_1', max_length=128, primary_key=True, serialize=False, verbose_name='日志id'),
+        ),
+    ]

+ 0 - 0
apps/log/migrations/__init__.py


+ 77 - 0
apps/log/models.py

@@ -0,0 +1,77 @@
+import datetime
+import re
+
+import jwt
+from django.db import models
+
+# Create your models here.
+from TestLaboratory.settings import SECRET_KEY
+from apps.user.models import User, Role
+from utils.util_add_id import get_id, gen_next_id
+
+
+class Log(models.Model):
+    id = models.CharField(max_length=128, primary_key=True, verbose_name='日志id', default='TestLaboratory_V1_Log_1')
+    action = models.CharField(max_length=100, verbose_name='操作名称')
+    entity_class = models.CharField(max_length=50, verbose_name='实体类型')
+    entity_name = models.CharField(max_length=50, null=True, verbose_name='实体名称')
+    method = models.CharField(max_length=30, verbose_name='操作方法')
+    executor_id = models.CharField(max_length=50, verbose_name='操作者id')
+    executor_username = models.CharField(max_length=50, verbose_name='操作者名称')
+    executor_identify = models.IntegerField(verbose_name='操作者身份')
+    create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
+    update_time = models.DateTimeField(auto_now=True, verbose_name='修改时间')
+    delete = models.BooleanField(default=False)
+
+    def __str__(self):
+        return self.executor_username + self.action
+
+    class Meta:
+        verbose_name = '日志'
+        verbose_name_plural = verbose_name
+        db_table = 'log'
+
+
+def gen_log(action, entity_class, entity_name, method, executor):
+    log_id = get_id(Log, "TestLaboratory_V1_Log_1")
+    create_time = update_time = datetime.datetime.now()
+    try:
+        log_new = Log.objects.create(id=log_id, action=action, entity_class=entity_class, entity_name=entity_name,
+                                     method=method, executor_id=executor.id, executor_username=executor.username,
+                                     executor_identify=executor.identify, create_time=create_time,
+                                     update_time=update_time)
+    except:
+        log_id = gen_next_id(log_id)
+        log_new = Log.objects.create(id=log_id, action=action, entity_class=entity_class, entity_name=entity_name,
+                                     method=method, executor_id=executor.id, executor_username=executor.username,
+                                     executor_identify=executor.identify, create_time=create_time,
+                                     update_time=update_time)
+
+
+def get_log(request):
+    token = request.META.get('HTTP_ACCESSTOKEN')
+    SALT = SECRET_KEY
+
+    payload = jwt.decode(token, SALT, True)
+
+    user = User.objects.get(id=payload['userid'])
+
+    role = Role.objects.get(id="TestLaboratory_V1_Role_" + str(user.identify + 1))
+    permission = role.permissions.filter(method=request.method)
+    # print(user.username)
+
+    perm = None
+    for per in permission:
+        path = request.path
+        if request.path[-1] == '/':
+            path = request.path[:-1]
+        # print(per.url, per.name, path, re.search(per.url, path))
+
+        if re.search(per.url, path):
+            # print("匹配", per.url, request.path)
+            if not perm:
+                perm = per
+            elif len(per.url) > len(perm.url):
+                perm = per
+    # print(perm.url, perm.name, perm.method)
+    return user, perm.name, perm.method

+ 3 - 0
apps/log/tests.py

@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.

+ 6 - 0
apps/log/urls.py

@@ -0,0 +1,6 @@
+from django.urls import path
+from .views.loglistview import LogListView
+
+urlpatterns = [
+    path('', LogListView.as_view())
+]

+ 0 - 0
apps/log/views/__init__.py


+ 47 - 0
apps/log/views/loglistview.py

@@ -0,0 +1,47 @@
+from django.db.models import Q
+from rest_framework.response import Response
+from rest_framework.views import APIView
+from django.http import HttpResponse
+
+from apps.log.models import Log
+
+
+class LogListView(APIView):
+    # view log_list
+    @staticmethod
+    def get(request, *args, **kwargs):
+        entity_class = request.GET.get('entity_class')
+        action = request.GET.get('action')
+        executor_username = request.GET.get('executor_username')
+        executor_identify = request.GET.get('executor_identify')
+        sort = request.GET.get('sort')
+        dont_get = request.GET.get('dontGet')
+
+        log_all = Log.objects.order_by('-create_time')
+        if entity_class:
+            log_all = log_all.filter(entity_class=entity_class)
+        if action:
+            log_all = log_all.filter(method=action)
+        if executor_username:
+            log_all = log_all.filter(executor_username=executor_username)
+        if executor_identify:
+            log_all = log_all.filter(executor_identify=executor_identify)
+        if dont_get == '1':
+            log_all = log_all.filter(~Q(method='GET'))
+        if sort:
+            log_all = log_all.order_by(sort)
+
+        log_infos = []
+        for log in log_all:
+            log_infos.append({
+                'entity_class': log.entity_class,
+                'entity_name': log.entity_name,
+                'action': log.action,
+                'method': log.method,
+                'executor_id': log.executor_id,
+                'executor_username': log.executor_username,
+                'executor_identify': log.executor_identify,
+                'execute_time': log.create_time
+            })
+
+        return Response(log_infos)

+ 0 - 0
apps/plan/__init__.py


+ 3 - 0
apps/plan/admin.py

@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.

+ 6 - 0
apps/plan/apps.py

@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class TestPlanConfig(AppConfig):
+    default_auto_field = 'django.db.models.BigAutoField'
+    name = 'apps.plan'

+ 0 - 0
apps/plan/dao/__init__.py


+ 0 - 0
apps/plan/dao/plandao.py


+ 38 - 0
apps/plan/migrations/0001_initial.py

@@ -0,0 +1,38 @@
+# Generated by Django 3.2 on 2021-07-17 20:05
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    initial = True
+
+    dependencies = [
+        ('user', '0001_initial'),
+        ('software', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='TestPlan',
+            fields=[
+                ('id', models.CharField(default='TestLaboratory_V1_Plan_1', max_length=30, primary_key=True, serialize=False, unique=True, verbose_name='测试计划编号')),
+                ('title', models.CharField(max_length=30, verbose_name='测试计划标题')),
+                ('version', models.CharField(max_length=20, verbose_name='软件版本')),
+                ('state', models.IntegerField(verbose_name='测试计划执行状态')),
+                ('description', models.TextField(verbose_name='测试计划描述')),
+                ('statement_file', models.TextField(verbose_name='说明文档id路径')),
+                ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
+                ('update_time', models.DateTimeField(auto_now=True, verbose_name='修改时间')),
+                ('delete', models.BooleanField(default=False)),
+                ('creator', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='user.user', verbose_name='测试计划执行者')),
+                ('software', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='software.software', verbose_name='测试软件')),
+            ],
+            options={
+                'verbose_name': '测试计划',
+                'verbose_name_plural': '测试计划',
+                'db_table': 'plan',
+            },
+        ),
+    ]

+ 18 - 0
apps/plan/migrations/0002_alter_testplan_id.py

@@ -0,0 +1,18 @@
+# Generated by Django 3.2.4 on 2022-06-19 10:43
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('plan', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='testplan',
+            name='id',
+            field=models.CharField(default='TestLaboratory_V1_Plan_1', max_length=128, primary_key=True, serialize=False, unique=True, verbose_name='测试计划编号'),
+        ),
+    ]

+ 0 - 0
apps/plan/migrations/__init__.py


+ 68 - 0
apps/plan/models.py

@@ -0,0 +1,68 @@
+import os
+
+from django.db import models
+
+# Create your models here.
+from TestLaboratory.settings import PLAN_ROOT
+from apps.file.models import FileManager
+from apps.software.models import Software
+from apps.user.models import User
+from utils.util_add_id import gen_next_id
+
+
+class TestPlan(models.Model):
+    id = models.CharField(max_length=128, unique=True, primary_key=True, verbose_name='测试计划编号',
+                          default='TestLaboratory_V1_Plan_1')
+    title = models.CharField(max_length=30, verbose_name='测试计划标题')
+    software = models.ForeignKey(Software, on_delete=models.CASCADE, verbose_name='测试软件')
+    version = models.CharField(max_length=20, verbose_name='软件版本')
+    creator = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, verbose_name='测试计划执行者')
+    state = models.IntegerField(verbose_name='测试计划执行状态')
+    description = models.TextField(verbose_name='测试计划描述')
+    statement_file = models.TextField(verbose_name='说明文档id路径')
+    create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
+    update_time = models.DateTimeField(auto_now=True, verbose_name='修改时间')
+    delete = models.BooleanField(default=False)
+
+    def __str__(self):
+        return self.title
+
+    class Meta:
+        verbose_name = '测试计划'
+        verbose_name_plural = verbose_name
+        db_table = 'plan'
+
+
+# def save_files(files, plan_id, time_stamp):
+#     """
+#
+#     """
+#     fids = []
+#     file_paths = []
+#     for file in files:
+#         try:
+#             file_dir = PLAN_ROOT + "/" + plan_id + "/" + str(time_stamp)
+#             if not os.path.exists(file_dir):
+#                 os.makedirs(file_dir)
+#             file_path = file_dir + "/" + file.name
+#             with open(file_path, 'wb') as f:
+#                 f.write(file.read())
+#             f.close()
+#         except:
+#             return 0
+#
+#         if not FileManager.objects.last():
+#             id_file = "TestLaboratory_V1_File_1"
+#         else:
+#             id_file = gen_next_id(FileManager.objects.order_by('create_time').last().id)
+#
+#         try:
+#             file_new = FileManager.objects.create(id=id_file, category="plan_statement",
+#                                                   path=plan_id + "/" + str(time_stamp) + "/" + file.name,
+#                                                   create_time=time_stamp,
+#                                                   update_time=time_stamp)
+#         except:
+#             return 1
+#         file_paths.append(PLAN_ROOT + file_new.path)
+#         fids.append(id_file)
+#     return file_paths, fids

+ 3 - 0
apps/plan/tests.py

@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.

+ 9 - 0
apps/plan/urls.py

@@ -0,0 +1,9 @@
+from django.urls import path
+from .views.planlistview import PlanListView
+from .views.planview import PlanView, PlanEdit
+
+urlpatterns = [
+    path('', PlanListView.as_view()),
+    path('<str:plan_id>', PlanView.as_view()),
+    path('<str:plan_id>/edit', PlanEdit.as_view())
+]

+ 0 - 0
apps/plan/views/__init__.py


+ 168 - 0
apps/plan/views/planlistview.py

@@ -0,0 +1,168 @@
+import datetime
+import json
+import os
+
+from rest_framework.response import Response
+from rest_framework.views import APIView
+from django.http import HttpResponse
+
+from apps.file.models import FileManager
+from apps.log.models import get_log, gen_log
+from apps.plan.models import TestPlan
+from apps.software.models import Software
+from apps.user.middleware.rolecontrol import RoleControl
+from apps.user.models import User
+from utils.util_add_id import gen_next_id, get_id
+from TestLaboratory.settings import PLAN_ROOT
+
+import logging
+logger = logging.getLogger('django')
+
+
+class PlanListView(APIView):
+    # 登录权限验证
+    authentication_classes = []
+
+    # create plan
+    @staticmethod
+    @RoleControl
+    def post(request, *args, **kwargs):
+        software_id = request.POST.get('software_id')
+        version_number = request.POST.get('version_number')
+        title = request.POST.get('title')
+        description = request.POST.get('description')
+        files = request.FILES.getlist('files')
+        creator_id = request.POST.get('creator')
+
+        # step1:判断所属软件及版本是否存在
+        software = Software.objects.filter(id=software_id, delete=False)
+        if not software:
+            logger.error("软件已删除或不存在")
+            return HttpResponse(status=404, content='软件已删除或不存在')
+        software = software[0]
+
+        # 判断版本是否存在
+        software_version = software.version_set.filter(number=version_number, delete=False)
+        if not software_version:
+            logger.error("软件版本不存在或已删除,请重新选择")
+            return HttpResponse(status=404, content='软件版本不存在或已删除,请重新选择')
+        software_version = software_version[0]
+
+        # 判断用户是否存在
+        creator = User.objects.filter(id=creator_id, delete=False)
+        if not creator:
+            logger.error("用户不存在,创建失败")
+            return HttpResponse(status=404, content='用户不存在,创建失败')
+        creator = creator[0]
+
+        # step5:设置创建时间和修改时间
+        create_time = update_time = datetime.datetime.now()
+
+        # step6:创建测试计划的数据库记录
+        id_plan = get_id(TestPlan, "TestLaboratory_V1_Plan_1")
+
+        # step7:创建文件的数据库字段
+        fids = ''
+        file_paths = []
+        file_ids = []
+        for file in files:
+            try:
+                file_dir = PLAN_ROOT + "/" + id_plan + "/" + str(create_time)
+                if not os.path.exists(file_dir):
+                    os.makedirs(file_dir)
+                file_path = file_dir + "/" + file.name
+                with open(file_path, 'wb') as f:
+                    f.write(file.read())
+                f.close()
+            except:
+                logger.error("文件上传失败")
+                return HttpResponse(status=500, content="文件上传失败")
+
+            if not FileManager.objects.last():
+                id_file = "TestLaboratory_V1_File_1"
+            else:
+                id_file = gen_next_id(FileManager.objects.order_by('create_time').last().id)
+
+            try:
+                file_new = FileManager.objects.create(id=id_file, category="plan_statement",
+                                                      path=id_plan + "/" + str(create_time) + "/" + file.name,
+                                                      create_time=create_time,
+                                                      update_time=update_time)
+            except:
+                logger.error("文件路径写入数据库失败")
+                return HttpResponse(status=500, content="文件路径写入数据库失败")
+            file_ids.append(id_file)
+            file_paths.append(PLAN_ROOT + file_new.path)
+            fids += id_file + "&&"
+        fids = fids[:-2]
+
+        try:
+            plan = TestPlan.objects.create(id=id_plan, title=title, description=description, state='0',
+                                           creator=creator, software=software, version=software_version.number,
+                                           statement_file=fids,
+                                           create_time=create_time, update_time=update_time)
+        except:
+            logger.error("测试计划写入数据库失败")
+            return HttpResponse(status=500, content='测试计划写入数据库失败')
+
+        executor, action, method = get_log(request)
+        gen_log(action, "测试计划", plan.title, method, executor)
+        return Response({
+            'software_id': software.id,
+            'software_name': software.name,
+            'version': version_number,
+            'creator_id': creator.id,
+            'creator': creator.username,
+            'id': plan.id,
+            'title': title,
+            'description': description,
+            'state': '执行中',
+            'files': [{'file_id': file_id, 'file_name': file.name, 'file_url': file_path} for (file_id, file, file_path)
+                      in zip(file_ids, files, file_paths)]
+        })
+
+    # view plan_list
+    @staticmethod
+    @RoleControl
+    def get(request, *args, **kwargs):
+        title = request.GET.get('title')
+        software_id = request.GET.get('software_id')
+        state = request.GET.get('state')
+        creator_id = request.GET.get('creator_id')
+        sort = request.GET.get('sort')
+
+        plan_all = TestPlan.objects.order_by('-create_time')
+        if title:
+            plan_all = plan_all.filter(title__contains=title)
+        if software_id:
+            plan_all = plan_all.filter(software__id__contains=software_id)
+        if state:
+            plan_all = plan_all.filter(state=state)
+        if creator_id:
+            plan_all = plan_all.filter(creator__id__contains=creator_id)
+        """
+        sort=update_time, -update_time, create_time, -create_time, title, -title, state
+        """
+        if sort:
+            plan_all = plan_all.order_by(sort)
+        plan_all = plan_all.filter(delete=False)
+
+        if not plan_all:
+            return Response([])
+
+        info = []
+        for plan in plan_all.all():
+            info.append({
+                'software_id': plan.software.id,
+                'software_name': plan.software.name,
+                'version': plan.version,
+                'creator_id': plan.creator.id,
+                'creator': plan.creator.username,
+                'id': plan.id,
+                'title': plan.title,
+                'description': plan.description,
+                'state': plan.state,
+                'create_time': plan.create_time,
+                'update_time': plan.update_time,
+            })
+        return Response(info)

+ 146 - 0
apps/plan/views/planview.py

@@ -0,0 +1,146 @@
+import datetime
+
+from rest_framework.response import Response
+from rest_framework.views import APIView
+from django.http import HttpResponse
+
+from TestLaboratory.settings import PLAN_ROOT, HTTP_HEAD
+from apps.log.models import get_log, gen_log
+from apps.plan.models import TestPlan
+from apps.file.models import FileManager
+from utils.util_add_id import get_id
+from utils.util_file.util_fileio import write_file
+
+import logging
+logger = logging.getLogger('django')
+
+
+class PlanView(APIView):
+    # 登录权限验证
+    authentication_classes = []
+
+    # view plan
+    @staticmethod
+    def get(request, plan_id, *args, **kwargs):
+        plan = TestPlan.objects.filter(id=plan_id, delete=False)
+        if not plan:
+            logger.error("测试计划不存在")
+            return HttpResponse(status=404, content='测试计划不存在')
+        plan = plan[0]
+
+        plan_sids = plan.statement_file.split('&&')
+
+        file_names = []
+        file_urls = []
+        if plan_sids[0]:
+            for statement_id in plan_sids:
+                statement = FileManager.objects.get(id=statement_id)
+                file_urls.append(PLAN_ROOT + statement.path)
+                file_names.append(statement.path.split('/')[-1])
+
+        tasks_info = []
+        tasks = plan.testtask_set.filter(delete=False)
+        if tasks:
+            for task in tasks:
+                tasks_info.append({
+                    'id': task.id,
+                    'title': task.title,
+                    'description': task.description,
+                    'state': task.state,
+                    'create_time': task.create_time,
+                    'update_time': task.update_time
+                })
+        executor, action, method = get_log(request)
+        gen_log(action, "测试计划", plan.title, method, executor)
+        return Response({
+            'software_id': plan.software.id,
+            'software_name': plan.software.name,
+            'version': plan.version,
+            'creator_id': plan.creator.id,
+            'creator': plan.creator.username,
+            'id': plan.id,
+            'title': plan.title,
+            'description': plan.description,
+            'state': plan.state,
+            'tasks': tasks_info,
+            'create_time': plan.create_time,
+            'update_time': plan.update_time,
+            'statements': [{'file_id': file_sid, 'name': file_name, 'url': HTTP_HEAD + file_url} for file_sid, file_name
+                           , file_url in zip(plan_sids, file_names, file_urls)]
+        })
+
+    # delete plan_list
+    @staticmethod
+    def delete(request, plan_id, *args, **kwargs):
+        plan = TestPlan.objects.filter(id=plan_id, delete=False)
+        if not plan:
+            logger.error("测试计划已删除或不存在")
+            return HttpResponse(status=404, content='测试计划已删除或不存在')
+        plan = plan[0]
+
+        plan_sids = plan.statement_file.split('&&')
+
+        if plan_sids[0]:
+            for statement_id in plan_sids:
+                statement = FileManager.objects.get(id=statement_id)
+                statement.delete = True
+                statement.save()
+        plan.delete = True
+        plan.save()
+        executor, action, method = get_log(request)
+        gen_log(action, "测试计划", plan.title, method, executor)
+        return HttpResponse("删除完成")
+
+
+class PlanEdit(APIView):
+    # 登录权限验证
+    authentication_classes = []
+
+    # edit plan_list
+    @staticmethod
+    def post(request, plan_id, *args, **kwargs):
+        title = request.POST.get('title')
+        state = request.POST.get('state')
+        description = request.POST.get('description')
+        files = request.FILES.getlist('files')
+
+        plan = TestPlan.objects.filter(id=plan_id, delete=False)
+        if not plan:
+            logger.error("测试计划已删除或不存在")
+            return HttpResponse(status=404, content='测试计划已删除或不存在')
+        plan = plan[0]
+
+        plan.title = title
+        plan.state = state
+        plan.description = description
+
+        fids = ''
+        for file in files:
+            create_time = update_time = datetime.datetime.now()
+            try:
+                write_file(file,
+                           PLAN_ROOT + "/" + plan_id + "/" + str(create_time))
+            except:
+                logger.error("文件上传失败")
+                return HttpResponse(status=500, content="文件上传失败")
+
+            id_file = get_id(FileManager, "TestLaboratory_V1_File_1")
+
+            try:
+                file_new = FileManager.objects.create(id=id_file,
+                                                      category="plan_statement",
+                                                      path=plan_id + "/" + str(
+                                                          create_time) + "/" + file.name,
+                                                      create_time=create_time,
+                                                      update_time=update_time)
+            except:
+                logger.error("文件路径写入数据库失败")
+                return HttpResponse(status=500, content="文件路径写入数据库失败")
+            fids += id_file + '&&'
+        fids = fids[:-2]
+
+        plan.statement_file += fids
+        plan.save()
+        executor, action, method = get_log(request)
+        gen_log(action, "测试计划", plan.title, method, executor)
+        return HttpResponse("编辑已保存")

+ 0 - 0
apps/software/__init__.py


+ 3 - 0
apps/software/admin.py

@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.

+ 6 - 0
apps/software/apps.py

@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class SoftwareConfig(AppConfig):
+    default_auto_field = 'django.db.models.BigAutoField'
+    name = 'apps.software'

+ 49 - 0
apps/software/migrations/0001_initial.py

@@ -0,0 +1,49 @@
+# Generated by Django 3.2 on 2021-07-17 20:05
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    initial = True
+
+    dependencies = [
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='Software',
+            fields=[
+                ('name', models.CharField(max_length=50, verbose_name='被测试软件名')),
+                ('id', models.CharField(default='TestLaboratory_V1_Software_1', max_length=30, primary_key=True, serialize=False, unique=True, verbose_name='被测试软件编号')),
+                ('type', models.CharField(max_length=20, verbose_name='被测试软件类型')),
+                ('report_file', models.TextField(null=True, verbose_name='测试报告id文件')),
+                ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
+                ('update_time', models.DateTimeField(auto_now=True, verbose_name='修改时间')),
+                ('delete', models.BooleanField(default=False)),
+            ],
+            options={
+                'verbose_name': '被测试软件',
+                'verbose_name_plural': '被测试软件',
+                'db_table': 'software',
+            },
+        ),
+        migrations.CreateModel(
+            name='Version',
+            fields=[
+                ('id', models.CharField(default='TestLaboratory_V1_Version_1', max_length=30, primary_key=True, serialize=False, unique=True, verbose_name='软件版本')),
+                ('number', models.CharField(max_length=20, verbose_name='版本号')),
+                ('version_file', models.TextField(verbose_name='软件路径id文件')),
+                ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
+                ('update_time', models.DateTimeField(auto_now=True, verbose_name='修改时间')),
+                ('delete', models.BooleanField(default=False)),
+                ('software', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='software.software', verbose_name='所属软件')),
+            ],
+            options={
+                'verbose_name': '软件版本',
+                'verbose_name_plural': '软件版本',
+                'db_table': 'version',
+            },
+        ),
+    ]

+ 18 - 0
apps/software/migrations/0002_alter_software_name.py

@@ -0,0 +1,18 @@
+# Generated by Django 3.2.4 on 2022-06-19 10:43
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('software', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='software',
+            name='name',
+            field=models.CharField(max_length=128, verbose_name='被测试软件名'),
+        ),
+    ]

+ 0 - 0
apps/software/migrations/__init__.py


+ 95 - 0
apps/software/models.py

@@ -0,0 +1,95 @@
+import datetime
+
+from django.db import models
+
+from utils.util_add_id import gen_next_id
+from utils.util_create_file import create_version_file
+
+# Create your models here.
+
+
+class Software(models.Model):
+    name = models.CharField(max_length=128, verbose_name='被测试软件名')
+    id = models.CharField(max_length=30, unique=True, primary_key=True, verbose_name='被测试软件编号',
+                          default='TestLaboratory_V1_Software_1')
+    type = models.CharField(max_length=20, verbose_name='被测试软件类型')
+    report_file = models.TextField(null=True, verbose_name='测试报告id文件')
+    create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
+    update_time = models.DateTimeField(auto_now=True, verbose_name='修改时间')
+    delete = models.BooleanField(default=False)
+
+    def __str__(self):
+        return self.name
+
+    class Meta:
+        verbose_name = '被测试软件'
+        verbose_name_plural = verbose_name
+        db_table = 'software'
+
+
+class Version(models.Model):
+    id = models.CharField(max_length=30, unique=True, primary_key=True, verbose_name='软件版本',
+                          default='TestLaboratory_V1_Version_1')
+    number = models.CharField(max_length=20, verbose_name='版本号')
+    software = models.ForeignKey(Software, on_delete=models.CASCADE, verbose_name='所属软件')
+    version_file = models.TextField(verbose_name='软件路径id文件')
+    create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
+    update_time = models.DateTimeField(auto_now=True, verbose_name='修改时间')
+    delete = models.BooleanField(default=False)
+
+    def __str__(self):
+        return self.software.name + " " + self.number
+
+    class Meta:
+        verbose_name = '软件版本'
+        verbose_name_plural = verbose_name
+        db_table = 'version'
+
+
+def create_software(name, software_type, version_num, file):
+    # duplicated
+    # # step1:判断软件是否已存在
+    # if Software.objects.filter(name=name):
+    #     return {'code': 500, 'msg': '被测试软件已存在,请修改软件名重新上传'}
+
+    # step2:创建id和时间
+    pass
+
+
+def get_software(name, software_type, sort):
+    pass
+
+
+def create_version(software_id, version_num, file):
+    # # step1:判断版本所属软件是否存在
+    # software = Software.objects.filter(id=software_id, delete=False)
+    # if not software:
+    #     return {'code': 500, 'msg': '被测软件不存在,请检查是否未创建或已删除'}
+    # software = software[0]
+    #
+    # # step2:判断版本是否存在
+    # software_version = software.version_set.filter(num=version_num, delete=False)
+    # if software_version:
+    #     return {'code': 500, 'msg': '软件版本已存在,请修改版本号后重新上传'}
+    #
+    # # step3:上传版本软件实体到指定路径,返回文件路径
+    # create_info = create_version_file(file, software.name, version_num)
+    # if create_info['code'] != 200:
+    #     return create_info
+    # file_path = create_info['file_path']
+    #
+    # # step4:创建版本id
+    # id_version = None
+    # if not Version.objects.order_by('create_time').last():
+    #     id_version = "Version_1"
+    # else:
+    #     id_version = gen_next_id(Version.objects.order_by('create_time').last().id)
+    #
+    # # step5:设置创建时间和修改时间
+    # create_time = update_time = datetime.datetime.now()
+    #
+    # # step6:数据库新增字段
+    # version = Version.objects.create(id=id_version, num=version_num, software=software, create_time=create_time,
+    #                                  update_time=update_time, file=file_path)
+    # return {'code': 200, 'msg': '新版本创建成功'}
+    pass

+ 3 - 0
apps/software/tests.py

@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.

+ 0 - 0
apps/software/tests/__init__.py


+ 0 - 0
apps/software/tests/softwaretest.py


+ 16 - 0
apps/software/urls.py

@@ -0,0 +1,16 @@
+from django.urls import path
+from .views.softwarelistview import SoftwareListView
+from .views.softwareview import SoftwareView, SoftwareEdit
+from .views.report import Report
+from .views.versionlistview import VersionListView
+from .views.versionview import VersionView, VersionEdit
+
+urlpatterns = [
+    path('versions', VersionListView.as_view()),
+    path('', SoftwareListView.as_view()),
+    path('<str:software_id>', SoftwareView.as_view()),
+    path('<str:software_id>/edit', SoftwareEdit.as_view()),
+    path('<str:software_id>/versions', VersionView.as_view()),
+    path('<str:software_id>/versions/edit', VersionEdit.as_view()),
+    path('<str:software_id>/report', Report.as_view())
+]

+ 0 - 0
apps/software/views/__init__.py


+ 77 - 0
apps/software/views/report.py

@@ -0,0 +1,77 @@
+from django.http import HttpResponse
+from rest_framework.views import APIView
+
+import util_doc
+from apps.software.models import Software
+
+import logging
+logger = logging.getLogger('django')
+
+
+class Report(APIView):
+    authentication_classes = []
+
+    # generate report
+    @staticmethod
+    # @RoleControl
+    def get(request, software_id, *args, **kwargs):
+        software = Software.objects.filter(id=software_id, delete=False)
+        if not software:
+            logger.error("软件已删除或不存在")
+            return HttpResponse(status=404, content='软件已删除或不存在')
+        software = software[0]
+
+        executor_id = request.GET.get('executor_id')
+        if not executor_id:
+            logger.error("测试人员id不能为空")
+            return HttpResponse(status=404, content="测试人员id不能为空")
+        cases_info = {}
+        cases = software.testcase_set.filter(executor_id=executor_id, delete=False)
+        executors = set()
+        if cases:
+            for case in cases:
+                state = '未执行'
+                if case.state == 1:
+                    state = '执行成功'
+                elif case.state == 2:
+                    state = '执行失败'
+                cases_info[case.id_in_task] = {
+                    'id': case.id_in_task,
+                    'version': case.version,
+                    'name': case.name,
+                    'requisite': case.requisite,
+                    'priority': case.priority,
+                    'environment': case.environment,
+                    'type': case.type,
+                    'process': case.process,
+                    'expected_result': case.expected_result,
+                    'actual_result': case.actual_result,
+                    'state': state,
+                    'writer': case.writer,
+                    'assessor': case.assessor,
+                    'executor': case.executor,
+                    'remark': case.remark,
+                    'execute_time': str(case.update_time)[:10]
+                }
+                executors.add(case.executor)
+
+        creators = []
+        plans = software.testplan_set.filter(delete=False)
+        if plans:
+            for plan in plans:
+                creators.append(plan.creator.name)
+
+        version_number = software.version_set.order_by('-create_time')[0].number
+
+        file_name = '软件测试报告.docx'
+        print(str(software.create_time).split(' ')[0])
+        document = util_doc.process_doc(software.name, software.type, version_number, str(software.create_time)
+                                        .split(' ')[0], executors, cases_info)
+
+        response = HttpResponse(content_type='application/octet-stream')
+        response['Content-Disposition'] = 'attachment; filename="{0}"'.format(file_name).encode('utf-8', 'ISO-8859-1')
+        document.save(response)
+
+        # executor, action, method = get_log(request)
+        # gen_log(action, "测试报告", software_id+"-测试报告", method, executor)
+        return response

+ 126 - 0
apps/software/views/softwarelistview.py

@@ -0,0 +1,126 @@
+import datetime
+import os
+
+from rest_framework.response import Response
+from rest_framework.views import APIView
+from django.http import HttpResponse, JsonResponse, Http404
+
+from TestLaboratory.settings import SOFTWARE_ROOT
+from apps.file.models import FileManager
+from apps.log.models import gen_log, get_log
+from apps.software.models import Software, create_software, get_software, Version
+import json
+
+from apps.user.middleware.rolecontrol import RoleControl
+from utils.util_add_id import gen_next_id
+
+import logging
+logger = logging.getLogger('django')
+
+
+class SoftwareListView(APIView):
+    # create software
+    @staticmethod
+    @RoleControl
+    def post(request, *args, **kwargs):
+        name = request.POST.get('name')
+        software_type = request.POST.get('type')
+        version_num = request.POST.get('version')
+        file = request.FILES.get('file')
+        if not file:
+            logger.error("软件上传失败")
+            return HttpResponse(status=404, content='软件上传失败')
+
+        # create_msg = create_software(name, software_type, version, file)
+        id_ = None
+        if not Software.objects.order_by('create_time').last():
+            id_ = "TestLaboratory_V1_Software_1"
+        else:
+            id_ = gen_next_id(Software.objects.order_by('create_time').last().id)
+        create_time = update_time = datetime.datetime.now()
+
+        # step4:创建版本id
+        id_version = None
+        if not Version.objects.last():
+            id_version = "TestLaboratory_V1_Version_1"
+        else:
+            id_version = gen_next_id(Version.objects.order_by('create_time').last().id)
+
+        # step6:上传文件:
+        file_dir = SOFTWARE_ROOT + id_ + "/" + id_version + "/" + str(create_time)
+        if not os.path.exists(file_dir):
+            os.makedirs(file_dir)
+        file_path = file_dir + "/" + file.name
+        with open(file_path, 'wb') as f:
+            f.write(file.read())
+        f.close()
+
+        # step7:创建文件对象
+        id_file = None
+        if not FileManager.objects.last():
+            id_file = "TestLaboratory_V1_File_1"
+        else:
+            id_file = gen_next_id(FileManager.objects.order_by('create_time').last().id)
+
+        try:
+            file_new = FileManager.objects.create(id=id_file, category="software",
+                                                  path=id_ + "/" + id_version + "/" +
+                                                       str(create_time) + "/" + file.name,
+                                                  create_time=create_time, update_time=update_time)
+
+            # step3:数据库新增软件字段
+            software = Software.objects.create(name=name, id=id_, type=software_type, create_time=create_time,
+                                               update_time=update_time)
+            # step5:创建版本
+            version = Version.objects.create(id=id_version, number=version_num, software=software,
+                                             version_file=id_file,
+                                             create_time=create_time,
+                                             update_time=update_time)
+
+        except:
+            logger.error("测试项目创建失败")
+            return HttpResponse(content='创建失败', status=500)
+        executor, action, method = get_log(request)
+        gen_log(action, "测试目标", software.name, method, executor)
+        return Response({
+            'id': software.id,
+            'name': software.name,
+            'type': software.type,
+            'create_time': software.create_time,
+            'update_time': software.update_time,
+        })
+
+    # software list
+    @staticmethod
+    @RoleControl
+    def get(request, *args, **kwargs):
+        name = request.GET.get('name')
+        software_type = request.GET.get('type')
+        sort = request.GET.get('sort')
+
+        software_all = Software.objects.order_by('-create_time')
+        if name:
+            software_all = software_all.filter(name__contains=name)
+        if software_type:
+            software_all = software_all.filter(type__contains=software_type)
+        """
+        sort=update_time, -update_time, create_time, -create_time, name, -name, type, -type
+        """
+        if sort:
+            software_all = software_all.order_by(sort)
+
+        software_all = software_all.filter(delete=False)
+
+        if not software_all:
+            return Response([])
+
+        info = []
+        for software in software_all.all():
+            info.append({
+                'id': software.id,
+                'name': software.name,
+                'type': software.type,
+                'create_time': software.create_time,
+                'update_time': software.update_time,
+            })
+        return Response(info)

+ 132 - 0
apps/software/views/softwareview.py

@@ -0,0 +1,132 @@
+import json
+
+from django.http import HttpResponse
+from rest_framework.response import Response
+from rest_framework.views import APIView
+
+from TestLaboratory.settings import SOFTWARE_ROOT, HTTP_HEAD
+from apps.file.models import FileManager
+from apps.log.models import get_log, gen_log
+from apps.software.models import Software
+
+from django.http.request import QueryDict
+
+from apps.user.middleware.rolecontrol import RoleControl
+
+import logging
+logger = logging.getLogger('django')
+
+
+class SoftwareView(APIView):
+    # 登录权限验证
+    authentication_classes = []
+
+    # view software
+    @staticmethod
+    @RoleControl
+    def get(request, software_id, *args, **kwargs):
+        software = Software.objects.filter(id=software_id, delete=False)
+        if not software:
+            logger.error("软件已删除或不存在")
+            return HttpResponse(status=404, content='软件已删除或不存在')
+        software = software[0]
+        try:
+            versions = []
+            for version in software.version_set.filter(delete=False):
+                version_fid = version.version_file
+                file_path = FileManager.objects.filter(id=version_fid)[0].path
+
+                versions.append({
+                    'id': version.id, 'version': version.number,
+                    'file': file_path.split('/')[-1],
+                    'file_url': HTTP_HEAD+SOFTWARE_ROOT+file_path,
+                    'create_time': version.create_time, 'update_time': version.update_time
+                })
+
+            task_num = 0
+            if software.testplan_set.filter(delete=False):
+                for plan in software.testplan_set.filter(delete=False):
+                    task_num += len(plan.testtask_set.filter(delete=False))
+
+            software_info = {'name': software.name, 'type': software.type, 'create_time': software.create_time,
+                             'update_time': software.update_time,
+                             'versions': versions,
+                             'case_num': len(software.testcase_set.filter(delete=False)),
+                             'task_num': task_num}
+        except:
+            logger.error("测试项目查询失败")
+            return HttpResponse(status=500, content='查询失败')
+        executor, action, method = get_log(request)
+        gen_log(action, "测试目标", software.name, method, executor)
+        return Response(software_info)
+
+    # delete software
+    @staticmethod
+    @RoleControl
+    def delete(request, software_id, *args, **kwargs):
+        software = Software.objects.filter(id=software_id, delete=False)
+        if not software:
+            logger.error("软件已删除或不存在")
+            return HttpResponse(status=404, content='软件{}已删除或不存在'.format(software_id))
+        software = software[0]
+
+        if software.version_set.exists():
+            for version in software.version_set.filter():
+                version_fid = version.version_file
+                file_version = FileManager.objects.filter(id=version_fid)[0]
+                file_version.delete = True
+                file_version.save()
+
+                # 删除测试计划
+                plan_set = software.testplan_set.filter(delete=False)
+                if plan_set:
+                    for plan in plan_set:
+                        task_set = plan.testtask_set.filter(delete=False)
+                        if task_set:
+                            for task in task_set:
+                                task.delete = True
+                                task.save()
+                        plan.delete = True
+                        plan.save()
+
+                version.delete = True
+                version.save()
+
+        software.delete = True
+        software.save()
+
+        executor, action, method = get_log(request)
+        gen_log(action, "测试目标", software.name, method, executor)
+        return HttpResponse(status=200, content='删除完成')
+
+
+class SoftwareEdit(APIView):
+    # 登录权限验证
+    authentication_classes = []
+
+    # edit software
+    @staticmethod
+    @RoleControl
+    def post(request, software_id, *args, **kwargs):
+        software = Software.objects.filter(id=software_id, delete=False)
+        if not software:
+            logger.error("软件已删除或不存在")
+            return HttpResponse(status=404, content='软件已删除或不存在')
+        software = software[0]
+
+        name = request.POST.get('name')
+        software_type = request.POST.get('type')
+
+        if not name:
+            logger.error("软件名不能为空")
+            return HttpResponse(status=500, content='软件名不能为空')
+        if name == software.name and software_type == software.type:
+            logger.error("未监测到软件名称及类型变化,编辑不保存")
+            return HttpResponse(status=500, content='未监测到软件名称及类型变化,编辑不保存')
+        software.name = name
+        software.type = software_type
+        software.save()
+        # print(software)
+        executor, action, method = get_log(request)
+        gen_log(action, "测试目标", software.name, method, executor)
+        return HttpResponse(status=200, content='编辑已保存')

+ 50 - 0
apps/software/views/versionlistview.py

@@ -0,0 +1,50 @@
+from django.http import HttpResponse
+from rest_framework.response import Response
+from rest_framework.views import APIView
+
+from apps.software.models import Software, Version
+from apps.task.models import TestCase
+from apps.user.middleware.rolecontrol import RoleControl
+
+import logging
+logger = logging.getLogger('django')
+
+
+class VersionListView(APIView):
+
+    # create version_list
+    @staticmethod
+    @RoleControl
+    def get(request, *args, **kwargs):
+        software_id = request.GET.get('software_id')
+        sort = request.GET.get('sort')
+        version_all = Version.objects.filter(delete=False).order_by('-create_time')
+
+        if software_id:
+
+            software = Software.objects.filter(id=software_id, delete=False)
+            if not software:
+                logger.error("软件不存在")
+                return HttpResponse(status=404, content='软件不存在')
+            software = software[0]
+            version_all = version_all.filter(software=software)
+
+        version_infos = []
+
+        if not version_all:
+            return Response(version_infos)
+
+        if sort:
+            version_all = version_all.order_by(sort)
+
+        for version in version_all:
+            software = Software.objects.filter(id=version.software_id, delete=False)[0]
+            fail_num = len(TestCase.objects.filter(software=software, version=version.number, state=2, delete=False))
+            version_infos.append({
+                'software_id': software.id,
+                'software_name': software.name,
+                'version_number': version.number,
+                'create_time': version.create_time,
+                'fail_num': fail_num
+            })
+        return Response(version_infos)

+ 304 - 0
apps/software/views/versionview.py

@@ -0,0 +1,304 @@
+import datetime
+
+from django.db.models import Q
+from rest_framework.response import Response
+from rest_framework.views import APIView
+from django.http import HttpResponse
+
+from TestLaboratory.settings import SOFTWARE_ROOT, CASE_FILE_EXPORT
+from apps.file.models import FileManager
+from apps.log.models import get_log, gen_log
+from apps.software.models import Software, Version
+from apps.task.models import TestCase
+from apps.user.middleware.rolecontrol import RoleControl
+from util_xlsx import write_to_xlsx
+from utils.util_add_id import gen_next_id
+
+import os
+
+from utils.util_file.util_fileio import write_file
+
+import logging
+logger = logging.getLogger('django')
+
+
+class VersionView(APIView):
+    authentication_classes = []
+
+    # create version
+    @staticmethod
+    @RoleControl
+    def post(request, software_id, *args, **kwargs):
+        version_num = request.POST.get('version')
+        file = request.FILES.get('file')
+
+        # step1:判断版本所属软件是否存在
+        software = Software.objects.filter(id=software_id, delete=False)
+        if not software:
+            logger.error("软件已删除或不存在")
+            return HttpResponse(status=404, content='软件已删除或不存在')
+        software = software[0]
+
+        # step2:判断版本是否存在
+        software_version = software.version_set.filter(number=version_num, delete=False)
+        if software_version:
+            logger.error("软件版本已存在,请修改版本号后重新上传")
+            return HttpResponse(status=404, content='软件版本已存在,请修改版本号后重新上传')
+
+        # step3:判断文件是否上传
+        if not file:
+            logger.error("软件上传失败")
+            return HttpResponse(status=404, content='软件上传失败')
+
+        # step4:创建版本id
+        id_version = None
+        if not Version.objects.order_by('create_time').last():
+            id_version = "TestLaboratory_V1_Version_1"
+        else:
+            id_version = gen_next_id(Version.objects.order_by('create_time').last().id)
+
+        # step5:设置创建时间和修改时间
+        create_time = update_time = datetime.datetime.now()
+
+        # step6:上传文件:
+        file_dir = SOFTWARE_ROOT + software.id + "/" + id_version + "/" + str(create_time)
+        if not os.path.exists(file_dir):
+            os.makedirs(file_dir)
+        file_path = file_dir + "/" + file.name
+        with open(file_path, 'wb') as f:
+            f.write(file.read())
+        f.close()
+
+        # step7:创建文件对象
+        id_file = None
+        if not FileManager.objects.last():
+            id_file = "TestLaboratory_V1_File_1"
+        else:
+            id_file = gen_next_id(FileManager.objects.order_by('create_time').last().id)
+
+        try:
+            # step5:创建版本
+            version = Version.objects.create(id=id_version, number=version_num, software=software,
+                                             version_file=id_file,
+                                             create_time=create_time,
+                                             update_time=update_time)
+        except:
+            logger.error("软件版本创建失败")
+            return HttpResponse(content='软件版本创建失败', status=500)
+        try:
+            file_new = FileManager.objects.create(id=id_file, category="software",
+                                                  path=software.id + "/" + version.id + "/" +
+                                                       str(create_time) + "/" + file.name,
+                                                  create_time=create_time, update_time=update_time)
+        except:
+            logger.error("文件数据创建失败")
+            return HttpResponse(content='文件数据创建失败', status=500)
+        executor, action, method = get_log(request)
+        gen_log(action, "目标版本", software.name+"-"+version.number, method, executor)
+        return Response({
+            'software_name': software.name,
+            'software_id': software.id,
+            'id': version.id,
+            'number': version.number,
+            'file_id': file_new.id,
+            'file_name': file_new.path.split('/')[-1],
+            'create_time': version.create_time,
+            'update_time': version.update_time,
+        })
+
+    # delete version
+    @staticmethod
+    @RoleControl
+    def delete(request, software_id, *args, **kwargs):
+        version_number = request.GET.get('version_number')
+        software = Software.objects.filter(id=software_id, delete=False)
+        if not software:
+            logger.error("软件已删除或不存在")
+            return HttpResponse(content='软件已删除或不存在', status=500)
+        software = software[0]
+
+        versions = len(list(software.version_set.filter(delete=False)))
+        if versions == 0:
+            logger.error("被测软件无版本信息,请检查错误")
+            return HttpResponse(content='被测软件无版本信息,请检查错误', status=500)
+        elif versions == 1:
+            version = software.version_set.filter(number=version_number, delete=False)[0]
+            version_fid = version.version_file
+            version_file = FileManager.objects.filter(id=version_fid)[0]
+            software.delete = True
+            version.delete = True
+            version_file.delete = True
+
+            # 删除测试计划
+            plan_set = software.testplan_set.filter(delete=False)
+            if plan_set:
+                for plan in plan_set:
+                    task_set = plan.testtask_set.filter(delete=False)
+                    if task_set:
+                        for task in task_set:
+                            task.delete = True
+                            task.save()
+                    plan.delete = True
+                    plan.save()
+
+            software.save()
+            version.save()
+            version_file.save()
+            return HttpResponse(status=200, content='软件及版本信息删除完成')
+        else:
+            version = software.version_set.filter(number=version_number, delete=False)[0]
+            version_fid = version.version_file
+            version_file = FileManager.objects.filter(id=version_fid)[0]
+            version.delete = True
+            version_file.delete = True
+
+            # 删除测试计划
+            plan_set = software.testplan_set.filter(delete=False)
+            if plan_set:
+                for plan in plan_set:
+                    task_set = plan.testtask_set.filter(delete=False)
+                    if task_set:
+                        for task in task_set:
+                            task.delete = True
+                            task.save()
+                    plan.delete = True
+                    plan.save()
+
+            version.save()
+            version_file.save()
+            executor, action, method = get_log(request)
+            gen_log(action, "目标版本", software.name + "-" + version.number, method, executor)
+            return HttpResponse(status=200, content='删除完成')
+
+    @staticmethod
+    # @RoleControl
+    def get(request, software_id, *args, **kwargs):
+        export_type = request.GET.get('type')
+        version_number = request.GET.get('version_number')
+        software = Software.objects.filter(id=software_id, delete=False)
+        if not software:
+            logger.error("软件已删除或不存在")
+            return HttpResponse(content='软件已删除或不存在', status=500)
+        software = software[0]
+
+        version = software.version_set.filter(number=version_number, delete=False)
+        if not version:
+            logger.error("软件版本已删除或不存在")
+            return HttpResponse(status=404, content='软件版本已删除或不存在')
+        version = version[0]
+
+        cases = None
+        file_name = None
+        if export_type == '0':
+            cases = TestCase.objects.filter(software=software, version=version_number, delete=False)
+            file_name = '全部测试用例.xls'
+        elif export_type == '1':
+            cases = TestCase.objects.filter(~Q(state=0), software=software, version=version_number, delete=False)
+            file_name = '已执行测试用例.xls'
+        else:
+            cases = TestCase.objects.filter(software=software, version=version_number, state=2, delete=False)
+            file_name = '不合格测试用例.xls'
+
+        if not cases:
+            logger.error("无符合要求的测试用例")
+            return HttpResponse(status=404, content='无符合要求的测试用例')
+
+        update_time = datetime.datetime.now()
+        file_dir = CASE_FILE_EXPORT + software_id + "/" + str(update_time)
+        if not os.path.exists(file_dir):
+            os.makedirs(file_dir)
+        file_path = file_dir + "/" + file_name
+
+        cases_info = [['被测软件ID:', software_id],
+                      ['被测软件名:', software.name],
+                      ['被测软件版本', version_number],
+                      ['编号', '名称', '前置条件', '优先级', '测试环境', '测试类型', '测试步骤', '预期结果', '实际结果', '状态',
+                       '编写人', '评审员', '执行人', '备注']]
+
+        for case in cases:
+            state = '未执行'
+            if case.state == 1:
+                state = '执行成功'
+            elif case.state == 2:
+                state = '执行失败'
+            cases_info.append([case.id_in_task, case.name, case.requisite, case.priority, case.environment,
+                               case.type, case.process, case.expected_result, case.actual_result, state,
+                               case.writer, case.assessor, case.executor, case.remark])
+
+        sio = write_to_xlsx(file_path, cases_info)
+        sio.seek(0)
+        response = HttpResponse(sio.getvalue(), content_type='multipart/form-data')
+        response['Content-Disposition'] = 'attachment; filename="{0}"'.format(file_name).encode('utf-8')
+        response.write(sio.getvalue())
+        # executor, action, method = get_log(request)
+        # gen_log(action, "目标版本", software.name + "-" + version.number, method, executor)
+        return response
+
+
+class VersionEdit(APIView):
+    authentication_classes = []
+
+    # edit version
+    # 版本号不能修改,本质上只能修改上传的文件
+    @staticmethod
+    @RoleControl
+    def post(request, software_id, *args, **kwargs):
+        file = request.FILES.get('file')
+        version_number = request.POST.get('version_number')
+
+        # 判断软件是否存在
+        software = Software.objects.filter(id=software_id, delete=False)
+        if not software:
+            return HttpResponse(status=404, content='软件已删除或不存在')
+        software = software[0]
+
+        # 判断版本是否存在
+        software_version = software.version_set.filter(number=version_number,
+                                                       delete=False)
+        if not software_version:
+            return HttpResponse(status=404,
+                                content='软件版本不存在或已删除,请重新选择需要编辑的软件版本')
+        software_version = software_version[0]
+
+        # 删除原版本文件
+        version_fid = software_version.version_file
+        version_file = FileManager.objects.filter(id=version_fid)
+        if not version_file:
+            logger.error("软件不存在,无法编辑")
+            return HttpResponse(status=500, content='软件不存在,无法编辑')
+        version_file = version_file[0]
+        version_file.delete = True
+        version_file.save()
+
+        # step5:设置创建时间和修改时间
+        create_time = update_time = datetime.datetime.now()
+
+        # step6:上传文件:
+        write_file(file,
+                   SOFTWARE_ROOT + software.id + "/" + software_version.id + "/" + str(
+                       create_time))
+
+        # step7:创建文件对象
+        id_file = None
+        if not FileManager.objects.last():
+            id_file = "TestLaboratory_V1_File_1"
+        else:
+            id_file = gen_next_id(
+                FileManager.objects.order_by('create_time').last().id)
+        software_version.version_file = id_file
+        software_version.save()
+
+        try:
+            file_new = FileManager.objects.create(id=id_file,
+                                                  category="software",
+                                                  path=software.id + "/" + software_version.id + "/" +
+                                                       str(create_time) + "/" + file.name,
+                                                  create_time=create_time,
+                                                  update_time=update_time)
+        except:
+            logger.error("软件版本编辑失败,请重试")
+            return HttpResponse(content='软件版本编辑失败,请重试', status=500)
+        executor, action, method = get_log(request)
+        gen_log(action, "目标版本", software.name + "-" + software_version.number,
+                method, executor)
+        return HttpResponse(status=200, content='修改已保存')

+ 0 - 0
apps/task/__init__.py


+ 3 - 0
apps/task/admin.py

@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.

+ 6 - 0
apps/task/apps.py

@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class TestTaskConfig(AppConfig):
+    default_auto_field = 'django.db.models.BigAutoField'
+    name = 'apps.task'

+ 74 - 0
apps/task/migrations/0001_initial.py

@@ -0,0 +1,74 @@
+# Generated by Django 3.2 on 2021-07-17 20:05
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    initial = True
+
+    dependencies = [
+        ('user', '0001_initial'),
+        ('plan', '0001_initial'),
+        ('software', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='TestTask',
+            fields=[
+                ('id', models.CharField(default='TestLaboratory_V1_Task_1', max_length=30, primary_key=True, serialize=False, unique=True, verbose_name='测试任务编号')),
+                ('title', models.CharField(max_length=50, verbose_name='测试任务标题')),
+                ('state', models.IntegerField(verbose_name='测试任务状态')),
+                ('description', models.TextField(verbose_name='测试任务描述')),
+                ('statement_file', models.TextField(verbose_name='说明文档id文件')),
+                ('case_file', models.TextField(null=True, verbose_name='测试用例文档')),
+                ('case_all', models.IntegerField(null=True, verbose_name='全部测试用例数量')),
+                ('case_not_execute', models.IntegerField(null=True, verbose_name='未执行测试用例数量')),
+                ('case_success', models.IntegerField(null=True, verbose_name='成功测试用例数量')),
+                ('case_fail', models.IntegerField(null=True, verbose_name='失败测试用例数量')),
+                ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
+                ('update_time', models.DateTimeField(auto_now=True, verbose_name='修改时间')),
+                ('delete', models.BooleanField(default=False)),
+                ('executor', models.ManyToManyField(to='user.User', verbose_name='测试任务执行者')),
+                ('plan', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='plan.testplan', verbose_name='测试任务所属计划')),
+            ],
+            options={
+                'verbose_name': '测试任务',
+                'verbose_name_plural': '测试任务',
+                'db_table': 'task',
+            },
+        ),
+        migrations.CreateModel(
+            name='TestCase',
+            fields=[
+                ('id', models.CharField(default='TestLaboratory_V1_Case_1', max_length=30, primary_key=True, serialize=False, unique=True, verbose_name='测试用例编号')),
+                ('id_in_task', models.CharField(default='1', max_length=30, verbose_name='在测试任务中编号')),
+                ('version', models.CharField(max_length=30, null=True, verbose_name='软件版本')),
+                ('name', models.CharField(max_length=50, verbose_name='名称')),
+                ('state', models.IntegerField(verbose_name='状态')),
+                ('writer', models.CharField(max_length=30, null=True, verbose_name='测试用例编写人')),
+                ('executor', models.CharField(max_length=30, null=True, verbose_name='测试用例执行人')),
+                ('assessor', models.CharField(max_length=30, null=True, verbose_name='测试用例评审员')),
+                ('requisite', models.TextField(null=True, verbose_name='前置条件')),
+                ('priority', models.CharField(max_length=10, null=True, verbose_name='优先级')),
+                ('environment', models.TextField(null=True, verbose_name='测试环境')),
+                ('type', models.CharField(max_length=30, null=True, verbose_name='测试类型')),
+                ('process', models.TextField(verbose_name='测试步骤')),
+                ('expected_result', models.TextField(verbose_name='预期结果')),
+                ('actual_result', models.TextField(null=True, verbose_name='实际结果')),
+                ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
+                ('update_time', models.DateTimeField(auto_now=True, verbose_name='修改时间')),
+                ('delete', models.BooleanField(default=False)),
+                ('remark', models.TextField(null=True, verbose_name='备注')),
+                ('software', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='software.software', verbose_name='测试软件')),
+                ('task', models.ForeignKey(default='TestLaboratory_V1_Task_1', on_delete=django.db.models.deletion.CASCADE, to='task.testtask', verbose_name='测试用例所在测试任务')),
+            ],
+            options={
+                'verbose_name': '测试用例',
+                'verbose_name_plural': '测试用例',
+                'db_table': 'test_case',
+            },
+        ),
+    ]

+ 18 - 0
apps/task/migrations/0002_alter_testtask_id.py

@@ -0,0 +1,18 @@
+# Generated by Django 3.2.4 on 2022-06-19 10:43
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('task', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='testtask',
+            name='id',
+            field=models.CharField(default='TestLaboratory_V1_Task_1', max_length=128, primary_key=True, serialize=False, unique=True, verbose_name='测试任务编号'),
+        ),
+    ]

+ 29 - 0
apps/task/migrations/0003_auto_20220626_1630.py

@@ -0,0 +1,29 @@
+# Generated by Django 3.2.4 on 2022-06-26 16:30
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('user', '0002_auto_20220619_1043'),
+        ('task', '0002_alter_testtask_id'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='testcase',
+            name='executor_id',
+            field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='user.user', verbose_name='执行者id'),
+        ),
+        migrations.RemoveField(
+            model_name='testtask',
+            name='executor',
+        ),
+        migrations.AddField(
+            model_name='testtask',
+            name='executor',
+            field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='user.user', verbose_name='测试任务执行者'),
+        ),
+    ]

+ 0 - 0
apps/task/migrations/__init__.py


+ 69 - 0
apps/task/models.py

@@ -0,0 +1,69 @@
+from django.db import models
+
+# Create your models here.
+from apps.plan.models import TestPlan
+from apps.software.models import Software
+from apps.user.models import User
+
+
+class TestTask(models.Model):
+    id = models.CharField(max_length=128, unique=True, primary_key=True, verbose_name='测试任务编号',
+                          default='TestLaboratory_V1_Task_1')
+    title = models.CharField(max_length=50, verbose_name='测试任务标题')
+    executor = models.ForeignKey(User, null=True, on_delete=models.CASCADE, verbose_name='测试任务执行者')
+    # 0: 发布,release;(此阶段可以上传未执行的测试用例)1:执行测试用例,execute(此阶段需上传包含测试结果的测试用例);2:执行完成
+    state = models.IntegerField(verbose_name='测试任务状态')
+    description = models.TextField(verbose_name='测试任务描述')
+    plan = models.ForeignKey(TestPlan, on_delete=models.CASCADE, verbose_name='测试任务所属计划')
+    statement_file = models.TextField(verbose_name='说明文档id文件')
+    case_file = models.TextField(null=True, verbose_name='测试用例文档')
+    case_all = models.IntegerField(null=True, verbose_name='全部测试用例数量')
+    case_not_execute = models.IntegerField(null=True, verbose_name='未执行测试用例数量')
+    case_success = models.IntegerField(null=True, verbose_name='成功测试用例数量')
+    case_fail = models.IntegerField(null=True, verbose_name='失败测试用例数量')
+    create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
+    update_time = models.DateTimeField(auto_now=True, verbose_name='修改时间')
+    delete = models.BooleanField(default=False)
+
+    def __str__(self):
+        return self.title
+
+    class Meta:
+        verbose_name = '测试任务'
+        verbose_name_plural = verbose_name
+        db_table = 'task'
+
+
+class TestCase(models.Model):
+    id = models.CharField(max_length=30, unique=True, primary_key=True, verbose_name='测试用例编号',
+                          default='TestLaboratory_V1_Case_1')
+    task = models.ForeignKey(TestTask, on_delete=models.CASCADE, verbose_name='测试用例所在测试任务',
+                             default='TestLaboratory_V1_Task_1')
+    id_in_task = models.CharField(max_length=30, verbose_name='在测试任务中编号', default='1')
+    software = models.ForeignKey(Software, null=True, on_delete=models.CASCADE, verbose_name='测试软件')
+    executor_id = models.ForeignKey(User, null=True, on_delete=models.CASCADE, verbose_name='执行者id')
+    version = models.CharField(max_length=30, null=True, verbose_name='软件版本')
+    name = models.CharField(max_length=50, verbose_name='名称')
+    state = models.IntegerField(verbose_name='状态')
+    writer = models.CharField(max_length=30, verbose_name='测试用例编写人', null=True)
+    executor = models.CharField(max_length=30, verbose_name='测试用例执行人', null=True)
+    assessor = models.CharField(max_length=30, verbose_name='测试用例评审员', null=True)
+    requisite = models.TextField(null=True, verbose_name='前置条件')
+    priority = models.CharField(max_length=10, null=True, verbose_name='优先级')
+    environment = models.TextField(null=True, verbose_name='测试环境')
+    type = models.CharField(max_length=30, null=True, verbose_name='测试类型')
+    process = models.TextField(verbose_name='测试步骤')
+    expected_result = models.TextField(verbose_name='预期结果')
+    actual_result = models.TextField(verbose_name='实际结果', null=True)
+    create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
+    update_time = models.DateTimeField(auto_now=True, verbose_name='修改时间')
+    delete = models.BooleanField(default=False)
+    remark = models.TextField(null=True, verbose_name='备注')
+
+    def __str__(self):
+        return self.name
+
+    class Meta:
+        verbose_name = '测试用例'
+        verbose_name_plural = verbose_name
+        db_table = 'test_case'

+ 3 - 0
apps/task/tests.py

@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.

+ 10 - 0
apps/task/urls.py

@@ -0,0 +1,10 @@
+from django.urls import path
+from .views import tasklistview, taskview, caseview
+
+urlpatterns = [
+    path('', tasklistview.TaskListView.as_view()),
+    path('<str:task_id>', taskview.TaskView.as_view()),
+    path('<str:task_id>/edit', taskview.TaskEdit.as_view()),
+    path('<str:task_id>/case-file', caseview.CaseView.as_view()),
+    path('<str:task_id>/case-file/execute', caseview.CaseExecute.as_view())
+]

+ 0 - 0
apps/task/views/__init__.py


+ 259 - 0
apps/task/views/caseview.py

@@ -0,0 +1,259 @@
+import datetime
+import os
+
+from django.db.models import Q
+from django.http import HttpResponse, HttpResponseBadRequest
+from rest_framework.response import Response
+from rest_framework.views import APIView
+
+from TestLaboratory.settings import CASE_FILE_ROOT, CASE_FILE_EXPORT, HTTP_HEAD
+from apps.file.models import FileManager
+from apps.log.models import get_log, gen_log
+from apps.task.models import TestTask, TestCase
+from apps.user.middleware.rolecontrol import RoleControl
+from util_xlsx import load_xlsx, write_to_xlsx
+from utils.util_add_id import get_id
+from utils.util_file.util_fileio import write_file
+
+import logging
+logger = logging.getLogger('django')
+
+
+class CaseView(APIView):
+    # 登录权限验证
+    authentication_classes = []
+
+    # get testcase
+    @staticmethod
+    # @RoleControl
+    def get(request, task_id, *args, **kwargs):
+        export_type = request.GET.get('type')
+
+        task = TestTask.objects.filter(id=task_id, delete=False)
+        if not task:
+            logger.error("测试任务已删除或不存在")
+            return HttpResponse(status=404, content='测试任务已删除或不存在')
+        task = task[0]
+
+        cases = None
+        file_name = None
+        if export_type == '0':
+            cases = TestCase.objects.filter(task=task, delete=False)
+            file_name = task.title+'-全部测试用例.xls'
+        elif export_type == '1':
+            cases = TestCase.objects.filter(~Q(state=0), task=task, delete=False)
+            file_name = task.title+'-已执行测试用例.xls'
+        elif export_type == '2':
+            cases = TestCase.objects.filter(state=2, task=task, delete=False)
+            file_name = task.title+'-不合格测试用例.xls'
+
+        if not cases:
+            logger.error("无符合要求的测试用例")
+            return HttpResponse(status=404, content='无符合要求的测试用例')
+
+        update_time = datetime.datetime.now()
+        file_dir = CASE_FILE_EXPORT + task_id + "/" + str(update_time)
+        if not os.path.exists(file_dir):
+            os.makedirs(file_dir)
+        file_path = file_dir + "/" + file_name
+
+        cases_info = [['被测软件ID:', task.plan.software.id],
+                      ['被测软件名:', task.plan.software.name],
+                      ['被测软件版本', task.plan.version],
+                      ['编号', '名称', '前置条件', '优先级', '测试环境', '测试类型', '测试步骤', '预期结果', '实际结果', '状态',
+                       '编写人', '评审员', '执行人', '备注']]
+
+        for case in cases:
+            state = '未执行'
+            if case.state == 1:
+                state = '执行成功'
+            elif case.state == 2:
+                state = '执行失败'
+            cases_info.append([case.id_in_task, case.name, case.requisite, case.priority, case.environment,
+                               case.type, case.process, case.expected_result, case.actual_result, state,
+                               case.writer, case.assessor, case.executor, case.remark])
+
+        sio = write_to_xlsx(file_path, cases_info)
+        sio.seek(0)
+        response = HttpResponse(sio.getvalue(), content_type='application/vnd.ms-excel')
+        # response['Content-Disposition'] = 'attachment; filename*=UTF-8''{}'.format(escape_uri_path(file_name))
+        response['Content-Disposition'] = 'attachment; filename="{0}"'.format(file_name).encode('utf-8', 'ISO-8859-1')
+        response.write(sio.getvalue())
+        # executor, action, method = get_log(request)
+        # gen_log(action, "测试任务", task.title, method, executor)
+        return response
+
+
+class CaseExecute(APIView):
+    # 登录权限验证
+    authentication_classes = []
+
+    # execute task
+    @staticmethod
+    @RoleControl
+    def post(request, task_id, *args, **kwargs):
+        executor, action, method = get_log(request)
+        task = TestTask.objects.filter(id=task_id, delete=False)
+        if not task:
+            logger.error("测试任务已删除或不存在")
+            return HttpResponseBadRequest(status=404, content='测试任务已删除或不存在')
+        task = task[0]
+
+        file = request.FILES.get('file')
+
+        create_time = update_time = datetime.datetime.now()
+        write_file(file, CASE_FILE_ROOT + task_id + "/" + str(create_time))
+
+        return_info = load_xlsx(
+            CASE_FILE_ROOT + task_id + "/" + str(create_time) + '/' + file.name,
+            task)
+        if return_info['code'] != 200:
+            logger.error(str(return_info['info']))
+            return Response({'status': return_info['code'],
+                             'error': str(return_info['info'])})
+
+        case_info_dict_list = []
+        case_infos = return_info['info']
+
+        total_num = len(case_infos)
+        not_execute_num = success_num = fail_num = 0
+        id_error = []
+        for i in range(len(case_infos)):
+            case_info_list = case_infos[i]
+
+            cases_same_id = TestCase.objects.filter(id=task.id,
+                                                    id_in_task=str(
+                                                        case_info_list[0]),
+                                                    delete=False)
+            if cases_same_id:
+                case_name = cases_same_id[0].name
+                if case_name != case_info_list[1]:
+                    id_error.append('第{}行id为{}的测试用例名称与前版本不同,请检查错误'
+                                    .format(i + 3, cases_same_id[0].id_in_task))
+            state = 0
+            if case_info_list[9] == '未执行':
+                not_execute_num += 1
+            if case_info_list[9] == '执行成功':
+                state = 1
+                success_num += 1
+            elif case_info_list[9] == '执行失败':
+                state = 2
+                fail_num += 1
+            case_info = {
+                'id_in_task': str(case_info_list[0]),
+                'name': case_info_list[1],
+                'requisite': case_info_list[2],
+                'priority': case_info_list[3],
+                'environment': case_info_list[4],
+                'type': case_info_list[5],
+                'process': case_info_list[6],
+                'expected_result': case_info_list[7],
+                'actual_result': case_info_list[8],
+                'state': state,
+                'writer': case_info_list[10],
+                'assessor': case_info_list[11],
+                'executor': case_info_list[12],
+                'task': task,
+                'remark': case_info_list[13],
+                'software': task.plan.software,
+                'version': task.plan.version
+            }
+            case_info_dict_list.append(case_info)
+        if id_error:
+            logger.error("id错误")
+            return Response({'status': 50001, 'error': id_error})
+        # 在创建测试用例阶段,每次创建都为新建,新建之前删除task下的所有测试用例
+        if task.state == 0:
+            cases_before = task.testcase_set.filter(delete=False)
+            for case_before in cases_before:
+                case_before.delete = True
+                case_before.save()
+            for case_info in case_info_dict_list:
+                case_info['id'] = get_id(TestCase, "TestLaboratory_V1_Case_1")
+                case_info['create_time'] = create_time
+                case_info['update_time'] = update_time
+                case_info['executor_id_id'] = executor.id
+                TestCase.objects.create(**case_info)
+
+        elif task.state == 1:
+            for i in range(len(case_info_dict_list)):
+                case_info = case_info_dict_list[i]
+                case = TestCase.objects.filter(delete=False, task=task,
+                                               id_in_task=case_info[
+                                                   'id_in_task'])
+                if not case:
+                    logger.error(
+                        'id为{}的测试用例不存在'.format(case_info['id_in_task']))
+                    return Response({'status': 50001,
+                                     'error': 'id为{}的测试用例不存在'.format(
+                                         case_info['id_in_task'])})
+                elif len(case) > 1:
+                    logger.error(
+                        'id为{}的测试用例存在多个'.format(case_info['id_in_task']))
+                    return Response({'status': 50001,
+                                     'error': 'id为{}的测试用例存在多个'.format(
+                                         case_info['id_in_task'])})
+                case.update(**case_info)
+                case[0].save()
+
+        file_id = get_id(FileManager, "TestLaboratory_V1_File_1")
+        try:
+            file_new = FileManager.objects.create(id=file_id,
+                                                  category="case_file",
+                                                  path=task_id + "/" + str(
+                                                      create_time) + "/" + file.name,
+                                                  create_time=create_time,
+                                                  update_time=update_time)
+        except:
+            logger.error("文件路径写入数据库失败")
+            return Response({'status': 50001, 'error': "文件路径写入数据库失败"})
+
+        task.case_file = file_id
+        task.case_all = len(task.testcase_set.filter(delete=False))
+        task.case_not_execute = len(
+            task.testcase_set.filter(delete=False, state=0))
+        task.case_success = len(task.testcase_set.filter(delete=False, state=1))
+        task.case_fail = len(task.testcase_set.filter(delete=False, state=2))
+        print(task.case_all, task.case_not_execute, task.case_success,
+              task.case_fail)
+
+        task.save()
+        executionInfo = [
+            {
+                'operationName': 'filename',
+                'content': file.name
+            },
+            {
+                'operationName': 'fileurl',
+                'content': HTTP_HEAD + CASE_FILE_ROOT + file_new.path
+            },
+            {
+                'operationName': 'state',
+                'content': task.state
+            },
+            {
+                'operationName': 'updateTime',
+                'content': update_time
+            },
+            {
+                'operationName': 'totalNum',
+                'content': task.case_all
+            },
+            {
+                'operationName': 'notExecuteNum',
+                'content': task.case_not_execute
+            },
+            {
+                'operationName': 'successNum',
+                'content': task.case_success
+            },
+            {
+                'operationName': 'failNum',
+                'content': task.case_fail
+            }
+        ]
+
+
+        gen_log(action, "测试任务", task.title, method, executor)
+
+        return Response(executionInfo)

+ 147 - 0
apps/task/views/tasklistview.py

@@ -0,0 +1,147 @@
+import datetime
+
+from rest_framework.response import Response
+from rest_framework.views import APIView
+from django.http import HttpResponse
+
+from TestLaboratory.settings import TASK_ROOT, CASE_TEMPLATE_ROOT, HTTP_HEAD
+from apps.file.models import FileManager
+from apps.log.models import get_log, gen_log
+from apps.plan.models import TestPlan
+from apps.task.models import TestTask, TestCase
+from apps.user.middleware.rolecontrol import RoleControl
+from apps.user.models import User
+from utils.util_add_id import get_id
+from utils.util_file.util_fileio import write_file
+
+import logging
+logger = logging.getLogger('django')
+
+
+class TaskListView(APIView):
+    # 登录权限验证
+    authentication_classes = []
+
+    # create task
+    @staticmethod
+    @RoleControl
+    def post(request, *args, **kwargs):
+        plan_id = request.POST.get('plan_id')
+        title = request.POST.get('title')
+        description = request.POST.get('description')
+        executors_id = request.POST.get('executors').split('&&')
+        statement_files = request.FILES.getlist('files')
+
+        # step1:判断所属软件及版本是否存在
+        plan = TestPlan.objects.filter(id=plan_id, delete=False)
+        if not plan:
+            logger.error("测试计划已删除或不存在")
+            return HttpResponse(status=404, content='测试计划已删除或不存在')
+
+        # step1:创建文件的数据库字段
+        fids = ''
+        file_paths = []
+        file_ids = []
+        for file in statement_files:
+            create_time = update_time = datetime.datetime.now()
+            try:
+                write_file(file, TASK_ROOT + "/" + str(create_time))
+            except:
+                logger.error("文件上传失败")
+                return HttpResponse(status=500, content="文件上传失败")
+
+            id_file = get_id(FileManager, "TestLaboratory_V1_File_1")
+
+            try:
+                file_new = FileManager.objects.create(id=id_file, category="plan_statement",
+                                                      path=str(create_time) + "/" + file.name,
+                                                      create_time=create_time,
+                                                      update_time=update_time)
+            except:
+                logger.error("文件路径写入数据库失败")
+                return HttpResponse(status=500, content="文件路径写入数据库失败")
+            file_ids.append(id_file)
+            file_paths.append(TASK_ROOT + file_new.path)
+            fids += id_file + '&&'
+        fids = fids[:-2]
+
+        task_files = {'case-template': CASE_TEMPLATE_ROOT, 'statement-fids': fids}
+
+        create_time = update_time = datetime.datetime.now()
+        return_value = []
+        for executor_id in executors_id:
+            executor = User.objects.filter(id=executor_id, delete=False)
+            if not executor:
+                logger.error("执行者用户不存在")
+                return HttpResponse(status=404, content='执行者用户不存在')
+
+            task_id = get_id(TestTask, 'TestLaboratory_V1_Task_1')
+            task = TestTask.objects.create(id=task_id, title=title, description=description, state=0, plan_id=plan_id,
+                                           statement_file=fids, executor_id=executor_id,
+                                           create_time=create_time, update_time=update_time)
+            executor, action, method = get_log(request)
+            gen_log(action, "测试任务", task.title, method, executor)
+            return_value.append({
+                'software_id': task.plan.software.id,
+                'software_name': task.plan.software.name,
+                'version': task.plan.version,
+                'id': task.id,
+                'executor_id': executor_id,
+                'title': title,
+                'description': description,
+                'state': 0,
+                'files': [{'file_id': id_file, 'file_name': file.name, 'file_url': HTTP_HEAD + file_path} for
+                          (id_file, file, file_path)
+                          in zip(file_ids, statement_files, file_paths)],
+                'case_template': HTTP_HEAD + CASE_TEMPLATE_ROOT
+            })
+        return Response(return_value)
+
+    # view task_list
+    @staticmethod
+    @RoleControl
+    def get(request, *args, **kwargs):
+        title = request.GET.get('title')
+        software_id = request.GET.get('software_id')
+        state = request.GET.get('state')
+        executor_id = request.GET.get('executor_id')
+        sort = request.GET.get('sort')
+
+        task_all = TestTask.objects.order_by('-create_time')
+        if title:
+            task_all = task_all.filter(title__contains=title)
+        if software_id:
+            task_all = task_all.filter(plan__software__id=software_id)
+        if state:
+            task_all = task_all.filter(state=state)
+        if executor_id:
+            task_all = task_all.filter(executor_id=executor_id)
+        """
+        sort=update_time, -update_time, create_time, -create_time, title, -title, state
+        """
+        if sort:
+            task_all = task_all.order_by(sort)
+        task_all = task_all.filter(delete=False)
+
+        if not task_all:
+            return Response([])
+
+        info = []
+        for task in task_all.all():
+            fail_num = len(TestCase.objects.filter(task=task, state=2, delete=False))
+            info.append({
+                'software_id': task.plan.software.id,
+                'software_name': task.plan.software.name,
+                'version': task.plan.version,
+                # 'executor_id': task.executor_id,
+                'executor': {'id': task.executor.id, 'name': task.executor.username},
+                'plan_name': task.plan.title,
+                'id': task.id,
+                'title': task.title,
+                'description': task.description,
+                'state': task.state,
+                'fail_num': fail_num,
+                'create_time': task.create_time,
+                'update_time': task.update_time,
+            })
+        return Response(info)

+ 200 - 0
apps/task/views/taskview.py

@@ -0,0 +1,200 @@
+import datetime
+
+from rest_framework.response import Response
+from rest_framework.views import APIView
+from django.http import HttpResponse
+
+from TestLaboratory.settings import PLAN_ROOT, SOFTWARE_ROOT, HTTP_HEAD, TASK_ROOT, CASE_TEMPLATE_ROOT, CASE_FILE_ROOT
+from apps.file.models import FileManager
+from apps.log.models import get_log, gen_log
+from apps.task.models import TestTask
+from apps.user.middleware.rolecontrol import RoleControl
+from apps.user.models import User
+from utils.util_add_id import get_id
+from utils.util_file.util_fileio import write_file
+
+import logging
+logger = logging.getLogger('django')
+
+
+class TaskView(APIView):
+    # 登录权限验证
+    authentication_classes = []
+
+    # delete task
+    @staticmethod
+    @RoleControl
+    def delete(request, task_id, *args, **kwargs):
+        task = TestTask.objects.filter(id=task_id, delete=False)
+        if not task:
+            logger.error("测试任务已删除或不存在")
+            return HttpResponse(status=404, content='测试任务已删除或不存在')
+        task = task[0]
+
+        task_sids = task.statement_file.split('&&')
+
+        if task_sids[0]:
+            for statement_id in task_sids:
+                statement = FileManager.objects.get(id=statement_id)
+                statement.delete = True
+                statement.save()
+        task.delete = True
+        task.save()
+        executor, action, method = get_log(request)
+        gen_log(action, "测试任务", task.title, method, executor)
+        return HttpResponse("删除完成")
+
+    # view task
+    @staticmethod
+    @RoleControl
+    def get(request, task_id, *args, **kwargs):
+        task = TestTask.objects.filter(id=task_id, delete=False)
+        if not task:
+            logger.error("测试计划已删除或不存在")
+            return HttpResponse(status=404, content='测试计划已删除或不存在')
+        task = task[0]
+
+        version = task.plan.software.version_set.filter(number=task.plan.version, delete=False)
+        if not version:
+            logger.error("测试任务对应软件版本不存在或已删除")
+            return HttpResponse(status=404, content='测试任务对应软件版本不存在或已删除')
+        version = version[0]
+
+        version_fid = version.version_file
+        version_file = FileManager.objects.filter(id=version_fid)
+        if not version_file:
+            logger.error("无法定位测试任务对应的软件实体")
+            return HttpResponse(status=404, content='无法定位测试任务对应的软件实体')
+        version_file = version_file[0]
+
+        plan_sids = task.plan.statement_file.split('&&')
+        plan_file_names = []
+        plan_file_urls = []
+        if plan_sids[0]:
+            for plan_statement_id in plan_sids:
+                plan_statement = FileManager.objects.get(id=plan_statement_id)
+                plan_file_urls.append(PLAN_ROOT + plan_statement.path)
+                plan_file_names.append(plan_statement.path.split('/')[-1])
+
+        task_sids = task.statement_file.split('&&')
+        task_file_names = []
+        task_file_urls = []
+        if task_sids[0]:
+            for task_statement_id in task_sids:
+                task_statement = FileManager.objects.get(id=task_statement_id)
+                task_file_urls.append(TASK_ROOT + task_statement.path)
+                task_file_names.append(task_statement.path.split('/')[-1])
+
+        case_file_info = {}
+        if task.case_file:
+            case_file = FileManager.objects.get(id=task.case_file)
+            case_file_info = {'file_name': case_file.path.split('/')[-1], 'file_url': HTTP_HEAD + CASE_FILE_ROOT + case_file.path}
+
+        executor, action, method = get_log(request)
+        gen_log(action, "测试任务", task.title, method, executor)
+        return Response({
+            'software_id': task.plan.software.id,
+            'software_name': task.plan.software.name,
+            'version': task.plan.version,
+            'version_file': {'file_name': version_file.path.split('/')[-1], 'file_url': HTTP_HEAD + SOFTWARE_ROOT
+                                                                                        + version_file.path},
+            'plan': {
+                'id': task.plan.id,
+                'title': task.plan.title,
+                'description': task.plan.description,
+                'state': task.plan.state,
+                'statements': [{'file_id': file_sid, 'file_name': file_name, 'file_url': HTTP_HEAD + file_url} for
+                               file_sid, file_name, file_url in zip(plan_sids, plan_file_names, plan_file_urls)],
+                'creator': {'executor_id': task.plan.creator.id, 'executor_name': task.plan.creator.username},
+            },
+            'task': {
+                'id': task.id,
+                'title': task.title,
+                'description': task.description,
+                'state': task.state,
+                'statements': [{'file_id': file_sid, 'file_name': file_name, 'file_url': HTTP_HEAD + file_url} for
+                               file_sid, file_name, file_url in zip(task_sids, task_file_names, task_file_urls)],
+                'case_template': HTTP_HEAD + CASE_TEMPLATE_ROOT,
+                'case_file': case_file_info,
+                'executor': {'id': task.executor.id, 'name': task.executor.username},
+                'case_all': task.case_all,
+                'case_not_execute': task.case_not_execute,
+                'case_success': task.case_success,
+                'case_fail': task.case_fail,
+            },
+            'create_time': task.create_time,
+            'update_time': task.update_time,
+        })
+
+
+class TaskEdit(APIView):
+    # 登录权限验证
+    authentication_classes = []
+
+    # edit task
+    @staticmethod
+    @RoleControl
+    def post(request, task_id, *args, **kwargs):
+        title = request.POST.get('title')
+        state = request.POST.get('state')
+        description = request.POST.get('description')
+        files = request.FILES.getlist('files')
+        edit_state_only = request.POST.get('edit_state_only')
+
+        task = TestTask.objects.filter(id=task_id, delete=False)
+        if not task:
+            logger.error("测试任务已删除或不存在")
+            return HttpResponse(status=404, content='测试任务已删除或不存在')
+        task = task[0]
+
+        if edit_state_only == '1':
+            task.state = int(state)
+            task.save()
+            return HttpResponse(status=200, content='编辑已保存')
+
+        if title:
+            task.title = title
+        if state:
+            task.state = int(state)
+        if description:
+            task.description = description
+
+        fids = ''
+        for file in files:
+            create_time = update_time = datetime.datetime.now()
+            try:
+                write_file(file,
+                           PLAN_ROOT + "/" + task_id + "/" + str(create_time))
+            except:
+                logger.error("文件上传失败")
+                return HttpResponse(status=500, content="文件上传失败")
+
+            id_file = get_id(FileManager, "TestLaboratory_V1_File_1")
+
+            try:
+                file_new = FileManager.objects.create(id=id_file,
+                                                      category="plan_statement",
+                                                      path=task_id + "/" + str(
+                                                          create_time) + "/" + file.name,
+                                                      create_time=create_time,
+                                                      update_time=update_time)
+            except:
+                logger.error("文件路径写入数据库失败")
+                return HttpResponse(status=500, content="文件路径写入数据库失败")
+            fids += id_file + '&&'
+        fids = fids[:-2]
+
+        task_sids = task.statement_file.split('&&')
+        print(task_sids)
+
+        if task_sids is not None and task_sids[0] != '':
+            for statement_id in task_sids:
+                statement = FileManager.objects.get(id=statement_id)
+                statement.delete = True
+                statement.save()
+
+        task.statement_file = fids
+        task.save()
+        executor, action, method = get_log(request)
+        gen_log(action, "测试任务", task.title, method, executor)
+        return HttpResponse("编辑已保存")

+ 0 - 0
apps/user/__init__.py


+ 3 - 0
apps/user/admin.py

@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.

+ 6 - 0
apps/user/apps.py

@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class UserConfig(AppConfig):
+    default_auto_field = 'django.db.models.BigAutoField'
+    name = 'apps.user'

+ 0 - 0
apps/user/middleware/__init__.py


+ 26 - 0
apps/user/middleware/authentication.py

@@ -0,0 +1,26 @@
+import jwt
+# from jwt import exceptions
+from rest_framework.authentication import BaseAuthentication
+from rest_framework.exceptions import AuthenticationFailed
+
+
+class JwtAutentication(BaseAuthentication):
+    def authenticate(self, request):
+        token = request.META.get('HTTP_ACCESSTOKEN')
+        SALT = 'django-insecure-zo64fvv02msf-se7!dek5*w$17#3nh6zta#!i=79bt9d#f88@i'
+
+        try:
+            payload = jwt.decode(token, SALT, True)
+        # except exceptions.ExpiredSignatureError:
+        #     raise AuthenticationFailed({'code': 1003, "error": "token已失效"})
+        except jwt.DecodeError:
+            raise AuthenticationFailed({'code': 1005, 'error': "token认证失败"})
+        except jwt.InvalidTokenError:
+            raise AuthenticationFailed({'code': 1003, 'error': "非法token"})
+
+        return
+
+
+
+
+

+ 48 - 0
apps/user/middleware/rolecontrol.py

@@ -0,0 +1,48 @@
+import re
+
+import jwt
+from django.http import HttpResponse
+# from jwt import exceptions
+from rest_framework.exceptions import AuthenticationFailed
+
+from apps.user.models import User, Role
+
+
+class RoleControl(object):
+    """使用类装饰器来权限校验"""
+    def __init__(self, func):
+        self.func = func
+
+    def __call__(self, request, *args, **kwargs):
+        token = request.META.get('HTTP_ACCESSTOKEN')
+        SALT = 'django-insecure-zo64fvv02msf-se7!dek5*w$17#3nh6zta#!i=79bt9d#f88@i'
+
+        try:
+            payload = jwt.decode(token, SALT, True)
+        # except exceptions.ExpiredSignatureError:
+        #     raise AuthenticationFailed({'code': 1003, "error": "token已失效"})
+        except jwt.DecodeError:
+            raise AuthenticationFailed({'code': 1003, 'error': "token认证失败"})
+        except jwt.InvalidTokenError:
+            raise AuthenticationFailed({'code': 1003, 'error': "非法token"})
+
+        user = User.objects.get(id=payload['userid'])
+
+        role = Role.objects.get(id="TestLaboratory_V1_Role_"+str(user.identify+1))
+        permission = role.permissions.filter(method=request.method)
+
+        match = False
+        path = request.path
+        if path[-1] == '\n':
+            path = path[:-1]
+        if path[-1] == '/':
+            path = path[:-1]
+        for per in permission:
+            if re.fullmatch(per.url, path):
+                match = True
+                break
+
+        if not match:
+            return HttpResponse(status=500, content='用户无访问权限')
+
+        return self.func(request, *args, **kwargs)

+ 68 - 0
apps/user/migrations/0001_initial.py

@@ -0,0 +1,68 @@
+# Generated by Django 3.2 on 2021-07-17 20:05
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    initial = True
+
+    dependencies = [
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='Permission',
+            fields=[
+                ('id', models.CharField(default='TestLaboratory_V1_Permission_1', max_length=50, primary_key=True, serialize=False, unique=True, verbose_name='权限ID')),
+                ('name', models.CharField(max_length=25, verbose_name='权限名')),
+                ('url', models.URLField(blank=True, max_length=125, null=True, verbose_name='URL')),
+                ('method', models.CharField(max_length=20, null=True, verbose_name='方法')),
+                ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
+                ('update_time', models.DateTimeField(auto_now=True, verbose_name='修改时间')),
+                ('delete', models.BooleanField(default=False)),
+            ],
+            options={
+                'verbose_name': '权限',
+                'verbose_name_plural': '权限',
+                'db_table': 'permission',
+            },
+        ),
+        migrations.CreateModel(
+            name='Role',
+            fields=[
+                ('id', models.CharField(default='TestLaboratory_V1_Role_1', max_length=50, primary_key=True, serialize=False, unique=True, verbose_name='角色ID')),
+                ('name', models.CharField(max_length=25, unique=True, verbose_name='角色名')),
+                ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
+                ('update_time', models.DateTimeField(auto_now=True, verbose_name='修改时间')),
+                ('delete', models.BooleanField(default=False)),
+                ('permissions', models.ManyToManyField(blank=True, to='user.Permission', verbose_name='权限信息')),
+            ],
+            options={
+                'verbose_name': '角色',
+                'verbose_name_plural': '角色',
+                'db_table': 'role',
+            },
+        ),
+        migrations.CreateModel(
+            name='User',
+            fields=[
+                ('id', models.CharField(default='TestLaboratory_V1_User_1', max_length=50, primary_key=True, serialize=False, unique=True, verbose_name='用户编号')),
+                ('username', models.CharField(max_length=30, unique=True, verbose_name='用户名')),
+                ('name', models.CharField(max_length=20, verbose_name='姓名')),
+                ('password', models.CharField(default='123456', max_length=60, verbose_name='密码')),
+                ('tel', models.CharField(max_length=20, verbose_name='联系方式')),
+                ('identify', models.IntegerField(verbose_name='身份信息')),
+                ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
+                ('update_time', models.DateTimeField(auto_now=True, verbose_name='修改时间')),
+                ('is_login', models.BooleanField(default=False)),
+                ('delete', models.BooleanField(default=False)),
+                ('roles', models.ManyToManyField(blank=True, to='user.Role', verbose_name='角色信息')),
+            ],
+            options={
+                'verbose_name': '用户',
+                'verbose_name_plural': '用户',
+                'db_table': 'user',
+            },
+        ),
+    ]

+ 33 - 0
apps/user/migrations/0002_auto_20220619_1043.py

@@ -0,0 +1,33 @@
+# Generated by Django 3.2.4 on 2022-06-19 10:43
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('user', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='user',
+            name='group',
+            field=models.CharField(default='默认分组', max_length=128, verbose_name='分组'),
+        ),
+        migrations.AlterField(
+            model_name='permission',
+            name='id',
+            field=models.CharField(default='TestLaboratory_V1_Permission_1', max_length=128, primary_key=True, serialize=False, unique=True, verbose_name='权限ID'),
+        ),
+        migrations.AlterField(
+            model_name='role',
+            name='id',
+            field=models.CharField(default='TestLaboratory_V1_Role_1', max_length=128, primary_key=True, serialize=False, unique=True, verbose_name='角色ID'),
+        ),
+        migrations.AlterField(
+            model_name='user',
+            name='id',
+            field=models.CharField(default='TestLaboratory_V1_User_1', max_length=128, primary_key=True, serialize=False, unique=True, verbose_name='用户编号'),
+        ),
+    ]

+ 0 - 0
apps/user/migrations/__init__.py


+ 69 - 0
apps/user/models.py

@@ -0,0 +1,69 @@
+from django.contrib.auth.base_user import AbstractBaseUser
+from django.contrib.auth.models import PermissionsMixin, AbstractUser
+from django.db import models
+
+
+# Create your models here.
+class Permission(models.Model):
+    id = models.CharField(max_length=128, primary_key=True, unique=True, verbose_name='权限ID',
+                          default="TestLaboratory_V1_Permission_1")
+    name = models.CharField(max_length=25, verbose_name='权限名')
+    url = models.URLField(max_length=125, null=True, blank=True, verbose_name='URL')
+    # get, post, put, delete
+    method = models.CharField(max_length=20, null=True, verbose_name='方法')
+    create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
+    update_time = models.DateTimeField(auto_now=True, verbose_name='修改时间')
+    delete = models.BooleanField(default=False)
+
+    def __str__(self):
+        return self.name
+
+    class Meta:
+        verbose_name = '权限'
+        verbose_name_plural = verbose_name
+        db_table = 'permission'
+
+
+class Role(models.Model):
+    id = models.CharField(max_length=128, primary_key=True, unique=True, verbose_name='角色ID',
+                          default='TestLaboratory_V1_Role_1')
+    name = models.CharField(max_length=25, unique=True, verbose_name='角色名')
+    permissions = models.ManyToManyField(Permission, blank=True, verbose_name='权限信息')
+    create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
+    update_time = models.DateTimeField(auto_now=True, verbose_name='修改时间')
+    delete = models.BooleanField(default=False)
+
+    def __str__(self):
+        return self.name
+
+    class Meta:
+        verbose_name = '角色'
+        verbose_name_plural = verbose_name
+        db_table = 'role'
+
+
+class User(models.Model):
+    id = models.CharField(max_length=128, unique=True, primary_key=True, verbose_name='用户编号',
+                          default='TestLaboratory_V1_User_1')
+    username = models.CharField(max_length=30, unique=True, verbose_name='用户名')
+    name = models.CharField(max_length=20, verbose_name='姓名')
+    password = models.CharField(max_length=60, default='123456', verbose_name='密码')
+    tel = models.CharField(max_length=20, verbose_name='联系方式')
+    identify = models.IntegerField(verbose_name='身份信息')
+    roles = models.ManyToManyField(Role, blank=True, verbose_name='角色信息')
+    group = models.CharField(max_length=128, default='默认分组', verbose_name='分组')
+    create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
+    update_time = models.DateTimeField(auto_now=True, verbose_name='修改时间')
+    is_login = models.BooleanField(default=False)
+    delete = models.BooleanField(default=False)
+
+    def __str__(self):
+        return self.username
+
+    class Meta:
+        verbose_name = '用户'
+        verbose_name_plural = verbose_name
+        db_table = 'user'
+
+
+

+ 3 - 0
apps/user/tests.py

@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.

Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff