├── .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 | 
13 |
14 |
15 | __三个步骤:__
16 |
17 | 🐍 **seldom**
18 |
19 | > 1. 通过seldom框架编写自动化测试用例。
20 |
21 | 
22 |
23 | 🌐 **Github/gitee托管项目代码**
24 |
25 | > 2. 通过git管理自动化测试脚本
26 |
27 | 
28 |
29 | 💻 **seldom-platfrom**
30 |
31 | > 3. 通过seldom-platfrom平台解析用例,执行、查看结果、定时任务...
32 |
33 | 
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 | 
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 | 
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 |
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 |
4 |
--------------------------------------------------------------------------------
/backend/staticfiles/admin/img/icon-alert.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/backend/staticfiles/admin/img/icon-calendar.svg:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/backend/staticfiles/admin/img/icon-changelink.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/backend/staticfiles/admin/img/icon-clock.svg:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/backend/staticfiles/admin/img/icon-deletelink.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/backend/staticfiles/admin/img/icon-no.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/backend/staticfiles/admin/img/icon-unknown-alt.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/backend/staticfiles/admin/img/icon-unknown.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/backend/staticfiles/admin/img/icon-viewlink.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/backend/staticfiles/admin/img/icon-yes.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/backend/staticfiles/admin/img/inline-delete.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/backend/staticfiles/admin/img/search.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/backend/staticfiles/admin/img/selector-icons.svg:
--------------------------------------------------------------------------------
1 |
35 |
--------------------------------------------------------------------------------
/backend/staticfiles/admin/img/sorting-icons.svg:
--------------------------------------------------------------------------------
1 |
20 |
--------------------------------------------------------------------------------
/backend/staticfiles/admin/img/tooltag-add.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/backend/staticfiles/admin/img/tooltag-arrowright.svg:
--------------------------------------------------------------------------------
1 |
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 | 
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 | 
63 |
64 |
65 | 🐍 1. **seldom**
66 |
67 | 通过seldom框架编写自动化测试用例。
68 |
69 | 
70 |
71 | 🌐 2. **Github/gitee托管项目代码**
72 |
73 | 将你的代码托管到git平台, `github`、`gitlab`、`gitee`或者私有git平台都可以。
74 |
75 | 
76 |
77 | 💻 **seldom-platfrom**
78 |
79 | 通过seldom-platfrom平台解析用例,执行、查看结果、定时任务...
80 |
81 | 
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 |
8 |
9 |
10 |
11 |
12 |
13 |
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 |
54 |
55 |
56 |
60 |
61 | {{ result.name }}
62 |
63 |
64 | {{ result.create_time }}
65 |
66 |
67 | {{ result.run_time }}
68 |
69 |
70 |
71 |
72 | 运行日志
73 |
79 |
80 |
81 |
82 |
83 |
--------------------------------------------------------------------------------
/frontendv3/src/components/CaseSyncLog.vue:
--------------------------------------------------------------------------------
1 |
46 |
47 |
48 |
49 |
55 |
56 |
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 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
44 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | © 2025 Powered by SeldomQA Team
56 |
57 |
58 |
59 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/frontendv3/src/layouts/CenterNav.vue:
--------------------------------------------------------------------------------
1 |
76 |
77 |
78 |
83 |
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 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/frontendv3/src/pages/Manager.vue:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
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
--------------------------------------------------------------------------------