From 5ac60de2effb491eac53a016346875c1d5ef2c63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D1=82=D0=B5=D0=BF=D0=B0=D0=BD=D0=B5=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=20=D0=9E=D0=BB=D1=8C=D0=B3=D0=B0?= Date: Tue, 13 Apr 2021 16:53:18 +0300 Subject: [PATCH 01/45] Fix some spelling problems --- docs/source/conf.py | 3 +-- docs/source/overview.rst | 15 +++++++++------ main/forms.py | 1 + main/views.py | 11 ++++++----- 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 7e99943..3b57f8a 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -12,10 +12,9 @@ # import os import sys -import importlib import inspect import enchant -from enchant import checker + sys.path.insert(0, os.path.abspath('../../')) diff --git a/docs/source/overview.rst b/docs/source/overview.rst index a7ca229..0e3bebb 100644 --- a/docs/source/overview.rst +++ b/docs/source/overview.rst @@ -69,12 +69,15 @@ Запрос прав доступа ******************** -На странице запроса прав Вам доступна информация о количестве и списке работающих над тикетами сотрудников, -а также возможность сдать и запросить права. +На странице запроса прав Вам доступна информация о количестве и списке работающих над тикетами сотрудников. + +Если Вы не являетесь инженером, то на данной странице Вы можете запросить права. + +Если Вы являетесь инженером, то права можно сдать. .. image:: _static/request.png -Успешное изменение прав: +Успешное изменение прав - список инженеров пополнился новым пользователем: .. image:: _static/role_change.png @@ -84,9 +87,9 @@ Для администратора существует удобный интерфейс страницы управления, в котором представлены: -* Количество свободных инженерных мест -* Количество и список инженеров и легких агентов -* Возможность группового назначения прав с использованием чек-боксов +* количество свободных инженерных мест +* количество и список инженеров и легких агентов +* возможность группового назначения прав с использованием чекбоксов .. image:: _static/admin_manage.png diff --git a/main/forms.py b/main/forms.py index 613fc34..6b31dbd 100644 --- a/main/forms.py +++ b/main/forms.py @@ -12,6 +12,7 @@ class CustomRegistrationForm(RegistrationFormUniqueEmail): :param visible_fields.email: Поле для ввода email, зарегистрированного на Zendesk :type visible_fields.email: :class:`django_registration.forms.RegistrationFormUniqueEmail` + """ def __init__(self, *args, **kwargs) -> RegistrationFormUniqueEmail: diff --git a/main/views.py b/main/views.py index a58331b..cacca82 100644 --- a/main/views.py +++ b/main/views.py @@ -18,6 +18,7 @@ from django.urls import reverse_lazy, reverse from django.views.generic import FormView from django_registration.views import RegistrationView from django.contrib import messages +import django.utils # Django REST from rest_framework import viewsets @@ -40,7 +41,7 @@ class CustomRegistrationView(RegistrationView): Отображение и логика работы страницы регистрации пользователя. :param form_class: Форма, которую необходимо заполнить для регистрации - :type form_class: :class:`forms.CustomRegistrationForm` + :type form_class: :class:`main.forms.CustomRegistrationForm` :param template_name: Указание пути к html-странице django регистрации :type template_name: :class:`str` :param success_url: Указание пути к html-странице завершения регистрации @@ -48,10 +49,10 @@ class CustomRegistrationView(RegistrationView): :param is_allowed: Определение зарегистрирован ли пользователь с введенным email на Zendesk и принадлежит ли он к организации SYSTEM :type is_allowed: :class:`bool` """ - form_class = CustomRegistrationForm - template_name = 'django_registration/registration_form.html' - success_url = reverse_lazy('django_registration_complete') - is_allowed = True + form_class: CustomRegistrationForm = CustomRegistrationForm + template_name: str = 'django_registration/registration_form.html' + success_url: str = reverse_lazy('django_registration_complete') + is_allowed: bool = True def register(self, form: CustomRegistrationForm) -> User: """ From df97d170bf625568ce1619796c73641c53e90668 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D1=82=D0=B5=D0=BF=D0=B0=D0=BD=D0=B5=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=20=D0=9E=D0=BB=D1=8C=D0=B3=D0=B0?= Date: Thu, 15 Apr 2021 11:49:08 +0300 Subject: [PATCH 02/45] Add autodoc_typehints = 'none' --- docs/source/conf.py | 1 + main/extra_func.py | 1 + main/forms.py | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 3b57f8a..993b68f 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -181,6 +181,7 @@ intersphinx_mapping = { } autodoc_default_flags = ['members'] +autodoc_typehints = "none" # spell checking spelling_lang = 'ru_RU' diff --git a/main/extra_func.py b/main/extra_func.py index 8b75346..e0aa110 100644 --- a/main/extra_func.py +++ b/main/extra_func.py @@ -23,6 +23,7 @@ class ZendeskAdmin: :type token: :class:`str` :param password: Пароль администратора, указанный в env :type password: :class:`str` + """ credentials: dict = { diff --git a/main/forms.py b/main/forms.py index 6b31dbd..dd45b16 100644 --- a/main/forms.py +++ b/main/forms.py @@ -96,7 +96,7 @@ class StatisticForm(forms.Form): :param range_end: Дата и время окончания работы :type range_end: :class:`django.forms.fields.DateField` """ - email = forms.EmailField( + email: str = forms.EmailField( label='Электроная почта', widget=forms.EmailInput( attrs={ From d3471331b1aee89f0863f109f2c9e7fb29c38779 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D1=82=D0=B5=D0=BF=D0=B0=D0=BD=D0=B5=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=20=D0=9E=D0=BB=D1=8C=D0=B3=D0=B0?= Date: Thu, 15 Apr 2021 14:43:54 +0300 Subject: [PATCH 03/45] Add READme docs --- README.md | 189 ++++++++++++++++-------------- docs/source/code.rst | 2 +- docs/source/conf.py | 15 ++- docs/source/index.rst | 1 + docs/source/readme.rst | 4 + docs/source/spelling_wordlist.txt | 24 +++- main/extra_func.py | 4 +- main/models.py | 1 - 8 files changed, 136 insertions(+), 104 deletions(-) create mode 100644 docs/source/readme.rst diff --git a/README.md b/README.md index 14e3869..ffd6e42 100644 --- a/README.md +++ b/README.md @@ -1,126 +1,133 @@ -# ZenDesk Access Controller +************************* +ZenDesk Access Controller +************************* -## Управление правами доступа +****************************** +Управление правами доступа +****************************** -Идея - написать программу(Web приложение), которая будет выдавать права пользователям системы по запросу самого -пользователя. Например, из 12 человек 3 сейчас работают с правами админа, по окончании рабочей смены они сдают -свои права (освобождают места) и другие пользователи могут запросить эти права в свое пользование. +**Идея** - Web приложение, выдает права пользователям системы по запросу самого пользователя. Например, из 12 человек 3 +сейчас работают с правами админа, по окончании рабочей смены они сдают свои права (освобождают места) и другие +пользователи могут права запросить. -Оставшиеся 9 человек получают права легкого агента - без прав редактирования, а только чтение. +Оставшиеся 9 человек получают права легкого агента - без прав редактирования, только чтение. -Из технологий - программа должна взаимодействовать с api системы Zendesk(система обращений клиентов - жалобы), -проверять авторизованного пользователя на права(будет возможность менять права напрямую из Zendesk - нужна -синхронизация прав с приоритетом у Zendesk). +Технологически приложение взаимодействует с **api** системы **Zendesk** (система обращений клиентов - жалобы), +проверяет авторизованного пользователя на права с возможностью менять права напрямую из Zendesk (синхронизация +с приоритетом у Zendesk). -Если руками в самом Zendesk права у пользователя отобрали или наоборот -присвоили, то наша программа обновляет статус пользователя в соответствии с данными синхронизации -(например, раз в минуту). - -Так же в идеале должна быть проверка, что пользователь сайта существует на сайте Zendesk(по токену). - -Сэндбокс Zendesk нам предоставит моя компания, библиотеку для работы с api уже подсказали. -Сама программа (наша) будет обладать админскими правами и реализовывать контроль и выдачу прав другим пользователям. +Присутствует проверка, регистрации пользователя сайта на сайте Zendesk (по токену). *Итого:* 1. Реализовать авторизацию пользователей с проверкой по API на существование такого пользователя 2. Реализовать интерфейс со статистикой рабочих мест(занято, свободно, кто занимает) -3. Реализовать логирование действий(когда взял права, когда отдал - запись в файл и БД) -4. Реализовать передачу прав приложением по запросу от пользователя и замену прав пользователя - у которого права отбираются внутри Zendesk (на легкий агент) +3. Реализовать логгирование действий(когда взял права, когда отдал - запись в файл и БД) +4. Реализовать передачу прав приложением по запросу от пользователя и замену прав пользователя у которого права отбираются внутри Zendesk (на легкий агент) 5. Реализовать синхронизацию по API на проверку прав(не менялись ли в системе Zendesk) 6. Реализовать возможность добавить большее количество админских прав 7. Реализовать возможность добавления легких агентов(права только на просмотр) 8. Реализовать на общей странице текущую информацию о пользователе - текущие права, карточка пользователя - -## Технологический стек: +************************ +Технологический стек: +************************ - Python 3 - Django 3 +************** +Quickstart +************** +``sudo apt install make`` -## Quickstart -```bash -sudo apt install make -pip install --upgrade pip -pip install -r requirements.txt -./manage.py migrate -./manage.py loaddata data.json -./manage.py runserver -``` +``pip install --upgrade pip`` -##ZenDesk Access Controller instruction for eng +``pip install -r requirements.txt`` -##Перед запуском для тестирования: +``./manage.py migrate`` -Убедитесь, что вы зарегистрированы в песочнице ZenDesk, у вас назначена организация (SYSTEM) -Для админов ZenDesk дополнительно - создайте токен доступа в ZenDesk -При запуске в Docker убедитесь что папка, которая будет служить хранилищем для БД, открыта на запись и чтение +``./manage.py loaddata data.json`` + +``./manage.py runserver`` + +********************************** +Перед запуском для тестирования: +********************************** + +* убедитесь, что вы зарегистрированы в песочнице ZenDesk, у вас назначена организация (SYSTEM) +* для админов ZenDesk дополнительно - создайте токен доступа в ZenDesk +* при запуске в Docker убедитесь что папка, которая будет служить хранилищем для БД, открыта на запись и чтение + +***************************** +Запуск на локальной машине: +***************************** + +* скопировать репозиторий на локальную машину +* перейти в папку приложения +* активировать виртуальное окружение +* выполнить команду **pip install -r requirements.txt** +* в виртуальное окружение добавить следующие переменные: -##Запуск на локальной машине: - -скопировать репозиторий на локальную машину -перейти в папку приложения -активировать вирутальное окружение -выполнить команду pip install -r requirements.txt -в вирутальное окружение добавить следующие переменные : +| *ACCESS_CONTROLLER_API_EMAIL={EMAIL}* - почта админа в ZenDesk +| *ACCESS_CONTROLLER_API_PASSWORD={PASSWORD}* - пароль админа ZenDesk +| *ACCESS_CONTROLLER_API_TOKEN={API_TOKEN}* - API токен ZenDesk +| *ZD_DOMAIN={DOMAIN}* - домен ZenDesk +| *ENG_CROLE_ID={ENGINEER_CUSTOM_ROLE_ID}* - id роли инженера( custom_role_id сотрудника смены) +| *LA_CROLE_ID={LIGHT_AGENT_CUSTOM_ROLE_ID}* - id роли легкого агента (custom_role_id роли -легкий агент) +| *EMPL_GROUP={EMPLOYEE_GROUP_NAME}* - имя группы которой принадлежат сотрудники ССКС +| *BUF_GROUP={BUFFER_GROUP_NAME}* - имя буферной группы для передачи смен(через нее происходит управление тикетами) +| *ST_EMAIL={SOLVED_TICKETS_EMAIL}* - почта на которую будут переназначатся закрытые тикеты +| *LICENSE_NO={LICENSE_NO}* - количество лицензий, отображаемых как доступные в приложении +| *SHIFTH={SHIFT_HOURS}* - количество часов в рабочей смене (нужно для статистики, пока не реализовано но требует указания значения) -ACCESS_CONTROLLER_API_EMAIL={EMAIL} - почта админа в ZenDesk -ACCESS_CONTROLLER_API_PASSWORD={PASSWORD} - пароль админа ZenDesk -ACCESS_CONTROLLER_API_TOKEN={API_TOKEN} - API токен зендеск -ZD_DOMAIN={DOMAIN} - домен ZenDesk -ENG_CROLE_ID={ENGINEER_CUSTOM_ROLE_ID} - id роли инженера( custom_role_id сотрдника смены) -LA_CROLE_ID={LIGHT_AGENT_CUSTOM_ROLE_ID} - id роли легкого агента (custom_role_id роли -легкий агент) -EMPL_GROUP={EMPLOYEE_GROUP_NAME} - имя группы которой принадлежат сотрудники ССКС -BUF_GROUP={BUFFER_GROUP_NAME} - имя буферной группы для передачи смен(через нее происходит управление тикетами) -ST_EMAIL={SOLVED_TICKETS_EMAIL} - почта на которую будут переназначятся закрытые тикеты -LICENSE_NO={LICENSE_NO} - количество лицензий, отображаемых как доступные в приложении -SHIFTH={SHIFT_HOURS} - количество часов в рабочей смене (нужно для статистики, пока не реализовано но требует указания значения) +* выполнить команду **python manage.py makemigrations** +* выполнить команду **python manage.py migrate** +* запустить приложение командой **python manage.py runserver** (можно указать в параметрах для файла manage.py) +* перейти по ссылке в консоли (вероятнее всего откроется по адресу http://127.0.0.1:8000/) +****************** +Запуск в Docker: +****************** -выполнить команду python manage.py makemigrations -выполнить команду python manage.py migrate -запустить приложение командой python manage.py runserver (можно указать в параметрах для файла manage.py) -перейти по ссылке в консоли (вероятнее всего откроется по адресу http://127.0.0.1:8000/) +Требуется установленный и настроенный Docker +* скопировать репозиторий на локальную машину +* в командной строке перейти в папку проекта +* выполнить команду **docker build**. +* выполнить команду **docker images** (нам нужен id созданного образа) +* выполнить команду **docker run -d -p 8000:8000 -e ACCESS_CONTROLLER_API_EMAIL={EMAIL} -e ACCESS_CONTROLLER_API_PASSWORD={PASSWORD}**...(перечисляем все параметры виртуального окружения, разделяя их -e) -v {абсолютный путь к папке, в которой будет размещена база}:/zendesk-access-controller/db {id образа докера} +* открыть запущенный контейнер в браузере (можно перейти по ссылке http://localhost:8000/) -##Запуск в Docker: -Требуется установленный и настроеный Docker +***************************** +Запуск с тестовыми юзерами: +***************************** -скопировать репозиторий на локальную машину -в командной строке перейти в папку проекта -выполнить команду docker build . -выполнить команду docker images (нам нужен id созданного образа) -выполнить команду docker run -d -p 8000:8000 -e ACCESS_CONTROLLER_API_EMAIL={EMAIL} -e ACCESS_CONTROLLER_API_PASSWORD={PASSWORD} -...(перечисляем все параметры виртуального окружени разделяя их -e) -v {абсолютный путь к папке, в которой будет размещена база}:/zendesk-access-controller/db {id образа докера} -открываем запущеный контейнер в браузере (можно перейти по ссылке http://localhost:8000/) +| На локальной машине - перед запуском команды **python manage.py runserver** выполнить команду **python manage.py loaddata data.json**. +| Это создаст тестового админа и тестового пользователя в приложении для песочницы ZenDesk. +| **Админ - admin@gmail.com / zendeskadmin** , +| **пользователь - 123@test.ru / zendeskuser** . +| Не сработает если домен песочницы отличается от ngenix1612197338 (на другом домене нужно будет создать сначала пользователей в песочнице с правами админа и легкого агента +| с этими же почтами, назначить им организацию (SYSTEM)) +******************************* +Параметры тестовой песочницы: +******************************* -##Запуск с тестовыми юзерами: +| *ACCESS_CONTROLLER_API_EMAIL={EMAIL}* - почта админа в ZenDesk - взять у роководителя(если вы не админ) +| *ACCESS_CONTROLLER_API_PASSWORD={PASSWORD}* - пароль админа ZenDesk - взять у роководителя(если вы не админ) +| *ACCESS_CONTROLLER_API_TOKEN={API_TOKEN}* - API токен зендеск - взять у роководителя(если вы не админ) +| *ZD_DOMAIN* =ngenix1612197338 +| *ENG_CROLE_ID* =360005209000 +| *LA_CROLE_ID* =360005208980 +| *EMPL_GROUP* =Поддержка +| *BUF_GROUP* =Сменная группа +| *ST_EMAIL* =d.krikov@ngenix.net +| *LICENSE_NO* =3 +| *SHIFTH* =12 -На локальной машине - перед запуском команды python manage.py runserver выполнить команду python manage.py loaddata data.json -Это создаст тестового админа и тестового пользователя в приложении для песочницы ZenDesk. Админ - admin@gmail.com / zendeskadmin , пользователь - 123@test.ru / zendeskuser . -Не сработает если домен песочницы отличается от ngenix1612197338 (на другом домене нужно будет создать сначала пользователей в песочнице с правами админа и легкого агента -с этими же почтами, назначить им организацию (SYSTEM)) - - -##Параметры тестовой песочницы: - -ACCESS_CONTROLLER_API_EMAIL={EMAIL} - почта админа в ZenDesk - взять у роководителя(если вы не админ) -ACCESS_CONTROLLER_API_PASSWORD={PASSWORD} - пароль админа ZenDesk - взять у роководителя(если вы не админ) -ACCESS_CONTROLLER_API_TOKEN={API_TOKEN} - API токен зендеск - взять у роководителя(если вы не админ) -ZD_DOMAIN=ngenix1612197338 -ENG_CROLE_ID=360005209000 -LA_CROLE_ID=360005208980 -EMPL_GROUP=Поддержка -BUF_GROUP=Сменная группа -ST_EMAIL=d.krikov@ngenix.net - -LICENSE_NO=3 -SHIFTH=12 - - -## Read more +************ +Read more +************ - Zenpy: [http://docs.facetoe.com.au](http://docs.facetoe.com.au) - Zendesk API: [https://developer.zendesk.com/rest_api/docs/](https://developer.zendesk.com/rest_api/docs/) diff --git a/docs/source/code.rst b/docs/source/code.rst index 7479081..1f0bd15 100644 --- a/docs/source/code.rst +++ b/docs/source/code.rst @@ -6,7 +6,7 @@ Models ******* .. automodule:: main.models - :members: + :members: ****** diff --git a/docs/source/conf.py b/docs/source/conf.py index 993b68f..3330341 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -114,12 +114,15 @@ def skip_queryset(app, what, name, obj, skip, options): return True return skip +def fix_sig(app, what, name, obj, options, signature, return_annotation): + return ("", "") -# def setup(app): -# # Register the docstring processor with sphinx -# app.connect('autodoc-process-docstring', process_django_models) -# app.connect('autodoc-skip-member', skip_queryset) -# app.connect('autodoc-process-docstring', process_modules) +def setup(app): + # Register the docstring processor with sphinx + app.connect('autodoc-process-docstring', process_django_models) + app.connect('autodoc-skip-member', skip_queryset) + app.connect('autodoc-process-docstring', process_modules) + app.connect("autodoc-process-signature", fix_sig) @@ -181,7 +184,7 @@ intersphinx_mapping = { } autodoc_default_flags = ['members'] -autodoc_typehints = "none" +autodoc_typehints = "description" # spell checking spelling_lang = 'ru_RU' diff --git a/docs/source/index.rst b/docs/source/index.rst index 96f9c69..4de50ad 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -12,6 +12,7 @@ overview code + readme todo diff --git a/docs/source/readme.rst b/docs/source/readme.rst new file mode 100644 index 0000000..0cee3b2 --- /dev/null +++ b/docs/source/readme.rst @@ -0,0 +1,4 @@ +READ.me +================== + +.. include:: ../../README.md diff --git a/docs/source/spelling_wordlist.txt b/docs/source/spelling_wordlist.txt index bd64cf9..da91912 100644 --- a/docs/source/spelling_wordlist.txt +++ b/docs/source/spelling_wordlist.txt @@ -45,7 +45,9 @@ start end date Токен +токен токеном +токену аутентифицирован (datetime.time) datetime @@ -82,7 +84,23 @@ Serializer Serializers Сериализатор переадресации - - - +чекбоксов +админских +админские +Python +Docker +докер +докера +Докер +репозиторий +zendesk-access-controller/db +-e +-v +e +v +zendesk +db +юзерами +Read +Zenpy diff --git a/main/extra_func.py b/main/extra_func.py index e0aa110..70b57d3 100644 --- a/main/extra_func.py +++ b/main/extra_func.py @@ -570,7 +570,7 @@ class StatisticData: def post_engineer_logic(self, last_log): """ - Функция обрабатывает случай, когда нам изветсно что инженер работал и после диапазона + Функция обрабатывает случай, когда нам известно что инженер работал и после диапазона """ self.fill_daterange(last_log.change_time.date() + timedelta(days=1), self.end_date + timedelta(days=1)) if last_log.change_time.date() == timezone.now().date(): @@ -585,7 +585,7 @@ class StatisticData: def prev_engineer_logic(self, first_log): """ - Функция обрабатывает случай, когда нам изветсно что инженер начал работу до диапазона + Функция обрабатывает случай, когда нам известно что инженер начал работу до диапазона """ self.fill_daterange(max(User.objects.get(email=self.email).date_joined.date(), self.start_date), first_log.change_time.date()) diff --git a/main/models.py b/main/models.py index ac6f91c..c723806 100644 --- a/main/models.py +++ b/main/models.py @@ -49,7 +49,6 @@ class RoleChangeLogs(models.Model): """ Модель для логирования изменений ролей пользователя. """ - user = models.ForeignKey(to=User, on_delete=models.CASCADE, help_text='Пользователь, которому присвоили другую роль') old_role = models.IntegerField(default=0, help_text='Старая роль') new_role = models.IntegerField(default=0, help_text='Присвоенная роль') From bf2c1cc1be8de2d6de6cb5c3c212ab26ca19aa9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D1=82=D0=B5=D0=BF=D0=B0=D0=BD=D0=B5=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=20=D0=9E=D0=BB=D1=8C=D0=B3=D0=B0?= Date: Thu, 15 Apr 2021 17:22:45 +0300 Subject: [PATCH 04/45] Add Union and Optional types --- main/extra_func.py | 49 +++++++++++++++++++++++++++------------------- main/views.py | 11 ++++++----- 2 files changed, 35 insertions(+), 25 deletions(-) diff --git a/main/extra_func.py b/main/extra_func.py index 70b57d3..ea37003 100644 --- a/main/extra_func.py +++ b/main/extra_func.py @@ -1,5 +1,6 @@ import os from datetime import timedelta, datetime, date +from typing import Optional from django.contrib.auth.models import User from django.core.exceptions import ObjectDoesNotExist @@ -47,7 +48,10 @@ class ZendeskAdmin: def get_user_name(self, email: str) -> str: """ - Функция **get_user_name** возвращает имя пользователя по его email + Функция **get_user_name** возвращает имя пользователя по его email. + + :param email: Email пользователя + :return: Имя пользователя """ user = self.admin.users.search(email).values[0] return user.name @@ -72,7 +76,7 @@ class ZendeskAdmin: user = self.admin.users.search(email).values[0] return user.id - def get_user_image(self, email: str) -> str: + def get_user_image(self, email: str) -> Optional[str]: """ Функция возвращает url-ссылку на аватар пользователя по его email. @@ -91,9 +95,9 @@ class ZendeskAdmin: """ return self.admin.users.search(email).values[0] - def get_group(self, name: str) -> str: + def get_group(self, name: str) -> Optional[str]: """ - Функция возвращает группы, к которым принадлежит пользователь. + Функция возвращает группу, к которой принадлежит пользователь. :param name: Имя пользователя :return: Группы пользователя (в случае отсутствия None) @@ -103,7 +107,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. @@ -204,7 +208,7 @@ def make_light_agent(user_profile: UserProfile, who_changes: User) -> UserProfil def get_users_list() -> list: """ - Функция **get_users_list** возвращает список пользователей Zendesk, относящихся к организации SYSTEM. + Функция **get_users_list** возвращает список пользователей Zendesk, относящихся к организации SYSTEM. """ zendesk = ZendeskAdmin() @@ -214,9 +218,12 @@ def get_users_list() -> list: return users -def get_tickets_list(email): +def get_tickets_list(email: str) -> list: """ - Функция возвращает список тикетов пользователя Zendesk + Функция возвращает список тикетов пользователя Zendesk. + + :param email: Email пользователя + :return: Список тикетов пользователя """ return ZendeskAdmin().admin.search(assignee=email, type='ticket') @@ -260,6 +267,8 @@ def check_user_auth(email: str, password: str) -> bool: """ Функция проверяет, верны ли входные данные. + :param email: Email пользователя + :param password: Пароль пользователя :raise: :class:`APIException`: исключение, вызываемое если пользователь не аутентифицирован """ creds = { @@ -293,7 +302,7 @@ def update_user_in_model(profile: UserProfile, zendesk_user: User) -> UserProfil def count_users(users) -> tuple: """ - Функция подсчета количества сотрудников с ролями engineer и light_agent + Функция подсчета количества сотрудников с ролями engineer и light_agent """ engineers, light_agents = 0, 0 for user in users: @@ -304,9 +313,9 @@ def count_users(users) -> tuple: return engineers, light_agents -def update_users_in_model(): +def update_users_in_model() -> list: """ - Обновляет пользователей в модели UserProfile по списку пользователей в организации + Функция обновляет пользователей в модели UserProfile по списку пользователей в организации """ users = get_users_list() for user in users: @@ -318,7 +327,7 @@ def update_users_in_model(): return users -def daterange(start_date, end_date) -> list: +def daterange(start_date: date, end_date: date) -> list: """ Функция возвращает список дней с start_date по end_date, исключая правую границу. @@ -332,7 +341,7 @@ def daterange(start_date, end_date) -> list: return dates -def get_timedelta(log, time=None) -> timedelta: +def get_timedelta(log: RoleChangeLogs, time: datetime =None) -> timedelta: """ Функция возвращает объект класса timedelta, который хранит промежуток времени от начала суток до момента, который находится в log (объект класса RoleChangeLogs) или в time(datetime.time), если введён. @@ -399,7 +408,7 @@ class StatisticData: else: self.statistic = stat - def get_statistic(self) -> dict: + def get_statistic(self) -> Optional[dict]: """ Функция возвращает статистику работы пользователя. @@ -447,7 +456,7 @@ class StatisticData: self.display = display_format return True - def get_data(self) -> list: + def get_data(self) -> Optional[list]: """ Функция возвращает данные - список объектов RoleChangeLogs. """ @@ -515,7 +524,7 @@ class StatisticData: return False return True - def _init_data(self): + def _init_data(self) -> Optional[list]: """ Функция возвращает логи в диапазоне дат start_date - end_date для пользователя с указанным email. @@ -532,7 +541,7 @@ class StatisticData: except User.DoesNotExist: self.errors += ['Пользователь не найден'] - def _init_statistic(self) -> dict: + def _init_statistic(self) -> Optional[dict]: """ Функция заполняет словарь, в котором ключ - дата, значение - кол-во проработанных в этот день секунд. @@ -554,7 +563,7 @@ 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) -> dict: """ Функция обрабатывает основную часть работы инженера """ @@ -568,7 +577,7 @@ 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): """ Функция обрабатывает случай, когда нам известно что инженер работал и после диапазона """ @@ -583,7 +592,7 @@ 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) ->dict: """ Функция обрабатывает случай, когда нам известно что инженер начал работу до диапазона """ diff --git a/main/views.py b/main/views.py index cacca82..29c2796 100644 --- a/main/views.py +++ b/main/views.py @@ -1,6 +1,7 @@ import logging import os from datetime import datetime +from typing import Union, Tuple from django.contrib.auth.decorators import login_required from django.contrib.auth.forms import PasswordResetForm @@ -54,7 +55,7 @@ class CustomRegistrationView(RegistrationView): success_url: str = reverse_lazy('django_registration_complete') is_allowed: bool = True - def register(self, form: CustomRegistrationForm) -> User: + def register(self, form: CustomRegistrationForm) -> Union[User, bool]: """ Функция регистрации пользователя. 1. Ввод email пользователя, указанный на Zendesk @@ -94,7 +95,7 @@ class CustomRegistrationView(RegistrationView): self.is_allowed = False @staticmethod - def set_permission(user: User) -> None: + def set_permission(user: User) -> User: """ Функция дает разрешение на просмотр страница администратора, если пользователь имеет роль admin. @@ -140,7 +141,7 @@ def profile_page(request: WSGIRequest) -> HttpResponse: return render(request, 'pages/profile.html', context) -def auth_user(request: WSGIRequest) -> ZenpyUser: +def auth_user(request: WSGIRequest) -> Tuple: """ Функция возвращает профиль пользователя на Zendesk. @@ -270,7 +271,7 @@ class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin,SuccessMessageMi self.make_light_agents(users) return super().form_valid(form) - def make_engineers(self, users): + def make_engineers(self, users: list) -> list: """ Функция проходит по списку пользователей, проставляя статус "engineer". @@ -280,7 +281,7 @@ class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin,SuccessMessageMi for user in users: make_engineer(user, self.request.user) - def make_light_agents(self, users): + def make_light_agents(self, users: list) -> list: """ Функция проходит по списку пользователей, проставляя статус "light agent". From 015d5f65a7eab00c1c8efad97f77ab9001e52470 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D1=82=D0=B5=D0=BF=D0=B0=D0=BD=D0=B5=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=20=D0=9E=D0=BB=D1=8C=D0=B3=D0=B0?= Date: Sun, 25 Apr 2021 17:51:50 +0300 Subject: [PATCH 05/45] Translate README.md to README.rst --- .env.example | 6 +-- docs/source/conf.py | 5 ++- docs/source/readme.rst | 2 +- main/extra_func.py | 85 +++++++++++++++++++++++++++--------------- main/serializers.py | 2 +- main/views.py | 29 +++++++++----- 6 files changed, 83 insertions(+), 46 deletions(-) diff --git a/.env.example b/.env.example index f86db45..58e7b51 100644 --- a/.env.example +++ b/.env.example @@ -20,6 +20,6 @@ LICENSE_NO=3 SHIFTH=12 ACTRL_ZENDESK_SUBDOMAIN="ngenix1612197338" -ACTRL_API_EMAIL="email@example.com" -ACTRL_API_TOKEN="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" -ACTRL_API_PASSWORD="" +ACTRL_API_EMAIL="stepanenko_olga@mail.ru" +ACTRL_API_TOKEN="X1x4QeNa4xRdul2rTIKhac98AsXMwd5bOGAyZOtU" + diff --git a/docs/source/conf.py b/docs/source/conf.py index cefcc10..ea14247 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -139,7 +139,8 @@ extensions = { 'sphinx.ext.graphviz', 'sphinx.ext.inheritance_diagram', 'sphinx_autodoc_typehints', - 'sphinxcontrib.spelling' + 'sphinxcontrib.spelling', + } @@ -173,6 +174,7 @@ html_static_path = ['_static'] # -- Extension configuration ------------------------------------------------- + # -- Options for intersphinx extension --------------------------------------- # Example configuration for intersphinx: refer to the Python standard library. @@ -211,3 +213,4 @@ always_document_param_types = True typehints_document_rtype = True napoleon_attr_annotations = True + diff --git a/docs/source/readme.rst b/docs/source/readme.rst index 0cee3b2..25dfdf6 100644 --- a/docs/source/readme.rst +++ b/docs/source/readme.rst @@ -1,4 +1,4 @@ READ.me ================== -.. include:: ../../README.md +.. include:: ../../README.rst diff --git a/main/extra_func.py b/main/extra_func.py index 86b6739..acaabb5 100644 --- a/main/extra_func.py +++ b/main/extra_func.py @@ -1,9 +1,11 @@ import logging from datetime import timedelta, datetime, date -from typing import Optional +from typing import Optional, Union from django.contrib.auth.models import User from django.core.exceptions import ObjectDoesNotExist +from django.core.handlers.wsgi import WSGIRequest +from django.http import HttpResponseRedirect, HttpResponsePermanentRedirect from django.shortcuts import redirect from django.utils import timezone from zenpy import Zenpy @@ -41,7 +43,7 @@ def make_engineer(user_profile: UserProfile, who_changes: User) -> None: update_role(user_profile, ROLES['engineer']) -def make_light_agent(user_profile: UserProfile, who_changes: User) -> None: +def make_light_agent(user_profile: UserProfile) -> None: """ Функция устанавливает пользователю роль легкого агента. @@ -77,7 +79,7 @@ def make_light_agent(user_profile: UserProfile, who_changes: User) -> None: def get_users_list() -> list: """ - Функция **get_users_list** возвращает список пользователей Zendesk, относящихся к организации SYSTEM. + Функция **get_users_list** возвращает список пользователей Zendesk, относящихся к организации SYSTEM. """ zendesk = zenpy @@ -87,14 +89,14 @@ def get_users_list() -> list: return users -def get_tickets_list(email): +def get_tickets_list(email) -> list: """ Функция возвращает список тикетов пользователя Zendesk """ return zenpy.admin.search(assignee=email, type='ticket') -def update_profile(user_profile: UserProfile): +def update_profile(user_profile: UserProfile) -> None: """ Функция обновляет профиль пользователя в соответствии с текущим в Zendesk. @@ -148,7 +150,7 @@ def check_user_auth(email: str, password: str) -> bool: return True -def update_user_in_model(profile: UserProfile, zendesk_user: ZenpyUser): +def update_user_in_model(profile: UserProfile, zendesk_user: ZenpyUser) -> None: """ Функция обновляет профиль пользователя при изменении данных пользователя на Zendesk. @@ -164,7 +166,7 @@ def update_user_in_model(profile: UserProfile, zendesk_user: ZenpyUser): profile.save() -def count_users(users) -> tuple: +def count_users(users: list) -> tuple: """ Функция подсчета количества сотрудников с ролями engineer и light_agent """ @@ -177,7 +179,7 @@ def count_users(users) -> tuple: return engineers, light_agents -def update_users_in_model(): +def update_users_in_model() -> list: """ Обновляет пользователей в модели UserProfile по списку пользователей в организации """ @@ -191,7 +193,7 @@ def update_users_in_model(): return users -def daterange(start_date, end_date) -> list: +def daterange(start_date: timedelta, end_date: timedelta) -> list: """ Функция возвращает список дней с start_date по end_date, исключая правую границу. @@ -205,7 +207,7 @@ def daterange(start_date, end_date) -> list: return dates -def get_timedelta(log, time=None) -> timedelta: +def get_timedelta(log: RoleChangeLogs, time: timedelta=None) -> timedelta: """ Функция возвращает объект класса timedelta, который хранит промежуток времени от начала суток до момента, который находится в log (объект класса RoleChangeLogs) или в time(datetime.time), если введён. @@ -272,7 +274,7 @@ class StatisticData: else: self.statistic = stat - def get_statistic(self) -> dict: + def get_statistic(self) -> Optional[dict]: """ Функция возвращает статистику работы пользователя. @@ -388,7 +390,7 @@ class StatisticData: return False return True - def _init_data(self): + def _init_data(self) -> None: """ Функция возвращает логи в диапазоне дат start_date - end_date для пользователя с указанным email. @@ -405,7 +407,7 @@ class StatisticData: except User.DoesNotExist: self.errors += ['Пользователь не найден'] - def _init_statistic(self) -> dict: + def _init_statistic(self) -> None: """ Функция заполняет словарь, в котором ключ - дата, значение - кол-во проработанных в этот день секунд. @@ -427,9 +429,11 @@ 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: Индекс текущего лога """ current_log, next_log = self.data[log_index], self.data[log_index + 1] if current_log.change_time.date() != next_log.change_time.date(): @@ -441,9 +445,11 @@ 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: Последний лог """ 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(): @@ -456,15 +462,17 @@ 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: """ - Функция обрабатывает случай, когда нам изветсно что инженер начал работу до диапазона + Функция обрабатывает случай, когда нам извеcтно, что инженер начал работу до диапазона. + + :param first_log: Первый лог """ self.fill_daterange(max(User.objects.get(email=self.email).date_joined.date(), self.start_date), first_log.change_time.date()) self.statistic[first_log.change_time.date()] += get_timedelta(first_log).total_seconds() - def fill_daterange(self, first: date, last: date, val: int = 24 * 3600) -> dict: + def fill_daterange(self, first: date, last: date, val: int = 24 * 3600) -> None: """ Функция заполняет диапазон дат значением val (по умолчанию val = кол-во секунд в 1 дне). @@ -475,7 +483,7 @@ class StatisticData: for day in daterange(first, last): self.statistic[day] = val - def clear_statistic(self) -> dict: + def clear_statistic(self) -> None: """ Функция осуществляет обновление всех дней. """ @@ -487,7 +495,12 @@ class DatabaseHandler(logging.Handler): def __init__(self): logging.Handler.__init__(self) - def emit(self, record): + def emit(self, record: logging.LogRecord) -> None: + """ + Функция осуществляет запись об изменении роли пользователя. + + :param record: Запись в сущность main.rolchangelogs + """ database = RoleChangeLogs() users = record.msg if users[1]: @@ -511,7 +524,12 @@ class CsvFormatter(logging.Formatter): def __init__(self): logging.Formatter.__init__(self) - def format(self, record): + def format(self, record: logging.LogRecord) -> str: + """ + Функция форматирует запись смены роли пользователя в строку + :param record: Запись смены роли пользователя. + :return: Строка с записью смены пользователя. + """ users = record.msg if users[1]: user = users[0] @@ -532,12 +550,12 @@ class CsvFormatter(logging.Formatter): return msg -def log(user, admin=0): +def log(user: User, admin: int = 0) -> None: """ - Осуществляет запись логов в базу данных и csv файл - :param admin: - :param user: - :return: + Функция осуществляет запись логов в базу данных и csv файл. + + :param admin: Админ, который меняет роль + :param user: Пользователь, которому изменена роль """ users = [user, admin] logger = logging.getLogger('MY_LOGGER') @@ -552,10 +570,15 @@ def log(user, admin=0): logger.info(users) -def set_session_params_for_work_page(request, count=None, is_confirm=True): +def set_session_params_for_work_page(request: WSGIRequest, count: int = None, is_confirm: bool = True) -> \ + Union[HttpResponsePermanentRedirect, HttpResponseRedirect]: """ - Функция для страницы получения прав - Устанавливает данные сессии о успешности запроса и количестве назначенных тикетов + Функция для страницы получения прав, устанавливает данные сессии о успешности запроса и количестве назначенных тикетов. + + :param request: Получение данных с рабочей страницы пользователя + :param count: Количество запрошенных тикетов + :param is_confirm: Назначение тикетов + :return: Перезагрузка страницы "Управление правами" соответствующего пользователя """ request.session['is_confirm'] = is_confirm request.session['count_tickets'] = count diff --git a/main/serializers.py b/main/serializers.py index 8436b54..f5100b4 100644 --- a/main/serializers.py +++ b/main/serializers.py @@ -29,7 +29,7 @@ class ZendeskUserSerializer(serializers.Serializer): email = serializers.EmailField() @staticmethod - def get_zendesk_role(obj): + def get_zendesk_role(obj) -> str: if obj.custom_role_id == ZENDESK_ROLES['engineer']: return 'engineer' elif obj.custom_role_id == ZENDESK_ROLES['light_agent']: diff --git a/main/views.py b/main/views.py index d21a6e9..bc05036 100644 --- a/main/views.py +++ b/main/views.py @@ -67,7 +67,7 @@ class CustomRegistrationView(RegistrationView): } redirect_url = 'done' - def register(self, form: CustomRegistrationForm) -> User: + def register(self, form: CustomRegistrationForm) -> None: """ Функция регистрации пользователя. 1. Ввод email пользователя, указанный на Zendesk @@ -124,7 +124,7 @@ class CustomRegistrationView(RegistrationView): ) user.user_permissions.add(permission) - def get_success_url(self, user: User = None): + def get_success_url(self, user: User = None) -> Dict: """ Функция возвращает url-адрес страницы, куда нужно перейти после успешной/не успешной регистрации. Используется самой django-registration. @@ -135,7 +135,13 @@ class CustomRegistrationView(RegistrationView): return self.urls[self.redirect_url] -def registration_error(request): +def registration_error(request: WSGIRequest) -> HttpResponse: + """ + Функция отображения страницы ошибки регистрации. + + :param request: регистрация + :return: адресация на страницу ошибки + """ return render(request, 'django_registration/registration_error.html') @@ -199,7 +205,7 @@ def work_page(request: WSGIRequest, id: int) -> HttpResponse: @login_required() -def work_hand_over(request: WSGIRequest): +def work_hand_over(request: WSGIRequest) -> HttpResponseRedirect: """ Функция позволяет текущему пользователю сдать права, а именно сменить в Zendesk роль с "engineer" на "light_agent" @@ -224,7 +230,12 @@ def work_become_engineer(request: WSGIRequest) -> HttpResponseRedirect: @login_required() -def work_get_tickets(request): +def work_get_tickets(request: WSGIRequest) -> HttpResponse: + """ + + :param request: + :return: + """ zenpy_user = zenpy.get_user(request.user.email) if zenpy_user.role == 'admin' or zenpy_user.custom_role_id == ZENDESK_ROLES['engineer']: tickets = [ticket for ticket in zenpy.admin.search(type="ticket") if @@ -280,7 +291,7 @@ class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageM self.make_light_agents(users) return super().form_valid(form) - def make_engineers(self, users): + def make_engineers(self, users: list) -> None: """ Функция проходит по списку пользователей, проставляя статус "engineer". @@ -291,7 +302,7 @@ class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageM make_engineer(user, self.request.user) log(user, self.request.user.userprofile) - def make_light_agents(self, users): + def make_light_agents(self, users: list) -> None: """ Функция проходит по списку пользователей, проставляя статус "light agent". @@ -333,7 +344,7 @@ class UsersViewSet(viewsets.ReadOnlyModelViewSet): return Response(res) @staticmethod - def choose_users(zendesk, model): + def choose_users(zendesk, model) -> list: users = [] for zendesk_user in zendesk: if zendesk_user.name not in [user.name for user in model]: @@ -341,7 +352,7 @@ class UsersViewSet(viewsets.ReadOnlyModelViewSet): return users @staticmethod - def get_zendesk_users(users): + def get_zendesk_users(users: list) -> ZendeskUserSerializer: zendesk_users = ZendeskUserSerializer( data=[user for user in users if user.role != 'admin'], many=True From aa59eb31ec8408ae1b300ab191a6eadd4b42d4b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D1=82=D0=B5=D0=BF=D0=B0=D0=BD=D0=B5=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=20=D0=9E=D0=BB=D1=8C=D0=B3=D0=B0?= Date: Sun, 25 Apr 2021 17:59:34 +0300 Subject: [PATCH 06/45] Add m2r to requirements --- docs/source/conf.py | 3 ++- requirements/common.txt | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index ea14247..b23bfb4 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -14,7 +14,7 @@ import os import sys import inspect import enchant - +import m2r sys.path.insert(0, os.path.abspath('../../')) @@ -140,6 +140,7 @@ extensions = { 'sphinx.ext.inheritance_diagram', 'sphinx_autodoc_typehints', 'sphinxcontrib.spelling', + # 'm2r' } diff --git a/requirements/common.txt b/requirements/common.txt index 6b3e7fa..095c3af 100644 --- a/requirements/common.txt +++ b/requirements/common.txt @@ -14,3 +14,4 @@ sphinx-rtd-theme==0.5.1 sphinx-autodoc-typehints==1.11.1 pyenchant==3.2.0 sphinxcontrib-spelling==7.1.0 +m2r == 0.2.1 From 8dd8e7640aeac6cda8b87f6495dd6ad42e931282 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D1=82=D0=B5=D0=BF=D0=B0=D0=BD=D0=B5=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=20=D0=9E=D0=BB=D1=8C=D0=B3=D0=B0?= Date: Tue, 27 Apr 2021 09:40:05 +0300 Subject: [PATCH 07/45] Update docs for files (except views) --- docs/source/spelling_wordlist.txt | 1 + main/forms.py | 2 ++ main/models.py | 13 ++++++------ main/serializers.py | 33 +++++++++++++++++++++++++++++-- main/zendesk_admin.py | 12 ++++++++--- 5 files changed, 50 insertions(+), 11 deletions(-) diff --git a/docs/source/spelling_wordlist.txt b/docs/source/spelling_wordlist.txt index da91912..1582561 100644 --- a/docs/source/spelling_wordlist.txt +++ b/docs/source/spelling_wordlist.txt @@ -83,6 +83,7 @@ functions Serializer Serializers Сериализатор +сериализатор переадресации чекбоксов админских diff --git a/main/forms.py b/main/forms.py index 81b2e8a..0f6b44c 100644 --- a/main/forms.py +++ b/main/forms.py @@ -56,6 +56,8 @@ class CustomAuthenticationForm(AuthenticationForm): :param username: Поле для ввода email пользователя :type username: :class:`django.forms.fields.CharField` + :param error_messages: Список ошибок авторизации + :type error_messages: :class:`dict` """ username = forms.CharField( label="Электронная почта", diff --git a/main/models.py b/main/models.py index e48fb1e..456bff9 100644 --- a/main/models.py +++ b/main/models.py @@ -11,13 +11,11 @@ class UserProfile(models.Model): """ Модель профиля пользователя. - Профиль создается и изменяется при создании и изменении модель User. + Профиль создается и изменяется при создании и изменении модель User """ - class Meta: permissions = ( - ('has_control_access', 'Can view admin page'), - ) + ('has_control_access', 'Can view admin page')) user = models.OneToOneField(to=User, on_delete=models.CASCADE, help_text='Пользователь') role = models.CharField(default='None', max_length=100, help_text='Глобальное имя роли пользователя') @@ -49,11 +47,13 @@ class RoleChangeLogs(models.Model): """ Модель для логирования изменений ролей пользователя. """ - user = models.ForeignKey(to=User, on_delete=models.CASCADE, help_text='Пользователь, которому присвоили другую роль') + user = models.ForeignKey(to=User, on_delete=models.CASCADE, help_text='Пользователь, которому присвоили другую ' + 'роль') old_role = models.IntegerField(default=0, help_text='Старая роль') new_role = models.IntegerField(default=0, help_text='Присвоенная роль') change_time = models.DateTimeField(default=timezone.now, help_text='Дата и время изменения роли') - changed_by = models.ForeignKey(to=User, on_delete=models.CASCADE, related_name='changed_by', help_text='Кем была изменена роль') + changed_by = models.ForeignKey(to=User, on_delete=models.CASCADE, related_name='changed_by', + help_text='Кем была изменена роль') class UnassignedTicketStatus(models.IntegerChoices): @@ -81,3 +81,4 @@ class UnassignedTicket(models.Model): ticket_id = models.IntegerField(help_text='Номер тикера, для которого сняли ответственного') status = models.IntegerField(choices=UnassignedTicketStatus.choices, default=UnassignedTicketStatus.UNASSIGNED, help_text='Статус тикета') + diff --git a/main/serializers.py b/main/serializers.py index f5100b4..5ed3138 100644 --- a/main/serializers.py +++ b/main/serializers.py @@ -7,6 +7,11 @@ from access_controller.settings import ZENDESK_ROLES class UserSerializer(serializers.HyperlinkedModelSerializer): """ Класс serializer для модели User. + + :param model: Модель, на основании которой создается сериализатор + :type model: :class:`django.contrib.auth.Models` + :param fields: Передаваемые поля + :type email: :class:`list` """ class Meta: model = User @@ -14,7 +19,16 @@ class UserSerializer(serializers.HyperlinkedModelSerializer): class ProfileSerializer(serializers.HyperlinkedModelSerializer): - """Класс serializer для модели профиля пользователя""" + """ + Класс serializer для модели профиля пользователя. + + :param user: Вложенный сериализатор + :type user: :class:`UserSerializer` + :param model: Модель, на основании которой создается сериализатор + :type model: :class:`django.contrib.auth.Models` + :param fields: Передаваемые поля + :type email: :class:`list` + """ user = UserSerializer() class Meta: @@ -23,13 +37,28 @@ class ProfileSerializer(serializers.HyperlinkedModelSerializer): class ZendeskUserSerializer(serializers.Serializer): - """Класс serializer для объектов пользователей из zenpy""" + """ + Класс serializer для объектов пользователей из Zenpy. + + :param name: Имя пользователя + :type name: :class:`str` + :param zendesk_role: Роль из Zendesk + :type zendesk_role: :class:`str` + :param email: Email пользователя + :type email: :class:`str` + """ name = serializers.CharField() zendesk_role = serializers.SerializerMethodField('get_zendesk_role') email = serializers.EmailField() @staticmethod def get_zendesk_role(obj) -> str: + """ + Функция строкового заполнения поля сериализатора zendesk_role. + + :param obj: объект пользователя Zendesk + :return: роль engineer либо light_agent + """ if obj.custom_role_id == ZENDESK_ROLES['engineer']: return 'engineer' elif obj.custom_role_id == ZENDESK_ROLES['light_agent']: diff --git a/main/zendesk_admin.py b/main/zendesk_admin.py index 8ecb877..9d83109 100644 --- a/main/zendesk_admin.py +++ b/main/zendesk_admin.py @@ -10,17 +10,23 @@ from access_controller.settings import ACTRL_ZENDESK_SUBDOMAIN, ACTRL_API_EMAIL, class ZendeskAdmin: """ - Класс **ZendeskAdmin** существует, чтобы в каждой функции отдельно не проверять аккаунт администратора. + Класс **ZendeskAdmin** содержит описание всего функционала администратора. :param credentials: Полномочия (первым указывается учетная запись организации в Zendesk) :type credentials: :class:`Dict[str, str]` + :param admin: Администратор + :type admin: :class:`Zenpy` + :param buffer_group_id: ID буферной группы + :type buffer_group_id: :class:`int` + :param solved_tickets_user_id: ID пользователя, который решил тикет + :type solved_tickets_user_id: :class:`int` """ def __init__(self, credentials: Dict[str, str]): self.credentials = credentials self.admin = self.create_admin() - self.buffer_group_id: int = self.get_group(ZENDESK_GROUPS['buffer']).id - self.solved_tickets_user_id: int = self.get_user(SOLVED_TICKETS_EMAIL).id + self.buffer_group_id= self.get_group(ZENDESK_GROUPS['buffer']).id + self.solved_tickets_user_id = self.get_user(SOLVED_TICKETS_EMAIL).id def check_user(self, email: str) -> bool: """ From 68833ccdd52d2e53561185ad4229a05c8b1ac79d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D1=82=D0=B5=D0=BF=D0=B0=D0=BD=D0=B5=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=20=D0=9E=D0=BB=D1=8C=D0=B3=D0=B0?= Date: Tue, 27 Apr 2021 09:56:38 +0300 Subject: [PATCH 08/45] Fix models problem --- main/models.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/main/models.py b/main/models.py index 456bff9..ac6f91c 100644 --- a/main/models.py +++ b/main/models.py @@ -11,11 +11,13 @@ class UserProfile(models.Model): """ Модель профиля пользователя. - Профиль создается и изменяется при создании и изменении модель User + Профиль создается и изменяется при создании и изменении модель User. """ + class Meta: permissions = ( - ('has_control_access', 'Can view admin page')) + ('has_control_access', 'Can view admin page'), + ) user = models.OneToOneField(to=User, on_delete=models.CASCADE, help_text='Пользователь') role = models.CharField(default='None', max_length=100, help_text='Глобальное имя роли пользователя') @@ -47,13 +49,12 @@ class RoleChangeLogs(models.Model): """ Модель для логирования изменений ролей пользователя. """ - user = models.ForeignKey(to=User, on_delete=models.CASCADE, help_text='Пользователь, которому присвоили другую ' - 'роль') + + user = models.ForeignKey(to=User, on_delete=models.CASCADE, help_text='Пользователь, которому присвоили другую роль') old_role = models.IntegerField(default=0, help_text='Старая роль') new_role = models.IntegerField(default=0, help_text='Присвоенная роль') change_time = models.DateTimeField(default=timezone.now, help_text='Дата и время изменения роли') - changed_by = models.ForeignKey(to=User, on_delete=models.CASCADE, related_name='changed_by', - help_text='Кем была изменена роль') + changed_by = models.ForeignKey(to=User, on_delete=models.CASCADE, related_name='changed_by', help_text='Кем была изменена роль') class UnassignedTicketStatus(models.IntegerChoices): @@ -80,5 +81,3 @@ class UnassignedTicket(models.Model): assignee = models.ForeignKey(to=User, on_delete=models.CASCADE, related_name='tickets', help_text='Пользователь, с которого снят тикет') ticket_id = models.IntegerField(help_text='Номер тикера, для которого сняли ответственного') status = models.IntegerField(choices=UnassignedTicketStatus.choices, default=UnassignedTicketStatus.UNASSIGNED, help_text='Статус тикета') - - From bd8ba97a465c775fd38bd9cfb0fc99838edc24e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D1=82=D0=B5=D0=BF=D0=B0=D0=BD=D0=B5=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=20=D0=9E=D0=BB=D1=8C=D0=B3=D0=B0?= Date: Tue, 27 Apr 2021 12:10:29 +0300 Subject: [PATCH 09/45] Add pylint, add init-file in access_controller folder to run pylint --- README.md | 2 ++ __init__.py | 0 docs/source/conf.py | 7 ++----- main/models.py | 7 ++++--- requirements/common.txt | 2 ++ 5 files changed, 10 insertions(+), 8 deletions(-) create mode 100644 __init__.py diff --git a/README.md b/README.md index e0b2f61..603f229 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,8 @@ docker run -d -p 8000:8000 \ ## Параметры тестовой песочницы: Пример полной конфигурации можно найти в [.env.example](.env.example). Почту и токен админа ZenDesk взять у руководителя (если вы не админ). +## Для проверки pylint используем: +pylint --load-plugins pylint_django ../access_controller ## Read more - Zenpy: [http://docs.facetoe.com.au](http://docs.facetoe.com.au) diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/docs/source/conf.py b/docs/source/conf.py index b23bfb4..2bd665b 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -134,15 +134,12 @@ extensions = { 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.viewcode', - 'sphinx.ext.napoleon', 'sphinx_rtd_theme', 'sphinx.ext.graphviz', 'sphinx.ext.inheritance_diagram', 'sphinx_autodoc_typehints', 'sphinxcontrib.spelling', # 'm2r' - - } @@ -193,7 +190,7 @@ autodoc_typehints = "description" # spell checking spelling_lang = 'ru_RU' tokenizer_lang = 'ru_RU' -spelling_exclude_patterns=['ignored_*'] +spelling_exclude_patterns=['ignored_*', '../../main/models.py'] spelling_show_suggestions = True spelling_show_whole_line=True spelling_warning=True @@ -213,5 +210,5 @@ typehints_fully_qualified = True always_document_param_types = True typehints_document_rtype = True -napoleon_attr_annotations = True + diff --git a/main/models.py b/main/models.py index ac6f91c..0d49acf 100644 --- a/main/models.py +++ b/main/models.py @@ -49,12 +49,13 @@ class RoleChangeLogs(models.Model): """ Модель для логирования изменений ролей пользователя. """ - - user = models.ForeignKey(to=User, on_delete=models.CASCADE, help_text='Пользователь, которому присвоили другую роль') + user = models.ForeignKey(to=User, on_delete=models.CASCADE, + help_text='Пользователь, которому присвоили другую роль') old_role = models.IntegerField(default=0, help_text='Старая роль') new_role = models.IntegerField(default=0, help_text='Присвоенная роль') change_time = models.DateTimeField(default=timezone.now, help_text='Дата и время изменения роли') - changed_by = models.ForeignKey(to=User, on_delete=models.CASCADE, related_name='changed_by', help_text='Кем была изменена роль') + changed_by = models.ForeignKey(to=User, on_delete=models.CASCADE, related_name='changed_by', + help_text='Кем была изменена роль') class UnassignedTicketStatus(models.IntegerChoices): diff --git a/requirements/common.txt b/requirements/common.txt index 095c3af..4417add 100644 --- a/requirements/common.txt +++ b/requirements/common.txt @@ -15,3 +15,5 @@ sphinx-autodoc-typehints==1.11.1 pyenchant==3.2.0 sphinxcontrib-spelling==7.1.0 m2r == 0.2.1 +pylint == 2.8.2 +pylint-django == 2.4.4 From 3f6cf23f4e50718d6f5beab0680be3f2c177c25e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D1=82=D0=B5=D0=BF=D0=B0=D0=BD=D0=B5=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=20=D0=9E=D0=BB=D1=8C=D0=B3=D0=B0?= Date: Tue, 27 Apr 2021 17:28:01 +0300 Subject: [PATCH 10/45] Add autopep8. First draft --- main/views.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/main/views.py b/main/views.py index bc05036..2059a17 100644 --- a/main/views.py +++ b/main/views.py @@ -25,13 +25,15 @@ from main.extra_func import check_user_exist, update_profile, get_user_organizat make_engineer, make_light_agent, get_users_list, update_users_in_model, count_users, \ StatisticData, log, set_session_params_for_work_page from main.zendesk_admin import zenpy -from main.forms import AdminPageUsers, CustomRegistrationForm, CustomAuthenticationForm, StatisticForm +from main.forms import AdminPageUsers, CustomRegistrationForm, CustomAuthenticationForm, \ + StatisticForm from main.serializers import ProfileSerializer, ZendeskUserSerializer from .models import UserProfile def setup_context(profile_lit: bool = False, control_lit: bool = False, work_lit: bool = False, - registration_lit: bool = False, login_lit: bool = False, stats_lit: bool = False) -> Dict[str, Any]: + registration_lit: bool = False, login_lit: bool = False, + stats_lit: bool = False) -> Dict[str, Any]: context = { 'profile_lit': profile_lit, @@ -54,7 +56,8 @@ class CustomRegistrationView(RegistrationView): :type template_name: :class:`str` :param success_url: Указание пути к html-странице завершения регистрации :type success_url: :class:`django.utils.functional.lazy..__proxy__` - :param is_allowed: Определение зарегистрирован ли пользователь с введенным email на Zendesk и принадлежит ли он к организации SYSTEM + :param is_allowed: Определение зарегистрирован ли пользователь с введенным email на Zendesk + и принадлежит ли он к организации SYSTEM :type is_allowed: :class:`bool` """ extra_context = setup_context(registration_lit=True) @@ -71,7 +74,8 @@ class CustomRegistrationView(RegistrationView): """ Функция регистрации пользователя. 1. Ввод email пользователя, указанный на Zendesk - 2. В случае если пользователь с данным паролем зарегистрирован на Zendesk и относится к организации SYSTEM, + 2. В случае если пользователь с данным паролем зарегистрирован на Zendesk + и относится к организации SYSTEM, происходит сброс ссылки с установлением пароля на указанный email 3. Создается пользователь class User, а также его профиль. @@ -79,7 +83,8 @@ class CustomRegistrationView(RegistrationView): :return: user """ self.redirect_url = 'done' - if check_user_exist(form.data['email']) and get_user_organization(form.data['email']) == 'SYSTEM': + if check_user_exist(form.data['email']) and \ + get_user_organization(form.data['email']) == 'SYSTEM': forms = PasswordResetForm(self.request.POST) if forms.is_valid(): opts = { @@ -386,7 +391,8 @@ def statistic_page(request: WSGIRequest) -> HttpResponse: if form.is_valid(): start_date, end_date = form.cleaned_data['range_start'], form.cleaned_data['range_end'] interval, show = form.cleaned_data['interval'], form.cleaned_data['display_format'] - data = StatisticData(start_date, end_date, form.cleaned_data['email']) + data = StatisticData(start_date, end_date, + form.cleaned_data['email']) data.set_display(show) data.set_interval(interval) stats = data.get_statistic() From 8e0610840d5191175b47979ef0e622cd799c90b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D1=82=D0=B5=D0=BF=D0=B0=D0=BD=D0=B5=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=20=D0=9E=D0=BB=D1=8C=D0=B3=D0=B0?= Date: Tue, 27 Apr 2021 19:41:47 +0300 Subject: [PATCH 11/45] Pylint 7.5 --- README.md | 5 ++- main/apps.py | 6 +++ main/extra_func.py | 83 +++++++++++++++++++++++++++-------------- main/forms.py | 3 +- main/models.py | 39 +++++++++++++------ main/views.py | 26 +++++++++---- requirements/common.txt | 1 + 7 files changed, 113 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 603f229..97d63bb 100644 --- a/README.md +++ b/README.md @@ -148,7 +148,10 @@ docker run -d -p 8000:8000 \ Пример полной конфигурации можно найти в [.env.example](.env.example). Почту и токен админа ZenDesk взять у руководителя (если вы не админ). ## Для проверки pylint используем: -pylint --load-plugins pylint_django ../access_controller +pylint --load-plugins pylint_django --disable=E5110,C0415 ../access_controller + +## Для приведения файлов к стандарту PEP8 используем: +autopep8 --in-place ## Read more - Zenpy: [http://docs.facetoe.com.au](http://docs.facetoe.com.au) diff --git a/main/apps.py b/main/apps.py index 833bff6..b521b37 100644 --- a/main/apps.py +++ b/main/apps.py @@ -1,5 +1,11 @@ +""" +Стандартный файл Django конфигурации приложения. +""" from django.apps import AppConfig class MainConfig(AppConfig): + """ + Старт приложения + """ name = 'main' diff --git a/main/extra_func.py b/main/extra_func.py index acaabb5..2e759b2 100644 --- a/main/extra_func.py +++ b/main/extra_func.py @@ -38,7 +38,8 @@ def make_engineer(user_profile: UserProfile, who_changes: User) -> None: Функция устанавливает пользователю роль инженера. :param user_profile: Профиль пользователя - :return: Вызов функции **update_role** с параметрами: профиль пользователя, роль "engineer" + :return: Вызов функции **update_role** с параметрами: + профиль пользователя, роль "engineer" """ update_role(user_profile, ROLES['engineer']) @@ -48,7 +49,8 @@ def make_light_agent(user_profile: UserProfile) -> None: Функция устанавливает пользователю роль легкого агента. :param user_profile: Профиль пользователя - :return: Вызов функции **update_role** с параметрами: профиль пользователя, роль "light_agent" + :return: Вызов функции **update_role** с параметрами: + профиль пользователя, роль "light_agent" """ tickets = get_tickets_list(user_profile.user.email) ticket: ZenpyTicket @@ -56,7 +58,8 @@ def make_light_agent(user_profile: UserProfile) -> None: UnassignedTicket.objects.create( assignee=user_profile.user, ticket_id=ticket.id, - status=UnassignedTicketStatus.SOLVED if ticket.status == 'solved' else UnassignedTicketStatus.UNASSIGNED + status=UnassignedTicketStatus.SOLVED if ticket.status == 'solved' + else UnassignedTicketStatus.UNASSIGNED ) if ticket.status == 'solved': ticket.assignee_id = zenpy.solved_tickets_user_id @@ -79,7 +82,8 @@ def make_light_agent(user_profile: UserProfile) -> None: def get_users_list() -> list: """ - Функция **get_users_list** возвращает список пользователей Zendesk, относящихся к организации SYSTEM. + Функция **get_users_list** возвращает список + пользователей Zendesk, относящихся к организации SYSTEM. """ zendesk = zenpy @@ -101,7 +105,8 @@ def update_profile(user_profile: UserProfile) -> None: Функция обновляет профиль пользователя в соответствии с текущим в Zendesk. :param user_profile: Профиль пользователя - :return: Обновленный, в соответствие с текущими данными в Zendesk, профиль пользователя + :return: Обновленный, в соответствие с текущими данными + в Zendesk, профиль пользователя """ user = zenpy.get_user(user_profile.user.email) user_profile.name = user.name @@ -135,7 +140,8 @@ def check_user_auth(email: str, password: str) -> bool: """ Функция проверяет, верны ли входные данные. - :raise: :class:`APIException`: исключение, вызываемое если пользователь не аутентифицирован + :raise: :class:`APIException`: исключение, + вызываемое если пользователь не аутентифицирован """ creds = { 'email': email, @@ -207,10 +213,12 @@ def daterange(start_date: timedelta, end_date: timedelta) -> list: return dates -def get_timedelta(log: RoleChangeLogs, time: timedelta=None) -> timedelta: +def get_timedelta(log: RoleChangeLogs, time: timedelta = None) -> timedelta: """ - Функция возвращает объект класса timedelta, который хранит промежуток времени от начала суток до момента, - который находится в log (объект класса RoleChangeLogs) или в time(datetime.time), если введён. + Функция возвращает объект класса timedelta, + который хранит промежуток времени от начала суток до момента, + который находится в log (объект класса RoleChangeLogs) + или в time(datetime.time), если введён. :param log: Лог :param time: Время @@ -278,7 +286,9 @@ class StatisticData: """ Функция возвращает статистику работы пользователя. - :return: Словарь statistic с применением формата отображения и интервала работы(если они есть). None, если были ошибки при создании. + :return: Словарь statistic с применением формата отображения + и интервала работы(если они есть). + None, если были ошибки при создании. """ if self.is_valid_statistic(): stat = self.statistic @@ -369,9 +379,12 @@ class StatisticData: if self.interval == 'months': # Переделываем ключи под формат('начало_месяца - конец_месяца') for key, value in stat.items(): - current_month_start = max(self.start_date, date(year=key.year, month=key.month, day=1)) - current_month_end = min(self.end_date, last_day_of_month(date(year=key.year, month=key.month, day=1))) - index = ' - '.join([str(current_month_start), str(current_month_end)]) + current_month_start = max(self.start_date, date( + year=key.year, month=key.month, day=1)) + current_month_end = min(self.end_date, last_day_of_month( + date(year=key.year, month=key.month, day=1))) + index = ' - '.join([str(current_month_start), + str(current_month_end)]) if new_stat.get(index): new_stat[index] += value else: @@ -392,16 +405,20 @@ class StatisticData: def _init_data(self) -> None: """ - Функция возвращает логи в диапазоне дат start_date - end_date для пользователя с указанным email. + Функция возвращает логи в диапазоне дат + start_date - end_date для пользователя с указанным email. - :return: Данные о смене статусов пользователя. Если пользователь не найден или интервал времени некорректен - ошибку. + :return: Данные о смене статусов пользователя. + Если пользователь не найден или интервал времени некорректен - ошибку. """ if not self.check_time(): - self.errors += ['Конец диапазона должен быть позже начала диапазона и раньше текущего времени'] + self.errors += [ + 'Конец диапазона должен быть позже начала диапазона и раньше текущего времени'] return try: self.data = RoleChangeLogs.objects.filter( - change_time__range=[self.start_date, self.end_date + timedelta(days=1)], + change_time__range=[self.start_date, + self.end_date + timedelta(days=1)], user=User.objects.get(email=self.email), ).order_by('change_time') except User.DoesNotExist: @@ -409,7 +426,8 @@ class StatisticData: def _init_statistic(self) -> None: """ - Функция заполняет словарь, в котором ключ - дата, значение - кол-во проработанных в этот день секунд. + Функция заполняет словарь, в котором ключ - дата, + значение - кол-во проработанных в этот день секунд. :return: Статистика работы пользователя (statistic) """ @@ -439,11 +457,14 @@ class StatisticData: if current_log.change_time.date() != next_log.change_time.date(): self.statistic[current_log.change_time.date()] += ( timedelta(days=1) - get_timedelta(current_log)).total_seconds() - self.statistic[next_log.change_time.date()] += get_timedelta(next_log).total_seconds() - self.fill_daterange(current_log.change_time.date() + timedelta(days=1), next_log.change_time.date()) + self.statistic[next_log.change_time.date( + )] += get_timedelta(next_log).total_seconds() + self.fill_daterange(current_log.change_time.date( + ) + timedelta(days=1), next_log.change_time.date()) else: elapsed_time = next_log.change_time - current_log.change_time - self.statistic[current_log.change_time.date()] += elapsed_time.total_seconds() + self.statistic[current_log.change_time.date( + )] += elapsed_time.total_seconds() def post_engineer_logic(self, last_log: RoleChangeLogs) -> None: """ @@ -451,16 +472,19 @@ class StatisticData: :param last_log: Последний лог """ - self.fill_daterange(last_log.change_time.date() + timedelta(days=1), self.end_date + timedelta(days=1)) + self.fill_daterange(last_log.change_time.date( + ) + timedelta(days=1), self.end_date + timedelta(days=1)) if last_log.change_time.date() == timezone.now().date(): self.statistic[last_log.change_time.date()] += ( - get_timedelta(None, timezone.now().time()) - get_timedelta(last_log) + get_timedelta(None, timezone.now().time()) - + get_timedelta(last_log) ).total_seconds() else: self.statistic[last_log.change_time.date()] += ( timedelta(days=1) - get_timedelta(last_log)).total_seconds() if self.end_date == timezone.now().date(): - self.statistic[self.end_date] = get_timedelta(None, timezone.now().time()).total_seconds() + self.statistic[self.end_date] = get_timedelta( + None, timezone.now().time()).total_seconds() def prev_engineer_logic(self, first_log: RoleChangeLogs) -> None: """ @@ -470,7 +494,8 @@ class StatisticData: """ self.fill_daterange(max(User.objects.get(email=self.email).date_joined.date(), self.start_date), first_log.change_time.date()) - self.statistic[first_log.change_time.date()] += get_timedelta(first_log).total_seconds() + 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) -> None: """ @@ -488,7 +513,8 @@ class StatisticData: Функция осуществляет обновление всех дней. """ self.statistic.clear() - self.fill_daterange(self.start_date, self.end_date + timedelta(days=1), 0) + self.fill_daterange( + self.start_date, self.end_date + timedelta(days=1), 0) class DatabaseHandler(logging.Handler): @@ -571,9 +597,10 @@ def log(user: User, admin: int = 0) -> None: def set_session_params_for_work_page(request: WSGIRequest, count: int = None, is_confirm: bool = True) -> \ - Union[HttpResponsePermanentRedirect, HttpResponseRedirect]: + Union[HttpResponsePermanentRedirect, HttpResponseRedirect]: """ - Функция для страницы получения прав, устанавливает данные сессии о успешности запроса и количестве назначенных тикетов. + Функция для страницы получения прав, устанавливает данные сессии + о успешности запроса и количестве назначенных тикетов. :param request: Получение данных с рабочей страницы пользователя :param count: Количество запрошенных тикетов diff --git a/main/forms.py b/main/forms.py index 0f6b44c..36d2e88 100644 --- a/main/forms.py +++ b/main/forms.py @@ -66,8 +66,7 @@ class CustomAuthenticationForm(AuthenticationForm): error_messages = { 'invalid_login': "Пожалуйста, введите правильные электронную почту и пароль. Оба поля " - "могут быть чувствительны к регистру." - , + "могут быть чувствительны к регистру.", 'inactive': "Аккаунт не активен.", } diff --git a/main/models.py b/main/models.py index 0d49acf..c955636 100644 --- a/main/models.py +++ b/main/models.py @@ -19,11 +19,15 @@ class UserProfile(models.Model): ('has_control_access', 'Can view admin page'), ) - user = models.OneToOneField(to=User, 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='Код роли пользователя') + user = models.OneToOneField( + to=User, 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='Код роли пользователя') image = models.URLField(null=True, blank=True, help_text='Аватарка') - name = models.CharField(default='None', max_length=100, help_text='Имя пользователя на нашем сайте') + name = models.CharField(default='None', max_length=100, + help_text='Имя пользователя на нашем сайте') @property def zendesk_role(self): @@ -49,12 +53,16 @@ class RoleChangeLogs(models.Model): """ Модель для логирования изменений ролей пользователя. """ - user = models.ForeignKey(to=User, on_delete=models.CASCADE, + user = models.ForeignKey(to=User, + on_delete=models.CASCADE, help_text='Пользователь, которому присвоили другую роль') old_role = models.IntegerField(default=0, help_text='Старая роль') new_role = models.IntegerField(default=0, help_text='Присвоенная роль') - change_time = models.DateTimeField(default=timezone.now, help_text='Дата и время изменения роли') - changed_by = models.ForeignKey(to=User, on_delete=models.CASCADE, related_name='changed_by', + change_time = models.DateTimeField( + default=timezone.now, help_text='Дата и время изменения роли') + changed_by = models.ForeignKey(to=User, + on_delete=models.CASCADE, + related_name='changed_by', help_text='Кем была изменена роль') @@ -64,13 +72,15 @@ class UnassignedTicketStatus(models.IntegerChoices): :param UNASSIGNED: Снят с пользователя, перенесён в буферную группу :param RESTORED: Авторство восстановлено - :param NOT_FOUND: Пока нас не было, тикет испарился из буферной группы. Дополнительные действия не требуются + :param NOT_FOUND: Пока нас не было, тикет испарился из буферной группы. + Дополнительные действия не требуются :param CLOSED: Тикет уже был закрыт. Дополнительные действия не требуются :param SOLVED: Тикет решён. Записан на пользователя с почтой SOLVED_TICKETS_EMAIL """ UNASSIGNED = 0, 'Снят с пользователя, перенесён в буферную группу' RESTORED = 1, 'Авторство восстановлено' - NOT_FOUND = 2, 'Пока нас не было, тикет испарился из буферной группы. Дополнительные действия не требуются' + NOT_FOUND = 2, 'Пока нас не было, тикет испарился из ' \ + 'буферной группы. Дополнительные действия не требуются' CLOSED = 3, 'Тикет уже был закрыт. Дополнительные действия не требуются' SOLVED = 4, 'Тикет решён. Записан на пользователя с почтой SOLVED_TICKETS_EMAIL' @@ -79,6 +89,11 @@ class UnassignedTicket(models.Model): """ Модель не распределенного тикета. """ - assignee = models.ForeignKey(to=User, on_delete=models.CASCADE, related_name='tickets', help_text='Пользователь, с которого снят тикет') - ticket_id = models.IntegerField(help_text='Номер тикера, для которого сняли ответственного') - status = models.IntegerField(choices=UnassignedTicketStatus.choices, default=UnassignedTicketStatus.UNASSIGNED, help_text='Статус тикета') + assignee = models.ForeignKey(to=User, on_delete=models.CASCADE, + related_name='tickets', + help_text='Пользователь, с которого снят тикет') + ticket_id = models.IntegerField( + help_text='Номер тикера, для которого сняли ответственного') + status = models.IntegerField(choices=UnassignedTicketStatus.choices, + default=UnassignedTicketStatus.UNASSIGNED, + help_text='Статус тикета') diff --git a/main/views.py b/main/views.py index 2059a17..68ea8d1 100644 --- a/main/views.py +++ b/main/views.py @@ -1,3 +1,7 @@ +""" +Основной функционал приложения. +""" + from smtplib import SMTPException from typing import Dict, Any @@ -117,7 +121,8 @@ class CustomRegistrationView(RegistrationView): @staticmethod def set_permission(user: User) -> None: """ - Функция дает разрешение на просмотр страница администратора, если пользователь имеет роль admin. + Функция дает разрешение на просмотр страница администратора, + если пользователь имеет роль admin. :param user: авторизованный пользователь (получает разрешение, имея роль "admin") """ @@ -131,7 +136,8 @@ class CustomRegistrationView(RegistrationView): def get_success_url(self, user: User = None) -> Dict: """ - Функция возвращает url-адрес страницы, куда нужно перейти после успешной/не успешной регистрации. + Функция возвращает url-адрес страницы, куда нужно перейти после + успешной/не успешной регистрации. Используется самой django-registration. :param user: пользователь, пытающийся зарегистрироваться @@ -172,11 +178,13 @@ def profile_page(request: WSGIRequest) -> HttpResponse: @login_required() def work_page(request: WSGIRequest, id: int) -> HttpResponse: """ - Функция отображения страницы "Управления правами" для текущего пользователя (login_required). + Функция отображения страницы "Управления правами" + для текущего пользователя (login_required). :param request: объект пользователя :param id: id пользователя, используется для динамической адресации - :return: адресация на страницу "Управления правами" (либо на страницу "Авторизации", если id и user.id не совпадают + :return: адресация на страницу "Управления правами" + (либо на страницу "Авторизации", если id и user.id не совпадают) """ users = get_users_list() if request.user.id == id: @@ -212,7 +220,8 @@ def work_page(request: WSGIRequest, id: int) -> HttpResponse: @login_required() def work_hand_over(request: WSGIRequest) -> HttpResponseRedirect: """ - Функция позволяет текущему пользователю сдать права, а именно сменить в Zendesk роль с "engineer" на "light_agent" + Функция позволяет текущему пользователю сдать права, + а именно сменить в Zendesk роль с "engineer" на "light_agent" :param request: данные текущего пользователя (login_required) :return: перезагрузка текущей страницы после выполнения смены роли @@ -224,7 +233,9 @@ def work_hand_over(request: WSGIRequest) -> HttpResponseRedirect: @login_required() def work_become_engineer(request: WSGIRequest) -> HttpResponseRedirect: """ - Функция позволяет текущему пользователю получить права, а именно сменить в Zendesk роль с "light_agent" на "engineer" + Функция позволяет текущему пользователю получить права, + а именно сменить в Zendesk роль с "light_agent" на + "engineer" :param request: данные текущего пользователя (login_required) :return: перезагрузка текущей страницы после выполнения смены роли @@ -371,7 +382,8 @@ def statistic_page(request: WSGIRequest) -> HttpResponse: """ Функция отображения страницы статистики (для "superuser"). - :param request: данные о пользователе: email, время и интервал работы. Данные получаем через forms.StatisticForm + :param request: данные о пользователе: email, время и интервал работы. + Данные получаем через forms.StatisticForm :return: адресация на страницу статистики """ diff --git a/requirements/common.txt b/requirements/common.txt index 4417add..58befa7 100644 --- a/requirements/common.txt +++ b/requirements/common.txt @@ -17,3 +17,4 @@ sphinxcontrib-spelling==7.1.0 m2r == 0.2.1 pylint == 2.8.2 pylint-django == 2.4.4 +autopep8 = 1.5.6 From d8b15d087b2bb72e4ce6d4bfcbe145b5469fe77c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D1=82=D0=B5=D0=BF=D0=B0=D0=BD=D0=B5=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=20=D0=9E=D0=BB=D1=8C=D0=B3=D0=B0?= Date: Wed, 28 Apr 2021 11:53:29 +0300 Subject: [PATCH 12/45] Add pylint settings - .pylintrc file --- README.md | 1 + access_controller/settings.py | 6 ++++++ main/models.py | 21 +++++++-------------- main/views.py | 34 ++++++++++++---------------------- 4 files changed, 26 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 97d63bb..51a76ef 100644 --- a/README.md +++ b/README.md @@ -149,6 +149,7 @@ docker run -d -p 8000:8000 \ ## Для проверки pylint используем: pylint --load-plugins pylint_django --disable=E5110,C0415 ../access_controller +pylint ../access_controller ## Для приведения файлов к стандарту PEP8 используем: autopep8 --in-place diff --git a/access_controller/settings.py b/access_controller/settings.py index a7585ed..7b1f707 100644 --- a/access_controller/settings.py +++ b/access_controller/settings.py @@ -13,6 +13,8 @@ import os from pathlib import Path # Build paths inside the project like this: BASE_DIR / 'subdir'. +import django as django + BASE_DIR = Path(__file__).resolve().parent.parent # Quick-start development settings - unsuitable for production @@ -140,6 +142,8 @@ ACCOUNT_ACTIVATION_DAYS = 7 LOGIN_REDIRECT_URL = '/' LOGOUT_REDIRECT_URL = '/' + + # Название_приложения.Название_файла.Название_класса_обработчика AUTHENTICATION_BACKENDS = [ 'access_controller.auth.EmailAuthBackend', @@ -177,3 +181,5 @@ ACTRL_ZENDESK_SUBDOMAIN = os.getenv('ACTRL_ZENDESK_SUBDOMAIN') or os.getenv('ZD_ ACTRL_API_EMAIL = os.getenv('ACTRL_API_EMAIL') or os.getenv('ACCESS_CONTROLLER_API_EMAIL') 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') + + diff --git a/main/models.py b/main/models.py index c955636..35daa68 100644 --- a/main/models.py +++ b/main/models.py @@ -19,15 +19,11 @@ class UserProfile(models.Model): ('has_control_access', 'Can view admin page'), ) - user = models.OneToOneField( - to=User, 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='Код роли пользователя') + user = models.OneToOneField(to=User, 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='Код роли пользователя') image = models.URLField(null=True, blank=True, help_text='Аватарка') - name = models.CharField(default='None', max_length=100, - help_text='Имя пользователя на нашем сайте') + name = models.CharField(default='None', max_length=100, help_text='Имя пользователя на нашем сайте') @property def zendesk_role(self): @@ -89,11 +85,8 @@ class UnassignedTicket(models.Model): """ Модель не распределенного тикета. """ - assignee = models.ForeignKey(to=User, on_delete=models.CASCADE, - related_name='tickets', + assignee = models.ForeignKey(to=User, on_delete=models.CASCADE, related_name='tickets', help_text='Пользователь, с которого снят тикет') - ticket_id = models.IntegerField( - help_text='Номер тикера, для которого сняли ответственного') - status = models.IntegerField(choices=UnassignedTicketStatus.choices, - default=UnassignedTicketStatus.UNASSIGNED, + ticket_id = models.IntegerField(help_text='Номер тикера, для которого сняли ответственного') + status = models.IntegerField(choices=UnassignedTicketStatus.choices, default=UnassignedTicketStatus.UNASSIGNED, help_text='Статус тикета') diff --git a/main/views.py b/main/views.py index 68ea8d1..dc0c65a 100644 --- a/main/views.py +++ b/main/views.py @@ -25,12 +25,10 @@ from rest_framework import viewsets from rest_framework.response import Response from access_controller.settings import DEFAULT_FROM_EMAIL, ZENDESK_ROLES, ZENDESK_MAX_AGENTS -from main.extra_func import check_user_exist, update_profile, get_user_organization, \ - make_engineer, make_light_agent, get_users_list, update_users_in_model, count_users, \ - StatisticData, log, set_session_params_for_work_page +from main.extra_func import check_user_exist, update_profile, get_user_organization, make_engineer, make_light_agent, \ + get_users_list, update_users_in_model, count_users, StatisticData, log, set_session_params_for_work_page from main.zendesk_admin import zenpy -from main.forms import AdminPageUsers, CustomRegistrationForm, CustomAuthenticationForm, \ - StatisticForm +from main.forms import AdminPageUsers, CustomRegistrationForm, CustomAuthenticationForm, StatisticForm from main.serializers import ProfileSerializer, ZendeskUserSerializer from .models import UserProfile @@ -78,8 +76,7 @@ class CustomRegistrationView(RegistrationView): """ Функция регистрации пользователя. 1. Ввод email пользователя, указанный на Zendesk - 2. В случае если пользователь с данным паролем зарегистрирован на Zendesk - и относится к организации SYSTEM, + 2. В случае если пользователь с данным паролем зарегистрирован на Zendesk и относится к организации SYSTEM, происходит сброс ссылки с установлением пароля на указанный email 3. Создается пользователь class User, а также его профиль. @@ -87,8 +84,7 @@ class CustomRegistrationView(RegistrationView): :return: user """ self.redirect_url = 'done' - if check_user_exist(form.data['email']) and \ - get_user_organization(form.data['email']) == 'SYSTEM': + if check_user_exist(form.data['email']) and get_user_organization(form.data['email']) == 'SYSTEM': forms = PasswordResetForm(self.request.POST) if forms.is_valid(): opts = { @@ -121,8 +117,7 @@ class CustomRegistrationView(RegistrationView): @staticmethod def set_permission(user: User) -> None: """ - Функция дает разрешение на просмотр страница администратора, - если пользователь имеет роль admin. + Функция дает разрешение на просмотр страница администратора, если пользователь имеет роль admin. :param user: авторизованный пользователь (получает разрешение, имея роль "admin") """ @@ -136,8 +131,7 @@ class CustomRegistrationView(RegistrationView): def get_success_url(self, user: User = None) -> Dict: """ - Функция возвращает url-адрес страницы, куда нужно перейти после - успешной/не успешной регистрации. + Функция возвращает url-адрес страницы, куда нужно перейти после успешной/не успешной регистрации. Используется самой django-registration. :param user: пользователь, пытающийся зарегистрироваться @@ -178,8 +172,7 @@ def profile_page(request: WSGIRequest) -> HttpResponse: @login_required() def work_page(request: WSGIRequest, id: int) -> HttpResponse: """ - Функция отображения страницы "Управления правами" - для текущего пользователя (login_required). + Функция отображения страницы "Управления правами" для текущего пользователя (login_required). :param request: объект пользователя :param id: id пользователя, используется для динамической адресации @@ -220,8 +213,7 @@ def work_page(request: WSGIRequest, id: int) -> HttpResponse: @login_required() def work_hand_over(request: WSGIRequest) -> HttpResponseRedirect: """ - Функция позволяет текущему пользователю сдать права, - а именно сменить в Zendesk роль с "engineer" на "light_agent" + Функция позволяет текущему пользователю сдать права, а именно сменить в Zendesk роль с "engineer" на "light_agent". :param request: данные текущего пользователя (login_required) :return: перезагрузка текущей страницы после выполнения смены роли @@ -233,9 +225,8 @@ def work_hand_over(request: WSGIRequest) -> HttpResponseRedirect: @login_required() def work_become_engineer(request: WSGIRequest) -> HttpResponseRedirect: """ - Функция позволяет текущему пользователю получить права, - а именно сменить в Zendesk роль с "light_agent" на - "engineer" + Функция позволяет текущему пользователю получить права, а именно сменить в Zendesk роль с "light_agent" на + "engineer". :param request: данные текущего пользователя (login_required) :return: перезагрузка текущей страницы после выполнения смены роли @@ -382,8 +373,7 @@ def statistic_page(request: WSGIRequest) -> HttpResponse: """ Функция отображения страницы статистики (для "superuser"). - :param request: данные о пользователе: email, время и интервал работы. - Данные получаем через forms.StatisticForm + :param request: данные о пользователе: email, время и интервал работы. Данные получаем через forms.StatisticForm :return: адресация на страницу статистики """ From 3b54a520cb6a730c60988ec8c7de654494257e66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D1=82=D0=B5=D0=BF=D0=B0=D0=BD=D0=B5=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=20=D0=9E=D0=BB=D1=8C=D0=B3=D0=B0?= Date: Wed, 28 Apr 2021 14:15:40 +0300 Subject: [PATCH 13/45] Use pylint for views --- .env.example | 25 ----------- README.md | 1 - data.json | 12 +++--- docs/source/spelling_wordlist.txt | 1 + main/views.py | 72 +++++++++++++++++++++---------- 5 files changed, 57 insertions(+), 54 deletions(-) delete mode 100644 .env.example diff --git a/.env.example b/.env.example deleted file mode 100644 index 58e7b51..0000000 --- a/.env.example +++ /dev/null @@ -1,25 +0,0 @@ -ACTRL_DEBUG=1 - -ACTRL_SECRET_KEY="v1i_fb\$_jf2#1v_lcsbu&eon4u-os0^px=s^iycegdycqy&5)6" -ACTRL_HOST="actrl.example.com" - -ACTRL_EMAIL_HOST="smtp.mail.ru" -ACTRL_EMAIL_PORT=2525 -ACTRL_EMAIL_TLS=1 -ACTRL_EMAIL_HOST_USER="djgr.02@mail.ru" -ACTRL_EMAIL_HOST_PASSWORD="djangogroup02" -ACTRL_FROM_EMAIL="djgr.02@mail.ru" -ACTRL_SERVER_EMAIL="djgr.02@mail.ru" - -ENG_CROLE_ID=360005209000 -LA_CROLE_ID=360005208980 -EMPL_GROUP="Поддержка" -BUF_GROUP="Сменная группа" -ST_EMAIL="d.krikov@ngenix.net" -LICENSE_NO=3 -SHIFTH=12 - -ACTRL_ZENDESK_SUBDOMAIN="ngenix1612197338" -ACTRL_API_EMAIL="stepanenko_olga@mail.ru" -ACTRL_API_TOKEN="X1x4QeNa4xRdul2rTIKhac98AsXMwd5bOGAyZOtU" - diff --git a/README.md b/README.md index 51a76ef..cca37dd 100644 --- a/README.md +++ b/README.md @@ -148,7 +148,6 @@ docker run -d -p 8000:8000 \ Пример полной конфигурации можно найти в [.env.example](.env.example). Почту и токен админа ZenDesk взять у руководителя (если вы не админ). ## Для проверки pylint используем: -pylint --load-plugins pylint_django --disable=E5110,C0415 ../access_controller pylint ../access_controller ## Для приведения файлов к стандарту PEP8 используем: diff --git a/data.json b/data.json index a4310a4..97678f3 100644 --- a/data.json +++ b/data.json @@ -1,7 +1,7 @@ [ { "model": "auth.user", - "pk": 1, + "pk": 3, "fields": { "password": "pbkdf2_sha256$216000$gHBBCr1jBELf$ZkEDW3IEd8Wij7u8vkv+0Eze32CS01bcaYWhcD9OIC4=", "last_login": null, @@ -19,16 +19,16 @@ }, { "model": "main.userprofile", - "pk": 1, + "pk": 3, "fields": { "name": "ZendeskAdmin", - "user": 1, + "user": 3, "role": "admin" } }, { "model": "auth.user", - "pk": 2, + "pk": 4, "fields": { "password": "pbkdf2_sha256$216000$5qLJgrm2Quq9$KDBNNymVZXkUx0HKBPFst2m83kLe0egPBnkW7KnkORU=", "last_login": null, @@ -46,10 +46,10 @@ }, { "model": "main.userprofile", - "pk": 2, + "pk": 4, "fields": { "name": "UserForAccessTest", - "user": 2, + "user": 4, "role": "agent", "custom_role_id": "360005209000" } diff --git a/docs/source/spelling_wordlist.txt b/docs/source/spelling_wordlist.txt index 1582561..4ccad53 100644 --- a/docs/source/spelling_wordlist.txt +++ b/docs/source/spelling_wordlist.txt @@ -104,4 +104,5 @@ db юзерами Read Zenpy +залогинен diff --git a/main/views.py b/main/views.py index dc0c65a..7c58221 100644 --- a/main/views.py +++ b/main/views.py @@ -1,9 +1,5 @@ -""" -Основной функционал приложения. -""" - from smtplib import SMTPException -from typing import Dict, Any +from typing import Dict, Any, Optional from django.contrib import messages from django.contrib.auth.decorators import login_required @@ -17,7 +13,7 @@ from django.contrib.messages.views import SuccessMessageMixin from django.core.handlers.wsgi import WSGIRequest from django.http import HttpResponseRedirect, HttpResponse from django.shortcuts import render, redirect -from django.urls import reverse_lazy, reverse +from django.urls import reverse_lazy from django.views.generic import FormView from django_registration.views import RegistrationView # Django REST @@ -34,8 +30,18 @@ from .models import UserProfile def setup_context(profile_lit: bool = False, control_lit: bool = False, work_lit: bool = False, - registration_lit: bool = False, login_lit: bool = False, - stats_lit: bool = False) -> Dict[str, Any]: + registration_lit: bool = False, login_lit: bool = False, stats_lit: bool = False) -> Dict[str, Any]: + """ + Функция добавления в контекст статуса пользователя. + + :param profile_lit: True, при создании профиля пользователя, иначе False + :param control_lit: False + :param work_lit: True, при установке пользователю рабочей роли, иначе False + :param registration_lit: True, при регистрации пользователя, иначе False + :param login_lit: True, если пользователь залогинен, иначе False + :param stats_lit: True, при получении пользователем прав администратора (просмотр статистики), иначе False + :return: Контекст (context) + """ context = { 'profile_lit': profile_lit, @@ -72,7 +78,7 @@ class CustomRegistrationView(RegistrationView): } redirect_url = 'done' - def register(self, form: CustomRegistrationForm) -> None: + def register(self, form: CustomRegistrationForm) -> Optional[User]: """ Функция регистрации пользователя. 1. Ввод email пользователя, указанный на Zendesk @@ -81,7 +87,7 @@ class CustomRegistrationView(RegistrationView): 3. Создается пользователь class User, а также его профиль. :param form: Email пользователя на Zendesk - :return: user + :return: User """ self.redirect_url = 'done' if check_user_exist(form.data['email']) and get_user_organization(form.data['email']) == 'SYSTEM': @@ -109,10 +115,12 @@ class CustomRegistrationView(RegistrationView): return user except SMTPException: self.redirect_url = 'email_sending_error' + return None else: raise ValueError('Непредвиденная ошибка') else: self.redirect_url = 'invalid_zendesk_email' + return None @staticmethod def set_permission(user: User) -> None: @@ -176,8 +184,7 @@ def work_page(request: WSGIRequest, id: int) -> HttpResponse: :param request: объект пользователя :param id: id пользователя, используется для динамической адресации - :return: адресация на страницу "Управления правами" - (либо на страницу "Авторизации", если id и user.id не совпадают) + :return: адресация на страницу "Управления правами" (либо на страницу "Авторизации", если id и user.id не совпадают """ users = get_users_list() if request.user.id == id: @@ -213,7 +220,7 @@ def work_page(request: WSGIRequest, id: int) -> HttpResponse: @login_required() def work_hand_over(request: WSGIRequest) -> HttpResponseRedirect: """ - Функция позволяет текущему пользователю сдать права, а именно сменить в Zendesk роль с "engineer" на "light_agent". + Функция позволяет текущему пользователю сдать права, а именно сменить в Zendesk роль с "engineer" на "light_agent" :param request: данные текущего пользователя (login_required) :return: перезагрузка текущей страницы после выполнения смены роли @@ -225,13 +232,12 @@ def work_hand_over(request: WSGIRequest) -> HttpResponseRedirect: @login_required() def work_become_engineer(request: WSGIRequest) -> HttpResponseRedirect: """ - Функция позволяет текущему пользователю получить права, а именно сменить в Zendesk роль с "light_agent" на - "engineer". + Функция позволяет текущему пользователю получить права, а именно сменить в Zendesk роль с "light_agent" + на "engineer". :param request: данные текущего пользователя (login_required) :return: перезагрузка текущей страницы после выполнения смены роли """ - make_engineer(request.user.userprofile, request.user) return set_session_params_for_work_page(request) @@ -331,12 +337,24 @@ class CustomLoginView(LoginView): class UsersViewSet(viewsets.ReadOnlyModelViewSet): """ - Класс для получения пользователей с помощью api + Класс для получения пользователей с помощью api. + + :param queryset: Список пользователей с ролью 'agent' + :type queryset: :class:`str` + :param serializer_class: Класс сериализатор для модели профиля пользователя + :type serializer_class :class:`ProfileSerializer` """ queryset = UserProfile.objects.filter(role='agent') serializer_class = ProfileSerializer - def list(self, request, *args, **kwargs): + def list(self, request: WSGIRequest, *args, **kwargs) -> Response: + """ + Функция возвращает список пользователей, список пользователей Zendesk, количество engineers и light-agents. + :param request: Запрос + :param args: Аргументы + :param kwargs: Параметры + :return: Список пользователей + """ users = update_users_in_model() count = count_users(users.values) profiles = UserProfile.objects.filter(role='agent') @@ -351,7 +369,13 @@ class UsersViewSet(viewsets.ReadOnlyModelViewSet): return Response(res) @staticmethod - def choose_users(zendesk, model) -> list: + def choose_users(zendesk: list, model: list) -> list: + """ + Функция формирует список пользователей, которые не зарегистрированы у нас. + :param zendesk: Список пользователей Zendesk + :param model: Список пользователей (модель Userprofile) + :return: Список + """ users = [] for zendesk_user in zendesk: if zendesk_user.name not in [user.name for user in model]: @@ -359,7 +383,12 @@ class UsersViewSet(viewsets.ReadOnlyModelViewSet): return users @staticmethod - def get_zendesk_users(users: list) -> ZendeskUserSerializer: + def get_zendesk_users(users: list) -> list: + """ + Получение списка пользователей Zendesk, не являющихся админами. + :param users: Список пользователей + :return: Список пользователей, не являющимися администраторами. + """ zendesk_users = ZendeskUserSerializer( data=[user for user in users if user.role != 'admin'], many=True @@ -393,8 +422,7 @@ def statistic_page(request: WSGIRequest) -> HttpResponse: if form.is_valid(): start_date, end_date = form.cleaned_data['range_start'], form.cleaned_data['range_end'] interval, show = form.cleaned_data['interval'], form.cleaned_data['display_format'] - data = StatisticData(start_date, end_date, - form.cleaned_data['email']) + data = StatisticData(start_date, end_date, form.cleaned_data['email']) data.set_display(show) data.set_interval(interval) stats = data.get_statistic() From a7dd7d84e006847a39503ee9f327915599afd56f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D1=82=D0=B5=D0=BF=D0=B0=D0=BD=D0=B5=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=20=D0=9E=D0=BB=D1=8C=D0=B3=D0=B0?= Date: Wed, 28 Apr 2021 15:39:25 +0300 Subject: [PATCH 14/45] Use pylint for extra_func.py --- .pylintrc | 624 +++++++++++++++++++++++++++++++++++++++++++++ main/extra_func.py | 17 +- 2 files changed, 636 insertions(+), 5 deletions(-) create mode 100644 .pylintrc diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..efe65af --- /dev/null +++ b/.pylintrc @@ -0,0 +1,624 @@ +[MASTER] + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-allow-list= + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. (This is an alternative name to extension-pkg-allow-list +# for backward compatibility.) +extension-pkg-whitelist= + +# Specify a score threshold to be exceeded before program exits with error. +fail-under=10.0 + +# Files or directories to be skipped. They should be base names, not paths. +ignore=CVS + +# Files or directories matching the regex patterns are skipped. The regex +# matches against base names, not paths. +ignore-patterns= + +# Python code to execute, usually for sys.path manipulation such as +#pygtk.require(). +init-hook="from pylint.config import find_pylintrc; import os, sys; sys.path.append(os.path.dirname(find_pylintrc()))" + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use. +jobs=1 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python module names) to load, +# usually to register additional checkers. +load-plugins= pylint_django + +# Pickle collected data for later comparisons. +persistent=yes + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=print-statement, + parameter-unpacking, + unpacking-in-except, + old-raise-syntax, + backtick, + long-suffix, + old-ne-operator, + old-octal-literal, + import-star-module-level, + non-ascii-bytes-literal, + raw-checker-failed, + bad-inline-option, + locally-disabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + use-symbolic-message-instead, + apply-builtin, + basestring-builtin, + buffer-builtin, + cmp-builtin, + coerce-builtin, + execfile-builtin, + file-builtin, + long-builtin, + raw_input-builtin, + reduce-builtin, + standarderror-builtin, + unicode-builtin, + xrange-builtin, + coerce-method, + delslice-method, + getslice-method, + setslice-method, + no-absolute-import, + old-division, + dict-iter-method, + dict-view-method, + next-method-called, + metaclass-assignment, + indexing-exception, + raising-string, + reload-builtin, + oct-method, + hex-method, + nonzero-method, + cmp-method, + input-builtin, + round-builtin, + intern-builtin, + unichr-builtin, + map-builtin-not-iterating, + zip-builtin-not-iterating, + range-builtin-not-iterating, + filter-builtin-not-iterating, + using-cmp-argument, + eq-without-hash, + div-method, + idiv-method, + rdiv-method, + exception-message-attribute, + invalid-str-codec, + sys-max-int, + bad-python3-import, + deprecated-string-function, + deprecated-str-translate-call, + deprecated-itertools-function, + deprecated-types-field, + next-method-defined, + dict-items-not-iterating, + dict-keys-not-iterating, + dict-values-not-iterating, + deprecated-operator-function, + deprecated-urllib-function, + xreadlines-attribute, + deprecated-sys-function, + exception-escape, + comprehension-escape, + E5110, + C045, + + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=c-extension-no-member + + +[REPORTS] + +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'error', 'warning', 'refactor', and 'convention' +# which contain the number of messages in each category, as well as 'statement' +# which is the total number of statements analyzed. This score is used by the +# global evaluation report (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +#msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit,argparse.parse_error + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +bad-names-rgxs= + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. +#class-attribute-rgx= + +# Naming style matching correct class constant names. +class-const-naming-style=UPPER_CASE + +# Regular expression matching correct class constant names. Overrides class- +# const-naming-style. +#class-const-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + ex, + Run, + _ + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs= + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. +#variable-rgx= + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=120 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis). It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + +# List of decorators that change the signature of a decorated function. +signature-mutators= + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=no + +# This flag controls whether the implicit-str-concat should generate a warning +# on implicit string concatenation in sequences defined over several lines. +check-str-concat-over-line-jumps=no + + +[SIMILARITIES] + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + +# Regular expression of note tags to take in consideration. +#notes-rgx= + + +[LOGGING] + +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of names allowed to shadow builtins +allowed-redefined-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. Available dictionaries: en (aspell), en_AU +# (aspell), en_CA (aspell), en_GB (aspell), en_US (aspell), fr_CA (myspell), +# fr_MC (myspell), fr_CH (myspell), fr_LU (myspell), fr_FR (myspell), fr_BE +# (myspell), de_DE (myspell), es_VE (myspell), es_MX (myspell), es_CL +# (myspell), es_CR (myspell), es_US (myspell), it_CH (myspell), pt_BR +# (myspell), es_DO (myspell), en_ZA (myspell), es_PY (myspell), es_GT +# (myspell), es_CU (myspell), es_SV (myspell), es_PE (myspell), es_CO +# (myspell), de_CH (myspell), ru_RU (myspell), es_NI (myspell), es_ES +# (myspell), es_HN (myspell), it_IT (myspell), pt_PT (myspell), de_DE_frami +# (myspell), es_AR (myspell), de_CH_frami (myspell), es_PR (myspell), es_UY +# (myspell), de_AT_frami (myspell), de_AT (myspell), es_PA (myspell), fr +# (myspell), es_EC (myspell), es_BO (myspell). +spelling-dict= + +# List of comma separated words that should be considered directives if they +# appear and the beginning of a comment and should not be checked. +spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains the private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. +spelling-store-unknown-words=no + + +[DESIGN] + +# Maximum number of arguments for function / method. +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in an if statement (see R0916). +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=12 + +# Maximum number of locals for function / method body. +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[IMPORTS] + +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules=optparse,tkinter.tix + +# Output a graph (.gv or any supported image format) of external dependencies +# to the given file (report RP0402 must not be disabled). +ext-import-graph= + +# Output a graph (.gv or any supported image format) of all (i.e. internal and +# external) dependencies to the given file (report RP0402 must not be +# disabled). +import-graph= + +# Output a graph (.gv or any supported image format) of internal dependencies +# to the given file (report RP0402 must not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= + + +[CLASSES] + +# Warn about protected attribute access inside special methods +check-protected-access-in-special-methods=no + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp, + __post_init__ + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=cls + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "BaseException, Exception". +overgeneral-exceptions=BaseException, + Exception diff --git a/main/extra_func.py b/main/extra_func.py index 2e759b2..8d2ef91 100644 --- a/main/extra_func.py +++ b/main/extra_func.py @@ -1,3 +1,6 @@ +""" +Вспомогательные функции со списками пользователей, статистикой и т.д. +""" import logging from datetime import timedelta, datetime, date from typing import Optional, Union @@ -44,7 +47,7 @@ def make_engineer(user_profile: UserProfile, who_changes: User) -> None: update_role(user_profile, ROLES['engineer']) -def make_light_agent(user_profile: UserProfile) -> None: +def make_light_agent(user_profile: UserProfile, who_changes: User) -> None: """ Функция устанавливает пользователю роль легкого агента. @@ -66,7 +69,6 @@ def make_light_agent(user_profile: UserProfile) -> None: else: ticket.assignee = None ticket.group_id = zenpy.buffer_group_id - zenpy.admin.tickets.update(tickets.values) attempts, success = 5, False @@ -405,8 +407,7 @@ class StatisticData: def _init_data(self) -> None: """ - Функция возвращает логи в диапазоне дат - start_date - end_date для пользователя с указанным email. + Функция возвращает логи в диапазоне дат start_date - end_date для пользователя с указанным email. :return: Данные о смене статусов пользователя. Если пользователь не найден или интервал времени некорректен - ошибку. @@ -434,7 +435,7 @@ class StatisticData: self.clear_statistic() if not self.get_data(): self.warnings += ['Не обнаружены изменения роли в данном промежутке'] - return None + return first_log, last_log = self.data[0], self.data[len(self.data) - 1] if first_log.old_role == ROLES['engineer']: @@ -518,6 +519,9 @@ class StatisticData: class DatabaseHandler(logging.Handler): + """ + Класс записи изменений ролей в базу данных. + """ def __init__(self): logging.Handler.__init__(self) @@ -547,6 +551,9 @@ class DatabaseHandler(logging.Handler): class CsvFormatter(logging.Formatter): + """ + Класс преобразования смены ролей пользователей в строковый формат. + """ def __init__(self): logging.Formatter.__init__(self) From e5460d90fc3c572cbd593ce35b5a7098c70a9135 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D1=82=D0=B5=D0=BF=D0=B0=D0=BD=D0=B5=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=20=D0=9E=D0=BB=D1=8C=D0=B3=D0=B0?= Date: Wed, 28 Apr 2021 16:18:55 +0300 Subject: [PATCH 15/45] Use pylint for models.py --- main/extra_func.py | 26 +++++++++++--------------- main/models.py | 41 +++++++++++++++++++++++++++++++---------- main/views.py | 5 +++++ 3 files changed, 47 insertions(+), 25 deletions(-) diff --git a/main/extra_func.py b/main/extra_func.py index 8d2ef91..f215544 100644 --- a/main/extra_func.py +++ b/main/extra_func.py @@ -107,8 +107,7 @@ def update_profile(user_profile: UserProfile) -> None: Функция обновляет профиль пользователя в соответствии с текущим в Zendesk. :param user_profile: Профиль пользователя - :return: Обновленный, в соответствие с текущими данными - в Zendesk, профиль пользователя + :return: Обновленный, в соответствие с текущими данными в Zendesk, профиль пользователя """ user = zenpy.get_user(user_profile.user.email) user_profile.name = user.name @@ -142,8 +141,7 @@ def check_user_auth(email: str, password: str) -> bool: """ Функция проверяет, верны ли входные данные. - :raise: :class:`APIException`: исключение, - вызываемое если пользователь не аутентифицирован + :raise: :class:`APIException`: исключение, вызываемое если пользователь не аутентифицирован """ creds = { 'email': email, @@ -176,7 +174,7 @@ def update_user_in_model(profile: UserProfile, zendesk_user: ZenpyUser) -> None: def count_users(users: list) -> tuple: """ - Функция подсчета количества сотрудников с ролями engineer и light_agent + Функция подсчета количества сотрудников с ролями engineer и light_agent. """ engineers, light_agents = 0, 0 for user in users: @@ -189,7 +187,7 @@ def count_users(users: list) -> tuple: def update_users_in_model() -> list: """ - Обновляет пользователей в модели UserProfile по списку пользователей в организации + Обновляет пользователей в модели UserProfile по списку пользователей в организации. """ users = get_users_list() for user in users: @@ -217,10 +215,8 @@ def daterange(start_date: timedelta, end_date: timedelta) -> list: def get_timedelta(log: RoleChangeLogs, time: timedelta = None) -> timedelta: """ - Функция возвращает объект класса timedelta, - который хранит промежуток времени от начала суток до момента, - который находится в log (объект класса RoleChangeLogs) - или в time(datetime.time), если введён. + Функция возвращает объект класса timedelta, который хранит промежуток времени от начала суток до момента, + который находится в log (объект класса RoleChangeLogs) или в time(datetime.time), если введён. :param log: Лог :param time: Время @@ -427,8 +423,7 @@ class StatisticData: def _init_statistic(self) -> None: """ - Функция заполняет словарь, в котором ключ - дата, - значение - кол-во проработанных в этот день секунд. + Функция заполняет словарь, в котором ключ - дата, значение - кол-во проработанных в этот день секунд. :return: Статистика работы пользователя (statistic) """ @@ -559,7 +554,8 @@ class CsvFormatter(logging.Formatter): def format(self, record: logging.LogRecord) -> str: """ - Функция форматирует запись смены роли пользователя в строку + Функция форматирует запись смены роли пользователя в строку. + :param record: Запись смены роли пользователя. :return: Строка с записью смены пользователя. """ @@ -606,8 +602,8 @@ def log(user: User, admin: int = 0) -> None: def set_session_params_for_work_page(request: WSGIRequest, count: int = None, is_confirm: bool = True) -> \ Union[HttpResponsePermanentRedirect, HttpResponseRedirect]: """ - Функция для страницы получения прав, устанавливает данные сессии - о успешности запроса и количестве назначенных тикетов. + Функция для страницы получения прав, устанавливает данные сессии о успешности запроса и количестве + назначенных тикетов. :param request: Получение данных с рабочей страницы пользователя :param count: Количество запрошенных тикетов diff --git a/main/models.py b/main/models.py index 35daa68..fcde327 100644 --- a/main/models.py +++ b/main/models.py @@ -1,3 +1,8 @@ +""" +Модели, использующиеся в приложении. +""" + + from django.db import models from django.contrib.auth.models import User from django.db.models.signals import post_save @@ -26,7 +31,12 @@ class UserProfile(models.Model): name = models.CharField(default='None', max_length=100, help_text='Имя пользователя на нашем сайте') @property - def zendesk_role(self): + def zendesk_role(self) -> str: + """ + Функция возвращает роль пользователя в Zendesk в формате str, либо UNDERFINED, если пользователь не найден. + + :return: Роль пользователя в Zendesk + """ id = self.custom_role_id for role, r_id in ZENDESK_ROLES.items(): if r_id == id: @@ -35,13 +45,28 @@ class UserProfile(models.Model): @receiver(post_save, sender=User) -def create_user_profile(sender, instance, created, **kwargs): +def create_user_profile(instance, created, **kwargs) -> None: + """ + Функция создания профиля пользователя (Userprofile) при регистрации пользователя. + + :param instance: Экземпляр класса User + :param created: Создание профиля пользователя + :param kwargs: Параметры + :return: Обновленный список объектов профилей пользователей + """ if created: UserProfile.objects.create(user=instance) @receiver(post_save, sender=User) -def save_user_profile(sender, instance, **kwargs): +def save_user_profile(instance, **kwargs) -> None: + """ + Функция записи БД профиля пользователя. + + :param instance: Экземпляр класса User + :param kwargs: Параметры + :return: Запись профиля пользователя + """ instance.userprofile.save() @@ -49,16 +74,12 @@ class RoleChangeLogs(models.Model): """ Модель для логирования изменений ролей пользователя. """ - user = models.ForeignKey(to=User, - on_delete=models.CASCADE, + user = models.ForeignKey(to=User, on_delete=models.CASCADE, help_text='Пользователь, которому присвоили другую роль') old_role = models.IntegerField(default=0, help_text='Старая роль') new_role = models.IntegerField(default=0, help_text='Присвоенная роль') - change_time = models.DateTimeField( - default=timezone.now, help_text='Дата и время изменения роли') - changed_by = models.ForeignKey(to=User, - on_delete=models.CASCADE, - related_name='changed_by', + change_time = models.DateTimeField(default=timezone.now, help_text='Дата и время изменения роли') + changed_by = models.ForeignKey(to=User, on_delete=models.CASCADE, related_name='changed_by', help_text='Кем была изменена роль') diff --git a/main/views.py b/main/views.py index 7c58221..15f393e 100644 --- a/main/views.py +++ b/main/views.py @@ -1,3 +1,8 @@ +""" +View функции. +""" + + from smtplib import SMTPException from typing import Dict, Any, Optional From 4666570177d748dd0971c93418feb30acf09d5fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D1=82=D0=B5=D0=BF=D0=B0=D0=BD=D0=B5=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=20=D0=9E=D0=BB=D1=8C=D0=B3=D0=B0?= Date: Wed, 28 Apr 2021 16:42:19 +0300 Subject: [PATCH 16/45] Use pylint for zendesk_admin.py --- .pylintrc | 5 ++++- main/zendesk_admin.py | 11 +++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/.pylintrc b/.pylintrc index efe65af..49866c6 100644 --- a/.pylintrc +++ b/.pylintrc @@ -145,7 +145,7 @@ disable=print-statement, exception-escape, comprehension-escape, E5110, - C045, + C0415, # Enable the message, report, category or checker with the given id(s). You can @@ -261,6 +261,9 @@ function-naming-style=snake_case # Good variable names which should always be accepted, separated by a comma. good-names=i, + id, + e, + n, j, k, ex, diff --git a/main/zendesk_admin.py b/main/zendesk_admin.py index 9d83109..6e2a5b3 100644 --- a/main/zendesk_admin.py +++ b/main/zendesk_admin.py @@ -1,3 +1,7 @@ +""" +Функционал работы администратора Zendesk. +""" + from typing import Optional, Dict from zenpy import Zenpy @@ -35,7 +39,7 @@ class ZendeskAdmin: :param email: Email пользователя :return: Является ли зарегистрированным """ - return True if self.admin.search(email, type='user') else False + return bool(self.admin.search(email, type='user')) def get_user(self, email: str) -> ZenpyUser: """ @@ -85,9 +89,8 @@ class ZendeskAdmin: admin = Zenpy(**self.credentials) try: admin.search(self.credentials['email'], type='user') - except APIException: - raise ValueError('invalid access_controller`s login data') - + except APIException as invalid_data: + raise ValueError('invalid access_controller`s login data') from invalid_data return admin From 2e3e567d2d7d2cb441373f26d9cae7e0afe5fb8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D1=82=D0=B5=D0=BF=D0=B0=D0=BD=D0=B5=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=20=D0=9E=D0=BB=D1=8C=D0=B3=D0=B0?= Date: Wed, 28 Apr 2021 18:57:36 +0300 Subject: [PATCH 17/45] Use pylint for some files --- access_controller/asgi.py | 5 +---- access_controller/urls.py | 1 - main/admin.py | 7 ++++++- main/apiauth.py | 5 +++-- main/forms.py | 3 +++ main/serializers.py | 14 +++++++++++--- main/tests.py | 7 +++++-- main/urls.py | 4 ++++ 8 files changed, 33 insertions(+), 13 deletions(-) diff --git a/access_controller/asgi.py b/access_controller/asgi.py index 11dc22e..824ff57 100644 --- a/access_controller/asgi.py +++ b/access_controller/asgi.py @@ -8,10 +8,7 @@ https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/ """ import os - - - -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'access_controller.settings') from django.core.asgi import get_asgi_application +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'access_controller.settings') application = get_asgi_application() diff --git a/access_controller/urls.py b/access_controller/urls.py index 63dc19f..11e9fc7 100644 --- a/access_controller/urls.py +++ b/access_controller/urls.py @@ -14,7 +14,6 @@ Including another URLconf 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin -from django.contrib.auth import views from django.urls import path, include from main.views import main_page, profile_page, CustomRegistrationView, CustomLoginView, registration_error diff --git a/main/admin.py b/main/admin.py index 8c38f3f..b1e5a2d 100644 --- a/main/admin.py +++ b/main/admin.py @@ -1,3 +1,8 @@ -from django.contrib import admin +""" +Встроенный файл +""" + + +# from django.contrib import admin # Register your models here. diff --git a/main/apiauth.py b/main/apiauth.py index 08a018c..b6488ba 100644 --- a/main/apiauth.py +++ b/main/apiauth.py @@ -1,5 +1,6 @@ -import os - +""" +Авторизация по Zenpy. +""" from zenpy import Zenpy from zenpy.lib.api_objects import User as ZenpyUser diff --git a/main/forms.py b/main/forms.py index 36d2e88..5666255 100644 --- a/main/forms.py +++ b/main/forms.py @@ -1,3 +1,6 @@ +""" +Формы. +""" from django import forms from django.contrib.auth.forms import AuthenticationForm from django_registration.forms import RegistrationFormUniqueEmail diff --git a/main/serializers.py b/main/serializers.py index 5ed3138..e72dc7a 100644 --- a/main/serializers.py +++ b/main/serializers.py @@ -1,3 +1,6 @@ +""" +Сериализаторы. +""" from django.contrib.auth.models import User from rest_framework import serializers from main.models import UserProfile @@ -61,7 +64,12 @@ class ZendeskUserSerializer(serializers.Serializer): """ if obj.custom_role_id == ZENDESK_ROLES['engineer']: return 'engineer' - elif obj.custom_role_id == ZENDESK_ROLES['light_agent']: + if obj.custom_role_id == ZENDESK_ROLES['light_agent']: return 'light_agent' - else: - return "empty" + return "empty" + + def create(self, validated_data): + pass + + def update(self, instance, validated_data): + pass diff --git a/main/tests.py b/main/tests.py index b733ed1..4a18882 100644 --- a/main/tests.py +++ b/main/tests.py @@ -1,2 +1,5 @@ -from django.test import TestCase, Client -import access_controller.settings as sets +""" +Тесты. +""" +# from django.test import TestCase, Client +# import access_controller.settings as sets diff --git a/main/urls.py b/main/urls.py index fffe11d..5c55d9d 100644 --- a/main/urls.py +++ b/main/urls.py @@ -1,3 +1,7 @@ +""" +REST framework adds support for automatic URL routing to Django. +""" + from rest_framework.routers import DefaultRouter from main.views import UsersViewSet From cc83f292dec377a83d74386ac135000294c25f61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D1=82=D0=B5=D0=BF=D0=B0=D0=BD=D0=B5=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=20=D0=9E=D0=BB=D1=8C=D0=B3=D0=B0?= Date: Wed, 28 Apr 2021 19:36:21 +0300 Subject: [PATCH 18/45] Fix autopep8 problem --- .env.example | 25 +++++++++++++++++++++++++ requirements/common.txt | 2 +- 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..58e7b51 --- /dev/null +++ b/.env.example @@ -0,0 +1,25 @@ +ACTRL_DEBUG=1 + +ACTRL_SECRET_KEY="v1i_fb\$_jf2#1v_lcsbu&eon4u-os0^px=s^iycegdycqy&5)6" +ACTRL_HOST="actrl.example.com" + +ACTRL_EMAIL_HOST="smtp.mail.ru" +ACTRL_EMAIL_PORT=2525 +ACTRL_EMAIL_TLS=1 +ACTRL_EMAIL_HOST_USER="djgr.02@mail.ru" +ACTRL_EMAIL_HOST_PASSWORD="djangogroup02" +ACTRL_FROM_EMAIL="djgr.02@mail.ru" +ACTRL_SERVER_EMAIL="djgr.02@mail.ru" + +ENG_CROLE_ID=360005209000 +LA_CROLE_ID=360005208980 +EMPL_GROUP="Поддержка" +BUF_GROUP="Сменная группа" +ST_EMAIL="d.krikov@ngenix.net" +LICENSE_NO=3 +SHIFTH=12 + +ACTRL_ZENDESK_SUBDOMAIN="ngenix1612197338" +ACTRL_API_EMAIL="stepanenko_olga@mail.ru" +ACTRL_API_TOKEN="X1x4QeNa4xRdul2rTIKhac98AsXMwd5bOGAyZOtU" + diff --git a/requirements/common.txt b/requirements/common.txt index 58befa7..69294d4 100644 --- a/requirements/common.txt +++ b/requirements/common.txt @@ -17,4 +17,4 @@ sphinxcontrib-spelling==7.1.0 m2r == 0.2.1 pylint == 2.8.2 pylint-django == 2.4.4 -autopep8 = 1.5.6 +autopep8 == 1.5.6 From 3155374185b37a75e4ab172a1969b738ffb2140e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D1=82=D0=B5=D0=BF=D0=B0=D0=BD=D0=B5=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=20=D0=9E=D0=BB=D1=8C=D0=B3=D0=B0?= Date: Wed, 28 Apr 2021 21:17:41 +0300 Subject: [PATCH 19/45] Spell new docs --- README.md | 15 +++--- docs/source/conf.py | 17 +++---- docs/source/spelling_wordlist.txt | 84 +++++++++++++++++++++++++++++++ main/views.py | 2 +- 4 files changed, 101 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 154ca60..8838171 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,3 @@ -# ZenDesk Access Controller - ## Управление правами доступа Идея - написать программу(Web приложение), которая будет выдавать права пользователям системы по запросу самого @@ -39,7 +37,7 @@ ## Quickstart -Перед запуском требуется неообходимо `.env` файл. +Перед запуском необходимо создать `.env` файл. ```bash cp .env.example .env ``` @@ -71,7 +69,7 @@ pip install -r requirements/dev.txt ``` ACTRL_DEBUG={0/1} - включить режим дебага ACTRL_HOST={HOSTNAME} - при запуске без дебага, надо указать домен на котором будет работать приложение -ACTRL_SECRET_KEY={DJANGO_SECRET_KEY} - секретный ключ сгенерированый Django +ACTRL_SECRET_KEY={DJANGO_SECRET_KEY} - секретный ключ сгенерированный Django ACTRL_EMAIL_HOST={SMTP_HOST} - домен почтового сервера через который приложение будет отправлять письма, например "smtp.gmail.com" ACTRL_EMAIL_PORT={SMTP_PORT} - порт для почтового сервера, например 587, 465 , 2525 @@ -133,7 +131,7 @@ docker run -d -p 8000:8000 \ -v {ABSOLUTE_PATH_TO_DB}:/zendesk-access-controller/db \ access_controller:latest ``` -- открываем запущеный контейнер в браузере (можно перейти по ссылке http://localhost:8000/) +- открываем запущенный контейнер в браузере (можно перейти по ссылке http://localhost:8000/) ## Запуск с тестовыми юзерами: @@ -144,7 +142,7 @@ docker run -d -p 8000:8000 \ - Пользователь - `123@test.ru` / `zendeskuser` Не сработает если домен песочницы отличается от `ngenix1612197338` (на другом домене нужно будет создать сначала пользователей в песочнице с правами админа и легкого агента -с этими же почтами, назначить им организацию `SYSTEM`) +с этими же email, назначить им организацию `SYSTEM`) ## Параметры тестовой песочницы: @@ -156,6 +154,11 @@ pylint ../access_controller ## Для приведения файлов к стандарту PEP8 используем: autopep8 --in-place +##Для проверки орфографии: +cd docs +(set -a && source ../.env && make spelling) + + ## Read more - Zenpy: [http://docs.facetoe.com.au](http://docs.facetoe.com.au) - Zendesk API: [https://developer.zendesk.com/rest_api/docs/](https://developer.zendesk.com/rest_api/docs/) diff --git a/docs/source/conf.py b/docs/source/conf.py index 2bd665b..1495347 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -14,12 +14,11 @@ import os import sys import inspect import enchant -import m2r +import django sys.path.insert(0, os.path.abspath('../../')) -import django os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'access_controller.settings') os.environ.setdefault('DJANGO_CONFIGURATION', 'Dev') @@ -117,13 +116,12 @@ def skip_queryset(app, what, name, obj, skip, options): def fix_sig(app, what, name, obj, options, signature, return_annotation): return ("", "") -def setup(app): - # Register the docstring processor with sphinx - app.connect('autodoc-process-docstring', process_django_models) - app.connect('autodoc-skip-member', skip_queryset) - app.connect('autodoc-process-docstring', process_modules) - app.connect("autodoc-process-signature", fix_sig) - +# def setup(app): +# # Register the docstring processor with sphinx +# app.connect('autodoc-process-docstring', process_django_models) +# app.connect('autodoc-skip-member', skip_queryset) +# app.connect('autodoc-process-docstring', process_modules) +# app.connect("autodoc-process-signature", fix_sig) # Add any Sphinx extension module names here, as strings. They can be @@ -139,7 +137,6 @@ extensions = { 'sphinx.ext.inheritance_diagram', 'sphinx_autodoc_typehints', 'sphinxcontrib.spelling', - # 'm2r' } diff --git a/docs/source/spelling_wordlist.txt b/docs/source/spelling_wordlist.txt index 4ccad53..ad71758 100644 --- a/docs/source/spelling_wordlist.txt +++ b/docs/source/spelling_wordlist.txt @@ -83,7 +83,9 @@ functions Serializer Serializers Сериализатор +Сериализаторы сериализатор +сериализатора переадресации чекбоксов админских @@ -105,4 +107,86 @@ db Read Zenpy залогинен +т +д +rolchangelogs +извеcтно +role +View +Model +type +param +rtype +return +UsersViewSet +list +engineers +agents +request +rest +framework +response +Сэндбокс +админскими +логирование +code +block +d +p +ACTRL_DEBUG +дебага +ACTRL_HOST +HOSTNAME +ACTRL_SECRET_KEY +DJANGO_SECRET_KEY +ACTRL_EMAIL_HOST +SMTP_HOST +smtp.gmail.com +ACTRL_EMAIL_PORT +SMTP_PORT +ACTRL_EMAIL_TLS +USE_TLS +TLS +ACTRL_EMAIL_HOST_USER +USERNAME +ACTRL_EMAIL_HOST_PASSWORD +PASSWORD +ACTRL_FROM_EMAIL +EMAIL +ACTRL_SERVER_EMAIL +ACTRL_API_EMAIL +ACTRL_API_PASSWORD +ACTRL_API_TOKEN +API_TOKEN +ACTRL_ZENDESK_SUBDOMAIN +DOMAIN +ENG_CROLE_ID +ENGINEER_CUSTOM_ROLE_ID +custom_role_id +LA_CROLE_ID +LIGHT_AGENT_CUSTOM_ROLE_ID +EMPL_GROUP +EMPLOYEE_GROUP_NAME +ССКС +BUF_GROUP +BUFFER_GROUP_NAME +буферной +тикетами +ST_EMAIL +SOLVED_TICKETS_EMAIL +LICENSE_NO +LICENSE_NO +SHIFTH +SHIFT_HOURS +ABSOLUTE +ABSOLUTE_PATH_TO_DB +PATH +TO +DB +latest +in +place +cd +docs +a diff --git a/main/views.py b/main/views.py index 15f393e..38e7f73 100644 --- a/main/views.py +++ b/main/views.py @@ -347,7 +347,7 @@ class UsersViewSet(viewsets.ReadOnlyModelViewSet): :param queryset: Список пользователей с ролью 'agent' :type queryset: :class:`str` :param serializer_class: Класс сериализатор для модели профиля пользователя - :type serializer_class :class:`ProfileSerializer` + :type serializer_class: :class:`ProfileSerializer` """ queryset = UserProfile.objects.filter(role='agent') serializer_class = ProfileSerializer From eea8e0aab80536041fcc20de28d4a620cedaa341 Mon Sep 17 00:00:00 2001 From: Andrew Smirnov Date: Thu, 29 Apr 2021 20:37:38 +0300 Subject: [PATCH 20/45] Pylint improvements --- README.md | 2 +- access_controller/settings.py | 32 +++++++++++++------------------- main/extra_func.py | 10 ++++------ main/models.py | 16 +++++++--------- main/serializers.py | 4 ++-- main/views.py | 8 ++++---- 6 files changed, 31 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 154ca60..ea118fe 100644 --- a/README.md +++ b/README.md @@ -151,7 +151,7 @@ docker run -d -p 8000:8000 \ Пример полной конфигурации можно найти в [.env.example](.env.example). Почту и токен админа ZenDesk взять у руководителя (если вы не админ). ## Для проверки pylint используем: -pylint ../access_controller +pylint ## Для приведения файлов к стандарту PEP8 используем: autopep8 --in-place diff --git a/access_controller/settings.py b/access_controller/settings.py index 7b1f707..cc29c25 100644 --- a/access_controller/settings.py +++ b/access_controller/settings.py @@ -13,18 +13,16 @@ import os from pathlib import Path # Build paths inside the project like this: BASE_DIR / 'subdir'. -import django as django - BASE_DIR = Path(__file__).resolve().parent.parent # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = os.getenv('ACTRL_SECRET_KEY','empty') +SECRET_KEY = os.getenv('ACTRL_SECRET_KEY', 'empty') # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = bool(int(os.getenv('ACTRL_DEBUG',1))) +DEBUG = bool(int(os.getenv('ACTRL_DEBUG', '1'))) ALLOWED_HOSTS = [ '127.0.0.1', @@ -59,13 +57,13 @@ MIDDLEWARE = [ ROOT_URLCONF = 'access_controller.urls' EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' -EMAIL_HOST = os.getenv('ACTRL_EMAIL_HOST','smtp.gmail.com') -EMAIL_PORT = int(os.getenv('ACTRL_EMAIL_PORT',587)) -EMAIL_USE_TLS = bool(int(os.getenv('ACTRL_EMAIL_TLS',1))) -EMAIL_HOST_USER = os.getenv('ACTRL_EMAIL_HOST_USER','group02django@gmail.com') -EMAIL_HOST_PASSWORD = os.getenv('ACTRL_EMAIL_HOST_PASSWORD','djangogroup02') -DEFAULT_FROM_EMAIL = os.getenv('ACTRL_FROM_EMAIL',EMAIL_HOST_USER) -SERVER_EMAIL = os.getenv('ACTRL_SERVER_EMAIL',EMAIL_HOST_USER) +EMAIL_HOST = os.getenv('ACTRL_EMAIL_HOST', 'smtp.gmail.com') +EMAIL_PORT = int(os.getenv('ACTRL_EMAIL_PORT', '587')) +EMAIL_USE_TLS = bool(int(os.getenv('ACTRL_EMAIL_TLS', '1'))) +EMAIL_HOST_USER = os.getenv('ACTRL_EMAIL_HOST_USER', 'group02django@gmail.com') +EMAIL_HOST_PASSWORD = os.getenv('ACTRL_EMAIL_HOST_PASSWORD', 'djangogroup02') +DEFAULT_FROM_EMAIL = os.getenv('ACTRL_FROM_EMAIL', EMAIL_HOST_USER) +SERVER_EMAIL = os.getenv('ACTRL_SERVER_EMAIL', EMAIL_HOST_USER) TEMPLATES = [ { @@ -142,8 +140,6 @@ ACCOUNT_ACTIVATION_DAYS = 7 LOGIN_REDIRECT_URL = '/' LOGOUT_REDIRECT_URL = '/' - - # Название_приложения.Название_файла.Название_класса_обработчика AUTHENTICATION_BACKENDS = [ 'access_controller.auth.EmailAuthBackend', @@ -154,8 +150,8 @@ AUTHENTICATION_BACKENDS = [ ZENDESK_ROLES = { - 'engineer': int(os.getenv('ENG_CROLE_ID',0)), - 'light_agent': int(os.getenv('LA_CROLE_ID',0)), + 'engineer': int(os.getenv('ENG_CROLE_ID', '0')), + 'light_agent': int(os.getenv('LA_CROLE_ID', '0')), } ZENDESK_GROUPS = { @@ -165,7 +161,7 @@ ZENDESK_GROUPS = { SOLVED_TICKETS_EMAIL = os.getenv('ST_EMAIL') -ZENDESK_MAX_AGENTS = int(os.getenv('LICENSE_NO',0)) +ZENDESK_MAX_AGENTS = int(os.getenv('LICENSE_NO', '0')) REST_FRAMEWORK = { # Use Django's standard `django.contrib.auth` permissions, @@ -175,11 +171,9 @@ REST_FRAMEWORK = { ] } -ONE_DAY = int(os.getenv('SHIFTH',0)) # Количество часов в 1 рабочем дне +ONE_DAY = int(os.getenv('SHIFTH', '0')) # Количество часов в 1 рабочем дне ACTRL_ZENDESK_SUBDOMAIN = os.getenv('ACTRL_ZENDESK_SUBDOMAIN') or os.getenv('ZD_DOMAIN') ACTRL_API_EMAIL = os.getenv('ACTRL_API_EMAIL') or os.getenv('ACCESS_CONTROLLER_API_EMAIL') 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') - - diff --git a/main/extra_func.py b/main/extra_func.py index f215544..9cbbbdf 100644 --- a/main/extra_func.py +++ b/main/extra_func.py @@ -36,7 +36,7 @@ def update_role(user_profile: UserProfile, role: int) -> None: zendesk.admin.users.update(user) -def make_engineer(user_profile: UserProfile, who_changes: User) -> None: +def make_engineer(user_profile: UserProfile) -> None: """ Функция устанавливает пользователю роль инженера. @@ -47,7 +47,7 @@ def make_engineer(user_profile: UserProfile, who_changes: User) -> None: update_role(user_profile, ROLES['engineer']) -def make_light_agent(user_profile: UserProfile, who_changes: User) -> None: +def make_light_agent(user_profile: UserProfile) -> None: """ Функция устанавливает пользователю роль легкого агента. @@ -293,8 +293,7 @@ class StatisticData: stat = self._use_display(stat) stat = self._use_interval(stat) return stat - else: - return None + return None def is_valid_statistic(self) -> bool: """ @@ -336,8 +335,7 @@ class StatisticData: """ if self.is_valid_data(): return self.data - else: - return None + return None def is_valid_data(self) -> bool: """ diff --git a/main/models.py b/main/models.py index fcde327..172549f 100644 --- a/main/models.py +++ b/main/models.py @@ -1,10 +1,8 @@ """ Модели, использующиеся в приложении. """ - - +from django.contrib.auth import get_user_model from django.db import models -from django.contrib.auth.models import User from django.db.models.signals import post_save from django.dispatch import receiver from django.utils import timezone @@ -24,7 +22,7 @@ class UserProfile(models.Model): ('has_control_access', 'Can view admin page'), ) - user = models.OneToOneField(to=User, on_delete=models.CASCADE, help_text='Пользователь') + user = models.OneToOneField(to=get_user_model(), on_delete=models.CASCADE, help_text='Пользователь') role = models.CharField(default='None', max_length=100, help_text='Глобальное имя роли пользователя') custom_role_id = models.IntegerField(default=0, help_text='Код роли пользователя') image = models.URLField(null=True, blank=True, help_text='Аватарка') @@ -44,7 +42,7 @@ class UserProfile(models.Model): return 'UNDEFINED' -@receiver(post_save, sender=User) +@receiver(post_save, sender=get_user_model()) def create_user_profile(instance, created, **kwargs) -> None: """ Функция создания профиля пользователя (Userprofile) при регистрации пользователя. @@ -58,7 +56,7 @@ def create_user_profile(instance, created, **kwargs) -> None: UserProfile.objects.create(user=instance) -@receiver(post_save, sender=User) +@receiver(post_save, sender=get_user_model()) def save_user_profile(instance, **kwargs) -> None: """ Функция записи БД профиля пользователя. @@ -74,12 +72,12 @@ class RoleChangeLogs(models.Model): """ Модель для логирования изменений ролей пользователя. """ - user = models.ForeignKey(to=User, on_delete=models.CASCADE, + 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='Присвоенная роль') change_time = models.DateTimeField(default=timezone.now, help_text='Дата и время изменения роли') - changed_by = models.ForeignKey(to=User, on_delete=models.CASCADE, related_name='changed_by', + changed_by = models.ForeignKey(to=get_user_model(), on_delete=models.CASCADE, related_name='changed_by', help_text='Кем была изменена роль') @@ -106,7 +104,7 @@ class UnassignedTicket(models.Model): """ Модель не распределенного тикета. """ - assignee = models.ForeignKey(to=User, on_delete=models.CASCADE, related_name='tickets', + assignee = models.ForeignKey(to=get_user_model(), on_delete=models.CASCADE, related_name='tickets', help_text='Пользователь, с которого снят тикет') ticket_id = models.IntegerField(help_text='Номер тикера, для которого сняли ответственного') status = models.IntegerField(choices=UnassignedTicketStatus.choices, default=UnassignedTicketStatus.UNASSIGNED, diff --git a/main/serializers.py b/main/serializers.py index e72dc7a..70c4352 100644 --- a/main/serializers.py +++ b/main/serializers.py @@ -1,7 +1,7 @@ """ Сериализаторы. """ -from django.contrib.auth.models import User +from django.contrib.auth import get_user_model from rest_framework import serializers from main.models import UserProfile from access_controller.settings import ZENDESK_ROLES @@ -17,7 +17,7 @@ class UserSerializer(serializers.HyperlinkedModelSerializer): :type email: :class:`list` """ class Meta: - model = User + model = get_user_model() fields = ['email'] diff --git a/main/views.py b/main/views.py index 15f393e..92fe965 100644 --- a/main/views.py +++ b/main/views.py @@ -230,7 +230,7 @@ def work_hand_over(request: WSGIRequest) -> HttpResponseRedirect: :param request: данные текущего пользователя (login_required) :return: перезагрузка текущей страницы после выполнения смены роли """ - make_light_agent(request.user.userprofile, request.user) + make_light_agent(request.user.userprofile) return set_session_params_for_work_page(request) @@ -243,7 +243,7 @@ def work_become_engineer(request: WSGIRequest) -> HttpResponseRedirect: :param request: данные текущего пользователя (login_required) :return: перезагрузка текущей страницы после выполнения смены роли """ - make_engineer(request.user.userprofile, request.user) + make_engineer(request.user.userprofile) return set_session_params_for_work_page(request) @@ -317,7 +317,7 @@ class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageM :return: Обновленный список пользователей """ for user in users: - make_engineer(user, self.request.user) + make_engineer(user) log(user, self.request.user.userprofile) def make_light_agents(self, users: list) -> None: @@ -328,7 +328,7 @@ class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageM :return: Обновленный список пользователей """ for user in users: - make_light_agent(user, self.request.user) + make_light_agent(user) log(user, self.request.user.userprofile) From 010dff2f9ddf0a5a861efcdacac2cb0c248ee26a Mon Sep 17 00:00:00 2001 From: Andrew Smirnov Date: Thu, 29 Apr 2021 21:08:11 +0300 Subject: [PATCH 21/45] Fix model docs and enchant spelling tokenizer --- docs/source/conf.py | 44 ++++++++++++++++++++------------------------ main/models.py | 8 ++++++-- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 1495347..b4be21c 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -16,10 +16,8 @@ import inspect import enchant import django - sys.path.insert(0, os.path.abspath('../../')) - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'access_controller.settings') os.environ.setdefault('DJANGO_CONFIGURATION', 'Dev') @@ -37,7 +35,6 @@ from django.db.models.query import QuerySet QuerySet.__repr__ = lambda self: self.__class__.__name__ - django.setup() # -- Project information ----------------------------------------------------- @@ -87,6 +84,7 @@ def process_django_models(app, what, name, obj, options, lines): lines.append(':type %s: %s.%s' % (field.attname, module, field_type.__name__)) if enchant is not None: lines += spelling_white_list + lines.append('') return lines @@ -113,15 +111,17 @@ def skip_queryset(app, what, name, obj, skip, options): return True return skip -def fix_sig(app, what, name, obj, options, signature, return_annotation): - return ("", "") -# def setup(app): -# # Register the docstring processor with sphinx -# app.connect('autodoc-process-docstring', process_django_models) -# app.connect('autodoc-skip-member', skip_queryset) -# app.connect('autodoc-process-docstring', process_modules) -# app.connect("autodoc-process-signature", fix_sig) +def fix_sig(app, what, name, obj, options, signature, return_annotation): + return "", "" + + +def setup(app): + # Register the docstring processor with sphinx + app.connect('autodoc-process-docstring', process_django_models) + app.connect('autodoc-skip-member', skip_queryset) + app.connect('autodoc-process-docstring', process_modules) + app.connect("autodoc-process-signature", fix_sig) # Add any Sphinx extension module names here, as strings. They can be @@ -139,7 +139,6 @@ extensions = { 'sphinxcontrib.spelling', } - # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -186,17 +185,17 @@ autodoc_typehints = "description" # spell checking spelling_lang = 'ru_RU' -tokenizer_lang = 'ru_RU' -spelling_exclude_patterns=['ignored_*', '../../main/models.py'] +tokenizer_lang = 'en_US' +spelling_exclude_patterns = ['ignored_*', '../../main/models.py'] spelling_show_suggestions = True -spelling_show_whole_line=True -spelling_warning=True +spelling_show_whole_line = True +spelling_warning = True spelling_ignore_pypi_package_names = True -spelling_ignore_wiki_words=True -spelling_ignore_acronyms=True -spelling_ignore_python_builtins=True -spelling_ignore_importable_modules=True -spelling_ignore_contributor_names=True +spelling_ignore_wiki_words = True +spelling_ignore_acronyms = True +spelling_ignore_python_builtins = True +spelling_ignore_importable_modules = True +spelling_ignore_contributor_names = True # -- Options for todo extension ---------------------------------------------- @@ -206,6 +205,3 @@ set_type_checking_flag = True typehints_fully_qualified = True always_document_param_types = True typehints_document_rtype = True - - - diff --git a/main/models.py b/main/models.py index fcde327..6515fe5 100644 --- a/main/models.py +++ b/main/models.py @@ -17,6 +17,7 @@ class UserProfile(models.Model): Модель профиля пользователя. Профиль создается и изменяется при создании и изменении модель User. + """ class Meta: @@ -33,7 +34,9 @@ class UserProfile(models.Model): @property def zendesk_role(self) -> str: """ - Функция возвращает роль пользователя в Zendesk в формате str, либо UNDERFINED, если пользователь не найден. + Функция возвращает роль пользователя в Zendesk. + + В формате str, либо UNDEFINED, если пользователь не найден :return: Роль пользователя в Zendesk """ @@ -72,7 +75,7 @@ def save_user_profile(instance, **kwargs) -> None: class RoleChangeLogs(models.Model): """ - Модель для логирования изменений ролей пользователя. + Модель для логгирования изменений ролей пользователя """ user = models.ForeignKey(to=User, on_delete=models.CASCADE, help_text='Пользователь, которому присвоили другую роль') @@ -105,6 +108,7 @@ class UnassignedTicketStatus(models.IntegerChoices): class UnassignedTicket(models.Model): """ Модель не распределенного тикета. + """ assignee = models.ForeignKey(to=User, on_delete=models.CASCADE, related_name='tickets', help_text='Пользователь, с которого снят тикет') From 4041790e6feae57eb8ffae537ae11701c6c74ecb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D1=82=D0=B5=D0=BF=D0=B0=D0=BD=D0=B5=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=20=D0=9E=D0=BB=D1=8C=D0=B3=D0=B0?= Date: Thu, 6 May 2021 11:17:34 +0300 Subject: [PATCH 22/45] Spell new docs, create new README --- README.md | 9 ++++++++- docs/source/spelling_wordlist.txt | 1 + main/models.py | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8838171..4835b7e 100644 --- a/README.md +++ b/README.md @@ -152,11 +152,18 @@ docker run -d -p 8000:8000 \ pylint ../access_controller ## Для приведения файлов к стандарту PEP8 используем: -autopep8 --in-place +autopep8 --in-place filename ##Для проверки орфографии: cd docs + (set -a && source ../.env && make spelling) +##Для обновления документации: +m2r README.md + +cd docs + +(set -a && source ../.env && make html) ## Read more diff --git a/docs/source/spelling_wordlist.txt b/docs/source/spelling_wordlist.txt index ad71758..6292fea 100644 --- a/docs/source/spelling_wordlist.txt +++ b/docs/source/spelling_wordlist.txt @@ -189,4 +189,5 @@ place cd docs a +Аватарка diff --git a/main/models.py b/main/models.py index 6515fe5..ea3388a 100644 --- a/main/models.py +++ b/main/models.py @@ -112,6 +112,6 @@ class UnassignedTicket(models.Model): """ assignee = models.ForeignKey(to=User, on_delete=models.CASCADE, related_name='tickets', help_text='Пользователь, с которого снят тикет') - ticket_id = models.IntegerField(help_text='Номер тикера, для которого сняли ответственного') + ticket_id = models.IntegerField(help_text='Номер тикета, для которого сняли ответственного') status = models.IntegerField(choices=UnassignedTicketStatus.choices, default=UnassignedTicketStatus.UNASSIGNED, help_text='Статус тикета') From cd1196efb2b3722414e83680297c96ba13d562e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D1=82=D0=B5=D0=BF=D0=B0=D0=BD=D0=B5=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=20=D0=9E=D0=BB=D1=8C=D0=B3=D0=B0?= Date: Thu, 6 May 2021 11:26:22 +0300 Subject: [PATCH 23/45] Merge with develop --- README.rst | 201 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 README.rst diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..7a4e6ec --- /dev/null +++ b/README.rst @@ -0,0 +1,201 @@ + +Управление правами доступа +-------------------------- + +Идея - написать программу(Web приложение), которая будет выдавать права пользователям системы по запросу самого +пользователя. Например, из 12 человек 3 сейчас работают с правами админа, по окончании рабочей смены они сдают +свои права (освобождают места) и другие пользователи могут запросить эти права в свое пользование. + +Оставшиеся 9 человек получают права легкого агента - без прав редактирования, а только чтение. + +Из технологий - программа должна взаимодействовать с api системы Zendesk(система обращений клиентов - жалобы), +проверять авторизованного пользователя на права(будет возможность менять права напрямую из Zendesk - нужна +синхронизация прав с приоритетом у Zendesk). + +Если руками в самом Zendesk права у пользователя отобрали или наоборот +присвоили, то наша программа обновляет статус пользователя в соответствии с данными синхронизации +(например, раз в минуту). + +Так же в идеале должна быть проверка, что пользователь сайта существует на сайте Zendesk(по токену). + +Сэндбокс Zendesk нам предоставит моя компания, библиотеку для работы с api уже подсказали. +Сама программа (наша) будет обладать админскими правами и реализовывать контроль и выдачу прав другим пользователям. + +*Итого:* + + +#. Реализовать авторизацию пользователей с проверкой по API на существование такого пользователя +#. Реализовать интерфейс со статистикой рабочих мест(занято, свободно, кто занимает) +#. Реализовать логирование действий(когда взял права, когда отдал - запись в файл и БД) +#. Реализовать передачу прав приложением по запросу от пользователя и замену прав пользователя + у которого права отбираются внутри Zendesk (на легкий агент) +#. Реализовать синхронизацию по API на проверку прав(не менялись ли в системе Zendesk) +#. Реализовать возможность добавить большее количество админских прав +#. Реализовать возможность добавления легких агентов(права только на просмотр) +#. Реализовать на общей странице текущую информацию о пользователе - текущие права, карточка пользователя + +Технологический стек: +--------------------- + + +* Python 3 +* Django 3 + +Quickstart +---------- + +Перед запуском необходимо создать ``.env`` файл. + +.. code-block:: bash + + cp .env.example .env + +Заменить переменные в ``.env`` на актуальные. + +.. code-block:: bash + + 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) + +Перед запуском для тестирования: +-------------------------------- + +Убедитесь, что вы зарегистрированы в песочнице ZenDesk, у вас назначена организация ``SYSTEM`` +Для админов ZenDesk дополнительно - создайте токен доступа в ZenDesk +При запуске в Docker убедитесь что папка, которая будет служить хранилищем для БД, открыта на запись и чтение + +Запуск на локальной машине: +--------------------------- + + +* Скопировать репозиторий на локальную машину +* Перейти в папку приложения +* Активировать виртуальное окружение +* Выполнить команду ``pip install -r requirements/dev.txt`` +* В виртуальное окружение добавить следующие переменные: + +.. code-block:: + + ACTRL_DEBUG={0/1} - включить режим дебага + ACTRL_HOST={HOSTNAME} - при запуске без дебага, надо указать домен на котором будет работать приложение + ACTRL_SECRET_KEY={DJANGO_SECRET_KEY} - секретный ключ сгенерированный Django + + ACTRL_EMAIL_HOST={SMTP_HOST} - домен почтового сервера через который приложение будет отправлять письма, например "smtp.gmail.com" + ACTRL_EMAIL_PORT={SMTP_PORT} - порт для почтового сервера, например 587, 465 , 2525 + ACTRL_EMAIL_TLS={USE_TLS} - использовать TLS для подключения к почтовому серверу, 0 или 1 + ACTRL_EMAIL_HOST_USER={USERNAME} - логин с которым приложение входит на почтовый сервер + ACTRL_EMAIL_HOST_PASSWORD={PASSWORD} - пароль/ключ с которым приложение входит на почтовый сервер + ACTRL_FROM_EMAIL={EMAIL} - адрес с которого приложение отправляет письма + ACTRL_SERVER_EMAIL={EMAIL} - адрес на который отвечают пользователя + + ACTRL_API_EMAIL={EMAIL} - почта админа в ZenDesk + ACTRL_API_PASSWORD={PASSWORD} - пароль админа ZenDesk + ACTRL_API_TOKEN={API_TOKEN} - API токен зендеск + ACTRL_ZENDESK_SUBDOMAIN={DOMAIN} - домен ZenDesk + + ENG_CROLE_ID={ENGINEER_CUSTOM_ROLE_ID} - id роли инженера( custom_role_id сотрдника смены) + LA_CROLE_ID={LIGHT_AGENT_CUSTOM_ROLE_ID} - id роли легкого агента (custom_role_id роли -легкий агент) + EMPL_GROUP={EMPLOYEE_GROUP_NAME} - имя группы которой принадлежат сотрудники ССКС + BUF_GROUP={BUFFER_GROUP_NAME} - имя буферной группы для передачи смен(через нее происходит управление тикетами) + ST_EMAIL={SOLVED_TICKETS_EMAIL} - почта на которую будут переназначятся закрытые тикеты + LICENSE_NO={LICENSE_NO} - количество лицензий, отображаемых как доступные в приложении + SHIFTH={SHIFT_HOURS} - количество часов в рабочей смене (нужно для статистики, пока не реализовано но требует указания значения) + + +* Выполнить команду ``python manage.py migrate`` +* Запустить приложение командой ``python manage.py runserver`` (можно указать в параметрах для файла manage.py) +* Перейти по ссылке в консоли (вероятнее всего откроется по адресу http://127.0.0.1:8000/) + +Запуск в Docker: +---------------- + +Требуется установленный и настроенный Docker + + +* Скопировать репозиторий на локальную машину +* В командной строке перейти в папку проекта +* Выполнить команду ``docker build --tag access_controller:latest .`` +* Выполнить команду + .. code-block:: bash + + docker run -d -p 8000:8000 \ + ACTRL_DEBUG={0/1} \ + ACTRL_HOST={HOSTNAME} \ + ACTRL_SECRET_KEY={DJANGO_SECRET_KEY} \ + ACTRL_EMAIL_HOST={SMTP_HOST} \ + ACTRL_EMAIL_PORT={SMTP_PORT} \ + ACTRL_EMAIL_TLS={USE_TLS} \ + ACTRL_EMAIL_HOST_USER={USERNAME} \ + ACTRL_EMAIL_HOST_PASSWORD={PASSWORD} \ + ACTRL_FROM_EMAIL={EMAIL} \ + ACTRL_SERVER_EMAIL={EMAIL} \ + ACTRL_API_EMAIL={EMAIL} \ + ACTRL_API_PASSWORD={PASSWORD} \ + ACTRL_API_TOKEN={API_TOKEN} \ + ACTRL_ZENDESK_SUBDOMAIN={DOMAIN} \ + ENG_CROLE_ID={ENGINEER_CUSTOM_ROLE_ID} \ + LA_CROLE_ID={LIGHT_AGENT_CUSTOM_ROLE_ID} \ + EMPL_GROUP={EMPLOYEE_GROUP_NAME} \ + BUF_GROUP={BUFFER_GROUP_NAME} \ + ST_EMAIL={SOLVED_TICKETS_EMAIL} \ + LICENSE_NO={LICENSE_NO} \ + SHIFTH={SHIFT_HOURS} \ + -v {ABSOLUTE_PATH_TO_DB}:/zendesk-access-controller/db \ + access_controller:latest + +* открываем запущенный контейнер в браузере (можно перейти по ссылке http://localhost:8000/) + +Запуск с тестовыми юзерами: +--------------------------- + +На локальной машине - перед запуском команды ``python manage.py runserver`` выполнить команду ``python manage.py loaddata data.json`` +Это создаст тестового админа и тестового пользователя в приложении для песочницы ZenDesk. + + +* Админ - ``admin@gmail.com`` / ``zendeskadmin`` +* Пользователь - ``123@test.ru`` / ``zendeskuser`` + +Не сработает если домен песочницы отличается от ``ngenix1612197338`` (на другом домене нужно будет создать сначала пользователей в песочнице с правами админа и легкого агента +с этими же email, назначить им организацию ``SYSTEM``\ ) + +Параметры тестовой песочницы: +----------------------------- + +Пример полной конфигурации можно найти в `.env.example <.env.example>`_. Почту и токен админа ZenDesk взять у руководителя (если вы не админ). + +Для проверки pylint используем: +------------------------------- + +pylint ../access_controller + +Для приведения файлов к стандарту PEP8 используем: +-------------------------------------------------- + +autopep8 --in-place filename + +Для проверки орфографии: +------------------------ + +cd docs + +(set -a && source ../.env && make spelling) + +Для обновления документации: +---------------------------- + +m2r README.md + +cd docs + +(set -a && source ../.env && make html) + +Read more +--------- + + +* Zenpy: `http://docs.facetoe.com.au `_ +* Zendesk API: `https://developer.zendesk.com/rest_api/docs/ `_ From ab275fe3f8cc76ac58298b290624595c52b235ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D1=82=D0=B5=D0=BF=D0=B0=D0=BD=D0=B5=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=20=D0=9E=D0=BB=D1=8C=D0=B3=D0=B0?= Date: Thu, 6 May 2021 11:56:11 +0300 Subject: [PATCH 24/45] Remove second argument from light_agent and make_engineer functions --- main/extra_func.py | 14 ++++++-------- main/views.py | 8 ++++---- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/main/extra_func.py b/main/extra_func.py index 4dfbcba..61c337b 100644 --- a/main/extra_func.py +++ b/main/extra_func.py @@ -21,13 +21,12 @@ from main.models import UserProfile, RoleChangeLogs, UnassignedTicket, Unassigne from main.zendesk_admin import zenpy -def update_role(user_profile: UserProfile, role: int, who_changes: User) -> None: +def update_role(user_profile: UserProfile, role: int) -> None: """ Функция меняет роль пользователя. :param user_profile: Профиль пользователя :param role: Новая роль - :param who_changes: Пользователь, меняющий роль :return: Пользователь с обновленной ролью """ zendesk = zenpy @@ -35,21 +34,21 @@ def update_role(user_profile: UserProfile, role: int, who_changes: User) -> None user.custom_role_id = role user_profile.custom_role_id = role user_profile.save() - log(user_profile, who_changes.userprofile) + log(user_profile) zendesk.admin.users.update(user) -def make_engineer(user_profile: UserProfile, who_changes: User) -> None: +def make_engineer(user_profile: UserProfile) -> None: """ Функция устанавливает пользователю роль инженера. :param user_profile: Профиль пользователя :return: Вызов функции **update_role** с параметрами: профиль пользователя, роль "engineer" """ - update_role(user_profile, ROLES['engineer'], who_changes) + update_role(user_profile, ROLES['engineer']) -def make_light_agent(user_profile: UserProfile, who_changes: User) -> None: +def make_light_agent(user_profile: UserProfile) -> None: """ Функция устанавливает пользователю роль легкого агента. @@ -69,14 +68,13 @@ def make_light_agent(user_profile: UserProfile, who_changes: User) -> None: else: ticket.assignee = None ticket.group_id = zenpy.buffer_group_id - if tickets.count: zenpy.admin.tickets.update(tickets.values) attempts, success = 5, False while not success and attempts != 0: try: - update_role(user_profile, ROLES['light_agent'], who_changes) + update_role(user_profile, ROLES['light_agent']) success = True except APIException as e: attempts -= 1 diff --git a/main/views.py b/main/views.py index 35fa607..8f58b70 100644 --- a/main/views.py +++ b/main/views.py @@ -230,7 +230,7 @@ def work_hand_over(request: WSGIRequest) -> HttpResponseRedirect: :param request: данные текущего пользователя (login_required) :return: перезагрузка текущей страницы после выполнения смены роли """ - make_light_agent(request.user.userprofile, request.user) + make_light_agent(request.user.userprofile) return set_session_params_for_work_page(request) @@ -243,7 +243,7 @@ def work_become_engineer(request: WSGIRequest) -> HttpResponseRedirect: :param request: данные текущего пользователя (login_required) :return: перезагрузка текущей страницы после выполнения смены роли """ - make_engineer(request.user.userprofile, request.user) + make_engineer(request.user.userprofile) return set_session_params_for_work_page(request) @@ -317,7 +317,7 @@ class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageM :return: Обновленный список пользователей """ for user in users: - make_engineer(user, self.request.user) + make_engineer(user) def make_light_agents(self, users: list) -> None: """ @@ -327,7 +327,7 @@ class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageM :return: Обновленный список пользователей """ for user in users: - make_light_agent(user, self.request.user) + make_light_agent(user) class CustomLoginView(LoginView): From 9c9b4dd08b7f6f19080e17fbd6e58388ebb78365 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D1=82=D0=B5=D0=BF=D0=B0=D0=BD=D0=B5=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=20=D0=9E=D0=BB=D1=8C=D0=B3=D0=B0?= Date: Thu, 6 May 2021 12:26:45 +0300 Subject: [PATCH 25/45] Change User to get_user_model --- access_controller/settings.py | 14 +++++++------- main/extra_func.py | 18 +++++++++--------- main/models.py | 14 +++++++------- main/serializers.py | 4 ++-- main/tests.py | 6 +++--- main/views.py | 9 +++++---- 6 files changed, 33 insertions(+), 32 deletions(-) diff --git a/access_controller/settings.py b/access_controller/settings.py index 60367a3..37a7e63 100644 --- a/access_controller/settings.py +++ b/access_controller/settings.py @@ -24,7 +24,7 @@ BASE_DIR = Path(__file__).resolve().parent.parent SECRET_KEY = os.getenv('ACTRL_SECRET_KEY', 'empty') # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = bool(int(os.getenv('ACTRL_DEBUG', 1))) +DEBUG = bool(int(os.getenv('ACTRL_DEBUG', '1'))) ALLOWED_HOSTS = [ '127.0.0.1', @@ -60,8 +60,8 @@ ROOT_URLCONF = 'access_controller.urls' EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' EMAIL_HOST = os.getenv('ACTRL_EMAIL_HOST', 'smtp.gmail.com') -EMAIL_PORT = int(os.getenv('ACTRL_EMAIL_PORT', 587)) -EMAIL_USE_TLS = bool(int(os.getenv('ACTRL_EMAIL_TLS', 1))) +EMAIL_PORT = int(os.getenv('ACTRL_EMAIL_PORT', '587')) +EMAIL_USE_TLS = bool(int(os.getenv('ACTRL_EMAIL_TLS', '1'))) EMAIL_HOST_USER = os.getenv('ACTRL_EMAIL_HOST_USER', 'group02django@gmail.com') EMAIL_HOST_PASSWORD = os.getenv('ACTRL_EMAIL_HOST_PASSWORD', 'djangogroup02') DEFAULT_FROM_EMAIL = os.getenv('ACTRL_FROM_EMAIL', EMAIL_HOST_USER) @@ -154,8 +154,8 @@ AUTHENTICATION_BACKENDS = [ ZENDESK_ROLES = { - 'engineer': int(os.getenv('ENG_CROLE_ID', 0)), - 'light_agent': int(os.getenv('LA_CROLE_ID', 0)), + 'engineer': int(os.getenv('ENG_CROLE_ID', '0')), + 'light_agent': int(os.getenv('LA_CROLE_ID', '0')), } ZENDESK_GROUPS = { @@ -165,7 +165,7 @@ ZENDESK_GROUPS = { SOLVED_TICKETS_EMAIL = os.getenv('ST_EMAIL') -ZENDESK_MAX_AGENTS = int(os.getenv('LICENSE_NO', 0)) +ZENDESK_MAX_AGENTS = int(os.getenv('LICENSE_NO', '0')) REST_FRAMEWORK = { # Use Django's standard `django.contrib.auth` permissions, @@ -175,7 +175,7 @@ REST_FRAMEWORK = { ] } -ONE_DAY = int(os.getenv('SHIFTH', 0)) # Количество часов в 1 рабочем дне +ONE_DAY = int(os.getenv('SHIFTH', '0')) # Количество часов в 1 рабочем дне ACTRL_ZENDESK_SUBDOMAIN = os.getenv('ACTRL_ZENDESK_SUBDOMAIN') or os.getenv('ZD_DOMAIN') ACTRL_API_EMAIL = os.getenv('ACTRL_API_EMAIL') or os.getenv('ACCESS_CONTROLLER_API_EMAIL') diff --git a/main/extra_func.py b/main/extra_func.py index 4c4b9c9..3c796be 100644 --- a/main/extra_func.py +++ b/main/extra_func.py @@ -5,7 +5,7 @@ import logging from datetime import timedelta, datetime, date from typing import Optional, Union -from django.contrib.auth.models import User +from django.contrib.auth import get_user_model from django.core.exceptions import ObjectDoesNotExist from django.core.handlers.wsgi import WSGIRequest from django.http import HttpResponseRedirect, HttpResponsePermanentRedirect @@ -21,7 +21,7 @@ from main.models import UserProfile, RoleChangeLogs, UnassignedTicket, Unassigne from main.zendesk_admin import zenpy -def update_role(user_profile: UserProfile, role: int, who_changes: User) -> None: +def update_role(user_profile: UserProfile, role: int, who_changes: get_user_model()) -> None: """ Функция меняет роль пользователя. @@ -39,7 +39,7 @@ def update_role(user_profile: UserProfile, role: int, who_changes: User) -> None zendesk.admin.users.update(user) -def make_engineer(user_profile: UserProfile, who_changes: User) -> None: +def make_engineer(user_profile: UserProfile, who_changes: get_user_model()) -> None: """ Функция устанавливает пользователю роль инженера. @@ -49,7 +49,7 @@ def make_engineer(user_profile: UserProfile, who_changes: User) -> None: update_role(user_profile, ROLES['engineer'], who_changes) -def make_light_agent(user_profile: UserProfile, who_changes: User) -> None: +def make_light_agent(user_profile: UserProfile, who_changes: get_user_model()) -> None: """ Функция устанавливает пользователю роль легкого агента. @@ -193,7 +193,7 @@ def update_users_in_model(): users = get_users_list() for user in users: try: - profile = User.objects.get(email=user.email).userprofile + profile = get_user_model().objects.get(email=user.email).userprofile update_user_in_model(profile, user) except ObjectDoesNotExist: pass @@ -409,9 +409,9 @@ class StatisticData: try: self.data = RoleChangeLogs.objects.filter( change_time__range=[self.start_date, self.end_date + timedelta(days=1)], - user=User.objects.get(email=self.email), + user=get_user_model().objects.get(email=self.email), ).order_by('change_time') - except User.DoesNotExist: + except get_user_model().DoesNotExist: self.errors += ['Пользователь не найден'] def _init_statistic(self) -> None: @@ -475,7 +475,7 @@ class StatisticData: :param first_log: Первый лог """ - self.fill_daterange(max(User.objects.get(email=self.email).date_joined.date(), self.start_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() @@ -564,7 +564,7 @@ class CsvFormatter(logging.Formatter): return msg -def log(user: User, admin: int = 0) -> None: +def log(user: get_user_model(), admin: int = 0) -> None: """ Функция осуществляет запись логов в базу данных и csv файл. diff --git a/main/models.py b/main/models.py index fcde327..9ec53ca 100644 --- a/main/models.py +++ b/main/models.py @@ -4,7 +4,7 @@ from django.db import models -from django.contrib.auth.models import User +from django.contrib.auth import get_user_model from django.db.models.signals import post_save from django.dispatch import receiver from django.utils import timezone @@ -24,7 +24,7 @@ class UserProfile(models.Model): ('has_control_access', 'Can view admin page'), ) - user = models.OneToOneField(to=User, on_delete=models.CASCADE, help_text='Пользователь') + user = models.OneToOneField(to=get_user_model(), on_delete=models.CASCADE, help_text='Пользователь') role = models.CharField(default='None', max_length=100, help_text='Глобальное имя роли пользователя') custom_role_id = models.IntegerField(default=0, help_text='Код роли пользователя') image = models.URLField(null=True, blank=True, help_text='Аватарка') @@ -44,7 +44,7 @@ class UserProfile(models.Model): return 'UNDEFINED' -@receiver(post_save, sender=User) +@receiver(post_save, sender=get_user_model()) def create_user_profile(instance, created, **kwargs) -> None: """ Функция создания профиля пользователя (Userprofile) при регистрации пользователя. @@ -58,7 +58,7 @@ def create_user_profile(instance, created, **kwargs) -> None: UserProfile.objects.create(user=instance) -@receiver(post_save, sender=User) +@receiver(post_save, sender=get_user_model()) def save_user_profile(instance, **kwargs) -> None: """ Функция записи БД профиля пользователя. @@ -74,12 +74,12 @@ class RoleChangeLogs(models.Model): """ Модель для логирования изменений ролей пользователя. """ - user = models.ForeignKey(to=User, on_delete=models.CASCADE, + 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='Присвоенная роль') change_time = models.DateTimeField(default=timezone.now, help_text='Дата и время изменения роли') - changed_by = models.ForeignKey(to=User, on_delete=models.CASCADE, related_name='changed_by', + changed_by = models.ForeignKey(to=get_user_model(), on_delete=models.CASCADE, related_name='changed_by', help_text='Кем была изменена роль') @@ -106,7 +106,7 @@ class UnassignedTicket(models.Model): """ Модель не распределенного тикета. """ - assignee = models.ForeignKey(to=User, on_delete=models.CASCADE, related_name='tickets', + assignee = models.ForeignKey(to=get_user_model(), on_delete=models.CASCADE, related_name='tickets', help_text='Пользователь, с которого снят тикет') ticket_id = models.IntegerField(help_text='Номер тикера, для которого сняли ответственного') status = models.IntegerField(choices=UnassignedTicketStatus.choices, default=UnassignedTicketStatus.UNASSIGNED, diff --git a/main/serializers.py b/main/serializers.py index e72dc7a..70c4352 100644 --- a/main/serializers.py +++ b/main/serializers.py @@ -1,7 +1,7 @@ """ Сериализаторы. """ -from django.contrib.auth.models import User +from django.contrib.auth import get_user_model from rest_framework import serializers from main.models import UserProfile from access_controller.settings import ZENDESK_ROLES @@ -17,7 +17,7 @@ class UserSerializer(serializers.HyperlinkedModelSerializer): :type email: :class:`list` """ class Meta: - model = User + model = get_user_model() fields = ['email'] diff --git a/main/tests.py b/main/tests.py index c06bc21..42c8542 100644 --- a/main/tests.py +++ b/main/tests.py @@ -1,6 +1,6 @@ from urllib.parse import urlparse -from django.contrib.auth.models import User +from django.contrib.auth import get_user_model from django.core import mail from django.test import TestCase, Client from django.urls import reverse @@ -71,13 +71,13 @@ class RegistrationTestCase(TestCase): def test_registration_user_creating(self): with self.settings(EMAIL_BACKEND=self.email_backend): self.client.post(reverse('registration'), data={'email': self.any_zendesk_user_email}) - user = User.objects.get(email=self.any_zendesk_user_email) + user = get_user_model().objects.get(email=self.any_zendesk_user_email) zendesk_user = zenpy.get_user(self.any_zendesk_user_email) self.assertEqual(user.userprofile.name, zendesk_user.name) def test_permissions_applying(self): with self.settings(EMAIL_BACKEND=self.email_backend): self.client.post(reverse('registration'), data={'email': self.zendesk_admin_email}) - user = User.objects.get(email=self.zendesk_admin_email) + user = get_user_model().objects.get(email=self.zendesk_admin_email) self.assertEqual(user.userprofile.role, 'admin') self.assertTrue(user.has_perm('main.has_control_access')) diff --git a/main/views.py b/main/views.py index 8490a65..4a7567b 100644 --- a/main/views.py +++ b/main/views.py @@ -9,7 +9,8 @@ from typing import Dict, Any, Optional from django.contrib import messages from django.contrib.auth.decorators import login_required from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin -from django.contrib.auth.models import User, Permission +from django.contrib.auth import get_user_model +from django.contrib.auth.models import Permission from django.contrib.auth.tokens import default_token_generator from django.contrib.auth.forms import PasswordResetForm from django.contrib.auth.views import LoginView @@ -83,7 +84,7 @@ class CustomRegistrationView(RegistrationView): } redirect_url = 'done' - def register(self, form: CustomRegistrationForm) -> Optional[User]: + def register(self, form: CustomRegistrationForm) -> Optional[get_user_model()]: """ Функция регистрации пользователя. 1. Ввод email пользователя, указанный на Zendesk @@ -128,7 +129,7 @@ class CustomRegistrationView(RegistrationView): return None @staticmethod - def set_permission(user: User) -> None: + def set_permission(user: get_user_model()) -> None: """ Функция дает разрешение на просмотр страница администратора, если пользователь имеет роль admin. @@ -142,7 +143,7 @@ class CustomRegistrationView(RegistrationView): ) user.user_permissions.add(permission) - def get_success_url(self, user: User = None) -> Dict: + def get_success_url(self, user: get_user_model() = None) -> Dict: """ Функция возвращает url-адрес страницы, куда нужно перейти после успешной/не успешной регистрации. Используется самой django-registration. From f24c94914fd310f877df2d6827e1b6c357fa467f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D1=82=D0=B5=D0=BF=D0=B0=D0=BD=D0=B5=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=20=D0=9E=D0=BB=D1=8C=D0=B3=D0=B0?= Date: Thu, 6 May 2021 12:43:44 +0300 Subject: [PATCH 26/45] Change User to get_user_model - fix problems --- access_controller/auth.py | 10 +++++----- main/extra_func.py | 6 ++---- main/views.py | 4 ++-- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/access_controller/auth.py b/access_controller/auth.py index be707e1..00b43a2 100644 --- a/access_controller/auth.py +++ b/access_controller/auth.py @@ -1,19 +1,19 @@ from django.contrib.auth.backends import ModelBackend -from django.contrib.auth.models import User +from django.contrib.auth import get_user_model class EmailAuthBackend(ModelBackend): def authenticate(self, request, username=None, password=None, **kwargs): try: - user = User.objects.get(email=username) + user = get_user_model().objects.get(email=username) if user.check_password(password): return user return None - except User.DoesNotExist: + except get_user_model().DoesNotExist: return None def get_user(self, user_id): try: - return User.objects.get(pk=user_id) - except User.DoesNotExist: + return get_user_model().objects.get(pk=user_id) + except get_user_model().DoesNotExist: return None diff --git a/main/extra_func.py b/main/extra_func.py index 3c796be..10ed367 100644 --- a/main/extra_func.py +++ b/main/extra_func.py @@ -292,8 +292,7 @@ class StatisticData: stat = self._use_display(stat) stat = self._use_interval(stat) return stat - else: - return None + return None def is_valid_statistic(self) -> bool: """ @@ -335,8 +334,7 @@ class StatisticData: """ if self.is_valid_data(): return self.data - else: - return None + return None def is_valid_data(self) -> bool: """ diff --git a/main/views.py b/main/views.py index 4a7567b..fe31416 100644 --- a/main/views.py +++ b/main/views.py @@ -109,10 +109,10 @@ class CustomRegistrationView(RegistrationView): 'html_email_template_name': None, 'extra_email_context': None, } - user = User.objects.create_user( + user = get_user_model().objects.create_user( username=form.data['email'], email=form.data['email'], - password=User.objects.make_random_password(length=50) + password=get_user_model().objects.make_random_password(length=50) ) try: update_profile(user.userprofile) From 276c979b73125a3ce201d7db871418e872c94554 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D1=82=D0=B5=D0=BF=D0=B0=D0=BD=D0=B5=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=20=D0=9E=D0=BB=D1=8C=D0=B3=D0=B0?= Date: Thu, 6 May 2021 12:51:40 +0300 Subject: [PATCH 27/45] Change make_engineer --- main/extra_func.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/main/extra_func.py b/main/extra_func.py index 10ed367..cc8451b 100644 --- a/main/extra_func.py +++ b/main/extra_func.py @@ -39,17 +39,17 @@ def update_role(user_profile: UserProfile, role: int, who_changes: get_user_mode zendesk.admin.users.update(user) -def make_engineer(user_profile: UserProfile, who_changes: get_user_model()) -> None: +def make_engineer(user_profile: UserProfile) -> None: """ Функция устанавливает пользователю роль инженера. :param user_profile: Профиль пользователя :return: Вызов функции **update_role** с параметрами: профиль пользователя, роль "engineer" """ - update_role(user_profile, ROLES['engineer'], who_changes) + update_role(user_profile, ROLES['engineer']) -def make_light_agent(user_profile: UserProfile, who_changes: get_user_model()) -> None: +def make_light_agent(user_profile: UserProfile) -> None: """ Функция устанавливает пользователю роль легкого агента. @@ -76,7 +76,7 @@ def make_light_agent(user_profile: UserProfile, who_changes: get_user_model()) - attempts, success = 5, False while not success and attempts != 0: try: - update_role(user_profile, ROLES['light_agent'], who_changes) + update_role(user_profile, ROLES['light_agent']) success = True except APIException as e: attempts -= 1 From e0b855d27ad342c4e785eec8ded9280cfc69fc67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D1=82=D0=B5=D0=BF=D0=B0=D0=BD=D0=B5=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=20=D0=9E=D0=BB=D1=8C=D0=B3=D0=B0?= Date: Thu, 6 May 2021 14:37:08 +0300 Subject: [PATCH 28/45] Add ru tokenizer --- .../site-packages/enchant/tokenize/ru.py | 185 ++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 venv/lib/python3.6/site-packages/enchant/tokenize/ru.py diff --git a/venv/lib/python3.6/site-packages/enchant/tokenize/ru.py b/venv/lib/python3.6/site-packages/enchant/tokenize/ru.py new file mode 100644 index 0000000..7e15379 --- /dev/null +++ b/venv/lib/python3.6/site-packages/enchant/tokenize/ru.py @@ -0,0 +1,185 @@ +# pyenchant +# +# Copyright (C) 2004-2008, Ryan Kelly +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. +# +# In addition, as a special exception, you are +# given permission to link the code of this program with +# non-LGPL Spelling Provider libraries (eg: a MSFT Office +# spell checker backend) and distribute linked combinations including +# the two. You must obey the GNU Lesser General Public License in all +# respects for all of the code used other than said providers. If you modify +# this file, you may extend this exception to your version of the +# file, but you are not obligated to do so. If you do not wish to +# do so, delete this exception statement from your version. +# +""" + + enchant.tokenize.en: Tokenizer for the English language + + This module implements a PyEnchant text tokenizer for the English + language, based on very simple rules. + +""" + +import unicodedata + +import enchant.tokenize + + +class tokenize(enchant.tokenize.tokenize): # noqa: N801 + """Iterator splitting text into words, reporting position. + + This iterator takes a text string as input, and yields tuples + representing each distinct word found in the text. The tuples + take the form: + + (,) + + Where is the word string found and is the position + of the start of the word within the text. + + The optional argument may be used to specify a + list of additional characters that can form part of a word. + By default, this list contains only the apostrophe ('). Note that + these characters cannot appear at the start or end of a word. + """ + + _DOC_ERRORS = ["pos", "pos"] + + def __init__(self, text, valid_chars=None): + self._valid_chars = valid_chars + self._text = text + self._offset = 0 + # Select proper implementation of self._consume_alpha. + # 'text' isn't necessarily a string (it could be e.g. a mutable array) + # so we can't use isinstance(text, str) to detect unicode. + # Instead we typetest the first character of the text. + # If there's no characters then it doesn't matter what implementation + # we use since it won't be called anyway. + try: + char1 = text[0] + except IndexError: + self._initialize_for_binary() + else: + if isinstance(char1, str): + self._initialize_for_unicode() + else: + self._initialize_for_binary() + + def _initialize_for_binary(self): + self._consume_alpha = self._consume_alpha_b + if self._valid_chars is None: + self._valid_chars = ("'",) + + def _initialize_for_unicode(self): + self._consume_alpha = self._consume_alpha_u + if self._valid_chars is None: + # XXX TODO: this doesn't seem to work correctly with the + # MySpell provider, disabling for now. + # Allow unicode typographic apostrophe + # self._valid_chars = (u"'",u"\u2019") + self._valid_chars = ("'",) + + def _consume_alpha_b(self, text, offset): + """Consume an alphabetic character from the given bytestring. + + Given a bytestring and the current offset, this method returns + the number of characters occupied by the next alphabetic character + in the string. Non-ASCII bytes are interpreted as utf-8 and can + result in multiple characters being consumed. + """ + assert offset < len(text) + if text[offset].isalpha(): + return 1 + elif text[offset] >= "\x80": + return self._consume_alpha_utf8(text, offset) + return 0 + + def _consume_alpha_utf8(self, text, offset): + """Consume a sequence of utf8 bytes forming an alphabetic character.""" + incr = 2 + u = "" + while not u and incr <= 4: + try: + try: + # In the common case this will be a string + u = text[offset : offset + incr].decode("utf8") + except AttributeError: + # Looks like it was e.g. a mutable char array. + try: + s = text[offset : offset + incr].tostring() + except AttributeError: + s = "".join([c for c in text[offset : offset + incr]]) + u = s.decode("utf8") + except UnicodeDecodeError: + incr += 1 + if not u: + return 0 + if u.isalpha(): + return incr + if unicodedata.category(u)[0] == "M": + return incr + return 0 + + def _consume_alpha_u(self, text, offset): + """Consume an alphabetic character from the given unicode string. + + Given a unicode string and the current offset, this method returns + the number of characters occupied by the next alphabetic character + in the string. Trailing combining characters are consumed as a + single letter. + """ + assert offset < len(text) + incr = 0 + if text[offset].isalpha(): + incr = 1 + while offset + incr < len(text): + if unicodedata.category(text[offset + incr])[0] != "M": + break + incr += 1 + return incr + + def next(self): + text = self._text + offset = self._offset + while offset < len(text): + # Find start of next word (must be alpha) + while offset < len(text): + incr = self._consume_alpha(text, offset) + if incr: + break + offset += 1 + cur_pos = offset + # Find end of word using, allowing valid_chars + while offset < len(text): + incr = self._consume_alpha(text, offset) + if not incr: + if text[offset] in self._valid_chars: + incr = 1 + else: + break + offset += incr + # Return if word isn't empty + if cur_pos != offset: + # Make sure word doesn't end with a valid_char + while text[offset - 1] in self._valid_chars: + offset = offset - 1 + self._offset = offset + return (text[cur_pos:offset], cur_pos) + self._offset = offset + raise StopIteration() From b9cb22e2e56449d38ad868935463b39dc44786d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D1=82=D0=B5=D0=BF=D0=B0=D0=BD=D0=B5=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=20=D0=9E=D0=BB=D1=8C=D0=B3=D0=B0?= Date: Thu, 6 May 2021 14:49:45 +0300 Subject: [PATCH 29/45] Commit for lesson --- docs/source/spelling_wordlist.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/spelling_wordlist.txt b/docs/source/spelling_wordlist.txt index 6292fea..1e9713d 100644 --- a/docs/source/spelling_wordlist.txt +++ b/docs/source/spelling_wordlist.txt @@ -190,4 +190,5 @@ cd docs a Аватарка +filename From bb74072c32dc724263f079e213250ec11ee2d325 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D1=82=D0=B5=D0=BF=D0=B0=D0=BD=D0=B5=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=20=D0=9E=D0=BB=D1=8C=D0=B3=D0=B0?= Date: Thu, 6 May 2021 15:23:22 +0300 Subject: [PATCH 30/45] Change set_context function, using **kwargs --- main/views.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/main/views.py b/main/views.py index bdff369..e40b5e8 100644 --- a/main/views.py +++ b/main/views.py @@ -35,8 +35,7 @@ from main.serializers import ProfileSerializer, ZendeskUserSerializer from .models import UserProfile -def setup_context(profile_lit: bool = False, control_lit: bool = False, work_lit: bool = False, - registration_lit: bool = False, login_lit: bool = False, stats_lit: bool = False) -> Dict[str, Any]: +def setup_context(**kwargs) -> Dict[str, Any]: """ Функция добавления в контекст статуса пользователя. @@ -48,15 +47,12 @@ def setup_context(profile_lit: bool = False, control_lit: bool = False, work_lit :param stats_lit: True, при получении пользователем прав администратора (просмотр статистики), иначе False :return: Контекст (context) """ - - context = { - 'profile_lit': profile_lit, - 'control_lit': control_lit, - 'work_lit': work_lit, - 'registration_lit': registration_lit, - 'login_lit': login_lit, - 'stats_lit': stats_lit, - } + context = {} + for key in ('profile_lit', 'control_lit', 'work_lit', 'registration_lit', 'login_lit', 'stats_lit'): + if key in kwargs: + context.update({key: True}) + else: + context.update({key: False}) return context From 12db461b5d1c214259521b7f4395e7040c90ce29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D1=82=D0=B5=D0=BF=D0=B0=D0=BD=D0=B5=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=20=D0=9E=D0=BB=D1=8C=D0=B3=D0=B0?= Date: Thu, 6 May 2021 16:42:15 +0300 Subject: [PATCH 31/45] Pylint for views --- .pylintrc | 2 +- access_controller/urls.py | 2 +- main/extra_func.py | 2 +- main/views.py | 16 +++++++++------- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/.pylintrc b/.pylintrc index 49866c6..83b9512 100644 --- a/.pylintrc +++ b/.pylintrc @@ -537,7 +537,7 @@ max-branches=12 max-locals=15 # Maximum number of parents for a class (see R0901). -max-parents=7 +max-parents=10 # Maximum number of public methods for a class (see R0904). max-public-methods=20 diff --git a/access_controller/urls.py b/access_controller/urls.py index 2cab267..b474345 100644 --- a/access_controller/urls.py +++ b/access_controller/urls.py @@ -30,7 +30,7 @@ urlpatterns = [ path('accounts/register/error/', registration_error, name='registration_email_error'), path('accounts/login/', CustomLoginView.as_view(), name='login'), path('accounts/', include('django.contrib.auth.urls')), - path('work/', work_page, name="work"), + path('work/', work_page, name="work"), path('work/hand_over/', work_hand_over, name="work_hand_over"), path('work/become_engineer/', work_become_engineer, name="work_become_engineer"), path('work/get_tickets', work_get_tickets, name='work_get_tickets'), diff --git a/main/extra_func.py b/main/extra_func.py index cc8451b..fbbe3f7 100644 --- a/main/extra_func.py +++ b/main/extra_func.py @@ -76,7 +76,7 @@ def make_light_agent(user_profile: UserProfile) -> None: attempts, success = 5, False while not success and attempts != 0: try: - update_role(user_profile, ROLES['light_agent']) + update_role(user_profile, ROLES['light_agent'], get_user_model()) success = True except APIException as e: attempts -= 1 diff --git a/main/views.py b/main/views.py index e40b5e8..72fc682 100644 --- a/main/views.py +++ b/main/views.py @@ -28,7 +28,7 @@ from rest_framework.response import Response from access_controller.settings import DEFAULT_FROM_EMAIL, ZENDESK_ROLES, ZENDESK_MAX_AGENTS from main.extra_func import check_user_exist, update_profile, get_user_organization, make_engineer, make_light_agent, \ - get_users_list, update_users_in_model, count_users, StatisticData, log, set_session_params_for_work_page + get_users_list, update_users_in_model, count_users, StatisticData, set_session_params_for_work_page from main.zendesk_admin import zenpy from main.forms import AdminPageUsers, CustomRegistrationForm, CustomAuthenticationForm, StatisticForm from main.serializers import ProfileSerializer, ZendeskUserSerializer @@ -120,6 +120,7 @@ class CustomRegistrationView(RegistrationView): return None else: self.redirect_url = 'email_sending_error' + return None else: self.redirect_url = 'invalid_zendesk_email' return None @@ -180,7 +181,7 @@ def profile_page(request: WSGIRequest) -> HttpResponse: @login_required() -def work_page(request: WSGIRequest, id: int) -> HttpResponse: +def work_page(request: WSGIRequest, required_id: int) -> HttpResponse: """ Функция отображения страницы "Управления правами" для текущего пользователя (login_required). @@ -189,7 +190,7 @@ def work_page(request: WSGIRequest, id: int) -> HttpResponse: :return: адресация на страницу "Управления правами" (либо на страницу "Авторизации", если id и user.id не совпадают """ users = get_users_list() - if request.user.id == id: + if request.user.id == required_id: if request.session.get('is_confirm', None): messages.success(request, 'Изменения были применены') elif request.session.get('is_confirm', None) is not None: @@ -256,7 +257,7 @@ def work_get_tickets(request: WSGIRequest) -> HttpResponse: tickets = [ticket for ticket in zenpy.admin.search(type="ticket") if ticket.group.name == 'Сменная группа' and ticket.assignee is None] count = 0 - for i in range(len(tickets)): + for i in enumerate(tickets): if i == int(request.GET.get('count_tickets')): return set_session_params_for_work_page(request, count) tickets[i].assignee = zenpy_user @@ -306,7 +307,8 @@ class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageM self.make_light_agents(users) return super().form_valid(form) - def make_engineers(self, users: list) -> None: + @staticmethod + def make_engineers(users: list) -> None: """ Функция проходит по списку пользователей, проставляя статус "engineer". @@ -315,8 +317,8 @@ class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageM """ for user in users: make_engineer(user) - - def make_light_agents(self, users: list) -> None: + @staticmethod + def make_light_agents(users: list) -> None: """ Функция проходит по списку пользователей, проставляя статус "light agent". From 8646e668c1181851fd0624ba34185c21e5baf19e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D1=82=D0=B5=D0=BF=D0=B0=D0=BD=D0=B5=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=20=D0=9E=D0=BB=D1=8C=D0=B3=D0=B0?= Date: Thu, 6 May 2021 17:36:38 +0300 Subject: [PATCH 32/45] Pylint for extra_func --- .pylintrc | 2 +- main/extra_func.py | 20 +++++++++++--------- main/views.py | 16 ++++++++-------- 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/.pylintrc b/.pylintrc index 83b9512..07ab0f1 100644 --- a/.pylintrc +++ b/.pylintrc @@ -525,7 +525,7 @@ spelling-store-unknown-words=no max-args=5 # Maximum number of attributes for a class (see R0902). -max-attributes=7 +max-attributes=10 # Maximum number of boolean expressions in an if statement (see R0916). max-bool-expr=5 diff --git a/main/extra_func.py b/main/extra_func.py index fbbe3f7..af1555d 100644 --- a/main/extra_func.py +++ b/main/extra_func.py @@ -39,17 +39,17 @@ def update_role(user_profile: UserProfile, role: int, who_changes: get_user_mode zendesk.admin.users.update(user) -def make_engineer(user_profile: UserProfile) -> None: +def make_engineer(user_profile: UserProfile, who_changes: get_user_model()) -> None: """ Функция устанавливает пользователю роль инженера. :param user_profile: Профиль пользователя :return: Вызов функции **update_role** с параметрами: профиль пользователя, роль "engineer" """ - update_role(user_profile, ROLES['engineer']) + update_role(user_profile, ROLES['engineer'], who_changes) -def make_light_agent(user_profile: UserProfile) -> None: +def make_light_agent(user_profile: UserProfile, who_changes: get_user_model()) -> None: """ Функция устанавливает пользователю роль легкого агента. @@ -76,7 +76,7 @@ def make_light_agent(user_profile: UserProfile) -> None: attempts, success = 5, False while not success and attempts != 0: try: - update_role(user_profile, ROLES['light_agent'], get_user_model()) + update_role(user_profile, ROLES['light_agent'], who_changes) success = True except APIException as e: attempts -= 1 @@ -214,17 +214,17 @@ def daterange(start_date: timedelta, end_date: timedelta) -> list: return dates -def get_timedelta(log: RoleChangeLogs, time: timedelta = None) -> timedelta: +def get_timedelta(current_log: RoleChangeLogs, time: timedelta = None) -> timedelta: """ Функция возвращает объект класса timedelta, который хранит промежуток времени от начала суток до момента, который находится в log (объект класса RoleChangeLogs) или в time(datetime.time), если введён. - :param log: Лог + :param current_log: Лог :param time: Время :return: Сколько времени прошло от начала суток до события """ if time is None: - time = log.change_time.time() + time = current_log.change_time.time() time = timedelta(hours=time.hour, minutes=time.minute, seconds=time.second) return time @@ -285,7 +285,8 @@ class StatisticData: """ Функция возвращает статистику работы пользователя. - :return: Словарь statistic с применением формата отображения и интервала работы(если они есть). None, если были ошибки при создании. + :return: Словарь statistic с применением формата отображения и интервала работы(если они есть). None, если были + ошибки при создании. """ if self.is_valid_statistic(): stat = self.statistic @@ -399,7 +400,8 @@ class StatisticData: """ Функция возвращает логи в диапазоне дат start_date - end_date для пользователя с указанным email. - :return: Данные о смене статусов пользователя. Если пользователь не найден или интервал времени некорректен - ошибку. + :return: Данные о смене статусов пользователя. Если пользователь не найден или интервал времени + некорректен - ошибку. """ if not self.check_time(): self.errors += ['Конец диапазона должен быть позже начала диапазона и раньше текущего времени'] diff --git a/main/views.py b/main/views.py index 72fc682..ffc2c68 100644 --- a/main/views.py +++ b/main/views.py @@ -228,7 +228,7 @@ def work_hand_over(request: WSGIRequest) -> HttpResponseRedirect: :param request: данные текущего пользователя (login_required) :return: перезагрузка текущей страницы после выполнения смены роли """ - make_light_agent(request.user.userprofile) + make_light_agent(request.user.userprofile, request.user) return set_session_params_for_work_page(request) @@ -241,7 +241,7 @@ def work_become_engineer(request: WSGIRequest) -> HttpResponseRedirect: :param request: данные текущего пользователя (login_required) :return: перезагрузка текущей страницы после выполнения смены роли """ - make_engineer(request.user.userprofile) + make_engineer(request.user.userprofile, request.user) return set_session_params_for_work_page(request) @@ -307,8 +307,8 @@ class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageM self.make_light_agents(users) return super().form_valid(form) - @staticmethod - def make_engineers(users: list) -> None: + + def make_engineers(self, users: list) -> None: """ Функция проходит по списку пользователей, проставляя статус "engineer". @@ -316,9 +316,9 @@ class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageM :return: Обновленный список пользователей """ for user in users: - make_engineer(user) - @staticmethod - def make_light_agents(users: list) -> None: + make_engineer(user, self.request.user) + + def make_light_agents(self, users: list) -> None: """ Функция проходит по списку пользователей, проставляя статус "light agent". @@ -326,7 +326,7 @@ class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageM :return: Обновленный список пользователей """ for user in users: - make_light_agent(user) + make_light_agent(user, self.request.user) class CustomLoginView(LoginView): From 22d4833ae0171b39f3b0aaa5826d8ce64d1b8199 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D1=82=D0=B5=D0=BF=D0=B0=D0=BD=D0=B5=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=20=D0=9E=D0=BB=D1=8C=D0=B3=D0=B0?= Date: Thu, 6 May 2021 17:53:28 +0300 Subject: [PATCH 33/45] Pylint for lesson --- README.md | 2 +- access_controller/settings.py | 4 +--- main/models.py | 3 +-- main/tests.py | 35 ++++++++++++++++++++++++++++++++--- 4 files changed, 35 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 4835b7e..8885598 100644 --- a/README.md +++ b/README.md @@ -149,7 +149,7 @@ docker run -d -p 8000:8000 \ Пример полной конфигурации можно найти в [.env.example](.env.example). Почту и токен админа ZenDesk взять у руководителя (если вы не админ). ## Для проверки pylint используем: -pylint ../access_controller +pylint ../access_controller (каталог, где лежит проект) ## Для приведения файлов к стандарту PEP8 используем: autopep8 --in-place filename diff --git a/access_controller/settings.py b/access_controller/settings.py index 37a7e63..b1b98a9 100644 --- a/access_controller/settings.py +++ b/access_controller/settings.py @@ -13,7 +13,7 @@ import os from pathlib import Path # Build paths inside the project like this: BASE_DIR / 'subdir'. -import django as django + BASE_DIR = Path(__file__).resolve().parent.parent @@ -181,5 +181,3 @@ ACTRL_ZENDESK_SUBDOMAIN = os.getenv('ACTRL_ZENDESK_SUBDOMAIN') or os.getenv('ZD_ ACTRL_API_EMAIL = os.getenv('ACTRL_API_EMAIL') or os.getenv('ACCESS_CONTROLLER_API_EMAIL') 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') - - diff --git a/main/models.py b/main/models.py index 24db2f2..6c5563a 100644 --- a/main/models.py +++ b/main/models.py @@ -40,9 +40,8 @@ class UserProfile(models.Model): :return: Роль пользователя в Zendesk """ - id = self.custom_role_id for role, r_id in ZENDESK_ROLES.items(): - if r_id == id: + if r_id == self.custom_role_id: return role return 'UNDEFINED' diff --git a/main/tests.py b/main/tests.py index 42c8542..b36c130 100644 --- a/main/tests.py +++ b/main/tests.py @@ -1,3 +1,8 @@ +""" +Тесты. +""" + + from urllib.parse import urlparse from django.contrib.auth import get_user_model @@ -11,31 +16,49 @@ from main.zendesk_admin import zenpy class RegistrationTestCase(TestCase): + """ + Класс тестирования регистрации пользователя. + """ fixtures = ['fixtures/data.json'] - def setUp(self): + def setUp(self) -> None: + """ + Функция предтестовых настроек. + """ self.email_backend = 'django.core.mail.backends.locmem.EmailBackend' self.any_zendesk_user_email = 'idar.sokurov.05@mail.ru' self.zendesk_admin_email = 'idar.sokurov.05@mail.ru' self.client = Client() - def test_registration_complete_redirect(self): + def test_registration_complete_redirect(self) -> None: + """ + Функция тестирования успешно завершенной регистрации. + """ 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): + """ + Функция тестирования неуспешной регистрации. + """ 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): + """ + Функция тестирования попытки регистрации уже зарегистрированного пользователя. + """ 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_email_sending(self): + """ + Функция тестирования отправки email. # TODO: Найти способ лучше проверять сообщения + """ email_template = [ '', 'Вы получили это письмо, потому что вы (или кто-то другой) запросили восстановление пароля ' @@ -62,13 +85,16 @@ class RegistrationTestCase(TestCase): self.assertEqual(mail.outbox[0].from_email, sets.DEFAULT_FROM_EMAIL) message = mail.outbox[0].body.split('\n') - for i in range(len(message)): + for i in enumerate(message): if email_template[i] != 'url': self.assertEqual(message[i], email_template[i]) else: self.assertTrue(urlparse(message[i]).scheme) def test_registration_user_creating(self): + """ + Функция тестирования регистрации пользователя (сверяем имя с именем в Zendesk. + """ with self.settings(EMAIL_BACKEND=self.email_backend): self.client.post(reverse('registration'), data={'email': self.any_zendesk_user_email}) user = get_user_model().objects.get(email=self.any_zendesk_user_email) @@ -76,6 +102,9 @@ class RegistrationTestCase(TestCase): self.assertEqual(user.userprofile.name, zendesk_user.name) def test_permissions_applying(self): + """ + Функция тестирования проверке присвоения роли admin. + """ with self.settings(EMAIL_BACKEND=self.email_backend): self.client.post(reverse('registration'), data={'email': self.zendesk_admin_email}) user = get_user_model().objects.get(email=self.zendesk_admin_email) From 5ea0aa945460b18b9d0c90176ab554aa4a17a942 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D1=82=D0=B5=D0=BF=D0=B0=D0=BD=D0=B5=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=20=D0=9E=D0=BB=D1=8C=D0=B3=D0=B0?= Date: Thu, 6 May 2021 18:17:20 +0300 Subject: [PATCH 34/45] Fix --- access_controller/settings.py | 2 -- main/extra_func.py | 23 ++++++++++++++--------- main/views.py | 1 - 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/access_controller/settings.py b/access_controller/settings.py index 1619d5d..b1b98a9 100644 --- a/access_controller/settings.py +++ b/access_controller/settings.py @@ -181,5 +181,3 @@ ACTRL_ZENDESK_SUBDOMAIN = os.getenv('ACTRL_ZENDESK_SUBDOMAIN') or os.getenv('ZD_ ACTRL_API_EMAIL = os.getenv('ACTRL_API_EMAIL') or os.getenv('ACCESS_CONTROLLER_API_EMAIL') 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') - - diff --git a/main/extra_func.py b/main/extra_func.py index 2c7e0a4..bfa090b 100644 --- a/main/extra_func.py +++ b/main/extra_func.py @@ -5,7 +5,7 @@ import logging from datetime import timedelta, datetime, date from typing import Optional, Union -from django.contrib.auth.models import User +from django.contrib.auth import get_user_model from django.core.exceptions import ObjectDoesNotExist from django.core.handlers.wsgi import WSGIRequest from django.http import HttpResponseRedirect, HttpResponsePermanentRedirect @@ -34,7 +34,6 @@ def update_role(user_profile: UserProfile, role: int, who_changes: get_user_mode user.custom_role_id = role user_profile.custom_role_id = role user_profile.save() - log(user_profile, who_changes.userprofile) zendesk.admin.users.update(user) @@ -54,7 +53,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 @@ -62,7 +62,8 @@ def make_light_agent(user_profile: UserProfile, who_changes: get_user_model()) - UnassignedTicket.objects.create( assignee=user_profile.user, ticket_id=ticket.id, - status=UnassignedTicketStatus.SOLVED if ticket.status == 'solved' else UnassignedTicketStatus.UNASSIGNED + status=UnassignedTicketStatus.SOLVED if ticket.status == 'solved' + else UnassignedTicketStatus.UNASSIGNED ) if ticket.status == 'solved': ticket.assignee_id = zenpy.solved_tickets_user_id @@ -285,8 +286,9 @@ class StatisticData: """ Функция возвращает статистику работы пользователя. - :return: Словарь statistic с применением формата отображения и интервала работы(если они есть). None, если были - ошибки при создании. + :return: Словарь statistic с применением формата отображения + и интервала работы(если они есть). + None, если были ошибки при создании. """ if self.is_valid_statistic(): stat = self.statistic @@ -375,9 +377,12 @@ class StatisticData: if self.interval == 'months': # Переделываем ключи под формат('начало_месяца - конец_месяца') for key, value in stat.items(): - current_month_start = max(self.start_date, date(year=key.year, month=key.month, day=1)) - current_month_end = min(self.end_date, last_day_of_month(date(year=key.year, month=key.month, day=1))) - index = ' - '.join([str(current_month_start), str(current_month_end)]) + current_month_start = max(self.start_date, date( + year=key.year, month=key.month, day=1)) + current_month_end = min(self.end_date, last_day_of_month( + date(year=key.year, month=key.month, day=1))) + index = ' - '.join([str(current_month_start), + str(current_month_end)]) if new_stat.get(index): new_stat[index] += value else: diff --git a/main/views.py b/main/views.py index 7a0eca5..ed5effa 100644 --- a/main/views.py +++ b/main/views.py @@ -307,7 +307,6 @@ class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageM self.make_light_agents(users) return super().form_valid(form) - def make_engineers(self, users: list) -> None: """ Функция проходит по списку пользователей, проставляя статус "engineer". From 015016bd6efde3ca81feb88e687bf96bdfe12b91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D1=82=D0=B5=D0=BF=D0=B0=D0=BD=D0=B5=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=20=D0=9E=D0=BB=D1=8C=D0=B3=D0=B0?= Date: Mon, 10 May 2021 20:43:17 +0300 Subject: [PATCH 35/45] Merge with "develop", begin to check again --- .env.example | 4 +- data.json | 12 +- main/extra_func.py | 252 ----------------------------------------- main/models.py | 1 - main/statistic_data.py | 45 ++++---- main/views.py | 19 ++-- 6 files changed, 42 insertions(+), 291 deletions(-) diff --git a/.env.example b/.env.example index 58e7b51..6026a6a 100644 --- a/.env.example +++ b/.env.example @@ -3,8 +3,8 @@ ACTRL_DEBUG=1 ACTRL_SECRET_KEY="v1i_fb\$_jf2#1v_lcsbu&eon4u-os0^px=s^iycegdycqy&5)6" ACTRL_HOST="actrl.example.com" -ACTRL_EMAIL_HOST="smtp.mail.ru" -ACTRL_EMAIL_PORT=2525 +ACTRL_EMAIL_HOST="smtp.gmail.com" +ACTRL_EMAIL_PORT=587 ACTRL_EMAIL_TLS=1 ACTRL_EMAIL_HOST_USER="djgr.02@mail.ru" ACTRL_EMAIL_HOST_PASSWORD="djangogroup02" diff --git a/data.json b/data.json index 97678f3..a4310a4 100644 --- a/data.json +++ b/data.json @@ -1,7 +1,7 @@ [ { "model": "auth.user", - "pk": 3, + "pk": 1, "fields": { "password": "pbkdf2_sha256$216000$gHBBCr1jBELf$ZkEDW3IEd8Wij7u8vkv+0Eze32CS01bcaYWhcD9OIC4=", "last_login": null, @@ -19,16 +19,16 @@ }, { "model": "main.userprofile", - "pk": 3, + "pk": 1, "fields": { "name": "ZendeskAdmin", - "user": 3, + "user": 1, "role": "admin" } }, { "model": "auth.user", - "pk": 4, + "pk": 2, "fields": { "password": "pbkdf2_sha256$216000$5qLJgrm2Quq9$KDBNNymVZXkUx0HKBPFst2m83kLe0egPBnkW7KnkORU=", "last_login": null, @@ -46,10 +46,10 @@ }, { "model": "main.userprofile", - "pk": 4, + "pk": 2, "fields": { "name": "UserForAccessTest", - "user": 4, + "user": 2, "role": "agent", "custom_role_id": "360005209000" } diff --git a/main/extra_func.py b/main/extra_func.py index 43a1fe9..f13b013 100644 --- a/main/extra_func.py +++ b/main/extra_func.py @@ -244,258 +244,6 @@ def last_day_of_month(day: int) -> int: return next_month - timedelta(days=next_month.day) -class StatisticData: - """ - Класс для учета статистики интервалов работы пользователей. - Передаваемые параметры: start_date, end_date, email, stat. - - :param display: Формат отображения времени (часы, минуты) - :type display: :class:`list` - :param interval: Интервал времени в часах и минутах - :type interval: :class:`list` - :param start_date: Дата начала работы - :type start_date: :class:`date` - :param end_date: Дата окончания работы - :type end_date: :class:`date` - :param email: Email пользователя - :type email: :class:`str` - :param errors: Список ошибок - :type errors: :class:`list` - :param warnings: Список предупреждений - :type warnings: :class:`list` - :param data: Ретроспектива смены ролей пользователя - :type data: :class:`dict` - :param statistic: Интервалы работы пользователя - :type statistic: :class:`dict` - """ - - def __init__(self, start_date, end_date, user_email, stat=None): - self.display = None - self.interval = None - self.start_date = start_date - self.end_date = end_date - self.email = user_email - self.errors = list() - self.warnings = list() - self.data = dict() - self.statistic = dict() - self._init_data() - if stat is None: - self._init_statistic() - else: - self.statistic = stat - - def get_statistic(self) -> dict: - """ - Функция возвращает статистику работы пользователя. - - :return: Словарь statistic с применением формата отображения и интервала работы(если они есть). None, если были ошибки при создании. - """ - if self.is_valid_statistic(): - stat = self.statistic - stat = self._use_display(stat) - stat = self._use_interval(stat) - return stat - else: - return None - - def is_valid_statistic(self) -> bool: - """ - Функция проверяет были ли ошибки при создании статистики. - - :return: True, при отсутствии ошибок - """ - return not self.errors and self.statistic - - def set_interval(self, interval: list) -> bool: - """ - Функция проверяет корректность представления интервала работы. - - :param interval: Интервал должен быть указан в днях или месяцах. - :return: True, если указан верно - """ - if interval not in ['months', 'days']: - self.errors += ['Интервал работы должен быть в днях или месяцах'] - return False - self.interval = interval - return True - - def set_display(self, display_format: list) -> bool: - """ - Функция проверяет корректность формата отображения интервала. - - :param display_format: Формат отображения должен быть указан в днях или месяцах. - :return: True, если указан верно - """ - if display_format not in ['days', 'hours']: - self.errors += ['Формат отображения должен быть в часах или днях'] - return False - self.display = display_format - return True - - def get_data(self) -> Optional[dict]: - """ - Функция возвращает данные - список объектов RoleChangeLogs. - """ - if self.is_valid_data(): - return self.data - else: - return None - - def is_valid_data(self) -> bool: - """ - Функция определяет были ли ошибки при получении логов. - - :return: True, если ошибок нет - """ - return not self.errors - - def _use_display(self, stat: list) -> list: - """ - Функция приводит данные к формату отображения. - - :param stat: Список данных статистики пользователя - :return: Обновленный список - """ - if not self.is_valid_statistic() or not self.display: - return stat - new_stat = {} - for key, item in stat.items(): - if self.display == 'hours': - new_stat[key] = item / 3600 - elif self.display == 'days': - new_stat[key] = item / (ONE_DAY * 3600) - return new_stat - - def _use_interval(self, stat: dict) -> dict: - """ - Функция объединяет ключи и значения в соответствии с интервалом работы. - - :param stat: Статистика работы пользователя - :return: Обновленная статистика - """ - if not self.is_valid_statistic() or not self.interval: - return stat - new_stat = {} - if self.interval == 'months': - # Переделываем ключи под формат('начало_месяца - конец_месяца') - for key, value in stat.items(): - current_month_start = max(self.start_date, date(year=key.year, month=key.month, day=1)) - current_month_end = min(self.end_date, last_day_of_month(date(year=key.year, month=key.month, day=1))) - index = ' - '.join([str(current_month_start), str(current_month_end)]) - if new_stat.get(index): - new_stat[index] += value - else: - new_stat[index] = value - elif self.interval == 'days': - new_stat = stat # статистика изначально в днях - return new_stat - - def check_time(self) -> bool: - """ - Функция проверяет корректность введенного времени. - - :return: True, если время указано корректно. Иначе, False - """ - if self.end_date < self.start_date or self.end_date > datetime.now().date(): - return False - return True - - def _init_data(self): - """ - Функция возвращает логи в диапазоне дат start_date - end_date для пользователя с указанным email. - - :return: Данные о смене статусов пользователя. Если пользователь не найден или интервал времени некорректен - ошибку. - """ - if not self.check_time(): - self.errors += ['Конец диапазона должен быть позже начала диапазона и раньше текущего времени'] - return - try: - self.data = RoleChangeLogs.objects.filter( - change_time__range=[self.start_date, self.end_date + timedelta(days=1)], - user=User.objects.get(email=self.email), - ).order_by('change_time') - except User.DoesNotExist: - self.errors += ['Пользователь не найден'] - - def _init_statistic(self) -> dict: - """ - Функция заполняет словарь, в котором ключ - дата, значение - кол-во проработанных в этот день секунд. - - :return: Статистика работы пользователя (statistic) - """ - self.clear_statistic() - if not self.get_data(): - self.warnings += ['Не обнаружены изменения роли в данном промежутке'] - return None - first_log, last_log = self.data[0], self.data[len(self.data) - 1] - - if first_log.old_role == ROLES['engineer']: - self.prev_engineer_logic(first_log) - - if last_log.new_role == ROLES['engineer']: - self.post_engineer_logic(last_log) - - for log_index in range(len(self.data) - 1): - if self.data[log_index].new_role == ROLES['engineer']: - self.engineer_logic(log_index) - - def engineer_logic(self, log_index): - """ - Функция обрабатывает основную часть работы инженера - """ - current_log, next_log = self.data[log_index], self.data[log_index + 1] - if current_log.change_time.date() != next_log.change_time.date(): - self.statistic[current_log.change_time.date()] += ( - timedelta(days=1) - get_timedelta(current_log)).total_seconds() - self.statistic[next_log.change_time.date()] += get_timedelta(next_log).total_seconds() - self.fill_daterange(current_log.change_time.date() + timedelta(days=1), next_log.change_time.date()) - else: - elapsed_time = next_log.change_time - current_log.change_time - self.statistic[current_log.change_time.date()] += elapsed_time.total_seconds() - - def post_engineer_logic(self, last_log): - """ - Функция обрабатывает случай, когда нам изветсно что инженер работал и после диапазона - """ - self.fill_daterange(last_log.change_time.date() + timedelta(days=1), self.end_date + timedelta(days=1)) - if last_log.change_time.date() == timezone.now().date(): - self.statistic[last_log.change_time.date()] += ( - get_timedelta(None, timezone.now().time()) - get_timedelta(last_log) - ).total_seconds() - else: - self.statistic[last_log.change_time.date()] += ( - timedelta(days=1) - get_timedelta(last_log)).total_seconds() - if self.end_date == timezone.now().date(): - self.statistic[self.end_date] = get_timedelta(None, timezone.now().time()).total_seconds() - - def prev_engineer_logic(self, first_log): - """ - Функция обрабатывает случай, когда нам изветсно что инженер начал работу до диапазона - """ - self.fill_daterange(max(User.objects.get(email=self.email).date_joined.date(), self.start_date), - first_log.change_time.date()) - self.statistic[first_log.change_time.date()] += get_timedelta(first_log).total_seconds() - - def fill_daterange(self, first: date, last: date, val: int = 24 * 3600) -> dict: - """ - Функция заполняет диапазон дат значением val (по умолчанию val = кол-во секунд в 1 дне). - - :param first: Начальная дата интервала - :param last: Последняя дата интервала - :param val: Количество секунд в одном дне - """ - for day in daterange(first, last): - self.statistic[day] = val - - def clear_statistic(self) -> dict: - """ - Функция осуществляет обновление всех дней. - """ - self.statistic.clear() - self.fill_daterange(self.start_date, self.end_date + timedelta(days=1), 0) - - class DatabaseHandler(logging.Handler): """ Класс записи изменений ролей в базу данных. diff --git a/main/models.py b/main/models.py index 2d1b4cb..c934ab1 100644 --- a/main/models.py +++ b/main/models.py @@ -37,7 +37,6 @@ class UserProfile(models.Model): :return: Роль пользователя в Zendesk """ - id = self.custom_role_id for role, r_id in ZENDESK_ROLES.items(): if r_id == self.custom_role_id: return role diff --git a/main/statistic_data.py b/main/statistic_data.py index fa1ab24..f569c68 100644 --- a/main/statistic_data.py +++ b/main/statistic_data.py @@ -1,7 +1,10 @@ +""" +Обработка статистики. +""" from datetime import date, datetime, timedelta from typing import Optional -from django.contrib.auth.models import User +from django.contrib.auth import get_user_model from django.utils import timezone from access_controller.settings import ONE_DAY, ZENDESK_ROLES as ROLES @@ -50,19 +53,19 @@ class StatisticData: else: self.statistic = stat - def get_statistic(self) -> dict: + def get_statistic(self) -> Optional[dict]: """ Функция возвращает статистику работы пользователя. - :return: Словарь statistic с применением формата отображения и интервала работы(если они есть). None, если были ошибки при создании. + :return: Словарь statistic с применением формата отображения и интервала работы(если они есть). + None, если были ошибки при создании. """ if self.is_valid_statistic(): stat = self.statistic stat = self._use_display(stat) stat = self._use_interval(stat) return stat - else: - return None + return None def is_valid_statistic(self) -> bool: """ @@ -104,8 +107,7 @@ class StatisticData: """ if self.is_valid_data(): return self.data - else: - return None + return None def is_valid_data(self) -> bool: """ @@ -170,7 +172,8 @@ class StatisticData: """ Функция возвращает логи в диапазоне дат start_date - end_date для пользователя с указанным email. - :return: Данные о смене статусов пользователя. Если пользователь не найден или интервал времени некорректен - ошибку. + :return: Данные о смене статусов пользователя. Если пользователь не найден или + интервал времени некорректен - ошибку. """ if not self.check_time(): self.errors += ['Конец диапазона должен быть позже начала диапазона и раньше текущего времени'] @@ -178,12 +181,12 @@ class StatisticData: try: self.data = RoleChangeLogs.objects.filter( change_time__range=[self.start_date, self.end_date + timedelta(days=1)], - user=User.objects.get(email=self.email), + user=get_user_model().objects.get(email=self.email), ).order_by('change_time') - except User.DoesNotExist: + except get_user_model().DoesNotExist: self.errors += ['Пользователь не найден'] - def _init_statistic(self) -> dict: + def _init_statistic(self) -> None: """ Функция заполняет словарь, в котором ключ - дата, значение - кол-во проработанных в этот день секунд. @@ -192,18 +195,18 @@ class StatisticData: self.clear_statistic() if not self.get_data(): self.warnings += ['Не обнаружены изменения роли в данном промежутке'] - return None - first_log, last_log = self.data[0], self.data[len(self.data) - 1] + else: + first_log, last_log = self.data[0], self.data[len(self.data) - 1] - if first_log.old_role == ROLES['engineer']: - self.prev_engineer_logic(first_log) + if first_log.old_role == ROLES['engineer']: + self.prev_engineer_logic(first_log) - if last_log.new_role == ROLES['engineer']: - self.post_engineer_logic(last_log) + if last_log.new_role == ROLES['engineer']: + self.post_engineer_logic(last_log) - for log_index in range(len(self.data) - 1): - if self.data[log_index].new_role == ROLES['engineer']: - self.engineer_logic(log_index) + for log_index in range(len(self.data) - 1): + if self.data[log_index].new_role == ROLES['engineer']: + self.engineer_logic(log_index) def engineer_logic(self, log_index): """ @@ -238,7 +241,7 @@ class StatisticData: """ Функция обрабатывает случай, когда нам изветсно что инженер начал работу до диапазона """ - self.fill_daterange(max(User.objects.get(email=self.email).date_joined.date(), self.start_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() diff --git a/main/views.py b/main/views.py index 570af70..e8c1317 100644 --- a/main/views.py +++ b/main/views.py @@ -19,7 +19,7 @@ from django.contrib.messages.views import SuccessMessageMixin from django.core.handlers.wsgi import WSGIRequest from django.http import HttpResponseRedirect, HttpResponse from django.shortcuts import render, redirect -from django.urls import reverse_lazy, reverse +from django.urls import reverse_lazy from django.views.generic import FormView from django_registration.views import RegistrationView # Django REST @@ -29,12 +29,12 @@ from rest_framework.response import Response from access_controller.settings import DEFAULT_FROM_EMAIL, ZENDESK_ROLES, ZENDESK_MAX_AGENTS, ZENDESK_GROUPS from main.extra_func import check_user_exist, update_profile, get_user_organization, \ make_engineer, make_light_agent, get_users_list, update_users_in_model, count_users, \ - log, set_session_params_for_work_page, get_tickets_list_for_group -from .statistic_data import StatisticData + set_session_params_for_work_page, get_tickets_list_for_group from main.zendesk_admin import zenpy -from main.requester import TicketListRequester from main.forms import AdminPageUsers, CustomRegistrationForm, CustomAuthenticationForm, StatisticForm from main.serializers import ProfileSerializer, ZendeskUserSerializer +from .statistic_data import StatisticData + from .models import UserProfile @@ -69,7 +69,8 @@ class CustomRegistrationView(RegistrationView): :type template_name: :class:`str` :param success_url: Указание пути к html-странице завершения регистрации :type success_url: :class:`django.utils.functional.lazy..__proxy__` - :param is_allowed: Определение зарегистрирован ли пользователь с введенным email на Zendesk и принадлежит ли он к организации SYSTEM + :param is_allowed: Определение зарегистрирован ли пользователь с введенным email на Zendesk и + принадлежит ли он к организации SYSTEM :type is_allowed: :class:`bool` """ extra_context = setup_context(registration_lit=True) @@ -91,7 +92,7 @@ class CustomRegistrationView(RegistrationView): 3. Создается пользователь class User, а также его профиль. :param form: Email пользователя на Zendesk - :return: user + :return: User """ self.redirect_url = 'done' if check_user_exist(form.data['email']) and get_user_organization(form.data['email']) == 'SYSTEM': @@ -235,12 +236,12 @@ def work_hand_over(request: WSGIRequest) -> HttpResponseRedirect: @login_required() def work_become_engineer(request: WSGIRequest) -> HttpResponseRedirect: """ - Функция позволяет текущему пользователю получить права, а именно сменить в Zendesk роль с "light_agent" на "engineer" + Функция позволяет текущему пользователю получить права, а именно сменить в Zendesk роль с "light_agent" + на "engineer". :param request: данные текущего пользователя (login_required) :return: перезагрузка текущей страницы после выполнения смены роли """ - make_engineer(request.user.userprofile, request.user) return set_session_params_for_work_page(request) @@ -311,7 +312,7 @@ class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageM self.make_light_agents(users) return super().form_valid(form) - def make_engineers(self, users): + def make_engineers(self, users: list) -> None: """ Функция проходит по списку пользователей, проставляя статус "engineer". From f803d81caf462221b704afa37a56c214a7eb4d06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D1=82=D0=B5=D0=BF=D0=B0=D0=BD=D0=B5=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=20=D0=9E=D0=BB=D1=8C=D0=B3=D0=B0?= Date: Mon, 10 May 2021 20:49:55 +0300 Subject: [PATCH 36/45] Pylint for extra_func.py --- README.rst | 2 +- main/extra_func.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 7a4e6ec..846db24 100644 --- a/README.rst +++ b/README.rst @@ -170,7 +170,7 @@ Quickstart Для проверки pylint используем: ------------------------------- -pylint ../access_controller +pylint ../access_controller_new Для приведения файлов к стандарту PEP8 используем: -------------------------------------------------- diff --git a/main/extra_func.py b/main/extra_func.py index f13b013..4760bb4 100644 --- a/main/extra_func.py +++ b/main/extra_func.py @@ -2,11 +2,13 @@ Вспомогательные функции со списками пользователей, статистикой и т.д. """ import logging -from datetime import timedelta, datetime, date -from typing import Optional, Union +from datetime import timedelta +from typing import Union from django.contrib.auth import get_user_model from django.core.exceptions import ObjectDoesNotExist +from django.core.handlers.wsgi import WSGIRequest +from django.http import HttpResponseRedirect, HttpResponsePermanentRedirect from django.shortcuts import redirect from django.utils import timezone from zenpy import Zenpy From a6ceae824013b192ee1faa942b58f616ebc70153 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D1=82=D0=B5=D0=BF=D0=B0=D0=BD=D0=B5=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=20=D0=9E=D0=BB=D1=8C=D0=B3=D0=B0?= Date: Wed, 12 May 2021 20:34:52 +0300 Subject: [PATCH 37/45] Pylint 10 --- .pylintrc | 2 +- README.rst | 2 +- access_controller/auth.py | 12 ++++++++ main/requester.py | 21 +++++++++++-- main/tests.py | 65 +++++++++++++++++++++++++++------------ 5 files changed, 77 insertions(+), 25 deletions(-) diff --git a/.pylintrc b/.pylintrc index 07ab0f1..b736d99 100644 --- a/.pylintrc +++ b/.pylintrc @@ -145,7 +145,7 @@ disable=print-statement, exception-escape, comprehension-escape, E5110, - C0415, + C0415 # Enable the message, report, category or checker with the given id(s). You can diff --git a/README.rst b/README.rst index 846db24..a10945a 100644 --- a/README.rst +++ b/README.rst @@ -170,7 +170,7 @@ Quickstart Для проверки pylint используем: ------------------------------- -pylint ../access_controller_new +pylint ../access_controller_new (папка проекта) Для приведения файлов к стандарту PEP8 используем: -------------------------------------------------- diff --git a/access_controller/auth.py b/access_controller/auth.py index 00b43a2..7cfd71b 100644 --- a/access_controller/auth.py +++ b/access_controller/auth.py @@ -1,9 +1,18 @@ +""" +Авторизация пользователя. +""" from django.contrib.auth.backends import ModelBackend from django.contrib.auth import get_user_model class EmailAuthBackend(ModelBackend): + """ + Класс авторизации пользователя по email. + """ def authenticate(self, request, username=None, password=None, **kwargs): + """ + Функция получения пользователя (модель User) по email. + """ try: user = get_user_model().objects.get(email=username) if user.check_password(password): @@ -13,6 +22,9 @@ class EmailAuthBackend(ModelBackend): return None def get_user(self, user_id): + """ + Функция получения пользователя по id. + """ try: return get_user_model().objects.get(pk=user_id) except get_user_model().DoesNotExist: diff --git a/main/requester.py b/main/requester.py index 468abee..d0c57ed 100644 --- a/main/requester.py +++ b/main/requester.py @@ -1,3 +1,6 @@ +""" +Обработка тикетов. +""" import requests from zenpy import TicketApi from zenpy.lib.api_objects import Ticket @@ -6,6 +9,9 @@ from main.zendesk_admin import zenpy class TicketListRequester: + """ + Класс обработки тикетов. + """ def __init__(self): self.email = zenpy.credentials['email'] if zenpy.credentials.get('token'): @@ -15,11 +21,17 @@ 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): + def get_tickets_list_for_user(self, zendesk_user: zenpy) -> str: + """ + Функция получения списка тикетов пользователя Zendesk. + """ url = self.prefix + f'users/{zendesk_user.id}/tickets/assigned' return self._get_tickets(url) - def get_tickets_list_for_group(self, group): + def get_tickets_list_for_group(self, group: zenpy) -> list(): + """ + Функция получения списка тикетов группы пользователей Zendesk. + """ url = self.prefix + '/tickets' all_tickets = self._get_tickets(url) tickets = list() @@ -28,7 +40,10 @@ class TicketListRequester: tickets.append(ticket) return tickets - def _get_tickets(self, url): + def _get_tickets(self, url: str) -> list(): + """ + Функция получения полного списка тикетов по url. + """ response = requests.get(url, auth=(self.email, self.token_or_password)) tickets = [] if response.status_code != 200: diff --git a/main/tests.py b/main/tests.py index 0cb93a0..f9fed9a 100644 --- a/main/tests.py +++ b/main/tests.py @@ -98,7 +98,7 @@ class RegistrationTestCase(TestCase): """ with self.settings(EMAIL_BACKEND=self.email_backend): self.client.post(reverse('registration'), data={'email': self.any_zendesk_user_email}) - user = User.objects.get(email=self.any_zendesk_user_email) + user = get_user_model().objects.get(email=self.any_zendesk_user_email) zendesk_user = zenpy.get_user(self.any_zendesk_user_email) self.assertEqual(user.userprofile.name, zendesk_user.name) @@ -108,71 +108,96 @@ class RegistrationTestCase(TestCase): """ with self.settings(EMAIL_BACKEND=self.email_backend): self.client.post(reverse('registration'), data={'email': self.zendesk_admin_email}) - user = User.objects.get(email=self.zendesk_admin_email) + user = get_user_model().objects.get(email=self.zendesk_admin_email) self.assertEqual(user.userprofile.role, 'admin') self.assertTrue(user.has_perm('main.has_control_access')) class MakeEngineerTestCase(TestCase): + """ + Класс тестов для проверки функции назначения роли engineer. + """ fixtures = ['fixtures/test_make_engineer.json'] def setUp(self): + """ + Предустановленные значения для проведения тестов. + """ self.light_agent = '123@test.ru' self.admin = 'admin@gmail.com' self.engineer = 'customer@example.com' self.client = Client() - self.client.force_login(User.objects.get(email=self.light_agent)) + self.client.force_login(get_user_model().objects.get(email=self.light_agent)) self.admin_client = Client() - self.admin_client.force_login(User.objects.get(email=self.admin)) + self.admin_client.force_login(get_user_model().objects.get(email=self.admin)) @patch('main.extra_func.zenpy') - def test_redirect(self, ZenpyMock): - user = User.objects.get(email=self.light_agent) + def test_redirect(self): + """ + Функция проверки переадресации пользователя на рабочую страницу после назначения роли engineer. + """ + user = get_user_model().objects.get(email=self.light_agent) resp = self.client.post(reverse_lazy('work_become_engineer')) self.assertRedirects(resp, reverse('work', args=[user.id])) self.assertEqual(resp.status_code, 302) @patch('main.extra_func.zenpy') - def test_light_agent_make_engineer(self, ZenpyMock): + def test_light_agent_make_engineer(self, zenpy_mock): + """ + Функция проверки назначения light_agent на роль engineer. + """ self.client.post(reverse_lazy('work_become_engineer')) - self.assertEqual(ZenpyMock.update_user.call_args[0][0].custom_role_id, sets.ZENDESK_ROLES['engineer']) + self.assertEqual(zenpy_mock.update_user.call_args[0][0].custom_role_id, sets.ZENDESK_ROLES['engineer']) @patch('main.extra_func.zenpy') - def test_admin_make_engineer(self, ZenpyMock): + def test_admin_make_engineer(self, zenpy_mock): + """ + Функция проверки назначения admin на роль engineer. + """ self.admin_client.post(reverse_lazy('work_become_engineer')) - self.assertEqual(ZenpyMock.update_user.call_args[0][0].custom_role_id, sets.ZENDESK_ROLES['engineer']) + self.assertEqual(zenpy_mock.update_user.call_args[0][0].custom_role_id, sets.ZENDESK_ROLES['engineer']) @patch('main.extra_func.zenpy') - def test_engineer_make_engineer(self, ZenpyMock): + def test_engineer_make_engineer(self, zenpy_mock): + """ + Функция проверки назначения engineer на роль engineer. + """ client = Client() - client.force_login(User.objects.get(email=self.engineer)) + client.force_login(get_user_model().objects.get(email=self.engineer)) client.post(reverse_lazy('work_become_engineer')) - self.assertEqual(ZenpyMock.update_user.call_args[0][0].custom_role_id, sets.ZENDESK_ROLES['engineer']) + self.assertEqual(zenpy_mock.update_user.call_args[0][0].custom_role_id, sets.ZENDESK_ROLES['engineer']) @patch('main.extra_func.zenpy') - def test_control_page_make_one(self, ZenpyMock): + def test_control_page_make_one(self, zenpy_mock): + """ + Функция проверки назначения администратором на роль engineer одного пользователя. + """ self.admin_client.post( reverse_lazy('control'), - data={'users': [User.objects.get(email=self.light_agent).userprofile.id], 'engineer': 'engineer'} + data={'users': [get_user_model().objects.get(email=self.light_agent).userprofile.id], + 'engineer': 'engineer'} ) - call_list = ZenpyMock.update_user.call_args_list + call_list = zenpy_mock.update_user.call_args_list mock_object = call_list[0][0][0] self.assertEqual(len(call_list), 1) self.assertEqual(mock_object.custom_role_id, sets.ZENDESK_ROLES['engineer']) @patch('main.extra_func.zenpy') - def test_control_page_make_many(self, ZenpyMock): + def test_control_page_make_many(self, zenpy_mock): + """ + Функция проверки назначения администратором на роль engineer нескольких пользователей. + """ self.admin_client.post( reverse_lazy('control'), data={ 'users': [ - User.objects.get(email=self.light_agent).userprofile.id, - User.objects.get(email=self.engineer).userprofile.id, + get_user_model().objects.get(email=self.light_agent).userprofile.id, + get_user_model().objects.get(email=self.engineer).userprofile.id, ], 'engineer': 'engineer' } ) - call_list = ZenpyMock.update_user.call_args_list + call_list = zenpy_mock.update_user.call_args_list mock_objects = list(call_list) self.assertEqual(len(call_list), 2) for obj in mock_objects: From f893bc15fe06cfbc65fe8f86863555d96ac3ef75 Mon Sep 17 00:00:00 2001 From: Andrew Smirnov Date: Thu, 13 May 2021 20:38:25 +0300 Subject: [PATCH 38/45] Fix pylint exec cmd --- .pylintrc | 3 +-- README.md | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.pylintrc b/.pylintrc index b736d99..2fe14cc 100644 --- a/.pylintrc +++ b/.pylintrc @@ -36,7 +36,7 @@ limit-inference-results=100 # List of plugins (as comma separated values of python module names) to load, # usually to register additional checkers. -load-plugins= pylint_django +load-plugins=pylint_django # Pickle collected data for later comparisons. persistent=yes @@ -144,7 +144,6 @@ disable=print-statement, deprecated-sys-function, exception-escape, comprehension-escape, - E5110, C0415 diff --git a/README.md b/README.md index 8885598..c3b9666 100644 --- a/README.md +++ b/README.md @@ -149,7 +149,7 @@ docker run -d -p 8000:8000 \ Пример полной конфигурации можно найти в [.env.example](.env.example). Почту и токен админа ZenDesk взять у руководителя (если вы не админ). ## Для проверки pylint используем: -pylint ../access_controller (каталог, где лежит проект) +pylint --django-settings-module=access_controller.access_controller.settings ../access_controller (каталог, где лежит проект) ## Для приведения файлов к стандарту PEP8 используем: autopep8 --in-place filename From 561b7e3a446b4d5391a9283df9d6263c38ee89aa Mon Sep 17 00:00:00 2001 From: Andrew Smirnov Date: Thu, 13 May 2021 20:46:10 +0300 Subject: [PATCH 39/45] Mode doc and style requirements in dev file --- requirements/common.txt | 13 ------------- requirements/dev.txt | 12 ++++++++++++ 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/requirements/common.txt b/requirements/common.txt index 0ba9415..c8a8d08 100644 --- a/requirements/common.txt +++ b/requirements/common.txt @@ -7,18 +7,5 @@ zenpy~=2.0.24 django_registration==3.1.1 djangorestframework==3.12.2 -# Documentation -Sphinx==3.4.3 -sphinx-rtd-theme==0.5.1 -sphinx-autodoc-typehints==1.11.1 -pyenchant==3.2.0 -sphinxcontrib-spelling==7.1.0 -m2r == 0.2.1 - -# Code style -pylint == 2.8.2 -pylint-django == 2.4.4 -autopep8 == 1.5.6 - # Misc python-dotenv==0.17.1 diff --git a/requirements/dev.txt b/requirements/dev.txt index 73c27d0..0ce1a9e 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,3 +1,15 @@ # Development specific dependencies -r common.txt +# Documentation +Sphinx==3.4.3 +sphinx-rtd-theme==0.5.1 +sphinx-autodoc-typehints==1.11.1 +pyenchant==3.2.0 +sphinxcontrib-spelling==7.1.0 +m2r == 0.2.1 + +# Code style +pylint == 2.8.2 +pylint-django == 2.4.4 +autopep8 == 1.5.6 From 354e3f7373d5383b746d7174f958215ad45d7ef1 Mon Sep 17 00:00:00 2001 From: Andrew Smirnov Date: Thu, 13 May 2021 20:55:10 +0300 Subject: [PATCH 40/45] Fix tests.py --- main/tests.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/main/tests.py b/main/tests.py index 6d72f2e..d7001af 100644 --- a/main/tests.py +++ b/main/tests.py @@ -115,7 +115,7 @@ class MakeEngineerTestCase(TestCase): self.admin_client.force_login(get_user_model().objects.get(email=self.admin)) @patch('main.extra_func.zenpy') - def test_redirect(self): + def test_redirect(self, zenpy_mock): """ Функция проверки переадресации пользователя на рабочую страницу после назначения роли engineer. """ @@ -123,6 +123,7 @@ class MakeEngineerTestCase(TestCase): resp = self.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): @@ -194,7 +195,7 @@ class PasswordResetTestCase(TestCase): self.user = '123@test.ru' self.email_backend = 'django.core.mail.backends.locmem.EmailBackend' self.client = Client() - self.client.force_login(User.objects.get(email=self.user)) + self.client.force_login(get_user_model().objects.get(email=self.user)) def test_redirect(self): with self.settings(EMAIL_BACKEND=self.email_backend): @@ -236,14 +237,14 @@ class PasswordChangeTestCase(TestCase): def setUp(self): self.user = '123@test.ru' self.client = Client() - self.client.force_login(User.objects.get(email=self.user)) + self.client.force_login(get_user_model().objects.get(email=self.user)) self.set_password() def set_password(self): - user: User = User.objects.get(email=self.user) + user: get_user_model() = get_user_model().objects.get(email=self.user) user.set_password('ImpossiblyHardPassword') user.save() - self.client.force_login(User.objects.get(email=self.user)) + self.client.force_login(get_user_model().objects.get(email=self.user)) def test_change_successful(self): self.client.post( @@ -254,7 +255,7 @@ class PasswordChangeTestCase(TestCase): 'new_password2': 'EasyPassword', } ) - user = User.objects.get(email=self.user) + user = get_user_model().objects.get(email=self.user) self.assertTrue(user.check_password('EasyPassword')) def test_invalid_old_password(self): @@ -331,9 +332,9 @@ class GetTicketsTestCase(TestCase): self.light_agent = '123@test.ru' self.engineer = 'customer@example.com' self.client = Client() - self.client.force_login(User.objects.get(email=self.engineer)) + self.client.force_login(get_user_model().objects.get(email=self.engineer)) self.light_agent_client = Client() - self.light_agent_client.force_login(User.objects.get(email=self.light_agent)) + self.light_agent_client.force_login(get_user_model().objects.get(email=self.light_agent)) @patch('main.views.zenpy.get_user') @patch('main.extra_func.zenpy') @@ -342,7 +343,7 @@ class GetTicketsTestCase(TestCase): Функция проверки переадресации пользователя на рабочую страницу. """ GetUserMock.return_value = Mock() - user = User.objects.get(email=self.engineer) + user = get_user_model().objects.get(email=self.engineer) resp = self.client.post(reverse('work_get_tickets')) self.assertRedirects(resp, reverse('work', args=[user.id])) self.assertEqual(resp.status_code, 302) @@ -421,9 +422,9 @@ class ProfileTestCase(TestCase): self.zendesk_agent_email = 'krav-88@mail.ru' self.zendesk_admin_email = 'idar.sokurov.05@mail.ru' self.client = Client() - self.client.force_login(User.objects.get(email=self.zendesk_agent_email)) + self.client.force_login(get_user_model().objects.get(email=self.zendesk_agent_email)) self.admin_client = Client() - self.admin_client.force_login(User.objects.get(email=self.zendesk_admin_email)) + self.admin_client.force_login(get_user_model().objects.get(email=self.zendesk_admin_email)) def test_correct_username(self): """ From 0fd3c0fa1bca193b3e0ef9e46042ffe6fe9f01ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D1=82=D0=B5=D0=BF=D0=B0=D0=BD=D0=B5=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=20=D0=9E=D0=BB=D1=8C=D0=B3=D0=B0?= Date: Sat, 15 May 2021 14:52:08 +0300 Subject: [PATCH 41/45] Resolve C0415 and E5110 problems --- .pylintrc | 4 ++-- README.rst | 2 +- main/tests.py | 6 ++++++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.pylintrc b/.pylintrc index 2fe14cc..79dc22f 100644 --- a/.pylintrc +++ b/.pylintrc @@ -15,7 +15,7 @@ extension-pkg-whitelist= fail-under=10.0 # Files or directories to be skipped. They should be base names, not paths. -ignore=CVS +ignore=CVS, manage.py # Files or directories matching the regex patterns are skipped. The regex # matches against base names, not paths. @@ -144,7 +144,7 @@ disable=print-statement, deprecated-sys-function, exception-escape, comprehension-escape, - C0415 + # Enable the message, report, category or checker with the given id(s). You can diff --git a/README.rst b/README.rst index a10945a..bfd4aff 100644 --- a/README.rst +++ b/README.rst @@ -170,7 +170,7 @@ Quickstart Для проверки pylint используем: ------------------------------- -pylint ../access_controller_new (папка проекта) +pylint --django-settings-module=access_controller_new.access_controller.settings ../access_controller_new Для приведения файлов к стандарту PEP8 используем: -------------------------------------------------- diff --git a/main/tests.py b/main/tests.py index d7001af..61ff9e3 100644 --- a/main/tests.py +++ b/main/tests.py @@ -189,9 +189,15 @@ class MakeEngineerTestCase(TestCase): class PasswordResetTestCase(TestCase): + """ + Класс тестов сброса пароля. + """ fixtures = ['fixtures/test_make_engineer.json'] def setUp(self): + """ + Предустановленные значения для проведения тестов + """ self.user = '123@test.ru' self.email_backend = 'django.core.mail.backends.locmem.EmailBackend' self.client = Client() From aba5d9d563c9e9745cd8af4fca692298c1d8779c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D1=82=D0=B5=D0=BF=D0=B0=D0=BD=D0=B5=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=20=D0=9E=D0=BB=D1=8C=D0=B3=D0=B0?= Date: Sat, 15 May 2021 15:44:21 +0300 Subject: [PATCH 42/45] Rename parametrs to snack_case. Pylint 10. --- README.rst | 3 + .../registration/password_reset_form.html | 2 +- main/tests.py | 88 ++++++++++++++----- 3 files changed, 68 insertions(+), 25 deletions(-) diff --git a/README.rst b/README.rst index bfd4aff..e3c1a03 100644 --- a/README.rst +++ b/README.rst @@ -172,6 +172,9 @@ Quickstart pylint --django-settings-module=access_controller_new.access_controller.settings ../access_controller_new +Вместо "access_controller_new" необходимо указать папку проекта. + + Для приведения файлов к стандарту PEP8 используем: -------------------------------------------------- diff --git a/main/templates/registration/password_reset_form.html b/main/templates/registration/password_reset_form.html index 39cf045..f2980e4 100644 --- a/main/templates/registration/password_reset_form.html +++ b/main/templates/registration/password_reset_form.html @@ -12,7 +12,7 @@

