This commit is contained in:
Dmitriy Andreev 2021-02-26 21:54:22 +03:00
commit bcc3bdcafa
31 changed files with 1009 additions and 220 deletions

View File

@ -51,6 +51,15 @@ MIDDLEWARE = [
ROOT_URLCONF = 'access_controller.urls' ROOT_URLCONF = 'access_controller.urls'
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.mail.ru'
EMAIL_PORT = 2525
EMAIL_USE_TLS = True
EMAIL_HOST_USER = 'djgr.02@mail.ru'
EMAIL_HOST_PASSWORD = 'djangogroup02'
SERVER_EMAIL = EMAIL_HOST_USER
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER
TEMPLATES = [ TEMPLATES = [
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', 'BACKEND': 'django.template.backends.django.DjangoTemplates',
@ -116,13 +125,55 @@ USE_TZ = True
# https://docs.djangoproject.com/en/3.1/howto/static-files/ # https://docs.djangoproject.com/en/3.1/howto/static-files/
STATIC_URL = '/static/' STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'static') STATIC_ROOT = os.path.join(BASE_DIR, 'staticroot')
STATICFILES_DIRS = [ STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'staticfiles'), os.path.join(BASE_DIR, 'static'),
] ]
MEDIA_ROOT = BASE_DIR / 'media' ACCOUNT_ACTIVATION_DAYS = 7
MEDIA_URL = '/media/'
LOGIN_REDIRECT_URL = '/' LOGIN_REDIRECT_URL = '/'
LOGOUT_REDIRECT_URL = '/' LOGOUT_REDIRECT_URL = '/'
# Logging system
# https://docs.djangoproject.com/en/3.1/topics/logging/
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}',
'style': '{',
},
'simple': {
'format': '{levelname} {message}',
'style': '{',
},
},
'handlers': {
'console': {
'level': 'INFO',
'class': 'logging.StreamHandler',
'formatter': 'simple'
},
'mail_admins': {
'level': 'ERROR',
'class': 'django.utils.log.AdminEmailHandler',
}
},
'loggers': {
'django': {
'handlers': ['console'],
'propagate': True,
},
'main.index': {
'handlers': ['console'],
'level': 'INFO',
}
}
}
ZENDESK_ROLES = {
'engineer': 360005209000,
'light_agent': 360005208980,
}

View File

@ -13,23 +13,43 @@ Including another URLconf
1. Import the include() function: from django.urls import include, path 1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
""" """
from django.conf.urls.static import static
from django.contrib import admin from django.contrib import admin
from django.urls import path, include
from django.contrib.auth.views import LoginView from django.contrib.auth.views import LoginView
from django.contrib.auth import views as auth_views
from django.urls import path, include from django.urls import path, include
from access_controller import settings from main.views import main_page, profile_page, CustomRegistrationView, AdminPageView
from main.views import *
urlpatterns = [ urlpatterns = [
path('admin/', admin.site.urls, name='admin'), path('admin/', admin.site.urls, name='admin'),
path('', main_page), path('', main_page, name='index'),
path('register/', CustomRegistrationView.as_view(), name='registration'), path('accounts/profile/', profile_page, name='profile'),
# path('', include('django_registration.backends.one_step.urls')), path('accounts/register/', CustomRegistrationView.as_view(), name='registration'),
path('profile/', profile_page, name='profile'), path('accounts/login/', LoginView.as_view(extra_context={}), name='login'), # TODO add extra context
path('accounts/login/', LoginView.as_view(extra_context={})), # TODO add extra context path('accounts/', include('django.contrib.auth.urls')),
path('accounts/', include('django.contrib.auth.urls')) path('accounts/', include('django_registration.backends.activation.urls')),
path('accounts/login/', include('django.contrib.auth.urls')),
path('control/', AdminPageView.as_view(), name='control')
] ]
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) urlpatterns += [
path(
'password_reset/',
auth_views.PasswordResetView.as_view(),
name='password_reset'
),
path(
'password-reset/done/',
auth_views.PasswordResetDoneView.as_view(),
name='password_reset_done'
),
path(
'reset/<uidb64>/<token>/',
auth_views.PasswordResetConfirmView.as_view(),
name='password_reset_confirm'
),
path(
'reset/done/',
auth_views.PasswordResetCompleteView.as_view(),
name='password_reset_complete'
),
]

View File

@ -1,9 +1,34 @@
***** Документация разработчика
TODOs =========================
*****
******
Models
******
.. automodule:: main.models
:members:
******
Forms
******
.. automodule:: main.forms
:members:
***************
Extra Functions Extra Functions
--------------- ***************
.. automodule:: main.extra_func .. automodule:: main.extra_func
:members: :members:
*****
Views
*****
.. automodule:: main.views
:members:

