Merge branch 'feature/documentation' into 'develop'

Feature/documentation

See merge request 2020-2021/online/s101/group-02/access_controller!89
This commit is contained in:
Кравченко Артем 2021-05-27 12:59:23 +00:00
commit 385d75de3e
17 changed files with 638 additions and 206 deletions

View File

@ -149,25 +149,22 @@ docker run -d -p 8000:8000 \
Пример полной конфигурации можно найти в [.env.example](.env.example). Почту и токен админа ZenDesk взять у руководителя (если вы не админ).
## Для проверки pylint используем:
```bash
pylint --django-settings-module=access_controller.settings main
```
pylint --django-settings-module=access_controller.access_controller.settings ../access_controller (каталог, где лежит проект)
## Для приведения файлов к стандарту PEP8 используем:
```bash
autopep8 --in-place filename
```
##Для проверки орфографии:
```bash
(cd docs && make spelling)
```
cd docs
make spelling
##Для обновления документации:
```bash
m2r README.md
(cd docs && make html)
```
cd docs
make html
## Read more
- Zenpy: [http://docs.facetoe.com.au](http://docs.facetoe.com.au)

View File

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

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

View File

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

View File

@ -13,7 +13,6 @@ from access_controller.settings import ZENDESK_ROLES
class UserProfile(models.Model):
"""
Модель профиля пользователя.
Профиль создается и изменяется при создании и изменении модель User.
"""
@ -31,11 +30,7 @@ class UserProfile(models.Model):
@property
def zendesk_role(self) -> str:
"""
Функция возвращает роль пользователя в Zendesk.
В формате str, либо UNDEFINED, если пользователь не найден
:return: Роль пользователя в Zendesk
Роль пользователя в Zendesk, либо UNDEFINED, если пользователь не найден.
"""
for role, r_id in ZENDESK_ROLES.items():
if r_id == self.custom_role_id:
@ -44,12 +39,12 @@ class UserProfile(models.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) при регистрации пользователя.
:param instance: Экземпляр класса User
:param created: Создание профиля пользователя
:param created: Существует ли пользователь
:param kwargs: Параметры
:return: Обновленный список объектов профилей пользователей
"""
@ -58,7 +53,7 @@ def create_user_profile(instance, created, **kwargs) -> None:
@receiver(post_save, sender=get_user_model())
def save_user_profile(instance, **kwargs) -> None:
def save_user_profile(instance: get_user_model(), **kwargs) -> None:
"""
Функция записи БД профиля пользователя.
@ -84,7 +79,7 @@ class RoleChangeLogs(models.Model):
class UnassignedTicketStatus(models.IntegerChoices):
"""
Класс статусов не распределенных тикетов.
Модель статусов не распределенных тикетов.
:param UNASSIGNED: Снят с пользователя, перенесён в буферную группу
:param RESTORED: Авторство восстановлено
@ -95,7 +90,7 @@ class UnassignedTicketStatus(models.IntegerChoices):
"""
UNASSIGNED = 0, 'Снят с пользователя, перенесён в буферную группу'
RESTORED = 1, 'Авторство восстановлено'
NOT_FOUND = 2, 'Пока нас не было, тикет испарился из ' \
NOT_FOUND = 2, 'Пока нас не было, тикет был перенесен из ' \
'буферной группы. Дополнительные действия не требуются'
CLOSED = 3, 'Тикет уже был закрыт. Дополнительные действия не требуются'
SOLVED = 4, 'Тикет решён. Записан на пользователя с почтой SOLVED_TICKETS_EMAIL'

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
"""
Тесты.
Тестирование работы программы.
"""
@ -21,14 +21,35 @@ from main.extra_func import log
class UsersBaseTestCase(TestCase):
"""Базовый класс загружения данных для тестов с пользователями"""
"""
Базовый класс загрузки данных для тестов с пользователями.
Для тестов используются фикстуры тестовых пользователей (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.admin = 'admin@gmail.com'
self.engineer = 'customer@example.com'
self.engineer = 'customer@example.com'
self.agent_client = Client()
self.agent_client.force_login(get_user_model().objects.get(email=self.light_agent))
self.admin_client = Client()
@ -39,13 +60,27 @@ class UsersBaseTestCase(TestCase):
class RegistrationTestCase(TestCase):
"""
Класс тестирования регистрации пользователя.
Класс тестирования регистрации.
Для тестов используются фикстуры с данными пользователей engineer и light_agent (data.json).
"""
fixtures = ['fixtures/data.json']
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.any_zendesk_user_email = 'idar.sokurov.05@mail.ru'
@ -54,31 +89,43 @@ class RegistrationTestCase(TestCase):
def test_registration_complete_redirect(self) -> None:
"""
Функция тестирования успешно завершенной регистрации.
Функция тестирования успешной регистрации пользователя.
Проверяет, что в случае если email пользователя зарегистрирован на Zendesk, была заполнена форма регистрации
и направлено письмо со ссылкой для завершения регистрации, происходит редирект на страницу завершения
регистрации.
"""
with self.settings(EMAIL_BACKEND=self.email_backend):
resp = self.client.post(reverse('registration'), data={'email': self.any_zendesk_user_email})
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):
resp = self.client.post(reverse('registration'), data={'email': self.any_zendesk_user_email + 'asd'})
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'):
resp = self.client.post(reverse('registration'), data={'email': '123@test.ru'})
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):
response: HttpResponseRedirect = \
@ -94,9 +141,11 @@ class RegistrationTestCase(TestCase):
correct_body = render_to_string('registration/password_reset_email.html', email_context, response.request)
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):
self.client.post(reverse('registration'), data={'email': self.any_zendesk_user_email})
@ -104,9 +153,11 @@ class RegistrationTestCase(TestCase):
zendesk_user = zenpy.get_user(self.any_zendesk_user_email)
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):
self.client.post(reverse('registration'), data={'email': self.zendesk_admin_email})
@ -117,47 +168,67 @@ class RegistrationTestCase(TestCase):
class MakeEngineerTestCase(UsersBaseTestCase):
"""
Класс тестов для проверки функции назначения роли engineer.
Класс тестирования присвоения пользователю роли engineer.
В тестах используется @patch('main.extra_func.zenpy') Mock для работы с API Zendesk.
"""
@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)
resp = self.agent_client.post(reverse_lazy('work_become_engineer'))
self.assertRedirects(resp, reverse('work', args=[user.id]))
self.assertEqual(resp.status_code, 302)
self.assertFalse(_zenpy_mock.called)
@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.assertEqual(zenpy_mock.update_user.call_args[0][0].custom_role_id, sets.ZENDESK_ROLES['engineer'])
@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.assertEqual(zenpy_mock.update_user.call_args[0][0].custom_role_id, sets.ZENDESK_ROLES['engineer'])
@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.assertEqual(zenpy_mock.update_user.call_args[0][0].custom_role_id, sets.ZENDESK_ROLES['engineer'])
@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(
reverse_lazy('control'),
@ -170,9 +241,13 @@ class MakeEngineerTestCase(UsersBaseTestCase):
self.assertEqual(mock_object.custom_role_id, sets.ZENDESK_ROLES['engineer'])
@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(
reverse_lazy('control'),
@ -192,10 +267,23 @@ class MakeEngineerTestCase(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.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)
resp = self.engineer_client.post(reverse_lazy('work_hand_over'))
self.assertRedirects(resp, reverse('work', args=[user.id]))
@ -203,7 +291,15 @@ class MakeLightAgentTestCase(UsersBaseTestCase):
@patch('main.requester.TicketListRequester.get_tickets_list_for_user', side_effect=[[]])
@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.assertEqual(zenpy_mock.update_user.call_args[0][0].custom_role_id, sets.ZENDESK_ROLES['light_agent'])
@ -211,7 +307,18 @@ class MakeLightAgentTestCase(UsersBaseTestCase):
[Mock(id=1, status='solved'), Mock(id=2, status='open'), Mock(id=3, status='open')]
])
@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()
self.engineer_client.post(reverse_lazy('work_hand_over'))
@ -223,7 +330,15 @@ class MakeLightAgentTestCase(UsersBaseTestCase):
@patch('main.requester.TicketListRequester.get_tickets_list_for_user', side_effect=[[]])
@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.assertEqual(zenpy_mock.update_user.call_args[0][0].custom_role_id, sets.ZENDESK_ROLES['light_agent'])
@ -231,7 +346,18 @@ class MakeLightAgentTestCase(UsersBaseTestCase):
[Mock(id=1, status='solved'), Mock(id=2, status='open'), Mock(id=3, status='open')]
])
@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()
self.admin_client.post(reverse_lazy('work_hand_over'))
@ -243,19 +369,33 @@ class MakeLightAgentTestCase(UsersBaseTestCase):
@patch('main.requester.TicketListRequester.get_tickets_list_for_user', side_effect=[[]])
@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.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.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(
reverse_lazy('control'),
data={
'users': [get_user_model().objects.get(email=self.engineer).userprofile.id],
'light_agent': 'light_agent'
}
data={'users': [get_user_model().objects.get(email=self.engineer).userprofile.id],
'light_agent': 'light_agent'}
)
call_list = zenpy_mock.update_user.call_args_list
mock_object = call_list[0][0][0]
@ -264,7 +404,16 @@ class MakeLightAgentTestCase(UsersBaseTestCase):
@patch('main.requester.TicketListRequester.get_tickets_list_for_user', side_effect=[[], []])
@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(
reverse_lazy('control'),
data={
@ -284,27 +433,30 @@ class MakeLightAgentTestCase(UsersBaseTestCase):
class PasswordResetTestCase(UsersBaseTestCase):
"""
Класс тестов сброса пароля.
Класс тестирования сброса пароля.
"""
def setUp(self):
"""
Предустановленные значения для проведения тестов.
"""
super().setUp()
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):
resp = self.agent_client.post(reverse_lazy('password_reset'), data={'email': self.light_agent})
self.assertRedirects(resp, reverse('password_reset_done'))
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):
response: HttpResponseRedirect = \
@ -320,25 +472,25 @@ class PasswordResetTestCase(UsersBaseTestCase):
correct_body = render_to_string('registration/password_reset_email.html', email_context, response.request)
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'):
resp = self.agent_client.post(reverse_lazy('password_reset'), data={'email': 1})
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):
resp = self.agent_client.post(
reverse_lazy('password_reset'),
data={
'email': self.light_agent + str(random.random())
}
)
resp = self.agent_client.post(reverse_lazy('password_reset'),
data={'email': self.light_agent + str(random.random())})
self.assertRedirects(resp, reverse('password_reset_done'))
self.assertEqual(resp.status_code, 302)
self.assertEqual(len(mail.outbox), 0)
@ -348,25 +500,27 @@ class PasswordChangeTestCase(UsersBaseTestCase):
"""
Класс тестирования смены пароля.
"""
def setUp(self):
"""
Предустановленные значения для проведения тестов.
"""
def setUp(self) -> None:
super().setUp()
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.save()
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(
reverse_lazy('password_change'),
@ -379,9 +533,11 @@ class PasswordChangeTestCase(UsersBaseTestCase):
user = get_user_model().objects.get(email=self.light_agent)
self.assertTrue(user.check_password('EasyPassword'))
def test_invalid_old_password(self):
def test_invalid_old_password(self) -> None:
"""
Функция тестирования отработки неверно введенного старого пароля при смене.
Функция тестирования смены пароля, при неверном вводе старого пароля.
Проверяет текст уведомления пользователя 'Ваш старый пароль введен неправильно'.
"""
with translation.override('ru'):
resp = self.agent_client.post(
@ -394,9 +550,11 @@ class PasswordChangeTestCase(UsersBaseTestCase):
)
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'):
resp = self.agent_client.post(
@ -411,7 +569,9 @@ class PasswordChangeTestCase(UsersBaseTestCase):
def test_invalid_new_password1(self):
"""
Функция тестирования случая с неправильно подобранным новым паролем (слишком короткий).
Функция тестирования попытки смены пароля, когда новый пароль не соответствует требованиям: слишком короткий.
Проверяет текст уведомления пользователя 'Введённый пароль слишком короткий'.
"""
with translation.override('ru'):
resp = self.agent_client.post(
@ -424,9 +584,12 @@ class PasswordChangeTestCase(UsersBaseTestCase):
)
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'):
resp = self.agent_client.post(
@ -441,7 +604,10 @@ class PasswordChangeTestCase(UsersBaseTestCase):
def test_invalid_new_password3(self):
"""
Функция тестирования случая с неправильно подобранным новым паролем (совпадает с именем пользователя).
Функция тестирования попытки смены пароля, когда новый пароль не соответствует требованиям: аналогичен имени
пользователя.
Проверяет текст уведомления пользователя 'Введённый пароль слишком похож на имя пользователя'.
"""
with translation.override('ru'):
resp = self.agent_client.post(
@ -458,26 +624,40 @@ class PasswordChangeTestCase(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.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()
user = get_user_model().objects.get(email=self.engineer)
resp = self.engineer_client.post(reverse('work_get_tickets'))
self.assertRedirects(resp, reverse('work', args=[user.id]))
self.assertEqual(resp.status_code, 302)
self.assertFalse(_zenpy_mock.called)
@patch('main.views.zenpy')
@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.
Проверяет соответствие ответственного за тикет объекта tickets и тестового клиента правами инженера,
направившего запрос на назначение одного тикета.
:param zenpy_mock: Mock объекта zenpy для функций, работающих с API Zendesk.
:param group_tickets_mock: Mock списка не назначенных и нерешенных тикетов группы.
"""
group_tickets_mock.return_value = [Mock()]
zenpy_mock.get_user.return_value = Mock(role='agent', custom_role_id=sets.ZENDESK_ROLES['engineer'])
@ -487,9 +667,15 @@ class GetTicketsTestCase(UsersBaseTestCase):
@patch('main.views.get_tickets_list_for_group')
@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.
Проверяет соответствие ответственного за тикеты объекта tickets и тестового клиента правами инженера,
направившего запрос на назначение трех тикетов.
:param zenpy_mock: Mock объекта zenpy для функций, работающих с API Zendesk.
:param group_tickets_mock: Mock списка не назначенных и нерешенных тикетов группы.
"""
group_tickets_mock.return_value = [Mock()] * 3
zenpy_mock.get_user.return_value = Mock(role='agent', custom_role_id=sets.ZENDESK_ROLES['engineer'])
@ -500,9 +686,12 @@ class GetTicketsTestCase(UsersBaseTestCase):
@patch('main.views.zenpy.get_user')
@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.
: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'])
self.agent_client.post(reverse('work_get_tickets'), data={'count_tickets': 3})
@ -511,9 +700,14 @@ class GetTicketsTestCase(UsersBaseTestCase):
@patch('main.views.zenpy')
@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
zenpy_mock.get_user.return_value = Mock(role='agent', custom_role_id=sets.ZENDESK_ROLES['engineer'])
@ -523,9 +717,15 @@ class GetTicketsTestCase(UsersBaseTestCase):
@patch('main.views.get_tickets_list_for_group')
@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
zenpy_mock.get_user.return_value = Mock(role='agent', custom_role_id=sets.ZENDESK_ROLES['engineer'])
@ -537,12 +737,25 @@ class GetTicketsTestCase(UsersBaseTestCase):
class ProfileTestCase(TestCase):
"""
Класс тестов для проверки синхронизации профиля пользователя.
Для тестов используются фикстуры тестовых пользователей (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_admin_email = 'idar.sokurov.05@mail.ru'
@ -551,32 +764,42 @@ class ProfileTestCase(TestCase):
self.admin_client = Client()
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'))
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'))
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'))
self.assertEqual(resp.context['profile'].role, zenpy.get_user(self.zendesk_agent_email).role)
resp = self.admin_client.get(reverse('profile'))
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'))
user = zenpy.get_user(self.zendesk_agent_email)
@ -585,9 +808,11 @@ class ProfileTestCase(TestCase):
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)
def test_correct_image(self):
def test_correct_image(self) -> None:
"""
Функция проверки синхронизации изображения пользователя.
Проверяет соответствие аватарки пользователя из контекста страницы профиля аватарке пользователя в Zendesk.
"""
resp = self.client.get(reverse('profile'))
user = zenpy.get_user(self.zendesk_agent_email)
@ -595,38 +820,78 @@ class ProfileTestCase(TestCase):
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()
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.engineer_profile = get_user_model().objects.get(email=self.engineer).userprofile
@staticmethod
def get_file_output():
def get_file_output() -> str:
"""
Получение данных из файла логов.
"""
with open('logs/logs.csv', 'r') as file:
file_output = file.readlines()[-1]
return file_output
def test_engineer_with_admin(self):
def test_engineer_with_admin(self) -> None:
"""
Функция проверки корректной записи лога по смене роли инженера в файл.
Сравнивает запись в файле и созданный лог с переданными значениями профилей инженера и администратора
для смены прав.
"""
log(self.engineer_profile, self.admin_profile)
file_output = self.get_file_output()
self.assertEqual(file_output, f'UserForAccessTest,engineer,'
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)
file_output = self.get_file_output()
self.assertEqual(file_output, f'UserForAccessTest,engineer,'
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)
file_output = self.get_file_output()
self.assertEqual(file_output, f'UserForAccessTest,light_agent,'
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)
file_output = self.get_file_output()
self.assertEqual(file_output, f'UserForAccessTest,light_agent,'

View File

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