From 301924693582ecf8e7d3347a9755bf10655a5a73 Mon Sep 17 00:00:00 2001 From: Sokurov Idar Date: Fri, 23 Apr 2021 00:31:43 +0300 Subject: [PATCH 01/13] Add custom ticket requests --- main/extra_func.py | 267 ++--------------------------------------- main/requester.py | 28 +++++ main/statistic_data.py | 261 ++++++++++++++++++++++++++++++++++++++++ main/views.py | 6 +- main/zendesk_admin.py | 1 - 5 files changed, 300 insertions(+), 263 deletions(-) create mode 100644 main/requester.py create mode 100644 main/statistic_data.py diff --git a/main/extra_func.py b/main/extra_func.py index 86b6739..3f36ef4 100644 --- a/main/extra_func.py +++ b/main/extra_func.py @@ -1,6 +1,5 @@ import logging -from datetime import timedelta, datetime, date -from typing import Optional +from datetime import timedelta from django.contrib.auth.models import User from django.core.exceptions import ObjectDoesNotExist @@ -10,8 +9,9 @@ from zenpy import Zenpy from zenpy.lib.exception import APIException from zenpy.lib.api_objects import User as ZenpyUser, Ticket as ZenpyTicket -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.requester import TicketListRequester from main.zendesk_admin import zenpy @@ -61,10 +61,9 @@ def make_light_agent(user_profile: UserProfile, who_changes: User) -> None: else: ticket.assignee = None ticket.group_id = zenpy.buffer_group_id - - zenpy.admin.tickets.update(tickets.values) - - attempts, success = 5, False + if tickets: + zenpy.admin.tickets.update(tickets) + attempts, success = 20, False while not success and attempts != 0: try: update_role(user_profile, ROLES['light_agent']) @@ -91,7 +90,7 @@ def get_tickets_list(email): """ Функция возвращает список тикетов пользователя Zendesk """ - return zenpy.admin.search(assignee=email, type='ticket') + return TicketListRequester().get_tickets_list_for_user(zenpy.get_user(email)) def update_profile(user_profile: UserProfile): @@ -231,258 +230,6 @@ def last_day_of_month(day: int) -> int: return next_month - timedelta(days=next_month.day) -class StatisticData: - """ - Класс для учета статистики интервалов работы пользователей. - Передаваемые параметры: start_date, end_date, email, stat. - - :param display: Формат отображения времени (часы, минуты) - :type display: :class:`list` - :param interval: Интервал времени в часах и минутах - :type interval: :class:`list` - :param start_date: Дата начала работы - :type start_date: :class:`date` - :param end_date: Дата окончания работы - :type end_date: :class:`date` - :param email: Email пользователя - :type email: :class:`str` - :param errors: Список ошибок - :type errors: :class:`list` - :param warnings: Список предупреждений - :type warnings: :class:`list` - :param data: Ретроспектива смены ролей пользователя - :type data: :class:`dict` - :param statistic: Интервалы работы пользователя - :type statistic: :class:`dict` - """ - - def __init__(self, start_date, end_date, user_email, stat=None): - self.display = None - self.interval = None - self.start_date = start_date - self.end_date = end_date - self.email = user_email - self.errors = list() - self.warnings = list() - self.data = dict() - self.statistic = dict() - self._init_data() - if stat is None: - self._init_statistic() - else: - self.statistic = stat - - def get_statistic(self) -> dict: - """ - Функция возвращает статистику работы пользователя. - - :return: Словарь statistic с применением формата отображения и интервала работы(если они есть). None, если были ошибки при создании. - """ - if self.is_valid_statistic(): - stat = self.statistic - stat = self._use_display(stat) - stat = self._use_interval(stat) - return stat - else: - return None - - def is_valid_statistic(self) -> bool: - """ - Функция проверяет были ли ошибки при создании статистики. - - :return: True, при отсутствии ошибок - """ - return not self.errors and self.statistic - - def set_interval(self, interval: list) -> bool: - """ - Функция проверяет корректность представления интервала работы. - - :param interval: Интервал должен быть указан в днях или месяцах. - :return: True, если указан верно - """ - if interval not in ['months', 'days']: - self.errors += ['Интервал работы должен быть в днях или месяцах'] - return False - self.interval = interval - return True - - def set_display(self, display_format: list) -> bool: - """ - Функция проверяет корректность формата отображения интервала. - - :param display_format: Формат отображения должен быть указан в днях или месяцах. - :return: True, если указан верно - """ - if display_format not in ['days', 'hours']: - self.errors += ['Формат отображения должен быть в часах или днях'] - return False - self.display = display_format - return True - - def get_data(self) -> Optional[dict]: - """ - Функция возвращает данные - список объектов RoleChangeLogs. - """ - if self.is_valid_data(): - return self.data - else: - return None - - def is_valid_data(self) -> bool: - """ - Функция определяет были ли ошибки при получении логов. - - :return: True, если ошибок нет - """ - return not self.errors - - def _use_display(self, stat: list) -> list: - """ - Функция приводит данные к формату отображения. - - :param stat: Список данных статистики пользователя - :return: Обновленный список - """ - if not self.is_valid_statistic() or not self.display: - return stat - new_stat = {} - for key, item in stat.items(): - if self.display == 'hours': - new_stat[key] = item / 3600 - elif self.display == 'days': - new_stat[key] = item / (ONE_DAY * 3600) - return new_stat - - def _use_interval(self, stat: dict) -> dict: - """ - Функция объединяет ключи и значения в соответствии с интервалом работы. - - :param stat: Статистика работы пользователя - :return: Обновленная статистика - """ - if not self.is_valid_statistic() or not self.interval: - return stat - new_stat = {} - if self.interval == 'months': - # Переделываем ключи под формат('начало_месяца - конец_месяца') - for key, value in stat.items(): - current_month_start = max(self.start_date, date(year=key.year, month=key.month, day=1)) - current_month_end = min(self.end_date, last_day_of_month(date(year=key.year, month=key.month, day=1))) - index = ' - '.join([str(current_month_start), str(current_month_end)]) - if new_stat.get(index): - new_stat[index] += value - else: - new_stat[index] = value - elif self.interval == 'days': - new_stat = stat # статистика изначально в днях - return new_stat - - def check_time(self) -> bool: - """ - Функция проверяет корректность введенного времени. - - :return: True, если время указано корректно. Иначе, False - """ - if self.end_date < self.start_date or self.end_date > datetime.now().date(): - return False - return True - - def _init_data(self): - """ - Функция возвращает логи в диапазоне дат start_date - end_date для пользователя с указанным email. - - :return: Данные о смене статусов пользователя. Если пользователь не найден или интервал времени некорректен - ошибку. - """ - if not self.check_time(): - self.errors += ['Конец диапазона должен быть позже начала диапазона и раньше текущего времени'] - return - try: - self.data = RoleChangeLogs.objects.filter( - change_time__range=[self.start_date, self.end_date + timedelta(days=1)], - user=User.objects.get(email=self.email), - ).order_by('change_time') - except User.DoesNotExist: - self.errors += ['Пользователь не найден'] - - def _init_statistic(self) -> dict: - """ - Функция заполняет словарь, в котором ключ - дата, значение - кол-во проработанных в этот день секунд. - - :return: Статистика работы пользователя (statistic) - """ - self.clear_statistic() - if not self.get_data(): - self.warnings += ['Не обнаружены изменения роли в данном промежутке'] - return None - first_log, last_log = self.data[0], self.data[len(self.data) - 1] - - if first_log.old_role == ROLES['engineer']: - self.prev_engineer_logic(first_log) - - if last_log.new_role == ROLES['engineer']: - self.post_engineer_logic(last_log) - - for log_index in range(len(self.data) - 1): - if self.data[log_index].new_role == ROLES['engineer']: - self.engineer_logic(log_index) - - def engineer_logic(self, log_index): - """ - Функция обрабатывает основную часть работы инженера - """ - current_log, next_log = self.data[log_index], self.data[log_index + 1] - if current_log.change_time.date() != next_log.change_time.date(): - self.statistic[current_log.change_time.date()] += ( - timedelta(days=1) - get_timedelta(current_log)).total_seconds() - self.statistic[next_log.change_time.date()] += get_timedelta(next_log).total_seconds() - self.fill_daterange(current_log.change_time.date() + timedelta(days=1), next_log.change_time.date()) - else: - elapsed_time = next_log.change_time - current_log.change_time - self.statistic[current_log.change_time.date()] += elapsed_time.total_seconds() - - def post_engineer_logic(self, last_log): - """ - Функция обрабатывает случай, когда нам изветсно что инженер работал и после диапазона - """ - self.fill_daterange(last_log.change_time.date() + timedelta(days=1), self.end_date + timedelta(days=1)) - if last_log.change_time.date() == timezone.now().date(): - self.statistic[last_log.change_time.date()] += ( - get_timedelta(None, timezone.now().time()) - get_timedelta(last_log) - ).total_seconds() - else: - self.statistic[last_log.change_time.date()] += ( - timedelta(days=1) - get_timedelta(last_log)).total_seconds() - if self.end_date == timezone.now().date(): - self.statistic[self.end_date] = get_timedelta(None, timezone.now().time()).total_seconds() - - def prev_engineer_logic(self, first_log): - """ - Функция обрабатывает случай, когда нам изветсно что инженер начал работу до диапазона - """ - self.fill_daterange(max(User.objects.get(email=self.email).date_joined.date(), self.start_date), - first_log.change_time.date()) - self.statistic[first_log.change_time.date()] += get_timedelta(first_log).total_seconds() - - def fill_daterange(self, first: date, last: date, val: int = 24 * 3600) -> dict: - """ - Функция заполняет диапазон дат значением val (по умолчанию val = кол-во секунд в 1 дне). - - :param first: Начальная дата интервала - :param last: Последняя дата интервала - :param val: Количество секунд в одном дне - """ - for day in daterange(first, last): - self.statistic[day] = val - - def clear_statistic(self) -> dict: - """ - Функция осуществляет обновление всех дней. - """ - self.statistic.clear() - self.fill_daterange(self.start_date, self.end_date + timedelta(days=1), 0) - - class DatabaseHandler(logging.Handler): def __init__(self): logging.Handler.__init__(self) diff --git a/main/requester.py b/main/requester.py new file mode 100644 index 0000000..35f81b6 --- /dev/null +++ b/main/requester.py @@ -0,0 +1,28 @@ +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') + + def get_tickets_list_for_user(self, zendesk_user): + url = f'https://ngenix1612197338.zendesk.com/api/v2/users/{zendesk_user.id}/tickets/assigned' + return self._get_tickets(url) + + 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 diff --git a/main/statistic_data.py b/main/statistic_data.py new file mode 100644 index 0000000..fa1ab24 --- /dev/null +++ b/main/statistic_data.py @@ -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) diff --git a/main/views.py b/main/views.py index d21a6e9..8432e33 100644 --- a/main/views.py +++ b/main/views.py @@ -23,8 +23,10 @@ from rest_framework.response import Response from access_controller.settings import DEFAULT_FROM_EMAIL, ZENDESK_ROLES, ZENDESK_MAX_AGENTS from main.extra_func import check_user_exist, update_profile, get_user_organization, \ make_engineer, make_light_agent, get_users_list, update_users_in_model, count_users, \ - StatisticData, log, set_session_params_for_work_page + log, set_session_params_for_work_page +from .statistic_data import StatisticData from main.zendesk_admin import zenpy +from .requester import TicketListRequester from main.forms import AdminPageUsers, CustomRegistrationForm, CustomAuthenticationForm, StatisticForm from main.serializers import ProfileSerializer, ZendeskUserSerializer from .models import UserProfile @@ -234,8 +236,8 @@ def work_get_tickets(request): if i == int(request.GET.get('count_tickets')): return set_session_params_for_work_page(request, count) tickets[i].assignee = zenpy_user - zenpy.admin.tickets.update(tickets[i]) count += 1 + zenpy.admin.tickets.update(tickets) return set_session_params_for_work_page(request, count) return set_session_params_for_work_page(request, is_confirm=False) diff --git a/main/zendesk_admin.py b/main/zendesk_admin.py index 8ecb877..283d91b 100644 --- a/main/zendesk_admin.py +++ b/main/zendesk_admin.py @@ -3,7 +3,6 @@ from typing import Optional, Dict from zenpy import Zenpy from zenpy.lib.api_objects import User as ZenpyUser, Group as ZenpyGroup from zenpy.lib.exception import APIException - from access_controller.settings import ACTRL_ZENDESK_SUBDOMAIN, ACTRL_API_EMAIL, ACTRL_API_TOKEN, ACTRL_API_PASSWORD, \ ZENDESK_GROUPS, SOLVED_TICKETS_EMAIL From cf2e9ccf212a8f439551a26c5c1ef57f1152b861 Mon Sep 17 00:00:00 2001 From: Sokurov Idar Date: Thu, 6 May 2021 00:41:20 +0300 Subject: [PATCH 02/13] Add get group tickets requests --- main/extra_func.py | 14 ++++++++++---- main/requester.py | 14 ++++++++++++-- main/views.py | 16 ++++++++++------ 3 files changed, 32 insertions(+), 12 deletions(-) diff --git a/main/extra_func.py b/main/extra_func.py index 44c4d58..e5c6259 100644 --- a/main/extra_func.py +++ b/main/extra_func.py @@ -62,10 +62,9 @@ def make_light_agent(user_profile: UserProfile, who_changes: User) -> None: else: ticket.assignee = None ticket.group_id = zenpy.buffer_group_id - - zenpy.admin.tickets.update(tickets.values) - - attempts, success = 5, False + if tickets: + zenpy.admin.tickets.update(tickets) + attempts, success = 20, False while not success and attempts != 0: try: update_role(user_profile, ROLES['light_agent']) @@ -95,6 +94,13 @@ def get_tickets_list(email): 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): """ Функция обновляет профиль пользователя в соответствии с текущим в Zendesk. diff --git a/main/requester.py b/main/requester.py index 35f81b6..468abee 100644 --- a/main/requester.py +++ b/main/requester.py @@ -13,15 +13,25 @@ class TicketListRequester: 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 = f'https://ngenix1612197338.zendesk.com/api/v2/users/{zendesk_user.id}/tickets/assigned' + 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: + if response.status_code != 200: return None for ticket in response.json()['tickets']: tickets.append(Ticket(api=TicketApi, **ticket)) diff --git a/main/views.py b/main/views.py index 8432e33..f6d0a9a 100644 --- a/main/views.py +++ b/main/views.py @@ -20,13 +20,13 @@ from django_registration.views import RegistrationView from rest_framework import viewsets from rest_framework.response import Response -from access_controller.settings import DEFAULT_FROM_EMAIL, ZENDESK_ROLES, ZENDESK_MAX_AGENTS +from access_controller.settings import DEFAULT_FROM_EMAIL, ZENDESK_ROLES, ZENDESK_MAX_AGENTS, ZENDESK_GROUPS from main.extra_func import check_user_exist, update_profile, get_user_organization, \ make_engineer, make_light_agent, get_users_list, update_users_in_model, count_users, \ - log, set_session_params_for_work_page + log, set_session_params_for_work_page, get_tickets_list_for_group from .statistic_data import StatisticData from main.zendesk_admin import zenpy -from .requester import TicketListRequester +from main.requester import TicketListRequester from main.forms import AdminPageUsers, CustomRegistrationForm, CustomAuthenticationForm, StatisticForm from main.serializers import ProfileSerializer, ZendeskUserSerializer from .models import UserProfile @@ -229,15 +229,19 @@ def work_become_engineer(request: WSGIRequest) -> HttpResponseRedirect: def work_get_tickets(request): zenpy_user = zenpy.get_user(request.user.email) if zenpy_user.role == 'admin' or zenpy_user.custom_role_id == ZENDESK_ROLES['engineer']: - tickets = [ticket for ticket in zenpy.admin.search(type="ticket") if - ticket.group.name == 'Сменная группа' and ticket.assignee is None] + tickets = get_tickets_list_for_group(ZENDESK_GROUPS['buffer']) + assigned_tickets = [] count = 0 for i in range(len(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) tickets[i].assignee = zenpy_user + assigned_tickets.append(tickets[i]) count += 1 - zenpy.admin.tickets.update(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, is_confirm=False) From 0086d4909e37666cd5c6229235711e1d4ab45413 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=BE=D0=BA=D1=83=D1=80=D0=BE=D0=B2=20=D0=98=D0=B4?= =?UTF-8?q?=D0=B0=D1=80?= Date: Thu, 6 May 2021 15:22:57 +0000 Subject: [PATCH 03/13] Feature/tests/make_eng from work and control pages --- access_controller/urls.py | 1 - fixtures/test_make_engineer.json | 85 +++++++++++++++++++++++++++ main/extra_func.py | 2 +- main/templates/pages/adm_ruleset.html | 4 +- main/tests.py | 69 +++++++++++++++++++++- main/zendesk_admin.py | 9 ++- 6 files changed, 164 insertions(+), 6 deletions(-) create mode 100644 fixtures/test_make_engineer.json diff --git a/access_controller/urls.py b/access_controller/urls.py index 7df57b3..2cab267 100644 --- a/access_controller/urls.py +++ b/access_controller/urls.py @@ -14,7 +14,6 @@ Including another URLconf 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin -from django.contrib.auth import views from django.urls import path, include from main.views import main_page, profile_page, CustomRegistrationView, CustomLoginView, registration_error diff --git a/fixtures/test_make_engineer.json b/fixtures/test_make_engineer.json new file mode 100644 index 0000000..1154342 --- /dev/null +++ b/fixtures/test_make_engineer.json @@ -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" + } + } +] diff --git a/main/extra_func.py b/main/extra_func.py index c41625a..4494fc7 100644 --- a/main/extra_func.py +++ b/main/extra_func.py @@ -31,7 +31,7 @@ def update_role(user_profile: UserProfile, role: int, who_changes: User) -> None user_profile.custom_role_id = role user_profile.save() log(user_profile, who_changes.userprofile) - zendesk.admin.users.update(user) + zendesk.update_user(user) def make_engineer(user_profile: UserProfile, who_changes: User) -> None: diff --git a/main/templates/pages/adm_ruleset.html b/main/templates/pages/adm_ruleset.html index dc3cf54..cbbfc1b 100644 --- a/main/templates/pages/adm_ruleset.html +++ b/main/templates/pages/adm_ruleset.html @@ -17,9 +17,9 @@ - - + {# Для #} + {# Уведомлений #} {% endblock%} {% block content %}
diff --git a/main/tests.py b/main/tests.py index c06bc21..99d58cc 100644 --- a/main/tests.py +++ b/main/tests.py @@ -1,9 +1,10 @@ +from unittest.mock import patch from urllib.parse import urlparse from django.contrib.auth.models import User from django.core import mail from django.test import TestCase, Client -from django.urls import reverse +from django.urls import reverse, reverse_lazy from django.utils import translation import access_controller.settings as sets @@ -81,3 +82,69 @@ class RegistrationTestCase(TestCase): user = User.objects.get(email=self.zendesk_admin_email) self.assertEqual(user.userprofile.role, 'admin') self.assertTrue(user.has_perm('main.has_control_access')) + + +class MakeEngineerTestCase(TestCase): + 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']) diff --git a/main/zendesk_admin.py b/main/zendesk_admin.py index 8ecb877..2d12700 100644 --- a/main/zendesk_admin.py +++ b/main/zendesk_admin.py @@ -1,5 +1,4 @@ from typing import Optional, Dict - from zenpy import Zenpy from zenpy.lib.api_objects import User as ZenpyUser, Group as ZenpyGroup from zenpy.lib.exception import APIException @@ -22,6 +21,14 @@ class ZendeskAdmin: self.buffer_group_id: int = self.get_group(ZENDESK_GROUPS['buffer']).id self.solved_tickets_user_id: int = self.get_user(SOLVED_TICKETS_EMAIL).id + def update_user(self, user: ZenpyUser) -> bool: + """ + Функция сохраняет изменение пользователя в Zendesk. + + :param user: Пользователь с изменёнными данными + """ + self.admin.users.update(user) + def check_user(self, email: str) -> bool: """ Функция осуществляет проверку существования пользователя в Zendesk по email. From 6bc1c6d1089ceadd24bcd10a809816a51abb5068 Mon Sep 17 00:00:00 2001 From: Iurii Tatishchev Date: Thu, 6 May 2021 08:45:15 -0700 Subject: [PATCH 04/13] Move main helper files to main/lib. --- main/apiauth.py | 49 ------- main/extra_func.py | 319 ----------------------------------------- main/requester.py | 2 +- main/statistic_data.py | 261 --------------------------------- main/tests.py | 14 +- main/views.py | 11 +- main/zendesk_admin.py | 99 ------------- 7 files changed, 13 insertions(+), 742 deletions(-) delete mode 100644 main/apiauth.py delete mode 100644 main/extra_func.py delete mode 100644 main/statistic_data.py delete mode 100644 main/zendesk_admin.py diff --git a/main/apiauth.py b/main/apiauth.py deleted file mode 100644 index 08a018c..0000000 --- a/main/apiauth.py +++ /dev/null @@ -1,49 +0,0 @@ -import os - -from zenpy import Zenpy -from zenpy.lib.api_objects import User as ZenpyUser - -from access_controller.settings import ACTRL_ZENDESK_SUBDOMAIN, ACTRL_API_EMAIL, ACTRL_API_TOKEN, ACTRL_API_PASSWORD - - -def api_auth() -> dict: - """ - Функция создания пользователя с использованием Zendesk API. - - Получает из env Zendesk - email, token, password пользователя. - Если данные валидны и пользователь Zendesk с указанным email и токеном или паролем существует, - создается словарь данных пользователя, полученных через API c Zendesk. - - :return: данные пользователя - """ - credentials = { - 'subdomain': ACTRL_ZENDESK_SUBDOMAIN - } - email = ACTRL_API_EMAIL - token = ACTRL_API_TOKEN - password = ACTRL_API_PASSWORD - - if email is None: - raise ValueError('access_controller email not in env') - credentials['email'] = email - - # prefer token, use password if token not provided - if token: - credentials['token'] = token - elif password: - credentials['password'] = password - else: - raise ValueError('access_controller token or password not in env') - - zenpy_client = Zenpy(**credentials) - zenpy_user: ZenpyUser = zenpy_client.users.search(email).values[0] - - user = { - 'id': zenpy_user.id, - 'name': zenpy_user.name, # Zendesk doesn't have separate first and last name fields - 'email': zenpy_user.email, - 'role': zenpy_user.role, # str like 'admin' or 'agent', not id - 'photo': zenpy_user.photo['content_url'] if zenpy_user.photo is not None else None, - } - - return user diff --git a/main/extra_func.py b/main/extra_func.py deleted file mode 100644 index e6a2a97..0000000 --- a/main/extra_func.py +++ /dev/null @@ -1,319 +0,0 @@ -import logging -from datetime import timedelta - -from django.contrib.auth.models import User -from django.core.exceptions import ObjectDoesNotExist -from django.shortcuts import redirect -from django.utils import timezone -from zenpy import Zenpy -from zenpy.lib.exception import APIException -from zenpy.lib.api_objects import User as ZenpyUser, Ticket as ZenpyTicket -from zenpy.lib.generator import SearchResultGenerator - -from access_controller.settings import ZENDESK_ROLES as ROLES, ACTRL_ZENDESK_SUBDOMAIN -from main.models import UserProfile, RoleChangeLogs, UnassignedTicket, UnassignedTicketStatus -from main.requester import TicketListRequester -from main.zendesk_admin import zenpy - - -def update_role(user_profile: UserProfile, role: int, who_changes: User) -> None: - """ - Функция меняет роль пользователя. - - :param user_profile: Профиль пользователя - :param role: Новая роль - :param who_changes: Пользователь, меняющий роль - :return: Пользователь с обновленной ролью - """ - zendesk = zenpy - user = zendesk.get_user(user_profile.user.email) - user.custom_role_id = role - user_profile.custom_role_id = role - user_profile.save() - log(user_profile, who_changes.userprofile) - zendesk.update_user(user) - - -def make_engineer(user_profile: UserProfile, who_changes: User) -> None: - """ - Функция устанавливает пользователю роль инженера. - - :param user_profile: Профиль пользователя - :return: Вызов функции **update_role** с параметрами: профиль пользователя, роль "engineer" - """ - update_role(user_profile, ROLES['engineer'], who_changes) - - -def make_light_agent(user_profile: UserProfile, who_changes: User) -> None: - """ - Функция устанавливает пользователю роль легкого агента. - - :param user_profile: Профиль пользователя - :return: Вызов функции **update_role** с параметрами: профиль пользователя, роль "light_agent" - """ - tickets: SearchResultGenerator = get_tickets_list(user_profile.user.email) - ticket: ZenpyTicket - 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_id = zenpy.solved_tickets_user_id - else: - ticket.assignee = None - ticket.group_id = zenpy.buffer_group_id - if tickets: - zenpy.admin.tickets.update(tickets) - attempts, success = 20, False - while not success and attempts != 0: - try: - update_role(user_profile, ROLES['light_agent'], who_changes) - success = True - except APIException as e: - attempts -= 1 - if attempts == 0: - raise e - - -def get_users_list() -> list: - """ - Функция **get_users_list** возвращает список пользователей Zendesk, относящихся к организации SYSTEM. - """ - zendesk = zenpy - - # У пользователей должна быть организация 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 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): - """ - Функция обновляет профиль пользователя в соответствии с текущим в Zendesk. - - :param user_profile: Профиль пользователя - :return: Обновленный, в соответствие с текущими данными в Zendesk, профиль пользователя - """ - user = zenpy.get_user(user_profile.user.email) - user_profile.name = user.name - user_profile.role = user.role - user_profile.custom_role_id = user.custom_role_id if user.custom_role_id else 0 - user_profile.image = user.photo['content_url'] if user.photo else None - user_profile.save() - - -def check_user_exist(email: str) -> bool: - """ - Функция проверяет, существует ли пользователь. - - :param email: Email пользователя - :return: Зарегистрирован ли пользователь в Zendesk - """ - return zenpy.check_user(email) - - -def get_user_organization(email: str) -> str: - """ - Функция возвращает организацию пользователя. - - :param email: Email пользователя - :return: Организация пользователя - """ - return zenpy.get_user_org(email) - - -def check_user_auth(email: str, password: str) -> bool: - """ - Функция проверяет, верны ли входные данные. - - :raise: :class:`APIException`: исключение, вызываемое если пользователь не аутентифицирован - """ - creds = { - 'email': email, - 'password': password, - 'subdomain': ACTRL_ZENDESK_SUBDOMAIN, - } - try: - user = Zenpy(**creds) - user.search(email, type='user') - except APIException: - return False - return True - - -def update_user_in_model(profile: UserProfile, zendesk_user: ZenpyUser): - """ - Функция обновляет профиль пользователя при изменении данных пользователя на 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, исключая правую границу. - - :param start_date: Начальная дата - :param end_date: Конечная дата - :return: Список дней, не включая конечную дату - """ - dates = [] - for n in range(int((end_date - start_date).days)): - dates.append(start_date + timedelta(n)) - return dates - - -def get_timedelta(log, time=None) -> timedelta: - """ - Функция возвращает объект класса timedelta, который хранит промежуток времени от начала суток до момента, - который находится в log (объект класса RoleChangeLogs) или в time(datetime.time), если введён. - - :param log: Лог - :param time: Время - :return: Сколько времени прошло от начала суток до события - """ - if time is None: - time = log.change_time.time() - time = timedelta(hours=time.hour, minutes=time.minute, seconds=time.second) - return time - - -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 DatabaseHandler(logging.Handler): - def __init__(self): - logging.Handler.__init__(self) - - def emit(self, record): - database = RoleChangeLogs() - users = record.msg - if users[1]: - user = users[0] - admin = users[1] - elif not users[1]: - user = users[0] - admin = users[0] - database.name = user.name - database.user = user.user - database.changed_by = admin.user - if user.custom_role_id == ROLES['engineer']: - database.old_role = ROLES['light_agent'] - elif user.custom_role_id == ROLES['light_agent']: - database.old_role = ROLES['engineer'] - database.new_role = user.custom_role_id - database.save() - - -class CsvFormatter(logging.Formatter): - def __init__(self): - logging.Formatter.__init__(self) - - def format(self, record): - users = record.msg - if users[1]: - user = users[0] - admin = users[1] - elif not users[1]: - user = users[0] - admin = users[0] - msg = '' - msg += user.name - if user.custom_role_id == ROLES['engineer']: - msg += ',engineer,' - elif user.custom_role_id == ROLES['light_agent']: - msg += ',light_agent,' - time = str(timezone.now().today()) - msg += time[:16] - msg += ',' - msg += admin.name - return msg - - -def log(user, admin=None): - """ - Осуществляет запись логов в базу данных и csv файл - :param admin: - :param user: - :return: - """ - users = [user, admin] - logger = logging.getLogger('MY_LOGGER') - if not logger.hasHandlers(): - dbhandler = DatabaseHandler() - csvformatter = CsvFormatter() - csvhandler = logging.FileHandler('logs/logs.csv', "a") - csvhandler.setFormatter(csvformatter) - logger.addHandler(dbhandler) - logger.addHandler(csvhandler) - logger.setLevel('INFO') - logger.info(users) - - -def set_session_params_for_work_page(request, count=None, is_confirm=True): - """ - Функция для страницы получения прав - Устанавливает данные сессии о успешности запроса и количестве назначенных тикетов - """ - request.session['is_confirm'] = is_confirm - request.session['count_tickets'] = count - return redirect('work', request.user.id) diff --git a/main/requester.py b/main/requester.py index 468abee..d3bdd18 100644 --- a/main/requester.py +++ b/main/requester.py @@ -2,7 +2,7 @@ import requests from zenpy import TicketApi from zenpy.lib.api_objects import Ticket -from main.zendesk_admin import zenpy +from main.lib.zendesk_admin import zenpy class TicketListRequester: diff --git a/main/statistic_data.py b/main/statistic_data.py deleted file mode 100644 index fa1ab24..0000000 --- a/main/statistic_data.py +++ /dev/null @@ -1,261 +0,0 @@ -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) diff --git a/main/tests.py b/main/tests.py index 99d58cc..6271ae8 100644 --- a/main/tests.py +++ b/main/tests.py @@ -8,7 +8,7 @@ from django.urls import reverse, reverse_lazy from django.utils import translation import access_controller.settings as sets -from main.zendesk_admin import zenpy +from main.lib.zendesk_admin import zenpy class RegistrationTestCase(TestCase): @@ -96,31 +96,31 @@ class MakeEngineerTestCase(TestCase): self.admin_client = Client() self.admin_client.force_login(User.objects.get(email=self.admin)) - @patch('main.extra_func.zenpy') + @patch('main.lib.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') + @patch('main.lib.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') + @patch('main.lib.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') + @patch('main.lib.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') + @patch('main.lib.extra_func.zenpy') def test_control_page_make_one(self, ZenpyMock): self.admin_client.post( reverse_lazy('control'), @@ -131,7 +131,7 @@ class MakeEngineerTestCase(TestCase): self.assertEqual(len(call_list), 1) self.assertEqual(mock_object.custom_role_id, sets.ZENDESK_ROLES['engineer']) - @patch('main.extra_func.zenpy') + @patch('main.lib.extra_func.zenpy') def test_control_page_make_many(self, ZenpyMock): self.admin_client.post( reverse_lazy('control'), diff --git a/main/views.py b/main/views.py index 467e925..4355141 100644 --- a/main/views.py +++ b/main/views.py @@ -13,7 +13,7 @@ from django.contrib.messages.views import SuccessMessageMixin from django.core.handlers.wsgi import WSGIRequest from django.http import HttpResponseRedirect, HttpResponse from django.shortcuts import render, redirect -from django.urls import reverse_lazy, reverse +from django.urls import reverse_lazy from django.views.generic import FormView from django_registration.views import RegistrationView # Django REST @@ -21,12 +21,11 @@ from rest_framework import viewsets from rest_framework.response import Response from access_controller.settings import DEFAULT_FROM_EMAIL, ZENDESK_ROLES, ZENDESK_MAX_AGENTS, ZENDESK_GROUPS -from main.extra_func import check_user_exist, update_profile, get_user_organization, \ +from main.lib.extra_func import check_user_exist, update_profile, get_user_organization, \ make_engineer, make_light_agent, get_users_list, update_users_in_model, count_users, \ - log, set_session_params_for_work_page, get_tickets_list_for_group -from .statistic_data import StatisticData -from main.zendesk_admin import zenpy -from main.requester import TicketListRequester + set_session_params_for_work_page, get_tickets_list_for_group +from main.lib.statistic_data import StatisticData +from main.lib.zendesk_admin import zenpy from main.forms import AdminPageUsers, CustomRegistrationForm, CustomAuthenticationForm, StatisticForm from main.serializers import ProfileSerializer, ZendeskUserSerializer from .models import UserProfile diff --git a/main/zendesk_admin.py b/main/zendesk_admin.py deleted file mode 100644 index 2a689ce..0000000 --- a/main/zendesk_admin.py +++ /dev/null @@ -1,99 +0,0 @@ -from typing import Optional, Dict -from zenpy import Zenpy -from zenpy.lib.api_objects import User as ZenpyUser, Group as ZenpyGroup -from zenpy.lib.exception import APIException -from access_controller.settings import ACTRL_ZENDESK_SUBDOMAIN, ACTRL_API_EMAIL, ACTRL_API_TOKEN, ACTRL_API_PASSWORD, \ - ZENDESK_GROUPS, SOLVED_TICKETS_EMAIL - - -class ZendeskAdmin: - """ - Класс **ZendeskAdmin** существует, чтобы в каждой функции отдельно не проверять аккаунт администратора. - - :param credentials: Полномочия (первым указывается учетная запись организации в Zendesk) - :type credentials: :class:`Dict[str, str]` - """ - - def __init__(self, credentials: Dict[str, str]): - self.credentials = credentials - self.admin = self.create_admin() - self.buffer_group_id: int = self.get_group(ZENDESK_GROUPS['buffer']).id - self.solved_tickets_user_id: int = self.get_user(SOLVED_TICKETS_EMAIL).id - - def update_user(self, user: ZenpyUser) -> bool: - """ - Функция сохраняет изменение пользователя в Zendesk. - - :param user: Пользователь с изменёнными данными - """ - self.admin.users.update(user) - - def check_user(self, email: str) -> bool: - """ - Функция осуществляет проверку существования пользователя в Zendesk по email. - - :param email: Email пользователя - :return: Является ли зарегистрированным - """ - return True if self.admin.search(email, type='user') else False - - def get_user(self, email: str) -> ZenpyUser: - """ - Функция возвращает пользователя (объект) по его email. - - :param email: Email пользователя - :return: Объект пользователя, найденного в БД - """ - return self.admin.users.search(email).values[0] - - def get_group(self, name: str) -> Optional[ZenpyGroup]: - """ - Функция возвращает группу по названию - - :param name: Имя пользователя - :return: Группы пользователя (в случае отсутствия None) - """ - groups = self.admin.search(name, type='group') - for group in groups: - return group - return None - - def get_user_org(self, email: str) -> str: - """ - Функция возвращает организацию, к которой относится пользователь по его 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: - """ - Функция создает администратора, проверяя наличие вводимых данных в env. - - :raise: :class:`ValueError`: исключение, вызываемое если email не введен в env - :raise: :class:`APIException`: исключение, вызываемое если пользователя с таким email не существует в Zendesk - """ - - if self.credentials.get('email') is None: - raise ValueError('access_controller email not in env') - - if self.credentials.get('token') is None and self.credentials.get('password') is None: - raise ValueError('access_controller token or password not in env') - - admin = Zenpy(**self.credentials) - try: - admin.search(self.credentials['email'], type='user') - except APIException: - raise ValueError('invalid access_controller`s login data') - - return admin - - -zenpy = ZendeskAdmin({ - 'subdomain': ACTRL_ZENDESK_SUBDOMAIN, - 'email': ACTRL_API_EMAIL, - 'token': ACTRL_API_TOKEN, - 'password': ACTRL_API_PASSWORD, -}) From d426e9a2c44c257f4fce20803015bec47d6bd206 Mon Sep 17 00:00:00 2001 From: Iurii Tatishchev Date: Thu, 6 May 2021 08:46:41 -0700 Subject: [PATCH 05/13] fix --- main/lib/__init__.py | 0 main/lib/__pycache__/__init__.cpython-39.pyc | Bin 0 -> 200 bytes .../lib/__pycache__/extra_func.cpython-39.pyc | Bin 0 -> 11882 bytes .../__pycache__/statistic_data.cpython-39.pyc | Bin 0 -> 11471 bytes .../__pycache__/zendesk_admin.cpython-39.pyc | Bin 0 -> 4804 bytes main/lib/extra_func.py | 319 ++++++++++++++++++ main/lib/statistic_data.py | 261 ++++++++++++++ main/lib/zendesk_admin.py | 99 ++++++ 8 files changed, 679 insertions(+) create mode 100644 main/lib/__init__.py create mode 100644 main/lib/__pycache__/__init__.cpython-39.pyc create mode 100644 main/lib/__pycache__/extra_func.cpython-39.pyc create mode 100644 main/lib/__pycache__/statistic_data.cpython-39.pyc create mode 100644 main/lib/__pycache__/zendesk_admin.cpython-39.pyc create mode 100644 main/lib/extra_func.py create mode 100644 main/lib/statistic_data.py create mode 100644 main/lib/zendesk_admin.py diff --git a/main/lib/__init__.py b/main/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/main/lib/__pycache__/__init__.cpython-39.pyc b/main/lib/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..70ee7f6ed0f60b941986434ab357ce214c10c586 GIT binary patch literal 200 zcmYe~<>g`k0z&ryk0@&Ee@O9 Q{FKt1R6CF}J_9iW0F?tVr2qf` literal 0 HcmV?d00001 diff --git a/main/lib/__pycache__/extra_func.cpython-39.pyc b/main/lib/__pycache__/extra_func.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..506e57c529b5d7bf576e43a4ca5b8b21158116e7 GIT binary patch literal 11882 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 a0}p(kQlm{aOu>hA=~~aU3ZKgFfBp+R{3Hed literal 0 HcmV?d00001 diff --git a/main/lib/extra_func.py b/main/lib/extra_func.py new file mode 100644 index 0000000..ad251cd --- /dev/null +++ b/main/lib/extra_func.py @@ -0,0 +1,319 @@ +import logging +from datetime import timedelta + +from django.contrib.auth.models import User +from django.core.exceptions import ObjectDoesNotExist +from django.shortcuts import redirect +from django.utils import timezone +from zenpy import Zenpy +from zenpy.lib.exception import APIException +from zenpy.lib.api_objects import User as ZenpyUser, Ticket as ZenpyTicket +from zenpy.lib.generator import SearchResultGenerator + +from access_controller.settings import ZENDESK_ROLES as ROLES, ACTRL_ZENDESK_SUBDOMAIN +from main.models import UserProfile, RoleChangeLogs, UnassignedTicket, UnassignedTicketStatus +from main.requester import TicketListRequester +from main.lib.zendesk_admin import zenpy + + +def update_role(user_profile: UserProfile, role: int, who_changes: User) -> None: + """ + Функция меняет роль пользователя. + + :param user_profile: Профиль пользователя + :param role: Новая роль + :param who_changes: Пользователь, меняющий роль + :return: Пользователь с обновленной ролью + """ + zendesk = zenpy + user = zendesk.get_user(user_profile.user.email) + user.custom_role_id = role + user_profile.custom_role_id = role + user_profile.save() + log(user_profile, who_changes.userprofile) + zendesk.update_user(user) + + +def make_engineer(user_profile: UserProfile, who_changes: User) -> None: + """ + Функция устанавливает пользователю роль инженера. + + :param user_profile: Профиль пользователя + :return: Вызов функции **update_role** с параметрами: профиль пользователя, роль "engineer" + """ + update_role(user_profile, ROLES['engineer'], who_changes) + + +def make_light_agent(user_profile: UserProfile, who_changes: User) -> None: + """ + Функция устанавливает пользователю роль легкого агента. + + :param user_profile: Профиль пользователя + :return: Вызов функции **update_role** с параметрами: профиль пользователя, роль "light_agent" + """ + tickets: SearchResultGenerator = get_tickets_list(user_profile.user.email) + ticket: ZenpyTicket + 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_id = zenpy.solved_tickets_user_id + else: + ticket.assignee = None + ticket.group_id = zenpy.buffer_group_id + if tickets: + zenpy.admin.tickets.update(tickets) + attempts, success = 20, False + while not success and attempts != 0: + try: + update_role(user_profile, ROLES['light_agent'], who_changes) + success = True + except APIException as e: + attempts -= 1 + if attempts == 0: + raise e + + +def get_users_list() -> list: + """ + Функция **get_users_list** возвращает список пользователей Zendesk, относящихся к организации SYSTEM. + """ + zendesk = zenpy + + # У пользователей должна быть организация 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 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): + """ + Функция обновляет профиль пользователя в соответствии с текущим в Zendesk. + + :param user_profile: Профиль пользователя + :return: Обновленный, в соответствие с текущими данными в Zendesk, профиль пользователя + """ + user = zenpy.get_user(user_profile.user.email) + user_profile.name = user.name + user_profile.role = user.role + user_profile.custom_role_id = user.custom_role_id if user.custom_role_id else 0 + user_profile.image = user.photo['content_url'] if user.photo else None + user_profile.save() + + +def check_user_exist(email: str) -> bool: + """ + Функция проверяет, существует ли пользователь. + + :param email: Email пользователя + :return: Зарегистрирован ли пользователь в Zendesk + """ + return zenpy.check_user(email) + + +def get_user_organization(email: str) -> str: + """ + Функция возвращает организацию пользователя. + + :param email: Email пользователя + :return: Организация пользователя + """ + return zenpy.get_user_org(email) + + +def check_user_auth(email: str, password: str) -> bool: + """ + Функция проверяет, верны ли входные данные. + + :raise: :class:`APIException`: исключение, вызываемое если пользователь не аутентифицирован + """ + creds = { + 'email': email, + 'password': password, + 'subdomain': ACTRL_ZENDESK_SUBDOMAIN, + } + try: + user = Zenpy(**creds) + user.search(email, type='user') + except APIException: + return False + return True + + +def update_user_in_model(profile: UserProfile, zendesk_user: ZenpyUser): + """ + Функция обновляет профиль пользователя при изменении данных пользователя на 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, исключая правую границу. + + :param start_date: Начальная дата + :param end_date: Конечная дата + :return: Список дней, не включая конечную дату + """ + dates = [] + for n in range(int((end_date - start_date).days)): + dates.append(start_date + timedelta(n)) + return dates + + +def get_timedelta(log, time=None) -> timedelta: + """ + Функция возвращает объект класса timedelta, который хранит промежуток времени от начала суток до момента, + который находится в log (объект класса RoleChangeLogs) или в time(datetime.time), если введён. + + :param log: Лог + :param time: Время + :return: Сколько времени прошло от начала суток до события + """ + if time is None: + time = log.change_time.time() + time = timedelta(hours=time.hour, minutes=time.minute, seconds=time.second) + return time + + +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 DatabaseHandler(logging.Handler): + def __init__(self): + logging.Handler.__init__(self) + + def emit(self, record): + database = RoleChangeLogs() + users = record.msg + if users[1]: + user = users[0] + admin = users[1] + elif not users[1]: + user = users[0] + admin = users[0] + database.name = user.name + database.user = user.user + database.changed_by = admin.user + if user.custom_role_id == ROLES['engineer']: + database.old_role = ROLES['light_agent'] + elif user.custom_role_id == ROLES['light_agent']: + database.old_role = ROLES['engineer'] + database.new_role = user.custom_role_id + database.save() + + +class CsvFormatter(logging.Formatter): + def __init__(self): + logging.Formatter.__init__(self) + + def format(self, record): + users = record.msg + if users[1]: + user = users[0] + admin = users[1] + elif not users[1]: + user = users[0] + admin = users[0] + msg = '' + msg += user.name + if user.custom_role_id == ROLES['engineer']: + msg += ',engineer,' + elif user.custom_role_id == ROLES['light_agent']: + msg += ',light_agent,' + time = str(timezone.now().today()) + msg += time[:16] + msg += ',' + msg += admin.name + return msg + + +def log(user, admin=None): + """ + Осуществляет запись логов в базу данных и csv файл + :param admin: + :param user: + :return: + """ + users = [user, admin] + logger = logging.getLogger('MY_LOGGER') + if not logger.hasHandlers(): + dbhandler = DatabaseHandler() + csvformatter = CsvFormatter() + csvhandler = logging.FileHandler('logs/logs.csv', "a") + csvhandler.setFormatter(csvformatter) + logger.addHandler(dbhandler) + logger.addHandler(csvhandler) + logger.setLevel('INFO') + logger.info(users) + + +def set_session_params_for_work_page(request, count=None, is_confirm=True): + """ + Функция для страницы получения прав + Устанавливает данные сессии о успешности запроса и количестве назначенных тикетов + """ + request.session['is_confirm'] = is_confirm + request.session['count_tickets'] = count + return redirect('work', request.user.id) diff --git a/main/lib/statistic_data.py b/main/lib/statistic_data.py new file mode 100644 index 0000000..2e1061d --- /dev/null +++ b/main/lib/statistic_data.py @@ -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.lib.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) diff --git a/main/lib/zendesk_admin.py b/main/lib/zendesk_admin.py new file mode 100644 index 0000000..2a689ce --- /dev/null +++ b/main/lib/zendesk_admin.py @@ -0,0 +1,99 @@ +from typing import Optional, Dict +from zenpy import Zenpy +from zenpy.lib.api_objects import User as ZenpyUser, Group as ZenpyGroup +from zenpy.lib.exception import APIException +from access_controller.settings import ACTRL_ZENDESK_SUBDOMAIN, ACTRL_API_EMAIL, ACTRL_API_TOKEN, ACTRL_API_PASSWORD, \ + ZENDESK_GROUPS, SOLVED_TICKETS_EMAIL + + +class ZendeskAdmin: + """ + Класс **ZendeskAdmin** существует, чтобы в каждой функции отдельно не проверять аккаунт администратора. + + :param credentials: Полномочия (первым указывается учетная запись организации в Zendesk) + :type credentials: :class:`Dict[str, str]` + """ + + def __init__(self, credentials: Dict[str, str]): + self.credentials = credentials + self.admin = self.create_admin() + self.buffer_group_id: int = self.get_group(ZENDESK_GROUPS['buffer']).id + self.solved_tickets_user_id: int = self.get_user(SOLVED_TICKETS_EMAIL).id + + def update_user(self, user: ZenpyUser) -> bool: + """ + Функция сохраняет изменение пользователя в Zendesk. + + :param user: Пользователь с изменёнными данными + """ + self.admin.users.update(user) + + def check_user(self, email: str) -> bool: + """ + Функция осуществляет проверку существования пользователя в Zendesk по email. + + :param email: Email пользователя + :return: Является ли зарегистрированным + """ + return True if self.admin.search(email, type='user') else False + + def get_user(self, email: str) -> ZenpyUser: + """ + Функция возвращает пользователя (объект) по его email. + + :param email: Email пользователя + :return: Объект пользователя, найденного в БД + """ + return self.admin.users.search(email).values[0] + + def get_group(self, name: str) -> Optional[ZenpyGroup]: + """ + Функция возвращает группу по названию + + :param name: Имя пользователя + :return: Группы пользователя (в случае отсутствия None) + """ + groups = self.admin.search(name, type='group') + for group in groups: + return group + return None + + def get_user_org(self, email: str) -> str: + """ + Функция возвращает организацию, к которой относится пользователь по его 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: + """ + Функция создает администратора, проверяя наличие вводимых данных в env. + + :raise: :class:`ValueError`: исключение, вызываемое если email не введен в env + :raise: :class:`APIException`: исключение, вызываемое если пользователя с таким email не существует в Zendesk + """ + + if self.credentials.get('email') is None: + raise ValueError('access_controller email not in env') + + if self.credentials.get('token') is None and self.credentials.get('password') is None: + raise ValueError('access_controller token or password not in env') + + admin = Zenpy(**self.credentials) + try: + admin.search(self.credentials['email'], type='user') + except APIException: + raise ValueError('invalid access_controller`s login data') + + return admin + + +zenpy = ZendeskAdmin({ + 'subdomain': ACTRL_ZENDESK_SUBDOMAIN, + 'email': ACTRL_API_EMAIL, + 'token': ACTRL_API_TOKEN, + 'password': ACTRL_API_PASSWORD, +}) From be1bfdd259c6c2d8dd1987178d1ce78e2e9ee9f5 Mon Sep 17 00:00:00 2001 From: Iurii Tatishchev Date: Thu, 6 May 2021 08:47:15 -0700 Subject: [PATCH 06/13] Revert "Move main helper files to main/lib." This reverts commit 6bc1c6d1089ceadd24bcd10a809816a51abb5068. --- main/apiauth.py | 49 +++++++ main/extra_func.py | 319 +++++++++++++++++++++++++++++++++++++++++ main/requester.py | 2 +- main/statistic_data.py | 261 +++++++++++++++++++++++++++++++++ main/tests.py | 14 +- main/views.py | 11 +- main/zendesk_admin.py | 99 +++++++++++++ 7 files changed, 742 insertions(+), 13 deletions(-) create mode 100644 main/apiauth.py create mode 100644 main/extra_func.py create mode 100644 main/statistic_data.py create mode 100644 main/zendesk_admin.py diff --git a/main/apiauth.py b/main/apiauth.py new file mode 100644 index 0000000..08a018c --- /dev/null +++ b/main/apiauth.py @@ -0,0 +1,49 @@ +import os + +from zenpy import Zenpy +from zenpy.lib.api_objects import User as ZenpyUser + +from access_controller.settings import ACTRL_ZENDESK_SUBDOMAIN, ACTRL_API_EMAIL, ACTRL_API_TOKEN, ACTRL_API_PASSWORD + + +def api_auth() -> dict: + """ + Функция создания пользователя с использованием Zendesk API. + + Получает из env Zendesk - email, token, password пользователя. + Если данные валидны и пользователь Zendesk с указанным email и токеном или паролем существует, + создается словарь данных пользователя, полученных через API c Zendesk. + + :return: данные пользователя + """ + credentials = { + 'subdomain': ACTRL_ZENDESK_SUBDOMAIN + } + email = ACTRL_API_EMAIL + token = ACTRL_API_TOKEN + password = ACTRL_API_PASSWORD + + if email is None: + raise ValueError('access_controller email not in env') + credentials['email'] = email + + # prefer token, use password if token not provided + if token: + credentials['token'] = token + elif password: + credentials['password'] = password + else: + raise ValueError('access_controller token or password not in env') + + zenpy_client = Zenpy(**credentials) + zenpy_user: ZenpyUser = zenpy_client.users.search(email).values[0] + + user = { + 'id': zenpy_user.id, + 'name': zenpy_user.name, # Zendesk doesn't have separate first and last name fields + 'email': zenpy_user.email, + 'role': zenpy_user.role, # str like 'admin' or 'agent', not id + 'photo': zenpy_user.photo['content_url'] if zenpy_user.photo is not None else None, + } + + return user diff --git a/main/extra_func.py b/main/extra_func.py new file mode 100644 index 0000000..e6a2a97 --- /dev/null +++ b/main/extra_func.py @@ -0,0 +1,319 @@ +import logging +from datetime import timedelta + +from django.contrib.auth.models import User +from django.core.exceptions import ObjectDoesNotExist +from django.shortcuts import redirect +from django.utils import timezone +from zenpy import Zenpy +from zenpy.lib.exception import APIException +from zenpy.lib.api_objects import User as ZenpyUser, Ticket as ZenpyTicket +from zenpy.lib.generator import SearchResultGenerator + +from access_controller.settings import ZENDESK_ROLES as ROLES, ACTRL_ZENDESK_SUBDOMAIN +from main.models import UserProfile, RoleChangeLogs, UnassignedTicket, UnassignedTicketStatus +from main.requester import TicketListRequester +from main.zendesk_admin import zenpy + + +def update_role(user_profile: UserProfile, role: int, who_changes: User) -> None: + """ + Функция меняет роль пользователя. + + :param user_profile: Профиль пользователя + :param role: Новая роль + :param who_changes: Пользователь, меняющий роль + :return: Пользователь с обновленной ролью + """ + zendesk = zenpy + user = zendesk.get_user(user_profile.user.email) + user.custom_role_id = role + user_profile.custom_role_id = role + user_profile.save() + log(user_profile, who_changes.userprofile) + zendesk.update_user(user) + + +def make_engineer(user_profile: UserProfile, who_changes: User) -> None: + """ + Функция устанавливает пользователю роль инженера. + + :param user_profile: Профиль пользователя + :return: Вызов функции **update_role** с параметрами: профиль пользователя, роль "engineer" + """ + update_role(user_profile, ROLES['engineer'], who_changes) + + +def make_light_agent(user_profile: UserProfile, who_changes: User) -> None: + """ + Функция устанавливает пользователю роль легкого агента. + + :param user_profile: Профиль пользователя + :return: Вызов функции **update_role** с параметрами: профиль пользователя, роль "light_agent" + """ + tickets: SearchResultGenerator = get_tickets_list(user_profile.user.email) + ticket: ZenpyTicket + 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_id = zenpy.solved_tickets_user_id + else: + ticket.assignee = None + ticket.group_id = zenpy.buffer_group_id + if tickets: + zenpy.admin.tickets.update(tickets) + attempts, success = 20, False + while not success and attempts != 0: + try: + update_role(user_profile, ROLES['light_agent'], who_changes) + success = True + except APIException as e: + attempts -= 1 + if attempts == 0: + raise e + + +def get_users_list() -> list: + """ + Функция **get_users_list** возвращает список пользователей Zendesk, относящихся к организации SYSTEM. + """ + zendesk = zenpy + + # У пользователей должна быть организация 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 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): + """ + Функция обновляет профиль пользователя в соответствии с текущим в Zendesk. + + :param user_profile: Профиль пользователя + :return: Обновленный, в соответствие с текущими данными в Zendesk, профиль пользователя + """ + user = zenpy.get_user(user_profile.user.email) + user_profile.name = user.name + user_profile.role = user.role + user_profile.custom_role_id = user.custom_role_id if user.custom_role_id else 0 + user_profile.image = user.photo['content_url'] if user.photo else None + user_profile.save() + + +def check_user_exist(email: str) -> bool: + """ + Функция проверяет, существует ли пользователь. + + :param email: Email пользователя + :return: Зарегистрирован ли пользователь в Zendesk + """ + return zenpy.check_user(email) + + +def get_user_organization(email: str) -> str: + """ + Функция возвращает организацию пользователя. + + :param email: Email пользователя + :return: Организация пользователя + """ + return zenpy.get_user_org(email) + + +def check_user_auth(email: str, password: str) -> bool: + """ + Функция проверяет, верны ли входные данные. + + :raise: :class:`APIException`: исключение, вызываемое если пользователь не аутентифицирован + """ + creds = { + 'email': email, + 'password': password, + 'subdomain': ACTRL_ZENDESK_SUBDOMAIN, + } + try: + user = Zenpy(**creds) + user.search(email, type='user') + except APIException: + return False + return True + + +def update_user_in_model(profile: UserProfile, zendesk_user: ZenpyUser): + """ + Функция обновляет профиль пользователя при изменении данных пользователя на 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, исключая правую границу. + + :param start_date: Начальная дата + :param end_date: Конечная дата + :return: Список дней, не включая конечную дату + """ + dates = [] + for n in range(int((end_date - start_date).days)): + dates.append(start_date + timedelta(n)) + return dates + + +def get_timedelta(log, time=None) -> timedelta: + """ + Функция возвращает объект класса timedelta, который хранит промежуток времени от начала суток до момента, + который находится в log (объект класса RoleChangeLogs) или в time(datetime.time), если введён. + + :param log: Лог + :param time: Время + :return: Сколько времени прошло от начала суток до события + """ + if time is None: + time = log.change_time.time() + time = timedelta(hours=time.hour, minutes=time.minute, seconds=time.second) + return time + + +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 DatabaseHandler(logging.Handler): + def __init__(self): + logging.Handler.__init__(self) + + def emit(self, record): + database = RoleChangeLogs() + users = record.msg + if users[1]: + user = users[0] + admin = users[1] + elif not users[1]: + user = users[0] + admin = users[0] + database.name = user.name + database.user = user.user + database.changed_by = admin.user + if user.custom_role_id == ROLES['engineer']: + database.old_role = ROLES['light_agent'] + elif user.custom_role_id == ROLES['light_agent']: + database.old_role = ROLES['engineer'] + database.new_role = user.custom_role_id + database.save() + + +class CsvFormatter(logging.Formatter): + def __init__(self): + logging.Formatter.__init__(self) + + def format(self, record): + users = record.msg + if users[1]: + user = users[0] + admin = users[1] + elif not users[1]: + user = users[0] + admin = users[0] + msg = '' + msg += user.name + if user.custom_role_id == ROLES['engineer']: + msg += ',engineer,' + elif user.custom_role_id == ROLES['light_agent']: + msg += ',light_agent,' + time = str(timezone.now().today()) + msg += time[:16] + msg += ',' + msg += admin.name + return msg + + +def log(user, admin=None): + """ + Осуществляет запись логов в базу данных и csv файл + :param admin: + :param user: + :return: + """ + users = [user, admin] + logger = logging.getLogger('MY_LOGGER') + if not logger.hasHandlers(): + dbhandler = DatabaseHandler() + csvformatter = CsvFormatter() + csvhandler = logging.FileHandler('logs/logs.csv', "a") + csvhandler.setFormatter(csvformatter) + logger.addHandler(dbhandler) + logger.addHandler(csvhandler) + logger.setLevel('INFO') + logger.info(users) + + +def set_session_params_for_work_page(request, count=None, is_confirm=True): + """ + Функция для страницы получения прав + Устанавливает данные сессии о успешности запроса и количестве назначенных тикетов + """ + request.session['is_confirm'] = is_confirm + request.session['count_tickets'] = count + return redirect('work', request.user.id) diff --git a/main/requester.py b/main/requester.py index d3bdd18..468abee 100644 --- a/main/requester.py +++ b/main/requester.py @@ -2,7 +2,7 @@ import requests from zenpy import TicketApi from zenpy.lib.api_objects import Ticket -from main.lib.zendesk_admin import zenpy +from main.zendesk_admin import zenpy class TicketListRequester: diff --git a/main/statistic_data.py b/main/statistic_data.py new file mode 100644 index 0000000..fa1ab24 --- /dev/null +++ b/main/statistic_data.py @@ -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) diff --git a/main/tests.py b/main/tests.py index 6271ae8..99d58cc 100644 --- a/main/tests.py +++ b/main/tests.py @@ -8,7 +8,7 @@ from django.urls import reverse, reverse_lazy from django.utils import translation import access_controller.settings as sets -from main.lib.zendesk_admin import zenpy +from main.zendesk_admin import zenpy class RegistrationTestCase(TestCase): @@ -96,31 +96,31 @@ class MakeEngineerTestCase(TestCase): self.admin_client = Client() self.admin_client.force_login(User.objects.get(email=self.admin)) - @patch('main.lib.extra_func.zenpy') + @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.lib.extra_func.zenpy') + @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.lib.extra_func.zenpy') + @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.lib.extra_func.zenpy') + @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.lib.extra_func.zenpy') + @patch('main.extra_func.zenpy') def test_control_page_make_one(self, ZenpyMock): self.admin_client.post( reverse_lazy('control'), @@ -131,7 +131,7 @@ class MakeEngineerTestCase(TestCase): self.assertEqual(len(call_list), 1) self.assertEqual(mock_object.custom_role_id, sets.ZENDESK_ROLES['engineer']) - @patch('main.lib.extra_func.zenpy') + @patch('main.extra_func.zenpy') def test_control_page_make_many(self, ZenpyMock): self.admin_client.post( reverse_lazy('control'), diff --git a/main/views.py b/main/views.py index 4355141..467e925 100644 --- a/main/views.py +++ b/main/views.py @@ -13,7 +13,7 @@ from django.contrib.messages.views import SuccessMessageMixin from django.core.handlers.wsgi import WSGIRequest from django.http import HttpResponseRedirect, HttpResponse from django.shortcuts import render, redirect -from django.urls import reverse_lazy +from django.urls import reverse_lazy, reverse from django.views.generic import FormView from django_registration.views import RegistrationView # Django REST @@ -21,11 +21,12 @@ from rest_framework import viewsets from rest_framework.response import Response from access_controller.settings import DEFAULT_FROM_EMAIL, ZENDESK_ROLES, ZENDESK_MAX_AGENTS, ZENDESK_GROUPS -from main.lib.extra_func import check_user_exist, update_profile, get_user_organization, \ +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, \ - set_session_params_for_work_page, get_tickets_list_for_group -from main.lib.statistic_data import StatisticData -from main.lib.zendesk_admin import zenpy + 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.requester import TicketListRequester from main.forms import AdminPageUsers, CustomRegistrationForm, CustomAuthenticationForm, StatisticForm from main.serializers import ProfileSerializer, ZendeskUserSerializer from .models import UserProfile diff --git a/main/zendesk_admin.py b/main/zendesk_admin.py new file mode 100644 index 0000000..2a689ce --- /dev/null +++ b/main/zendesk_admin.py @@ -0,0 +1,99 @@ +from typing import Optional, Dict +from zenpy import Zenpy +from zenpy.lib.api_objects import User as ZenpyUser, Group as ZenpyGroup +from zenpy.lib.exception import APIException +from access_controller.settings import ACTRL_ZENDESK_SUBDOMAIN, ACTRL_API_EMAIL, ACTRL_API_TOKEN, ACTRL_API_PASSWORD, \ + ZENDESK_GROUPS, SOLVED_TICKETS_EMAIL + + +class ZendeskAdmin: + """ + Класс **ZendeskAdmin** существует, чтобы в каждой функции отдельно не проверять аккаунт администратора. + + :param credentials: Полномочия (первым указывается учетная запись организации в Zendesk) + :type credentials: :class:`Dict[str, str]` + """ + + def __init__(self, credentials: Dict[str, str]): + self.credentials = credentials + self.admin = self.create_admin() + self.buffer_group_id: int = self.get_group(ZENDESK_GROUPS['buffer']).id + self.solved_tickets_user_id: int = self.get_user(SOLVED_TICKETS_EMAIL).id + + def update_user(self, user: ZenpyUser) -> bool: + """ + Функция сохраняет изменение пользователя в Zendesk. + + :param user: Пользователь с изменёнными данными + """ + self.admin.users.update(user) + + def check_user(self, email: str) -> bool: + """ + Функция осуществляет проверку существования пользователя в Zendesk по email. + + :param email: Email пользователя + :return: Является ли зарегистрированным + """ + return True if self.admin.search(email, type='user') else False + + def get_user(self, email: str) -> ZenpyUser: + """ + Функция возвращает пользователя (объект) по его email. + + :param email: Email пользователя + :return: Объект пользователя, найденного в БД + """ + return self.admin.users.search(email).values[0] + + def get_group(self, name: str) -> Optional[ZenpyGroup]: + """ + Функция возвращает группу по названию + + :param name: Имя пользователя + :return: Группы пользователя (в случае отсутствия None) + """ + groups = self.admin.search(name, type='group') + for group in groups: + return group + return None + + def get_user_org(self, email: str) -> str: + """ + Функция возвращает организацию, к которой относится пользователь по его 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: + """ + Функция создает администратора, проверяя наличие вводимых данных в env. + + :raise: :class:`ValueError`: исключение, вызываемое если email не введен в env + :raise: :class:`APIException`: исключение, вызываемое если пользователя с таким email не существует в Zendesk + """ + + if self.credentials.get('email') is None: + raise ValueError('access_controller email not in env') + + if self.credentials.get('token') is None and self.credentials.get('password') is None: + raise ValueError('access_controller token or password not in env') + + admin = Zenpy(**self.credentials) + try: + admin.search(self.credentials['email'], type='user') + except APIException: + raise ValueError('invalid access_controller`s login data') + + return admin + + +zenpy = ZendeskAdmin({ + 'subdomain': ACTRL_ZENDESK_SUBDOMAIN, + 'email': ACTRL_API_EMAIL, + 'token': ACTRL_API_TOKEN, + 'password': ACTRL_API_PASSWORD, +}) From e886004069a26f9bda6666aa0bf62e28929d90f1 Mon Sep 17 00:00:00 2001 From: Iurii Tatishchev Date: Thu, 6 May 2021 09:07:21 -0700 Subject: [PATCH 07/13] Remove unnecessary files. --- main/lib/__init__.py | 0 main/lib/__pycache__/__init__.cpython-39.pyc | Bin 200 -> 0 bytes .../lib/__pycache__/extra_func.cpython-39.pyc | Bin 11882 -> 0 bytes .../__pycache__/statistic_data.cpython-39.pyc | Bin 11471 -> 0 bytes .../__pycache__/zendesk_admin.cpython-39.pyc | Bin 4804 -> 0 bytes main/lib/extra_func.py | 319 ------------------ main/lib/statistic_data.py | 261 -------------- main/lib/zendesk_admin.py | 99 ------ 8 files changed, 679 deletions(-) delete mode 100644 main/lib/__init__.py delete mode 100644 main/lib/__pycache__/__init__.cpython-39.pyc delete mode 100644 main/lib/__pycache__/extra_func.cpython-39.pyc delete mode 100644 main/lib/__pycache__/statistic_data.cpython-39.pyc delete mode 100644 main/lib/__pycache__/zendesk_admin.cpython-39.pyc delete mode 100644 main/lib/extra_func.py delete mode 100644 main/lib/statistic_data.py delete mode 100644 main/lib/zendesk_admin.py diff --git a/main/lib/__init__.py b/main/lib/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/main/lib/__pycache__/__init__.cpython-39.pyc b/main/lib/__pycache__/__init__.cpython-39.pyc deleted file mode 100644 index 70ee7f6ed0f60b941986434ab357ce214c10c586..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 200 zcmYe~<>g`k0z&ryk0@&Ee@O9 Q{FKt1R6CF}J_9iW0F?tVr2qf` diff --git a/main/lib/__pycache__/extra_func.cpython-39.pyc b/main/lib/__pycache__/extra_func.cpython-39.pyc deleted file mode 100644 index 506e57c529b5d7bf576e43a4ca5b8b21158116e7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11882 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 a0}p(kQlm{aOu>hA=~~aU3ZKgFfBp+R{3Hed diff --git a/main/lib/extra_func.py b/main/lib/extra_func.py deleted file mode 100644 index ad251cd..0000000 --- a/main/lib/extra_func.py +++ /dev/null @@ -1,319 +0,0 @@ -import logging -from datetime import timedelta - -from django.contrib.auth.models import User -from django.core.exceptions import ObjectDoesNotExist -from django.shortcuts import redirect -from django.utils import timezone -from zenpy import Zenpy -from zenpy.lib.exception import APIException -from zenpy.lib.api_objects import User as ZenpyUser, Ticket as ZenpyTicket -from zenpy.lib.generator import SearchResultGenerator - -from access_controller.settings import ZENDESK_ROLES as ROLES, ACTRL_ZENDESK_SUBDOMAIN -from main.models import UserProfile, RoleChangeLogs, UnassignedTicket, UnassignedTicketStatus -from main.requester import TicketListRequester -from main.lib.zendesk_admin import zenpy - - -def update_role(user_profile: UserProfile, role: int, who_changes: User) -> None: - """ - Функция меняет роль пользователя. - - :param user_profile: Профиль пользователя - :param role: Новая роль - :param who_changes: Пользователь, меняющий роль - :return: Пользователь с обновленной ролью - """ - zendesk = zenpy - user = zendesk.get_user(user_profile.user.email) - user.custom_role_id = role - user_profile.custom_role_id = role - user_profile.save() - log(user_profile, who_changes.userprofile) - zendesk.update_user(user) - - -def make_engineer(user_profile: UserProfile, who_changes: User) -> None: - """ - Функция устанавливает пользователю роль инженера. - - :param user_profile: Профиль пользователя - :return: Вызов функции **update_role** с параметрами: профиль пользователя, роль "engineer" - """ - update_role(user_profile, ROLES['engineer'], who_changes) - - -def make_light_agent(user_profile: UserProfile, who_changes: User) -> None: - """ - Функция устанавливает пользователю роль легкого агента. - - :param user_profile: Профиль пользователя - :return: Вызов функции **update_role** с параметрами: профиль пользователя, роль "light_agent" - """ - tickets: SearchResultGenerator = get_tickets_list(user_profile.user.email) - ticket: ZenpyTicket - 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_id = zenpy.solved_tickets_user_id - else: - ticket.assignee = None - ticket.group_id = zenpy.buffer_group_id - if tickets: - zenpy.admin.tickets.update(tickets) - attempts, success = 20, False - while not success and attempts != 0: - try: - update_role(user_profile, ROLES['light_agent'], who_changes) - success = True - except APIException as e: - attempts -= 1 - if attempts == 0: - raise e - - -def get_users_list() -> list: - """ - Функция **get_users_list** возвращает список пользователей Zendesk, относящихся к организации SYSTEM. - """ - zendesk = zenpy - - # У пользователей должна быть организация 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 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): - """ - Функция обновляет профиль пользователя в соответствии с текущим в Zendesk. - - :param user_profile: Профиль пользователя - :return: Обновленный, в соответствие с текущими данными в Zendesk, профиль пользователя - """ - user = zenpy.get_user(user_profile.user.email) - user_profile.name = user.name - user_profile.role = user.role - user_profile.custom_role_id = user.custom_role_id if user.custom_role_id else 0 - user_profile.image = user.photo['content_url'] if user.photo else None - user_profile.save() - - -def check_user_exist(email: str) -> bool: - """ - Функция проверяет, существует ли пользователь. - - :param email: Email пользователя - :return: Зарегистрирован ли пользователь в Zendesk - """ - return zenpy.check_user(email) - - -def get_user_organization(email: str) -> str: - """ - Функция возвращает организацию пользователя. - - :param email: Email пользователя - :return: Организация пользователя - """ - return zenpy.get_user_org(email) - - -def check_user_auth(email: str, password: str) -> bool: - """ - Функция проверяет, верны ли входные данные. - - :raise: :class:`APIException`: исключение, вызываемое если пользователь не аутентифицирован - """ - creds = { - 'email': email, - 'password': password, - 'subdomain': ACTRL_ZENDESK_SUBDOMAIN, - } - try: - user = Zenpy(**creds) - user.search(email, type='user') - except APIException: - return False - return True - - -def update_user_in_model(profile: UserProfile, zendesk_user: ZenpyUser): - """ - Функция обновляет профиль пользователя при изменении данных пользователя на 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, исключая правую границу. - - :param start_date: Начальная дата - :param end_date: Конечная дата - :return: Список дней, не включая конечную дату - """ - dates = [] - for n in range(int((end_date - start_date).days)): - dates.append(start_date + timedelta(n)) - return dates - - -def get_timedelta(log, time=None) -> timedelta: - """ - Функция возвращает объект класса timedelta, который хранит промежуток времени от начала суток до момента, - который находится в log (объект класса RoleChangeLogs) или в time(datetime.time), если введён. - - :param log: Лог - :param time: Время - :return: Сколько времени прошло от начала суток до события - """ - if time is None: - time = log.change_time.time() - time = timedelta(hours=time.hour, minutes=time.minute, seconds=time.second) - return time - - -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 DatabaseHandler(logging.Handler): - def __init__(self): - logging.Handler.__init__(self) - - def emit(self, record): - database = RoleChangeLogs() - users = record.msg - if users[1]: - user = users[0] - admin = users[1] - elif not users[1]: - user = users[0] - admin = users[0] - database.name = user.name - database.user = user.user - database.changed_by = admin.user - if user.custom_role_id == ROLES['engineer']: - database.old_role = ROLES['light_agent'] - elif user.custom_role_id == ROLES['light_agent']: - database.old_role = ROLES['engineer'] - database.new_role = user.custom_role_id - database.save() - - -class CsvFormatter(logging.Formatter): - def __init__(self): - logging.Formatter.__init__(self) - - def format(self, record): - users = record.msg - if users[1]: - user = users[0] - admin = users[1] - elif not users[1]: - user = users[0] - admin = users[0] - msg = '' - msg += user.name - if user.custom_role_id == ROLES['engineer']: - msg += ',engineer,' - elif user.custom_role_id == ROLES['light_agent']: - msg += ',light_agent,' - time = str(timezone.now().today()) - msg += time[:16] - msg += ',' - msg += admin.name - return msg - - -def log(user, admin=None): - """ - Осуществляет запись логов в базу данных и csv файл - :param admin: - :param user: - :return: - """ - users = [user, admin] - logger = logging.getLogger('MY_LOGGER') - if not logger.hasHandlers(): - dbhandler = DatabaseHandler() - csvformatter = CsvFormatter() - csvhandler = logging.FileHandler('logs/logs.csv', "a") - csvhandler.setFormatter(csvformatter) - logger.addHandler(dbhandler) - logger.addHandler(csvhandler) - logger.setLevel('INFO') - logger.info(users) - - -def set_session_params_for_work_page(request, count=None, is_confirm=True): - """ - Функция для страницы получения прав - Устанавливает данные сессии о успешности запроса и количестве назначенных тикетов - """ - request.session['is_confirm'] = is_confirm - request.session['count_tickets'] = count - return redirect('work', request.user.id) diff --git a/main/lib/statistic_data.py b/main/lib/statistic_data.py deleted file mode 100644 index 2e1061d..0000000 --- a/main/lib/statistic_data.py +++ /dev/null @@ -1,261 +0,0 @@ -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.lib.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) diff --git a/main/lib/zendesk_admin.py b/main/lib/zendesk_admin.py deleted file mode 100644 index 2a689ce..0000000 --- a/main/lib/zendesk_admin.py +++ /dev/null @@ -1,99 +0,0 @@ -from typing import Optional, Dict -from zenpy import Zenpy -from zenpy.lib.api_objects import User as ZenpyUser, Group as ZenpyGroup -from zenpy.lib.exception import APIException -from access_controller.settings import ACTRL_ZENDESK_SUBDOMAIN, ACTRL_API_EMAIL, ACTRL_API_TOKEN, ACTRL_API_PASSWORD, \ - ZENDESK_GROUPS, SOLVED_TICKETS_EMAIL - - -class ZendeskAdmin: - """ - Класс **ZendeskAdmin** существует, чтобы в каждой функции отдельно не проверять аккаунт администратора. - - :param credentials: Полномочия (первым указывается учетная запись организации в Zendesk) - :type credentials: :class:`Dict[str, str]` - """ - - def __init__(self, credentials: Dict[str, str]): - self.credentials = credentials - self.admin = self.create_admin() - self.buffer_group_id: int = self.get_group(ZENDESK_GROUPS['buffer']).id - self.solved_tickets_user_id: int = self.get_user(SOLVED_TICKETS_EMAIL).id - - def update_user(self, user: ZenpyUser) -> bool: - """ - Функция сохраняет изменение пользователя в Zendesk. - - :param user: Пользователь с изменёнными данными - """ - self.admin.users.update(user) - - def check_user(self, email: str) -> bool: - """ - Функция осуществляет проверку существования пользователя в Zendesk по email. - - :param email: Email пользователя - :return: Является ли зарегистрированным - """ - return True if self.admin.search(email, type='user') else False - - def get_user(self, email: str) -> ZenpyUser: - """ - Функция возвращает пользователя (объект) по его email. - - :param email: Email пользователя - :return: Объект пользователя, найденного в БД - """ - return self.admin.users.search(email).values[0] - - def get_group(self, name: str) -> Optional[ZenpyGroup]: - """ - Функция возвращает группу по названию - - :param name: Имя пользователя - :return: Группы пользователя (в случае отсутствия None) - """ - groups = self.admin.search(name, type='group') - for group in groups: - return group - return None - - def get_user_org(self, email: str) -> str: - """ - Функция возвращает организацию, к которой относится пользователь по его 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: - """ - Функция создает администратора, проверяя наличие вводимых данных в env. - - :raise: :class:`ValueError`: исключение, вызываемое если email не введен в env - :raise: :class:`APIException`: исключение, вызываемое если пользователя с таким email не существует в Zendesk - """ - - if self.credentials.get('email') is None: - raise ValueError('access_controller email not in env') - - if self.credentials.get('token') is None and self.credentials.get('password') is None: - raise ValueError('access_controller token or password not in env') - - admin = Zenpy(**self.credentials) - try: - admin.search(self.credentials['email'], type='user') - except APIException: - raise ValueError('invalid access_controller`s login data') - - return admin - - -zenpy = ZendeskAdmin({ - 'subdomain': ACTRL_ZENDESK_SUBDOMAIN, - 'email': ACTRL_API_EMAIL, - 'token': ACTRL_API_TOKEN, - 'password': ACTRL_API_PASSWORD, -}) From 63949dd54e79c21c2d73edd9790fac18accab834 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B5=D0=B2=20=D0=94=D0=BC?= =?UTF-8?q?=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Mon, 10 May 2021 15:05:34 +0000 Subject: [PATCH 08/13] Fix/header --- data.json | 5 +++-- main/templates/base/menu.html | 34 ++++++++++++++++++++-------------- main/views.py | 31 ++++++------------------------- 3 files changed, 29 insertions(+), 41 deletions(-) diff --git a/data.json b/data.json index a4310a4..8b2e666 100644 --- a/data.json +++ b/data.json @@ -22,8 +22,9 @@ "pk": 1, "fields": { "name": "ZendeskAdmin", - "user": 1, - "role": "admin" + "user": 3, + "role": "admin", + "user_id": 1 } }, { diff --git a/main/templates/base/menu.html b/main/templates/base/menu.html index ef5df18..17c5f81 100644 --- a/main/templates/base/menu.html +++ b/main/templates/base/menu.html @@ -3,56 +3,62 @@ -
-
- - -
+
+ {% csrf_token %} + {{ get_tickets_form.count_tickets }} + +
{% for message in messages %} diff --git a/main/tests.py b/main/tests.py index 5f71955..59df965 100644 --- a/main/tests.py +++ b/main/tests.py @@ -1,5 +1,5 @@ import random -from unittest.mock import patch +from unittest.mock import patch, Mock from django.contrib.auth.models import User from django.core import mail @@ -43,7 +43,7 @@ class RegistrationTestCase(TestCase): self.client.post(reverse('registration'), data={'email': self.any_zendesk_user_email}) self.assertEqual(response.status_code, 302) self.assertEqual(len(mail.outbox), 1) - self.assertEqual(mail.outbox[0].to, [self.zendesk_admin_email]) + self.assertEqual(mail.outbox[0].to, [self.any_zendesk_user_email]) # context that the email template was rendered with email_context = response.context[0].dicts[1] @@ -262,3 +262,93 @@ class PasswordChangeTestCase(TestCase): } ) self.assertContains(resp, 'Введённый пароль слишком похож на имя пользователя', count=1, status_code=200) + + +class GetTicketsTestCase(TestCase): + """ + Класс тестов для проверки функции получения тикетов. + """ + fixtures = ['fixtures/test_make_engineer.json'] + + def setUp(self): + """ + Предустановленные значения для проведения тестов. + """ + self.light_agent = '123@test.ru' + self.engineer = 'customer@example.com' + self.client = Client() + self.client.force_login(User.objects.get(email=self.engineer)) + self.light_agent_client = Client() + self.light_agent_client.force_login(User.objects.get(email=self.light_agent)) + + @patch('main.views.zenpy.get_user') + @patch('main.extra_func.zenpy') + def test_redirect(self, ZenpyMock, GetUserMock): + """ + Функция проверки переадресации пользователя на рабочую страницу. + """ + GetUserMock.return_value = Mock() + user = User.objects.get(email=self.engineer) + resp = self.client.post(reverse('work_get_tickets')) + self.assertRedirects(resp, reverse('work', args=[user.id])) + self.assertEqual(resp.status_code, 302) + + @patch('main.views.zenpy') + @patch('main.views.get_tickets_list_for_group') + def test_take_one_ticket(self, TicketsMock, ZenpyViewsMock): + """ + Функция проверки назначения одного тикета на engineer. + """ + TicketsMock.return_value = [Mock()] + ZenpyViewsMock.get_user.return_value = Mock(role='agent', custom_role_id=sets.ZENDESK_ROLES['engineer']) + self.client.post(reverse('work_get_tickets'), data={'count_tickets': 1}) + tickets = ZenpyViewsMock.update_tickets.call_args + self.assertEqual(tickets[0][0][0].assignee, ZenpyViewsMock.get_user.return_value) + + @patch('main.views.get_tickets_list_for_group') + @patch('main.views.zenpy') + def test_take_many_tickets(self, ZenpyMock, TicketsMock): + """ + Функция проверки назначения нескольких тикетов на engineer. + """ + TicketsMock.return_value = [Mock()] * 3 + ZenpyMock.get_user.return_value = Mock(role='agent', custom_role_id=sets.ZENDESK_ROLES['engineer']) + self.client.post(reverse('work_get_tickets'), data={'count_tickets': 3}) + tickets = ZenpyMock.update_tickets.call_args + for ticket in tickets[0][0]: + self.assertEqual(ticket.assignee, ZenpyMock.get_user.return_value) + + @patch('main.views.zenpy.get_user') + @patch('main.views.zenpy') + def test_light_agent_take_ticket(self, ZenpyMock, GetUserMock): + """ + Функция проверки попытки назначения тикета на light_agent. + """ + GetUserMock.return_value = Mock(role='agent', custom_role_id=sets.ZENDESK_ROLES['light_agent']) + self.light_agent_client.post(reverse('work_get_tickets'), data={'count_tickets': 3}) + tickets = ZenpyMock.update_tickets.call_args + self.assertIsNone(tickets) + + @patch('main.views.zenpy') + @patch('main.views.get_tickets_list_for_group') + def test_take_zero_tickets(self, TicketsMock, ZenpyMock): + """ + Функция проверки попытки назначения нуля тикета на engineer. + """ + TicketsMock.return_value = [Mock()] * 3 + ZenpyMock.get_user.return_value = Mock(role='agent', custom_role_id=sets.ZENDESK_ROLES['engineer']) + self.client.post(reverse('work_get_tickets'), data={'count_tickets': 0}) + tickets = ZenpyMock.update_tickets.call_args[0][0] + self.assertListEqual(tickets, []) + + @patch('main.views.get_tickets_list_for_group') + @patch('main.views.zenpy') + def test_take_invalid_count_tickets(self, ZenpyMock, TicketsMock, ): + """ + Функция проверки попытки назначения нуля тикетов на engineer. + """ + TicketsMock.return_value = [Mock()] * 3 + ZenpyMock.get_user.return_value = Mock(role='agent', custom_role_id=sets.ZENDESK_ROLES['engineer']) + self.client.post(reverse('work_get_tickets'), data={'count_tickets': 'asd'}) + tickets = ZenpyMock.update_tickets.call_args + self.assertIsNone(tickets) diff --git a/main/views.py b/main/views.py index 22978c2..4602bcf 100644 --- a/main/views.py +++ b/main/views.py @@ -1,19 +1,18 @@ from smtplib import SMTPException -from typing import Dict, Any from django.contrib import messages from django.contrib.auth.decorators import login_required +from django.contrib.auth.forms import PasswordResetForm from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin from django.contrib.auth.models import User, Permission from django.contrib.auth.tokens import default_token_generator -from django.contrib.auth.forms import PasswordResetForm from django.contrib.auth.views import LoginView from django.contrib.contenttypes.models import ContentType from django.contrib.messages.views import SuccessMessageMixin from django.core.handlers.wsgi import WSGIRequest from django.http import HttpResponseRedirect, HttpResponse from django.shortcuts import render, redirect -from django.urls import reverse_lazy, reverse +from django.urls import reverse_lazy from django.views.generic import FormView from django_registration.views import RegistrationView # Django REST @@ -23,13 +22,13 @@ from rest_framework.response import Response from access_controller.settings import DEFAULT_FROM_EMAIL, ZENDESK_ROLES, ZENDESK_MAX_AGENTS, ZENDESK_GROUPS from main.extra_func import check_user_exist, update_profile, get_user_organization, \ make_engineer, make_light_agent, get_users_list, update_users_in_model, count_users, \ - log, set_session_params_for_work_page, get_tickets_list_for_group -from .statistic_data import StatisticData -from main.zendesk_admin import zenpy -from main.requester import TicketListRequester -from main.forms import AdminPageUsers, CustomRegistrationForm, CustomAuthenticationForm, StatisticForm + set_session_params_for_work_page, get_tickets_list_for_group +from main.forms import AdminPageUsers, CustomRegistrationForm, CustomAuthenticationForm, StatisticForm, \ + WorkGetTicketsForm from main.serializers import ProfileSerializer, ZendeskUserSerializer +from main.zendesk_admin import zenpy from .models import UserProfile +from .statistic_data import StatisticData class CustomRegistrationView(RegistrationView): @@ -178,6 +177,7 @@ def work_page(request: WSGIRequest, id: int) -> HttpResponse: 'messages': messages.get_messages(request), 'licences_remaining': max(0, ZENDESK_MAX_AGENTS - len(engineers)), 'pagename': 'Управление правами', + 'get_tickets_form': WorkGetTicketsForm() } return render(request, 'pages/work.html', context) return redirect("login") @@ -211,21 +211,18 @@ def work_become_engineer(request: WSGIRequest) -> HttpResponseRedirect: @login_required() def work_get_tickets(request): zenpy_user = zenpy.get_user(request.user.email) - if zenpy_user.role == 'admin' or zenpy_user.custom_role_id == ZENDESK_ROLES['engineer']: - tickets = get_tickets_list_for_group(ZENDESK_GROUPS['buffer']) - assigned_tickets = [] - count = 0 - for i in range(len(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) - tickets[i].assignee = zenpy_user - assigned_tickets.append(tickets[i]) - count += 1 - if assigned_tickets: - zenpy.admin.tickets.update(assigned_tickets) - return set_session_params_for_work_page(request, count) + + if request.method == 'POST': + if zenpy_user.role == 'admin' or zenpy_user.custom_role_id == ZENDESK_ROLES['engineer']: + form = WorkGetTicketsForm(request.POST) + if form.is_valid(): + tickets = get_tickets_list_for_group(ZENDESK_GROUPS['buffer']) + assigned_tickets = [] + for i in range(min(form.cleaned_data['count_tickets'], len(tickets))): + tickets[i].assignee = zenpy_user + assigned_tickets.append(tickets[i]) + zenpy.update_tickets(assigned_tickets) + return set_session_params_for_work_page(request, len(assigned_tickets)) return set_session_params_for_work_page(request, is_confirm=False) diff --git a/main/zendesk_admin.py b/main/zendesk_admin.py index 2a689ce..627d900 100644 --- a/main/zendesk_admin.py +++ b/main/zendesk_admin.py @@ -1,7 +1,9 @@ -from typing import Optional, Dict +from typing import Optional, Dict, List + 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, Ticket as ZenpyTicket from zenpy.lib.exception import APIException + from access_controller.settings import ACTRL_ZENDESK_SUBDOMAIN, ACTRL_API_EMAIL, ACTRL_API_TOKEN, ACTRL_API_PASSWORD, \ ZENDESK_GROUPS, SOLVED_TICKETS_EMAIL @@ -28,6 +30,15 @@ class ZendeskAdmin: """ self.admin.users.update(user) + def update_tickets(self, tickets: List[ZenpyTicket]): + """ + Функция сохраняет изменение тикетов в Zendesk. + + :param tickets: Тикеты с изменёнными данными + """ + if tickets: + self.admin.tickets.update(tickets) + def check_user(self, email: str) -> bool: """ Функция осуществляет проверку существования пользователя в Zendesk по email. From 74f8b1ce1fbad9c334e2a276b2d69b816869dcaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=BE=D0=BA=D1=83=D1=80=D0=BE=D0=B2=20=D0=98=D0=B4?= =?UTF-8?q?=D0=B0=D1=80?= Date: Thu, 13 May 2021 04:57:51 +0000 Subject: [PATCH 12/13] Add profile tests --- fixtures/profile.json | 59 ++++++++++++++++++++++++++++++++++++++++++ main/tests.py | 60 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 fixtures/profile.json diff --git a/fixtures/profile.json b/fixtures/profile.json new file mode 100644 index 0000000..8ce02db --- /dev/null +++ b/fixtures/profile.json @@ -0,0 +1,59 @@ +[ + { + "model": "auth.user", + "pk": 1, + "fields": { + "password": "pbkdf2_sha256$216000$gHBBCr1jBELf$ZkEDW3IEd8Wij7u8vkv+0Eze32CS01bcaYWhcD9OIC4=", + "last_login": null, + "is_superuser": true, + "username": "idar.sokurov.05@mail.ru", + "first_name": "", + "last_name": "", + "email": "idar.sokurov.05@mail.ru", + "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": "krav-88@mail.ru", + "first_name": "", + "last_name": "", + "email": "krav-88@mail.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" + } + } +] diff --git a/main/tests.py b/main/tests.py index 59df965..b086488 100644 --- a/main/tests.py +++ b/main/tests.py @@ -352,3 +352,63 @@ class GetTicketsTestCase(TestCase): self.client.post(reverse('work_get_tickets'), data={'count_tickets': 'asd'}) tickets = ZenpyMock.update_tickets.call_args self.assertIsNone(tickets) + + +class ProfileTestCase(TestCase): + """ + Класс тестов для проверки синхронизации профиля пользователя. + """ + fixtures = ['fixtures/profile.json'] + + def setUp(self): + """ + Предустановленные значения для проведения тестов. + """ + self.zendesk_agent_email = 'krav-88@mail.ru' + self.zendesk_admin_email = 'idar.sokurov.05@mail.ru' + self.client = Client() + self.client.force_login(User.objects.get(email=self.zendesk_agent_email)) + self.admin_client = Client() + self.admin_client.force_login(User.objects.get(email=self.zendesk_admin_email)) + + def test_correct_username(self): + """ + Функция проверки синхронизации имени пользователя. + """ + resp = self.client.get(reverse('profile')) + self.assertEqual(resp.context['profile'].name, zenpy.get_user(self.zendesk_agent_email).name) + + def test_correct_email(self): + """ + Функция проверки синхронизации почты пользователя. + """ + resp = self.client.get(reverse('profile')) + self.assertEqual(resp.context['profile'].user.email, zenpy.get_user(self.zendesk_agent_email).email) + + def test_correct_role(self): + """ + Функция проверки синхронизации роли пользователя. + """ + resp = self.client.get(reverse('profile')) + self.assertEqual(resp.context['profile'].role, zenpy.get_user(self.zendesk_agent_email).role) + resp = self.admin_client.get(reverse('profile')) + self.assertEqual(resp.context['profile'].role, zenpy.get_user(self.zendesk_admin_email).role) + + def test_correct_custom_role_id(self): + """ + Функция проверки синхронизации рабочей роли пользователя. + """ + resp = self.client.get(reverse('profile')) + user = zenpy.get_user(self.zendesk_agent_email) + self.assertEqual(resp.context['profile'].custom_role_id, user.custom_role_id if user.custom_role_id else 0) + resp = self.admin_client.get(reverse('profile')) + user = zenpy.get_user(self.zendesk_admin_email) + self.assertEqual(resp.context['profile'].custom_role_id, user.custom_role_id if user.custom_role_id else 0) + + def test_correct_image(self): + """ + Функция проверки синхронизации изображения пользователя. + """ + resp = self.client.get(reverse('profile')) + user = zenpy.get_user(self.zendesk_agent_email) + self.assertEqual(resp.context['profile'].image, user.photo['content_url'] if user.photo else None) From 86995e01b3a32d97adcc1860f2e62b5e46f25ad7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A2=D0=B0=D1=82=D0=B8=D1=89=D0=B5=D0=B2=20=D0=AE=D1=80?= =?UTF-8?q?=D0=B8=D0=B9?= Date: Thu, 13 May 2021 17:04:56 +0000 Subject: [PATCH 13/13] Add `python-dotenv` --- README.md | 8 ++++---- access_controller/settings.py | 4 ++++ requirements/common.txt | 3 +++ 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index eb66f37..8489e7d 100644 --- a/README.md +++ b/README.md @@ -48,9 +48,9 @@ cp .env.example .env sudo apt install make pip install --upgrade pip pip install -r requirements/dev.txt -(set -a && source .env && ./manage.py migrate) -(set -a && source .env && ./manage.py loaddata data.json) -(set -a && source .env && ./manage.py runserver) +./manage.py migrate +./manage.py loaddata data.json +./manage.py runserver ``` ## Перед запуском для тестирования: @@ -65,7 +65,7 @@ pip install -r requirements/dev.txt - Перейти в папку приложения - Активировать виртуальное окружение - Выполнить команду `pip install -r requirements/dev.txt` -- В виртуальное окружение добавить следующие переменные: +- В файл `.env` добавить следующие переменные: ``` diff --git a/access_controller/settings.py b/access_controller/settings.py index a74ad7f..7361a60 100644 --- a/access_controller/settings.py +++ b/access_controller/settings.py @@ -11,6 +11,7 @@ https://docs.djangoproject.com/en/3.1/ref/settings/ """ import os from pathlib import Path +from dotenv import load_dotenv # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -18,6 +19,9 @@ BASE_DIR = Path(__file__).resolve().parent.parent # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/ +# Load environment variables from .env +load_dotenv() + # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = os.getenv('ACTRL_SECRET_KEY', 'empty') diff --git a/requirements/common.txt b/requirements/common.txt index 6b3e7fa..0bbb21b 100644 --- a/requirements/common.txt +++ b/requirements/common.txt @@ -14,3 +14,6 @@ sphinx-rtd-theme==0.5.1 sphinx-autodoc-typehints==1.11.1 pyenchant==3.2.0 sphinxcontrib-spelling==7.1.0 + +# Misc +python-dotenv==0.17.1