View File

@ -6,13 +6,14 @@
Welcome to ZenDesk Access Controller's documentation! Welcome to ZenDesk Access Controller's documentation!
===================================================== =====================================================
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 2
:caption: Contents: :caption: Contents:
code.rst overview
todo.rst code
todo
Indices and tables Indices and tables

18
docs/source/overview.rst Normal file
View File

@ -0,0 +1,18 @@
Документация пользователя
=========================
**Управление правами доступа**
**ZenDesk Access Controller** - Web-приложение, для выдачи прав пользователям системы по запросу самого пользователя.
Например, из 12 человек 3 сейчас работают с правами администратора, по окончании рабочей смены они сдают свои права (освобождают места) и другие пользователи могут запросить эти права в свое пользование.
Оставшиеся 9 человек получают права легкого агента - без прав редактирования, а только чтение.
**Интерфейс пользователя:**
.. |copy| unicode:: 0xA9 .. Школа программистов S101, группа 2. 2021гю

View File

@ -1,5 +1,11 @@
Что необходимо доделать?
=======================
***** *****
TODOs TODOs
***** *****
.. todolist:: .. todolist::

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

BIN
layouts/work/work.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

38
main/apiauth.py Normal file
View File

@ -0,0 +1,38 @@
import os
from zenpy import Zenpy
from zenpy.lib.api_objects import User as ZenpyUser
def api_auth():
credentials = {
'subdomain': 'ngenix1612197338'
}
email = os.getenv('ACCESS_CONTROLLER_API_EMAIL')
token = os.getenv('ACCESS_CONTROLLER_API_TOKEN')
password = os.getenv('ACCESS_CONTROLLER_API_PASSWORD')
if email is None:
raise ValueError('access_controller email not in env')
credentials['email'] = os.getenv('ACCESS_CONTROLLER_API_EMAIL')
# prefer token, use password if token not provided
if token:
credentials['token'] = token
elif password:
credentials['password'] = password
else:
raise ValueError('access_controller token or password not in env')
zenpy_client = Zenpy(**credentials)
zenpy_user: ZenpyUser = zenpy_client.users.search(email).values[0]
user = {
'id': zenpy_user.id,
'name': zenpy_user.name, # Zendesk doesn't have separate first and last name fields
'email': zenpy_user.email,
'role': zenpy_user.role, # str like 'admin' or 'agent', not id
'photo': zenpy_user.photo['content_url'] if zenpy_user.photo is not None else None,
}
return user

View File

