Merge branch 'develop' into feature/statistic/backend

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

View File

@ -16,6 +16,7 @@ Including another URLconf
from django.contrib import admin from django.contrib import admin
from django.contrib.auth import views as auth_views from django.contrib.auth import views as auth_views
from django.urls import path, include from django.urls import path, include
from main.views import work_page, work_hand_over, work_become_engineer, AdminPageView
from main.views import main_page, profile_page, CustomRegistrationView, CustomLoginView from main.views import main_page, profile_page, CustomRegistrationView, CustomLoginView
from main.views import work_page, work_hand_over, work_become_engineer, \ from main.views import work_page, work_hand_over, work_become_engineer, \

View File

@ -14,21 +14,26 @@ import os
import sys import sys
import importlib import importlib
import inspect import inspect
sys.path.insert(0, os.path.abspath('../../')) sys.path.insert(0, os.path.abspath('../../'))
import django import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'access_controller.settings') os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'access_controller.settings')
os.environ.setdefault('DJANGO_CONFIGURATION', 'Dev') os.environ.setdefault('DJANGO_CONFIGURATION', 'Dev')
# Fix Django's FileFields # Fix Django's FileFields
from django.db.models.fields.files import FileDescriptor 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 # Stop Django from executing DB queries
from django.db.models.query import QuerySet from django.db.models.query import QuerySet
QuerySet.__repr__ = lambda self: self.__class__.__name__ QuerySet.__repr__ = lambda self: self.__class__.__name__
try: try:
import enchant # NoQA import enchant # NoQA
@ -46,7 +51,8 @@ author = 'SHP S101, group 2'
# The full version, including alpha/beta/rc tags # The full version, including alpha/beta/rc tags
release = 'v0.01' 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 --------------------------------------------------- # -- General configuration ---------------------------------------------------
@ -121,7 +127,6 @@ def setup(app):
app.connect('autodoc-process-docstring', process_modules) app.connect('autodoc-process-docstring', process_modules)
# Add any Sphinx extension module names here, as strings. They can be # Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones. # ones.
@ -132,7 +137,6 @@ extensions = [
'sphinx.ext.viewcode', 'sphinx.ext.viewcode',
'sphinx_rtd_theme', 'sphinx_rtd_theme',
'sphinx.ext.graphviz', 'sphinx.ext.graphviz',
'sphinx.ext.napoleon',
'sphinx.ext.inheritance_diagram', 'sphinx.ext.inheritance_diagram',
'sphinx_autodoc_typehints' 'sphinx_autodoc_typehints'
@ -141,7 +145,6 @@ extensions = [
if enchant is not None: if enchant is not None:
extensions.append('sphinxcontrib.spelling') extensions.append('sphinxcontrib.spelling')
# Add any paths that contain templates here, relative to this directory. # Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates'] templates_path = ['_templates']
@ -157,7 +160,6 @@ language = 'ru'
# This pattern also affects html_static_path and html_extra_path. # This pattern also affects html_static_path and html_extra_path.
exclude_patterns = [] exclude_patterns = []
# -- Options for HTML output ------------------------------------------------- # -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for # 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". # so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static'] html_static_path = ['_static']
# -- Extension configuration ------------------------------------------------- # -- Extension configuration -------------------------------------------------
# -- Options for intersphinx extension --------------------------------------- # -- Options for intersphinx extension ---------------------------------------
@ -179,14 +180,9 @@ html_static_path = ['_static']
intersphinx_mapping = { intersphinx_mapping = {
'https://docs.python.org/3/': None, 'https://docs.python.org/3/': None,
'django': ( 'django': (
'https://docs.djangoproject.com/en/dev/', 'https://docs.djangoproject.com/en/dev/',
'https://docs.djangoproject.com/en/dev/_objects/' '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'] autodoc_default_flags = ['members']

View File

@ -75,7 +75,10 @@ class ZendeskAdmin:
def get_user(self, email: str) -> str: 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] return self.admin.users.search(email).values[0]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,10 +5,7 @@ from django.dispatch import receiver
class UserProfile(models.Model): class UserProfile(models.Model):
""" """Модель профиля пользователя"""
Модель профиля пользователя
"""
user = models.OneToOneField(to=User, on_delete=models.CASCADE, help_text='Пользователь') user = models.OneToOneField(to=User, on_delete=models.CASCADE, help_text='Пользователь')
role = models.CharField(default='None', max_length=100, 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): 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='Имя пользователя') name = models.TextField(help_text='Имя пользователя')
old_role = models.TextField(help_text='Старая роль') old_role = models.TextField(help_text='Старая роль')

View File

@ -11,6 +11,9 @@
{% 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="{% url 'profile' %}">Профиль</a> <a class="btn btn-secondary" href="{% url 'profile' %}">Профиль</a>
{% if perms.main.has_control_access %}
<a class="btn btn-secondary" href="{% url 'control' %}">Управление</a>
{% endif %}
<a class="btn btn-secondary" href="{% url 'logout' %}">Выйти</a> <a class="btn btn-secondary" href="{% url 'logout' %}">Выйти</a>
</div> </div>
{% else %} {% else %}

View File

@ -2,7 +2,7 @@
{% load static %} {% load static %}
{% block title %}Управление{%endblock %} {% block title %}Управление{% endblock %}
{% block heading %}Управление{% endblock %} {% block heading %}Управление{% endblock %}
@ -16,19 +16,24 @@
<p class="row page-description">Основная информация о странице</p> <p class="row page-description">Основная информация о странице</p>
</div> </div>
{% block form %}
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
<div class="row justify-content-center new-section"> <div class="row justify-content-center new-section">
{% block hidden_form %}
<div style="display: none"> <div style="display: none">
{% for field in form.users %} {% for field in form.users %}
{{ field.tag }} {{ field.tag }}
{% endfor %} {% endfor %}
</div> </div>
{% endblock %}
<div class="col-10"> <div class="col-10">
<h6 class="table-title">Список сотрудников</h6> <h6 class="table-title">Список сотрудников</h6>
{% block table %}
<table class="light-table"> <table class="light-table">
<thead> <thead>
@ -52,10 +57,12 @@
</tbody> </tbody>
</table> </table>
{% endblock%}
</div> </div>
</div> </div>
{% block count %}
<div class="row justify-content-center new-section"> <div class="row justify-content-center new-section">
<div class="col-5"> <div class="col-5">
<div class="info"> <div class="info">
@ -91,19 +98,11 @@
</div> </div>
</div> </div>
{% endblock %}
</form> </form>
{% endblock %}
</div> </div>
<script> <script src="{% static 'main/js/control.js'%}"></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 %} {% endblock %}

View File

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

View File

@ -1,19 +1,30 @@
import logging import logging
import os import os
from django.contrib.auth.decorators import login_required
from django.contrib.auth.forms import PasswordResetForm
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import User
from django.contrib.auth.tokens import default_token_generator from django.contrib.auth.tokens import default_token_generator
from django.contrib.auth.forms import PasswordResetForm
from django.contrib.auth.views import LoginView from django.contrib.auth.views import LoginView
from django.core.exceptions import PermissionDenied from django.contrib.contenttypes.models import ContentType
from django.http import HttpResponseRedirect from django.core.handlers.wsgi import WSGIRequest
from django.shortcuts import get_list_or_404, redirect, reverse, render from django.http import HttpResponseRedirect, HttpResponse
from django.urls import reverse_lazy from django.shortcuts import render, get_list_or_404, redirect
from django.urls import reverse_lazy, reverse
from django.views.generic import FormView from django.views.generic import FormView
from django_registration.views import RegistrationView
from zenpy import Zenpy from zenpy import Zenpy
from access_controller.settings import EMAIL_HOST_USER
from main.extra_func import check_user_exist, update_profile, get_user_organization, \
make_engineer, make_light_agent, get_users_list
from django.contrib.auth.models import User, Permission
from main.models import UserProfile
from main.forms import CustomRegistrationForm, AdminPageUsers, CustomAuthenticationForm
from django_registration.views import RegistrationView
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.core.exceptions import PermissionDenied
from access_controller.settings import ZENDESK_ROLES
from zenpy.lib.api_objects import User as ZenpyUser from zenpy.lib.api_objects import User as ZenpyUser
from access_controller.settings import EMAIL_HOST_USER, ZENDESK_ROLES from access_controller.settings import EMAIL_HOST_USER, ZENDESK_ROLES
@ -22,17 +33,27 @@ from main.extra_func import check_user_exist, update_profile, get_user_organizat
from main.forms import AdminPageUsers, CustomRegistrationForm, CustomAuthenticationForm, StatisticForm from main.forms import AdminPageUsers, CustomRegistrationForm, CustomAuthenticationForm, StatisticForm
from .models import UserProfile, RoleChangeLogs from .models import UserProfile, RoleChangeLogs
content_type_temp = ContentType.objects.get_for_model(UserProfile)
permission_temp, created = Permission.objects.get_or_create(
codename='has_control_access',
content_type=content_type_temp,
)
class CustomRegistrationView(RegistrationView): class CustomRegistrationView(RegistrationView):
""" """
Отображение и логика работы страницы регистрации пользователя Отображение и логика работы страницы регистрации пользователя
1. Ввод email пользователя, указанный на Zendesk
2. В случае если пользователь с данным паролем зарегистрирован на Zendesk и относится к определенной организации, происходит сброс ссылки с установлением пароля на указанный email
3. Создается пользователь class User, а также его профиль
""" """
form_class = CustomRegistrationForm form_class = CustomRegistrationForm
template_name = 'django_registration/registration_form.html' template_name = 'django_registration/registration_form.html'
success_url = reverse_lazy('django_registration_complete') success_url = reverse_lazy('django_registration_complete')
is_allowed = True is_allowed = True
def register(self, form): def register(self, form: CustomRegistrationForm) -> User:
self.is_allowed = True self.is_allowed = True
if check_user_exist(form.data['email']) and get_user_organization(form.data['email']) == 'SYSTEM': if check_user_exist(form.data['email']) and get_user_organization(form.data['email']) == 'SYSTEM':
forms = PasswordResetForm(self.request.POST) forms = PasswordResetForm(self.request.POST)
@ -54,13 +75,27 @@ class CustomRegistrationView(RegistrationView):
) )
forms.save(**opts) forms.save(**opts)
update_profile(user.userprofile) update_profile(user.userprofile)
self.set_permission(user)
return user return user
else: else:
raise ValueError('Непредвиденная ошибка') raise ValueError('Непредвиденная ошибка')
else: else:
self.is_allowed = False self.is_allowed = False
def get_success_url(self, user=None): @staticmethod
def set_permission(user) -> None:
"""
Дает разрешение на просмотр страница администратора, если пользователь имеет роль admin
"""
if user.userprofile.role == 'admin':
content_type = ContentType.objects.get_for_model(UserProfile)
permission = Permission.objects.get(
codename='has_control_access',
content_type=content_type,
)
user.user_permissions.add(permission)
def get_success_url(self, user: User = None) -> success_url:
""" """
Возвращает url-адрес страницы, куда нужно перейти после успешной/неуспешной регистрации Возвращает url-адрес страницы, куда нужно перейти после успешной/неуспешной регистрации
Используется самой django-registration Используется самой django-registration
@ -72,23 +107,14 @@ class CustomRegistrationView(RegistrationView):
@login_required() @login_required()
def profile_page(request): def profile_page(request: WSGIRequest) -> HttpResponse:
""" """
Отображение страницы профиля Отображение страницы профиля
:param request: объект с деталями запроса
:type request: :class:`django.http.HttpResponse`
:return: объект ответа сервера с HTML-кодом внутри
""" """
user_profile = request.user.userprofile user_profile: UserProfile = request.user.userprofile
update_profile(user_profile) update_profile(user_profile)
context = { context = {
'email': user_profile.user.email, 'profile': user_profile,
'name': user_profile.name,
'role': user_profile.role,
'id': user_profile.id,
'image_url': user_profile.image,
'pagename': 'Страница профиля' 'pagename': 'Страница профиля'
} }
return render(request, 'pages/profile.html', context) return render(request, 'pages/profile.html', context)
@ -140,17 +166,24 @@ def work_become_engineer(request):
def main_page(request): def main_page(request):
"""
Отображение логгирования на главной странице
"""
logger = logging.getLogger('main.index') logger = logging.getLogger('main.index')
logger.info('Index page opened') logger.info('Index page opened')
return render(request, 'pages/index.html') return render(request, 'pages/index.html')
class AdminPageView(FormView, LoginRequiredMixin): class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin, FormView):
permission_required = 'main.has_control_access'
template_name = 'pages/adm_ruleset.html' template_name = 'pages/adm_ruleset.html'
form_class = AdminPageUsers form_class = AdminPageUsers
success_url = '/control/' success_url = '/control/'
def form_valid(self, form): def form_valid(self, form: AdminPageUsers) -> AdminPageUsers:
"""
Функция установки ролей пользователям
"""
if 'engineer' in self.request.POST: if 'engineer' in self.request.POST:
self.make_engineers(form.cleaned_data['users']) self.make_engineers(form.cleaned_data['users'])
elif 'light_agent' in self.request.POST: elif 'light_agent' in self.request.POST:
@ -166,7 +199,13 @@ class AdminPageView(FormView, LoginRequiredMixin):
[make_light_agent(user) for user in users] [make_light_agent(user) for user in users]
@staticmethod @staticmethod
def count_users(users): # TODO: this func counts users from all zendesk instead of just from a model def count_users(users) -> tuple:
"""
Функция подсчета количества сотрудников с ролями engineer и light_a
.. todo::
this func counts users from all zendesk instead of just from a model:
"""
engineers, light_agents = 0, 0 engineers, light_agents = 0, 0
for user in users: for user in users:
if user.custom_role_id == ZENDESK_ROLES['engineer']: if user.custom_role_id == ZENDESK_ROLES['engineer']:
@ -175,7 +214,10 @@ class AdminPageView(FormView, LoginRequiredMixin):
light_agents += 1 light_agents += 1
return engineers, light_agents return engineers, light_agents
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs) -> dict:
"""
Функция формирования контента страницы администратора (с проверкой прав доступа)
"""
if self.request.user.userprofile.role != 'admin': if self.request.user.userprofile.role != 'admin':
raise PermissionDenied raise PermissionDenied
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)

View File

@ -4,8 +4,9 @@ Pillow==8.1.0
zenpy~=2.0.24 zenpy~=2.0.24
django_registration==3.1.1 django_registration==3.1.1
# Documentation # Documentation
Sphinx==3.4.3 Sphinx==3.4.3
sphinx-rtd-theme==0.5.1 sphinx-rtd-theme==0.5.1
sphinx-autodoc-typehints==1.11.1 sphinx-autodoc-typehints==1.11.1
pyenchant==3.2.0
sphinxcontrib-spelling==7.1.0

View File

@ -0,0 +1,9 @@
"use strict";
let checkboxes = document.getElementsByName("users");
let fields = document.querySelectorAll(".checkbox_field");
if (checkboxes.length == fields.length) {
for (let i = 0; i < fields.length; ++i) {
let el = checkboxes[i].cloneNode(true);
fields[i].appendChild(el);
}
}