diff --git a/.gitignore b/.gitignore index d798070..1f6ea85 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ local_settings.py db.sqlite3 db.sqlite3-journal media/ +logs/ # If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/ # in your Git repository. Update and uncomment the following line accordingly. 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 020480d..ddcd372 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 @@ -53,10 +53,10 @@ MIDDLEWARE = [ ROOT_URLCONF = 'access_controller.urls' EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' -EMAIL_HOST = 'smtp.mail.ru' -EMAIL_PORT = 2525 +EMAIL_HOST = 'smtp.gmail.com' +EMAIL_PORT = 587 EMAIL_USE_TLS = True -EMAIL_HOST_USER = 'djgr.02@mail.ru' +EMAIL_HOST_USER = 'group02django@gmail.com' EMAIL_HOST_PASSWORD = 'djangogroup02' SERVER_EMAIL = EMAIL_HOST_USER DEFAULT_FROM_EMAIL = EMAIL_HOST_USER @@ -143,41 +143,7 @@ AUTHENTICATION_BACKENDS = [ # Logging system # https://docs.djangoproject.com/en/3.1/topics/logging/ -LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'formatters': { - 'verbose': { - 'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}', - 'style': '{', - }, - 'simple': { - 'format': '{levelname} {message}', - 'style': '{', - }, - }, - 'handlers': { - 'console': { - 'level': 'INFO', - 'class': 'logging.StreamHandler', - 'formatter': 'simple' - }, - 'mail_admins': { - 'level': 'ERROR', - 'class': 'django.utils.log.AdminEmailHandler', - } - }, - 'loggers': { - 'django': { - 'handlers': ['console'], - 'propagate': True, - }, - 'main.index': { - 'handlers': ['console'], - 'level': 'INFO', - } - } -} + ZENDESK_ROLES = { 'engineer': 360005209000, diff --git a/access_controller/urls.py b/access_controller/urls.py index e174717..57d1d01 100644 --- a/access_controller/urls.py +++ b/access_controller/urls.py @@ -17,29 +17,31 @@ from django.contrib import admin from django.contrib.auth import views as auth_views from django.urls import path, include -from main.views import main_page, profile_page, CustomRegistrationView, CustomLoginView -from main.views import work_page, work_hand_over, work_become_engineer, \ +from main.views import main_page, profile_page, CustomRegistrationView, CustomLoginView, registration_error +from main.views import work_page, work_hand_over, work_become_engineer, work_get_tickets, \ AdminPageView, statistic_page from main.urls import router - urlpatterns = [ path('admin/', admin.site.urls, name='admin'), path('', main_page, name='index'), path('accounts/profile/', profile_page, name='profile'), path('accounts/register/', CustomRegistrationView.as_view(), name='registration'), + path('accounts/register/error/', registration_error, name='registration_email_error'), path('accounts/login/', CustomLoginView.as_view(), name='login'), path('accounts/', include('django.contrib.auth.urls')), path('accounts/', include('django_registration.backends.one_step.urls')), path('work/', 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('work/get_tickets', work_get_tickets, name='work_get_tickets'), path('accounts/', include('django_registration.backends.activation.urls')), path('accounts/login/', include('django.contrib.auth.urls')), path('control/', AdminPageView.as_view(), name='control'), - path('statistic/', statistic_page, name='statistic') + path('statistic/', statistic_page, name='statistic'), ] + urlpatterns += [ path( 'password_reset/', @@ -47,7 +49,7 @@ urlpatterns += [ name='password_reset' ), path( - 'password-reset/done/', + 'password_reset/done/', auth_views.PasswordResetDoneView.as_view(), name='password_reset_done' ), diff --git a/data.json b/data.json index a4310a4..97678f3 100644 --- a/data.json +++ b/data.json @@ -1,7 +1,7 @@ [ { "model": "auth.user", - "pk": 1, + "pk": 3, "fields": { "password": "pbkdf2_sha256$216000$gHBBCr1jBELf$ZkEDW3IEd8Wij7u8vkv+0Eze32CS01bcaYWhcD9OIC4=", "last_login": null, @@ -19,16 +19,16 @@ }, { "model": "main.userprofile", - "pk": 1, + "pk": 3, "fields": { "name": "ZendeskAdmin", - "user": 1, + "user": 3, "role": "admin" } }, { "model": "auth.user", - "pk": 2, + "pk": 4, "fields": { "password": "pbkdf2_sha256$216000$5qLJgrm2Quq9$KDBNNymVZXkUx0HKBPFst2m83kLe0egPBnkW7KnkORU=", "last_login": null, @@ -46,10 +46,10 @@ }, { "model": "main.userprofile", - "pk": 2, + "pk": 4, "fields": { "name": "UserForAccessTest", - "user": 2, + "user": 4, "role": "agent", "custom_role_id": "360005209000" } diff --git a/docs/source/_static/admin_manage.png b/docs/source/_static/admin_manage.png new file mode 100644 index 0000000..38764b4 Binary files /dev/null and b/docs/source/_static/admin_manage.png differ diff --git a/docs/source/_static/admin_manage_done.png b/docs/source/_static/admin_manage_done.png new file mode 100644 index 0000000..6ae475b Binary files /dev/null and b/docs/source/_static/admin_manage_done.png differ diff --git a/docs/source/_static/login.png b/docs/source/_static/login.png index 2cf59cf..149ba55 100644 Binary files a/docs/source/_static/login.png and b/docs/source/_static/login.png differ diff --git a/docs/source/_static/main.png b/docs/source/_static/main.png index fcfd4f9..78afe48 100644 Binary files a/docs/source/_static/main.png and b/docs/source/_static/main.png differ diff --git a/docs/source/_static/main_logined.png b/docs/source/_static/main_logined.png deleted file mode 100644 index 5105101..0000000 Binary files a/docs/source/_static/main_logined.png and /dev/null differ diff --git a/docs/source/_static/main_logined_agent.png b/docs/source/_static/main_logined_agent.png deleted file mode 100644 index e4f76d1..0000000 Binary files a/docs/source/_static/main_logined_agent.png and /dev/null differ diff --git a/docs/source/_static/main_logout.png b/docs/source/_static/main_logout.png new file mode 100644 index 0000000..4dc98b7 Binary files /dev/null and b/docs/source/_static/main_logout.png differ diff --git a/docs/source/_static/permission_management.png b/docs/source/_static/permission_management.png deleted file mode 100644 index c1897ed..0000000 Binary files a/docs/source/_static/permission_management.png and /dev/null differ diff --git a/docs/source/_static/permission_request.png b/docs/source/_static/permission_request.png deleted file mode 100644 index 8752e91..0000000 Binary files a/docs/source/_static/permission_request.png and /dev/null differ diff --git a/docs/source/_static/profile.png b/docs/source/_static/profile.png index b84d139..c3ab5bf 100644 Binary files a/docs/source/_static/profile.png and b/docs/source/_static/profile.png differ diff --git a/docs/source/_static/request.png b/docs/source/_static/request.png new file mode 100644 index 0000000..bf80c3f Binary files /dev/null and b/docs/source/_static/request.png differ diff --git a/docs/source/_static/role_change.png b/docs/source/_static/role_change.png new file mode 100644 index 0000000..ff2d983 Binary files /dev/null and b/docs/source/_static/role_change.png differ diff --git a/docs/source/overview.rst b/docs/source/overview.rst index fb3c627..a7ca229 100644 --- a/docs/source/overview.rst +++ b/docs/source/overview.rst @@ -3,7 +3,7 @@ ========================= ****************************** -**Управление правами доступа** +Управление правами доступа ****************************** @@ -22,7 +22,7 @@ * **"Войти"** - если Вы уже являетесь зарегистрированным пользователем * **"Зарегистрироваться"** - при первом входе -.. image:: _static/main.png +.. image:: _static/main_logout.png Внимание! Для регистрации используется email с сайта Zendesk. Регистрация по каждому email возможна один раз @@ -32,7 +32,7 @@ * **"Профиль"** - просмотреть свои данные и запросить права доступа * **"Запрос прав"** - получение прав для работы с тикетами или **"Управление"** - доступно для администратора и предоставляет возможность группового назначения ролей пользователям -.. image:: _static/main_logined_agent.png +.. image:: _static/main.png ************* Регистрация @@ -72,7 +72,11 @@ На странице запроса прав Вам доступна информация о количестве и списке работающих над тикетами сотрудников, а также возможность сдать и запросить права. -.. image:: _static/permission_request.png +.. image:: _static/request.png + +Успешное изменение прав: + +.. image:: _static/role_change.png ****************************************** Управление правами доступа администратором @@ -84,6 +88,10 @@ * Количество и список инженеров и легких агентов * Возможность группового назначения прав с использованием чек-боксов -.. image:: _static/permission_management.png +.. image:: _static/admin_manage.png + +Изменение прав пользователей наглядно отразится в таблице пользователей: + +.. image:: _static/admin_manage_done.png .. |copy| unicode:: 0xA9 .. Школа программистов S101, группа 2. 2021гю diff --git a/docs/source/spelling_wordlist.txt b/docs/source/spelling_wordlist.txt index f190ae2..bd64cf9 100644 --- a/docs/source/spelling_wordlist.txt +++ b/docs/source/spelling_wordlist.txt @@ -80,6 +80,8 @@ API functions Serializer Serializers +Сериализатор +переадресации 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/logs/.gitkeep b/logs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/main/extra_func.py b/main/extra_func.py index 077615f..758875a 100644 --- a/main/extra_func.py +++ b/main/extra_func.py @@ -1,3 +1,4 @@ +import logging import os from datetime import timedelta, datetime, date @@ -7,6 +8,7 @@ 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, ZENDESK_GROUPS, SOLVED_TICKETS_EMAIL from main.models import UserProfile, RoleChangeLogs, UnassignedTicket, UnassignedTicketStatus @@ -92,12 +94,12 @@ class ZendeskAdmin: def get_group(self, name: str) -> str: """ - Функция возвращает группы, к которым принадлежит пользователь. + Функция возвращает группу по названию :param name: Имя пользователя :return: Группы пользователя (в случае отсутствия None) """ - groups = self.admin.search(name) + groups = self.admin.search(name, type='group') for group in groups: return group return None @@ -139,7 +141,7 @@ class ZendeskAdmin: raise ValueError('invalid access_controller`s login data') -def update_role(user_profile: UserProfile, role: str) -> UserProfile: +def update_role(user_profile: UserProfile, role: int) -> UserProfile: """ Функция меняет роль пользователя. @@ -162,12 +164,6 @@ def make_engineer(user_profile: UserProfile, who_changes: User) -> UserProfile: :param user_profile: Профиль пользователя :return: Вызов функции **update_role** с параметрами: профиль пользователя, роль "engineer" """ - RoleChangeLogs.objects.create( - user=user_profile.user, - old_role=user_profile.custom_role_id, - new_role=ROLES['engineer'], - changed_by=who_changes - ) update_role(user_profile, ROLES['engineer']) @@ -191,13 +187,6 @@ def make_light_agent(user_profile: UserProfile, who_changes: User) -> UserProfil ticket.assignee = None ticket.group = ZendeskAdmin().get_group(ZENDESK_GROUPS['buffer']) ZendeskAdmin().admin.tickets.update(ticket) - - RoleChangeLogs.objects.create( - user=user_profile.user, - old_role=user_profile.custom_role_id, - new_role=ROLES['light_agent'], - changed_by=who_changes - ) update_role(user_profile, ROLES['light_agent']) @@ -274,7 +263,7 @@ def check_user_auth(email: str, password: str) -> bool: return True -def update_user_in_model(profile: UserProfile, zendesk_user: User) -> UserProfile: +def update_user_in_model(profile: UserProfile, zendesk_user: User): """ Функция обновляет профиль пользователя при изменении данных пользователя на Zendesk. @@ -381,6 +370,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 +533,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 +586,6 @@ class StatisticData: :param first: Начальная дата интервала :param last: Последняя дата интервала :param val: Количество секунд в одном дне - :return: Статистику пользователя с указанным количеством секунд в заданных днях """ for day in daterange(first, last): self.statistic[day] = val @@ -584,8 +593,75 @@ 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) + + +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) 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/migrations/0017_auto_20210408_1943.py b/main/migrations/0017_auto_20210408_1943.py new file mode 100644 index 0000000..4db02cf --- /dev/null +++ b/main/migrations/0017_auto_20210408_1943.py @@ -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='Статус тикета'), + ), + ] diff --git a/main/models.py b/main/models.py index 782b79f..ac6f91c 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..8436b54 100644 --- a/main/serializers.py +++ b/main/serializers.py @@ -1,6 +1,7 @@ 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): @@ -13,11 +14,25 @@ class UserSerializer(serializers.HyperlinkedModelSerializer): class ProfileSerializer(serializers.HyperlinkedModelSerializer): - """ - Класс serializer для модель профиля пользователя. - """ + """Класс serializer для модели профиля пользователя""" user = UserSerializer() class Meta: model = UserProfile - fields = ['user', 'id', 'role', 'name'] + 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" diff --git a/main/templates/base/menu.html b/main/templates/base/menu.html index 92aaec1..93799a5 100644 --- a/main/templates/base/menu.html +++ b/main/templates/base/menu.html @@ -5,23 +5,48 @@ diff --git a/main/templates/django_registration/registration_error.html b/main/templates/django_registration/registration_error.html new file mode 100644 index 0000000..603e103 --- /dev/null +++ b/main/templates/django_registration/registration_error.html @@ -0,0 +1,15 @@ +{% extends 'base/base.html' %} +{% load static %} + +{% block title %} + Регистрация завершена +{% endblock %} + +{% block heading %} + Регистрация +{% endblock %} + +{% block content %} +
+

Произошла ошибка при отправке электронного сообщения.

+{% endblock %} diff --git a/main/templates/pages/adm_ruleset.html b/main/templates/pages/adm_ruleset.html index c2a354c..c0a5ad9 100644 --- a/main/templates/pages/adm_ruleset.html +++ b/main/templates/pages/adm_ruleset.html @@ -30,19 +30,11 @@
- {% block hidden_form %} -
- {% for field in form.users %} - {{ field.tag }} - {% endfor %} -
- {% endblock %} -
Список сотрудников
{% block table %} - +
@@ -50,25 +42,11 @@ + - - {% for user in users %} - - - - - - - {% endfor %} - -
NameRole Checked
{{ 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 %} -
- {% 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/templates/pages/work.html b/main/templates/pages/work.html index 67c5846..d9043b4 100644 --- a/main/templates/pages/work.html +++ b/main/templates/pages/work.html @@ -60,6 +60,12 @@ Получить права инженера Сдать права инженера +
+
+ + +
+
{% include 'base/success_messages.html' %} diff --git a/main/views.py b/main/views.py index 18d7e52..9971c58 100644 --- a/main/views.py +++ b/main/views.py @@ -2,39 +2,51 @@ import logging import os from datetime import datetime +from smtplib import SMTPException + +from django.contrib import messages from django.contrib.auth.decorators import login_required -from django.contrib.auth.forms import PasswordResetForm 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.exceptions import PermissionDenied from django.core.handlers.wsgi import WSGIRequest from django.http import HttpResponseRedirect, HttpResponse -from django.shortcuts import render, get_list_or_404, redirect +from django.shortcuts import render, redirect, get_list_or_404 from django.urls import reverse_lazy, reverse from django.views.generic import FormView from django_registration.views import RegistrationView -from django.contrib import messages - # Django REST from rest_framework import viewsets from rest_framework.response import Response - from zenpy.lib.api_objects import User as ZenpyUser -from access_controller.settings import EMAIL_HOST_USER, ZENDESK_ROLES, ZENDESK_MAX_AGENTS -from main.extra_func import ZendeskAdmin +from access_controller.settings import EMAIL_HOST_USER, ZENDESK_ROLES, ZENDESK_MAX_AGENTS, ZENDESK_GROUPS 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 + StatisticData, log, ZendeskAdmin from main.forms import AdminPageUsers, CustomRegistrationForm, CustomAuthenticationForm, StatisticForm -from main.serializers import ProfileSerializer +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): + print(profile_lit, control_lit, work_lit, registration_lit, login_lit) + context = { + 'profile_lit': profile_lit, + 'control_lit': control_lit, + 'work_lit': work_lit, + 'registration_lit': registration_lit, + 'login_lit': login_lit, + } + return context + + class CustomRegistrationView(RegistrationView): """ Отображение и логика работы страницы регистрации пользователя. @@ -48,10 +60,15 @@ class CustomRegistrationView(RegistrationView): :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: CustomRegistrationForm) -> User: """ @@ -64,7 +81,7 @@ class CustomRegistrationView(RegistrationView): :param form: Email пользователя на Zendesk :return: user """ - self.is_allowed = True + 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(): @@ -83,14 +100,17 @@ class CustomRegistrationView(RegistrationView): email=form.data['email'], password=User.objects.make_random_password(length=50) ) - forms.save(**opts) - update_profile(user.userprofile) - self.set_permission(user) - return user + 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.is_allowed = False + self.redirect_url = 'invalid_zendesk_email' @staticmethod def set_permission(user: User) -> None: @@ -107,7 +127,7 @@ class CustomRegistrationView(RegistrationView): ) user.user_permissions.add(permission) - def get_success_url(self, user: User = None) -> success_url: + def get_success_url(self, user: User = None): """ Функция возвращает url-адрес страницы, куда нужно перейти после успешной/не успешной регистрации. Используется самой django-registration. @@ -115,10 +135,11 @@ class CustomRegistrationView(RegistrationView): :param user: пользователь, пытающийся зарегистрироваться :return: адресация на страницу успешной регистрации """ - if self.is_allowed: - return reverse_lazy('password_reset_done') - 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() @@ -131,11 +152,12 @@ def profile_page(request: WSGIRequest) -> HttpResponse: """ user_profile: UserProfile = request.user.userprofile update_profile(user_profile) - context = { + context = setup_context(profile_lit=True) + context.update({ 'profile': user_profile, 'pagename': 'Страница профиля', 'ZENDESK_ROLES': ZENDESK_ROLES, - } + }) return render(request, 'pages/profile.html', context) @@ -165,19 +187,20 @@ def work_page(request: WSGIRequest, id: int) -> HttpResponse: 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 = { + 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") @@ -194,6 +217,7 @@ def user_update(zenpy_user: User, admin: User, request: WSGIRequest) -> UserProf admin.users.update(zenpy_user) request.user.userprofile.role = "agent" + request.user.userprofile.custom_role_id = zenpy_user.custom_role_id request.user.userprofile.save() messages.success(request, "Права были изменены") @@ -207,10 +231,7 @@ def work_hand_over(request: WSGIRequest) -> HttpResponseRedirect: :param request: данные текущего пользователя (login_required) :return: перезагрузка текущей страницы после выполнения смены роли """ - zenpy_user, admin = auth_user(request) - if zenpy_user.custom_role_id == ZENDESK_ROLES['engineer']: - zenpy_user.custom_role_id = ZENDESK_ROLES['light_agent'] - user_update(zenpy_user, admin, request) + make_light_agent(request.user.userprofile,request.user) return HttpResponseRedirect(reverse('work', args=(request.user.id,))) @@ -223,17 +244,31 @@ def work_become_engineer(request: WSGIRequest) -> HttpResponseRedirect: :return: перезагрузка текущей страницы после выполнения смены роли """ zenpy_user, admin = auth_user(request) - if zenpy_user.custom_role_id == ZENDESK_ROLES['light_agent']: - zenpy_user.custom_role_id = ZENDESK_ROLES['engineer'] - user_update(zenpy_user, admin, request) + + make_engineer(request.user.userprofile,request.user) + return HttpResponseRedirect(reverse('work', args=(request.user.id,))) + +@login_required() +def work_get_tickets(request): + zenpy_user, admin = auth_user(request) + count_tickets = int(request.GET["count_tickets"]) + tickets = [ticket for ticket in admin.search(type="ticket") if ticket.group.name == 'Сменная группа' and ticket.assignee is None] + for i in range(len(tickets)): + if i == count_tickets: + return HttpResponseRedirect(reverse('work', args=(request.user.id,))) + tickets[i].assignee = zenpy_user + admin.tickets.update(tickets[i]) return HttpResponseRedirect(reverse('work', args=(request.user.id,))) -def main_page(request): +def main_page(request: WSGIRequest) -> HttpResponse: + """ + Функция переадресации на главную страницу. + """ return render(request, 'pages/index.html') -class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin,SuccessMessageMixin, FormView): +class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, FormView): """ Класс отображения страницы администратора. @@ -275,29 +310,50 @@ class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin,SuccessMessageMi """ 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) def get_context_data(self, **kwargs) -> dict: """ Функция формирования контента страницы администратора (с проверкой прав доступа) """ - context = super().get_context_data(**kwargs) + + # context = super().get_context_data(**kwargs) + + # context['licences_remaining'] = max(0, ZENDESK_MAX_AGENTS - context['engineers']) + # return context + + context = setup_context(control_lit=True) + context.update(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']) + context.update({ + 'users': users, + 'ZENDESK_ROLES': ZENDESK_ROLES, + 'engineers': context['engineers'], + 'light_agents': context['light_agents'], + 'licences_remaining': max(0, ZENDESK_MAX_AGENTS - context['engineers']), + }) return context # TODO: need to get profile page url + class CustomLoginView(LoginView): """ Отображение страницы авторизации пользователя """ + extra_context = setup_context(login_lit=True) form_class = CustomAuthenticationForm @@ -309,16 +365,34 @@ class UsersViewSet(viewsets.ReadOnlyModelViewSet): serializer_class = ProfileSerializer def list(self, request, *args, **kwargs): - users = update_users_in_model().values - count = count_users(users) + users = update_users_in_model() + count = count_users(users.values) profiles = UserProfile.objects.filter(role='agent') serializer = self.get_serializer(profiles, many=True) - return Response({ + res = { 'users': serializer.data, 'engineers': count[0], - 'light_agents': count[1] - }) + 'light_agents': count[1], + "zendesk_users": self.get_zendesk_users(self.choose_users(users.values, profiles)) + } + 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() @@ -329,12 +403,18 @@ def statistic_page(request: WSGIRequest) -> HttpResponse: :param request: данные о пользователе: email, время и интервал работы. Данные получаем через forms.StatisticForm :return: адресация на страницу статистики """ + + # if not request.user.has_perm('main.has_control_access'): + # raise PermissionDenied + # context = { + if not request.user.is_superuser: return redirect('index') - context = { + context = setup_context() + context.update({ 'pagename': 'страница статистики', 'errors': list(), - } + }) if request.method == "POST": form = StatisticForm(request.POST) if form.is_valid(): @@ -352,4 +432,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/manage.py b/manage.py index 3601c76..2554164 100755 --- a/manage.py +++ b/manage.py @@ -6,7 +6,8 @@ import sys def main(): """Run administrative tasks.""" - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'access_controller.settings') + os.environ.setdefault('DJANGO_SETTINGS_MODULE', + 'access_controller.settings') try: from django.core.management import execute_from_command_line except ImportError as exc: 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..d534270 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,7 @@ padding: 10px; background: #3B91D4; color: white; -} \ No newline at end of file + + width: 100%; +} + diff --git a/static/main/js/control.js b/static/main/js/control.js index e518c36..82f9f27 100644 --- a/static/main/js/control.js +++ b/static/main/js/control.js @@ -1,31 +1,15 @@ "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 { +class ModelUserTableRow extends React.Component { render() { return ( - + {this.props.user.name} {this.props.user.user.email} - {this.props.user.role} + {this.props.user.zendesk_role} ( + + )), + document.getElementById("tbody") + ); + } +} + +class ZendeskUserTableRow extends React.Component { + render() { + return ( + + + {this.props.user.name} + + {this.props.user.email} + {this.props.user.zendesk_role} + + + ); + } +} + +class ZendeskUserTableRows extends React.Component { + render() { + return ReactDOM.createPortal( + this.props.users.map((user, key) => ( + + )), + document.getElementById("tbody") + ); + } +} + class TableBody extends React.Component { constructor(props) { super(props); @@ -46,15 +67,17 @@ class TableBody extends React.Component { users: [], engineers: 0, light_agents: 0, + zendesk_users: [], }; } - 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, light_agents: response.data.light_agents, + zendesk_users: response.data.zendesk_users, }); let elements = document.querySelectorAll(".info-quantity-value"); elements[0].innerHTML = this.state.engineers; @@ -62,7 +85,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); @@ -73,11 +101,13 @@ class TableBody extends React.Component { } render() { - return this.state.users.map((user, key) => ( - - )); + return ( + + + + + ); } } -ReactDOM.render(, document.getElementById("new_tbody")); -setTimeout(() => document.getElementById("old_tbody").remove(), 60000); +ReactDOM.render(, document.getElementById("tbody"));