From 2a1e98237e8af7c6762fb79f469322eb6183bfcd Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Mon, 9 Dec 2019 12:50:27 +0200 Subject: [PATCH 01/60] init app template v1 --- README.rst | 27 +++++++++++ app.py | 12 ----- entry.py | 11 +++++ requirements.txt | 19 ++++++++ task_office/__init__.py | 0 task_office/api/__init__.py | 3 ++ task_office/api/views.py | 15 ++++++ task_office/app.py | 68 ++++++++++++++++++++++++++ task_office/commands.py | 15 ++++++ task_office/exceptions.py | 47 ++++++++++++++++++ task_office/extensions.py | 44 +++++++++++++++++ task_office/settings.py | 96 +++++++++++++++++++++++++++++++++++++ 12 files changed, 345 insertions(+), 12 deletions(-) delete mode 100644 app.py create mode 100755 entry.py create mode 100644 requirements.txt create mode 100644 task_office/__init__.py create mode 100644 task_office/api/__init__.py create mode 100644 task_office/api/views.py create mode 100644 task_office/app.py create mode 100644 task_office/commands.py create mode 100644 task_office/exceptions.py create mode 100644 task_office/extensions.py create mode 100644 task_office/settings.py diff --git a/README.rst b/README.rst index 2ecae64..72d54bc 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,33 @@ =============================== Task Office =============================== +Task Office - pet app with using Flask + + +.. image:: https://img.shields.io/badge/code%20style-black-000000.svg + :target: https://github.com/ambv/black + :alt: Black code style + + +Run Task Office +^^^^^^^^^^^^^^^^^^ +Before running shell commands, set the ``FLASK_APP`` and ``FLASK_DEBUG`` +environment variables :: + + export FLASK_APP=/entry.py + export FLASK_DEBUG=1 + + +Run the following commands to create your app's +database tables and perform the initial migration :: + + flask db init + flask db migrate + flask db upgrade + +To run the web application use:: + + flask run --with-threads Features -------- diff --git a/app.py b/app.py deleted file mode 100644 index 4b059ec..0000000 --- a/app.py +++ /dev/null @@ -1,12 +0,0 @@ -from flask import Flask - -app = Flask(__name__) - - -@app.route('/') -def hello_world(): - return 'Hello World!' - - -if __name__ == '__main__': - app.run() diff --git a/entry.py b/entry.py new file mode 100755 index 0000000..2908f48 --- /dev/null +++ b/entry.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +"""Create an application instance.""" +from flask.helpers import get_debug_flag + +from task_office.app import create_app +from task_office.settings import DevConfig, ProdConfig + +# CONFIG = DevConfig if get_debug_flag() else ProdConfig +CONFIG = DevConfig + +app = create_app(CONFIG) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..803a5d1 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,19 @@ +Click==7.0 +Flask==1.1.1 +itsdangerous==1.1.0 +Jinja2==2.10.3 +MarkupSafe==1.1.1 +Werkzeug==0.16.0 +SQLAlchemy==1.1.9 +Flask_Caching +Flask_SQLAlchemy==2.2 +marshmallow +Flask_Bcrypt +flask_apispec +PyJWT +Flask-JWT-Extended +unicode_slugify +psycopg2 +Flask-Migrate +gunicorn +Flask-Cors diff --git a/task_office/__init__.py b/task_office/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/task_office/api/__init__.py b/task_office/api/__init__.py new file mode 100644 index 0000000..67ce501 --- /dev/null +++ b/task_office/api/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from .views import * diff --git a/task_office/api/views.py b/task_office/api/views.py new file mode 100644 index 0000000..48f3df1 --- /dev/null +++ b/task_office/api/views.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- + +from flask import Blueprint, render_template, current_app + +# from ..extensions import manager +# from ..models import MyModel + + +def initialize_api(): + # List all Flask-Restless APIs here + # model_api = manager.create_api(MyModel, methods=['GET']) + pass + + +api = Blueprint("api", __name__, url_prefix="/api") diff --git a/task_office/app.py b/task_office/app.py new file mode 100644 index 0000000..2a45d43 --- /dev/null +++ b/task_office/app.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +"""The app module, containing the app factory function.""" +from flask import Flask +from task_office.extensions import bcrypt, cache, db, migrate, cors + +from task_office import commands +from task_office.settings import ProdConfig +from task_office.exceptions import InvalidUsage + + +def create_app(config_object=ProdConfig): + """An application factory, as explained here: + http://flask.pocoo.org/docs/patterns/appfactories/. + + :param config_object: The configuration object to use. + """ + app = Flask(__name__.split(".")[0]) + app.url_map.strict_slashes = False + app.config.from_object(config_object) + register_extensions(app) + register_blueprints(app) + register_errorhandlers(app) + register_shellcontext(app) + register_commands(app) + return app + + +def register_extensions(app): + """Register Flask extensions.""" + bcrypt.init_app(app) + cache.init_app(app) + db.init_app(app) + migrate.init_app(app, db) + + +def register_blueprints(app): + """Register Flask blueprints.""" + origins = app.config.get("CORS_ORIGIN_WHITELIST", "*") + # cors.init_app(articles.views.blueprint, origins=origins) + + # app.register_blueprint(articles.views.blueprint) + + +def register_errorhandlers(app): + def errorhandler(error): + response = error.to_json() + response.status_code = error.status_code + return response + + app.errorhandler(InvalidUsage)(errorhandler) + + +def register_shellcontext(app): + """Register shell context objects.""" + + def shell_context(): + """Shell context objects.""" + return { + "db": db, + # 'Article': articles.models.Article, + } + + app.shell_context_processor(shell_context) + + +def register_commands(app): + """Register Click commands.""" + app.cli.add_command(commands.test) diff --git a/task_office/commands.py b/task_office/commands.py new file mode 100644 index 0000000..c7cb61e --- /dev/null +++ b/task_office/commands.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +"""Click commands.""" +import os + +import click + +HERE = os.path.abspath(os.path.dirname(__file__)) +PROJECT_ROOT = os.path.join(HERE, os.pardir) +TEST_PATH = os.path.join(PROJECT_ROOT, "tests") + + +@click.command() +def test(): + """Run the tests.""" + pass diff --git a/task_office/exceptions.py b/task_office/exceptions.py new file mode 100644 index 0000000..c89c2bd --- /dev/null +++ b/task_office/exceptions.py @@ -0,0 +1,47 @@ +from flask import jsonify + + +def template(data, code=500): + return {"message": {"errors": {"body": data}}, "status_code": code} + + +USER_NOT_FOUND = template(["User not found"], code=404) +USER_ALREADY_REGISTERED = template(["User already registered"], code=422) +UNKNOWN_ERROR = template([], code=500) +ARTICLE_NOT_FOUND = template(["Article not found"], code=404) +COMMENT_NOT_OWNED = template(["Not your article"], code=422) + + +class InvalidUsage(Exception): + status_code = 500 + + def __init__(self, message, status_code=None, payload=None): + Exception.__init__(self) + self.message = message + if status_code is not None: + self.status_code = status_code + self.payload = payload + + def to_json(self): + rv = self.message + return jsonify(rv) + + @classmethod + def user_not_found(cls): + return cls(**USER_NOT_FOUND) + + @classmethod + def user_already_registered(cls): + return cls(**USER_ALREADY_REGISTERED) + + @classmethod + def unknown_error(cls): + return cls(**UNKNOWN_ERROR) + + @classmethod + def article_not_found(cls): + return cls(**ARTICLE_NOT_FOUND) + + @classmethod + def comment_not_owned(cls): + return cls(**COMMENT_NOT_OWNED) diff --git a/task_office/extensions.py b/task_office/extensions.py new file mode 100644 index 0000000..8287672 --- /dev/null +++ b/task_office/extensions.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +"""Extensions module. Each extension is initialized in the app factory located in app.py.""" + +from flask_bcrypt import Bcrypt +from flask_caching import Cache +from flask_cors import CORS + +from flask_migrate import Migrate +from flask_sqlalchemy import SQLAlchemy, Model + + +class CRUDMixin(Model): + """Mixin that adds convenience methods for CRUD (create, read, update, delete) operations.""" + + @classmethod + def create(cls, **kwargs): + """Create a new record and save it the database.""" + instance = cls(**kwargs) + return instance.save() + + def update(self, commit=True, **kwargs): + """Update specific fields of a record.""" + for attr, value in kwargs.items(): + setattr(self, attr, value) + return commit and self.save() or self + + def save(self, commit=True): + """Save the record.""" + db.session.add(self) + if commit: + db.session.commit() + return self + + def delete(self, commit=True): + """Remove the record from the database.""" + db.session.delete(self) + return commit and db.session.commit() + + +bcrypt = Bcrypt() +db = SQLAlchemy(model_class=CRUDMixin) +migrate = Migrate() +cache = Cache() +cors = CORS() diff --git a/task_office/settings.py b/task_office/settings.py new file mode 100644 index 0000000..bbb1f19 --- /dev/null +++ b/task_office/settings.py @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- +"""Application configuration.""" +import os +from datetime import timedelta + + +class Config(object): + """Base configuration.""" + + SECRET_KEY = os.environ.get("CONDUIT_SECRET", "secret-key") # TODO: Change me + APP_DIR = os.path.abspath(os.path.dirname(__file__)) # This directory + PROJECT_ROOT = os.path.abspath(os.path.join(APP_DIR, os.pardir)) + BCRYPT_LOG_ROUNDS = 13 + DEBUG_TB_INTERCEPT_REDIRECTS = False + CACHE_TYPE = "simple" # Can be "memcached", "redis", etc. + SQLALCHEMY_TRACK_MODIFICATIONS = False + JWT_AUTH_USERNAME_KEY = "email" + JWT_AUTH_HEADER_PREFIX = "Token" + CORS_ORIGIN_WHITELIST = [ + "http://0.0.0.0:4100", + "http://localhost:4100", + "http://0.0.0.0:8000", + "http://localhost:8000", + "http://0.0.0.0:4200", + "http://localhost:4200", + "http://0.0.0.0:4000", + "http://localhost:4000", + ] + JWT_HEADER_TYPE = "Token" + + +class ProdConfig(Config): + """Production configuration.""" + + ENV = "PROD" + DEBUG = False + DATABASE = { + "DB_NAME": os.environ.get("POSTGRES_DB", "task_office"), + "DB_USER": os.environ.get("POSTGRES_USER", "task_office_user"), + "DB_PASSWORD": os.environ.get("POSTGRES_PASSWORD", "task_office_user"), + "DB_HOST": os.environ.get("POSTGRES_HOST", "127.0.0.1"), + "DB_PORT": os.environ.get("POSTGRES_PORT", "5432"), + } + SQLALCHEMY_DATABASE_URI = "psql://{username}:{password}@{host}:{db_port}/{db_name}".format( + username=DATABASE["DB_USER"], + password=DATABASE["DB_PASSWORD"], + host=DATABASE["DB_HOST"], + db_port=DATABASE["DB_PORT"], + db_name=DATABASE["DB_NAME"], + ) + + +class DevConfig(Config): + """Development configuration.""" + + ENV = "DEV" + DEBUG = True + # Put the db file in project root + DATABASE = { + "DB_NAME": os.environ.get("POSTGRES_DB", "task_office_dev"), + "DB_USER": os.environ.get("POSTGRES_USER", "task_office_user"), + "DB_PASSWORD": os.environ.get("POSTGRES_PASSWORD", "task_office_user"), + "DB_HOST": os.environ.get("POSTGRES_HOST", "127.0.0.1"), + "DB_PORT": os.environ.get("POSTGRES_PORT", "5432"), + } + SQLALCHEMY_DATABASE_URI = "psql://{username}:{password}@{host}:{db_port}/{db_name}".format( + username=DATABASE["DB_USER"], + password=DATABASE["DB_PASSWORD"], + host=DATABASE["DB_HOST"], + db_port=DATABASE["DB_PORT"], + db_name=DATABASE["DB_NAME"], + ) + CACHE_TYPE = "simple" # Can be "memcached", "redis", etc. + JWT_ACCESS_TOKEN_EXPIRES = timedelta(10 ** 6) + + +class TestConfig(Config): + """Test configuration.""" + + TESTING = True + DEBUG = True + DATABASE = { + "DB_NAME": os.environ.get("POSTGRES_DB", "task_office_test"), + "DB_USER": os.environ.get("POSTGRES_USER", "task_office_user"), + "DB_PASSWORD": os.environ.get("POSTGRES_PASSWORD", "task_office_user"), + "DB_HOST": os.environ.get("POSTGRES_HOST", "127.0.0.1"), + "DB_PORT": os.environ.get("POSTGRES_PORT", "5432"), + } + SQLALCHEMY_DATABASE_URI = "psql://{username}:{password}@{host}:{db_port}/{db_name}".format( + username=DATABASE["DB_USER"], + password=DATABASE["DB_PASSWORD"], + host=DATABASE["DB_HOST"], + db_port=DATABASE["DB_PORT"], + db_name=DATABASE["DB_NAME"], + ) + BCRYPT_LOG_ROUNDS = 4 From 9958b4acbf149d2ed20d01f231fd4c07f2a0f740 Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Mon, 16 Dec 2019 16:54:59 +0200 Subject: [PATCH 02/60] init app template v2, init auth app --- entry.py | 5 +- migrations/README | 1 + migrations/alembic.ini | 45 +++++++++++++ migrations/env.py | 96 +++++++++++++++++++++++++++ migrations/script.py.mako | 24 +++++++ migrations/versions/93f7475719c6_.py | 43 ++++++++++++ requirements.txt | 1 + task_office/api/views.py | 15 ----- task_office/app.py | 6 +- task_office/{api => auth}/__init__.py | 0 task_office/auth/models.py | 37 +++++++++++ task_office/auth/serializers.py | 38 +++++++++++ task_office/auth/views.py | 40 +++++++++++ task_office/compat.py | 19 ++++++ task_office/database.py | 68 +++++++++++++++++++ task_office/settings.py | 20 ++++-- 16 files changed, 431 insertions(+), 27 deletions(-) create mode 100644 migrations/README create mode 100644 migrations/alembic.ini create mode 100644 migrations/env.py create mode 100644 migrations/script.py.mako create mode 100644 migrations/versions/93f7475719c6_.py delete mode 100644 task_office/api/views.py rename task_office/{api => auth}/__init__.py (100%) create mode 100644 task_office/auth/models.py create mode 100644 task_office/auth/serializers.py create mode 100644 task_office/auth/views.py create mode 100644 task_office/compat.py create mode 100644 task_office/database.py diff --git a/entry.py b/entry.py index 2908f48..28ea042 100755 --- a/entry.py +++ b/entry.py @@ -3,9 +3,6 @@ from flask.helpers import get_debug_flag from task_office.app import create_app -from task_office.settings import DevConfig, ProdConfig - -# CONFIG = DevConfig if get_debug_flag() else ProdConfig -CONFIG = DevConfig +from task_office.settings import CONFIG app = create_app(CONFIG) diff --git a/migrations/README b/migrations/README new file mode 100644 index 0000000..98e4f9c --- /dev/null +++ b/migrations/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/migrations/alembic.ini b/migrations/alembic.ini new file mode 100644 index 0000000..f8ed480 --- /dev/null +++ b/migrations/alembic.ini @@ -0,0 +1,45 @@ +# A generic, single database configuration. + +[alembic] +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/migrations/env.py b/migrations/env.py new file mode 100644 index 0000000..d766af8 --- /dev/null +++ b/migrations/env.py @@ -0,0 +1,96 @@ +from __future__ import with_statement + +import logging +from logging.config import fileConfig + +from sqlalchemy import engine_from_config +from sqlalchemy import pool + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +logger = logging.getLogger("alembic.env") + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +from flask import current_app + +config.set_main_option( + "sqlalchemy.url", + current_app.config.get("SQLALCHEMY_DATABASE_URI").replace("%", "%%"), +) +target_metadata = current_app.extensions["migrate"].db.metadata + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure(url=url, target_metadata=target_metadata, literal_binds=True) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # this callback is used to prevent an auto-migration from being generated + # when there are no changes to the schema + # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html + def process_revision_directives(context, revision, directives): + if getattr(config.cmd_opts, "autogenerate", False): + script = directives[0] + if script.upgrade_ops.is_empty(): + directives[:] = [] + logger.info("No changes in schema detected.") + + connectable = engine_from_config( + config.get_section(config.config_ini_section), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=target_metadata, + process_revision_directives=process_revision_directives, + **current_app.extensions["migrate"].configure_args + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/migrations/script.py.mako b/migrations/script.py.mako new file mode 100644 index 0000000..2c01563 --- /dev/null +++ b/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/migrations/versions/93f7475719c6_.py b/migrations/versions/93f7475719c6_.py new file mode 100644 index 0000000..d037119 --- /dev/null +++ b/migrations/versions/93f7475719c6_.py @@ -0,0 +1,43 @@ +"""empty message + +Revision ID: 93f7475719c6 +Revises: +Create Date: 2019-12-16 16:03:52.702698 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = "93f7475719c6" +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "users", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("uuid", postgresql.UUID(), nullable=False), + sa.Column("meta", sa.JSON(), nullable=True), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.Column("updated_at", sa.DateTime(), nullable=False), + sa.Column("username", sa.String(length=80), nullable=False), + sa.Column("email", sa.String(length=100), nullable=False), + sa.Column("password", sa.Binary(), nullable=True), + sa.Column("bio", sa.String(length=300), nullable=True), + sa.Column("image", sa.String(length=120), nullable=True), + sa.PrimaryKeyConstraint("id", "uuid"), + sa.UniqueConstraint("email"), + sa.UniqueConstraint("username"), + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("users") + # ### end Alembic commands ### diff --git a/requirements.txt b/requirements.txt index 803a5d1..f8c97d7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,3 +17,4 @@ psycopg2 Flask-Migrate gunicorn Flask-Cors +uuid diff --git a/task_office/api/views.py b/task_office/api/views.py deleted file mode 100644 index 48f3df1..0000000 --- a/task_office/api/views.py +++ /dev/null @@ -1,15 +0,0 @@ -# -*- coding: utf-8 -*- - -from flask import Blueprint, render_template, current_app - -# from ..extensions import manager -# from ..models import MyModel - - -def initialize_api(): - # List all Flask-Restless APIs here - # model_api = manager.create_api(MyModel, methods=['GET']) - pass - - -api = Blueprint("api", __name__, url_prefix="/api") diff --git a/task_office/app.py b/task_office/app.py index 2a45d43..196012b 100644 --- a/task_office/app.py +++ b/task_office/app.py @@ -3,7 +3,7 @@ from flask import Flask from task_office.extensions import bcrypt, cache, db, migrate, cors -from task_office import commands +from task_office import commands, auth from task_office.settings import ProdConfig from task_office.exceptions import InvalidUsage @@ -36,9 +36,9 @@ def register_extensions(app): def register_blueprints(app): """Register Flask blueprints.""" origins = app.config.get("CORS_ORIGIN_WHITELIST", "*") - # cors.init_app(articles.views.blueprint, origins=origins) + cors.init_app(auth.views.blueprint, origins=origins) - # app.register_blueprint(articles.views.blueprint) + app.register_blueprint(auth.views.blueprint) def register_errorhandlers(app): diff --git a/task_office/api/__init__.py b/task_office/auth/__init__.py similarity index 100% rename from task_office/api/__init__.py rename to task_office/auth/__init__.py diff --git a/task_office/auth/models.py b/task_office/auth/models.py new file mode 100644 index 0000000..488146b --- /dev/null +++ b/task_office/auth/models.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +"""User models.""" + +from task_office.database import Column, Model, PKMixin, db, DTMixin +from task_office.extensions import bcrypt + + +class User(PKMixin, DTMixin, Model): + + __tablename__ = "users" + + username = Column(db.String(80), unique=True, nullable=False) + email = Column(db.String(100), unique=True, nullable=False) + password = Column(db.Binary(128), nullable=True) + bio = Column(db.String(300), nullable=True) + image = Column(db.String(120), nullable=True) + token: str = "" + + def __init__(self, username, email, password=None, **kwargs): + """Create instance.""" + db.Model.__init__(self, username=username, email=email, **kwargs) + if password: + self.set_password(password) + else: + self.password = None + + def set_password(self, password): + """Set password.""" + self.password = bcrypt.generate_password_hash(password) + + def check_password(self, value): + """Check password.""" + return bcrypt.check_password_hash(self.password, value) + + def __repr__(self): + """Represent instance as a unique string.""" + return "".format(username=self.username) diff --git a/task_office/auth/serializers.py b/task_office/auth/serializers.py new file mode 100644 index 0000000..d499755 --- /dev/null +++ b/task_office/auth/serializers.py @@ -0,0 +1,38 @@ +# coding: utf-8 + +from marshmallow import Schema, fields, pre_load, post_dump + + +class UserSchema(Schema): + username = fields.Str() + email = fields.Email() + password = fields.Str(load_only=True) + bio = fields.Str() + image = fields.Url() + token = fields.Str(dump_only=True) + createdAt = fields.DateTime(attribute="created_at", dump_only=True) + updatedAt = fields.DateTime(attribute="updated_at") + # ugly hack. + user = fields.Nested("self", exclude=("user",), default=True, load_only=True) + + @pre_load + def make_user(self, data, **kwargs): + data = data["user"] + # some of the frontends send this like an empty string and some send + # null + if not data.get("email", True): + del data["email"] + if not data.get("image", True): + del data["image"] + return data + + @post_dump + def dump_user(self, data, **kwargs): + return {"user": data} + + class Meta: + strict = True + + +user_schema = UserSchema() +user_schemas = UserSchema(many=True) diff --git a/task_office/auth/views.py b/task_office/auth/views.py new file mode 100644 index 0000000..f062007 --- /dev/null +++ b/task_office/auth/views.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +"""User views.""" + +from flask import Blueprint, request +from flask_apispec import use_kwargs, marshal_with +from flask_jwt_extended import ( + jwt_required, + current_user, +) + +from .serializers import user_schema +from ..settings import CONFIG + +blueprint = Blueprint("user", __name__, url_prefix=CONFIG.API_V1_PREFIX) + + +@blueprint.route("/api/user", methods=("GET",)) +@jwt_required +@marshal_with(user_schema) +def get_user(): + user = current_user + # Not sure about this + user.token = request.headers.environ["HTTP_AUTHORIZATION"].split("Token ")[1] + return current_user + + +@blueprint.route("/api/user", methods=("PUT",)) +@jwt_required +@use_kwargs(user_schema) +@marshal_with(user_schema) +def update_user(**kwargs): + user = current_user + # take in consideration the password + password = kwargs.pop("password", None) + if password: + user.set_password(password) + if "updated_at" in kwargs: + kwargs["updated_at"] = user.created_at.replace(tzinfo=None) + user.update(**kwargs) + return user diff --git a/task_office/compat.py b/task_office/compat.py new file mode 100644 index 0000000..d9fae32 --- /dev/null +++ b/task_office/compat.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +"""Python 2/3 compatibility module.""" +import sys + +PY2 = int(sys.version[0]) == 2 + +if PY2: + pass + # text_type = unicode # noqa + # binary_type = str + # string_types = (str, unicode) # noqa + # unicode = unicode # noqa + # basestring = basestring # noqa +else: + text_type = str + binary_type = bytes + string_types = (str,) + unicode = str + basestring = (str, bytes) diff --git a/task_office/database.py b/task_office/database.py new file mode 100644 index 0000000..dd9f813 --- /dev/null +++ b/task_office/database.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +"""Database module, including the SQLAlchemy database object and DB-related utilities.""" +import datetime as dt +import uuid as uuid + +from sqlalchemy import JSON +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import relationship + +from .compat import basestring +from .extensions import db + +# Alias common SQLAlchemy names +Column = db.Column +relationship = relationship +Model = db.Model + + +class PKMixin(object): + """ + A mixin that adds a surrogate pk(s) fields + """ + + __table_args__ = {"extend_existing": True} + + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + uuid = db.Column(UUID, primary_key=True, default=uuid.uuid4()) + meta = db.Column(JSON, default=dict) + + @classmethod + def get_by_id(cls, record_id): + """Get record by ID.""" + if any( + ( + isinstance(record_id, basestring) and record_id.isdigit(), + isinstance(record_id, (int, float)), + ) + ): + return cls.query.get(int(record_id)) + + +class DTMixin(object): + """ + A mixin that adds a created_at, updated_at fields + """ + + __table_args__ = {"extend_existing": True} + + created_at = Column(db.DateTime, nullable=False, default=dt.datetime.utcnow) + updated_at = Column( + db.DateTime, + nullable=False, + default=dt.datetime.utcnow, + onupdate=dt.datetime.utcnow, + ) + + +def reference_col(tablename, nullable=False, pk_name="id", **kwargs): + """Column that adds primary key foreign key reference. + + Usage: :: + + category_id = reference_col('category') + category = relationship('Category', backref='categories') + """ + return db.Column( + db.ForeignKey("{0}.{1}".format(tablename, pk_name)), nullable=nullable, **kwargs + ) diff --git a/task_office/settings.py b/task_office/settings.py index bbb1f19..0d8d02a 100644 --- a/task_office/settings.py +++ b/task_office/settings.py @@ -8,6 +8,7 @@ class Config(object): """Base configuration.""" SECRET_KEY = os.environ.get("CONDUIT_SECRET", "secret-key") # TODO: Change me + API_V1_PREFIX = "/api/v1" APP_DIR = os.path.abspath(os.path.dirname(__file__)) # This directory PROJECT_ROOT = os.path.abspath(os.path.join(APP_DIR, os.pardir)) BCRYPT_LOG_ROUNDS = 13 @@ -32,7 +33,6 @@ class Config(object): class ProdConfig(Config): """Production configuration.""" - ENV = "PROD" DEBUG = False DATABASE = { "DB_NAME": os.environ.get("POSTGRES_DB", "task_office"), @@ -41,7 +41,7 @@ class ProdConfig(Config): "DB_HOST": os.environ.get("POSTGRES_HOST", "127.0.0.1"), "DB_PORT": os.environ.get("POSTGRES_PORT", "5432"), } - SQLALCHEMY_DATABASE_URI = "psql://{username}:{password}@{host}:{db_port}/{db_name}".format( + SQLALCHEMY_DATABASE_URI = "postgresql://{username}:{password}@{host}:{db_port}/{db_name}".format( username=DATABASE["DB_USER"], password=DATABASE["DB_PASSWORD"], host=DATABASE["DB_HOST"], @@ -53,7 +53,6 @@ class ProdConfig(Config): class DevConfig(Config): """Development configuration.""" - ENV = "DEV" DEBUG = True # Put the db file in project root DATABASE = { @@ -63,7 +62,7 @@ class DevConfig(Config): "DB_HOST": os.environ.get("POSTGRES_HOST", "127.0.0.1"), "DB_PORT": os.environ.get("POSTGRES_PORT", "5432"), } - SQLALCHEMY_DATABASE_URI = "psql://{username}:{password}@{host}:{db_port}/{db_name}".format( + SQLALCHEMY_DATABASE_URI = "postgresql://{username}:{password}@{host}:{db_port}/{db_name}".format( username=DATABASE["DB_USER"], password=DATABASE["DB_PASSWORD"], host=DATABASE["DB_HOST"], @@ -86,7 +85,7 @@ class TestConfig(Config): "DB_HOST": os.environ.get("POSTGRES_HOST", "127.0.0.1"), "DB_PORT": os.environ.get("POSTGRES_PORT", "5432"), } - SQLALCHEMY_DATABASE_URI = "psql://{username}:{password}@{host}:{db_port}/{db_name}".format( + SQLALCHEMY_DATABASE_URI = "postgresql:://{username}:{password}@{host}:{db_port}/{db_name}".format( username=DATABASE["DB_USER"], password=DATABASE["DB_PASSWORD"], host=DATABASE["DB_HOST"], @@ -94,3 +93,14 @@ class TestConfig(Config): db_name=DATABASE["DB_NAME"], ) BCRYPT_LOG_ROUNDS = 4 + + +MODE = os.environ.get("MODE") + +configurations = { + "dev": DevConfig, + "prod": ProdConfig, + "test": TestConfig +} + +CONFIG = configurations.get(MODE, "dev") From ee6bbd9862fedd4e08404d52984ddb5a972410d9 Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Fri, 20 Dec 2019 16:21:52 +0200 Subject: [PATCH 03/60] implemented sign-up, custom validators, improved project template, improved errors template --- entry.py | 1 - .../{93f7475719c6_.py => 5462f5f914dd_.py} | 15 ++-- task_office/__init__.py | 1 + task_office/app.py | 5 +- task_office/auth/models.py | 9 ++- task_office/auth/serializers.py | 74 ++++++++++++------- task_office/auth/views.py | 40 +++------- task_office/core/__init__.py | 0 task_office/core/serializers.py | 40 ++++++++++ task_office/core/validators.py | 31 ++++++++ task_office/database.py | 15 +--- task_office/exceptions.py | 28 +------ task_office/settings.py | 27 +++---- 13 files changed, 163 insertions(+), 123 deletions(-) rename migrations/versions/{93f7475719c6_.py => 5462f5f914dd_.py} (74%) create mode 100644 task_office/core/__init__.py create mode 100644 task_office/core/serializers.py create mode 100644 task_office/core/validators.py diff --git a/entry.py b/entry.py index 28ea042..7b268b2 100755 --- a/entry.py +++ b/entry.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- """Create an application instance.""" -from flask.helpers import get_debug_flag from task_office.app import create_app from task_office.settings import CONFIG diff --git a/migrations/versions/93f7475719c6_.py b/migrations/versions/5462f5f914dd_.py similarity index 74% rename from migrations/versions/93f7475719c6_.py rename to migrations/versions/5462f5f914dd_.py index d037119..7d614f6 100644 --- a/migrations/versions/93f7475719c6_.py +++ b/migrations/versions/5462f5f914dd_.py @@ -1,8 +1,8 @@ """empty message -Revision ID: 93f7475719c6 +Revision ID: 5462f5f914dd Revises: -Create Date: 2019-12-16 16:03:52.702698 +Create Date: 2019-12-17 10:11:52.139678 """ from alembic import op @@ -10,7 +10,7 @@ from sqlalchemy.dialects import postgresql # revision identifiers, used by Alembic. -revision = "93f7475719c6" +revision = "5462f5f914dd" down_revision = None branch_labels = None depends_on = None @@ -27,17 +27,20 @@ def upgrade(): sa.Column("updated_at", sa.DateTime(), nullable=False), sa.Column("username", sa.String(length=80), nullable=False), sa.Column("email", sa.String(length=100), nullable=False), - sa.Column("password", sa.Binary(), nullable=True), sa.Column("bio", sa.String(length=300), nullable=True), - sa.Column("image", sa.String(length=120), nullable=True), + sa.Column("phone", sa.String(length=300), nullable=True), + sa.Column("password", sa.Binary(), nullable=True), + sa.Column("is_active", sa.Boolean(), nullable=True), + sa.Column("is_superuser", sa.Boolean(), nullable=True), sa.PrimaryKeyConstraint("id", "uuid"), - sa.UniqueConstraint("email"), sa.UniqueConstraint("username"), ) + op.create_index(op.f("ix_users_email"), "users", ["email"], unique=True) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f("ix_users_email"), table_name="users") op.drop_table("users") # ### end Alembic commands ### diff --git a/task_office/__init__.py b/task_office/__init__.py index e69de29..40a96af 100644 --- a/task_office/__init__.py +++ b/task_office/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/task_office/app.py b/task_office/app.py index 196012b..3ad7c90 100644 --- a/task_office/app.py +++ b/task_office/app.py @@ -1,14 +1,13 @@ # -*- coding: utf-8 -*- """The app module, containing the app factory function.""" from flask import Flask -from task_office.extensions import bcrypt, cache, db, migrate, cors from task_office import commands, auth -from task_office.settings import ProdConfig from task_office.exceptions import InvalidUsage +from task_office.extensions import bcrypt, cache, db, migrate, cors -def create_app(config_object=ProdConfig): +def create_app(config_object): """An application factory, as explained here: http://flask.pocoo.org/docs/patterns/appfactories/. diff --git a/task_office/auth/models.py b/task_office/auth/models.py index 488146b..61297ce 100644 --- a/task_office/auth/models.py +++ b/task_office/auth/models.py @@ -10,11 +10,12 @@ class User(PKMixin, DTMixin, Model): __tablename__ = "users" username = Column(db.String(80), unique=True, nullable=False) - email = Column(db.String(100), unique=True, nullable=False) - password = Column(db.Binary(128), nullable=True) + email = Column(db.String(255), unique=True, nullable=False, index=True) bio = Column(db.String(300), nullable=True) - image = Column(db.String(120), nullable=True) - token: str = "" + phone = Column(db.String(300), nullable=True) + password = Column(db.Binary(128), nullable=True) + is_active = Column(db.Boolean(), default=True) + is_superuser = Column(db.Boolean(), default=False) def __init__(self, username, email, password=None, **kwargs): """Create instance.""" diff --git a/task_office/auth/serializers.py b/task_office/auth/serializers.py index d499755..4dfeaf0 100644 --- a/task_office/auth/serializers.py +++ b/task_office/auth/serializers.py @@ -1,30 +1,17 @@ # coding: utf-8 -from marshmallow import Schema, fields, pre_load, post_dump - - -class UserSchema(Schema): - username = fields.Str() - email = fields.Email() - password = fields.Str(load_only=True) - bio = fields.Str() - image = fields.Url() - token = fields.Str(dump_only=True) - createdAt = fields.DateTime(attribute="created_at", dump_only=True) - updatedAt = fields.DateTime(attribute="updated_at") - # ugly hack. - user = fields.Nested("self", exclude=("user",), default=True, load_only=True) - - @pre_load - def make_user(self, data, **kwargs): - data = data["user"] - # some of the frontends send this like an empty string and some send - # null - if not data.get("email", True): - del data["email"] - if not data.get("image", True): - del data["image"] - return data +from marshmallow import fields, post_dump, validates_schema +from marshmallow.validate import Length + +from task_office.auth.models import User +from task_office.core.serializers import BaseSchema, XSchema +from task_office.core.validators import Unique + + +class UserSchema(BaseSchema): + username = fields.Str(dump_only=True) + email = fields.Email(dump_only=True) + bio = fields.Str(dump_only=True) @post_dump def dump_user(self, data, **kwargs): @@ -36,3 +23,40 @@ class Meta: user_schema = UserSchema() user_schemas = UserSchema(many=True) + + +class UserSignUpSchema(XSchema): + error_messages = {"passwords_dismatch": "Passwords do not match"} + + password_confirm = fields.Str( + required=True, load_only=True, validate=[Length(min=6, max=32)] + ) + password = fields.Str( + required=True, + load_only=True, + validate=[Length(min=6, max=32)], + error_messages={"required": "Please provide a name."}, + ) + email = fields.Email( + required=True, validate=[Length(max=255), Unique(User, "email")] + ) + username = fields.Str( + required=True, validate=[Length(min=6, max=80), Unique(User, "username")] + ) + + @validates_schema + def validate_schema(self, data, **kwargs): + password_confirm = data["password_confirm"] + password = data["password"] + if password != password_confirm: + self.throw_error(value="", key_error="passwords_dismatch", code=422) + + class Meta: + strict = True + + @post_dump + def dump_data(self, data, **kwargs): + return data + + +user_signup_schema = UserSignUpSchema() diff --git a/task_office/auth/views.py b/task_office/auth/views.py index f062007..4ec39bc 100644 --- a/task_office/auth/views.py +++ b/task_office/auth/views.py @@ -1,40 +1,22 @@ # -*- coding: utf-8 -*- """User views.""" -from flask import Blueprint, request +from flask import Blueprint from flask_apispec import use_kwargs, marshal_with -from flask_jwt_extended import ( - jwt_required, - current_user, -) -from .serializers import user_schema +from .models import User +from .serializers import user_schema, user_signup_schema from ..settings import CONFIG -blueprint = Blueprint("user", __name__, url_prefix=CONFIG.API_V1_PREFIX) +blueprint = Blueprint("auth", __name__, url_prefix=CONFIG.API_V1_PREFIX + "/auth") -@blueprint.route("/api/user", methods=("GET",)) -@jwt_required +@blueprint.route("/sign-up", methods=("post",)) +@use_kwargs(user_signup_schema) @marshal_with(user_schema) -def get_user(): - user = current_user - # Not sure about this - user.token = request.headers.environ["HTTP_AUTHORIZATION"].split("Token ")[1] - return current_user - - -@blueprint.route("/api/user", methods=("PUT",)) -@jwt_required -@use_kwargs(user_schema) -@marshal_with(user_schema) -def update_user(**kwargs): - user = current_user - # take in consideration the password - password = kwargs.pop("password", None) - if password: - user.set_password(password) - if "updated_at" in kwargs: - kwargs["updated_at"] = user.created_at.replace(tzinfo=None) - user.update(**kwargs) +def sign_up(**kwargs): + data = kwargs + data.pop("password_confirm") + user = User(**data) + user.save() return user diff --git a/task_office/core/__init__.py b/task_office/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/task_office/core/serializers.py b/task_office/core/serializers.py new file mode 100644 index 0000000..a8a2f1a --- /dev/null +++ b/task_office/core/serializers.py @@ -0,0 +1,40 @@ +import logging + +from marshmallow import Schema, fields + +from task_office.exceptions import InvalidUsage + + +class XSchema(Schema): + def _format_error(self, value="", msg_code="", field_name="detail"): + return { + field_name: [self.error_messages.get(msg_code, "").format(value=value)], + "msg_code": msg_code if msg_code in self.error_messages else "", + } + + def _throw_exception(self, messages, code): + raise InvalidUsage(messages=messages, status_code=code) + + def throw_error(self, value, key_error, field_name="detail", code=422): + messages = [self._format_error(value, key_error, field_name)] + self._throw_exception(messages=messages, code=code) + + def handle_error(self, exc, data, **kwargs): + """Log and raise our custom exception when (de)serialization fails.""" + errors_data = exc.messages + logging.error(errors_data) + messages = [] + for field_name, msgs in errors_data.items(): + formatted_error = self._format_error(field_name=field_name) + formatted_error[field_name] = msgs + messages.append(formatted_error) + self._throw_exception(messages=messages, code=422) + + +class BaseSchema(XSchema): + created_at = fields.DateTime(attribute="created_at", dump_only=True) + updated_at = fields.DateTime(attribute="updated_at", dump_only=True) + uuid = fields.UUID(dump_only=True) + + class Meta: + strict = True diff --git a/task_office/core/validators.py b/task_office/core/validators.py new file mode 100644 index 0000000..cf6c777 --- /dev/null +++ b/task_office/core/validators.py @@ -0,0 +1,31 @@ +import typing + +from marshmallow import ValidationError +from marshmallow.validate import Validator + + +class Unique(Validator): + """Validator which is entity exists by some field.""" + + already_exists = "Already exists with value {}" + + def __init__(self, model: object, field_name: str): + + self.model = model + self.field_name = field_name + + def _repr_args(self) -> str: + return "model={!r}, field_name={!r}".format(self.model, self.field_name) + + def _format_error(self, value, message: str) -> str: + return message.format(value) + + def __call__(self, value) -> typing.Any: + param = {self.field_name: value} + + obj = self.model.query.filter_by(**param).first() + + if obj: + message = self.already_exists + raise ValidationError(self._format_error(value, message)) + return value diff --git a/task_office/database.py b/task_office/database.py index dd9f813..4518dc2 100644 --- a/task_office/database.py +++ b/task_office/database.py @@ -24,7 +24,7 @@ class PKMixin(object): __table_args__ = {"extend_existing": True} id = db.Column(db.Integer, primary_key=True, autoincrement=True) - uuid = db.Column(UUID, primary_key=True, default=uuid.uuid4()) + uuid = db.Column(UUID, primary_key=True, default=uuid.uuid4().__str__()) meta = db.Column(JSON, default=dict) @classmethod @@ -53,16 +53,3 @@ class DTMixin(object): default=dt.datetime.utcnow, onupdate=dt.datetime.utcnow, ) - - -def reference_col(tablename, nullable=False, pk_name="id", **kwargs): - """Column that adds primary key foreign key reference. - - Usage: :: - - category_id = reference_col('category') - category = relationship('Category', backref='categories') - """ - return db.Column( - db.ForeignKey("{0}.{1}".format(tablename, pk_name)), nullable=nullable, **kwargs - ) diff --git a/task_office/exceptions.py b/task_office/exceptions.py index c89c2bd..177576b 100644 --- a/task_office/exceptions.py +++ b/task_office/exceptions.py @@ -2,46 +2,26 @@ def template(data, code=500): - return {"message": {"errors": {"body": data}}, "status_code": code} + return {"messages": {"errors": data}, "status_code": code} -USER_NOT_FOUND = template(["User not found"], code=404) -USER_ALREADY_REGISTERED = template(["User already registered"], code=422) UNKNOWN_ERROR = template([], code=500) -ARTICLE_NOT_FOUND = template(["Article not found"], code=404) -COMMENT_NOT_OWNED = template(["Not your article"], code=422) class InvalidUsage(Exception): status_code = 500 - def __init__(self, message, status_code=None, payload=None): + def __init__(self, messages, status_code=None, payload=None): Exception.__init__(self) - self.message = message + self.messages = template(data=messages, code=status_code) if status_code is not None: self.status_code = status_code self.payload = payload def to_json(self): - rv = self.message + rv = self.messages return jsonify(rv) - @classmethod - def user_not_found(cls): - return cls(**USER_NOT_FOUND) - - @classmethod - def user_already_registered(cls): - return cls(**USER_ALREADY_REGISTERED) - @classmethod def unknown_error(cls): return cls(**UNKNOWN_ERROR) - - @classmethod - def article_not_found(cls): - return cls(**ARTICLE_NOT_FOUND) - - @classmethod - def comment_not_owned(cls): - return cls(**COMMENT_NOT_OWNED) diff --git a/task_office/settings.py b/task_office/settings.py index 0d8d02a..73f9086 100644 --- a/task_office/settings.py +++ b/task_office/settings.py @@ -1,22 +1,22 @@ # -*- coding: utf-8 -*- """Application configuration.""" import os -from datetime import timedelta class Config(object): """Base configuration.""" - SECRET_KEY = os.environ.get("CONDUIT_SECRET", "secret-key") # TODO: Change me - API_V1_PREFIX = "/api/v1" + PROJECT_NAME = os.environ.get("PROJECT_NAME", "Task Office") + APP_DIR = os.path.abspath(os.path.dirname(__file__)) # This directory PROJECT_ROOT = os.path.abspath(os.path.join(APP_DIR, os.pardir)) - BCRYPT_LOG_ROUNDS = 13 + + SECRET_KEY = os.environ.get("CONDUIT_SECRET", "secret-key") # TODO: Change me + API_V1_PREFIX = "/api/v1" + DEBUG_TB_INTERCEPT_REDIRECTS = False - CACHE_TYPE = "simple" # Can be "memcached", "redis", etc. SQLALCHEMY_TRACK_MODIFICATIONS = False - JWT_AUTH_USERNAME_KEY = "email" - JWT_AUTH_HEADER_PREFIX = "Token" + CORS_ORIGIN_WHITELIST = [ "http://0.0.0.0:4100", "http://localhost:4100", @@ -27,7 +27,6 @@ class Config(object): "http://0.0.0.0:4000", "http://localhost:4000", ] - JWT_HEADER_TYPE = "Token" class ProdConfig(Config): @@ -70,7 +69,6 @@ class DevConfig(Config): db_name=DATABASE["DB_NAME"], ) CACHE_TYPE = "simple" # Can be "memcached", "redis", etc. - JWT_ACCESS_TOKEN_EXPIRES = timedelta(10 ** 6) class TestConfig(Config): @@ -92,15 +90,10 @@ class TestConfig(Config): db_port=DATABASE["DB_PORT"], db_name=DATABASE["DB_NAME"], ) - BCRYPT_LOG_ROUNDS = 4 -MODE = os.environ.get("MODE") +MODE = os.environ.get("MODE", default="dev") -configurations = { - "dev": DevConfig, - "prod": ProdConfig, - "test": TestConfig -} +configurations = {"dev": DevConfig, "prod": ProdConfig, "test": TestConfig} -CONFIG = configurations.get(MODE, "dev") +CONFIG = configurations.get(MODE) From 3ccbb1b852876a2258f138203cca7674a7df8343 Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Wed, 25 Dec 2019 10:29:49 +0200 Subject: [PATCH 04/60] configured flask-jwt, implemented sign-in. --- task_office/app.py | 4 +-- task_office/auth/serializers.py | 64 ++++++++++++++++++++++++++++----- task_office/auth/views.py | 35 +++++++++++++++++- task_office/extensions.py | 7 ++++ task_office/settings.py | 8 +++++ task_office/utils.py | 12 +++++++ 6 files changed, 118 insertions(+), 12 deletions(-) create mode 100644 task_office/utils.py diff --git a/task_office/app.py b/task_office/app.py index 3ad7c90..40764ef 100644 --- a/task_office/app.py +++ b/task_office/app.py @@ -4,7 +4,7 @@ from task_office import commands, auth from task_office.exceptions import InvalidUsage -from task_office.extensions import bcrypt, cache, db, migrate, cors +from task_office.extensions import bcrypt, cache, db, migrate, cors, jwt def create_app(config_object): @@ -36,8 +36,8 @@ def register_blueprints(app): """Register Flask blueprints.""" origins = app.config.get("CORS_ORIGIN_WHITELIST", "*") cors.init_app(auth.views.blueprint, origins=origins) - app.register_blueprint(auth.views.blueprint) + jwt.init_app(app) def register_errorhandlers(app): diff --git a/task_office/auth/serializers.py b/task_office/auth/serializers.py index 4dfeaf0..446a4c5 100644 --- a/task_office/auth/serializers.py +++ b/task_office/auth/serializers.py @@ -1,5 +1,4 @@ # coding: utf-8 - from marshmallow import fields, post_dump, validates_schema from marshmallow.validate import Length @@ -32,10 +31,7 @@ class UserSignUpSchema(XSchema): required=True, load_only=True, validate=[Length(min=6, max=32)] ) password = fields.Str( - required=True, - load_only=True, - validate=[Length(min=6, max=32)], - error_messages={"required": "Please provide a name."}, + required=True, load_only=True, validate=[Length(min=6, max=32)] ) email = fields.Email( required=True, validate=[Length(max=255), Unique(User, "email")] @@ -54,9 +50,59 @@ def validate_schema(self, data, **kwargs): class Meta: strict = True - @post_dump - def dump_data(self, data, **kwargs): - return data - user_signup_schema = UserSignUpSchema() + + +class UserSignInSchema(XSchema): + error_messages = {"usr_not_found": "User not found"} + + password = fields.Str( + required=True, + load_only=True, + validate=[Length(min=6, max=32)], + error_messages={"required": "Please provide a name."}, + ) + email = fields.Email(required=True, load_only=True, validate=[Length(max=255)]) + + @validates_schema + def validate_schema(self, data, **kwargs): + email = data["email"] + password = data["password"] + user = User.query.filter_by(email=email).first() + if user is not None and user.check_password(password): + data["user"] = user + else: + self.throw_error(value="", key_error="usr_not_found", code=422) + + class Meta: + strict = True + + +user_signin_schema = UserSignInSchema() + + +class TokenSchema(XSchema): + token = fields.Str() + lifetime = fields.Integer() + + +token_schema = TokenSchema() + + +class SignedTokensSchema(XSchema): + access = fields.Nested(TokenSchema) + refresh = fields.Nested(TokenSchema) + header_type = fields.Str() + time_zone_info = fields.Str() + + +signed_tokens_schema = SignedTokensSchema() + + +class SignedSchema(XSchema): + tokens = fields.Nested(SignedTokensSchema) + user = fields.Nested(UserSchema) + + +signed_schema = SignedSchema() diff --git a/task_office/auth/views.py b/task_office/auth/views.py index 4ec39bc..960a645 100644 --- a/task_office/auth/views.py +++ b/task_office/auth/views.py @@ -1,11 +1,18 @@ # -*- coding: utf-8 -*- """User views.""" +from datetime import datetime from flask import Blueprint from flask_apispec import use_kwargs, marshal_with +from flask_jwt_extended import create_access_token, create_refresh_token from .models import User -from .serializers import user_schema, user_signup_schema +from .serializers import ( + user_schema, + user_signup_schema, + user_signin_schema, + signed_schema, +) from ..settings import CONFIG blueprint = Blueprint("auth", __name__, url_prefix=CONFIG.API_V1_PREFIX + "/auth") @@ -20,3 +27,29 @@ def sign_up(**kwargs): user = User(**data) user.save() return user + + +@blueprint.route("/sign-in", methods=("post",)) +@use_kwargs(user_signin_schema) +@marshal_with(signed_schema) +def sign_in(**kwargs): + data = kwargs + refresh_lf = datetime.timestamp( + datetime.utcnow() + CONFIG.JWT_REFRESH_TOKEN_EXPIRES + ) + access_lf = datetime.timestamp(datetime.utcnow() + CONFIG.JWT_ACCESS_TOKEN_EXPIRES) + return { + "user": data["user"], + "tokens": { + "access": { + "lifetime": access_lf, + "token": create_access_token(identity=data["user"], fresh=True), + }, + "refresh": { + "lifetime": refresh_lf, + "token": create_refresh_token(identity=data["user"]), + }, + "header_type": CONFIG.JWT_AUTH_HEADER_PREFIX, + "time_zone_info": "utc", + }, + } diff --git a/task_office/extensions.py b/task_office/extensions.py index 8287672..e368efd 100644 --- a/task_office/extensions.py +++ b/task_office/extensions.py @@ -4,6 +4,7 @@ from flask_bcrypt import Bcrypt from flask_caching import Cache from flask_cors import CORS +from flask_jwt_extended import JWTManager from flask_migrate import Migrate from flask_sqlalchemy import SQLAlchemy, Model @@ -42,3 +43,9 @@ def delete(self, commit=True): migrate = Migrate() cache = Cache() cors = CORS() + +from task_office.utils import jwt_identity, identity_loader # noqa + +jwt = JWTManager() +jwt.user_loader_callback_loader(jwt_identity) +jwt.user_identity_loader(identity_loader) diff --git a/task_office/settings.py b/task_office/settings.py index 73f9086..991190b 100644 --- a/task_office/settings.py +++ b/task_office/settings.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- """Application configuration.""" import os +from datetime import timedelta class Config(object): @@ -28,6 +29,13 @@ class Config(object): "http://localhost:4000", ] + # JWT + JWT_AUTH_USERNAME_KEY = "uuid" + JWT_AUTH_HEADER_PREFIX = "Bearer" + JWT_ACCESS_TOKEN_EXPIRES = timedelta(minutes=30) + JWT_REFRESH_TOKEN_EXPIRES = timedelta(days=7) + JWT_BLACKLIST_TOKEN_CHECKS = ["access", "refresh"] + class ProdConfig(Config): """Production configuration.""" diff --git a/task_office/utils.py b/task_office/utils.py new file mode 100644 index 0000000..aaf8ea6 --- /dev/null +++ b/task_office/utils.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +"""Helper utilities and decorators.""" + + +def jwt_identity(payload): + from task_office.auth import User + + return User.get_by_id(payload) + + +def identity_loader(user): + return user.id From 883ec922419b563d7bd4d0f0d46f28fd7fe9f891 Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Sun, 29 Dec 2019 14:43:53 +0200 Subject: [PATCH 05/60] improved app settings --- .env.example | 21 ++++++ requirements/__init__.py | 0 requirements.txt => requirements/base.txt | 21 +++--- requirements/dev.txt | 1 + requirements/prod.txt | 1 + task_office/settings.py | 83 ++++++++--------------- 6 files changed, 61 insertions(+), 66 deletions(-) create mode 100644 .env.example create mode 100644 requirements/__init__.py rename requirements.txt => requirements/base.txt (75%) create mode 100644 requirements/dev.txt create mode 100644 requirements/prod.txt diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..6131d86 --- /dev/null +++ b/.env.example @@ -0,0 +1,21 @@ +# General +# ------------------------------------------------------------------------------ +FLASK_APP=entry.py +FLASK_DEBUG=0 +MODE=dev +PROJECT_NAME=Task-Office +FLASK_SECRET=eBH93Dcn4_Pm0ryYDaGa3bCo-ZYvMabuUF6xPefYKe-lvTYp53IrTsjQZ12Lga2Bdfjt7cFmcIAB-RZapRmmzQ + +# JWT +# ------------------------------------------------------------------------------ +JWT_ACCESS_TOKEN_EXPIRES=30 +JWT_REFRESH_TOKEN_EXPIRES=7 + + +# DB +# ------------------------------------------------------------------------------ +POSTGRES_HOST=127.0.0.1 +POSTGRES_PORT=5432 +POSTGRES_DB=task_office_dev +POSTGRES_USER=task_office_user +POSTGRES_PASSWORD=task_office_user diff --git a/requirements/__init__.py b/requirements/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/requirements.txt b/requirements/base.txt similarity index 75% rename from requirements.txt rename to requirements/base.txt index f8c97d7..efc80fd 100644 --- a/requirements.txt +++ b/requirements/base.txt @@ -1,20 +1,17 @@ -Click==7.0 Flask==1.1.1 -itsdangerous==1.1.0 -Jinja2==2.10.3 -MarkupSafe==1.1.1 -Werkzeug==0.16.0 -SQLAlchemy==1.1.9 +Flask-Migrate Flask_Caching -Flask_SQLAlchemy==2.2 -marshmallow Flask_Bcrypt flask_apispec -PyJWT Flask-JWT-Extended -unicode_slugify +Flask-Cors +Flask_SQLAlchemy==2.2 +SQLAlchemy==1.1.9 psycopg2 -Flask-Migrate +marshmallow +MarkupSafe==1.1.1 gunicorn -Flask-Cors uuid +environs +unicode_slugify +Click==7.0 \ No newline at end of file diff --git a/requirements/dev.txt b/requirements/dev.txt new file mode 100644 index 0000000..ee51fcd --- /dev/null +++ b/requirements/dev.txt @@ -0,0 +1 @@ +-r ./base.txt \ No newline at end of file diff --git a/requirements/prod.txt b/requirements/prod.txt new file mode 100644 index 0000000..ee51fcd --- /dev/null +++ b/requirements/prod.txt @@ -0,0 +1 @@ +-r ./base.txt \ No newline at end of file diff --git a/task_office/settings.py b/task_office/settings.py index 991190b..e429497 100644 --- a/task_office/settings.py +++ b/task_office/settings.py @@ -3,16 +3,26 @@ import os from datetime import timedelta +from environs import Env + +env = Env() + class Config(object): """Base configuration.""" - PROJECT_NAME = os.environ.get("PROJECT_NAME", "Task Office") - - APP_DIR = os.path.abspath(os.path.dirname(__file__)) # This directory + APP_DIR = os.path.abspath(os.path.dirname(__file__)) PROJECT_ROOT = os.path.abspath(os.path.join(APP_DIR, os.pardir)) - SECRET_KEY = os.environ.get("CONDUIT_SECRET", "secret-key") # TODO: Change me + READ_DOT_ENV_FILE = env.bool("FLASK_READ_DOT_ENV_FILE", default=True) + if READ_DOT_ENV_FILE: + # OS environment variables take precedence over variables from .env + env.read_env(os.path.join(PROJECT_ROOT, ".env")) + + FLASK_DEBUG = env.int("FLASK_DEBUG", 0) + + PROJECT_NAME = env.str("PROJECT_NAME", "Task Office") + SECRET_KEY = env.str("FLASK_SECRET", "secret-key") API_V1_PREFIX = "/api/v1" DEBUG_TB_INTERCEPT_REDIRECTS = False @@ -32,21 +42,19 @@ class Config(object): # JWT JWT_AUTH_USERNAME_KEY = "uuid" JWT_AUTH_HEADER_PREFIX = "Bearer" - JWT_ACCESS_TOKEN_EXPIRES = timedelta(minutes=30) - JWT_REFRESH_TOKEN_EXPIRES = timedelta(days=7) + JWT_ACCESS_TOKEN_EXPIRES = timedelta( + minutes=env.int("JWT_ACCESS_TOKEN_EXPIRES", 30) + ) + JWT_REFRESH_TOKEN_EXPIRES = timedelta(days=env.int("JWT_REFRESH_TOKEN_EXPIRES", 7)) JWT_BLACKLIST_TOKEN_CHECKS = ["access", "refresh"] - -class ProdConfig(Config): - """Production configuration.""" - - DEBUG = False + # DB DATABASE = { - "DB_NAME": os.environ.get("POSTGRES_DB", "task_office"), - "DB_USER": os.environ.get("POSTGRES_USER", "task_office_user"), - "DB_PASSWORD": os.environ.get("POSTGRES_PASSWORD", "task_office_user"), - "DB_HOST": os.environ.get("POSTGRES_HOST", "127.0.0.1"), - "DB_PORT": os.environ.get("POSTGRES_PORT", "5432"), + "DB_NAME": env.str("POSTGRES_DB", "task_office"), + "DB_USER": env.str("POSTGRES_USER", "task_office_user"), + "DB_PASSWORD": env.str("POSTGRES_PASSWORD", "task_office_user"), + "DB_HOST": env.str("POSTGRES_HOST", "127.0.0.1"), + "DB_PORT": env.int("POSTGRES_PORT", "5432"), } SQLALCHEMY_DATABASE_URI = "postgresql://{username}:{password}@{host}:{db_port}/{db_name}".format( username=DATABASE["DB_USER"], @@ -57,51 +65,18 @@ class ProdConfig(Config): ) +class ProdConfig(Config): + """Production configuration.""" + + class DevConfig(Config): """Development configuration.""" - DEBUG = True - # Put the db file in project root - DATABASE = { - "DB_NAME": os.environ.get("POSTGRES_DB", "task_office_dev"), - "DB_USER": os.environ.get("POSTGRES_USER", "task_office_user"), - "DB_PASSWORD": os.environ.get("POSTGRES_PASSWORD", "task_office_user"), - "DB_HOST": os.environ.get("POSTGRES_HOST", "127.0.0.1"), - "DB_PORT": os.environ.get("POSTGRES_PORT", "5432"), - } - SQLALCHEMY_DATABASE_URI = "postgresql://{username}:{password}@{host}:{db_port}/{db_name}".format( - username=DATABASE["DB_USER"], - password=DATABASE["DB_PASSWORD"], - host=DATABASE["DB_HOST"], - db_port=DATABASE["DB_PORT"], - db_name=DATABASE["DB_NAME"], - ) CACHE_TYPE = "simple" # Can be "memcached", "redis", etc. -class TestConfig(Config): - """Test configuration.""" - - TESTING = True - DEBUG = True - DATABASE = { - "DB_NAME": os.environ.get("POSTGRES_DB", "task_office_test"), - "DB_USER": os.environ.get("POSTGRES_USER", "task_office_user"), - "DB_PASSWORD": os.environ.get("POSTGRES_PASSWORD", "task_office_user"), - "DB_HOST": os.environ.get("POSTGRES_HOST", "127.0.0.1"), - "DB_PORT": os.environ.get("POSTGRES_PORT", "5432"), - } - SQLALCHEMY_DATABASE_URI = "postgresql:://{username}:{password}@{host}:{db_port}/{db_name}".format( - username=DATABASE["DB_USER"], - password=DATABASE["DB_PASSWORD"], - host=DATABASE["DB_HOST"], - db_port=DATABASE["DB_PORT"], - db_name=DATABASE["DB_NAME"], - ) - - MODE = os.environ.get("MODE", default="dev") -configurations = {"dev": DevConfig, "prod": ProdConfig, "test": TestConfig} +configurations = {"dev": DevConfig, "prod": ProdConfig} CONFIG = configurations.get(MODE) From 3cb275216ac561a9aaf84ad5cdf1e17afae38811 Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Mon, 30 Dec 2019 18:11:38 +0200 Subject: [PATCH 06/60] init auto autogenerated open api --- .env.example | 2 ++ requirements/base.txt | 1 - requirements/dev.txt | 6 +++++- task_office/app.py | 15 ++++++++++++--- task_office/auth/serializers.py | 9 +++++++++ task_office/settings.py | 24 +++++++++++------------- task_office/swagger/__init__.py | 3 +++ task_office/swagger/specs.py | 15 +++++++++++++++ task_office/swagger/views.py | 26 ++++++++++++++++++++++++++ 9 files changed, 83 insertions(+), 18 deletions(-) create mode 100644 task_office/swagger/__init__.py create mode 100644 task_office/swagger/specs.py create mode 100644 task_office/swagger/views.py diff --git a/.env.example b/.env.example index 6131d86..afd76f4 100644 --- a/.env.example +++ b/.env.example @@ -3,8 +3,10 @@ FLASK_APP=entry.py FLASK_DEBUG=0 MODE=dev +USE_DOCS=True PROJECT_NAME=Task-Office FLASK_SECRET=eBH93Dcn4_Pm0ryYDaGa3bCo-ZYvMabuUF6xPefYKe-lvTYp53IrTsjQZ12Lga2Bdfjt7cFmcIAB-RZapRmmzQ +CORS_ORIGIN_WHITELIST=http://localhost:4000,http://localhost:4001 # JWT # ------------------------------------------------------------------------------ diff --git a/requirements/base.txt b/requirements/base.txt index efc80fd..ec1241b 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -2,7 +2,6 @@ Flask==1.1.1 Flask-Migrate Flask_Caching Flask_Bcrypt -flask_apispec Flask-JWT-Extended Flask-Cors Flask_SQLAlchemy==2.2 diff --git a/requirements/dev.txt b/requirements/dev.txt index ee51fcd..12847d6 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1 +1,5 @@ --r ./base.txt \ No newline at end of file +-r ./base.txt + +flask-swagger-ui==3.20.9 +apispec>=1.0.0 +apispec-webframeworks \ No newline at end of file diff --git a/task_office/app.py b/task_office/app.py index 40764ef..7240904 100644 --- a/task_office/app.py +++ b/task_office/app.py @@ -2,9 +2,11 @@ """The app module, containing the app factory function.""" from flask import Flask -from task_office import commands, auth +from task_office import commands, auth, swagger from task_office.exceptions import InvalidUsage from task_office.extensions import bcrypt, cache, db, migrate, cors, jwt +from task_office.settings import CONFIG +from task_office.swagger import SWAGGER_URL def create_app(config_object): @@ -13,7 +15,11 @@ def create_app(config_object): :param config_object: The configuration object to use. """ - app = Flask(__name__.split(".")[0]) + app = Flask( + __name__.split(".")[0], + static_folder=CONFIG.STATIC_DIR, + static_url_path=CONFIG.STATIC_URL, + ) app.url_map.strict_slashes = False app.config.from_object(config_object) register_extensions(app) @@ -29,6 +35,7 @@ def register_extensions(app): bcrypt.init_app(app) cache.init_app(app) db.init_app(app) + jwt.init_app(app) migrate.init_app(app, db) @@ -37,7 +44,9 @@ def register_blueprints(app): origins = app.config.get("CORS_ORIGIN_WHITELIST", "*") cors.init_app(auth.views.blueprint, origins=origins) app.register_blueprint(auth.views.blueprint) - jwt.init_app(app) + if CONFIG.USE_DOCS: + app.register_blueprint(swagger.views.blueprint_swagger, url_prefix=SWAGGER_URL) + app.register_blueprint(swagger.views.blueprint) def register_errorhandlers(app): diff --git a/task_office/auth/serializers.py b/task_office/auth/serializers.py index 446a4c5..0bb4879 100644 --- a/task_office/auth/serializers.py +++ b/task_office/auth/serializers.py @@ -5,6 +5,7 @@ from task_office.auth.models import User from task_office.core.serializers import BaseSchema, XSchema from task_office.core.validators import Unique +from task_office.swagger import API_SPEC class UserSchema(BaseSchema): @@ -106,3 +107,11 @@ class SignedSchema(XSchema): signed_schema = SignedSchema() + + +API_SPEC.components.schema("UserSchema", schema=UserSchema) +API_SPEC.components.schema("UserSignInSchema", schema=UserSignInSchema) +API_SPEC.components.schema("UserSignUpSchema", schema=UserSignUpSchema) +API_SPEC.components.schema("TokenSchema", schema=TokenSchema) +API_SPEC.components.schema("SignedTokensSchema", schema=SignedTokensSchema) +API_SPEC.components.schema("SignedSchema", schema=SignedSchema) diff --git a/task_office/settings.py b/task_office/settings.py index e429497..2ce5c02 100644 --- a/task_office/settings.py +++ b/task_office/settings.py @@ -11,33 +11,31 @@ class Config(object): """Base configuration.""" + # Project dirs APP_DIR = os.path.abspath(os.path.dirname(__file__)) PROJECT_ROOT = os.path.abspath(os.path.join(APP_DIR, os.pardir)) + # Environment variables setting READ_DOT_ENV_FILE = env.bool("FLASK_READ_DOT_ENV_FILE", default=True) if READ_DOT_ENV_FILE: # OS environment variables take precedence over variables from .env env.read_env(os.path.join(PROJECT_ROOT, ".env")) - FLASK_DEBUG = env.int("FLASK_DEBUG", 0) - PROJECT_NAME = env.str("PROJECT_NAME", "Task Office") SECRET_KEY = env.str("FLASK_SECRET", "secret-key") - API_V1_PREFIX = "/api/v1" + API_V1_PREFIX = "/api/v1" + USE_DOCS = env.bool("USE_DOCS", False) + FLASK_DEBUG = env.int("FLASK_DEBUG", 0) DEBUG_TB_INTERCEPT_REDIRECTS = False + + # Static settings + STATIC_DIR = os.path.abspath(os.path.join(PROJECT_ROOT, "static")) + STATIC_URL = API_V1_PREFIX + "/static" + SQLALCHEMY_TRACK_MODIFICATIONS = False - CORS_ORIGIN_WHITELIST = [ - "http://0.0.0.0:4100", - "http://localhost:4100", - "http://0.0.0.0:8000", - "http://localhost:8000", - "http://0.0.0.0:4200", - "http://localhost:4200", - "http://0.0.0.0:4000", - "http://localhost:4000", - ] + CORS_ORIGIN_WHITELIST = env.list("CORS_ORIGIN_WHITELIST", []) # JWT JWT_AUTH_USERNAME_KEY = "uuid" diff --git a/task_office/swagger/__init__.py b/task_office/swagger/__init__.py new file mode 100644 index 0000000..67ce501 --- /dev/null +++ b/task_office/swagger/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from .views import * diff --git a/task_office/swagger/specs.py b/task_office/swagger/specs.py new file mode 100644 index 0000000..a950c39 --- /dev/null +++ b/task_office/swagger/specs.py @@ -0,0 +1,15 @@ +from apispec import APISpec +from apispec.ext.marshmallow import MarshmallowPlugin +from apispec_webframeworks.flask import FlaskPlugin + +from task_office.settings import CONFIG + +API_SPEC = APISpec( + openapi_version="3.0.0", + title=CONFIG.PROJECT_NAME, + version="1.0.0", + info=dict(description="Some Description"), + plugins=[FlaskPlugin(), MarshmallowPlugin()], +) + +# TODO(Medniy) wait for a DocumentedBlueprint and make urls generation diff --git a/task_office/swagger/views.py b/task_office/swagger/views.py new file mode 100644 index 0000000..3f3d496 --- /dev/null +++ b/task_office/swagger/views.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +"""Swagger views.""" + +from flask import Blueprint, jsonify +from flask_swagger_ui import get_swaggerui_blueprint + +from .specs import API_SPEC +from ..settings import CONFIG + +SWAGGER_URL = CONFIG.API_V1_PREFIX + "/docs" + +API_URL = "/api/v1/docs/open-api" + + +blueprint = Blueprint("docs", __name__, url_prefix=CONFIG.API_V1_PREFIX + "/docs") + +blueprint_swagger = get_swaggerui_blueprint( + SWAGGER_URL, + API_URL, + config={"app_name": CONFIG.PROJECT_NAME}, +) + + +@blueprint.route("/open-api", methods=("get",)) +def api_swagger(**kwargs): + return jsonify(API_SPEC.to_dict()) From 42f2352e6936fb2f591259a055fad323293e2351 Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Tue, 31 Dec 2019 16:43:47 +0200 Subject: [PATCH 07/60] implemented auth refresh token, implemented jwt extended errors wrapped, refactoring --- task_office/app.py | 20 ++++- task_office/auth/jwt_error_handlers.py | 106 +++++++++++++++++++++++++ task_office/auth/models.py | 4 +- task_office/auth/serializers.py | 13 ++- task_office/auth/views.py | 23 +++++- task_office/core/models/__init__.py | 1 + task_office/core/models/mixins.py | 48 +++++++++++ task_office/database.py | 44 ---------- task_office/exceptions.py | 2 +- task_office/swagger/views.py | 4 +- 10 files changed, 206 insertions(+), 59 deletions(-) create mode 100644 task_office/auth/jwt_error_handlers.py create mode 100644 task_office/core/models/__init__.py create mode 100644 task_office/core/models/mixins.py diff --git a/task_office/app.py b/task_office/app.py index 7240904..c8f5518 100644 --- a/task_office/app.py +++ b/task_office/app.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- """The app module, containing the app factory function.""" from flask import Flask +from task_office.auth.jwt_error_handlers import jwt_errors_map from task_office import commands, auth, swagger from task_office.exceptions import InvalidUsage @@ -24,7 +25,7 @@ def create_app(config_object): app.config.from_object(config_object) register_extensions(app) register_blueprints(app) - register_errorhandlers(app) + register_error_handlers(app) register_shellcontext(app) register_commands(app) return app @@ -49,13 +50,24 @@ def register_blueprints(app): app.register_blueprint(swagger.views.blueprint) -def register_errorhandlers(app): - def errorhandler(error): +def register_error_handlers(app): + def error_handler(error): response = error.to_json() response.status_code = error.status_code return response - app.errorhandler(InvalidUsage)(errorhandler) + app.errorhandler(InvalidUsage)(error_handler) + + # register errors(wrapped) for jwt extended custom + def jwt_error_handler(error): + wrapped_error_handler = jwt_errors_map[str(error.__class__.__name__)]["handler"] + unwrapped_error = wrapped_error_handler(error) + return error_handler(unwrapped_error) + + [ + app.errorhandler(payload["error"])(jwt_error_handler) + for error_name, payload in jwt_errors_map.items() + ] def register_shellcontext(app): diff --git a/task_office/auth/jwt_error_handlers.py b/task_office/auth/jwt_error_handlers.py new file mode 100644 index 0000000..67951db --- /dev/null +++ b/task_office/auth/jwt_error_handlers.py @@ -0,0 +1,106 @@ +from flask_jwt_extended import get_jwt_identity +from flask_jwt_extended.exceptions import ( + NoAuthorizationError, + CSRFError, + InvalidHeaderError, + JWTDecodeError, + WrongTokenError, + RevokedTokenError, + FreshTokenRequired, + UserLoadError, + UserClaimsVerificationError, +) +from jwt import ExpiredSignatureError, InvalidTokenError + +from task_office.exceptions import InvalidUsage + + +def handle_auth_error(e): + return InvalidUsage(messages=[str(e)], status_code=401) + + +def handle_expired_error(e): + return InvalidUsage(messages=[str(e)], status_code=401) + + +def handle_invalid_header_error(e): + return InvalidUsage(messages=[str(e)], status_code=422) + + +def handle_invalid_token_error(e): + return InvalidUsage(messages=[str(e)], status_code=422) + + +def handle_jwt_decode_error(e): + return InvalidUsage(messages=[str(e)], status_code=422) + + +def handle_wrong_token_error(e): + return InvalidUsage(messages=[str(e)], status_code=401) + + +def handle_revoked_token_error(e): + return InvalidUsage(messages=["Token has been revoked"], status_code=401) + + +def handle_fresh_token_required(e): + return InvalidUsage(messages=["Fresh token required"], status_code=401) + + +def handler_user_load_error(e): + # The identity is already saved before this exception was raised, + # otherwise a different exception would be raised, which is why we + # can safely call get_jwt_identity() here + identity = get_jwt_identity() + return InvalidUsage( + messages=["Error loading the user {}".format(identity)], status_code=401 + ) + + +def handle_failed_user_claims_verification(e): + return InvalidUsage(messages=["User claims verification failed"], status_code=400) + + +jwt_errors_map = { + NoAuthorizationError.__name__: { + "handler": handle_auth_error, + "error": NoAuthorizationError, + }, + CSRFError.__name__: {"handler": handle_auth_error, "error": CSRFError}, + ExpiredSignatureError.__name__: { + "handler": handle_expired_error, + "error": ExpiredSignatureError, + }, + InvalidHeaderError.__name__: { + "handler": handle_invalid_header_error, + "error": InvalidHeaderError, + }, + InvalidTokenError.__name__: { + "handler": handle_invalid_token_error, + "error": InvalidTokenError, + }, + JWTDecodeError.__name__: { + "handler": handle_jwt_decode_error, + "error": JWTDecodeError, + }, + WrongTokenError.__name__: { + "handler": handle_wrong_token_error, + "error": WrongTokenError, + }, + RevokedTokenError.__name__: { + "handler": handle_revoked_token_error, + "error": RevokedTokenError, + }, + FreshTokenRequired.__name__: { + "handler": handle_fresh_token_required, + "error": FreshTokenRequired, + }, + UserLoadError.__name__: { + "handler": handler_user_load_error, + "error": UserLoadError, + }, + UserClaimsVerificationError.__name__: { + "handler": handle_failed_user_claims_verification, + "error": UserClaimsVerificationError, + }, +} diff --git a/task_office/auth/models.py b/task_office/auth/models.py index 61297ce..b23d80c 100644 --- a/task_office/auth/models.py +++ b/task_office/auth/models.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """User models.""" - -from task_office.database import Column, Model, PKMixin, db, DTMixin +from task_office.core.models.mixins import DTMixin, PKMixin +from task_office.database import Column, Model, db from task_office.extensions import bcrypt diff --git a/task_office/auth/serializers.py b/task_office/auth/serializers.py index 0bb4879..750a99b 100644 --- a/task_office/auth/serializers.py +++ b/task_office/auth/serializers.py @@ -88,9 +88,6 @@ class TokenSchema(XSchema): lifetime = fields.Integer() -token_schema = TokenSchema() - - class SignedTokensSchema(XSchema): access = fields.Nested(TokenSchema) refresh = fields.Nested(TokenSchema) @@ -98,7 +95,9 @@ class SignedTokensSchema(XSchema): time_zone_info = fields.Str() -signed_tokens_schema = SignedTokensSchema() +class RefreshedAccessTokenSchema(XSchema): + access = fields.Nested(TokenSchema) + time_zone_info = fields.Str() class SignedSchema(XSchema): @@ -107,6 +106,9 @@ class SignedSchema(XSchema): signed_schema = SignedSchema() +signed_tokens_schema = SignedTokensSchema() +refreshed_access_tokens_schema = RefreshedAccessTokenSchema() +token_schema = TokenSchema() API_SPEC.components.schema("UserSchema", schema=UserSchema) @@ -114,4 +116,7 @@ class SignedSchema(XSchema): API_SPEC.components.schema("UserSignUpSchema", schema=UserSignUpSchema) API_SPEC.components.schema("TokenSchema", schema=TokenSchema) API_SPEC.components.schema("SignedTokensSchema", schema=SignedTokensSchema) +API_SPEC.components.schema( + "RefreshedAccessTokenSchema", schema=RefreshedAccessTokenSchema +) API_SPEC.components.schema("SignedSchema", schema=SignedSchema) diff --git a/task_office/auth/views.py b/task_office/auth/views.py index 960a645..c740cf4 100644 --- a/task_office/auth/views.py +++ b/task_office/auth/views.py @@ -4,7 +4,12 @@ from flask import Blueprint from flask_apispec import use_kwargs, marshal_with -from flask_jwt_extended import create_access_token, create_refresh_token +from flask_jwt_extended import ( + create_access_token, + create_refresh_token, + jwt_refresh_token_required, + get_jwt_identity, +) from .models import User from .serializers import ( @@ -12,6 +17,7 @@ user_signup_schema, user_signin_schema, signed_schema, + refreshed_access_tokens_schema, ) from ..settings import CONFIG @@ -53,3 +59,18 @@ def sign_in(**kwargs): "time_zone_info": "utc", }, } + + +@blueprint.route("/refresh", methods=("post",)) +@jwt_refresh_token_required +@marshal_with(refreshed_access_tokens_schema) +def refresh(**kwargs): + current_user = User.get_by_id(get_jwt_identity()) + access_lf = datetime.timestamp(datetime.utcnow() + CONFIG.JWT_ACCESS_TOKEN_EXPIRES) + return { + "access": { + "lifetime": access_lf, + "token": create_access_token(identity=current_user, fresh=True), + }, + "time_zone_info": "utc", + } diff --git a/task_office/core/models/__init__.py b/task_office/core/models/__init__.py new file mode 100644 index 0000000..40a96af --- /dev/null +++ b/task_office/core/models/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/task_office/core/models/mixins.py b/task_office/core/models/mixins.py new file mode 100644 index 0000000..441922a --- /dev/null +++ b/task_office/core/models/mixins.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +import datetime as dt +import uuid as uuid + +from sqlalchemy import JSON +from sqlalchemy.dialects.postgresql import UUID + +from task_office.compat import basestring +from task_office.extensions import db + + +class PKMixin(object): + """ + A mixin that adds a surrogate pk(s) fields + """ + + __table_args__ = {"extend_existing": True} + + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + uuid = db.Column(UUID, primary_key=True, default=uuid.uuid4().__str__()) + meta = db.Column(JSON, default=dict) + + @classmethod + def get_by_id(cls, record_id): + """Get record by ID.""" + if any( + ( + isinstance(record_id, basestring) and record_id.isdigit(), + isinstance(record_id, (int, float)), + ) + ): + return cls.query.filter_by(id=record_id).first() + + +class DTMixin(object): + """ + A mixin that adds a created_at, updated_at fields + """ + + __table_args__ = {"extend_existing": True} + + created_at = db.Column(db.DateTime, nullable=False, default=dt.datetime.utcnow) + updated_at = db.Column( + db.DateTime, + nullable=False, + default=dt.datetime.utcnow, + onupdate=dt.datetime.utcnow, + ) diff --git a/task_office/database.py b/task_office/database.py index 4518dc2..fc4838b 100644 --- a/task_office/database.py +++ b/task_office/database.py @@ -1,55 +1,11 @@ # -*- coding: utf-8 -*- """Database module, including the SQLAlchemy database object and DB-related utilities.""" -import datetime as dt -import uuid as uuid -from sqlalchemy import JSON -from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import relationship -from .compat import basestring from .extensions import db # Alias common SQLAlchemy names Column = db.Column relationship = relationship Model = db.Model - - -class PKMixin(object): - """ - A mixin that adds a surrogate pk(s) fields - """ - - __table_args__ = {"extend_existing": True} - - id = db.Column(db.Integer, primary_key=True, autoincrement=True) - uuid = db.Column(UUID, primary_key=True, default=uuid.uuid4().__str__()) - meta = db.Column(JSON, default=dict) - - @classmethod - def get_by_id(cls, record_id): - """Get record by ID.""" - if any( - ( - isinstance(record_id, basestring) and record_id.isdigit(), - isinstance(record_id, (int, float)), - ) - ): - return cls.query.get(int(record_id)) - - -class DTMixin(object): - """ - A mixin that adds a created_at, updated_at fields - """ - - __table_args__ = {"extend_existing": True} - - created_at = Column(db.DateTime, nullable=False, default=dt.datetime.utcnow) - updated_at = Column( - db.DateTime, - nullable=False, - default=dt.datetime.utcnow, - onupdate=dt.datetime.utcnow, - ) diff --git a/task_office/exceptions.py b/task_office/exceptions.py index 177576b..c2228c0 100644 --- a/task_office/exceptions.py +++ b/task_office/exceptions.py @@ -11,7 +11,7 @@ def template(data, code=500): class InvalidUsage(Exception): status_code = 500 - def __init__(self, messages, status_code=None, payload=None): + def __init__(self, messages, status_code=500, payload=None): Exception.__init__(self) self.messages = template(data=messages, code=status_code) if status_code is not None: diff --git a/task_office/swagger/views.py b/task_office/swagger/views.py index 3f3d496..8934651 100644 --- a/task_office/swagger/views.py +++ b/task_office/swagger/views.py @@ -15,9 +15,7 @@ blueprint = Blueprint("docs", __name__, url_prefix=CONFIG.API_V1_PREFIX + "/docs") blueprint_swagger = get_swaggerui_blueprint( - SWAGGER_URL, - API_URL, - config={"app_name": CONFIG.PROJECT_NAME}, + SWAGGER_URL, API_URL, config={"app_name": CONFIG.PROJECT_NAME} ) From 7821ad8c9990d46bd1baea5daf6363ee6edd1da0 Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Sat, 4 Jan 2020 14:54:22 +0200 Subject: [PATCH 08/60] configured default datetime format for api --- task_office/core/serializers.py | 9 +++++++-- task_office/settings.py | 1 + 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/task_office/core/serializers.py b/task_office/core/serializers.py index a8a2f1a..c67708c 100644 --- a/task_office/core/serializers.py +++ b/task_office/core/serializers.py @@ -3,6 +3,7 @@ from marshmallow import Schema, fields from task_office.exceptions import InvalidUsage +from task_office.settings import CONFIG class XSchema(Schema): @@ -32,8 +33,12 @@ def handle_error(self, exc, data, **kwargs): class BaseSchema(XSchema): - created_at = fields.DateTime(attribute="created_at", dump_only=True) - updated_at = fields.DateTime(attribute="updated_at", dump_only=True) + created_at = fields.DateTime( + attribute="created_at", dump_only=True, format=CONFIG.API_DATETIME_FORMAT + ) + updated_at = fields.DateTime( + attribute="updated_at", dump_only=True, format=CONFIG.API_DATETIME_FORMAT + ) uuid = fields.UUID(dump_only=True) class Meta: diff --git a/task_office/settings.py b/task_office/settings.py index 2ce5c02..a1f6c7d 100644 --- a/task_office/settings.py +++ b/task_office/settings.py @@ -25,6 +25,7 @@ class Config(object): SECRET_KEY = env.str("FLASK_SECRET", "secret-key") API_V1_PREFIX = "/api/v1" + API_DATETIME_FORMAT = "%Y-%m-%d %I:%M:%S" USE_DOCS = env.bool("USE_DOCS", False) FLASK_DEBUG = env.int("FLASK_DEBUG", 0) DEBUG_TB_INTERCEPT_REDIRECTS = False From 0292c43d404f28b9e1fea1c00634459aa71dc065 Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Sat, 4 Jan 2020 15:50:09 +0200 Subject: [PATCH 09/60] configured flask-babel translations --- README.rst | 10 +++++ babel.cfg | 3 ++ requirements/base.txt | 3 +- task_office/app.py | 3 +- task_office/auth/jwt_error_handlers.py | 11 ++++-- task_office/auth/serializers.py | 10 ++--- task_office/auth/views.py | 4 +- task_office/core/validators.py | 4 +- task_office/extensions.py | 32 ++++++++++++++-- task_office/settings.py | 7 ++++ translations/en/LC_MESSAGES/messages.po | 48 ++++++++++++++++++++++++ translations/ru/LC_MESSAGES/messages.po | 49 +++++++++++++++++++++++++ translations/uk/LC_MESSAGES/messages.po | 49 +++++++++++++++++++++++++ 13 files changed, 213 insertions(+), 20 deletions(-) create mode 100644 babel.cfg create mode 100644 translations/en/LC_MESSAGES/messages.po create mode 100644 translations/ru/LC_MESSAGES/messages.po create mode 100644 translations/uk/LC_MESSAGES/messages.po diff --git a/README.rst b/README.rst index 72d54bc..7d9e40e 100644 --- a/README.rst +++ b/README.rst @@ -29,6 +29,16 @@ To run the web application use:: flask run --with-threads + +Translations commands:: + + pybabel extract -F babel.cfg -k lazy_gettext -o messages.pot . + pybabel init -i messages.pot -d translations -l en + pybabel init -i messages.pot -d translations -l uk + pybabel init -i messages.pot -d translations -l ru + pybabel compile -d translations + pybabel update -i messages.pot -d translations + Features -------- diff --git a/babel.cfg b/babel.cfg new file mode 100644 index 0000000..0cc0bac --- /dev/null +++ b/babel.cfg @@ -0,0 +1,3 @@ +[python: **.py] +[jinja2: **/templates/**.html] +extensions=jinja2.ext.autoescape,jinja2.ext.with_ \ No newline at end of file diff --git a/requirements/base.txt b/requirements/base.txt index ec1241b..1112f11 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -13,4 +13,5 @@ gunicorn uuid environs unicode_slugify -Click==7.0 \ No newline at end of file +Click==7.0 +Flask-Babel>=0.12.2 \ No newline at end of file diff --git a/task_office/app.py b/task_office/app.py index c8f5518..e08a269 100644 --- a/task_office/app.py +++ b/task_office/app.py @@ -5,7 +5,7 @@ from task_office import commands, auth, swagger from task_office.exceptions import InvalidUsage -from task_office.extensions import bcrypt, cache, db, migrate, cors, jwt +from task_office.extensions import bcrypt, cache, db, migrate, cors, jwt, babel from task_office.settings import CONFIG from task_office.swagger import SWAGGER_URL @@ -38,6 +38,7 @@ def register_extensions(app): db.init_app(app) jwt.init_app(app) migrate.init_app(app, db) + babel.init_app(app) def register_blueprints(app): diff --git a/task_office/auth/jwt_error_handlers.py b/task_office/auth/jwt_error_handlers.py index 67951db..fc4c15e 100644 --- a/task_office/auth/jwt_error_handlers.py +++ b/task_office/auth/jwt_error_handlers.py @@ -1,3 +1,4 @@ +from flask_babel import lazy_gettext as _ from flask_jwt_extended import get_jwt_identity from flask_jwt_extended.exceptions import ( NoAuthorizationError, @@ -40,11 +41,11 @@ def handle_wrong_token_error(e): def handle_revoked_token_error(e): - return InvalidUsage(messages=["Token has been revoked"], status_code=401) + return InvalidUsage(messages=[_("Token has been revoked")], status_code=401) def handle_fresh_token_required(e): - return InvalidUsage(messages=["Fresh token required"], status_code=401) + return InvalidUsage(messages=[_("Fresh token required")], status_code=401) def handler_user_load_error(e): @@ -53,12 +54,14 @@ def handler_user_load_error(e): # can safely call get_jwt_identity() here identity = get_jwt_identity() return InvalidUsage( - messages=["Error loading the user {}".format(identity)], status_code=401 + messages=[_("Error loading the user {}").format(identity)], status_code=401 ) def handle_failed_user_claims_verification(e): - return InvalidUsage(messages=["User claims verification failed"], status_code=400) + return InvalidUsage( + messages=[_("User claims verification failed")], status_code=400 + ) jwt_errors_map = { diff --git a/task_office/auth/serializers.py b/task_office/auth/serializers.py index 750a99b..c8f0e4b 100644 --- a/task_office/auth/serializers.py +++ b/task_office/auth/serializers.py @@ -1,4 +1,5 @@ # coding: utf-8 +from flask_babel import lazy_gettext as _ from marshmallow import fields, post_dump, validates_schema from marshmallow.validate import Length @@ -26,7 +27,7 @@ class Meta: class UserSignUpSchema(XSchema): - error_messages = {"passwords_dismatch": "Passwords do not match"} + error_messages = {"passwords_dismatch": _("Passwords do not match")} password_confirm = fields.Str( required=True, load_only=True, validate=[Length(min=6, max=32)] @@ -56,13 +57,10 @@ class Meta: class UserSignInSchema(XSchema): - error_messages = {"usr_not_found": "User not found"} + error_messages = {"usr_not_found": _("User not found")} password = fields.Str( - required=True, - load_only=True, - validate=[Length(min=6, max=32)], - error_messages={"required": "Please provide a name."}, + required=True, load_only=True, validate=[Length(min=6, max=32)] ) email = fields.Email(required=True, load_only=True, validate=[Length(max=255)]) diff --git a/task_office/auth/views.py b/task_office/auth/views.py index c740cf4..81f4196 100644 --- a/task_office/auth/views.py +++ b/task_office/auth/views.py @@ -56,7 +56,7 @@ def sign_in(**kwargs): "token": create_refresh_token(identity=data["user"]), }, "header_type": CONFIG.JWT_AUTH_HEADER_PREFIX, - "time_zone_info": "utc", + "time_zone_info": CONFIG.TIME_ZONE, }, } @@ -72,5 +72,5 @@ def refresh(**kwargs): "lifetime": access_lf, "token": create_access_token(identity=current_user, fresh=True), }, - "time_zone_info": "utc", + "time_zone_info": CONFIG.TIME_ZONE, } diff --git a/task_office/core/validators.py b/task_office/core/validators.py index cf6c777..cc16a24 100644 --- a/task_office/core/validators.py +++ b/task_office/core/validators.py @@ -1,5 +1,5 @@ import typing - +from flask_babel import lazy_gettext as _ from marshmallow import ValidationError from marshmallow.validate import Validator @@ -7,7 +7,7 @@ class Unique(Validator): """Validator which is entity exists by some field.""" - already_exists = "Already exists with value {}" + already_exists = _("Already exists with value {}") def __init__(self, model: object, field_name: str): diff --git a/task_office/extensions.py b/task_office/extensions.py index e368efd..82b79db 100644 --- a/task_office/extensions.py +++ b/task_office/extensions.py @@ -1,14 +1,17 @@ # -*- coding: utf-8 -*- """Extensions module. Each extension is initialized in the app factory located in app.py.""" - +from flask import g, request +from flask_babel import Babel from flask_bcrypt import Bcrypt from flask_caching import Cache from flask_cors import CORS from flask_jwt_extended import JWTManager - from flask_migrate import Migrate from flask_sqlalchemy import SQLAlchemy, Model +from task_office.settings import CONFIG +from task_office.utils import jwt_identity, identity_loader + class CRUDMixin(Model): """Mixin that adds convenience methods for CRUD (create, read, update, delete) operations.""" @@ -44,8 +47,29 @@ def delete(self, commit=True): cache = Cache() cors = CORS() -from task_office.utils import jwt_identity, identity_loader # noqa - +# JWT +# ------------------------------------------------------------------------------ jwt = JWTManager() jwt.user_loader_callback_loader(jwt_identity) jwt.user_identity_loader(identity_loader) + +# Babel +# https://pythonhosted.org/Flask-Babel/ +# ------------------------------------------------------------------------------ +babel = Babel(default_locale=CONFIG.LOCALE, default_timezone=CONFIG.TIME_ZONE) + + +@babel.localeselector +def get_locale(): + # if a user is logged in, use the locale from the user settings + user = getattr(g, "user", None) + if user is not None: + return user.locale + return request.accept_languages.best_match(list(CONFIG.LANGUAGES.keys())) + + +@babel.timezoneselector +def get_timezone(): + user = getattr(g, "user", None) + if user is not None: + return user.timezone diff --git a/task_office/settings.py b/task_office/settings.py index a1f6c7d..c0d567b 100644 --- a/task_office/settings.py +++ b/task_office/settings.py @@ -30,6 +30,13 @@ class Config(object): FLASK_DEBUG = env.int("FLASK_DEBUG", 0) DEBUG_TB_INTERCEPT_REDIRECTS = False + TIME_ZONE = "UTC" + LANGUAGES = {"ru": "Russian", "en": "English", "uk": "Ukrainian"} + LOCALE = "en" + + # https://pythonhosted.org/Flask-Babel/ + BABEL_TRANSLATION_DIRECTORIES = os.path.join(PROJECT_ROOT, "translations") + # Static settings STATIC_DIR = os.path.abspath(os.path.join(PROJECT_ROOT, "static")) STATIC_URL = API_V1_PREFIX + "/static" diff --git a/translations/en/LC_MESSAGES/messages.po b/translations/en/LC_MESSAGES/messages.po new file mode 100644 index 0000000..8d77a9f --- /dev/null +++ b/translations/en/LC_MESSAGES/messages.po @@ -0,0 +1,48 @@ +# English translations for PROJECT. +# Copyright (C) 2020 ORGANIZATION +# This file is distributed under the same license as the PROJECT project. +# FIRST AUTHOR , 2020. +# +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2020-01-04 15:41+0200\n" +"PO-Revision-Date: 2020-01-04 15:13+0200\n" +"Last-Translator: FULL NAME \n" +"Language: en\n" +"Language-Team: en \n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.8.0\n" + +#: task_office/auth/jwt_error_handlers.py:44 +msgid "Token has been revoked" +msgstr "Token has been revoked" + +#: task_office/auth/jwt_error_handlers.py:48 +msgid "Fresh token required" +msgstr "Fresh token required" + +#: task_office/auth/jwt_error_handlers.py:57 +msgid "Error loading the user {}" +msgstr "Error loading the user {}" + +#: task_office/auth/jwt_error_handlers.py:62 +msgid "User claims verification failed" +msgstr "User claims verification failed" + +#: task_office/auth/serializers.py:30 +msgid "Passwords do not match" +msgstr "Passwords do not match" + +#: task_office/auth/serializers.py:60 +msgid "User not found" +msgstr "User not found" + +#: task_office/core/validators.py:10 +msgid "Already exists with value {}" +msgstr "Already exists with value {}" + diff --git a/translations/ru/LC_MESSAGES/messages.po b/translations/ru/LC_MESSAGES/messages.po new file mode 100644 index 0000000..0d97921 --- /dev/null +++ b/translations/ru/LC_MESSAGES/messages.po @@ -0,0 +1,49 @@ +# Russian translations for PROJECT. +# Copyright (C) 2020 ORGANIZATION +# This file is distributed under the same license as the PROJECT project. +# FIRST AUTHOR , 2020. +# +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2020-01-04 15:41+0200\n" +"PO-Revision-Date: 2020-01-04 15:13+0200\n" +"Last-Translator: FULL NAME \n" +"Language: ru\n" +"Language-Team: ru \n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.8.0\n" + +#: task_office/auth/jwt_error_handlers.py:44 +msgid "Token has been revoked" +msgstr "Токен был отменен" + +#: task_office/auth/jwt_error_handlers.py:48 +msgid "Fresh token required" +msgstr "Фреш токен необходим" + +#: task_office/auth/jwt_error_handlers.py:57 +msgid "Error loading the user {}" +msgstr "Ошибка загрузки пользователя" + +#: task_office/auth/jwt_error_handlers.py:62 +msgid "User claims verification failed" +msgstr "Верификация данных пользователя провалена" + +#: task_office/auth/serializers.py:30 +msgid "Passwords do not match" +msgstr "Пароли не совпадают" + +#: task_office/auth/serializers.py:60 +msgid "User not found" +msgstr "Пользователь не найден" + +#: task_office/core/validators.py:10 +msgid "Already exists with value {}" +msgstr "Уже сужествует из значением {}" + diff --git a/translations/uk/LC_MESSAGES/messages.po b/translations/uk/LC_MESSAGES/messages.po new file mode 100644 index 0000000..132cc0e --- /dev/null +++ b/translations/uk/LC_MESSAGES/messages.po @@ -0,0 +1,49 @@ +# Ukrainian translations for PROJECT. +# Copyright (C) 2020 ORGANIZATION +# This file is distributed under the same license as the PROJECT project. +# FIRST AUTHOR , 2020. +# +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2020-01-04 15:41+0200\n" +"PO-Revision-Date: 2020-01-04 15:13+0200\n" +"Last-Translator: FULL NAME \n" +"Language: uk\n" +"Language-Team: uk \n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.8.0\n" + +#: task_office/auth/jwt_error_handlers.py:44 +msgid "Token has been revoked" +msgstr "Токен був відхилений" + +#: task_office/auth/jwt_error_handlers.py:48 +msgid "Fresh token required" +msgstr "Фреш токен необхідно" + +#: task_office/auth/jwt_error_handlers.py:57 +msgid "Error loading the user {}" +msgstr "Помилка завантаження користувача {}" + +#: task_office/auth/jwt_error_handlers.py:62 +msgid "User claims verification failed" +msgstr "Верифікація даних користувача провалена" + +#: task_office/auth/serializers.py:30 +msgid "Passwords do not match" +msgstr "Паролі не збігються" + +#: task_office/auth/serializers.py:60 +msgid "User not found" +msgstr "Користувач не знайдений" + +#: task_office/core/validators.py:10 +msgid "Already exists with value {}" +msgstr "Вже існує із значенням {}" + From 304e51d16362c9c2eb788e1bba8c6232070e8fb9 Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Sun, 5 Jan 2020 16:50:10 +0200 Subject: [PATCH 10/60] init boards app, improved PK mixin, created PK validator --- .../{5462f5f914dd_.py => 13ec0ac4d518_.py} | 30 ++++++++++++++++--- task_office/app.py | 3 +- task_office/boards/__init__.py | 3 ++ task_office/boards/models.py | 26 ++++++++++++++++ task_office/boards/serializers.py | 28 +++++++++++++++++ task_office/boards/views.py | 23 ++++++++++++++ task_office/core/models/mixins.py | 4 ++- task_office/core/validators.py | 26 ++++++++++++++++ task_office/database.py | 13 ++++++++ task_office/swagger/specs.py | 1 + 10 files changed, 151 insertions(+), 6 deletions(-) rename migrations/versions/{5462f5f914dd_.py => 13ec0ac4d518_.py} (53%) create mode 100644 task_office/boards/__init__.py create mode 100644 task_office/boards/models.py create mode 100644 task_office/boards/serializers.py create mode 100644 task_office/boards/views.py diff --git a/migrations/versions/5462f5f914dd_.py b/migrations/versions/13ec0ac4d518_.py similarity index 53% rename from migrations/versions/5462f5f914dd_.py rename to migrations/versions/13ec0ac4d518_.py index 7d614f6..5718b27 100644 --- a/migrations/versions/5462f5f914dd_.py +++ b/migrations/versions/13ec0ac4d518_.py @@ -1,8 +1,8 @@ """empty message -Revision ID: 5462f5f914dd +Revision ID: 13ec0ac4d518 Revises: -Create Date: 2019-12-17 10:11:52.139678 +Create Date: 2020-01-05 16:27:53.055312 """ from alembic import op @@ -10,7 +10,7 @@ from sqlalchemy.dialects import postgresql # revision identifiers, used by Alembic. -revision = "5462f5f914dd" +revision = "13ec0ac4d518" down_revision = None branch_labels = None depends_on = None @@ -26,7 +26,7 @@ def upgrade(): sa.Column("created_at", sa.DateTime(), nullable=False), sa.Column("updated_at", sa.DateTime(), nullable=False), sa.Column("username", sa.String(length=80), nullable=False), - sa.Column("email", sa.String(length=100), nullable=False), + sa.Column("email", sa.String(length=255), nullable=False), sa.Column("bio", sa.String(length=300), nullable=True), sa.Column("phone", sa.String(length=300), nullable=True), sa.Column("password", sa.Binary(), nullable=True), @@ -34,13 +34,35 @@ def upgrade(): sa.Column("is_superuser", sa.Boolean(), nullable=True), sa.PrimaryKeyConstraint("id", "uuid"), sa.UniqueConstraint("username"), + sa.UniqueConstraint("uuid"), ) op.create_index(op.f("ix_users_email"), "users", ["email"], unique=True) + op.create_table( + "boards", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("uuid", postgresql.UUID(), nullable=False), + sa.Column("meta", sa.JSON(), nullable=True), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.Column("updated_at", sa.DateTime(), nullable=False), + sa.Column("name", sa.String(length=80), nullable=False), + sa.Column("description", sa.String(length=255), nullable=True), + sa.Column("owner_uuid", postgresql.UUID(), nullable=False), + sa.Column("is_active", sa.Boolean(), nullable=True), + sa.ForeignKeyConstraint(["owner_uuid"], ["users.uuid"]), + sa.PrimaryKeyConstraint("id", "uuid"), + sa.UniqueConstraint("name", "owner_uuid", name="unique_name_owner_board"), + sa.UniqueConstraint("uuid"), + ) + op.create_index( + op.f("ix_boards_description"), "boards", ["description"], unique=False + ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f("ix_boards_description"), table_name="boards") + op.drop_table("boards") op.drop_index(op.f("ix_users_email"), table_name="users") op.drop_table("users") # ### end Alembic commands ### diff --git a/task_office/app.py b/task_office/app.py index e08a269..7dbedc4 100644 --- a/task_office/app.py +++ b/task_office/app.py @@ -3,7 +3,7 @@ from flask import Flask from task_office.auth.jwt_error_handlers import jwt_errors_map -from task_office import commands, auth, swagger +from task_office import commands, auth, swagger, boards from task_office.exceptions import InvalidUsage from task_office.extensions import bcrypt, cache, db, migrate, cors, jwt, babel from task_office.settings import CONFIG @@ -46,6 +46,7 @@ def register_blueprints(app): origins = app.config.get("CORS_ORIGIN_WHITELIST", "*") cors.init_app(auth.views.blueprint, origins=origins) app.register_blueprint(auth.views.blueprint) + app.register_blueprint(boards.views.blueprint) if CONFIG.USE_DOCS: app.register_blueprint(swagger.views.blueprint_swagger, url_prefix=SWAGGER_URL) app.register_blueprint(swagger.views.blueprint) diff --git a/task_office/boards/__init__.py b/task_office/boards/__init__.py new file mode 100644 index 0000000..67ce501 --- /dev/null +++ b/task_office/boards/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from .views import * diff --git a/task_office/boards/models.py b/task_office/boards/models.py new file mode 100644 index 0000000..b2d6813 --- /dev/null +++ b/task_office/boards/models.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +"""Boards models.""" +from task_office.core.models.mixins import DTMixin, PKMixin +from task_office.database import Column, Model, db, reference_col, relationship + + +class Board(PKMixin, DTMixin, Model): + + __tablename__ = "boards" + __table_args__ = ( + db.UniqueConstraint("name", "owner_uuid", name="unique_name_owner_board"), + ) + + name = Column(db.String(80), nullable=False) + description = Column(db.String(255), unique=False, nullable=True, index=True) + owner_uuid = reference_col("users", pk_name="uuid", nullable=False) + owner = relationship("User", backref=db.backref("boardds")) + is_active = Column(db.Boolean(), default=True) + + def __init__(self, *args, **kwargs): + """Create instance.""" + db.Model.__init__(self, *args, **kwargs) + + def __repr__(self): + """Represent instance as a unique string.""" + return "".format(name=self.name) diff --git a/task_office/boards/serializers.py b/task_office/boards/serializers.py new file mode 100644 index 0000000..82630dc --- /dev/null +++ b/task_office/boards/serializers.py @@ -0,0 +1,28 @@ +# coding: utf-8 +from marshmallow import fields, validates_schema +from marshmallow.validate import Length + +from task_office.auth import User +from task_office.core.serializers import BaseSchema +from task_office.core.validators import PK_Exists +from task_office.swagger import API_SPEC + + +class BoardInSchema(BaseSchema): + name = fields.Str(required=True, allow_none=False, validate=[Length(max=255)]) + description = fields.Str(allow_none=True, required=False, default="") + owner_uuid = fields.UUID(required=True, validate=[PK_Exists(User, "uuid")]) + is_active = fields.Boolean(default=True) + + class Meta: + strict = True + + @validates_schema + def validate_schema(self, data, **kwargs): + data["owner_uuid"] = str(data.pop("owner_uuid")) + + +board_schema = BoardInSchema() + + +API_SPEC.components.schema("BoardInSchema", schema=BoardInSchema) diff --git a/task_office/boards/views.py b/task_office/boards/views.py new file mode 100644 index 0000000..cd2c67a --- /dev/null +++ b/task_office/boards/views.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +"""Boards views.""" + +from flask import Blueprint +from flask_apispec import use_kwargs, marshal_with +from flask_jwt_extended import jwt_required + +from .models import Board +from .serializers import board_schema +from ..settings import CONFIG + +blueprint = Blueprint("boards", __name__, url_prefix=CONFIG.API_V1_PREFIX + "/boards") + + +@blueprint.route("", methods=("post",)) +@jwt_required +@use_kwargs(board_schema) +@marshal_with(board_schema) +def create_boards(**kwargs): + data = kwargs + board = Board(**data) + board.save() + return board diff --git a/task_office/core/models/mixins.py b/task_office/core/models/mixins.py index 441922a..d50aeff 100644 --- a/task_office/core/models/mixins.py +++ b/task_office/core/models/mixins.py @@ -17,7 +17,9 @@ class PKMixin(object): __table_args__ = {"extend_existing": True} id = db.Column(db.Integer, primary_key=True, autoincrement=True) - uuid = db.Column(UUID, primary_key=True, default=uuid.uuid4().__str__()) + uuid = db.Column( + UUID, primary_key=True, unique=True, default=lambda: uuid.uuid4().__str__() + ) meta = db.Column(JSON, default=dict) @classmethod diff --git a/task_office/core/validators.py b/task_office/core/validators.py index cc16a24..02c0d23 100644 --- a/task_office/core/validators.py +++ b/task_office/core/validators.py @@ -29,3 +29,29 @@ def __call__(self, value) -> typing.Any: message = self.already_exists raise ValidationError(self._format_error(value, message)) return value + + +class PK_Exists(Validator): + """Validator of entity pk.""" + + not_found = _("Not found with value {}") + + def __init__(self, model: object, field_name: str = "uuid"): + + self.model = model + self.field_name = field_name + + def _repr_args(self) -> str: + return "model={!r}, field_name={!r}".format(self.model, self.field_name) + + def _format_error(self, value, message: str) -> str: + return message.format(value) + + def __call__(self, value) -> typing.Any: + param = {self.field_name: str(value)} + obj = self.model.query.filter_by(**param).first() + + if not obj: + message = self.not_found + raise ValidationError(self._format_error(value, message)) + return value diff --git a/task_office/database.py b/task_office/database.py index fc4838b..60ce0ca 100644 --- a/task_office/database.py +++ b/task_office/database.py @@ -9,3 +9,16 @@ Column = db.Column relationship = relationship Model = db.Model + + +def reference_col(tablename, nullable=False, pk_name="uuid", **kwargs): + """Column that adds primary key foreign key reference. + + Usage: :: + + category_id = reference_col('category') + category = relationship('Category', backref='categories') + """ + return db.Column( + db.ForeignKey("{0}.{1}".format(tablename, pk_name)), nullable=nullable, **kwargs + ) diff --git a/task_office/swagger/specs.py b/task_office/swagger/specs.py index a950c39..1d89e38 100644 --- a/task_office/swagger/specs.py +++ b/task_office/swagger/specs.py @@ -13,3 +13,4 @@ ) # TODO(Medniy) wait for a DocumentedBlueprint and make urls generation +# https://github.com/marshmallow-code/apispec-webframeworks/pull/27 From 90b4c363e41f58a8c58794025b88145a84aa1e1b Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Mon, 6 Jan 2020 16:39:07 +0200 Subject: [PATCH 11/60] init permissions app --- migrations/versions/2c4e91200810_.py | 45 ++++++++++++++++++++++ requirements/base.txt | 1 + task_office/app.py | 3 +- task_office/core/enums.py | 32 ++++++++++++++++ task_office/database.py | 1 + task_office/permissions/__init__.py | 3 ++ task_office/permissions/models.py | 37 ++++++++++++++++++ task_office/permissions/serializers.py | 53 ++++++++++++++++++++++++++ task_office/permissions/views.py | 25 ++++++++++++ 9 files changed, 199 insertions(+), 1 deletion(-) create mode 100644 migrations/versions/2c4e91200810_.py create mode 100644 task_office/core/enums.py create mode 100644 task_office/permissions/__init__.py create mode 100644 task_office/permissions/models.py create mode 100644 task_office/permissions/serializers.py create mode 100644 task_office/permissions/views.py diff --git a/migrations/versions/2c4e91200810_.py b/migrations/versions/2c4e91200810_.py new file mode 100644 index 0000000..b0cc1fe --- /dev/null +++ b/migrations/versions/2c4e91200810_.py @@ -0,0 +1,45 @@ +"""empty message + +Revision ID: 2c4e91200810 +Revises: 13ec0ac4d518 +Create Date: 2020-01-06 15:48:01.464419 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = "2c4e91200810" +down_revision = "13ec0ac4d518" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "permissions", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("uuid", postgresql.UUID(), nullable=False), + sa.Column("meta", sa.JSON(), nullable=True), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.Column("updated_at", sa.DateTime(), nullable=False), + sa.Column("role", sa.Integer(), nullable=True), + sa.Column("user_uuid", postgresql.UUID(), nullable=False), + sa.Column("board_uuid", postgresql.UUID(), nullable=False), + sa.ForeignKeyConstraint(["board_uuid"], ["boards.uuid"]), + sa.ForeignKeyConstraint(["user_uuid"], ["users.uuid"]), + sa.PrimaryKeyConstraint("id", "uuid"), + sa.UniqueConstraint( + "board_uuid", "user_uuid", name="unique_board_owner_permission" + ), + sa.UniqueConstraint("uuid"), + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("permissions") + # ### end Alembic commands ### diff --git a/requirements/base.txt b/requirements/base.txt index 1112f11..626aeca 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -8,6 +8,7 @@ Flask_SQLAlchemy==2.2 SQLAlchemy==1.1.9 psycopg2 marshmallow +marshmallow-enum==1.5.1 MarkupSafe==1.1.1 gunicorn uuid diff --git a/task_office/app.py b/task_office/app.py index 7dbedc4..270a67d 100644 --- a/task_office/app.py +++ b/task_office/app.py @@ -3,7 +3,7 @@ from flask import Flask from task_office.auth.jwt_error_handlers import jwt_errors_map -from task_office import commands, auth, swagger, boards +from task_office import commands, auth, swagger, boards, permissions from task_office.exceptions import InvalidUsage from task_office.extensions import bcrypt, cache, db, migrate, cors, jwt, babel from task_office.settings import CONFIG @@ -47,6 +47,7 @@ def register_blueprints(app): cors.init_app(auth.views.blueprint, origins=origins) app.register_blueprint(auth.views.blueprint) app.register_blueprint(boards.views.blueprint) + app.register_blueprint(permissions.views.blueprint) if CONFIG.USE_DOCS: app.register_blueprint(swagger.views.blueprint_swagger, url_prefix=SWAGGER_URL) app.register_blueprint(swagger.views.blueprint) diff --git a/task_office/core/enums.py b/task_office/core/enums.py new file mode 100644 index 0000000..d436aa4 --- /dev/null +++ b/task_office/core/enums.py @@ -0,0 +1,32 @@ +from enum import Enum +from flask_babel import lazy_gettext as _ + + +class XEnum(Enum): + """ + Base Custom Enum for project + """ + + # ADMIN = 0, _('Admin'), _('Approximate quantity: format +/-') + # EDITOR = 1, _('Editor'), _('Strict, exact quantity: format numeric') + + @classmethod + def choices(cls): + return [(tag.name, tag.value) for tag in cls] + + @classmethod + def get_names(cls): + return [item.name for item in cls] + + def __new__(cls, value, name, description): + member = object.__new__(cls) + member._value_ = value + member.fullname = name + member.description = description + return member + + def __int__(self): + return self.value + + def __str__(self): + return self.value diff --git a/task_office/database.py b/task_office/database.py index 60ce0ca..2511095 100644 --- a/task_office/database.py +++ b/task_office/database.py @@ -7,6 +7,7 @@ # Alias common SQLAlchemy names Column = db.Column +Enum = db.Enum relationship = relationship Model = db.Model diff --git a/task_office/permissions/__init__.py b/task_office/permissions/__init__.py new file mode 100644 index 0000000..67ce501 --- /dev/null +++ b/task_office/permissions/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from .views import * diff --git a/task_office/permissions/models.py b/task_office/permissions/models.py new file mode 100644 index 0000000..e27ef94 --- /dev/null +++ b/task_office/permissions/models.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +"""Permission models.""" + +from flask_babel import lazy_gettext as _ + +from task_office.core.enums import XEnum +from task_office.core.models.mixins import DTMixin, PKMixin +from task_office.database import Column, Model, db, reference_col, relationship + + +class Permission(PKMixin, DTMixin, Model): + + __tablename__ = "permissions" + __table_args__ = ( + db.UniqueConstraint( + "board_uuid", "user_uuid", name="unique_board_owner_permission" + ), + ) + + class Role(XEnum): + OWNER = 9, _("Owner"), _("Owner of board(creator)") + EDITOR = 8, _("Editor"), _("Editor of board") + STAFF = 7, _("Staff"), _("Ordinary user") + + role = Column(db.Integer(), default=Role.STAFF.value) + user_uuid = reference_col("users", pk_name="uuid", nullable=False) + user = relationship("User", backref=db.backref("perms")) + board_uuid = reference_col("boards", pk_name="uuid", nullable=False) + board = relationship("Board", backref=db.backref("perms")) + + def __init__(self, *args, **kwargs): + """Create instance.""" + db.Model.__init__(self, *args, **kwargs) + + def __repr__(self): + """Represent instance as a unique string.""" + return "".format(name=self.uuid) diff --git a/task_office/permissions/serializers.py b/task_office/permissions/serializers.py new file mode 100644 index 0000000..ec7efb1 --- /dev/null +++ b/task_office/permissions/serializers.py @@ -0,0 +1,53 @@ +# coding: utf-8 +from flask_babel import lazy_gettext as _ +from marshmallow import fields, validates_schema +from marshmallow_enum import EnumField + +from task_office.auth import User +from task_office.boards import Board +from task_office.core.serializers import BaseSchema +from task_office.core.validators import PK_Exists +from task_office.exceptions import InvalidUsage +from task_office.permissions.models import Permission +from task_office.swagger import API_SPEC + + +class PermissionInSchema(BaseSchema): + role = EnumField(Permission.Role, required=True, by_value=True) + board_uuid = fields.UUID( + required=True, validate=[PK_Exists(Board, "uuid")], allow_none=False + ) + user_uuid = fields.UUID( + required=True, validate=[PK_Exists(User, "uuid")], allow_none=False + ) + + class Meta: + strict = True + + @validates_schema + def validate_schema(self, data, **kwargs): + data["board_uuid"] = str(data.pop("board_uuid")) + data["user_uuid"] = str(data.pop("user_uuid")) + data["role"] = data.pop("role").value + obj = Permission.query.filter_by( + board_uuid=data["board_uuid"], user_uuid=data["user_uuid"] + ).first() + if obj: + raise InvalidUsage(messages=[_("Already exists")], status_code=422) + + +class PermissionOutSchema(BaseSchema): + role = fields.Integer(dump_only=True) + board_uuid = fields.UUID(dump_only=True) + user_uuid = fields.UUID(dump_only=True) + + class Meta: + strict = True + + +permission_in_schema = PermissionInSchema() +permission_out_schema = PermissionOutSchema() + + +API_SPEC.components.schema("PermissionInSchema", schema=PermissionInSchema) +API_SPEC.components.schema("PermissionOutSchema", schema=PermissionOutSchema) diff --git a/task_office/permissions/views.py b/task_office/permissions/views.py new file mode 100644 index 0000000..7674f90 --- /dev/null +++ b/task_office/permissions/views.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +"""Boards views.""" + +from flask import Blueprint +from flask_apispec import use_kwargs, marshal_with +from flask_jwt_extended import jwt_required + +from .models import Permission +from .serializers import permission_in_schema, permission_out_schema +from ..settings import CONFIG + +blueprint = Blueprint( + "permissions", __name__, url_prefix=CONFIG.API_V1_PREFIX + "/permissions" +) + + +@blueprint.route("/", methods=("post",)) +@jwt_required +@use_kwargs(permission_in_schema) +@marshal_with(permission_out_schema) +def create_permission(**kwargs): + data = kwargs + permission = Permission(**data) + permission.save() + return permission From f25e536791236de5d8947233508a9a7b2004de8f Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Mon, 6 Jan 2020 16:54:05 +0200 Subject: [PATCH 12/60] implemented permissions meta --- task_office/core/enums.py | 9 ++++++++- task_office/permissions/views.py | 13 ++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/task_office/core/enums.py b/task_office/core/enums.py index d436aa4..5b90690 100644 --- a/task_office/core/enums.py +++ b/task_office/core/enums.py @@ -14,11 +14,18 @@ class XEnum(Enum): def choices(cls): return [(tag.name, tag.value) for tag in cls] + @classmethod + def dict_choices(cls): + return [ + {"name": tag.name, "value": tag.value, "description": tag.description} + for tag in cls + ] + @classmethod def get_names(cls): return [item.name for item in cls] - def __new__(cls, value, name, description): + def __new__(cls, value, name, description=""): member = object.__new__(cls) member._value_ = value member.fullname = name diff --git a/task_office/permissions/views.py b/task_office/permissions/views.py index 7674f90..e3eee5f 100644 --- a/task_office/permissions/views.py +++ b/task_office/permissions/views.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""Boards views.""" +"""Permissions views.""" from flask import Blueprint from flask_apispec import use_kwargs, marshal_with @@ -14,6 +14,17 @@ ) +@blueprint.route("/meta", methods=("get",)) +@jwt_required +def get_meta_data(): + """ + Additional data for Permissions + """ + data = dict() + data["roles"] = Permission.Role.dict_choices() + return data + + @blueprint.route("/", methods=("post",)) @jwt_required @use_kwargs(permission_in_schema) From 7be4d693a32c0fef63e58f45e6b1ca2d73d93c81 Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Tue, 7 Jan 2020 22:06:29 +0200 Subject: [PATCH 13/60] init custom pagination, init list of permissions --- task_office/core/enums.py | 5 +++ task_office/core/helpers/__init__.py | 0 task_office/core/helpers/listed_response.py | 36 +++++++++++++++++++ task_office/core/serializers.py | 21 ++++++++++- task_office/permissions/models.py | 2 +- task_office/permissions/schemas/__init__.py | 0 .../basic_schemas.py} | 34 ++++++++++++++++-- task_office/permissions/views.py | 22 ++++++++++-- task_office/settings.py | 3 ++ 9 files changed, 116 insertions(+), 7 deletions(-) create mode 100644 task_office/core/helpers/__init__.py create mode 100644 task_office/core/helpers/listed_response.py create mode 100644 task_office/permissions/schemas/__init__.py rename task_office/permissions/{serializers.py => schemas/basic_schemas.py} (63%) diff --git a/task_office/core/enums.py b/task_office/core/enums.py index 5b90690..25e9113 100644 --- a/task_office/core/enums.py +++ b/task_office/core/enums.py @@ -37,3 +37,8 @@ def __int__(self): def __str__(self): return self.value + + +class OrderingDirection(XEnum): + ASC = "asc", _("Ascend") + DESC = "desc", _("Descend") diff --git a/task_office/core/helpers/__init__.py b/task_office/core/helpers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/task_office/core/helpers/listed_response.py b/task_office/core/helpers/listed_response.py new file mode 100644 index 0000000..d9bf85d --- /dev/null +++ b/task_office/core/helpers/listed_response.py @@ -0,0 +1,36 @@ +class ListedResponseHelper: + RESPONSE_TEMPLATE = {"count": 0, "results": []} + + @staticmethod + def _get_query_ordered(query, order_param): + if order_param: + query = query.order_by(order_param.fullname) + return query + + @staticmethod + def _get_query_filtered(query, filter_params): + if filter_params: + query.filter(**filter_params) + return query + + @staticmethod + def _get_query_paginated(query, limit, offset): + if offset > 0: + query = query.offset(offset) + query = query.limit(limit) + return query + + def serialize(self, query, query_params, schema): + query = self._get_query_filtered(query, query_params.get("searching", {})) + query = self._get_query_ordered(query, query_params.get("ordering", "")) + query = self._get_query_paginated( + query, query_params.get("limit"), query_params.get("offset") + ) + count = query.count() + data = dict(self.RESPONSE_TEMPLATE) + data["count"] = count + data["results"] = schema.dump(query) + return data + + +listed_response = ListedResponseHelper() diff --git a/task_office/core/serializers.py b/task_office/core/serializers.py index c67708c..d84c560 100644 --- a/task_office/core/serializers.py +++ b/task_office/core/serializers.py @@ -1,6 +1,7 @@ import logging -from marshmallow import Schema, fields +from flask_babel import lazy_gettext as _ +from marshmallow import Schema, fields, validates_schema from task_office.exceptions import InvalidUsage from task_office.settings import CONFIG @@ -43,3 +44,21 @@ class BaseSchema(XSchema): class Meta: strict = True + + +class ListInSchema(XSchema): + error_messages = { + "max_limit_exceeded": _("Max limit {} exceeded").format(CONFIG.MAX_LIMIT_VALUE) + } + + limit = fields.Integer(default=CONFIG.DEFAULT_OFFSET_VALUE, required=True) + offset = fields.Integer(default=0, required=True) + + @validates_schema + def validate_schema(self, data, **kwargs): + limit = data["limit"] + if limit > CONFIG.MAX_LIMIT_VALUE: + self.throw_error(value="", key_error="max_limit_exceeded", code=400) + + +list_in_schema = ListInSchema() diff --git a/task_office/permissions/models.py b/task_office/permissions/models.py index e27ef94..b6a17b3 100644 --- a/task_office/permissions/models.py +++ b/task_office/permissions/models.py @@ -34,4 +34,4 @@ def __init__(self, *args, **kwargs): def __repr__(self): """Represent instance as a unique string.""" - return "".format(name=self.uuid) + return "".format(self.uuid) diff --git a/task_office/permissions/schemas/__init__.py b/task_office/permissions/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/task_office/permissions/serializers.py b/task_office/permissions/schemas/basic_schemas.py similarity index 63% rename from task_office/permissions/serializers.py rename to task_office/permissions/schemas/basic_schemas.py index ec7efb1..54570d7 100644 --- a/task_office/permissions/serializers.py +++ b/task_office/permissions/schemas/basic_schemas.py @@ -5,7 +5,8 @@ from task_office.auth import User from task_office.boards import Board -from task_office.core.serializers import BaseSchema +from task_office.core.enums import XEnum, OrderingDirection +from task_office.core.serializers import BaseSchema, ListInSchema, XSchema from task_office.core.validators import PK_Exists from task_office.exceptions import InvalidUsage from task_office.permissions.models import Permission @@ -47,7 +48,34 @@ class Meta: permission_in_schema = PermissionInSchema() permission_out_schema = PermissionOutSchema() - - +permission_list_out_schema = PermissionOutSchema(many=True) API_SPEC.components.schema("PermissionInSchema", schema=PermissionInSchema) API_SPEC.components.schema("PermissionOutSchema", schema=PermissionOutSchema) + + +class PermissionInListSchema(ListInSchema): + class OrderingMap(XEnum): + CREATED_AT_ASC = ( + "-created_at", + Permission.created_at.asc(), + OrderingDirection.ASC, + ) + CREATED_AET_DESC = ( + "created_at", + Permission.created_at.desc(), + OrderingDirection.DESC, + ) + + # board_uuid = fields.UUID( + # required=True, validate=[PK_Exists(Board, "uuid")], allow_none=False + # ) + + searching = fields.Nested(XSchema, required=False) + ordering = EnumField(OrderingMap, required=False, by_value=True) + + class Meta: + strict = True + + +permissions_in_list_schema = PermissionInListSchema() +API_SPEC.components.schema("PermissionInListSchema", schema=PermissionInListSchema) diff --git a/task_office/permissions/views.py b/task_office/permissions/views.py index e3eee5f..a5c344a 100644 --- a/task_office/permissions/views.py +++ b/task_office/permissions/views.py @@ -6,7 +6,13 @@ from flask_jwt_extended import jwt_required from .models import Permission -from .serializers import permission_in_schema, permission_out_schema +from .schemas.basic_schemas import ( + permission_in_schema, + permission_out_schema, + permissions_in_list_schema, + permission_list_out_schema, +) +from ..core.helpers.listed_response import listed_response from ..settings import CONFIG blueprint = Blueprint( @@ -25,7 +31,7 @@ def get_meta_data(): return data -@blueprint.route("/", methods=("post",)) +@blueprint.route("", methods=("post",)) @jwt_required @use_kwargs(permission_in_schema) @marshal_with(permission_out_schema) @@ -34,3 +40,15 @@ def create_permission(**kwargs): permission = Permission(**data) permission.save() return permission + + +@blueprint.route("", methods=("get",)) +@jwt_required +@use_kwargs(permissions_in_list_schema) +def get_list_permission(**kwargs): + data = kwargs + query = Permission.query + data = listed_response.serialize( + query=query, query_params=data, schema=permission_list_out_schema + ) + return data diff --git a/task_office/settings.py b/task_office/settings.py index c0d567b..1c95801 100644 --- a/task_office/settings.py +++ b/task_office/settings.py @@ -54,6 +54,9 @@ class Config(object): JWT_REFRESH_TOKEN_EXPIRES = timedelta(days=env.int("JWT_REFRESH_TOKEN_EXPIRES", 7)) JWT_BLACKLIST_TOKEN_CHECKS = ["access", "refresh"] + DEFAULT_OFFSET_VALUE = 0 + MAX_LIMIT_VALUE = 50 + # DB DATABASE = { "DB_NAME": env.str("POSTGRES_DB", "task_office"), From 5740a0bfca9cce3e6491c1341fabbc09ef9df38c Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Thu, 9 Jan 2020 21:43:39 +0200 Subject: [PATCH 14/60] renamed serializers to schemas, improved boards, permissions --- .../auth/{serializers.py => schemas.py} | 2 +- task_office/auth/views.py | 2 +- .../boards/{serializers.py => schemas.py} | 2 +- task_office/boards/views.py | 7 +++-- .../core/{serializers.py => schemas.py} | 13 +++++---- task_office/core/utils.py | 9 ++++++ .../permissions/schemas/basic_schemas.py | 8 +++--- task_office/permissions/views.py | 28 ++++++++++++++----- task_office/settings.py | 1 + task_office/utils.py | 2 +- 10 files changed, 52 insertions(+), 22 deletions(-) rename task_office/auth/{serializers.py => schemas.py} (98%) rename task_office/boards/{serializers.py => schemas.py} (94%) rename task_office/core/{serializers.py => schemas.py} (84%) create mode 100644 task_office/core/utils.py diff --git a/task_office/auth/serializers.py b/task_office/auth/schemas.py similarity index 98% rename from task_office/auth/serializers.py rename to task_office/auth/schemas.py index c8f0e4b..77fdd5c 100644 --- a/task_office/auth/serializers.py +++ b/task_office/auth/schemas.py @@ -4,7 +4,7 @@ from marshmallow.validate import Length from task_office.auth.models import User -from task_office.core.serializers import BaseSchema, XSchema +from task_office.core.schemas import BaseSchema, XSchema from task_office.core.validators import Unique from task_office.swagger import API_SPEC diff --git a/task_office/auth/views.py b/task_office/auth/views.py index 81f4196..8cab598 100644 --- a/task_office/auth/views.py +++ b/task_office/auth/views.py @@ -12,7 +12,7 @@ ) from .models import User -from .serializers import ( +from .schemas import ( user_schema, user_signup_schema, user_signin_schema, diff --git a/task_office/boards/serializers.py b/task_office/boards/schemas.py similarity index 94% rename from task_office/boards/serializers.py rename to task_office/boards/schemas.py index 82630dc..29a6136 100644 --- a/task_office/boards/serializers.py +++ b/task_office/boards/schemas.py @@ -3,7 +3,7 @@ from marshmallow.validate import Length from task_office.auth import User -from task_office.core.serializers import BaseSchema +from task_office.core.schemas import BaseSchema from task_office.core.validators import PK_Exists from task_office.swagger import API_SPEC diff --git a/task_office/boards/views.py b/task_office/boards/views.py index cd2c67a..1e46c29 100644 --- a/task_office/boards/views.py +++ b/task_office/boards/views.py @@ -6,10 +6,13 @@ from flask_jwt_extended import jwt_required from .models import Board -from .serializers import board_schema +from .schemas import board_schema from ..settings import CONFIG -blueprint = Blueprint("boards", __name__, url_prefix=CONFIG.API_V1_PREFIX + "/boards") +BOARDS_PREFIX = CONFIG.API_V1_PREFIX + "/boards" +BOARD_RETRIEVE_URL = BOARDS_PREFIX + "/" + +blueprint = Blueprint("boards", __name__, url_prefix=BOARDS_PREFIX) @blueprint.route("", methods=("post",)) diff --git a/task_office/core/serializers.py b/task_office/core/schemas.py similarity index 84% rename from task_office/core/serializers.py rename to task_office/core/schemas.py index d84c560..fa19605 100644 --- a/task_office/core/serializers.py +++ b/task_office/core/schemas.py @@ -1,7 +1,8 @@ import logging +import uuid from flask_babel import lazy_gettext as _ -from marshmallow import Schema, fields, validates_schema +from marshmallow import Schema, fields, validates_schema, post_dump from task_office.exceptions import InvalidUsage from task_office.settings import CONFIG @@ -42,8 +43,10 @@ class BaseSchema(XSchema): ) uuid = fields.UUID(dump_only=True) - class Meta: - strict = True + @post_dump + def dump_data(self, data, **kwargs): + data["uuid"] = uuid.UUID(data.pop("uuid")).hex + return data class ListInSchema(XSchema): @@ -51,8 +54,8 @@ class ListInSchema(XSchema): "max_limit_exceeded": _("Max limit {} exceeded").format(CONFIG.MAX_LIMIT_VALUE) } - limit = fields.Integer(default=CONFIG.DEFAULT_OFFSET_VALUE, required=True) - offset = fields.Integer(default=0, required=True) + limit = fields.Integer(default=CONFIG.DEFAULT_LIMIT_VALUE, required=True) + offset = fields.Integer(default=CONFIG.DEFAULT_OFFSET_VALUE, required=True) @validates_schema def validate_schema(self, data, **kwargs): diff --git a/task_office/core/utils.py b/task_office/core/utils.py new file mode 100644 index 0000000..4ab80aa --- /dev/null +++ b/task_office/core/utils.py @@ -0,0 +1,9 @@ +from uuid import UUID + + +def is_uuid(uuid): + try: + UUID(uuid).version + return True + except ValueError: + return False diff --git a/task_office/permissions/schemas/basic_schemas.py b/task_office/permissions/schemas/basic_schemas.py index 54570d7..03baf4b 100644 --- a/task_office/permissions/schemas/basic_schemas.py +++ b/task_office/permissions/schemas/basic_schemas.py @@ -6,7 +6,7 @@ from task_office.auth import User from task_office.boards import Board from task_office.core.enums import XEnum, OrderingDirection -from task_office.core.serializers import BaseSchema, ListInSchema, XSchema +from task_office.core.schemas import BaseSchema, ListInSchema, XSchema from task_office.core.validators import PK_Exists from task_office.exceptions import InvalidUsage from task_office.permissions.models import Permission @@ -66,9 +66,9 @@ class OrderingMap(XEnum): OrderingDirection.DESC, ) - # board_uuid = fields.UUID( - # required=True, validate=[PK_Exists(Board, "uuid")], allow_none=False - # ) + board_uuid = fields.UUID( + required=True, validate=[PK_Exists(Board, "uuid")], allow_none=False + ) searching = fields.Nested(XSchema, required=False) ordering = EnumField(OrderingMap, required=False, by_value=True) diff --git a/task_office/permissions/views.py b/task_office/permissions/views.py index a5c344a..6b9623e 100644 --- a/task_office/permissions/views.py +++ b/task_office/permissions/views.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- """Permissions views.""" - -from flask import Blueprint +from flask import Blueprint, request from flask_apispec import use_kwargs, marshal_with +from flask_babel import lazy_gettext as _ from flask_jwt_extended import jwt_required from .models import Permission @@ -12,20 +12,30 @@ permissions_in_list_schema, permission_list_out_schema, ) +from ..boards import BOARD_RETRIEVE_URL from ..core.helpers.listed_response import listed_response -from ..settings import CONFIG +from ..core.utils import is_uuid +from ..exceptions import InvalidUsage blueprint = Blueprint( - "permissions", __name__, url_prefix=CONFIG.API_V1_PREFIX + "/permissions" + "permissions", __name__, url_prefix=BOARD_RETRIEVE_URL + "/permissions" ) @blueprint.route("/meta", methods=("get",)) @jwt_required -def get_meta_data(): +def get_meta_data(board_uuid): """ Additional data for Permissions """ + + if not is_uuid(board_uuid): + raise InvalidUsage(messages=[_("Not found")], status_code=404) + + perm = Permission.query.filter_by(board_uuid=board_uuid).first() + if not perm: + raise InvalidUsage(messages=[_("Not found")], status_code=404) + data = dict() data["roles"] = Permission.Role.dict_choices() return data @@ -37,6 +47,8 @@ def get_meta_data(): @marshal_with(permission_out_schema) def create_permission(**kwargs): data = kwargs + if data["board_uuid"] not in request.url: + raise InvalidUsage(messages=[_("Not found")], status_code=404) permission = Permission(**data) permission.save() return permission @@ -47,8 +59,10 @@ def create_permission(**kwargs): @use_kwargs(permissions_in_list_schema) def get_list_permission(**kwargs): data = kwargs - query = Permission.query + if str(data["board_uuid"]) not in request.url: + raise InvalidUsage(messages=[_("Not found")], status_code=404) + data = listed_response.serialize( - query=query, query_params=data, schema=permission_list_out_schema + query=Permission.query, query_params=data, schema=permission_list_out_schema ) return data diff --git a/task_office/settings.py b/task_office/settings.py index 1c95801..0c75efb 100644 --- a/task_office/settings.py +++ b/task_office/settings.py @@ -55,6 +55,7 @@ class Config(object): JWT_BLACKLIST_TOKEN_CHECKS = ["access", "refresh"] DEFAULT_OFFSET_VALUE = 0 + DEFAULT_LIMIT_VALUE = 15 MAX_LIMIT_VALUE = 50 # DB diff --git a/task_office/utils.py b/task_office/utils.py index aaf8ea6..83129ef 100644 --- a/task_office/utils.py +++ b/task_office/utils.py @@ -2,7 +2,7 @@ """Helper utilities and decorators.""" -def jwt_identity(payload): +def jwt_identity(identifier): from task_office.auth import User return User.get_by_id(payload) From e591a6a083eb81b488492ecc035d41a7e2fc9adf Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Sun, 12 Jan 2020 19:43:45 +0200 Subject: [PATCH 15/60] improved boards, permissions endpoints --- task_office/auth/schemas.py | 18 ++++- task_office/boards/constants.py | 4 + task_office/boards/models.py | 26 ------- task_office/boards/schemas.py | 40 +++++++++- task_office/boards/views.py | 64 +++++++++++++--- .../models.py => core/models/db_models.py} | 26 ++++++- .../permissions/schemas/basic_schemas.py | 31 +++----- task_office/permissions/views.py | 73 +++++++++++++++---- task_office/utils.py | 20 ++++- 9 files changed, 225 insertions(+), 77 deletions(-) create mode 100644 task_office/boards/constants.py delete mode 100644 task_office/boards/models.py rename task_office/{permissions/models.py => core/models/db_models.py} (61%) diff --git a/task_office/auth/schemas.py b/task_office/auth/schemas.py index 77fdd5c..54e9307 100644 --- a/task_office/auth/schemas.py +++ b/task_office/auth/schemas.py @@ -1,4 +1,6 @@ # coding: utf-8 +import uuid + from flask_babel import lazy_gettext as _ from marshmallow import fields, post_dump, validates_schema from marshmallow.validate import Length @@ -14,9 +16,19 @@ class UserSchema(BaseSchema): email = fields.Email(dump_only=True) bio = fields.Str(dump_only=True) + class Meta: + strict = True + + +class UserSchemaNested(XSchema): + uuid = fields.UUID(dump_only=True) + username = fields.Str(dump_only=True) + email = fields.Email(dump_only=True) + @post_dump - def dump_user(self, data, **kwargs): - return {"user": data} + def dump_data(self, data, **kwargs): + data["uuid"] = uuid.UUID(data.pop("uuid")).hex + return data class Meta: strict = True @@ -24,6 +36,7 @@ class Meta: user_schema = UserSchema() user_schemas = UserSchema(many=True) +user_schema_nested = UserSchemaNested() class UserSignUpSchema(XSchema): @@ -110,6 +123,7 @@ class SignedSchema(XSchema): API_SPEC.components.schema("UserSchema", schema=UserSchema) +API_SPEC.components.schema("UserSchemaNested", schema=UserSchemaNested) API_SPEC.components.schema("UserSignInSchema", schema=UserSignInSchema) API_SPEC.components.schema("UserSignUpSchema", schema=UserSignUpSchema) API_SPEC.components.schema("TokenSchema", schema=TokenSchema) diff --git a/task_office/boards/constants.py b/task_office/boards/constants.py new file mode 100644 index 0000000..00b1eca --- /dev/null +++ b/task_office/boards/constants.py @@ -0,0 +1,4 @@ +from task_office.settings import CONFIG + +BOARDS_PREFIX = CONFIG.API_V1_PREFIX + "/boards" +BOARD_RETRIEVE_URL = BOARDS_PREFIX + "/" diff --git a/task_office/boards/models.py b/task_office/boards/models.py deleted file mode 100644 index b2d6813..0000000 --- a/task_office/boards/models.py +++ /dev/null @@ -1,26 +0,0 @@ -# -*- coding: utf-8 -*- -"""Boards models.""" -from task_office.core.models.mixins import DTMixin, PKMixin -from task_office.database import Column, Model, db, reference_col, relationship - - -class Board(PKMixin, DTMixin, Model): - - __tablename__ = "boards" - __table_args__ = ( - db.UniqueConstraint("name", "owner_uuid", name="unique_name_owner_board"), - ) - - name = Column(db.String(80), nullable=False) - description = Column(db.String(255), unique=False, nullable=True, index=True) - owner_uuid = reference_col("users", pk_name="uuid", nullable=False) - owner = relationship("User", backref=db.backref("boardds")) - is_active = Column(db.Boolean(), default=True) - - def __init__(self, *args, **kwargs): - """Create instance.""" - db.Model.__init__(self, *args, **kwargs) - - def __repr__(self): - """Represent instance as a unique string.""" - return "".format(name=self.name) diff --git a/task_office/boards/schemas.py b/task_office/boards/schemas.py index 29a6136..b8d4c30 100644 --- a/task_office/boards/schemas.py +++ b/task_office/boards/schemas.py @@ -1,9 +1,14 @@ # coding: utf-8 +import uuid + from marshmallow import fields, validates_schema from marshmallow.validate import Length +from marshmallow_enum import EnumField from task_office.auth import User -from task_office.core.schemas import BaseSchema +from task_office.auth.schemas import UserSchemaNested +from task_office.core.enums import XEnum +from task_office.core.schemas import BaseSchema, ListInSchema, XSchema from task_office.core.validators import PK_Exists from task_office.swagger import API_SPEC @@ -22,7 +27,38 @@ def validate_schema(self, data, **kwargs): data["owner_uuid"] = str(data.pop("owner_uuid")) -board_schema = BoardInSchema() +class BoardOutSchema(BaseSchema): + name = fields.Str(dump_only=True) + description = fields.Str(dump_only=True) + owner = fields.Nested(UserSchemaNested, dump_only=True) + is_active = fields.Boolean(dump_only=True) + + class Meta: + strict = True + + @validates_schema + def validate_schema(self, data, **kwargs): + data["uuid"] = uuid.UUID(data.pop("uuid")).hex +board_in_schema = BoardInSchema() +board_out_schema = BoardOutSchema() +boards_list_out_schema = BoardOutSchema(many=True) API_SPEC.components.schema("BoardInSchema", schema=BoardInSchema) +API_SPEC.components.schema("BoardOutSchema", schema=BoardOutSchema) + + +class BoardInListSchema(ListInSchema): + class OrderingMap(XEnum): + pass + + searching = fields.Nested(XSchema, required=False) + ordering = EnumField(OrderingMap, required=False, by_value=True) + + class Meta: + strict = True + + +boards_in_list_schema = BoardInListSchema() +API_SPEC.components.schema("BoardInListSchema", schema=BoardInListSchema) + diff --git a/task_office/boards/views.py b/task_office/boards/views.py index 1e46c29..1dd4ba4 100644 --- a/task_office/boards/views.py +++ b/task_office/boards/views.py @@ -1,26 +1,70 @@ # -*- coding: utf-8 -*- """Boards views.""" - +from flask_babel import lazy_gettext as _ from flask import Blueprint from flask_apispec import use_kwargs, marshal_with -from flask_jwt_extended import jwt_required - -from .models import Board -from .schemas import board_schema -from ..settings import CONFIG +from flask_jwt_extended import jwt_required, get_current_user -BOARDS_PREFIX = CONFIG.API_V1_PREFIX + "/boards" -BOARD_RETRIEVE_URL = BOARDS_PREFIX + "/" +from .constants import BOARDS_PREFIX +from .schemas import ( + board_in_schema, + boards_in_list_schema, + boards_list_out_schema, + board_out_schema, +) +from ..core.helpers.listed_response import listed_response +from ..core.models.db_models import Board, Permission +from ..core.utils import is_uuid +from ..exceptions import InvalidUsage +from ..utils import empty_query_required, non_empty_query_required blueprint = Blueprint("boards", __name__, url_prefix=BOARDS_PREFIX) @blueprint.route("", methods=("post",)) @jwt_required -@use_kwargs(board_schema) -@marshal_with(board_schema) +@use_kwargs(board_in_schema) +@marshal_with(board_out_schema) def create_boards(**kwargs): data = kwargs + # Check name, owner_uuid are unique for board + empty_query_required(Board, name=data["name"], owner_uuid=str(data["owner_uuid"])) board = Board(**data) board.save() + + # Create permission for creator + Permission( + user_uuid=str(data["owner_uuid"]), + board_uuid=str(board.uuid), + role=Permission.Role.OWNER.value, + ).save() + + return board + + +@blueprint.route("", methods=("get",)) +@jwt_required +@use_kwargs(boards_in_list_schema) +def get_list_boards(**kwargs): + data = kwargs + user = get_current_user() + + boards = Board.query.join(Permission).filter(Permission.user_uuid == user.uuid) + # Serialize to paginated response + data = listed_response.serialize( + query=boards, query_params=data, schema=boards_list_out_schema + ) + return data + + +@blueprint.route("/", methods=("get",)) +@jwt_required +@marshal_with(board_out_schema) +def get_board_by_uuid(board_uuid): + # board_uuid in request url + if not is_uuid(board_uuid): + raise InvalidUsage(messages=[_("Not found")], status_code=404) + + board = non_empty_query_required(Board, uuid=str(board_uuid))[1] + return board diff --git a/task_office/permissions/models.py b/task_office/core/models/db_models.py similarity index 61% rename from task_office/permissions/models.py rename to task_office/core/models/db_models.py index b6a17b3..45b3c41 100644 --- a/task_office/permissions/models.py +++ b/task_office/core/models/db_models.py @@ -1,13 +1,33 @@ # -*- coding: utf-8 -*- -"""Permission models.""" - +"""Boards models.""" from flask_babel import lazy_gettext as _ - from task_office.core.enums import XEnum from task_office.core.models.mixins import DTMixin, PKMixin from task_office.database import Column, Model, db, reference_col, relationship +class Board(PKMixin, DTMixin, Model): + + __tablename__ = "boards" + __table_args__ = ( + db.UniqueConstraint("name", "owner_uuid", name="unique_name_owner_board"), + ) + + name = Column(db.String(80), nullable=False) + description = Column(db.String(255), unique=False, nullable=True, index=True) + owner_uuid = reference_col("users", pk_name="uuid", nullable=False) + owner = relationship("User", backref=db.backref("boardds")) + is_active = Column(db.Boolean(), default=True) + + def __init__(self, *args, **kwargs): + """Create instance.""" + db.Model.__init__(self, *args, **kwargs) + + def __repr__(self): + """Represent instance as a unique string.""" + return "".format(name=self.name) + + class Permission(PKMixin, DTMixin, Model): __tablename__ = "permissions" diff --git a/task_office/permissions/schemas/basic_schemas.py b/task_office/permissions/schemas/basic_schemas.py index 03baf4b..f047294 100644 --- a/task_office/permissions/schemas/basic_schemas.py +++ b/task_office/permissions/schemas/basic_schemas.py @@ -1,23 +1,20 @@ # coding: utf-8 -from flask_babel import lazy_gettext as _ -from marshmallow import fields, validates_schema +import uuid + +from marshmallow import fields, validates_schema, post_dump from marshmallow_enum import EnumField from task_office.auth import User -from task_office.boards import Board +from task_office.auth.schemas import UserSchemaNested from task_office.core.enums import XEnum, OrderingDirection +from task_office.core.models.db_models import Permission from task_office.core.schemas import BaseSchema, ListInSchema, XSchema from task_office.core.validators import PK_Exists -from task_office.exceptions import InvalidUsage -from task_office.permissions.models import Permission from task_office.swagger import API_SPEC class PermissionInSchema(BaseSchema): role = EnumField(Permission.Role, required=True, by_value=True) - board_uuid = fields.UUID( - required=True, validate=[PK_Exists(Board, "uuid")], allow_none=False - ) user_uuid = fields.UUID( required=True, validate=[PK_Exists(User, "uuid")], allow_none=False ) @@ -27,20 +24,20 @@ class Meta: @validates_schema def validate_schema(self, data, **kwargs): - data["board_uuid"] = str(data.pop("board_uuid")) data["user_uuid"] = str(data.pop("user_uuid")) data["role"] = data.pop("role").value - obj = Permission.query.filter_by( - board_uuid=data["board_uuid"], user_uuid=data["user_uuid"] - ).first() - if obj: - raise InvalidUsage(messages=[_("Already exists")], status_code=422) class PermissionOutSchema(BaseSchema): role = fields.Integer(dump_only=True) board_uuid = fields.UUID(dump_only=True) - user_uuid = fields.UUID(dump_only=True) + user = fields.Nested(UserSchemaNested, dump_only=True) + + @post_dump + def dump_data(self, data, **kwargs): + data["board_uuid"] = uuid.UUID(data.pop("board_uuid")).hex + data["uuid"] = uuid.UUID(data.pop("uuid")).hex + return data class Meta: strict = True @@ -66,10 +63,6 @@ class OrderingMap(XEnum): OrderingDirection.DESC, ) - board_uuid = fields.UUID( - required=True, validate=[PK_Exists(Board, "uuid")], allow_none=False - ) - searching = fields.Nested(XSchema, required=False) ordering = EnumField(OrderingMap, required=False, by_value=True) diff --git a/task_office/permissions/views.py b/task_office/permissions/views.py index 6b9623e..6562429 100644 --- a/task_office/permissions/views.py +++ b/task_office/permissions/views.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- """Permissions views.""" -from flask import Blueprint, request +from datetime import datetime + +from flask import Blueprint from flask_apispec import use_kwargs, marshal_with from flask_babel import lazy_gettext as _ from flask_jwt_extended import jwt_required -from .models import Permission +from task_office.boards.constants import BOARD_RETRIEVE_URL from .schemas.basic_schemas import ( permission_in_schema, permission_out_schema, permissions_in_list_schema, permission_list_out_schema, ) -from ..boards import BOARD_RETRIEVE_URL from ..core.helpers.listed_response import listed_response +from ..core.models.db_models import Permission, Board from ..core.utils import is_uuid from ..exceptions import InvalidUsage +from ..utils import non_empty_query_required, empty_query_required blueprint = Blueprint( "permissions", __name__, url_prefix=BOARD_RETRIEVE_URL + "/permissions" @@ -28,13 +31,9 @@ def get_meta_data(board_uuid): """ Additional data for Permissions """ - if not is_uuid(board_uuid): raise InvalidUsage(messages=[_("Not found")], status_code=404) - - perm = Permission.query.filter_by(board_uuid=board_uuid).first() - if not perm: - raise InvalidUsage(messages=[_("Not found")], status_code=404) + non_empty_query_required(Permission, board_uuid=str(board_uuid)) data = dict() data["roles"] = Permission.Role.dict_choices() @@ -45,11 +44,37 @@ def get_meta_data(board_uuid): @jwt_required @use_kwargs(permission_in_schema) @marshal_with(permission_out_schema) -def create_permission(**kwargs): +def create_permission(board_uuid, **kwargs): + data = kwargs + # Check board_uuid in request_url + if not is_uuid(board_uuid): + raise InvalidUsage(messages=[_("Not found")], status_code=404) + non_empty_query_required(Board, uuid=str(board_uuid)) + + # Check board_uuid, user_uuid are unique for permission + empty_query_required(Permission, board_uuid=board_uuid, user_uuid=data["user_uuid"]) + + permission = Permission(board_uuid=board_uuid, **data) + permission.save() + return permission + + +@blueprint.route("/", methods=("put",)) +@jwt_required +@use_kwargs(permission_in_schema) +@marshal_with(permission_out_schema) +def update_permission(board_uuid, permission_uuid, **kwargs): data = kwargs - if data["board_uuid"] not in request.url: + # Check is valid board uuid and permission_uuid in request url + if not is_uuid(board_uuid) or not is_uuid(permission_uuid): raise InvalidUsage(messages=[_("Not found")], status_code=404) - permission = Permission(**data) + + permission = non_empty_query_required( + Permission, uuid=str(permission_uuid), board_uuid=str(board_uuid) + ) + permission = permission.first() + + permission.update(updated_at=datetime.utcnow(), **data) permission.save() return permission @@ -57,12 +82,32 @@ def create_permission(**kwargs): @blueprint.route("", methods=("get",)) @jwt_required @use_kwargs(permissions_in_list_schema) -def get_list_permission(**kwargs): +def get_list_permission(board_uuid, **kwargs): data = kwargs - if str(data["board_uuid"]) not in request.url: + # Check board_uuid in request_url + if not is_uuid(board_uuid): raise InvalidUsage(messages=[_("Not found")], status_code=404) + permissions = non_empty_query_required(Permission, board_uuid=str(board_uuid))[0] + + # Serialize to paginated response data = listed_response.serialize( - query=Permission.query, query_params=data, schema=permission_list_out_schema + query=permissions, query_params=data, schema=permission_list_out_schema ) return data + + +@blueprint.route("/", methods=("get",)) +@jwt_required +@marshal_with(permission_out_schema) +def get_permission_by_uuid(board_uuid, permission_uuid): + # Check is valid board uuid and permission_uuid in request url + if not is_uuid(board_uuid) or not is_uuid(permission_uuid): + raise InvalidUsage(messages=[_("Not found")], status_code=404) + + permission = non_empty_query_required( + Permission, uuid=str(permission_uuid), board_uuid=str(board_uuid) + ) + permission = permission.first() + + return permission diff --git a/task_office/utils.py b/task_office/utils.py index 83129ef..e171c59 100644 --- a/task_office/utils.py +++ b/task_office/utils.py @@ -1,12 +1,30 @@ # -*- coding: utf-8 -*- """Helper utilities and decorators.""" +from flask_babel import lazy_gettext as _ +from task_office.exceptions import InvalidUsage def jwt_identity(identifier): from task_office.auth import User - return User.get_by_id(payload) + return User.get_by_id(identifier) def identity_loader(user): return user.id + + +def non_empty_query_required(model, **params): + qs = model.query.filter_by(**params) + obj_first = qs.first() + if not obj_first: + raise InvalidUsage(messages=[_("Not found")], status_code=404) + return qs, obj_first + + +def empty_query_required(model, **params): + qs = model.query.filter_by(**params) + obj_first = qs.first() + if obj_first: + raise InvalidUsage(messages=[_("Already exists")], status_code=422) + return qs, obj_first \ No newline at end of file From 25fb45220050d462cbbf68f7f8cc994ffb4fb77d Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Mon, 13 Jan 2020 06:58:03 +0200 Subject: [PATCH 16/60] light refactoring, improved permissions endpoint --- task_office/boards/schemas/__init__.py | 0 .../{schemas.py => schemas/basic_schemas.py} | 7 +++---- task_office/boards/views.py | 2 +- .../permissions/schemas/basic_schemas.py | 6 +++--- task_office/permissions/views.py | 17 +++++++++++++++-- task_office/utils.py | 2 +- 6 files changed, 23 insertions(+), 11 deletions(-) create mode 100644 task_office/boards/schemas/__init__.py rename task_office/boards/{schemas.py => schemas/basic_schemas.py} (91%) diff --git a/task_office/boards/schemas/__init__.py b/task_office/boards/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/task_office/boards/schemas.py b/task_office/boards/schemas/basic_schemas.py similarity index 91% rename from task_office/boards/schemas.py rename to task_office/boards/schemas/basic_schemas.py index b8d4c30..e1a6b89 100644 --- a/task_office/boards/schemas.py +++ b/task_office/boards/schemas/basic_schemas.py @@ -48,7 +48,7 @@ def validate_schema(self, data, **kwargs): API_SPEC.components.schema("BoardOutSchema", schema=BoardOutSchema) -class BoardInListSchema(ListInSchema): +class BoardListInSchema(ListInSchema): class OrderingMap(XEnum): pass @@ -59,6 +59,5 @@ class Meta: strict = True -boards_in_list_schema = BoardInListSchema() -API_SPEC.components.schema("BoardInListSchema", schema=BoardInListSchema) - +boards_in_list_schema = BoardListInSchema() +API_SPEC.components.schema("BoardInListSchema", schema=BoardListInSchema) diff --git a/task_office/boards/views.py b/task_office/boards/views.py index 1dd4ba4..e5ee04a 100644 --- a/task_office/boards/views.py +++ b/task_office/boards/views.py @@ -6,7 +6,7 @@ from flask_jwt_extended import jwt_required, get_current_user from .constants import BOARDS_PREFIX -from .schemas import ( +from .schemas.basic_schemas import ( board_in_schema, boards_in_list_schema, boards_list_out_schema, diff --git a/task_office/permissions/schemas/basic_schemas.py b/task_office/permissions/schemas/basic_schemas.py index f047294..1b877d1 100644 --- a/task_office/permissions/schemas/basic_schemas.py +++ b/task_office/permissions/schemas/basic_schemas.py @@ -50,7 +50,7 @@ class Meta: API_SPEC.components.schema("PermissionOutSchema", schema=PermissionOutSchema) -class PermissionInListSchema(ListInSchema): +class PermissionListInSchema(ListInSchema): class OrderingMap(XEnum): CREATED_AT_ASC = ( "-created_at", @@ -70,5 +70,5 @@ class Meta: strict = True -permissions_in_list_schema = PermissionInListSchema() -API_SPEC.components.schema("PermissionInListSchema", schema=PermissionInListSchema) +permissions_in_list_schema = PermissionListInSchema() +API_SPEC.components.schema("PermissionListInSchema", schema=PermissionListInSchema) diff --git a/task_office/permissions/views.py b/task_office/permissions/views.py index 6562429..1be59c6 100644 --- a/task_office/permissions/views.py +++ b/task_office/permissions/views.py @@ -54,6 +54,13 @@ def create_permission(board_uuid, **kwargs): # Check board_uuid, user_uuid are unique for permission empty_query_required(Permission, board_uuid=board_uuid, user_uuid=data["user_uuid"]) + role = data["role"] + if role == Permission.Role.OWNER.value: + if Permission.query.filter_by( + board_uuid=board_uuid, role=Permission.Role.OWNER.value + ).first(): + raise InvalidUsage(messages=[_("Not allowed")], status_code=422) + permission = Permission(board_uuid=board_uuid, **data) permission.save() return permission @@ -69,10 +76,16 @@ def update_permission(board_uuid, permission_uuid, **kwargs): if not is_uuid(board_uuid) or not is_uuid(permission_uuid): raise InvalidUsage(messages=[_("Not found")], status_code=404) + role = data["role"] + if role == Permission.Role.OWNER.value: + if Permission.query.filter_by( + board_uuid=board_uuid, role=Permission.Role.OWNER.value + ).first(): + raise InvalidUsage(messages=[_("Not allowed")], status_code=422) + permission = non_empty_query_required( Permission, uuid=str(permission_uuid), board_uuid=str(board_uuid) - ) - permission = permission.first() + )[1] permission.update(updated_at=datetime.utcnow(), **data) permission.save() diff --git a/task_office/utils.py b/task_office/utils.py index e171c59..b45c408 100644 --- a/task_office/utils.py +++ b/task_office/utils.py @@ -27,4 +27,4 @@ def empty_query_required(model, **params): obj_first = qs.first() if obj_first: raise InvalidUsage(messages=[_("Already exists")], status_code=422) - return qs, obj_first \ No newline at end of file + return qs, obj_first From dfa3489347e18a703af4dfa736288224c7ef682c Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Sat, 1 Feb 2020 12:30:27 +0200 Subject: [PATCH 17/60] init columns endpoints, improvements --- migrations/versions/978b8947df90_.py | 74 +++++++++++++++++++ task_office/app.py | 3 +- task_office/boards/views.py | 3 +- task_office/columns/__init__.py | 3 + task_office/columns/schemas/__init__.py | 0 task_office/columns/schemas/basic_schemas.py | 65 ++++++++++++++++ task_office/columns/views.py | 61 +++++++++++++++ task_office/core/models/db_models.py | 74 ++++++++++++++++++- task_office/core/utils.py | 40 ++++++++++ .../permissions/schemas/basic_schemas.py | 6 +- task_office/permissions/views.py | 3 +- task_office/utils.py | 18 ----- 12 files changed, 323 insertions(+), 27 deletions(-) create mode 100644 migrations/versions/978b8947df90_.py create mode 100644 task_office/columns/__init__.py create mode 100644 task_office/columns/schemas/__init__.py create mode 100644 task_office/columns/schemas/basic_schemas.py create mode 100644 task_office/columns/views.py diff --git a/migrations/versions/978b8947df90_.py b/migrations/versions/978b8947df90_.py new file mode 100644 index 0000000..1a65138 --- /dev/null +++ b/migrations/versions/978b8947df90_.py @@ -0,0 +1,74 @@ +"""empty message + +Revision ID: 978b8947df90 +Revises: 2c4e91200810 +Create Date: 2020-02-01 10:55:06.021809 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = "978b8947df90" +down_revision = "2c4e91200810" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "columns", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("uuid", postgresql.UUID(), nullable=False), + sa.Column("meta", sa.JSON(), nullable=True), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.Column("updated_at", sa.DateTime(), nullable=False), + sa.Column("name", sa.String(length=120), nullable=False), + sa.Column("position", sa.Integer(), nullable=True), + sa.Column("board_uuid", postgresql.UUID(), nullable=False), + sa.ForeignKeyConstraint(["board_uuid"], ["boards.uuid"]), + sa.PrimaryKeyConstraint("id", "uuid"), + sa.UniqueConstraint( + "board_uuid", "name", name="unique_board__board_column_name" + ), + sa.UniqueConstraint("uuid"), + ) + op.create_table( + "tasks", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("uuid", postgresql.UUID(), nullable=False), + sa.Column("meta", sa.JSON(), nullable=True), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.Column("updated_at", sa.DateTime(), nullable=False), + sa.Column("expire_at", sa.DateTime(), nullable=True), + sa.Column("label", sa.String(length=80), nullable=True), + sa.Column("name", sa.String(length=120), nullable=False), + sa.Column("description", sa.String(length=120), nullable=False), + sa.Column("state", sa.Integer(), nullable=True), + sa.Column("position", sa.Integer(), nullable=True), + sa.Column("column_uuid", postgresql.UUID(), nullable=False), + sa.Column("creator_uuid", postgresql.UUID(), nullable=False), + sa.ForeignKeyConstraint(["column_uuid"], ["columns.uuid"]), + sa.ForeignKeyConstraint(["creator_uuid"], ["users.uuid"]), + sa.PrimaryKeyConstraint("id", "uuid"), + sa.UniqueConstraint("uuid"), + ) + op.create_table( + "users_tasks", + sa.Column("task_uuid", postgresql.UUID(), nullable=False), + sa.Column("user_uuid", postgresql.UUID(), nullable=False), + sa.ForeignKeyConstraint(["task_uuid"], ["tasks.uuid"]), + sa.ForeignKeyConstraint(["user_uuid"], ["users.uuid"]), + sa.PrimaryKeyConstraint("task_uuid", "user_uuid"), + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("users_tasks") + op.drop_table("tasks") + op.drop_table("columns") + # ### end Alembic commands ### diff --git a/task_office/app.py b/task_office/app.py index 270a67d..d46cfab 100644 --- a/task_office/app.py +++ b/task_office/app.py @@ -3,7 +3,7 @@ from flask import Flask from task_office.auth.jwt_error_handlers import jwt_errors_map -from task_office import commands, auth, swagger, boards, permissions +from task_office import commands, auth, swagger, boards, permissions, columns from task_office.exceptions import InvalidUsage from task_office.extensions import bcrypt, cache, db, migrate, cors, jwt, babel from task_office.settings import CONFIG @@ -48,6 +48,7 @@ def register_blueprints(app): app.register_blueprint(auth.views.blueprint) app.register_blueprint(boards.views.blueprint) app.register_blueprint(permissions.views.blueprint) + app.register_blueprint(columns.views.blueprint) if CONFIG.USE_DOCS: app.register_blueprint(swagger.views.blueprint_swagger, url_prefix=SWAGGER_URL) app.register_blueprint(swagger.views.blueprint) diff --git a/task_office/boards/views.py b/task_office/boards/views.py index e5ee04a..d61005e 100644 --- a/task_office/boards/views.py +++ b/task_office/boards/views.py @@ -14,9 +14,8 @@ ) from ..core.helpers.listed_response import listed_response from ..core.models.db_models import Board, Permission -from ..core.utils import is_uuid +from ..core.utils import is_uuid, empty_query_required, non_empty_query_required from ..exceptions import InvalidUsage -from ..utils import empty_query_required, non_empty_query_required blueprint = Blueprint("boards", __name__, url_prefix=BOARDS_PREFIX) diff --git a/task_office/columns/__init__.py b/task_office/columns/__init__.py new file mode 100644 index 0000000..67ce501 --- /dev/null +++ b/task_office/columns/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from .views import * diff --git a/task_office/columns/schemas/__init__.py b/task_office/columns/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/task_office/columns/schemas/basic_schemas.py b/task_office/columns/schemas/basic_schemas.py new file mode 100644 index 0000000..3e8e0f4 --- /dev/null +++ b/task_office/columns/schemas/basic_schemas.py @@ -0,0 +1,65 @@ +# coding: utf-8 +import uuid + +from marshmallow import fields, post_dump +from marshmallow.validate import Length, Range +from marshmallow_enum import EnumField + +from task_office.core.enums import XEnum, OrderingDirection +from task_office.core.models.db_models import BoardColumn +from task_office.core.schemas import BaseSchema, ListInSchema, XSchema +from task_office.swagger import API_SPEC + + +class ColumnInSchema(BaseSchema): + name = fields.Str(required=True, allow_none=False, validate=[Length(max=120)]) + position = fields.Integer(required=False, default=0, validate=[Range(min=0)]) + + class Meta: + strict = True + + +class ColumnOutSchema(BaseSchema): + name = fields.Str(dump_only=True) + position = fields.Integer(dump_only=True) + board_uuid = fields.UUID(dump_only=True) + + @post_dump + def dump_data(self, data, **kwargs): + data["board_uuid"] = uuid.UUID(data.pop("board_uuid")).hex + data["uuid"] = uuid.UUID(data.pop("uuid")).hex + return data + + class Meta: + strict = True + + +column_in_schema = ColumnInSchema() +column_out_schema = ColumnOutSchema() +columns_list_out_schema = ColumnOutSchema(many=True) +API_SPEC.components.schema("ColumnInSchema", schema=ColumnInSchema) +API_SPEC.components.schema("ColumnOutSchema", schema=ColumnOutSchema) + + +class ColumnsListInSchema(ListInSchema): + class OrderingMap(XEnum): + CREATED_AT_ASC = ( + "-created_at", + BoardColumn.created_at.asc(), + OrderingDirection.ASC, + ) + CREATED_AET_DESC = ( + "created_at", + BoardColumn.created_at.desc(), + OrderingDirection.DESC, + ) + + searching = fields.Nested(XSchema, required=False) + ordering = EnumField(OrderingMap, required=False, by_value=True) + + class Meta: + strict = True + + +columns_in_list_schema = ColumnsListInSchema() +API_SPEC.components.schema("ColumnsListInSchema", schema=ColumnsListInSchema) diff --git a/task_office/columns/views.py b/task_office/columns/views.py new file mode 100644 index 0000000..63daab4 --- /dev/null +++ b/task_office/columns/views.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +"""Columns views.""" + +from flask import Blueprint +from flask_apispec import use_kwargs, marshal_with +from flask_jwt_extended import jwt_required + +from task_office.boards.constants import BOARD_RETRIEVE_URL +from .schemas.basic_schemas import ( + column_in_schema, + columns_in_list_schema, + columns_list_out_schema, + column_out_schema, +) +from ..core.helpers.listed_response import listed_response +from ..core.models.db_models import BoardColumn, Board +from ..core.utils import validate_request_url_uuid, query + +blueprint = Blueprint("columns", __name__, url_prefix=BOARD_RETRIEVE_URL + "/columns") + + +@blueprint.route("/meta", methods=("get",)) +@jwt_required +def get_meta_data(board_uuid): + """ + Additional data for Columns + """ + validate_request_url_uuid(Board, "uuid", board_uuid, True) + data = dict() + return data + + +@blueprint.route("", methods=("post",)) +@jwt_required +@use_kwargs(column_in_schema) +@marshal_with(column_out_schema) +def create_column(board_uuid, **kwargs): + data = kwargs + validate_request_url_uuid(Board, "uuid", board_uuid, True) + + column = BoardColumn(board_uuid=board_uuid, **data) + column.save() + return column + + +@blueprint.route("", methods=("get",)) +@jwt_required +@use_kwargs(columns_in_list_schema) +def get_list_columns(board_uuid, **kwargs): + data = kwargs + + # Check board_uuid in request_url + validate_request_url_uuid(Board, "uuid", board_uuid, True) + + columns = query(BoardColumn, board_uuid=str(board_uuid)) + + # Serialize to paginated response + data = listed_response.serialize( + query=columns, query_params=data, schema=columns_list_out_schema + ) + return data diff --git a/task_office/core/models/db_models.py b/task_office/core/models/db_models.py index 45b3c41..46988b0 100644 --- a/task_office/core/models/db_models.py +++ b/task_office/core/models/db_models.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- """Boards models.""" from flask_babel import lazy_gettext as _ +from sqlalchemy.dialects.postgresql import UUID + from task_office.core.enums import XEnum from task_office.core.models.mixins import DTMixin, PKMixin from task_office.database import Column, Model, db, reference_col, relationship @@ -16,7 +18,7 @@ class Board(PKMixin, DTMixin, Model): name = Column(db.String(80), nullable=False) description = Column(db.String(255), unique=False, nullable=True, index=True) owner_uuid = reference_col("users", pk_name="uuid", nullable=False) - owner = relationship("User", backref=db.backref("boardds")) + owner = relationship("User", backref=db.backref("boards")) is_active = Column(db.Boolean(), default=True) def __init__(self, *args, **kwargs): @@ -55,3 +57,73 @@ def __init__(self, *args, **kwargs): def __repr__(self): """Represent instance as a unique string.""" return "".format(self.uuid) + + +class BoardColumn(PKMixin, DTMixin, Model): + + __tablename__ = "columns" + __table_args__ = ( + db.UniqueConstraint( + "board_uuid", "name", name="unique_board__board_column_name" + ), + ) + + name = Column(db.String(120), nullable=False) + position = Column(db.Integer(), default=0) + board_uuid = reference_col("boards", pk_name="uuid", nullable=False) + board = relationship("Board", backref=db.backref("columns")) + + def __init__(self, *args, **kwargs): + """Create instance.""" + db.Model.__init__(self, *args, **kwargs) + + def __repr__(self): + """Represent instance as a unique string.""" + return "".format(self.uuid) + + +users_tasks = db.Table( + "users_tasks", + Column("task_uuid", UUID, db.ForeignKey("tasks.uuid"), primary_key=True), + Column("user_uuid", UUID, db.ForeignKey("users.uuid"), primary_key=True), +) + + +class Task(PKMixin, DTMixin, Model): + + __tablename__ = "tasks" + + class State(XEnum): + NEW = 1, _("New"), _("New") + IN_PROCESS = 2, _("In process"), _("In process") + REJECTED = 3, _("Rejected"), _("Rejected") + DONE = 4, _("Done"), _("Done") + + expire_at = db.Column(db.DateTime, nullable=True, default=None) + + label = Column(db.String(80), default="") + name = Column(db.String(120), nullable=False) + description = Column(db.String(120), nullable=False) + state = Column(db.Integer(), default=State.NEW.value) + position = Column(db.Integer(), default=0) + + column_uuid = reference_col("columns", pk_name="uuid", nullable=False) + column = relationship("BoardColumn", backref=db.backref("tasks")) + + creator_uuid = reference_col("users", pk_name="uuid", nullable=False) + creator = relationship("User", backref=db.backref("tasks")) + + performers = db.relationship( + "User", + secondary=users_tasks, + lazy="subquery", + backref=db.backref("tasks_to_perform", lazy=True), + ) + + def __init__(self, *args, **kwargs): + """Create instance.""" + db.Model.__init__(self, *args, **kwargs) + + def __repr__(self): + """Represent instance as a unique string.""" + return "".format(self.uuid) diff --git a/task_office/core/utils.py b/task_office/core/utils.py index 4ab80aa..9e6d5d9 100644 --- a/task_office/core/utils.py +++ b/task_office/core/utils.py @@ -1,5 +1,31 @@ from uuid import UUID +from flask import request +from flask_babel import lazy_gettext as _ + +from task_office.database import Model +from task_office.exceptions import InvalidUsage + + +def query(model, **params): + return model.query.filter_by(**params) + + +def non_empty_query_required(model, **params): + qs = query(model, **params) + obj_first = qs.first() + if not obj_first: + raise InvalidUsage(messages=[_("Not found")], status_code=404) + return qs, obj_first + + +def empty_query_required(model, **params): + qs = query(model, **params) + obj_first = qs.first() + if obj_first: + raise InvalidUsage(messages=[_("Already exists")], status_code=422) + return qs, obj_first + def is_uuid(uuid): try: @@ -7,3 +33,17 @@ def is_uuid(uuid): return True except ValueError: return False + + +def validate_request_url_uuid( + model: Model, key: str, uuid: str, must_exists: bool = False +): + if not is_uuid(uuid): + raise InvalidUsage(messages=[_("Not found")], status_code=404) + + request_url_splitted = request.url.split("/") + if uuid not in request_url_splitted: + raise InvalidUsage(messages=[_("Not found")], status_code=404) + + if must_exists: + non_empty_query_required(model, **{key: uuid}) diff --git a/task_office/permissions/schemas/basic_schemas.py b/task_office/permissions/schemas/basic_schemas.py index 1b877d1..6c6d429 100644 --- a/task_office/permissions/schemas/basic_schemas.py +++ b/task_office/permissions/schemas/basic_schemas.py @@ -50,7 +50,7 @@ class Meta: API_SPEC.components.schema("PermissionOutSchema", schema=PermissionOutSchema) -class PermissionListInSchema(ListInSchema): +class PermissionsListInSchema(ListInSchema): class OrderingMap(XEnum): CREATED_AT_ASC = ( "-created_at", @@ -70,5 +70,5 @@ class Meta: strict = True -permissions_in_list_schema = PermissionListInSchema() -API_SPEC.components.schema("PermissionListInSchema", schema=PermissionListInSchema) +permissions_in_list_schema = PermissionsListInSchema() +API_SPEC.components.schema("PermissionsListInSchema", schema=PermissionsListInSchema) diff --git a/task_office/permissions/views.py b/task_office/permissions/views.py index 1be59c6..b83722e 100644 --- a/task_office/permissions/views.py +++ b/task_office/permissions/views.py @@ -16,9 +16,8 @@ ) from ..core.helpers.listed_response import listed_response from ..core.models.db_models import Permission, Board -from ..core.utils import is_uuid +from ..core.utils import is_uuid, non_empty_query_required, empty_query_required from ..exceptions import InvalidUsage -from ..utils import non_empty_query_required, empty_query_required blueprint = Blueprint( "permissions", __name__, url_prefix=BOARD_RETRIEVE_URL + "/permissions" diff --git a/task_office/utils.py b/task_office/utils.py index b45c408..10cf963 100644 --- a/task_office/utils.py +++ b/task_office/utils.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- """Helper utilities and decorators.""" -from flask_babel import lazy_gettext as _ -from task_office.exceptions import InvalidUsage def jwt_identity(identifier): @@ -12,19 +10,3 @@ def jwt_identity(identifier): def identity_loader(user): return user.id - - -def non_empty_query_required(model, **params): - qs = model.query.filter_by(**params) - obj_first = qs.first() - if not obj_first: - raise InvalidUsage(messages=[_("Not found")], status_code=404) - return qs, obj_first - - -def empty_query_required(model, **params): - qs = model.query.filter_by(**params) - obj_first = qs.first() - if obj_first: - raise InvalidUsage(messages=[_("Already exists")], status_code=422) - return qs, obj_first From 03bf5b4552fb6243a4e711331bee1494f58e8105 Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Sun, 2 Feb 2020 16:04:18 +0200 Subject: [PATCH 18/60] improved columns endpoints --- task_office/boards/schemas/basic_schemas.py | 4 +- task_office/columns/schemas/basic_schemas.py | 20 +++- task_office/columns/utils.py | 38 ++++++++ task_office/columns/views.py | 94 ++++++++++++++++++- task_office/core/utils.py | 6 +- task_office/core/validators.py | 2 +- task_office/exceptions.py | 9 +- .../permissions/schemas/basic_schemas.py | 4 +- 8 files changed, 158 insertions(+), 19 deletions(-) create mode 100644 task_office/columns/utils.py diff --git a/task_office/boards/schemas/basic_schemas.py b/task_office/boards/schemas/basic_schemas.py index e1a6b89..3d1df95 100644 --- a/task_office/boards/schemas/basic_schemas.py +++ b/task_office/boards/schemas/basic_schemas.py @@ -9,14 +9,14 @@ from task_office.auth.schemas import UserSchemaNested from task_office.core.enums import XEnum from task_office.core.schemas import BaseSchema, ListInSchema, XSchema -from task_office.core.validators import PK_Exists +from task_office.core.validators import PKExists from task_office.swagger import API_SPEC class BoardInSchema(BaseSchema): name = fields.Str(required=True, allow_none=False, validate=[Length(max=255)]) description = fields.Str(allow_none=True, required=False, default="") - owner_uuid = fields.UUID(required=True, validate=[PK_Exists(User, "uuid")]) + owner_uuid = fields.UUID(required=True, validate=[PKExists(User, "uuid")]) is_active = fields.Boolean(default=True) class Meta: diff --git a/task_office/columns/schemas/basic_schemas.py b/task_office/columns/schemas/basic_schemas.py index 3e8e0f4..83ab8d3 100644 --- a/task_office/columns/schemas/basic_schemas.py +++ b/task_office/columns/schemas/basic_schemas.py @@ -11,9 +11,19 @@ from task_office.swagger import API_SPEC -class ColumnInSchema(BaseSchema): +class ColumnPostSchema(BaseSchema): name = fields.Str(required=True, allow_none=False, validate=[Length(max=120)]) - position = fields.Integer(required=False, default=0, validate=[Range(min=0)]) + position = fields.Integer( + required=True, default=1, allow_none=False, validate=[Range(min=1)] + ) + + class Meta: + strict = True + + +class ColumnPutSchema(BaseSchema): + name = fields.Str(required=False, allow_none=False, validate=[Length(max=120)]) + position = fields.Integer(required=False, allow_none=False, validate=[Range(min=1)]) class Meta: strict = True @@ -34,10 +44,12 @@ class Meta: strict = True -column_in_schema = ColumnInSchema() +column_post_schema = ColumnPostSchema() +column_put_schema = ColumnPutSchema() column_out_schema = ColumnOutSchema() columns_list_out_schema = ColumnOutSchema(many=True) -API_SPEC.components.schema("ColumnInSchema", schema=ColumnInSchema) +API_SPEC.components.schema("ColumnPostSchema", schema=ColumnPostSchema) +API_SPEC.components.schema("ColumnPutSchema", schema=ColumnPutSchema) API_SPEC.components.schema("ColumnOutSchema", schema=ColumnOutSchema) diff --git a/task_office/columns/utils.py b/task_office/columns/utils.py new file mode 100644 index 0000000..40b161e --- /dev/null +++ b/task_office/columns/utils.py @@ -0,0 +1,38 @@ +from task_office.core.models.db_models import BoardColumn +from task_office.database import Model +from task_office.extensions import db + + +def reset_columns_ordering( + instance: Model, board_uuid: str, new_value: int, old_value: int = None +) -> bool: + if new_value != old_value: + if old_value is None: + # if column already created + qs = BoardColumn.query.filter( + BoardColumn.id != instance.id, + BoardColumn.board_uuid == board_uuid, + BoardColumn.position >= new_value, + ) + qs.update(dict(position=BoardColumn.position + 1)) + elif new_value > old_value: + qs = BoardColumn.query.filter( + BoardColumn.id != instance.id, + BoardColumn.board_uuid == board_uuid, + BoardColumn.position > old_value, + BoardColumn.position <= new_value, + ) + qs.update(dict(position=BoardColumn.position - 1)) + else: + BoardColumn.query.filter( + BoardColumn.id != instance.id, + BoardColumn.board_uuid == board_uuid, + BoardColumn.position < old_value, + BoardColumn.position >= new_value, + ).update(dict(position=BoardColumn.position + 1)) + + db.session.commit() + + return True + + return False diff --git a/task_office/columns/views.py b/task_office/columns/views.py index 63daab4..cd2a6b6 100644 --- a/task_office/columns/views.py +++ b/task_office/columns/views.py @@ -1,20 +1,27 @@ # -*- coding: utf-8 -*- """Columns views.""" +from datetime import datetime from flask import Blueprint from flask_apispec import use_kwargs, marshal_with +from flask_babel import lazy_gettext as _ from flask_jwt_extended import jwt_required +from sqlalchemy import func from task_office.boards.constants import BOARD_RETRIEVE_URL from .schemas.basic_schemas import ( - column_in_schema, + column_post_schema, columns_in_list_schema, columns_list_out_schema, column_out_schema, + column_put_schema, ) +from .utils import reset_columns_ordering from ..core.helpers.listed_response import listed_response from ..core.models.db_models import BoardColumn, Board -from ..core.utils import validate_request_url_uuid, query +from ..core.utils import validate_request_url_uuid, non_empty_query_required +from ..exceptions import InvalidUsage +from ..extensions import db blueprint = Blueprint("columns", __name__, url_prefix=BOARD_RETRIEVE_URL + "/columns") @@ -32,14 +39,91 @@ def get_meta_data(board_uuid): @blueprint.route("", methods=("post",)) @jwt_required -@use_kwargs(column_in_schema) +@use_kwargs(column_post_schema) @marshal_with(column_out_schema) def create_column(board_uuid, **kwargs): data = kwargs validate_request_url_uuid(Board, "uuid", board_uuid, True) + # validate max position + data["position"] = data.get("position", 1) + position = data["position"] + if position > 1: + max_position = db.session.query(func.max(BoardColumn.position)).scalar() + max_position = 1 if max_position is None else max_position + 1 + if position > max_position: + raise InvalidUsage( + messages=[_("Must be between {} and {}".format(1, max_position))], + status_code=422, + key="position", + ) + + # validate unique name for board + q_by_name = BoardColumn.query.filter_by( + board_uuid=board_uuid, name=data["name"] + ).first() + if q_by_name: + raise InvalidUsage( + messages=[_("Already exists with value {}".format(data["name"]))], + status_code=422, + key="name", + ) + column = BoardColumn(board_uuid=board_uuid, **data) column.save() + + reset_columns_ordering(column, board_uuid, position) + + return column + + +@blueprint.route("/", methods=("put",)) +@jwt_required +@use_kwargs(column_put_schema) +@marshal_with(column_out_schema) +def update_column(board_uuid, column_uuid, **kwargs): + data = kwargs + + validate_request_url_uuid(Board, "uuid", board_uuid, True) + validate_request_url_uuid(BoardColumn, "uuid", column_uuid, True) + + # validate max position value + position = data.get("position", None) + if position is not None and position > 1: + max_position = db.session.query(func.max(BoardColumn.position)).scalar() + max_position = 1 if max_position is None else max_position + 1 + + if position > max_position: + raise InvalidUsage( + messages=[_("Must be between {} and {}.".format(1, max_position))], + status_code=422, + key="position", + ) + + # validate unique name for board + q_by_name = BoardColumn.query.filter( + BoardColumn.uuid != column_uuid, + BoardColumn.board_uuid == board_uuid, + BoardColumn.name == data["name"], + ).first() + if q_by_name: + raise InvalidUsage( + messages=[_("Already exists with value {}".format(data["name"]))], + status_code=422, + key="name", + ) + + column = non_empty_query_required( + BoardColumn, uuid=str(column_uuid), board_uuid=str(board_uuid) + )[1] + + if data: + old_position = column.position + column.update(updated_at=datetime.utcnow(), **data) + column.save() + if position: + reset_columns_ordering(column, board_uuid, position, old_position) + return column @@ -52,7 +136,9 @@ def get_list_columns(board_uuid, **kwargs): # Check board_uuid in request_url validate_request_url_uuid(Board, "uuid", board_uuid, True) - columns = query(BoardColumn, board_uuid=str(board_uuid)) + columns = BoardColumn.query.order_by("position", "-id").filter_by( + board_uuid=board_uuid + ) # Serialize to paginated response data = listed_response.serialize( diff --git a/task_office/core/utils.py b/task_office/core/utils.py index 9e6d5d9..3940642 100644 --- a/task_office/core/utils.py +++ b/task_office/core/utils.py @@ -7,12 +7,12 @@ from task_office.exceptions import InvalidUsage -def query(model, **params): +def _query(model, **params): return model.query.filter_by(**params) def non_empty_query_required(model, **params): - qs = query(model, **params) + qs = _query(model, **params) obj_first = qs.first() if not obj_first: raise InvalidUsage(messages=[_("Not found")], status_code=404) @@ -20,7 +20,7 @@ def non_empty_query_required(model, **params): def empty_query_required(model, **params): - qs = query(model, **params) + qs = _query(model, **params) obj_first = qs.first() if obj_first: raise InvalidUsage(messages=[_("Already exists")], status_code=422) diff --git a/task_office/core/validators.py b/task_office/core/validators.py index 02c0d23..578412b 100644 --- a/task_office/core/validators.py +++ b/task_office/core/validators.py @@ -31,7 +31,7 @@ def __call__(self, value) -> typing.Any: return value -class PK_Exists(Validator): +class PKExists(Validator): """Validator of entity pk.""" not_found = _("Not found with value {}") diff --git a/task_office/exceptions.py b/task_office/exceptions.py index c2228c0..3731374 100644 --- a/task_office/exceptions.py +++ b/task_office/exceptions.py @@ -11,12 +11,15 @@ def template(data, code=500): class InvalidUsage(Exception): status_code = 500 - def __init__(self, messages, status_code=500, payload=None): + def __init__(self, messages, status_code=500, key=None): Exception.__init__(self) - self.messages = template(data=messages, code=status_code) + self.key = key + payload = messages + if self.key: + payload = {self.key: messages} + self.messages = template(data=payload, code=status_code) if status_code is not None: self.status_code = status_code - self.payload = payload def to_json(self): rv = self.messages diff --git a/task_office/permissions/schemas/basic_schemas.py b/task_office/permissions/schemas/basic_schemas.py index 6c6d429..52e4a32 100644 --- a/task_office/permissions/schemas/basic_schemas.py +++ b/task_office/permissions/schemas/basic_schemas.py @@ -9,14 +9,14 @@ from task_office.core.enums import XEnum, OrderingDirection from task_office.core.models.db_models import Permission from task_office.core.schemas import BaseSchema, ListInSchema, XSchema -from task_office.core.validators import PK_Exists +from task_office.core.validators import PKExists from task_office.swagger import API_SPEC class PermissionInSchema(BaseSchema): role = EnumField(Permission.Role, required=True, by_value=True) user_uuid = fields.UUID( - required=True, validate=[PK_Exists(User, "uuid")], allow_none=False + required=True, validate=[PKExists(User, "uuid")], allow_none=False ) class Meta: From 86038a4c8e38ba99db04c87760c9bc67366f7b4f Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Sat, 15 Feb 2020 13:07:19 +0200 Subject: [PATCH 19/60] init tasks endpoints --- TODO.rst | 19 +++ task_office/app.py | 3 +- task_office/columns/schemas/constants.py | 4 + task_office/core/models/db_models.py | 1 - task_office/tasks/__init__.py | 3 + task_office/tasks/constants.py | 4 + task_office/tasks/schemas/__init__.py | 0 task_office/tasks/schemas/basic_schemas.py | 129 +++++++++++++++ task_office/tasks/utils.py | 38 +++++ task_office/tasks/views.py | 179 +++++++++++++++++++++ 10 files changed, 378 insertions(+), 2 deletions(-) create mode 100644 TODO.rst create mode 100644 task_office/columns/schemas/constants.py create mode 100644 task_office/tasks/__init__.py create mode 100644 task_office/tasks/constants.py create mode 100644 task_office/tasks/schemas/__init__.py create mode 100644 task_office/tasks/schemas/basic_schemas.py create mode 100644 task_office/tasks/utils.py create mode 100644 task_office/tasks/views.py diff --git a/TODO.rst b/TODO.rst new file mode 100644 index 0000000..989a16d --- /dev/null +++ b/TODO.rst @@ -0,0 +1,19 @@ +================= +TODO List: +================= + +Issues General +^^^^^^^^^^^^^^ +* Try to handle all errors and wrap them by our custom error class(InvalidUsage), with using translations +* Implement Documented BluePrint + +Issues By Features +^^^^^^^^^^^^^^^^^^ +Columns +------- +* If column will removing reset columns ordering in board + +Tasks +----- +* !!Done!! Check is unique task name for current Board in post, put +* If task will changing column reset tasks ordering in new and ald columns diff --git a/task_office/app.py b/task_office/app.py index d46cfab..25648de 100644 --- a/task_office/app.py +++ b/task_office/app.py @@ -3,7 +3,7 @@ from flask import Flask from task_office.auth.jwt_error_handlers import jwt_errors_map -from task_office import commands, auth, swagger, boards, permissions, columns +from task_office import commands, auth, swagger, boards, permissions, columns, tasks from task_office.exceptions import InvalidUsage from task_office.extensions import bcrypt, cache, db, migrate, cors, jwt, babel from task_office.settings import CONFIG @@ -49,6 +49,7 @@ def register_blueprints(app): app.register_blueprint(boards.views.blueprint) app.register_blueprint(permissions.views.blueprint) app.register_blueprint(columns.views.blueprint) + app.register_blueprint(tasks.views.blueprint) if CONFIG.USE_DOCS: app.register_blueprint(swagger.views.blueprint_swagger, url_prefix=SWAGGER_URL) app.register_blueprint(swagger.views.blueprint) diff --git a/task_office/columns/schemas/constants.py b/task_office/columns/schemas/constants.py new file mode 100644 index 0000000..fcc6ddb --- /dev/null +++ b/task_office/columns/schemas/constants.py @@ -0,0 +1,4 @@ +from task_office.boards.constants import BOARD_RETRIEVE_URL + +COLUMNS_PREFIX = BOARD_RETRIEVE_URL + "/columns" +COLUMNS_RETRIEVE_URL = COLUMNS_PREFIX + "/" diff --git a/task_office/core/models/db_models.py b/task_office/core/models/db_models.py index 46988b0..957cca6 100644 --- a/task_office/core/models/db_models.py +++ b/task_office/core/models/db_models.py @@ -100,7 +100,6 @@ class State(XEnum): DONE = 4, _("Done"), _("Done") expire_at = db.Column(db.DateTime, nullable=True, default=None) - label = Column(db.String(80), default="") name = Column(db.String(120), nullable=False) description = Column(db.String(120), nullable=False) diff --git a/task_office/tasks/__init__.py b/task_office/tasks/__init__.py new file mode 100644 index 0000000..67ce501 --- /dev/null +++ b/task_office/tasks/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from .views import * diff --git a/task_office/tasks/constants.py b/task_office/tasks/constants.py new file mode 100644 index 0000000..7554b79 --- /dev/null +++ b/task_office/tasks/constants.py @@ -0,0 +1,4 @@ +from task_office.boards.constants import BOARD_RETRIEVE_URL + +TASKS_PREFIX = BOARD_RETRIEVE_URL + "/tasks" +TASKS_RETRIEVE_URL = TASKS_PREFIX + "/" diff --git a/task_office/tasks/schemas/__init__.py b/task_office/tasks/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/task_office/tasks/schemas/basic_schemas.py b/task_office/tasks/schemas/basic_schemas.py new file mode 100644 index 0000000..2473772 --- /dev/null +++ b/task_office/tasks/schemas/basic_schemas.py @@ -0,0 +1,129 @@ +# coding: utf-8 +import uuid + +from marshmallow import fields, post_dump, pre_load, validates_schema +from marshmallow.validate import Length, Range +from marshmallow_enum import EnumField + +from task_office.auth.schemas import UserSchemaNested +from task_office.core.enums import XEnum, OrderingDirection +from task_office.core.models.db_models import BoardColumn, Task +from task_office.core.schemas import BaseSchema, ListInSchema, XSchema +from task_office.core.validators import PKExists +from task_office.settings import CONFIG +from task_office.swagger import API_SPEC + + +class TaskPostSchema(BaseSchema): + expire_at = fields.DateTime(required=False, allow_none=False, default=None) + label = fields.Str( + required=False, allow_none=False, validate=[Length(max=80)], default="" + ) + name = fields.Str( + required=True, allow_none=False, validate=[Length(min=1, max=120)] + ) + description = fields.Str( + required=False, allow_none=False, validate=[Length(max=120)], default="" + ) + state = EnumField( + Task.State, + required=False, + allow_none=False, + by_value=True, + default=Task.State.NEW.value, + ) + position = fields.Integer( + required=True, default=1, allow_none=False, validate=[Range(min=1)] + ) + column_uuid = fields.UUID( + required=True, validate=[PKExists(BoardColumn, "uuid")], allow_none=False + ) + + class Meta: + strict = True + + @validates_schema + def validate_schema(self, data, **kwargs): + data["column_uuid"] = str(data.pop("column_uuid")) + data["state"] = data.pop("state", Task.State.NEW).value + + +class TaskPutSchema(BaseSchema): + expire_at = fields.DateTime(required=False, allow_none=False) + label = fields.Str(required=False, allow_none=False, validate=[Length(max=80)]) + name = fields.Str( + required=False, allow_none=False, validate=[Length(min=1, max=120)] + ) + description = fields.Str( + required=False, allow_none=False, validate=[Length(max=120)] + ) + state = EnumField(Task.State, required=False, allow_none=False, by_value=True) + position = fields.Integer(required=False, allow_none=False, validate=[Range(min=1)]) + column_uuid = fields.UUID( + required=True, validate=[PKExists(BoardColumn, "uuid")], allow_none=False + ) + + class Meta: + strict = True + + @validates_schema + def validate_schema(self, data, **kwargs): + data["column_uuid"] = str(data.pop("column_uuid")) + state = data.get("state", None) + if state is not None: + data["state"] = state.value + + +class TaskOutSchema(BaseSchema): + expire_at = fields.DateTime( + attribute="expire_at", dump_only=True, format=CONFIG.API_DATETIME_FORMAT + ) + label = fields.Str(dump_only=True) + name = fields.Str(dump_only=True) + description = fields.Str(dump_only=True) + state = fields.Integer(dump_only=True) + position = fields.Integer(dump_only=True) + column_uuid = fields.UUID(dump_only=True) + performers = UserSchemaNested(many=True) + + @post_dump + def dump_data(self, data, **kwargs): + data["uuid"] = uuid.UUID(data.pop("uuid")).hex + data["column_uuid"] = uuid.UUID(data.pop("column_uuid")).hex + return data + + class Meta: + strict = True + + +task_post_schema = TaskPostSchema() +task_put_schema = TaskPutSchema() +task_out_schema = TaskOutSchema() +tasks_list_out_schema = TaskOutSchema(many=True) +API_SPEC.components.schema("TaskPostSchema", schema=TaskPostSchema) +API_SPEC.components.schema("TaskPutSchema", schema=TaskPutSchema) +API_SPEC.components.schema("TaskOutSchema", schema=TaskOutSchema) + + +class TasksListInSchema(ListInSchema): + class OrderingMap(XEnum): + CREATED_AT_ASC = ( + "-created_at", + BoardColumn.created_at.asc(), + OrderingDirection.ASC, + ) + CREATED_AET_DESC = ( + "created_at", + BoardColumn.created_at.desc(), + OrderingDirection.DESC, + ) + + searching = fields.Nested(XSchema, required=False) + ordering = EnumField(OrderingMap, required=False, by_value=True) + + class Meta: + strict = True + + +tasks_in_list_schema = TasksListInSchema() +API_SPEC.components.schema("TasksListInSchema", schema=TasksListInSchema) diff --git a/task_office/tasks/utils.py b/task_office/tasks/utils.py new file mode 100644 index 0000000..491c6d8 --- /dev/null +++ b/task_office/tasks/utils.py @@ -0,0 +1,38 @@ +from task_office.core.models.db_models import Task +from task_office.database import Model +from task_office.extensions import db + + +def reset_tasks_ordering( + instance: Model, column_uuid: str, new_value: int, old_value: int = None +) -> bool: + if new_value != old_value: + if old_value is None: + # if column already created + qs = Task.query.filter( + Task.id != instance.id, + Task.column_uuid == column_uuid, + Task.position >= new_value, + ) + qs.update(dict(position=Task.position + 1)) + elif new_value > old_value: + qs = Task.query.filter( + Task.id != instance.id, + Task.column_uuid == column_uuid, + Task.position > old_value, + Task.position <= new_value, + ) + qs.update(dict(position=Task.position - 1)) + else: + Task.query.filter( + Task.id != instance.id, + Task.column_uuid == column_uuid, + Task.position < old_value, + Task.position >= new_value, + ).update(dict(position=Task.position + 1)) + + db.session.commit() + + return True + + return False diff --git a/task_office/tasks/views.py b/task_office/tasks/views.py new file mode 100644 index 0000000..f6e909c --- /dev/null +++ b/task_office/tasks/views.py @@ -0,0 +1,179 @@ +# -*- coding: utf-8 -*- +"""Tasks views.""" +from datetime import datetime + +from flask import Blueprint +from flask_apispec import use_kwargs, marshal_with +from flask_babel import lazy_gettext as _ +from flask_jwt_extended import jwt_required, get_current_user +from sqlalchemy import func + +from .constants import TASKS_PREFIX +from .schemas.basic_schemas import ( + tasks_in_list_schema, + tasks_list_out_schema, + task_post_schema, + task_out_schema, + task_put_schema, +) +from .utils import reset_tasks_ordering +from ..core.helpers.listed_response import listed_response +from ..core.models.db_models import BoardColumn, Board, Task +from ..core.utils import validate_request_url_uuid, non_empty_query_required +from ..exceptions import InvalidUsage +from ..extensions import db + +blueprint = Blueprint("tasks", __name__, url_prefix=TASKS_PREFIX) + + +@blueprint.route("/meta", methods=("get",)) +@jwt_required +def get_meta_data(board_uuid): + """ + Additional data for tasks + """ + validate_request_url_uuid(Board, "uuid", board_uuid, True) + data = dict() + return data + + +@blueprint.route("", methods=("post",)) +@jwt_required +@use_kwargs(task_post_schema) +@marshal_with(task_out_schema) +def create_task(board_uuid, **kwargs): + """ + :param board_uuid: + :param kwargs: + :return: + """ + + data = kwargs + validate_request_url_uuid(Board, "uuid", board_uuid, True) + + # validate max position + data["position"] = data.get("position", 1) + position = data["position"] + if position > 1: + max_position = db.session.query(func.max(Task.position)).scalar() + max_position = 1 if max_position is None else max_position + 1 + if position > max_position: + raise InvalidUsage( + messages=[_("Must be between {} and {}".format(1, max_position))], + status_code=422, + key="position", + ) + + # validate unique task name for board + q_by_name = ( + BoardColumn.query.filter(board_uuid == board_uuid) + .join(Task) + .filter(Task.name == data["name"]) + .first() + ) + if q_by_name: + raise InvalidUsage( + messages=[_("Already exists with value {}".format(data["name"]))], + status_code=422, + key="name", + ) + + # set creator(current user) of task + user = get_current_user() + user_uuid = str(user.uuid) if user else None + data["creator_uuid"] = user_uuid + + # save task + task = Task(**data) + task.save() + + # reset tasks ordering fo column + reset_tasks_ordering(task, data["column_uuid"], position) + + return task + + +@blueprint.route("/", methods=("put",)) +@jwt_required +@use_kwargs(task_put_schema) +@marshal_with(task_out_schema) +def update_task(board_uuid, task_uuid, **kwargs): + """ + :param board_uuid: + :param task_uuid: + :param kwargs: + :return: + """ + data = kwargs + validate_request_url_uuid(Board, "uuid", board_uuid, True) + validate_request_url_uuid(Task, "uuid", task_uuid, True) + + # validate max position value + position = data.get("position", None) + if position is not None and position > 1: + max_position = db.session.query(func.max(Task.position)).scalar() + max_position = 1 if max_position is None else max_position + 1 + + if position > max_position: + raise InvalidUsage( + messages=[_("Must be between {} and {}.".format(1, max_position))], + status_code=422, + key="position", + ) + + # get and check is required instance exists + task = non_empty_query_required( + Task, uuid=task_uuid, column_uuid=data["column_uuid"] + )[1] + + # validate unique task name for board + name = data.get("name", None) + if name is not None and name != task.name: + q_by_name = ( + BoardColumn.query.filter(board_uuid == board_uuid) + .join(Task) + .filter(Task.name == data["name"]) + .first() + ) + if q_by_name: + raise InvalidUsage( + messages=[_("Already exists with value {}".format(data["name"]))], + status_code=422, + key="name", + ) + + if data: + old_position = task.position + task.update(updated_at=datetime.utcnow(), **data) + task.save() + if position is not None and position > 1: + reset_tasks_ordering(task, data["column_uuid"], position, old_position) + + return task + + +@blueprint.route("", methods=("get",)) +@jwt_required +@use_kwargs(tasks_in_list_schema) +def get_list_tasks(board_uuid, **kwargs): + """ + :param board_uuid: + :param kwargs: + :return: + """ + data = kwargs + + # Check board_uuid in request_url + validate_request_url_uuid(Board, "uuid", board_uuid, True) + + tasks = ( + Task.query.join(BoardColumn) + .filter(BoardColumn.board_uuid == board_uuid) + .order_by(Task.position.asc()) + ) + + # Serialize to paginated response + data = listed_response.serialize( + query=tasks, query_params=data, schema=tasks_list_out_schema + ) + return data From a86e6cc331839b2ce2e68b780060023f129a89c5 Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Sat, 15 Feb 2020 15:20:07 +0200 Subject: [PATCH 20/60] refactoring --- task_office/__init__.py | 2 +- task_office/app.py | 1 - task_office/auth/__init__.py | 2 - task_office/auth/models.py | 1 - task_office/auth/schemas.py | 18 +------- task_office/auth/views.py | 12 +++++ task_office/boards/__init__.py | 2 - task_office/boards/schemas/basic_schemas.py | 26 +++++------ task_office/boards/views.py | 19 ++++---- task_office/columns/__init__.py | 2 - .../columns/{schemas => }/constants.py | 0 task_office/columns/schemas/basic_schemas.py | 16 +++---- task_office/columns/utils.py | 1 + task_office/columns/views.py | 35 ++++++++++----- task_office/commands.py | 1 - task_office/compat.py | 1 - task_office/core/models/__init__.py | 2 +- task_office/core/models/db_models.py | 1 - task_office/core/models/mixins.py | 1 - task_office/core/schemas/__init__.py | 0 .../{schemas.py => schemas/base_schemas.py} | 4 +- task_office/core/schemas/nested_schemas.py | 44 +++++++++++++++++++ task_office/database.py | 1 - task_office/extensions.py | 1 - task_office/permissions/__init__.py | 2 - .../permissions/schemas/basic_schemas.py | 28 ++++++------ task_office/permissions/views.py | 43 +++++++++++++----- task_office/settings.py | 1 - task_office/swagger/__init__.py | 2 - task_office/swagger/views.py | 5 ++- task_office/tasks/__init__.py | 2 - task_office/tasks/schemas/basic_schemas.py | 22 +++++----- task_office/tasks/views.py | 14 +++--- task_office/utils.py | 1 - 34 files changed, 186 insertions(+), 127 deletions(-) rename task_office/columns/{schemas => }/constants.py (100%) create mode 100644 task_office/core/schemas/__init__.py rename task_office/core/{schemas.py => schemas/base_schemas.py} (97%) create mode 100644 task_office/core/schemas/nested_schemas.py diff --git a/task_office/__init__.py b/task_office/__init__.py index 40a96af..8b13789 100644 --- a/task_office/__init__.py +++ b/task_office/__init__.py @@ -1 +1 @@ -# -*- coding: utf-8 -*- + diff --git a/task_office/app.py b/task_office/app.py index 25648de..6cb9227 100644 --- a/task_office/app.py +++ b/task_office/app.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """The app module, containing the app factory function.""" from flask import Flask from task_office.auth.jwt_error_handlers import jwt_errors_map diff --git a/task_office/auth/__init__.py b/task_office/auth/__init__.py index 67ce501..6b274ab 100644 --- a/task_office/auth/__init__.py +++ b/task_office/auth/__init__.py @@ -1,3 +1 @@ -# -*- coding: utf-8 -*- - from .views import * diff --git a/task_office/auth/models.py b/task_office/auth/models.py index b23d80c..5f06e09 100644 --- a/task_office/auth/models.py +++ b/task_office/auth/models.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """User models.""" from task_office.core.models.mixins import DTMixin, PKMixin from task_office.database import Column, Model, db diff --git a/task_office/auth/schemas.py b/task_office/auth/schemas.py index 54e9307..0e3c541 100644 --- a/task_office/auth/schemas.py +++ b/task_office/auth/schemas.py @@ -6,7 +6,7 @@ from marshmallow.validate import Length from task_office.auth.models import User -from task_office.core.schemas import BaseSchema, XSchema +from task_office.core.schemas.base_schemas import BaseSchema, XSchema from task_office.core.validators import Unique from task_office.swagger import API_SPEC @@ -20,23 +20,8 @@ class Meta: strict = True -class UserSchemaNested(XSchema): - uuid = fields.UUID(dump_only=True) - username = fields.Str(dump_only=True) - email = fields.Email(dump_only=True) - - @post_dump - def dump_data(self, data, **kwargs): - data["uuid"] = uuid.UUID(data.pop("uuid")).hex - return data - - class Meta: - strict = True - - user_schema = UserSchema() user_schemas = UserSchema(many=True) -user_schema_nested = UserSchemaNested() class UserSignUpSchema(XSchema): @@ -123,7 +108,6 @@ class SignedSchema(XSchema): API_SPEC.components.schema("UserSchema", schema=UserSchema) -API_SPEC.components.schema("UserSchemaNested", schema=UserSchemaNested) API_SPEC.components.schema("UserSignInSchema", schema=UserSignInSchema) API_SPEC.components.schema("UserSignUpSchema", schema=UserSignUpSchema) API_SPEC.components.schema("TokenSchema", schema=TokenSchema) diff --git a/task_office/auth/views.py b/task_office/auth/views.py index 8cab598..366b9e8 100644 --- a/task_office/auth/views.py +++ b/task_office/auth/views.py @@ -28,6 +28,10 @@ @use_kwargs(user_signup_schema) @marshal_with(user_schema) def sign_up(**kwargs): + """ + :param kwargs: + :return: + """ data = kwargs data.pop("password_confirm") user = User(**data) @@ -39,6 +43,10 @@ def sign_up(**kwargs): @use_kwargs(user_signin_schema) @marshal_with(signed_schema) def sign_in(**kwargs): + """ + :param kwargs: + :return: + """ data = kwargs refresh_lf = datetime.timestamp( datetime.utcnow() + CONFIG.JWT_REFRESH_TOKEN_EXPIRES @@ -65,6 +73,10 @@ def sign_in(**kwargs): @jwt_refresh_token_required @marshal_with(refreshed_access_tokens_schema) def refresh(**kwargs): + """ + :param kwargs: + :return: + """ current_user = User.get_by_id(get_jwt_identity()) access_lf = datetime.timestamp(datetime.utcnow() + CONFIG.JWT_ACCESS_TOKEN_EXPIRES) return { diff --git a/task_office/boards/__init__.py b/task_office/boards/__init__.py index 67ce501..6b274ab 100644 --- a/task_office/boards/__init__.py +++ b/task_office/boards/__init__.py @@ -1,3 +1 @@ -# -*- coding: utf-8 -*- - from .views import * diff --git a/task_office/boards/schemas/basic_schemas.py b/task_office/boards/schemas/basic_schemas.py index 3d1df95..95b0f13 100644 --- a/task_office/boards/schemas/basic_schemas.py +++ b/task_office/boards/schemas/basic_schemas.py @@ -6,14 +6,14 @@ from marshmallow_enum import EnumField from task_office.auth import User -from task_office.auth.schemas import UserSchemaNested from task_office.core.enums import XEnum -from task_office.core.schemas import BaseSchema, ListInSchema, XSchema +from task_office.core.schemas.base_schemas import BaseSchema, ListSchema, XSchema +from task_office.core.schemas.nested_schemas import NestedUserDumpSchema from task_office.core.validators import PKExists from task_office.swagger import API_SPEC -class BoardInSchema(BaseSchema): +class BoardPutSchema(BaseSchema): name = fields.Str(required=True, allow_none=False, validate=[Length(max=255)]) description = fields.Str(allow_none=True, required=False, default="") owner_uuid = fields.UUID(required=True, validate=[PKExists(User, "uuid")]) @@ -27,10 +27,10 @@ def validate_schema(self, data, **kwargs): data["owner_uuid"] = str(data.pop("owner_uuid")) -class BoardOutSchema(BaseSchema): +class BoardDumpSchema(BaseSchema): name = fields.Str(dump_only=True) description = fields.Str(dump_only=True) - owner = fields.Nested(UserSchemaNested, dump_only=True) + owner = fields.Nested(NestedUserDumpSchema, dump_only=True) is_active = fields.Boolean(dump_only=True) class Meta: @@ -41,14 +41,14 @@ def validate_schema(self, data, **kwargs): data["uuid"] = uuid.UUID(data.pop("uuid")).hex -board_in_schema = BoardInSchema() -board_out_schema = BoardOutSchema() -boards_list_out_schema = BoardOutSchema(many=True) -API_SPEC.components.schema("BoardInSchema", schema=BoardInSchema) -API_SPEC.components.schema("BoardOutSchema", schema=BoardOutSchema) +board_put_schema = BoardPutSchema() +board_dump_schema = BoardDumpSchema() +board_list_dump_schema = BoardDumpSchema(many=True) +API_SPEC.components.schema("BoardPutSchema", schema=BoardPutSchema) +API_SPEC.components.schema("BoardDumpSchema", schema=BoardDumpSchema) -class BoardListInSchema(ListInSchema): +class BoardListQuerySchema(ListSchema): class OrderingMap(XEnum): pass @@ -59,5 +59,5 @@ class Meta: strict = True -boards_in_list_schema = BoardListInSchema() -API_SPEC.components.schema("BoardInListSchema", schema=BoardListInSchema) +board_list_query_schema = BoardListQuerySchema() +API_SPEC.components.schema("BoardListQuerySchema", schema=BoardListQuerySchema) diff --git a/task_office/boards/views.py b/task_office/boards/views.py index d61005e..21dd48a 100644 --- a/task_office/boards/views.py +++ b/task_office/boards/views.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """Boards views.""" from flask_babel import lazy_gettext as _ from flask import Blueprint @@ -7,10 +6,10 @@ from .constants import BOARDS_PREFIX from .schemas.basic_schemas import ( - board_in_schema, - boards_in_list_schema, - boards_list_out_schema, - board_out_schema, + board_put_schema, + board_list_query_schema, + board_list_dump_schema, + board_dump_schema, ) from ..core.helpers.listed_response import listed_response from ..core.models.db_models import Board, Permission @@ -22,8 +21,8 @@ @blueprint.route("", methods=("post",)) @jwt_required -@use_kwargs(board_in_schema) -@marshal_with(board_out_schema) +@use_kwargs(board_put_schema) +@marshal_with(board_dump_schema) def create_boards(**kwargs): data = kwargs # Check name, owner_uuid are unique for board @@ -43,7 +42,7 @@ def create_boards(**kwargs): @blueprint.route("", methods=("get",)) @jwt_required -@use_kwargs(boards_in_list_schema) +@use_kwargs(board_list_query_schema) def get_list_boards(**kwargs): data = kwargs user = get_current_user() @@ -51,14 +50,14 @@ def get_list_boards(**kwargs): boards = Board.query.join(Permission).filter(Permission.user_uuid == user.uuid) # Serialize to paginated response data = listed_response.serialize( - query=boards, query_params=data, schema=boards_list_out_schema + query=boards, query_params=data, schema=board_list_dump_schema ) return data @blueprint.route("/", methods=("get",)) @jwt_required -@marshal_with(board_out_schema) +@marshal_with(board_dump_schema) def get_board_by_uuid(board_uuid): # board_uuid in request url if not is_uuid(board_uuid): diff --git a/task_office/columns/__init__.py b/task_office/columns/__init__.py index 67ce501..6b274ab 100644 --- a/task_office/columns/__init__.py +++ b/task_office/columns/__init__.py @@ -1,3 +1 @@ -# -*- coding: utf-8 -*- - from .views import * diff --git a/task_office/columns/schemas/constants.py b/task_office/columns/constants.py similarity index 100% rename from task_office/columns/schemas/constants.py rename to task_office/columns/constants.py diff --git a/task_office/columns/schemas/basic_schemas.py b/task_office/columns/schemas/basic_schemas.py index 83ab8d3..83c7c63 100644 --- a/task_office/columns/schemas/basic_schemas.py +++ b/task_office/columns/schemas/basic_schemas.py @@ -7,7 +7,7 @@ from task_office.core.enums import XEnum, OrderingDirection from task_office.core.models.db_models import BoardColumn -from task_office.core.schemas import BaseSchema, ListInSchema, XSchema +from task_office.core.schemas.base_schemas import BaseSchema, ListSchema, XSchema from task_office.swagger import API_SPEC @@ -29,7 +29,7 @@ class Meta: strict = True -class ColumnOutSchema(BaseSchema): +class ColumnDumpSchema(BaseSchema): name = fields.Str(dump_only=True) position = fields.Integer(dump_only=True) board_uuid = fields.UUID(dump_only=True) @@ -46,14 +46,14 @@ class Meta: column_post_schema = ColumnPostSchema() column_put_schema = ColumnPutSchema() -column_out_schema = ColumnOutSchema() -columns_list_out_schema = ColumnOutSchema(many=True) +column_dump_schema = ColumnDumpSchema() +column_listed_dump_schema = ColumnDumpSchema(many=True) API_SPEC.components.schema("ColumnPostSchema", schema=ColumnPostSchema) API_SPEC.components.schema("ColumnPutSchema", schema=ColumnPutSchema) -API_SPEC.components.schema("ColumnOutSchema", schema=ColumnOutSchema) +API_SPEC.components.schema("ColumnDumpSchema", schema=ColumnDumpSchema) -class ColumnsListInSchema(ListInSchema): +class ColumnListQuerySchema(ListSchema): class OrderingMap(XEnum): CREATED_AT_ASC = ( "-created_at", @@ -73,5 +73,5 @@ class Meta: strict = True -columns_in_list_schema = ColumnsListInSchema() -API_SPEC.components.schema("ColumnsListInSchema", schema=ColumnsListInSchema) +column_list_query_schema = ColumnListQuerySchema() +API_SPEC.components.schema("ColumnListQuerySchema", schema=ColumnListQuerySchema) diff --git a/task_office/columns/utils.py b/task_office/columns/utils.py index 40b161e..2031e5f 100644 --- a/task_office/columns/utils.py +++ b/task_office/columns/utils.py @@ -1,3 +1,4 @@ +"""Columns utils.""" from task_office.core.models.db_models import BoardColumn from task_office.database import Model from task_office.extensions import db diff --git a/task_office/columns/views.py b/task_office/columns/views.py index cd2a6b6..27897de 100644 --- a/task_office/columns/views.py +++ b/task_office/columns/views.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """Columns views.""" from datetime import datetime @@ -8,12 +7,12 @@ from flask_jwt_extended import jwt_required from sqlalchemy import func -from task_office.boards.constants import BOARD_RETRIEVE_URL +from .constants import COLUMNS_PREFIX from .schemas.basic_schemas import ( column_post_schema, - columns_in_list_schema, - columns_list_out_schema, - column_out_schema, + column_list_query_schema, + column_listed_dump_schema, + column_dump_schema, column_put_schema, ) from .utils import reset_columns_ordering @@ -23,7 +22,7 @@ from ..exceptions import InvalidUsage from ..extensions import db -blueprint = Blueprint("columns", __name__, url_prefix=BOARD_RETRIEVE_URL + "/columns") +blueprint = Blueprint("columns", __name__, url_prefix=COLUMNS_PREFIX) @blueprint.route("/meta", methods=("get",)) @@ -40,8 +39,13 @@ def get_meta_data(board_uuid): @blueprint.route("", methods=("post",)) @jwt_required @use_kwargs(column_post_schema) -@marshal_with(column_out_schema) +@marshal_with(column_dump_schema) def create_column(board_uuid, **kwargs): + """ + :param board_uuid: + :param kwargs: + :return: + """ data = kwargs validate_request_url_uuid(Board, "uuid", board_uuid, True) @@ -80,8 +84,14 @@ def create_column(board_uuid, **kwargs): @blueprint.route("/", methods=("put",)) @jwt_required @use_kwargs(column_put_schema) -@marshal_with(column_out_schema) +@marshal_with(column_dump_schema) def update_column(board_uuid, column_uuid, **kwargs): + """ + :param board_uuid: + :param column_uuid: + :param kwargs: + :return: + """ data = kwargs validate_request_url_uuid(Board, "uuid", board_uuid, True) @@ -129,8 +139,13 @@ def update_column(board_uuid, column_uuid, **kwargs): @blueprint.route("", methods=("get",)) @jwt_required -@use_kwargs(columns_in_list_schema) +@use_kwargs(column_list_query_schema) def get_list_columns(board_uuid, **kwargs): + """ + :param board_uuid: + :param kwargs: + :return: + """ data = kwargs # Check board_uuid in request_url @@ -142,6 +157,6 @@ def get_list_columns(board_uuid, **kwargs): # Serialize to paginated response data = listed_response.serialize( - query=columns, query_params=data, schema=columns_list_out_schema + query=columns, query_params=data, schema=column_listed_dump_schema ) return data diff --git a/task_office/commands.py b/task_office/commands.py index c7cb61e..306dc7c 100644 --- a/task_office/commands.py +++ b/task_office/commands.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """Click commands.""" import os diff --git a/task_office/compat.py b/task_office/compat.py index d9fae32..448c90a 100644 --- a/task_office/compat.py +++ b/task_office/compat.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """Python 2/3 compatibility module.""" import sys diff --git a/task_office/core/models/__init__.py b/task_office/core/models/__init__.py index 40a96af..8b13789 100644 --- a/task_office/core/models/__init__.py +++ b/task_office/core/models/__init__.py @@ -1 +1 @@ -# -*- coding: utf-8 -*- + diff --git a/task_office/core/models/db_models.py b/task_office/core/models/db_models.py index 957cca6..648d7f7 100644 --- a/task_office/core/models/db_models.py +++ b/task_office/core/models/db_models.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """Boards models.""" from flask_babel import lazy_gettext as _ from sqlalchemy.dialects.postgresql import UUID diff --git a/task_office/core/models/mixins.py b/task_office/core/models/mixins.py index d50aeff..f7b51b0 100644 --- a/task_office/core/models/mixins.py +++ b/task_office/core/models/mixins.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import datetime as dt import uuid as uuid diff --git a/task_office/core/schemas/__init__.py b/task_office/core/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/task_office/core/schemas.py b/task_office/core/schemas/base_schemas.py similarity index 97% rename from task_office/core/schemas.py rename to task_office/core/schemas/base_schemas.py index fa19605..c81a341 100644 --- a/task_office/core/schemas.py +++ b/task_office/core/schemas/base_schemas.py @@ -49,7 +49,7 @@ def dump_data(self, data, **kwargs): return data -class ListInSchema(XSchema): +class ListSchema(XSchema): error_messages = { "max_limit_exceeded": _("Max limit {} exceeded").format(CONFIG.MAX_LIMIT_VALUE) } @@ -64,4 +64,4 @@ def validate_schema(self, data, **kwargs): self.throw_error(value="", key_error="max_limit_exceeded", code=400) -list_in_schema = ListInSchema() +list_schema = ListSchema() diff --git a/task_office/core/schemas/nested_schemas.py b/task_office/core/schemas/nested_schemas.py new file mode 100644 index 0000000..09c8b26 --- /dev/null +++ b/task_office/core/schemas/nested_schemas.py @@ -0,0 +1,44 @@ +# coding: utf-8 +import uuid + +from marshmallow import fields, post_dump + +from task_office.core.schemas.base_schemas import XSchema +from task_office.swagger import API_SPEC + + +class NestedUserDumpSchema(XSchema): + uuid = fields.UUID(dump_only=True) + username = fields.Str(dump_only=True) + email = fields.Email(dump_only=True) + + @post_dump + def dump_data(self, data, **kwargs): + data["uuid"] = uuid.UUID(data.pop("uuid")).hex + return data + + class Meta: + strict = True + + +nested_user_dump_schema = NestedUserDumpSchema() + + +class NestedColumnDumpSchema(XSchema): + uuid = fields.UUID(dump_only=True) + name = fields.Str(dump_only=True) + + @post_dump + def dump_data(self, data, **kwargs): + data["uuid"] = uuid.UUID(data.pop("uuid")).hex + return data + + class Meta: + strict = True + + +nested_column_dump_schema = NestedColumnDumpSchema() + + +API_SPEC.components.schema("NestedUserSchema", schema=NestedUserDumpSchema) +API_SPEC.components.schema("NestedColumnDumpSchema", schema=NestedColumnDumpSchema) diff --git a/task_office/database.py b/task_office/database.py index 2511095..fea5875 100644 --- a/task_office/database.py +++ b/task_office/database.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """Database module, including the SQLAlchemy database object and DB-related utilities.""" from sqlalchemy.orm import relationship diff --git a/task_office/extensions.py b/task_office/extensions.py index 82b79db..a44e2a5 100644 --- a/task_office/extensions.py +++ b/task_office/extensions.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """Extensions module. Each extension is initialized in the app factory located in app.py.""" from flask import g, request from flask_babel import Babel diff --git a/task_office/permissions/__init__.py b/task_office/permissions/__init__.py index 67ce501..6b274ab 100644 --- a/task_office/permissions/__init__.py +++ b/task_office/permissions/__init__.py @@ -1,3 +1 @@ -# -*- coding: utf-8 -*- - from .views import * diff --git a/task_office/permissions/schemas/basic_schemas.py b/task_office/permissions/schemas/basic_schemas.py index 52e4a32..248a69b 100644 --- a/task_office/permissions/schemas/basic_schemas.py +++ b/task_office/permissions/schemas/basic_schemas.py @@ -5,15 +5,15 @@ from marshmallow_enum import EnumField from task_office.auth import User -from task_office.auth.schemas import UserSchemaNested from task_office.core.enums import XEnum, OrderingDirection from task_office.core.models.db_models import Permission -from task_office.core.schemas import BaseSchema, ListInSchema, XSchema +from task_office.core.schemas.base_schemas import BaseSchema, ListSchema, XSchema +from task_office.core.schemas.nested_schemas import NestedUserDumpSchema from task_office.core.validators import PKExists from task_office.swagger import API_SPEC -class PermissionInSchema(BaseSchema): +class PermissionQuerySchema(BaseSchema): role = EnumField(Permission.Role, required=True, by_value=True) user_uuid = fields.UUID( required=True, validate=[PKExists(User, "uuid")], allow_none=False @@ -28,10 +28,10 @@ def validate_schema(self, data, **kwargs): data["role"] = data.pop("role").value -class PermissionOutSchema(BaseSchema): +class PermissionDumpSchema(BaseSchema): role = fields.Integer(dump_only=True) board_uuid = fields.UUID(dump_only=True) - user = fields.Nested(UserSchemaNested, dump_only=True) + user = fields.Nested(NestedUserDumpSchema, dump_only=True) @post_dump def dump_data(self, data, **kwargs): @@ -43,14 +43,14 @@ class Meta: strict = True -permission_in_schema = PermissionInSchema() -permission_out_schema = PermissionOutSchema() -permission_list_out_schema = PermissionOutSchema(many=True) -API_SPEC.components.schema("PermissionInSchema", schema=PermissionInSchema) -API_SPEC.components.schema("PermissionOutSchema", schema=PermissionOutSchema) +permission_query_schema = PermissionQuerySchema() +permission_dump_schema = PermissionDumpSchema() +permission_list_dump_schema = PermissionDumpSchema(many=True) +API_SPEC.components.schema("PermissionInSchema", schema=PermissionQuerySchema) +API_SPEC.components.schema("PermissionOutSchema", schema=PermissionDumpSchema) -class PermissionsListInSchema(ListInSchema): +class PermissionListQuerySchema(ListSchema): class OrderingMap(XEnum): CREATED_AT_ASC = ( "-created_at", @@ -70,5 +70,7 @@ class Meta: strict = True -permissions_in_list_schema = PermissionsListInSchema() -API_SPEC.components.schema("PermissionsListInSchema", schema=PermissionsListInSchema) +permissions_list_query_schema = PermissionListQuerySchema() +API_SPEC.components.schema( + "PermissionListQuerySchema", schema=PermissionListQuerySchema +) diff --git a/task_office/permissions/views.py b/task_office/permissions/views.py index b83722e..9231b21 100644 --- a/task_office/permissions/views.py +++ b/task_office/permissions/views.py @@ -9,10 +9,10 @@ from task_office.boards.constants import BOARD_RETRIEVE_URL from .schemas.basic_schemas import ( - permission_in_schema, - permission_out_schema, - permissions_in_list_schema, - permission_list_out_schema, + permission_query_schema, + permission_dump_schema, + permissions_list_query_schema, + permission_list_dump_schema, ) from ..core.helpers.listed_response import listed_response from ..core.models.db_models import Permission, Board @@ -41,9 +41,14 @@ def get_meta_data(board_uuid): @blueprint.route("", methods=("post",)) @jwt_required -@use_kwargs(permission_in_schema) -@marshal_with(permission_out_schema) +@use_kwargs(permission_query_schema) +@marshal_with(permission_dump_schema) def create_permission(board_uuid, **kwargs): + """ + :param board_uuid: + :param kwargs: + :return: + """ data = kwargs # Check board_uuid in request_url if not is_uuid(board_uuid): @@ -67,9 +72,15 @@ def create_permission(board_uuid, **kwargs): @blueprint.route("/", methods=("put",)) @jwt_required -@use_kwargs(permission_in_schema) -@marshal_with(permission_out_schema) +@use_kwargs(permission_query_schema) +@marshal_with(permission_dump_schema) def update_permission(board_uuid, permission_uuid, **kwargs): + """ + :param board_uuid: + :param permission_uuid: + :param kwargs: + :return: + """ data = kwargs # Check is valid board uuid and permission_uuid in request url if not is_uuid(board_uuid) or not is_uuid(permission_uuid): @@ -93,8 +104,13 @@ def update_permission(board_uuid, permission_uuid, **kwargs): @blueprint.route("", methods=("get",)) @jwt_required -@use_kwargs(permissions_in_list_schema) +@use_kwargs(permissions_list_query_schema) def get_list_permission(board_uuid, **kwargs): + """ + :param board_uuid: + :param kwargs: + :return: + """ data = kwargs # Check board_uuid in request_url if not is_uuid(board_uuid): @@ -104,15 +120,20 @@ def get_list_permission(board_uuid, **kwargs): # Serialize to paginated response data = listed_response.serialize( - query=permissions, query_params=data, schema=permission_list_out_schema + query=permissions, query_params=data, schema=permission_list_dump_schema ) return data @blueprint.route("/", methods=("get",)) @jwt_required -@marshal_with(permission_out_schema) +@marshal_with(permission_dump_schema) def get_permission_by_uuid(board_uuid, permission_uuid): + """ + :param board_uuid: + :param permission_uuid: + :return: + """ # Check is valid board uuid and permission_uuid in request url if not is_uuid(board_uuid) or not is_uuid(permission_uuid): raise InvalidUsage(messages=[_("Not found")], status_code=404) diff --git a/task_office/settings.py b/task_office/settings.py index 0c75efb..23dc3c5 100644 --- a/task_office/settings.py +++ b/task_office/settings.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """Application configuration.""" import os from datetime import timedelta diff --git a/task_office/swagger/__init__.py b/task_office/swagger/__init__.py index 67ce501..6b274ab 100644 --- a/task_office/swagger/__init__.py +++ b/task_office/swagger/__init__.py @@ -1,3 +1 @@ -# -*- coding: utf-8 -*- - from .views import * diff --git a/task_office/swagger/views.py b/task_office/swagger/views.py index 8934651..1f55785 100644 --- a/task_office/swagger/views.py +++ b/task_office/swagger/views.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """Swagger views.""" from flask import Blueprint, jsonify @@ -21,4 +20,8 @@ @blueprint.route("/open-api", methods=("get",)) def api_swagger(**kwargs): + """ + :param kwargs: + :return: + """ return jsonify(API_SPEC.to_dict()) diff --git a/task_office/tasks/__init__.py b/task_office/tasks/__init__.py index 67ce501..6b274ab 100644 --- a/task_office/tasks/__init__.py +++ b/task_office/tasks/__init__.py @@ -1,3 +1 @@ -# -*- coding: utf-8 -*- - from .views import * diff --git a/task_office/tasks/schemas/basic_schemas.py b/task_office/tasks/schemas/basic_schemas.py index 2473772..f1ba551 100644 --- a/task_office/tasks/schemas/basic_schemas.py +++ b/task_office/tasks/schemas/basic_schemas.py @@ -1,14 +1,14 @@ # coding: utf-8 import uuid -from marshmallow import fields, post_dump, pre_load, validates_schema +from marshmallow import fields, post_dump, validates_schema from marshmallow.validate import Length, Range from marshmallow_enum import EnumField -from task_office.auth.schemas import UserSchemaNested from task_office.core.enums import XEnum, OrderingDirection from task_office.core.models.db_models import BoardColumn, Task -from task_office.core.schemas import BaseSchema, ListInSchema, XSchema +from task_office.core.schemas.base_schemas import BaseSchema, ListSchema, XSchema +from task_office.core.schemas.nested_schemas import NestedUserDumpSchema from task_office.core.validators import PKExists from task_office.settings import CONFIG from task_office.swagger import API_SPEC @@ -74,7 +74,7 @@ def validate_schema(self, data, **kwargs): data["state"] = state.value -class TaskOutSchema(BaseSchema): +class TaskDumpSchema(BaseSchema): expire_at = fields.DateTime( attribute="expire_at", dump_only=True, format=CONFIG.API_DATETIME_FORMAT ) @@ -84,7 +84,7 @@ class TaskOutSchema(BaseSchema): state = fields.Integer(dump_only=True) position = fields.Integer(dump_only=True) column_uuid = fields.UUID(dump_only=True) - performers = UserSchemaNested(many=True) + performers = NestedUserDumpSchema(many=True) @post_dump def dump_data(self, data, **kwargs): @@ -98,14 +98,14 @@ class Meta: task_post_schema = TaskPostSchema() task_put_schema = TaskPutSchema() -task_out_schema = TaskOutSchema() -tasks_list_out_schema = TaskOutSchema(many=True) +task_dump_schema = TaskDumpSchema() +tasks_listed_dump_schema = TaskDumpSchema(many=True) API_SPEC.components.schema("TaskPostSchema", schema=TaskPostSchema) API_SPEC.components.schema("TaskPutSchema", schema=TaskPutSchema) -API_SPEC.components.schema("TaskOutSchema", schema=TaskOutSchema) +API_SPEC.components.schema("TaskDumpSchema", schema=TaskDumpSchema) -class TasksListInSchema(ListInSchema): +class TaskListQuerySchema(ListSchema): class OrderingMap(XEnum): CREATED_AT_ASC = ( "-created_at", @@ -125,5 +125,5 @@ class Meta: strict = True -tasks_in_list_schema = TasksListInSchema() -API_SPEC.components.schema("TasksListInSchema", schema=TasksListInSchema) +task_list_query_schema = TaskListQuerySchema() +API_SPEC.components.schema("TaskListQuerySchema", schema=TaskListQuerySchema) diff --git a/task_office/tasks/views.py b/task_office/tasks/views.py index f6e909c..faed40a 100644 --- a/task_office/tasks/views.py +++ b/task_office/tasks/views.py @@ -10,10 +10,10 @@ from .constants import TASKS_PREFIX from .schemas.basic_schemas import ( - tasks_in_list_schema, - tasks_list_out_schema, + task_list_query_schema, + tasks_listed_dump_schema, task_post_schema, - task_out_schema, + task_dump_schema, task_put_schema, ) from .utils import reset_tasks_ordering @@ -40,7 +40,7 @@ def get_meta_data(board_uuid): @blueprint.route("", methods=("post",)) @jwt_required @use_kwargs(task_post_schema) -@marshal_with(task_out_schema) +@marshal_with(task_dump_schema) def create_task(board_uuid, **kwargs): """ :param board_uuid: @@ -96,7 +96,7 @@ def create_task(board_uuid, **kwargs): @blueprint.route("/", methods=("put",)) @jwt_required @use_kwargs(task_put_schema) -@marshal_with(task_out_schema) +@marshal_with(task_dump_schema) def update_task(board_uuid, task_uuid, **kwargs): """ :param board_uuid: @@ -154,7 +154,7 @@ def update_task(board_uuid, task_uuid, **kwargs): @blueprint.route("", methods=("get",)) @jwt_required -@use_kwargs(tasks_in_list_schema) +@use_kwargs(task_list_query_schema) def get_list_tasks(board_uuid, **kwargs): """ :param board_uuid: @@ -174,6 +174,6 @@ def get_list_tasks(board_uuid, **kwargs): # Serialize to paginated response data = listed_response.serialize( - query=tasks, query_params=data, schema=tasks_list_out_schema + query=tasks, query_params=data, schema=tasks_listed_dump_schema ) return data diff --git a/task_office/utils.py b/task_office/utils.py index 10cf963..735a4c5 100644 --- a/task_office/utils.py +++ b/task_office/utils.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """Helper utilities and decorators.""" From 0c655643fba1ea31a2fde2fe7ed394a8f1fcaec3 Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Sun, 16 Feb 2020 10:27:55 +0200 Subject: [PATCH 21/60] fixed task list ordering --- task_office/core/helpers/listed_response.py | 2 +- task_office/tasks/schemas/basic_schemas.py | 34 ++++++++++++--------- task_office/tasks/schemas/search_schemas.py | 21 +++++++++++++ task_office/tasks/views.py | 6 +--- 4 files changed, 42 insertions(+), 21 deletions(-) create mode 100644 task_office/tasks/schemas/search_schemas.py diff --git a/task_office/core/helpers/listed_response.py b/task_office/core/helpers/listed_response.py index d9bf85d..1501dd8 100644 --- a/task_office/core/helpers/listed_response.py +++ b/task_office/core/helpers/listed_response.py @@ -10,7 +10,7 @@ def _get_query_ordered(query, order_param): @staticmethod def _get_query_filtered(query, filter_params): if filter_params: - query.filter(**filter_params) + query = query.filter(**filter_params) return query @staticmethod diff --git a/task_office/tasks/schemas/basic_schemas.py b/task_office/tasks/schemas/basic_schemas.py index f1ba551..9655a54 100644 --- a/task_office/tasks/schemas/basic_schemas.py +++ b/task_office/tasks/schemas/basic_schemas.py @@ -1,7 +1,6 @@ -# coding: utf-8 import uuid -from marshmallow import fields, post_dump, validates_schema +from marshmallow import fields, post_dump, validates_schema, pre_load from marshmallow.validate import Length, Range from marshmallow_enum import EnumField @@ -12,6 +11,7 @@ from task_office.core.validators import PKExists from task_office.settings import CONFIG from task_office.swagger import API_SPEC +from task_office.tasks.schemas.search_schemas import SearchTaskSchema class TaskPostSchema(BaseSchema): @@ -107,23 +107,27 @@ class Meta: class TaskListQuerySchema(ListSchema): class OrderingMap(XEnum): - CREATED_AT_ASC = ( - "-created_at", - BoardColumn.created_at.asc(), - OrderingDirection.ASC, - ) - CREATED_AET_DESC = ( - "created_at", - BoardColumn.created_at.desc(), - OrderingDirection.DESC, - ) - - searching = fields.Nested(XSchema, required=False) - ordering = EnumField(OrderingMap, required=False, by_value=True) + CREATED_AT_ASC = ("-created_at", Task.created_at.asc(), OrderingDirection.ASC) + CREATED_AT_DESC = ("created_at", Task.created_at.desc(), OrderingDirection.DESC) + POSITION_ASC = ("-position", Task.position.asc(), OrderingDirection.ASC) + POSITION_DESC = ("position", Task.position.desc(), OrderingDirection.DESC) + + searching = fields.Nested(SearchTaskSchema, required=False) + ordering = EnumField( + OrderingMap, + required=False, + by_value=True, + default=OrderingMap.POSITION_DESC.value, + ) class Meta: strict = True + @pre_load + def preload_data(self, data, **kwargs): + data["ordering"] = data.get("ordering", self.OrderingMap.POSITION_ASC) + return data + task_list_query_schema = TaskListQuerySchema() API_SPEC.components.schema("TaskListQuerySchema", schema=TaskListQuerySchema) diff --git a/task_office/tasks/schemas/search_schemas.py b/task_office/tasks/schemas/search_schemas.py new file mode 100644 index 0000000..3fb12f7 --- /dev/null +++ b/task_office/tasks/schemas/search_schemas.py @@ -0,0 +1,21 @@ +from marshmallow import fields, pre_load + +from task_office.core.schemas.base_schemas import XSchema +from task_office.swagger import API_SPEC + + +class SearchTaskSchema(XSchema): + label = fields.Str(required=False, allow_none=False) + name = fields.Str(required=False, allow_none=False) + description = fields.Str(required=False) + + class Meta: + strict = True + + @pre_load + def preload_data(self, data, **kwargs): + return data + + +search_task_schema = SearchTaskSchema() +API_SPEC.components.schema("SearchTaskSchema", schema=SearchTaskSchema) diff --git a/task_office/tasks/views.py b/task_office/tasks/views.py index faed40a..8dbd923 100644 --- a/task_office/tasks/views.py +++ b/task_office/tasks/views.py @@ -166,11 +166,7 @@ def get_list_tasks(board_uuid, **kwargs): # Check board_uuid in request_url validate_request_url_uuid(Board, "uuid", board_uuid, True) - tasks = ( - Task.query.join(BoardColumn) - .filter(BoardColumn.board_uuid == board_uuid) - .order_by(Task.position.asc()) - ) + tasks = Task.query.join(BoardColumn).filter(BoardColumn.board_uuid == board_uuid) # Serialize to paginated response data = listed_response.serialize( From 7d9ae7f95d61aea92ef09ff91fd7e34fc02e7f4d Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Sat, 22 Feb 2020 12:27:49 +0200 Subject: [PATCH 22/60] implemented tasks search --- task_office/core/constants.py | 8 ++++++++ task_office/core/helpers/listed_response.py | 6 +++++- task_office/core/schemas/base_schemas.py | 20 +++++++++++++++++++- task_office/core/utils.py | 19 +++++++++++++++---- task_office/tasks/schemas/search_schemas.py | 19 ++++++++++++------- 5 files changed, 59 insertions(+), 13 deletions(-) create mode 100644 task_office/core/constants.py diff --git a/task_office/core/constants.py b/task_office/core/constants.py new file mode 100644 index 0000000..4ad2102 --- /dev/null +++ b/task_office/core/constants.py @@ -0,0 +1,8 @@ +LOOKUP_MAP = { + "gt": lambda q, k, v: q.filter(k > v), + "gte": lambda q, k, v: q.filter(k >= v), + "lt": lambda q, k, v: q.filter(k < v), + "lte": lambda q, k, v: q.filter(k <= v), + "e": lambda q, k, v: q.filter(k == v), + "ne": lambda q, k, v: q.filter(k != v), +} diff --git a/task_office/core/helpers/listed_response.py b/task_office/core/helpers/listed_response.py index 1501dd8..1f27cbe 100644 --- a/task_office/core/helpers/listed_response.py +++ b/task_office/core/helpers/listed_response.py @@ -1,3 +1,6 @@ +from task_office.core.utils import lookup_filter + + class ListedResponseHelper: RESPONSE_TEMPLATE = {"count": 0, "results": []} @@ -10,7 +13,8 @@ def _get_query_ordered(query, order_param): @staticmethod def _get_query_filtered(query, filter_params): if filter_params: - query = query.filter(**filter_params) + for k, v in filter_params.items(): + query = lookup_filter(query, v["key"], v["value"], v["lookup"]) return query @staticmethod diff --git a/task_office/core/schemas/base_schemas.py b/task_office/core/schemas/base_schemas.py index c81a341..ab0cbe1 100644 --- a/task_office/core/schemas/base_schemas.py +++ b/task_office/core/schemas/base_schemas.py @@ -2,7 +2,7 @@ import uuid from flask_babel import lazy_gettext as _ -from marshmallow import Schema, fields, validates_schema, post_dump +from marshmallow import Schema, fields, validates_schema, post_dump, post_load from task_office.exceptions import InvalidUsage from task_office.settings import CONFIG @@ -65,3 +65,21 @@ def validate_schema(self, data, **kwargs): list_schema = ListSchema() + + +class SearchSchema(XSchema): + FIELDS_MAP = {} + + @post_load + def post_load_data(self, data, **kwargs): + res = {} + for k, v in data.items(): + k_separated = k.split("__") + key = k_separated[0] + key_mapped = self.FIELDS_MAP[key] + lookup = k_separated[1] if "__" in k else "" + res[key] = {"value": v, "lookup": lookup, "key": key_mapped} + return res + + +search_schema = SearchSchema() diff --git a/task_office/core/utils.py b/task_office/core/utils.py index 3940642..93e92fb 100644 --- a/task_office/core/utils.py +++ b/task_office/core/utils.py @@ -1,17 +1,22 @@ +from typing import Any from uuid import UUID from flask import request from flask_babel import lazy_gettext as _ +from flask_sqlalchemy import BaseQuery -from task_office.database import Model +from task_office.core.constants import LOOKUP_MAP from task_office.exceptions import InvalidUsage +from task_office.extensions import db +Model = db.Model -def _query(model, **params): + +def _query(model: Model, **params) -> BaseQuery: return model.query.filter_by(**params) -def non_empty_query_required(model, **params): +def non_empty_query_required(model: Model, **params) -> (BaseQuery, Model): qs = _query(model, **params) obj_first = qs.first() if not obj_first: @@ -19,7 +24,7 @@ def non_empty_query_required(model, **params): return qs, obj_first -def empty_query_required(model, **params): +def empty_query_required(model: Model, **params) -> (BaseQuery, Model): qs = _query(model, **params) obj_first = qs.first() if obj_first: @@ -27,6 +32,12 @@ def empty_query_required(model, **params): return qs, obj_first +def lookup_filter( + query: BaseQuery, key: str, value: Any, lookup: str = "" +) -> BaseQuery: + return LOOKUP_MAP.get(lookup, LOOKUP_MAP.get("e"))(query, key, value) + + def is_uuid(uuid): try: UUID(uuid).version diff --git a/task_office/tasks/schemas/search_schemas.py b/task_office/tasks/schemas/search_schemas.py index 3fb12f7..6df5940 100644 --- a/task_office/tasks/schemas/search_schemas.py +++ b/task_office/tasks/schemas/search_schemas.py @@ -1,21 +1,26 @@ -from marshmallow import fields, pre_load +from marshmallow import fields -from task_office.core.schemas.base_schemas import XSchema +from task_office.core.models.db_models import Task +from task_office.core.schemas.base_schemas import SearchSchema from task_office.swagger import API_SPEC -class SearchTaskSchema(XSchema): +class SearchTaskSchema(SearchSchema): + FIELDS_MAP = { + "label": Task.label, + "name": Task.name, + "description": Task.description, + "position": Task.position, + } + label = fields.Str(required=False, allow_none=False) name = fields.Str(required=False, allow_none=False) description = fields.Str(required=False) + position = fields.Integer(required=False) class Meta: strict = True - @pre_load - def preload_data(self, data, **kwargs): - return data - search_task_schema = SearchTaskSchema() API_SPEC.components.schema("SearchTaskSchema", schema=SearchTaskSchema) From a402280b34ae22e7c551ca47cf2db0f8fd7518a5 Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Sat, 22 Feb 2020 15:11:17 +0200 Subject: [PATCH 23/60] implemented tasks by columns --- task_office/columns/schemas/basic_schemas.py | 3 -- task_office/core/models/db_models.py | 4 +- task_office/tasks/schemas/basic_schemas.py | 44 +++++++++++++++++++- task_office/tasks/views.py | 26 ++++++++++++ 4 files changed, 72 insertions(+), 5 deletions(-) diff --git a/task_office/columns/schemas/basic_schemas.py b/task_office/columns/schemas/basic_schemas.py index 83c7c63..57f8bb8 100644 --- a/task_office/columns/schemas/basic_schemas.py +++ b/task_office/columns/schemas/basic_schemas.py @@ -1,4 +1,3 @@ -# coding: utf-8 import uuid from marshmallow import fields, post_dump @@ -32,11 +31,9 @@ class Meta: class ColumnDumpSchema(BaseSchema): name = fields.Str(dump_only=True) position = fields.Integer(dump_only=True) - board_uuid = fields.UUID(dump_only=True) @post_dump def dump_data(self, data, **kwargs): - data["board_uuid"] = uuid.UUID(data.pop("board_uuid")).hex data["uuid"] = uuid.UUID(data.pop("uuid")).hex return data diff --git a/task_office/core/models/db_models.py b/task_office/core/models/db_models.py index 648d7f7..bfffc65 100644 --- a/task_office/core/models/db_models.py +++ b/task_office/core/models/db_models.py @@ -106,7 +106,9 @@ class State(XEnum): position = Column(db.Integer(), default=0) column_uuid = reference_col("columns", pk_name="uuid", nullable=False) - column = relationship("BoardColumn", backref=db.backref("tasks")) + column = relationship( + "BoardColumn", backref=db.backref(name="tasks", order_by="Task.position.asc()") + ) creator_uuid = reference_col("users", pk_name="uuid", nullable=False) creator = relationship("User", backref=db.backref("tasks")) diff --git a/task_office/tasks/schemas/basic_schemas.py b/task_office/tasks/schemas/basic_schemas.py index 9655a54..878eea9 100644 --- a/task_office/tasks/schemas/basic_schemas.py +++ b/task_office/tasks/schemas/basic_schemas.py @@ -6,7 +6,7 @@ from task_office.core.enums import XEnum, OrderingDirection from task_office.core.models.db_models import BoardColumn, Task -from task_office.core.schemas.base_schemas import BaseSchema, ListSchema, XSchema +from task_office.core.schemas.base_schemas import BaseSchema, ListSchema, SearchSchema from task_office.core.schemas.nested_schemas import NestedUserDumpSchema from task_office.core.validators import PKExists from task_office.settings import CONFIG @@ -131,3 +131,45 @@ def preload_data(self, data, **kwargs): task_list_query_schema = TaskListQuerySchema() API_SPEC.components.schema("TaskListQuerySchema", schema=TaskListQuerySchema) + + +class ColumnWithTasksDumpSchema(BaseSchema): + name = fields.Str(dump_only=True) + position = fields.Integer(dump_only=True) + tasks = fields.Nested(TaskDumpSchema, many=True) + + @post_dump + def dump_data(self, data, **kwargs): + data["uuid"] = uuid.UUID(data.pop("uuid")).hex + return data + + class Meta: + strict = True + + +columns_listed_dump_schema = ColumnWithTasksDumpSchema(many=True) +API_SPEC.components.schema( + "ColumnWithTasksDumpSchema", schema=ColumnWithTasksDumpSchema +) + + +class TaskListByColumnsQuerySchema(ListSchema): + class OrderingMap(XEnum): + POSITION_ASC = ("-position", BoardColumn.position.asc(), OrderingDirection.ASC) + POSITION_DESC = ( + "position", + BoardColumn.position.desc(), + OrderingDirection.DESC, + ) + + searching = fields.Nested(SearchSchema, required=False) + ordering = EnumField(OrderingMap, required=False, by_value=True) + + class Meta: + strict = True + + +task_list_by_columns_query_schema = TaskListByColumnsQuerySchema() +API_SPEC.components.schema( + "TaskListByColumnsQuerySchema", schema=TaskListByColumnsQuerySchema +) diff --git a/task_office/tasks/views.py b/task_office/tasks/views.py index 8dbd923..7ca872f 100644 --- a/task_office/tasks/views.py +++ b/task_office/tasks/views.py @@ -7,6 +7,7 @@ from flask_babel import lazy_gettext as _ from flask_jwt_extended import jwt_required, get_current_user from sqlalchemy import func +from sqlalchemy.orm import aliased from .constants import TASKS_PREFIX from .schemas.basic_schemas import ( @@ -15,8 +16,11 @@ task_post_schema, task_dump_schema, task_put_schema, + task_list_by_columns_query_schema, + columns_listed_dump_schema, ) from .utils import reset_tasks_ordering +from ..auth import User from ..core.helpers.listed_response import listed_response from ..core.models.db_models import BoardColumn, Board, Task from ..core.utils import validate_request_url_uuid, non_empty_query_required @@ -173,3 +177,25 @@ def get_list_tasks(board_uuid, **kwargs): query=tasks, query_params=data, schema=tasks_listed_dump_schema ) return data + + +@blueprint.route("/by-columns", methods=("get",)) +@jwt_required +@use_kwargs(task_list_by_columns_query_schema) +def get_list_tasks_by_columns(board_uuid, **kwargs): + """ + :param board_uuid: + :param kwargs: + :return: + """ + data = kwargs + + # Check board_uuid in request_url + validate_request_url_uuid(Board, "uuid", board_uuid, True) + + columns_with_tasks = BoardColumn.query.filter(BoardColumn.board_uuid == board_uuid) + # Serialize to paginated response + data = listed_response.serialize( + query=columns_with_tasks, query_params=data, schema=columns_listed_dump_schema + ) + return data From 9bf961203d210a546c72d35548f504682a6e272b Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Sat, 22 Feb 2020 15:45:34 +0200 Subject: [PATCH 24/60] improved meta data for tasks --- TODO.rst | 1 + task_office/tasks/schemas/basic_schemas.py | 29 +++++++++++----------- task_office/tasks/views.py | 16 +++++++++--- 3 files changed, 29 insertions(+), 17 deletions(-) diff --git a/TODO.rst b/TODO.rst index 989a16d..8143072 100644 --- a/TODO.rst +++ b/TODO.rst @@ -17,3 +17,4 @@ Tasks ----- * !!Done!! Check is unique task name for current Board in post, put * If task will changing column reset tasks ordering in new and ald columns +* In ../tasks/by-columns implements search by tasks fields diff --git a/task_office/tasks/schemas/basic_schemas.py b/task_office/tasks/schemas/basic_schemas.py index 878eea9..7a4a753 100644 --- a/task_office/tasks/schemas/basic_schemas.py +++ b/task_office/tasks/schemas/basic_schemas.py @@ -1,10 +1,11 @@ import uuid +from flask_babel import lazy_gettext as _ from marshmallow import fields, post_dump, validates_schema, pre_load from marshmallow.validate import Length, Range from marshmallow_enum import EnumField -from task_office.core.enums import XEnum, OrderingDirection +from task_office.core.enums import XEnum from task_office.core.models.db_models import BoardColumn, Task from task_office.core.schemas.base_schemas import BaseSchema, ListSchema, SearchSchema from task_office.core.schemas.nested_schemas import NestedUserDumpSchema @@ -106,13 +107,15 @@ class Meta: class TaskListQuerySchema(ListSchema): + SEARCHING_SCHEMA = SearchTaskSchema + class OrderingMap(XEnum): - CREATED_AT_ASC = ("-created_at", Task.created_at.asc(), OrderingDirection.ASC) - CREATED_AT_DESC = ("created_at", Task.created_at.desc(), OrderingDirection.DESC) - POSITION_ASC = ("-position", Task.position.asc(), OrderingDirection.ASC) - POSITION_DESC = ("position", Task.position.desc(), OrderingDirection.DESC) + CREATED_AT_ASC = ("-created_at", Task.created_at.asc(), _("ascending")) + CREATED_AT_DESC = ("created_at", Task.created_at.desc(), _("descending")) + POSITION_ASC = ("-position", Task.position.asc(), _("ascending")) + POSITION_DESC = ("position", Task.position.desc(), _("descending")) - searching = fields.Nested(SearchTaskSchema, required=False) + searching = fields.Nested(SEARCHING_SCHEMA, required=False) ordering = EnumField( OrderingMap, required=False, @@ -154,15 +157,13 @@ class Meta: class TaskListByColumnsQuerySchema(ListSchema): + SEARCHING_SCHEMA = SearchSchema + class OrderingMap(XEnum): - POSITION_ASC = ("-position", BoardColumn.position.asc(), OrderingDirection.ASC) - POSITION_DESC = ( - "position", - BoardColumn.position.desc(), - OrderingDirection.DESC, - ) - - searching = fields.Nested(SearchSchema, required=False) + POSITION_ASC = ("-position", BoardColumn.position.asc(), _("ascending")) + POSITION_DESC = ("position", BoardColumn.position.desc(), _("descending")) + + searching = fields.Nested(SEARCHING_SCHEMA, required=False) ordering = EnumField(OrderingMap, required=False, by_value=True) class Meta: diff --git a/task_office/tasks/views.py b/task_office/tasks/views.py index 7ca872f..f1e19c4 100644 --- a/task_office/tasks/views.py +++ b/task_office/tasks/views.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """Tasks views.""" from datetime import datetime @@ -7,7 +6,6 @@ from flask_babel import lazy_gettext as _ from flask_jwt_extended import jwt_required, get_current_user from sqlalchemy import func -from sqlalchemy.orm import aliased from .constants import TASKS_PREFIX from .schemas.basic_schemas import ( @@ -20,7 +18,6 @@ columns_listed_dump_schema, ) from .utils import reset_tasks_ordering -from ..auth import User from ..core.helpers.listed_response import listed_response from ..core.models.db_models import BoardColumn, Board, Task from ..core.utils import validate_request_url_uuid, non_empty_query_required @@ -38,6 +35,19 @@ def get_meta_data(board_uuid): """ validate_request_url_uuid(Board, "uuid", board_uuid, True) data = dict() + data["task_state_choices"] = Task.State.dict_choices() + data["task_list"] = { + "ordering_choices": task_list_query_schema.OrderingMap.dict_choices(), + "searching_choices": list( + task_list_query_schema.SEARCHING_SCHEMA.FIELDS_MAP.keys() + ), + } + data["task_list_by_columns"] = { + "ordering_choices": task_list_by_columns_query_schema.OrderingMap.dict_choices(), + "searching_choices": list( + task_list_by_columns_query_schema.SEARCHING_SCHEMA.FIELDS_MAP.keys() + ), + } return data From 60128adce92c26c2b2e2c96be78e75b640168f23 Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Sun, 23 Feb 2020 12:10:54 +0200 Subject: [PATCH 25/60] implemented tasks by columns, light improvements --- task_office/auth/models.py | 37 -------------------- task_office/auth/schemas.py | 7 ++-- task_office/auth/views.py | 3 +- task_office/boards/schemas/basic_schemas.py | 21 ++++++++++- task_office/boards/schemas/search_schemas.py | 19 ++++++++++ task_office/boards/views.py | 36 ++++++++++++++----- task_office/core/models/db_models.py | 34 ++++++++++++++++++ task_office/core/schemas/nested_schemas.py | 2 +- task_office/core/utils.py | 14 ++++---- 9 files changed, 112 insertions(+), 61 deletions(-) delete mode 100644 task_office/auth/models.py create mode 100644 task_office/boards/schemas/search_schemas.py diff --git a/task_office/auth/models.py b/task_office/auth/models.py deleted file mode 100644 index 5f06e09..0000000 --- a/task_office/auth/models.py +++ /dev/null @@ -1,37 +0,0 @@ -"""User models.""" -from task_office.core.models.mixins import DTMixin, PKMixin -from task_office.database import Column, Model, db -from task_office.extensions import bcrypt - - -class User(PKMixin, DTMixin, Model): - - __tablename__ = "users" - - username = Column(db.String(80), unique=True, nullable=False) - email = Column(db.String(255), unique=True, nullable=False, index=True) - bio = Column(db.String(300), nullable=True) - phone = Column(db.String(300), nullable=True) - password = Column(db.Binary(128), nullable=True) - is_active = Column(db.Boolean(), default=True) - is_superuser = Column(db.Boolean(), default=False) - - def __init__(self, username, email, password=None, **kwargs): - """Create instance.""" - db.Model.__init__(self, username=username, email=email, **kwargs) - if password: - self.set_password(password) - else: - self.password = None - - def set_password(self, password): - """Set password.""" - self.password = bcrypt.generate_password_hash(password) - - def check_password(self, value): - """Check password.""" - return bcrypt.check_password_hash(self.password, value) - - def __repr__(self): - """Represent instance as a unique string.""" - return "".format(username=self.username) diff --git a/task_office/auth/schemas.py b/task_office/auth/schemas.py index 0e3c541..1292a50 100644 --- a/task_office/auth/schemas.py +++ b/task_office/auth/schemas.py @@ -1,11 +1,8 @@ -# coding: utf-8 -import uuid - from flask_babel import lazy_gettext as _ -from marshmallow import fields, post_dump, validates_schema +from marshmallow import fields, validates_schema from marshmallow.validate import Length -from task_office.auth.models import User +from task_office.core.models.db_models import User from task_office.core.schemas.base_schemas import BaseSchema, XSchema from task_office.core.validators import Unique from task_office.swagger import API_SPEC diff --git a/task_office/auth/views.py b/task_office/auth/views.py index 366b9e8..6cc74ab 100644 --- a/task_office/auth/views.py +++ b/task_office/auth/views.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """User views.""" from datetime import datetime @@ -11,7 +10,6 @@ get_jwt_identity, ) -from .models import User from .schemas import ( user_schema, user_signup_schema, @@ -19,6 +17,7 @@ signed_schema, refreshed_access_tokens_schema, ) +from ..core.models.db_models import User from ..settings import CONFIG blueprint = Blueprint("auth", __name__, url_prefix=CONFIG.API_V1_PREFIX + "/auth") diff --git a/task_office/boards/schemas/basic_schemas.py b/task_office/boards/schemas/basic_schemas.py index 95b0f13..49167f7 100644 --- a/task_office/boards/schemas/basic_schemas.py +++ b/task_office/boards/schemas/basic_schemas.py @@ -1,4 +1,3 @@ -# coding: utf-8 import uuid from marshmallow import fields, validates_schema @@ -6,6 +5,7 @@ from marshmallow_enum import EnumField from task_office.auth import User +from task_office.boards.schemas.search_schemas import SearchUserSchema from task_office.core.enums import XEnum from task_office.core.schemas.base_schemas import BaseSchema, ListSchema, XSchema from task_office.core.schemas.nested_schemas import NestedUserDumpSchema @@ -61,3 +61,22 @@ class Meta: board_list_query_schema = BoardListQuerySchema() API_SPEC.components.schema("BoardListQuerySchema", schema=BoardListQuerySchema) + + +class UserListByBoardQuerySchema(ListSchema): + SEARCHING_SCHEMA = SearchUserSchema + + class OrderingMap(XEnum): + pass + + searching = fields.Nested(SEARCHING_SCHEMA, required=False) + ordering = EnumField(OrderingMap, required=False, by_value=True) + + class Meta: + strict = True + + +user_list_by_board_query_schema = UserListByBoardQuerySchema() +API_SPEC.components.schema( + "UserListByBoardQuerySchema", schema=UserListByBoardQuerySchema +) diff --git a/task_office/boards/schemas/search_schemas.py b/task_office/boards/schemas/search_schemas.py new file mode 100644 index 0000000..cf3ca49 --- /dev/null +++ b/task_office/boards/schemas/search_schemas.py @@ -0,0 +1,19 @@ +from marshmallow import fields + +from task_office.core.models.db_models import User +from task_office.core.schemas.base_schemas import SearchSchema +from task_office.swagger import API_SPEC + + +class SearchUserSchema(SearchSchema): + FIELDS_MAP = {"username": User.username, "email": User.email} + + username = fields.Str(required=False, allow_none=False) + email = fields.Email(required=False, allow_none=False) + + class Meta: + strict = True + + +search_user_schema = SearchUserSchema() +API_SPEC.components.schema("SearchUserSchema", schema=SearchUserSchema) diff --git a/task_office/boards/views.py b/task_office/boards/views.py index 21dd48a..e94b23f 100644 --- a/task_office/boards/views.py +++ b/task_office/boards/views.py @@ -1,8 +1,8 @@ """Boards views.""" -from flask_babel import lazy_gettext as _ from flask import Blueprint from flask_apispec import use_kwargs, marshal_with from flask_jwt_extended import jwt_required, get_current_user +from sqlalchemy.orm import aliased from .constants import BOARDS_PREFIX from .schemas.basic_schemas import ( @@ -10,11 +10,12 @@ board_list_query_schema, board_list_dump_schema, board_dump_schema, + user_list_by_board_query_schema, ) from ..core.helpers.listed_response import listed_response -from ..core.models.db_models import Board, Permission -from ..core.utils import is_uuid, empty_query_required, non_empty_query_required -from ..exceptions import InvalidUsage +from ..core.models.db_models import Board, Permission, User +from ..core.schemas.nested_schemas import nested_user_list_dump_schema +from ..core.utils import empty_query_required, validate_request_url_uuid blueprint = Blueprint("boards", __name__, url_prefix=BOARDS_PREFIX) @@ -58,11 +59,28 @@ def get_list_boards(**kwargs): @blueprint.route("/", methods=("get",)) @jwt_required @marshal_with(board_dump_schema) -def get_board_by_uuid(board_uuid): - # board_uuid in request url - if not is_uuid(board_uuid): - raise InvalidUsage(messages=[_("Not found")], status_code=404) +def get_board(board_uuid): - board = non_empty_query_required(Board, uuid=str(board_uuid))[1] + board = validate_request_url_uuid(Board, "uuid", board_uuid, True)[1] return board + + +@blueprint.route("//users", methods=("get",)) +@jwt_required +@use_kwargs(user_list_by_board_query_schema) +def get_board_users(board_uuid, **kwargs): + data = kwargs + + # board_uuid in request url + validate_request_url_uuid(Board, "uuid", board_uuid, True) + + perms_q = aliased( + Permission.query.filter(Permission.board_uuid == board_uuid).subquery(), + name="perms", + ) + users_q = User.query.join(perms_q).filter() + data = listed_response.serialize( + query=users_q, query_params=data, schema=nested_user_list_dump_schema + ) + return data diff --git a/task_office/core/models/db_models.py b/task_office/core/models/db_models.py index bfffc65..7e1aa62 100644 --- a/task_office/core/models/db_models.py +++ b/task_office/core/models/db_models.py @@ -5,6 +5,40 @@ from task_office.core.enums import XEnum from task_office.core.models.mixins import DTMixin, PKMixin from task_office.database import Column, Model, db, reference_col, relationship +from task_office.extensions import bcrypt + + +class User(PKMixin, DTMixin, Model): + + __tablename__ = "users" + + username = Column(db.String(80), unique=True, nullable=False) + email = Column(db.String(255), unique=True, nullable=False, index=True) + bio = Column(db.String(300), nullable=True) + phone = Column(db.String(300), nullable=True) + password = Column(db.Binary(128), nullable=True) + is_active = Column(db.Boolean(), default=True) + is_superuser = Column(db.Boolean(), default=False) + + def __init__(self, username, email, password=None, **kwargs): + """Create instance.""" + db.Model.__init__(self, username=username, email=email, **kwargs) + if password: + self.set_password(password) + else: + self.password = None + + def set_password(self, password): + """Set password.""" + self.password = bcrypt.generate_password_hash(password) + + def check_password(self, value): + """Check password.""" + return bcrypt.check_password_hash(self.password, value) + + def __repr__(self): + """Represent instance as a unique string.""" + return "".format(username=self.username) class Board(PKMixin, DTMixin, Model): diff --git a/task_office/core/schemas/nested_schemas.py b/task_office/core/schemas/nested_schemas.py index 09c8b26..d1337bc 100644 --- a/task_office/core/schemas/nested_schemas.py +++ b/task_office/core/schemas/nested_schemas.py @@ -1,4 +1,3 @@ -# coding: utf-8 import uuid from marshmallow import fields, post_dump @@ -22,6 +21,7 @@ class Meta: nested_user_dump_schema = NestedUserDumpSchema() +nested_user_list_dump_schema = NestedUserDumpSchema(many=True) class NestedColumnDumpSchema(XSchema): diff --git a/task_office/core/utils.py b/task_office/core/utils.py index 93e92fb..07a81c4 100644 --- a/task_office/core/utils.py +++ b/task_office/core/utils.py @@ -1,4 +1,4 @@ -from typing import Any +from typing import Any, Union, Tuple from uuid import UUID from flask import request @@ -38,7 +38,7 @@ def lookup_filter( return LOOKUP_MAP.get(lookup, LOOKUP_MAP.get("e"))(query, key, value) -def is_uuid(uuid): +def is_uuid(uuid) -> bool: try: UUID(uuid).version return True @@ -48,13 +48,15 @@ def is_uuid(uuid): def validate_request_url_uuid( model: Model, key: str, uuid: str, must_exists: bool = False -): +) -> Union[Tuple, None]: if not is_uuid(uuid): raise InvalidUsage(messages=[_("Not found")], status_code=404) - request_url_splitted = request.url.split("/") - if uuid not in request_url_splitted: + request_url_separated = request.url.split("/") + if uuid not in request_url_separated: raise InvalidUsage(messages=[_("Not found")], status_code=404) if must_exists: - non_empty_query_required(model, **{key: uuid}) + return non_empty_query_required(model, **{key: uuid}) + + return None From f05546d3a8b48d8d6c5764ee1e2ec3dce257316b Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Sun, 23 Feb 2020 22:41:03 +0200 Subject: [PATCH 26/60] tasks with performers --- TODO.rst | 1 + .../permissions/schemas/basic_schemas.py | 1 - task_office/tasks/schemas/basic_schemas.py | 22 +++++++++++++++++-- task_office/tasks/views.py | 15 ++++++++++++- 4 files changed, 35 insertions(+), 4 deletions(-) diff --git a/TODO.rst b/TODO.rst index 8143072..b883a8f 100644 --- a/TODO.rst +++ b/TODO.rst @@ -18,3 +18,4 @@ Tasks * !!Done!! Check is unique task name for current Board in post, put * If task will changing column reset tasks ordering in new and ald columns * In ../tasks/by-columns implements search by tasks fields +* In tasks[post, put] validate is performers applied to board diff --git a/task_office/permissions/schemas/basic_schemas.py b/task_office/permissions/schemas/basic_schemas.py index 248a69b..fa0e89c 100644 --- a/task_office/permissions/schemas/basic_schemas.py +++ b/task_office/permissions/schemas/basic_schemas.py @@ -1,4 +1,3 @@ -# coding: utf-8 import uuid from marshmallow import fields, validates_schema, post_dump diff --git a/task_office/tasks/schemas/basic_schemas.py b/task_office/tasks/schemas/basic_schemas.py index 7a4a753..c535636 100644 --- a/task_office/tasks/schemas/basic_schemas.py +++ b/task_office/tasks/schemas/basic_schemas.py @@ -6,7 +6,7 @@ from marshmallow_enum import EnumField from task_office.core.enums import XEnum -from task_office.core.models.db_models import BoardColumn, Task +from task_office.core.models.db_models import BoardColumn, Task, User from task_office.core.schemas.base_schemas import BaseSchema, ListSchema, SearchSchema from task_office.core.schemas.nested_schemas import NestedUserDumpSchema from task_office.core.validators import PKExists @@ -39,6 +39,13 @@ class TaskPostSchema(BaseSchema): column_uuid = fields.UUID( required=True, validate=[PKExists(BoardColumn, "uuid")], allow_none=False ) + performers = fields.List( + fields.UUID( + required=False, validate=[PKExists(User, "uuid")], allow_none=False + ), + required=False, + allow_none=False, + ) class Meta: strict = True @@ -47,6 +54,8 @@ class Meta: def validate_schema(self, data, **kwargs): data["column_uuid"] = str(data.pop("column_uuid")) data["state"] = data.pop("state", Task.State.NEW).value + if data.get("performers", None): + data["performers"] = [str(item) for item in data["performers"]] class TaskPutSchema(BaseSchema): @@ -63,6 +72,13 @@ class TaskPutSchema(BaseSchema): column_uuid = fields.UUID( required=True, validate=[PKExists(BoardColumn, "uuid")], allow_none=False ) + performers = fields.List( + fields.UUID( + required=False, validate=[PKExists(User, "uuid")], allow_none=False + ), + required=False, + allow_none=False, + ) class Meta: strict = True @@ -73,6 +89,8 @@ def validate_schema(self, data, **kwargs): state = data.get("state", None) if state is not None: data["state"] = state.value + if data.get("performers", None): + data["performers"] = [str(item) for item in data["performers"]] class TaskDumpSchema(BaseSchema): @@ -85,7 +103,7 @@ class TaskDumpSchema(BaseSchema): state = fields.Integer(dump_only=True) position = fields.Integer(dump_only=True) column_uuid = fields.UUID(dump_only=True) - performers = NestedUserDumpSchema(many=True) + performers = fields.Nested(NestedUserDumpSchema, many=True) @post_dump def dump_data(self, data, **kwargs): diff --git a/task_office/tasks/views.py b/task_office/tasks/views.py index f1e19c4..96f94c1 100644 --- a/task_office/tasks/views.py +++ b/task_office/tasks/views.py @@ -19,7 +19,7 @@ ) from .utils import reset_tasks_ordering from ..core.helpers.listed_response import listed_response -from ..core.models.db_models import BoardColumn, Board, Task +from ..core.models.db_models import BoardColumn, Board, Task, User from ..core.utils import validate_request_url_uuid, non_empty_query_required from ..exceptions import InvalidUsage from ..extensions import db @@ -97,6 +97,11 @@ def create_task(board_uuid, **kwargs): user_uuid = str(user.uuid) if user else None data["creator_uuid"] = user_uuid + # getting performers before task save + if data.get("performers", None): + performers = User.query.filter(User.uuid.in_(data["performers"])).all() + data["performers"] = performers + # save task task = Task(**data) task.save() @@ -157,9 +162,17 @@ def update_task(board_uuid, task_uuid, **kwargs): ) if data: + + # getting performers before task save + if data.get("performers", None): + performers = User.query.filter(User.uuid.in_(data["performers"])).all() + data["performers"] = performers + old_position = task.position + task.update(updated_at=datetime.utcnow(), **data) task.save() + if position is not None and position > 1: reset_tasks_ordering(task, data["column_uuid"], position, old_position) From 4f02c2db794b55c20fddfffca5815d93b6030c4b Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Sat, 14 Mar 2020 14:08:01 +0200 Subject: [PATCH 27/60] configured flask-cache via redis --- requirements/base.txt | 3 ++- task_office/app.py | 2 +- task_office/auth/utils.py | 25 +++++++++++++++++++++++++ task_office/settings.py | 20 ++++++++++++++++++-- 4 files changed, 46 insertions(+), 4 deletions(-) create mode 100644 task_office/auth/utils.py diff --git a/requirements/base.txt b/requirements/base.txt index 626aeca..f317a77 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -15,4 +15,5 @@ uuid environs unicode_slugify Click==7.0 -Flask-Babel>=0.12.2 \ No newline at end of file +Flask-Babel>=0.12.2 +redis==2.10.6 \ No newline at end of file diff --git a/task_office/app.py b/task_office/app.py index 6cb9227..d513656 100644 --- a/task_office/app.py +++ b/task_office/app.py @@ -33,7 +33,7 @@ def create_app(config_object): def register_extensions(app): """Register Flask extensions.""" bcrypt.init_app(app) - cache.init_app(app) + cache.init_app(app, config=CONFIG.CACHE) db.init_app(app) jwt.init_app(app) migrate.init_app(app, db) diff --git a/task_office/auth/utils.py b/task_office/auth/utils.py new file mode 100644 index 0000000..4ae9f9b --- /dev/null +++ b/task_office/auth/utils.py @@ -0,0 +1,25 @@ +import uuid + +from flask_jwt_extended import get_current_user + +from task_office.extensions import cache +from task_office.settings import CONFIG + + +def _get_cached_permissions(): + user = get_current_user() + perms = dict() + if user: + pk = uuid.UUID(user.uuid).hex + key = f"perms_{pk}" + perms = cache.get(key, perms, timeout=CONFIG.JWT_ACCESS_TOKEN_EXPIRES.seconds) + if not perms: + perms = {uuid.UUID(item.board_uuid).hex: item.role for item in user.perms} + if perms: + cache.set(key, perms, timeout=CONFIG.JWT_ACCESS_TOKEN_EXPIRES.seconds) + return perms + + +def delete_cached_permissions(user_uuid_hexed): + key = f"perms_{user_uuid_hexed}" + return cache.delete(key) diff --git a/task_office/settings.py b/task_office/settings.py index 23dc3c5..2f28459 100644 --- a/task_office/settings.py +++ b/task_office/settings.py @@ -40,8 +40,6 @@ class Config(object): STATIC_DIR = os.path.abspath(os.path.join(PROJECT_ROOT, "static")) STATIC_URL = API_V1_PREFIX + "/static" - SQLALCHEMY_TRACK_MODIFICATIONS = False - CORS_ORIGIN_WHITELIST = env.list("CORS_ORIGIN_WHITELIST", []) # JWT @@ -58,6 +56,7 @@ class Config(object): MAX_LIMIT_VALUE = 50 # DB + SQLALCHEMY_TRACK_MODIFICATIONS = False DATABASE = { "DB_NAME": env.str("POSTGRES_DB", "task_office"), "DB_USER": env.str("POSTGRES_USER", "task_office_user"), @@ -73,6 +72,23 @@ class Config(object): db_name=DATABASE["DB_NAME"], ) + CACHE = { + "CACHE_TYPE": env.str("CACHE_TYPE", "redis"), + "CACHE_REDIS_HOST": env.str("CACHE_REDIS_HOST"), + "CACHE_REDIS_PORT": env.int("CACHE_REDIS_PORT"), + "CACHE_REDIS_PASSWORD": env.str("CACHE_REDIS_PASSWORD"), + "CACHE_REDIS_DB": env.int("CACHE_REDIS_DB"), + "CACHE_DEFAULT_TIMEOUT": env.int( + "CACHE_DEFAULT_TIMEOUT", JWT_ACCESS_TOKEN_EXPIRES.seconds + ), + "CACHE_REDIS_URL": "redis://:{password}@{host}:{port}/{db}".format( + password=env.str("CACHE_REDIS_PASSWORD"), + host=env.str("CACHE_REDIS_HOST"), + port=env.str("CACHE_REDIS_PORT"), + db=env.str("CACHE_REDIS_DB"), + ), + } + class ProdConfig(Config): """Production configuration.""" From 8072097054ec8514550f73d62f8f3d1747839eba Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Sat, 21 Mar 2020 10:52:45 +0200 Subject: [PATCH 28/60] implemented permissions via decorator --- task_office/auth/utils.py | 21 +++++++++++++++++-- task_office/boards/views.py | 3 +++ task_office/core/models/db_models.py | 6 +++--- task_office/permissions/views.py | 30 ++++++++++++++++++---------- task_office/settings.py | 1 + task_office/tasks/views.py | 8 +++++++- 6 files changed, 52 insertions(+), 17 deletions(-) diff --git a/task_office/auth/utils.py b/task_office/auth/utils.py index 4ae9f9b..5f706d6 100644 --- a/task_office/auth/utils.py +++ b/task_office/auth/utils.py @@ -1,7 +1,9 @@ import uuid +from flask_babel import lazy_gettext as _ from flask_jwt_extended import get_current_user +from task_office.exceptions import InvalidUsage from task_office.extensions import cache from task_office.settings import CONFIG @@ -12,7 +14,7 @@ def _get_cached_permissions(): if user: pk = uuid.UUID(user.uuid).hex key = f"perms_{pk}" - perms = cache.get(key, perms, timeout=CONFIG.JWT_ACCESS_TOKEN_EXPIRES.seconds) + perms = cache.get(key) if not perms: perms = {uuid.UUID(item.board_uuid).hex: item.role for item in user.perms} if perms: @@ -20,6 +22,21 @@ def _get_cached_permissions(): return perms -def delete_cached_permissions(user_uuid_hexed): +def reset_cached_permissions(user_uuid_hexed): key = f"perms_{user_uuid_hexed}" return cache.delete(key) + + +def permission(required_role: int): + def decorator(func): + def wrapper(*args, **kwargs): + board_uuid = kwargs.get("board_uuid", None) + perms = _get_cached_permissions() + current_role = perms.get(board_uuid, 9999999) + if current_role >= required_role: + raise InvalidUsage(messages=[_("Not allowed")], status_code=403) + return func(*args, **kwargs) + + return wrapper + + return decorator diff --git a/task_office/boards/views.py b/task_office/boards/views.py index e94b23f..f762c69 100644 --- a/task_office/boards/views.py +++ b/task_office/boards/views.py @@ -12,6 +12,7 @@ board_dump_schema, user_list_by_board_query_schema, ) +from ..auth.utils import permission from ..core.helpers.listed_response import listed_response from ..core.models.db_models import Board, Permission, User from ..core.schemas.nested_schemas import nested_user_list_dump_schema @@ -59,6 +60,7 @@ def get_list_boards(**kwargs): @blueprint.route("/", methods=("get",)) @jwt_required @marshal_with(board_dump_schema) +@permission(required_role=Permission.Role.STAFF.value) def get_board(board_uuid): board = validate_request_url_uuid(Board, "uuid", board_uuid, True)[1] @@ -69,6 +71,7 @@ def get_board(board_uuid): @blueprint.route("//users", methods=("get",)) @jwt_required @use_kwargs(user_list_by_board_query_schema) +@permission(required_role=Permission.Role.STAFF.value) def get_board_users(board_uuid, **kwargs): data = kwargs diff --git a/task_office/core/models/db_models.py b/task_office/core/models/db_models.py index 7e1aa62..3787363 100644 --- a/task_office/core/models/db_models.py +++ b/task_office/core/models/db_models.py @@ -73,9 +73,9 @@ class Permission(PKMixin, DTMixin, Model): ) class Role(XEnum): - OWNER = 9, _("Owner"), _("Owner of board(creator)") - EDITOR = 8, _("Editor"), _("Editor of board") - STAFF = 7, _("Staff"), _("Ordinary user") + OWNER = 1, _("Owner"), _("Owner of board(creator)") + EDITOR = 2, _("Editor"), _("Editor of board") + STAFF = 3, _("Staff"), _("Ordinary user") role = Column(db.Integer(), default=Role.STAFF.value) user_uuid = reference_col("users", pk_name="uuid", nullable=False) diff --git a/task_office/permissions/views.py b/task_office/permissions/views.py index 9231b21..c55c880 100644 --- a/task_office/permissions/views.py +++ b/task_office/permissions/views.py @@ -1,5 +1,5 @@ -# -*- coding: utf-8 -*- """Permissions views.""" +import uuid from datetime import datetime from flask import Blueprint @@ -14,6 +14,7 @@ permissions_list_query_schema, permission_list_dump_schema, ) +from ..auth.utils import permission, reset_cached_permissions from ..core.helpers.listed_response import listed_response from ..core.models.db_models import Permission, Board from ..core.utils import is_uuid, non_empty_query_required, empty_query_required @@ -26,6 +27,7 @@ @blueprint.route("/meta", methods=("get",)) @jwt_required +@permission(required_role=Permission.Role.EDITOR.value) def get_meta_data(board_uuid): """ Additional data for Permissions @@ -43,6 +45,7 @@ def get_meta_data(board_uuid): @jwt_required @use_kwargs(permission_query_schema) @marshal_with(permission_dump_schema) +@permission(required_role=Permission.Role.EDITOR.value) def create_permission(board_uuid, **kwargs): """ :param board_uuid: @@ -65,8 +68,9 @@ def create_permission(board_uuid, **kwargs): ).first(): raise InvalidUsage(messages=[_("Not allowed")], status_code=422) - permission = Permission(board_uuid=board_uuid, **data) - permission.save() + perm = Permission(board_uuid=board_uuid, **data) + perm.save() + reset_cached_permissions(uuid.UUID(perm.user_uuid).hex) return permission @@ -74,6 +78,7 @@ def create_permission(board_uuid, **kwargs): @jwt_required @use_kwargs(permission_query_schema) @marshal_with(permission_dump_schema) +@permission(required_role=Permission.Role.EDITOR.value) def update_permission(board_uuid, permission_uuid, **kwargs): """ :param board_uuid: @@ -93,18 +98,20 @@ def update_permission(board_uuid, permission_uuid, **kwargs): ).first(): raise InvalidUsage(messages=[_("Not allowed")], status_code=422) - permission = non_empty_query_required( + perm = non_empty_query_required( Permission, uuid=str(permission_uuid), board_uuid=str(board_uuid) )[1] - permission.update(updated_at=datetime.utcnow(), **data) - permission.save() + perm.update(updated_at=datetime.utcnow(), **data) + perm.save() + reset_cached_permissions(uuid.UUID(perm.user_uuid).hex) return permission @blueprint.route("", methods=("get",)) @jwt_required @use_kwargs(permissions_list_query_schema) +@permission(required_role=Permission.Role.STAFF.value) def get_list_permission(board_uuid, **kwargs): """ :param board_uuid: @@ -116,11 +123,11 @@ def get_list_permission(board_uuid, **kwargs): if not is_uuid(board_uuid): raise InvalidUsage(messages=[_("Not found")], status_code=404) - permissions = non_empty_query_required(Permission, board_uuid=str(board_uuid))[0] + perms = non_empty_query_required(Permission, board_uuid=str(board_uuid))[0] # Serialize to paginated response data = listed_response.serialize( - query=permissions, query_params=data, schema=permission_list_dump_schema + query=perms, query_params=data, schema=permission_list_dump_schema ) return data @@ -128,6 +135,7 @@ def get_list_permission(board_uuid, **kwargs): @blueprint.route("/", methods=("get",)) @jwt_required @marshal_with(permission_dump_schema) +@permission(required_role=Permission.Role.STAFF.value) def get_permission_by_uuid(board_uuid, permission_uuid): """ :param board_uuid: @@ -138,9 +146,9 @@ def get_permission_by_uuid(board_uuid, permission_uuid): if not is_uuid(board_uuid) or not is_uuid(permission_uuid): raise InvalidUsage(messages=[_("Not found")], status_code=404) - permission = non_empty_query_required( + perm = non_empty_query_required( Permission, uuid=str(permission_uuid), board_uuid=str(board_uuid) ) - permission = permission.first() + perm = perm[1] - return permission + return perm diff --git a/task_office/settings.py b/task_office/settings.py index 2f28459..aa46ecf 100644 --- a/task_office/settings.py +++ b/task_office/settings.py @@ -87,6 +87,7 @@ class Config(object): port=env.str("CACHE_REDIS_PORT"), db=env.str("CACHE_REDIS_DB"), ), + "OPTIONS": {"PASSWORD": env.str("CACHE_REDIS_PASSWORD")}, } diff --git a/task_office/tasks/views.py b/task_office/tasks/views.py index 96f94c1..2d01758 100644 --- a/task_office/tasks/views.py +++ b/task_office/tasks/views.py @@ -18,8 +18,9 @@ columns_listed_dump_schema, ) from .utils import reset_tasks_ordering +from ..auth.utils import permission from ..core.helpers.listed_response import listed_response -from ..core.models.db_models import BoardColumn, Board, Task, User +from ..core.models.db_models import BoardColumn, Board, Task, User, Permission from ..core.utils import validate_request_url_uuid, non_empty_query_required from ..exceptions import InvalidUsage from ..extensions import db @@ -29,6 +30,7 @@ @blueprint.route("/meta", methods=("get",)) @jwt_required +@permission(required_role=Permission.Role.STAFF.value) def get_meta_data(board_uuid): """ Additional data for tasks @@ -55,6 +57,7 @@ def get_meta_data(board_uuid): @jwt_required @use_kwargs(task_post_schema) @marshal_with(task_dump_schema) +@permission(required_role=Permission.Role.EDITOR.value) def create_task(board_uuid, **kwargs): """ :param board_uuid: @@ -116,6 +119,7 @@ def create_task(board_uuid, **kwargs): @jwt_required @use_kwargs(task_put_schema) @marshal_with(task_dump_schema) +@permission(required_role=Permission.Role.EDITOR.value) def update_task(board_uuid, task_uuid, **kwargs): """ :param board_uuid: @@ -182,6 +186,7 @@ def update_task(board_uuid, task_uuid, **kwargs): @blueprint.route("", methods=("get",)) @jwt_required @use_kwargs(task_list_query_schema) +@permission(required_role=Permission.Role.STAFF.value) def get_list_tasks(board_uuid, **kwargs): """ :param board_uuid: @@ -205,6 +210,7 @@ def get_list_tasks(board_uuid, **kwargs): @blueprint.route("/by-columns", methods=("get",)) @jwt_required @use_kwargs(task_list_by_columns_query_schema) +@permission(required_role=Permission.Role.STAFF.value) def get_list_tasks_by_columns(board_uuid, **kwargs): """ :param board_uuid: From fccec081b3d06ca5ad0bea3a14cf507285d27c3d Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Sat, 21 Mar 2020 18:54:14 +0200 Subject: [PATCH 29/60] refactoring v1 --- task_office/auth/schemas.py | 16 ++++++++-------- task_office/auth/utils.py | 1 + task_office/auth/views.py | 2 +- task_office/boards/constants.py | 2 +- task_office/boards/schemas/basic_schemas.py | 10 +++++----- task_office/boards/schemas/search_schemas.py | 4 ++-- task_office/columns/schemas/basic_schemas.py | 10 +++++----- task_office/core/schemas/nested_schemas.py | 8 +++++--- task_office/permissions/schemas/basic_schemas.py | 8 ++++---- task_office/settings.py | 14 +++++++++++++- task_office/swagger/specs.py | 16 ---------------- task_office/swagger/views.py | 10 +++++----- task_office/tasks/schemas/basic_schemas.py | 13 ++++++------- task_office/tasks/schemas/search_schemas.py | 4 ++-- 14 files changed, 58 insertions(+), 60 deletions(-) delete mode 100644 task_office/swagger/specs.py diff --git a/task_office/auth/schemas.py b/task_office/auth/schemas.py index 1292a50..5e4ae08 100644 --- a/task_office/auth/schemas.py +++ b/task_office/auth/schemas.py @@ -5,7 +5,7 @@ from task_office.core.models.db_models import User from task_office.core.schemas.base_schemas import BaseSchema, XSchema from task_office.core.validators import Unique -from task_office.swagger import API_SPEC +from task_office.settings import CONFIG class UserSchema(BaseSchema): @@ -104,12 +104,12 @@ class SignedSchema(XSchema): token_schema = TokenSchema() -API_SPEC.components.schema("UserSchema", schema=UserSchema) -API_SPEC.components.schema("UserSignInSchema", schema=UserSignInSchema) -API_SPEC.components.schema("UserSignUpSchema", schema=UserSignUpSchema) -API_SPEC.components.schema("TokenSchema", schema=TokenSchema) -API_SPEC.components.schema("SignedTokensSchema", schema=SignedTokensSchema) -API_SPEC.components.schema( +CONFIG.API_SPEC.components.schema("UserSchema", schema=UserSchema) +CONFIG.API_SPEC.components.schema("UserSignInSchema", schema=UserSignInSchema) +CONFIG.API_SPEC.components.schema("UserSignUpSchema", schema=UserSignUpSchema) +CONFIG.API_SPEC.components.schema("TokenSchema", schema=TokenSchema) +CONFIG.API_SPEC.components.schema("SignedTokensSchema", schema=SignedTokensSchema) +CONFIG.API_SPEC.components.schema( "RefreshedAccessTokenSchema", schema=RefreshedAccessTokenSchema ) -API_SPEC.components.schema("SignedSchema", schema=SignedSchema) +CONFIG.API_SPEC.components.schema("SignedSchema", schema=SignedSchema) diff --git a/task_office/auth/utils.py b/task_office/auth/utils.py index 5f706d6..8515c60 100644 --- a/task_office/auth/utils.py +++ b/task_office/auth/utils.py @@ -37,6 +37,7 @@ def wrapper(*args, **kwargs): raise InvalidUsage(messages=[_("Not allowed")], status_code=403) return func(*args, **kwargs) + wrapper.__name__ = func.__name__ return wrapper return decorator diff --git a/task_office/auth/views.py b/task_office/auth/views.py index 6cc74ab..f7b1150 100644 --- a/task_office/auth/views.py +++ b/task_office/auth/views.py @@ -20,7 +20,7 @@ from ..core.models.db_models import User from ..settings import CONFIG -blueprint = Blueprint("auth", __name__, url_prefix=CONFIG.API_V1_PREFIX + "/auth") +blueprint = Blueprint("auth", __name__, url_prefix=CONFIG.API_V1_PREFIX + "auth") @blueprint.route("/sign-up", methods=("post",)) diff --git a/task_office/boards/constants.py b/task_office/boards/constants.py index 00b1eca..de4db6a 100644 --- a/task_office/boards/constants.py +++ b/task_office/boards/constants.py @@ -1,4 +1,4 @@ from task_office.settings import CONFIG -BOARDS_PREFIX = CONFIG.API_V1_PREFIX + "/boards" +BOARDS_PREFIX = CONFIG.API_V1_PREFIX + "boards" BOARD_RETRIEVE_URL = BOARDS_PREFIX + "/" diff --git a/task_office/boards/schemas/basic_schemas.py b/task_office/boards/schemas/basic_schemas.py index 49167f7..1261f13 100644 --- a/task_office/boards/schemas/basic_schemas.py +++ b/task_office/boards/schemas/basic_schemas.py @@ -10,7 +10,7 @@ from task_office.core.schemas.base_schemas import BaseSchema, ListSchema, XSchema from task_office.core.schemas.nested_schemas import NestedUserDumpSchema from task_office.core.validators import PKExists -from task_office.swagger import API_SPEC +from task_office.settings import CONFIG class BoardPutSchema(BaseSchema): @@ -44,8 +44,8 @@ def validate_schema(self, data, **kwargs): board_put_schema = BoardPutSchema() board_dump_schema = BoardDumpSchema() board_list_dump_schema = BoardDumpSchema(many=True) -API_SPEC.components.schema("BoardPutSchema", schema=BoardPutSchema) -API_SPEC.components.schema("BoardDumpSchema", schema=BoardDumpSchema) +CONFIG.API_SPEC.components.schema("BoardPutSchema", schema=BoardPutSchema) +CONFIG.API_SPEC.components.schema("BoardDumpSchema", schema=BoardDumpSchema) class BoardListQuerySchema(ListSchema): @@ -60,7 +60,7 @@ class Meta: board_list_query_schema = BoardListQuerySchema() -API_SPEC.components.schema("BoardListQuerySchema", schema=BoardListQuerySchema) +CONFIG.API_SPEC.components.schema("BoardListQuerySchema", schema=BoardListQuerySchema) class UserListByBoardQuerySchema(ListSchema): @@ -77,6 +77,6 @@ class Meta: user_list_by_board_query_schema = UserListByBoardQuerySchema() -API_SPEC.components.schema( +CONFIG.API_SPEC.components.schema( "UserListByBoardQuerySchema", schema=UserListByBoardQuerySchema ) diff --git a/task_office/boards/schemas/search_schemas.py b/task_office/boards/schemas/search_schemas.py index cf3ca49..ce1a658 100644 --- a/task_office/boards/schemas/search_schemas.py +++ b/task_office/boards/schemas/search_schemas.py @@ -2,7 +2,7 @@ from task_office.core.models.db_models import User from task_office.core.schemas.base_schemas import SearchSchema -from task_office.swagger import API_SPEC +from task_office.settings import CONFIG class SearchUserSchema(SearchSchema): @@ -16,4 +16,4 @@ class Meta: search_user_schema = SearchUserSchema() -API_SPEC.components.schema("SearchUserSchema", schema=SearchUserSchema) +CONFIG.API_SPEC.components.schema("SearchUserSchema", schema=SearchUserSchema) diff --git a/task_office/columns/schemas/basic_schemas.py b/task_office/columns/schemas/basic_schemas.py index 57f8bb8..ad05899 100644 --- a/task_office/columns/schemas/basic_schemas.py +++ b/task_office/columns/schemas/basic_schemas.py @@ -7,7 +7,7 @@ from task_office.core.enums import XEnum, OrderingDirection from task_office.core.models.db_models import BoardColumn from task_office.core.schemas.base_schemas import BaseSchema, ListSchema, XSchema -from task_office.swagger import API_SPEC +from task_office.settings import CONFIG class ColumnPostSchema(BaseSchema): @@ -45,9 +45,9 @@ class Meta: column_put_schema = ColumnPutSchema() column_dump_schema = ColumnDumpSchema() column_listed_dump_schema = ColumnDumpSchema(many=True) -API_SPEC.components.schema("ColumnPostSchema", schema=ColumnPostSchema) -API_SPEC.components.schema("ColumnPutSchema", schema=ColumnPutSchema) -API_SPEC.components.schema("ColumnDumpSchema", schema=ColumnDumpSchema) +CONFIG.API_SPEC.components.schema("ColumnPostSchema", schema=ColumnPostSchema) +CONFIG.API_SPEC.components.schema("ColumnPutSchema", schema=ColumnPutSchema) +CONFIG.API_SPEC.components.schema("ColumnDumpSchema", schema=ColumnDumpSchema) class ColumnListQuerySchema(ListSchema): @@ -71,4 +71,4 @@ class Meta: column_list_query_schema = ColumnListQuerySchema() -API_SPEC.components.schema("ColumnListQuerySchema", schema=ColumnListQuerySchema) +CONFIG.API_SPEC.components.schema("ColumnListQuerySchema", schema=ColumnListQuerySchema) diff --git a/task_office/core/schemas/nested_schemas.py b/task_office/core/schemas/nested_schemas.py index d1337bc..3b4481a 100644 --- a/task_office/core/schemas/nested_schemas.py +++ b/task_office/core/schemas/nested_schemas.py @@ -3,7 +3,7 @@ from marshmallow import fields, post_dump from task_office.core.schemas.base_schemas import XSchema -from task_office.swagger import API_SPEC +from task_office.settings import CONFIG class NestedUserDumpSchema(XSchema): @@ -40,5 +40,7 @@ class Meta: nested_column_dump_schema = NestedColumnDumpSchema() -API_SPEC.components.schema("NestedUserSchema", schema=NestedUserDumpSchema) -API_SPEC.components.schema("NestedColumnDumpSchema", schema=NestedColumnDumpSchema) +CONFIG.API_SPEC.components.schema("NestedUserSchema", schema=NestedUserDumpSchema) +CONFIG.API_SPEC.components.schema( + "NestedColumnDumpSchema", schema=NestedColumnDumpSchema +) diff --git a/task_office/permissions/schemas/basic_schemas.py b/task_office/permissions/schemas/basic_schemas.py index fa0e89c..3bb5d7c 100644 --- a/task_office/permissions/schemas/basic_schemas.py +++ b/task_office/permissions/schemas/basic_schemas.py @@ -9,7 +9,7 @@ from task_office.core.schemas.base_schemas import BaseSchema, ListSchema, XSchema from task_office.core.schemas.nested_schemas import NestedUserDumpSchema from task_office.core.validators import PKExists -from task_office.swagger import API_SPEC +from ...settings import CONFIG class PermissionQuerySchema(BaseSchema): @@ -45,8 +45,8 @@ class Meta: permission_query_schema = PermissionQuerySchema() permission_dump_schema = PermissionDumpSchema() permission_list_dump_schema = PermissionDumpSchema(many=True) -API_SPEC.components.schema("PermissionInSchema", schema=PermissionQuerySchema) -API_SPEC.components.schema("PermissionOutSchema", schema=PermissionDumpSchema) +CONFIG.API_SPEC.components.schema("PermissionInSchema", schema=PermissionQuerySchema) +CONFIG.API_SPEC.components.schema("PermissionOutSchema", schema=PermissionDumpSchema) class PermissionListQuerySchema(ListSchema): @@ -70,6 +70,6 @@ class Meta: permissions_list_query_schema = PermissionListQuerySchema() -API_SPEC.components.schema( +CONFIG.API_SPEC.components.schema( "PermissionListQuerySchema", schema=PermissionListQuerySchema ) diff --git a/task_office/settings.py b/task_office/settings.py index aa46ecf..21ce00d 100644 --- a/task_office/settings.py +++ b/task_office/settings.py @@ -2,6 +2,9 @@ import os from datetime import timedelta +from apispec import APISpec +from apispec.ext.marshmallow import MarshmallowPlugin +from apispec_webframeworks.flask import FlaskPlugin from environs import Env env = Env() @@ -23,9 +26,18 @@ class Config(object): PROJECT_NAME = env.str("PROJECT_NAME", "Task Office") SECRET_KEY = env.str("FLASK_SECRET", "secret-key") - API_V1_PREFIX = "/api/v1" + API_V1_PREFIX = "/api/v1/" API_DATETIME_FORMAT = "%Y-%m-%d %I:%M:%S" USE_DOCS = env.bool("USE_DOCS", False) + + API_SPEC = APISpec( + openapi_version="3.0.2", + title=PROJECT_NAME, + version="1.0.0", + info=dict(description=f"{PROJECT_NAME} API"), + plugins=[FlaskPlugin(), MarshmallowPlugin()], + ) + FLASK_DEBUG = env.int("FLASK_DEBUG", 0) DEBUG_TB_INTERCEPT_REDIRECTS = False diff --git a/task_office/swagger/specs.py b/task_office/swagger/specs.py deleted file mode 100644 index 1d89e38..0000000 --- a/task_office/swagger/specs.py +++ /dev/null @@ -1,16 +0,0 @@ -from apispec import APISpec -from apispec.ext.marshmallow import MarshmallowPlugin -from apispec_webframeworks.flask import FlaskPlugin - -from task_office.settings import CONFIG - -API_SPEC = APISpec( - openapi_version="3.0.0", - title=CONFIG.PROJECT_NAME, - version="1.0.0", - info=dict(description="Some Description"), - plugins=[FlaskPlugin(), MarshmallowPlugin()], -) - -# TODO(Medniy) wait for a DocumentedBlueprint and make urls generation -# https://github.com/marshmallow-code/apispec-webframeworks/pull/27 diff --git a/task_office/swagger/views.py b/task_office/swagger/views.py index 1f55785..cc32050 100644 --- a/task_office/swagger/views.py +++ b/task_office/swagger/views.py @@ -3,15 +3,14 @@ from flask import Blueprint, jsonify from flask_swagger_ui import get_swaggerui_blueprint -from .specs import API_SPEC -from ..settings import CONFIG +from task_office.settings import CONFIG -SWAGGER_URL = CONFIG.API_V1_PREFIX + "/docs" +SWAGGER_URL = CONFIG.API_V1_PREFIX + "docs" API_URL = "/api/v1/docs/open-api" -blueprint = Blueprint("docs", __name__, url_prefix=CONFIG.API_V1_PREFIX + "/docs") +blueprint = Blueprint("docs", __name__, url_prefix=SWAGGER_URL) blueprint_swagger = get_swaggerui_blueprint( SWAGGER_URL, API_URL, config={"app_name": CONFIG.PROJECT_NAME} @@ -24,4 +23,5 @@ def api_swagger(**kwargs): :param kwargs: :return: """ - return jsonify(API_SPEC.to_dict()) + data = CONFIG.API_SPEC.to_dict() + return jsonify(data) diff --git a/task_office/tasks/schemas/basic_schemas.py b/task_office/tasks/schemas/basic_schemas.py index c535636..508c6f5 100644 --- a/task_office/tasks/schemas/basic_schemas.py +++ b/task_office/tasks/schemas/basic_schemas.py @@ -11,7 +11,6 @@ from task_office.core.schemas.nested_schemas import NestedUserDumpSchema from task_office.core.validators import PKExists from task_office.settings import CONFIG -from task_office.swagger import API_SPEC from task_office.tasks.schemas.search_schemas import SearchTaskSchema @@ -119,9 +118,9 @@ class Meta: task_put_schema = TaskPutSchema() task_dump_schema = TaskDumpSchema() tasks_listed_dump_schema = TaskDumpSchema(many=True) -API_SPEC.components.schema("TaskPostSchema", schema=TaskPostSchema) -API_SPEC.components.schema("TaskPutSchema", schema=TaskPutSchema) -API_SPEC.components.schema("TaskDumpSchema", schema=TaskDumpSchema) +CONFIG.API_SPEC.components.schema("TaskPostSchema", schema=TaskPostSchema) +CONFIG.API_SPEC.components.schema("TaskPutSchema", schema=TaskPutSchema) +CONFIG.API_SPEC.components.schema("TaskDumpSchema", schema=TaskDumpSchema) class TaskListQuerySchema(ListSchema): @@ -151,7 +150,7 @@ def preload_data(self, data, **kwargs): task_list_query_schema = TaskListQuerySchema() -API_SPEC.components.schema("TaskListQuerySchema", schema=TaskListQuerySchema) +CONFIG.API_SPEC.components.schema("TaskListQuerySchema", schema=TaskListQuerySchema) class ColumnWithTasksDumpSchema(BaseSchema): @@ -169,7 +168,7 @@ class Meta: columns_listed_dump_schema = ColumnWithTasksDumpSchema(many=True) -API_SPEC.components.schema( +CONFIG.API_SPEC.components.schema( "ColumnWithTasksDumpSchema", schema=ColumnWithTasksDumpSchema ) @@ -189,6 +188,6 @@ class Meta: task_list_by_columns_query_schema = TaskListByColumnsQuerySchema() -API_SPEC.components.schema( +CONFIG.API_SPEC.components.schema( "TaskListByColumnsQuerySchema", schema=TaskListByColumnsQuerySchema ) diff --git a/task_office/tasks/schemas/search_schemas.py b/task_office/tasks/schemas/search_schemas.py index 6df5940..6779e79 100644 --- a/task_office/tasks/schemas/search_schemas.py +++ b/task_office/tasks/schemas/search_schemas.py @@ -2,7 +2,7 @@ from task_office.core.models.db_models import Task from task_office.core.schemas.base_schemas import SearchSchema -from task_office.swagger import API_SPEC +from task_office.settings import CONFIG class SearchTaskSchema(SearchSchema): @@ -23,4 +23,4 @@ class Meta: search_task_schema = SearchTaskSchema() -API_SPEC.components.schema("SearchTaskSchema", schema=SearchTaskSchema) +CONFIG.API_SPEC.components.schema("SearchTaskSchema", schema=SearchTaskSchema) From e051254eb75c65dfea335100138ddf09558db310 Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Sun, 22 Mar 2020 20:55:46 +0200 Subject: [PATCH 30/60] init api docs --- TODO.rst | 2 +- task_office/swagger/api_paths.py | 478 +++++++++++++++++++++++++++++++ task_office/swagger/views.py | 5 +- 3 files changed, 482 insertions(+), 3 deletions(-) create mode 100644 task_office/swagger/api_paths.py diff --git a/TODO.rst b/TODO.rst index b883a8f..b8a30a8 100644 --- a/TODO.rst +++ b/TODO.rst @@ -5,7 +5,7 @@ TODO List: Issues General ^^^^^^^^^^^^^^ * Try to handle all errors and wrap them by our custom error class(InvalidUsage), with using translations -* Implement Documented BluePrint +* Implement auto-generated api docs with swagger ui Issues By Features ^^^^^^^^^^^^^^^^^^ diff --git a/task_office/swagger/api_paths.py b/task_office/swagger/api_paths.py new file mode 100644 index 0000000..f1118a4 --- /dev/null +++ b/task_office/swagger/api_paths.py @@ -0,0 +1,478 @@ +API_PATHS = { + # Auth + "/api/v1/auth/sign-up": { + "post": { + "tags": ["Auth"], + "summary": "Sign Up, Create new User", + "requestBody": { + "description": "Sign Up Post Object", + "required": True, + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/UserSignUpSchema"} + } + }, + }, + "produces": ["application/json"], + "responses": { + "201": { + "required": True, + "description": "OK", + "schema": {"$ref": "#/components/schemas/UserSchema"}, + }, + "400": {"description": "Failed. Bad post data."}, + "422": {"description": "Failed. Bad post data."}, + }, + } + }, + "/api/v1/auth/sign-in": { + "post": { + "tags": ["Auth"], + "summary": "Sign In User", + "requestBody": { + "description": "Sign In Post Object", + "required": True, + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/UserSignInSchema"} + } + }, + }, + "produces": ["application/json"], + "responses": { + "201": { + "required": True, + "description": "OK", + "schema": {"$ref": "#/components/schemas/SignedSchema"}, + }, + "400": {"description": "Failed. Bad post data."}, + "422": {"description": "Failed. Bad post data."}, + }, + } + }, + "/api/v1/auth/refresh": { + "post": { + "tags": ["Auth"], + "summary": "Refresh Access token", + "requestBody": { + "description": "Refresh Post Object", + "required": True, + "content": {"application/json": {}}, + }, + "produces": ["application/json"], + "responses": { + "200": { + "required": True, + "description": "OK", + "schema": { + "$ref": "#/components/schemas/RefreshedAccessTokenSchema" + }, + }, + "400": {"description": "Failed. Bad post data."}, + "401": {"description": "Failed. Bad post data."}, + "422": {"description": "Failed. Bad post data."}, + }, + } + }, + # Boards + "/api/v1/boards": { + "post": { + "tags": ["Boards"], + "summary": "Boards create", + "requestBody": { + "description": "Boards Post Object", + "required": True, + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/BoardPutSchema"} + } + }, + }, + "produces": ["application/json"], + "responses": { + "200": { + "required": True, + "description": "OK", + "schema": {"$ref": "#/components/schemas/BoardDumpSchema"}, + }, + "400": {"description": "Failed. Bad data."}, + "401": {"description": "Failed. Not authorized."}, + "403": {"description": "Failed. Not denied."}, + "422": {"description": "Failed. Bad data."}, + }, + }, + "get": { + "tags": ["Boards"], + "summary": "Returns Boards", + "requestBody": { + "description": "Boards get Object", + "required": True, + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/BoardListQuerySchema"} + } + }, + }, + "responses": { + "200": { + "description": "OK", + "schema": {"$ref": "#/components/schemas/BoardDumpSchema"}, + }, + "400": {"description": "Failed. Bad data."}, + "401": {"description": "Failed. Not authorized."}, + "403": {"description": "Failed. Not denied."}, + "422": {"description": "Failed. Bad data."}, + }, + }, + }, + "/api/v1/boards/": { + "get": { + "tags": ["Boards"], + "summary": "Returns Boards", + "responses": { + "200": { + "description": "OK", + "schema": {"$ref": "#/components/schemas/BoardDumpSchema"}, + }, + "400": {"description": "Failed. Bad data."}, + "401": {"description": "Failed. Not authorized."}, + "403": {"description": "Failed. Not denied."}, + "404": {"description": "Failed. Not found."}, + "422": {"description": "Failed. Bad data."}, + }, + } + }, + "/api/v1/boards//users": { + "get": { + "tags": ["Boards"], + "summary": "Returns Users of boards", + "requestBody": { + "description": "Users get Object", + "required": True, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserListByBoardQuerySchema" + } + } + }, + }, + "responses": { + "200": { + "description": "OK", + "schema": {"$ref": "#/components/schemas/NestedUserDumpSchema"}, + }, + "400": {"description": "Failed. Bad data."}, + "401": {"description": "Failed. Not authorized."}, + "403": {"description": "Failed. Not denied."}, + "404": {"description": "Failed. Not found."}, + "422": {"description": "Failed. Bad data."}, + }, + } + }, + "/api/v1/boards//columns": { + "post": { + "tags": ["Columns"], + "summary": "Boards Columns create", + "requestBody": { + "description": "Boards Columns Post Object", + "required": True, + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/ColumnPostSchema"} + } + }, + }, + "produces": ["application/json"], + "responses": { + "200": { + "required": True, + "description": "OK", + "schema": {"$ref": "#/components/schemas/ColumnDumpSchema"}, + }, + "400": {"description": "Failed. Bad data."}, + "401": {"description": "Failed. Not authorized."}, + "403": {"description": "Failed. Not denied."}, + "404": {"description": "Failed. Not found."}, + "422": {"description": "Failed. Bad data."}, + }, + }, + "get": { + "tags": ["Columns"], + "summary": "Returns Boards Columns", + "requestBody": { + "description": "Boards Columns Get Object", + "required": True, + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/ColumnListQuerySchema"} + } + }, + }, + "responses": { + "200": { + "description": "OK", + "schema": {"$ref": "#/components/schemas/ColumnDumpSchema"}, + }, + "400": {"description": "Failed. Bad data."}, + "401": {"description": "Failed. Not authorized."}, + "403": {"description": "Failed. Not denied."}, + "404": {"description": "Failed. Not found."}, + "422": {"description": "Failed. Bad data."}, + }, + }, + }, + "/api/v1/boards//columns/": { + "put": { + "tags": ["Columns"], + "summary": "Boards Columns Update", + "requestBody": { + "description": "Update Boards Columns Object", + "required": True, + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/ColumnPutSchema"} + } + }, + }, + "produces": ["application/json"], + "responses": { + "200": { + "required": True, + "description": "OK", + "schema": {"$ref": "#/components/schemas/ColumnDumpSchema"}, + }, + "400": {"description": "Failed. Bad data."}, + "401": {"description": "Failed. Not authorized."}, + "403": {"description": "Failed. Not denied."}, + "404": {"description": "Failed. Not found."}, + "422": {"description": "Failed. Bad data."}, + }, + } + }, + # Tasks + "/api/v1/boards//tasks": { + "post": { + "tags": ["Tasks"], + "summary": "Tasks create", + "requestBody": { + "description": "Tasks Post Object", + "required": True, + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/TaskPostSchema"} + } + }, + }, + "produces": ["application/json"], + "responses": { + "200": { + "required": True, + "description": "OK", + "schema": {"$ref": "#/components/schemas/TaskDumpSchema"}, + }, + "400": {"description": "Failed. Bad data."}, + "401": {"description": "Failed. Not authorized."}, + "403": {"description": "Failed. Not denied."}, + "422": {"description": "Failed. Bad data."}, + }, + }, + "get": { + "tags": ["Tasks"], + "summary": "Returns Tasks", + "requestBody": { + "description": "Tasks Get list of Objects", + "required": True, + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/TaskListQuerySchema"} + } + }, + }, + "responses": { + "200": { + "description": "OK", + "schema": {"$ref": "#/components/schemas/TaskDumpSchema"}, + }, + "400": {"description": "Failed. Bad data."}, + "401": {"description": "Failed. Not authorized."}, + "403": {"description": "Failed. Not denied."}, + "422": {"description": "Failed. Bad data."}, + }, + }, + }, + "/api/v1/boards//tasks/": { + "put": { + "tags": ["Tasks"], + "summary": "Tasks Update", + "requestBody": { + "description": "Tasks Put Object", + "required": True, + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/TaskPutSchema"} + } + }, + }, + "produces": ["application/json"], + "responses": { + "200": { + "required": True, + "description": "OK", + "schema": {"$ref": "#/components/schemas/TaskDumpSchema"}, + }, + "400": {"description": "Failed. Bad data."}, + "401": {"description": "Failed. Not authorized."}, + "403": {"description": "Failed. Not denied."}, + "422": {"description": "Failed. Bad data."}, + }, + } + }, + "/api/v1/boards//tasks/by-columns": { + "get": { + "tags": ["Tasks"], + "summary": "Returns Tasks", + "requestBody": { + "description": "Tasks by columns Get list of Objects", + "required": True, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TaskListByColumnsQuerySchema" + } + } + }, + }, + "responses": { + "200": { + "description": "OK", + "schema": {"$ref": "#/components/schemas/TaskDumpSchema"}, + }, + "400": {"description": "Failed. Bad data."}, + "401": {"description": "Failed. Not authorized."}, + "403": {"description": "Failed. Not denied."}, + "422": {"description": "Failed. Bad data."}, + }, + } + }, + "/api/v1/boards//tasks/meta": { + "get": { + "tags": ["Tasks"], + "summary": "Returns additional tasks info", + "responses": { + "200": {"description": "OK", "schema": {}}, + "400": {"description": "Failed. Bad data."}, + "401": {"description": "Failed. Not authorized."}, + "403": {"description": "Failed. Not denied."}, + "422": {"description": "Failed. Bad data."}, + }, + } + }, + # Permissions + "/api/v1/boards//permissions": { + "post": { + "tags": ["Permissions"], + "summary": "Permissions create", + "requestBody": { + "description": "Permissions Post Object", + "required": True, + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/PermissionQuerySchema"} + } + }, + }, + "produces": ["application/json"], + "responses": { + "200": { + "required": True, + "description": "OK", + "schema": {"$ref": "#/components/schemas/ PermissionDumpSchema"}, + }, + "400": {"description": "Failed. Bad data."}, + "401": {"description": "Failed. Not authorized."}, + "403": {"description": "Failed. Not denied."}, + "422": {"description": "Failed. Bad data."}, + }, + }, + "get": { + "tags": ["Permissions"], + "summary": "Returns Permissions", + "requestBody": { + "description": "Permissions get list of Objects", + "required": True, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PermissionListQuerySchema" + } + } + }, + }, + "responses": { + "200": { + "description": "OK", + "schema": {"$ref": "#/components/schemas/PermissionDumpSchema"}, + }, + "400": {"description": "Failed. Bad data."}, + "401": {"description": "Failed. Not authorized."}, + "403": {"description": "Failed. Not denied."}, + "422": {"description": "Failed. Bad data."}, + }, + }, + }, + "/api/v1/boards//permissions/": { + "put": { + "tags": ["Permissions"], + "summary": "Permissions update", + "requestBody": { + "description": "Permissions Put Object", + "required": True, + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/PermissionQuerySchema"} + } + }, + }, + "produces": ["application/json"], + "responses": { + "200": { + "required": True, + "description": "OK", + "schema": {"$ref": "#/components/schemas/ PermissionDumpSchema"}, + }, + "400": {"description": "Failed. Bad data."}, + "401": {"description": "Failed. Not authorized."}, + "403": {"description": "Failed. Not denied."}, + "422": {"description": "Failed. Bad data."}, + }, + }, + "get": { + "tags": ["Permissions"], + "summary": "Returns Permissions", + "responses": { + "200": { + "description": "OK", + "schema": {"$ref": "#/components/schemas/PermissionDumpSchema"}, + }, + "400": {"description": "Failed. Bad data."}, + "401": {"description": "Failed. Not authorized."}, + "403": {"description": "Failed. Not denied."}, + "422": {"description": "Failed. Bad data."}, + }, + }, + }, + "/api/v1/boards//permissions/meta": { + "get": { + "tags": ["Permissions"], + "summary": "Returns additions permissions info", + "responses": { + "200": {"description": "OK", "schema": {}}, + "400": {"description": "Failed. Bad data."}, + "401": {"description": "Failed. Not authorized."}, + "403": {"description": "Failed. Not denied."}, + "422": {"description": "Failed. Bad data."}, + }, + } + }, +} diff --git a/task_office/swagger/views.py b/task_office/swagger/views.py index cc32050..89a3092 100644 --- a/task_office/swagger/views.py +++ b/task_office/swagger/views.py @@ -4,10 +4,10 @@ from flask_swagger_ui import get_swaggerui_blueprint from task_office.settings import CONFIG +from task_office.swagger.api_paths import API_PATHS SWAGGER_URL = CONFIG.API_V1_PREFIX + "docs" - -API_URL = "/api/v1/docs/open-api" +API_URL = SWAGGER_URL + "/open-api" blueprint = Blueprint("docs", __name__, url_prefix=SWAGGER_URL) @@ -24,4 +24,5 @@ def api_swagger(**kwargs): :return: """ data = CONFIG.API_SPEC.to_dict() + data["paths"] = API_PATHS return jsonify(data) From 6938425dbf2cc96ca18ca9445f0662f20e1b02a4 Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Sat, 28 Mar 2020 17:56:45 +0200 Subject: [PATCH 31/60] boards update --- task_office/auth/utils.py | 22 ++++++++- task_office/boards/schemas/basic_schemas.py | 13 ++---- task_office/boards/views.py | 50 ++++++++++++++++++--- task_office/permissions/views.py | 6 +-- task_office/swagger/api_paths.py | 29 +++++++++++- 5 files changed, 99 insertions(+), 21 deletions(-) diff --git a/task_office/auth/utils.py b/task_office/auth/utils.py index 8515c60..4345229 100644 --- a/task_office/auth/utils.py +++ b/task_office/auth/utils.py @@ -3,6 +3,7 @@ from flask_babel import lazy_gettext as _ from flask_jwt_extended import get_current_user +from task_office.core.models.db_models import Permission from task_office.exceptions import InvalidUsage from task_office.extensions import cache from task_office.settings import CONFIG @@ -22,11 +23,30 @@ def _get_cached_permissions(): return perms -def reset_cached_permissions(user_uuid_hexed): +def reset_permissions(user_uuid_hexed): + """ + Clear permissions from cache + :param user_uuid_hexed: + :return: + """ key = f"perms_{user_uuid_hexed}" return cache.delete(key) +def reset_permissions_for_board_staff(board_uuid): + """ + Clear permissions for board staff(Role.STAFF) + :param board_uuid: + :return: + """ + perms = Permission.query.filter_by( + board_uuid=board_uuid, role=Permission.Role.STAFF.value + ).all() + for item in perms: + user_uuid = uuid.UUID(str(item.user_uuid)).hex + reset_permissions(user_uuid) + + def permission(required_role: int): def decorator(func): def wrapper(*args, **kwargs): diff --git a/task_office/boards/schemas/basic_schemas.py b/task_office/boards/schemas/basic_schemas.py index 1261f13..8971075 100644 --- a/task_office/boards/schemas/basic_schemas.py +++ b/task_office/boards/schemas/basic_schemas.py @@ -4,28 +4,21 @@ from marshmallow.validate import Length from marshmallow_enum import EnumField -from task_office.auth import User from task_office.boards.schemas.search_schemas import SearchUserSchema from task_office.core.enums import XEnum from task_office.core.schemas.base_schemas import BaseSchema, ListSchema, XSchema from task_office.core.schemas.nested_schemas import NestedUserDumpSchema -from task_office.core.validators import PKExists from task_office.settings import CONFIG -class BoardPutSchema(BaseSchema): +class BoardActionsSchema(BaseSchema): name = fields.Str(required=True, allow_none=False, validate=[Length(max=255)]) description = fields.Str(allow_none=True, required=False, default="") - owner_uuid = fields.UUID(required=True, validate=[PKExists(User, "uuid")]) is_active = fields.Boolean(default=True) class Meta: strict = True - @validates_schema - def validate_schema(self, data, **kwargs): - data["owner_uuid"] = str(data.pop("owner_uuid")) - class BoardDumpSchema(BaseSchema): name = fields.Str(dump_only=True) @@ -41,10 +34,10 @@ def validate_schema(self, data, **kwargs): data["uuid"] = uuid.UUID(data.pop("uuid")).hex -board_put_schema = BoardPutSchema() +board_action_schema = BoardActionsSchema() board_dump_schema = BoardDumpSchema() board_list_dump_schema = BoardDumpSchema(many=True) -CONFIG.API_SPEC.components.schema("BoardPutSchema", schema=BoardPutSchema) +CONFIG.API_SPEC.components.schema("BoardActionsSchema", schema=BoardActionsSchema) CONFIG.API_SPEC.components.schema("BoardDumpSchema", schema=BoardDumpSchema) diff --git a/task_office/boards/views.py b/task_office/boards/views.py index f762c69..186e8f8 100644 --- a/task_office/boards/views.py +++ b/task_office/boards/views.py @@ -1,4 +1,6 @@ """Boards views.""" +from datetime import datetime + from flask import Blueprint from flask_apispec import use_kwargs, marshal_with from flask_jwt_extended import jwt_required, get_current_user @@ -6,29 +8,40 @@ from .constants import BOARDS_PREFIX from .schemas.basic_schemas import ( - board_put_schema, + board_action_schema, board_list_query_schema, board_list_dump_schema, board_dump_schema, user_list_by_board_query_schema, ) -from ..auth.utils import permission +from ..auth.utils import permission, reset_permissions_for_board_staff from ..core.helpers.listed_response import listed_response from ..core.models.db_models import Board, Permission, User from ..core.schemas.nested_schemas import nested_user_list_dump_schema -from ..core.utils import empty_query_required, validate_request_url_uuid +from ..core.utils import ( + empty_query_required, + validate_request_url_uuid, + non_empty_query_required, +) blueprint = Blueprint("boards", __name__, url_prefix=BOARDS_PREFIX) @blueprint.route("", methods=("post",)) @jwt_required -@use_kwargs(board_put_schema) +@use_kwargs(board_action_schema) @marshal_with(board_dump_schema) def create_boards(**kwargs): data = kwargs + + # validate current user + user = get_current_user() + user_uuid = str(user.uuid) if user else None + data["owner_uuid"] = user_uuid + # Check name, owner_uuid are unique for board - empty_query_required(Board, name=data["name"], owner_uuid=str(data["owner_uuid"])) + empty_query_required(Board, name=data["name"], owner_uuid=data["owner_uuid"]) + board = Board(**data) board.save() @@ -42,6 +55,33 @@ def create_boards(**kwargs): return board +@blueprint.route("/", methods=("put",)) +@jwt_required +@use_kwargs(board_action_schema) +@marshal_with(board_dump_schema) +def update_board(board_uuid, **kwargs): + """ + :param board_uuid: + :param kwargs: + :return: + """ + data = kwargs + validate_request_url_uuid(Board, "uuid", board_uuid, True) + + board = non_empty_query_required(Board, uuid=board_uuid)[1] + + if data.get("name", board.name) != board.name: + empty_query_required(Board, name=data["name"], owner_uuid=str(board.owner_uuid)) + + if data.get("is_active", None) is not None: + reset_permissions_for_board_staff(board_uuid) + + board.update(updated_at=datetime.utcnow(), **data) + board.save() + + return board + + @blueprint.route("", methods=("get",)) @jwt_required @use_kwargs(board_list_query_schema) diff --git a/task_office/permissions/views.py b/task_office/permissions/views.py index c55c880..be1c7f7 100644 --- a/task_office/permissions/views.py +++ b/task_office/permissions/views.py @@ -14,7 +14,7 @@ permissions_list_query_schema, permission_list_dump_schema, ) -from ..auth.utils import permission, reset_cached_permissions +from ..auth.utils import permission, reset_permissions from ..core.helpers.listed_response import listed_response from ..core.models.db_models import Permission, Board from ..core.utils import is_uuid, non_empty_query_required, empty_query_required @@ -70,7 +70,7 @@ def create_permission(board_uuid, **kwargs): perm = Permission(board_uuid=board_uuid, **data) perm.save() - reset_cached_permissions(uuid.UUID(perm.user_uuid).hex) + reset_permissions(uuid.UUID(perm.user_uuid).hex) return permission @@ -104,7 +104,7 @@ def update_permission(board_uuid, permission_uuid, **kwargs): perm.update(updated_at=datetime.utcnow(), **data) perm.save() - reset_cached_permissions(uuid.UUID(perm.user_uuid).hex) + reset_permissions(uuid.UUID(perm.user_uuid).hex) return permission diff --git a/task_office/swagger/api_paths.py b/task_office/swagger/api_paths.py index f1118a4..5891bda 100644 --- a/task_office/swagger/api_paths.py +++ b/task_office/swagger/api_paths.py @@ -84,7 +84,7 @@ "required": True, "content": { "application/json": { - "schema": {"$ref": "#/components/schemas/BoardPutSchema"} + "schema": {"$ref": "#/components/schemas/BoardActionsSchema"} } }, }, @@ -140,7 +140,32 @@ "404": {"description": "Failed. Not found."}, "422": {"description": "Failed. Bad data."}, }, - } + }, + "put": { + "tags": ["Boards"], + "summary": "Boards update", + "requestBody": { + "description": "Boards Put Object", + "required": True, + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/BoardActionsSchema"} + } + }, + }, + "produces": ["application/json"], + "responses": { + "200": { + "required": True, + "description": "OK", + "schema": {"$ref": "#/components/schemas/BoardDumpSchema"}, + }, + "400": {"description": "Failed. Bad data."}, + "401": {"description": "Failed. Not authorized."}, + "403": {"description": "Failed. Not denied."}, + "422": {"description": "Failed. Bad data."}, + }, + }, }, "/api/v1/boards//users": { "get": { From c9da1480a6b976a191afb1188c5f662a1fe5cc50 Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Sat, 28 Mar 2020 18:40:06 +0200 Subject: [PATCH 32/60] updated translations --- README.rst | 14 ++- translations/en/LC_MESSAGES/messages.po | 106 +++++++++++++++++- translations/messages.pot | 141 ++++++++++++++++++++++++ translations/ru/LC_MESSAGES/messages.po | 104 ++++++++++++++++- translations/uk/LC_MESSAGES/messages.po | 104 ++++++++++++++++- 5 files changed, 451 insertions(+), 18 deletions(-) create mode 100644 translations/messages.pot diff --git a/README.rst b/README.rst index 7d9e40e..c444aa2 100644 --- a/README.rst +++ b/README.rst @@ -32,12 +32,22 @@ To run the web application use:: Translations commands:: - pybabel extract -F babel.cfg -k lazy_gettext -o messages.pot . + https://pythonhosted.org/Flask-Babel/ + + # Create(if not exists) map of translations + pybabel extract -F babel.cfg -k lazy_gettext -o translations/messages.pot . + + # Init translations(if not exists) pybabel init -i messages.pot -d translations -l en pybabel init -i messages.pot -d translations -l uk pybabel init -i messages.pot -d translations -l ru + + # Update map and translations + pybabel update -i translations/messages.pot -d translations + + # Compile translations pybabel compile -d translations - pybabel update -i messages.pot -d translations + Features -------- diff --git a/translations/en/LC_MESSAGES/messages.po b/translations/en/LC_MESSAGES/messages.po index 8d77a9f..bae377a 100644 --- a/translations/en/LC_MESSAGES/messages.po +++ b/translations/en/LC_MESSAGES/messages.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2020-01-04 15:41+0200\n" -"PO-Revision-Date: 2020-01-04 15:13+0200\n" +"POT-Creation-Date: 2020-03-28 18:08+0200\n" +"PO-Revision-Date: 2020-03-28 18:05+0200\n" "Last-Translator: FULL NAME \n" "Language: en\n" "Language-Team: en \n" @@ -30,19 +30,113 @@ msgstr "Fresh token required" msgid "Error loading the user {}" msgstr "Error loading the user {}" -#: task_office/auth/jwt_error_handlers.py:62 +#: task_office/auth/jwt_error_handlers.py:63 msgid "User claims verification failed" msgstr "User claims verification failed" -#: task_office/auth/serializers.py:30 +#: task_office/auth/schemas.py:25 msgid "Passwords do not match" msgstr "Passwords do not match" -#: task_office/auth/serializers.py:60 +#: task_office/auth/schemas.py:55 msgid "User not found" msgstr "User not found" -#: task_office/core/validators.py:10 +#: task_office/auth/utils.py:57 task_office/permissions/views.py:69 +#: task_office/permissions/views.py:99 +msgid "Not allowed" +msgstr "Not allowed" + +#: task_office/columns/views.py:60 task_office/tasks/views.py:79 +msgid "Must be between {} and {}" +msgstr "Must be between {} and {}" + +#: task_office/columns/views.py:71 task_office/columns/views.py:121 +#: task_office/core/validators.py:10 task_office/tasks/views.py:93 +#: task_office/tasks/views.py:163 msgid "Already exists with value {}" msgstr "Already exists with value {}" +#: task_office/columns/views.py:108 task_office/tasks/views.py:142 +msgid "Must be between {} and {}." +msgstr "Must be between {} and {}." + +#: task_office/core/enums.py:43 +msgid "Ascend" +msgstr "Ascend" + +#: task_office/core/enums.py:44 +msgid "Descend" +msgstr "Descend" + +#: task_office/core/utils.py:23 task_office/core/utils.py:53 +#: task_office/core/utils.py:57 task_office/permissions/views.py:36 +#: task_office/permissions/views.py:58 task_office/permissions/views.py:92 +#: task_office/permissions/views.py:124 task_office/permissions/views.py:147 +msgid "Not found" +msgstr "Not found" + +#: task_office/core/utils.py:31 +msgid "Already exists" +msgstr "Already exists" + +#: task_office/core/validators.py:37 +msgid "Not found with value {}" +msgstr "Not found with value {}" + +#: task_office/core/models/db_models.py:76 +msgid "Owner" +msgstr "Owner" + +#: task_office/core/models/db_models.py:76 +msgid "Owner of board(creator)" +msgstr "Owner of board(creator)" + +#: task_office/core/models/db_models.py:77 +msgid "Editor" +msgstr "Editor" + +#: task_office/core/models/db_models.py:77 +msgid "Editor of board" +msgstr "Editor of board" + +#: task_office/core/models/db_models.py:78 +msgid "Staff" +msgstr "Staff" + +#: task_office/core/models/db_models.py:78 +msgid "Ordinary user" +msgstr "Ordinary user" + +#: task_office/core/models/db_models.py:130 +msgid "New" +msgstr "New" + +#: task_office/core/models/db_models.py:131 +msgid "In process" +msgstr "In process" + +#: task_office/core/models/db_models.py:132 +msgid "Rejected" +msgstr "Rejected" + +#: task_office/core/models/db_models.py:133 +msgid "Done" +msgstr "Done" + +#: task_office/core/schemas/base_schemas.py:54 +msgid "Max limit {} exceeded" +msgstr "Max limit {} exceeded" + +#: task_office/tasks/schemas/basic_schemas.py:130 +#: task_office/tasks/schemas/basic_schemas.py:132 +#: task_office/tasks/schemas/basic_schemas.py:180 +msgid "ascending" +msgstr "ascending" + +#: task_office/tasks/schemas/basic_schemas.py:131 +#: task_office/tasks/schemas/basic_schemas.py:133 +#: task_office/tasks/schemas/basic_schemas.py:181 +msgid "descending" +msgstr "descending" + diff --git a/translations/messages.pot b/translations/messages.pot new file mode 100644 index 0000000..851218f --- /dev/null +++ b/translations/messages.pot @@ -0,0 +1,141 @@ +# Translations template for PROJECT. +# Copyright (C) 2020 ORGANIZATION +# This file is distributed under the same license as the PROJECT project. +# FIRST AUTHOR , 2020. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2020-03-28 18:08+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.8.0\n" + +#: task_office/auth/jwt_error_handlers.py:44 +msgid "Token has been revoked" +msgstr "" + +#: task_office/auth/jwt_error_handlers.py:48 +msgid "Fresh token required" +msgstr "" + +#: task_office/auth/jwt_error_handlers.py:57 +msgid "Error loading the user {}" +msgstr "" + +#: task_office/auth/jwt_error_handlers.py:63 +msgid "User claims verification failed" +msgstr "" + +#: task_office/auth/schemas.py:25 +msgid "Passwords do not match" +msgstr "" + +#: task_office/auth/schemas.py:55 +msgid "User not found" +msgstr "" + +#: task_office/auth/utils.py:57 task_office/permissions/views.py:69 +#: task_office/permissions/views.py:99 +msgid "Not allowed" +msgstr "" + +#: task_office/columns/views.py:60 task_office/tasks/views.py:79 +msgid "Must be between {} and {}" +msgstr "" + +#: task_office/columns/views.py:71 task_office/columns/views.py:121 +#: task_office/core/validators.py:10 task_office/tasks/views.py:93 +#: task_office/tasks/views.py:163 +msgid "Already exists with value {}" +msgstr "" + +#: task_office/columns/views.py:108 task_office/tasks/views.py:142 +msgid "Must be between {} and {}." +msgstr "" + +#: task_office/core/enums.py:43 +msgid "Ascend" +msgstr "" + +#: task_office/core/enums.py:44 +msgid "Descend" +msgstr "" + +#: task_office/core/utils.py:23 task_office/core/utils.py:53 +#: task_office/core/utils.py:57 task_office/permissions/views.py:36 +#: task_office/permissions/views.py:58 task_office/permissions/views.py:92 +#: task_office/permissions/views.py:124 task_office/permissions/views.py:147 +msgid "Not found" +msgstr "" + +#: task_office/core/utils.py:31 +msgid "Already exists" +msgstr "" + +#: task_office/core/validators.py:37 +msgid "Not found with value {}" +msgstr "" + +#: task_office/core/models/db_models.py:76 +msgid "Owner" +msgstr "" + +#: task_office/core/models/db_models.py:76 +msgid "Owner of board(creator)" +msgstr "" + +#: task_office/core/models/db_models.py:77 +msgid "Editor" +msgstr "" + +#: task_office/core/models/db_models.py:77 +msgid "Editor of board" +msgstr "" + +#: task_office/core/models/db_models.py:78 +msgid "Staff" +msgstr "" + +#: task_office/core/models/db_models.py:78 +msgid "Ordinary user" +msgstr "" + +#: task_office/core/models/db_models.py:130 +msgid "New" +msgstr "" + +#: task_office/core/models/db_models.py:131 +msgid "In process" +msgstr "" + +#: task_office/core/models/db_models.py:132 +msgid "Rejected" +msgstr "" + +#: task_office/core/models/db_models.py:133 +msgid "Done" +msgstr "" + +#: task_office/core/schemas/base_schemas.py:54 +msgid "Max limit {} exceeded" +msgstr "" + +#: task_office/tasks/schemas/basic_schemas.py:130 +#: task_office/tasks/schemas/basic_schemas.py:132 +#: task_office/tasks/schemas/basic_schemas.py:180 +msgid "ascending" +msgstr "" + +#: task_office/tasks/schemas/basic_schemas.py:131 +#: task_office/tasks/schemas/basic_schemas.py:133 +#: task_office/tasks/schemas/basic_schemas.py:181 +msgid "descending" +msgstr "" + diff --git a/translations/ru/LC_MESSAGES/messages.po b/translations/ru/LC_MESSAGES/messages.po index 0d97921..eddf699 100644 --- a/translations/ru/LC_MESSAGES/messages.po +++ b/translations/ru/LC_MESSAGES/messages.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2020-01-04 15:41+0200\n" +"POT-Creation-Date: 2020-03-28 18:08+0200\n" "PO-Revision-Date: 2020-01-04 15:13+0200\n" "Last-Translator: FULL NAME \n" "Language: ru\n" @@ -31,19 +31,113 @@ msgstr "Фреш токен необходим" msgid "Error loading the user {}" msgstr "Ошибка загрузки пользователя" -#: task_office/auth/jwt_error_handlers.py:62 +#: task_office/auth/jwt_error_handlers.py:63 msgid "User claims verification failed" msgstr "Верификация данных пользователя провалена" -#: task_office/auth/serializers.py:30 +#: task_office/auth/schemas.py:25 msgid "Passwords do not match" msgstr "Пароли не совпадают" -#: task_office/auth/serializers.py:60 +#: task_office/auth/schemas.py:55 msgid "User not found" msgstr "Пользователь не найден" -#: task_office/core/validators.py:10 +#: task_office/auth/utils.py:57 task_office/permissions/views.py:69 +#: task_office/permissions/views.py:99 +msgid "Not allowed" +msgstr "Не разрешено" + +#: task_office/columns/views.py:60 task_office/tasks/views.py:79 +msgid "Must be between {} and {}" +msgstr "Должен быть между {} и {}" + +#: task_office/columns/views.py:71 task_office/columns/views.py:121 +#: task_office/core/validators.py:10 task_office/tasks/views.py:93 +#: task_office/tasks/views.py:163 msgid "Already exists with value {}" msgstr "Уже сужествует из значением {}" +#: task_office/columns/views.py:108 task_office/tasks/views.py:142 +msgid "Должен быть между {} и {}." +msgstr "" + +#: task_office/core/enums.py:43 +msgid "Ascend" +msgstr "Больше" + +#: task_office/core/enums.py:44 +msgid "Descend" +msgstr "Меньше" + +#: task_office/core/utils.py:23 task_office/core/utils.py:53 +#: task_office/core/utils.py:57 task_office/permissions/views.py:36 +#: task_office/permissions/views.py:58 task_office/permissions/views.py:92 +#: task_office/permissions/views.py:124 task_office/permissions/views.py:147 +msgid "Not found" +msgstr "Не найдено" + +#: task_office/core/utils.py:31 +msgid "Already exists" +msgstr "Уже существует" + +#: task_office/core/validators.py:37 +msgid "Not found with value {}" +msgstr "Не найдено из ззначением {}" + +#: task_office/core/models/db_models.py:76 +msgid "Owner" +msgstr "Владелец" + +#: task_office/core/models/db_models.py:76 +msgid "Owner of board(creator)" +msgstr "Владелеу доски(создатель)" + +#: task_office/core/models/db_models.py:77 +msgid "Editor" +msgstr "Редактор" + +#: task_office/core/models/db_models.py:77 +msgid "Editor of board" +msgstr "Редактор доски" + +#: task_office/core/models/db_models.py:78 +msgid "Staff" +msgstr "Персонал" + +#: task_office/core/models/db_models.py:78 +msgid "Ordinary user" +msgstr "Обычный пользователь" + +#: task_office/core/models/db_models.py:130 +msgid "New" +msgstr "Новый" + +#: task_office/core/models/db_models.py:131 +msgid "In process" +msgstr "В процессе" + +#: task_office/core/models/db_models.py:132 +msgid "Rejected" +msgstr "Отклонено" + +#: task_office/core/models/db_models.py:133 +msgid "Done" +msgstr "Сделано" + +#: task_office/core/schemas/base_schemas.py:54 +msgid "Max limit {} exceeded" +msgstr "Максимальный лимит {} превышен" + +#: task_office/tasks/schemas/basic_schemas.py:130 +#: task_office/tasks/schemas/basic_schemas.py:132 +#: task_office/tasks/schemas/basic_schemas.py:180 +msgid "ascending" +msgstr "Увеличение" + +#: task_office/tasks/schemas/basic_schemas.py:131 +#: task_office/tasks/schemas/basic_schemas.py:133 +#: task_office/tasks/schemas/basic_schemas.py:181 +msgid "descending" +msgstr "Уменьшение" + diff --git a/translations/uk/LC_MESSAGES/messages.po b/translations/uk/LC_MESSAGES/messages.po index 132cc0e..fc01dce 100644 --- a/translations/uk/LC_MESSAGES/messages.po +++ b/translations/uk/LC_MESSAGES/messages.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2020-01-04 15:41+0200\n" +"POT-Creation-Date: 2020-03-28 18:08+0200\n" "PO-Revision-Date: 2020-01-04 15:13+0200\n" "Last-Translator: FULL NAME \n" "Language: uk\n" @@ -31,19 +31,113 @@ msgstr "Фреш токен необхідно" msgid "Error loading the user {}" msgstr "Помилка завантаження користувача {}" -#: task_office/auth/jwt_error_handlers.py:62 +#: task_office/auth/jwt_error_handlers.py:63 msgid "User claims verification failed" msgstr "Верифікація даних користувача провалена" -#: task_office/auth/serializers.py:30 +#: task_office/auth/schemas.py:25 msgid "Passwords do not match" msgstr "Паролі не збігються" -#: task_office/auth/serializers.py:60 +#: task_office/auth/schemas.py:55 msgid "User not found" msgstr "Користувач не знайдений" -#: task_office/core/validators.py:10 +#: task_office/auth/utils.py:57 task_office/permissions/views.py:69 +#: task_office/permissions/views.py:99 +msgid "Not allowed" +msgstr "" + +#: task_office/columns/views.py:60 task_office/tasks/views.py:79 +msgid "Must be between {} and {}" +msgstr "" + +#: task_office/columns/views.py:71 task_office/columns/views.py:121 +#: task_office/core/validators.py:10 task_office/tasks/views.py:93 +#: task_office/tasks/views.py:163 msgid "Already exists with value {}" msgstr "Вже існує із значенням {}" +#: task_office/columns/views.py:108 task_office/tasks/views.py:142 +msgid "Must be between {} and {}." +msgstr "Повинний бути між {} і {}" + +#: task_office/core/enums.py:43 +msgid "Ascend" +msgstr "Збільшення" + +#: task_office/core/enums.py:44 +msgid "Descend" +msgstr "Зменшення" + +#: task_office/core/utils.py:23 task_office/core/utils.py:53 +#: task_office/core/utils.py:57 task_office/permissions/views.py:36 +#: task_office/permissions/views.py:58 task_office/permissions/views.py:92 +#: task_office/permissions/views.py:124 task_office/permissions/views.py:147 +msgid "Not found" +msgstr "Не знайдено" + +#: task_office/core/utils.py:31 +msgid "Already exists" +msgstr "Вже існує" + +#: task_office/core/validators.py:37 +msgid "Not found with value {}" +msgstr "Не знайдено із значенням {}" + +#: task_office/core/models/db_models.py:76 +msgid "Owner" +msgstr "Власник" + +#: task_office/core/models/db_models.py:76 +msgid "Owner of board(creator)" +msgstr "Власник дошки(творець)" + +#: task_office/core/models/db_models.py:77 +msgid "Editor" +msgstr "Редактор" + +#: task_office/core/models/db_models.py:77 +msgid "Editor of board" +msgstr "Редактор дошки" + +#: task_office/core/models/db_models.py:78 +msgid "Staff" +msgstr "Персонал" + +#: task_office/core/models/db_models.py:78 +msgid "Ordinary user" +msgstr "Звичайний користувач" + +#: task_office/core/models/db_models.py:130 +msgid "New" +msgstr "Новий" + +#: task_office/core/models/db_models.py:131 +msgid "In process" +msgstr "В процесі" + +#: task_office/core/models/db_models.py:132 +msgid "Rejected" +msgstr "Відхилено" + +#: task_office/core/models/db_models.py:133 +msgid "Done" +msgstr "Завершено" + +#: task_office/core/schemas/base_schemas.py:54 +msgid "Max limit {} exceeded" +msgstr "Максимальне обмеження {} перевищено" + +#: task_office/tasks/schemas/basic_schemas.py:130 +#: task_office/tasks/schemas/basic_schemas.py:132 +#: task_office/tasks/schemas/basic_schemas.py:180 +msgid "ascending" +msgstr "Збільшення" + +#: task_office/tasks/schemas/basic_schemas.py:131 +#: task_office/tasks/schemas/basic_schemas.py:133 +#: task_office/tasks/schemas/basic_schemas.py:181 +msgid "Зменшення" +msgstr "" + From 405955500e4eac6c6c42cb523d81532ab20ae31c Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Sat, 28 Mar 2020 18:54:48 +0200 Subject: [PATCH 33/60] updated .env.example, migrations --- .env.example | 11 ++- migrations/versions/13ec0ac4d518_.py | 68 --------------- migrations/versions/2c4e91200810_.py | 45 ---------- migrations/versions/978b8947df90_.py | 74 ---------------- migrations/versions/d99344769ddd_.py | 123 +++++++++++++++++++++++++++ requirements/base.txt | 2 +- 6 files changed, 134 insertions(+), 189 deletions(-) delete mode 100644 migrations/versions/13ec0ac4d518_.py delete mode 100644 migrations/versions/2c4e91200810_.py delete mode 100644 migrations/versions/978b8947df90_.py create mode 100644 migrations/versions/d99344769ddd_.py diff --git a/.env.example b/.env.example index afd76f4..5c3a0aa 100644 --- a/.env.example +++ b/.env.example @@ -4,9 +4,9 @@ FLASK_APP=entry.py FLASK_DEBUG=0 MODE=dev USE_DOCS=True +CORS_ORIGIN_WHITELIST=* PROJECT_NAME=Task-Office FLASK_SECRET=eBH93Dcn4_Pm0ryYDaGa3bCo-ZYvMabuUF6xPefYKe-lvTYp53IrTsjQZ12Lga2Bdfjt7cFmcIAB-RZapRmmzQ -CORS_ORIGIN_WHITELIST=http://localhost:4000,http://localhost:4001 # JWT # ------------------------------------------------------------------------------ @@ -21,3 +21,12 @@ POSTGRES_PORT=5432 POSTGRES_DB=task_office_dev POSTGRES_USER=task_office_user POSTGRES_PASSWORD=task_office_user + +# Cache +# ------------------------------------------------------------------------------ +CACHE_TYPE=redis +CACHE_REDIS_HOST=127.0.0.1 +CACHE_REDIS_PORT=6379 +CACHE_REDIS_PASSWORD=redis111 +CACHE_REDIS_DB=0 +CACHE_DEFAULT_TIMEOUT=900 \ No newline at end of file diff --git a/migrations/versions/13ec0ac4d518_.py b/migrations/versions/13ec0ac4d518_.py deleted file mode 100644 index 5718b27..0000000 --- a/migrations/versions/13ec0ac4d518_.py +++ /dev/null @@ -1,68 +0,0 @@ -"""empty message - -Revision ID: 13ec0ac4d518 -Revises: -Create Date: 2020-01-05 16:27:53.055312 - -""" -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import postgresql - -# revision identifiers, used by Alembic. -revision = "13ec0ac4d518" -down_revision = None -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.create_table( - "users", - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column("uuid", postgresql.UUID(), nullable=False), - sa.Column("meta", sa.JSON(), nullable=True), - sa.Column("created_at", sa.DateTime(), nullable=False), - sa.Column("updated_at", sa.DateTime(), nullable=False), - sa.Column("username", sa.String(length=80), nullable=False), - sa.Column("email", sa.String(length=255), nullable=False), - sa.Column("bio", sa.String(length=300), nullable=True), - sa.Column("phone", sa.String(length=300), nullable=True), - sa.Column("password", sa.Binary(), nullable=True), - sa.Column("is_active", sa.Boolean(), nullable=True), - sa.Column("is_superuser", sa.Boolean(), nullable=True), - sa.PrimaryKeyConstraint("id", "uuid"), - sa.UniqueConstraint("username"), - sa.UniqueConstraint("uuid"), - ) - op.create_index(op.f("ix_users_email"), "users", ["email"], unique=True) - op.create_table( - "boards", - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column("uuid", postgresql.UUID(), nullable=False), - sa.Column("meta", sa.JSON(), nullable=True), - sa.Column("created_at", sa.DateTime(), nullable=False), - sa.Column("updated_at", sa.DateTime(), nullable=False), - sa.Column("name", sa.String(length=80), nullable=False), - sa.Column("description", sa.String(length=255), nullable=True), - sa.Column("owner_uuid", postgresql.UUID(), nullable=False), - sa.Column("is_active", sa.Boolean(), nullable=True), - sa.ForeignKeyConstraint(["owner_uuid"], ["users.uuid"]), - sa.PrimaryKeyConstraint("id", "uuid"), - sa.UniqueConstraint("name", "owner_uuid", name="unique_name_owner_board"), - sa.UniqueConstraint("uuid"), - ) - op.create_index( - op.f("ix_boards_description"), "boards", ["description"], unique=False - ) - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.drop_index(op.f("ix_boards_description"), table_name="boards") - op.drop_table("boards") - op.drop_index(op.f("ix_users_email"), table_name="users") - op.drop_table("users") - # ### end Alembic commands ### diff --git a/migrations/versions/2c4e91200810_.py b/migrations/versions/2c4e91200810_.py deleted file mode 100644 index b0cc1fe..0000000 --- a/migrations/versions/2c4e91200810_.py +++ /dev/null @@ -1,45 +0,0 @@ -"""empty message - -Revision ID: 2c4e91200810 -Revises: 13ec0ac4d518 -Create Date: 2020-01-06 15:48:01.464419 - -""" -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import postgresql - -# revision identifiers, used by Alembic. -revision = "2c4e91200810" -down_revision = "13ec0ac4d518" -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.create_table( - "permissions", - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column("uuid", postgresql.UUID(), nullable=False), - sa.Column("meta", sa.JSON(), nullable=True), - sa.Column("created_at", sa.DateTime(), nullable=False), - sa.Column("updated_at", sa.DateTime(), nullable=False), - sa.Column("role", sa.Integer(), nullable=True), - sa.Column("user_uuid", postgresql.UUID(), nullable=False), - sa.Column("board_uuid", postgresql.UUID(), nullable=False), - sa.ForeignKeyConstraint(["board_uuid"], ["boards.uuid"]), - sa.ForeignKeyConstraint(["user_uuid"], ["users.uuid"]), - sa.PrimaryKeyConstraint("id", "uuid"), - sa.UniqueConstraint( - "board_uuid", "user_uuid", name="unique_board_owner_permission" - ), - sa.UniqueConstraint("uuid"), - ) - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.drop_table("permissions") - # ### end Alembic commands ### diff --git a/migrations/versions/978b8947df90_.py b/migrations/versions/978b8947df90_.py deleted file mode 100644 index 1a65138..0000000 --- a/migrations/versions/978b8947df90_.py +++ /dev/null @@ -1,74 +0,0 @@ -"""empty message - -Revision ID: 978b8947df90 -Revises: 2c4e91200810 -Create Date: 2020-02-01 10:55:06.021809 - -""" -from alembic import op -import sqlalchemy as sa -from sqlalchemy.dialects import postgresql - -# revision identifiers, used by Alembic. -revision = "978b8947df90" -down_revision = "2c4e91200810" -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.create_table( - "columns", - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column("uuid", postgresql.UUID(), nullable=False), - sa.Column("meta", sa.JSON(), nullable=True), - sa.Column("created_at", sa.DateTime(), nullable=False), - sa.Column("updated_at", sa.DateTime(), nullable=False), - sa.Column("name", sa.String(length=120), nullable=False), - sa.Column("position", sa.Integer(), nullable=True), - sa.Column("board_uuid", postgresql.UUID(), nullable=False), - sa.ForeignKeyConstraint(["board_uuid"], ["boards.uuid"]), - sa.PrimaryKeyConstraint("id", "uuid"), - sa.UniqueConstraint( - "board_uuid", "name", name="unique_board__board_column_name" - ), - sa.UniqueConstraint("uuid"), - ) - op.create_table( - "tasks", - sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), - sa.Column("uuid", postgresql.UUID(), nullable=False), - sa.Column("meta", sa.JSON(), nullable=True), - sa.Column("created_at", sa.DateTime(), nullable=False), - sa.Column("updated_at", sa.DateTime(), nullable=False), - sa.Column("expire_at", sa.DateTime(), nullable=True), - sa.Column("label", sa.String(length=80), nullable=True), - sa.Column("name", sa.String(length=120), nullable=False), - sa.Column("description", sa.String(length=120), nullable=False), - sa.Column("state", sa.Integer(), nullable=True), - sa.Column("position", sa.Integer(), nullable=True), - sa.Column("column_uuid", postgresql.UUID(), nullable=False), - sa.Column("creator_uuid", postgresql.UUID(), nullable=False), - sa.ForeignKeyConstraint(["column_uuid"], ["columns.uuid"]), - sa.ForeignKeyConstraint(["creator_uuid"], ["users.uuid"]), - sa.PrimaryKeyConstraint("id", "uuid"), - sa.UniqueConstraint("uuid"), - ) - op.create_table( - "users_tasks", - sa.Column("task_uuid", postgresql.UUID(), nullable=False), - sa.Column("user_uuid", postgresql.UUID(), nullable=False), - sa.ForeignKeyConstraint(["task_uuid"], ["tasks.uuid"]), - sa.ForeignKeyConstraint(["user_uuid"], ["users.uuid"]), - sa.PrimaryKeyConstraint("task_uuid", "user_uuid"), - ) - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.drop_table("users_tasks") - op.drop_table("tasks") - op.drop_table("columns") - # ### end Alembic commands ### diff --git a/migrations/versions/d99344769ddd_.py b/migrations/versions/d99344769ddd_.py new file mode 100644 index 0000000..20547a6 --- /dev/null +++ b/migrations/versions/d99344769ddd_.py @@ -0,0 +1,123 @@ +"""empty message + +Revision ID: d99344769ddd +Revises: +Create Date: 2020-03-28 18:44:56.147877 + +""" +import sqlalchemy as sa +from alembic import op +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = 'd99344769ddd' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('users', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('uuid', postgresql.UUID(), nullable=False), + sa.Column('meta', sa.JSON(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=False), + sa.Column('username', sa.String(length=80), nullable=False), + sa.Column('email', sa.String(length=255), nullable=False), + sa.Column('bio', sa.String(length=300), nullable=True), + sa.Column('phone', sa.String(length=300), nullable=True), + sa.Column('password', sa.Binary(), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=True), + sa.Column('is_superuser', sa.Boolean(), nullable=True), + sa.PrimaryKeyConstraint('id', 'uuid'), + sa.UniqueConstraint('username'), + sa.UniqueConstraint('uuid') + ) + op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True) + op.create_table('boards', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('uuid', postgresql.UUID(), nullable=False), + sa.Column('meta', sa.JSON(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=False), + sa.Column('name', sa.String(length=80), nullable=False), + sa.Column('description', sa.String(length=255), nullable=True), + sa.Column('owner_uuid', postgresql.UUID(), nullable=False), + sa.Column('is_active', sa.Boolean(), nullable=True), + sa.ForeignKeyConstraint(['owner_uuid'], ['users.uuid'], ), + sa.PrimaryKeyConstraint('id', 'uuid'), + sa.UniqueConstraint('name', 'owner_uuid', name='unique_name_owner_board'), + sa.UniqueConstraint('uuid') + ) + op.create_index(op.f('ix_boards_description'), 'boards', ['description'], unique=False) + op.create_table('columns', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('uuid', postgresql.UUID(), nullable=False), + sa.Column('meta', sa.JSON(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=False), + sa.Column('name', sa.String(length=120), nullable=False), + sa.Column('position', sa.Integer(), nullable=True), + sa.Column('board_uuid', postgresql.UUID(), nullable=False), + sa.ForeignKeyConstraint(['board_uuid'], ['boards.uuid'], ), + sa.PrimaryKeyConstraint('id', 'uuid'), + sa.UniqueConstraint('board_uuid', 'name', name='unique_board__board_column_name'), + sa.UniqueConstraint('uuid') + ) + op.create_table('permissions', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('uuid', postgresql.UUID(), nullable=False), + sa.Column('meta', sa.JSON(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=False), + sa.Column('role', sa.Integer(), nullable=True), + sa.Column('user_uuid', postgresql.UUID(), nullable=False), + sa.Column('board_uuid', postgresql.UUID(), nullable=False), + sa.ForeignKeyConstraint(['board_uuid'], ['boards.uuid'], ), + sa.ForeignKeyConstraint(['user_uuid'], ['users.uuid'], ), + sa.PrimaryKeyConstraint('id', 'uuid'), + sa.UniqueConstraint('board_uuid', 'user_uuid', name='unique_board_owner_permission'), + sa.UniqueConstraint('uuid') + ) + op.create_table('tasks', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('uuid', postgresql.UUID(), nullable=False), + sa.Column('meta', sa.JSON(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=False), + sa.Column('expire_at', sa.DateTime(), nullable=True), + sa.Column('label', sa.String(length=80), nullable=True), + sa.Column('name', sa.String(length=120), nullable=False), + sa.Column('description', sa.String(length=120), nullable=False), + sa.Column('state', sa.Integer(), nullable=True), + sa.Column('position', sa.Integer(), nullable=True), + sa.Column('column_uuid', postgresql.UUID(), nullable=False), + sa.Column('creator_uuid', postgresql.UUID(), nullable=False), + sa.ForeignKeyConstraint(['column_uuid'], ['columns.uuid'], ), + sa.ForeignKeyConstraint(['creator_uuid'], ['users.uuid'], ), + sa.PrimaryKeyConstraint('id', 'uuid'), + sa.UniqueConstraint('uuid') + ) + op.create_table('users_tasks', + sa.Column('task_uuid', postgresql.UUID(), nullable=False), + sa.Column('user_uuid', postgresql.UUID(), nullable=False), + sa.ForeignKeyConstraint(['task_uuid'], ['tasks.uuid'], ), + sa.ForeignKeyConstraint(['user_uuid'], ['users.uuid'], ), + sa.PrimaryKeyConstraint('task_uuid', 'user_uuid') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('users_tasks') + op.drop_table('tasks') + op.drop_table('permissions') + op.drop_table('columns') + op.drop_index(op.f('ix_boards_description'), table_name='boards') + op.drop_table('boards') + op.drop_index(op.f('ix_users_email'), table_name='users') + op.drop_table('users') + # ### end Alembic commands ### diff --git a/requirements/base.txt b/requirements/base.txt index f317a77..8b00a25 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -10,7 +10,7 @@ psycopg2 marshmallow marshmallow-enum==1.5.1 MarkupSafe==1.1.1 -gunicorn +gunicorn==20.0.4 uuid environs unicode_slugify From e4c27047a358d83e1dece3b545417238984b18a9 Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Sat, 28 Mar 2020 19:00:44 +0200 Subject: [PATCH 34/60] root url --- migrations/versions/d99344769ddd_.py | 196 ++++++++++++++------------- task_office/app.py | 14 +- task_office/core/__init__.py | 1 + task_office/core/views.py | 11 ++ 4 files changed, 128 insertions(+), 94 deletions(-) create mode 100644 task_office/core/views.py diff --git a/migrations/versions/d99344769ddd_.py b/migrations/versions/d99344769ddd_.py index 20547a6..df0a1ee 100644 --- a/migrations/versions/d99344769ddd_.py +++ b/migrations/versions/d99344769ddd_.py @@ -10,7 +10,7 @@ from sqlalchemy.dialects import postgresql # revision identifiers, used by Alembic. -revision = 'd99344769ddd' +revision = "d99344769ddd" down_revision = None branch_labels = None depends_on = None @@ -18,106 +18,118 @@ def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_table('users', - sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), - sa.Column('uuid', postgresql.UUID(), nullable=False), - sa.Column('meta', sa.JSON(), nullable=True), - sa.Column('created_at', sa.DateTime(), nullable=False), - sa.Column('updated_at', sa.DateTime(), nullable=False), - sa.Column('username', sa.String(length=80), nullable=False), - sa.Column('email', sa.String(length=255), nullable=False), - sa.Column('bio', sa.String(length=300), nullable=True), - sa.Column('phone', sa.String(length=300), nullable=True), - sa.Column('password', sa.Binary(), nullable=True), - sa.Column('is_active', sa.Boolean(), nullable=True), - sa.Column('is_superuser', sa.Boolean(), nullable=True), - sa.PrimaryKeyConstraint('id', 'uuid'), - sa.UniqueConstraint('username'), - sa.UniqueConstraint('uuid') + op.create_table( + "users", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("uuid", postgresql.UUID(), nullable=False), + sa.Column("meta", sa.JSON(), nullable=True), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.Column("updated_at", sa.DateTime(), nullable=False), + sa.Column("username", sa.String(length=80), nullable=False), + sa.Column("email", sa.String(length=255), nullable=False), + sa.Column("bio", sa.String(length=300), nullable=True), + sa.Column("phone", sa.String(length=300), nullable=True), + sa.Column("password", sa.Binary(), nullable=True), + sa.Column("is_active", sa.Boolean(), nullable=True), + sa.Column("is_superuser", sa.Boolean(), nullable=True), + sa.PrimaryKeyConstraint("id", "uuid"), + sa.UniqueConstraint("username"), + sa.UniqueConstraint("uuid"), ) - op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True) - op.create_table('boards', - sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), - sa.Column('uuid', postgresql.UUID(), nullable=False), - sa.Column('meta', sa.JSON(), nullable=True), - sa.Column('created_at', sa.DateTime(), nullable=False), - sa.Column('updated_at', sa.DateTime(), nullable=False), - sa.Column('name', sa.String(length=80), nullable=False), - sa.Column('description', sa.String(length=255), nullable=True), - sa.Column('owner_uuid', postgresql.UUID(), nullable=False), - sa.Column('is_active', sa.Boolean(), nullable=True), - sa.ForeignKeyConstraint(['owner_uuid'], ['users.uuid'], ), - sa.PrimaryKeyConstraint('id', 'uuid'), - sa.UniqueConstraint('name', 'owner_uuid', name='unique_name_owner_board'), - sa.UniqueConstraint('uuid') + op.create_index(op.f("ix_users_email"), "users", ["email"], unique=True) + op.create_table( + "boards", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("uuid", postgresql.UUID(), nullable=False), + sa.Column("meta", sa.JSON(), nullable=True), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.Column("updated_at", sa.DateTime(), nullable=False), + sa.Column("name", sa.String(length=80), nullable=False), + sa.Column("description", sa.String(length=255), nullable=True), + sa.Column("owner_uuid", postgresql.UUID(), nullable=False), + sa.Column("is_active", sa.Boolean(), nullable=True), + sa.ForeignKeyConstraint(["owner_uuid"], ["users.uuid"]), + sa.PrimaryKeyConstraint("id", "uuid"), + sa.UniqueConstraint("name", "owner_uuid", name="unique_name_owner_board"), + sa.UniqueConstraint("uuid"), ) - op.create_index(op.f('ix_boards_description'), 'boards', ['description'], unique=False) - op.create_table('columns', - sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), - sa.Column('uuid', postgresql.UUID(), nullable=False), - sa.Column('meta', sa.JSON(), nullable=True), - sa.Column('created_at', sa.DateTime(), nullable=False), - sa.Column('updated_at', sa.DateTime(), nullable=False), - sa.Column('name', sa.String(length=120), nullable=False), - sa.Column('position', sa.Integer(), nullable=True), - sa.Column('board_uuid', postgresql.UUID(), nullable=False), - sa.ForeignKeyConstraint(['board_uuid'], ['boards.uuid'], ), - sa.PrimaryKeyConstraint('id', 'uuid'), - sa.UniqueConstraint('board_uuid', 'name', name='unique_board__board_column_name'), - sa.UniqueConstraint('uuid') + op.create_index( + op.f("ix_boards_description"), "boards", ["description"], unique=False ) - op.create_table('permissions', - sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), - sa.Column('uuid', postgresql.UUID(), nullable=False), - sa.Column('meta', sa.JSON(), nullable=True), - sa.Column('created_at', sa.DateTime(), nullable=False), - sa.Column('updated_at', sa.DateTime(), nullable=False), - sa.Column('role', sa.Integer(), nullable=True), - sa.Column('user_uuid', postgresql.UUID(), nullable=False), - sa.Column('board_uuid', postgresql.UUID(), nullable=False), - sa.ForeignKeyConstraint(['board_uuid'], ['boards.uuid'], ), - sa.ForeignKeyConstraint(['user_uuid'], ['users.uuid'], ), - sa.PrimaryKeyConstraint('id', 'uuid'), - sa.UniqueConstraint('board_uuid', 'user_uuid', name='unique_board_owner_permission'), - sa.UniqueConstraint('uuid') + op.create_table( + "columns", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("uuid", postgresql.UUID(), nullable=False), + sa.Column("meta", sa.JSON(), nullable=True), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.Column("updated_at", sa.DateTime(), nullable=False), + sa.Column("name", sa.String(length=120), nullable=False), + sa.Column("position", sa.Integer(), nullable=True), + sa.Column("board_uuid", postgresql.UUID(), nullable=False), + sa.ForeignKeyConstraint(["board_uuid"], ["boards.uuid"]), + sa.PrimaryKeyConstraint("id", "uuid"), + sa.UniqueConstraint( + "board_uuid", "name", name="unique_board__board_column_name" + ), + sa.UniqueConstraint("uuid"), ) - op.create_table('tasks', - sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), - sa.Column('uuid', postgresql.UUID(), nullable=False), - sa.Column('meta', sa.JSON(), nullable=True), - sa.Column('created_at', sa.DateTime(), nullable=False), - sa.Column('updated_at', sa.DateTime(), nullable=False), - sa.Column('expire_at', sa.DateTime(), nullable=True), - sa.Column('label', sa.String(length=80), nullable=True), - sa.Column('name', sa.String(length=120), nullable=False), - sa.Column('description', sa.String(length=120), nullable=False), - sa.Column('state', sa.Integer(), nullable=True), - sa.Column('position', sa.Integer(), nullable=True), - sa.Column('column_uuid', postgresql.UUID(), nullable=False), - sa.Column('creator_uuid', postgresql.UUID(), nullable=False), - sa.ForeignKeyConstraint(['column_uuid'], ['columns.uuid'], ), - sa.ForeignKeyConstraint(['creator_uuid'], ['users.uuid'], ), - sa.PrimaryKeyConstraint('id', 'uuid'), - sa.UniqueConstraint('uuid') + op.create_table( + "permissions", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("uuid", postgresql.UUID(), nullable=False), + sa.Column("meta", sa.JSON(), nullable=True), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.Column("updated_at", sa.DateTime(), nullable=False), + sa.Column("role", sa.Integer(), nullable=True), + sa.Column("user_uuid", postgresql.UUID(), nullable=False), + sa.Column("board_uuid", postgresql.UUID(), nullable=False), + sa.ForeignKeyConstraint(["board_uuid"], ["boards.uuid"]), + sa.ForeignKeyConstraint(["user_uuid"], ["users.uuid"]), + sa.PrimaryKeyConstraint("id", "uuid"), + sa.UniqueConstraint( + "board_uuid", "user_uuid", name="unique_board_owner_permission" + ), + sa.UniqueConstraint("uuid"), ) - op.create_table('users_tasks', - sa.Column('task_uuid', postgresql.UUID(), nullable=False), - sa.Column('user_uuid', postgresql.UUID(), nullable=False), - sa.ForeignKeyConstraint(['task_uuid'], ['tasks.uuid'], ), - sa.ForeignKeyConstraint(['user_uuid'], ['users.uuid'], ), - sa.PrimaryKeyConstraint('task_uuid', 'user_uuid') + op.create_table( + "tasks", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("uuid", postgresql.UUID(), nullable=False), + sa.Column("meta", sa.JSON(), nullable=True), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.Column("updated_at", sa.DateTime(), nullable=False), + sa.Column("expire_at", sa.DateTime(), nullable=True), + sa.Column("label", sa.String(length=80), nullable=True), + sa.Column("name", sa.String(length=120), nullable=False), + sa.Column("description", sa.String(length=120), nullable=False), + sa.Column("state", sa.Integer(), nullable=True), + sa.Column("position", sa.Integer(), nullable=True), + sa.Column("column_uuid", postgresql.UUID(), nullable=False), + sa.Column("creator_uuid", postgresql.UUID(), nullable=False), + sa.ForeignKeyConstraint(["column_uuid"], ["columns.uuid"]), + sa.ForeignKeyConstraint(["creator_uuid"], ["users.uuid"]), + sa.PrimaryKeyConstraint("id", "uuid"), + sa.UniqueConstraint("uuid"), + ) + op.create_table( + "users_tasks", + sa.Column("task_uuid", postgresql.UUID(), nullable=False), + sa.Column("user_uuid", postgresql.UUID(), nullable=False), + sa.ForeignKeyConstraint(["task_uuid"], ["tasks.uuid"]), + sa.ForeignKeyConstraint(["user_uuid"], ["users.uuid"]), + sa.PrimaryKeyConstraint("task_uuid", "user_uuid"), ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('users_tasks') - op.drop_table('tasks') - op.drop_table('permissions') - op.drop_table('columns') - op.drop_index(op.f('ix_boards_description'), table_name='boards') - op.drop_table('boards') - op.drop_index(op.f('ix_users_email'), table_name='users') - op.drop_table('users') + op.drop_table("users_tasks") + op.drop_table("tasks") + op.drop_table("permissions") + op.drop_table("columns") + op.drop_index(op.f("ix_boards_description"), table_name="boards") + op.drop_table("boards") + op.drop_index(op.f("ix_users_email"), table_name="users") + op.drop_table("users") # ### end Alembic commands ### diff --git a/task_office/app.py b/task_office/app.py index d513656..b7fefef 100644 --- a/task_office/app.py +++ b/task_office/app.py @@ -1,8 +1,17 @@ """The app module, containing the app factory function.""" from flask import Flask -from task_office.auth.jwt_error_handlers import jwt_errors_map -from task_office import commands, auth, swagger, boards, permissions, columns, tasks +from task_office import ( + commands, + auth, + swagger, + boards, + permissions, + columns, + tasks, + core, +) +from task_office.auth.jwt_error_handlers import jwt_errors_map from task_office.exceptions import InvalidUsage from task_office.extensions import bcrypt, cache, db, migrate, cors, jwt, babel from task_office.settings import CONFIG @@ -49,6 +58,7 @@ def register_blueprints(app): app.register_blueprint(permissions.views.blueprint) app.register_blueprint(columns.views.blueprint) app.register_blueprint(tasks.views.blueprint) + app.register_blueprint(core.views.blueprint) if CONFIG.USE_DOCS: app.register_blueprint(swagger.views.blueprint_swagger, url_prefix=SWAGGER_URL) app.register_blueprint(swagger.views.blueprint) diff --git a/task_office/core/__init__.py b/task_office/core/__init__.py index e69de29..6b274ab 100644 --- a/task_office/core/__init__.py +++ b/task_office/core/__init__.py @@ -0,0 +1 @@ +from .views import * diff --git a/task_office/core/views.py b/task_office/core/views.py new file mode 100644 index 0000000..10e0a7f --- /dev/null +++ b/task_office/core/views.py @@ -0,0 +1,11 @@ +"""Core views.""" +from datetime import datetime + +from flask import Blueprint + +blueprint = Blueprint("", __name__, url_prefix="/") + + +@blueprint.route("", methods=("get",)) +def root(): + return {"datetime": datetime.utcnow()} From 944aae4faa2fd229e1e27fd2750a22a11f08026d Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Sat, 28 Mar 2020 23:39:30 +0200 Subject: [PATCH 35/60] light fixes, improvements --- README.rst | 3 +++ task_office/auth/schemas.py | 2 +- task_office/auth/utils.py | 4 ++-- task_office/boards/schemas/basic_schemas.py | 2 -- task_office/boards/views.py | 6 +++--- task_office/core/models/mixins.py | 3 +++ task_office/permissions/schemas/basic_schemas.py | 4 ++-- task_office/permissions/views.py | 4 ++-- translations/uk/LC_MESSAGES/messages.po | 8 ++++---- 9 files changed, 20 insertions(+), 16 deletions(-) diff --git a/README.rst b/README.rst index c444aa2..50d59da 100644 --- a/README.rst +++ b/README.rst @@ -28,6 +28,9 @@ database tables and perform the initial migration :: To run the web application use:: flask run --with-threads + # Test user: + username: amigo@gmaill.com + password: amigo1111 Translations commands:: diff --git a/task_office/auth/schemas.py b/task_office/auth/schemas.py index 5e4ae08..fbbd93f 100644 --- a/task_office/auth/schemas.py +++ b/task_office/auth/schemas.py @@ -34,7 +34,7 @@ class UserSignUpSchema(XSchema): required=True, validate=[Length(max=255), Unique(User, "email")] ) username = fields.Str( - required=True, validate=[Length(min=6, max=80), Unique(User, "username")] + required=True, validate=[Length(min=3, max=80), Unique(User, "username")] ) @validates_schema diff --git a/task_office/auth/utils.py b/task_office/auth/utils.py index 4345229..706b34e 100644 --- a/task_office/auth/utils.py +++ b/task_office/auth/utils.py @@ -52,8 +52,8 @@ def decorator(func): def wrapper(*args, **kwargs): board_uuid = kwargs.get("board_uuid", None) perms = _get_cached_permissions() - current_role = perms.get(board_uuid, 9999999) - if current_role >= required_role: + current_role = perms.get(board_uuid, -1) + if current_role > required_role: raise InvalidUsage(messages=[_("Not allowed")], status_code=403) return func(*args, **kwargs) diff --git a/task_office/boards/schemas/basic_schemas.py b/task_office/boards/schemas/basic_schemas.py index 8971075..698a371 100644 --- a/task_office/boards/schemas/basic_schemas.py +++ b/task_office/boards/schemas/basic_schemas.py @@ -7,7 +7,6 @@ from task_office.boards.schemas.search_schemas import SearchUserSchema from task_office.core.enums import XEnum from task_office.core.schemas.base_schemas import BaseSchema, ListSchema, XSchema -from task_office.core.schemas.nested_schemas import NestedUserDumpSchema from task_office.settings import CONFIG @@ -23,7 +22,6 @@ class Meta: class BoardDumpSchema(BaseSchema): name = fields.Str(dump_only=True) description = fields.Str(dump_only=True) - owner = fields.Nested(NestedUserDumpSchema, dump_only=True) is_active = fields.Boolean(dump_only=True) class Meta: diff --git a/task_office/boards/views.py b/task_office/boards/views.py index 186e8f8..63b17d1 100644 --- a/task_office/boards/views.py +++ b/task_office/boards/views.py @@ -36,7 +36,7 @@ def create_boards(**kwargs): # validate current user user = get_current_user() - user_uuid = str(user.uuid) if user else None + user_uuid = user.hexed_uuid() if user else None data["owner_uuid"] = user_uuid # Check name, owner_uuid are unique for board @@ -47,8 +47,8 @@ def create_boards(**kwargs): # Create permission for creator Permission( - user_uuid=str(data["owner_uuid"]), - board_uuid=str(board.uuid), + user_uuid=data["owner_uuid"], + board_uuid=board.hexed_uuid(), role=Permission.Role.OWNER.value, ).save() diff --git a/task_office/core/models/mixins.py b/task_office/core/models/mixins.py index f7b51b0..ac61ee9 100644 --- a/task_office/core/models/mixins.py +++ b/task_office/core/models/mixins.py @@ -32,6 +32,9 @@ def get_by_id(cls, record_id): ): return cls.query.filter_by(id=record_id).first() + def hexed_uuid(self): + return uuid.UUID(str(self.uuid)).hex + class DTMixin(object): """ diff --git a/task_office/permissions/schemas/basic_schemas.py b/task_office/permissions/schemas/basic_schemas.py index 3bb5d7c..02b79e7 100644 --- a/task_office/permissions/schemas/basic_schemas.py +++ b/task_office/permissions/schemas/basic_schemas.py @@ -45,8 +45,8 @@ class Meta: permission_query_schema = PermissionQuerySchema() permission_dump_schema = PermissionDumpSchema() permission_list_dump_schema = PermissionDumpSchema(many=True) -CONFIG.API_SPEC.components.schema("PermissionInSchema", schema=PermissionQuerySchema) -CONFIG.API_SPEC.components.schema("PermissionOutSchema", schema=PermissionDumpSchema) +CONFIG.API_SPEC.components.schema("PermissionQuerySchema", schema=PermissionQuerySchema) +CONFIG.API_SPEC.components.schema("PermissionDumpSchema", schema=PermissionDumpSchema) class PermissionListQuerySchema(ListSchema): diff --git a/task_office/permissions/views.py b/task_office/permissions/views.py index be1c7f7..ac22f46 100644 --- a/task_office/permissions/views.py +++ b/task_office/permissions/views.py @@ -71,7 +71,7 @@ def create_permission(board_uuid, **kwargs): perm = Permission(board_uuid=board_uuid, **data) perm.save() reset_permissions(uuid.UUID(perm.user_uuid).hex) - return permission + return perm @blueprint.route("/", methods=("put",)) @@ -105,7 +105,7 @@ def update_permission(board_uuid, permission_uuid, **kwargs): perm.update(updated_at=datetime.utcnow(), **data) perm.save() reset_permissions(uuid.UUID(perm.user_uuid).hex) - return permission + return perm @blueprint.route("", methods=("get",)) diff --git a/translations/uk/LC_MESSAGES/messages.po b/translations/uk/LC_MESSAGES/messages.po index fc01dce..a63626d 100644 --- a/translations/uk/LC_MESSAGES/messages.po +++ b/translations/uk/LC_MESSAGES/messages.po @@ -46,11 +46,11 @@ msgstr "Користувач не знайдений" #: task_office/auth/utils.py:57 task_office/permissions/views.py:69 #: task_office/permissions/views.py:99 msgid "Not allowed" -msgstr "" +msgstr "Не дозволено" #: task_office/columns/views.py:60 task_office/tasks/views.py:79 msgid "Must be between {} and {}" -msgstr "" +msgstr "Повинний бути між {} і {}" #: task_office/columns/views.py:71 task_office/columns/views.py:121 #: task_office/core/validators.py:10 task_office/tasks/views.py:93 @@ -60,7 +60,7 @@ msgstr "Вже існує із значенням {}" #: task_office/columns/views.py:108 task_office/tasks/views.py:142 msgid "Must be between {} and {}." -msgstr "Повинний бути між {} і {}" +msgstr "Повинний бути між {} і {}." #: task_office/core/enums.py:43 msgid "Ascend" @@ -139,5 +139,5 @@ msgstr "Збільшення" #: task_office/tasks/schemas/basic_schemas.py:133 #: task_office/tasks/schemas/basic_schemas.py:181 msgid "Зменшення" -msgstr "" +msgstr "Зменшення" From 9e97a8390b1e26365bab59500e711aa916d2f936 Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Sun, 5 Apr 2020 21:46:26 +0300 Subject: [PATCH 36/60] run with docker, light refactor, updated readme --- .deploy/__init__.py | 0 .deploy/local/__init__.py | 0 .deploy/local/api/Dockerfile | 11 + .deploy/local/api/__init__.py | 0 .deploy/local/nginx/Dockerfile | 4 + .deploy/local/nginx/__init__.py | 0 .deploy/local/nginx/default.conf | 17 + .deploy/local/postgres/__init__.py | 0 .deploy/local/postgres/init.sql | 626 +++++++++++++++++++++++++++++ .env.example | 6 +- README.rst | 37 +- docker-compose.yml | 58 +++ requirements/base.txt | 2 +- requirements/dev.txt | 4 +- entry.py => wsgi.py | 1 - 15 files changed, 744 insertions(+), 22 deletions(-) create mode 100644 .deploy/__init__.py create mode 100644 .deploy/local/__init__.py create mode 100644 .deploy/local/api/Dockerfile create mode 100644 .deploy/local/api/__init__.py create mode 100644 .deploy/local/nginx/Dockerfile create mode 100644 .deploy/local/nginx/__init__.py create mode 100644 .deploy/local/nginx/default.conf create mode 100644 .deploy/local/postgres/__init__.py create mode 100755 .deploy/local/postgres/init.sql create mode 100644 docker-compose.yml rename entry.py => wsgi.py (85%) diff --git a/.deploy/__init__.py b/.deploy/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/.deploy/local/__init__.py b/.deploy/local/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/.deploy/local/api/Dockerfile b/.deploy/local/api/Dockerfile new file mode 100644 index 0000000..008a4dd --- /dev/null +++ b/.deploy/local/api/Dockerfile @@ -0,0 +1,11 @@ +FROM python:3.7 +ENV PYTHONUNBUFFERED 1 +RUN mkdir /app +WORKDIR /app +COPY . /app/ +RUN pip install --upgrade pip +RUN if [ ${MODE} = prod ]; \ + then pip install -r requirements/prod.txt; \ + else pip install -r requirements/dev.txt; \ + fi + diff --git a/.deploy/local/api/__init__.py b/.deploy/local/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/.deploy/local/nginx/Dockerfile b/.deploy/local/nginx/Dockerfile new file mode 100644 index 0000000..99b07a9 --- /dev/null +++ b/.deploy/local/nginx/Dockerfile @@ -0,0 +1,4 @@ +FROM nginx:1.17-alpine + +RUN rm /etc/nginx/conf.d/default.conf +COPY default.conf /etc/nginx/conf.d \ No newline at end of file diff --git a/.deploy/local/nginx/__init__.py b/.deploy/local/nginx/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/.deploy/local/nginx/default.conf b/.deploy/local/nginx/default.conf new file mode 100644 index 0000000..57e5f4f --- /dev/null +++ b/.deploy/local/nginx/default.conf @@ -0,0 +1,17 @@ +upstream localhost { + server web_service:8000; +} + +server { + listen 8000; + server_name localhost; + + location /static/ { + autoindex on; + alias /app/static/; + } + + location / { + proxy_pass http://localhost; + } +} diff --git a/.deploy/local/postgres/__init__.py b/.deploy/local/postgres/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/.deploy/local/postgres/init.sql b/.deploy/local/postgres/init.sql new file mode 100755 index 0000000..93c3d06 --- /dev/null +++ b/.deploy/local/postgres/init.sql @@ -0,0 +1,626 @@ +-- +-- PostgreSQL database dump +-- + +-- Dumped from database version 10.9 (Ubuntu 10.9-0ubuntu0.18.04.1) +-- Dumped by pg_dump version 10.9 (Ubuntu 10.9-0ubuntu0.18.04.1) + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +-- +-- Name: plpgsql; Type: EXTENSION; Schema: -; Owner: +-- + +CREATE EXTENSION IF NOT EXISTS plpgsql WITH SCHEMA pg_catalog; + + +-- +-- Name: EXTENSION plpgsql; Type: COMMENT; Schema: -; Owner: +-- + +COMMENT ON EXTENSION plpgsql IS 'PL/pgSQL procedural language'; + + +SET default_tablespace = ''; + +SET default_with_oids = false; + +-- +-- Name: alembic_version; Type: TABLE; Schema: public; Owner: task_office_user +-- + +CREATE TABLE public.alembic_version ( + version_num character varying(32) NOT NULL +); + + +ALTER TABLE public.alembic_version OWNER TO task_office_user; + +-- +-- Name: boards; Type: TABLE; Schema: public; Owner: task_office_user +-- + +CREATE TABLE public.boards ( + id integer NOT NULL, + uuid uuid NOT NULL, + meta json, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL, + name character varying(80) NOT NULL, + description character varying(255), + owner_uuid uuid NOT NULL, + is_active boolean +); + + +ALTER TABLE public.boards OWNER TO task_office_user; + +-- +-- Name: boards_id_seq; Type: SEQUENCE; Schema: public; Owner: task_office_user +-- + +CREATE SEQUENCE public.boards_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.boards_id_seq OWNER TO task_office_user; + +-- +-- Name: boards_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: task_office_user +-- + +ALTER SEQUENCE public.boards_id_seq OWNED BY public.boards.id; + + +-- +-- Name: columns; Type: TABLE; Schema: public; Owner: task_office_user +-- + +CREATE TABLE public.columns ( + id integer NOT NULL, + uuid uuid NOT NULL, + meta json, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL, + name character varying(120) NOT NULL, + "position" integer, + board_uuid uuid NOT NULL +); + + +ALTER TABLE public.columns OWNER TO task_office_user; + +-- +-- Name: columns_id_seq; Type: SEQUENCE; Schema: public; Owner: task_office_user +-- + +CREATE SEQUENCE public.columns_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.columns_id_seq OWNER TO task_office_user; + +-- +-- Name: columns_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: task_office_user +-- + +ALTER SEQUENCE public.columns_id_seq OWNED BY public.columns.id; + + +-- +-- Name: permissions; Type: TABLE; Schema: public; Owner: task_office_user +-- + +CREATE TABLE public.permissions ( + id integer NOT NULL, + uuid uuid NOT NULL, + meta json, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL, + role integer, + user_uuid uuid NOT NULL, + board_uuid uuid NOT NULL +); + + +ALTER TABLE public.permissions OWNER TO task_office_user; + +-- +-- Name: permissions_id_seq; Type: SEQUENCE; Schema: public; Owner: task_office_user +-- + +CREATE SEQUENCE public.permissions_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.permissions_id_seq OWNER TO task_office_user; + +-- +-- Name: permissions_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: task_office_user +-- + +ALTER SEQUENCE public.permissions_id_seq OWNED BY public.permissions.id; + + +-- +-- Name: tasks; Type: TABLE; Schema: public; Owner: task_office_user +-- + +CREATE TABLE public.tasks ( + id integer NOT NULL, + uuid uuid NOT NULL, + meta json, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL, + expire_at timestamp without time zone, + label character varying(80), + name character varying(120) NOT NULL, + description character varying(120) NOT NULL, + state integer, + "position" integer, + column_uuid uuid NOT NULL, + creator_uuid uuid NOT NULL +); + + +ALTER TABLE public.tasks OWNER TO task_office_user; + +-- +-- Name: tasks_id_seq; Type: SEQUENCE; Schema: public; Owner: task_office_user +-- + +CREATE SEQUENCE public.tasks_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.tasks_id_seq OWNER TO task_office_user; + +-- +-- Name: tasks_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: task_office_user +-- + +ALTER SEQUENCE public.tasks_id_seq OWNED BY public.tasks.id; + + +-- +-- Name: users; Type: TABLE; Schema: public; Owner: task_office_user +-- + +CREATE TABLE public.users ( + id integer NOT NULL, + uuid uuid NOT NULL, + meta json, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL, + username character varying(80) NOT NULL, + email character varying(255) NOT NULL, + bio character varying(300), + phone character varying(300), + password bytea, + is_active boolean, + is_superuser boolean +); + + +ALTER TABLE public.users OWNER TO task_office_user; + +-- +-- Name: users_id_seq; Type: SEQUENCE; Schema: public; Owner: task_office_user +-- + +CREATE SEQUENCE public.users_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER TABLE public.users_id_seq OWNER TO task_office_user; + +-- +-- Name: users_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: task_office_user +-- + +ALTER SEQUENCE public.users_id_seq OWNED BY public.users.id; + + +-- +-- Name: users_tasks; Type: TABLE; Schema: public; Owner: task_office_user +-- + +CREATE TABLE public.users_tasks ( + task_uuid uuid NOT NULL, + user_uuid uuid NOT NULL +); + + +ALTER TABLE public.users_tasks OWNER TO task_office_user; + +-- +-- Name: boards id; Type: DEFAULT; Schema: public; Owner: task_office_user +-- + +ALTER TABLE ONLY public.boards ALTER COLUMN id SET DEFAULT nextval('public.boards_id_seq'::regclass); + + +-- +-- Name: columns id; Type: DEFAULT; Schema: public; Owner: task_office_user +-- + +ALTER TABLE ONLY public.columns ALTER COLUMN id SET DEFAULT nextval('public.columns_id_seq'::regclass); + + +-- +-- Name: permissions id; Type: DEFAULT; Schema: public; Owner: task_office_user +-- + +ALTER TABLE ONLY public.permissions ALTER COLUMN id SET DEFAULT nextval('public.permissions_id_seq'::regclass); + + +-- +-- Name: tasks id; Type: DEFAULT; Schema: public; Owner: task_office_user +-- + +ALTER TABLE ONLY public.tasks ALTER COLUMN id SET DEFAULT nextval('public.tasks_id_seq'::regclass); + + +-- +-- Name: users id; Type: DEFAULT; Schema: public; Owner: task_office_user +-- + +ALTER TABLE ONLY public.users ALTER COLUMN id SET DEFAULT nextval('public.users_id_seq'::regclass); + + +-- +-- Data for Name: alembic_version; Type: TABLE DATA; Schema: public; Owner: task_office_user +-- + +COPY public.alembic_version (version_num) FROM stdin; +d99344769ddd +\. + + +-- +-- Data for Name: boards; Type: TABLE DATA; Schema: public; Owner: task_office_user +-- + +COPY public.boards (id, uuid, meta, created_at, updated_at, name, description, owner_uuid, is_active) FROM stdin; +1 74cb6568-2c5c-437b-a8e5-a226443f220c {} 2020-04-04 19:37:13.167729 2020-04-04 19:37:13.167734 Board #1 name Board #1 description 2ce70298-e3d9-47aa-a58e-ce55d9c351fd t +2 f46f0533-f94b-40ab-aa4d-286bb433d79d {} 2020-04-04 19:37:24.722797 2020-04-04 19:37:24.722803 Board #2 name Board #2 description 2ce70298-e3d9-47aa-a58e-ce55d9c351fd t +3 3dd6f7b8-d72a-4da8-a94f-5f665577c2bb {} 2020-04-04 19:37:33.883561 2020-04-04 19:37:33.883567 Board #3 name Board #3 description 2ce70298-e3d9-47aa-a58e-ce55d9c351fd t +\. + + +-- +-- Data for Name: columns; Type: TABLE DATA; Schema: public; Owner: task_office_user +-- + +COPY public.columns (id, uuid, meta, created_at, updated_at, name, "position", board_uuid) FROM stdin; +4 4223251e-6a4d-4be5-a4db-632381dcc18e {} 2020-04-04 19:42:45.543069 2020-04-04 19:42:45.543076 High priority 1 74cb6568-2c5c-437b-a8e5-a226443f220c +1 09a57486-cb0f-4981-bbfe-880161810aee {} 2020-04-04 19:40:47.659333 2020-04-04 19:42:45.553613 Done 4 74cb6568-2c5c-437b-a8e5-a226443f220c +3 e55ff02a-ca1b-4f17-998c-b606e5c295b4 {} 2020-04-04 19:42:35.497317 2020-04-04 19:42:45.553613 Low priority 2 74cb6568-2c5c-437b-a8e5-a226443f220c +2 a793e93e-ed03-47f5-995c-cf13dada93c0 {} 2020-04-04 19:41:18.115626 2020-04-04 19:42:45.553613 Rejected tasks 3 74cb6568-2c5c-437b-a8e5-a226443f220c +6 74a9c56e-c44b-4d1d-bb9c-378c3e580d2c {} 2020-04-04 19:47:28.867338 2020-04-04 19:47:28.867344 Column name2 1 f46f0533-f94b-40ab-aa4d-286bb433d79d +5 e806769c-746a-40d2-8666-f970acac1455 {} 2020-04-04 19:47:23.347919 2020-04-04 19:47:28.874041 Column name1 2 f46f0533-f94b-40ab-aa4d-286bb433d79d +\. + + +-- +-- Data for Name: permissions; Type: TABLE DATA; Schema: public; Owner: task_office_user +-- + +COPY public.permissions (id, uuid, meta, created_at, updated_at, role, user_uuid, board_uuid) FROM stdin; +1 5220680b-33d4-4973-b35a-576a98ef5a88 {} 2020-04-04 19:37:13.177124 2020-04-04 19:37:13.17713 1 2ce70298-e3d9-47aa-a58e-ce55d9c351fd 74cb6568-2c5c-437b-a8e5-a226443f220c +2 46ee1a2b-4500-4c74-a7a7-2a4ec4884778 {} 2020-04-04 19:37:24.72541 2020-04-04 19:37:24.725415 1 2ce70298-e3d9-47aa-a58e-ce55d9c351fd f46f0533-f94b-40ab-aa4d-286bb433d79d +3 bdc8b9fc-ecca-4101-8baf-8682883b8eb4 {} 2020-04-04 19:37:33.886127 2020-04-04 19:37:33.886132 1 2ce70298-e3d9-47aa-a58e-ce55d9c351fd 3dd6f7b8-d72a-4da8-a94f-5f665577c2bb +\. + + +-- +-- Data for Name: tasks; Type: TABLE DATA; Schema: public; Owner: task_office_user +-- + +COPY public.tasks (id, uuid, meta, created_at, updated_at, expire_at, label, name, description, state, "position", column_uuid, creator_uuid) FROM stdin; +2 61fdc381-9c8b-46b7-9e98-dd04ecbf1103 {} 2020-04-04 19:46:03.883278 2020-04-04 19:46:03.883284 2020-05-25 05:30:11 Label1 Task #2 name Task #2 description 1 1 4223251e-6a4d-4be5-a4db-632381dcc18e 2ce70298-e3d9-47aa-a58e-ce55d9c351fd +1 89771883-849f-4aef-930c-285f0131ae56 {} 2020-04-04 19:44:47.12337 2020-04-04 19:46:03.887337 2020-05-25 05:30:11 Label1 Task #1 name Task #1 description 1 2 4223251e-6a4d-4be5-a4db-632381dcc18e 2ce70298-e3d9-47aa-a58e-ce55d9c351fd +3 a4a9594b-7b12-4b9c-b892-d48d666f711c {} 2020-04-04 19:49:44.812372 2020-04-04 19:49:44.812383 2021-05-25 05:30:11 Label1 Task ##2 name Task ##2 description 1 1 74a9c56e-c44b-4d1d-bb9c-378c3e580d2c 2ce70298-e3d9-47aa-a58e-ce55d9c351fd +\. + + +-- +-- Data for Name: users; Type: TABLE DATA; Schema: public; Owner: task_office_user +-- + +COPY public.users (id, uuid, meta, created_at, updated_at, username, email, bio, phone, password, is_active, is_superuser) FROM stdin; +1 2ce70298-e3d9-47aa-a58e-ce55d9c351fd {} 2020-04-04 19:35:33.864782 2020-04-04 19:35:33.86479 Amigo amigo@gmaill.com \N \N \\x243262243132246741684f36616f4a744d7350364d745737426d43637557616477596d3837617578447a31354e7a4c36653856356953595961683853 t f +2 df362410-c706-4ad8-9009-98eee9ff4a82 {} 2020-04-04 19:36:14.527745 2020-04-04 19:36:14.527751 Sergio sergio@gmaill.com \N \N \\x2432622431322443423036463073594363497750682e7354323641766542664852414f6e58594a4d623837326a594b305364374151315576456b4c6d t f +\. + + +-- +-- Data for Name: users_tasks; Type: TABLE DATA; Schema: public; Owner: task_office_user +-- + +COPY public.users_tasks (task_uuid, user_uuid) FROM stdin; +61fdc381-9c8b-46b7-9e98-dd04ecbf1103 df362410-c706-4ad8-9009-98eee9ff4a82 +a4a9594b-7b12-4b9c-b892-d48d666f711c 2ce70298-e3d9-47aa-a58e-ce55d9c351fd +a4a9594b-7b12-4b9c-b892-d48d666f711c df362410-c706-4ad8-9009-98eee9ff4a82 +\. + + +-- +-- Name: boards_id_seq; Type: SEQUENCE SET; Schema: public; Owner: task_office_user +-- + +SELECT pg_catalog.setval('public.boards_id_seq', 3, true); + + +-- +-- Name: columns_id_seq; Type: SEQUENCE SET; Schema: public; Owner: task_office_user +-- + +SELECT pg_catalog.setval('public.columns_id_seq', 6, true); + + +-- +-- Name: permissions_id_seq; Type: SEQUENCE SET; Schema: public; Owner: task_office_user +-- + +SELECT pg_catalog.setval('public.permissions_id_seq', 3, true); + + +-- +-- Name: tasks_id_seq; Type: SEQUENCE SET; Schema: public; Owner: task_office_user +-- + +SELECT pg_catalog.setval('public.tasks_id_seq', 3, true); + + +-- +-- Name: users_id_seq; Type: SEQUENCE SET; Schema: public; Owner: task_office_user +-- + +SELECT pg_catalog.setval('public.users_id_seq', 2, true); + + +-- +-- Name: alembic_version alembic_version_pkc; Type: CONSTRAINT; Schema: public; Owner: task_office_user +-- + +ALTER TABLE ONLY public.alembic_version + ADD CONSTRAINT alembic_version_pkc PRIMARY KEY (version_num); + + +-- +-- Name: boards boards_pkey; Type: CONSTRAINT; Schema: public; Owner: task_office_user +-- + +ALTER TABLE ONLY public.boards + ADD CONSTRAINT boards_pkey PRIMARY KEY (id, uuid); + + +-- +-- Name: boards boards_uuid_key; Type: CONSTRAINT; Schema: public; Owner: task_office_user +-- + +ALTER TABLE ONLY public.boards + ADD CONSTRAINT boards_uuid_key UNIQUE (uuid); + + +-- +-- Name: columns columns_pkey; Type: CONSTRAINT; Schema: public; Owner: task_office_user +-- + +ALTER TABLE ONLY public.columns + ADD CONSTRAINT columns_pkey PRIMARY KEY (id, uuid); + + +-- +-- Name: columns columns_uuid_key; Type: CONSTRAINT; Schema: public; Owner: task_office_user +-- + +ALTER TABLE ONLY public.columns + ADD CONSTRAINT columns_uuid_key UNIQUE (uuid); + + +-- +-- Name: permissions permissions_pkey; Type: CONSTRAINT; Schema: public; Owner: task_office_user +-- + +ALTER TABLE ONLY public.permissions + ADD CONSTRAINT permissions_pkey PRIMARY KEY (id, uuid); + + +-- +-- Name: permissions permissions_uuid_key; Type: CONSTRAINT; Schema: public; Owner: task_office_user +-- + +ALTER TABLE ONLY public.permissions + ADD CONSTRAINT permissions_uuid_key UNIQUE (uuid); + + +-- +-- Name: tasks tasks_pkey; Type: CONSTRAINT; Schema: public; Owner: task_office_user +-- + +ALTER TABLE ONLY public.tasks + ADD CONSTRAINT tasks_pkey PRIMARY KEY (id, uuid); + + +-- +-- Name: tasks tasks_uuid_key; Type: CONSTRAINT; Schema: public; Owner: task_office_user +-- + +ALTER TABLE ONLY public.tasks + ADD CONSTRAINT tasks_uuid_key UNIQUE (uuid); + + +-- +-- Name: columns unique_board__board_column_name; Type: CONSTRAINT; Schema: public; Owner: task_office_user +-- + +ALTER TABLE ONLY public.columns + ADD CONSTRAINT unique_board__board_column_name UNIQUE (board_uuid, name); + + +-- +-- Name: permissions unique_board_owner_permission; Type: CONSTRAINT; Schema: public; Owner: task_office_user +-- + +ALTER TABLE ONLY public.permissions + ADD CONSTRAINT unique_board_owner_permission UNIQUE (board_uuid, user_uuid); + + +-- +-- Name: boards unique_name_owner_board; Type: CONSTRAINT; Schema: public; Owner: task_office_user +-- + +ALTER TABLE ONLY public.boards + ADD CONSTRAINT unique_name_owner_board UNIQUE (name, owner_uuid); + + +-- +-- Name: users users_pkey; Type: CONSTRAINT; Schema: public; Owner: task_office_user +-- + +ALTER TABLE ONLY public.users + ADD CONSTRAINT users_pkey PRIMARY KEY (id, uuid); + + +-- +-- Name: users_tasks users_tasks_pkey; Type: CONSTRAINT; Schema: public; Owner: task_office_user +-- + +ALTER TABLE ONLY public.users_tasks + ADD CONSTRAINT users_tasks_pkey PRIMARY KEY (task_uuid, user_uuid); + + +-- +-- Name: users users_username_key; Type: CONSTRAINT; Schema: public; Owner: task_office_user +-- + +ALTER TABLE ONLY public.users + ADD CONSTRAINT users_username_key UNIQUE (username); + + +-- +-- Name: users users_uuid_key; Type: CONSTRAINT; Schema: public; Owner: task_office_user +-- + +ALTER TABLE ONLY public.users + ADD CONSTRAINT users_uuid_key UNIQUE (uuid); + + +-- +-- Name: ix_boards_description; Type: INDEX; Schema: public; Owner: task_office_user +-- + +CREATE INDEX ix_boards_description ON public.boards USING btree (description); + + +-- +-- Name: ix_users_email; Type: INDEX; Schema: public; Owner: task_office_user +-- + +CREATE UNIQUE INDEX ix_users_email ON public.users USING btree (email); + + +-- +-- Name: boards boards_owner_uuid_fkey; Type: FK CONSTRAINT; Schema: public; Owner: task_office_user +-- + +ALTER TABLE ONLY public.boards + ADD CONSTRAINT boards_owner_uuid_fkey FOREIGN KEY (owner_uuid) REFERENCES public.users(uuid); + + +-- +-- Name: columns columns_board_uuid_fkey; Type: FK CONSTRAINT; Schema: public; Owner: task_office_user +-- + +ALTER TABLE ONLY public.columns + ADD CONSTRAINT columns_board_uuid_fkey FOREIGN KEY (board_uuid) REFERENCES public.boards(uuid); + + +-- +-- Name: permissions permissions_board_uuid_fkey; Type: FK CONSTRAINT; Schema: public; Owner: task_office_user +-- + +ALTER TABLE ONLY public.permissions + ADD CONSTRAINT permissions_board_uuid_fkey FOREIGN KEY (board_uuid) REFERENCES public.boards(uuid); + + +-- +-- Name: permissions permissions_user_uuid_fkey; Type: FK CONSTRAINT; Schema: public; Owner: task_office_user +-- + +ALTER TABLE ONLY public.permissions + ADD CONSTRAINT permissions_user_uuid_fkey FOREIGN KEY (user_uuid) REFERENCES public.users(uuid); + + +-- +-- Name: tasks tasks_column_uuid_fkey; Type: FK CONSTRAINT; Schema: public; Owner: task_office_user +-- + +ALTER TABLE ONLY public.tasks + ADD CONSTRAINT tasks_column_uuid_fkey FOREIGN KEY (column_uuid) REFERENCES public.columns(uuid); + + +-- +-- Name: tasks tasks_creator_uuid_fkey; Type: FK CONSTRAINT; Schema: public; Owner: task_office_user +-- + +ALTER TABLE ONLY public.tasks + ADD CONSTRAINT tasks_creator_uuid_fkey FOREIGN KEY (creator_uuid) REFERENCES public.users(uuid); + + +-- +-- Name: users_tasks users_tasks_task_uuid_fkey; Type: FK CONSTRAINT; Schema: public; Owner: task_office_user +-- + +ALTER TABLE ONLY public.users_tasks + ADD CONSTRAINT users_tasks_task_uuid_fkey FOREIGN KEY (task_uuid) REFERENCES public.tasks(uuid); + + +-- +-- Name: users_tasks users_tasks_user_uuid_fkey; Type: FK CONSTRAINT; Schema: public; Owner: task_office_user +-- + +ALTER TABLE ONLY public.users_tasks + ADD CONSTRAINT users_tasks_user_uuid_fkey FOREIGN KEY (user_uuid) REFERENCES public.users(uuid); + + +-- +-- PostgreSQL database dump complete +-- + diff --git a/.env.example b/.env.example index 5c3a0aa..204308b 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,6 @@ # General # ------------------------------------------------------------------------------ -FLASK_APP=entry.py +FLASK_APP=wsgi.py FLASK_DEBUG=0 MODE=dev USE_DOCS=True @@ -16,7 +16,7 @@ JWT_REFRESH_TOKEN_EXPIRES=7 # DB # ------------------------------------------------------------------------------ -POSTGRES_HOST=127.0.0.1 +POSTGRES_HOST=postgres_service POSTGRES_PORT=5432 POSTGRES_DB=task_office_dev POSTGRES_USER=task_office_user @@ -25,7 +25,7 @@ POSTGRES_PASSWORD=task_office_user # Cache # ------------------------------------------------------------------------------ CACHE_TYPE=redis -CACHE_REDIS_HOST=127.0.0.1 +CACHE_REDIS_HOST=redis_service CACHE_REDIS_PORT=6379 CACHE_REDIS_PASSWORD=redis111 CACHE_REDIS_DB=0 diff --git a/README.rst b/README.rst index 50d59da..21adb11 100644 --- a/README.rst +++ b/README.rst @@ -11,28 +11,37 @@ Task Office - pet app with using Flask Run Task Office ^^^^^^^^^^^^^^^^^^ -Before running shell commands, set the ``FLASK_APP`` and ``FLASK_DEBUG`` -environment variables :: - export FLASK_APP=/entry.py - export FLASK_DEBUG=1 +To run the app use:: + $ cd + $ cp .env.example .env -Run the following commands to create your app's -database tables and perform the initial migration :: + # run with flask wsgi: + # install, run postgres, redis, actualize .env + $ flask run --with-threads + # type http://127.0.0.1:5000/ in browser - flask db init - flask db migrate - flask db upgrade + # run with docker-compose: + # install Docker, Docker Compose + # https://docs.docker.com/v17.12/install/ + # https://docs.docker.com/compose/install/ + $ docker-compose up --build + # type http://localhost/ in browser -To run the web application use:: +Test user credentials:: - flask run --with-threads - # Test user: username: amigo@gmaill.com password: amigo1111 +Run the following commands to create your app's +database tables and perform the initial migration :: + + flask db init + flask db migrate + flask db upgrade + Translations commands:: https://pythonhosted.org/Flask-Babel/ @@ -52,7 +61,3 @@ Translations commands:: pybabel compile -d translations -Features --------- - -* TODO diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..cbb41ba --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,58 @@ +version: '3' + +services: + nginx_service: + image: nginx + container_name: nginx_1 + ports: + - 80:8000 + volumes: + - ./:/app + - .deploy/local/nginx/default.conf:/etc/nginx/conf.d/default.conf + - static_volume:/app/static + - templates_volume:/app/templates + expose: + - 80 + depends_on: + - web_service + + web_service: + container_name: web_1 + restart: always + env_file: + - .env + build: + context: ./ + dockerfile: .deploy/local/api/Dockerfile + command: > + bash -c "flask db upgrade && chmod 755 -R /app/static && gunicorn -w 3 -b 0.0.0.0:8000 wsgi:app" + volumes: + - ./:/app + - static_volume:/app/static + - templates_volume:/app/templates + expose: + - 8000 + depends_on: + - postgres_service + - redis_service + + postgres_service: + container_name: postgres_1 + image: postgres + env_file: + - .env + volumes: + - postgres_volume:/var/lib/postgresql/data/ + - .deploy/local/postgres/init.sql:/docker-entrypoint-initdb.d/init.sql + + redis_service: + container_name: redis_1 + image: redis + env_file: + - .env + +volumes: + postgres_volume: + driver: local + static_volume: + templates_volume: diff --git a/requirements/base.txt b/requirements/base.txt index 8b00a25..de557a8 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -6,7 +6,7 @@ Flask-JWT-Extended Flask-Cors Flask_SQLAlchemy==2.2 SQLAlchemy==1.1.9 -psycopg2 +psycopg2-binary marshmallow marshmallow-enum==1.5.1 MarkupSafe==1.1.1 diff --git a/requirements/dev.txt b/requirements/dev.txt index 12847d6..7c31ffa 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -2,4 +2,6 @@ flask-swagger-ui==3.20.9 apispec>=1.0.0 -apispec-webframeworks \ No newline at end of file +apispec-webframeworks +flask_apispec +pyyaml \ No newline at end of file diff --git a/entry.py b/wsgi.py similarity index 85% rename from entry.py rename to wsgi.py index 7b268b2..7676bc3 100755 --- a/entry.py +++ b/wsgi.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """Create an application instance.""" from task_office.app import create_app From 868b6900b5e4342ef20dbd76d748d3734ab96f5f Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Sun, 5 Apr 2020 21:52:38 +0300 Subject: [PATCH 37/60] updated .gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 8dd3ca0..5a904b9 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,7 @@ # C extensions *.so -.idea/* +.idea* # Local development ignores venv data.sqlite From 8154b01eaa9e987e2fc9ccd264ecfa69b017d387 Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Sun, 5 Apr 2020 22:43:25 +0300 Subject: [PATCH 38/60] light remarks --- .env.example | 1 + TODO.rst | 4 +++- task_office/settings.py | 5 ++--- task_office/tasks/views.py | 1 - 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.env.example b/.env.example index 204308b..1168e3a 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,7 @@ # General # ------------------------------------------------------------------------------ FLASK_APP=wsgi.py +FLASK_READ_DOT_ENV_FILE=True FLASK_DEBUG=0 MODE=dev USE_DOCS=True diff --git a/TODO.rst b/TODO.rst index b8a30a8..897d33b 100644 --- a/TODO.rst +++ b/TODO.rst @@ -5,7 +5,9 @@ TODO List: Issues General ^^^^^^^^^^^^^^ * Try to handle all errors and wrap them by our custom error class(InvalidUsage), with using translations -* Implement auto-generated api docs with swagger ui +* Implement auto-generated api +* Improve pagination +* Configure pytest Issues By Features ^^^^^^^^^^^^^^^^^^ diff --git a/task_office/settings.py b/task_office/settings.py index 21ce00d..f9b1618 100644 --- a/task_office/settings.py +++ b/task_office/settings.py @@ -18,7 +18,7 @@ class Config(object): PROJECT_ROOT = os.path.abspath(os.path.join(APP_DIR, os.pardir)) # Environment variables setting - READ_DOT_ENV_FILE = env.bool("FLASK_READ_DOT_ENV_FILE", default=True) + READ_DOT_ENV_FILE = env.bool("FLASK_READ_DOT_ENV_FILE", default=False) if READ_DOT_ENV_FILE: # OS environment variables take precedence over variables from .env env.read_env(os.path.join(PROJECT_ROOT, ".env")) @@ -63,6 +63,7 @@ class Config(object): JWT_REFRESH_TOKEN_EXPIRES = timedelta(days=env.int("JWT_REFRESH_TOKEN_EXPIRES", 7)) JWT_BLACKLIST_TOKEN_CHECKS = ["access", "refresh"] + # Pagination DEFAULT_OFFSET_VALUE = 0 DEFAULT_LIMIT_VALUE = 15 MAX_LIMIT_VALUE = 50 @@ -110,8 +111,6 @@ class ProdConfig(Config): class DevConfig(Config): """Development configuration.""" - CACHE_TYPE = "simple" # Can be "memcached", "redis", etc. - MODE = os.environ.get("MODE", default="dev") diff --git a/task_office/tasks/views.py b/task_office/tasks/views.py index 2d01758..0d94fbc 100644 --- a/task_office/tasks/views.py +++ b/task_office/tasks/views.py @@ -166,7 +166,6 @@ def update_task(board_uuid, task_uuid, **kwargs): ) if data: - # getting performers before task save if data.get("performers", None): performers = User.query.filter(User.uuid.in_(data["performers"])).all() From 20bd01815de506f7d643200c8e4ec14ec4bb8a3e Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Mon, 6 Apr 2020 08:53:14 +0300 Subject: [PATCH 39/60] pagination improved, updated translations --- task_office/boards/views.py | 1 - task_office/core/helpers/listed_response.py | 31 ++++- task_office/core/schemas/base_schemas.py | 25 ++-- translations/en/LC_MESSAGES/messages.po | 13 +- translations/messages.pot | 141 -------------------- translations/ru/LC_MESSAGES/messages.po | 20 ++- translations/uk/LC_MESSAGES/messages.po | 15 ++- 7 files changed, 68 insertions(+), 178 deletions(-) delete mode 100644 translations/messages.pot diff --git a/task_office/boards/views.py b/task_office/boards/views.py index 63b17d1..c4848c9 100644 --- a/task_office/boards/views.py +++ b/task_office/boards/views.py @@ -88,7 +88,6 @@ def update_board(board_uuid, **kwargs): def get_list_boards(**kwargs): data = kwargs user = get_current_user() - boards = Board.query.join(Permission).filter(Permission.user_uuid == user.uuid) # Serialize to paginated response data = listed_response.serialize( diff --git a/task_office/core/helpers/listed_response.py b/task_office/core/helpers/listed_response.py index 1f27cbe..1cc040b 100644 --- a/task_office/core/helpers/listed_response.py +++ b/task_office/core/helpers/listed_response.py @@ -1,8 +1,15 @@ +from flask import request +from flask_babel import lazy_gettext as _ + from task_office.core.utils import lookup_filter +from task_office.exceptions import InvalidUsage +from task_office.settings import CONFIG class ListedResponseHelper: - RESPONSE_TEMPLATE = {"count": 0, "results": []} + error_messages = {"max_offset_exceeded": _("Max value {} exceeded")} + RESPONSE_TEMPLATE = {"count": None, "results": [], "next": None, "prev": None} + RESPONSE_PAGINATED_QS_TEMPLATE = "?limit={}&offset={}" @staticmethod def _get_query_ordered(query, order_param): @@ -27,11 +34,27 @@ def _get_query_paginated(query, limit, offset): def serialize(self, query, query_params, schema): query = self._get_query_filtered(query, query_params.get("searching", {})) query = self._get_query_ordered(query, query_params.get("ordering", "")) - query = self._get_query_paginated( - query, query_params.get("limit"), query_params.get("offset") - ) count = query.count() + + limit = query_params.get("limit", CONFIG.DEFAULT_LIMIT_VALUE) + offset = query_params.get("offset", CONFIG.DEFAULT_OFFSET_VALUE) + query = self._get_query_paginated(query, limit, offset) + data = dict(self.RESPONSE_TEMPLATE) + if offset >= count: + raise InvalidUsage( + messages=[self.error_messages["max_offset_exceeded"].format(count - 1)], + status_code=422, + key="offset", + ) + if (offset + limit) <= count: + data[ + "next" + ] = f"{request.base_url}{self.RESPONSE_PAGINATED_QS_TEMPLATE.format(limit, offset + limit)}" + if (offset - limit) > 0: + data[ + "prev" + ] = f"{request.base_url}{self.RESPONSE_PAGINATED_QS_TEMPLATE.format(limit, offset - limit)}" data["count"] = count data["results"] = schema.dump(query) return data diff --git a/task_office/core/schemas/base_schemas.py b/task_office/core/schemas/base_schemas.py index ab0cbe1..05d816e 100644 --- a/task_office/core/schemas/base_schemas.py +++ b/task_office/core/schemas/base_schemas.py @@ -1,8 +1,8 @@ import logging import uuid -from flask_babel import lazy_gettext as _ -from marshmallow import Schema, fields, validates_schema, post_dump, post_load +from marshmallow import Schema, fields, post_dump, post_load +from marshmallow.validate import Range from task_office.exceptions import InvalidUsage from task_office.settings import CONFIG @@ -50,18 +50,15 @@ def dump_data(self, data, **kwargs): class ListSchema(XSchema): - error_messages = { - "max_limit_exceeded": _("Max limit {} exceeded").format(CONFIG.MAX_LIMIT_VALUE) - } - - limit = fields.Integer(default=CONFIG.DEFAULT_LIMIT_VALUE, required=True) - offset = fields.Integer(default=CONFIG.DEFAULT_OFFSET_VALUE, required=True) - - @validates_schema - def validate_schema(self, data, **kwargs): - limit = data["limit"] - if limit > CONFIG.MAX_LIMIT_VALUE: - self.throw_error(value="", key_error="max_limit_exceeded", code=400) + + limit = fields.Integer( + default=CONFIG.DEFAULT_LIMIT_VALUE, + required=False, + validate=[Range(min=1, max=CONFIG.MAX_LIMIT_VALUE)], + ) + offset = fields.Integer( + default=CONFIG.DEFAULT_OFFSET_VALUE, required=False, validate=[Range(min=0)] + ) list_schema = ListSchema() diff --git a/translations/en/LC_MESSAGES/messages.po b/translations/en/LC_MESSAGES/messages.po index bae377a..b014038 100644 --- a/translations/en/LC_MESSAGES/messages.po +++ b/translations/en/LC_MESSAGES/messages.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2020-03-28 18:08+0200\n" +"POT-Creation-Date: 2020-04-06 08:46+0300\n" "PO-Revision-Date: 2020-03-28 18:05+0200\n" "Last-Translator: FULL NAME \n" "Language: en\n" @@ -84,6 +84,10 @@ msgstr "Already exists" msgid "Not found with value {}" msgstr "Not found with value {}" +#: task_office/core/helpers/listed_response.py:10 +msgid "Max value {} exceeded" +msgstr "Max value {} exceeded" + #: task_office/core/models/db_models.py:76 msgid "Owner" msgstr "Owner" @@ -124,10 +128,6 @@ msgstr "Rejected" msgid "Done" msgstr "Done" -#: task_office/core/schemas/base_schemas.py:54 -msgid "Max limit {} exceeded" -msgstr "Max limit {} exceeded" - #: task_office/tasks/schemas/basic_schemas.py:130 #: task_office/tasks/schemas/basic_schemas.py:132 #: task_office/tasks/schemas/basic_schemas.py:180 @@ -140,3 +140,6 @@ msgstr "ascending" msgid "descending" msgstr "descending" +#~ msgid "Max limit {} exceeded" +#~ msgstr "Max limit {} exceeded" + diff --git a/translations/messages.pot b/translations/messages.pot deleted file mode 100644 index 851218f..0000000 --- a/translations/messages.pot +++ /dev/null @@ -1,141 +0,0 @@ -# Translations template for PROJECT. -# Copyright (C) 2020 ORGANIZATION -# This file is distributed under the same license as the PROJECT project. -# FIRST AUTHOR , 2020. -# -#, fuzzy -msgid "" -msgstr "" -"Project-Id-Version: PROJECT VERSION\n" -"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2020-03-28 18:08+0200\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME \n" -"Language-Team: LANGUAGE \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=utf-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.8.0\n" - -#: task_office/auth/jwt_error_handlers.py:44 -msgid "Token has been revoked" -msgstr "" - -#: task_office/auth/jwt_error_handlers.py:48 -msgid "Fresh token required" -msgstr "" - -#: task_office/auth/jwt_error_handlers.py:57 -msgid "Error loading the user {}" -msgstr "" - -#: task_office/auth/jwt_error_handlers.py:63 -msgid "User claims verification failed" -msgstr "" - -#: task_office/auth/schemas.py:25 -msgid "Passwords do not match" -msgstr "" - -#: task_office/auth/schemas.py:55 -msgid "User not found" -msgstr "" - -#: task_office/auth/utils.py:57 task_office/permissions/views.py:69 -#: task_office/permissions/views.py:99 -msgid "Not allowed" -msgstr "" - -#: task_office/columns/views.py:60 task_office/tasks/views.py:79 -msgid "Must be between {} and {}" -msgstr "" - -#: task_office/columns/views.py:71 task_office/columns/views.py:121 -#: task_office/core/validators.py:10 task_office/tasks/views.py:93 -#: task_office/tasks/views.py:163 -msgid "Already exists with value {}" -msgstr "" - -#: task_office/columns/views.py:108 task_office/tasks/views.py:142 -msgid "Must be between {} and {}." -msgstr "" - -#: task_office/core/enums.py:43 -msgid "Ascend" -msgstr "" - -#: task_office/core/enums.py:44 -msgid "Descend" -msgstr "" - -#: task_office/core/utils.py:23 task_office/core/utils.py:53 -#: task_office/core/utils.py:57 task_office/permissions/views.py:36 -#: task_office/permissions/views.py:58 task_office/permissions/views.py:92 -#: task_office/permissions/views.py:124 task_office/permissions/views.py:147 -msgid "Not found" -msgstr "" - -#: task_office/core/utils.py:31 -msgid "Already exists" -msgstr "" - -#: task_office/core/validators.py:37 -msgid "Not found with value {}" -msgstr "" - -#: task_office/core/models/db_models.py:76 -msgid "Owner" -msgstr "" - -#: task_office/core/models/db_models.py:76 -msgid "Owner of board(creator)" -msgstr "" - -#: task_office/core/models/db_models.py:77 -msgid "Editor" -msgstr "" - -#: task_office/core/models/db_models.py:77 -msgid "Editor of board" -msgstr "" - -#: task_office/core/models/db_models.py:78 -msgid "Staff" -msgstr "" - -#: task_office/core/models/db_models.py:78 -msgid "Ordinary user" -msgstr "" - -#: task_office/core/models/db_models.py:130 -msgid "New" -msgstr "" - -#: task_office/core/models/db_models.py:131 -msgid "In process" -msgstr "" - -#: task_office/core/models/db_models.py:132 -msgid "Rejected" -msgstr "" - -#: task_office/core/models/db_models.py:133 -msgid "Done" -msgstr "" - -#: task_office/core/schemas/base_schemas.py:54 -msgid "Max limit {} exceeded" -msgstr "" - -#: task_office/tasks/schemas/basic_schemas.py:130 -#: task_office/tasks/schemas/basic_schemas.py:132 -#: task_office/tasks/schemas/basic_schemas.py:180 -msgid "ascending" -msgstr "" - -#: task_office/tasks/schemas/basic_schemas.py:131 -#: task_office/tasks/schemas/basic_schemas.py:133 -#: task_office/tasks/schemas/basic_schemas.py:181 -msgid "descending" -msgstr "" - diff --git a/translations/ru/LC_MESSAGES/messages.po b/translations/ru/LC_MESSAGES/messages.po index eddf699..00e4f4d 100644 --- a/translations/ru/LC_MESSAGES/messages.po +++ b/translations/ru/LC_MESSAGES/messages.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2020-03-28 18:08+0200\n" +"POT-Creation-Date: 2020-04-06 08:46+0300\n" "PO-Revision-Date: 2020-01-04 15:13+0200\n" "Last-Translator: FULL NAME \n" "Language: ru\n" @@ -59,8 +59,8 @@ msgid "Already exists with value {}" msgstr "Уже сужествует из значением {}" #: task_office/columns/views.py:108 task_office/tasks/views.py:142 -msgid "Должен быть между {} и {}." -msgstr "" +msgid "Must be between {} and {}." +msgstr "Должен быть между {} и {}." #: task_office/core/enums.py:43 msgid "Ascend" @@ -85,6 +85,10 @@ msgstr "Уже существует" msgid "Not found with value {}" msgstr "Не найдено из ззначением {}" +#: task_office/core/helpers/listed_response.py:10 +msgid "Max value {} exceeded" +msgstr "Максимальное значение {} превышен" + #: task_office/core/models/db_models.py:76 msgid "Owner" msgstr "Владелец" @@ -125,10 +129,6 @@ msgstr "Отклонено" msgid "Done" msgstr "Сделано" -#: task_office/core/schemas/base_schemas.py:54 -msgid "Max limit {} exceeded" -msgstr "Максимальный лимит {} превышен" - #: task_office/tasks/schemas/basic_schemas.py:130 #: task_office/tasks/schemas/basic_schemas.py:132 #: task_office/tasks/schemas/basic_schemas.py:180 @@ -141,3 +141,9 @@ msgstr "Увеличение" msgid "descending" msgstr "Уменьшение" +#~ msgid "Должен быть между {} и {}." +#~ msgstr "" + +#~ msgid "Max limit {} exceeded" +#~ msgstr "Максимальный лимит {} превышен" + diff --git a/translations/uk/LC_MESSAGES/messages.po b/translations/uk/LC_MESSAGES/messages.po index a63626d..81aa138 100644 --- a/translations/uk/LC_MESSAGES/messages.po +++ b/translations/uk/LC_MESSAGES/messages.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2020-03-28 18:08+0200\n" +"POT-Creation-Date: 2020-04-06 08:46+0300\n" "PO-Revision-Date: 2020-01-04 15:13+0200\n" "Last-Translator: FULL NAME \n" "Language: uk\n" @@ -85,6 +85,10 @@ msgstr "Вже існує" msgid "Not found with value {}" msgstr "Не знайдено із значенням {}" +#: task_office/core/helpers/listed_response.py:10 +msgid "Max value {} exceeded" +msgstr "Максимальне значення {} перевищено" + #: task_office/core/models/db_models.py:76 msgid "Owner" msgstr "Власник" @@ -125,10 +129,6 @@ msgstr "Відхилено" msgid "Done" msgstr "Завершено" -#: task_office/core/schemas/base_schemas.py:54 -msgid "Max limit {} exceeded" -msgstr "Максимальне обмеження {} перевищено" - #: task_office/tasks/schemas/basic_schemas.py:130 #: task_office/tasks/schemas/basic_schemas.py:132 #: task_office/tasks/schemas/basic_schemas.py:180 @@ -138,6 +138,9 @@ msgstr "Збільшення" #: task_office/tasks/schemas/basic_schemas.py:131 #: task_office/tasks/schemas/basic_schemas.py:133 #: task_office/tasks/schemas/basic_schemas.py:181 -msgid "Зменшення" +msgid "descending" msgstr "Зменшення" +#~ msgid "Max limit {} exceeded" +#~ msgstr "Максимальне обмеження {} перевищено" + From 6b3e84af140a2ffe525159c068591088bb07eed5 Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Wed, 8 Apr 2020 20:55:19 +0300 Subject: [PATCH 40/60] updated requirements --- requirements/base.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/base.txt b/requirements/base.txt index de557a8..ae2cf72 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -5,7 +5,7 @@ Flask_Bcrypt Flask-JWT-Extended Flask-Cors Flask_SQLAlchemy==2.2 -SQLAlchemy==1.1.9 +SQLAlchemy>=1.3.0 psycopg2-binary marshmallow marshmallow-enum==1.5.1 From 354746eed45a05916a69a56310ef3d4dbfaabb9d Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Fri, 10 Apr 2020 21:43:51 +0300 Subject: [PATCH 41/60] refactoring v1 --- task_office/app.py | 14 +++--- task_office/auth/schemas.py | 16 +++---- task_office/auth/utils.py | 6 ++- task_office/auth/views.py | 20 +++++---- task_office/boards/constants.py | 4 +- task_office/boards/schemas/basic_schemas.py | 12 +++--- task_office/boards/schemas/search_schemas.py | 4 +- task_office/columns/schemas/basic_schemas.py | 12 +++--- task_office/core/helpers/listed_response.py | 6 +-- task_office/core/schemas/base_schemas.py | 16 +++---- task_office/core/schemas/nested_schemas.py | 6 +-- task_office/extensions.py | 6 +-- .../permissions/schemas/basic_schemas.py | 12 ++++-- task_office/settings.py | 43 +++++++++---------- task_office/swagger/views.py | 8 ++-- task_office/tasks/schemas/basic_schemas.py | 18 ++++---- task_office/tasks/schemas/search_schemas.py | 4 +- wsgi.py | 4 +- 18 files changed, 108 insertions(+), 103 deletions(-) diff --git a/task_office/app.py b/task_office/app.py index b7fefef..a89b964 100644 --- a/task_office/app.py +++ b/task_office/app.py @@ -14,7 +14,7 @@ from task_office.auth.jwt_error_handlers import jwt_errors_map from task_office.exceptions import InvalidUsage from task_office.extensions import bcrypt, cache, db, migrate, cors, jwt, babel -from task_office.settings import CONFIG +from task_office.settings import app_config from task_office.swagger import SWAGGER_URL @@ -26,15 +26,15 @@ def create_app(config_object): """ app = Flask( __name__.split(".")[0], - static_folder=CONFIG.STATIC_DIR, - static_url_path=CONFIG.STATIC_URL, + static_folder=app_config.STATIC_DIR, + static_url_path=app_config.STATIC_URL, ) app.url_map.strict_slashes = False app.config.from_object(config_object) register_extensions(app) register_blueprints(app) register_error_handlers(app) - register_shellcontext(app) + register_shell_context(app) register_commands(app) return app @@ -42,7 +42,7 @@ def create_app(config_object): def register_extensions(app): """Register Flask extensions.""" bcrypt.init_app(app) - cache.init_app(app, config=CONFIG.CACHE) + cache.init_app(app, config=app_config.CACHE) db.init_app(app) jwt.init_app(app) migrate.init_app(app, db) @@ -59,7 +59,7 @@ def register_blueprints(app): app.register_blueprint(columns.views.blueprint) app.register_blueprint(tasks.views.blueprint) app.register_blueprint(core.views.blueprint) - if CONFIG.USE_DOCS: + if app_config.USE_DOCS: app.register_blueprint(swagger.views.blueprint_swagger, url_prefix=SWAGGER_URL) app.register_blueprint(swagger.views.blueprint) @@ -84,7 +84,7 @@ def jwt_error_handler(error): ] -def register_shellcontext(app): +def register_shell_context(app): """Register shell context objects.""" def shell_context(): diff --git a/task_office/auth/schemas.py b/task_office/auth/schemas.py index fbbd93f..9cf20b3 100644 --- a/task_office/auth/schemas.py +++ b/task_office/auth/schemas.py @@ -5,7 +5,7 @@ from task_office.core.models.db_models import User from task_office.core.schemas.base_schemas import BaseSchema, XSchema from task_office.core.validators import Unique -from task_office.settings import CONFIG +from task_office.settings import app_config class UserSchema(BaseSchema): @@ -104,12 +104,12 @@ class SignedSchema(XSchema): token_schema = TokenSchema() -CONFIG.API_SPEC.components.schema("UserSchema", schema=UserSchema) -CONFIG.API_SPEC.components.schema("UserSignInSchema", schema=UserSignInSchema) -CONFIG.API_SPEC.components.schema("UserSignUpSchema", schema=UserSignUpSchema) -CONFIG.API_SPEC.components.schema("TokenSchema", schema=TokenSchema) -CONFIG.API_SPEC.components.schema("SignedTokensSchema", schema=SignedTokensSchema) -CONFIG.API_SPEC.components.schema( +app_config.API_SPEC.components.schema("UserSchema", schema=UserSchema) +app_config.API_SPEC.components.schema("UserSignInSchema", schema=UserSignInSchema) +app_config.API_SPEC.components.schema("UserSignUpSchema", schema=UserSignUpSchema) +app_config.API_SPEC.components.schema("TokenSchema", schema=TokenSchema) +app_config.API_SPEC.components.schema("SignedTokensSchema", schema=SignedTokensSchema) +app_config.API_SPEC.components.schema( "RefreshedAccessTokenSchema", schema=RefreshedAccessTokenSchema ) -CONFIG.API_SPEC.components.schema("SignedSchema", schema=SignedSchema) +app_config.API_SPEC.components.schema("SignedSchema", schema=SignedSchema) diff --git a/task_office/auth/utils.py b/task_office/auth/utils.py index 706b34e..d6dd535 100644 --- a/task_office/auth/utils.py +++ b/task_office/auth/utils.py @@ -6,7 +6,7 @@ from task_office.core.models.db_models import Permission from task_office.exceptions import InvalidUsage from task_office.extensions import cache -from task_office.settings import CONFIG +from task_office.settings import app_config def _get_cached_permissions(): @@ -19,7 +19,9 @@ def _get_cached_permissions(): if not perms: perms = {uuid.UUID(item.board_uuid).hex: item.role for item in user.perms} if perms: - cache.set(key, perms, timeout=CONFIG.JWT_ACCESS_TOKEN_EXPIRES.seconds) + cache.set( + key, perms, timeout=app_config.JWT_ACCESS_TOKEN_EXPIRES.seconds + ) return perms diff --git a/task_office/auth/views.py b/task_office/auth/views.py index f7b1150..21a17e4 100644 --- a/task_office/auth/views.py +++ b/task_office/auth/views.py @@ -18,9 +18,9 @@ refreshed_access_tokens_schema, ) from ..core.models.db_models import User -from ..settings import CONFIG +from ..settings import app_config -blueprint = Blueprint("auth", __name__, url_prefix=CONFIG.API_V1_PREFIX + "auth") +blueprint = Blueprint("auth", __name__, url_prefix=app_config.API_V1_PREFIX + "auth") @blueprint.route("/sign-up", methods=("post",)) @@ -48,9 +48,11 @@ def sign_in(**kwargs): """ data = kwargs refresh_lf = datetime.timestamp( - datetime.utcnow() + CONFIG.JWT_REFRESH_TOKEN_EXPIRES + datetime.utcnow() + app_config.JWT_REFRESH_TOKEN_EXPIRES + ) + access_lf = datetime.timestamp( + datetime.utcnow() + app_config.JWT_ACCESS_TOKEN_EXPIRES ) - access_lf = datetime.timestamp(datetime.utcnow() + CONFIG.JWT_ACCESS_TOKEN_EXPIRES) return { "user": data["user"], "tokens": { @@ -62,8 +64,8 @@ def sign_in(**kwargs): "lifetime": refresh_lf, "token": create_refresh_token(identity=data["user"]), }, - "header_type": CONFIG.JWT_AUTH_HEADER_PREFIX, - "time_zone_info": CONFIG.TIME_ZONE, + "header_type": app_config.JWT_AUTH_HEADER_PREFIX, + "time_zone_info": app_config.TIME_ZONE, }, } @@ -77,11 +79,13 @@ def refresh(**kwargs): :return: """ current_user = User.get_by_id(get_jwt_identity()) - access_lf = datetime.timestamp(datetime.utcnow() + CONFIG.JWT_ACCESS_TOKEN_EXPIRES) + access_lf = datetime.timestamp( + datetime.utcnow() + app_config.JWT_ACCESS_TOKEN_EXPIRES + ) return { "access": { "lifetime": access_lf, "token": create_access_token(identity=current_user, fresh=True), }, - "time_zone_info": CONFIG.TIME_ZONE, + "time_zone_info": app_config.TIME_ZONE, } diff --git a/task_office/boards/constants.py b/task_office/boards/constants.py index de4db6a..733a0a2 100644 --- a/task_office/boards/constants.py +++ b/task_office/boards/constants.py @@ -1,4 +1,4 @@ -from task_office.settings import CONFIG +from task_office.settings import app_config -BOARDS_PREFIX = CONFIG.API_V1_PREFIX + "boards" +BOARDS_PREFIX = app_config.API_V1_PREFIX + "boards" BOARD_RETRIEVE_URL = BOARDS_PREFIX + "/" diff --git a/task_office/boards/schemas/basic_schemas.py b/task_office/boards/schemas/basic_schemas.py index 698a371..8b3d768 100644 --- a/task_office/boards/schemas/basic_schemas.py +++ b/task_office/boards/schemas/basic_schemas.py @@ -7,7 +7,7 @@ from task_office.boards.schemas.search_schemas import SearchUserSchema from task_office.core.enums import XEnum from task_office.core.schemas.base_schemas import BaseSchema, ListSchema, XSchema -from task_office.settings import CONFIG +from task_office.settings import app_config class BoardActionsSchema(BaseSchema): @@ -35,8 +35,8 @@ def validate_schema(self, data, **kwargs): board_action_schema = BoardActionsSchema() board_dump_schema = BoardDumpSchema() board_list_dump_schema = BoardDumpSchema(many=True) -CONFIG.API_SPEC.components.schema("BoardActionsSchema", schema=BoardActionsSchema) -CONFIG.API_SPEC.components.schema("BoardDumpSchema", schema=BoardDumpSchema) +app_config.API_SPEC.components.schema("BoardActionsSchema", schema=BoardActionsSchema) +app_config.API_SPEC.components.schema("BoardDumpSchema", schema=BoardDumpSchema) class BoardListQuerySchema(ListSchema): @@ -51,7 +51,9 @@ class Meta: board_list_query_schema = BoardListQuerySchema() -CONFIG.API_SPEC.components.schema("BoardListQuerySchema", schema=BoardListQuerySchema) +app_config.API_SPEC.components.schema( + "BoardListQuerySchema", schema=BoardListQuerySchema +) class UserListByBoardQuerySchema(ListSchema): @@ -68,6 +70,6 @@ class Meta: user_list_by_board_query_schema = UserListByBoardQuerySchema() -CONFIG.API_SPEC.components.schema( +app_config.API_SPEC.components.schema( "UserListByBoardQuerySchema", schema=UserListByBoardQuerySchema ) diff --git a/task_office/boards/schemas/search_schemas.py b/task_office/boards/schemas/search_schemas.py index ce1a658..17b608b 100644 --- a/task_office/boards/schemas/search_schemas.py +++ b/task_office/boards/schemas/search_schemas.py @@ -2,7 +2,7 @@ from task_office.core.models.db_models import User from task_office.core.schemas.base_schemas import SearchSchema -from task_office.settings import CONFIG +from task_office.settings import app_config class SearchUserSchema(SearchSchema): @@ -16,4 +16,4 @@ class Meta: search_user_schema = SearchUserSchema() -CONFIG.API_SPEC.components.schema("SearchUserSchema", schema=SearchUserSchema) +app_config.API_SPEC.components.schema("SearchUserSchema", schema=SearchUserSchema) diff --git a/task_office/columns/schemas/basic_schemas.py b/task_office/columns/schemas/basic_schemas.py index ad05899..858bed0 100644 --- a/task_office/columns/schemas/basic_schemas.py +++ b/task_office/columns/schemas/basic_schemas.py @@ -7,7 +7,7 @@ from task_office.core.enums import XEnum, OrderingDirection from task_office.core.models.db_models import BoardColumn from task_office.core.schemas.base_schemas import BaseSchema, ListSchema, XSchema -from task_office.settings import CONFIG +from task_office.settings import app_config class ColumnPostSchema(BaseSchema): @@ -45,9 +45,9 @@ class Meta: column_put_schema = ColumnPutSchema() column_dump_schema = ColumnDumpSchema() column_listed_dump_schema = ColumnDumpSchema(many=True) -CONFIG.API_SPEC.components.schema("ColumnPostSchema", schema=ColumnPostSchema) -CONFIG.API_SPEC.components.schema("ColumnPutSchema", schema=ColumnPutSchema) -CONFIG.API_SPEC.components.schema("ColumnDumpSchema", schema=ColumnDumpSchema) +app_config.API_SPEC.components.schema("ColumnPostSchema", schema=ColumnPostSchema) +app_config.API_SPEC.components.schema("ColumnPutSchema", schema=ColumnPutSchema) +app_config.API_SPEC.components.schema("ColumnDumpSchema", schema=ColumnDumpSchema) class ColumnListQuerySchema(ListSchema): @@ -71,4 +71,6 @@ class Meta: column_list_query_schema = ColumnListQuerySchema() -CONFIG.API_SPEC.components.schema("ColumnListQuerySchema", schema=ColumnListQuerySchema) +app_config.API_SPEC.components.schema( + "ColumnListQuerySchema", schema=ColumnListQuerySchema +) diff --git a/task_office/core/helpers/listed_response.py b/task_office/core/helpers/listed_response.py index 1cc040b..8e6e5cc 100644 --- a/task_office/core/helpers/listed_response.py +++ b/task_office/core/helpers/listed_response.py @@ -3,7 +3,7 @@ from task_office.core.utils import lookup_filter from task_office.exceptions import InvalidUsage -from task_office.settings import CONFIG +from task_office.settings import app_config class ListedResponseHelper: @@ -36,8 +36,8 @@ def serialize(self, query, query_params, schema): query = self._get_query_ordered(query, query_params.get("ordering", "")) count = query.count() - limit = query_params.get("limit", CONFIG.DEFAULT_LIMIT_VALUE) - offset = query_params.get("offset", CONFIG.DEFAULT_OFFSET_VALUE) + limit = query_params.get("limit", app_config.DEFAULT_LIMIT_VALUE) + offset = query_params.get("offset", app_config.DEFAULT_OFFSET_VALUE) query = self._get_query_paginated(query, limit, offset) data = dict(self.RESPONSE_TEMPLATE) diff --git a/task_office/core/schemas/base_schemas.py b/task_office/core/schemas/base_schemas.py index 05d816e..a39293d 100644 --- a/task_office/core/schemas/base_schemas.py +++ b/task_office/core/schemas/base_schemas.py @@ -5,7 +5,7 @@ from marshmallow.validate import Range from task_office.exceptions import InvalidUsage -from task_office.settings import CONFIG +from task_office.settings import app_config class XSchema(Schema): @@ -35,12 +35,8 @@ def handle_error(self, exc, data, **kwargs): class BaseSchema(XSchema): - created_at = fields.DateTime( - attribute="created_at", dump_only=True, format=CONFIG.API_DATETIME_FORMAT - ) - updated_at = fields.DateTime( - attribute="updated_at", dump_only=True, format=CONFIG.API_DATETIME_FORMAT - ) + created_at = fields.DateTime(attribute="created_at", dump_only=True) + updated_at = fields.DateTime(attribute="updated_at", dump_only=True) uuid = fields.UUID(dump_only=True) @post_dump @@ -52,12 +48,12 @@ def dump_data(self, data, **kwargs): class ListSchema(XSchema): limit = fields.Integer( - default=CONFIG.DEFAULT_LIMIT_VALUE, + default=app_config.DEFAULT_LIMIT_VALUE, required=False, - validate=[Range(min=1, max=CONFIG.MAX_LIMIT_VALUE)], + validate=[Range(min=1, max=app_config.MAX_LIMIT_VALUE)], ) offset = fields.Integer( - default=CONFIG.DEFAULT_OFFSET_VALUE, required=False, validate=[Range(min=0)] + default=app_config.DEFAULT_OFFSET_VALUE, required=False, validate=[Range(min=0)] ) diff --git a/task_office/core/schemas/nested_schemas.py b/task_office/core/schemas/nested_schemas.py index 3b4481a..8e1b85e 100644 --- a/task_office/core/schemas/nested_schemas.py +++ b/task_office/core/schemas/nested_schemas.py @@ -3,7 +3,7 @@ from marshmallow import fields, post_dump from task_office.core.schemas.base_schemas import XSchema -from task_office.settings import CONFIG +from task_office.settings import app_config class NestedUserDumpSchema(XSchema): @@ -40,7 +40,7 @@ class Meta: nested_column_dump_schema = NestedColumnDumpSchema() -CONFIG.API_SPEC.components.schema("NestedUserSchema", schema=NestedUserDumpSchema) -CONFIG.API_SPEC.components.schema( +app_config.API_SPEC.components.schema("NestedUserSchema", schema=NestedUserDumpSchema) +app_config.API_SPEC.components.schema( "NestedColumnDumpSchema", schema=NestedColumnDumpSchema ) diff --git a/task_office/extensions.py b/task_office/extensions.py index a44e2a5..1aef84c 100644 --- a/task_office/extensions.py +++ b/task_office/extensions.py @@ -8,7 +8,7 @@ from flask_migrate import Migrate from flask_sqlalchemy import SQLAlchemy, Model -from task_office.settings import CONFIG +from task_office.settings import app_config from task_office.utils import jwt_identity, identity_loader @@ -55,7 +55,7 @@ def delete(self, commit=True): # Babel # https://pythonhosted.org/Flask-Babel/ # ------------------------------------------------------------------------------ -babel = Babel(default_locale=CONFIG.LOCALE, default_timezone=CONFIG.TIME_ZONE) +babel = Babel(default_locale=app_config.LOCALE, default_timezone=app_config.TIME_ZONE) @babel.localeselector @@ -64,7 +64,7 @@ def get_locale(): user = getattr(g, "user", None) if user is not None: return user.locale - return request.accept_languages.best_match(list(CONFIG.LANGUAGES.keys())) + return request.accept_languages.best_match(list(app_config.LANGUAGES.keys())) @babel.timezoneselector diff --git a/task_office/permissions/schemas/basic_schemas.py b/task_office/permissions/schemas/basic_schemas.py index 02b79e7..4654e4e 100644 --- a/task_office/permissions/schemas/basic_schemas.py +++ b/task_office/permissions/schemas/basic_schemas.py @@ -9,7 +9,7 @@ from task_office.core.schemas.base_schemas import BaseSchema, ListSchema, XSchema from task_office.core.schemas.nested_schemas import NestedUserDumpSchema from task_office.core.validators import PKExists -from ...settings import CONFIG +from ...settings import app_config class PermissionQuerySchema(BaseSchema): @@ -45,8 +45,12 @@ class Meta: permission_query_schema = PermissionQuerySchema() permission_dump_schema = PermissionDumpSchema() permission_list_dump_schema = PermissionDumpSchema(many=True) -CONFIG.API_SPEC.components.schema("PermissionQuerySchema", schema=PermissionQuerySchema) -CONFIG.API_SPEC.components.schema("PermissionDumpSchema", schema=PermissionDumpSchema) +app_config.API_SPEC.components.schema( + "PermissionQuerySchema", schema=PermissionQuerySchema +) +app_config.API_SPEC.components.schema( + "PermissionDumpSchema", schema=PermissionDumpSchema +) class PermissionListQuerySchema(ListSchema): @@ -70,6 +74,6 @@ class Meta: permissions_list_query_schema = PermissionListQuerySchema() -CONFIG.API_SPEC.components.schema( +app_config.API_SPEC.components.schema( "PermissionListQuerySchema", schema=PermissionListQuerySchema ) diff --git a/task_office/settings.py b/task_office/settings.py index f9b1618..5c1f691 100644 --- a/task_office/settings.py +++ b/task_office/settings.py @@ -13,21 +13,28 @@ class Config(object): """Base configuration.""" - # Project dirs + # PROJECT DIRS APP_DIR = os.path.abspath(os.path.dirname(__file__)) PROJECT_ROOT = os.path.abspath(os.path.join(APP_DIR, os.pardir)) - # Environment variables setting READ_DOT_ENV_FILE = env.bool("FLASK_READ_DOT_ENV_FILE", default=False) if READ_DOT_ENV_FILE: # OS environment variables take precedence over variables from .env env.read_env(os.path.join(PROJECT_ROOT, ".env")) - PROJECT_NAME = env.str("PROJECT_NAME", "Task Office") - SECRET_KEY = env.str("FLASK_SECRET", "secret-key") + # GENERAL + PROJECT_NAME = env.str("PROJECT_NAME", "NoName") + SECRET_KEY = env.str("FLASK_SECRET", "NoKey") + FLASK_DEBUG = env.int("FLASK_DEBUG", 0) + TIME_ZONE = "UTC" + + LANGUAGES = {"ru": "Russian", "en": "English", "uk": "Ukrainian"} + LOCALE = "en" + # https://pythonhosted.org/Flask-Babel/ + BABEL_TRANSLATION_DIRECTORIES = os.path.join(PROJECT_ROOT, "translations") + # API API_V1_PREFIX = "/api/v1/" - API_DATETIME_FORMAT = "%Y-%m-%d %I:%M:%S" USE_DOCS = env.bool("USE_DOCS", False) API_SPEC = APISpec( @@ -38,22 +45,17 @@ class Config(object): plugins=[FlaskPlugin(), MarshmallowPlugin()], ) - FLASK_DEBUG = env.int("FLASK_DEBUG", 0) - DEBUG_TB_INTERCEPT_REDIRECTS = False - - TIME_ZONE = "UTC" - LANGUAGES = {"ru": "Russian", "en": "English", "uk": "Ukrainian"} - LOCALE = "en" + # API PAGINATION + DEFAULT_OFFSET_VALUE = 0 + DEFAULT_LIMIT_VALUE = 15 + MAX_LIMIT_VALUE = 50 - # https://pythonhosted.org/Flask-Babel/ - BABEL_TRANSLATION_DIRECTORIES = os.path.join(PROJECT_ROOT, "translations") + CORS_ORIGIN_WHITELIST = env.list("CORS_ORIGIN_WHITELIST", []) - # Static settings + # STATIC DIRS STATIC_DIR = os.path.abspath(os.path.join(PROJECT_ROOT, "static")) STATIC_URL = API_V1_PREFIX + "/static" - CORS_ORIGIN_WHITELIST = env.list("CORS_ORIGIN_WHITELIST", []) - # JWT JWT_AUTH_USERNAME_KEY = "uuid" JWT_AUTH_HEADER_PREFIX = "Bearer" @@ -63,11 +65,6 @@ class Config(object): JWT_REFRESH_TOKEN_EXPIRES = timedelta(days=env.int("JWT_REFRESH_TOKEN_EXPIRES", 7)) JWT_BLACKLIST_TOKEN_CHECKS = ["access", "refresh"] - # Pagination - DEFAULT_OFFSET_VALUE = 0 - DEFAULT_LIMIT_VALUE = 15 - MAX_LIMIT_VALUE = 50 - # DB SQLALCHEMY_TRACK_MODIFICATIONS = False DATABASE = { @@ -114,6 +111,6 @@ class DevConfig(Config): MODE = os.environ.get("MODE", default="dev") -configurations = {"dev": DevConfig, "prod": ProdConfig} +CONFIG_SETS = {"dev": DevConfig, "prod": ProdConfig} -CONFIG = configurations.get(MODE) +app_config = CONFIG_SETS.get(MODE) diff --git a/task_office/swagger/views.py b/task_office/swagger/views.py index 89a3092..cf15973 100644 --- a/task_office/swagger/views.py +++ b/task_office/swagger/views.py @@ -3,17 +3,17 @@ from flask import Blueprint, jsonify from flask_swagger_ui import get_swaggerui_blueprint -from task_office.settings import CONFIG +from task_office.settings import app_config from task_office.swagger.api_paths import API_PATHS -SWAGGER_URL = CONFIG.API_V1_PREFIX + "docs" +SWAGGER_URL = app_config.API_V1_PREFIX + "docs" API_URL = SWAGGER_URL + "/open-api" blueprint = Blueprint("docs", __name__, url_prefix=SWAGGER_URL) blueprint_swagger = get_swaggerui_blueprint( - SWAGGER_URL, API_URL, config={"app_name": CONFIG.PROJECT_NAME} + SWAGGER_URL, API_URL, config={"app_name": app_config.PROJECT_NAME} ) @@ -23,6 +23,6 @@ def api_swagger(**kwargs): :param kwargs: :return: """ - data = CONFIG.API_SPEC.to_dict() + data = app_config.API_SPEC.to_dict() data["paths"] = API_PATHS return jsonify(data) diff --git a/task_office/tasks/schemas/basic_schemas.py b/task_office/tasks/schemas/basic_schemas.py index 508c6f5..a91043b 100644 --- a/task_office/tasks/schemas/basic_schemas.py +++ b/task_office/tasks/schemas/basic_schemas.py @@ -10,7 +10,7 @@ from task_office.core.schemas.base_schemas import BaseSchema, ListSchema, SearchSchema from task_office.core.schemas.nested_schemas import NestedUserDumpSchema from task_office.core.validators import PKExists -from task_office.settings import CONFIG +from task_office.settings import app_config from task_office.tasks.schemas.search_schemas import SearchTaskSchema @@ -93,9 +93,7 @@ def validate_schema(self, data, **kwargs): class TaskDumpSchema(BaseSchema): - expire_at = fields.DateTime( - attribute="expire_at", dump_only=True, format=CONFIG.API_DATETIME_FORMAT - ) + expire_at = fields.DateTime(attribute="expire_at", dump_only=True) label = fields.Str(dump_only=True) name = fields.Str(dump_only=True) description = fields.Str(dump_only=True) @@ -118,9 +116,9 @@ class Meta: task_put_schema = TaskPutSchema() task_dump_schema = TaskDumpSchema() tasks_listed_dump_schema = TaskDumpSchema(many=True) -CONFIG.API_SPEC.components.schema("TaskPostSchema", schema=TaskPostSchema) -CONFIG.API_SPEC.components.schema("TaskPutSchema", schema=TaskPutSchema) -CONFIG.API_SPEC.components.schema("TaskDumpSchema", schema=TaskDumpSchema) +app_config.API_SPEC.components.schema("TaskPostSchema", schema=TaskPostSchema) +app_config.API_SPEC.components.schema("TaskPutSchema", schema=TaskPutSchema) +app_config.API_SPEC.components.schema("TaskDumpSchema", schema=TaskDumpSchema) class TaskListQuerySchema(ListSchema): @@ -150,7 +148,7 @@ def preload_data(self, data, **kwargs): task_list_query_schema = TaskListQuerySchema() -CONFIG.API_SPEC.components.schema("TaskListQuerySchema", schema=TaskListQuerySchema) +app_config.API_SPEC.components.schema("TaskListQuerySchema", schema=TaskListQuerySchema) class ColumnWithTasksDumpSchema(BaseSchema): @@ -168,7 +166,7 @@ class Meta: columns_listed_dump_schema = ColumnWithTasksDumpSchema(many=True) -CONFIG.API_SPEC.components.schema( +app_config.API_SPEC.components.schema( "ColumnWithTasksDumpSchema", schema=ColumnWithTasksDumpSchema ) @@ -188,6 +186,6 @@ class Meta: task_list_by_columns_query_schema = TaskListByColumnsQuerySchema() -CONFIG.API_SPEC.components.schema( +app_config.API_SPEC.components.schema( "TaskListByColumnsQuerySchema", schema=TaskListByColumnsQuerySchema ) diff --git a/task_office/tasks/schemas/search_schemas.py b/task_office/tasks/schemas/search_schemas.py index 6779e79..2ddc8f7 100644 --- a/task_office/tasks/schemas/search_schemas.py +++ b/task_office/tasks/schemas/search_schemas.py @@ -2,7 +2,7 @@ from task_office.core.models.db_models import Task from task_office.core.schemas.base_schemas import SearchSchema -from task_office.settings import CONFIG +from task_office.settings import app_config class SearchTaskSchema(SearchSchema): @@ -23,4 +23,4 @@ class Meta: search_task_schema = SearchTaskSchema() -CONFIG.API_SPEC.components.schema("SearchTaskSchema", schema=SearchTaskSchema) +app_config.API_SPEC.components.schema("SearchTaskSchema", schema=SearchTaskSchema) diff --git a/wsgi.py b/wsgi.py index 7676bc3..3703f31 100755 --- a/wsgi.py +++ b/wsgi.py @@ -1,6 +1,6 @@ """Create an application instance.""" from task_office.app import create_app -from task_office.settings import CONFIG +from task_office.settings import app_config -app = create_app(CONFIG) +app = create_app(app_config) From 7195ff4d455d32ae0baa6a399e2bb6a4258958fc Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Sat, 11 Apr 2020 12:32:38 +0300 Subject: [PATCH 42/60] refactoring v2 --- task_office/api/__init__.py | 0 task_office/api/v1/__init__.py | 1 + task_office/api/v1/views.py | 5 +++++ task_office/app.py | 18 +++++++++--------- task_office/auth/views.py | 10 +++++----- task_office/boards/constants.py | 6 ++---- task_office/boards/views.py | 16 +++++++--------- task_office/columns/constants.py | 6 +++--- task_office/columns/views.py | 16 +++++++--------- task_office/core/views.py | 4 ++-- task_office/permissions/constants.py | 4 ++++ task_office/permissions/views.py | 20 ++++++++------------ task_office/settings.py | 4 ++-- task_office/swagger/views.py | 10 +++++----- task_office/tasks/constants.py | 6 +++--- task_office/tasks/views.py | 18 ++++++++---------- 16 files changed, 71 insertions(+), 73 deletions(-) create mode 100644 task_office/api/__init__.py create mode 100644 task_office/api/v1/__init__.py create mode 100644 task_office/api/v1/views.py create mode 100644 task_office/permissions/constants.py diff --git a/task_office/api/__init__.py b/task_office/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/task_office/api/v1/__init__.py b/task_office/api/v1/__init__.py new file mode 100644 index 0000000..14cd5bd --- /dev/null +++ b/task_office/api/v1/__init__.py @@ -0,0 +1 @@ +from . import views diff --git a/task_office/api/v1/views.py b/task_office/api/v1/views.py new file mode 100644 index 0000000..eee9842 --- /dev/null +++ b/task_office/api/v1/views.py @@ -0,0 +1,5 @@ +from flask.blueprints import Blueprint + +from task_office.settings import app_config + +bp = Blueprint("api_v1", __name__, url_prefix=app_config.API_PREFIX) diff --git a/task_office/app.py b/task_office/app.py index a89b964..43e2fff 100644 --- a/task_office/app.py +++ b/task_office/app.py @@ -52,16 +52,16 @@ def register_extensions(app): def register_blueprints(app): """Register Flask blueprints.""" origins = app.config.get("CORS_ORIGIN_WHITELIST", "*") - cors.init_app(auth.views.blueprint, origins=origins) - app.register_blueprint(auth.views.blueprint) - app.register_blueprint(boards.views.blueprint) - app.register_blueprint(permissions.views.blueprint) - app.register_blueprint(columns.views.blueprint) - app.register_blueprint(tasks.views.blueprint) - app.register_blueprint(core.views.blueprint) + cors.init_app(auth.views.bp, origins=origins) + app.register_blueprint(auth.views.bp) + app.register_blueprint(boards.views.bp) + app.register_blueprint(permissions.views.bp) + app.register_blueprint(columns.views.bp) + app.register_blueprint(tasks.views.bp) + app.register_blueprint(core.views.bp) if app_config.USE_DOCS: - app.register_blueprint(swagger.views.blueprint_swagger, url_prefix=SWAGGER_URL) - app.register_blueprint(swagger.views.blueprint) + app.register_blueprint(swagger.views.bp_swagger, url_prefix=SWAGGER_URL) + app.register_blueprint(swagger.views.bp) def register_error_handlers(app): diff --git a/task_office/auth/views.py b/task_office/auth/views.py index 21a17e4..5f96340 100644 --- a/task_office/auth/views.py +++ b/task_office/auth/views.py @@ -1,7 +1,6 @@ """User views.""" from datetime import datetime -from flask import Blueprint from flask_apispec import use_kwargs, marshal_with from flask_jwt_extended import ( create_access_token, @@ -17,13 +16,14 @@ signed_schema, refreshed_access_tokens_schema, ) +from ..api.v1.views import bp from ..core.models.db_models import User from ..settings import app_config -blueprint = Blueprint("auth", __name__, url_prefix=app_config.API_V1_PREFIX + "auth") +APP_PREFIX = "/auth" -@blueprint.route("/sign-up", methods=("post",)) +@bp.route(APP_PREFIX + "/sign-up", methods=("post",)) @use_kwargs(user_signup_schema) @marshal_with(user_schema) def sign_up(**kwargs): @@ -38,7 +38,7 @@ def sign_up(**kwargs): return user -@blueprint.route("/sign-in", methods=("post",)) +@bp.route(APP_PREFIX + "/sign-in", methods=("post",)) @use_kwargs(user_signin_schema) @marshal_with(signed_schema) def sign_in(**kwargs): @@ -70,7 +70,7 @@ def sign_in(**kwargs): } -@blueprint.route("/refresh", methods=("post",)) +@bp.route(APP_PREFIX + "/refresh", methods=("post",)) @jwt_refresh_token_required @marshal_with(refreshed_access_tokens_schema) def refresh(**kwargs): diff --git a/task_office/boards/constants.py b/task_office/boards/constants.py index 733a0a2..9489ccd 100644 --- a/task_office/boards/constants.py +++ b/task_office/boards/constants.py @@ -1,4 +1,2 @@ -from task_office.settings import app_config - -BOARDS_PREFIX = app_config.API_V1_PREFIX + "boards" -BOARD_RETRIEVE_URL = BOARDS_PREFIX + "/" +APP_PREFIX = "/boards" +APP_PREFIX_RETRIEVE = APP_PREFIX + "/" diff --git a/task_office/boards/views.py b/task_office/boards/views.py index c4848c9..47d1cd1 100644 --- a/task_office/boards/views.py +++ b/task_office/boards/views.py @@ -1,12 +1,11 @@ """Boards views.""" from datetime import datetime -from flask import Blueprint from flask_apispec import use_kwargs, marshal_with from flask_jwt_extended import jwt_required, get_current_user from sqlalchemy.orm import aliased -from .constants import BOARDS_PREFIX +from .constants import APP_PREFIX, APP_PREFIX_RETRIEVE from .schemas.basic_schemas import ( board_action_schema, board_list_query_schema, @@ -14,6 +13,7 @@ board_dump_schema, user_list_by_board_query_schema, ) +from ..api.v1.views import bp from ..auth.utils import permission, reset_permissions_for_board_staff from ..core.helpers.listed_response import listed_response from ..core.models.db_models import Board, Permission, User @@ -24,10 +24,8 @@ non_empty_query_required, ) -blueprint = Blueprint("boards", __name__, url_prefix=BOARDS_PREFIX) - -@blueprint.route("", methods=("post",)) +@bp.route(APP_PREFIX, methods=("post",)) @jwt_required @use_kwargs(board_action_schema) @marshal_with(board_dump_schema) @@ -55,7 +53,7 @@ def create_boards(**kwargs): return board -@blueprint.route("/", methods=("put",)) +@bp.route(APP_PREFIX_RETRIEVE, methods=("put",)) @jwt_required @use_kwargs(board_action_schema) @marshal_with(board_dump_schema) @@ -82,7 +80,7 @@ def update_board(board_uuid, **kwargs): return board -@blueprint.route("", methods=("get",)) +@bp.route(APP_PREFIX, methods=("get",)) @jwt_required @use_kwargs(board_list_query_schema) def get_list_boards(**kwargs): @@ -96,7 +94,7 @@ def get_list_boards(**kwargs): return data -@blueprint.route("/", methods=("get",)) +@bp.route(APP_PREFIX_RETRIEVE, methods=("get",)) @jwt_required @marshal_with(board_dump_schema) @permission(required_role=Permission.Role.STAFF.value) @@ -107,7 +105,7 @@ def get_board(board_uuid): return board -@blueprint.route("//users", methods=("get",)) +@bp.route(APP_PREFIX_RETRIEVE + "/users", methods=("get",)) @jwt_required @use_kwargs(user_list_by_board_query_schema) @permission(required_role=Permission.Role.STAFF.value) diff --git a/task_office/columns/constants.py b/task_office/columns/constants.py index fcc6ddb..0e437a0 100644 --- a/task_office/columns/constants.py +++ b/task_office/columns/constants.py @@ -1,4 +1,4 @@ -from task_office.boards.constants import BOARD_RETRIEVE_URL +from task_office.boards.constants import APP_PREFIX_RETRIEVE as BOARD_PREFIX_RETRIEVE -COLUMNS_PREFIX = BOARD_RETRIEVE_URL + "/columns" -COLUMNS_RETRIEVE_URL = COLUMNS_PREFIX + "/" +APP_PREFIX = BOARD_PREFIX_RETRIEVE + "/columns" +APP_PREFIX_RETRIEVE = APP_PREFIX + "/" diff --git a/task_office/columns/views.py b/task_office/columns/views.py index 27897de..37da921 100644 --- a/task_office/columns/views.py +++ b/task_office/columns/views.py @@ -1,13 +1,12 @@ """Columns views.""" from datetime import datetime -from flask import Blueprint from flask_apispec import use_kwargs, marshal_with from flask_babel import lazy_gettext as _ from flask_jwt_extended import jwt_required from sqlalchemy import func -from .constants import COLUMNS_PREFIX +from .constants import APP_PREFIX, APP_PREFIX_RETRIEVE from .schemas.basic_schemas import ( column_post_schema, column_list_query_schema, @@ -16,18 +15,17 @@ column_put_schema, ) from .utils import reset_columns_ordering +from ..api.v1.views import bp from ..core.helpers.listed_response import listed_response from ..core.models.db_models import BoardColumn, Board from ..core.utils import validate_request_url_uuid, non_empty_query_required from ..exceptions import InvalidUsage from ..extensions import db -blueprint = Blueprint("columns", __name__, url_prefix=COLUMNS_PREFIX) - -@blueprint.route("/meta", methods=("get",)) +@bp.route(APP_PREFIX + "/meta", methods=("get",)) @jwt_required -def get_meta_data(board_uuid): +def get_columns_meta_data(board_uuid): """ Additional data for Columns """ @@ -36,7 +34,7 @@ def get_meta_data(board_uuid): return data -@blueprint.route("", methods=("post",)) +@bp.route(APP_PREFIX, methods=("post",)) @jwt_required @use_kwargs(column_post_schema) @marshal_with(column_dump_schema) @@ -81,7 +79,7 @@ def create_column(board_uuid, **kwargs): return column -@blueprint.route("/", methods=("put",)) +@bp.route(APP_PREFIX_RETRIEVE, methods=("put",)) @jwt_required @use_kwargs(column_put_schema) @marshal_with(column_dump_schema) @@ -137,7 +135,7 @@ def update_column(board_uuid, column_uuid, **kwargs): return column -@blueprint.route("", methods=("get",)) +@bp.route(APP_PREFIX, methods=("get",)) @jwt_required @use_kwargs(column_list_query_schema) def get_list_columns(board_uuid, **kwargs): diff --git a/task_office/core/views.py b/task_office/core/views.py index 10e0a7f..3ddd172 100644 --- a/task_office/core/views.py +++ b/task_office/core/views.py @@ -3,9 +3,9 @@ from flask import Blueprint -blueprint = Blueprint("", __name__, url_prefix="/") +bp = Blueprint("", __name__, url_prefix="") -@blueprint.route("", methods=("get",)) +@bp.route("/", methods=("get",)) def root(): return {"datetime": datetime.utcnow()} diff --git a/task_office/permissions/constants.py b/task_office/permissions/constants.py new file mode 100644 index 0000000..2121431 --- /dev/null +++ b/task_office/permissions/constants.py @@ -0,0 +1,4 @@ +from task_office.boards.constants import APP_PREFIX_RETRIEVE as BOARD_PREFIX_RETRIEVE + +APP_PREFIX = BOARD_PREFIX_RETRIEVE + "/permissions" +APP_PREFIX_RETRIEVE = APP_PREFIX + "/" diff --git a/task_office/permissions/views.py b/task_office/permissions/views.py index ac22f46..a7aec99 100644 --- a/task_office/permissions/views.py +++ b/task_office/permissions/views.py @@ -2,33 +2,29 @@ import uuid from datetime import datetime -from flask import Blueprint from flask_apispec import use_kwargs, marshal_with from flask_babel import lazy_gettext as _ from flask_jwt_extended import jwt_required -from task_office.boards.constants import BOARD_RETRIEVE_URL +from .constants import APP_PREFIX, APP_PREFIX_RETRIEVE from .schemas.basic_schemas import ( permission_query_schema, permission_dump_schema, permissions_list_query_schema, permission_list_dump_schema, ) +from ..api.v1.views import bp from ..auth.utils import permission, reset_permissions from ..core.helpers.listed_response import listed_response from ..core.models.db_models import Permission, Board from ..core.utils import is_uuid, non_empty_query_required, empty_query_required from ..exceptions import InvalidUsage -blueprint = Blueprint( - "permissions", __name__, url_prefix=BOARD_RETRIEVE_URL + "/permissions" -) - -@blueprint.route("/meta", methods=("get",)) +@bp.route(APP_PREFIX + "/meta", methods=("get",)) @jwt_required @permission(required_role=Permission.Role.EDITOR.value) -def get_meta_data(board_uuid): +def get_permissions_meta_data(board_uuid): """ Additional data for Permissions """ @@ -41,7 +37,7 @@ def get_meta_data(board_uuid): return data -@blueprint.route("", methods=("post",)) +@bp.route(APP_PREFIX, methods=("post",)) @jwt_required @use_kwargs(permission_query_schema) @marshal_with(permission_dump_schema) @@ -74,7 +70,7 @@ def create_permission(board_uuid, **kwargs): return perm -@blueprint.route("/", methods=("put",)) +@bp.route(APP_PREFIX_RETRIEVE, methods=("put",)) @jwt_required @use_kwargs(permission_query_schema) @marshal_with(permission_dump_schema) @@ -108,7 +104,7 @@ def update_permission(board_uuid, permission_uuid, **kwargs): return perm -@blueprint.route("", methods=("get",)) +@bp.route(APP_PREFIX, methods=("get",)) @jwt_required @use_kwargs(permissions_list_query_schema) @permission(required_role=Permission.Role.STAFF.value) @@ -132,7 +128,7 @@ def get_list_permission(board_uuid, **kwargs): return data -@blueprint.route("/", methods=("get",)) +@bp.route(APP_PREFIX_RETRIEVE, methods=("get",)) @jwt_required @marshal_with(permission_dump_schema) @permission(required_role=Permission.Role.STAFF.value) diff --git a/task_office/settings.py b/task_office/settings.py index 5c1f691..8d8948c 100644 --- a/task_office/settings.py +++ b/task_office/settings.py @@ -34,7 +34,7 @@ class Config(object): BABEL_TRANSLATION_DIRECTORIES = os.path.join(PROJECT_ROOT, "translations") # API - API_V1_PREFIX = "/api/v1/" + API_PREFIX = "/api/v1" USE_DOCS = env.bool("USE_DOCS", False) API_SPEC = APISpec( @@ -54,7 +54,7 @@ class Config(object): # STATIC DIRS STATIC_DIR = os.path.abspath(os.path.join(PROJECT_ROOT, "static")) - STATIC_URL = API_V1_PREFIX + "/static" + STATIC_URL = API_PREFIX + "/static" # JWT JWT_AUTH_USERNAME_KEY = "uuid" diff --git a/task_office/swagger/views.py b/task_office/swagger/views.py index cf15973..f3685e2 100644 --- a/task_office/swagger/views.py +++ b/task_office/swagger/views.py @@ -6,18 +6,18 @@ from task_office.settings import app_config from task_office.swagger.api_paths import API_PATHS -SWAGGER_URL = app_config.API_V1_PREFIX + "docs" +APP_PREFIX = "/docs" +SWAGGER_URL = app_config.API_PREFIX + APP_PREFIX API_URL = SWAGGER_URL + "/open-api" -blueprint = Blueprint("docs", __name__, url_prefix=SWAGGER_URL) - -blueprint_swagger = get_swaggerui_blueprint( +bp = Blueprint("docs", __name__, url_prefix=SWAGGER_URL) +bp_swagger = get_swaggerui_blueprint( SWAGGER_URL, API_URL, config={"app_name": app_config.PROJECT_NAME} ) -@blueprint.route("/open-api", methods=("get",)) +@bp.route("/open-api", methods=("get",)) def api_swagger(**kwargs): """ :param kwargs: diff --git a/task_office/tasks/constants.py b/task_office/tasks/constants.py index 7554b79..336d8b3 100644 --- a/task_office/tasks/constants.py +++ b/task_office/tasks/constants.py @@ -1,4 +1,4 @@ -from task_office.boards.constants import BOARD_RETRIEVE_URL +from task_office.boards.constants import APP_PREFIX_RETRIEVE as BOARD_PREFIX_RETRIEVE -TASKS_PREFIX = BOARD_RETRIEVE_URL + "/tasks" -TASKS_RETRIEVE_URL = TASKS_PREFIX + "/" +APP_PREFIX = BOARD_PREFIX_RETRIEVE + "/tasks" +APP_PREFIX_RETRIEVE = APP_PREFIX + "/" diff --git a/task_office/tasks/views.py b/task_office/tasks/views.py index 0d94fbc..cd67ebf 100644 --- a/task_office/tasks/views.py +++ b/task_office/tasks/views.py @@ -1,13 +1,12 @@ """Tasks views.""" from datetime import datetime -from flask import Blueprint from flask_apispec import use_kwargs, marshal_with from flask_babel import lazy_gettext as _ from flask_jwt_extended import jwt_required, get_current_user from sqlalchemy import func -from .constants import TASKS_PREFIX +from .constants import APP_PREFIX, APP_PREFIX_RETRIEVE from .schemas.basic_schemas import ( task_list_query_schema, tasks_listed_dump_schema, @@ -18,6 +17,7 @@ columns_listed_dump_schema, ) from .utils import reset_tasks_ordering +from ..api.v1.views import bp from ..auth.utils import permission from ..core.helpers.listed_response import listed_response from ..core.models.db_models import BoardColumn, Board, Task, User, Permission @@ -25,13 +25,11 @@ from ..exceptions import InvalidUsage from ..extensions import db -blueprint = Blueprint("tasks", __name__, url_prefix=TASKS_PREFIX) - -@blueprint.route("/meta", methods=("get",)) +@bp.route(APP_PREFIX + "/meta", methods=("get",)) @jwt_required @permission(required_role=Permission.Role.STAFF.value) -def get_meta_data(board_uuid): +def get_tasks_meta_data(board_uuid): """ Additional data for tasks """ @@ -53,7 +51,7 @@ def get_meta_data(board_uuid): return data -@blueprint.route("", methods=("post",)) +@bp.route(APP_PREFIX, methods=("post",)) @jwt_required @use_kwargs(task_post_schema) @marshal_with(task_dump_schema) @@ -115,7 +113,7 @@ def create_task(board_uuid, **kwargs): return task -@blueprint.route("/", methods=("put",)) +@bp.route(APP_PREFIX_RETRIEVE, methods=("put",)) @jwt_required @use_kwargs(task_put_schema) @marshal_with(task_dump_schema) @@ -182,7 +180,7 @@ def update_task(board_uuid, task_uuid, **kwargs): return task -@blueprint.route("", methods=("get",)) +@bp.route(APP_PREFIX, methods=("get",)) @jwt_required @use_kwargs(task_list_query_schema) @permission(required_role=Permission.Role.STAFF.value) @@ -206,7 +204,7 @@ def get_list_tasks(board_uuid, **kwargs): return data -@blueprint.route("/by-columns", methods=("get",)) +@bp.route(APP_PREFIX + "/by-columns", methods=("get",)) @jwt_required @use_kwargs(task_list_by_columns_query_schema) @permission(required_role=Permission.Role.STAFF.value) From cf68e04fa9c91a9abb8d91b0f1ca0d37af828b2f Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Sat, 11 Apr 2020 13:32:31 +0300 Subject: [PATCH 43/60] refactoring v3 --- task_office/app.py | 39 +++++++-------- task_office/core/models/db_models.py | 2 +- task_office/core/models/mixins.py | 10 ++-- task_office/database.py | 2 +- task_office/extensions.py | 74 ---------------------------- task_office/extensions/__init__.py | 0 task_office/extensions/babel.py | 25 ++++++++++ task_office/extensions/bcrypt.py | 3 ++ task_office/extensions/cache.py | 8 +++ task_office/extensions/cors.py | 9 ++++ task_office/extensions/db.py | 32 ++++++++++++ task_office/extensions/jwt.py | 10 ++++ task_office/extensions/migrate.py | 7 +++ 13 files changed, 118 insertions(+), 103 deletions(-) delete mode 100644 task_office/extensions.py create mode 100644 task_office/extensions/__init__.py create mode 100644 task_office/extensions/babel.py create mode 100644 task_office/extensions/bcrypt.py create mode 100644 task_office/extensions/cache.py create mode 100644 task_office/extensions/cors.py create mode 100644 task_office/extensions/db.py create mode 100644 task_office/extensions/jwt.py create mode 100644 task_office/extensions/migrate.py diff --git a/task_office/app.py b/task_office/app.py index 43e2fff..a181884 100644 --- a/task_office/app.py +++ b/task_office/app.py @@ -1,19 +1,16 @@ """The app module, containing the app factory function.""" from flask import Flask -from task_office import ( - commands, - auth, - swagger, - boards, - permissions, - columns, - tasks, - core, -) +from task_office import commands, swagger, core +from task_office.api import v1 as api_v1 from task_office.auth.jwt_error_handlers import jwt_errors_map from task_office.exceptions import InvalidUsage -from task_office.extensions import bcrypt, cache, db, migrate, cors, jwt, babel +from task_office.extensions.babel import babel +from task_office.extensions.bcrypt import bcrypt +from task_office.extensions.cors import init_cors +from task_office.extensions.db import db +from task_office.extensions.jwt import init_jwt +from task_office.extensions.migrate import init_migrate from task_office.settings import app_config from task_office.swagger import SWAGGER_URL @@ -41,24 +38,22 @@ def create_app(config_object): def register_extensions(app): """Register Flask extensions.""" - bcrypt.init_app(app) - cache.init_app(app, config=app_config.CACHE) db.init_app(app) - jwt.init_app(app) - migrate.init_app(app, db) babel.init_app(app) + bcrypt.init_app(app) + + init_jwt(app) + init_migrate(app, db) + init_cors(app) def register_blueprints(app): """Register Flask blueprints.""" - origins = app.config.get("CORS_ORIGIN_WHITELIST", "*") - cors.init_app(auth.views.bp, origins=origins) - app.register_blueprint(auth.views.bp) - app.register_blueprint(boards.views.bp) - app.register_blueprint(permissions.views.bp) - app.register_blueprint(columns.views.bp) - app.register_blueprint(tasks.views.bp) + app.register_blueprint(api_v1.views.bp) + + # root endpoint app.register_blueprint(core.views.bp) + if app_config.USE_DOCS: app.register_blueprint(swagger.views.bp_swagger, url_prefix=SWAGGER_URL) app.register_blueprint(swagger.views.bp) diff --git a/task_office/core/models/db_models.py b/task_office/core/models/db_models.py index 3787363..b34a61e 100644 --- a/task_office/core/models/db_models.py +++ b/task_office/core/models/db_models.py @@ -5,7 +5,7 @@ from task_office.core.enums import XEnum from task_office.core.models.mixins import DTMixin, PKMixin from task_office.database import Column, Model, db, reference_col, relationship -from task_office.extensions import bcrypt +from task_office.extensions.bcrypt import bcrypt class User(PKMixin, DTMixin, Model): diff --git a/task_office/core/models/mixins.py b/task_office/core/models/mixins.py index ac61ee9..2969a2e 100644 --- a/task_office/core/models/mixins.py +++ b/task_office/core/models/mixins.py @@ -5,7 +5,7 @@ from sqlalchemy.dialects.postgresql import UUID from task_office.compat import basestring -from task_office.extensions import db +from task_office.extensions.db import db class PKMixin(object): @@ -22,15 +22,15 @@ class PKMixin(object): meta = db.Column(JSON, default=dict) @classmethod - def get_by_id(cls, record_id): + def get_by_id(cls, item_id): """Get record by ID.""" if any( ( - isinstance(record_id, basestring) and record_id.isdigit(), - isinstance(record_id, (int, float)), + isinstance(item_id, basestring) and item_id.isdigit(), + isinstance(item_id, (int, float)), ) ): - return cls.query.filter_by(id=record_id).first() + return cls.query.filter_by(id=item_id).first() def hexed_uuid(self): return uuid.UUID(str(self.uuid)).hex diff --git a/task_office/database.py b/task_office/database.py index fea5875..664d2dd 100644 --- a/task_office/database.py +++ b/task_office/database.py @@ -2,7 +2,7 @@ from sqlalchemy.orm import relationship -from .extensions import db +from .extensions.db import db # Alias common SQLAlchemy names Column = db.Column diff --git a/task_office/extensions.py b/task_office/extensions.py deleted file mode 100644 index 1aef84c..0000000 --- a/task_office/extensions.py +++ /dev/null @@ -1,74 +0,0 @@ -"""Extensions module. Each extension is initialized in the app factory located in app.py.""" -from flask import g, request -from flask_babel import Babel -from flask_bcrypt import Bcrypt -from flask_caching import Cache -from flask_cors import CORS -from flask_jwt_extended import JWTManager -from flask_migrate import Migrate -from flask_sqlalchemy import SQLAlchemy, Model - -from task_office.settings import app_config -from task_office.utils import jwt_identity, identity_loader - - -class CRUDMixin(Model): - """Mixin that adds convenience methods for CRUD (create, read, update, delete) operations.""" - - @classmethod - def create(cls, **kwargs): - """Create a new record and save it the database.""" - instance = cls(**kwargs) - return instance.save() - - def update(self, commit=True, **kwargs): - """Update specific fields of a record.""" - for attr, value in kwargs.items(): - setattr(self, attr, value) - return commit and self.save() or self - - def save(self, commit=True): - """Save the record.""" - db.session.add(self) - if commit: - db.session.commit() - return self - - def delete(self, commit=True): - """Remove the record from the database.""" - db.session.delete(self) - return commit and db.session.commit() - - -bcrypt = Bcrypt() -db = SQLAlchemy(model_class=CRUDMixin) -migrate = Migrate() -cache = Cache() -cors = CORS() - -# JWT -# ------------------------------------------------------------------------------ -jwt = JWTManager() -jwt.user_loader_callback_loader(jwt_identity) -jwt.user_identity_loader(identity_loader) - -# Babel -# https://pythonhosted.org/Flask-Babel/ -# ------------------------------------------------------------------------------ -babel = Babel(default_locale=app_config.LOCALE, default_timezone=app_config.TIME_ZONE) - - -@babel.localeselector -def get_locale(): - # if a user is logged in, use the locale from the user settings - user = getattr(g, "user", None) - if user is not None: - return user.locale - return request.accept_languages.best_match(list(app_config.LANGUAGES.keys())) - - -@babel.timezoneselector -def get_timezone(): - user = getattr(g, "user", None) - if user is not None: - return user.timezone diff --git a/task_office/extensions/__init__.py b/task_office/extensions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/task_office/extensions/babel.py b/task_office/extensions/babel.py new file mode 100644 index 0000000..7d4c6f6 --- /dev/null +++ b/task_office/extensions/babel.py @@ -0,0 +1,25 @@ +# Babel +# https://pythonhosted.org/Flask-Babel/ +# ------------------------------------------------------------------------------ +from flask import request, g +from flask_babel import Babel + +from task_office.settings import app_config + +babel = Babel(default_locale=app_config.LOCALE, default_timezone=app_config.TIME_ZONE) + + +@babel.localeselector +def get_locale(): + # if a user is logged in, use the locale from the user settings + user = getattr(g, "user", None) + if user is not None: + return user.locale + return request.accept_languages.best_match(list(app_config.LANGUAGES.keys())) + + +@babel.timezoneselector +def get_timezone(): + user = getattr(g, "user", None) + if user is not None: + return user.timezone diff --git a/task_office/extensions/bcrypt.py b/task_office/extensions/bcrypt.py new file mode 100644 index 0000000..ac60495 --- /dev/null +++ b/task_office/extensions/bcrypt.py @@ -0,0 +1,3 @@ +from flask_bcrypt import Bcrypt + +bcrypt = Bcrypt() diff --git a/task_office/extensions/cache.py b/task_office/extensions/cache.py new file mode 100644 index 0000000..6b612aa --- /dev/null +++ b/task_office/extensions/cache.py @@ -0,0 +1,8 @@ +from flask import Flask +from flask_caching import Cache + +from task_office.settings import app_config + + +def init_cache(app: Flask): + cache = Cache(app=app, config=app_config.CACHE) diff --git a/task_office/extensions/cors.py b/task_office/extensions/cors.py new file mode 100644 index 0000000..cd66f9d --- /dev/null +++ b/task_office/extensions/cors.py @@ -0,0 +1,9 @@ +from flask import Flask +from flask_cors import CORS + + +def init_cors(app: Flask): + cors = CORS( + app, + resources={r"/*": {"origins": app.config.get("CORS_ORIGIN_WHITELIST", "*")}}, + ) diff --git a/task_office/extensions/db.py b/task_office/extensions/db.py new file mode 100644 index 0000000..837f35f --- /dev/null +++ b/task_office/extensions/db.py @@ -0,0 +1,32 @@ +from flask_sqlalchemy import SQLAlchemy, Model + + +class CRUDMixin(Model): + """Mixin that adds convenience methods for CRUD (create, read, update, delete) operations.""" + + @classmethod + def create(cls, **kwargs): + """Create a new item and save it the database.""" + instance = cls(**kwargs) + return instance.save() + + def update(self, commit=True, **kwargs): + """Update specific fields of a item.""" + for attr, value in kwargs.items(): + setattr(self, attr, value) + return commit and self.save() or self + + def save(self, commit=True): + """Save the item.""" + db.session.add(self) + if commit: + db.session.commit() + return self + + def delete(self, commit=True): + """Remove the item from the database.""" + db.session.delete(self) + return commit and db.session.commit() + + +db = SQLAlchemy(model_class=CRUDMixin) diff --git a/task_office/extensions/jwt.py b/task_office/extensions/jwt.py new file mode 100644 index 0000000..389a75f --- /dev/null +++ b/task_office/extensions/jwt.py @@ -0,0 +1,10 @@ +from flask import Flask +from flask_jwt_extended import JWTManager + +from task_office.utils import jwt_identity, identity_loader + + +def init_jwt(app: Flask): + jwt = JWTManager(app=app) + jwt.user_loader_callback_loader(jwt_identity) + jwt.user_identity_loader(identity_loader) diff --git a/task_office/extensions/migrate.py b/task_office/extensions/migrate.py new file mode 100644 index 0000000..022cc5d --- /dev/null +++ b/task_office/extensions/migrate.py @@ -0,0 +1,7 @@ +from flask import Flask +from flask_migrate import Migrate +from flask_sqlalchemy import SQLAlchemy + + +def init_migrate(app: Flask, db: SQLAlchemy): + migrate = Migrate(app=app, db=db) From a2d867e63325da97935ca94eeb9c89707f6d5629 Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Sun, 19 Apr 2020 14:29:42 +0300 Subject: [PATCH 44/60] init tests --- requirements/dev.txt | 9 ++++- task_office/core/models/db_models.py | 2 +- task_office/extensions/jwt.py | 14 +++++-- task_office/utils.py | 11 ------ tests/__init__.py | 0 tests/conftest.py | 56 ++++++++++++++++++++++++++++ tests/factories.py | 28 ++++++++++++++ 7 files changed, 104 insertions(+), 16 deletions(-) delete mode 100644 task_office/utils.py create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/factories.py diff --git a/requirements/dev.txt b/requirements/dev.txt index 7c31ffa..03a7f7e 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,7 +1,14 @@ -r ./base.txt +# API docs flask-swagger-ui==3.20.9 apispec>=1.0.0 apispec-webframeworks flask_apispec -pyyaml \ No newline at end of file +pyyaml + +# Tests +pytest +factory-boy +WebTest +Faker \ No newline at end of file diff --git a/task_office/core/models/db_models.py b/task_office/core/models/db_models.py index b34a61e..4519814 100644 --- a/task_office/core/models/db_models.py +++ b/task_office/core/models/db_models.py @@ -147,7 +147,7 @@ class State(XEnum): creator_uuid = reference_col("users", pk_name="uuid", nullable=False) creator = relationship("User", backref=db.backref("tasks")) - performers = db.relationship( + performers = relationship( "User", secondary=users_tasks, lazy="subquery", diff --git a/task_office/extensions/jwt.py b/task_office/extensions/jwt.py index 389a75f..13e897d 100644 --- a/task_office/extensions/jwt.py +++ b/task_office/extensions/jwt.py @@ -1,10 +1,18 @@ from flask import Flask from flask_jwt_extended import JWTManager -from task_office.utils import jwt_identity, identity_loader + +def _jwt_identity(identifier): + from task_office.auth import User + + return User.get_by_id(identifier) + + +def _identity_loader(user): + return user.id def init_jwt(app: Flask): jwt = JWTManager(app=app) - jwt.user_loader_callback_loader(jwt_identity) - jwt.user_identity_loader(identity_loader) + jwt.user_loader_callback_loader(_jwt_identity) + jwt.user_identity_loader(_identity_loader) diff --git a/task_office/utils.py b/task_office/utils.py deleted file mode 100644 index 735a4c5..0000000 --- a/task_office/utils.py +++ /dev/null @@ -1,11 +0,0 @@ -"""Helper utilities and decorators.""" - - -def jwt_identity(identifier): - from task_office.auth import User - - return User.get_by_id(identifier) - - -def identity_loader(user): - return user.id diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..2f0e790 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,56 @@ +import pytest +from webtest import TestApp + +from task_office.app import create_app +from task_office.extensions.db import db as _db +from task_office.settings import app_config +from .factories import UserFactory + + +@pytest.yield_fixture(scope="function") +def app(): + """An application for the tests.""" + _app = create_app(app_config) + + with _app.app_context(): + _db.create_all() + + ctx = _app.test_request_context() + ctx.push() + + yield _app + + ctx.pop() + + +@pytest.fixture(scope="function") +def testapp(app): + """A Webtest app.""" + return TestApp(app) + + +@pytest.yield_fixture(scope="function") +def db(app): + """A database for the tests.""" + _db.app = app + with app.app_context(): + _db.create_all() + + yield _db + + # Explicitly close DB connection + _db.session.close() + _db.drop_all() + + +@pytest.fixture +def user(db): + """A user for the tests.""" + + class User: + def get(self): + user = UserFactory() + db.session.commit() + return user + + return User() diff --git a/tests/factories.py b/tests/factories.py new file mode 100644 index 0000000..89f2611 --- /dev/null +++ b/tests/factories.py @@ -0,0 +1,28 @@ +from factory import PostGenerationMethodCall, Sequence +from factory.alchemy import SQLAlchemyModelFactory + +from task_office.core.models.db_models import User +from task_office.extensions.db import db as _db + + +class BaseFactory(SQLAlchemyModelFactory): + """Base factory.""" + + class Meta: + """Factory configuration.""" + + abstract = True + sqlalchemy_session = _db.session + + +class UserFactory(BaseFactory): + """User factory.""" + + username = Sequence(lambda n: "user{0}".format(n)) + email = Sequence(lambda n: "user{0}@example.com".format(n)) + password = PostGenerationMethodCall("set_password", "password") + + class Meta: + """Factory configuration.""" + + model = User From b15bfecc4b25c1a2c48b2fce8873e6b0ac4832b9 Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Mon, 20 Apr 2020 09:49:39 +0300 Subject: [PATCH 45/60] core utils tests, light refactoring --- pytest.ini | 2 + requirements/dev.txt | 3 +- task_office/settings.py | 19 +++++++- tests/conftest.py | 16 ++----- tests/factories.py | 18 ++++++- tests/fixtures/__init__.py | 0 tests/fixtures/model_fixtures.py | 78 +++++++++++++++++++++++++++++++ tests/integration/__init__.py | 0 tests/unit/__init__.py | 0 tests/unit/app/__init__.py | 0 tests/unit/app/core/__init__.py | 0 tests/unit/app/core/fixtures.py | 29 ++++++++++++ tests/unit/app/core/test_utils.py | 66 ++++++++++++++++++++++++++ 13 files changed, 216 insertions(+), 15 deletions(-) create mode 100644 pytest.ini create mode 100644 tests/fixtures/__init__.py create mode 100644 tests/fixtures/model_fixtures.py create mode 100644 tests/integration/__init__.py create mode 100644 tests/unit/__init__.py create mode 100644 tests/unit/app/__init__.py create mode 100644 tests/unit/app/core/__init__.py create mode 100644 tests/unit/app/core/fixtures.py create mode 100644 tests/unit/app/core/test_utils.py diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..c1fa878 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +addopts = -p no:warnings \ No newline at end of file diff --git a/requirements/dev.txt b/requirements/dev.txt index 03a7f7e..4a2c857 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -8,7 +8,8 @@ flask_apispec pyyaml # Tests -pytest +pytest>=5.4.0 +pytest-mock>=3.1.0 factory-boy WebTest Faker \ No newline at end of file diff --git a/task_office/settings.py b/task_office/settings.py index 8d8948c..24b2874 100644 --- a/task_office/settings.py +++ b/task_office/settings.py @@ -109,8 +109,25 @@ class DevConfig(Config): """Development configuration.""" +class TestConfig(Config): + """Test configuration.""" + + DATABASE = Config.DATABASE + DATABASE["DB_NAME"] = env.str("POSTGRES_DB_TEST", "task_office_test") + SQLALCHEMY_DATABASE_URI = "postgresql://{username}:{password}@{host}:{db_port}/{db_name}".format( + username=DATABASE["DB_USER"], + password=DATABASE["DB_PASSWORD"], + host=DATABASE["DB_HOST"], + db_port=DATABASE["DB_PORT"], + db_name=DATABASE["DB_NAME"], + ) + CACHE = { + "CACHE_TYPE": "simple", + } + + MODE = os.environ.get("MODE", default="dev") -CONFIG_SETS = {"dev": DevConfig, "prod": ProdConfig} +CONFIG_SETS = {"prod": ProdConfig, "dev": DevConfig, "test": TestConfig} app_config = CONFIG_SETS.get(MODE) diff --git a/tests/conftest.py b/tests/conftest.py index 2f0e790..991830e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,7 +4,6 @@ from task_office.app import create_app from task_office.extensions.db import db as _db from task_office.settings import app_config -from .factories import UserFactory @pytest.yield_fixture(scope="function") @@ -43,14 +42,7 @@ def db(app): _db.drop_all() -@pytest.fixture -def user(db): - """A user for the tests.""" - - class User: - def get(self): - user = UserFactory() - db.session.commit() - return user - - return User() +pytest_plugins = [ + "tests.fixtures.model_fixtures", + "tests.unit.app.core.fixtures", +] diff --git a/tests/factories.py b/tests/factories.py index 89f2611..189e926 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -1,13 +1,17 @@ +import uuid + from factory import PostGenerationMethodCall, Sequence from factory.alchemy import SQLAlchemyModelFactory -from task_office.core.models.db_models import User +from task_office.core.models.db_models import User, Board from task_office.extensions.db import db as _db class BaseFactory(SQLAlchemyModelFactory): """Base factory.""" + uuid = Sequence(lambda n: uuid.uuid4().hex) + class Meta: """Factory configuration.""" @@ -26,3 +30,15 @@ class Meta: """Factory configuration.""" model = User + + +class BoardFactory(BaseFactory): + """Board factory.""" + + name = Sequence(lambda n: "Board Name #{0}".format(n)) + description = Sequence(lambda n: "Board Description #{0}".format(n)) + + class Meta: + """Factory configuration.""" + + model = Board diff --git a/tests/fixtures/__init__.py b/tests/fixtures/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/model_fixtures.py b/tests/fixtures/model_fixtures.py new file mode 100644 index 0000000..d0ab6a4 --- /dev/null +++ b/tests/fixtures/model_fixtures.py @@ -0,0 +1,78 @@ +import pytest + +from tests.factories import UserFactory, BoardFactory + + +@pytest.fixture +def session_users(): + """A users for the tests.""" + users = UserFactory.create_batch(3) + + class User: + @staticmethod + def get_single(): + return users[0] + + @staticmethod + def get_list(): + return users + + return User() + + +@pytest.fixture(scope="function") +def function_users(db): + """A users for the tests.""" + users = UserFactory.create_batch(3) + db.session.commit() + + class User: + @staticmethod + def get_single(): + return users[0] + + @staticmethod + def get_list(): + return users + + return User() + + +@pytest.fixture +def session_boards(session_users): + """A boards for the tests.""" + user = session_users.get_single() + boards = BoardFactory.create_batch(3) + for item in boards: + item.user = user + item.save() + + class Board: + @staticmethod + def get_single(): + return boards[0] + + @staticmethod + def get_list(): + return boards + + return Board() + + +@pytest.fixture(scope="function") +def function_boards(function_users, db): + """A boards for the tests.""" + user = function_users.get_single() + boards = BoardFactory.create_batch(5, owner_uuid=str(user.uuid)) + db.session.commit() + + class Boardx: + @staticmethod + def get_single(): + return boards[0] + + @staticmethod + def get_list(): + return boards + + return Boardx() diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/app/__init__.py b/tests/unit/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/app/core/__init__.py b/tests/unit/app/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/app/core/fixtures.py b/tests/unit/app/core/fixtures.py new file mode 100644 index 0000000..bd1c7c8 --- /dev/null +++ b/tests/unit/app/core/fixtures.py @@ -0,0 +1,29 @@ +import pytest + +VALID_UUID_LIST = [ + "31b2e088-d37b-4e90-9983-1ef6f44b932a", + "31b2e088d37b4e9099831ef6f44b932a", +] + +INVALID_UUID_LIST = [ + "31b2e088-d37b-4e90-9983-1ef6f44b932z", + "12345678910", + "ZXsdpk12-21-zxczxcl-58484848-zx-xcxc", +] + +INVALID_BOARDS_QUERY_DATA = [{"uuid": item} for item in VALID_UUID_LIST] + + +@pytest.fixture(params=VALID_UUID_LIST) +def valid_uuid_list(request): + return request.param + + +@pytest.fixture(params=INVALID_UUID_LIST) +def invalid_uuid_list(request): + return request.param + + +@pytest.fixture(params=INVALID_BOARDS_QUERY_DATA) +def invalid_boards_query_list(request, db): + return request.param diff --git a/tests/unit/app/core/test_utils.py b/tests/unit/app/core/test_utils.py new file mode 100644 index 0000000..364f521 --- /dev/null +++ b/tests/unit/app/core/test_utils.py @@ -0,0 +1,66 @@ +import uuid + +import pytest +from flask import request + +from task_office.core.models.db_models import Board +from task_office.core.utils import ( + is_uuid, + non_empty_query_required, + empty_query_required, + validate_request_url_uuid, +) +from task_office.exceptions import InvalidUsage + + +def test_is_uuid_success(valid_uuid_list): + assert is_uuid(valid_uuid_list) is True + + +def test_is_uuid_failed(invalid_uuid_list): + assert is_uuid(invalid_uuid_list) is False + + +def test_non_empty_query_required_success(function_boards): + non_empty_query_required(Board, name=function_boards.get_single().name) + + +def test_non_empty_query_required_failed(invalid_boards_query_list): + with pytest.raises(InvalidUsage): + non_empty_query_required(Board, **invalid_boards_query_list) + + +def test_empty_query_required_success(invalid_boards_query_list): + empty_query_required(Board, **invalid_boards_query_list) + + +def test_empty_query_required_failed(function_boards): + with pytest.raises(InvalidUsage): + empty_query_required(Board, name=function_boards.get_single().name) + + +def test_request_url_uuid_success( + function_boards, testapp, +): + for item in function_boards.get_list(): + uuid_hexed = uuid.UUID(str(item.uuid)).hex + url = f"http://some-host/api/v1/boards/{uuid_hexed}/" + request.url = url + validate_request_url_uuid(Board, "uuid", uuid_hexed, True) + + +def test_request_url_uuid_failed_case1(invalid_uuid_list, testapp): + with pytest.raises(InvalidUsage): + uuid_hexed = invalid_uuid_list + url = f"http://some-host/api/v1/boards/{uuid_hexed}/" + request.url = url + validate_request_url_uuid(Board, "uuid", uuid_hexed, False) + + +def test_request_url_uuid_failed_case2(testapp): + for item in range(2): + with pytest.raises(InvalidUsage): + uuid_hexed = uuid.uuid4().hex + url = f"http://some-host/api/v1/boards/{uuid_hexed}/" + request.url = url + validate_request_url_uuid(Board, "uuid", uuid_hexed, True) From 107bd37074dea000cdf45eda17b5f323f2b71b38 Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Sat, 25 Apr 2020 12:16:46 +0300 Subject: [PATCH 46/60] enabled pytest-cov --- README.rst | 3 +++ requirements/dev.txt | 1 + 2 files changed, 4 insertions(+) diff --git a/README.rst b/README.rst index 21adb11..52b9659 100644 --- a/README.rst +++ b/README.rst @@ -61,3 +61,6 @@ Translations commands:: pybabel compile -d translations +Run tests:: + + pytest --cov=task_office tests/ \ No newline at end of file diff --git a/requirements/dev.txt b/requirements/dev.txt index 4a2c857..c53a4bb 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -10,6 +10,7 @@ pyyaml # Tests pytest>=5.4.0 pytest-mock>=3.1.0 +pytest-cov>=2.8.1 factory-boy WebTest Faker \ No newline at end of file From f82a37d06a94c274f1f1e73e3dd8cc82fc00473e Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Sat, 25 Apr 2020 17:24:45 +0300 Subject: [PATCH 47/60] implemented auth tests, light refactor --- README.rst | 3 +- task_office/auth/constants.py | 2 + task_office/auth/views.py | 3 +- tests/conftest.py | 1 + tests/factories.py | 5 +- tests/fixtures/model_fixtures.py | 17 --- tests/integration/app/__init__.py | 0 tests/integration/app/auth/__init__.py | 0 tests/integration/app/auth/fixtures.py | 77 ++++++++++++++ tests/integration/app/auth/test_api_auth.py | 111 ++++++++++++++++++++ tests/unit/app/auth/__init__.py | 0 tests/unit/app/auth/test_utils.py | 3 + 12 files changed, 201 insertions(+), 21 deletions(-) create mode 100644 task_office/auth/constants.py create mode 100644 tests/integration/app/__init__.py create mode 100644 tests/integration/app/auth/__init__.py create mode 100644 tests/integration/app/auth/fixtures.py create mode 100644 tests/integration/app/auth/test_api_auth.py create mode 100644 tests/unit/app/auth/__init__.py create mode 100644 tests/unit/app/auth/test_utils.py diff --git a/README.rst b/README.rst index 52b9659..7fd3768 100644 --- a/README.rst +++ b/README.rst @@ -63,4 +63,5 @@ Translations commands:: Run tests:: - pytest --cov=task_office tests/ \ No newline at end of file + pytest -s -v --cov=task_office tests/ + diff --git a/task_office/auth/constants.py b/task_office/auth/constants.py new file mode 100644 index 0000000..a64a485 --- /dev/null +++ b/task_office/auth/constants.py @@ -0,0 +1,2 @@ +APP_PREFIX = "/auth" +APP_PREFIX_RETRIEVE = APP_PREFIX diff --git a/task_office/auth/views.py b/task_office/auth/views.py index 5f96340..ec8f24b 100644 --- a/task_office/auth/views.py +++ b/task_office/auth/views.py @@ -9,6 +9,7 @@ get_jwt_identity, ) +from .constants import APP_PREFIX from .schemas import ( user_schema, user_signup_schema, @@ -20,8 +21,6 @@ from ..core.models.db_models import User from ..settings import app_config -APP_PREFIX = "/auth" - @bp.route(APP_PREFIX + "/sign-up", methods=("post",)) @use_kwargs(user_signup_schema) diff --git a/tests/conftest.py b/tests/conftest.py index 991830e..e45cd04 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -45,4 +45,5 @@ def db(app): pytest_plugins = [ "tests.fixtures.model_fixtures", "tests.unit.app.core.fixtures", + "tests.integration.app.auth.fixtures", ] diff --git a/tests/factories.py b/tests/factories.py index 189e926..e95e8b2 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -19,12 +19,15 @@ class Meta: sqlalchemy_session = _db.session +USER_FACTORY_DEFAULT_PASSWORD = "password" + + class UserFactory(BaseFactory): """User factory.""" username = Sequence(lambda n: "user{0}".format(n)) email = Sequence(lambda n: "user{0}@example.com".format(n)) - password = PostGenerationMethodCall("set_password", "password") + password = PostGenerationMethodCall("set_password", USER_FACTORY_DEFAULT_PASSWORD) class Meta: """Factory configuration.""" diff --git a/tests/fixtures/model_fixtures.py b/tests/fixtures/model_fixtures.py index d0ab6a4..b2652f4 100644 --- a/tests/fixtures/model_fixtures.py +++ b/tests/fixtures/model_fixtures.py @@ -3,23 +3,6 @@ from tests.factories import UserFactory, BoardFactory -@pytest.fixture -def session_users(): - """A users for the tests.""" - users = UserFactory.create_batch(3) - - class User: - @staticmethod - def get_single(): - return users[0] - - @staticmethod - def get_list(): - return users - - return User() - - @pytest.fixture(scope="function") def function_users(db): """A users for the tests.""" diff --git a/tests/integration/app/__init__.py b/tests/integration/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/app/auth/__init__.py b/tests/integration/app/auth/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/app/auth/fixtures.py b/tests/integration/app/auth/fixtures.py new file mode 100644 index 0000000..0dadfa9 --- /dev/null +++ b/tests/integration/app/auth/fixtures.py @@ -0,0 +1,77 @@ +import pytest + +SIGN_UP_USERS_VALID_DATA = [ + { + "password_confirm": "user11", + "password": "user11", + "email": "user1@gmaill.com", + "username": "usr1", + }, + { + "password_confirm": "user5678910124556122345811111111", + "password": "user5678910124556122345811111111", + "email": "user2@gmaill.com", + "username": "usr2", + }, + { + "password_confirm": "user11", + "password": "user11", + "email": "user3@gmaill.com", + "username": "usr3123456usr3123456usr3123456usr3123456usr3123456usr3123456usr3123456usr3123456", + }, +] + +SIGN_UP_USERS_INVALID_DATA = [ + # to short password + { + "password_confirm": "us", + "password": "us", + "email": "user1@gmaill.com", + "username": "usr1", + }, + # invalid email + { + "password_confirm": "user11", + "password": "user11", + "email": "user1gmaill.com", + "username": "usr1", + }, + # to short username + { + "password_confirm": "user11", + "password": "user11", + "email": "user1@gmaill.com", + "username": "us", + }, + # to long password + { + "password_confirm": "user56789101245561223458111111111", + "password": "user56789101245561223458111111111", + "email": "user2@gmaill.com", + "username": "usr2", + }, + # to long username + { + "password_confirm": "user11", + "password": "user11", + "email": "user3@gmaill.com", + "username": "usr3123456usr3123456usr3123456usr3123456usr3123456usr3123456usr3123456usr31234561", + }, + # passwords not equal + { + "password_confirm": "user12", + "password": "user11", + "email": "user3@gmaill.com", + "username": "usr3123456usr3123456usr3123456usr3123456usr3123456usr3123456usr3123456usr3123456", + }, +] + + +@pytest.fixture(params=SIGN_UP_USERS_VALID_DATA) +def valid_sign_up_users_valid_data(request): + return request.param + + +@pytest.fixture(params=SIGN_UP_USERS_INVALID_DATA) +def valid_sign_up_users_invalid_data(request): + return request.param diff --git a/tests/integration/app/auth/test_api_auth.py b/tests/integration/app/auth/test_api_auth.py new file mode 100644 index 0000000..47b8913 --- /dev/null +++ b/tests/integration/app/auth/test_api_auth.py @@ -0,0 +1,111 @@ +import uuid + +import pytest +import webtest +from flask import url_for + +from tests.factories import USER_FACTORY_DEFAULT_PASSWORD + + +def test_sign_up_success(testapp, valid_sign_up_users_valid_data): + url = url_for("api_v1.sign_up") + resp = testapp.post_json(url, valid_sign_up_users_valid_data) + assert resp.status == "200 OK" + + +def test_sign_up_failed(testapp, valid_sign_up_users_invalid_data): + url = url_for("api_v1.sign_up") + with pytest.raises(webtest.app.AppError): + resp = testapp.post_json(url, valid_sign_up_users_invalid_data) + assert "422 UNPROCESSABLE ENTITY" == resp.status + + +def test_sign_up_already_exists_username(testapp, function_users): + available_username = function_users.get_single().username + + data = { + "password_confirm": "user11", + "password": "user11", + "email": "user1@gmaill.com", + "username": available_username, + } + + url = url_for("api_v1.sign_up") + with pytest.raises(webtest.app.AppError): + resp = testapp.post_json(url, data) + assert "422 UNPROCESSABLE ENTITY" == resp.status + + +def test_sign_up_already_exists_email(testapp, function_users): + available_email = function_users.get_single().email + + data = { + "password_confirm": "user11", + "password": "user11", + "email": available_email, + "username": "user", + } + + url = url_for("api_v1.sign_up") + with pytest.raises(webtest.app.AppError): + resp = testapp.post_json(url, data) + assert "422 UNPROCESSABLE ENTITY" == resp.status + + +def test_sign_up_in_success(testapp, valid_sign_up_users_valid_data): + # sign up + sign_up_url = url_for("api_v1.sign_up") + resp = testapp.post_json(sign_up_url, valid_sign_up_users_valid_data) + assert resp.status == "200 OK" + + # sign in + sign_in_url = url_for("api_v1.sign_in") + data = { + "email": valid_sign_up_users_valid_data["email"], + "password": valid_sign_up_users_valid_data["password"], + } + resp = testapp.post_json(sign_in_url, data) + assert resp.status == "200 OK" + + +def test_sign_in_data__with_valid_data(testapp, function_users): + sign_in_url = url_for("api_v1.sign_in") + data = { + "email": function_users.get_single().email, + "password": USER_FACTORY_DEFAULT_PASSWORD, + } + resp = testapp.post_json(sign_in_url, data) + assert resp.status == "200 OK" + + +def test_sign_in_data_with_invalid_data(testapp,): + sign_in_url = url_for("api_v1.sign_in") + with pytest.raises(webtest.app.AppError): + data = { + "email": f"user{uuid.uuid4().hex}@gmail.com", + "password": "some_wrong_password", + } + resp = testapp.post_json(sign_in_url, data) + assert resp.status == "422 UNPROCESSABLE ENTITY" + + +def test_sign_in_data_with_invalid_email(testapp, function_users): + sign_in_url = url_for("api_v1.sign_in") + with pytest.raises(webtest.app.AppError): + data = { + "email": "user{}@gmail.com".format(uuid.uuid4().hex), + "password": USER_FACTORY_DEFAULT_PASSWORD, + } + resp = testapp.post_json(sign_in_url, data) + assert resp.status == "422 UNPROCESSABLE ENTITY" + + +def test_sign_in_data__with_invalid_password(testapp, function_users): + sign_in_url = url_for("api_v1.sign_in") + with pytest.raises(webtest.app.AppError): + data = { + "email": function_users.get_single().email, + "password": "some_wrong_password", + } + resp = testapp.post_json(sign_in_url, data) + assert resp.status == "422 UNPROCESSABLE ENTITY" diff --git a/tests/unit/app/auth/__init__.py b/tests/unit/app/auth/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/app/auth/test_utils.py b/tests/unit/app/auth/test_utils.py new file mode 100644 index 0000000..f8d3907 --- /dev/null +++ b/tests/unit/app/auth/test_utils.py @@ -0,0 +1,3 @@ +# TODO(Medniy2000) Implement auth utils test cases: +# _get_cached_permissions reset_permissions, +# permission, reset_permissions_for_board_staff From ad26898984109baf34895c3f529da8c43672765b Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Sat, 25 Apr 2020 18:57:02 +0300 Subject: [PATCH 48/60] light refactor, init boards tests --- task_office/boards/views.py | 2 +- tests/fixtures/model_fixtures.py | 8 +- tests/integration/app/auth/test_api_auth.py | 80 +++++++------------ tests/integration/app/boards/__init__.py | 0 .../integration/app/boards/test_api_boards.py | 45 +++++++++++ tests/unit/app/core/test_utils.py | 12 +-- 6 files changed, 87 insertions(+), 60 deletions(-) create mode 100644 tests/integration/app/boards/__init__.py create mode 100644 tests/integration/app/boards/test_api_boards.py diff --git a/task_office/boards/views.py b/task_office/boards/views.py index 47d1cd1..0b31333 100644 --- a/task_office/boards/views.py +++ b/task_office/boards/views.py @@ -29,7 +29,7 @@ @jwt_required @use_kwargs(board_action_schema) @marshal_with(board_dump_schema) -def create_boards(**kwargs): +def create_board(**kwargs): data = kwargs # validate current user diff --git a/tests/fixtures/model_fixtures.py b/tests/fixtures/model_fixtures.py index b2652f4..9d25b6f 100644 --- a/tests/fixtures/model_fixtures.py +++ b/tests/fixtures/model_fixtures.py @@ -4,7 +4,7 @@ @pytest.fixture(scope="function") -def function_users(db): +def func_users(db): """A users for the tests.""" users = UserFactory.create_batch(3) db.session.commit() @@ -22,7 +22,7 @@ def get_list(): @pytest.fixture -def session_boards(session_users): +def ses_boards(session_users): """A boards for the tests.""" user = session_users.get_single() boards = BoardFactory.create_batch(3) @@ -43,9 +43,9 @@ def get_list(): @pytest.fixture(scope="function") -def function_boards(function_users, db): +def func_boards(func_users, db): """A boards for the tests.""" - user = function_users.get_single() + user = func_users.get_single() boards = BoardFactory.create_batch(5, owner_uuid=str(user.uuid)) db.session.commit() diff --git a/tests/integration/app/auth/test_api_auth.py b/tests/integration/app/auth/test_api_auth.py index 47b8913..c92770d 100644 --- a/tests/integration/app/auth/test_api_auth.py +++ b/tests/integration/app/auth/test_api_auth.py @@ -1,7 +1,5 @@ import uuid -import pytest -import webtest from flask import url_for from tests.factories import USER_FACTORY_DEFAULT_PASSWORD @@ -9,19 +7,16 @@ def test_sign_up_success(testapp, valid_sign_up_users_valid_data): url = url_for("api_v1.sign_up") - resp = testapp.post_json(url, valid_sign_up_users_valid_data) - assert resp.status == "200 OK" + testapp.post_json(url, valid_sign_up_users_valid_data, status=200) def test_sign_up_failed(testapp, valid_sign_up_users_invalid_data): url = url_for("api_v1.sign_up") - with pytest.raises(webtest.app.AppError): - resp = testapp.post_json(url, valid_sign_up_users_invalid_data) - assert "422 UNPROCESSABLE ENTITY" == resp.status + testapp.post_json(url, valid_sign_up_users_invalid_data, status=422) -def test_sign_up_already_exists_username(testapp, function_users): - available_username = function_users.get_single().username +def test_sign_up_already_exists_username(testapp, func_users): + available_username = func_users.get_single().username data = { "password_confirm": "user11", @@ -31,13 +26,11 @@ def test_sign_up_already_exists_username(testapp, function_users): } url = url_for("api_v1.sign_up") - with pytest.raises(webtest.app.AppError): - resp = testapp.post_json(url, data) - assert "422 UNPROCESSABLE ENTITY" == resp.status + testapp.post_json(url, data, status=422) -def test_sign_up_already_exists_email(testapp, function_users): - available_email = function_users.get_single().email +def test_sign_up_already_exists_email(testapp, func_users): + available_email = func_users.get_single().email data = { "password_confirm": "user11", @@ -47,16 +40,13 @@ def test_sign_up_already_exists_email(testapp, function_users): } url = url_for("api_v1.sign_up") - with pytest.raises(webtest.app.AppError): - resp = testapp.post_json(url, data) - assert "422 UNPROCESSABLE ENTITY" == resp.status + testapp.post_json(url, data, status=422) def test_sign_up_in_success(testapp, valid_sign_up_users_valid_data): # sign up sign_up_url = url_for("api_v1.sign_up") - resp = testapp.post_json(sign_up_url, valid_sign_up_users_valid_data) - assert resp.status == "200 OK" + testapp.post_json(sign_up_url, valid_sign_up_users_valid_data, status=200) # sign in sign_in_url = url_for("api_v1.sign_in") @@ -64,48 +54,40 @@ def test_sign_up_in_success(testapp, valid_sign_up_users_valid_data): "email": valid_sign_up_users_valid_data["email"], "password": valid_sign_up_users_valid_data["password"], } - resp = testapp.post_json(sign_in_url, data) - assert resp.status == "200 OK" + testapp.post_json(sign_in_url, data, status=200) -def test_sign_in_data__with_valid_data(testapp, function_users): +def test_sign_in_data__with_valid_data(testapp, func_users): sign_in_url = url_for("api_v1.sign_in") data = { - "email": function_users.get_single().email, + "email": func_users.get_single().email, "password": USER_FACTORY_DEFAULT_PASSWORD, } - resp = testapp.post_json(sign_in_url, data) - assert resp.status == "200 OK" + testapp.post_json(sign_in_url, data, status=200) -def test_sign_in_data_with_invalid_data(testapp,): +def test_sign_in_data_with_invalid_data(testapp): sign_in_url = url_for("api_v1.sign_in") - with pytest.raises(webtest.app.AppError): - data = { - "email": f"user{uuid.uuid4().hex}@gmail.com", - "password": "some_wrong_password", - } - resp = testapp.post_json(sign_in_url, data) - assert resp.status == "422 UNPROCESSABLE ENTITY" + data = { + "email": f"user{uuid.uuid4().hex}@gmail.com", + "password": "some_wrong_password", + } + testapp.post_json(sign_in_url, data, status=422) -def test_sign_in_data_with_invalid_email(testapp, function_users): +def test_sign_in_data_with_invalid_email(testapp): sign_in_url = url_for("api_v1.sign_in") - with pytest.raises(webtest.app.AppError): - data = { - "email": "user{}@gmail.com".format(uuid.uuid4().hex), - "password": USER_FACTORY_DEFAULT_PASSWORD, - } - resp = testapp.post_json(sign_in_url, data) - assert resp.status == "422 UNPROCESSABLE ENTITY" + data = { + "email": "user{}@gmail.com".format(uuid.uuid4().hex), + "password": USER_FACTORY_DEFAULT_PASSWORD, + } + testapp.post_json(sign_in_url, data, status=422) -def test_sign_in_data__with_invalid_password(testapp, function_users): +def test_sign_in_data__with_invalid_password(testapp, func_users): sign_in_url = url_for("api_v1.sign_in") - with pytest.raises(webtest.app.AppError): - data = { - "email": function_users.get_single().email, - "password": "some_wrong_password", - } - resp = testapp.post_json(sign_in_url, data) - assert resp.status == "422 UNPROCESSABLE ENTITY" + data = { + "email": func_users.get_single().email, + "password": "some_wrong_password", + } + testapp.post_json(sign_in_url, data, status=422) diff --git a/tests/integration/app/boards/__init__.py b/tests/integration/app/boards/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/app/boards/test_api_boards.py b/tests/integration/app/boards/test_api_boards.py new file mode 100644 index 0000000..5f95aa3 --- /dev/null +++ b/tests/integration/app/boards/test_api_boards.py @@ -0,0 +1,45 @@ +import uuid + +from flask import url_for + + +def test_get_list_boards_without_auth(testapp): + url = url_for("api_v1.get_list_boards") + testapp.get(url, status=401) + + +def test_get_board_without_auth(testapp, func_boards): + url = url_for( + "api_v1.get_board", board_uuid=uuid.UUID(func_boards.get_single().uuid).hex + ) + testapp.get(url, status=401) + + +def test_get_board_users_without_auth(testapp, func_boards): + url = url_for( + "api_v1.get_board_users", + board_uuid=uuid.UUID(func_boards.get_single().uuid).hex, + ) + testapp.get(url, status=401) + + +def test_create_board_without_auth(testapp): + url = url_for("api_v1.create_board") + data = { + "name": "Board # name", + "description": "Board #3 description", + "is_active": True, + } + testapp.post_json(url, data, status=401) + + +def test_update_board_without_auth(testapp, func_boards): + url = url_for( + "api_v1.update_board", board_uuid=uuid.UUID(func_boards.get_single().uuid).hex, + ) + data = { + "name": "Board # name", + "description": "Board #3 description", + "is_active": True, + } + testapp.put_json(url, data, status=401) diff --git a/tests/unit/app/core/test_utils.py b/tests/unit/app/core/test_utils.py index 364f521..033f654 100644 --- a/tests/unit/app/core/test_utils.py +++ b/tests/unit/app/core/test_utils.py @@ -21,8 +21,8 @@ def test_is_uuid_failed(invalid_uuid_list): assert is_uuid(invalid_uuid_list) is False -def test_non_empty_query_required_success(function_boards): - non_empty_query_required(Board, name=function_boards.get_single().name) +def test_non_empty_query_required_success(func_boards): + non_empty_query_required(Board, name=func_boards.get_single().name) def test_non_empty_query_required_failed(invalid_boards_query_list): @@ -34,15 +34,15 @@ def test_empty_query_required_success(invalid_boards_query_list): empty_query_required(Board, **invalid_boards_query_list) -def test_empty_query_required_failed(function_boards): +def test_empty_query_required_failed(func_boards): with pytest.raises(InvalidUsage): - empty_query_required(Board, name=function_boards.get_single().name) + empty_query_required(Board, name=func_boards.get_single().name) def test_request_url_uuid_success( - function_boards, testapp, + func_boards, testapp, ): - for item in function_boards.get_list(): + for item in func_boards.get_list(): uuid_hexed = uuid.UUID(str(item.uuid)).hex url = f"http://some-host/api/v1/boards/{uuid_hexed}/" request.url = url From af443c05ec34cb4983e493290b3e9fbc636eb612 Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Sun, 26 Apr 2020 11:14:10 +0300 Subject: [PATCH 49/60] light fixes, boards tests --- task_office/app.py | 2 + task_office/auth/utils.py | 2 +- task_office/boards/schemas/basic_schemas.py | 6 +- task_office/core/helpers/listed_response.py | 2 +- task_office/core/utils.py | 22 ++++--- task_office/extensions/cache.py | 7 +-- tests/conftest.py | 15 +++++ tests/fixtures/model_fixtures.py | 4 +- tests/integration/app/auth/fixtures.py | 4 +- tests/integration/app/auth/test_api_auth.py | 16 ++--- tests/integration/app/boards/fixtures.py | 62 +++++++++++++++++++ .../integration/app/boards/test_api_boards.py | 40 ++++++++++++ 12 files changed, 152 insertions(+), 30 deletions(-) create mode 100644 tests/integration/app/boards/fixtures.py diff --git a/task_office/app.py b/task_office/app.py index a181884..07f702f 100644 --- a/task_office/app.py +++ b/task_office/app.py @@ -7,6 +7,7 @@ from task_office.exceptions import InvalidUsage from task_office.extensions.babel import babel from task_office.extensions.bcrypt import bcrypt +from task_office.extensions.cache import cache from task_office.extensions.cors import init_cors from task_office.extensions.db import db from task_office.extensions.jwt import init_jwt @@ -41,6 +42,7 @@ def register_extensions(app): db.init_app(app) babel.init_app(app) bcrypt.init_app(app) + cache.init_app(app, app_config.CACHE) init_jwt(app) init_migrate(app, db) diff --git a/task_office/auth/utils.py b/task_office/auth/utils.py index d6dd535..1aeff57 100644 --- a/task_office/auth/utils.py +++ b/task_office/auth/utils.py @@ -5,7 +5,7 @@ from task_office.core.models.db_models import Permission from task_office.exceptions import InvalidUsage -from task_office.extensions import cache +from task_office.extensions.cache import cache from task_office.settings import app_config diff --git a/task_office/boards/schemas/basic_schemas.py b/task_office/boards/schemas/basic_schemas.py index 8b3d768..3d81bba 100644 --- a/task_office/boards/schemas/basic_schemas.py +++ b/task_office/boards/schemas/basic_schemas.py @@ -11,8 +11,10 @@ class BoardActionsSchema(BaseSchema): - name = fields.Str(required=True, allow_none=False, validate=[Length(max=255)]) - description = fields.Str(allow_none=True, required=False, default="") + name = fields.Str(required=True, allow_none=False, validate=[Length(min=1, max=80)]) + description = fields.Str( + allow_none=False, required=False, default="", validate=[Length(min=0, max=255)] + ) is_active = fields.Boolean(default=True) class Meta: diff --git a/task_office/core/helpers/listed_response.py b/task_office/core/helpers/listed_response.py index 8e6e5cc..c3e3fd8 100644 --- a/task_office/core/helpers/listed_response.py +++ b/task_office/core/helpers/listed_response.py @@ -41,7 +41,7 @@ def serialize(self, query, query_params, schema): query = self._get_query_paginated(query, limit, offset) data = dict(self.RESPONSE_TEMPLATE) - if offset >= count: + if offset >= count and offset > 0: raise InvalidUsage( messages=[self.error_messages["max_offset_exceeded"].format(count - 1)], status_code=422, diff --git a/task_office/core/utils.py b/task_office/core/utils.py index 07a81c4..34abb37 100644 --- a/task_office/core/utils.py +++ b/task_office/core/utils.py @@ -1,3 +1,5 @@ +import random +import string from typing import Any, Union, Tuple from uuid import UUID @@ -38,14 +40,6 @@ def lookup_filter( return LOOKUP_MAP.get(lookup, LOOKUP_MAP.get("e"))(query, key, value) -def is_uuid(uuid) -> bool: - try: - UUID(uuid).version - return True - except ValueError: - return False - - def validate_request_url_uuid( model: Model, key: str, uuid: str, must_exists: bool = False ) -> Union[Tuple, None]: @@ -60,3 +54,15 @@ def validate_request_url_uuid( return non_empty_query_required(model, **{key: uuid}) return None + + +def generate_str(size=6, chars=string.ascii_uppercase + string.digits): + return "".join(random.choice(chars) for _ in range(size)) + + +def is_uuid(uuid) -> bool: + try: + UUID(uuid).version + return True + except ValueError: + return False diff --git a/task_office/extensions/cache.py b/task_office/extensions/cache.py index 6b612aa..c017dec 100644 --- a/task_office/extensions/cache.py +++ b/task_office/extensions/cache.py @@ -1,8 +1,3 @@ -from flask import Flask from flask_caching import Cache -from task_office.settings import app_config - - -def init_cache(app: Flask): - cache = Cache(app=app, config=app_config.CACHE) +cache = Cache() diff --git a/tests/conftest.py b/tests/conftest.py index e45cd04..7f94875 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,9 +1,11 @@ import pytest +from flask import url_for from webtest import TestApp from task_office.app import create_app from task_office.extensions.db import db as _db from task_office.settings import app_config +from tests.factories import USER_FACTORY_DEFAULT_PASSWORD @pytest.yield_fixture(scope="function") @@ -42,8 +44,21 @@ def db(app): _db.drop_all() +@pytest.fixture(scope="function") +def auth_user(func_users, testapp): + sign_in_url = url_for("api_v1.sign_in") + user = func_users.get_single() + data = { + "email": user.email, + "password": USER_FACTORY_DEFAULT_PASSWORD, + } + resp = testapp.post_json(sign_in_url, data, status=200) + return {"current_user": user, "auth_data": resp.json} + + pytest_plugins = [ "tests.fixtures.model_fixtures", "tests.unit.app.core.fixtures", "tests.integration.app.auth.fixtures", + "tests.integration.app.boards.fixtures", ] diff --git a/tests/fixtures/model_fixtures.py b/tests/fixtures/model_fixtures.py index 9d25b6f..bdf1637 100644 --- a/tests/fixtures/model_fixtures.py +++ b/tests/fixtures/model_fixtures.py @@ -49,7 +49,7 @@ def func_boards(func_users, db): boards = BoardFactory.create_batch(5, owner_uuid=str(user.uuid)) db.session.commit() - class Boardx: + class Board: @staticmethod def get_single(): return boards[0] @@ -58,4 +58,4 @@ def get_single(): def get_list(): return boards - return Boardx() + return Board() diff --git a/tests/integration/app/auth/fixtures.py b/tests/integration/app/auth/fixtures.py index 0dadfa9..252fafd 100644 --- a/tests/integration/app/auth/fixtures.py +++ b/tests/integration/app/auth/fixtures.py @@ -68,10 +68,10 @@ @pytest.fixture(params=SIGN_UP_USERS_VALID_DATA) -def valid_sign_up_users_valid_data(request): +def sign_up_users_valid_data(request): return request.param @pytest.fixture(params=SIGN_UP_USERS_INVALID_DATA) -def valid_sign_up_users_invalid_data(request): +def sign_up_users_invalid_data(request): return request.param diff --git a/tests/integration/app/auth/test_api_auth.py b/tests/integration/app/auth/test_api_auth.py index c92770d..c21f153 100644 --- a/tests/integration/app/auth/test_api_auth.py +++ b/tests/integration/app/auth/test_api_auth.py @@ -5,14 +5,14 @@ from tests.factories import USER_FACTORY_DEFAULT_PASSWORD -def test_sign_up_success(testapp, valid_sign_up_users_valid_data): +def test_sign_up_success(testapp, sign_up_users_valid_data): url = url_for("api_v1.sign_up") - testapp.post_json(url, valid_sign_up_users_valid_data, status=200) + testapp.post_json(url, sign_up_users_valid_data, status=200) -def test_sign_up_failed(testapp, valid_sign_up_users_invalid_data): +def test_sign_up_failed(testapp, sign_up_users_invalid_data): url = url_for("api_v1.sign_up") - testapp.post_json(url, valid_sign_up_users_invalid_data, status=422) + testapp.post_json(url, sign_up_users_invalid_data, status=422) def test_sign_up_already_exists_username(testapp, func_users): @@ -43,16 +43,16 @@ def test_sign_up_already_exists_email(testapp, func_users): testapp.post_json(url, data, status=422) -def test_sign_up_in_success(testapp, valid_sign_up_users_valid_data): +def test_sign_up_in_success(testapp, sign_up_users_valid_data): # sign up sign_up_url = url_for("api_v1.sign_up") - testapp.post_json(sign_up_url, valid_sign_up_users_valid_data, status=200) + testapp.post_json(sign_up_url, sign_up_users_valid_data, status=200) # sign in sign_in_url = url_for("api_v1.sign_in") data = { - "email": valid_sign_up_users_valid_data["email"], - "password": valid_sign_up_users_valid_data["password"], + "email": sign_up_users_valid_data["email"], + "password": sign_up_users_valid_data["password"], } testapp.post_json(sign_in_url, data, status=200) diff --git a/tests/integration/app/boards/fixtures.py b/tests/integration/app/boards/fixtures.py new file mode 100644 index 0000000..d740099 --- /dev/null +++ b/tests/integration/app/boards/fixtures.py @@ -0,0 +1,62 @@ +import pytest + +from task_office.core.utils import generate_str + +BOARDS_VALID_DATA = [ + # Typical board data + {"name": "Board 1# name", "description": "Board #3 description", "is_active": True}, + # Data without is_active + {"name": "Board 2# name", "description": "Board #3 description"}, + # Data without description + {"name": "Board 3# name", "is_active": True}, + # Data without description, is_active + {"name": "Board 4# name"}, + # Data with min length name + {"name": "B", "description": "Board #3 description", "is_active": True}, + # Data with max length name(80) + { + "name": generate_str(80), + "description": "Board #3 description", + "is_active": True, + }, + # Data with min length description + {"name": "Board 5# name", "description": "", "is_active": True}, + # Data with max length description(255) + {"name": "Board 5# name", "description": generate_str(255), "is_active": True}, +] + + +BOARDS_INVALID_DATA = [ + # Data with zero length name + {"name": "", "description": "Board #1 description", "is_active": True}, + # Data without name + {"description": "Board #2 description", "is_active": True}, + # Data without None name + {"name": None, "description": "Board #3 description", "is_active": True}, + # Data with exceeded length name + { + "name": generate_str(81), + "description": "Board #1 description", + "is_active": True, + }, + # Data with exceeded description + {"name": "Board 1# name", "description": generate_str(256), "is_active": True}, + # Data with None description + {"name": "Board 2# name", "description": None, "is_active": True}, + # Data with incorrect name type + {"name": True, "description": None, "is_active": True}, + # Data with incorrect description type + {"name": "Board 3# name", "description": 12345, "is_active": True}, + # Data with incorrect is_active type + {"name": "Board 4# name", "description": "Board #2 description", "is_active": 48}, +] + + +@pytest.fixture(params=BOARDS_VALID_DATA) +def boards_valid_data(request): + return request.param + + +@pytest.fixture(params=BOARDS_INVALID_DATA) +def boards_invalid_data(request): + return request.param diff --git a/tests/integration/app/boards/test_api_boards.py b/tests/integration/app/boards/test_api_boards.py index 5f95aa3..a10ef12 100644 --- a/tests/integration/app/boards/test_api_boards.py +++ b/tests/integration/app/boards/test_api_boards.py @@ -43,3 +43,43 @@ def test_update_board_without_auth(testapp, func_boards): "is_active": True, } testapp.put_json(url, data, status=401) + + +def test_get_boards_list(testapp, auth_user): + url = url_for("api_v1.get_list_boards") + token = auth_user["auth_data"]["tokens"]["access"]["token"] + headers = {"Authorization": f"Bearer {token}"} + testapp.get(url, headers=headers, status=200) + + +def test_get_board(testapp, func_boards, auth_user): + url = url_for( + "api_v1.get_board", board_uuid=uuid.UUID(func_boards.get_single().uuid).hex + ) + token = auth_user["auth_data"]["tokens"]["access"]["token"] + headers = {"Authorization": f"Bearer {token}"} + testapp.get(url, headers=headers, status=200) + + +def test_get_board_users(testapp, func_boards, auth_user): + url = url_for( + "api_v1.get_board_users", + board_uuid=uuid.UUID(func_boards.get_single().uuid).hex, + ) + token = auth_user["auth_data"]["tokens"]["access"]["token"] + headers = {"Authorization": f"Bearer {token}"} + testapp.get(url, headers=headers, status=200) + + +def test_create_board_success(testapp, auth_user, boards_valid_data): + url = url_for("api_v1.create_board") + token = auth_user["auth_data"]["tokens"]["access"]["token"] + headers = {"Authorization": f"Bearer {token}"} + testapp.post_json(url, boards_valid_data, headers=headers, status=200) + + +def test_create_board_failed(testapp, auth_user, boards_invalid_data): + url = url_for("api_v1.create_board") + token = auth_user["auth_data"]["tokens"]["access"]["token"] + headers = {"Authorization": f"Bearer {token}"} + testapp.post_json(url, boards_invalid_data, headers=headers, status=422) From f9ee68daa4f859f0bc3a1531c76c545769f22aff Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Sun, 26 Apr 2020 11:37:55 +0300 Subject: [PATCH 50/60] light fixes, boards tests v2 --- .../integration/app/boards/test_api_boards.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/integration/app/boards/test_api_boards.py b/tests/integration/app/boards/test_api_boards.py index a10ef12..a243ce5 100644 --- a/tests/integration/app/boards/test_api_boards.py +++ b/tests/integration/app/boards/test_api_boards.py @@ -83,3 +83,35 @@ def test_create_board_failed(testapp, auth_user, boards_invalid_data): token = auth_user["auth_data"]["tokens"]["access"]["token"] headers = {"Authorization": f"Bearer {token}"} testapp.post_json(url, boards_invalid_data, headers=headers, status=422) + + +def test_update_board(testapp, func_boards, auth_user): + url = url_for( + "api_v1.update_board", board_uuid=uuid.UUID(func_boards.get_single().uuid).hex + ) + token = auth_user["auth_data"]["tokens"]["access"]["token"] + + headers = {"Authorization": f"Bearer {token}"} + data = { + "name": "Board 1# updated name", + "description": "Board #3 updated description", + "is_active": True, + } + resp = testapp.put_json(url, data, headers=headers, status=200) + + assert resp.json["name"] == data["name"] + assert resp.json["description"] == data["description"] + assert resp.json["is_active"] == data["is_active"] + + +def test_update_board_not_exists(testapp, auth_user): + url = url_for("api_v1.update_board", board_uuid=uuid.uuid4().hex) + token = auth_user["auth_data"]["tokens"]["access"]["token"] + + headers = {"Authorization": f"Bearer {token}"} + data = { + "name": "Board 1# updated name", + "description": "Board #3 updated description", + "is_active": True, + } + testapp.put_json(url, data, headers=headers, status=404) From ad618790db9e5332f41ad8ee64efc358ccde46eb Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Sun, 26 Apr 2020 11:53:21 +0300 Subject: [PATCH 51/60] test_utils improvements --- task_office/app.py | 6 ++++++ task_office/core/utils.py | 2 +- tests/unit/app/core/test_utils.py | 8 +++----- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/task_office/app.py b/task_office/app.py index 07f702f..2b50070 100644 --- a/task_office/app.py +++ b/task_office/app.py @@ -15,6 +15,12 @@ from task_office.settings import app_config from task_office.swagger import SWAGGER_URL +from task_office.tasks import views +from task_office.permissions import views +from task_office.boards import views +from task_office.columns import views +from task_office.auth import views + def create_app(config_object): """An application factory, as explained here: diff --git a/task_office/core/utils.py b/task_office/core/utils.py index 34abb37..42c1682 100644 --- a/task_office/core/utils.py +++ b/task_office/core/utils.py @@ -1,5 +1,5 @@ -import random import string +import random from typing import Any, Union, Tuple from uuid import UUID diff --git a/tests/unit/app/core/test_utils.py b/tests/unit/app/core/test_utils.py index 033f654..da604bc 100644 --- a/tests/unit/app/core/test_utils.py +++ b/tests/unit/app/core/test_utils.py @@ -1,7 +1,7 @@ import uuid import pytest -from flask import request +from flask import request, url_for from task_office.core.models.db_models import Board from task_office.core.utils import ( @@ -44,8 +44,7 @@ def test_request_url_uuid_success( ): for item in func_boards.get_list(): uuid_hexed = uuid.UUID(str(item.uuid)).hex - url = f"http://some-host/api/v1/boards/{uuid_hexed}/" - request.url = url + request.url = url_for("api_v1.get_board", board_uuid=uuid_hexed,) validate_request_url_uuid(Board, "uuid", uuid_hexed, True) @@ -61,6 +60,5 @@ def test_request_url_uuid_failed_case2(testapp): for item in range(2): with pytest.raises(InvalidUsage): uuid_hexed = uuid.uuid4().hex - url = f"http://some-host/api/v1/boards/{uuid_hexed}/" - request.url = url + request.url = url_for("api_v1.get_board", board_uuid=uuid_hexed,) validate_request_url_uuid(Board, "uuid", uuid_hexed, True) From 2492066707985352e694e671d2668b064cb2604e Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Sun, 3 May 2020 10:31:26 +0300 Subject: [PATCH 52/60] tasks, boards factories --- task_office/core/enums.py | 6 ++- tests/factories.py | 53 ++++++++++++++++++- tests/integration/app/columns/__init__.py | 0 tests/integration/app/columns/fixtures.py | 0 .../app/columns/test_api_columns.py | 0 tests/unit/app/core/test_utils.py | 3 +- 6 files changed, 58 insertions(+), 4 deletions(-) create mode 100644 tests/integration/app/columns/__init__.py create mode 100644 tests/integration/app/columns/fixtures.py create mode 100644 tests/integration/app/columns/test_api_columns.py diff --git a/task_office/core/enums.py b/task_office/core/enums.py index 25e9113..1ba70f3 100644 --- a/task_office/core/enums.py +++ b/task_office/core/enums.py @@ -4,7 +4,7 @@ class XEnum(Enum): """ - Base Custom Enum for project + Base Enum for project """ # ADMIN = 0, _('Admin'), _('Approximate quantity: format +/-') @@ -25,6 +25,10 @@ def dict_choices(cls): def get_names(cls): return [item.name for item in cls] + @classmethod + def get_values(cls): + return [item.value for item in cls] + def __new__(cls, value, name, description=""): member = object.__new__(cls) member._value_ = value diff --git a/tests/factories.py b/tests/factories.py index e95e8b2..af7853b 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -1,9 +1,13 @@ +import datetime import uuid +import factory from factory import PostGenerationMethodCall, Sequence from factory.alchemy import SQLAlchemyModelFactory +from factory.fuzzy import FuzzyChoice, FuzzyDateTime +from pytz import UTC -from task_office.core.models.db_models import User, Board +from task_office.core.models.db_models import User, Board, BoardColumn, Task from task_office.extensions.db import db as _db @@ -35,12 +39,59 @@ class Meta: model = User +class BoardColumnFactory(BaseFactory): + """BoardColumn factory.""" + + name = Sequence(lambda n: "BoardColumn Name #{0}".format(n)) + position = Sequence(lambda n: n) + + @factory.post_generation + def tasks(self, create, extracted, **kwargs): + if create: + TaskFactory(column_uuid=self.uuid), + TaskFactory(column_uuid=self.uuid) + + class Meta: + """Factory configuration.""" + + model = BoardColumn + + +class TaskFactory(BaseFactory): + """Task factory.""" + + expire_at = FuzzyDateTime(datetime.datetime(2020, 1, 1, tzinfo=UTC)) + label = Sequence(lambda n: "Task Label #{0}".format(n)) + name = Sequence(lambda n: "Task Name #{0}".format(n)) + description = Sequence(lambda n: "Task Description #{0}".format(n)) + state = FuzzyChoice(choices=Task.State.get_values()) + position = Sequence(lambda n: n) + creator = factory.SubFactory(UserFactory) + + @factory.post_generation + def set_creator_uuid(self, create, extracted, **kwargs): + if create: + self.creator_uuid = self.creator.uuid + + class Meta: + """Factory configuration.""" + + model = Task + + class BoardFactory(BaseFactory): """Board factory.""" name = Sequence(lambda n: "Board Name #{0}".format(n)) description = Sequence(lambda n: "Board Description #{0}".format(n)) + @factory.post_generation + def columns(self, create, extracted, **kwargs): + if create: + BoardColumnFactory(board_uuid=self.uuid) + BoardColumnFactory(board_uuid=self.uuid) + BoardColumnFactory(board_uuid=self.uuid) + class Meta: """Factory configuration.""" diff --git a/tests/integration/app/columns/__init__.py b/tests/integration/app/columns/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/app/columns/fixtures.py b/tests/integration/app/columns/fixtures.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/app/columns/test_api_columns.py b/tests/integration/app/columns/test_api_columns.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/app/core/test_utils.py b/tests/unit/app/core/test_utils.py index da604bc..9b73283 100644 --- a/tests/unit/app/core/test_utils.py +++ b/tests/unit/app/core/test_utils.py @@ -51,8 +51,7 @@ def test_request_url_uuid_success( def test_request_url_uuid_failed_case1(invalid_uuid_list, testapp): with pytest.raises(InvalidUsage): uuid_hexed = invalid_uuid_list - url = f"http://some-host/api/v1/boards/{uuid_hexed}/" - request.url = url + request.url = url_for("api_v1.get_board", board_uuid=uuid_hexed,) validate_request_url_uuid(Board, "uuid", uuid_hexed, False) From d1756bb1c5c54254103c3394553fd59b9567e4bc Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Sun, 3 May 2020 15:04:58 +0300 Subject: [PATCH 53/60] columns tests --- task_office/columns/schemas/basic_schemas.py | 8 +- task_office/columns/utils.py | 2 +- task_office/columns/views.py | 6 +- task_office/swagger/api_paths.py | 18 ++++ tests/conftest.py | 1 + tests/integration/app/columns/fixtures.py | 39 +++++++ .../app/columns/test_api_columns.py | 100 ++++++++++++++++++ tests/unit/app/columns/__init__.py | 0 tests/unit/app/columns/test_utils.py | 2 + 9 files changed, 170 insertions(+), 6 deletions(-) create mode 100644 tests/unit/app/columns/__init__.py create mode 100644 tests/unit/app/columns/test_utils.py diff --git a/task_office/columns/schemas/basic_schemas.py b/task_office/columns/schemas/basic_schemas.py index 858bed0..9026891 100644 --- a/task_office/columns/schemas/basic_schemas.py +++ b/task_office/columns/schemas/basic_schemas.py @@ -11,7 +11,9 @@ class ColumnPostSchema(BaseSchema): - name = fields.Str(required=True, allow_none=False, validate=[Length(max=120)]) + name = fields.Str( + required=True, allow_none=False, validate=[Length(min=1, max=120)] + ) position = fields.Integer( required=True, default=1, allow_none=False, validate=[Range(min=1)] ) @@ -21,7 +23,9 @@ class Meta: class ColumnPutSchema(BaseSchema): - name = fields.Str(required=False, allow_none=False, validate=[Length(max=120)]) + name = fields.Str( + required=False, allow_none=False, validate=[Length(min=1, max=120)] + ) position = fields.Integer(required=False, allow_none=False, validate=[Range(min=1)]) class Meta: diff --git a/task_office/columns/utils.py b/task_office/columns/utils.py index 2031e5f..bbab9a5 100644 --- a/task_office/columns/utils.py +++ b/task_office/columns/utils.py @@ -1,7 +1,7 @@ """Columns utils.""" from task_office.core.models.db_models import BoardColumn from task_office.database import Model -from task_office.extensions import db +from task_office.extensions.db import db def reset_columns_ordering( diff --git a/task_office/columns/views.py b/task_office/columns/views.py index 37da921..cc027cd 100644 --- a/task_office/columns/views.py +++ b/task_office/columns/views.py @@ -149,9 +149,9 @@ def get_list_columns(board_uuid, **kwargs): # Check board_uuid in request_url validate_request_url_uuid(Board, "uuid", board_uuid, True) - columns = BoardColumn.query.order_by("position", "-id").filter_by( - board_uuid=board_uuid - ) + columns = BoardColumn.query.order_by( + BoardColumn.position, BoardColumn.id.asc() + ).filter_by(board_uuid=board_uuid) # Serialize to paginated response data = listed_response.serialize( diff --git a/task_office/swagger/api_paths.py b/task_office/swagger/api_paths.py index 5891bda..3e579e7 100644 --- a/task_office/swagger/api_paths.py +++ b/task_office/swagger/api_paths.py @@ -247,6 +247,24 @@ }, }, }, + "/api/v1/boards//columns/meta": { + "get": { + "tags": ["Columns"], + "summary": "Returns Boards Columns Metadata", + "requestBody": { + "description": "Boards Columns Get Metadata", + "required": True, + "content": {"application/json": {}}, + }, + "responses": { + "200": {"description": "OK"}, + "401": {"description": "Failed. Not authorized."}, + "403": {"description": "Failed. Not denied."}, + "404": {"description": "Failed. Not found."}, + "422": {"description": "Failed. Bad data."}, + }, + }, + }, "/api/v1/boards//columns/": { "put": { "tags": ["Columns"], diff --git a/tests/conftest.py b/tests/conftest.py index 7f94875..1018660 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -61,4 +61,5 @@ def auth_user(func_users, testapp): "tests.unit.app.core.fixtures", "tests.integration.app.auth.fixtures", "tests.integration.app.boards.fixtures", + "tests.integration.app.columns.fixtures", ] diff --git a/tests/integration/app/columns/fixtures.py b/tests/integration/app/columns/fixtures.py index e69de29..9102334 100644 --- a/tests/integration/app/columns/fixtures.py +++ b/tests/integration/app/columns/fixtures.py @@ -0,0 +1,39 @@ +import pytest + +from task_office.core.utils import generate_str + +COLUMNS_VALID_DATA = [ + # Typical board data + {"name": "BoardColumn 1# name", "position": 1}, + # With min length name + {"name": "B", "position": 1}, + # With max length name + {"name": generate_str(120), "position": 1}, +] + +COLUMNS_INVALID_DATA = [ + # With empty name + {"name": "", "position": 1}, + # With None Name + {"name": None, "position": 1}, + # Without name + {"position": 1}, + # With exceeded name length + {"name": generate_str(121), "position": 1}, + # With to small position + {"name": "BoardColumn 1# name", "position": 0}, + # With None position + {"name": "BoardColumn 1# name", "position": None}, + # Without position + {"name": "BoardColumn 1# name"}, +] + + +@pytest.fixture(params=COLUMNS_VALID_DATA) +def columns_valid_data(request): + return request.param + + +@pytest.fixture(params=COLUMNS_INVALID_DATA) +def columns_invalid_data(request): + return request.param diff --git a/tests/integration/app/columns/test_api_columns.py b/tests/integration/app/columns/test_api_columns.py index e69de29..e8ba20c 100644 --- a/tests/integration/app/columns/test_api_columns.py +++ b/tests/integration/app/columns/test_api_columns.py @@ -0,0 +1,100 @@ +import uuid +from flask import url_for + + +def test_get_board_columns_without_auth(testapp, func_boards): + url = url_for( + "api_v1.get_list_columns", + board_uuid=uuid.UUID(func_boards.get_single().uuid).hex, + ) + testapp.get(url, status=401) + + +def test_create_board_columns_without_auth(testapp, func_boards): + url = url_for( + "api_v1.create_column", board_uuid=uuid.UUID(func_boards.get_single().uuid).hex, + ) + data = { + "name": "Column # name", + "position": 1, + } + testapp.post_json(url, data, status=401) + + +def test_get_board_columns_metadata_without_auth(testapp, func_boards): + url = url_for( + "api_v1.get_columns_meta_data", + board_uuid=uuid.UUID(func_boards.get_single().uuid).hex, + ) + testapp.get(url, status=401) + + +def test_get_board_columns_metadata_success(testapp, func_boards, auth_user): + url = url_for( + "api_v1.get_columns_meta_data", + board_uuid=uuid.UUID(func_boards.get_single().uuid).hex, + ) + token = auth_user["auth_data"]["tokens"]["access"]["token"] + headers = {"Authorization": f"Bearer {token}"} + testapp.get(url, headers=headers, status=200) + + +def test_get_board_columns_success(testapp, func_boards, auth_user): + url = url_for( + "api_v1.get_list_columns", + board_uuid=uuid.UUID(func_boards.get_single().uuid).hex, + ) + token = auth_user["auth_data"]["tokens"]["access"]["token"] + headers = {"Authorization": f"Bearer {token}"} + testapp.get(url, headers=headers, status=200) + + +def test_create_board_columns_success( + testapp, auth_user, func_boards, columns_valid_data +): + url = url_for( + "api_v1.create_column", board_uuid=uuid.UUID(func_boards.get_single().uuid).hex, + ) + token = auth_user["auth_data"]["tokens"]["access"]["token"] + headers = {"Authorization": f"Bearer {token}"} + testapp.post_json(url, columns_valid_data, headers=headers, status=200) + + +def test_create_board_columns_failed( + testapp, auth_user, func_boards, columns_invalid_data +): + url = url_for( + "api_v1.create_column", board_uuid=uuid.UUID(func_boards.get_single().uuid).hex, + ) + token = auth_user["auth_data"]["tokens"]["access"]["token"] + headers = {"Authorization": f"Bearer {token}"} + testapp.post_json(url, columns_invalid_data, headers=headers, status=422) + + +def test_update_board_columns(testapp, func_boards, auth_user): + board_uuid = uuid.UUID(func_boards.get_single().uuid).hex + column_uuid = uuid.UUID(func_boards.get_single().columns[0].uuid).hex + url = url_for( + "api_v1.update_column", board_uuid=board_uuid, column_uuid=column_uuid + ) + token = auth_user["auth_data"]["tokens"]["access"]["token"] + + headers = {"Authorization": f"Bearer {token}"} + data = {"name": "BoardColumn 1# name", "position": 1} + resp = testapp.put_json(url, data, headers=headers, status=200) + + assert resp.json["name"] == data["name"] + assert resp.json["position"] == data["position"] + + +def test_update_board_columns_not_exists(testapp, func_boards, auth_user): + board_uuid = uuid.UUID(func_boards.get_single().uuid).hex + column_uuid = uuid.uuid4().hex + url = url_for( + "api_v1.update_column", board_uuid=board_uuid, column_uuid=column_uuid + ) + token = auth_user["auth_data"]["tokens"]["access"]["token"] + + headers = {"Authorization": f"Bearer {token}"} + data = {"name": "BoardColumn 1# name", "position": 1} + testapp.put_json(url, data, headers=headers, status=404) diff --git a/tests/unit/app/columns/__init__.py b/tests/unit/app/columns/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/app/columns/test_utils.py b/tests/unit/app/columns/test_utils.py new file mode 100644 index 0000000..3ef660f --- /dev/null +++ b/tests/unit/app/columns/test_utils.py @@ -0,0 +1,2 @@ +# TODO(Medniy2000) Implement columns utils test cases: +# reset_columns_ordering From 781f9e1b3566dbb42cb7a8cafb25d0be35d3b437 Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Mon, 4 May 2020 11:02:57 +0300 Subject: [PATCH 54/60] added auth refresh checkpoints --- tests/integration/app/auth/fixtures.py | 11 +++++++++++ tests/integration/app/auth/test_api_auth.py | 13 +++++++++++++ 2 files changed, 24 insertions(+) diff --git a/tests/integration/app/auth/fixtures.py b/tests/integration/app/auth/fixtures.py index 252fafd..c3a9b11 100644 --- a/tests/integration/app/auth/fixtures.py +++ b/tests/integration/app/auth/fixtures.py @@ -1,5 +1,7 @@ import pytest +from task_office.core.utils import generate_str + SIGN_UP_USERS_VALID_DATA = [ { "password_confirm": "user11", @@ -66,6 +68,10 @@ }, ] +REFRESH_INVALID = [ + "", None, generate_str(25), +] + @pytest.fixture(params=SIGN_UP_USERS_VALID_DATA) def sign_up_users_valid_data(request): @@ -75,3 +81,8 @@ def sign_up_users_valid_data(request): @pytest.fixture(params=SIGN_UP_USERS_INVALID_DATA) def sign_up_users_invalid_data(request): return request.param + + +@pytest.fixture(params=REFRESH_INVALID) +def refresh_users_invalid_data(request): + return request.param diff --git a/tests/integration/app/auth/test_api_auth.py b/tests/integration/app/auth/test_api_auth.py index c21f153..9239fe4 100644 --- a/tests/integration/app/auth/test_api_auth.py +++ b/tests/integration/app/auth/test_api_auth.py @@ -91,3 +91,16 @@ def test_sign_in_data__with_invalid_password(testapp, func_users): "password": "some_wrong_password", } testapp.post_json(sign_in_url, data, status=422) + + +def test_refresh_success(testapp, auth_user): + url = url_for("api_v1.refresh",) + token = auth_user["auth_data"]["tokens"]["refresh"]["token"] + headers = {"Authorization": f"Bearer {token}"} + testapp.post_json(url, headers=headers, status=200) + + +def test_refresh_failed(testapp, refresh_users_invalid_data): + url = url_for("api_v1.refresh",) + headers = {"Authorization": f"Bearer {refresh_users_invalid_data}"} + testapp.post_json(url, headers=headers, status=422) From c31a5a3d5b45a7e1f9035c4f7113bdb7d4f9b5a4 Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Sat, 9 May 2020 14:17:43 +0300 Subject: [PATCH 55/60] tasks tests --- tests/conftest.py | 1 + tests/integration/app/auth/fixtures.py | 4 +- tests/integration/app/tasks/__init__.py | 0 tests/integration/app/tasks/fixtures.py | 191 ++++++++++++++++++ tests/integration/app/tasks/test_api_tasks.py | 130 ++++++++++++ 5 files changed, 325 insertions(+), 1 deletion(-) create mode 100644 tests/integration/app/tasks/__init__.py create mode 100644 tests/integration/app/tasks/fixtures.py create mode 100644 tests/integration/app/tasks/test_api_tasks.py diff --git a/tests/conftest.py b/tests/conftest.py index 1018660..42e7d5f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -62,4 +62,5 @@ def auth_user(func_users, testapp): "tests.integration.app.auth.fixtures", "tests.integration.app.boards.fixtures", "tests.integration.app.columns.fixtures", + "tests.integration.app.tasks.fixtures", ] diff --git a/tests/integration/app/auth/fixtures.py b/tests/integration/app/auth/fixtures.py index c3a9b11..c9508b1 100644 --- a/tests/integration/app/auth/fixtures.py +++ b/tests/integration/app/auth/fixtures.py @@ -69,7 +69,9 @@ ] REFRESH_INVALID = [ - "", None, generate_str(25), + "", + None, + generate_str(25), ] diff --git a/tests/integration/app/tasks/__init__.py b/tests/integration/app/tasks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/app/tasks/fixtures.py b/tests/integration/app/tasks/fixtures.py new file mode 100644 index 0000000..bbc8549 --- /dev/null +++ b/tests/integration/app/tasks/fixtures.py @@ -0,0 +1,191 @@ +import pytest + +from task_office.core.utils import generate_str + +TASKS_VALID_DATA = [ + # Typical task data + { + "label": "Label #1", + "expire_at": "2020-05-25 05:30:11", + "name": "Task #1 name", + "description": "Task #1 description", + "position": 1, + "state": 1, + "column_uuid": None, + "performers": [], + }, + # Task with max label name + { + "label": generate_str(size=80), + "expire_at": "2020-05-25 05:30:11", + "name": "Task #2 name", + "description": "Task #2 description", + "position": 1, + "state": 1, + "column_uuid": None, + "performers": [], + }, + # Task without label + { + "expire_at": "2020-05-25 05:30:11", + "name": "Task #3 name", + "description": "Task #3 description", + "position": 1, + "state": 1, + "column_uuid": None, + "performers": [], + }, + # Task without expire_at + { + "label": "Label #1", + "name": "Task #4 name", + "description": "Task #4 description", + "position": 1, + "state": 1, + "column_uuid": None, + "performers": [], + }, + # Task with min name length + { + "label": "Label #1", + "expire_at": "2020-05-25 05:30:11", + "name": "T", + "description": "Task #5 description", + "position": 1, + "state": 1, + "column_uuid": None, + "performers": [], + }, + # Task with max name length + { + "label": "Label #1", + "expire_at": "2020-05-25 05:30:11", + "name": "Task #6 name", + "description": "Task #6 description", + "position": 1, + "state": 1, + "column_uuid": None, + "performers": [], + }, + # Task with max description length + { + "label": "Label #1", + "expire_at": "2020-05-25 05:30:11", + "name": "Task #7 name", + "description": generate_str(120), + "position": 1, + "state": 1, + "column_uuid": None, + "performers": [], + }, + # Task without state + { + "label": "Label #1", + "expire_at": "2020-05-25 05:30:11", + "name": "Task #8 name", + "description": "Task #8 description", + "position": 1, + "column_uuid": None, + "performers": [], + }, + # Task without position + { + "label": "Label #1", + "expire_at": "2020-05-25 05:30:11", + "name": "Task #9 name", + "description": "Task #9 description", + "state": 1, + "column_uuid": None, + "performers": [], + }, +] + +TASKS_INVALID_DATA = [ + # Task with None expire_at + { + "label": "Label #1", + "expire_at": None, + "name": "Task #1 name", + "description": "Task #1 description", + "position": 1, + "state": 1, + "column_uuid": None, + "performers": [], + }, + # Task with None label + { + "label": None, + "expire_at": "2020-05-25 05:30:11", + "name": "Task #1 name", + "description": "Task #1 description", + "position": 1, + "state": 1, + "column_uuid": None, + "performers": [], + }, + # Task with empty name + { + "label": "Label #1", + "expire_at": "2020-05-25 05:30:11", + "name": "", + "description": "Task #1 description", + "position": 1, + "state": 1, + "column_uuid": None, + "performers": [], + }, + # Task with exceeded name length + { + "label": "Label #1", + "expire_at": "2020-05-25 05:30:11", + "name": generate_str(120), + "description": "Task #1 description", + "position": 1, + "state": 1, + "column_uuid": None, + "performers": [], + }, + # Task with exceeded description length + { + "label": "Label #1", + "expire_at": "2020-05-25 05:30:11", + "name": "Task #1 name", + "description": generate_str(121), + "position": 1, + "state": 1, + "column_uuid": None, + "performers": [], + }, + # Task with None state + { + "label": "Label #1", + "expire_at": "2020-05-25 05:30:11", + "name": "Task #1 name", + "description": "Task #1 description", + "position": 1, + "state": None, + "column_uuid": None, + "performers": [], + }, + # Task with None position + { + "label": "Label #1", + "expire_at": "2020-05-25 05:30:11", + "name": "Task #1 name", + "description": "Task #1 description", + "position": None, + "state": 1, + "column_uuid": None, + "performers": [], + }, +] + + +@pytest.fixture(params=TASKS_VALID_DATA) +def tasks_valid_data(request): + return request.param + + +@pytest.fixture(params=TASKS_INVALID_DATA) +def tasks_invalid_data(request): + return request.param diff --git a/tests/integration/app/tasks/test_api_tasks.py b/tests/integration/app/tasks/test_api_tasks.py new file mode 100644 index 0000000..7189adc --- /dev/null +++ b/tests/integration/app/tasks/test_api_tasks.py @@ -0,0 +1,130 @@ +import uuid +from flask import url_for + + +def test_get_tasks_without_auth(testapp, func_boards): + board_uuid = func_boards.get_single().uuid + url = url_for("api_v1.get_list_tasks", board_uuid=board_uuid,) + testapp.get(url, status=401) + + +def test_get_tasks_by_columns_without_auth(testapp, func_boards): + board_uuid = func_boards.get_single().uuid + url = url_for("api_v1.get_list_tasks_by_columns", board_uuid=board_uuid,) + testapp.get(url, status=401) + + +def test_create_task_without_auth(testapp, func_boards): + url = url_for( + "api_v1.create_task", board_uuid=uuid.UUID(func_boards.get_single().uuid).hex, + ) + data = { + "label": "Label#", + "expire_at": "2020-05-25 05:30:11", + "name": "Task # name", + "description": "Task # description", + "position": 1, + "state": 1, + "column_uuid": func_boards.get_single().columns[0].uuid, + } + testapp.post_json(url, data, status=401) + + +def test_update_task_without_auth(testapp, func_boards): + task_uuid = func_boards.get_single().columns[0].tasks[0].uuid + board_uuid = func_boards.get_single().uuid + url = url_for("api_v1.update_task", board_uuid=board_uuid, task_uuid=task_uuid) + data = { + "label": "Label#", + "expire_at": "2020-05-25 05:30:11", + "name": "Task # name", + "description": "Task # description", + "position": 1, + "state": 1, + "column_uuid": func_boards.get_single().columns[0].uuid, + } + testapp.put_json(url, data, status=401) + + +def test_get_tasks_metadata_without_auth(testapp, func_boards): + url = url_for( + "api_v1.get_tasks_meta_data", + board_uuid=uuid.UUID(func_boards.get_single().uuid).hex, + ) + testapp.get(url, status=401) + + +def test_get_tasks_metadata_success(testapp, func_boards, auth_user): + url = url_for( + "api_v1.get_tasks_meta_data", + board_uuid=uuid.UUID(func_boards.get_single().uuid).hex, + ) + token = auth_user["auth_data"]["tokens"]["access"]["token"] + headers = {"Authorization": f"Bearer {token}"} + testapp.get(url, headers=headers, status=200) + + +def test_get_tasks(testapp, func_boards, auth_user): + board_uuid = func_boards.get_single().uuid + url = url_for("api_v1.get_list_tasks", board_uuid=board_uuid,) + token = auth_user["auth_data"]["tokens"]["access"]["token"] + headers = {"Authorization": f"Bearer {token}"} + testapp.get(url, headers=headers, status=200) + + +def test_get_tasks_by_columns(testapp, func_boards, auth_user): + board_uuid = func_boards.get_single().uuid + url = url_for("api_v1.get_list_tasks_by_columns", board_uuid=board_uuid,) + token = auth_user["auth_data"]["tokens"]["access"]["token"] + headers = {"Authorization": f"Bearer {token}"} + testapp.get(url, headers=headers, status=200) + + +def create_tasks_success(testapp, func_boards, auth_user, tasks_valid_data): + url = url_for( + "api_v1.create_task", board_uuid=uuid.UUID(func_boards.get_single().uuid).hex, + ) + + tasks_valid_data["performers"].apend(auth_user["auth_data"]["uuid"]) + tasks_valid_data["column_uuid"] = func_boards.get_single().columns[0].uuid + + token = auth_user["auth_data"]["tokens"]["access"]["token"] + headers = {"Authorization": f"Bearer {token}"} + + testapp.post_json(url, tasks_valid_data, headers=headers, status=200) + + +def create_tasks_failed(testapp, func_boards, auth_user, tasks_invalid_data): + url = url_for("api_v1.create_task", board_uuid=func_boards.get_single().uuid,) + + tasks_invalid_data["performers"].apend(auth_user["auth_data"]["uuid"]) + tasks_invalid_data["column_uuid"] = func_boards.get_single().columns[0].uuid + + token = auth_user["auth_data"]["tokens"]["access"]["token"] + headers = {"Authorization": f"Bearer {token}"} + + testapp.post_json(url, tasks_invalid_data, headers=headers, status=422) + + +def update_tasks_success(testapp, func_boards, auth_user): + board = func_boards.get_single() + + token = auth_user["auth_data"]["tokens"]["access"]["token"] + headers = {"Authorization": f"Bearer {token}"} + + item_number = 100 + for task in board.columns[0].tasks: + url = url_for("api_v1.update_task", board_uuid=board.uuid, task_uuid=task.uuid) + data = ( + { + "label": f"Label #{item_number}", + "expire_at": "2020-05-25 05:30:11", + "name": f"Task #{item_number} name", + "description": f"Task #{item_number} description", + }, + ) + resp = testapp.put_json(url, data, headers=headers, status=200) + resp_data = resp.json + assert data["label"] == resp_data["label"] + assert data["name"] == resp_data["name"] + assert data["description"] == resp_data["description"] From b5084aef7490c05aa74d3b4db3d5fd9248686f88 Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Sun, 10 May 2020 20:30:54 +0300 Subject: [PATCH 56/60] permissions checkpoints --- docker-compose.yml | 1 + task_office/permissions/views.py | 1 - tests/TODO.rst | 11 ++ tests/conftest.py | 1 + tests/factories.py | 22 +++- tests/fixtures/model_fixtures.py | 15 ++- .../app/columns/test_api_columns.py | 24 ++-- tests/integration/app/permissions/__init__.py | 0 tests/integration/app/permissions/fixtures.py | 17 +++ .../app/permissions/test_api_permissions.py | 116 ++++++++++++++++++ 10 files changed, 186 insertions(+), 22 deletions(-) create mode 100644 tests/TODO.rst create mode 100644 tests/integration/app/permissions/__init__.py create mode 100644 tests/integration/app/permissions/fixtures.py create mode 100644 tests/integration/app/permissions/test_api_permissions.py diff --git a/docker-compose.yml b/docker-compose.yml index cbb41ba..5fcd4c2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -48,6 +48,7 @@ services: redis_service: container_name: redis_1 image: redis + command: redis-server --requirepass ${CACHE_REDIS_PASSWORD} env_file: - .env diff --git a/task_office/permissions/views.py b/task_office/permissions/views.py index a7aec99..684da8b 100644 --- a/task_office/permissions/views.py +++ b/task_office/permissions/views.py @@ -99,7 +99,6 @@ def update_permission(board_uuid, permission_uuid, **kwargs): )[1] perm.update(updated_at=datetime.utcnow(), **data) - perm.save() reset_permissions(uuid.UUID(perm.user_uuid).hex) return perm diff --git a/tests/TODO.rst b/tests/TODO.rst new file mode 100644 index 0000000..23369e5 --- /dev/null +++ b/tests/TODO.rst @@ -0,0 +1,11 @@ +================= +TODO List: +================= + +Issues General +^^^^^^^^^^^^^^ +* Implement checkpoints for endpoints with available search, ordering +* Check: Are permissions work correct? +* Check: Are cached permissions work correct? +* Check: Pagination test cases + diff --git a/tests/conftest.py b/tests/conftest.py index 42e7d5f..9adb50c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -63,4 +63,5 @@ def auth_user(func_users, testapp): "tests.integration.app.boards.fixtures", "tests.integration.app.columns.fixtures", "tests.integration.app.tasks.fixtures", + "tests.integration.app.permissions.fixtures", ] diff --git a/tests/factories.py b/tests/factories.py index af7853b..7b58a45 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -7,7 +7,7 @@ from factory.fuzzy import FuzzyChoice, FuzzyDateTime from pytz import UTC -from task_office.core.models.db_models import User, Board, BoardColumn, Task +from task_office.core.models.db_models import User, Board, BoardColumn, Task, Permission from task_office.extensions.db import db as _db @@ -79,6 +79,17 @@ class Meta: model = Task +class PermissionFactory(BaseFactory): + """Permission factory.""" + + role = FuzzyChoice(choices=Permission.Role.get_values()) + + class Meta: + """Factory configuration.""" + + model = Permission + + class BoardFactory(BaseFactory): """Board factory.""" @@ -92,6 +103,15 @@ def columns(self, create, extracted, **kwargs): BoardColumnFactory(board_uuid=self.uuid) BoardColumnFactory(board_uuid=self.uuid) + @factory.post_generation + def perms(self, create, extracted, **kwargs): + if create: + PermissionFactory( + board_uuid=self.uuid, + user_uuid=UserFactory().uuid, + role=Permission.Role.EDITOR.value, + ) + class Meta: """Factory configuration.""" diff --git a/tests/fixtures/model_fixtures.py b/tests/fixtures/model_fixtures.py index bdf1637..e12441b 100644 --- a/tests/fixtures/model_fixtures.py +++ b/tests/fixtures/model_fixtures.py @@ -1,6 +1,7 @@ import pytest -from tests.factories import UserFactory, BoardFactory +from task_office.core.models.db_models import Permission +from tests.factories import UserFactory, BoardFactory, PermissionFactory @pytest.fixture(scope="function") @@ -25,10 +26,11 @@ def get_list(): def ses_boards(session_users): """A boards for the tests.""" user = session_users.get_single() - boards = BoardFactory.create_batch(3) + boards = BoardFactory.create_batch(3, owner_uuid=str(user.uuid)) for item in boards: - item.user = user - item.save() + PermissionFactory( + role=Permission.Role.EDITOR.value, user_uuid=user.uuid, board_uuid=item.uuid + ) class Board: @staticmethod @@ -47,6 +49,11 @@ def func_boards(func_users, db): """A boards for the tests.""" user = func_users.get_single() boards = BoardFactory.create_batch(5, owner_uuid=str(user.uuid)) + boards = BoardFactory.create_batch(3, owner_uuid=str(user.uuid)) + for item in boards: + PermissionFactory( + role=Permission.Role.EDITOR.value, user_uuid=user.uuid, board_uuid=item.uuid + ) db.session.commit() class Board: diff --git a/tests/integration/app/columns/test_api_columns.py b/tests/integration/app/columns/test_api_columns.py index e8ba20c..ea8f93e 100644 --- a/tests/integration/app/columns/test_api_columns.py +++ b/tests/integration/app/columns/test_api_columns.py @@ -31,8 +31,7 @@ def test_get_board_columns_metadata_without_auth(testapp, func_boards): def test_get_board_columns_metadata_success(testapp, func_boards, auth_user): url = url_for( - "api_v1.get_columns_meta_data", - board_uuid=uuid.UUID(func_boards.get_single().uuid).hex, + "api_v1.get_columns_meta_data", board_uuid=func_boards.get_single().uuid, ) token = auth_user["auth_data"]["tokens"]["access"]["token"] headers = {"Authorization": f"Bearer {token}"} @@ -40,10 +39,7 @@ def test_get_board_columns_metadata_success(testapp, func_boards, auth_user): def test_get_board_columns_success(testapp, func_boards, auth_user): - url = url_for( - "api_v1.get_list_columns", - board_uuid=uuid.UUID(func_boards.get_single().uuid).hex, - ) + url = url_for("api_v1.get_list_columns", board_uuid=func_boards.get_single().uuid,) token = auth_user["auth_data"]["tokens"]["access"]["token"] headers = {"Authorization": f"Bearer {token}"} testapp.get(url, headers=headers, status=200) @@ -52,9 +48,7 @@ def test_get_board_columns_success(testapp, func_boards, auth_user): def test_create_board_columns_success( testapp, auth_user, func_boards, columns_valid_data ): - url = url_for( - "api_v1.create_column", board_uuid=uuid.UUID(func_boards.get_single().uuid).hex, - ) + url = url_for("api_v1.create_column", board_uuid=func_boards.get_single().uuid,) token = auth_user["auth_data"]["tokens"]["access"]["token"] headers = {"Authorization": f"Bearer {token}"} testapp.post_json(url, columns_valid_data, headers=headers, status=200) @@ -63,19 +57,17 @@ def test_create_board_columns_success( def test_create_board_columns_failed( testapp, auth_user, func_boards, columns_invalid_data ): - url = url_for( - "api_v1.create_column", board_uuid=uuid.UUID(func_boards.get_single().uuid).hex, - ) + url = url_for("api_v1.create_column", board_uuid=func_boards.get_single().uuid,) token = auth_user["auth_data"]["tokens"]["access"]["token"] headers = {"Authorization": f"Bearer {token}"} testapp.post_json(url, columns_invalid_data, headers=headers, status=422) def test_update_board_columns(testapp, func_boards, auth_user): - board_uuid = uuid.UUID(func_boards.get_single().uuid).hex - column_uuid = uuid.UUID(func_boards.get_single().columns[0].uuid).hex + board = func_boards.get_single() + column = board.columns[0] url = url_for( - "api_v1.update_column", board_uuid=board_uuid, column_uuid=column_uuid + "api_v1.update_column", board_uuid=board.uuid, column_uuid=column.uuid ) token = auth_user["auth_data"]["tokens"]["access"]["token"] @@ -88,7 +80,7 @@ def test_update_board_columns(testapp, func_boards, auth_user): def test_update_board_columns_not_exists(testapp, func_boards, auth_user): - board_uuid = uuid.UUID(func_boards.get_single().uuid).hex + board_uuid = func_boards.get_single() column_uuid = uuid.uuid4().hex url = url_for( "api_v1.update_column", board_uuid=board_uuid, column_uuid=column_uuid diff --git a/tests/integration/app/permissions/__init__.py b/tests/integration/app/permissions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/app/permissions/fixtures.py b/tests/integration/app/permissions/fixtures.py new file mode 100644 index 0000000..2a493c7 --- /dev/null +++ b/tests/integration/app/permissions/fixtures.py @@ -0,0 +1,17 @@ +import pytest + +from task_office.core.models.db_models import Permission + +PERMISSIONS_VALID_DATA = [{"role": role} for role in Permission.Role.get_values()] + +PERMISSIONS_INVALID_DATA = [] + + +@pytest.fixture(params=PERMISSIONS_VALID_DATA) +def role_valid_data(request): + return request.param + + +@pytest.fixture(params=PERMISSIONS_INVALID_DATA) +def role_invalid_data(request): + return request.param diff --git a/tests/integration/app/permissions/test_api_permissions.py b/tests/integration/app/permissions/test_api_permissions.py new file mode 100644 index 0000000..2063fb5 --- /dev/null +++ b/tests/integration/app/permissions/test_api_permissions.py @@ -0,0 +1,116 @@ +import uuid +from flask import url_for + +from task_office.core.models.db_models import Permission +from task_office.core.utils import non_empty_query_required +from tests.factories import UserFactory + + +def test_get_board_permissions_list_without_auth(testapp, func_boards): + url = url_for( + "api_v1.get_list_permission", + board_uuid=uuid.UUID(func_boards.get_single().uuid).hex, + ) + testapp.get(url, status=401) + + +def test_create_board_permissions_without_auth(testapp, func_boards, func_users): + board = func_boards.get_single() + url = url_for("api_v1.create_permission", board_uuid=board.uuid) + data = {"role": 2, "user_uuid": func_users.get_single().uuid} + testapp.post_json(url, data, status=401) + + +def test_update_board_permission_without_auth(testapp, func_boards): + board = func_boards.get_single() + permission = board.perms[0] + url = url_for( + "api_v1.update_permission", + board_uuid=board.uuid, + permission_uuid=permission.uuid, + ) + data = { + "role": 3, + } + testapp.put_json(url, data, status=401) + + +def test_get_board_permission_without_auth(testapp, func_boards): + board = func_boards.get_single() + permission = board.perms[0] + url = url_for( + "api_v1.get_permission_by_uuid", + board_uuid=board.uuid, + permission_uuid=permission.uuid, + ) + testapp.get(url, status=401) + + +def test_get_board_permissions_meta_without_auth(testapp, func_boards): + board = func_boards.get_single() + url = url_for("api_v1.get_permissions_meta_data", board_uuid=board.uuid,) + testapp.get(url, status=401) + + +def test_get_board_permissions_list(testapp, func_boards): + url = url_for( + "api_v1.get_list_permission", + board_uuid=uuid.UUID(func_boards.get_single().uuid).hex, + ) + testapp.get(url, status=401) + + +def test_get_board_permission(testapp, func_boards, auth_user): + board = func_boards.get_single() + permission = board.perms[0] + url = url_for( + "api_v1.get_permission_by_uuid", + board_uuid=board.uuid, + permission_uuid=permission.uuid, + ) + token = auth_user["auth_data"]["tokens"]["access"]["token"] + headers = {"Authorization": f"Bearer {token}"} + + testapp.get(url, headers=headers, status=200) + + +def test_get_board_permissions_meta(testapp, func_boards, auth_user): + board = func_boards.get_single() + url = url_for("api_v1.get_permissions_meta_data", board_uuid=board.uuid,) + + token = auth_user["auth_data"]["tokens"]["access"]["token"] + headers = {"Authorization": f"Bearer {token}"} + + testapp.get(url, headers=headers, status=200) + + +def test_create_board_permissions(testapp, func_boards, auth_user, role_valid_data): + board = func_boards.get_single() + url = url_for("api_v1.create_permission", board_uuid=board.uuid) + role_valid_data["user_uuid"] = UserFactory().uuid + + token = auth_user["auth_data"]["tokens"]["access"]["token"] + headers = {"Authorization": f"Bearer {token}"} + + testapp.post_json(url, role_valid_data, headers=headers, status=200) + + +def test_update_board_permission(testapp, auth_user): + permission = auth_user["current_user"].perms[0] + + url = url_for( + "api_v1.update_permission", + board_uuid=permission.board_uuid, + permission_uuid=permission.uuid, + ) + + token = auth_user["auth_data"]["tokens"]["access"]["token"] + headers = {"Authorization": f"Bearer {token}"} + + data = { + "role": Permission.Role.STAFF.value, + "user_uuid": auth_user["current_user"].uuid, + } + resp = testapp.put_json(url, data, headers=headers, status=200) + + assert resp.json["role"] == permission.role From ffd12c3604926223b8f8258ac39b07b37b41cf19 Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Sun, 10 May 2020 22:06:54 +0300 Subject: [PATCH 57/60] updated: readme, todo; implemented: docker-compose-tests --- README.rst | 27 ++-- TODO.rst | 4 +- docker-compose-tests.yml | 35 +++++ task_office/settings.py | 13 -- .../app/permissions/test_api_permissions.py | 4 +- translations/en/LC_MESSAGES/messages.po | 145 ------------------ 6 files changed, 54 insertions(+), 174 deletions(-) create mode 100644 docker-compose-tests.yml delete mode 100644 translations/en/LC_MESSAGES/messages.po diff --git a/README.rst b/README.rst index 7fd3768..ea4095e 100644 --- a/README.rst +++ b/README.rst @@ -12,27 +12,35 @@ Task Office - pet app with using Flask Run Task Office ^^^^^^^^^^^^^^^^^^ -To run the app use:: +To run app local use:: $ cd $ cp .env.example .env - # run with flask wsgi: + # to run app perform: # install, run postgres, redis, actualize .env $ flask run --with-threads # type http://127.0.0.1:5000/ in browser - # run with docker-compose: + # !!! run tests only with docker-compose!!! + +To run app with docker use:: + + # to run app perform:: # install Docker, Docker Compose # https://docs.docker.com/v17.12/install/ # https://docs.docker.com/compose/install/ + $ cd $ docker-compose up --build - # type http://localhost/ in browser + # type http://127.0.0.1:80/ in browser -Test user credentials:: + # test user credentials:: + # username: amigo@gmaill.com + # password: amigo1111 - username: amigo@gmaill.com - password: amigo1111 + # run tests: + $ docker-compose -f docker-compose-tests.yml up + $ docker-compose -f docker-compose-tests.yml rm -fsv Run the following commands to create your app's @@ -50,7 +58,6 @@ Translations commands:: pybabel extract -F babel.cfg -k lazy_gettext -o translations/messages.pot . # Init translations(if not exists) - pybabel init -i messages.pot -d translations -l en pybabel init -i messages.pot -d translations -l uk pybabel init -i messages.pot -d translations -l ru @@ -61,7 +68,3 @@ Translations commands:: pybabel compile -d translations -Run tests:: - - pytest -s -v --cov=task_office tests/ - diff --git a/TODO.rst b/TODO.rst index 897d33b..8cdfebc 100644 --- a/TODO.rst +++ b/TODO.rst @@ -6,8 +6,8 @@ Issues General ^^^^^^^^^^^^^^ * Try to handle all errors and wrap them by our custom error class(InvalidUsage), with using translations * Implement auto-generated api -* Improve pagination -* Configure pytest +* Improve: app config, docker-compose-test.yaml, app config for test mode +* Speed up tests duration Issues By Features ^^^^^^^^^^^^^^^^^^ diff --git a/docker-compose-tests.yml b/docker-compose-tests.yml new file mode 100644 index 0000000..808b28d --- /dev/null +++ b/docker-compose-tests.yml @@ -0,0 +1,35 @@ +version: '3' + +services: + + web_service_test: + container_name: web_1_test + env_file: + - .env + environment: + MODE: "test" + build: + context: ./ + dockerfile: .deploy/local/api/Dockerfile + command: > + bash -c "flask db upgrade && pytest -s -v --cov=task_office tests/" + volumes: + - ./:/app + expose: + - 8000 + depends_on: + - postgres_service_test + - redis_service_test + + postgres_service_test: + container_name: postgres_1_test + image: postgres + env_file: + - .env + + redis_service_test: + container_name: redis_1_test + image: redis + command: redis-server --requirepass ${CACHE_REDIS_PASSWORD} + env_file: + - .env \ No newline at end of file diff --git a/task_office/settings.py b/task_office/settings.py index 24b2874..ef44555 100644 --- a/task_office/settings.py +++ b/task_office/settings.py @@ -112,19 +112,6 @@ class DevConfig(Config): class TestConfig(Config): """Test configuration.""" - DATABASE = Config.DATABASE - DATABASE["DB_NAME"] = env.str("POSTGRES_DB_TEST", "task_office_test") - SQLALCHEMY_DATABASE_URI = "postgresql://{username}:{password}@{host}:{db_port}/{db_name}".format( - username=DATABASE["DB_USER"], - password=DATABASE["DB_PASSWORD"], - host=DATABASE["DB_HOST"], - db_port=DATABASE["DB_PORT"], - db_name=DATABASE["DB_NAME"], - ) - CACHE = { - "CACHE_TYPE": "simple", - } - MODE = os.environ.get("MODE", default="dev") diff --git a/tests/integration/app/permissions/test_api_permissions.py b/tests/integration/app/permissions/test_api_permissions.py index 2063fb5..8a1e565 100644 --- a/tests/integration/app/permissions/test_api_permissions.py +++ b/tests/integration/app/permissions/test_api_permissions.py @@ -1,8 +1,8 @@ import uuid + from flask import url_for from task_office.core.models.db_models import Permission -from task_office.core.utils import non_empty_query_required from tests.factories import UserFactory @@ -95,7 +95,7 @@ def test_create_board_permissions(testapp, func_boards, auth_user, role_valid_da testapp.post_json(url, role_valid_data, headers=headers, status=200) -def test_update_board_permission(testapp, auth_user): +def test_update_board_permission(testapp, auth_user, func_boards): permission = auth_user["current_user"].perms[0] url = url_for( diff --git a/translations/en/LC_MESSAGES/messages.po b/translations/en/LC_MESSAGES/messages.po deleted file mode 100644 index b014038..0000000 --- a/translations/en/LC_MESSAGES/messages.po +++ /dev/null @@ -1,145 +0,0 @@ -# English translations for PROJECT. -# Copyright (C) 2020 ORGANIZATION -# This file is distributed under the same license as the PROJECT project. -# FIRST AUTHOR , 2020. -# -msgid "" -msgstr "" -"Project-Id-Version: PROJECT VERSION\n" -"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2020-04-06 08:46+0300\n" -"PO-Revision-Date: 2020-03-28 18:05+0200\n" -"Last-Translator: FULL NAME \n" -"Language: en\n" -"Language-Team: en \n" -"Plural-Forms: nplurals=2; plural=(n != 1)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=utf-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.8.0\n" - -#: task_office/auth/jwt_error_handlers.py:44 -msgid "Token has been revoked" -msgstr "Token has been revoked" - -#: task_office/auth/jwt_error_handlers.py:48 -msgid "Fresh token required" -msgstr "Fresh token required" - -#: task_office/auth/jwt_error_handlers.py:57 -msgid "Error loading the user {}" -msgstr "Error loading the user {}" - -#: task_office/auth/jwt_error_handlers.py:63 -msgid "User claims verification failed" -msgstr "User claims verification failed" - -#: task_office/auth/schemas.py:25 -msgid "Passwords do not match" -msgstr "Passwords do not match" - -#: task_office/auth/schemas.py:55 -msgid "User not found" -msgstr "User not found" - -#: task_office/auth/utils.py:57 task_office/permissions/views.py:69 -#: task_office/permissions/views.py:99 -msgid "Not allowed" -msgstr "Not allowed" - -#: task_office/columns/views.py:60 task_office/tasks/views.py:79 -msgid "Must be between {} and {}" -msgstr "Must be between {} and {}" - -#: task_office/columns/views.py:71 task_office/columns/views.py:121 -#: task_office/core/validators.py:10 task_office/tasks/views.py:93 -#: task_office/tasks/views.py:163 -msgid "Already exists with value {}" -msgstr "Already exists with value {}" - -#: task_office/columns/views.py:108 task_office/tasks/views.py:142 -msgid "Must be between {} and {}." -msgstr "Must be between {} and {}." - -#: task_office/core/enums.py:43 -msgid "Ascend" -msgstr "Ascend" - -#: task_office/core/enums.py:44 -msgid "Descend" -msgstr "Descend" - -#: task_office/core/utils.py:23 task_office/core/utils.py:53 -#: task_office/core/utils.py:57 task_office/permissions/views.py:36 -#: task_office/permissions/views.py:58 task_office/permissions/views.py:92 -#: task_office/permissions/views.py:124 task_office/permissions/views.py:147 -msgid "Not found" -msgstr "Not found" - -#: task_office/core/utils.py:31 -msgid "Already exists" -msgstr "Already exists" - -#: task_office/core/validators.py:37 -msgid "Not found with value {}" -msgstr "Not found with value {}" - -#: task_office/core/helpers/listed_response.py:10 -msgid "Max value {} exceeded" -msgstr "Max value {} exceeded" - -#: task_office/core/models/db_models.py:76 -msgid "Owner" -msgstr "Owner" - -#: task_office/core/models/db_models.py:76 -msgid "Owner of board(creator)" -msgstr "Owner of board(creator)" - -#: task_office/core/models/db_models.py:77 -msgid "Editor" -msgstr "Editor" - -#: task_office/core/models/db_models.py:77 -msgid "Editor of board" -msgstr "Editor of board" - -#: task_office/core/models/db_models.py:78 -msgid "Staff" -msgstr "Staff" - -#: task_office/core/models/db_models.py:78 -msgid "Ordinary user" -msgstr "Ordinary user" - -#: task_office/core/models/db_models.py:130 -msgid "New" -msgstr "New" - -#: task_office/core/models/db_models.py:131 -msgid "In process" -msgstr "In process" - -#: task_office/core/models/db_models.py:132 -msgid "Rejected" -msgstr "Rejected" - -#: task_office/core/models/db_models.py:133 -msgid "Done" -msgstr "Done" - -#: task_office/tasks/schemas/basic_schemas.py:130 -#: task_office/tasks/schemas/basic_schemas.py:132 -#: task_office/tasks/schemas/basic_schemas.py:180 -msgid "ascending" -msgstr "ascending" - -#: task_office/tasks/schemas/basic_schemas.py:131 -#: task_office/tasks/schemas/basic_schemas.py:133 -#: task_office/tasks/schemas/basic_schemas.py:181 -msgid "descending" -msgstr "descending" - -#~ msgid "Max limit {} exceeded" -#~ msgstr "Max limit {} exceeded" - From c9d7ef4c3fb40ca06ebf1acf1a14a170c6b7c497 Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Sat, 16 May 2020 15:43:22 +0300 Subject: [PATCH 58/60] light fixtures improve --- .gitignore | 2 ++ tests/fixtures/model_fixtures.py | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 5a904b9..fb70ca3 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,8 @@ *.so .idea* +.env + # Local development ignores venv data.sqlite diff --git a/tests/fixtures/model_fixtures.py b/tests/fixtures/model_fixtures.py index e12441b..26227a7 100644 --- a/tests/fixtures/model_fixtures.py +++ b/tests/fixtures/model_fixtures.py @@ -8,7 +8,6 @@ def func_users(db): """A users for the tests.""" users = UserFactory.create_batch(3) - db.session.commit() class User: @staticmethod From 48dedb1270df686199a7da4fb97673b702b21680 Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Sat, 16 May 2020 15:59:02 +0300 Subject: [PATCH 59/60] updated readme --- README.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index ea4095e..2764307 100644 --- a/README.rst +++ b/README.rst @@ -43,6 +43,10 @@ To run app with docker use:: $ docker-compose -f docker-compose-tests.yml rm -fsv +API docs url:: + + /api/v1/docs + Run the following commands to create your app's database tables and perform the initial migration :: @@ -67,4 +71,3 @@ Translations commands:: # Compile translations pybabel compile -d translations - From 788aff87b35ed214051a3b917c3277237629dc72 Mon Sep 17 00:00:00 2001 From: Medniy2000 Date: Sat, 30 May 2020 13:25:47 +0300 Subject: [PATCH 60/60] tests fixes --- docker-compose-tests.yml | 10 +- requirements/dev.txt | 5 +- task_office/settings.py | 2 +- tests/integration/app/auth/fixtures.py | 89 -------- tests/integration/app/auth/test_api_auth.py | 100 ++++++++- tests/integration/app/boards/fixtures.py | 61 ------ .../integration/app/boards/test_api_boards.py | 67 +++++- tests/integration/app/columns/fixtures.py | 38 ---- .../app/columns/test_api_columns.py | 50 ++++- tests/integration/app/permissions/fixtures.py | 16 -- .../app/permissions/test_api_permissions.py | 11 +- tests/integration/app/tasks/fixtures.py | 190 ---------------- tests/integration/app/tasks/test_api_tasks.py | 202 +++++++++++++++++- 13 files changed, 403 insertions(+), 438 deletions(-) diff --git a/docker-compose-tests.yml b/docker-compose-tests.yml index 808b28d..a541457 100644 --- a/docker-compose-tests.yml +++ b/docker-compose-tests.yml @@ -2,7 +2,7 @@ version: '3' services: - web_service_test: + web_service: container_name: web_1_test env_file: - .env @@ -18,16 +18,16 @@ services: expose: - 8000 depends_on: - - postgres_service_test - - redis_service_test + - postgres_service + - redis_service - postgres_service_test: + postgres_service: container_name: postgres_1_test image: postgres env_file: - .env - redis_service_test: + redis_service: container_name: redis_1_test image: redis command: redis-server --requirepass ${CACHE_REDIS_PASSWORD} diff --git a/requirements/dev.txt b/requirements/dev.txt index c53a4bb..a0bbaa8 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -13,4 +13,7 @@ pytest-mock>=3.1.0 pytest-cov>=2.8.1 factory-boy WebTest -Faker \ No newline at end of file +Faker + +# Formatters +black \ No newline at end of file diff --git a/task_office/settings.py b/task_office/settings.py index ef44555..d3d8745 100644 --- a/task_office/settings.py +++ b/task_office/settings.py @@ -17,7 +17,7 @@ class Config(object): APP_DIR = os.path.abspath(os.path.dirname(__file__)) PROJECT_ROOT = os.path.abspath(os.path.join(APP_DIR, os.pardir)) - READ_DOT_ENV_FILE = env.bool("FLASK_READ_DOT_ENV_FILE", default=False) + READ_DOT_ENV_FILE = env.bool("FLASK_READ_DOT_ENV_FILE", default=True) if READ_DOT_ENV_FILE: # OS environment variables take precedence over variables from .env env.read_env(os.path.join(PROJECT_ROOT, ".env")) diff --git a/tests/integration/app/auth/fixtures.py b/tests/integration/app/auth/fixtures.py index c9508b1..8b13789 100644 --- a/tests/integration/app/auth/fixtures.py +++ b/tests/integration/app/auth/fixtures.py @@ -1,90 +1 @@ -import pytest -from task_office.core.utils import generate_str - -SIGN_UP_USERS_VALID_DATA = [ - { - "password_confirm": "user11", - "password": "user11", - "email": "user1@gmaill.com", - "username": "usr1", - }, - { - "password_confirm": "user5678910124556122345811111111", - "password": "user5678910124556122345811111111", - "email": "user2@gmaill.com", - "username": "usr2", - }, - { - "password_confirm": "user11", - "password": "user11", - "email": "user3@gmaill.com", - "username": "usr3123456usr3123456usr3123456usr3123456usr3123456usr3123456usr3123456usr3123456", - }, -] - -SIGN_UP_USERS_INVALID_DATA = [ - # to short password - { - "password_confirm": "us", - "password": "us", - "email": "user1@gmaill.com", - "username": "usr1", - }, - # invalid email - { - "password_confirm": "user11", - "password": "user11", - "email": "user1gmaill.com", - "username": "usr1", - }, - # to short username - { - "password_confirm": "user11", - "password": "user11", - "email": "user1@gmaill.com", - "username": "us", - }, - # to long password - { - "password_confirm": "user56789101245561223458111111111", - "password": "user56789101245561223458111111111", - "email": "user2@gmaill.com", - "username": "usr2", - }, - # to long username - { - "password_confirm": "user11", - "password": "user11", - "email": "user3@gmaill.com", - "username": "usr3123456usr3123456usr3123456usr3123456usr3123456usr3123456usr3123456usr31234561", - }, - # passwords not equal - { - "password_confirm": "user12", - "password": "user11", - "email": "user3@gmaill.com", - "username": "usr3123456usr3123456usr3123456usr3123456usr3123456usr3123456usr3123456usr3123456", - }, -] - -REFRESH_INVALID = [ - "", - None, - generate_str(25), -] - - -@pytest.fixture(params=SIGN_UP_USERS_VALID_DATA) -def sign_up_users_valid_data(request): - return request.param - - -@pytest.fixture(params=SIGN_UP_USERS_INVALID_DATA) -def sign_up_users_invalid_data(request): - return request.param - - -@pytest.fixture(params=REFRESH_INVALID) -def refresh_users_invalid_data(request): - return request.param diff --git a/tests/integration/app/auth/test_api_auth.py b/tests/integration/app/auth/test_api_auth.py index 9239fe4..3fdfb13 100644 --- a/tests/integration/app/auth/test_api_auth.py +++ b/tests/integration/app/auth/test_api_auth.py @@ -1,18 +1,89 @@ import uuid +import pytest from flask import url_for +from task_office.core.utils import generate_str from tests.factories import USER_FACTORY_DEFAULT_PASSWORD +SIGN_UP_USERS_VALID_DATA = [ + { + "password_confirm": "user11", + "password": "user11", + "email": "user1@gmaill.com", + "username": "usr1", + }, + { + "password_confirm": "user5678910124556122345811111111", + "password": "user5678910124556122345811111111", + "email": "user2@gmaill.com", + "username": "usr2", + }, + { + "password_confirm": "user11", + "password": "user11", + "email": "user3@gmaill.com", + "username": "usr3123456usr3123456usr3123456usr3123456usr3123456usr3123456usr3123456usr3123456", + }, +] -def test_sign_up_success(testapp, sign_up_users_valid_data): + +@pytest.mark.parametrize("data", SIGN_UP_USERS_VALID_DATA) +def test_sign_up_success(testapp, data): url = url_for("api_v1.sign_up") - testapp.post_json(url, sign_up_users_valid_data, status=200) + testapp.post_json(url, data, status=200) + + +SIGN_UP_USERS_INVALID_DATA = [ + # to short password + { + "password_confirm": "us", + "password": "us", + "email": "user1@gmaill.com", + "username": "usr1", + }, + # invalid email + { + "password_confirm": "user11", + "password": "user11", + "email": "user1gmaill.com", + "username": "usr1", + }, + # to short username + { + "password_confirm": "user11", + "password": "user11", + "email": "user1@gmaill.com", + "username": "us", + }, + # to long password + { + "password_confirm": "user56789101245561223458111111111", + "password": "user56789101245561223458111111111", + "email": "user2@gmaill.com", + "username": "usr2", + }, + # to long username + { + "password_confirm": "user11", + "password": "user11", + "email": "user3@gmaill.com", + "username": "usr3123456usr3123456usr3123456usr3123456usr3123456usr3123456usr3123456usr31234561", + }, + # passwords not equal + { + "password_confirm": "user12", + "password": "user11", + "email": "user3@gmaill.com", + "username": "usr3123456usr3123456usr3123456usr3123456usr3123456usr3123456usr3123456usr3123456", + }, +] -def test_sign_up_failed(testapp, sign_up_users_invalid_data): +@pytest.mark.parametrize("data", SIGN_UP_USERS_INVALID_DATA) +def test_sign_up_failed(testapp, data): url = url_for("api_v1.sign_up") - testapp.post_json(url, sign_up_users_invalid_data, status=422) + testapp.post_json(url, data, status=422) def test_sign_up_already_exists_username(testapp, func_users): @@ -43,16 +114,17 @@ def test_sign_up_already_exists_email(testapp, func_users): testapp.post_json(url, data, status=422) -def test_sign_up_in_success(testapp, sign_up_users_valid_data): +@pytest.mark.parametrize("data", SIGN_UP_USERS_VALID_DATA) +def test_sign_up_in_success(testapp, data): # sign up sign_up_url = url_for("api_v1.sign_up") - testapp.post_json(sign_up_url, sign_up_users_valid_data, status=200) + testapp.post_json(sign_up_url, data, status=200) # sign in sign_in_url = url_for("api_v1.sign_in") data = { - "email": sign_up_users_valid_data["email"], - "password": sign_up_users_valid_data["password"], + "email": data["email"], + "password": data["password"], } testapp.post_json(sign_in_url, data, status=200) @@ -100,7 +172,15 @@ def test_refresh_success(testapp, auth_user): testapp.post_json(url, headers=headers, status=200) -def test_refresh_failed(testapp, refresh_users_invalid_data): +REFRESH_INVALID = [ + "", + None, + generate_str(25), +] + + +@pytest.mark.parametrize("data", REFRESH_INVALID) +def test_refresh_failed(testapp, data): url = url_for("api_v1.refresh",) - headers = {"Authorization": f"Bearer {refresh_users_invalid_data}"} + headers = {"Authorization": f"Bearer {data}"} testapp.post_json(url, headers=headers, status=422) diff --git a/tests/integration/app/boards/fixtures.py b/tests/integration/app/boards/fixtures.py index d740099..5871ed8 100644 --- a/tests/integration/app/boards/fixtures.py +++ b/tests/integration/app/boards/fixtures.py @@ -1,62 +1 @@ import pytest - -from task_office.core.utils import generate_str - -BOARDS_VALID_DATA = [ - # Typical board data - {"name": "Board 1# name", "description": "Board #3 description", "is_active": True}, - # Data without is_active - {"name": "Board 2# name", "description": "Board #3 description"}, - # Data without description - {"name": "Board 3# name", "is_active": True}, - # Data without description, is_active - {"name": "Board 4# name"}, - # Data with min length name - {"name": "B", "description": "Board #3 description", "is_active": True}, - # Data with max length name(80) - { - "name": generate_str(80), - "description": "Board #3 description", - "is_active": True, - }, - # Data with min length description - {"name": "Board 5# name", "description": "", "is_active": True}, - # Data with max length description(255) - {"name": "Board 5# name", "description": generate_str(255), "is_active": True}, -] - - -BOARDS_INVALID_DATA = [ - # Data with zero length name - {"name": "", "description": "Board #1 description", "is_active": True}, - # Data without name - {"description": "Board #2 description", "is_active": True}, - # Data without None name - {"name": None, "description": "Board #3 description", "is_active": True}, - # Data with exceeded length name - { - "name": generate_str(81), - "description": "Board #1 description", - "is_active": True, - }, - # Data with exceeded description - {"name": "Board 1# name", "description": generate_str(256), "is_active": True}, - # Data with None description - {"name": "Board 2# name", "description": None, "is_active": True}, - # Data with incorrect name type - {"name": True, "description": None, "is_active": True}, - # Data with incorrect description type - {"name": "Board 3# name", "description": 12345, "is_active": True}, - # Data with incorrect is_active type - {"name": "Board 4# name", "description": "Board #2 description", "is_active": 48}, -] - - -@pytest.fixture(params=BOARDS_VALID_DATA) -def boards_valid_data(request): - return request.param - - -@pytest.fixture(params=BOARDS_INVALID_DATA) -def boards_invalid_data(request): - return request.param diff --git a/tests/integration/app/boards/test_api_boards.py b/tests/integration/app/boards/test_api_boards.py index a243ce5..e756184 100644 --- a/tests/integration/app/boards/test_api_boards.py +++ b/tests/integration/app/boards/test_api_boards.py @@ -1,7 +1,10 @@ import uuid +import pytest as pytest from flask import url_for +from task_office.core.utils import generate_str + def test_get_list_boards_without_auth(testapp): url = url_for("api_v1.get_list_boards") @@ -71,18 +74,70 @@ def test_get_board_users(testapp, func_boards, auth_user): testapp.get(url, headers=headers, status=200) -def test_create_board_success(testapp, auth_user, boards_valid_data): +BOARDS_VALID_DATA = [ + # Typical board data + {"name": "Board 1# name", "description": "Board #3 description", "is_active": True}, + # Data without is_active + {"name": "Board 2# name", "description": "Board #3 description"}, + # Data without description + {"name": "Board 3# name", "is_active": True}, + # Data without description, is_active + {"name": "Board 4# name"}, + # Data with min length name + {"name": "B", "description": "Board #3 description", "is_active": True}, + # Data with max length name(80) + { + "name": generate_str(80), + "description": "Board #3 description", + "is_active": True, + }, + # Data with min length description + {"name": "Board 5# name", "description": "", "is_active": True}, + # Data with max length description(255) + {"name": "Board 5# name", "description": generate_str(255), "is_active": True}, +] + + +@pytest.mark.parametrize("data", BOARDS_VALID_DATA) +def test_create_board_success(testapp, auth_user, data): url = url_for("api_v1.create_board") token = auth_user["auth_data"]["tokens"]["access"]["token"] headers = {"Authorization": f"Bearer {token}"} - testapp.post_json(url, boards_valid_data, headers=headers, status=200) - - -def test_create_board_failed(testapp, auth_user, boards_invalid_data): + testapp.post_json(url, data, headers=headers, status=200) + + +BOARDS_INVALID_DATA = [ + # Data with zero length name + {"name": "", "description": "Board #1 description", "is_active": True}, + # Data without name + {"description": "Board #2 description", "is_active": True}, + # Data without None name + {"name": None, "description": "Board #3 description", "is_active": True}, + # Data with exceeded length name + { + "name": generate_str(81), + "description": "Board #1 description", + "is_active": True, + }, + # Data with exceeded description + {"name": "Board 1# name", "description": generate_str(256), "is_active": True}, + # Data with None description + {"name": "Board 2# name", "description": None, "is_active": True}, + # Data with incorrect name type + {"name": True, "description": None, "is_active": True}, + # Data with incorrect description type + {"name": "Board 3# name", "description": 12345, "is_active": True}, + # Data with incorrect is_active type + {"name": "Board 4# name", "description": "Board #2 description", "is_active": 48}, +] + + +@pytest.mark.parametrize("data", BOARDS_INVALID_DATA) +def test_create_board_failed(testapp, auth_user, data): url = url_for("api_v1.create_board") token = auth_user["auth_data"]["tokens"]["access"]["token"] headers = {"Authorization": f"Bearer {token}"} - testapp.post_json(url, boards_invalid_data, headers=headers, status=422) + testapp.post_json(url, data, headers=headers, status=422) def test_update_board(testapp, func_boards, auth_user): diff --git a/tests/integration/app/columns/fixtures.py b/tests/integration/app/columns/fixtures.py index 9102334..5871ed8 100644 --- a/tests/integration/app/columns/fixtures.py +++ b/tests/integration/app/columns/fixtures.py @@ -1,39 +1 @@ import pytest - -from task_office.core.utils import generate_str - -COLUMNS_VALID_DATA = [ - # Typical board data - {"name": "BoardColumn 1# name", "position": 1}, - # With min length name - {"name": "B", "position": 1}, - # With max length name - {"name": generate_str(120), "position": 1}, -] - -COLUMNS_INVALID_DATA = [ - # With empty name - {"name": "", "position": 1}, - # With None Name - {"name": None, "position": 1}, - # Without name - {"position": 1}, - # With exceeded name length - {"name": generate_str(121), "position": 1}, - # With to small position - {"name": "BoardColumn 1# name", "position": 0}, - # With None position - {"name": "BoardColumn 1# name", "position": None}, - # Without position - {"name": "BoardColumn 1# name"}, -] - - -@pytest.fixture(params=COLUMNS_VALID_DATA) -def columns_valid_data(request): - return request.param - - -@pytest.fixture(params=COLUMNS_INVALID_DATA) -def columns_invalid_data(request): - return request.param diff --git a/tests/integration/app/columns/test_api_columns.py b/tests/integration/app/columns/test_api_columns.py index ea8f93e..7b6d96d 100644 --- a/tests/integration/app/columns/test_api_columns.py +++ b/tests/integration/app/columns/test_api_columns.py @@ -1,6 +1,10 @@ import uuid + +import pytest from flask import url_for +from task_office.core.utils import generate_str + def test_get_board_columns_without_auth(testapp, func_boards): url = url_for( @@ -45,22 +49,48 @@ def test_get_board_columns_success(testapp, func_boards, auth_user): testapp.get(url, headers=headers, status=200) -def test_create_board_columns_success( - testapp, auth_user, func_boards, columns_valid_data -): +COLUMNS_VALID_DATA = [ + # Typical board data + {"name": "BoardColumn 1# name", "position": 1}, + # With min length name + {"name": "B", "position": 1}, + # With max length name + {"name": generate_str(120), "position": 1}, +] + + +@pytest.mark.parametrize("data", COLUMNS_VALID_DATA) +def test_create_board_columns_success(testapp, auth_user, func_boards, data): url = url_for("api_v1.create_column", board_uuid=func_boards.get_single().uuid,) token = auth_user["auth_data"]["tokens"]["access"]["token"] headers = {"Authorization": f"Bearer {token}"} - testapp.post_json(url, columns_valid_data, headers=headers, status=200) - - -def test_create_board_columns_failed( - testapp, auth_user, func_boards, columns_invalid_data -): + testapp.post_json(url, data, headers=headers, status=200) + + +COLUMNS_INVALID_DATA = [ + # With empty name + {"name": "", "position": 1}, + # With None Name + {"name": None, "position": 1}, + # Without name + {"position": 1}, + # With exceeded name length + {"name": generate_str(121), "position": 1}, + # With to small position + {"name": "BoardColumn 1# name", "position": 0}, + # With None position + {"name": "BoardColumn 1# name", "position": None}, + # Without position + {"name": "BoardColumn 1# name"}, +] + + +@pytest.mark.parametrize("data", COLUMNS_INVALID_DATA) +def test_create_board_columns_failed(testapp, auth_user, func_boards, data): url = url_for("api_v1.create_column", board_uuid=func_boards.get_single().uuid,) token = auth_user["auth_data"]["tokens"]["access"]["token"] headers = {"Authorization": f"Bearer {token}"} - testapp.post_json(url, columns_invalid_data, headers=headers, status=422) + testapp.post_json(url, data, headers=headers, status=422) def test_update_board_columns(testapp, func_boards, auth_user): diff --git a/tests/integration/app/permissions/fixtures.py b/tests/integration/app/permissions/fixtures.py index 2a493c7..5871ed8 100644 --- a/tests/integration/app/permissions/fixtures.py +++ b/tests/integration/app/permissions/fixtures.py @@ -1,17 +1 @@ import pytest - -from task_office.core.models.db_models import Permission - -PERMISSIONS_VALID_DATA = [{"role": role} for role in Permission.Role.get_values()] - -PERMISSIONS_INVALID_DATA = [] - - -@pytest.fixture(params=PERMISSIONS_VALID_DATA) -def role_valid_data(request): - return request.param - - -@pytest.fixture(params=PERMISSIONS_INVALID_DATA) -def role_invalid_data(request): - return request.param diff --git a/tests/integration/app/permissions/test_api_permissions.py b/tests/integration/app/permissions/test_api_permissions.py index 8a1e565..677bfda 100644 --- a/tests/integration/app/permissions/test_api_permissions.py +++ b/tests/integration/app/permissions/test_api_permissions.py @@ -1,5 +1,6 @@ import uuid +import pytest from flask import url_for from task_office.core.models.db_models import Permission @@ -84,15 +85,19 @@ def test_get_board_permissions_meta(testapp, func_boards, auth_user): testapp.get(url, headers=headers, status=200) -def test_create_board_permissions(testapp, func_boards, auth_user, role_valid_data): +PERMISSIONS_VALID_DATA = [{"role": role} for role in Permission.Role.get_values()] + + +@pytest.mark.parametrize("data", PERMISSIONS_VALID_DATA) +def test_create_board_permissions(testapp, func_boards, auth_user, data): board = func_boards.get_single() url = url_for("api_v1.create_permission", board_uuid=board.uuid) - role_valid_data["user_uuid"] = UserFactory().uuid + data["user_uuid"] = UserFactory().uuid token = auth_user["auth_data"]["tokens"]["access"]["token"] headers = {"Authorization": f"Bearer {token}"} - testapp.post_json(url, role_valid_data, headers=headers, status=200) + testapp.post_json(url, data, headers=headers, status=200) def test_update_board_permission(testapp, auth_user, func_boards): diff --git a/tests/integration/app/tasks/fixtures.py b/tests/integration/app/tasks/fixtures.py index bbc8549..5871ed8 100644 --- a/tests/integration/app/tasks/fixtures.py +++ b/tests/integration/app/tasks/fixtures.py @@ -1,191 +1 @@ import pytest - -from task_office.core.utils import generate_str - -TASKS_VALID_DATA = [ - # Typical task data - { - "label": "Label #1", - "expire_at": "2020-05-25 05:30:11", - "name": "Task #1 name", - "description": "Task #1 description", - "position": 1, - "state": 1, - "column_uuid": None, - "performers": [], - }, - # Task with max label name - { - "label": generate_str(size=80), - "expire_at": "2020-05-25 05:30:11", - "name": "Task #2 name", - "description": "Task #2 description", - "position": 1, - "state": 1, - "column_uuid": None, - "performers": [], - }, - # Task without label - { - "expire_at": "2020-05-25 05:30:11", - "name": "Task #3 name", - "description": "Task #3 description", - "position": 1, - "state": 1, - "column_uuid": None, - "performers": [], - }, - # Task without expire_at - { - "label": "Label #1", - "name": "Task #4 name", - "description": "Task #4 description", - "position": 1, - "state": 1, - "column_uuid": None, - "performers": [], - }, - # Task with min name length - { - "label": "Label #1", - "expire_at": "2020-05-25 05:30:11", - "name": "T", - "description": "Task #5 description", - "position": 1, - "state": 1, - "column_uuid": None, - "performers": [], - }, - # Task with max name length - { - "label": "Label #1", - "expire_at": "2020-05-25 05:30:11", - "name": "Task #6 name", - "description": "Task #6 description", - "position": 1, - "state": 1, - "column_uuid": None, - "performers": [], - }, - # Task with max description length - { - "label": "Label #1", - "expire_at": "2020-05-25 05:30:11", - "name": "Task #7 name", - "description": generate_str(120), - "position": 1, - "state": 1, - "column_uuid": None, - "performers": [], - }, - # Task without state - { - "label": "Label #1", - "expire_at": "2020-05-25 05:30:11", - "name": "Task #8 name", - "description": "Task #8 description", - "position": 1, - "column_uuid": None, - "performers": [], - }, - # Task without position - { - "label": "Label #1", - "expire_at": "2020-05-25 05:30:11", - "name": "Task #9 name", - "description": "Task #9 description", - "state": 1, - "column_uuid": None, - "performers": [], - }, -] - -TASKS_INVALID_DATA = [ - # Task with None expire_at - { - "label": "Label #1", - "expire_at": None, - "name": "Task #1 name", - "description": "Task #1 description", - "position": 1, - "state": 1, - "column_uuid": None, - "performers": [], - }, - # Task with None label - { - "label": None, - "expire_at": "2020-05-25 05:30:11", - "name": "Task #1 name", - "description": "Task #1 description", - "position": 1, - "state": 1, - "column_uuid": None, - "performers": [], - }, - # Task with empty name - { - "label": "Label #1", - "expire_at": "2020-05-25 05:30:11", - "name": "", - "description": "Task #1 description", - "position": 1, - "state": 1, - "column_uuid": None, - "performers": [], - }, - # Task with exceeded name length - { - "label": "Label #1", - "expire_at": "2020-05-25 05:30:11", - "name": generate_str(120), - "description": "Task #1 description", - "position": 1, - "state": 1, - "column_uuid": None, - "performers": [], - }, - # Task with exceeded description length - { - "label": "Label #1", - "expire_at": "2020-05-25 05:30:11", - "name": "Task #1 name", - "description": generate_str(121), - "position": 1, - "state": 1, - "column_uuid": None, - "performers": [], - }, - # Task with None state - { - "label": "Label #1", - "expire_at": "2020-05-25 05:30:11", - "name": "Task #1 name", - "description": "Task #1 description", - "position": 1, - "state": None, - "column_uuid": None, - "performers": [], - }, - # Task with None position - { - "label": "Label #1", - "expire_at": "2020-05-25 05:30:11", - "name": "Task #1 name", - "description": "Task #1 description", - "position": None, - "state": 1, - "column_uuid": None, - "performers": [], - }, -] - - -@pytest.fixture(params=TASKS_VALID_DATA) -def tasks_valid_data(request): - return request.param - - -@pytest.fixture(params=TASKS_INVALID_DATA) -def tasks_invalid_data(request): - return request.param diff --git a/tests/integration/app/tasks/test_api_tasks.py b/tests/integration/app/tasks/test_api_tasks.py index 7189adc..3628c32 100644 --- a/tests/integration/app/tasks/test_api_tasks.py +++ b/tests/integration/app/tasks/test_api_tasks.py @@ -1,6 +1,10 @@ import uuid + +import pytest from flask import url_for +from task_office.core.utils import generate_str + def test_get_tasks_without_auth(testapp, func_boards): board_uuid = func_boards.get_single().uuid @@ -80,30 +84,212 @@ def test_get_tasks_by_columns(testapp, func_boards, auth_user): testapp.get(url, headers=headers, status=200) -def create_tasks_success(testapp, func_boards, auth_user, tasks_valid_data): +TASKS_VALID_DATA = [ + # Typical task data + { + "label": "Label #1", + "expire_at": "2020-05-25 05:30:11", + "name": "Task #1 name", + "description": "Task #1 description", + "position": 1, + "state": 1, + "column_uuid": None, + "performers": [], + }, + # Task with max label name + { + "label": generate_str(size=80), + "expire_at": "2020-05-25 05:30:11", + "name": "Task #2 name", + "description": "Task #2 description", + "position": 1, + "state": 1, + "column_uuid": None, + "performers": [], + }, + # Task without label + { + "expire_at": "2020-05-25 05:30:11", + "name": "Task #3 name", + "description": "Task #3 description", + "position": 1, + "state": 1, + "column_uuid": None, + "performers": [], + }, + # Task without expire_at + { + "label": "Label #1", + "name": "Task #4 name", + "description": "Task #4 description", + "position": 1, + "state": 1, + "column_uuid": None, + "performers": [], + }, + # Task with min name length + { + "label": "Label #1", + "expire_at": "2020-05-25 05:30:11", + "name": "T", + "description": "Task #5 description", + "position": 1, + "state": 1, + "column_uuid": None, + "performers": [], + }, + # Task with max name length + { + "label": "Label #1", + "expire_at": "2020-05-25 05:30:11", + "name": "Task #6 name", + "description": "Task #6 description", + "position": 1, + "state": 1, + "column_uuid": None, + "performers": [], + }, + # Task with max description length + { + "label": "Label #1", + "expire_at": "2020-05-25 05:30:11", + "name": "Task #7 name", + "description": generate_str(120), + "position": 1, + "state": 1, + "column_uuid": None, + "performers": [], + }, + # Task without state + { + "label": "Label #1", + "expire_at": "2020-05-25 05:30:11", + "name": "Task #8 name", + "description": "Task #8 description", + "position": 1, + "column_uuid": None, + "performers": [], + }, + # Task without position + { + "label": "Label #1", + "expire_at": "2020-05-25 05:30:11", + "name": "Task #9 name", + "description": "Task #9 description", + "state": 1, + "column_uuid": None, + "performers": [], + }, +] + + +@pytest.mark.parametrize("data", TASKS_VALID_DATA) +def create_tasks_success(testapp, func_boards, auth_user, data): url = url_for( "api_v1.create_task", board_uuid=uuid.UUID(func_boards.get_single().uuid).hex, ) - tasks_valid_data["performers"].apend(auth_user["auth_data"]["uuid"]) - tasks_valid_data["column_uuid"] = func_boards.get_single().columns[0].uuid + data["performers"].apend(auth_user["auth_data"]["uuid"]) + data["column_uuid"] = func_boards.get_single().columns[0].uuid token = auth_user["auth_data"]["tokens"]["access"]["token"] headers = {"Authorization": f"Bearer {token}"} - testapp.post_json(url, tasks_valid_data, headers=headers, status=200) + testapp.post_json(url, data, headers=headers, status=200) + + +TASKS_INVALID_DATA = [ + # Task with None expire_at + { + "label": "Label #1", + "expire_at": None, + "name": "Task #1 name", + "description": "Task #1 description", + "position": 1, + "state": 1, + "column_uuid": None, + "performers": [], + }, + # Task with None label + { + "label": None, + "expire_at": "2020-05-25 05:30:11", + "name": "Task #1 name", + "description": "Task #1 description", + "position": 1, + "state": 1, + "column_uuid": None, + "performers": [], + }, + # Task with empty name + { + "label": "Label #1", + "expire_at": "2020-05-25 05:30:11", + "name": "", + "description": "Task #1 description", + "position": 1, + "state": 1, + "column_uuid": None, + "performers": [], + }, + # Task with exceeded name length + { + "label": "Label #1", + "expire_at": "2020-05-25 05:30:11", + "name": generate_str(120), + "description": "Task #1 description", + "position": 1, + "state": 1, + "column_uuid": None, + "performers": [], + }, + # Task with exceeded description length + { + "label": "Label #1", + "expire_at": "2020-05-25 05:30:11", + "name": "Task #1 name", + "description": generate_str(121), + "position": 1, + "state": 1, + "column_uuid": None, + "performers": [], + }, + # Task with None state + { + "label": "Label #1", + "expire_at": "2020-05-25 05:30:11", + "name": "Task #1 name", + "description": "Task #1 description", + "position": 1, + "state": None, + "column_uuid": None, + "performers": [], + }, + # Task with None position + { + "label": "Label #1", + "expire_at": "2020-05-25 05:30:11", + "name": "Task #1 name", + "description": "Task #1 description", + "position": None, + "state": 1, + "column_uuid": None, + "performers": [], + }, +] -def create_tasks_failed(testapp, func_boards, auth_user, tasks_invalid_data): +@pytest.mark.parametrize("data", TASKS_VALID_DATA) +def create_tasks_failed(testapp, func_boards, auth_user, data): url = url_for("api_v1.create_task", board_uuid=func_boards.get_single().uuid,) - tasks_invalid_data["performers"].apend(auth_user["auth_data"]["uuid"]) - tasks_invalid_data["column_uuid"] = func_boards.get_single().columns[0].uuid + data["performers"].apend(auth_user["auth_data"]["uuid"]) + data["column_uuid"] = func_boards.get_single().columns[0].uuid token = auth_user["auth_data"]["tokens"]["access"]["token"] headers = {"Authorization": f"Bearer {token}"} - testapp.post_json(url, tasks_invalid_data, headers=headers, status=422) + testapp.post_json(url, data, headers=headers, status=422) def update_tasks_success(testapp, func_boards, auth_user):