Merge branch 'develop' into feature/statistic/backend

# Conflicts:
#	access_controller/urls.py
#	main/forms.py
#	main/views.py
This commit is contained in:
Sokurov Idar 2021-03-04 23:16:21 +03:00
commit 4a604f0941
15 changed files with 218 additions and 88 deletions

View File

@ -16,6 +16,7 @@ Including another URLconf
from django.contrib import admin
from django.contrib.auth import views as auth_views
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 work_page, work_hand_over, work_become_engineer, \

View File

@ -14,21 +14,26 @@ 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
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
@ -46,7 +51,8 @@ author = 'SHP S101, group 2'
# The full version, including alpha/beta/rc tags
release = 'v0.01'
#Django sphinx setup by https://gist.github.com/codingjoe/314bda5a07ff3b41f247
# Django sphinx setup by https://gist.github.com/codingjoe/314bda5a07ff3b41f247
# -- General configuration ---------------------------------------------------
@ -121,7 +127,6 @@ def setup(app):
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.
@ -132,7 +137,6 @@ extensions = [
'sphinx.ext.viewcode',
'sphinx_rtd_theme',
'sphinx.ext.graphviz',
'sphinx.ext.napoleon',
'sphinx.ext.inheritance_diagram',
'sphinx_autodoc_typehints'
@ -141,7 +145,6 @@ extensions = [
if enchant is not None:
extensions.append('sphinxcontrib.spelling')
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
@ -157,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
@ -170,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 ---------------------------------------
@ -179,14 +180,9 @@ 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/'
),
'djangoextensions': ('https://django-extensions.readthedocs.org/en/latest/', None),
'geoposition': ('https://django-geoposition.readthedocs.org/en/latest/', None),
'braces': ('https://django-braces.readthedocs.org/en/latest/', None),
'select2': ('https://django-select2.readthedocs.org/en/latest/', None),
'celery': ('https://celery.readthedocs.org/en/latest/', None),
'https://docs.djangoproject.com/en/dev/',
'https://docs.djangoproject.com/en/dev/_objects/'
),
}
autodoc_default_flags = ['members']

View File

@ -75,7 +75,10 @@ class ZendeskAdmin:
def get_user(self, email: str) -> str:
"""
Функция **get_user** возвращает пользователя (объект) по его email
Функция **get_user** возвращает пользователя (объект) по его email
:param email: email пользователя
:return: email пользователя, найденного в БД
"""
return self.admin.users.search(email).values[0]

View File

