This commit is contained in:
Dmitriy Andreev 2021-04-15 19:03:44 +03:00
commit 82bddb8d82
38 changed files with 686 additions and 280 deletions

1
.gitignore vendored
View File

@ -12,6 +12,7 @@ local_settings.py
db.sqlite3 db.sqlite3
db.sqlite3-journal db.sqlite3-journal
media/ media/
logs/
# If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/ # If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/
# in your Git repository. Update and uncomment the following line accordingly. # in your Git repository. Update and uncomment the following line accordingly.

View File

@ -47,9 +47,79 @@ pip install -r requirements.txt
./manage.py loaddata data.json ./manage.py loaddata data.json
./manage.py runserver ./manage.py runserver
``` ```
Создать токен
Указать почту и токен в окружении ##ZenDesk Access Controller instruction for eng
##Перед запуском для тестирования:
Убедитесь, что вы зарегистрированы в песочнице ZenDesk, у вас назначена организация (SYSTEM)
Для админов ZenDesk дополнительно - создайте токен доступа в ZenDesk
При запуске в Docker убедитесь что папка, которая будет служить хранилищем для БД, открыта на запись и чтение
##Запуск на локальной машине:
скопировать репозиторий на локальную машину
перейти в папку приложения
активировать вирутальное окружение
выполнить команду pip install -r requirements.txt
в вирутальное окружение добавить следующие переменные :
ACCESS_CONTROLLER_API_EMAIL={EMAIL} - почта админа в ZenDesk
ACCESS_CONTROLLER_API_PASSWORD={PASSWORD} - пароль админа ZenDesk
ACCESS_CONTROLLER_API_TOKEN={API_TOKEN} - API токен зендеск
ZD_DOMAIN={DOMAIN} - домен ZenDesk
ENG_CROLE_ID={ENGINEER_CUSTOM_ROLE_ID} - id роли инженера( custom_role_id сотрдника смены)
LA_CROLE_ID={LIGHT_AGENT_CUSTOM_ROLE_ID} - id роли легкого агента (custom_role_id роли -легкий агент)
EMPL_GROUP={EMPLOYEE_GROUP_NAME} - имя группы которой принадлежат сотрудники ССКС
BUF_GROUP={BUFFER_GROUP_NAME} - имя буферной группы для передачи смен(через нее происходит управление тикетами)
ST_EMAIL={SOLVED_TICKETS_EMAIL} - почта на которую будут переназначятся закрытые тикеты
LICENSE_NO={LICENSE_NO} - количество лицензий, отображаемых как доступные в приложении
SHIFTH={SHIFT_HOURS} - количество часов в рабочей смене (нужно для статистики, пока не реализовано но требует указания значения)
выполнить команду python manage.py makemigrations
выполнить команду python manage.py migrate
запустить приложение командой python manage.py runserver (можно указать в параметрах для файла manage.py)
перейти по ссылке в консоли (вероятнее всего откроется по адресу http://127.0.0.1:8000/)
##Запуск в Docker:
Требуется установленный и настроеный Docker
скопировать репозиторий на локальную машину
в командной строке перейти в папку проекта
выполнить команду docker build .
выполнить команду docker images (нам нужен id созданного образа)
выполнить команду docker run -d -p 8000:8000 -e ACCESS_CONTROLLER_API_EMAIL={EMAIL} -e ACCESS_CONTROLLER_API_PASSWORD={PASSWORD}
...(перечисляем все параметры виртуального окружени разделяя их -e) -v {абсолютный путь к папке, в которой будет размещена база}:/zendesk-access-controller/db {id образа докера}
открываем запущеный контейнер в браузере (можно перейти по ссылке http://localhost:8000/)
##Запуск с тестовыми юзерами:
На локальной машине - перед запуском команды python manage.py runserver выполнить команду python manage.py loaddata data.json
Это создаст тестового админа и тестового пользователя в приложении для песочницы ZenDesk. Админ - admin@gmail.com / zendeskadmin , пользователь - 123@test.ru / zendeskuser .
Не сработает если домен песочницы отличается от ngenix1612197338 (на другом домене нужно будет создать сначала пользователей в песочнице с правами админа и легкого агента
с этими же почтами, назначить им организацию (SYSTEM))
##Параметры тестовой песочницы:
ACCESS_CONTROLLER_API_EMAIL={EMAIL} - почта админа в ZenDesk - взять у роководителя(если вы не админ)
ACCESS_CONTROLLER_API_PASSWORD={PASSWORD} - пароль админа ZenDesk - взять у роководителя(если вы не админ)
ACCESS_CONTROLLER_API_TOKEN={API_TOKEN} - API токен зендеск - взять у роководителя(если вы не админ)
ZD_DOMAIN=ngenix1612197338
ENG_CROLE_ID=360005209000
LA_CROLE_ID=360005208980
EMPL_GROUP=Поддержка
BUF_GROUP=Сменная группа
ST_EMAIL=d.krikov@ngenix.net
LICENSE_NO=3
SHIFTH=12
## Read more ## Read more
- Zenpy: [http://docs.facetoe.com.au](http://docs.facetoe.com.au) - Zenpy: [http://docs.facetoe.com.au](http://docs.facetoe.com.au)

View File

@ -9,8 +9,9 @@ https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/
import os import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'access_controller.settings') os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'access_controller.settings')
from django.core.asgi import get_asgi_application
application = get_asgi_application() application = get_asgi_application()

View File

@ -24,7 +24,7 @@ SECRET_KEY = 'v1i_fb$_jf2#1v_lcsbu&eon4u-os0^px=s^iycegdycqy&5)6'
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True DEBUG = True
ALLOWED_HOSTS = [] ALLOWED_HOSTS = ['127.0.0.1']
# Application definition # Application definition
@ -53,10 +53,10 @@ MIDDLEWARE = [
ROOT_URLCONF = 'access_controller.urls' ROOT_URLCONF = 'access_controller.urls'
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.mail.ru' EMAIL_HOST = 'smtp.gmail.com'
EMAIL_PORT = 2525 EMAIL_PORT = 587
EMAIL_USE_TLS = True EMAIL_USE_TLS = True
EMAIL_HOST_USER = 'djgr.02@mail.ru' EMAIL_HOST_USER = 'group02django@gmail.com'
EMAIL_HOST_PASSWORD = 'djangogroup02' EMAIL_HOST_PASSWORD = 'djangogroup02'
SERVER_EMAIL = EMAIL_HOST_USER SERVER_EMAIL = EMAIL_HOST_USER
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER DEFAULT_FROM_EMAIL = EMAIL_HOST_USER
@ -143,41 +143,7 @@ AUTHENTICATION_BACKENDS = [
# Logging system # Logging system
# https://docs.djangoproject.com/en/3.1/topics/logging/ # https://docs.djangoproject.com/en/3.1/topics/logging/
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}',
'style': '{',
},
'simple': {
'format': '{levelname} {message}',
'style': '{',
},
},
'handlers': {
'console': {
'level': 'INFO',
'class': 'logging.StreamHandler',
'formatter': 'simple'
},
'mail_admins': {
'level': 'ERROR',
'class': 'django.utils.log.AdminEmailHandler',
}
},
'loggers': {
'django': {
'handlers': ['console'],
'propagate': True,
},
'main.index': {
'handlers': ['console'],
'level': 'INFO',
}
}
}
ZENDESK_ROLES = { ZENDESK_ROLES = {
'engineer': 360005209000, 'engineer': 360005209000,

View File

@ -17,29 +17,31 @@ from django.contrib import admin
from django.contrib.auth import views as auth_views from django.contrib.auth import views as auth_views
from django.urls import path, include from django.urls import path, include
from main.views import main_page, profile_page, CustomRegistrationView, CustomLoginView from main.views import main_page, profile_page, CustomRegistrationView, CustomLoginView, registration_error
from main.views import work_page, work_hand_over, work_become_engineer, \ from main.views import work_page, work_hand_over, work_become_engineer, work_get_tickets, \
AdminPageView, statistic_page AdminPageView, statistic_page
from main.urls import router from main.urls import router
urlpatterns = [ urlpatterns = [
path('admin/', admin.site.urls, name='admin'), path('admin/', admin.site.urls, name='admin'),
path('', main_page, name='index'), path('', main_page, name='index'),
path('accounts/profile/', profile_page, name='profile'), path('accounts/profile/', profile_page, name='profile'),
path('accounts/register/', CustomRegistrationView.as_view(), name='registration'), path('accounts/register/', CustomRegistrationView.as_view(), name='registration'),
path('accounts/register/error/', registration_error, name='registration_email_error'),
path('accounts/login/', CustomLoginView.as_view(), name='login'), path('accounts/login/', CustomLoginView.as_view(), name='login'),
path('accounts/', include('django.contrib.auth.urls')), path('accounts/', include('django.contrib.auth.urls')),
path('accounts/', include('django_registration.backends.one_step.urls')), path('accounts/', include('django_registration.backends.one_step.urls')),
path('work/<int:id>', work_page, name="work"), path('work/<int:id>', work_page, name="work"),
path('work/hand_over/', work_hand_over, name="work_hand_over"), path('work/hand_over/', work_hand_over, name="work_hand_over"),
path('work/become_engineer/', work_become_engineer, name="work_become_engineer"), path('work/become_engineer/', work_become_engineer, name="work_become_engineer"),
path('work/get_tickets', work_get_tickets, name='work_get_tickets'),
path('accounts/', include('django_registration.backends.activation.urls')), path('accounts/', include('django_registration.backends.activation.urls')),
path('accounts/login/', include('django.contrib.auth.urls')), path('accounts/login/', include('django.contrib.auth.urls')),
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'),
] ]
urlpatterns += [ urlpatterns += [
path( path(
'password_reset/', 'password_reset/',
@ -47,7 +49,7 @@ urlpatterns += [
name='password_reset' name='password_reset'
), ),
path( path(
'password-reset/done/', 'password_reset/done/',
auth_views.PasswordResetDoneView.as_view(), auth_views.PasswordResetDoneView.as_view(),
name='password_reset_done' name='password_reset_done'
), ),

View File

@ -1,7 +1,7 @@
[ [
{ {
"model": "auth.user", "model": "auth.user",
"pk": 1, "pk": 3,
"fields": { "fields": {
"password": "pbkdf2_sha256$216000$gHBBCr1jBELf$ZkEDW3IEd8Wij7u8vkv+0Eze32CS01bcaYWhcD9OIC4=", "password": "pbkdf2_sha256$216000$gHBBCr1jBELf$ZkEDW3IEd8Wij7u8vkv+0Eze32CS01bcaYWhcD9OIC4=",
"last_login": null, "last_login": null,
@ -19,16 +19,16 @@
}, },
{ {
"model": "main.userprofile", "model": "main.userprofile",
"pk": 1, "pk": 3,
"fields": { "fields": {
"name": "ZendeskAdmin", "name": "ZendeskAdmin",
"user": 1, "user": 3,
"role": "admin" "role": "admin"
} }
}, },
{ {
"model": "auth.user", "model": "auth.user",
"pk": 2, "pk": 4,
"fields": { "fields": {
"password": "pbkdf2_sha256$216000$5qLJgrm2Quq9$KDBNNymVZXkUx0HKBPFst2m83kLe0egPBnkW7KnkORU=", "password": "pbkdf2_sha256$216000$5qLJgrm2Quq9$KDBNNymVZXkUx0HKBPFst2m83kLe0egPBnkW7KnkORU=",
"last_login": null, "last_login": null,
@ -46,10 +46,10 @@
}, },
{ {
"model": "main.userprofile", "model": "main.userprofile",
"pk": 2, "pk": 4,
"fields": { "fields": {
"name": "UserForAccessTest", "name": "UserForAccessTest",
"user": 2, "user": 4,
"role": "agent", "role": "agent",
"custom_role_id": "360005209000" "custom_role_id": "360005209000"
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

View File

@ -3,7 +3,7 @@
========================= =========================
****************************** ******************************
**Управление правами доступа** Управление правами доступа
****************************** ******************************
@ -22,7 +22,7 @@
* **"Войти"** - если Вы уже являетесь зарегистрированным пользователем * **"Войти"** - если Вы уже являетесь зарегистрированным пользователем
* **"Зарегистрироваться"** - при первом входе * **"Зарегистрироваться"** - при первом входе
.. image:: _static/main.png .. image:: _static/main_logout.png
Внимание! Для регистрации используется email с сайта Zendesk. Регистрация по каждому email Внимание! Для регистрации используется email с сайта Zendesk. Регистрация по каждому email
возможна один раз возможна один раз
@ -32,7 +32,7 @@
* **"Профиль"** - просмотреть свои данные и запросить права доступа * **"Профиль"** - просмотреть свои данные и запросить права доступа
* **"Запрос прав"** - получение прав для работы с тикетами или **"Управление"** - доступно для администратора и предоставляет возможность группового назначения ролей пользователям * **"Запрос прав"** - получение прав для работы с тикетами или **"Управление"** - доступно для администратора и предоставляет возможность группового назначения ролей пользователям
.. image:: _static/main_logined_agent.png .. image:: _static/main.png
************* *************
Регистрация Регистрация
@ -72,7 +72,11 @@
На странице запроса прав Вам доступна информация о количестве и списке работающих над тикетами сотрудников, На странице запроса прав Вам доступна информация о количестве и списке работающих над тикетами сотрудников,
а также возможность сдать и запросить права. а также возможность сдать и запросить права.
.. image:: _static/permission_request.png .. image:: _static/request.png
Успешное изменение прав:
.. image:: _static/role_change.png
****************************************** ******************************************
Управление правами доступа администратором Управление правами доступа администратором
@ -84,6 +88,10 @@
* Количество и список инженеров и легких агентов * Количество и список инженеров и легких агентов
* Возможность группового назначения прав с использованием чек-боксов * Возможность группового назначения прав с использованием чек-боксов
.. image:: _static/permission_management.png .. image:: _static/admin_manage.png
Изменение прав пользователей наглядно отразится в таблице пользователей:
.. image:: _static/admin_manage_done.png
.. |copy| unicode:: 0xA9 .. Школа программистов S101, группа 2. 2021гю .. |copy| unicode:: 0xA9 .. Школа программистов S101, группа 2. 2021гю

View File

@ -80,6 +80,8 @@ API
functions functions
Serializer Serializer
Serializers Serializers
Сериализатор
переадресации

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

0
logs/.gitkeep Normal file
View File

View File

@ -1,3 +1,4 @@
import logging
import os import os
from datetime import timedelta, datetime, date from datetime import timedelta, datetime, date
@ -7,6 +8,7 @@ from django.utils import timezone
from zenpy import Zenpy from zenpy import Zenpy
from zenpy.lib.exception import APIException from zenpy.lib.exception import APIException
from access_controller.settings import ZENDESK_ROLES as ROLES, ONE_DAY, ZENDESK_GROUPS, SOLVED_TICKETS_EMAIL from access_controller.settings import ZENDESK_ROLES as ROLES, ONE_DAY, ZENDESK_GROUPS, SOLVED_TICKETS_EMAIL
from main.models import UserProfile, RoleChangeLogs, UnassignedTicket, UnassignedTicketStatus from main.models import UserProfile, RoleChangeLogs, UnassignedTicket, UnassignedTicketStatus
@ -92,12 +94,12 @@ class ZendeskAdmin:
def get_group(self, name: str) -> str: def get_group(self, name: str) -> str:
""" """
Функция возвращает группы, к которым принадлежит пользователь. Функция возвращает группу по названию
:param name: Имя пользователя :param name: Имя пользователя
:return: Группы пользователя (в случае отсутствия None) :return: Группы пользователя (в случае отсутствия None)
""" """
groups = self.admin.search(name) groups = self.admin.search(name, type='group')
for group in groups: for group in groups:
return group return group
return None return None
@ -139,7 +141,7 @@ class ZendeskAdmin:
raise ValueError('invalid access_controller`s login data') raise ValueError('invalid access_controller`s login data')
def update_role(user_profile: UserProfile, role: str) -> UserProfile: def update_role(user_profile: UserProfile, role: int) -> UserProfile:
""" """
Функция меняет роль пользователя. Функция меняет роль пользователя.
@ -162,12 +164,6 @@ def make_engineer(user_profile: UserProfile, who_changes: User) -> UserProfile:
:param user_profile: Профиль пользователя :param user_profile: Профиль пользователя
:return: Вызов функции **update_role** с параметрами: профиль пользователя, роль "engineer" :return: Вызов функции **update_role** с параметрами: профиль пользователя, роль "engineer"
""" """
RoleChangeLogs.objects.create(
user=user_profile.user,
old_role=user_profile.custom_role_id,
new_role=ROLES['engineer'],
changed_by=who_changes
)
update_role(user_profile, ROLES['engineer']) update_role(user_profile, ROLES['engineer'])
@ -191,13 +187,6 @@ def make_light_agent(user_profile: UserProfile, who_changes: User) -> UserProfil
ticket.assignee = None ticket.assignee = None
ticket.group = ZendeskAdmin().get_group(ZENDESK_GROUPS['buffer']) ticket.group = ZendeskAdmin().get_group(ZENDESK_GROUPS['buffer'])
ZendeskAdmin().admin.tickets.update(ticket) ZendeskAdmin().admin.tickets.update(ticket)
RoleChangeLogs.objects.create(
user=user_profile.user,
old_role=user_profile.custom_role_id,
new_role=ROLES['light_agent'],
changed_by=who_changes
)
update_role(user_profile, ROLES['light_agent']) update_role(user_profile, ROLES['light_agent'])
@ -274,7 +263,7 @@ def check_user_auth(email: str, password: str) -> bool:
return True return True
def update_user_in_model(profile: UserProfile, zendesk_user: User) -> UserProfile: def update_user_in_model(profile: UserProfile, zendesk_user: User):
""" """
Функция обновляет профиль пользователя при изменении данных пользователя на Zendesk. Функция обновляет профиль пользователя при изменении данных пользователя на Zendesk.
@ -381,6 +370,7 @@ class StatisticData:
:param statistic: Интервалы работы пользователя :param statistic: Интервалы работы пользователя
:type statistic: :class:`dict` :type statistic: :class:`dict`
""" """
def __init__(self, start_date, end_date, user_email, stat=None): def __init__(self, start_date, end_date, user_email, stat=None):
self.display = None self.display = None
self.interval = None self.interval = None
@ -543,31 +533,51 @@ class StatisticData:
first_log, last_log = self.data[0], self.data[len(self.data) - 1] first_log, last_log = self.data[0], self.data[len(self.data) - 1]
if first_log.old_role == ROLES['engineer']: if first_log.old_role == ROLES['engineer']:
self.statistic[first_log.change_time.date()] += get_timedelta(first_log).total_seconds() self.prev_engineer_logic(first_log)
if last_log.new_role == ROLES['engineer']: # TODO отдельная функция if last_log.new_role == ROLES['engineer']:
self.fill_daterange(last_log.change_time.date() + timedelta(days=1), self.end_date + timedelta(days=1)) self.post_engineer_logic(last_log)
if last_log.change_time.date() == timezone.now().date():
self.statistic[last_log.change_time.date()] += (
get_timedelta(None, timezone.now().time()) - get_timedelta(last_log)
).total_seconds()
else:
self.statistic[last_log.change_time.date()] += (
timedelta(days=1) - get_timedelta(last_log)).total_seconds()
if self.end_date == timezone.now().date():
self.statistic[self.end_date] = get_timedelta(None, timezone.now().time()).total_seconds()
for log_index in range(len(self.data) - 1): # TODO отдельная функция for log_index in range(len(self.data) - 1):
if self.data[log_index].new_role == ROLES['engineer']: if self.data[log_index].new_role == ROLES['engineer']:
current_log, next_log = self.data[log_index], self.data[log_index + 1] self.engineer_logic(log_index)
if current_log.change_time.date() != next_log.change_time.date():
self.statistic[current_log.change_time.date()] += ( def engineer_logic(self, log_index):
timedelta(days=1) - get_timedelta(current_log)).total_seconds() """
self.statistic[next_log.change_time.date()] += get_timedelta(next_log).total_seconds() Функция обрабатывает основную часть работы инженера
self.fill_daterange(current_log.change_time.date() + timedelta(days=1), next_log.change_time.date()) """
else: current_log, next_log = self.data[log_index], self.data[log_index + 1]
elapsed_time = next_log.change_time - current_log.change_time if current_log.change_time.date() != next_log.change_time.date():
self.statistic[current_log.change_time.date()] += elapsed_time.total_seconds() self.statistic[current_log.change_time.date()] += (
timedelta(days=1) - get_timedelta(current_log)).total_seconds()
self.statistic[next_log.change_time.date()] += get_timedelta(next_log).total_seconds()
self.fill_daterange(current_log.change_time.date() + timedelta(days=1), next_log.change_time.date())
else:
elapsed_time = next_log.change_time - current_log.change_time
self.statistic[current_log.change_time.date()] += elapsed_time.total_seconds()
def post_engineer_logic(self, last_log):
"""
Функция обрабатывает случай, когда нам изветсно что инженер работал и после диапазона
"""
self.fill_daterange(last_log.change_time.date() + timedelta(days=1), self.end_date + timedelta(days=1))
if last_log.change_time.date() == timezone.now().date():
self.statistic[last_log.change_time.date()] += (
get_timedelta(None, timezone.now().time()) - get_timedelta(last_log)
).total_seconds()
else:
self.statistic[last_log.change_time.date()] += (
timedelta(days=1) - get_timedelta(last_log)).total_seconds()
if self.end_date == timezone.now().date():
self.statistic[self.end_date] = get_timedelta(None, timezone.now().time()).total_seconds()
def prev_engineer_logic(self, first_log):
"""
Функция обрабатывает случай, когда нам изветсно что инженер начал работу до диапазона
"""
self.fill_daterange(max(User.objects.get(email=self.email).date_joined.date(), self.start_date),
first_log.change_time.date())
self.statistic[first_log.change_time.date()] += get_timedelta(first_log).total_seconds()
def fill_daterange(self, first: date, last: date, val: int = 24 * 3600) -> dict: def fill_daterange(self, first: date, last: date, val: int = 24 * 3600) -> dict:
""" """
@ -576,7 +586,6 @@ class StatisticData:
:param first: Начальная дата интервала :param first: Начальная дата интервала
:param last: Последняя дата интервала :param last: Последняя дата интервала
:param val: Количество секунд в одном дне :param val: Количество секунд в одном дне
:return: Статистику пользователя с указанным количеством секунд в заданных днях
""" """
for day in daterange(first, last): for day in daterange(first, last):
self.statistic[day] = val self.statistic[day] = val
@ -584,8 +593,75 @@ class StatisticData:
def clear_statistic(self) -> dict: def clear_statistic(self) -> dict:
""" """
Функция осуществляет обновление всех дней. Функция осуществляет обновление всех дней.
:return: Статистику пользователя с количеством рабочих секунд = 0
""" """
self.statistic.clear() self.statistic.clear()
self.fill_daterange(self.start_date, self.end_date + timedelta(days=1), 0) self.fill_daterange(self.start_date, self.end_date + timedelta(days=1), 0)
class DatabaseHandler(logging.Handler):
def __init__(self):
logging.Handler.__init__(self)
def emit(self, record):
database = RoleChangeLogs()
users = record.msg
if users[1]:
user = users[0]
admin = users[1]
elif not users[1]:
user = users[0]
admin = users[0]
database.name = user.name
database.user = user.user
database.changed_by = admin.user
if user.custom_role_id == ROLES['engineer']:
database.old_role = ROLES['light_agent']
elif user.custom_role_id == ROLES['light_agent']:
database.old_role = ROLES['engineer']
database.new_role = user.custom_role_id
database.save()
class CsvFormatter(logging.Formatter):
def __init__(self):
logging.Formatter.__init__(self)
def format(self, record):
users = record.msg
if users[1]:
user = users[0]
admin = users[1]
elif not users[1]:
user = users[0]
admin = users[0]
msg = ''
msg += user.name
if user.custom_role_id == ROLES['engineer']:
msg += ',engineer,'
elif user.custom_role_id == ROLES['light_agent']:
msg += ',light_agent,'
time = str(timezone.now().today())
msg += time[:16]
msg += ','
msg += admin.name
return msg
def log(user, admin=0):
"""
Осуществляет запись логов в базу данных и csv файл
:param admin:
:param user:
:return:
"""
users = [user, admin]
logger = logging.getLogger('MY_LOGGER')
if not logger.hasHandlers():
dbhandler = DatabaseHandler()
csvformatter = CsvFormatter()
csvhandler = logging.FileHandler('logs/logs.csv', "a")
csvhandler.setFormatter(csvformatter)
logger.addHandler(dbhandler)
logger.addHandler(csvhandler)
logger.setLevel('INFO')
logger.info(users)

View File

@ -2,7 +2,6 @@ from django import forms
from django.contrib.auth.forms import AuthenticationForm from django.contrib.auth.forms import AuthenticationForm
from django_registration.forms import RegistrationFormUniqueEmail from django_registration.forms import RegistrationFormUniqueEmail
from access_controller.settings import ZENDESK_ROLES
from main.models import UserProfile from main.models import UserProfile
@ -14,6 +13,7 @@ class CustomRegistrationForm(RegistrationFormUniqueEmail):
:param visible_fields.email: Поле для ввода email, зарегистрированного на Zendesk :param visible_fields.email: Поле для ввода email, зарегистрированного на Zendesk
:type visible_fields.email: :class:`django_registration.forms.RegistrationFormUniqueEmail` :type visible_fields.email: :class:`django_registration.forms.RegistrationFormUniqueEmail`
""" """
def __init__(self, *args, **kwargs) -> RegistrationFormUniqueEmail: def __init__(self, *args, **kwargs) -> RegistrationFormUniqueEmail:
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
for visible in self.visible_fields(): for visible in self.visible_fields():
@ -70,6 +70,16 @@ class CustomAuthenticationForm(AuthenticationForm):
} }
INTERVAL_CHOICES = [
('days', 'Дни'),
('months', 'Месяцы')
]
DISPLAY_CHOICES = [
('hours', 'Часы'),
('days', 'Дни/Смены')
]
class StatisticForm(forms.Form): class StatisticForm(forms.Form):
""" """
Форма отображения интервалов работы пользователя. Форма отображения интервалов работы пользователя.
@ -87,26 +97,47 @@ class StatisticForm(forms.Form):
""" """
email = forms.EmailField( email = forms.EmailField(
label='Электроная почта', label='Электроная почта',
) widget=forms.EmailInput(
interval = forms.CharField( # TODO: Переделать под html страницу
label='Интервал работы',
)
display_format = forms.CharField( # TODO: Переделать под html страницу
label='Формат отображения',
)
range_start = forms.DateField( # TODO: Переделать под html страницу
label='Начало диапазона',
widget=forms.DateInput(
attrs={ attrs={
'type': 'date', 'placeholder': 'example@ngenix.ru',
'class': 'form-control',
'style': 'background-color:#f2f2f2;'
} }
), ),
) )
range_end = forms.DateField( # TODO: Переделать под html страницу interval = forms.ChoiceField(
label='Конец диапазона', label='Выберите интервалы времени работы',
choices=INTERVAL_CHOICES,
widget=forms.RadioSelect(
attrs={
'class': 'btn-check',
}
)
)
display_format = forms.ChoiceField(
label='Выберите формат отображения',
choices=DISPLAY_CHOICES,
widget=forms.RadioSelect(
attrs={
'class': 'btn-check',
}
)
)
range_start = forms.DateField(
label='Начало статистики',
widget=forms.DateInput( widget=forms.DateInput(
attrs={ attrs={
'type': 'date', 'type': 'date',
'class': 'btn btn-secondary text-primary bg-white',
}
),
)
range_end = forms.DateField(
label='Конец статистики',
widget=forms.DateInput(
attrs={
'type': 'date',
'class': 'btn btn-secondary text-primary bg-white',
} }
), ),
) )

View File

@ -0,0 +1,26 @@
# Generated by Django 3.1.7 on 2021-04-08 16:43
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('main', '0016_merge_20210330_0043'),
]
operations = [
migrations.AlterField(
model_name='unassignedticket',
name='assignee',
field=models.ForeignKey(help_text='Пользователь, с которого снят тикет', on_delete=django.db.models.deletion.CASCADE, related_name='tickets', to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='unassignedticket',
name='status',
field=models.IntegerField(choices=[(0, 'Снят с пользователя, перенесён в буферную группу'), (1, 'Авторство восстановлено'), (2, 'Пока нас не было, тикет испарился из буферной группы. Дополнительные действия не требуются'), (3, 'Тикет уже был закрыт. Дополнительные действия не требуются'), (4, 'Тикет решён. Записан на пользователя с почтой SOLVED_TICKETS_EMAIL')], default=0, help_text='Статус тикета'),
),
]

View File

@ -4,6 +4,8 @@ from django.db.models.signals import post_save
from django.dispatch import receiver from django.dispatch import receiver
from django.utils import timezone from django.utils import timezone
from access_controller.settings import ZENDESK_ROLES
class UserProfile(models.Model): class UserProfile(models.Model):
""" """
@ -23,6 +25,14 @@ class UserProfile(models.Model):
image = models.URLField(null=True, blank=True, help_text='Аватарка') image = models.URLField(null=True, blank=True, help_text='Аватарка')
name = models.CharField(default='None', max_length=100, help_text='Имя пользователя на нашем сайте') name = models.CharField(default='None', max_length=100, help_text='Имя пользователя на нашем сайте')
@property
def zendesk_role(self):
id = self.custom_role_id
for role, r_id in ZENDESK_ROLES.items():
if r_id == id:
return role
return 'UNDEFINED'
@receiver(post_save, sender=User) @receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs): def create_user_profile(sender, instance, created, **kwargs):

View File

@ -1,6 +1,7 @@
from django.contrib.auth.models import User from django.contrib.auth.models import User
from rest_framework import serializers from rest_framework import serializers
from main.models import UserProfile from main.models import UserProfile
from access_controller.settings import ZENDESK_ROLES
class UserSerializer(serializers.HyperlinkedModelSerializer): class UserSerializer(serializers.HyperlinkedModelSerializer):
@ -13,11 +14,25 @@ class UserSerializer(serializers.HyperlinkedModelSerializer):
class ProfileSerializer(serializers.HyperlinkedModelSerializer): class ProfileSerializer(serializers.HyperlinkedModelSerializer):
""" """Класс serializer для модели профиля пользователя"""
Класс serializer для модель профиля пользователя.
"""
user = UserSerializer() user = UserSerializer()
class Meta: class Meta:
model = UserProfile model = UserProfile
fields = ['user', 'id', 'role', 'name'] fields = ['user', 'id', 'name', 'zendesk_role']
class ZendeskUserSerializer(serializers.Serializer):
"""Класс serializer для объектов пользователей из zenpy"""
name = serializers.CharField()
zendesk_role = serializers.SerializerMethodField('get_zendesk_role')
email = serializers.EmailField()
@staticmethod
def get_zendesk_role(obj):
if obj.custom_role_id == ZENDESK_ROLES['engineer']:
return 'engineer'
elif obj.custom_role_id == ZENDESK_ROLES['light_agent']:
return 'light_agent'
else:
return "empty"

View File

@ -5,23 +5,48 @@
<nav class="navbar navbar-light" style="background-color: #113A60;"> <nav class="navbar navbar-light" style="background-color: #113A60;">
<a class="navbar-brand" href="{% url 'index' %}"> <a class="navbar-brand" href="{% url 'index' %}">
<img src="{% static 'main/img/logo_real.png' %}" width="107" height="22" class="d-inline-block align-top" alt="" loading="lazy"> <img src="{% static 'main/img/logo_real.png' %}" width="107" height="22" class="d-inline-block align-top" style="margin-left: 15px" alt="" loading="lazy">
<t style="color:#FFFFFF">Access Controller</t> <t style="color:#FFFFFF">Access Controller</t>
</a> </a>
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
<div class="btn-group" role="group" aria-label="Basic example"> <div class="btn-group" role="group" aria-label="Basic example" style="margin-right: 9px">
<a class="btn btn-secondary" href="{% url 'profile' %}">Профиль</a> <a {% if profile_lit %}
class="btn btn-primary"
{% else %}
class="btn btn-secondary"
{% endif %}
href="{% url 'profile' %}">Профиль</a>
{% if perms.main.has_control_access %} {% if perms.main.has_control_access %}
<a class="btn btn-secondary" href="{% url 'control' %}">Управление</a> <a {% if control_lit %}
class="btn btn-primary"
{% else %}
class="btn btn-secondary"
{% endif %}
href="{% url 'control' %}">Управление</a>
{% else %} {% else %}
<a class="btn btn-secondary" href="{% url 'work' request.user.id %}">Запрос прав</a> <a {% if work_lit %}
class="btn btn-primary"
{% else %}
class="btn btn-secondary"
{% endif %}
href="{% url 'work' request.user.id %}">Запрос прав</a>
{% endif %} {% endif %}
<a class="btn btn-secondary" href="{% url 'logout' %}">Выйти</a> <a class="btn btn-secondary" href="{% url 'logout' %}">Выйти</a>
</div> </div>
{% else %} {% else %}
<div class="btn-group" role="group" aria-label="Basic example"> <div class="btn-group" role="group" aria-label="Basic example" style="margin-right: 9px">
<a class="btn btn-secondary" href="/accounts/login">Войти</a> <a {% if login_lit %}
<a class="btn btn-secondary" href="/accounts/register">Зарегистрироваться</a> class="btn btn-primary"
{% else %}
class="btn btn-secondary"
{% endif %}
href="/accounts/login">Войти</a>
<a {% if registration_lit %}
class="btn btn-primary"
{% else %}
class="btn btn-secondary"
{% endif %}
href="/accounts/register">Зарегистрироваться</a>
</div> </div>
{% endif %} {% endif %}
</nav> </nav>

View File

@ -0,0 +1,15 @@
{% extends 'base/base.html' %}
{% load static %}
{% block title %}
Регистрация завершена
{% endblock %}
{% block heading %}
Регистрация
{% endblock %}
{% block content %}
<br>
<h4> Произошла ошибка при отправке электронного сообщения.</h4>
{% endblock %}

View File

@ -30,19 +30,11 @@
<div class="row justify-content-center new-section"> <div class="row justify-content-center new-section">
{% block hidden_form %}
<div style="display: none">
{% for field in form.users %}
{{ field.tag }}
{% endfor %}
</div>
{% endblock %}
<div class="col-10"> <div class="col-10">
<h6 class="table-title">Список сотрудников</h6> <h6 class="table-title">Список сотрудников</h6>
{% block table %} {% block table %}
<table class="light-table"> <table class="table table-dark light-table">
<thead> <thead>
<th>Name</th> <th>Name</th>
@ -50,25 +42,11 @@
<th>Role</th> <th>Role</th>
<th>Checked</th> <th>Checked</th>
</thead> </thead>
<tbody id="tbody"></tbody>
<tbody id="old_tbody">
{% for user in users %}
<tr>
<td><a href="#">{{ user.name }}</a></td>
<td>{{ user.user.email }}</td>
<td>{% if user.custom_role_id == ZENDESK_ROLES.engineer %}
engineer
{% elif user.custom_role_id == ZENDESK_ROLES.light_agent %}
light_agent
{% endif %}
</td>
<td class="checkbox_field"></td>
</tr>
{% endfor %}
</tbody>
<tbody id="new_tbody"></tbody>
</table> </table>
{% endblock%} <p id="loading">Данные загружаются...</p>
{% endblock %}
</div> </div>
</div> </div>
@ -96,7 +74,9 @@
</div> </div>
</div> </div>
{% endblock %}
{% block buttons %}
<div class="col-5"> <div class="col-5">
<button type="submit" name="engineer" class="request-acess-button default-button"> <button type="submit" name="engineer" class="request-acess-button default-button">
@ -107,8 +87,9 @@
Назначить выбранных на роль легкого агента Назначить выбранных на роль легкого агента
</button> </button>
</div> </div>
{% endblock %}
</div> </div>
{% endblock %}
</form> </form>
{% endblock %} {% endblock %}

View File

@ -1,49 +0,0 @@
{% extends 'base/base.html' %}
{% load static %}
{% block title %}{{ pagename }}{% endblock %}
{% block heading %} Пример страницы статистики(палками не бейти плиз){% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'main/css/work.css' %}">
{% endblock %}
{% block content %}
<div>
<form action="" method="post">
{% csrf_token %}
{% for field in form %}
{{field.label}}
{{field}}
<br>
{% endfor %}
<input type="submit">
</form>
<ul>
{% for error in errors %}
<li><span class="badge bg-danger">{{error}}</span></li>
{% endfor %}
</ul>
{%if form.errors%}
<ul>
{% for field, errors in form.errors.items %}
{% for error in errors %}
<li><span class="badge bg-danger">{{error}}</span></li>
{% endfor %}
{% endfor %}
</ul>
{%endif%}
<ul>
{% for warning in warnings %}
<li><span class="badge bg-warning">{{warning}}</span></li>
{% endfor %}
</ul>
{% for key,val in log_stats.items %}
<h3>{{key}} <b>|</b> {{val}}</h3>
<br>
{% endfor %}
</div>
{% endblock %}

View File

@ -0,0 +1,106 @@
{% extends 'base/base.html' %}
{% load static %}
{% block title %}{{ pagename }}{% endblock %}
{% block heading %} Страницы просмотра статистики{% endblock %}
{% block content%}
<div class="mt-5">
<div class="container-fluid" style="font-size:2rem">
<form method="post">
{% csrf_token %}
<div class="row g-3">
<div class="col-auto">
{{ form.email.label }}
</div>
<div class="col-auto mt-4">
{{ form.email }}
</div>
</div>
<div class="row g-3 mt-4">
<div class="col-auto">
{{ form.interval.label }}
</div>
<div class="col-auto">
{% for radio in form.interval%}
{{ radio.tag }}
<label class="btn btn-secondary text-primary bg-white" for="{{ radio.id_for_label }}">
{{ radio.choice_label }}
</label>
{% endfor %}
</div>
</div>
<div class="row g-3 mt-4">
<div class="col-auto">
{{ form.display_format.label }}
</div>
<div class="col-auto">
{% for radio in form.display_format%}
{{ radio.tag }}
<label class="btn btn-secondary text-primary bg-white" for="{{ radio.id_for_label }}">
{{ radio.choice_label }}
</label>
{% endfor %}
</div>
</div>
<div class="row g-3 mt-4">
<div class="col-auto">
{{ form.range_start.label}}
</div>
<div class="col-auto">
<div class='col-sm-7'>
{{ form.range_start}}
</div>
</div>
</div>
<div class="row g-3 mt-4">
<div class="col-auto">
{{ form.range_end.label}}
</div>
<div class="col-auto">
<div class='col-sm-7'>
{{ form.range_end}}
</div>
</div>
</div>
<div class="form-row text-center">
<div class="col-12">
<button type="submit" class="btn btn-primary bg-white text-primary">Посмотреть статистику</button>
</div>
</div>
</form>
</div>
<ul>
{% for error in errors %}
<li><span class="badge bg-danger">{{error}}</span></li>
{% endfor %}
</ul>
<ul>
{% for warning in warnings %}
<li><span class="badge bg-warning">{{warning}}</span></li>
{% endfor %}
</ul>
<div class="container-fluid">
<table class="table table-bordered text-center text-secondary mt-5" style="background-color:#f2f2f2;">
<thead>
<tr>
<td scope="col">Пользователи/Даты</td>
{% for date in log_stats.keys %}
<td scope="col">{{date}}</td>
{% endfor %}
</tr>
</thead>
<tbody>
<tr>
<td>{{ form.email.value }}</td>
{% for time in log_stats.values %}
<td>{{time}}</td>
{% endfor %}
</tr>
</tbody>
</table>
</div>
</div>
{% endblock %}

View File

@ -60,6 +60,12 @@
<a href="/work/become_engineer" class="request-acess-button default-button">Получить права инженера</a> <a href="/work/become_engineer" class="request-acess-button default-button">Получить права инженера</a>
<a href="/work/hand_over" class="hand-over-acess-button default-button">Сдать права инженера</a> <a href="/work/hand_over" class="hand-over-acess-button default-button">Сдать права инженера</a>
</div> </div>
<div class="col-10">
<form method="GET" action="/work/get_tickets">
<input class="form-control mb-3" type="number" min="1" value="1" name="count_tickets">
<button type="submit" class="default-button">Взять тикеты в работу</button>
</form>
</div>
</div> </div>
{% include 'base/success_messages.html' %} {% include 'base/success_messages.html' %}
</div> </div>

View File

@ -2,39 +2,51 @@ import logging
import os import os
from datetime import datetime from datetime import datetime
from smtplib import SMTPException
from django.contrib import messages
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth.forms import PasswordResetForm
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.contrib.auth.models import User, Permission from django.contrib.auth.models import User, Permission
from django.contrib.auth.tokens import default_token_generator from django.contrib.auth.tokens import default_token_generator
from django.contrib.auth.forms import PasswordResetForm
from django.contrib.auth.views import LoginView from django.contrib.auth.views import LoginView
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.core.handlers.wsgi import WSGIRequest from django.core.handlers.wsgi import WSGIRequest
from django.http import HttpResponseRedirect, HttpResponse from django.http import HttpResponseRedirect, HttpResponse
from django.shortcuts import render, get_list_or_404, redirect from django.shortcuts import render, redirect, get_list_or_404
from django.urls import reverse_lazy, reverse from django.urls import reverse_lazy, reverse
from django.views.generic import FormView from django.views.generic import FormView
from django_registration.views import RegistrationView from django_registration.views import RegistrationView
from django.contrib import messages
# Django REST # Django REST
from rest_framework import viewsets from rest_framework import viewsets
from rest_framework.response import Response from rest_framework.response import Response
from zenpy.lib.api_objects import User as ZenpyUser from zenpy.lib.api_objects import User as ZenpyUser
from access_controller.settings import EMAIL_HOST_USER, ZENDESK_ROLES, ZENDESK_MAX_AGENTS from access_controller.settings import EMAIL_HOST_USER, ZENDESK_ROLES, ZENDESK_MAX_AGENTS, ZENDESK_GROUPS
from main.extra_func import ZendeskAdmin
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, \
StatisticData StatisticData, log, ZendeskAdmin
from main.forms import AdminPageUsers, CustomRegistrationForm, CustomAuthenticationForm, StatisticForm from main.forms import AdminPageUsers, CustomRegistrationForm, CustomAuthenticationForm, StatisticForm
from main.serializers import ProfileSerializer from main.serializers import ProfileSerializer, ZendeskUserSerializer
from .models import UserProfile from .models import UserProfile
def setup_context(profile_lit: bool = False, control_lit: bool = False, work_lit: bool = False,
registration_lit: bool = False, login_lit: bool = False):
print(profile_lit, control_lit, work_lit, registration_lit, login_lit)
context = {
'profile_lit': profile_lit,
'control_lit': control_lit,
'work_lit': work_lit,
'registration_lit': registration_lit,
'login_lit': login_lit,
}
return context
class CustomRegistrationView(RegistrationView): class CustomRegistrationView(RegistrationView):
""" """
Отображение и логика работы страницы регистрации пользователя. Отображение и логика работы страницы регистрации пользователя.
@ -48,10 +60,15 @@ class CustomRegistrationView(RegistrationView):
:param is_allowed: Определение зарегистрирован ли пользователь с введенным email на Zendesk и принадлежит ли он к организации SYSTEM :param is_allowed: Определение зарегистрирован ли пользователь с введенным email на Zendesk и принадлежит ли он к организации SYSTEM
:type is_allowed: :class:`bool` :type is_allowed: :class:`bool`
""" """
extra_context = setup_context(registration_lit=True)
form_class = CustomRegistrationForm form_class = CustomRegistrationForm
template_name = 'django_registration/registration_form.html' template_name = 'django_registration/registration_form.html'
success_url = reverse_lazy('django_registration_complete') urls = {
is_allowed = True 'done': reverse_lazy('password_reset_done'),
'invalid_zendesk_email': reverse_lazy('django_registration_disallowed'),
'email_sending_error': reverse_lazy('registration_email_error'),
}
redirect_url = 'done'
def register(self, form: CustomRegistrationForm) -> User: def register(self, form: CustomRegistrationForm) -> User:
""" """
@ -64,7 +81,7 @@ class CustomRegistrationView(RegistrationView):
:param form: Email пользователя на Zendesk :param form: Email пользователя на Zendesk
:return: user :return: user
""" """
self.is_allowed = True self.redirect_url = 'done'
if check_user_exist(form.data['email']) and get_user_organization(form.data['email']) == 'SYSTEM': if check_user_exist(form.data['email']) and get_user_organization(form.data['email']) == 'SYSTEM':
forms = PasswordResetForm(self.request.POST) forms = PasswordResetForm(self.request.POST)
if forms.is_valid(): if forms.is_valid():
@ -83,14 +100,17 @@ class CustomRegistrationView(RegistrationView):
email=form.data['email'], email=form.data['email'],
password=User.objects.make_random_password(length=50) password=User.objects.make_random_password(length=50)
) )
forms.save(**opts) try:
update_profile(user.userprofile) update_profile(user.userprofile)
self.set_permission(user) self.set_permission(user)
return user forms.save(**opts)
return user
except SMTPException:
self.redirect_url = 'email_sending_error'
else: else:
raise ValueError('Непредвиденная ошибка') raise ValueError('Непредвиденная ошибка')
else: else:
self.is_allowed = False self.redirect_url = 'invalid_zendesk_email'
@staticmethod @staticmethod
def set_permission(user: User) -> None: def set_permission(user: User) -> None:
@ -107,7 +127,7 @@ class CustomRegistrationView(RegistrationView):
) )
user.user_permissions.add(permission) user.user_permissions.add(permission)
def get_success_url(self, user: User = None) -> success_url: def get_success_url(self, user: User = None):
""" """
Функция возвращает url-адрес страницы, куда нужно перейти после успешной/не успешной регистрации. Функция возвращает url-адрес страницы, куда нужно перейти после успешной/не успешной регистрации.
Используется самой django-registration. Используется самой django-registration.
@ -115,10 +135,11 @@ class CustomRegistrationView(RegistrationView):
:param user: пользователь, пытающийся зарегистрироваться :param user: пользователь, пытающийся зарегистрироваться
:return: адресация на страницу успешной регистрации :return: адресация на страницу успешной регистрации
""" """
if self.is_allowed: return self.urls[self.redirect_url]
return reverse_lazy('password_reset_done')
else:
return reverse_lazy('django_registration_disallowed') def registration_error(request):
return render(request, 'django_registration/registration_error.html')
@login_required() @login_required()
@ -131,11 +152,12 @@ def profile_page(request: WSGIRequest) -> HttpResponse:
""" """
user_profile: UserProfile = request.user.userprofile user_profile: UserProfile = request.user.userprofile
update_profile(user_profile) update_profile(user_profile)
context = { context = setup_context(profile_lit=True)
context.update({
'profile': user_profile, 'profile': user_profile,
'pagename': 'Страница профиля', 'pagename': 'Страница профиля',
'ZENDESK_ROLES': ZENDESK_ROLES, 'ZENDESK_ROLES': ZENDESK_ROLES,
} })
return render(request, 'pages/profile.html', context) return render(request, 'pages/profile.html', context)
@ -171,13 +193,14 @@ def work_page(request: WSGIRequest, id: int) -> HttpResponse:
elif user.custom_role_id == ZENDESK_ROLES['light_agent']: elif user.custom_role_id == ZENDESK_ROLES['light_agent']:
light_agents.append(user) light_agents.append(user)
context = { context = setup_context(work_lit=True)
context.update({
'engineers': engineers, 'engineers': engineers,
'agents': light_agents, 'agents': light_agents,
'messages': messages.get_messages(request), 'messages': messages.get_messages(request),
'licences_remaining': max(0, ZENDESK_MAX_AGENTS - len(engineers)), 'licences_remaining': max(0, ZENDESK_MAX_AGENTS - len(engineers)),
'pagename': 'Управление правами' 'pagename': 'Управление правами'
} })
return render(request, 'pages/work.html', context) return render(request, 'pages/work.html', context)
return redirect("login") return redirect("login")
@ -194,6 +217,7 @@ def user_update(zenpy_user: User, admin: User, request: WSGIRequest) -> UserProf
admin.users.update(zenpy_user) admin.users.update(zenpy_user)
request.user.userprofile.role = "agent" request.user.userprofile.role = "agent"
request.user.userprofile.custom_role_id = zenpy_user.custom_role_id
request.user.userprofile.save() request.user.userprofile.save()
messages.success(request, "Права были изменены") messages.success(request, "Права были изменены")
@ -207,10 +231,7 @@ def work_hand_over(request: WSGIRequest) -> HttpResponseRedirect:
:param request: данные текущего пользователя (login_required) :param request: данные текущего пользователя (login_required)
:return: перезагрузка текущей страницы после выполнения смены роли :return: перезагрузка текущей страницы после выполнения смены роли
""" """
zenpy_user, admin = auth_user(request) make_light_agent(request.user.userprofile,request.user)
if zenpy_user.custom_role_id == ZENDESK_ROLES['engineer']:
zenpy_user.custom_role_id = ZENDESK_ROLES['light_agent']
user_update(zenpy_user, admin, request)
return HttpResponseRedirect(reverse('work', args=(request.user.id,))) return HttpResponseRedirect(reverse('work', args=(request.user.id,)))
@ -223,17 +244,31 @@ def work_become_engineer(request: WSGIRequest) -> HttpResponseRedirect:
:return: перезагрузка текущей страницы после выполнения смены роли :return: перезагрузка текущей страницы после выполнения смены роли
""" """
zenpy_user, admin = auth_user(request) zenpy_user, admin = auth_user(request)
if zenpy_user.custom_role_id == ZENDESK_ROLES['light_agent']:
zenpy_user.custom_role_id = ZENDESK_ROLES['engineer'] make_engineer(request.user.userprofile,request.user)
user_update(zenpy_user, admin, request) return HttpResponseRedirect(reverse('work', args=(request.user.id,)))
@login_required()
def work_get_tickets(request):
zenpy_user, admin = auth_user(request)
count_tickets = int(request.GET["count_tickets"])
tickets = [ticket for ticket in admin.search(type="ticket") if ticket.group.name == 'Сменная группа' and ticket.assignee is None]
for i in range(len(tickets)):
if i == count_tickets:
return HttpResponseRedirect(reverse('work', args=(request.user.id,)))
tickets[i].assignee = zenpy_user
admin.tickets.update(tickets[i])
return HttpResponseRedirect(reverse('work', args=(request.user.id,))) return HttpResponseRedirect(reverse('work', args=(request.user.id,)))
def main_page(request): def main_page(request: WSGIRequest) -> HttpResponse:
"""
Функция переадресации на главную страницу.
"""
return render(request, 'pages/index.html') return render(request, 'pages/index.html')
class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin,SuccessMessageMixin, FormView): class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, FormView):
""" """
Класс отображения страницы администратора. Класс отображения страницы администратора.
@ -275,29 +310,50 @@ class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin,SuccessMessageMi
""" """
for user in users: for user in users:
make_engineer(user, self.request.user) make_engineer(user, self.request.user)
log(user, self.request.user.userprofile)
def make_light_agents(self, users): def make_light_agents(self, users):
"""
Функция проходит по списку пользователей, проставляя статус "light agent".
:param users: Список пользователей
:return: Обновленный список пользователей
"""
for user in users: for user in users:
make_light_agent(user, self.request.user) make_light_agent(user, self.request.user)
log(user, self.request.user.userprofile)
def get_context_data(self, **kwargs) -> dict: def get_context_data(self, **kwargs) -> dict:
""" """
Функция формирования контента страницы администратора (с проверкой прав доступа) Функция формирования контента страницы администратора (с проверкой прав доступа)
""" """
context = super().get_context_data(**kwargs)
# context = super().get_context_data(**kwargs)
# context['licences_remaining'] = max(0, ZENDESK_MAX_AGENTS - context['engineers'])
# return context
context = setup_context(control_lit=True)
context.update(super().get_context_data(**kwargs))
users = get_list_or_404( users = get_list_or_404(
UserProfile, role='agent') UserProfile, role='agent')
context['users'] = users
context['ZENDESK_ROLES'] = ZENDESK_ROLES
context['engineers'], context['light_agents'] = count_users(get_users_list()) context['engineers'], context['light_agents'] = count_users(get_users_list())
context['licences_remaining'] = max(0, ZENDESK_MAX_AGENTS - context['engineers']) context.update({
'users': users,
'ZENDESK_ROLES': ZENDESK_ROLES,
'engineers': context['engineers'],
'light_agents': context['light_agents'],
'licences_remaining': max(0, ZENDESK_MAX_AGENTS - context['engineers']),
})
return context # TODO: need to get profile page url return context # TODO: need to get profile page url
class CustomLoginView(LoginView): class CustomLoginView(LoginView):
""" """
Отображение страницы авторизации пользователя Отображение страницы авторизации пользователя
""" """
extra_context = setup_context(login_lit=True)
form_class = CustomAuthenticationForm form_class = CustomAuthenticationForm
@ -309,16 +365,34 @@ class UsersViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = ProfileSerializer serializer_class = ProfileSerializer
def list(self, request, *args, **kwargs): def list(self, request, *args, **kwargs):
users = update_users_in_model().values users = update_users_in_model()
count = count_users(users) count = count_users(users.values)
profiles = UserProfile.objects.filter(role='agent') profiles = UserProfile.objects.filter(role='agent')
serializer = self.get_serializer(profiles, many=True) serializer = self.get_serializer(profiles, many=True)
return Response({ res = {
'users': serializer.data, 'users': serializer.data,
'engineers': count[0], 'engineers': count[0],
'light_agents': count[1] 'light_agents': count[1],
}) "zendesk_users": self.get_zendesk_users(self.choose_users(users.values, profiles))
}
return Response(res)
@staticmethod
def choose_users(zendesk, model):
users = []
for zendesk_user in zendesk:
if zendesk_user.name not in [user.name for user in model]:
users.append(zendesk_user)
return users
@staticmethod
def get_zendesk_users(users):
zendesk_users = ZendeskUserSerializer(
data=[user for user in users if user.role != 'admin'],
many=True
)
zendesk_users.is_valid()
return zendesk_users.data
@login_required() @login_required()
@ -329,12 +403,18 @@ def statistic_page(request: WSGIRequest) -> HttpResponse:
:param request: данные о пользователе: email, время и интервал работы. Данные получаем через forms.StatisticForm :param request: данные о пользователе: email, время и интервал работы. Данные получаем через forms.StatisticForm
:return: адресация на страницу статистики :return: адресация на страницу статистики
""" """
# if not request.user.has_perm('main.has_control_access'):
# raise PermissionDenied
# context = {
if not request.user.is_superuser: if not request.user.is_superuser:
return redirect('index') return redirect('index')
context = { context = setup_context()
context.update({
'pagename': 'страница статистики', 'pagename': 'страница статистики',
'errors': list(), 'errors': list(),
} })
if request.method == "POST": if request.method == "POST":
form = StatisticForm(request.POST) form = StatisticForm(request.POST)
if form.is_valid(): if form.is_valid():
@ -352,4 +432,4 @@ def statistic_page(request: WSGIRequest) -> HttpResponse:
if request.method == 'GET': if request.method == 'GET':
form = StatisticForm() form = StatisticForm()
context['form'] = form context['form'] = form
return render(request, 'pages/stat.html', context) return render(request, 'pages/statistic.html', context)

View File

@ -6,7 +6,8 @@ import sys
def main(): def main():
"""Run administrative tasks.""" """Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'access_controller.settings') os.environ.setdefault('DJANGO_SETTINGS_MODULE',
'access_controller.settings')
try: try:
from django.core.management import execute_from_command_line from django.core.management import execute_from_command_line
except ImportError as exc: except ImportError as exc:

View File

@ -4,6 +4,7 @@ Pillow==8.1.0
zenpy~=2.0.24 zenpy~=2.0.24
django_registration==3.1.1 django_registration==3.1.1
djangorestframework==3.12.2 djangorestframework==3.12.2
daphne==3.0.1
# Documentation # Documentation

View File

@ -13,8 +13,7 @@
background: #45729C; background: #45729C;
} */ } */
.form-check-input { .form-check-input {
border-radius: 0px; border-radius: 0;
background-image: url("../img/check.png");
width: 30px; width: 30px;
height: 30px; height: 30px;
background-size: 20px auto; background-size: 20px auto;
@ -125,4 +124,7 @@
padding: 10px; padding: 10px;
background: #3B91D4; background: #3B91D4;
color: white; color: white;
width: 100%;
} }

View File

@ -1,31 +1,15 @@
"use strict"; "use strict";
function move_checkboxes() {
let checkboxes = document.getElementsByName("users");
let fields = document.querySelectorAll(".checkbox_field");
if (checkboxes.length == fields.length) {
for (let i = 0; i < fields.length; ++i) {
let el = checkboxes[i].cloneNode(true);
fields[i].appendChild(el);
}
} else {
alert(
"Количество пользователей агентов не соответствует количеству полей в форме AdminPageUsers"
);
}
}
move_checkboxes();
// React // React
class TableRow extends React.Component { class ModelUserTableRow extends React.Component {
render() { render() {
return ( return (
<tr> <tr className={"table-dark"}>
<td> <td>
<a href="#">{this.props.user.name}</a> <a href="#">{this.props.user.name}</a>
</td> </td>
<td>{this.props.user.user.email}</td> <td>{this.props.user.user.email}</td>
<td>{this.props.user.role}</td> <td>{this.props.user.zendesk_role}</td>
<td> <td>
<input <input
type="checkbox" type="checkbox"
@ -39,6 +23,43 @@ class TableRow extends React.Component {
} }
} }
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"}>
<td>
<a href="#">{this.props.user.name}</a>
</td>
<td>{this.props.user.email}</td>
<td>{this.props.user.zendesk_role}</td>
<td></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 { class TableBody extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
@ -46,15 +67,17 @@ class TableBody extends React.Component {
users: [], users: [],
engineers: 0, engineers: 0,
light_agents: 0, light_agents: 0,
zendesk_users: [],
}; };
} }
get_users() { async get_users() {
axios.get("/api/users").then((response) => { await axios.get("/api/users").then((response) => {
this.setState({ this.setState({
users: response.data.users, users: response.data.users,
engineers: response.data.engineers, engineers: response.data.engineers,
light_agents: response.data.light_agents, light_agents: response.data.light_agents,
zendesk_users: response.data.zendesk_users,
}); });
let elements = document.querySelectorAll(".info-quantity-value"); let elements = document.querySelectorAll(".info-quantity-value");
elements[0].innerHTML = this.state.engineers; elements[0].innerHTML = this.state.engineers;
@ -62,7 +85,12 @@ class TableBody extends React.Component {
}); });
} }
delete_pretext() {
document.getElementById("loading").remove();
}
componentDidMount() { componentDidMount() {
this.get_users().then(() => this.delete_pretext());
this.interval = setInterval(() => { this.interval = setInterval(() => {
this.get_users(); this.get_users();
}, 60000); }, 60000);
@ -73,11 +101,13 @@ class TableBody extends React.Component {
} }
render() { render() {
return this.state.users.map((user, key) => ( return (
<TableRow user={user} key={key} /> <tr>
)); <ModelUserTableRows users={this.state.users} />
<ZendeskUserTableRows users={this.state.zendesk_users} />
</tr>
);
} }
} }
ReactDOM.render(<TableBody />, document.getElementById("new_tbody")); ReactDOM.render(<TableBody />, document.getElementById("tbody"));
setTimeout(() => document.getElementById("old_tbody").remove(), 60000);