@ -5,57 +5,151 @@ from zenpy.lib.exception import APIException
from main.models import UserProfile from main.models import UserProfile
from access_controller.settings import ZENDESK_ROLES as ROLES
# Дополнительные функции
def set_and_get_name(user_profile: UserProfile): class ZendeskAdmin:
""" """
Функция устанавливает поле :class:`username` текущим именем в Zendesk Класс **ZendeskAdmin** существует, чтобы в каждой фунциии отдельно не проверять аккаунт администратора
.. TODO:: :param credentials: Полномочия (первым указывается учетная запись организации в Zendesk)
Переделать с получением данных через API :type credentials: :class:`list of dictionaries`
:param email: Email администратора, указанный в env
:param UP: Объект профиля пользователя :type email: :class:`email`
:type UP: :class:`main.models.UserProfile` :param token: Токен администратора (формируется в Zendesk, указывается в env)
:return: Имя пользователя :type token: :class:`str`
:rtype: :class:`str` :param password: Пароль администратора, указанный в env
:type password: :class:`str`
""" """
return user_profile.user.username
credentials = {
'subdomain': 'ngenix1612197338'
}
email = os.getenv('ACCESS_CONTROLLER_API_EMAIL')
token = os.getenv('ACCESS_CONTROLLER_API_TOKEN')
password = os.getenv('ACCESS_CONTROLLER_API_PASSWORD')
def set_and_get_email(user_profile: UserProfile): # TODO: Переделать с получением данных через API def __init__(self):
self.create_admin()
def check_user(self, email: str) -> bool:
""" """
Функция устанавливает поле :class:`user.email` текущей почтой в Zendesk Функция **check_user** осуществляет проверку существования пользователя в Zendesk
:param UP: Объект профиля пользователя :param email: Электронная почта пользователя
:type UP: :class:`main.models.UserProfile` :type email: :class:`email`
:return: Почта пользователя :return: True, если существует, иначе False
:rtype: :class:`str` :rtype: :class:`bool`
""" """
return user_profile.user.email return True if self.admin.search(email, type='user') else False
def get_user_name(self, email: str) -> str:
def set_and_get_role(user_profile: UserProfile): # TODO: Переделать с получением данных через API
""" """
Функция устанавливает поле :class:`role` текущей ролью в Zendesk Функция **get_user_name** возвращает имя пользователя
:param UP: Объект профиля пользователя :param user_name: Имя пользователя
:type UP: :class:`main.models.UserProfile` :type user_name: :class:`str`
:return: Роль пользователя
:rtype: :class:`str`
""" """
return user_profile.role user = self.admin.users.search(email).values[0]
return user.name
def get_user_role(self, email: str) -> str:
def load_and_get_image(user_profile: UserProfile): # TODO: Переделать с получением изображения через API
""" """
Функция загружает и устанавливает изображение в поле :class:`image` Функция **get_user_role** возвращает роль пользователя
:param UP: Объект профиля пользователя :param user_role: Роль пользователя
:type UP: :class:`main.models.UserProfile` :type user_role: :class:`str`
:return: Название изображения
:rtype: :class:`str`
""" """
return user_profile.image.name user = self.admin.users.search(email).values[0]
return user.role
def get_user_id(self, email: str) -> str:
"""
Функция **get_user_id** возвращает id пользователя
:param user_id: ID пользователя
:type user_id: :class:`str`
"""
user = self.admin.users.search(email).values[0]
return user.id
def get_user_image(self, email: str) -> str:
"""
Функция **get_user_image** возвращает аватар пользователя
:param user_image: Аватар пользователя
:type user_image: :class:`img`
"""
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:
return self.admin.users.search(email).values[0]
def get_user_org(self, email: str) -> str:
user = self.admin.users.search(email).values[0]
return user.organization.name
def create_admin(self) -> None:
"""
Функция **Create_admin()** создает администратора, проверяя наличие вводимых данных в env.
:param credentials: В список полномочий администратора вносятся email, token, password из env
:raise: :class:`ValueError`: исключение, вызываемое если email не введен в env
:raise: :class:`APIException`: исключение, вызываемое если пользователя с таким email не существует в Zendesk
"""
if self.email is None:
raise ValueError('access_controller email not in env')
self.credentials['email'] = os.getenv('ACCESS_CONTROLLER_API_EMAIL')
if self.token:
self.credentials['token'] = self.token
elif self.password:
self.credentials['password'] = self.password
else:
raise ValueError('access_controller token or password not in env')
self.admin = Zenpy(**self.credentials)
try:
self.admin.search(self.email, type='user')
except APIException:
raise ValueError('invalid access_controller`s login data')
def update_role(user_profile, 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):
update_role(user_profile, ROLES['engineer'])
def make_light_agent(user_profile):
update_role(user_profile, ROLES['light_agent'])
def get_users_list():
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):
"""
Функция обновляет профиль пользователя в соотвтетствии с текущим в Zendesk
:param user_profile: Объект профиля пользователя
:type user_profile: :class:`main.models.UserProfile`
"""
user = ZendeskAdmin().get_user(user_profile.user.email)
user_profile.name = user.name
user_profile.role = user.role
user_profile.image = user.photo['content_url'] if user.photo else None
user_profile.save()
def check_user_exist(email: str) -> bool: def check_user_exist(email: str) -> bool:
@ -67,16 +161,19 @@ def check_user_exist(email: str) -> bool:
:return: True, если существует, иначе False :return: True, если существует, иначе False
:rtype: :class:`bool` :rtype: :class:`bool`
""" """
admin_creds = { return ZendeskAdmin().check_user(email)
'email': os.environ.get('Admin_email'),
'subdomain': 'ngenix1612197338',
'token': os.environ.get('Oauth_token'), def get_user_organization(email: str) -> str:
} """
admin = Zenpy(**admin_creds) Функция возвращает организацию пользователя
zenpy_user = admin.search(email, type='user')
if zenpy_user: :param email: Электронная почта пользователя
return True :type email: :class:`str`
return False :return: Название организации
:rtype: :class:`str`
"""
return ZendeskAdmin().get_user_org(email)
def check_user_auth(email: str, password: str) -> bool: def check_user_auth(email: str, password: str) -> bool:
@ -88,15 +185,15 @@ def check_user_auth(email: str, password: str) -> bool:
:param password: Пароль пользователя :param password: Пароль пользователя
:type password: :class:`str` :type password: :class:`str`
:return: True, если входные данные верны, иначе False :return: True, если входные данные верны, иначе False
:raise :class:`APIException`: исключение, вызываемое если пользователь не аутентифицирован :raise: :class:`APIException`: исключение, вызываемое если пользователь не аутентифицирован
:rtype: :class:`bool` :rtype: :class:`bool`
""" """
try:
creds = { creds = {
'email': email, 'email': email,
'subdomain': 'ngenix1612197338',
'password': password, 'password': password,
'subdomain': 'ngenix1612197338',
} }
try:
user = Zenpy(**creds) user = Zenpy(**creds)
user.search(email, type='user') user.search(email, type='user')
except APIException: except APIException:

