├── .gitignore ├── LICENSE ├── README.md ├── architecture.drawio ├── architecture.png ├── backend ├── Dockerfile ├── Pipfile ├── README.md ├── api.png ├── app_case │ ├── __init__.py │ ├── admin.py │ ├── api.py │ ├── apps.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_testcase_label_testcasetemp_label.py │ │ └── __init__.py │ ├── models.py │ ├── running.py │ ├── schema │ │ └── __init__.py │ └── tests.py ├── app_project │ ├── __init__.py │ ├── admin.py │ ├── api.py │ ├── apps.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_env_remote.py │ │ ├── 0003_env_is_clear_cache.py │ │ └── __init__.py │ ├── models.py │ ├── schema │ │ └── __init__.py │ └── tests.py ├── app_task │ ├── __init__.py │ ├── admin.py │ ├── api.py │ ├── apps.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_testtask_is_delete.py │ │ ├── 0003_alter_testtask_timed.py │ │ └── __init__.py │ ├── models.py │ ├── running.py │ ├── schema │ │ └── __init__.py │ └── tests.py ├── app_team │ ├── __init__.py │ ├── admin.py │ ├── api.py │ ├── apps.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── schema │ │ └── __init__.py │ └── tests.py ├── app_user │ ├── __init__.py │ ├── admin.py │ ├── api.py │ ├── apps.py │ ├── migrations │ │ └── __init__.py │ ├── models.py │ ├── schema │ │ └── __init__.py │ └── tests.py ├── app_utils │ ├── __init__.py │ ├── background.py │ ├── email_utils.py │ ├── git_utils.py │ ├── module_utils.py │ ├── pagination.py │ ├── permission.py │ ├── project_utils.py │ ├── response.py │ ├── running_utils.py │ └── token.py ├── backend │ ├── __init__.py │ ├── api.py │ ├── asgi.py │ ├── config.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── dev.sqlite3 ├── docker-compose.yaml ├── docs │ ├── deploy.md │ └── env-config.png ├── logs │ ├── debug.log │ └── log.txt ├── manage.py ├── reports │ ├── .keep │ ├── 1716275493.xml │ └── 1_1716275534.xml ├── requirements.txt ├── resource │ └── github │ │ └── .keep ├── static │ └── images │ │ ├── .keep │ │ ├── 2d82cb919cf05116adf720f8f7437ac9.png │ │ └── c8f816777c4a29ad3f797ab16aba2ea5.jpg ├── staticfiles │ ├── admin │ │ ├── css │ │ │ ├── autocomplete.css │ │ │ ├── base.css │ │ │ ├── changelists.css │ │ │ ├── dark_mode.css │ │ │ ├── dashboard.css │ │ │ ├── forms.css │ │ │ ├── login.css │ │ │ ├── nav_sidebar.css │ │ │ ├── responsive.css │ │ │ ├── responsive_rtl.css │ │ │ ├── rtl.css │ │ │ ├── vendor │ │ │ │ └── select2 │ │ │ │ │ ├── LICENSE-SELECT2.md │ │ │ │ │ ├── select2.css │ │ │ │ │ └── select2.min.css │ │ │ └── widgets.css │ │ ├── img │ │ │ ├── LICENSE │ │ │ ├── README.txt │ │ │ ├── calendar-icons.svg │ │ │ ├── gis │ │ │ │ ├── move_vertex_off.svg │ │ │ │ └── move_vertex_on.svg │ │ │ ├── icon-addlink.svg │ │ │ ├── icon-alert.svg │ │ │ ├── icon-calendar.svg │ │ │ ├── icon-changelink.svg │ │ │ ├── icon-clock.svg │ │ │ ├── icon-deletelink.svg │ │ │ ├── icon-no.svg │ │ │ ├── icon-unknown-alt.svg │ │ │ ├── icon-unknown.svg │ │ │ ├── icon-viewlink.svg │ │ │ ├── icon-yes.svg │ │ │ ├── inline-delete.svg │ │ │ ├── search.svg │ │ │ ├── selector-icons.svg │ │ │ ├── sorting-icons.svg │ │ │ ├── tooltag-add.svg │ │ │ └── tooltag-arrowright.svg │ │ └── js │ │ │ ├── SelectBox.js │ │ │ ├── SelectFilter2.js │ │ │ ├── actions.js │ │ │ ├── admin │ │ │ ├── DateTimeShortcuts.js │ │ │ └── RelatedObjectLookups.js │ │ │ ├── autocomplete.js │ │ │ ├── calendar.js │ │ │ ├── cancel.js │ │ │ ├── change_form.js │ │ │ ├── collapse.js │ │ │ ├── core.js │ │ │ ├── filters.js │ │ │ ├── inlines.js │ │ │ ├── jquery.init.js │ │ │ ├── nav_sidebar.js │ │ │ ├── popup_response.js │ │ │ ├── prepopulate.js │ │ │ ├── prepopulate_init.js │ │ │ ├── theme.js │ │ │ ├── urlify.js │ │ │ └── vendor │ │ │ ├── jquery │ │ │ ├── LICENSE.txt │ │ │ ├── jquery.js │ │ │ └── jquery.min.js │ │ │ ├── select2 │ │ │ ├── LICENSE.md │ │ │ ├── i18n │ │ │ │ ├── af.js │ │ │ │ ├── ar.js │ │ │ │ ├── az.js │ │ │ │ ├── bg.js │ │ │ │ ├── bn.js │ │ │ │ ├── bs.js │ │ │ │ ├── ca.js │ │ │ │ ├── cs.js │ │ │ │ ├── da.js │ │ │ │ ├── de.js │ │ │ │ ├── dsb.js │ │ │ │ ├── el.js │ │ │ │ ├── en.js │ │ │ │ ├── es.js │ │ │ │ ├── et.js │ │ │ │ ├── eu.js │ │ │ │ ├── fa.js │ │ │ │ ├── fi.js │ │ │ │ ├── fr.js │ │ │ │ ├── gl.js │ │ │ │ ├── he.js │ │ │ │ ├── hi.js │ │ │ │ ├── hr.js │ │ │ │ ├── hsb.js │ │ │ │ ├── hu.js │ │ │ │ ├── hy.js │ │ │ │ ├── id.js │ │ │ │ ├── is.js │ │ │ │ ├── it.js │ │ │ │ ├── ja.js │ │ │ │ ├── ka.js │ │ │ │ ├── km.js │ │ │ │ ├── ko.js │ │ │ │ ├── lt.js │ │ │ │ ├── lv.js │ │ │ │ ├── mk.js │ │ │ │ ├── ms.js │ │ │ │ ├── nb.js │ │ │ │ ├── ne.js │ │ │ │ ├── nl.js │ │ │ │ ├── pl.js │ │ │ │ ├── ps.js │ │ │ │ ├── pt-BR.js │ │ │ │ ├── pt.js │ │ │ │ ├── ro.js │ │ │ │ ├── ru.js │ │ │ │ ├── sk.js │ │ │ │ ├── sl.js │ │ │ │ ├── sq.js │ │ │ │ ├── sr-Cyrl.js │ │ │ │ ├── sr.js │ │ │ │ ├── sv.js │ │ │ │ ├── th.js │ │ │ │ ├── tk.js │ │ │ │ ├── tr.js │ │ │ │ ├── uk.js │ │ │ │ ├── vi.js │ │ │ │ ├── zh-CN.js │ │ │ │ └── zh-TW.js │ │ │ ├── select2.full.js │ │ │ └── select2.full.min.js │ │ │ └── xregexp │ │ │ ├── LICENSE.txt │ │ │ ├── xregexp.js │ │ │ └── xregexp.min.js │ └── images │ │ ├── 2d82cb919cf05116adf720f8f7437ac9.png │ │ └── c8f816777c4a29ad3f797ab16aba2ea5.jpg └── uwsgi.ini ├── doc └── promotion_article.md ├── frontendv3 ├── .gitignore ├── README.md ├── dist │ ├── assets │ │ ├── Center.176a5796.js │ │ ├── Env.9aade454.js │ │ ├── Login.21652916.css │ │ ├── Login.ca62ba9b.js │ │ ├── Manager.37945dbb.js │ │ ├── Project.7295eff2.js │ │ ├── Task.b33c6915.js │ │ ├── index.367778c0.js │ │ ├── index.c53f830a.css │ │ └── login-bg.009bc7f3.png │ ├── favicon.ico │ └── index.html ├── index.html ├── package.json ├── playwright.config.ts ├── pnpm-lock.yaml ├── public │ └── favicon.ico ├── shims-vue.d.ts ├── src │ ├── App.vue │ ├── assets │ │ ├── home-bg.png │ │ ├── login-bg.png │ │ ├── seldom-icon.gif │ │ └── seldom-platform.gif │ ├── components │ │ ├── CaseResult.vue │ │ ├── CaseSync.vue │ │ ├── CaseSyncLog.vue │ │ ├── EnvForm.vue │ │ ├── ProjectForm.vue │ │ ├── TaskModal.vue │ │ ├── TaskReport.vue │ │ ├── TaskReportModal.vue │ │ ├── TaskTimed.vue │ │ └── TeamForm.vue │ ├── config │ │ └── base-url.ts │ ├── env.d.ts │ ├── layouts │ │ ├── BaseLayout.vue │ │ ├── BaseNav.vue │ │ ├── CenterNav.vue │ │ └── ManagerNav.vue │ ├── main.ts │ ├── pages │ │ ├── Center.vue │ │ ├── Login.vue │ │ ├── Manager.vue │ │ ├── center │ │ │ ├── Env.vue │ │ │ ├── Project.vue │ │ │ └── Team.vue │ │ └── manager │ │ │ ├── Case.vue │ │ │ └── Task.vue │ ├── request │ │ ├── case.ts │ │ ├── common │ │ │ └── http.ts │ │ ├── project.ts │ │ ├── task.ts │ │ ├── team.ts │ │ └── user.ts │ ├── router.ts │ └── store │ │ └── index.ts ├── tests │ └── example.spec.ts ├── tsconfig.json ├── tsconfig.node.json ├── view.png └── vite.config.ts ├── img ├── github.png ├── login-page.png ├── metersphere.png ├── seldom-code.png ├── seldom-platform-code.png ├── v2_case_list.png └── v2_sync_case.png └── wechat.jpg /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | backend/reports/*.xml 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | pip-wheel-metadata/ 25 | share/python-wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | *.py,cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | .python-version 87 | 88 | # pipenv 89 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 90 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 91 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 92 | # install all needed dependencies. 93 | #Pipfile.lock 94 | 95 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 96 | __pypackages__/ 97 | 98 | # Celery stuff 99 | celerybeat-schedule 100 | celerybeat.pid 101 | 102 | # SageMath parsed files 103 | *.sage.py 104 | 105 | # Environments 106 | .env 107 | .venv 108 | env/ 109 | venv/ 110 | ENV/ 111 | env.bak/ 112 | venv.bak/ 113 | 114 | # Spyder project settings 115 | .spyderproject 116 | .spyproject 117 | 118 | # Rope project settings 119 | .ropeproject 120 | 121 | # mkdocs documentation 122 | /site 123 | 124 | # mypy 125 | .mypy_cache/ 126 | .dmypy.json 127 | dmypy.json 128 | 129 | # Pyre type checker 130 | .pyre/ 131 | .idea/ 132 | .DS_Store 133 | .vscode -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # seldom-platform 2 | 3 | > Write your automated test cases based on Seldom framework and leave the rest to the platform. 4 | 5 | 基于Seldom框架编写你的自动化测试用例,剩下的事情交给平台. 6 | 7 | > 这根传统的测试平台非常不一样,传统的测试平台创建用例是非常低效的,也非常不灵活。但是,平台的优势在于维护测试用例的用例的管理,定时任务,以及结果的可视化管理。selenium-platform可以解析seldom框架编写的自动化用例。~ 这是一个完美的方案。 8 | 9 | 10 | ## seldomQA 架构 11 | 12 | ![](./architecture.png) 13 | 14 | 15 | __三个步骤:__ 16 | 17 | 🐍 **seldom** 18 | 19 | > 1. 通过seldom框架编写自动化测试用例。 20 | 21 | ![](./img/seldom-code.png) 22 | 23 | 🌐 **Github/gitee托管项目代码** 24 | 25 | > 2. 通过git管理自动化测试脚本 26 | 27 | ![](./img/github.png) 28 | 29 | 💻 **seldom-platfrom** 30 | 31 | > 3. 通过seldom-platfrom平台解析用例,执行、查看结果、定时任务... 32 | 33 | ![](./img/seldom-platform-code.png) 34 | 35 | ## 功能支持 36 | 37 | ### 支持测试类型: 38 | 39 | - API ✔️ 40 | - Web UI ✔️ 41 | - App UI ❌(暂不支持) 42 | 43 | > 注:App不支持并不是无法支持,App的运行需要平台接入真机,体验平台部署在阿里云不支持。 44 | 45 | ### 功能开发进度: 46 | 47 | | 功能 | 子模块 | 进度 | 说明 | 48 | | --------- | -------- | ---- | ---- | 49 | | 登录&注册 | - | ✔️ | - | 50 | | - | 用户权限控制 | ✔️ | 根据自己的需求控制用户和接口权限 | 51 | | 配置中心 | 项目配置 | ✔️ | - | 52 | | - | 环境配置 | ✔️ | 需要提供更多自动化运行配置联系作者 | 53 | | - | 团队配置 | ✔️ | - | 54 | | 项目管理 | 用例管理 | ✔️ | - | 55 | | - | 用例标签 | ✔️ | - | 56 | | - | 任务管理 | ✔️ | - | 57 | | - | 任务定时 | ✔️ | 由 `schedule-server`服务提供 | 58 | | 其他 | 实时日志 | 开发中.. | - | 59 | | 其他 | 统计 | 开发中.. | - | 60 | 61 | > 注:定时任务需要启动 [schedule-server](https://github.com/SeldomQA/schedule-server) 服务 62 | 63 | ## 项目说明 64 | 65 | | 项目 | 说明 | 文档 | 66 | | ------------ | ---------------------------------- | ------------------------------ | 67 | | backend | 后端: django + django-ninjia | [link](./backend/README.md) | 68 | | frontendv3 | 前端:vue3 + naive-ui | [link](./frontendv3/README.md) | 69 | 70 | 71 | > __注:__ 72 | > `frontendv3` 是基于Vue3编写的前端,旧版包含的有frontend是基于Vue2编写。 73 | 74 | ### 相关文档 75 | 76 | [《seldom-platform平台使用手册》](https://www.yuque.com/chongshi/raflru/ghot2m) 77 | 78 | [《seldom-platform开发&部署》](https://www.yuque.com/chongshi/raflru/uxp8h7) 79 | 80 | ## 在线体验 81 | 82 | 体验地址:http://seldom.testpub.cn/ (__请添加微信,获得体验账号__) 83 | 84 | * 微信(WeChat) 85 | 86 |
87 |

微信

88 |
89 | 90 | 91 | * QQ官方交流群:948994709 92 | -------------------------------------------------------------------------------- /architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeldomQA/seldom-platform/2ed82ef3abaebca8fc19c1e2105bbdae04d369cc/architecture.png -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use the official Python image as a base image (aliyun - centOS linux) 2 | FROM alibaba-cloud-linux-3-registry.cn-hangzhou.cr.aliyuncs.com/alinux3/python:3.11.1 3 | 4 | # Set environment variables 5 | ENV PYTHONDONTWRITEBYTECODE=1 6 | ENV PYTHONUNBUFFERED=1 7 | 8 | # Set the working directory 9 | WORKDIR /app 10 | 11 | # Install system dependencies using yum 12 | #RUN yum update -y && \ 13 | # yum install -y gcc gcc-c++ make libpq-devel && \ 14 | # yum clean all 15 | 16 | # Install Python dependencies 17 | COPY requirements.txt /app/ 18 | RUN pip install --upgrade pip 19 | RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.doubanio.com/simple/ 20 | 21 | # Install uwsgi 22 | RUN pip install uwsgi 23 | 24 | # Copy the project files 25 | COPY . /app/ 26 | 27 | # Expose the port the app runs on 28 | EXPOSE 8000 29 | 30 | # Run the application 31 | CMD ["uwsgi", "--http", "0.0.0.0:8000", "--chdir", "/app", "--wsgi-file", "backend/wsgi.py", "--master", "--processes", "4", "--threads", "2"] -------------------------------------------------------------------------------- /backend/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | django = "==4.2.11" 8 | django-ninja = "==1.1.0" 9 | seldom = "==3.6.0" 10 | 11 | [dev-packages] 12 | 13 | [requires] 14 | python_version = "3.11" 15 | -------------------------------------------------------------------------------- /backend/README.md: -------------------------------------------------------------------------------- 1 | # django-ninja 2 | 3 | 基于django 的后端项目。 4 | 5 | ## 主要技术栈 6 | 7 | * django 8 | * django-ninja 9 | * seldom 10 | 11 | ## 安装 12 | 13 | ### 安装依赖库 14 | 15 | ```shell 16 | > pip install -r requirements.txt 17 | ``` 18 | 19 | ### 执行数据库同步(可选) 20 | 21 | > 如果需要干净的数据库,删除 `dev.sqlite3` 数据库文件,执行下面的步骤。 22 | 23 | ```bash 24 | > python manage.py makemigrations 25 | > python manage.py migrate 26 | > python manage.py collectstatic # 迁移静态资源: static -> staticfiles 27 | 28 | > python .\manage.py createsuperuser # 创建管理员账号 29 | 用户名 (leave blank to use 'user'): guest 30 | 电子邮件地址: guest@gmail.com 31 | Password: 32 | Password (again): 33 | Superuser created successfully. 34 | ``` 35 | 36 | * `makemigrations` 命令用于检测你对模型(models)所做的更改,并创建一个或多个迁移文件,这些文件描述了将这些更改应用到数据库所需的步骤。 37 | * `migrate` 命令用于将 `makemigrations` 命令生成的迁移文件应用到数据库中。 38 | * `createsuperuser` 命令用户创建超级管理员账号。 39 | 40 | ### 运行Redis 41 | 42 | > 可以根据自己的平台选择安装Redis. 43 | 44 | - Windows: https://github.com/tporadowski/redis 45 | - Linux:https://github.com/redis/redis 46 | 47 | ```shell 48 | > redis-server # 启动redis 49 | ``` 50 | 51 | ## 运行&部署 52 | 53 | ### 开发运行 54 | 55 | ```shell 56 | > python manage.py runserver 57 | ``` 58 | 59 | ### 部署运行 60 | 61 | > 部署事项: 62 | > 1. uwsgi 仅支持在Linux上安装,用pip安装。 63 | > 2. 修改`uwsgi.ini` 中项目路径,带 `->` 配置项需要修改。 64 | > 3. 关闭 `backend/setting.py` 文件中设置 `debug=False`。 65 | 66 | * 安装 uwsgi 67 | 68 | ```shell 69 | > pip install uwsgi 70 | ``` 71 | 72 | * 命令启动 73 | 74 | ```shell 75 | > uwsgi --http 127.0.0.1:8080 --chdir /home/app/seldom-platform/backend/ --wsgi-file backend/wsgi.py --master --processes 4 --threads 2 76 | ``` 77 | 78 | 配置文件启动(参考`uwsgi.ini`文件) 79 | 80 | ```shell 81 | > uwsgi --ini uwsgi.ini & 82 | ``` 83 | 84 | ### 更多部署配置 85 | 86 | * Supervisor管理后端进程(待补充) 87 | * Web测试服务需要使用 docker-selenium 88 | 89 | [点击查看](./docs/deploy.md) 90 | 91 | ## 查看接口 92 | 93 | * 浏览器访问:http://localhost:8000/api/docs 94 | 95 | ![](./api.png) 96 | 97 | * 健康检查接口:http://localhost:8000/api/ping 98 | -------------------------------------------------------------------------------- /backend/api.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeldomQA/seldom-platform/2ed82ef3abaebca8fc19c1e2105bbdae04d369cc/backend/api.png -------------------------------------------------------------------------------- /backend/app_case/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeldomQA/seldom-platform/2ed82ef3abaebca8fc19c1e2105bbdae04d369cc/backend/app_case/__init__.py -------------------------------------------------------------------------------- /backend/app_case/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from app_case.models import TestCase, CaseResult, TestCaseTemp 3 | 4 | 5 | @admin.register(TestCase) 6 | class TestCaseAdmin(admin.ModelAdmin): 7 | list_display = ('id', 'project', 'file_name', 'class_name', 'case_name', 'status', 'create_time') 8 | list_filter = ('status', 'project') 9 | search_fields = ('file_name', 'class_name', 'case_name') 10 | list_per_page = 20 11 | 12 | # 编辑字段 13 | fields = ('project', 'file_name', 'class_name', 'class_doc', 'case_name', 'case_doc', 'status') 14 | # 只读字段 15 | readonly_fields = ('case_hash',) 16 | 17 | 18 | @admin.register(CaseResult) 19 | class CaseResultAdmin(admin.ModelAdmin): 20 | list_display = ('id', 'case', 'name', 'passed', 'error', 'failure', 'skipped', 'tests', 'run_time', 'create_time') 21 | list_filter = ('case',) 22 | search_fields = ('name',) 23 | list_per_page = 20 24 | 25 | # 编辑字段 26 | fields = ('case', 'name', 'passed', 'error', 'failure', 'skipped', 'tests', 'run_time', 'report', 'system_out') 27 | # 只读字段 28 | readonly_fields = ('run_time', 'report', 'system_out') 29 | 30 | 31 | @admin.register(TestCaseTemp) 32 | class TestCaseTempAdmin(admin.ModelAdmin): 33 | list_display = ('id', 'project', 'file_name', 'class_name', 'case_name', 'create_time') 34 | list_filter = ('project',) 35 | search_fields = ('file_name', 'class_name', 'case_name') 36 | list_per_page = 20 37 | 38 | # 编辑字段 39 | fields = ('project', 'file_name', 'class_name', 'class_doc', 'case_name', 'case_doc') 40 | # 只读字段 41 | readonly_fields = ('case_hash',) 42 | -------------------------------------------------------------------------------- /backend/app_case/api.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import threading 4 | import time 5 | 6 | from django.shortcuts import get_object_or_404 7 | from ninja import Router 8 | from seldom.utils import file 9 | 10 | from app_case.models import TestCase, CaseResult 11 | from app_case.running import seldom_running 12 | from app_case.schema import RunCaseIn 13 | from app_project.models import Project 14 | from app_utils.git_utils import LocalGitResource 15 | from app_utils.module_utils import clear_test_modules 16 | from app_utils.response import response, Error 17 | 18 | router = Router(tags=["case"]) 19 | 20 | logger = logging.getLogger('myapp') 21 | 22 | 23 | @router.post('/{case_id}/running') 24 | def running_case(request, case_id: int, env: RunCaseIn): 25 | """ 26 | 运行测试用例 27 | """ 28 | # 运行环境 29 | env = env.env 30 | case = get_object_or_404(TestCase, pk=case_id) 31 | case.status = 1 32 | case.save() 33 | 34 | case_info = [{ 35 | "file": case.file_name, 36 | "class": { 37 | "name": case.class_name, 38 | "doc": case.class_doc 39 | }, 40 | "method": { 41 | "name": case.case_name, 42 | "doc": case.case_doc 43 | } 44 | }] 45 | 46 | # 项目目录添加环境变量 47 | project = Project.objects.get(id=case.project_id) 48 | 49 | # 项目相关目录 50 | local = LocalGitResource(project.name, project.address) 51 | project_root_dir = local.git_project_dir(suffix=project.run_version) 52 | project_case_dir = file.join(project_root_dir, project.case_dir) 53 | # 判断目录是否存在 54 | if os.path.exists(project_case_dir) is False: 55 | return response(error=Error.CASE_DIR_ERROR) 56 | 57 | # * 清除测试模块 58 | clear_test_modules(project_case_dir) 59 | # 添加环境变量 60 | file.add_to_path(project_root_dir) 61 | 62 | # 定义报告 63 | report_name = f'{str(time.time()).split(".")[0]}.xml' 64 | # 丢给线程执行用例 65 | threads = [] 66 | t = threading.Thread(target=seldom_running, args=(project_case_dir, case_info, report_name, case.id, env)) 67 | threads.append(t) 68 | for t in threads: 69 | t.start() 70 | 71 | return response() 72 | 73 | 74 | @router.get('/{case_id}/result') 75 | def get_case_result(request, case_id: int): 76 | """ 77 | 获取测试用例执行结果 78 | """ 79 | results = CaseResult.objects.filter(case_id=case_id).order_by("-create_time") 80 | if len(results) == 0: 81 | result = [] 82 | else: 83 | result = results[0] 84 | return response(result=result) 85 | -------------------------------------------------------------------------------- /backend/app_case/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AppCaseConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'app_case' 7 | -------------------------------------------------------------------------------- /backend/app_case/migrations/0002_testcase_label_testcasetemp_label.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.11 on 2024-12-19 19:13 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('app_case', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='testcase', 15 | name='label', 16 | field=models.TextField(blank=True, default='', null=True, verbose_name='用例标签'), 17 | ), 18 | migrations.AddField( 19 | model_name='testcasetemp', 20 | name='label', 21 | field=models.TextField(blank=True, default='', null=True, verbose_name='用例标签'), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /backend/app_case/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeldomQA/seldom-platform/2ed82ef3abaebca8fc19c1e2105bbdae04d369cc/backend/app_case/migrations/__init__.py -------------------------------------------------------------------------------- /backend/app_case/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from app_project.models import Project 4 | 5 | 6 | class TestCaseTemp(models.Model): 7 | """ 8 | 测试用例备份表 9 | """ 10 | project = models.ForeignKey(Project, on_delete=models.CASCADE) 11 | file_name = models.CharField("文件名", max_length=500, null=False, default="") 12 | class_name = models.CharField("类名", max_length=200, null=False, default="") 13 | class_doc = models.TextField("类描述", null=True, blank=True, default="") 14 | case_name = models.CharField("方法名", max_length=200, null=False, default="") 15 | case_doc = models.TextField("方法描述", null=True, blank=True, default="") 16 | label = models.TextField("用例标签", null=True, blank=True, default="") 17 | case_hash = models.CharField("用例hash", max_length=200, null=False, default="") 18 | create_time = models.DateTimeField("创建时间", auto_now_add=True) 19 | 20 | def __str__(self): 21 | return self.case_name 22 | 23 | 24 | class TestCase(models.Model): 25 | """ 26 | 测试类&用例 27 | """ 28 | project = models.ForeignKey(Project, on_delete=models.CASCADE) 29 | file_name = models.CharField("文件名", max_length=500, null=False, default="") 30 | class_name = models.CharField("类名", max_length=200, null=False, default="") 31 | class_doc = models.TextField("类描述", null=True, blank=True, default="") 32 | case_name = models.CharField("方法名", max_length=200, null=False, default="") 33 | case_doc = models.TextField("方法描述", null=True, blank=True, default="") 34 | label = models.TextField("用例标签", null=True, blank=True, default="") 35 | status = models.IntegerField("状态", default=0) # 0未执行、1执行中、2已执行 36 | case_hash = models.CharField("用例hash", max_length=200, null=False, default="") 37 | create_time = models.DateTimeField("创建时间", auto_now_add=True) 38 | update_time = models.DateTimeField("更新时间", auto_now=True) 39 | 40 | def __str__(self): 41 | return self.case_name 42 | 43 | 44 | class CaseResult(models.Model): 45 | """ 46 | 测试用例保存结果 47 | """ 48 | case = models.ForeignKey(TestCase, on_delete=models.CASCADE) 49 | name = models.CharField("名称", max_length=100, blank=False, default="") 50 | report = models.TextField("报告内容", null=True, default="") 51 | passed = models.IntegerField("通过用例", default=0) 52 | error = models.IntegerField("错误用例", default=0) 53 | failure = models.IntegerField("失败用例", default=0) 54 | skipped = models.IntegerField("跳过用例", default=0) 55 | tests = models.IntegerField("总用例数", default=0) 56 | system_out = models.TextField("日志", null=True, default="") 57 | run_time = models.FloatField("运行时长", default=0) 58 | create_time = models.DateTimeField("创建时间", auto_now_add=True) 59 | 60 | def __str__(self): 61 | return self.name 62 | -------------------------------------------------------------------------------- /backend/app_case/running.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | from xml.dom.minidom import parse 4 | 5 | from seldom.logging import log 6 | 7 | from app_case.models import TestCase, CaseResult 8 | from app_project.models import Env 9 | from app_utils import background 10 | from app_utils.running_utils import configure_test_runner 11 | from backend.settings import REPORT_DIR 12 | 13 | # Use 10 background threads. 14 | background.n = 10 15 | 16 | 17 | @background.task 18 | def seldom_running(test_dir: str, case_info: list, report_name: str, case_id: int, env: int): 19 | """ 20 | seldom运行用例 21 | :param test_dir: 测试目录 22 | :param case_info: 23 | :param report_name: 24 | :param case_id: 25 | :param env: 26 | :return: 27 | """ 28 | # 配置运行环境 29 | env = Env.objects.get(id=env) 30 | 31 | # 使用工具函数配置运行器 32 | main_extend = configure_test_runner(env, test_dir, report_name) 33 | main_extend.run_cases(case_info) 34 | time.sleep(1) 35 | 36 | # 打开xml文档 37 | report_path = os.path.join(REPORT_DIR, report_name) 38 | dom = parse(report_path) 39 | # 得到文档元素对象 40 | root = dom.documentElement 41 | # 获取(一组)标签 42 | testsuite = root.getElementsByTagName('testsuite') 43 | name = testsuite[0].getAttribute("name") 44 | run_time = testsuite[0].getAttribute("time") 45 | errors = testsuite[0].getAttribute("errors") 46 | failures = testsuite[0].getAttribute("failures") 47 | skipped = testsuite[0].getAttribute("skipped") 48 | tests = testsuite[0].getAttribute("tests") 49 | passed = int(tests) - int(errors) - int(failures) - int(skipped) 50 | 51 | testcase = root.getElementsByTagName('testcase') 52 | system_out = "" 53 | for case in testcase: 54 | system_out = system_out + "Case Name: " + case.getAttribute("name") + "\n" 55 | try: 56 | system_out = system_out + case.childNodes[1].firstChild.data + "\n" 57 | except (AttributeError, IndexError) as e: 58 | pass 59 | 60 | try: 61 | system_out = system_out + case.childNodes[3].firstChild.data + "\n" 62 | except (AttributeError, IndexError) as e: 63 | pass 64 | 65 | try: 66 | system_out = system_out + case.childNodes[5].firstChild.data 67 | except (AttributeError, IndexError) as e: 68 | pass 69 | 70 | with open(report_path, "r", encoding="utf-8") as f: 71 | report_text = f.read() 72 | # 保存表 73 | CaseResult.objects.create( 74 | case_id=case_id, 75 | name=name, 76 | report=report_text, 77 | passed=passed, 78 | error=errors, 79 | failure=failures, 80 | skipped=skipped, 81 | tests=tests, 82 | system_out=system_out, 83 | run_time=run_time, 84 | ) 85 | # 修改状态 86 | test_case = TestCase.objects.get(id=case_id) 87 | test_case.status = 2 88 | test_case.save() 89 | 90 | # 删除报告文件 91 | # os.remove(report_path) 92 | 93 | log.info("running end!!") 94 | -------------------------------------------------------------------------------- /backend/app_case/schema/__init__.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | from ninja import Schema 4 | 5 | 6 | class EnvType(str, Enum): 7 | """环境类型""" 8 | product = "product" 9 | preannouncement = "preannouncement" 10 | 11 | 12 | class RunCaseIn(Schema): 13 | """运行测试用例入参""" 14 | env: int # 环境ID, 从Env表查询 15 | -------------------------------------------------------------------------------- /backend/app_case/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /backend/app_project/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeldomQA/seldom-platform/2ed82ef3abaebca8fc19c1e2105bbdae04d369cc/backend/app_project/__init__.py -------------------------------------------------------------------------------- /backend/app_project/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from app_project.models import Project, Env 4 | 5 | 6 | @admin.register(Project) 7 | class ProjectAdmin(admin.ModelAdmin): 8 | list_display = ( 9 | 'id', 'name', 'address', 'case_dir', 'is_delete', 'is_clone', 'run_version', 'test_num', 'create_time') 10 | list_filter = ('is_delete', 'is_clone') 11 | search_fields = ('name', 'address') 12 | list_per_page = 20 13 | 14 | # 编辑字段 15 | fields = ('name', 'address', 'case_dir', 'is_delete', 'is_clone', 'run_version', 'test_num') 16 | # 只读字段 17 | readonly_fields = ('is_clone', 'run_version', 'test_num') 18 | 19 | 20 | @admin.register(Env) 21 | class EnvAdmin(admin.ModelAdmin): 22 | list_display = ('id', 'name', 'test_type', 'env', 'browser', 'base_url', 'remote', 'is_delete', 'create_time') 23 | list_filter = ('test_type', 'is_delete') 24 | search_fields = ('name',) 25 | list_per_page = 20 26 | 27 | # 编辑字段 28 | fields = ('name', 'test_type', 'env', 'browser', 'base_url', 'remote', 'is_delete') 29 | -------------------------------------------------------------------------------- /backend/app_project/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AppProjectConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'app_project' 7 | -------------------------------------------------------------------------------- /backend/app_project/migrations/0002_env_remote.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.11 on 2024-07-30 20:54 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('app_project', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='env', 15 | name='remote', 16 | field=models.CharField(default='', max_length=200, null=True, verbose_name='remote'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /backend/app_project/migrations/0003_env_is_clear_cache.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.11 on 2024-11-18 23:58 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('app_project', '0002_env_remote'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='env', 15 | name='is_clear_cache', 16 | field=models.BooleanField(default=False, verbose_name='是否清除缓存'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /backend/app_project/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeldomQA/seldom-platform/2ed82ef3abaebca8fc19c1e2105bbdae04d369cc/backend/app_project/migrations/__init__.py -------------------------------------------------------------------------------- /backend/app_project/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Project(models.Model): 5 | """ 6 | 项目表 7 | """ 8 | name = models.CharField("名称", max_length=50, null=False) 9 | address = models.CharField("项目地址", max_length=200, null=False) 10 | case_dir = models.CharField("用例目录", max_length=200, default="test_dir") 11 | is_delete = models.BooleanField("删除", null=True, default=False) 12 | create_time = models.DateTimeField(auto_now_add=True) 13 | update_time = models.DateTimeField(auto_now=True) 14 | cover_name = models.CharField("封面名称", max_length=64, default="") 15 | path_name = models.CharField("封面路径名称", max_length=64, default="") 16 | test_num = models.IntegerField("测试文件数", default=0) 17 | is_clone = models.IntegerField("克隆", default=0) 18 | run_version = models.CharField("当前运行版本(蓝绿运行)", max_length=200, default="") 19 | 20 | def __str__(self): 21 | return self.name 22 | 23 | 24 | class Env(models.Model): 25 | """ 26 | 环境管理 27 | 说明: 28 | * test_type = http/web/app # seldom框架支持三种类型测试 29 | * env = Seldom.env # 指定当前运行环境 env=production/develop/test 30 | * browser = seldom.main(browser="xxx") # web测试,指定当前运行的浏览器 xxx=chrome/firefox/edge 31 | * base_url = seldom.main(base_url="xxx") # http接口测试:指定当前运行的URL xxx=http://www.httpbin.org 32 | * remote selenium grid 远程节点 33 | """ 34 | name = models.CharField("名称", max_length=50, null=False) 35 | test_type = models.CharField("环境值", max_length=20, null=True, default="http") 36 | env = models.CharField("环境值", max_length=50, null=True, default="") 37 | rerun = models.IntegerField("重跑次数", default=0) 38 | is_clear_cache = models.BooleanField("是否清除缓存", default=False) 39 | browser = models.CharField("环境值", max_length=20, null=True, default="") 40 | base_url = models.CharField("URL", max_length=200, null=True, default="") 41 | remote = models.CharField("remote", max_length=200, null=True, default="") 42 | app_server = models.CharField("APP服务", max_length=100, null=True, default="") 43 | app_info = models.CharField("APP信息", max_length=1000, null=True, default="{}") 44 | is_delete = models.BooleanField('删除', default=False) 45 | create_time = models.DateTimeField(auto_now_add=True) 46 | update_time = models.DateTimeField(auto_now=True) 47 | 48 | def __str__(self): 49 | return self.name 50 | -------------------------------------------------------------------------------- /backend/app_project/schema/__init__.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from ninja import Schema 4 | 5 | 6 | class ProjectIn(Schema): 7 | """项目入参""" 8 | name: str 9 | address: str 10 | case_dir: str 11 | cover_name: str = None 12 | path_name: str = None 13 | 14 | 15 | class EnvIn(Schema): 16 | """环境入参""" 17 | name: str 18 | test_type: str 19 | env: Optional[str] = None 20 | rerun: int = None 21 | is_clear_cache: bool = False 22 | browser: Optional[str] = None 23 | base_url: Optional[str] = None 24 | remote: Optional[str] = None 25 | app_server: Optional[str] = None 26 | app_info: Optional[str] = "{}" 27 | 28 | 29 | class MergeCase(Schema): 30 | """合并用例""" 31 | add_case: list 32 | del_case: list 33 | -------------------------------------------------------------------------------- /backend/app_project/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /backend/app_task/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeldomQA/seldom-platform/2ed82ef3abaebca8fc19c1e2105bbdae04d369cc/backend/app_task/__init__.py -------------------------------------------------------------------------------- /backend/app_task/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from app_task.models import TestTask, TaskReport, ReportDetails 3 | from app_project.models import Env 4 | from app_team.models import Team 5 | 6 | 7 | @admin.register(TestTask) 8 | class TestTaskAdmin(admin.ModelAdmin): 9 | list_display = ('id', 'name', 'project', 'get_env_name', 'get_team_name', 'status', 'is_delete', 'execute_count', 'create_time') 10 | list_filter = ('status', 'is_delete', 'project') 11 | search_fields = ('name',) 12 | list_per_page = 20 13 | 14 | # 编辑字段 15 | fields = ('name', 'project', 'env_id', 'team_id', 'status', 'is_delete', 'execute_count', 'case') 16 | # 只读字段 17 | readonly_fields = ('execute_count',) 18 | 19 | def get_env_name(self, obj): 20 | try: 21 | env = Env.objects.get(id=obj.env_id) 22 | return env.name 23 | except Env.DoesNotExist: 24 | return '-' 25 | get_env_name.short_description = '环境' 26 | get_env_name.admin_order_field = 'env_id' 27 | 28 | def get_team_name(self, obj): 29 | try: 30 | team = Team.objects.get(id=obj.team_id) 31 | return team.name 32 | except Team.DoesNotExist: 33 | return '-' 34 | get_team_name.short_description = '团队' 35 | get_team_name.admin_order_field = 'team_id' 36 | 37 | 38 | @admin.register(TaskReport) 39 | class TaskReportAdmin(admin.ModelAdmin): 40 | list_display = ('id', 'task', 'name', 'passed', 'error', 'failure', 'skipped', 'tests', 'run_time', 'create_time') 41 | list_filter = ('task',) 42 | search_fields = ('name',) 43 | list_per_page = 20 44 | 45 | # 编辑字段 46 | fields = ('task', 'name', 'passed', 'error', 'failure', 'skipped', 'tests', 'run_time', 'report') 47 | # 只读字段 48 | readonly_fields = ('run_time', 'report') 49 | 50 | 51 | @admin.register(ReportDetails) 52 | class ReportDetailsAdmin(admin.ModelAdmin): 53 | list_display = ('id', 'result', 'class_name', 'name', 'run_time', 'create_time') 54 | list_filter = ('result',) 55 | search_fields = ('class_name', 'name') 56 | list_per_page = 20 57 | 58 | # 编辑字段 59 | fields = ('result', 'class_name', 'name', 'run_time', 'doc', 'system_out', 'system_err', 'failure_out', 'error_out', 'skipped_message') 60 | # 只读字段 61 | readonly_fields = ('run_time', 'system_out', 'system_err', 'failure_out', 'error_out') 62 | -------------------------------------------------------------------------------- /backend/app_task/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AppTaskConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'app_task' 7 | -------------------------------------------------------------------------------- /backend/app_task/migrations/0002_testtask_is_delete.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.11 on 2024-11-25 15:36 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('app_task', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='testtask', 15 | name='is_delete', 16 | field=models.BooleanField(default=False, null=True, verbose_name='删除'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /backend/app_task/migrations/0003_alter_testtask_timed.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.11 on 2024-12-09 16:23 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('app_task', '0002_testtask_is_delete'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='testtask', 15 | name='timed', 16 | field=models.CharField(default='', max_length=500, null=True, verbose_name='定时任务'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /backend/app_task/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeldomQA/seldom-platform/2ed82ef3abaebca8fc19c1e2105bbdae04d369cc/backend/app_task/migrations/__init__.py -------------------------------------------------------------------------------- /backend/app_task/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from app_project.models import Project 4 | 5 | 6 | class TestTask(models.Model): 7 | """ 8 | 测试任务 9 | """ 10 | project = models.ForeignKey(Project, on_delete=models.CASCADE) 11 | name = models.CharField("任务名", max_length=200, null=False, default="") 12 | status = models.IntegerField("状态", default=0) # 0未执行、1执行中、2已执行 13 | env_id = models.IntegerField("环境ID", null=True) 14 | team_id = models.IntegerField("团队ID", null=True) 15 | email = models.CharField("发送告警邮箱", max_length=100, null=True) 16 | timed = models.CharField("定时任务", max_length=500, null=True, default="") 17 | execute_count = models.IntegerField("执行次数", null=True, default=0) 18 | is_delete = models.BooleanField("删除", null=True, default=False) 19 | create_time = models.DateTimeField("创建时间", auto_now_add=True) 20 | update_time = models.DateTimeField("更新时间", auto_now=True) 21 | 22 | def __str__(self): 23 | return self.name 24 | 25 | 26 | class TaskCaseRelevance(models.Model): 27 | """ 28 | 任务用例关联表 29 | """ 30 | task = models.ForeignKey(TestTask, on_delete=models.CASCADE) 31 | case_hash = models.CharField("用例hash", max_length=200, null=False) 32 | create_time = models.DateTimeField("创建时间", auto_now_add=True) 33 | 34 | 35 | class TaskReport(models.Model): 36 | """ 37 | 测试任务报告 38 | """ 39 | task = models.ForeignKey(TestTask, on_delete=models.CASCADE) 40 | name = models.CharField("名称", max_length=500, blank=False, default="") 41 | report = models.TextField("报告内容", null=True, default="") 42 | passed = models.IntegerField("通过用例", default=0) 43 | error = models.IntegerField("错误用例", default=0) 44 | failure = models.IntegerField("失败用例", default=0) 45 | skipped = models.IntegerField("跳过用例", default=0) 46 | tests = models.IntegerField("总用例数", default=0) 47 | run_time = models.CharField("运行时长", max_length=100, default="0") 48 | create_time = models.DateTimeField("创建时间", auto_now_add=True) 49 | 50 | def __str__(self): 51 | return self.name 52 | 53 | 54 | class ReportDetails(models.Model): 55 | """ 56 | 任务报告详情 57 | """ 58 | result = models.ForeignKey(TaskReport, on_delete=models.CASCADE) 59 | class_name = models.CharField("用例类称", max_length=500, blank=False, default="") 60 | name = models.CharField("用例名称", max_length=500, blank=False, default="") 61 | run_time = models.CharField("运行时长", max_length=100, default="0") 62 | doc = models.TextField("用例描述", null=True, default="") 63 | system_out = models.TextField("用例日志", null=True, default="") 64 | system_err = models.TextField("用例错误", null=True, default="") 65 | failure_out = models.TextField("用例错误", null=True, default="") 66 | error_out = models.TextField("用例错误", null=True, default="") 67 | skipped_message = models.TextField("用例错误", null=True, default="") 68 | create_time = models.DateTimeField("创建时间", auto_now_add=True) 69 | 70 | def __str__(self): 71 | return self.name 72 | -------------------------------------------------------------------------------- /backend/app_task/schema/__init__.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Union 3 | 4 | from ninja import Schema 5 | 6 | 7 | class TaskIn(Schema): 8 | """任务入参""" 9 | project: str 10 | name: str 11 | env_id: int 12 | team_id: int 13 | cases: list 14 | 15 | 16 | class TaskOut(Schema): 17 | id: int 18 | name: str 19 | status: int 20 | env_id: int 21 | env: str 22 | team_id: int 23 | team: str 24 | timed_status: str 25 | timed_conf: dict 26 | email: str = None 27 | execute_count: int 28 | create_time: Union[datetime] 29 | update_time: Union[datetime] 30 | project_id: int 31 | 32 | 33 | class ReportIn(Schema): 34 | """报告查询入参""" 35 | type: str = None 36 | 37 | 38 | class ReportOut(Schema): 39 | """任务报告出参""" 40 | id: int 41 | name: str 42 | passed: int 43 | error: int 44 | failure: int 45 | skipped: int 46 | tests: int 47 | run_time: str 48 | create_time: Union[datetime] 49 | 50 | 51 | class TimedIn(Schema): 52 | """Crontab 定时任务入参""" 53 | task_id: int 54 | second: str = "0" # 0 - 59s 55 | minute: str = "*" # 0 - 59m 56 | hour: str = "*" # 0-23h 57 | day: str = "*" # 1 - 31 58 | month: str = "*" # 1~12 59 | day_of_week: str = "*" # 一周中的第几天(0 - 6) or (mon、tue、wed、thu、fri、fri、sat、sun) 60 | -------------------------------------------------------------------------------- /backend/app_task/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /backend/app_team/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeldomQA/seldom-platform/2ed82ef3abaebca8fc19c1e2105bbdae04d369cc/backend/app_team/__init__.py -------------------------------------------------------------------------------- /backend/app_team/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from app_team.models import Team 3 | 4 | 5 | @admin.register(Team) 6 | class TeamAdmin(admin.ModelAdmin): 7 | list_display = ('id', 'name', 'email', 'is_delete', 'create_time') 8 | list_filter = ('is_delete',) 9 | search_fields = ('name', 'email') 10 | list_per_page = 20 11 | 12 | # 编辑字段 13 | fields = ('name', 'email', 'is_delete') 14 | -------------------------------------------------------------------------------- /backend/app_team/api.py: -------------------------------------------------------------------------------- 1 | """ 2 | author:lancao 3 | date:2022-09-12 4 | function:团队管理 5 | """ 6 | from django.shortcuts import get_object_or_404 7 | from ninja import Router 8 | 9 | from app_team.models import Team 10 | from app_team.schema import TeamIn 11 | from app_utils.email_utils import validate_email 12 | from app_utils.response import response, Error, model_to_dict 13 | 14 | router = Router(tags=["team"]) 15 | 16 | 17 | @router.post('/create') 18 | def create_team(request, team: TeamIn): 19 | """ 20 | 创建团队 21 | """ 22 | if validate_email(team.email) is False: 23 | return response(error=Error.TEAM_EMAIL_ERROR) 24 | 25 | team_obj = Team.objects.filter(name=team.name, email=team.email) 26 | if len(team_obj) > 0: 27 | return response(error=Error.TEAM_EXIST_ERROR) 28 | 29 | team_obj = Team.objects.create( 30 | name=team.name, 31 | email=team.email 32 | ) 33 | return response(result=model_to_dict(team_obj)) 34 | 35 | 36 | @router.get('/list') 37 | def get_teams(request): 38 | """ 39 | 获取团队列表 40 | """ 41 | teams = Team.objects.filter(is_delete=False) 42 | team_list = [] 43 | for team in teams: 44 | team_list.append(model_to_dict(team)) 45 | return response(result=team_list) 46 | 47 | 48 | @router.get('/{team_id}/') 49 | def get_team(request, team_id: int): 50 | """ 51 | 通过团队Id查询团队 52 | """ 53 | team_obj = get_object_or_404(Team, pk=team_id, is_delete=False) 54 | return response(result=model_to_dict(team_obj)) 55 | 56 | 57 | @router.put('/{team_id}/') 58 | def update_team(request, team_id: int, team: TeamIn): 59 | """ 60 | 通过团队Id更新团队 61 | """ 62 | if validate_email(team.email) is False: 63 | return response(error=Error.TEAM_EMAIL_ERROR) 64 | 65 | team_obj = get_object_or_404(Team, pk=team_id) 66 | team_obj.name = team.name 67 | team_obj.email = team.email 68 | team_obj.save() 69 | return response(result=model_to_dict(team_obj)) 70 | 71 | 72 | @router.delete('/{team_id}/') 73 | def delete_team(request, team_id: int): 74 | """ 75 | 通过团队Id删除团队 76 | """ 77 | team_obj = get_object_or_404(Team, pk=team_id) 78 | team_obj.is_delete = True 79 | team_obj.save() 80 | return response() 81 | -------------------------------------------------------------------------------- /backend/app_team/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AppTeamConfig(AppConfig): 5 | name = 'app_team' 6 | -------------------------------------------------------------------------------- /backend/app_team/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.11 on 2024-05-21 14:38 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | initial = True 8 | 9 | dependencies = [] 10 | 11 | operations = [ 12 | migrations.CreateModel( 13 | name="Team", 14 | fields=[ 15 | ("id", models.AutoField(primary_key=True, serialize=False)), 16 | ("name", models.CharField(max_length=200, verbose_name="团队名")), 17 | ("email", models.TextField(default="", null=True, verbose_name="团队邮箱")), 18 | ("is_delete", models.BooleanField(default=False, verbose_name="删除")), 19 | ("create_time", models.DateTimeField(auto_now_add=True)), 20 | ("update_time", models.DateTimeField(auto_now=True)), 21 | ], 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /backend/app_team/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeldomQA/seldom-platform/2ed82ef3abaebca8fc19c1e2105bbdae04d369cc/backend/app_team/migrations/__init__.py -------------------------------------------------------------------------------- /backend/app_team/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Team(models.Model): 5 | """ 6 | 团队表 7 | """ 8 | id = models.AutoField(primary_key=True) 9 | name = models.CharField("团队名", max_length=200, null=False) 10 | email = models.TextField("团队邮箱", null=True, default="") 11 | is_delete = models.BooleanField('删除', default=False) 12 | create_time = models.DateTimeField(auto_now_add=True) 13 | update_time = models.DateTimeField(auto_now=True) 14 | 15 | def __str__(self) -> models.CharField: 16 | return self.name 17 | -------------------------------------------------------------------------------- /backend/app_team/schema/__init__.py: -------------------------------------------------------------------------------- 1 | from ninja import Schema 2 | 3 | 4 | class TeamIn(Schema): 5 | """项目入参""" 6 | name: str 7 | email: str 8 | -------------------------------------------------------------------------------- /backend/app_team/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /backend/app_user/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeldomQA/seldom-platform/2ed82ef3abaebca8fc19c1e2105bbdae04d369cc/backend/app_user/__init__.py -------------------------------------------------------------------------------- /backend/app_user/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /backend/app_user/api.py: -------------------------------------------------------------------------------- 1 | from app_user.schema import RegisterIn, LoginIn, LogoutIn 2 | from django.contrib.auth.models import User, Permission 3 | from django.contrib.contenttypes.models import ContentType 4 | from ninja import Router 5 | 6 | from app_utils.response import response, Error 7 | from app_utils.token import CustomToken 8 | from backend.settings import ALLOW_REGISTRATION 9 | 10 | router = Router(tags=["user"]) 11 | 12 | 13 | @router.post("/register", auth=None) 14 | def register(request, params: RegisterIn): 15 | """ 16 | 用户注册 17 | """ 18 | # 增加注册限制(体验平台) 19 | if ALLOW_REGISTRATION is False: 20 | return response(error=Error.REGISTER_RESTRICT) 21 | 22 | username = params.username 23 | password = params.password 24 | password2 = params.password2 25 | 26 | if password != password2: 27 | return response(error=Error.PAWD_ERROR) 28 | 29 | try: 30 | User.objects.get_by_natural_key(username) 31 | except User.DoesNotExist: 32 | pass 33 | else: 34 | return response(error=Error.USER_HAS_REGISTERED) 35 | 36 | # 创建用户 37 | user = User.objects.create_user(username=username, password=password) 38 | 39 | # 为新用户添加基本权限 40 | content_type = ContentType.objects.get_for_model(User) 41 | basic_permissions = Permission.objects.filter(content_type=content_type) 42 | for perm in basic_permissions: 43 | user.user_permissions.add(perm) 44 | 45 | user_info = { 46 | "id": user.id, 47 | "username": user.username, 48 | "permissions": [perm.codename for perm in user.user_permissions.all()] 49 | } 50 | return response(result=user_info) 51 | 52 | 53 | @router.post("/login", auth=None) 54 | def login(request, data: LoginIn): 55 | """ 56 | 用户登录 57 | """ 58 | try: 59 | user = User.objects.get(username=data.username) 60 | if user.check_password(data.password): 61 | # 生成token 62 | token_method = CustomToken() 63 | token = token_method.create_token(user.id, user.username) 64 | 65 | return response(result={ 66 | "token": token, 67 | "user_id": user.id, 68 | "username": user.username, 69 | "permissions": [perm.codename for perm in user.user_permissions.all()] 70 | }) 71 | else: 72 | return response(error=Error.LOGIN_PAWD_ERROR) 73 | except User.DoesNotExist: 74 | return response(error=Error.LOGIN_USSER_ERROR) 75 | 76 | 77 | @router.post("/logout", auth=None) 78 | def logout(request, params: LogoutIn): 79 | """ 80 | 退出登录 81 | auth=None 该接口不需要认证 82 | """ 83 | token_method = CustomToken() 84 | token_method.invalidate_token(params.token) 85 | return response() 86 | -------------------------------------------------------------------------------- /backend/app_user/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AppUserConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'app_user' 7 | -------------------------------------------------------------------------------- /backend/app_user/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeldomQA/seldom-platform/2ed82ef3abaebca8fc19c1e2105bbdae04d369cc/backend/app_user/migrations/__init__.py -------------------------------------------------------------------------------- /backend/app_user/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /backend/app_user/schema/__init__.py: -------------------------------------------------------------------------------- 1 | from ninja import Schema 2 | 3 | 4 | class RegisterIn(Schema): 5 | """ 6 | 注册入参 7 | """ 8 | username: str 9 | password: str 10 | password2: str 11 | 12 | 13 | class LoginIn(Schema): 14 | """ 15 | 登录入参 16 | """ 17 | username: str 18 | password: str 19 | 20 | 21 | class LogoutIn(Schema): 22 | """ 23 | 退出入参 24 | """ 25 | token: str 26 | -------------------------------------------------------------------------------- /backend/app_user/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /backend/app_utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .response import response -------------------------------------------------------------------------------- /backend/app_utils/background.py: -------------------------------------------------------------------------------- 1 | import multiprocessing 2 | import concurrent.futures 3 | 4 | 5 | def default_n(): 6 | return multiprocessing.cpu_count() 7 | 8 | 9 | n = default_n() 10 | pool = concurrent.futures.ThreadPoolExecutor(max_workers=n) 11 | callbacks = [] 12 | results = [] 13 | 14 | 15 | def run(f, *args, **kwargs): 16 | 17 | pool._max_workers = n 18 | pool._adjust_thread_count() 19 | 20 | f = pool.submit(f, *args, **kwargs) 21 | results.append(f) 22 | 23 | return f 24 | 25 | 26 | def task(f): 27 | def do_task(*args, **kwargs): 28 | result = run(f, *args, **kwargs) 29 | 30 | for cb in callbacks: 31 | result.add_done_callback(cb) 32 | 33 | return result 34 | 35 | return do_task 36 | 37 | 38 | def callback(f): 39 | callbacks.append(f) 40 | 41 | def register_callback(): 42 | f() 43 | 44 | return register_callback -------------------------------------------------------------------------------- /backend/app_utils/email_utils.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from XTestRunner.config import RunResult 4 | 5 | 6 | def validate_email(email: str) -> bool: 7 | """ 8 | 验证邮箱格式是否正确。 9 | 10 | :param email: 待验证的邮箱地址 11 | :return: 如果邮箱格式正确,返回 True;否则返回 False 12 | """ 13 | # 定义正则表达式 14 | email_regex = r'^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$' 15 | 16 | # 使用正则表达式匹配邮箱 17 | if not re.match(email_regex, email): 18 | return False 19 | 20 | return True 21 | 22 | 23 | def send_email_config(passed, errors, failures, skipped, tests): 24 | """ 25 | XTestRunner Test Result 26 | """ 27 | RunResult.passed = passed 28 | RunResult.errors = errors 29 | RunResult.failed = failures 30 | RunResult.skipped = skipped 31 | RunResult.count = tests 32 | p_percent = '0.00' 33 | e_percent = '0.00' 34 | f_percent = '0.00' 35 | s_percent = '0.00' 36 | if tests > 0: 37 | p_percent = '{:.2%}'.format(RunResult.passed / tests) 38 | e_percent = '{:.2%}'.format(RunResult.errors / tests) 39 | f_percent = '{:.2%}'.format(RunResult.failed / tests) 40 | s_percent = '{:.2%}'.format(RunResult.skipped / tests) 41 | 42 | RunResult.count = tests 43 | RunResult.pass_rate = p_percent 44 | RunResult.error_rate = e_percent 45 | RunResult.failure_rate = f_percent 46 | RunResult.skip_rate = s_percent 47 | -------------------------------------------------------------------------------- /backend/app_utils/module_utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | 5 | def find_test_modules(directory: str) -> list: 6 | """ 7 | 列出指定目录下的所有目录名和去掉后缀的文件名(不遍历子目录) 8 | :param directory: 要列出的目录路径 9 | returns: 10 | """ 11 | dir_names = [] 12 | file_names = [] 13 | 14 | with os.scandir(directory) as entries: 15 | for entry in entries: 16 | if entry.is_dir(): 17 | dir_names.append(entry.name) 18 | elif entry.is_file(): 19 | file_name, _ = os.path.splitext(entry.name) 20 | if file_name == "__init__": 21 | continue 22 | file_names.append(file_name) 23 | 24 | return dir_names + file_names 25 | 26 | 27 | def clear_test_modules(directory: str) -> None: 28 | """ 29 | 清除测试用例目录(test_dir)下所有的测试模块 30 | :param directory: 31 | returns: 32 | """ 33 | if not os.path.isdir(directory): 34 | raise NotADirectoryError(f"{directory} is not a valid directory") 35 | 36 | test_modules = find_test_modules(directory) 37 | for module_name in list(sys.modules): 38 | for m in test_modules: 39 | if module_name.startswith(m): 40 | try: 41 | del sys.modules[module_name] 42 | except KeyError: 43 | continue 44 | -------------------------------------------------------------------------------- /backend/app_utils/pagination.py: -------------------------------------------------------------------------------- 1 | """ 2 | author: @虫师 3 | date: 2022-03-20 4 | function: 分页器 5 | """ 6 | from typing import List, Any 7 | 8 | from django.db.models import QuerySet 9 | from ninja import Field, Schema 10 | from ninja.pagination import PaginationBase 11 | 12 | 13 | class CustomPagination(PaginationBase): 14 | """ 15 | 自定义分页器 16 | """ 17 | 18 | class Input(Schema): 19 | page: int = Field(1, gt=0) 20 | size: int = 6 21 | 22 | class Output(Schema): 23 | success: bool = True 24 | code: dict = {"code": "", "message": ""} 25 | total: int 26 | page: int 27 | size: int 28 | result: List[Any] 29 | 30 | items_attribute: str = "result" 31 | 32 | def paginate_queryset(self, queryset: QuerySet, pagination: Input, **params): 33 | page: int = pagination.page 34 | size: int = pagination.size 35 | offset = (page - 1) * size 36 | data = { 37 | "result": queryset[offset: offset + size], 38 | "total": len(queryset), 39 | "page": page, 40 | "size": size 41 | } 42 | 43 | return data 44 | -------------------------------------------------------------------------------- /backend/app_utils/permission.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | from typing import List, Union 3 | 4 | from ninja.errors import HttpError 5 | 6 | from app_utils.response import ErrorCode 7 | 8 | 9 | def check_permissions(permissions: Union[str, List[str]]): 10 | """ 11 | 检查用户权限的装饰器 12 | :param permissions: 单个权限字符串或权限列表 13 | """ 14 | if isinstance(permissions, str): 15 | permissions = [permissions] 16 | 17 | def decorator(func): 18 | @wraps(func) 19 | def wrapper(request, *args, **kwargs): 20 | # 检查用户是否有任一所需权限 21 | for permission in permissions: 22 | if request.user.has_perm(permission): 23 | return func(request, *args, **kwargs) 24 | 25 | # 如果没有任何所需权限,返回权限错误 26 | raise HttpError( 27 | 200, 28 | ErrorCode.PERMISSION_DENIED 29 | ) 30 | 31 | return wrapper 32 | 33 | return decorator 34 | 35 | 36 | # 项目权限 37 | PROJECT_PERMISSIONS = { 38 | "CREATE": "project.add_project", 39 | "CHANGE": "project.change_project", 40 | "DELETE": "project.delete_project", 41 | "VIEW": "project.view_project", 42 | } 43 | 44 | # 环境权限 45 | ENV_PERMISSIONS = { 46 | "CREATE": "project.add_env", 47 | "CHANGE": "project.change_env", 48 | "DELETE": "project.delete_env", 49 | "VIEW": "project.view_env", 50 | } 51 | -------------------------------------------------------------------------------- /backend/app_utils/project_utils.py: -------------------------------------------------------------------------------- 1 | import errno 2 | import hashlib 3 | import os 4 | import shutil 5 | import stat 6 | 7 | 8 | def get_hash(string: str) -> str: 9 | """ 10 | 生成字符串hash 11 | :param string: 12 | """ 13 | md5 = hashlib.md5() 14 | sign_bytes_utf8 = string.encode() 15 | md5.update(sign_bytes_utf8) 16 | string_hash = md5.hexdigest() 17 | return string_hash 18 | 19 | 20 | def copytree(source_dir: str, target_dir: str) -> None: 21 | """ 22 | 复制项目目录 23 | :param source_dir: 24 | :param target_dir: 25 | """ 26 | # 如果目标目录不存在则创建 27 | if not os.path.exists(target_dir): 28 | os.makedirs(target_dir) 29 | 30 | def handle_remove_read_only(func, path, exc): 31 | """ 32 | 删除目录 33 | :param func: 34 | :param path: 35 | :param exc: 36 | :return: 37 | """ 38 | excvalue = exc[1] 39 | if func in (os.rmdir, os.remove, os.unlink) and excvalue.errno == errno.EACCES: 40 | os.chmod(path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) # 0777 41 | func(path) 42 | else: 43 | raise 44 | 45 | # 删除 46 | shutil.rmtree(target_dir, onerror=handle_remove_read_only) 47 | # 复制 48 | shutil.copytree(source_dir, target_dir) 49 | -------------------------------------------------------------------------------- /backend/app_utils/running_utils.py: -------------------------------------------------------------------------------- 1 | from seldom import Seldom, TestMainExtend 2 | from seldom.utils import cache 3 | from selenium.webdriver import ChromeOptions, FirefoxOptions, EdgeOptions 4 | 5 | from app_project.models import Env 6 | 7 | 8 | def configure_browser(env: Env): 9 | """ 10 | 配置浏览器设置 11 | :param env: Env 对象 12 | :return: browser_conf 配置字典 13 | """ 14 | browser_conf = None 15 | if env.browser != "": 16 | browser_type = env.browser 17 | browser_conf = {} 18 | if env.remote != "" and env.remote is not None: 19 | browser_conf["command_executor"] = env.remote 20 | # 设置浏览器headless模式 21 | if browser_type in ["gc", "chrome"]: 22 | chrome_options = ChromeOptions() 23 | chrome_options.add_argument("--headless=new") 24 | browser_conf["browser"] = "chrome" 25 | browser_conf["options"] = chrome_options 26 | elif browser_type in ["ff", "firefox"]: 27 | firefox_options = FirefoxOptions() 28 | firefox_options.add_argument("-headless") 29 | browser_conf["browser"] = "firefox" 30 | browser_conf["options"] = firefox_options 31 | elif browser_type in ["edge"]: 32 | edge_options = EdgeOptions() 33 | edge_options.add_argument("--headless=new") 34 | browser_conf["browser"] = "edge" 35 | browser_conf["options"] = edge_options 36 | return browser_conf 37 | 38 | 39 | def configure_test_runner(env: Env, test_dir: str, report_name: str): 40 | """ 41 | 配置测试运行器 42 | :param env: Env 对象 43 | :param test_dir: 测试目录 44 | :param report_name: 报告名称 45 | :return: TestMainExtend 实例 46 | """ 47 | Seldom.env = env.env if env.env != "" else None 48 | base_url = env.base_url if env.base_url != "" else None 49 | browser_conf = configure_browser(env) 50 | 51 | # running pre - is clear all cache 52 | if env.is_clear_cache: 53 | cache.clear() 54 | main_extend = TestMainExtend(path=test_dir, report=report_name, rerun=env.rerun) 55 | if env.test_type == "http": 56 | main_extend = TestMainExtend(path=test_dir, report=report_name, base_url=base_url, rerun=env.rerun) 57 | elif env.test_type == "web": 58 | main_extend = TestMainExtend(path=test_dir, report=report_name, browser=browser_conf, rerun=env.rerun) 59 | 60 | return main_extend 61 | -------------------------------------------------------------------------------- /backend/app_utils/token.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | from typing import Optional, Dict 3 | import jwt 4 | from django.contrib.auth.models import User 5 | from django.core.cache import cache 6 | 7 | SECRET_KEY = "seldom-platform-007" # 建议放到配置文件中 8 | 9 | 10 | class CustomToken: 11 | """ 12 | 自定义token类 13 | """ 14 | 15 | def __init__(self): 16 | self.secret = SECRET_KEY 17 | self.expire_hours = 1 # token 过期时间(小时) 18 | self.blacklist_prefix = "token_blacklist_" 19 | 20 | def create_token(self, user_id: int, username: str) -> str: 21 | """ 22 | 生成token 23 | :param user_id: 用户id 24 | :param username: 用户名 25 | :return: token字符串 26 | """ 27 | user = User.objects.get(id=user_id) 28 | payload = { 29 | "user_id": user_id, 30 | "username": username, 31 | "permissions": [perm.codename for perm in user.user_permissions.all()], 32 | "exp": datetime.utcnow() + timedelta(hours=self.expire_hours), 33 | "iat": datetime.utcnow() 34 | } 35 | 36 | token = jwt.encode(payload, self.secret, algorithm="HS256") 37 | return token 38 | 39 | def check_token(self, token: str) -> bool: 40 | """ 41 | 验证token 42 | :param token: token字符串 43 | :return: bool 44 | """ 45 | try: 46 | # 先检查是否在黑名单中 47 | if self.is_blacklisted(token): 48 | return False 49 | 50 | jwt.decode(token, self.secret, algorithms=["HS256"]) 51 | return True 52 | except jwt.ExpiredSignatureError: 53 | return False 54 | except jwt.InvalidTokenError: 55 | return False 56 | 57 | def get_token_info(self, token: str) -> Optional[Dict]: 58 | """ 59 | 获取token中的信息 60 | :param token: token字符串 61 | :return: dict/None 62 | """ 63 | try: 64 | payload = jwt.decode(token, self.secret, algorithms=["HS256"]) 65 | return payload 66 | except: 67 | return None 68 | 69 | def invalidate_token(self, token: str) -> None: 70 | """ 71 | 使token失效(加入黑名单) 72 | :param token: token字符串 73 | """ 74 | try: 75 | # 获取token的过期时间 76 | payload = jwt.decode(token, self.secret, algorithms=["HS256"]) 77 | exp = datetime.fromtimestamp(payload['exp']) 78 | now = datetime.utcnow() 79 | 80 | # 计算剩余有效时间(秒) 81 | ttl = int((exp - now).total_seconds()) 82 | 83 | if ttl > 0: 84 | # 将token加入黑名单,过期时间与token原过期时间一致 85 | cache.set(f"{self.blacklist_prefix}{token}", True, ttl) 86 | except jwt.InvalidTokenError: 87 | pass 88 | 89 | def is_blacklisted(self, token: str) -> bool: 90 | """ 91 | 检查token是否在黑名单中 92 | :param token: token字符串 93 | :return: bool 94 | """ 95 | return cache.get(f"{self.blacklist_prefix}{token}", False) 96 | -------------------------------------------------------------------------------- /backend/backend/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeldomQA/seldom-platform/2ed82ef3abaebca8fc19c1e2105bbdae04d369cc/backend/backend/__init__.py -------------------------------------------------------------------------------- /backend/backend/api.py: -------------------------------------------------------------------------------- 1 | """ 2 | author: @bugmaster 3 | data: 2020-06-11 4 | function: api接口 5 | """ 6 | from django.contrib.auth.models import User 7 | from ninja import NinjaAPI 8 | from ninja.errors import HttpError 9 | from ninja.security import HttpBearer 10 | 11 | from app_case.api import router as case_router 12 | from app_project.api import router as project_router 13 | from app_task.api import router as task_router 14 | from app_team.api import router as team_router 15 | from app_user.api import router as user_router 16 | from app_utils.response import ErrorCode 17 | from app_utils.response import resp_error_dict 18 | from app_utils.token import CustomToken 19 | 20 | 21 | class InvalidToken(Exception): 22 | """无效token""" 23 | pass 24 | 25 | 26 | class GlobalAuth(HttpBearer): 27 | 28 | def authenticate(self, request, token): 29 | """ 30 | 验证token 31 | :param request: 请求对象 32 | :param token: token字符串 33 | :return: token/None 34 | """ 35 | token_method = CustomToken() 36 | is_token = token_method.check_token(token) 37 | if is_token is False: 38 | raise InvalidToken 39 | 40 | # 从token中获取用户信息并添加到request中 41 | try: 42 | user_info = token_method.get_token_info(token) 43 | user_id = user_info.get("user_id") 44 | user = User.objects.get(id=user_id) 45 | request.user = user 46 | return token 47 | except Exception: 48 | raise InvalidToken 49 | 50 | 51 | # 启用全局认证 52 | api = NinjaAPI(auth=GlobalAuth()) 53 | 54 | 55 | # 自定义异常处理 56 | @api.exception_handler(InvalidToken) 57 | def on_invalid_token(request, exc): 58 | """无效token返回类型 """ 59 | return api.create_response( 60 | request, 61 | resp_error_dict(ErrorCode.TOKEN_INVALID), 62 | status=401 63 | ) 64 | 65 | 66 | @api.exception_handler(HttpError) 67 | def on_http_error(request, exc): 68 | """处理HTTP错误""" 69 | error = ErrorCode.SYSTEM_ERROR 70 | try: 71 | http_status = exc.args[0] 72 | if http_status == 200: 73 | error = exc.args[1] 74 | except BaseException: 75 | pass 76 | 77 | return api.create_response( 78 | request, 79 | resp_error_dict(error), 80 | status=200 81 | ) 82 | 83 | 84 | @api.get('/ping', auth=None) 85 | def api_check(request): 86 | """ 87 | 健康检查接口 - 不需要认证 88 | """ 89 | return {"result": "ok"} 90 | 91 | 92 | api.add_router("/project/", project_router) 93 | api.add_router("/case/", case_router) 94 | api.add_router("/task/", task_router) 95 | api.add_router("/user/", user_router) 96 | api.add_router("/team/", team_router) 97 | -------------------------------------------------------------------------------- /backend/backend/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for backend project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /backend/backend/config.py: -------------------------------------------------------------------------------- 1 | """ 2 | 配置文件 3 | """ 4 | # 定时任务服务 5 | TIMED_SERVER = "http://127.0.0.1:8004" 6 | # 本机服务 7 | THIS_SERVER = "http://127.0.0.1:8003" 8 | 9 | 10 | class EmailConfig: 11 | """ 12 | 测试任务:发送邮箱账号配置 13 | """ 14 | user = "xxxx@gmail.com" 15 | password = "abc123" 16 | host = "smtp.gmail.com" 17 | -------------------------------------------------------------------------------- /backend/backend/urls.py: -------------------------------------------------------------------------------- 1 | """backend URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/4.0/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import path 18 | from backend.api import api 19 | 20 | urlpatterns = [ 21 | path('admin/', admin.site.urls), 22 | path("api/", api.urls), 23 | ] 24 | -------------------------------------------------------------------------------- /backend/backend/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for backend project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /backend/dev.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeldomQA/seldom-platform/2ed82ef3abaebca8fc19c1e2105bbdae04d369cc/backend/dev.sqlite3 -------------------------------------------------------------------------------- /backend/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.11' 2 | 3 | services: 4 | backend: 5 | image: seldom-backend:latest 6 | ports: 7 | - "8005:8000" 8 | environment: 9 | - DATABASE_URL=your_db_url 10 | - REDIS_HOST=your_redis_host 11 | depends_on: 12 | - selenium 13 | 14 | selenium: 15 | image: selenium/standalone-chrome:latest 16 | ports: 17 | - "4444:4444" 18 | - "7900:7900" 19 | shm_size: '2gb' 20 | 21 | redis: 22 | image: redis:latest 23 | ports: 24 | - "6379:6379" 25 | volumes: 26 | - redis-data:/data 27 | 28 | volumes: 29 | redis-data: -------------------------------------------------------------------------------- /backend/docs/deploy.md: -------------------------------------------------------------------------------- 1 | # 更多部署细节 2 | 3 | ## Web环境部署 4 | 5 | Web环境需要用到`浏览器`和`浏览器驱动`,如果是使用的云服务器Linux,那么默认是没有的。这里推荐使用:docker-selenium 6 | 7 | docker-selenium: https://github.com/SeleniumHQ/docker-selenium 8 | 9 | ### 通过官方仓库安装 Docker(推荐方式) 10 | 11 | 在 Ubuntu 上安装 Docker 有多种方式,这里提供一种常见方式(__如果是你已经安装了docker,请跳过!__): 12 | 13 | * 安装必要依赖包: 14 | 15 | ```bash 16 | # 更新软件包索引 17 | sudo apt-get update 18 | # 安装必要的依赖包 19 | sudo apt-get install -y apt-transport-https ca-certificates curl software-properties-common 20 | ``` 21 | 22 | * 添加 Docker 的官方 GPG 密钥: 23 | 24 | ```bash 25 | # 默认 26 | curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - 27 | # 国内阿里云 28 | curl -fsSL http://mirrors.aliyun.com/docker-ce/linux/ubuntu/gpg | sudo apt-key add - 29 | ``` 30 | 31 | * 将 Docker 仓库添加到 APT 源: 32 | 33 | ```bash 34 | # 默认(可选) 35 | sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" 36 | # 国内阿里云(可选) 37 | sudo add-apt-repository "deb [arch=amd64] http://mirrors.aliyun.com/docker-ce/linux/ubuntu $(lsb_release -cs) stable" 38 | # 更新 39 | sudo apt-get update 40 | ``` 41 | 42 | * 安装 Docker,以及验证: 43 | 44 | ```bash 45 | # 安装 46 | sudo apt-get install -y docker-ce docker-ce-cli containerd.io 47 | # 查看状态 48 | sudo systemctl status docker 49 | ``` 50 | 51 | ### 安装docker-selenium 52 | 53 | 下面是基于阿里云安装`selenium`,申请的服务器比较垃圾,这里就启动了一个 chrome 容器。 54 | 55 | ```shell 56 | # 拉取镜像 57 | docker pull 9bt26at4.mirror.aliyuncs.com/selenium/standalone-chrome:latest 58 | # 启动容器 59 | docker run -d -p 4444:4444 -p 7900:7900 --shm-size="2g" 9bt26at4.mirror.aliyuncs.com/selenium/standalone-chrome 60 | 61 | # 查看容器 62 | docker ps 63 | CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS 64 | NAMES 65 | 8e5bb1825dcc 9bt26at4.mirror.aliyuncs.com/selenium/standalone-chrome "/opt/bin/entry_poin…" 4 minutes ago Up 4 minutes 0.0.0.0:4444->4444/tcp, :::4444->4444/tcp, 0.0.0.0:7900->7900/tcp, :::7900->7900/tcp, 5900/tcp distracted_lamarr 66 | 67 | # 查看你容器日志 68 | > docker logs -f 8e5bb1825dcc 69 | ``` 70 | 71 | 与这个容器对应的前端配置: 72 | 73 | ![](env-config.png) 74 | 75 | -------------------------------------------------------------------------------- /backend/docs/env-config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeldomQA/seldom-platform/2ed82ef3abaebca8fc19c1e2105bbdae04d369cc/backend/docs/env-config.png -------------------------------------------------------------------------------- /backend/logs/debug.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeldomQA/seldom-platform/2ed82ef3abaebca8fc19c1e2105bbdae04d369cc/backend/logs/debug.log -------------------------------------------------------------------------------- /backend/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | from django.core.management import execute_from_command_line 7 | 8 | def main(): 9 | """Run administrative tasks.""" 10 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings') 11 | try: 12 | from django.core.management import execute_from_command_line 13 | except ImportError as exc: 14 | raise ImportError( 15 | "Couldn't import Django. Are you sure it's installed and " 16 | "available on your PYTHONPATH environment variable? Did you " 17 | "forget to activate a virtual environment?" 18 | ) from exc 19 | print(sys.argv) 20 | execute_from_command_line(sys.argv) 21 | 22 | 23 | if __name__ == '__main__': 24 | main() 25 | -------------------------------------------------------------------------------- /backend/reports/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeldomQA/seldom-platform/2ed82ef3abaebca8fc19c1e2105bbdae04d369cc/backend/reports/.keep -------------------------------------------------------------------------------- /backend/reports/1716275493.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 200. 27 | 2024-05-21 15:11:34 | INFO | case.py | 👀 assertJSON -> {'headers': {'Host': 'httpbin.org', 'User-Agent': 'python-requests/2.26.0'}}. 28 | 2024-05-21 15:11:34 | WARNING | case.py | ['💡 Assert data has not key: args', '💡 Assert data has not key: origin', '💡 Assert data has not key: url'] 29 | ]]> 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /backend/requirements.txt: -------------------------------------------------------------------------------- 1 | django==4.2.11 2 | django-ninja==1.3.0 3 | django-cors-headers==3.13.0 4 | django-redis==5.2.0 5 | pyjwt==2.10.0 6 | seldom==3.11.0 -------------------------------------------------------------------------------- /backend/resource/github/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeldomQA/seldom-platform/2ed82ef3abaebca8fc19c1e2105bbdae04d369cc/backend/resource/github/.keep -------------------------------------------------------------------------------- /backend/static/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeldomQA/seldom-platform/2ed82ef3abaebca8fc19c1e2105bbdae04d369cc/backend/static/images/.keep -------------------------------------------------------------------------------- /backend/static/images/2d82cb919cf05116adf720f8f7437ac9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeldomQA/seldom-platform/2ed82ef3abaebca8fc19c1e2105bbdae04d369cc/backend/static/images/2d82cb919cf05116adf720f8f7437ac9.png -------------------------------------------------------------------------------- /backend/static/images/c8f816777c4a29ad3f797ab16aba2ea5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeldomQA/seldom-platform/2ed82ef3abaebca8fc19c1e2105bbdae04d369cc/backend/static/images/c8f816777c4a29ad3f797ab16aba2ea5.jpg -------------------------------------------------------------------------------- /backend/staticfiles/admin/css/dashboard.css: -------------------------------------------------------------------------------- 1 | /* DASHBOARD */ 2 | .dashboard td, .dashboard th { 3 | word-break: break-word; 4 | } 5 | 6 | .dashboard .module table th { 7 | width: 100%; 8 | } 9 | 10 | .dashboard .module table td { 11 | white-space: nowrap; 12 | } 13 | 14 | .dashboard .module table td a { 15 | display: block; 16 | padding-right: .6em; 17 | } 18 | 19 | /* RECENT ACTIONS MODULE */ 20 | 21 | .module ul.actionlist { 22 | margin-left: 0; 23 | } 24 | 25 | ul.actionlist li { 26 | list-style-type: none; 27 | overflow: hidden; 28 | text-overflow: ellipsis; 29 | } 30 | -------------------------------------------------------------------------------- /backend/staticfiles/admin/css/login.css: -------------------------------------------------------------------------------- 1 | /* LOGIN FORM */ 2 | 3 | .login { 4 | background: var(--darkened-bg); 5 | height: auto; 6 | } 7 | 8 | .login #header { 9 | height: auto; 10 | padding: 15px 16px; 11 | justify-content: center; 12 | } 13 | 14 | .login #header h1 { 15 | font-size: 1.125rem; 16 | margin: 0; 17 | } 18 | 19 | .login #header h1 a { 20 | color: var(--header-link-color); 21 | } 22 | 23 | .login #content { 24 | padding: 20px 20px 0; 25 | } 26 | 27 | .login #container { 28 | background: var(--body-bg); 29 | border: 1px solid var(--hairline-color); 30 | border-radius: 4px; 31 | overflow: hidden; 32 | width: 28em; 33 | min-width: 300px; 34 | margin: 100px auto; 35 | height: auto; 36 | } 37 | 38 | .login .form-row { 39 | padding: 4px 0; 40 | } 41 | 42 | .login .form-row label { 43 | display: block; 44 | line-height: 2em; 45 | } 46 | 47 | .login .form-row #id_username, .login .form-row #id_password { 48 | padding: 8px; 49 | width: 100%; 50 | box-sizing: border-box; 51 | } 52 | 53 | .login .submit-row { 54 | padding: 1em 0 0 0; 55 | margin: 0; 56 | text-align: center; 57 | } 58 | 59 | .login .password-reset-link { 60 | text-align: center; 61 | } 62 | -------------------------------------------------------------------------------- /backend/staticfiles/admin/css/nav_sidebar.css: -------------------------------------------------------------------------------- 1 | .sticky { 2 | position: sticky; 3 | top: 0; 4 | max-height: 100vh; 5 | } 6 | 7 | .toggle-nav-sidebar { 8 | z-index: 20; 9 | left: 0; 10 | display: flex; 11 | align-items: center; 12 | justify-content: center; 13 | flex: 0 0 23px; 14 | width: 23px; 15 | border: 0; 16 | border-right: 1px solid var(--hairline-color); 17 | background-color: var(--body-bg); 18 | cursor: pointer; 19 | font-size: 1.25rem; 20 | color: var(--link-fg); 21 | padding: 0; 22 | } 23 | 24 | [dir="rtl"] .toggle-nav-sidebar { 25 | border-left: 1px solid var(--hairline-color); 26 | border-right: 0; 27 | } 28 | 29 | .toggle-nav-sidebar:hover, 30 | .toggle-nav-sidebar:focus { 31 | background-color: var(--darkened-bg); 32 | } 33 | 34 | #nav-sidebar { 35 | z-index: 15; 36 | flex: 0 0 275px; 37 | left: -276px; 38 | margin-left: -276px; 39 | border-top: 1px solid transparent; 40 | border-right: 1px solid var(--hairline-color); 41 | background-color: var(--body-bg); 42 | overflow: auto; 43 | } 44 | 45 | [dir="rtl"] #nav-sidebar { 46 | border-left: 1px solid var(--hairline-color); 47 | border-right: 0; 48 | left: 0; 49 | margin-left: 0; 50 | right: -276px; 51 | margin-right: -276px; 52 | } 53 | 54 | .toggle-nav-sidebar::before { 55 | content: '\00BB'; 56 | } 57 | 58 | .main.shifted .toggle-nav-sidebar::before { 59 | content: '\00AB'; 60 | } 61 | 62 | .main > #nav-sidebar { 63 | visibility: hidden; 64 | } 65 | 66 | .main.shifted > #nav-sidebar { 67 | margin-left: 0; 68 | visibility: visible; 69 | } 70 | 71 | [dir="rtl"] .main.shifted > #nav-sidebar { 72 | margin-right: 0; 73 | } 74 | 75 | #nav-sidebar .module th { 76 | width: 100%; 77 | overflow-wrap: anywhere; 78 | } 79 | 80 | #nav-sidebar .module th, 81 | #nav-sidebar .module caption { 82 | padding-left: 16px; 83 | } 84 | 85 | #nav-sidebar .module td { 86 | white-space: nowrap; 87 | } 88 | 89 | [dir="rtl"] #nav-sidebar .module th, 90 | [dir="rtl"] #nav-sidebar .module caption { 91 | padding-left: 8px; 92 | padding-right: 16px; 93 | } 94 | 95 | #nav-sidebar .current-app .section:link, 96 | #nav-sidebar .current-app .section:visited { 97 | color: var(--header-color); 98 | font-weight: bold; 99 | } 100 | 101 | #nav-sidebar .current-model { 102 | background: var(--selected-row); 103 | } 104 | 105 | .main > #nav-sidebar + .content { 106 | max-width: calc(100% - 23px); 107 | } 108 | 109 | .main.shifted > #nav-sidebar + .content { 110 | max-width: calc(100% - 299px); 111 | } 112 | 113 | @media (max-width: 767px) { 114 | #nav-sidebar, #toggle-nav-sidebar { 115 | display: none; 116 | } 117 | 118 | .main > #nav-sidebar + .content, 119 | .main.shifted > #nav-sidebar + .content { 120 | max-width: 100%; 121 | } 122 | } 123 | 124 | #nav-filter { 125 | width: 100%; 126 | box-sizing: border-box; 127 | padding: 2px 5px; 128 | margin: 5px 0; 129 | border: 1px solid var(--border-color); 130 | background-color: var(--darkened-bg); 131 | color: var(--body-fg); 132 | } 133 | 134 | #nav-filter:focus { 135 | border-color: var(--body-quiet-color); 136 | } 137 | 138 | #nav-filter.no-results { 139 | background: var(--message-error-bg); 140 | } 141 | 142 | #nav-sidebar table { 143 | width: 100%; 144 | } 145 | -------------------------------------------------------------------------------- /backend/staticfiles/admin/css/responsive_rtl.css: -------------------------------------------------------------------------------- 1 | /* TABLETS */ 2 | 3 | @media (max-width: 1024px) { 4 | [dir="rtl"] .colMS { 5 | margin-right: 0; 6 | } 7 | 8 | [dir="rtl"] #user-tools { 9 | text-align: right; 10 | } 11 | 12 | [dir="rtl"] #changelist .actions label { 13 | padding-left: 10px; 14 | padding-right: 0; 15 | } 16 | 17 | [dir="rtl"] #changelist .actions select { 18 | margin-left: 0; 19 | margin-right: 15px; 20 | } 21 | 22 | [dir="rtl"] .change-list .filtered .results, 23 | [dir="rtl"] .change-list .filtered .paginator, 24 | [dir="rtl"] .filtered #toolbar, 25 | [dir="rtl"] .filtered div.xfull, 26 | [dir="rtl"] .filtered .actions, 27 | [dir="rtl"] #changelist-filter { 28 | margin-left: 0; 29 | } 30 | 31 | [dir="rtl"] .inline-group ul.tools a.add, 32 | [dir="rtl"] .inline-group div.add-row a, 33 | [dir="rtl"] .inline-group .tabular tr.add-row td a { 34 | padding: 8px 26px 8px 10px; 35 | background-position: calc(100% - 8px) 9px; 36 | } 37 | 38 | [dir="rtl"] .related-widget-wrapper-link + .selector { 39 | margin-right: 0; 40 | margin-left: 15px; 41 | } 42 | 43 | [dir="rtl"] .selector .selector-filter label { 44 | margin-right: 0; 45 | margin-left: 8px; 46 | } 47 | 48 | [dir="rtl"] .object-tools li { 49 | float: right; 50 | } 51 | 52 | [dir="rtl"] .object-tools li + li { 53 | margin-left: 0; 54 | margin-right: 15px; 55 | } 56 | 57 | [dir="rtl"] .dashboard .module table td a { 58 | padding-left: 0; 59 | padding-right: 16px; 60 | } 61 | } 62 | 63 | /* MOBILE */ 64 | 65 | @media (max-width: 767px) { 66 | [dir="rtl"] .aligned .related-lookup, 67 | [dir="rtl"] .aligned .datetimeshortcuts { 68 | margin-left: 0; 69 | margin-right: 15px; 70 | } 71 | 72 | [dir="rtl"] .aligned ul, 73 | [dir="rtl"] form .aligned ul.errorlist { 74 | margin-right: 0; 75 | } 76 | 77 | [dir="rtl"] #changelist-filter { 78 | margin-left: 0; 79 | margin-right: 0; 80 | } 81 | [dir="rtl"] .aligned .vCheckboxLabel { 82 | padding: 1px 5px 0 0; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /backend/staticfiles/admin/css/vendor/select2/LICENSE-SELECT2.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2012-2017 Kevin Brown, Igor Vaynberg, and Select2 contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /backend/staticfiles/admin/img/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Code Charm Ltd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /backend/staticfiles/admin/img/README.txt: -------------------------------------------------------------------------------- 1 | All icons are taken from Font Awesome (http://fontawesome.io/) project. 2 | The Font Awesome font is licensed under the SIL OFL 1.1: 3 | - https://scripts.sil.org/OFL 4 | 5 | SVG icons source: https://github.com/encharm/Font-Awesome-SVG-PNG 6 | Font-Awesome-SVG-PNG is licensed under the MIT license (see file license 7 | in current folder). 8 | -------------------------------------------------------------------------------- /backend/staticfiles/admin/img/calendar-icons.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /backend/staticfiles/admin/img/gis/move_vertex_off.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/staticfiles/admin/img/gis/move_vertex_on.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/staticfiles/admin/img/icon-addlink.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /backend/staticfiles/admin/img/icon-alert.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /backend/staticfiles/admin/img/icon-calendar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /backend/staticfiles/admin/img/icon-changelink.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /backend/staticfiles/admin/img/icon-clock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /backend/staticfiles/admin/img/icon-deletelink.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /backend/staticfiles/admin/img/icon-no.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /backend/staticfiles/admin/img/icon-unknown-alt.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /backend/staticfiles/admin/img/icon-unknown.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /backend/staticfiles/admin/img/icon-viewlink.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /backend/staticfiles/admin/img/icon-yes.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /backend/staticfiles/admin/img/inline-delete.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /backend/staticfiles/admin/img/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /backend/staticfiles/admin/img/selector-icons.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /backend/staticfiles/admin/img/sorting-icons.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /backend/staticfiles/admin/img/tooltag-add.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /backend/staticfiles/admin/img/tooltag-arrowright.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/autocomplete.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | { 3 | const $ = django.jQuery; 4 | 5 | $.fn.djangoAdminSelect2 = function() { 6 | $.each(this, function(i, element) { 7 | $(element).select2({ 8 | ajax: { 9 | data: (params) => { 10 | return { 11 | term: params.term, 12 | page: params.page, 13 | app_label: element.dataset.appLabel, 14 | model_name: element.dataset.modelName, 15 | field_name: element.dataset.fieldName 16 | }; 17 | } 18 | } 19 | }); 20 | }); 21 | return this; 22 | }; 23 | 24 | $(function() { 25 | // Initialize all autocomplete widgets except the one in the template 26 | // form used when a new formset is added. 27 | $('.admin-autocomplete').not('[name*=__prefix__]').djangoAdminSelect2(); 28 | }); 29 | 30 | document.addEventListener('formset:added', (event) => { 31 | $(event.target).find('.admin-autocomplete').djangoAdminSelect2(); 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/cancel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | { 3 | // Call function fn when the DOM is loaded and ready. If it is already 4 | // loaded, call the function now. 5 | // http://youmightnotneedjquery.com/#ready 6 | function ready(fn) { 7 | if (document.readyState !== 'loading') { 8 | fn(); 9 | } else { 10 | document.addEventListener('DOMContentLoaded', fn); 11 | } 12 | } 13 | 14 | ready(function() { 15 | function handleClick(event) { 16 | event.preventDefault(); 17 | const params = new URLSearchParams(window.location.search); 18 | if (params.has('_popup')) { 19 | window.close(); // Close the popup. 20 | } else { 21 | window.history.back(); // Otherwise, go back. 22 | } 23 | } 24 | 25 | document.querySelectorAll('.cancel-link').forEach(function(el) { 26 | el.addEventListener('click', handleClick); 27 | }); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/change_form.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | { 3 | const inputTags = ['BUTTON', 'INPUT', 'SELECT', 'TEXTAREA']; 4 | const modelName = document.getElementById('django-admin-form-add-constants').dataset.modelName; 5 | if (modelName) { 6 | const form = document.getElementById(modelName + '_form'); 7 | for (const element of form.elements) { 8 | // HTMLElement.offsetParent returns null when the element is not 9 | // rendered. 10 | if (inputTags.includes(element.tagName) && !element.disabled && element.offsetParent) { 11 | element.focus(); 12 | break; 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/collapse.js: -------------------------------------------------------------------------------- 1 | /*global gettext*/ 2 | 'use strict'; 3 | { 4 | window.addEventListener('load', function() { 5 | // Add anchor tag for Show/Hide link 6 | const fieldsets = document.querySelectorAll('fieldset.collapse'); 7 | for (const [i, elem] of fieldsets.entries()) { 8 | // Don't hide if fields in this fieldset have errors 9 | if (elem.querySelectorAll('div.errors, ul.errorlist').length === 0) { 10 | elem.classList.add('collapsed'); 11 | const h2 = elem.querySelector('h2'); 12 | const link = document.createElement('a'); 13 | link.id = 'fieldsetcollapser' + i; 14 | link.className = 'collapse-toggle'; 15 | link.href = '#'; 16 | link.textContent = gettext('Show'); 17 | h2.appendChild(document.createTextNode(' (')); 18 | h2.appendChild(link); 19 | h2.appendChild(document.createTextNode(')')); 20 | } 21 | } 22 | // Add toggle to hide/show anchor tag 23 | const toggleFunc = function(ev) { 24 | if (ev.target.matches('.collapse-toggle')) { 25 | ev.preventDefault(); 26 | ev.stopPropagation(); 27 | const fieldset = ev.target.closest('fieldset'); 28 | if (fieldset.classList.contains('collapsed')) { 29 | // Show 30 | ev.target.textContent = gettext('Hide'); 31 | fieldset.classList.remove('collapsed'); 32 | } else { 33 | // Hide 34 | ev.target.textContent = gettext('Show'); 35 | fieldset.classList.add('collapsed'); 36 | } 37 | } 38 | }; 39 | document.querySelectorAll('fieldset.module').forEach(function(el) { 40 | el.addEventListener('click', toggleFunc); 41 | }); 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/filters.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Persist changelist filters state (collapsed/expanded). 3 | */ 4 | 'use strict'; 5 | { 6 | // Init filters. 7 | let filters = JSON.parse(sessionStorage.getItem('django.admin.filtersState')); 8 | 9 | if (!filters) { 10 | filters = {}; 11 | } 12 | 13 | Object.entries(filters).forEach(([key, value]) => { 14 | const detailElement = document.querySelector(`[data-filter-title='${CSS.escape(key)}']`); 15 | 16 | // Check if the filter is present, it could be from other view. 17 | if (detailElement) { 18 | value ? detailElement.setAttribute('open', '') : detailElement.removeAttribute('open'); 19 | } 20 | }); 21 | 22 | // Save filter state when clicks. 23 | const details = document.querySelectorAll('details'); 24 | details.forEach(detail => { 25 | detail.addEventListener('toggle', event => { 26 | filters[`${event.target.dataset.filterTitle}`] = detail.open; 27 | sessionStorage.setItem('django.admin.filtersState', JSON.stringify(filters)); 28 | }); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/jquery.init.js: -------------------------------------------------------------------------------- 1 | /*global jQuery:false*/ 2 | 'use strict'; 3 | /* Puts the included jQuery into our own namespace using noConflict and passing 4 | * it 'true'. This ensures that the included jQuery doesn't pollute the global 5 | * namespace (i.e. this preserves pre-existing values for both window.$ and 6 | * window.jQuery). 7 | */ 8 | window.django = {jQuery: jQuery.noConflict(true)}; 9 | -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/nav_sidebar.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | { 3 | const toggleNavSidebar = document.getElementById('toggle-nav-sidebar'); 4 | if (toggleNavSidebar !== null) { 5 | const navSidebar = document.getElementById('nav-sidebar'); 6 | const main = document.getElementById('main'); 7 | let navSidebarIsOpen = localStorage.getItem('django.admin.navSidebarIsOpen'); 8 | if (navSidebarIsOpen === null) { 9 | navSidebarIsOpen = 'true'; 10 | } 11 | main.classList.toggle('shifted', navSidebarIsOpen === 'true'); 12 | navSidebar.setAttribute('aria-expanded', navSidebarIsOpen); 13 | 14 | toggleNavSidebar.addEventListener('click', function() { 15 | if (navSidebarIsOpen === 'true') { 16 | navSidebarIsOpen = 'false'; 17 | } else { 18 | navSidebarIsOpen = 'true'; 19 | } 20 | localStorage.setItem('django.admin.navSidebarIsOpen', navSidebarIsOpen); 21 | main.classList.toggle('shifted'); 22 | navSidebar.setAttribute('aria-expanded', navSidebarIsOpen); 23 | }); 24 | } 25 | 26 | function initSidebarQuickFilter() { 27 | const options = []; 28 | const navSidebar = document.getElementById('nav-sidebar'); 29 | if (!navSidebar) { 30 | return; 31 | } 32 | navSidebar.querySelectorAll('th[scope=row] a').forEach((container) => { 33 | options.push({title: container.innerHTML, node: container}); 34 | }); 35 | 36 | function checkValue(event) { 37 | let filterValue = event.target.value; 38 | if (filterValue) { 39 | filterValue = filterValue.toLowerCase(); 40 | } 41 | if (event.key === 'Escape') { 42 | filterValue = ''; 43 | event.target.value = ''; // clear input 44 | } 45 | let matches = false; 46 | for (const o of options) { 47 | let displayValue = ''; 48 | if (filterValue) { 49 | if (o.title.toLowerCase().indexOf(filterValue) === -1) { 50 | displayValue = 'none'; 51 | } else { 52 | matches = true; 53 | } 54 | } 55 | // show/hide parent 56 | o.node.parentNode.parentNode.style.display = displayValue; 57 | } 58 | if (!filterValue || matches) { 59 | event.target.classList.remove('no-results'); 60 | } else { 61 | event.target.classList.add('no-results'); 62 | } 63 | sessionStorage.setItem('django.admin.navSidebarFilterValue', filterValue); 64 | } 65 | 66 | const nav = document.getElementById('nav-filter'); 67 | nav.addEventListener('change', checkValue, false); 68 | nav.addEventListener('input', checkValue, false); 69 | nav.addEventListener('keyup', checkValue, false); 70 | 71 | const storedValue = sessionStorage.getItem('django.admin.navSidebarFilterValue'); 72 | if (storedValue) { 73 | nav.value = storedValue; 74 | checkValue({target: nav, key: ''}); 75 | } 76 | } 77 | window.initSidebarQuickFilter = initSidebarQuickFilter; 78 | initSidebarQuickFilter(); 79 | } 80 | -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/popup_response.js: -------------------------------------------------------------------------------- 1 | /*global opener */ 2 | 'use strict'; 3 | { 4 | const initData = JSON.parse(document.getElementById('django-admin-popup-response-constants').dataset.popupResponse); 5 | switch(initData.action) { 6 | case 'change': 7 | opener.dismissChangeRelatedObjectPopup(window, initData.value, initData.obj, initData.new_value); 8 | break; 9 | case 'delete': 10 | opener.dismissDeleteRelatedObjectPopup(window, initData.value); 11 | break; 12 | default: 13 | opener.dismissAddRelatedObjectPopup(window, initData.value, initData.obj); 14 | break; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/prepopulate.js: -------------------------------------------------------------------------------- 1 | /*global URLify*/ 2 | 'use strict'; 3 | { 4 | const $ = django.jQuery; 5 | $.fn.prepopulate = function(dependencies, maxLength, allowUnicode) { 6 | /* 7 | Depends on urlify.js 8 | Populates a selected field with the values of the dependent fields, 9 | URLifies and shortens the string. 10 | dependencies - array of dependent fields ids 11 | maxLength - maximum length of the URLify'd string 12 | allowUnicode - Unicode support of the URLify'd string 13 | */ 14 | return this.each(function() { 15 | const prepopulatedField = $(this); 16 | 17 | const populate = function() { 18 | // Bail if the field's value has been changed by the user 19 | if (prepopulatedField.data('_changed')) { 20 | return; 21 | } 22 | 23 | const values = []; 24 | $.each(dependencies, function(i, field) { 25 | field = $(field); 26 | if (field.val().length > 0) { 27 | values.push(field.val()); 28 | } 29 | }); 30 | prepopulatedField.val(URLify(values.join(' '), maxLength, allowUnicode)); 31 | }; 32 | 33 | prepopulatedField.data('_changed', false); 34 | prepopulatedField.on('change', function() { 35 | prepopulatedField.data('_changed', true); 36 | }); 37 | 38 | if (!prepopulatedField.val()) { 39 | $(dependencies.join(',')).on('keyup change focus', populate); 40 | } 41 | }); 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/prepopulate_init.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | { 3 | const $ = django.jQuery; 4 | const fields = $('#django-admin-prepopulated-fields-constants').data('prepopulatedFields'); 5 | $.each(fields, function(index, field) { 6 | $( 7 | '.empty-form .form-row .field-' + field.name + 8 | ', .empty-form.form-row .field-' + field.name + 9 | ', .empty-form .form-row.field-' + field.name 10 | ).addClass('prepopulated_field'); 11 | $(field.id).data('dependency_list', field.dependency_list).prepopulate( 12 | field.dependency_ids, field.maxLength, field.allowUnicode 13 | ); 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/theme.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | { 3 | window.addEventListener('load', function(e) { 4 | 5 | function setTheme(mode) { 6 | if (mode !== "light" && mode !== "dark" && mode !== "auto") { 7 | console.error(`Got invalid theme mode: ${mode}. Resetting to auto.`); 8 | mode = "auto"; 9 | } 10 | document.documentElement.dataset.theme = mode; 11 | localStorage.setItem("theme", mode); 12 | } 13 | 14 | function cycleTheme() { 15 | const currentTheme = localStorage.getItem("theme") || "auto"; 16 | const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches; 17 | 18 | if (prefersDark) { 19 | // Auto (dark) -> Light -> Dark 20 | if (currentTheme === "auto") { 21 | setTheme("light"); 22 | } else if (currentTheme === "light") { 23 | setTheme("dark"); 24 | } else { 25 | setTheme("auto"); 26 | } 27 | } else { 28 | // Auto (light) -> Dark -> Light 29 | if (currentTheme === "auto") { 30 | setTheme("dark"); 31 | } else if (currentTheme === "dark") { 32 | setTheme("light"); 33 | } else { 34 | setTheme("auto"); 35 | } 36 | } 37 | } 38 | 39 | function initTheme() { 40 | // set theme defined in localStorage if there is one, or fallback to auto mode 41 | const currentTheme = localStorage.getItem("theme"); 42 | currentTheme ? setTheme(currentTheme) : setTheme("auto"); 43 | } 44 | 45 | function setupTheme() { 46 | // Attach event handlers for toggling themes 47 | const buttons = document.getElementsByClassName("theme-toggle"); 48 | Array.from(buttons).forEach((btn) => { 49 | btn.addEventListener("click", cycleTheme); 50 | }); 51 | initTheme(); 52 | } 53 | 54 | setupTheme(); 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/jquery/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright OpenJS Foundation and other contributors, https://openjsf.org/ 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2012-2017 Kevin Brown, Igor Vaynberg, and Select2 contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/af.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/af",[],function(){return{errorLoading:function(){return"Die resultate kon nie gelaai word nie."},inputTooLong:function(e){var n=e.input.length-e.maximum,r="Verwyders asseblief "+n+" character";return 1!=n&&(r+="s"),r},inputTooShort:function(e){return"Voer asseblief "+(e.minimum-e.input.length)+" of meer karakters"},loadingMore:function(){return"Meer resultate word gelaai…"},maximumSelected:function(e){var n="Kies asseblief net "+e.maximum+" item";return 1!=e.maximum&&(n+="s"),n},noResults:function(){return"Geen resultate gevind"},searching:function(){return"Besig…"},removeAllItems:function(){return"Verwyder alle items"}}}),e.define,e.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/ar.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/ar",[],function(){return{errorLoading:function(){return"لا يمكن تحميل النتائج"},inputTooLong:function(n){return"الرجاء حذف "+(n.input.length-n.maximum)+" عناصر"},inputTooShort:function(n){return"الرجاء إضافة "+(n.minimum-n.input.length)+" عناصر"},loadingMore:function(){return"جاري تحميل نتائج إضافية..."},maximumSelected:function(n){return"تستطيع إختيار "+n.maximum+" بنود فقط"},noResults:function(){return"لم يتم العثور على أي نتائج"},searching:function(){return"جاري البحث…"},removeAllItems:function(){return"قم بإزالة كل العناصر"}}}),n.define,n.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/az.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/az",[],function(){return{inputTooLong:function(n){return n.input.length-n.maximum+" simvol silin"},inputTooShort:function(n){return n.minimum-n.input.length+" simvol daxil edin"},loadingMore:function(){return"Daha çox nəticə yüklənir…"},maximumSelected:function(n){return"Sadəcə "+n.maximum+" element seçə bilərsiniz"},noResults:function(){return"Nəticə tapılmadı"},searching:function(){return"Axtarılır…"},removeAllItems:function(){return"Bütün elementləri sil"}}}),n.define,n.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/bg.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/bg",[],function(){return{inputTooLong:function(n){var e=n.input.length-n.maximum,u="Моля въведете с "+e+" по-малко символ";return e>1&&(u+="a"),u},inputTooShort:function(n){var e=n.minimum-n.input.length,u="Моля въведете още "+e+" символ";return e>1&&(u+="a"),u},loadingMore:function(){return"Зареждат се още…"},maximumSelected:function(n){var e="Можете да направите до "+n.maximum+" ";return n.maximum>1?e+="избора":e+="избор",e},noResults:function(){return"Няма намерени съвпадения"},searching:function(){return"Търсене…"},removeAllItems:function(){return"Премахнете всички елементи"}}}),n.define,n.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/bn.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/bn",[],function(){return{errorLoading:function(){return"ফলাফলগুলি লোড করা যায়নি।"},inputTooLong:function(n){var e=n.input.length-n.maximum,u="অনুগ্রহ করে "+e+" টি অক্ষর মুছে দিন।";return 1!=e&&(u="অনুগ্রহ করে "+e+" টি অক্ষর মুছে দিন।"),u},inputTooShort:function(n){return n.minimum-n.input.length+" টি অক্ষর অথবা অধিক অক্ষর লিখুন।"},loadingMore:function(){return"আরো ফলাফল লোড হচ্ছে ..."},maximumSelected:function(n){var e=n.maximum+" টি আইটেম নির্বাচন করতে পারবেন।";return 1!=n.maximum&&(e=n.maximum+" টি আইটেম নির্বাচন করতে পারবেন।"),e},noResults:function(){return"কোন ফলাফল পাওয়া যায়নি।"},searching:function(){return"অনুসন্ধান করা হচ্ছে ..."}}}),n.define,n.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/bs.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/bs",[],function(){function e(e,n,r,t){return e%10==1&&e%100!=11?n:e%10>=2&&e%10<=4&&(e%100<12||e%100>14)?r:t}return{errorLoading:function(){return"Preuzimanje nije uspijelo."},inputTooLong:function(n){var r=n.input.length-n.maximum,t="Obrišite "+r+" simbol";return t+=e(r,"","a","a")},inputTooShort:function(n){var r=n.minimum-n.input.length,t="Ukucajte bar još "+r+" simbol";return t+=e(r,"","a","a")},loadingMore:function(){return"Preuzimanje još rezultata…"},maximumSelected:function(n){var r="Možete izabrati samo "+n.maximum+" stavk";return r+=e(n.maximum,"u","e","i")},noResults:function(){return"Ništa nije pronađeno"},searching:function(){return"Pretraga…"},removeAllItems:function(){return"Uklonite sve stavke"}}}),e.define,e.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/ca.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/ca",[],function(){return{errorLoading:function(){return"La càrrega ha fallat"},inputTooLong:function(e){var n=e.input.length-e.maximum,r="Si us plau, elimina "+n+" car";return r+=1==n?"àcter":"àcters"},inputTooShort:function(e){var n=e.minimum-e.input.length,r="Si us plau, introdueix "+n+" car";return r+=1==n?"àcter":"àcters"},loadingMore:function(){return"Carregant més resultats…"},maximumSelected:function(e){var n="Només es pot seleccionar "+e.maximum+" element";return 1!=e.maximum&&(n+="s"),n},noResults:function(){return"No s'han trobat resultats"},searching:function(){return"Cercant…"},removeAllItems:function(){return"Treu tots els elements"}}}),e.define,e.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/cs.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/cs",[],function(){function e(e,n){switch(e){case 2:return n?"dva":"dvě";case 3:return"tři";case 4:return"čtyři"}return""}return{errorLoading:function(){return"Výsledky nemohly být načteny."},inputTooLong:function(n){var t=n.input.length-n.maximum;return 1==t?"Prosím, zadejte o jeden znak méně.":t<=4?"Prosím, zadejte o "+e(t,!0)+" znaky méně.":"Prosím, zadejte o "+t+" znaků méně."},inputTooShort:function(n){var t=n.minimum-n.input.length;return 1==t?"Prosím, zadejte ještě jeden znak.":t<=4?"Prosím, zadejte ještě další "+e(t,!0)+" znaky.":"Prosím, zadejte ještě dalších "+t+" znaků."},loadingMore:function(){return"Načítají se další výsledky…"},maximumSelected:function(n){var t=n.maximum;return 1==t?"Můžete zvolit jen jednu položku.":t<=4?"Můžete zvolit maximálně "+e(t,!1)+" položky.":"Můžete zvolit maximálně "+t+" položek."},noResults:function(){return"Nenalezeny žádné položky."},searching:function(){return"Vyhledávání…"},removeAllItems:function(){return"Odstraňte všechny položky"}}}),e.define,e.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/da.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/da",[],function(){return{errorLoading:function(){return"Resultaterne kunne ikke indlæses."},inputTooLong:function(e){return"Angiv venligst "+(e.input.length-e.maximum)+" tegn mindre"},inputTooShort:function(e){return"Angiv venligst "+(e.minimum-e.input.length)+" tegn mere"},loadingMore:function(){return"Indlæser flere resultater…"},maximumSelected:function(e){var n="Du kan kun vælge "+e.maximum+" emne";return 1!=e.maximum&&(n+="r"),n},noResults:function(){return"Ingen resultater fundet"},searching:function(){return"Søger…"},removeAllItems:function(){return"Fjern alle elementer"}}}),e.define,e.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/de.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/de",[],function(){return{errorLoading:function(){return"Die Ergebnisse konnten nicht geladen werden."},inputTooLong:function(e){return"Bitte "+(e.input.length-e.maximum)+" Zeichen weniger eingeben"},inputTooShort:function(e){return"Bitte "+(e.minimum-e.input.length)+" Zeichen mehr eingeben"},loadingMore:function(){return"Lade mehr Ergebnisse…"},maximumSelected:function(e){var n="Sie können nur "+e.maximum+" Element";return 1!=e.maximum&&(n+="e"),n+=" auswählen"},noResults:function(){return"Keine Übereinstimmungen gefunden"},searching:function(){return"Suche…"},removeAllItems:function(){return"Entferne alle Elemente"}}}),e.define,e.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/dsb.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/dsb",[],function(){var n=["znamuško","znamušce","znamuška","znamuškow"],e=["zapisk","zapiska","zapiski","zapiskow"],u=function(n,e){return 1===n?e[0]:2===n?e[1]:n>2&&n<=4?e[2]:n>=5?e[3]:void 0};return{errorLoading:function(){return"Wuslědki njejsu se dali zacytaś."},inputTooLong:function(e){var a=e.input.length-e.maximum;return"Pšosym lašuj "+a+" "+u(a,n)},inputTooShort:function(e){var a=e.minimum-e.input.length;return"Pšosym zapódaj nanejmjenjej "+a+" "+u(a,n)},loadingMore:function(){return"Dalšne wuslědki se zacytaju…"},maximumSelected:function(n){return"Móžoš jano "+n.maximum+" "+u(n.maximum,e)+"wubraś."},noResults:function(){return"Žedne wuslědki namakane"},searching:function(){return"Pyta se…"},removeAllItems:function(){return"Remove all items"}}}),n.define,n.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/el.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/el",[],function(){return{errorLoading:function(){return"Τα αποτελέσματα δεν μπόρεσαν να φορτώσουν."},inputTooLong:function(n){var e=n.input.length-n.maximum,u="Παρακαλώ διαγράψτε "+e+" χαρακτήρ";return 1==e&&(u+="α"),1!=e&&(u+="ες"),u},inputTooShort:function(n){return"Παρακαλώ συμπληρώστε "+(n.minimum-n.input.length)+" ή περισσότερους χαρακτήρες"},loadingMore:function(){return"Φόρτωση περισσότερων αποτελεσμάτων…"},maximumSelected:function(n){var e="Μπορείτε να επιλέξετε μόνο "+n.maximum+" επιλογ";return 1==n.maximum&&(e+="ή"),1!=n.maximum&&(e+="ές"),e},noResults:function(){return"Δεν βρέθηκαν αποτελέσματα"},searching:function(){return"Αναζήτηση…"},removeAllItems:function(){return"Καταργήστε όλα τα στοιχεία"}}}),n.define,n.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/en.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/en",[],function(){return{errorLoading:function(){return"The results could not be loaded."},inputTooLong:function(e){var n=e.input.length-e.maximum,r="Please delete "+n+" character";return 1!=n&&(r+="s"),r},inputTooShort:function(e){return"Please enter "+(e.minimum-e.input.length)+" or more characters"},loadingMore:function(){return"Loading more results…"},maximumSelected:function(e){var n="You can only select "+e.maximum+" item";return 1!=e.maximum&&(n+="s"),n},noResults:function(){return"No results found"},searching:function(){return"Searching…"},removeAllItems:function(){return"Remove all items"}}}),e.define,e.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/es.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/es",[],function(){return{errorLoading:function(){return"No se pudieron cargar los resultados"},inputTooLong:function(e){var n=e.input.length-e.maximum,r="Por favor, elimine "+n+" car";return r+=1==n?"ácter":"acteres"},inputTooShort:function(e){var n=e.minimum-e.input.length,r="Por favor, introduzca "+n+" car";return r+=1==n?"ácter":"acteres"},loadingMore:function(){return"Cargando más resultados…"},maximumSelected:function(e){var n="Sólo puede seleccionar "+e.maximum+" elemento";return 1!=e.maximum&&(n+="s"),n},noResults:function(){return"No se encontraron resultados"},searching:function(){return"Buscando…"},removeAllItems:function(){return"Eliminar todos los elementos"}}}),e.define,e.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/et.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/et",[],function(){return{inputTooLong:function(e){var n=e.input.length-e.maximum,t="Sisesta "+n+" täht";return 1!=n&&(t+="e"),t+=" vähem"},inputTooShort:function(e){var n=e.minimum-e.input.length,t="Sisesta "+n+" täht";return 1!=n&&(t+="e"),t+=" rohkem"},loadingMore:function(){return"Laen tulemusi…"},maximumSelected:function(e){var n="Saad vaid "+e.maximum+" tulemus";return 1==e.maximum?n+="e":n+="t",n+=" valida"},noResults:function(){return"Tulemused puuduvad"},searching:function(){return"Otsin…"},removeAllItems:function(){return"Eemalda kõik esemed"}}}),e.define,e.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/eu.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/eu",[],function(){return{inputTooLong:function(e){var t=e.input.length-e.maximum,n="Idatzi ";return n+=1==t?"karaktere bat":t+" karaktere",n+=" gutxiago"},inputTooShort:function(e){var t=e.minimum-e.input.length,n="Idatzi ";return n+=1==t?"karaktere bat":t+" karaktere",n+=" gehiago"},loadingMore:function(){return"Emaitza gehiago kargatzen…"},maximumSelected:function(e){return 1===e.maximum?"Elementu bakarra hauta dezakezu":e.maximum+" elementu hauta ditzakezu soilik"},noResults:function(){return"Ez da bat datorrenik aurkitu"},searching:function(){return"Bilatzen…"},removeAllItems:function(){return"Kendu elementu guztiak"}}}),e.define,e.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/fa.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/fa",[],function(){return{errorLoading:function(){return"امکان بارگذاری نتایج وجود ندارد."},inputTooLong:function(n){return"لطفاً "+(n.input.length-n.maximum)+" کاراکتر را حذف نمایید"},inputTooShort:function(n){return"لطفاً تعداد "+(n.minimum-n.input.length)+" کاراکتر یا بیشتر وارد نمایید"},loadingMore:function(){return"در حال بارگذاری نتایج بیشتر..."},maximumSelected:function(n){return"شما تنها می‌توانید "+n.maximum+" آیتم را انتخاب نمایید"},noResults:function(){return"هیچ نتیجه‌ای یافت نشد"},searching:function(){return"در حال جستجو..."},removeAllItems:function(){return"همه موارد را حذف کنید"}}}),n.define,n.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/fi.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/fi",[],function(){return{errorLoading:function(){return"Tuloksia ei saatu ladattua."},inputTooLong:function(n){return"Ole hyvä ja anna "+(n.input.length-n.maximum)+" merkkiä vähemmän"},inputTooShort:function(n){return"Ole hyvä ja anna "+(n.minimum-n.input.length)+" merkkiä lisää"},loadingMore:function(){return"Ladataan lisää tuloksia…"},maximumSelected:function(n){return"Voit valita ainoastaan "+n.maximum+" kpl"},noResults:function(){return"Ei tuloksia"},searching:function(){return"Haetaan…"},removeAllItems:function(){return"Poista kaikki kohteet"}}}),n.define,n.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/fr.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/fr",[],function(){return{errorLoading:function(){return"Les résultats ne peuvent pas être chargés."},inputTooLong:function(e){var n=e.input.length-e.maximum;return"Supprimez "+n+" caractère"+(n>1?"s":"")},inputTooShort:function(e){var n=e.minimum-e.input.length;return"Saisissez au moins "+n+" caractère"+(n>1?"s":"")},loadingMore:function(){return"Chargement de résultats supplémentaires…"},maximumSelected:function(e){return"Vous pouvez seulement sélectionner "+e.maximum+" élément"+(e.maximum>1?"s":"")},noResults:function(){return"Aucun résultat trouvé"},searching:function(){return"Recherche en cours…"},removeAllItems:function(){return"Supprimer tous les éléments"}}}),e.define,e.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/gl.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/gl",[],function(){return{errorLoading:function(){return"Non foi posíbel cargar os resultados."},inputTooLong:function(e){var n=e.input.length-e.maximum;return 1===n?"Elimine un carácter":"Elimine "+n+" caracteres"},inputTooShort:function(e){var n=e.minimum-e.input.length;return 1===n?"Engada un carácter":"Engada "+n+" caracteres"},loadingMore:function(){return"Cargando máis resultados…"},maximumSelected:function(e){return 1===e.maximum?"Só pode seleccionar un elemento":"Só pode seleccionar "+e.maximum+" elementos"},noResults:function(){return"Non se atoparon resultados"},searching:function(){return"Buscando…"},removeAllItems:function(){return"Elimina todos os elementos"}}}),e.define,e.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/he.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/he",[],function(){return{errorLoading:function(){return"שגיאה בטעינת התוצאות"},inputTooLong:function(n){var e=n.input.length-n.maximum,r="נא למחוק ";return r+=1===e?"תו אחד":e+" תווים"},inputTooShort:function(n){var e=n.minimum-n.input.length,r="נא להכניס ";return r+=1===e?"תו אחד":e+" תווים",r+=" או יותר"},loadingMore:function(){return"טוען תוצאות נוספות…"},maximumSelected:function(n){var e="באפשרותך לבחור עד ";return 1===n.maximum?e+="פריט אחד":e+=n.maximum+" פריטים",e},noResults:function(){return"לא נמצאו תוצאות"},searching:function(){return"מחפש…"},removeAllItems:function(){return"הסר את כל הפריטים"}}}),n.define,n.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/hi.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/hi",[],function(){return{errorLoading:function(){return"परिणामों को लोड नहीं किया जा सका।"},inputTooLong:function(n){var e=n.input.length-n.maximum,r=e+" अक्षर को हटा दें";return e>1&&(r=e+" अक्षरों को हटा दें "),r},inputTooShort:function(n){return"कृपया "+(n.minimum-n.input.length)+" या अधिक अक्षर दर्ज करें"},loadingMore:function(){return"अधिक परिणाम लोड हो रहे है..."},maximumSelected:function(n){return"आप केवल "+n.maximum+" आइटम का चयन कर सकते हैं"},noResults:function(){return"कोई परिणाम नहीं मिला"},searching:function(){return"खोज रहा है..."},removeAllItems:function(){return"सभी वस्तुओं को हटा दें"}}}),n.define,n.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/hr.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/hr",[],function(){function n(n){var e=" "+n+" znak";return n%10<5&&n%10>0&&(n%100<5||n%100>19)?n%10>1&&(e+="a"):e+="ova",e}return{errorLoading:function(){return"Preuzimanje nije uspjelo."},inputTooLong:function(e){return"Unesite "+n(e.input.length-e.maximum)},inputTooShort:function(e){return"Unesite još "+n(e.minimum-e.input.length)},loadingMore:function(){return"Učitavanje rezultata…"},maximumSelected:function(n){return"Maksimalan broj odabranih stavki je "+n.maximum},noResults:function(){return"Nema rezultata"},searching:function(){return"Pretraga…"},removeAllItems:function(){return"Ukloni sve stavke"}}}),n.define,n.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/hsb.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/hsb",[],function(){var n=["znamješko","znamješce","znamješka","znamješkow"],e=["zapisk","zapiskaj","zapiski","zapiskow"],u=function(n,e){return 1===n?e[0]:2===n?e[1]:n>2&&n<=4?e[2]:n>=5?e[3]:void 0};return{errorLoading:function(){return"Wuslědki njedachu so začitać."},inputTooLong:function(e){var a=e.input.length-e.maximum;return"Prošu zhašej "+a+" "+u(a,n)},inputTooShort:function(e){var a=e.minimum-e.input.length;return"Prošu zapodaj znajmjeńša "+a+" "+u(a,n)},loadingMore:function(){return"Dalše wuslědki so začitaja…"},maximumSelected:function(n){return"Móžeš jenož "+n.maximum+" "+u(n.maximum,e)+"wubrać"},noResults:function(){return"Žane wuslědki namakane"},searching:function(){return"Pyta so…"},removeAllItems:function(){return"Remove all items"}}}),n.define,n.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/hu.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/hu",[],function(){return{errorLoading:function(){return"Az eredmények betöltése nem sikerült."},inputTooLong:function(e){return"Túl hosszú. "+(e.input.length-e.maximum)+" karakterrel több, mint kellene."},inputTooShort:function(e){return"Túl rövid. Még "+(e.minimum-e.input.length)+" karakter hiányzik."},loadingMore:function(){return"Töltés…"},maximumSelected:function(e){return"Csak "+e.maximum+" elemet lehet kiválasztani."},noResults:function(){return"Nincs találat."},searching:function(){return"Keresés…"},removeAllItems:function(){return"Távolítson el minden elemet"}}}),e.define,e.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/hy.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/hy",[],function(){return{errorLoading:function(){return"Արդյունքները հնարավոր չէ բեռնել։"},inputTooLong:function(n){return"Խնդրում ենք հեռացնել "+(n.input.length-n.maximum)+" նշան"},inputTooShort:function(n){return"Խնդրում ենք մուտքագրել "+(n.minimum-n.input.length)+" կամ ավել նշաններ"},loadingMore:function(){return"Բեռնվում են նոր արդյունքներ․․․"},maximumSelected:function(n){return"Դուք կարող եք ընտրել առավելագույնը "+n.maximum+" կետ"},noResults:function(){return"Արդյունքներ չեն գտնվել"},searching:function(){return"Որոնում․․․"},removeAllItems:function(){return"Հեռացնել բոլոր տարրերը"}}}),n.define,n.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/id.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/id",[],function(){return{errorLoading:function(){return"Data tidak boleh diambil."},inputTooLong:function(n){return"Hapuskan "+(n.input.length-n.maximum)+" huruf"},inputTooShort:function(n){return"Masukkan "+(n.minimum-n.input.length)+" huruf lagi"},loadingMore:function(){return"Mengambil data…"},maximumSelected:function(n){return"Anda hanya dapat memilih "+n.maximum+" pilihan"},noResults:function(){return"Tidak ada data yang sesuai"},searching:function(){return"Mencari…"},removeAllItems:function(){return"Hapus semua item"}}}),n.define,n.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/is.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/is",[],function(){return{inputTooLong:function(n){var t=n.input.length-n.maximum,e="Vinsamlegast styttið texta um "+t+" staf";return t<=1?e:e+"i"},inputTooShort:function(n){var t=n.minimum-n.input.length,e="Vinsamlegast skrifið "+t+" staf";return t>1&&(e+="i"),e+=" í viðbót"},loadingMore:function(){return"Sæki fleiri niðurstöður…"},maximumSelected:function(n){return"Þú getur aðeins valið "+n.maximum+" atriði"},noResults:function(){return"Ekkert fannst"},searching:function(){return"Leita…"},removeAllItems:function(){return"Fjarlægðu öll atriði"}}}),n.define,n.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/it.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/it",[],function(){return{errorLoading:function(){return"I risultati non possono essere caricati."},inputTooLong:function(e){var n=e.input.length-e.maximum,t="Per favore cancella "+n+" caratter";return t+=1!==n?"i":"e"},inputTooShort:function(e){return"Per favore inserisci "+(e.minimum-e.input.length)+" o più caratteri"},loadingMore:function(){return"Caricando più risultati…"},maximumSelected:function(e){var n="Puoi selezionare solo "+e.maximum+" element";return 1!==e.maximum?n+="i":n+="o",n},noResults:function(){return"Nessun risultato trovato"},searching:function(){return"Sto cercando…"},removeAllItems:function(){return"Rimuovi tutti gli oggetti"}}}),e.define,e.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/ja.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/ja",[],function(){return{errorLoading:function(){return"結果が読み込まれませんでした"},inputTooLong:function(n){return n.input.length-n.maximum+" 文字を削除してください"},inputTooShort:function(n){return"少なくとも "+(n.minimum-n.input.length)+" 文字を入力してください"},loadingMore:function(){return"読み込み中…"},maximumSelected:function(n){return n.maximum+" 件しか選択できません"},noResults:function(){return"対象が見つかりません"},searching:function(){return"検索しています…"},removeAllItems:function(){return"すべてのアイテムを削除"}}}),n.define,n.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/ka.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/ka",[],function(){return{errorLoading:function(){return"მონაცემების ჩატვირთვა შეუძლებელია."},inputTooLong:function(n){return"გთხოვთ აკრიფეთ "+(n.input.length-n.maximum)+" სიმბოლოთი ნაკლები"},inputTooShort:function(n){return"გთხოვთ აკრიფეთ "+(n.minimum-n.input.length)+" სიმბოლო ან მეტი"},loadingMore:function(){return"მონაცემების ჩატვირთვა…"},maximumSelected:function(n){return"თქვენ შეგიძლიათ აირჩიოთ არაუმეტეს "+n.maximum+" ელემენტი"},noResults:function(){return"რეზულტატი არ მოიძებნა"},searching:function(){return"ძიება…"},removeAllItems:function(){return"ამოიღე ყველა ელემენტი"}}}),n.define,n.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/km.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/km",[],function(){return{errorLoading:function(){return"មិនអាចទាញយកទិន្នន័យ"},inputTooLong:function(n){return"សូមលុបចេញ "+(n.input.length-n.maximum)+" អក្សរ"},inputTooShort:function(n){return"សូមបញ្ចូល"+(n.minimum-n.input.length)+" អក្សរ រឺ ច្រើនជាងនេះ"},loadingMore:function(){return"កំពុងទាញយកទិន្នន័យបន្ថែម..."},maximumSelected:function(n){return"អ្នកអាចជ្រើសរើសបានតែ "+n.maximum+" ជម្រើសប៉ុណ្ណោះ"},noResults:function(){return"មិនមានលទ្ធផល"},searching:function(){return"កំពុងស្វែងរក..."},removeAllItems:function(){return"លុបធាតុទាំងអស់"}}}),n.define,n.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/ko.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/ko",[],function(){return{errorLoading:function(){return"결과를 불러올 수 없습니다."},inputTooLong:function(n){return"너무 깁니다. "+(n.input.length-n.maximum)+" 글자 지워주세요."},inputTooShort:function(n){return"너무 짧습니다. "+(n.minimum-n.input.length)+" 글자 더 입력해주세요."},loadingMore:function(){return"불러오는 중…"},maximumSelected:function(n){return"최대 "+n.maximum+"개까지만 선택 가능합니다."},noResults:function(){return"결과가 없습니다."},searching:function(){return"검색 중…"},removeAllItems:function(){return"모든 항목 삭제"}}}),n.define,n.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/lt.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/lt",[],function(){function n(n,e,i,t){return n%10==1&&(n%100<11||n%100>19)?e:n%10>=2&&n%10<=9&&(n%100<11||n%100>19)?i:t}return{inputTooLong:function(e){var i=e.input.length-e.maximum,t="Pašalinkite "+i+" simbol";return t+=n(i,"į","ius","ių")},inputTooShort:function(e){var i=e.minimum-e.input.length,t="Įrašykite dar "+i+" simbol";return t+=n(i,"į","ius","ių")},loadingMore:function(){return"Kraunama daugiau rezultatų…"},maximumSelected:function(e){var i="Jūs galite pasirinkti tik "+e.maximum+" element";return i+=n(e.maximum,"ą","us","ų")},noResults:function(){return"Atitikmenų nerasta"},searching:function(){return"Ieškoma…"},removeAllItems:function(){return"Pašalinti visus elementus"}}}),n.define,n.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/lv.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/lv",[],function(){function e(e,n,u,i){return 11===e?n:e%10==1?u:i}return{inputTooLong:function(n){var u=n.input.length-n.maximum,i="Lūdzu ievadiet par "+u;return(i+=" simbol"+e(u,"iem","u","iem"))+" mazāk"},inputTooShort:function(n){var u=n.minimum-n.input.length,i="Lūdzu ievadiet vēl "+u;return i+=" simbol"+e(u,"us","u","us")},loadingMore:function(){return"Datu ielāde…"},maximumSelected:function(n){var u="Jūs varat izvēlēties ne vairāk kā "+n.maximum;return u+=" element"+e(n.maximum,"us","u","us")},noResults:function(){return"Sakritību nav"},searching:function(){return"Meklēšana…"},removeAllItems:function(){return"Noņemt visus vienumus"}}}),e.define,e.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/mk.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/mk",[],function(){return{inputTooLong:function(n){var e=(n.input.length,n.maximum,"Ве молиме внесете "+n.maximum+" помалку карактер");return 1!==n.maximum&&(e+="и"),e},inputTooShort:function(n){var e=(n.minimum,n.input.length,"Ве молиме внесете уште "+n.maximum+" карактер");return 1!==n.maximum&&(e+="и"),e},loadingMore:function(){return"Вчитување резултати…"},maximumSelected:function(n){var e="Можете да изберете само "+n.maximum+" ставк";return 1===n.maximum?e+="а":e+="и",e},noResults:function(){return"Нема пронајдено совпаѓања"},searching:function(){return"Пребарување…"},removeAllItems:function(){return"Отстрани ги сите предмети"}}}),n.define,n.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/ms.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/ms",[],function(){return{errorLoading:function(){return"Keputusan tidak berjaya dimuatkan."},inputTooLong:function(n){return"Sila hapuskan "+(n.input.length-n.maximum)+" aksara"},inputTooShort:function(n){return"Sila masukkan "+(n.minimum-n.input.length)+" atau lebih aksara"},loadingMore:function(){return"Sedang memuatkan keputusan…"},maximumSelected:function(n){return"Anda hanya boleh memilih "+n.maximum+" pilihan"},noResults:function(){return"Tiada padanan yang ditemui"},searching:function(){return"Mencari…"},removeAllItems:function(){return"Keluarkan semua item"}}}),n.define,n.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/nb.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/nb",[],function(){return{errorLoading:function(){return"Kunne ikke hente resultater."},inputTooLong:function(e){return"Vennligst fjern "+(e.input.length-e.maximum)+" tegn"},inputTooShort:function(e){return"Vennligst skriv inn "+(e.minimum-e.input.length)+" tegn til"},loadingMore:function(){return"Laster flere resultater…"},maximumSelected:function(e){return"Du kan velge maks "+e.maximum+" elementer"},noResults:function(){return"Ingen treff"},searching:function(){return"Søker…"},removeAllItems:function(){return"Fjern alle elementer"}}}),e.define,e.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/ne.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/ne",[],function(){return{errorLoading:function(){return"नतिजाहरु देखाउन सकिएन।"},inputTooLong:function(n){var e=n.input.length-n.maximum,u="कृपया "+e+" अक्षर मेटाउनुहोस्।";return 1!=e&&(u+="कृपया "+e+" अक्षरहरु मेटाउनुहोस्।"),u},inputTooShort:function(n){return"कृपया बाँकी रहेका "+(n.minimum-n.input.length)+" वा अरु धेरै अक्षरहरु भर्नुहोस्।"},loadingMore:function(){return"अरु नतिजाहरु भरिँदैछन् …"},maximumSelected:function(n){var e="तँपाई "+n.maximum+" वस्तु मात्र छान्न पाउँनुहुन्छ।";return 1!=n.maximum&&(e="तँपाई "+n.maximum+" वस्तुहरु मात्र छान्न पाउँनुहुन्छ।"),e},noResults:function(){return"कुनै पनि नतिजा भेटिएन।"},searching:function(){return"खोजि हुँदैछ…"}}}),n.define,n.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/nl.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/nl",[],function(){return{errorLoading:function(){return"De resultaten konden niet worden geladen."},inputTooLong:function(e){return"Gelieve "+(e.input.length-e.maximum)+" karakters te verwijderen"},inputTooShort:function(e){return"Gelieve "+(e.minimum-e.input.length)+" of meer karakters in te voeren"},loadingMore:function(){return"Meer resultaten laden…"},maximumSelected:function(e){var n=1==e.maximum?"kan":"kunnen",r="Er "+n+" maar "+e.maximum+" item";return 1!=e.maximum&&(r+="s"),r+=" worden geselecteerd"},noResults:function(){return"Geen resultaten gevonden…"},searching:function(){return"Zoeken…"},removeAllItems:function(){return"Verwijder alle items"}}}),e.define,e.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/pl.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/pl",[],function(){var n=["znak","znaki","znaków"],e=["element","elementy","elementów"],r=function(n,e){return 1===n?e[0]:n>1&&n<=4?e[1]:n>=5?e[2]:void 0};return{errorLoading:function(){return"Nie można załadować wyników."},inputTooLong:function(e){var t=e.input.length-e.maximum;return"Usuń "+t+" "+r(t,n)},inputTooShort:function(e){var t=e.minimum-e.input.length;return"Podaj przynajmniej "+t+" "+r(t,n)},loadingMore:function(){return"Trwa ładowanie…"},maximumSelected:function(n){return"Możesz zaznaczyć tylko "+n.maximum+" "+r(n.maximum,e)},noResults:function(){return"Brak wyników"},searching:function(){return"Trwa wyszukiwanie…"},removeAllItems:function(){return"Usuń wszystkie przedmioty"}}}),n.define,n.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/ps.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/ps",[],function(){return{errorLoading:function(){return"پايلي نه سي ترلاسه کېدای"},inputTooLong:function(n){var e=n.input.length-n.maximum,r="د مهربانۍ لمخي "+e+" توری ړنګ کړئ";return 1!=e&&(r=r.replace("توری","توري")),r},inputTooShort:function(n){return"لږ تر لږه "+(n.minimum-n.input.length)+" يا ډېر توري وليکئ"},loadingMore:function(){return"نوري پايلي ترلاسه کيږي..."},maximumSelected:function(n){var e="تاسو يوازي "+n.maximum+" قلم په نښه کولای سی";return 1!=n.maximum&&(e=e.replace("قلم","قلمونه")),e},noResults:function(){return"پايلي و نه موندل سوې"},searching:function(){return"لټول کيږي..."},removeAllItems:function(){return"ټول توکي لرې کړئ"}}}),n.define,n.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/pt-BR.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/pt-BR",[],function(){return{errorLoading:function(){return"Os resultados não puderam ser carregados."},inputTooLong:function(e){var n=e.input.length-e.maximum,r="Apague "+n+" caracter";return 1!=n&&(r+="es"),r},inputTooShort:function(e){return"Digite "+(e.minimum-e.input.length)+" ou mais caracteres"},loadingMore:function(){return"Carregando mais resultados…"},maximumSelected:function(e){var n="Você só pode selecionar "+e.maximum+" ite";return 1==e.maximum?n+="m":n+="ns",n},noResults:function(){return"Nenhum resultado encontrado"},searching:function(){return"Buscando…"},removeAllItems:function(){return"Remover todos os itens"}}}),e.define,e.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/pt.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/pt",[],function(){return{errorLoading:function(){return"Os resultados não puderam ser carregados."},inputTooLong:function(e){var r=e.input.length-e.maximum,n="Por favor apague "+r+" ";return n+=1!=r?"caracteres":"caractere"},inputTooShort:function(e){return"Introduza "+(e.minimum-e.input.length)+" ou mais caracteres"},loadingMore:function(){return"A carregar mais resultados…"},maximumSelected:function(e){var r="Apenas pode seleccionar "+e.maximum+" ";return r+=1!=e.maximum?"itens":"item"},noResults:function(){return"Sem resultados"},searching:function(){return"A procurar…"},removeAllItems:function(){return"Remover todos os itens"}}}),e.define,e.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/ro.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/ro",[],function(){return{errorLoading:function(){return"Rezultatele nu au putut fi incărcate."},inputTooLong:function(e){var t=e.input.length-e.maximum,n="Vă rugăm să ștergeți"+t+" caracter";return 1!==t&&(n+="e"),n},inputTooShort:function(e){return"Vă rugăm să introduceți "+(e.minimum-e.input.length)+" sau mai multe caractere"},loadingMore:function(){return"Se încarcă mai multe rezultate…"},maximumSelected:function(e){var t="Aveți voie să selectați cel mult "+e.maximum;return t+=" element",1!==e.maximum&&(t+="e"),t},noResults:function(){return"Nu au fost găsite rezultate"},searching:function(){return"Căutare…"},removeAllItems:function(){return"Eliminați toate elementele"}}}),e.define,e.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/ru.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/ru",[],function(){function n(n,e,r,u){return n%10<5&&n%10>0&&n%100<5||n%100>20?n%10>1?r:e:u}return{errorLoading:function(){return"Невозможно загрузить результаты"},inputTooLong:function(e){var r=e.input.length-e.maximum,u="Пожалуйста, введите на "+r+" символ";return u+=n(r,"","a","ов"),u+=" меньше"},inputTooShort:function(e){var r=e.minimum-e.input.length,u="Пожалуйста, введите ещё хотя бы "+r+" символ";return u+=n(r,"","a","ов")},loadingMore:function(){return"Загрузка данных…"},maximumSelected:function(e){var r="Вы можете выбрать не более "+e.maximum+" элемент";return r+=n(e.maximum,"","a","ов")},noResults:function(){return"Совпадений не найдено"},searching:function(){return"Поиск…"},removeAllItems:function(){return"Удалить все элементы"}}}),n.define,n.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/sk.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/sk",[],function(){var e={2:function(e){return e?"dva":"dve"},3:function(){return"tri"},4:function(){return"štyri"}};return{errorLoading:function(){return"Výsledky sa nepodarilo načítať."},inputTooLong:function(n){var t=n.input.length-n.maximum;return 1==t?"Prosím, zadajte o jeden znak menej":t>=2&&t<=4?"Prosím, zadajte o "+e[t](!0)+" znaky menej":"Prosím, zadajte o "+t+" znakov menej"},inputTooShort:function(n){var t=n.minimum-n.input.length;return 1==t?"Prosím, zadajte ešte jeden znak":t<=4?"Prosím, zadajte ešte ďalšie "+e[t](!0)+" znaky":"Prosím, zadajte ešte ďalších "+t+" znakov"},loadingMore:function(){return"Načítanie ďalších výsledkov…"},maximumSelected:function(n){return 1==n.maximum?"Môžete zvoliť len jednu položku":n.maximum>=2&&n.maximum<=4?"Môžete zvoliť najviac "+e[n.maximum](!1)+" položky":"Môžete zvoliť najviac "+n.maximum+" položiek"},noResults:function(){return"Nenašli sa žiadne položky"},searching:function(){return"Vyhľadávanie…"},removeAllItems:function(){return"Odstráňte všetky položky"}}}),e.define,e.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/sl.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/sl",[],function(){return{errorLoading:function(){return"Zadetkov iskanja ni bilo mogoče naložiti."},inputTooLong:function(e){var n=e.input.length-e.maximum,t="Prosim zbrišite "+n+" znak";return 2==n?t+="a":1!=n&&(t+="e"),t},inputTooShort:function(e){var n=e.minimum-e.input.length,t="Prosim vpišite še "+n+" znak";return 2==n?t+="a":1!=n&&(t+="e"),t},loadingMore:function(){return"Nalagam več zadetkov…"},maximumSelected:function(e){var n="Označite lahko največ "+e.maximum+" predmet";return 2==e.maximum?n+="a":1!=e.maximum&&(n+="e"),n},noResults:function(){return"Ni zadetkov."},searching:function(){return"Iščem…"},removeAllItems:function(){return"Odstranite vse elemente"}}}),e.define,e.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/sq.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/sq",[],function(){return{errorLoading:function(){return"Rezultatet nuk mund të ngarkoheshin."},inputTooLong:function(e){var n=e.input.length-e.maximum,t="Të lutem fshi "+n+" karakter";return 1!=n&&(t+="e"),t},inputTooShort:function(e){return"Të lutem shkruaj "+(e.minimum-e.input.length)+" ose më shumë karaktere"},loadingMore:function(){return"Duke ngarkuar më shumë rezultate…"},maximumSelected:function(e){var n="Mund të zgjedhësh vetëm "+e.maximum+" element";return 1!=e.maximum&&(n+="e"),n},noResults:function(){return"Nuk u gjet asnjë rezultat"},searching:function(){return"Duke kërkuar…"},removeAllItems:function(){return"Hiq të gjitha sendet"}}}),e.define,e.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/sr-Cyrl.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/sr-Cyrl",[],function(){function n(n,e,r,u){return n%10==1&&n%100!=11?e:n%10>=2&&n%10<=4&&(n%100<12||n%100>14)?r:u}return{errorLoading:function(){return"Преузимање није успело."},inputTooLong:function(e){var r=e.input.length-e.maximum,u="Обришите "+r+" симбол";return u+=n(r,"","а","а")},inputTooShort:function(e){var r=e.minimum-e.input.length,u="Укуцајте бар још "+r+" симбол";return u+=n(r,"","а","а")},loadingMore:function(){return"Преузимање још резултата…"},maximumSelected:function(e){var r="Можете изабрати само "+e.maximum+" ставк";return r+=n(e.maximum,"у","е","и")},noResults:function(){return"Ништа није пронађено"},searching:function(){return"Претрага…"},removeAllItems:function(){return"Уклоните све ставке"}}}),n.define,n.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/sr.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/sr",[],function(){function n(n,e,r,t){return n%10==1&&n%100!=11?e:n%10>=2&&n%10<=4&&(n%100<12||n%100>14)?r:t}return{errorLoading:function(){return"Preuzimanje nije uspelo."},inputTooLong:function(e){var r=e.input.length-e.maximum,t="Obrišite "+r+" simbol";return t+=n(r,"","a","a")},inputTooShort:function(e){var r=e.minimum-e.input.length,t="Ukucajte bar još "+r+" simbol";return t+=n(r,"","a","a")},loadingMore:function(){return"Preuzimanje još rezultata…"},maximumSelected:function(e){var r="Možete izabrati samo "+e.maximum+" stavk";return r+=n(e.maximum,"u","e","i")},noResults:function(){return"Ništa nije pronađeno"},searching:function(){return"Pretraga…"},removeAllItems:function(){return"Уклоните све ставке"}}}),n.define,n.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/sv.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/sv",[],function(){return{errorLoading:function(){return"Resultat kunde inte laddas."},inputTooLong:function(n){return"Vänligen sudda ut "+(n.input.length-n.maximum)+" tecken"},inputTooShort:function(n){return"Vänligen skriv in "+(n.minimum-n.input.length)+" eller fler tecken"},loadingMore:function(){return"Laddar fler resultat…"},maximumSelected:function(n){return"Du kan max välja "+n.maximum+" element"},noResults:function(){return"Inga träffar"},searching:function(){return"Söker…"},removeAllItems:function(){return"Ta bort alla objekt"}}}),n.define,n.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/th.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/th",[],function(){return{errorLoading:function(){return"ไม่สามารถค้นข้อมูลได้"},inputTooLong:function(n){return"โปรดลบออก "+(n.input.length-n.maximum)+" ตัวอักษร"},inputTooShort:function(n){return"โปรดพิมพ์เพิ่มอีก "+(n.minimum-n.input.length)+" ตัวอักษร"},loadingMore:function(){return"กำลังค้นข้อมูลเพิ่ม…"},maximumSelected:function(n){return"คุณสามารถเลือกได้ไม่เกิน "+n.maximum+" รายการ"},noResults:function(){return"ไม่พบข้อมูล"},searching:function(){return"กำลังค้นข้อมูล…"},removeAllItems:function(){return"ลบรายการทั้งหมด"}}}),n.define,n.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/tk.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var e=jQuery.fn.select2.amd;e.define("select2/i18n/tk",[],function(){return{errorLoading:function(){return"Netije ýüklenmedi."},inputTooLong:function(e){return e.input.length-e.maximum+" harp bozuň."},inputTooShort:function(e){return"Ýene-de iň az "+(e.minimum-e.input.length)+" harp ýazyň."},loadingMore:function(){return"Köpräk netije görkezilýär…"},maximumSelected:function(e){return"Diňe "+e.maximum+" sanysyny saýlaň."},noResults:function(){return"Netije tapylmady."},searching:function(){return"Gözlenýär…"},removeAllItems:function(){return"Remove all items"}}}),e.define,e.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/tr.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/tr",[],function(){return{errorLoading:function(){return"Sonuç yüklenemedi"},inputTooLong:function(n){return n.input.length-n.maximum+" karakter daha girmelisiniz"},inputTooShort:function(n){return"En az "+(n.minimum-n.input.length)+" karakter daha girmelisiniz"},loadingMore:function(){return"Daha fazla…"},maximumSelected:function(n){return"Sadece "+n.maximum+" seçim yapabilirsiniz"},noResults:function(){return"Sonuç bulunamadı"},searching:function(){return"Aranıyor…"},removeAllItems:function(){return"Tüm öğeleri kaldır"}}}),n.define,n.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/uk.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/uk",[],function(){function n(n,e,u,r){return n%100>10&&n%100<15?r:n%10==1?e:n%10>1&&n%10<5?u:r}return{errorLoading:function(){return"Неможливо завантажити результати"},inputTooLong:function(e){return"Будь ласка, видаліть "+(e.input.length-e.maximum)+" "+n(e.maximum,"літеру","літери","літер")},inputTooShort:function(n){return"Будь ласка, введіть "+(n.minimum-n.input.length)+" або більше літер"},loadingMore:function(){return"Завантаження інших результатів…"},maximumSelected:function(e){return"Ви можете вибрати лише "+e.maximum+" "+n(e.maximum,"пункт","пункти","пунктів")},noResults:function(){return"Нічого не знайдено"},searching:function(){return"Пошук…"},removeAllItems:function(){return"Видалити всі елементи"}}}),n.define,n.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/vi.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/vi",[],function(){return{inputTooLong:function(n){return"Vui lòng xóa bớt "+(n.input.length-n.maximum)+" ký tự"},inputTooShort:function(n){return"Vui lòng nhập thêm từ "+(n.minimum-n.input.length)+" ký tự trở lên"},loadingMore:function(){return"Đang lấy thêm kết quả…"},maximumSelected:function(n){return"Chỉ có thể chọn được "+n.maximum+" lựa chọn"},noResults:function(){return"Không tìm thấy kết quả"},searching:function(){return"Đang tìm…"},removeAllItems:function(){return"Xóa tất cả các mục"}}}),n.define,n.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/zh-CN.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/zh-CN",[],function(){return{errorLoading:function(){return"无法载入结果。"},inputTooLong:function(n){return"请删除"+(n.input.length-n.maximum)+"个字符"},inputTooShort:function(n){return"请再输入至少"+(n.minimum-n.input.length)+"个字符"},loadingMore:function(){return"载入更多结果…"},maximumSelected:function(n){return"最多只能选择"+n.maximum+"个项目"},noResults:function(){return"未找到结果"},searching:function(){return"搜索中…"},removeAllItems:function(){return"删除所有项目"}}}),n.define,n.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/select2/i18n/zh-TW.js: -------------------------------------------------------------------------------- 1 | /*! Select2 4.0.13 | https://github.com/select2/select2/blob/master/LICENSE.md */ 2 | 3 | !function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/zh-TW",[],function(){return{inputTooLong:function(n){return"請刪掉"+(n.input.length-n.maximum)+"個字元"},inputTooShort:function(n){return"請再輸入"+(n.minimum-n.input.length)+"個字元"},loadingMore:function(){return"載入中…"},maximumSelected:function(n){return"你只能選擇最多"+n.maximum+"項"},noResults:function(){return"沒有找到相符的項目"},searching:function(){return"搜尋中…"},removeAllItems:function(){return"刪除所有項目"}}}),n.define,n.require}(); -------------------------------------------------------------------------------- /backend/staticfiles/admin/js/vendor/xregexp/LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2007-2017 Steven Levithan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /backend/staticfiles/images/2d82cb919cf05116adf720f8f7437ac9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeldomQA/seldom-platform/2ed82ef3abaebca8fc19c1e2105bbdae04d369cc/backend/staticfiles/images/2d82cb919cf05116adf720f8f7437ac9.png -------------------------------------------------------------------------------- /backend/staticfiles/images/c8f816777c4a29ad3f797ab16aba2ea5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeldomQA/seldom-platform/2ed82ef3abaebca8fc19c1e2105bbdae04d369cc/backend/staticfiles/images/c8f816777c4a29ad3f797ab16aba2ea5.jpg -------------------------------------------------------------------------------- /backend/uwsgi.ini: -------------------------------------------------------------------------------- 1 | # uwsgi.ini file 2 | [uwsgi] 3 | 4 | # Django-related settings -> You need to edit 5 | http = :8080 6 | 7 | # the base directory (full path) -> You need to edit 8 | chdir = /home/app/seldom-platform/backend 9 | 10 | # Django s wsgi file 11 | module = backend.wsgi 12 | 13 | # process-related settings 14 | # master 15 | master = true 16 | 17 | # maximum number of worker processes 18 | processes = 4 19 | 20 | # ... with appropriate permissions - may be needed 21 | # chmod-socket = 664 22 | # clear environment on exit 23 | vacuum = true 24 | 25 | # static dir 26 | ; static-map = /static=/home/app/seldom-platform/backend/static 27 | 28 | py-autoreload = 2 29 | 30 | # log file -> You need to edit 31 | logto = /home/app/seldom-platform/backend/logs/log.txt 32 | 33 | -------------------------------------------------------------------------------- /doc/promotion_article.md: -------------------------------------------------------------------------------- 1 | # 颠覆传统的自动化测试平台 2 | 3 | 4 | ## 1. 传统的自动化测试平台 5 | 6 | 近些年,中等以上规模的公司测试团队都在建设自己的自动化测试平台。主要要以 `HTTP接口测试` 和 `性能测试` 为主;一些平台还支持 `Web UI测试`和`App UI`测试等,试图通过UI界面配置来替代`代码自动化测试编写`。 7 | 8 | ![](../img/metersphere.png) 9 | 10 | 图:来自MeterSphere平台。 11 | 12 | 自动化测试平台的好处,显而易见。 13 | 14 | * **降低使用门槛**:不需要测试人员懂编程语言和测试库。 15 | * **可视化的用例管理**:可以清晰地查看和编辑、统计用例。 16 | * **历史结果统计与分析**:可以通过各种维度统计测试相关指标。 17 | 18 | 然而,自动化测试平台的缺点,也很明显。 19 | 20 | * **用例的编写效率**:相比较于代码编写用例,UI配置的效率是比较低的。 21 | * **很难重构**:对于用例的编写,需要不断地抽象和封装,来调整用例的编写,然而,平台很难做到这一点。 22 | * **对于复杂用例的限制**:对于复杂的场景用例不支持,或需要通过非常复杂的配置。 23 | * **持续的投入成本**:一个企业级的测试平台,需要不断地投入开发资源去维护升级,来应对各种业务需求。 24 | 25 | 隐含缺点,没错!还有无法放到台面上说的缺点。 26 | 27 | * **对于测试工程编程能力的限制**:少部分测试开发工程师通过开发平台得到技能的成长。大部分的测试工程师的技能被限制在测试平台使用层面。 28 | 29 | 30 | ## 2. 框架与平台之争 31 | 32 | 当我们决定为公司引入自动化测试的时候,必然会遇到 使用框架编写,还是通过平台编写的问题。 33 | 34 | ### 框架的优势: 35 | 36 | * **灵活高**:当你具备编程能力,就能体会到编程的方式编写自动化用例有很高的可控性。例如:`if 判断`、`for 循环`、`封装`、`变量传递`、`参数加密` 等等,编写用例非常简洁高效。 37 | 38 | * **扩展方便**:我们可以通过库的方式无限扩展框架的能力,例如:需要用到`MySQL`数据库,安装个`pymysql`库就可以;需要实现`AES`加密,安装个`pycryptodome`库就可以了。 39 | 40 | * **利于测试工程师的成长**:这一点其实也挺重要的,在编写自动化测试代码的过程中会对编程语言有更多的使用经验,针对业务功能的测试的理解也会更加深刻;当然,技能的提升对于升职、跳槽都是有益处。 41 | 42 | ### 平台的优势: 43 | 44 | * **用例管理更透明**:通过平台管理用例,可以轻松地查看用例,统计用例的数量。 45 | 46 | * **更多运行方式**:平台支持更多的运行方式,单个用例运行,任务执行,定时执行等,平台可以轻松实现更多运行方式。 47 | 48 | * **便于数据统计**:通过平台更便于测试数据的统计。`天/周/月/年执行次数`,`天/周/年/成功率`等。 49 | 50 | 51 | 那么,是否有一种方案可以兼顾到 `框架` 和 `平台` 各自的优势呢? 52 | 53 | 54 | ## 3. Seldom-platform自动化测试平台 55 | 56 | ### 3.1 平台技术方案 57 | 58 | > 这根传统的测试平台非常不一样,传统的测试平台创建用例是非常低效的,也非常不灵活。但是,平台的优势在于维护测试用例的用例的管理,定时任务,以及结果的可视化管理。selenium-platform可以解析seldom框架编写的自动化用例。~ 这是一个完美的方案。 59 | 60 | * seldom-platform架构 61 | 62 | ![](../architecture.png) 63 | 64 | 65 | 🐍 1. **seldom** 66 | 67 | 通过seldom框架编写自动化测试用例。 68 | 69 | ![](../img/seldom-code.png) 70 | 71 | 🌐 2. **Github/gitee托管项目代码** 72 | 73 | 将你的代码托管到git平台, `github`、`gitlab`、`gitee`或者私有git平台都可以。 74 | 75 | ![](../img/github.png) 76 | 77 | 💻 **seldom-platfrom** 78 | 79 | 通过seldom-platfrom平台解析用例,执行、查看结果、定时任务... 80 | 81 | ![](../img/seldom-platform-code.png) 82 | 83 | 从上面的实现方案,`seldom-platform`充当了`CI`的角色,但是,又与`CI`有很大不同,`CI` 支能配置命令来执行自动化项目。Seldom-Platform可以对自动化项目做到`用例级`可视化管理。除了`不支持编写测试用例`(本来,编写测试用例也应该交给更擅长的`框架`来做。) 84 | 85 | ### 3.2 平台特点 86 | 87 | * 零成本支持任何类型测试 88 | 89 | 当我们将编写用例这件事情交给 `框架` 来完成之后,那么平台可以几乎零成本的实现任何类型的测试: `Web UI`、`App UI`、`HTTP`、`WebSocket`、`db数据库` 等。然而,传统的自动化测试平台每种类型的测试都需要做专门的支持。 90 | 91 | * 降低平台开发的成本 92 | 93 | 当平台不再负责用例的编写,那么成本可以得到很大的降低。想象你要在平台上实现用例的创建、用例依赖、数据依赖,模块依赖。不同类型的测试交互也会有很大的差异,显然要付出不小的开发成本。 94 | 95 | 96 | * 平衡测试编写与管理 97 | 98 | 测试工程师可以自由的使用 seldom框架编写自动化测试用例,同时,这并不会限制他技术成长。 99 | 测试管理者可以可视化的管理测试用例,查看、运行、统计等可以非常直观的管理用例。 100 | 101 | 102 | ### 相关资料 103 | 104 | > 目前 seldom-platform 已经到 2.0版本,重构了前端交互,提供更加友好的交互设计,以及更稳定的功能。 105 | 106 | 关注平台的更多使用细节,请访问开源项目,以及在线体验平台。 107 | 108 | seldom-platform开源平台:https://github.com/SeldomQA/seldom-platform 109 | 110 | 体验地址:http://seldom.testpub.cn/ 111 | 112 | 虫师:SeldomQA开源项目作者,专注于提供更高效的自动化测试解决方案。新书《自动化测试框架设计》 113 | 114 | -------------------------------------------------------------------------------- /frontendv3/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .vscode 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | -------------------------------------------------------------------------------- /frontendv3/dist/assets/Center.176a5796.js: -------------------------------------------------------------------------------- 1 | import"./index.367778c0.js";import{au as r}from"./index.367778c0.js";export{r as default}; 2 | -------------------------------------------------------------------------------- /frontendv3/dist/assets/Env.9aade454.js: -------------------------------------------------------------------------------- 1 | import"./index.367778c0.js";import{b as r}from"./index.367778c0.js";export{r as default}; 2 | -------------------------------------------------------------------------------- /frontendv3/dist/assets/Login.21652916.css: -------------------------------------------------------------------------------- 1 | .main{display:grid;grid-template-columns:1fr 1fr;padding:0 10rem}header{line-height:1.5;max-height:100vh;place-items:center;padding-right:calc(var(--section-gap) / 2)}.title{text-align:center}.login-card{width:350px;height:320px}.content{margin:auto}.login{display:flex;justify-content:flex-end;align-items:center;background-repeat:no-repeat;background-size:auto 100%;background-position:left}.features{text-align:center;margin-top:100px}.feature-option{height:120px}.feature-icon{top:5px}.feature-title{font-size:1.2em;font-weight:700;margin-left:15px} 2 | -------------------------------------------------------------------------------- /frontendv3/dist/assets/Manager.37945dbb.js: -------------------------------------------------------------------------------- 1 | import"./index.367778c0.js";import{c as r}from"./index.367778c0.js";export{r as default}; 2 | -------------------------------------------------------------------------------- /frontendv3/dist/assets/Project.7295eff2.js: -------------------------------------------------------------------------------- 1 | import"./index.367778c0.js";import{a as r}from"./index.367778c0.js";export{r as default}; 2 | -------------------------------------------------------------------------------- /frontendv3/dist/assets/Task.b33c6915.js: -------------------------------------------------------------------------------- 1 | import"./index.367778c0.js";import{_ as r}from"./index.367778c0.js";export{r as default}; 2 | -------------------------------------------------------------------------------- /frontendv3/dist/assets/index.c53f830a.css: -------------------------------------------------------------------------------- 1 | #app{font-family:Avenir,Helvetica,Arial,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;color:#2c3e50;height:100%}body{margin:0}html{background-attachment:fixed;background-size:100% 100%;z-index:-1000;background-color:#fbf9f6}html,body{height:100%}.n-input__input-el{height:auto!important}.breadcrumb-navigation{height:40px;font-size:1.17em;font-weight:700}.dialog-footer{margin-top:10px;float:right}.foot-page{margin-top:15px;height:30px;float:right}.main-card{min-height:700px}.n-layout-header[data-v-532357f5],.n-layout-footer[data-v-532357f5]{padding:20px}.dialog-footer[data-v-05542a2f]{float:right}.filter-line{padding-bottom:24px}.card-group{text-align:left}.image-container{height:120px;width:120px;display:flex;align-items:center;justify-content:center;overflow:hidden;margin-left:60px}.image{height:100%;width:100%;object-fit:cover}.enter-btn{float:right;margin-top:15px}.dialog-footer[data-v-ad6d6629]{margin-top:24px;text-align:right}.filter-line{padding-bottom:20px}.card-style{width:300px;margin-right:20px}.filter-line[data-v-a49a6e34]{padding-bottom:20px}.case-result[data-v-b8e707db]{width:100%}.result-info[data-v-b8e707db]{margin-bottom:16px}.case-sync-modal[data-v-b368be28]{width:100%}.case-lists[data-v-b368be28]{margin-top:20px}.add-case-list[data-v-b368be28],.del-case-list[data-v-b368be28]{width:365px;height:400px;overflow-y:auto}.case-list[data-v-b368be28]{width:100%}.dialog-footer[data-v-b368be28]{margin-top:16px;text-align:right}.case-sync-log[data-v-552ad481]{width:100%}.filter-line[data-v-9735b953]{padding-bottom:20px}.filetree[data-v-9735b953]{border:solid 1px var(--n-border-color)}.n-tree-node-content[data-v-9735b953]{text-align:left}.class-list[data-v-e9e8c5b9]{height:800px;overflow-y:scroll}.empty-state[data-v-e9e8c5b9]{display:flex;flex-direction:column;align-items:center;justify-content:center;height:500px;color:#909399}.info-icon[data-v-e9e8c5b9]{margin-bottom:16px}.empty-text[data-v-e9e8c5b9]{font-size:14px;margin:0}.table[data-v-8ab73390]{height:100%}.transfer-style[data-v-f467b6da]{height:500px}.filetree[data-v-f467b6da]{border:solid 1px var(--n-border-color)}.dialog-footer[data-v-888e8543]{margin-top:24px;text-align:right} 2 | -------------------------------------------------------------------------------- /frontendv3/dist/assets/login-bg.009bc7f3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeldomQA/seldom-platform/2ed82ef3abaebca8fc19c1e2105bbdae04d369cc/frontendv3/dist/assets/login-bg.009bc7f3.png -------------------------------------------------------------------------------- /frontendv3/dist/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeldomQA/seldom-platform/2ed82ef3abaebca8fc19c1e2105bbdae04d369cc/frontendv3/dist/favicon.ico -------------------------------------------------------------------------------- /frontendv3/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Seldom Platform 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /frontendv3/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Seldom Platform 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /frontendv3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontendv3", 3 | "private": true, 4 | "version": "0.0.0", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build", 8 | "preview": "vite preview", 9 | "test:e2e": "npx playwright test" 10 | }, 11 | "dependencies": { 12 | "awesome-cron": "^0.0.14", 13 | "axios": "^1.7.7", 14 | "chart.js": "^4.2.1", 15 | "frontendv3": "file:", 16 | "pnpm": "^8.15.8", 17 | "qs": "^6.10.5", 18 | "vue": "^3.2.25", 19 | "vue-chartjs": "^5.2.0", 20 | "vue-router": "4", 21 | "vuex": "^4.0.2" 22 | }, 23 | "devDependencies": { 24 | "@playwright/test": "^1.52.0", 25 | "@types/node": "^18.8.3", 26 | "@types/qs": "^6.9.7", 27 | "@vicons/ionicons5": "^0.12.0", 28 | "@vitejs/plugin-vue": "^3.1.0", 29 | "naive-ui": "^2.40.1", 30 | "seemly": "^0.3.6", 31 | "typescript": "^4.5.4", 32 | "unplugin-vue-components": "^0.22.7", 33 | "vite": "^3.1.3", 34 | "vite-plugin-pages": "^0.26.0", 35 | "vue-tsc": "^0.34.7" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /frontendv3/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeldomQA/seldom-platform/2ed82ef3abaebca8fc19c1e2105bbdae04d369cc/frontendv3/public/favicon.ico -------------------------------------------------------------------------------- /frontendv3/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '~/request/project' -------------------------------------------------------------------------------- /frontendv3/src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | 15 | 67 | -------------------------------------------------------------------------------- /frontendv3/src/assets/home-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeldomQA/seldom-platform/2ed82ef3abaebca8fc19c1e2105bbdae04d369cc/frontendv3/src/assets/home-bg.png -------------------------------------------------------------------------------- /frontendv3/src/assets/login-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeldomQA/seldom-platform/2ed82ef3abaebca8fc19c1e2105bbdae04d369cc/frontendv3/src/assets/login-bg.png -------------------------------------------------------------------------------- /frontendv3/src/assets/seldom-icon.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeldomQA/seldom-platform/2ed82ef3abaebca8fc19c1e2105bbdae04d369cc/frontendv3/src/assets/seldom-icon.gif -------------------------------------------------------------------------------- /frontendv3/src/assets/seldom-platform.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeldomQA/seldom-platform/2ed82ef3abaebca8fc19c1e2105bbdae04d369cc/frontendv3/src/assets/seldom-platform.gif -------------------------------------------------------------------------------- /frontendv3/src/components/CaseResult.vue: -------------------------------------------------------------------------------- 1 | 52 | 53 | 82 | 83 | -------------------------------------------------------------------------------- /frontendv3/src/components/CaseSyncLog.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 57 | 58 | -------------------------------------------------------------------------------- /frontendv3/src/config/base-url.ts: -------------------------------------------------------------------------------- 1 | let baseUrl = ""; 2 | let mode: string = import.meta.env.MODE; 3 | switch (mode) { 4 | case "development": 5 | baseUrl = "http://127.0.0.1:8000"; //开发环境url 6 | break; 7 | case "production": 8 | baseUrl = ""; //生产环境url 9 | break; 10 | } 11 | 12 | export default baseUrl; 13 | -------------------------------------------------------------------------------- /frontendv3/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // declare module '*.vue' { 5 | // import type { DefineComponent } from 'vue' 6 | // // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types 7 | // const component: DefineComponent<{}, {}, any> 8 | // export default component 9 | // } 10 | 11 | declare module "*.vue" { 12 | import type { ComponentOptions, ComponentOptions } from "vue"; 13 | const Component: ComponentOptions; 14 | export default Component; 15 | } 16 | 17 | declare module "*.md" { 18 | const Component: ComponentOptions; 19 | export default Component; 20 | } 21 | -------------------------------------------------------------------------------- /frontendv3/src/layouts/BaseLayout.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 61 | 62 | -------------------------------------------------------------------------------- /frontendv3/src/layouts/CenterNav.vue: -------------------------------------------------------------------------------- 1 | 76 | 77 | 84 | -------------------------------------------------------------------------------- /frontendv3/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | import App from "./App.vue"; 3 | import { createRouter, routes } from "./router"; 4 | 5 | const app = createApp(App); 6 | const router = createRouter(); 7 | app.use(router); 8 | app.mount("#app"); 9 | -------------------------------------------------------------------------------- /frontendv3/src/pages/Center.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 9 | -------------------------------------------------------------------------------- /frontendv3/src/pages/Manager.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 9 | -------------------------------------------------------------------------------- /frontendv3/src/request/case.ts: -------------------------------------------------------------------------------- 1 | import request from "./common/http"; 2 | import { TNomalObject } from "./common/http"; 3 | 4 | class CaseApi { 5 | 6 | runningCase(case_id: string, data: TNomalObject) { 7 | return request.post("/api/case/" + case_id + "/running", data); 8 | } 9 | 10 | getCaseResult(case_id: string) { 11 | return request.get("/api/case/" + case_id + "/result"); 12 | } 13 | 14 | getSyncLog() { 15 | return request.get("/api/project/sync_log") 16 | } 17 | 18 | } 19 | 20 | export default new CaseApi(); 21 | -------------------------------------------------------------------------------- /frontendv3/src/request/project.ts: -------------------------------------------------------------------------------- 1 | import request from "./common/http"; 2 | import { TNomalObject } from "./common/http"; 3 | 4 | class ProjectApi { 5 | createProject(data: TNomalObject) { 6 | return request.post("/api/project/create", data); 7 | } 8 | 9 | getProjects() { 10 | return request.get("/api/project/list"); 11 | } 12 | 13 | getProject(pid: string) { 14 | return request.get("/api/project/" + pid + "/"); 15 | } 16 | 17 | updateProject(pid: string, data: TNomalObject) { 18 | return request.put("/api/project/" + pid + "/", data); 19 | } 20 | 21 | syncCode(pid: string) { 22 | return request.get("/api/project/" + pid + "/sync_code"); 23 | } 24 | 25 | syncCase(pid: string, sync_mode: string) { 26 | return request.get("/api/project/" + pid + "/sync_case", { sync_mode }); 27 | } 28 | 29 | syncResult(pid: string) { 30 | return request.get("/api/project/" + pid + "/sync_result"); 31 | } 32 | 33 | syncMerge(pid: string, data: TNomalObject) { 34 | return request.post("/api/project/" + pid + "/sync_merge", data); 35 | } 36 | 37 | getSyncLog() { 38 | return request.get("/api/project/sync_log"); 39 | } 40 | 41 | deleteProject(pid: string) { 42 | return request.del("/api/project/" + pid + "/"); 43 | } 44 | 45 | cloneProject(pid: string) { 46 | return request.get("/api/project/" + pid + "/clone"); 47 | } 48 | 49 | // 后续有具体删除需求备用 50 | // removeProjectCover(pid:string) { 51 | // return request.put('/api/project/cover/remove/' + pid + '/') 52 | // } 53 | 54 | syncProjectCase(pid: string) { 55 | return request.get("/api/project/" + pid + "/sync"); 56 | } 57 | 58 | getProjectTree(pid: string) { 59 | return request.get("/api/project/" + pid + "/files"); 60 | } 61 | 62 | getProjectCases(pid: string, file_name: string, label?: string) { 63 | const params: Record = { file_name }; 64 | if (label) { 65 | params.label_name = label; 66 | } 67 | return request.get("/api/project/" + pid + "/cases", params); 68 | } 69 | 70 | getProjectSubdirectory(pid: string, file_name: string) { 71 | return request.get("/api/project/" + pid + "/subdirectory", { file_name }); 72 | } 73 | 74 | // Env functions 75 | createEnv(data: TNomalObject) { 76 | return request.post("/api/project/env", data); 77 | } 78 | 79 | getEnv(id: string) { 80 | return request.get("/api/project/env/" + id + "/"); 81 | } 82 | 83 | getEnvs() { 84 | return request.get("/api/project/env/list"); 85 | } 86 | 87 | deleteEnv(id: string) { 88 | return request.del("/api/project/env/" + id + "/"); 89 | } 90 | 91 | updateEnv(id: string, data: TNomalObject) { 92 | return request.put("/api/project/env/" + id + "/", data); 93 | } 94 | } 95 | 96 | export default new ProjectApi(); 97 | -------------------------------------------------------------------------------- /frontendv3/src/request/task.ts: -------------------------------------------------------------------------------- 1 | import request from "./common/http"; 2 | import { TNomalObject } from "./common/http"; 3 | 4 | class TaskApi { 5 | createTask(data: TNomalObject) { 6 | return request.post("/api/task/create", data); 7 | } 8 | 9 | getTaskAll(data: TNomalObject) { 10 | return request.get("/api/task/list", data); 11 | } 12 | 13 | getTaskDetails(tid: string) { 14 | return request.get("/api/task/" + tid + "/"); 15 | } 16 | 17 | updateTask(tid: string, data: TNomalObject) { 18 | return request.put("/api/task/" + tid + "/", data); 19 | } 20 | 21 | deleteTask(tid: string) { 22 | return request.del("/api/task/" + tid + "/"); 23 | } 24 | 25 | runningTask(tid: string) { 26 | return request.get("/api/task/" + tid + "/running"); 27 | } 28 | 29 | addTimed(tid: string, data: TNomalObject) { 30 | return request.post("/api/task/" + tid + "/timed", data); 31 | } 32 | 33 | switchTimed(taskId: string) { 34 | return request.put(`/api/task/timed/switch?task_id=${taskId}`); 35 | } 36 | 37 | deleteTimed(taskId: string) { 38 | return request.del(`/api/task/timed/delete?task_id=${taskId}`); 39 | } 40 | 41 | getReportAll(data: TNomalObject) { 42 | return request.get("/api/task/reports", data); 43 | } 44 | 45 | getReportResult(rid: string, data: TNomalObject) { 46 | return request.post("/api/task/report/" + rid + "/results", data); 47 | } 48 | 49 | createTimed(data: any) { 50 | return request.post("/api/task/timed/create", data); 51 | } 52 | } 53 | 54 | export default new TaskApi(); 55 | -------------------------------------------------------------------------------- /frontendv3/src/request/team.ts: -------------------------------------------------------------------------------- 1 | import request from "./common/http"; 2 | import { TNomalObject } from "./common/http"; 3 | 4 | class TeamApi { 5 | createTeam(data: TNomalObject) { 6 | return request.post("/api/team/create", data); 7 | } 8 | 9 | getTeamAll(data?: TNomalObject) { 10 | return request.get("/api/team/list", data); 11 | } 12 | 13 | getTeamDetails(tid: string) { 14 | return request.get("/api/team/" + tid + "/"); 15 | } 16 | 17 | updateTeam(tid: string, data: TNomalObject) { 18 | return request.put("/api/team/" + tid + "/", data); 19 | } 20 | 21 | deleteTeam(tid: string) { 22 | return request.del("/api/team/" + tid + "/"); 23 | } 24 | } 25 | 26 | export default new TeamApi(); 27 | -------------------------------------------------------------------------------- /frontendv3/src/request/user.ts: -------------------------------------------------------------------------------- 1 | import request from "./common/http"; 2 | import { TNomalObject } from "./common/http"; 3 | 4 | class UserApi { 5 | login(data: TNomalObject) { 6 | return request.post("/api/user/login", data); 7 | } 8 | 9 | logout(data: TNomalObject) { 10 | return request.post("/api/user/logout", data); 11 | } 12 | 13 | register(data: TNomalObject) { 14 | return request.post("/api/user/register", data); 15 | } 16 | } 17 | 18 | export default new UserApi(); 19 | -------------------------------------------------------------------------------- /frontendv3/src/router.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createRouter as _createRouter, 3 | createMemoryHistory, 4 | createWebHashHistory, 5 | createWebHistory, 6 | } from "vue-router"; 7 | 8 | import pageRoutes from "~pages"; 9 | import Center from "~/pages/Center.vue"; 10 | import Project from "~/pages/center/Project.vue"; 11 | import Env from "~/pages/center/Env.vue"; 12 | import Team from "~/pages/center/Team.vue"; 13 | import Manager from "~/pages/Manager.vue"; 14 | import Case from "~/pages/manager/Case.vue"; 15 | import Task from "~/pages/manager/Task.vue"; 16 | 17 | // export const routes = pageRoutes; 18 | 19 | export const routes = [ 20 | ...pageRoutes, 21 | { 22 | path: '/center', 23 | name: 'center', 24 | component: Center, 25 | children: [ 26 | { 27 | path: 'project', 28 | name: 'center-Project', 29 | component: Project, 30 | }, 31 | { 32 | path: 'env', 33 | name: 'center-Env', 34 | component: Env, 35 | }, 36 | { 37 | path: 'team', 38 | name: 'center-Team', 39 | component: Team, 40 | } 41 | ] 42 | }, 43 | { 44 | path: '/manager', 45 | name: 'manager', 46 | component: Manager, 47 | children: [ 48 | { 49 | path: 'case', 50 | name: 'manager-Case', 51 | component: Case, 52 | }, 53 | { 54 | path: 'task', 55 | name: 'manager-Task', 56 | component: Task, 57 | } 58 | ] 59 | } 60 | ] 61 | 62 | console.log("layout", routes) 63 | 64 | export function createRouter() { 65 | const router = _createRouter({ 66 | history: createWebHistory(), 67 | routes, 68 | }); 69 | 70 | // 开启登陆页 71 | // 导航守卫,控制一些页面登录才能访问 72 | router.beforeEach((to, from, next) => { 73 | if (to.path === "/login") { 74 | // 当路由为login时就直接下一步操作 75 | next(); 76 | } else { 77 | // 否则就需要判断 78 | if (sessionStorage.token) { 79 | // 如果有用户名就进行下一步操作 80 | next(); 81 | } else { 82 | next({ path: "/login" }); // 没有用户名就跳转到login页面 83 | } 84 | } 85 | }); 86 | return router; 87 | } 88 | -------------------------------------------------------------------------------- /frontendv3/src/store/index.ts: -------------------------------------------------------------------------------- 1 | 2 | // 项目的读写 3 | class ProjectStorage { 4 | 5 | public setProject(projectId: number, projectName: string): void { 6 | const project = { id: projectId, name: projectName }; 7 | sessionStorage.setItem('currentProject', JSON.stringify(project)); 8 | } 9 | 10 | public getProject(): { id?: number, name?: string } | null { 11 | const projectDataJSON = sessionStorage.getItem('currentProject'); 12 | if (projectDataJSON) { 13 | const projectData = JSON.parse(projectDataJSON); 14 | return projectData; 15 | } 16 | return null; 17 | } 18 | } 19 | 20 | // 创建一个单例实例,方便全局使用 21 | const projectStorage = new ProjectStorage(); 22 | 23 | export default projectStorage; 24 | -------------------------------------------------------------------------------- /frontendv3/tests/example.spec.ts: -------------------------------------------------------------------------------- 1 | // tests/example.spec.ts 2 | import { test, expect } from '@playwright/test'; 3 | 4 | test('basic test', async ({ page }) => { 5 | await page.goto('/'); 6 | const title = await page.title(); 7 | expect(title).toBe('Seldom Platform'); 8 | }); -------------------------------------------------------------------------------- /frontendv3/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "useDefineForClassFields": true, 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "strict": true, 8 | "jsx": "preserve", 9 | "sourceMap": true, 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "esModuleInterop": true, 13 | "lib": ["esnext", "dom"], 14 | "skipLibCheck": true, 15 | "baseUrl": ".", 16 | "paths": { 17 | "~/*": ["./src/*"], 18 | "@/*": ["./src/compoents/*"] 19 | } 20 | }, 21 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], 22 | "references": [{ "path": "./tsconfig.node.json" }] 23 | } 24 | -------------------------------------------------------------------------------- /frontendv3/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "esnext", 5 | "moduleResolution": "node" 6 | }, 7 | "include": ["vite.config.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /frontendv3/view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeldomQA/seldom-platform/2ed82ef3abaebca8fc19c1e2105bbdae04d369cc/frontendv3/view.png -------------------------------------------------------------------------------- /frontendv3/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import vue from "@vitejs/plugin-vue"; 3 | import { resolve } from "path"; 4 | 5 | import Components from "unplugin-vue-components/vite"; 6 | import { NaiveUiResolver } from "unplugin-vue-components/resolvers"; 7 | import Pages from "vite-plugin-pages"; 8 | 9 | // https://vitejs.dev/config/ 10 | export default defineConfig({ 11 | server: { 12 | host: "0.0.0.0", 13 | port: 3333, 14 | }, 15 | plugins: [ 16 | vue(), 17 | Components({ 18 | extensions: ["vue", "md"], 19 | dts: true, 20 | include: [/\.vue$/, /\.vue\?vue/, /\.md$/], 21 | resolvers: [NaiveUiResolver()], 22 | }), 23 | Pages({ 24 | pagesDir: "src/pages", 25 | }), 26 | ], 27 | resolve: { 28 | alias: { 29 | "~": resolve(__dirname, "./src"), 30 | "@": resolve(__dirname, "./src/components"), 31 | }, 32 | } 33 | }); 34 | -------------------------------------------------------------------------------- /img/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeldomQA/seldom-platform/2ed82ef3abaebca8fc19c1e2105bbdae04d369cc/img/github.png -------------------------------------------------------------------------------- /img/login-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeldomQA/seldom-platform/2ed82ef3abaebca8fc19c1e2105bbdae04d369cc/img/login-page.png -------------------------------------------------------------------------------- /img/metersphere.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeldomQA/seldom-platform/2ed82ef3abaebca8fc19c1e2105bbdae04d369cc/img/metersphere.png -------------------------------------------------------------------------------- /img/seldom-code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeldomQA/seldom-platform/2ed82ef3abaebca8fc19c1e2105bbdae04d369cc/img/seldom-code.png -------------------------------------------------------------------------------- /img/seldom-platform-code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeldomQA/seldom-platform/2ed82ef3abaebca8fc19c1e2105bbdae04d369cc/img/seldom-platform-code.png -------------------------------------------------------------------------------- /img/v2_case_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeldomQA/seldom-platform/2ed82ef3abaebca8fc19c1e2105bbdae04d369cc/img/v2_case_list.png -------------------------------------------------------------------------------- /img/v2_sync_case.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeldomQA/seldom-platform/2ed82ef3abaebca8fc19c1e2105bbdae04d369cc/img/v2_sync_case.png -------------------------------------------------------------------------------- /wechat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeldomQA/seldom-platform/2ed82ef3abaebca8fc19c1e2105bbdae04d369cc/wechat.jpg --------------------------------------------------------------------------------