Merge branch 'develop' into feature/pylint

# Conflicts:
#	main/extra_func.py
#	main/tests.py
#	main/views.py
This commit is contained in:
Степаненко Ольга 2021-05-10 20:22:39 +03:00
commit 492260dc79
9 changed files with 565 additions and 119 deletions

View File

@ -0,0 +1,85 @@
[
{
"model": "auth.user",
"pk": 1,
"fields": {
"password": "pbkdf2_sha256$216000$gHBBCr1jBELf$ZkEDW3IEd8Wij7u8vkv+0Eze32CS01bcaYWhcD9OIC4=",
"last_login": null,
"is_superuser": true,
"username": "admin@gmail.com",
"first_name": "",
"last_name": "",
"email": "admin@gmail.com",
"is_staff": true,
"is_active": true,
"date_joined": "2021-03-10T16:38:56.303Z",
"groups": [],
"user_permissions": [33]
}
},
{
"model": "main.userprofile",
"pk": 1,
"fields": {
"name": "ZendeskAdmin",
"user": 1,
"role": "admin"
}
},
{
"model": "auth.user",
"pk": 2,
"fields": {
"password": "pbkdf2_sha256$216000$5qLJgrm2Quq9$KDBNNymVZXkUx0HKBPFst2m83kLe0egPBnkW7KnkORU=",
"last_login": null,
"is_superuser": false,
"username": "123@test.ru",
"first_name": "",
"last_name": "",
"email": "123@test.ru",
"is_staff": false,
"is_active": true,
"date_joined": "2021-03-10T16:38:56.303Z",
"groups": [],
"user_permissions": []
}
},
{
"model": "main.userprofile",
"pk": 2,
"fields": {
"name": "UserForAccessTest",
"user": 2,
"role": "agent",
"custom_role_id": "360005209000"
}
},
{
"model": "auth.user",
"pk": 3,
"fields": {
"password": "pbkdf2_sha256$216000$5qLJgrm2Quq9$KDBNNymVZXkUx0HKBPFst2m83kLe0egPBnkW7KnkORU=",
"last_login": null,
"is_superuser": false,
"username": "customer@example.com",
"first_name": "",
"last_name": "",
"email": "customer@example.com",
"is_staff": false,
"is_active": true,
"date_joined": "2021-04-15T16:38:56.303Z",
"groups": [],
"user_permissions": []
}
},
{
"model": "main.userprofile",
"pk": 3,
"fields": {
"name": "UserForAccessTest",
"user": 3,
"role": "agent",
"custom_role_id": "360005209000"
}
}
]

View File

