merge dev

This commit is contained in:
Artyom Kravchenko
2021-03-30 17:32:35 +03:00
39 changed files with 1095 additions and 226 deletions

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

@@ -2,25 +2,27 @@ import os
from datetime import timedelta, datetime, date
from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist
from django.utils import timezone
from zenpy import Zenpy
from zenpy.lib.exception import APIException
from access_controller.settings import ZENDESK_ROLES as ROLES, ONE_DAY
from main.models import UserProfile, RoleChangeLogs
from access_controller.settings import ZENDESK_ROLES as ROLES, ONE_DAY, ZENDESK_GROUPS, SOLVED_TICKETS_EMAIL
from main.models import UserProfile, RoleChangeLogs, UnassignedTicket, UnassignedTicketStatus
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 = {
@@ -29,19 +31,16 @@ class ZendeskAdmin:
email: str = os.getenv('ACCESS_CONTROLLER_API_EMAIL')
token: str = os.getenv('ACCESS_CONTROLLER_API_TOKEN')
password: str = os.getenv('ACCESS_CONTROLLER_API_PASSWORD')
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
self.create_admin()
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
@@ -54,49 +53,73 @@ 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: str) -> str:
"""
Функция возвращает группы, к которым принадлежит пользователь.
:param name: Имя пользователя
:return: Группы пользователя (в случае отсутствия None)
"""
groups = self.admin.search(name)
for group in groups:
return group
return None
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:
@@ -118,41 +141,91 @@ 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)
user.custom_role_id = role
user_profile.custom_role_id = role
user_profile.save()
zendesk.admin.users.update(user)
def make_engineer(user_profile: UserProfile) -> 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,
old_role=user_profile.custom_role_id,
new_role=ROLES['engineer'],
changed_by=who_changes
)
update_role(user_profile, ROLES['engineer'])
def make_light_agent(user_profile: UserProfile) -> 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:
UnassignedTicket.objects.create(
assignee=user_profile.user,
ticket_id=ticket.id,
status=UnassignedTicketStatus.SOLVED if ticket.status == 'solved' else UnassignedTicketStatus.UNASSIGNED
)
if ticket.status == 'solved':
ticket.assignee = ZendeskAdmin().get_user(SOLVED_TICKETS_EMAIL)
else:
ticket.assignee = None
ticket.group = ZendeskAdmin().get_group(ZENDESK_GROUPS['buffer'])
ZendeskAdmin().admin.tickets.update(ticket)
RoleChangeLogs.objects.create(
user=user_profile.user,
old_role=user_profile.custom_role_id,
new_role=ROLES['light_agent'],
changed_by=who_changes
)
update_role(user_profile, ROLES['light_agent'])
def get_users_list() -> list:
"""
Функция **get_users_list** возвращает список пользователей Zendesk, относящихся к организации.
Функция **get_users_list** возвращает список пользователей Zendesk, относящихся к организации SYSTEM.
"""
zendesk = ZendeskAdmin()
admin = zendesk.get_user(zendesk.email)
org = next(zendesk.admin.users.organizations(user=admin))
return zendesk.admin.organizations.users(org)
# У пользователей должна быть организация SYSTEM
org = next(zendesk.admin.search(type='organization', name='SYSTEM'))
users = zendesk.admin.organizations.users(org)
return users
def get_tickets_list(email):
"""
Функция возвращает список тикетов пользователя Zendesk
"""
return ZendeskAdmin().admin.search(assignee=email, type='ticket')
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
@@ -164,21 +237,93 @@ 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`: исключение, вызываемое если пользователь не аутентифицирован
"""
creds = {
'email': email,
'password': password,
'subdomain': 'ngenix1612197338',
}
try:
user = Zenpy(**creds)
user.search(email, type='user')
except APIException:
return False
return True
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
if zendesk_user.custom_role_id is not None:
profile.custom_role_id = int(zendesk_user.custom_role_id)
profile.save()
def count_users(users) -> tuple:
"""
Функция подсчета количества сотрудников с ролями engineer и light_agent
"""
engineers, light_agents = 0, 0
for user in users:
if user.custom_role_id == ROLES['engineer']:
engineers += 1
elif user.custom_role_id == ROLES['light_agent']:
light_agents += 1
return engineers, light_agents
def update_users_in_model():
"""
Обновляет пользователей в модели UserProfile по списку пользователей в организации
"""
users = get_users_list()
for user in users:
try:
profile = User.objects.get(email=user.email).userprofile
update_user_in_model(profile, user)
except ObjectDoesNotExist:
pass
return users
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)):
@@ -188,8 +333,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()
@@ -197,15 +346,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
@@ -216,16 +391,17 @@ class StatisticData:
self.warnings = list()
self.data = dict()
self.statistic = dict()
self._set_data()
self._init_data()
if stat is None:
self._set_statistic()
self._init_statistic()
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
@@ -235,15 +411,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 += ['Интервал работы должен быть в днях или месяцах']
@@ -251,9 +432,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 += ['Формат отображения должен быть в часах или днях']
@@ -261,26 +445,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
@@ -292,9 +479,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
@@ -313,17 +503,21 @@ 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
return True
def _set_data(self):
def _init_data(self):
"""
Получение логов в диапазоне дат start_date-end_date для пользователя с почтой email
Функция возвращает логи в диапазоне дат start_date - end_date для пользователя с указанным email.
:return: Данные о смене статусов пользователя. Если пользователь не найден или интервал времени некорректен - ошибку.
"""
if not self.check_time():
self.errors += ['Конец диапазона должен быть позже начала диапазона и раньше текущего времени']
@@ -336,9 +530,11 @@ class StatisticData:
except User.DoesNotExist:
self.errors += ['Пользователь не найден']
def _set_statistic(self):
def _init_statistic(self) -> dict:
"""
Функция заполняет словарь, в котором ключ - дата, значение - кол-во проработанных в этот день секунд
Функция заполняет словарь, в котором ключ - дата, значение - кол-во проработанных в этот день секунд.
:return: Статистика работы пользователя (statistic)
"""
self.clear_statistic()
if not self.get_data():
@@ -347,16 +543,21 @@ class StatisticData:
first_log, last_log = self.data[0], self.data[len(self.data) - 1]
if first_log.old_role == ROLES['engineer']:
self.fill_daterange(self.start_date, first_log.change_time.date())
self.statistic[first_log.change_time.date()] += get_timedelta(first_log).total_seconds()
if last_log.new_role == ROLES['engineer']:
if last_log.new_role == ROLES['engineer']: # TODO отдельная функция
self.fill_daterange(last_log.change_time.date() + timedelta(days=1), self.end_date + timedelta(days=1))
self.statistic[last_log.change_time.date()] += (timedelta(days=1) - get_timedelta(last_log)).total_seconds()
if self.end_date == datetime.now().date():
self.statistic[self.end_date] = get_timedelta(None, datetime.now().time()).total_seconds()
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()
for log_index in range(len(self.data) - 1):
for log_index in range(len(self.data) - 1): # TODO отдельная функция
if self.data[log_index].new_role == ROLES['engineer']:
current_log, next_log = self.data[log_index], self.data[log_index + 1]
if current_log.change_time.date() != next_log.change_time.date():
@@ -368,17 +569,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

@@ -0,0 +1,29 @@
# Generated by Django 3.1.6 on 2021-03-11 17:27
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('main', '0011_auto_20210311_1734'),
]
operations = [
migrations.RemoveField(
model_name='rolechangelogs',
name='name',
),
migrations.CreateModel(
name='UnassignedTicket',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('ticket_id', models.IntegerField(help_text='Номер тикера, для которого сняли ответственного')),
('status', models.IntegerField(choices=[(0, 'Снят с пользователя, перенесён в буферную группу'), (1, 'Авторство восстановлено'), (2, 'Пока нас не было, тикет испарился из буферной группы. Дополнительные действия не требуются')], default=0)),
('assignee', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tickets', to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.6 on 2021-03-11 17:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0012_auto_20210311_2027'),
]
operations = [
migrations.AlterField(
model_name='unassignedticket',
name='status',
field=models.IntegerField(choices=[(0, 'Снят с пользователя, перенесён в буферную группу'), (1, 'Авторство восстановлено'), (2, 'Пока нас не было, тикет испарился из буферной группы. Дополнительные действия не требуются'), (3, 'Тикет уже был закрыт. Дополнительные действия не требуются')], default=0),
),
]

View File

@@ -1,12 +1,13 @@
# Generated by Django 3.1.6 on 2021-03-12 09:25
# Generated by Django 3.1.6 on 2021-03-14 11:55
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('main', '0011_auto_20210311_1734'),
('main', '0013_auto_20210311_2040'),
]
operations = [
@@ -15,6 +16,11 @@ class Migration(migrations.Migration):
name='custom_role_id',
field=models.IntegerField(default=0, help_text='Код роли пользователя'),
),
migrations.AlterField(
model_name='rolechangelogs',
name='change_time',
field=models.DateTimeField(default=django.utils.timezone.now, help_text='Дата и время изменения роли'),
),
migrations.AlterField(
model_name='userprofile',
name='role',

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.6 on 2021-03-21 13:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0014_auto_20210314_1455'),
]
operations = [
migrations.AlterField(
model_name='unassignedticket',
name='status',
field=models.IntegerField(choices=[(0, 'Снят с пользователя, перенесён в буферную группу'), (1, 'Авторство восстановлено'), (2, 'Пока нас не было, тикет испарился из буферной группы. Дополнительные действия не требуются'), (3, 'Тикет уже был закрыт. Дополнительные действия не требуются'), (4, 'Тикет решён. Записан на пользователя с почтой SOLVED_TICKETS_EMAIL')], default=0),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.1.6 on 2021-03-29 21:07
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0014_auto_20210314_1455'),
]
operations = [
migrations.AlterField(
model_name='unassignedticket',
name='status',
field=models.IntegerField(choices=[(0, 'Снят с пользователя, перенесён в буферную группу'), (1, 'Авторство восстановлено'), (2, 'Пока нас не было, тикет испарился из буферной группы. Дополнительные действия не требуются'), (3, 'Тикет уже был закрыт. Дополнительные действия не требуются'), (4, 'Тикет решён. Записан на пользователя с почтой SOLVED_TICKETS_EMAIL')], default=0),
),
]

View File

@@ -0,0 +1,14 @@
# Generated by Django 3.1.6 on 2021-03-29 21:43
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('main', '0015_auto_20210330_0007'),
('main', '0015_auto_20210321_1600'),
]
operations = [
]

View File

@@ -2,10 +2,15 @@ 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
class UserProfile(models.Model):
"""Модель профиля пользователя"""
"""
Модель профиля пользователя.
Профиль создается и изменяется при создании и изменении модель User.
"""
class Meta:
permissions = (
@@ -31,13 +36,39 @@ def save_user_profile(sender, instance, **kwargs):
class RoleChangeLogs(models.Model):
"""Модель для логирования изменений ролей пользователя"""
user = models.ForeignKey(to=User, on_delete=models.CASCADE,
help_text='Пользователь, которому присвоили другую роль')
name = models.TextField(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='Дата и время изменения роли')
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, 'Пока нас не было, тикет испарился из буферной группы. Дополнительные действия не требуются'
CLOSED = 3, 'Тикет уже был закрыт. Дополнительные действия не требуются'
SOLVED = 4, 'Тикет решён. Записан на пользователя с почтой SOLVED_TICKETS_EMAIL'
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='Статус тикета')

23
main/serializers.py Normal file
View File

@@ -0,0 +1,23 @@
from django.contrib.auth.models import User
from rest_framework import serializers
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:
model = UserProfile
fields = ['user', 'id', 'role', 'name']

View File

@@ -27,6 +27,7 @@
</style>
{% block extra_css %}{% endblock %}
{% block extra_scripts %}{% endblock %}
</head>
<body class="d-flex flex-column h-100">

View File

@@ -0,0 +1,14 @@
<div class="mt-5">
{% for message in messages %}
<div
class="alert alert-{{ message.tags }} alert-dismissible fade show p-2"
role="alert"
style="display: flex; align-items: center; justify-content: space-between;"
>
{{ message }}
<div>
<button type="button" class="btn btn-light p-2" data-bs-dismiss="alert" aria-label="Close">X</button>
</div>
</div>
{% endfor %}
</div>

View File

@@ -10,11 +10,19 @@
<link rel="stylesheet" href="{% static 'main/css/work.css' %}"/>
{% endblock %}
{% block extra_scripts %}
<script src="https://unpkg.com/react@17/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js" crossorigin></script>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
{% endblock%}
{% block content %}
<div class="container-md">
<div class="new-section">
<p class="row page-description">Основная информация о странице</p>
</div>
<div class="new-section">
<p class="row page-description">Свободных Мест: {{ licences_remaining }}</p>
</div>
{% block form %}
<form method="post">
@@ -37,25 +45,28 @@
<table class="light-table">
<thead>
<th>ID</th>
<th>Name</th>
<th>Email</th>
<th>Role</th>
<th>Name(link to profile)</th>
<th>Checked</th>
</thead>
<tbody>
<tbody id="old_tbody">
{% for user in users %}
<tr>
<td>{{ user.id }}</td>
<td>{{ user.user.email }}</td>
<td>{{ user.role }}</td>
<td><a href="#">{{ user.name }}</a></td>
<td>{{ user.user.email }}</td>
<td>{% if user.custom_role_id == ZENDESK_ROLES.engineer %}
engineer
{% elif user.custom_role_id == ZENDESK_ROLES.light_agent %}
light_agent
{% endif %}
</td>
<td class="checkbox_field"></td>
</tr>
{% endfor %}
</tbody>
<tbody id="new_tbody"></tbody>
</table>
{% endblock%}
@@ -95,14 +106,15 @@
<button type="submit" name="light_agent" class="hand-over-acess-button default-button">
Назначить выбранных на роль легкого агента
</button>
</div>
</div>
{% endblock %}
</form>
{% endblock %}
{% include 'base/success_messages.html' %}
</div>
<script src="{% static 'main/js/control.js'%}"></script>
<script src="{% static 'main/js/control.js'%}" type="text/babel"></script>
{% endblock %}

View File

@@ -37,7 +37,13 @@
<br>
<h5><span class="badge bg-secondary text-light">Электронная почта</span> {{ profile.user.email }}</h5>
<br>
<h5><span class="badge bg-secondary text-light">Текущая роль</span> {{ profile.role }}</h5>
<h5><span class="badge bg-secondary text-light">Текущая роль</span>
{% if profile.custom_role_id == ZENDESK_ROLES.engineer %}
engineer
{% elif profile.custom_role_id == ZENDESK_ROLES.light_agent %}
light_agent
{% endif %}
</h5>
</div>
</div>
<div align="center">

View File

@@ -14,7 +14,7 @@
<div class="container-md">
<div class="new-section">
<p class="row page-description">Основаная информация о странице</p>
<p class="row page-description">Свободных Мест: {{ licences_remaining }}</p>
</div>
<div class="row justify-content-center new-section">
@@ -61,6 +61,7 @@
<a href="/work/hand_over" class="hand-over-acess-button default-button">Сдать права инженера</a>
</div>
</div>
{% include 'base/success_messages.html' %}
</div>
{% endblock %}

View File

@@ -30,7 +30,7 @@
{% endif %}
<div class="text-center">
<button type="submit" class="btn btn-primary">Войти</button>
<a href="password_reset" class="btn btn-link" style="display: block;">Забыли пароль?</a>
<a href="{% url 'password_reset' %}" class="btn btn-link" style="display: block;">Забыли пароль?</a>
</div>
</form>
</div>

6
main/urls.py Normal file
View File

@@ -0,0 +1,6 @@
from rest_framework.routers import DefaultRouter
from main.views import UsersViewSet
router = DefaultRouter()
router.register(r'users', UsersViewSet)

View File

@@ -9,6 +9,7 @@ from django.contrib.auth.models import User, Permission
from django.contrib.auth.tokens import default_token_generator
from django.contrib.auth.views import LoginView
from django.contrib.contenttypes.models import ContentType
from django.contrib.messages.views import SuccessMessageMixin
from django.core.exceptions import PermissionDenied
from django.core.handlers.wsgi import WSGIRequest
from django.http import HttpResponseRedirect, HttpResponse
@@ -16,22 +17,36 @@ from django.shortcuts import render, get_list_or_404, redirect
from django.urls import reverse_lazy, reverse
from django.views.generic import FormView
from django_registration.views import RegistrationView
from zenpy import Zenpy
from django.contrib import messages
# Django REST
from rest_framework import viewsets
from rest_framework.response import Response
from zenpy.lib.api_objects import User as ZenpyUser
from access_controller.settings import EMAIL_HOST_USER, ZENDESK_ROLES
from main.extra_func import check_user_exist, update_profile, get_user_organization, make_engineer, make_light_agent, \
get_users_list, StatisticData
from access_controller.settings import EMAIL_HOST_USER, ZENDESK_ROLES, ZENDESK_MAX_AGENTS
from main.extra_func import ZendeskAdmin
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
from main.forms import AdminPageUsers, CustomRegistrationForm, CustomAuthenticationForm, StatisticForm
from main.serializers import ProfileSerializer
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'
@@ -39,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)
@@ -68,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)
@@ -82,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')
@@ -94,30 +124,42 @@ class CustomRegistrationView(RegistrationView):
@login_required()
def profile_page(request: WSGIRequest) -> HttpResponse:
"""
Отображение страницы профиля
Функция отображения страницы профиля.
:param request: данные пользователя из БД
:return: адресация на страницу пользователя
"""
user_profile: UserProfile = request.user.userprofile
update_profile(user_profile)
context = {
'profile': user_profile,
'pagename': 'Страница профиля'
'pagename': 'Страница профиля',
'ZENDESK_ROLES': ZENDESK_ROLES,
}
return render(request, 'pages/profile.html', context)
def auth_user(request):
admin_creds = {
'email': os.environ.get('ACCESS_CONTROLLER_API_EMAIL'),
'subdomain': 'ngenix1612197338',
'token': os.environ.get('ACCESS_CONTROLLER_API_TOKEN'),
}
admin = Zenpy(**admin_creds)
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 = []
@@ -132,94 +174,123 @@ def work_page(request, id):
context = {
'engineers': engineers,
'agents': light_agents,
'messages': messages.get_messages(request),
'licences_remaining': max(0, ZENDESK_MAX_AGENTS - len(engineers)),
'pagename': 'Управление правами'
}
return render(request, 'pages/work.html', context)
return redirect("login")
@login_required()
def work_hand_over(request):
zenpy_user, admin = auth_user(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()
messages.success(request, "Права были изменены")
@login_required()
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']
admin.users.update(zenpy_user)
request.user.userprofile.role = "agent"
request.user.userprofile.save()
user_update(zenpy_user, admin, request)
return HttpResponseRedirect(reverse('work', args=(request.user.id,)))
@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']
admin.users.update(zenpy_user)
request.user.userprofile.role = "agent"
request.user.userprofile.save()
user_update(zenpy_user, admin, request)
return HttpResponseRedirect(reverse('work', args=(request.user.id,)))
def main_page(request):
"""
Отображение логгирования на главной странице
"""
logger = logging.getLogger('main.index')
logger.info('Index page opened')
return render(request, 'pages/index.html')
class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin, FormView):
class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin,SuccessMessageMixin, 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
success_url = '/control/'
success_message = "Права были изменены."
def form_valid(self, form: AdminPageUsers) -> AdminPageUsers:
"""
Функция установки ролей пользователям
Функция обновления страницы AdminPageUsers.
:param form: Форма страницы администратора
:return: Обновленная страница (пользователям проставлены новые статусы)
"""
users = form.cleaned_data['users']
if 'engineer' in self.request.POST:
self.make_engineers(form.cleaned_data['users'])
self.make_engineers(users)
elif 'light_agent' in self.request.POST:
self.make_light_agents(form.cleaned_data['users'])
self.make_light_agents(users)
return super().form_valid(form)
@staticmethod
def make_engineers(users):
[make_engineer(user) for user in users]
@staticmethod
def make_light_agents(users):
[make_light_agent(user) for user in users]
@staticmethod
def count_users(users) -> tuple:
def make_engineers(self, users):
"""
Функция подсчета количества сотрудников с ролями engineer и light_a
Функция проходит по списку пользователей, проставляя статус "engineer".
.. todo::
this func counts users from all zendesk instead of just from a model:
:param users: Список пользователей
:return: Обновленный список пользователей
"""
engineers, light_agents = 0, 0
for user in users:
if user.custom_role_id == ZENDESK_ROLES['engineer']:
engineers += 1
elif user.custom_role_id == ZENDESK_ROLES['light_agent']:
light_agents += 1
return engineers, light_agents
make_engineer(user, self.request.user)
def make_light_agents(self, users):
for user in users:
make_light_agent(user, self.request.user)
def get_context_data(self, **kwargs) -> dict:
"""
Функция формирования контента страницы администратора (с проверкой прав доступа)
"""
if self.request.user.userprofile.role != 'admin':
raise PermissionDenied
context = super().get_context_data(**kwargs)
context['users'] = get_list_or_404(
users = get_list_or_404(
UserProfile, role='agent')
context['engineers'], context['light_agents'] = self.count_users(get_users_list())
context['users'] = users
context['ZENDESK_ROLES'] = ZENDESK_ROLES
context['engineers'], context['light_agents'] = count_users(get_users_list())
context['licences_remaining'] = max(0, ZENDESK_MAX_AGENTS - context['engineers'])
return context # TODO: need to get profile page url
@@ -230,8 +301,34 @@ class CustomLoginView(LoginView):
form_class = CustomAuthenticationForm
class UsersViewSet(viewsets.ReadOnlyModelViewSet):
"""
Класс для получения пользователей с помощью api
"""
queryset = UserProfile.objects.filter(role='agent')
serializer_class = ProfileSerializer
def list(self, request, *args, **kwargs):
users = update_users_in_model().values
count = count_users(users)
profiles = UserProfile.objects.filter(role='agent')
serializer = self.get_serializer(profiles, many=True)
return Response({
'users': serializer.data,
'engineers': count[0],
'light_agents': count[1]
})
@login_required()
def statistic_page(request):
def statistic_page(request: WSGIRequest) -> HttpResponse:
"""
Функция отображения страницы статистики (для "superuser").
:param request: данные о пользователе: email, время и интервал работы. Данные получаем через forms.StatisticForm
:return: адресация на страницу статистики
"""
if not request.user.is_superuser:
return redirect('index')
context = {