Merge branch 'develop' into hotfix/ticket_unassignment

# Conflicts:
#	access_controller/settings.py
#	main/extra_func.py
#	main/views.py
This commit is contained in:
Sokurov Idar 2021-03-18 20:18:13 +03:00
commit e563475ecd
12 changed files with 305 additions and 46 deletions

View File

@ -36,6 +36,7 @@ INSTALLED_APPS = [
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'django_registration', 'django_registration',
'rest_framework',
'main', 'main',
] ]
@ -187,6 +188,16 @@ ZENDESK_GROUPS = {
'employees': 'Поддержка', 'employees': 'Поддержка',
'buffer': 'Сменная группа', 'buffer': 'Сменная группа',
} }
SOLVED_TICKETS_EMAIL = 'd.krikov@ngenix.net' SOLVED_TICKETS_EMAIL = 'd.krikov@ngenix.net'
REST_FRAMEWORK = {
# Use Django's standard `django.contrib.auth` permissions,
# or allow read-only access for unauthenticated users.
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
]
}
ONE_DAY = 12 # Количество часов в 1 рабочем дне ONE_DAY = 12 # Количество часов в 1 рабочем дне

View File

@ -16,23 +16,26 @@ Including another URLconf
from django.contrib import admin from django.contrib import admin
from django.contrib.auth import views as auth_views 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 work_page, work_hand_over, work_become_engineer, AdminPageView
from main.views import main_page, profile_page, CustomRegistrationView, CustomLoginView 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 work_page, work_hand_over, work_become_engineer, \
AdminPageView, statistic_page AdminPageView, statistic_page
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/login/', CustomLoginView.as_view(extra_context={}), name='login',), # TODO add extra context 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('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('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')
] ]
@ -59,3 +62,8 @@ urlpatterns += [
name='password_reset_complete' name='password_reset_complete'
), ),
] ]
# Django REST
urlpatterns += [
path('api/', include(router.urls))
]

57
data.json Normal file
View File

@ -0,0 +1,57 @@
[
{
"model": "auth.user",
"pk": 1,
"fields": {
"password": "pbkdf2_sha256$216000$gHBBCr1jBELf$ZkEDW3IEd8Wij7u8vkv+0Eze32CS01bcaYWhcD9OIC4=",
"last_login": null,
"is_superuser": true,
"username": "admin@gmail.com",
"first_name": "",
"last_name": "",
"email": "admin@gmail.com",
"is_staff": true,
"is_active": true,
"date_joined": "2021-03-10T16:38:56.303Z",
"groups": [],
"user_permissions": [33]
}
},
{
"model": "main.userprofile",
"pk": 1,
"fields": {
"name": "ZendeskAdmin",
"user": 1,
"role": "admin"
}
},
{
"model": "auth.user",
"pk": 2,
"fields": {
"password": "pbkdf2_sha256$216000$5qLJgrm2Quq9$KDBNNymVZXkUx0HKBPFst2m83kLe0egPBnkW7KnkORU=",
"last_login": null,
"is_superuser": false,
"username": "123@test.ru",
"first_name": "",
"last_name": "",
"email": "123@test.ru",
"is_staff": false,
"is_active": true,
"date_joined": "2021-03-10T16:38:56.303Z",
"groups": [],
"user_permissions": []
}
},
{
"model": "main.userprofile",
"pk": 2,
"fields": {
"name": "UserForAccessTest",
"user": 2,
"role": "agent",
"custom_role_id": "360005209000"
}
}
]

View File

@ -6,6 +6,11 @@ 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 main.models import UserProfile, RoleChangeLogs
from django.core.exceptions import ObjectDoesNotExist
from access_controller.settings import ZENDESK_ROLES as ROLES, ONE_DAY
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
from main.models import UserProfile, RoleChangeLogs, UnassignedTicket, UnassignedTicketStatus from main.models import UserProfile, RoleChangeLogs, UnassignedTicket, UnassignedTicketStatus
@ -172,9 +177,11 @@ def get_users_list() -> list:
Функция **get_users_list** возвращает список пользователей Zendesk, относящихся к организации. Функция **get_users_list** возвращает список пользователей Zendesk, относящихся к организации.
""" """
zendesk = ZendeskAdmin() zendesk = ZendeskAdmin()
admin = zendesk.get_user(zendesk.email)
org = next(zendesk.admin.users.organizations(user=admin)) # У пользователей должна быть организация SYSTEM
return zendesk.admin.organizations.users(org) org = next(zendesk.admin.search(type='organization', name='SYSTEM'))
users = zendesk.admin.organizations.users(org)
return users
def get_tickets_list(email): def get_tickets_list(email):
@ -210,6 +217,63 @@ def get_user_organization(email: str) -> str:
return ZendeskAdmin().get_user_org(email) return ZendeskAdmin().get_user_org(email)
def check_user_auth(email: str, password: str) -> bool:
"""
Функция проверяет, верны ли входные данные
:raise: :class:`APIException`: исключение, вызываемое если пользователь не аутентифицирован
"""
creds = {
'email': email,
'password': password,
'subdomain': 'ngenix1612197338',
}
try:
user = Zenpy(**creds)
user.search(email, type='user')
except APIException:
return False
return True
def update_user_in_model(profile, zendesk_user):
profile.name = zendesk_user.name
profile.role = zendesk_user.role
profile.image = zendesk_user.photo['content_url'] if zendesk_user.photo else None
profile.custom_role_id = zendesk_user.custom_role_id
profile.save()
def count_users(users) -> tuple:
"""
Функция подсчета количества сотрудников с ролями engineer и light_a
.. todo::
this func counts users from all zendesk instead of just from a model:
"""
engineers, light_agents = 0, 0
for user in users:
if user.custom_role_id == ROLES['engineer']:
engineers += 1
elif user.custom_role_id == ROLES['light_agent']:
light_agents += 1
return engineers, light_agents
def update_users_in_model():
"""
Обновляет пользователей в модели UserProfile по списку пользователей в организации
"""
users = get_users_list()
for user in users:
try:
profile = User.objects.get(email=user.email).userprofile
update_user_in_model(profile, user)
except ObjectDoesNotExist:
pass
return users
def daterange(start_date, end_date) -> list: def daterange(start_date, end_date) -> list:
""" """
Возвращает список дней с start_date по end_date исключая правую границу Возвращает список дней с start_date по end_date исключая правую границу

17
main/serializers.py Normal file
View File

@ -0,0 +1,17 @@
from django.contrib.auth.models import User
from rest_framework import serializers
from main.models import UserProfile
class UserSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = User
fields = ['email']
class ProfileSerializer(serializers.HyperlinkedModelSerializer):
user = UserSerializer()
class Meta:
model = UserProfile
fields = ['user', 'id', 'role', 'name']

View File

@ -27,6 +27,7 @@
</style> </style>
{% block extra_css %}{% endblock %} {% block extra_css %}{% endblock %}
{% block extra_scripts %}{% endblock %}
</head> </head>
<body class="d-flex flex-column h-100"> <body class="d-flex flex-column h-100">

View File

@ -10,11 +10,15 @@
<link rel="stylesheet" href="{% static 'main/css/work.css' %}"/> <link rel="stylesheet" href="{% static 'main/css/work.css' %}"/>
{% endblock %} {% endblock %}
{% block extra_scripts %}
<script src="https://unpkg.com/react@17/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js" crossorigin></script>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
{% endblock%}
{% block content %} {% block content %}
<div class="container-md"> <div class="container-md">
<div class="new-section">
<p class="row page-description">Основная информация о странице</p>
</div>
{% block form %} {% block form %}
<form method="post"> <form method="post">
@ -37,25 +41,23 @@
<table class="light-table"> <table class="light-table">
<thead> <thead>
<th>ID</th> <th>Name</th>
<th>Email</th> <th>Email</th>
<th>Role</th> <th>Role</th>
<th>Name(link to profile)</th>
<th>Checked</th> <th>Checked</th>
</thead> </thead>
<tbody> <tbody id="old_tbody">
{% for user in users %} {% for user in users %}
<tr> <tr>
<td>{{ user.id }}</td> <td><a href="#">{{ user.name }}</a></td>
<td>{{ user.user.email }}</td> <td>{{ user.user.email }}</td>
<td>{{ user.role }}</td> <td>{{ user.role }}</td>
<td><a href="#">{{ user.name }}</a></td>
<td class="checkbox_field"></td> <td class="checkbox_field"></td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
<tbody id="new_tbody"></tbody>
</table> </table>
{% endblock%} {% endblock%}
@ -103,6 +105,6 @@
{% endblock %} {% endblock %}
</div> </div>
<script src="{% static 'main/js/control.js'%}"></script> <script src="{% static 'main/js/control.js'%}" type="text/babel"></script>
{% endblock %} {% endblock %}

View File

@ -30,7 +30,7 @@
{% endif %} {% endif %}
<div class="text-center"> <div class="text-center">
<button type="submit" class="btn btn-primary">Войти</button> <button type="submit" class="btn btn-primary">Войти</button>
<a href="password_reset" class="btn btn-link" style="display: block;">Забыли пароль?</a> <a href="{% url 'password_reset' %}" class="btn btn-link" style="display: block;">Забыли пароль?</a>
</div> </div>
</form> </form>
</div> </div>

6
main/urls.py Normal file
View File

@ -0,0 +1,6 @@
from rest_framework.routers import DefaultRouter
from main.views import UsersViewSet
router = DefaultRouter()
router.register(r'users', UsersViewSet)

View File

@ -1,9 +1,7 @@
import logging import logging
import os import os
from django.contrib.auth.decorators import login_required
from django.contrib.auth.forms import PasswordResetForm 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.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.views import LoginView from django.contrib.auth.views import LoginView
@ -15,16 +13,31 @@ from django.shortcuts import render, get_list_or_404, redirect
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.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from zenpy import Zenpy from zenpy import Zenpy
from zenpy.lib.api_objects import User as ZenpyUser from zenpy.lib.api_objects import User as ZenpyUser
from access_controller.settings import EMAIL_HOST_USER, ZENDESK_ROLES, ZENDESK_GROUPS from access_controller.settings import EMAIL_HOST_USER, ZENDESK_ROLES, ZENDESK_GROUPS
from main.extra_func import check_user_exist, update_profile, get_user_organization, make_engineer, make_light_agent, \ from main.extra_func import check_user_exist, update_profile, get_user_organization, make_engineer, make_light_agent, \
get_users_list, StatisticData, get_tickets_list, ZendeskAdmin get_users_list, StatisticData, get_tickets_list, 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
from main.models import UserProfile
from main.forms import AdminPageUsers, CustomRegistrationForm, CustomAuthenticationForm, StatisticForm from main.forms import AdminPageUsers, CustomRegistrationForm, CustomAuthenticationForm, StatisticForm
from .models import UserProfile, UnassignedTicket, UnassignedTicketStatus from .models import UserProfile, UnassignedTicket, UnassignedTicketStatus
from access_controller.settings import EMAIL_HOST_USER, ZENDESK_ROLES
# Django REST
from rest_framework import viewsets, status
from main.serializers import ProfileSerializer
from rest_framework.response import Response
class CustomRegistrationView(RegistrationView): class CustomRegistrationView(RegistrationView):
""" """
Отображение и логика работы страницы регистрации пользователя Отображение и логика работы страницы регистрации пользователя
@ -175,10 +188,11 @@ class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin, FormView):
""" """
Функция установки ролей пользователям Функция установки ролей пользователям
""" """
users = form.cleaned_data['users']
if 'engineer' in self.request.POST: if 'engineer' in self.request.POST:
self.make_engineers(form.cleaned_data['users']) self.make_engineers(users)
elif 'light_agent' in self.request.POST: elif 'light_agent' in self.request.POST:
self.make_light_agents(form.cleaned_data['users']) self.make_light_agents(users)
return super().form_valid(form) return super().form_valid(form)
def make_engineers(self, users): def make_engineers(self, users):
@ -189,32 +203,15 @@ class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin, FormView):
for user in users: for user in users:
make_light_agent(user, self.request.user) make_light_agent(user, self.request.user)
@staticmethod
def count_users(users) -> tuple:
"""
Функция подсчета количества сотрудников с ролями engineer и light_a
.. todo::
this func counts users from all zendesk instead of just from a model:
"""
engineers, light_agents = 0, 0
for user in users:
if user.custom_role_id == ZENDESK_ROLES['engineer']:
engineers += 1
elif user.custom_role_id == ZENDESK_ROLES['light_agent']:
light_agents += 1
return engineers, light_agents
def get_context_data(self, **kwargs) -> dict: def get_context_data(self, **kwargs) -> dict:
""" """
Функция формирования контента страницы администратора (с проверкой прав доступа) Функция формирования контента страницы администратора (с проверкой прав доступа)
""" """
if self.request.user.userprofile.role != 'admin':
raise PermissionDenied
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['users'] = get_list_or_404( users = get_list_or_404(
UserProfile, role='agent') UserProfile, role='agent')
context['engineers'], context['light_agents'] = self.count_users(get_users_list()) context['users'] = users
context['engineers'], context['light_agents'] = count_users(get_users_list())
return context # TODO: need to get profile page url return context # TODO: need to get profile page url
@ -225,6 +222,26 @@ class CustomLoginView(LoginView):
form_class = CustomAuthenticationForm form_class = CustomAuthenticationForm
class UsersViewSet(viewsets.ReadOnlyModelViewSet):
"""
Класс для получения пользователей с помощью api
"""
queryset = UserProfile.objects.filter(role='agent')
serializer_class = ProfileSerializer
def list(self, request, *args, **kwargs):
users = update_users_in_model().values
count = count_users(users)
profiles = UserProfile.objects.filter(role='agent')
serializer = self.get_serializer(profiles, many=True)
return Response({
'users': serializer.data,
'engineers': count[0],
'light_agents': count[1]
})
@login_required() @login_required()
def statistic_page(request): def statistic_page(request):
if not request.user.has_perm('main.has_control_access'): if not request.user.has_perm('main.has_control_access'):

View File

@ -3,6 +3,8 @@ Django==3.1.6
Pillow==8.1.0 Pillow==8.1.0
zenpy~=2.0.24 zenpy~=2.0.24
django_registration==3.1.1 django_registration==3.1.1
djangorestframework==3.12.2
# Documentation # Documentation
Sphinx==3.4.3 Sphinx==3.4.3

View File

@ -1,9 +1,83 @@
"use strict"; "use strict";
let checkboxes = document.getElementsByName("users");
let fields = document.querySelectorAll(".checkbox_field"); function move_checkboxes() {
if (checkboxes.length == fields.length) { let checkboxes = document.getElementsByName("users");
let fields = document.querySelectorAll(".checkbox_field");
if (checkboxes.length == fields.length) {
for (let i = 0; i < fields.length; ++i) { for (let i = 0; i < fields.length; ++i) {
let el = checkboxes[i].cloneNode(true); let el = checkboxes[i].cloneNode(true);
fields[i].appendChild(el); fields[i].appendChild(el);
} }
} else {
alert(
"Количество пользователей агентов не соответствует количеству полей в форме AdminPageUsers"
);
}
} }
move_checkboxes();
// React
class TableRow extends React.Component {
render() {
return (
<tr>
<td>
<a href="#">{this.props.user.name}</a>
</td>
<td>{this.props.user.user.email}</td>
<td>{this.props.user.role}</td>
<td>
<input
type="checkbox"
value={this.props.user.id}
className="form-check-input"
name="users"
/>
</td>
</tr>
);
}
}
class TableBody extends React.Component {
constructor(props) {
super(props);
this.state = {
users: [],
engineers: 0,
light_agents: 0,
};
}
get_users() {
axios.get("/api/users").then((response) => {
this.setState({
users: response.data.users,
engineers: response.data.engineers,
light_agents: response.data.light_agents,
});
let elements = document.querySelectorAll(".info-quantity-value");
elements[0].innerHTML = this.state.engineers;
elements[1].innerHTML = this.state.light_agents;
});
}
componentDidMount() {
this.interval = setInterval(() => {
this.get_users();
}, 10000);
}
componentWillUnmount() {
clearInterval(this.interval);
}
render() {
return this.state.users.map((user, key) => (
<TableRow user={user} key={key} />
));
}
}
ReactDOM.render(<TableBody />, document.getElementById("new_tbody"));
setTimeout(() => document.getElementById("old_tbody").remove(), 10000);