Merge branch 'develop' into feature/react_test
# Conflicts: # README.md # main/tests.py
This commit is contained in:
commit
3cdc944ce4
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_HOST="actrl.example.com"
|
||||
|
||||
ACTRL_EMAIL_HOST="smtp.mail.ru"
|
||||
ACTRL_EMAIL_PORT=2525
|
||||
ACTRL_EMAIL_HOST="smtp.gmail.com"
|
||||
ACTRL_EMAIL_PORT=587
|
||||
ACTRL_EMAIL_TLS=1
|
||||
ACTRL_EMAIL_HOST_USER="djgr.02@mail.ru"
|
||||
ACTRL_EMAIL_HOST_PASSWORD="djangogroup02"
|
||||
@ -20,6 +20,6 @@ LICENSE_NO=3
|
||||
SHIFTH=12
|
||||
|
||||
ACTRL_ZENDESK_SUBDOMAIN="ngenix1612197338"
|
||||
ACTRL_API_EMAIL="email@example.com"
|
||||
ACTRL_API_TOKEN="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
ACTRL_API_PASSWORD=""
|
||||
ACTRL_API_EMAIL="stepanenko_olga@mail.ru"
|
||||
ACTRL_API_TOKEN="X1x4QeNa4xRdul2rTIKhac98AsXMwd5bOGAyZOtU"
|
||||
|
||||
|
627
.pylintrc
Normal file
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
|
27
README.md
27
README.md
@ -1,5 +1,3 @@
|
||||
# ZenDesk Access Controller
|
||||
|
||||
## Управление правами доступа
|
||||
|
||||
Идея - написать программу(Web приложение), которая будет выдавать права пользователям системы по запросу самого
|
||||
@ -39,7 +37,7 @@
|
||||
|
||||
|
||||
## Quickstart
|
||||
Перед запуском требуется необходимо `.env` файл.
|
||||
Перед запуском необходимо создать `.env` файл.
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
@ -100,7 +98,7 @@ npm test
|
||||
```
|
||||
ACTRL_DEBUG={0/1} - включить режим дебага
|
||||
ACTRL_HOST={HOSTNAME} - при запуске без дебага, надо указать домен на котором будет работать приложение
|
||||
ACTRL_SECRET_KEY={DJANGO_SECRET_KEY} - секретный ключ сгенерированый Django
|
||||
ACTRL_SECRET_KEY={DJANGO_SECRET_KEY} - секретный ключ сгенерированный Django
|
||||
|
||||
ACTRL_EMAIL_HOST={SMTP_HOST} - домен почтового сервера через который приложение будет отправлять письма, например "smtp.gmail.com"
|
||||
ACTRL_EMAIL_PORT={SMTP_PORT} - порт для почтового сервера, например 587, 465 , 2525
|
||||
@ -162,7 +160,7 @@ docker run -d -p 8000:8000 \
|
||||
-v {ABSOLUTE_PATH_TO_DB}:/zendesk-access-controller/db \
|
||||
access_controller:latest
|
||||
```
|
||||
- открываем запущеный контейнер в браузере (можно перейти по ссылке http://localhost:8000/)
|
||||
- открываем запущенный контейнер в браузере (можно перейти по ссылке http://localhost:8000/)
|
||||
|
||||
|
||||
## Запуск с тестовыми юзерами:
|
||||
@ -173,12 +171,29 @@ docker run -d -p 8000:8000 \
|
||||
- Пользователь - `123@test.ru` / `zendeskuser`
|
||||
|
||||
Не сработает если домен песочницы отличается от `ngenix1612197338` (на другом домене нужно будет создать сначала пользователей в песочнице с правами админа и легкого агента
|
||||
с этими же почтами, назначить им организацию `SYSTEM`)
|
||||
с этими же email, назначить им организацию `SYSTEM`)
|
||||
|
||||
|
||||
## Параметры тестовой песочницы:
|
||||
Пример полной конфигурации можно найти в [.env.example](.env.example). Почту и токен админа ZenDesk взять у руководителя (если вы не админ).
|
||||
|
||||
## Для проверки pylint используем:
|
||||
pylint --django-settings-module=access_controller.access_controller.settings ../access_controller (каталог, где лежит проект)
|
||||
|
||||
## Для приведения файлов к стандарту PEP8 используем:
|
||||
autopep8 --in-place filename
|
||||
|
||||
##Для проверки орфографии:
|
||||
cd docs
|
||||
|
||||
(set -a && source ../.env && make spelling)
|
||||
##Для обновления документации:
|
||||
m2r README.md
|
||||
|
||||
cd docs
|
||||
|
||||
(set -a && source ../.env && make html)
|
||||
|
||||
|
||||
## Read more
|
||||
- Zenpy: [http://docs.facetoe.com.au](http://docs.facetoe.com.au)
|
||||
|
204
README.rst
Normal file
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/>`_
|
0
__init__.py
Normal file
0
__init__.py
Normal file
@ -8,10 +8,7 @@ https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'access_controller.settings')
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'access_controller.settings')
|
||||
application = get_asgi_application()
|
||||
|
@ -1,19 +1,31 @@
|
||||
"""
|
||||
Авторизация пользователя.
|
||||
"""
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
|
||||
class EmailAuthBackend(ModelBackend):
|
||||
"""
|
||||
Класс авторизации пользователя по email.
|
||||
"""
|
||||
def authenticate(self, request, username=None, password=None, **kwargs):
|
||||
"""
|
||||
Функция получения пользователя (модель User) по email.
|
||||
"""
|
||||
try:
|
||||
user = User.objects.get(email=username)
|
||||
user = get_user_model().objects.get(email=username)
|
||||
if user.check_password(password):
|
||||
return user
|
||||
return None
|
||||
except User.DoesNotExist:
|
||||
except get_user_model().DoesNotExist:
|
||||
return None
|
||||
|
||||
def get_user(self, user_id):
|
||||
"""
|
||||
Функция получения пользователя по id.
|
||||
"""
|
||||
try:
|
||||
return User.objects.get(pk=user_id)
|
||||
except User.DoesNotExist:
|
||||
return get_user_model().objects.get(pk=user_id)
|
||||
except get_user_model().DoesNotExist:
|
||||
return None
|
||||
|
@ -14,6 +14,8 @@ from pathlib import Path
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
@ -26,7 +28,7 @@ load_dotenv()
|
||||
SECRET_KEY = os.getenv('ACTRL_SECRET_KEY', 'empty')
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = bool(int(os.getenv('ACTRL_DEBUG', 1)))
|
||||
DEBUG = bool(int(os.getenv('ACTRL_DEBUG', '1')))
|
||||
|
||||
ALLOWED_HOSTS = [
|
||||
'127.0.0.1',
|
||||
@ -62,8 +64,8 @@ ROOT_URLCONF = 'access_controller.urls'
|
||||
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||
EMAIL_HOST = os.getenv('ACTRL_EMAIL_HOST', 'smtp.gmail.com')
|
||||
EMAIL_PORT = int(os.getenv('ACTRL_EMAIL_PORT', 587))
|
||||
EMAIL_USE_TLS = bool(int(os.getenv('ACTRL_EMAIL_TLS', 1)))
|
||||
EMAIL_PORT = int(os.getenv('ACTRL_EMAIL_PORT', '587'))
|
||||
EMAIL_USE_TLS = bool(int(os.getenv('ACTRL_EMAIL_TLS', '1')))
|
||||
EMAIL_HOST_USER = os.getenv('ACTRL_EMAIL_HOST_USER', 'group02django@gmail.com')
|
||||
EMAIL_HOST_PASSWORD = os.getenv('ACTRL_EMAIL_HOST_PASSWORD', 'djangogroup02')
|
||||
DEFAULT_FROM_EMAIL = os.getenv('ACTRL_FROM_EMAIL', EMAIL_HOST_USER)
|
||||
@ -148,6 +150,8 @@ ACCOUNT_ACTIVATION_DAYS = 7
|
||||
LOGIN_REDIRECT_URL = '/'
|
||||
LOGOUT_REDIRECT_URL = '/'
|
||||
|
||||
|
||||
|
||||
# Название_приложения.Название_файла.Название_класса_обработчика
|
||||
AUTHENTICATION_BACKENDS = [
|
||||
'access_controller.auth.EmailAuthBackend',
|
||||
@ -158,8 +162,8 @@ AUTHENTICATION_BACKENDS = [
|
||||
|
||||
|
||||
ZENDESK_ROLES = {
|
||||
'engineer': int(os.getenv('ENG_CROLE_ID', 0)),
|
||||
'light_agent': int(os.getenv('LA_CROLE_ID', 0)),
|
||||
'engineer': int(os.getenv('ENG_CROLE_ID', '0')),
|
||||
'light_agent': int(os.getenv('LA_CROLE_ID', '0')),
|
||||
}
|
||||
|
||||
ZENDESK_GROUPS = {
|
||||
@ -169,7 +173,7 @@ ZENDESK_GROUPS = {
|
||||
|
||||
SOLVED_TICKETS_EMAIL = os.getenv('ST_EMAIL')
|
||||
|
||||
ZENDESK_MAX_AGENTS = int(os.getenv('LICENSE_NO', 0))
|
||||
ZENDESK_MAX_AGENTS = int(os.getenv('LICENSE_NO', '0'))
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
# Use Django's standard `django.contrib.auth` permissions,
|
||||
@ -179,7 +183,7 @@ REST_FRAMEWORK = {
|
||||
]
|
||||
}
|
||||
|
||||
ONE_DAY = int(os.getenv('SHIFTH', 0)) # Количество часов в 1 рабочем дне
|
||||
ONE_DAY = int(os.getenv('SHIFTH', '0')) # Количество часов в 1 рабочем дне
|
||||
|
||||
ACTRL_ZENDESK_SUBDOMAIN = os.getenv('ACTRL_ZENDESK_SUBDOMAIN') or os.getenv('ZD_DOMAIN')
|
||||
ACTRL_API_EMAIL = os.getenv('ACTRL_API_EMAIL') or os.getenv('ACCESS_CONTROLLER_API_EMAIL')
|
||||
|
@ -31,7 +31,7 @@ urlpatterns = [
|
||||
path('accounts/register/error/', registration_error, name='registration_email_error'),
|
||||
path('accounts/login/', CustomLoginView.as_view(), name='login'),
|
||||
path('accounts/', include('django.contrib.auth.urls')),
|
||||
path('work/<int:id>', work_page, name="work"),
|
||||
path('work/<int:required_id>', work_page, name="work"),
|
||||
path('work/hand_over/', work_hand_over, name="work_hand_over"),
|
||||
path('work/become_engineer/', work_become_engineer, name="work_become_engineer"),
|
||||
path('work/get_tickets', work_get_tickets, name='work_get_tickets'),
|
||||
|
@ -6,7 +6,7 @@ Models
|
||||
*******
|
||||
|
||||
.. automodule:: main.models
|
||||
:members:
|
||||
:members:
|
||||
|
||||
|
||||
******
|
||||
|
@ -12,16 +12,12 @@
|
||||
#
|
||||
import os
|
||||
import sys
|
||||
import importlib
|
||||
import inspect
|
||||
import enchant
|
||||
from enchant import checker
|
||||
|
||||
import django
|
||||
|
||||
sys.path.insert(0, os.path.abspath('../../'))
|
||||
|
||||
import django
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'access_controller.settings')
|
||||
os.environ.setdefault('DJANGO_CONFIGURATION', 'Dev')
|
||||
|
||||
@ -39,7 +35,6 @@ from django.db.models.query import QuerySet
|
||||
|
||||
QuerySet.__repr__ = lambda self: self.__class__.__name__
|
||||
|
||||
|
||||
django.setup()
|
||||
|
||||
# -- Project information -----------------------------------------------------
|
||||
@ -89,6 +84,7 @@ def process_django_models(app, what, name, obj, options, lines):
|
||||
lines.append(':type %s: %s.%s' % (field.attname, module, field_type.__name__))
|
||||
if enchant is not None:
|
||||
lines += spelling_white_list
|
||||
lines.append('')
|
||||
return lines
|
||||
|
||||
|
||||
@ -116,13 +112,17 @@ def skip_queryset(app, what, name, obj, skip, options):
|
||||
return skip
|
||||
|
||||
|
||||
# def setup(app):
|
||||
# # Register the docstring processor with sphinx
|
||||
# app.connect('autodoc-process-docstring', process_django_models)
|
||||
# app.connect('autodoc-skip-member', skip_queryset)
|
||||
# app.connect('autodoc-process-docstring', process_modules)
|
||||
def fix_sig(app, what, name, obj, options, signature, return_annotation):
|
||||
return "", ""
|
||||
|
||||
|
||||
def setup(app):
|
||||
# Register the docstring processor with sphinx
|
||||
app.connect('autodoc-process-docstring', process_django_models)
|
||||
app.connect('autodoc-skip-member', skip_queryset)
|
||||
app.connect('autodoc-process-docstring', process_modules)
|
||||
app.connect("autodoc-process-signature", fix_sig)
|
||||
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
@ -132,16 +132,13 @@ extensions = {
|
||||
'sphinx.ext.autodoc',
|
||||
'sphinx.ext.intersphinx',
|
||||
'sphinx.ext.viewcode',
|
||||
'sphinx.ext.napoleon',
|
||||
'sphinx_rtd_theme',
|
||||
'sphinx.ext.graphviz',
|
||||
'sphinx.ext.inheritance_diagram',
|
||||
'sphinx_autodoc_typehints',
|
||||
'sphinxcontrib.spelling'
|
||||
|
||||
'sphinxcontrib.spelling',
|
||||
}
|
||||
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
@ -171,6 +168,7 @@ html_static_path = ['_static']
|
||||
|
||||
# -- Extension configuration -------------------------------------------------
|
||||
|
||||
|
||||
# -- Options for intersphinx extension ---------------------------------------
|
||||
|
||||
# Example configuration for intersphinx: refer to the Python standard library.
|
||||
@ -183,20 +181,21 @@ intersphinx_mapping = {
|
||||
}
|
||||
|
||||
autodoc_default_flags = ['members']
|
||||
autodoc_typehints = "description"
|
||||
|
||||
# spell checking
|
||||
spelling_lang = 'ru_RU'
|
||||
tokenizer_lang = 'ru_RU'
|
||||
spelling_exclude_patterns=['ignored_*']
|
||||
tokenizer_lang = 'en_US'
|
||||
spelling_exclude_patterns = ['ignored_*', '../../main/models.py']
|
||||
spelling_show_suggestions = True
|
||||
spelling_show_whole_line=True
|
||||
spelling_warning=True
|
||||
spelling_show_whole_line = True
|
||||
spelling_warning = True
|
||||
spelling_ignore_pypi_package_names = True
|
||||
spelling_ignore_wiki_words=True
|
||||
spelling_ignore_acronyms=True
|
||||
spelling_ignore_python_builtins=True
|
||||
spelling_ignore_importable_modules=True
|
||||
spelling_ignore_contributor_names=True
|
||||
spelling_ignore_wiki_words = True
|
||||
spelling_ignore_acronyms = True
|
||||
spelling_ignore_python_builtins = True
|
||||
spelling_ignore_importable_modules = True
|
||||
spelling_ignore_contributor_names = True
|
||||
|
||||
# -- Options for todo extension ----------------------------------------------
|
||||
|
||||
@ -206,5 +205,3 @@ set_type_checking_flag = True
|
||||
typehints_fully_qualified = True
|
||||
always_document_param_types = True
|
||||
typehints_document_rtype = True
|
||||
|
||||
napoleon_attr_annotations = True
|
||||
|
@ -12,6 +12,7 @@
|
||||
|
||||
overview
|
||||
code
|
||||
readme
|
||||
todo
|
||||
|
||||
|
||||
|
@ -69,12 +69,15 @@
|
||||
Запрос прав доступа
|
||||
********************
|
||||
|
||||
На странице запроса прав Вам доступна информация о количестве и списке работающих над тикетами сотрудников,
|
||||
а также возможность сдать и запросить права.
|
||||
На странице запроса прав Вам доступна информация о количестве и списке работающих над тикетами сотрудников.
|
||||
|
||||
Если Вы не являетесь инженером, то на данной странице Вы можете запросить права.
|
||||
|
||||
Если Вы являетесь инженером, то права можно сдать.
|
||||
|
||||
.. image:: _static/request.png
|
||||
|
||||
Успешное изменение прав:
|
||||
Успешное изменение прав - список инженеров пополнился новым пользователем:
|
||||
|
||||
.. image:: _static/role_change.png
|
||||
|
||||
@ -84,9 +87,9 @@
|
||||
|
||||
Для администратора существует удобный интерфейс страницы управления, в котором представлены:
|
||||
|
||||
* Количество свободных инженерных мест
|
||||
* Количество и список инженеров и легких агентов
|
||||
* Возможность группового назначения прав с использованием чек-боксов
|
||||
* количество свободных инженерных мест
|
||||
* количество и список инженеров и легких агентов
|
||||
* возможность группового назначения прав с использованием чекбоксов
|
||||
|
||||
.. image:: _static/admin_manage.png
|
||||
|
||||
|
4
docs/source/readme.rst
Normal file
4
docs/source/readme.rst
Normal file
@ -0,0 +1,4 @@
|
||||
READ.me
|
||||
==================
|
||||
|
||||
.. include:: ../../README.rst
|
@ -45,7 +45,9 @@ start
|
||||
end
|
||||
date
|
||||
Токен
|
||||
токен
|
||||
токеном
|
||||
токену
|
||||
аутентифицирован
|
||||
(datetime.time)
|
||||
datetime
|
||||
@ -81,8 +83,112 @@ functions
|
||||
Serializer
|
||||
Serializers
|
||||
Сериализатор
|
||||
Сериализаторы
|
||||
сериализатор
|
||||
сериализатора
|
||||
переадресации
|
||||
|
||||
|
||||
|
||||
чекбоксов
|
||||
админских
|
||||
админские
|
||||
Python
|
||||
Docker
|
||||
докер
|
||||
докера
|
||||
Докер
|
||||
репозиторий
|
||||
zendesk-access-controller/db
|
||||
-e
|
||||
-v
|
||||
e
|
||||
v
|
||||
zendesk
|
||||
db
|
||||
юзерами
|
||||
Read
|
||||
Zenpy
|
||||
залогинен
|
||||
т
|
||||
д
|
||||
rolchangelogs
|
||||
извеcтно
|
||||
role
|
||||
View
|
||||
Model
|
||||
type
|
||||
param
|
||||
rtype
|
||||
return
|
||||
UsersViewSet
|
||||
list
|
||||
engineers
|
||||
agents
|
||||
request
|
||||
rest
|
||||
framework
|
||||
response
|
||||
Сэндбокс
|
||||
админскими
|
||||
логирование
|
||||
code
|
||||
block
|
||||
d
|
||||
p
|
||||
ACTRL_DEBUG
|
||||
дебага
|
||||
ACTRL_HOST
|
||||
HOSTNAME
|
||||
ACTRL_SECRET_KEY
|
||||
DJANGO_SECRET_KEY
|
||||
ACTRL_EMAIL_HOST
|
||||
SMTP_HOST
|
||||
smtp.gmail.com
|
||||
ACTRL_EMAIL_PORT
|
||||
SMTP_PORT
|
||||
ACTRL_EMAIL_TLS
|
||||
USE_TLS
|
||||
TLS
|
||||
ACTRL_EMAIL_HOST_USER
|
||||
USERNAME
|
||||
ACTRL_EMAIL_HOST_PASSWORD
|
||||
PASSWORD
|
||||
ACTRL_FROM_EMAIL
|
||||
EMAIL
|
||||
ACTRL_SERVER_EMAIL
|
||||
ACTRL_API_EMAIL
|
||||
ACTRL_API_PASSWORD
|
||||
ACTRL_API_TOKEN
|
||||
API_TOKEN
|
||||
ACTRL_ZENDESK_SUBDOMAIN
|
||||
DOMAIN
|
||||
ENG_CROLE_ID
|
||||
ENGINEER_CUSTOM_ROLE_ID
|
||||
custom_role_id
|
||||
LA_CROLE_ID
|
||||
LIGHT_AGENT_CUSTOM_ROLE_ID
|
||||
EMPL_GROUP
|
||||
EMPLOYEE_GROUP_NAME
|
||||
ССКС
|
||||
BUF_GROUP
|
||||
BUFFER_GROUP_NAME
|
||||
буферной
|
||||
тикетами
|
||||
ST_EMAIL
|
||||
SOLVED_TICKETS_EMAIL
|
||||
LICENSE_NO
|
||||
LICENSE_NO
|
||||
SHIFTH
|
||||
SHIFT_HOURS
|
||||
ABSOLUTE
|
||||
ABSOLUTE_PATH_TO_DB
|
||||
PATH
|
||||
TO
|
||||
DB
|
||||
latest
|
||||
in
|
||||
place
|
||||
cd
|
||||
docs
|
||||
a
|
||||
Аватарка
|
||||
filename
|
||||
|
||||
|
@ -1,3 +1,8 @@
|
||||
from django.contrib import admin
|
||||
"""
|
||||
Встроенный файл
|
||||
"""
|
||||
|
||||
|
||||
# from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
|
0
main/apiauth.py
Normal file
0
main/apiauth.py
Normal file
@ -1,5 +1,11 @@
|
||||
"""
|
||||
Стандартный файл Django конфигурации приложения.
|
||||
"""
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class MainConfig(AppConfig):
|
||||
"""
|
||||
Старт приложения
|
||||
"""
|
||||
name = 'main'
|
||||
|
@ -1,8 +1,14 @@
|
||||
"""
|
||||
Вспомогательные функции со списками пользователей, статистикой и т.д.
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from typing import Union
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.core.handlers.wsgi import WSGIRequest
|
||||
from django.http import HttpResponseRedirect, HttpResponsePermanentRedirect
|
||||
from django.shortcuts import redirect
|
||||
from django.utils import timezone
|
||||
from zenpy import Zenpy
|
||||
@ -16,7 +22,7 @@ from main.requester import TicketListRequester
|
||||
from main.zendesk_admin import zenpy
|
||||
|
||||
|
||||
def update_role(user_profile: UserProfile, role: int, who_changes: User) -> None:
|
||||
def update_role(user_profile: UserProfile, role: int, who_changes: get_user_model()) -> None:
|
||||
"""
|
||||
Функция меняет роль пользователя.
|
||||
|
||||
@ -34,7 +40,7 @@ def update_role(user_profile: UserProfile, role: int, who_changes: User) -> None
|
||||
zendesk.update_user(user)
|
||||
|
||||
|
||||
def make_engineer(user_profile: UserProfile, who_changes: User) -> None:
|
||||
def make_engineer(user_profile: UserProfile, who_changes: get_user_model()) -> None:
|
||||
"""
|
||||
Функция устанавливает пользователю роль инженера.
|
||||
|
||||
@ -44,7 +50,7 @@ def make_engineer(user_profile: UserProfile, who_changes: User) -> None:
|
||||
update_role(user_profile, ROLES['engineer'], who_changes)
|
||||
|
||||
|
||||
def make_light_agent(user_profile: UserProfile, who_changes: User) -> None:
|
||||
def make_light_agent(user_profile: UserProfile, who_changes: get_user_model()) -> None:
|
||||
"""
|
||||
Функция устанавливает пользователю роль легкого агента.
|
||||
|
||||
@ -89,7 +95,7 @@ def get_users_list() -> list:
|
||||
return users
|
||||
|
||||
|
||||
def get_tickets_list(email):
|
||||
def get_tickets_list(email) -> list:
|
||||
"""
|
||||
Функция возвращает список тикетов пользователя Zendesk
|
||||
"""
|
||||
@ -103,7 +109,7 @@ def get_tickets_list_for_group(group_name):
|
||||
return TicketListRequester().get_tickets_list_for_group(zenpy.get_group(group_name))
|
||||
|
||||
|
||||
def update_profile(user_profile: UserProfile):
|
||||
def update_profile(user_profile: UserProfile) -> None:
|
||||
"""
|
||||
Функция обновляет профиль пользователя в соответствии с текущим в Zendesk.
|
||||
|
||||
@ -157,7 +163,7 @@ def check_user_auth(email: str, password: str) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def update_user_in_model(profile: UserProfile, zendesk_user: ZenpyUser):
|
||||
def update_user_in_model(profile: UserProfile, zendesk_user: ZenpyUser) -> None:
|
||||
"""
|
||||
Функция обновляет профиль пользователя при изменении данных пользователя на Zendesk.
|
||||
|
||||
@ -173,7 +179,7 @@ def update_user_in_model(profile: UserProfile, zendesk_user: ZenpyUser):
|
||||
profile.save()
|
||||
|
||||
|
||||
def count_users(users) -> tuple:
|
||||
def count_users(users: list) -> tuple:
|
||||
"""
|
||||
Функция подсчета количества сотрудников с ролями engineer и light_agent
|
||||
"""
|
||||
@ -186,21 +192,21 @@ def count_users(users) -> tuple:
|
||||
return engineers, light_agents
|
||||
|
||||
|
||||
def update_users_in_model():
|
||||
def update_users_in_model() -> list:
|
||||
"""
|
||||
Обновляет пользователей в модели UserProfile по списку пользователей в организации
|
||||
"""
|
||||
users = get_users_list()
|
||||
for user in users:
|
||||
try:
|
||||
profile = User.objects.get(email=user.email).userprofile
|
||||
profile = get_user_model().objects.get(email=user.email).userprofile
|
||||
update_user_in_model(profile, user)
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
return users
|
||||
|
||||
|
||||
def daterange(start_date, end_date) -> list:
|
||||
def daterange(start_date: timedelta, end_date: timedelta) -> list:
|
||||
"""
|
||||
Функция возвращает список дней с start_date по end_date, исключая правую границу.
|
||||
|
||||
@ -214,17 +220,17 @@ def daterange(start_date, end_date) -> list:
|
||||
return dates
|
||||
|
||||
|
||||
def get_timedelta(log, time=None) -> timedelta:
|
||||
def get_timedelta(current_log: RoleChangeLogs, time: timedelta = None) -> timedelta:
|
||||
"""
|
||||
Функция возвращает объект класса timedelta, который хранит промежуток времени от начала суток до момента,
|
||||
который находится в log (объект класса RoleChangeLogs) или в time(datetime.time), если введён.
|
||||
|
||||
:param log: Лог
|
||||
:param current_log: Лог
|
||||
:param time: Время
|
||||
:return: Сколько времени прошло от начала суток до события
|
||||
"""
|
||||
if time is None:
|
||||
time = log.change_time.time()
|
||||
time = current_log.change_time.time()
|
||||
time = timedelta(hours=time.hour, minutes=time.minute, seconds=time.second)
|
||||
return time
|
||||
|
||||
@ -241,6 +247,9 @@ def last_day_of_month(day: int) -> int:
|
||||
|
||||
|
||||
class DatabaseHandler(logging.Handler):
|
||||
"""
|
||||
Класс записи изменений ролей в базу данных.
|
||||
"""
|
||||
def __init__(self):
|
||||
logging.Handler.__init__(self)
|
||||
|
||||
@ -265,10 +274,19 @@ class DatabaseHandler(logging.Handler):
|
||||
|
||||
|
||||
class CsvFormatter(logging.Formatter):
|
||||
"""
|
||||
Класс преобразования смены ролей пользователей в строковый формат.
|
||||
"""
|
||||
def __init__(self):
|
||||
logging.Formatter.__init__(self)
|
||||
|
||||
def format(self, record):
|
||||
def format(self, record: logging.LogRecord) -> str:
|
||||
"""
|
||||
Функция форматирует запись смены роли пользователя в строку.
|
||||
|
||||
:param record: Запись смены роли пользователя.
|
||||
:return: Строка с записью смены пользователя.
|
||||
"""
|
||||
users = record.msg
|
||||
if users[1]:
|
||||
user = users[0]
|
||||
@ -291,10 +309,10 @@ class CsvFormatter(logging.Formatter):
|
||||
|
||||
def log(user, admin=None):
|
||||
"""
|
||||
Осуществляет запись логов в базу данных и csv файл
|
||||
:param admin:
|
||||
:param user:
|
||||
:return:
|
||||
Функция осуществляет запись логов в базу данных и csv файл.
|
||||
|
||||
:param admin: Админ, который меняет роль
|
||||
:param user: Пользователь, которому изменена роль
|
||||
"""
|
||||
users = [user, admin]
|
||||
logger = logging.getLogger('MY_LOGGER')
|
||||
@ -309,10 +327,16 @@ def log(user, admin=None):
|
||||
logger.info(users)
|
||||
|
||||
|
||||
def set_session_params_for_work_page(request, count=None, is_confirm=True):
|
||||
def set_session_params_for_work_page(request: WSGIRequest, count: int = None, is_confirm: bool = True) -> \
|
||||
Union[HttpResponsePermanentRedirect, HttpResponseRedirect]:
|
||||
"""
|
||||
Функция для страницы получения прав
|
||||
Устанавливает данные сессии о успешности запроса и количестве назначенных тикетов
|
||||
Функция для страницы получения прав, устанавливает данные сессии о успешности запроса и количестве
|
||||
назначенных тикетов.
|
||||
|
||||
:param request: Получение данных с рабочей страницы пользователя
|
||||
:param count: Количество запрошенных тикетов
|
||||
:param is_confirm: Назначение тикетов
|
||||
:return: Перезагрузка страницы "Управление правами" соответствующего пользователя
|
||||
"""
|
||||
request.session['is_confirm'] = is_confirm
|
||||
request.session['count_tickets'] = count
|
||||
|
@ -1,3 +1,6 @@
|
||||
"""
|
||||
Формы.
|
||||
"""
|
||||
from django import forms
|
||||
from django.contrib.auth.forms import AuthenticationForm
|
||||
from django_registration.forms import RegistrationFormUniqueEmail
|
||||
@ -56,6 +59,8 @@ class CustomAuthenticationForm(AuthenticationForm):
|
||||
|
||||
:param username: Поле для ввода email пользователя
|
||||
:type username: :class:`django.forms.fields.CharField`
|
||||
:param error_messages: Список ошибок авторизации
|
||||
:type error_messages: :class:`dict`
|
||||
"""
|
||||
username = forms.CharField(
|
||||
label="Электронная почта",
|
||||
@ -64,8 +69,7 @@ class CustomAuthenticationForm(AuthenticationForm):
|
||||
error_messages = {
|
||||
'invalid_login':
|
||||
"Пожалуйста, введите правильные электронную почту и пароль. Оба поля "
|
||||
"могут быть чувствительны к регистру."
|
||||
,
|
||||
"могут быть чувствительны к регистру.",
|
||||
'inactive': "Аккаунт не активен.",
|
||||
}
|
||||
|
||||
|
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.contrib.auth.models import User
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
from django.utils import timezone
|
||||
@ -19,42 +22,64 @@ class UserProfile(models.Model):
|
||||
('has_control_access', 'Can view admin page'),
|
||||
)
|
||||
|
||||
user = models.OneToOneField(to=User, on_delete=models.CASCADE, help_text='Пользователь')
|
||||
user = models.OneToOneField(to=get_user_model(), on_delete=models.CASCADE, help_text='Пользователь')
|
||||
role = models.CharField(default='None', max_length=100, help_text='Глобальное имя роли пользователя')
|
||||
custom_role_id = models.IntegerField(default=0, help_text='Код роли пользователя')
|
||||
image = models.URLField(null=True, blank=True, help_text='Аватарка')
|
||||
name = models.CharField(default='None', max_length=100, help_text='Имя пользователя на нашем сайте')
|
||||
|
||||
@property
|
||||
def zendesk_role(self):
|
||||
id = self.custom_role_id
|
||||
def zendesk_role(self) -> str:
|
||||
"""
|
||||
Функция возвращает роль пользователя в Zendesk.
|
||||
|
||||
В формате str, либо UNDEFINED, если пользователь не найден
|
||||
|
||||
:return: Роль пользователя в Zendesk
|
||||
"""
|
||||
for role, r_id in ZENDESK_ROLES.items():
|
||||
if r_id == id:
|
||||
if r_id == self.custom_role_id:
|
||||
return role
|
||||
return 'UNDEFINED'
|
||||
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def create_user_profile(sender, instance, created, **kwargs):
|
||||
@receiver(post_save, sender=get_user_model())
|
||||
def create_user_profile(instance, created, **kwargs) -> None:
|
||||
"""
|
||||
Функция создания профиля пользователя (Userprofile) при регистрации пользователя.
|
||||
|
||||
:param instance: Экземпляр класса User
|
||||
:param created: Создание профиля пользователя
|
||||
:param kwargs: Параметры
|
||||
:return: Обновленный список объектов профилей пользователей
|
||||
"""
|
||||
if created:
|
||||
UserProfile.objects.create(user=instance)
|
||||
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def save_user_profile(sender, instance, **kwargs):
|
||||
@receiver(post_save, sender=get_user_model())
|
||||
def save_user_profile(instance, **kwargs) -> None:
|
||||
"""
|
||||
Функция записи БД профиля пользователя.
|
||||
|
||||
:param instance: Экземпляр класса User
|
||||
:param kwargs: Параметры
|
||||
:return: Запись профиля пользователя
|
||||
"""
|
||||
instance.userprofile.save()
|
||||
|
||||
|
||||
class RoleChangeLogs(models.Model):
|
||||
"""
|
||||
Модель для логирования изменений ролей пользователя.
|
||||
Модель для логгирования изменений ролей пользователя
|
||||
"""
|
||||
|
||||
user = models.ForeignKey(to=User, on_delete=models.CASCADE, help_text='Пользователь, которому присвоили другую роль')
|
||||
user = models.ForeignKey(to=get_user_model(), on_delete=models.CASCADE,
|
||||
help_text='Пользователь, которому присвоили другую роль')
|
||||
old_role = models.IntegerField(default=0, help_text='Старая роль')
|
||||
new_role = models.IntegerField(default=0, help_text='Присвоенная роль')
|
||||
change_time = models.DateTimeField(default=timezone.now, help_text='Дата и время изменения роли')
|
||||
changed_by = models.ForeignKey(to=User, on_delete=models.CASCADE, related_name='changed_by', help_text='Кем была изменена роль')
|
||||
changed_by = models.ForeignKey(to=get_user_model(), on_delete=models.CASCADE, related_name='changed_by',
|
||||
help_text='Кем была изменена роль')
|
||||
|
||||
|
||||
class UnassignedTicketStatus(models.IntegerChoices):
|
||||
@ -63,13 +88,15 @@ class UnassignedTicketStatus(models.IntegerChoices):
|
||||
|
||||
:param UNASSIGNED: Снят с пользователя, перенесён в буферную группу
|
||||
:param RESTORED: Авторство восстановлено
|
||||
:param NOT_FOUND: Пока нас не было, тикет испарился из буферной группы. Дополнительные действия не требуются
|
||||
:param NOT_FOUND: Пока нас не было, тикет испарился из буферной группы.
|
||||
Дополнительные действия не требуются
|
||||
:param CLOSED: Тикет уже был закрыт. Дополнительные действия не требуются
|
||||
:param SOLVED: Тикет решён. Записан на пользователя с почтой SOLVED_TICKETS_EMAIL
|
||||
"""
|
||||
UNASSIGNED = 0, 'Снят с пользователя, перенесён в буферную группу'
|
||||
RESTORED = 1, 'Авторство восстановлено'
|
||||
NOT_FOUND = 2, 'Пока нас не было, тикет испарился из буферной группы. Дополнительные действия не требуются'
|
||||
NOT_FOUND = 2, 'Пока нас не было, тикет испарился из ' \
|
||||
'буферной группы. Дополнительные действия не требуются'
|
||||
CLOSED = 3, 'Тикет уже был закрыт. Дополнительные действия не требуются'
|
||||
SOLVED = 4, 'Тикет решён. Записан на пользователя с почтой SOLVED_TICKETS_EMAIL'
|
||||
|
||||
@ -78,7 +105,8 @@ class UnassignedTicket(models.Model):
|
||||
"""
|
||||
Модель не распределенного тикета.
|
||||
"""
|
||||
assignee = models.ForeignKey(to=User, on_delete=models.CASCADE, related_name='tickets', help_text='Пользователь, с которого снят тикет')
|
||||
ticket_id = models.IntegerField(help_text='Номер тикера, для которого сняли ответственного')
|
||||
status = models.IntegerField(choices=UnassignedTicketStatus.choices, default=UnassignedTicketStatus.UNASSIGNED, help_text='Статус тикета')
|
||||
|
||||
assignee = models.ForeignKey(to=get_user_model(), on_delete=models.CASCADE, related_name='tickets',
|
||||
help_text='Пользователь, с которого снят тикет')
|
||||
ticket_id = models.IntegerField(help_text='Номер тикета, для которого сняли ответственного')
|
||||
status = models.IntegerField(choices=UnassignedTicketStatus.choices, default=UnassignedTicketStatus.UNASSIGNED,
|
||||
help_text='Статус тикета')
|
||||
|
@ -1,3 +1,6 @@
|
||||
"""
|
||||
Обработка тикетов.
|
||||
"""
|
||||
import requests
|
||||
from zenpy import TicketApi
|
||||
from zenpy.lib.api_objects import Ticket
|
||||
@ -6,6 +9,9 @@ from main.zendesk_admin import zenpy
|
||||
|
||||
|
||||
class TicketListRequester:
|
||||
"""
|
||||
Класс обработки тикетов.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.email = zenpy.credentials['email']
|
||||
if zenpy.credentials.get('token'):
|
||||
@ -15,11 +21,17 @@ class TicketListRequester:
|
||||
self.token_or_password = zenpy.credentials.get('password')
|
||||
self.prefix = f'https://{zenpy.credentials.get("subdomain")}.zendesk.com/api/v2/'
|
||||
|
||||
def get_tickets_list_for_user(self, zendesk_user):
|
||||
def get_tickets_list_for_user(self, zendesk_user: zenpy) -> str:
|
||||
"""
|
||||
Функция получения списка тикетов пользователя Zendesk.
|
||||
"""
|
||||
url = self.prefix + f'users/{zendesk_user.id}/tickets/assigned'
|
||||
return self._get_tickets(url)
|
||||
|
||||
def get_tickets_list_for_group(self, group):
|
||||
def get_tickets_list_for_group(self, group: zenpy) -> list():
|
||||
"""
|
||||
Функция получения списка тикетов группы пользователей Zendesk.
|
||||
"""
|
||||
url = self.prefix + '/tickets'
|
||||
all_tickets = self._get_tickets(url)
|
||||
tickets = list()
|
||||
@ -28,7 +40,10 @@ class TicketListRequester:
|
||||
tickets.append(ticket)
|
||||
return tickets
|
||||
|
||||
def _get_tickets(self, url):
|
||||
def _get_tickets(self, url: str) -> list():
|
||||
"""
|
||||
Функция получения полного списка тикетов по url.
|
||||
"""
|
||||
response = requests.get(url, auth=(self.email, self.token_or_password))
|
||||
tickets = []
|
||||
if response.status_code != 200:
|
||||
|
@ -1,4 +1,7 @@
|
||||
from django.contrib.auth.models import User
|
||||
"""
|
||||
Сериализаторы.
|
||||
"""
|
||||
from django.contrib.auth import get_user_model
|
||||
from rest_framework import serializers
|
||||
from main.models import UserProfile
|
||||
from access_controller.settings import ZENDESK_ROLES
|
||||
@ -7,14 +10,28 @@ from access_controller.settings import ZENDESK_ROLES
|
||||
class UserSerializer(serializers.HyperlinkedModelSerializer):
|
||||
"""
|
||||
Класс serializer для модели User.
|
||||
|
||||
:param model: Модель, на основании которой создается сериализатор
|
||||
:type model: :class:`django.contrib.auth.Models`
|
||||
:param fields: Передаваемые поля
|
||||
:type email: :class:`list`
|
||||
"""
|
||||
class Meta:
|
||||
model = User
|
||||
model = get_user_model()
|
||||
fields = ['email']
|
||||
|
||||
|
||||
class ProfileSerializer(serializers.HyperlinkedModelSerializer):
|
||||
"""Класс serializer для модели профиля пользователя"""
|
||||
"""
|
||||
Класс serializer для модели профиля пользователя.
|
||||
|
||||
:param user: Вложенный сериализатор
|
||||
:type user: :class:`UserSerializer`
|
||||
:param model: Модель, на основании которой создается сериализатор
|
||||
:type model: :class:`django.contrib.auth.Models`
|
||||
:param fields: Передаваемые поля
|
||||
:type email: :class:`list`
|
||||
"""
|
||||
user = UserSerializer()
|
||||
|
||||
class Meta:
|
||||
@ -23,16 +40,36 @@ class ProfileSerializer(serializers.HyperlinkedModelSerializer):
|
||||
|
||||
|
||||
class ZendeskUserSerializer(serializers.Serializer):
|
||||
"""Класс serializer для объектов пользователей из zenpy"""
|
||||
"""
|
||||
Класс serializer для объектов пользователей из Zenpy.
|
||||
|
||||
:param name: Имя пользователя
|
||||
:type name: :class:`str`
|
||||
:param zendesk_role: Роль из Zendesk
|
||||
:type zendesk_role: :class:`str`
|
||||
:param email: Email пользователя
|
||||
:type email: :class:`str`
|
||||
"""
|
||||
name = serializers.CharField()
|
||||
zendesk_role = serializers.SerializerMethodField('get_zendesk_role')
|
||||
email = serializers.EmailField()
|
||||
|
||||
@staticmethod
|
||||
def get_zendesk_role(obj):
|
||||
def get_zendesk_role(obj) -> str:
|
||||
"""
|
||||
Функция строкового заполнения поля сериализатора zendesk_role.
|
||||
|
||||
:param obj: объект пользователя Zendesk
|
||||
:return: роль engineer либо light_agent
|
||||
"""
|
||||
if obj.custom_role_id == ZENDESK_ROLES['engineer']:
|
||||
return 'engineer'
|
||||
elif obj.custom_role_id == ZENDESK_ROLES['light_agent']:
|
||||
if obj.custom_role_id == ZENDESK_ROLES['light_agent']:
|
||||
return 'light_agent'
|
||||
else:
|
||||
return "empty"
|
||||
return "empty"
|
||||
|
||||
def create(self, validated_data):
|
||||
pass
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
pass
|
||||
|
@ -1,7 +1,10 @@
|
||||
"""
|
||||
Обработка статистики.
|
||||
"""
|
||||
from datetime import date, datetime, timedelta
|
||||
from typing import Optional
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.utils import timezone
|
||||
|
||||
from access_controller.settings import ONE_DAY, ZENDESK_ROLES as ROLES
|
||||
@ -50,19 +53,19 @@ class StatisticData:
|
||||
else:
|
||||
self.statistic = stat
|
||||
|
||||
def get_statistic(self) -> dict:
|
||||
def get_statistic(self) -> Optional[dict]:
|
||||
"""
|
||||
Функция возвращает статистику работы пользователя.
|
||||
|
||||
:return: Словарь statistic с применением формата отображения и интервала работы(если они есть). None, если были ошибки при создании.
|
||||
:return: Словарь statistic с применением формата отображения и интервала работы(если они есть).
|
||||
None, если были ошибки при создании.
|
||||
"""
|
||||
if self.is_valid_statistic():
|
||||
stat = self.statistic
|
||||
stat = self._use_display(stat)
|
||||
stat = self._use_interval(stat)
|
||||
return stat
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
def is_valid_statistic(self) -> bool:
|
||||
"""
|
||||
@ -104,8 +107,7 @@ class StatisticData:
|
||||
"""
|
||||
if self.is_valid_data():
|
||||
return self.data
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
def is_valid_data(self) -> bool:
|
||||
"""
|
||||
@ -170,7 +172,8 @@ class StatisticData:
|
||||
"""
|
||||
Функция возвращает логи в диапазоне дат start_date - end_date для пользователя с указанным email.
|
||||
|
||||
:return: Данные о смене статусов пользователя. Если пользователь не найден или интервал времени некорректен - ошибку.
|
||||
:return: Данные о смене статусов пользователя. Если пользователь не найден или
|
||||
интервал времени некорректен - ошибку.
|
||||
"""
|
||||
if not self.check_time():
|
||||
self.errors += ['Конец диапазона должен быть позже начала диапазона и раньше текущего времени']
|
||||
@ -178,12 +181,12 @@ class StatisticData:
|
||||
try:
|
||||
self.data = RoleChangeLogs.objects.filter(
|
||||
change_time__range=[self.start_date, self.end_date + timedelta(days=1)],
|
||||
user=User.objects.get(email=self.email),
|
||||
user=get_user_model().objects.get(email=self.email),
|
||||
).order_by('change_time')
|
||||
except User.DoesNotExist:
|
||||
except get_user_model().DoesNotExist:
|
||||
self.errors += ['Пользователь не найден']
|
||||
|
||||
def _init_statistic(self) -> dict:
|
||||
def _init_statistic(self) -> None:
|
||||
"""
|
||||
Функция заполняет словарь, в котором ключ - дата, значение - кол-во проработанных в этот день секунд.
|
||||
|
||||
@ -192,18 +195,18 @@ class StatisticData:
|
||||
self.clear_statistic()
|
||||
if not self.get_data():
|
||||
self.warnings += ['Не обнаружены изменения роли в данном промежутке']
|
||||
return None
|
||||
first_log, last_log = self.data[0], self.data[len(self.data) - 1]
|
||||
else:
|
||||
first_log, last_log = self.data[0], self.data[len(self.data) - 1]
|
||||
|
||||
if first_log.old_role == ROLES['engineer']:
|
||||
self.prev_engineer_logic(first_log)
|
||||
if first_log.old_role == ROLES['engineer']:
|
||||
self.prev_engineer_logic(first_log)
|
||||
|
||||
if last_log.new_role == ROLES['engineer']:
|
||||
self.post_engineer_logic(last_log)
|
||||
if last_log.new_role == ROLES['engineer']:
|
||||
self.post_engineer_logic(last_log)
|
||||
|
||||
for log_index in range(len(self.data) - 1):
|
||||
if self.data[log_index].new_role == ROLES['engineer']:
|
||||
self.engineer_logic(log_index)
|
||||
for log_index in range(len(self.data) - 1):
|
||||
if self.data[log_index].new_role == ROLES['engineer']:
|
||||
self.engineer_logic(log_index)
|
||||
|
||||
def engineer_logic(self, log_index):
|
||||
"""
|
||||
@ -238,7 +241,7 @@ class StatisticData:
|
||||
"""
|
||||
Функция обрабатывает случай, когда нам изветсно что инженер начал работу до диапазона
|
||||
"""
|
||||
self.fill_daterange(max(User.objects.get(email=self.email).date_joined.date(), self.start_date),
|
||||
self.fill_daterange(max(get_user_model().objects.get(email=self.email).date_joined.date(), self.start_date),
|
||||
first_log.change_time.date())
|
||||
self.statistic[first_log.change_time.date()] += get_timedelta(first_log).total_seconds()
|
||||
|
||||
|
@ -12,7 +12,7 @@
|
||||
<p>Введте свой e-mail адрес для восстановления пароля.</p>
|
||||
<form action="." method="post">
|
||||
{{ form.as_p }}
|
||||
<p><input class="btn btn-success" type="submit" value="Отпрваить e-mail"></p>
|
||||
<p><input class="btn btn-success" type="submit" value="Отправить e-mail"></p>
|
||||
{% csrf_token %}
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
169
main/tests.py
169
main/tests.py
@ -1,7 +1,11 @@
|
||||
"""
|
||||
Тесты.
|
||||
"""
|
||||
|
||||
|
||||
import random
|
||||
from unittest.mock import patch, Mock
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core import mail
|
||||
from django.http import HttpResponseRedirect
|
||||
@ -27,38 +31,56 @@ class UsersBaseTestCase(TestCase):
|
||||
self.admin = 'admin@gmail.com'
|
||||
self.engineer = 'customer@example.com'
|
||||
self.agent_client = Client()
|
||||
self.agent_client.force_login(User.objects.get(email=self.light_agent))
|
||||
self.agent_client.force_login(get_user_model().objects.get(email=self.light_agent))
|
||||
self.admin_client = Client()
|
||||
self.admin_client.force_login(User.objects.get(email=self.admin))
|
||||
self.admin_client.force_login(get_user_model().objects.get(email=self.admin))
|
||||
self.engineer_client = Client()
|
||||
self.engineer_client.force_login(User.objects.get(email=self.engineer))
|
||||
self.engineer_client.force_login(get_user_model().objects.get(email=self.engineer))
|
||||
|
||||
|
||||
class RegistrationTestCase(TestCase):
|
||||
"""
|
||||
Класс тестирования регистрации пользователя.
|
||||
"""
|
||||
fixtures = ['fixtures/data.json']
|
||||
|
||||
def setUp(self):
|
||||
def setUp(self) -> None:
|
||||
"""
|
||||
Функция предтестовых настроек.
|
||||
"""
|
||||
self.email_backend = 'django.core.mail.backends.locmem.EmailBackend'
|
||||
self.any_zendesk_user_email = 'idar.sokurov.05@mail.ru'
|
||||
self.zendesk_admin_email = 'idar.sokurov.05@mail.ru'
|
||||
self.client = Client()
|
||||
|
||||
def test_registration_complete_redirect(self):
|
||||
def test_registration_complete_redirect(self) -> None:
|
||||
"""
|
||||
Функция тестирования успешно завершенной регистрации.
|
||||
"""
|
||||
with self.settings(EMAIL_BACKEND=self.email_backend):
|
||||
resp = self.client.post(reverse('registration'), data={'email': self.any_zendesk_user_email})
|
||||
self.assertRedirects(resp, reverse('password_reset_done'))
|
||||
|
||||
def test_registration_fail_redirect(self):
|
||||
"""
|
||||
Функция тестирования неуспешной регистрации.
|
||||
"""
|
||||
with self.settings(EMAIL_BACKEND=self.email_backend):
|
||||
resp = self.client.post(reverse('registration'), data={'email': self.any_zendesk_user_email + 'asd'})
|
||||
self.assertRedirects(resp, reverse('django_registration_disallowed'))
|
||||
|
||||
def test_registration_user_already_exist(self):
|
||||
"""
|
||||
Функция тестирования попытки регистрации уже зарегистрированного пользователя.
|
||||
"""
|
||||
with self.settings(EMAIL_BACKEND=self.email_backend) and translation.override('ru'):
|
||||
resp = self.client.post(reverse('registration'), data={'email': '123@test.ru'})
|
||||
self.assertContains(resp, 'Этот адрес электронной почты уже используется', count=1, status_code=200)
|
||||
|
||||
def test_registration_send_email(self):
|
||||
"""
|
||||
Функция тестирования отправки email.
|
||||
"""
|
||||
with self.settings(EMAIL_BACKEND=self.email_backend):
|
||||
response: HttpResponseRedirect = \
|
||||
self.client.post(reverse('registration'), data={'email': self.any_zendesk_user_email})
|
||||
@ -74,49 +96,74 @@ class RegistrationTestCase(TestCase):
|
||||
self.assertEqual(mail.outbox[0].body, correct_body)
|
||||
|
||||
def test_registration_user_creating(self):
|
||||
"""
|
||||
Функция тестирования регистрации пользователя (сверяем имя с именем в Zendesk.
|
||||
"""
|
||||
with self.settings(EMAIL_BACKEND=self.email_backend):
|
||||
self.client.post(reverse('registration'), data={'email': self.any_zendesk_user_email})
|
||||
user = User.objects.get(email=self.any_zendesk_user_email)
|
||||
user = get_user_model().objects.get(email=self.any_zendesk_user_email)
|
||||
zendesk_user = zenpy.get_user(self.any_zendesk_user_email)
|
||||
self.assertEqual(user.userprofile.name, zendesk_user.name)
|
||||
|
||||
def test_permissions_applying(self):
|
||||
"""
|
||||
Функция тестирования проверке присвоения роли admin.
|
||||
"""
|
||||
with self.settings(EMAIL_BACKEND=self.email_backend):
|
||||
self.client.post(reverse('registration'), data={'email': self.zendesk_admin_email})
|
||||
user = User.objects.get(email=self.zendesk_admin_email)
|
||||
user = get_user_model().objects.get(email=self.zendesk_admin_email)
|
||||
self.assertEqual(user.userprofile.role, 'admin')
|
||||
self.assertTrue(user.has_perm('main.has_control_access'))
|
||||
|
||||
|
||||
class MakeEngineerTestCase(UsersBaseTestCase):
|
||||
|
||||
"""
|
||||
Класс тестов для проверки функции назначения роли engineer.
|
||||
"""
|
||||
@patch('main.extra_func.zenpy')
|
||||
def test_become_engineer_redirect(self, _zenpy_mock):
|
||||
user = User.objects.get(email=self.light_agent)
|
||||
"""
|
||||
Функция проверки переадресации пользователя на рабочую страницу после назначения роли engineer.
|
||||
"""
|
||||
user = get_user_model().objects.get(email=self.light_agent)
|
||||
resp = self.agent_client.post(reverse_lazy('work_become_engineer'))
|
||||
self.assertRedirects(resp, reverse('work', args=[user.id]))
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
self.assertFalse(_zenpy_mock.called)
|
||||
|
||||
@patch('main.extra_func.zenpy')
|
||||
def test_light_agent_make_engineer(self, zenpy_mock):
|
||||
"""
|
||||
Функция проверки назначения light_agent на роль engineer.
|
||||
"""
|
||||
self.agent_client.post(reverse_lazy('work_become_engineer'))
|
||||
self.assertEqual(zenpy_mock.update_user.call_args[0][0].custom_role_id, sets.ZENDESK_ROLES['engineer'])
|
||||
|
||||
@patch('main.extra_func.zenpy')
|
||||
def test_admin_make_engineer(self, zenpy_mock):
|
||||
"""
|
||||
Функция проверки назначения admin на роль engineer.
|
||||
"""
|
||||
self.admin_client.post(reverse_lazy('work_become_engineer'))
|
||||
self.assertEqual(zenpy_mock.update_user.call_args[0][0].custom_role_id, sets.ZENDESK_ROLES['engineer'])
|
||||
|
||||
@patch('main.extra_func.zenpy')
|
||||
def test_engineer_make_engineer(self, zenpy_mock):
|
||||
"""
|
||||
Функция проверки назначения engineer на роль engineer.
|
||||
"""
|
||||
self.engineer_client.post(reverse_lazy('work_become_engineer'))
|
||||
self.assertEqual(zenpy_mock.update_user.call_args[0][0].custom_role_id, sets.ZENDESK_ROLES['engineer'])
|
||||
|
||||
@patch('main.extra_func.zenpy')
|
||||
def test_control_page_make_engineer_one(self, zenpy_mock):
|
||||
"""
|
||||
Функция проверки назначения администратором на роль engineer одного пользователя.
|
||||
"""
|
||||
self.admin_client.post(
|
||||
reverse_lazy('control'),
|
||||
data={'users': [User.objects.get(email=self.light_agent).userprofile.id], 'engineer': 'engineer'}
|
||||
data={'users': [get_user_model().objects.get(email=self.light_agent).userprofile.id],
|
||||
'engineer': 'engineer'}
|
||||
)
|
||||
call_list = zenpy_mock.update_user.call_args_list
|
||||
mock_object = call_list[0][0][0]
|
||||
@ -125,12 +172,15 @@ class MakeEngineerTestCase(UsersBaseTestCase):
|
||||
|
||||
@patch('main.extra_func.zenpy')
|
||||
def test_control_page_make_engineer_many(self, zenpy_mock):
|
||||
"""
|
||||
Функция проверки назначения администратором на роль engineer нескольких пользователей.
|
||||
"""
|
||||
self.admin_client.post(
|
||||
reverse_lazy('control'),
|
||||
data={
|
||||
'users': [
|
||||
User.objects.get(email=self.light_agent).userprofile.id,
|
||||
User.objects.get(email=self.engineer).userprofile.id,
|
||||
get_user_model().objects.get(email=self.light_agent).userprofile.id,
|
||||
get_user_model().objects.get(email=self.engineer).userprofile.id,
|
||||
],
|
||||
'engineer': 'engineer'
|
||||
}
|
||||
@ -147,7 +197,7 @@ class MakeLightAgentTestCase(UsersBaseTestCase):
|
||||
@patch('main.requester.TicketListRequester.get_tickets_list_for_user', side_effect=[[]])
|
||||
@patch('main.extra_func.zenpy')
|
||||
def test_hand_over_redirect(self, _zenpy_mock, _user_tickets_mock):
|
||||
user = User.objects.get(email=self.engineer)
|
||||
user = get_user_model().objects.get(email=self.engineer)
|
||||
resp = self.engineer_client.post(reverse_lazy('work_hand_over'))
|
||||
self.assertRedirects(resp, reverse('work', args=[user.id]))
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
@ -203,7 +253,10 @@ class MakeLightAgentTestCase(UsersBaseTestCase):
|
||||
def test_control_page_make_light_agent_one(self, zenpy_mock, _user_tickets_mock):
|
||||
self.admin_client.post(
|
||||
reverse_lazy('control'),
|
||||
data={'users': [User.objects.get(email=self.engineer).userprofile.id], 'light_agent': 'light_agent'}
|
||||
data={
|
||||
'users': [get_user_model().objects.get(email=self.engineer).userprofile.id],
|
||||
'light_agent': 'light_agent'
|
||||
}
|
||||
)
|
||||
call_list = zenpy_mock.update_user.call_args_list
|
||||
mock_object = call_list[0][0][0]
|
||||
@ -217,8 +270,8 @@ class MakeLightAgentTestCase(UsersBaseTestCase):
|
||||
reverse_lazy('control'),
|
||||
data={
|
||||
'users': [
|
||||
User.objects.get(email=self.light_agent).userprofile.id,
|
||||
User.objects.get(email=self.engineer).userprofile.id,
|
||||
get_user_model().objects.get(email=self.light_agent).userprofile.id,
|
||||
get_user_model().objects.get(email=self.engineer).userprofile.id,
|
||||
],
|
||||
'light_agent': 'light_agent'
|
||||
}
|
||||
@ -231,18 +284,29 @@ class MakeLightAgentTestCase(UsersBaseTestCase):
|
||||
|
||||
|
||||
class PasswordResetTestCase(UsersBaseTestCase):
|
||||
|
||||
"""
|
||||
Класс тестов сброса пароля.
|
||||
"""
|
||||
def setUp(self):
|
||||
"""
|
||||
Предустановленные значения для проведения тестов.
|
||||
"""
|
||||
super().setUp()
|
||||
self.email_backend = 'django.core.mail.backends.locmem.EmailBackend'
|
||||
|
||||
def test_redirect(self):
|
||||
"""
|
||||
Функция проверки переадресации на страницу уведомления о сбросе пароля на email.
|
||||
"""
|
||||
with self.settings(EMAIL_BACKEND=self.email_backend):
|
||||
resp = self.agent_client.post(reverse_lazy('password_reset'), data={'email': self.light_agent})
|
||||
self.assertRedirects(resp, reverse('password_reset_done'))
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
|
||||
def test_send_email(self):
|
||||
"""
|
||||
Функция проверки содержания и отправки письма для установки пароля.
|
||||
"""
|
||||
with self.settings(EMAIL_BACKEND=self.email_backend):
|
||||
response: HttpResponseRedirect = \
|
||||
self.agent_client.post(reverse_lazy('password_reset'), data={'email': self.light_agent})
|
||||
@ -258,31 +322,53 @@ class PasswordResetTestCase(UsersBaseTestCase):
|
||||
self.assertEqual(mail.outbox[0].body, correct_body)
|
||||
|
||||
def test_email_invalid(self):
|
||||
"""
|
||||
Функция проверки уведомления клиента о некорректности введенного email.
|
||||
"""
|
||||
with self.settings(EMAIL_BACKEND=self.email_backend) and translation.override('ru'):
|
||||
resp = self.agent_client.post(reverse_lazy('password_reset'), data={'email': 1})
|
||||
self.assertContains(resp, 'Введите правильный адрес электронной почты.', count=1, status_code=200)
|
||||
|
||||
def test_user_does_not_exist(self):
|
||||
"""
|
||||
Функция корректности отработки неверно введенного email.
|
||||
"""
|
||||
with self.settings(EMAIL_BACKEND=self.email_backend):
|
||||
resp = self.agent_client.post(reverse_lazy('password_reset'), data={'email': self.light_agent + str(random.random())})
|
||||
resp = self.agent_client.post(
|
||||
reverse_lazy('password_reset'),
|
||||
data={
|
||||
'email': self.light_agent + str(random.random())
|
||||
}
|
||||
)
|
||||
self.assertRedirects(resp, reverse('password_reset_done'))
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
self.assertEqual(len(mail.outbox), 0)
|
||||
|
||||
|
||||
class PasswordChangeTestCase(UsersBaseTestCase):
|
||||
|
||||
"""
|
||||
Класс тестирования смены пароля.
|
||||
"""
|
||||
def setUp(self):
|
||||
"""
|
||||
Предустановленные значения для проведения тестов.
|
||||
"""
|
||||
super().setUp()
|
||||
self.set_password()
|
||||
|
||||
def set_password(self):
|
||||
user: User = User.objects.get(email=self.light_agent)
|
||||
"""
|
||||
Пароль, сформированный для тестирования.
|
||||
"""
|
||||
user: get_user_model() = get_user_model().objects.get(email=self.light_agent)
|
||||
user.set_password('ImpossiblyHardPassword')
|
||||
user.save()
|
||||
self.agent_client.force_login(User.objects.get(email=self.light_agent))
|
||||
self.agent_client.force_login(get_user_model().objects.get(email=self.light_agent))
|
||||
|
||||
def test_change_successful(self):
|
||||
"""
|
||||
Функция тестирования успешного изменения пароля.
|
||||
"""
|
||||
self.agent_client.post(
|
||||
reverse_lazy('password_change'),
|
||||
data={
|
||||
@ -291,10 +377,13 @@ class PasswordChangeTestCase(UsersBaseTestCase):
|
||||
'new_password2': 'EasyPassword',
|
||||
}
|
||||
)
|
||||
user = User.objects.get(email=self.light_agent)
|
||||
user = get_user_model().objects.get(email=self.light_agent)
|
||||
self.assertTrue(user.check_password('EasyPassword'))
|
||||
|
||||
def test_invalid_old_password(self):
|
||||
"""
|
||||
Функция тестирования отработки неверно введенного старого пароля при смене.
|
||||
"""
|
||||
with translation.override('ru'):
|
||||
resp = self.agent_client.post(
|
||||
reverse_lazy('password_change'),
|
||||
@ -307,6 +396,9 @@ class PasswordChangeTestCase(UsersBaseTestCase):
|
||||
self.assertContains(resp, 'Ваш старый пароль введен неправильно', count=1, status_code=200)
|
||||
|
||||
def test_different_new_passwords(self):
|
||||
"""
|
||||
Функция тестирования случая с вводом двух разных новых паролей.
|
||||
"""
|
||||
with translation.override('ru'):
|
||||
resp = self.agent_client.post(
|
||||
reverse_lazy('password_change'),
|
||||
@ -319,6 +411,9 @@ class PasswordChangeTestCase(UsersBaseTestCase):
|
||||
self.assertContains(resp, 'Введенные пароли не совпадают', count=1, status_code=200)
|
||||
|
||||
def test_invalid_new_password1(self):
|
||||
"""
|
||||
Функция тестирования случая с неправильно подобранным новым паролем (слишком короткий).
|
||||
"""
|
||||
with translation.override('ru'):
|
||||
resp = self.agent_client.post(
|
||||
reverse_lazy('password_change'),
|
||||
@ -331,6 +426,9 @@ class PasswordChangeTestCase(UsersBaseTestCase):
|
||||
self.assertContains(resp, 'Введённый пароль слишком короткий', count=1, status_code=200)
|
||||
|
||||
def test_invalid_new_password2(self):
|
||||
"""
|
||||
Функция тестирования случая с неправильно подобранным новым паролем (употребляются только цифры).
|
||||
"""
|
||||
with translation.override('ru'):
|
||||
resp = self.agent_client.post(
|
||||
reverse_lazy('password_change'),
|
||||
@ -343,6 +441,9 @@ class PasswordChangeTestCase(UsersBaseTestCase):
|
||||
self.assertContains(resp, 'Введённый пароль состоит только из цифр', count=1, status_code=200)
|
||||
|
||||
def test_invalid_new_password3(self):
|
||||
"""
|
||||
Функция тестирования случая с неправильно подобранным новым паролем (совпадает с именем пользователя).
|
||||
"""
|
||||
with translation.override('ru'):
|
||||
resp = self.agent_client.post(
|
||||
reverse_lazy('password_change'),
|
||||
@ -367,10 +468,11 @@ class GetTicketsTestCase(UsersBaseTestCase):
|
||||
Функция проверки переадресации пользователя на рабочую страницу.
|
||||
"""
|
||||
get_user_mock.return_value = Mock()
|
||||
user = User.objects.get(email=self.engineer)
|
||||
user = get_user_model().objects.get(email=self.engineer)
|
||||
resp = self.engineer_client.post(reverse('work_get_tickets'))
|
||||
self.assertRedirects(resp, reverse('work', args=[user.id]))
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
self.assertFalse(_zenpy_mock.called)
|
||||
|
||||
@patch('main.views.zenpy')
|
||||
@patch('main.views.get_tickets_list_for_group')
|
||||
@ -410,11 +512,11 @@ class GetTicketsTestCase(UsersBaseTestCase):
|
||||
|
||||
@patch('main.views.zenpy')
|
||||
@patch('main.views.get_tickets_list_for_group')
|
||||
def test_take_zero_tickets(self, TicketsMock, zenpy_mock):
|
||||
def test_take_zero_tickets(self, tickets_mock, zenpy_mock):
|
||||
"""
|
||||
Функция проверки попытки назначения нуля тикета на engineer.
|
||||
"""
|
||||
TicketsMock.return_value = [Mock()] * 3
|
||||
tickets_mock.return_value = [Mock()] * 3
|
||||
zenpy_mock.get_user.return_value = Mock(role='agent', custom_role_id=sets.ZENDESK_ROLES['engineer'])
|
||||
self.engineer_client.post(reverse('work_get_tickets'), data={'count_tickets': 0})
|
||||
tickets = zenpy_mock.update_tickets.call_args[0][0]
|
||||
@ -446,9 +548,9 @@ class ProfileTestCase(TestCase):
|
||||
self.zendesk_agent_email = 'krav-88@mail.ru'
|
||||
self.zendesk_admin_email = 'idar.sokurov.05@mail.ru'
|
||||
self.client = Client()
|
||||
self.client.force_login(User.objects.get(email=self.zendesk_agent_email))
|
||||
self.client.force_login(get_user_model().objects.get(email=self.zendesk_agent_email))
|
||||
self.admin_client = Client()
|
||||
self.admin_client.force_login(User.objects.get(email=self.zendesk_admin_email))
|
||||
self.admin_client.force_login(get_user_model().objects.get(email=self.zendesk_admin_email))
|
||||
|
||||
def test_correct_username(self):
|
||||
"""
|
||||
@ -497,15 +599,14 @@ class LoggingTestCase(UsersBaseTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.admin_profile = User.objects.get(email=self.admin).userprofile
|
||||
self.agent_profile = User.objects.get(email=self.light_agent).userprofile
|
||||
self.engineer_profile = User.objects.get(email=self.engineer).userprofile
|
||||
self.admin_profile = get_user_model().objects.get(email=self.admin).userprofile
|
||||
self.agent_profile = get_user_model().objects.get(email=self.light_agent).userprofile
|
||||
self.engineer_profile = get_user_model().objects.get(email=self.engineer).userprofile
|
||||
|
||||
@staticmethod
|
||||
def get_file_output():
|
||||
file = open('logs/logs.csv', 'r')
|
||||
file_output = file.readlines()[-1]
|
||||
file.close()
|
||||
with open('logs/logs.csv', 'r') as file:
|
||||
file_output = file.readlines()[-1]
|
||||
return file_output
|
||||
|
||||
def test_engineer_with_admin(self):
|
||||
|
@ -1,3 +1,7 @@
|
||||
"""
|
||||
REST framework adds support for automatic URL routing to Django.
|
||||
"""
|
||||
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from main.views import UsersViewSet
|
||||
|
||||
|
116
main/views.py
116
main/views.py
@ -1,10 +1,17 @@
|
||||
"""
|
||||
View функции.
|
||||
"""
|
||||
|
||||
|
||||
from smtplib import SMTPException
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.forms import PasswordResetForm
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
|
||||
from django.contrib.auth.models import User, Permission
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.contrib.auth.tokens import default_token_generator
|
||||
from django.contrib.auth.views import LoginView
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
@ -15,7 +22,7 @@ from django.shortcuts import render, redirect
|
||||
from django.urls import reverse_lazy
|
||||
from django.views.generic import FormView
|
||||
from django_registration.views import RegistrationView
|
||||
# Django REST
|
||||
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.response import Response
|
||||
|
||||
@ -23,14 +30,35 @@ from access_controller.settings import DEFAULT_FROM_EMAIL, ZENDESK_ROLES, ZENDES
|
||||
from main.extra_func import check_user_exist, update_profile, get_user_organization, \
|
||||
make_engineer, make_light_agent, get_users_list, update_users_in_model, count_users, \
|
||||
set_session_params_for_work_page, get_tickets_list_for_group
|
||||
from main.forms import AdminPageUsers, CustomRegistrationForm, CustomAuthenticationForm, StatisticForm, \
|
||||
WorkGetTicketsForm
|
||||
from main.forms import AdminPageUsers, CustomRegistrationForm, CustomAuthenticationForm, \
|
||||
StatisticForm, WorkGetTicketsForm
|
||||
from main.serializers import ProfileSerializer, ZendeskUserSerializer
|
||||
from main.zendesk_admin import zenpy
|
||||
from .models import UserProfile
|
||||
from .statistic_data import StatisticData
|
||||
|
||||
|
||||
def setup_context(**kwargs) -> Dict[str, Any]:
|
||||
"""
|
||||
Функция добавления в контекст статуса пользователя.
|
||||
|
||||
:param profile_lit: True, при создании профиля пользователя, иначе False
|
||||
:param control_lit: False
|
||||
:param work_lit: True, при установке пользователю рабочей роли, иначе False
|
||||
:param registration_lit: True, при регистрации пользователя, иначе False
|
||||
:param login_lit: True, если пользователь залогинен, иначе False
|
||||
:param stats_lit: True, при получении пользователем прав администратора (просмотр статистики), иначе False
|
||||
:return: Контекст (context)
|
||||
"""
|
||||
context = {}
|
||||
for key in ('profile_lit', 'control_lit', 'work_lit', 'registration_lit', 'login_lit', 'stats_lit'):
|
||||
if key in kwargs:
|
||||
context.update({key: True})
|
||||
else:
|
||||
context.update({key: False})
|
||||
return context
|
||||
|
||||
|
||||
class CustomRegistrationView(RegistrationView):
|
||||
"""
|
||||
Отображение и логика работы страницы регистрации пользователя.
|
||||
@ -41,9 +69,11 @@ class CustomRegistrationView(RegistrationView):
|
||||
:type template_name: :class:`str`
|
||||
:param success_url: Указание пути к html-странице завершения регистрации
|
||||
:type success_url: :class:`django.utils.functional.lazy.<locals>.__proxy__`
|
||||
:param is_allowed: Определение зарегистрирован ли пользователь с введенным email на Zendesk и принадлежит ли он к организации SYSTEM
|
||||
:param is_allowed: Определение зарегистрирован ли пользователь с введенным email на Zendesk и
|
||||
принадлежит ли он к организации SYSTEM
|
||||
:type is_allowed: :class:`bool`
|
||||
"""
|
||||
extra_context = setup_context(registration_lit=True)
|
||||
form_class = CustomRegistrationForm
|
||||
template_name = 'django_registration/registration_form.html'
|
||||
urls = {
|
||||
@ -53,7 +83,7 @@ class CustomRegistrationView(RegistrationView):
|
||||
}
|
||||
redirect_url = 'done'
|
||||
|
||||
def register(self, form: CustomRegistrationForm) -> User:
|
||||
def register(self, form: CustomRegistrationForm) -> Optional[get_user_model()]:
|
||||
"""
|
||||
Функция регистрации пользователя.
|
||||
1. Ввод email пользователя, указанный на Zendesk
|
||||
@ -62,7 +92,7 @@ class CustomRegistrationView(RegistrationView):
|
||||
3. Создается пользователь class User, а также его профиль.
|
||||
|
||||
:param form: Email пользователя на Zendesk
|
||||
:return: user
|
||||
:return: User
|
||||
"""
|
||||
self.redirect_url = 'done'
|
||||
if check_user_exist(form.data['email']) and get_user_organization(form.data['email']) == 'SYSTEM':
|
||||
@ -78,10 +108,10 @@ class CustomRegistrationView(RegistrationView):
|
||||
'html_email_template_name': None,
|
||||
'extra_email_context': None,
|
||||
}
|
||||
user = User.objects.create_user(
|
||||
user = get_user_model().objects.create_user(
|
||||
username=form.data['email'],
|
||||
email=form.data['email'],
|
||||
password=User.objects.make_random_password(length=50)
|
||||
password=get_user_model().objects.make_random_password(length=50)
|
||||
)
|
||||
try:
|
||||
update_profile(user.userprofile)
|
||||
@ -90,13 +120,16 @@ class CustomRegistrationView(RegistrationView):
|
||||
return user
|
||||
except SMTPException:
|
||||
self.redirect_url = 'email_sending_error'
|
||||
return None
|
||||
else:
|
||||
self.redirect_url = 'email_sending_error'
|
||||
return None
|
||||
else:
|
||||
self.redirect_url = 'invalid_zendesk_email'
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def set_permission(user: User) -> None:
|
||||
def set_permission(user: get_user_model()) -> None:
|
||||
"""
|
||||
Функция дает разрешение на просмотр страница администратора, если пользователь имеет роль admin.
|
||||
|
||||
@ -110,7 +143,7 @@ class CustomRegistrationView(RegistrationView):
|
||||
)
|
||||
user.user_permissions.add(permission)
|
||||
|
||||
def get_success_url(self, user: User = None):
|
||||
def get_success_url(self, user: get_user_model() = None) -> Dict:
|
||||
"""
|
||||
Функция возвращает url-адрес страницы, куда нужно перейти после успешной/не успешной регистрации.
|
||||
Используется самой django-registration.
|
||||
@ -121,7 +154,13 @@ class CustomRegistrationView(RegistrationView):
|
||||
return self.urls[self.redirect_url]
|
||||
|
||||
|
||||
def registration_error(request):
|
||||
def registration_error(request: WSGIRequest) -> HttpResponse:
|
||||
"""
|
||||
Функция отображения страницы ошибки регистрации.
|
||||
|
||||
:param request: регистрация
|
||||
:return: адресация на страницу ошибки
|
||||
"""
|
||||
return render(request, 'django_registration/registration_error.html')
|
||||
|
||||
|
||||
@ -144,7 +183,7 @@ def profile_page(request: WSGIRequest) -> HttpResponse:
|
||||
|
||||
|
||||
@login_required()
|
||||
def work_page(request: WSGIRequest, id: int) -> HttpResponse:
|
||||
def work_page(request: WSGIRequest, required_id: int) -> HttpResponse:
|
||||
"""
|
||||
Функция отображения страницы "Управления правами" для текущего пользователя (login_required).
|
||||
|
||||
@ -153,7 +192,7 @@ def work_page(request: WSGIRequest, id: int) -> HttpResponse:
|
||||
:return: адресация на страницу "Управления правами" (либо на страницу "Авторизации", если id и user.id не совпадают
|
||||
"""
|
||||
users = get_users_list()
|
||||
if request.user.id == id:
|
||||
if request.user.id == required_id:
|
||||
if request.session.get('is_confirm', None):
|
||||
messages.success(request, 'Изменения были применены')
|
||||
elif request.session.get('is_confirm', None) is not None:
|
||||
@ -184,7 +223,7 @@ def work_page(request: WSGIRequest, id: int) -> HttpResponse:
|
||||
|
||||
|
||||
@login_required()
|
||||
def work_hand_over(request: WSGIRequest):
|
||||
def work_hand_over(request: WSGIRequest) -> HttpResponseRedirect:
|
||||
"""
|
||||
Функция позволяет текущему пользователю сдать права, а именно сменить в Zendesk роль с "engineer" на "light_agent"
|
||||
|
||||
@ -198,18 +237,23 @@ def work_hand_over(request: WSGIRequest):
|
||||
@login_required()
|
||||
def work_become_engineer(request: WSGIRequest) -> HttpResponseRedirect:
|
||||
"""
|
||||
Функция позволяет текущему пользователю получить права, а именно сменить в Zendesk роль с "light_agent" на "engineer"
|
||||
Функция позволяет текущему пользователю получить права, а именно сменить в Zendesk роль с "light_agent"
|
||||
на "engineer".
|
||||
|
||||
:param request: данные текущего пользователя (login_required)
|
||||
:return: перезагрузка текущей страницы после выполнения смены роли
|
||||
"""
|
||||
|
||||
make_engineer(request.user.userprofile, request.user)
|
||||
return set_session_params_for_work_page(request)
|
||||
|
||||
|
||||
@login_required()
|
||||
def work_get_tickets(request):
|
||||
def work_get_tickets(request: WSGIRequest) -> HttpResponse:
|
||||
"""
|
||||
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
zenpy_user = zenpy.get_user(request.user.email)
|
||||
|
||||
if request.method == 'POST':
|
||||
@ -266,7 +310,7 @@ class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageM
|
||||
self.make_light_agents(users)
|
||||
return super().form_valid(form)
|
||||
|
||||
def make_engineers(self, users):
|
||||
def make_engineers(self, users: list) -> None:
|
||||
"""
|
||||
Функция проходит по списку пользователей, проставляя статус "engineer".
|
||||
|
||||
@ -276,7 +320,7 @@ class AdminPageView(LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageM
|
||||
for user in users:
|
||||
make_engineer(user, self.request.user)
|
||||
|
||||
def make_light_agents(self, users):
|
||||
def make_light_agents(self, users: list) -> None:
|
||||
"""
|
||||
Функция проходит по списку пользователей, проставляя статус "light agent".
|
||||
|
||||
@ -291,17 +335,30 @@ class CustomLoginView(LoginView):
|
||||
"""
|
||||
Отображение страницы авторизации пользователя
|
||||
"""
|
||||
extra_context = setup_context(login_lit=True)
|
||||
form_class = CustomAuthenticationForm
|
||||
|
||||
|
||||
class UsersViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""
|
||||
Класс для получения пользователей с помощью api
|
||||
Класс для получения пользователей с помощью api.
|
||||
|
||||
:param queryset: Список пользователей с ролью 'agent'
|
||||
:type queryset: :class:`str`
|
||||
:param serializer_class: Класс сериализатор для модели профиля пользователя
|
||||
:type serializer_class: :class:`ProfileSerializer`
|
||||
"""
|
||||
queryset = UserProfile.objects.filter(role='agent')
|
||||
serializer_class = ProfileSerializer
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
def list(self, request: WSGIRequest, *args, **kwargs) -> Response:
|
||||
"""
|
||||
Функция возвращает список пользователей, список пользователей Zendesk, количество engineers и light-agents.
|
||||
:param request: Запрос
|
||||
:param args: Аргументы
|
||||
:param kwargs: Параметры
|
||||
:return: Список пользователей
|
||||
"""
|
||||
users = update_users_in_model()
|
||||
count = count_users(users.values)
|
||||
profiles = UserProfile.objects.filter(role='agent')
|
||||
@ -316,7 +373,13 @@ class UsersViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
return Response(res)
|
||||
|
||||
@staticmethod
|
||||
def choose_users(zendesk, model):
|
||||
def choose_users(zendesk: list, model: list) -> list:
|
||||
"""
|
||||
Функция формирует список пользователей, которые не зарегистрированы у нас.
|
||||
:param zendesk: Список пользователей Zendesk
|
||||
:param model: Список пользователей (модель Userprofile)
|
||||
:return: Список
|
||||
"""
|
||||
users = []
|
||||
for zendesk_user in zendesk:
|
||||
if zendesk_user.name not in [user.name for user in model]:
|
||||
@ -324,7 +387,12 @@ class UsersViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
return users
|
||||
|
||||
@staticmethod
|
||||
def get_zendesk_users(users):
|
||||
def get_zendesk_users(users: list) -> list:
|
||||
"""
|
||||
Получение списка пользователей Zendesk, не являющихся админами.
|
||||
:param users: Список пользователей
|
||||
:return: Список пользователей, не являющимися администраторами.
|
||||
"""
|
||||
zendesk_users = ZendeskUserSerializer(
|
||||
data=[user for user in users if user.role != 'admin'],
|
||||
many=True
|
||||
|
@ -1,3 +1,7 @@
|
||||
"""
|
||||
Функционал работы администратора Zendesk.
|
||||
"""
|
||||
|
||||
from typing import Optional, Dict, List
|
||||
|
||||
from zenpy import Zenpy
|
||||
@ -10,17 +14,23 @@ from access_controller.settings import ACTRL_ZENDESK_SUBDOMAIN, ACTRL_API_EMAIL,
|
||||
|
||||
class ZendeskAdmin:
|
||||
"""
|
||||
Класс **ZendeskAdmin** существует, чтобы в каждой функции отдельно не проверять аккаунт администратора.
|
||||
Класс **ZendeskAdmin** содержит описание всего функционала администратора.
|
||||
|
||||
:param credentials: Полномочия (первым указывается учетная запись организации в Zendesk)
|
||||
:type credentials: :class:`Dict[str, str]`
|
||||
:param admin: Администратор
|
||||
:type admin: :class:`Zenpy`
|
||||
:param buffer_group_id: ID буферной группы
|
||||
:type buffer_group_id: :class:`int`
|
||||
:param solved_tickets_user_id: ID пользователя, который решил тикет
|
||||
:type solved_tickets_user_id: :class:`int`
|
||||
"""
|
||||
|
||||
def __init__(self, credentials: Dict[str, str]):
|
||||
self.credentials = credentials
|
||||
self.admin = self.create_admin()
|
||||
self.buffer_group_id: int = self.get_group(ZENDESK_GROUPS['buffer']).id
|
||||
self.solved_tickets_user_id: int = self.get_user(SOLVED_TICKETS_EMAIL).id
|
||||
self.buffer_group_id= self.get_group(ZENDESK_GROUPS['buffer']).id
|
||||
self.solved_tickets_user_id = self.get_user(SOLVED_TICKETS_EMAIL).id
|
||||
|
||||
def update_user(self, user: ZenpyUser) -> bool:
|
||||
"""
|
||||
@ -46,7 +56,7 @@ class ZendeskAdmin:
|
||||
:param email: Email пользователя
|
||||
:return: Является ли зарегистрированным
|
||||
"""
|
||||
return True if self.admin.search(email, type='user') else False
|
||||
return bool(self.admin.search(email, type='user'))
|
||||
|
||||
def get_user(self, email: str) -> ZenpyUser:
|
||||
"""
|
||||
@ -96,9 +106,8 @@ class ZendeskAdmin:
|
||||
admin = Zenpy(**self.credentials)
|
||||
try:
|
||||
admin.search(self.credentials['email'], type='user')
|
||||
except APIException:
|
||||
raise ValueError('invalid access_controller`s login data')
|
||||
|
||||
except APIException as invalid_data:
|
||||
raise ValueError('invalid access_controller`s login data') from invalid_data
|
||||
return admin
|
||||
|
||||
|
||||
|
@ -7,6 +7,12 @@ sphinx-rtd-theme==0.5.2
|
||||
sphinx-autodoc-typehints==1.12.0
|
||||
pyenchant==3.2.0
|
||||
sphinxcontrib-spelling==7.2.1
|
||||
m2r == 0.2.1
|
||||
|
||||
# Tests
|
||||
coverage==5.5
|
||||
|
||||
# Code style
|
||||
pylint == 2.8.2
|
||||
pylint-django == 2.4.4
|
||||
autopep8 == 1.5.6
|
||||
|
Loading…
x
Reference in New Issue
Block a user