Введте свой e-mail адрес для восстановления пароля.

{{ form.as_p }} -

+

{% csrf_token %}
{% endblock %} diff --git a/main/tests.py b/main/tests.py index 61ff9e3..9d87037 100644 --- a/main/tests.py +++ b/main/tests.py @@ -196,7 +196,7 @@ class PasswordResetTestCase(TestCase): def setUp(self): """ - Предустановленные значения для проведения тестов + Предустановленные значения для проведения тестов. """ self.user = '123@test.ru' self.email_backend = 'django.core.mail.backends.locmem.EmailBackend' @@ -204,12 +204,18 @@ class PasswordResetTestCase(TestCase): self.client.force_login(get_user_model().objects.get(email=self.user)) def test_redirect(self): + """ + Функция проверки переадресации на страницу уведомления о сбросе пароля на email. + """ with self.settings(EMAIL_BACKEND=self.email_backend): resp = self.client.post(reverse_lazy('password_reset'), data={'email': self.user}) self.assertRedirects(resp, reverse('password_reset_done')) self.assertEqual(resp.status_code, 302) def test_send_email(self): + """ + Функция проверки содержания и отправки письма для установки пароля. + """ with self.settings(EMAIL_BACKEND=self.email_backend): response: HttpResponseRedirect = \ self.client.post(reverse_lazy('password_reset'), data={'email': self.user}) @@ -225,11 +231,17 @@ class PasswordResetTestCase(TestCase): self.assertEqual(mail.outbox[0].body, correct_body) def test_email_invalid(self): + """ + Функция проверки уведомления клиента о некорректности введенного email. + """ with self.settings(EMAIL_BACKEND=self.email_backend) and translation.override('ru'): resp = self.client.post(reverse_lazy('password_reset'), data={'email': 1}) self.assertContains(resp, 'Введите правильный адрес электронной почты.', count=1, status_code=200) def test_user_does_not_exist(self): + """ + Функция корректности отработки неверно введенного email. + """ with self.settings(EMAIL_BACKEND=self.email_backend): resp = self.client.post(reverse_lazy('password_reset'), data={'email': self.user + str(random.random())}) self.assertRedirects(resp, reverse('password_reset_done')) @@ -238,21 +250,33 @@ class PasswordResetTestCase(TestCase): class PasswordChangeTestCase(TestCase): + """ + Класс тестирования смены пароля. + """ fixtures = ['fixtures/test_make_engineer.json'] def setUp(self): + """ + Предустановленные значения для проведения тестов. + """ self.user = '123@test.ru' self.client = Client() self.client.force_login(get_user_model().objects.get(email=self.user)) self.set_password() def set_password(self): + """ + Пароль, сформированный для тестирования. + """ user: get_user_model() = get_user_model().objects.get(email=self.user) user.set_password('ImpossiblyHardPassword') user.save() self.client.force_login(get_user_model().objects.get(email=self.user)) def test_change_successful(self): + """ + Функция тестирования успешного изменения пароля. + """ self.client.post( reverse_lazy('password_change'), data={ @@ -265,6 +289,9 @@ class PasswordChangeTestCase(TestCase): self.assertTrue(user.check_password('EasyPassword')) def test_invalid_old_password(self): + """ + Функция тестирования отработки неверно введенного старого пароля при смене. + """ with translation.override('ru'): resp = self.client.post( reverse_lazy('password_change'), @@ -277,6 +304,9 @@ class PasswordChangeTestCase(TestCase): self.assertContains(resp, 'Ваш старый пароль введен неправильно', count=1, status_code=200) def test_different_new_passwords(self): + """ + Функция тестирования случая с вводом двух разных новых паролей. + """ with translation.override('ru'): resp = self.client.post( reverse_lazy('password_change'), @@ -289,6 +319,9 @@ class PasswordChangeTestCase(TestCase): self.assertContains(resp, 'Введенные пароли не совпадают', count=1, status_code=200) def test_invalid_new_password1(self): + """ + Функция тестирования случая с неправильно подобранным новым паролем (слишком короткий). + """ with translation.override('ru'): resp = self.client.post( reverse_lazy('password_change'), @@ -301,6 +334,9 @@ class PasswordChangeTestCase(TestCase): self.assertContains(resp, 'Введённый пароль слишком короткий', count=1, status_code=200) def test_invalid_new_password2(self): + """ + Функция тестирования случая с неправильно подобранным новым паролем (употребляются только цифры). + """ with translation.override('ru'): resp = self.client.post( reverse_lazy('password_change'), @@ -313,6 +349,9 @@ class PasswordChangeTestCase(TestCase): self.assertContains(resp, 'Введённый пароль состоит только из цифр', count=1, status_code=200) def test_invalid_new_password3(self): + """ + Функция тестирования случая с неправильно подобранным новым паролем (совпадает с именем пользователя). + """ with translation.override('ru'): resp = self.client.post( reverse_lazy('password_change'), @@ -344,74 +383,75 @@ class GetTicketsTestCase(TestCase): @patch('main.views.zenpy.get_user') @patch('main.extra_func.zenpy') - def test_redirect(self, ZenpyMock, GetUserMock): + def test_redirect(self, zenpy_mock, get_user_mock): """ Функция проверки переадресации пользователя на рабочую страницу. """ - GetUserMock.return_value = Mock() + get_user_mock.return_value = Mock() user = get_user_model().objects.get(email=self.engineer) resp = self.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, TicketsMock, ZenpyViewsMock): + def test_take_one_ticket(self, tickets_mock, zenpy_views_mock): """ Функция проверки назначения одного тикета на engineer. """ - TicketsMock.return_value = [Mock()] - ZenpyViewsMock.get_user.return_value = Mock(role='agent', custom_role_id=sets.ZENDESK_ROLES['engineer']) + tickets_mock.return_value = [Mock()] + zenpy_views_mock.get_user.return_value = Mock(role='agent', custom_role_id=sets.ZENDESK_ROLES['engineer']) self.client.post(reverse('work_get_tickets'), data={'count_tickets': 1}) - tickets = ZenpyViewsMock.update_tickets.call_args - self.assertEqual(tickets[0][0][0].assignee, ZenpyViewsMock.get_user.return_value) + tickets = zenpy_views_mock.update_tickets.call_args + self.assertEqual(tickets[0][0][0].assignee, zenpy_views_mock.get_user.return_value) @patch('main.views.get_tickets_list_for_group') @patch('main.views.zenpy') - def test_take_many_tickets(self, ZenpyMock, TicketsMock): + def test_take_many_tickets(self, zenpy_mock, tickets_mock): """ Функция проверки назначения нескольких тикетов на engineer. """ - TicketsMock.return_value = [Mock()] * 3 - ZenpyMock.get_user.return_value = Mock(role='agent', custom_role_id=sets.ZENDESK_ROLES['engineer']) + tickets_mock.return_value = [Mock()] * 3 + zenpy_mock.get_user.return_value = Mock(role='agent', custom_role_id=sets.ZENDESK_ROLES['engineer']) self.client.post(reverse('work_get_tickets'), data={'count_tickets': 3}) - tickets = ZenpyMock.update_tickets.call_args + tickets = zenpy_mock.update_tickets.call_args for ticket in tickets[0][0]: - self.assertEqual(ticket.assignee, ZenpyMock.get_user.return_value) + self.assertEqual(ticket.assignee, zenpy_mock.get_user.return_value) @patch('main.views.zenpy.get_user') @patch('main.views.zenpy') - def test_light_agent_take_ticket(self, ZenpyMock, GetUserMock): + def test_light_agent_take_ticket(self, zenpy_mock, get_user_mock): """ Функция проверки попытки назначения тикета на light_agent. """ - GetUserMock.return_value = Mock(role='agent', custom_role_id=sets.ZENDESK_ROLES['light_agent']) + get_user_mock.return_value = Mock(role='agent', custom_role_id=sets.ZENDESK_ROLES['light_agent']) self.light_agent_client.post(reverse('work_get_tickets'), data={'count_tickets': 3}) - tickets = ZenpyMock.update_tickets.call_args + tickets = zenpy_mock.update_tickets.call_args self.assertIsNone(tickets) @patch('main.views.zenpy') @patch('main.views.get_tickets_list_for_group') - def test_take_zero_tickets(self, TicketsMock, ZenpyMock): + def test_take_zero_tickets(self, tickets_mock, zenpy_mock): """ Функция проверки попытки назначения нуля тикета на engineer. """ - TicketsMock.return_value = [Mock()] * 3 - ZenpyMock.get_user.return_value = Mock(role='agent', custom_role_id=sets.ZENDESK_ROLES['engineer']) + tickets_mock.return_value = [Mock()] * 3 + zenpy_mock.get_user.return_value = Mock(role='agent', custom_role_id=sets.ZENDESK_ROLES['engineer']) self.client.post(reverse('work_get_tickets'), data={'count_tickets': 0}) - tickets = ZenpyMock.update_tickets.call_args[0][0] + tickets = zenpy_mock.update_tickets.call_args[0][0] self.assertListEqual(tickets, []) @patch('main.views.get_tickets_list_for_group') @patch('main.views.zenpy') - def test_take_invalid_count_tickets(self, ZenpyMock, TicketsMock, ): + def test_take_invalid_count_tickets(self, zenpy_mock, tickets_mock): """ Функция проверки попытки назначения нуля тикетов на engineer. """ - TicketsMock.return_value = [Mock()] * 3 - ZenpyMock.get_user.return_value = Mock(role='agent', custom_role_id=sets.ZENDESK_ROLES['engineer']) + tickets_mock.return_value = [Mock()] * 3 + zenpy_mock.get_user.return_value = Mock(role='agent', custom_role_id=sets.ZENDESK_ROLES['engineer']) self.client.post(reverse('work_get_tickets'), data={'count_tickets': 'asd'}) - tickets = ZenpyMock.update_tickets.call_args + tickets = zenpy_mock.update_tickets.call_args self.assertIsNone(tickets) From 0b144dd8810246be9bf8fd319a36941d2cb74bda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D1=82=D0=B5=D0=BF=D0=B0=D0=BD=D0=B5=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=20=D0=9E=D0=BB=D1=8C=D0=B3=D0=B0?= Date: Sun, 16 May 2021 11:01:10 +0300 Subject: [PATCH 43/45] Move django-settings for pylint to pylintrc --- .pylintrc | 1 + README.rst | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.pylintrc b/.pylintrc index 79dc22f..0f1221a 100644 --- a/.pylintrc +++ b/.pylintrc @@ -37,6 +37,7 @@ limit-inference-results=100 # List of plugins (as comma separated values of python module names) to load, # usually to register additional checkers. load-plugins=pylint_django +django-settings-module=access_controller_new.access_controller.settings # Pickle collected data for later comparisons. persistent=yes diff --git a/README.rst b/README.rst index e3c1a03..f1b08b5 100644 --- a/README.rst +++ b/README.rst @@ -170,7 +170,7 @@ Quickstart Для проверки pylint используем: ------------------------------- -pylint --django-settings-module=access_controller_new.access_controller.settings ../access_controller_new +pylint ../access_controller_new Вместо "access_controller_new" необходимо указать папку проекта. From 8039e286ef318599db0f1cc9be6b5c6f2c809377 Mon Sep 17 00:00:00 2001 From: Andrew Smirnov Date: Thu, 20 May 2021 20:03:13 +0300 Subject: [PATCH 44/45] Fix tests.py, add migration, fix pylint --- .../0018_alter_unassignedticket_ticket_id.py | 18 +++++++++++++ main/tests.py | 27 ++++++++++++------- 2 files changed, 35 insertions(+), 10 deletions(-) create mode 100644 main/migrations/0018_alter_unassignedticket_ticket_id.py diff --git a/main/migrations/0018_alter_unassignedticket_ticket_id.py b/main/migrations/0018_alter_unassignedticket_ticket_id.py new file mode 100644 index 0000000..7899c3c --- /dev/null +++ b/main/migrations/0018_alter_unassignedticket_ticket_id.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.3 on 2021-05-20 17:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0017_auto_20210408_1943'), + ] + + operations = [ + migrations.AlterField( + model_name='unassignedticket', + name='ticket_id', + field=models.IntegerField(help_text='Номер тикета, для которого сняли ответственного'), + ), + ] diff --git a/main/tests.py b/main/tests.py index 5365782..7946c01 100644 --- a/main/tests.py +++ b/main/tests.py @@ -30,11 +30,11 @@ class UsersBaseTestCase(TestCase): self.admin = 'admin@gmail.com' self.engineer = 'customer@example.com' self.agent_client = Client() - self.agent_client.force_login(User.objects.get(email=self.light_agent)) + self.agent_client.force_login(get_user_model().objects.get(email=self.light_agent)) self.admin_client = Client() - self.admin_client.force_login(User.objects.get(email=self.admin)) + self.admin_client.force_login(get_user_model().objects.get(email=self.admin)) self.engineer_client = Client() - self.engineer_client.force_login(User.objects.get(email=self.engineer)) + self.engineer_client.force_login(get_user_model().objects.get(email=self.engineer)) class RegistrationTestCase(TestCase): @@ -252,7 +252,10 @@ class MakeLightAgentTestCase(UsersBaseTestCase): def test_control_page_make_light_agent_one(self, zenpy_mock, _user_tickets_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] @@ -330,7 +333,12 @@ class PasswordResetTestCase(UsersBaseTestCase): Функция корректности отработки неверно введенного 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) @@ -503,11 +511,11 @@ class GetTicketsTestCase(UsersBaseTestCase): @patch('main.views.zenpy') @patch('main.views.get_tickets_list_for_group') - def test_take_zero_tickets(self, TicketsMock, zenpy_mock): + def test_take_zero_tickets(self, tickets_mock, zenpy_mock): """ Функция проверки попытки назначения нуля тикета на engineer. """ - TicketsMock.return_value = [Mock()] * 3 + tickets_mock.return_value = [Mock()] * 3 zenpy_mock.get_user.return_value = Mock(role='agent', custom_role_id=sets.ZENDESK_ROLES['engineer']) self.engineer_client.post(reverse('work_get_tickets'), data={'count_tickets': 0}) tickets = zenpy_mock.update_tickets.call_args[0][0] @@ -596,9 +604,8 @@ class LoggingTestCase(UsersBaseTestCase): @staticmethod def get_file_output(): - file = open('logs/logs.csv', 'r') - file_output = file.readlines()[-1] - file.close() + with open('logs/logs.csv', 'r') as file: + file_output = file.readlines()[-1] return file_output def test_engineer_with_admin(self): From 724d246b6befdce8d438df1f0fd659408d6f2c17 Mon Sep 17 00:00:00 2001 From: Iurii Tatishchev Date: Tue, 25 May 2021 09:44:02 -0700 Subject: [PATCH 45/45] Remove venv from git --- .../site-packages/enchant/tokenize/ru.py | 185 ------------------ 1 file changed, 185 deletions(-) delete mode 100644 venv/lib/python3.6/site-packages/enchant/tokenize/ru.py diff --git a/venv/lib/python3.6/site-packages/enchant/tokenize/ru.py b/venv/lib/python3.6/site-packages/enchant/tokenize/ru.py deleted file mode 100644 index 7e15379..0000000 --- a/venv/lib/python3.6/site-packages/enchant/tokenize/ru.py +++ /dev/null @@ -1,185 +0,0 @@ -# pyenchant -# -# Copyright (C) 2004-2008, Ryan Kelly -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the -# Free Software Foundation, Inc., 59 Temple Place - Suite 330, -# Boston, MA 02111-1307, USA. -# -# In addition, as a special exception, you are -# given permission to link the code of this program with -# non-LGPL Spelling Provider libraries (eg: a MSFT Office -# spell checker backend) and distribute linked combinations including -# the two. You must obey the GNU Lesser General Public License in all -# respects for all of the code used other than said providers. If you modify -# this file, you may extend this exception to your version of the -# file, but you are not obligated to do so. If you do not wish to -# do so, delete this exception statement from your version. -# -""" - - enchant.tokenize.en: Tokenizer for the English language - - This module implements a PyEnchant text tokenizer for the English - language, based on very simple rules. - -""" - -import unicodedata - -import enchant.tokenize - - -class tokenize(enchant.tokenize.tokenize): # noqa: N801 - """Iterator splitting text into words, reporting position. - - This iterator takes a text string as input, and yields tuples - representing each distinct word found in the text. The tuples - take the form: - - (,) - - Where is the word string found and is the position - of the start of the word within the text. - - The optional argument may be used to specify a - list of additional characters that can form part of a word. - By default, this list contains only the apostrophe ('). Note that - these characters cannot appear at the start or end of a word. - """ - - _DOC_ERRORS = ["pos", "pos"] - - def __init__(self, text, valid_chars=None): - self._valid_chars = valid_chars - self._text = text - self._offset = 0 - # Select proper implementation of self._consume_alpha. - # 'text' isn't necessarily a string (it could be e.g. a mutable array) - # so we can't use isinstance(text, str) to detect unicode. - # Instead we typetest the first character of the text. - # If there's no characters then it doesn't matter what implementation - # we use since it won't be called anyway. - try: - char1 = text[0] - except IndexError: - self._initialize_for_binary() - else: - if isinstance(char1, str): - self._initialize_for_unicode() - else: - self._initialize_for_binary() - - def _initialize_for_binary(self): - self._consume_alpha = self._consume_alpha_b - if self._valid_chars is None: - self._valid_chars = ("'",) - - def _initialize_for_unicode(self): - self._consume_alpha = self._consume_alpha_u - if self._valid_chars is None: - # XXX TODO: this doesn't seem to work correctly with the - # MySpell provider, disabling for now. - # Allow unicode typographic apostrophe - # self._valid_chars = (u"'",u"\u2019") - self._valid_chars = ("'",) - - def _consume_alpha_b(self, text, offset): - """Consume an alphabetic character from the given bytestring. - - Given a bytestring and the current offset, this method returns - the number of characters occupied by the next alphabetic character - in the string. Non-ASCII bytes are interpreted as utf-8 and can - result in multiple characters being consumed. - """ - assert offset < len(text) - if text[offset].isalpha(): - return 1 - elif text[offset] >= "\x80": - return self._consume_alpha_utf8(text, offset) - return 0 - - def _consume_alpha_utf8(self, text, offset): - """Consume a sequence of utf8 bytes forming an alphabetic character.""" - incr = 2 - u = "" - while not u and incr <= 4: - try: - try: - # In the common case this will be a string - u = text[offset : offset + incr].decode("utf8") - except AttributeError: - # Looks like it was e.g. a mutable char array. - try: - s = text[offset : offset + incr].tostring() - except AttributeError: - s = "".join([c for c in text[offset : offset + incr]]) - u = s.decode("utf8") - except UnicodeDecodeError: - incr += 1 - if not u: - return 0 - if u.isalpha(): - return incr - if unicodedata.category(u)[0] == "M": - return incr - return 0 - - def _consume_alpha_u(self, text, offset): - """Consume an alphabetic character from the given unicode string. - - Given a unicode string and the current offset, this method returns - the number of characters occupied by the next alphabetic character - in the string. Trailing combining characters are consumed as a - single letter. - """ - assert offset < len(text) - incr = 0 - if text[offset].isalpha(): - incr = 1 - while offset + incr < len(text): - if unicodedata.category(text[offset + incr])[0] != "M": - break - incr += 1 - return incr - - def next(self): - text = self._text - offset = self._offset - while offset < len(text): - # Find start of next word (must be alpha) - while offset < len(text): - incr = self._consume_alpha(text, offset) - if incr: - break - offset += 1 - cur_pos = offset - # Find end of word using, allowing valid_chars - while offset < len(text): - incr = self._consume_alpha(text, offset) - if not incr: - if text[offset] in self._valid_chars: - incr = 1 - else: - break - offset += incr - # Return if word isn't empty - if cur_pos != offset: - # Make sure word doesn't end with a valid_char - while text[offset - 1] in self._valid_chars: - offset = offset - 1 - self._offset = offset - return (text[cur_pos:offset], cur_pos) - self._offset = offset - raise StopIteration()