View File

@ -1,34 +1,40 @@
from django import forms from django import forms
from django_registration.forms import RegistrationFormUniqueEmail from django_registration.forms import RegistrationFormUniqueEmail
from main.models import UserProfile
class CustomRegistrationForm(RegistrationFormUniqueEmail): class CustomRegistrationForm(RegistrationFormUniqueEmail):
""" """
Форма для регистрации :class:`django_registration.forms.RegistrationFormUniqueEmail` Форма для регистрации :class:`django_registration.forms.RegistrationFormUniqueEmail`
с полем для ввода пароля от Zendesk аккаунта и с добавлением bootstrap-класса 'form-control' для всех полей с добавлением bootstrap-класса 'form-control'
:param password_zen: Поле для ввода пароля от Zendesk :param password_zen: Поле для ввода пароля от Zendesk
:type password_zen: :class:`django.forms.CharField` :type password_zen: :class:`django.forms.CharField`
""" """
password_zen = forms.CharField(
required=True,
label="Пароль от Zendesk аккаунта",
strip=False,
widget=forms.PasswordInput(attrs={
'class': 'form-control'
})
)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
for visible in self.visible_fields(): for visible in self.visible_fields():
if visible.field.widget.attrs.get('class', False): if visible.field.widget.attrs.get('class', False):
print(visible.field.widget.attrs['class'].find('form-control'))
if visible.field.widget.attrs['class'].find('form-control') < 0: if visible.field.widget.attrs['class'].find('form-control') < 0:
visible.field.widget.attrs['class'] += 'form-control' visible.field.widget.attrs['class'] += 'form-control'
else: else:
visible.field.widget.attrs['class'] = 'form-control' visible.field.widget.attrs['class'] = 'form-control'
if visible.html_name !='email':
visible.field.required = False
class Meta(RegistrationFormUniqueEmail.Meta): class Meta(RegistrationFormUniqueEmail.Meta):
fields = RegistrationFormUniqueEmail.Meta.fields fields = RegistrationFormUniqueEmail.Meta.fields
fields.insert(2, 'password_zen')
class AdminPageUsers(forms.Form):
users = forms.ModelMultipleChoiceField(
queryset=UserProfile.objects.filter(role='agent'),
widget=forms.CheckboxSelectMultiple(
attrs={
'class': 'form-check-input'
}
),
label=''
)

View File

@ -0,0 +1,23 @@
# Generated by Django 3.1.6 on 2021-02-16 19:22
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0002_userprofile_name'),
]
operations = [
migrations.AlterField(
model_name='userprofile',
name='image',
field=models.URLField(blank=True, null=True),
),
migrations.AlterField(
model_name='userprofile',
name='role',
field=models.CharField(default='None', max_length=100),
),
]

View File

