Merged develop into current

This commit is contained in:
Timofey Mazurov 2021-03-15 19:09:45 +03:00
commit 46012f487b
14 changed files with 447 additions and 40 deletions

View File

@ -135,7 +135,6 @@ ACCOUNT_ACTIVATION_DAYS = 7
LOGIN_REDIRECT_URL = '/'
LOGOUT_REDIRECT_URL = '/'
# Название_приложения.Названиеайла.Название_класса_обработчика
AUTHENTICATION_BACKENDS = [
'access_controller.auth.EmailAuthBackend',
@ -149,3 +148,5 @@ ZENDESK_ROLES = {
'engineer': 360005209000,
'light_agent': 360005208980,
}
ONE_DAY = 12 # Количество часов в 1 рабочем дне

View File

@ -19,6 +19,8 @@ from django.urls import path, include
from main.views import work_page, work_hand_over, work_become_engineer, AdminPageView
from main.views import main_page, profile_page, CustomRegistrationView, CustomLoginView
from main.views import work_page, work_hand_over, work_become_engineer, \
AdminPageView, statistic_page
urlpatterns = [
path('admin/', admin.site.urls, name='admin'),
@ -31,9 +33,8 @@ urlpatterns = [
path('work/<int:id>', work_page, name="work"),
path('work/hand_over/', work_hand_over, name="work_hand_over"),
path('work/become_engineer/', work_become_engineer, name="work_become_engineer"),
path('accounts/', include('django_registration.backends.activation.urls')),
path('accounts/login/', include('django.contrib.auth.urls')),
path('control/', AdminPageView.as_view(), name='control')
path('control/', AdminPageView.as_view(), name='control'),
path('statistic/', statistic_page, name='statistic')
]
urlpatterns += [

View File

@ -1,17 +1,15 @@
import logging
import os
from datetime import timedelta, datetime, date
from django.contrib.auth.models import User
from django.utils import timezone
from zenpy import Zenpy
from zenpy.lib.exception import APIException
from access_controller.settings import ZENDESK_ROLES as ROLES, ONE_DAY
from main.models import UserProfile, RoleChangeLogs
from access_controller.settings import ZENDESK_ROLES as ROLES
# from access_controller.main.models import RoleChangeLogs
class ZendeskAdmin:
"""
@ -77,7 +75,7 @@ class ZendeskAdmin:
user = self.admin.users.search(email).values[0]
return user.photo['content_url'] if user.photo else None
def get_user(self, email: str) -> str:
def get_user(self, email: str):
"""
Функция **get_user** возвращает пользователя (объект) по его email
@ -91,7 +89,7 @@ class ZendeskAdmin:
Функция **get_user_org** возвращает организацию, к которой относится пользователь по его email
"""
user = self.admin.users.search(email).values[0]
return user.organization.name
return user.organization.name if user.organization else None
def create_admin(self) -> Zenpy:
"""
@ -105,7 +103,7 @@ class ZendeskAdmin:
if self.email is None:
raise ValueError('access_controller email not in env')
self.credentials['email'] = os.getenv('ACCESS_CONTROLLER_API_EMAIL')
self.credentials['email'] = self.email
if self.token:
self.credentials['token'] = self.token
@ -161,6 +159,7 @@ def update_profile(user_profile: UserProfile) -> UserProfile:
user = ZendeskAdmin().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()
@ -179,23 +178,212 @@ def get_user_organization(email: str) -> str:
return ZendeskAdmin().get_user_org(email)
def check_user_auth(email: str, password: str) -> bool:
def daterange(start_date, end_date) -> list:
"""
Функция проверяет, верны ли входные данные
Возвращает список дней с start_date по end_date исключая правую границу
"""
dates = []
for n in range(int((end_date - start_date).days)):
dates.append(start_date + timedelta(n))
return dates
:raise: :class:`APIException`: исключение, вызываемое если пользователь не аутентифицирован
def get_timedelta(log, time=None) -> timedelta:
"""
creds = {
'email': email,
'password': password,
'subdomain': 'ngenix1612197338',
}
try:
user = Zenpy(**creds)
user.search(email, type='user')
except APIException:
return False
return True
Возвращает объект класса timedelta, который хранит промежуток времени от начала суток до момента,
который находится в log (объект класса RoleChangeLogs) или в time(datetime.time), если введён
"""
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):
"""
Возвращает последний день любого месяца
"""
next_month = day.replace(day=28) + timedelta(days=4)
return next_month - timedelta(days=next_month.day)
class StatisticData:
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._set_data()
if stat is None:
self._set_statistic()
else:
self.statistic = stat
def get_statistic(self):
"""
Вернуть словарь 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):
"""
Были ли ошибки при создании статистики
"""
return not self.errors and self.statistic
def set_interval(self, interval):
"""
Устанавливает интервал работы
"""
if interval not in ['months', 'days']:
self.errors += ['Интервал работы должен быть в днях или месяцах']
return False
self.interval = interval
return True
def set_display(self, display_format):
"""
Устанавливает формат отображения
"""
if display_format not in ['days', 'hours']:
self.errors += ['Формат отображения должен быть в часах или днях']
return False
self.display = display_format
return True
def get_data(self):
"""
Вернуть данные
data - массив объектов RoleChangeLogs, является списком логов пользователя
data может быть пустым списком
"""
if self.is_valid_data():
return self.data
else:
return None
def is_valid_data(self):
"""
Были ли ошибки при получении логов
"""
return not self.errors
def _use_display(self, stat):
"""
Приводит данные к формату отображения
"""
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):
"""
Объединяет ключи и значения в соответствии с интервалом работы
"""
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):
"""
Проверка на правильность введенного времени
"""
if self.end_date < self.start_date or self.end_date > datetime.now().date():
return False
return True
def _set_data(self):
"""
Получение логов в диапазоне дат start_date-end_date для пользователя с почтой email
"""
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 _set_statistic(self):
"""
Функция заполняет словарь, в котором ключ - дата, значение - кол-во проработанных в этот день секунд
"""
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.fill_daterange(self.start_date, first_log.change_time.date())
self.statistic[first_log.change_time.date()] += get_timedelta(first_log).total_seconds()
if last_log.new_role == ROLES['engineer']:
self.fill_daterange(last_log.change_time.date() + timedelta(days=1), self.end_date + timedelta(days=1))
self.statistic[last_log.change_time.date()] += (timedelta(days=1) - get_timedelta(last_log)).total_seconds()
if self.end_date == datetime.now().date():
self.statistic[self.end_date] = get_timedelta(None, datetime.now().time()).total_seconds()
for log_index in range(len(self.data) - 1):
if self.data[log_index].new_role == ROLES['engineer']:
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 fill_daterange(self, first, last, val=24 * 3600):
"""
Заполение диапазона дат значением val
по умолчанию val = кол-во секунд в 1 дне
"""
for day in daterange(first, last):
self.statistic[day] = val
def clear_statistic(self):
"""
Обнуление всех дней
"""
self.statistic.clear()
self.fill_daterange(self.start_date, self.end_date + timedelta(days=1), 0)
class DatabaseHandler(logging.Handler):

View File

@ -2,6 +2,7 @@ from django import forms
from django.contrib.auth.forms import AuthenticationForm
from django_registration.forms import RegistrationFormUniqueEmail
from access_controller.settings import ZENDESK_ROLES
from main.models import UserProfile
@ -14,7 +15,6 @@ class CustomRegistrationForm(RegistrationFormUniqueEmail):
:param visible_fields.email: Поле для ввода email, зарегистирированного на Zendesk
:type visible_fields.email: :class:`django_registration.forms.RegistrationFormUniqueEmail`
"""
def __init__(self, *args, **kwargs) -> RegistrationFormUniqueEmail:
super().__init__(*args, **kwargs)
for visible in self.visible_fields():
@ -23,7 +23,7 @@ class CustomRegistrationForm(RegistrationFormUniqueEmail):
visible.field.widget.attrs['class'] += 'form-control'
else:
visible.field.widget.attrs['class'] = 'form-control'
if visible.html_name !='email':
if visible.html_name != 'email':
visible.field.required = False
class Meta(RegistrationFormUniqueEmail.Meta):
@ -42,7 +42,8 @@ class AdminPageUsers(forms.Form):
queryset=UserProfile.objects.filter(role='agent'),
widget=forms.CheckboxSelectMultiple(
attrs={
'class': 'form-check-input'
'class': 'form-check-input',
}
),
label=''
@ -65,3 +66,31 @@ class CustomAuthenticationForm(AuthenticationForm):
,
'inactive': "Аккаунт не активен.",
}
class StatisticForm(forms.Form):
email = forms.EmailField(
label='Электроная почта',
)
interval = forms.CharField( # TODO: Переделать под html страницу
label='Интервал работы',
)
display_format = forms.CharField( # TODO: Переделать под html страницу
label='Формат отображения',
)
range_start = forms.DateField( # TODO: Переделать под html страницу
label='Начало диапазона',
widget=forms.DateInput(
attrs={
'type': 'date',
}
),
)
range_end = forms.DateField( # TODO: Переделать под html страницу
label='Конец диапазона',
widget=forms.DateInput(
attrs={
'type': 'date',
}
),
)

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,23 @@
# Generated by Django 3.1.6 on 2021-03-12 09:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0011_auto_20210311_1734'),
]
operations = [
migrations.AddField(
model_name='userprofile',
name='custom_role_id',
field=models.IntegerField(default=0, help_text='Код роли пользователя'),
),
migrations.AlterField(
model_name='userprofile',
name='role',
field=models.CharField(default='None', help_text='Глобальное имя роли пользователя', max_length=100),
),
]

View File

@ -8,8 +8,14 @@ from django.utils import timezone
class UserProfile(models.Model):
"""Модель профиля пользователя"""
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='Код роли пользователя')
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='Имя пользователя на нашем сайте')
@ -27,8 +33,11 @@ def save_user_profile(sender, instance, **kwargs):
class RoleChangeLogs(models.Model):
"""Модель для логирования изменений ролей пользователя"""
user = models.ForeignKey(to=User, on_delete=models.CASCADE, help_text='Пользователь, которому присвоили другую роль')
user = models.ForeignKey(to=User, on_delete=models.CASCADE,
help_text='Пользователь, которому присвоили другую роль')
name = models.TextField(help_text='Имя пользователя')
new_role = models.TextField(help_text='Присвоенная роль')
old_role = models.IntegerField(default=0, help_text='Старая роль')
new_role = models.IntegerField(default=0, help_text='Присвоенная роль')
change_time = models.DateTimeField(help_text='Дата и время изменения роли', default=timezone.now)
changed_by = models.ForeignKey(to=User, on_delete=models.CASCADE, related_name='changed_by', help_text='Кем была изменена роль')
changed_by = models.ForeignKey(to=User, on_delete=models.CASCADE, related_name='changed_by',
help_text='Кем была изменена роль')

