Merge develop into feature/registration_failed/html

This commit is contained in:
Andrew Smirnov
2021-04-29 20:03:04 +03:00
98 changed files with 3899 additions and 363 deletions

View File

@@ -3,18 +3,29 @@ 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():
def api_auth() -> dict:
"""
Функция создания пользователя с использованием Zendesk API.
Получает из env Zendesk - email, token, password пользователя.
Если данные валидны и пользователь Zendesk с указанным email и токеном или паролем существует,
создается словарь данных пользователя, полученных через API c Zendesk.
: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:

View File

@@ -1,157 +1,564 @@
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 zenpy.lib.generator import SearchResultGenerator
from main.models import UserProfile
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:
def update_role(user_profile: UserProfile, role: int) -> None:
"""
Класс **ZendeskAdmin** существует, чтобы в каждой фунциии отдельно не проверять аккаунт администратора
Функция меняет роль пользователя.
:param credentials: Полномочия (первым указывается учетная запись организации в Zendesk)
:type credentials: :class:`list of dictionaries`
:param email: Email администратора, указанный в env
:type email: :class:`email`
:param token: Токен администратора (формируется в Zendesk, указывается в env)
:type token: :class:`str`
:param password: Пароль администратора, указанный в env
:type password: :class:`str`
:param user_profile: Профиль пользователя
:param role: Новая роль
:return: Пользователь с обновленной ролью
"""
zendesk = zenpy
user = zendesk.get_user(user_profile.user.email)
user.custom_role_id = role
user_profile.custom_role_id = role
user_profile.save()
zendesk.admin.users.update(user)
credentials = {
'subdomain': 'ngenix1612197338'
}
email = os.getenv('ACCESS_CONTROLLER_API_EMAIL')
token = os.getenv('ACCESS_CONTROLLER_API_TOKEN')
password = os.getenv('ACCESS_CONTROLLER_API_PASSWORD')
def __init__(self):
self.create_admin()
def make_engineer(user_profile: UserProfile, who_changes: User) -> None:
"""
Функция устанавливает пользователю роль инженера.
def check_user(self, email: str) -> bool:
"""
Функция **check_user** осуществляет проверку существования пользователя в Zendesk
:param user_profile: Профиль пользователя
:return: Вызов функции **update_role** с параметрами: профиль пользователя, роль "engineer"
"""
update_role(user_profile, ROLES['engineer'])
:param email: Электронная почта пользователя
:type email: :class:`email`
:return: True, если существует, иначе False
:rtype: :class:`bool`
"""
return True if self.admin.search(email, type='user') else False
def get_user_name(self, email: str) -> str:
"""
Функция **get_user_name** возвращает имя пользователя
def make_light_agent(user_profile: UserProfile, who_changes: User) -> None:
"""
Функция устанавливает пользователю роль легкого агента.
:param user_name: Имя пользователя
:type user_name: :class:`str`
"""
user = self.admin.users.search(email).values[0]
return user.name
def get_user_role(self, email: str) -> str:
"""
Функция **get_user_role** возвращает роль пользователя
:param user_role: Роль пользователя
:type user_role: :class:`str`
"""
user = self.admin.users.search(email).values[0]
return user.role
def get_user_id(self, email: str) -> str:
"""
Функция **get_user_id** возвращает id пользователя
:param user_id: ID пользователя
:type user_id: :class:`str`
"""
user = self.admin.users.search(email).values[0]
return user.id
def get_user_image(self, email: str) -> str:
"""
Функция **get_user_image** возвращает аватар пользователя
:param user_image: Аватар пользователя
:type user_image: :class:`img`
"""
user = self.admin.users.search(email).values[0]
return user.photo['content_url'] if user.photo else None
def create_admin(self) -> None:
"""
Функция **Create_admin()** создает администратора, проверяя наличие вводимых данных в env.
:param credentials: В список полномочий администратора вносятся email, token, password из env
: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'] = os.getenv('ACCESS_CONTROLLER_API_EMAIL')
if self.token:
self.credentials['token'] = self.token
elif self.password:
self.credentials['password'] = self.password
:param user_profile: Профиль пользователя
:return: Вызов функции **update_role** с параметрами: профиль пользователя, роль "light_agent"
"""
tickets: SearchResultGenerator = get_tickets_list(user_profile.user.email)
ticket: ZenpyTicket
for ticket in tickets:
UnassignedTicket.objects.create(
assignee=user_profile.user,
ticket_id=ticket.id,
status=UnassignedTicketStatus.SOLVED if ticket.status == 'solved' else UnassignedTicketStatus.UNASSIGNED
)
if ticket.status == 'solved':
ticket.assignee_id = zenpy.solved_tickets_user_id
else:
raise ValueError('access_controller token or password not in env')
self.admin = Zenpy(**self.credentials)
ticket.assignee = None
ticket.group_id = zenpy.buffer_group_id
if tickets.count:
zenpy.admin.tickets.update(tickets.values)
attempts, success = 5, False
while not success and attempts != 0:
try:
self.admin.search(self.email, type='user')
except APIException:
raise ValueError('invalid access_controller`s login data')
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.
"""
zendesk = zenpy
# У пользователей должна быть организация SYSTEM
org = next(zendesk.admin.search(type='organization', name='SYSTEM'))
users = zendesk.admin.organizations.users(org)
return users
def get_tickets_list(email):
"""
Функция возвращает список тикетов пользователя Zendesk
"""
return zenpy.admin.search(assignee=email, type='ticket')
def update_profile(user_profile: UserProfile):
"""
Функция обновляет профиль пользователя в соотвтетствии с текущим в Zendesk
Функция обновляет профиль пользователя в соответствии с текущим в Zendesk.
:param user_profile: Объект профиля пользователя
:type user_profile: :class:`main.models.UserProfile`
:param user_profile: Профиль пользователя
:return: Обновленный, в соответствие с текущими данными в Zendesk, профиль пользователя
"""
user_profile.name = ZendeskAdmin().get_user_name(user_profile.user.email)
user_profile.role = ZendeskAdmin().get_user_role(user_profile.user.email)
user_profile.image = ZendeskAdmin().get_user_image(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
user_profile.image = user.photo['content_url'] if user.photo else None
user_profile.save()
def check_user_exist(email: str) -> bool:
"""
Функция проверяет, существует ли пользователь
Функция проверяет, существует ли пользователь.
:param email: Электронная почта пользователя
:type email: :class:`str`
:return: True, если существует, иначе False
:rtype: :class:`bool`
:param email: Email пользователя
:return: Зарегистрирован ли пользователь в Zendesk
"""
return ZendeskAdmin().check_user(email)
return zenpy.check_user(email)
def get_user_organization(email: str) -> str:
"""
Функция возвращает организацию пользователя.
:param email: Email пользователя
:return: Организация пользователя
"""
return zenpy.get_user_org(email)
def check_user_auth(email: str, password: str) -> bool:
"""
Функция проверяет, верны ли входные данные
Функция проверяет, верны ли входные данные.
:param email: Электроная почта пользователя
:type email: :class:`str`
:param password: Пароль пользователя
:type password: :class:`str`
:return: True, если входные данные верны, иначе False
:raise: :class:`APIException`: исключение, вызываемое если пользователь не аутентифицирован
:rtype: :class:`bool`
"""
creds = {
'email': email,
'password': password,
'subdomain': 'ngenix1612197338',
}
'email': email,
'password': password,
'subdomain': ACTRL_ZENDESK_SUBDOMAIN,
}
try:
user = Zenpy(**creds)
user.search(email, type='user')
except APIException:
return False
return True
def update_user_in_model(profile: UserProfile, zendesk_user: ZenpyUser):
"""
Функция обновляет профиль пользователя при изменении данных пользователя на Zendesk.
:param profile: Профиль пользователя
:param zendesk_user: Данные пользователя в Zendesk
:return: Обновленный профиль пользователя
"""
profile.name = zendesk_user.name
profile.role = zendesk_user.role
profile.image = zendesk_user.photo['content_url'] if zendesk_user.photo else None
if zendesk_user.custom_role_id is not None:
profile.custom_role_id = int(zendesk_user.custom_role_id)
profile.save()
def count_users(users) -> tuple:
"""
Функция подсчета количества сотрудников с ролями engineer и light_agent
"""
engineers, light_agents = 0, 0
for user in users:
if user.custom_role_id == ROLES['engineer']:
engineers += 1
elif user.custom_role_id == ROLES['light_agent']:
light_agents += 1
return engineers, light_agents
def update_users_in_model():
"""
Обновляет пользователей в модели UserProfile по списку пользователей в организации
"""
users = get_users_list()
for user in users:
try:
profile = User.objects.get(email=user.email).userprofile
update_user_in_model(profile, user)
except ObjectDoesNotExist:
pass
return users
def daterange(start_date, end_date) -> list:
"""
Функция возвращает список дней с start_date по end_date, исключая правую границу.
:param start_date: Начальная дата
:param end_date: Конечная дата
:return: Список дней, не включая конечную дату
"""
dates = []
for n in range(int((end_date - start_date).days)):
dates.append(start_date + timedelta(n))
return dates
def get_timedelta(log, time=None) -> timedelta:
"""
Функция возвращает объект класса timedelta, который хранит промежуток времени от начала суток до момента,
который находится в log (объект класса RoleChangeLogs) или в time(datetime.time), если введён.
:param log: Лог
:param time: Время
:return: Сколько времени прошло от начала суток до события
"""
if time is None:
time = log.change_time.time()
time = timedelta(hours=time.hour, minutes=time.minute, seconds=time.second)
return time
def last_day_of_month(day: int) -> int:
"""
Функция возвращает последний день текущего месяца.
:param day: Текущий день
:return: Последний день месяца
"""
next_month = day.replace(day=28) + timedelta(days=4)
return next_month - timedelta(days=next_month.day)
class StatisticData:
"""
Класс для учета статистики интервалов работы пользователей.
Передаваемые параметры: start_date, end_date, email, stat.
:param display: Формат отображения времени (часы, минуты)
:type display: :class:`list`
:param interval: Интервал времени в часах и минутах
:type interval: :class:`list`
:param start_date: Дата начала работы
:type start_date: :class:`date`
:param end_date: Дата окончания работы
:type end_date: :class:`date`
:param email: Email пользователя
:type email: :class:`str`
:param errors: Список ошибок
:type errors: :class:`list`
:param warnings: Список предупреждений
:type warnings: :class:`list`
:param data: Ретроспектива смены ролей пользователя
:type data: :class:`dict`
:param statistic: Интервалы работы пользователя
:type statistic: :class:`dict`
"""
def __init__(self, start_date, end_date, user_email, stat=None):
self.display = None
self.interval = None
self.start_date = start_date
self.end_date = end_date
self.email = user_email
self.errors = list()
self.warnings = list()
self.data = dict()
self.statistic = dict()
self._init_data()
if stat is None:
self._init_statistic()
else:
self.statistic = stat
def get_statistic(self) -> dict:
"""
Функция возвращает статистику работы пользователя.
:return: Словарь statistic с применением формата отображения и интервала работы(если они есть). None, если были ошибки при создании.
"""
if self.is_valid_statistic():
stat = self.statistic
stat = self._use_display(stat)
stat = self._use_interval(stat)
return stat
else:
return None
def is_valid_statistic(self) -> bool:
"""
Функция проверяет были ли ошибки при создании статистики.
:return: True, при отсутствии ошибок
"""
return not self.errors and self.statistic
def set_interval(self, interval: list) -> bool:
"""
Функция проверяет корректность представления интервала работы.
:param interval: Интервал должен быть указан в днях или месяцах.
:return: True, если указан верно
"""
if interval not in ['months', 'days']:
self.errors += ['Интервал работы должен быть в днях или месяцах']
return False
self.interval = interval
return True
def set_display(self, display_format: list) -> bool:
"""
Функция проверяет корректность формата отображения интервала.
:param display_format: Формат отображения должен быть указан в днях или месяцах.
:return: True, если указан верно
"""
if display_format not in ['days', 'hours']:
self.errors += ['Формат отображения должен быть в часах или днях']
return False
self.display = display_format
return True
def get_data(self) -> Optional[dict]:
"""
Функция возвращает данные - список объектов RoleChangeLogs.
"""
if self.is_valid_data():
return self.data
else:
return None
def is_valid_data(self) -> bool:
"""
Функция определяет были ли ошибки при получении логов.
:return: True, если ошибок нет
"""
return not self.errors
def _use_display(self, stat: list) -> list:
"""
Функция приводит данные к формату отображения.
:param stat: Список данных статистики пользователя
:return: Обновленный список
"""
if not self.is_valid_statistic() or not self.display:
return stat
new_stat = {}
for key, item in stat.items():
if self.display == 'hours':
new_stat[key] = item / 3600
elif self.display == 'days':
new_stat[key] = item / (ONE_DAY * 3600)
return new_stat
def _use_interval(self, stat: dict) -> dict:
"""
Функция объединяет ключи и значения в соответствии с интервалом работы.
:param stat: Статистика работы пользователя
:return: Обновленная статистика
"""
if not self.is_valid_statistic() or not self.interval:
return stat
new_stat = {}
if self.interval == 'months':
# Переделываем ключи под формат('началоесяца - конец_месяца')
for key, value in stat.items():
current_month_start = max(self.start_date, date(year=key.year, month=key.month, day=1))
current_month_end = min(self.end_date, last_day_of_month(date(year=key.year, month=key.month, day=1)))
index = ' - '.join([str(current_month_start), str(current_month_end)])
if new_stat.get(index):
new_stat[index] += value
else:
new_stat[index] = value
elif self.interval == 'days':
new_stat = stat # статистика изначально в днях
return new_stat
def check_time(self) -> bool:
"""
Функция проверяет корректность введенного времени.
:return: True, если время указано корректно. Иначе, False
"""
if self.end_date < self.start_date or self.end_date > datetime.now().date():
return False
return True
def _init_data(self):
"""
Функция возвращает логи в диапазоне дат start_date - end_date для пользователя с указанным email.
:return: Данные о смене статусов пользователя. Если пользователь не найден или интервал времени некорректен - ошибку.
"""
if not self.check_time():
self.errors += ['Конец диапазона должен быть позже начала диапазона и раньше текущего времени']
return
try:
self.data = RoleChangeLogs.objects.filter(
change_time__range=[self.start_date, self.end_date + timedelta(days=1)],
user=User.objects.get(email=self.email),
).order_by('change_time')
except User.DoesNotExist:
self.errors += ['Пользователь не найден']
def _init_statistic(self) -> dict:
"""
Функция заполняет словарь, в котором ключ - дата, значение - кол-во проработанных в этот день секунд.
:return: Статистика работы пользователя (statistic)
"""
self.clear_statistic()
if not self.get_data():
self.warnings += ['Не обнаружены изменения роли в данном промежутке']
return None
first_log, last_log = self.data[0], self.data[len(self.data) - 1]
if first_log.old_role == ROLES['engineer']:
self.prev_engineer_logic(first_log)
if last_log.new_role == ROLES['engineer']:
self.post_engineer_logic(last_log)
for log_index in range(len(self.data) - 1):
if self.data[log_index].new_role == ROLES['engineer']:
self.engineer_logic(log_index)
def engineer_logic(self, log_index):
"""
Функция обрабатывает основную часть работы инженера
"""
current_log, next_log = self.data[log_index], self.data[log_index + 1]
if current_log.change_time.date() != next_log.change_time.date():
self.statistic[current_log.change_time.date()] += (
timedelta(days=1) - get_timedelta(current_log)).total_seconds()
self.statistic[next_log.change_time.date()] += get_timedelta(next_log).total_seconds()
self.fill_daterange(current_log.change_time.date() + timedelta(days=1), next_log.change_time.date())
else:
elapsed_time = next_log.change_time - current_log.change_time
self.statistic[current_log.change_time.date()] += elapsed_time.total_seconds()
def post_engineer_logic(self, last_log):
"""
Функция обрабатывает случай, когда нам изветсно что инженер работал и после диапазона
"""
self.fill_daterange(last_log.change_time.date() + timedelta(days=1), self.end_date + timedelta(days=1))
if last_log.change_time.date() == timezone.now().date():
self.statistic[last_log.change_time.date()] += (
get_timedelta(None, timezone.now().time()) - get_timedelta(last_log)
).total_seconds()
else:
self.statistic[last_log.change_time.date()] += (
timedelta(days=1) - get_timedelta(last_log)).total_seconds()
if self.end_date == timezone.now().date():
self.statistic[self.end_date] = get_timedelta(None, timezone.now().time()).total_seconds()
def prev_engineer_logic(self, first_log):
"""
Функция обрабатывает случай, когда нам изветсно что инженер начал работу до диапазона
"""
self.fill_daterange(max(User.objects.get(email=self.email).date_joined.date(), self.start_date),
first_log.change_time.date())
self.statistic[first_log.change_time.date()] += get_timedelta(first_log).total_seconds()
def fill_daterange(self, first: date, last: date, val: int = 24 * 3600) -> dict:
"""
Функция заполняет диапазон дат значением val (по умолчанию val = кол-во секунд в 1 дне).
:param first: Начальная дата интервала
:param last: Последняя дата интервала
:param val: Количество секунд в одном дне
"""
for day in daterange(first, last):
self.statistic[day] = val
def clear_statistic(self) -> dict:
"""
Функция осуществляет обновление всех дней.
"""
self.statistic.clear()
self.fill_daterange(self.start_date, self.end_date + timedelta(days=1), 0)
class DatabaseHandler(logging.Handler):
def __init__(self):
logging.Handler.__init__(self)
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)

View File

@@ -1,34 +1,142 @@
from django import forms
from django.contrib.auth.forms import AuthenticationForm
from django_registration.forms import RegistrationFormUniqueEmail
from main.models import UserProfile
class CustomRegistrationForm(RegistrationFormUniqueEmail):
"""
Форма для регистрации :class:`django_registration.forms.RegistrationFormUniqueEmail`
с полем для ввода пароля от Zendesk аккаунта и с добавлением bootstrap-класса 'form-control' для всех полей
с добавлением bootstrap-класса "form-control".
:param password_zen: Поле для ввода пароля от Zendesk
:type password_zen: :class:`django.forms.CharField`
:param visible_fields.email: Поле для ввода email, зарегистрированного на Zendesk
:type visible_fields.email: :class:`django_registration.forms.RegistrationFormUniqueEmail`
"""
password_zen = forms.CharField(
required=True,
label="Пароль от Zendesk аккаунта",
strip=False,
widget=forms.PasswordInput(attrs={
'class': 'form-control'
})
)
def __init__(self, *args, **kwargs):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
for visible in self.visible_fields():
if visible.field.widget.attrs.get('class', False):
print(visible.field.widget.attrs['class'].find('form-control'))
if visible.field.widget.attrs['class'].find('form-control') < 0:
visible.field.widget.attrs['class'] += 'form-control'
else:
visible.field.widget.attrs['class'] = 'form-control'
if visible.html_name != 'email':
visible.field.required = False
class Meta(RegistrationFormUniqueEmail.Meta):
fields = RegistrationFormUniqueEmail.Meta.fields
fields.insert(2, 'password_zen')
class AdminPageUsers(forms.Form):
"""
Форма для установки статусов engineer или light_agent пользователям.
:param users: Поле для установки статуса
:type users: :class:`ModelMultipleChoiceField`
"""
users = forms.ModelMultipleChoiceField(
queryset=UserProfile.objects.filter(role='agent'),
widget=forms.CheckboxSelectMultiple(
attrs={
'class': 'form-check-input',
}
),
label=''
)
class CustomAuthenticationForm(AuthenticationForm):
"""
Форма для авторизации :class:`django.contrib.auth.forms.AuthenticationForm`
с изменением поля username на email.
:param username: Поле для ввода email пользователя
:type username: :class:`django.forms.fields.CharField`
"""
username = forms.CharField(
label="Электронная почта",
widget=forms.EmailInput(),
)
error_messages = {
'invalid_login':
"Пожалуйста, введите правильные электронную почту и пароль. Оба поля "
"могут быть чувствительны к регистру."
,
'inactive': "Аккаунт не активен.",
}
INTERVAL_CHOICES = [
('days', 'Дни'),
('months', 'Месяцы')
]
DISPLAY_CHOICES = [
('hours', 'Часы'),
('days', 'Дни/Смены')
]
class StatisticForm(forms.Form):
"""
Форма отображения интервалов работы пользователя.
:param email: Поле для ввода email пользователя
:type email: :class:`django.forms.fields.EmailField`
:param interval: Расчет интервала рабочего времени
:type interval: :class:`django.forms.fields.CharField`
:param display_format: Формат отображения данных
:type display_format: :class:`django.forms.fields.CharField`
:param range_start: Дата и время начала работы
:type range_start: :class:`django.forms.fields.DateField`
:param range_end: Дата и время окончания работы
:type range_end: :class:`django.forms.fields.DateField`
"""
email = forms.EmailField(
label='Электронная почта',
widget=forms.EmailInput(
attrs={
'placeholder': 'example@ngenix.ru',
'class': 'form-control',
}
),
)
interval = forms.ChoiceField(
label='Выберите интервалы времени работы',
choices=INTERVAL_CHOICES,
widget=forms.RadioSelect(
attrs={
'class': 'btn-check',
}
)
)
display_format = forms.ChoiceField(
label='Выберите формат отображения',
choices=DISPLAY_CHOICES,
widget=forms.RadioSelect(
attrs={
'class': 'btn-check',
}
)
)
range_start = forms.DateField(
label='Начало статистики',
widget=forms.DateInput(
attrs={
'type': 'date',
'class': 'btn btn-secondary text-primary bg-white',
}
),
)
range_end = forms.DateField(
label='Конец статистики',
widget=forms.DateInput(
attrs={
'type': 'date',
'class': 'btn btn-secondary text-primary bg-white',
}
),
)

View File

@@ -0,0 +1,17 @@
# Generated by Django 3.1.6 on 2021-03-02 19:55
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('main', '0004_rolechangelogs'),
]
operations = [
migrations.AlterModelOptions(
name='userprofile',
options={'permissions': [('admin', 'Have access to control page')]},
),
]

View File

@@ -0,0 +1,16 @@
# Generated by Django 3.1.6 on 2021-03-03 19:32
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('main', '0005_auto_20210302_2255'),
]
operations = [
migrations.DeleteModel(
name='UserProfile',
),
]

View File

@@ -0,0 +1,29 @@
# Generated by Django 3.1.6 on 2021-03-03 19:35
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', '0006_delete_userprofile'),
]
operations = [
migrations.CreateModel(
name='UserProfile',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('role', models.CharField(default='None', max_length=100)),
('image', models.URLField(blank=True, null=True)),
('name', models.CharField(default='None', max_length=100)),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'permissions': [('control_access', 'User has access to control page')],
},
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 3.1.6 on 2021-03-03 20:05
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('main', '0007_userprofile'),
]
operations = [
migrations.AlterModelOptions(
name='userprofile',
options={},
),
]

View File

@@ -0,0 +1,61 @@
# Generated by Django 3.1.6 on 2021-03-11 08:00
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', '0008_auto_20210303_2305'),
]
operations = [
migrations.AlterField(
model_name='rolechangelogs',
name='change_time',
field=models.DateTimeField(help_text='Дата и время изменения роли'),
),
migrations.AlterField(
model_name='rolechangelogs',
name='changed_by',
field=models.ForeignKey(help_text='Кем была изменена роль', on_delete=django.db.models.deletion.CASCADE, related_name='changed_by', to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='rolechangelogs',
name='name',
field=models.TextField(help_text='Имя пользователя'),
),
migrations.AlterField(
model_name='rolechangelogs',
name='new_role',
field=models.TextField(help_text='Присвоенная роль'),
),
migrations.AlterField(
model_name='rolechangelogs',
name='user',
field=models.ForeignKey(help_text='Пользователь, которому присвоили другую роль', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='userprofile',
name='image',
field=models.URLField(blank=True, help_text='Аватарка', null=True),
),
migrations.AlterField(
model_name='userprofile',
name='name',
field=models.CharField(default='None', help_text='Имя пользователя на нашем сайте', max_length=100),
),
migrations.AlterField(
model_name='userprofile',
name='role',
field=models.CharField(default='None', help_text='Код роли пользователя', max_length=100),
),
migrations.AlterField(
model_name='userprofile',
name='user',
field=models.OneToOneField(help_text='Пользователь', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 3.1.6 on 2021-03-11 08:04
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('main', '0009_models_help_text'),
]
operations = [
migrations.AlterModelOptions(
name='userprofile',
options={'permissions': (('has_control_access', 'Can view admin page'),)},
),
]

View File

@@ -0,0 +1,28 @@
# Generated by Django 3.1.6 on 2021-03-11 14:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0010_userprofile_meta'),
]
operations = [
migrations.AddField(
model_name='rolechangelogs',
name='old_role',
field=models.IntegerField(default=0, help_text='Старая роль'),
),
migrations.AlterField(
model_name='rolechangelogs',
name='new_role',
field=models.IntegerField(default=0, help_text='Присвоенная роль'),
),
migrations.AlterField(
model_name='userprofile',
name='role',
field=models.IntegerField(default=0, help_text='Код роли пользователя'),
),
]

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

@@ -0,0 +1,29 @@
# Generated by Django 3.1.6 on 2021-03-14 11:55
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('main', '0013_auto_20210311_2040'),
]
operations = [
migrations.AddField(
model_name='userprofile',
name='custom_role_id',
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(
model_name='userprofile',
name='role',
field=models.CharField(default='None', help_text='Глобальное имя роли пользователя', max_length=100),
),
]

View File

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

View File

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

View File

@@ -0,0 +1,14 @@
# Generated by Django 3.1.6 on 2021-03-29 21:43
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('main', '0015_auto_20210330_0007'),
('main', '0015_auto_20210321_1600'),
]
operations = [
]

View 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='Статус тикета'),
),
]

View File

@@ -1,26 +1,37 @@
from django.contrib.auth.models import User
from django.db import models
from django.contrib.auth.models import User
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.utils import timezone
from access_controller.settings import ZENDESK_ROLES
class UserProfile(models.Model):
"""
Модель профиля пользователя
Модель профиля пользователя.
:param user: OneToOneField к модели :class:`django.contrib.auth.models.User`
:param role: Код роли пользователя
:type role: :class:`integer`
:param image: Аватарка
:type image: :class:`img`
:param name: Имя пользователя на нашем сайте
:type name: :class:`str`
Профиль создается и изменяется при создании и изменении модель User.
"""
user = models.OneToOneField(to=User, on_delete=models.CASCADE)
role = models.CharField(default='None', max_length=100)
image = models.URLField(null=True, blank=True)
name = models.CharField(default='None', max_length=100)
class Meta:
permissions = (
('has_control_access', 'Can view admin page'),
)
user = models.OneToOneField(to=User, on_delete=models.CASCADE, help_text='Пользователь')
role = models.CharField(default='None', max_length=100, help_text='Глобальное имя роли пользователя')
custom_role_id = models.IntegerField(default=0, help_text='Код роли пользователя')
image = models.URLField(null=True, blank=True, help_text='Аватарка')
name = models.CharField(default='None', max_length=100, help_text='Имя пользователя на нашем сайте')
@property
def zendesk_role(self):
id = self.custom_role_id
for role, r_id in ZENDESK_ROLES.items():
if r_id == id:
return role
return 'UNDEFINED'
@receiver(post_save, sender=User)
@@ -36,19 +47,38 @@ def save_user_profile(sender, instance, **kwargs):
class RoleChangeLogs(models.Model):
"""
Модель для логирования изменений ролей пользователя
:param user: Пользователь, которому присвоили другую роль, ForeignKey к модели :class:`django.contrib.auth.models.User`
:param name: Имя пользователя
:type name: :class:`str`
:param new_role: Присвоенная роль
:type new_role: :class:`str`
:param change_time: Дата изменения роли`
:type change_time: :class:`datetime.datetime`
:param changed_by: Кем была изменена роль, ForeignKey к модели :class:`django.contrib.auth.models.User`
Модель для логирования изменений ролей пользователя.
"""
user = models.ForeignKey(to=User, on_delete=models.CASCADE)
name = models.TextField()
new_role = models.TextField()
change_time = models.DateTimeField()
changed_by = models.ForeignKey(to=User, on_delete=models.CASCADE, related_name='changed_by')
user = models.ForeignKey(to=User, on_delete=models.CASCADE, help_text='Пользователь, которому присвоили другую роль')
old_role = models.IntegerField(default=0, help_text='Старая роль')
new_role = models.IntegerField(default=0, help_text='Присвоенная роль')
change_time = models.DateTimeField(default=timezone.now, help_text='Дата и время изменения роли')
changed_by = models.ForeignKey(to=User, on_delete=models.CASCADE, related_name='changed_by', help_text='Кем была изменена роль')
class UnassignedTicketStatus(models.IntegerChoices):
"""
Класс статусов не распределенных тикетов.
:param UNASSIGNED: Снят с пользователя, перенесён в буферную группу
:param RESTORED: Авторство восстановлено
:param NOT_FOUND: Пока нас не было, тикет испарился из буферной группы. Дополнительные действия не требуются
:param CLOSED: Тикет уже был закрыт. Дополнительные действия не требуются
:param SOLVED: Тикет решён. Записан на пользователя с почтой SOLVED_TICKETS_EMAIL
"""
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', help_text='Пользователь, с которого снят тикет')
ticket_id = models.IntegerField(help_text='Номер тикера, для которого сняли ответственного')
status = models.IntegerField(choices=UnassignedTicketStatus.choices, default=UnassignedTicketStatus.UNASSIGNED, help_text='Статус тикета')

38
main/serializers.py Normal file
View File

@@ -0,0 +1,38 @@
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):
"""
Класс serializer для модели User.
"""
class Meta:
model = User
fields = ['email']
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"

View File

@@ -2,57 +2,58 @@
<html lang="ru" class="h-100">
{% load static %}
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{% block title %}{% endblock %}</title>
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css"
integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2"
crossorigin="anonymous">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/5.0.0-alpha1/css/bootstrap.min.css"
integrity="sha384-r4NyP46KrjDleawBgD5tp8Y7UzmLA05oM1iAEQ17CSuDqnUK2+k9luXQOfXJCJ4I" crossorigin="anonymous">
<style>
.bd-placeholder-img {
font-size: 1.125rem;
text-anchor: middle;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
.bd-placeholder-img {
font-size: 1.125rem;
text-anchor: middle;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
@media (min-width: 768px) {
.bd-placeholder-img-lg {
font-size: 3.5rem;
}
@media (min-width: 768px) {
.bd-placeholder-img-lg {
font-size: 3.5rem;
}
</style>
}
</style>
{% block extra_css %}{% endblock %}
{% block extra_scripts %}{% endblock %}
</head>
<body class="d-flex flex-column h-100">
{% include 'base/menu.html' %}
{% include 'base/menu.html' %}
<main class="flex-shrink-0">
<div class="container mt-4 mb-4">
<h1 class="mb-4 text-center">
{% block heading %}
<main class="flex-shrink-0">
<div class="container mt-4 mb-4">
<h1 class="mb-4 text-center">
{% block heading %}
{% endblock %}
</h1>
{% block content %}
{% endblock %}
</h1>
{% block content %}
{% endblock %}
</div>
</main>
</div>
</main>
<footer class="footer mt-auto py-3 bg-light">
<div class="container">
<small class="text-muted mt-auto">Сайт сделан учениками Школы Программистов (Группа №02)</small>
</div>
</footer>
<footer class="footer mt-auto py-3 bg-light">
<div class="container">
<small class="text-muted mt-auto">Сайт сделан учениками Школы Программистов (Группа №02)</small>
</div>
</footer>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/js/bootstrap.bundle.min.js"
integrity="sha384-ygbV9kiqUc6oa4msXn9868pTtWMgiQaeYH7/t7LECLbyPA2x65Kgf80OJFdroafW"
crossorigin="anonymous"
></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/js/bootstrap.bundle.min.js"
integrity="sha384-ygbV9kiqUc6oa4msXn9868pTtWMgiQaeYH7/t7LECLbyPA2x65Kgf80OJFdroafW" crossorigin="anonymous">
</script>
</body>
</html>

View File

@@ -3,20 +3,56 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<nav class="navbar navbar-light" style="background-color: #00FF00;">
<nav class="navbar navbar-light" style="background-color: #113A60;">
<a class="navbar-brand" href="{% url 'index' %}">
<img src="{% static 'main/img/logo.png' %}" width="30" height="30" class="d-inline-block align-top" alt="" loading="lazy">
Access Controller
<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 {% 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 {% 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>

View File

@@ -11,5 +11,5 @@
{% block content %}
<br>
<h4> Нет пользователя с указаным адресом электронной почты, либо был введён неверный пароль</h4>
<h4> Нет пользователя с указаным адресом электронной почты.</h4>
{% endblock %}

View File

@@ -10,5 +10,5 @@
{% block content %}
<br>
<h4> Регистрация прошла успешно. <a href="/login/">Войти сейчас</a></h4>
<h4> Регистрация прошла успешно. <a href="{% url 'login'%}">Войти сейчас</a></h4>
{% endblock %}

View File

@@ -0,0 +1,15 @@
{% extends 'base/base.html' %}
{% load static %}
{% block title %}
Регистрация завершена
{% endblock %}
{% block heading %}
Регистрация
{% endblock %}
{% block content %}
<br>
<h4> Произошла ошибка при отправке электронного сообщения.</h4>
{% endblock %}

View File

@@ -11,14 +11,12 @@
{% block content %}
<form method="post" action="">
{% csrf_token %}
{% for field in form %}
{{ field.label_tag }}
{{ field }}
{{ form.email.label_tag }}
{{ form.email }}
<br>
{% if field.errors %}
<span>{{ field.errors }}</span>
{% if form.email.errors %}
<span>{{ form.email.errors }}</span>
{% endif %}
{% endfor %}
<input type="submit" value="Зарегистрироваться" class="clearfix">
</form>
{% endblock %}

View File

@@ -0,0 +1,112 @@
{% extends 'base/base.html' %}
{% load static %}
{% block title %}Управление{% endblock %}
{% block heading %}Управление{% endblock %}
{% block extra_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="https://unpkg.com/react@17/umd/react.development.js" crossorigin></script>
<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>
<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" id="licences_remaining">Свободных Мест:</p>
</div>
{% for message in messages %}
<script>create_notification('{{message}}','','{{message.tags}}',2000)</script>
{% endfor %}
{% block form %}
<form method="post">
{% csrf_token %}
<div class="row justify-content-center new-section">
<div class="col-10">
<h6 class="table-title">Список сотрудников</h6>
{% block 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>
</thead>
<tbody id="tbody"></tbody>
</table>
<p id="loading">Данные загружаются...</p>
{% endblock %}
</div>
</div>
{% block count %}
<div class="row justify-content-center new-section">
<div class="col-5">
<div class="info">
<div class="info-row">
<div class="info-target">Инженеров:</div>
<div class="info-quantity">
<div class="status-circle-small light-green"></div>
<span class="info-quantity-value">{{ engineers }}</span>
</div>
</div>
<div class="info-row">
<div class="info-target">Легких агентов:</div>
<div class="info-quantity">
<div class="status-circle-small light-yellow"></div>
<span class="info-quantity-value">{{ light_agents }}</span>
</div>
</div>
</div>
</div>
{% endblock %}
{% block buttons %}
<div class="col-5">
<button type="submit" name="engineer" class="request-acess-button default-button">
Назначить выбранных на роль инженера
</button>
<button type="submit" name="light_agent" class="hand-over-acess-button default-button">
Назначить выбранных на роль легкого агента
</button>
</div>
{% endblock %}
</div>
</form>
{% endblock %}
</div>
{% endblock %}

View File

@@ -10,17 +10,14 @@
{% block extra_css %}
<style>
.img{
<style>
.img {
width:auto;
height:auto;
max-width:100px!important;
max-height:100px!important;
}
</style>
</style>
{% endblock %}
{% block content %}
@@ -28,24 +25,31 @@
<div class="row">
<div class="col-auto">
<div class="container">
{% if image_url %}
<img src={{image_url}} class="img img-thumbnail" alt="Аватар">
{% else %}
<img src="{% static 'no_avatar.png' %}" class="img img-thumbnail" alt="Нет изображения">
{% endif %}
<img
src="{% if profile.image %}{{ profile.image }}{% else %}{% static 'no_avatar.png' %}{% endif %}"
class="img img-thumbnail"
alt="Нет изображения"
>
</div>
<a href="{%url 'password_change' %}">Сменить пароль</a>
</div>
<div class="col">
<h5><span class="badge bg-secondary text-light">Имя пользователя</span> {{name}}</h5>
<h5><span class="badge bg-secondary text-light">Имя пользователя</span> {{ profile.name }}</h5>
<br>
<h5><span class="badge bg-secondary text-light">Электронная почта</span> {{email}}</h5>
<h5><span class="badge bg-secondary text-light">Электронная почта</span> {{ profile.user.email }}</h5>
<br>
<h5><span class="badge bg-secondary text-light">Текущая роль</span> {{role}}</h5>
<h5><span class="badge bg-secondary text-light">Текущая роль</span>
{% if profile.custom_role_id == ZENDESK_ROLES.engineer %}
engineer
{% elif profile.custom_role_id == ZENDESK_ROLES.light_agent %}
light_agent
{% endif %}
</h5>
</div>
</div>
<div align="center">
<form action="">
<button class="btn btn-primary"><big>Запросить права доступа</big></button>
<a href="{% url 'work' profile.user.id %}" class="btn btn-primary"><big>Запросить права доступа</big></a>
</form>
</div>
{% endblock %}

View File

@@ -0,0 +1,106 @@
{% extends 'base/base.html' %}
{% load static %}
{% block title %}{{ pagename }}{% endblock %}
{% block heading %} Страницы просмотра статистики{% endblock %}
{% block content%}
<div class="mt-5">
<div class="container-fluid" style="font-size:2rem">
<form method="post">
{% csrf_token %}
<div class="row g-3">
<div class="col-auto">
{{ form.email.label }}
</div>
<div class="col-auto mt-4">
{{ form.email }}
</div>
</div>
<div class="row g-3 mt-4">
<div class="col-auto">
{{ form.interval.label }}
</div>
<div class="col-auto">
{% for radio in form.interval%}
{{ radio.tag }}
<label class="btn btn-outline-secondary" for="{{ radio.id_for_label }}">
{{ radio.choice_label }}
</label>
{% endfor %}
</div>
</div>
<div class="row g-3 mt-4">
<div class="col-auto">
{{ form.display_format.label }}
</div>
<div class="col-auto">
{% for radio in form.display_format%}
{{ radio.tag }}
<label class="btn btn-outline-secondary" for="{{ radio.id_for_label }}">
{{ radio.choice_label }}
</label>
{% endfor %}
</div>
</div>
<div class="row g-3 mt-4">
<div class="col-auto">
{{ form.range_start.label}}
</div>
<div class="col-auto">
<div class='col-sm-7'>
{{ form.range_start}}
</div>
</div>
</div>
<div class="row g-3 mt-4">
<div class="col-auto">
{{ form.range_end.label}}
</div>
<div class="col-auto">
<div class='col-sm-7'>
{{ form.range_end}}
</div>
</div>
</div>
<div class="form-row text-center">
<div class="col-12">
<button type="submit" class="btn btn-outline-primary">Посмотреть статистику</button>
</div>
</div>
</form>
</div>
<ul>
{% for error in errors %}
<li><span class="badge bg-danger">{{error}}</span></li>
{% endfor %}
</ul>
<ul>
{% for warning in warnings %}
<li><span class="badge bg-warning">{{warning}}</span></li>
{% endfor %}
</ul>
<div class="container-fluid">
<table class="table table-bordered text-center text-secondary mt-5" style="background-color:#f2f2f2;">
<thead>
<tr>
<td scope="col">&nbsp;</td>
{% for date in log_stats.keys %}
<td scope="col">{{ date | date:'d.m' }}</td>
{% endfor %}
</tr>
</thead>
<tbody>
<tr>
<td>{{ form.email.value }}</td>
{% for time in log_stats.values %}
<td>{{ time | floatformat:2 }}</td>
{% endfor %}
</tr>
</tbody>
</table>
</div>
</div>
{% endblock %}

View File

@@ -4,17 +4,22 @@
{% block title %}{{ pagename }}{% endblock %}
{% block heading %}Управление{% endblock %}
{% 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 %}
<div class="container-md">
<div class="new-section">
<p class="row page-description">Основаная информация о странице</p>
<p class="row page-description">Свободных Мест: {{ licences_remaining }}</p>
</div>
<div class="row justify-content-center new-section">
@@ -22,25 +27,16 @@
<h6 class="table-title">Список сотрудников с правами инженера</h6>
<table class="light-table">
<thead>
<th>ID</th>
<th>email</th>
<th>Expiration Date</th>
<th>Name(link to profile)</th>
<th>Email</th>
<th>Name</th>
</thead>
<tbody>
<tr>
<td>1</td>
<td>big_boss123@example.ru</td>
<td>19:30 18.02.21</td>
<td><a href="#">Иван Иванов</a></td>
</tr>
<tr>
<td>2</td>
<td>gachi_cool456@example.ru</td>
<td>21:00 18.02.21</td>
<td><a href="#">Пётр Петров</a></td>
</tr>
{% for engineer in engineers %}
<tr>
<td>{{ engineer.email }}</td>
<td>{{ engineer.name }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
@@ -52,23 +48,33 @@
<div class="info-target">инженеров: </div>
<div class="info-quantity">
<div class="status-circle-small light-green"></div>
<span class="info-quantity-value">13</span>
<span class="info-quantity-value">{{ engineers|length }}</span>
</div>
</div>
<div class="info-row">
<div class="info-target">легких агентов:</div>
<div class="info-quantity">
<div class="status-circle-small light-yellow"></div>
<span class="info-quantity-value">22</span>
<span class="info-quantity-value">{{ agents|length }}</span>
</div>
</div>
</div>
</div>
<div class="col-5">
<button class="request-acess-button default-button">Получить права инженера</button>
<button class="hand-over-acess-button default-button">Сдать права инженера</button>
<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}}',2000)</script>
{% endfor %}
</div>
</div>
{% endblock %}
{% endblock %}

View File

@@ -1,10 +1,9 @@
{% extends 'base/base.html' %}
{% block title %}
Авторизация
{% endblock %}
{% block heading %}
Авторизация
{% endblock %}
{% block title %}Авторизация{% endblock %}
{% block heading %}Авторизация{% endblock %}
{% block content %}
<div class="container">
<div class="card mx-auto" style="width: 40rem">
@@ -31,7 +30,7 @@
{% endif %}
<div class="text-center">
<button type="submit" class="btn btn-primary">Войти</button>
<a href="" class="btn btn-link" style="display: block;">Забыли пароль?</a>
<a href="{% url 'password_reset' %}" class="btn btn-link" style="display: block;">Забыли пароль?</a>
</div>
</form>
</div>

View File

@@ -0,0 +1,12 @@
{% extends "base/base.html" %}
{% load static %}
{% block title %}Пароль успешно изменен{% endblock title %}
{% block heading %}Пароль успешно изменен{% endblock %}
{% block content %}
<div>
<h4>Ваш пароль был изменен.</h4>
</div>
{% endblock content %}

View File

@@ -0,0 +1,14 @@
{% extends "base/base.html" %}
{% load static %}
{% block title %}Изменение пароля{% endblock title %}
{% block heading %}Сменить пароль{% endblock %}
{% block content %}
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Сменить" class="btn btn-success">
</form>
{% endblock content %}

View File

@@ -0,0 +1,13 @@
{% extends "base/base.html" %}
{% block title %}
{{ pagename }}
{% endblock %}
{% block heading %}
Восстановление пароля
{% endblock %}
{% block content %}
<p>Ваш новый пароль был установлен. Вы можете <a href="{% url 'login' %}">войти</a> сейчас</p>
{% endblock %}

View File

@@ -0,0 +1,23 @@
{% extends "base/base.html" %}
{% block title %}
{{ pagename }}
{% endblock %}
{% block heading %}
Восстановление пароля
{% endblock %}
{% block content %}
{% if validlink %}
<p>Пожалуйста, введите пароль дважды:</p>
<form action="." method="post">
{{ form.as_p }}
{% csrf_token %}
<p><input class="btn btn-success" type="submit" value="Сменить пароль"/></p>
</form>
{% else %}
<p>Неверная ссылка восстановления пароля, возможно она уже была использована.
Пожалуйста, запросите новый сброс пароля</p>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,14 @@
{% extends "base/base.html" %}
{% block title %}
{{ pagename }}
{% endblock %}
{% block heading %}
Восстановление пароля
{% endblock %}
{% block content %}
<p>Мы отправили вам на почту инструкцию по восстановлению</p>
<p>Если вы не получили сообщение, убедитесь что верно ввели адрес электронной почты.</p>
{% endblock %}

View File

@@ -0,0 +1,18 @@
{% extends "base/base.html" %}
{% block title %}
{{ pagename }}
{% endblock %}
{% block heading %}
Забыли пароль?
{% endblock %}
{% block content %}
<p>Введте свой e-mail адрес для восстановления пароля.</p>
<form action="." method="post">
{{ form.as_p }}
<p><input class="btn btn-success" type="submit" value="Отпрваить e-mail"></p>
{% csrf_token %}
</form>
{% endblock %}

View File

@@ -1,3 +1,2 @@
from django.test import TestCase
# Create your tests here.
from django.test import TestCase, Client
import access_controller.settings as sets

6
main/urls.py Normal file
View File

@@ -0,0 +1,6 @@
from rest_framework.routers import DefaultRouter
from main.views import UsersViewSet
router = DefaultRouter()
router.register(r'users', UsersViewSet)

View File

@@ -1,75 +1,393 @@
from django.shortcuts import render
from django.urls import reverse_lazy
from main.extra_func import check_user_exist, check_user_auth, update_profile
from main.models import UserProfile
from django.contrib.auth.models import User
from main.forms import CustomRegistrationForm
from django_registration.views import RegistrationView
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.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.contrib.auth.models import User, Permission
from django.contrib.auth.tokens import default_token_generator
from django.contrib.auth.forms import PasswordResetForm
from django.contrib.auth.views import LoginView
from django.contrib.contenttypes.models import ContentType
from django.contrib.messages.views import SuccessMessageMixin
from django.core.handlers.wsgi import WSGIRequest
from django.http import HttpResponseRedirect, HttpResponse
from django.shortcuts import render, redirect
from django.urls import reverse_lazy, reverse
from django.views.generic import FormView
from django_registration.views import RegistrationView
# Django REST
from rest_framework import viewsets
from rest_framework.response import Response
from zenpy import Zenpy
from access_controller.settings import DEFAULT_FROM_EMAIL, ZENDESK_ROLES, ZENDESK_MAX_AGENTS
from main.extra_func import check_user_exist, update_profile, get_user_organization, \
make_engineer, make_light_agent, get_users_list, update_users_in_model, count_users, \
StatisticData, log, set_session_params_for_work_page
from main.zendesk_admin import zenpy
from main.forms import AdminPageUsers, CustomRegistrationForm, CustomAuthenticationForm, StatisticForm
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:`forms.CustomRegistrationForm`
:param template_name: Указание пути к html-странице django регистрации
:type template_name: :class:`str`
:param success_url: Указание пути к html-странице завершения регистрации
:type success_url: :class:`django.utils.functional.lazy.<locals>.__proxy__`
:param is_allowed: Определение зарегистрирован ли пользователь с введенным email на Zendesk и принадлежит ли он к организации SYSTEM
:type is_allowed: :class:`bool`
"""
extra_context = setup_context(registration_lit=True)
form_class = CustomRegistrationForm
template_name = 'django_registration/registration_form.html'
success_url = reverse_lazy('django_registration_complete')
is_allowed = True
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):
self.is_allowed = True
if check_user_exist(form.data['email']) and check_user_auth(form.data['email'], form.data['password_zen']):
user = User.objects.create_user(
username=form.data['username'],
email=form.data['email'],
password=form.data['password1']
def register(self, form: CustomRegistrationForm) -> User:
"""
Функция регистрации пользователя.
1. Ввод email пользователя, указанный на Zendesk
2. В случае если пользователь с данным паролем зарегистрирован на Zendesk и относится к организации SYSTEM,
происходит сброс ссылки с установлением пароля на указанный email
3. Создается пользователь class User, а также его профиль.
:param form: Email пользователя на Zendesk
:return: user
"""
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': DEFAULT_FROM_EMAIL,
'email_template_name': 'registration/password_reset_email.html',
'subject_template_name': 'registration/password_reset_subject.txt',
'request': self.request,
'html_email_template_name': None,
'extra_email_context': None,
}
user = User.objects.create_user(
username=form.data['email'],
email=form.data['email'],
password=User.objects.make_random_password(length=50)
)
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.redirect_url = 'invalid_zendesk_email'
@staticmethod
def set_permission(user: User) -> None:
"""
Функция дает разрешение на просмотр страница администратора, если пользователь имеет роль admin.
:param user: авторизованный пользователь (получает разрешение, имея роль "admin")
"""
if user.userprofile.role == 'admin':
content_type = ContentType.objects.get_for_model(UserProfile)
permission = Permission.objects.get(
codename='has_control_access',
content_type=content_type,
)
profile = user.userprofile
update_profile(profile)
else:
self.is_allowed = False
user.user_permissions.add(permission)
def get_success_url(self, user=None):
def get_success_url(self, user: User = None):
"""
Возвращает url-адрес страницы, куда нужно перейти после успешной/неуспешной регистрации
Используется самой django-registration
Функция возвращает url-адрес страницы, куда нужно перейти после успешной/не успешной регистрации.
Используется самой django-registration.
:param user: пользователь, пытающийся зарегистрироваться
:return: адресация на страницу успешной регистрации
"""
if self.is_allowed:
return reverse_lazy('django_registration_complete')
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()
def profile_page(request):
def profile_page(request: WSGIRequest) -> HttpResponse:
"""
Отображение страницы профиля
Функция отображения страницы профиля.
:param request: объект с деталями запроса
:type request: :class:`django.http.HttpResponse`
:return: объект ответа сервера с HTML-кодом внутри
:param request: данные пользователя из БД
:return: адресация на страницу пользователя
"""
user_profile = request.user.userprofile
user_profile: UserProfile = request.user.userprofile
update_profile(user_profile)
context = {
'email': user_profile.user.email,
'name': user_profile.name,
'role': user_profile.role,
'image_url': user_profile.image,
'pagename': 'Страница профиля'
}
context = setup_context(profile_lit=True)
context.update({
'profile': user_profile,
'pagename': 'Страница профиля',
'ZENDESK_ROLES': ZENDESK_ROLES,
})
return render(request, 'pages/profile.html', context)
def main_page(request):
@login_required()
def work_page(request: WSGIRequest, id: int) -> HttpResponse:
"""
Функция отображения страницы "Управления правами" для текущего пользователя (login_required).
:param request: объект пользователя
:param id: id пользователя, используется для динамической адресации
:return: адресация на страницу "Управления правами" (либо на страницу "Авторизации", если id и user.id не совпадают
"""
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 = 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': 'Управление правами',
})
return render(request, 'pages/work.html', context)
return redirect("login")
@login_required()
def work_hand_over(request: WSGIRequest):
"""
Функция позволяет текущему пользователю сдать права, а именно сменить в Zendesk роль с "engineer" на "light_agent"
:param request: данные текущего пользователя (login_required)
:return: перезагрузка текущей страницы после выполнения смены роли
"""
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 роль с "light_agent" на "engineer"
:param request: данные текущего пользователя (login_required)
:return: перезагрузка текущей страницы после выполнения смены роли
"""
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:
"""
Функция переадресации на главную страницу.
"""
return render(request, 'pages/index.html')
class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, FormView):
"""
Класс отображения страницы администратора.
:param permission_required: Права доступа к странице администратора
:type permission_required: :class:`str`
:param template_name: HTML-шаблон страницы администратора
:type template_name: :class:`str`
:param form_class: Форма страницы администратора
:type form_class: :class:`forms.AdminPageUsersForm`
:param success_url: Адрес страницы администратора
:type success_url: :class:`HttpResponseRedirect`
"""
permission_required = 'main.has_control_access'
template_name = 'pages/adm_ruleset.html'
form_class = AdminPageUsers
success_url = '/control/'
success_message = "Права были изменены."
def form_valid(self, form: AdminPageUsers) -> AdminPageUsers:
"""
Функция обновления страницы AdminPageUsers.
:param form: Форма страницы администратора
:return: Обновленная страница (пользователям проставлены новые статусы)
"""
users = form.cleaned_data['users']
if 'engineer' in self.request.POST:
self.make_engineers(users)
elif 'light_agent' in self.request.POST:
self.make_light_agents(users)
return super().form_valid(form)
def make_engineers(self, users):
"""
Функция проходит по списку пользователей, проставляя статус "engineer".
:param users: Список пользователей
:return: Обновленный список пользователей
"""
for user in users:
make_engineer(user, self.request.user)
log(user, self.request.user.userprofile)
def make_light_agents(self, users):
"""
Функция проходит по списку пользователей, проставляя статус "light agent".
:param users: Список пользователей
:return: Обновленный список пользователей
"""
for user in users:
make_light_agent(user, self.request.user)
log(user, self.request.user.userprofile)
class CustomLoginView(LoginView):
"""
Отображение страницы авторизации пользователя
"""
extra_context = setup_context(login_lit=True)
form_class = CustomAuthenticationForm
class UsersViewSet(viewsets.ReadOnlyModelViewSet):
"""
Класс для получения пользователей с помощью api
"""
queryset = UserProfile.objects.filter(role='agent')
serializer_class = ProfileSerializer
def list(self, request, *args, **kwargs):
users = update_users_in_model()
count = count_users(users.values)
profiles = UserProfile.objects.filter(role='agent')
serializer = self.get_serializer(profiles, many=True)
res = {
'users': serializer.data,
'engineers': count[0],
'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()
def statistic_page(request: WSGIRequest) -> HttpResponse:
"""
Функция отображения страницы статистики (для "superuser").
:param request: данные о пользователе: email, время и интервал работы. Данные получаем через forms.StatisticForm
:return: адресация на страницу статистики
"""
# 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
context['log_stats'] = stats if not context['errors'] else None
elif request.method == 'GET':
form = StatisticForm()
context['form'] = form
return render(request, 'pages/statistic.html', context)
def registration_failed(request):
return render(request, 'pages/registration_failed.html')

93
main/zendesk_admin.py Normal file
View 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,
})