diff --git a/README.md b/README.md index 47468bb..14e3869 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/access_controller/asgi.py b/access_controller/asgi.py index 7f60a1a..11dc22e 100644 --- a/access_controller/asgi.py +++ b/access_controller/asgi.py @@ -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() diff --git a/access_controller/settings.py b/access_controller/settings.py index a1caf3b..7a54626 100644 --- a/access_controller/settings.py +++ b/access_controller/settings.py @@ -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 diff --git a/layouts/statistic/statistic.png b/layouts/statistic/statistic.png new file mode 100644 index 0000000..931b7e1 Binary files /dev/null and b/layouts/statistic/statistic.png differ diff --git a/main/extra_func.py b/main/extra_func.py index 077615f..8b75346 100644 --- a/main/extra_func.py +++ b/main/extra_func.py @@ -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) diff --git a/main/forms.py b/main/forms.py index 2ad67cb..613fc34 100644 --- a/main/forms.py +++ b/main/forms.py @@ -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', } ), ) diff --git a/main/models.py b/main/models.py index 2f61ebf..1920c4d 100644 --- a/main/models.py +++ b/main/models.py @@ -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): diff --git a/main/serializers.py b/main/serializers.py index 5a4fd1a..cfc70a8 100644 --- a/main/serializers.py +++ b/main/serializers.py @@ -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'] diff --git a/main/templates/pages/adm_ruleset.html b/main/templates/pages/adm_ruleset.html index c2a354c..1fb9baf 100644 --- a/main/templates/pages/adm_ruleset.html +++ b/main/templates/pages/adm_ruleset.html @@ -30,14 +30,6 @@
- {% block hidden_form %} -
- {% for field in form.users %} - {{ field.tag }} - {% endfor %} -
- {% endblock %} -
Список сотрудников
@@ -50,25 +42,11 @@ Role Checked - - - {% for user in users %} - - {{ user.name }} - {{ user.user.email }} - {% if user.custom_role_id == ZENDESK_ROLES.engineer %} - engineer - {% elif user.custom_role_id == ZENDESK_ROLES.light_agent %} - light_agent - {% endif %} - - - - {% endfor %} + - - {% endblock%} +

Данные загружаются...

+ {% endblock %}
@@ -96,7 +74,9 @@ + {% endblock %} + {% block buttons %}
+ {% endblock %} + - {% endblock %} {% endblock %} diff --git a/main/templates/pages/stat.html b/main/templates/pages/stat.html deleted file mode 100644 index 9279123..0000000 --- a/main/templates/pages/stat.html +++ /dev/null @@ -1,49 +0,0 @@ -{% extends 'base/base.html' %} - -{% load static %} - -{% block title %}{{ pagename }}{% endblock %} - -{% block heading %} Пример страницы статистики(палками не бейти плиз){% endblock %} - -{% block extra_css %} - -{% endblock %} - -{% block content %} -
-
- {% csrf_token %} - {% for field in form %} - {{field.label}} - {{field}} -
- {% endfor %} - -
- - {%if form.errors%} - - {%endif%} - - {% for key,val in log_stats.items %} -

{{key}} | {{val}}

-
- {% endfor %} -
-{% endblock %} - diff --git a/main/templates/pages/statistic.html b/main/templates/pages/statistic.html new file mode 100644 index 0000000..9a9219b --- /dev/null +++ b/main/templates/pages/statistic.html @@ -0,0 +1,106 @@ +{% extends 'base/base.html' %} + +{% load static %} + +{% block title %}{{ pagename }}{% endblock %} + +{% block heading %} Страницы просмотра статистики{% endblock %} + +{% block content%} +
+
+
+ {% csrf_token %} +
+
+ {{ form.email.label }} +
+
+ {{ form.email }} +
+
+
+
+ {{ form.interval.label }} +
+
+ {% for radio in form.interval%} + {{ radio.tag }} + + {% endfor %} +
+
+
+
+ {{ form.display_format.label }} +
+
+ {% for radio in form.display_format%} + {{ radio.tag }} + + {% endfor %} +
+
+
+
+ {{ form.range_start.label}} +
+
+
+ {{ form.range_start}} +
+
+
+
+
+ {{ form.range_end.label}} +
+
+
+ {{ form.range_end}} +
+
+
+
+
+ +
+
+
+
+ + +
+ + + + + {% for date in log_stats.keys %} + + {% endfor %} + + + + + + {% for time in log_stats.values %} + + {% endfor %} + + +
Пользователи/Даты{{date}}
{{ form.email.value }}{{time}}
+
+
+{% endblock %} diff --git a/main/views.py b/main/views.py index 18d7e52..0a1a070 100644 --- a/main/views.py +++ b/main/views.py @@ -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) diff --git a/requirements.txt b/requirements.txt index b32a382..16ae562 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/static/main/css/work.css b/static/main/css/work.css index 790ac6d..d20922d 100644 --- a/static/main/css/work.css +++ b/static/main/css/work.css @@ -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; -} \ No newline at end of file +} diff --git a/static/main/js/control.js b/static/main/js/control.js index e518c36..55d1d1a 100644 --- a/static/main/js/control.js +++ b/static/main/js/control.js @@ -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 { {this.props.user.name} {this.props.user.user.email} - {this.props.user.role} + {this.props.user.zendesk_role} { + 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(, document.getElementById("new_tbody")); -setTimeout(() => document.getElementById("old_tbody").remove(), 60000); +ReactDOM.render(, document.getElementById("tbody"));