@ -7,11 +7,14 @@ from main.models import UserProfile
class CustomRegistrationForm(RegistrationFormUniqueEmail):
"""
Форма для регистрации :class:`django_registration.forms.RegistrationFormUniqueEmail`
с добавлением bootstrap-класса 'form-control'
"""
Форма для регистрации :class:`django_registration.forms.RegistrationFormUniqueEmail`
def __init__(self, *args, **kwargs):
с добавлением bootstrap-класса "form-control"
:param visible_fields.email: Поле для ввода email, зарегистирированного на Zendesk
:type visible_fields.email: :class:`django_registration.forms.RegistrationFormUniqueEmail`
"""
def __init__(self, *args, **kwargs) -> RegistrationFormUniqueEmail:
super().__init__(*args, **kwargs)
for visible in self.visible_fields():
if visible.field.widget.attrs.get('class', False):
@ -28,7 +31,10 @@ class CustomRegistrationForm(RegistrationFormUniqueEmail):
class AdminPageUsers(forms.Form):
"""
Форма для установки статуса
Форма для установки статусов engineer или light_agent пользователям
:param users: Поле для установки статуса
:type users: :class:`ModelMultipleChoiceField`
"""
users = forms.ModelMultipleChoiceField(
@ -44,8 +50,8 @@ class AdminPageUsers(forms.Form):
class CustomAuthenticationForm(AuthenticationForm):
"""
Форма для авторизации :class:`django.contrib.auth.forms.AuthenticationForm`
с изменением поля username на email
Форма для авторизации :class:`django.contrib.auth.forms.AuthenticationForm`
с изменением поля username на email
"""
username = forms.CharField(
label="Электронная почта",

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

@ -5,10 +5,7 @@ from django.dispatch import receiver
class UserProfile(models.Model):
"""
Модель профиля пользователя
"""
"""Модель профиля пользователя"""
user = models.OneToOneField(to=User, on_delete=models.CASCADE, help_text='Пользователь')
role = models.CharField(default='None', max_length=100, help_text='Код роли пользователя')
@ -28,10 +25,7 @@ def save_user_profile(sender, instance, **kwargs):
class RoleChangeLogs(models.Model):
"""
Модель для логирования изменений ролей пользователя
"""
"""Модель для логирования изменений ролей пользователя"""
user = models.ForeignKey(to=User, on_delete=models.CASCADE, help_text='Пользователь, которому присвоили другую роль')
name = models.TextField(help_text='Имя пользователя')
old_role = models.TextField(help_text='Старая роль')

View File

@ -11,6 +11,9 @@
{% 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>
{% endif %}
<a class="btn btn-secondary" href="{% url 'logout' %}">Выйти</a>
</div>
{% else %}

View File

@ -2,7 +2,7 @@
{% load static %}
{% block title %}Управление{%endblock %}
{% block title %}Управление{% endblock %}
{% block heading %}Управление{% endblock %}
@ -16,19 +16,24 @@
<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>
@ -52,10 +57,12 @@
</tbody>
</table>
{% endblock%}
</div>
</div>
{% block count %}
<div class="row justify-content-center new-section">
<div class="col-5">
<div class="info">
@ -91,19 +98,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'%}"></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="">
<a href="/work/{{ id }}" class="btn btn-primary"><big>Запросить права доступа</big></a>
<a href="{% url 'work' profile.user.id %}" class="btn btn-primary"><big>Запросить права доступа</big></a>
</form>
</div>
{% endblock %}

View File

@ -1,19 +1,30 @@
import logging
import os
from django.contrib.auth.decorators import login_required
from django.contrib.auth.forms import PasswordResetForm
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import User
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.core.exceptions import PermissionDenied
from django.http import HttpResponseRedirect
from django.shortcuts import get_list_or_404, redirect, reverse, render
from django.urls import reverse_lazy
from django.contrib.contenttypes.models import ContentType
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
from zenpy import Zenpy
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, Permission
from main.models import UserProfile
from main.forms import CustomRegistrationForm, AdminPageUsers, CustomAuthenticationForm
from django_registration.views import RegistrationView
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.core.exceptions import PermissionDenied
from access_controller.settings import ZENDESK_ROLES
from zenpy.lib.api_objects import User as ZenpyUser
from access_controller.settings import EMAIL_HOST_USER, ZENDESK_ROLES
@ -22,17 +33,27 @@ from main.extra_func import check_user_exist, update_profile, get_user_organizat
from main.forms import AdminPageUsers, CustomRegistrationForm, CustomAuthenticationForm, StatisticForm
from .models import UserProfile, RoleChangeLogs
content_type_temp = ContentType.objects.get_for_model(UserProfile)
permission_temp, created = Permission.objects.get_or_create(
codename='has_control_access',
content_type=content_type_temp,
)
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)
@ -54,13 +75,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
@ -72,23 +107,14 @@ 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,
'id': user_profile.id,
'image_url': user_profile.image,
'profile': user_profile,
'pagename': 'Страница профиля'
}
return render(request, 'pages/profile.html', context)
@ -140,17 +166,24 @@ def work_become_engineer(request):
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:
"""
Функция установки ролей пользователям
"""
if 'engineer' in self.request.POST:
self.make_engineers(form.cleaned_data['users'])
elif 'light_agent' in self.request.POST:
@ -166,7 +199,13 @@ class AdminPageView(FormView, LoginRequiredMixin):
[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
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']:
@ -175,7 +214,10 @@ class AdminPageView(FormView, LoginRequiredMixin):
light_agents += 1
return engineers, light_agents
def get_context_data(self, **kwargs):
def get_context_data(self, **kwargs) -> dict:
"""
Функция формирования контента страницы администратора (с проверкой прав доступа)
"""
if self.request.user.userprofile.role != 'admin':
raise PermissionDenied
context = super().get_context_data(**kwargs)

View File

@ -4,8 +4,9 @@ Pillow==8.1.0
zenpy~=2.0.24
django_registration==3.1.1
# 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

View File

@ -0,0 +1,9 @@
"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);
}
}