@ -0,0 +1,27 @@
# Generated by Django 3.1.6 on 2021-02-17 17:25
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', '0003_auto_20210216_2222'),
]
operations = [
migrations.CreateModel(
name='RoleChangeLogs',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.TextField()),
('new_role', models.TextField()),
('change_time', models.DateTimeField()),
('changed_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='changed_by', to=settings.AUTH_USER_MODEL)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@ -1,11 +1,54 @@
import os
from django.contrib.auth.models import User
from django.db import models 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
class UserProfile(models.Model): 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) user = models.OneToOneField(to=User, on_delete=models.CASCADE)
role = models.IntegerField() role = models.CharField(default='None', max_length=100)
image = models.ImageField(upload_to='user_avatars') image = models.URLField(null=True, blank=True)
name = models.CharField(default='None', max_length=100) name = models.CharField(default='None', max_length=100)
@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
if created:
UserProfile.objects.create(user=instance)
@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
instance.userprofile.save()
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')

View File

@ -2,14 +2,13 @@
<html lang="ru" class="h-100"> <html lang="ru" class="h-100">
{% load static %} {% load static %}
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{% block title %}{% endblock %}</title> <title>{% block title %}{% endblock %}</title>
<link rel="stylesheet" <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/5.0.0-alpha1/css/bootstrap.min.css"
href="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css" integrity="sha384-r4NyP46KrjDleawBgD5tp8Y7UzmLA05oM1iAEQ17CSuDqnUK2+k9luXQOfXJCJ4I" crossorigin="anonymous">
integrity="sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2"
crossorigin="anonymous">
<style> <style>
.bd-placeholder-img { .bd-placeholder-img {
@ -25,9 +24,11 @@
font-size: 3.5rem; font-size: 3.5rem;
} }
} }
</style> </style>
{% block extra_css %}{% endblock %} {% block extra_css %}{% endblock %}
</head> </head>
<body class="d-flex flex-column h-100"> <body class="d-flex flex-column h-100">
{% include 'base/menu.html' %} {% include 'base/menu.html' %}
@ -48,11 +49,10 @@
</div> </div>
</footer> </footer>
<script <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/js/bootstrap.bundle.min.js"
src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/js/bootstrap.bundle.min.js" integrity="sha384-ygbV9kiqUc6oa4msXn9868pTtWMgiQaeYH7/t7LECLbyPA2x65Kgf80OJFdroafW" crossorigin="anonymous">
integrity="sha384-ygbV9kiqUc6oa4msXn9868pTtWMgiQaeYH7/t7LECLbyPA2x65Kgf80OJFdroafW" </script>
crossorigin="anonymous"
></script>
</body> </body>
</html> </html>

View File

@ -4,24 +4,20 @@
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<nav class="navbar navbar-light" style="background-color: #00FF00;"> <nav class="navbar navbar-light" style="background-color: #00FF00;">
<a class="navbar-brand" href="#"> <a class="navbar-brand" href="{% url 'index' %}">
<img src="{% static 'main/img/logo.png' %}" width="30" height="30" class="d-inline-block align-top" alt="" loading="lazy"> <img src="{% static 'main/img/logo.png' %}" width="30" height="30" class="d-inline-block align-top" alt="" loading="lazy">
Access Controller Access Controller
</a> </a>
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
<div class="btn-group" role="group" aria-label="Basic example"> <div class="btn-group" role="group" aria-label="Basic example">
<a class="btn btn-secondary" href="/accounts/logout">Выйти</a> <a class="btn btn-secondary" href="{% url 'profile' %}">Профиль</a>
<a class="btn btn-secondary" href="">Профиль</a> <a class="btn btn-secondary" href="{% url 'logout' %}">Выйти</a>
</div> </div>
{% else %} {% else %}
<div class="btn-group" role="group" aria-label="Basic example"> <div class="btn-group" role="group" aria-label="Basic example">
<a class="btn btn-secondary" href="/accounts/login">Войти</a> <a class="btn btn-secondary" href="/accounts/login">Войти</a>
<a class="btn btn-secondary" href="/accounts/register">Зарегистрироваться</a> <a class="btn btn-secondary" href="/accounts/register">Зарегистрироваться</a>
</div> </div>
{% endif %} {% endif %}
</nav> </nav>
</header> </header>

View File

@ -11,5 +11,5 @@
{% block content %} {% block content %}
<br> <br>
<h4> Нет пользователя с указаным адресом электронной почты, либо был введён неверный пароль</h4> <h4> Нет пользователя с указаным адресом электронной почты.</h4>
{% endblock %} {% endblock %}

View File

