Merge branch 'develop' into feature/docker

This commit is contained in:
Iurii Tatishchev 2021-04-15 09:40:00 -07:00
parent 5387962406
commit cc08c0171b
Signed by: CaZzzer
GPG Key ID: 926BE949E29DCD03
30 changed files with 385 additions and 179 deletions

1
.gitignore vendored
View File

@ -13,6 +13,7 @@ db.sqlite3
db.sqlite3-journal db.sqlite3-journal
db/ db/
media/ media/
logs/
# If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/ # 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. # in your Git repository. Update and uncomment the following line accordingly.

View File

@ -10,10 +10,10 @@ For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.1/ref/settings/ https://docs.djangoproject.com/en/3.1/ref/settings/
""" """
import os import os
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'. # Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 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/
@ -91,7 +91,7 @@ WSGI_APPLICATION = 'access_controller.wsgi.application'
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.sqlite3', 'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(str(BASE_DIR),'db','zd_db.sqlite3'), 'NAME': BASE_DIR / 'db' / 'zd_db.sqlite3'
} }
} }
@ -147,41 +147,6 @@ AUTHENTICATION_BACKENDS = [
# Logging system # Logging system
# https://docs.djangoproject.com/en/3.1/topics/logging/ # 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 = { ZENDESK_ROLES = {

View File

@ -14,54 +14,30 @@ Including another URLconf
2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
""" """
from django.contrib import admin from django.contrib import admin
from django.contrib.auth import views as auth_views
from django.urls import path, include from django.urls import path, include
from main.views import main_page, profile_page, CustomRegistrationView, CustomLoginView from main.views import main_page, profile_page, CustomRegistrationView, CustomLoginView, registration_error
from main.views import work_page, work_hand_over, work_become_engineer, \ from main.views import work_page, work_hand_over, work_become_engineer, work_get_tickets, \
AdminPageView, statistic_page AdminPageView, statistic_page
from main.urls import router from main.urls import router
urlpatterns = [ urlpatterns = [
path('admin/', admin.site.urls, name='admin'), path('admin/', admin.site.urls, name='admin'),
path('', main_page, name='index'), path('', main_page, name='index'),
path('accounts/profile/', profile_page, name='profile'), path('accounts/profile/', profile_page, name='profile'),
path('accounts/register/', CustomRegistrationView.as_view(), name='registration'), 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/login/', CustomLoginView.as_view(), name='login'),
path('accounts/', include('django.contrib.auth.urls')), path('accounts/', include('django.contrib.auth.urls')),
path('accounts/', include('django_registration.backends.one_step.urls')),
path('work/<int:id>', work_page, name="work"), path('work/<int:id>', work_page, name="work"),
path('work/hand_over/', work_hand_over, name="work_hand_over"), path('work/hand_over/', work_hand_over, name="work_hand_over"),
path('work/become_engineer/', work_become_engineer, name="work_become_engineer"), 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/', include('django_registration.backends.activation.urls')),
path('accounts/login/', include('django.contrib.auth.urls')),
path('control/', AdminPageView.as_view(), name='control'), path('control/', AdminPageView.as_view(), name='control'),
path('statistic/', statistic_page, name='statistic') path('statistic/', statistic_page, name='statistic'),
] ]
urlpatterns += [
path(
'password_reset/',
auth_views.PasswordResetView.as_view(),
name='password_reset'
),
path(
'password-reset/done/',
auth_views.PasswordResetDoneView.as_view(),
name='password_reset_done'
),
path(
'reset/<uidb64>/<token>/',
auth_views.PasswordResetConfirmView.as_view(),
name='password_reset_confirm'
),
path(
'reset/done/',
auth_views.PasswordResetCompleteView.as_view(),
name='password_reset_complete'
),
]
# Django REST # Django REST
urlpatterns += [ urlpatterns += [

View File

@ -1,7 +1,7 @@
[ [
{ {
"model": "auth.user", "model": "auth.user",
"pk": 1, "pk": 3,
"fields": { "fields": {
"password": "pbkdf2_sha256$216000$gHBBCr1jBELf$ZkEDW3IEd8Wij7u8vkv+0Eze32CS01bcaYWhcD9OIC4=", "password": "pbkdf2_sha256$216000$gHBBCr1jBELf$ZkEDW3IEd8Wij7u8vkv+0Eze32CS01bcaYWhcD9OIC4=",
"last_login": null, "last_login": null,
@ -19,16 +19,16 @@
}, },
{ {
"model": "main.userprofile", "model": "main.userprofile",
"pk": 1, "pk": 3,
"fields": { "fields": {
"name": "ZendeskAdmin", "name": "ZendeskAdmin",
"user": 1, "user": 3,
"role": "admin" "role": "admin"
} }
}, },
{ {
"model": "auth.user", "model": "auth.user",
"pk": 2, "pk": 4,
"fields": { "fields": {
"password": "pbkdf2_sha256$216000$5qLJgrm2Quq9$KDBNNymVZXkUx0HKBPFst2m83kLe0egPBnkW7KnkORU=", "password": "pbkdf2_sha256$216000$5qLJgrm2Quq9$KDBNNymVZXkUx0HKBPFst2m83kLe0egPBnkW7KnkORU=",
"last_login": null, "last_login": null,
@ -46,10 +46,10 @@
}, },
{ {
"model": "main.userprofile", "model": "main.userprofile",
"pk": 2, "pk": 4,
"fields": { "fields": {
"name": "UserForAccessTest", "name": "UserForAccessTest",
"user": 2, "user": 4,
"role": "agent", "role": "agent",
"custom_role_id": "360005209000" "custom_role_id": "360005209000"
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

View File

@ -3,7 +3,7 @@
========================= =========================
****************************** ******************************
**Управление правами доступа** Управление правами доступа
****************************** ******************************
@ -22,7 +22,7 @@
* **"Войти"** - если Вы уже являетесь зарегистрированным пользователем * **"Войти"** - если Вы уже являетесь зарегистрированным пользователем
* **"Зарегистрироваться"** - при первом входе * **"Зарегистрироваться"** - при первом входе
.. image:: _static/main.png .. image:: _static/main_logout.png
Внимание! Для регистрации используется email с сайта Zendesk. Регистрация по каждому email Внимание! Для регистрации используется 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гю .. |copy| unicode:: 0xA9 .. Школа программистов S101, группа 2. 2021гю

View File

@ -80,6 +80,8 @@ API
functions functions
Serializer Serializer
Serializers Serializers
Сериализатор
переадресации

0
logs/.gitkeep Normal file
View File

View File

@ -1,3 +1,5 @@
import logging
import os
from datetime import timedelta, datetime, date from datetime import timedelta, datetime, date
from django.contrib.auth.models import User from django.contrib.auth.models import User
@ -6,6 +8,7 @@ from django.utils import timezone
from zenpy import Zenpy from zenpy import Zenpy
from zenpy.lib.exception import APIException from zenpy.lib.exception import APIException
from access_controller.settings import ZENDESK_ROLES as ROLES, ONE_DAY, ZENDESK_GROUPS, SOLVED_TICKETS_EMAIL, \ from access_controller.settings import ZENDESK_ROLES as ROLES, ONE_DAY, ZENDESK_GROUPS, SOLVED_TICKETS_EMAIL, \
ACTRL_API_EMAIL, ACTRL_API_TOKEN, ACTRL_API_PASSWORD, ACTRL_ZENDESK_SUBDOMAIN ACTRL_API_EMAIL, ACTRL_API_TOKEN, ACTRL_API_PASSWORD, ACTRL_ZENDESK_SUBDOMAIN
from main.models import UserProfile, RoleChangeLogs, UnassignedTicket, UnassignedTicketStatus from main.models import UserProfile, RoleChangeLogs, UnassignedTicket, UnassignedTicketStatus
@ -92,12 +95,12 @@ class ZendeskAdmin:
def get_group(self, name: str) -> str: def get_group(self, name: str) -> str:
""" """
Функция возвращает группы, к которым принадлежит пользователь. Функция возвращает группу по названию
:param name: Имя пользователя :param name: Имя пользователя
:return: Группы пользователя (в случае отсутствия None) :return: Группы пользователя (в случае отсутствия None)
""" """
groups = self.admin.search(name) groups = self.admin.search(name, type='group')
for group in groups: for group in groups:
return group return group
return None return None
@ -139,7 +142,7 @@ class ZendeskAdmin:
raise ValueError('invalid access_controller`s login data') 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 +165,6 @@ def make_engineer(user_profile: UserProfile, who_changes: User) -> UserProfile:
:param user_profile: Профиль пользователя :param user_profile: Профиль пользователя
:return: Вызов функции **update_role** с параметрами: профиль пользователя, роль "engineer" :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']) update_role(user_profile, ROLES['engineer'])
@ -191,13 +188,6 @@ def make_light_agent(user_profile: UserProfile, who_changes: User) -> UserProfil
ticket.assignee = None ticket.assignee = None
ticket.group = ZendeskAdmin().get_group(ZENDESK_GROUPS['buffer']) ticket.group = ZendeskAdmin().get_group(ZENDESK_GROUPS['buffer'])
ZendeskAdmin().admin.tickets.update(ticket) 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']) update_role(user_profile, ROLES['light_agent'])
@ -274,7 +264,7 @@ def check_user_auth(email: str, password: str) -> bool:
return True return True
def update_user_in_model(profile: UserProfile, zendesk_user: User) -> UserProfile: def update_user_in_model(profile: UserProfile, zendesk_user: User):
""" """
Функция обновляет профиль пользователя при изменении данных пользователя на Zendesk. Функция обновляет профиль пользователя при изменении данных пользователя на Zendesk.
@ -607,3 +597,72 @@ class StatisticData:
""" """
self.statistic.clear() self.statistic.clear()
self.fill_daterange(self.start_date, self.end_date + timedelta(days=1), 0) self.fill_daterange(self.start_date, self.end_date + timedelta(days=1), 0)
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)

View File

@ -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='Статус тикета'),
),
]

View File

@ -1,6 +1,7 @@
from django.contrib.auth.models import User from django.contrib.auth.models import User
from rest_framework import serializers from rest_framework import serializers
from main.models import UserProfile from main.models import UserProfile
from access_controller.settings import ZENDESK_ROLES
class UserSerializer(serializers.HyperlinkedModelSerializer): class UserSerializer(serializers.HyperlinkedModelSerializer):
@ -13,9 +14,25 @@ class UserSerializer(serializers.HyperlinkedModelSerializer):
class ProfileSerializer(serializers.HyperlinkedModelSerializer): class ProfileSerializer(serializers.HyperlinkedModelSerializer):
"""Сериализатор для модели профиля пользователя""" """Класс serializer для модели профиля пользователя"""
user = UserSerializer() user = UserSerializer()
class Meta: class Meta:
model = UserProfile model = UserProfile
fields = ['user', 'id', 'name', 'zendesk_role'] 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"

View File

@ -5,23 +5,48 @@
<nav class="navbar navbar-light" style="background-color: #113A60;"> <nav class="navbar navbar-light" 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" 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 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"> <div class="btn-group" role="group" aria-label="Basic example" style="margin-right: 9px">
<a class="btn btn-secondary" href="{% url 'profile' %}">Профиль</a> <a {% if profile_lit %}
class="btn btn-primary"
{% else %}
class="btn btn-secondary"
{% endif %}
href="{% url 'profile' %}">Профиль</a>
{% if perms.main.has_control_access %} {% if perms.main.has_control_access %}
<a class="btn btn-secondary" href="{% url 'control' %}">Управление</a> <a {% if control_lit %}
class="btn btn-primary"
{% else %}
class="btn btn-secondary"
{% endif %}
href="{% url 'control' %}">Управление</a>
{% else %} {% else %}
<a class="btn btn-secondary" href="{% url 'work' request.user.id %}">Запрос прав</a> <a {% if work_lit %}
class="btn btn-primary"
{% else %}
class="btn btn-secondary"
{% endif %}
href="{% url 'work' request.user.id %}">Запрос прав</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"> <div class="btn-group" role="group" aria-label="Basic example" style="margin-right: 9px">
<a class="btn btn-secondary" href="/accounts/login">Войти</a> <a {% if login_lit %}
<a class="btn btn-secondary" href="/accounts/register">Зарегистрироваться</a> class="btn btn-primary"
{% else %}
class="btn btn-secondary"
{% endif %}
href="/accounts/login">Войти</a>
<a {% if registration_lit %}
class="btn btn-primary"
{% else %}
class="btn btn-secondary"
{% endif %}
href="/accounts/register">Зарегистрироваться</a>
</div> </div>
{% endif %} {% endif %}
</nav> </nav>

View File

@ -0,0 +1,15 @@
{% extends 'base/base.html' %}
{% load static %}
{% block title %}
Регистрация завершена
{% endblock %}
{% block heading %}
Регистрация
{% endblock %}
{% block content %}
<br>
<h4> Произошла ошибка при отправке электронного сообщения.</h4>
{% endblock %}

View File

@ -34,7 +34,7 @@
<h6 class="table-title">Список сотрудников</h6> <h6 class="table-title">Список сотрудников</h6>
{% block table %} {% block table %}
<table class="light-table"> <table class="table table-dark light-table">
<thead> <thead>
<th>Name</th> <th>Name</th>
@ -42,8 +42,8 @@
<th>Role</th> <th>Role</th>
<th>Checked</th> <th>Checked</th>
</thead> </thead>
<tbody id="tbody"> <tbody id="tbody"></tbody>
</tbody>
</table> </table>
<p id="loading">Данные загружаются...</p> <p id="loading">Данные загружаются...</p>
{% endblock %} {% endblock %}

View File

@ -60,6 +60,12 @@
<a href="/work/become_engineer" class="request-acess-button default-button">Получить права инженера</a> <a href="/work/become_engineer" class="request-acess-button default-button">Получить права инженера</a>
<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">
<form method="GET" action="/work/get_tickets">
<input class="form-control mb-3" type="number" min="1" value="1" name="count_tickets">
<button type="submit" class="default-button">Взять тикеты в работу</button>
</form>
</div>
</div> </div>
{% include 'base/success_messages.html' %} {% include 'base/success_messages.html' %}
</div> </div>

View File

@ -1,40 +1,46 @@
import logging from smtplib import SMTPException
import os
from datetime import datetime
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.exceptions import PermissionDenied
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, get_list_or_404, redirect from django.shortcuts import render, redirect, get_list_or_404
from django.urls import reverse_lazy, reverse from django.urls import reverse_lazy, reverse
from django.views.generic import FormView from django.views.generic import FormView
from django_registration.views import RegistrationView from django_registration.views import RegistrationView
from django.contrib import messages
# Django REST # Django REST
from rest_framework import viewsets from rest_framework import viewsets
from rest_framework.response import Response from rest_framework.response import Response
from zenpy.lib.api_objects import User as ZenpyUser from zenpy.lib.api_objects import User as ZenpyUser
from access_controller.settings import DEFAULT_FROM_EMAIL, ZENDESK_ROLES, ZENDESK_MAX_AGENTS from access_controller.settings import DEFAULT_FROM_EMAIL, ZENDESK_ROLES, ZENDESK_MAX_AGENTS
from main.extra_func import ZendeskAdmin
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, \
StatisticData StatisticData, log, ZendeskAdmin
from main.forms import AdminPageUsers, CustomRegistrationForm, CustomAuthenticationForm, StatisticForm from main.forms import AdminPageUsers, CustomRegistrationForm, CustomAuthenticationForm, StatisticForm
from main.serializers import ProfileSerializer from main.serializers import ProfileSerializer, ZendeskUserSerializer
from .models import UserProfile 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):
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): class CustomRegistrationView(RegistrationView):
""" """
Отображение и логика работы страницы регистрации пользователя. Отображение и логика работы страницы регистрации пользователя.
@ -48,10 +54,15 @@ 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'
success_url = reverse_lazy('django_registration_complete') urls = {
is_allowed = True '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: def register(self, form: CustomRegistrationForm) -> User:
""" """
@ -64,7 +75,7 @@ class CustomRegistrationView(RegistrationView):
:param form: Email пользователя на Zendesk :param form: Email пользователя на Zendesk
:return: user :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': if check_user_exist(form.data['email']) and get_user_organization(form.data['email']) == 'SYSTEM':
forms = PasswordResetForm(self.request.POST) forms = PasswordResetForm(self.request.POST)
if forms.is_valid(): if forms.is_valid():
@ -83,14 +94,17 @@ class CustomRegistrationView(RegistrationView):
email=form.data['email'], email=form.data['email'],
password=User.objects.make_random_password(length=50) password=User.objects.make_random_password(length=50)
) )
forms.save(**opts) try:
update_profile(user.userprofile) update_profile(user.userprofile)
self.set_permission(user) self.set_permission(user)
return user forms.save(**opts)
return user
except SMTPException:
self.redirect_url = 'email_sending_error'
else: else:
raise ValueError('Непредвиденная ошибка') raise ValueError('Непредвиденная ошибка')
else: else:
self.is_allowed = False self.redirect_url = 'invalid_zendesk_email'
@staticmethod @staticmethod
def set_permission(user: User) -> None: def set_permission(user: User) -> None:
@ -107,7 +121,7 @@ class CustomRegistrationView(RegistrationView):
) )
user.user_permissions.add(permission) user.user_permissions.add(permission)
def get_success_url(self, user: User = None) -> success_url: def get_success_url(self, user: User = None):
""" """
Функция возвращает url-адрес страницы, куда нужно перейти после успешной/не успешной регистрации. Функция возвращает url-адрес страницы, куда нужно перейти после успешной/не успешной регистрации.
Используется самой django-registration. Используется самой django-registration.
@ -115,10 +129,11 @@ class CustomRegistrationView(RegistrationView):
:param user: пользователь, пытающийся зарегистрироваться :param user: пользователь, пытающийся зарегистрироваться
:return: адресация на страницу успешной регистрации :return: адресация на страницу успешной регистрации
""" """
if self.is_allowed: return self.urls[self.redirect_url]
return reverse_lazy('password_reset_done')
else:
return reverse_lazy('django_registration_disallowed') def registration_error(request):
return render(request, 'django_registration/registration_error.html')
@login_required() @login_required()
@ -131,11 +146,12 @@ 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 = { context = setup_context(profile_lit=True)
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)
@ -171,33 +187,18 @@ def work_page(request: WSGIRequest, id: int) -> HttpResponse:
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 = { context = setup_context(work_lit=True)
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': 'Управление правами'
} })
return render(request, 'pages/work.html', context) return render(request, 'pages/work.html', context)
return redirect("login") return redirect("login")
def user_update(zenpy_user: User, admin: User, request: WSGIRequest) -> UserProfile:
"""
Функция устанавливает пользователю роль "agent" (изменяет профиль).
:param zenpy_user: Пользователь Zendesk
:param admin: Пользователь
:param request: Запрос установки роли "agent" в Userprofile
:return: Обновленный профиль пользователя
"""
admin.users.update(zenpy_user)
request.user.userprofile.role = "agent"
request.user.userprofile.save()
messages.success(request, "Права были изменены")
@login_required() @login_required()
def work_hand_over(request: WSGIRequest) -> HttpResponseRedirect: def work_hand_over(request: WSGIRequest) -> HttpResponseRedirect:
""" """
@ -207,10 +208,7 @@ def work_hand_over(request: WSGIRequest) -> HttpResponseRedirect:
:param request: данные текущего пользователя (login_required) :param request: данные текущего пользователя (login_required)
:return: перезагрузка текущей страницы после выполнения смены роли :return: перезагрузка текущей страницы после выполнения смены роли
""" """
zenpy_user, admin = auth_user(request) make_light_agent(request.user.userprofile,request.user)
if zenpy_user.custom_role_id == ZENDESK_ROLES['engineer']:
zenpy_user.custom_role_id = ZENDESK_ROLES['light_agent']
user_update(zenpy_user, admin, request)
return HttpResponseRedirect(reverse('work', args=(request.user.id,))) return HttpResponseRedirect(reverse('work', args=(request.user.id,)))
@ -223,17 +221,31 @@ def work_become_engineer(request: WSGIRequest) -> HttpResponseRedirect:
:return: перезагрузка текущей страницы после выполнения смены роли :return: перезагрузка текущей страницы после выполнения смены роли
""" """
zenpy_user, admin = auth_user(request) zenpy_user, admin = auth_user(request)
if zenpy_user.custom_role_id == ZENDESK_ROLES['light_agent']:
zenpy_user.custom_role_id = ZENDESK_ROLES['engineer'] make_engineer(request.user.userprofile,request.user)
user_update(zenpy_user, admin, request) 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,))) return HttpResponseRedirect(reverse('work', args=(request.user.id,)))
def main_page(request): def main_page(request: WSGIRequest) -> HttpResponse:
"""
Функция переадресации на главную страницу.
"""
return render(request, 'pages/index.html') return render(request, 'pages/index.html')
class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin,SuccessMessageMixin, FormView): class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, FormView):
""" """
Класс отображения страницы администратора. Класс отображения страницы администратора.
@ -275,25 +287,49 @@ class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin,SuccessMessageMi
""" """
for user in users: for user in users:
make_engineer(user, self.request.user) make_engineer(user, self.request.user)
log(user, self.request.user.userprofile)
def make_light_agents(self, users): def make_light_agents(self, users):
"""
Функция проходит по списку пользователей, проставляя статус "light agent".
:param users: Список пользователей
:return: Обновленный список пользователей
"""
for user in users: for user in users:
make_light_agent(user, self.request.user) make_light_agent(user, self.request.user)
log(user, self.request.user.userprofile)
def get_context_data(self, **kwargs) -> dict: 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['engineers'], context['light_agents'] = count_users(get_users_list()) context['engineers'], context['light_agents'] = count_users(get_users_list())
context['licences_remaining'] = max(0, ZENDESK_MAX_AGENTS - context['engineers']) context.update({
return context '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): class CustomLoginView(LoginView):
""" """
Отображение страницы авторизации пользователя Отображение страницы авторизации пользователя
""" """
extra_context = setup_context(login_lit=True)
form_class = CustomAuthenticationForm form_class = CustomAuthenticationForm
@ -305,16 +341,34 @@ class UsersViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = ProfileSerializer serializer_class = ProfileSerializer
def list(self, request, *args, **kwargs): def list(self, request, *args, **kwargs):
users = update_users_in_model().values users = update_users_in_model()
count = count_users(users) count = count_users(users.values)
profiles = UserProfile.objects.filter(role='agent') profiles = UserProfile.objects.filter(role='agent')
serializer = self.get_serializer(profiles, many=True) serializer = self.get_serializer(profiles, many=True)
return Response({ res = {
'users': serializer.data, 'users': serializer.data,
'engineers': count[0], '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() @login_required()
@ -325,12 +379,18 @@ def statistic_page(request: WSGIRequest) -> HttpResponse:
:param request: данные о пользователе: email, время и интервал работы. Данные получаем через forms.StatisticForm :param request: данные о пользователе: email, время и интервал работы. Данные получаем через forms.StatisticForm
:return: адресация на страницу статистики :return: адресация на страницу статистики
""" """
if not request.user.has_perm('main.has_control_access'):
return PermissionDenied # if not request.user.has_perm('main.has_control_access'):
context = { # raise PermissionDenied
# context = {
if not request.user.is_superuser:
return redirect('index')
context = setup_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():

View File

@ -6,7 +6,8 @@ import sys
def main(): def main():
"""Run administrative tasks.""" """Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'access_controller.settings') os.environ.setdefault('DJANGO_SETTINGS_MODULE',
'access_controller.settings')
try: try:
from django.core.management import execute_from_command_line from django.core.management import execute_from_command_line
except ImportError as exc: except ImportError as exc:

View File

@ -124,4 +124,7 @@
padding: 10px; padding: 10px;
background: #3B91D4; background: #3B91D4;
color: white; color: white;
width: 100%;
} }

View File

@ -1,10 +1,10 @@
"use strict"; "use strict";
// React // React
class TableRow extends React.Component { class ModelUserTableRow extends React.Component {
render() { render() {
return ( return (
<tr> <tr className={"table-dark"}>
<td> <td>
<a href="#">{this.props.user.name}</a> <a href="#">{this.props.user.name}</a>
</td> </td>
@ -23,6 +23,43 @@ class TableRow extends React.Component {
} }
} }
class ModelUserTableRows extends React.Component {
render() {
return ReactDOM.createPortal(
this.props.users.map((user, key) => (
<ModelUserTableRow user={user} key={key} />
)),
document.getElementById("tbody")
);
}
}
class ZendeskUserTableRow extends React.Component {
render() {
return (
<tr className={"table-secondary"}>
<td>
<a href="#">{this.props.user.name}</a>
</td>
<td>{this.props.user.email}</td>
<td>{this.props.user.zendesk_role}</td>
<td></td>
</tr>
);
}
}
class ZendeskUserTableRows extends React.Component {
render() {
return ReactDOM.createPortal(
this.props.users.map((user, key) => (
<ZendeskUserTableRow user={user} key={key} />
)),
document.getElementById("tbody")
);
}
}
class TableBody extends React.Component { class TableBody extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
@ -30,6 +67,7 @@ class TableBody extends React.Component {
users: [], users: [],
engineers: 0, engineers: 0,
light_agents: 0, light_agents: 0,
zendesk_users: [],
}; };
} }
@ -39,6 +77,7 @@ class TableBody extends React.Component {
users: response.data.users, users: response.data.users,
engineers: response.data.engineers, engineers: response.data.engineers,
light_agents: response.data.light_agents, light_agents: response.data.light_agents,
zendesk_users: response.data.zendesk_users,
}); });
let elements = document.querySelectorAll(".info-quantity-value"); let elements = document.querySelectorAll(".info-quantity-value");
elements[0].innerHTML = this.state.engineers; elements[0].innerHTML = this.state.engineers;
@ -62,9 +101,12 @@ class TableBody extends React.Component {
} }
render() { render() {
return this.state.users.map((user, key) => ( return (
<TableRow user={user} key={key} /> <tr>
)); <ModelUserTableRows users={this.state.users} />
<ZendeskUserTableRows users={this.state.zendesk_users} />
</tr>
);
} }
} }