Merge branch 'develop' into feature/react_test

# Conflicts:
#	access_controller/settings.py
#	main/templates/pages/adm_ruleset.html
#	static/main/js/control.js
This commit is contained in:
Yuriy Kulakov 2021-05-27 19:35:59 +03:00
commit 0a85ddacd2
24 changed files with 727 additions and 281 deletions

View File

@ -186,13 +186,13 @@ autopep8 --in-place filename
##Для проверки орфографии: ##Для проверки орфографии:
cd docs cd docs
(set -a && source ../.env && make spelling) make spelling
##Для обновления документации: ##Для обновления документации:
m2r README.md m2r README.md
cd docs cd docs
(set -a && source ../.env && make html) make html
## Read more ## Read more

View File

@ -57,9 +57,9 @@ Quickstart
sudo apt install make sudo apt install make
pip install --upgrade pip pip install --upgrade pip
pip install -r requirements/dev.txt pip install -r requirements/dev.txt
(set -a && source .env && ./manage.py migrate) ./manage.py migrate
(set -a && source .env && ./manage.py loaddata data.json) ./manage.py loaddata data.json
(set -a && source .env && ./manage.py runserver) ./manage.py runserver
Перед запуском для тестирования: Перед запуском для тестирования:
-------------------------------- --------------------------------
@ -76,7 +76,7 @@ Quickstart
* Перейти в папку приложения * Перейти в папку приложения
* Активировать виртуальное окружение * Активировать виртуальное окружение
* Выполнить команду ``pip install -r requirements/dev.txt`` * Выполнить команду ``pip install -r requirements/dev.txt``
* В виртуальное окружение добавить следующие переменные: * В файл ``.env`` добавить следующие переменные:
.. code-block:: .. code-block::
@ -170,10 +170,7 @@ Quickstart
Для проверки pylint используем: Для проверки pylint используем:
------------------------------- -------------------------------
pylint ../access_controller_new pylint ../access_controller (каталог, где лежит проект)
Вместо "access_controller_new" необходимо указать папку проекта.
Для приведения файлов к стандарту PEP8 используем: Для приведения файлов к стандарту PEP8 используем:
-------------------------------------------------- --------------------------------------------------
@ -185,7 +182,7 @@ autopep8 --in-place filename
cd docs cd docs
(set -a && source ../.env && make spelling) make spelling
Для обновления документации: Для обновления документации:
---------------------------- ----------------------------
@ -194,7 +191,7 @@ m2r README.md
cd docs cd docs
(set -a && source ../.env && make html) make html
Read more Read more
--------- ---------

View File

