Merge branch 'hotfix/ticket_unassignment' into 'develop'

Hotfix/ticket unassignment

See merge request 2020-2021/online/s101/group-02/access_controller!35
This commit is contained in:
Кравченко Артем 2021-03-18 17:21:04 +00:00
commit 1bb8e44d03
7 changed files with 162 additions and 59 deletions

View File

@ -184,6 +184,13 @@ ZENDESK_ROLES = {
'light_agent': 360005208980, 'light_agent': 360005208980,
} }
ZENDESK_GROUPS = {
'employees': 'Поддержка',
'buffer': 'Сменная группа',
}
SOLVED_TICKETS_EMAIL = 'd.krikov@ngenix.net'
REST_FRAMEWORK = { REST_FRAMEWORK = {
# Use Django's standard `django.contrib.auth` permissions, # Use Django's standard `django.contrib.auth` permissions,
# or allow read-only access for unauthenticated users. # or allow read-only access for unauthenticated users.
@ -193,3 +200,4 @@ REST_FRAMEWORK = {
} }
ONE_DAY = 12 # Количество часов в 1 рабочем дне ONE_DAY = 12 # Количество часов в 1 рабочем дне

View File

@ -2,14 +2,13 @@ import os
from datetime import timedelta, datetime, date from datetime import timedelta, datetime, date
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist
from django.utils import timezone
from zenpy import Zenpy from zenpy import Zenpy
from zenpy.lib.exception import APIException from zenpy.lib.exception import APIException
from main.models import UserProfile, RoleChangeLogs from access_controller.settings import ZENDESK_ROLES as ROLES, ONE_DAY, ZENDESK_GROUPS, SOLVED_TICKETS_EMAIL
from django.core.exceptions import ObjectDoesNotExist from main.models import UserProfile, RoleChangeLogs, UnassignedTicket, UnassignedTicketStatus
from access_controller.settings import ZENDESK_ROLES as ROLES, ONE_DAY
class ZendeskAdmin: class ZendeskAdmin:
@ -32,12 +31,6 @@ class ZendeskAdmin:
email: str = os.getenv('ACCESS_CONTROLLER_API_EMAIL') email: str = os.getenv('ACCESS_CONTROLLER_API_EMAIL')
token: str = os.getenv('ACCESS_CONTROLLER_API_TOKEN') token: str = os.getenv('ACCESS_CONTROLLER_API_TOKEN')
password: str = os.getenv('ACCESS_CONTROLLER_API_PASSWORD') password: str = os.getenv('ACCESS_CONTROLLER_API_PASSWORD')
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self): def __init__(self):
self.create_admin() self.create_admin()
@ -85,6 +78,12 @@ class ZendeskAdmin:
""" """
return self.admin.users.search(email).values[0] return self.admin.users.search(email).values[0]
def get_group(self, name):
groups = self.admin.search(name)
for group in groups:
return group
return None
def get_user_org(self, email: str) -> str: def get_user_org(self, email: str) -> str:
""" """
Функция **get_user_org** возвращает организацию, к которой относится пользователь по его email Функция **get_user_org** возвращает организацию, к которой относится пользователь по его email
@ -129,23 +128,49 @@ def update_role(user_profile: UserProfile, role: str) -> UserProfile:
zendesk.admin.users.update(user) zendesk.admin.users.update(user)
def make_engineer(user_profile: UserProfile) -> UserProfile: def make_engineer(user_profile: UserProfile, who_changes: User) -> UserProfile:
""" """
Функция **make_engineer** устанавливапет пользователю роль инженера. Функция **make_engineer** устанавливает пользователю роль инженера.
""" """
RoleChangeLogs.objects.create(
user=user_profile.user,
old_role=user_profile.custom_role_id,
new_role=ROLES['engineer'],
changed_by=who_changes
)
update_role(user_profile, ROLES['engineer']) update_role(user_profile, ROLES['engineer'])
def make_light_agent(user_profile: UserProfile) -> UserProfile: def make_light_agent(user_profile: UserProfile, who_changes: User) -> UserProfile:
""" """
Функция **make_light_agent** устанавливапет пользователю роль легкого агента. Функция **make_light_agent** устанавливапет пользователю роль легкого агента.
""" """
tickets = get_tickets_list(user_profile.user.email)
for ticket in tickets:
UnassignedTicket.objects.create(
assignee=user_profile.user,
ticket_id=ticket.id,
status=UnassignedTicketStatus.SOLVED if ticket.status == 'solved' else UnassignedTicketStatus.UNASSIGNED
)
if ticket.status == 'solved':
ticket.assignee = ZendeskAdmin().get_user(SOLVED_TICKETS_EMAIL)
else:
ticket.assignee = None
ticket.group = ZendeskAdmin().get_group(ZENDESK_GROUPS['buffer'])
ZendeskAdmin().admin.tickets.update(ticket)
RoleChangeLogs.objects.create(
user=user_profile.user,
old_role=user_profile.custom_role_id,
new_role=ROLES['light_agent'],
changed_by=who_changes
)
update_role(user_profile, ROLES['light_agent']) update_role(user_profile, ROLES['light_agent'])
def get_users_list() -> list: def get_users_list() -> list:
""" """
Функция **get_users_list** возвращает список пользователей Zendesk, относящихся к организации. Функция **get_users_list** возвращает список пользователей Zendesk, относящихся к организации.
""" """
zendesk = ZendeskAdmin() zendesk = ZendeskAdmin()
@ -155,9 +180,16 @@ def get_users_list() -> list:
return users return users
def get_tickets_list(email):
"""
Функция возвращает список тикетов пользователя Zendesk
"""
return ZendeskAdmin().admin.search(assignee=email, type='ticket')
def update_profile(user_profile: UserProfile) -> UserProfile: def update_profile(user_profile: UserProfile) -> UserProfile:
""" """
Функция обновляет профиль пользователя в соотвтетствии с текущим в Zendesk Функция обновляет профиль пользователя в соотвтетствии с текущим в Zendesk
""" """
user = ZendeskAdmin().get_user(user_profile.user.email) user = ZendeskAdmin().get_user(user_profile.user.email)
user_profile.name = user.name user_profile.name = user.name
@ -169,7 +201,7 @@ def update_profile(user_profile: UserProfile) -> UserProfile:
def check_user_exist(email: str) -> bool: def check_user_exist(email: str) -> bool:
""" """
Функция проверяет, существует ли пользователь Функция проверяет, существует ли пользователь
""" """
return ZendeskAdmin().check_user(email) return ZendeskAdmin().check_user(email)
@ -278,9 +310,9 @@ class StatisticData:
self.warnings = list() self.warnings = list()
self.data = dict() self.data = dict()
self.statistic = dict() self.statistic = dict()
self._set_data() self._init_data()
if stat is None: if stat is None:
self._set_statistic() self._init_statistic()
else: else:
self.statistic = stat self.statistic = stat
@ -383,7 +415,7 @@ class StatisticData:
return False return False
return True return True
def _set_data(self): def _init_data(self):
""" """
Получение логов в диапазоне дат start_date-end_date для пользователя с почтой email Получение логов в диапазоне дат start_date-end_date для пользователя с почтой email
""" """
@ -398,7 +430,7 @@ class StatisticData:
except User.DoesNotExist: except User.DoesNotExist:
self.errors += ['Пользователь не найден'] self.errors += ['Пользователь не найден']
def _set_statistic(self): def _init_statistic(self):
""" """
Функция заполняет словарь, в котором ключ - дата, значение - кол-во проработанных в этот день секунд Функция заполняет словарь, в котором ключ - дата, значение - кол-во проработанных в этот день секунд
""" """
@ -409,14 +441,19 @@ class StatisticData:
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.fill_daterange(self.start_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()
if last_log.new_role == ROLES['engineer']: if last_log.new_role == ROLES['engineer']:
self.fill_daterange(last_log.change_time.date() + timedelta(days=1), self.end_date + timedelta(days=1)) self.fill_daterange(last_log.change_time.date() + timedelta(days=1), self.end_date + timedelta(days=1))
self.statistic[last_log.change_time.date()] += (timedelta(days=1) - get_timedelta(last_log)).total_seconds() if last_log.change_time.date() == timezone.now().date():
if self.end_date == datetime.now().date(): self.statistic[last_log.change_time.date()] += (
self.statistic[self.end_date] = get_timedelta(None, datetime.now().time()).total_seconds() get_timedelta(None, timezone.now().time()) - get_timedelta(last_log)
).total_seconds()
else:
self.statistic[last_log.change_time.date()] += (
timedelta(days=1) - get_timedelta(last_log)).total_seconds()
if self.end_date == timezone.now().date():
self.statistic[self.end_date] = get_timedelta(None, timezone.now().time()).total_seconds()
for log_index in range(len(self.data) - 1): for log_index in range(len(self.data) - 1):
if self.data[log_index].new_role == ROLES['engineer']: if self.data[log_index].new_role == ROLES['engineer']:

View File

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

View File

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

View File

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

View File

@ -2,6 +2,7 @@ from django.db import models
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db.models.signals import post_save from django.db.models.signals import post_save
from django.dispatch import receiver from django.dispatch import receiver
from django.utils import timezone
class UserProfile(models.Model): class UserProfile(models.Model):
@ -34,9 +35,22 @@ class RoleChangeLogs(models.Model):
"""Модель для логирования изменений ролей пользователя""" """Модель для логирования изменений ролей пользователя"""
user = models.ForeignKey(to=User, on_delete=models.CASCADE, user = models.ForeignKey(to=User, on_delete=models.CASCADE,
help_text='Пользователь, которому присвоили другую роль') help_text='Пользователь, которому присвоили другую роль')
name = models.TextField(help_text='Имя пользователя')
old_role = models.IntegerField(default=0, help_text='Старая роль') old_role = models.IntegerField(default=0, help_text='Старая роль')
new_role = models.IntegerField(default=0, help_text='Присвоенная роль') new_role = models.IntegerField(default=0, help_text='Присвоенная роль')
change_time = models.DateTimeField(help_text='Дата и время изменения роли') change_time = models.DateTimeField(help_text='Дата и время изменения роли', default=timezone.now)
changed_by = models.ForeignKey(to=User, on_delete=models.CASCADE, related_name='changed_by', changed_by = models.ForeignKey(to=User, on_delete=models.CASCADE, related_name='changed_by',
help_text='Кем была изменена роль') help_text='Кем была изменена роль')
class UnassignedTicketStatus(models.IntegerChoices):
UNASSIGNED = 0, 'Снят с пользователя, перенесён в буферную группу'
RESTORED = 1, 'Авторство восстановлено'
NOT_FOUND = 2, 'Пока нас не было, тикет испарился из буферной группы. Дополнительные действия не требуются'
CLOSED = 3, 'Тикет уже был закрыт. Дополнительные действия не требуются'
SOLVED = 4, 'Тикет решён. Записан на пользователя с почтой SOLVED_TICKETS_EMAIL'
class UnassignedTicket(models.Model):
assignee = models.ForeignKey(to=User, on_delete=models.CASCADE, related_name='tickets')
ticket_id = models.IntegerField(help_text='Номер тикера, для которого сняли ответственного')
status = models.IntegerField(choices=UnassignedTicketStatus.choices, default=UnassignedTicketStatus.UNASSIGNED)

View File

@ -1,36 +1,32 @@
import logging import logging
import os
from datetime import datetime
from django.contrib.auth.decorators import login_required
from django.contrib.auth.forms import PasswordResetForm 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.models import User, Permission
from django.contrib.auth.tokens import default_token_generator from django.contrib.auth.tokens import default_token_generator
from django.contrib.auth.views import LoginView from django.contrib.auth.views import LoginView
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import PermissionDenied
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, get_list_or_404, redirect from django.shortcuts import render, get_list_or_404, redirect
from django.urls import reverse_lazy, reverse from django.urls import reverse_lazy, reverse
from django.views.generic import FormView from django.views.generic import FormView
from django_registration.views import RegistrationView from django_registration.views import RegistrationView
from django.contrib.auth.decorators import login_required # Django REST
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin from rest_framework import viewsets
from rest_framework.response import Response
from zenpy import Zenpy
from zenpy.lib.api_objects import User as ZenpyUser from zenpy.lib.api_objects import User as ZenpyUser
from access_controller.settings import EMAIL_HOST_USER, ZENDESK_ROLES
from main.extra_func import ZendeskAdmin
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, \
StatisticData StatisticData
from main.models import UserProfile
from main.forms import AdminPageUsers, CustomRegistrationForm, CustomAuthenticationForm, StatisticForm from main.forms import AdminPageUsers, CustomRegistrationForm, CustomAuthenticationForm, StatisticForm
from access_controller.settings import EMAIL_HOST_USER, ZENDESK_ROLES
# Django REST
from rest_framework import viewsets, status
from main.serializers import ProfileSerializer from main.serializers import ProfileSerializer
from rest_framework.response import Response from .models import UserProfile
class CustomRegistrationView(RegistrationView): class CustomRegistrationView(RegistrationView):
@ -114,12 +110,7 @@ def profile_page(request: WSGIRequest) -> HttpResponse:
def auth_user(request): def auth_user(request):
admin_creds = { admin = ZendeskAdmin().admin
'email': os.environ.get('ACCESS_CONTROLLER_API_EMAIL'),
'subdomain': 'ngenix1612197338',
'token': os.environ.get('ACCESS_CONTROLLER_API_TOKEN'),
}
admin = Zenpy(**admin_creds)
zenpy_user: ZenpyUser = admin.users.search(request.user.email).values[0] zenpy_user: ZenpyUser = admin.users.search(request.user.email).values[0]
return zenpy_user, admin return zenpy_user, admin
@ -171,7 +162,7 @@ def work_become_engineer(request):
def main_page(request): def main_page(request):
""" """
Отображение логгирования на главной странице Отображение логгирования на главной странице
""" """
logger = logging.getLogger('main.index') logger = logging.getLogger('main.index')
logger.info('Index page opened') logger.info('Index page opened')
@ -186,7 +177,7 @@ class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin, FormView):
def form_valid(self, form: AdminPageUsers) -> AdminPageUsers: def form_valid(self, form: AdminPageUsers) -> AdminPageUsers:
""" """
Функция установки ролей пользователям Функция установки ролей пользователям
""" """
users = form.cleaned_data['users'] users = form.cleaned_data['users']
if 'engineer' in self.request.POST: if 'engineer' in self.request.POST:
@ -195,13 +186,13 @@ class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin, FormView):
self.make_light_agents(users) self.make_light_agents(users)
return super().form_valid(form) return super().form_valid(form)
@staticmethod def make_engineers(self, users):
def make_engineers(users): for user in users:
[make_engineer(user) for user in users] make_engineer(user, self.request.user)
@staticmethod def make_light_agents(self, users):
def make_light_agents(users): for user in users:
[make_light_agent(user) for user in users] make_light_agent(user, self.request.user)
def get_context_data(self, **kwargs) -> dict: def get_context_data(self, **kwargs) -> dict:
""" """
@ -244,8 +235,8 @@ class UsersViewSet(viewsets.ReadOnlyModelViewSet):
@login_required() @login_required()
def statistic_page(request): def statistic_page(request):
if not request.user.is_superuser: if not request.user.has_perm('main.has_control_access'):
return redirect('index') raise PermissionDenied
context = { context = {
'pagename': 'страница статистики', 'pagename': 'страница статистики',
'errors': list(), 'errors': list(),