@ -7,8 +7,6 @@ from typing import Optional, Union
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.core.handlers.wsgi import WSGIRequest
from django.http import HttpResponseRedirect, HttpResponsePermanentRedirect
from django.shortcuts import redirect from django.shortcuts import redirect
from django.utils import timezone from django.utils import timezone
from zenpy import Zenpy from zenpy import Zenpy
@ -16,8 +14,9 @@ from zenpy.lib.exception import APIException
from zenpy.lib.api_objects import User as ZenpyUser, Ticket as ZenpyTicket from zenpy.lib.api_objects import User as ZenpyUser, Ticket as ZenpyTicket
from zenpy.lib.generator import SearchResultGenerator from zenpy.lib.generator import SearchResultGenerator
from access_controller.settings import ZENDESK_ROLES as ROLES, ONE_DAY, ACTRL_ZENDESK_SUBDOMAIN from access_controller.settings import ZENDESK_ROLES as ROLES, ACTRL_ZENDESK_SUBDOMAIN
from main.models import UserProfile, RoleChangeLogs, UnassignedTicket, UnassignedTicketStatus from main.models import UserProfile, RoleChangeLogs, UnassignedTicket, UnassignedTicketStatus
from main.requester import TicketListRequester
from main.zendesk_admin import zenpy from main.zendesk_admin import zenpy
@ -27,6 +26,7 @@ def update_role(user_profile: UserProfile, role: int, who_changes: get_user_mode
:param user_profile: Профиль пользователя :param user_profile: Профиль пользователя
:param role: Новая роль :param role: Новая роль
:param who_changes: Пользователь, меняющий роль
:return: Пользователь с обновленной ролью :return: Пользователь с обновленной ролью
""" """
zendesk = zenpy zendesk = zenpy
@ -34,7 +34,8 @@ def update_role(user_profile: UserProfile, role: int, who_changes: get_user_mode
user.custom_role_id = role user.custom_role_id = role
user_profile.custom_role_id = role user_profile.custom_role_id = role
user_profile.save() user_profile.save()
zendesk.admin.users.update(user) log(user_profile, who_changes.userprofile)
zendesk.update_user(user)
def make_engineer(user_profile: UserProfile, who_changes: get_user_model()) -> None: def make_engineer(user_profile: UserProfile, who_changes: get_user_model()) -> None:
@ -42,8 +43,7 @@ def make_engineer(user_profile: UserProfile, who_changes: get_user_model()) -> N
Функция устанавливает пользователю роль инженера. Функция устанавливает пользователю роль инженера.
:param user_profile: Профиль пользователя :param user_profile: Профиль пользователя
:return: Вызов функции **update_role** с параметрами: :return: Вызов функции **update_role** с параметрами: профиль пользователя, роль "engineer"
профиль пользователя, роль "engineer"
""" """
update_role(user_profile, ROLES['engineer'], who_changes) update_role(user_profile, ROLES['engineer'], who_changes)
@ -53,8 +53,7 @@ def make_light_agent(user_profile: UserProfile, who_changes: get_user_model()) -
Функция устанавливает пользователю роль легкого агента. Функция устанавливает пользователю роль легкого агента.
:param user_profile: Профиль пользователя :param user_profile: Профиль пользователя
:return: Вызов функции **update_role** с параметрами: :return: Вызов функции **update_role** с параметрами: профиль пользователя, роль "light_agent"
профиль пользователя, роль "light_agent"
""" """
tickets: SearchResultGenerator = get_tickets_list(user_profile.user.email) tickets: SearchResultGenerator = get_tickets_list(user_profile.user.email)
ticket: ZenpyTicket ticket: ZenpyTicket
@ -62,18 +61,16 @@ def make_light_agent(user_profile: UserProfile, who_changes: get_user_model()) -
UnassignedTicket.objects.create( UnassignedTicket.objects.create(
assignee=user_profile.user, assignee=user_profile.user,
ticket_id=ticket.id, ticket_id=ticket.id,
status=UnassignedTicketStatus.SOLVED if ticket.status == 'solved' status=UnassignedTicketStatus.SOLVED if ticket.status == 'solved' else UnassignedTicketStatus.UNASSIGNED
else UnassignedTicketStatus.UNASSIGNED
) )
if ticket.status == 'solved': if ticket.status == 'solved':
ticket.assignee_id = zenpy.solved_tickets_user_id ticket.assignee_id = zenpy.solved_tickets_user_id
else: else:
ticket.assignee = None ticket.assignee = None
ticket.group_id = zenpy.buffer_group_id ticket.group_id = zenpy.buffer_group_id
if tickets:
if tickets.count: zenpy.admin.tickets.update(tickets)
zenpy.admin.tickets.update(tickets.values) attempts, success = 20, False
attempts, success = 5, False
while not success and attempts != 0: while not success and attempts != 0:
try: try:
update_role(user_profile, ROLES['light_agent'], who_changes) update_role(user_profile, ROLES['light_agent'], who_changes)
@ -86,8 +83,7 @@ def make_light_agent(user_profile: UserProfile, who_changes: get_user_model()) -
def get_users_list() -> list: def get_users_list() -> list:
""" """
Функция **get_users_list** возвращает список Функция **get_users_list** возвращает список пользователей Zendesk, относящихся к организации SYSTEM.
пользователей Zendesk, относящихся к организации SYSTEM.
""" """
zendesk = zenpy zendesk = zenpy
@ -101,7 +97,14 @@ def get_tickets_list(email) -> list:
""" """
Функция возвращает список тикетов пользователя Zendesk Функция возвращает список тикетов пользователя Zendesk
""" """
return zenpy.admin.search(assignee=email, type='ticket') return TicketListRequester().get_tickets_list_for_user(zenpy.get_user(email))
def get_tickets_list_for_group(group_name):
"""
Функция возвращает список неназначенных, нерешённых тикетов группы Zendesk
"""
return TicketListRequester().get_tickets_list_for_group(zenpy.get_group(group_name))
def update_profile(user_profile: UserProfile) -> None: def update_profile(user_profile: UserProfile) -> None:
@ -176,7 +179,7 @@ def update_user_in_model(profile: UserProfile, zendesk_user: ZenpyUser) -> None:
def count_users(users: list) -> tuple: def count_users(users: list) -> tuple:
""" """
Функция подсчета количества сотрудников с ролями engineer и light_agent. Функция подсчета количества сотрудников с ролями engineer и light_agent
""" """
engineers, light_agents = 0, 0 engineers, light_agents = 0, 0
for user in users: for user in users:
@ -189,7 +192,7 @@ def count_users(users: list) -> tuple:
def update_users_in_model() -> list: def update_users_in_model() -> list:
""" """
Обновляет пользователей в модели UserProfile по списку пользователей в организации. Обновляет пользователей в модели UserProfile по списку пользователей в организации
""" """
users = get_users_list() users = get_users_list()
for user in users: for user in users:
@ -282,20 +285,19 @@ class StatisticData:
else: else:
self.statistic = stat self.statistic = stat
def get_statistic(self) -> Optional[dict]: def get_statistic(self) -> dict:
""" """
Функция возвращает статистику работы пользователя. Функция возвращает статистику работы пользователя.
:return: Словарь statistic с применением формата отображения :return: Словарь statistic с применением формата отображения и интервала работы(если они есть). None, если были ошибки при создании.
и интервала работы(если они есть).
None, если были ошибки при создании.
""" """
if self.is_valid_statistic(): if self.is_valid_statistic():
stat = self.statistic stat = self.statistic
stat = self._use_display(stat) stat = self._use_display(stat)
stat = self._use_interval(stat) stat = self._use_interval(stat)
return stat return stat
return None else:
return None
def is_valid_statistic(self) -> bool: def is_valid_statistic(self) -> bool:
""" """
@ -337,7 +339,8 @@ class StatisticData:
""" """
if self.is_valid_data(): if self.is_valid_data():
return self.data return self.data
return None else:
return None
def is_valid_data(self) -> bool: def is_valid_data(self) -> bool:
""" """
@ -377,12 +380,9 @@ class StatisticData:
if self.interval == 'months': if self.interval == 'months':
# Переделываем ключи под формат('началоесяца - конец_месяца') # Переделываем ключи под формат('началоесяца - конец_месяца')
for key, value in stat.items(): for key, value in stat.items():
current_month_start = max(self.start_date, date( current_month_start = max(self.start_date, date(year=key.year, month=key.month, day=1))
year=key.year, month=key.month, day=1)) current_month_end = min(self.end_date, last_day_of_month(date(year=key.year, month=key.month, day=1)))
current_month_end = min(self.end_date, last_day_of_month( index = ' - '.join([str(current_month_start), str(current_month_end)])
date(year=key.year, month=key.month, day=1)))
index = ' - '.join([str(current_month_start),
str(current_month_end)])
if new_stat.get(index): if new_stat.get(index):
new_stat[index] += value new_stat[index] += value
else: else:
@ -401,12 +401,11 @@ class StatisticData:
return False return False
return True return True
def _init_data(self) -> None: def _init_data(self):
""" """
Функция возвращает логи в диапазоне дат start_date - end_date для пользователя с указанным email. Функция возвращает логи в диапазоне дат start_date - end_date для пользователя с указанным email.
:return: Данные о смене статусов пользователя. Если пользователь не найден или интервал времени :return: Данные о смене статусов пользователя. Если пользователь не найден или интервал времени некорректен - ошибку.
некорректен - ошибку.
""" """
if not self.check_time(): if not self.check_time():
self.errors += ['Конец диапазона должен быть позже начала диапазона и раньше текущего времени'] self.errors += ['Конец диапазона должен быть позже начала диапазона и раньше текущего времени']
@ -414,12 +413,12 @@ class StatisticData:
try: try:
self.data = RoleChangeLogs.objects.filter( self.data = RoleChangeLogs.objects.filter(
change_time__range=[self.start_date, self.end_date + timedelta(days=1)], change_time__range=[self.start_date, self.end_date + timedelta(days=1)],
user=get_user_model().objects.get(email=self.email), user=User.objects.get(email=self.email),
).order_by('change_time') ).order_by('change_time')
except get_user_model().DoesNotExist: except User.DoesNotExist:
self.errors += ['Пользователь не найден'] self.errors += ['Пользователь не найден']
def _init_statistic(self) -> None: def _init_statistic(self) -> dict:
""" """
Функция заполняет словарь, в котором ключ - дата, значение - кол-во проработанных в этот день секунд. Функция заполняет словарь, в котором ключ - дата, значение - кол-во проработанных в этот день секунд.
@ -428,7 +427,7 @@ class StatisticData:
self.clear_statistic() self.clear_statistic()
if not self.get_data(): if not self.get_data():
self.warnings += ['Не обнаружены изменения роли в данном промежутке'] self.warnings += ['Не обнаружены изменения роли в данном промежутке']
return return None
first_log, last_log = self.data[0], self.data[len(self.data) - 1] first_log, last_log = self.data[0], self.data[len(self.data) - 1]
if first_log.old_role == ROLES['engineer']: if first_log.old_role == ROLES['engineer']:
@ -441,57 +440,44 @@ class StatisticData:
if self.data[log_index].new_role == ROLES['engineer']: if self.data[log_index].new_role == ROLES['engineer']:
self.engineer_logic(log_index) self.engineer_logic(log_index)
def engineer_logic(self, log_index: int) -> None: def engineer_logic(self, log_index):
""" """
Функция обрабатывает основную часть работы инженера. Функция обрабатывает основную часть работы инженера
:param log_index: Индекс текущего лога
""" """
current_log, next_log = self.data[log_index], self.data[log_index + 1] current_log, next_log = self.data[log_index], self.data[log_index + 1]
if current_log.change_time.date() != next_log.change_time.date(): if current_log.change_time.date() != next_log.change_time.date():
self.statistic[current_log.change_time.date()] += ( self.statistic[current_log.change_time.date()] += (
timedelta(days=1) - get_timedelta(current_log)).total_seconds() timedelta(days=1) - get_timedelta(current_log)).total_seconds()
self.statistic[next_log.change_time.date( self.statistic[next_log.change_time.date()] += get_timedelta(next_log).total_seconds()
)] += get_timedelta(next_log).total_seconds() self.fill_daterange(current_log.change_time.date() + timedelta(days=1), next_log.change_time.date())
self.fill_daterange(current_log.change_time.date(
) + timedelta(days=1), next_log.change_time.date())
else: else:
elapsed_time = next_log.change_time - current_log.change_time elapsed_time = next_log.change_time - current_log.change_time
self.statistic[current_log.change_time.date( self.statistic[current_log.change_time.date()] += elapsed_time.total_seconds()
)] += elapsed_time.total_seconds()
def post_engineer_logic(self, last_log: RoleChangeLogs) -> None: def post_engineer_logic(self, last_log):
""" """
Функция обрабатывает случай, когда нам известно что инженер работал и после диапазона. Функция обрабатывает случай, когда нам изветсно что инженер работал и после диапазона
:param last_log: Последний лог
""" """
self.fill_daterange(last_log.change_time.date( self.fill_daterange(last_log.change_time.date() + timedelta(days=1), self.end_date + timedelta(days=1))
) + timedelta(days=1), self.end_date + timedelta(days=1))
if last_log.change_time.date() == timezone.now().date(): if last_log.change_time.date() == timezone.now().date():
self.statistic[last_log.change_time.date()] += ( self.statistic[last_log.change_time.date()] += (
get_timedelta(None, timezone.now().time()) - get_timedelta(None, timezone.now().time()) - get_timedelta(last_log)
get_timedelta(last_log)
).total_seconds() ).total_seconds()
else: else:
self.statistic[last_log.change_time.date()] += ( self.statistic[last_log.change_time.date()] += (
timedelta(days=1) - get_timedelta(last_log)).total_seconds() timedelta(days=1) - get_timedelta(last_log)).total_seconds()
if self.end_date == timezone.now().date(): if self.end_date == timezone.now().date():
self.statistic[self.end_date] = get_timedelta( self.statistic[self.end_date] = get_timedelta(None, timezone.now().time()).total_seconds()
None, timezone.now().time()).total_seconds()
def prev_engineer_logic(self, first_log: RoleChangeLogs) -> None: def prev_engineer_logic(self, first_log):
""" """
Функция обрабатывает случай, когда нам извеcтно, что инженер начал работу до диапазона. Функция обрабатывает случай, когда нам изветсно что инженер начал работу до диапазона
:param first_log: Первый лог
""" """
self.fill_daterange(max(get_user_model().objects.get(email=self.email).date_joined.date(), self.start_date), self.fill_daterange(max(User.objects.get(email=self.email).date_joined.date(), self.start_date),
first_log.change_time.date()) first_log.change_time.date())
self.statistic[first_log.change_time.date( self.statistic[first_log.change_time.date()] += get_timedelta(first_log).total_seconds()
)] += get_timedelta(first_log).total_seconds()
def fill_daterange(self, first: date, last: date, val: int = 24 * 3600) -> None: def fill_daterange(self, first: date, last: date, val: int = 24 * 3600) -> dict:
""" """
Функция заполняет диапазон дат значением val (по умолчанию val = кол-во секунд в 1 дне). Функция заполняет диапазон дат значением val (по умолчанию val = кол-во секунд в 1 дне).
@ -502,13 +488,12 @@ class StatisticData:
for day in daterange(first, last): for day in daterange(first, last):
self.statistic[day] = val self.statistic[day] = val
def clear_statistic(self) -> None: def clear_statistic(self) -> dict:
""" """
Функция осуществляет обновление всех дней. Функция осуществляет обновление всех дней.
""" """
self.statistic.clear() self.statistic.clear()
self.fill_daterange( self.fill_daterange(self.start_date, self.end_date + timedelta(days=1), 0)
self.start_date, self.end_date + timedelta(days=1), 0)
class DatabaseHandler(logging.Handler): class DatabaseHandler(logging.Handler):
@ -518,12 +503,7 @@ class DatabaseHandler(logging.Handler):
def __init__(self): def __init__(self):
logging.Handler.__init__(self) logging.Handler.__init__(self)
def emit(self, record: logging.LogRecord) -> None: def emit(self, record):
"""
Функция осуществляет запись об изменении роли пользователя.
:param record: Запись в сущность main.rolchangelogs
"""
database = RoleChangeLogs() database = RoleChangeLogs()
users = record.msg users = record.msg
if users[1]: if users[1]:
@ -577,7 +557,7 @@ class CsvFormatter(logging.Formatter):
return msg return msg
def log(user: get_user_model(), admin: int = 0) -> None: def log(user, admin=None):
""" """
Функция осуществляет запись логов в базу данных и csv файл. Функция осуществляет запись логов в базу данных и csv файл.

38
main/requester.py Normal file
View File

@ -0,0 +1,38 @@
import requests
from zenpy import TicketApi
from zenpy.lib.api_objects import Ticket
from main.zendesk_admin import zenpy
class TicketListRequester:
def __init__(self):
self.email = zenpy.credentials['email']
if zenpy.credentials.get('token'):
self.token_or_password = zenpy.credentials.get('token')
self.email += '/token'
else:
self.token_or_password = zenpy.credentials.get('password')
self.prefix = f'https://{zenpy.credentials.get("subdomain")}.zendesk.com/api/v2/'
def get_tickets_list_for_user(self, zendesk_user):
url = self.prefix + f'users/{zendesk_user.id}/tickets/assigned'
return self._get_tickets(url)
def get_tickets_list_for_group(self, group):
url = self.prefix + '/tickets'
all_tickets = self._get_tickets(url)
tickets = list()
for ticket in all_tickets:
if (ticket.status != 'solved') and (ticket.group_id == group.id) and (ticket.assignee_id is None):
tickets.append(ticket)
return tickets
def _get_tickets(self, url):
response = requests.get(url, auth=(self.email, self.token_or_password))
tickets = []
if response.status_code != 200:
return None
for ticket in response.json()['tickets']:
tickets.append(Ticket(api=TicketApi, **ticket))
return tickets

261
main/statistic_data.py Normal file
View File

@ -0,0 +1,261 @@
from datetime import date, datetime, timedelta
from typing import Optional
from django.contrib.auth.models import User
from django.utils import timezone
from access_controller.settings import ONE_DAY, ZENDESK_ROLES as ROLES
from main.extra_func import last_day_of_month, get_timedelta, daterange
from main.models import RoleChangeLogs
class StatisticData:
"""
Класс для учета статистики интервалов работы пользователей.
Передаваемые параметры: start_date, end_date, email, stat.
:param display: Формат отображения времени (часы, минуты)
:type display: :class:`list`
:param interval: Интервал времени в часах и минутах
:type interval: :class:`list`
:param start_date: Дата начала работы
:type start_date: :class:`date`
:param end_date: Дата окончания работы
:type end_date: :class:`date`
:param email: Email пользователя
:type email: :class:`str`
:param errors: Список ошибок
:type errors: :class:`list`
:param warnings: Список предупреждений
:type warnings: :class:`list`
:param data: Ретроспектива смены ролей пользователя
:type data: :class:`dict`
:param statistic: Интервалы работы пользователя
:type statistic: :class:`dict`
"""
def __init__(self, start_date, end_date, user_email, stat=None):
self.display = None
self.interval = None
self.start_date = start_date
self.end_date = end_date
self.email = user_email
self.errors = list()
self.warnings = list()
self.data = dict()
self.statistic = dict()
self._init_data()
if stat is None:
self._init_statistic()
else:
self.statistic = stat
def get_statistic(self) -> dict:
"""
Функция возвращает статистику работы пользователя.
:return: Словарь statistic с применением формата отображения и интервала работы(если они есть). None, если были ошибки при создании.
"""
if self.is_valid_statistic():
stat = self.statistic
stat = self._use_display(stat)
stat = self._use_interval(stat)
return stat
else:
return None
def is_valid_statistic(self) -> bool:
"""
Функция проверяет были ли ошибки при создании статистики.
:return: True, при отсутствии ошибок
"""
return not self.errors and self.statistic
def set_interval(self, interval: list) -> bool:
"""
Функция проверяет корректность представления интервала работы.
:param interval: Интервал должен быть указан в днях или месяцах.
:return: True, если указан верно
"""
if interval not in ['months', 'days']:
self.errors += ['Интервал работы должен быть в днях или месяцах']
return False
self.interval = interval
return True
def set_display(self, display_format: list) -> bool:
"""
Функция проверяет корректность формата отображения интервала.
:param display_format: Формат отображения должен быть указан в днях или месяцах.
:return: True, если указан верно
"""
if display_format not in ['days', 'hours']:
self.errors += ['Формат отображения должен быть в часах или днях']
return False
self.display = display_format
return True
def get_data(self) -> Optional[dict]:
"""
Функция возвращает данные - список объектов RoleChangeLogs.
"""
if self.is_valid_data():
return self.data
else:
return None
def is_valid_data(self) -> bool:
"""
Функция определяет были ли ошибки при получении логов.
:return: True, если ошибок нет
"""
return not self.errors
def _use_display(self, stat: list) -> list:
"""
Функция приводит данные к формату отображения.
:param stat: Список данных статистики пользователя
:return: Обновленный список
"""
if not self.is_valid_statistic() or not self.display:
return stat
new_stat = {}
for key, item in stat.items():
if self.display == 'hours':
new_stat[key] = item / 3600
elif self.display == 'days':
new_stat[key] = item / (ONE_DAY * 3600)
return new_stat
def _use_interval(self, stat: dict) -> dict:
"""
Функция объединяет ключи и значения в соответствии с интервалом работы.
:param stat: Статистика работы пользователя
:return: Обновленная статистика
"""
if not self.is_valid_statistic() or not self.interval:
return stat
new_stat = {}
if self.interval == 'months':
# Переделываем ключи под формат('началоесяца - конец_месяца')
for key, value in stat.items():
current_month_start = max(self.start_date, date(year=key.year, month=key.month, day=1))
current_month_end = min(self.end_date, last_day_of_month(date(year=key.year, month=key.month, day=1)))
index = ' - '.join([str(current_month_start), str(current_month_end)])
if new_stat.get(index):
new_stat[index] += value
else:
new_stat[index] = value
elif self.interval == 'days':
new_stat = stat # статистика изначально в днях
return new_stat
def check_time(self) -> bool:
"""
Функция проверяет корректность введенного времени.
:return: True, если время указано корректно. Иначе, False
"""
if self.end_date < self.start_date or self.end_date > datetime.now().date():
return False
return True
def _init_data(self):
"""
Функция возвращает логи в диапазоне дат start_date - end_date для пользователя с указанным email.
:return: Данные о смене статусов пользователя. Если пользователь не найден или интервал времени некорректен - ошибку.
"""
if not self.check_time():
self.errors += ['Конец диапазона должен быть позже начала диапазона и раньше текущего времени']
return
try:
self.data = RoleChangeLogs.objects.filter(
change_time__range=[self.start_date, self.end_date + timedelta(days=1)],
user=User.objects.get(email=self.email),
).order_by('change_time')
except User.DoesNotExist:
self.errors += ['Пользователь не найден']
def _init_statistic(self) -> dict:
"""
Функция заполняет словарь, в котором ключ - дата, значение - кол-во проработанных в этот день секунд.
:return: Статистика работы пользователя (statistic)
"""
self.clear_statistic()
if not self.get_data():
self.warnings += ['Не обнаружены изменения роли в данном промежутке']
return None
first_log, last_log = self.data[0], self.data[len(self.data) - 1]
if first_log.old_role == ROLES['engineer']:
self.prev_engineer_logic(first_log)
if last_log.new_role == ROLES['engineer']:
self.post_engineer_logic(last_log)
for log_index in range(len(self.data) - 1):
if self.data[log_index].new_role == ROLES['engineer']:
self.engineer_logic(log_index)
def engineer_logic(self, log_index):
"""
Функция обрабатывает основную часть работы инженера
"""
current_log, next_log = self.data[log_index], self.data[log_index + 1]
if current_log.change_time.date() != next_log.change_time.date():
self.statistic[current_log.change_time.date()] += (
timedelta(days=1) - get_timedelta(current_log)).total_seconds()
self.statistic[next_log.change_time.date()] += get_timedelta(next_log).total_seconds()
self.fill_daterange(current_log.change_time.date() + timedelta(days=1), next_log.change_time.date())
else:
elapsed_time = next_log.change_time - current_log.change_time
self.statistic[current_log.change_time.date()] += elapsed_time.total_seconds()
def post_engineer_logic(self, last_log):
"""
Функция обрабатывает случай, когда нам изветсно что инженер работал и после диапазона
"""
self.fill_daterange(last_log.change_time.date() + timedelta(days=1), self.end_date + timedelta(days=1))
if last_log.change_time.date() == timezone.now().date():
self.statistic[last_log.change_time.date()] += (
get_timedelta(None, timezone.now().time()) - get_timedelta(last_log)
).total_seconds()
else:
self.statistic[last_log.change_time.date()] += (
timedelta(days=1) - get_timedelta(last_log)).total_seconds()
if self.end_date == timezone.now().date():
self.statistic[self.end_date] = get_timedelta(None, timezone.now().time()).total_seconds()
def prev_engineer_logic(self, first_log):
"""
Функция обрабатывает случай, когда нам изветсно что инженер начал работу до диапазона
"""
self.fill_daterange(max(User.objects.get(email=self.email).date_joined.date(), self.start_date),
first_log.change_time.date())
self.statistic[first_log.change_time.date()] += get_timedelta(first_log).total_seconds()
def fill_daterange(self, first: date, last: date, val: int = 24 * 3600) -> dict:
"""
Функция заполняет диапазон дат значением val (по умолчанию val = кол-во секунд в 1 дне).
:param first: Начальная дата интервала
:param last: Последняя дата интервала
:param val: Количество секунд в одном дне
"""
for day in daterange(first, last):
self.statistic[day] = val
def clear_statistic(self) -> dict:
"""
Функция осуществляет обновление всех дней.
"""
self.statistic.clear()
self.fill_daterange(self.start_date, self.end_date + timedelta(days=1), 0)

View File

@ -3,56 +3,62 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<nav class="navbar navbar-light" style="background-color: #113A60;"> <nav class="navbar navbar-light py-3" style="background-color: #113A60;">
<a class="navbar-brand" href="{% url 'index' %}"> <a class="navbar-brand" href="{% url 'index' %}">
<img src="{% static 'main/img/logo_real.png' %}" width="107" height="22" class="d-inline-block align-top" style="margin-left: 15px" alt="" loading="lazy"> <img src="{% static 'main/img/logo_real.png' %}" width="107" height="22" class="d-inline-block align-top" style="margin-left: 15px" alt="" loading="lazy">
<t style="color:#FFFFFF">Access Controller</t> <t class="px-2" style="color:#FFFFFF">Access Controller</t>
</a> </a>
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
<div class="btn-group" role="group" aria-label="Basic example" style="margin-right: 9px"> <div class="btn-group" role="group" aria-label="Basic example" style="margin-right: 9px">
<a {% if profile_lit %} {% url 'profile' as profile_url %}
<a {% if request.path == profile_url %}
class="btn btn-primary" class="btn btn-primary"
{% else %} {% else %}
class="btn btn-secondary" class="btn btn-secondary"
{% endif %} {% endif %}
href="{% url 'profile' %}">Профиль</a> href="{{ profile_url }}">Профиль</a>
{% if perms.main.has_control_access %} {% if perms.main.has_control_access %}
<a {% if control_lit %} {% url 'control' as control_url %}
<a {% if request.path == control_url %}
class="btn btn-primary" class="btn btn-primary"
{% else %} {% else %}
class="btn btn-secondary" class="btn btn-secondary"
{% endif %} {% endif %}
href="{% url 'control' %}">Управление</a> href="{{ control_url }}">Управление</a>
<a {% if stats_lit %} {% url 'statistic' as statistic_url %}
<a {% if request.path == statistic_url %}
class="btn btn-primary" class="btn btn-primary"
{% else %} {% else %}
class="btn btn-secondary" class="btn btn-secondary"
{% endif %} {% endif %}
href="{% url 'statistic' %}">Статистика</a> href="{{ statistic_url }}">Статистика</a>
{% else %} {% else %}
<a {% if work_lit %} {% url 'work' request.user.id as work_url %}
<a {% if request.path == work_url %}
class="btn btn-primary" class="btn btn-primary"
{% else %} {% else %}
class="btn btn-secondary" class="btn btn-secondary"
{% endif %} {% endif %}
href="{% url 'work' request.user.id %}">Запрос прав</a> href="{{ work_url }}">Запрос прав</a>
{% endif %} {% endif %}
<a class="btn btn-secondary" href="{% url 'logout' %}">Выйти</a> <a class="btn btn-secondary" href="{% url 'logout' %}">Выйти</a>
</div> </div>
{% else %} {% else %}
<div class="btn-group" role="group" aria-label="Basic example" style="margin-right: 9px"> <div class="btn-group" role="group" aria-label="Basic example" style="margin-right: 9px">
<a {% if login_lit %} {% url 'login' as login_url %}
<a {% if request.path == login_url %}
class="btn btn-primary" class="btn btn-primary"
{% else %} {% else %}
class="btn btn-secondary" class="btn btn-secondary"
{% endif %} {% endif %}
href="/accounts/login">Войти</a> href="{{ login_url }}">Войти</a>
<a {% if registration_lit %} {% url 'registration' as registration_url %}
<a {% if request.path == registration_url %}
class="btn btn-primary" class="btn btn-primary"
{% else %} {% else %}
class="btn btn-secondary" class="btn btn-secondary"
{% endif %} {% endif %}
href="/accounts/register">Зарегистрироваться</a> href="{{ registration_url }}">Зарегистрироваться</a>
</div> </div>
{% endif %} {% endif %}
</nav> </nav>

View File

@ -17,9 +17,9 @@
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></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> <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="{% static 'modules/notifications/dist/notifications.js' %}"></script>
<script src="{% static 'main/js/control.js'%}" type="text/babel"></script> <script src="{% static 'main/js/control.js'%}" type="text/babel"></script>
<script src="{% static 'main/js/notifications.js' %}"></script> <script src="{% static 'main/js/notifications.js' %}"></script> {# Для #}
<script src="{% static 'modules/notifications/dist/notifications.js' %}"></script> {# Уведомлений #}
{% endblock%} {% endblock%}
{% block content %} {% block content %}
<div class="container-md"> <div class="container-md">

View File

@ -3,12 +3,13 @@
""" """
from unittest.mock import patch
from urllib.parse import urlparse from urllib.parse import urlparse
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.core import mail from django.core import mail
from django.test import TestCase, Client from django.test import TestCase, Client
from django.urls import reverse from django.urls import reverse, reverse_lazy
from django.utils import translation from django.utils import translation
import access_controller.settings as sets import access_controller.settings as sets
@ -97,7 +98,7 @@ class RegistrationTestCase(TestCase):
""" """
with self.settings(EMAIL_BACKEND=self.email_backend): with self.settings(EMAIL_BACKEND=self.email_backend):
self.client.post(reverse('registration'), data={'email': self.any_zendesk_user_email}) self.client.post(reverse('registration'), data={'email': self.any_zendesk_user_email})
user = get_user_model().objects.get(email=self.any_zendesk_user_email) user = User.objects.get(email=self.any_zendesk_user_email)
zendesk_user = zenpy.get_user(self.any_zendesk_user_email) zendesk_user = zenpy.get_user(self.any_zendesk_user_email)
self.assertEqual(user.userprofile.name, zendesk_user.name) self.assertEqual(user.userprofile.name, zendesk_user.name)
@ -107,6 +108,72 @@ class RegistrationTestCase(TestCase):
""" """
with self.settings(EMAIL_BACKEND=self.email_backend): with self.settings(EMAIL_BACKEND=self.email_backend):
self.client.post(reverse('registration'), data={'email': self.zendesk_admin_email}) self.client.post(reverse('registration'), data={'email': self.zendesk_admin_email})
user = get_user_model().objects.get(email=self.zendesk_admin_email) user = User.objects.get(email=self.zendesk_admin_email)
self.assertEqual(user.userprofile.role, 'admin') self.assertEqual(user.userprofile.role, 'admin')
self.assertTrue(user.has_perm('main.has_control_access')) self.assertTrue(user.has_perm('main.has_control_access'))
class MakeEngineerTestCase(TestCase):
fixtures = ['fixtures/test_make_engineer.json']
def setUp(self):
self.light_agent = '123@test.ru'
self.admin = 'admin@gmail.com'
self.engineer = 'customer@example.com'
self.client = Client()
self.client.force_login(User.objects.get(email=self.light_agent))
self.admin_client = Client()
self.admin_client.force_login(User.objects.get(email=self.admin))
@patch('main.extra_func.zenpy')
def test_redirect(self, ZenpyMock):
user = User.objects.get(email=self.light_agent)
resp = self.client.post(reverse_lazy('work_become_engineer'))
self.assertRedirects(resp, reverse('work', args=[user.id]))
self.assertEqual(resp.status_code, 302)
@patch('main.extra_func.zenpy')
def test_light_agent_make_engineer(self, ZenpyMock):
self.client.post(reverse_lazy('work_become_engineer'))
self.assertEqual(ZenpyMock.update_user.call_args[0][0].custom_role_id, sets.ZENDESK_ROLES['engineer'])
@patch('main.extra_func.zenpy')
def test_admin_make_engineer(self, ZenpyMock):
self.admin_client.post(reverse_lazy('work_become_engineer'))
self.assertEqual(ZenpyMock.update_user.call_args[0][0].custom_role_id, sets.ZENDESK_ROLES['engineer'])
@patch('main.extra_func.zenpy')
def test_engineer_make_engineer(self, ZenpyMock):
client = Client()
client.force_login(User.objects.get(email=self.engineer))
client.post(reverse_lazy('work_become_engineer'))
self.assertEqual(ZenpyMock.update_user.call_args[0][0].custom_role_id, sets.ZENDESK_ROLES['engineer'])
@patch('main.extra_func.zenpy')
def test_control_page_make_one(self, ZenpyMock):
self.admin_client.post(
reverse_lazy('control'),
data={'users': [User.objects.get(email=self.light_agent).userprofile.id], 'engineer': 'engineer'}
)
call_list = ZenpyMock.update_user.call_args_list
mock_object = call_list[0][0][0]
self.assertEqual(len(call_list), 1)
self.assertEqual(mock_object.custom_role_id, sets.ZENDESK_ROLES['engineer'])
@patch('main.extra_func.zenpy')
def test_control_page_make_many(self, ZenpyMock):
self.admin_client.post(
reverse_lazy('control'),
data={
'users': [
User.objects.get(email=self.light_agent).userprofile.id,
User.objects.get(email=self.engineer).userprofile.id,
],
'engineer': 'engineer'
}
)
call_list = ZenpyMock.update_user.call_args_list
mock_objects = list(call_list)
self.assertEqual(len(call_list), 2)
for obj in mock_objects:
self.assertEqual(obj[0][0].custom_role_id, sets.ZENDESK_ROLES['engineer'])

View File

@ -19,17 +19,20 @@ from django.contrib.messages.views import SuccessMessageMixin
from django.core.handlers.wsgi import WSGIRequest from django.core.handlers.wsgi import WSGIRequest
from django.http import HttpResponseRedirect, HttpResponse from django.http import HttpResponseRedirect, HttpResponse
from django.shortcuts import render, redirect from django.shortcuts import render, redirect
from django.urls import reverse_lazy from django.urls import reverse_lazy, reverse
from django.views.generic import FormView from django.views.generic import FormView
from django_registration.views import RegistrationView from django_registration.views import RegistrationView
# Django REST # Django REST
from rest_framework import viewsets from rest_framework import viewsets
from rest_framework.response import Response from rest_framework.response import Response
from access_controller.settings import DEFAULT_FROM_EMAIL, ZENDESK_ROLES, ZENDESK_MAX_AGENTS from access_controller.settings import DEFAULT_FROM_EMAIL, ZENDESK_ROLES, ZENDESK_MAX_AGENTS, ZENDESK_GROUPS
from main.extra_func import check_user_exist, update_profile, get_user_organization, make_engineer, make_light_agent, \ from main.extra_func import check_user_exist, update_profile, get_user_organization, \
get_users_list, update_users_in_model, count_users, StatisticData, log, set_session_params_for_work_page make_engineer, make_light_agent, get_users_list, update_users_in_model, count_users, \
log, set_session_params_for_work_page, get_tickets_list_for_group
from .statistic_data import StatisticData
from main.zendesk_admin import zenpy from main.zendesk_admin import zenpy
from main.requester import TicketListRequester
from main.forms import AdminPageUsers, CustomRegistrationForm, CustomAuthenticationForm, StatisticForm from main.forms import AdminPageUsers, CustomRegistrationForm, CustomAuthenticationForm, StatisticForm
from main.serializers import ProfileSerializer, ZendeskUserSerializer from main.serializers import ProfileSerializer, ZendeskUserSerializer
from .models import UserProfile from .models import UserProfile
@ -66,8 +69,7 @@ class CustomRegistrationView(RegistrationView):
:type template_name: :class:`str` :type template_name: :class:`str`
:param success_url: Указание пути к html-странице завершения регистрации :param success_url: Указание пути к html-странице завершения регистрации
:type success_url: :class:`django.utils.functional.lazy.<locals>.__proxy__` :type success_url: :class:`django.utils.functional.lazy.<locals>.__proxy__`
:param is_allowed: Определение зарегистрирован ли пользователь с введенным email на Zendesk :param is_allowed: Определение зарегистрирован ли пользователь с введенным email на Zendesk и принадлежит ли он к организации SYSTEM
и принадлежит ли он к организации SYSTEM
:type is_allowed: :class:`bool` :type is_allowed: :class:`bool`
""" """
extra_context = setup_context(registration_lit=True) extra_context = setup_context(registration_lit=True)
@ -89,7 +91,7 @@ class CustomRegistrationView(RegistrationView):
3. Создается пользователь class User, а также его профиль. 3. Создается пользователь class User, а также его профиль.
:param form: Email пользователя на Zendesk :param form: Email пользователя на Zendesk
:return: User :return: user
""" """
self.redirect_url = 'done' self.redirect_url = 'done'
if check_user_exist(form.data['email']) and get_user_organization(form.data['email']) == 'SYSTEM': if check_user_exist(form.data['email']) and get_user_organization(form.data['email']) == 'SYSTEM':
@ -171,12 +173,11 @@ def profile_page(request: WSGIRequest) -> HttpResponse:
""" """
user_profile: UserProfile = request.user.userprofile user_profile: UserProfile = request.user.userprofile
update_profile(user_profile) update_profile(user_profile)
context = setup_context(profile_lit=True) context = {
context.update({
'profile': user_profile, 'profile': user_profile,
'pagename': 'Страница профиля', 'pagename': 'Страница профиля',
'ZENDESK_ROLES': ZENDESK_ROLES, 'ZENDESK_ROLES': ZENDESK_ROLES,
}) }
return render(request, 'pages/profile.html', context) return render(request, 'pages/profile.html', context)
@ -208,14 +209,13 @@ def work_page(request: WSGIRequest, required_id: int) -> HttpResponse:
engineers.append(user) engineers.append(user)
elif user.custom_role_id == ZENDESK_ROLES['light_agent']: elif user.custom_role_id == ZENDESK_ROLES['light_agent']:
light_agents.append(user) light_agents.append(user)
context = setup_context(work_lit=True) context = {
context.update({
'engineers': engineers, 'engineers': engineers,
'agents': light_agents, 'agents': light_agents,
'messages': messages.get_messages(request), 'messages': messages.get_messages(request),
'licences_remaining': max(0, ZENDESK_MAX_AGENTS - len(engineers)), 'licences_remaining': max(0, ZENDESK_MAX_AGENTS - len(engineers)),
'pagename': 'Управление правами', 'pagename': 'Управление правами',
}) }
return render(request, 'pages/work.html', context) return render(request, 'pages/work.html', context)
return redirect("login") return redirect("login")
@ -235,12 +235,12 @@ def work_hand_over(request: WSGIRequest) -> HttpResponseRedirect:
@login_required() @login_required()
def work_become_engineer(request: WSGIRequest) -> HttpResponseRedirect: def work_become_engineer(request: WSGIRequest) -> HttpResponseRedirect:
""" """
Функция позволяет текущему пользователю получить права, а именно сменить в Zendesk роль с "light_agent" Функция позволяет текущему пользователю получить права, а именно сменить в Zendesk роль с "light_agent" на "engineer"
на "engineer".
:param request: данные текущего пользователя (login_required) :param request: данные текущего пользователя (login_required)
:return: перезагрузка текущей страницы после выполнения смены роли :return: перезагрузка текущей страницы после выполнения смены роли
""" """
make_engineer(request.user.userprofile, request.user) make_engineer(request.user.userprofile, request.user)
return set_session_params_for_work_page(request) return set_session_params_for_work_page(request)
@ -254,15 +254,19 @@ def work_get_tickets(request: WSGIRequest) -> HttpResponse:
""" """
zenpy_user = zenpy.get_user(request.user.email) zenpy_user = zenpy.get_user(request.user.email)
if zenpy_user.role == 'admin' or zenpy_user.custom_role_id == ZENDESK_ROLES['engineer']: if zenpy_user.role == 'admin' or zenpy_user.custom_role_id == ZENDESK_ROLES['engineer']:
tickets = [ticket for ticket in zenpy.admin.search(type="ticket") if tickets = get_tickets_list_for_group(ZENDESK_GROUPS['buffer'])
ticket.group.name == 'Сменная группа' and ticket.assignee is None] assigned_tickets = []
count = 0 count = 0
for i in enumerate(tickets): for i in enumerate(tickets):
if i == int(request.GET.get('count_tickets')): if i == int(request.GET.get('count_tickets')):
if assigned_tickets:
zenpy.admin.tickets.update(assigned_tickets)
return set_session_params_for_work_page(request, count) return set_session_params_for_work_page(request, count)
tickets[i].assignee = zenpy_user tickets[i].assignee = zenpy_user
zenpy.admin.tickets.update(tickets[i]) assigned_tickets.append(tickets[i])
count += 1 count += 1
if assigned_tickets:
zenpy.admin.tickets.update(assigned_tickets)
return set_session_params_for_work_page(request, count) return set_session_params_for_work_page(request, count)
return set_session_params_for_work_page(request, is_confirm=False) return set_session_params_for_work_page(request, is_confirm=False)
@ -307,7 +311,7 @@ class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageM
self.make_light_agents(users) self.make_light_agents(users)
return super().form_valid(form) return super().form_valid(form)
def make_engineers(self, users: list) -> None: def make_engineers(self, users):
""" """
Функция проходит по списку пользователей, проставляя статус "engineer". Функция проходит по списку пользователей, проставляя статус "engineer".
@ -413,11 +417,10 @@ def statistic_page(request: WSGIRequest) -> HttpResponse:
if not request.user.has_perm("main.has_control_access"): if not request.user.has_perm("main.has_control_access"):
return redirect('index') return redirect('index')
context = setup_context(stats_lit=True) context = {
context.update({
'pagename': 'страница статистики', 'pagename': 'страница статистики',
'errors': list(), 'errors': list(),
}) }
if request.method == "POST": if request.method == "POST":
form = StatisticForm(request.POST) form = StatisticForm(request.POST)
if form.is_valid(): if form.is_valid():

View File

@ -3,11 +3,9 @@
""" """
from typing import Optional, Dict from typing import Optional, Dict
from zenpy import Zenpy from zenpy import Zenpy
from zenpy.lib.api_objects import User as ZenpyUser, Group as ZenpyGroup from zenpy.lib.api_objects import User as ZenpyUser, Group as ZenpyGroup
from zenpy.lib.exception import APIException from zenpy.lib.exception import APIException
from access_controller.settings import ACTRL_ZENDESK_SUBDOMAIN, ACTRL_API_EMAIL, ACTRL_API_TOKEN, ACTRL_API_PASSWORD, \ from access_controller.settings import ACTRL_ZENDESK_SUBDOMAIN, ACTRL_API_EMAIL, ACTRL_API_TOKEN, ACTRL_API_PASSWORD, \
ZENDESK_GROUPS, SOLVED_TICKETS_EMAIL ZENDESK_GROUPS, SOLVED_TICKETS_EMAIL
@ -32,6 +30,14 @@ class ZendeskAdmin:
self.buffer_group_id= self.get_group(ZENDESK_GROUPS['buffer']).id self.buffer_group_id= self.get_group(ZENDESK_GROUPS['buffer']).id
self.solved_tickets_user_id = self.get_user(SOLVED_TICKETS_EMAIL).id self.solved_tickets_user_id = self.get_user(SOLVED_TICKETS_EMAIL).id
def update_user(self, user: ZenpyUser) -> bool:
"""
Функция сохраняет изменение пользователя в Zendesk.
:param user: Пользователь с изменёнными данными
"""
self.admin.users.update(user)
def check_user(self, email: str) -> bool: def check_user(self, email: str) -> bool:
""" """
Функция осуществляет проверку существования пользователя в Zendesk по email. Функция осуществляет проверку существования пользователя в Zendesk по email.