diff --git a/README.md b/README.md index ccaefad..c7310d0 100644 --- a/README.md +++ b/README.md @@ -186,13 +186,13 @@ autopep8 --in-place filename ##Для проверки орфографии: cd docs -(set -a && source ../.env && make spelling) +make spelling ##Для обновления документации: m2r README.md cd docs -(set -a && source ../.env && make html) +make html ## Read more diff --git a/README.rst b/README.rst index f1b08b5..219cdd7 100644 --- a/README.rst +++ b/README.rst @@ -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 --------- diff --git a/access_controller/settings.py b/access_controller/settings.py index 6d2c8d5..96567e4 100644 --- a/access_controller/settings.py +++ b/access_controller/settings.py @@ -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 @@ -151,7 +152,6 @@ LOGIN_REDIRECT_URL = '/' LOGOUT_REDIRECT_URL = '/' - # Название_приложения.Название_файла.Название_класса_обработчика AUTHENTICATION_BACKENDS = [ '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_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' diff --git a/data.json b/data.json index a4310a4..8b2e666 100644 --- a/data.json +++ b/data.json @@ -22,8 +22,9 @@ "pk": 1, "fields": { "name": "ZendeskAdmin", - "user": 1, - "role": "admin" + "user": 3, + "role": "admin", + "user_id": 1 } }, { diff --git a/docs/source/_static/statistic.png b/docs/source/_static/statistic.png new file mode 100644 index 0000000..279df9b Binary files /dev/null and b/docs/source/_static/statistic.png differ diff --git a/docs/source/_static/take_tickets.png b/docs/source/_static/take_tickets.png new file mode 100644 index 0000000..09cd2b8 Binary files /dev/null and b/docs/source/_static/take_tickets.png differ diff --git a/docs/source/code.rst b/docs/source/code.rst index 5473db2..3c1df33 100644 --- a/docs/source/code.rst +++ b/docs/source/code.rst @@ -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: diff --git a/docs/source/overview.rst b/docs/source/overview.rst index 0e3bebb..42ef622 100644 --- a/docs/source/overview.rst +++ b/docs/source/overview.rst @@ -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г. diff --git a/docs/source/spelling_wordlist.txt b/docs/source/spelling_wordlist.txt index 1e9713d..706d045 100644 --- a/docs/source/spelling_wordlist.txt +++ b/docs/source/spelling_wordlist.txt @@ -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 diff --git a/main/extra_func.py b/main/extra_func.py index 6d68944..e652a7e 100644 --- a/main/extra_func.py +++ b/main/extra_func.py @@ -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 diff --git a/main/forms.py b/main/forms.py index 929f3d9..e4db54a 100644 --- a/main/forms.py +++ b/main/forms.py @@ -1,5 +1,5 @@ """ -Формы. +Формы, использующиеся в приложении. """ from django import forms from django.contrib.auth.forms import AuthenticationForm diff --git a/main/models.py b/main/models.py index c934ab1..5f2e716 100644 --- a/main/models.py +++ b/main/models.py @@ -13,7 +13,6 @@ from access_controller.settings import ZENDESK_ROLES class UserProfile(models.Model): """ Модель профиля пользователя. - Профиль создается и изменяется при создании и изменении модель User. """ @@ -24,18 +23,14 @@ class UserProfile(models.Model): user = models.OneToOneField(to=get_user_model(), on_delete=models.CASCADE, 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='Аватарка') name = models.CharField(default='None', max_length=100, help_text='Имя пользователя на нашем сайте') @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: """ Функция записи БД профиля пользователя. @@ -75,8 +70,8 @@ class RoleChangeLogs(models.Model): """ user = models.ForeignKey(to=get_user_model(), on_delete=models.CASCADE, help_text='Пользователь, которому присвоили другую роль') - old_role = models.IntegerField(default=0, help_text='Старая роль') - new_role = models.IntegerField(default=0, help_text='Присвоенная роль') + old_role = models.BigIntegerField(default=0, help_text='Старая роль') + new_role = models.BigIntegerField(default=0, 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', help_text='Кем была изменена роль') @@ -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' diff --git a/main/requester.py b/main/requester.py index d0c57ed..f5ba9e4 100644 --- a/main/requester.py +++ b/main/requester.py @@ -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 = [] diff --git a/main/serializers.py b/main/serializers.py index 70c4352..00b85a6 100644 --- a/main/serializers.py +++ b/main/serializers.py @@ -1,5 +1,5 @@ """ -Сериализаторы. +Сериализаторы, используемые в приложении. """ from django.contrib.auth import get_user_model from rest_framework import serializers diff --git a/main/statistic_data.py b/main/statistic_data.py index f569c68..cfb506e 100644 --- a/main/statistic_data.py +++ b/main/statistic_data.py @@ -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) diff --git a/main/templates/base/base.html b/main/templates/base/base.html index 166195d..e82af52 100644 --- a/main/templates/base/base.html +++ b/main/templates/base/base.html @@ -19,6 +19,8 @@ user-select: none; } + + @media (min-width: 768px) { .bd-placeholder-img-lg { font-size: 3.5rem; diff --git a/main/templates/pages/adm_ruleset.html b/main/templates/pages/adm_ruleset.html index cff4681..1c36f24 100644 --- a/main/templates/pages/adm_ruleset.html +++ b/main/templates/pages/adm_ruleset.html @@ -24,7 +24,7 @@ {% csrf_token %}
-
Список сотрудников
+

Список сотрудников

{% block table %}
diff --git a/main/templates/pages/profile.html b/main/templates/pages/profile.html index 4b7016a..d5f95b3 100644 --- a/main/templates/pages/profile.html +++ b/main/templates/pages/profile.html @@ -23,7 +23,7 @@ {% block content %}
-
+

