diff --git a/access_controller/urls.py b/access_controller/urls.py index 6554616..1406e69 100644 --- a/access_controller/urls.py +++ b/access_controller/urls.py @@ -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, \ diff --git a/docs/source/conf.py b/docs/source/conf.py index fdcc4e4..c9efe47 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -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'] diff --git a/main/extra_func.py b/main/extra_func.py index 8929d3e..623be63 100644 --- a/main/extra_func.py +++ b/main/extra_func.py @@ -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] diff --git a/main/forms.py b/main/forms.py index ff6a9d3..efafa01 100644 --- a/main/forms.py +++ b/main/forms.py @@ -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="Электронная почта", diff --git a/main/migrations/0005_auto_20210302_2255.py b/main/migrations/0005_auto_20210302_2255.py new file mode 100644 index 0000000..dff2dc2 --- /dev/null +++ b/main/migrations/0005_auto_20210302_2255.py @@ -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')]}, + ), + ] diff --git a/main/migrations/0006_delete_userprofile.py b/main/migrations/0006_delete_userprofile.py new file mode 100644 index 0000000..23adaab --- /dev/null +++ b/main/migrations/0006_delete_userprofile.py @@ -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', + ), + ] diff --git a/main/migrations/0007_userprofile.py b/main/migrations/0007_userprofile.py new file mode 100644 index 0000000..2b05dd7 --- /dev/null +++ b/main/migrations/0007_userprofile.py @@ -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')], + }, + ), + ] diff --git a/main/migrations/0008_auto_20210303_2305.py b/main/migrations/0008_auto_20210303_2305.py new file mode 100644 index 0000000..8082682 --- /dev/null +++ b/main/migrations/0008_auto_20210303_2305.py @@ -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={}, + ), + ] diff --git a/main/models.py b/main/models.py index 13bd55e..74aa5ba 100644 --- a/main/models.py +++ b/main/models.py @@ -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='Старая роль') diff --git a/main/templates/base/menu.html b/main/templates/base/menu.html index 8448b0c..f64ceb3 100644 --- a/main/templates/base/menu.html +++ b/main/templates/base/menu.html @@ -11,6 +11,9 @@ {% if request.user.is_authenticated %}
Профиль + {% if perms.main.has_control_access %} + Управление + {% endif %} Выйти
{% else %} diff --git a/main/templates/pages/adm_ruleset.html b/main/templates/pages/adm_ruleset.html index 1bddc5f..387cd73 100644 --- a/main/templates/pages/adm_ruleset.html +++ b/main/templates/pages/adm_ruleset.html @@ -2,7 +2,7 @@ {% load static %} -{% block title %}Управление{%endblock %} +{% block title %}Управление{% endblock %} {% block heading %}Управление{% endblock %} @@ -16,19 +16,24 @@

Основная информация о странице

+ {% block form %}
{% csrf_token %}
+ {% block hidden_form %}
{% for field in form.users %} {{ field.tag }} {% endfor %}
+ {% endblock %}
Список сотрудников
+ + {% block table %} @@ -52,10 +57,12 @@
+ {% endblock%}
+ {% block count %}
@@ -91,19 +98,11 @@
+ {% endblock %} + {% endblock %}
- + {% endblock %} diff --git a/main/templates/pages/profile.html b/main/templates/pages/profile.html index 6c6ecd2..a0f21f9 100644 --- a/main/templates/pages/profile.html +++ b/main/templates/pages/profile.html @@ -10,17 +10,14 @@ {% block extra_css %} - + {% endblock %} {% block content %} @@ -28,24 +25,24 @@
- {% if image_url %} - Аватар - {% else %} - Нет изображения - {% endif %} + Нет изображения
-
Имя пользователя {{name}}
+
Имя пользователя {{ profile.name }}

-
Электронная почта {{email}}
+
Электронная почта {{ profile.user.email }}

-
Текущая роль {{role}}
+
Текущая роль {{ profile.role }}
- Запросить права доступа + Запросить права доступа
{% endblock %} diff --git a/main/views.py b/main/views.py index a489b01..3f2df8f 100644 --- a/main/views.py +++ b/main/views.py @@ -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) diff --git a/requirements.txt b/requirements.txt index 4161b6c..7a4f941 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/static/main/js/control.js b/static/main/js/control.js new file mode 100644 index 0000000..1fd4f9c --- /dev/null +++ b/static/main/js/control.js @@ -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); + } +}