diff --git a/docs/Makefile b/docs/Makefile index 461302a..827600b 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -3,14 +3,17 @@ # You can set these variables from the command line, and also # from the environment for the first two. -SPHINXOPTS ?= -SPHINXBUILD ?= sphinx-build +ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source +SPHINXOPTS = +SPHINXBUILD = sphinx-build SOURCEDIR = source BUILDDIR = build + # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + @echo " spelling to check for typos in documentation" .PHONY: help Makefile @@ -19,5 +22,10 @@ help: %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + spelling: - $(SPHINXBUILD) -b spelling -W $(ALLSPHINXOPTS) $(BUILDDIR)/spelling + $(SPHINXBUILD) -b spelling -W $(SOURCEDIR) $(BUILDDIR)/spelling + @echo + @echo "Check finished. Wrong words can be found in " \ + "build/spelling/output.txt." + diff --git a/docs/source/_static/login.png b/docs/source/_static/login.png new file mode 100644 index 0000000..2cf59cf Binary files /dev/null and b/docs/source/_static/login.png differ diff --git a/docs/source/_static/main.png b/docs/source/_static/main.png new file mode 100644 index 0000000..fcfd4f9 Binary files /dev/null and b/docs/source/_static/main.png differ diff --git a/docs/source/_static/main_logined.png b/docs/source/_static/main_logined.png new file mode 100644 index 0000000..5105101 Binary files /dev/null and b/docs/source/_static/main_logined.png differ diff --git a/docs/source/_static/main_logined_agent.png b/docs/source/_static/main_logined_agent.png new file mode 100644 index 0000000..e4f76d1 Binary files /dev/null and b/docs/source/_static/main_logined_agent.png differ diff --git a/docs/source/_static/permission_management.png b/docs/source/_static/permission_management.png new file mode 100644 index 0000000..c1897ed Binary files /dev/null and b/docs/source/_static/permission_management.png differ diff --git a/docs/source/_static/permission_request.png b/docs/source/_static/permission_request.png new file mode 100644 index 0000000..8752e91 Binary files /dev/null and b/docs/source/_static/permission_request.png differ diff --git a/docs/source/_static/profile.png b/docs/source/_static/profile.png new file mode 100644 index 0000000..b84d139 Binary files /dev/null and b/docs/source/_static/profile.png differ diff --git a/docs/source/_static/registration.png b/docs/source/_static/registration.png new file mode 100644 index 0000000..b6e8111 Binary files /dev/null and b/docs/source/_static/registration.png differ diff --git a/docs/source/code.rst b/docs/source/code.rst index 85d207b..7479081 100644 --- a/docs/source/code.rst +++ b/docs/source/code.rst @@ -1,10 +1,9 @@ Документация разработчика ========================= - -****** +******* Models -****** +******* .. automodule:: main.models :members: @@ -26,9 +25,27 @@ Extra Functions :members: +*************** +Serializers +*************** + +.. automodule:: main.serializers + :members: + + +*************** +API functions +*************** + +.. automodule:: main.apiauth + :members: + + ***** Views ***** .. automodule:: main.views :members: + + diff --git a/docs/source/conf.py b/docs/source/conf.py index c9efe47..7e99943 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -14,6 +14,9 @@ import os import sys import importlib import inspect +import enchant +from enchant import checker + sys.path.insert(0, os.path.abspath('../../')) @@ -35,10 +38,7 @@ ManagerDescriptor.__get__ = lambda self, *args, **kwargs: self.manager from django.db.models.query import QuerySet QuerySet.__repr__ = lambda self: self.__class__.__name__ -try: - import enchant # NoQA -except ImportError: - enchant = None + django.setup() @@ -52,8 +52,6 @@ author = 'SHP S101, group 2' release = 'v0.01' -# Django sphinx setup by https://gist.github.com/codingjoe/314bda5a07ff3b41f247 - # -- General configuration --------------------------------------------------- def process_django_models(app, what, name, obj, options, lines): @@ -91,7 +89,6 @@ 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 - print('ok') return lines @@ -119,18 +116,18 @@ def skip_queryset(app, what, name, obj, skip, options): return skip -def setup(app): - # Register the docstring processor with sphinx - app.connect('autodoc-process-docstring', process_django_models) - app.connect('autodoc-skip-member', skip_queryset) - if enchant is not None: - 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) + # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = [ +extensions = { 'sphinx.ext.todo', 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx', @@ -138,12 +135,11 @@ extensions = [ 'sphinx_rtd_theme', 'sphinx.ext.graphviz', 'sphinx.ext.inheritance_diagram', - 'sphinx_autodoc_typehints' + 'sphinx_autodoc_typehints', + 'sphinxcontrib.spelling' -] +} -if enchant is not None: - extensions.append('sphinxcontrib.spelling') # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -188,16 +184,23 @@ intersphinx_mapping = { autodoc_default_flags = ['members'] # spell checking -spelling_lang = 'en_US' -spelling_word_list_filename = 'spelling_wordlist.txt' +spelling_lang = 'ru_RU' +tokenizer_lang = 'ru_RU' +spelling_exclude_patterns=['ignored_*'] spelling_show_suggestions = 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 # -- Options for todo extension ---------------------------------------------- # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = True - set_type_checking_flag = True typehints_fully_qualified = True always_document_param_types = True diff --git a/docs/source/index.rst b/docs/source/index.rst index 83cf46a..96f9c69 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -3,7 +3,7 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. -Welcome to ZenDesk Access Controller's documentation! +Документация контроллера прав доступа ===================================================== .. toctree:: @@ -15,7 +15,6 @@ Welcome to ZenDesk Access Controller's documentation! todo - Indices and tables ================== diff --git a/docs/source/overview.rst b/docs/source/overview.rst index 447e273..fb3c627 100644 --- a/docs/source/overview.rst +++ b/docs/source/overview.rst @@ -2,17 +2,88 @@ Документация пользователя ========================= +****************************** **Управление правами доступа** +****************************** -**ZenDesk Access Controller** - Web-приложение, для выдачи прав пользователям системы по запросу самого пользователя. +**ZenDesk Access Controller** - web-приложение, для выдачи прав пользователям системы по запросу самого пользователя. Например, из 12 человек 3 сейчас работают с правами администратора, по окончании рабочей смены они сдают свои права (освобождают места) и другие пользователи могут запросить эти права в свое пользование. Оставшиеся 9 человек получают права легкого агента - без прав редактирования, а только чтение. -**Интерфейс пользователя:** +***************** +Главная страница +***************** +Меню главной страницы предоставляет Вам выбор: +* **"Войти"** - если Вы уже являетесь зарегистрированным пользователем +* **"Зарегистрироваться"** - при первом входе + +.. image:: _static/main.png + +Внимание! Для регистрации используется email с сайта Zendesk. Регистрация по каждому email +возможна один раз + +**После авторизации пользователь может выбрать из следующих разделов меню:** + +* **"Профиль"** - просмотреть свои данные и запросить права доступа +* **"Запрос прав"** - получение прав для работы с тикетами или **"Управление"** - доступно для администратора и предоставляет возможность группового назначения ролей пользователям + +.. image:: _static/main_logined_agent.png + +************* +Регистрация +************* + +Для регистрации необходимо ввести email, который указан Вами в Zendesk. + +.. image:: _static/registration.png + +На электронную почту придет ссылка, пройдя по которой, Вам необходимо задать пароль. + +*********** +Авторизация +*********** + +Для входа необходимо ввести email и пароль + +.. image:: _static/login.png + +Если Вы не помните пароль необходимо пройти по ссылке "Забыли пароль" и указать email. +На Вашу почту придет ссылка для установки нового пароля. + +******** +Профиль +******** + +Профиль пользователя - это Ваша рабочая страница. + +Здесь Вы можете просмотреть информацию пользователя (Ваши данные с Zendesk) и запросить права доступа для работы с тикетами. + +.. image:: _static/profile.png + +******************** +Запрос прав доступа +******************** + +На странице запроса прав Вам доступна информация о количестве и списке работающих над тикетами сотрудников, +а также возможность сдать и запросить права. + +.. image:: _static/permission_request.png + +****************************************** +Управление правами доступа администратором +****************************************** + +Для администратора существует удобный интерфейс страницы управления, в котором представлены: + +* Количество свободных инженерных мест +* Количество и список инженеров и легких агентов +* Возможность группового назначения прав с использованием чек-боксов + +.. image:: _static/permission_management.png .. |copy| unicode:: 0xA9 .. Школа программистов S101, группа 2. 2021гю diff --git a/docs/source/spelling_wordlist.txt b/docs/source/spelling_wordlist.txt new file mode 100644 index 0000000..f190ae2 --- /dev/null +++ b/docs/source/spelling_wordlist.txt @@ -0,0 +1,86 @@ +тикетами +тикета +тикетов +тикет +web +Indices +and +tables +Models +логирования +User +user +superuser +light +light_agent +admin +agent +bootstrap +form +control +Zendesk +email +Extra +Functions +env +ID +url +None +token +password +engineer +SYSTEM +start_date +end_date +timedelta +log +RoleChangeLogs +time(datetime.time) +stat +statistic +True +False +val +start +end +date +Токен +токеном +аутентифицирован +(datetime.time) +datetime +time +serializer +валидны +html +subdomain +логгирования +логгирование +forms +StatisticForm +Userprofile +login +login_required +required +id +prom +home +PycharmProjects +Access +access +controler +controller +main +views +py +docstring +of +page +API +functions +Serializer +Serializers + + + + diff --git a/main/apiauth.py b/main/apiauth.py index 4e3f761..c24e85f 100644 --- a/main/apiauth.py +++ b/main/apiauth.py @@ -4,7 +4,16 @@ from zenpy import Zenpy from zenpy.lib.api_objects import User as ZenpyUser -def api_auth(): +def api_auth() -> dict: + """ + Функция создания пользователя с использованием Zendesk API. + + Получает из env Zendesk - email, token, password пользователя. + Если данные валидны и пользователь Zendesk с указанным email и токеном или паролем существует, + создается словарь данных пользователя, полученных через API c Zendesk. + + :return: данные пользователя + """ credentials = { 'subdomain': 'ngenix1612197338' } diff --git a/main/extra_func.py b/main/extra_func.py index f5488d6..b87ac27 100644 --- a/main/extra_func.py +++ b/main/extra_func.py @@ -13,16 +13,16 @@ from main.models import UserProfile, RoleChangeLogs, UnassignedTicket, Unassigne class ZendeskAdmin: """ - Класс **ZendeskAdmin** существует, чтобы в каждой фунциии отдельно не проверять аккаунт администратора + Класс **ZendeskAdmin** существует, чтобы в каждой функции отдельно не проверять аккаунт администратора. - :param credentials: Полномочия (первым указывается учетная запись организации в Zendesk) - :type credentials: :class:`dict` - :param email: Email администратора, указанный в env - :type email: :class:`str` - :param token: Токен администратора (формируется в Zendesk, указывается в env) - :type token: :class:`str` - :param password: Пароль администратора, указанный в env - :type password: :class:`str` + :param credentials: Полномочия (первым указывается учетная запись организации в Zendesk) + :type credentials: :class:`dict` + :param email: Email администратора, указанный в env + :type email: :class:`str` + :param token: Токен администратора (формируется в Zendesk, указывается в env) + :type token: :class:`str` + :param password: Пароль администратора, указанный в env + :type password: :class:`str` """ credentials: dict = { @@ -37,7 +37,10 @@ class ZendeskAdmin: def check_user(self, email: str) -> bool: """ - Функция **check_user** осуществляет проверку существования пользователя в Zendesk по email + Функция осуществляет проверку существования пользователя в Zendesk по email. + + :param email: Email пользователя + :return: Является ли зарегистрированным """ return True if self.admin.search(email, type='user') else False @@ -50,35 +53,50 @@ class ZendeskAdmin: def get_user_role(self, email: str) -> str: """ - Функция **get_user_role** возвращает роль пользователя по его email + Функция возвращает роль пользователя по его email. + + :param email: Email пользователя + :return: Роль пользователя """ user = self.admin.users.search(email).values[0] return user.role def get_user_id(self, email: str) -> str: """ - Функция **get_user_id** возвращает id пользователя по его email + Функция возвращает id пользователя по его email + + :param email: Email пользователя + :return: ID пользователя """ user = self.admin.users.search(email).values[0] return user.id def get_user_image(self, email: str) -> str: """ - Функция **get_user_image** возвращает url-ссылку на аватар пользователя по его email + Функция возвращает url-ссылку на аватар пользователя по его email. + + :param email: Email пользователя + :return: Аватар пользователя """ user = self.admin.users.search(email).values[0] return user.photo['content_url'] if user.photo else None def get_user(self, email: str): """ - Функция **get_user** возвращает пользователя (объект) по его email + Функция возвращает пользователя (объект) по его email. - :param email: email пользователя - :return: email пользователя, найденного в БД + :param email: Email пользователя + :return: Объект пользователя, найденного в БД """ return self.admin.users.search(email).values[0] - def get_group(self, name): + def get_group(self, name: str) -> str: + """ + Функция возвращает группы, к которым принадлежит пользователь. + + :param name: Имя пользователя + :return: Группы пользователя (в случае отсутствия None) + """ groups = self.admin.search(name) for group in groups: return group @@ -86,19 +104,22 @@ class ZendeskAdmin: def get_user_org(self, email: str) -> str: """ - Функция **get_user_org** возвращает организацию, к которой относится пользователь по его email + Функция возвращает организацию, к которой относится пользователь по его email. + + :param email: Email пользователя + :return: Организация пользователя """ user = self.admin.users.search(email).values[0] return user.organization.name if user.organization else None def create_admin(self) -> Zenpy: """ - Функция **Create_admin()** создает администратора, проверяя наличие вводимых данных в env. + Функция создает администратора, проверяя наличие вводимых данных в env. - :param credentials: В список полномочий администратора вносятся email, token, password из env - :type credentials: :class:`dict` - :raise: :class:`ValueError`: исключение, вызываемое если email не введен в env - :raise: :class:`APIException`: исключение, вызываемое если пользователя с таким email не существует в Zendesk + :param credentials: В список полномочий администратора вносятся email, token, password из env + :type credentials: :class:`dict` + :raise: :class:`ValueError`: исключение, вызываемое если email не введен в env + :raise: :class:`APIException`: исключение, вызываемое если пользователя с таким email не существует в Zendesk """ if self.email is None: @@ -120,7 +141,11 @@ class ZendeskAdmin: def update_role(user_profile: UserProfile, role: str) -> UserProfile: """ - Функция **update_role** меняет роль пользователя. + Функция меняет роль пользователя. + + :param user_profile: Профиль пользователя + :param role: Новая роль + :return: Пользователь с обновленной ролью """ zendesk = ZendeskAdmin() user = zendesk.get_user(user_profile.user.email) @@ -130,7 +155,10 @@ def update_role(user_profile: UserProfile, role: str) -> UserProfile: def make_engineer(user_profile: UserProfile, who_changes: User) -> UserProfile: """ - Функция **make_engineer** устанавливает пользователю роль инженера. + Функция устанавливает пользователю роль инженера. + + :param user_profile: Профиль пользователя + :return: Вызов функции **update_role** с параметрами: профиль пользователя, роль "engineer" """ RoleChangeLogs.objects.create( user=user_profile.user, @@ -143,7 +171,10 @@ def make_engineer(user_profile: UserProfile, who_changes: User) -> UserProfile: def make_light_agent(user_profile: UserProfile, who_changes: User) -> UserProfile: """ - Функция **make_light_agent** устанавливапет пользователю роль легкого агента. + Функция устанавливает пользователю роль легкого агента. + + :param user_profile: Профиль пользователя + :return: Вызов функции **update_role** с параметрами: профиль пользователя, роль "light_agent" """ tickets = get_tickets_list(user_profile.user.email) for ticket in tickets: @@ -170,7 +201,7 @@ def make_light_agent(user_profile: UserProfile, who_changes: User) -> UserProfil def get_users_list() -> list: """ - Функция **get_users_list** возвращает список пользователей Zendesk, относящихся к организации. + Функция **get_users_list** возвращает список пользователей Zendesk, относящихся к организации SYSTEM. """ zendesk = ZendeskAdmin() @@ -189,7 +220,10 @@ def get_tickets_list(email): def update_profile(user_profile: UserProfile) -> UserProfile: """ - Функция обновляет профиль пользователя в соотвтетствии с текущим в Zendesk + Функция обновляет профиль пользователя в соответствии с текущим в Zendesk. + + :param user_profile: Профиль пользователя + :return: Обновленный, в соответствие с текущими данными в Zendesk, профиль пользователя """ user = ZendeskAdmin().get_user(user_profile.user.email) user_profile.name = user.name @@ -201,21 +235,27 @@ def update_profile(user_profile: UserProfile) -> UserProfile: def check_user_exist(email: str) -> bool: """ - Функция проверяет, существует ли пользователь + Функция проверяет, существует ли пользователь. + + :param email: Email пользователя + :return: Зарегистрирован ли пользователь в Zendesk """ return ZendeskAdmin().check_user(email) def get_user_organization(email: str) -> str: """ - Функция возвращает организацию пользователя + Функция возвращает организацию пользователя. + + :param email: Email пользователя + :return: Организация пользователя """ return ZendeskAdmin().get_user_org(email) def check_user_auth(email: str, password: str) -> bool: """ - Функция проверяет, верны ли входные данные + Функция проверяет, верны ли входные данные. :raise: :class:`APIException`: исключение, вызываемое если пользователь не аутентифицирован """ @@ -232,7 +272,14 @@ def check_user_auth(email: str, password: str) -> bool: return True -def update_user_in_model(profile, zendesk_user): +def update_user_in_model(profile: UserProfile, zendesk_user: User) -> UserProfile: + """ + Функция обновляет профиль пользователя при изменении данных пользователя на Zendesk. + + :param profile: Профиль пользователя + :param zendesk_user: Данные пользователя в Zendesk + :return: Обновленный профиль пользователя + """ profile.name = zendesk_user.name profile.role = zendesk_user.role profile.image = zendesk_user.photo['content_url'] if zendesk_user.photo else None @@ -243,7 +290,7 @@ def update_user_in_model(profile, zendesk_user): def count_users(users) -> tuple: """ - Функция подсчета количества сотрудников с ролями engineer и light_a + Функция подсчета количества сотрудников с ролями engineer и light_agent """ engineers, light_agents = 0, 0 for user in users: @@ -270,7 +317,11 @@ def update_users_in_model(): def daterange(start_date, end_date) -> list: """ - Возвращает список дней с start_date по end_date исключая правую границу + Функция возвращает список дней с start_date по end_date, исключая правую границу. + + :param start_date: Начальная дата + :param end_date: Конечная дата + :return: Список дней, не включая конечную дату """ dates = [] for n in range(int((end_date - start_date).days)): @@ -280,8 +331,12 @@ def daterange(start_date, end_date) -> list: def get_timedelta(log, time=None) -> timedelta: """ - Возвращает объект класса timedelta, который хранит промежуток времени от начала суток до момента, - который находится в log (объект класса RoleChangeLogs) или в time(datetime.time), если введён + Функция возвращает объект класса timedelta, который хранит промежуток времени от начала суток до момента, + который находится в log (объект класса RoleChangeLogs) или в time(datetime.time), если введён. + + :param log: Лог + :param time: Время + :return: Сколько времени прошло от начала суток до события """ if time is None: time = log.change_time.time() @@ -289,15 +344,41 @@ def get_timedelta(log, time=None) -> timedelta: return time -def last_day_of_month(day): +def last_day_of_month(day: int) -> int: """ - Возвращает последний день любого месяца + Функция возвращает последний день текущего месяца. + + :param day: Текущий день + :return: Последний день месяца """ next_month = day.replace(day=28) + timedelta(days=4) 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 @@ -314,10 +395,11 @@ class StatisticData: else: self.statistic = stat - def get_statistic(self): + def get_statistic(self) -> dict: """ - Вернуть словарь statistic с применением формата отображения и интеравала работы(если они есть) - None, если были ошибки при создании + Функция возвращает статистику работы пользователя. + + :return: Словарь statistic с применением формата отображения и интервала работы(если они есть). None, если были ошибки при создании. """ if self.is_valid_statistic(): stat = self.statistic @@ -327,15 +409,20 @@ class StatisticData: else: return None - def is_valid_statistic(self): + def is_valid_statistic(self) -> bool: """ - Были ли ошибки при создании статистики + Функция проверяет были ли ошибки при создании статистики. + + :return: True, при отсутствии ошибок """ return not self.errors and self.statistic - def set_interval(self, interval): + def set_interval(self, interval: list) -> bool: """ - Устанавливает интервал работы + Функция проверяет корректность представления интервала работы. + + :param interval: Интервал должен быть указан в днях или месяцах. + :return: True, если указан верно """ if interval not in ['months', 'days']: self.errors += ['Интервал работы должен быть в днях или месяцах'] @@ -343,9 +430,12 @@ class StatisticData: self.interval = interval return True - def set_display(self, display_format): + def set_display(self, display_format: list) -> bool: """ - Устанавливает формат отображения + Функция проверяет корректность формата отображения интервала. + + :param display_format: Формат отображения должен быть указан в днях или месяцах. + :return: True, если указан верно """ if display_format not in ['days', 'hours']: self.errors += ['Формат отображения должен быть в часах или днях'] @@ -353,26 +443,29 @@ class StatisticData: self.display = display_format return True - def get_data(self): + def get_data(self) -> list: """ - Вернуть данные - data - массив объектов RoleChangeLogs, является списком логов пользователя - data может быть пустым списком + Функция возвращает данные - список объектов RoleChangeLogs. """ if self.is_valid_data(): return self.data else: return None - def is_valid_data(self): + def is_valid_data(self) -> bool: """ - Были ли ошибки при получении логов + Функция определяет были ли ошибки при получении логов. + + :return: True, если ошибок нет """ return not self.errors - def _use_display(self, stat): + def _use_display(self, stat: list) -> list: """ - Приводит данные к формату отображения + Функция приводит данные к формату отображения. + + :param stat: Список данных статистики пользователя + :return: Обновленный список """ if not self.is_valid_statistic() or not self.display: return stat @@ -384,9 +477,12 @@ class StatisticData: new_stat[key] = item / (ONE_DAY * 3600) return new_stat - def _use_interval(self, stat): + def _use_interval(self, stat: dict) -> dict: """ - Объединяет ключи и значения в соответствии с интервалом работы + Функция объединяет ключи и значения в соответствии с интервалом работы. + + :param stat: Статистика работы пользователя + :return: Обновленная статистика """ if not self.is_valid_statistic() or not self.interval: return stat @@ -405,9 +501,11 @@ class StatisticData: new_stat = stat # статистика изначально в днях return new_stat - def check_time(self): + def check_time(self) -> bool: """ - Проверка на правильность введенного времени + Функция проверяет корректность введенного времени. + + :return: True, если время указано корректно. Иначе, False """ if self.end_date < self.start_date or self.end_date > datetime.now().date(): return False @@ -415,7 +513,9 @@ class StatisticData: def _init_data(self): """ - Получение логов в диапазоне дат start_date-end_date для пользователя с почтой email + Функция возвращает логи в диапазоне дат start_date - end_date для пользователя с указанным email. + + :return: Данные о смене статусов пользователя. Если пользователь не найден или интервал времени некорректен - ошибку. """ if not self.check_time(): self.errors += ['Конец диапазона должен быть позже начала диапазона и раньше текущего времени'] @@ -428,9 +528,11 @@ class StatisticData: except User.DoesNotExist: self.errors += ['Пользователь не найден'] - def _init_statistic(self): + def _init_statistic(self) -> dict: """ - Функция заполняет словарь, в котором ключ - дата, значение - кол-во проработанных в этот день секунд + Функция заполняет словарь, в котором ключ - дата, значение - кол-во проработанных в этот день секунд. + + :return: Статистика работы пользователя (statistic) """ self.clear_statistic() if not self.get_data(): @@ -465,17 +567,23 @@ class StatisticData: elapsed_time = next_log.change_time - current_log.change_time self.statistic[current_log.change_time.date()] += elapsed_time.total_seconds() - def fill_daterange(self, first, last, val=24 * 3600): + def fill_daterange(self, first: date, last: date, val: int = 24 * 3600) -> dict: """ - Заполение диапазона дат значением val - по умолчанию val = кол-во секунд в 1 дне + Функция заполняет диапазон дат значением val (по умолчанию val = кол-во секунд в 1 дне). + + :param first: Начальная дата интервала + :param last: Последняя дата интервала + :param val: Количество секунд в одном дне + :return: Статистику пользователя с указанным количеством секунд в заданных днях """ for day in daterange(first, last): self.statistic[day] = val - def clear_statistic(self): + def clear_statistic(self) -> dict: """ - Обнуление всех дней + Функция осуществляет обновление всех дней. + + :return: Статистику пользователя с количеством рабочих секунд = 0 """ self.statistic.clear() self.fill_daterange(self.start_date, self.end_date + timedelta(days=1), 0) diff --git a/main/forms.py b/main/forms.py index a8c2ec1..2ad67cb 100644 --- a/main/forms.py +++ b/main/forms.py @@ -8,12 +8,11 @@ from main.models import UserProfile class CustomRegistrationForm(RegistrationFormUniqueEmail): """ - Форма для регистрации :class:`django_registration.forms.RegistrationFormUniqueEmail` + Форма для регистрации :class:`django_registration.forms.RegistrationFormUniqueEmail` + с добавлением bootstrap-класса "form-control". - с добавлением bootstrap-класса "form-control" - - :param visible_fields.email: Поле для ввода email, зарегистирированного на Zendesk - :type visible_fields.email: :class:`django_registration.forms.RegistrationFormUniqueEmail` + :param visible_fields.email: Поле для ввода email, зарегистрированного на Zendesk + :type visible_fields.email: :class:`django_registration.forms.RegistrationFormUniqueEmail` """ def __init__(self, *args, **kwargs) -> RegistrationFormUniqueEmail: super().__init__(*args, **kwargs) @@ -32,10 +31,10 @@ class CustomRegistrationForm(RegistrationFormUniqueEmail): class AdminPageUsers(forms.Form): """ - Форма для установки статусов engineer или light_agent пользователям + Форма для установки статусов engineer или light_agent пользователям. - :param users: Поле для установки статуса - :type users: :class:`ModelMultipleChoiceField` + :param users: Поле для установки статуса + :type users: :class:`ModelMultipleChoiceField` """ users = forms.ModelMultipleChoiceField( @@ -52,8 +51,11 @@ class AdminPageUsers(forms.Form): class CustomAuthenticationForm(AuthenticationForm): """ - Форма для авторизации :class:`django.contrib.auth.forms.AuthenticationForm` - с изменением поля username на email + Форма для авторизации :class:`django.contrib.auth.forms.AuthenticationForm` + с изменением поля username на email. + + :param username: Поле для ввода email пользователя + :type username: :class:`django.forms.fields.CharField` """ username = forms.CharField( label="Электронная почта", @@ -69,6 +71,20 @@ class CustomAuthenticationForm(AuthenticationForm): class StatisticForm(forms.Form): + """ + Форма отображения интервалов работы пользователя. + + :param email: Поле для ввода email пользователя + :type email: :class:`django.forms.fields.EmailField` + :param interval: Расчет интервала рабочего времени + :type interval: :class:`django.forms.fields.CharField` + :param display_format: Формат отображения данных + :type display_format: :class:`django.forms.fields.CharField` + :param range_start: Дата и время начала работы + :type range_start: :class:`django.forms.fields.DateField` + :param range_end: Дата и время окончания работы + :type range_end: :class:`django.forms.fields.DateField` + """ email = forms.EmailField( label='Электроная почта', ) diff --git a/main/models.py b/main/models.py index 7bd76bb..782b79f 100644 --- a/main/models.py +++ b/main/models.py @@ -6,7 +6,11 @@ from django.utils import timezone class UserProfile(models.Model): - """Модель профиля пользователя""" + """ + Модель профиля пользователя. + + Профиль создается и изменяется при создании и изменении модель User. + """ class Meta: permissions = ( @@ -32,17 +36,27 @@ def save_user_profile(sender, instance, **kwargs): 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(help_text='Дата и время изменения роли', default=timezone.now) - changed_by = models.ForeignKey(to=User, on_delete=models.CASCADE, related_name='changed_by', - 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='Кем была изменена роль') class UnassignedTicketStatus(models.IntegerChoices): + """ + Класс статусов не распределенных тикетов. + + :param UNASSIGNED: Снят с пользователя, перенесён в буферную группу + :param RESTORED: Авторство восстановлено + :param NOT_FOUND: Пока нас не было, тикет испарился из буферной группы. Дополнительные действия не требуются + :param CLOSED: Тикет уже был закрыт. Дополнительные действия не требуются + :param SOLVED: Тикет решён. Записан на пользователя с почтой SOLVED_TICKETS_EMAIL + """ UNASSIGNED = 0, 'Снят с пользователя, перенесён в буферную группу' RESTORED = 1, 'Авторство восстановлено' NOT_FOUND = 2, 'Пока нас не было, тикет испарился из буферной группы. Дополнительные действия не требуются' @@ -51,6 +65,9 @@ class UnassignedTicketStatus(models.IntegerChoices): 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) + status = models.IntegerField(choices=UnassignedTicketStatus.choices, default=UnassignedTicketStatus.UNASSIGNED, help_text='Статус тикета') diff --git a/main/serializers.py b/main/serializers.py index f72fc86..5a4fd1a 100644 --- a/main/serializers.py +++ b/main/serializers.py @@ -4,12 +4,18 @@ from main.models import UserProfile class UserSerializer(serializers.HyperlinkedModelSerializer): + """ + Класс serializer для модели User. + """ class Meta: model = User fields = ['email'] class ProfileSerializer(serializers.HyperlinkedModelSerializer): + """ + Класс serializer для модель профиля пользователя. + """ user = UserSerializer() class Meta: diff --git a/main/views.py b/main/views.py index 2c00277..8cb568e 100644 --- a/main/views.py +++ b/main/views.py @@ -1,4 +1,6 @@ import logging +import os +from datetime import datetime from django.contrib.auth.decorators import login_required from django.contrib.auth.forms import PasswordResetForm @@ -35,11 +37,16 @@ from .models import UserProfile class CustomRegistrationView(RegistrationView): """ - Отображение и логика работы страницы регистрации пользователя + Отображение и логика работы страницы регистрации пользователя. - 1. Ввод email пользователя, указанный на Zendesk - 2. В случае если пользователь с данным паролем зарегистрирован на Zendesk и относится к определенной организации, происходит сброс ссылки с установлением пароля на указанный email - 3. Создается пользователь class User, а также его профиль + :param form_class: Форма, которую необходимо заполнить для регистрации + :type form_class: :class:`forms.CustomRegistrationForm` + :param template_name: Указание пути к html-странице django регистрации + :type template_name: :class:`str` + :param success_url: Указание пути к html-странице завершения регистрации + :type success_url: :class:`django.utils.functional.lazy..__proxy__` + :param is_allowed: Определение зарегистрирован ли пользователь с введенным email на Zendesk и принадлежит ли он к организации SYSTEM + :type is_allowed: :class:`bool` """ form_class = CustomRegistrationForm template_name = 'django_registration/registration_form.html' @@ -47,6 +54,16 @@ class CustomRegistrationView(RegistrationView): is_allowed = True def register(self, form: CustomRegistrationForm) -> User: + """ + Функция регистрации пользователя. + 1. Ввод email пользователя, указанный на Zendesk + 2. В случае если пользователь с данным паролем зарегистрирован на Zendesk и относится к организации SYSTEM, + происходит сброс ссылки с установлением пароля на указанный email + 3. Создается пользователь class User, а также его профиль. + + :param form: Email пользователя на Zendesk + :return: user + """ self.is_allowed = True if check_user_exist(form.data['email']) and get_user_organization(form.data['email']) == 'SYSTEM': forms = PasswordResetForm(self.request.POST) @@ -76,9 +93,11 @@ class CustomRegistrationView(RegistrationView): self.is_allowed = False @staticmethod - def set_permission(user) -> None: + def set_permission(user: User) -> None: """ - Дает разрешение на просмотр страница администратора, если пользователь имеет роль admin + Функция дает разрешение на просмотр страница администратора, если пользователь имеет роль admin. + + :param user: авторизованный пользователь (получает разрешение, имея роль "admin") """ if user.userprofile.role == 'admin': content_type = ContentType.objects.get_for_model(UserProfile) @@ -90,8 +109,11 @@ class CustomRegistrationView(RegistrationView): def get_success_url(self, user: User = None) -> success_url: """ - Возвращает url-адрес страницы, куда нужно перейти после успешной/неуспешной регистрации - Используется самой django-registration + Функция возвращает url-адрес страницы, куда нужно перейти после успешной/не успешной регистрации. + Используется самой django-registration. + + :param user: пользователь, пытающийся зарегистрироваться + :return: адресация на страницу успешной регистрации """ if self.is_allowed: return reverse_lazy('password_reset_done') @@ -102,7 +124,10 @@ class CustomRegistrationView(RegistrationView): @login_required() def profile_page(request: WSGIRequest) -> HttpResponse: """ - Отображение страницы профиля + Функция отображения страницы профиля. + + :param request: данные пользователя из БД + :return: адресация на страницу пользователя """ user_profile: UserProfile = request.user.userprofile update_profile(user_profile) @@ -113,14 +138,27 @@ def profile_page(request: WSGIRequest) -> HttpResponse: return render(request, 'pages/profile.html', context) -def auth_user(request): +def auth_user(request: WSGIRequest) -> ZenpyUser: + """ + Функция возвращает профиль пользователя на Zendesk. + + :param request: email, subdomain и token пользователя + :return: объект пользователя Zendesk + """ admin = ZendeskAdmin().admin zenpy_user: ZenpyUser = admin.users.search(request.user.email).values[0] return zenpy_user, admin @login_required() -def work_page(request, id): +def work_page(request: WSGIRequest, id: int) -> HttpResponse: + """ + Функция отображения страницы "Управления правами" для текущего пользователя (login_required). + + :param request: объект пользователя + :param id: id пользователя, используется для динамической адресации + :return: адресация на страницу "Управления правами" (либо на страницу "Авторизации", если id и user.id не совпадают + """ users = get_users_list() if request.user.id == id: engineers = [] @@ -143,7 +181,16 @@ def work_page(request, id): return redirect("login") -def user_update(zenpy_user, admin, request): +def user_update(zenpy_user: User, admin: User, request: WSGIRequest) -> UserProfile: + """ + Функция устанавливает пользователю роль "agent" (изменяет профиль). + + :param zenpy_user: Пользователь Zendesk + :param admin: Пользователь + :param request: Запрос установки роли "agent" в Userprofile + :return: Обновленный профиль пользователя + """ + admin.users.update(zenpy_user) request.user.userprofile.role = "agent" request.user.userprofile.save() @@ -151,7 +198,14 @@ def user_update(zenpy_user, admin, request): @login_required() -def work_hand_over(request): +def work_hand_over(request: WSGIRequest) -> HttpResponseRedirect: + """ + Функция позволяет текущему пользователю (login_required) сдать права, а именно сменить в Zendesk роль с "engineer" на "light agent" + и установить роль "agent" в БД. Действия выполняются, если исходная роль пользователя "engineer". + + :param request: данные текущего пользователя (login_required) + :return: перезагрузка текущей страницы после выполнения смены роли + """ zenpy_user, admin = auth_user(request) if zenpy_user.custom_role_id == ZENDESK_ROLES['engineer']: zenpy_user.custom_role_id = ZENDESK_ROLES['light_agent'] @@ -160,7 +214,13 @@ def work_hand_over(request): @login_required() -def work_become_engineer(request): +def work_become_engineer(request: WSGIRequest) -> HttpResponseRedirect: + """ + Функция меняет роль пользователя в Zendesk на "engineer" и присваивает роль "agent" в БД (в случае, если исходная роль пользователя была "light_agent"). + + :param request: данные текущего пользователя (login_required) + :return: перезагрузка текущей страницы после выполнения смены роли + """ zenpy_user, admin = auth_user(request) if zenpy_user.custom_role_id == ZENDESK_ROLES['light_agent']: zenpy_user.custom_role_id = ZENDESK_ROLES['engineer'] @@ -168,16 +228,34 @@ def work_become_engineer(request): return HttpResponseRedirect(reverse('work', args=(request.user.id,))) -def main_page(request): +def main_page(request: WSGIRequest) -> HttpResponse: """ - Отображение логгирования на главной странице + Функция отображения логгирования на главной странице. + + .. todo:: + Дописать параметры в документацию: + + :param request: + :return: """ logger = logging.getLogger('main.index') logger.info('Index page opened') return render(request, 'pages/index.html') -class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, FormView): +class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin, FormView): + """ + Класс отображения страницы администратора. + + :param permission_required: Права доступа к странице администратора + :type permission_required: :class:`str` + :param template_name: HTML-шаблон страницы администратора + :type template_name: :class:`str` + :param form_class: Форма страницы администратора + :type form_class: :class:`forms.AdminPageUsersForm` + :param success_url: Адрес страницы администратора + :type success_url: :class:`HttpResponseRedirect` + """ permission_required = 'main.has_control_access' template_name = 'pages/adm_ruleset.html' form_class = AdminPageUsers @@ -186,7 +264,10 @@ class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageM def form_valid(self, form: AdminPageUsers) -> AdminPageUsers: """ - Функция установки ролей пользователям + Функция обновления страницы AdminPageUsers. + + :param form: Форма страницы администратора + :return: Обновленная страница (пользователям проставлены новые статусы) """ users = form.cleaned_data['users'] if 'engineer' in self.request.POST: @@ -196,6 +277,12 @@ class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageM return super().form_valid(form) def make_engineers(self, users): + """ + Функция проходит по списку пользователей, проставляя статус "engineer". + + :param users: Список пользователей + :return: Обновленный список пользователей + """ for user in users: make_engineer(user, self.request.user) @@ -244,9 +331,15 @@ class UsersViewSet(viewsets.ReadOnlyModelViewSet): @login_required() -def statistic_page(request): - if not request.user.has_perm('main.has_control_access'): - raise PermissionDenied +def statistic_page(request: WSGIRequest) -> HttpResponse: + """ + Функция отображения страницы статистики (для "superuser"). + + :param request: данные о пользователе: email, время и интервал работы. Данные получаем через forms.StatisticForm + :return: адресация на страницу статистики + """ + if not request.user.is_superuser: + return redirect('index') context = { 'pagename': 'страница статистики', 'errors': list(),