This commit is contained in:
Artyom Kravchenko 2021-04-06 18:04:22 +03:00
commit 2ea31e1594
8 changed files with 213 additions and 142 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

View File

@ -381,6 +381,7 @@ class StatisticData:
:param statistic: Интервалы работы пользователя :param statistic: Интервалы работы пользователя
:type statistic: :class:`dict` :type statistic: :class:`dict`
""" """
def __init__(self, start_date, end_date, user_email, stat=None): def __init__(self, start_date, end_date, user_email, stat=None):
self.display = None self.display = None
self.interval = None self.interval = None
@ -543,31 +544,51 @@ class StatisticData:
first_log, last_log = self.data[0], self.data[len(self.data) - 1] first_log, last_log = self.data[0], self.data[len(self.data) - 1]
if first_log.old_role == ROLES['engineer']: if first_log.old_role == ROLES['engineer']:
self.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 отдельная функция 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.post_engineer_logic(last_log)
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()
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']: if self.data[log_index].new_role == ROLES['engineer']:
current_log, next_log = self.data[log_index], self.data[log_index + 1] self.engineer_logic(log_index)
if current_log.change_time.date() != next_log.change_time.date():
self.statistic[current_log.change_time.date()] += ( def engineer_logic(self, log_index):
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: current_log, next_log = self.data[log_index], self.data[log_index + 1]
elapsed_time = next_log.change_time - current_log.change_time if current_log.change_time.date() != next_log.change_time.date():
self.statistic[current_log.change_time.date()] += elapsed_time.total_seconds() 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: def fill_daterange(self, first: date, last: date, val: int = 24 * 3600) -> dict:
""" """
@ -576,7 +597,6 @@ class StatisticData:
:param first: Начальная дата интервала :param first: Начальная дата интервала
:param last: Последняя дата интервала :param last: Последняя дата интервала
:param val: Количество секунд в одном дне :param val: Количество секунд в одном дне
:return: Статистику пользователя с указанным количеством секунд в заданных днях
""" """
for day in daterange(first, last): for day in daterange(first, last):
self.statistic[day] = val self.statistic[day] = val
@ -584,8 +604,6 @@ class StatisticData:
def clear_statistic(self) -> dict: def clear_statistic(self) -> dict:
""" """
Функция осуществляет обновление всех дней. Функция осуществляет обновление всех дней.
:return: Статистику пользователя с количеством рабочих секунд = 0
""" """
self.statistic.clear() self.statistic.clear()
self.fill_daterange(self.start_date, self.end_date + timedelta(days=1), 0) 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.contrib.auth.forms import AuthenticationForm
from django_registration.forms import RegistrationFormUniqueEmail from django_registration.forms import RegistrationFormUniqueEmail
from access_controller.settings import ZENDESK_ROLES
from main.models import UserProfile from main.models import UserProfile
@ -14,6 +13,7 @@ class CustomRegistrationForm(RegistrationFormUniqueEmail):
:param visible_fields.email: Поле для ввода email, зарегистрированного на Zendesk :param visible_fields.email: Поле для ввода email, зарегистрированного на Zendesk
:type visible_fields.email: :class:`django_registration.forms.RegistrationFormUniqueEmail` :type visible_fields.email: :class:`django_registration.forms.RegistrationFormUniqueEmail`
""" """
def __init__(self, *args, **kwargs) -> RegistrationFormUniqueEmail: def __init__(self, *args, **kwargs) -> RegistrationFormUniqueEmail:
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
for visible in self.visible_fields(): 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): class StatisticForm(forms.Form):
""" """
Форма отображения интервалов работы пользователя. Форма отображения интервалов работы пользователя.
@ -87,26 +97,47 @@ class StatisticForm(forms.Form):
""" """
email = forms.EmailField( email = forms.EmailField(
label='Электроная почта', label='Электроная почта',
) widget=forms.EmailInput(
interval = forms.CharField( # TODO: Переделать под html страницу
label='Интервал работы',
)
display_format = forms.CharField( # TODO: Переделать под html страницу
label='Формат отображения',
)
range_start = forms.DateField( # TODO: Переделать под html страницу
label='Начало диапазона',
widget=forms.DateInput(
attrs={ attrs={
'type': 'date', 'placeholder': 'example@ngenix.ru',
'class': 'form-control',
'style': 'background-color:#f2f2f2;'
} }
), ),
) )
range_end = forms.DateField( # TODO: Переделать под html страницу interval = forms.ChoiceField(
label='Конец диапазона', 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( widget=forms.DateInput(
attrs={ attrs={
'type': 'date', '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

@ -30,14 +30,6 @@
<div class="row justify-content-center new-section"> <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"> <div class="col-10">
<h6 class="table-title">Список сотрудников</h6> <h6 class="table-title">Список сотрудников</h6>
@ -50,25 +42,11 @@
<th>Role</th> <th>Role</th>
<th>Checked</th> <th>Checked</th>
</thead> </thead>
<tbody id="tbody">
<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> </tbody>
<tbody id="new_tbody"></tbody>
</table> </table>
{% endblock%} <p id="loading">Данные загружаются...</p>
{% endblock %}
</div> </div>
</div> </div>
@ -96,7 +74,9 @@
</div> </div>
</div> </div>
{% endblock %}
{% block buttons %}
<div class="col-5"> <div class="col-5">
<button type="submit" name="engineer" class="request-acess-button default-button"> <button type="submit" name="engineer" class="request-acess-button default-button">
@ -107,8 +87,9 @@
Назначить выбранных на роль легкого агента Назначить выбранных на роль легкого агента
</button> </button>
</div> </div>
{% endblock %}
</div> </div>
{% endblock %}
</form> </form>
{% endblock %} {% 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) 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['engineers'], context['light_agents'] = count_users(get_users_list())
context['licences_remaining'] = max(0, ZENDESK_MAX_AGENTS - context['engineers']) context['licences_remaining'] = max(0, ZENDESK_MAX_AGENTS - context['engineers'])
return context # TODO: need to get profile page url return context
class CustomLoginView(LoginView): class CustomLoginView(LoginView):
@ -329,8 +325,8 @@ def statistic_page(request: WSGIRequest) -> HttpResponse:
:param request: данные о пользователе: email, время и интервал работы. Данные получаем через forms.StatisticForm :param request: данные о пользователе: email, время и интервал работы. Данные получаем через forms.StatisticForm
:return: адресация на страницу статистики :return: адресация на страницу статистики
""" """
if not request.user.is_superuser: if not request.user.has_perm('main.has_control_access'):
return redirect('index') return PermissionDenied
context = { context = {
'pagename': 'страница статистики', 'pagename': 'страница статистики',
'errors': list(), 'errors': list(),
@ -352,4 +348,4 @@ def statistic_page(request: WSGIRequest) -> HttpResponse:
if request.method == 'GET': if request.method == 'GET':
form = StatisticForm() form = StatisticForm()
context['form'] = form context['form'] = form
return render(request, 'pages/stat.html', context) return render(request, 'pages/statistic.html', context)

View File

@ -1,21 +1,5 @@
"use strict"; "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 // React
class TableRow extends React.Component { class TableRow extends React.Component {
render() { render() {
@ -49,8 +33,8 @@ class TableBody extends React.Component {
}; };
} }
get_users() { async get_users() {
axios.get("/api/users").then((response) => { await axios.get("/api/users").then((response) => {
this.setState({ this.setState({
users: response.data.users, users: response.data.users,
engineers: response.data.engineers, engineers: response.data.engineers,
@ -62,7 +46,12 @@ class TableBody extends React.Component {
}); });
} }
delete_pretext() {
document.getElementById("loading").remove();
}
componentDidMount() { componentDidMount() {
this.get_users().then(() => this.delete_pretext());
this.interval = setInterval(() => { this.interval = setInterval(() => {
this.get_users(); this.get_users();
}, 60000); }, 60000);
@ -79,5 +68,4 @@ class TableBody extends React.Component {
} }
} }
ReactDOM.render(<TableBody />, document.getElementById("new_tbody")); ReactDOM.render(<TableBody />, document.getElementById("tbody"));
setTimeout(() => document.getElementById("old_tbody").remove(), 60000);