From bf69c870cdd88d2da042fe68d49f1cfceababcb2 Mon Sep 17 00:00:00 2001 From: Andrey Kovalev Date: Wed, 17 Feb 2021 20:43:57 +0300 Subject: [PATCH 01/54] Add rights management backend --- access_controller/urls.py | 5 +++- main/templates/pages/work.html | 31 ++++++++++++++++++++ main/views.py | 53 +++++++++++++++++++++++++++++++++- 3 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 main/templates/pages/work.html diff --git a/access_controller/urls.py b/access_controller/urls.py index b2603b2..bf79ff6 100644 --- a/access_controller/urls.py +++ b/access_controller/urls.py @@ -20,7 +20,7 @@ from django.urls import path, include from access_controller import settings from access_controller.settings import DEBUG -from main.views import main_page, profile_page, CustomRegistrationView +from main.views import main_page, profile_page, CustomRegistrationView, work_page, work_hand_over, work_become_engineer urlpatterns = [ path('admin/', admin.site.urls, name='admin'), @@ -30,4 +30,7 @@ urlpatterns = [ path('accounts/login/', LoginView.as_view(extra_context={}), name='login'), # TODO add extra context path('accounts/', include('django.contrib.auth.urls')), path('accounts/', include('django_registration.backends.one_step.urls')), + path('work/', 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"), ] diff --git a/main/templates/pages/work.html b/main/templates/pages/work.html new file mode 100644 index 0000000..5f49290 --- /dev/null +++ b/main/templates/pages/work.html @@ -0,0 +1,31 @@ +{% extends 'base/base.html' %} + +{% load static %} + + +{% block title %}{{ pagename }}{% endblock %} + + +{% block heading %}Управление правами{% endblock %} + +{% block content %} +
+

Инженеры

+{%for engineer in engineers%} + {{ engineer.name }} +{% endfor %} + +

Агенты

+{%for agent in agents%} + {{ agent.name }} +{% endfor %} + +
+ +{% if role == "engineer" %} + Сдать смену +{% else %} + Запросить права инженера +{% endif %} + +{% endblock %} diff --git a/main/views.py b/main/views.py index d9f8fd9..ee22299 100644 --- a/main/views.py +++ b/main/views.py @@ -1,4 +1,5 @@ -from django.shortcuts import render +from django.shortcuts import render, redirect, reverse +from django.http import HttpResponseRedirect from django.urls import reverse_lazy from main.extra_func import set_and_get_name, set_and_get_email, load_and_get_image, set_and_get_role, check_user_exist, \ @@ -12,6 +13,11 @@ from django_registration.views import RegistrationView from django.contrib.auth.decorators import login_required from zenpy import Zenpy +from zenpy.lib.api_objects import User as ZenpyUser + +from .models import UserProfile + +import os class CustomRegistrationView(RegistrationView): @@ -75,6 +81,51 @@ def profile_page(request): } return render(request, 'pages/profile.html', context) +def auth_user(request): + admin_creds = { + 'email': os.environ.get('Admin_email'), + 'subdomain': 'ngenix1612197338', + 'token': os.environ.get('Oauth_token'), + } + admin = Zenpy(**admin_creds) + zenpy_user: ZenpyUser = admin.users.search(request.user.email).values[0] + return zenpy_user, admin + +@login_required() +def work_page(request, id): + if request.user.is_authenticated and request.user.id == id: + zenpy_user, _ = auth_user(request) + + context = { + 'engineers': UserProfile.objects.filter(role=1), + 'agents': UserProfile.objects.filter(role=0), + 'role': zenpy_user.role, + '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 request.user.is_authenticated and zenpy_user.role == "end-user": + zenpy_user.role = "agent" + admin.users.update(zenpy_user) + request.user.userprofile.role = 0 + 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 request.user.is_authenticated and zenpy_user.role == "agent": + zenpy_user.role = "end-user" + admin.users.update(zenpy_user) + request.user.userprofile.role = 1 + request.user.userprofile.save() + return HttpResponseRedirect(reverse('work', args=(request.user.id, ))) + def main_page(request): return render(request, 'pages/index.html') From 8ba662c0caecb5265cf2b891c0365157ecaf24a4 Mon Sep 17 00:00:00 2001 From: Andrey Kovalev Date: Wed, 17 Feb 2021 21:38:41 +0300 Subject: [PATCH 02/54] Fix role end-user -> admin --- main/templates/pages/work.html | 2 +- main/views.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/main/templates/pages/work.html b/main/templates/pages/work.html index 5f49290..4719492 100644 --- a/main/templates/pages/work.html +++ b/main/templates/pages/work.html @@ -22,7 +22,7 @@
-{% if role == "engineer" %} +{% if role == "admin" %} Сдать смену {% else %} Запросить права инженера diff --git a/main/views.py b/main/views.py index ee22299..21e5f97 100644 --- a/main/views.py +++ b/main/views.py @@ -108,7 +108,7 @@ def work_page(request, id): @login_required() def work_hand_over(request): zenpy_user, admin = auth_user(request) - if request.user.is_authenticated and zenpy_user.role == "end-user": + if request.user.is_authenticated and zenpy_user.role == "admin": zenpy_user.role = "agent" admin.users.update(zenpy_user) request.user.userprofile.role = 0 @@ -120,7 +120,7 @@ def work_hand_over(request): def work_become_engineer(request): zenpy_user, admin = auth_user(request) if request.user.is_authenticated and zenpy_user.role == "agent": - zenpy_user.role = "end-user" + zenpy_user.role = "admin" admin.users.update(zenpy_user) request.user.userprofile.role = 1 request.user.userprofile.save() From b1d42ece61614080af92453548788b5a0fc00822 Mon Sep 17 00:00:00 2001 From: Andrey Kovalev Date: Thu, 25 Feb 2021 19:01:09 +0300 Subject: [PATCH 03/54] Fixed name parametrs in env --- main/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main/views.py b/main/views.py index 917d5fa..5d362d7 100644 --- a/main/views.py +++ b/main/views.py @@ -98,9 +98,9 @@ def profile_page(request): def auth_user(request): admin_creds = { - 'email': os.environ.get('Admin_email'), + 'email': os.environ.get('ACCESS_CONTROLLER_API_EMAIL'), 'subdomain': 'ngenix1612197338', - 'token': os.environ.get('Oauth_token'), + 'token': os.environ.get('ACCESS_CONTROLLER_API_TOKEN'), } admin = Zenpy(**admin_creds) zenpy_user: ZenpyUser = admin.users.search(request.user.email).values[0] From 33375967493c7f13b5d75ac1ef424b1f7ff39b76 Mon Sep 17 00:00:00 2001 From: Andrew Smirnov Date: Thu, 25 Feb 2021 20:05:04 +0300 Subject: [PATCH 04/54] Remove useless auth check --- main/views.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/main/views.py b/main/views.py index 5d362d7..c1e7794 100644 --- a/main/views.py +++ b/main/views.py @@ -1,4 +1,3 @@ - from django.shortcuts import render, redirect, reverse from django.http import HttpResponseRedirect @@ -17,7 +16,6 @@ from django_registration.views import RegistrationView from django.contrib.auth.decorators import login_required import logging - from zenpy import Zenpy from zenpy.lib.api_objects import User as ZenpyUser @@ -26,7 +24,6 @@ from .models import UserProfile import os - class CustomRegistrationView(RegistrationView): """ Отображение и логика работы страницы регистрации пользователя @@ -96,6 +93,7 @@ def profile_page(request): } return render(request, 'pages/profile.html', context) + def auth_user(request): admin_creds = { 'email': os.environ.get('ACCESS_CONTROLLER_API_EMAIL'), @@ -106,11 +104,12 @@ def auth_user(request): zenpy_user: ZenpyUser = admin.users.search(request.user.email).values[0] return zenpy_user, admin + @login_required() def work_page(request, id): if request.user.is_authenticated and request.user.id == id: zenpy_user, _ = auth_user(request) - + context = { 'engineers': UserProfile.objects.filter(role=1), 'agents': UserProfile.objects.filter(role=0), @@ -120,26 +119,27 @@ def work_page(request, id): return render(request, 'pages/work.html', context) return redirect("login") + @login_required() def work_hand_over(request): zenpy_user, admin = auth_user(request) - if request.user.is_authenticated and zenpy_user.role == "admin": + if zenpy_user.role == "admin": zenpy_user.role = "agent" admin.users.update(zenpy_user) request.user.userprofile.role = 0 request.user.userprofile.save() - return HttpResponseRedirect(reverse('work', args=(request.user.id, ))) + return HttpResponseRedirect(reverse('work', args=(request.user.id,))) @login_required() def work_become_engineer(request): zenpy_user, admin = auth_user(request) - if request.user.is_authenticated and zenpy_user.role == "agent": + if zenpy_user.role == "agent": zenpy_user.role = "admin" admin.users.update(zenpy_user) request.user.userprofile.role = 1 request.user.userprofile.save() - return HttpResponseRedirect(reverse('work', args=(request.user.id, ))) + return HttpResponseRedirect(reverse('work', args=(request.user.id,))) def main_page(request): From f7522c9c69db883e92b042f70cd8d84dffecd2f7 Mon Sep 17 00:00:00 2001 From: Andrey Kovalev Date: Sun, 28 Feb 2021 17:27:07 +0300 Subject: [PATCH 05/54] Fix page html code --- main/templates/pages/profile.html | 2 +- main/templates/pages/work.html | 60 ++++++------------------------- main/views.py | 18 +++++----- 3 files changed, 20 insertions(+), 60 deletions(-) diff --git a/main/templates/pages/profile.html b/main/templates/pages/profile.html index bfd8cd7..6c6ecd2 100644 --- a/main/templates/pages/profile.html +++ b/main/templates/pages/profile.html @@ -45,7 +45,7 @@
- + Запросить права доступа
{% endblock %} diff --git a/main/templates/pages/work.html b/main/templates/pages/work.html index c6f2e01..2fcb4cf 100644 --- a/main/templates/pages/work.html +++ b/main/templates/pages/work.html @@ -2,39 +2,10 @@ {% load static %} - - {% block title %}{{ pagename }}{% endblock %} - {% block heading %}Управление правами{% endblock %} -{% block content %} -
-

Инженеры

-{%for engineer in engineers%} - {{ engineer.name }} -{% endfor %} - -

Агенты

-{%for agent in agents%} - {{ agent.name }} -{% endfor %} - -
- -{% if role == "admin" %} - Сдать смену -{% else %} - Запросить права инженера -{% endif %} - -{% endblock %} - -{% block title %}{{ pagename }}{% endblock %} - -{% block heading %}Управление{% endblock %} - {% block extra_css %} {% endblock %} @@ -52,24 +23,15 @@ - - - + - - - - - - - - - - - - - + {% for engineer in engineers %} + + + + + {% endfor %}
IDemailExpiration DateName(link to profile)Name
1big_boss123@example.ru19:30 18.02.21Иван Иванов
2gachi_cool456@example.ru21:00 18.02.21Пётр Петров
{{ engineer.id }}{{ engineer.name }}
@@ -81,22 +43,22 @@
инженеров:
- 13 + {{ engineers|length }}
легких агентов:
- 22 + {{ agents|length }}
- - + Получить права инженера + Сдать права инженера
diff --git a/main/views.py b/main/views.py index c1e7794..70facff 100644 --- a/main/views.py +++ b/main/views.py @@ -88,6 +88,7 @@ def profile_page(request): 'email': user_profile.user.email, 'name': user_profile.name, 'role': user_profile.role, + 'id': user_profile.id, 'image_url': user_profile.image, 'pagename': 'Страница профиля' } @@ -107,13 +108,10 @@ def auth_user(request): @login_required() def work_page(request, id): - if request.user.is_authenticated and request.user.id == id: - zenpy_user, _ = auth_user(request) - + if request.user.id == id: context = { - 'engineers': UserProfile.objects.filter(role=1), - 'agents': UserProfile.objects.filter(role=0), - 'role': zenpy_user.role, + 'engineers': UserProfile.objects.filter(role="admin"), + 'agents': UserProfile.objects.filter(role="agent"), 'pagename': 'Управление правами' } return render(request, 'pages/work.html', context) @@ -123,10 +121,10 @@ def work_page(request, id): @login_required() def work_hand_over(request): zenpy_user, admin = auth_user(request) - if zenpy_user.role == "admin": + if zenpy_user.role == "admin" or zenpy_user.role == "end-user": zenpy_user.role = "agent" admin.users.update(zenpy_user) - request.user.userprofile.role = 0 + request.user.userprofile.role = "agent" request.user.userprofile.save() return HttpResponseRedirect(reverse('work', args=(request.user.id,))) @@ -134,10 +132,10 @@ def work_hand_over(request): @login_required() def work_become_engineer(request): zenpy_user, admin = auth_user(request) - if zenpy_user.role == "agent": + if zenpy_user.role == "agent" or zenpy_user.role == "end-user": zenpy_user.role = "admin" admin.users.update(zenpy_user) - request.user.userprofile.role = 1 + request.user.userprofile.role = "admin" request.user.userprofile.save() return HttpResponseRedirect(reverse('work', args=(request.user.id,))) From 042e59a989d65c913a27bf3bd1347aec7730c68c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D1=82=D0=B5=D0=BF=D0=B0=D0=BD=D0=B5=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=20=D0=9E=D0=BB=D1=8C=D0=B3=D0=B0?= Date: Mon, 1 Mar 2021 20:50:25 +0300 Subject: [PATCH 06/54] Change conf --- docs/Makefile | 3 ++ docs/source/conf.py | 89 -------------------------------------------- docs/source/todo.rst | 2 +- 3 files changed, 4 insertions(+), 90 deletions(-) delete mode 100644 docs/source/conf.py diff --git a/docs/Makefile b/docs/Makefile index d0c3cbf..461302a 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -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 diff --git a/docs/source/conf.py b/docs/source/conf.py deleted file mode 100644 index 438d5a4..0000000 --- a/docs/source/conf.py +++ /dev/null @@ -1,89 +0,0 @@ -# Configuration file for the Sphinx documentation builder. -# -# This file only contains a selection of the most common options. For a full -# list see the documentation: -# https://www.sphinx-doc.org/en/master/usage/configuration.html - -# -- Path setup -------------------------------------------------------------- - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# -import os -import sys -sys.path.insert(0, os.path.abspath('../../')) - -import django -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'access_controller.settings') -django.setup() - -# -- Project information ----------------------------------------------------- - -project = 'ZenDesk Access Controller' -copyright = '2021, SHP S101, group 2' -author = 'SHP S101, group 2' - -# The full version, including alpha/beta/rc tags -release = 'v0.01' - - -# -- General configuration --------------------------------------------------- - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = [ - 'sphinx.ext.todo', - 'sphinx.ext.autodoc', - 'sphinx.ext.intersphinx', - 'sphinx.ext.viewcode', - 'sphinx_rtd_theme', -] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# -# This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. -language = 'ru' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# 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 -# a list of builtin themes. -# -html_theme = "sphinx_rtd_theme" - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - - -# -- Extension configuration ------------------------------------------------- - -# -- Options for intersphinx extension --------------------------------------- - -# Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = { - 'https://docs.python.org/3/': None, - 'django': ( - 'https://docs.djangoproject.com/en/dev/', - 'https://docs.djangoproject.com/en/dev/_objects/' - ), -} - -# -- Options for todo extension ---------------------------------------------- - -# If true, `todo` and `todoList` produce output, else they produce nothing. -todo_include_todos = True diff --git a/docs/source/todo.rst b/docs/source/todo.rst index a8d024b..ad6e4ed 100644 --- a/docs/source/todo.rst +++ b/docs/source/todo.rst @@ -1,5 +1,5 @@ Что необходимо доделать? -======================= +======================== From cd504275321e476a6fd0ccfdaa99f761604c61c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D1=82=D0=B5=D0=BF=D0=B0=D0=BD=D0=B5=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=20=D0=9E=D0=BB=D1=8C=D0=B3=D0=B0?= Date: Mon, 1 Mar 2021 20:52:39 +0300 Subject: [PATCH 07/54] Change conf --- docs/source/conf.py | 201 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 docs/source/conf.py diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..6c8988c --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,201 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +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 ----------------------------------------------------- + +project = 'ZenDesk Access Controller' +copyright = '2021, SHP S101, group 2' +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 + +# -- 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 + 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. +extensions = [ + 'sphinx.ext.todo', + 'sphinx.ext.autodoc', + 'sphinx.ext.intersphinx', + 'sphinx.ext.viewcode', + 'sphinx_rtd_theme', + 'sphinx.ext.graphviz', + 'sphinx.ext.napoleon', + 'sphinx.ext.inheritance_diagram', + +] + +if enchant is not None: + extensions.append('sphinxcontrib.spelling') + + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = 'ru' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# 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 +# a list of builtin themes. +# +html_theme = "sphinx_rtd_theme" + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + + +# -- Extension configuration ------------------------------------------------- + +# -- Options for intersphinx extension --------------------------------------- + +# Example configuration for intersphinx: refer to the Python standard library. +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), +} + +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 From a64e641a33e0d5eb623e15ad227098432ed8c2e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D1=82=D0=B5=D0=BF=D0=B0=D0=BD=D0=B5=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=20=D0=9E=D0=BB=D1=8C=D0=B3=D0=B0?= Date: Mon, 1 Mar 2021 21:18:27 +0300 Subject: [PATCH 08/54] Change conf using github snippet --- docs/source/conf.py | 1 + main/models.py | 1 + 2 files changed, 2 insertions(+) diff --git a/docs/source/conf.py b/docs/source/conf.py index 6c8988c..b70857e 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -85,6 +85,7 @@ def process_django_models(app, what, name, obj, options, lines): 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 diff --git a/main/models.py b/main/models.py index b7b30dd..2cf76ac 100644 --- a/main/models.py +++ b/main/models.py @@ -15,6 +15,7 @@ class UserProfile(models.Model): :type image: :class:`img` :param name: Имя пользователя на нашем сайте :type name: :class:`str` + """ user = models.OneToOneField(to=User, on_delete=models.CASCADE) From f9e6b79f0124755a90bb7b48f9f5e5104c260d7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D1=82=D0=B5=D0=BF=D0=B0=D0=BD=D0=B5=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=20=D0=9E=D0=BB=D1=8C=D0=B3=D0=B0?= Date: Tue, 2 Mar 2021 09:52:18 +0300 Subject: [PATCH 09/54] Change models documentation using snippets --- main/models.py | 34 +++++++++------------------------- 1 file changed, 9 insertions(+), 25 deletions(-) diff --git a/main/models.py b/main/models.py index 2cf76ac..8665570 100644 --- a/main/models.py +++ b/main/models.py @@ -8,20 +8,12 @@ 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` - """ - 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='Код роли пользователя') + image = models.URLField(null=True, blank=True, help_text='Аватарка') + name = models.CharField(default='None', max_length=100, help_text='Имя пользователя на нашем сайте') @receiver(post_save, sender=User) @@ -39,17 +31,9 @@ class RoleChangeLogs(models.Model): """ Модель для логирования изменений ролей пользователя - :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') + user = models.ForeignKey(to=User, on_delete=models.CASCADE, help_text='Пользователь, которому присвоили другую роль') + name = models.TextField(help_text='Имя пользователя') + new_role = models.TextField(help_text='Присвоенная роль') + change_time = models.DateTimeField(help_text='Дата и время изменения роли') + changed_by = models.ForeignKey(to=User, on_delete=models.CASCADE, related_name='changed_by', help_text='Кем была изменена роль') From 4a9b3d71a42678327d8a8861f3376d281f0d7f9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D1=82=D0=B5=D0=BF=D0=B0=D0=BD=D0=B5=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=20=D0=9E=D0=BB=D1=8C=D0=B3=D0=B0?= Date: Tue, 2 Mar 2021 12:30:56 +0300 Subject: [PATCH 10/54] Add typehints --- docs/source/conf.py | 6 ++++++ main/extra_func.py | 31 ++++++++++--------------------- 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index b70857e..fdcc4e4 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -134,6 +134,7 @@ extensions = [ 'sphinx.ext.graphviz', 'sphinx.ext.napoleon', 'sphinx.ext.inheritance_diagram', + 'sphinx_autodoc_typehints' ] @@ -200,3 +201,8 @@ spelling_ignore_pypi_package_names = True # 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 diff --git a/main/extra_func.py b/main/extra_func.py index 20c3712..2d780b8 100644 --- a/main/extra_func.py +++ b/main/extra_func.py @@ -20,65 +20,54 @@ class ZendeskAdmin: :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 + Функция **check_user** осуществляет проверку существования пользователя в Zendesk по email - :param email: Электронная почта пользователя - :type email: :class:`email` - :return: True, если существует, иначе False - :rtype: :class:`bool` """ return True if self.admin.search(email, type='user') else False def get_user_name(self, email: str) -> str: """ - Функция **get_user_name** возвращает имя пользователя + Функция **get_user_name** возвращает имя пользователя по его email - :param user_name: Имя пользователя - :type user_name: :class:`str` """ user = self.admin.users.search(email).values[0] return user.name def get_user_role(self, email: str) -> str: """ - Функция **get_user_role** возвращает роль пользователя + Функция **get_user_role** возвращает роль пользователя по его email - :param user_role: Роль пользователя - :type user_role: :class:`str` """ user = self.admin.users.search(email).values[0] return user.role def get_user_id(self, email: str) -> str: """ - Функция **get_user_id** возвращает id пользователя + Функция **get_user_id** возвращает id пользователя по его email - :param user_id: ID пользователя - :type user_id: :class:`str` """ user = self.admin.users.search(email).values[0] return user.id def get_user_image(self, email: str) -> str: """ - Функция **get_user_image** возвращает аватар пользователя + Функция **get_user_image** возвращает аватар пользователя по его email - :param user_image: Аватар пользователя - :type user_image: :class:`img` """ user = self.admin.users.search(email).values[0] return user.photo['content_url'] if user.photo else None From edbe0a66af24762b70678d175f30b90f20dc35d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D1=82=D0=B5=D0=BF=D0=B0=D0=BD=D0=B5=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=20=D0=9E=D0=BB=D1=8C=D0=B3=D0=B0?= Date: Tue, 2 Mar 2021 15:10:28 +0300 Subject: [PATCH 11/54] Change extra_func documentation --- main/extra_func.py | 65 +++++++++++++++++++++------------------------- 1 file changed, 30 insertions(+), 35 deletions(-) diff --git a/main/extra_func.py b/main/extra_func.py index 2d780b8..9e104be 100644 --- a/main/extra_func.py +++ b/main/extra_func.py @@ -13,9 +13,9 @@ 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 @@ -23,7 +23,7 @@ class ZendeskAdmin: """ - credentials:dict = { + credentials: dict = { 'subdomain': 'ngenix1612197338' } email: str = os.getenv('ACCESS_CONTROLLER_API_EMAIL') @@ -36,14 +36,12 @@ class ZendeskAdmin: def check_user(self, email: str) -> 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** возвращает имя пользователя по его email - """ user = self.admin.users.search(email).values[0] return user.name @@ -51,7 +49,6 @@ class ZendeskAdmin: def get_user_role(self, email: str) -> str: """ Функция **get_user_role** возвращает роль пользователя по его email - """ user = self.admin.users.search(email).values[0] return user.role @@ -59,7 +56,6 @@ class ZendeskAdmin: def get_user_id(self, email: str) -> str: """ Функция **get_user_id** возвращает id пользователя по его email - """ user = self.admin.users.search(email).values[0] return user.id @@ -67,23 +63,29 @@ class ZendeskAdmin: def get_user_image(self, email: str) -> str: """ Функция **get_user_image** возвращает аватар пользователя по его 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: + """ + Функция **get_user** возвращает пользователя (объект) по его email + """ return self.admin.users.search(email).values[0] def get_user_org(self, email: str) -> str: + """ + Функция **get_user_org** возвращает организацию, к которой относится пользователь по его email + """ user = self.admin.users.search(email).values[0] return user.organization.name - def create_admin(self) -> 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 """ @@ -105,34 +107,43 @@ 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) -> UserProfile: + """ + Функция **make_engineer** устанавливапет пользователю роль инженера. + """ update_role(user_profile, ROLES['engineer']) -def make_light_agent(user_profile): +def make_light_agent(user_profile: UserProfile) -> UserProfile: + """ + Функция **make_light_agent** устанавливапет пользователю роль легкого агента. + """ 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) -def update_profile(user_profile: UserProfile): +def update_profile(user_profile: UserProfile) -> UserProfile: """ - Функция обновляет профиль пользователя в соотвтетствии с текущим в Zendesk - - :param user_profile: Объект профиля пользователя - :type user_profile: :class:`main.models.UserProfile` + Функция обновляет профиль пользователя в соотвтетствии с текущим в Zendesk """ user = ZendeskAdmin().get_user(user_profile.user.email) user_profile.name = user.name @@ -143,12 +154,7 @@ 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) @@ -156,11 +162,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) @@ -169,13 +170,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, From 525a5a61d226356187505ad459c39c2fede3b67f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D1=82=D0=B5=D0=BF=D0=B0=D0=BD=D0=B5=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=20=D0=9E=D0=BB=D1=8C=D0=B3=D0=B0?= Date: Tue, 2 Mar 2021 16:52:20 +0300 Subject: [PATCH 12/54] Change extra_func documentation --- main/extra_func.py | 2 +- main/forms.py | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/main/extra_func.py b/main/extra_func.py index 9e104be..b07f242 100644 --- a/main/extra_func.py +++ b/main/extra_func.py @@ -78,7 +78,7 @@ class ZendeskAdmin: Функция **get_user_org** возвращает организацию, к которой относится пользователь по его email """ user = self.admin.users.search(email).values[0] - return user.organization.name + return user.organization def create_admin(self) -> Zenpy: """ diff --git a/main/forms.py b/main/forms.py index d80932c..176302e 100644 --- a/main/forms.py +++ b/main/forms.py @@ -6,11 +6,11 @@ from main.models import UserProfile class CustomRegistrationForm(RegistrationFormUniqueEmail): """ - Форма для регистрации :class:`django_registration.forms.RegistrationFormUniqueEmail` - с добавлением bootstrap-класса 'form-control' + Форма для регистрации :class:`django_registration.forms.RegistrationFormUniqueEmail` + с добавлением bootstrap-класса 'form-control' - :param password_zen: Поле для ввода пароля от Zendesk - :type password_zen: :class:`django.forms.CharField` + :param password_zen: Поле для ввода пароля от Zendesk + :type password_zen: :class:`django.forms.CharField` """ def __init__(self, *args, **kwargs): @@ -29,6 +29,13 @@ class CustomRegistrationForm(RegistrationFormUniqueEmail): class AdminPageUsers(forms.Form): + """ + Форма для установки статуса + + :param password_zen: Поле для ввода пароля от Zendesk + :type password_zen: :class:`django.forms.CharField` + """ + users = forms.ModelMultipleChoiceField( queryset=UserProfile.objects.filter(role='agent'), widget=forms.CheckboxSelectMultiple( From f50ce49d7766cd08432c7f97a6c37aec3f63242d Mon Sep 17 00:00:00 2001 From: Sokurov Idar Date: Tue, 2 Mar 2021 21:44:13 +0300 Subject: [PATCH 13/54] update login with email --- access_controller/auth.py | 19 +++++++++++++++++++ access_controller/settings.py | 6 ++++++ access_controller/urls.py | 5 +++-- main/extra_func.py | 15 +++++++++------ main/forms.py | 23 +++++++++++++++++++---- main/views.py | 18 ++++++++++++------ 6 files changed, 68 insertions(+), 18 deletions(-) create mode 100644 access_controller/auth.py diff --git a/access_controller/auth.py b/access_controller/auth.py new file mode 100644 index 0000000..be707e1 --- /dev/null +++ b/access_controller/auth.py @@ -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 diff --git a/access_controller/settings.py b/access_controller/settings.py index 480cfbd..8b4ec96 100644 --- a/access_controller/settings.py +++ b/access_controller/settings.py @@ -135,6 +135,12 @@ 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 = { diff --git a/access_controller/urls.py b/access_controller/urls.py index 45781bf..b91e747 100644 --- a/access_controller/urls.py +++ b/access_controller/urls.py @@ -14,17 +14,18 @@ Including another URLconf 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin +from django.contrib.auth.forms import AuthenticationForm 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 +from main.views import main_page, profile_page, CustomRegistrationView, CustomLoginView 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(extra_context={}), name='login',), # TODO add extra context path('accounts/', include('django.contrib.auth.urls')), path('accounts/', include('django_registration.backends.activation.urls')), path('accounts/login/', include('django.contrib.auth.urls')), diff --git a/main/extra_func.py b/main/extra_func.py index 9e0a564..3ce296a 100644 --- a/main/extra_func.py +++ b/main/extra_func.py @@ -26,6 +26,12 @@ class ZendeskAdmin: email = os.getenv('ACCESS_CONTROLLER_API_EMAIL') token = os.getenv('ACCESS_CONTROLLER_API_TOKEN') password = os.getenv('ACCESS_CONTROLLER_API_PASSWORD') + _instance=None + + def __new__(cls, *args, **kwargs): + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance def __init__(self): self.create_admin() @@ -73,10 +79,7 @@ class ZendeskAdmin: def get_user_image(self, email: str) -> str: """ - Функция **get_user_image** возвращает аватар пользователя - - :param user_image: Аватар пользователя - :type user_image: :class:`img` + Функция **get_user_image** возвращает url-ссылку на аватар пользователя """ user = self.admin.users.search(email).values[0] return user.photo['content_url'] if user.photo else None @@ -86,7 +89,7 @@ class ZendeskAdmin: def get_user_org(self, email: str) -> str: user = self.admin.users.search(email).values[0] - return user.organization.name + return user.organization.name def create_admin(self) -> None: """ @@ -140,7 +143,7 @@ def check_user_exist(email: str) -> bool: return ZendeskAdmin().check_user(email) -def get_user_organization(email: str) -> bool: +def get_user_organization(email: str) -> str: """ Функция возвращает организацию пользователя diff --git a/main/forms.py b/main/forms.py index 6a058bb..e33e1ae 100644 --- a/main/forms.py +++ b/main/forms.py @@ -1,4 +1,5 @@ from django import forms +from django.contrib.auth.forms import AuthenticationForm from django_registration.forms import RegistrationFormUniqueEmail @@ -6,11 +7,7 @@ class CustomRegistrationForm(RegistrationFormUniqueEmail): """ Форма для регистрации :class:`django_registration.forms.RegistrationFormUniqueEmail` с добавлением bootstrap-класса 'form-control' - - :param password_zen: Поле для ввода пароля от Zendesk - :type password_zen: :class:`django.forms.CharField` """ - def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) for visible in self.visible_fields(): @@ -24,3 +21,21 @@ class CustomRegistrationForm(RegistrationFormUniqueEmail): class Meta(RegistrationFormUniqueEmail.Meta): fields = RegistrationFormUniqueEmail.Meta.fields + + +class CustomAuthenticationForm(AuthenticationForm): + """ + Форма для авторизации :class:`django.contrib.auth.forms.AuthenticationForm` + с изменением поля username на email + """ + username = forms.CharField( + label="Электронная почта", + widget=forms.EmailInput(), + ) + error_messages = { + 'invalid_login': + "Пожалуйста, введите правильные электронную почту и пароль. Оба поля " + "могут быть чувствительны к регистру." + , + 'inactive': "Аккаунт не активен.", + } diff --git a/main/views.py b/main/views.py index a73017f..8fb892a 100644 --- a/main/views.py +++ b/main/views.py @@ -1,18 +1,17 @@ +import logging + 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.contrib.auth.views import LoginView from django.shortcuts import render from django.urls import reverse_lazy -from django_registration.backends.one_step.views import RegistrationView +from django_registration.views import RegistrationView from access_controller.settings import EMAIL_HOST_USER from main.extra_func import check_user_exist, update_profile, get_user_organization -from main.forms import CustomRegistrationForm -from django_registration.views import RegistrationView -from django.contrib.auth.decorators import login_required -import logging - +from main.forms import CustomRegistrationForm, CustomAuthenticationForm class CustomRegistrationView(RegistrationView): @@ -89,3 +88,10 @@ def main_page(request): logger = logging.getLogger('main.index') logger.info('Index page opened') return render(request, 'pages/index.html') + + +class CustomLoginView(LoginView): + """ + Отображение страницы авторизации пользователя + """ + form_class = CustomAuthenticationForm From 6cdd787cc8fcf4f8240fc3efdeeeb631d678ac32 Mon Sep 17 00:00:00 2001 From: Artyom Kravchenko Date: Wed, 3 Mar 2021 13:46:44 +0300 Subject: [PATCH 14/54] add typehints to requirements --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b2e4c64..4161b6c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,4 +8,4 @@ django_registration==3.1.1 # Documentation Sphinx==3.4.3 sphinx-rtd-theme==0.5.1 - +sphinx-autodoc-typehints==1.11.1 From 0cda9da3116820266e9f3113875cf0c87fb2ee24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D1=82=D0=B5=D0=BF=D0=B0=D0=BD=D0=B5=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=20=D0=9E=D0=BB=D1=8C=D0=B3=D0=B0?= Date: Wed, 3 Mar 2021 20:59:53 +0300 Subject: [PATCH 15/54] Add views and forms documentation --- main/forms.py | 15 ++++++++------- main/views.py | 35 +++++++++++++++++++++++++---------- requirements.txt | 1 + 3 files changed, 34 insertions(+), 17 deletions(-) diff --git a/main/forms.py b/main/forms.py index 176302e..63a3556 100644 --- a/main/forms.py +++ b/main/forms.py @@ -7,13 +7,14 @@ from main.models import UserProfile class CustomRegistrationForm(RegistrationFormUniqueEmail): """ Форма для регистрации :class:`django_registration.forms.RegistrationFormUniqueEmail` - с добавлением bootstrap-класса 'form-control' - :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): @@ -30,10 +31,10 @@ class CustomRegistrationForm(RegistrationFormUniqueEmail): class AdminPageUsers(forms.Form): """ - Форма для установки статуса + Форма для установки статусов engineer или light_agent пользователям - :param password_zen: Поле для ввода пароля от Zendesk - :type password_zen: :class:`django.forms.CharField` + :param users: Поле для установки статуса + :type users: :class:`ModelMultipleChoiceField` """ users = forms.ModelMultipleChoiceField( diff --git a/main/views.py b/main/views.py index 72d4e52..c7c1849 100644 --- a/main/views.py +++ b/main/views.py @@ -27,13 +27,17 @@ from access_controller.settings import ZENDESK_ROLES 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) @@ -61,7 +65,7 @@ class CustomRegistrationView(RegistrationView): else: self.is_allowed = False - def get_success_url(self, user=None): + def get_success_url(self, user: User = None) -> success_url: """ Возвращает url-адрес страницы, куда нужно перейти после успешной/неуспешной регистрации Используется самой django-registration @@ -73,13 +77,9 @@ class CustomRegistrationView(RegistrationView): @login_required() -def profile_page(request): +def profile_page(request: UserProfile) -> UserProfile: """ Отображение страницы профиля - - :param request: объект с деталями запроса - :type request: :class:`django.http.HttpResponse` - :return: объект ответа сервера с HTML-кодом внутри """ user_profile = request.user.userprofile update_profile(user_profile) @@ -95,17 +95,26 @@ def profile_page(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 - логика работы страницы администратора + """ 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: @@ -121,7 +130,10 @@ 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: User) -> int: #.. todolist:: :this func counts users from all zendesk instead of just from a model: + """ + Функция подсчета количества сотрудников с ролями engineer и light_a + """ engineers, light_agents = 0, 0 for user in users: if user.custom_role_id == ZENDESK_ROLES['engineer']: @@ -130,7 +142,10 @@ class AdminPageView(FormView, LoginRequiredMixin): light_agents += 1 return engineers, light_agents - def get_context_data(self, **kwargs): + def get_context_data(self, **kwargs) -> list: + """ + Функция формирования контента страницы администратора (с проверкой прав доступа) + """ if self.request.user.userprofile.role != 'admin': raise PermissionDenied context = super().get_context_data(**kwargs) diff --git a/requirements.txt b/requirements.txt index b2e4c64..826cc73 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,4 +8,5 @@ django_registration==3.1.1 # Documentation Sphinx==3.4.3 sphinx-rtd-theme==0.5.1 +sphinx-autodoc-typehints==1.11.1 From cd47331ae7496b7a3b02cc1d1ba18c4bf05848ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D1=82=D0=B5=D0=BF=D0=B0=D0=BD=D0=B5=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=20=D0=9E=D0=BB=D1=8C=D0=B3=D0=B0?= Date: Wed, 3 Mar 2021 21:05:31 +0300 Subject: [PATCH 16/54] After mergin develop --- main/forms.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main/forms.py b/main/forms.py index a9624dd..32ed00b 100644 --- a/main/forms.py +++ b/main/forms.py @@ -51,8 +51,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="Электронная почта", From fd1e048ebe67bbab7ba42cc599bb175bf5995e49 Mon Sep 17 00:00:00 2001 From: Sokurov Idar Date: Thu, 4 Mar 2021 01:25:56 +0300 Subject: [PATCH 17/54] statistic backend first version --- access_controller/urls.py | 10 ++-- main/forms.py | 28 ++++++++- main/migrations/0005_auto_20210304_0119.py | 67 ++++++++++++++++++++++ main/models.py | 1 + main/templates/pages/stat.html | 26 +++++++++ main/views.py | 56 +++++++++++++++++- 6 files changed, 178 insertions(+), 10 deletions(-) create mode 100644 main/migrations/0005_auto_20210304_0119.py create mode 100644 main/templates/pages/stat.html diff --git a/access_controller/urls.py b/access_controller/urls.py index 45d815b..6554616 100644 --- a/access_controller/urls.py +++ b/access_controller/urls.py @@ -14,15 +14,12 @@ Including another URLconf 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin -from django.contrib.auth.forms import AuthenticationForm -from django.contrib.auth.views import LoginView from django.contrib.auth import views as auth_views from django.urls import path, include -from access_controller import settings -from access_controller.settings import DEBUG -from main.views import main_page, profile_page, CustomRegistrationView, 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, \ + AdminPageView, statistic_page urlpatterns = [ path('admin/', admin.site.urls, name='admin'), @@ -37,7 +34,8 @@ urlpatterns = [ 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 += [ diff --git a/main/forms.py b/main/forms.py index e69de1d..ff6a9d3 100644 --- a/main/forms.py +++ b/main/forms.py @@ -10,6 +10,7 @@ class CustomRegistrationForm(RegistrationFormUniqueEmail): Форма для регистрации :class:`django_registration.forms.RegistrationFormUniqueEmail` с добавлением bootstrap-класса 'form-control' """ + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) for visible in self.visible_fields(): @@ -18,7 +19,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): @@ -57,3 +58,28 @@ class CustomAuthenticationForm(AuthenticationForm): , 'inactive': "Аккаунт не активен.", } + + +class StatisticForm(forms.Form): + email = forms.EmailField( + label='Электроная почта', + ) + inter = forms.CharField( # TODO: Переделать под html страницу + label='Выбор интервала', + ) + dio_start = forms.DateField( # TODO: Переделать под html страницу + label='Начало диапазона', + widget=forms.DateInput( + attrs={ + 'type': 'date', + } + ), + ) + dio_end = forms.DateField( # TODO: Переделать под html страницу + label='Конец диапазона', + widget=forms.DateInput( + attrs={ + 'type': 'date', + } + ), + ) diff --git a/main/migrations/0005_auto_20210304_0119.py b/main/migrations/0005_auto_20210304_0119.py new file mode 100644 index 0000000..361ec25 --- /dev/null +++ b/main/migrations/0005_auto_20210304_0119.py @@ -0,0 +1,67 @@ +# Generated by Django 3.1.6 on 2021-03-03 22:19 + +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', '0004_rolechangelogs'), + ] + + operations = [ + migrations.AddField( + model_name='rolechangelogs', + name='old_role', + field=models.TextField(default='agent', help_text='Старая роль'), + preserve_default=False, + ), + 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), + ), + ] diff --git a/main/models.py b/main/models.py index 8665570..13bd55e 100644 --- a/main/models.py +++ b/main/models.py @@ -34,6 +34,7 @@ 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='Старая роль') new_role = models.TextField(help_text='Присвоенная роль') change_time = models.DateTimeField(help_text='Дата и время изменения роли') changed_by = models.ForeignKey(to=User, on_delete=models.CASCADE, related_name='changed_by', help_text='Кем была изменена роль') diff --git a/main/templates/pages/stat.html b/main/templates/pages/stat.html new file mode 100644 index 0000000..1ac47d7 --- /dev/null +++ b/main/templates/pages/stat.html @@ -0,0 +1,26 @@ +{% extends 'base/base.html' %} + +{% load static %} + +{% block title %}{{ pagename }}{% endblock %} + +{% block heading %}Статистика{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} +
+
+ {% csrf_token %} + {% for field in form %} + {{field.label}} + {{field}} +
+ {% endfor %} + +
+
+{% endblock %} + diff --git a/main/views.py b/main/views.py index b765bd0..532aac0 100644 --- a/main/views.py +++ b/main/views.py @@ -9,7 +9,7 @@ from django.contrib.auth.tokens import default_token_generator 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.shortcuts import get_list_or_404, redirect, reverse, render, get_object_or_404 from django.urls import reverse_lazy from django.views.generic import FormView from django_registration.views import RegistrationView @@ -19,8 +19,8 @@ from zenpy.lib.api_objects import User as ZenpyUser from access_controller.settings import EMAIL_HOST_USER, ZENDESK_ROLES from main.extra_func import check_user_exist, update_profile, get_user_organization, make_engineer, make_light_agent, \ get_users_list -from main.forms import AdminPageUsers, CustomRegistrationForm, CustomAuthenticationForm -from .models import UserProfile +from main.forms import AdminPageUsers, CustomRegistrationForm, CustomAuthenticationForm, StatisticForm +from .models import UserProfile, RoleChangeLogs class CustomRegistrationView(RegistrationView): @@ -190,3 +190,53 @@ class CustomLoginView(LoginView): Отображение страницы авторизации пользователя """ form_class = CustomAuthenticationForm + + +@login_required() +def statistic_page(request): + if not request.user.is_superuser: + return redirect('index') + context = { + 'pagename': 'страница статистики', + } + if request.method == "POST": + form = StatisticForm(request.POST) + if form.is_valid(): + start_date = form.cleaned_data['dio_start'] + end_date = form.cleaned_data['dio_end'] + try: + data = RoleChangeLogs.objects.filter( + change_time__range=[start_date, end_date], + user=User.objects.get(email=form.cleaned_data['email']), + ) + except User.DoesNotExist: + data = [] + context['errors'] = 'Пользователь не найден' + if not data: + context['errors'] = 'Не найдено изменений роли за этот промежуток времени' + else: + sep = form.cleaned_data['inter'] + stat = {} + if sep == 'days': + for day in daterange(start_date, end_date+timedelta(days=1)): + stat[day] = 0 + if data[0].old_role == 'engineer': + stat[start_date] += data[0].change_time.time().hour + if data[-1].old_role == 'engineer': + stat[end_date] += 24-data[-1].change_time.time().hour + else: + context['errors'] = form.errors + + if request.method == 'GET': + form = StatisticForm() + + context['form'] = form + return render(request, 'pages/stat.html', context) + + +from datetime import timedelta, date + + +def daterange(start_date, end_date): + for n in range(int((end_date - start_date).days)): + yield start_date + timedelta(n) From d46b90010de3cd8d62bb5b6ad896b9f2a301ef9b Mon Sep 17 00:00:00 2001 From: Yuriy Kulakov Date: Thu, 4 Mar 2021 18:22:49 +0300 Subject: [PATCH 18/54] Added permissions system to control page --- main/migrations/0005_auto_20210302_2255.py | 17 +++++++++++++ main/migrations/0006_delete_userprofile.py | 16 ++++++++++++ main/migrations/0007_userprofile.py | 29 ++++++++++++++++++++++ main/migrations/0008_auto_20210303_2305.py | 17 +++++++++++++ main/templates/base/menu.html | 3 +++ main/templates/pages/adm_ruleset.html | 23 ++++++++--------- main/views.py | 26 ++++++++++++++----- static/main/js/control.js | 9 +++++++ 8 files changed, 122 insertions(+), 18 deletions(-) create mode 100644 main/migrations/0005_auto_20210302_2255.py create mode 100644 main/migrations/0006_delete_userprofile.py create mode 100644 main/migrations/0007_userprofile.py create mode 100644 main/migrations/0008_auto_20210303_2305.py create mode 100644 static/main/js/control.js 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/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/views.py b/main/views.py index 72d4e52..517ea84 100644 --- a/main/views.py +++ b/main/views.py @@ -1,22 +1,20 @@ -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.contrib.contenttypes.models import ContentType 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 django.contrib.auth.models import User, Permission 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.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin from django.core.exceptions import PermissionDenied import logging @@ -55,12 +53,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 + @staticmethod + def set_permission(user) -> None: + """ + Дает разрешение на просмотр страница администратора, если пользователь имеет роль admin + """ + + content_type = ContentType.objects.get_for_model(UserProfile) + permission, created = Permission.objects.get_or_create( + codename='has_control_access', + content_type=content_type, + ) + if user.userprofile.role == 'admin': + user.user_permissions.add(permission) + def get_success_url(self, user=None): """ Возвращает url-адрес страницы, куда нужно перейти после успешной/неуспешной регистрации @@ -100,7 +113,8 @@ def main_page(request): return render(request, 'pages/index.html') -class AdminPageView(FormView, LoginRequiredMixin): +class AdminPageView(FormView, LoginRequiredMixin, PermissionRequiredMixin): + permission_required = 'main.has_control_access' template_name = 'pages/adm_ruleset.html' form_class = AdminPageUsers success_url = '/control/' 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); + } +} From 525bb256974383a40981d20813d63551ca2649a1 Mon Sep 17 00:00:00 2001 From: Yuriy Kulakov Date: Thu, 4 Mar 2021 19:06:45 +0300 Subject: [PATCH 19/54] Merge branch 'develop' into feature/adm_ruleset/backend --- main/views.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/main/views.py b/main/views.py index 979aa6b..5c5ed12 100644 --- a/main/views.py +++ b/main/views.py @@ -1,15 +1,15 @@ -import os - -from django.contrib.auth.forms import PasswordResetForm import logging import os -from django.contrib.auth.decorators import login_required from django.contrib.auth.tokens import default_token_generator +from django.contrib.auth.forms import PasswordResetForm +from django.contrib.auth.views import LoginView from django.contrib.contenttypes.models import ContentType -from django.shortcuts import render, get_list_or_404 -from django.urls import reverse_lazy +from django.http import HttpResponseRedirect +from django.shortcuts import render, get_list_or_404, redirect +from django.urls import reverse_lazy, reverse from django.views.generic import FormView +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, \ @@ -17,13 +17,14 @@ from main.extra_func import check_user_exist, update_profile, get_user_organizat from django.contrib.auth.models import User, Permission from main.models import UserProfile -from main.forms import CustomRegistrationForm, AdminPageUsers +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 ZENDESK_ROLES +from access_controller.settings import ZENDESK_ROLES +from zenpy.lib.api_objects import User as ZenpyUser class CustomRegistrationView(RegistrationView): From 656081295ee723b9e734e187129f0337e4bbefcc Mon Sep 17 00:00:00 2001 From: Andrew Smirnov Date: Thu, 4 Mar 2021 20:01:10 +0300 Subject: [PATCH 20/54] Profile view page refactoring --- main/templates/pages/profile.html | 27 ++++++++++++--------------- main/views.py | 14 +++++--------- 2 files changed, 17 insertions(+), 24 deletions(-) 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 5c5ed12..b5d953a 100644 --- a/main/views.py +++ b/main/views.py @@ -5,7 +5,8 @@ from django.contrib.auth.tokens import default_token_generator from django.contrib.auth.forms import PasswordResetForm from django.contrib.auth.views import LoginView from django.contrib.contenttypes.models import ContentType -from django.http import HttpResponseRedirect +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 @@ -95,19 +96,14 @@ class CustomRegistrationView(RegistrationView): @login_required() -def profile_page(request: UserProfile) -> UserProfile: +def profile_page(request: WSGIRequest) -> HttpResponse: """ Отображение страницы профиля """ - 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) From 9ddf1db2a3c4d6e658bc88e84adc63b75eb8c257 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D1=83=D0=BB=D0=B0=D0=BA=D0=BE=D0=B2=20=D0=AE=D1=80?= =?UTF-8?q?=D0=B8=D0=B9?= Date: Thu, 4 Mar 2021 20:45:55 +0300 Subject: [PATCH 21/54] Fixed bug with permissions --- access_controller/urls.py | 6 +----- main/views.py | 24 +++++++++++++++--------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/access_controller/urls.py b/access_controller/urls.py index 45d815b..3595e4f 100644 --- a/access_controller/urls.py +++ b/access_controller/urls.py @@ -14,13 +14,9 @@ Including another URLconf 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin -from django.contrib.auth.forms import AuthenticationForm -from django.contrib.auth.views import LoginView from django.contrib.auth import views as auth_views from django.urls import path, include -from access_controller import settings -from access_controller.settings import DEBUG -from main.views import main_page, profile_page, CustomRegistrationView, work_page, work_hand_over, work_become_engineer, AdminPageView +from main.views import work_page, work_hand_over, work_become_engineer, AdminPageView from main.views import main_page, profile_page, CustomRegistrationView, CustomLoginView diff --git a/main/views.py b/main/views.py index 5c5ed12..7486d69 100644 --- a/main/views.py +++ b/main/views.py @@ -27,6 +27,13 @@ from access_controller.settings import ZENDESK_ROLES from zenpy.lib.api_objects import User as ZenpyUser +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): """ Отображение и логика работы страницы регистрации пользователя @@ -74,13 +81,12 @@ class CustomRegistrationView(RegistrationView): """ Дает разрешение на просмотр страница администратора, если пользователь имеет роль admin """ - - content_type = ContentType.objects.get_for_model(UserProfile) - permission, created = Permission.objects.get_or_create( - codename='has_control_access', - content_type=content_type, - ) 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: @@ -167,7 +173,7 @@ def main_page(request): return render(request, 'pages/index.html') -class AdminPageView(FormView, LoginRequiredMixin, PermissionRequiredMixin): +class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin, FormView): permission_required = 'main.has_control_access' template_name = 'pages/adm_ruleset.html' form_class = AdminPageUsers @@ -192,7 +198,7 @@ class AdminPageView(FormView, LoginRequiredMixin, PermissionRequiredMixin): [make_light_agent(user) for user in users] @staticmethod - def count_users(users: User) -> int: #.. todolist:: :this func counts users from all zendesk instead of just from a model: + def count_users(users) -> tuple: #.. todolist:: :this func counts users from all zendesk instead of just from a model: """ Функция подсчета количества сотрудников с ролями engineer и light_a """ @@ -204,7 +210,7 @@ class AdminPageView(FormView, LoginRequiredMixin, PermissionRequiredMixin): light_agents += 1 return engineers, light_agents - def get_context_data(self, **kwargs) -> list: + def get_context_data(self, **kwargs) -> dict: """ Функция формирования контента страницы администратора (с проверкой прав доступа) """ From 1d7ff524b0bd65dcf82ff0ae18da011f978cc323 Mon Sep 17 00:00:00 2001 From: Andrew Smirnov Date: Thu, 4 Mar 2021 20:54:45 +0300 Subject: [PATCH 22/54] Documentation fix Add enchant, remove intersphinx useless links, remove napoleon, fix todos --- docs/source/conf.py | 30 +++++++++++++----------------- main/extra_func.py | 5 ++++- main/models.py | 10 ++-------- main/views.py | 5 ++++- requirements.txt | 4 ++-- 5 files changed, 25 insertions(+), 29 deletions(-) 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 6227bf1..691bd37 100644 --- a/main/extra_func.py +++ b/main/extra_func.py @@ -74,7 +74,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/models.py b/main/models.py index 8665570..f8f8385 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='Имя пользователя') new_role = models.TextField(help_text='Присвоенная роль') diff --git a/main/views.py b/main/views.py index b5d953a..0e6aa41 100644 --- a/main/views.py +++ b/main/views.py @@ -188,9 +188,12 @@ class AdminPageView(FormView, LoginRequiredMixin, PermissionRequiredMixin): [make_light_agent(user) for user in users] @staticmethod - def count_users(users: User) -> int: #.. todolist:: :this func counts users from all zendesk instead of just from a model: + def count_users(users: User) -> int: """ Функция подсчета количества сотрудников с ролями 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: diff --git a/requirements.txt b/requirements.txt index 826cc73..7a4f941 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,9 +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 From 5909750fcc0e71f88ed388b8ecab9a4016566e41 Mon Sep 17 00:00:00 2001 From: Andrew Smirnov Date: Thu, 4 Mar 2021 20:56:38 +0300 Subject: [PATCH 23/54] Remove todolist commant --- main/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/views.py b/main/views.py index 5343906..ca8c9c2 100644 --- a/main/views.py +++ b/main/views.py @@ -194,7 +194,7 @@ class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin, FormView): [make_light_agent(user) for user in users] @staticmethod - def count_users(users) -> tuple: #.. todolist:: :this func counts users from all zendesk instead of just from a model: + def count_users(users) -> tuple: """ Функция подсчета количества сотрудников с ролями engineer и light_a From 8566e2b5b644fe012cd21d2207050f57e3ae9680 Mon Sep 17 00:00:00 2001 From: Sokurov Idar Date: Thu, 4 Mar 2021 23:13:10 +0300 Subject: [PATCH 24/54] Updated version with days iteration --- main/extra_func.py | 15 +++++++ main/templates/pages/stat.html | 20 +++++---- main/views.py | 79 ++++++++++++++++++++++++---------- 3 files changed, 84 insertions(+), 30 deletions(-) diff --git a/main/extra_func.py b/main/extra_func.py index 6227bf1..8929d3e 100644 --- a/main/extra_func.py +++ b/main/extra_func.py @@ -1,4 +1,5 @@ import os +from datetime import timedelta, datetime from zenpy import Zenpy from zenpy.lib.exception import APIException @@ -188,3 +189,17 @@ def check_user_auth(email: str, password: str) -> bool: except APIException: return False return True + + +def daterange(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 = log.change_time.time() + time = timedelta(hours=time.hour, minutes=time.minute, seconds=time.second) + print(time) + return time diff --git a/main/templates/pages/stat.html b/main/templates/pages/stat.html index 1ac47d7..ea5e793 100644 --- a/main/templates/pages/stat.html +++ b/main/templates/pages/stat.html @@ -12,15 +12,19 @@ {% block content %}
-
- {% csrf_token %} - {% for field in form %} - {{field.label}} - {{field}} -
+ + {% csrf_token %} + {% for field in form %} + {{field.label}} + {{field}} +
+ {% endfor %} + +
+ {% for key,val in stats_logs.items %} +

{{key}} {{val}}

+
{% endfor %} - -
{% endblock %} diff --git a/main/views.py b/main/views.py index 532aac0..a489b01 100644 --- a/main/views.py +++ b/main/views.py @@ -9,7 +9,7 @@ from django.contrib.auth.tokens import default_token_generator 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, get_object_or_404 +from django.shortcuts import get_list_or_404, redirect, reverse, render from django.urls import reverse_lazy from django.views.generic import FormView from django_registration.views import RegistrationView @@ -18,7 +18,7 @@ from zenpy.lib.api_objects import User as ZenpyUser from access_controller.settings import EMAIL_HOST_USER, ZENDESK_ROLES from main.extra_func import check_user_exist, update_profile, get_user_organization, make_engineer, make_light_agent, \ - get_users_list + get_users_list, daterange, get_timedelta from main.forms import AdminPageUsers, CustomRegistrationForm, CustomAuthenticationForm, StatisticForm from .models import UserProfile, RoleChangeLogs @@ -196,34 +196,74 @@ class CustomLoginView(LoginView): def statistic_page(request): if not request.user.is_superuser: return redirect('index') + context = { 'pagename': 'страница статистики', } + if request.method == "POST": form = StatisticForm(request.POST) + if form.is_valid(): start_date = form.cleaned_data['dio_start'] end_date = form.cleaned_data['dio_end'] + try: data = RoleChangeLogs.objects.filter( change_time__range=[start_date, end_date], user=User.objects.get(email=form.cleaned_data['email']), - ) + ).order_by('change_time') except User.DoesNotExist: - data = [] context['errors'] = 'Пользователь не найден' - if not data: - context['errors'] = 'Не найдено изменений роли за этот промежуток времени' - else: - sep = form.cleaned_data['inter'] - stat = {} - if sep == 'days': - for day in daterange(start_date, end_date+timedelta(days=1)): - stat[day] = 0 - if data[0].old_role == 'engineer': - stat[start_date] += data[0].change_time.time().hour - if data[-1].old_role == 'engineer': - stat[end_date] += 24-data[-1].change_time.time().hour + context['form'] = StatisticForm() + return render(request, 'pages/stat.html', context) + sep = form.cleaned_data['inter'] # Разрез + stat = {} + + # Этот кусок кода будет заполнять массив stat, в котором ключ - дата, значение кол-во часов + # Далее будет заполнение контекса в зависимости от выбранного интервала и отображения + # (пока есть только отображение в часах, разрез в днях) + + # Обнуление всех дней + for day in daterange(start_date, end_date + timedelta(days=1)): + stat[day] = 0 + + # Проеврка крайних случаев + # Если окажется, что инженер работал ещё до начала диапазона + if data[0].old_role == 'engineer': + for day in daterange(start_date, data[0].change_time.date()): + stat[day] = 24 + stat[data[0].change_time.date()] += data[0].change_time.time().hour + # Если окажется, что инженер закончил работать после диапазона + if data[len(data) - 1].new_role == 'engineer': + for day in daterange(data[len(data) - 1].change_time.date() + timedelta(days=1), + end_date + timedelta(days=1)): + stat[day] = 24 + stat[data[len(data) - 1].change_time.date()] += int( + (timedelta(days=1) - get_timedelta(data[len(data) - 1])).total_seconds() // 3600) + + # Цикл по логам + for log_index in range(len(data) - 1): + if data[log_index].new_role == 'engineer': + log1, log2 = data[log_index], data[log_index + 1] + + # Если сессия закончилась НЕ в тот же день, что и началась + if log1.change_time.date() != log2.change_time.date(): + stat[log1.change_time.date()] += int( + (timedelta(days=1) - get_timedelta(log1)).total_seconds() // 3600) + stat[log2.change_time.date()] += log2.change_time.time().hour + + # Если проработал несколько дней подряд, то заполнить эти дни по 24 часа + for day in daterange(log1.change_time.date() + timedelta(days=1), log2.change_time.date()): + stat[day] = 24 + + # Если сессия закончилась в тот же день, что и началась + else: + times = log2.change_time - log1.change_time + stat[log1.change_time.date()] += times.seconds // 3600 + + if sep == 'days': + context['stats_logs'] = stat else: context['errors'] = form.errors @@ -234,9 +274,4 @@ def statistic_page(request): return render(request, 'pages/stat.html', context) -from datetime import timedelta, date - - -def daterange(start_date, end_date): - for n in range(int((end_date - start_date).days)): - yield start_date + timedelta(n) +from datetime import timedelta From fe944f487587eb26d6344f9c8b4ae2702a007cda Mon Sep 17 00:00:00 2001 From: Artyom Kravchenko Date: Sat, 6 Mar 2021 01:36:19 +0300 Subject: [PATCH 25/54] Fixed info bug on page --- main/templates/pages/work.html | 4 ++-- main/views.py | 14 ++++++++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/main/templates/pages/work.html b/main/templates/pages/work.html index 2fcb4cf..a588ef5 100644 --- a/main/templates/pages/work.html +++ b/main/templates/pages/work.html @@ -22,13 +22,13 @@
Список сотрудников с правами инженера
- + {% for engineer in engineers %} - + {% endfor %} diff --git a/main/views.py b/main/views.py index ca8c9c2..0008255 100644 --- a/main/views.py +++ b/main/views.py @@ -128,10 +128,20 @@ def auth_user(request): @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': UserProfile.objects.filter(role="admin"), - 'agents': UserProfile.objects.filter(role="agent"), + 'engineers': engineers, + 'agents': light_agents, 'pagename': 'Управление правами' } return render(request, 'pages/work.html', context) From 8488ea88c29d980a2c706f27aa394ac6b1294b3c Mon Sep 17 00:00:00 2001 From: Yuriy Kulakov Date: Sun, 7 Mar 2021 21:30:02 +0300 Subject: [PATCH 26/54] Added Django REST but have some bugs --- access_controller/settings.py | 9 ++++ access_controller/urls.py | 12 +++-- main/extra_func.py | 45 ++++++++++++++++--- main/serializers.py | 17 ++++++++ main/templates/base/base.html | 1 + main/templates/pages/adm_ruleset.html | 18 +++++--- main/urls.py | 6 +++ main/views.py | 43 ++++++++++-------- requirements.txt | 2 + static/main/js/control.js | 63 ++++++++++++++++++++++++--- 10 files changed, 176 insertions(+), 40 deletions(-) create mode 100644 main/serializers.py create mode 100644 main/urls.py diff --git a/access_controller/settings.py b/access_controller/settings.py index 96703b6..a26931d 100644 --- a/access_controller/settings.py +++ b/access_controller/settings.py @@ -36,6 +36,7 @@ INSTALLED_APPS = [ 'django.contrib.messages', 'django.contrib.staticfiles', 'django_registration', + 'rest_framework', 'main', ] @@ -183,3 +184,11 @@ ZENDESK_ROLES = { 'engineer': 360005209000, 'light_agent': 360005208980, } + +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' + ] +} diff --git a/access_controller/urls.py b/access_controller/urls.py index 3595e4f..92edfe1 100644 --- a/access_controller/urls.py +++ b/access_controller/urls.py @@ -17,15 +17,16 @@ 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.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/', CustomLoginView.as_view(extra_context={}), name='login',), # TODO add extra context + path('accounts/login/', CustomLoginView.as_view(extra_context={}), name='login', ), # TODO add extra context path('accounts/', include('django.contrib.auth.urls')), path('accounts/', include('django_registration.backends.one_step.urls')), path('work/', work_page, name="work"), @@ -34,7 +35,7 @@ urlpatterns = [ path('accounts/', include('django_registration.backends.activation.urls')), path('accounts/login/', include('django.contrib.auth.urls')), path('control/', AdminPageView.as_view(), name='control') - ] +] urlpatterns += [ path( @@ -58,3 +59,8 @@ urlpatterns += [ name='password_reset_complete' ), ] + +# Django REST +urlpatterns += [ + path('api/', include(router.urls)) +] diff --git a/main/extra_func.py b/main/extra_func.py index 691bd37..5cfb400 100644 --- a/main/extra_func.py +++ b/main/extra_func.py @@ -3,9 +3,9 @@ import os from zenpy import Zenpy from zenpy.lib.exception import APIException -from main.models import UserProfile +from main.models import UserProfile, User -from access_controller.settings import ZENDESK_ROLES as ROLES +from access_controller.settings import ZENDESK_ROLES as ROLES, ZENDESK_ROLES class ZendeskAdmin: @@ -28,7 +28,7 @@ class ZendeskAdmin: email: str = os.getenv('ACCESS_CONTROLLER_API_EMAIL') token: str = os.getenv('ACCESS_CONTROLLER_API_TOKEN') password: str = os.getenv('ACCESS_CONTROLLER_API_PASSWORD') - _instance=None + _instance = None def __new__(cls, *args, **kwargs): if cls._instance is None: @@ -144,8 +144,9 @@ 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)) + + # У пользователей должна быть организация SYSTEM + org = next(zendesk.admin.search(type='organization', name='SYSTEM')) return zendesk.admin.organizations.users(org) @@ -191,3 +192,37 @@ 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.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 == ZENDESK_ROLES['engineer']: + engineers += 1 + elif user.custom_role_id == ZENDESK_ROLES['light_agent']: + light_agents += 1 + return engineers, light_agents + + +def update_users_in_model(): + """ + Обновляет пользователей в модели UserProfile по списку пользователей в организации + """ + users = get_users_list() + for user in users: + profile = User.objects.get(email=user.email).userprofile + update_user_in_model(profile, user) + return users diff --git a/main/serializers.py b/main/serializers.py new file mode 100644 index 0000000..26d08c2 --- /dev/null +++ b/main/serializers.py @@ -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', 'role', 'name'] diff --git a/main/templates/base/base.html b/main/templates/base/base.html index 2aebfe0..166195d 100644 --- a/main/templates/base/base.html +++ b/main/templates/base/base.html @@ -27,6 +27,7 @@ {% block extra_css %}{% endblock %} + {% block extra_scripts %}{% endblock %} diff --git a/main/templates/pages/adm_ruleset.html b/main/templates/pages/adm_ruleset.html index 387cd73..dd9d614 100644 --- a/main/templates/pages/adm_ruleset.html +++ b/main/templates/pages/adm_ruleset.html @@ -10,6 +10,13 @@ {% endblock %} +{% block extra_scripts %} + + + + +{% endblock%} + {% block content %}
@@ -37,25 +44,22 @@
IDEmail Name
{{ engineer.id }}{{ engineer.email }} {{ engineer.name }}
- + - - + {% for user in users %} - + - {% endfor %} -
IDName Email RoleName(link to profile) Checked
{{ user.id }}{{ user.name }} {{ user.user.email }} {{ user.role }}{{ user.name }}
{% endblock%} @@ -103,6 +107,6 @@ {% endblock %} - + {% endblock %} diff --git a/main/urls.py b/main/urls.py new file mode 100644 index 0000000..fffe11d --- /dev/null +++ b/main/urls.py @@ -0,0 +1,6 @@ +from rest_framework.routers import DefaultRouter +from main.views import UsersViewSet + + +router = DefaultRouter() +router.register(r'users', UsersViewSet) diff --git a/main/views.py b/main/views.py index ca8c9c2..36f82da 100644 --- a/main/views.py +++ b/main/views.py @@ -14,7 +14,7 @@ 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 + make_engineer, make_light_agent, get_users_list, update_users_in_model, count_users from django.contrib.auth.models import User, Permission from main.models import UserProfile @@ -27,6 +27,11 @@ from django.core.exceptions import PermissionDenied from access_controller.settings import ZENDESK_ROLES from zenpy.lib.api_objects import User as ZenpyUser +# Django REST +from rest_framework import viewsets, status +from main.serializers import ProfileSerializer +from rest_framework.response import Response +from rest_framework.decorators import action content_type_temp = ContentType.objects.get_for_model(UserProfile) permission_temp, created = Permission.objects.get_or_create( @@ -193,22 +198,6 @@ class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin, FormView): def make_light_agents(users): [make_light_agent(user) for user in users] - @staticmethod - def count_users(users) -> tuple: - """ - Функция подсчета количества сотрудников с ролями engineer и light_a - - .. todo:: - this func counts users from all zendesk instead of just from a model: - """ - engineers, light_agents = 0, 0 - for user in users: - if user.custom_role_id == ZENDESK_ROLES['engineer']: - engineers += 1 - elif user.custom_role_id == ZENDESK_ROLES['light_agent']: - light_agents += 1 - return engineers, light_agents - def get_context_data(self, **kwargs) -> dict: """ Функция формирования контента страницы администратора (с проверкой прав доступа) @@ -216,9 +205,10 @@ class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin, FormView): if self.request.user.userprofile.role != 'admin': raise PermissionDenied 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(users) return context # TODO: need to get profile page url @@ -227,3 +217,18 @@ 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() + profiles = UserProfile.objects.filter(role='agent') + count = count_users(users) + serializer = self.get_serializer(data=profiles, many=True) + return Response(serializer.data + {'engineers': count[0], 'light_agents': count[1]}) diff --git a/requirements.txt b/requirements.txt index 7a4f941..b32a382 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,8 @@ 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 diff --git a/static/main/js/control.js b/static/main/js/control.js index 1fd4f9c..6404741 100644 --- a/static/main/js/control.js +++ b/static/main/js/control.js @@ -1,9 +1,60 @@ "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); + +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); + } } } + +class TableRow extends React.Component { + render() { + return ( + + + {this.props.user.name} + + {this.props.user.user.email} + {this.props.user.role} + + + ); + } +} + +class TableBody extends React.Component { + constructor(props) { + super(props); + this.state = { users: [] }; + } + + get_users() { + axios.get("/api/users").then((response) => { + this.setState({ users: response.data }); + }); + } + + componentDidMount() { + this.interval = setInterval(() => { + this.get_users(); + move_checkboxes(); + }, 1000); + } + + componentWillUnmount() { + clearInterval(this.interval); + } + + render() { + return this.state.users.map((user, key) => ( + + )); + } +} + +move_checkboxes(); +ReactDOM.render(, document.getElementById("table")); From 7ee8cc96af1f5b8e15b3eca57fcdc15fb21d707f Mon Sep 17 00:00:00 2001 From: Artyom Kravchenko Date: Sun, 7 Mar 2021 23:08:56 +0300 Subject: [PATCH 27/54] fixed get and end engineer role fuctions --- main/views.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/main/views.py b/main/views.py index 0008255..3ec1319 100644 --- a/main/views.py +++ b/main/views.py @@ -135,7 +135,7 @@ def work_page(request, id): for user in users: if user.custom_role_id == ZENDESK_ROLES['engineer']: - engineers.append((user)) + engineers.append(user) elif user.custom_role_id == ZENDESK_ROLES['light_agent']: light_agents.append(user) @@ -151,8 +151,9 @@ def work_page(request, id): @login_required() def work_hand_over(request): zenpy_user, admin = auth_user(request) - if zenpy_user.role == "admin" or zenpy_user.role == "end-user": - zenpy_user.role = "agent" + + 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() @@ -162,10 +163,10 @@ def work_hand_over(request): @login_required() def work_become_engineer(request): zenpy_user, admin = auth_user(request) - if zenpy_user.role == "agent" or zenpy_user.role == "end-user": - zenpy_user.role = "admin" + 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 = "admin" + request.user.userprofile.role = "agent" request.user.userprofile.save() return HttpResponseRedirect(reverse('work', args=(request.user.id,))) From b01f5fb236f5e822607cc31208a32ef39da3afc7 Mon Sep 17 00:00:00 2001 From: Artyom Kravchenko Date: Mon, 8 Mar 2021 02:28:00 +0300 Subject: [PATCH 28/54] add button from menu for engineer rules request --- main/templates/base/menu.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/main/templates/base/menu.html b/main/templates/base/menu.html index f64ceb3..a389341 100644 --- a/main/templates/base/menu.html +++ b/main/templates/base/menu.html @@ -13,6 +13,8 @@ Профиль {% if perms.main.has_control_access %} Управление + {% else %} + Запрос прав {% endif %} Выйти From 49046022d77457c0cb8281883970795bacfd7d0a Mon Sep 17 00:00:00 2001 From: Sokurov Idar Date: Mon, 8 Mar 2021 17:50:47 +0300 Subject: [PATCH 29/54] small refactor: change variable`s names, add a bit documentation --- main/extra_func.py | 17 +++++---- main/forms.py | 6 +-- main/views.py | 94 ++++++++++++++++++++++++---------------------- 3 files changed, 61 insertions(+), 56 deletions(-) diff --git a/main/extra_func.py b/main/extra_func.py index 623be63..8d5abfd 100644 --- a/main/extra_func.py +++ b/main/extra_func.py @@ -1,12 +1,11 @@ import os -from datetime import timedelta, datetime +from datetime import timedelta 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 main.models import UserProfile class ZendeskAdmin: @@ -29,7 +28,7 @@ class ZendeskAdmin: email: str = os.getenv('ACCESS_CONTROLLER_API_EMAIL') token: str = os.getenv('ACCESS_CONTROLLER_API_TOKEN') password: str = os.getenv('ACCESS_CONTROLLER_API_PASSWORD') - _instance=None + _instance = None def __new__(cls, *args, **kwargs): if cls._instance is None: @@ -194,15 +193,17 @@ def check_user_auth(email: str, password: str) -> bool: return True -def daterange(start_date, end_date): +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)) + dates.append(start_date + timedelta(n)) return dates -def get_timedelta(log): +def get_timedelta(log) -> timedelta: + # Возвращает объект класса timedelta, который хранит промежуток времени от начала суток до момента, + # который находится в log(RoleChangeLogs) time = log.change_time.time() time = timedelta(hours=time.hour, minutes=time.minute, seconds=time.second) - print(time) return time diff --git a/main/forms.py b/main/forms.py index efafa01..7046b35 100644 --- a/main/forms.py +++ b/main/forms.py @@ -70,10 +70,10 @@ class StatisticForm(forms.Form): email = forms.EmailField( label='Электроная почта', ) - inter = forms.CharField( # TODO: Переделать под html страницу + interval = forms.CharField( # TODO: Переделать под html страницу label='Выбор интервала', ) - dio_start = forms.DateField( # TODO: Переделать под html страницу + range_start = forms.DateField( # TODO: Переделать под html страницу label='Начало диапазона', widget=forms.DateInput( attrs={ @@ -81,7 +81,7 @@ class StatisticForm(forms.Form): } ), ) - dio_end = forms.DateField( # TODO: Переделать под html страницу + range_end = forms.DateField( # TODO: Переделать под html страницу label='Конец диапазона', widget=forms.DateInput( attrs={ diff --git a/main/views.py b/main/views.py index 3f2df8f..79c14bc 100644 --- a/main/views.py +++ b/main/views.py @@ -1,30 +1,22 @@ import logging import os +from datetime import timedelta -from django.contrib.auth.tokens import default_token_generator +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 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 import Zenpy from zenpy.lib.api_objects import User as ZenpyUser from access_controller.settings import EMAIL_HOST_USER, ZENDESK_ROLES @@ -247,8 +239,8 @@ def statistic_page(request): form = StatisticForm(request.POST) if form.is_valid(): - start_date = form.cleaned_data['dio_start'] - end_date = form.cleaned_data['dio_end'] + start_date = form.cleaned_data['range_start'] + end_date = form.cleaned_data['range_end'] try: data = RoleChangeLogs.objects.filter( @@ -259,53 +251,68 @@ def statistic_page(request): context['errors'] = 'Пользователь не найден' context['form'] = StatisticForm() return render(request, 'pages/stat.html', context) - sep = form.cleaned_data['inter'] # Разрез + + interval = form.cleaned_data['interval'] # интервал TODO: переделать под html-страницу + show = form.cleaned_data['display_format'] # формат отображения TODO: переделать под html-страницу stat = {} - # Этот кусок кода будет заполнять массив stat, в котором ключ - дата, значение кол-во часов - # Далее будет заполнение контекса в зависимости от выбранного интервала и отображения - # (пока есть только отображение в часах, разрез в днях) + # Этот кусок кода будет заполнять массив stat, в котором ключ - дата, значение - кол-во секунд + # Далее будет заполнение контекса в зависимости от выбранного интервала(дни/месяцы) и формата отобржаения(дни/часы) + # (пока есть только отображение в часах, интервал в днях) # Обнуление всех дней for day in daterange(start_date, end_date + timedelta(days=1)): stat[day] = 0 + first_log = data[0] + last_log = data[len(data) - 1] # Проеврка крайних случаев - # Если окажется, что инженер работал ещё до начала диапазона - if data[0].old_role == 'engineer': - for day in daterange(start_date, data[0].change_time.date()): - stat[day] = 24 - stat[data[0].change_time.date()] += data[0].change_time.time().hour - # Если окажется, что инженер закончил работать после диапазона - if data[len(data) - 1].new_role == 'engineer': - for day in daterange(data[len(data) - 1].change_time.date() + timedelta(days=1), + # Если окажется, что инженер работал ещё до начала диапазона(мы видим, что он был инженером до диапазона) + if first_log.old_role == 'engineer': + for day in daterange(start_date, first_log.change_time.date()): + stat[day] = 24 * 3600 + stat[first_log.change_time.date()] += get_timedelta(first_log.change_time.time()).total_seconds() + + # Если окажется, что инженер закончил работать после диапазона(мы видим, что он был инженером и после диапазона) + if last_log.new_role == 'engineer': + for day in daterange(last_log.change_time.date() + timedelta(days=1), end_date + timedelta(days=1)): - stat[day] = 24 - stat[data[len(data) - 1].change_time.date()] += int( - (timedelta(days=1) - get_timedelta(data[len(data) - 1])).total_seconds() // 3600) + stat[day] = 24 * 3600 + stat[last_log.change_time.date()] += (timedelta(days=1) - get_timedelta(last_log)).total_seconds() # Цикл по логам for log_index in range(len(data) - 1): if data[log_index].new_role == 'engineer': - log1, log2 = data[log_index], data[log_index + 1] + current_log, next_log = data[log_index], data[log_index + 1] # Если сессия закончилась НЕ в тот же день, что и началась - if log1.change_time.date() != log2.change_time.date(): - stat[log1.change_time.date()] += int( - (timedelta(days=1) - get_timedelta(log1)).total_seconds() // 3600) - stat[log2.change_time.date()] += log2.change_time.time().hour + if current_log.change_time.date() != next_log.change_time.date(): + stat[current_log.change_time.date()] += ( + timedelta(days=1) - get_timedelta(current_log)).total_seconds() + stat[next_log.change_time.date()] += get_timedelta(next_log).total_seconds() # Если проработал несколько дней подряд, то заполнить эти дни по 24 часа - for day in daterange(log1.change_time.date() + timedelta(days=1), log2.change_time.date()): - stat[day] = 24 + for day in daterange(current_log.change_time.date() + timedelta(days=1), + next_log.change_time.date()): + stat[day] = 24 * 3600 # Если сессия закончилась в тот же день, что и началась else: - times = log2.change_time - log1.change_time - stat[log1.change_time.date()] += times.seconds // 3600 + elapsed_time = next_log.change_time - current_log.change_time + stat[current_log.change_time.date()] += elapsed_time.total_seconds() - if sep == 'days': + # Переделываем наши значения под формат отображения + for key, item in stat.items(): + if show == 'hours': + stat[key] = item / 3600 + elif show == 'days': + stat[key] = item / 86400 + + # Переделываем наши значения под формат отображения + if interval == 'days': context['stats_logs'] = stat + elif interval == 'months': + pass else: context['errors'] = form.errors @@ -314,6 +321,3 @@ def statistic_page(request): context['form'] = form return render(request, 'pages/stat.html', context) - - -from datetime import timedelta From b96deab2cb4a51e88f2f38b817aa21c888a77b43 Mon Sep 17 00:00:00 2001 From: Sokurov Idar Date: Mon, 8 Mar 2021 21:07:09 +0300 Subject: [PATCH 30/54] Update statistic backend --- main/extra_func.py | 62 ++++++++++++++-- main/forms.py | 5 +- main/templates/pages/stat.html | 13 +++- main/views.py | 127 ++++++++++++--------------------- 4 files changed, 117 insertions(+), 90 deletions(-) diff --git a/main/extra_func.py b/main/extra_func.py index 8d5abfd..03fd61a 100644 --- a/main/extra_func.py +++ b/main/extra_func.py @@ -1,5 +1,5 @@ import os -from datetime import timedelta +from datetime import timedelta, datetime from zenpy import Zenpy from zenpy.lib.exception import APIException @@ -194,7 +194,9 @@ def check_user_auth(email: str, password: str) -> bool: def daterange(start_date, end_date) -> list: - # Возвращает список дней с start_date по end_date исключая правую границу + """ + Возвращает список дней с start_date по end_date исключая правую границу + """ dates = [] for n in range(int((end_date - start_date).days)): dates.append(start_date + timedelta(n)) @@ -202,8 +204,60 @@ def daterange(start_date, end_date) -> list: def get_timedelta(log) -> timedelta: - # Возвращает объект класса timedelta, который хранит промежуток времени от начала суток до момента, - # который находится в log(RoleChangeLogs) + """ + Возвращает объект класса timedelta, который хранит промежуток времени от начала суток до момента, + который находится в log (объект класса RoleChangeLogs) + """ 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) + + +def get_statistic_from_data(data, start_date, end_date): + """ + Функция возвращает словарь, в котором ключ - дата, значение - кол-во проработанных в этот день секунд + data - массив объектов RoleChangeLogs, является списком логов пользователя + """ + if not data: + return None + # Обнуление всех дней + stat = {} + for day in daterange(start_date, end_date + timedelta(days=1)): + stat[day] = 0 + first_log, last_log = data[0], data[len(data) - 1] + + # Если инженер работал ещё до начала диапазона + if int(first_log.old_role) == ROLES['engineer']: + for day in daterange(start_date, first_log.change_time.date()): + stat[day] = 24 * 3600 + stat[first_log.change_time.date()] += get_timedelta(first_log).total_seconds() + + # Если инженер закончил работать после диапазона + if int(last_log.new_role) == ROLES['engineer']: + for day in daterange(last_log.change_time.date() + timedelta(days=1), end_date + timedelta(days=1)): + stat[day] = 24 * 3600 + stat[last_log.change_time.date()] += (timedelta(days=1)-get_timedelta(last_log)).total_seconds() + # Цикл по логам + for log_index in range(len(data) - 1): + if int(data[log_index].new_role) == ROLES['engineer']: + current_log, next_log = data[log_index], data[log_index + 1] + # Если сессия закончилась НЕ в тот же день, что и началась + if current_log.change_time.date() != next_log.change_time.date(): + stat[current_log.change_time.date()] += (timedelta(days=1) - get_timedelta(current_log)).total_seconds() + stat[next_log.change_time.date()] += get_timedelta(next_log).total_seconds() + # Если проработал несколько дней подряд, то заполнить эти дни по 24 часа + for day in daterange(current_log.change_time.date() + timedelta(days=1),next_log.change_time.date()): + stat[day] = 24 * 3600 + # Если сессия закончилась в тот же день, что и началась + else: + elapsed_time = next_log.change_time - current_log.change_time + stat[current_log.change_time.date()] += elapsed_time.total_seconds() + return stat diff --git a/main/forms.py b/main/forms.py index 7046b35..e98b7e1 100644 --- a/main/forms.py +++ b/main/forms.py @@ -71,7 +71,10 @@ class StatisticForm(forms.Form): label='Электроная почта', ) interval = forms.CharField( # TODO: Переделать под html страницу - label='Выбор интервала', + label='Интервал работы', + ) + display_format = forms.CharField( # TODO: Переделать под html страницу + label='Формат отображения', ) range_start = forms.DateField( # TODO: Переделать под html страницу label='Начало диапазона', diff --git a/main/templates/pages/stat.html b/main/templates/pages/stat.html index ea5e793..2409e0f 100644 --- a/main/templates/pages/stat.html +++ b/main/templates/pages/stat.html @@ -4,7 +4,7 @@ {% block title %}{{ pagename }}{% endblock %} -{% block heading %}Статистика{% endblock %} +{% block heading %} Пример страницы статистики(палками не бейти плиз){% endblock %} {% block extra_css %} @@ -21,10 +21,17 @@ {% endfor %} - {% for key,val in stats_logs.items %} -

{{key}} {{val}}

+ {% for key,val in log_stats.items %} +

{{key}} | {{val}}


{% endfor %} + +
    + {% for error in errors %} +
  • {{error}}
  • + {% endfor %} +
+ {% endblock %} diff --git a/main/views.py b/main/views.py index 79c14bc..f7a6967 100644 --- a/main/views.py +++ b/main/views.py @@ -1,6 +1,6 @@ import logging import os -from datetime import timedelta +from datetime import timedelta, date, datetime from django.contrib.auth.decorators import login_required from django.contrib.auth.forms import PasswordResetForm @@ -21,7 +21,7 @@ from zenpy.lib.api_objects import User as ZenpyUser from access_controller.settings import EMAIL_HOST_USER, ZENDESK_ROLES from main.extra_func import check_user_exist, update_profile, get_user_organization, make_engineer, make_light_agent, \ - get_users_list, daterange, get_timedelta + get_users_list, last_day_of_month, get_statistic_from_data from main.forms import AdminPageUsers, CustomRegistrationForm, CustomAuthenticationForm, StatisticForm from .models import UserProfile, RoleChangeLogs @@ -230,94 +230,57 @@ class CustomLoginView(LoginView): def statistic_page(request): if not request.user.is_superuser: return redirect('index') - context = { 'pagename': 'страница статистики', + 'errors': [], } - if request.method == "POST": form = StatisticForm(request.POST) - - if form.is_valid(): - start_date = form.cleaned_data['range_start'] - end_date = form.cleaned_data['range_end'] - - try: - data = RoleChangeLogs.objects.filter( - change_time__range=[start_date, end_date], - user=User.objects.get(email=form.cleaned_data['email']), - ).order_by('change_time') - except User.DoesNotExist: - context['errors'] = 'Пользователь не найден' - context['form'] = StatisticForm() - return render(request, 'pages/stat.html', context) - - interval = form.cleaned_data['interval'] # интервал TODO: переделать под html-страницу - show = form.cleaned_data['display_format'] # формат отображения TODO: переделать под html-страницу - stat = {} - - # Этот кусок кода будет заполнять массив stat, в котором ключ - дата, значение - кол-во секунд - # Далее будет заполнение контекса в зависимости от выбранного интервала(дни/месяцы) и формата отобржаения(дни/часы) - # (пока есть только отображение в часах, интервал в днях) - - # Обнуление всех дней - for day in daterange(start_date, end_date + timedelta(days=1)): - stat[day] = 0 - - first_log = data[0] - last_log = data[len(data) - 1] - # Проеврка крайних случаев - # Если окажется, что инженер работал ещё до начала диапазона(мы видим, что он был инженером до диапазона) - if first_log.old_role == 'engineer': - for day in daterange(start_date, first_log.change_time.date()): - stat[day] = 24 * 3600 - stat[first_log.change_time.date()] += get_timedelta(first_log.change_time.time()).total_seconds() - - # Если окажется, что инженер закончил работать после диапазона(мы видим, что он был инженером и после диапазона) - if last_log.new_role == 'engineer': - for day in daterange(last_log.change_time.date() + timedelta(days=1), - end_date + timedelta(days=1)): - stat[day] = 24 * 3600 - stat[last_log.change_time.date()] += (timedelta(days=1) - get_timedelta(last_log)).total_seconds() - - # Цикл по логам - for log_index in range(len(data) - 1): - if data[log_index].new_role == 'engineer': - current_log, next_log = data[log_index], data[log_index + 1] - - # Если сессия закончилась НЕ в тот же день, что и началась - if current_log.change_time.date() != next_log.change_time.date(): - stat[current_log.change_time.date()] += ( - timedelta(days=1) - get_timedelta(current_log)).total_seconds() - stat[next_log.change_time.date()] += get_timedelta(next_log).total_seconds() - - # Если проработал несколько дней подряд, то заполнить эти дни по 24 часа - for day in daterange(current_log.change_time.date() + timedelta(days=1), - next_log.change_time.date()): - stat[day] = 24 * 3600 - - # Если сессия закончилась в тот же день, что и началась - else: - elapsed_time = next_log.change_time - current_log.change_time - stat[current_log.change_time.date()] += elapsed_time.total_seconds() - - # Переделываем наши значения под формат отображения - for key, item in stat.items(): - if show == 'hours': - stat[key] = item / 3600 - elif show == 'days': - stat[key] = item / 86400 - - # Переделываем наши значения под формат отображения - if interval == 'days': - context['stats_logs'] = stat - elif interval == 'months': - pass + if not form.is_valid(): + context['errors'] += form.errors else: - context['errors'] = form.errors + start_date, end_date, data = form.cleaned_data['range_start'], form.cleaned_data['range_end'], list() + if end_date < start_date or end_date > datetime.now().date(): + context['errors'] += ['Конец диапазона должен быть больше начала диапазона и меньше текущего времени'] + else: + try: + data = RoleChangeLogs.objects.filter( + change_time__range=[start_date, end_date + timedelta(days=1)], + user=User.objects.get(email=form.cleaned_data['email']), + ).order_by('change_time') + except User.DoesNotExist: + context['errors'] = ['Пользователь не найден'] + stats = get_statistic_from_data(data, start_date, end_date) + if stats is None: + context['errors'] += ['Не найдено изменений роли в указаном диапазоне'] + interval = form.cleaned_data['interval'] # интервал работы TODO: переделать под html-страницу + show = form.cleaned_data['display_format'] # формат отображения + if not (show in ['hours', 'days']): # Работа с форматом отображения + context['errors'] += ['Формат отображения должен быть в часах или днях'] + elif stats: + for key, item in stats.items(): + if show == 'hours': + stats[key] = item / 3600 + elif show == 'days': + stats[key] = item / 86400 + + if not (interval in ['days', 'months']): # Работа с интервалом работы + context['errors'] += ['Интервал работы должен быть в днях или месяцах'] + elif interval == 'months' and stats: # Переделываем ключи под формат в прототипе(начало_месяца-конец_месяца) + new_stats = {} + for key, value in stats.items(): + current_month_start, current_month_end = max(start_date, + date(year=key.year, month=key.month, day=1)), min( + 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_stats.get(index): + new_stats[index] += value + else: + new_stats[index] = value + stats = new_stats + 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) From ce55ec61e66406b581ff0c2e5be2d7d3f2ce3e70 Mon Sep 17 00:00:00 2001 From: Sokurov Idar Date: Mon, 8 Mar 2021 22:48:31 +0300 Subject: [PATCH 31/54] extract function for easier processing a list of emails in future --- main/extra_func.py | 45 +++++++++++++++++++++------------------------ main/views.py | 25 +++++++++---------------- 2 files changed, 30 insertions(+), 40 deletions(-) diff --git a/main/extra_func.py b/main/extra_func.py index 03fd61a..9fb141d 100644 --- a/main/extra_func.py +++ b/main/extra_func.py @@ -1,11 +1,12 @@ import os from datetime import timedelta, datetime +from django.contrib.auth.models import User from zenpy import Zenpy from zenpy.lib.exception import APIException from access_controller.settings import ZENDESK_ROLES as ROLES -from main.models import UserProfile +from main.models import UserProfile, RoleChangeLogs class ZendeskAdmin: @@ -86,7 +87,7 @@ class ZendeskAdmin: Функция **get_user_org** возвращает организацию, к которой относится пользователь по его email """ user = self.admin.users.search(email).values[0] - return user.organization.name + return user.organization.name if user.organization else None def create_admin(self) -> Zenpy: """ @@ -100,7 +101,7 @@ class ZendeskAdmin: 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 @@ -174,25 +175,6 @@ def get_user_organization(email: str) -> str: return ZendeskAdmin().get_user_org(email) -def check_user_auth(email: str, password: str) -> bool: - """ - Функция проверяет, верны ли входные данные - - :raise: :class:`APIException`: исключение, вызываемое если пользователь не аутентифицирован - """ - creds = { - 'email': email, - 'password': password, - 'subdomain': 'ngenix1612197338', - } - try: - user = Zenpy(**creds) - user.search(email, type='user') - except APIException: - return False - return True - - def daterange(start_date, end_date) -> list: """ Возвращает список дней с start_date по end_date исключая правую границу @@ -244,7 +226,7 @@ def get_statistic_from_data(data, start_date, end_date): if int(last_log.new_role) == ROLES['engineer']: for day in daterange(last_log.change_time.date() + timedelta(days=1), end_date + timedelta(days=1)): stat[day] = 24 * 3600 - stat[last_log.change_time.date()] += (timedelta(days=1)-get_timedelta(last_log)).total_seconds() + stat[last_log.change_time.date()] += (timedelta(days=1) - get_timedelta(last_log)).total_seconds() # Цикл по логам for log_index in range(len(data) - 1): if int(data[log_index].new_role) == ROLES['engineer']: @@ -254,10 +236,25 @@ def get_statistic_from_data(data, start_date, end_date): stat[current_log.change_time.date()] += (timedelta(days=1) - get_timedelta(current_log)).total_seconds() stat[next_log.change_time.date()] += get_timedelta(next_log).total_seconds() # Если проработал несколько дней подряд, то заполнить эти дни по 24 часа - for day in daterange(current_log.change_time.date() + timedelta(days=1),next_log.change_time.date()): + for day in daterange(current_log.change_time.date() + timedelta(days=1), next_log.change_time.date()): stat[day] = 24 * 3600 # Если сессия закончилась в тот же день, что и началась else: elapsed_time = next_log.change_time - current_log.change_time stat[current_log.change_time.date()] += elapsed_time.total_seconds() return stat + + +def get_data_logs(context, start_date, end_date, email): + """ + Функция возвращает список из лог-ов в диапазоне дат start_date-end_date для пользователя с почтой email + """ + data = [] + try: + data = RoleChangeLogs.objects.filter( + change_time__range=[start_date, end_date + timedelta(days=1)], + user=User.objects.get(email=email), + ).order_by('change_time') + except User.DoesNotExist: + context['errors'] = ['Пользователь не найден'] + return data diff --git a/main/views.py b/main/views.py index 72918e5..b2d4fc0 100644 --- a/main/views.py +++ b/main/views.py @@ -1,6 +1,6 @@ import logging import os -from datetime import timedelta, date, datetime +from datetime import date, datetime from django.contrib.auth.decorators import login_required from django.contrib.auth.forms import PasswordResetForm @@ -21,9 +21,9 @@ from zenpy.lib.api_objects import User as ZenpyUser from access_controller.settings import EMAIL_HOST_USER, ZENDESK_ROLES from main.extra_func import check_user_exist, update_profile, get_user_organization, make_engineer, make_light_agent, \ - get_users_list, last_day_of_month, get_statistic_from_data + get_users_list, last_day_of_month, get_statistic_from_data, get_data_logs from main.forms import AdminPageUsers, CustomRegistrationForm, CustomAuthenticationForm, StatisticForm -from .models import UserProfile, RoleChangeLogs +from .models import UserProfile content_type_temp = ContentType.objects.get_for_model(UserProfile) permission_temp, created = Permission.objects.get_or_create( @@ -252,20 +252,13 @@ def statistic_page(request): else: start_date, end_date, data = form.cleaned_data['range_start'], form.cleaned_data['range_end'], list() if end_date < start_date or end_date > datetime.now().date(): - context['errors'] += ['Конец диапазона должен быть больше начала диапазона и меньше текущего времени'] + context['errors'] += ['Конец диапазона должен быть позже начала диапазона и раньше текущего времени'] else: - try: - data = RoleChangeLogs.objects.filter( - change_time__range=[start_date, end_date + timedelta(days=1)], - user=User.objects.get(email=form.cleaned_data['email']), - ).order_by('change_time') - except User.DoesNotExist: - context['errors'] = ['Пользователь не найден'] + data = get_data_logs(context, start_date, end_date, form.cleaned_data['email']) stats = get_statistic_from_data(data, start_date, end_date) if stats is None: context['errors'] += ['Не найдено изменений роли в указаном диапазоне'] - interval = form.cleaned_data['interval'] # интервал работы TODO: переделать под html-страницу - show = form.cleaned_data['display_format'] # формат отображения + interval, show = form.cleaned_data['interval'], form.cleaned_data['display_format'] if not (show in ['hours', 'days']): # Работа с форматом отображения context['errors'] += ['Формат отображения должен быть в часах или днях'] @@ -281,15 +274,15 @@ def statistic_page(request): elif interval == 'months' and stats: # Переделываем ключи под формат в прототипе(начало_месяца-конец_месяца) new_stats = {} for key, value in stats.items(): - current_month_start, current_month_end = max(start_date, - date(year=key.year, month=key.month, day=1)), min( - end_date, last_day_of_month(date(year=key.year, month=key.month, day=1))) + current_month_start = max(start_date, date(year=key.year, month=key.month, day=1)) + current_month_end = min(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_stats.get(index): new_stats[index] += value else: new_stats[index] = value stats = new_stats + context['log_stats'] = stats if not context['errors'] else None if request.method == 'GET': form = StatisticForm() From 5efec2641a8ef630e84861a5d60e4d658be2be6b Mon Sep 17 00:00:00 2001 From: Sokurov Idar Date: Tue, 9 Mar 2021 00:53:11 +0300 Subject: [PATCH 32/54] Refactor statistic page with class StatisticData --- main/extra_func.py | 201 ++++++++++++++++++++++++--------- main/templates/pages/stat.html | 9 ++ main/views.py | 51 +++------ 3 files changed, 174 insertions(+), 87 deletions(-) diff --git a/main/extra_func.py b/main/extra_func.py index 9fb141d..c47e6a1 100644 --- a/main/extra_func.py +++ b/main/extra_func.py @@ -1,5 +1,5 @@ import os -from datetime import timedelta, datetime +from datetime import timedelta, datetime, date from django.contrib.auth.models import User from zenpy import Zenpy @@ -203,58 +203,155 @@ def last_day_of_month(day): return next_month - timedelta(days=next_month.day) -def get_statistic_from_data(data, start_date, end_date): - """ - Функция возвращает словарь, в котором ключ - дата, значение - кол-во проработанных в этот день секунд - data - массив объектов RoleChangeLogs, является списком логов пользователя - """ - if not data: - return None - # Обнуление всех дней - stat = {} - for day in daterange(start_date, end_date + timedelta(days=1)): - stat[day] = 0 - first_log, last_log = data[0], data[len(data) - 1] +class StatisticData: + def __init__(self, start_date, end_date, user_email): + self.errors = list() + self.data = None + self.statistic = dict() + self.start_date = start_date + self.end_date = end_date + self.email = user_email + self._set_data() + self._set_statistic() - # Если инженер работал ещё до начала диапазона - if int(first_log.old_role) == ROLES['engineer']: - for day in daterange(start_date, first_log.change_time.date()): - stat[day] = 24 * 3600 - stat[first_log.change_time.date()] += get_timedelta(first_log).total_seconds() + def use_interval(self, interval): + """ + Объединяет ключи и значения в соответствии с интервалом работы + """ + if not self.is_valid_statistic(): + return None + if not (interval in ['days', 'months']): + self.errors += ['Интервал работы должен быть в днях или месяцах'] + return None + stat = {} + if interval == 'months': + # Переделываем ключи под формат, как в прототипе('начало_месяца - конец_месяца') + for key, value in self.get_statistic().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 stat.get(index): + stat[index] += value + else: + stat[index] = value + elif interval == 'days': + stat = self.get_statistic() + return stat - # Если инженер закончил работать после диапазона - if int(last_log.new_role) == ROLES['engineer']: - for day in daterange(last_log.change_time.date() + timedelta(days=1), end_date + timedelta(days=1)): - stat[day] = 24 * 3600 - stat[last_log.change_time.date()] += (timedelta(days=1) - get_timedelta(last_log)).total_seconds() - # Цикл по логам - for log_index in range(len(data) - 1): - if int(data[log_index].new_role) == ROLES['engineer']: - current_log, next_log = data[log_index], data[log_index + 1] - # Если сессия закончилась НЕ в тот же день, что и началась - if current_log.change_time.date() != next_log.change_time.date(): - stat[current_log.change_time.date()] += (timedelta(days=1) - get_timedelta(current_log)).total_seconds() - stat[next_log.change_time.date()] += get_timedelta(next_log).total_seconds() - # Если проработал несколько дней подряд, то заполнить эти дни по 24 часа - for day in daterange(current_log.change_time.date() + timedelta(days=1), next_log.change_time.date()): - stat[day] = 24 * 3600 - # Если сессия закончилась в тот же день, что и началась - else: - elapsed_time = next_log.change_time - current_log.change_time - stat[current_log.change_time.date()] += elapsed_time.total_seconds() - return stat + def use_display(self, display_format): + """ + Приводит данные к формату отображения, возвращает их + """ + if not self.get_statistic(): + return None + if not (display_format in ['hours', 'days']): + self.errors += ['Формат отображения должен быть в часах или днях'] + return None + for key, item in self.statistic.items(): + if display_format == 'hours': + self.statistic[key] = item / 3600 + elif display_format == 'days': + self.statistic[key] = item / 86400 + return self.statistic.copy() + def pop_errors(self): + """ + Возвращает все текущие ошибки + """ + errors = self.errors.copy() + self.errors.clear() + return errors -def get_data_logs(context, start_date, end_date, email): - """ - Функция возвращает список из лог-ов в диапазоне дат start_date-end_date для пользователя с почтой email - """ - data = [] - try: - data = RoleChangeLogs.objects.filter( - change_time__range=[start_date, end_date + timedelta(days=1)], - user=User.objects.get(email=email), - ).order_by('change_time') - except User.DoesNotExist: - context['errors'] = ['Пользователь не найден'] - return data + def get_data(self): + """ + Вернуть данные + data - массив объектов RoleChangeLogs, является списком логов пользователя + """ + if self.is_valid_data(): + return self.data + else: + return None + + def get_statistic(self): + """ + Вернуть словарь statistic или None, если были ошибки при создании + """ + if self.is_valid_statistic(): + return self.statistic.copy() + else: + return None + + def is_valid_statistic(self): + """ + Были ли ошибки при создании статистики + """ + return not self.errors and self.statistic + + def is_valid_data(self): + """ + Были ли ошибки при создании объекта + """ + return not self.errors + + def _set_data(self): + """ + Получение списка из лог-ов в диапазоне дат start_date-end_date для пользователя с почтой email + """ + 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') + self.errors.clear() + except User.DoesNotExist: + self.errors += ['Пользователь не найден'] + + def _set_statistic(self): + """ + Функция заполняет словарь, в котором ключ - дата, значение - кол-во проработанных в этот день секунд + """ + if not self.get_data(): + self.errors += ['Не обнаружены изменения роли в данном промежутке'] + return None + + self.clear_statistic() + first_log, last_log = self.data[0], self.data[len(self.data) - 1] + + # Если инженер работал ещё до начала диапазона + if int(first_log.old_role) == ROLES['engineer']: + self.fill_daterange(self.start_date, first_log.change_time.date()) + self.statistic[first_log.change_time.date()] += get_timedelta(first_log).total_seconds() + + # Если инженер закончил работать после диапазона + if int(last_log.new_role) == ROLES['engineer']: + self.fill_daterange(last_log.change_time.date() + timedelta(days=1), self.end_date + timedelta(days=1)) + self.statistic[last_log.change_time.date()] += (timedelta(days=1) - get_timedelta(last_log)).total_seconds() + # Цикл по логам + for log_index in range(len(self.data) - 1): + if int(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() + # Если проработал несколько дней подряд, то заполнить эти дни по 24 часа + 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.fill_daterange(self.start_date, self.end_date + timedelta(days=1), 0) diff --git a/main/templates/pages/stat.html b/main/templates/pages/stat.html index 2409e0f..69afbde 100644 --- a/main/templates/pages/stat.html +++ b/main/templates/pages/stat.html @@ -31,6 +31,15 @@
  • {{error}}
  • {% endfor %} + {%if form.errors%} +
      + {% for field, errors in form.errors.items %} + {% for error in errors %} +
    • {{error}}
    • + {% endfor %} + {% endfor %} +
    + {%endif%} {% endblock %} diff --git a/main/views.py b/main/views.py index b2d4fc0..bb9434f 100644 --- a/main/views.py +++ b/main/views.py @@ -1,6 +1,6 @@ import logging import os -from datetime import date, datetime +from datetime import datetime from django.contrib.auth.decorators import login_required from django.contrib.auth.forms import PasswordResetForm @@ -21,7 +21,7 @@ from zenpy.lib.api_objects import User as ZenpyUser from access_controller.settings import EMAIL_HOST_USER, ZENDESK_ROLES from main.extra_func import check_user_exist, update_profile, get_user_organization, make_engineer, make_light_agent, \ - get_users_list, last_day_of_month, get_statistic_from_data, get_data_logs + get_users_list, StatisticData from main.forms import AdminPageUsers, CustomRegistrationForm, CustomAuthenticationForm, StatisticForm from .models import UserProfile @@ -247,42 +247,23 @@ def statistic_page(request): } if request.method == "POST": form = StatisticForm(request.POST) - if not form.is_valid(): - context['errors'] += form.errors - else: - start_date, end_date, data = form.cleaned_data['range_start'], form.cleaned_data['range_end'], list() + if form.is_valid(): + start_date, end_date, stats = form.cleaned_data['range_start'], form.cleaned_data['range_end'], dict() + interval, show = form.cleaned_data['interval'], form.cleaned_data['display_format'] if end_date < start_date or end_date > datetime.now().date(): context['errors'] += ['Конец диапазона должен быть позже начала диапазона и раньше текущего времени'] else: - data = get_data_logs(context, start_date, end_date, form.cleaned_data['email']) - stats = get_statistic_from_data(data, start_date, end_date) - if stats is None: - context['errors'] += ['Не найдено изменений роли в указаном диапазоне'] - interval, show = form.cleaned_data['interval'], form.cleaned_data['display_format'] - - if not (show in ['hours', 'days']): # Работа с форматом отображения - context['errors'] += ['Формат отображения должен быть в часах или днях'] - elif stats: - for key, item in stats.items(): - if show == 'hours': - stats[key] = item / 3600 - elif show == 'days': - stats[key] = item / 86400 - - if not (interval in ['days', 'months']): # Работа с интервалом работы - context['errors'] += ['Интервал работы должен быть в днях или месяцах'] - elif interval == 'months' and stats: # Переделываем ключи под формат в прототипе(начало_месяца-конец_месяца) - new_stats = {} - for key, value in stats.items(): - current_month_start = max(start_date, date(year=key.year, month=key.month, day=1)) - current_month_end = min(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_stats.get(index): - new_stats[index] += value - else: - new_stats[index] = value - stats = new_stats - + Data = StatisticData(start_date, end_date, form.cleaned_data['email']) + stats = Data.get_statistic() + if Data.errors: + context['errors'] += Data.pop_errors() + else: + stats = Data.use_display(show) + if stats is None: + context['errors'] += Data.pop_errors() + stats = Data.use_interval(interval) + if stats is None: + context['errors'] += Data.pop_errors() context['log_stats'] = stats if not context['errors'] else None if request.method == 'GET': form = StatisticForm() From 3c43e0e38a7d8d235d4dad5f91cd637c8936ce58 Mon Sep 17 00:00:00 2001 From: Iurii Tatishchev Date: Thu, 11 Mar 2021 00:04:05 -0800 Subject: [PATCH 33/54] Add models help text migration --- main/migrations/0009_models_help_text.py | 61 ++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 main/migrations/0009_models_help_text.py diff --git a/main/migrations/0009_models_help_text.py b/main/migrations/0009_models_help_text.py new file mode 100644 index 0000000..4bc87e1 --- /dev/null +++ b/main/migrations/0009_models_help_text.py @@ -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), + ), + ] From c2d6a790f9378a3b5eac569f9d25046e644c2544 Mon Sep 17 00:00:00 2001 From: Iurii Tatishchev Date: Thu, 11 Mar 2021 00:10:44 -0800 Subject: [PATCH 34/54] Add meta with permissions to UserProfile, fix `./manage.py migrate` errors --- main/migrations/0010_userprofile_meta.py | 17 +++++++++++++++++ main/models.py | 5 +++++ main/views.py | 7 ------- 3 files changed, 22 insertions(+), 7 deletions(-) create mode 100644 main/migrations/0010_userprofile_meta.py diff --git a/main/migrations/0010_userprofile_meta.py b/main/migrations/0010_userprofile_meta.py new file mode 100644 index 0000000..28fa435 --- /dev/null +++ b/main/migrations/0010_userprofile_meta.py @@ -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'),)}, + ), + ] diff --git a/main/models.py b/main/models.py index f8f8385..52d7462 100644 --- a/main/models.py +++ b/main/models.py @@ -7,6 +7,11 @@ from django.dispatch import receiver class UserProfile(models.Model): """Модель профиля пользователя""" + class Meta: + permissions = ( + ('has_control_access', 'Can view admin page'), + ) + user = models.OneToOneField(to=User, on_delete=models.CASCADE, help_text='Пользователь') role = models.CharField(default='None', max_length=100, help_text='Код роли пользователя') image = models.URLField(null=True, blank=True, help_text='Аватарка') diff --git a/main/views.py b/main/views.py index 3ec1319..17201c5 100644 --- a/main/views.py +++ b/main/views.py @@ -28,13 +28,6 @@ from access_controller.settings import ZENDESK_ROLES from zenpy.lib.api_objects import User as ZenpyUser -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): """ Отображение и логика работы страницы регистрации пользователя From b825bcdff3596341b28246c7e45db5927348bd91 Mon Sep 17 00:00:00 2001 From: Sokurov Idar Date: Thu, 11 Mar 2021 17:30:55 +0300 Subject: [PATCH 35/54] Update class StatisticData --- main/extra_func.py | 186 +++++++++++++++++++++++++-------------------- main/forms.py | 3 +- main/models.py | 6 +- main/views.py | 25 +++--- 4 files changed, 116 insertions(+), 104 deletions(-) diff --git a/main/extra_func.py b/main/extra_func.py index c47e6a1..167fb09 100644 --- a/main/extra_func.py +++ b/main/extra_func.py @@ -204,80 +204,32 @@ def last_day_of_month(day): class StatisticData: - def __init__(self, start_date, end_date, user_email): - self.errors = list() - self.data = None - self.statistic = dict() + 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._set_data() - self._set_statistic() - - def use_interval(self, interval): - """ - Объединяет ключи и значения в соответствии с интервалом работы - """ - if not self.is_valid_statistic(): - return None - if not (interval in ['days', 'months']): - self.errors += ['Интервал работы должен быть в днях или месяцах'] - return None - stat = {} - if interval == 'months': - # Переделываем ключи под формат, как в прототипе('начало_месяца - конец_месяца') - for key, value in self.get_statistic().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 stat.get(index): - stat[index] += value - else: - stat[index] = value - elif interval == 'days': - stat = self.get_statistic() - return stat - - def use_display(self, display_format): - """ - Приводит данные к формату отображения, возвращает их - """ - if not self.get_statistic(): - return None - if not (display_format in ['hours', 'days']): - self.errors += ['Формат отображения должен быть в часах или днях'] - return None - for key, item in self.statistic.items(): - if display_format == 'hours': - self.statistic[key] = item / 3600 - elif display_format == 'days': - self.statistic[key] = item / 86400 - return self.statistic.copy() - - def pop_errors(self): - """ - Возвращает все текущие ошибки - """ - errors = self.errors.copy() - self.errors.clear() - return errors - - def get_data(self): - """ - Вернуть данные - data - массив объектов RoleChangeLogs, является списком логов пользователя - """ - if self.is_valid_data(): - return self.data + if stat is None: + self._set_statistic() else: - return None + self.statistic = stat def get_statistic(self): """ - Вернуть словарь statistic или None, если были ошибки при создании + Вернуть словарь statistic с применением формата отображения и интеравала работы(если они есть) + None, если были ошибки при создании """ if self.is_valid_statistic(): - return self.statistic.copy() + stat = self.statistic + self._use_display(stat) + self._use_interval(stat) + return stat else: return None @@ -287,22 +239,96 @@ class StatisticData: """ 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 + for key, item in self.statistic.items(): + if self.display == 'hours': + self.statistic[key] = item / 3600 + elif self.display == 'days': + self.statistic[key] = item / 86400 + + def _use_interval(self, stat): + """ + Объединяет ключи и значения в соответствии с интервалом работы + """ + if not self.is_valid_statistic() or not self.interval: + return + 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 # статистика изначально в днях + self.statistic = 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 _set_data(self): """ - Получение списка из лог-ов в диапазоне дат start_date-end_date для пользователя с почтой email + Получение логов в диапазоне дат 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') - self.errors.clear() except User.DoesNotExist: self.errors += ['Пользователь не найден'] @@ -310,43 +336,35 @@ class StatisticData: """ Функция заполняет словарь, в котором ключ - дата, значение - кол-во проработанных в этот день секунд """ - if not self.get_data(): - self.errors += ['Не обнаружены изменения роли в данном промежутке'] - return None - 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 int(first_log.old_role) == ROLES['engineer']: + if first_log.old_role == ROLES['engineer']: self.fill_daterange(self.start_date, first_log.change_time.date()) self.statistic[first_log.change_time.date()] += get_timedelta(first_log).total_seconds() - - # Если инженер закончил работать после диапазона - if int(last_log.new_role) == ROLES['engineer']: + if last_log.new_role == ROLES['engineer']: self.fill_daterange(last_log.change_time.date() + timedelta(days=1), self.end_date + timedelta(days=1)) self.statistic[last_log.change_time.date()] += (timedelta(days=1) - get_timedelta(last_log)).total_seconds() - # Цикл по логам for log_index in range(len(self.data) - 1): - if int(self.data[log_index].new_role) == ROLES['engineer']: + 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() - # Если проработал несколько дней подряд, то заполнить эти дни по 24 часа 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 дне + Заполение диапазона дат значением val + по умолчанию val = кол-во секунд в 1 дне """ + self.statistic.clear() for day in daterange(first, last): self.statistic[day] = val diff --git a/main/forms.py b/main/forms.py index e98b7e1..53eb63a 100644 --- a/main/forms.py +++ b/main/forms.py @@ -2,6 +2,7 @@ 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 @@ -38,7 +39,7 @@ class AdminPageUsers(forms.Form): """ users = forms.ModelMultipleChoiceField( - queryset=UserProfile.objects.filter(role='agent'), + queryset=UserProfile.objects.all(), widget=forms.CheckboxSelectMultiple( attrs={ 'class': 'form-check-input' diff --git a/main/models.py b/main/models.py index 74aa5ba..fa2583d 100644 --- a/main/models.py +++ b/main/models.py @@ -8,7 +8,7 @@ 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='Код роли пользователя') + role = 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='Имя пользователя на нашем сайте') @@ -28,7 +28,7 @@ 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='Старая роль') - new_role = models.TextField(help_text='Присвоенная роль') + old_role = models.IntegerField(help_text='Старая роль') + new_role = models.IntegerField(help_text='Присвоенная роль') change_time = models.DateTimeField(help_text='Дата и время изменения роли') changed_by = models.ForeignKey(to=User, on_delete=models.CASCADE, related_name='changed_by', help_text='Кем была изменена роль') diff --git a/main/views.py b/main/views.py index bb9434f..372adc9 100644 --- a/main/views.py +++ b/main/views.py @@ -243,27 +243,20 @@ def statistic_page(request): return redirect('index') context = { 'pagename': 'страница статистики', - 'errors': [], } if request.method == "POST": form = StatisticForm(request.POST) if form.is_valid(): - start_date, end_date, stats = form.cleaned_data['range_start'], form.cleaned_data['range_end'], dict() + 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'] - if end_date < start_date or end_date > datetime.now().date(): - context['errors'] += ['Конец диапазона должен быть позже начала диапазона и раньше текущего времени'] - else: - Data = StatisticData(start_date, end_date, form.cleaned_data['email']) - stats = Data.get_statistic() - if Data.errors: - context['errors'] += Data.pop_errors() - else: - stats = Data.use_display(show) - if stats is None: - context['errors'] += Data.pop_errors() - stats = Data.use_interval(interval) - if stats is None: - context['errors'] += Data.pop_errors() + 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() From dd08821aea9b06a34b303ee4066763c7a986a512 Mon Sep 17 00:00:00 2001 From: Sokurov Idar Date: Thu, 11 Mar 2021 18:14:57 +0300 Subject: [PATCH 36/54] fix bag --- main/extra_func.py | 33 ++++++----- main/migrations/0005_auto_20210304_0119.py | 67 ---------------------- main/migrations/0011_auto_20210311_1734.py | 28 +++++++++ main/models.py | 10 ++-- main/templates/pages/stat.html | 23 ++++---- main/views.py | 1 + 6 files changed, 68 insertions(+), 94 deletions(-) delete mode 100644 main/migrations/0005_auto_20210304_0119.py create mode 100644 main/migrations/0011_auto_20210311_1734.py diff --git a/main/extra_func.py b/main/extra_func.py index 167fb09..cef57b4 100644 --- a/main/extra_func.py +++ b/main/extra_func.py @@ -185,12 +185,13 @@ def daterange(start_date, end_date) -> list: return dates -def get_timedelta(log) -> timedelta: +def get_timedelta(log,time=None) -> timedelta: """ Возвращает объект класса timedelta, который хранит промежуток времени от начала суток до момента, - который находится в log (объект класса RoleChangeLogs) + который находится в log (объект класса RoleChangeLogs) или в time(datetime.time), если введён """ - time = log.change_time.time() + if time is None: + time = log.change_time.time() time = timedelta(hours=time.hour, minutes=time.minute, seconds=time.second) return time @@ -227,8 +228,8 @@ class StatisticData: """ if self.is_valid_statistic(): stat = self.statistic - self._use_display(stat) - self._use_interval(stat) + stat = self._use_display(stat) + stat = self._use_interval(stat) return stat else: return None @@ -281,19 +282,21 @@ class StatisticData: Приводит данные к формату отображения """ if not self.is_valid_statistic() or not self.display: - return - for key, item in self.statistic.items(): + return stat + new_stat = {} + for key, item in stat.items(): if self.display == 'hours': - self.statistic[key] = item / 3600 + new_stat[key] = item / 3600 elif self.display == 'days': - self.statistic[key] = item / 86400 + new_stat[key] = item / 86400 + return new_stat def _use_interval(self, stat): """ Объединяет ключи и значения в соответствии с интервалом работы """ if not self.is_valid_statistic() or not self.interval: - return + return stat new_stat = {} if self.interval == 'months': # Переделываем ключи под формат('начало_месяца - конец_месяца') @@ -307,7 +310,7 @@ class StatisticData: new_stat[index] = value elif self.interval == 'days': new_stat = stat # статистика изначально в днях - self.statistic = new_stat + return new_stat def check_time(self): """ @@ -341,12 +344,16 @@ class StatisticData: 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.fill_daterange(self.start_date, first_log.change_time.date()) 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)) + self.fill_daterange(last_log.change_time.date() + timedelta(days=1), self.end_date) + self.statistic[self.end_date + timedelta(days=1)] = get_timedelta(None,datetime.now().time()).total_seconds() self.statistic[last_log.change_time.date()] += (timedelta(days=1) - get_timedelta(last_log)).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] @@ -364,7 +371,6 @@ class StatisticData: Заполение диапазона дат значением val по умолчанию val = кол-во секунд в 1 дне """ - self.statistic.clear() for day in daterange(first, last): self.statistic[day] = val @@ -372,4 +378,5 @@ class StatisticData: """ Обнуление всех дней """ + self.statistic.clear() self.fill_daterange(self.start_date, self.end_date + timedelta(days=1), 0) diff --git a/main/migrations/0005_auto_20210304_0119.py b/main/migrations/0005_auto_20210304_0119.py deleted file mode 100644 index 361ec25..0000000 --- a/main/migrations/0005_auto_20210304_0119.py +++ /dev/null @@ -1,67 +0,0 @@ -# Generated by Django 3.1.6 on 2021-03-03 22:19 - -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', '0004_rolechangelogs'), - ] - - operations = [ - migrations.AddField( - model_name='rolechangelogs', - name='old_role', - field=models.TextField(default='agent', help_text='Старая роль'), - preserve_default=False, - ), - 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), - ), - ] diff --git a/main/migrations/0011_auto_20210311_1734.py b/main/migrations/0011_auto_20210311_1734.py new file mode 100644 index 0000000..c228bfc --- /dev/null +++ b/main/migrations/0011_auto_20210311_1734.py @@ -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='Код роли пользователя'), + ), + ] diff --git a/main/models.py b/main/models.py index 72947df..95d38f2 100644 --- a/main/models.py +++ b/main/models.py @@ -31,9 +31,11 @@ def save_user_profile(sender, instance, **kwargs): class RoleChangeLogs(models.Model): """Модель для логирования изменений ролей пользователя""" - user = models.ForeignKey(to=User, on_delete=models.CASCADE, help_text='Пользователь, которому присвоили другую роль') + user = models.ForeignKey(to=User, on_delete=models.CASCADE, + help_text='Пользователь, которому присвоили другую роль') name = models.TextField(help_text='Имя пользователя') - old_role = models.IntegerField(help_text='Старая роль') - new_role = models.IntegerField(help_text='Присвоенная роль') + old_role = models.IntegerField(default=0, help_text='Старая роль') + new_role = models.IntegerField(default=0, help_text='Присвоенная роль') change_time = models.DateTimeField(help_text='Дата и время изменения роли') - changed_by = models.ForeignKey(to=User, on_delete=models.CASCADE, related_name='changed_by', help_text='Кем была изменена роль') + changed_by = models.ForeignKey(to=User, on_delete=models.CASCADE, related_name='changed_by', + help_text='Кем была изменена роль') diff --git a/main/templates/pages/stat.html b/main/templates/pages/stat.html index 69afbde..9279123 100644 --- a/main/templates/pages/stat.html +++ b/main/templates/pages/stat.html @@ -21,26 +21,29 @@ {% endfor %} - {% for key,val in log_stats.items %} -

    {{key}} | {{val}}

    -
    - {% endfor %} -
      {% for error in errors %} -
    • {{error}}
    • +
    • {{error}}
    • {% endfor %}
    {%if form.errors%}
      {% for field, errors in form.errors.items %} - {% for error in errors %} -
    • {{error}}
    • - {% endfor %} + {% for error in errors %} +
    • {{error}}
    • + {% endfor %} {% endfor %}
    {%endif%} - +
      + {% for warning in warnings %} +
    • {{warning}}
    • + {% endfor %} +
    + {% for key,val in log_stats.items %} +

    {{key}} | {{val}}

    +
    + {% endfor %} {% endblock %} diff --git a/main/views.py b/main/views.py index 5a0569f..36365f2 100644 --- a/main/views.py +++ b/main/views.py @@ -236,6 +236,7 @@ def statistic_page(request): return redirect('index') context = { 'pagename': 'страница статистики', + 'errors': list(), } if request.method == "POST": form = StatisticForm(request.POST) From 60e1da2825d904f251a1e1513a413d10905c5bb1 Mon Sep 17 00:00:00 2001 From: Sokurov Idar Date: Thu, 11 Mar 2021 19:17:11 +0300 Subject: [PATCH 37/54] 12-hour working day --- access_controller/settings.py | 3 ++- access_controller/urls.py | 2 -- main/extra_func.py | 8 ++++---- .../django_registration/registration_complete.html | 2 +- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/access_controller/settings.py b/access_controller/settings.py index 96703b6..deadf32 100644 --- a/access_controller/settings.py +++ b/access_controller/settings.py @@ -135,7 +135,6 @@ ACCOUNT_ACTIVATION_DAYS = 7 LOGIN_REDIRECT_URL = '/' LOGOUT_REDIRECT_URL = '/' - # Название_приложения.Название_файла.Название_класса_обработчика AUTHENTICATION_BACKENDS = [ 'access_controller.auth.EmailAuthBackend', @@ -183,3 +182,5 @@ ZENDESK_ROLES = { 'engineer': 360005209000, 'light_agent': 360005208980, } + +ONE_DAY = 12 # Количество часов в 1 рабочем дне diff --git a/access_controller/urls.py b/access_controller/urls.py index 1406e69..da72f11 100644 --- a/access_controller/urls.py +++ b/access_controller/urls.py @@ -33,8 +33,6 @@ urlpatterns = [ path('work/', 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('statistic/', statistic_page, name='statistic') ] diff --git a/main/extra_func.py b/main/extra_func.py index cef57b4..bb7b084 100644 --- a/main/extra_func.py +++ b/main/extra_func.py @@ -5,7 +5,7 @@ from django.contrib.auth.models import User from zenpy import Zenpy from zenpy.lib.exception import APIException -from access_controller.settings import ZENDESK_ROLES as ROLES +from access_controller.settings import ZENDESK_ROLES as ROLES, ONE_DAY from main.models import UserProfile, RoleChangeLogs @@ -73,7 +73,7 @@ class ZendeskAdmin: 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 @@ -156,7 +156,7 @@ def update_profile(user_profile: UserProfile) -> UserProfile: """ user = ZendeskAdmin().get_user(user_profile.user.email) user_profile.name = user.name - user_profile.role = user.role + user_profile.role = 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() @@ -288,7 +288,7 @@ class StatisticData: if self.display == 'hours': new_stat[key] = item / 3600 elif self.display == 'days': - new_stat[key] = item / 86400 + new_stat[key] = item / (ONE_DAY * 3600) return new_stat def _use_interval(self, stat): diff --git a/main/templates/django_registration/registration_complete.html b/main/templates/django_registration/registration_complete.html index bb064bb..1b4aa88 100644 --- a/main/templates/django_registration/registration_complete.html +++ b/main/templates/django_registration/registration_complete.html @@ -10,5 +10,5 @@ {% block content %}
    -

    Регистрация прошла успешно. Войти сейчас

    +

    Регистрация прошла успешно. Войти сейчас

    {% endblock %} From 72b70cc585f3e2e46ff53e520e3a19326949cd91 Mon Sep 17 00:00:00 2001 From: Yuriy Kulakov Date: Thu, 11 Mar 2021 19:29:16 +0300 Subject: [PATCH 38/54] Fixed bug with api response --- main/extra_func.py | 12 +++++++++--- main/serializers.py | 2 +- main/views.py | 13 +++++++++---- static/main/js/control.js | 31 +++++++++++++++++++++++++------ 4 files changed, 44 insertions(+), 14 deletions(-) diff --git a/main/extra_func.py b/main/extra_func.py index 5cfb400..295a677 100644 --- a/main/extra_func.py +++ b/main/extra_func.py @@ -1,9 +1,12 @@ import os +from django.contrib.auth.models import User from zenpy import Zenpy from zenpy.lib.exception import APIException -from main.models import UserProfile, User +from main.models import UserProfile +from django.core.exceptions import ObjectDoesNotExist + from access_controller.settings import ZENDESK_ROLES as ROLES, ZENDESK_ROLES @@ -223,6 +226,9 @@ def update_users_in_model(): """ users = get_users_list() for user in users: - profile = User.objects.get(email=user.email).userprofile - update_user_in_model(profile, user) + try: + profile = User.objects.get(email=user.email).userprofile + update_user_in_model(profile, user) + except ObjectDoesNotExist: + pass return users diff --git a/main/serializers.py b/main/serializers.py index 26d08c2..f72fc86 100644 --- a/main/serializers.py +++ b/main/serializers.py @@ -14,4 +14,4 @@ class ProfileSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = UserProfile - fields = ['user', 'role', 'name'] + fields = ['user', 'id', 'role', 'name'] diff --git a/main/views.py b/main/views.py index 36f82da..e831d34 100644 --- a/main/views.py +++ b/main/views.py @@ -208,7 +208,7 @@ class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin, FormView): users = get_list_or_404( UserProfile, role='agent') context['users'] = users - context['engineers'], context['light_agents'] = count_users(users) + context['engineers'], context['light_agents'] = count_users(get_users_list()) return context # TODO: need to get profile page url @@ -228,7 +228,12 @@ class UsersViewSet(viewsets.ReadOnlyModelViewSet): def list(self, request, *args, **kwargs): users = update_users_in_model() - profiles = UserProfile.objects.filter(role='agent') count = count_users(users) - serializer = self.get_serializer(data=profiles, many=True) - return Response(serializer.data + {'engineers': count[0], 'light_agents': count[1]}) + 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] + }) + diff --git a/static/main/js/control.js b/static/main/js/control.js index 6404741..e645196 100644 --- a/static/main/js/control.js +++ b/static/main/js/control.js @@ -10,7 +10,9 @@ function move_checkboxes() { } } } +move_checkboxes(); +// React class TableRow extends React.Component { render() { return ( @@ -20,7 +22,13 @@ class TableRow extends React.Component { {this.props.user.user.email} {this.props.user.role} - + + + ); } @@ -29,20 +37,31 @@ class TableRow extends React.Component { class TableBody extends React.Component { constructor(props) { super(props); - this.state = { users: [] }; + this.state = { + users: [], + engineers: 0, + light_agents: 0, + }; } get_users() { axios.get("/api/users").then((response) => { - this.setState({ users: response.data }); + this.setState({ + users: response.data.users, + engineers: response.data.engineers, + light_agents: response.data.light_agents, + }); + let elements = document.querySelectorAll(".info-quantity-value"); + console.log(elements) + elements[0].innerHTML = this.state.engineers; + elements[1].innerHTML = this.state.light_agents; }); } componentDidMount() { this.interval = setInterval(() => { this.get_users(); - move_checkboxes(); - }, 1000); + }, 10000); } componentWillUnmount() { @@ -56,5 +75,5 @@ class TableBody extends React.Component { } } -move_checkboxes(); ReactDOM.render(, document.getElementById("table")); + From 9068febd302f3f2bf7d0f3a3f73f7b8e405eeb5f Mon Sep 17 00:00:00 2001 From: Sokurov Idar Date: Thu, 11 Mar 2021 19:55:09 +0300 Subject: [PATCH 39/54] bugfix --- main/extra_func.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/main/extra_func.py b/main/extra_func.py index bb7b084..b76eb55 100644 --- a/main/extra_func.py +++ b/main/extra_func.py @@ -185,7 +185,7 @@ def daterange(start_date, end_date) -> list: return dates -def get_timedelta(log,time=None) -> timedelta: +def get_timedelta(log, time=None) -> timedelta: """ Возвращает объект класса timedelta, который хранит промежуток времени от начала суток до момента, который находится в log (объект класса RoleChangeLogs) или в time(datetime.time), если введён @@ -350,9 +350,10 @@ class StatisticData: 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) - self.statistic[self.end_date + timedelta(days=1)] = get_timedelta(None,datetime.now().time()).total_seconds() + self.fill_daterange(last_log.change_time.date() + timedelta(days=1), self.end_date + timedelta(days=1)) self.statistic[last_log.change_time.date()] += (timedelta(days=1) - get_timedelta(last_log)).total_seconds() + if self.end_date == datetime.now().date(): + self.statistic[self.end_date] = get_timedelta(None, datetime.now().time()).total_seconds() for log_index in range(len(self.data) - 1): if self.data[log_index].new_role == ROLES['engineer']: From 674df0e66b1f5139c9058596bbf7fbc4bbe0206f Mon Sep 17 00:00:00 2001 From: Andrew Smirnov Date: Thu, 11 Mar 2021 20:28:04 +0300 Subject: [PATCH 40/54] Add model for tracking unassigned tickets --- main/migrations/0012_auto_20210311_2027.py | 29 ++++++++++++++++++++++ main/models.py | 13 +++++++++- 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 main/migrations/0012_auto_20210311_2027.py diff --git a/main/migrations/0012_auto_20210311_2027.py b/main/migrations/0012_auto_20210311_2027.py new file mode 100644 index 0000000..113e51e --- /dev/null +++ b/main/migrations/0012_auto_20210311_2027.py @@ -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)), + ], + ), + ] diff --git a/main/models.py b/main/models.py index 95d38f2..4811e8d 100644 --- a/main/models.py +++ b/main/models.py @@ -33,9 +33,20 @@ class RoleChangeLogs(models.Model): """Модель для логирования изменений ролей пользователя""" user = models.ForeignKey(to=User, on_delete=models.CASCADE, help_text='Пользователь, которому присвоили другую роль') - name = models.TextField(help_text='Имя пользователя') old_role = models.IntegerField(default=0, help_text='Старая роль') new_role = models.IntegerField(default=0, help_text='Присвоенная роль') change_time = models.DateTimeField(help_text='Дата и время изменения роли') changed_by = models.ForeignKey(to=User, on_delete=models.CASCADE, related_name='changed_by', help_text='Кем была изменена роль') + + +class UnassignedTicketStatus(models.IntegerChoices): + UNASSIGNED = 0, 'Снят с пользователя, перенесён в буферную группу' + RESTORED = 1, 'Авторство восстановлено' + NOT_FOUND = 2, 'Пока нас не было, тикет испарился из буферной группы. Дополнительные действия не требуются' + + +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) From d2e76fdbd96e6b84980d1b11de85fd923cb43129 Mon Sep 17 00:00:00 2001 From: Andrew Smirnov Date: Thu, 11 Mar 2021 20:54:56 +0300 Subject: [PATCH 41/54] Update model, add documentation todos, add sampla code for logging. --- main/extra_func.py | 33 +++++++++++++++++++--- main/migrations/0013_auto_20210311_2040.py | 18 ++++++++++++ main/models.py | 6 ++-- main/views.py | 7 +++-- 4 files changed, 55 insertions(+), 9 deletions(-) create mode 100644 main/migrations/0013_auto_20210311_2040.py diff --git a/main/extra_func.py b/main/extra_func.py index b76eb55..d2f1810 100644 --- a/main/extra_func.py +++ b/main/extra_func.py @@ -6,7 +6,7 @@ from zenpy import Zenpy from zenpy.lib.exception import APIException from access_controller.settings import ZENDESK_ROLES as ROLES, ONE_DAY -from main.models import UserProfile, RoleChangeLogs +from main.models import UserProfile, RoleChangeLogs, UnassignedTicket, UnassignedTicketStatus class ZendeskAdmin: @@ -128,16 +128,41 @@ def update_role(user_profile: UserProfile, role: str) -> UserProfile: def make_engineer(user_profile: UserProfile) -> UserProfile: """ - Функция **make_engineer** устанавливапет пользователю роль инженера. + Функция **make_engineer** устанавливает пользователю роль инженера. """ + update_role(user_profile, ROLES['engineer']) -def make_light_agent(user_profile: UserProfile) -> UserProfile: +def make_light_agent(user_profile: UserProfile, who_changes: User) -> UserProfile: """ - Функция **make_light_agent** устанавливапет пользователю роль легкого агента. + Функция **make_light_agent** устанавливапет пользователю роль легкого агента. + + .. todo:: + Решить проблему с ошибкой при выполнении этой функции из-за неотвязанных тикетов. А именно: + + - найти все тикеты, ответственным которых является снимаемый аккаунт + - для всех этих тикетов - перенести ответственность на буферную группу. + - [PARTIALY DONE] создать записи о снятых тикетах и их прошлом авторстве. Если тикет уже был закрыт - выставить в логе CLOSED. Иначе UNASSIGNED + - [DONE] после этого снять права c инженера + - [DONE] создать запись в логе о снятии прав инженера """ + + # tickets = [] + # # TODO: set ticket fields correct + # for ticket in tickets: + # UnassignedTicket.create( + # assignee=user_profile.user, + # ticket_id=ticket.number, + # status=UnassignedTicketStatus.UNASSIGNED if ticket.status=='opened' else UnassignedTicketStatus.CLOSED + # ) update_role(user_profile, ROLES['light_agent']) + RoleChangeLogs.create( + user=user_profile.user, + old_role=ROLES['engineer'], + new_role=ROLES['light_agent'], + changed_by=who_changes + ) def get_users_list() -> list: diff --git a/main/migrations/0013_auto_20210311_2040.py b/main/migrations/0013_auto_20210311_2040.py new file mode 100644 index 0000000..5648813 --- /dev/null +++ b/main/migrations/0013_auto_20210311_2040.py @@ -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), + ), + ] diff --git a/main/models.py b/main/models.py index 4811e8d..087784c 100644 --- a/main/models.py +++ b/main/models.py @@ -2,6 +2,7 @@ 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): @@ -12,7 +13,7 @@ class UserProfile(models.Model): ('has_control_access', 'Can view admin page'), ) - user = models.OneToOneField(to=User, on_delete=models.CASCADE, help_text='Пользователь') + user = models.OneToOneField(to=User, on_delete=models.CASCADE, help_text='Пользователь', related_name='user') role = 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='Имя пользователя на нашем сайте') @@ -35,7 +36,7 @@ class RoleChangeLogs(models.Model): help_text='Пользователь, которому присвоили другую роль') old_role = models.IntegerField(default=0, help_text='Старая роль') new_role = models.IntegerField(default=0, help_text='Присвоенная роль') - change_time = models.DateTimeField(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='Кем была изменена роль') @@ -44,6 +45,7 @@ class UnassignedTicketStatus(models.IntegerChoices): UNASSIGNED = 0, 'Снят с пользователя, перенесён в буферную группу' RESTORED = 1, 'Авторство восстановлено' NOT_FOUND = 2, 'Пока нас не было, тикет испарился из буферной группы. Дополнительные действия не требуются' + CLOSED = 3, 'Тикет уже был закрыт. Дополнительные действия не требуются' class UnassignedTicket(models.Model): diff --git a/main/views.py b/main/views.py index 36365f2..4c596ad 100644 --- a/main/views.py +++ b/main/views.py @@ -190,9 +190,10 @@ class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin, FormView): 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] + def make_light_agents(self, users): + for user in users: + make_light_agent(user, self.request.user) + @staticmethod def count_users(users) -> tuple: From 5665282ce5619076a844e048e8ad32f2455c317c Mon Sep 17 00:00:00 2001 From: Artyom Kravchenko Date: Fri, 12 Mar 2021 12:34:45 +0300 Subject: [PATCH 42/54] fixed UserProfile model --- main/extra_func.py | 3 ++- main/migrations/0012_auto_20210312_1225.py | 23 ++++++++++++++++++++++ main/models.py | 3 ++- 3 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 main/migrations/0012_auto_20210312_1225.py diff --git a/main/extra_func.py b/main/extra_func.py index b76eb55..ada4a88 100644 --- a/main/extra_func.py +++ b/main/extra_func.py @@ -156,7 +156,8 @@ def update_profile(user_profile: UserProfile) -> UserProfile: """ user = ZendeskAdmin().get_user(user_profile.user.email) user_profile.name = user.name - user_profile.role = user.custom_role_id if user.custom_role_id else 0 + 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() diff --git a/main/migrations/0012_auto_20210312_1225.py b/main/migrations/0012_auto_20210312_1225.py new file mode 100644 index 0000000..6d33580 --- /dev/null +++ b/main/migrations/0012_auto_20210312_1225.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.6 on 2021-03-12 09:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0011_auto_20210311_1734'), + ] + + operations = [ + migrations.AddField( + model_name='userprofile', + name='custom_role_id', + field=models.IntegerField(default=0, help_text='Код роли пользователя'), + ), + migrations.AlterField( + model_name='userprofile', + name='role', + field=models.CharField(default='None', help_text='Глобальное имя роли пользователя', max_length=100), + ), + ] diff --git a/main/models.py b/main/models.py index 95d38f2..112e29e 100644 --- a/main/models.py +++ b/main/models.py @@ -13,7 +13,8 @@ class UserProfile(models.Model): ) user = models.OneToOneField(to=User, on_delete=models.CASCADE, help_text='Пользователь') - role = models.IntegerField(default=0, 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='Имя пользователя на нашем сайте') From abe44fec5f2cd85bc7e7966e7759b474322164a0 Mon Sep 17 00:00:00 2001 From: Yuriy Kulakov Date: Fri, 12 Mar 2021 13:01:41 +0300 Subject: [PATCH 43/54] Refactored some functions in AdminPageView --- main/views.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/main/views.py b/main/views.py index e831d34..3b96d92 100644 --- a/main/views.py +++ b/main/views.py @@ -184,10 +184,11 @@ class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin, FormView): """ Функция установки ролей пользователям """ + 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 @@ -202,8 +203,6 @@ class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin, FormView): """ Функция формирования контента страницы администратора (с проверкой прав доступа) """ - if self.request.user.userprofile.role != 'admin': - raise PermissionDenied context = super().get_context_data(**kwargs) users = get_list_or_404( UserProfile, role='agent') From bd500207a33b0d8b138e70ce5541e18cc52eaf1d Mon Sep 17 00:00:00 2001 From: Artyom Kravchenko Date: Fri, 12 Mar 2021 13:58:27 +0300 Subject: [PATCH 44/54] Fixed AdminPageUSer form --- main/forms.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/main/forms.py b/main/forms.py index 53eb63a..a8c2ec1 100644 --- a/main/forms.py +++ b/main/forms.py @@ -39,10 +39,11 @@ class AdminPageUsers(forms.Form): """ users = forms.ModelMultipleChoiceField( - queryset=UserProfile.objects.all(), + queryset=UserProfile.objects.filter(role='agent'), widget=forms.CheckboxSelectMultiple( attrs={ - 'class': 'form-check-input' + 'class': 'form-check-input', + } ), label='' From f6948d016de2c7899787d16ea636466ca7491de1 Mon Sep 17 00:00:00 2001 From: Artyom Kravchenko Date: Fri, 12 Mar 2021 14:39:51 +0300 Subject: [PATCH 45/54] add some design to menu template(logo and color) --- main/templates/base/menu.html | 6 +++--- static/main/img/logo_real.png | Bin 0 -> 1499 bytes 2 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 static/main/img/logo_real.png diff --git a/main/templates/base/menu.html b/main/templates/base/menu.html index a389341..92aaec1 100644 --- a/main/templates/base/menu.html +++ b/main/templates/base/menu.html @@ -3,10 +3,10 @@ -