Merge branch 'develop' of https://gitlab.informatics.ru/2020-2021/online/s101/group-02/access_controller into develop
This commit is contained in:
commit
381ae363e3
@ -48,9 +48,9 @@ cp .env.example .env
|
|||||||
sudo apt install make
|
sudo apt install make
|
||||||
pip install --upgrade pip
|
pip install --upgrade pip
|
||||||
pip install -r requirements/dev.txt
|
pip install -r requirements/dev.txt
|
||||||
(set -a && source .env && ./manage.py migrate)
|
./manage.py migrate
|
||||||
(set -a && source .env && ./manage.py loaddata data.json)
|
./manage.py loaddata data.json
|
||||||
(set -a && source .env && ./manage.py runserver)
|
./manage.py runserver
|
||||||
```
|
```
|
||||||
|
|
||||||
## Перед запуском для тестирования:
|
## Перед запуском для тестирования:
|
||||||
@ -65,7 +65,7 @@ pip install -r requirements/dev.txt
|
|||||||
- Перейти в папку приложения
|
- Перейти в папку приложения
|
||||||
- Активировать виртуальное окружение
|
- Активировать виртуальное окружение
|
||||||
- Выполнить команду `pip install -r requirements/dev.txt`
|
- Выполнить команду `pip install -r requirements/dev.txt`
|
||||||
- В виртуальное окружение добавить следующие переменные:
|
- В файл `.env` добавить следующие переменные:
|
||||||
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
@ -11,6 +11,7 @@ https://docs.djangoproject.com/en/3.1/ref/settings/
|
|||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
@ -18,6 +19,9 @@ BASE_DIR = Path(__file__).resolve().parent.parent
|
|||||||
# Quick-start development settings - unsuitable for production
|
# Quick-start development settings - unsuitable for production
|
||||||
# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/
|
# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/
|
||||||
|
|
||||||
|
# Load environment variables from .env
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
# 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')
|
||||||
|
|
||||||
|
59
fixtures/profile.json
Normal file
59
fixtures/profile.json
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"model": "auth.user",
|
||||||
|
"pk": 1,
|
||||||
|
"fields": {
|
||||||
|
"password": "pbkdf2_sha256$216000$gHBBCr1jBELf$ZkEDW3IEd8Wij7u8vkv+0Eze32CS01bcaYWhcD9OIC4=",
|
||||||
|
"last_login": null,
|
||||||
|
"is_superuser": true,
|
||||||
|
"username": "idar.sokurov.05@mail.ru",
|
||||||
|
"first_name": "",
|
||||||
|
"last_name": "",
|
||||||
|
"email": "idar.sokurov.05@mail.ru",
|
||||||
|
"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": "krav-88@mail.ru",
|
||||||
|
"first_name": "",
|
||||||
|
"last_name": "",
|
||||||
|
"email": "krav-88@mail.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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
@ -6,8 +6,8 @@ from django.core.exceptions import ObjectDoesNotExist
|
|||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from zenpy import Zenpy
|
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.api_objects import User as ZenpyUser, Ticket as ZenpyTicket
|
||||||
|
from zenpy.lib.exception import APIException
|
||||||
from zenpy.lib.generator import SearchResultGenerator
|
from zenpy.lib.generator import SearchResultGenerator
|
||||||
|
|
||||||
from access_controller.settings import ZENDESK_ROLES as ROLES, ACTRL_ZENDESK_SUBDOMAIN
|
from access_controller.settings import ZENDESK_ROLES as ROLES, ACTRL_ZENDESK_SUBDOMAIN
|
||||||
|
@ -140,3 +140,23 @@ class StatisticForm(forms.Form):
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class WorkGetTicketsForm(forms.Form):
|
||||||
|
"""
|
||||||
|
Форма получения количества тикетов для страницы work и work_get_tickets.
|
||||||
|
|
||||||
|
:param count_tickets: Поле для ввода количества тикетов
|
||||||
|
:type count_tickets: :class:`django.forms.fields.IntegerField`
|
||||||
|
"""
|
||||||
|
count_tickets = forms.IntegerField(
|
||||||
|
min_value=0,
|
||||||
|
max_value=100,
|
||||||
|
required=True,
|
||||||
|
widget=forms.NumberInput(
|
||||||
|
attrs={
|
||||||
|
'class': 'form-control mb-3',
|
||||||
|
'value': 1
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
@ -3,56 +3,62 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
|
||||||
<nav class="navbar navbar-light" style="background-color: #113A60;">
|
<nav class="navbar navbar-light py-3" style="background-color: #113A60;">
|
||||||
<a class="navbar-brand" href="{% url 'index' %}">
|
<a class="navbar-brand" href="{% url 'index' %}">
|
||||||
<img src="{% static 'main/img/logo_real.png' %}" width="107" height="22" class="d-inline-block align-top" style="margin-left: 15px" alt="" loading="lazy">
|
<img src="{% static 'main/img/logo_real.png' %}" width="107" height="22" class="d-inline-block align-top" style="margin-left: 15px" alt="" loading="lazy">
|
||||||
<t style="color:#FFFFFF">Access Controller</t>
|
<t class="px-2" style="color:#FFFFFF">Access Controller</t>
|
||||||
</a>
|
</a>
|
||||||
{% if request.user.is_authenticated %}
|
{% if request.user.is_authenticated %}
|
||||||
<div class="btn-group" role="group" aria-label="Basic example" style="margin-right: 9px">
|
<div class="btn-group" role="group" aria-label="Basic example" style="margin-right: 9px">
|
||||||
<a {% if profile_lit %}
|
{% url 'profile' as profile_url %}
|
||||||
|
<a {% if request.path == profile_url %}
|
||||||
class="btn btn-primary"
|
class="btn btn-primary"
|
||||||
{% else %}
|
{% else %}
|
||||||
class="btn btn-secondary"
|
class="btn btn-secondary"
|
||||||
{% endif %}
|
{% endif %}
|
||||||
href="{% url 'profile' %}">Профиль</a>
|
href="{{ profile_url }}">Профиль</a>
|
||||||
{% if perms.main.has_control_access %}
|
{% if perms.main.has_control_access %}
|
||||||
<a {% if control_lit %}
|
{% url 'control' as control_url %}
|
||||||
|
<a {% if request.path == control_url %}
|
||||||
class="btn btn-primary"
|
class="btn btn-primary"
|
||||||
{% else %}
|
{% else %}
|
||||||
class="btn btn-secondary"
|
class="btn btn-secondary"
|
||||||
{% endif %}
|
{% endif %}
|
||||||
href="{% url 'control' %}">Управление</a>
|
href="{{ control_url }}">Управление</a>
|
||||||
<a {% if stats_lit %}
|
{% url 'statistic' as statistic_url %}
|
||||||
|
<a {% if request.path == statistic_url %}
|
||||||
class="btn btn-primary"
|
class="btn btn-primary"
|
||||||
{% else %}
|
{% else %}
|
||||||
class="btn btn-secondary"
|
class="btn btn-secondary"
|
||||||
{% endif %}
|
{% endif %}
|
||||||
href="{% url 'statistic' %}">Статистика</a>
|
href="{{ statistic_url }}">Статистика</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a {% if work_lit %}
|
{% url 'work' request.user.id as work_url %}
|
||||||
|
<a {% if request.path == work_url %}
|
||||||
class="btn btn-primary"
|
class="btn btn-primary"
|
||||||
{% else %}
|
{% else %}
|
||||||
class="btn btn-secondary"
|
class="btn btn-secondary"
|
||||||
{% endif %}
|
{% endif %}
|
||||||
href="{% url 'work' request.user.id %}">Запрос прав</a>
|
href="{{ work_url }}">Запрос прав</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a class="btn btn-secondary" href="{% url 'logout' %}">Выйти</a>
|
<a class="btn btn-secondary" href="{% url 'logout' %}">Выйти</a>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="btn-group" role="group" aria-label="Basic example" style="margin-right: 9px">
|
<div class="btn-group" role="group" aria-label="Basic example" style="margin-right: 9px">
|
||||||
<a {% if login_lit %}
|
{% url 'login' as login_url %}
|
||||||
|
<a {% if request.path == login_url %}
|
||||||
class="btn btn-primary"
|
class="btn btn-primary"
|
||||||
{% else %}
|
{% else %}
|
||||||
class="btn btn-secondary"
|
class="btn btn-secondary"
|
||||||
{% endif %}
|
{% endif %}
|
||||||
href="/accounts/login">Войти</a>
|
href="{{ login_url }}">Войти</a>
|
||||||
<a {% if registration_lit %}
|
{% url 'registration' as registration_url %}
|
||||||
|
<a {% if request.path == registration_url %}
|
||||||
class="btn btn-primary"
|
class="btn btn-primary"
|
||||||
{% else %}
|
{% else %}
|
||||||
class="btn btn-secondary"
|
class="btn btn-secondary"
|
||||||
{% endif %}
|
{% endif %}
|
||||||
href="/accounts/register">Зарегистрироваться</a>
|
href="{{ registration_url }}">Зарегистрироваться</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</nav>
|
</nav>
|
||||||
|
@ -66,10 +66,11 @@
|
|||||||
<a href="/work/hand_over" class="hand-over-acess-button default-button">Сдать права инженера</a>
|
<a href="/work/hand_over" class="hand-over-acess-button default-button">Сдать права инженера</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-10">
|
<div class="col-10">
|
||||||
<form method="GET" action="/work/get_tickets">
|
<form method="post" action="{% url 'work_get_tickets' %}">
|
||||||
<input class="form-control mb-3" type="number" min="1" value="1" name="count_tickets">
|
{% csrf_token %}
|
||||||
<button type="submit" class="default-button">Взять тикеты в работу</button>
|
{{ get_tickets_form.count_tickets }}
|
||||||
</form>
|
<button type="submit" class="default-button">Взять тикеты в работу</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{% for message in messages %}
|
{% for message in messages %}
|
||||||
<script>create_notification('{{message}}','','{{message.tags}}',2000)</script>
|
<script>create_notification('{{message}}','','{{message.tags}}',2000)</script>
|
||||||
|
330
main/tests.py
330
main/tests.py
@ -1,8 +1,10 @@
|
|||||||
from unittest.mock import patch
|
import random
|
||||||
from urllib.parse import urlparse
|
from unittest.mock import patch, Mock
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.core import mail
|
from django.core import mail
|
||||||
|
from django.http import HttpResponseRedirect
|
||||||
|
from django.template.loader import render_to_string
|
||||||
from django.test import TestCase, Client
|
from django.test import TestCase, Client
|
||||||
from django.urls import reverse, reverse_lazy
|
from django.urls import reverse, reverse_lazy
|
||||||
from django.utils import translation
|
from django.utils import translation
|
||||||
@ -35,39 +37,20 @@ class RegistrationTestCase(TestCase):
|
|||||||
resp = self.client.post(reverse('registration'), data={'email': '123@test.ru'})
|
resp = self.client.post(reverse('registration'), data={'email': '123@test.ru'})
|
||||||
self.assertContains(resp, 'Этот адрес электронной почты уже используется', count=1, status_code=200)
|
self.assertContains(resp, 'Этот адрес электронной почты уже используется', count=1, status_code=200)
|
||||||
|
|
||||||
def test_registration_email_sending(self):
|
def test_registration_send_email(self):
|
||||||
# TODO: Найти способ лучше проверять сообщения
|
with self.settings(EMAIL_BACKEND=self.email_backend):
|
||||||
email_template = [
|
response: HttpResponseRedirect = \
|
||||||
'',
|
self.client.post(reverse('registration'), data={'email': self.any_zendesk_user_email})
|
||||||
'Вы получили это письмо, потому что вы (или кто-то другой) запросили восстановление пароля '
|
self.assertEqual(response.status_code, 302)
|
||||||
'от учётной записи на сайте 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(len(mail.outbox), 1)
|
||||||
self.assertEqual(mail.outbox[0].to, [self.zendesk_admin_email])
|
self.assertEqual(mail.outbox[0].to, [self.any_zendesk_user_email])
|
||||||
self.assertEqual(mail.outbox[0].from_email, sets.DEFAULT_FROM_EMAIL)
|
|
||||||
|
|
||||||
message = mail.outbox[0].body.split('\n')
|
# context that the email template was rendered with
|
||||||
for i in range(len(message)):
|
email_context = response.context[0].dicts[1]
|
||||||
if email_template[i] != 'url':
|
correct_subject = render_to_string('registration/password_reset_subject.txt', email_context, response.request)
|
||||||
self.assertEqual(message[i], email_template[i])
|
self.assertEqual(mail.outbox[0].subject, correct_subject.strip())
|
||||||
else:
|
correct_body = render_to_string('registration/password_reset_email.html', email_context, response.request)
|
||||||
self.assertTrue(urlparse(message[i]).scheme)
|
self.assertEqual(mail.outbox[0].body, correct_body)
|
||||||
|
|
||||||
def test_registration_user_creating(self):
|
def test_registration_user_creating(self):
|
||||||
with self.settings(EMAIL_BACKEND=self.email_backend):
|
with self.settings(EMAIL_BACKEND=self.email_backend):
|
||||||
@ -148,3 +131,284 @@ class MakeEngineerTestCase(TestCase):
|
|||||||
self.assertEqual(len(call_list), 2)
|
self.assertEqual(len(call_list), 2)
|
||||||
for obj in mock_objects:
|
for obj in mock_objects:
|
||||||
self.assertEqual(obj[0][0].custom_role_id, sets.ZENDESK_ROLES['engineer'])
|
self.assertEqual(obj[0][0].custom_role_id, sets.ZENDESK_ROLES['engineer'])
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordResetTestCase(TestCase):
|
||||||
|
fixtures = ['fixtures/test_make_engineer.json']
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.user = '123@test.ru'
|
||||||
|
self.email_backend = 'django.core.mail.backends.locmem.EmailBackend'
|
||||||
|
self.client = Client()
|
||||||
|
self.client.force_login(User.objects.get(email=self.user))
|
||||||
|
|
||||||
|
def test_redirect(self):
|
||||||
|
with self.settings(EMAIL_BACKEND=self.email_backend):
|
||||||
|
resp = self.client.post(reverse_lazy('password_reset'), data={'email': self.user})
|
||||||
|
self.assertRedirects(resp, reverse('password_reset_done'))
|
||||||
|
self.assertEqual(resp.status_code, 302)
|
||||||
|
|
||||||
|
def test_send_email(self):
|
||||||
|
with self.settings(EMAIL_BACKEND=self.email_backend):
|
||||||
|
response: HttpResponseRedirect = \
|
||||||
|
self.client.post(reverse_lazy('password_reset'), data={'email': self.user})
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(len(mail.outbox), 1)
|
||||||
|
self.assertEqual(mail.outbox[0].to, [self.user])
|
||||||
|
|
||||||
|
# context that the email template was rendered with
|
||||||
|
email_context = response.context[0].dicts[1]
|
||||||
|
correct_subject = render_to_string('registration/password_reset_subject.txt', email_context, response.request)
|
||||||
|
self.assertEqual(mail.outbox[0].subject, correct_subject.strip())
|
||||||
|
correct_body = render_to_string('registration/password_reset_email.html', email_context, response.request)
|
||||||
|
self.assertEqual(mail.outbox[0].body, correct_body)
|
||||||
|
|
||||||
|
def test_email_invalid(self):
|
||||||
|
with self.settings(EMAIL_BACKEND=self.email_backend) and translation.override('ru'):
|
||||||
|
resp = self.client.post(reverse_lazy('password_reset'), data={'email': 1})
|
||||||
|
self.assertContains(resp, 'Введите правильный адрес электронной почты.', count=1, status_code=200)
|
||||||
|
|
||||||
|
def test_user_does_not_exist(self):
|
||||||
|
with self.settings(EMAIL_BACKEND=self.email_backend):
|
||||||
|
resp = self.client.post(reverse_lazy('password_reset'), data={'email': self.user + str(random.random())})
|
||||||
|
self.assertRedirects(resp, reverse('password_reset_done'))
|
||||||
|
self.assertEqual(resp.status_code, 302)
|
||||||
|
self.assertEqual(len(mail.outbox), 0)
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordChangeTestCase(TestCase):
|
||||||
|
fixtures = ['fixtures/test_make_engineer.json']
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.user = '123@test.ru'
|
||||||
|
self.client = Client()
|
||||||
|
self.client.force_login(User.objects.get(email=self.user))
|
||||||
|
self.set_password()
|
||||||
|
|
||||||
|
def set_password(self):
|
||||||
|
user: User = User.objects.get(email=self.user)
|
||||||
|
user.set_password('ImpossiblyHardPassword')
|
||||||
|
user.save()
|
||||||
|
self.client.force_login(User.objects.get(email=self.user))
|
||||||
|
|
||||||
|
def test_change_successful(self):
|
||||||
|
self.client.post(
|
||||||
|
reverse_lazy('password_change'),
|
||||||
|
data={
|
||||||
|
'old_password': 'ImpossiblyHardPassword',
|
||||||
|
'new_password1': 'EasyPassword',
|
||||||
|
'new_password2': 'EasyPassword',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
user = User.objects.get(email=self.user)
|
||||||
|
self.assertTrue(user.check_password('EasyPassword'))
|
||||||
|
|
||||||
|
def test_invalid_old_password(self):
|
||||||
|
with translation.override('ru'):
|
||||||
|
resp = self.client.post(
|
||||||
|
reverse_lazy('password_change'),
|
||||||
|
data={
|
||||||
|
'old_password': 'EasyPassword',
|
||||||
|
'new_password1': 'EasyPassword',
|
||||||
|
'new_password2': 'EasyPassword',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.assertContains(resp, 'Ваш старый пароль введен неправильно', count=1, status_code=200)
|
||||||
|
|
||||||
|
def test_different_new_passwords(self):
|
||||||
|
with translation.override('ru'):
|
||||||
|
resp = self.client.post(
|
||||||
|
reverse_lazy('password_change'),
|
||||||
|
data={
|
||||||
|
'old_password': 'ImpossiblyHardPassword',
|
||||||
|
'new_password1': 'EasyPassword',
|
||||||
|
'new_password2': 'EasyPassword1',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.assertContains(resp, 'Введенные пароли не совпадают', count=1, status_code=200)
|
||||||
|
|
||||||
|
def test_invalid_new_password1(self):
|
||||||
|
with translation.override('ru'):
|
||||||
|
resp = self.client.post(
|
||||||
|
reverse_lazy('password_change'),
|
||||||
|
data={
|
||||||
|
'old_password': 'ImpossiblyHardPassword',
|
||||||
|
'new_password1': 'short',
|
||||||
|
'new_password2': 'short',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.assertContains(resp, 'Введённый пароль слишком короткий', count=1, status_code=200)
|
||||||
|
|
||||||
|
def test_invalid_new_password2(self):
|
||||||
|
with translation.override('ru'):
|
||||||
|
resp = self.client.post(
|
||||||
|
reverse_lazy('password_change'),
|
||||||
|
data={
|
||||||
|
'old_password': 'ImpossiblyHardPassword',
|
||||||
|
'new_password1': '123123123123123123132123123',
|
||||||
|
'new_password2': '123123123123123123132123123',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.assertContains(resp, 'Введённый пароль состоит только из цифр', count=1, status_code=200)
|
||||||
|
|
||||||
|
def test_invalid_new_password3(self):
|
||||||
|
with translation.override('ru'):
|
||||||
|
resp = self.client.post(
|
||||||
|
reverse_lazy('password_change'),
|
||||||
|
data={
|
||||||
|
'old_password': 'ImpossiblyHardPassword',
|
||||||
|
'new_password1': self.user,
|
||||||
|
'new_password2': self.user,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.assertContains(resp, 'Введённый пароль слишком похож на имя пользователя', count=1, status_code=200)
|
||||||
|
|
||||||
|
|
||||||
|
class GetTicketsTestCase(TestCase):
|
||||||
|
"""
|
||||||
|
Класс тестов для проверки функции получения тикетов.
|
||||||
|
"""
|
||||||
|
fixtures = ['fixtures/test_make_engineer.json']
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""
|
||||||
|
Предустановленные значения для проведения тестов.
|
||||||
|
"""
|
||||||
|
self.light_agent = '123@test.ru'
|
||||||
|
self.engineer = 'customer@example.com'
|
||||||
|
self.client = Client()
|
||||||
|
self.client.force_login(User.objects.get(email=self.engineer))
|
||||||
|
self.light_agent_client = Client()
|
||||||
|
self.light_agent_client.force_login(User.objects.get(email=self.light_agent))
|
||||||
|
|
||||||
|
@patch('main.views.zenpy.get_user')
|
||||||
|
@patch('main.extra_func.zenpy')
|
||||||
|
def test_redirect(self, ZenpyMock, GetUserMock):
|
||||||
|
"""
|
||||||
|
Функция проверки переадресации пользователя на рабочую страницу.
|
||||||
|
"""
|
||||||
|
GetUserMock.return_value = Mock()
|
||||||
|
user = User.objects.get(email=self.engineer)
|
||||||
|
resp = self.client.post(reverse('work_get_tickets'))
|
||||||
|
self.assertRedirects(resp, reverse('work', args=[user.id]))
|
||||||
|
self.assertEqual(resp.status_code, 302)
|
||||||
|
|
||||||
|
@patch('main.views.zenpy')
|
||||||
|
@patch('main.views.get_tickets_list_for_group')
|
||||||
|
def test_take_one_ticket(self, TicketsMock, ZenpyViewsMock):
|
||||||
|
"""
|
||||||
|
Функция проверки назначения одного тикета на engineer.
|
||||||
|
"""
|
||||||
|
TicketsMock.return_value = [Mock()]
|
||||||
|
ZenpyViewsMock.get_user.return_value = Mock(role='agent', custom_role_id=sets.ZENDESK_ROLES['engineer'])
|
||||||
|
self.client.post(reverse('work_get_tickets'), data={'count_tickets': 1})
|
||||||
|
tickets = ZenpyViewsMock.update_tickets.call_args
|
||||||
|
self.assertEqual(tickets[0][0][0].assignee, ZenpyViewsMock.get_user.return_value)
|
||||||
|
|
||||||
|
@patch('main.views.get_tickets_list_for_group')
|
||||||
|
@patch('main.views.zenpy')
|
||||||
|
def test_take_many_tickets(self, ZenpyMock, TicketsMock):
|
||||||
|
"""
|
||||||
|
Функция проверки назначения нескольких тикетов на engineer.
|
||||||
|
"""
|
||||||
|
TicketsMock.return_value = [Mock()] * 3
|
||||||
|
ZenpyMock.get_user.return_value = Mock(role='agent', custom_role_id=sets.ZENDESK_ROLES['engineer'])
|
||||||
|
self.client.post(reverse('work_get_tickets'), data={'count_tickets': 3})
|
||||||
|
tickets = ZenpyMock.update_tickets.call_args
|
||||||
|
for ticket in tickets[0][0]:
|
||||||
|
self.assertEqual(ticket.assignee, ZenpyMock.get_user.return_value)
|
||||||
|
|
||||||
|
@patch('main.views.zenpy.get_user')
|
||||||
|
@patch('main.views.zenpy')
|
||||||
|
def test_light_agent_take_ticket(self, ZenpyMock, GetUserMock):
|
||||||
|
"""
|
||||||
|
Функция проверки попытки назначения тикета на light_agent.
|
||||||
|
"""
|
||||||
|
GetUserMock.return_value = Mock(role='agent', custom_role_id=sets.ZENDESK_ROLES['light_agent'])
|
||||||
|
self.light_agent_client.post(reverse('work_get_tickets'), data={'count_tickets': 3})
|
||||||
|
tickets = ZenpyMock.update_tickets.call_args
|
||||||
|
self.assertIsNone(tickets)
|
||||||
|
|
||||||
|
@patch('main.views.zenpy')
|
||||||
|
@patch('main.views.get_tickets_list_for_group')
|
||||||
|
def test_take_zero_tickets(self, TicketsMock, ZenpyMock):
|
||||||
|
"""
|
||||||
|
Функция проверки попытки назначения нуля тикета на engineer.
|
||||||
|
"""
|
||||||
|
TicketsMock.return_value = [Mock()] * 3
|
||||||
|
ZenpyMock.get_user.return_value = Mock(role='agent', custom_role_id=sets.ZENDESK_ROLES['engineer'])
|
||||||
|
self.client.post(reverse('work_get_tickets'), data={'count_tickets': 0})
|
||||||
|
tickets = ZenpyMock.update_tickets.call_args[0][0]
|
||||||
|
self.assertListEqual(tickets, [])
|
||||||
|
|
||||||
|
@patch('main.views.get_tickets_list_for_group')
|
||||||
|
@patch('main.views.zenpy')
|
||||||
|
def test_take_invalid_count_tickets(self, ZenpyMock, TicketsMock, ):
|
||||||
|
"""
|
||||||
|
Функция проверки попытки назначения нуля тикетов на engineer.
|
||||||
|
"""
|
||||||
|
TicketsMock.return_value = [Mock()] * 3
|
||||||
|
ZenpyMock.get_user.return_value = Mock(role='agent', custom_role_id=sets.ZENDESK_ROLES['engineer'])
|
||||||
|
self.client.post(reverse('work_get_tickets'), data={'count_tickets': 'asd'})
|
||||||
|
tickets = ZenpyMock.update_tickets.call_args
|
||||||
|
self.assertIsNone(tickets)
|
||||||
|
|
||||||
|
|
||||||
|
class ProfileTestCase(TestCase):
|
||||||
|
"""
|
||||||
|
Класс тестов для проверки синхронизации профиля пользователя.
|
||||||
|
"""
|
||||||
|
fixtures = ['fixtures/profile.json']
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""
|
||||||
|
Предустановленные значения для проведения тестов.
|
||||||
|
"""
|
||||||
|
self.zendesk_agent_email = 'krav-88@mail.ru'
|
||||||
|
self.zendesk_admin_email = 'idar.sokurov.05@mail.ru'
|
||||||
|
self.client = Client()
|
||||||
|
self.client.force_login(User.objects.get(email=self.zendesk_agent_email))
|
||||||
|
self.admin_client = Client()
|
||||||
|
self.admin_client.force_login(User.objects.get(email=self.zendesk_admin_email))
|
||||||
|
|
||||||
|
def test_correct_username(self):
|
||||||
|
"""
|
||||||
|
Функция проверки синхронизации имени пользователя.
|
||||||
|
"""
|
||||||
|
resp = self.client.get(reverse('profile'))
|
||||||
|
self.assertEqual(resp.context['profile'].name, zenpy.get_user(self.zendesk_agent_email).name)
|
||||||
|
|
||||||
|
def test_correct_email(self):
|
||||||
|
"""
|
||||||
|
Функция проверки синхронизации почты пользователя.
|
||||||
|
"""
|
||||||
|
resp = self.client.get(reverse('profile'))
|
||||||
|
self.assertEqual(resp.context['profile'].user.email, zenpy.get_user(self.zendesk_agent_email).email)
|
||||||
|
|
||||||
|
def test_correct_role(self):
|
||||||
|
"""
|
||||||
|
Функция проверки синхронизации роли пользователя.
|
||||||
|
"""
|
||||||
|
resp = self.client.get(reverse('profile'))
|
||||||
|
self.assertEqual(resp.context['profile'].role, zenpy.get_user(self.zendesk_agent_email).role)
|
||||||
|
resp = self.admin_client.get(reverse('profile'))
|
||||||
|
self.assertEqual(resp.context['profile'].role, zenpy.get_user(self.zendesk_admin_email).role)
|
||||||
|
|
||||||
|
def test_correct_custom_role_id(self):
|
||||||
|
"""
|
||||||
|
Функция проверки синхронизации рабочей роли пользователя.
|
||||||
|
"""
|
||||||
|
resp = self.client.get(reverse('profile'))
|
||||||
|
user = zenpy.get_user(self.zendesk_agent_email)
|
||||||
|
self.assertEqual(resp.context['profile'].custom_role_id, user.custom_role_id if user.custom_role_id else 0)
|
||||||
|
resp = self.admin_client.get(reverse('profile'))
|
||||||
|
user = zenpy.get_user(self.zendesk_admin_email)
|
||||||
|
self.assertEqual(resp.context['profile'].custom_role_id, user.custom_role_id if user.custom_role_id else 0)
|
||||||
|
|
||||||
|
def test_correct_image(self):
|
||||||
|
"""
|
||||||
|
Функция проверки синхронизации изображения пользователя.
|
||||||
|
"""
|
||||||
|
resp = self.client.get(reverse('profile'))
|
||||||
|
user = zenpy.get_user(self.zendesk_agent_email)
|
||||||
|
self.assertEqual(resp.context['profile'].image, user.photo['content_url'] if user.photo else None)
|
||||||
|
@ -1,19 +1,18 @@
|
|||||||
from smtplib import SMTPException
|
from smtplib import SMTPException
|
||||||
from typing import Dict, Any
|
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.decorators import login_required
|
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.mixins import LoginRequiredMixin, PermissionRequiredMixin
|
||||||
from django.contrib.auth.models import User, Permission
|
from django.contrib.auth.models import User, Permission
|
||||||
from django.contrib.auth.tokens import default_token_generator
|
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.auth.views import LoginView
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
from django.contrib.messages.views import SuccessMessageMixin
|
||||||
from django.core.handlers.wsgi import WSGIRequest
|
from django.core.handlers.wsgi import WSGIRequest
|
||||||
from django.http import HttpResponseRedirect, HttpResponse
|
from django.http import HttpResponseRedirect, HttpResponse
|
||||||
from django.shortcuts import render, redirect
|
from django.shortcuts import render, redirect
|
||||||
from django.urls import reverse_lazy, reverse
|
from django.urls import reverse_lazy
|
||||||
from django.views.generic import FormView
|
from django.views.generic import FormView
|
||||||
from django_registration.views import RegistrationView
|
from django_registration.views import RegistrationView
|
||||||
# Django REST
|
# Django REST
|
||||||
@ -23,27 +22,13 @@ from rest_framework.response import Response
|
|||||||
from access_controller.settings import DEFAULT_FROM_EMAIL, ZENDESK_ROLES, ZENDESK_MAX_AGENTS, ZENDESK_GROUPS
|
from access_controller.settings import DEFAULT_FROM_EMAIL, ZENDESK_ROLES, ZENDESK_MAX_AGENTS, ZENDESK_GROUPS
|
||||||
from main.extra_func import check_user_exist, update_profile, get_user_organization, \
|
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, \
|
make_engineer, make_light_agent, get_users_list, update_users_in_model, count_users, \
|
||||||
log, set_session_params_for_work_page, get_tickets_list_for_group
|
set_session_params_for_work_page, get_tickets_list_for_group
|
||||||
from .statistic_data import StatisticData
|
from main.forms import AdminPageUsers, CustomRegistrationForm, CustomAuthenticationForm, StatisticForm, \
|
||||||
from main.zendesk_admin import zenpy
|
WorkGetTicketsForm
|
||||||
from main.requester import TicketListRequester
|
|
||||||
from main.forms import AdminPageUsers, CustomRegistrationForm, CustomAuthenticationForm, StatisticForm
|
|
||||||
from main.serializers import ProfileSerializer, ZendeskUserSerializer
|
from main.serializers import ProfileSerializer, ZendeskUserSerializer
|
||||||
|
from main.zendesk_admin import zenpy
|
||||||
from .models import UserProfile
|
from .models import UserProfile
|
||||||
|
from .statistic_data import StatisticData
|
||||||
|
|
||||||
def setup_context(profile_lit: bool = False, control_lit: bool = False, work_lit: bool = False,
|
|
||||||
registration_lit: bool = False, login_lit: bool = False, stats_lit: bool = False) -> Dict[str, Any]:
|
|
||||||
|
|
||||||
context = {
|
|
||||||
'profile_lit': profile_lit,
|
|
||||||
'control_lit': control_lit,
|
|
||||||
'work_lit': work_lit,
|
|
||||||
'registration_lit': registration_lit,
|
|
||||||
'login_lit': login_lit,
|
|
||||||
'stats_lit': stats_lit,
|
|
||||||
}
|
|
||||||
return context
|
|
||||||
|
|
||||||
|
|
||||||
class CustomRegistrationView(RegistrationView):
|
class CustomRegistrationView(RegistrationView):
|
||||||
@ -59,7 +44,6 @@ class CustomRegistrationView(RegistrationView):
|
|||||||
:param is_allowed: Определение зарегистрирован ли пользователь с введенным email на Zendesk и принадлежит ли он к организации SYSTEM
|
:param is_allowed: Определение зарегистрирован ли пользователь с введенным email на Zendesk и принадлежит ли он к организации SYSTEM
|
||||||
:type is_allowed: :class:`bool`
|
:type is_allowed: :class:`bool`
|
||||||
"""
|
"""
|
||||||
extra_context = setup_context(registration_lit=True)
|
|
||||||
form_class = CustomRegistrationForm
|
form_class = CustomRegistrationForm
|
||||||
template_name = 'django_registration/registration_form.html'
|
template_name = 'django_registration/registration_form.html'
|
||||||
urls = {
|
urls = {
|
||||||
@ -151,12 +135,11 @@ def profile_page(request: WSGIRequest) -> HttpResponse:
|
|||||||
"""
|
"""
|
||||||
user_profile: UserProfile = request.user.userprofile
|
user_profile: UserProfile = request.user.userprofile
|
||||||
update_profile(user_profile)
|
update_profile(user_profile)
|
||||||
context = setup_context(profile_lit=True)
|
context = {
|
||||||
context.update({
|
|
||||||
'profile': user_profile,
|
'profile': user_profile,
|
||||||
'pagename': 'Страница профиля',
|
'pagename': 'Страница профиля',
|
||||||
'ZENDESK_ROLES': ZENDESK_ROLES,
|
'ZENDESK_ROLES': ZENDESK_ROLES,
|
||||||
})
|
}
|
||||||
return render(request, 'pages/profile.html', context)
|
return render(request, 'pages/profile.html', context)
|
||||||
|
|
||||||
|
|
||||||
@ -188,14 +171,14 @@ def work_page(request: WSGIRequest, id: int) -> HttpResponse:
|
|||||||
engineers.append(user)
|
engineers.append(user)
|
||||||
elif user.custom_role_id == ZENDESK_ROLES['light_agent']:
|
elif user.custom_role_id == ZENDESK_ROLES['light_agent']:
|
||||||
light_agents.append(user)
|
light_agents.append(user)
|
||||||
context = setup_context(work_lit=True)
|
context = {
|
||||||
context.update({
|
|
||||||
'engineers': engineers,
|
'engineers': engineers,
|
||||||
'agents': light_agents,
|
'agents': light_agents,
|
||||||
'messages': messages.get_messages(request),
|
'messages': messages.get_messages(request),
|
||||||
'licences_remaining': max(0, ZENDESK_MAX_AGENTS - len(engineers)),
|
'licences_remaining': max(0, ZENDESK_MAX_AGENTS - len(engineers)),
|
||||||
'pagename': 'Управление правами',
|
'pagename': 'Управление правами',
|
||||||
})
|
'get_tickets_form': WorkGetTicketsForm()
|
||||||
|
}
|
||||||
return render(request, 'pages/work.html', context)
|
return render(request, 'pages/work.html', context)
|
||||||
return redirect("login")
|
return redirect("login")
|
||||||
|
|
||||||
@ -228,21 +211,18 @@ def work_become_engineer(request: WSGIRequest) -> HttpResponseRedirect:
|
|||||||
@login_required()
|
@login_required()
|
||||||
def work_get_tickets(request):
|
def work_get_tickets(request):
|
||||||
zenpy_user = zenpy.get_user(request.user.email)
|
zenpy_user = zenpy.get_user(request.user.email)
|
||||||
if zenpy_user.role == 'admin' or zenpy_user.custom_role_id == ZENDESK_ROLES['engineer']:
|
|
||||||
tickets = get_tickets_list_for_group(ZENDESK_GROUPS['buffer'])
|
if request.method == 'POST':
|
||||||
assigned_tickets = []
|
if zenpy_user.role == 'admin' or zenpy_user.custom_role_id == ZENDESK_ROLES['engineer']:
|
||||||
count = 0
|
form = WorkGetTicketsForm(request.POST)
|
||||||
for i in range(len(tickets)):
|
if form.is_valid():
|
||||||
if i == int(request.GET.get('count_tickets')):
|
tickets = get_tickets_list_for_group(ZENDESK_GROUPS['buffer'])
|
||||||
if assigned_tickets:
|
assigned_tickets = []
|
||||||
zenpy.admin.tickets.update(assigned_tickets)
|
for i in range(min(form.cleaned_data['count_tickets'], len(tickets))):
|
||||||
return set_session_params_for_work_page(request, count)
|
tickets[i].assignee = zenpy_user
|
||||||
tickets[i].assignee = zenpy_user
|
assigned_tickets.append(tickets[i])
|
||||||
assigned_tickets.append(tickets[i])
|
zenpy.update_tickets(assigned_tickets)
|
||||||
count += 1
|
return set_session_params_for_work_page(request, len(assigned_tickets))
|
||||||
if assigned_tickets:
|
|
||||||
zenpy.admin.tickets.update(assigned_tickets)
|
|
||||||
return set_session_params_for_work_page(request, count)
|
|
||||||
return set_session_params_for_work_page(request, is_confirm=False)
|
return set_session_params_for_work_page(request, is_confirm=False)
|
||||||
|
|
||||||
|
|
||||||
@ -311,7 +291,6 @@ class CustomLoginView(LoginView):
|
|||||||
"""
|
"""
|
||||||
Отображение страницы авторизации пользователя
|
Отображение страницы авторизации пользователя
|
||||||
"""
|
"""
|
||||||
extra_context = setup_context(login_lit=True)
|
|
||||||
form_class = CustomAuthenticationForm
|
form_class = CustomAuthenticationForm
|
||||||
|
|
||||||
|
|
||||||
@ -369,11 +348,10 @@ def statistic_page(request: WSGIRequest) -> HttpResponse:
|
|||||||
|
|
||||||
if not request.user.has_perm("main.has_control_access"):
|
if not request.user.has_perm("main.has_control_access"):
|
||||||
return redirect('index')
|
return redirect('index')
|
||||||
context = setup_context(stats_lit=True)
|
context = {
|
||||||
context.update({
|
|
||||||
'pagename': 'страница статистики',
|
'pagename': 'страница статистики',
|
||||||
'errors': list(),
|
'errors': list(),
|
||||||
})
|
}
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
form = StatisticForm(request.POST)
|
form = StatisticForm(request.POST)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
from typing import Optional, Dict
|
from typing import Optional, Dict, List
|
||||||
|
|
||||||
from zenpy import Zenpy
|
from zenpy import Zenpy
|
||||||
from zenpy.lib.api_objects import User as ZenpyUser, Group as ZenpyGroup
|
from zenpy.lib.api_objects import User as ZenpyUser, Group as ZenpyGroup, Ticket as ZenpyTicket
|
||||||
from zenpy.lib.exception import APIException
|
from zenpy.lib.exception import APIException
|
||||||
|
|
||||||
from access_controller.settings import ACTRL_ZENDESK_SUBDOMAIN, ACTRL_API_EMAIL, ACTRL_API_TOKEN, ACTRL_API_PASSWORD, \
|
from access_controller.settings import ACTRL_ZENDESK_SUBDOMAIN, ACTRL_API_EMAIL, ACTRL_API_TOKEN, ACTRL_API_PASSWORD, \
|
||||||
ZENDESK_GROUPS, SOLVED_TICKETS_EMAIL
|
ZENDESK_GROUPS, SOLVED_TICKETS_EMAIL
|
||||||
|
|
||||||
@ -28,6 +30,15 @@ class ZendeskAdmin:
|
|||||||
"""
|
"""
|
||||||
self.admin.users.update(user)
|
self.admin.users.update(user)
|
||||||
|
|
||||||
|
def update_tickets(self, tickets: List[ZenpyTicket]):
|
||||||
|
"""
|
||||||
|
Функция сохраняет изменение тикетов в Zendesk.
|
||||||
|
|
||||||
|
:param tickets: Тикеты с изменёнными данными
|
||||||
|
"""
|
||||||
|
if tickets:
|
||||||
|
self.admin.tickets.update(tickets)
|
||||||
|
|
||||||
def check_user(self, email: str) -> bool:
|
def check_user(self, email: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Функция осуществляет проверку существования пользователя в Zendesk по email.
|
Функция осуществляет проверку существования пользователя в Zendesk по email.
|
||||||
|
@ -14,3 +14,6 @@ sphinx-rtd-theme==0.5.1
|
|||||||
sphinx-autodoc-typehints==1.11.1
|
sphinx-autodoc-typehints==1.11.1
|
||||||
pyenchant==3.2.0
|
pyenchant==3.2.0
|
||||||
sphinxcontrib-spelling==7.1.0
|
sphinxcontrib-spelling==7.1.0
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
python-dotenv==0.17.1
|
||||||
|
Loading…
x
Reference in New Issue
Block a user