Merge branch 'develop' into hotfix/get_tickets
This commit is contained in:
commit
d943f474b1
@ -7,23 +7,25 @@
|
|||||||
*.sql.gz
|
*.sql.gz
|
||||||
*.sqlite3
|
*.sqlite3
|
||||||
.cache
|
.cache
|
||||||
.env
|
.env*
|
||||||
.project
|
.project
|
||||||
.idea
|
.idea
|
||||||
.pydevproject
|
.pydevproject
|
||||||
.idea/workspace.xml
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
build/
|
||||||
.git/
|
.git/
|
||||||
.sass-cache
|
.gitlab/
|
||||||
.vagrant/
|
.gitignore
|
||||||
|
venv/
|
||||||
|
fixtures/
|
||||||
|
db/
|
||||||
|
data.json
|
||||||
__pycache__
|
__pycache__
|
||||||
dist
|
dist
|
||||||
docs
|
|
||||||
env
|
|
||||||
logs
|
logs
|
||||||
src/{{ project_name }}/settings/local.py
|
|
||||||
src/node_modules
|
|
||||||
web/media
|
|
||||||
web/static/CACHE
|
|
||||||
stats
|
|
||||||
Dockerfile
|
Dockerfile
|
||||||
|
docker-compose*
|
||||||
|
.dockerignore
|
||||||
|
layouts
|
||||||
|
staticroot
|
||||||
|
.editorconfig
|
||||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -373,4 +373,8 @@ $RECYCLE.BIN/
|
|||||||
# Windows shortcuts
|
# Windows shortcuts
|
||||||
*.lnk
|
*.lnk
|
||||||
|
|
||||||
|
### react ###
|
||||||
|
main/control_page_js_modules/node_modules
|
||||||
|
static/main/js/control_page/dist
|
||||||
|
node_modules
|
||||||
# End of https://www.toptal.com/developers/gitignore/api/django,pycharm+all,python,linux,macos,windows
|
# End of https://www.toptal.com/developers/gitignore/api/django,pycharm+all,python,linux,macos,windows
|
||||||
|
@ -1,7 +1,19 @@
|
|||||||
image: python:3-alpine
|
image: python:3.9-alpine
|
||||||
|
|
||||||
|
|
||||||
|
variables:
|
||||||
|
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache"
|
||||||
|
|
||||||
|
cache:
|
||||||
|
paths:
|
||||||
|
- .cache/pip
|
||||||
|
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- test
|
- test
|
||||||
|
- style
|
||||||
|
- build
|
||||||
|
- deploy
|
||||||
|
|
||||||
django_test:
|
django_test:
|
||||||
stage: test
|
stage: test
|
||||||
@ -21,3 +33,35 @@ coverage:
|
|||||||
artifacts:
|
artifacts:
|
||||||
paths:
|
paths:
|
||||||
- public/coverage
|
- public/coverage
|
||||||
|
|
||||||
|
pylint:
|
||||||
|
stage: style
|
||||||
|
allow_failure: true
|
||||||
|
before_script:
|
||||||
|
- pip install -r requirements/dev.txt
|
||||||
|
script:
|
||||||
|
- pylint --reports=yes main
|
||||||
|
|
||||||
|
docker:
|
||||||
|
stage: build
|
||||||
|
only:
|
||||||
|
- master
|
||||||
|
tags:
|
||||||
|
- docker
|
||||||
|
before_script:
|
||||||
|
- docker info
|
||||||
|
- docker login -u $REGISTRY_USERNAME -p $REGISTRY_PASSWORD gitlab.cazzzer.com:5050
|
||||||
|
script:
|
||||||
|
- docker build . -t gitlab.cazzzer.com:5050/cazzzer/zendesk-access-controller:alpine
|
||||||
|
- docker push gitlab.cazzzer.com:5050/cazzzer/zendesk-access-controller:alpine
|
||||||
|
after_script:
|
||||||
|
- docker logout
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
stage: deploy
|
||||||
|
only:
|
||||||
|
- master
|
||||||
|
tags:
|
||||||
|
- cazzzer-internal
|
||||||
|
script:
|
||||||
|
- sudo -u cazzzer /home/cazzzer/deploy_access_controller.sh
|
||||||
|
32
Dockerfile
32
Dockerfile
@ -1,10 +1,26 @@
|
|||||||
FROM python:3.6
|
FROM python:3.9 as builder
|
||||||
COPY ./ /access_controller
|
# enchant dependency for sphinx
|
||||||
|
RUN apt-get update && apt-get install -y python-enchant
|
||||||
|
# copy source files
|
||||||
WORKDIR /access_controller/
|
WORKDIR /access_controller/
|
||||||
RUN pip install -r requirements/prod.txt
|
COPY ./ /access_controller
|
||||||
RUN python manage.py makemigrations
|
# dev requirements for building sphinx docs
|
||||||
|
RUN pip install -r requirements/dev.txt -r requirements/prod.txt
|
||||||
|
# build static and documentation files into build/webserver
|
||||||
|
RUN ./manage.py collectstatic --no-input && ./documentation.sh
|
||||||
|
# create production venv to copy to final image
|
||||||
|
# production venv is built here because `cryptography` requires rust which doesn't ship on alpine
|
||||||
|
RUN python -m venv venv && venv/bin/pip install -r requirements/prod.txt
|
||||||
|
# move files necessary to run the app to the build folder (including production venv)
|
||||||
|
RUN mv venv access_controller main manage.py build
|
||||||
|
|
||||||
|
FROM python:3.9-alpine
|
||||||
|
WORKDIR /access_controller/
|
||||||
|
COPY --from=builder ["/access_controller/build", "./"]
|
||||||
|
RUN apk update && apk add postgresql-libs && adduser -D -H -u 5579 actrl && chmod -R -w ./
|
||||||
|
USER actrl
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
COPY start.sh /var/
|
CMD . venv/bin/activate && \
|
||||||
CMD bash /var/start.sh
|
python manage.py migrate && \
|
||||||
|
cp -rT webserver srv && \
|
||||||
|
daphne -b 0.0.0.0 access_controller.asgi:application
|
||||||
|
29
README.md
29
README.md
@ -41,6 +41,15 @@
|
|||||||
```bash
|
```bash
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
```
|
```
|
||||||
|
Установить модули для работы js
|
||||||
|
```bash
|
||||||
|
sudo apt install npm
|
||||||
|
cd main/control_page_js_modules/
|
||||||
|
npm install
|
||||||
|
sudo npm -g install npx
|
||||||
|
npx webpack
|
||||||
|
```
|
||||||
|
|
||||||
Заменить переменные в `.env` на актуальные.
|
Заменить переменные в `.env` на актуальные.
|
||||||
```bash
|
```bash
|
||||||
sudo apt install make
|
sudo apt install make
|
||||||
@ -57,6 +66,26 @@ pip install -r requirements/dev.txt
|
|||||||
Для админов ZenDesk дополнительно - создайте токен доступа в ZenDesk
|
Для админов ZenDesk дополнительно - создайте токен доступа в ZenDesk
|
||||||
При запуске в Docker убедитесь что папка, которая будет служить хранилищем для БД, открыта на запись и чтение
|
При запуске в Docker убедитесь что папка, которая будет служить хранилищем для БД, открыта на запись и чтение
|
||||||
|
|
||||||
|
Для запуска тестов страницы управления:
|
||||||
|
1. Установить npm и npx
|
||||||
|
```bash
|
||||||
|
sudo apt install npm
|
||||||
|
```
|
||||||
|
2. Перейти в static папку со страницей управления:
|
||||||
|
```bash
|
||||||
|
cd main/control_page_js_modules/
|
||||||
|
```
|
||||||
|
3. Выполнить установку модулей для js
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
sudo npm -g install npx
|
||||||
|
npx webpack
|
||||||
|
```
|
||||||
|
4. Тестирование в той же папке
|
||||||
|
```bash
|
||||||
|
npm test
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Запуск на локальной машине:
|
## Запуск на локальной машине:
|
||||||
- Скопировать репозиторий на локальную машину
|
- Скопировать репозиторий на локальную машину
|
||||||
|
0
README.rst
Normal file
0
README.rst
Normal file
@ -15,8 +15,6 @@ from pathlib import Path
|
|||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
|
|
||||||
|
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
# Quick-start development settings - unsuitable for production
|
# Quick-start development settings - unsuitable for production
|
||||||
@ -29,7 +27,7 @@ load_dotenv()
|
|||||||
SECRET_KEY = os.getenv('ACTRL_SECRET_KEY', 'empty')
|
SECRET_KEY = os.getenv('ACTRL_SECRET_KEY', 'empty')
|
||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = bool(int(os.getenv('ACTRL_DEBUG', '1')))
|
DEBUG = bool(int(os.getenv('ACTRL_DEBUG', '0')))
|
||||||
|
|
||||||
ALLOWED_HOSTS = [
|
ALLOWED_HOSTS = [
|
||||||
'127.0.0.1',
|
'127.0.0.1',
|
||||||
@ -94,13 +92,24 @@ WSGI_APPLICATION = 'access_controller.wsgi.application'
|
|||||||
|
|
||||||
# Database
|
# Database
|
||||||
# https://docs.djangoproject.com/en/3.1/ref/settings/#databases
|
# https://docs.djangoproject.com/en/3.1/ref/settings/#databases
|
||||||
|
if DEBUG:
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
'default': {
|
||||||
'ENGINE': 'django.db.backends.sqlite3',
|
'ENGINE': 'django.db.backends.sqlite3',
|
||||||
'NAME': BASE_DIR / 'db' / 'zd_db.sqlite3'
|
'NAME': BASE_DIR / 'db' / 'zd_db.sqlite3'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.postgresql',
|
||||||
|
'NAME': os.getenv('ACTRL_DB_NAME'),
|
||||||
|
'USER': os.getenv('ACTRL_DB_USER'),
|
||||||
|
'PASSWORD': os.getenv('ACTRL_DB_PASSWORD'),
|
||||||
|
'HOST': os.getenv('ACTRL_DB_HOST'),
|
||||||
|
'PORT': os.getenv('ACTRL_DB_PORT'),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
# Password validation
|
# Password validation
|
||||||
# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators
|
# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators
|
||||||
@ -137,10 +146,14 @@ USE_TZ = True
|
|||||||
# https://docs.djangoproject.com/en/3.1/howto/static-files/
|
# https://docs.djangoproject.com/en/3.1/howto/static-files/
|
||||||
|
|
||||||
STATIC_URL = '/static/'
|
STATIC_URL = '/static/'
|
||||||
STATIC_ROOT = os.path.join(BASE_DIR, 'staticroot')
|
STATIC_ROOT = BASE_DIR / 'build' / 'webserver' / 'static'
|
||||||
STATICFILES_DIRS = [
|
STATICFILES_DIRS = [
|
||||||
os.path.join(BASE_DIR, 'static'),
|
os.path.join(BASE_DIR, 'static'),
|
||||||
]
|
]
|
||||||
|
STATICFILES_FINDERS = [
|
||||||
|
'django.contrib.staticfiles.finders.FileSystemFinder',
|
||||||
|
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
|
||||||
|
]
|
||||||
|
|
||||||
ACCOUNT_ACTIVATION_DAYS = 7
|
ACCOUNT_ACTIVATION_DAYS = 7
|
||||||
|
|
||||||
|
@ -39,6 +39,7 @@ urlpatterns = [
|
|||||||
path('registration_failed/', registration_failed, name='registration_failed'),
|
path('registration_failed/', registration_failed, name='registration_failed'),
|
||||||
path('control/', AdminPageView.as_view(), name='control'),
|
path('control/', AdminPageView.as_view(), name='control'),
|
||||||
path('statistic/', statistic_page, name='statistic'),
|
path('statistic/', statistic_page, name='statistic'),
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
# Django REST
|
# Django REST
|
||||||
|
35
docker-compose.yml
Normal file
35
docker-compose.yml
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
image: gitlab.cazzzer.com:5050/cazzzer/zendesk-access-controller:alpine
|
||||||
|
env_file:
|
||||||
|
- .env.prod
|
||||||
|
volumes:
|
||||||
|
- static:/access_controller/srv
|
||||||
|
expose:
|
||||||
|
- 8000
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: postgres:13-alpine
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data/
|
||||||
|
expose:
|
||||||
|
- 5432
|
||||||
|
env_file:
|
||||||
|
- .env.prod.db
|
||||||
|
|
||||||
|
nginx:
|
||||||
|
build: ./nginx
|
||||||
|
volumes:
|
||||||
|
- static:/srv/access_controller
|
||||||
|
ports:
|
||||||
|
- 8004:80
|
||||||
|
depends_on:
|
||||||
|
- web
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
static:
|
@ -11,11 +11,15 @@
|
|||||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||||
#
|
#
|
||||||
import os
|
import os
|
||||||
|
import sphinx.events
|
||||||
import sys
|
import sys
|
||||||
import inspect
|
import inspect
|
||||||
import enchant
|
import enchant
|
||||||
import django
|
import django
|
||||||
|
|
||||||
|
|
||||||
|
sphinx.events.core_events['autodoc-process-signature'] = ''
|
||||||
|
|
||||||
sys.path.insert(0, os.path.abspath('../../'))
|
sys.path.insert(0, os.path.abspath('../../'))
|
||||||
|
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'access_controller.settings')
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'access_controller.settings')
|
||||||
@ -47,6 +51,8 @@ author = 'SHP S101, group 2'
|
|||||||
release = 'v0.01'
|
release = 'v0.01'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# -- General configuration ---------------------------------------------------
|
# -- General configuration ---------------------------------------------------
|
||||||
|
|
||||||
def process_django_models(app, what, name, obj, options, lines):
|
def process_django_models(app, what, name, obj, options, lines):
|
||||||
@ -112,8 +118,9 @@ def skip_queryset(app, what, name, obj, skip, options):
|
|||||||
return skip
|
return skip
|
||||||
|
|
||||||
|
|
||||||
def fix_sig(app, what, name, obj, options, signature, return_annotation):
|
def process_signature(app, what: str, name: str, obj, options, signature, return_annotation):
|
||||||
return "", ""
|
if not callable(obj):
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
def setup(app):
|
def setup(app):
|
||||||
@ -121,7 +128,7 @@ def setup(app):
|
|||||||
app.connect('autodoc-process-docstring', process_django_models)
|
app.connect('autodoc-process-docstring', process_django_models)
|
||||||
app.connect('autodoc-skip-member', skip_queryset)
|
app.connect('autodoc-skip-member', skip_queryset)
|
||||||
app.connect('autodoc-process-docstring', process_modules)
|
app.connect('autodoc-process-docstring', process_modules)
|
||||||
app.connect("autodoc-process-signature", fix_sig)
|
app.connect("autodoc-process-signature", process_signature)
|
||||||
|
|
||||||
|
|
||||||
# Add any Sphinx extension module names here, as strings. They can be
|
# Add any Sphinx extension module names here, as strings. They can be
|
||||||
|
@ -1,7 +1,23 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
m2r README.md
|
retry() {
|
||||||
cd docs
|
retries=$1
|
||||||
make html
|
shift
|
||||||
cd ..
|
|
||||||
|
count=0
|
||||||
|
until "$@"; do
|
||||||
|
exit=$?
|
||||||
|
count=$((count + 1))
|
||||||
|
if [ $count -lt $retries ]; then
|
||||||
|
echo "Retry $count/$retries exited $exit, retrying..."
|
||||||
|
else
|
||||||
|
echo "Retry $count/$retries exited $exit, no more retries left."
|
||||||
|
return $exit
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
m2r README.md --overwrite
|
||||||
|
sphinx-build -b html docs/source build/webserver/docs
|
||||||
rm README.rst
|
rm README.rst
|
||||||
|
8
main/control_page_js_modules/.babelrc
Normal file
8
main/control_page_js_modules/.babelrc
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"presets": ["@babel/preset-env", "@babel/preset-react"],
|
||||||
|
"plugins": [
|
||||||
|
["@babel/plugin-transform-runtime", {
|
||||||
|
"regenerator": true
|
||||||
|
}], "@babel/plugin-syntax-jsx"
|
||||||
|
]
|
||||||
|
}
|
98
main/control_page_js_modules/__tests__/control.test.js
Normal file
98
main/control_page_js_modules/__tests__/control.test.js
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import React from "react";
|
||||||
|
import {render, unmountComponentAtNode} from "react-dom";
|
||||||
|
import {act} from "react-dom/test-utils";
|
||||||
|
import {Table} from "../src/control";
|
||||||
|
import * as test_data from "./test_users.json"
|
||||||
|
import axios from "axios";
|
||||||
|
import MockAdapter from "axios-mock-adapter";
|
||||||
|
|
||||||
|
let mock = null
|
||||||
|
let container = null
|
||||||
|
beforeEach(() => {
|
||||||
|
mock = new MockAdapter(axios);
|
||||||
|
mock.onGet('/api/users').reply(200, test_data)
|
||||||
|
container = document.createElement('div')
|
||||||
|
container.id = "table"
|
||||||
|
document.body.appendChild(container)
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
unmountComponentAtNode(container);
|
||||||
|
mock.restore()
|
||||||
|
container.remove();
|
||||||
|
container = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("has no rows without axios request", () => {
|
||||||
|
act(() => {
|
||||||
|
render(<Table/>, container);
|
||||||
|
});
|
||||||
|
let tbody = container.querySelector("#tbody")
|
||||||
|
expect(tbody.getElementsByTagName('tr').length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows valid number of free workplaces", async () => {
|
||||||
|
await act(async () => {
|
||||||
|
render(<Table/>, container)
|
||||||
|
})
|
||||||
|
let element = container.querySelector('#licences_remaining')
|
||||||
|
let licences = Number(element.innerHTML.replace(/Свободных мест: /, ''))
|
||||||
|
expect(licences).toEqual(1)
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Pretext must be deleted on render", () => {
|
||||||
|
act(() => {
|
||||||
|
render(<Table/>, container)
|
||||||
|
})
|
||||||
|
expect(document.body).not.toContain(container.querySelector('#loading'))
|
||||||
|
});
|
||||||
|
|
||||||
|
it("has valid number of table rows with axios request", async () => {
|
||||||
|
await act(async () => {
|
||||||
|
render(<Table/>, container)
|
||||||
|
})
|
||||||
|
let tbody = container.querySelector("#tbody")
|
||||||
|
expect(tbody.getElementsByTagName('tr').length)
|
||||||
|
.toEqual(test_data.users.length + test_data.zendesk_users.length)
|
||||||
|
});
|
||||||
|
|
||||||
|
it("show valid number for engineers and light agents", async () => {
|
||||||
|
await act(async () => {
|
||||||
|
render(<Table/>, container)
|
||||||
|
})
|
||||||
|
let engineers = container.querySelector('#engineers')
|
||||||
|
let agents = container.querySelector('#agents')
|
||||||
|
expect(Number(engineers.textContent)).toEqual(test_data.engineers)
|
||||||
|
expect(Number(agents.textContent)).toEqual(test_data.light_agents)
|
||||||
|
});
|
||||||
|
|
||||||
|
it("called one request on mount", async () => {
|
||||||
|
let requests = jest.spyOn(Table.prototype, "getUsers")
|
||||||
|
await act(async () => {
|
||||||
|
render(<Table/>, container)
|
||||||
|
})
|
||||||
|
expect(requests).toHaveBeenCalledTimes(1)
|
||||||
|
requests.mockRestore()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("checkbox count equals users from db count", async () => {
|
||||||
|
await act(async () => {
|
||||||
|
render(<Table/>, container)
|
||||||
|
})
|
||||||
|
let tbody = container.querySelector("#tbody")
|
||||||
|
let checkboxes = tbody.querySelectorAll("input[type='checkbox']")
|
||||||
|
let users = test_data.users
|
||||||
|
expect(checkboxes.length).toEqual(users.length)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("requests occur every one minute", async () => {
|
||||||
|
jest.useFakeTimers()
|
||||||
|
let requests = jest.spyOn(Table.prototype, "getUsers")
|
||||||
|
await act(async () => {
|
||||||
|
render(<Table/>, container)
|
||||||
|
})
|
||||||
|
jest.advanceTimersByTime(60000)
|
||||||
|
expect(requests).toHaveBeenCalledTimes(2)
|
||||||
|
jest.useRealTimers()
|
||||||
|
requests.mockRestore()
|
||||||
|
})
|
32
main/control_page_js_modules/__tests__/test_users.json
Normal file
32
main/control_page_js_modules/__tests__/test_users.json
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"users": [
|
||||||
|
{
|
||||||
|
"user": {
|
||||||
|
"email": "123@test.ru"
|
||||||
|
},
|
||||||
|
"id": 2,
|
||||||
|
"name": "UserForAccessTest",
|
||||||
|
"zendesk_role": "light_agent"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"engineers": 2,
|
||||||
|
"light_agents": 2,
|
||||||
|
"zendesk_users": [
|
||||||
|
{
|
||||||
|
"name": "Степаненко Ольга s101",
|
||||||
|
"zendesk_role": "engineer",
|
||||||
|
"email": "stepanenko_olga@mail.ru"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "TEST",
|
||||||
|
"zendesk_role": "engineer",
|
||||||
|
"email": "akovalev1305@gmail.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Vasua",
|
||||||
|
"zendesk_role": "light_agent",
|
||||||
|
"email": "krav-88@mail.ru"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"max_agents": 3
|
||||||
|
}
|
9
main/control_page_js_modules/jest.config.js
Normal file
9
main/control_page_js_modules/jest.config.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
module.exports = {
|
||||||
|
verbose: true,
|
||||||
|
testPathIgnorePatterns: [
|
||||||
|
"./node_modules/"
|
||||||
|
],
|
||||||
|
roots: [
|
||||||
|
"./__tests__"
|
||||||
|
],
|
||||||
|
}
|
17600
main/control_page_js_modules/package-lock.json
generated
Normal file
17600
main/control_page_js_modules/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
main/control_page_js_modules/package.json
Normal file
28
main/control_page_js_modules/package.json
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "control_page",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "../static/main/js/control_page/dist/index_bundle.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "jest"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/cli": "^7.13.16",
|
||||||
|
"@babel/core": "^7.13.16",
|
||||||
|
"@babel/plugin-transform-runtime": "^7.13.15",
|
||||||
|
"@babel/preset-env": "^7.13.15",
|
||||||
|
"@babel/preset-react": "^7.13.13",
|
||||||
|
"axios": "^0.21.1",
|
||||||
|
"axios-mock-adapter": "^1.19.0",
|
||||||
|
"babel-loader": "^8.2.2",
|
||||||
|
"jest": "^26.6.3",
|
||||||
|
"jsx": "^0.9.89",
|
||||||
|
"react": "^17.0.2",
|
||||||
|
"react-dom": "^17.0.2",
|
||||||
|
"save-dev": "0.0.1-security",
|
||||||
|
"webpack": "^5.36.2",
|
||||||
|
"webpack-cli": "^4.6.0"
|
||||||
|
}
|
||||||
|
}
|
183
main/control_page_js_modules/src/control.js
Normal file
183
main/control_page_js_modules/src/control.js
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
import React from "react";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
function FreeWorkplaces(props) {
|
||||||
|
return (
|
||||||
|
<div className="new-section">
|
||||||
|
<p className="row page-description" id="licences_remaining">Свободных мест: {props.count}</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function WorkersCount(props) {
|
||||||
|
return (
|
||||||
|
<div className="row justify-content-center new-section d-flex align-items-center">
|
||||||
|
<div className="col-5">
|
||||||
|
<div className="info">
|
||||||
|
<div className="info-row">
|
||||||
|
<div className="info-target px-4">Инженеров:</div>
|
||||||
|
<div className="info-quantity">
|
||||||
|
<div className="status-circle-small light-green"></div>
|
||||||
|
<span className="info-quantity-value" id="engineers">{props.engineers}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="info-row">
|
||||||
|
<div className="info-target px-4">Легких агентов:</div>
|
||||||
|
<div className="info-quantity">
|
||||||
|
<div className="status-circle-small light-yellow"></div>
|
||||||
|
<span className="info-quantity-value" id="agents">{props.light_agents}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-5">
|
||||||
|
<button type="submit" name="engineer" className="btn default-button btn-warning btn-block btn-sm py-3">
|
||||||
|
Назначить выбранных на роль инженера
|
||||||
|
</button>
|
||||||
|
<button type="submit" name="light_agent" className="btn default-button btn-warning btn-block btn-sm py-3">
|
||||||
|
Назначить выбранных на роль легкого агента
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
class ModelUserTableRow extends React.Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<tr className={"table-dark"}>
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
value={this.props.user.id}
|
||||||
|
className="form-check-input"
|
||||||
|
name="users"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="#">{this.props.user.name}</a>
|
||||||
|
</td>
|
||||||
|
<td>{this.props.user.user.email}</td>
|
||||||
|
<td>{this.props.user.zendesk_role}</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ModelUserTableRows extends React.Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
this.props.users.map((user, key) => (
|
||||||
|
<ModelUserTableRow user={user} key={key} />
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ZendeskUserTableRow extends React.Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<tr className={"table-secondary text-secondary"}>
|
||||||
|
<td></td>
|
||||||
|
<td>
|
||||||
|
<a href="#" style={{ color: "grey", fontStyle: "italic" }}>
|
||||||
|
{this.props.user.name}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td style={{ color: "grey", fontStyle: "italic" }}>
|
||||||
|
{this.props.user.email}
|
||||||
|
</td>
|
||||||
|
<td style={{ color: "grey", fontStyle: "italic" }}>
|
||||||
|
{this.props.user.zendesk_role}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ZendeskUserTableRows extends React.Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
this.props.users.map((user, key) => (
|
||||||
|
<ZendeskUserTableRow user={user} key={key} />
|
||||||
|
))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Table extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
users: [],
|
||||||
|
engineers: null,
|
||||||
|
light_agents: null,
|
||||||
|
zendesk_users: [],
|
||||||
|
max_agents: null,
|
||||||
|
renderLoad: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUsers() {
|
||||||
|
await axios.get("/api/users").then((response) => {
|
||||||
|
this.setState({
|
||||||
|
users: response.data.users,
|
||||||
|
engineers: response.data.engineers,
|
||||||
|
light_agents: response.data.light_agents,
|
||||||
|
zendesk_users: response.data.zendesk_users,
|
||||||
|
max_agents: response.data.max_agents,
|
||||||
|
renderLoad: false
|
||||||
|
});
|
||||||
|
return response
|
||||||
|
}).catch(reason => {
|
||||||
|
console.log(reason)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.getUsers().then(() => {})
|
||||||
|
.catch(reason => {
|
||||||
|
console.log(reason)
|
||||||
|
});
|
||||||
|
this.interval = setInterval(() => {
|
||||||
|
this.getUsers().catch(reason => {
|
||||||
|
console.log(reason)
|
||||||
|
})
|
||||||
|
}, 60000);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
clearInterval(this.interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<FreeWorkplaces count={Math.max(this.state.max_agents - this.state.engineers, 0)}/>
|
||||||
|
<table className="table table-dark">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="form-check-input"
|
||||||
|
id="head-checkbox"
|
||||||
|
/>
|
||||||
|
</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Role</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="tbody">
|
||||||
|
<ModelUserTableRows users={this.state.users}/>
|
||||||
|
<ZendeskUserTableRows users={this.state.zendesk_users}/>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{this.state.renderLoad === true ? <p id="loading">Данные загружаются...</p> : null}
|
||||||
|
<WorkersCount engineers={this.state.engineers} light_agents={this.state.light_agents}/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
17
main/control_page_js_modules/src/index.js
Normal file
17
main/control_page_js_modules/src/index.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import {Table} from "./control"
|
||||||
|
import ReactDOM from "react-dom";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
|
||||||
|
function headCheckbox() {
|
||||||
|
let headCheckbox = document.getElementById("head-checkbox");
|
||||||
|
headCheckbox.addEventListener("click", () => {
|
||||||
|
let checkboxes = document.getElementsByName("users");
|
||||||
|
for (let checkbox of checkboxes)
|
||||||
|
checkbox.checked = headCheckbox.checked;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ReactDOM.render(<Table />, document.getElementById("table"));
|
||||||
|
headCheckbox();
|
31
main/control_page_js_modules/webpack.config.js
Normal file
31
main/control_page_js_modules/webpack.config.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
entry: './src/index.js',
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.(js)$/,
|
||||||
|
exclude: path.resolve(__dirname, 'node_modules/'),
|
||||||
|
use: {
|
||||||
|
loader: 'babel-loader',
|
||||||
|
options: {
|
||||||
|
presets: ['@babel/preset-env', "@babel/preset-react"],
|
||||||
|
plugins: [["@babel/plugin-transform-runtime", {"regenerator": true}]],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
extensions: [
|
||||||
|
'.js',
|
||||||
|
'.jsx'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
path: path.resolve('../../static/main/js/control_page', 'dist'),
|
||||||
|
filename: 'index_bundle.js'
|
||||||
|
},
|
||||||
|
mode: 'development',
|
||||||
|
}
|
@ -6,6 +6,8 @@ from datetime import timedelta, date
|
|||||||
from typing import Union, Optional
|
from typing import Union, Optional
|
||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.contrib.auth.models import Permission
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.core.handlers.wsgi import WSGIRequest
|
from django.core.handlers.wsgi import WSGIRequest
|
||||||
from django.http import HttpResponseRedirect, HttpResponsePermanentRedirect
|
from django.http import HttpResponseRedirect, HttpResponsePermanentRedirect
|
||||||
@ -123,6 +125,7 @@ def update_profile(user_profile: UserProfile) -> None:
|
|||||||
:return: Обновленный, в соответствие с текущими данными в Zendesk, профиль пользователя
|
:return: Обновленный, в соответствие с текущими данными в Zendesk, профиль пользователя
|
||||||
"""
|
"""
|
||||||
user = zenpy.get_user(user_profile.user.email)
|
user = zenpy.get_user(user_profile.user.email)
|
||||||
|
update_permission(user_profile, user)
|
||||||
user_profile.name = user.name
|
user_profile.name = user.name
|
||||||
user_profile.role = user.role
|
user_profile.role = user.role
|
||||||
user_profile.custom_role_id = user.custom_role_id if user.custom_role_id else 0
|
user_profile.custom_role_id = user.custom_role_id if user.custom_role_id else 0
|
||||||
@ -130,6 +133,52 @@ def update_profile(user_profile: UserProfile) -> None:
|
|||||||
user_profile.save()
|
user_profile.save()
|
||||||
|
|
||||||
|
|
||||||
|
def update_permission(user_profile: UserProfile, user: ZenpyUser):
|
||||||
|
"""
|
||||||
|
Функция обновляет права доступа пользователя в БД.
|
||||||
|
|
||||||
|
:param user_profile: Профиль пользователя
|
||||||
|
:param user: Данные пользователя в Zendesk
|
||||||
|
"""
|
||||||
|
if user_profile.role != user.role:
|
||||||
|
user_profile.role = user.role
|
||||||
|
user_profile.save()
|
||||||
|
set_permission(user_profile.user)
|
||||||
|
del_permission(user_profile.user)
|
||||||
|
|
||||||
|
|
||||||
|
def set_permission(user: get_user_model()) -> None:
|
||||||
|
"""
|
||||||
|
Функция дает разрешение на просмотр страница администратора, если пользователь имеет роль admin.
|
||||||
|
|
||||||
|
:param user: Авторизованный пользователь (получает разрешение, имея роль "admin")
|
||||||
|
"""
|
||||||
|
if user.userprofile.role == 'admin':
|
||||||
|
content_type = ContentType.objects.get_for_model(UserProfile)
|
||||||
|
permission = Permission.objects.get(
|
||||||
|
codename='has_control_access',
|
||||||
|
content_type=content_type,
|
||||||
|
)
|
||||||
|
user.user_permissions.add(permission)
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
|
||||||
|
def del_permission(user: get_user_model()) -> None:
|
||||||
|
"""
|
||||||
|
Функция забирает разрешение на просмотр страница администратора, если пользователь не имеет роль admin.
|
||||||
|
|
||||||
|
:param user: Авторизованный пользователь (теряет разрешение, не имея роль "admin")
|
||||||
|
"""
|
||||||
|
if user.userprofile.role == 'agent' and user.has_perm('main.has_control_access'):
|
||||||
|
content_type = ContentType.objects.get_for_model(UserProfile)
|
||||||
|
permission = Permission.objects.get(
|
||||||
|
codename='has_control_access',
|
||||||
|
content_type=content_type,
|
||||||
|
)
|
||||||
|
user.user_permissions.remove(permission)
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
|
||||||
def check_user_exist(email: str) -> bool:
|
def check_user_exist(email: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Функция проверяет, существует ли пользователь.
|
Функция проверяет, существует ли пользователь.
|
||||||
@ -180,6 +229,7 @@ def update_user_in_model(profile: UserProfile, zendesk_user: ZenpyUser) -> None:
|
|||||||
:param zendesk_user: Данные пользователя в Zendesk
|
:param zendesk_user: Данные пользователя в Zendesk
|
||||||
:return: Обновленный профиль пользователя
|
:return: Обновленный профиль пользователя
|
||||||
"""
|
"""
|
||||||
|
update_permission(profile, zendesk_user)
|
||||||
profile.name = zendesk_user.name
|
profile.name = zendesk_user.name
|
||||||
profile.role = zendesk_user.role
|
profile.role = zendesk_user.role
|
||||||
profile.image = zendesk_user.photo['content_url'] if zendesk_user.photo else None
|
profile.image = zendesk_user.photo['content_url'] if zendesk_user.photo else None
|
||||||
@ -262,6 +312,7 @@ class DatabaseHandler(logging.Handler):
|
|||||||
"""
|
"""
|
||||||
Класс записи изменений ролей в базу данных.
|
Класс записи изменений ролей в базу данных.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
logging.Handler.__init__(self)
|
logging.Handler.__init__(self)
|
||||||
|
|
||||||
@ -295,6 +346,7 @@ class CsvFormatter(logging.Formatter):
|
|||||||
"""
|
"""
|
||||||
Класс преобразования смены ролей пользователей в строковый формат.
|
Класс преобразования смены ролей пользователей в строковый формат.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
logging.Formatter.__init__(self)
|
logging.Formatter.__init__(self)
|
||||||
|
|
||||||
@ -346,7 +398,7 @@ def log(user: get_user_model(), admin: get_user_model() = None) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def set_session_params_for_work_page(request: WSGIRequest, count: int = None, is_confirm: bool = True) -> \
|
def set_session_params_for_work_page(request: WSGIRequest, count: int = None, is_confirm: bool = True) -> \
|
||||||
Union[HttpResponsePermanentRedirect, HttpResponseRedirect]:
|
Union[HttpResponsePermanentRedirect, HttpResponseRedirect]:
|
||||||
"""
|
"""
|
||||||
Функция для страницы получения прав, устанавливает данные сессии о успешности запроса и количестве
|
Функция для страницы получения прав, устанавливает данные сессии о успешности запроса и количестве
|
||||||
назначенных тикетов.
|
назначенных тикетов.
|
||||||
|
@ -9,6 +9,8 @@
|
|||||||
<title>{% block title %}{% endblock %}</title>
|
<title>{% block title %}{% endblock %}</title>
|
||||||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/5.0.0-alpha1/css/bootstrap.min.css"
|
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/5.0.0-alpha1/css/bootstrap.min.css"
|
||||||
integrity="sha384-r4NyP46KrjDleawBgD5tp8Y7UzmLA05oM1iAEQ17CSuDqnUK2+k9luXQOfXJCJ4I" crossorigin="anonymous">
|
integrity="sha384-r4NyP46KrjDleawBgD5tp8Y7UzmLA05oM1iAEQ17CSuDqnUK2+k9luXQOfXJCJ4I" crossorigin="anonymous">
|
||||||
|
<link rel="icon" href="{% static 'main/favs/favicon.ico'%}" type="image/x-icon">
|
||||||
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.bd-placeholder-img {
|
.bd-placeholder-img {
|
||||||
|
@ -42,6 +42,8 @@
|
|||||||
href="{{ work_url }}">Запрос прав</a>
|
href="{{ work_url }}">Запрос прав</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a class="btn btn-secondary" href="{% url 'logout' %}">Выйти</a>
|
<a class="btn btn-secondary" href="{% url 'logout' %}">Выйти</a>
|
||||||
|
<a class="btn btn-secondary"
|
||||||
|
href="https://actrl.cazzzer.com/docs/index.html"> Документация</a>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="btn-group" role="group" aria-label="Basic example" style="margin-right: 9px">
|
<div class="btn-group" role="group" aria-label="Basic example" style="margin-right: 9px">
|
||||||
@ -59,6 +61,9 @@
|
|||||||
class="btn btn-secondary"
|
class="btn btn-secondary"
|
||||||
{% endif %}
|
{% endif %}
|
||||||
href="{{ registration_url }}">Зарегистрироваться</a>
|
href="{{ registration_url }}">Зарегистрироваться</a>
|
||||||
|
{% url 'documentation' as documentation_url %}
|
||||||
|
<a class="btn btn-secondary"
|
||||||
|
href="{{ documentation_url }}">Документация</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</nav>
|
</nav>
|
||||||
|
@ -12,22 +12,12 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_scripts %}
|
{% block extra_scripts %}
|
||||||
<script src="https://unpkg.com/react@17/umd/react.development.js" crossorigin></script>
|
|
||||||
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js" crossorigin></script>
|
|
||||||
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
|
||||||
|
|
||||||
<script src="{% static 'main/js/control.js'%}" type="text/babel"></script>
|
|
||||||
<script src="{% static 'main/js/notifications.js' %}"></script> {# Для #}
|
<script src="{% static 'main/js/notifications.js' %}"></script> {# Для #}
|
||||||
<script src="{% static 'modules/notifications/dist/notifications.js' %}"></script> {# Уведомлений #}
|
<script src="{% static 'modules/notifications/dist/notifications.js' %}"></script> {# Уведомлений #}
|
||||||
{% endblock%}
|
{% endblock%}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container-md">
|
<div class="container-md">
|
||||||
|
|
||||||
<div class="new-section">
|
|
||||||
<p class="row page-description" id="licences_remaining">Свободных Мест:</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% for message in messages %}
|
{% for message in messages %}
|
||||||
<script>create_notification('{{message}}','','{{message.tags}}',2000)</script>
|
<script>create_notification('{{message}}','','{{message.tags}}',2000)</script>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@ -35,78 +25,21 @@
|
|||||||
{% block form %}
|
{% block form %}
|
||||||
<form method="post">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
<div class="row justify-content-center new-section">
|
<div class="row justify-content-center new-section">
|
||||||
|
|
||||||
<div class="col-10">
|
<div class="col-10">
|
||||||
<h3 class="py-4 text-center font-weight-bold">Список сотрудников</h3>
|
<h3 class="py-4 text-center font-weight-bold mb-0">Список сотрудников</h3>
|
||||||
|
|
||||||
{% block table %}
|
{% block table %}
|
||||||
<table class="table table-dark">
|
<div id="table"></div>
|
||||||
|
|
||||||
<thead>
|
|
||||||
<th>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
class="form-check-input"
|
|
||||||
id="head-checkbox"
|
|
||||||
/>
|
|
||||||
</th>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Email</th>
|
|
||||||
<th>Role</th>
|
|
||||||
</thead>
|
|
||||||
<tbody id="tbody"></tbody>
|
|
||||||
|
|
||||||
</table>
|
|
||||||
<p id="loading">Данные загружаются...</p>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% block count %}
|
|
||||||
<div class="row justify-content-center new-section">
|
|
||||||
<div class="col-5">
|
|
||||||
<div class="info">
|
|
||||||
|
|
||||||
<div class="info-row">
|
|
||||||
<div class="info-target px-4"><h6>Инженеров:</h6></div>
|
|
||||||
<div class="info-quantity">
|
|
||||||
<div class="status-circle-small light-green"></div>
|
|
||||||
<span class="info-quantity-value">{{ engineers }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-row">
|
|
||||||
<div class="info-target px-4"><h6>Легких агентов:</h6></div>
|
|
||||||
<div class="info-quantity">
|
|
||||||
<div class="status-circle-small light-yellow"></div>
|
|
||||||
<span class="info-quantity-value">{{ light_agents }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block buttons %}
|
|
||||||
<div class="col-5">
|
|
||||||
|
|
||||||
<button type="submit" name="engineer" class="btn default-button btn-warning btn-block btn-sm py-3">
|
|
||||||
Назначить выбранных на роль инженера
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button type="submit" name="light_agent" class="btn default-button btn-warning btn-block btn-sm py-3">
|
|
||||||
Назначить выбранных на роль легкого агента
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script src="{% static 'main/js/control_page/dist/index_bundle.js' %}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
252
main/templates/pages/documentation.html
Normal file
252
main/templates/pages/documentation.html
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html class="writer-html5" lang="ru" >
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
|
||||||
|
<title>Документация контроллера прав доступа — документация ZenDesk Access Controller v0.01</title>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="_static/css/theme.css" type="text/css" />
|
||||||
|
<link rel="stylesheet" href="_static/pygments.css" type="text/css" />
|
||||||
|
<link rel="stylesheet" href="_static/graphviz.css" type="text/css" />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!--[if lt IE 9]>
|
||||||
|
<script src="_static/js/html5shiv.min.js"></script>
|
||||||
|
<![endif]-->
|
||||||
|
|
||||||
|
|
||||||
|
<script type="text/javascript" id="documentation_options" data-url_root="./" src="_static/documentation_options.js"></script>
|
||||||
|
<script src="_static/jquery.js"></script>
|
||||||
|
<script src="_static/underscore.js"></script>
|
||||||
|
<script src="_static/doctools.js"></script>
|
||||||
|
<script src="_static/translations.js"></script>
|
||||||
|
|
||||||
|
<script type="text/javascript" src="_static/js/theme.js"></script>
|
||||||
|
|
||||||
|
|
||||||
|
<link rel="index" title="Алфавитный указатель" href="genindex.html" />
|
||||||
|
<link rel="search" title="Поиск" href="search.html" />
|
||||||
|
<link rel="next" title="Документация пользователя" href="overview.html" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="wy-body-for-nav">
|
||||||
|
|
||||||
|
|
||||||
|
<div class="wy-grid-for-nav">
|
||||||
|
|
||||||
|
<nav data-toggle="wy-nav-shift" class="wy-nav-side">
|
||||||
|
<div class="wy-side-scroll">
|
||||||
|
<div class="wy-side-nav-search" >
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<a href="#" class="icon icon-home"> ZenDesk Access Controller
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</a>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div role="search">
|
||||||
|
<form id="rtd-search-form" class="wy-form" action="search.html" method="get">
|
||||||
|
<input type="text" name="q" placeholder="Поиск в документации" />
|
||||||
|
<input type="hidden" name="check_keywords" value="yes" />
|
||||||
|
<input type="hidden" name="area" value="default" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="wy-menu wy-menu-vertical" data-spy="affix" role="navigation" aria-label="main navigation">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<p class="caption"><span class="caption-text">Contents:</span></p>
|
||||||
|
<ul>
|
||||||
|
<li class="toctree-l1"><a class="reference internal" href="overview.html">Документация пользователя</a></li>
|
||||||
|
<li class="toctree-l1"><a class="reference internal" href="code.html">Документация разработчика</a></li>
|
||||||
|
<li class="toctree-l1"><a class="reference internal" href="readme.html">READ.me</a></li>
|
||||||
|
<li class="toctree-l1"><a class="reference internal" href="todo.html">Что необходимо доделать?</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<section data-toggle="wy-nav-shift" class="wy-nav-content-wrap">
|
||||||
|
|
||||||
|
|
||||||
|
<nav class="wy-nav-top" aria-label="top navigation">
|
||||||
|
|
||||||
|
<i data-toggle="wy-nav-top" class="fa fa-bars"></i>
|
||||||
|
<a href="#">ZenDesk Access Controller</a>
|
||||||
|
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="wy-nav-content">
|
||||||
|
|
||||||
|
<div class="rst-content">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div role="navigation" aria-label="breadcrumbs navigation">
|
||||||
|
|
||||||
|
<ul class="wy-breadcrumbs">
|
||||||
|
|
||||||
|
<li><a href="#" class="icon icon-home"></a> »</li>
|
||||||
|
|
||||||
|
<li>Документация контроллера прав доступа</li>
|
||||||
|
|
||||||
|
|
||||||
|
<li class="wy-breadcrumbs-aside">
|
||||||
|
|
||||||
|
|
||||||
|
<a href="_sources/index.rst.txt" rel="nofollow"> Просмотреть исходный код страницы</a>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
|
||||||
|
<hr/>
|
||||||
|
</div>
|
||||||
|
<div role="main" class="document" itemscope="itemscope" itemtype="http://schema.org/Article">
|
||||||
|
<div itemprop="articleBody">
|
||||||
|
|
||||||
|
<div class="section" id="id1">
|
||||||
|
<h1>Документация контроллера прав доступа<a class="headerlink" href="#id1" title="Ссылка на этот заголовок">¶</a></h1>
|
||||||
|
<div class="toctree-wrapper compound">
|
||||||
|
<p class="caption"><span class="caption-text">Contents:</span></p>
|
||||||
|
<ul>
|
||||||
|
<li class="toctree-l1"><a class="reference internal" href="overview.html">Документация пользователя</a><ul>
|
||||||
|
<li class="toctree-l2"><a class="reference internal" href="overview.html#id2">Управление правами доступа</a></li>
|
||||||
|
<li class="toctree-l2"><a class="reference internal" href="overview.html#id3">Главная страница</a></li>
|
||||||
|
<li class="toctree-l2"><a class="reference internal" href="overview.html#id4">Регистрация</a></li>
|
||||||
|
<li class="toctree-l2"><a class="reference internal" href="overview.html#id5">Авторизация</a></li>
|
||||||
|
<li class="toctree-l2"><a class="reference internal" href="overview.html#id6">Профиль</a></li>
|
||||||
|
<li class="toctree-l2"><a class="reference internal" href="overview.html#id7">Запрос прав доступа</a></li>
|
||||||
|
<li class="toctree-l2"><a class="reference internal" href="overview.html#id8">Управление правами доступа администратором</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li class="toctree-l1"><a class="reference internal" href="code.html">Документация разработчика</a><ul>
|
||||||
|
<li class="toctree-l2"><a class="reference internal" href="code.html#module-main.models">Models</a></li>
|
||||||
|
<li class="toctree-l2"><a class="reference internal" href="code.html#module-main.forms">Forms</a></li>
|
||||||
|
<li class="toctree-l2"><a class="reference internal" href="code.html#module-main.extra_func">Extra Functions</a></li>
|
||||||
|
<li class="toctree-l2"><a class="reference internal" href="code.html#module-main.serializers">Serializers</a></li>
|
||||||
|
<li class="toctree-l2"><a class="reference internal" href="code.html#module-main.views">Views</a></li>
|
||||||
|
<li class="toctree-l2"><a class="reference internal" href="code.html#module-main.requester">Обработка тикетов</a></li>
|
||||||
|
<li class="toctree-l2"><a class="reference internal" href="code.html#module-main.statistic_data">Обработка статистики</a></li>
|
||||||
|
<li class="toctree-l2"><a class="reference internal" href="code.html#module-main.zendesk_admin">Функционал администратора Zendesk</a></li>
|
||||||
|
<li class="toctree-l2"><a class="reference internal" href="code.html#module-main.tests">Тесты</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li class="toctree-l1"><a class="reference internal" href="readme.html">READ.me</a></li>
|
||||||
|
<li class="toctree-l1"><a class="reference internal" href="todo.html">Что необходимо доделать?</a><ul>
|
||||||
|
<li class="toctree-l2"><a class="reference internal" href="todo.html#todos">TODOs</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section" id="indices-and-tables">
|
||||||
|
<h1>Indices and tables<a class="headerlink" href="#indices-and-tables" title="Ссылка на этот заголовок">¶</a></h1>
|
||||||
|
<ul class="simple">
|
||||||
|
<li><p><a class="reference internal" href="genindex.html"><span class="std std-ref">Index</span></a></p></li>
|
||||||
|
<li><p><a class="reference internal" href="py-modindex.html"><span class="std std-ref">Module Index</span></a></p></li>
|
||||||
|
<li><p><a class="reference internal" href="search.html"><span class="std std-ref">Search Page</span></a></p></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<footer>
|
||||||
|
<div class="rst-footer-buttons" role="navigation" aria-label="footer navigation">
|
||||||
|
<a href="overview.html" class="btn btn-neutral float-right" title="Документация пользователя" accesskey="n" rel="next">Следующая <span class="fa fa-arrow-circle-right" aria-hidden="true"></span></a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr/>
|
||||||
|
|
||||||
|
<div role="contentinfo">
|
||||||
|
<p>
|
||||||
|
© Copyright 2021, SHP S101, group 2.
|
||||||
|
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Собрано при помощи <a href="https://www.sphinx-doc.org/">Sphinx</a> с использованием
|
||||||
|
|
||||||
|
<a href="https://github.com/readthedocs/sphinx_rtd_theme">темы,</a>
|
||||||
|
|
||||||
|
предоставленной <a href="https://readthedocs.org">Read the Docs</a>.
|
||||||
|
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
jQuery(function () {
|
||||||
|
SphinxRtdTheme.Navigation.enable(true);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -88,7 +88,11 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td scope="col"> </td>
|
<td scope="col"> </td>
|
||||||
{% for date in log_stats.keys %}
|
{% for date in log_stats.keys %}
|
||||||
|
{% if interval == 'days' %}
|
||||||
<td scope="col">{{ date | date:'d.m' }}</td>
|
<td scope="col">{{ date | date:'d.m' }}</td>
|
||||||
|
{% else %}
|
||||||
|
<td scope="col">{{ date.1 | date:'F' }}</td>
|
||||||
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
@ -11,6 +11,7 @@ from django.core import mail
|
|||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.test import TestCase, Client
|
from django.test import TestCase, Client
|
||||||
|
|
||||||
from django.urls import reverse, reverse_lazy
|
from django.urls import reverse, reverse_lazy
|
||||||
from django.utils import translation, timezone
|
from django.utils import translation, timezone
|
||||||
|
|
||||||
@ -896,3 +897,24 @@ class LoggingTestCase(UsersBaseTestCase):
|
|||||||
file_output = self.get_file_output()
|
file_output = self.get_file_output()
|
||||||
self.assertEqual(file_output, f'UserForAccessTest,light_agent,'
|
self.assertEqual(file_output, f'UserForAccessTest,light_agent,'
|
||||||
f'{str(timezone.now().today())[:16]},UserForAccessTest\n')
|
f'{str(timezone.now().today())[:16]},UserForAccessTest\n')
|
||||||
|
|
||||||
|
|
||||||
|
class ControlAccessTests(TestCase):
|
||||||
|
"""
|
||||||
|
Класс тестов для проверки доступа к странице управления
|
||||||
|
"""
|
||||||
|
fixtures = ['fixtures/data.json']
|
||||||
|
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self.User = get_user_model()
|
||||||
|
self.client = Client()
|
||||||
|
|
||||||
|
def test_admin_has_perm(self):
|
||||||
|
self.client.force_login(self.User.objects.get(email='admin@gmail.com'))
|
||||||
|
self.response = self.client.get(reverse('control'))
|
||||||
|
self.assertEqual(self.response.status_code, 200)
|
||||||
|
|
||||||
|
def test_engineer_doesnt_have_perm(self):
|
||||||
|
self.client.force_login(self.User.objects.get(email='123@test.ru'))
|
||||||
|
self.response = self.client.get(reverse('control'))
|
||||||
|
self.assertEqual(self.response.status_code, 403)
|
||||||
|
@ -29,7 +29,7 @@ from rest_framework.response import Response
|
|||||||
from access_controller.settings import DEFAULT_FROM_EMAIL, ZENDESK_ROLES, ZENDESK_MAX_AGENTS, ZENDESK_GROUPS
|
from access_controller.settings import DEFAULT_FROM_EMAIL, ZENDESK_ROLES, ZENDESK_MAX_AGENTS, ZENDESK_GROUPS
|
||||||
from main.extra_func import check_user_exist, update_profile, get_user_organization, \
|
from main.extra_func import check_user_exist, update_profile, get_user_organization, \
|
||||||
make_engineer, make_light_agent, get_users_list, update_users_in_model, count_users, \
|
make_engineer, make_light_agent, get_users_list, update_users_in_model, count_users, \
|
||||||
set_session_params_for_work_page, get_tickets_list_for_group
|
set_session_params_for_work_page, get_tickets_list_for_group, set_permission
|
||||||
from main.forms import AdminPageUsers, CustomRegistrationForm, CustomAuthenticationForm, \
|
from main.forms import AdminPageUsers, CustomRegistrationForm, CustomAuthenticationForm, \
|
||||||
StatisticForm, WorkGetTicketsForm
|
StatisticForm, WorkGetTicketsForm
|
||||||
from main.serializers import ProfileSerializer, ZendeskUserSerializer
|
from main.serializers import ProfileSerializer, ZendeskUserSerializer
|
||||||
@ -118,7 +118,7 @@ class CustomRegistrationView(RegistrationView):
|
|||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
update_profile(user.userprofile)
|
update_profile(user.userprofile)
|
||||||
self.set_permission(user)
|
set_permission(user)
|
||||||
forms.save(**opts)
|
forms.save(**opts)
|
||||||
return user
|
return user
|
||||||
except SMTPException:
|
except SMTPException:
|
||||||
@ -131,21 +131,6 @@ class CustomRegistrationView(RegistrationView):
|
|||||||
self.redirect_url = 'invalid_zendesk_email'
|
self.redirect_url = 'invalid_zendesk_email'
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def set_permission(user: get_user_model()) -> None:
|
|
||||||
"""
|
|
||||||
Функция дает разрешение на просмотр страница администратора, если пользователь имеет роль admin.
|
|
||||||
|
|
||||||
:param user: Авторизованный пользователь (получает разрешение, имея роль "admin")
|
|
||||||
"""
|
|
||||||
if user.userprofile.role == 'admin':
|
|
||||||
content_type = ContentType.objects.get_for_model(UserProfile)
|
|
||||||
permission = Permission.objects.get(
|
|
||||||
codename='has_control_access',
|
|
||||||
content_type=content_type,
|
|
||||||
)
|
|
||||||
user.user_permissions.add(permission)
|
|
||||||
|
|
||||||
def get_success_url(self, user: get_user_model() = None) -> Dict:
|
def get_success_url(self, user: get_user_model() = None) -> Dict:
|
||||||
"""
|
"""
|
||||||
Функция возвращает url-адрес страницы, куда нужно перейти после успешной/не успешной регистрации.
|
Функция возвращает url-адрес страницы, куда нужно перейти после успешной/не успешной регистрации.
|
||||||
@ -447,6 +432,7 @@ def statistic_page(request: WSGIRequest) -> HttpResponse:
|
|||||||
context['errors'] = data.errors
|
context['errors'] = data.errors
|
||||||
if data.warnings:
|
if data.warnings:
|
||||||
context['warnings'] = data.warnings
|
context['warnings'] = data.warnings
|
||||||
|
context['interval'] = data.interval
|
||||||
context['log_stats'] = stats if not context['errors'] else None
|
context['log_stats'] = stats if not context['errors'] else None
|
||||||
elif request.method == 'GET':
|
elif request.method == 'GET':
|
||||||
form = StatisticForm()
|
form = StatisticForm()
|
||||||
@ -458,3 +444,5 @@ def registration_failed(request: WSGIRequest) -> HttpResponse:
|
|||||||
Функция отображения страницы "Регистрация закрыта".
|
Функция отображения страницы "Регистрация закрыта".
|
||||||
"""
|
"""
|
||||||
return render(request, 'pages/registration_failed.html')
|
return render(request, 'pages/registration_failed.html')
|
||||||
|
|
||||||
|
|
||||||
|
4
nginx/Dockerfile
Normal file
4
nginx/Dockerfile
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
FROM nginx:1.21-alpine
|
||||||
|
|
||||||
|
RUN rm /etc/nginx/conf.d/default.conf
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d
|
21
nginx/nginx.conf
Normal file
21
nginx/nginx.conf
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
upstream actrl {
|
||||||
|
server web:8000;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
|
||||||
|
listen 80;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://actrl;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_redirect off;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ /(static|docs)/ {
|
||||||
|
root /srv/access_controller/;
|
||||||
|
autoindex off;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
# Production specific dependencies
|
# Production specific dependencies
|
||||||
-r common.txt
|
-r common.txt
|
||||||
|
|
||||||
|
psycopg2==2.8.6
|
||||||
daphne==3.0.2
|
daphne==3.0.2
|
||||||
Twisted[tls,http2]==21.2.0
|
Twisted[tls,http2]==21.2.0
|
||||||
|
6
start.sh
6
start.sh
@ -1,6 +0,0 @@
|
|||||||
cd /access_controller/
|
|
||||||
|
|
||||||
python manage.py migrate
|
|
||||||
|
|
||||||
python manage.py collectstatic --noinput
|
|
||||||
daphne -b 0.0.0.0 access_controller.asgi:application
|
|
BIN
static/main/favs/favicon.ico
Normal file
BIN
static/main/favs/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 318 B |
@ -1,138 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
|
|
||||||
function head_checkbox() {
|
|
||||||
let head_checkbox = document.getElementById("head-checkbox");
|
|
||||||
head_checkbox.addEventListener("click", () => {
|
|
||||||
let checkboxes = document.getElementsByName("users");
|
|
||||||
for (let checkbox of checkboxes) checkbox.click();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// React
|
|
||||||
class ModelUserTableRow extends React.Component {
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<tr className={"table-dark"}>
|
|
||||||
<td>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
value={this.props.user.id}
|
|
||||||
className="form-check-input"
|
|
||||||
name="users"
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td>{this.props.user.name}</td>
|
|
||||||
<td>{this.props.user.user.email}</td>
|
|
||||||
<td>{this.props.user.zendesk_role}</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ModelUserTableRows extends React.Component {
|
|
||||||
render() {
|
|
||||||
return ReactDOM.createPortal(
|
|
||||||
this.props.users.map((user, key) => (
|
|
||||||
<ModelUserTableRow user={user} key={key} />
|
|
||||||
)),
|
|
||||||
document.getElementById("tbody")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ZendeskUserTableRow extends React.Component {
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<tr className={"table-secondary text-secondary"}>
|
|
||||||
<td></td>
|
|
||||||
<td>
|
|
||||||
<a href="#" style={{ color: "grey", fontStyle: "italic" }}>
|
|
||||||
{this.props.user.name}
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td style={{ color: "grey", fontStyle: "italic" }}>
|
|
||||||
{this.props.user.email}
|
|
||||||
</td>
|
|
||||||
<td style={{ color: "grey", fontStyle: "italic" }}>
|
|
||||||
{this.props.user.zendesk_role}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ZendeskUserTableRows extends React.Component {
|
|
||||||
render() {
|
|
||||||
return ReactDOM.createPortal(
|
|
||||||
this.props.users.map((user, key) => (
|
|
||||||
<ZendeskUserTableRow user={user} key={key} />
|
|
||||||
)),
|
|
||||||
document.getElementById("tbody")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class TableBody extends React.Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
users: [],
|
|
||||||
engineers: 0,
|
|
||||||
light_agents: 0,
|
|
||||||
zendesk_users: [],
|
|
||||||
max_agents: 3,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
change_elemnts_html() {
|
|
||||||
let elements = document.querySelectorAll(".info-quantity-value");
|
|
||||||
let licences = document.getElementById("licences_remaining");
|
|
||||||
elements[0].innerHTML = this.state.engineers;
|
|
||||||
elements[1].innerHTML = this.state.light_agents;
|
|
||||||
let max_licences = Math.max(
|
|
||||||
this.state.max_agents - this.state.engineers,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
licences.innerHTML = "Свободных мест: " + max_licences;
|
|
||||||
}
|
|
||||||
|
|
||||||
async get_users() {
|
|
||||||
await axios.get("/api/users").then((response) => {
|
|
||||||
this.setState({
|
|
||||||
users: response.data.users,
|
|
||||||
engineers: response.data.engineers,
|
|
||||||
light_agents: response.data.light_agents,
|
|
||||||
zendesk_users: response.data.zendesk_users,
|
|
||||||
max_agents: response.data.max_agents,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
this.change_elemnts_html();
|
|
||||||
}
|
|
||||||
|
|
||||||
delete_pretext() {
|
|
||||||
document.getElementById("loading").remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.get_users().then(() => this.delete_pretext());
|
|
||||||
this.interval = setInterval(() => {
|
|
||||||
this.get_users();
|
|
||||||
}, 60000);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
clearInterval(this.interval);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<tr>
|
|
||||||
<ModelUserTableRows users={this.state.users} />
|
|
||||||
<ZendeskUserTableRows users={this.state.zendesk_users} />
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ReactDOM.render(<TableBody />, document.getElementById("tbody"));
|
|
||||||
head_checkbox();
|
|
0
static/main/js/control_page/.gitkeep
Normal file
0
static/main/js/control_page/.gitkeep
Normal file
Loading…
x
Reference in New Issue
Block a user