Merge branch 'develop' into feature/docker
1
.gitignore
vendored
@ -13,6 +13,7 @@ db.sqlite3
|
||||
db.sqlite3-journal
|
||||
db/
|
||||
media/
|
||||
logs/
|
||||
|
||||
# If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/
|
||||
# in your Git repository. Update and uncomment the following line accordingly.
|
||||
|
@ -10,10 +10,10 @@ For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/3.1/ref/settings/
|
||||
"""
|
||||
import os
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
# 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
|
||||
# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/
|
||||
@ -91,7 +91,7 @@ WSGI_APPLICATION = 'access_controller.wsgi.application'
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'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
|
||||
# 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 = {
|
||||
|
@ -14,54 +14,30 @@ Including another URLconf
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth import views as auth_views
|
||||
from django.urls import path, include
|
||||
|
||||
from main.views import main_page, profile_page, CustomRegistrationView, CustomLoginView
|
||||
from main.views import work_page, work_hand_over, work_become_engineer, \
|
||||
from main.views import main_page, profile_page, CustomRegistrationView, CustomLoginView, registration_error
|
||||
from main.views import work_page, work_hand_over, work_become_engineer, work_get_tickets, \
|
||||
AdminPageView, statistic_page
|
||||
from main.urls import router
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls, name='admin'),
|
||||
path('', main_page, name='index'),
|
||||
path('accounts/profile/', profile_page, name='profile'),
|
||||
path('accounts/register/', CustomRegistrationView.as_view(), name='registration'),
|
||||
path('accounts/register/error/', registration_error, name='registration_email_error'),
|
||||
path('accounts/login/', CustomLoginView.as_view(), name='login'),
|
||||
path('accounts/', include('django.contrib.auth.urls')),
|
||||
path('accounts/', include('django_registration.backends.one_step.urls')),
|
||||
path('work/<int:id>', work_page, name="work"),
|
||||
path('work/hand_over/', work_hand_over, name="work_hand_over"),
|
||||
path('work/become_engineer/', work_become_engineer, name="work_become_engineer"),
|
||||
path('work/get_tickets', work_get_tickets, name='work_get_tickets'),
|
||||
path('accounts/', include('django_registration.backends.activation.urls')),
|
||||
path('accounts/login/', include('django.contrib.auth.urls')),
|
||||
path('control/', AdminPageView.as_view(), name='control'),
|
||||
path('statistic/', statistic_page, name='statistic')
|
||||
path('statistic/', statistic_page, name='statistic'),
|
||||
]
|
||||
|
||||
urlpatterns += [
|
||||
path(
|
||||
'password_reset/',
|
||||
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
|
||||
urlpatterns += [
|
||||
|
12
data.json
@ -1,7 +1,7 @@
|
||||
[
|
||||
{
|
||||
"model": "auth.user",
|
||||
"pk": 1,
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"password": "pbkdf2_sha256$216000$gHBBCr1jBELf$ZkEDW3IEd8Wij7u8vkv+0Eze32CS01bcaYWhcD9OIC4=",
|
||||
"last_login": null,
|
||||
@ -19,16 +19,16 @@
|
||||
},
|
||||
{
|
||||
"model": "main.userprofile",
|
||||
"pk": 1,
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"name": "ZendeskAdmin",
|
||||
"user": 1,
|
||||
"user": 3,
|
||||
"role": "admin"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "auth.user",
|
||||
"pk": 2,
|
||||
"pk": 4,
|
||||
"fields": {
|
||||
"password": "pbkdf2_sha256$216000$5qLJgrm2Quq9$KDBNNymVZXkUx0HKBPFst2m83kLe0egPBnkW7KnkORU=",
|
||||
"last_login": null,
|
||||
@ -46,10 +46,10 @@
|
||||
},
|
||||
{
|
||||
"model": "main.userprofile",
|
||||
"pk": 2,
|
||||
"pk": 4,
|
||||
"fields": {
|
||||
"name": "UserForAccessTest",
|
||||
"user": 2,
|
||||
"user": 4,
|
||||
"role": "agent",
|
||||
"custom_role_id": "360005209000"
|
||||
}
|
||||
|
BIN
docs/source/_static/admin_manage.png
Normal file
After Width: | Height: | Size: 85 KiB |
BIN
docs/source/_static/admin_manage_done.png
Normal file
After Width: | Height: | Size: 84 KiB |
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 63 KiB |
Before Width: | Height: | Size: 53 KiB |
BIN
docs/source/_static/main_logout.png
Normal file
After Width: | Height: | Size: 100 KiB |
Before Width: | Height: | Size: 73 KiB |
Before Width: | Height: | Size: 76 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 65 KiB |
BIN
docs/source/_static/request.png
Normal file
After Width: | Height: | Size: 73 KiB |
BIN
docs/source/_static/role_change.png
Normal file
After Width: | Height: | Size: 63 KiB |
@ -3,7 +3,7 @@
|
||||
=========================
|
||||
|
||||
******************************
|
||||
**Управление правами доступа**
|
||||
Управление правами доступа
|
||||
******************************
|
||||
|
||||
|
||||
@ -22,7 +22,7 @@
|
||||
* **"Войти"** - если Вы уже являетесь зарегистрированным пользователем
|
||||
* **"Зарегистрироваться"** - при первом входе
|
||||
|
||||
.. image:: _static/main.png
|
||||
.. image:: _static/main_logout.png
|
||||
|
||||
Внимание! Для регистрации используется email с сайта Zendesk. Регистрация по каждому email
|
||||
возможна один раз
|
||||
@ -32,7 +32,7 @@
|
||||
* **"Профиль"** - просмотреть свои данные и запросить права доступа
|
||||
* **"Запрос прав"** - получение прав для работы с тикетами или **"Управление"** - доступно для администратора и предоставляет возможность группового назначения ролей пользователям
|
||||
|
||||
.. image:: _static/main_logined_agent.png
|
||||
.. image:: _static/main.png
|
||||
|
||||
*************
|
||||
Регистрация
|
||||
@ -72,7 +72,11 @@
|
||||
На странице запроса прав Вам доступна информация о количестве и списке работающих над тикетами сотрудников,
|
||||
а также возможность сдать и запросить права.
|
||||
|
||||
.. image:: _static/permission_request.png
|
||||
.. image:: _static/request.png
|
||||
|
||||
Успешное изменение прав:
|
||||
|
||||
.. image:: _static/role_change.png
|
||||
|
||||
******************************************
|
||||
Управление правами доступа администратором
|
||||
@ -84,6 +88,10 @@
|
||||
* Количество и список инженеров и легких агентов
|
||||
* Возможность группового назначения прав с использованием чек-боксов
|
||||
|
||||
.. image:: _static/permission_management.png
|
||||
.. image:: _static/admin_manage.png
|
||||
|
||||
Изменение прав пользователей наглядно отразится в таблице пользователей:
|
||||
|
||||
.. image:: _static/admin_manage_done.png
|
||||
|
||||
.. |copy| unicode:: 0xA9 .. Школа программистов S101, группа 2. 2021гю
|
||||
|
@ -80,6 +80,8 @@ API
|
||||
functions
|
||||
Serializer
|
||||
Serializers
|
||||
Сериализатор
|
||||
переадресации
|
||||
|
||||
|
||||
|
||||
|
0
logs/.gitkeep
Normal file
@ -1,3 +1,5 @@
|
||||
import logging
|
||||
import os
|
||||
from datetime import timedelta, datetime, date
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
@ -6,6 +8,7 @@ from django.utils import timezone
|
||||
from zenpy import Zenpy
|
||||
from zenpy.lib.exception import APIException
|
||||
|
||||
|
||||
from access_controller.settings import ZENDESK_ROLES as ROLES, ONE_DAY, ZENDESK_GROUPS, SOLVED_TICKETS_EMAIL, \
|
||||
ACTRL_API_EMAIL, ACTRL_API_TOKEN, ACTRL_API_PASSWORD, ACTRL_ZENDESK_SUBDOMAIN
|
||||
from main.models import UserProfile, RoleChangeLogs, UnassignedTicket, UnassignedTicketStatus
|
||||
@ -92,12 +95,12 @@ class ZendeskAdmin:
|
||||
|
||||
def get_group(self, name: str) -> str:
|
||||
"""
|
||||
Функция возвращает группы, к которым принадлежит пользователь.
|
||||
Функция возвращает группу по названию
|
||||
|
||||
:param name: Имя пользователя
|
||||
:return: Группы пользователя (в случае отсутствия None)
|
||||
"""
|
||||
groups = self.admin.search(name)
|
||||
groups = self.admin.search(name, type='group')
|
||||
for group in groups:
|
||||
return group
|
||||
return None
|
||||
@ -139,7 +142,7 @@ class ZendeskAdmin:
|
||||
raise ValueError('invalid access_controller`s login data')
|
||||
|
||||
|
||||
def update_role(user_profile: UserProfile, role: str) -> UserProfile:
|
||||
def update_role(user_profile: UserProfile, role: int) -> UserProfile:
|
||||
"""
|
||||
Функция меняет роль пользователя.
|
||||
|
||||
@ -162,12 +165,6 @@ def make_engineer(user_profile: UserProfile, who_changes: User) -> UserProfile:
|
||||
:param user_profile: Профиль пользователя
|
||||
:return: Вызов функции **update_role** с параметрами: профиль пользователя, роль "engineer"
|
||||
"""
|
||||
RoleChangeLogs.objects.create(
|
||||
user=user_profile.user,
|
||||
old_role=user_profile.custom_role_id,
|
||||
new_role=ROLES['engineer'],
|
||||
changed_by=who_changes
|
||||
)
|
||||
update_role(user_profile, ROLES['engineer'])
|
||||
|
||||
|
||||
@ -191,13 +188,6 @@ def make_light_agent(user_profile: UserProfile, who_changes: User) -> UserProfil
|
||||
ticket.assignee = None
|
||||
ticket.group = ZendeskAdmin().get_group(ZENDESK_GROUPS['buffer'])
|
||||
ZendeskAdmin().admin.tickets.update(ticket)
|
||||
|
||||
RoleChangeLogs.objects.create(
|
||||
user=user_profile.user,
|
||||
old_role=user_profile.custom_role_id,
|
||||
new_role=ROLES['light_agent'],
|
||||
changed_by=who_changes
|
||||
)
|
||||
update_role(user_profile, ROLES['light_agent'])
|
||||
|
||||
|
||||
@ -274,7 +264,7 @@ def check_user_auth(email: str, password: str) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def update_user_in_model(profile: UserProfile, zendesk_user: User) -> UserProfile:
|
||||
def update_user_in_model(profile: UserProfile, zendesk_user: User):
|
||||
"""
|
||||
Функция обновляет профиль пользователя при изменении данных пользователя на Zendesk.
|
||||
|
||||
@ -607,3 +597,72 @@ class StatisticData:
|
||||
"""
|
||||
self.statistic.clear()
|
||||
self.fill_daterange(self.start_date, self.end_date + timedelta(days=1), 0)
|
||||
|
||||
|
||||
class DatabaseHandler(logging.Handler):
|
||||
def __init__(self):
|
||||
logging.Handler.__init__(self)
|
||||
|
||||
def emit(self, record):
|
||||
database = RoleChangeLogs()
|
||||
users = record.msg
|
||||
if users[1]:
|
||||
user = users[0]
|
||||
admin = users[1]
|
||||
elif not users[1]:
|
||||
user = users[0]
|
||||
admin = users[0]
|
||||
database.name = user.name
|
||||
database.user = user.user
|
||||
database.changed_by = admin.user
|
||||
if user.custom_role_id == ROLES['engineer']:
|
||||
database.old_role = ROLES['light_agent']
|
||||
elif user.custom_role_id == ROLES['light_agent']:
|
||||
database.old_role = ROLES['engineer']
|
||||
database.new_role = user.custom_role_id
|
||||
database.save()
|
||||
|
||||
|
||||
class CsvFormatter(logging.Formatter):
|
||||
def __init__(self):
|
||||
logging.Formatter.__init__(self)
|
||||
|
||||
def format(self, record):
|
||||
users = record.msg
|
||||
if users[1]:
|
||||
user = users[0]
|
||||
admin = users[1]
|
||||
elif not users[1]:
|
||||
user = users[0]
|
||||
admin = users[0]
|
||||
msg = ''
|
||||
msg += user.name
|
||||
if user.custom_role_id == ROLES['engineer']:
|
||||
msg += ',engineer,'
|
||||
elif user.custom_role_id == ROLES['light_agent']:
|
||||
msg += ',light_agent,'
|
||||
time = str(timezone.now().today())
|
||||
msg += time[:16]
|
||||
msg += ','
|
||||
msg += admin.name
|
||||
return msg
|
||||
|
||||
|
||||
def log(user, admin=0):
|
||||
"""
|
||||
Осуществляет запись логов в базу данных и csv файл
|
||||
:param admin:
|
||||
:param user:
|
||||
:return:
|
||||
"""
|
||||
users = [user, admin]
|
||||
logger = logging.getLogger('MY_LOGGER')
|
||||
if not logger.hasHandlers():
|
||||
dbhandler = DatabaseHandler()
|
||||
csvformatter = CsvFormatter()
|
||||
csvhandler = logging.FileHandler('logs/logs.csv', "a")
|
||||
csvhandler.setFormatter(csvformatter)
|
||||
logger.addHandler(dbhandler)
|
||||
logger.addHandler(csvhandler)
|
||||
logger.setLevel('INFO')
|
||||
logger.info(users)
|
||||
|
26
main/migrations/0017_auto_20210408_1943.py
Normal 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='Статус тикета'),
|
||||
),
|
||||
]
|
@ -1,6 +1,7 @@
|
||||
from django.contrib.auth.models import User
|
||||
from rest_framework import serializers
|
||||
from main.models import UserProfile
|
||||
from access_controller.settings import ZENDESK_ROLES
|
||||
|
||||
|
||||
class UserSerializer(serializers.HyperlinkedModelSerializer):
|
||||
@ -13,9 +14,25 @@ class UserSerializer(serializers.HyperlinkedModelSerializer):
|
||||
|
||||
|
||||
class ProfileSerializer(serializers.HyperlinkedModelSerializer):
|
||||
"""Сериализатор для модели профиля пользователя"""
|
||||
"""Класс serializer для модели профиля пользователя"""
|
||||
user = UserSerializer()
|
||||
|
||||
class Meta:
|
||||
model = UserProfile
|
||||
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"
|
||||
|
@ -5,23 +5,48 @@
|
||||
|
||||
<nav class="navbar navbar-light" style="background-color: #113A60;">
|
||||
<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>
|
||||
</a>
|
||||
{% if request.user.is_authenticated %}
|
||||
<div class="btn-group" role="group" aria-label="Basic example">
|
||||
<a class="btn btn-secondary" href="{% url 'profile' %}">Профиль</a>
|
||||
<div class="btn-group" role="group" aria-label="Basic example" style="margin-right: 9px">
|
||||
<a {% if profile_lit %}
|
||||
class="btn btn-primary"
|
||||
{% else %}
|
||||
class="btn btn-secondary"
|
||||
{% endif %}
|
||||
href="{% url 'profile' %}">Профиль</a>
|
||||
{% 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 %}
|
||||
<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 %}
|
||||
<a class="btn btn-secondary" href="{% url 'logout' %}">Выйти</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="btn-group" role="group" aria-label="Basic example">
|
||||
<a class="btn btn-secondary" href="/accounts/login">Войти</a>
|
||||
<a class="btn btn-secondary" href="/accounts/register">Зарегистрироваться</a>
|
||||
<div class="btn-group" role="group" aria-label="Basic example" style="margin-right: 9px">
|
||||
<a {% if login_lit %}
|
||||
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>
|
||||
{% endif %}
|
||||
</nav>
|
||||
|
15
main/templates/django_registration/registration_error.html
Normal file
@ -0,0 +1,15 @@
|
||||
{% extends 'base/base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}
|
||||
Регистрация завершена
|
||||
{% endblock %}
|
||||
|
||||
{% block heading %}
|
||||
Регистрация
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<br>
|
||||
<h4> Произошла ошибка при отправке электронного сообщения.</h4>
|
||||
{% endblock %}
|
@ -34,7 +34,7 @@
|
||||
<h6 class="table-title">Список сотрудников</h6>
|
||||
|
||||
{% block table %}
|
||||
<table class="light-table">
|
||||
<table class="table table-dark light-table">
|
||||
|
||||
<thead>
|
||||
<th>Name</th>
|
||||
@ -42,8 +42,8 @@
|
||||
<th>Role</th>
|
||||
<th>Checked</th>
|
||||
</thead>
|
||||
<tbody id="tbody">
|
||||
</tbody>
|
||||
<tbody id="tbody"></tbody>
|
||||
|
||||
</table>
|
||||
<p id="loading">Данные загружаются...</p>
|
||||
{% endblock %}
|
||||
|
@ -60,6 +60,12 @@
|
||||
<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>
|
||||
</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>
|
||||
{% include 'base/success_messages.html' %}
|
||||
</div>
|
||||
|
192
main/views.py
@ -1,40 +1,46 @@
|
||||
import logging
|
||||
import os
|
||||
from datetime import datetime
|
||||
from smtplib import SMTPException
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.forms import PasswordResetForm
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
|
||||
from django.contrib.auth.models import User, Permission
|
||||
from django.contrib.auth.tokens import default_token_generator
|
||||
from django.contrib.auth.forms import PasswordResetForm
|
||||
from django.contrib.auth.views import LoginView
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.core.handlers.wsgi import WSGIRequest
|
||||
from django.http import HttpResponseRedirect, HttpResponse
|
||||
from django.shortcuts import render, get_list_or_404, redirect
|
||||
from django.shortcuts import render, redirect, get_list_or_404
|
||||
from django.urls import reverse_lazy, reverse
|
||||
from django.views.generic import FormView
|
||||
from django_registration.views import RegistrationView
|
||||
from django.contrib import messages
|
||||
|
||||
# Django REST
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.response import Response
|
||||
|
||||
from zenpy.lib.api_objects import User as ZenpyUser
|
||||
|
||||
from access_controller.settings import 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, \
|
||||
make_engineer, make_light_agent, get_users_list, update_users_in_model, count_users, \
|
||||
StatisticData
|
||||
StatisticData, log, ZendeskAdmin
|
||||
from main.forms import AdminPageUsers, CustomRegistrationForm, CustomAuthenticationForm, StatisticForm
|
||||
from main.serializers import ProfileSerializer
|
||||
from main.serializers import ProfileSerializer, ZendeskUserSerializer
|
||||
from .models import UserProfile
|
||||
|
||||
|
||||
def setup_context(profile_lit: bool = False, control_lit: bool = False, work_lit: bool = False,
|
||||
registration_lit: bool = False, login_lit: bool = False):
|
||||
context = {
|
||||
'profile_lit': profile_lit,
|
||||
'control_lit': control_lit,
|
||||
'work_lit': work_lit,
|
||||
'registration_lit': registration_lit,
|
||||
'login_lit': login_lit,
|
||||
}
|
||||
return context
|
||||
|
||||
|
||||
class CustomRegistrationView(RegistrationView):
|
||||
"""
|
||||
Отображение и логика работы страницы регистрации пользователя.
|
||||
@ -48,10 +54,15 @@ class CustomRegistrationView(RegistrationView):
|
||||
:param is_allowed: Определение зарегистрирован ли пользователь с введенным email на Zendesk и принадлежит ли он к организации SYSTEM
|
||||
:type is_allowed: :class:`bool`
|
||||
"""
|
||||
extra_context = setup_context(registration_lit=True)
|
||||
form_class = CustomRegistrationForm
|
||||
template_name = 'django_registration/registration_form.html'
|
||||
success_url = reverse_lazy('django_registration_complete')
|
||||
is_allowed = True
|
||||
urls = {
|
||||
'done': reverse_lazy('password_reset_done'),
|
||||
'invalid_zendesk_email': reverse_lazy('django_registration_disallowed'),
|
||||
'email_sending_error': reverse_lazy('registration_email_error'),
|
||||
}
|
||||
redirect_url = 'done'
|
||||
|
||||
def register(self, form: CustomRegistrationForm) -> User:
|
||||
"""
|
||||
@ -64,7 +75,7 @@ class CustomRegistrationView(RegistrationView):
|
||||
:param form: Email пользователя на Zendesk
|
||||
:return: user
|
||||
"""
|
||||
self.is_allowed = True
|
||||
self.redirect_url = 'done'
|
||||
if check_user_exist(form.data['email']) and get_user_organization(form.data['email']) == 'SYSTEM':
|
||||
forms = PasswordResetForm(self.request.POST)
|
||||
if forms.is_valid():
|
||||
@ -83,14 +94,17 @@ class CustomRegistrationView(RegistrationView):
|
||||
email=form.data['email'],
|
||||
password=User.objects.make_random_password(length=50)
|
||||
)
|
||||
forms.save(**opts)
|
||||
update_profile(user.userprofile)
|
||||
self.set_permission(user)
|
||||
return user
|
||||
try:
|
||||
update_profile(user.userprofile)
|
||||
self.set_permission(user)
|
||||
forms.save(**opts)
|
||||
return user
|
||||
except SMTPException:
|
||||
self.redirect_url = 'email_sending_error'
|
||||
else:
|
||||
raise ValueError('Непредвиденная ошибка')
|
||||
else:
|
||||
self.is_allowed = False
|
||||
self.redirect_url = 'invalid_zendesk_email'
|
||||
|
||||
@staticmethod
|
||||
def set_permission(user: User) -> None:
|
||||
@ -107,7 +121,7 @@ class CustomRegistrationView(RegistrationView):
|
||||
)
|
||||
user.user_permissions.add(permission)
|
||||
|
||||
def get_success_url(self, user: User = None) -> success_url:
|
||||
def get_success_url(self, user: User = None):
|
||||
"""
|
||||
Функция возвращает url-адрес страницы, куда нужно перейти после успешной/не успешной регистрации.
|
||||
Используется самой django-registration.
|
||||
@ -115,10 +129,11 @@ class CustomRegistrationView(RegistrationView):
|
||||
:param user: пользователь, пытающийся зарегистрироваться
|
||||
:return: адресация на страницу успешной регистрации
|
||||
"""
|
||||
if self.is_allowed:
|
||||
return reverse_lazy('password_reset_done')
|
||||
else:
|
||||
return reverse_lazy('django_registration_disallowed')
|
||||
return self.urls[self.redirect_url]
|
||||
|
||||
|
||||
def registration_error(request):
|
||||
return render(request, 'django_registration/registration_error.html')
|
||||
|
||||
|
||||
@login_required()
|
||||
@ -131,11 +146,12 @@ def profile_page(request: WSGIRequest) -> HttpResponse:
|
||||
"""
|
||||
user_profile: UserProfile = request.user.userprofile
|
||||
update_profile(user_profile)
|
||||
context = {
|
||||
context = setup_context(profile_lit=True)
|
||||
context.update({
|
||||
'profile': user_profile,
|
||||
'pagename': 'Страница профиля',
|
||||
'ZENDESK_ROLES': ZENDESK_ROLES,
|
||||
}
|
||||
})
|
||||
return render(request, 'pages/profile.html', context)
|
||||
|
||||
|
||||
@ -171,33 +187,18 @@ def work_page(request: WSGIRequest, id: int) -> HttpResponse:
|
||||
elif user.custom_role_id == ZENDESK_ROLES['light_agent']:
|
||||
light_agents.append(user)
|
||||
|
||||
context = {
|
||||
context = setup_context(work_lit=True)
|
||||
context.update({
|
||||
'engineers': engineers,
|
||||
'agents': light_agents,
|
||||
'messages': messages.get_messages(request),
|
||||
'licences_remaining': max(0, ZENDESK_MAX_AGENTS - len(engineers)),
|
||||
'pagename': 'Управление правами'
|
||||
}
|
||||
})
|
||||
return render(request, 'pages/work.html', context)
|
||||
return redirect("login")
|
||||
|
||||
|
||||
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()
|
||||
def work_hand_over(request: WSGIRequest) -> HttpResponseRedirect:
|
||||
"""
|
||||
@ -207,10 +208,7 @@ def work_hand_over(request: WSGIRequest) -> HttpResponseRedirect:
|
||||
:param request: данные текущего пользователя (login_required)
|
||||
:return: перезагрузка текущей страницы после выполнения смены роли
|
||||
"""
|
||||
zenpy_user, admin = auth_user(request)
|
||||
if zenpy_user.custom_role_id == ZENDESK_ROLES['engineer']:
|
||||
zenpy_user.custom_role_id = ZENDESK_ROLES['light_agent']
|
||||
user_update(zenpy_user, admin, request)
|
||||
make_light_agent(request.user.userprofile,request.user)
|
||||
return HttpResponseRedirect(reverse('work', args=(request.user.id,)))
|
||||
|
||||
|
||||
@ -223,17 +221,31 @@ def work_become_engineer(request: WSGIRequest) -> HttpResponseRedirect:
|
||||
:return: перезагрузка текущей страницы после выполнения смены роли
|
||||
"""
|
||||
zenpy_user, admin = auth_user(request)
|
||||
if zenpy_user.custom_role_id == ZENDESK_ROLES['light_agent']:
|
||||
zenpy_user.custom_role_id = ZENDESK_ROLES['engineer']
|
||||
user_update(zenpy_user, admin, request)
|
||||
|
||||
make_engineer(request.user.userprofile,request.user)
|
||||
return HttpResponseRedirect(reverse('work', args=(request.user.id,)))
|
||||
|
||||
@login_required()
|
||||
def work_get_tickets(request):
|
||||
zenpy_user, admin = auth_user(request)
|
||||
count_tickets = int(request.GET["count_tickets"])
|
||||
tickets = [ticket for ticket in admin.search(type="ticket") if ticket.group.name == 'Сменная группа' and ticket.assignee is None]
|
||||
for i in range(len(tickets)):
|
||||
if i == count_tickets:
|
||||
return HttpResponseRedirect(reverse('work', args=(request.user.id,)))
|
||||
tickets[i].assignee = zenpy_user
|
||||
admin.tickets.update(tickets[i])
|
||||
return HttpResponseRedirect(reverse('work', args=(request.user.id,)))
|
||||
|
||||
|
||||
def main_page(request):
|
||||
def main_page(request: WSGIRequest) -> HttpResponse:
|
||||
"""
|
||||
Функция переадресации на главную страницу.
|
||||
"""
|
||||
return render(request, 'pages/index.html')
|
||||
|
||||
|
||||
class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin,SuccessMessageMixin, FormView):
|
||||
class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, FormView):
|
||||
"""
|
||||
Класс отображения страницы администратора.
|
||||
|
||||
@ -275,25 +287,49 @@ class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin,SuccessMessageMi
|
||||
"""
|
||||
for user in users:
|
||||
make_engineer(user, self.request.user)
|
||||
log(user, self.request.user.userprofile)
|
||||
|
||||
def make_light_agents(self, users):
|
||||
"""
|
||||
Функция проходит по списку пользователей, проставляя статус "light agent".
|
||||
|
||||
:param users: Список пользователей
|
||||
:return: Обновленный список пользователей
|
||||
"""
|
||||
for user in users:
|
||||
make_light_agent(user, self.request.user)
|
||||
log(user, self.request.user.userprofile)
|
||||
|
||||
def get_context_data(self, **kwargs) -> dict:
|
||||
"""
|
||||
Функция формирования контента страницы администратора (с проверкой прав доступа)
|
||||
"""
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
# context = super().get_context_data(**kwargs)
|
||||
|
||||
# context['licences_remaining'] = max(0, ZENDESK_MAX_AGENTS - context['engineers'])
|
||||
# return context
|
||||
|
||||
context = setup_context(control_lit=True)
|
||||
context.update(super().get_context_data(**kwargs))
|
||||
users = get_list_or_404(
|
||||
UserProfile, role='agent')
|
||||
context['engineers'], context['light_agents'] = count_users(get_users_list())
|
||||
context['licences_remaining'] = max(0, ZENDESK_MAX_AGENTS - context['engineers'])
|
||||
return context
|
||||
context.update({
|
||||
'users': users,
|
||||
'ZENDESK_ROLES': ZENDESK_ROLES,
|
||||
'engineers': context['engineers'],
|
||||
'light_agents': context['light_agents'],
|
||||
'licences_remaining': max(0, ZENDESK_MAX_AGENTS - context['engineers']),
|
||||
})
|
||||
return context # TODO: need to get profile page url
|
||||
|
||||
|
||||
class CustomLoginView(LoginView):
|
||||
"""
|
||||
Отображение страницы авторизации пользователя
|
||||
"""
|
||||
extra_context = setup_context(login_lit=True)
|
||||
form_class = CustomAuthenticationForm
|
||||
|
||||
|
||||
@ -305,16 +341,34 @@ class UsersViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
serializer_class = ProfileSerializer
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
users = update_users_in_model().values
|
||||
count = count_users(users)
|
||||
users = update_users_in_model()
|
||||
count = count_users(users.values)
|
||||
profiles = UserProfile.objects.filter(role='agent')
|
||||
serializer = self.get_serializer(profiles, many=True)
|
||||
return Response({
|
||||
res = {
|
||||
'users': serializer.data,
|
||||
'engineers': count[0],
|
||||
'light_agents': count[1]
|
||||
})
|
||||
'light_agents': count[1],
|
||||
"zendesk_users": self.get_zendesk_users(self.choose_users(users.values, profiles))
|
||||
}
|
||||
return Response(res)
|
||||
|
||||
@staticmethod
|
||||
def choose_users(zendesk, model):
|
||||
users = []
|
||||
for zendesk_user in zendesk:
|
||||
if zendesk_user.name not in [user.name for user in model]:
|
||||
users.append(zendesk_user)
|
||||
return users
|
||||
|
||||
@staticmethod
|
||||
def get_zendesk_users(users):
|
||||
zendesk_users = ZendeskUserSerializer(
|
||||
data=[user for user in users if user.role != 'admin'],
|
||||
many=True
|
||||
)
|
||||
zendesk_users.is_valid()
|
||||
return zendesk_users.data
|
||||
|
||||
|
||||
@login_required()
|
||||
@ -325,12 +379,18 @@ def statistic_page(request: WSGIRequest) -> HttpResponse:
|
||||
:param request: данные о пользователе: email, время и интервал работы. Данные получаем через forms.StatisticForm
|
||||
:return: адресация на страницу статистики
|
||||
"""
|
||||
if not request.user.has_perm('main.has_control_access'):
|
||||
return PermissionDenied
|
||||
context = {
|
||||
|
||||
# if not request.user.has_perm('main.has_control_access'):
|
||||
# raise PermissionDenied
|
||||
# context = {
|
||||
|
||||
if not request.user.is_superuser:
|
||||
return redirect('index')
|
||||
context = setup_context()
|
||||
context.update({
|
||||
'pagename': 'страница статистики',
|
||||
'errors': list(),
|
||||
}
|
||||
})
|
||||
if request.method == "POST":
|
||||
form = StatisticForm(request.POST)
|
||||
if form.is_valid():
|
||||
|
@ -6,7 +6,8 @@ import sys
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'access_controller.settings')
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE',
|
||||
'access_controller.settings')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
|
@ -124,4 +124,7 @@
|
||||
padding: 10px;
|
||||
background: #3B91D4;
|
||||
color: white;
|
||||
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
"use strict";
|
||||
|
||||
// React
|
||||
class TableRow extends React.Component {
|
||||
class ModelUserTableRow extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<tr>
|
||||
<tr className={"table-dark"}>
|
||||
<td>
|
||||
<a href="#">{this.props.user.name}</a>
|
||||
</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 {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@ -30,6 +67,7 @@ class TableBody extends React.Component {
|
||||
users: [],
|
||||
engineers: 0,
|
||||
light_agents: 0,
|
||||
zendesk_users: [],
|
||||
};
|
||||
}
|
||||
|
||||
@ -39,6 +77,7 @@ class TableBody extends React.Component {
|
||||
users: response.data.users,
|
||||
engineers: response.data.engineers,
|
||||
light_agents: response.data.light_agents,
|
||||
zendesk_users: response.data.zendesk_users,
|
||||
});
|
||||
let elements = document.querySelectorAll(".info-quantity-value");
|
||||
elements[0].innerHTML = this.state.engineers;
|
||||
@ -62,9 +101,12 @@ class TableBody extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.state.users.map((user, key) => (
|
||||
<TableRow user={user} key={key} />
|
||||
));
|
||||
return (
|
||||
<tr>
|
||||
<ModelUserTableRows users={this.state.users} />
|
||||
<ZendeskUserTableRows users={this.state.zendesk_users} />
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|