Merge branch 'feature/pylint' into 'develop'

Feature/pylint

See merge request 2020-2021/online/s101/group-02/access_controller!75
This commit is contained in:
Кравченко Артем 2021-05-25 14:03:00 +00:00
commit 6ec670a82f
32 changed files with 1690 additions and 206 deletions

View File

@ -3,8 +3,8 @@ ACTRL_DEBUG=1
ACTRL_SECRET_KEY="v1i_fb\$_jf2#1v_lcsbu&eon4u-os0^px=s^iycegdycqy&5)6"
ACTRL_HOST="actrl.example.com"
ACTRL_EMAIL_HOST="smtp.mail.ru"
ACTRL_EMAIL_PORT=2525
ACTRL_EMAIL_HOST="smtp.gmail.com"
ACTRL_EMAIL_PORT=587
ACTRL_EMAIL_TLS=1
ACTRL_EMAIL_HOST_USER="djgr.02@mail.ru"
ACTRL_EMAIL_HOST_PASSWORD="djangogroup02"
@ -20,6 +20,6 @@ LICENSE_NO=3
SHIFTH=12
ACTRL_ZENDESK_SUBDOMAIN="ngenix1612197338"
ACTRL_API_EMAIL="email@example.com"
ACTRL_API_TOKEN="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
ACTRL_API_PASSWORD=""
ACTRL_API_EMAIL="stepanenko_olga@mail.ru"
ACTRL_API_TOKEN="X1x4QeNa4xRdul2rTIKhac98AsXMwd5bOGAyZOtU"

627
.pylintrc Normal file
View File

