diff --git a/.vscode/launch.json b/.vscode/launch.json index dcd074d6..f677f73c 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -11,6 +11,13 @@ "script": "${workspaceFolder}/rsconcept/RunServer.ps1", "args": [] }, + { + "name": "Lint", + "type": "PowerShell", + "request": "launch", + "script": "${workspaceFolder}/rsconcept/RunLint.ps1", + "args": [] + }, { "name": "Test", "type": "PowerShell", diff --git a/.vscode/settings.json b/.vscode/settings.json index 897e6a8c..9355f26b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,23 +1,18 @@ { "python.testing.unittestArgs": [ - "-v", - "-s", - "./tests", - "-p", - "test*.py" - ], - "python.testing.pytestEnabled": false, - "python.testing.unittestEnabled": true, - "python.linting.pylintEnabled": true, - "python.linting.enabled": true, - "python.linting.pylintArgs": [ - "--max-line-length=120", - "--disable=invalid-name", - "--disable=line-too-long", - "--disable=no-else-return", - "--disable=too-many-return-statements", - "--disable=no-else-break", - "--disable=no-else-continue" + "-v", + "-s", + "./tests", + "-p", + "test*.py" ], - "eslint.workingDirectories": [{ "mode": "auto" }] + "python.testing.pytestEnabled": false, + "python.testing.unittestEnabled": true, + "eslint.workingDirectories": [ + { + "mode": "auto" + } + ], + "python.linting.pylintEnabled": true, + "python.linting.enabled": true } \ No newline at end of file diff --git a/README.md b/README.md index 9e089614..f4c08ffc 100644 --- a/README.md +++ b/README.md @@ -81,8 +81,8 @@ This readme file is used mostly to document project dependencies
VS Code plugins
-  - 
-  - 
+  - Pylance
+  - Pylint
   
diff --git a/rsconcept/RunLint.ps1 b/rsconcept/RunLint.ps1 new file mode 100644 index 00000000..eb253d39 --- /dev/null +++ b/rsconcept/RunLint.ps1 @@ -0,0 +1,6 @@ + # Run coverage analysis +Set-Location $PSScriptRoot\backend + +$pylint = "$PSScriptRoot\backend\venv\Scripts\pylint.exe" + +& $pylint cctext project apps \ No newline at end of file diff --git a/rsconcept/backend/.pylintrc b/rsconcept/backend/.pylintrc new file mode 100644 index 00000000..a348a176 --- /dev/null +++ b/rsconcept/backend/.pylintrc @@ -0,0 +1,633 @@ +[MAIN] + +# 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 + +# Clear in-memory caches upon conclusion of linting. Useful if running pylint +# in a server-like mode. +clear-cache-post-run=no + +# Load and enable all available extensions. Use --list-extensions to see a list +# all available extensions. +#enable-all-extensions= + +# In error mode, messages with a category besides ERROR or FATAL are +# suppressed, and no reports are done by default. Error mode is compatible with +# disabling specific errors. +#errors-only= + +# Always return a 0 (non-error) status code, even if lint errors are found. +# This is primarily useful in continuous integration scripts. +#exit-zero= + +# 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=pyconcept + +# 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= + +# Return non-zero exit code if any of these messages/categories are detected, +# even if score is above --fail-under value. Syntax same as enable. Messages +# specified are enabled, while categories only check already-enabled messages. +fail-on= + +# Specify a score threshold under which the program will exit with error. +fail-under=10 + +# Interpret the stdin as a python script, whose filename needs to be passed as +# the module_or_package argument. +#from-stdin= + +# Files or directories to be skipped. They should be base names, not paths. +ignore=CVS + +# Add files or directories matching the regular expressions patterns to the +# ignore-list. The regex matches against paths and can be in Posix or Windows +# format. Because '\\' represents the directory delimiter on Windows systems, +# it can't be used as an escape character. +ignore-paths=t_.*,.*migrations.* + +# Files or directories matching the regular expression patterns are skipped. +# The regex matches against base names, not paths. The default value ignores +# Emacs file locks +ignore-patterns=t_.*?py + +# 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= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use, and will cap the count on Windows to +# avoid hangs. +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= + +# Pickle collected data for later comparisons. +persistent=yes + +# Minimum Python version to use for version dependent checks. Will default to +# the version used to run pylint. +py-version=3.9 + +# Discover python modules and packages in the file system subtree. +recursive=no + +# Add paths to the list of the source roots. Supports globbing patterns. The +# source root is an absolute path or a path relative to the current working +# directory used to determine a package namespace for modules located under the +# source root. +source-roots= + +# 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 + +# In verbose mode, extra non-checker-related info will be displayed. +#verbose= + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. If left empty, argument names will be checked with the set +# 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. If left empty, attribute names will be checked with the set 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. If left empty, class attribute names will be checked +# with the set 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. If left empty, class constant names will be checked with +# the set 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. If left empty, class names will be checked with the set 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. If left empty, constant names will be checked with the set 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. If left empty, function names will be checked with the set +# naming style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + 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. If left empty, inline iteration names will be checked +# with the set 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. If left empty, method names will be checked with the set 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. If left empty, module names will be checked with the set 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 + +# Regular expression matching correct type alias names. If left empty, type +# alias names will be checked with the set naming style. +#typealias-rgx= + +# Regular expression matching correct type variable names. If left empty, type +# variable names will be checked with the set naming style. +#typevar-rgx= + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. If left empty, variable names will be checked with the set +# naming style. +#variable-rgx= + + +[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, + asyncSetUp, + __post_init__ + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict,_fields,_replace,_source,_make,os._exit + +# 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=mcs + + +[DESIGN] + +# List of regular expressions of class ancestor names to ignore when counting +# public methods (see R0903) +exclude-too-few-public-methods= + +# List of qualified class names to ignore when counting class parents (see +# R0901) +ignored-parents= + +# Maximum number of arguments for function / method. +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# 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=7 + +# 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 + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when caught. +overgeneral-exceptions=builtins.BaseException,builtins.Exception + + +[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*(# )??$ + +# 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=100 + +# 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 + + +[IMPORTS] + +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + +# Allow explicit reexports by alias from a package __init__. +allow-reexport-from-package=no + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules= + +# 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= + + +[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 + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, CONTROL_FLOW, 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 re-enable 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=too-many-public-methods, + invalid-name, + no-else-break, + no-else-continue, + no-else-return, + no-member, + too-many-return-statements, + too-many-locals, + too-many-instance-attributes, + too-few-public-methods, + unused-argument, + missing-function-docstring, + attribute-defined-outside-init, + ungrouped-imports + +# 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 + + +[METHOD_ARGS] + +# List of qualified names (i.e., library.method) which require a timeout +# parameter e.g. 'requests.api.get,requests.api.post' +timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request +max-line-length=120 + +[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= + + +[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 + + +[REPORTS] + +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'fatal', 'error', 'warning', 'refactor', +# 'convention', and 'info' 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=max(0, 0 if fatal else 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= + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[SIMILARITIES] + +# Comments are removed from the similarity computation +ignore-comments=yes + +# Docstrings are removed from the similarity computation +ignore-docstrings=yes + +# Imports are removed from the similarity computation +ignore-imports=yes + +# Signatures are removed from the similarity computation +ignore-signatures=yes + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. No available dictionaries : You need to install +# both the python package and the system dependency for enchant to work.. +spelling-dict= + +# List of comma separated words that should be considered directives if they +# appear at 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 + + +[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 + + +[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 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 symbolic message names to ignore for Mixin members. +ignored-checks-for-mixins=no-member, + not-async-context-manager, + not-context-manager, + attribute-defined-outside-init + +# 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,argparse.Namespace + +# 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 + +# Regex pattern to define which classes are considered mixins. +mixin-class-rgx=.*[Mm]ixin + +# List of decorators that change the signature of a decorated function. +signature-mutators= + + +[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. +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 diff --git a/rsconcept/backend/apps/rsform/admin.py b/rsconcept/backend/apps/rsform/admin.py index 932a8170..c3d4ecbe 100644 --- a/rsconcept/backend/apps/rsform/admin.py +++ b/rsconcept/backend/apps/rsform/admin.py @@ -1,14 +1,15 @@ +''' Admin view: RSForms for conceptual schemas. ''' from django.contrib import admin from . import models class ConstituentaAdmin(admin.ModelAdmin): - pass + ''' Admin model: Constituenta. ''' class RSFormAdmin(admin.ModelAdmin): - pass + ''' Admin model: RSForm. ''' admin.site.register(models.Constituenta, ConstituentaAdmin) diff --git a/rsconcept/backend/apps/rsform/apps.py b/rsconcept/backend/apps/rsform/apps.py index de57d51f..39f7228f 100644 --- a/rsconcept/backend/apps/rsform/apps.py +++ b/rsconcept/backend/apps/rsform/apps.py @@ -1,6 +1,8 @@ +''' Application: RSForms for conceptual schemas. ''' from django.apps import AppConfig class RsformConfig(AppConfig): + ''' Application config. ''' default_auto_field = 'django.db.models.BigAutoField' name = 'apps.rsform' diff --git a/rsconcept/backend/apps/rsform/models.py b/rsconcept/backend/apps/rsform/models.py index 3b672aa8..e0712e0d 100644 --- a/rsconcept/backend/apps/rsform/models.py +++ b/rsconcept/backend/apps/rsform/models.py @@ -1,12 +1,12 @@ +''' Models: RSForms for conceptual schemas. ''' import json +import pyconcept from django.db import models, transaction from django.core.validators import MinValueValidator from django.core.exceptions import ValidationError from django.urls import reverse from apps.users.models import User -import pyconcept - class CstType(models.TextChoices): ''' Type of constituenta ''' @@ -65,6 +65,7 @@ class RSForm(models.Model): ) class Meta: + ''' Model metadata. ''' verbose_name = 'Схема' verbose_name_plural = 'Схемы' @@ -73,7 +74,7 @@ class RSForm(models.Model): return Constituenta.objects.filter(schema=self) @transaction.atomic - def insert_at(self, position: int, alias: str, type: CstType) -> 'Constituenta': + def insert_at(self, position: int, alias: str, insert_type: CstType) -> 'Constituenta': ''' Insert new constituenta at given position. All following constituents order is shifted by 1 position ''' if position <= 0: raise ValidationError('Invalid position: should be positive integer') @@ -86,7 +87,7 @@ class RSForm(models.Model): schema=self, order=position, alias=alias, - cst_type=type + cst_type=insert_type ) self._update_from_core() self.save() @@ -94,7 +95,7 @@ class RSForm(models.Model): return result @transaction.atomic - def insert_last(self, alias: str, type: CstType) -> 'Constituenta': + def insert_last(self, alias: str, insert_type: CstType) -> 'Constituenta': ''' Insert new constituenta at last position ''' position = 1 if self.constituents().exists(): @@ -103,7 +104,7 @@ class RSForm(models.Model): schema=self, order=position, alias=alias, - cst_type=type + cst_type=insert_type ) self._update_from_core() self.save() @@ -181,6 +182,7 @@ class RSForm(models.Model): comment=data.get('comment', ''), is_common=is_common ) + # pylint: disable=protected-access schema._create_items_from_trs(data['items']) return schema @@ -211,13 +213,13 @@ class RSForm(models.Model): def _update_from_core(self) -> dict: checked = json.loads(pyconcept.check_schema(json.dumps(self.to_trs()))) update_list = self.constituents().only('id', 'order') - if (len(checked['items']) != update_list.count()): + if len(checked['items']) != update_list.count(): raise ValidationError order = 1 for cst in checked['items']: - id = cst['entityUID'] + cst_id = cst['entityUID'] for oldCst in update_list: - if oldCst.id == id: + if oldCst.id == cst_id: oldCst.order = order order += 1 break @@ -228,8 +230,8 @@ class RSForm(models.Model): def _create_items_from_trs(self, items): order = 1 for cst in items: - object = Constituenta.create_from_trs(cst, self, order) - object.save() + cst_object = Constituenta.create_from_trs(cst, self, order) + cst_object.save() order += 1 @@ -292,6 +294,7 @@ class Constituenta(models.Model): ) class Meta: + ''' Model metadata. ''' verbose_name = 'Конституета' verbose_name_plural = 'Конституенты' @@ -310,6 +313,7 @@ class Constituenta(models.Model): order=order, cst_type=data['cstType'], ) + # pylint: disable=protected-access cst._load_texts(data) return cst diff --git a/rsconcept/backend/apps/rsform/serializers.py b/rsconcept/backend/apps/rsform/serializers.py index 1e5421c5..a45e7746 100644 --- a/rsconcept/backend/apps/rsform/serializers.py +++ b/rsconcept/backend/apps/rsform/serializers.py @@ -1,3 +1,4 @@ +''' Serializers for conceptual schema API. ''' import json from rest_framework import serializers @@ -6,27 +7,52 @@ from .models import Constituenta, RSForm class FileSerializer(serializers.Serializer): + ''' Serializer: File input. ''' file = serializers.FileField(allow_empty_file=False) + def create(self, validated_data): + raise NotImplementedError('unexpected `create()` call') + + def update(self, instance, validated_data): + raise NotImplementedError('unexpected `update()` call') + class ExpressionSerializer(serializers.Serializer): + ''' Serializer: RSLang expression. ''' expression = serializers.CharField() + def create(self, validated_data): + raise NotImplementedError('unexpected `create()` call') + + def update(self, instance, validated_data): + raise NotImplementedError('unexpected `update()` call') + class RSFormSerializer(serializers.ModelSerializer): + ''' Serializer: General purpose RSForm data. ''' class Meta: + ''' serializer metadata. ''' model = RSForm fields = '__all__' read_only_fields = ('owner', 'id') class RSFormUploadSerializer(serializers.Serializer): + ''' Upload data for RSForm serializer. ''' file = serializers.FileField() load_metadata = serializers.BooleanField() + def create(self, validated_data): + raise NotImplementedError('unexpected `create()` call') + + def update(self, instance, validated_data): + raise NotImplementedError('unexpected `update()` call') + class RSFormContentsSerializer(serializers.ModelSerializer): + ''' Serializer: Detailed data for RSForm. ''' class Meta: + ''' serializer metadata. ''' model = RSForm def to_representation(self, instance: RSForm): @@ -37,77 +63,10 @@ class RSFormContentsSerializer(serializers.ModelSerializer): return result -class ConstituentaSerializer(serializers.ModelSerializer): - class Meta: - model = Constituenta - fields = '__all__' - read_only_fields = ('id', 'order', 'alias', 'cst_type') - - def update(self, instance: Constituenta, validated_data) -> Constituenta: - if ('term_raw' in validated_data): - validated_data['term_resolved'] = validated_data['term_raw'] - if ('definition_raw' in validated_data): - validated_data['definition_resolved'] = validated_data['definition_raw'] - - result = super().update(instance, validated_data) - instance.schema.save() - return result - - -class StandaloneCstSerializer(serializers.ModelSerializer): - id = serializers.IntegerField() - - class Meta: - model = Constituenta - exclude = ('schema', ) - - def validate(self, attrs): - try: - attrs['object'] = Constituenta.objects.get(pk=attrs['id']) - except Constituenta.DoesNotExist: - raise serializers.ValidationError({f"{attrs['id']}": 'Конституента не существует'}) - return attrs - - -class CstCreateSerializer(serializers.ModelSerializer): - insert_after = serializers.IntegerField(required=False, allow_null=True) - - class Meta: - model = Constituenta - fields = 'alias', 'cst_type', 'convention', 'term_raw', 'definition_raw', 'definition_formal', 'insert_after' - - def validate(self, attrs): - if ('term_raw' in attrs): - attrs['term_resolved'] = attrs['term_raw'] - if ('definition_raw' in attrs): - attrs['definition_resolved'] = attrs['definition_raw'] - return attrs - - -class CstListSerlializer(serializers.Serializer): - items = serializers.ListField( - child=StandaloneCstSerializer() - ) - - def validate(self, attrs): - schema = self.context['schema'] - cstList = [] - for item in attrs['items']: - cst = item['object'] - if (cst.schema != schema): - raise serializers.ValidationError( - {'items': f'Конституенты должны относиться к данной схеме: {item}'}) - cstList.append(cst) - attrs['constituents'] = cstList - return attrs - - -class CstMoveSerlializer(CstListSerlializer): - move_to = serializers.IntegerField() - - class RSFormDetailsSerlializer(serializers.BaseSerializer): + ''' Serializer: Processed data for RSForm. ''' class Meta: + ''' serializer metadata. ''' model = RSForm def to_representation(self, instance: RSForm): @@ -120,3 +79,98 @@ class RSFormDetailsSerlializer(serializers.BaseSerializer): result['is_common'] = instance.is_common result['owner'] = (instance.owner.pk if instance.owner is not None else None) return result + + def create(self, validated_data): + raise NotImplementedError('unexpected `create()` call') + + def update(self, instance, validated_data): + raise NotImplementedError('unexpected `update()` call') + + +class ConstituentaSerializer(serializers.ModelSerializer): + ''' Serializer: Constituenta data. ''' + class Meta: + ''' serializer metadata. ''' + model = Constituenta + fields = '__all__' + read_only_fields = ('id', 'order', 'alias', 'cst_type') + + def update(self, instance: Constituenta, validated_data) -> Constituenta: + if 'term_raw' in validated_data: + validated_data['term_resolved'] = validated_data['term_raw'] + if 'definition_raw' in validated_data: + validated_data['definition_resolved'] = validated_data['definition_raw'] + + result = super().update(instance, validated_data) + instance.schema.save() + return result + + +class StandaloneCstSerializer(serializers.ModelSerializer): + ''' Serializer: Constituenta in current context. ''' + id = serializers.IntegerField() + + class Meta: + ''' serializer metadata. ''' + model = Constituenta + exclude = ('schema', ) + + def validate(self, attrs): + try: + attrs['object'] = Constituenta.objects.get(pk=attrs['id']) + except Constituenta.DoesNotExist as exception: + raise serializers.ValidationError({f"{attrs['id']}": 'Конституента не существует'}) from exception + return attrs + + +class CstCreateSerializer(serializers.ModelSerializer): + ''' Serializer: Constituenta creation. ''' + insert_after = serializers.IntegerField(required=False, allow_null=True) + + class Meta: + ''' serializer metadata. ''' + model = Constituenta + fields = 'alias', 'cst_type', 'convention', 'term_raw', 'definition_raw', 'definition_formal', 'insert_after' + + def validate(self, attrs): + if 'term_raw' in attrs: + attrs['term_resolved'] = attrs['term_raw'] + if 'definition_raw' in attrs: + attrs['definition_resolved'] = attrs['definition_raw'] + return attrs + + +class CstListSerlializer(serializers.Serializer): + ''' Serializer: List of constituents from one origin. ''' + items = serializers.ListField( + child=StandaloneCstSerializer() + ) + + def validate(self, attrs): + schema = self.context['schema'] + cstList = [] + for item in attrs['items']: + cst = item['object'] + if cst.schema != schema: + raise serializers.ValidationError( + {'items': f'Конституенты должны относиться к данной схеме: {item}'}) + cstList.append(cst) + attrs['constituents'] = cstList + return attrs + + def create(self, validated_data): + raise NotImplementedError('unexpected `create()` call') + + def update(self, instance, validated_data): + raise NotImplementedError('unexpected `update()` call') + + +class CstMoveSerlializer(CstListSerlializer): + ''' Serializer: Change constituenta position. ''' + move_to = serializers.IntegerField() + + def create(self, validated_data): + raise NotImplementedError('unexpected `create()` call') + + def update(self, instance, validated_data): + raise NotImplementedError('unexpected `update()` call') diff --git a/rsconcept/backend/apps/rsform/tests/__init__.py b/rsconcept/backend/apps/rsform/tests/__init__.py index a8afc3a5..c14522a1 100644 --- a/rsconcept/backend/apps/rsform/tests/__init__.py +++ b/rsconcept/backend/apps/rsform/tests/__init__.py @@ -1,3 +1,4 @@ +''' Tests. ''' # flake8: noqa from .t_imports import * from .t_views import * diff --git a/rsconcept/backend/apps/rsform/urls.py b/rsconcept/backend/apps/rsform/urls.py index e52ca68e..28264267 100644 --- a/rsconcept/backend/apps/rsform/urls.py +++ b/rsconcept/backend/apps/rsform/urls.py @@ -1,4 +1,4 @@ -''' Routing for rsform api ''' +''' Routing: RSForms for conceptual schemas. ''' from django.urls import path, include from rest_framework import routers from . import views diff --git a/rsconcept/backend/apps/rsform/utils.py b/rsconcept/backend/apps/rsform/utils.py index 94653f2d..b987923f 100644 --- a/rsconcept/backend/apps/rsform/utils.py +++ b/rsconcept/backend/apps/rsform/utils.py @@ -19,7 +19,6 @@ class SchemaOwnerOrAdmin(BasePermission): def read_trs(file) -> dict: ''' Read JSON from TRS file ''' - # TODO: deal with different versions with ZipFile(file, 'r') as archive: json_data = archive.read('document.json') return json.loads(json_data) diff --git a/rsconcept/backend/apps/rsform/views.py b/rsconcept/backend/apps/rsform/views.py index 4c8fd019..97255456 100644 --- a/rsconcept/backend/apps/rsform/views.py +++ b/rsconcept/backend/apps/rsform/views.py @@ -1,3 +1,4 @@ +''' REST API: RSForms for conceptual schemas. ''' import json from django.http import HttpResponse from django_filters.rest_framework import DjangoFilterBackend @@ -14,9 +15,7 @@ from . import utils class LibraryView(generics.ListAPIView): - ''' - Get list of rsforms available for active user. - ''' + ''' Endpoint: Get list of rsforms available for active user. ''' permission_classes = (permissions.AllowAny,) serializer_class = serializers.RSFormSerializer @@ -29,6 +28,7 @@ class LibraryView(generics.ListAPIView): class ConstituentAPIView(generics.RetrieveUpdateAPIView): + ''' Endpoint: Get / Update Constituenta. ''' queryset = models.Constituenta.objects.all() serializer_class = serializers.ConstituentaSerializer @@ -41,14 +41,16 @@ class ConstituentAPIView(generics.RetrieveUpdateAPIView): return result +# pylint: disable=too-many-ancestors class RSFormViewSet(viewsets.ModelViewSet): + ''' Endpoint: RSForm operations. ''' queryset = models.RSForm.objects.all() serializer_class = serializers.RSFormSerializer filter_backends = (DjangoFilterBackend, filters.OrderingFilter) filterset_fields = ['owner', 'is_common'] ordering_fields = ('owner', 'title', 'time_update') - ordering = ('-time_update') + ordering = '-time_update' def _get_schema(self) -> models.RSForm: return self.get_object() @@ -71,7 +73,7 @@ class RSFormViewSet(viewsets.ModelViewSet): @action(detail=True, methods=['post'], url_path='cst-create') def cst_create(self, request, pk): - ''' Create new constituenta ''' + ''' Create new constituenta. ''' schema = self._get_schema() serializer = serializers.CstCreateSerializer(data=request.data) serializer.is_valid(raise_exception=True) @@ -100,7 +102,7 @@ class RSFormViewSet(viewsets.ModelViewSet): @action(detail=True, methods=['patch'], url_path='cst-multidelete') def cst_multidelete(self, request, pk): - ''' Delete multiple constituents ''' + ''' Endpoint: Delete multiple constituents. ''' schema = self._get_schema() serializer = serializers.CstListSerlializer(data=request.data, context={'schema': schema}) serializer.is_valid(raise_exception=True) @@ -111,7 +113,7 @@ class RSFormViewSet(viewsets.ModelViewSet): @action(detail=True, methods=['patch'], url_path='cst-moveto') def cst_moveto(self, request, pk): - ''' Delete multiple constituents ''' + ''' Endpoint: Move multiple constituents. ''' schema: models.RSForm = self._get_schema() serializer = serializers.CstMoveSerlializer(data=request.data, context={'schema': schema}) serializer.is_valid(raise_exception=True) @@ -122,7 +124,7 @@ class RSFormViewSet(viewsets.ModelViewSet): @action(detail=True, methods=['patch'], url_path='reset-aliases') def reset_aliases(self, request, pk): - ''' Recreate all aliases based on order ''' + ''' Endpoint: Recreate all aliases based on order. ''' schema = self._get_schema() result = json.loads(pyconcept.reset_aliases(json.dumps(schema.to_trs()))) schema.load_trs(data=result, sync_metadata=False, skip_update=True) @@ -131,7 +133,7 @@ class RSFormViewSet(viewsets.ModelViewSet): @action(detail=True, methods=['patch'], url_path='load-trs') def load_trs(self, request, pk): - ''' Load data from file and replace current schema ''' + ''' Endpoint: Load data from file and replace current schema. ''' serializer = serializers.RSFormUploadSerializer(data=request.data) serializer.is_valid(raise_exception=True) schema = self._get_schema() @@ -143,7 +145,7 @@ class RSFormViewSet(viewsets.ModelViewSet): @action(detail=True, methods=['post'], url_path='clone') def clone(self, request, pk): - ''' Clone RSForm constituents and create new schema using new metadata ''' + ''' Endpoint: Clone RSForm constituents and create new schema using new metadata. ''' serializer = serializers.RSFormSerializer(data=request.data) serializer.is_valid(raise_exception=True) new_schema = models.RSForm.objects.create( @@ -159,6 +161,7 @@ class RSFormViewSet(viewsets.ModelViewSet): @action(detail=True, methods=['post']) def claim(self, request, pk=None): + ''' Endpoint: Claim ownership of RSForm. ''' schema = self._get_schema() if schema.owner == self.request.user: return Response(status=304) @@ -169,20 +172,20 @@ class RSFormViewSet(viewsets.ModelViewSet): @action(detail=True, methods=['get']) def contents(self, request, pk): - ''' View schema db contents (including constituents) ''' + ''' Endpoint: View schema db contents (including constituents). ''' schema = serializers.RSFormContentsSerializer(self._get_schema()).data return Response(schema) @action(detail=True, methods=['get']) def details(self, request, pk): - ''' Detailed schema view including statuses ''' + ''' Endpoint: Detailed schema view including statuses. ''' schema = self._get_schema() serializer = serializers.RSFormDetailsSerlializer(schema) return Response(serializer.data) @action(detail=True, methods=['post']) def check(self, request, pk): - ''' Check RS expression against schema context ''' + ''' Endpoint: Check RSLang expression against schema context. ''' schema = self._get_schema().to_trs() serializer = serializers.ExpressionSerializer(data=request.data) serializer.is_valid(raise_exception=True) @@ -192,7 +195,7 @@ class RSFormViewSet(viewsets.ModelViewSet): @action(detail=True, methods=['get'], url_path='export-trs') def export_trs(self, request, pk): - ''' Download Exteor compatible file ''' + ''' Endpoint: Download Exteor compatible file. ''' schema = self._get_schema().to_trs() trs = utils.write_trs(schema) filename = self._get_schema().alias @@ -208,10 +211,10 @@ class RSFormViewSet(viewsets.ModelViewSet): class TrsImportView(views.APIView): - ''' Upload RS form in Exteor format ''' + ''' Endpoint: Upload RS form in Exteor format. ''' serializer_class = serializers.FileSerializer - def post(self, request, format=None): + def post(self, request): data = utils.read_trs(request.FILES['file'].file) owner = self.request.user if owner.is_anonymous: @@ -223,11 +226,11 @@ class TrsImportView(views.APIView): @api_view(['POST']) def create_rsform(request): - ''' Create RSForm from user input and/or trs file ''' + ''' Endpoint: Create RSForm from user input and/or trs file. ''' owner = request.user if owner.is_anonymous: owner = None - if ('file' not in request.FILES): + if 'file' not in request.FILES: serializer = serializers.RSFormSerializer(data=request.data) serializer.is_valid(raise_exception=True) schema = models.RSForm.objects.create( @@ -239,16 +242,16 @@ def create_rsform(request): ) else: data = utils.read_trs(request.FILES['file'].file) - if ('title' in request.data and request.data['title'] != ''): + if 'title' in request.data and request.data['title'] != '': data['title'] = request.data['title'] if data['title'] == '': data['title'] = 'Без названия ' + request.FILES['file'].fileName - if ('alias' in request.data and request.data['alias'] != ''): + if 'alias' in request.data and request.data['alias'] != '': data['alias'] = request.data['alias'] - if ('comment' in request.data and request.data['comment'] != ''): + if 'comment' in request.data and request.data['comment'] != '': data['comment'] = request.data['comment'] is_common = True - if ('is_common' in request.data): + if 'is_common' in request.data: is_common = request.data['is_common'] == 'true' schema = models.RSForm.create_from_trs(owner, data, is_common) result = serializers.RSFormSerializer(schema) @@ -257,7 +260,7 @@ def create_rsform(request): @api_view(['POST']) def parse_expression(request): - '''Parse RS expression ''' + ''' Endpoint: Parse RS expression. ''' serializer = serializers.ExpressionSerializer(data=request.data) serializer.is_valid(raise_exception=True) expression = serializer.validated_data['expression'] @@ -267,7 +270,7 @@ def parse_expression(request): @api_view(['POST']) def convert_to_ascii(request): - ''' Convert to ASCII syntax ''' + ''' Endpoint: Convert expression to ASCII syntax. ''' serializer = serializers.ExpressionSerializer(data=request.data) serializer.is_valid(raise_exception=True) expression = serializer.validated_data['expression'] @@ -277,7 +280,7 @@ def convert_to_ascii(request): @api_view(['POST']) def convert_to_math(request): - '''Convert to MATH syntax ''' + ''' Endpoint: Convert expression to MATH syntax. ''' serializer = serializers.ExpressionSerializer(data=request.data) serializer.is_valid(raise_exception=True) expression = serializer.validated_data['expression'] diff --git a/rsconcept/backend/apps/users/admin.py b/rsconcept/backend/apps/users/admin.py index 8c38f3f3..4793aa56 100644 --- a/rsconcept/backend/apps/users/admin.py +++ b/rsconcept/backend/apps/users/admin.py @@ -1,3 +1 @@ -from django.contrib import admin - -# Register your models here. +''' Admin: User profile and Authentification. ''' diff --git a/rsconcept/backend/apps/users/apps.py b/rsconcept/backend/apps/users/apps.py index 2bb189ca..41e69e08 100644 --- a/rsconcept/backend/apps/users/apps.py +++ b/rsconcept/backend/apps/users/apps.py @@ -1,6 +1,8 @@ +''' Application: User profile and Authentification. ''' from django.apps import AppConfig class UsersConfig(AppConfig): + ''' Application config. ''' default_auto_field = 'django.db.models.BigAutoField' name = 'apps.users' diff --git a/rsconcept/backend/apps/users/models.py b/rsconcept/backend/apps/users/models.py index dbdcd8cb..8bf01a73 100644 --- a/rsconcept/backend/apps/users/models.py +++ b/rsconcept/backend/apps/users/models.py @@ -1 +1,5 @@ +''' Models: User profile and Authentification. ''' + +# Note: using User import to isolate original +# pylint: disable=unused-import,ungrouped-imports from django.contrib.auth.models import User diff --git a/rsconcept/backend/apps/users/serializers.py b/rsconcept/backend/apps/users/serializers.py index 6b76243a..30ad8170 100644 --- a/rsconcept/backend/apps/users/serializers.py +++ b/rsconcept/backend/apps/users/serializers.py @@ -1,3 +1,4 @@ +''' Serializers: User profile and Authentification. ''' from django.contrib.auth import authenticate from django.contrib.auth.password_validation import validate_password from rest_framework import serializers @@ -6,7 +7,7 @@ from . import models class LoginSerializer(serializers.Serializer): - ''' User authentification by login/password. ''' + ''' Serializer: User authentification by login/password. ''' username = serializers.CharField( label='Имя пользователя', write_only=True @@ -32,10 +33,17 @@ class LoginSerializer(serializers.Serializer): attrs['user'] = user return attrs + def create(self, validated_data): + raise NotImplementedError('unexpected `create()` call') + + def update(self, instance, validated_data): + raise NotImplementedError('unexpected `update()` call') + class AuthSerializer(serializers.ModelSerializer): - ''' Authentication data serializaer ''' + ''' Serializer: Authentication data. ''' class Meta: + ''' serializer metadata. ''' model = models.User fields = [ 'id', @@ -45,8 +53,9 @@ class AuthSerializer(serializers.ModelSerializer): class UserInfoSerializer(serializers.ModelSerializer): - ''' User data serializaer ''' + ''' Serializer: User data. ''' class Meta: + ''' serializer metadata. ''' model = models.User fields = [ 'id', @@ -57,10 +66,11 @@ class UserInfoSerializer(serializers.ModelSerializer): class UserSerializer(serializers.ModelSerializer): - ''' User data serializaer ''' + ''' Serializer: User data. ''' id = serializers.IntegerField(read_only=True) class Meta: + ''' serializer metadata. ''' model = models.User fields = [ 'id', @@ -70,25 +80,27 @@ class UserSerializer(serializers.ModelSerializer): 'last_name', ] + class ChangePasswordSerializer(serializers.Serializer): - """ - Serializer for password change endpoint. - """ + ''' Serializer: Change password. ''' old_password = serializers.CharField(required=True) new_password = serializers.CharField(required=True) - # def validate(self, attrs): - # if attrs['new_password'] != "123": - # raise serializers.ValidationError({"password": r"Пароль не '123'"}) - # return attrs + def create(self, validated_data): + raise NotImplementedError('unexpected `create()` call') + + def update(self, instance, validated_data): + raise NotImplementedError('unexpected `update()` call') + class SignupSerializer(serializers.ModelSerializer): - ''' User profile create ''' + ''' Serializer: Create user profile. ''' id = serializers.IntegerField(read_only=True) password = serializers.CharField(write_only=True, required=True, validators=[validate_password]) password2 = serializers.CharField(write_only=True, required=True) class Meta: + ''' serializer metadata. ''' model = models.User fields = [ 'id', diff --git a/rsconcept/backend/apps/users/tests/__init__.py b/rsconcept/backend/apps/users/tests/__init__.py index 47eaac6c..c52ff2c0 100644 --- a/rsconcept/backend/apps/users/tests/__init__.py +++ b/rsconcept/backend/apps/users/tests/__init__.py @@ -1,3 +1,4 @@ +''' Tests. ''' # flake8: noqa from .t_views import * from .t_serializers import * diff --git a/rsconcept/backend/apps/users/urls.py b/rsconcept/backend/apps/users/urls.py index 403f3932..9d9892ac 100644 --- a/rsconcept/backend/apps/users/urls.py +++ b/rsconcept/backend/apps/users/urls.py @@ -1,4 +1,4 @@ -''' Routing for users management ''' +''' Routing: User profile and Authentification. ''' from django.urls import path from . import views diff --git a/rsconcept/backend/apps/users/views.py b/rsconcept/backend/apps/users/views.py index 7c09a51d..56055cb8 100644 --- a/rsconcept/backend/apps/users/views.py +++ b/rsconcept/backend/apps/users/views.py @@ -1,3 +1,4 @@ +''' REST API: User profile and Authentification. ''' from django.contrib.auth import login, logout from rest_framework import status, permissions, views, generics @@ -6,15 +7,13 @@ from rest_framework.response import Response from . import serializers from . import models -from django.contrib.auth.models import User - class LoginAPIView(views.APIView): ''' - Login user via username + password. + Endpoint: Login user via username + password. ''' permission_classes = (permissions.AllowAny,) - def post(self, request, format=None): + def post(self, request): serializer = serializers.LoginSerializer( data=self.request.data, context={'request': self.request} @@ -27,11 +26,11 @@ class LoginAPIView(views.APIView): class LogoutAPIView(views.APIView): ''' - Logout current user. + Endpoint: Logout current user. ''' permission_classes = (permissions.IsAuthenticated,) - def post(self, request, format=None): + def post(self, request): logout(request) return Response(None, status=status.HTTP_204_NO_CONTENT) @@ -57,7 +56,7 @@ class AuthAPIView(generics.RetrieveAPIView): class ActiveUsersView(generics.ListAPIView): ''' - Get list of active user. + Endpoint: Get list of active users. ''' permission_classes = (permissions.AllowAny,) serializer_class = serializers.UserSerializer @@ -68,20 +67,19 @@ class ActiveUsersView(generics.ListAPIView): class UserProfileAPIView(generics.RetrieveUpdateAPIView): ''' - User profile info. + Endpoint: User profile info. ''' permission_classes = (permissions.IsAuthenticated,) serializer_class = serializers.UserSerializer def get_object(self): return self.request.user - + + class UpdatePassword(views.APIView): - """ - An endpoint for changing password. - """ - # {"username": "admin", "password": "1234"} - # {"old_password": "1234", "new_password": "1234"} + ''' + Endpoint: Change password for current user. + ''' permission_classes = (permissions.IsAuthenticated, ) def get_object(self, queryset=None): @@ -89,16 +87,13 @@ class UpdatePassword(views.APIView): def patch(self, request, *args, **kwargs): self.object = self.get_object() - serializer = serializers.ChangePasswordSerializer(data=request.data) - if serializer.is_valid(): - # Check old password old_password = serializer.data.get("old_password") if not self.object.check_password(old_password): - return Response({"old_password": ["Wrong password."]}, + return Response({"old_password": ["Wrong password."]}, status=status.HTTP_400_BAD_REQUEST) - # set_password also hashes the password that the user will get + # Note: set_password also hashes the password that the user will get self.object.set_password(serializer.data.get("new_password")) self.object.save() return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/rsconcept/backend/cctext/__init__.py b/rsconcept/backend/cctext/__init__.py index 69504647..dc23dee3 100644 --- a/rsconcept/backend/cctext/__init__.py +++ b/rsconcept/backend/cctext/__init__.py @@ -1,7 +1,7 @@ -'''Concept core text processing library''' +''' Concept core text processing library. ''' from .syntax import RuSyntax, Capitalization -from .rumodel import Morphology, SemanticRole, NamedEntityRole, WordTag, morpho -from .ruparser import RuParser, WordToken, Collation +from .rumodel import Morphology, SemanticRole, WordTag, morpho +from .ruparser import PhraseParser, WordToken, Collation from .conceptapi import ( parse, normalize, @@ -9,3 +9,5 @@ from .conceptapi import ( match_all_morpho, find_substr, split_tags ) + +# TODO: implement Part of speech transition for VERB <-> NOUN diff --git a/rsconcept/backend/cctext/conceptapi.py b/rsconcept/backend/cctext/conceptapi.py index eb2e6a29..e28271a9 100644 --- a/rsconcept/backend/cctext/conceptapi.py +++ b/rsconcept/backend/cctext/conceptapi.py @@ -5,9 +5,9 @@ Concept API Python functions. ''' from cctext.rumodel import Morphology from .syntax import RuSyntax -from .ruparser import RuParser +from .ruparser import PhraseParser -parser = RuParser() +parser = PhraseParser() def split_tags(tags: str) -> frozenset[str]: diff --git a/rsconcept/backend/cctext/rumodel.py b/rsconcept/backend/cctext/rumodel.py index 9580351b..cd8cae38 100644 --- a/rsconcept/backend/cctext/rumodel.py +++ b/rsconcept/backend/cctext/rumodel.py @@ -1,6 +1,6 @@ ''' Russian language models. ''' from __future__ import annotations -from enum import Enum, IntEnum, unique +from enum import Enum, unique from pymorphy2 import MorphAnalyzer from pymorphy2.tagset import OpencorporaTag as WordTag @@ -9,36 +9,6 @@ from pymorphy2.tagset import OpencorporaTag as WordTag morpho = MorphAnalyzer() -@unique -class NamedEntityRole(IntEnum): - ''' Enumerating NER types. ''' - unknwn = 0 - loc = 1 - per = 2 - org = 3 - - @staticmethod - def from_str(text: str) -> NamedEntityRole: - ''' From text to ID. ''' - if text == 'LOC': - return NamedEntityRole.loc - elif text == 'PER': - return NamedEntityRole.per - elif text == 'ORG': - return NamedEntityRole.org - return NamedEntityRole.unknwn - - def as_str(self) -> str: - ''' From ID to text. ''' - if self.value == NamedEntityRole.loc: - return 'LOC' - elif self.value == NamedEntityRole.per: - return 'PER' - elif self.value == NamedEntityRole.org: - return 'ORG' - return 'UNKN' - - @unique class SemanticRole(Enum): ''' Enumerating semantic types for different parse patterns. ''' diff --git a/rsconcept/backend/cctext/ruparser.py b/rsconcept/backend/cctext/ruparser.py index 53332047..f9603989 100644 --- a/rsconcept/backend/cctext/ruparser.py +++ b/rsconcept/backend/cctext/ruparser.py @@ -109,7 +109,6 @@ class Collation: current_word += self.coordination[current_word] def _inflect_main_word(self, origin: Morphology, target_tags: frozenset[str]) -> Morphology: - # TODO: implement Part of speech transition for VERB <-> NOUN full_tags = origin.complete_tags(target_tags) inflected = self.words[self.main_word].inflect(full_tags) if not inflected: @@ -141,7 +140,7 @@ class Collation: return result -class RuParser: +class PhraseParser: ''' Russian grammar parser. ''' def __init__(self): pass @@ -205,14 +204,14 @@ class RuParser: model_after = self.parse(cntxt_after) model_before = self.parse(cntxt_before) - etalon = RuParser._choose_context_etalon(target_morpho, model_before, model_after) + etalon = PhraseParser._choose_context_etalon(target_morpho, model_before, model_after) if not etalon: return text etalon_moprho = etalon.get_morpho() if not etalon_moprho.can_coordinate: return text - new_form = RuParser._combine_morpho(target_morpho, etalon_moprho.tag) + new_form = PhraseParser._combine_morpho(target_morpho, etalon_moprho.tag) return target.inflect(new_form) def inflect_substitute(self, substitute_normal: str, original: str) -> str: @@ -274,7 +273,7 @@ class RuParser: main_wait = 0 word_index = 0 for segment in segments: - if main_wait > RuParser._MAIN_WAIT_LIMIT: + if main_wait > PhraseParser._MAIN_WAIT_LIMIT: break segment_index += 1 priority = self._parse_segment(result, segment, require_index, require_tags) @@ -309,7 +308,7 @@ class RuParser: if require_index != INDEX_NONE: form = forms[require_index] if not require_tags or form.tag.grammemes.issuperset(require_tags): - (local_max, segment_score) = RuParser._get_priority_for(form.tag) + (local_max, segment_score) = PhraseParser._get_priority_for(form.tag) main_index = require_index needs_coordination = Morphology.is_dependable(form.tag.POS) else: @@ -317,7 +316,7 @@ class RuParser: for (index, form) in enumerate(forms): if require_tags and not form.tag.grammemes.issuperset(require_tags): continue - (local_priority, global_priority) = RuParser._get_priority_for(form.tag) + (local_priority, global_priority) = PhraseParser._get_priority_for(form.tag) needs_coordination = needs_coordination or Morphology.is_dependable(form.tag.POS) local_sum += global_priority * form.score score_sum += form.score @@ -347,7 +346,7 @@ class RuParser: if index != target.main_word: word.main = INDEX_NONE else: - word.main = RuParser._find_coordination(word.forms, main_morpho.tag, index < target.main_word) + word.main = PhraseParser._find_coordination(word.forms, main_morpho.tag, index < target.main_word) needs_change = word.main != INDEX_NONE if not needs_change or not main_coordinate: target.coordination[index] = NO_COORDINATION @@ -392,14 +391,14 @@ class RuParser: @staticmethod def _filtered_parse(text: str): capital = Capitalization.from_text(text) - score_filter = RuParser._filter_score(morpho.parse(text)) - for form in RuParser._filter_capital(score_filter, capital): + score_filter = PhraseParser._filter_score(morpho.parse(text)) + for form in PhraseParser._filter_capital(score_filter, capital): yield form @staticmethod def _filter_score(generator): for form in generator: - if form.score < RuParser._FILTER_SCORE: + if form.score < PhraseParser._FILTER_SCORE: break yield form diff --git a/rsconcept/backend/cctext/tests/__init__.py b/rsconcept/backend/cctext/tests/__init__.py index e69de29b..beaff6d5 100644 --- a/rsconcept/backend/cctext/tests/__init__.py +++ b/rsconcept/backend/cctext/tests/__init__.py @@ -0,0 +1 @@ +''' Tests. ''' diff --git a/rsconcept/backend/cctext/tests/testRuParser.py b/rsconcept/backend/cctext/tests/testRuParser.py index b7f9c042..2df9a7d0 100644 --- a/rsconcept/backend/cctext/tests/testRuParser.py +++ b/rsconcept/backend/cctext/tests/testRuParser.py @@ -1,9 +1,9 @@ ''' Test russian language parsing. ''' import unittest -from cctext import RuParser +from cctext import PhraseParser -parser = RuParser() +parser = PhraseParser() class TestRuParser(unittest.TestCase): diff --git a/rsconcept/backend/requirements.txt b/rsconcept/backend/requirements.txt index 387e349b..ee52da28 100644 --- a/rsconcept/backend/requirements.txt +++ b/rsconcept/backend/requirements.txt @@ -3,10 +3,11 @@ django djangorestframework django-cors-headers django-filter -psycopg2-binary -gunicorn coreapi pymorphy2 pymorphy2-dicts-ru pymorphy2-dicts-uk -razdel \ No newline at end of file +razdel + +psycopg2-binary +gunicorn \ No newline at end of file diff --git a/rsconcept/backend/requirements_dev.txt b/rsconcept/backend/requirements_dev.txt index a7619849..3d2c5107 100644 --- a/rsconcept/backend/requirements_dev.txt +++ b/rsconcept/backend/requirements_dev.txt @@ -3,9 +3,11 @@ django djangorestframework django-cors-headers django-filter -coverage coreapi pymorphy2 pymorphy2-dicts-ru pymorphy2-dicts-uk -razdel \ No newline at end of file +razdel + +pylint +coverage \ No newline at end of file