Merge branch 'develop' into feature/docker

This commit is contained in:
Artyom Kravchenko 2021-04-08 15:45:43 +03:00
commit 0e6f016d1e
15 changed files with 304 additions and 154 deletions

View File

@ -47,9 +47,79 @@ pip install -r requirements.txt
./manage.py loaddata data.json
./manage.py runserver
```
Создать токен
Указать почту и токен в окружении
##ZenDesk Access Controller instruction for eng
##Перед запуском для тестирования:
Убедитесь, что вы зарегистрированы в песочнице ZenDesk, у вас назначена организация (SYSTEM)
Для админов ZenDesk дополнительно - создайте токен доступа в ZenDesk
При запуске в Docker убедитесь что папка, которая будет служить хранилищем для БД, открыта на запись и чтение
##Запуск на локальной машине:
скопировать репозиторий на локальную машину
перейти в папку приложения
активировать вирутальное окружение
выполнить команду pip install -r requirements.txt
в вирутальное окружение добавить следующие переменные :
ACCESS_CONTROLLER_API_EMAIL={EMAIL} - почта админа в ZenDesk
ACCESS_CONTROLLER_API_PASSWORD={PASSWORD} - пароль админа ZenDesk
ACCESS_CONTROLLER_API_TOKEN={API_TOKEN} - API токен зендеск
ZD_DOMAIN={DOMAIN} - домен ZenDesk
ENG_CROLE_ID={ENGINEER_CUSTOM_ROLE_ID} - id роли инженера( custom_role_id сотрдника смены)
LA_CROLE_ID={LIGHT_AGENT_CUSTOM_ROLE_ID} - id роли легкого агента (custom_role_id роли -легкий агент)
EMPL_GROUP={EMPLOYEE_GROUP_NAME} - имя группы которой принадлежат сотрудники ССКС
BUF_GROUP={BUFFER_GROUP_NAME} - имя буферной группы для передачи смен(через нее происходит управление тикетами)
ST_EMAIL={SOLVED_TICKETS_EMAIL} - почта на которую будут переназначятся закрытые тикеты
LICENSE_NO={LICENSE_NO} - количество лицензий, отображаемых как доступные в приложении
SHIFTH={SHIFT_HOURS} - количество часов в рабочей смене (нужно для статистики, пока не реализовано но требует указания значения)
выполнить команду python manage.py makemigrations
выполнить команду python manage.py migrate
запустить приложение командой python manage.py runserver (можно указать в параметрах для файла manage.py)
перейти по ссылке в консоли (вероятнее всего откроется по адресу http://127.0.0.1:8000/)
##Запуск в Docker:
Требуется установленный и настроеный Docker
скопировать репозиторий на локальную машину
в командной строке перейти в папку проекта
выполнить команду docker build .
выполнить команду docker images (нам нужен id созданного образа)
выполнить команду docker run -d -p 8000:8000 -e ACCESS_CONTROLLER_API_EMAIL={EMAIL} -e ACCESS_CONTROLLER_API_PASSWORD={PASSWORD}
...(перечисляем все параметры виртуального окружени разделяя их -e) -v {абсолютный путь к папке, в которой будет размещена база}:/zendesk-access-controller/db {id образа докера}
открываем запущеный контейнер в браузере (можно перейти по ссылке http://localhost:8000/)
##Запуск с тестовыми юзерами:
На локальной машине - перед запуском команды python manage.py runserver выполнить команду python manage.py loaddata data.json
Это создаст тестового админа и тестового пользователя в приложении для песочницы ZenDesk. Админ - admin@gmail.com / zendeskadmin , пользователь - 123@test.ru / zendeskuser .
Не сработает если домен песочницы отличается от ngenix1612197338 (на другом домене нужно будет создать сначала пользователей в песочнице с правами админа и легкого агента
с этими же почтами, назначить им организацию (SYSTEM))
##Параметры тестовой песочницы:
ACCESS_CONTROLLER_API_EMAIL={EMAIL} - почта админа в ZenDesk - взять у роководителя(если вы не админ)
ACCESS_CONTROLLER_API_PASSWORD={PASSWORD} - пароль админа ZenDesk - взять у роководителя(если вы не админ)
ACCESS_CONTROLLER_API_TOKEN={API_TOKEN} - API токен зендеск - взять у роководителя(если вы не админ)
ZD_DOMAIN=ngenix1612197338
ENG_CROLE_ID=360005209000
LA_CROLE_ID=360005208980
EMPL_GROUP=Поддержка
BUF_GROUP=Сменная группа
ST_EMAIL=d.krikov@ngenix.net
LICENSE_NO=3
SHIFTH=12
## Read more
- Zenpy: [http://docs.facetoe.com.au](http://docs.facetoe.com.au)

View File

@ -9,8 +9,9 @@ https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'access_controller.settings')
from django.core.asgi import get_asgi_application
application = get_asgi_application()

View File

@ -24,7 +24,7 @@ SECRET_KEY = 'v1i_fb$_jf2#1v_lcsbu&eon4u-os0^px=s^iycegdycqy&5)6'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
ALLOWED_HOSTS = ['127.0.0.1']
# Application definition

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

View File

@ -381,6 +381,7 @@ class StatisticData:
:param statistic: Интервалы работы пользователя
:type statistic: :class:`dict`
"""
def __init__(self, start_date, end_date, user_email, stat=None):
self.display = None
self.interval = None
@ -543,31 +544,51 @@ class StatisticData:
first_log, last_log = self.data[0], self.data[len(self.data) - 1]
if first_log.old_role == ROLES['engineer']:
self.statistic[first_log.change_time.date()] += get_timedelta(first_log).total_seconds()
self.prev_engineer_logic(first_log)
if last_log.new_role == ROLES['engineer']: # TODO отдельная функция
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()
if last_log.new_role == ROLES['engineer']:
self.post_engineer_logic(last_log)
for log_index in range(len(self.data) - 1): # TODO отдельная функция
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()
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:
"""
@ -576,7 +597,6 @@ class StatisticData:
:param first: Начальная дата интервала
:param last: Последняя дата интервала
:param val: Количество секунд в одном дне
:return: Статистику пользователя с указанным количеством секунд в заданных днях
"""
for day in daterange(first, last):
self.statistic[day] = val
@ -584,8 +604,6 @@ class StatisticData:
def clear_statistic(self) -> dict:
"""
Функция осуществляет обновление всех дней.
:return: Статистику пользователя с количеством рабочих секунд = 0
"""
self.statistic.clear()
self.fill_daterange(self.start_date, self.end_date + timedelta(days=1), 0)

View File

@ -2,7 +2,6 @@ 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,6 +13,7 @@ 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():
@ -70,6 +70,16 @@ class CustomAuthenticationForm(AuthenticationForm):
}
INTERVAL_CHOICES = [
('days', 'Дни'),
('months', 'Месяцы')
]
DISPLAY_CHOICES = [
('hours', 'Часы'),
('days', 'Дни/Смены')
]
class StatisticForm(forms.Form):
"""
Форма отображения интервалов работы пользователя.
@ -87,26 +97,47 @@ 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(
widget=forms.EmailInput(
attrs={
'type': 'date',
'placeholder': 'example@ngenix.ru',
'class': 'form-control',
'style': 'background-color:#f2f2f2;'
}
),
)
range_end = forms.DateField( # TODO: Переделать под html страницу
label='Конец диапазона',
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

@ -4,6 +4,8 @@ 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):
"""
@ -23,6 +25,14 @@ class UserProfile(models.Model):
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)
def create_user_profile(sender, instance, created, **kwargs):

View File

@ -13,11 +13,9 @@ class UserSerializer(serializers.HyperlinkedModelSerializer):
class ProfileSerializer(serializers.HyperlinkedModelSerializer):
"""
Класс serializer для модель профиля пользователя.
"""
"""Сериализатор для модели профиля пользователя"""
user = UserSerializer()
class Meta:
model = UserProfile
fields = ['user', 'id', 'role', 'name']
fields = ['user', 'id', 'name', 'zendesk_role']

View File

@ -30,14 +30,6 @@
<div class="row justify-content-center new-section">
{% block hidden_form %}
<div style="display: none">
{% for field in form.users %}
{{ field.tag }}
{% endfor %}
</div>
{% endblock %}
<div class="col-10">
<h6 class="table-title">Список сотрудников</h6>
@ -50,25 +42,11 @@
<th>Role</th>
<th>Checked</th>
</thead>
<tbody id="old_tbody">
{% for user in users %}
<tr>
<td><a href="#">{{ user.name }}</a></td>
<td>{{ user.user.email }}</td>
<td>{% if user.custom_role_id == ZENDESK_ROLES.engineer %}
engineer
{% elif user.custom_role_id == ZENDESK_ROLES.light_agent %}
light_agent
{% endif %}
</td>
<td class="checkbox_field"></td>
</tr>
{% endfor %}
<tbody id="tbody">
</tbody>
<tbody id="new_tbody"></tbody>
</table>
{% endblock%}
<p id="loading">Данные загружаются...</p>
{% endblock %}
</div>
</div>
@ -96,7 +74,9 @@
</div>
</div>
{% endblock %}
{% block buttons %}
<div class="col-5">
<button type="submit" name="engineer" class="request-acess-button default-button">
@ -107,8 +87,9 @@
Назначить выбранных на роль легкого агента
</button>
</div>
{% endblock %}
</div>
{% endblock %}
</form>
{% endblock %}

View File

@ -1,49 +0,0 @@
{% 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

@ -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-secondary text-primary bg-white" 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-secondary text-primary bg-white" 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-primary bg-white text-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">Пользователи/Даты</td>
{% for date in log_stats.keys %}
<td scope="col">{{date}}</td>
{% endfor %}
</tr>
</thead>
<tbody>
<tr>
<td>{{ form.email.value }}</td>
{% for time in log_stats.values %}
<td>{{time}}</td>
{% endfor %}
</tr>
</tbody>
</table>
</div>
</div>
{% endblock %}

View File

@ -285,13 +285,9 @@ class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin,SuccessMessageMi
Функция формирования контента страницы администратора (с проверкой прав доступа)
"""
context = super().get_context_data(**kwargs)
users = get_list_or_404(
UserProfile, role='agent')
context['users'] = users
context['ZENDESK_ROLES'] = ZENDESK_ROLES
context['engineers'], context['light_agents'] = count_users(get_users_list())
context['licences_remaining'] = max(0, ZENDESK_MAX_AGENTS - context['engineers'])
return context # TODO: need to get profile page url
return context
class CustomLoginView(LoginView):
@ -329,8 +325,8 @@ def statistic_page(request: WSGIRequest) -> HttpResponse:
:param request: данные о пользователе: email, время и интервал работы. Данные получаем через forms.StatisticForm
:return: адресация на страницу статистики
"""
if not request.user.is_superuser:
return redirect('index')
if not request.user.has_perm('main.has_control_access'):
return PermissionDenied
context = {
'pagename': 'страница статистики',
'errors': list(),
@ -352,4 +348,4 @@ def statistic_page(request: WSGIRequest) -> HttpResponse:
if request.method == 'GET':
form = StatisticForm()
context['form'] = form
return render(request, 'pages/stat.html', context)
return render(request, 'pages/statistic.html', context)

View File

@ -4,6 +4,7 @@ Pillow==8.1.0
zenpy~=2.0.24
django_registration==3.1.1
djangorestframework==3.12.2
daphne==3.0.1
# Documentation

View File

@ -13,8 +13,7 @@
background: #45729C;
} */
.form-check-input {
border-radius: 0px;
background-image: url("../img/check.png");
border-radius: 0;
width: 30px;
height: 30px;
background-size: 20px auto;
@ -125,4 +124,4 @@
padding: 10px;
background: #3B91D4;
color: white;
}
}

View File

@ -1,21 +1,5 @@
"use strict";
function move_checkboxes() {
let checkboxes = document.getElementsByName("users");
let fields = document.querySelectorAll(".checkbox_field");
if (checkboxes.length == fields.length) {
for (let i = 0; i < fields.length; ++i) {
let el = checkboxes[i].cloneNode(true);
fields[i].appendChild(el);
}
} else {
alert(
"Количество пользователей агентов не соответствует количеству полей в форме AdminPageUsers"
);
}
}
move_checkboxes();
// React
class TableRow extends React.Component {
render() {
@ -25,7 +9,7 @@ class TableRow extends React.Component {
<a href="#">{this.props.user.name}</a>
</td>
<td>{this.props.user.user.email}</td>
<td>{this.props.user.role}</td>
<td>{this.props.user.zendesk_role}</td>
<td>
<input
type="checkbox"
@ -49,8 +33,8 @@ class TableBody extends React.Component {
};
}
get_users() {
axios.get("/api/users").then((response) => {
async get_users() {
await axios.get("/api/users").then((response) => {
this.setState({
users: response.data.users,
engineers: response.data.engineers,
@ -62,7 +46,12 @@ class TableBody extends React.Component {
});
}
delete_pretext() {
document.getElementById("loading").remove();
}
componentDidMount() {
this.get_users().then(() => this.delete_pretext());
this.interval = setInterval(() => {
this.get_users();
}, 60000);
@ -79,5 +68,4 @@ class TableBody extends React.Component {
}
}
ReactDOM.render(<TableBody />, document.getElementById("new_tbody"));
setTimeout(() => document.getElementById("old_tbody").remove(), 60000);
ReactDOM.render(<TableBody />, document.getElementById("tbody"));