Merge with "develop", begin to check again

This commit is contained in:
Степаненко Ольга 2021-05-10 20:43:17 +03:00
parent 492260dc79
commit 015016bd6e
6 changed files with 42 additions and 291 deletions

View File

@ -3,8 +3,8 @@ ACTRL_DEBUG=1
ACTRL_SECRET_KEY="v1i_fb\$_jf2#1v_lcsbu&eon4u-os0^px=s^iycegdycqy&5)6" ACTRL_SECRET_KEY="v1i_fb\$_jf2#1v_lcsbu&eon4u-os0^px=s^iycegdycqy&5)6"
ACTRL_HOST="actrl.example.com" ACTRL_HOST="actrl.example.com"
ACTRL_EMAIL_HOST="smtp.mail.ru" ACTRL_EMAIL_HOST="smtp.gmail.com"
ACTRL_EMAIL_PORT=2525 ACTRL_EMAIL_PORT=587
ACTRL_EMAIL_TLS=1 ACTRL_EMAIL_TLS=1
ACTRL_EMAIL_HOST_USER="djgr.02@mail.ru" ACTRL_EMAIL_HOST_USER="djgr.02@mail.ru"
ACTRL_EMAIL_HOST_PASSWORD="djangogroup02" ACTRL_EMAIL_HOST_PASSWORD="djangogroup02"

View File