View File

@ -3,10 +3,10 @@
<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" 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">

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,49 @@
{% extends 'base/base.html' %}
{% load static %}
{% block title %}{{ pagename }}{% endblock %}
{% block heading %} Пример страницы статистики(палками не бейти плиз){% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'main/css/work.css' %}">
{% endblock %}
{% block content %}
<div>
<form action="" method="post">
{% csrf_token %}
{% for field in form %}
{{field.label}}
{{field}}
<br>
{% endfor %}
<input type="submit">
</form>
<ul>
{% for error in errors %}
<li><span class="badge bg-danger">{{error}}</span></li>
{% endfor %}
</ul>
{%if form.errors%}
<ul>
{% for field, errors in form.errors.items %}
{% for error in errors %}
<li><span class="badge bg-danger">{{error}}</span></li>
{% endfor %}
{% endfor %}
</ul>
{%endif%}
<ul>
{% for warning in warnings %}
<li><span class="badge bg-warning">{{warning}}</span></li>
{% endfor %}
</ul>
{% for key,val in log_stats.items %}
<h3>{{key}} <b>|</b> {{val}}</h3>
<br>
{% endfor %}
</div>
{% endblock %}

View File

@ -14,7 +14,7 @@ from zenpy import Zenpy
from access_controller.settings import EMAIL_HOST_USER
from main.extra_func import check_user_exist, update_profile, get_user_organization, \
make_engineer, make_light_agent, get_users_list
make_engineer, make_light_agent, get_users_list, log
from django.contrib.auth.models import User, Permission
from main.models import UserProfile
@ -27,7 +27,6 @@ from django.core.exceptions import PermissionDenied
from access_controller.settings import ZENDESK_ROLES
from zenpy.lib.api_objects import User as ZenpyUser
content_type_temp = ContentType.objects.get_for_model(UserProfile)
permission_temp, created = Permission.objects.get_or_create(
codename='has_control_access',
@ -112,6 +111,7 @@ def profile_page(request: WSGIRequest) -> HttpResponse:
'profile': user_profile,
'pagename': 'Страница профиля'
}
log(request.user)
return render(request, 'pages/profile.html', context)
@ -151,12 +151,12 @@ def work_page(request, id):
@login_required()
def work_hand_over(request):
zenpy_user, admin = auth_user(request)
if zenpy_user.custom_role_id == ZENDESK_ROLES['engineer']:
zenpy_user.custom_role_id = ZENDESK_ROLES['light_agent']
admin.users.update(zenpy_user)
request.user.userprofile.role = "agent"
request.user.userprofile.save()
# log(request.user.userprofile.user)
return HttpResponseRedirect(reverse('work', args=(request.user.id,)))
@ -168,6 +168,7 @@ def work_become_engineer(request):
admin.users.update(zenpy_user)
request.user.userprofile.role = "agent"
request.user.userprofile.save()
# log(request.user.userprofile.user)
return HttpResponseRedirect(reverse('work', args=(request.user.id,)))

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB