This commit is contained in:
Dmitriy Andreev 2021-03-18 20:42:04 +03:00
commit 95781c3ee9
34 changed files with 1346 additions and 238 deletions

19
access_controller/auth.py Normal file
View File

@ -0,0 +1,19 @@
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth.models import User
class EmailAuthBackend(ModelBackend):
def authenticate(self, request, username=None, password=None, **kwargs):
try:
user = User.objects.get(email=username)
if user.check_password(password):
return user
return None
except User.DoesNotExist:
return None
def get_user(self, user_id):
try:
return User.objects.get(pk=user_id)
except User.DoesNotExist:
return None

View File

@ -36,6 +36,7 @@ INSTALLED_APPS = [
'django.contrib.messages',
'django.contrib.staticfiles',
'django_registration',
'rest_framework',
'main',
]
@ -135,6 +136,11 @@ ACCOUNT_ACTIVATION_DAYS = 7
LOGIN_REDIRECT_URL = '/'
LOGOUT_REDIRECT_URL = '/'
# Название_приложения.Названиеайла.Название_класса_обработчика
AUTHENTICATION_BACKENDS = [
'access_controller.auth.EmailAuthBackend',
]
# Logging system
# https://docs.djangoproject.com/en/3.1/topics/logging/
LOGGING = {
@ -177,3 +183,21 @@ ZENDESK_ROLES = {
'engineer': 360005209000,
'light_agent': 360005208980,
}
ZENDESK_GROUPS = {
'employees': 'Поддержка',
'buffer': 'Сменная группа',
}
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 рабочем дне

View File

@ -14,21 +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.views import LoginView
from django.contrib.auth import views as auth_views
from django.urls import path, include
from main.views import main_page, profile_page, CustomRegistrationView, AdminPageView
from main.views import main_page, profile_page, CustomRegistrationView, CustomLoginView
from main.views import work_page, work_hand_over, work_become_engineer, \
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/login/', LoginView.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_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('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')
]
urlpatterns += [
@ -53,3 +62,8 @@ urlpatterns += [
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

@ -18,3 +18,6 @@ help:
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
spelling:
$(SPHINXBUILD) -b spelling -W $(ALLSPHINXOPTS) $(BUILDDIR)/spelling

View File

@ -12,10 +12,34 @@
#
import os
import sys
import importlib
import inspect
sys.path.insert(0, os.path.abspath('../../'))
import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'access_controller.settings')
os.environ.setdefault('DJANGO_CONFIGURATION', 'Dev')
# Fix Django's FileFields
from django.db.models.fields.files import FileDescriptor
FileDescriptor.__get__ = lambda self, *args, **kwargs: self
from django.db.models.manager import ManagerDescriptor
ManagerDescriptor.__get__ = lambda self, *args, **kwargs: self.manager
# Stop Django from executing DB queries
from django.db.models.query import QuerySet
QuerySet.__repr__ = lambda self: self.__class__.__name__
try:
import enchant # NoQA
except ImportError:
enchant = None
django.setup()
# -- Project information -----------------------------------------------------
@ -28,8 +52,81 @@ author = 'SHP S101, group 2'
release = 'v0.01'
# Django sphinx setup by https://gist.github.com/codingjoe/314bda5a07ff3b41f247
# -- General configuration ---------------------------------------------------
def process_django_models(app, what, name, obj, options, lines):
"""Append params from fields to model documentation."""
from django.utils.encoding import force_text
from django.utils.html import strip_tags
from django.db import models
spelling_white_list = ['', '.. spelling::']
if inspect.isclass(obj) and issubclass(obj, models.Model):
for field in obj._meta.fields:
help_text = strip_tags(force_text(field.help_text))
verbose_name = force_text(field.verbose_name).capitalize()
if help_text:
lines.append(':param %s: %s - %s' % (field.attname, verbose_name, help_text))
else:
lines.append(':param %s: %s' % (field.attname, verbose_name))
if enchant is not None:
from enchant.tokenize import basic_tokenize
words = verbose_name.replace('-', '.').replace('_', '.').split('.')
words = [s for s in words if s != '']
for word in words:
spelling_white_list += [" %s" % ''.join(i for i in word if not i.isdigit())]
spelling_white_list += [" %s" % w[0] for w in basic_tokenize(word)]
field_type = type(field)
module = field_type.__module__
if 'django.db.models' in module:
# scope with django.db.models * imports
module = 'django.db.models'
lines.append(':type %s: %s.%s' % (field.attname, module, field_type.__name__))
if enchant is not None:
lines += spelling_white_list
print('ok')
return lines
def process_modules(app, what, name, obj, options, lines):
"""Add module names to spelling white list."""
if what != 'module':
return lines
from enchant.tokenize import basic_tokenize
spelling_white_list = ['', '.. spelling::']
words = name.replace('-', '.').replace('_', '.').split('.')
words = [s for s in words if s != '']
for word in words:
spelling_white_list += [" %s" % ''.join(i for i in word if not i.isdigit())]
spelling_white_list += [" %s" % w[0] for w in basic_tokenize(word)]
lines += spelling_white_list
return lines
def skip_queryset(app, what, name, obj, skip, options):
"""Skip queryset subclasses to avoid database queries."""
from django.db import models
if isinstance(obj, (models.QuerySet, models.manager.BaseManager)) or name.endswith('objects'):
return True
return skip
def setup(app):
# Register the docstring processor with sphinx
app.connect('autodoc-process-docstring', process_django_models)
app.connect('autodoc-skip-member', skip_queryset)
if enchant is not None:
app.connect('autodoc-process-docstring', process_modules)
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
@ -39,8 +136,15 @@ extensions = [
'sphinx.ext.intersphinx',
'sphinx.ext.viewcode',
'sphinx_rtd_theme',
'sphinx.ext.graphviz',
'sphinx.ext.inheritance_diagram',
'sphinx_autodoc_typehints'
]
if enchant is not None:
extensions.append('sphinxcontrib.spelling')
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
@ -56,7 +160,6 @@ language = 'ru'
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = []
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
@ -69,7 +172,6 @@ html_theme = "sphinx_rtd_theme"
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# -- Extension configuration -------------------------------------------------
# -- Options for intersphinx extension ---------------------------------------
@ -78,12 +180,25 @@ html_static_path = ['_static']
intersphinx_mapping = {
'https://docs.python.org/3/': None,
'django': (
'https://docs.djangoproject.com/en/dev/',
'https://docs.djangoproject.com/en/dev/_objects/'
),
'https://docs.djangoproject.com/en/dev/',
'https://docs.djangoproject.com/en/dev/_objects/'
),
}
autodoc_default_flags = ['members']
# spell checking
spelling_lang = 'en_US'
spelling_word_list_filename = 'spelling_wordlist.txt'
spelling_show_suggestions = True
spelling_ignore_pypi_package_names = True
# -- Options for todo extension ----------------------------------------------
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = True
set_type_checking_flag = True
typehints_fully_qualified = True
always_document_param_types = True
typehints_document_rtype = True

View File

@ -1,5 +1,5 @@
Что необходимо доделать?
=======================
========================

View File

@ -1,11 +1,14 @@
import os
from datetime import timedelta, datetime, date
from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist
from django.utils import timezone
from zenpy import Zenpy
from zenpy.lib.exception import APIException
from main.models import UserProfile
from access_controller.settings import ZENDESK_ROLES as ROLES
from access_controller.settings import ZENDESK_ROLES as ROLES, ONE_DAY, ZENDESK_GROUPS, SOLVED_TICKETS_EMAIL
from main.models import UserProfile, RoleChangeLogs, UnassignedTicket, UnassignedTicketStatus
class ZendeskAdmin:
@ -13,95 +16,94 @@ class ZendeskAdmin:
Класс **ZendeskAdmin** существует, чтобы в каждой фунциии отдельно не проверять аккаунт администратора
:param credentials: Полномочия (первым указывается учетная запись организации в Zendesk)
:type credentials: :class:`list of dictionaries`
:type credentials: :class:`dict`
:param email: Email администратора, указанный в env
:type email: :class:`email`
:type email: :class:`str`
:param token: Токен администратора (формируется в Zendesk, указывается в env)
:type token: :class:`str`
:param password: Пароль администратора, указанный в env
:type password: :class:`str`
"""
credentials = {
credentials: dict = {
'subdomain': 'ngenix1612197338'
}
email = os.getenv('ACCESS_CONTROLLER_API_EMAIL')
token = os.getenv('ACCESS_CONTROLLER_API_TOKEN')
password = os.getenv('ACCESS_CONTROLLER_API_PASSWORD')
email: str = os.getenv('ACCESS_CONTROLLER_API_EMAIL')
token: str = os.getenv('ACCESS_CONTROLLER_API_TOKEN')
password: str = os.getenv('ACCESS_CONTROLLER_API_PASSWORD')
def __init__(self):
self.create_admin()
def check_user(self, email: str) -> bool:
"""
Функция **check_user** осуществляет проверку существования пользователя в Zendesk
:param email: Электронная почта пользователя
:type email: :class:`email`
:return: True, если существует, иначе False
:rtype: :class:`bool`
Функция **check_user** осуществляет проверку существования пользователя в Zendesk по email
"""
return True if self.admin.search(email, type='user') else False
def get_user_name(self, email: str) -> str:
"""
Функция **get_user_name** возвращает имя пользователя
:param user_name: Имя пользователя
:type user_name: :class:`str`
Функция **get_user_name** возвращает имя пользователя по его email
"""
user = self.admin.users.search(email).values[0]
return user.name
def get_user_role(self, email: str) -> str:
"""
Функция **get_user_role** возвращает роль пользователя
:param user_role: Роль пользователя
:type user_role: :class:`str`
Функция **get_user_role** возвращает роль пользователя по его email
"""
user = self.admin.users.search(email).values[0]
return user.role
def get_user_id(self, email: str) -> str:
"""
Функция **get_user_id** возвращает id пользователя
:param user_id: ID пользователя
:type user_id: :class:`str`
Функция **get_user_id** возвращает id пользователя по его email
"""
user = self.admin.users.search(email).values[0]
return user.id
def get_user_image(self, email: str) -> str:
"""
Функция **get_user_image** возвращает аватар пользователя
:param user_image: Аватар пользователя
:type user_image: :class:`img`
Функция **get_user_image** возвращает url-ссылку на аватар пользователя по его email
"""
user = self.admin.users.search(email).values[0]
return user.photo['content_url'] if user.photo else None
def get_user(self, email: str) -> str:
def get_user(self, email: str):
"""
Функция **get_user** возвращает пользователя (объект) по его email
:param email: email пользователя
:return: email пользователя, найденного в БД
"""
return self.admin.users.search(email).values[0]
def get_user_org(self, email: str) -> str:
user = self.admin.users.search(email).values[0]
return user.organization.name
def get_group(self, name):
groups = self.admin.search(name)
for group in groups:
return group
return None
def create_admin(self) -> None:
def get_user_org(self, email: str) -> str:
"""
Функция **get_user_org** возвращает организацию, к которой относится пользователь по его email
"""
user = self.admin.users.search(email).values[0]
return user.organization.name if user.organization else None
def create_admin(self) -> Zenpy:
"""
Функция **Create_admin()** создает администратора, проверяя наличие вводимых данных в env.
:param credentials: В список полномочий администратора вносятся email, token, password из env
:type credentials: :class:`dict`
:raise: :class:`ValueError`: исключение, вызываемое если email не введен в env
:raise: :class:`APIException`: исключение, вызываемое если пользователя с таким email не существует в Zendesk
"""
if self.email is None:
raise ValueError('access_controller email not in env')
self.credentials['email'] = os.getenv('ACCESS_CONTROLLER_API_EMAIL')
self.credentials['email'] = self.email
if self.token:
self.credentials['token'] = self.token
@ -116,38 +118,83 @@ class ZendeskAdmin:
raise ValueError('invalid access_controller`s login data')
def update_role(user_profile, role):
def update_role(user_profile: UserProfile, role: str) -> UserProfile:
"""
Функция **update_role** меняет роль пользователя.
"""
zendesk = ZendeskAdmin()
user = zendesk.get_user(user_profile.user.email)
user.custom_role_id = role
zendesk.admin.users.update(user)
def make_engineer(user_profile):
def make_engineer(user_profile: UserProfile, who_changes: User) -> UserProfile:
"""
Функция **make_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'])
def make_light_agent(user_profile):
def make_light_agent(user_profile: UserProfile, who_changes: User) -> UserProfile:
"""
Функция **make_light_agent** устанавливапет пользователю роль легкого агента.
"""
tickets = get_tickets_list(user_profile.user.email)
for ticket in tickets:
UnassignedTicket.objects.create(
assignee=user_profile.user,
ticket_id=ticket.id,
status=UnassignedTicketStatus.SOLVED if ticket.status == 'solved' else UnassignedTicketStatus.UNASSIGNED
)
if ticket.status == 'solved':
ticket.assignee = ZendeskAdmin().get_user(SOLVED_TICKETS_EMAIL)
else:
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'])
def get_users_list():
def get_users_list() -> list:
"""
Функция **get_users_list** возвращает список пользователей Zendesk, относящихся к организации.
"""
zendesk = ZendeskAdmin()
admin = zendesk.get_user(zendesk.email)
org = next(zendesk.admin.users.organizations(user=admin))
return zendesk.admin.organizations.users(org)
# У пользователей должна быть организация SYSTEM
org = next(zendesk.admin.search(type='organization', name='SYSTEM'))
users = zendesk.admin.organizations.users(org)
return users
def update_profile(user_profile: UserProfile):
def get_tickets_list(email):
"""
Функция возвращает список тикетов пользователя Zendesk
"""
return ZendeskAdmin().admin.search(assignee=email, type='ticket')
def update_profile(user_profile: UserProfile) -> UserProfile:
"""
Функция обновляет профиль пользователя в соотвтетствии с текущим в Zendesk
:param user_profile: Объект профиля пользователя
:type user_profile: :class:`main.models.UserProfile`
"""
user = ZendeskAdmin().get_user(user_profile.user.email)
user_profile.name = user.name
user_profile.role = user.role
user_profile.custom_role_id = user.custom_role_id if user.custom_role_id else 0
user_profile.image = user.photo['content_url'] if user.photo else None
user_profile.save()
@ -155,11 +202,6 @@ def update_profile(user_profile: UserProfile):
def check_user_exist(email: str) -> bool:
"""
Функция проверяет, существует ли пользователь
:param email: Электронная почта пользователя
:type email: :class:`str`
:return: True, если существует, иначе False
:rtype: :class:`bool`
"""
return ZendeskAdmin().check_user(email)
@ -167,11 +209,6 @@ def check_user_exist(email: str) -> bool:
def get_user_organization(email: str) -> str:
"""
Функция возвращает организацию пользователя
:param email: Электронная почта пользователя
:type email: :class:`str`
:return: Название организации
:rtype: :class:`str`
"""
return ZendeskAdmin().get_user_org(email)
@ -180,13 +217,7 @@ def check_user_auth(email: str, password: str) -> bool:
"""
Функция проверяет, верны ли входные данные
:param email: Электроная почта пользователя
:type email: :class:`str`
:param password: Пароль пользователя
:type password: :class:`str`
:return: True, если входные данные верны, иначе False
:raise: :class:`APIException`: исключение, вызываемое если пользователь не аутентифицирован
:rtype: :class:`bool`
"""
creds = {
'email': email,
@ -199,3 +230,254 @@ def check_user_auth(email: str, password: str) -> bool:
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:
"""
Возвращает список дней с start_date по end_date исключая правую границу
"""
dates = []
for n in range(int((end_date - start_date).days)):
dates.append(start_date + timedelta(n))
return dates
def get_timedelta(log, time=None) -> timedelta:
"""
Возвращает объект класса timedelta, который хранит промежуток времени от начала суток до момента,
который находится в log (объект класса RoleChangeLogs) или в time(datetime.time), если введён
"""
if time is None:
time = log.change_time.time()
time = timedelta(hours=time.hour, minutes=time.minute, seconds=time.second)
return time
def last_day_of_month(day):
"""
Возвращает последний день любого месяца
"""
next_month = day.replace(day=28) + timedelta(days=4)
return next_month - timedelta(days=next_month.day)
class StatisticData:
def __init__(self, start_date, end_date, user_email, stat=None):
self.display = None
self.interval = None
self.start_date = start_date
self.end_date = end_date
self.email = user_email
self.errors = list()
self.warnings = list()
self.data = dict()
self.statistic = dict()
self._init_data()
if stat is None:
self._init_statistic()
else:
self.statistic = stat
def get_statistic(self):
"""
Вернуть словарь statistic с применением формата отображения и интеравала работы(если они есть)
None, если были ошибки при создании
"""
if self.is_valid_statistic():
stat = self.statistic
stat = self._use_display(stat)
stat = self._use_interval(stat)
return stat
else:
return None
def is_valid_statistic(self):
"""
Были ли ошибки при создании статистики
"""
return not self.errors and self.statistic
def set_interval(self, interval):
"""
Устанавливает интервал работы
"""
if interval not in ['months', 'days']:
self.errors += ['Интервал работы должен быть в днях или месяцах']
return False
self.interval = interval
return True
def set_display(self, display_format):
"""
Устанавливает формат отображения
"""
if display_format not in ['days', 'hours']:
self.errors += ['Формат отображения должен быть в часах или днях']
return False
self.display = display_format
return True
def get_data(self):
"""
Вернуть данные
data - массив объектов RoleChangeLogs, является списком логов пользователя
data может быть пустым списком
"""
if self.is_valid_data():
return self.data
else:
return None
def is_valid_data(self):
"""
Были ли ошибки при получении логов
"""
return not self.errors
def _use_display(self, stat):
"""
Приводит данные к формату отображения
"""
if not self.is_valid_statistic() or not self.display:
return stat
new_stat = {}
for key, item in stat.items():
if self.display == 'hours':
new_stat[key] = item / 3600
elif self.display == 'days':
new_stat[key] = item / (ONE_DAY * 3600)
return new_stat
def _use_interval(self, stat):
"""
Объединяет ключи и значения в соответствии с интервалом работы
"""
if not self.is_valid_statistic() or not self.interval:
return stat
new_stat = {}
if self.interval == 'months':
# Переделываем ключи под формат('началоесяца - конец_месяца')
for key, value in stat.items():
current_month_start = max(self.start_date, date(year=key.year, month=key.month, day=1))
current_month_end = min(self.end_date, last_day_of_month(date(year=key.year, month=key.month, day=1)))
index = ' - '.join([str(current_month_start), str(current_month_end)])
if new_stat.get(index):
new_stat[index] += value
else:
new_stat[index] = value
elif self.interval == 'days':
new_stat = stat # статистика изначально в днях
return new_stat
def check_time(self):
"""
Проверка на правильность введенного времени
"""
if self.end_date < self.start_date or self.end_date > datetime.now().date():
return False
return True
def _init_data(self):
"""
Получение логов в диапазоне дат start_date-end_date для пользователя с почтой email
"""
if not self.check_time():
self.errors += ['Конец диапазона должен быть позже начала диапазона и раньше текущего времени']
return
try:
self.data = RoleChangeLogs.objects.filter(
change_time__range=[self.start_date, self.end_date + timedelta(days=1)],
user=User.objects.get(email=self.email),
).order_by('change_time')
except User.DoesNotExist:
self.errors += ['Пользователь не найден']
def _init_statistic(self):
"""
Функция заполняет словарь, в котором ключ - дата, значение - кол-во проработанных в этот день секунд
"""
self.clear_statistic()
if not self.get_data():
self.warnings += ['Не обнаружены изменения роли в данном промежутке']
return None
first_log, last_log = self.data[0], self.data[len(self.data) - 1]
if first_log.old_role == ROLES['engineer']:
self.statistic[first_log.change_time.date()] += get_timedelta(first_log).total_seconds()
if last_log.new_role == ROLES['engineer']:
self.fill_daterange(last_log.change_time.date() + timedelta(days=1), self.end_date + timedelta(days=1))
if last_log.change_time.date() == timezone.now().date():
self.statistic[last_log.change_time.date()] += (
get_timedelta(None, timezone.now().time()) - get_timedelta(last_log)
).total_seconds()
else:
self.statistic[last_log.change_time.date()] += (
timedelta(days=1) - get_timedelta(last_log)).total_seconds()
if self.end_date == timezone.now().date():
self.statistic[self.end_date] = get_timedelta(None, timezone.now().time()).total_seconds()
for log_index in range(len(self.data) - 1):
if self.data[log_index].new_role == ROLES['engineer']:
current_log, next_log = self.data[log_index], self.data[log_index + 1]
if current_log.change_time.date() != next_log.change_time.date():
self.statistic[current_log.change_time.date()] += (
timedelta(days=1) - get_timedelta(current_log)).total_seconds()
self.statistic[next_log.change_time.date()] += get_timedelta(next_log).total_seconds()
self.fill_daterange(current_log.change_time.date() + timedelta(days=1), next_log.change_time.date())
else:
elapsed_time = next_log.change_time - current_log.change_time
self.statistic[current_log.change_time.date()] += elapsed_time.total_seconds()
def fill_daterange(self, first, last, val=24 * 3600):
"""
Заполение диапазона дат значением val
по умолчанию val = кол-во секунд в 1 дне
"""
for day in daterange(first, last):
self.statistic[day] = val
def clear_statistic(self):
"""
Обнуление всех дней
"""
self.statistic.clear()
self.fill_daterange(self.start_date, self.end_date + timedelta(days=1), 0)

View File

@ -1,19 +1,21 @@
from django import forms
from django.contrib.auth.forms import AuthenticationForm
from django_registration.forms import RegistrationFormUniqueEmail
from access_controller.settings import ZENDESK_ROLES
from main.models import UserProfile
class CustomRegistrationForm(RegistrationFormUniqueEmail):
"""
Форма для регистрации :class:`django_registration.forms.RegistrationFormUniqueEmail`
с добавлением bootstrap-класса 'form-control'
Форма для регистрации :class:`django_registration.forms.RegistrationFormUniqueEmail`
:param password_zen: Поле для ввода пароля от Zendesk
:type password_zen: :class:`django.forms.CharField`
с добавлением bootstrap-класса "form-control"
:param visible_fields.email: Поле для ввода email, зарегистирированного на Zendesk
:type visible_fields.email: :class:`django_registration.forms.RegistrationFormUniqueEmail`
"""
def __init__(self, *args, **kwargs):
def __init__(self, *args, **kwargs) -> RegistrationFormUniqueEmail:
super().__init__(*args, **kwargs)
for visible in self.visible_fields():
if visible.field.widget.attrs.get('class', False):
@ -21,7 +23,7 @@ class CustomRegistrationForm(RegistrationFormUniqueEmail):
visible.field.widget.attrs['class'] += 'form-control'
else:
visible.field.widget.attrs['class'] = 'form-control'
if visible.html_name !='email':
if visible.html_name != 'email':
visible.field.required = False
class Meta(RegistrationFormUniqueEmail.Meta):
@ -29,12 +31,66 @@ class CustomRegistrationForm(RegistrationFormUniqueEmail):
class AdminPageUsers(forms.Form):
"""
Форма для установки статусов engineer или light_agent пользователям
:param users: Поле для установки статуса
:type users: :class:`ModelMultipleChoiceField`
"""
users = forms.ModelMultipleChoiceField(
queryset=UserProfile.objects.filter(role='agent'),
widget=forms.CheckboxSelectMultiple(
attrs={
'class': 'form-check-input'
'class': 'form-check-input',
}
),
label=''
)
class CustomAuthenticationForm(AuthenticationForm):
"""
Форма для авторизации :class:`django.contrib.auth.forms.AuthenticationForm`
с изменением поля username на email
"""
username = forms.CharField(
label="Электронная почта",
widget=forms.EmailInput(),
)
error_messages = {
'invalid_login':
"Пожалуйста, введите правильные электронную почту и пароль. Оба поля "
"могут быть чувствительны к регистру."
,
'inactive': "Аккаунт не активен.",
}
class StatisticForm(forms.Form):
email = forms.EmailField(
label='Электроная почта',
)
interval = forms.CharField( # TODO: Переделать под html страницу
label='Интервал работы',
)
display_format = forms.CharField( # TODO: Переделать под html страницу
label='Формат отображения',
)
range_start = forms.DateField( # TODO: Переделать под html страницу
label='Начало диапазона',
widget=forms.DateInput(
attrs={
'type': 'date',
}
),
)
range_end = forms.DateField( # TODO: Переделать под html страницу
label='Конец диапазона',
widget=forms.DateInput(
attrs={
'type': 'date',
}
),
)

View File

@ -0,0 +1,17 @@
# Generated by Django 3.1.6 on 2021-03-02 19:55
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('main', '0004_rolechangelogs'),
]
operations = [
migrations.AlterModelOptions(
name='userprofile',
options={'permissions': [('admin', 'Have access to control page')]},
),
]

View File

@ -0,0 +1,16 @@
# Generated by Django 3.1.6 on 2021-03-03 19:32
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('main', '0005_auto_20210302_2255'),
]
operations = [
migrations.DeleteModel(
name='UserProfile',
),
]

View File

@ -0,0 +1,29 @@
# Generated by Django 3.1.6 on 2021-03-03 19:35
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', '0006_delete_userprofile'),
]
operations = [
migrations.CreateModel(
name='UserProfile',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('role', models.CharField(default='None', max_length=100)),
('image', models.URLField(blank=True, null=True)),
('name', models.CharField(default='None', max_length=100)),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'permissions': [('control_access', 'User has access to control page')],
},
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 3.1.6 on 2021-03-03 20:05
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('main', '0007_userprofile'),
]
operations = [
migrations.AlterModelOptions(
name='userprofile',
options={},
),
]

View File

@ -0,0 +1,61 @@
# Generated by Django 3.1.6 on 2021-03-11 08:00
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', '0008_auto_20210303_2305'),
]
operations = [
migrations.AlterField(
model_name='rolechangelogs',
name='change_time',
field=models.DateTimeField(help_text='Дата и время изменения роли'),
),
migrations.AlterField(
model_name='rolechangelogs',
name='changed_by',
field=models.ForeignKey(help_text='Кем была изменена роль', on_delete=django.db.models.deletion.CASCADE, related_name='changed_by', to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='rolechangelogs',
name='name',
field=models.TextField(help_text='Имя пользователя'),
),
migrations.AlterField(
model_name='rolechangelogs',
name='new_role',
field=models.TextField(help_text='Присвоенная роль'),
),
migrations.AlterField(
model_name='rolechangelogs',
name='user',
field=models.ForeignKey(help_text='Пользователь, которому присвоили другую роль', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='userprofile',
name='image',
field=models.URLField(blank=True, help_text='Аватарка', null=True),
),
migrations.AlterField(
model_name='userprofile',
name='name',
field=models.CharField(default='None', help_text='Имя пользователя на нашем сайте', max_length=100),
),
migrations.AlterField(
model_name='userprofile',
name='role',
field=models.CharField(default='None', help_text='Код роли пользователя', max_length=100),
),
migrations.AlterField(
model_name='userprofile',
name='user',
field=models.OneToOneField(help_text='Пользователь', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 3.1.6 on 2021-03-11 08:04
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('main', '0009_models_help_text'),
]
operations = [
migrations.AlterModelOptions(
name='userprofile',
options={'permissions': (('has_control_access', 'Can view admin page'),)},
),
]

View File

@ -0,0 +1,28 @@
# Generated by Django 3.1.6 on 2021-03-11 14:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0010_userprofile_meta'),
]
operations = [
migrations.AddField(
model_name='rolechangelogs',
name='old_role',
field=models.IntegerField(default=0, help_text='Старая роль'),
),
migrations.AlterField(
model_name='rolechangelogs',
name='new_role',
field=models.IntegerField(default=0, help_text='Присвоенная роль'),
),
migrations.AlterField(
model_name='userprofile',
name='role',
field=models.IntegerField(default=0, help_text='Код роли пользователя'),
),
]

View File

@ -0,0 +1,29 @@
# Generated by Django 3.1.6 on 2021-03-11 17:27
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', '0011_auto_20210311_1734'),
]
operations = [
migrations.RemoveField(
model_name='rolechangelogs',
name='name',
),
migrations.CreateModel(
name='UnassignedTicket',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('ticket_id', models.IntegerField(help_text='Номер тикера, для которого сняли ответственного')),
('status', models.IntegerField(choices=[(0, 'Снят с пользователя, перенесён в буферную группу'), (1, 'Авторство восстановлено'), (2, 'Пока нас не было, тикет испарился из буферной группы. Дополнительные действия не требуются')], default=0)),
('assignee', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tickets', to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.1.6 on 2021-03-11 17:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0012_auto_20210311_2027'),
]
operations = [
migrations.AlterField(
model_name='unassignedticket',
name='status',
field=models.IntegerField(choices=[(0, 'Снят с пользователя, перенесён в буферную группу'), (1, 'Авторство восстановлено'), (2, 'Пока нас не было, тикет испарился из буферной группы. Дополнительные действия не требуются'), (3, 'Тикет уже был закрыт. Дополнительные действия не требуются')], default=0),
),
]

View File

@ -0,0 +1,29 @@
# Generated by Django 3.1.6 on 2021-03-14 11:55
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('main', '0013_auto_20210311_2040'),
]
operations = [
migrations.AddField(
model_name='userprofile',
name='custom_role_id',
field=models.IntegerField(default=0, help_text='Код роли пользователя'),
),
migrations.AlterField(
model_name='rolechangelogs',
name='change_time',
field=models.DateTimeField(default=django.utils.timezone.now, help_text='Дата и время изменения роли'),
),
migrations.AlterField(
model_name='userprofile',
name='role',
field=models.CharField(default='None', help_text='Глобальное имя роли пользователя', max_length=100),
),
]

View File

@ -2,25 +2,22 @@ from django.db import models
from django.contrib.auth.models import User
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.utils import timezone
class UserProfile(models.Model):
"""
Модель профиля пользователя
"""Модель профиля пользователя"""
:param user: OneToOneField к модели :class:`django.contrib.auth.models.User`
:param role: Код роли пользователя
:type role: :class:`integer`
:param image: Аватарка
:type image: :class:`img`
:param name: Имя пользователя на нашем сайте
:type name: :class:`str`
"""
class Meta:
permissions = (
('has_control_access', 'Can view admin page'),
)
user = models.OneToOneField(to=User, on_delete=models.CASCADE)
role = models.CharField(default='None', max_length=100)
image = models.URLField(null=True, blank=True)
name = models.CharField(default='None', max_length=100)
user = models.OneToOneField(to=User, on_delete=models.CASCADE, help_text='Пользователь')
role = models.CharField(default='None', max_length=100, help_text='Глобальное имя роли пользователя')
custom_role_id = models.IntegerField(default=0, help_text='Код роли пользователя')
image = models.URLField(null=True, blank=True, help_text='Аватарка')
name = models.CharField(default='None', max_length=100, help_text='Имя пользователя на нашем сайте')
@receiver(post_save, sender=User)
@ -35,20 +32,25 @@ def save_user_profile(sender, instance, **kwargs):
class RoleChangeLogs(models.Model):
"""
Модель для логирования изменений ролей пользователя
"""Модель для логирования изменений ролей пользователя"""
user = models.ForeignKey(to=User, on_delete=models.CASCADE,
help_text='Пользователь, которому присвоили другую роль')
old_role = models.IntegerField(default=0, help_text='Старая роль')
new_role = models.IntegerField(default=0, help_text='Присвоенная роль')
change_time = models.DateTimeField(help_text='Дата и время изменения роли', default=timezone.now)
changed_by = models.ForeignKey(to=User, on_delete=models.CASCADE, related_name='changed_by',
help_text='Кем была изменена роль')
:param user: Пользователь, которому присвоили другую роль, ForeignKey к модели :class:`django.contrib.auth.models.User`
:param name: Имя пользователя
:type name: :class:`str`
:param new_role: Присвоенная роль
:type new_role: :class:`str`
:param change_time: Дата изменения роли`
:type change_time: :class:`datetime.datetime`
:param changed_by: Кем была изменена роль, ForeignKey к модели :class:`django.contrib.auth.models.User`
"""
user = models.ForeignKey(to=User, on_delete=models.CASCADE)
name = models.TextField()
new_role = models.TextField()
change_time = models.DateTimeField()
changed_by = models.ForeignKey(to=User, on_delete=models.CASCADE, related_name='changed_by')
class UnassignedTicketStatus(models.IntegerChoices):
UNASSIGNED = 0, 'Снят с пользователя, перенесён в буферную группу'
RESTORED = 1, 'Авторство восстановлено'
NOT_FOUND = 2, 'Пока нас не было, тикет испарился из буферной группы. Дополнительные действия не требуются'
CLOSED = 3, 'Тикет уже был закрыт. Дополнительные действия не требуются'
SOLVED = 4, 'Тикет решён. Записан на пользователя с почтой SOLVED_TICKETS_EMAIL'
class UnassignedTicket(models.Model):
assignee = models.ForeignKey(to=User, on_delete=models.CASCADE, related_name='tickets')
ticket_id = models.IntegerField(help_text='Номер тикера, для которого сняли ответственного')
status = models.IntegerField(choices=UnassignedTicketStatus.choices, default=UnassignedTicketStatus.UNASSIGNED)

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>
{% block extra_css %}{% endblock %}
{% block extra_scripts %}{% endblock %}
</head>
<body class="d-flex flex-column h-100">

View File

@ -3,14 +3,19 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<nav class="navbar navbar-light" style="background-color: #00FF00;">
<nav class="navbar navbar-light" style="background-color: #113A60;">
<a class="navbar-brand" href="{% url 'index' %}">
<img src="{% static 'main/img/logo.png' %}" width="30" height="30" class="d-inline-block align-top" alt="" loading="lazy">
Access Controller
<img src="{% static 'main/img/logo_real.png' %}" width="107" height="22" class="d-inline-block align-top" 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>
{% if perms.main.has_control_access %}
<a class="btn btn-secondary" href="{% url 'control' %}">Управление</a>
{% else %}
<a class="btn btn-secondary" href="{% url 'work' request.user.id %}">Запрос прав</a>
{% endif %}
<a class="btn btn-secondary" href="{% url 'logout' %}">Выйти</a>
</div>
{% else %}

View File

@ -10,5 +10,5 @@
{% block content %}
<br>
<h4> Регистрация прошла успешно. <a href="/login/">Войти сейчас</a></h4>
<h4> Регистрация прошла успешно. <a href="{% url 'login'%}">Войти сейчас</a></h4>
{% endblock %}

View File

@ -2,7 +2,7 @@
{% load static %}
{% block title %}Управление{%endblock %}
{% block title %}Управление{% endblock %}
{% block heading %}Управление{% endblock %}
@ -10,52 +10,61 @@
<link rel="stylesheet" href="{% static 'main/css/work.css' %}"/>
{% 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 %}
<div class="container-md">
<div class="new-section">
<p class="row page-description">Основная информация о странице</p>
</div>
{% block form %}
<form method="post">
{% csrf_token %}
<div class="row justify-content-center new-section">
{% block hidden_form %}
<div style="display: none">
{% for field in form.users %}
{{ field.tag }}
{% endfor %}
</div>
{% endblock %}
<div class="col-10">
<h6 class="table-title">Список сотрудников</h6>
{% block table %}
<table class="light-table">
<thead>
<th>ID</th>
<th>Name</th>
<th>Email</th>
<th>Role</th>
<th>Name(link to profile)</th>
<th>Checked</th>
</thead>
<tbody>
<tbody id="old_tbody">
{% for user in users %}
<tr>
<td>{{ user.id }}</td>
<td><a href="#">{{ user.name }}</a></td>
<td>{{ user.user.email }}</td>
<td>{{ user.role }}</td>
<td><a href="#">{{ user.name }}</a></td>
<td class="checkbox_field"></td>
</tr>
{% endfor %}
</tbody>
<tbody id="new_tbody"></tbody>
</table>
{% endblock%}
</div>
</div>
{% block count %}
<div class="row justify-content-center new-section">
<div class="col-5">
<div class="info">
@ -91,19 +100,11 @@
</div>
</div>
{% endblock %}
</form>
{% endblock %}
</div>
<script>
"use strict";
let checkboxes = document.getElementsByName("users");
let fields = document.querySelectorAll(".checkbox_field");
if (checkboxes.length == fields.length) {
for (let i = 0; i < fields.length; ++i) {
let el = checkboxes[i].cloneNode(true);
fields[i].appendChild(el);
}
}
</script>
<script src="{% static 'main/js/control.js'%}" type="text/babel"></script>
{% endblock %}

View File

@ -10,17 +10,14 @@
{% block extra_css %}
<style>
.img{
<style>
.img {
width:auto;
height:auto;
max-width:100px!important;
max-height:100px!important;
}
</style>
</style>
{% endblock %}
{% block content %}
@ -28,24 +25,24 @@
<div class="row">
<div class="col-auto">
<div class="container">
{% if image_url %}
<img src={{image_url}} class="img img-thumbnail" alt="Аватар">
{% else %}
<img src="{% static 'no_avatar.png' %}" class="img img-thumbnail" alt="Нет изображения">
{% endif %}
<img
src="{% if profile.image %}{{ profile.image }}{% else %}{% static 'no_avatar.png' %}{% endif %}"
class="img img-thumbnail"
alt="Нет изображения"
>
</div>
</div>
<div class="col">
<h5><span class="badge bg-secondary text-light">Имя пользователя</span> {{name}}</h5>
<h5><span class="badge bg-secondary text-light">Имя пользователя</span> {{ profile.name }}</h5>
<br>
<h5><span class="badge bg-secondary text-light">Электронная почта</span> {{email}}</h5>
<h5><span class="badge bg-secondary text-light">Электронная почта</span> {{ profile.user.email }}</h5>
<br>
<h5><span class="badge bg-secondary text-light">Текущая роль</span> {{role}}</h5>
<h5><span class="badge bg-secondary text-light">Текущая роль</span> {{ profile.role }}</h5>
</div>
</div>
<div align="center">
<form action="">
<button class="btn btn-primary"><big>Запросить права доступа</big></button>
<a href="{% url 'work' profile.user.id %}" class="btn btn-primary"><big>Запросить права доступа</big></a>
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,49 @@
{% extends 'base/base.html' %}
{% load static %}
{% block title %}{{ pagename }}{% endblock %}
{% block heading %} Пример страницы статистики(палками не бейти плиз){% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'main/css/work.css' %}">
{% endblock %}
{% block content %}
<div>
<form action="" method="post">
{% csrf_token %}
{% for field in form %}
{{field.label}}
{{field}}
<br>
{% endfor %}
<input type="submit">
</form>
<ul>
{% for error in errors %}
<li><span class="badge bg-danger">{{error}}</span></li>
{% endfor %}
</ul>
{%if form.errors%}
<ul>
{% for field, errors in form.errors.items %}
{% for error in errors %}
<li><span class="badge bg-danger">{{error}}</span></li>
{% endfor %}
{% endfor %}
</ul>
{%endif%}
<ul>
{% for warning in warnings %}
<li><span class="badge bg-warning">{{warning}}</span></li>
{% endfor %}
</ul>
{% for key,val in log_stats.items %}
<h3>{{key}} <b>|</b> {{val}}</h3>
<br>
{% endfor %}
</div>
{% endblock %}

View File

@ -4,7 +4,7 @@
{% block title %}{{ pagename }}{% endblock %}
{% block heading %}Управление{% endblock %}
{% block heading %}Управление правами{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'main/css/work.css' %}">
@ -22,25 +22,16 @@
<h6 class="table-title">Список сотрудников с правами инженера</h6>
<table class="light-table">
<thead>
<th>ID</th>
<th>email</th>
<th>Expiration Date</th>
<th>Name(link to profile)</th>
<th>Email</th>
<th>Name</th>
</thead>
<tbody>
<tr>
<td>1</td>
<td>big_boss123@example.ru</td>
<td>19:30 18.02.21</td>
<td><a href="#">Иван Иванов</a></td>
</tr>
<tr>
<td>2</td>
<td>gachi_cool456@example.ru</td>
<td>21:00 18.02.21</td>
<td><a href="#">Пётр Петров</a></td>
</tr>
{% for engineer in engineers %}
<tr>
<td>{{ engineer.email }}</td>
<td>{{ engineer.name }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
@ -52,23 +43,24 @@
<div class="info-target">инженеров: </div>
<div class="info-quantity">
<div class="status-circle-small light-green"></div>
<span class="info-quantity-value">13</span>
<span class="info-quantity-value">{{ engineers|length }}</span>
</div>
</div>
<div class="info-row">
<div class="info-target">легких агентов:</div>
<div class="info-quantity">
<div class="status-circle-small light-yellow"></div>
<span class="info-quantity-value">22</span>
<span class="info-quantity-value">{{ agents|length }}</span>
</div>
</div>
</div>
</div>
<div class="col-5">
<button class="request-acess-button default-button">Получить права инженера</button>
<button class="hand-over-acess-button default-button">Сдать права инженера</button>
<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>
</div>
{% endblock %}
{% endblock %}

View File

@ -30,7 +30,7 @@
{% endif %}
<div class="text-center">
<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>
</form>
</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,39 +1,48 @@
from django.contrib.auth.decorators import login_required
from django.contrib.auth.forms import PasswordResetForm
from django.contrib.auth.models import User
from django.contrib.auth.tokens import default_token_generator
from django.shortcuts import render, get_list_or_404
from django.urls import reverse_lazy
from django.views.generic import FormView
from django_registration.backends.one_step.views import RegistrationView
from access_controller.settings import EMAIL_HOST_USER
from main.extra_func import check_user_exist, update_profile, get_user_organization, \
make_engineer, make_light_agent, get_users_list
from django.contrib.auth.models import User
from main.models import UserProfile
from main.forms import CustomRegistrationForm, AdminPageUsers
from django_registration.views import RegistrationView
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import PermissionDenied
import logging
from access_controller.settings import ZENDESK_ROLES
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.views import LoginView
from django.contrib.contenttypes.models import ContentType
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.urls import reverse_lazy, reverse
from django.views.generic import FormView
from django_registration.views import RegistrationView
# 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 EMAIL_HOST_USER, ZENDESK_ROLES
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
from main.forms import AdminPageUsers, CustomRegistrationForm, CustomAuthenticationForm, StatisticForm
from main.serializers import ProfileSerializer
from .models import UserProfile
class CustomRegistrationView(RegistrationView):
"""
Отображение и логика работы страницы регистрации пользователя
1. Ввод email пользователя, указанный на Zendesk
2. В случае если пользователь с данным паролем зарегистрирован на Zendesk и относится к определенной организации, происходит сброс ссылки с установлением пароля на указанный email
3. Создается пользователь class User, а также его профиль
"""
form_class = CustomRegistrationForm
template_name = 'django_registration/registration_form.html'
success_url = reverse_lazy('django_registration_complete')
is_allowed = True
def register(self, form):
def register(self, form: CustomRegistrationForm) -> User:
self.is_allowed = True
if check_user_exist(form.data['email']) and get_user_organization(form.data['email']) == 'SYSTEM':
forms = PasswordResetForm(self.request.POST)
@ -55,13 +64,27 @@ class CustomRegistrationView(RegistrationView):
)
forms.save(**opts)
update_profile(user.userprofile)
self.set_permission(user)
return user
else:
raise ValueError('Непредвиденная ошибка')
else:
self.is_allowed = False
def get_success_url(self, user=None):
@staticmethod
def set_permission(user) -> None:
"""
Дает разрешение на просмотр страница администратора, если пользователь имеет роль admin
"""
if user.userprofile.role == 'admin':
content_type = ContentType.objects.get_for_model(UserProfile)
permission = Permission.objects.get(
codename='has_control_access',
content_type=content_type,
)
user.user_permissions.add(permission)
def get_success_url(self, user: User = None) -> success_url:
"""
Возвращает url-адрес страницы, куда нужно перейти после успешной/неуспешной регистрации
Используется самой django-registration
@ -73,68 +96,166 @@ class CustomRegistrationView(RegistrationView):
@login_required()
def profile_page(request):
def profile_page(request: WSGIRequest) -> HttpResponse:
"""
Отображение страницы профиля
:param request: объект с деталями запроса
:type request: :class:`django.http.HttpResponse`
:return: объект ответа сервера с HTML-кодом внутри
"""
user_profile = request.user.userprofile
user_profile: UserProfile = request.user.userprofile
update_profile(user_profile)
context = {
'email': user_profile.user.email,
'name': user_profile.name,
'role': user_profile.role,
'image_url': user_profile.image,
'profile': user_profile,
'pagename': 'Страница профиля'
}
return render(request, 'pages/profile.html', context)
def auth_user(request):
admin = ZendeskAdmin().admin
zenpy_user: ZenpyUser = admin.users.search(request.user.email).values[0]
return zenpy_user, admin
@login_required()
def work_page(request, id):
users = get_users_list()
if request.user.id == id:
engineers = []
light_agents = []
for user in users:
if user.custom_role_id == ZENDESK_ROLES['engineer']:
engineers.append(user)
elif user.custom_role_id == ZENDESK_ROLES['light_agent']:
light_agents.append(user)
context = {
'engineers': engineers,
'agents': light_agents,
'pagename': 'Управление правами'
}
return render(request, 'pages/work.html', context)
return redirect("login")
@login_required()
def work_hand_over(request):
zenpy_user, admin = auth_user(request)
if zenpy_user.custom_role_id == ZENDESK_ROLES['engineer']:
zenpy_user.custom_role_id = ZENDESK_ROLES['light_agent']
admin.users.update(zenpy_user)
request.user.userprofile.role = "agent"
request.user.userprofile.save()
return HttpResponseRedirect(reverse('work', args=(request.user.id,)))
@login_required()
def work_become_engineer(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']
admin.users.update(zenpy_user)
request.user.userprofile.role = "agent"
request.user.userprofile.save()
return HttpResponseRedirect(reverse('work', args=(request.user.id,)))
def main_page(request):
"""
Отображение логгирования на главной странице
"""
logger = logging.getLogger('main.index')
logger.info('Index page opened')
return render(request, 'pages/index.html')
class AdminPageView(FormView, LoginRequiredMixin):
class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin, FormView):
permission_required = 'main.has_control_access'
template_name = 'pages/adm_ruleset.html'
form_class = AdminPageUsers
success_url = '/control/'
def form_valid(self, form):
def form_valid(self, form: AdminPageUsers) -> AdminPageUsers:
"""
Функция установки ролей пользователям
"""
users = form.cleaned_data['users']
if 'engineer' in self.request.POST:
self.make_engineers(form.cleaned_data['users'])
self.make_engineers(users)
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)
@staticmethod
def make_engineers(users):
[make_engineer(user) for user in users]
@staticmethod
def make_light_agents(users):
[make_light_agent(user) for user in users]
@staticmethod
def count_users(users): # TODO: this func counts users from all zendesk instead of just from a model
engineers, light_agents = 0, 0
def make_engineers(self, users):
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
make_engineer(user, self.request.user)
def get_context_data(self, **kwargs):
if self.request.user.userprofile.role != 'admin':
raise PermissionDenied
def make_light_agents(self, users):
for user in users:
make_light_agent(user, self.request.user)
def get_context_data(self, **kwargs) -> dict:
"""
Функция формирования контента страницы администратора (с проверкой прав доступа)
"""
context = super().get_context_data(**kwargs)
context['users'] = get_list_or_404(
users = get_list_or_404(
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
class CustomLoginView(LoginView):
"""
Отображение страницы авторизации пользователя
"""
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()
def statistic_page(request):
if not request.user.has_perm('main.has_control_access'):
raise PermissionDenied
context = {
'pagename': 'страница статистики',
'errors': list(),
}
if request.method == "POST":
form = StatisticForm(request.POST)
if form.is_valid():
start_date, end_date = form.cleaned_data['range_start'], form.cleaned_data['range_end']
interval, show = form.cleaned_data['interval'], form.cleaned_data['display_format']
Data = StatisticData(start_date, end_date, form.cleaned_data['email'])
Data.set_display(show)
Data.set_interval(interval)
stats = Data.get_statistic()
if Data.errors:
context['errors'] = Data.errors
if Data.warnings:
context['warnings'] = Data.warnings
context['log_stats'] = stats if not context['errors'] else None
if request.method == 'GET':
form = StatisticForm()
context['form'] = form
return render(request, 'pages/stat.html', context)

View File

@ -3,9 +3,12 @@ Django==3.1.6
Pillow==8.1.0
zenpy~=2.0.24
django_registration==3.1.1
djangorestframework==3.12.2
# Documentation
Sphinx==3.4.3
sphinx-rtd-theme==0.5.1
sphinx-autodoc-typehints==1.11.1
pyenchant==3.2.0
sphinxcontrib-spelling==7.1.0

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

83
static/main/js/control.js Normal file
View File

@ -0,0 +1,83 @@
"use strict";
function move_checkboxes() {
let checkboxes = document.getElementsByName("users");
let fields = document.querySelectorAll(".checkbox_field");
if (checkboxes.length == fields.length) {
for (let i = 0; i < fields.length; ++i) {
let el = checkboxes[i].cloneNode(true);
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);