@ -0,0 +1,627 @@
[MASTER]
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code.
extension-pkg-allow-list=
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code. (This is an alternative name to extension-pkg-allow-list
# for backward compatibility.)
extension-pkg-whitelist=
# Specify a score threshold to be exceeded before program exits with error.
fail-under=10.0
# Files or directories to be skipped. They should be base names, not paths.
ignore=CVS, manage.py
# Files or directories matching the regex patterns are skipped. The regex
# matches against base names, not paths.
ignore-patterns=
# Python code to execute, usually for sys.path manipulation such as
#pygtk.require().
init-hook="from pylint.config import find_pylintrc; import os, sys; sys.path.append(os.path.dirname(find_pylintrc()))"
# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
# number of processors available to use.
jobs=1
# Control the amount of potential inferred values when inferring a single
# object. This can help the performance when dealing with large functions or
# complex, nested conditions.
limit-inference-results=100
# List of plugins (as comma separated values of python module names) to load,
# usually to register additional checkers.
load-plugins=pylint_django
django-settings-module=access_controller_new.access_controller.settings
# Pickle collected data for later comparisons.
persistent=yes
# When enabled, pylint would attempt to guess common misconfiguration and emit
# user-friendly hints instead of false-positive error messages.
suggestion-mode=yes
# Allow loading of arbitrary C extensions. Extensions are imported into the
# active Python interpreter and may run arbitrary code.
unsafe-load-any-extension=no
[MESSAGES CONTROL]
# Only show warnings with the listed confidence levels. Leave empty to show
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED.
confidence=
# Disable the message, report, category or checker with the given id(s). You
# can either give multiple identifiers separated by comma (,) or put this
# option multiple times (only on the command line, not in the configuration
# file where it should appear only once). You can also use "--disable=all" to
# disable everything first and then reenable specific checks. For example, if
# you want to run only the similarities checker, you can use "--disable=all
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use "--disable=all --enable=classes
# --disable=W".
disable=print-statement,
parameter-unpacking,
unpacking-in-except,
old-raise-syntax,
backtick,
long-suffix,
old-ne-operator,
old-octal-literal,
import-star-module-level,
non-ascii-bytes-literal,
raw-checker-failed,
bad-inline-option,
locally-disabled,
file-ignored,
suppressed-message,
useless-suppression,
deprecated-pragma,
use-symbolic-message-instead,
apply-builtin,
basestring-builtin,
buffer-builtin,
cmp-builtin,
coerce-builtin,
execfile-builtin,
file-builtin,
long-builtin,
raw_input-builtin,
reduce-builtin,
standarderror-builtin,
unicode-builtin,
xrange-builtin,
coerce-method,
delslice-method,
getslice-method,
setslice-method,
no-absolute-import,
old-division,
dict-iter-method,
dict-view-method,
next-method-called,
metaclass-assignment,
indexing-exception,
raising-string,
reload-builtin,
oct-method,
hex-method,
nonzero-method,
cmp-method,
input-builtin,
round-builtin,
intern-builtin,
unichr-builtin,
map-builtin-not-iterating,
zip-builtin-not-iterating,
range-builtin-not-iterating,
filter-builtin-not-iterating,
using-cmp-argument,
eq-without-hash,
div-method,
idiv-method,
rdiv-method,
exception-message-attribute,
invalid-str-codec,
sys-max-int,
bad-python3-import,
deprecated-string-function,
deprecated-str-translate-call,
deprecated-itertools-function,
deprecated-types-field,
next-method-defined,
dict-items-not-iterating,
dict-keys-not-iterating,
dict-values-not-iterating,
deprecated-operator-function,
deprecated-urllib-function,
xreadlines-attribute,
deprecated-sys-function,
exception-escape,
comprehension-escape,
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
# multiple time (only on the command line, not in the configuration file where
# it should appear only once). See also the "--disable" option for examples.
enable=c-extension-no-member
[REPORTS]
# Python expression which should return a score less than or equal to 10. You
# have access to the variables 'error', 'warning', 'refactor', and 'convention'
# which contain the number of messages in each category, as well as 'statement'
# which is the total number of statements analyzed. This score is used by the
# global evaluation report (RP0004).
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
# Template used to display messages. This is a python new-style format string
# used to format the message information. See doc for all details.
#msg-template=
# Set the output format. Available formats are text, parseable, colorized, json
# and msvs (visual studio). You can also give a reporter class, e.g.
# mypackage.mymodule.MyReporterClass.
output-format=text
# Tells whether to display a full report or only the messages.
reports=no
# Activate the evaluation score.
score=yes
[REFACTORING]
# Maximum number of nested blocks for function / method body
max-nested-blocks=5
# Complete name of functions that never returns. When checking for
# inconsistent-return-statements if a never returning function is called then
# it will be considered as an explicit return statement and no message will be
# printed.
never-returning-functions=sys.exit,argparse.parse_error
[BASIC]
# Naming style matching correct argument names.
argument-naming-style=snake_case
# Regular expression matching correct argument names. Overrides argument-
# naming-style.
#argument-rgx=
# Naming style matching correct attribute names.
attr-naming-style=snake_case
# Regular expression matching correct attribute names. Overrides attr-naming-
# style.
#attr-rgx=
# Bad variable names which should always be refused, separated by a comma.
bad-names=foo,
bar,
baz,
toto,
tutu,
tata
# Bad variable names regexes, separated by a comma. If names match any regex,
# they will always be refused
bad-names-rgxs=
# Naming style matching correct class attribute names.
class-attribute-naming-style=any
# Regular expression matching correct class attribute names. Overrides class-
# attribute-naming-style.
#class-attribute-rgx=
# Naming style matching correct class constant names.
class-const-naming-style=UPPER_CASE
# Regular expression matching correct class constant names. Overrides class-
# const-naming-style.
#class-const-rgx=
# Naming style matching correct class names.
class-naming-style=PascalCase
# Regular expression matching correct class names. Overrides class-naming-
# style.
#class-rgx=
# Naming style matching correct constant names.
const-naming-style=UPPER_CASE
# Regular expression matching correct constant names. Overrides const-naming-
# style.
#const-rgx=
# Minimum line length for functions/classes that require docstrings, shorter
# ones are exempt.
docstring-min-length=-1
# Naming style matching correct function names.
function-naming-style=snake_case
# Regular expression matching correct function names. Overrides function-
# naming-style.
#function-rgx=
# Good variable names which should always be accepted, separated by a comma.
good-names=i,
id,
e,
n,
j,
k,
ex,
Run,
_
# Good variable names regexes, separated by a comma. If names match any regex,
# they will always be accepted
good-names-rgxs=
# Include a hint for the correct naming format with invalid-name.
include-naming-hint=no
# Naming style matching correct inline iteration names.
inlinevar-naming-style=any
# Regular expression matching correct inline iteration names. Overrides
# inlinevar-naming-style.
#inlinevar-rgx=
# Naming style matching correct method names.
method-naming-style=snake_case
# Regular expression matching correct method names. Overrides method-naming-
# style.
#method-rgx=
# Naming style matching correct module names.
module-naming-style=snake_case
# Regular expression matching correct module names. Overrides module-naming-
# style.
#module-rgx=
# Colon-delimited sets of names that determine each other's naming style when
# the name regexes allow several styles.
name-group=
# Regular expression which should only match function or class names that do
# not require a docstring.
no-docstring-rgx=^_
# List of decorators that produce properties, such as abc.abstractproperty. Add
# to this list to register other decorators that produce valid properties.
# These decorators are taken in consideration only for invalid-name.
property-classes=abc.abstractproperty
# Naming style matching correct variable names.
variable-naming-style=snake_case
# Regular expression matching correct variable names. Overrides variable-
# naming-style.
#variable-rgx=
[FORMAT]
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
expected-line-ending-format=
# Regexp for a line that is allowed to be longer than the limit.
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
# Number of spaces of indent required inside a hanging or continued line.
indent-after-paren=4
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
# tab).
indent-string=' '
# Maximum number of characters on a single line.
max-line-length=120
# Maximum number of lines in a module.
max-module-lines=1000
# Allow the body of a class to be on the same line as the declaration if body
# contains single statement.
single-line-class-stmt=no
# Allow the body of an if to be on the same line as the test if there is no
# else.
single-line-if-stmt=no
[TYPECHECK]
# List of decorators that produce context managers, such as
# contextlib.contextmanager. Add to this list to register other decorators that
# produce valid context managers.
contextmanager-decorators=contextlib.contextmanager
# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E1101 when accessed. Python regular
# expressions are accepted.
generated-members=
# Tells whether missing members accessed in mixin class should be ignored. A
# mixin class is detected if its name ends with "mixin" (case insensitive).
ignore-mixin-members=yes
# Tells whether to warn about missing members when the owner of the attribute
# is inferred to be None.
ignore-none=yes
# This flag controls whether pylint should warn about no-member and similar
# checks whenever an opaque object is returned when inferring. The inference
# can return multiple potential results while evaluating a Python object, but
# some branches might not be evaluated, which results in partial inference. In
# that case, it might be useful to still emit no-member and other checks for
# the rest of the inferred objects.
ignore-on-opaque-inference=yes
# List of class names for which member attributes should not be checked (useful
# for classes with dynamically set attributes). This supports the use of
# qualified names.
ignored-classes=optparse.Values,thread._local,_thread._local
# List of module names for which member attributes should not be checked
# (useful for modules/projects where namespaces are manipulated during runtime
# and thus existing member attributes cannot be deduced by static analysis). It
# supports qualified module names, as well as Unix pattern matching.
ignored-modules=
# Show a hint with possible names when a member name was not found. The aspect
# of finding the hint is based on edit distance.
missing-member-hint=yes
# The minimum edit distance a name should have in order to be considered a
# similar match for a missing member name.
missing-member-hint-distance=1
# The total number of similar names that should be taken in consideration when
# showing a hint for a missing member.
missing-member-max-choices=1
# List of decorators that change the signature of a decorated function.
signature-mutators=
[STRING]
# This flag controls whether inconsistent-quotes generates a warning when the
# character used as a quote delimiter is used inconsistently within a module.
check-quote-consistency=no
# This flag controls whether the implicit-str-concat should generate a warning
# on implicit string concatenation in sequences defined over several lines.
check-str-concat-over-line-jumps=no
[SIMILARITIES]
# Ignore comments when computing similarities.
ignore-comments=yes
# Ignore docstrings when computing similarities.
ignore-docstrings=yes
# Ignore imports when computing similarities.
ignore-imports=no
# Minimum lines number of a similarity.
min-similarity-lines=4
[MISCELLANEOUS]
# List of note tags to take in consideration, separated by a comma.
notes=FIXME,
XXX,
TODO
# Regular expression of note tags to take in consideration.
#notes-rgx=
[LOGGING]
# The type of string formatting that logging methods do. `old` means using %
# formatting, `new` is for `{}` formatting.
logging-format-style=old
# Logging modules to check that the string format arguments are in logging
# function parameter format.
logging-modules=logging
[VARIABLES]
# List of additional names supposed to be defined in builtins. Remember that
# you should avoid defining new builtins when possible.
additional-builtins=
# Tells whether unused global variables should be treated as a violation.
allow-global-unused-variables=yes
# List of names allowed to shadow builtins
allowed-redefined-builtins=
# List of strings which can identify a callback function by name. A callback
# name must start or end with one of those strings.
callbacks=cb_,
_cb
# A regular expression matching the name of dummy variables (i.e. expected to
# not be used).
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
# Argument names that match this expression will be ignored. Default to name
# with leading underscore.
ignored-argument-names=_.*|^ignored_|^unused_
# Tells whether we should check for unused import in __init__ files.
init-import=no
# List of qualified module names which can have objects that can redefine
# builtins.
redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io
[SPELLING]
# Limits count of emitted suggestions for spelling mistakes.
max-spelling-suggestions=4
# Spelling dictionary name. Available dictionaries: en (aspell), en_AU
# (aspell), en_CA (aspell), en_GB (aspell), en_US (aspell), fr_CA (myspell),
# fr_MC (myspell), fr_CH (myspell), fr_LU (myspell), fr_FR (myspell), fr_BE
# (myspell), de_DE (myspell), es_VE (myspell), es_MX (myspell), es_CL
# (myspell), es_CR (myspell), es_US (myspell), it_CH (myspell), pt_BR
# (myspell), es_DO (myspell), en_ZA (myspell), es_PY (myspell), es_GT
# (myspell), es_CU (myspell), es_SV (myspell), es_PE (myspell), es_CO
# (myspell), de_CH (myspell), ru_RU (myspell), es_NI (myspell), es_ES
# (myspell), es_HN (myspell), it_IT (myspell), pt_PT (myspell), de_DE_frami
# (myspell), es_AR (myspell), de_CH_frami (myspell), es_PR (myspell), es_UY
# (myspell), de_AT_frami (myspell), de_AT (myspell), es_PA (myspell), fr
# (myspell), es_EC (myspell), es_BO (myspell).
spelling-dict=
# List of comma separated words that should be considered directives if they
# appear and the beginning of a comment and should not be checked.
spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy:
# List of comma separated words that should not be checked.
spelling-ignore-words=
# A path to a file that contains the private dictionary; one word per line.
spelling-private-dict-file=
# Tells whether to store unknown words to the private dictionary (see the
# --spelling-private-dict-file option) instead of raising a message.
spelling-store-unknown-words=no
[DESIGN]
# Maximum number of arguments for function / method.
max-args=5
# Maximum number of attributes for a class (see R0902).
max-attributes=10
# Maximum number of boolean expressions in an if statement (see R0916).
max-bool-expr=5
# Maximum number of branch for function / method body.
max-branches=12
# Maximum number of locals for function / method body.
max-locals=15
# Maximum number of parents for a class (see R0901).
max-parents=10
# Maximum number of public methods for a class (see R0904).
max-public-methods=20
# Maximum number of return / yield for function / method body.
max-returns=6
# Maximum number of statements in function / method body.
max-statements=50
# Minimum number of public methods for a class (see R0903).
min-public-methods=2
[IMPORTS]
# List of modules that can be imported at any level, not just the top level
# one.
allow-any-import-level=
# Allow wildcard imports from modules that define __all__.
allow-wildcard-with-all=no
# Analyse import fallback blocks. This can be used to support both Python 2 and
# 3 compatible code, which means that the block might have code that exists
# only in one or another interpreter, leading to false positives when analysed.
analyse-fallback-blocks=no
# Deprecated modules which should not be used, separated by a comma.
deprecated-modules=optparse,tkinter.tix
# Output a graph (.gv or any supported image format) of external dependencies
# to the given file (report RP0402 must not be disabled).
ext-import-graph=
# Output a graph (.gv or any supported image format) of all (i.e. internal and
# external) dependencies to the given file (report RP0402 must not be
# disabled).
import-graph=
# Output a graph (.gv or any supported image format) of internal dependencies
# to the given file (report RP0402 must not be disabled).
int-import-graph=
# Force import order to recognize a module as part of the standard
# compatibility libraries.
known-standard-library=
# Force import order to recognize a module as part of a third party library.
known-third-party=enchant
# Couples of modules and preferred modules, separated by a comma.
preferred-modules=
[CLASSES]
# Warn about protected attribute access inside special methods
check-protected-access-in-special-methods=no
# List of method names used to declare (i.e. assign) instance attributes.
defining-attr-methods=__init__,
__new__,
setUp,
__post_init__
# List of member names, which should be excluded from the protected access
# warning.
exclude-protected=_asdict,
_fields,
_replace,
_source,
_make
# List of valid names for the first argument in a class method.
valid-classmethod-first-arg=cls
# List of valid names for the first argument in a metaclass class method.
valid-metaclass-classmethod-first-arg=cls
[EXCEPTIONS]
# Exceptions that will emit a warning when being caught. Defaults to
# "BaseException, Exception".
overgeneral-exceptions=BaseException,
Exception

View File

@ -1,5 +1,3 @@
# ZenDesk Access Controller
## Управление правами доступа
Идея - написать программу(Web приложение), которая будет выдавать права пользователям системы по запросу самого
@ -39,7 +37,7 @@
## Quickstart
Перед запуском требуется неообходимо `.env` файл.
Перед запуском необходимо создать `.env` файл.
```bash
cp .env.example .env
```
@ -71,7 +69,7 @@ pip install -r requirements/dev.txt
```
ACTRL_DEBUG={0/1} - включить режим дебага
ACTRL_HOST={HOSTNAME} - при запуске без дебага, надо указать домен на котором будет работать приложение
ACTRL_SECRET_KEY={DJANGO_SECRET_KEY} - секретный ключ сгенерированый Django
ACTRL_SECRET_KEY={DJANGO_SECRET_KEY} - секретный ключ сгенерированный Django
ACTRL_EMAIL_HOST={SMTP_HOST} - домен почтового сервера через который приложение будет отправлять письма, например "smtp.gmail.com"
ACTRL_EMAIL_PORT={SMTP_PORT} - порт для почтового сервера, например 587, 465 , 2525
@ -133,7 +131,7 @@ docker run -d -p 8000:8000 \
-v {ABSOLUTE_PATH_TO_DB}:/zendesk-access-controller/db \
access_controller:latest
```
- открываем запущеный контейнер в браузере (можно перейти по ссылке http://localhost:8000/)
- открываем запущенный контейнер в браузере (можно перейти по ссылке http://localhost:8000/)
## Запуск с тестовыми юзерами:
@ -144,12 +142,29 @@ docker run -d -p 8000:8000 \
- Пользователь - `123@test.ru` / `zendeskuser`
Не сработает если домен песочницы отличается от `ngenix1612197338` (на другом домене нужно будет создать сначала пользователей в песочнице с правами админа и легкого агента
с этими же почтами, назначить им организацию `SYSTEM`)
с этими же email, назначить им организацию `SYSTEM`)
## Параметры тестовой песочницы:
Пример полной конфигурации можно найти в [.env.example](.env.example). Почту и токен админа ZenDesk взять у руководителя (если вы не админ).
## Для проверки pylint используем:
pylint --django-settings-module=access_controller.access_controller.settings ../access_controller (каталог, где лежит проект)
## Для приведения файлов к стандарту PEP8 используем:
autopep8 --in-place filename
##Для проверки орфографии:
cd docs
(set -a && source ../.env && make spelling)
##Для обновления документации:
m2r README.md
cd docs
(set -a && source ../.env && make html)
## Read more
- Zenpy: [http://docs.facetoe.com.au](http://docs.facetoe.com.au)

204
README.rst Normal file
View File

@ -0,0 +1,204 @@
Управление правами доступа
--------------------------
Идея - написать программу(Web приложение), которая будет выдавать права пользователям системы по запросу самого
пользователя. Например, из 12 человек 3 сейчас работают с правами админа, по окончании рабочей смены они сдают
свои права (освобождают места) и другие пользователи могут запросить эти права в свое пользование.
Оставшиеся 9 человек получают права легкого агента - без прав редактирования, а только чтение.
Из технологий - программа должна взаимодействовать с api системы Zendesk(система обращений клиентов - жалобы),
проверять авторизованного пользователя на права(будет возможность менять права напрямую из Zendesk - нужна
синхронизация прав с приоритетом у Zendesk).
Если руками в самом Zendesk права у пользователя отобрали или наоборот
присвоили, то наша программа обновляет статус пользователя в соответствии с данными синхронизации
(например, раз в минуту).
Так же в идеале должна быть проверка, что пользователь сайта существует на сайте Zendesk(по токену).
Сэндбокс Zendesk нам предоставит моя компания, библиотеку для работы с api уже подсказали.
Сама программа (наша) будет обладать админскими правами и реализовывать контроль и выдачу прав другим пользователям.
*Итого:*
#. Реализовать авторизацию пользователей с проверкой по API на существование такого пользователя
#. Реализовать интерфейс со статистикой рабочих мест(занято, свободно, кто занимает)
#. Реализовать логирование действий(когда взял права, когда отдал - запись в файл и БД)
#. Реализовать передачу прав приложением по запросу от пользователя и замену прав пользователя
у которого права отбираются внутри Zendesk (на легкий агент)
#. Реализовать синхронизацию по API на проверку прав(не менялись ли в системе Zendesk)
#. Реализовать возможность добавить большее количество админских прав
#. Реализовать возможность добавления легких агентов(права только на просмотр)
#. Реализовать на общей странице текущую информацию о пользователе - текущие права, карточка пользователя
Технологический стек:
---------------------
* Python 3
* Django 3
Quickstart
----------
Перед запуском необходимо создать ``.env`` файл.
.. code-block:: bash
cp .env.example .env
Заменить переменные в ``.env`` на актуальные.
.. code-block:: bash
sudo apt install make
pip install --upgrade pip
pip install -r requirements/dev.txt
(set -a && source .env && ./manage.py migrate)
(set -a && source .env && ./manage.py loaddata data.json)
(set -a && source .env && ./manage.py runserver)
Перед запуском для тестирования:
--------------------------------
Убедитесь, что вы зарегистрированы в песочнице ZenDesk, у вас назначена организация ``SYSTEM``
Для админов ZenDesk дополнительно - создайте токен доступа в ZenDesk
При запуске в Docker убедитесь что папка, которая будет служить хранилищем для БД, открыта на запись и чтение
Запуск на локальной машине:
---------------------------
* Скопировать репозиторий на локальную машину
* Перейти в папку приложения
* Активировать виртуальное окружение
* Выполнить команду ``pip install -r requirements/dev.txt``
* В виртуальное окружение добавить следующие переменные:
.. code-block::
ACTRL_DEBUG={0/1} - включить режим дебага
ACTRL_HOST={HOSTNAME} - при запуске без дебага, надо указать домен на котором будет работать приложение
ACTRL_SECRET_KEY={DJANGO_SECRET_KEY} - секретный ключ сгенерированный Django
ACTRL_EMAIL_HOST={SMTP_HOST} - домен почтового сервера через который приложение будет отправлять письма, например "smtp.gmail.com"
ACTRL_EMAIL_PORT={SMTP_PORT} - порт для почтового сервера, например 587, 465 , 2525
ACTRL_EMAIL_TLS={USE_TLS} - использовать TLS для подключения к почтовому серверу, 0 или 1
ACTRL_EMAIL_HOST_USER={USERNAME} - логин с которым приложение входит на почтовый сервер
ACTRL_EMAIL_HOST_PASSWORD={PASSWORD} - пароль/ключ с которым приложение входит на почтовый сервер
ACTRL_FROM_EMAIL={EMAIL} - адрес с которого приложение отправляет письма
ACTRL_SERVER_EMAIL={EMAIL} - адрес на который отвечают пользователя
ACTRL_API_EMAIL={EMAIL} - почта админа в ZenDesk
ACTRL_API_PASSWORD={PASSWORD} - пароль админа ZenDesk
ACTRL_API_TOKEN={API_TOKEN} - API токен зендеск
ACTRL_ZENDESK_SUBDOMAIN={DOMAIN} - домен ZenDesk
ENG_CROLE_ID={ENGINEER_CUSTOM_ROLE_ID} - id роли инженера( custom_role_id сотрдника смены)
LA_CROLE_ID={LIGHT_AGENT_CUSTOM_ROLE_ID} - id роли легкого агента (custom_role_id роли -легкий агент)
EMPL_GROUP={EMPLOYEE_GROUP_NAME} - имя группы которой принадлежат сотрудники ССКС
BUF_GROUP={BUFFER_GROUP_NAME} - имя буферной группы для передачи смен(через нее происходит управление тикетами)
ST_EMAIL={SOLVED_TICKETS_EMAIL} - почта на которую будут переназначятся закрытые тикеты
LICENSE_NO={LICENSE_NO} - количество лицензий, отображаемых как доступные в приложении
SHIFTH={SHIFT_HOURS} - количество часов в рабочей смене (нужно для статистики, пока не реализовано но требует указания значения)
* Выполнить команду ``python manage.py migrate``
* Запустить приложение командой ``python manage.py runserver`` (можно указать в параметрах для файла manage.py)
* Перейти по ссылке в консоли (вероятнее всего откроется по адресу http://127.0.0.1:8000/)
Запуск в Docker:
----------------
Требуется установленный и настроенный Docker
* Скопировать репозиторий на локальную машину
* В командной строке перейти в папку проекта
* Выполнить команду ``docker build --tag access_controller:latest .``
* Выполнить команду
.. code-block:: bash
docker run -d -p 8000:8000 \
ACTRL_DEBUG={0/1} \
ACTRL_HOST={HOSTNAME} \
ACTRL_SECRET_KEY={DJANGO_SECRET_KEY} \
ACTRL_EMAIL_HOST={SMTP_HOST} \
ACTRL_EMAIL_PORT={SMTP_PORT} \
ACTRL_EMAIL_TLS={USE_TLS} \
ACTRL_EMAIL_HOST_USER={USERNAME} \
ACTRL_EMAIL_HOST_PASSWORD={PASSWORD} \
ACTRL_FROM_EMAIL={EMAIL} \
ACTRL_SERVER_EMAIL={EMAIL} \
ACTRL_API_EMAIL={EMAIL} \
ACTRL_API_PASSWORD={PASSWORD} \
ACTRL_API_TOKEN={API_TOKEN} \
ACTRL_ZENDESK_SUBDOMAIN={DOMAIN} \
ENG_CROLE_ID={ENGINEER_CUSTOM_ROLE_ID} \
LA_CROLE_ID={LIGHT_AGENT_CUSTOM_ROLE_ID} \
EMPL_GROUP={EMPLOYEE_GROUP_NAME} \
BUF_GROUP={BUFFER_GROUP_NAME} \
ST_EMAIL={SOLVED_TICKETS_EMAIL} \
LICENSE_NO={LICENSE_NO} \
SHIFTH={SHIFT_HOURS} \
-v {ABSOLUTE_PATH_TO_DB}:/zendesk-access-controller/db \
access_controller:latest
* открываем запущенный контейнер в браузере (можно перейти по ссылке http://localhost:8000/)
Запуск с тестовыми юзерами:
---------------------------
На локальной машине - перед запуском команды ``python manage.py runserver`` выполнить команду ``python manage.py loaddata data.json``
Это создаст тестового админа и тестового пользователя в приложении для песочницы ZenDesk.
* Админ - ``admin@gmail.com`` / ``zendeskadmin``
* Пользователь - ``123@test.ru`` / ``zendeskuser``
Не сработает если домен песочницы отличается от ``ngenix1612197338`` (на другом домене нужно будет создать сначала пользователей в песочнице с правами админа и легкого агента
с этими же email, назначить им организацию ``SYSTEM``\ )
Параметры тестовой песочницы:
-----------------------------
Пример полной конфигурации можно найти в `.env.example <.env.example>`_. Почту и токен админа ZenDesk взять у руководителя (если вы не админ).
Для проверки pylint используем:
-------------------------------
pylint ../access_controller_new
Вместо "access_controller_new" необходимо указать папку проекта.
Для приведения файлов к стандарту PEP8 используем:
--------------------------------------------------
autopep8 --in-place filename
Для проверки орфографии:
------------------------
cd docs
(set -a && source ../.env && make spelling)
Для обновления документации:
----------------------------
m2r README.md
cd docs
(set -a && source ../.env && make html)
Read more
---------
* Zenpy: `http://docs.facetoe.com.au <http://docs.facetoe.com.au>`_
* Zendesk API: `https://developer.zendesk.com/rest_api/docs/ <https://developer.zendesk.com/rest_api/docs/>`_

0
__init__.py Normal file
View File

View File

@ -8,10 +8,7 @@ https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/
"""
import os
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'access_controller.settings')
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'access_controller.settings')
application = get_asgi_application()

View File

@ -1,19 +1,31 @@
"""
Авторизация пользователя.
"""
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth.models import User
from django.contrib.auth import get_user_model
class EmailAuthBackend(ModelBackend):
"""
Класс авторизации пользователя по email.
"""
def authenticate(self, request, username=None, password=None, **kwargs):
"""
Функция получения пользователя (модель User) по email.
"""
try:
user = User.objects.get(email=username)
user = get_user_model().objects.get(email=username)
if user.check_password(password):
return user
return None
except User.DoesNotExist:
except get_user_model().DoesNotExist:
return None
def get_user(self, user_id):
"""
Функция получения пользователя по id.
"""
try:
return User.objects.get(pk=user_id)
except User.DoesNotExist:
return get_user_model().objects.get(pk=user_id)
except get_user_model().DoesNotExist:
return None

View File

@ -14,6 +14,8 @@ from pathlib import Path
from dotenv import load_dotenv
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
@ -26,7 +28,7 @@ load_dotenv()
SECRET_KEY = os.getenv('ACTRL_SECRET_KEY', 'empty')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = bool(int(os.getenv('ACTRL_DEBUG', 1)))
DEBUG = bool(int(os.getenv('ACTRL_DEBUG', '1')))
ALLOWED_HOSTS = [
'127.0.0.1',
@ -62,8 +64,8 @@ ROOT_URLCONF = 'access_controller.urls'
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = os.getenv('ACTRL_EMAIL_HOST', 'smtp.gmail.com')
EMAIL_PORT = int(os.getenv('ACTRL_EMAIL_PORT', 587))
EMAIL_USE_TLS = bool(int(os.getenv('ACTRL_EMAIL_TLS', 1)))
EMAIL_PORT = int(os.getenv('ACTRL_EMAIL_PORT', '587'))
EMAIL_USE_TLS = bool(int(os.getenv('ACTRL_EMAIL_TLS', '1')))
EMAIL_HOST_USER = os.getenv('ACTRL_EMAIL_HOST_USER', 'group02django@gmail.com')
EMAIL_HOST_PASSWORD = os.getenv('ACTRL_EMAIL_HOST_PASSWORD', 'djangogroup02')
DEFAULT_FROM_EMAIL = os.getenv('ACTRL_FROM_EMAIL', EMAIL_HOST_USER)
@ -144,6 +146,8 @@ ACCOUNT_ACTIVATION_DAYS = 7
LOGIN_REDIRECT_URL = '/'
LOGOUT_REDIRECT_URL = '/'
# Название_приложения.Названиеайла.Название_класса_обработчика
AUTHENTICATION_BACKENDS = [
'access_controller.auth.EmailAuthBackend',
@ -154,8 +158,8 @@ AUTHENTICATION_BACKENDS = [
ZENDESK_ROLES = {
'engineer': int(os.getenv('ENG_CROLE_ID', 0)),
'light_agent': int(os.getenv('LA_CROLE_ID', 0)),
'engineer': int(os.getenv('ENG_CROLE_ID', '0')),
'light_agent': int(os.getenv('LA_CROLE_ID', '0')),
}
ZENDESK_GROUPS = {
@ -165,7 +169,7 @@ ZENDESK_GROUPS = {
SOLVED_TICKETS_EMAIL = os.getenv('ST_EMAIL')
ZENDESK_MAX_AGENTS = int(os.getenv('LICENSE_NO', 0))
ZENDESK_MAX_AGENTS = int(os.getenv('LICENSE_NO', '0'))
REST_FRAMEWORK = {
# Use Django's standard `django.contrib.auth` permissions,
@ -175,7 +179,7 @@ REST_FRAMEWORK = {
]
}
ONE_DAY = int(os.getenv('SHIFTH', 0)) # Количество часов в 1 рабочем дне
ONE_DAY = int(os.getenv('SHIFTH', '0')) # Количество часов в 1 рабочем дне
ACTRL_ZENDESK_SUBDOMAIN = os.getenv('ACTRL_ZENDESK_SUBDOMAIN') or os.getenv('ZD_DOMAIN')
ACTRL_API_EMAIL = os.getenv('ACTRL_API_EMAIL') or os.getenv('ACCESS_CONTROLLER_API_EMAIL')

View File

@ -31,7 +31,7 @@ urlpatterns = [
path('accounts/register/error/', registration_error, name='registration_email_error'),
path('accounts/login/', CustomLoginView.as_view(), name='login'),
path('accounts/', include('django.contrib.auth.urls')),
path('work/<int:id>', work_page, name="work"),
path('work/<int:required_id>', 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('work/get_tickets', work_get_tickets, name='work_get_tickets'),

View File

@ -6,7 +6,7 @@ Models
*******
.. automodule:: main.models
:members:
:members:
******

View File

@ -12,16 +12,12 @@
#
import os
import sys
import importlib
import inspect
import enchant
from enchant import checker
import django
sys.path.insert(0, os.path.abspath('../../'))
import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'access_controller.settings')
os.environ.setdefault('DJANGO_CONFIGURATION', 'Dev')
@ -39,7 +35,6 @@ from django.db.models.query import QuerySet
QuerySet.__repr__ = lambda self: self.__class__.__name__
django.setup()
# -- Project information -----------------------------------------------------
@ -89,6 +84,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
lines.append('')
return lines
@ -116,13 +112,17 @@ def skip_queryset(app, what, name, obj, skip, options):
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)
# app.connect('autodoc-process-docstring', process_modules)
def fix_sig(app, what, name, obj, options, signature, return_annotation):
return "", ""
def setup(app):
# Register the docstring processor with sphinx
app.connect('autodoc-process-docstring', process_django_models)
app.connect('autodoc-skip-member', skip_queryset)
app.connect('autodoc-process-docstring', process_modules)
app.connect("autodoc-process-signature", fix_sig)
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
@ -132,16 +132,13 @@ extensions = {
'sphinx.ext.autodoc',
'sphinx.ext.intersphinx',
'sphinx.ext.viewcode',
'sphinx.ext.napoleon',
'sphinx_rtd_theme',
'sphinx.ext.graphviz',
'sphinx.ext.inheritance_diagram',
'sphinx_autodoc_typehints',
'sphinxcontrib.spelling'
'sphinxcontrib.spelling',
}
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
@ -171,6 +168,7 @@ html_static_path = ['_static']
# -- Extension configuration -------------------------------------------------
# -- Options for intersphinx extension ---------------------------------------
# Example configuration for intersphinx: refer to the Python standard library.
@ -183,20 +181,21 @@ intersphinx_mapping = {
}
autodoc_default_flags = ['members']
autodoc_typehints = "description"
# spell checking
spelling_lang = 'ru_RU'
tokenizer_lang = 'ru_RU'
spelling_exclude_patterns=['ignored_*']
tokenizer_lang = 'en_US'
spelling_exclude_patterns = ['ignored_*', '../../main/models.py']
spelling_show_suggestions = True
spelling_show_whole_line=True
spelling_warning=True
spelling_show_whole_line = True
spelling_warning = True
spelling_ignore_pypi_package_names = True
spelling_ignore_wiki_words=True
spelling_ignore_acronyms=True
spelling_ignore_python_builtins=True
spelling_ignore_importable_modules=True
spelling_ignore_contributor_names=True
spelling_ignore_wiki_words = True
spelling_ignore_acronyms = True
spelling_ignore_python_builtins = True
spelling_ignore_importable_modules = True
spelling_ignore_contributor_names = True
# -- Options for todo extension ----------------------------------------------
@ -206,5 +205,3 @@ set_type_checking_flag = True
typehints_fully_qualified = True
always_document_param_types = True
typehints_document_rtype = True
napoleon_attr_annotations = True

View File

@ -12,6 +12,7 @@
overview
code
readme
todo

View File

@ -69,12 +69,15 @@
Запрос прав доступа
********************
На странице запроса прав Вам доступна информация о количестве и списке работающих над тикетами сотрудников,
а также возможность сдать и запросить права.
На странице запроса прав Вам доступна информация о количестве и списке работающих над тикетами сотрудников.
Если Вы не являетесь инженером, то на данной странице Вы можете запросить права.
Если Вы являетесь инженером, то права можно сдать.
.. image:: _static/request.png
Успешное изменение прав:
Успешное изменение прав - список инженеров пополнился новым пользователем:
.. image:: _static/role_change.png
@ -84,9 +87,9 @@
Для администратора существует удобный интерфейс страницы управления, в котором представлены:
* Количество свободных инженерных мест
* Количество и список инженеров и легких агентов
* Возможность группового назначения прав с использованием чек-боксов
* количество свободных инженерных мест
* количество и список инженеров и легких агентов
* возможность группового назначения прав с использованием чекбоксов
.. image:: _static/admin_manage.png

4
docs/source/readme.rst Normal file
View File

@ -0,0 +1,4 @@
READ.me
==================
.. include:: ../../README.rst

View File

@ -45,7 +45,9 @@ start
end
date
Токен
токен
токеном
токену
аутентифицирован
(datetime.time)
datetime
@ -81,8 +83,112 @@ functions
Serializer
Serializers
Сериализатор
Сериализаторы
сериализатор
сериализатора
переадресации
чекбоксов
админских
админские
Python
Docker
докер
докера
Докер
репозиторий
zendesk-access-controller/db
-e
-v
e
v
zendesk
db
юзерами
Read
Zenpy
залогинен
т
д
rolchangelogs
извеcтно
role
View
Model
type
param
rtype
return
UsersViewSet
list
engineers
agents
request
rest
framework
response
Сэндбокс
админскими
логирование
code
block
d
p
ACTRL_DEBUG
дебага
ACTRL_HOST
HOSTNAME
ACTRL_SECRET_KEY
DJANGO_SECRET_KEY
ACTRL_EMAIL_HOST
SMTP_HOST
smtp.gmail.com
ACTRL_EMAIL_PORT
SMTP_PORT
ACTRL_EMAIL_TLS
USE_TLS
TLS
ACTRL_EMAIL_HOST_USER
USERNAME
ACTRL_EMAIL_HOST_PASSWORD
PASSWORD
ACTRL_FROM_EMAIL
EMAIL
ACTRL_SERVER_EMAIL
ACTRL_API_EMAIL
ACTRL_API_PASSWORD
ACTRL_API_TOKEN
API_TOKEN
ACTRL_ZENDESK_SUBDOMAIN
DOMAIN
ENG_CROLE_ID
ENGINEER_CUSTOM_ROLE_ID
custom_role_id
LA_CROLE_ID
LIGHT_AGENT_CUSTOM_ROLE_ID
EMPL_GROUP
EMPLOYEE_GROUP_NAME
ССКС
BUF_GROUP
BUFFER_GROUP_NAME
буферной
тикетами
ST_EMAIL
SOLVED_TICKETS_EMAIL
LICENSE_NO
LICENSE_NO
SHIFTH
SHIFT_HOURS
ABSOLUTE
ABSOLUTE_PATH_TO_DB
PATH
TO
DB
latest
in
place
cd
docs
a
Аватарка
filename

View File

@ -1,3 +1,8 @@
from django.contrib import admin
"""
Встроенный файл
"""
# from django.contrib import admin
# Register your models here.

0
main/apiauth.py Normal file
View File

View File

@ -1,5 +1,11 @@
"""
Стандартный файл Django конфигурации приложения.
"""
from django.apps import AppConfig
class MainConfig(AppConfig):
"""
Старт приложения
"""
name = 'main'

View File

@ -1,8 +1,14 @@
"""
Вспомогательные функции со списками пользователей, статистикой и т.д.
"""
import logging
from datetime import timedelta
from typing import Union
from django.contrib.auth.models import User
from django.contrib.auth import get_user_model
from django.core.exceptions import ObjectDoesNotExist
from django.core.handlers.wsgi import WSGIRequest
from django.http import HttpResponseRedirect, HttpResponsePermanentRedirect
from django.shortcuts import redirect
from django.utils import timezone
from zenpy import Zenpy
@ -16,7 +22,7 @@ from main.requester import TicketListRequester
from main.zendesk_admin import zenpy
def update_role(user_profile: UserProfile, role: int, who_changes: User) -> None:
def update_role(user_profile: UserProfile, role: int, who_changes: get_user_model()) -> None:
"""
Функция меняет роль пользователя.
@ -34,7 +40,7 @@ def update_role(user_profile: UserProfile, role: int, who_changes: User) -> None
zendesk.update_user(user)
def make_engineer(user_profile: UserProfile, who_changes: User) -> None:
def make_engineer(user_profile: UserProfile, who_changes: get_user_model()) -> None:
"""
Функция устанавливает пользователю роль инженера.
@ -44,7 +50,7 @@ def make_engineer(user_profile: UserProfile, who_changes: User) -> None:
update_role(user_profile, ROLES['engineer'], who_changes)
def make_light_agent(user_profile: UserProfile, who_changes: User) -> None:
def make_light_agent(user_profile: UserProfile, who_changes: get_user_model()) -> None:
"""
Функция устанавливает пользователю роль легкого агента.
@ -89,7 +95,7 @@ def get_users_list() -> list:
return users
def get_tickets_list(email):
def get_tickets_list(email) -> list:
"""
Функция возвращает список тикетов пользователя Zendesk
"""
@ -103,7 +109,7 @@ def get_tickets_list_for_group(group_name):
return TicketListRequester().get_tickets_list_for_group(zenpy.get_group(group_name))
def update_profile(user_profile: UserProfile):
def update_profile(user_profile: UserProfile) -> None:
"""
Функция обновляет профиль пользователя в соответствии с текущим в Zendesk.
@ -157,7 +163,7 @@ def check_user_auth(email: str, password: str) -> bool:
return True
def update_user_in_model(profile: UserProfile, zendesk_user: ZenpyUser):
def update_user_in_model(profile: UserProfile, zendesk_user: ZenpyUser) -> None:
"""
Функция обновляет профиль пользователя при изменении данных пользователя на Zendesk.
@ -173,7 +179,7 @@ def update_user_in_model(profile: UserProfile, zendesk_user: ZenpyUser):
profile.save()
def count_users(users) -> tuple:
def count_users(users: list) -> tuple:
"""
Функция подсчета количества сотрудников с ролями engineer и light_agent
"""
@ -186,21 +192,21 @@ def count_users(users) -> tuple:
return engineers, light_agents
def update_users_in_model():
def update_users_in_model() -> list:
"""
Обновляет пользователей в модели UserProfile по списку пользователей в организации
"""
users = get_users_list()
for user in users:
try:
profile = User.objects.get(email=user.email).userprofile
profile = get_user_model().objects.get(email=user.email).userprofile
update_user_in_model(profile, user)
except ObjectDoesNotExist:
pass
return users
def daterange(start_date, end_date) -> list:
def daterange(start_date: timedelta, end_date: timedelta) -> list:
"""
Функция возвращает список дней с start_date по end_date, исключая правую границу.
@ -214,17 +220,17 @@ def daterange(start_date, end_date) -> list:
return dates
def get_timedelta(log, time=None) -> timedelta:
def get_timedelta(current_log: RoleChangeLogs, time: timedelta = None) -> timedelta:
"""
Функция возвращает объект класса timedelta, который хранит промежуток времени от начала суток до момента,
который находится в log (объект класса RoleChangeLogs) или в time(datetime.time), если введён.
:param log: Лог
:param current_log: Лог
:param time: Время
:return: Сколько времени прошло от начала суток до события
"""
if time is None:
time = log.change_time.time()
time = current_log.change_time.time()
time = timedelta(hours=time.hour, minutes=time.minute, seconds=time.second)
return time
@ -241,6 +247,9 @@ def last_day_of_month(day: int) -> int:
class DatabaseHandler(logging.Handler):
"""
Класс записи изменений ролей в базу данных.
"""
def __init__(self):
logging.Handler.__init__(self)
@ -265,10 +274,19 @@ class DatabaseHandler(logging.Handler):
class CsvFormatter(logging.Formatter):
"""
Класс преобразования смены ролей пользователей в строковый формат.
"""
def __init__(self):
logging.Formatter.__init__(self)
def format(self, record):
def format(self, record: logging.LogRecord) -> str:
"""
Функция форматирует запись смены роли пользователя в строку.
:param record: Запись смены роли пользователя.
:return: Строка с записью смены пользователя.
"""
users = record.msg
if users[1]:
user = users[0]
@ -291,10 +309,10 @@ class CsvFormatter(logging.Formatter):
def log(user, admin=None):
"""
Осуществляет запись логов в базу данных и csv файл
:param admin:
:param user:
:return:
Функция осуществляет запись логов в базу данных и csv файл.
:param admin: Админ, который меняет роль
:param user: Пользователь, которому изменена роль
"""
users = [user, admin]
logger = logging.getLogger('MY_LOGGER')
@ -309,10 +327,16 @@ def log(user, admin=None):
logger.info(users)
def set_session_params_for_work_page(request, count=None, is_confirm=True):
def set_session_params_for_work_page(request: WSGIRequest, count: int = None, is_confirm: bool = True) -> \
Union[HttpResponsePermanentRedirect, HttpResponseRedirect]:
"""
Функция для страницы получения прав
Устанавливает данные сессии о успешности запроса и количестве назначенных тикетов
Функция для страницы получения прав, устанавливает данные сессии о успешности запроса и количестве
назначенных тикетов.
:param request: Получение данных с рабочей страницы пользователя
:param count: Количество запрошенных тикетов
:param is_confirm: Назначение тикетов
:return: Перезагрузка страницы "Управление правами" соответствующего пользователя
"""
request.session['is_confirm'] = is_confirm
request.session['count_tickets'] = count

View File

@ -1,3 +1,6 @@
"""
Формы.
"""
from django import forms
from django.contrib.auth.forms import AuthenticationForm
from django_registration.forms import RegistrationFormUniqueEmail
@ -56,6 +59,8 @@ class CustomAuthenticationForm(AuthenticationForm):
:param username: Поле для ввода email пользователя
:type username: :class:`django.forms.fields.CharField`
:param error_messages: Список ошибок авторизации
:type error_messages: :class:`dict`
"""
username = forms.CharField(
label="Электронная почта",
@ -64,8 +69,7 @@ class CustomAuthenticationForm(AuthenticationForm):
error_messages = {
'invalid_login':
"Пожалуйста, введите правильные электронную почту и пароль. Оба поля "
"могут быть чувствительны к регистру."
,
"могут быть чувствительны к регистру.",
'inactive': "Аккаунт не активен.",
}

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.3 on 2021-05-20 17:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0017_auto_20210408_1943'),
]
operations = [
migrations.AlterField(
model_name='unassignedticket',
name='ticket_id',
field=models.IntegerField(help_text='Номер тикета, для которого сняли ответственного'),
),
]

View File

@ -1,5 +1,8 @@
"""
Модели, использующиеся в приложении.
"""
from django.contrib.auth import get_user_model
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
@ -19,42 +22,64 @@ 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=get_user_model(), on_delete=models.CASCADE, 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='Имя пользователя на нашем сайте')
@property
def zendesk_role(self):
id = self.custom_role_id
def zendesk_role(self) -> str:
"""
Функция возвращает роль пользователя в Zendesk.
В формате str, либо UNDEFINED, если пользователь не найден
:return: Роль пользователя в Zendesk
"""
for role, r_id in ZENDESK_ROLES.items():
if r_id == id:
if r_id == self.custom_role_id:
return role
return 'UNDEFINED'
@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
@receiver(post_save, sender=get_user_model())
def create_user_profile(instance, created, **kwargs) -> None:
"""
Функция создания профиля пользователя (Userprofile) при регистрации пользователя.
:param instance: Экземпляр класса User
:param created: Создание профиля пользователя
:param kwargs: Параметры
:return: Обновленный список объектов профилей пользователей
"""
if created:
UserProfile.objects.create(user=instance)
@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
@receiver(post_save, sender=get_user_model())
def save_user_profile(instance, **kwargs) -> None:
"""
Функция записи БД профиля пользователя.
:param instance: Экземпляр класса User
:param kwargs: Параметры
:return: Запись профиля пользователя
"""
instance.userprofile.save()
class RoleChangeLogs(models.Model):
"""
Модель для логирования изменений ролей пользователя.
Модель для логгирования изменений ролей пользователя
"""
user = models.ForeignKey(to=User, on_delete=models.CASCADE, help_text='Пользователь, которому присвоили другую роль')
user = models.ForeignKey(to=get_user_model(), on_delete=models.CASCADE,
help_text='Пользователь, которому присвоили другую роль')
old_role = models.IntegerField(default=0, help_text='Старая роль')
new_role = models.IntegerField(default=0, help_text='Присвоенная роль')
change_time = models.DateTimeField(default=timezone.now, help_text='Дата и время изменения роли')
changed_by = models.ForeignKey(to=User, on_delete=models.CASCADE, related_name='changed_by', help_text='Кем была изменена роль')
changed_by = models.ForeignKey(to=get_user_model(), on_delete=models.CASCADE, related_name='changed_by',
help_text='Кем была изменена роль')
class UnassignedTicketStatus(models.IntegerChoices):
@ -63,13 +88,15 @@ class UnassignedTicketStatus(models.IntegerChoices):
:param UNASSIGNED: Снят с пользователя, перенесён в буферную группу
:param RESTORED: Авторство восстановлено
:param NOT_FOUND: Пока нас не было, тикет испарился из буферной группы. Дополнительные действия не требуются
:param NOT_FOUND: Пока нас не было, тикет испарился из буферной группы.
Дополнительные действия не требуются
:param CLOSED: Тикет уже был закрыт. Дополнительные действия не требуются
:param SOLVED: Тикет решён. Записан на пользователя с почтой SOLVED_TICKETS_EMAIL
"""
UNASSIGNED = 0, 'Снят с пользователя, перенесён в буферную группу'
RESTORED = 1, 'Авторство восстановлено'
NOT_FOUND = 2, 'Пока нас не было, тикет испарился из буферной группы. Дополнительные действия не требуются'
NOT_FOUND = 2, 'Пока нас не было, тикет испарился из ' \
'буферной группы. Дополнительные действия не требуются'
CLOSED = 3, 'Тикет уже был закрыт. Дополнительные действия не требуются'
SOLVED = 4, 'Тикет решён. Записан на пользователя с почтой SOLVED_TICKETS_EMAIL'
@ -78,7 +105,8 @@ class UnassignedTicket(models.Model):
"""
Модель не распределенного тикета.
"""
assignee = models.ForeignKey(to=User, on_delete=models.CASCADE, related_name='tickets', help_text='Пользователь, с которого снят тикет')
ticket_id = models.IntegerField(help_text='Номер тикера, для которого сняли ответственного')
status = models.IntegerField(choices=UnassignedTicketStatus.choices, default=UnassignedTicketStatus.UNASSIGNED, help_text='Статус тикета')
assignee = models.ForeignKey(to=get_user_model(), on_delete=models.CASCADE, related_name='tickets',
help_text='Пользователь, с которого снят тикет')
ticket_id = models.IntegerField(help_text='Номер тикета, для которого сняли ответственного')
status = models.IntegerField(choices=UnassignedTicketStatus.choices, default=UnassignedTicketStatus.UNASSIGNED,
help_text='Статус тикета')

View File

@ -1,3 +1,6 @@
"""
Обработка тикетов.
"""
import requests
from zenpy import TicketApi
from zenpy.lib.api_objects import Ticket
@ -6,6 +9,9 @@ from main.zendesk_admin import zenpy
class TicketListRequester:
"""
Класс обработки тикетов.
"""
def __init__(self):
self.email = zenpy.credentials['email']
if zenpy.credentials.get('token'):
@ -15,11 +21,17 @@ class TicketListRequester:
self.token_or_password = zenpy.credentials.get('password')
self.prefix = f'https://{zenpy.credentials.get("subdomain")}.zendesk.com/api/v2/'
def get_tickets_list_for_user(self, zendesk_user):
def get_tickets_list_for_user(self, zendesk_user: zenpy) -> str:
"""
Функция получения списка тикетов пользователя Zendesk.
"""
url = self.prefix + f'users/{zendesk_user.id}/tickets/assigned'
return self._get_tickets(url)
def get_tickets_list_for_group(self, group):
def get_tickets_list_for_group(self, group: zenpy) -> list():
"""
Функция получения списка тикетов группы пользователей Zendesk.
"""
url = self.prefix + '/tickets'
all_tickets = self._get_tickets(url)
tickets = list()
@ -28,7 +40,10 @@ class TicketListRequester:
tickets.append(ticket)
return tickets
def _get_tickets(self, url):
def _get_tickets(self, url: str) -> list():
"""
Функция получения полного списка тикетов по url.
"""
response = requests.get(url, auth=(self.email, self.token_or_password))
tickets = []
if response.status_code != 200:

View File

@ -1,4 +1,7 @@
from django.contrib.auth.models import User
"""
Сериализаторы.
"""
from django.contrib.auth import get_user_model
from rest_framework import serializers
from main.models import UserProfile
from access_controller.settings import ZENDESK_ROLES
@ -7,14 +10,28 @@ from access_controller.settings import ZENDESK_ROLES
class UserSerializer(serializers.HyperlinkedModelSerializer):
"""
Класс serializer для модели User.
:param model: Модель, на основании которой создается сериализатор
:type model: :class:`django.contrib.auth.Models`
:param fields: Передаваемые поля
:type email: :class:`list`
"""
class Meta:
model = User
model = get_user_model()
fields = ['email']
class ProfileSerializer(serializers.HyperlinkedModelSerializer):
"""Класс serializer для модели профиля пользователя"""
"""
Класс serializer для модели профиля пользователя.
:param user: Вложенный сериализатор
:type user: :class:`UserSerializer`
:param model: Модель, на основании которой создается сериализатор
:type model: :class:`django.contrib.auth.Models`
:param fields: Передаваемые поля
:type email: :class:`list`
"""
user = UserSerializer()
class Meta:
@ -23,16 +40,36 @@ class ProfileSerializer(serializers.HyperlinkedModelSerializer):
class ZendeskUserSerializer(serializers.Serializer):
"""Класс serializer для объектов пользователей из zenpy"""
"""
Класс serializer для объектов пользователей из Zenpy.
:param name: Имя пользователя
:type name: :class:`str`
:param zendesk_role: Роль из Zendesk
:type zendesk_role: :class:`str`
:param email: Email пользователя
:type email: :class:`str`
"""
name = serializers.CharField()
zendesk_role = serializers.SerializerMethodField('get_zendesk_role')
email = serializers.EmailField()
@staticmethod
def get_zendesk_role(obj):
def get_zendesk_role(obj) -> str:
"""
Функция строкового заполнения поля сериализатора zendesk_role.
:param obj: объект пользователя Zendesk
:return: роль engineer либо light_agent
"""
if obj.custom_role_id == ZENDESK_ROLES['engineer']:
return 'engineer'
elif obj.custom_role_id == ZENDESK_ROLES['light_agent']:
if obj.custom_role_id == ZENDESK_ROLES['light_agent']:
return 'light_agent'
else:
return "empty"
return "empty"
def create(self, validated_data):
pass
def update(self, instance, validated_data):
pass

View File

@ -1,7 +1,10 @@
"""
Обработка статистики.
"""
from datetime import date, datetime, timedelta
from typing import Optional
from django.contrib.auth.models import User
from django.contrib.auth import get_user_model
from django.utils import timezone
from access_controller.settings import ONE_DAY, ZENDESK_ROLES as ROLES
@ -50,19 +53,19 @@ class StatisticData:
else:
self.statistic = stat
def get_statistic(self) -> dict:
def get_statistic(self) -> Optional[dict]:
"""
Функция возвращает статистику работы пользователя.
:return: Словарь statistic с применением формата отображения и интервала работы(если они есть). None, если были ошибки при создании.
:return: Словарь statistic с применением формата отображения и интервала работы(если они есть).
None, если были ошибки при создании.
"""
if self.is_valid_statistic():
stat = self.statistic
stat = self._use_display(stat)
stat = self._use_interval(stat)
return stat
else:
return None
return None
def is_valid_statistic(self) -> bool:
"""
@ -104,8 +107,7 @@ class StatisticData:
"""
if self.is_valid_data():
return self.data
else:
return None
return None
def is_valid_data(self) -> bool:
"""
@ -170,7 +172,8 @@ class StatisticData:
"""
Функция возвращает логи в диапазоне дат start_date - end_date для пользователя с указанным email.
:return: Данные о смене статусов пользователя. Если пользователь не найден или интервал времени некорректен - ошибку.
:return: Данные о смене статусов пользователя. Если пользователь не найден или
интервал времени некорректен - ошибку.
"""
if not self.check_time():
self.errors += ['Конец диапазона должен быть позже начала диапазона и раньше текущего времени']
@ -178,12 +181,12 @@ class StatisticData:
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),
user=get_user_model().objects.get(email=self.email),
).order_by('change_time')
except User.DoesNotExist:
except get_user_model().DoesNotExist:
self.errors += ['Пользователь не найден']
def _init_statistic(self) -> dict:
def _init_statistic(self) -> None:
"""
Функция заполняет словарь, в котором ключ - дата, значение - кол-во проработанных в этот день секунд.
@ -192,18 +195,18 @@ class StatisticData:
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]
else:
first_log, last_log = self.data[0], self.data[len(self.data) - 1]
if first_log.old_role == ROLES['engineer']:
self.prev_engineer_logic(first_log)
if first_log.old_role == ROLES['engineer']:
self.prev_engineer_logic(first_log)
if last_log.new_role == ROLES['engineer']:
self.post_engineer_logic(last_log)
if last_log.new_role == ROLES['engineer']:
self.post_engineer_logic(last_log)
for log_index in range(len(self.data) - 1):
if self.data[log_index].new_role == ROLES['engineer']:
self.engineer_logic(log_index)
for log_index in range(len(self.data) - 1):
if self.data[log_index].new_role == ROLES['engineer']:
self.engineer_logic(log_index)
def engineer_logic(self, log_index):
"""
@ -238,7 +241,7 @@ class StatisticData:
"""
Функция обрабатывает случай, когда нам изветсно что инженер начал работу до диапазона
"""
self.fill_daterange(max(User.objects.get(email=self.email).date_joined.date(), self.start_date),
self.fill_daterange(max(get_user_model().objects.get(email=self.email).date_joined.date(), self.start_date),
first_log.change_time.date())
self.statistic[first_log.change_time.date()] += get_timedelta(first_log).total_seconds()

View File

@ -12,7 +12,7 @@
<p>Введте свой e-mail адрес для восстановления пароля.</p>
<form action="." method="post">
{{ form.as_p }}
<p><input class="btn btn-success" type="submit" value="Отпрваить e-mail"></p>
<p><input class="btn btn-success" type="submit" value="Отправить e-mail"></p>
{% csrf_token %}
</form>
{% endblock %}

View File

@ -1,7 +1,12 @@
"""
Тесты.
"""
import random
from unittest.mock import patch, Mock
from django.contrib.auth.models import User
from django.contrib.auth import get_user_model
from django.core import mail
from django.http import HttpResponseRedirect
from django.template.loader import render_to_string
@ -25,38 +30,56 @@ class UsersBaseTestCase(TestCase):
self.admin = 'admin@gmail.com'
self.engineer = 'customer@example.com'
self.agent_client = Client()
self.agent_client.force_login(User.objects.get(email=self.light_agent))
self.agent_client.force_login(get_user_model().objects.get(email=self.light_agent))
self.admin_client = Client()
self.admin_client.force_login(User.objects.get(email=self.admin))
self.admin_client.force_login(get_user_model().objects.get(email=self.admin))
self.engineer_client = Client()
self.engineer_client.force_login(User.objects.get(email=self.engineer))
self.engineer_client.force_login(get_user_model().objects.get(email=self.engineer))
class RegistrationTestCase(TestCase):
"""
Класс тестирования регистрации пользователя.
"""
fixtures = ['fixtures/data.json']
def setUp(self):
def setUp(self) -> None:
"""
Функция предтестовых настроек.
"""
self.email_backend = 'django.core.mail.backends.locmem.EmailBackend'
self.any_zendesk_user_email = 'idar.sokurov.05@mail.ru'
self.zendesk_admin_email = 'idar.sokurov.05@mail.ru'
self.client = Client()
def test_registration_complete_redirect(self):
def test_registration_complete_redirect(self) -> None:
"""
Функция тестирования успешно завершенной регистрации.
"""
with self.settings(EMAIL_BACKEND=self.email_backend):
resp = self.client.post(reverse('registration'), data={'email': self.any_zendesk_user_email})
self.assertRedirects(resp, reverse('password_reset_done'))
def test_registration_fail_redirect(self):
"""
Функция тестирования неуспешной регистрации.
"""
with self.settings(EMAIL_BACKEND=self.email_backend):
resp = self.client.post(reverse('registration'), data={'email': self.any_zendesk_user_email + 'asd'})
self.assertRedirects(resp, reverse('django_registration_disallowed'))
def test_registration_user_already_exist(self):
"""
Функция тестирования попытки регистрации уже зарегистрированного пользователя.
"""
with self.settings(EMAIL_BACKEND=self.email_backend) and translation.override('ru'):
resp = self.client.post(reverse('registration'), data={'email': '123@test.ru'})
self.assertContains(resp, 'Этот адрес электронной почты уже используется', count=1, status_code=200)
def test_registration_send_email(self):
"""
Функция тестирования отправки email.
"""
with self.settings(EMAIL_BACKEND=self.email_backend):
response: HttpResponseRedirect = \
self.client.post(reverse('registration'), data={'email': self.any_zendesk_user_email})
@ -72,49 +95,74 @@ class RegistrationTestCase(TestCase):
self.assertEqual(mail.outbox[0].body, correct_body)
def test_registration_user_creating(self):
"""
Функция тестирования регистрации пользователя (сверяем имя с именем в Zendesk.
"""
with self.settings(EMAIL_BACKEND=self.email_backend):
self.client.post(reverse('registration'), data={'email': self.any_zendesk_user_email})
user = User.objects.get(email=self.any_zendesk_user_email)
user = get_user_model().objects.get(email=self.any_zendesk_user_email)
zendesk_user = zenpy.get_user(self.any_zendesk_user_email)
self.assertEqual(user.userprofile.name, zendesk_user.name)
def test_permissions_applying(self):
"""
Функция тестирования проверке присвоения роли admin.
"""
with self.settings(EMAIL_BACKEND=self.email_backend):
self.client.post(reverse('registration'), data={'email': self.zendesk_admin_email})
user = User.objects.get(email=self.zendesk_admin_email)
user = get_user_model().objects.get(email=self.zendesk_admin_email)
self.assertEqual(user.userprofile.role, 'admin')
self.assertTrue(user.has_perm('main.has_control_access'))
class MakeEngineerTestCase(UsersBaseTestCase):
"""
Класс тестов для проверки функции назначения роли engineer.
"""
@patch('main.extra_func.zenpy')
def test_become_engineer_redirect(self, _zenpy_mock):
user = User.objects.get(email=self.light_agent)
"""
Функция проверки переадресации пользователя на рабочую страницу после назначения роли engineer.
"""
user = get_user_model().objects.get(email=self.light_agent)
resp = self.agent_client.post(reverse_lazy('work_become_engineer'))
self.assertRedirects(resp, reverse('work', args=[user.id]))
self.assertEqual(resp.status_code, 302)
self.assertFalse(_zenpy_mock.called)
@patch('main.extra_func.zenpy')
def test_light_agent_make_engineer(self, zenpy_mock):
"""
Функция проверки назначения light_agent на роль engineer.
"""
self.agent_client.post(reverse_lazy('work_become_engineer'))
self.assertEqual(zenpy_mock.update_user.call_args[0][0].custom_role_id, sets.ZENDESK_ROLES['engineer'])
@patch('main.extra_func.zenpy')
def test_admin_make_engineer(self, zenpy_mock):
"""
Функция проверки назначения admin на роль engineer.
"""
self.admin_client.post(reverse_lazy('work_become_engineer'))
self.assertEqual(zenpy_mock.update_user.call_args[0][0].custom_role_id, sets.ZENDESK_ROLES['engineer'])
@patch('main.extra_func.zenpy')
def test_engineer_make_engineer(self, zenpy_mock):
"""
Функция проверки назначения engineer на роль engineer.
"""
self.engineer_client.post(reverse_lazy('work_become_engineer'))
self.assertEqual(zenpy_mock.update_user.call_args[0][0].custom_role_id, sets.ZENDESK_ROLES['engineer'])
@patch('main.extra_func.zenpy')
def test_control_page_make_engineer_one(self, zenpy_mock):
"""
Функция проверки назначения администратором на роль engineer одного пользователя.
"""
self.admin_client.post(
reverse_lazy('control'),
data={'users': [User.objects.get(email=self.light_agent).userprofile.id], 'engineer': 'engineer'}
data={'users': [get_user_model().objects.get(email=self.light_agent).userprofile.id],
'engineer': 'engineer'}
)
call_list = zenpy_mock.update_user.call_args_list
mock_object = call_list[0][0][0]
@ -123,12 +171,15 @@ class MakeEngineerTestCase(UsersBaseTestCase):
@patch('main.extra_func.zenpy')
def test_control_page_make_engineer_many(self, zenpy_mock):
"""
Функция проверки назначения администратором на роль engineer нескольких пользователей.
"""
self.admin_client.post(
reverse_lazy('control'),
data={
'users': [
User.objects.get(email=self.light_agent).userprofile.id,
User.objects.get(email=self.engineer).userprofile.id,
get_user_model().objects.get(email=self.light_agent).userprofile.id,
get_user_model().objects.get(email=self.engineer).userprofile.id,
],
'engineer': 'engineer'
}
@ -145,7 +196,7 @@ class MakeLightAgentTestCase(UsersBaseTestCase):
@patch('main.requester.TicketListRequester.get_tickets_list_for_user', side_effect=[[]])
@patch('main.extra_func.zenpy')
def test_hand_over_redirect(self, _zenpy_mock, _user_tickets_mock):
user = User.objects.get(email=self.engineer)
user = get_user_model().objects.get(email=self.engineer)
resp = self.engineer_client.post(reverse_lazy('work_hand_over'))
self.assertRedirects(resp, reverse('work', args=[user.id]))
self.assertEqual(resp.status_code, 302)
@ -201,7 +252,10 @@ class MakeLightAgentTestCase(UsersBaseTestCase):
def test_control_page_make_light_agent_one(self, zenpy_mock, _user_tickets_mock):
self.admin_client.post(
reverse_lazy('control'),
data={'users': [User.objects.get(email=self.engineer).userprofile.id], 'light_agent': 'light_agent'}
data={
'users': [get_user_model().objects.get(email=self.engineer).userprofile.id],
'light_agent': 'light_agent'
}
)
call_list = zenpy_mock.update_user.call_args_list
mock_object = call_list[0][0][0]
@ -215,8 +269,8 @@ class MakeLightAgentTestCase(UsersBaseTestCase):
reverse_lazy('control'),
data={
'users': [
User.objects.get(email=self.light_agent).userprofile.id,
User.objects.get(email=self.engineer).userprofile.id,
get_user_model().objects.get(email=self.light_agent).userprofile.id,
get_user_model().objects.get(email=self.engineer).userprofile.id,
],
'light_agent': 'light_agent'
}
@ -229,18 +283,29 @@ class MakeLightAgentTestCase(UsersBaseTestCase):
class PasswordResetTestCase(UsersBaseTestCase):
"""
Класс тестов сброса пароля.
"""
def setUp(self):
"""
Предустановленные значения для проведения тестов.
"""
super().setUp()
self.email_backend = 'django.core.mail.backends.locmem.EmailBackend'
def test_redirect(self):
"""
Функция проверки переадресации на страницу уведомления о сбросе пароля на email.
"""
with self.settings(EMAIL_BACKEND=self.email_backend):
resp = self.agent_client.post(reverse_lazy('password_reset'), data={'email': self.light_agent})
self.assertRedirects(resp, reverse('password_reset_done'))
self.assertEqual(resp.status_code, 302)
def test_send_email(self):
"""
Функция проверки содержания и отправки письма для установки пароля.
"""
with self.settings(EMAIL_BACKEND=self.email_backend):
response: HttpResponseRedirect = \
self.agent_client.post(reverse_lazy('password_reset'), data={'email': self.light_agent})
@ -256,31 +321,53 @@ class PasswordResetTestCase(UsersBaseTestCase):
self.assertEqual(mail.outbox[0].body, correct_body)
def test_email_invalid(self):
"""
Функция проверки уведомления клиента о некорректности введенного email.
"""
with self.settings(EMAIL_BACKEND=self.email_backend) and translation.override('ru'):
resp = self.agent_client.post(reverse_lazy('password_reset'), data={'email': 1})
self.assertContains(resp, 'Введите правильный адрес электронной почты.', count=1, status_code=200)
def test_user_does_not_exist(self):
"""
Функция корректности отработки неверно введенного email.
"""
with self.settings(EMAIL_BACKEND=self.email_backend):
resp = self.agent_client.post(reverse_lazy('password_reset'), data={'email': self.light_agent + str(random.random())})
resp = self.agent_client.post(
reverse_lazy('password_reset'),
data={
'email': self.light_agent + str(random.random())
}
)
self.assertRedirects(resp, reverse('password_reset_done'))
self.assertEqual(resp.status_code, 302)
self.assertEqual(len(mail.outbox), 0)
class PasswordChangeTestCase(UsersBaseTestCase):
"""
Класс тестирования смены пароля.
"""
def setUp(self):
"""
Предустановленные значения для проведения тестов.
"""
super().setUp()
self.set_password()
def set_password(self):
user: User = User.objects.get(email=self.light_agent)
"""
Пароль, сформированный для тестирования.
"""
user: get_user_model() = get_user_model().objects.get(email=self.light_agent)
user.set_password('ImpossiblyHardPassword')
user.save()
self.agent_client.force_login(User.objects.get(email=self.light_agent))
self.agent_client.force_login(get_user_model().objects.get(email=self.light_agent))
def test_change_successful(self):
"""
Функция тестирования успешного изменения пароля.
"""
self.agent_client.post(
reverse_lazy('password_change'),
data={
@ -289,10 +376,13 @@ class PasswordChangeTestCase(UsersBaseTestCase):
'new_password2': 'EasyPassword',
}
)
user = User.objects.get(email=self.light_agent)
user = get_user_model().objects.get(email=self.light_agent)
self.assertTrue(user.check_password('EasyPassword'))
def test_invalid_old_password(self):
"""
Функция тестирования отработки неверно введенного старого пароля при смене.
"""
with translation.override('ru'):
resp = self.agent_client.post(
reverse_lazy('password_change'),
@ -305,6 +395,9 @@ class PasswordChangeTestCase(UsersBaseTestCase):
self.assertContains(resp, 'Ваш старый пароль введен неправильно', count=1, status_code=200)
def test_different_new_passwords(self):
"""
Функция тестирования случая с вводом двух разных новых паролей.
"""
with translation.override('ru'):
resp = self.agent_client.post(
reverse_lazy('password_change'),
@ -317,6 +410,9 @@ class PasswordChangeTestCase(UsersBaseTestCase):
self.assertContains(resp, 'Введенные пароли не совпадают', count=1, status_code=200)
def test_invalid_new_password1(self):
"""
Функция тестирования случая с неправильно подобранным новым паролем (слишком короткий).
"""
with translation.override('ru'):
resp = self.agent_client.post(
reverse_lazy('password_change'),
@ -329,6 +425,9 @@ class PasswordChangeTestCase(UsersBaseTestCase):
self.assertContains(resp, 'Введённый пароль слишком короткий', count=1, status_code=200)
def test_invalid_new_password2(self):
"""
Функция тестирования случая с неправильно подобранным новым паролем (употребляются только цифры).
"""
with translation.override('ru'):
resp = self.agent_client.post(
reverse_lazy('password_change'),
@ -341,6 +440,9 @@ class PasswordChangeTestCase(UsersBaseTestCase):
self.assertContains(resp, 'Введённый пароль состоит только из цифр', count=1, status_code=200)
def test_invalid_new_password3(self):
"""
Функция тестирования случая с неправильно подобранным новым паролем (совпадает с именем пользователя).
"""
with translation.override('ru'):
resp = self.agent_client.post(
reverse_lazy('password_change'),
@ -365,10 +467,11 @@ class GetTicketsTestCase(UsersBaseTestCase):
Функция проверки переадресации пользователя на рабочую страницу.
"""
get_user_mock.return_value = Mock()
user = User.objects.get(email=self.engineer)
user = get_user_model().objects.get(email=self.engineer)
resp = self.engineer_client.post(reverse('work_get_tickets'))
self.assertRedirects(resp, reverse('work', args=[user.id]))
self.assertEqual(resp.status_code, 302)
self.assertFalse(_zenpy_mock.called)
@patch('main.views.zenpy')
@patch('main.views.get_tickets_list_for_group')
@ -408,11 +511,11 @@ class GetTicketsTestCase(UsersBaseTestCase):
@patch('main.views.zenpy')
@patch('main.views.get_tickets_list_for_group')
def test_take_zero_tickets(self, TicketsMock, zenpy_mock):
def test_take_zero_tickets(self, tickets_mock, zenpy_mock):
"""
Функция проверки попытки назначения нуля тикета на engineer.
"""
TicketsMock.return_value = [Mock()] * 3
tickets_mock.return_value = [Mock()] * 3
zenpy_mock.get_user.return_value = Mock(role='agent', custom_role_id=sets.ZENDESK_ROLES['engineer'])
self.engineer_client.post(reverse('work_get_tickets'), data={'count_tickets': 0})
tickets = zenpy_mock.update_tickets.call_args[0][0]
@ -444,9 +547,9 @@ class ProfileTestCase(TestCase):
self.zendesk_agent_email = 'krav-88@mail.ru'
self.zendesk_admin_email = 'idar.sokurov.05@mail.ru'
self.client = Client()
self.client.force_login(User.objects.get(email=self.zendesk_agent_email))
self.client.force_login(get_user_model().objects.get(email=self.zendesk_agent_email))
self.admin_client = Client()
self.admin_client.force_login(User.objects.get(email=self.zendesk_admin_email))
self.admin_client.force_login(get_user_model().objects.get(email=self.zendesk_admin_email))
def test_correct_username(self):
"""
@ -495,15 +598,14 @@ class LoggingTestCase(UsersBaseTestCase):
def setUp(self):
super().setUp()
self.admin_profile = User.objects.get(email=self.admin).userprofile
self.agent_profile = User.objects.get(email=self.light_agent).userprofile
self.engineer_profile = User.objects.get(email=self.engineer).userprofile
self.admin_profile = get_user_model().objects.get(email=self.admin).userprofile
self.agent_profile = get_user_model().objects.get(email=self.light_agent).userprofile
self.engineer_profile = get_user_model().objects.get(email=self.engineer).userprofile
@staticmethod
def get_file_output():
file = open('logs/logs.csv', 'r')
file_output = file.readlines()[-1]
file.close()
with open('logs/logs.csv', 'r') as file:
file_output = file.readlines()[-1]
return file_output
def test_engineer_with_admin(self):

View File

@ -1,3 +1,7 @@
"""
REST framework adds support for automatic URL routing to Django.
"""
from rest_framework.routers import DefaultRouter
from main.views import UsersViewSet

View File

@ -1,10 +1,17 @@
"""
View функции.
"""
from smtplib import SMTPException
from typing import Dict, Any, Optional
from django.contrib import messages
from django.contrib.auth import get_user_model
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.models import Permission
from django.contrib.auth.tokens import default_token_generator
from django.contrib.auth.views import LoginView
from django.contrib.contenttypes.models import ContentType
@ -15,7 +22,7 @@ from django.shortcuts import render, redirect
from django.urls import reverse_lazy
from django.views.generic import FormView
from django_registration.views import RegistrationView
# Django REST
from rest_framework import viewsets
from rest_framework.response import Response
@ -23,14 +30,35 @@ from access_controller.settings import DEFAULT_FROM_EMAIL, ZENDESK_ROLES, ZENDES
from main.extra_func import check_user_exist, update_profile, get_user_organization, \
make_engineer, make_light_agent, get_users_list, update_users_in_model, count_users, \
set_session_params_for_work_page, get_tickets_list_for_group
from main.forms import AdminPageUsers, CustomRegistrationForm, CustomAuthenticationForm, StatisticForm, \
WorkGetTicketsForm
from main.forms import AdminPageUsers, CustomRegistrationForm, CustomAuthenticationForm, \
StatisticForm, WorkGetTicketsForm
from main.serializers import ProfileSerializer, ZendeskUserSerializer
from main.zendesk_admin import zenpy
from .models import UserProfile
from .statistic_data import StatisticData
def setup_context(**kwargs) -> Dict[str, Any]:
"""
Функция добавления в контекст статуса пользователя.
:param profile_lit: True, при создании профиля пользователя, иначе False
:param control_lit: False
:param work_lit: True, при установке пользователю рабочей роли, иначе False
:param registration_lit: True, при регистрации пользователя, иначе False
:param login_lit: True, если пользователь залогинен, иначе False
:param stats_lit: True, при получении пользователем прав администратора (просмотр статистики), иначе False
:return: Контекст (context)
"""
context = {}
for key in ('profile_lit', 'control_lit', 'work_lit', 'registration_lit', 'login_lit', 'stats_lit'):
if key in kwargs:
context.update({key: True})
else:
context.update({key: False})
return context
class CustomRegistrationView(RegistrationView):
"""
Отображение и логика работы страницы регистрации пользователя.
@ -41,9 +69,11 @@ class CustomRegistrationView(RegistrationView):
:type template_name: :class:`str`
:param success_url: Указание пути к html-странице завершения регистрации
:type success_url: :class:`django.utils.functional.lazy.<locals>.__proxy__`
:param is_allowed: Определение зарегистрирован ли пользователь с введенным email на Zendesk и принадлежит ли он к организации SYSTEM
:param is_allowed: Определение зарегистрирован ли пользователь с введенным email на Zendesk и
принадлежит ли он к организации SYSTEM
:type is_allowed: :class:`bool`
"""
extra_context = setup_context(registration_lit=True)
form_class = CustomRegistrationForm
template_name = 'django_registration/registration_form.html'
urls = {
@ -53,7 +83,7 @@ class CustomRegistrationView(RegistrationView):
}
redirect_url = 'done'
def register(self, form: CustomRegistrationForm) -> User:
def register(self, form: CustomRegistrationForm) -> Optional[get_user_model()]:
"""
Функция регистрации пользователя.
1. Ввод email пользователя, указанный на Zendesk
@ -62,7 +92,7 @@ class CustomRegistrationView(RegistrationView):
3. Создается пользователь class User, а также его профиль.
:param form: Email пользователя на Zendesk
:return: user
:return: User
"""
self.redirect_url = 'done'
if check_user_exist(form.data['email']) and get_user_organization(form.data['email']) == 'SYSTEM':
@ -78,10 +108,10 @@ class CustomRegistrationView(RegistrationView):
'html_email_template_name': None,
'extra_email_context': None,
}
user = User.objects.create_user(
user = get_user_model().objects.create_user(
username=form.data['email'],
email=form.data['email'],
password=User.objects.make_random_password(length=50)
password=get_user_model().objects.make_random_password(length=50)
)
try:
update_profile(user.userprofile)
@ -90,13 +120,16 @@ class CustomRegistrationView(RegistrationView):
return user
except SMTPException:
self.redirect_url = 'email_sending_error'
return None
else:
self.redirect_url = 'email_sending_error'
return None
else:
self.redirect_url = 'invalid_zendesk_email'
return None
@staticmethod
def set_permission(user: User) -> None:
def set_permission(user: get_user_model()) -> None:
"""
Функция дает разрешение на просмотр страница администратора, если пользователь имеет роль admin.
@ -110,7 +143,7 @@ class CustomRegistrationView(RegistrationView):
)
user.user_permissions.add(permission)
def get_success_url(self, user: User = None):
def get_success_url(self, user: get_user_model() = None) -> Dict:
"""
Функция возвращает url-адрес страницы, куда нужно перейти после успешной/не успешной регистрации.
Используется самой django-registration.
@ -121,7 +154,13 @@ class CustomRegistrationView(RegistrationView):
return self.urls[self.redirect_url]
def registration_error(request):
def registration_error(request: WSGIRequest) -> HttpResponse:
"""
Функция отображения страницы ошибки регистрации.
:param request: регистрация
:return: адресация на страницу ошибки
"""
return render(request, 'django_registration/registration_error.html')
@ -144,7 +183,7 @@ def profile_page(request: WSGIRequest) -> HttpResponse:
@login_required()
def work_page(request: WSGIRequest, id: int) -> HttpResponse:
def work_page(request: WSGIRequest, required_id: int) -> HttpResponse:
"""
Функция отображения страницы "Управления правами" для текущего пользователя (login_required).
@ -153,7 +192,7 @@ def work_page(request: WSGIRequest, id: int) -> HttpResponse:
:return: адресация на страницу "Управления правами" (либо на страницу "Авторизации", если id и user.id не совпадают
"""
users = get_users_list()
if request.user.id == id:
if request.user.id == required_id:
if request.session.get('is_confirm', None):
messages.success(request, 'Изменения были применены')
elif request.session.get('is_confirm', None) is not None:
@ -184,7 +223,7 @@ def work_page(request: WSGIRequest, id: int) -> HttpResponse:
@login_required()
def work_hand_over(request: WSGIRequest):
def work_hand_over(request: WSGIRequest) -> HttpResponseRedirect:
"""
Функция позволяет текущему пользователю сдать права, а именно сменить в Zendesk роль с "engineer" на "light_agent"
@ -198,18 +237,23 @@ def work_hand_over(request: WSGIRequest):
@login_required()
def work_become_engineer(request: WSGIRequest) -> HttpResponseRedirect:
"""
Функция позволяет текущему пользователю получить права, а именно сменить в Zendesk роль с "light_agent" на "engineer"
Функция позволяет текущему пользователю получить права, а именно сменить в Zendesk роль с "light_agent"
на "engineer".
:param request: данные текущего пользователя (login_required)
:return: перезагрузка текущей страницы после выполнения смены роли
"""
make_engineer(request.user.userprofile, request.user)
return set_session_params_for_work_page(request)
@login_required()
def work_get_tickets(request):
def work_get_tickets(request: WSGIRequest) -> HttpResponse:
"""
:param request:
:return:
"""
zenpy_user = zenpy.get_user(request.user.email)
if request.method == 'POST':
@ -266,7 +310,7 @@ class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageM
self.make_light_agents(users)
return super().form_valid(form)
def make_engineers(self, users):
def make_engineers(self, users: list) -> None:
"""
Функция проходит по списку пользователей, проставляя статус "engineer".
@ -276,7 +320,7 @@ class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageM
for user in users:
make_engineer(user, self.request.user)
def make_light_agents(self, users):
def make_light_agents(self, users: list) -> None:
"""
Функция проходит по списку пользователей, проставляя статус "light agent".
@ -291,17 +335,30 @@ class CustomLoginView(LoginView):
"""
Отображение страницы авторизации пользователя
"""
extra_context = setup_context(login_lit=True)
form_class = CustomAuthenticationForm
class UsersViewSet(viewsets.ReadOnlyModelViewSet):
"""
Класс для получения пользователей с помощью api
Класс для получения пользователей с помощью api.
:param queryset: Список пользователей с ролью 'agent'
:type queryset: :class:`str`
:param serializer_class: Класс сериализатор для модели профиля пользователя
:type serializer_class: :class:`ProfileSerializer`
"""
queryset = UserProfile.objects.filter(role='agent')
serializer_class = ProfileSerializer
def list(self, request, *args, **kwargs):
def list(self, request: WSGIRequest, *args, **kwargs) -> Response:
"""
Функция возвращает список пользователей, список пользователей Zendesk, количество engineers и light-agents.
:param request: Запрос
:param args: Аргументы
:param kwargs: Параметры
:return: Список пользователей
"""
users = update_users_in_model()
count = count_users(users.values)
profiles = UserProfile.objects.filter(role='agent')
@ -316,7 +373,13 @@ class UsersViewSet(viewsets.ReadOnlyModelViewSet):
return Response(res)
@staticmethod
def choose_users(zendesk, model):
def choose_users(zendesk: list, model: list) -> list:
"""
Функция формирует список пользователей, которые не зарегистрированы у нас.
:param zendesk: Список пользователей Zendesk
:param model: Список пользователей (модель Userprofile)
:return: Список
"""
users = []
for zendesk_user in zendesk:
if zendesk_user.name not in [user.name for user in model]:
@ -324,7 +387,12 @@ class UsersViewSet(viewsets.ReadOnlyModelViewSet):
return users
@staticmethod
def get_zendesk_users(users):
def get_zendesk_users(users: list) -> list:
"""
Получение списка пользователей Zendesk, не являющихся админами.
:param users: Список пользователей
:return: Список пользователей, не являющимися администраторами.
"""
zendesk_users = ZendeskUserSerializer(
data=[user for user in users if user.role != 'admin'],
many=True

View File

@ -1,3 +1,7 @@
"""
Функционал работы администратора Zendesk.
"""
from typing import Optional, Dict, List
from zenpy import Zenpy
@ -10,17 +14,23 @@ from access_controller.settings import ACTRL_ZENDESK_SUBDOMAIN, ACTRL_API_EMAIL,
class ZendeskAdmin:
"""
Класс **ZendeskAdmin** существует, чтобы в каждой функции отдельно не проверять аккаунт администратора.
Класс **ZendeskAdmin** содержит описание всего функционала администратора.
:param credentials: Полномочия (первым указывается учетная запись организации в Zendesk)
:type credentials: :class:`Dict[str, str]`
:param admin: Администратор
:type admin: :class:`Zenpy`
:param buffer_group_id: ID буферной группы
:type buffer_group_id: :class:`int`
:param solved_tickets_user_id: ID пользователя, который решил тикет
:type solved_tickets_user_id: :class:`int`
"""
def __init__(self, credentials: Dict[str, str]):
self.credentials = credentials
self.admin = self.create_admin()
self.buffer_group_id: int = self.get_group(ZENDESK_GROUPS['buffer']).id
self.solved_tickets_user_id: int = self.get_user(SOLVED_TICKETS_EMAIL).id
self.buffer_group_id= self.get_group(ZENDESK_GROUPS['buffer']).id
self.solved_tickets_user_id = self.get_user(SOLVED_TICKETS_EMAIL).id
def update_user(self, user: ZenpyUser) -> bool:
"""
@ -46,7 +56,7 @@ class ZendeskAdmin:
:param email: Email пользователя
:return: Является ли зарегистрированным
"""
return True if self.admin.search(email, type='user') else False
return bool(self.admin.search(email, type='user'))
def get_user(self, email: str) -> ZenpyUser:
"""
@ -96,9 +106,8 @@ class ZendeskAdmin:
admin = Zenpy(**self.credentials)
try:
admin.search(self.credentials['email'], type='user')
except APIException:
raise ValueError('invalid access_controller`s login data')
except APIException as invalid_data:
raise ValueError('invalid access_controller`s login data') from invalid_data
return admin

View File

@ -7,6 +7,12 @@ sphinx-rtd-theme==0.5.2
sphinx-autodoc-typehints==1.12.0
pyenchant==3.2.0
sphinxcontrib-spelling==7.2.1
m2r == 0.2.1
# Tests
coverage==5.5
# Code style
pylint == 2.8.2
pylint-django == 2.4.4
autopep8 == 1.5.6

View File

@ -0,0 +1,185 @@
# pyenchant
#
# Copyright (C) 2004-2008, Ryan Kelly
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
# Boston, MA 02111-1307, USA.
#
# In addition, as a special exception, you are
# given permission to link the code of this program with
# non-LGPL Spelling Provider libraries (eg: a MSFT Office
# spell checker backend) and distribute linked combinations including
# the two. You must obey the GNU Lesser General Public License in all
# respects for all of the code used other than said providers. If you modify
# this file, you may extend this exception to your version of the
# file, but you are not obligated to do so. If you do not wish to
# do so, delete this exception statement from your version.
#
"""
enchant.tokenize.en: Tokenizer for the English language
This module implements a PyEnchant text tokenizer for the English
language, based on very simple rules.
"""
import unicodedata
import enchant.tokenize
class tokenize(enchant.tokenize.tokenize): # noqa: N801
"""Iterator splitting text into words, reporting position.
This iterator takes a text string as input, and yields tuples
representing each distinct word found in the text. The tuples
take the form:
(<word>,<pos>)
Where <word> is the word string found and <pos> is the position
of the start of the word within the text.
The optional argument <valid_chars> may be used to specify a
list of additional characters that can form part of a word.
By default, this list contains only the apostrophe ('). Note that
these characters cannot appear at the start or end of a word.
"""
_DOC_ERRORS = ["pos", "pos"]
def __init__(self, text, valid_chars=None):
self._valid_chars = valid_chars
self._text = text
self._offset = 0
# Select proper implementation of self._consume_alpha.
# 'text' isn't necessarily a string (it could be e.g. a mutable array)
# so we can't use isinstance(text, str) to detect unicode.
# Instead we typetest the first character of the text.
# If there's no characters then it doesn't matter what implementation
# we use since it won't be called anyway.
try:
char1 = text[0]
except IndexError:
self._initialize_for_binary()
else:
if isinstance(char1, str):
self._initialize_for_unicode()
else:
self._initialize_for_binary()
def _initialize_for_binary(self):
self._consume_alpha = self._consume_alpha_b
if self._valid_chars is None:
self._valid_chars = ("'",)
def _initialize_for_unicode(self):
self._consume_alpha = self._consume_alpha_u
if self._valid_chars is None:
# XXX TODO: this doesn't seem to work correctly with the
# MySpell provider, disabling for now.
# Allow unicode typographic apostrophe
# self._valid_chars = (u"'",u"\u2019")
self._valid_chars = ("'",)
def _consume_alpha_b(self, text, offset):
"""Consume an alphabetic character from the given bytestring.
Given a bytestring and the current offset, this method returns
the number of characters occupied by the next alphabetic character
in the string. Non-ASCII bytes are interpreted as utf-8 and can
result in multiple characters being consumed.
"""
assert offset < len(text)
if text[offset].isalpha():
return 1
elif text[offset] >= "\x80":
return self._consume_alpha_utf8(text, offset)
return 0
def _consume_alpha_utf8(self, text, offset):
"""Consume a sequence of utf8 bytes forming an alphabetic character."""
incr = 2
u = ""
while not u and incr <= 4:
try:
try:
# In the common case this will be a string
u = text[offset : offset + incr].decode("utf8")
except AttributeError:
# Looks like it was e.g. a mutable char array.
try:
s = text[offset : offset + incr].tostring()
except AttributeError:
s = "".join([c for c in text[offset : offset + incr]])
u = s.decode("utf8")
except UnicodeDecodeError:
incr += 1
if not u:
return 0
if u.isalpha():
return incr
if unicodedata.category(u)[0] == "M":
return incr
return 0
def _consume_alpha_u(self, text, offset):
"""Consume an alphabetic character from the given unicode string.
Given a unicode string and the current offset, this method returns
the number of characters occupied by the next alphabetic character
in the string. Trailing combining characters are consumed as a
single letter.
"""
assert offset < len(text)
incr = 0
if text[offset].isalpha():
incr = 1
while offset + incr < len(text):
if unicodedata.category(text[offset + incr])[0] != "M":
break
incr += 1
return incr
def next(self):
text = self._text
offset = self._offset
while offset < len(text):
# Find start of next word (must be alpha)
while offset < len(text):
incr = self._consume_alpha(text, offset)
if incr:
break
offset += 1
cur_pos = offset
# Find end of word using, allowing valid_chars
while offset < len(text):
incr = self._consume_alpha(text, offset)
if not incr:
if text[offset] in self._valid_chars:
incr = 1
else:
break
offset += incr
# Return if word isn't empty
if cur_pos != offset:
# Make sure word doesn't end with a valid_char
while text[offset - 1] in self._valid_chars:
offset = offset - 1
self._offset = offset
return (text[cur_pos:offset], cur_pos)
self._offset = offset
raise StopIteration()