Имя пользователя

{{ profile.name }}
@@ -44,7 +46,7 @@ {% elif profile.custom_role_id == ZENDESK_ROLES.light_agent %}
light_agent
{% else %} -
None
+
Без роли
{% endif %}
@@ -52,7 +54,7 @@
- Запросить права доступа + Запросить права доступа
{% endblock %} diff --git a/main/templates/pages/statistic.html b/main/templates/pages/statistic.html index b467250..82b714a 100644 --- a/main/templates/pages/statistic.html +++ b/main/templates/pages/statistic.html @@ -7,21 +7,21 @@ {% block heading %} Страницы просмотра статистики{% endblock %} {% block content%} -
+
{% csrf_token %}
-
- {{ form.email.label }} +
+

{{ form.email.label }}

{{ form.email }}
-
- {{ form.interval.label }} +
+

{{ form.interval.label }}

{% for radio in form.interval%} @@ -33,8 +33,8 @@
-
- {{ form.display_format.label }} +
+

{{ form.display_format.label }}

{% for radio in form.display_format%} @@ -46,8 +46,8 @@
-
- {{ form.range_start.label}} +
+

{{ form.range_start.label}}

@@ -56,8 +56,8 @@
-
- {{ form.range_end.label}} +
+

{{ form.range_end.label}}

@@ -65,9 +65,9 @@
-
+
- +
diff --git a/main/templates/pages/work.html b/main/templates/pages/work.html index bd46341..8144b1b 100644 --- a/main/templates/pages/work.html +++ b/main/templates/pages/work.html @@ -16,66 +16,73 @@ {% endblock %} {% block content %} -
-
-

Свободных Мест: {{ licences_remaining }}

-
+

Свободных Мест: {{ licences_remaining }}

-
-
-
Список сотрудников с правами инженера
- - - - - - - {% for engineer in engineers %} - - - - - {% endfor %} - -
EmailName
{{ engineer.email }}{{ engineer.name }}
-
-
-
-
-
-
-
инженеров:
-
-
- {{ engineers|length }} -
-
-
-
легких агентов:
-
-
- {{ agents|length }} -
- -
-
-
- -
-
- {% csrf_token %} - {{ get_tickets_form.count_tickets }} - -
-
- {% for message in messages %} - - {% endfor %} -
+
+
+

Список сотрудников с правами инженера

+ + + + + + + {% for engineer in engineers %} + + + + + {% endfor %} + +
EmailName
{{ engineer.email }}{{ engineer.name }}
+
+
+
+
+
+
инженеров:
+
+
+ {{ engineers|length }} +
+
+
+
легких агентов:
+
+
+ {{ agents|length }} +
+ +
+
+
+
+ + + +
+
+ +
+
+ +
+
+
+
+ +{% for message in messages %} + +{% endfor %} {% endblock %} diff --git a/main/tests.py b/main/tests.py index 4865c8d..d214b5b 100644 --- a/main/tests.py +++ b/main/tests.py @@ -1,5 +1,5 @@ """ -Тесты. +Тестирование работы программы. """ @@ -22,14 +22,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() @@ -40,13 +61,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' @@ -55,31 +90,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 = \ @@ -95,9 +142,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}) @@ -105,9 +154,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}) @@ -118,47 +169,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'), @@ -171,9 +242,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'), @@ -193,10 +268,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])) @@ -204,7 +292,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']) @@ -212,7 +308,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')) @@ -224,7 +331,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']) @@ -232,7 +347,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')) @@ -244,19 +370,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] @@ -265,7 +405,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={ @@ -285,27 +434,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 = \ @@ -321,25 +473,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) @@ -349,25 +501,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'), @@ -380,9 +534,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( @@ -395,9 +551,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( @@ -412,7 +570,9 @@ class PasswordChangeTestCase(UsersBaseTestCase): def test_invalid_new_password1(self): """ - Функция тестирования случая с неправильно подобранным новым паролем (слишком короткий). + Функция тестирования попытки смены пароля, когда новый пароль не соответствует требованиям: слишком короткий. + + Проверяет текст уведомления пользователя 'Введённый пароль слишком короткий'. """ with translation.override('ru'): resp = self.agent_client.post( @@ -425,9 +585,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( @@ -442,7 +605,10 @@ class PasswordChangeTestCase(UsersBaseTestCase): def test_invalid_new_password3(self): """ - Функция тестирования случая с неправильно подобранным новым паролем (совпадает с именем пользователя). + Функция тестирования попытки смены пароля, когда новый пароль не соответствует требованиям: аналогичен имени + пользователя. + + Проверяет текст уведомления пользователя 'Введённый пароль слишком похож на имя пользователя'. """ with translation.override('ru'): resp = self.agent_client.post( @@ -459,26 +625,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']) @@ -488,9 +668,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']) @@ -501,9 +687,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}) @@ -512,9 +701,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']) @@ -524,9 +718,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']) @@ -538,12 +738,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' @@ -552,32 +765,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) @@ -586,9 +809,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) @@ -596,38 +821,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,' diff --git a/main/views.py b/main/views.py index 7c00d8f..d79eec5 100644 --- a/main/views.py +++ b/main/views.py @@ -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') diff --git a/main/zendesk_admin.py b/main/zendesk_admin.py index 92c1f57..631790d 100644 --- a/main/zendesk_admin.py +++ b/main/zendesk_admin.py @@ -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') diff --git a/__init__.py b/static/main/js/control.js similarity index 100% rename from __init__.py rename to static/main/js/control.js