diff --git a/access_controller/settings.py b/access_controller/settings.py index 7b1f707..60367a3 100644 --- a/access_controller/settings.py +++ b/access_controller/settings.py @@ -21,10 +21,10 @@ BASE_DIR = Path(__file__).resolve().parent.parent # See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = os.getenv('ACTRL_SECRET_KEY','empty') +SECRET_KEY = os.getenv('ACTRL_SECRET_KEY', 'empty') # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = bool(int(os.getenv('ACTRL_DEBUG',1))) +DEBUG = bool(int(os.getenv('ACTRL_DEBUG', 1))) ALLOWED_HOSTS = [ '127.0.0.1', @@ -59,13 +59,13 @@ MIDDLEWARE = [ ROOT_URLCONF = 'access_controller.urls' EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' -EMAIL_HOST = os.getenv('ACTRL_EMAIL_HOST','smtp.gmail.com') -EMAIL_PORT = int(os.getenv('ACTRL_EMAIL_PORT',587)) -EMAIL_USE_TLS = bool(int(os.getenv('ACTRL_EMAIL_TLS',1))) -EMAIL_HOST_USER = os.getenv('ACTRL_EMAIL_HOST_USER','group02django@gmail.com') -EMAIL_HOST_PASSWORD = os.getenv('ACTRL_EMAIL_HOST_PASSWORD','djangogroup02') -DEFAULT_FROM_EMAIL = os.getenv('ACTRL_FROM_EMAIL',EMAIL_HOST_USER) -SERVER_EMAIL = os.getenv('ACTRL_SERVER_EMAIL',EMAIL_HOST_USER) +EMAIL_HOST = os.getenv('ACTRL_EMAIL_HOST', 'smtp.gmail.com') +EMAIL_PORT = int(os.getenv('ACTRL_EMAIL_PORT', 587)) +EMAIL_USE_TLS = bool(int(os.getenv('ACTRL_EMAIL_TLS', 1))) +EMAIL_HOST_USER = os.getenv('ACTRL_EMAIL_HOST_USER', 'group02django@gmail.com') +EMAIL_HOST_PASSWORD = os.getenv('ACTRL_EMAIL_HOST_PASSWORD', 'djangogroup02') +DEFAULT_FROM_EMAIL = os.getenv('ACTRL_FROM_EMAIL', EMAIL_HOST_USER) +SERVER_EMAIL = os.getenv('ACTRL_SERVER_EMAIL', EMAIL_HOST_USER) TEMPLATES = [ { @@ -154,8 +154,8 @@ AUTHENTICATION_BACKENDS = [ ZENDESK_ROLES = { - 'engineer': int(os.getenv('ENG_CROLE_ID',0)), - 'light_agent': int(os.getenv('LA_CROLE_ID',0)), + 'engineer': int(os.getenv('ENG_CROLE_ID', 0)), + 'light_agent': int(os.getenv('LA_CROLE_ID', 0)), } ZENDESK_GROUPS = { @@ -165,7 +165,7 @@ ZENDESK_GROUPS = { SOLVED_TICKETS_EMAIL = os.getenv('ST_EMAIL') -ZENDESK_MAX_AGENTS = int(os.getenv('LICENSE_NO',0)) +ZENDESK_MAX_AGENTS = int(os.getenv('LICENSE_NO', 0)) REST_FRAMEWORK = { # Use Django's standard `django.contrib.auth` permissions, @@ -175,7 +175,7 @@ REST_FRAMEWORK = { ] } -ONE_DAY = int(os.getenv('SHIFTH',0)) # Количество часов в 1 рабочем дне +ONE_DAY = int(os.getenv('SHIFTH', 0)) # Количество часов в 1 рабочем дне ACTRL_ZENDESK_SUBDOMAIN = os.getenv('ACTRL_ZENDESK_SUBDOMAIN') or os.getenv('ZD_DOMAIN') ACTRL_API_EMAIL = os.getenv('ACTRL_API_EMAIL') or os.getenv('ACCESS_CONTROLLER_API_EMAIL') diff --git a/access_controller/urls.py b/access_controller/urls.py index 11e9fc7..2cab267 100644 --- a/access_controller/urls.py +++ b/access_controller/urls.py @@ -21,6 +21,7 @@ from main.views import work_page, work_hand_over, work_become_engineer, work_get AdminPageView, statistic_page from main.urls import router + urlpatterns = [ path('admin/', admin.site.urls, name='admin'), path('', main_page, name='index'), diff --git a/fixtures/data.json b/fixtures/data.json new file mode 100644 index 0000000..a4310a4 --- /dev/null +++ b/fixtures/data.json @@ -0,0 +1,57 @@ +[ + { + "model": "auth.user", + "pk": 1, + "fields": { + "password": "pbkdf2_sha256$216000$gHBBCr1jBELf$ZkEDW3IEd8Wij7u8vkv+0Eze32CS01bcaYWhcD9OIC4=", + "last_login": null, + "is_superuser": true, + "username": "admin@gmail.com", + "first_name": "", + "last_name": "", + "email": "admin@gmail.com", + "is_staff": true, + "is_active": true, + "date_joined": "2021-03-10T16:38:56.303Z", + "groups": [], + "user_permissions": [33] + } + }, + { + "model": "main.userprofile", + "pk": 1, + "fields": { + "name": "ZendeskAdmin", + "user": 1, + "role": "admin" + } + }, + { + "model": "auth.user", + "pk": 2, + "fields": { + "password": "pbkdf2_sha256$216000$5qLJgrm2Quq9$KDBNNymVZXkUx0HKBPFst2m83kLe0egPBnkW7KnkORU=", + "last_login": null, + "is_superuser": false, + "username": "123@test.ru", + "first_name": "", + "last_name": "", + "email": "123@test.ru", + "is_staff": false, + "is_active": true, + "date_joined": "2021-03-10T16:38:56.303Z", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "main.userprofile", + "pk": 2, + "fields": { + "name": "UserForAccessTest", + "user": 2, + "role": "agent", + "custom_role_id": "360005209000" + } + } +] diff --git a/layouts/registration_success/registration_success.png b/layouts/registration_success/registration_success.png new file mode 100644 index 0000000..67e7074 Binary files /dev/null and b/layouts/registration_success/registration_success.png differ diff --git a/layouts/work/workv2.png b/layouts/work/workv2.png new file mode 100644 index 0000000..620ddb1 Binary files /dev/null and b/layouts/work/workv2.png differ diff --git a/main/extra_func.py b/main/extra_func.py index f215544..4dfbcba 100644 --- a/main/extra_func.py +++ b/main/extra_func.py @@ -14,18 +14,20 @@ from django.utils import timezone from zenpy import Zenpy from zenpy.lib.exception import APIException from zenpy.lib.api_objects import User as ZenpyUser, Ticket as ZenpyTicket +from zenpy.lib.generator import SearchResultGenerator from access_controller.settings import ZENDESK_ROLES as ROLES, ONE_DAY, ACTRL_ZENDESK_SUBDOMAIN from main.models import UserProfile, RoleChangeLogs, UnassignedTicket, UnassignedTicketStatus from main.zendesk_admin import zenpy -def update_role(user_profile: UserProfile, role: int) -> None: +def update_role(user_profile: UserProfile, role: int, who_changes: User) -> None: """ Функция меняет роль пользователя. :param user_profile: Профиль пользователя :param role: Новая роль + :param who_changes: Пользователь, меняющий роль :return: Пользователь с обновленной ролью """ zendesk = zenpy @@ -33,6 +35,7 @@ def update_role(user_profile: UserProfile, role: int) -> None: user.custom_role_id = role user_profile.custom_role_id = role user_profile.save() + log(user_profile, who_changes.userprofile) zendesk.admin.users.update(user) @@ -41,10 +44,9 @@ def make_engineer(user_profile: UserProfile, who_changes: User) -> None: Функция устанавливает пользователю роль инженера. :param user_profile: Профиль пользователя - :return: Вызов функции **update_role** с параметрами: - профиль пользователя, роль "engineer" + :return: Вызов функции **update_role** с параметрами: профиль пользователя, роль "engineer" """ - update_role(user_profile, ROLES['engineer']) + update_role(user_profile, ROLES['engineer'], who_changes) def make_light_agent(user_profile: UserProfile, who_changes: User) -> None: @@ -52,29 +54,29 @@ def make_light_agent(user_profile: UserProfile, who_changes: User) -> None: Функция устанавливает пользователю роль легкого агента. :param user_profile: Профиль пользователя - :return: Вызов функции **update_role** с параметрами: - профиль пользователя, роль "light_agent" + :return: Вызов функции **update_role** с параметрами: профиль пользователя, роль "light_agent" """ - tickets = get_tickets_list(user_profile.user.email) + tickets: SearchResultGenerator = get_tickets_list(user_profile.user.email) ticket: ZenpyTicket for ticket in tickets: UnassignedTicket.objects.create( assignee=user_profile.user, ticket_id=ticket.id, - status=UnassignedTicketStatus.SOLVED if ticket.status == 'solved' - else UnassignedTicketStatus.UNASSIGNED + status=UnassignedTicketStatus.SOLVED if ticket.status == 'solved' else UnassignedTicketStatus.UNASSIGNED ) if ticket.status == 'solved': ticket.assignee_id = zenpy.solved_tickets_user_id else: ticket.assignee = None ticket.group_id = zenpy.buffer_group_id - zenpy.admin.tickets.update(tickets.values) + + if tickets.count: + zenpy.admin.tickets.update(tickets.values) attempts, success = 5, False while not success and attempts != 0: try: - update_role(user_profile, ROLES['light_agent']) + update_role(user_profile, ROLES['light_agent'], who_changes) success = True except APIException as e: attempts -= 1 @@ -84,8 +86,7 @@ def make_light_agent(user_profile: UserProfile, who_changes: User) -> None: def get_users_list() -> list: """ - Функция **get_users_list** возвращает список - пользователей Zendesk, относящихся к организации SYSTEM. + Функция **get_users_list** возвращает список пользователей Zendesk, относящихся к организации SYSTEM. """ zendesk = zenpy @@ -102,7 +103,7 @@ def get_tickets_list(email) -> list: return zenpy.admin.search(assignee=email, type='ticket') -def update_profile(user_profile: UserProfile) -> None: +def update_profile(user_profile: UserProfile): """ Функция обновляет профиль пользователя в соответствии с текущим в Zendesk. @@ -174,7 +175,7 @@ def update_user_in_model(profile: UserProfile, zendesk_user: ZenpyUser) -> None: def count_users(users: list) -> tuple: """ - Функция подсчета количества сотрудников с ролями engineer и light_agent. + Функция подсчета количества сотрудников с ролями engineer и light_agent """ engineers, light_agents = 0, 0 for user in users: @@ -187,7 +188,7 @@ def count_users(users: list) -> tuple: def update_users_in_model() -> list: """ - Обновляет пользователей в модели UserProfile по списку пользователей в организации. + Обновляет пользователей в модели UserProfile по списку пользователей в организации """ users = get_users_list() for user in users: @@ -284,9 +285,7 @@ class StatisticData: """ Функция возвращает статистику работы пользователя. - :return: Словарь statistic с применением формата отображения - и интервала работы(если они есть). - None, если были ошибки при создании. + :return: Словарь statistic с применением формата отображения и интервала работы(если они есть). None, если были ошибки при создании. """ if self.is_valid_statistic(): stat = self.statistic @@ -377,12 +376,9 @@ class StatisticData: if self.interval == 'months': # Переделываем ключи под формат('начало_месяца - конец_месяца') for key, value in stat.items(): - current_month_start = max(self.start_date, date( - year=key.year, month=key.month, day=1)) - current_month_end = min(self.end_date, last_day_of_month( - date(year=key.year, month=key.month, day=1))) - index = ' - '.join([str(current_month_start), - str(current_month_end)]) + current_month_start = max(self.start_date, date(year=key.year, month=key.month, day=1)) + current_month_end = min(self.end_date, last_day_of_month(date(year=key.year, month=key.month, day=1))) + index = ' - '.join([str(current_month_start), str(current_month_end)]) if new_stat.get(index): new_stat[index] += value else: @@ -405,17 +401,14 @@ class StatisticData: """ Функция возвращает логи в диапазоне дат start_date - end_date для пользователя с указанным email. - :return: Данные о смене статусов пользователя. - Если пользователь не найден или интервал времени некорректен - ошибку. + :return: Данные о смене статусов пользователя. Если пользователь не найден или интервал времени некорректен - ошибку. """ if not self.check_time(): - self.errors += [ - 'Конец диапазона должен быть позже начала диапазона и раньше текущего времени'] + self.errors += ['Конец диапазона должен быть позже начала диапазона и раньше текущего времени'] return try: self.data = RoleChangeLogs.objects.filter( - change_time__range=[self.start_date, - self.end_date + timedelta(days=1)], + change_time__range=[self.start_date, self.end_date + timedelta(days=1)], user=User.objects.get(email=self.email), ).order_by('change_time') except User.DoesNotExist: @@ -445,55 +438,42 @@ class StatisticData: def engineer_logic(self, log_index: int) -> None: """ - Функция обрабатывает основную часть работы инженера. - - :param 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()) + 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.statistic[current_log.change_time.date()] += elapsed_time.total_seconds() def post_engineer_logic(self, last_log: RoleChangeLogs) -> None: """ - Функция обрабатывает случай, когда нам известно что инженер работал и после диапазона. - - :param last_log: Последний лог + Функция обрабатывает случай, когда нам изветсно что инженер работал и после диапазона """ - self.fill_daterange(last_log.change_time.date( - ) + timedelta(days=1), self.end_date + timedelta(days=1)) + 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) + 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() + self.statistic[self.end_date] = get_timedelta(None, timezone.now().time()).total_seconds() - def prev_engineer_logic(self, first_log: RoleChangeLogs) -> None: + def prev_engineer_logic(self, first_log): """ - Функция обрабатывает случай, когда нам извеcтно, что инженер начал работу до диапазона. - - :param 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() + 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) -> None: + def fill_daterange(self, first: date, last: date, val: int = 24 * 3600) -> dict: """ Функция заполняет диапазон дат значением val (по умолчанию val = кол-во секунд в 1 дне). @@ -509,8 +489,7 @@ class StatisticData: Функция осуществляет обновление всех дней. """ 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) class DatabaseHandler(logging.Handler): @@ -579,12 +558,12 @@ class CsvFormatter(logging.Formatter): return msg -def log(user: User, admin: int = 0) -> None: +def log(user, admin=None): """ - Функция осуществляет запись логов в базу данных и csv файл. - - :param admin: Админ, который меняет роль - :param user: Пользователь, которому изменена роль + Осуществляет запись логов в базу данных и csv файл + :param admin: + :param user: + :return: """ users = [user, admin] logger = logging.getLogger('MY_LOGGER') diff --git a/main/tests.py b/main/tests.py index 4a18882..c06bc21 100644 --- a/main/tests.py +++ b/main/tests.py @@ -1,5 +1,83 @@ -""" -Тесты. -""" -# from django.test import TestCase, Client -# import access_controller.settings as sets +from urllib.parse import urlparse + +from django.contrib.auth.models import User +from django.core import mail +from django.test import TestCase, Client +from django.urls import reverse +from django.utils import translation + +import access_controller.settings as sets +from main.zendesk_admin import zenpy + + +class RegistrationTestCase(TestCase): + fixtures = ['fixtures/data.json'] + + def setUp(self): + self.email_backend = 'django.core.mail.backends.locmem.EmailBackend' + self.any_zendesk_user_email = 'idar.sokurov.05@mail.ru' + self.zendesk_admin_email = 'idar.sokurov.05@mail.ru' + self.client = Client() + + def test_registration_complete_redirect(self): + with self.settings(EMAIL_BACKEND=self.email_backend): + resp = self.client.post(reverse('registration'), data={'email': self.any_zendesk_user_email}) + self.assertRedirects(resp, reverse('password_reset_done')) + + def test_registration_fail_redirect(self): + with self.settings(EMAIL_BACKEND=self.email_backend): + resp = self.client.post(reverse('registration'), data={'email': self.any_zendesk_user_email + 'asd'}) + self.assertRedirects(resp, reverse('django_registration_disallowed')) + + def test_registration_user_already_exist(self): + with self.settings(EMAIL_BACKEND=self.email_backend) and translation.override('ru'): + resp = self.client.post(reverse('registration'), data={'email': '123@test.ru'}) + self.assertContains(resp, 'Этот адрес электронной почты уже используется', count=1, status_code=200) + + def test_registration_email_sending(self): + # TODO: Найти способ лучше проверять сообщения + email_template = [ + '', + 'Вы получили это письмо, потому что вы (или кто-то другой) запросили восстановление пароля ' + 'от учётной записи на сайте testserver, которая связана с этим адресом электронной почты.', + '', + 'Пожалуйста, перейдите на эту страницу и введите новый пароль:', + '', + 'url', + '', + f'Ваше имя пользователя (на случай, если вы его забыли): {self.any_zendesk_user_email}', + '', + 'Спасибо, что используете наш сайт!', + '', + 'Команда сайта testserver', + '', + '', + '', + ] + with self.settings(EMAIL_BACKEND=self.email_backend) and translation.override('ru'): + self.client.post(reverse('registration'), data={'email': self.any_zendesk_user_email}) + + self.assertEqual(len(mail.outbox), 1) + self.assertEqual(mail.outbox[0].to, [self.zendesk_admin_email]) + self.assertEqual(mail.outbox[0].from_email, sets.DEFAULT_FROM_EMAIL) + + message = mail.outbox[0].body.split('\n') + for i in range(len(message)): + if email_template[i] != 'url': + self.assertEqual(message[i], email_template[i]) + else: + self.assertTrue(urlparse(message[i]).scheme) + + def test_registration_user_creating(self): + with self.settings(EMAIL_BACKEND=self.email_backend): + self.client.post(reverse('registration'), data={'email': self.any_zendesk_user_email}) + user = User.objects.get(email=self.any_zendesk_user_email) + zendesk_user = zenpy.get_user(self.any_zendesk_user_email) + self.assertEqual(user.userprofile.name, zendesk_user.name) + + def test_permissions_applying(self): + with self.settings(EMAIL_BACKEND=self.email_backend): + self.client.post(reverse('registration'), data={'email': self.zendesk_admin_email}) + user = User.objects.get(email=self.zendesk_admin_email) + self.assertEqual(user.userprofile.role, 'admin') + self.assertTrue(user.has_perm('main.has_control_access')) diff --git a/main/views.py b/main/views.py index 38e7f73..35fa607 100644 --- a/main/views.py +++ b/main/views.py @@ -122,7 +122,7 @@ class CustomRegistrationView(RegistrationView): self.redirect_url = 'email_sending_error' return None else: - raise ValueError('Непредвиденная ошибка') + self.redirect_url = 'email_sending_error' else: self.redirect_url = 'invalid_zendesk_email' return None @@ -318,7 +318,6 @@ class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageM """ for user in users: make_engineer(user, self.request.user) - log(user, self.request.user.userprofile) def make_light_agents(self, users: list) -> None: """ @@ -329,7 +328,6 @@ class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageM """ for user in users: make_light_agent(user, self.request.user) - log(user, self.request.user.userprofile) class CustomLoginView(LoginView): diff --git a/static/main/js/control.js b/static/main/js/control.js index ee62c5c..6dd9172 100644 --- a/static/main/js/control.js +++ b/static/main/js/control.js @@ -1,6 +1,4 @@ "use strict"; -import React from "react"; -import ReactDOM from "react-dom"; function head_checkbox() { let head_checkbox = document.getElementById("head-checkbox");