Merge branch 'develop' of https://gitlab.informatics.ru/2020-2021/online/s101/group-02/access_controller into develop
This commit is contained in:
commit
709718ec44
14
.coveragerc
Normal file
14
.coveragerc
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
[run]
|
||||||
|
command_line = manage.py test
|
||||||
|
|
||||||
|
branch = true
|
||||||
|
|
||||||
|
source =
|
||||||
|
main/
|
||||||
|
|
||||||
|
|
||||||
|
omit =
|
||||||
|
main/migrations/*
|
||||||
|
main/apps.py
|
||||||
|
main/tests.py
|
||||||
|
|
10
.env.example
10
.env.example
@ -3,8 +3,8 @@ ACTRL_DEBUG=1
|
|||||||
ACTRL_SECRET_KEY="v1i_fb\$_jf2#1v_lcsbu&eon4u-os0^px=s^iycegdycqy&5)6"
|
ACTRL_SECRET_KEY="v1i_fb\$_jf2#1v_lcsbu&eon4u-os0^px=s^iycegdycqy&5)6"
|
||||||
ACTRL_HOST="actrl.example.com"
|
ACTRL_HOST="actrl.example.com"
|
||||||
|
|
||||||
ACTRL_EMAIL_HOST="smtp.mail.ru"
|
ACTRL_EMAIL_HOST="smtp.gmail.com"
|
||||||
ACTRL_EMAIL_PORT=2525
|
ACTRL_EMAIL_PORT=587
|
||||||
ACTRL_EMAIL_TLS=1
|
ACTRL_EMAIL_TLS=1
|
||||||
ACTRL_EMAIL_HOST_USER="djgr.02@mail.ru"
|
ACTRL_EMAIL_HOST_USER="djgr.02@mail.ru"
|
||||||
ACTRL_EMAIL_HOST_PASSWORD="djangogroup02"
|
ACTRL_EMAIL_HOST_PASSWORD="djangogroup02"
|
||||||
@ -20,6 +20,6 @@ LICENSE_NO=3
|
|||||||
SHIFTH=12
|
SHIFTH=12
|
||||||
|
|
||||||
ACTRL_ZENDESK_SUBDOMAIN="ngenix1612197338"
|
ACTRL_ZENDESK_SUBDOMAIN="ngenix1612197338"
|
||||||
ACTRL_API_EMAIL="email@example.com"
|
ACTRL_API_EMAIL="stepanenko_olga@mail.ru"
|
||||||
ACTRL_API_TOKEN="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
ACTRL_API_TOKEN="X1x4QeNa4xRdul2rTIKhac98AsXMwd5bOGAyZOtU"
|
||||||
ACTRL_API_PASSWORD=""
|
|
||||||
|
23
.gitlab-ci.yml
Normal file
23
.gitlab-ci.yml
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
image: python:3-alpine
|
||||||
|
|
||||||
|
stages:
|
||||||
|
- test
|
||||||
|
|
||||||
|
django_test:
|
||||||
|
stage: test
|
||||||
|
before_script:
|
||||||
|
- pip install -r requirements/dev.txt
|
||||||
|
script:
|
||||||
|
- python manage.py test
|
||||||
|
|
||||||
|
coverage:
|
||||||
|
stage: test
|
||||||
|
before_script:
|
||||||
|
- pip install -r requirements/dev.txt
|
||||||
|
script:
|
||||||
|
- coverage run
|
||||||
|
- coverage report -m
|
||||||
|
- coverage html -d public/coverage
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- public/coverage
|
627
.pylintrc
Normal file
627
.pylintrc
Normal 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
|
30
README.md
30
README.md
@ -1,5 +1,3 @@
|
|||||||
# ZenDesk Access Controller
|
|
||||||
|
|
||||||
## Управление правами доступа
|
## Управление правами доступа
|
||||||
|
|
||||||
Идея - написать программу(Web приложение), которая будет выдавать права пользователям системы по запросу самого
|
Идея - написать программу(Web приложение), которая будет выдавать права пользователям системы по запросу самого
|
||||||
@ -39,7 +37,7 @@
|
|||||||
|
|
||||||
|
|
||||||
## Quickstart
|
## Quickstart
|
||||||
Перед запуском требуется неообходимо `.env` файл.
|
Перед запуском необходимо создать `.env` файл.
|
||||||
```bash
|
```bash
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
```
|
```
|
||||||
@ -71,7 +69,7 @@ pip install -r requirements/dev.txt
|
|||||||
```
|
```
|
||||||
ACTRL_DEBUG={0/1} - включить режим дебага
|
ACTRL_DEBUG={0/1} - включить режим дебага
|
||||||
ACTRL_HOST={HOSTNAME} - при запуске без дебага, надо указать домен на котором будет работать приложение
|
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_HOST={SMTP_HOST} - домен почтового сервера через который приложение будет отправлять письма, например "smtp.gmail.com"
|
||||||
ACTRL_EMAIL_PORT={SMTP_PORT} - порт для почтового сервера, например 587, 465 , 2525
|
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 \
|
-v {ABSOLUTE_PATH_TO_DB}:/zendesk-access-controller/db \
|
||||||
access_controller:latest
|
access_controller:latest
|
||||||
```
|
```
|
||||||
- открываем запущеный контейнер в браузере (можно перейти по ссылке http://localhost:8000/)
|
- открываем запущенный контейнер в браузере (можно перейти по ссылке http://localhost:8000/)
|
||||||
|
|
||||||
|
|
||||||
## Запуск с тестовыми юзерами:
|
## Запуск с тестовыми юзерами:
|
||||||
@ -144,12 +142,32 @@ docker run -d -p 8000:8000 \
|
|||||||
- Пользователь - `123@test.ru` / `zendeskuser`
|
- Пользователь - `123@test.ru` / `zendeskuser`
|
||||||
|
|
||||||
Не сработает если домен песочницы отличается от `ngenix1612197338` (на другом домене нужно будет создать сначала пользователей в песочнице с правами админа и легкого агента
|
Не сработает если домен песочницы отличается от `ngenix1612197338` (на другом домене нужно будет создать сначала пользователей в песочнице с правами админа и легкого агента
|
||||||
с этими же почтами, назначить им организацию `SYSTEM`)
|
с этими же email, назначить им организацию `SYSTEM`)
|
||||||
|
|
||||||
|
|
||||||
## Параметры тестовой песочницы:
|
## Параметры тестовой песочницы:
|
||||||
Пример полной конфигурации можно найти в [.env.example](.env.example). Почту и токен админа ZenDesk взять у руководителя (если вы не админ).
|
Пример полной конфигурации можно найти в [.env.example](.env.example). Почту и токен админа ZenDesk взять у руководителя (если вы не админ).
|
||||||
|
|
||||||
|
## Для проверки pylint используем:
|
||||||
|
```bash
|
||||||
|
pylint --django-settings-module=access_controller.settings main
|
||||||
|
```
|
||||||
|
|
||||||
|
## Для приведения файлов к стандарту PEP8 используем:
|
||||||
|
```bash
|
||||||
|
autopep8 --in-place filename
|
||||||
|
```
|
||||||
|
|
||||||
|
##Для проверки орфографии:
|
||||||
|
```bash
|
||||||
|
(cd docs && make spelling)
|
||||||
|
```
|
||||||
|
|
||||||
|
##Для обновления документации:
|
||||||
|
```bash
|
||||||
|
m2r README.md
|
||||||
|
(cd docs && make html)
|
||||||
|
```
|
||||||
|
|
||||||
## Read more
|
## Read more
|
||||||
- Zenpy: [http://docs.facetoe.com.au](http://docs.facetoe.com.au)
|
- Zenpy: [http://docs.facetoe.com.au](http://docs.facetoe.com.au)
|
||||||
|
204
README.rst
Normal file
204
README.rst
Normal 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/>`_
|
@ -8,10 +8,7 @@ https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'access_controller.settings')
|
|
||||||
from django.core.asgi import get_asgi_application
|
from django.core.asgi import get_asgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'access_controller.settings')
|
||||||
application = get_asgi_application()
|
application = get_asgi_application()
|
||||||
|
@ -1,19 +1,31 @@
|
|||||||
|
"""
|
||||||
|
Авторизация пользователя.
|
||||||
|
"""
|
||||||
from django.contrib.auth.backends import ModelBackend
|
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):
|
class EmailAuthBackend(ModelBackend):
|
||||||
|
"""
|
||||||
|
Класс авторизации пользователя по email.
|
||||||
|
"""
|
||||||
def authenticate(self, request, username=None, password=None, **kwargs):
|
def authenticate(self, request, username=None, password=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Функция получения пользователя (модель User) по email.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
user = User.objects.get(email=username)
|
user = get_user_model().objects.get(email=username)
|
||||||
if user.check_password(password):
|
if user.check_password(password):
|
||||||
return user
|
return user
|
||||||
return None
|
return None
|
||||||
except User.DoesNotExist:
|
except get_user_model().DoesNotExist:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_user(self, user_id):
|
def get_user(self, user_id):
|
||||||
|
"""
|
||||||
|
Функция получения пользователя по id.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
return User.objects.get(pk=user_id)
|
return get_user_model().objects.get(pk=user_id)
|
||||||
except User.DoesNotExist:
|
except get_user_model().DoesNotExist:
|
||||||
return None
|
return None
|
||||||
|
@ -14,6 +14,8 @@ from pathlib import Path
|
|||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
|
|
||||||
|
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
# Quick-start development settings - unsuitable for production
|
# Quick-start development settings - unsuitable for production
|
||||||
@ -26,7 +28,7 @@ load_dotenv()
|
|||||||
SECRET_KEY = os.getenv('ACTRL_SECRET_KEY', 'empty')
|
SECRET_KEY = os.getenv('ACTRL_SECRET_KEY', 'empty')
|
||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
# 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 = [
|
ALLOWED_HOSTS = [
|
||||||
'127.0.0.1',
|
'127.0.0.1',
|
||||||
@ -62,8 +64,8 @@ ROOT_URLCONF = 'access_controller.urls'
|
|||||||
|
|
||||||
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||||
EMAIL_HOST = os.getenv('ACTRL_EMAIL_HOST', 'smtp.gmail.com')
|
EMAIL_HOST = os.getenv('ACTRL_EMAIL_HOST', 'smtp.gmail.com')
|
||||||
EMAIL_PORT = int(os.getenv('ACTRL_EMAIL_PORT', 587))
|
EMAIL_PORT = int(os.getenv('ACTRL_EMAIL_PORT', '587'))
|
||||||
EMAIL_USE_TLS = bool(int(os.getenv('ACTRL_EMAIL_TLS', 1)))
|
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_USER = os.getenv('ACTRL_EMAIL_HOST_USER', 'group02django@gmail.com')
|
||||||
EMAIL_HOST_PASSWORD = os.getenv('ACTRL_EMAIL_HOST_PASSWORD', 'djangogroup02')
|
EMAIL_HOST_PASSWORD = os.getenv('ACTRL_EMAIL_HOST_PASSWORD', 'djangogroup02')
|
||||||
DEFAULT_FROM_EMAIL = os.getenv('ACTRL_FROM_EMAIL', EMAIL_HOST_USER)
|
DEFAULT_FROM_EMAIL = os.getenv('ACTRL_FROM_EMAIL', EMAIL_HOST_USER)
|
||||||
@ -144,6 +146,8 @@ ACCOUNT_ACTIVATION_DAYS = 7
|
|||||||
LOGIN_REDIRECT_URL = '/'
|
LOGIN_REDIRECT_URL = '/'
|
||||||
LOGOUT_REDIRECT_URL = '/'
|
LOGOUT_REDIRECT_URL = '/'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Название_приложения.Название_файла.Название_класса_обработчика
|
# Название_приложения.Название_файла.Название_класса_обработчика
|
||||||
AUTHENTICATION_BACKENDS = [
|
AUTHENTICATION_BACKENDS = [
|
||||||
'access_controller.auth.EmailAuthBackend',
|
'access_controller.auth.EmailAuthBackend',
|
||||||
@ -154,8 +158,8 @@ AUTHENTICATION_BACKENDS = [
|
|||||||
|
|
||||||
|
|
||||||
ZENDESK_ROLES = {
|
ZENDESK_ROLES = {
|
||||||
'engineer': int(os.getenv('ENG_CROLE_ID', 0)),
|
'engineer': int(os.getenv('ENG_CROLE_ID', '0')),
|
||||||
'light_agent': int(os.getenv('LA_CROLE_ID', 0)),
|
'light_agent': int(os.getenv('LA_CROLE_ID', '0')),
|
||||||
}
|
}
|
||||||
|
|
||||||
ZENDESK_GROUPS = {
|
ZENDESK_GROUPS = {
|
||||||
@ -165,7 +169,7 @@ ZENDESK_GROUPS = {
|
|||||||
|
|
||||||
SOLVED_TICKETS_EMAIL = os.getenv('ST_EMAIL')
|
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 = {
|
REST_FRAMEWORK = {
|
||||||
# Use Django's standard `django.contrib.auth` permissions,
|
# 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_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')
|
ACTRL_API_EMAIL = os.getenv('ACTRL_API_EMAIL') or os.getenv('ACCESS_CONTROLLER_API_EMAIL')
|
||||||
|
@ -16,10 +16,11 @@ Including another URLconf
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import path, include
|
from django.urls import path, include
|
||||||
|
|
||||||
|
from main.urls import router
|
||||||
from main.views import main_page, profile_page, CustomRegistrationView, CustomLoginView, registration_error
|
from main.views import main_page, profile_page, CustomRegistrationView, CustomLoginView, registration_error
|
||||||
|
from main.views import registration_failed
|
||||||
from main.views import work_page, work_hand_over, work_become_engineer, work_get_tickets, \
|
from main.views import work_page, work_hand_over, work_become_engineer, work_get_tickets, \
|
||||||
AdminPageView, statistic_page
|
AdminPageView, statistic_page
|
||||||
from main.urls import router
|
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
@ -30,11 +31,12 @@ urlpatterns = [
|
|||||||
path('accounts/register/error/', registration_error, name='registration_email_error'),
|
path('accounts/register/error/', registration_error, name='registration_email_error'),
|
||||||
path('accounts/login/', CustomLoginView.as_view(), name='login'),
|
path('accounts/login/', CustomLoginView.as_view(), name='login'),
|
||||||
path('accounts/', include('django.contrib.auth.urls')),
|
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/hand_over/', work_hand_over, name="work_hand_over"),
|
||||||
path('work/become_engineer/', work_become_engineer, name="work_become_engineer"),
|
path('work/become_engineer/', work_become_engineer, name="work_become_engineer"),
|
||||||
path('work/get_tickets', work_get_tickets, name='work_get_tickets'),
|
path('work/get_tickets', work_get_tickets, name='work_get_tickets'),
|
||||||
path('accounts/', include('django_registration.backends.activation.urls')),
|
path('accounts/', include('django_registration.backends.activation.urls')),
|
||||||
|
path('registration_failed/', registration_failed, name='registration_failed'),
|
||||||
path('control/', AdminPageView.as_view(), name='control'),
|
path('control/', AdminPageView.as_view(), name='control'),
|
||||||
path('statistic/', statistic_page, name='statistic'),
|
path('statistic/', statistic_page, name='statistic'),
|
||||||
]
|
]
|
||||||
@ -42,4 +44,5 @@ urlpatterns = [
|
|||||||
# Django REST
|
# Django REST
|
||||||
urlpatterns += [
|
urlpatterns += [
|
||||||
path('api/', include(router.urls))
|
path('api/', include(router.urls))
|
||||||
|
|
||||||
]
|
]
|
||||||
|
@ -6,7 +6,7 @@ Models
|
|||||||
*******
|
*******
|
||||||
|
|
||||||
.. automodule:: main.models
|
.. automodule:: main.models
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
|
||||||
******
|
******
|
||||||
@ -33,14 +33,6 @@ Serializers
|
|||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
|
||||||
***************
|
|
||||||
API functions
|
|
||||||
***************
|
|
||||||
|
|
||||||
.. automodule:: main.apiauth
|
|
||||||
:members:
|
|
||||||
|
|
||||||
|
|
||||||
*****
|
*****
|
||||||
Views
|
Views
|
||||||
*****
|
*****
|
||||||
|
@ -12,16 +12,12 @@
|
|||||||
#
|
#
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import importlib
|
|
||||||
import inspect
|
import inspect
|
||||||
import enchant
|
import enchant
|
||||||
from enchant import checker
|
import django
|
||||||
|
|
||||||
|
|
||||||
sys.path.insert(0, os.path.abspath('../../'))
|
sys.path.insert(0, os.path.abspath('../../'))
|
||||||
|
|
||||||
import django
|
|
||||||
|
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'access_controller.settings')
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'access_controller.settings')
|
||||||
os.environ.setdefault('DJANGO_CONFIGURATION', 'Dev')
|
os.environ.setdefault('DJANGO_CONFIGURATION', 'Dev')
|
||||||
|
|
||||||
@ -39,7 +35,6 @@ from django.db.models.query import QuerySet
|
|||||||
|
|
||||||
QuerySet.__repr__ = lambda self: self.__class__.__name__
|
QuerySet.__repr__ = lambda self: self.__class__.__name__
|
||||||
|
|
||||||
|
|
||||||
django.setup()
|
django.setup()
|
||||||
|
|
||||||
# -- Project information -----------------------------------------------------
|
# -- 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__))
|
lines.append(':type %s: %s.%s' % (field.attname, module, field_type.__name__))
|
||||||
if enchant is not None:
|
if enchant is not None:
|
||||||
lines += spelling_white_list
|
lines += spelling_white_list
|
||||||
|
lines.append('')
|
||||||
return lines
|
return lines
|
||||||
|
|
||||||
|
|
||||||
@ -116,13 +112,17 @@ def skip_queryset(app, what, name, obj, skip, options):
|
|||||||
return skip
|
return skip
|
||||||
|
|
||||||
|
|
||||||
# def setup(app):
|
def fix_sig(app, what, name, obj, options, signature, return_annotation):
|
||||||
# # Register the docstring processor with sphinx
|
return "", ""
|
||||||
# app.connect('autodoc-process-docstring', process_django_models)
|
|
||||||
# app.connect('autodoc-skip-member', skip_queryset)
|
|
||||||
# app.connect('autodoc-process-docstring', process_modules)
|
|
||||||
|
|
||||||
|
|
||||||
|
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
|
# Add any Sphinx extension module names here, as strings. They can be
|
||||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||||
@ -132,16 +132,13 @@ extensions = {
|
|||||||
'sphinx.ext.autodoc',
|
'sphinx.ext.autodoc',
|
||||||
'sphinx.ext.intersphinx',
|
'sphinx.ext.intersphinx',
|
||||||
'sphinx.ext.viewcode',
|
'sphinx.ext.viewcode',
|
||||||
'sphinx.ext.napoleon',
|
|
||||||
'sphinx_rtd_theme',
|
'sphinx_rtd_theme',
|
||||||
'sphinx.ext.graphviz',
|
'sphinx.ext.graphviz',
|
||||||
'sphinx.ext.inheritance_diagram',
|
'sphinx.ext.inheritance_diagram',
|
||||||
'sphinx_autodoc_typehints',
|
'sphinx_autodoc_typehints',
|
||||||
'sphinxcontrib.spelling'
|
'sphinxcontrib.spelling',
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# Add any paths that contain templates here, relative to this directory.
|
# Add any paths that contain templates here, relative to this directory.
|
||||||
templates_path = ['_templates']
|
templates_path = ['_templates']
|
||||||
|
|
||||||
@ -171,6 +168,7 @@ html_static_path = ['_static']
|
|||||||
|
|
||||||
# -- Extension configuration -------------------------------------------------
|
# -- Extension configuration -------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
# -- Options for intersphinx extension ---------------------------------------
|
# -- Options for intersphinx extension ---------------------------------------
|
||||||
|
|
||||||
# Example configuration for intersphinx: refer to the Python standard library.
|
# Example configuration for intersphinx: refer to the Python standard library.
|
||||||
@ -183,20 +181,21 @@ intersphinx_mapping = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
autodoc_default_flags = ['members']
|
autodoc_default_flags = ['members']
|
||||||
|
autodoc_typehints = "description"
|
||||||
|
|
||||||
# spell checking
|
# spell checking
|
||||||
spelling_lang = 'ru_RU'
|
spelling_lang = 'ru_RU'
|
||||||
tokenizer_lang = 'ru_RU'
|
tokenizer_lang = 'en_US'
|
||||||
spelling_exclude_patterns=['ignored_*']
|
spelling_exclude_patterns = ['ignored_*', '../../main/models.py']
|
||||||
spelling_show_suggestions = True
|
spelling_show_suggestions = True
|
||||||
spelling_show_whole_line=True
|
spelling_show_whole_line = True
|
||||||
spelling_warning=True
|
spelling_warning = True
|
||||||
spelling_ignore_pypi_package_names = True
|
spelling_ignore_pypi_package_names = True
|
||||||
spelling_ignore_wiki_words=True
|
spelling_ignore_wiki_words = True
|
||||||
spelling_ignore_acronyms=True
|
spelling_ignore_acronyms = True
|
||||||
spelling_ignore_python_builtins=True
|
spelling_ignore_python_builtins = True
|
||||||
spelling_ignore_importable_modules=True
|
spelling_ignore_importable_modules = True
|
||||||
spelling_ignore_contributor_names=True
|
spelling_ignore_contributor_names = True
|
||||||
|
|
||||||
# -- Options for todo extension ----------------------------------------------
|
# -- Options for todo extension ----------------------------------------------
|
||||||
|
|
||||||
@ -206,5 +205,3 @@ set_type_checking_flag = True
|
|||||||
typehints_fully_qualified = True
|
typehints_fully_qualified = True
|
||||||
always_document_param_types = True
|
always_document_param_types = True
|
||||||
typehints_document_rtype = True
|
typehints_document_rtype = True
|
||||||
|
|
||||||
napoleon_attr_annotations = True
|
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
overview
|
overview
|
||||||
code
|
code
|
||||||
|
readme
|
||||||
todo
|
todo
|
||||||
|
|
||||||
|
|
||||||
|
@ -69,12 +69,15 @@
|
|||||||
Запрос прав доступа
|
Запрос прав доступа
|
||||||
********************
|
********************
|
||||||
|
|
||||||
На странице запроса прав Вам доступна информация о количестве и списке работающих над тикетами сотрудников,
|
На странице запроса прав Вам доступна информация о количестве и списке работающих над тикетами сотрудников.
|
||||||
а также возможность сдать и запросить права.
|
|
||||||
|
Если Вы не являетесь инженером, то на данной странице Вы можете запросить права.
|
||||||
|
|
||||||
|
Если Вы являетесь инженером, то права можно сдать.
|
||||||
|
|
||||||
.. image:: _static/request.png
|
.. image:: _static/request.png
|
||||||
|
|
||||||
Успешное изменение прав:
|
Успешное изменение прав - список инженеров пополнился новым пользователем:
|
||||||
|
|
||||||
.. image:: _static/role_change.png
|
.. image:: _static/role_change.png
|
||||||
|
|
||||||
@ -84,9 +87,9 @@
|
|||||||
|
|
||||||
Для администратора существует удобный интерфейс страницы управления, в котором представлены:
|
Для администратора существует удобный интерфейс страницы управления, в котором представлены:
|
||||||
|
|
||||||
* Количество свободных инженерных мест
|
* количество свободных инженерных мест
|
||||||
* Количество и список инженеров и легких агентов
|
* количество и список инженеров и легких агентов
|
||||||
* Возможность группового назначения прав с использованием чек-боксов
|
* возможность группового назначения прав с использованием чекбоксов
|
||||||
|
|
||||||
.. image:: _static/admin_manage.png
|
.. image:: _static/admin_manage.png
|
||||||
|
|
||||||
|
4
docs/source/readme.rst
Normal file
4
docs/source/readme.rst
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
READ.me
|
||||||
|
==================
|
||||||
|
|
||||||
|
.. include:: ../../README.rst
|
@ -45,7 +45,9 @@ start
|
|||||||
end
|
end
|
||||||
date
|
date
|
||||||
Токен
|
Токен
|
||||||
|
токен
|
||||||
токеном
|
токеном
|
||||||
|
токену
|
||||||
аутентифицирован
|
аутентифицирован
|
||||||
(datetime.time)
|
(datetime.time)
|
||||||
datetime
|
datetime
|
||||||
@ -81,8 +83,112 @@ functions
|
|||||||
Serializer
|
Serializer
|
||||||
Serializers
|
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
|
||||||
|
|
||||||
|
@ -51,7 +51,7 @@
|
|||||||
"name": "UserForAccessTest",
|
"name": "UserForAccessTest",
|
||||||
"user": 2,
|
"user": 2,
|
||||||
"role": "agent",
|
"role": "agent",
|
||||||
"custom_role_id": "360005209000"
|
"custom_role_id": "360005208980"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
BIN
layouts/registration_failed/registration_failed.png
Normal file
BIN
layouts/registration_failed/registration_failed.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 27 KiB |
@ -1,3 +1,8 @@
|
|||||||
from django.contrib import admin
|
"""
|
||||||
|
Встроенный файл
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# from django.contrib import admin
|
||||||
|
|
||||||
# Register your models here.
|
# Register your models here.
|
||||||
|
@ -1,49 +0,0 @@
|
|||||||
import os
|
|
||||||
|
|
||||||
from zenpy import Zenpy
|
|
||||||
from zenpy.lib.api_objects import User as ZenpyUser
|
|
||||||
|
|
||||||
from access_controller.settings import ACTRL_ZENDESK_SUBDOMAIN, ACTRL_API_EMAIL, ACTRL_API_TOKEN, ACTRL_API_PASSWORD
|
|
||||||
|
|
||||||
|
|
||||||
def api_auth() -> dict:
|
|
||||||
"""
|
|
||||||
Функция создания пользователя с использованием Zendesk API.
|
|
||||||
|
|
||||||
Получает из env Zendesk - email, token, password пользователя.
|
|
||||||
Если данные валидны и пользователь Zendesk с указанным email и токеном или паролем существует,
|
|
||||||
создается словарь данных пользователя, полученных через API c Zendesk.
|
|
||||||
|
|
||||||
:return: данные пользователя
|
|
||||||
"""
|
|
||||||
credentials = {
|
|
||||||
'subdomain': ACTRL_ZENDESK_SUBDOMAIN
|
|
||||||
}
|
|
||||||
email = ACTRL_API_EMAIL
|
|
||||||
token = ACTRL_API_TOKEN
|
|
||||||
password = ACTRL_API_PASSWORD
|
|
||||||
|
|
||||||
if email is None:
|
|
||||||
raise ValueError('access_controller email not in env')
|
|
||||||
credentials['email'] = email
|
|
||||||
|
|
||||||
# prefer token, use password if token not provided
|
|
||||||
if token:
|
|
||||||
credentials['token'] = token
|
|
||||||
elif password:
|
|
||||||
credentials['password'] = password
|
|
||||||
else:
|
|
||||||
raise ValueError('access_controller token or password not in env')
|
|
||||||
|
|
||||||
zenpy_client = Zenpy(**credentials)
|
|
||||||
zenpy_user: ZenpyUser = zenpy_client.users.search(email).values[0]
|
|
||||||
|
|
||||||
user = {
|
|
||||||
'id': zenpy_user.id,
|
|
||||||
'name': zenpy_user.name, # Zendesk doesn't have separate first and last name fields
|
|
||||||
'email': zenpy_user.email,
|
|
||||||
'role': zenpy_user.role, # str like 'admin' or 'agent', not id
|
|
||||||
'photo': zenpy_user.photo['content_url'] if zenpy_user.photo is not None else None,
|
|
||||||
}
|
|
||||||
|
|
||||||
return user
|
|
@ -1,5 +1,11 @@
|
|||||||
|
"""
|
||||||
|
Стандартный файл Django конфигурации приложения.
|
||||||
|
"""
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
class MainConfig(AppConfig):
|
class MainConfig(AppConfig):
|
||||||
|
"""
|
||||||
|
Старт приложения
|
||||||
|
"""
|
||||||
name = 'main'
|
name = 'main'
|
||||||
|
@ -1,8 +1,14 @@
|
|||||||
|
"""
|
||||||
|
Вспомогательные функции со списками пользователей, статистикой и т.д.
|
||||||
|
"""
|
||||||
import logging
|
import logging
|
||||||
from datetime import timedelta
|
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.exceptions import ObjectDoesNotExist
|
||||||
|
from django.core.handlers.wsgi import WSGIRequest
|
||||||
|
from django.http import HttpResponseRedirect, HttpResponsePermanentRedirect
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from zenpy import Zenpy
|
from zenpy import Zenpy
|
||||||
@ -16,7 +22,7 @@ from main.requester import TicketListRequester
|
|||||||
from main.zendesk_admin import zenpy
|
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)
|
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)
|
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
|
return users
|
||||||
|
|
||||||
|
|
||||||
def get_tickets_list(email):
|
def get_tickets_list(email) -> list:
|
||||||
"""
|
"""
|
||||||
Функция возвращает список тикетов пользователя Zendesk
|
Функция возвращает список тикетов пользователя 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))
|
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.
|
Функция обновляет профиль пользователя в соответствии с текущим в Zendesk.
|
||||||
|
|
||||||
@ -157,7 +163,7 @@ def check_user_auth(email: str, password: str) -> bool:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def update_user_in_model(profile: UserProfile, zendesk_user: ZenpyUser):
|
def update_user_in_model(profile: UserProfile, zendesk_user: ZenpyUser) -> None:
|
||||||
"""
|
"""
|
||||||
Функция обновляет профиль пользователя при изменении данных пользователя на Zendesk.
|
Функция обновляет профиль пользователя при изменении данных пользователя на Zendesk.
|
||||||
|
|
||||||
@ -173,7 +179,7 @@ def update_user_in_model(profile: UserProfile, zendesk_user: ZenpyUser):
|
|||||||
profile.save()
|
profile.save()
|
||||||
|
|
||||||
|
|
||||||
def count_users(users) -> tuple:
|
def count_users(users: list) -> tuple:
|
||||||
"""
|
"""
|
||||||
Функция подсчета количества сотрудников с ролями engineer и light_agent
|
Функция подсчета количества сотрудников с ролями engineer и light_agent
|
||||||
"""
|
"""
|
||||||
@ -186,21 +192,21 @@ def count_users(users) -> tuple:
|
|||||||
return engineers, light_agents
|
return engineers, light_agents
|
||||||
|
|
||||||
|
|
||||||
def update_users_in_model():
|
def update_users_in_model() -> list:
|
||||||
"""
|
"""
|
||||||
Обновляет пользователей в модели UserProfile по списку пользователей в организации
|
Обновляет пользователей в модели UserProfile по списку пользователей в организации
|
||||||
"""
|
"""
|
||||||
users = get_users_list()
|
users = get_users_list()
|
||||||
for user in users:
|
for user in users:
|
||||||
try:
|
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)
|
update_user_in_model(profile, user)
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
pass
|
pass
|
||||||
return users
|
return users
|
||||||
|
|
||||||
|
|
||||||
def daterange(start_date, end_date) -> list:
|
def daterange(start_date: timedelta, end_date: timedelta) -> list:
|
||||||
"""
|
"""
|
||||||
Функция возвращает список дней с start_date по end_date, исключая правую границу.
|
Функция возвращает список дней с start_date по end_date, исключая правую границу.
|
||||||
|
|
||||||
@ -214,17 +220,17 @@ def daterange(start_date, end_date) -> list:
|
|||||||
return dates
|
return dates
|
||||||
|
|
||||||
|
|
||||||
def get_timedelta(log, time=None) -> timedelta:
|
def get_timedelta(current_log: RoleChangeLogs, time: timedelta = None) -> timedelta:
|
||||||
"""
|
"""
|
||||||
Функция возвращает объект класса timedelta, который хранит промежуток времени от начала суток до момента,
|
Функция возвращает объект класса timedelta, который хранит промежуток времени от начала суток до момента,
|
||||||
который находится в log (объект класса RoleChangeLogs) или в time(datetime.time), если введён.
|
который находится в log (объект класса RoleChangeLogs) или в time(datetime.time), если введён.
|
||||||
|
|
||||||
:param log: Лог
|
:param current_log: Лог
|
||||||
:param time: Время
|
:param time: Время
|
||||||
:return: Сколько времени прошло от начала суток до события
|
:return: Сколько времени прошло от начала суток до события
|
||||||
"""
|
"""
|
||||||
if time is None:
|
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)
|
time = timedelta(hours=time.hour, minutes=time.minute, seconds=time.second)
|
||||||
return time
|
return time
|
||||||
|
|
||||||
@ -241,6 +247,9 @@ def last_day_of_month(day: int) -> int:
|
|||||||
|
|
||||||
|
|
||||||
class DatabaseHandler(logging.Handler):
|
class DatabaseHandler(logging.Handler):
|
||||||
|
"""
|
||||||
|
Класс записи изменений ролей в базу данных.
|
||||||
|
"""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
logging.Handler.__init__(self)
|
logging.Handler.__init__(self)
|
||||||
|
|
||||||
@ -265,10 +274,19 @@ class DatabaseHandler(logging.Handler):
|
|||||||
|
|
||||||
|
|
||||||
class CsvFormatter(logging.Formatter):
|
class CsvFormatter(logging.Formatter):
|
||||||
|
"""
|
||||||
|
Класс преобразования смены ролей пользователей в строковый формат.
|
||||||
|
"""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
logging.Formatter.__init__(self)
|
logging.Formatter.__init__(self)
|
||||||
|
|
||||||
def format(self, record):
|
def format(self, record: logging.LogRecord) -> str:
|
||||||
|
"""
|
||||||
|
Функция форматирует запись смены роли пользователя в строку.
|
||||||
|
|
||||||
|
:param record: Запись смены роли пользователя.
|
||||||
|
:return: Строка с записью смены пользователя.
|
||||||
|
"""
|
||||||
users = record.msg
|
users = record.msg
|
||||||
if users[1]:
|
if users[1]:
|
||||||
user = users[0]
|
user = users[0]
|
||||||
@ -291,10 +309,10 @@ class CsvFormatter(logging.Formatter):
|
|||||||
|
|
||||||
def log(user, admin=None):
|
def log(user, admin=None):
|
||||||
"""
|
"""
|
||||||
Осуществляет запись логов в базу данных и csv файл
|
Функция осуществляет запись логов в базу данных и csv файл.
|
||||||
:param admin:
|
|
||||||
:param user:
|
:param admin: Админ, который меняет роль
|
||||||
:return:
|
:param user: Пользователь, которому изменена роль
|
||||||
"""
|
"""
|
||||||
users = [user, admin]
|
users = [user, admin]
|
||||||
logger = logging.getLogger('MY_LOGGER')
|
logger = logging.getLogger('MY_LOGGER')
|
||||||
@ -309,10 +327,16 @@ def log(user, admin=None):
|
|||||||
logger.info(users)
|
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['is_confirm'] = is_confirm
|
||||||
request.session['count_tickets'] = count
|
request.session['count_tickets'] = count
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
"""
|
||||||
|
Формы.
|
||||||
|
"""
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.auth.forms import AuthenticationForm
|
from django.contrib.auth.forms import AuthenticationForm
|
||||||
from django_registration.forms import RegistrationFormUniqueEmail
|
from django_registration.forms import RegistrationFormUniqueEmail
|
||||||
@ -56,6 +59,8 @@ class CustomAuthenticationForm(AuthenticationForm):
|
|||||||
|
|
||||||
:param username: Поле для ввода email пользователя
|
:param username: Поле для ввода email пользователя
|
||||||
:type username: :class:`django.forms.fields.CharField`
|
:type username: :class:`django.forms.fields.CharField`
|
||||||
|
:param error_messages: Список ошибок авторизации
|
||||||
|
:type error_messages: :class:`dict`
|
||||||
"""
|
"""
|
||||||
username = forms.CharField(
|
username = forms.CharField(
|
||||||
label="Электронная почта",
|
label="Электронная почта",
|
||||||
@ -64,8 +69,7 @@ class CustomAuthenticationForm(AuthenticationForm):
|
|||||||
error_messages = {
|
error_messages = {
|
||||||
'invalid_login':
|
'invalid_login':
|
||||||
"Пожалуйста, введите правильные электронную почту и пароль. Оба поля "
|
"Пожалуйста, введите правильные электронную почту и пароль. Оба поля "
|
||||||
"могут быть чувствительны к регистру."
|
"могут быть чувствительны к регистру.",
|
||||||
,
|
|
||||||
'inactive': "Аккаунт не активен.",
|
'inactive': "Аккаунт не активен.",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
18
main/migrations/0018_alter_unassignedticket_ticket_id.py
Normal file
18
main/migrations/0018_alter_unassignedticket_ticket_id.py
Normal 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='Номер тикета, для которого сняли ответственного'),
|
||||||
|
),
|
||||||
|
]
|
@ -1,5 +1,8 @@
|
|||||||
|
"""
|
||||||
|
Модели, использующиеся в приложении.
|
||||||
|
"""
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.contrib.auth.models import User
|
|
||||||
from django.db.models.signals import post_save
|
from django.db.models.signals import post_save
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
@ -19,42 +22,64 @@ class UserProfile(models.Model):
|
|||||||
('has_control_access', 'Can view admin page'),
|
('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='Глобальное имя роли пользователя')
|
role = models.CharField(default='None', max_length=100, help_text='Глобальное имя роли пользователя')
|
||||||
custom_role_id = models.IntegerField(default=0, help_text='Код роли пользователя')
|
custom_role_id = models.IntegerField(default=0, help_text='Код роли пользователя')
|
||||||
image = models.URLField(null=True, blank=True, help_text='Аватарка')
|
image = models.URLField(null=True, blank=True, help_text='Аватарка')
|
||||||
name = models.CharField(default='None', max_length=100, help_text='Имя пользователя на нашем сайте')
|
name = models.CharField(default='None', max_length=100, help_text='Имя пользователя на нашем сайте')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def zendesk_role(self):
|
def zendesk_role(self) -> str:
|
||||||
id = self.custom_role_id
|
"""
|
||||||
|
Функция возвращает роль пользователя в Zendesk.
|
||||||
|
|
||||||
|
В формате str, либо UNDEFINED, если пользователь не найден
|
||||||
|
|
||||||
|
:return: Роль пользователя в Zendesk
|
||||||
|
"""
|
||||||
for role, r_id in ZENDESK_ROLES.items():
|
for role, r_id in ZENDESK_ROLES.items():
|
||||||
if r_id == id:
|
if r_id == self.custom_role_id:
|
||||||
return role
|
return role
|
||||||
return 'UNDEFINED'
|
return 'UNDEFINED'
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=User)
|
@receiver(post_save, sender=get_user_model())
|
||||||
def create_user_profile(sender, instance, created, **kwargs):
|
def create_user_profile(instance, created, **kwargs) -> None:
|
||||||
|
"""
|
||||||
|
Функция создания профиля пользователя (Userprofile) при регистрации пользователя.
|
||||||
|
|
||||||
|
:param instance: Экземпляр класса User
|
||||||
|
:param created: Создание профиля пользователя
|
||||||
|
:param kwargs: Параметры
|
||||||
|
:return: Обновленный список объектов профилей пользователей
|
||||||
|
"""
|
||||||
if created:
|
if created:
|
||||||
UserProfile.objects.create(user=instance)
|
UserProfile.objects.create(user=instance)
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=User)
|
@receiver(post_save, sender=get_user_model())
|
||||||
def save_user_profile(sender, instance, **kwargs):
|
def save_user_profile(instance, **kwargs) -> None:
|
||||||
|
"""
|
||||||
|
Функция записи БД профиля пользователя.
|
||||||
|
|
||||||
|
:param instance: Экземпляр класса User
|
||||||
|
:param kwargs: Параметры
|
||||||
|
:return: Запись профиля пользователя
|
||||||
|
"""
|
||||||
instance.userprofile.save()
|
instance.userprofile.save()
|
||||||
|
|
||||||
|
|
||||||
class RoleChangeLogs(models.Model):
|
class RoleChangeLogs(models.Model):
|
||||||
"""
|
"""
|
||||||
Модель для логирования изменений ролей пользователя.
|
Модель для логгирования изменений ролей пользователя
|
||||||
"""
|
"""
|
||||||
|
user = models.ForeignKey(to=get_user_model(), on_delete=models.CASCADE,
|
||||||
user = models.ForeignKey(to=User, on_delete=models.CASCADE, help_text='Пользователь, которому присвоили другую роль')
|
help_text='Пользователь, которому присвоили другую роль')
|
||||||
old_role = models.IntegerField(default=0, help_text='Старая роль')
|
old_role = models.IntegerField(default=0, help_text='Старая роль')
|
||||||
new_role = models.IntegerField(default=0, help_text='Присвоенная роль')
|
new_role = models.IntegerField(default=0, help_text='Присвоенная роль')
|
||||||
change_time = models.DateTimeField(default=timezone.now, 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):
|
class UnassignedTicketStatus(models.IntegerChoices):
|
||||||
@ -63,13 +88,15 @@ class UnassignedTicketStatus(models.IntegerChoices):
|
|||||||
|
|
||||||
:param UNASSIGNED: Снят с пользователя, перенесён в буферную группу
|
:param UNASSIGNED: Снят с пользователя, перенесён в буферную группу
|
||||||
:param RESTORED: Авторство восстановлено
|
:param RESTORED: Авторство восстановлено
|
||||||
:param NOT_FOUND: Пока нас не было, тикет испарился из буферной группы. Дополнительные действия не требуются
|
:param NOT_FOUND: Пока нас не было, тикет испарился из буферной группы.
|
||||||
|
Дополнительные действия не требуются
|
||||||
:param CLOSED: Тикет уже был закрыт. Дополнительные действия не требуются
|
:param CLOSED: Тикет уже был закрыт. Дополнительные действия не требуются
|
||||||
:param SOLVED: Тикет решён. Записан на пользователя с почтой SOLVED_TICKETS_EMAIL
|
:param SOLVED: Тикет решён. Записан на пользователя с почтой SOLVED_TICKETS_EMAIL
|
||||||
"""
|
"""
|
||||||
UNASSIGNED = 0, 'Снят с пользователя, перенесён в буферную группу'
|
UNASSIGNED = 0, 'Снят с пользователя, перенесён в буферную группу'
|
||||||
RESTORED = 1, 'Авторство восстановлено'
|
RESTORED = 1, 'Авторство восстановлено'
|
||||||
NOT_FOUND = 2, 'Пока нас не было, тикет испарился из буферной группы. Дополнительные действия не требуются'
|
NOT_FOUND = 2, 'Пока нас не было, тикет испарился из ' \
|
||||||
|
'буферной группы. Дополнительные действия не требуются'
|
||||||
CLOSED = 3, 'Тикет уже был закрыт. Дополнительные действия не требуются'
|
CLOSED = 3, 'Тикет уже был закрыт. Дополнительные действия не требуются'
|
||||||
SOLVED = 4, 'Тикет решён. Записан на пользователя с почтой SOLVED_TICKETS_EMAIL'
|
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='Пользователь, с которого снят тикет')
|
assignee = models.ForeignKey(to=get_user_model(), on_delete=models.CASCADE, related_name='tickets',
|
||||||
ticket_id = models.IntegerField(help_text='Номер тикера, для которого сняли ответственного')
|
help_text='Пользователь, с которого снят тикет')
|
||||||
status = models.IntegerField(choices=UnassignedTicketStatus.choices, default=UnassignedTicketStatus.UNASSIGNED, help_text='Статус тикета')
|
ticket_id = models.IntegerField(help_text='Номер тикета, для которого сняли ответственного')
|
||||||
|
status = models.IntegerField(choices=UnassignedTicketStatus.choices, default=UnassignedTicketStatus.UNASSIGNED,
|
||||||
|
help_text='Статус тикета')
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
"""
|
||||||
|
Обработка тикетов.
|
||||||
|
"""
|
||||||
import requests
|
import requests
|
||||||
from zenpy import TicketApi
|
from zenpy import TicketApi
|
||||||
from zenpy.lib.api_objects import Ticket
|
from zenpy.lib.api_objects import Ticket
|
||||||
@ -6,6 +9,9 @@ from main.zendesk_admin import zenpy
|
|||||||
|
|
||||||
|
|
||||||
class TicketListRequester:
|
class TicketListRequester:
|
||||||
|
"""
|
||||||
|
Класс обработки тикетов.
|
||||||
|
"""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.email = zenpy.credentials['email']
|
self.email = zenpy.credentials['email']
|
||||||
if zenpy.credentials.get('token'):
|
if zenpy.credentials.get('token'):
|
||||||
@ -15,11 +21,17 @@ class TicketListRequester:
|
|||||||
self.token_or_password = zenpy.credentials.get('password')
|
self.token_or_password = zenpy.credentials.get('password')
|
||||||
self.prefix = f'https://{zenpy.credentials.get("subdomain")}.zendesk.com/api/v2/'
|
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'
|
url = self.prefix + f'users/{zendesk_user.id}/tickets/assigned'
|
||||||
return self._get_tickets(url)
|
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'
|
url = self.prefix + '/tickets'
|
||||||
all_tickets = self._get_tickets(url)
|
all_tickets = self._get_tickets(url)
|
||||||
tickets = list()
|
tickets = list()
|
||||||
@ -28,7 +40,10 @@ class TicketListRequester:
|
|||||||
tickets.append(ticket)
|
tickets.append(ticket)
|
||||||
return tickets
|
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))
|
response = requests.get(url, auth=(self.email, self.token_or_password))
|
||||||
tickets = []
|
tickets = []
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
|
@ -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 rest_framework import serializers
|
||||||
from main.models import UserProfile
|
from main.models import UserProfile
|
||||||
from access_controller.settings import ZENDESK_ROLES
|
from access_controller.settings import ZENDESK_ROLES
|
||||||
@ -7,14 +10,28 @@ from access_controller.settings import ZENDESK_ROLES
|
|||||||
class UserSerializer(serializers.HyperlinkedModelSerializer):
|
class UserSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
"""
|
"""
|
||||||
Класс serializer для модели User.
|
Класс serializer для модели User.
|
||||||
|
|
||||||
|
:param model: Модель, на основании которой создается сериализатор
|
||||||
|
:type model: :class:`django.contrib.auth.Models`
|
||||||
|
:param fields: Передаваемые поля
|
||||||
|
:type email: :class:`list`
|
||||||
"""
|
"""
|
||||||
class Meta:
|
class Meta:
|
||||||
model = User
|
model = get_user_model()
|
||||||
fields = ['email']
|
fields = ['email']
|
||||||
|
|
||||||
|
|
||||||
class ProfileSerializer(serializers.HyperlinkedModelSerializer):
|
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()
|
user = UserSerializer()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -23,16 +40,36 @@ class ProfileSerializer(serializers.HyperlinkedModelSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class ZendeskUserSerializer(serializers.Serializer):
|
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()
|
name = serializers.CharField()
|
||||||
zendesk_role = serializers.SerializerMethodField('get_zendesk_role')
|
zendesk_role = serializers.SerializerMethodField('get_zendesk_role')
|
||||||
email = serializers.EmailField()
|
email = serializers.EmailField()
|
||||||
|
|
||||||
@staticmethod
|
@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']:
|
if obj.custom_role_id == ZENDESK_ROLES['engineer']:
|
||||||
return 'engineer'
|
return 'engineer'
|
||||||
elif obj.custom_role_id == ZENDESK_ROLES['light_agent']:
|
if obj.custom_role_id == ZENDESK_ROLES['light_agent']:
|
||||||
return 'light_agent'
|
return 'light_agent'
|
||||||
else:
|
return "empty"
|
||||||
return "empty"
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
pass
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
|
"""
|
||||||
|
Обработка статистики.
|
||||||
|
"""
|
||||||
from datetime import date, datetime, timedelta
|
from datetime import date, datetime, timedelta
|
||||||
from typing import Optional
|
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 django.utils import timezone
|
||||||
|
|
||||||
from access_controller.settings import ONE_DAY, ZENDESK_ROLES as ROLES
|
from access_controller.settings import ONE_DAY, ZENDESK_ROLES as ROLES
|
||||||
@ -50,19 +53,19 @@ class StatisticData:
|
|||||||
else:
|
else:
|
||||||
self.statistic = stat
|
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():
|
if self.is_valid_statistic():
|
||||||
stat = self.statistic
|
stat = self.statistic
|
||||||
stat = self._use_display(stat)
|
stat = self._use_display(stat)
|
||||||
stat = self._use_interval(stat)
|
stat = self._use_interval(stat)
|
||||||
return stat
|
return stat
|
||||||
else:
|
return None
|
||||||
return None
|
|
||||||
|
|
||||||
def is_valid_statistic(self) -> bool:
|
def is_valid_statistic(self) -> bool:
|
||||||
"""
|
"""
|
||||||
@ -104,8 +107,7 @@ class StatisticData:
|
|||||||
"""
|
"""
|
||||||
if self.is_valid_data():
|
if self.is_valid_data():
|
||||||
return self.data
|
return self.data
|
||||||
else:
|
return None
|
||||||
return None
|
|
||||||
|
|
||||||
def is_valid_data(self) -> bool:
|
def is_valid_data(self) -> bool:
|
||||||
"""
|
"""
|
||||||
@ -170,7 +172,8 @@ class StatisticData:
|
|||||||
"""
|
"""
|
||||||
Функция возвращает логи в диапазоне дат start_date - end_date для пользователя с указанным email.
|
Функция возвращает логи в диапазоне дат start_date - end_date для пользователя с указанным email.
|
||||||
|
|
||||||
:return: Данные о смене статусов пользователя. Если пользователь не найден или интервал времени некорректен - ошибку.
|
:return: Данные о смене статусов пользователя. Если пользователь не найден или
|
||||||
|
интервал времени некорректен - ошибку.
|
||||||
"""
|
"""
|
||||||
if not self.check_time():
|
if not self.check_time():
|
||||||
self.errors += ['Конец диапазона должен быть позже начала диапазона и раньше текущего времени']
|
self.errors += ['Конец диапазона должен быть позже начала диапазона и раньше текущего времени']
|
||||||
@ -178,12 +181,12 @@ class StatisticData:
|
|||||||
try:
|
try:
|
||||||
self.data = RoleChangeLogs.objects.filter(
|
self.data = RoleChangeLogs.objects.filter(
|
||||||
change_time__range=[self.start_date, self.end_date + timedelta(days=1)],
|
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')
|
).order_by('change_time')
|
||||||
except User.DoesNotExist:
|
except get_user_model().DoesNotExist:
|
||||||
self.errors += ['Пользователь не найден']
|
self.errors += ['Пользователь не найден']
|
||||||
|
|
||||||
def _init_statistic(self) -> dict:
|
def _init_statistic(self) -> None:
|
||||||
"""
|
"""
|
||||||
Функция заполняет словарь, в котором ключ - дата, значение - кол-во проработанных в этот день секунд.
|
Функция заполняет словарь, в котором ключ - дата, значение - кол-во проработанных в этот день секунд.
|
||||||
|
|
||||||
@ -192,18 +195,18 @@ class StatisticData:
|
|||||||
self.clear_statistic()
|
self.clear_statistic()
|
||||||
if not self.get_data():
|
if not self.get_data():
|
||||||
self.warnings += ['Не обнаружены изменения роли в данном промежутке']
|
self.warnings += ['Не обнаружены изменения роли в данном промежутке']
|
||||||
return None
|
else:
|
||||||
first_log, last_log = self.data[0], self.data[len(self.data) - 1]
|
first_log, last_log = self.data[0], self.data[len(self.data) - 1]
|
||||||
|
|
||||||
if first_log.old_role == ROLES['engineer']:
|
if first_log.old_role == ROLES['engineer']:
|
||||||
self.prev_engineer_logic(first_log)
|
self.prev_engineer_logic(first_log)
|
||||||
|
|
||||||
if last_log.new_role == ROLES['engineer']:
|
if last_log.new_role == ROLES['engineer']:
|
||||||
self.post_engineer_logic(last_log)
|
self.post_engineer_logic(last_log)
|
||||||
|
|
||||||
for log_index in range(len(self.data) - 1):
|
for log_index in range(len(self.data) - 1):
|
||||||
if self.data[log_index].new_role == ROLES['engineer']:
|
if self.data[log_index].new_role == ROLES['engineer']:
|
||||||
self.engineer_logic(log_index)
|
self.engineer_logic(log_index)
|
||||||
|
|
||||||
def engineer_logic(self, 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())
|
first_log.change_time.date())
|
||||||
self.statistic[first_log.change_time.date()] += get_timedelta(first_log).total_seconds()
|
self.statistic[first_log.change_time.date()] += get_timedelta(first_log).total_seconds()
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
{% block title %}{{ pagename }}{% endblock %}
|
{% block title %}{{ pagename }}{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
{% block heading %}Профиль{% endblock %}
|
{% block heading %}<h1>Профиль</h1>{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
{% block extra_css %}
|
{% block extra_css %}
|
||||||
@ -22,7 +22,7 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<br>
|
<br>
|
||||||
<div class="row">
|
<div class="row px-4 py-4">
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<img
|
<img
|
||||||
@ -34,22 +34,25 @@
|
|||||||
<a href="{%url 'password_change' %}">Сменить пароль</a>
|
<a href="{%url 'password_change' %}">Сменить пароль</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h5><span class="badge bg-secondary text-light">Имя пользователя</span> {{ profile.name }}</h5>
|
<h4><span class="badge bg-secondary text-light">Имя пользователя</span></h4> <h5><strong>{{ profile.name }}</strong></h5>
|
||||||
<br>
|
<br>
|
||||||
<h5><span class="badge bg-secondary text-light">Электронная почта</span> {{ profile.user.email }}</h5>
|
<h4><span class="badge bg-secondary text-light">Электронная почта</span></h4> <h5><strong>{{ profile.user.email }}</strong></h5>
|
||||||
<br>
|
<br>
|
||||||
<h5><span class="badge bg-secondary text-light">Текущая роль</span>
|
<h4><span class="badge bg-secondary text-light">Текущая роль</span> </h4>
|
||||||
{% if profile.custom_role_id == ZENDESK_ROLES.engineer %}
|
{% if profile.custom_role_id == ZENDESK_ROLES.engineer %}
|
||||||
engineer
|
<h5><strong>engineer</strong></h5>
|
||||||
{% elif profile.custom_role_id == ZENDESK_ROLES.light_agent %}
|
{% elif profile.custom_role_id == ZENDESK_ROLES.light_agent %}
|
||||||
light_agent
|
<h5><strong>light_agent</strong></h5>
|
||||||
|
{% else %}
|
||||||
|
<h5><strong><small class="text-muted">None</small></strong></h5>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</h5>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div align="center">
|
<br>
|
||||||
|
<div align="center" >
|
||||||
<form action="">
|
<form action="">
|
||||||
<a href="{% url 'work' profile.user.id %}" class="btn btn-primary"><big>Запросить права доступа</big></a>
|
<a href="{% url 'work' profile.user.id %}" class="btn btn-primary btn-lg">Запросить права доступа</a>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
12
main/templates/pages/registration_failed.html
Normal file
12
main/templates/pages/registration_failed.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{% extends 'base/base.html' %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
Регистрация закрыта
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div style="margin: 10%; text-align: center;">
|
||||||
|
<p style="color: rgb(201, 58, 63); font-size: 72px;">К сожалению, регистрация закрыта.</p>
|
||||||
|
<a type="button" class="btn btn-outline-primary btn-lg" href="/">На главную</a>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -12,7 +12,7 @@
|
|||||||
<p>Введте свой e-mail адрес для восстановления пароля.</p>
|
<p>Введте свой e-mail адрес для восстановления пароля.</p>
|
||||||
<form action="." method="post">
|
<form action="." method="post">
|
||||||
{{ form.as_p }}
|
{{ 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 %}
|
{% csrf_token %}
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
441
main/tests.py
441
main/tests.py
@ -1,43 +1,85 @@
|
|||||||
|
"""
|
||||||
|
Тесты.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
import random
|
import random
|
||||||
from unittest.mock import patch, Mock
|
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.core import mail
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.test import TestCase, Client
|
from django.test import TestCase, Client
|
||||||
from django.urls import reverse, reverse_lazy
|
from django.urls import reverse, reverse_lazy
|
||||||
from django.utils import translation
|
from django.utils import translation, timezone
|
||||||
|
|
||||||
import access_controller.settings as sets
|
import access_controller.settings as sets
|
||||||
from main.zendesk_admin import zenpy
|
from main.zendesk_admin import zenpy
|
||||||
|
|
||||||
|
from main.extra_func import log
|
||||||
|
|
||||||
class RegistrationTestCase(TestCase):
|
|
||||||
fixtures = ['fixtures/data.json']
|
class UsersBaseTestCase(TestCase):
|
||||||
|
"""Базовый класс загружения данных для тестов с пользователями"""
|
||||||
|
fixtures = ['fixtures/test_users.json']
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
"""Добавление в переменные почт и клиентов для пользователей"""
|
||||||
|
self.light_agent = '123@test.ru'
|
||||||
|
self.admin = 'admin@gmail.com'
|
||||||
|
self.engineer = 'customer@example.com'
|
||||||
|
self.agent_client = Client()
|
||||||
|
self.agent_client.force_login(get_user_model().objects.get(email=self.light_agent))
|
||||||
|
self.admin_client = Client()
|
||||||
|
self.admin_client.force_login(get_user_model().objects.get(email=self.admin))
|
||||||
|
self.engineer_client = Client()
|
||||||
|
self.engineer_client.force_login(get_user_model().objects.get(email=self.engineer))
|
||||||
|
|
||||||
|
|
||||||
|
class RegistrationTestCase(TestCase):
|
||||||
|
"""
|
||||||
|
Класс тестирования регистрации пользователя.
|
||||||
|
"""
|
||||||
|
fixtures = ['fixtures/data.json']
|
||||||
|
|
||||||
|
def setUp(self) -> None:
|
||||||
|
"""
|
||||||
|
Функция предтестовых настроек.
|
||||||
|
"""
|
||||||
self.email_backend = 'django.core.mail.backends.locmem.EmailBackend'
|
self.email_backend = 'django.core.mail.backends.locmem.EmailBackend'
|
||||||
self.any_zendesk_user_email = 'idar.sokurov.05@mail.ru'
|
self.any_zendesk_user_email = 'idar.sokurov.05@mail.ru'
|
||||||
self.zendesk_admin_email = 'idar.sokurov.05@mail.ru'
|
self.zendesk_admin_email = 'idar.sokurov.05@mail.ru'
|
||||||
self.client = Client()
|
self.client = Client()
|
||||||
|
|
||||||
def test_registration_complete_redirect(self):
|
def test_registration_complete_redirect(self) -> None:
|
||||||
|
"""
|
||||||
|
Функция тестирования успешно завершенной регистрации.
|
||||||
|
"""
|
||||||
with self.settings(EMAIL_BACKEND=self.email_backend):
|
with self.settings(EMAIL_BACKEND=self.email_backend):
|
||||||
resp = self.client.post(reverse('registration'), data={'email': self.any_zendesk_user_email})
|
resp = self.client.post(reverse('registration'), data={'email': self.any_zendesk_user_email})
|
||||||
self.assertRedirects(resp, reverse('password_reset_done'))
|
self.assertRedirects(resp, reverse('password_reset_done'))
|
||||||
|
|
||||||
def test_registration_fail_redirect(self):
|
def test_registration_fail_redirect(self):
|
||||||
|
"""
|
||||||
|
Функция тестирования неуспешной регистрации.
|
||||||
|
"""
|
||||||
with self.settings(EMAIL_BACKEND=self.email_backend):
|
with self.settings(EMAIL_BACKEND=self.email_backend):
|
||||||
resp = self.client.post(reverse('registration'), data={'email': self.any_zendesk_user_email + 'asd'})
|
resp = self.client.post(reverse('registration'), data={'email': self.any_zendesk_user_email + 'asd'})
|
||||||
self.assertRedirects(resp, reverse('django_registration_disallowed'))
|
self.assertRedirects(resp, reverse('django_registration_disallowed'))
|
||||||
|
|
||||||
def test_registration_user_already_exist(self):
|
def test_registration_user_already_exist(self):
|
||||||
|
"""
|
||||||
|
Функция тестирования попытки регистрации уже зарегистрированного пользователя.
|
||||||
|
"""
|
||||||
with self.settings(EMAIL_BACKEND=self.email_backend) and translation.override('ru'):
|
with self.settings(EMAIL_BACKEND=self.email_backend) and translation.override('ru'):
|
||||||
resp = self.client.post(reverse('registration'), data={'email': '123@test.ru'})
|
resp = self.client.post(reverse('registration'), data={'email': '123@test.ru'})
|
||||||
self.assertContains(resp, 'Этот адрес электронной почты уже используется', count=1, status_code=200)
|
self.assertContains(resp, 'Этот адрес электронной почты уже используется', count=1, status_code=200)
|
||||||
|
|
||||||
def test_registration_send_email(self):
|
def test_registration_send_email(self):
|
||||||
|
"""
|
||||||
|
Функция тестирования отправки email.
|
||||||
|
"""
|
||||||
with self.settings(EMAIL_BACKEND=self.email_backend):
|
with self.settings(EMAIL_BACKEND=self.email_backend):
|
||||||
response: HttpResponseRedirect = \
|
response: HttpResponseRedirect = \
|
||||||
self.client.post(reverse('registration'), data={'email': self.any_zendesk_user_email})
|
self.client.post(reverse('registration'), data={'email': self.any_zendesk_user_email})
|
||||||
@ -53,108 +95,223 @@ class RegistrationTestCase(TestCase):
|
|||||||
self.assertEqual(mail.outbox[0].body, correct_body)
|
self.assertEqual(mail.outbox[0].body, correct_body)
|
||||||
|
|
||||||
def test_registration_user_creating(self):
|
def test_registration_user_creating(self):
|
||||||
|
"""
|
||||||
|
Функция тестирования регистрации пользователя (сверяем имя с именем в Zendesk.
|
||||||
|
"""
|
||||||
with self.settings(EMAIL_BACKEND=self.email_backend):
|
with self.settings(EMAIL_BACKEND=self.email_backend):
|
||||||
self.client.post(reverse('registration'), data={'email': self.any_zendesk_user_email})
|
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)
|
zendesk_user = zenpy.get_user(self.any_zendesk_user_email)
|
||||||
self.assertEqual(user.userprofile.name, zendesk_user.name)
|
self.assertEqual(user.userprofile.name, zendesk_user.name)
|
||||||
|
|
||||||
def test_permissions_applying(self):
|
def test_permissions_applying(self):
|
||||||
|
"""
|
||||||
|
Функция тестирования проверке присвоения роли admin.
|
||||||
|
"""
|
||||||
with self.settings(EMAIL_BACKEND=self.email_backend):
|
with self.settings(EMAIL_BACKEND=self.email_backend):
|
||||||
self.client.post(reverse('registration'), data={'email': self.zendesk_admin_email})
|
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.assertEqual(user.userprofile.role, 'admin')
|
||||||
self.assertTrue(user.has_perm('main.has_control_access'))
|
self.assertTrue(user.has_perm('main.has_control_access'))
|
||||||
|
|
||||||
|
|
||||||
class MakeEngineerTestCase(TestCase):
|
class MakeEngineerTestCase(UsersBaseTestCase):
|
||||||
fixtures = ['fixtures/test_make_engineer.json']
|
"""
|
||||||
|
Класс тестов для проверки функции назначения роли engineer.
|
||||||
def setUp(self):
|
"""
|
||||||
self.light_agent = '123@test.ru'
|
|
||||||
self.admin = 'admin@gmail.com'
|
|
||||||
self.engineer = 'customer@example.com'
|
|
||||||
self.client = Client()
|
|
||||||
self.client.force_login(User.objects.get(email=self.light_agent))
|
|
||||||
self.admin_client = Client()
|
|
||||||
self.admin_client.force_login(User.objects.get(email=self.admin))
|
|
||||||
|
|
||||||
@patch('main.extra_func.zenpy')
|
@patch('main.extra_func.zenpy')
|
||||||
def test_redirect(self, ZenpyMock):
|
def test_become_engineer_redirect(self, _zenpy_mock):
|
||||||
user = User.objects.get(email=self.light_agent)
|
"""
|
||||||
resp = self.client.post(reverse_lazy('work_become_engineer'))
|
Функция проверки переадресации пользователя на рабочую страницу после назначения роли 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.assertRedirects(resp, reverse('work', args=[user.id]))
|
||||||
self.assertEqual(resp.status_code, 302)
|
self.assertEqual(resp.status_code, 302)
|
||||||
|
self.assertFalse(_zenpy_mock.called)
|
||||||
|
|
||||||
@patch('main.extra_func.zenpy')
|
@patch('main.extra_func.zenpy')
|
||||||
def test_light_agent_make_engineer(self, ZenpyMock):
|
def test_light_agent_make_engineer(self, zenpy_mock):
|
||||||
self.client.post(reverse_lazy('work_become_engineer'))
|
"""
|
||||||
self.assertEqual(ZenpyMock.update_user.call_args[0][0].custom_role_id, sets.ZENDESK_ROLES['engineer'])
|
Функция проверки назначения 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')
|
@patch('main.extra_func.zenpy')
|
||||||
def test_admin_make_engineer(self, ZenpyMock):
|
def test_admin_make_engineer(self, zenpy_mock):
|
||||||
|
"""
|
||||||
|
Функция проверки назначения admin на роль engineer.
|
||||||
|
"""
|
||||||
self.admin_client.post(reverse_lazy('work_become_engineer'))
|
self.admin_client.post(reverse_lazy('work_become_engineer'))
|
||||||
self.assertEqual(ZenpyMock.update_user.call_args[0][0].custom_role_id, sets.ZENDESK_ROLES['engineer'])
|
self.assertEqual(zenpy_mock.update_user.call_args[0][0].custom_role_id, sets.ZENDESK_ROLES['engineer'])
|
||||||
|
|
||||||
@patch('main.extra_func.zenpy')
|
@patch('main.extra_func.zenpy')
|
||||||
def test_engineer_make_engineer(self, ZenpyMock):
|
def test_engineer_make_engineer(self, zenpy_mock):
|
||||||
client = Client()
|
"""
|
||||||
client.force_login(User.objects.get(email=self.engineer))
|
Функция проверки назначения engineer на роль engineer.
|
||||||
client.post(reverse_lazy('work_become_engineer'))
|
"""
|
||||||
self.assertEqual(ZenpyMock.update_user.call_args[0][0].custom_role_id, sets.ZENDESK_ROLES['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')
|
@patch('main.extra_func.zenpy')
|
||||||
def test_control_page_make_one(self, ZenpyMock):
|
def test_control_page_make_engineer_one(self, zenpy_mock):
|
||||||
|
"""
|
||||||
|
Функция проверки назначения администратором на роль engineer одного пользователя.
|
||||||
|
"""
|
||||||
self.admin_client.post(
|
self.admin_client.post(
|
||||||
reverse_lazy('control'),
|
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 = ZenpyMock.update_user.call_args_list
|
call_list = zenpy_mock.update_user.call_args_list
|
||||||
mock_object = call_list[0][0][0]
|
mock_object = call_list[0][0][0]
|
||||||
self.assertEqual(len(call_list), 1)
|
self.assertEqual(len(call_list), 1)
|
||||||
self.assertEqual(mock_object.custom_role_id, sets.ZENDESK_ROLES['engineer'])
|
self.assertEqual(mock_object.custom_role_id, sets.ZENDESK_ROLES['engineer'])
|
||||||
|
|
||||||
@patch('main.extra_func.zenpy')
|
@patch('main.extra_func.zenpy')
|
||||||
def test_control_page_make_many(self, ZenpyMock):
|
def test_control_page_make_engineer_many(self, zenpy_mock):
|
||||||
|
"""
|
||||||
|
Функция проверки назначения администратором на роль engineer нескольких пользователей.
|
||||||
|
"""
|
||||||
self.admin_client.post(
|
self.admin_client.post(
|
||||||
reverse_lazy('control'),
|
reverse_lazy('control'),
|
||||||
data={
|
data={
|
||||||
'users': [
|
'users': [
|
||||||
User.objects.get(email=self.light_agent).userprofile.id,
|
get_user_model().objects.get(email=self.light_agent).userprofile.id,
|
||||||
User.objects.get(email=self.engineer).userprofile.id,
|
get_user_model().objects.get(email=self.engineer).userprofile.id,
|
||||||
],
|
],
|
||||||
'engineer': 'engineer'
|
'engineer': 'engineer'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
call_list = ZenpyMock.update_user.call_args_list
|
call_list = zenpy_mock.update_user.call_args_list
|
||||||
mock_objects = list(call_list)
|
mock_objects = list(call_list)
|
||||||
self.assertEqual(len(call_list), 2)
|
self.assertEqual(len(call_list), 2)
|
||||||
for obj in mock_objects:
|
for obj in mock_objects:
|
||||||
self.assertEqual(obj[0][0].custom_role_id, sets.ZENDESK_ROLES['engineer'])
|
self.assertEqual(obj[0][0].custom_role_id, sets.ZENDESK_ROLES['engineer'])
|
||||||
|
|
||||||
|
|
||||||
class PasswordResetTestCase(TestCase):
|
class MakeLightAgentTestCase(UsersBaseTestCase):
|
||||||
fixtures = ['fixtures/test_make_engineer.json']
|
|
||||||
|
|
||||||
|
@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 = 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)
|
||||||
|
|
||||||
|
@patch('main.requester.TicketListRequester.get_tickets_list_for_user', side_effect=[[]])
|
||||||
|
@patch('main.extra_func.zenpy')
|
||||||
|
def test_engineer_make_light_agent_no_tickets(self, zenpy_mock, _user_tickets_mock):
|
||||||
|
self.engineer_client.post(reverse_lazy('work_hand_over'))
|
||||||
|
self.assertEqual(zenpy_mock.update_user.call_args[0][0].custom_role_id, sets.ZENDESK_ROLES['light_agent'])
|
||||||
|
|
||||||
|
@patch('main.requester.TicketListRequester.get_tickets_list_for_user', side_effect=[
|
||||||
|
[Mock(id=1, status='solved'), Mock(id=2, status='open'), Mock(id=3, status='open')]
|
||||||
|
])
|
||||||
|
@patch('main.extra_func.zenpy')
|
||||||
|
def test_engineer_make_light_agent_with_tickets(self, zenpy_mock, _user_tickets_mock):
|
||||||
|
zenpy_mock.solved_tickets_user_id = Mock()
|
||||||
|
self.engineer_client.post(reverse_lazy('work_hand_over'))
|
||||||
|
|
||||||
|
tickets_update = zenpy_mock.admin.tickets.update.call_args[0][0]
|
||||||
|
self.assertEqual(tickets_update[0].assignee_id, zenpy_mock.solved_tickets_user_id)
|
||||||
|
self.assertIsNone(tickets_update[1].assignee)
|
||||||
|
self.assertIsNone(tickets_update[2].assignee)
|
||||||
|
self.assertEqual(zenpy_mock.update_user.call_args[0][0].custom_role_id, sets.ZENDESK_ROLES['light_agent'])
|
||||||
|
|
||||||
|
@patch('main.requester.TicketListRequester.get_tickets_list_for_user', side_effect=[[]])
|
||||||
|
@patch('main.extra_func.zenpy')
|
||||||
|
def test_admin_make_light_agent_no_tickets(self, zenpy_mock, _user_tickets_mock):
|
||||||
|
self.admin_client.post(reverse_lazy('work_hand_over'))
|
||||||
|
self.assertEqual(zenpy_mock.update_user.call_args[0][0].custom_role_id, sets.ZENDESK_ROLES['light_agent'])
|
||||||
|
|
||||||
|
@patch('main.requester.TicketListRequester.get_tickets_list_for_user', side_effect=[
|
||||||
|
[Mock(id=1, status='solved'), Mock(id=2, status='open'), Mock(id=3, status='open')]
|
||||||
|
])
|
||||||
|
@patch('main.extra_func.zenpy')
|
||||||
|
def test_admin_make_light_agent_with_tickets(self, zenpy_mock, _user_tickets_mock):
|
||||||
|
zenpy_mock.solved_tickets_user_id = Mock()
|
||||||
|
self.admin_client.post(reverse_lazy('work_hand_over'))
|
||||||
|
|
||||||
|
tickets_update = zenpy_mock.admin.tickets.update.call_args[0][0]
|
||||||
|
self.assertEqual(tickets_update[0].assignee_id, zenpy_mock.solved_tickets_user_id)
|
||||||
|
self.assertIsNone(tickets_update[1].assignee)
|
||||||
|
self.assertIsNone(tickets_update[2].assignee)
|
||||||
|
self.assertEqual(zenpy_mock.update_user.call_args[0][0].custom_role_id, sets.ZENDESK_ROLES['light_agent'])
|
||||||
|
|
||||||
|
@patch('main.requester.TicketListRequester.get_tickets_list_for_user', side_effect=[[]])
|
||||||
|
@patch('main.extra_func.zenpy')
|
||||||
|
def test_light_agent_make_light_agent(self, zenpy_mock, _user_tickets_mock):
|
||||||
|
self.agent_client.post(reverse_lazy('work_hand_over'))
|
||||||
|
self.assertEqual(zenpy_mock.update_user.call_args[0][0].custom_role_id, sets.ZENDESK_ROLES['light_agent'])
|
||||||
|
|
||||||
|
@patch('main.requester.TicketListRequester.get_tickets_list_for_user', side_effect=[[]])
|
||||||
|
@patch('main.extra_func.zenpy')
|
||||||
|
def test_control_page_make_light_agent_one(self, zenpy_mock, _user_tickets_mock):
|
||||||
|
self.admin_client.post(
|
||||||
|
reverse_lazy('control'),
|
||||||
|
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]
|
||||||
|
self.assertEqual(len(call_list), 1)
|
||||||
|
self.assertEqual(mock_object.custom_role_id, sets.ZENDESK_ROLES['light_agent'])
|
||||||
|
|
||||||
|
@patch('main.requester.TicketListRequester.get_tickets_list_for_user', side_effect=[[], []])
|
||||||
|
@patch('main.extra_func.zenpy')
|
||||||
|
def test_control_page_make_light_agent_many(self, zenpy_mock, _user_tickets_mock):
|
||||||
|
self.admin_client.post(
|
||||||
|
reverse_lazy('control'),
|
||||||
|
data={
|
||||||
|
'users': [
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
call_list = zenpy_mock.update_user.call_args_list
|
||||||
|
mock_objects = list(call_list)
|
||||||
|
self.assertEqual(len(call_list), 2)
|
||||||
|
for obj in mock_objects:
|
||||||
|
self.assertEqual(obj[0][0].custom_role_id, sets.ZENDESK_ROLES['light_agent'])
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordResetTestCase(UsersBaseTestCase):
|
||||||
|
"""
|
||||||
|
Класс тестов сброса пароля.
|
||||||
|
"""
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.user = '123@test.ru'
|
"""
|
||||||
|
Предустановленные значения для проведения тестов.
|
||||||
|
"""
|
||||||
|
super().setUp()
|
||||||
self.email_backend = 'django.core.mail.backends.locmem.EmailBackend'
|
self.email_backend = 'django.core.mail.backends.locmem.EmailBackend'
|
||||||
self.client = Client()
|
|
||||||
self.client.force_login(User.objects.get(email=self.user))
|
|
||||||
|
|
||||||
def test_redirect(self):
|
def test_redirect(self):
|
||||||
|
"""
|
||||||
|
Функция проверки переадресации на страницу уведомления о сбросе пароля на email.
|
||||||
|
"""
|
||||||
with self.settings(EMAIL_BACKEND=self.email_backend):
|
with self.settings(EMAIL_BACKEND=self.email_backend):
|
||||||
resp = self.client.post(reverse_lazy('password_reset'), data={'email': self.user})
|
resp = self.agent_client.post(reverse_lazy('password_reset'), data={'email': self.light_agent})
|
||||||
self.assertRedirects(resp, reverse('password_reset_done'))
|
self.assertRedirects(resp, reverse('password_reset_done'))
|
||||||
self.assertEqual(resp.status_code, 302)
|
self.assertEqual(resp.status_code, 302)
|
||||||
|
|
||||||
def test_send_email(self):
|
def test_send_email(self):
|
||||||
|
"""
|
||||||
|
Функция проверки содержания и отправки письма для установки пароля.
|
||||||
|
"""
|
||||||
with self.settings(EMAIL_BACKEND=self.email_backend):
|
with self.settings(EMAIL_BACKEND=self.email_backend):
|
||||||
response: HttpResponseRedirect = \
|
response: HttpResponseRedirect = \
|
||||||
self.client.post(reverse_lazy('password_reset'), data={'email': self.user})
|
self.agent_client.post(reverse_lazy('password_reset'), data={'email': self.light_agent})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(len(mail.outbox), 1)
|
self.assertEqual(len(mail.outbox), 1)
|
||||||
self.assertEqual(mail.outbox[0].to, [self.user])
|
self.assertEqual(mail.outbox[0].to, [self.light_agent])
|
||||||
|
|
||||||
# context that the email template was rendered with
|
# context that the email template was rendered with
|
||||||
email_context = response.context[0].dicts[1]
|
email_context = response.context[0].dicts[1]
|
||||||
@ -164,35 +321,54 @@ class PasswordResetTestCase(TestCase):
|
|||||||
self.assertEqual(mail.outbox[0].body, correct_body)
|
self.assertEqual(mail.outbox[0].body, correct_body)
|
||||||
|
|
||||||
def test_email_invalid(self):
|
def test_email_invalid(self):
|
||||||
|
"""
|
||||||
|
Функция проверки уведомления клиента о некорректности введенного email.
|
||||||
|
"""
|
||||||
with self.settings(EMAIL_BACKEND=self.email_backend) and translation.override('ru'):
|
with self.settings(EMAIL_BACKEND=self.email_backend) and translation.override('ru'):
|
||||||
resp = self.client.post(reverse_lazy('password_reset'), data={'email': 1})
|
resp = self.agent_client.post(reverse_lazy('password_reset'), data={'email': 1})
|
||||||
self.assertContains(resp, 'Введите правильный адрес электронной почты.', count=1, status_code=200)
|
self.assertContains(resp, 'Введите правильный адрес электронной почты.', count=1, status_code=200)
|
||||||
|
|
||||||
def test_user_does_not_exist(self):
|
def test_user_does_not_exist(self):
|
||||||
|
"""
|
||||||
|
Функция корректности отработки неверно введенного email.
|
||||||
|
"""
|
||||||
with self.settings(EMAIL_BACKEND=self.email_backend):
|
with self.settings(EMAIL_BACKEND=self.email_backend):
|
||||||
resp = self.client.post(reverse_lazy('password_reset'), data={'email': self.user + 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.assertRedirects(resp, reverse('password_reset_done'))
|
||||||
self.assertEqual(resp.status_code, 302)
|
self.assertEqual(resp.status_code, 302)
|
||||||
self.assertEqual(len(mail.outbox), 0)
|
self.assertEqual(len(mail.outbox), 0)
|
||||||
|
|
||||||
|
|
||||||
class PasswordChangeTestCase(TestCase):
|
class PasswordChangeTestCase(UsersBaseTestCase):
|
||||||
fixtures = ['fixtures/test_make_engineer.json']
|
"""
|
||||||
|
Класс тестирования смены пароля.
|
||||||
|
"""
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.user = '123@test.ru'
|
"""
|
||||||
self.client = Client()
|
Предустановленные значения для проведения тестов.
|
||||||
self.client.force_login(User.objects.get(email=self.user))
|
"""
|
||||||
|
super().setUp()
|
||||||
self.set_password()
|
self.set_password()
|
||||||
|
|
||||||
def set_password(self):
|
def set_password(self):
|
||||||
user: User = User.objects.get(email=self.user)
|
"""
|
||||||
|
Пароль, сформированный для тестирования.
|
||||||
|
"""
|
||||||
|
user: get_user_model() = get_user_model().objects.get(email=self.light_agent)
|
||||||
user.set_password('ImpossiblyHardPassword')
|
user.set_password('ImpossiblyHardPassword')
|
||||||
user.save()
|
user.save()
|
||||||
self.client.force_login(User.objects.get(email=self.user))
|
self.agent_client.force_login(get_user_model().objects.get(email=self.light_agent))
|
||||||
|
|
||||||
def test_change_successful(self):
|
def test_change_successful(self):
|
||||||
self.client.post(
|
"""
|
||||||
|
Функция тестирования успешного изменения пароля.
|
||||||
|
"""
|
||||||
|
self.agent_client.post(
|
||||||
reverse_lazy('password_change'),
|
reverse_lazy('password_change'),
|
||||||
data={
|
data={
|
||||||
'old_password': 'ImpossiblyHardPassword',
|
'old_password': 'ImpossiblyHardPassword',
|
||||||
@ -200,12 +376,15 @@ class PasswordChangeTestCase(TestCase):
|
|||||||
'new_password2': 'EasyPassword',
|
'new_password2': 'EasyPassword',
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
user = User.objects.get(email=self.user)
|
user = get_user_model().objects.get(email=self.light_agent)
|
||||||
self.assertTrue(user.check_password('EasyPassword'))
|
self.assertTrue(user.check_password('EasyPassword'))
|
||||||
|
|
||||||
def test_invalid_old_password(self):
|
def test_invalid_old_password(self):
|
||||||
|
"""
|
||||||
|
Функция тестирования отработки неверно введенного старого пароля при смене.
|
||||||
|
"""
|
||||||
with translation.override('ru'):
|
with translation.override('ru'):
|
||||||
resp = self.client.post(
|
resp = self.agent_client.post(
|
||||||
reverse_lazy('password_change'),
|
reverse_lazy('password_change'),
|
||||||
data={
|
data={
|
||||||
'old_password': 'EasyPassword',
|
'old_password': 'EasyPassword',
|
||||||
@ -216,8 +395,11 @@ class PasswordChangeTestCase(TestCase):
|
|||||||
self.assertContains(resp, 'Ваш старый пароль введен неправильно', count=1, status_code=200)
|
self.assertContains(resp, 'Ваш старый пароль введен неправильно', count=1, status_code=200)
|
||||||
|
|
||||||
def test_different_new_passwords(self):
|
def test_different_new_passwords(self):
|
||||||
|
"""
|
||||||
|
Функция тестирования случая с вводом двух разных новых паролей.
|
||||||
|
"""
|
||||||
with translation.override('ru'):
|
with translation.override('ru'):
|
||||||
resp = self.client.post(
|
resp = self.agent_client.post(
|
||||||
reverse_lazy('password_change'),
|
reverse_lazy('password_change'),
|
||||||
data={
|
data={
|
||||||
'old_password': 'ImpossiblyHardPassword',
|
'old_password': 'ImpossiblyHardPassword',
|
||||||
@ -228,8 +410,11 @@ class PasswordChangeTestCase(TestCase):
|
|||||||
self.assertContains(resp, 'Введенные пароли не совпадают', count=1, status_code=200)
|
self.assertContains(resp, 'Введенные пароли не совпадают', count=1, status_code=200)
|
||||||
|
|
||||||
def test_invalid_new_password1(self):
|
def test_invalid_new_password1(self):
|
||||||
|
"""
|
||||||
|
Функция тестирования случая с неправильно подобранным новым паролем (слишком короткий).
|
||||||
|
"""
|
||||||
with translation.override('ru'):
|
with translation.override('ru'):
|
||||||
resp = self.client.post(
|
resp = self.agent_client.post(
|
||||||
reverse_lazy('password_change'),
|
reverse_lazy('password_change'),
|
||||||
data={
|
data={
|
||||||
'old_password': 'ImpossiblyHardPassword',
|
'old_password': 'ImpossiblyHardPassword',
|
||||||
@ -240,8 +425,11 @@ class PasswordChangeTestCase(TestCase):
|
|||||||
self.assertContains(resp, 'Введённый пароль слишком короткий', count=1, status_code=200)
|
self.assertContains(resp, 'Введённый пароль слишком короткий', count=1, status_code=200)
|
||||||
|
|
||||||
def test_invalid_new_password2(self):
|
def test_invalid_new_password2(self):
|
||||||
|
"""
|
||||||
|
Функция тестирования случая с неправильно подобранным новым паролем (употребляются только цифры).
|
||||||
|
"""
|
||||||
with translation.override('ru'):
|
with translation.override('ru'):
|
||||||
resp = self.client.post(
|
resp = self.agent_client.post(
|
||||||
reverse_lazy('password_change'),
|
reverse_lazy('password_change'),
|
||||||
data={
|
data={
|
||||||
'old_password': 'ImpossiblyHardPassword',
|
'old_password': 'ImpossiblyHardPassword',
|
||||||
@ -252,105 +440,97 @@ class PasswordChangeTestCase(TestCase):
|
|||||||
self.assertContains(resp, 'Введённый пароль состоит только из цифр', count=1, status_code=200)
|
self.assertContains(resp, 'Введённый пароль состоит только из цифр', count=1, status_code=200)
|
||||||
|
|
||||||
def test_invalid_new_password3(self):
|
def test_invalid_new_password3(self):
|
||||||
|
"""
|
||||||
|
Функция тестирования случая с неправильно подобранным новым паролем (совпадает с именем пользователя).
|
||||||
|
"""
|
||||||
with translation.override('ru'):
|
with translation.override('ru'):
|
||||||
resp = self.client.post(
|
resp = self.agent_client.post(
|
||||||
reverse_lazy('password_change'),
|
reverse_lazy('password_change'),
|
||||||
data={
|
data={
|
||||||
'old_password': 'ImpossiblyHardPassword',
|
'old_password': 'ImpossiblyHardPassword',
|
||||||
'new_password1': self.user,
|
'new_password1': self.light_agent,
|
||||||
'new_password2': self.user,
|
'new_password2': self.light_agent,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
self.assertContains(resp, 'Введённый пароль слишком похож на имя пользователя', count=1, status_code=200)
|
self.assertContains(resp, 'Введённый пароль слишком похож на имя пользователя', count=1, status_code=200)
|
||||||
|
|
||||||
|
|
||||||
class GetTicketsTestCase(TestCase):
|
class GetTicketsTestCase(UsersBaseTestCase):
|
||||||
"""
|
"""
|
||||||
Класс тестов для проверки функции получения тикетов.
|
Класс тестов для проверки функции получения тикетов.
|
||||||
"""
|
"""
|
||||||
fixtures = ['fixtures/test_make_engineer.json']
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
"""
|
|
||||||
Предустановленные значения для проведения тестов.
|
|
||||||
"""
|
|
||||||
self.light_agent = '123@test.ru'
|
|
||||||
self.engineer = 'customer@example.com'
|
|
||||||
self.client = Client()
|
|
||||||
self.client.force_login(User.objects.get(email=self.engineer))
|
|
||||||
self.light_agent_client = Client()
|
|
||||||
self.light_agent_client.force_login(User.objects.get(email=self.light_agent))
|
|
||||||
|
|
||||||
@patch('main.views.zenpy.get_user')
|
@patch('main.views.zenpy.get_user')
|
||||||
@patch('main.extra_func.zenpy')
|
@patch('main.extra_func.zenpy')
|
||||||
def test_redirect(self, ZenpyMock, GetUserMock):
|
def test_redirect(self, _zenpy_mock, get_user_mock):
|
||||||
"""
|
"""
|
||||||
Функция проверки переадресации пользователя на рабочую страницу.
|
Функция проверки переадресации пользователя на рабочую страницу.
|
||||||
"""
|
"""
|
||||||
GetUserMock.return_value = Mock()
|
get_user_mock.return_value = Mock()
|
||||||
user = User.objects.get(email=self.engineer)
|
user = get_user_model().objects.get(email=self.engineer)
|
||||||
resp = self.client.post(reverse('work_get_tickets'))
|
resp = self.engineer_client.post(reverse('work_get_tickets'))
|
||||||
self.assertRedirects(resp, reverse('work', args=[user.id]))
|
self.assertRedirects(resp, reverse('work', args=[user.id]))
|
||||||
self.assertEqual(resp.status_code, 302)
|
self.assertEqual(resp.status_code, 302)
|
||||||
|
self.assertFalse(_zenpy_mock.called)
|
||||||
|
|
||||||
@patch('main.views.zenpy')
|
@patch('main.views.zenpy')
|
||||||
@patch('main.views.get_tickets_list_for_group')
|
@patch('main.views.get_tickets_list_for_group')
|
||||||
def test_take_one_ticket(self, TicketsMock, ZenpyViewsMock):
|
def test_take_one_ticket(self, group_tickets_mock, zenpy_mock):
|
||||||
"""
|
"""
|
||||||
Функция проверки назначения одного тикета на engineer.
|
Функция проверки назначения одного тикета на engineer.
|
||||||
"""
|
"""
|
||||||
TicketsMock.return_value = [Mock()]
|
group_tickets_mock.return_value = [Mock()]
|
||||||
ZenpyViewsMock.get_user.return_value = Mock(role='agent', custom_role_id=sets.ZENDESK_ROLES['engineer'])
|
zenpy_mock.get_user.return_value = Mock(role='agent', custom_role_id=sets.ZENDESK_ROLES['engineer'])
|
||||||
self.client.post(reverse('work_get_tickets'), data={'count_tickets': 1})
|
self.engineer_client.post(reverse('work_get_tickets'), data={'count_tickets': 1})
|
||||||
tickets = ZenpyViewsMock.update_tickets.call_args
|
tickets = zenpy_mock.update_tickets.call_args
|
||||||
self.assertEqual(tickets[0][0][0].assignee, ZenpyViewsMock.get_user.return_value)
|
self.assertEqual(tickets[0][0][0].assignee, zenpy_mock.get_user.return_value)
|
||||||
|
|
||||||
@patch('main.views.get_tickets_list_for_group')
|
@patch('main.views.get_tickets_list_for_group')
|
||||||
@patch('main.views.zenpy')
|
@patch('main.views.zenpy')
|
||||||
def test_take_many_tickets(self, ZenpyMock, TicketsMock):
|
def test_take_many_tickets(self, zenpy_mock, group_tickets_mock):
|
||||||
"""
|
"""
|
||||||
Функция проверки назначения нескольких тикетов на engineer.
|
Функция проверки назначения нескольких тикетов на engineer.
|
||||||
"""
|
"""
|
||||||
TicketsMock.return_value = [Mock()] * 3
|
group_tickets_mock.return_value = [Mock()] * 3
|
||||||
ZenpyMock.get_user.return_value = Mock(role='agent', custom_role_id=sets.ZENDESK_ROLES['engineer'])
|
zenpy_mock.get_user.return_value = Mock(role='agent', custom_role_id=sets.ZENDESK_ROLES['engineer'])
|
||||||
self.client.post(reverse('work_get_tickets'), data={'count_tickets': 3})
|
self.engineer_client.post(reverse('work_get_tickets'), data={'count_tickets': 3})
|
||||||
tickets = ZenpyMock.update_tickets.call_args
|
tickets = zenpy_mock.update_tickets.call_args
|
||||||
for ticket in tickets[0][0]:
|
for ticket in tickets[0][0]:
|
||||||
self.assertEqual(ticket.assignee, ZenpyMock.get_user.return_value)
|
self.assertEqual(ticket.assignee, zenpy_mock.get_user.return_value)
|
||||||
|
|
||||||
@patch('main.views.zenpy.get_user')
|
@patch('main.views.zenpy.get_user')
|
||||||
@patch('main.views.zenpy')
|
@patch('main.views.zenpy')
|
||||||
def test_light_agent_take_ticket(self, ZenpyMock, GetUserMock):
|
def test_light_agent_take_ticket(self, zenpy_mock, get_user_mock):
|
||||||
"""
|
"""
|
||||||
Функция проверки попытки назначения тикета на light_agent.
|
Функция проверки попытки назначения тикета на light_agent.
|
||||||
"""
|
"""
|
||||||
GetUserMock.return_value = Mock(role='agent', custom_role_id=sets.ZENDESK_ROLES['light_agent'])
|
get_user_mock.return_value = Mock(role='agent', custom_role_id=sets.ZENDESK_ROLES['light_agent'])
|
||||||
self.light_agent_client.post(reverse('work_get_tickets'), data={'count_tickets': 3})
|
self.agent_client.post(reverse('work_get_tickets'), data={'count_tickets': 3})
|
||||||
tickets = ZenpyMock.update_tickets.call_args
|
tickets = zenpy_mock.update_tickets.call_args
|
||||||
self.assertIsNone(tickets)
|
self.assertIsNone(tickets)
|
||||||
|
|
||||||
@patch('main.views.zenpy')
|
@patch('main.views.zenpy')
|
||||||
@patch('main.views.get_tickets_list_for_group')
|
@patch('main.views.get_tickets_list_for_group')
|
||||||
def test_take_zero_tickets(self, TicketsMock, ZenpyMock):
|
def test_take_zero_tickets(self, tickets_mock, zenpy_mock):
|
||||||
"""
|
"""
|
||||||
Функция проверки попытки назначения нуля тикета на engineer.
|
Функция проверки попытки назначения нуля тикета на engineer.
|
||||||
"""
|
"""
|
||||||
TicketsMock.return_value = [Mock()] * 3
|
tickets_mock.return_value = [Mock()] * 3
|
||||||
ZenpyMock.get_user.return_value = Mock(role='agent', custom_role_id=sets.ZENDESK_ROLES['engineer'])
|
zenpy_mock.get_user.return_value = Mock(role='agent', custom_role_id=sets.ZENDESK_ROLES['engineer'])
|
||||||
self.client.post(reverse('work_get_tickets'), data={'count_tickets': 0})
|
self.engineer_client.post(reverse('work_get_tickets'), data={'count_tickets': 0})
|
||||||
tickets = ZenpyMock.update_tickets.call_args[0][0]
|
tickets = zenpy_mock.update_tickets.call_args[0][0]
|
||||||
self.assertListEqual(tickets, [])
|
self.assertListEqual(tickets, [])
|
||||||
|
|
||||||
@patch('main.views.get_tickets_list_for_group')
|
@patch('main.views.get_tickets_list_for_group')
|
||||||
@patch('main.views.zenpy')
|
@patch('main.views.zenpy')
|
||||||
def test_take_invalid_count_tickets(self, ZenpyMock, TicketsMock, ):
|
def test_take_invalid_count_tickets(self, zenpy_mock, group_tickets_mock):
|
||||||
"""
|
"""
|
||||||
Функция проверки попытки назначения нуля тикетов на engineer.
|
Функция проверки попытки назначения нуля тикетов на engineer.
|
||||||
"""
|
"""
|
||||||
TicketsMock.return_value = [Mock()] * 3
|
group_tickets_mock.return_value = [Mock()] * 3
|
||||||
ZenpyMock.get_user.return_value = Mock(role='agent', custom_role_id=sets.ZENDESK_ROLES['engineer'])
|
zenpy_mock.get_user.return_value = Mock(role='agent', custom_role_id=sets.ZENDESK_ROLES['engineer'])
|
||||||
self.client.post(reverse('work_get_tickets'), data={'count_tickets': 'asd'})
|
self.engineer_client.post(reverse('work_get_tickets'), data={'count_tickets': 'asd'})
|
||||||
tickets = ZenpyMock.update_tickets.call_args
|
tickets = zenpy_mock.update_tickets.call_args
|
||||||
self.assertIsNone(tickets)
|
self.assertIsNone(tickets)
|
||||||
|
|
||||||
|
|
||||||
@ -367,9 +547,9 @@ class ProfileTestCase(TestCase):
|
|||||||
self.zendesk_agent_email = 'krav-88@mail.ru'
|
self.zendesk_agent_email = 'krav-88@mail.ru'
|
||||||
self.zendesk_admin_email = 'idar.sokurov.05@mail.ru'
|
self.zendesk_admin_email = 'idar.sokurov.05@mail.ru'
|
||||||
self.client = Client()
|
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 = 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):
|
def test_correct_username(self):
|
||||||
"""
|
"""
|
||||||
@ -412,3 +592,42 @@ class ProfileTestCase(TestCase):
|
|||||||
resp = self.client.get(reverse('profile'))
|
resp = self.client.get(reverse('profile'))
|
||||||
user = zenpy.get_user(self.zendesk_agent_email)
|
user = zenpy.get_user(self.zendesk_agent_email)
|
||||||
self.assertEqual(resp.context['profile'].image, user.photo['content_url'] if user.photo else None)
|
self.assertEqual(resp.context['profile'].image, user.photo['content_url'] if user.photo else None)
|
||||||
|
|
||||||
|
|
||||||
|
class LoggingTestCase(UsersBaseTestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
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():
|
||||||
|
with open('logs/logs.csv', 'r') as file:
|
||||||
|
file_output = file.readlines()[-1]
|
||||||
|
return file_output
|
||||||
|
|
||||||
|
def test_engineer_with_admin(self):
|
||||||
|
log(self.engineer_profile, self.admin_profile)
|
||||||
|
file_output = self.get_file_output()
|
||||||
|
self.assertEqual(file_output, f'UserForAccessTest,engineer,'
|
||||||
|
f'{str(timezone.now().today())[:16]},ZendeskAdmin\n')
|
||||||
|
|
||||||
|
def test_engineer_without_admin(self):
|
||||||
|
log(self.engineer_profile)
|
||||||
|
file_output = self.get_file_output()
|
||||||
|
self.assertEqual(file_output, f'UserForAccessTest,engineer,'
|
||||||
|
f'{str(timezone.now().today())[:16]},UserForAccessTest\n')
|
||||||
|
|
||||||
|
def test_light_agent_with_admin(self):
|
||||||
|
log(self.agent_profile, self.admin_profile)
|
||||||
|
file_output = self.get_file_output()
|
||||||
|
self.assertEqual(file_output, f'UserForAccessTest,light_agent,'
|
||||||
|
f'{str(timezone.now().today())[:16]},ZendeskAdmin\n')
|
||||||
|
|
||||||
|
def test_light_agent_without_admin(self):
|
||||||
|
log(self.agent_profile)
|
||||||
|
file_output = self.get_file_output()
|
||||||
|
self.assertEqual(file_output, f'UserForAccessTest,light_agent,'
|
||||||
|
f'{str(timezone.now().today())[:16]},UserForAccessTest\n')
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
"""
|
||||||
|
REST framework adds support for automatic URL routing to Django.
|
||||||
|
"""
|
||||||
|
|
||||||
from rest_framework.routers import DefaultRouter
|
from rest_framework.routers import DefaultRouter
|
||||||
from main.views import UsersViewSet
|
from main.views import UsersViewSet
|
||||||
|
|
||||||
|
119
main/views.py
119
main/views.py
@ -1,10 +1,17 @@
|
|||||||
|
"""
|
||||||
|
View функции.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
from smtplib import SMTPException
|
from smtplib import SMTPException
|
||||||
|
from typing import Dict, Any, Optional
|
||||||
|
|
||||||
from django.contrib import messages
|
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.decorators import login_required
|
||||||
from django.contrib.auth.forms import PasswordResetForm
|
from django.contrib.auth.forms import PasswordResetForm
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
|
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.tokens import default_token_generator
|
||||||
from django.contrib.auth.views import LoginView
|
from django.contrib.auth.views import LoginView
|
||||||
from django.contrib.contenttypes.models import ContentType
|
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.urls import reverse_lazy
|
||||||
from django.views.generic import FormView
|
from django.views.generic import FormView
|
||||||
from django_registration.views import RegistrationView
|
from django_registration.views import RegistrationView
|
||||||
# Django REST
|
|
||||||
from rest_framework import viewsets
|
from rest_framework import viewsets
|
||||||
from rest_framework.response import Response
|
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, \
|
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, \
|
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
|
set_session_params_for_work_page, get_tickets_list_for_group
|
||||||
from main.forms import AdminPageUsers, CustomRegistrationForm, CustomAuthenticationForm, StatisticForm, \
|
from main.forms import AdminPageUsers, CustomRegistrationForm, CustomAuthenticationForm, \
|
||||||
WorkGetTicketsForm
|
StatisticForm, WorkGetTicketsForm
|
||||||
from main.serializers import ProfileSerializer, ZendeskUserSerializer
|
from main.serializers import ProfileSerializer, ZendeskUserSerializer
|
||||||
from main.zendesk_admin import zenpy
|
from main.zendesk_admin import zenpy
|
||||||
from .models import UserProfile
|
from .models import UserProfile
|
||||||
from .statistic_data import StatisticData
|
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):
|
class CustomRegistrationView(RegistrationView):
|
||||||
"""
|
"""
|
||||||
Отображение и логика работы страницы регистрации пользователя.
|
Отображение и логика работы страницы регистрации пользователя.
|
||||||
@ -41,9 +69,11 @@ class CustomRegistrationView(RegistrationView):
|
|||||||
:type template_name: :class:`str`
|
:type template_name: :class:`str`
|
||||||
:param success_url: Указание пути к html-странице завершения регистрации
|
:param success_url: Указание пути к html-странице завершения регистрации
|
||||||
:type success_url: :class:`django.utils.functional.lazy.<locals>.__proxy__`
|
: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`
|
:type is_allowed: :class:`bool`
|
||||||
"""
|
"""
|
||||||
|
extra_context = setup_context(registration_lit=True)
|
||||||
form_class = CustomRegistrationForm
|
form_class = CustomRegistrationForm
|
||||||
template_name = 'django_registration/registration_form.html'
|
template_name = 'django_registration/registration_form.html'
|
||||||
urls = {
|
urls = {
|
||||||
@ -53,7 +83,7 @@ class CustomRegistrationView(RegistrationView):
|
|||||||
}
|
}
|
||||||
redirect_url = 'done'
|
redirect_url = 'done'
|
||||||
|
|
||||||
def register(self, form: CustomRegistrationForm) -> User:
|
def register(self, form: CustomRegistrationForm) -> Optional[get_user_model()]:
|
||||||
"""
|
"""
|
||||||
Функция регистрации пользователя.
|
Функция регистрации пользователя.
|
||||||
1. Ввод email пользователя, указанный на Zendesk
|
1. Ввод email пользователя, указанный на Zendesk
|
||||||
@ -62,7 +92,7 @@ class CustomRegistrationView(RegistrationView):
|
|||||||
3. Создается пользователь class User, а также его профиль.
|
3. Создается пользователь class User, а также его профиль.
|
||||||
|
|
||||||
:param form: Email пользователя на Zendesk
|
:param form: Email пользователя на Zendesk
|
||||||
:return: user
|
:return: User
|
||||||
"""
|
"""
|
||||||
self.redirect_url = 'done'
|
self.redirect_url = 'done'
|
||||||
if check_user_exist(form.data['email']) and get_user_organization(form.data['email']) == 'SYSTEM':
|
if check_user_exist(form.data['email']) and get_user_organization(form.data['email']) == 'SYSTEM':
|
||||||
@ -78,10 +108,10 @@ class CustomRegistrationView(RegistrationView):
|
|||||||
'html_email_template_name': None,
|
'html_email_template_name': None,
|
||||||
'extra_email_context': None,
|
'extra_email_context': None,
|
||||||
}
|
}
|
||||||
user = User.objects.create_user(
|
user = get_user_model().objects.create_user(
|
||||||
username=form.data['email'],
|
username=form.data['email'],
|
||||||
email=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:
|
try:
|
||||||
update_profile(user.userprofile)
|
update_profile(user.userprofile)
|
||||||
@ -90,13 +120,16 @@ class CustomRegistrationView(RegistrationView):
|
|||||||
return user
|
return user
|
||||||
except SMTPException:
|
except SMTPException:
|
||||||
self.redirect_url = 'email_sending_error'
|
self.redirect_url = 'email_sending_error'
|
||||||
|
return None
|
||||||
else:
|
else:
|
||||||
self.redirect_url = 'email_sending_error'
|
self.redirect_url = 'email_sending_error'
|
||||||
|
return None
|
||||||
else:
|
else:
|
||||||
self.redirect_url = 'invalid_zendesk_email'
|
self.redirect_url = 'invalid_zendesk_email'
|
||||||
|
return None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def set_permission(user: User) -> None:
|
def set_permission(user: get_user_model()) -> None:
|
||||||
"""
|
"""
|
||||||
Функция дает разрешение на просмотр страница администратора, если пользователь имеет роль admin.
|
Функция дает разрешение на просмотр страница администратора, если пользователь имеет роль admin.
|
||||||
|
|
||||||
@ -110,7 +143,7 @@ class CustomRegistrationView(RegistrationView):
|
|||||||
)
|
)
|
||||||
user.user_permissions.add(permission)
|
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-адрес страницы, куда нужно перейти после успешной/не успешной регистрации.
|
Функция возвращает url-адрес страницы, куда нужно перейти после успешной/не успешной регистрации.
|
||||||
Используется самой django-registration.
|
Используется самой django-registration.
|
||||||
@ -121,7 +154,13 @@ class CustomRegistrationView(RegistrationView):
|
|||||||
return self.urls[self.redirect_url]
|
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')
|
return render(request, 'django_registration/registration_error.html')
|
||||||
|
|
||||||
|
|
||||||
@ -144,7 +183,7 @@ def profile_page(request: WSGIRequest) -> HttpResponse:
|
|||||||
|
|
||||||
|
|
||||||
@login_required()
|
@login_required()
|
||||||
def work_page(request: WSGIRequest, id: int) -> HttpResponse:
|
def work_page(request: WSGIRequest, required_id: int) -> HttpResponse:
|
||||||
"""
|
"""
|
||||||
Функция отображения страницы "Управления правами" для текущего пользователя (login_required).
|
Функция отображения страницы "Управления правами" для текущего пользователя (login_required).
|
||||||
|
|
||||||
@ -153,7 +192,7 @@ def work_page(request: WSGIRequest, id: int) -> HttpResponse:
|
|||||||
:return: адресация на страницу "Управления правами" (либо на страницу "Авторизации", если id и user.id не совпадают
|
:return: адресация на страницу "Управления правами" (либо на страницу "Авторизации", если id и user.id не совпадают
|
||||||
"""
|
"""
|
||||||
users = get_users_list()
|
users = get_users_list()
|
||||||
if request.user.id == id:
|
if request.user.id == required_id:
|
||||||
if request.session.get('is_confirm', None):
|
if request.session.get('is_confirm', None):
|
||||||
messages.success(request, 'Изменения были применены')
|
messages.success(request, 'Изменения были применены')
|
||||||
elif request.session.get('is_confirm', None) is not None:
|
elif request.session.get('is_confirm', None) is not None:
|
||||||
@ -184,7 +223,7 @@ def work_page(request: WSGIRequest, id: int) -> HttpResponse:
|
|||||||
|
|
||||||
|
|
||||||
@login_required()
|
@login_required()
|
||||||
def work_hand_over(request: WSGIRequest):
|
def work_hand_over(request: WSGIRequest) -> HttpResponseRedirect:
|
||||||
"""
|
"""
|
||||||
Функция позволяет текущему пользователю сдать права, а именно сменить в Zendesk роль с "engineer" на "light_agent"
|
Функция позволяет текущему пользователю сдать права, а именно сменить в Zendesk роль с "engineer" на "light_agent"
|
||||||
|
|
||||||
@ -198,18 +237,23 @@ def work_hand_over(request: WSGIRequest):
|
|||||||
@login_required()
|
@login_required()
|
||||||
def work_become_engineer(request: WSGIRequest) -> HttpResponseRedirect:
|
def work_become_engineer(request: WSGIRequest) -> HttpResponseRedirect:
|
||||||
"""
|
"""
|
||||||
Функция позволяет текущему пользователю получить права, а именно сменить в Zendesk роль с "light_agent" на "engineer"
|
Функция позволяет текущему пользователю получить права, а именно сменить в Zendesk роль с "light_agent"
|
||||||
|
на "engineer".
|
||||||
|
|
||||||
:param request: данные текущего пользователя (login_required)
|
:param request: данные текущего пользователя (login_required)
|
||||||
:return: перезагрузка текущей страницы после выполнения смены роли
|
:return: перезагрузка текущей страницы после выполнения смены роли
|
||||||
"""
|
"""
|
||||||
|
|
||||||
make_engineer(request.user.userprofile, request.user)
|
make_engineer(request.user.userprofile, request.user)
|
||||||
return set_session_params_for_work_page(request)
|
return set_session_params_for_work_page(request)
|
||||||
|
|
||||||
|
|
||||||
@login_required()
|
@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)
|
zenpy_user = zenpy.get_user(request.user.email)
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
@ -266,7 +310,7 @@ class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageM
|
|||||||
self.make_light_agents(users)
|
self.make_light_agents(users)
|
||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
||||||
def make_engineers(self, users):
|
def make_engineers(self, users: list) -> None:
|
||||||
"""
|
"""
|
||||||
Функция проходит по списку пользователей, проставляя статус "engineer".
|
Функция проходит по списку пользователей, проставляя статус "engineer".
|
||||||
|
|
||||||
@ -276,7 +320,7 @@ class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageM
|
|||||||
for user in users:
|
for user in users:
|
||||||
make_engineer(user, self.request.user)
|
make_engineer(user, self.request.user)
|
||||||
|
|
||||||
def make_light_agents(self, users):
|
def make_light_agents(self, users: list) -> None:
|
||||||
"""
|
"""
|
||||||
Функция проходит по списку пользователей, проставляя статус "light agent".
|
Функция проходит по списку пользователей, проставляя статус "light agent".
|
||||||
|
|
||||||
@ -291,17 +335,30 @@ class CustomLoginView(LoginView):
|
|||||||
"""
|
"""
|
||||||
Отображение страницы авторизации пользователя
|
Отображение страницы авторизации пользователя
|
||||||
"""
|
"""
|
||||||
|
extra_context = setup_context(login_lit=True)
|
||||||
form_class = CustomAuthenticationForm
|
form_class = CustomAuthenticationForm
|
||||||
|
|
||||||
|
|
||||||
class UsersViewSet(viewsets.ReadOnlyModelViewSet):
|
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')
|
queryset = UserProfile.objects.filter(role='agent')
|
||||||
serializer_class = ProfileSerializer
|
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()
|
users = update_users_in_model()
|
||||||
count = count_users(users.values)
|
count = count_users(users.values)
|
||||||
profiles = UserProfile.objects.filter(role='agent')
|
profiles = UserProfile.objects.filter(role='agent')
|
||||||
@ -316,7 +373,13 @@ class UsersViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
return Response(res)
|
return Response(res)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def choose_users(zendesk, model):
|
def choose_users(zendesk: list, model: list) -> list:
|
||||||
|
"""
|
||||||
|
Функция формирует список пользователей, которые не зарегистрированы у нас.
|
||||||
|
:param zendesk: Список пользователей Zendesk
|
||||||
|
:param model: Список пользователей (модель Userprofile)
|
||||||
|
:return: Список
|
||||||
|
"""
|
||||||
users = []
|
users = []
|
||||||
for zendesk_user in zendesk:
|
for zendesk_user in zendesk:
|
||||||
if zendesk_user.name not in [user.name for user in model]:
|
if zendesk_user.name not in [user.name for user in model]:
|
||||||
@ -324,7 +387,12 @@ class UsersViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
return users
|
return users
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_zendesk_users(users):
|
def get_zendesk_users(users: list) -> list:
|
||||||
|
"""
|
||||||
|
Получение списка пользователей Zendesk, не являющихся админами.
|
||||||
|
:param users: Список пользователей
|
||||||
|
:return: Список пользователей, не являющимися администраторами.
|
||||||
|
"""
|
||||||
zendesk_users = ZendeskUserSerializer(
|
zendesk_users = ZendeskUserSerializer(
|
||||||
data=[user for user in users if user.role != 'admin'],
|
data=[user for user in users if user.role != 'admin'],
|
||||||
many=True
|
many=True
|
||||||
@ -370,3 +438,6 @@ def statistic_page(request: WSGIRequest) -> HttpResponse:
|
|||||||
form = StatisticForm()
|
form = StatisticForm()
|
||||||
context['form'] = form
|
context['form'] = form
|
||||||
return render(request, 'pages/statistic.html', context)
|
return render(request, 'pages/statistic.html', context)
|
||||||
|
|
||||||
|
def registration_failed(request):
|
||||||
|
return render(request, 'pages/registration_failed.html')
|
||||||
|
@ -1,26 +1,36 @@
|
|||||||
|
"""
|
||||||
|
Функционал работы администратора Zendesk.
|
||||||
|
"""
|
||||||
|
|
||||||
from typing import Optional, Dict, List
|
from typing import Optional, Dict, List
|
||||||
|
|
||||||
from zenpy import Zenpy
|
from zenpy import Zenpy
|
||||||
from zenpy.lib.api_objects import User as ZenpyUser, Group as ZenpyGroup, Ticket as ZenpyTicket
|
from zenpy.lib.api_objects import User as ZenpyUser, Group as ZenpyGroup, Ticket as ZenpyTicket
|
||||||
from zenpy.lib.exception import APIException
|
from zenpy.lib.exception import APIException
|
||||||
|
|
||||||
from access_controller.settings import ACTRL_ZENDESK_SUBDOMAIN, ACTRL_API_EMAIL, ACTRL_API_TOKEN, ACTRL_API_PASSWORD, \
|
from access_controller.settings import ACTRL_ZENDESK_SUBDOMAIN, ACTRL_API_EMAIL, ACTRL_API_TOKEN, \
|
||||||
ZENDESK_GROUPS, SOLVED_TICKETS_EMAIL
|
ZENDESK_GROUPS, SOLVED_TICKETS_EMAIL #ACTRL_API_PASSWORD,
|
||||||
|
|
||||||
|
|
||||||
class ZendeskAdmin:
|
class ZendeskAdmin:
|
||||||
"""
|
"""
|
||||||
Класс **ZendeskAdmin** существует, чтобы в каждой функции отдельно не проверять аккаунт администратора.
|
Класс **ZendeskAdmin** содержит описание всего функционала администратора.
|
||||||
|
|
||||||
:param credentials: Полномочия (первым указывается учетная запись организации в Zendesk)
|
:param credentials: Полномочия (первым указывается учетная запись организации в Zendesk)
|
||||||
:type credentials: :class:`Dict[str, str]`
|
: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]):
|
def __init__(self, credentials: Dict[str, str]):
|
||||||
self.credentials = credentials
|
self.credentials = credentials
|
||||||
self.admin = self.create_admin()
|
self.admin = self.create_admin()
|
||||||
self.buffer_group_id: int = self.get_group(ZENDESK_GROUPS['buffer']).id
|
self.buffer_group_id= self.get_group(ZENDESK_GROUPS['buffer']).id
|
||||||
self.solved_tickets_user_id: int = self.get_user(SOLVED_TICKETS_EMAIL).id
|
self.solved_tickets_user_id = self.get_user(SOLVED_TICKETS_EMAIL).id
|
||||||
|
|
||||||
def update_user(self, user: ZenpyUser) -> bool:
|
def update_user(self, user: ZenpyUser) -> bool:
|
||||||
"""
|
"""
|
||||||
@ -46,7 +56,7 @@ class ZendeskAdmin:
|
|||||||
:param email: Email пользователя
|
:param email: Email пользователя
|
||||||
:return: Является ли зарегистрированным
|
: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:
|
def get_user(self, email: str) -> ZenpyUser:
|
||||||
"""
|
"""
|
||||||
@ -96,9 +106,8 @@ class ZendeskAdmin:
|
|||||||
admin = Zenpy(**self.credentials)
|
admin = Zenpy(**self.credentials)
|
||||||
try:
|
try:
|
||||||
admin.search(self.credentials['email'], type='user')
|
admin.search(self.credentials['email'], type='user')
|
||||||
except APIException:
|
except APIException as invalid_data:
|
||||||
raise ValueError('invalid access_controller`s login data')
|
raise ValueError('invalid access_controller`s login data') from invalid_data
|
||||||
|
|
||||||
return admin
|
return admin
|
||||||
|
|
||||||
|
|
||||||
@ -106,5 +115,5 @@ zenpy = ZendeskAdmin({
|
|||||||
'subdomain': ACTRL_ZENDESK_SUBDOMAIN,
|
'subdomain': ACTRL_ZENDESK_SUBDOMAIN,
|
||||||
'email': ACTRL_API_EMAIL,
|
'email': ACTRL_API_EMAIL,
|
||||||
'token': ACTRL_API_TOKEN,
|
'token': ACTRL_API_TOKEN,
|
||||||
'password': ACTRL_API_PASSWORD,
|
#'password': ACTRL_API_PASSWORD,
|
||||||
})
|
})
|
||||||
|
@ -1,19 +1,10 @@
|
|||||||
# Contains requirements common to all environments
|
# Contains requirements common to all environments
|
||||||
|
|
||||||
# Engine
|
# Engine
|
||||||
Django==3.1.6
|
Django==3.2.3
|
||||||
Pillow==8.1.0
|
|
||||||
zenpy~=2.0.24
|
zenpy~=2.0.24
|
||||||
django_registration==3.1.1
|
django_registration==3.1.2
|
||||||
djangorestframework==3.12.2
|
djangorestframework==3.12.4
|
||||||
|
|
||||||
|
|
||||||
# Documentation
|
|
||||||
Sphinx==3.4.3
|
|
||||||
sphinx-rtd-theme==0.5.1
|
|
||||||
sphinx-autodoc-typehints==1.11.1
|
|
||||||
pyenchant==3.2.0
|
|
||||||
sphinxcontrib-spelling==7.1.0
|
|
||||||
|
|
||||||
# Misc
|
# Misc
|
||||||
python-dotenv==0.17.1
|
python-dotenv==0.17.1
|
||||||
|
@ -1,3 +1,18 @@
|
|||||||
# Development specific dependencies
|
# Development specific dependencies
|
||||||
-r common.txt
|
-r common.txt
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
Sphinx==3.5.4
|
||||||
|
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
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# Production specific dependencies
|
# Production specific dependencies
|
||||||
-r common.txt
|
-r common.txt
|
||||||
|
|
||||||
daphne==3.0.1
|
daphne==3.0.2
|
||||||
Twisted[tls,http2]==21.2.0
|
Twisted[tls,http2]==21.2.0
|
||||||
|
Loading…
x
Reference in New Issue
Block a user