Merge branch 'feature/documentation' into 'develop'

Feature/documentation

See merge request 2020-2021/online/s101/group-02/access_controller!41
This commit is contained in:
Кравченко Артем 2021-03-27 17:31:26 +00:00
commit 7cef5c3a86
20 changed files with 572 additions and 139 deletions

View File

@ -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."

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -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:

View File

@ -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

View File

@ -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
==================

View File

@ -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гю

View File

@ -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

View File

@ -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'
}

View File

@ -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)

View File

@ -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='Электроная почта',
)

View File

@ -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='Статус тикета')

View File

@ -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:

View File

@ -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.<locals>.__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(),