Merge branch 'develop' into feature/documentation
# Conflicts: # README.md # main/extra_func.py # main/forms.py # main/views.py
This commit is contained in:
@@ -3,6 +3,8 @@ 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:
|
||||
"""
|
||||
@@ -15,15 +17,15 @@ def api_auth() -> dict:
|
||||
:return: данные пользователя
|
||||
"""
|
||||
credentials = {
|
||||
'subdomain': 'ngenix1612197338'
|
||||
'subdomain': ACTRL_ZENDESK_SUBDOMAIN
|
||||
}
|
||||
email = os.getenv('ACCESS_CONTROLLER_API_EMAIL')
|
||||
token = os.getenv('ACCESS_CONTROLLER_API_TOKEN')
|
||||
password = os.getenv('ACCESS_CONTROLLER_API_PASSWORD')
|
||||
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'] = os.getenv('ACCESS_CONTROLLER_API_EMAIL')
|
||||
credentials['email'] = email
|
||||
|
||||
# prefer token, use password if token not provided
|
||||
if token:
|
||||
|
||||
@@ -1,150 +1,21 @@
|
||||
import os
|
||||
import logging
|
||||
from datetime import timedelta, datetime, date
|
||||
from typing import Optional
|
||||
|
||||
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 access_controller.settings import ZENDESK_ROLES as ROLES, ONE_DAY, ZENDESK_GROUPS, SOLVED_TICKETS_EMAIL
|
||||
from access_controller.settings import ZENDESK_ROLES as ROLES, ONE_DAY, ACTRL_ZENDESK_SUBDOMAIN
|
||||
from main.models import UserProfile, RoleChangeLogs, UnassignedTicket, UnassignedTicketStatus
|
||||
from main.zendesk_admin import zenpy
|
||||
|
||||
|
||||
class ZendeskAdmin:
|
||||
"""
|
||||
Класс **ZendeskAdmin** существует, чтобы в каждой функции отдельно не проверять аккаунт администратора.
|
||||
|
||||
:param credentials: Полномочия (первым указывается учетная запись организации в Zendesk)
|
||||
:type credentials: :class:`dict`
|
||||
:param email: Email администратора, указанный в env
|
||||
:type email: :class:`str`
|
||||
:param token: Токен администратора (формируется в Zendesk, указывается в env)
|
||||
:type token: :class:`str`
|
||||
:param password: Пароль администратора, указанный в env
|
||||
:type password: :class:`str`
|
||||
|
||||
"""
|
||||
|
||||
credentials: dict = {
|
||||
'subdomain': 'ngenix1612197338'
|
||||
}
|
||||
email: str = os.getenv('ACCESS_CONTROLLER_API_EMAIL')
|
||||
token: str = os.getenv('ACCESS_CONTROLLER_API_TOKEN')
|
||||
password: str = os.getenv('ACCESS_CONTROLLER_API_PASSWORD')
|
||||
|
||||
def __init__(self):
|
||||
self.create_admin()
|
||||
|
||||
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_name(self, email: str) -> str:
|
||||
"""
|
||||
Функция **get_user_name** возвращает имя пользователя по его email.
|
||||
|
||||
:param email: Email пользователя
|
||||
:return: Имя пользователя
|
||||
"""
|
||||
user = self.admin.users.search(email).values[0]
|
||||
return user.name
|
||||
|
||||
def get_user_role(self, email: str) -> str:
|
||||
"""
|
||||
Функция возвращает роль пользователя по его email.
|
||||
|
||||
:param email: Email пользователя
|
||||
:return: Роль пользователя
|
||||
"""
|
||||
user = self.admin.users.search(email).values[0]
|
||||
return user.role
|
||||
|
||||
def get_user_id(self, email: str) -> str:
|
||||
"""
|
||||
Функция возвращает id пользователя по его email
|
||||
|
||||
:param email: Email пользователя
|
||||
:return: ID пользователя
|
||||
"""
|
||||
user = self.admin.users.search(email).values[0]
|
||||
return user.id
|
||||
|
||||
def get_user_image(self, email: str) -> Optional[str]:
|
||||
"""
|
||||
Функция возвращает url-ссылку на аватар пользователя по его email.
|
||||
|
||||
:param email: Email пользователя
|
||||
:return: Аватар пользователя
|
||||
"""
|
||||
user = self.admin.users.search(email).values[0]
|
||||
return user.photo['content_url'] if user.photo else None
|
||||
|
||||
def get_user(self, email: str):
|
||||
"""
|
||||
Функция возвращает пользователя (объект) по его email.
|
||||
|
||||
:param email: Email пользователя
|
||||
:return: Объект пользователя, найденного в БД
|
||||
"""
|
||||
return self.admin.users.search(email).values[0]
|
||||
|
||||
def get_group(self, name: str) -> Optional[str]:
|
||||
"""
|
||||
Функция возвращает группу, к которой принадлежит пользователь.
|
||||
|
||||
:param name: Имя пользователя
|
||||
:return: Группы пользователя (в случае отсутствия None)
|
||||
"""
|
||||
groups = self.admin.search(name)
|
||||
for group in groups:
|
||||
return group
|
||||
return None
|
||||
|
||||
def get_user_org(self, email: str) -> Optional[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.
|
||||
|
||||
:param credentials: В список полномочий администратора вносятся email, token, password из env
|
||||
:type credentials: :class:`dict`
|
||||
:raise: :class:`ValueError`: исключение, вызываемое если email не введен в env
|
||||
:raise: :class:`APIException`: исключение, вызываемое если пользователя с таким email не существует в Zendesk
|
||||
"""
|
||||
|
||||
if self.email is None:
|
||||
raise ValueError('access_controller email not in env')
|
||||
self.credentials['email'] = self.email
|
||||
|
||||
if self.token:
|
||||
self.credentials['token'] = self.token
|
||||
elif self.password:
|
||||
self.credentials['password'] = self.password
|
||||
else:
|
||||
raise ValueError('access_controller token or password not in env')
|
||||
self.admin = Zenpy(**self.credentials)
|
||||
try:
|
||||
self.admin.search(self.email, type='user')
|
||||
except APIException:
|
||||
raise ValueError('invalid access_controller`s login data')
|
||||
|
||||
|
||||
def update_role(user_profile: UserProfile, role: str) -> UserProfile:
|
||||
def update_role(user_profile: UserProfile, role: int) -> None:
|
||||
"""
|
||||
Функция меняет роль пользователя.
|
||||
|
||||
@@ -152,7 +23,7 @@ def update_role(user_profile: UserProfile, role: str) -> UserProfile:
|
||||
:param role: Новая роль
|
||||
:return: Пользователь с обновленной ролью
|
||||
"""
|
||||
zendesk = ZendeskAdmin()
|
||||
zendesk = zenpy
|
||||
user = zendesk.get_user(user_profile.user.email)
|
||||
user.custom_role_id = role
|
||||
user_profile.custom_role_id = role
|
||||
@@ -160,23 +31,17 @@ def update_role(user_profile: UserProfile, role: str) -> UserProfile:
|
||||
zendesk.admin.users.update(user)
|
||||
|
||||
|
||||
def make_engineer(user_profile: UserProfile, who_changes: User) -> UserProfile:
|
||||
def make_engineer(user_profile: UserProfile, who_changes: User) -> None:
|
||||
"""
|
||||
Функция устанавливает пользователю роль инженера.
|
||||
|
||||
:param user_profile: Профиль пользователя
|
||||
:return: Вызов функции **update_role** с параметрами: профиль пользователя, роль "engineer"
|
||||
"""
|
||||
RoleChangeLogs.objects.create(
|
||||
user=user_profile.user,
|
||||
old_role=user_profile.custom_role_id,
|
||||
new_role=ROLES['engineer'],
|
||||
changed_by=who_changes
|
||||
)
|
||||
update_role(user_profile, ROLES['engineer'])
|
||||
|
||||
|
||||
def make_light_agent(user_profile: UserProfile, who_changes: User) -> UserProfile:
|
||||
def make_light_agent(user_profile: UserProfile, who_changes: User) -> None:
|
||||
"""
|
||||
Функция устанавливает пользователю роль легкого агента.
|
||||
|
||||
@@ -184,6 +49,7 @@ def make_light_agent(user_profile: UserProfile, who_changes: User) -> UserProfil
|
||||
:return: Вызов функции **update_role** с параметрами: профиль пользователя, роль "light_agent"
|
||||
"""
|
||||
tickets = get_tickets_list(user_profile.user.email)
|
||||
ticket: ZenpyTicket
|
||||
for ticket in tickets:
|
||||
UnassignedTicket.objects.create(
|
||||
assignee=user_profile.user,
|
||||
@@ -191,26 +57,29 @@ def make_light_agent(user_profile: UserProfile, who_changes: User) -> UserProfil
|
||||
status=UnassignedTicketStatus.SOLVED if ticket.status == 'solved' else UnassignedTicketStatus.UNASSIGNED
|
||||
)
|
||||
if ticket.status == 'solved':
|
||||
ticket.assignee = ZendeskAdmin().get_user(SOLVED_TICKETS_EMAIL)
|
||||
ticket.assignee_id = zenpy.solved_tickets_user_id
|
||||
else:
|
||||
ticket.assignee = None
|
||||
ticket.group = ZendeskAdmin().get_group(ZENDESK_GROUPS['buffer'])
|
||||
ZendeskAdmin().admin.tickets.update(ticket)
|
||||
ticket.group_id = zenpy.buffer_group_id
|
||||
|
||||
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'])
|
||||
zenpy.admin.tickets.update(tickets.values)
|
||||
|
||||
attempts, success = 5, False
|
||||
while not success and attempts != 0:
|
||||
try:
|
||||
update_role(user_profile, ROLES['light_agent'])
|
||||
success = True
|
||||
except APIException as e:
|
||||
attempts -= 1
|
||||
if attempts == 0:
|
||||
raise e
|
||||
|
||||
|
||||
def get_users_list() -> list:
|
||||
"""
|
||||
Функция **get_users_list** возвращает список пользователей Zendesk, относящихся к организации SYSTEM.
|
||||
Функция **get_users_list** возвращает список пользователей Zendesk, относящихся к организации SYSTEM.
|
||||
"""
|
||||
zendesk = ZendeskAdmin()
|
||||
zendesk = zenpy
|
||||
|
||||
# У пользователей должна быть организация SYSTEM
|
||||
org = next(zendesk.admin.search(type='organization', name='SYSTEM'))
|
||||
@@ -218,24 +87,21 @@ def get_users_list() -> list:
|
||||
return users
|
||||
|
||||
|
||||
def get_tickets_list(email: str) -> list:
|
||||
def get_tickets_list(email):
|
||||
"""
|
||||
Функция возвращает список тикетов пользователя Zendesk.
|
||||
|
||||
:param email: Email пользователя
|
||||
:return: Список тикетов пользователя
|
||||
Функция возвращает список тикетов пользователя Zendesk
|
||||
"""
|
||||
return ZendeskAdmin().admin.search(assignee=email, type='ticket')
|
||||
return zenpy.admin.search(assignee=email, type='ticket')
|
||||
|
||||
|
||||
def update_profile(user_profile: UserProfile) -> UserProfile:
|
||||
def update_profile(user_profile: UserProfile):
|
||||
"""
|
||||
Функция обновляет профиль пользователя в соответствии с текущим в Zendesk.
|
||||
|
||||
:param user_profile: Профиль пользователя
|
||||
:return: Обновленный, в соответствие с текущими данными в Zendesk, профиль пользователя
|
||||
"""
|
||||
user = ZendeskAdmin().get_user(user_profile.user.email)
|
||||
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
|
||||
@@ -250,7 +116,7 @@ def check_user_exist(email: str) -> bool:
|
||||
:param email: Email пользователя
|
||||
:return: Зарегистрирован ли пользователь в Zendesk
|
||||
"""
|
||||
return ZendeskAdmin().check_user(email)
|
||||
return zenpy.check_user(email)
|
||||
|
||||
|
||||
def get_user_organization(email: str) -> str:
|
||||
@@ -260,21 +126,19 @@ def get_user_organization(email: str) -> str:
|
||||
:param email: Email пользователя
|
||||
:return: Организация пользователя
|
||||
"""
|
||||
return ZendeskAdmin().get_user_org(email)
|
||||
return zenpy.get_user_org(email)
|
||||
|
||||
|
||||
def check_user_auth(email: str, password: str) -> bool:
|
||||
"""
|
||||
Функция проверяет, верны ли входные данные.
|
||||
|
||||
:param email: Email пользователя
|
||||
:param password: Пароль пользователя
|
||||
:raise: :class:`APIException`: исключение, вызываемое если пользователь не аутентифицирован
|
||||
"""
|
||||
creds = {
|
||||
'email': email,
|
||||
'password': password,
|
||||
'subdomain': 'ngenix1612197338',
|
||||
'subdomain': ACTRL_ZENDESK_SUBDOMAIN,
|
||||
}
|
||||
try:
|
||||
user = Zenpy(**creds)
|
||||
@@ -284,7 +148,7 @@ def check_user_auth(email: str, password: str) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def update_user_in_model(profile: UserProfile, zendesk_user: User) -> UserProfile:
|
||||
def update_user_in_model(profile: UserProfile, zendesk_user: ZenpyUser):
|
||||
"""
|
||||
Функция обновляет профиль пользователя при изменении данных пользователя на Zendesk.
|
||||
|
||||
@@ -302,7 +166,7 @@ def update_user_in_model(profile: UserProfile, zendesk_user: User) -> UserProfil
|
||||
|
||||
def count_users(users) -> tuple:
|
||||
"""
|
||||
Функция подсчета количества сотрудников с ролями engineer и light_agent
|
||||
Функция подсчета количества сотрудников с ролями engineer и light_agent
|
||||
"""
|
||||
engineers, light_agents = 0, 0
|
||||
for user in users:
|
||||
@@ -313,9 +177,9 @@ def count_users(users) -> tuple:
|
||||
return engineers, light_agents
|
||||
|
||||
|
||||
def update_users_in_model() -> list:
|
||||
def update_users_in_model():
|
||||
"""
|
||||
Функция обновляет пользователей в модели UserProfile по списку пользователей в организации
|
||||
Обновляет пользователей в модели UserProfile по списку пользователей в организации
|
||||
"""
|
||||
users = get_users_list()
|
||||
for user in users:
|
||||
@@ -327,7 +191,7 @@ def update_users_in_model() -> list:
|
||||
return users
|
||||
|
||||
|
||||
def daterange(start_date: date, end_date: date) -> list:
|
||||
def daterange(start_date, end_date) -> list:
|
||||
"""
|
||||
Функция возвращает список дней с start_date по end_date, исключая правую границу.
|
||||
|
||||
@@ -341,7 +205,7 @@ def daterange(start_date: date, end_date: date) -> list:
|
||||
return dates
|
||||
|
||||
|
||||
def get_timedelta(log: RoleChangeLogs, time: datetime =None) -> timedelta:
|
||||
def get_timedelta(log, time=None) -> timedelta:
|
||||
"""
|
||||
Функция возвращает объект класса timedelta, который хранит промежуток времени от начала суток до момента,
|
||||
который находится в log (объект класса RoleChangeLogs) или в time(datetime.time), если введён.
|
||||
@@ -408,7 +272,7 @@ class StatisticData:
|
||||
else:
|
||||
self.statistic = stat
|
||||
|
||||
def get_statistic(self) -> Optional[dict]:
|
||||
def get_statistic(self) -> dict:
|
||||
"""
|
||||
Функция возвращает статистику работы пользователя.
|
||||
|
||||
@@ -456,7 +320,7 @@ class StatisticData:
|
||||
self.display = display_format
|
||||
return True
|
||||
|
||||
def get_data(self) -> Optional[list]:
|
||||
def get_data(self) -> Optional[dict]:
|
||||
"""
|
||||
Функция возвращает данные - список объектов RoleChangeLogs.
|
||||
"""
|
||||
@@ -524,7 +388,7 @@ class StatisticData:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _init_data(self) -> Optional[list]:
|
||||
def _init_data(self):
|
||||
"""
|
||||
Функция возвращает логи в диапазоне дат start_date - end_date для пользователя с указанным email.
|
||||
|
||||
@@ -541,7 +405,7 @@ class StatisticData:
|
||||
except User.DoesNotExist:
|
||||
self.errors += ['Пользователь не найден']
|
||||
|
||||
def _init_statistic(self) -> Optional[dict]:
|
||||
def _init_statistic(self) -> dict:
|
||||
"""
|
||||
Функция заполняет словарь, в котором ключ - дата, значение - кол-во проработанных в этот день секунд.
|
||||
|
||||
@@ -563,7 +427,7 @@ class StatisticData:
|
||||
if self.data[log_index].new_role == ROLES['engineer']:
|
||||
self.engineer_logic(log_index)
|
||||
|
||||
def engineer_logic(self, log_index: int) -> dict:
|
||||
def engineer_logic(self, log_index):
|
||||
"""
|
||||
Функция обрабатывает основную часть работы инженера
|
||||
"""
|
||||
@@ -577,9 +441,9 @@ class StatisticData:
|
||||
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: RoleChangeLogs):
|
||||
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():
|
||||
@@ -592,9 +456,9 @@ class StatisticData:
|
||||
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: RoleChangeLogs) ->dict:
|
||||
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())
|
||||
@@ -617,3 +481,82 @@ class StatisticData:
|
||||
"""
|
||||
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)
|
||||
|
||||
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=0):
|
||||
"""
|
||||
Осуществляет запись логов в базу данных и 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)
|
||||
|
||||
@@ -12,10 +12,9 @@ class CustomRegistrationForm(RegistrationFormUniqueEmail):
|
||||
|
||||
:param visible_fields.email: Поле для ввода email, зарегистрированного на Zendesk
|
||||
:type visible_fields.email: :class:`django_registration.forms.RegistrationFormUniqueEmail`
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs) -> RegistrationFormUniqueEmail:
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
for visible in self.visible_fields():
|
||||
if visible.field.widget.attrs.get('class', False):
|
||||
@@ -96,13 +95,12 @@ class StatisticForm(forms.Form):
|
||||
:param range_end: Дата и время окончания работы
|
||||
:type range_end: :class:`django.forms.fields.DateField`
|
||||
"""
|
||||
email: str = forms.EmailField(
|
||||
label='Электроная почта',
|
||||
email = forms.EmailField(
|
||||
label='Электронная почта',
|
||||
widget=forms.EmailInput(
|
||||
attrs={
|
||||
'placeholder': 'example@ngenix.ru',
|
||||
'class': 'form-control',
|
||||
'style': 'background-color:#f2f2f2;'
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
26
main/migrations/0017_auto_20210408_1943.py
Normal file
26
main/migrations/0017_auto_20210408_1943.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# Generated by Django 3.1.7 on 2021-04-08 16:43
|
||||
|
||||
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', '0016_merge_20210330_0043'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='unassignedticket',
|
||||
name='assignee',
|
||||
field=models.ForeignKey(help_text='Пользователь, с которого снят тикет', on_delete=django.db.models.deletion.CASCADE, related_name='tickets', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='unassignedticket',
|
||||
name='status',
|
||||
field=models.IntegerField(choices=[(0, 'Снят с пользователя, перенесён в буферную группу'), (1, 'Авторство восстановлено'), (2, 'Пока нас не было, тикет испарился из буферной группы. Дополнительные действия не требуются'), (3, 'Тикет уже был закрыт. Дополнительные действия не требуются'), (4, 'Тикет решён. Записан на пользователя с почтой SOLVED_TICKETS_EMAIL')], default=0, help_text='Статус тикета'),
|
||||
),
|
||||
]
|
||||
@@ -80,3 +80,4 @@ class UnassignedTicket(models.Model):
|
||||
assignee = models.ForeignKey(to=User, on_delete=models.CASCADE, related_name='tickets', help_text='Пользователь, с которого снят тикет')
|
||||
ticket_id = models.IntegerField(help_text='Номер тикера, для которого сняли ответственного')
|
||||
status = models.IntegerField(choices=UnassignedTicketStatus.choices, default=UnassignedTicketStatus.UNASSIGNED, help_text='Статус тикета')
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from django.contrib.auth.models import User
|
||||
from rest_framework import serializers
|
||||
from main.models import UserProfile
|
||||
from access_controller.settings import ZENDESK_ROLES
|
||||
|
||||
|
||||
class UserSerializer(serializers.HyperlinkedModelSerializer):
|
||||
@@ -13,9 +14,25 @@ class UserSerializer(serializers.HyperlinkedModelSerializer):
|
||||
|
||||
|
||||
class ProfileSerializer(serializers.HyperlinkedModelSerializer):
|
||||
"""Сериализатор для модели профиля пользователя"""
|
||||
"""Класс serializer для модели профиля пользователя"""
|
||||
user = UserSerializer()
|
||||
|
||||
class Meta:
|
||||
model = UserProfile
|
||||
fields = ['user', 'id', 'name', 'zendesk_role']
|
||||
|
||||
|
||||
class ZendeskUserSerializer(serializers.Serializer):
|
||||
"""Класс serializer для объектов пользователей из zenpy"""
|
||||
name = serializers.CharField()
|
||||
zendesk_role = serializers.SerializerMethodField('get_zendesk_role')
|
||||
email = serializers.EmailField()
|
||||
|
||||
@staticmethod
|
||||
def get_zendesk_role(obj):
|
||||
if obj.custom_role_id == ZENDESK_ROLES['engineer']:
|
||||
return 'engineer'
|
||||
elif obj.custom_role_id == ZENDESK_ROLES['light_agent']:
|
||||
return 'light_agent'
|
||||
else:
|
||||
return "empty"
|
||||
|
||||
@@ -5,23 +5,54 @@
|
||||
|
||||
<nav class="navbar navbar-light" style="background-color: #113A60;">
|
||||
<a class="navbar-brand" href="{% url 'index' %}">
|
||||
<img src="{% static 'main/img/logo_real.png' %}" width="107" height="22" class="d-inline-block align-top" alt="" loading="lazy">
|
||||
<img src="{% static 'main/img/logo_real.png' %}" width="107" height="22" class="d-inline-block align-top" style="margin-left: 15px" alt="" loading="lazy">
|
||||
<t style="color:#FFFFFF">Access Controller</t>
|
||||
</a>
|
||||
{% if request.user.is_authenticated %}
|
||||
<div class="btn-group" role="group" aria-label="Basic example">
|
||||
<a class="btn btn-secondary" href="{% url 'profile' %}">Профиль</a>
|
||||
<div class="btn-group" role="group" aria-label="Basic example" style="margin-right: 9px">
|
||||
<a {% if profile_lit %}
|
||||
class="btn btn-primary"
|
||||
{% else %}
|
||||
class="btn btn-secondary"
|
||||
{% endif %}
|
||||
href="{% url 'profile' %}">Профиль</a>
|
||||
{% if perms.main.has_control_access %}
|
||||
<a class="btn btn-secondary" href="{% url 'control' %}">Управление</a>
|
||||
<a {% if control_lit %}
|
||||
class="btn btn-primary"
|
||||
{% else %}
|
||||
class="btn btn-secondary"
|
||||
{% endif %}
|
||||
href="{% url 'control' %}">Управление</a>
|
||||
<a {% if stats_lit %}
|
||||
class="btn btn-primary"
|
||||
{% else %}
|
||||
class="btn btn-secondary"
|
||||
{% endif %}
|
||||
href="{% url 'statistic' %}">Статистика</a>
|
||||
{% else %}
|
||||
<a class="btn btn-secondary" href="{% url 'work' request.user.id %}">Запрос прав</a>
|
||||
<a {% if work_lit %}
|
||||
class="btn btn-primary"
|
||||
{% else %}
|
||||
class="btn btn-secondary"
|
||||
{% endif %}
|
||||
href="{% url 'work' request.user.id %}">Запрос прав</a>
|
||||
{% endif %}
|
||||
<a class="btn btn-secondary" href="{% url 'logout' %}">Выйти</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="btn-group" role="group" aria-label="Basic example">
|
||||
<a class="btn btn-secondary" href="/accounts/login">Войти</a>
|
||||
<a class="btn btn-secondary" href="/accounts/register">Зарегистрироваться</a>
|
||||
<div class="btn-group" role="group" aria-label="Basic example" style="margin-right: 9px">
|
||||
<a {% if login_lit %}
|
||||
class="btn btn-primary"
|
||||
{% else %}
|
||||
class="btn btn-secondary"
|
||||
{% endif %}
|
||||
href="/accounts/login">Войти</a>
|
||||
<a {% if registration_lit %}
|
||||
class="btn btn-primary"
|
||||
{% else %}
|
||||
class="btn btn-secondary"
|
||||
{% endif %}
|
||||
href="/accounts/register">Зарегистрироваться</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</nav>
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
<div class="mt-5">
|
||||
{% for message in messages %}
|
||||
<div
|
||||
class="alert alert-{{ message.tags }} alert-dismissible fade show p-2"
|
||||
role="alert"
|
||||
style="display: flex; align-items: center; justify-content: space-between;"
|
||||
>
|
||||
{{ message }}
|
||||
<div>
|
||||
<button type="button" class="btn btn-light p-2" data-bs-dismiss="alert" aria-label="Close">X</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
15
main/templates/django_registration/registration_error.html
Normal file
15
main/templates/django_registration/registration_error.html
Normal file
@@ -0,0 +1,15 @@
|
||||
{% extends 'base/base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}
|
||||
Регистрация завершена
|
||||
{% endblock %}
|
||||
|
||||
{% block heading %}
|
||||
Регистрация
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<br>
|
||||
<h4> Произошла ошибка при отправке электронного сообщения.</h4>
|
||||
{% endblock %}
|
||||
@@ -7,7 +7,8 @@
|
||||
{% block heading %}Управление{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<link rel="stylesheet" href="{% static 'main/css/work.css' %}"/>
|
||||
<link rel="stylesheet" href="{% static 'main/css/work.css' %}" xmlns="http://www.w3.org/1999/html">
|
||||
<link rel="stylesheet" href="{% static 'modules/notifications/dist/notifications.css' %}">
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
@@ -15,15 +16,22 @@
|
||||
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js" crossorigin></script>
|
||||
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
{% endblock%}
|
||||
|
||||
<script src="{% static 'modules/notifications/dist/notifications.js' %}"></script>
|
||||
<script src="{% static 'main/js/control.js'%}" type="text/babel"></script>
|
||||
<script src="{% static 'main/js/notifications.js' %}"></script>
|
||||
{% endblock%}
|
||||
{% block content %}
|
||||
<div class="container-md">
|
||||
|
||||
<div class="new-section">
|
||||
<p class="row page-description">Свободных Мест: {{ licences_remaining }}</p>
|
||||
<p class="row page-description" id="licences_remaining">Свободных Мест:</p>
|
||||
</div>
|
||||
|
||||
{% for message in messages %}
|
||||
<script>create_notification('{{message}}','','{{message.tags}}',5000)</script>
|
||||
{% endfor %}
|
||||
|
||||
{% block form %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
@@ -34,16 +42,22 @@
|
||||
<h6 class="table-title">Список сотрудников</h6>
|
||||
|
||||
{% block table %}
|
||||
<table class="light-table">
|
||||
<table class="table table-dark light-table">
|
||||
|
||||
<thead>
|
||||
<th>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="form-check-input"
|
||||
id="head-checkbox"
|
||||
/>
|
||||
</th>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Role</th>
|
||||
<th>Checked</th>
|
||||
</thead>
|
||||
<tbody id="tbody">
|
||||
</tbody>
|
||||
<tbody id="tbody"></tbody>
|
||||
|
||||
</table>
|
||||
<p id="loading">Данные загружаются...</p>
|
||||
{% endblock %}
|
||||
@@ -93,9 +107,6 @@
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% include 'base/success_messages.html' %}
|
||||
</div>
|
||||
|
||||
<script src="{% static 'main/js/control.js'%}" type="text/babel"></script>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<div class="col-auto">
|
||||
{% for radio in form.interval%}
|
||||
{{ radio.tag }}
|
||||
<label class="btn btn-secondary text-primary bg-white" for="{{ radio.id_for_label }}">
|
||||
<label class="btn btn-outline-secondary" for="{{ radio.id_for_label }}">
|
||||
{{ radio.choice_label }}
|
||||
</label>
|
||||
{% endfor %}
|
||||
@@ -39,7 +39,7 @@
|
||||
<div class="col-auto">
|
||||
{% for radio in form.display_format%}
|
||||
{{ radio.tag }}
|
||||
<label class="btn btn-secondary text-primary bg-white" for="{{ radio.id_for_label }}">
|
||||
<label class="btn btn-outline-secondary" for="{{ radio.id_for_label }}">
|
||||
{{ radio.choice_label }}
|
||||
</label>
|
||||
{% endfor %}
|
||||
@@ -67,7 +67,7 @@
|
||||
</div>
|
||||
<div class="form-row text-center">
|
||||
<div class="col-12">
|
||||
<button type="submit" class="btn btn-primary bg-white text-primary">Посмотреть статистику</button>
|
||||
<button type="submit" class="btn btn-outline-primary">Посмотреть статистику</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -86,9 +86,9 @@
|
||||
<table class="table table-bordered text-center text-secondary mt-5" style="background-color:#f2f2f2;">
|
||||
<thead>
|
||||
<tr>
|
||||
<td scope="col">Пользователи/Даты</td>
|
||||
<td scope="col"> </td>
|
||||
{% for date in log_stats.keys %}
|
||||
<td scope="col">{{date}}</td>
|
||||
<td scope="col">{{ date | date:'d.m' }}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -96,7 +96,7 @@
|
||||
<tr>
|
||||
<td>{{ form.email.value }}</td>
|
||||
{% for time in log_stats.values %}
|
||||
<td>{{time}}</td>
|
||||
<td>{{ time | floatformat:2 }}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -7,7 +7,12 @@
|
||||
{% block heading %}Управление правами{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<link rel="stylesheet" href="{% static 'main/css/work.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'main/css/work.css' %}" xmlns="http://www.w3.org/1999/html">
|
||||
<link rel="stylesheet" href="{% static 'modules/notifications/dist/notifications.css' %}">
|
||||
{% endblock %}
|
||||
{% block extra_scripts %}
|
||||
<script src="{% static 'modules/notifications/dist/notifications.js' %}"></script>
|
||||
<script src="{% static 'main/js/notifications.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
@@ -60,8 +65,16 @@
|
||||
<a href="/work/become_engineer" class="request-acess-button default-button">Получить права инженера</a>
|
||||
<a href="/work/hand_over" class="hand-over-acess-button default-button">Сдать права инженера</a>
|
||||
</div>
|
||||
<div class="col-10">
|
||||
<form method="GET" action="/work/get_tickets">
|
||||
<input class="form-control mb-3" type="number" min="1" value="1" name="count_tickets">
|
||||
<button type="submit" class="default-button">Взять тикеты в работу</button>
|
||||
</form>
|
||||
</div>
|
||||
{% for message in messages %}
|
||||
<script>create_notification('{{message}}','','{{message.tags}}',5000)</script>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% include 'base/success_messages.html' %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
256
main/views.py
256
main/views.py
@@ -1,48 +1,55 @@
|
||||
import logging
|
||||
import os
|
||||
from datetime import datetime
|
||||
from typing import Union, Tuple
|
||||
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.exceptions import PermissionDenied
|
||||
from django.core.handlers.wsgi import WSGIRequest
|
||||
from django.http import HttpResponseRedirect, HttpResponse
|
||||
from django.shortcuts import render, get_list_or_404, redirect
|
||||
from django.shortcuts import render, redirect
|
||||
from django.urls import reverse_lazy, reverse
|
||||
from django.views.generic import FormView
|
||||
from django_registration.views import RegistrationView
|
||||
from django.contrib import messages
|
||||
import django.utils
|
||||
|
||||
# Django REST
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.response import Response
|
||||
|
||||
from zenpy.lib.api_objects import User as ZenpyUser
|
||||
|
||||
from access_controller.settings import EMAIL_HOST_USER, ZENDESK_ROLES, ZENDESK_MAX_AGENTS
|
||||
from main.extra_func import ZendeskAdmin
|
||||
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
|
||||
StatisticData, log, set_session_params_for_work_page
|
||||
from main.zendesk_admin import zenpy
|
||||
from main.forms import AdminPageUsers, CustomRegistrationForm, CustomAuthenticationForm, StatisticForm
|
||||
from main.serializers import ProfileSerializer
|
||||
from main.serializers import ProfileSerializer, ZendeskUserSerializer
|
||||
from .models import UserProfile
|
||||
|
||||
|
||||
def setup_context(profile_lit: bool = False, control_lit: bool = False, work_lit: bool = False,
|
||||
registration_lit: bool = False, login_lit: bool = False, stats_lit: bool = False) -> Dict[str, Any]:
|
||||
|
||||
context = {
|
||||
'profile_lit': profile_lit,
|
||||
'control_lit': control_lit,
|
||||
'work_lit': work_lit,
|
||||
'registration_lit': registration_lit,
|
||||
'login_lit': login_lit,
|
||||
'stats_lit': stats_lit,
|
||||
}
|
||||
return context
|
||||
|
||||
|
||||
class CustomRegistrationView(RegistrationView):
|
||||
"""
|
||||
Отображение и логика работы страницы регистрации пользователя.
|
||||
|
||||
:param form_class: Форма, которую необходимо заполнить для регистрации
|
||||
:type form_class: :class:`main.forms.CustomRegistrationForm`
|
||||
:type form_class: :class:`forms.CustomRegistrationForm`
|
||||
:param template_name: Указание пути к html-странице django регистрации
|
||||
:type template_name: :class:`str`
|
||||
:param success_url: Указание пути к html-странице завершения регистрации
|
||||
@@ -50,12 +57,17 @@ class CustomRegistrationView(RegistrationView):
|
||||
:param is_allowed: Определение зарегистрирован ли пользователь с введенным email на Zendesk и принадлежит ли он к организации SYSTEM
|
||||
:type is_allowed: :class:`bool`
|
||||
"""
|
||||
form_class: CustomRegistrationForm = CustomRegistrationForm
|
||||
template_name: str = 'django_registration/registration_form.html'
|
||||
success_url: str = reverse_lazy('django_registration_complete')
|
||||
is_allowed: bool = True
|
||||
extra_context = setup_context(registration_lit=True)
|
||||
form_class = CustomRegistrationForm
|
||||
template_name = 'django_registration/registration_form.html'
|
||||
urls = {
|
||||
'done': reverse_lazy('password_reset_done'),
|
||||
'invalid_zendesk_email': reverse_lazy('django_registration_disallowed'),
|
||||
'email_sending_error': reverse_lazy('registration_email_error'),
|
||||
}
|
||||
redirect_url = 'done'
|
||||
|
||||
def register(self, form: CustomRegistrationForm) -> Union[User, bool]:
|
||||
def register(self, form: CustomRegistrationForm) -> User:
|
||||
"""
|
||||
Функция регистрации пользователя.
|
||||
1. Ввод email пользователя, указанный на Zendesk
|
||||
@@ -66,14 +78,14 @@ class CustomRegistrationView(RegistrationView):
|
||||
:param form: Email пользователя на Zendesk
|
||||
:return: user
|
||||
"""
|
||||
self.is_allowed = True
|
||||
self.redirect_url = 'done'
|
||||
if check_user_exist(form.data['email']) and get_user_organization(form.data['email']) == 'SYSTEM':
|
||||
forms = PasswordResetForm(self.request.POST)
|
||||
if forms.is_valid():
|
||||
opts = {
|
||||
'use_https': self.request.is_secure(),
|
||||
'token_generator': default_token_generator,
|
||||
'from_email': EMAIL_HOST_USER,
|
||||
'from_email': DEFAULT_FROM_EMAIL,
|
||||
'email_template_name': 'registration/password_reset_email.html',
|
||||
'subject_template_name': 'registration/password_reset_subject.txt',
|
||||
'request': self.request,
|
||||
@@ -85,17 +97,20 @@ class CustomRegistrationView(RegistrationView):
|
||||
email=form.data['email'],
|
||||
password=User.objects.make_random_password(length=50)
|
||||
)
|
||||
forms.save(**opts)
|
||||
update_profile(user.userprofile)
|
||||
self.set_permission(user)
|
||||
return user
|
||||
try:
|
||||
update_profile(user.userprofile)
|
||||
self.set_permission(user)
|
||||
forms.save(**opts)
|
||||
return user
|
||||
except SMTPException:
|
||||
self.redirect_url = 'email_sending_error'
|
||||
else:
|
||||
raise ValueError('Непредвиденная ошибка')
|
||||
else:
|
||||
self.is_allowed = False
|
||||
self.redirect_url = 'invalid_zendesk_email'
|
||||
|
||||
@staticmethod
|
||||
def set_permission(user: User) -> User:
|
||||
def set_permission(user: User) -> None:
|
||||
"""
|
||||
Функция дает разрешение на просмотр страница администратора, если пользователь имеет роль admin.
|
||||
|
||||
@@ -109,7 +124,7 @@ class CustomRegistrationView(RegistrationView):
|
||||
)
|
||||
user.user_permissions.add(permission)
|
||||
|
||||
def get_success_url(self, user: User = None) -> success_url:
|
||||
def get_success_url(self, user: User = None):
|
||||
"""
|
||||
Функция возвращает url-адрес страницы, куда нужно перейти после успешной/не успешной регистрации.
|
||||
Используется самой django-registration.
|
||||
@@ -117,10 +132,11 @@ class CustomRegistrationView(RegistrationView):
|
||||
:param user: пользователь, пытающийся зарегистрироваться
|
||||
:return: адресация на страницу успешной регистрации
|
||||
"""
|
||||
if self.is_allowed:
|
||||
return reverse_lazy('password_reset_done')
|
||||
else:
|
||||
return reverse_lazy('django_registration_disallowed')
|
||||
return self.urls[self.redirect_url]
|
||||
|
||||
|
||||
def registration_error(request):
|
||||
return render(request, 'django_registration/registration_error.html')
|
||||
|
||||
|
||||
@login_required()
|
||||
@@ -133,26 +149,15 @@ def profile_page(request: WSGIRequest) -> HttpResponse:
|
||||
"""
|
||||
user_profile: UserProfile = request.user.userprofile
|
||||
update_profile(user_profile)
|
||||
context = {
|
||||
context = setup_context(profile_lit=True)
|
||||
context.update({
|
||||
'profile': user_profile,
|
||||
'pagename': 'Страница профиля',
|
||||
'ZENDESK_ROLES': ZENDESK_ROLES,
|
||||
}
|
||||
})
|
||||
return render(request, 'pages/profile.html', context)
|
||||
|
||||
|
||||
def auth_user(request: WSGIRequest) -> Tuple:
|
||||
"""
|
||||
Функция возвращает профиль пользователя на Zendesk.
|
||||
|
||||
:param request: email, subdomain и token пользователя
|
||||
:return: объект пользователя Zendesk
|
||||
"""
|
||||
admin = ZendeskAdmin().admin
|
||||
zenpy_user: ZenpyUser = admin.users.search(request.user.email).values[0]
|
||||
return zenpy_user, admin
|
||||
|
||||
|
||||
@login_required()
|
||||
def work_page(request: WSGIRequest, id: int) -> HttpResponse:
|
||||
"""
|
||||
@@ -164,71 +169,75 @@ def work_page(request: WSGIRequest, id: int) -> HttpResponse:
|
||||
"""
|
||||
users = get_users_list()
|
||||
if request.user.id == id:
|
||||
if request.session.get('is_confirm', None):
|
||||
messages.success(request, 'Изменения были применены')
|
||||
elif request.session.get('is_confirm', None) is not None:
|
||||
messages.error(request, 'Изменения не были применены')
|
||||
count = request.session.get('count_tickets', None)
|
||||
if count is not None:
|
||||
messages.success(request, f'{count} тикетов назначено')
|
||||
request.session['is_confirm'] = None
|
||||
request.session['count_tickets'] = None
|
||||
|
||||
engineers = []
|
||||
light_agents = []
|
||||
for user in users:
|
||||
|
||||
if user.custom_role_id == ZENDESK_ROLES['engineer']:
|
||||
engineers.append(user)
|
||||
elif user.custom_role_id == ZENDESK_ROLES['light_agent']:
|
||||
light_agents.append(user)
|
||||
|
||||
context = {
|
||||
context = setup_context(work_lit=True)
|
||||
context.update({
|
||||
'engineers': engineers,
|
||||
'agents': light_agents,
|
||||
'messages': messages.get_messages(request),
|
||||
'licences_remaining': max(0, ZENDESK_MAX_AGENTS - len(engineers)),
|
||||
'pagename': 'Управление правами'
|
||||
}
|
||||
'pagename': 'Управление правами',
|
||||
})
|
||||
return render(request, 'pages/work.html', context)
|
||||
return redirect("login")
|
||||
|
||||
|
||||
def user_update(zenpy_user: User, admin: User, request: WSGIRequest) -> UserProfile:
|
||||
"""
|
||||
Функция устанавливает пользователю роль "agent" (изменяет профиль).
|
||||
|
||||
:param zenpy_user: Пользователь Zendesk
|
||||
:param admin: Пользователь
|
||||
:param request: Запрос установки роли "agent" в Userprofile
|
||||
:return: Обновленный профиль пользователя
|
||||
"""
|
||||
|
||||
admin.users.update(zenpy_user)
|
||||
request.user.userprofile.role = "agent"
|
||||
request.user.userprofile.save()
|
||||
messages.success(request, "Права были изменены")
|
||||
|
||||
|
||||
@login_required()
|
||||
def work_hand_over(request: WSGIRequest) -> HttpResponseRedirect:
|
||||
def work_hand_over(request: WSGIRequest):
|
||||
"""
|
||||
Функция позволяет текущему пользователю (login_required) сдать права, а именно сменить в Zendesk роль с "engineer" на "light agent"
|
||||
и установить роль "agent" в БД. Действия выполняются, если исходная роль пользователя "engineer".
|
||||
Функция позволяет текущему пользователю сдать права, а именно сменить в Zendesk роль с "engineer" на "light_agent"
|
||||
|
||||
:param request: данные текущего пользователя (login_required)
|
||||
:return: перезагрузка текущей страницы после выполнения смены роли
|
||||
"""
|
||||
zenpy_user, admin = auth_user(request)
|
||||
if zenpy_user.custom_role_id == ZENDESK_ROLES['engineer']:
|
||||
zenpy_user.custom_role_id = ZENDESK_ROLES['light_agent']
|
||||
user_update(zenpy_user, admin, request)
|
||||
return HttpResponseRedirect(reverse('work', args=(request.user.id,)))
|
||||
make_light_agent(request.user.userprofile, request.user)
|
||||
return set_session_params_for_work_page(request)
|
||||
|
||||
|
||||
@login_required()
|
||||
def work_become_engineer(request: WSGIRequest) -> HttpResponseRedirect:
|
||||
"""
|
||||
Функция меняет роль пользователя в Zendesk на "engineer" и присваивает роль "agent" в БД (в случае, если исходная роль пользователя была "light_agent").
|
||||
Функция позволяет текущему пользователю получить права, а именно сменить в Zendesk роль с "light_agent" на "engineer"
|
||||
|
||||
:param request: данные текущего пользователя (login_required)
|
||||
:return: перезагрузка текущей страницы после выполнения смены роли
|
||||
"""
|
||||
zenpy_user, admin = auth_user(request)
|
||||
if zenpy_user.custom_role_id == ZENDESK_ROLES['light_agent']:
|
||||
zenpy_user.custom_role_id = ZENDESK_ROLES['engineer']
|
||||
user_update(zenpy_user, admin, request)
|
||||
return HttpResponseRedirect(reverse('work', args=(request.user.id,)))
|
||||
|
||||
make_engineer(request.user.userprofile, request.user)
|
||||
return set_session_params_for_work_page(request)
|
||||
|
||||
|
||||
@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 = [ticket for ticket in zenpy.admin.search(type="ticket") if
|
||||
ticket.group.name == 'Сменная группа' and ticket.assignee is None]
|
||||
count = 0
|
||||
for i in range(len(tickets)):
|
||||
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
|
||||
return set_session_params_for_work_page(request, count)
|
||||
return set_session_params_for_work_page(request, is_confirm=False)
|
||||
|
||||
|
||||
def main_page(request: WSGIRequest) -> HttpResponse:
|
||||
@@ -238,7 +247,7 @@ def main_page(request: WSGIRequest) -> HttpResponse:
|
||||
return render(request, 'pages/index.html')
|
||||
|
||||
|
||||
class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin,SuccessMessageMixin, FormView):
|
||||
class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, FormView):
|
||||
"""
|
||||
Класс отображения страницы администратора.
|
||||
|
||||
@@ -271,7 +280,7 @@ class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin,SuccessMessageMi
|
||||
self.make_light_agents(users)
|
||||
return super().form_valid(form)
|
||||
|
||||
def make_engineers(self, users: list) -> list:
|
||||
def make_engineers(self, users):
|
||||
"""
|
||||
Функция проходит по списку пользователей, проставляя статус "engineer".
|
||||
|
||||
@@ -280,8 +289,9 @@ class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin,SuccessMessageMi
|
||||
"""
|
||||
for user in users:
|
||||
make_engineer(user, self.request.user)
|
||||
log(user, self.request.user.userprofile)
|
||||
|
||||
def make_light_agents(self, users: list) -> list:
|
||||
def make_light_agents(self, users):
|
||||
"""
|
||||
Функция проходит по списку пользователей, проставляя статус "light agent".
|
||||
|
||||
@@ -290,21 +300,14 @@ class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin,SuccessMessageMi
|
||||
"""
|
||||
for user in users:
|
||||
make_light_agent(user, self.request.user)
|
||||
|
||||
def get_context_data(self, **kwargs) -> dict:
|
||||
"""
|
||||
Функция формирования контента страницы администратора (с проверкой прав доступа)
|
||||
"""
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['engineers'], context['light_agents'] = count_users(get_users_list())
|
||||
context['licences_remaining'] = max(0, ZENDESK_MAX_AGENTS - context['engineers'])
|
||||
return context
|
||||
log(user, self.request.user.userprofile)
|
||||
|
||||
|
||||
class CustomLoginView(LoginView):
|
||||
"""
|
||||
Отображение страницы авторизации пользователя
|
||||
"""
|
||||
extra_context = setup_context(login_lit=True)
|
||||
form_class = CustomAuthenticationForm
|
||||
|
||||
|
||||
@@ -316,16 +319,35 @@ class UsersViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
serializer_class = ProfileSerializer
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
users = update_users_in_model().values
|
||||
count = count_users(users)
|
||||
users = update_users_in_model()
|
||||
count = count_users(users.values)
|
||||
profiles = UserProfile.objects.filter(role='agent')
|
||||
serializer = self.get_serializer(profiles, many=True)
|
||||
return Response({
|
||||
res = {
|
||||
'users': serializer.data,
|
||||
'engineers': count[0],
|
||||
'light_agents': count[1]
|
||||
})
|
||||
'light_agents': count[1],
|
||||
'zendesk_users': self.get_zendesk_users(self.choose_users(users.values, profiles)),
|
||||
'max_agents': ZENDESK_MAX_AGENTS
|
||||
}
|
||||
return Response(res)
|
||||
|
||||
@staticmethod
|
||||
def choose_users(zendesk, model):
|
||||
users = []
|
||||
for zendesk_user in zendesk:
|
||||
if zendesk_user.name not in [user.name for user in model]:
|
||||
users.append(zendesk_user)
|
||||
return users
|
||||
|
||||
@staticmethod
|
||||
def get_zendesk_users(users):
|
||||
zendesk_users = ZendeskUserSerializer(
|
||||
data=[user for user in users if user.role != 'admin'],
|
||||
many=True
|
||||
)
|
||||
zendesk_users.is_valid()
|
||||
return zendesk_users.data
|
||||
|
||||
|
||||
@login_required()
|
||||
@@ -336,27 +358,33 @@ def statistic_page(request: WSGIRequest) -> HttpResponse:
|
||||
:param request: данные о пользователе: email, время и интервал работы. Данные получаем через forms.StatisticForm
|
||||
:return: адресация на страницу статистики
|
||||
"""
|
||||
if not request.user.has_perm('main.has_control_access'):
|
||||
return PermissionDenied
|
||||
context = {
|
||||
|
||||
# if not request.user.has_perm('main.has_control_access'):
|
||||
# raise PermissionDenied
|
||||
# context = {
|
||||
|
||||
if not request.user.has_perm("main.has_control_access"):
|
||||
return redirect('index')
|
||||
context = setup_context(stats_lit=True)
|
||||
context.update({
|
||||
'pagename': 'страница статистики',
|
||||
'errors': list(),
|
||||
}
|
||||
})
|
||||
if request.method == "POST":
|
||||
form = StatisticForm(request.POST)
|
||||
if form.is_valid():
|
||||
start_date, end_date = form.cleaned_data['range_start'], form.cleaned_data['range_end']
|
||||
interval, show = form.cleaned_data['interval'], form.cleaned_data['display_format']
|
||||
Data = StatisticData(start_date, end_date, form.cleaned_data['email'])
|
||||
Data.set_display(show)
|
||||
Data.set_interval(interval)
|
||||
stats = Data.get_statistic()
|
||||
if Data.errors:
|
||||
context['errors'] = Data.errors
|
||||
if Data.warnings:
|
||||
context['warnings'] = Data.warnings
|
||||
data = StatisticData(start_date, end_date, form.cleaned_data['email'])
|
||||
data.set_display(show)
|
||||
data.set_interval(interval)
|
||||
stats = data.get_statistic()
|
||||
if data.errors:
|
||||
context['errors'] = data.errors
|
||||
if data.warnings:
|
||||
context['warnings'] = data.warnings
|
||||
context['log_stats'] = stats if not context['errors'] else None
|
||||
if request.method == 'GET':
|
||||
elif request.method == 'GET':
|
||||
form = StatisticForm()
|
||||
context['form'] = form
|
||||
return render(request, 'pages/statistic.html', context)
|
||||
|
||||
93
main/zendesk_admin.py
Normal file
93
main/zendesk_admin.py
Normal file
@@ -0,0 +1,93 @@
|
||||
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 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,
|
||||
})
|
||||
Reference in New Issue
Block a user