@ -1,7 +1,7 @@
[ [
{ {
"model": "auth.user", "model": "auth.user",
"pk": 3, "pk": 1,
"fields": { "fields": {
"password": "pbkdf2_sha256$216000$gHBBCr1jBELf$ZkEDW3IEd8Wij7u8vkv+0Eze32CS01bcaYWhcD9OIC4=", "password": "pbkdf2_sha256$216000$gHBBCr1jBELf$ZkEDW3IEd8Wij7u8vkv+0Eze32CS01bcaYWhcD9OIC4=",
"last_login": null, "last_login": null,
@ -19,16 +19,16 @@
}, },
{ {
"model": "main.userprofile", "model": "main.userprofile",
"pk": 3, "pk": 1,
"fields": { "fields": {
"name": "ZendeskAdmin", "name": "ZendeskAdmin",
"user": 3, "user": 1,
"role": "admin" "role": "admin"
} }
}, },
{ {
"model": "auth.user", "model": "auth.user",
"pk": 4, "pk": 2,
"fields": { "fields": {
"password": "pbkdf2_sha256$216000$5qLJgrm2Quq9$KDBNNymVZXkUx0HKBPFst2m83kLe0egPBnkW7KnkORU=", "password": "pbkdf2_sha256$216000$5qLJgrm2Quq9$KDBNNymVZXkUx0HKBPFst2m83kLe0egPBnkW7KnkORU=",
"last_login": null, "last_login": null,
@ -46,10 +46,10 @@
}, },
{ {
"model": "main.userprofile", "model": "main.userprofile",
"pk": 4, "pk": 2,
"fields": { "fields": {
"name": "UserForAccessTest", "name": "UserForAccessTest",
"user": 4, "user": 2,
"role": "agent", "role": "agent",
"custom_role_id": "360005209000" "custom_role_id": "360005209000"
} }

View File

@ -244,258 +244,6 @@ def last_day_of_month(day: int) -> int:
return next_month - timedelta(days=next_month.day) 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): class DatabaseHandler(logging.Handler):
""" """
Класс записи изменений ролей в базу данных. Класс записи изменений ролей в базу данных.

View File

@ -37,7 +37,6 @@ class UserProfile(models.Model):
:return: Роль пользователя в Zendesk :return: Роль пользователя в Zendesk
""" """
id = self.custom_role_id
for role, r_id in ZENDESK_ROLES.items(): for role, r_id in ZENDESK_ROLES.items():
if r_id == self.custom_role_id: if r_id == self.custom_role_id:
return role return role

View File

@ -1,7 +1,10 @@
"""
Обработка статистики.
"""
from datetime import date, datetime, timedelta from datetime import date, datetime, timedelta
from typing import Optional from typing import Optional
from django.contrib.auth.models import User from django.contrib.auth import get_user_model
from django.utils import timezone from django.utils import timezone
from access_controller.settings import ONE_DAY, ZENDESK_ROLES as ROLES from access_controller.settings import ONE_DAY, ZENDESK_ROLES as ROLES
@ -50,19 +53,19 @@ class StatisticData:
else: else:
self.statistic = stat self.statistic = stat
def get_statistic(self) -> dict: def get_statistic(self) -> Optional[dict]:
""" """
Функция возвращает статистику работы пользователя. Функция возвращает статистику работы пользователя.
:return: Словарь statistic с применением формата отображения и интервала работы(если они есть). None, если были ошибки при создании. :return: Словарь statistic с применением формата отображения и интервала работы(если они есть).
None, если были ошибки при создании.
""" """
if self.is_valid_statistic(): if self.is_valid_statistic():
stat = self.statistic stat = self.statistic
stat = self._use_display(stat) stat = self._use_display(stat)
stat = self._use_interval(stat) stat = self._use_interval(stat)
return stat return stat
else: return None
return None
def is_valid_statistic(self) -> bool: def is_valid_statistic(self) -> bool:
""" """
@ -104,8 +107,7 @@ class StatisticData:
""" """
if self.is_valid_data(): if self.is_valid_data():
return self.data return self.data
else: return None
return None
def is_valid_data(self) -> bool: def is_valid_data(self) -> bool:
""" """
@ -170,7 +172,8 @@ class StatisticData:
""" """
Функция возвращает логи в диапазоне дат start_date - end_date для пользователя с указанным email. Функция возвращает логи в диапазоне дат start_date - end_date для пользователя с указанным email.
:return: Данные о смене статусов пользователя. Если пользователь не найден или интервал времени некорректен - ошибку. :return: Данные о смене статусов пользователя. Если пользователь не найден или
интервал времени некорректен - ошибку.
""" """
if not self.check_time(): if not self.check_time():
self.errors += ['Конец диапазона должен быть позже начала диапазона и раньше текущего времени'] self.errors += ['Конец диапазона должен быть позже начала диапазона и раньше текущего времени']
@ -178,12 +181,12 @@ class StatisticData:
try: try:
self.data = RoleChangeLogs.objects.filter( self.data = RoleChangeLogs.objects.filter(
change_time__range=[self.start_date, self.end_date + timedelta(days=1)], change_time__range=[self.start_date, self.end_date + timedelta(days=1)],
user=User.objects.get(email=self.email), user=get_user_model().objects.get(email=self.email),
).order_by('change_time') ).order_by('change_time')
except User.DoesNotExist: except get_user_model().DoesNotExist:
self.errors += ['Пользователь не найден'] self.errors += ['Пользователь не найден']
def _init_statistic(self) -> dict: def _init_statistic(self) -> None:
""" """
Функция заполняет словарь, в котором ключ - дата, значение - кол-во проработанных в этот день секунд. Функция заполняет словарь, в котором ключ - дата, значение - кол-во проработанных в этот день секунд.
@ -192,18 +195,18 @@ class StatisticData:
self.clear_statistic() self.clear_statistic()
if not self.get_data(): if not self.get_data():
self.warnings += ['Не обнаружены изменения роли в данном промежутке'] self.warnings += ['Не обнаружены изменения роли в данном промежутке']
return None else:
first_log, last_log = self.data[0], self.data[len(self.data) - 1] first_log, last_log = self.data[0], self.data[len(self.data) - 1]
if first_log.old_role == ROLES['engineer']: if first_log.old_role == ROLES['engineer']:
self.prev_engineer_logic(first_log) self.prev_engineer_logic(first_log)
if last_log.new_role == ROLES['engineer']: if last_log.new_role == ROLES['engineer']:
self.post_engineer_logic(last_log) self.post_engineer_logic(last_log)
for log_index in range(len(self.data) - 1): for log_index in range(len(self.data) - 1):
if self.data[log_index].new_role == ROLES['engineer']: if self.data[log_index].new_role == ROLES['engineer']:
self.engineer_logic(log_index) self.engineer_logic(log_index)
def engineer_logic(self, log_index): def engineer_logic(self, log_index):
""" """
@ -238,7 +241,7 @@ class StatisticData:
""" """
Функция обрабатывает случай, когда нам изветсно что инженер начал работу до диапазона Функция обрабатывает случай, когда нам изветсно что инженер начал работу до диапазона
""" """
self.fill_daterange(max(User.objects.get(email=self.email).date_joined.date(), self.start_date), self.fill_daterange(max(get_user_model().objects.get(email=self.email).date_joined.date(), self.start_date),
first_log.change_time.date()) first_log.change_time.date())
self.statistic[first_log.change_time.date()] += get_timedelta(first_log).total_seconds() self.statistic[first_log.change_time.date()] += get_timedelta(first_log).total_seconds()

View File

@ -19,7 +19,7 @@ from django.contrib.messages.views import SuccessMessageMixin
from django.core.handlers.wsgi import WSGIRequest from django.core.handlers.wsgi import WSGIRequest
from django.http import HttpResponseRedirect, HttpResponse from django.http import HttpResponseRedirect, HttpResponse
from django.shortcuts import render, redirect from django.shortcuts import render, redirect
from django.urls import reverse_lazy, reverse from django.urls import reverse_lazy
from django.views.generic import FormView from django.views.generic import FormView
from django_registration.views import RegistrationView from django_registration.views import RegistrationView
# Django REST # Django REST
@ -29,12 +29,12 @@ from rest_framework.response import Response
from access_controller.settings import DEFAULT_FROM_EMAIL, ZENDESK_ROLES, ZENDESK_MAX_AGENTS, ZENDESK_GROUPS 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.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, \ 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 set_session_params_for_work_page, get_tickets_list_for_group
from .statistic_data import StatisticData
from main.zendesk_admin import zenpy from main.zendesk_admin import zenpy
from main.requester import TicketListRequester
from main.forms import AdminPageUsers, CustomRegistrationForm, CustomAuthenticationForm, StatisticForm from main.forms import AdminPageUsers, CustomRegistrationForm, CustomAuthenticationForm, StatisticForm
from main.serializers import ProfileSerializer, ZendeskUserSerializer from main.serializers import ProfileSerializer, ZendeskUserSerializer
from .statistic_data import StatisticData
from .models import UserProfile from .models import UserProfile
@ -69,7 +69,8 @@ class CustomRegistrationView(RegistrationView):
:type template_name: :class:`str` :type template_name: :class:`str`
:param success_url: Указание пути к html-странице завершения регистрации :param success_url: Указание пути к html-странице завершения регистрации
:type success_url: :class:`django.utils.functional.lazy.<locals>.__proxy__` :type success_url: :class:`django.utils.functional.lazy.<locals>.__proxy__`
:param is_allowed: Определение зарегистрирован ли пользователь с введенным email на Zendesk и принадлежит ли он к организации SYSTEM :param is_allowed: Определение зарегистрирован ли пользователь с введенным email на Zendesk и
принадлежит ли он к организации SYSTEM
:type is_allowed: :class:`bool` :type is_allowed: :class:`bool`
""" """
extra_context = setup_context(registration_lit=True) extra_context = setup_context(registration_lit=True)
@ -91,7 +92,7 @@ class CustomRegistrationView(RegistrationView):
3. Создается пользователь class User, а также его профиль. 3. Создается пользователь class User, а также его профиль.
:param form: Email пользователя на Zendesk :param form: Email пользователя на Zendesk
:return: user :return: User
""" """
self.redirect_url = 'done' self.redirect_url = 'done'
if check_user_exist(form.data['email']) and get_user_organization(form.data['email']) == 'SYSTEM': if check_user_exist(form.data['email']) and get_user_organization(form.data['email']) == 'SYSTEM':
@ -235,12 +236,12 @@ def work_hand_over(request: WSGIRequest) -> HttpResponseRedirect:
@login_required() @login_required()
def work_become_engineer(request: WSGIRequest) -> HttpResponseRedirect: def work_become_engineer(request: WSGIRequest) -> HttpResponseRedirect:
""" """
Функция позволяет текущему пользователю получить права, а именно сменить в Zendesk роль с "light_agent" на "engineer" Функция позволяет текущему пользователю получить права, а именно сменить в Zendesk роль с "light_agent"
на "engineer".
:param request: данные текущего пользователя (login_required) :param request: данные текущего пользователя (login_required)
:return: перезагрузка текущей страницы после выполнения смены роли :return: перезагрузка текущей страницы после выполнения смены роли
""" """
make_engineer(request.user.userprofile, request.user) make_engineer(request.user.userprofile, request.user)
return set_session_params_for_work_page(request) return set_session_params_for_work_page(request)
@ -311,7 +312,7 @@ class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageM
self.make_light_agents(users) self.make_light_agents(users)
return super().form_valid(form) return super().form_valid(form)
def make_engineers(self, users): def make_engineers(self, users: list) -> None:
""" """
Функция проходит по списку пользователей, проставляя статус "engineer". Функция проходит по списку пользователей, проставляя статус "engineer".