@ -11,14 +11,12 @@
{% block content %} {% block content %}
<form method="post" action=""> <form method="post" action="">
{% csrf_token %} {% csrf_token %}
{% for field in form %} {{ form.email.label_tag }}
{{ field.label_tag }} {{ form.email }}
{{ field }}
<br> <br>
{% if field.errors %} {% if form.email.errors %}
<span>{{ field.errors }}</span> <span>{{ form.email.errors }}</span>
{% endif %} {% endif %}
{% endfor %}
<input type="submit" value="Зарегистрироваться" class="clearfix"> <input type="submit" value="Зарегистрироваться" class="clearfix">
</form> </form>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,109 @@
{% extends 'base/base.html' %}
{% load static %}
{% block title %}Управление{%endblock %}
{% block heading %}Управление{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'main/css/work.css' %}"/>
{% endblock %}
{% block content %}
<div class="container-md">
<div class="new-section">
<p class="row page-description">Основная информация о странице</p>
</div>
<form method="post">
{% csrf_token %}
<div class="row justify-content-center new-section">
<div style="display: none">
{% for field in form.users %}
{{ field.tag }}
{% endfor %}
</div>
<div class="col-10">
<h6 class="table-title">Список сотрудников</h6>
<table class="light-table">
<thead>
<th>ID</th>
<th>Email</th>
<th>Role</th>
<th>Name(link to profile)</th>
<th>Checked</th>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>{{ user.id }}</td>
<td>{{ user.user.email }}</td>
<td>{{ user.role }}</td>
<td><a href="#">{{ user.name }}</a></td>
<td class="checkbox_field"></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="row justify-content-center new-section">
<div class="col-5">
<div class="info">
<div class="info-row">
<div class="info-target">Инженеров:</div>
<div class="info-quantity">
<div class="status-circle-small light-green"></div>
<span class="info-quantity-value">{{ engineers }}</span>
</div>
</div>
<div class="info-row">
<div class="info-target">Легких агентов:</div>
<div class="info-quantity">
<div class="status-circle-small light-yellow"></div>
<span class="info-quantity-value">{{ light_agents }}</span>
</div>
</div>
</div>
</div>
<div class="col-5">
<button type="submit" name="engineer" class="request-acess-button default-button">
Назначить выбранных на роль инженера
</button>
<button type="submit" name="light_agent" class="hand-over-acess-button default-button">
Назначить выбранных на роль легкого агента
</button>
</div>
</div>
</form>
</div>
<script>
"use strict";
let checkboxes = document.getElementsByName("users");
let fields = document.querySelectorAll(".checkbox_field");
if (checkboxes.length == fields.length) {
for (let i = 0; i < fields.length; ++i) {
let el = checkboxes[i].cloneNode(true);
fields[i].appendChild(el);
}
}
</script>
{% endblock %}

View File

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

View File

@ -0,0 +1,74 @@
{% extends 'base/base.html' %}
{% load static %}
{% block title %}{{ pagename }}{% endblock %}
{% block heading %}Управление{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'main/css/work.css' %}">
{% endblock %}
{% block content %}
<div class="container-md">
<div class="new-section">
<p class="row page-description">Основаная информация о странице</p>
</div>
<div class="row justify-content-center new-section">
<div class="col-10">
<h6 class="table-title">Список сотрудников с правами инженера</h6>
<table class="light-table">
<thead>
<th>ID</th>
<th>email</th>
<th>Expiration Date</th>
<th>Name(link to profile)</th>
</thead>
<tbody>
<tr>
<td>1</td>
<td>big_boss123@example.ru</td>
<td>19:30 18.02.21</td>
<td><a href="#">Иван Иванов</a></td>
</tr>
<tr>
<td>2</td>
<td>gachi_cool456@example.ru</td>
<td>21:00 18.02.21</td>
<td><a href="#">Пётр Петров</a></td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="row justify-content-center new-section">
<div class="col-5">
<div class="info">
<div class="info-row">
<div class="info-target">инженеров: </div>
<div class="info-quantity">
<div class="status-circle-small light-green"></div>
<span class="info-quantity-value">13</span>
</div>
</div>
<div class="info-row">
<div class="info-target">легких агентов:</div>
<div class="info-quantity">
<div class="status-circle-small light-yellow"></div>
<span class="info-quantity-value">22</span>
</div>
</div>
</div>
</div>
<div class="col-5">
<button class="request-acess-button default-button">Получить права инженера</button>
<button class="hand-over-acess-button default-button">Сдать права инженера</button>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,10 +1,9 @@
{% extends 'base/base.html' %} {% extends 'base/base.html' %}
{% block title %}
Авторизация {% block title %}Авторизация{% endblock %}
{% endblock %}
{% block heading %} {% block heading %}Авторизация{% endblock %}
Авторизация
{% endblock %}
{% block content %} {% block content %}
<div class="container"> <div class="container">
<div class="card mx-auto" style="width: 40rem"> <div class="card mx-auto" style="width: 40rem">
@ -31,7 +30,7 @@
{% endif %} {% endif %}
<div class="text-center"> <div class="text-center">
<button type="submit" class="btn btn-primary">Войти</button> <button type="submit" class="btn btn-primary">Войти</button>
<a href="" class="btn btn-link" style="display: block;">Забыли пароль?</a> <a href="password_reset" class="btn btn-link" style="display: block;">Забыли пароль?</a>
</div> </div>
</form> </form>
</div> </div>

View File

@ -0,0 +1,13 @@
{% extends "base/base.html" %}
{% block title %}
{{ pagename }}
{% endblock %}
{% block heading %}
Восстановление пароля
{% endblock %}
{% block content %}
<p>Ваш новый пароль был установлен. Вы можете <a href="{% url 'login' %}">войти</a> сейчас</p>
{% endblock %}

View File