@ -10,6 +10,7 @@ For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.1/ref/settings/ https://docs.djangoproject.com/en/3.1/ref/settings/
""" """
import os import os
from pathlib import Path from pathlib import Path
from dotenv import load_dotenv from dotenv import load_dotenv
@ -151,7 +152,6 @@ LOGIN_REDIRECT_URL = '/'
LOGOUT_REDIRECT_URL = '/' LOGOUT_REDIRECT_URL = '/'
# Название_приложения.Названиеайла.Название_класса_обработчика # Название_приложения.Названиеайла.Название_класса_обработчика
AUTHENTICATION_BACKENDS = [ AUTHENTICATION_BACKENDS = [
'access_controller.auth.EmailAuthBackend', 'access_controller.auth.EmailAuthBackend',
@ -190,5 +190,5 @@ ACTRL_API_EMAIL = os.getenv('ACTRL_API_EMAIL') or os.getenv('ACCESS_CONTROLLER_A
ACTRL_API_TOKEN = os.getenv('ACTRL_API_TOKEN') or os.getenv('ACCESS_CONTROLLER_API_TOKEN') ACTRL_API_TOKEN = os.getenv('ACTRL_API_TOKEN') or os.getenv('ACCESS_CONTROLLER_API_TOKEN')
ACTRL_API_PASSWORD = os.getenv('ACTRL_API_PASSWORD') or os.getenv('ACCESS_CONTROLLER_API_PASSWORD') ACTRL_API_PASSWORD = os.getenv('ACTRL_API_PASSWORD') or os.getenv('ACCESS_CONTROLLER_API_PASSWORD')
NODE_PACKAGE_JSON = BASE_DIR / 'static/main/js/control_page_js_modules/package.json'
NODE_MODULES_ROOT = BASE_DIR / 'static/main/js/control_page_js_modules/node_modules' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

View File

@ -22,8 +22,9 @@
"pk": 1, "pk": 1,
"fields": { "fields": {
"name": "ZendeskAdmin", "name": "ZendeskAdmin",
"user": 1, "user": 3,
"role": "admin" "role": "admin",
"user_id": 1
} }
}, },
{ {

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

View File

@ -41,3 +41,33 @@ Views
:members: :members:
*****************
Обработка тикетов
*****************
.. automodule:: main.requester
:members:
*********************
Обработка статистики
*********************
.. automodule:: main.statistic_data
:members:
*********************************
Функционал администратора Zendesk
*********************************
.. automodule:: main.zendesk_admin
:members:
********
Тесты
********
.. automodule:: main.tests
:members:

View File

@ -81,6 +81,12 @@
.. image:: _static/role_change.png .. image:: _static/role_change.png
Являясь инженером, Вы можете запросить в работу необходимое количество тикетов.
.. image:: _static/take_tickets.png
Назначенные тикеты будут доступны в Zendesk.
****************************************** ******************************************
Управление правами доступа администратором Управление правами доступа администратором
****************************************** ******************************************
@ -97,4 +103,13 @@
.. image:: _static/admin_manage_done.png .. image:: _static/admin_manage_done.png
.. |copy| unicode:: 0xA9 .. Школа программистов S101, группа 2. 2021гю Вы можете смотреть статистику работы пользователя.
Для этого на странице статистика необходимо указать:
* email пользователя
* период, за который необходима статистика
* формат отображения данных
.. image:: _static/statistic.png
.. |copy| unicode:: 0xA9 .. Школа программистов S101, группа 2. 2021г.

View File

@ -191,4 +191,58 @@ docs
a a
Аватарка Аватарка
filename filename
work
form
work_get_tickets
get
tickets
Do
takes
whatever
it
to
actually
log
the
specified
logging
record
This
version
is
intended
be
implemented
by
subclasses
so
new
тикеты
StatisticForm
patch
zenpy
Mock
редирект
редиректа
предустановки
TicketListRequester
get_tickets_list_for_user
side
effect
for
залогиненный
предустанавливает
переадресация
фикстуры
profile
json
аватарки
аватарке
locmem
бэкенд
has
control
disallowed
test
users
Contents

View File

@ -1,9 +1,9 @@
""" """
Вспомогательные функции со списками пользователей, статистикой и т.д. Вспомогательные функции.
""" """
import logging import logging
from datetime import timedelta from datetime import timedelta, date
from typing import Union from typing import Union, Optional
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
@ -29,7 +29,6 @@ def update_role(user_profile: UserProfile, role: int, who_changes: get_user_mode
:param user_profile: Профиль пользователя :param user_profile: Профиль пользователя
:param role: Новая роль :param role: Новая роль
:param who_changes: Пользователь, меняющий роль :param who_changes: Пользователь, меняющий роль
:return: Пользователь с обновленной ролью
""" """
zendesk = zenpy zendesk = zenpy
user = zendesk.get_user(user_profile.user.email) user = zendesk.get_user(user_profile.user.email)
@ -55,7 +54,8 @@ def make_light_agent(user_profile: UserProfile, who_changes: get_user_model()) -
Функция устанавливает пользователю роль легкого агента. Функция устанавливает пользователю роль легкого агента.
:param user_profile: Профиль пользователя :param user_profile: Профиль пользователя
:return: Вызов функции **update_role** с параметрами: профиль пользователя, роль "light_agent" :return: Вызов функции **update_role** с параметрами: профиль пользователя, роль "light_agent".
Предварительно снимаем тикеты, находящие в работы у пользователя.
""" """
tickets: SearchResultGenerator = get_tickets_list(user_profile.user.email) tickets: SearchResultGenerator = get_tickets_list(user_profile.user.email)
ticket: ZenpyTicket ticket: ZenpyTicket
@ -85,7 +85,7 @@ def make_light_agent(user_profile: UserProfile, who_changes: get_user_model()) -
def get_users_list() -> list: def get_users_list() -> list:
""" """
Функция **get_users_list** возвращает список пользователей Zendesk, относящихся к организации SYSTEM. Функция возвращает список пользователей Zendesk, относящихся к организации SYSTEM.
""" """
zendesk = zenpy zendesk = zenpy
@ -95,23 +95,29 @@ def get_users_list() -> list:
return users return users
def get_tickets_list(email) -> list: def get_tickets_list(email: str) -> Optional[list]:
""" """
Функция возвращает список тикетов пользователя Zendesk Функция возвращает список тикетов пользователя Zendesk.
:param email: Email пользователя
:return: Список тикетов пользователя
""" """
return TicketListRequester().get_tickets_list_for_user(zenpy.get_user(email)) return TicketListRequester().get_tickets_list_for_user(zenpy.get_user(email))
def get_tickets_list_for_group(group_name): def get_tickets_list_for_group(group_name: str) -> Optional[list]:
""" """
Функция возвращает список неназначенных, нерешённых тикетов группы Zendesk Функция возвращает список не назначенных, не решённых тикетов группы Zendesk.
:param group_name: Название группы пользователя
:return: Список тикетов группы
""" """
return TicketListRequester().get_tickets_list_for_group(zenpy.get_group(group_name)) return TicketListRequester().get_tickets_list_for_group(zenpy.get_group(group_name))
def update_profile(user_profile: UserProfile) -> None: def update_profile(user_profile: UserProfile) -> None:
""" """
Функция обновляет профиль пользователя в соответствии с текущим в Zendesk. Функция обновляет профиль пользователя в БД в соответствии с текущим в Zendesk.
:param user_profile: Профиль пользователя :param user_profile: Профиль пользователя
:return: Обновленный, в соответствие с текущими данными в Zendesk, профиль пользователя :return: Обновленный, в соответствие с текущими данными в Zendesk, профиль пользователя
@ -148,6 +154,9 @@ def check_user_auth(email: str, password: str) -> bool:
""" """
Функция проверяет, верны ли входные данные. Функция проверяет, верны ли входные данные.
:param email: Email пользователя
:param password: Пароль пользователя
:return: Существует ли пользователь
:raise: :class:`APIException`: исключение, вызываемое если пользователь не аутентифицирован :raise: :class:`APIException`: исключение, вызываемое если пользователь не аутентифицирован
""" """
creds = { creds = {
@ -165,7 +174,7 @@ def check_user_auth(email: str, password: str) -> bool:
def update_user_in_model(profile: UserProfile, zendesk_user: ZenpyUser) -> None: def update_user_in_model(profile: UserProfile, zendesk_user: ZenpyUser) -> None:
""" """
Функция обновляет профиль пользователя при изменении данных пользователя на Zendesk. Функция обновляет профиль пользователя в модели при изменении данных пользователя на Zendesk.
:param profile: Профиль пользователя :param profile: Профиль пользователя
:param zendesk_user: Данные пользователя в Zendesk :param zendesk_user: Данные пользователя в Zendesk
@ -181,7 +190,10 @@ def update_user_in_model(profile: UserProfile, zendesk_user: ZenpyUser) -> None:
def count_users(users: list) -> tuple: def count_users(users: list) -> tuple:
""" """
Функция подсчета количества сотрудников с ролями engineer и light_agent Функция подсчета количества сотрудников с ролями engineer и light_agent.
:param users: Список пользователей
:return: Количество инженеров, количество light_agents
""" """
engineers, light_agents = 0, 0 engineers, light_agents = 0, 0
for user in users: for user in users:
@ -194,7 +206,7 @@ def count_users(users: list) -> tuple:
def update_users_in_model() -> list: def update_users_in_model() -> list:
""" """
Обновляет пользователей в модели UserProfile по списку пользователей в организации Обновляет пользователей в модели UserProfile по списку пользователей в организации.
""" """
users = get_users_list() users = get_users_list()
for user in users: for user in users:
@ -206,7 +218,7 @@ def update_users_in_model() -> list:
return users return users
def daterange(start_date: timedelta, end_date: timedelta) -> list: def daterange(start_date: date, end_date: date) -> list:
""" """
Функция возвращает список дней с start_date по end_date, исключая правую границу. Функция возвращает список дней с start_date по end_date, исключая правую границу.
@ -253,7 +265,13 @@ class DatabaseHandler(logging.Handler):
def __init__(self): def __init__(self):
logging.Handler.__init__(self) logging.Handler.__init__(self)
def emit(self, record): def emit(self, record: logging.LogRecord) -> None:
"""
Функция записи в базу данных лога с изменением роли пользователя.
:param record: Лог смены роли пользователя
:return: Запись в БД лога по смене роли пользователя с указанием новой и старой роли, а также автора изменения
"""
database = RoleChangeLogs() database = RoleChangeLogs()
users = record.msg users = record.msg
if users[1]: if users[1]:
@ -284,7 +302,7 @@ class CsvFormatter(logging.Formatter):
""" """
Функция форматирует запись смены роли пользователя в строку. Функция форматирует запись смены роли пользователя в строку.
:param record: Запись смены роли пользователя. :param record: Лог смены роли пользователя.
:return: Строка с записью смены пользователя. :return: Строка с записью смены пользователя.
""" """
users = record.msg users = record.msg
@ -307,7 +325,7 @@ class CsvFormatter(logging.Formatter):
return msg return msg
def log(user, admin=None): def log(user: get_user_model(), admin: get_user_model() = None) -> None:
""" """
Функция осуществляет запись логов в базу данных и csv файл. Функция осуществляет запись логов в базу данных и csv файл.
@ -335,7 +353,7 @@ def set_session_params_for_work_page(request: WSGIRequest, count: int = None, is
:param request: Получение данных с рабочей страницы пользователя :param request: Получение данных с рабочей страницы пользователя
:param count: Количество запрошенных тикетов :param count: Количество запрошенных тикетов
:param is_confirm: Назначение тикетов :param is_confirm: Назначены ли тикеты
:return: Перезагрузка страницы "Управление правами" соответствующего пользователя :return: Перезагрузка страницы "Управление правами" соответствующего пользователя
""" """
request.session['is_confirm'] = is_confirm request.session['is_confirm'] = is_confirm

View File

@ -1,5 +1,5 @@
""" """
Формы. Формы, использующиеся в приложении.
""" """
from django import forms from django import forms
from django.contrib.auth.forms import AuthenticationForm from django.contrib.auth.forms import AuthenticationForm

View File

@ -13,7 +13,6 @@ from access_controller.settings import ZENDESK_ROLES
class UserProfile(models.Model): class UserProfile(models.Model):
""" """
Модель профиля пользователя. Модель профиля пользователя.
Профиль создается и изменяется при создании и изменении модель User. Профиль создается и изменяется при создании и изменении модель User.
""" """
@ -24,18 +23,14 @@ class UserProfile(models.Model):
user = models.OneToOneField(to=get_user_model(), on_delete=models.CASCADE, help_text='Пользователь') user = models.OneToOneField(to=get_user_model(), on_delete=models.CASCADE, help_text='Пользователь')
role = models.CharField(default='None', max_length=100, help_text='Глобальное имя роли пользователя') role = models.CharField(default='None', max_length=100, help_text='Глобальное имя роли пользователя')
custom_role_id = models.IntegerField(default=0, help_text='Код роли пользователя') custom_role_id = models.BigIntegerField(default=0, help_text='Код роли пользователя')
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 @property
def zendesk_role(self) -> str: def zendesk_role(self) -> str:
""" """
Функция возвращает роль пользователя в Zendesk. Роль пользователя в Zendesk, либо UNDEFINED, если пользователь не найден.
В формате str, либо UNDEFINED, если пользователь не найден
:return: Роль пользователя в Zendesk
""" """
for role, r_id in ZENDESK_ROLES.items(): for role, r_id in ZENDESK_ROLES.items():
if r_id == self.custom_role_id: if r_id == self.custom_role_id:
@ -44,12 +39,12 @@ class UserProfile(models.Model):
@receiver(post_save, sender=get_user_model()) @receiver(post_save, sender=get_user_model())
def create_user_profile(instance, created, **kwargs) -> None: def create_user_profile(instance: get_user_model(), created: bool, **kwargs) -> None:
""" """
Функция создания профиля пользователя (Userprofile) при регистрации пользователя. Функция создания профиля пользователя (Userprofile) при регистрации пользователя.
:param instance: Экземпляр класса User :param instance: Экземпляр класса User
:param created: Создание профиля пользователя :param created: Существует ли пользователь
:param kwargs: Параметры :param kwargs: Параметры
:return: Обновленный список объектов профилей пользователей :return: Обновленный список объектов профилей пользователей
""" """
@ -58,7 +53,7 @@ def create_user_profile(instance, created, **kwargs) -> None:
@receiver(post_save, sender=get_user_model()) @receiver(post_save, sender=get_user_model())
def save_user_profile(instance, **kwargs) -> None: def save_user_profile(instance: get_user_model(), **kwargs) -> None:
""" """
Функция записи БД профиля пользователя. Функция записи БД профиля пользователя.
@ -75,8 +70,8 @@ class RoleChangeLogs(models.Model):
""" """
user = models.ForeignKey(to=get_user_model(), on_delete=models.CASCADE, user = models.ForeignKey(to=get_user_model(), on_delete=models.CASCADE,
help_text='Пользователь, которому присвоили другую роль') help_text='Пользователь, которому присвоили другую роль')
old_role = models.IntegerField(default=0, help_text='Старая роль') old_role = models.BigIntegerField(default=0, help_text='Старая роль')
new_role = models.IntegerField(default=0, help_text='Присвоенная роль') new_role = models.BigIntegerField(default=0, help_text='Присвоенная роль')
change_time = models.DateTimeField(default=timezone.now, help_text='Дата и время изменения роли') change_time = models.DateTimeField(default=timezone.now, help_text='Дата и время изменения роли')
changed_by = models.ForeignKey(to=get_user_model(), on_delete=models.CASCADE, related_name='changed_by', changed_by = models.ForeignKey(to=get_user_model(), on_delete=models.CASCADE, related_name='changed_by',
help_text='Кем была изменена роль') help_text='Кем была изменена роль')
@ -84,7 +79,7 @@ class RoleChangeLogs(models.Model):
class UnassignedTicketStatus(models.IntegerChoices): class UnassignedTicketStatus(models.IntegerChoices):
""" """
Класс статусов не распределенных тикетов. Модель статусов не распределенных тикетов.
:param UNASSIGNED: Снят с пользователя, перенесён в буферную группу :param UNASSIGNED: Снят с пользователя, перенесён в буферную группу
:param RESTORED: Авторство восстановлено :param RESTORED: Авторство восстановлено
@ -95,7 +90,7 @@ class UnassignedTicketStatus(models.IntegerChoices):
""" """
UNASSIGNED = 0, 'Снят с пользователя, перенесён в буферную группу' UNASSIGNED = 0, 'Снят с пользователя, перенесён в буферную группу'
RESTORED = 1, 'Авторство восстановлено' RESTORED = 1, 'Авторство восстановлено'
NOT_FOUND = 2, 'Пока нас не было, тикет испарился из ' \ NOT_FOUND = 2, 'Пока нас не было, тикет был перенесен из ' \
'буферной группы. Дополнительные действия не требуются' 'буферной группы. Дополнительные действия не требуются'
CLOSED = 3, 'Тикет уже был закрыт. Дополнительные действия не требуются' CLOSED = 3, 'Тикет уже был закрыт. Дополнительные действия не требуются'
SOLVED = 4, 'Тикет решён. Записан на пользователя с почтой SOLVED_TICKETS_EMAIL' SOLVED = 4, 'Тикет решён. Записан на пользователя с почтой SOLVED_TICKETS_EMAIL'

View File

@ -1,6 +1,8 @@
""" """
Обработка тикетов. Обработка тикетов, составление списков тикетов для пользователя и группы пользователей.
""" """
from typing import Optional
import requests import requests
from zenpy import TicketApi from zenpy import TicketApi
from zenpy.lib.api_objects import Ticket from zenpy.lib.api_objects import Ticket
@ -11,6 +13,13 @@ from main.zendesk_admin import zenpy
class TicketListRequester: class TicketListRequester:
""" """
Класс обработки тикетов. Класс обработки тикетов.
:param email: Email пользователя
:type display: :class:`str`
:param token_or_password: Токен или пароль
:type display: :class:`str`
:param prefix: Формат строка url страницы Zendesk
:type display: :class:`str`
""" """
def __init__(self): def __init__(self):
self.email = zenpy.credentials['email'] self.email = zenpy.credentials['email']
@ -21,16 +30,22 @@ class TicketListRequester:
self.token_or_password = zenpy.credentials.get('password') self.token_or_password = zenpy.credentials.get('password')
self.prefix = f'https://{zenpy.credentials.get("subdomain")}.zendesk.com/api/v2/' self.prefix = f'https://{zenpy.credentials.get("subdomain")}.zendesk.com/api/v2/'
def get_tickets_list_for_user(self, zendesk_user: zenpy) -> str: def get_tickets_list_for_user(self, zendesk_user: zenpy) -> Optional[list]:
""" """
Функция получения списка тикетов пользователя Zendesk. Функция получения списка тикетов пользователя Zendesk.
:param zendesk_user: Пользователь Zendesk
:return: Список тикетов, назначенных на данного пользователя в Zendesk
""" """
url = self.prefix + f'users/{zendesk_user.id}/tickets/assigned' url = self.prefix + f'users/{zendesk_user.id}/tickets/assigned'
return self._get_tickets(url) return self._get_tickets(url)
def get_tickets_list_for_group(self, group: zenpy) -> list(): def get_tickets_list_for_group(self, group: zenpy) -> Optional[list]:
""" """
Функция получения списка тикетов группы пользователей Zendesk. Функция получения списка тикетов группы пользователей Zendesk.
:param group: Название группы
:return: Список тикетов
""" """
url = self.prefix + '/tickets' url = self.prefix + '/tickets'
all_tickets = self._get_tickets(url) all_tickets = self._get_tickets(url)
@ -40,9 +55,12 @@ class TicketListRequester:
tickets.append(ticket) tickets.append(ticket)
return tickets return tickets
def _get_tickets(self, url: str) -> list(): def _get_tickets(self, url: str) -> Optional[list]:
""" """
Функция получения полного списка тикетов по url. Функция получения полного списка тикетов по url.
:param url: Url Zendesk c указанием тикетов, назначенных на пользователя
:return: Список тикетов
""" """
response = requests.get(url, auth=(self.email, self.token_or_password)) response = requests.get(url, auth=(self.email, self.token_or_password))
tickets = [] tickets = []

View File

@ -1,5 +1,5 @@
""" """
Сериализаторы. Сериализаторы, используемые в приложении.
""" """
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from rest_framework import serializers from rest_framework import serializers

View File

@ -1,6 +1,9 @@
""" """
Обработка статистики. Обработка статистики.
Обнаруживает факт изменения роли пользователя и вычисляет отработанное на смене время.
""" """
from datetime import date, datetime, timedelta from datetime import date, datetime, timedelta
from typing import Optional from typing import Optional
@ -14,7 +17,7 @@ from main.models import RoleChangeLogs
class StatisticData: class StatisticData:
""" """
Класс для учета статистики интервалов работы пользователей. Класс для учета статистики времени работы пользователей.
Передаваемые параметры: start_date, end_date, email, stat. Передаваемые параметры: start_date, end_date, email, stat.
:param display: Формат отображения времени (часы, минуты) :param display: Формат отображения времени (часы, минуты)
@ -37,7 +40,7 @@ class StatisticData:
: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: str, stat=None):
self.display = None self.display = None
self.interval = None self.interval = None
self.start_date = start_date self.start_date = start_date
@ -57,7 +60,8 @@ class StatisticData:
""" """
Функция возвращает статистику работы пользователя. Функция возвращает статистику работы пользователя.
:return: Словарь statistic с применением формата отображения и интервала работы(если они есть). :return: Словарь statistic с применением формата отображения
и интервала работы (если они есть).
None, если были ошибки при создании. None, если были ошибки при создании.
""" """
if self.is_valid_statistic(): if self.is_valid_statistic():
@ -117,7 +121,7 @@ class StatisticData:
""" """
return not self.errors return not self.errors
def _use_display(self, stat: list) -> list: def _use_display(self, stat: dict) -> dict:
""" """
Функция приводит данные к формату отображения. Функция приводит данные к формату отображения.
@ -136,7 +140,9 @@ class StatisticData:
def _use_interval(self, stat: dict) -> dict: def _use_interval(self, stat: dict) -> dict:
""" """
Функция объединяет ключи и значения в соответствии с интервалом работы. Переупаковка результата в соответствии с указанным временным диапазоном
Сжимает набор дней в месяцы, если указан режим работы "по месяцам"
:param stat: Статистика работы пользователя :param stat: Статистика работы пользователя
:return: Обновленная статистика :return: Обновленная статистика
@ -172,8 +178,8 @@ class StatisticData:
""" """
Функция возвращает логи в диапазоне дат start_date - end_date для пользователя с указанным email. Функция возвращает логи в диапазоне дат start_date - end_date для пользователя с указанным email.
:return: Данные о смене статусов пользователя. Если пользователь не найден или :return: Данные о смене статусов пользователя.
интервал времени некорректен - ошибку. Если пользователь не найден или интервал времени некорректен - ошибку.
""" """
if not self.check_time(): if not self.check_time():
self.errors += ['Конец диапазона должен быть позже начала диапазона и раньше текущего времени'] self.errors += ['Конец диапазона должен быть позже начала диапазона и раньше текущего времени']
@ -208,9 +214,12 @@ class StatisticData:
if self.data[log_index].new_role == ROLES['engineer']: if self.data[log_index].new_role == ROLES['engineer']:
self.engineer_logic(log_index) self.engineer_logic(log_index)
def engineer_logic(self, log_index): def engineer_logic(self, log_index: int) -> None:
""" """
Функция обрабатывает основную часть работы инженера Функция вычисляет время работы инженера.
:param log_index: Индекс текущего лога
:return: Дополняет статистику работы инженера временем между текущим и последующим логом
""" """
current_log, next_log = self.data[log_index], self.data[log_index + 1] current_log, next_log = self.data[log_index], self.data[log_index + 1]
if current_log.change_time.date() != next_log.change_time.date(): if current_log.change_time.date() != next_log.change_time.date():
@ -222,9 +231,14 @@ class StatisticData:
elapsed_time = next_log.change_time - current_log.change_time elapsed_time = next_log.change_time - current_log.change_time
self.statistic[current_log.change_time.date()] += elapsed_time.total_seconds() self.statistic[current_log.change_time.date()] += elapsed_time.total_seconds()
def post_engineer_logic(self, last_log): def post_engineer_logic(self, last_log: RoleChangeLogs) -> None:
""" """
Функция обрабатывает случай, когда нам изветсно что инженер работал и после диапазона Обработка случая, в котором инженер не закрыл смену.
В таком случае считается всё время от момента открытия смены до текущего момента.
:param last_log: Последний лог изменения роли, в результате которого пользователь назначен инженером.
:return: Дополняет статистику работы
""" """
self.fill_daterange(last_log.change_time.date() + timedelta(days=1), self.end_date + timedelta(days=1)) 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(): if last_log.change_time.date() == timezone.now().date():
@ -237,15 +251,25 @@ class StatisticData:
if self.end_date == timezone.now().date(): if self.end_date == timezone.now().date():
self.statistic[self.end_date] = get_timedelta(None, timezone.now().time()).total_seconds() self.statistic[self.end_date] = get_timedelta(None, timezone.now().time()).total_seconds()
def prev_engineer_logic(self, first_log): def prev_engineer_logic(self, first_log: RoleChangeLogs) -> None:
""" """
Функция обрабатывает случай, когда нам изветсно что инженер начал работу до диапазона Обработка случая, в котором инженер закрыл смену в отражаемом периоде, а открыл её до этого периода.
В таком случае должен быть учтён только период от начала отображаемого диапазона до закрытия смены.
:param first_log_log: Первый лог в диапазоне, в результате которого пользователь назначен легким агентом.
:return: Дополняет статистику работы
""" """
self.fill_daterange(max(get_user_model().objects.get(email=self.email).date_joined.date(), self.start_date), self.fill_daterange(
first_log.change_time.date()) max(
get_user_model().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() 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) -> None:
""" """
Функция заполняет диапазон дат значением val (по умолчанию val = кол-во секунд в 1 дне). Функция заполняет диапазон дат значением val (по умолчанию val = кол-во секунд в 1 дне).
@ -256,9 +280,11 @@ class StatisticData:
for day in daterange(first, last): for day in daterange(first, last):
self.statistic[day] = val self.statistic[day] = val
def clear_statistic(self) -> dict: def clear_statistic(self) -> None:
""" """
Функция осуществляет обновление всех дней. Чистка статистики и установка времени по умолчанию.
Устанавливает время смены в 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)

View File

@ -19,6 +19,8 @@
user-select: none; user-select: none;
} }
@media (min-width: 768px) { @media (min-width: 768px) {
.bd-placeholder-img-lg { .bd-placeholder-img-lg {
font-size: 3.5rem; font-size: 3.5rem;

View File

@ -24,7 +24,7 @@
{% 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">
<h6 class="table-title">Список сотрудников</h6> <h3 class="py-4 text-center font-weight-bold">Список сотрудников</h3>
{% block table %} {% block table %}
<div id="table"></div> <div id="table"></div>

View File

@ -23,7 +23,7 @@
{% block content %} {% block content %}
<br> <br>
<div class="row px-4 py-4"> <div class="row px-4 py-4">
<div class="col-auto"> <div class="col-auto px-4 py-4">
<div class="container"> <div class="container">
<img <img
src="{% if profile.image %}{{ profile.image }}{% else %}{% static 'no_avatar.png' %}{% endif %}" src="{% if profile.image %}{{ profile.image }}{% else %}{% static 'no_avatar.png' %}{% endif %}"
@ -31,7 +31,9 @@
alt="Нет изображения" alt="Нет изображения"
> >
</div> </div>
<a href="{%url 'password_change' %}">Сменить пароль</a> <div class="px-3 py-5">
<a href="{%url 'password_change' %}" class="btn btn-info">Сменить пароль</a>
</div>
</div> </div>
<div class="col"> <div class="col">
<h4><span class="badge bg-secondary text-light">Имя пользователя</span></h4> <h5><strong>{{ profile.name }}</strong></h5> <h4><span class="badge bg-secondary text-light">Имя пользователя</span></h4> <h5><strong>{{ profile.name }}</strong></h5>
@ -44,7 +46,7 @@
{% elif profile.custom_role_id == ZENDESK_ROLES.light_agent %} {% elif profile.custom_role_id == ZENDESK_ROLES.light_agent %}
<h5><strong>light_agent</strong></h5> <h5><strong>light_agent</strong></h5>
{% else %} {% else %}
<h5><strong><small class="text-muted">None</small></strong></h5> <h5><strong><small class="text-muted">Без роли</small></strong></h5>
{% endif %} {% endif %}
</div> </div>
@ -52,7 +54,7 @@
<br> <br>
<div align="center" > <div align="center" >
<form action=""> <form action="">
<a href="{% url 'work' profile.user.id %}" class="btn btn-primary btn-lg">Запросить права доступа</a> <a href="{% url 'work' profile.user.id %}" type="submit" class="btn default-button btn-success btn-lg px-4 py-3">Запросить права доступа</a>
</form> </form>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -7,21 +7,21 @@
{% block heading %} Страницы просмотра статистики{% endblock %} {% block heading %} Страницы просмотра статистики{% endblock %}
{% block content%} {% block content%}
<div class="mt-5"> <div class="mt-5 py-4">
<div class="container-fluid" style="font-size:2rem"> <div class="container-fluid" style="font-size:2rem">
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
<div class="row g-3"> <div class="row g-3">
<div class="col-auto"> <div class="col-auto p-2">
{{ form.email.label }} <h4 class="py-1">{{ form.email.label }}</h4>
</div> </div>
<div class="col-auto mt-4"> <div class="col-auto mt-4">
{{ form.email }} {{ form.email }}
</div> </div>
</div> </div>
<div class="row g-3 mt-4"> <div class="row g-3 mt-4">
<div class="col-auto"> <div class="col-auto p-2">
{{ form.interval.label }} <h4 class="py-1">{{ form.interval.label }}</h4>
</div> </div>
<div class="col-auto"> <div class="col-auto">
{% for radio in form.interval%} {% for radio in form.interval%}
@ -33,8 +33,8 @@
</div> </div>
</div> </div>
<div class="row g-3 mt-4"> <div class="row g-3 mt-4">
<div class="col-auto"> <div class="col-auto p-2">
{{ form.display_format.label }} <h4 class="py-1">{{ form.display_format.label }}</h4>
</div> </div>
<div class="col-auto"> <div class="col-auto">
{% for radio in form.display_format%} {% for radio in form.display_format%}
@ -46,8 +46,8 @@
</div> </div>
</div> </div>
<div class="row g-3 mt-4"> <div class="row g-3 mt-4">
<div class="col-auto"> <div class="col-auto p-2">
{{ form.range_start.label}} <h4 class="py-1">{{ form.range_start.label}}</h4>
</div> </div>
<div class="col-auto"> <div class="col-auto">
<div class='col-sm-7'> <div class='col-sm-7'>
@ -56,8 +56,8 @@
</div> </div>
</div> </div>
<div class="row g-3 mt-4"> <div class="row g-3 mt-4">
<div class="col-auto"> <div class="col-auto p-2">
{{ form.range_end.label}} <h4 class="py-1">{{ form.range_end.label}}</h4>
</div> </div>
<div class="col-auto"> <div class="col-auto">
<div class='col-sm-7'> <div class='col-sm-7'>
@ -65,9 +65,9 @@
</div> </div>
</div> </div>
</div> </div>
<div class="form-row text-center"> <div class="form-row text-center py-5">
<div class="col-12"> <div class="col-12">
<button type="submit" class="btn btn-outline-primary">Посмотреть статистику</button> <button type="submit" class="btn default-button btn-info py-3 px-5">Посмотреть статистику</button>
</div> </div>
</div> </div>
</form> </form>

View File

@ -16,66 +16,73 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="container-md">
<div class="new-section"> <p class="row page-description">Свободных Мест: {{ licences_remaining }}</p>
<p class="row page-description">Свободных Мест: {{ licences_remaining }}</p>
</div>
<div class="row justify-content-center new-section"> <div class="row justify-content-center new-section">
<div class="col-10"> <div class="col-10 py-3">
<h6 class="table-title">Список сотрудников с правами инженера</h6> <h3 class="py-4 text-center font-weight-bold">Список сотрудников с правами инженера</h3>
<table class="light-table"> <table class="table table-dark">
<thead> <thead>
<th>Email</th> <th>Email</th>
<th>Name</th> <th>Name</th>
</thead> </thead>
<tbody> <tbody>
{% for engineer in engineers %} {% for engineer in engineers %}
<tr> <tr>
<td>{{ engineer.email }}</td> <td>{{ engineer.email }}</td>
<td>{{ engineer.name }}</td> <td>{{ engineer.name }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
</div>
<div class="row justify-content-center new-section">
<div class="col-5">
<div class="info">
<div class="info-row">
<div class="info-target">инженеров: </div>
<div class="info-quantity">
<div class="status-circle-small light-green"></div>
<span class="info-quantity-value">{{ engineers|length }}</span>
</div>
</div>
<div class="info-row">
<div class="info-target">легких агентов:</div>
<div class="info-quantity">
<div class="status-circle-small light-yellow"></div>
<span class="info-quantity-value">{{ agents|length }}</span>
</div>
</div>
</div>
</div>
<div class="col-5">
<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>
</div>
<div class="col-10">
<form method="post" action="{% url 'work_get_tickets' %}">
{% csrf_token %}
{{ get_tickets_form.count_tickets }}
<button type="submit" class="default-button">Взять тикеты в работу</button>
</form>
</div>
{% for message in messages %}
<script>create_notification('{{message}}','','{{message.tags}}',2000)</script>
{% endfor %}
</div>
</div> </div>
<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|length }}</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">{{ agents|length }}</span>
</div>
</div>
</div>
</div>
<div class="col-5 mb-3 py-1">
<div class="row mb-2">
<div class="col">
<a href="/work/become_engineer" class="btn btn-success btn-block py-3">Получить права инженера</a>
</div>
</div>
<div class="row mb-2 py-1">
<div class="col">
<a href="/work/hand_over" class="btn btn-danger btn-block py-3">Сдать права инженера</a>
</div>
</div>
<form class="row g-3 align-items-center" method="GET" action="/work/get_tickets">
<div class="col-2">
<input class="form-control" type="number" min="1" value="1" name="count_tickets">
</div>
<div class="col-10">
<button type="submit" class="btn default-button btn-warning btn-block btn-sm py-3">Взять тикеты в работу</button>
</div>
</form>
</div>
</div>
{% for message in messages %}
<script>create_notification('{{message}}','','{{message.tags}}',5000)</script>
{% endfor %}
{% endblock %} {% endblock %}

View File

@ -1,5 +1,5 @@
""" """
Тесты. Тестирование работы программы.
""" """
@ -22,14 +22,35 @@ from main.extra_func import log
class UsersBaseTestCase(TestCase): class UsersBaseTestCase(TestCase):
"""Базовый класс загружения данных для тестов с пользователями""" """
Базовый класс загрузки данных для тестов с пользователями.
Для тестов используются фикстуры тестовых пользователей (test_users.json).
"""
fixtures = ['fixtures/test_users.json'] fixtures = ['fixtures/test_users.json']
def setUp(self): def setUp(self) -> None:
"""Добавление в переменные почт и клиентов для пользователей""" """
Функция предустановки значений переменных.
Добавляем email тестовых пользователей и создаем клиентов для тестов.
:param light_agent: email тестового пользователя с правами light_agent
:type light_agent: :class:`str`
:param engineer: email тестового пользователя с правами engineer
:type engineer: :class:`str`
:param admin: email тестового пользователя с правами admin
:type admin: :class:`str`
:param agent_client: клиент, залогиненный как пользователь с email light_agent
:type agent_client: :class:`django.test.client.Client`
:param engineer_client: клиент, залогиненный как пользователь с email engineer
:type engineer_client: :class:`django.test.client.Client`
:param admin_client: клиент, залогиненный как пользователь с email admin
:type admin_client: :class:`django.test.client.Client`
"""
self.light_agent = '123@test.ru' self.light_agent = '123@test.ru'
self.admin = 'admin@gmail.com' self.admin = 'admin@gmail.com'
self.engineer = 'customer@example.com' self.engineer = 'customer@example.com'
self.agent_client = Client() self.agent_client = Client()
self.agent_client.force_login(get_user_model().objects.get(email=self.light_agent)) self.agent_client.force_login(get_user_model().objects.get(email=self.light_agent))
self.admin_client = Client() self.admin_client = Client()
@ -40,13 +61,27 @@ class UsersBaseTestCase(TestCase):
class RegistrationTestCase(TestCase): class RegistrationTestCase(TestCase):
""" """
Класс тестирования регистрации пользователя. Класс тестирования регистрации.
Для тестов используются фикстуры с данными пользователей engineer и light_agent (data.json).
""" """
fixtures = ['fixtures/data.json'] fixtures = ['fixtures/data.json']
def setUp(self) -> None: def setUp(self) -> None:
""" """
Функция предтестовых настроек. Функция предустановки значений переменных.
Добавляем email тестовых пользователей и создаем клиентов для тестов.
:param email_backend: locmem бэкенд со списком отправленных писем
:type email_backend: :class:`str`
:param any_zendesk_user_email: email пользователя, зарегистрированного на Zendesk
:type any_zendesk_user_email: :class:`str`
:param zendesk_admin_email: email администратора
:type zendesk_admin_email: :class:`str`
:param client: новый клиент
:type client: :class:`django.test.client.Client`
""" """
self.email_backend = 'django.core.mail.backends.locmem.EmailBackend' self.email_backend = 'django.core.mail.backends.locmem.EmailBackend'
self.any_zendesk_user_email = 'idar.sokurov.05@mail.ru' self.any_zendesk_user_email = 'idar.sokurov.05@mail.ru'
@ -55,31 +90,43 @@ class RegistrationTestCase(TestCase):
def test_registration_complete_redirect(self) -> None: def test_registration_complete_redirect(self) -> None:
""" """
Функция тестирования успешно завершенной регистрации. Функция тестирования успешной регистрации пользователя.
Проверяет, что в случае если email пользователя зарегистрирован на Zendesk, была заполнена форма регистрации
и направлено письмо со ссылкой для завершения регистрации, происходит редирект на страницу завершения
регистрации.
""" """
with self.settings(EMAIL_BACKEND=self.email_backend): with self.settings(EMAIL_BACKEND=self.email_backend):
resp = self.client.post(reverse('registration'), data={'email': self.any_zendesk_user_email}) resp = self.client.post(reverse('registration'), data={'email': self.any_zendesk_user_email})
self.assertRedirects(resp, reverse('password_reset_done')) self.assertRedirects(resp, reverse('password_reset_done'))
def test_registration_fail_redirect(self): def test_registration_fail_redirect(self) -> None:
""" """
Функция тестирования неуспешной регистрации. Функция тестирования не успешной регистрации пользователя (введен email, не зарегистрированный на Zendesk).
Проверяет, что происходит редирект на страницу "registration disallowed"
""" """
with self.settings(EMAIL_BACKEND=self.email_backend): with self.settings(EMAIL_BACKEND=self.email_backend):
resp = self.client.post(reverse('registration'), data={'email': self.any_zendesk_user_email + 'asd'}) resp = self.client.post(reverse('registration'), data={'email': self.any_zendesk_user_email + 'asd'})
self.assertRedirects(resp, reverse('django_registration_disallowed')) self.assertRedirects(resp, reverse('django_registration_disallowed'))
def test_registration_user_already_exist(self): def test_registration_user_already_exist(self) -> None:
""" """
Функция тестирования попытки регистрации уже зарегистрированного пользователя. Функция тестирования попытки зарегистрироваться, используя email уже зарегистрированного в приложении
пользователя ("123@test.ru").
Проверяет, что пользователь получает сообщение "Этот адрес электронной почты уже используется"
""" """
with self.settings(EMAIL_BACKEND=self.email_backend) and translation.override('ru'): with self.settings(EMAIL_BACKEND=self.email_backend) and translation.override('ru'):
resp = self.client.post(reverse('registration'), data={'email': '123@test.ru'}) resp = self.client.post(reverse('registration'), data={'email': '123@test.ru'})
self.assertContains(resp, 'Этот адрес электронной почты уже используется', count=1, status_code=200) self.assertContains(resp, 'Этот адрес электронной почты уже используется', count=1, status_code=200)
def test_registration_send_email(self): def test_registration_send_email(self) -> None:
""" """
Функция тестирования отправки email. Функция тестирования отправки email пользователю при регистрации.
Проверяет отправку уведомления на указанный пользователем адрес, а также содержание письма (заголовка и тела)
через email locmem backend.
""" """
with self.settings(EMAIL_BACKEND=self.email_backend): with self.settings(EMAIL_BACKEND=self.email_backend):
response: HttpResponseRedirect = \ response: HttpResponseRedirect = \
@ -95,9 +142,11 @@ class RegistrationTestCase(TestCase):
correct_body = render_to_string('registration/password_reset_email.html', email_context, response.request) correct_body = render_to_string('registration/password_reset_email.html', email_context, response.request)
self.assertEqual(mail.outbox[0].body, correct_body) self.assertEqual(mail.outbox[0].body, correct_body)
def test_registration_user_creating(self): def test_registration_user_creating(self) -> None:
""" """
Функция тестирования регистрации пользователя (сверяем имя с именем в Zendesk. Функция тестирования создания пользователя приложения при регистрации.
Проверяет соответствие имени созданного пользователя с именем пользователя в Zendesk
""" """
with self.settings(EMAIL_BACKEND=self.email_backend): with self.settings(EMAIL_BACKEND=self.email_backend):
self.client.post(reverse('registration'), data={'email': self.any_zendesk_user_email}) self.client.post(reverse('registration'), data={'email': self.any_zendesk_user_email})
@ -105,9 +154,11 @@ class RegistrationTestCase(TestCase):
zendesk_user = zenpy.get_user(self.any_zendesk_user_email) zendesk_user = zenpy.get_user(self.any_zendesk_user_email)
self.assertEqual(user.userprofile.name, zendesk_user.name) self.assertEqual(user.userprofile.name, zendesk_user.name)
def test_permissions_applying(self): def test_permissions_applying(self) -> None:
""" """
Функция тестирования проверке присвоения роли admin. Функция тестирования создания администратора и присвоения ему соответствующих прав.
Проверяет, что у созданного пользователя роль "admin" и права "has_control_access".
""" """
with self.settings(EMAIL_BACKEND=self.email_backend): with self.settings(EMAIL_BACKEND=self.email_backend):
self.client.post(reverse('registration'), data={'email': self.zendesk_admin_email}) self.client.post(reverse('registration'), data={'email': self.zendesk_admin_email})
@ -118,47 +169,67 @@ class RegistrationTestCase(TestCase):
class MakeEngineerTestCase(UsersBaseTestCase): class MakeEngineerTestCase(UsersBaseTestCase):
""" """
Класс тестов для проверки функции назначения роли engineer. Класс тестирования присвоения пользователю роли engineer.
В тестах используется @patch('main.extra_func.zenpy') Mock для работы с API Zendesk.
""" """
@patch('main.extra_func.zenpy') @patch('main.extra_func.zenpy')
def test_become_engineer_redirect(self, _zenpy_mock): def test_become_engineer_redirect(self, _zenpy_mock: Mock) -> None:
""" """
Функция проверки переадресации пользователя на рабочую страницу после назначения роли engineer. Функция тестирования редиректа на рабочую страницу тестового пользователя при назначении его инженером.
:param _zenpy_mock: Mock объекта zenpy для функций, работающих с API Zendesk.
""" """
user = get_user_model().objects.get(email=self.light_agent) user = get_user_model().objects.get(email=self.light_agent)
resp = self.agent_client.post(reverse_lazy('work_become_engineer')) resp = self.agent_client.post(reverse_lazy('work_become_engineer'))
self.assertRedirects(resp, reverse('work', args=[user.id])) self.assertRedirects(resp, reverse('work', args=[user.id]))
self.assertEqual(resp.status_code, 302) self.assertEqual(resp.status_code, 302)
self.assertFalse(_zenpy_mock.called)
@patch('main.extra_func.zenpy') @patch('main.extra_func.zenpy')
def test_light_agent_make_engineer(self, zenpy_mock): def test_light_agent_make_engineer(self, zenpy_mock: Mock) -> None:
""" """
Функция проверки назначения light_agent на роль engineer. Функция тестирования назначения легкого агента на роль инженера.
Проверяет установку роли "engineer" в Zendesk.
:param zenpy_mock: Mock объекта zenpy для функций, работающих с API Zendesk.
""" """
self.agent_client.post(reverse_lazy('work_become_engineer')) self.agent_client.post(reverse_lazy('work_become_engineer'))
self.assertEqual(zenpy_mock.update_user.call_args[0][0].custom_role_id, sets.ZENDESK_ROLES['engineer']) self.assertEqual(zenpy_mock.update_user.call_args[0][0].custom_role_id, sets.ZENDESK_ROLES['engineer'])
@patch('main.extra_func.zenpy') @patch('main.extra_func.zenpy')
def test_admin_make_engineer(self, zenpy_mock): def test_admin_make_engineer(self, zenpy_mock: Mock) -> None:
""" """
Функция проверки назначения admin на роль engineer. Функция тестирования назначения администратора на роль инженера.
Проверяет установку роли "engineer" в Zendesk.
:param zenpy_mock: Mock объекта zenpy для функций, работающих с API Zendesk.
""" """
self.admin_client.post(reverse_lazy('work_become_engineer')) self.admin_client.post(reverse_lazy('work_become_engineer'))
self.assertEqual(zenpy_mock.update_user.call_args[0][0].custom_role_id, sets.ZENDESK_ROLES['engineer']) self.assertEqual(zenpy_mock.update_user.call_args[0][0].custom_role_id, sets.ZENDESK_ROLES['engineer'])
@patch('main.extra_func.zenpy') @patch('main.extra_func.zenpy')
def test_engineer_make_engineer(self, zenpy_mock): def test_engineer_make_engineer(self, zenpy_mock: Mock) -> None:
""" """
Функция проверки назначения engineer на роль engineer. Функция тестирования назначения инженера на роль инженера.
Проверяет установку роли "engineer" в Zendesk.
:param zenpy_mock: Mock объекта zenpy для функций, работающих с API Zendesk.
""" """
self.engineer_client.post(reverse_lazy('work_become_engineer')) self.engineer_client.post(reverse_lazy('work_become_engineer'))
self.assertEqual(zenpy_mock.update_user.call_args[0][0].custom_role_id, sets.ZENDESK_ROLES['engineer']) self.assertEqual(zenpy_mock.update_user.call_args[0][0].custom_role_id, sets.ZENDESK_ROLES['engineer'])
@patch('main.extra_func.zenpy') @patch('main.extra_func.zenpy')
def test_control_page_make_engineer_one(self, zenpy_mock): def test_control_page_make_engineer_one(self, zenpy_mock: Mock) -> None:
""" """
Функция проверки назначения администратором на роль engineer одного пользователя. Функция тестирования назначения администратором одного инженера на странице "Управление".
Проверяет обновление администратором роли пользователя с light_agent на engineer.
:param zenpy_mock: Mock объекта zenpy для функций, работающих с API Zendesk.
""" """
self.admin_client.post( self.admin_client.post(
reverse_lazy('control'), reverse_lazy('control'),
@ -171,9 +242,13 @@ class MakeEngineerTestCase(UsersBaseTestCase):
self.assertEqual(mock_object.custom_role_id, sets.ZENDESK_ROLES['engineer']) self.assertEqual(mock_object.custom_role_id, sets.ZENDESK_ROLES['engineer'])
@patch('main.extra_func.zenpy') @patch('main.extra_func.zenpy')
def test_control_page_make_engineer_many(self, zenpy_mock): def test_control_page_make_engineer_many(self, zenpy_mock: Mock) -> None:
""" """
Функция проверки назначения администратором на роль engineer нескольких пользователей. Функция тестирования назначения администратором нескольких инженеров на странице "Управление".
Проверяет обновление администратором ролей двух пользователей с light_agent на engineer.
:param zenpy_mock: Mock объекта zenpy для функций, работающих с API Zendesk.
""" """
self.admin_client.post( self.admin_client.post(
reverse_lazy('control'), reverse_lazy('control'),
@ -193,10 +268,23 @@ class MakeEngineerTestCase(UsersBaseTestCase):
class MakeLightAgentTestCase(UsersBaseTestCase): class MakeLightAgentTestCase(UsersBaseTestCase):
"""
Класс тестирования присвоения пользователю роли light_agent.
В тестах используется @patch('main.extra_func.zenpy') Mock для работы API Zendesk, а также
@patch('main.requester.TicketListRequester.get_tickets_list_for_user', side_effect=[[]]), предоставляющий пустой
список в качестве списка тикетов пользователя.
"""
@patch('main.requester.TicketListRequester.get_tickets_list_for_user', side_effect=[[]]) @patch('main.requester.TicketListRequester.get_tickets_list_for_user', side_effect=[[]])
@patch('main.extra_func.zenpy') @patch('main.extra_func.zenpy')
def test_hand_over_redirect(self, _zenpy_mock, _user_tickets_mock): def test_hand_over_redirect(self, _zenpy_mock: Mock, _user_tickets_mock: Mock) -> None:
"""
Функция тестирования переадресации инженера на рабочую страницу, после сдачи прав.
:param _zenpy_mock: Mock объекта zenpy для функций, работающих с API Zendesk.
:param _user_tickets_mock: Mock, заменяющий список тикетов пользователя на пустой список.
"""
user = get_user_model().objects.get(email=self.engineer) user = get_user_model().objects.get(email=self.engineer)
resp = self.engineer_client.post(reverse_lazy('work_hand_over')) resp = self.engineer_client.post(reverse_lazy('work_hand_over'))
self.assertRedirects(resp, reverse('work', args=[user.id])) self.assertRedirects(resp, reverse('work', args=[user.id]))
@ -204,7 +292,15 @@ class MakeLightAgentTestCase(UsersBaseTestCase):
@patch('main.requester.TicketListRequester.get_tickets_list_for_user', side_effect=[[]]) @patch('main.requester.TicketListRequester.get_tickets_list_for_user', side_effect=[[]])
@patch('main.extra_func.zenpy') @patch('main.extra_func.zenpy')
def test_engineer_make_light_agent_no_tickets(self, zenpy_mock, _user_tickets_mock): def test_engineer_make_light_agent_no_tickets(self, zenpy_mock: Mock, _user_tickets_mock: Mock) -> None:
"""
Функция тестирования назначения инженера легким агентом, в случае, когда у него в работе нет тикетов.
Проверяет назначение роли light_agent в Zendesk.
:param zenpy_mock: Mock объекта zenpy для функций, работающих с API Zendesk.
:param _user_tickets_mock: Mock, заменяющий список тикетов пользователя на пустой список.
"""
self.engineer_client.post(reverse_lazy('work_hand_over')) self.engineer_client.post(reverse_lazy('work_hand_over'))
self.assertEqual(zenpy_mock.update_user.call_args[0][0].custom_role_id, sets.ZENDESK_ROLES['light_agent']) self.assertEqual(zenpy_mock.update_user.call_args[0][0].custom_role_id, sets.ZENDESK_ROLES['light_agent'])
@ -212,7 +308,18 @@ class MakeLightAgentTestCase(UsersBaseTestCase):
[Mock(id=1, status='solved'), Mock(id=2, status='open'), Mock(id=3, status='open')] [Mock(id=1, status='solved'), Mock(id=2, status='open'), Mock(id=3, status='open')]
]) ])
@patch('main.extra_func.zenpy') @patch('main.extra_func.zenpy')
def test_engineer_make_light_agent_with_tickets(self, zenpy_mock, _user_tickets_mock): def test_engineer_make_light_agent_with_tickets(self, zenpy_mock: Mock, _user_tickets_mock: Mock) -> None:
"""
Функция тестирования назначения инженера легким агентом, в случае, когда у него в работе есть тикеты.
Для тестирования принимается, что в работе у инженера находится 3 тикета, один в состоянии: решен,
два в состоянии: открыт.
Проверяет распределение тикетов (поместить в решенные или назначить нового ответственного),
а также назначение роли light_agent в Zendesk.
:param zenpy_mock: Mock объекта zenpy для функций, работающих с API Zendesk.
:param _user_tickets_mock: Mock, заменяющий список тикетов пользователя на пустой список.
"""
zenpy_mock.solved_tickets_user_id = Mock() zenpy_mock.solved_tickets_user_id = Mock()
self.engineer_client.post(reverse_lazy('work_hand_over')) self.engineer_client.post(reverse_lazy('work_hand_over'))
@ -224,7 +331,15 @@ class MakeLightAgentTestCase(UsersBaseTestCase):
@patch('main.requester.TicketListRequester.get_tickets_list_for_user', side_effect=[[]]) @patch('main.requester.TicketListRequester.get_tickets_list_for_user', side_effect=[[]])
@patch('main.extra_func.zenpy') @patch('main.extra_func.zenpy')
def test_admin_make_light_agent_no_tickets(self, zenpy_mock, _user_tickets_mock): def test_admin_make_light_agent_no_tickets(self, zenpy_mock: Mock, _user_tickets_mock: Mock) -> None:
"""
Функция тестирования назначения администратора на роль легкого агента.
Проверяет назначение роли light_agent в Zendesk.
:param zenpy_mock: Mock объекта zenpy для функций, работающих с API Zendesk.
:param _user_tickets_mock: Mock, заменяющий список тикетов пользователя на пустой список.
"""
self.admin_client.post(reverse_lazy('work_hand_over')) self.admin_client.post(reverse_lazy('work_hand_over'))
self.assertEqual(zenpy_mock.update_user.call_args[0][0].custom_role_id, sets.ZENDESK_ROLES['light_agent']) self.assertEqual(zenpy_mock.update_user.call_args[0][0].custom_role_id, sets.ZENDESK_ROLES['light_agent'])
@ -232,7 +347,18 @@ class MakeLightAgentTestCase(UsersBaseTestCase):
[Mock(id=1, status='solved'), Mock(id=2, status='open'), Mock(id=3, status='open')] [Mock(id=1, status='solved'), Mock(id=2, status='open'), Mock(id=3, status='open')]
]) ])
@patch('main.extra_func.zenpy') @patch('main.extra_func.zenpy')
def test_admin_make_light_agent_with_tickets(self, zenpy_mock, _user_tickets_mock): def test_admin_make_light_agent_with_tickets(self, zenpy_mock: zenpy, _user_tickets_mock: list) -> None:
"""
Функция тестирования назначения администратора легким агентом, в случае, когда у него в работе есть тикеты.
Для тестирования принимается, что в работе находится 3 тикета, один в состоянии: решен,
два в состоянии: открыт.
Проверяет распределение тикетов (поместить в решенные или назначить нового ответственного),
а также назначение роли light_agent в Zendesk.
:param zenpy_mock: Mock объекта zenpy для функций, работающих с API Zendesk.
:param _user_tickets_mock: Mock, заменяющий список тикетов пользователя на пустой список.
"""
zenpy_mock.solved_tickets_user_id = Mock() zenpy_mock.solved_tickets_user_id = Mock()
self.admin_client.post(reverse_lazy('work_hand_over')) self.admin_client.post(reverse_lazy('work_hand_over'))
@ -244,19 +370,33 @@ class MakeLightAgentTestCase(UsersBaseTestCase):
@patch('main.requester.TicketListRequester.get_tickets_list_for_user', side_effect=[[]]) @patch('main.requester.TicketListRequester.get_tickets_list_for_user', side_effect=[[]])
@patch('main.extra_func.zenpy') @patch('main.extra_func.zenpy')
def test_light_agent_make_light_agent(self, zenpy_mock, _user_tickets_mock): def test_light_agent_make_light_agent(self, zenpy_mock: Mock, _user_tickets_mock: Mock) -> None:
"""
Функция тестирования назначения легкого агента на роль легкого агента.
Проверяет назначение роли light_agent в Zendesk.
:param zenpy_mock: Mock объекта zenpy для функций, работающих с API Zendesk.
:param _user_tickets_mock: Mock, заменяющий список тикетов пользователя на пустой список.
"""
self.agent_client.post(reverse_lazy('work_hand_over')) self.agent_client.post(reverse_lazy('work_hand_over'))
self.assertEqual(zenpy_mock.update_user.call_args[0][0].custom_role_id, sets.ZENDESK_ROLES['light_agent']) self.assertEqual(zenpy_mock.update_user.call_args[0][0].custom_role_id, sets.ZENDESK_ROLES['light_agent'])
@patch('main.requester.TicketListRequester.get_tickets_list_for_user', side_effect=[[]]) @patch('main.requester.TicketListRequester.get_tickets_list_for_user', side_effect=[[]])
@patch('main.extra_func.zenpy') @patch('main.extra_func.zenpy')
def test_control_page_make_light_agent_one(self, zenpy_mock, _user_tickets_mock): def test_control_page_make_light_agent_one(self, zenpy_mock: Mock, _user_tickets_mock: Mock) -> None:
"""
Функция тестирования назначения администратором одного легкого агента на странице "Управление".
Проверяет обновление администратором роли пользователя с engineer на light_agent.
:param zenpy_mock: Mock объекта zenpy для функций, работающих с API Zendesk.
:param _user_tickets_mock: Mock, заменяющий список тикетов пользователя на пустой список.
"""
self.admin_client.post( self.admin_client.post(
reverse_lazy('control'), reverse_lazy('control'),
data={ data={'users': [get_user_model().objects.get(email=self.engineer).userprofile.id],
'users': [get_user_model().objects.get(email=self.engineer).userprofile.id], 'light_agent': 'light_agent'}
'light_agent': 'light_agent'
}
) )
call_list = zenpy_mock.update_user.call_args_list call_list = zenpy_mock.update_user.call_args_list
mock_object = call_list[0][0][0] mock_object = call_list[0][0][0]
@ -265,7 +405,16 @@ class MakeLightAgentTestCase(UsersBaseTestCase):
@patch('main.requester.TicketListRequester.get_tickets_list_for_user', side_effect=[[], []]) @patch('main.requester.TicketListRequester.get_tickets_list_for_user', side_effect=[[], []])
@patch('main.extra_func.zenpy') @patch('main.extra_func.zenpy')
def test_control_page_make_light_agent_many(self, zenpy_mock, _user_tickets_mock): def test_control_page_make_light_agent_many(self, zenpy_mock: Mock, _user_tickets_mock: Mock) -> None:
"""
Функция тестирования назначения администратором нескольких легких агентов на странице "Управление".
Проверяет обновление администратором ролей двух пользователей с engineer на light_agent.
:param zenpy_mock: Mock объекта zenpy для функций, работающих с API Zendesk.
:param _user_tickets_mock: Mock, заменяющий список тикетов пользователя на пустой список.
"""
self.admin_client.post( self.admin_client.post(
reverse_lazy('control'), reverse_lazy('control'),
data={ data={
@ -285,27 +434,30 @@ class MakeLightAgentTestCase(UsersBaseTestCase):
class PasswordResetTestCase(UsersBaseTestCase): class PasswordResetTestCase(UsersBaseTestCase):
""" """
Класс тестов сброса пароля. Класс тестирования сброса пароля.
""" """
def setUp(self): def setUp(self):
"""
Предустановленные значения для проведения тестов.
"""
super().setUp() super().setUp()
self.email_backend = 'django.core.mail.backends.locmem.EmailBackend' self.email_backend = 'django.core.mail.backends.locmem.EmailBackend'
def test_redirect(self): def test_redirect(self) -> None:
""" """
Функция проверки переадресации на страницу уведомления о сбросе пароля на email. Функция тестирования успешной смены пароля.
Проверяется переадресация на страницу завершения смены пароля, в случае, когда пользователь существует и на его
email было направлено письмо для сброса пароля.
""" """
with self.settings(EMAIL_BACKEND=self.email_backend): with self.settings(EMAIL_BACKEND=self.email_backend):
resp = self.agent_client.post(reverse_lazy('password_reset'), data={'email': self.light_agent}) resp = self.agent_client.post(reverse_lazy('password_reset'), data={'email': self.light_agent})
self.assertRedirects(resp, reverse('password_reset_done')) self.assertRedirects(resp, reverse('password_reset_done'))
self.assertEqual(resp.status_code, 302) self.assertEqual(resp.status_code, 302)
def test_send_email(self): def test_send_email(self) -> None:
""" """
Функция проверки содержания и отправки письма для установки пароля. Функция тестирования отправки email для сброса пароля.
Проверяет наличие отправленного письма, и его содержание, сверяет email адресата с email пользователя.
""" """
with self.settings(EMAIL_BACKEND=self.email_backend): with self.settings(EMAIL_BACKEND=self.email_backend):
response: HttpResponseRedirect = \ response: HttpResponseRedirect = \
@ -321,25 +473,25 @@ class PasswordResetTestCase(UsersBaseTestCase):
correct_body = render_to_string('registration/password_reset_email.html', email_context, response.request) correct_body = render_to_string('registration/password_reset_email.html', email_context, response.request)
self.assertEqual(mail.outbox[0].body, correct_body) self.assertEqual(mail.outbox[0].body, correct_body)
def test_email_invalid(self): def test_email_invalid(self) -> None:
""" """
Функция проверки уведомления клиента о некорректности введенного email. Функция тестирования попытки смены пароля с некорректным email.
Проверяет уведомление пользователя о неверном адресе электронной почты.
""" """
with self.settings(EMAIL_BACKEND=self.email_backend) and translation.override('ru'): with self.settings(EMAIL_BACKEND=self.email_backend) and translation.override('ru'):
resp = self.agent_client.post(reverse_lazy('password_reset'), data={'email': 1}) resp = self.agent_client.post(reverse_lazy('password_reset'), data={'email': 1})
self.assertContains(resp, 'Введите правильный адрес электронной почты.', count=1, status_code=200) self.assertContains(resp, 'Введите правильный адрес электронной почты.', count=1, status_code=200)
def test_user_does_not_exist(self): def test_user_does_not_exist(self) -> None:
""" """
Функция корректности отработки неверно введенного email. Функция тестирования попытки смены пароля с email, который не зарегистрирован.
Проверяет отсутствие отправки письма о смене пароля.
""" """
with self.settings(EMAIL_BACKEND=self.email_backend): with self.settings(EMAIL_BACKEND=self.email_backend):
resp = self.agent_client.post( resp = self.agent_client.post(reverse_lazy('password_reset'),
reverse_lazy('password_reset'), data={'email': self.light_agent + str(random.random())})
data={
'email': self.light_agent + str(random.random())
}
)
self.assertRedirects(resp, reverse('password_reset_done')) self.assertRedirects(resp, reverse('password_reset_done'))
self.assertEqual(resp.status_code, 302) self.assertEqual(resp.status_code, 302)
self.assertEqual(len(mail.outbox), 0) self.assertEqual(len(mail.outbox), 0)
@ -349,25 +501,27 @@ class PasswordChangeTestCase(UsersBaseTestCase):
""" """
Класс тестирования смены пароля. Класс тестирования смены пароля.
""" """
def setUp(self):
""" def setUp(self) -> None:
Предустановленные значения для проведения тестов.
"""
super().setUp() super().setUp()
self.set_password() self.set_password()
def set_password(self): def set_password(self) -> None:
""" """
Пароль, сформированный для тестирования. Функция предустанавливает тестовому пользователю с ролью light_agent пароль 'ImpossiblyHardPassword' и создает
клиента с соответствующими данным для тестирования.
""" """
user: get_user_model() = get_user_model().objects.get(email=self.light_agent) user = get_user_model().objects.get(email=self.light_agent)
user.set_password('ImpossiblyHardPassword') user.set_password('ImpossiblyHardPassword')
user.save() user.save()
self.agent_client.force_login(get_user_model().objects.get(email=self.light_agent)) self.agent_client.force_login(get_user_model().objects.get(email=self.light_agent))
def test_change_successful(self): def test_change_successful(self) -> None:
""" """
Функция тестирования успешного изменения пароля. Функция тестирования успешной смены пароля.
Проверяет установку нового пароля пользователю при вводе корректных данных: старый пароль, новый пароль
(2 раза).
""" """
self.agent_client.post( self.agent_client.post(
reverse_lazy('password_change'), reverse_lazy('password_change'),
@ -380,9 +534,11 @@ class PasswordChangeTestCase(UsersBaseTestCase):
user = get_user_model().objects.get(email=self.light_agent) user = get_user_model().objects.get(email=self.light_agent)
self.assertTrue(user.check_password('EasyPassword')) self.assertTrue(user.check_password('EasyPassword'))
def test_invalid_old_password(self): def test_invalid_old_password(self) -> None:
""" """
Функция тестирования отработки неверно введенного старого пароля при смене. Функция тестирования смены пароля, при неверном вводе старого пароля.
Проверяет текст уведомления пользователя 'Ваш старый пароль введен неправильно'.
""" """
with translation.override('ru'): with translation.override('ru'):
resp = self.agent_client.post( resp = self.agent_client.post(
@ -395,9 +551,11 @@ class PasswordChangeTestCase(UsersBaseTestCase):
) )
self.assertContains(resp, 'Ваш старый пароль введен неправильно', count=1, status_code=200) self.assertContains(resp, 'Ваш старый пароль введен неправильно', count=1, status_code=200)
def test_different_new_passwords(self): def test_different_new_passwords(self) -> None:
""" """
Функция тестирования случая с вводом двух разных новых паролей. Функция тестирования смены пароля, при вводе не совпадающих новых паролей.
Проверяет текст уведомления пользователя 'Введенные пароли не совпадают'.
""" """
with translation.override('ru'): with translation.override('ru'):
resp = self.agent_client.post( resp = self.agent_client.post(
@ -412,7 +570,9 @@ class PasswordChangeTestCase(UsersBaseTestCase):
def test_invalid_new_password1(self): def test_invalid_new_password1(self):
""" """
Функция тестирования случая с неправильно подобранным новым паролем (слишком короткий). Функция тестирования попытки смены пароля, когда новый пароль не соответствует требованиям: слишком короткий.
Проверяет текст уведомления пользователя 'Введённый пароль слишком короткий'.
""" """
with translation.override('ru'): with translation.override('ru'):
resp = self.agent_client.post( resp = self.agent_client.post(
@ -425,9 +585,12 @@ class PasswordChangeTestCase(UsersBaseTestCase):
) )
self.assertContains(resp, 'Введённый пароль слишком короткий', count=1, status_code=200) self.assertContains(resp, 'Введённый пароль слишком короткий', count=1, status_code=200)
def test_invalid_new_password2(self): def test_invalid_new_password2(self) -> None:
""" """
Функция тестирования случая с неправильно подобранным новым паролем (употребляются только цифры). Функция тестирования попытки смены пароля, когда новый пароль не соответствует требованиям: состоит
только из цифр.
Проверяет текст уведомления пользователя 'Введённый пароль состоит только из цифр'.
""" """
with translation.override('ru'): with translation.override('ru'):
resp = self.agent_client.post( resp = self.agent_client.post(
@ -442,7 +605,10 @@ class PasswordChangeTestCase(UsersBaseTestCase):
def test_invalid_new_password3(self): def test_invalid_new_password3(self):
""" """
Функция тестирования случая с неправильно подобранным новым паролем (совпадает с именем пользователя). Функция тестирования попытки смены пароля, когда новый пароль не соответствует требованиям: аналогичен имени
пользователя.
Проверяет текст уведомления пользователя 'Введённый пароль слишком похож на имя пользователя'.
""" """
with translation.override('ru'): with translation.override('ru'):
resp = self.agent_client.post( resp = self.agent_client.post(
@ -459,26 +625,40 @@ class PasswordChangeTestCase(UsersBaseTestCase):
class GetTicketsTestCase(UsersBaseTestCase): class GetTicketsTestCase(UsersBaseTestCase):
""" """
Класс тестов для проверки функции получения тикетов. Класс тестов для проверки функции получения тикетов.
В тестах используются @patch('main.views.zenpy.get_user') и @patch('main.views.zenpy.get_user')
для работы с API Zendesk.
""" """
@patch('main.views.zenpy.get_user') @patch('main.views.zenpy.get_user')
@patch('main.extra_func.zenpy') @patch('main.extra_func.zenpy')
def test_redirect(self, _zenpy_mock, get_user_mock): def test_redirect(self, _zenpy_mock: Mock, get_user_mock: Mock) -> None:
""" """
Функция проверки переадресации пользователя на рабочую страницу. Функция проверки переадресации пользователя на рабочую страницу.
Проверяет редирект на рабочую страницу, в случае, когда пользователь с правами инженера заполняет форму
принятия тикетов в работу.
:param _zenpy_mock: Mock объекта zenpy для функций, работающих с API Zendesk.
:param get_user_mock: Mock объекта zenpy_user.
""" """
get_user_mock.return_value = Mock() get_user_mock.return_value = Mock()
user = get_user_model().objects.get(email=self.engineer) user = get_user_model().objects.get(email=self.engineer)
resp = self.engineer_client.post(reverse('work_get_tickets')) resp = self.engineer_client.post(reverse('work_get_tickets'))
self.assertRedirects(resp, reverse('work', args=[user.id])) self.assertRedirects(resp, reverse('work', args=[user.id]))
self.assertEqual(resp.status_code, 302) self.assertEqual(resp.status_code, 302)
self.assertFalse(_zenpy_mock.called)
@patch('main.views.zenpy') @patch('main.views.zenpy')
@patch('main.views.get_tickets_list_for_group') @patch('main.views.get_tickets_list_for_group')
def test_take_one_ticket(self, group_tickets_mock, zenpy_mock): def test_take_one_ticket(self, group_tickets_mock: Mock, zenpy_mock: Mock) -> None:
""" """
Функция проверки назначения одного тикета на engineer. Функция проверки назначения одного тикета на engineer.
Проверяет соответствие ответственного за тикет объекта tickets и тестового клиента правами инженера,
направившего запрос на назначение одного тикета.
:param zenpy_mock: Mock объекта zenpy для функций, работающих с API Zendesk.
:param group_tickets_mock: Mock списка не назначенных и нерешенных тикетов группы.
""" """
group_tickets_mock.return_value = [Mock()] group_tickets_mock.return_value = [Mock()]
zenpy_mock.get_user.return_value = Mock(role='agent', custom_role_id=sets.ZENDESK_ROLES['engineer']) zenpy_mock.get_user.return_value = Mock(role='agent', custom_role_id=sets.ZENDESK_ROLES['engineer'])
@ -488,9 +668,15 @@ class GetTicketsTestCase(UsersBaseTestCase):
@patch('main.views.get_tickets_list_for_group') @patch('main.views.get_tickets_list_for_group')
@patch('main.views.zenpy') @patch('main.views.zenpy')
def test_take_many_tickets(self, zenpy_mock, group_tickets_mock): def test_take_many_tickets(self, zenpy_mock: Mock, group_tickets_mock: Mock) -> None:
""" """
Функция проверки назначения нескольких тикетов на engineer. Функция проверки назначения нескольких тикетов на engineer.
Проверяет соответствие ответственного за тикеты объекта tickets и тестового клиента правами инженера,
направившего запрос на назначение трех тикетов.
:param zenpy_mock: Mock объекта zenpy для функций, работающих с API Zendesk.
:param group_tickets_mock: Mock списка не назначенных и нерешенных тикетов группы.
""" """
group_tickets_mock.return_value = [Mock()] * 3 group_tickets_mock.return_value = [Mock()] * 3
zenpy_mock.get_user.return_value = Mock(role='agent', custom_role_id=sets.ZENDESK_ROLES['engineer']) zenpy_mock.get_user.return_value = Mock(role='agent', custom_role_id=sets.ZENDESK_ROLES['engineer'])
@ -501,9 +687,12 @@ class GetTicketsTestCase(UsersBaseTestCase):
@patch('main.views.zenpy.get_user') @patch('main.views.zenpy.get_user')
@patch('main.views.zenpy') @patch('main.views.zenpy')
def test_light_agent_take_ticket(self, zenpy_mock, get_user_mock): def test_light_agent_take_ticket(self, zenpy_mock: Mock, get_user_mock: Mock) -> None:
""" """
Функция проверки попытки назначения тикета на light_agent. Функция проверки попытки назначения тикета на light_agent.
:param zenpy_mock: Mock объекта zenpy для функций, работающих с API Zendesk.
:param get_user_mock: Mock объекта zenpy_user.
""" """
get_user_mock.return_value = Mock(role='agent', custom_role_id=sets.ZENDESK_ROLES['light_agent']) get_user_mock.return_value = Mock(role='agent', custom_role_id=sets.ZENDESK_ROLES['light_agent'])
self.agent_client.post(reverse('work_get_tickets'), data={'count_tickets': 3}) self.agent_client.post(reverse('work_get_tickets'), data={'count_tickets': 3})
@ -512,9 +701,14 @@ class GetTicketsTestCase(UsersBaseTestCase):
@patch('main.views.zenpy') @patch('main.views.zenpy')
@patch('main.views.get_tickets_list_for_group') @patch('main.views.get_tickets_list_for_group')
def test_take_zero_tickets(self, tickets_mock, zenpy_mock): def test_take_zero_tickets(self, tickets_mock: Mock, zenpy_mock: Mock) -> None:
""" """
Функция проверки попытки назначения нуля тикета на engineer. Функция проверки попытки назначения нулевого количества тикетов.
Проверяет, что список тикетов остался пустым.
:param zenpy_mock: Mock объекта zenpy для функций, работающих с API Zendesk.
:param tickets_mock: Mock списка тикетов - возвращает пустой список.
""" """
tickets_mock.return_value = [Mock()] * 3 tickets_mock.return_value = [Mock()] * 3
zenpy_mock.get_user.return_value = Mock(role='agent', custom_role_id=sets.ZENDESK_ROLES['engineer']) zenpy_mock.get_user.return_value = Mock(role='agent', custom_role_id=sets.ZENDESK_ROLES['engineer'])
@ -524,9 +718,15 @@ class GetTicketsTestCase(UsersBaseTestCase):
@patch('main.views.get_tickets_list_for_group') @patch('main.views.get_tickets_list_for_group')
@patch('main.views.zenpy') @patch('main.views.zenpy')
def test_take_invalid_count_tickets(self, zenpy_mock, group_tickets_mock): def test_take_invalid_count_tickets(self, zenpy_mock: Mock, group_tickets_mock: Mock) -> None:
""" """
Функция проверки попытки назначения нуля тикетов на engineer. Функция проверки попытки назначения некорректного количества тикетов (введении в форму назначения тикетов
не числового значения, а строки).
Проверяет, отсутствие списка тикетов.
:param zenpy_mock: Mock объекта zenpy для функций, работающих с API Zendesk.
:param group_tickets_mock: Mock списка не назначенных и нерешенных тикетов группы.
""" """
group_tickets_mock.return_value = [Mock()] * 3 group_tickets_mock.return_value = [Mock()] * 3
zenpy_mock.get_user.return_value = Mock(role='agent', custom_role_id=sets.ZENDESK_ROLES['engineer']) zenpy_mock.get_user.return_value = Mock(role='agent', custom_role_id=sets.ZENDESK_ROLES['engineer'])
@ -538,12 +738,25 @@ class GetTicketsTestCase(UsersBaseTestCase):
class ProfileTestCase(TestCase): class ProfileTestCase(TestCase):
""" """
Класс тестов для проверки синхронизации профиля пользователя. Класс тестов для проверки синхронизации профиля пользователя.
Для тестов используются фикстуры тестовых пользователей (profile.json).
""" """
fixtures = ['fixtures/profile.json'] fixtures = ['fixtures/profile.json']
def setUp(self): def setUp(self) -> None:
""" """
Предустановленные значения для проведения тестов. Функция предустановки значений переменных.
Добавляем email тестовых пользователей Zendesk и создаем клиентов для тестов.
:param zendesk_agent_email: email тестового пользователя с правами light_agent
:type zendesk_agent_email: :class:`str`
:param zendesk_admin_email: email тестового пользователя с правами admin
:type zendesk_admin_email: :class:`str`
:param client: клиент, залогиненный как пользователь с email zendesk_agent_email
:type client: :class:`django.test.client.Client`
:param admin_client: клиент, залогиненный как пользователь с zendesk_admin_email
:type admin_client: :class:`django.test.client.Client`
""" """
self.zendesk_agent_email = 'krav-88@mail.ru' self.zendesk_agent_email = 'krav-88@mail.ru'
self.zendesk_admin_email = 'idar.sokurov.05@mail.ru' self.zendesk_admin_email = 'idar.sokurov.05@mail.ru'
@ -552,32 +765,42 @@ class ProfileTestCase(TestCase):
self.admin_client = Client() self.admin_client = Client()
self.admin_client.force_login(get_user_model().objects.get(email=self.zendesk_admin_email)) self.admin_client.force_login(get_user_model().objects.get(email=self.zendesk_admin_email))
def test_correct_username(self): def test_correct_username(self) -> None:
""" """
Функция проверки синхронизации имени пользователя. Функция проверки синхронизации имени пользователя.
Проверяет соответствие имени пользователя из контекста страницы профиля имени пользователя в Zendesk.
""" """
resp = self.client.get(reverse('profile')) resp = self.client.get(reverse('profile'))
self.assertEqual(resp.context['profile'].name, zenpy.get_user(self.zendesk_agent_email).name) self.assertEqual(resp.context['profile'].name, zenpy.get_user(self.zendesk_agent_email).name)
def test_correct_email(self): def test_correct_email(self) -> None:
""" """
Функция проверки синхронизации почты пользователя. Функция проверки синхронизации почты пользователя.
Проверяет соответствие email пользователя из контекста страницы профиля email пользователя в Zendesk.
""" """
resp = self.client.get(reverse('profile')) resp = self.client.get(reverse('profile'))
self.assertEqual(resp.context['profile'].user.email, zenpy.get_user(self.zendesk_agent_email).email) self.assertEqual(resp.context['profile'].user.email, zenpy.get_user(self.zendesk_agent_email).email)
def test_correct_role(self): def test_correct_role(self) -> None:
""" """
Функция проверки синхронизации роли пользователя. Функция проверки синхронизации роли пользователя.
Проверяет соответствие роли пользователя из контекста страницы профиля роли пользователя в Zendesk. Проверка
осуществляется на примере администратора и агента.
""" """
resp = self.client.get(reverse('profile')) resp = self.client.get(reverse('profile'))
self.assertEqual(resp.context['profile'].role, zenpy.get_user(self.zendesk_agent_email).role) self.assertEqual(resp.context['profile'].role, zenpy.get_user(self.zendesk_agent_email).role)
resp = self.admin_client.get(reverse('profile')) resp = self.admin_client.get(reverse('profile'))
self.assertEqual(resp.context['profile'].role, zenpy.get_user(self.zendesk_admin_email).role) self.assertEqual(resp.context['profile'].role, zenpy.get_user(self.zendesk_admin_email).role)
def test_correct_custom_role_id(self): def test_correct_custom_role_id(self) -> None:
""" """
Функция проверки синхронизации рабочей роли пользователя. Функция проверки синхронизации рабочей роли пользователя.
Проверяет соответствие id рабочей роли пользователя из контекста страницы профиля id
роли пользователя в Zendesk. Проверка осуществляется на примере администратора и агента.
""" """
resp = self.client.get(reverse('profile')) resp = self.client.get(reverse('profile'))
user = zenpy.get_user(self.zendesk_agent_email) user = zenpy.get_user(self.zendesk_agent_email)
@ -586,9 +809,11 @@ class ProfileTestCase(TestCase):
user = zenpy.get_user(self.zendesk_admin_email) user = zenpy.get_user(self.zendesk_admin_email)
self.assertEqual(resp.context['profile'].custom_role_id, user.custom_role_id if user.custom_role_id else 0) self.assertEqual(resp.context['profile'].custom_role_id, user.custom_role_id if user.custom_role_id else 0)
def test_correct_image(self): def test_correct_image(self) -> None:
""" """
Функция проверки синхронизации изображения пользователя. Функция проверки синхронизации изображения пользователя.
Проверяет соответствие аватарки пользователя из контекста страницы профиля аватарке пользователя в Zendesk.
""" """
resp = self.client.get(reverse('profile')) resp = self.client.get(reverse('profile'))
user = zenpy.get_user(self.zendesk_agent_email) user = zenpy.get_user(self.zendesk_agent_email)
@ -596,38 +821,78 @@ class ProfileTestCase(TestCase):
class LoggingTestCase(UsersBaseTestCase): class LoggingTestCase(UsersBaseTestCase):
"""
Класс тестирования процесса логгирования.
"""
def setUp(self): def setUp(self) -> None:
"""
Функция предустановки значений переменных.
Определяем профили пользователей с разными ролями.
:param admin_profile: профиль тестового пользователя с правами admin
:type admin_profile: :class:`Userprofile`
:param agent_profile: профиль тестового пользователя с правами light_agent
:type agent_profile: :class:`Userprofile`
:param engineer_profile: профиль тестового пользователя с правами engineer
:type engineer_profile: :class:`Userprofile`
"""
super().setUp() super().setUp()
self.admin_profile = get_user_model().objects.get(email=self.admin).userprofile self.admin_profile = get_user_model().objects.get(email=self.admin).userprofile
self.agent_profile = get_user_model().objects.get(email=self.light_agent).userprofile self.agent_profile = get_user_model().objects.get(email=self.light_agent).userprofile
self.engineer_profile = get_user_model().objects.get(email=self.engineer).userprofile self.engineer_profile = get_user_model().objects.get(email=self.engineer).userprofile
@staticmethod @staticmethod
def get_file_output(): def get_file_output() -> str:
"""
Получение данных из файла логов.
"""
with open('logs/logs.csv', 'r') as file: with open('logs/logs.csv', 'r') as file:
file_output = file.readlines()[-1] file_output = file.readlines()[-1]
return file_output return file_output
def test_engineer_with_admin(self): def test_engineer_with_admin(self) -> None:
"""
Функция проверки корректной записи лога по смене роли инженера в файл.
Сравнивает запись в файле и созданный лог с переданными значениями профилей инженера и администратора
для смены прав.
"""
log(self.engineer_profile, self.admin_profile) log(self.engineer_profile, self.admin_profile)
file_output = self.get_file_output() file_output = self.get_file_output()
self.assertEqual(file_output, f'UserForAccessTest,engineer,' self.assertEqual(file_output, f'UserForAccessTest,engineer,'
f'{str(timezone.now().today())[:16]},ZendeskAdmin\n') f'{str(timezone.now().today())[:16]},ZendeskAdmin\n')
def test_engineer_without_admin(self): def test_engineer_without_admin(self) -> None:
"""
Функция проверки корректной записи лога по смене роли инженера в файл без указания администратора.
Сравнивает запись в файле и созданный лог с переданным значением профиля инженера для смены прав.
"""
log(self.engineer_profile) log(self.engineer_profile)
file_output = self.get_file_output() file_output = self.get_file_output()
self.assertEqual(file_output, f'UserForAccessTest,engineer,' self.assertEqual(file_output, f'UserForAccessTest,engineer,'
f'{str(timezone.now().today())[:16]},UserForAccessTest\n') f'{str(timezone.now().today())[:16]},UserForAccessTest\n')
def test_light_agent_with_admin(self): def test_light_agent_with_admin(self) -> None:
"""
Функция проверки корректной записи лога по смене роли агента в файл.
Сравнивает запись в файле и созданный лог с переданными значениями профилей агента и администратора
для смены прав.
"""
log(self.agent_profile, self.admin_profile) log(self.agent_profile, self.admin_profile)
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]},ZendeskAdmin\n') f'{str(timezone.now().today())[:16]},ZendeskAdmin\n')
def test_light_agent_without_admin(self): def test_light_agent_without_admin(self) -> None:
"""
Функция проверки корректной записи лога по смене роли агента в файл без указания администратора.
Сравнивает запись в файле и созданный лог с переданным значением профиля агента для смены прав.
"""
log(self.agent_profile) log(self.agent_profile)
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,'

View File

@ -61,7 +61,7 @@ def setup_context(**kwargs) -> Dict[str, Any]:
class CustomRegistrationView(RegistrationView): class CustomRegistrationView(RegistrationView):
""" """
Отображение и логика работы страницы регистрации пользователя. Класс отображения и логики работы страницы регистрации пользователя.
:param form_class: Форма, которую необходимо заполнить для регистрации :param form_class: Форма, которую необходимо заполнить для регистрации
:type form_class: :class:`forms.CustomRegistrationForm` :type form_class: :class:`forms.CustomRegistrationForm`
@ -86,9 +86,12 @@ class CustomRegistrationView(RegistrationView):
def register(self, form: CustomRegistrationForm) -> Optional[get_user_model()]: def register(self, form: CustomRegistrationForm) -> Optional[get_user_model()]:
""" """
Функция регистрации пользователя. Функция регистрации пользователя.
1. Ввод email пользователя, указанный на Zendesk
1. Ввод email пользователя, указанный на Zendesk.
2. В случае если пользователь с данным паролем зарегистрирован на Zendesk и относится к организации SYSTEM, 2. В случае если пользователь с данным паролем зарегистрирован на Zendesk и относится к организации SYSTEM,
происходит сброс ссылки с установлением пароля на указанный email происходит сброс ссылки с установлением пароля на указанный email.
3. Создается пользователь class User, а также его профиль. 3. Создается пользователь class User, а также его профиль.
:param form: Email пользователя на Zendesk :param form: Email пользователя на Zendesk
@ -133,7 +136,7 @@ class CustomRegistrationView(RegistrationView):
""" """
Функция дает разрешение на просмотр страница администратора, если пользователь имеет роль admin. Функция дает разрешение на просмотр страница администратора, если пользователь имеет роль admin.
:param user: авторизованный пользователь (получает разрешение, имея роль "admin") :param user: Авторизованный пользователь (получает разрешение, имея роль "admin")
""" """
if user.userprofile.role == 'admin': if user.userprofile.role == 'admin':
content_type = ContentType.objects.get_for_model(UserProfile) content_type = ContentType.objects.get_for_model(UserProfile)
@ -148,8 +151,8 @@ class CustomRegistrationView(RegistrationView):
Функция возвращает url-адрес страницы, куда нужно перейти после успешной/не успешной регистрации. Функция возвращает url-адрес страницы, куда нужно перейти после успешной/не успешной регистрации.
Используется самой django-registration. Используется самой django-registration.
:param user: пользователь, пытающийся зарегистрироваться :param user: Пользователь, пытающийся зарегистрироваться
:return: адресация на страницу успешной регистрации :return: Адресация на страницу успешной регистрации
""" """
return self.urls[self.redirect_url] return self.urls[self.redirect_url]
@ -158,8 +161,8 @@ def registration_error(request: WSGIRequest) -> HttpResponse:
""" """
Функция отображения страницы ошибки регистрации. Функция отображения страницы ошибки регистрации.
:param request: регистрация :param request: Регистрация
:return: адресация на страницу ошибки :return: Адресация на страницу ошибки
""" """
return render(request, 'django_registration/registration_error.html') return render(request, 'django_registration/registration_error.html')
@ -169,8 +172,8 @@ def profile_page(request: WSGIRequest) -> HttpResponse:
""" """
Функция отображения страницы профиля. Функция отображения страницы профиля.
:param request: данные пользователя из БД :param request: Данные пользователя из БД
:return: адресация на страницу пользователя :return: Адресация на страницу пользователя
""" """
user_profile: UserProfile = request.user.userprofile user_profile: UserProfile = request.user.userprofile
update_profile(user_profile) update_profile(user_profile)
@ -187,9 +190,9 @@ def work_page(request: WSGIRequest, required_id: int) -> HttpResponse:
""" """
Функция отображения страницы "Управления правами" для текущего пользователя (login_required). Функция отображения страницы "Управления правами" для текущего пользователя (login_required).
:param request: объект пользователя :param request: Объект пользователя
:param id: id пользователя, используется для динамической адресации :param id: id пользователя, используется для динамической адресации
:return: адресация на страницу "Управления правами" (либо на страницу "Авторизации", если id и user.id не совпадают :return: Адресация на страницу "Управления правами" (либо на страницу "Авторизации", если id и user.id не совпадают
""" """
users = get_users_list() users = get_users_list()
if request.user.id == required_id: if request.user.id == required_id:
@ -227,8 +230,8 @@ def work_hand_over(request: WSGIRequest) -> HttpResponseRedirect:
""" """
Функция позволяет текущему пользователю сдать права, а именно сменить в Zendesk роль с "engineer" на "light_agent" Функция позволяет текущему пользователю сдать права, а именно сменить в Zendesk роль с "engineer" на "light_agent"
:param request: данные текущего пользователя (login_required) :param request: Данные текущего пользователя (login_required)
:return: перезагрузка текущей страницы после выполнения смены роли :return: Перезагрузка текущей страницы после выполнения смены роли
""" """
make_light_agent(request.user.userprofile, request.user) make_light_agent(request.user.userprofile, request.user)
return set_session_params_for_work_page(request) return set_session_params_for_work_page(request)
@ -240,8 +243,8 @@ def work_become_engineer(request: WSGIRequest) -> HttpResponseRedirect:
Функция позволяет текущему пользователю получить права, а именно сменить в Zendesk роль с "light_agent" Функция позволяет текущему пользователю получить права, а именно сменить в Zendesk роль с "light_agent"
на "engineer". на "engineer".
:param request: данные текущего пользователя (login_required) :param request: Данные текущего пользователя (login_required)
:return: перезагрузка текущей страницы после выполнения смены роли :return: Перезагрузка текущей страницы после выполнения смены роли
""" """
make_engineer(request.user.userprofile, request.user) make_engineer(request.user.userprofile, request.user)
return set_session_params_for_work_page(request) return set_session_params_for_work_page(request)
@ -250,9 +253,10 @@ def work_become_engineer(request: WSGIRequest) -> HttpResponseRedirect:
@login_required() @login_required()
def work_get_tickets(request: WSGIRequest) -> HttpResponse: def work_get_tickets(request: WSGIRequest) -> HttpResponse:
""" """
Функция получения тикетов в работу.
:param request: :param request: Запрос на принятие тикетов в работу
:return: :return: Перезагрузка рабочей страницы
""" """
zenpy_user = zenpy.get_user(request.user.email) zenpy_user = zenpy.get_user(request.user.email)
@ -289,6 +293,8 @@ class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageM
:type form_class: :class:`forms.AdminPageUsersForm` :type form_class: :class:`forms.AdminPageUsersForm`
:param success_url: Адрес страницы администратора :param success_url: Адрес страницы администратора
:type success_url: :class:`HttpResponseRedirect` :type success_url: :class:`HttpResponseRedirect`
:param success_message: Уведомление об изменении прав
:type success_url: :class:`str`
""" """
permission_required = 'main.has_control_access' permission_required = 'main.has_control_access'
template_name = 'pages/adm_ruleset.html' template_name = 'pages/adm_ruleset.html'
@ -333,7 +339,12 @@ class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageM
class CustomLoginView(LoginView): class CustomLoginView(LoginView):
""" """
Отображение страницы авторизации пользователя Класс отображения страницы авторизации пользователя.
:param extra_context: Добавление в контекст статус пользователя "залогинен"
:type extra_context: :class:`dict`
:param form_class: Форма страницы авторизации
:type form_class: :class: forms.CustomAuthenticationForm
""" """
extra_context = setup_context(login_lit=True) extra_context = setup_context(login_lit=True)
form_class = CustomAuthenticationForm form_class = CustomAuthenticationForm
@ -353,7 +364,8 @@ class UsersViewSet(viewsets.ReadOnlyModelViewSet):
def list(self, request: WSGIRequest, *args, **kwargs) -> Response: def list(self, request: WSGIRequest, *args, **kwargs) -> Response:
""" """
Функция возвращает список пользователей, список пользователей Zendesk, количество engineers и light-agents. Функция возвращает список пользователей Zendesk, количество engineers и light-agents.
:param request: Запрос :param request: Запрос
:param args: Аргументы :param args: Аргументы
:param kwargs: Параметры :param kwargs: Параметры
@ -376,6 +388,7 @@ class UsersViewSet(viewsets.ReadOnlyModelViewSet):
def choose_users(zendesk: list, model: list) -> list: def choose_users(zendesk: list, model: list) -> list:
""" """
Функция формирует список пользователей, которые не зарегистрированы у нас. Функция формирует список пользователей, которые не зарегистрированы у нас.
:param zendesk: Список пользователей Zendesk :param zendesk: Список пользователей Zendesk
:param model: Список пользователей (модель Userprofile) :param model: Список пользователей (модель Userprofile)
:return: Список :return: Список
@ -389,7 +402,8 @@ class UsersViewSet(viewsets.ReadOnlyModelViewSet):
@staticmethod @staticmethod
def get_zendesk_users(users: list) -> list: def get_zendesk_users(users: list) -> list:
""" """
Получение списка пользователей Zendesk, не являющихся админами. Функция получения списка пользователей Zendesk, не являющихся админами.
:param users: Список пользователей :param users: Список пользователей
:return: Список пользователей, не являющимися администраторами. :return: Список пользователей, не являющимися администраторами.
""" """
@ -406,8 +420,8 @@ def statistic_page(request: WSGIRequest) -> HttpResponse:
""" """
Функция отображения страницы статистики (для "superuser"). Функция отображения страницы статистики (для "superuser").
:param request: данные о пользователе: email, время и интервал работы. Данные получаем через forms.StatisticForm :param request: Данные о пользователе: email, время и интервал работы. Данные получаем через forms.StatisticForm
:return: адресация на страницу статистики :return: Адресация на страницу статистики
""" """
# if not request.user.has_perm('main.has_control_access'): # if not request.user.has_perm('main.has_control_access'):
@ -439,5 +453,8 @@ def statistic_page(request: WSGIRequest) -> HttpResponse:
context['form'] = form context['form'] = form
return render(request, 'pages/statistic.html', context) return render(request, 'pages/statistic.html', context)
def registration_failed(request): def registration_failed(request: WSGIRequest) -> HttpResponse:
"""
Функция отображения страницы "Регистрация закрыта".
"""
return render(request, 'pages/registration_failed.html') return render(request, 'pages/registration_failed.html')

View File

@ -32,7 +32,7 @@ class ZendeskAdmin:
self.buffer_group_id= self.get_group(ZENDESK_GROUPS['buffer']).id self.buffer_group_id= self.get_group(ZENDESK_GROUPS['buffer']).id
self.solved_tickets_user_id = self.get_user(SOLVED_TICKETS_EMAIL).id self.solved_tickets_user_id = self.get_user(SOLVED_TICKETS_EMAIL).id
def update_user(self, user: ZenpyUser) -> bool: def update_user(self, user: ZenpyUser) -> None:
""" """
Функция сохраняет изменение пользователя в Zendesk. Функция сохраняет изменение пользователя в Zendesk.
@ -40,7 +40,7 @@ class ZendeskAdmin:
""" """
self.admin.users.update(user) self.admin.users.update(user)
def update_tickets(self, tickets: List[ZenpyTicket]): def update_tickets(self, tickets: List[ZenpyTicket]) -> None:
""" """
Функция сохраняет изменение тикетов в Zendesk. Функция сохраняет изменение тикетов в Zendesk.
@ -79,7 +79,7 @@ class ZendeskAdmin:
return group return group
return None return None
def get_user_org(self, email: str) -> str: def get_user_org(self, email: str) -> Optional[str]:
""" """
Функция возвращает организацию, к которой относится пользователь по его email. Функция возвращает организацию, к которой относится пользователь по его email.
@ -96,7 +96,6 @@ class ZendeskAdmin:
:raise: :class:`ValueError`: исключение, вызываемое если email не введен в env :raise: :class:`ValueError`: исключение, вызываемое если email не введен в env
:raise: :class:`APIException`: исключение, вызываемое если пользователя с таким email не существует в Zendesk :raise: :class:`APIException`: исключение, вызываемое если пользователя с таким email не существует в Zendesk
""" """
if self.credentials.get('email') is None: if self.credentials.get('email') is None:
raise ValueError('access_controller email not in env') raise ValueError('access_controller email not in env')