@ -0,0 +1,23 @@
{% extends "base/base.html" %}
{% block title %}
{{ pagename }}
{% endblock %}
{% block heading %}
Восстановление пароля
{% endblock %}
{% block content %}
{% if validlink %}
<p>Пожалуйста, введите пароль дважды:</p>
<form action="." method="post">
{{ form.as_p }}
{% csrf_token %}
<p><input class="btn btn-success" type="submit" value="Сменить пароль"/></p>
</form>
{% else %}
<p>Неверная ссылка восстановления пароля, возможно она уже была использована.
Пожалуйста, запросите новый сброс пароля</p>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,14 @@
{% extends "base/base.html" %}
{% block title %}
{{ pagename }}
{% endblock %}
{% block heading %}
Восстановление пароля
{% endblock %}
{% block content %}
<p>Мы отправили вам на почту инструкцию по восстановлению</p>
<p>Если вы не получили сообщение, убедитесь что верно ввели адрес электронной почты.</p>
{% endblock %}

View File

@ -0,0 +1,18 @@
{% extends "base/base.html" %}
{% block title %}
{{ pagename }}
{% endblock %}
{% block heading %}
Забыли пароль?
{% endblock %}
{% block content %}
<p>Введте свой e-mail адрес для восстановления пароля.</p>
<form action="." method="post">
{{ form.as_p }}
<p><input class="btn btn-success" type="submit" value="Отпрваить e-mail"></p>
{% csrf_token %}
</form>
{% endblock %}

View File

@ -1,17 +1,27 @@
from django.shortcuts import render from django.contrib.auth.decorators import login_required
from django.contrib.auth.forms import PasswordResetForm
from django.contrib.auth.models import User
from django.contrib.auth.tokens import default_token_generator
from django.shortcuts import render, get_list_or_404
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.views.generic import FormView
from django_registration.backends.one_step.views import RegistrationView
from main.extra_func import set_and_get_name, set_and_get_email, load_and_get_image, set_and_get_role, check_user_exist, \ from access_controller.settings import EMAIL_HOST_USER
check_user_auth from main.extra_func import check_user_exist, update_profile, get_user_organization, \
from main.models import UserProfile make_engineer, make_light_agent, get_users_list
from django.contrib.auth.models import User from django.contrib.auth.models import User
from main.forms import CustomRegistrationForm from main.models import UserProfile
from main.forms import CustomRegistrationForm, AdminPageUsers
from django_registration.views import RegistrationView from django_registration.views import RegistrationView
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import PermissionDenied
from zenpy import Zenpy import logging
from access_controller.settings import ZENDESK_ROLES
class CustomRegistrationView(RegistrationView): class CustomRegistrationView(RegistrationView):
@ -24,22 +34,30 @@ class CustomRegistrationView(RegistrationView):
is_allowed = True is_allowed = True
def register(self, form): def register(self, form):
if check_user_exist(form.data['email']) and check_user_auth(form.data['email'], form.data['password_zen']): self.is_allowed = True
if check_user_exist(form.data['email']) and get_user_organization(form.data['email']) == 'SYSTEM':
forms = PasswordResetForm(self.request.POST)
if forms.is_valid():
opts = {
'use_https': self.request.is_secure(),
'token_generator': default_token_generator,
'from_email': EMAIL_HOST_USER,
'email_template_name': 'registration/password_reset_email.html',
'subject_template_name': 'registration/password_reset_subject.txt',
'request': self.request,
'html_email_template_name': None,
'extra_email_context': None,
}
user = User.objects.create_user( user = User.objects.create_user(
username=form.data['username'], username=form.data['email'],
email=form.data['email'], email=form.data['email'],
password=form.data['password1'] password=User.objects.make_random_password(length=50)
) )
profile = UserProfile( forms.save(**opts)
image='None.png', update_profile(user.userprofile)
user=user, return user
role=0, else:
) raise ValueError('Непредвиденная ошибка')
set_and_get_name(profile)
set_and_get_email(profile)
set_and_get_role(profile)
load_and_get_image(profile)
profile.save()
else: else:
self.is_allowed = False self.is_allowed = False
@ -49,7 +67,7 @@ class CustomRegistrationView(RegistrationView):
Используется самой django-registration Используется самой django-registration
""" """
if self.is_allowed: if self.is_allowed:
return reverse_lazy('django_registration_complete') return reverse_lazy('password_reset_done')
else: else:
return reverse_lazy('django_registration_disallowed') return reverse_lazy('django_registration_disallowed')
@ -59,25 +77,64 @@ def profile_page(request):
""" """
Отображение страницы профиля Отображение страницы профиля
:param request: объект с деталями запроса :param request: объект с деталями запроса
:type request: :class:`django.http.HttpResponse` :type request: :class:`django.http.HttpResponse`
:return: объект ответа сервера с HTML-кодом внутри :return: объект ответа сервера с HTML-кодом внутри
""" """
if request.user.is_authenticated: user_profile = request.user.userprofile
# UP = UserProfile.objects.get(user=request.user) update_profile(user_profile)
UP = UserProfile.objects.get(user=request.user)
# else: # TODO: Убрать после появления регистрации и авторизации, добавить login_required()
# UP = UserProfile.objects.get(user=1)
context = { context = {
'name': set_and_get_name(UP), 'email': user_profile.user.email,
'email': set_and_get_email(UP), 'name': user_profile.name,
'role': set_and_get_role(UP), 'role': user_profile.role,
'image_name': load_and_get_image(UP), 'image_url': user_profile.image,
'pagename': 'Страница профиля' 'pagename': 'Страница профиля'
} }
return render(request, 'pages/profile.html', context) return render(request, 'pages/profile.html', context)
def main_page(request): def main_page(request):
logger = logging.getLogger('main.index')
logger.info('Index page opened')
return render(request, 'pages/index.html') return render(request, 'pages/index.html')
class AdminPageView(FormView, LoginRequiredMixin):
template_name = 'pages/adm_ruleset.html'
form_class = AdminPageUsers
success_url = '/control/'
def form_valid(self, form):
if 'engineer' in self.request.POST:
self.make_engineers(form.cleaned_data['users'])
elif 'light_agent' in self.request.POST:
self.make_light_agents(form.cleaned_data['users'])
return super().form_valid(form)
@staticmethod
def make_engineers(users):
[make_engineer(user) for user in users]
@staticmethod
def make_light_agents(users):
[make_light_agent(user) for user in users]
@staticmethod
def count_users(users): # TODO: this func counts users from all zendesk instead of just from a model
engineers, light_agents = 0, 0
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):
if self.request.user.userprofile.role != 'admin':
raise PermissionDenied
context = super().get_context_data(**kwargs)
context['users'] = get_list_or_404(
UserProfile, role='agent')
context['engineers'], context['light_agents'] = self.count_users(get_users_list())
return context # TODO: need to get profile page url

View File

@ -2,7 +2,7 @@
Django==3.1.6 Django==3.1.6
Pillow==8.1.0 Pillow==8.1.0
zenpy~=2.0.24 zenpy~=2.0.24
django-registration==3.1.1 django_registration==3.1.1
# Documentation # Documentation

128
static/main/css/work.css Normal file
View File

@ -0,0 +1,128 @@
/* .all {
display: flex;
justify-content: start;
} */
/* .menu {
position: absolute;
top: 0;
left: 0;
display: inline-flex;
width: 150px;
height: 100vh;
background: #45729C;
} */
.form-check-input {
border-radius: 0px;
background-image: url("../img/check.png");
width: 30px;
height: 30px;
background-size: 20px auto;
}
.page-title {
margin: auto;
display: block;
text-align: center;
}
.page-description {
display: block;
margin: auto;
font-size: 1rem;
text-align: center;
}
.new-section {
margin-top: 50px;
}
.table-title {
text-align: center;
margin: auto;
font-size: 1.2rem;
font-weight: bolder;
}
.container {
width: calc(100% - 150px);
/* margin-left: calc(150px);
margin-right: calc((100vw - 150px) / 2); */
}
.light-table {
font-weight: bold;
margin: auto;
margin-top: 10px;
font-size: 1.2rem;
width: 100%;
}
.light-table th {
background: #515A63;
color: white;
padding: 10px;
}
.light-table td {
padding: 5px;
padding-left: 10px;
}
.light-table tr:nth-child(odd) {
background: #93A2AF;
}
.info-row {
margin: 5px auto;
height: 53px;
}
.info-target {
width: 200px;
display: inline-block;
font-size: 1.2rem;
}
.info-quantity {
max-width: 200px;
display: inline-block;
}
.light-green {
background: #83DC87;
}
.light-yellow {
background: #F3EB3C;
}
.info-quantity-value {
font-weight: bold;
}
.status-circle-small {
margin: auto;
display: inline-block;
border-radius: 4px;
width: 8px;
height: 8px;
}
.request-acess-button,
.hand-over-acess-button {
font-size: 1.2rem;
width: 100%;
}
.default-button {
border: none;
display: block;
margin: 5px auto;
text-align: center;
padding: 10px;
background: #3B91D4;
color: white;
}

BIN
static/main/img/check.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 B

View File

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB