diff --git a/.gitignore b/.gitignore index b2ea7be0b10236568201e0a54fa9b50492441b8e..2031b28e2eb023cebe696bd923e866bb37e1c495 100644 --- a/.gitignore +++ b/.gitignore @@ -56,7 +56,7 @@ parsetab.py /lib/hawat/static/vendor/ /lib/hawat/templates/iana-tz-data/ /lib/hawat/blueprints/design/static/vendor/ -/lib/vial/blueprints/design_bs3/static/vendor/ +/lib/hawat/blueprints/design_bs3/static/vendor/ # Local NodeJS modules for Grunt. /node_modules/ diff --git a/MANIFEST.in b/MANIFEST.in index 1a38294a6be6d266870834da7d57456541885535..76efd8c5ea77f79a409c0e3c9ffcace3e56533d0 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -8,5 +8,4 @@ recursive-include lib/hawat/templates * recursive-include lib/hawat/migrations * recursive-include lib/hawat/translations * recursive-include lib/hawat/blueprints * -recursive-include lib/vial/blueprints * global-exclude *.pyc diff --git a/conf/mentat-hawat-dev.py.conf b/conf/mentat-hawat-dev.py.conf index 597fb8616c8b362ec704089c2dac404184e6ebfb..6586d32c09840512f58f54b92caf3501665ae16b 100644 --- a/conf/mentat-hawat-dev.py.conf +++ b/conf/mentat-hawat-dev.py.conf @@ -40,11 +40,11 @@ HAWAT_LOG_FILE = '/var/tmp/mentat-hawat.py.log' HAWAT_LOG_FILE_LEVEL = 'debug' ENABLED_BLUEPRINTS = [ - 'vial.blueprints.auth', - 'vial.blueprints.auth_api', - 'vial.blueprints.design_bs3', - 'vial.blueprints.devtools', - 'vial.blueprints.changelogs', + 'hawat.blueprints.auth', + 'hawat.blueprints.auth_api', + 'hawat.blueprints.design_bs3', + 'hawat.blueprints.devtools', + 'hawat.blueprints.changelogs', 'hawat.blueprints.auth_env', 'hawat.blueprints.auth_dev', diff --git a/conf/mentat-hawat.py.conf b/conf/mentat-hawat.py.conf index 1f42ac269cf1e70ed1ae383f7cf9c14f6698c33a..48e422757c522a3b78b7cf819b73a33dde735640 100644 --- a/conf/mentat-hawat.py.conf +++ b/conf/mentat-hawat.py.conf @@ -33,11 +33,11 @@ HAWAT_SEARCH_QUERY_QUOTA = 7 # List of enabled application blueprints. ENABLED_BLUEPRINTS = [ - 'vial.blueprints.auth', - 'vial.blueprints.auth_api', - 'vial.blueprints.design_bs3', - 'vial.blueprints.devtools', - 'vial.blueprints.changelogs', + 'hawat.blueprints.auth', + 'hawat.blueprints.auth_api', + 'hawat.blueprints.design_bs3', + 'hawat.blueprints.devtools', + 'hawat.blueprints.changelogs', 'hawat.blueprints.auth_env', #'hawat.blueprints.auth_dev', diff --git a/conf/requirements.pip b/conf/requirements.pip index 0154ce96b200647d06640a551696979bf63aec68..969a2363094f82884227fd70167166442a7dfa65 100644 --- a/conf/requirements.pip +++ b/conf/requirements.pip @@ -9,6 +9,7 @@ alembic==1.4.2 jinja2==3.0.3 blinker==1.4 bsddb3==6.2.7 +werkzeug==2.0.2 flask==1.1.1 flask-login==0.5.0 flask-mail==0.9.1 @@ -35,3 +36,4 @@ idea-format==0.1.11 python-dateutil==2.8.1 PyBabel-json-md==0.1.0 itsdangerous==2.0.1 + diff --git a/conf/scripts/sqldb-init.sh b/conf/scripts/sqldb-init.sh index 542761c10ba6eb19c4be0c0b937fe98c5b1ed5d6..4fd239bd9511cd8141bf78946edffbe0e8f48be7 100755 --- a/conf/scripts/sqldb-init.sh +++ b/conf/scripts/sqldb-init.sh @@ -15,7 +15,7 @@ if [ $? -ne 0 ]; then fi -for dbname in "mentat_events" "mentat_main" "mentat_utest" "mentat_bench" "vial" +for dbname in "mentat_events" "mentat_main" "mentat_utest" "mentat_bench" do sudo -u postgres psql -c "SELECT datname FROM pg_catalog.pg_database;" | grep $dbname > /dev/null if [ $? -ne 0 ]; then diff --git a/doc/sphinx/_doclib/development.rst b/doc/sphinx/_doclib/development.rst index e8f8dc6245729058505476cc1e51401a74992d0f..d4614826a8df72baa56f10c22e0936b55d9258d0 100644 --- a/doc/sphinx/_doclib/development.rst +++ b/doc/sphinx/_doclib/development.rst @@ -741,7 +741,7 @@ When that is done execute following command from within the development Vagrant .. code-block:: shell - (venv) !DEV! mentat@mentat-devel /vagrant $ make data-fetch-dbsnapshot + (venv) !DEV! mentat@mentat-devel /vagrant $ make data-import-dbsnapshot That is about it. When working in Vagrat box please note and keep in mind following: diff --git a/hawat.local.conf b/hawat.local.conf index 9628b947444be71b2598752c923a6f0c2df547c8..0513681fe9db1c3786d529994c3174fb972d8a4e 100644 --- a/hawat.local.conf +++ b/hawat.local.conf @@ -6,11 +6,11 @@ HAWAT_LOG_DEFAULT_LEVEL = 'debug' HAWAT_LOG_FILE = '/var/tmp/mentat-hawat.py.log' HAWAT_LOG_FILE_LEVEL = 'debug' ENABLED_BLUEPRINTS = [ - 'vial.blueprints.auth', - 'vial.blueprints.auth_api', - 'vial.blueprints.design_bs3', - 'vial.blueprints.devtools', - 'vial.blueprints.changelogs', + 'hawat.blueprints.auth', + 'hawat.blueprints.auth_api', + 'hawat.blueprints.design_bs3', + 'hawat.blueprints.devtools', + 'hawat.blueprints.changelogs', 'hawat.blueprints.auth_env', 'hawat.blueprints.auth_dev', diff --git a/lib/hawat/__init__.py b/lib/hawat/__init__.py index 54dc7070251cb9bb47b579b644e91e3c17800839..e42ca07805dbf0ea992e974450e108935d895fa8 100644 --- a/lib/hawat/__init__.py +++ b/lib/hawat/__init__.py @@ -1,15 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ -This package contains a web user interface for the Mentat system. +This package contains the web user interface for the Mentat system. Architecture @@ -87,6 +87,6 @@ assert create_app assert create_app_full -@click.group(cls = FlaskGroup, create_app = create_app) +@click.group(cls=FlaskGroup, create_app=create_app) def cli(): """Command line interface for the Hawat application.""" diff --git a/lib/hawat/acl.py b/lib/hawat/acl.py index 7c143607454e991ffa004a6c096548dac7846009..5995164cc62e8e2327324d6be83efc25ef00c28e 100644 --- a/lib/hawat/acl.py +++ b/lib/hawat/acl.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ @@ -21,7 +21,7 @@ from functools import partial import flask_principal -import vial.const +import hawat.const MembershipNeed = partial(flask_principal.Need, 'membership') # pylint: disable=locally-disabled,invalid-name @@ -31,12 +31,12 @@ ManagementNeed = partial(flask_principal.Need, 'management') # pylint: disable ManagementNeed.__doc__ = """A need with the method preset to `"management"`.""" -ROLE_NAME_ADMIN = vial.const.ROLE_ADMIN -ROLE_NAME_MAINTAINER = vial.const.ROLE_MAINTAINER -ROLE_NAME_POWER = 'power' -ROLE_NAME_DEVELOPER = vial.const.ROLE_DEVELOPER -ROLE_NAME_USER = vial.const.ROLE_USER -ROLE_NAME_ANY = 'any' +ROLE_NAME_ADMIN = hawat.const.ROLE_ADMIN +ROLE_NAME_MAINTAINER = hawat.const.ROLE_MAINTAINER +ROLE_NAME_POWER = 'power' +ROLE_NAME_DEVELOPER = hawat.const.ROLE_DEVELOPER +ROLE_NAME_USER = hawat.const.ROLE_USER +ROLE_NAME_ANY = 'any' PERMISSION_ADMIN = flask_principal.Permission( diff --git a/lib/hawat/app.py b/lib/hawat/app.py index 8f860f579dcb18c940376cc0e9d6cc11131365e1..2f8f1c1939fec3243d5545fb6fb0325c3d911788 100644 --- a/lib/hawat/app.py +++ b/lib/hawat/app.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ @@ -28,7 +28,7 @@ import pyzenkit.utils from mentat.const import CKEY_CORE_DATABASE, CKEY_CORE_DATABASE_SQLSTORAGE -import vial +import hawat from hawat.const import CFGKEY_MENTAT_CORE import hawat.base @@ -40,15 +40,15 @@ APP_NAME = 'hawat' """Name of the application as a constant for Flask.""" -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- def create_app_full( - config_dict = None, - config_object = 'hawat.config.ProductionConfig', - config_file = None, - config_env = 'HAWAT_CONFIG_FILE', - config_func = None): + config_dict=None, + config_object='hawat.config.ProductionConfig', + config_file=None, + config_env='HAWAT_CONFIG_FILE', + config_func=None): """ Factory function for building Hawat application. This function takes number of optional arguments, that can be used to create a very customized instance of @@ -57,24 +57,31 @@ def create_app_full( value for the most common application setup, so for disabling it entirely it is necessary to provide ``None`` as a value. - :param dict config_dict: Initial default configurations. - :param str config_object: Name of the class or module containing configurations. - :param str config_file: Name of the file containing additional configurations. - :param str config_env: Name of the environment variable pointing to file containing configurations. - :param callable config_func: Callable that will receive app.config as parameter. + :param dict config_dict: Initial default configurations + :param str config_object: Name of the class or module containing configurations + :param str config_file: Name of the file containing additional configurations + :param str config_env: Name of the environment variable pointing to file containing configurations + :param callable config_func: Callable that will receive app.config as parameter :return: Hawat application :rtype: hawat.base.HawatApp """ + app = hawat.base.HawatApp(APP_NAME) - return vial.create_app_full( - hawat.base.HawatApp, - APP_NAME, - config_dict = config_dict, - config_object = config_object, - config_file = config_file, - config_env = config_env, - config_func = config_func or _config_app - ) + if config_dict and isinstance(config_dict, dict): + app.config.update(config_dict) + if config_object: + app.config.from_object(config_object) + if config_file: + app.config.from_pyfile(config_file) + if config_env and os.getenv(config_env, None): + app.config.from_envvar(config_env) + config_func = config_func or _config_app + if config_func and callable(config_func): + config_func(app.config) + + app.setup_app() + + return app def create_app(): @@ -91,8 +98,8 @@ def create_app(): if not os.path.isfile(config_file): config_file = None return create_app_full( - config_object = hawat.config.CONFIG_MAP[config_name], - config_file = config_file + config_object=hawat.config.CONFIG_MAP[config_name], + config_file=config_file ) @@ -103,7 +110,7 @@ def _config_app(app_config): dbcfg = app_config[CFGKEY_MENTAT_CORE][CKEY_CORE_DATABASE][CKEY_CORE_DATABASE_SQLSTORAGE] app_config['SQLALCHEMY_DATABASE_URI'] = dbcfg['url'] - app_config['SQLALCHEMY_ECHO'] = dbcfg['echo'] + app_config['SQLALCHEMY_ECHO'] = dbcfg['echo'] app_config['BABEL_TRANSLATION_DIRECTORIES'] = 'translations;' app_config['BABEL_TRANSLATION_DIRECTORIES'] += pyzenkit.utils.get_resource_path_fr( diff --git a/lib/hawat/babel.cfg b/lib/hawat/babel.cfg index 674492ec636d9eee72935d3d65178333216da3ac..68d9823af563536052433a8a0c1e514a3b0ca213 100644 --- a/lib/hawat/babel.cfg +++ b/lib/hawat/babel.cfg @@ -1,6 +1,5 @@ [python: hawat/**.py] [python: mentat/**.py] -[python: vial/**.py] [jinja2: hawat/**/templates/**.html] [jinja2: hawat/**/templates/**.txt] extensions=jinja2.ext.autoescape,jinja2.ext.with_ diff --git a/lib/hawat/base.py b/lib/hawat/base.py index 2b76e9e013d5a435f632ebac331780b246c09c50..de74a82f0dfeb679fd6823e3a1e7930660cc14d6 100644 --- a/lib/hawat/base.py +++ b/lib/hawat/base.py @@ -1,19 +1,33 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- __author__ = "Jan Mach <jan.mach@cesnet.cz>" __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" - import re +import sys +import traceback import datetime +import weakref +import json +import jinja2 + +import werkzeug.routing +import werkzeug.utils +import flask +import flask.app +import flask.views +import flask_babel +import flask_migrate +import flask_login +import flask_principal # # Flask related modules. @@ -27,11 +41,21 @@ from flask_babel import gettext # # Custom modules. # -import vial.app -import vial.const -import vial.menu -import vial.errors +import hawat.app +import hawat.const +import hawat.errors import hawat.events +import hawat.db +import hawat.acl +import hawat.log +import hawat.mailer +import hawat.intl +import hawat.forms +import hawat.utils +import hawat.jsglue +import hawat.view +import hawat.menu +import hawat.command import mentat import mentat._buildmeta @@ -42,12 +66,17 @@ import mentat.idea.internal import mentat.idea.jsondict import mentat.datatype.sqldb - CRE_QNAME = re.compile(r'^([\d]+)_([a-z]{6})$') RE_UQUERY = ' AS "_mentatq\\({:d}_[^)]+\\)_"' -class HawatApp(vial.app.Vial): +class HawatException(Exception): + """ + Custom class for :py:class:`hawat.app.Hawat` application exceptions. + """ + + +class HawatApp(flask.Flask): """ Custom implementation of :py:class:`flask.Flask` class. This class extends the capabilities of the base class with following additional features: @@ -70,6 +99,19 @@ class HawatApp(vial.app.Vial): def __init__(self, import_name, **kwargs): super().__init__(import_name, **kwargs) + self.csrf = None + + self.mailer = None + + self.menu_main = hawat.menu.Menu() + self.menu_auth = hawat.menu.Menu() + self.menu_anon = hawat.menu.Menu() + + self.sign_ins = {} + self.sign_ups = {} + self.resources = {} + self.infomailers = {} + self.csag = {} self.oads = {} @@ -80,6 +122,236 @@ class HawatApp(vial.app.Vial): """ return self.config[hawat.const.CFGKEY_MENTAT_CORE] + @property + def icons(self): + """ + Application icon registry. + """ + return self.config.get('ICONS') + + @flask.app.setupmethod + def add_url_rule(self, rule, endpoint=None, view_func=None, provide_automatic_options=None, **options): + """ + Reimplementation of :py:func:`flask.Flask.add_url_rule` method. This method + is capable of disabling selected application endpoints. Keep in mind, that + some URL rules (like application global 'static' endpoint) are created during + the :py:func:`flask.app.Flask.__init__` method and cannot be disabled, + because at that point the configuration of the application is not yet loaded. + """ + if self.config.get('DISABLED_ENDPOINTS', None) and self.config['DISABLED_ENDPOINTS'] and endpoint: + if endpoint in self.config['DISABLED_ENDPOINTS']: + self.logger.warning( # pylint: disable=locally-disabled,no-member + "Application endpoint '%s' is disabled by configuration.", + endpoint + ) + return + # self.logger.debug( # pylint: disable=locally-disabled,no-member + # "Registering URL route %s:%s:%s:%s", + # str(rule), + # str(endpoint), + # str(view_func), + # str(view_func.view_class) if hasattr(view_func, 'view_class') else '---none---', + # ) + super().add_url_rule(rule, endpoint, view_func, provide_automatic_options, **options) + + def register_blueprint(self, blueprint, **options): + """ + Reimplementation of :py:func:`flask.Flask.register_blueprint` method. This + method will perform standart blueprint registration and on top of that will + perform following additional tasks: + + * Register blueprint into custom internal registry. The registry lies + within application`s ``config`` under key :py:const:`hawat.const.CFGKEY_ENABLED_BLUEPRINTS`. + * Call blueprint`s ``register_app`` method, if available, with ``self`` as only argument. + + :param hawat.app.HawatBlueprint blueprint: Blueprint to be registered. + :param dict options: Additional options, will be passed down to :py:func:`flask.Flask.register_blueprint`. + """ + super().register_blueprint(blueprint, **options) + + if isinstance(blueprint, HawatBlueprint): + if hasattr(blueprint, 'register_app'): + blueprint.register_app(self) + + self.sign_ins.update(blueprint.sign_ins) + self.sign_ups.update(blueprint.sign_ups) + + def register_blueprints(self): + """ + Register all configured application blueprints. The configuration comes + from :py:const:`hawat.const.CFGKEY_ENABLED_BLUEPRINTS` configuration + subkey, which must contain list of string names of required blueprints. + The blueprint module must provide ``get_blueprint`` factory method, that + must return valid instance of :py:class:`hawat.app.HawatBlueprint`. This + method will call the :py:func:`hawat.app.Hawat.register_blueprint` for + each blueprint, that is being registered into the application. + + :raises hawat.app.HawatException: In case the factory method ``get_blueprint`` is not provided by loaded module. + """ + for name in self.config[hawat.const.CFGKEY_ENABLED_BLUEPRINTS]: + self.logger.debug( # pylint: disable=locally-disabled,no-member + "Loading pluggable module %s", + name + ) + mod = werkzeug.utils.import_string(name) + if hasattr(mod, 'get_blueprint'): + self.register_blueprint(mod.get_blueprint()) + else: + raise HawatException( + "Invalid blueprint module '{}', does not provide the 'get_blueprint' factory method.".format(name) + ) + + def log_exception(self, exc_info): + """ + Reimplementation of :py:func:`flask.Flask.log_exception` method. + """ + self.logger.error( # pylint: disable=locally-disabled,no-member + "Exception on %s [%s]" % (flask.request.full_path, flask.request.method), + exc_info=exc_info + ) + + def log_exception_with_label(self, tbexc, label=''): + """ + Log given exception traceback into application logger. + """ + self.logger.error( # pylint: disable=locally-disabled,no-member + '%s%s', + label, + ''.join(tbexc.format()) + ) + + # -------------------------------------------------------------------------- + + def get_modules(self, filter_func=None): + """ + Get all currently registered application modules. + """ + if not filter_func: + return self.blueprints + return { + k: v for k, v in self.blueprints.items() if filter_func(k, v) + } + + def has_endpoint(self, endpoint): + """ + Check if given routing endpoint is available. + + :param str endpoint: Application routing endpoint. + :return: ``True`` in case endpoint exists, ``False`` otherwise. + :rtype: bool + """ + return endpoint in self.view_functions + + def get_endpoints(self, filter_func=None): + """ + Get all currently registered application endpoints. + """ + if not filter_func: + return { + k: v.view_class for k, v in self.view_functions.items() if hasattr(v, 'view_class') + } + return { + k: v.view_class for k, v in self.view_functions.items() if + hasattr(v, 'view_class') and filter_func(k, v.view_class) + } + + def get_endpoint_class(self, endpoint, quiet=False): + """ + Get reference to view class registered to given routing endpoint. + + :param str endpoint: Application routing endpoint. + :param bool quiet: Suppress the exception in case given endpoint does not exist. + :return: Reference to view class. + :rtype: class + """ + if not endpoint in self.view_functions: + if quiet: + return None + raise HawatException( + "Unknown endpoint name '{}'.".format(endpoint) + ) + try: + return self.view_functions[endpoint].view_class + except AttributeError: + return hawat.view.DecoratedView(self.view_functions[endpoint]) + + def can_access_endpoint(self, endpoint, **kwargs): + """ + Check, that the current user can access given endpoint/view. + + :param str endpoint: Application routing endpoint. + :param dict kwargs: Optional endpoint parameters. + :return: ``True`` in case user can access the endpoint, ``False`` otherwise. + :rtype: bool + """ + try: + view_class = self.get_endpoint_class(endpoint) + + # Reject unauthenticated users in case view requires authentication. + if view_class.authentication: + if not flask_login.current_user.is_authenticated: + return False + + # Check view authorization rules. + if view_class.authorization: + for auth_rule in view_class.authorization: + if not auth_rule.can(): + return False + + # Check item action authorization callback, if exists. + if hasattr(view_class, 'authorize_item_action'): + if not view_class.authorize_item_action(**kwargs): + return False + + return True + + except HawatException: + return False + + def get_model(self, name): + """ + Return reference to class of given model. + + :param str name: Name of the model. + """ + return self.config[hawat.const.CFGKEY_MODELS][name] + + def get_resource(self, name): + """ + Return reference to given registered resource. + + :param str name: Name of the resource. + """ + return self.resources[name]() + + def set_resource(self, name, resource): + """ + Store reference to given resource. + + :param str name: Name of the resource. + :param resource: Resource to be registered. + """ + self.resources[name] = weakref.ref(resource) + + def set_infomailer(self, name, mailer): + """ + Register mailer handle to be usable by different web interface components. + + :param str name: Name of the informailer. + :param callable mailer: Mailer handle. + """ + self.infomailers.setdefault(name, []).append(mailer) + + def send_infomail(self, name, **kwargs): + """ + Send emails through all registered infomailer handles. + + :param str name: Name of the informailer. + :param **kwargs: Additional mailer arguments. + """ + for mailer in self.infomailers[name]: + mailer(**kwargs) + def get_csag(self, group_name): """ Return list of all registered context search actions for given group name @@ -102,8 +374,8 @@ class HawatApp(vial.app.Vial): :param URLParamsBuilder params_builder: URL parameter builder for this action. """ self.csag.setdefault(group_name, []).append({ - 'title': title, - 'view': view_class, + 'title': title, + 'view': view_class, 'params': params_builder }) @@ -119,8 +391,8 @@ class HawatApp(vial.app.Vial): """ self.csag.setdefault(group_name, []).append({ 'title': title, - 'icon': icon, - 'url': url_builder + 'icon': icon, + 'url': url_builder }) def get_oads(self, group_name): @@ -144,22 +416,124 @@ class HawatApp(vial.app.Vial): :param URLParamsBuilder params_builder: URL parameter builder for this action. """ self.oads.setdefault(group_name, []).append({ - 'view': view_class, - 'params': params_builder + 'view': view_class, + 'params': params_builder }) + # -------------------------------------------------------------------------- - #-------------------------------------------------------------------------- + def setup_app(self): + """ + Perform setup of the whole application. + """ + self._setup_app_logging() + self._setup_app_mailer() + self._setup_app_core() + self._setup_app_db() + self._setup_app_auth() + self._setup_app_acl() + self._setup_app_intl() + self._setup_app_menu() + self._setup_app_blueprints() + self._setup_app_cli() + self._setup_app_eventdb() + def _setup_app_logging(self): + """ + Setup logging to file and via email for given Hawat application. Logging + capabilities are adjustable by application configuration. - def setup_app(self): - super().setup_app() + :return: Modified Hawat application + :rtype: hawat.app.HawatApp + """ + hawat.log.setup_logging_default(self) + hawat.log.setup_logging_file(self) + if not self.debug: + hawat.log.setup_logging_email(self) - self._setup_app_eventdb() + return self + + def _setup_app_mailer(self): + """ + Setup mailer service for Hawat application. + + :return: Modified Hawat application + :rtype: hawat.app.HawatApp + """ + hawat.mailer.MAILER.init_app(self) + self.mailer = hawat.mailer.MAILER + return self def _setup_app_core(self): - super()._setup_app_core() + """ + Setup application core for given Hawat application. The application core + contains following features: + + * Error handlers + * Default routes + * Additional custom Jinja template variables + * Additional custom Jinja template macros + + :return: Modified Hawat application + :rtype: hawat.app.HawatApp + """ + + @self.errorhandler(400) + def eh_badrequest(err): # pylint: disable=locally-disabled,unused-variable + """Flask error handler to be called to service HTTP 400 error.""" + flask.current_app.logger.critical( + "BAD REQUEST\n\nRequest: %s\nTraceback:\n%s", + flask.request.full_path, + ''.join( + traceback.TracebackException( + *sys.exc_info() + ).format() + ) + ) + return hawat.errors.error_handler_switch(400, err) + + @self.errorhandler(403) + def eh_forbidden(err): # pylint: disable=locally-disabled,unused-variable + """Flask error handler to be called to service HTTP 403 error.""" + return hawat.errors.error_handler_switch(403, err) + + @self.errorhandler(404) + def eh_page_not_found(err): # pylint: disable=locally-disabled,unused-variable + """Flask error handler to be called to service HTTP 404 error.""" + return hawat.errors.error_handler_switch(404, err) + + @self.errorhandler(405) + def eh_method_not_allowed(err): # pylint: disable=locally-disabled,unused-variable + """Flask error handler to be called to service HTTP 405 error.""" + return hawat.errors.error_handler_switch(405, err) + + @self.errorhandler(410) + def eh_gone(err): # pylint: disable=locally-disabled,unused-variable + """Flask error handler to be called to service HTTP 410 error.""" + return hawat.errors.error_handler_switch(410, err) + + @self.errorhandler(500) + def eh_internal_server_error(err): # pylint: disable=locally-disabled,unused-variable + """Flask error handler to be called to service HTTP 500 error.""" + flask.current_app.logger.critical( + "INTERNAL SERVER ERROR\n\nRequest: %s\nTraceback:\n%s", + flask.request.full_path, + ''.join( + traceback.TracebackException( + *sys.exc_info() + ).format() + ), + ) + return hawat.errors.error_handler_switch(500, err) + + @self.before_request + def before_request(): # pylint: disable=locally-disabled,unused-variable + """ + Use Flask`s :py:func:`flask.Flask.before_request` hook for performing + various usefull tasks before each request. + """ + flask.g.requeststart = datetime.datetime.utcnow() @self.context_processor def jinja_inject_variables(): # pylint: disable=locally-disabled,unused-variable @@ -167,10 +541,236 @@ class HawatApp(vial.app.Vial): Inject additional variables into Jinja2 global template namespace. """ return dict( - hawat_version = mentat.__version__, - hawat_bversion = mentat._buildmeta.__bversion__, # pylint: disable=locally-disabled,protected-access - hawat_bversion_full = mentat._buildmeta.__bversion_full__, # pylint: disable=locally-disabled,protected-access - hawat_chart_dimensions = 'height:700px', + hawat_appname=flask.current_app.config['APPLICATION_NAME'], + hawat_appid=flask.current_app.config['APPLICATION_ID'], + hawat_current_app=flask.current_app, + hawat_current_menu_main=flask.current_app.menu_main, + hawat_current_menu_auth=flask.current_app.menu_auth, + hawat_current_menu_anon=flask.current_app.menu_anon, + hawat_current_view=self.get_endpoint_class(flask.request.endpoint, True), + hawat_logger=flask.current_app.logger, + hawat_cdt_utc=datetime.datetime.utcnow(), + hawat_cdt_local=datetime.datetime.now(), + ) + + @self.context_processor + def jinja2_inject_functions(): # pylint: disable=locally-disabled,unused-variable,too-many-locals + """ + Register additional helpers into Jinja2 global template namespace. + """ + + def get_modules_dict(): + """ + Return dictionary of all registered application pluggable modules. + """ + return flask.current_app.blueprints + + def get_endpoints_dict(): + """ + Return dictionary of all registered application view endpoints. + """ + return {k: v.view_class for k, v in flask.current_app.view_functions.items() if + hasattr(v, 'view_class')} + + def get_endpoint_class(endpoint, quiet=False): + """ + Return class reference to given view endpoint. + + :param str endpoint: Name of the view endpoint. + :param bool quiet: Suppress the exception in case given endpoint does not exist. + """ + return self.get_endpoint_class(endpoint, quiet) + + def check_endpoint_exists(endpoint): + """ + Check, that given application view endpoint exists and is registered within + the application. + + :param str endpoint: Name of the view endpoint. + :return: ``True`` in case endpoint exists, ``False`` otherwise. + :rtype: bool + """ + return endpoint in self.view_functions + + def get_icon(icon_name, default_icon='missing-icon'): + """ + Get HTML icon markup for given icon. + + :param str icon_name: Name of the icon. + :param str default_icon: Name of the default icon. + :return: Icon including HTML markup. + :rtype: flask.Markup + """ + return flask.Markup( + self.config.get('ICONS').get( + icon_name, + self.config.get('ICONS').get(default_icon) + ) + ) + + def get_module_icon(endpoint, default_icon='missing-icon'): + """ + Get HTML icon markup for parent module of given view endpoint. + + :param str endpoint: Name of the view endpoint. + :param str default_icon: Name of the default icon. + :return: Icon including HTML markup. + :rtype: flask.Markup + """ + return flask.Markup( + self.config.get('ICONS').get( + self.get_endpoint_class(endpoint).module_ref().get_module_icon(), + self.config.get('ICONS').get(default_icon) + ) + ) + + def get_endpoint_icon(endpoint, default_icon='missing-icon'): + """ + Get HTML icon markup for given view endpoint. + + :param str endpoint: Name of the view endpoint. + :return: Icon including HTML markup. + :rtype: flask.Markup + """ + return flask.Markup( + self.config.get('ICONS').get( + self.get_endpoint_class(endpoint).get_view_icon(), + self.config.get('ICONS').get(default_icon) + ) + ) + + def get_country_flag(country): + """ + Get URL to static country flag file. + + :param str country: Name of the icon. + :return: Country including HTML markup. + :rtype: flask.Markup + """ + if not hawat.const.CRE_COUNTRY_CODE.match(country): + return get_icon('flag') + + return flask.Markup( + '<img src="{}">'.format( + flask.url_for( + 'static', + filename='images/country-flags/flags-iso/shiny/16/{}.png'.format( + country.upper() + ) + ) + ) + ) + + def include_raw(filename): + """ + Include given file in raw form directly into the generated content. + This may be usefull for example for including JavaScript files + directly into the HTML page. + """ + return jinja2.Markup( + self.jinja_loader.get_source(self.jinja_env, filename)[0] + ) + + return dict( + get_modules_dict=get_modules_dict, + get_endpoints_dict=get_endpoints_dict, + get_endpoint_class=get_endpoint_class, + check_endpoint_exists=check_endpoint_exists, + + get_icon=get_icon, + get_module_icon=get_module_icon, + get_endpoint_icon=get_endpoint_icon, + get_country_flag=get_country_flag, + + get_timedelta=hawat.utils.get_timedelta, + get_datetime_utc=hawat.utils.get_datetime_utc, + get_datetime_local=hawat.utils.get_datetime_local, + parse_datetime=hawat.utils.parse_datetime, + + get_datetime_window=hawat.view.mixin.HawatUtils.get_datetime_window, + + check_file_exists=hawat.utils.check_file_exists, + + in_query_params=hawat.utils.in_query_params, + generate_query_params=hawat.utils.generate_query_params, + + current_datetime_utc=datetime.datetime.utcnow(), + + include_raw=include_raw, + json_to_yaml=hawat.utils.json_to_yaml, + get_uuid4=hawat.utils.get_uuid4, + load_json_from_file=hawat.utils.load_json_from_file, + make_copy_deep=hawat.utils.make_copy_deep + ) + + @self.template_filter('tojson_pretty') + def to_pretty_json(value): + return json.dumps( + value, + sort_keys=True, + indent=4 + ) + + class HawatJSONEncoder(flask.json.JSONEncoder): + """ + Custom JSON encoder for converting anything into JSON strings. + """ + + def default(self, obj): # pylint: disable=locally-disabled,method-hidden,arguments-differ + try: + if isinstance(obj, datetime.datetime): + return obj.isoformat() + 'Z' + except: # pylint: disable=locally-disabled,bare-except + pass + try: + return obj.to_dict() + except: # pylint: disable=locally-disabled,bare-except + pass + try: + return str(obj) + except: # pylint: disable=locally-disabled,bare-except + pass + return flask.json.JSONEncoder.default(self, obj) + + self.json_encoder = HawatJSONEncoder + + @self.route('/app-main.js') + def mainjs(): # pylint: disable=locally-disabled,unused-variable + """ + Default route for main application JavaScript file. + """ + return flask.make_response( + flask.render_template('app-main.js'), + 200, + {'Content-Type': 'text/javascript'} + ) + + # Initialize JSGlue plugin for using `flask.url_for()` method in JavaScript. + # jsglue = flask_jsglue.JSGlue() + jsglue = hawat.jsglue.JSGlue() + jsglue.init_app(self) + + @self.template_filter() + def pprint_item(item): # pylint: disable=locally-disabled,unused-variable + """ + Custom Jinja2 filter for full object attribute dump/pretty-print. + """ + res = [] + for key in dir(item): + res.append('%r: %r' % (key, getattr(item, key))) + return '\n'.join(res) + + @self.context_processor + def jinja_inject_variables(): # pylint: disable=locally-disabled,unused-variable + """ + Inject additional variables into Jinja2 global template namespace. + """ + return dict( + hawat_version=mentat.__version__, + hawat_bversion=mentat._buildmeta.__bversion__, # pylint: disable=locally-disabled,protected-access + hawat_bversion_full=mentat._buildmeta.__bversion_full__, + # pylint: disable=locally-disabled,protected-access + hawat_chart_dimensions='height:700px', ) @self.context_processor @@ -196,24 +796,25 @@ class HawatApp(vial.app.Vial): """ return mentat.const.REPORTING_INTERVALS_INV[seconds] - def get_limit_counter(limit = None): + def get_limit_counter(limit=None): """ Get fresh instance of limit counter. """ if not limit: limit = flask.current_app.config['HAWAT_LIMIT_AODS'] - return vial.utils.LimitCounter(limit) + return hawat.utils.LimitCounter(limit) return dict( - get_csag = get_csag, - get_reporting_interval_name = get_reporting_interval_name, - get_limit_counter = get_limit_counter, + get_csag=get_csag, + get_reporting_interval_name=get_reporting_interval_name, + get_limit_counter=get_limit_counter, ) class HawatJSONEncoder(flask.json.JSONEncoder): """ Custom JSON encoder for converting anything into JSON strings. """ + def default(self, obj): # pylint: disable=locally-disabled,method-hidden,arguments-differ try: if isinstance(obj, mentat.idea.internal.Idea): @@ -240,24 +841,48 @@ class HawatApp(vial.app.Vial): return self def _setup_app_db(self): - super()._setup_app_db() + """ + Setup application database service for given Hawat application. + + :return: Modified Hawat application + :rtype: hawat.app.HawatApp + """ + dbh = hawat.db.db_setup(**self.config['SQLALCHEMY_SETUP_ARGS']) + dbh.init_app(self) + + # Initialize database migration service and register it among the application + # resources for possible future use. + migrate = flask_migrate.Migrate( + app=self, + db=dbh, + directory=self.config['MIGRATE_DIRECTORY'] + ) + self.set_resource(hawat.const.RESOURCE_MIGRATE, migrate) + + self.logger.debug( + "Connected to database via SQLAlchemy ({})".format( + self.config['SQLALCHEMY_DATABASE_URI'] + ) + ) class StorageService: # pylint: disable=locally-disabled,too-few-public-methods """ - This is a thin proxy class, that can be used in place of :py:class:`mentat.services.sqlstorage.StorageService`. - This is necessary for certain services like :py:mod:`mentat.services.whois`, that require - some access to database storage service and are hardcoded to use :py:class:`mentat.services.sqlstorage.StorageService`. + This is a thin proxy class, that can be used in place of + :py:class:`mentat.services.sqlstorage.StorageService`. + This is necessary for certain services like :py:mod:`mentat.services.whois`, + that require some access to database storage service and are hardcoded to + use :py:class:`mentat.services.sqlstorage.StorageService`. This is necessary when using the services from Flask framework, because there is another storage service management feature in place using the py:mod:`flask_sqlalchemy` module. """ + @property def session(self): """ Thin proxy property for retrieving reference to current database session. """ - return vial.db.db_session() - + return hawat.db.db_session() class StorageServiceManager: # pylint: disable=locally-disabled,too-few-public-methods """ @@ -268,6 +893,7 @@ class HawatApp(vial.app.Vial): is another storage service management feature in place using the py:mod:`flask_sqlalchemy` module. """ + @staticmethod def service(): """ @@ -276,16 +902,293 @@ class HawatApp(vial.app.Vial): """ return StorageService() - mentat.services.sqlstorage.set_manager(StorageServiceManager()) + return self + + def _setup_app_auth(self): + """ + Setup application authentication features. + + :return: Modified Hawat application + :rtype: hawat.app.HawatApp + """ + + lim = flask_login.LoginManager() + lim.init_app(self) + lim.login_view = self.config['ENDPOINT_LOGIN'] + lim.login_message = flask_babel.gettext("Please log in to access this page.") + lim.login_message_category = self.config['LOGIN_MSGCAT'] + + self.set_resource(hawat.const.RESOURCE_LOGIN_MANAGER, lim) + + @lim.user_loader + def load_user(user_id): # pylint: disable=locally-disabled,unused-variable + """ + Flask-Login callback for loading current user`s data. + """ + user_model = self.get_model(hawat.const.MODEL_USER) + return hawat.db.db_get().session.query(user_model).filter(user_model.id == user_id).one_or_none() + + @self.route('/logout') + @flask_login.login_required + def logout(): # pylint: disable=locally-disabled,unused-variable + """ + Flask-Login callback for logging out current user. + """ + flask.current_app.logger.info( + "User '{}' just logged out.".format( + str(flask_login.current_user) + ) + ) + flask_login.logout_user() + flask.flash( + flask_babel.gettext('You have been successfully logged out.'), + hawat.const.FLASH_SUCCESS + ) + + # Remove session keys set by Flask-Principal. + for key in ('identity.name', 'identity.auth_type'): + flask.session.pop(key, None) + + # Tell Flask-Principal the identity changed. + flask_principal.identity_changed.send( + flask.current_app._get_current_object(), # pylint: disable=locally-disabled,protected-access + identity=flask_principal.AnonymousIdentity() + ) + + # Force user to index page. + return flask.redirect( + flask.url_for( + flask.current_app.config['ENDPOINT_LOGOUT_REDIRECT'] + ) + ) + + return self + + def _setup_app_acl(self): + """ + Setup application ACL features. + + :return: Modified Hawat application + :rtype: hawat.app.HawatApp + """ + fpp = flask_principal.Principal(self, skip_static=True) + self.set_resource(hawat.const.RESOURCE_PRINCIPAL, fpp) + + @flask_principal.identity_loaded.connect_via(self) + def on_identity_loaded(sender, identity): # pylint: disable=locally-disabled,unused-variable,unused-argument + """ + Flask-Principal callback for populating user identity object after login. + """ + # Set the identity user object. + identity.user = flask_login.current_user + + if not flask_login.current_user.is_authenticated: + flask.current_app.logger.debug( + "Loaded ACL identity for anonymous user '{}'.".format( + str(flask_login.current_user) + ) + ) + return + flask.current_app.logger.debug( + "Loading ACL identity for user '{}'.".format( + str(flask_login.current_user) + ) + ) + + # Add the UserNeed to the identity. + if hasattr(flask_login.current_user, 'get_id'): + identity.provides.add( + flask_principal.UserNeed(flask_login.current_user.id) + ) + + # Assuming the User model has a list of roles, update the + # identity with the roles that the user provides. + if hasattr(flask_login.current_user, 'roles'): + for role in flask_login.current_user.roles: + identity.provides.add( + flask_principal.RoleNeed(role) + ) + + # Assuming the User model has a list of group memberships, update the + # identity with the groups that the user is member of. + if hasattr(flask_login.current_user, 'memberships'): + for group in flask_login.current_user.memberships: + identity.provides.add( + hawat.acl.MembershipNeed(group.id) + ) + + # Assuming the User model has a list of group managements, update the + # identity with the groups that the user is manager of. + if hasattr(flask_login.current_user, 'managements'): + for group in flask_login.current_user.managements: + identity.provides.add( + hawat.acl.ManagementNeed(group.id) + ) + + @self.context_processor + def utility_acl_processor(): # pylint: disable=locally-disabled,unused-variable + """ + Register additional helpers related to authorization into Jinja global + namespace to enable them within the templates. + """ + + def can_access_endpoint(endpoint, item=None): + """ + Check if currently logged-in user can access given endpoint/view. + + :param str endpoint: Name of the application endpoint. + :param item: Optional item for additional validations. + :return: ``True`` in case user can access the endpoint, ``False`` otherwise. + :rtype: bool + """ + return flask.current_app.can_access_endpoint(endpoint, item=item) + + def permission_can(permission_name): + """ + Manually check currently logged-in user for given permission. + + :param str permission_name: Name of the permission. + :return: Check result. + :rtype: bool + """ + return hawat.acl.PERMISSIONS[permission_name].can() + + def is_it_me(item): + """ + Check if given user account is mine. + """ + return item.id == flask_login.current_user.id + + return dict( + can_access_endpoint=can_access_endpoint, + permission_can=permission_can, + is_it_me=is_it_me + ) + + return self + + def _setup_app_intl(self): + """ + Setup application`s internationalization sybsystem. + + :return: Modified Hawat application + :rtype: hawat.app.HawatApp + """ + hawat.intl.BABEL.init_app(self) + self.set_resource(hawat.const.RESOURCE_BABEL, hawat.intl.BABEL) + + @self.route('/locale/<code>') + def locale(code): # pylint: disable=locally-disabled,unused-variable + """ + Application route providing users with the option of changing locale. + """ + if code not in flask.current_app.config['SUPPORTED_LOCALES']: + return flask.abort(404) + + if flask_login.current_user.is_authenticated: + flask_login.current_user.locale = code + # Make sure current user is in SQLAlchemy session. Turns out, this + # step is not necessary and current user is already in session, + # because it was fetched from database few moments ago. + # hawat.db.db_session().add(flask_login.current_user) + hawat.db.db_session().commit() + + flask.session['locale'] = code + flask_babel.refresh() + + flask.flash( + flask.Markup(flask_babel.gettext( + 'Locale was succesfully changed to <strong>%(lcln)s (%(lclc)s)</strong>.', + lclc=code, + lcln=flask.current_app.config['SUPPORTED_LOCALES'][code] + )), + hawat.const.FLASH_SUCCESS + ) + + # Redirect user back to original page. + return flask.redirect( + hawat.forms.get_redirect_target( + default_url=flask.url_for( + flask.current_app.config['ENDPOINT_HOME'] + ) + ) + ) + + @self.before_request + def before_request(): # pylint: disable=locally-disabled,unused-variable + """ + Use Flask`s :py:func:`flask.Flask.before_request` hook for storing + currently selected locale and timezone to request`s session storage. + """ + if 'locale' not in flask.session: + flask.session['locale'] = hawat.intl.get_locale() + if 'timezone' not in flask.session: + flask.session['timezone'] = hawat.intl.get_timezone() + + @self.context_processor + def utility_processor(): # pylint: disable=locally-disabled,unused-variable + """ + Register additional internationalization helpers into Jinja global namespace. + """ + + return dict( + babel_get_locale=hawat.intl.get_locale, + babel_get_timezone=hawat.intl.get_timezone, + babel_format_datetime=flask_babel.format_datetime, + babel_format_date=flask_babel.format_date, + babel_format_time=flask_babel.format_time, + babel_format_timedelta=flask_babel.format_timedelta, + babel_format_decimal=flask_babel.format_decimal, + babel_format_percent=flask_babel.format_percent, + babel_format_bytes=hawat.intl.babel_format_bytes, + babel_translate_locale=hawat.intl.babel_translate_locale, + babel_language_in_locale=hawat.intl.babel_language_in_locale + ) + + return self + + def _setup_app_menu(self): + """ + Setup default application menu skeleton. + + :return: Modified Hawat application + :rtype: hawat.app.HawatApp + """ + for entry in self.config[hawat.const.CFGKEY_MENU_MAIN_SKELETON]: + self.menu_main.add_entry(**entry) + + return self + + def _setup_app_blueprints(self): + """ + Setup application blueprints. + + :return: Modified Hawat application + :rtype: hawat.app.HawatApp + """ + self.register_blueprints() + + return self + + def _setup_app_cli(self): + """ + Setup application command line interface. + + :return: Modified Hawat application + :rtype: hawat.app.HawatApp + """ + hawat.command.setup_cli(self) + + return self + def _setup_app_eventdb(self): """ - Setup application database service for given Vial application. + Setup application database service for given Hawat application. - :param vial.app.VialApp app: Vial application to be modified. - :return: Modified Vial application - :rtype: vial.app.VialApp + :return: Modified Hawat application + :rtype: hawat.app.HawatApp """ hawat.events.db_init(self) self.logger.info("Connected to event database") @@ -293,6 +1196,84 @@ class HawatApp(vial.app.Vial): return self +class HawatBlueprint(flask.Blueprint): + """ + Custom implementation of :py:class:`flask.Blueprint` class. This class extends + the capabilities of the base class with additional features: + + * Support for better integration into application and registration of view classes. + * Support for custom tweaking of application object. + * Support for custom style of authentication and authorization decorators + """ + + def __init__(self, name, import_name, **kwargs): + super().__init__(name, import_name, **kwargs) + + self.sign_ins = {} + self.sign_ups = {} + + @classmethod + def get_module_title(cls): + """ + Get human readable name for this blueprint/module. + + :return: Name (short summary) of the blueprint/module. + :rtype: str + """ + raise NotImplementedError() + + def get_module_icon(self): + """ + Return icon name for the module. Given name will be used as index to + built-in icon registry. + + :return: Icon for the module. + :rtype: str + """ + return 'module-{}'.format(self.name).replace('_', '-') + + def register_app(self, app): # pylint: disable=locally-disabled,no-self-use,unused-argument + """ + *Hook method:* Custom callback, which will be called from + :py:func:`hawat.app.Hawat.register_blueprint` method and which can + perform additional tweaking of Hawat application object. + + :param hawat.app.Hawat app: Application object. + """ + return + + def register_view_class(self, view_class, route_spec): + """ + Register given view class into the internal blueprint registry. + + :param hawat.view.BaseView view_class: View class (not instance!) + :param str route_spec: Routing information for the view. + """ + view_class.module_ref = weakref.ref(self) + view_class.module_name = self.name + + # Obtain view function. + view_func = view_class.as_view(view_class.get_view_name()) + + # Apply authorization decorators (if requested). + if view_class.authorization: + for auth in view_class.authorization: + view_func = auth.require(403)(view_func) + + # Apply authentication decorators (if requested). + if view_class.authentication: + view_func = flask_login.login_required(view_func) + + # Register endpoint to the application. + self.add_url_rule(route_spec, view_func=view_func) + + # Register SIGN IN and SIGN UP views to enable further special handling. + if hasattr(view_class, 'is_sign_in') and view_class.is_sign_in: + self.sign_ins[view_class.get_view_endpoint()] = view_class + if hasattr(view_class, 'is_sign_up') and view_class.is_sign_up: + self.sign_ups[view_class.get_view_endpoint()] = view_class + + class PsycopgMixin: """ Mixin class providing generic interface for interacting with SQL database @@ -355,8 +1336,8 @@ class PsycopgMixin: 400, gettext( "You have reached your event search query quota: %(limit)s queries. Please wait for your queries to finish and try again. You may also review all your <a href=\"%(url)s\">currently running queries</a>.", - limit = limit, - url = flask.url_for('dbstatus.queries_my') + limit=limit, + url=flask.url_for('dbstatus.queries_my') ) ) @@ -374,11 +1355,11 @@ class PsycopgMixin: query_name = self.get_qname() items_count_total, items = self.get_db().search_events( form_args, - qtype = self.get_qtype(), - qname = query_name + qtype=self.get_qtype(), + qname=query_name ) self.response_context.update( - sqlquery = self.get_db().cursor.lastquery.decode('utf-8'), - sqlquery_name = query_name + sqlquery=self.get_db().cursor.lastquery.decode('utf-8'), + sqlquery_name=query_name ) return items diff --git a/lib/vial/blueprints/auth/__init__.py b/lib/hawat/blueprints/auth/__init__.py similarity index 61% rename from lib/vial/blueprints/auth/__init__.py rename to lib/hawat/blueprints/auth/__init__.py index 67085a46cd178c27cfb5aae1906f6959b5c7bd29..88d5275e478128441b96fee62c04db8268dca705 100644 --- a/lib/vial/blueprints/auth/__init__.py +++ b/lib/hawat/blueprints/auth/__init__.py @@ -1,8 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- +# This file is part of Mentat system (https://mentat.cesnet.cz/). +# +# Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ @@ -10,16 +13,17 @@ This pluggable module provides access to all currently enabled authentication and user account registration methods. """ +__author__ = "Jan Mach <jan.mach@cesnet.cz>" +__credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" import flask import flask_login from flask_babel import lazy_gettext -import vial.const -from vial.app import VialBlueprint -from vial.view import SimpleView -from vial.view.mixin import HTMLMixin - +import hawat.const +from hawat.base import HawatBlueprint +from hawat.view import SimpleView +from hawat.view.mixin import HTMLMixin BLUEPRINT_NAME = 'auth' """Name of the blueprint as module global constant.""" @@ -33,11 +37,11 @@ class LoginView(HTMLMixin, SimpleView): @classmethod def get_view_name(cls): - return vial.const.ACTION_USER_LOGIN + return hawat.const.ACTION_USER_LOGIN @classmethod def get_view_icon(cls): - return vial.const.ACTION_USER_LOGIN + return hawat.const.ACTION_USER_LOGIN @classmethod def get_view_title(cls, **kwargs): @@ -51,14 +55,14 @@ class LoginView(HTMLMixin, SimpleView): # In case user is already authenticated redirect to endpoint after login. if flask_login.current_user.is_authenticated: return self.redirect( - default_url = flask.url_for( + default_url=flask.url_for( flask.current_app.config['ENDPOINT_LOGIN'] ) ) - # Otherwise lookup all currently enabled 'SIGN IN' endpoints. + # Otherwise, lookup all currently enabled 'SIGN IN' endpoints. self.response_context.update( - signin_endpoints = flask.current_app.get_endpoints( + signin_endpoints=flask.current_app.get_endpoints( lambda name, mod: getattr(mod, 'is_sign_in', False) ) ) @@ -74,11 +78,11 @@ class RegisterView(HTMLMixin, SimpleView): @classmethod def get_view_name(cls): - return vial.const.ACTION_USER_REGISTER + return hawat.const.ACTION_USER_REGISTER @classmethod def get_view_icon(cls): - return vial.const.ACTION_USER_REGISTER + return hawat.const.ACTION_USER_REGISTER @classmethod def get_view_title(cls, **kwargs): @@ -91,16 +95,16 @@ class RegisterView(HTMLMixin, SimpleView): def do_before_response(self, **kwargs): # Lookup all currently enabled 'SIGN UP' endpoints. self.response_context.update( - signup_endpoints = flask.current_app.get_endpoints( + signup_endpoints=flask.current_app.get_endpoints( lambda name, mod: getattr(mod, 'is_sign_up', False) ) ) -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- -class AuthBlueprint(VialBlueprint): +class AuthBlueprint(HawatBlueprint): """Pluggable module - authentication and registration directional service (*auth*).""" @classmethod @@ -111,37 +115,37 @@ class AuthBlueprint(VialBlueprint): app.menu_anon.add_entry( 'view', 'login', - position = 0, - view = LoginView, - hidelegend = True + position=0, + view=LoginView, + hidelegend=True ) app.menu_anon.add_entry( 'view', 'register', - position = 100, - view = RegisterView, - hidelegend = True + position=100, + view=RegisterView, + hidelegend=True ) -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- def get_blueprint(): """ - Mandatory interface for :py:mod:`vial.Vial` and factory function. This function - must return a valid instance of :py:class:`vial.app.VialBlueprint` or + Mandatory interface for :py:mod:`hawat.Hawat` and factory function. This function + must return a valid instance of :py:class:`hawat.app.HawatBlueprint` or :py:class:`flask.Blueprint`. """ hbp = AuthBlueprint( BLUEPRINT_NAME, __name__, - template_folder = 'templates', - url_prefix = '/{}'.format(BLUEPRINT_NAME) + template_folder='templates', + url_prefix='/{}'.format(BLUEPRINT_NAME) ) - hbp.register_view_class(LoginView, '/login') + hbp.register_view_class(LoginView, '/login') hbp.register_view_class(RegisterView, '/register') return hbp diff --git a/lib/vial/blueprints/auth/templates/auth/login.html b/lib/hawat/blueprints/auth/templates/auth/login.html similarity index 90% rename from lib/vial/blueprints/auth/templates/auth/login.html rename to lib/hawat/blueprints/auth/templates/auth/login.html index baf77796a005c03994167fe8d5d7d67fd760abba..0eb97e0ad236ad8d687444a1591f77054e1bd826 100644 --- a/lib/vial/blueprints/auth/templates/auth/login.html +++ b/lib/hawat/blueprints/auth/templates/auth/login.html @@ -1,10 +1,10 @@ {%- extends "_layout.html" %} -{% block title %}{{ vial_current_view.get_view_title() }}{% endblock %} +{% block title %}{{ hawat_current_view.get_view_title() }}{% endblock %} {%- block content %} - <h2>{{ vial_current_view.get_view_title() }}</h2> + <h2>{{ hawat_current_view.get_view_title() }}</h2> <hr> <div class="row"> <div class="well col-md-6 col-md-offset-3"> diff --git a/lib/vial/blueprints/auth/templates/auth/register.html b/lib/hawat/blueprints/auth/templates/auth/register.html similarity index 89% rename from lib/vial/blueprints/auth/templates/auth/register.html rename to lib/hawat/blueprints/auth/templates/auth/register.html index f8fa134dd23150a06f6af610eba665dd6fd42d4d..7390573728cdfc53ce65938a3a696e25548799d2 100644 --- a/lib/vial/blueprints/auth/templates/auth/register.html +++ b/lib/hawat/blueprints/auth/templates/auth/register.html @@ -1,10 +1,10 @@ {%- extends "_layout.html" %} -{% block title %}{{ vial_current_view.get_view_title() }}{% endblock %} +{% block title %}{{ hawat_current_view.get_view_title() }}{% endblock %} {%- block content %} - <h2>{{ vial_current_view.get_view_title() }}</h2> + <h2>{{ hawat_current_view.get_view_title() }}</h2> <hr> <div class="row"> <div class="well col-md-6 col-md-offset-3"> diff --git a/lib/vial/blueprints/auth_dev/templates/registration/email_admins.txt b/lib/hawat/blueprints/auth/templates/registration/email_admins.txt similarity index 89% rename from lib/vial/blueprints/auth_dev/templates/registration/email_admins.txt rename to lib/hawat/blueprints/auth/templates/registration/email_admins.txt index 5a58a233916601c9ee387da660ed227aaba30f82..54eb78a2ebc4b1b29d516420111aa3a6cfe2d840 100644 --- a/lib/vial/blueprints/auth_dev/templates/registration/email_admins.txt +++ b/lib/hawat/blueprints/auth/templates/registration/email_admins.txt @@ -1,6 +1,6 @@ {{ _('Dear administrator,') | wordwrap }} -{{ _('a new account "%(item_id)s" was just registered in %(app_name)s. Please review the following request and activate or delete the account:', item_id = account.login, app_name = vial_appname) | wordwrap }} +{{ _('a new account "%(item_id)s" was just registered in %(app_name)s. Please review the following request and activate or delete the account:', item_id = account.login, app_name = hawat_appname) | wordwrap }} {{ '{:16s}'.format(_('Login:')) }} {{ account.login }} {{ '{:16s}'.format(_('Full name:')) }} {{ account.fullname }} @@ -23,4 +23,4 @@ {{ _('Have a nice day') | wordwrap }} --- {{ vial_appname }} +-- {{ hawat_appname }} diff --git a/lib/vial/blueprints/auth_env/templates/auth_env/registration/email_managers.txt b/lib/hawat/blueprints/auth/templates/registration/email_managers.txt similarity index 86% rename from lib/vial/blueprints/auth_env/templates/auth_env/registration/email_managers.txt rename to lib/hawat/blueprints/auth/templates/registration/email_managers.txt index 6cc8bf534bb6a22f32d2ff3fb5c64d808c064f14..802547a45f4a96a435aa7ffb1d2988268ccfc455 100644 --- a/lib/vial/blueprints/auth_env/templates/auth_env/registration/email_managers.txt +++ b/lib/hawat/blueprints/auth/templates/registration/email_managers.txt @@ -1,6 +1,6 @@ {{ _('Dear group manager,') | wordwrap }} -{{ _('a new account "%(item_id)s" was just registered in %(app_name)s and user requested membership in your group "%(group_id)s". Please review the following information and approve or reject the request:', item_id = account.login, app_name = vial_appname, group_id = group.name) | wordwrap }} +{{ _('a new account "%(item_id)s" was just registered in %(app_name)s and user requested membership in your group "%(group_id)s". Please review the following information and approve or reject the request:', item_id = account.login, app_name = hawat_appname, group_id = group.name) | wordwrap }} {{ '{:16s}'.format(_('Login:')) }} {{ account.login }} {{ '{:16s}'.format(_('Full name:')) }} {{ account.fullname }} @@ -16,4 +16,4 @@ {{ _('Have a nice day') | wordwrap }} --- {{ vial_appname }} +-- {{ hawat_appname }} diff --git a/lib/vial/blueprints/auth_env/templates/auth_env/registration/email_user.txt b/lib/hawat/blueprints/auth/templates/registration/email_user.txt similarity index 93% rename from lib/vial/blueprints/auth_env/templates/auth_env/registration/email_user.txt rename to lib/hawat/blueprints/auth/templates/registration/email_user.txt index a3f2d82c948afb81c0bcc7c5e85c8fab05589f90..c20b45e5656e91b4b4b6f80becbb7df759097e64 100644 --- a/lib/vial/blueprints/auth_env/templates/auth_env/registration/email_user.txt +++ b/lib/hawat/blueprints/auth/templates/registration/email_user.txt @@ -1,6 +1,6 @@ {{ _('Dear user,') | wordwrap }} -{{ _('this email is a confirmation, that you have successfully registered your new user account "%(item_id)s" in %(app_name)s.', item_id = account.login, app_name = vial_appname) | wordwrap }} +{{ _('this email is a confirmation, that you have successfully registered your new user account "%(item_id)s" in %(app_name)s.', item_id = account.login, app_name = hawat_appname) | wordwrap }} {{ _('During the registration process you have provided following information:') | wordwrap }} @@ -27,4 +27,4 @@ {{ _('Have a nice day') | wordwrap }} --- {{ vial_appname }} +-- {{ hawat_appname }} diff --git a/lib/hawat/blueprints/auth/test/__init__.py b/lib/hawat/blueprints/auth/test/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..d1ca73f643293660969671e72f10bf562134ee4a --- /dev/null +++ b/lib/hawat/blueprints/auth/test/__init__.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------- +# This file is part of Mentat system (https://mentat.cesnet.cz/). +# +# Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) +# Use of this source is governed by the MIT license, see LICENSE file. +# ------------------------------------------------------------------------------- + + +""" +Unit tests for :py:mod:`hawat.blueprints.auth`. +""" + +__author__ = "Jan Mach <jan.mach@cesnet.cz>" +__credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" + +import sys +import unittest + +from hawat.test import HawatTestCase +from hawat.test.runner import TestRunnerMixin + +_IS_NOSE = sys.argv[0].endswith('nosetests') + + +@unittest.skipIf(_IS_NOSE, "broken under nosetest") +class AuthTestCase(TestRunnerMixin, HawatTestCase): + """ + Class for testing :py:mod:`hawat.blueprints.auth` blueprint. + """ + + def test_01_login(self): + """ + Test login directional page. + """ + response = self.client.get( + '/auth/login', + follow_redirects=True + ) + self.assertEqual(response.status_code, 200) + self.assertTrue( + b'Following is a list of all available user login options. Please choose the one appropriate for you.' in response.data) + + def test_02_register(self): + """ + Test registration directional page. + """ + response = self.client.get( + '/auth/register', + follow_redirects=True + ) + self.assertEqual(response.status_code, 200) + self.assertTrue( + b'Following is a list of all available user account registration options. Please choose the one most suitable for your needs.' in response.data) + + +# ------------------------------------------------------------------------------- + + +if __name__ == "__main__": + unittest.main() diff --git a/lib/vial/blueprints/auth_api/__init__.py b/lib/hawat/blueprints/auth_api/__init__.py similarity index 76% rename from lib/vial/blueprints/auth_api/__init__.py rename to lib/hawat/blueprints/auth_api/__init__.py index dcac174b478069ca55bd8f19d162ea1fd26cc7c0..62dc2b6f6fc2d1af60e9936d40cee3738d364bf0 100644 --- a/lib/vial/blueprints/auth_api/__init__.py +++ b/lib/hawat/blueprints/auth_api/__init__.py @@ -1,8 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- +# This file is part of Mentat system (https://mentat.cesnet.cz/). +# +# Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ @@ -10,7 +13,7 @@ This pluggable module provides API key based authentication service. When this module is enabled, users may generate and use API keys to authenticate themselves when accessing various API application endpoints. -Currently the API key may be provided via one of the following methods: +Currently, the API key may be provided via one of the following methods: * The ``Authorization`` HTTP header. @@ -47,30 +50,34 @@ Provided endpoints * *Methods:* ``GET``, ``POST`` """ +__author__ = "Jan Mach <jan.mach@cesnet.cz>" +__credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" import itsdangerous +import markupsafe import flask import flask_principal from flask_babel import gettext, lazy_gettext -import vial.const -import vial.forms -import vial.db -from vial.app import VialBlueprint -from vial.view import ItemChangeView -from vial.view.mixin import HTMLMixin, SQLAlchemyMixin - +import hawat.const +import hawat.forms +import hawat.db +import hawat.acl +from hawat.base import HawatBlueprint +from hawat.view import ItemChangeView +from hawat.view.mixin import HTMLMixin, SQLAlchemyMixin BLUEPRINT_NAME = 'auth_api' """Name of the blueprint as module global constant.""" -class GenerateKeyView(HTMLMixin, SQLAlchemyMixin, ItemChangeView): # pylint: disable=locally-disabled,too-many-ancestors +class GenerateKeyView(HTMLMixin, SQLAlchemyMixin, + ItemChangeView): # pylint: disable=locally-disabled,too-many-ancestors """ View for generating API keys for user accounts. """ - methods = ['GET','POST'] + methods = ['GET', 'POST'] authentication = True @@ -92,11 +99,11 @@ class GenerateKeyView(HTMLMixin, SQLAlchemyMixin, ItemChangeView): # pylint: di @property def dbmodel(self): - return self.get_model(vial.const.MODEL_USER) + return self.get_model(hawat.const.MODEL_USER) @property def dbchlogmodel(self): - return self.get_model(vial.const.MODEL_ITEM_CHANGELOG) + return self.get_model(hawat.const.MODEL_ITEM_CHANGELOG) @classmethod def authorize_item_action(cls, **kwargs): @@ -107,34 +114,34 @@ class GenerateKeyView(HTMLMixin, SQLAlchemyMixin, ItemChangeView): # pylint: di permission_me = flask_principal.Permission( flask_principal.UserNeed(kwargs['item'].id) ) - return vial.acl.PERMISSION_POWER.can() or permission_me.can() + return hawat.acl.PERMISSION_POWER.can() or permission_me.can() @staticmethod def get_message_success(**kwargs): return gettext( 'API key for user account <strong>%(item_id)s</strong> was successfully generated.', - item_id = flask.escape(str(kwargs['item'])) + item_id=markupsafe.escape(str(kwargs['item'])) ) @staticmethod def get_message_failure(**kwargs): return gettext( 'Unable to generate API key for user account <strong>%(item_id)s</strong>.', - item_id = flask.escape(str(kwargs['item'])) + item_id=markupsafe.escape(str(kwargs['item'])) ) @staticmethod def get_message_cancel(**kwargs): return gettext( 'Canceled generating API key for user account <strong>%(item_id)s</strong>.', - item_id = flask.escape(str(kwargs['item'])) + item_id=markupsafe.escape(str(kwargs['item'])) ) @classmethod def change_item(cls, **kwargs): serializer = itsdangerous.URLSafeTimedSerializer( flask.current_app.config['SECRET_KEY'], - salt = 'apikey-user' + salt='apikey-user' ) kwargs['item'].apikey = serializer.dumps(kwargs['item'].id) @@ -143,7 +150,7 @@ class DeleteKeyView(HTMLMixin, SQLAlchemyMixin, ItemChangeView): # pylint: disa """ View for deleting API keys from user accounts. """ - methods = ['GET','POST'] + methods = ['GET', 'POST'] authentication = True @@ -165,11 +172,11 @@ class DeleteKeyView(HTMLMixin, SQLAlchemyMixin, ItemChangeView): # pylint: disa @property def dbmodel(self): - return self.get_model(vial.const.MODEL_USER) + return self.get_model(hawat.const.MODEL_USER) @property def dbchlogmodel(self): - return self.get_model(vial.const.MODEL_ITEM_CHANGELOG) + return self.get_model(hawat.const.MODEL_ITEM_CHANGELOG) @classmethod def authorize_item_action(cls, **kwargs): @@ -180,27 +187,27 @@ class DeleteKeyView(HTMLMixin, SQLAlchemyMixin, ItemChangeView): # pylint: disa permission_me = flask_principal.Permission( flask_principal.UserNeed(kwargs['item'].id) ) - return vial.acl.PERMISSION_POWER.can() or permission_me.can() + return hawat.acl.PERMISSION_POWER.can() or permission_me.can() @staticmethod def get_message_success(**kwargs): return gettext( 'API key for user account <strong>%(item_id)s</strong> was successfully deleted.', - item_id = flask.escape(str(kwargs['item'])) + item_id=markupsafe.escape(str(kwargs['item'])) ) @staticmethod def get_message_failure(**kwargs): return gettext( 'Unable to delete API key for user account <strong>%(item_id)s</strong>.', - item_id = flask.escape(str(kwargs['item'])) + item_id=markupsafe.escape(str(kwargs['item'])) ) @staticmethod def get_message_cancel(**kwargs): return gettext( 'Canceled deleting API key for user account <strong>%(item_id)s</strong>.', - item_id = flask.escape(str(kwargs['item'])) + item_id=markupsafe.escape(str(kwargs['item'])) ) @classmethod @@ -208,10 +215,10 @@ class DeleteKeyView(HTMLMixin, SQLAlchemyMixin, ItemChangeView): # pylint: disa kwargs['item'].apikey = None -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- -class APIAuthBlueprint(VialBlueprint): +class APIAuthBlueprint(HawatBlueprint): """Pluggable module - API key authentication service (*auth_api*).""" @classmethod @@ -219,8 +226,8 @@ class APIAuthBlueprint(VialBlueprint): return lazy_gettext('API key authentication service') def register_app(self, app): - login_manager = app.get_resource(vial.const.RESOURCE_LOGIN_MANAGER) - user_model = app.get_model(vial.const.MODEL_USER) + login_manager = app.get_resource(hawat.const.RESOURCE_LOGIN_MANAGER) + user_model = app.get_model(hawat.const.MODEL_USER) @login_manager.request_loader def load_user_from_request(request): # pylint: disable=locally-disabled,unused-variable @@ -255,7 +262,7 @@ class APIAuthBlueprint(VialBlueprint): # Now login the user with provided API key. if api_key: - dbsess = vial.db.db_session() + dbsess = hawat.db.db_session() try: user = dbsess.query(user_model).filter(user_model.apikey == api_key).one() if user: @@ -279,24 +286,24 @@ class APIAuthBlueprint(VialBlueprint): return None -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- def get_blueprint(): """ - Mandatory interface for :py:mod:`vial.Vial` and factory function. This function - must return a valid instance of :py:class:`vial.app.VialBlueprint` or + Mandatory interface for :py:mod:`hawat.Hawat` and factory function. This function + must return a valid instance of :py:class:`hawat.app.HawatBlueprint` or :py:class:`flask.Blueprint`. """ hbp = APIAuthBlueprint( BLUEPRINT_NAME, __name__, - template_folder = 'templates', - url_prefix = '/{}'.format(BLUEPRINT_NAME) + template_folder='templates', + url_prefix='/{}'.format(BLUEPRINT_NAME) ) hbp.register_view_class(GenerateKeyView, '/<int:item_id>/key-generate') - hbp.register_view_class(DeleteKeyView, '/<int:item_id>/key-delete') + hbp.register_view_class(DeleteKeyView, '/<int:item_id>/key-delete') return hbp diff --git a/lib/vial/blueprints/auth_api/templates/auth_api/key-delete.html b/lib/hawat/blueprints/auth_api/templates/auth_api/key-delete.html similarity index 100% rename from lib/vial/blueprints/auth_api/templates/auth_api/key-delete.html rename to lib/hawat/blueprints/auth_api/templates/auth_api/key-delete.html diff --git a/lib/vial/blueprints/auth_api/templates/auth_api/key-generate.html b/lib/hawat/blueprints/auth_api/templates/auth_api/key-generate.html similarity index 100% rename from lib/vial/blueprints/auth_api/templates/auth_api/key-generate.html rename to lib/hawat/blueprints/auth_api/templates/auth_api/key-generate.html diff --git a/lib/vial/blueprints/auth_api/test/__init__.py b/lib/hawat/blueprints/auth_api/test/__init__.py similarity index 56% rename from lib/vial/blueprints/auth_api/test/__init__.py rename to lib/hawat/blueprints/auth_api/test/__init__.py index ca0ad8d7c1aeaca87374df00f0442b5842f9fe8e..2dd9bebfb4a00eb04c902c0e44bb9ec62405bdbf 100644 --- a/lib/vial/blueprints/auth_api/test/__init__.py +++ b/lib/hawat/blueprints/auth_api/test/__init__.py @@ -1,93 +1,100 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- +# This file is part of Mentat system (https://mentat.cesnet.cz/). +# +# Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ -Unit tests for :py:mod:`vial.blueprints.auth_api`. +Unit tests for :py:mod:`hawat.blueprints.auth_api`. """ +__author__ = "Jan Mach <jan.mach@cesnet.cz>" +__credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" import sys import unittest -import vial.const -import vial.test -import vial.db -from vial.test import VialTestCase -from vial.test.runner import TestRunnerMixin - +import hawat.const +import hawat.test +import hawat.db +from hawat.test import HawatTestCase +from hawat.test.runner import TestRunnerMixin _IS_NOSE = sys.argv[0].endswith('nosetests') + @unittest.skipIf(_IS_NOSE, "broken under nosetest") -class AuthAPITestCase(TestRunnerMixin, VialTestCase): +class AuthAPITestCase(TestRunnerMixin, HawatTestCase): """ - Class for testing :py:mod:`vial.blueprints.auth_api` blueprint. + Class for testing :py:mod:`hawat.blueprints.auth_api` blueprint. """ - def _req_key_generate(self, uid, status_code, confirm = False): + def _req_key_generate(self, uid, status_code, confirm=False): response = None if confirm: response = self.client.post( '/auth_api/{:d}/key-generate'.format(uid), - follow_redirects = True, - data = {'submit': 'Confirm'} + follow_redirects=True, + data={'submit': 'Confirm'} ) else: response = self.client.get( '/auth_api/{:d}/key-generate'.format(uid), - follow_redirects = True + follow_redirects=True ) self.assertEqual(response.status_code, status_code) return response - def _req_key_delete(self, uid, status_code, confirm = False): + def _req_key_delete(self, uid, status_code, confirm=False): response = None if confirm: response = self.client.post( '/auth_api/{:d}/key-delete'.format(uid), - follow_redirects = True, - data = {'submit': 'Confirm'} + follow_redirects=True, + data={'submit': 'Confirm'} ) else: response = self.client.get( '/auth_api/{:d}/key-delete'.format(uid), - follow_redirects = True + follow_redirects=True ) self.assertEqual(response.status_code, status_code) return response def _test_keymng_success(self, uname): - uobj = self.user_get(uname, with_app_ctx = True) + uobj = self.user_get(uname, with_app_ctx=True) uid = uobj.id self.assertEqual(uobj.apikey, 'apikey-{}'.format(uname)) response = self._req_key_generate(uid, 200) - uobj = self.user_get(uname, with_app_ctx = True) - self.assertTrue(b'Are you really sure you want to generate new API access key for following user account' in response.data) + uobj = self.user_get(uname, with_app_ctx=True) + self.assertTrue( + b'Are you really sure you want to generate new API access key for following user account' in response.data) self.assertEqual(uobj.apikey, 'apikey-{}'.format(uname)) response = self._req_key_generate(uid, 200, True) - uobj = self.user_get(uname, with_app_ctx = True) + uobj = self.user_get(uname, with_app_ctx=True) self.assertTrue(uobj.apikey != 'apikey-{}'.format(uname)) self.assertTrue(uobj.apikey) response = self._req_key_delete(uid, 200) - uobj = self.user_get(uname, with_app_ctx = True) - self.assertTrue(b'Are you really sure you want to delete API access key from following user account' in response.data) + uobj = self.user_get(uname, with_app_ctx=True) + self.assertTrue( + b'Are you really sure you want to delete API access key from following user account' in response.data) self.assertTrue(uobj.apikey != 'apikey-{}'.format(uname)) self.assertTrue(uobj.apikey) response = self._req_key_delete(uid, 200, True) - uobj = self.user_get(uname, with_app_ctx = True) + uobj = self.user_get(uname, with_app_ctx=True) self.assertEqual(uobj.apikey, None) self.assertFalse(uobj.apikey) def _test_keymng_failure(self, uname): - uobj = self.user_get(uname, with_app_ctx = True) + uobj = self.user_get(uname, with_app_ctx=True) uid = uobj.id self.assertEqual(uobj.apikey, 'apikey-{}'.format(uname)) @@ -96,7 +103,7 @@ class AuthAPITestCase(TestRunnerMixin, VialTestCase): self._req_key_delete(uid, 403) self._req_key_delete(uid, 403, True) - uobj = self.user_get(uname, with_app_ctx = True) + uobj = self.user_get(uname, with_app_ctx=True) self.assertEqual(uobj.apikey, 'apikey-{}'.format(uname)) def test_01_login_api(self): @@ -107,61 +114,60 @@ class AuthAPITestCase(TestRunnerMixin, VialTestCase): {'Authorization': 'apikey-admin'}, {'Authorization': 'key apikey-admin'}, {'Authorization': 'token apikey-admin'} - ): + ): response = self.client.get( '/', - follow_redirects = True, - headers = tcase + follow_redirects=True, + headers=tcase ) self.assertEqual(response.status_code, 200) self.assertTrue(b'Welcome!' in response.data) self.assertTrue(b'My account' in response.data) self.assertTrue(b'data-user-name="admin"' in response.data) - for tcase in ( {'api_key': 'apikey-admin'}, {'api_token': 'apikey-admin'} - ): + ): response = self.client.post( '/', - follow_redirects = True, - data = tcase + follow_redirects=True, + data=tcase ) self.assertEqual(response.status_code, 200) self.assertTrue(b'Welcome!' in response.data) self.assertTrue(b'My account' in response.data) self.assertTrue(b'data-user-name="admin"' in response.data) - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_USER) def test_02_keymng_user(self): """ Test, that 'user' can manage only his own API key. """ - self._test_keymng_success(vial.const.ROLE_USER) - self._test_keymng_failure(vial.const.ROLE_DEVELOPER) - self._test_keymng_failure(vial.const.ROLE_ADMIN) + self._test_keymng_success(hawat.const.ROLE_USER) + self._test_keymng_failure(hawat.const.ROLE_DEVELOPER) + self._test_keymng_failure(hawat.const.ROLE_ADMIN) - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_DEVELOPER) def test_03_keymng_developer(self): """ Test, that 'developer' can manage only his own API key. """ - self._test_keymng_failure(vial.const.ROLE_USER) - self._test_keymng_success(vial.const.ROLE_DEVELOPER) - self._test_keymng_failure(vial.const.ROLE_ADMIN) + self._test_keymng_failure(hawat.const.ROLE_USER) + self._test_keymng_success(hawat.const.ROLE_DEVELOPER) + self._test_keymng_failure(hawat.const.ROLE_ADMIN) - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_ADMIN) def test_04_keymng_admin(self): """ Test, that 'admin' user is able to manage all API keys. """ - self._test_keymng_success(vial.const.ROLE_USER) - self._test_keymng_success(vial.const.ROLE_DEVELOPER) - self._test_keymng_success(vial.const.ROLE_ADMIN) + self._test_keymng_success(hawat.const.ROLE_USER) + self._test_keymng_success(hawat.const.ROLE_DEVELOPER) + self._test_keymng_success(hawat.const.ROLE_ADMIN) -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- if __name__ == "__main__": diff --git a/lib/hawat/blueprints/auth_dev/__init__.py b/lib/hawat/blueprints/auth_dev/__init__.py index 46567e7ec97ee2f5bb366a0c6d131bb70442ec19..2bc11cfafb9c8a9af356513bd8548288f625184a 100644 --- a/lib/hawat/blueprints/auth_dev/__init__.py +++ b/lib/hawat/blueprints/auth_dev/__init__.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ @@ -37,25 +37,78 @@ Provided endpoints * *Methods:* ``GET``, ``POST`` """ - __author__ = "Jan Mach <jan.mach@cesnet.cz>" __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" - import flask +from flask_babel import lazy_gettext + +import hawat.const +import hawat.forms +import hawat.db +from hawat.base import HawatBlueprint +from hawat.view import BaseLoginView, BaseRegisterView +from hawat.view.mixin import HTMLMixin, SQLAlchemyMixin -import vial.const -import vial.forms -import vial.db -import vial.blueprints.auth_dev -from vial.blueprints.auth_dev import BLUEPRINT_NAME, LoginView, DevAuthBlueprint -from hawat.blueprints.auth_dev.forms import RegisterUserAccountForm +from hawat.blueprints.auth_dev.forms import LoginForm, RegisterUserAccountForm +BLUEPRINT_NAME = 'auth_dev' +"""Name of the blueprint as module global constant.""" -class RegisterView(vial.blueprints.auth_dev.RegisterView): + +class LoginView(HTMLMixin, SQLAlchemyMixin, BaseLoginView): + """ + View enabling special developer login. + """ + methods = ['GET', 'POST'] + + @classmethod + def get_view_title(cls, **kwargs): + return lazy_gettext('Developer login') + + @classmethod + def get_menu_title(cls, **kwargs): + return lazy_gettext('Login (dev)') + + @property + def dbmodel(self): + return self.get_model(hawat.const.MODEL_USER) + + @property + def search_by(self): + return self.dbmodel.login + + def get_user_login(self): + form = LoginForm() + self.response_context.update( + form=form + ) + if form.validate_on_submit(): + return form.login.data + return None + + +class RegisterView(HTMLMixin, SQLAlchemyMixin, BaseRegisterView): """ View responsible for registering new user account into application. """ + methods = ['GET', 'POST'] + + @classmethod + def get_menu_title(cls, **kwargs): + return lazy_gettext('Register (dev)') + + @classmethod + def get_view_title(cls, **kwargs): + return lazy_gettext('User account registration (dev)') + + @property + def dbmodel(self): + return self.get_model(hawat.const.MODEL_USER) + + @property + def dbchlogmodel(self): + return self.get_model(hawat.const.MODEL_ITEM_CHANGELOG) @staticmethod def get_item_form(item): @@ -70,29 +123,40 @@ class RegisterView(vial.blueprints.auth_dev.RegisterView): ) return RegisterUserAccountForm( - choices_roles = roles, - choices_locales = locales + choices_roles=roles, + choices_locales=locales ) -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- + + +class DevAuthBlueprint(HawatBlueprint): + """Pluggable module - developer authentication service (*auth_dev*).""" + + @classmethod + def get_module_title(cls): + return lazy_gettext('Developer authentication service') + + +# ------------------------------------------------------------------------------- def get_blueprint(): """ - Mandatory interface for :py:mod:`vial.Vial` and factory function. This function - must return a valid instance of :py:class:`vial.app.VialBlueprint` or + Mandatory interface for :py:mod:`hawat.Hawat` and factory function. This function + must return a valid instance of :py:class:`hawat.app.HawatBlueprint` or :py:class:`flask.Blueprint`. """ hbp = DevAuthBlueprint( BLUEPRINT_NAME, __name__, - template_folder = 'templates', - url_prefix = '/{}'.format(BLUEPRINT_NAME) + template_folder='templates', + url_prefix='/{}'.format(BLUEPRINT_NAME) ) - hbp.register_view_class(LoginView, '/login') + hbp.register_view_class(LoginView, '/login') hbp.register_view_class(RegisterView, '/register') return hbp diff --git a/lib/hawat/blueprints/auth_dev/forms.py b/lib/hawat/blueprints/auth_dev/forms.py index 91926a9834be6a0a967927d782868ccb87d01bb6..6b70702f8be7df655dadf7082fe5a5310d262917 100644 --- a/lib/hawat/blueprints/auth_dev/forms.py +++ b/lib/hawat/blueprints/auth_dev/forms.py @@ -1,52 +1,92 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ This module contains custom developer login form for Hawat. """ - __author__ = "Jan Mach <jan.mach@cesnet.cz>" __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" - import wtforms from wtforms.ext.sqlalchemy.fields import QuerySelectMultipleField +import flask +import flask_wtf from flask_babel import lazy_gettext -from vial.forms import check_login, check_unique_login, get_available_groups - +import hawat.const +import hawat.forms +import hawat.db +from hawat.forms import check_login, check_unique_login, get_available_groups from hawat.blueprints.users.forms import BaseUserAccountForm +class LoginForm(flask_wtf.FlaskForm): + """ + Class representing developer authentication login form. This form provides + list of all currently existing user accounts in simple selectbox, so that + the developer can quickly login as different user. + """ + login = wtforms.SelectField( + lazy_gettext('User account:'), + validators=[ + wtforms.validators.DataRequired(), + check_login + ] + ) + submit = wtforms.SubmitField( + lazy_gettext('Login') + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.set_choices() + + def set_choices(self): + """ + Load list of all user accounts and populate the ``choices`` attribute of + the ``login`` selectbox. + """ + dbsess = hawat.db.db_get().session + user_model = flask.current_app.get_model(hawat.const.MODEL_USER) + users = dbsess.query(user_model).order_by(user_model.login).all() + + choices = [] + for usr in users: + choices.append((usr.login, "{} ({}, #{})".format(usr.fullname, usr.login, usr.id))) + choices = sorted(choices, key=lambda x: x[1]) + self.login.choices = choices + + class RegisterUserAccountForm(BaseUserAccountForm): """ Class representing user account registration form. """ login = wtforms.StringField( lazy_gettext('Login:'), - validators = [ + validators=[ wtforms.validators.DataRequired(), - wtforms.validators.Length(min = 3, max = 50), + wtforms.validators.Length(min=3, max=50), check_login, check_unique_login ] ) memberships_wanted = QuerySelectMultipleField( lazy_gettext('Requested group memberships:'), - query_factory = get_available_groups + query_factory=get_available_groups ) justification = wtforms.TextAreaField( lazy_gettext('Justification:'), - validators = [ + validators=[ wtforms.validators.DataRequired(), - wtforms.validators.Length(min = 10, max = 500) + wtforms.validators.Length(min=10, max=500) ] ) diff --git a/lib/hawat/blueprints/auth_dev/templates/registration/email_admins.txt b/lib/hawat/blueprints/auth_dev/templates/registration/email_admins.txt index 4ec6751c6248fb055a02b53ef949107cb03e5fc1..5c63ba521fb0e2141b766b2e0109dd26a2350b91 100644 --- a/lib/hawat/blueprints/auth_dev/templates/registration/email_admins.txt +++ b/lib/hawat/blueprints/auth_dev/templates/registration/email_admins.txt @@ -1,6 +1,6 @@ {{ _('Dear administrator,') | wordwrap }} -{{ _('a new account "%(item_id)s" was just registered in %(app_name)s. Please review the following request and activate or delete the account:', item_id = account.login, app_name = vial_appname) | wordwrap }} +{{ _('a new account "%(item_id)s" was just registered in %(app_name)s. Please review the following request and activate or delete the account:', item_id = account.login, app_name = hawat_appname) | wordwrap }} {{ '{:16s}'.format(_('Login:')) }} {{ account.login }} {{ '{:16s}'.format(_('Full name:')) }} {{ account.fullname }} @@ -24,4 +24,4 @@ {{ _('Have a nice day') | wordwrap }} --- {{ vial_appname }} +-- {{ hawat_appname }} diff --git a/lib/hawat/blueprints/auth_dev/templates/registration/email_managers.txt b/lib/hawat/blueprints/auth_dev/templates/registration/email_managers.txt index c3b79bb78b0aea36f9464cc959db4eedc9cc9c07..2f8ae69b96b3e5739be5031e876e2b3621a42543 100644 --- a/lib/hawat/blueprints/auth_dev/templates/registration/email_managers.txt +++ b/lib/hawat/blueprints/auth_dev/templates/registration/email_managers.txt @@ -1,6 +1,6 @@ {{ _('Dear group manager,') | wordwrap }} -{{ _('a new account "%(item_id)s" was just registered in %(app_name)s and user requested membership in your group "%(group_id)s". Please review the following information and approve or reject the request:', item_id = account.login, app_name = vial_appname, group_id = group.name) | wordwrap }} +{{ _('a new account "%(item_id)s" was just registered in %(app_name)s and user requested membership in your group "%(group_id)s". Please review the following information and approve or reject the request:', item_id = account.login, app_name = hawat_appname, group_id = group.name) | wordwrap }} {{ '{:16s}'.format(_('Login:')) }} {{ account.login }} {{ '{:16s}'.format(_('Full name:')) }} {{ account.fullname }} @@ -17,4 +17,4 @@ {{ _('Have a nice day') | wordwrap }} --- {{ vial_appname }} +-- {{ hawat_appname }} diff --git a/lib/hawat/blueprints/auth_dev/templates/registration/email_user.txt b/lib/hawat/blueprints/auth_dev/templates/registration/email_user.txt index fd0a4f9051fd07f83c3f0a07324788cd36193f86..0eaccda2562a7a15b514a823ceb411cb92f47834 100644 --- a/lib/hawat/blueprints/auth_dev/templates/registration/email_user.txt +++ b/lib/hawat/blueprints/auth_dev/templates/registration/email_user.txt @@ -1,6 +1,6 @@ {{ _('Dear user,') | wordwrap }} -{{ _('this email is a confirmation, that you have successfully registered your new user account "%(item_id)s" in %(app_name)s.', item_id = account.login, app_name = vial_appname) | wordwrap }} +{{ _('this email is a confirmation, that you have successfully registered your new user account "%(item_id)s" in %(app_name)s.', item_id = account.login, app_name = hawat_appname) | wordwrap }} {{ _('During the registration process you have provided following information:') | wordwrap }} @@ -28,4 +28,4 @@ {{ _('Have a nice day') | wordwrap }} --- {{ vial_appname }} +-- {{ hawat_appname }} diff --git a/lib/hawat/blueprints/auth_dev/test/__init__.py b/lib/hawat/blueprints/auth_dev/test/__init__.py index 31d6fdc0fd9d735b9f4e2504022df12f5b8006a1..e38a07ea2f1d38996483229e9594df7db3709df0 100644 --- a/lib/hawat/blueprints/auth_dev/test/__init__.py +++ b/lib/hawat/blueprints/auth_dev/test/__init__.py @@ -1,24 +1,23 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ Unit tests for :py:mod:`hawat.blueprints.auth_dev`. """ - import unittest -import vial.const -import vial.test -import vial.test.fixtures -import vial.db +import hawat.const +import hawat.test +import hawat.test.fixtures +import hawat.db from hawat.test import RegistrationHawatTestCase from hawat.test.runner import TestRunnerMixin @@ -32,7 +31,7 @@ class AuthDevTestCase(TestRunnerMixin, RegistrationHawatTestCase): """ Test login/logout with *auth_dev* module - user 'user'. """ - response = self.login_dev(vial.const.ROLE_USER) + response = self.login_dev(hawat.const.ROLE_USER) self.assertEqual(response.status_code, 200) self.assertTrue(b'You have been successfully logged in as' in response.data) @@ -44,7 +43,7 @@ class AuthDevTestCase(TestRunnerMixin, RegistrationHawatTestCase): """ Test login/logout with *auth_dev* module - user 'developer'. """ - response = self.login_dev(vial.const.ROLE_DEVELOPER) + response = self.login_dev(hawat.const.ROLE_DEVELOPER) self.assertEqual(response.status_code, 200) self.assertTrue(b'You have been successfully logged in as' in response.data) @@ -56,7 +55,7 @@ class AuthDevTestCase(TestRunnerMixin, RegistrationHawatTestCase): """ Test login/logout with *auth_dev* module - user 'admin'. """ - response = self.login_dev(vial.const.ROLE_ADMIN) + response = self.login_dev(hawat.const.ROLE_ADMIN) self.assertEqual(response.status_code, 200) self.assertTrue(b'You have been successfully logged in as' in response.data) @@ -155,7 +154,7 @@ class AuthDevTestCase(TestRunnerMixin, RegistrationHawatTestCase): ) -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- if __name__ == "__main__": diff --git a/lib/hawat/blueprints/auth_env/__init__.py b/lib/hawat/blueprints/auth_env/__init__.py index a24d2c0a7375db6801158e34c1f5e75feec31231..debf20c2d71729a88da4895bce1379ecdb0b495f 100644 --- a/lib/hawat/blueprints/auth_env/__init__.py +++ b/lib/hawat/blueprints/auth_env/__init__.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ @@ -31,7 +31,7 @@ Currently following environment variables set up by the HTTP server are supporte ``eppn``,``REMOTE_USER`` (*MANDATORY*) The ``eppn`` server variable is set up by the _shibd_ daemon implementing the Shibboleth SSO service. The ``REMOTE_USER`` variable is set up by many - authentication providers. This environment variable is of course mandatory + authentication providers. This environment variable is of course mandatory, and it is used as an account username (login). ``cn``,``givenName``,``sn`` (*OPTIONAL*) @@ -69,26 +69,30 @@ Provided endpoints * *Methods:* ``GET``, ``POST`` """ - __author__ = "Jan Mach <jan.mach@cesnet.cz>" __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" - import flask -from flask_babel import gettext +from flask_babel import gettext, lazy_gettext + +import hawat.const +import hawat.forms +import hawat.db +from hawat.base import HawatBlueprint +from hawat.view import BaseLoginView, BaseRegisterView +from hawat.view.mixin import HTMLMixin, SQLAlchemyMixin -import vial.const -import vial.forms -import vial.db -import vial.blueprints.auth_env -from vial.blueprints.auth_env import BLUEPRINT_NAME, LoginView from hawat.blueprints.auth_env.forms import RegisterUserAccountForm +BLUEPRINT_NAME = 'auth_env' +"""Name of the blueprint as module global constant.""" + class RegistrationException(Exception): """ Exception describing problems with new user account registration. """ + def __init__(self, description): super().__init__() self.description = description @@ -107,10 +111,60 @@ def get_login_from_environment(): ) -class RegisterView(vial.blueprints.auth_env.RegisterView): +class LoginView(HTMLMixin, SQLAlchemyMixin, BaseLoginView): + """ + View responsible for user login via application environment. + """ + methods = ['GET'] + + @classmethod + def get_view_title(cls, **kwargs): + return lazy_gettext('Environment login') + + @classmethod + def get_menu_title(cls, **kwargs): + return lazy_gettext('Login (env)') + + @property + def dbmodel(self): + return self.get_model(hawat.const.MODEL_USER) + + @property + def search_by(self): + return self.dbmodel.login + + def get_user_login(self): + user_login = get_login_from_environment() + if not user_login: + self.flash( + gettext('User login was not received, unable to perform login process.'), + hawat.const.FLASH_FAILURE + ) + self.abort(403) + return user_login + + +class RegisterView(HTMLMixin, SQLAlchemyMixin, BaseRegisterView): """ View responsible for registering new user account into application. """ + methods = ['GET', 'POST'] + + @classmethod + def get_menu_title(cls, **kwargs): + return lazy_gettext('Register (env)') + + @classmethod + def get_view_title(cls, **kwargs): + return lazy_gettext('User account registration (env)') + + @property + def dbmodel(self): + return self.get_model(hawat.const.MODEL_USER) + + @property + def dbchlogmodel(self): + return self.get_model(hawat.const.MODEL_ITEM_CHANGELOG) def get_user_from_env(self): """ @@ -187,41 +241,55 @@ class RegisterView(vial.blueprints.auth_env.RegisterView): ) return RegisterUserAccountForm( - obj = item, - choices_locales = locales + obj=item, + choices_locales=locales ) + def dispatch_request(self): + """ + Mandatory interface required by the :py:func:`flask.views.View.dispatch_request`. + Will be called by the *Flask* framework to service the request. + """ + self.response_context.update( + apacheenv=flask.request.environ + ) + return super().dispatch_request() -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- -class EnvAuthBlueprint(vial.blueprints.auth_env.EnvAuthBlueprint): + +class EnvAuthBlueprint(HawatBlueprint): """Pluggable module - environment authentication service (*auth_env*).""" + @classmethod + def get_module_title(cls): + return lazy_gettext('Environment authentication service') + def register_app(self, app): app.set_infomailer('auth_env.register', RegisterView.inform_admins) app.set_infomailer('auth_env.register', RegisterView.inform_managers) app.set_infomailer('auth_env.register', RegisterView.inform_user) -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- def get_blueprint(): """ - Mandatory interface for :py:mod:`vial.Vial` and factory function. This function - must return a valid instance of :py:class:`vial.app.VialBlueprint` or + Mandatory interface for :py:mod:`hawat.Hawat` and factory function. This function + must return a valid instance of :py:class:`hawat.app.HawatBlueprint` or :py:class:`flask.Blueprint`. """ hbp = EnvAuthBlueprint( BLUEPRINT_NAME, __name__, - template_folder = 'templates', - url_prefix = '/{}'.format(BLUEPRINT_NAME) + template_folder='templates', + url_prefix='/{}'.format(BLUEPRINT_NAME) ) - hbp.register_view_class(LoginView, '/login') + hbp.register_view_class(LoginView, '/login') hbp.register_view_class(RegisterView, '/register') return hbp diff --git a/lib/hawat/blueprints/auth_env/forms.py b/lib/hawat/blueprints/auth_env/forms.py index 2013422aa266ee38bc1c54a528cf2031ed747c4d..bfe47ca3fadf8ee6ec4e2648bf1a044c0d549646 100644 --- a/lib/hawat/blueprints/auth_env/forms.py +++ b/lib/hawat/blueprints/auth_env/forms.py @@ -1,28 +1,26 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ This module contains custom user account registration form for Hawat. """ - __author__ = "Jan Mach <jan.mach@cesnet.cz>" __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" - import wtforms from wtforms.ext.sqlalchemy.fields import QuerySelectMultipleField from flask_babel import lazy_gettext -from vial.forms import get_available_groups +from hawat.forms import get_available_groups from hawat.blueprints.users.forms import BaseUserAccountForm @@ -33,12 +31,12 @@ class RegisterUserAccountForm(BaseUserAccountForm): """ memberships_wanted = QuerySelectMultipleField( lazy_gettext('Requested group memberships:'), - query_factory = get_available_groups + query_factory=get_available_groups ) justification = wtforms.TextAreaField( lazy_gettext('Justification:'), - validators = [ + validators=[ wtforms.validators.DataRequired(), - wtforms.validators.Length(min = 10, max = 500) + wtforms.validators.Length(min=10, max=500) ] ) diff --git a/lib/hawat/blueprints/auth_env/templates/auth_env/registration/email_admins.txt b/lib/hawat/blueprints/auth_env/templates/auth_env/registration/email_admins.txt index 4ec6751c6248fb055a02b53ef949107cb03e5fc1..5c63ba521fb0e2141b766b2e0109dd26a2350b91 100644 --- a/lib/hawat/blueprints/auth_env/templates/auth_env/registration/email_admins.txt +++ b/lib/hawat/blueprints/auth_env/templates/auth_env/registration/email_admins.txt @@ -1,6 +1,6 @@ {{ _('Dear administrator,') | wordwrap }} -{{ _('a new account "%(item_id)s" was just registered in %(app_name)s. Please review the following request and activate or delete the account:', item_id = account.login, app_name = vial_appname) | wordwrap }} +{{ _('a new account "%(item_id)s" was just registered in %(app_name)s. Please review the following request and activate or delete the account:', item_id = account.login, app_name = hawat_appname) | wordwrap }} {{ '{:16s}'.format(_('Login:')) }} {{ account.login }} {{ '{:16s}'.format(_('Full name:')) }} {{ account.fullname }} @@ -24,4 +24,4 @@ {{ _('Have a nice day') | wordwrap }} --- {{ vial_appname }} +-- {{ hawat_appname }} diff --git a/lib/hawat/blueprints/auth_env/templates/auth_env/registration/email_managers.txt b/lib/hawat/blueprints/auth_env/templates/auth_env/registration/email_managers.txt index c3b79bb78b0aea36f9464cc959db4eedc9cc9c07..2f8ae69b96b3e5739be5031e876e2b3621a42543 100644 --- a/lib/hawat/blueprints/auth_env/templates/auth_env/registration/email_managers.txt +++ b/lib/hawat/blueprints/auth_env/templates/auth_env/registration/email_managers.txt @@ -1,6 +1,6 @@ {{ _('Dear group manager,') | wordwrap }} -{{ _('a new account "%(item_id)s" was just registered in %(app_name)s and user requested membership in your group "%(group_id)s". Please review the following information and approve or reject the request:', item_id = account.login, app_name = vial_appname, group_id = group.name) | wordwrap }} +{{ _('a new account "%(item_id)s" was just registered in %(app_name)s and user requested membership in your group "%(group_id)s". Please review the following information and approve or reject the request:', item_id = account.login, app_name = hawat_appname, group_id = group.name) | wordwrap }} {{ '{:16s}'.format(_('Login:')) }} {{ account.login }} {{ '{:16s}'.format(_('Full name:')) }} {{ account.fullname }} @@ -17,4 +17,4 @@ {{ _('Have a nice day') | wordwrap }} --- {{ vial_appname }} +-- {{ hawat_appname }} diff --git a/lib/hawat/blueprints/auth_env/templates/auth_env/registration/email_user.txt b/lib/hawat/blueprints/auth_env/templates/auth_env/registration/email_user.txt index fd0a4f9051fd07f83c3f0a07324788cd36193f86..0eaccda2562a7a15b514a823ceb411cb92f47834 100644 --- a/lib/hawat/blueprints/auth_env/templates/auth_env/registration/email_user.txt +++ b/lib/hawat/blueprints/auth_env/templates/auth_env/registration/email_user.txt @@ -1,6 +1,6 @@ {{ _('Dear user,') | wordwrap }} -{{ _('this email is a confirmation, that you have successfully registered your new user account "%(item_id)s" in %(app_name)s.', item_id = account.login, app_name = vial_appname) | wordwrap }} +{{ _('this email is a confirmation, that you have successfully registered your new user account "%(item_id)s" in %(app_name)s.', item_id = account.login, app_name = hawat_appname) | wordwrap }} {{ _('During the registration process you have provided following information:') | wordwrap }} @@ -28,4 +28,4 @@ {{ _('Have a nice day') | wordwrap }} --- {{ vial_appname }} +-- {{ hawat_appname }} diff --git a/lib/hawat/blueprints/auth_env/test/__init__.py b/lib/hawat/blueprints/auth_env/test/__init__.py index 46a40a5a4e20f0e095bdb2aef9767a3fd6c2a55e..34888e5b503c4c0c7f5f367171e73459e2abcf7a 100644 --- a/lib/hawat/blueprints/auth_env/test/__init__.py +++ b/lib/hawat/blueprints/auth_env/test/__init__.py @@ -1,24 +1,23 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ Unit tests for :py:mod:`hawat.blueprints.auth_env`. """ - import unittest -import vial.const -import vial.test -import vial.test.fixtures -import vial.db +import hawat.const +import hawat.test +import hawat.test.fixtures +import hawat.db from hawat.test import RegistrationHawatTestCase from hawat.test.runner import TestRunnerMixin @@ -32,7 +31,7 @@ class AuthEnvTestCase(TestRunnerMixin, RegistrationHawatTestCase): """ Test login/logout with *auth_env* module - user 'user'. """ - response = self.login_env(vial.const.ROLE_USER) + response = self.login_env(hawat.const.ROLE_USER) self.assertEqual(response.status_code, 200) self.assertTrue(b'You have been successfully logged in as' in response.data) @@ -40,7 +39,7 @@ class AuthEnvTestCase(TestRunnerMixin, RegistrationHawatTestCase): self.assertEqual(response.status_code, 200) self.assertTrue(b'You have been successfully logged out' in response.data) - response = self.login_env(vial.const.ROLE_USER, 'REMOTE_USER') + response = self.login_env(hawat.const.ROLE_USER, 'REMOTE_USER') self.assertEqual(response.status_code, 200) self.assertTrue(b'You have been successfully logged in as' in response.data) @@ -52,7 +51,7 @@ class AuthEnvTestCase(TestRunnerMixin, RegistrationHawatTestCase): """ Test login/logout with *auth_env* module - user 'developer'. """ - response = self.login_env(vial.const.ROLE_DEVELOPER) + response = self.login_env(hawat.const.ROLE_DEVELOPER) self.assertEqual(response.status_code, 200) self.assertTrue(b'You have been successfully logged in as' in response.data) @@ -60,7 +59,7 @@ class AuthEnvTestCase(TestRunnerMixin, RegistrationHawatTestCase): self.assertEqual(response.status_code, 200) self.assertTrue(b'You have been successfully logged out' in response.data) - response = self.login_env(vial.const.ROLE_DEVELOPER, 'REMOTE_USER') + response = self.login_env(hawat.const.ROLE_DEVELOPER, 'REMOTE_USER') self.assertEqual(response.status_code, 200) self.assertTrue(b'You have been successfully logged in as' in response.data) @@ -72,7 +71,7 @@ class AuthEnvTestCase(TestRunnerMixin, RegistrationHawatTestCase): """ Test login/logout with *auth_env* module - user 'admin'. """ - response = self.login_env(vial.const.ROLE_ADMIN) + response = self.login_env(hawat.const.ROLE_ADMIN) self.assertEqual(response.status_code, 200) self.assertTrue(b'You have been successfully logged in as' in response.data) @@ -80,7 +79,7 @@ class AuthEnvTestCase(TestRunnerMixin, RegistrationHawatTestCase): self.assertEqual(response.status_code, 200) self.assertTrue(b'You have been successfully logged out' in response.data) - response = self.login_env(vial.const.ROLE_ADMIN, 'REMOTE_USER') + response = self.login_env(hawat.const.ROLE_ADMIN, 'REMOTE_USER') self.assertEqual(response.status_code, 200) self.assertTrue(b'You have been successfully logged in as' in response.data) @@ -187,7 +186,7 @@ class AuthEnvTestCase(TestRunnerMixin, RegistrationHawatTestCase): ) -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- if __name__ == "__main__": diff --git a/lib/hawat/blueprints/auth_pwd/__init__.py b/lib/hawat/blueprints/auth_pwd/__init__.py index 5e5462767ac27f1df18a196f7c205e4745cae425..e42cb091d3d11dd7d3ab28e6a2524a70f7d1cd1f 100644 --- a/lib/hawat/blueprints/auth_pwd/__init__.py +++ b/lib/hawat/blueprints/auth_pwd/__init__.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ @@ -23,25 +23,86 @@ Provided endpoints * *Methods:* ``GET``, ``POST`` """ - __author__ = "Jan Mach <jan.mach@cesnet.cz>" __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" - import flask +from flask_babel import lazy_gettext -import vial.const -import vial.forms -import vial.blueprints.auth_pwd -from vial.blueprints.auth_pwd import BLUEPRINT_NAME, LoginView, PwdAuthBlueprint -from hawat.blueprints.auth_pwd.forms import RegisterUserAccountForm +import hawat.const +import hawat.forms +from hawat.base import HawatBlueprint +from hawat.view import BaseLoginView, BaseRegisterView +from hawat.view.mixin import HTMLMixin, SQLAlchemyMixin +from hawat.blueprints.auth_pwd.forms import LoginForm, RegisterUserAccountForm +BLUEPRINT_NAME = 'auth_pwd' +"""Name of the blueprint as module global constant.""" -class RegisterView(vial.blueprints.auth_pwd.RegisterView): + +class LoginView(HTMLMixin, SQLAlchemyMixin, BaseLoginView): """ View enabling classical password login. """ + methods = ['GET', 'POST'] + + @classmethod + def get_view_title(cls, **kwargs): + return lazy_gettext('Password login') + + @classmethod + def get_menu_title(cls, **kwargs): + return lazy_gettext('Login (pwd)') + + @property + def dbmodel(self): + return self.get_model(hawat.const.MODEL_USER) + + @property + def search_by(self): + return self.dbmodel.login + + def get_user_login(self): + form = LoginForm() + self.response_context.update( + form=form + ) + if form.validate_on_submit(): + return form.login.data + return None + + def authenticate_user(self, user): + return user.check_password( + self.response_context['form'].password.data + ) + + +class RegisterView(HTMLMixin, SQLAlchemyMixin, BaseRegisterView): + """ + View enabling classical password login. + """ + methods = ['GET', 'POST'] + + @classmethod + def get_menu_title(cls, **kwargs): + return lazy_gettext('Register (pwd)') + + @classmethod + def get_view_title(cls, **kwargs): + return lazy_gettext('User account registration (pwd)') + + @property + def dbmodel(self): + return self.get_model(hawat.const.MODEL_USER) + + @property + def dbchlogmodel(self): + return self.get_model(hawat.const.MODEL_ITEM_CHANGELOG) + + @property + def search_by(self): + return self.dbmodel.login @staticmethod def get_item_form(item): @@ -49,28 +110,43 @@ class RegisterView(vial.blueprints.auth_pwd.RegisterView): flask.current_app.config['SUPPORTED_LOCALES'].items() ) return RegisterUserAccountForm( - choices_locales = locales + choices_locales=locales ) + def do_before_action(self, item): # pylint: disable=locally-disabled,no-self-use,unused-argument + super().do_before_action(item) + item.set_password(item.password) + + +# ------------------------------------------------------------------------------- + + +class PwdAuthBlueprint(HawatBlueprint): + """Pluggable module - classical authentication service (*auth_pwd*).""" + + @classmethod + def get_module_title(cls): + return lazy_gettext('Password authentication service') + -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- def get_blueprint(): """ - Mandatory interface for :py:mod:`vial.Vial` and factory function. This function - must return a valid instance of :py:class:`vial.app.VialBlueprint` or + Mandatory interface for :py:mod:`hawat.Hawat` and factory function. This function + must return a valid instance of :py:class:`hawat.app.HawatBlueprint` or :py:class:`flask.Blueprint`. """ hbp = PwdAuthBlueprint( BLUEPRINT_NAME, __name__, - template_folder = 'templates', - url_prefix = '/{}'.format(BLUEPRINT_NAME) + template_folder='templates', + url_prefix='/{}'.format(BLUEPRINT_NAME) ) - hbp.register_view_class(LoginView, '/login') + hbp.register_view_class(LoginView, '/login') hbp.register_view_class(RegisterView, '/register') return hbp diff --git a/lib/hawat/blueprints/auth_pwd/forms.py b/lib/hawat/blueprints/auth_pwd/forms.py index 9d8ac9972df0a25e636da4354090f1c8b3aed740..0f5695e0f81d226129e4e898f497cb87d65825a6 100644 --- a/lib/hawat/blueprints/auth_pwd/forms.py +++ b/lib/hawat/blueprints/auth_pwd/forms.py @@ -1,63 +1,87 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- -# This file is part of MyDojo package (https://github.com/honzamach/mydojo). +# ------------------------------------------------------------------------------- +# This file is part of Mentat system (https://mentat.cesnet.cz/). # -# Copyright (C) since 2018 Honza Mach <honza.mach.ml@gmail.com> +# Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ This module contains custom developer login form for Hawat. """ - -__author__ = "Honza Mach <honza.mach.ml@gmail.com>" - +__author__ = "Jan Mach <jan.mach@cesnet.cz>" +__credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" import wtforms +import flask_wtf from wtforms.ext.sqlalchemy.fields import QuerySelectMultipleField from flask_babel import lazy_gettext -from vial.forms import check_login, check_unique_login, get_available_groups +from hawat.forms import check_login, check_unique_login, get_available_groups from hawat.blueprints.users.forms import BaseUserAccountForm +class LoginForm(flask_wtf.FlaskForm): + """ + Class representing classical password authentication login form. + """ + login = wtforms.StringField( + lazy_gettext('Login:'), + validators=[ + wtforms.validators.DataRequired(), + wtforms.validators.Length(min=3, max=50), + check_login + ] + ) + password = wtforms.PasswordField( + lazy_gettext('Password:'), + validators=[ + wtforms.validators.DataRequired(), + wtforms.validators.Length(min=8), + ] + ) + submit = wtforms.SubmitField( + lazy_gettext('Login') + ) + + class RegisterUserAccountForm(BaseUserAccountForm): """ Class representing classical account registration form. """ login = wtforms.StringField( lazy_gettext('Login:'), - validators = [ + validators=[ wtforms.validators.DataRequired(), - wtforms.validators.Length(min = 3, max = 50), + wtforms.validators.Length(min=3, max=50), check_login, check_unique_login ] ) memberships_wanted = QuerySelectMultipleField( lazy_gettext('Requested group memberships:'), - query_factory = get_available_groups + query_factory=get_available_groups ) justification = wtforms.TextAreaField( lazy_gettext('Justification:'), - validators = [ + validators=[ wtforms.validators.DataRequired(), - wtforms.validators.Length(min = 10, max = 500) + wtforms.validators.Length(min=10, max=500) ] ) password = wtforms.PasswordField( lazy_gettext('Password:'), - validators = [ + validators=[ wtforms.validators.DataRequired(), - wtforms.validators.Length(min = 8), + wtforms.validators.Length(min=8), ] ) password2 = wtforms.PasswordField( lazy_gettext('Repeat Password:'), - validators = [ + validators=[ wtforms.validators.DataRequired(), wtforms.validators.EqualTo('password'), ] diff --git a/lib/hawat/blueprints/auth_pwd/templates/registration/email_admins.txt b/lib/hawat/blueprints/auth_pwd/templates/registration/email_admins.txt index 4ec6751c6248fb055a02b53ef949107cb03e5fc1..5c63ba521fb0e2141b766b2e0109dd26a2350b91 100644 --- a/lib/hawat/blueprints/auth_pwd/templates/registration/email_admins.txt +++ b/lib/hawat/blueprints/auth_pwd/templates/registration/email_admins.txt @@ -1,6 +1,6 @@ {{ _('Dear administrator,') | wordwrap }} -{{ _('a new account "%(item_id)s" was just registered in %(app_name)s. Please review the following request and activate or delete the account:', item_id = account.login, app_name = vial_appname) | wordwrap }} +{{ _('a new account "%(item_id)s" was just registered in %(app_name)s. Please review the following request and activate or delete the account:', item_id = account.login, app_name = hawat_appname) | wordwrap }} {{ '{:16s}'.format(_('Login:')) }} {{ account.login }} {{ '{:16s}'.format(_('Full name:')) }} {{ account.fullname }} @@ -24,4 +24,4 @@ {{ _('Have a nice day') | wordwrap }} --- {{ vial_appname }} +-- {{ hawat_appname }} diff --git a/lib/hawat/blueprints/auth_pwd/templates/registration/email_managers.txt b/lib/hawat/blueprints/auth_pwd/templates/registration/email_managers.txt index c3b79bb78b0aea36f9464cc959db4eedc9cc9c07..2f8ae69b96b3e5739be5031e876e2b3621a42543 100644 --- a/lib/hawat/blueprints/auth_pwd/templates/registration/email_managers.txt +++ b/lib/hawat/blueprints/auth_pwd/templates/registration/email_managers.txt @@ -1,6 +1,6 @@ {{ _('Dear group manager,') | wordwrap }} -{{ _('a new account "%(item_id)s" was just registered in %(app_name)s and user requested membership in your group "%(group_id)s". Please review the following information and approve or reject the request:', item_id = account.login, app_name = vial_appname, group_id = group.name) | wordwrap }} +{{ _('a new account "%(item_id)s" was just registered in %(app_name)s and user requested membership in your group "%(group_id)s". Please review the following information and approve or reject the request:', item_id = account.login, app_name = hawat_appname, group_id = group.name) | wordwrap }} {{ '{:16s}'.format(_('Login:')) }} {{ account.login }} {{ '{:16s}'.format(_('Full name:')) }} {{ account.fullname }} @@ -17,4 +17,4 @@ {{ _('Have a nice day') | wordwrap }} --- {{ vial_appname }} +-- {{ hawat_appname }} diff --git a/lib/hawat/blueprints/auth_pwd/templates/registration/email_user.txt b/lib/hawat/blueprints/auth_pwd/templates/registration/email_user.txt index fd0a4f9051fd07f83c3f0a07324788cd36193f86..0eaccda2562a7a15b514a823ceb411cb92f47834 100644 --- a/lib/hawat/blueprints/auth_pwd/templates/registration/email_user.txt +++ b/lib/hawat/blueprints/auth_pwd/templates/registration/email_user.txt @@ -1,6 +1,6 @@ {{ _('Dear user,') | wordwrap }} -{{ _('this email is a confirmation, that you have successfully registered your new user account "%(item_id)s" in %(app_name)s.', item_id = account.login, app_name = vial_appname) | wordwrap }} +{{ _('this email is a confirmation, that you have successfully registered your new user account "%(item_id)s" in %(app_name)s.', item_id = account.login, app_name = hawat_appname) | wordwrap }} {{ _('During the registration process you have provided following information:') | wordwrap }} @@ -28,4 +28,4 @@ {{ _('Have a nice day') | wordwrap }} --- {{ vial_appname }} +-- {{ hawat_appname }} diff --git a/lib/hawat/blueprints/auth_pwd/test/__init__.py b/lib/hawat/blueprints/auth_pwd/test/__init__.py index 0883718683e7c375a43b5158f5d02ca219d97883..b3c8b7f040d0aea4a71218ca4579e10092c330f5 100644 --- a/lib/hawat/blueprints/auth_pwd/test/__init__.py +++ b/lib/hawat/blueprints/auth_pwd/test/__init__.py @@ -1,24 +1,26 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ Unit tests for :py:mod:`hawat.blueprints.auth_pwd`. """ +__author__ = "Jan Mach <jan.mach@cesnet.cz>" +__credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" import unittest -import vial.const -import vial.test -import vial.test.fixtures -import vial.db +import hawat.const +import hawat.test +import hawat.test.fixtures +import hawat.db from hawat.test import RegistrationHawatTestCase from hawat.test.runner import TestRunnerMixin @@ -33,11 +35,11 @@ class AuthPwdTestCase(TestRunnerMixin, RegistrationHawatTestCase): Test login/logout with *auth_pwd* module - user 'user'. """ with self.app.app_context(): - user = self.user_get(vial.const.ROLE_USER) + user = self.user_get(hawat.const.ROLE_USER) user.set_password('password') self.user_save(user) - response = self.login_pwd(vial.const.ROLE_USER, 'password') + response = self.login_pwd(hawat.const.ROLE_USER, 'password') self.assertEqual(response.status_code, 200) self.assertTrue(b'You have been successfully logged in as' in response.data) @@ -50,11 +52,11 @@ class AuthPwdTestCase(TestRunnerMixin, RegistrationHawatTestCase): Test login/logout with *auth_pwd* module - user 'developer'. """ with self.app.app_context(): - user = self.user_get(vial.const.ROLE_DEVELOPER) + user = self.user_get(hawat.const.ROLE_DEVELOPER) user.set_password('password') self.user_save(user) - response = self.login_pwd(vial.const.ROLE_DEVELOPER, 'password') + response = self.login_pwd(hawat.const.ROLE_DEVELOPER, 'password') self.assertEqual(response.status_code, 200) self.assertTrue(b'You have been successfully logged in as' in response.data) @@ -67,11 +69,11 @@ class AuthPwdTestCase(TestRunnerMixin, RegistrationHawatTestCase): Test login/logout with *auth_pwd* module - user 'admin'. """ with self.app.app_context(): - user = self.user_get(vial.const.ROLE_ADMIN) + user = self.user_get(hawat.const.ROLE_ADMIN) user.set_password('password') self.user_save(user) - response = self.login_pwd(vial.const.ROLE_ADMIN, 'password') + response = self.login_pwd(hawat.const.ROLE_ADMIN, 'password') self.assertEqual(response.status_code, 200) self.assertTrue(b'You have been successfully logged in as' in response.data) @@ -174,7 +176,7 @@ class AuthPwdTestCase(TestRunnerMixin, RegistrationHawatTestCase): ) -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- if __name__ == "__main__": diff --git a/lib/vial/blueprints/changelogs/__init__.py b/lib/hawat/blueprints/changelogs/__init__.py similarity index 57% rename from lib/vial/blueprints/changelogs/__init__.py rename to lib/hawat/blueprints/changelogs/__init__.py index 5f4bac817f966984331803b142bbf2fe29cb6c38..094c78a467b117465aebbd455dd16fdcfeeb98d5 100644 --- a/lib/vial/blueprints/changelogs/__init__.py +++ b/lib/hawat/blueprints/changelogs/__init__.py @@ -1,24 +1,30 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- +# This file is part of Mentat system (https://mentat.cesnet.cz/). +# +# Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ This pluggable module provides access to item changelogs. """ +__author__ = "Jan Mach <jan.mach@cesnet.cz>" +__credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" import flask from flask_babel import lazy_gettext -import vial.acl -from vial.app import VialBlueprint -from vial.view import BaseSearchView, ItemShowView -from vial.view.mixin import HTMLMixin, SQLAlchemyMixin -from vial.blueprints.changelogs.forms import ItemChangeLogSearchForm - +import hawat.const +import hawat.acl +import hawat.menu +from hawat.base import HawatBlueprint +from hawat.view import BaseSearchView, ItemShowView +from hawat.view.mixin import HTMLMixin, SQLAlchemyMixin +from hawat.blueprints.changelogs.forms import ItemChangeLogSearchForm BLUEPRINT_NAME = 'changelogs' """Name of the blueprint as module global constant.""" @@ -32,7 +38,7 @@ class SearchView(HTMLMixin, SQLAlchemyMixin, BaseSearchView): # pylint: disable authentication = True - authorization = [vial.acl.PERMISSION_POWER] + authorization = [hawat.acl.PERMISSION_POWER] @classmethod def get_menu_title(cls, **kwargs): @@ -42,22 +48,22 @@ class SearchView(HTMLMixin, SQLAlchemyMixin, BaseSearchView): # pylint: disable def get_view_title(cls, **kwargs): return lazy_gettext('Search item changelogs') - #--------------------------------------------------------------------------- + # --------------------------------------------------------------------------- @property def dbmodel(self): - return self.get_model(vial.const.MODEL_ITEM_CHANGELOG) + return self.get_model(hawat.const.MODEL_ITEM_CHANGELOG) @staticmethod def get_search_form(request_args): - return ItemChangeLogSearchForm(request_args, meta = {'csrf': False}) + return ItemChangeLogSearchForm(request_args, meta={'csrf': False}) @staticmethod def build_query(query, model, form_args): - # Adjust query based on lower time boudary selection. + # Adjust query based on lower time boundary selection. if 'dt_from' in form_args and form_args['dt_from']: query = query.filter(model.createtime >= form_args['dt_from']) - # Adjust query based on upper time boudary selection. + # Adjust query based on upper time boundary selection. if 'dt_to' in form_args and form_args['dt_to']: query = query.filter(model.createtime <= form_args['dt_to']) # Adjust query based on changelog author selection. @@ -83,35 +89,37 @@ class SearchView(HTMLMixin, SQLAlchemyMixin, BaseSearchView): # pylint: disable context_action_menu.add_entry( 'submenu', 'more', - align_right = True, - legend = lazy_gettext('More actions') + align_right=True, + legend=lazy_gettext('More actions') ) context_action_menu.add_entry( 'endpoint', 'more.searchauthor', - endpoint = 'changelogs.search', - title = lazy_gettext('Other changes by the same author'), - link = lambda x: flask.url_for('changelogs.search', authors = x.author_id, dt_from = '', submit = 'Search'), - icon = 'action-search', - hidelegend = True + endpoint='changelogs.search', + title=lazy_gettext('Other changes by the same author'), + link=lambda x: flask.url_for('changelogs.search', authors=x.author_id, dt_from='', submit='Search'), + icon='action-search', + hidelegend=True ) context_action_menu.add_entry( 'endpoint', 'more.searchmodel', - endpoint = 'changelogs.search', - title = lazy_gettext('Other changes of the same item'), - link = lambda x: flask.url_for('changelogs.search', model = x.model, model_id = x.model_id, dt_from = '', submit = 'Search'), - icon = 'action-search', - hidelegend = True + endpoint='changelogs.search', + title=lazy_gettext('Other changes of the same item'), + link=lambda x: flask.url_for('changelogs.search', model=x.model, model_id=x.model_id, dt_from='', + submit='Search'), + icon='action-search', + hidelegend=True ) context_action_menu.add_entry( 'endpoint', 'more.searchboth', - endpoint = 'changelogs.search', - title = lazy_gettext('Other changes of the same item by the same author'), - link = lambda x: flask.url_for('changelogs.search', authors = x.author_id, model = x.model, model_id = x.model_id, dt_from = '', submit = 'Search'), - icon = 'action-search', - hidelegend = True + endpoint='changelogs.search', + title=lazy_gettext('Other changes of the same item by the same author'), + link=lambda x: flask.url_for('changelogs.search', authors=x.author_id, model=x.model, model_id=x.model_id, + dt_from='', submit='Search'), + icon='action-search', + hidelegend=True ) return context_action_menu @@ -124,7 +132,7 @@ class ShowView(HTMLMixin, SQLAlchemyMixin, ItemShowView): authentication = True - authorization = [vial.acl.PERMISSION_POWER] + authorization = [hawat.acl.PERMISSION_POWER] @classmethod def get_menu_title(cls, **kwargs): @@ -134,44 +142,44 @@ class ShowView(HTMLMixin, SQLAlchemyMixin, ItemShowView): def get_menu_legend(cls, **kwargs): return lazy_gettext( 'View details of item changelog record "%(item)s"', - item = str(kwargs['item']) + item=str(kwargs['item']) ) @classmethod def get_view_title(cls, **kwargs): return lazy_gettext('Show item changelog record details') - #--------------------------------------------------------------------------- + # --------------------------------------------------------------------------- @property def dbmodel(self): - return self.get_model(vial.const.MODEL_ITEM_CHANGELOG) + return self.get_model(hawat.const.MODEL_ITEM_CHANGELOG) @classmethod def get_breadcrumbs_menu(cls): # pylint: disable=locally-disabled,unused-argument - action_menu = vial.menu.Menu() + action_menu = hawat.menu.Menu() action_menu.add_entry( 'endpoint', 'home', - endpoint = flask.current_app.config['ENDPOINT_HOME'] + endpoint=flask.current_app.config['ENDPOINT_HOME'] ) action_menu.add_entry( 'endpoint', 'search', - endpoint = '{}.search'.format(cls.module_name) + endpoint='{}.search'.format(cls.module_name) ) action_menu.add_entry( 'endpoint', 'show', - endpoint = '{}.show'.format(cls.module_name) + endpoint='{}.show'.format(cls.module_name) ) return action_menu -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- -class ItemChangeLogsBlueprint(VialBlueprint): +class ItemChangeLogsBlueprint(HawatBlueprint): """Pluggable module - item changelog record management (*changelogs*).""" @classmethod @@ -182,29 +190,29 @@ class ItemChangeLogsBlueprint(VialBlueprint): app.menu_main.add_entry( 'view', 'admin.{}'.format(BLUEPRINT_NAME), - position = 80, - view = SearchView + position=80, + view=SearchView ) -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- def get_blueprint(): """ - Mandatory interface for :py:mod:`vial.Vial` and factory function. This function - must return a valid instance of :py:class:`vial.app.VialBlueprint` or + Mandatory interface for :py:mod:`hawat.Hawat` and factory function. This function + must return a valid instance of :py:class:`hawat.app.HawatBlueprint` or :py:class:`flask.Blueprint`. """ hbp = ItemChangeLogsBlueprint( BLUEPRINT_NAME, __name__, - template_folder = 'templates', - url_prefix = '/{}'.format(BLUEPRINT_NAME) + template_folder='templates', + url_prefix='/{}'.format(BLUEPRINT_NAME) ) hbp.register_view_class(SearchView, '/search') - hbp.register_view_class(ShowView, '/<int:item_id>/show') + hbp.register_view_class(ShowView, '/<int:item_id>/show') return hbp diff --git a/lib/vial/blueprints/changelogs/forms.py b/lib/hawat/blueprints/changelogs/forms.py similarity index 58% rename from lib/vial/blueprints/changelogs/forms.py rename to lib/hawat/blueprints/changelogs/forms.py index f6e9e7851e506d602638ea5edf51aa45771a8b4e..f209e82e6308a99654925b3368bb6fb4c31b372a 100644 --- a/lib/vial/blueprints/changelogs/forms.py +++ b/lib/hawat/blueprints/changelogs/forms.py @@ -1,14 +1,19 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- +# This file is part of Mentat system (https://mentat.cesnet.cz/). +# +# Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ This module contains custom item changelog search form for Hawat. """ +__author__ = "Jan Mach <jan.mach@cesnet.cz>" +__credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" import wtforms from wtforms.ext.sqlalchemy.fields import QuerySelectMultipleField @@ -16,18 +21,18 @@ import flask import flask_wtf from flask_babel import lazy_gettext -import vial.const -import vial.forms -import vial.db +import hawat.const +import hawat.forms +import hawat.db def get_item_operation_choices(): """ Return select choices for item changelog operations. """ - item_changelog_model = flask.current_app.get_model(vial.const.MODEL_ITEM_CHANGELOG) - operations_list = vial.db.db_query(item_changelog_model).\ - distinct(item_changelog_model.operation).\ + item_changelog_model = flask.current_app.get_model(hawat.const.MODEL_ITEM_CHANGELOG) + operations_list = hawat.db.db_query(item_changelog_model). \ + distinct(item_changelog_model.operation). \ all() return list( zip( @@ -36,13 +41,14 @@ def get_item_operation_choices(): ) ) + def get_item_model_choices(): """ Return select choices for item changelog item models. """ - item_changelog_model = flask.current_app.get_model(vial.const.MODEL_ITEM_CHANGELOG) - models_list = vial.db.db_query(item_changelog_model).\ - distinct(item_changelog_model.model).\ + item_changelog_model = flask.current_app.get_model(hawat.const.MODEL_ITEM_CHANGELOG) + models_list = hawat.db.db_query(item_changelog_model). \ + distinct(item_changelog_model.model). \ all() return list( zip( @@ -51,47 +57,48 @@ def get_item_model_choices(): ) ) -class ItemChangeLogSearchForm(vial.forms.BaseSearchForm): + +class ItemChangeLogSearchForm(hawat.forms.BaseSearchForm): """ Class representing item changelog search form. """ authors = QuerySelectMultipleField( lazy_gettext('Authors:'), - query_factory = vial.forms.get_available_users, - get_label = lambda x: '{} ({})'.format(x.fullname, x.login) + query_factory=hawat.forms.get_available_users, + get_label=lambda x: '{} ({})'.format(x.fullname, x.login) ) operations = wtforms.SelectMultipleField( lazy_gettext('Operations:'), - validators = [ + validators=[ wtforms.validators.Optional(), ], - filters = [lambda x: x or []] + filters=[lambda x: x or []] ) imodel = wtforms.SelectField( lazy_gettext('Item model:'), - validators = [ + validators=[ wtforms.validators.Optional(), ], - choices = [('', lazy_gettext('Nothing selected'))], - filters = [lambda x: x or None], - default = '' + choices=[('', lazy_gettext('Nothing selected'))], + filters=[lambda x: x or None], + default='' ) imodel_id = wtforms.IntegerField( lazy_gettext('Model ID:'), - validators = [ + validators=[ wtforms.validators.Optional(), ] ) - dt_from = vial.forms.SmartDateTimeField( + dt_from = hawat.forms.SmartDateTimeField( lazy_gettext('From:'), - validators = [ + validators=[ wtforms.validators.Optional() ], - default = lambda: vial.forms.default_dt_with_delta(vial.const.DEFAULT_RESULT_TIMEDELTA) + default=lambda: hawat.forms.default_dt_with_delta(hawat.const.DEFAULT_RESULT_TIMEDELTA) ) - dt_to = vial.forms.SmartDateTimeField( + dt_to = hawat.forms.SmartDateTimeField( lazy_gettext('To:'), - validators = [ + validators=[ wtforms.validators.Optional() ] ) @@ -105,10 +112,10 @@ class ItemChangeLogSearchForm(vial.forms.BaseSearchForm): @staticmethod def is_multivalue(field_name): """ - Check, if given form field is a multivalue field. + Check, if given form field is a multi-value field. - :param str field_name: Name of the form field. - :return: ``True``, if the field can contain multiple values, ``False`` otherwise. + :param str field_name: Name of the form field + :return: ``True``, if the field can contain multiple values, ``False`` otherwise :rtype: bool """ if field_name in ('authors', 'operations'): @@ -120,16 +127,16 @@ class ItemChangeLogDashboardForm(flask_wtf.FlaskForm): """ Class representing item changelog dashboard search form. """ - dt_from = vial.forms.SmartDateTimeField( + dt_from = hawat.forms.SmartDateTimeField( lazy_gettext('From:'), - validators = [ + validators=[ wtforms.validators.Optional() ], - default = lambda: vial.forms.default_dt_with_delta(vial.const.DEFAULT_RESULT_TIMEDELTA) + default=lambda: hawat.forms.default_dt_with_delta(hawat.const.DEFAULT_RESULT_TIMEDELTA) ) - dt_to = vial.forms.SmartDateTimeField( + dt_to = hawat.forms.SmartDateTimeField( lazy_gettext('To:'), - validators = [ + validators=[ wtforms.validators.Optional() ] ) diff --git a/lib/vial/blueprints/changelogs/templates/changelogs/search.html b/lib/hawat/blueprints/changelogs/templates/changelogs/search.html similarity index 97% rename from lib/vial/blueprints/changelogs/templates/changelogs/search.html rename to lib/hawat/blueprints/changelogs/templates/changelogs/search.html index 60b5b4918a2b0a731f304f73dba05cf6154eaf41..83ff8f70a4d7451e23825186807d9489452e2035 100644 --- a/lib/vial/blueprints/changelogs/templates/changelogs/search.html +++ b/lib/hawat/blueprints/changelogs/templates/changelogs/search.html @@ -31,6 +31,6 @@ {%- block contentsearchresult %} - {{ macros_page.render_changelog_records(items, vial_current_view.get_context_action_menu()) }} + {{ macros_page.render_changelog_records(items, hawat_current_view.get_context_action_menu()) }} {%- endblock contentsearchresult %} diff --git a/lib/vial/blueprints/changelogs/templates/changelogs/show.html b/lib/hawat/blueprints/changelogs/templates/changelogs/show.html similarity index 98% rename from lib/vial/blueprints/changelogs/templates/changelogs/show.html rename to lib/hawat/blueprints/changelogs/templates/changelogs/show.html index 3e806a029ffea5feb6c9206926b7da9fb35b8f3e..b32b940c37d0c9f885d577d008438b5e11307bf0 100644 --- a/lib/vial/blueprints/changelogs/templates/changelogs/show.html +++ b/lib/hawat/blueprints/changelogs/templates/changelogs/show.html @@ -6,7 +6,7 @@ <div class="col-lg-12"> {{ macros_page.render_breadcrumbs(item) }} - <h2>{{ vial_current_view.get_view_title() }}</h2> + <h2>{{ hawat_current_view.get_view_title() }}</h2> <hr> <h3>{{ item.__str__() }}</h3> <br> diff --git a/lib/hawat/blueprints/changelogs/test/__init__.py b/lib/hawat/blueprints/changelogs/test/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..5fcb2e3dd6a76b439d8f2f71c691208c2bf53bfb --- /dev/null +++ b/lib/hawat/blueprints/changelogs/test/__init__.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------- +# This file is part of Mentat system (https://mentat.cesnet.cz/). +# +# Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) +# Use of this source is governed by the MIT license, see LICENSE file. +# ------------------------------------------------------------------------------- + + +""" +Unit tests for :py:mod:`hawat.blueprints.changelogs`. +""" + +__author__ = "Jan Mach <jan.mach@cesnet.cz>" +__credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" + +import sys +import unittest + +import hawat.const +import hawat.test +import hawat.db +from hawat.test import HawatTestCase +from hawat.test.runner import TestRunnerMixin + + +_IS_NOSE = sys.argv[0].endswith('nosetests') + + +@unittest.skipIf(_IS_NOSE, "broken under nosetests") +class ChangeLogsSearchTestCase(TestRunnerMixin, HawatTestCase): + """Class for testing ``changelogs.search`` endpoint.""" + + def _attempt_fail(self): + self.assertGetURL( + '/changelogs/search', + 403 + ) + + def _attempt_succeed(self): + self.assertGetURL( + '/changelogs/search', + 200 + ) + + @hawat.test.do_as_user_decorator(hawat.const.ROLE_USER) + def test_01_as_user(self): + """Test access as user ``user``.""" + self._attempt_fail() + + @hawat.test.do_as_user_decorator(hawat.const.ROLE_DEVELOPER) + def test_02_as_developer(self): + """Test access as user ``developer``.""" + self._attempt_fail() + + @hawat.test.do_as_user_decorator(hawat.const.ROLE_MAINTAINER) + def test_03_as_maintainer(self): + """Test access as user ``maintainer``.""" + self._attempt_succeed() + + @hawat.test.do_as_user_decorator(hawat.const.ROLE_ADMIN) + def test_04_as_admin(self): + """Test access as user ``admin``.""" + self._attempt_succeed() + + +# ------------------------------------------------------------------------------- + + +if __name__ == "__main__": + unittest.main() diff --git a/lib/hawat/blueprints/dbstatus/__init__.py b/lib/hawat/blueprints/dbstatus/__init__.py index 068ae959849d70c4f1e66d985ce4add2093fbfee..d24fe95b8a0879b93fdea08f02795b792bb9d04a 100644 --- a/lib/hawat/blueprints/dbstatus/__init__.py +++ b/lib/hawat/blueprints/dbstatus/__init__.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ @@ -47,15 +47,14 @@ Provided endpoints """ - __author__ = "Jan Mach <jan.mach@cesnet.cz>" __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" - import sys import datetime import traceback +import markupsafe import werkzeug.routing import flask import flask_login @@ -66,16 +65,17 @@ import mentat.const import mentat.system from mentat.datatype.sqldb import UserModel, GroupModel, FilterModel, SettingsReportingModel -import vial.menu -import vial.acl -from vial.app import VialBlueprint -from vial.view import RenderableView, SimpleView -from vial.view.mixin import HTMLMixin, AJAXMixin, SQLAlchemyMixin -from vial.forms import ItemActionConfirmForm +import hawat.const +import hawat.menu +import hawat.acl +import hawat.db +from hawat.base import HawatBlueprint +from hawat.view import RenderableView, SimpleView +from hawat.view.mixin import HTMLMixin, AJAXMixin, SQLAlchemyMixin +from hawat.forms import ItemActionConfirmForm from hawat.base import PsycopgMixin from hawat.base import RE_UQUERY - BLUEPRINT_NAME = 'dbstatus' """Name of the blueprint as module global constant.""" @@ -86,7 +86,7 @@ class ViewView(HTMLMixin, PsycopgMixin, SimpleView): """ authentication = True - authorization = [vial.acl.PERMISSION_ADMIN] + authorization = [hawat.acl.PERMISSION_ADMIN] @classmethod def get_view_name(cls): @@ -116,15 +116,16 @@ class ViewView(HTMLMixin, PsycopgMixin, SimpleView): record['user_id'] = user_id record['query_id'] = query_id if user_id not in cache: - cache[user_id] = vial.db.db_get().session.query(UserModel).filter(UserModel.id == int(user_id)).one_or_none() + cache[user_id] = hawat.db.db_get().session.query(UserModel).filter( + UserModel.id == int(user_id)).one_or_none() record['user'] = cache[user_id] return result def do_before_response(self, **kwargs): self.response_context.update( - query_status_events = self._enrich_result_queries(self.get_db().queries_status()), - database_status_events = self.get_db().database_status(), - sw_versions = mentat.system.analyze_versions() + query_status_events=self._enrich_result_queries(self.get_db().queries_status()), + database_status_events=self.get_db().database_status(), + sw_versions=mentat.system.analyze_versions() ) dbstatistics_events = { @@ -142,17 +143,17 @@ class ViewView(HTMLMixin, PsycopgMixin, SimpleView): } } self.response_context.update( - database_statistics_events = dbstatistics_events + database_statistics_events=dbstatistics_events ) - action_menu = vial.menu.Menu() + action_menu = hawat.menu.Menu() action_menu.add_entry( 'endpoint', 'stop', - endpoint = 'dbstatus.query-stop', - hidetitle = True, - legend = lambda **x: lazy_gettext('Stop user query "%(item)s"', item = x['item']['query_name']), - cssclass = 'action-ajax' + endpoint='dbstatus.query-stop', + hidetitle=True, + legend=lambda **x: lazy_gettext('Stop user query "%(item)s"', item=x['item']['query_name']), + cssclass='action-ajax' ) self.response_context['context_action_menu_query'] = action_menu @@ -191,13 +192,14 @@ class MyQueriesView(HTMLMixin, PsycopgMixin, SimpleView): record['user_id'] = user_id record['query_id'] = query_id if user_id not in cache: - cache[user_id] = vial.db.db_get().session.query(UserModel).filter(UserModel.id == int(user_id)).one_or_none() + cache[user_id] = hawat.db.db_get().session.query(UserModel).filter( + UserModel.id == int(user_id)).one_or_none() record['user'] = cache[user_id] return result def do_before_response(self, **kwargs): self.response_context.update( - query_status_events = self._enrich_result_queries( + query_status_events=self._enrich_result_queries( self.get_db().queries_status( RE_UQUERY.format( int(flask_login.current_user.get_id()) @@ -206,14 +208,14 @@ class MyQueriesView(HTMLMixin, PsycopgMixin, SimpleView): ) ) - action_menu = vial.menu.Menu() + action_menu = hawat.menu.Menu() action_menu.add_entry( 'endpoint', 'stop', - endpoint = 'dbstatus.query-stop', - hidetitle = True, - legend = lambda **x: lazy_gettext('Stop user query "%(item)s"', item = x['item']['query_name']), - cssclass = 'action-ajax' + endpoint='dbstatus.query-stop', + hidetitle=True, + legend=lambda **x: lazy_gettext('Stop user query "%(item)s"', item=x['item']['query_name']), + cssclass='action-ajax' ) self.response_context['context_action_menu_query'] = action_menu @@ -244,7 +246,7 @@ class QueryStatusView(AJAXMixin, PsycopgMixin, RenderableView): def get_view_url(cls, **kwargs): return flask.url_for( cls.get_view_endpoint(), - item_id = kwargs['item']['query_name'] + item_id=kwargs['item']['query_name'] ) def do_before_response(self, **kwargs): @@ -253,9 +255,9 @@ class QueryStatusView(AJAXMixin, PsycopgMixin, RenderableView): self.abort(404) self.response_context.update( - user_id = kwargs['user_id'], - query_name = kwargs['item_id'], - query_status = query_status + user_id=kwargs['user_id'], + query_name=kwargs['item_id'], + query_status=query_status ) def dispatch_request(self, item_id): # pylint: disable=locally-disabled,arguments-differ @@ -264,13 +266,13 @@ class QueryStatusView(AJAXMixin, PsycopgMixin, RenderableView): Will be called by the **Flask** framework to service the request. """ user_id, _ = self.parse_qname(item_id) - if flask_login.current_user.get_id() != user_id and not vial.acl.PERMISSION_POWER.can(): + if flask_login.current_user.get_id() != user_id and not hawat.acl.PERMISSION_POWER.can(): self.abort( 403, gettext('You are not allowed to view status of this query.') ) - self.do_before_response(item_id = item_id, user_id = user_id) + self.do_before_response(item_id=item_id, user_id=user_id) return self.generate_response() @@ -278,7 +280,7 @@ class AbstractQueryStopView(PsycopgMixin, RenderableView): # pylint: disable=lo """ Application view providing ability to stop given query. """ - methods = ['GET','POST'] + methods = ['GET', 'POST'] authentication = True @@ -298,33 +300,33 @@ class AbstractQueryStopView(PsycopgMixin, RenderableView): # pylint: disable=lo def get_view_url(cls, **kwargs): return flask.url_for( cls.get_view_endpoint(), - item_id = kwargs['item']['query_name'] + item_id=kwargs['item']['query_name'] ) @classmethod def authorize_item_action(cls, **kwargs): user_id, _ = cls.parse_qname(kwargs['item']['query_name']) - return vial.acl.PERMISSION_POWER.can() or flask_login.current_user.get_id() == user_id + return hawat.acl.PERMISSION_POWER.can() or flask_login.current_user.get_id() == user_id @staticmethod def get_message_success(**kwargs): return gettext( 'Query <strong>%(item_id)s</strong> was successfully stopped.', - item_id = flask.escape(str(kwargs['item']['query_name'])) + item_id=markupsafe.escape(str(kwargs['item']['query_name'])) ) @staticmethod def get_message_failure(**kwargs): return gettext( 'Unable to stop query <strong>%(item_id)s</strong>.', - item_id = flask.escape(str(kwargs['item']['query_name'])) + item_id=markupsafe.escape(str(kwargs['item']['query_name'])) ) @staticmethod def get_message_cancel(**kwargs): return gettext( 'Canceled stopping query <strong>%(item_id)s</strong>.', - item_id = flask.escape(str(kwargs['item']['query_name'])) + item_id=markupsafe.escape(str(kwargs['item']['query_name'])) ) def get_url_next(self): @@ -341,13 +343,13 @@ class AbstractQueryStopView(PsycopgMixin, RenderableView): # pylint: disable=lo """ Check the form for *cancel* button press and cancel the action. """ - if hasattr(form, vial.const.FORM_ACTION_CANCEL): - if getattr(form, vial.const.FORM_ACTION_CANCEL).data: + if hasattr(form, hawat.const.FORM_ACTION_CANCEL): + if getattr(form, hawat.const.FORM_ACTION_CANCEL).data: self.flash( flask.Markup(self.get_message_cancel(**kwargs)), - vial.const.FLASH_INFO + hawat.const.FLASH_INFO ) - return self.redirect(default_url = self.get_url_next()) + return self.redirect(default_url=self.get_url_next()) return None @@ -360,57 +362,57 @@ class AbstractQueryStopView(PsycopgMixin, RenderableView): # pylint: disable=lo if not item: self.abort(404) - if not self.authorize_item_action(item = item): + if not self.authorize_item_action(item=item): self.abort(403) form = ItemActionConfirmForm() - cancel_response = self.check_action_cancel(form, item = item) + cancel_response = self.check_action_cancel(form, item=item) if cancel_response: return cancel_response if form.validate_on_submit(): form_data = form.data - if form_data[vial.const.FORM_ACTION_SUBMIT]: + if form_data[hawat.const.FORM_ACTION_SUBMIT]: try: action_status = self.get_db().query_cancel(item_id) if action_status: self.flash( - flask.Markup(self.get_message_success(item = item)), - vial.const.FLASH_SUCCESS + flask.Markup(self.get_message_success(item=item)), + hawat.const.FLASH_SUCCESS ) else: self.flash( - flask.Markup(self.get_message_failure(item = item)), - vial.const.FLASH_FAILURE + flask.Markup(self.get_message_failure(item=item)), + hawat.const.FLASH_FAILURE ) self.get_db().commit() - return self.redirect(default_url = self.get_url_next()) + return self.redirect(default_url=self.get_url_next()) except Exception: # pylint: disable=locally-disabled,broad-except self.get_db().commit() self.flash( - flask.Markup(self.get_message_failure(item = item)), - vial.const.FLASH_FAILURE + flask.Markup(self.get_message_failure(item=item)), + hawat.const.FLASH_FAILURE ) flask.current_app.log_exception_with_label( traceback.TracebackException(*sys.exc_info()), - self.get_message_failure(item = item) + self.get_message_failure(item=item) ) - return self.redirect(default_url = self.get_url_next()) + return self.redirect(default_url=self.get_url_next()) self.response_context.update( - confirm_form = form, - confirm_url = flask.url_for( + confirm_form=form, + confirm_url=flask.url_for( '{}.{}'.format( self.module_name, self.get_view_name() ), - item_id = item_id + item_id=item_id ), - item_id = item_id, - item = item + item_id=item_id, + item=item ) self.do_before_response() @@ -421,6 +423,7 @@ class QueryStopView(HTMLMixin, AbstractQueryStopView): """ Application view providing ability to stop given query. """ + @classmethod def get_view_name(cls): return 'query-stop' @@ -434,6 +437,7 @@ class ApiQueryStopView(AJAXMixin, AbstractQueryStopView): """ Application view providing ability to stop given query. """ + @classmethod def get_view_name(cls): return 'api-query-stop' @@ -445,7 +449,7 @@ class DashboardView(HTMLMixin, SQLAlchemyMixin, SimpleView): # pylint: disable= """ authentication = True - authorization = [vial.acl.PERMISSION_POWER] + authorization = [hawat.acl.PERMISSION_POWER] @classmethod def get_view_name(cls): @@ -463,206 +467,217 @@ class DashboardView(HTMLMixin, SQLAlchemyMixin, SimpleView): # pylint: disable= def get_view_template(cls): return '{}/dashboard.html'.format(cls.module_name) - #--------------------------------------------------------------------------- - + # --------------------------------------------------------------------------- def do_before_response(self, **kwargs): - """*Implementation* of :py:func:`vial.view.RenderableView.do_before_response`.""" - self.response_context['users_disabled'] = self.dbquery(UserModel).\ - filter(UserModel.enabled == False).\ - order_by(UserModel.createtime.desc()).\ + """*Implementation* of :py:func:`hawat.view.RenderableView.do_before_response`.""" + self.response_context['users_disabled'] = self.dbquery(UserModel). \ + filter(UserModel.enabled == False). \ + order_by(UserModel.createtime.desc()). \ all() - self.response_context['users_nomemberships'] = self.dbquery(UserModel).\ - filter(~UserModel.memberships.any()).\ - order_by(UserModel.createtime.desc()).\ + self.response_context['users_nomemberships'] = self.dbquery(UserModel). \ + filter(~UserModel.memberships.any()). \ + order_by(UserModel.createtime.desc()). \ all() - self.response_context['users_noroles'] = self.dbquery(UserModel).\ - filter(UserModel.roles == []).\ - order_by(UserModel.createtime.desc()).\ + self.response_context['users_noroles'] = self.dbquery(UserModel). \ + filter(UserModel.roles == []). \ + order_by(UserModel.createtime.desc()). \ all() - self.response_context['users_nologin'] = self.dbquery(UserModel).\ + self.response_context['users_nologin'] = self.dbquery(UserModel). \ filter( - or_( - UserModel.logintime.is_(None), - UserModel.logintime <= (datetime.datetime.utcnow() - datetime.timedelta(days = 365)) - ) - ).\ - order_by(UserModel.createtime.desc()).\ + or_( + UserModel.logintime.is_(None), + UserModel.logintime <= (datetime.datetime.utcnow() - datetime.timedelta(days=365)) + ) + ). \ + order_by(UserModel.createtime.desc()). \ all() - self.response_context['groups_disabled'] = self.dbquery(GroupModel).\ - filter(GroupModel.enabled == False).\ - order_by(GroupModel.createtime.desc()).\ + self.response_context['groups_disabled'] = self.dbquery(GroupModel). \ + filter(GroupModel.enabled == False). \ + order_by(GroupModel.createtime.desc()). \ all() - self.response_context['groups_nomembers'] = self.dbquery(GroupModel).\ - filter(~GroupModel.members.any()).\ - order_by(GroupModel.createtime.desc()).\ + self.response_context['groups_nomembers'] = self.dbquery(GroupModel). \ + filter(~GroupModel.members.any()). \ + order_by(GroupModel.createtime.desc()). \ all() - self.response_context['groups_nomanagers'] = self.dbquery(GroupModel).\ - filter(~GroupModel.members.any()).\ - order_by(GroupModel.createtime.desc()).\ + self.response_context['groups_nomanagers'] = self.dbquery(GroupModel). \ + filter(~GroupModel.members.any()). \ + order_by(GroupModel.createtime.desc()). \ all() - self.response_context['groups_nonetworks'] = self.dbquery(GroupModel).\ - filter(~GroupModel.networks.any()).\ - order_by(GroupModel.createtime.desc()).\ + self.response_context['groups_nonetworks'] = self.dbquery(GroupModel). \ + filter(~GroupModel.networks.any()). \ + order_by(GroupModel.createtime.desc()). \ all() - self.response_context['filters_disabled'] = self.dbquery(FilterModel).\ - filter(FilterModel.enabled == False).\ - order_by(FilterModel.createtime.desc()).\ + self.response_context['filters_disabled'] = self.dbquery(FilterModel). \ + filter(FilterModel.enabled == False). \ + order_by(FilterModel.createtime.desc()). \ all() - self.response_context['filters_nohits'] = self.dbquery(FilterModel).\ - filter(FilterModel.hits == 0).\ - order_by(FilterModel.createtime.desc()).\ + self.response_context['filters_nohits'] = self.dbquery(FilterModel). \ + filter(FilterModel.hits == 0). \ + order_by(FilterModel.createtime.desc()). \ all() - self.response_context['filters_future'] = self.dbquery(FilterModel).\ - filter(FilterModel.valid_from > (datetime.datetime.utcnow() + datetime.timedelta(days = 14))).\ - order_by(FilterModel.createtime.desc()).\ + self.response_context['filters_future'] = self.dbquery(FilterModel). \ + filter(FilterModel.valid_from > (datetime.datetime.utcnow() + datetime.timedelta(days=14))). \ + order_by(FilterModel.createtime.desc()). \ all() - self.response_context['filters_expired'] = self.dbquery(FilterModel).\ - filter(FilterModel.valid_to < datetime.datetime.utcnow()).\ - order_by(FilterModel.createtime.desc()).\ + self.response_context['filters_expired'] = self.dbquery(FilterModel). \ + filter(FilterModel.valid_to < datetime.datetime.utcnow()). \ + order_by(FilterModel.createtime.desc()). \ all() - self.response_context['settingsrep_redirected'] = self.dbquery(SettingsReportingModel).\ - filter(SettingsReportingModel.redirect == True).\ - order_by(SettingsReportingModel.createtime.desc()).\ + self.response_context['settingsrep_redirected'] = self.dbquery(SettingsReportingModel). \ + filter(SettingsReportingModel.redirect == True). \ + order_by(SettingsReportingModel.createtime.desc()). \ all() - self.response_context['settingsrep_modenone'] = self.dbquery(SettingsReportingModel).\ - filter(SettingsReportingModel.mode == mentat.const.REPORTING_MODE_NONE).\ - order_by(SettingsReportingModel.createtime.desc()).\ + self.response_context['settingsrep_modenone'] = self.dbquery(SettingsReportingModel). \ + filter(SettingsReportingModel.mode == mentat.const.REPORTING_MODE_NONE). \ + order_by(SettingsReportingModel.createtime.desc()). \ all() - self.response_context['settingsrep_emailscust'] = self.dbquery(SettingsReportingModel).\ + self.response_context['settingsrep_emailscust'] = self.dbquery(SettingsReportingModel). \ filter(SettingsReportingModel.emails_low != [] or SettingsReportingModel.emails_medium != [] or - SettingsReportingModel.emails_high != [] or SettingsReportingModel.emails_critical != []).\ - order_by(SettingsReportingModel.createtime.desc()).\ + SettingsReportingModel.emails_high != [] or SettingsReportingModel.emails_critical != []). \ + order_by(SettingsReportingModel.createtime.desc()). \ all() - action_menu = vial.menu.Menu() + action_menu = hawat.menu.Menu() action_menu.add_entry( 'endpoint', 'show', - endpoint = 'users.show', - hidetitle = True, - legend = lambda **x: lazy_gettext('View details of user account "%(item)s"', item = flask.escape(x['item'].login)) + endpoint='users.show', + hidetitle=True, + legend=lambda **x: lazy_gettext('View details of user account "%(item)s"', + item=markupsafe.escape(x['item'].login)) ) action_menu.add_entry( 'submenu', 'more', - legend = lazy_gettext('More actions') + legend=lazy_gettext('More actions') ) action_menu.add_entry( 'endpoint', 'more.update', - endpoint = 'users.update', - legend = lambda **x: lazy_gettext('Update details of user account "%(item)s"', item = flask.escape(x['item'].login)) + endpoint='users.update', + legend=lambda **x: lazy_gettext('Update details of user account "%(item)s"', + item=markupsafe.escape(x['item'].login)) ) action_menu.add_entry( 'endpoint', 'more.disable', - endpoint = 'users.disable', - icon = 'action-disable-user', - legend = lambda **x: lazy_gettext('Disable user account "%(item)s"', item = flask.escape(x['item'].login)) + endpoint='users.disable', + icon='action-disable-user', + legend=lambda **x: lazy_gettext('Disable user account "%(item)s"', + item=markupsafe.escape(x['item'].login)) ) action_menu.add_entry( 'endpoint', 'more.enable', - endpoint = 'users.enable', - icon = 'action-enable-user', - legend = lambda **x: lazy_gettext('Enable user account "%(item)s"', item = flask.escape(x['item'].login)) + endpoint='users.enable', + icon='action-enable-user', + legend=lambda **x: lazy_gettext('Enable user account "%(item)s"', + item=markupsafe.escape(x['item'].login)) ) action_menu.add_entry( 'endpoint', 'more.delete', - endpoint = 'users.delete', - icon = 'action-delete-user', - legend = lambda **x: lazy_gettext('Delete user account "%(item)s"', item = flask.escape(x['item'].login)) + endpoint='users.delete', + icon='action-delete-user', + legend=lambda **x: lazy_gettext('Delete user account "%(item)s"', + item=markupsafe.escape(x['item'].login)) ) self.response_context['context_action_menu_user'] = action_menu - action_menu = vial.menu.Menu() + action_menu = hawat.menu.Menu() action_menu.add_entry( 'endpoint', 'show', - endpoint = 'groups.show', - hidetitle = True, - legend = lambda **x: lazy_gettext('View details of group "%(item)s"', item = flask.escape(str(x['item']))) + endpoint='groups.show', + hidetitle=True, + legend=lambda **x: lazy_gettext('View details of group "%(item)s"', + item=markupsafe.escape(str(x['item']))) ) action_menu.add_entry( 'submenu', 'more', - legend = lazy_gettext('More actions') + legend=lazy_gettext('More actions') ) action_menu.add_entry( 'endpoint', 'more.update', - endpoint = 'groups.update', - legend = lambda **x: lazy_gettext('Update details of group "%(item)s"', item = flask.escape(str(x['item']))) + endpoint='groups.update', + legend=lambda **x: lazy_gettext('Update details of group "%(item)s"', + item=markupsafe.escape(str(x['item']))) ) action_menu.add_entry( 'endpoint', 'more.disable', - endpoint = 'groups.disable', - legend = lambda **x: lazy_gettext('Disable group "%(item)s"', item = flask.escape(str(x['item']))) + endpoint='groups.disable', + legend=lambda **x: lazy_gettext('Disable group "%(item)s"', item=markupsafe.escape(str(x['item']))) ) action_menu.add_entry( 'endpoint', 'more.enable', - endpoint = 'groups.enable', - legend = lambda **x: lazy_gettext('Enable group "%(item)s"', item = flask.escape(str(x['item']))) + endpoint='groups.enable', + legend=lambda **x: lazy_gettext('Enable group "%(item)s"', item=markupsafe.escape(str(x['item']))) ) action_menu.add_entry( 'endpoint', 'more.delete', - endpoint = 'groups.delete', - legend = lambda **x: lazy_gettext('Delete group "%(item)s"', item = flask.escape(str(x['item']))) + endpoint='groups.delete', + legend=lambda **x: lazy_gettext('Delete group "%(item)s"', item=markupsafe.escape(str(x['item']))) ) self.response_context['context_action_menu_group'] = action_menu - action_menu = vial.menu.Menu() + action_menu = hawat.menu.Menu() action_menu.add_entry( 'endpoint', 'show', - endpoint = 'filters.show', - hidetitle = True, - legend = lambda **x: lazy_gettext('View details of reporting filter "%(item)s"', item = flask.escape(x['item'].name)) + endpoint='filters.show', + hidetitle=True, + legend=lambda **x: lazy_gettext('View details of reporting filter "%(item)s"', + item=markupsafe.escape(x['item'].name)) ) action_menu.add_entry( 'submenu', 'more', - legend = lazy_gettext('More actions') + legend=lazy_gettext('More actions') ) action_menu.add_entry( 'endpoint', 'more.update', - endpoint = 'filters.update', - legend = lambda **x: lazy_gettext('Update details of reporting filter "%(item)s"', item = flask.escape(x['item'].name)) + endpoint='filters.update', + legend=lambda **x: lazy_gettext('Update details of reporting filter "%(item)s"', + item=markupsafe.escape(x['item'].name)) ) action_menu.add_entry( 'endpoint', 'more.disable', - endpoint = 'filters.disable', - legend = lambda **x: lazy_gettext('Disable reporting filter "%(item)s"', item = flask.escape(x['item'].name)) + endpoint='filters.disable', + legend=lambda **x: lazy_gettext('Disable reporting filter "%(item)s"', + item=markupsafe.escape(x['item'].name)) ) action_menu.add_entry( 'endpoint', 'more.enable', - endpoint = 'filters.enable', - legend = lambda **x: lazy_gettext('Enable reporting filter "%(item)s"', item = flask.escape(x['item'].name)) + endpoint='filters.enable', + legend=lambda **x: lazy_gettext('Enable reporting filter "%(item)s"', + item=markupsafe.escape(x['item'].name)) ) action_menu.add_entry( 'endpoint', 'more.delete', - endpoint = 'filters.delete', - legend = lambda **x: lazy_gettext('Delete reporting filter "%(item)s"', item = flask.escape(x['item'].name)) + endpoint='filters.delete', + legend=lambda **x: lazy_gettext('Delete reporting filter "%(item)s"', + item=markupsafe.escape(x['item'].name)) ) self.response_context['context_action_menu_filter'] = action_menu -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- -class DatabaseStatusBlueprint(VialBlueprint): +class DatabaseStatusBlueprint(HawatBlueprint): """Pluggable module - database status overview (*dbstatus*).""" @classmethod @@ -673,44 +688,44 @@ class DatabaseStatusBlueprint(VialBlueprint): app.menu_main.add_entry( 'view', 'dashboards.dbstatus', - position = 100, - view = DashboardView + position=100, + view=DashboardView ) app.menu_main.add_entry( 'view', 'admin.{}'.format(BLUEPRINT_NAME), - position = 30, - view = ViewView + position=30, + view=ViewView ) app.menu_auth.add_entry( 'view', 'queries_my', - position = 50, - view = MyQueriesView + position=50, + view=MyQueriesView ) -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- def get_blueprint(): """ - Mandatory interface for :py:mod:`vial.Vial` and factory function. This function - must return a valid instance of :py:class:`vial.app.VialBlueprint` or + Mandatory interface for :py:mod:`hawat.Hawat` and factory function. This function + must return a valid instance of :py:class:`hawat.app.HawatBlueprint` or :py:class:`flask.Blueprint`. """ hbp = DatabaseStatusBlueprint( BLUEPRINT_NAME, __name__, - template_folder = 'templates' + template_folder='templates' ) - hbp.register_view_class(ViewView, '/{}/view'.format(BLUEPRINT_NAME)) - hbp.register_view_class(MyQueriesView, '/{}/query/my'.format(BLUEPRINT_NAME)) - hbp.register_view_class(DashboardView, '/{}/dashboard'.format(BLUEPRINT_NAME)) - hbp.register_view_class(QueryStatusView, '/api/{}/query/<item_id>/status'.format(BLUEPRINT_NAME)) - hbp.register_view_class(QueryStopView, '/{}/query/<item_id>/stop'.format(BLUEPRINT_NAME)) + hbp.register_view_class(ViewView, '/{}/view'.format(BLUEPRINT_NAME)) + hbp.register_view_class(MyQueriesView, '/{}/query/my'.format(BLUEPRINT_NAME)) + hbp.register_view_class(DashboardView, '/{}/dashboard'.format(BLUEPRINT_NAME)) + hbp.register_view_class(QueryStatusView, '/api/{}/query/<item_id>/status'.format(BLUEPRINT_NAME)) + hbp.register_view_class(QueryStopView, '/{}/query/<item_id>/stop'.format(BLUEPRINT_NAME)) hbp.register_view_class(ApiQueryStopView, '/api/{}/query/<item_id>/stop'.format(BLUEPRINT_NAME)) return hbp diff --git a/lib/hawat/blueprints/dbstatus/templates/dbstatus/queries_my.html b/lib/hawat/blueprints/dbstatus/templates/dbstatus/queries_my.html index ecae88fe3b516ee32f9053d2f059a0f17d577b16..14e2269506dd38d9f6290c9f5fd4eb309ba8f6e8 100644 --- a/lib/hawat/blueprints/dbstatus/templates/dbstatus/queries_my.html +++ b/lib/hawat/blueprints/dbstatus/templates/dbstatus/queries_my.html @@ -6,9 +6,9 @@ <div class="col-lg-12"> <ol class="breadcrumb"> <li><a href="{{ url_for('home.index') }}">{{ _('Home') }}</a></li> - <li class="active">{{ vial_current_view.get_menu_title() }}</li> + <li class="active">{{ hawat_current_view.get_menu_title() }}</li> </ol> - <h2>{{ vial_current_view.get_view_title() }}</h2> + <h2>{{ hawat_current_view.get_view_title() }}</h2> <hr> </div> <!-- /.col-lg-12 --> diff --git a/lib/hawat/blueprints/dbstatus/templates/dbstatus/view.html b/lib/hawat/blueprints/dbstatus/templates/dbstatus/view.html index ab177d77f0e097f4783db707766ff76197b76d53..62d4383fa1746a0754ca84ffc14fcea7116d0351 100644 --- a/lib/hawat/blueprints/dbstatus/templates/dbstatus/view.html +++ b/lib/hawat/blueprints/dbstatus/templates/dbstatus/view.html @@ -8,7 +8,7 @@ <li><a href="{{ url_for('home.index') }}">{{ _('Home') }}</a></li> <li class="active">{{ _('Database status') }}</li> </ol> - <h2>{{ vial_current_view.get_view_title() }}</h2> + <h2>{{ hawat_current_view.get_view_title() }}</h2> <hr> </div> <!-- /.col-lg-12 --> diff --git a/lib/hawat/blueprints/dbstatus/test/__init__.py b/lib/hawat/blueprints/dbstatus/test/__init__.py index ec11876009abc552325df9fccf7305944c224b93..8d1a80f1139e1df336336750341d06f72ebdd8d3 100644 --- a/lib/hawat/blueprints/dbstatus/test/__init__.py +++ b/lib/hawat/blueprints/dbstatus/test/__init__.py @@ -15,14 +15,14 @@ Unit tests for :py:mod:`hawat.blueprints.dbstatus`. import unittest -import vial.const -import vial.test -import vial.db -from vial.test import VialTestCase +import hawat.const +import hawat.test +import hawat.db +from hawat.test import HawatTestCase from hawat.test.runner import TestRunnerMixin -class DBStatusViewTestCase(TestRunnerMixin, VialTestCase): +class DBStatusViewTestCase(TestRunnerMixin, HawatTestCase): """Class for testing ``dbstatus.view`` endpoint.""" def _attempt_fail_redirect(self): @@ -57,28 +57,28 @@ class DBStatusViewTestCase(TestRunnerMixin, VialTestCase): """Test access as anonymous user.""" self._attempt_fail_redirect() - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_USER) def test_02_as_user(self): """Test access as user ``user``.""" self._attempt_fail() - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_DEVELOPER) def test_03_as_developer(self): """Test access as user ``developer``.""" self._attempt_fail() - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_MAINTAINER) def test_04_as_maintainer(self): """Test access as user ``maintainer``.""" self._attempt_fail() - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_ADMIN) def test_05_as_admin(self): """Test access as user ``admin``.""" self._attempt_succeed() -class DBStatusMyQueriesTestCase(TestRunnerMixin, VialTestCase): +class DBStatusMyQueriesTestCase(TestRunnerMixin, HawatTestCase): """Class for testing ``dbstatus.queries_my`` endpoint.""" def _attempt_fail(self): @@ -105,28 +105,28 @@ class DBStatusMyQueriesTestCase(TestRunnerMixin, VialTestCase): """Test access as anonymous user.""" self._attempt_fail() - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_USER) def test_02_as_user(self): """Test access as user ``user``.""" self._attempt_succeed() - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_DEVELOPER) def test_03_as_developer(self): """Test access as user ``developer``.""" self._attempt_succeed() - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_MAINTAINER) def test_04_as_maintainer(self): """Test access as user ``maintainer``.""" self._attempt_succeed() - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_ADMIN) def test_05_as_admin(self): """Test access as user ``admin``.""" self._attempt_succeed() -class DBStatusDashboardTestCase(TestRunnerMixin, VialTestCase): +class DBStatusDashboardTestCase(TestRunnerMixin, HawatTestCase): """Class for testing ``dbstatus.dashboard`` endpoint.""" def _attempt_fail_redirect(self): @@ -159,22 +159,22 @@ class DBStatusDashboardTestCase(TestRunnerMixin, VialTestCase): """Test access as anonymous user.""" self._attempt_fail_redirect() - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_USER) def test_02_as_user(self): """Test access as user ``user``.""" self._attempt_fail() - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_DEVELOPER) def test_03_as_developer(self): """Test access as user ``developer``.""" self._attempt_fail() - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_MAINTAINER) def test_04_as_maintainer(self): """Test access as user ``maintainer``.""" self._attempt_succeed() - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_ADMIN) def test_05_as_admin(self): """Test access as user ``admin``.""" self._attempt_succeed() diff --git a/lib/vial/blueprints/design_bs3/__init__.py b/lib/hawat/blueprints/design_bs3/__init__.py similarity index 54% rename from lib/vial/blueprints/design_bs3/__init__.py rename to lib/hawat/blueprints/design_bs3/__init__.py index d76ceebec9f81f23057d6ef4319325c4f96d397d..49ff40b2ee5c2cacbb195e71410a0847559b0ecd 100644 --- a/lib/vial/blueprints/design_bs3/__init__.py +++ b/lib/hawat/blueprints/design_bs3/__init__.py @@ -1,19 +1,22 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- +# This file is part of Mentat system (https://mentat.cesnet.cz/). +# +# Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ -This pluggable module provides default application desing and style. Currently +This pluggable module provides default application design and style. Currently, there are no views provided by this module. .. note:: To completely change the design of the whole application you can implement - your own custom _design_ module and replace this one. However this requires + your own custom _design_ module and replace this one. However, this requires that you thoroughly study the design of this module and provide your own implementation for all API hooks, otherwise you may break the whole application. @@ -31,11 +34,12 @@ Module content """ +__author__ = "Jan Mach <jan.mach@cesnet.cz>" +__credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" from flask_babel import lazy_gettext -from vial.app import VialBlueprint - +from hawat.base import HawatBlueprint # # Name of the blueprint as module global constant. @@ -43,31 +47,32 @@ from vial.app import VialBlueprint BLUEPRINT_NAME = 'design' -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- -class DesignBlueprint(VialBlueprint): +class DesignBlueprint(HawatBlueprint): """Pluggable module - application design and style (*design*).""" @classmethod def get_module_title(cls): return lazy_gettext('Application design and style template') -#------------------------------------------------------------------------------- + +# ------------------------------------------------------------------------------- def get_blueprint(): """ - Mandatory interface for :py:mod:`vial.Vial` and factory function. This function - must return a valid instance of :py:class:`vial.app.VialBlueprint` or + Mandatory interface for :py:mod:`hawat.Hawat` and factory function. This function + must return a valid instance of :py:class:`hawat.app.HawatBlueprint` or :py:class:`flask.Blueprint`. """ hbp = DesignBlueprint( BLUEPRINT_NAME, __name__, - template_folder = 'templates', - static_folder = 'static', - static_url_path = '/static/design') + template_folder='templates', + static_folder='static', + static_url_path='/static/design') return hbp diff --git a/lib/vial/blueprints/design_bs3/templates/_layout.html b/lib/hawat/blueprints/design_bs3/templates/_layout.html similarity index 98% rename from lib/vial/blueprints/design_bs3/templates/_layout.html rename to lib/hawat/blueprints/design_bs3/templates/_layout.html index 2ce02ceeffb5253d98b050fd3ac13c0c28be8db4..9feddebdf30022b6461b92fbf2a80d3e7ca231e5 100644 --- a/lib/vial/blueprints/design_bs3/templates/_layout.html +++ b/lib/hawat/blueprints/design_bs3/templates/_layout.html @@ -184,7 +184,7 @@ -----------------------------------------------------------------------> <link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}"> - <title>{% block title %}{{ vial_current_view.get_view_title() }}{% endblock %} - Mentat</title> + <title>{% block title %}{{ hawat_current_view.get_view_title() }}{% endblock %} - Mentat</title> {%- endblock head %} </head> @@ -216,7 +216,7 @@ </button> <a class="navbar-brand" href="{{ url_for(config['ENDPOINT_HOME']) }}"> <img src="{{ url_for('static', filename='images/app-logo-mini.png') }}" class="mentat-logo-mini" alt="Mentat logo"> - {{ vial_appname }} + {{ hawat_appname }} </a> </div> <!-- #navbar-header --> <div id="navbar" class="navbar-collapse collapse"> @@ -224,7 +224,7 @@ <ul class="nav navbar-nav navbar-right"> {{ macros_site.render_submenu_auth() }} {{ macros_site.render_locale_switcher() }} - {%- if vial_current_view.has_help %} + {%- if hawat_current_view.has_help %} <li> {{ macros_page.render_help_link(icon = 'help', text = '') }} </li> @@ -266,7 +266,7 @@ <span data-toggle="tooltip" title="{{ _('Page generated in: ') }}">{{ get_icon('stopwatch') }}</span><span class="hidden-sm"> <em>{{ _('Page generated in: ') }}</em></span> {{ get_timedelta(g.requeststart) }} <br> <span data-toggle="tooltip" title="{{ hawat_bversion_full }}"> - {{ vial_appname }} {{ hawat_version }} + {{ hawat_appname }} {{ hawat_version }} </span> | © {{ _('since') }} 2011 | <a data-toggle="tooltip" title="Go to official web page of CESNET-CERTS, the CSIRT for CESNET2 network" href="http://csirt.cesnet.cz"> diff --git a/lib/vial/blueprints/design_bs3/templates/_layout_confirmation.html b/lib/hawat/blueprints/design_bs3/templates/_layout_confirmation.html similarity index 97% rename from lib/vial/blueprints/design_bs3/templates/_layout_confirmation.html rename to lib/hawat/blueprints/design_bs3/templates/_layout_confirmation.html index a4ad2bef9693f413c023f27d275661d2049b66c2..ea75314d7185066d03dc4e68e94230395dab5748 100644 --- a/lib/vial/blueprints/design_bs3/templates/_layout_confirmation.html +++ b/lib/hawat/blueprints/design_bs3/templates/_layout_confirmation.html @@ -11,7 +11,7 @@ <div class="modal-header"> <h4 class="modal-title"> - {{ get_icon('modal-question') }} {{ vial_current_view.get_view_title() }} + {{ get_icon('modal-question') }} {{ hawat_current_view.get_view_title() }} </h4> </div> diff --git a/lib/vial/blueprints/design_bs3/templates/_layout_creatupdate.html b/lib/hawat/blueprints/design_bs3/templates/_layout_creatupdate.html similarity index 92% rename from lib/vial/blueprints/design_bs3/templates/_layout_creatupdate.html rename to lib/hawat/blueprints/design_bs3/templates/_layout_creatupdate.html index d9f45dcbc41006ebed8a4eb4cad231b32e8cb622..7722837505db9401c45dd83686f0df3e79b9bfc3 100644 --- a/lib/vial/blueprints/design_bs3/templates/_layout_creatupdate.html +++ b/lib/hawat/blueprints/design_bs3/templates/_layout_creatupdate.html @@ -19,7 +19,7 @@ <form method="POST" action="{{ form_url }}" id="form-{{ item_type }}-{{ item_action }}"> <fieldset> - <legend>{{ vial_current_view.get_view_title() }}</legend> + <legend>{{ hawat_current_view.get_view_title() }}</legend> {%- block itemform_fields %}{% endblock itemform_fields %} @@ -35,7 +35,6 @@ <div class="btn-toolbar" role="toolbar" aria-label="{{ _('Form submission buttons') }}"> <div class="btn-group" role="group"> {%- block itemform_buttons %} - {{ form.cancel(class_='btn btn-default') }} {{ form.submit(class_='btn btn-primary') }} {% endblock itemform_buttons %} </div> diff --git a/lib/vial/blueprints/design_bs3/templates/_layout_events_search.html b/lib/hawat/blueprints/design_bs3/templates/_layout_events_search.html similarity index 99% rename from lib/vial/blueprints/design_bs3/templates/_layout_events_search.html rename to lib/hawat/blueprints/design_bs3/templates/_layout_events_search.html index ea69c603e362480e54bca8acd8db4916e5fb5a95..8cbcd99958615ef0a4e559c5829500e2ee8721ce 100644 --- a/lib/vial/blueprints/design_bs3/templates/_layout_events_search.html +++ b/lib/hawat/blueprints/design_bs3/templates/_layout_events_search.html @@ -8,7 +8,7 @@ <!-- Search form - BEGIN ----------------------------------> <div class="jumbotron" style="margin-top: 1em;"> - <h2>{{ vial_current_view.get_view_title() }}</h2> + <h2>{{ hawat_current_view.get_view_title() }}</h2> <hr> <form method="GET" class="form-horizontal" id="form-events-simple" action="{{ url_for(request.endpoint) }}"> <div> diff --git a/lib/vial/blueprints/design_bs3/templates/_layout_list.html b/lib/hawat/blueprints/design_bs3/templates/_layout_list.html similarity index 96% rename from lib/vial/blueprints/design_bs3/templates/_layout_list.html rename to lib/hawat/blueprints/design_bs3/templates/_layout_list.html index ba9f3b1c4353ca04adfc2c79116d5360f1890978..137b5880e520463f66ca715461de9cefc0d0b8ec 100644 --- a/lib/vial/blueprints/design_bs3/templates/_layout_list.html +++ b/lib/hawat/blueprints/design_bs3/templates/_layout_list.html @@ -7,7 +7,7 @@ {{ macros_page.render_breadcrumbs() }} - <h2>{{ vial_current_view.get_view_title() }}</h2> + <h2>{{ hawat_current_view.get_view_title() }}</h2> <div class="pull-right"> {{ macros_page.render_menu_actions() }} </div> diff --git a/lib/vial/blueprints/design_bs3/templates/_layout_login.html b/lib/hawat/blueprints/design_bs3/templates/_layout_login.html similarity index 87% rename from lib/vial/blueprints/design_bs3/templates/_layout_login.html rename to lib/hawat/blueprints/design_bs3/templates/_layout_login.html index bbe4f9269becd50f24ed24b37f8b5a480b0eee6c..f6b5e1d8cbe9631b625dd1d5c36b4c1a7e13eea4 100644 --- a/lib/vial/blueprints/design_bs3/templates/_layout_login.html +++ b/lib/hawat/blueprints/design_bs3/templates/_layout_login.html @@ -8,7 +8,7 @@ <form method="POST" action="{{ url_for(request.endpoint, next = next) }}"> <fieldset> - <legend>{{ get_icon(vial_current_view.get_view_icon()) }} {{ vial_current_view.get_view_title() }}</legend> + <legend>{{ get_icon(hawat_current_view.get_view_icon()) }} {{ hawat_current_view.get_view_title() }}</legend> {%- block loginformfields %}{% endblock loginformfields %} diff --git a/lib/vial/blueprints/design_bs3/templates/_layout_registration.html b/lib/hawat/blueprints/design_bs3/templates/_layout_registration.html similarity index 82% rename from lib/vial/blueprints/design_bs3/templates/_layout_registration.html rename to lib/hawat/blueprints/design_bs3/templates/_layout_registration.html index 0c42620c35387cc942d510116b6eabf758634a46..6537a5fc7c25505cb02c96ce2630895552a27e20 100644 --- a/lib/vial/blueprints/design_bs3/templates/_layout_registration.html +++ b/lib/hawat/blueprints/design_bs3/templates/_layout_registration.html @@ -1,6 +1,6 @@ {%- extends "_layout.html" %} -{% block title %}{{ vial_current_view.get_view_title() }}{% endblock %} +{% block title %}{{ hawat_current_view.get_view_title() }}{% endblock %} {%- block content %} @@ -10,7 +10,7 @@ <form method="POST" action="{{ url_for(request.endpoint, next = next) }}"> <fieldset> - <legend>{{ get_icon(vial_current_view.get_view_icon()) }} {{ vial_current_view.get_view_title() }}</legend> + <legend>{{ get_icon(hawat_current_view.get_view_icon()) }} {{ hawat_current_view.get_view_title() }}</legend> {%- block registrationformfields %}{% endblock registrationformfields %} diff --git a/lib/vial/blueprints/design_bs3/templates/_layout_search.html b/lib/hawat/blueprints/design_bs3/templates/_layout_search.html similarity index 97% rename from lib/vial/blueprints/design_bs3/templates/_layout_search.html rename to lib/hawat/blueprints/design_bs3/templates/_layout_search.html index a3bce432a121a95e9dd394c234f870f990d6a3f7..b866596491cc371f7646eaab119ebb8ba1759450 100644 --- a/lib/vial/blueprints/design_bs3/templates/_layout_search.html +++ b/lib/hawat/blueprints/design_bs3/templates/_layout_search.html @@ -9,7 +9,7 @@ <!-- Search form - BEGIN ----------------------------------> <div class="jumbotron" style="margin-top: 1em;"> - <h2>{{ vial_current_view.get_view_title() }}</h2> + <h2>{{ hawat_current_view.get_view_title() }}</h2> <hr> <form method="GET" class="form" action="{{ url_for(request.endpoint) }}"> {% block contentsearchform %}{% endblock contentsearchform %} diff --git a/lib/vial/blueprints/design_bs3/templates/_macros_chart.html b/lib/hawat/blueprints/design_bs3/templates/_macros_chart.html similarity index 100% rename from lib/vial/blueprints/design_bs3/templates/_macros_chart.html rename to lib/hawat/blueprints/design_bs3/templates/_macros_chart.html diff --git a/lib/vial/blueprints/design_bs3/templates/_macros_form.html b/lib/hawat/blueprints/design_bs3/templates/_macros_form.html similarity index 99% rename from lib/vial/blueprints/design_bs3/templates/_macros_form.html rename to lib/hawat/blueprints/design_bs3/templates/_macros_form.html index ea5ba52c6265d815d7efff9850b0a44d07751942..6cb67be767452aff4f00fb465c5a309fe0f0c1ff 100644 --- a/lib/vial/blueprints/design_bs3/templates/_macros_form.html +++ b/lib/hawat/blueprints/design_bs3/templates/_macros_form.html @@ -182,7 +182,7 @@ </p> {%- endif %} {{ caller() }} - {%- if vial_current_view.has_help %} + {%- if hawat_current_view.has_help %} <p> {{ macros_page.render_help_link(icon = 'reference', text = _('more help'), tooltip_text = '') }} </p> diff --git a/lib/vial/blueprints/design_bs3/templates/_macros_page.html b/lib/hawat/blueprints/design_bs3/templates/_macros_page.html similarity index 96% rename from lib/vial/blueprints/design_bs3/templates/_macros_page.html rename to lib/hawat/blueprints/design_bs3/templates/_macros_page.html index 97b5ef67f3641293d2e53cecd5db8cfb702b0261..b46dfb1f6b5d09bf0c412991c2407bc5fcfa9b46 100644 --- a/lib/vial/blueprints/design_bs3/templates/_macros_page.html +++ b/lib/hawat/blueprints/design_bs3/templates/_macros_page.html @@ -5,7 +5,7 @@ ----------------------------------------------------------------------------- #} {%- macro render_breadcrumbs(context_item = None) %} - {%- set breadcrumbs_menu = vial_current_view.get_breadcrumbs_menu() %} + {%- set breadcrumbs_menu = hawat_current_view.get_breadcrumbs_menu() %} {%- if breadcrumbs_menu %} <!-- Breadcrumbs menu widget - BEGIN ----------------------> <ol class="breadcrumb"> @@ -36,7 +36,7 @@ {%- macro render_menu_actions(context_item = None, action_menu = None) %} {%- if not action_menu %} - {%- set action_menu = vial_current_view.get_action_menu() %} + {%- set action_menu = hawat_current_view.get_action_menu() %} {%- endif %} {%- if action_menu %} {%- set menu_item_list = action_menu.get_entries(item = context_item) %} @@ -82,7 +82,7 @@ {%- macro render_menu_context_actions(context_item, action_menu = None, cssclass = '', kwargs = {}) %} {%- if not action_menu %} - {%- set action_menu = vial_current_view.get_context_action_menu() %} + {%- set action_menu = hawat_current_view.get_context_action_menu() %} {%- endif %} {%- if action_menu %} {%- set menu_item_list = action_menu.get_entries(item = context_item, **kwargs) %} @@ -204,5 +204,5 @@ {%- macro render_help_link(icon = 'reference', text = _('Help'), tooltip_text = _('View full online documentation'), tooltip_placement = 'bottom', url_suffix = '') %} -<a href="https://alchemist.cesnet.cz/mentat/doc/production/html/_doclib/hawat_plugin_{{ vial_current_view.module_name }}.html#section-hawat-plugin-{{ vial_current_view.module_name }}-features-{{ vial_current_view.get_view_name() }}{{ url_suffix }}" target="_blank"{% if tooltip_text %} data-toggle="tooltip" data-placement="{{ tooltip_placement }}" title="{{ tooltip_text }}"{% endif %}>{% if icon %}{{ get_icon(icon) }}{% endif %}{% if text %} {{ text }}{% endif %}</a> +<a href="https://alchemist.cesnet.cz/mentat/doc/production/html/_doclib/hawat_plugin_{{ hawat_current_view.module_name }}.html#section-hawat-plugin-{{ hawat_current_view.module_name }}-features-{{ hawat_current_view.get_view_name() }}{{ url_suffix }}" target="_blank"{% if tooltip_text %} data-toggle="tooltip" data-placement="{{ tooltip_placement }}" title="{{ tooltip_text }}"{% endif %}>{% if icon %}{{ get_icon(icon) }}{% endif %}{% if text %} {{ text }}{% endif %}</a> {%- endmacro %} diff --git a/lib/vial/blueprints/design_bs3/templates/_macros_site.html b/lib/hawat/blueprints/design_bs3/templates/_macros_site.html similarity index 99% rename from lib/vial/blueprints/design_bs3/templates/_macros_site.html rename to lib/hawat/blueprints/design_bs3/templates/_macros_site.html index c59a60ae8ee612898c533cd22ea6d53f0e730a1d..a9ca93dfd15397105b49737c9f56b149a48c7d2b 100644 --- a/lib/vial/blueprints/design_bs3/templates/_macros_site.html +++ b/lib/hawat/blueprints/design_bs3/templates/_macros_site.html @@ -11,7 +11,7 @@ <!-- Main menu widget - BEGIN -----------------------------> <ul class="nav navbar-nav"> - {%- for menu_item in vial_current_menu_main.get_entries() recursive %} + {%- for menu_item in hawat_current_menu_main.get_entries() recursive %} {%- set menu_item_submenu = menu_item.get_entries() %} {%- if menu_item.group %} <li role="separator" class="divider"></li> @@ -54,7 +54,7 @@ {%- if current_user.has_role('admin') %}{{ get_icon('role-admin') }}{% elif current_user.has_role('maintainer') %}{{ get_icon('role-maintainer') }}{% elif current_user.has_role('developer') %}{{ get_icon('role-developer') }}{% else %}{{ get_icon('role-user') }}{% endif %}<span class="hidden-sm hidden-md"> {{ current_user.login }}</span> <span class="caret"></span> </a> <ul class="dropdown-menu"> - {%- for menu_item in vial_current_menu_auth.get_entries() recursive %} + {%- for menu_item in hawat_current_menu_auth.get_entries() recursive %} {%- set menu_item_submenu = menu_item.get_entries() %} {%- if menu_item.group %} <li role="separator" class="divider"></li> @@ -91,7 +91,7 @@ {{ get_icon('role-anonymous') }}<span class="hidden-sm hidden-md"> {{ _('Anonymous') }}</span> <span class="caret"></span> </a> <ul class="dropdown-menu"> - {%- for menu_item in vial_current_menu_anon.get_entries() recursive %} + {%- for menu_item in hawat_current_menu_anon.get_entries() recursive %} {%- set menu_item_submenu = menu_item.get_entries() %} {%- if menu_item.group %} <li role="separator" class="divider"></li> diff --git a/lib/vial/blueprints/design_bs3/templates/form_delete.html b/lib/hawat/blueprints/design_bs3/templates/form_delete.html similarity index 100% rename from lib/vial/blueprints/design_bs3/templates/form_delete.html rename to lib/hawat/blueprints/design_bs3/templates/form_delete.html diff --git a/lib/vial/blueprints/design_bs3/templates/form_disable.html b/lib/hawat/blueprints/design_bs3/templates/form_disable.html similarity index 100% rename from lib/vial/blueprints/design_bs3/templates/form_disable.html rename to lib/hawat/blueprints/design_bs3/templates/form_disable.html diff --git a/lib/vial/blueprints/design_bs3/templates/form_enable.html b/lib/hawat/blueprints/design_bs3/templates/form_enable.html similarity index 100% rename from lib/vial/blueprints/design_bs3/templates/form_enable.html rename to lib/hawat/blueprints/design_bs3/templates/form_enable.html diff --git a/lib/vial/blueprints/design_bs3/templates/http_error.html b/lib/hawat/blueprints/design_bs3/templates/http_error.html similarity index 100% rename from lib/vial/blueprints/design_bs3/templates/http_error.html rename to lib/hawat/blueprints/design_bs3/templates/http_error.html diff --git a/lib/vial/blueprints/design_bs3/templates/spt_flashmessage.html b/lib/hawat/blueprints/design_bs3/templates/spt_flashmessage.html similarity index 100% rename from lib/vial/blueprints/design_bs3/templates/spt_flashmessage.html rename to lib/hawat/blueprints/design_bs3/templates/spt_flashmessage.html diff --git a/lib/vial/blueprints/devtools/__init__.py b/lib/hawat/blueprints/devtools/__init__.py similarity index 58% rename from lib/vial/blueprints/devtools/__init__.py rename to lib/hawat/blueprints/devtools/__init__.py index 1e7616a10794a05263ba42a719fc4235dc24db56..c3d97152cf41a24010adcf6e38d76d285bec3f5d 100644 --- a/lib/vial/blueprints/devtools/__init__.py +++ b/lib/hawat/blueprints/devtools/__init__.py @@ -1,23 +1,27 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- +# This file is part of Mentat system (https://mentat.cesnet.cz/). +# +# Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ This pluggable module provides various utility and development tools. """ +__author__ = "Jan Mach <jan.mach@cesnet.cz>" +__credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" import flask_debugtoolbar from flask_babel import lazy_gettext -import vial.acl -from vial.app import VialBlueprint -from vial.view import SimpleView -from vial.view.mixin import HTMLMixin - +import hawat.acl +from hawat.base import HawatBlueprint +from hawat.view import SimpleView +from hawat.view.mixin import HTMLMixin BLUEPRINT_NAME = 'devtools' """Name of the blueprint as module global constant.""" @@ -30,7 +34,7 @@ class ConfigView(HTMLMixin, SimpleView): authentication = True - authorization = [vial.acl.PERMISSION_DEVELOPER] + authorization = [hawat.acl.PERMISSION_DEVELOPER] @classmethod def get_view_name(cls): @@ -49,10 +53,10 @@ class ConfigView(HTMLMixin, SimpleView): return lazy_gettext('View system configuration') -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- -class DevtoolsBlueprint(VialBlueprint): +class DevtoolsBlueprint(HawatBlueprint): """Pluggable module - development tools (*devtools*).""" @classmethod @@ -65,26 +69,26 @@ class DevtoolsBlueprint(VialBlueprint): app.menu_main.add_entry( 'view', 'developer.devconfig', - position = 10, - view = ConfigView + position=10, + view=ConfigView ) -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- def get_blueprint(): """ - Mandatory interface for :py:mod:`vial.Vial` and factory function. This function - must return a valid instance of :py:class:`vial.app.VialBlueprint` or + Mandatory interface for :py:mod:`hawat.Hawat` and factory function. This function + must return a valid instance of :py:class:`hawat.app.HawatBlueprint` or :py:class:`flask.Blueprint`. """ hbp = DevtoolsBlueprint( BLUEPRINT_NAME, __name__, - template_folder = 'templates', - url_prefix = '/{}'.format(BLUEPRINT_NAME) + template_folder='templates', + url_prefix='/{}'.format(BLUEPRINT_NAME) ) hbp.developer_toolbar = flask_debugtoolbar.DebugToolbarExtension() # pylint: disable=locally-disabled,attribute-defined-outside-init diff --git a/lib/vial/blueprints/devtools/templates/devtools/config.html b/lib/hawat/blueprints/devtools/templates/devtools/config.html similarity index 92% rename from lib/vial/blueprints/devtools/templates/devtools/config.html rename to lib/hawat/blueprints/devtools/templates/devtools/config.html index 458c30a6ae397700cc2d4811648f7932c1777ca4..3c8748014b510bea26e04ea0bc43f64df73f3631 100644 --- a/lib/vial/blueprints/devtools/templates/devtools/config.html +++ b/lib/hawat/blueprints/devtools/templates/devtools/config.html @@ -4,7 +4,7 @@ <div class="row"> <div class="col-lg-12"> - <h2>{{ vial_current_view.get_view_title() }}</h2> + <h2>{{ hawat_current_view.get_view_title() }}</h2> <h3>{{ _('Enabled modules') }}</h3> <table class="table"> @@ -93,21 +93,21 @@ <h3>{{ _('Menu - main') }}</h3> <pre class="pre-scrollable"> -{{ vial_current_menu_main|pprint }} +{{ hawat_current_menu_main|pprint }} --- -{{ vial_current_menu_main.get_entries()|pprint }}</pre> +{{ hawat_current_menu_main.get_entries()|pprint }}</pre> <h3>{{ _('Menu - auth') }}</h3> <pre class="pre-scrollable"> -{{ vial_current_menu_auth|pprint }} +{{ hawat_current_menu_auth|pprint }} --- -{{ vial_current_menu_auth.get_entries()|pprint }}</pre> +{{ hawat_current_menu_auth.get_entries()|pprint }}</pre> <h3>{{ _('Menu - anon') }}</h3> <pre class="pre-scrollable"> -{{ vial_current_menu_anon|pprint }} +{{ hawat_current_menu_anon|pprint }} --- -{{ vial_current_menu_anon.get_entries()|pprint }}</pre> +{{ hawat_current_menu_anon.get_entries()|pprint }}</pre> </div><!-- /.col-lg-12 --> </div><!-- /.row --> diff --git a/lib/vial/blueprints/devtools/test/__init__.py b/lib/hawat/blueprints/devtools/test/__init__.py similarity index 77% rename from lib/vial/blueprints/devtools/test/__init__.py rename to lib/hawat/blueprints/devtools/test/__init__.py index 8a7353c7a7b6f655c71c6bec106c8f4fad2f5877..c35d8f328de3d8bdcf28f2b16afa89394d9ef423 100644 --- a/lib/vial/blueprints/devtools/test/__init__.py +++ b/lib/hawat/blueprints/devtools/test/__init__.py @@ -6,24 +6,24 @@ """ -Unit tests for :py:mod:`vial.blueprints.devtools`. +Unit tests for :py:mod:`hawat.blueprints.devtools`. """ import sys import unittest -import vial.const -import vial.test -import vial.db -from vial.test import VialTestCase -from vial.test.runner import TestRunnerMixin +import hawat.const +import hawat.test +import hawat.db +from hawat.test import HawatTestCase +from hawat.test.runner import TestRunnerMixin _IS_NOSE = sys.argv[0].endswith('nosetests') @unittest.skipIf(_IS_NOSE, "broken under nosetest") -class ConfigTestCase(TestRunnerMixin, VialTestCase): +class ConfigTestCase(TestRunnerMixin, HawatTestCase): """ Class for testing ``devtools.config`` endpoint. @@ -53,22 +53,22 @@ class ConfigTestCase(TestRunnerMixin, VialTestCase): """Test access as anonymous user.""" self._attempt_fail() - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_USER) def test_02_as_user(self): """Test access as user ``user``.""" self._attempt_fail() - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_DEVELOPER) def test_03_as_developer(self): """Test access as user ``developer``.""" self._attempt_succeed() - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_MAINTAINER) def test_04_as_maintainer(self): """Test access as user ``maintainer``.""" self._attempt_fail() - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_ADMIN) def test_05_as_admin(self): """Test access as user ``admin``.""" self._attempt_fail() diff --git a/lib/hawat/blueprints/dnsr/__init__.py b/lib/hawat/blueprints/dnsr/__init__.py index a4cc0774794b5b59c5ac8ad82efbc8bb50c907d7..bd17f8bc0c5f681a5635f558d13c693233665f85 100644 --- a/lib/hawat/blueprints/dnsr/__init__.py +++ b/lib/hawat/blueprints/dnsr/__init__.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ @@ -17,14 +17,14 @@ Provided endpoints ------------------ ``/dnsr/search`` - Endpoint providing search form for querying DNS service and formating result + Endpoint providing search form for querying DNS service and formatting result as HTML page. * *Authentication:* login required * *Methods:* ``GET`` ``/api/dnsr/search`` - Endpoint providing API search form for querying DNS service and formating + Endpoint providing API search form for querying DNS service and formatting result as JSON document. * *Authentication:* login required @@ -32,7 +32,7 @@ Provided endpoints * *Methods:* ``GET``, ``POST`` ``/snippet/dnsr/search`` - Endpoint providing API search form for querying DNS service and formating + Endpoint providing API search form for querying DNS service and formatting result as JSON document containing HTML snippets. * *Authentication:* login required @@ -40,28 +40,25 @@ Provided endpoints * *Methods:* ``GET``, ``POST`` """ - __author__ = "Jan Mach <jan.mach@cesnet.cz>" __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" - import flask from flask_babel import lazy_gettext import mentat.services.dnsr from mentat.const import tr_ -import vial.db -import vial.const -import vial.acl -from vial.app import VialBlueprint -from vial.view import RenderableView -from vial.view.mixin import HTMLMixin, AJAXMixin, SnippetMixin -from vial.utils import URLParamsBuilder +import hawat.db +import hawat.const +import hawat.acl +from hawat.base import HawatBlueprint +from hawat.view import RenderableView +from hawat.view.mixin import HTMLMixin, AJAXMixin, SnippetMixin +from hawat.utils import URLParamsBuilder import hawat.const from hawat.blueprints.dnsr.forms import DnsrSearchForm - BLUEPRINT_NAME = 'dnsr' """Name of the blueprint as module global constant.""" @@ -82,35 +79,35 @@ class AbstractSearchView(RenderableView): # pylint: disable=locally-disabled,ab def get_menu_title(cls, **kwargs): return lazy_gettext('Search DNS') - #--------------------------------------------------------------------------- + # --------------------------------------------------------------------------- def dispatch_request(self): """ Mandatory interface required by the :py:func:`flask.views.View.dispatch_request`. Will be called by the *Flask* framework to service the request. """ - form = DnsrSearchForm(flask.request.args, meta = {'csrf': False}) + form = DnsrSearchForm(flask.request.args, meta={'csrf': False}) - if vial.const.FORM_ACTION_SUBMIT in flask.request.args: + if hawat.const.FORM_ACTION_SUBMIT in flask.request.args: if form.validate(): form_data = form.data dnsr_service = mentat.services.dnsr.service() self.response_context.update( - search_item = form.search.data, - form_data = form_data + search_item=form.search.data, + form_data=form_data ) try: self.response_context.update( - search_result = dnsr_service.lookup( + search_result=dnsr_service.lookup( form.search.data ) ) except Exception as exc: - self.flash(str(exc), level = 'error') + self.flash(str(exc), level='error') self.response_context.update( - search_form = form, - request_args = flask.request.args, + search_form=form, + request_args=flask.request.args, ) return self.generate_response() @@ -132,7 +129,7 @@ class APISearchView(AJAXMixin, AbstractSearchView): # pylint: disable=locally-d View responsible for querying DNS service and presenting the results in the form of JSON document. """ - methods = ['GET','POST'] + methods = ['GET', 'POST'] @classmethod def get_view_name(cls): @@ -144,9 +141,9 @@ class SnippetSearchView(SnippetMixin, AbstractSearchView): # pylint: disable=lo View responsible for querying DNS service and presenting the results in the form of JSON document containing ready to use HTML page snippets. """ - methods = ['GET', 'POST'] + methods = ['GET', 'POST'] - renders = ['label', 'full'] + renders = ['label', 'full'] snippets = [ { @@ -160,10 +157,10 @@ class SnippetSearchView(SnippetMixin, AbstractSearchView): # pylint: disable=lo return 'sptsearch' -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- -class DnsrBlueprint(VialBlueprint): +class DnsrBlueprint(HawatBlueprint): """Pluggable module - DNS service (*dnsr*).""" @classmethod @@ -176,9 +173,9 @@ class DnsrBlueprint(VialBlueprint): app.menu_main.add_entry( 'view', 'more.{}'.format(BLUEPRINT_NAME), - position = 10, - group = lazy_gettext('Data services'), - view = SearchView + position=10, + group=lazy_gettext('Data services'), + view=SearchView ) # Register context actions provided by this module. @@ -202,24 +199,24 @@ class DnsrBlueprint(VialBlueprint): ) -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- def get_blueprint(): """ - Mandatory interface for :py:mod:`vial.Vial` and factory function. This function - must return a valid instance of :py:class:`vial.app.VialBlueprint` or + Mandatory interface for :py:mod:`hawat.Hawat` and factory function. This function + must return a valid instance of :py:class:`hawat.app.HawatBlueprint` or :py:class:`flask.Blueprint`. """ hbp = DnsrBlueprint( BLUEPRINT_NAME, __name__, - template_folder = 'templates' + template_folder='templates' ) - hbp.register_view_class(SearchView, '/{}/search'.format(BLUEPRINT_NAME)) - hbp.register_view_class(APISearchView, '/api/{}/search'.format(BLUEPRINT_NAME)) + hbp.register_view_class(SearchView, '/{}/search'.format(BLUEPRINT_NAME)) + hbp.register_view_class(APISearchView, '/api/{}/search'.format(BLUEPRINT_NAME)) hbp.register_view_class(SnippetSearchView, '/snippet/{}/search'.format(BLUEPRINT_NAME)) return hbp diff --git a/lib/hawat/blueprints/dnsr/forms.py b/lib/hawat/blueprints/dnsr/forms.py index 309d00deab299e40e3fe89d69f0b614481534ecb..488389746454ec4f300c1e6cfdb13bfe90c81b37 100644 --- a/lib/hawat/blueprints/dnsr/forms.py +++ b/lib/hawat/blueprints/dnsr/forms.py @@ -1,22 +1,20 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ This module contains custom DNS service search form for Hawat. """ - __author__ = "Jan Mach <jan.mach@cesnet.cz>" __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" - import wtforms import flask_wtf from flask_babel import lazy_gettext @@ -28,7 +26,7 @@ class DnsrSearchForm(flask_wtf.FlaskForm): """ search = wtforms.StringField( lazy_gettext('Search DNS:'), - validators = [ + validators=[ wtforms.validators.DataRequired() ] ) diff --git a/lib/hawat/blueprints/dnsr/templates/dnsr/search.html b/lib/hawat/blueprints/dnsr/templates/dnsr/search.html index 964d1ed2be07906ffe835b496d8ca3b5b9a4356d..852620068784611d1194645239161cd902c81814 100644 --- a/lib/hawat/blueprints/dnsr/templates/dnsr/search.html +++ b/lib/hawat/blueprints/dnsr/templates/dnsr/search.html @@ -6,7 +6,7 @@ <div class="col-lg-12"> <div class="jumbotron" style="margin-top: 1em;"> - <h2>{{ vial_current_view.get_view_title() }}</h2> + <h2>{{ hawat_current_view.get_view_title() }}</h2> <form method="GET" class="form-inline" action="{{ url_for('dnsr.search') }}"> <div class="form-group{% if search_form.search.errors %}{{ ' has-error' }}{% endif %}"> {{ search_form.search.label(class_='sr-only') }} diff --git a/lib/hawat/blueprints/dnsr/test/__init__.py b/lib/hawat/blueprints/dnsr/test/__init__.py index 535365322e43e5ecb5676c5055108ed582ac7a08..8a4bdf4d77be9910efb72c18ef91fe228098237f 100644 --- a/lib/hawat/blueprints/dnsr/test/__init__.py +++ b/lib/hawat/blueprints/dnsr/test/__init__.py @@ -1,28 +1,30 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ Unit tests for :py:mod:`hawat.blueprints.dnsr`. """ +__author__ = "Jan Mach <jan.mach@cesnet.cz>" +__credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" import unittest -import vial.const -import vial.test -import vial.db -from vial.test import VialTestCase +import hawat.const +import hawat.test +import hawat.db +from hawat.test import HawatTestCase from hawat.test.runner import TestRunnerMixin -class SearchTestCase(TestRunnerMixin, VialTestCase): +class SearchTestCase(TestRunnerMixin, HawatTestCase): """ Class for testing ``dnsr.search`` endpoint. """ @@ -35,7 +37,7 @@ class SearchTestCase(TestRunnerMixin, VialTestCase): b'Redirecting...', b'login?next=' ], - follow_redirects = False + follow_redirects=False ) def _attempt_succeed(self): @@ -51,28 +53,28 @@ class SearchTestCase(TestRunnerMixin, VialTestCase): """Test access as anonymous user.""" self._attempt_fail_redirect() - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_USER) def test_02_as_user(self): """Test access as user ``user``.""" self._attempt_succeed() - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_DEVELOPER) def test_03_as_developer(self): """Test access as user ``developer``.""" self._attempt_succeed() - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_MAINTAINER) def test_04_as_maintainer(self): """Test access as user ``maintainer``.""" self._attempt_succeed() - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_ADMIN) def test_05_as_admin(self): """Test access as user ``admin``.""" self._attempt_succeed() -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- if __name__ == "__main__": diff --git a/lib/hawat/blueprints/events/__init__.py b/lib/hawat/blueprints/events/__init__.py index bd71ddc6910ab4c903067c080ed80a6ee5666e62..7c60d29ab9d5384e2d00befb08929e0ece92b6f7 100644 --- a/lib/hawat/blueprints/events/__init__.py +++ b/lib/hawat/blueprints/events/__init__.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ @@ -14,11 +14,9 @@ related to `IDEA <https://idea.cesnet.cz/en/index>`__ events, database searching viewing event details and producing event dashboards. """ - __author__ = "Jan Mach <jan.mach@cesnet.cz>" __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" - import datetime import flask @@ -31,42 +29,44 @@ from mentat.const import tr_ import hawat.const import hawat.events -import vial.const -import vial.acl -from vial.app import VialBlueprint -from vial.view import BaseView, SimpleView, BaseSearchView, ItemShowView -from vial.view.mixin import HTMLMixin, AJAXMixin, SQLAlchemyMixin -from vial.utils import URLParamsBuilder +import hawat.const +import hawat.acl +import hawat.forms +import hawat.menu +from hawat.base import HawatBlueprint +from hawat.view import BaseView, SimpleView, BaseSearchView, ItemShowView +from hawat.view.mixin import HTMLMixin, AJAXMixin, SQLAlchemyMixin +from hawat.utils import URLParamsBuilder from hawat.base import PsycopgMixin from hawat.blueprints.events.forms import SimpleEventSearchForm, EventDashboardForm - BLUEPRINT_NAME = 'events' """Name of the blueprint as module global constant.""" -def _get_search_form(request_args = None): +def _get_search_form(request_args=None): choices = hawat.events.get_event_form_choices() form = SimpleEventSearchForm( request_args, - meta = {'csrf': False}, - choices_source_types = choices['source_types'], - choices_target_types = choices['target_types'], - choices_host_types = choices['host_types'], - choices_detectors = choices['detectors'], - choices_detector_types = choices['detector_types'], - choices_categories = choices['categories'], - choices_severities = choices['severities'], - choices_classes = choices['classes'], - choices_protocols = choices['protocols'], - choices_inspection_errs = choices['inspection_errs'], + meta={'csrf': False}, + choices_source_types=choices['source_types'], + choices_target_types=choices['target_types'], + choices_host_types=choices['host_types'], + choices_detectors=choices['detectors'], + choices_detector_types=choices['detector_types'], + choices_categories=choices['categories'], + choices_severities=choices['severities'], + choices_classes=choices['classes'], + choices_protocols=choices['protocols'], + choices_inspection_errs=choices['inspection_errs'], ) # In case no time bounds were set adjust them manually. - if request_args and not ('dt_from' in request_args or 'dt_to' in request_args or 'st_from' in request_args or 'st_to' in request_args): - form.dt_from.process_data(vial.forms.default_dt_with_delta()) - form.dt_to.process_data(vial.forms.default_dt()) + if request_args and not ( + 'dt_from' in request_args or 'dt_to' in request_args or 'st_from' in request_args or 'st_to' in request_args): + form.dt_from.process_data(hawat.forms.default_dt_with_delta()) + form.dt_to.process_data(hawat.forms.default_dt()) return form @@ -99,7 +99,7 @@ class AbstractSearchView(PsycopgMixin, BaseSearchView): # pylint: disable=local def do_before_response(self, **kwargs): self.response_context.update( - quicksearch_list = self.get_quicksearch_by_time() + quicksearch_list=self.get_quicksearch_by_time() ) @@ -121,33 +121,33 @@ class SearchView(HTMLMixin, AbstractSearchView): # pylint: disable=locally-disa @classmethod def get_breadcrumbs_menu(cls): - breadcrumbs_menu = vial.menu.Menu() + breadcrumbs_menu = hawat.menu.Menu() breadcrumbs_menu.add_entry( 'endpoint', 'home', - endpoint = flask.current_app.config['ENDPOINT_HOME'] + endpoint=flask.current_app.config['ENDPOINT_HOME'] ) breadcrumbs_menu.add_entry( 'endpoint', 'search', - endpoint = '{}.search'.format(cls.module_name) + endpoint='{}.search'.format(cls.module_name) ) return breadcrumbs_menu @classmethod def get_context_action_menu(cls): - action_menu = vial.menu.Menu() + action_menu = hawat.menu.Menu() action_menu.add_entry( 'endpoint', 'show', - endpoint = 'events.show', - hidetitle = True + endpoint='events.show', + hidetitle=True ) action_menu.add_entry( 'endpoint', 'download', - endpoint = 'events.download', - hidetitle = True + endpoint='events.download', + hidetitle=True ) return action_menu @@ -157,7 +157,7 @@ class APISearchView(AJAXMixin, AbstractSearchView): # pylint: disable=locally-d View responsible for searching the `IDEA <https://idea.cesnet.cz/en/index>`__ event database and presenting the results in the form of JSON document. """ - methods = ['GET','POST'] + methods = ['GET', 'POST'] @classmethod def get_view_name(cls): @@ -183,7 +183,7 @@ class AbstractShowView(PsycopgMixin, ItemShowView): # pylint: disable=locally-d def get_menu_legend(cls, **kwargs): return lazy_gettext( 'View details of event "%(item)s"', - item = flask.escape(kwargs['item'].get_id()) + item=flask.escape(kwargs['item'].get_id()) ) @@ -198,11 +198,11 @@ class ShowView(HTMLMixin, AbstractShowView): # pylint: disable=locally-disabled @classmethod def get_action_menu(cls): # pylint: disable=locally-disabled,unused-argument - action_menu = vial.menu.Menu() + action_menu = hawat.menu.Menu() action_menu.add_entry( 'endpoint', 'download', - endpoint = 'events.download' + endpoint='events.download' ) return action_menu @@ -212,7 +212,7 @@ class APIShowView(AJAXMixin, AbstractShowView): # pylint: disable=locally-disab Detailed `IDEA <https://idea.cesnet.cz/en/index>`__ event view that presents the result as HTML page. """ - methods = ['GET','POST'] + methods = ['GET', 'POST'] @classmethod def get_view_name(cls): @@ -239,7 +239,7 @@ class DownloadView(PsycopgMixin, BaseView): def get_view_url(cls, **kwargs): return flask.url_for( cls.get_view_endpoint(), - item_id = kwargs['item'].get_id() + item_id=kwargs['item'].get_id() ) @classmethod @@ -250,10 +250,10 @@ class DownloadView(PsycopgMixin, BaseView): def get_menu_legend(cls, **kwargs): return lazy_gettext( 'Download event "%(item)s"', - item = flask.escape(kwargs['item'].get_id()) + item=flask.escape(kwargs['item'].get_id()) ) - #--------------------------------------------------------------------------- + # --------------------------------------------------------------------------- def dispatch_request(self, item_id): # pylint: disable=locally-disabled,arguments-differ """ @@ -273,7 +273,7 @@ class DownloadView(PsycopgMixin, BaseView): ) response = flask.make_response( - item.to_json(indent = 4, sort_keys = True) + item.to_json(indent=4, sort_keys=True) ) response.mimetype = 'application/json' response.headers['Content-Disposition'] = 'attachment; filename={}.idea.json'.format(item_id) @@ -303,7 +303,7 @@ class AbstractDashboardView(SQLAlchemyMixin, BaseSearchView): # pylint: disable def get_view_template(cls): return '{}/{}.html'.format(cls.module_name, cls.get_view_name()) - #--------------------------------------------------------------------------- + # --------------------------------------------------------------------------- @property def dbmodel(self): @@ -311,7 +311,7 @@ class AbstractDashboardView(SQLAlchemyMixin, BaseSearchView): # pylint: disable @staticmethod def get_search_form(request_args): - return EventDashboardForm(request_args, meta = {'csrf': False}) + return EventDashboardForm(request_args, meta={'csrf': False}) @staticmethod def build_query(query, model, form_args): @@ -333,10 +333,10 @@ class AbstractDashboardView(SQLAlchemyMixin, BaseSearchView): # pylint: disable if items: dt_from = self.response_context['form_data'].get('dt_from', None) if dt_from: - dt_from = dt_from.replace(tzinfo = None) - dt_to = self.response_context['form_data'].get('dt_to', None) + dt_from = dt_from.replace(tzinfo=None) + dt_to = self.response_context['form_data'].get('dt_to', None) if dt_to: - dt_to = dt_to.replace(tzinfo = None) + dt_to = dt_to.replace(tzinfo=None) if not dt_from: dt_from = self.dbcolumn_min(self.dbmodel.createtime) @@ -344,18 +344,18 @@ class AbstractDashboardView(SQLAlchemyMixin, BaseSearchView): # pylint: disable dt_to = datetime.datetime.utcnow() self.response_context.update( - statistics = mentat.stats.idea.aggregate_timeline_groups( + statistics=mentat.stats.idea.aggregate_timeline_groups( items, - dt_from = dt_from, - dt_to = dt_to, - max_count = flask.current_app.config['HAWAT_CHART_TIMELINE_MAXSTEPS'], - min_step = 300 + dt_from=dt_from, + dt_to=dt_to, + max_count=flask.current_app.config['HAWAT_CHART_TIMELINE_MAXSTEPS'], + min_step=300 ) ) def do_before_response(self, **kwargs): self.response_context.update( - quicksearch_list = self.get_quicksearch_by_time() + quicksearch_list=self.get_quicksearch_by_time() ) @@ -376,7 +376,7 @@ class APIDashboardView(AJAXMixin, AbstractDashboardView): # pylint: disable=loc View responsible for presenting overall `IDEA <https://idea.cesnet.cz/en/index>`__ event statistics dashboard in the form of JSON document. """ - methods = ['GET','POST'] + methods = ['GET', 'POST'] @classmethod def get_view_name(cls): @@ -399,7 +399,7 @@ class APIMetadataView(AJAXMixin, SimpleView): """ authentication = True - methods = ['GET','POST'] + methods = ['GET', 'POST'] @classmethod def get_view_name(cls): @@ -413,10 +413,10 @@ class APIMetadataView(AJAXMixin, SimpleView): self.response_context.update(**hawat.events.get_event_enums()) -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- -class EventsBlueprint(VialBlueprint): +class EventsBlueprint(HawatBlueprint): """Pluggable module - `IDEA <https://idea.cesnet.cz/en/index>`__ event database (*events*).""" @classmethod @@ -427,15 +427,15 @@ class EventsBlueprint(VialBlueprint): app.menu_main.add_entry( 'view', 'dashboards.{}'.format(BLUEPRINT_NAME), - position = 10, - view = DashboardView + position=10, + view=DashboardView ) app.menu_main.add_entry( 'view', BLUEPRINT_NAME, - position = 140, - view = SearchView, - resptitle = True + position=140, + view=SearchView, + resptitle=True ) def _get_upb(): @@ -555,31 +555,31 @@ class EventsBlueprint(VialBlueprint): ) -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- def get_blueprint(): """ - Mandatory interface for :py:mod:`vial.Vial` and factory function. This function - must return a valid instance of :py:class:`vial.app.VialBlueprint` or + Mandatory interface for :py:mod:`hawat.Hawat` and factory function. This function + must return a valid instance of :py:class:`hawat.app.HawatBlueprint` or :py:class:`flask.Blueprint`. """ hbp = EventsBlueprint( BLUEPRINT_NAME, __name__, - template_folder = 'templates', - static_folder = 'static', - static_url_path = '/{}/static'.format(BLUEPRINT_NAME) + template_folder='templates', + static_folder='static', + static_url_path='/{}/static'.format(BLUEPRINT_NAME) ) - hbp.register_view_class(SearchView, '/{}/search'.format(BLUEPRINT_NAME)) - hbp.register_view_class(ShowView, '/{}/<item_id>/show'.format(BLUEPRINT_NAME)) - hbp.register_view_class(DownloadView, '/{}/<item_id>/download'.format(BLUEPRINT_NAME)) - hbp.register_view_class(DashboardView, '/{}/dashboard'.format(BLUEPRINT_NAME)) - hbp.register_view_class(APISearchView, '/api/{}/search'.format(BLUEPRINT_NAME)) - hbp.register_view_class(APIShowView, '/api/{}/<item_id>/show'.format(BLUEPRINT_NAME)) + hbp.register_view_class(SearchView, '/{}/search'.format(BLUEPRINT_NAME)) + hbp.register_view_class(ShowView, '/{}/<item_id>/show'.format(BLUEPRINT_NAME)) + hbp.register_view_class(DownloadView, '/{}/<item_id>/download'.format(BLUEPRINT_NAME)) + hbp.register_view_class(DashboardView, '/{}/dashboard'.format(BLUEPRINT_NAME)) + hbp.register_view_class(APISearchView, '/api/{}/search'.format(BLUEPRINT_NAME)) + hbp.register_view_class(APIShowView, '/api/{}/<item_id>/show'.format(BLUEPRINT_NAME)) hbp.register_view_class(APIDashboardView, '/api/{}/dashboard'.format(BLUEPRINT_NAME)) - hbp.register_view_class(APIMetadataView, '/api/{}/metadata'.format(BLUEPRINT_NAME)) + hbp.register_view_class(APIMetadataView, '/api/{}/metadata'.format(BLUEPRINT_NAME)) return hbp diff --git a/lib/hawat/blueprints/events/forms.py b/lib/hawat/blueprints/events/forms.py index ded3d5479c40ac33a090a23a5786aa12fbfb8d57..66236dc7a8adf9be731c5fce55852dfaa6575dea 100644 --- a/lib/hawat/blueprints/events/forms.py +++ b/lib/hawat/blueprints/events/forms.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ @@ -13,11 +13,9 @@ This module contains various `IDEA <https://idea.cesnet.cz/en/index>`__ event database search forms for Hawat application. """ - __author__ = "Jan Mach <jan.mach@cesnet.cz>" __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" - import wtforms from wtforms.ext.sqlalchemy.fields import QuerySelectMultipleField @@ -25,9 +23,9 @@ import flask_wtf from flask_babel import lazy_gettext from mentat.datatype.sqldb import GroupModel -import vial.const -import vial.forms -import vial.db +import hawat.const +import hawat.forms +import hawat.db import hawat.const @@ -35,297 +33,317 @@ def get_available_groups(): """ Query the database for list of all available groups. """ - return vial.db.db_query(GroupModel).order_by(GroupModel.name).all() + return hawat.db.db_query(GroupModel).order_by(GroupModel.name).all() -class SimpleEventSearchForm(vial.forms.BaseSearchForm): +class SimpleEventSearchForm(hawat.forms.BaseSearchForm): """ Class representing simple event search form. """ - dt_from = vial.forms.SmartDateTimeField( + dt_from = hawat.forms.SmartDateTimeField( lazy_gettext('Detection time from:'), - validators = [ + validators=[ wtforms.validators.Optional() ], - description = lazy_gettext('Lower time boundary for event detection time as provided by event detector. Timestamp is expected to be in the format <code>YYYY-MM-DD hh:mm:ss</code> and in the timezone according to the user`s preferences. Event detectors are usually outside of the control of Mentat system administrators and may sometimes emit events with invalid detection times, for example timestamps in the future.'), - default = lambda: vial.forms.default_dt_with_delta(hawat.const.DEFAULT_RESULT_TIMEDELTA) + description=lazy_gettext( + 'Lower time boundary for event detection time as provided by event detector. Timestamp is expected to be in the format <code>YYYY-MM-DD hh:mm:ss</code> and in the timezone according to the user`s preferences. Event detectors are usually outside of the control of Mentat system administrators and may sometimes emit events with invalid detection times, for example timestamps in the future.'), + default=lambda: hawat.forms.default_dt_with_delta(hawat.const.DEFAULT_RESULT_TIMEDELTA) ) - dt_to = vial.forms.SmartDateTimeField( + dt_to = hawat.forms.SmartDateTimeField( lazy_gettext('Detection time to:'), - validators = [ + validators=[ wtforms.validators.Optional() ], - description = lazy_gettext('Upper time boundary for event detection time as provided by event detector. Timestamp is expected to be in the format <code>YYYY-MM-DD hh:mm:ss</code> and in the timezone according to the user`s preferences. Event detectors are usually outside of the control of Mentat system administrators and may sometimes emit events with invalid detection times, for example timestamps in the future.'), - default = vial.forms.default_dt + description=lazy_gettext( + 'Upper time boundary for event detection time as provided by event detector. Timestamp is expected to be in the format <code>YYYY-MM-DD hh:mm:ss</code> and in the timezone according to the user`s preferences. Event detectors are usually outside of the control of Mentat system administrators and may sometimes emit events with invalid detection times, for example timestamps in the future.'), + default=hawat.forms.default_dt ) - st_from = vial.forms.SmartDateTimeField( + st_from = hawat.forms.SmartDateTimeField( lazy_gettext('Storage time from:'), - validators = [ + validators=[ wtforms.validators.Optional() ], - description = lazy_gettext('Lower time boundary for event storage time. Timestamp is expected to be in the format <code>YYYY-MM-DD hh:mm:ss</code> and in the timezone according to the user`s preferences. Event storage time is provided by Mentat system itself. It is a timestamp of the exact moment the event was stored into the database.') + description=lazy_gettext( + 'Lower time boundary for event storage time. Timestamp is expected to be in the format <code>YYYY-MM-DD hh:mm:ss</code> and in the timezone according to the user`s preferences. Event storage time is provided by Mentat system itself. It is a timestamp of the exact moment the event was stored into the database.') ) - st_to = vial.forms.SmartDateTimeField( + st_to = hawat.forms.SmartDateTimeField( lazy_gettext('Storage time to:'), - validators = [ + validators=[ wtforms.validators.Optional() ], - description = lazy_gettext('Upper time boundary for event storage time. Timestamp is expected to be in the format <code>YYYY-MM-DD hh:mm:ss</code> and in the timezone according to the user`s preferences. Event storage time is provided by Mentat system itself. It is a timestamp of the exact moment the event was stored into the database.') + description=lazy_gettext( + 'Upper time boundary for event storage time. Timestamp is expected to be in the format <code>YYYY-MM-DD hh:mm:ss</code> and in the timezone according to the user`s preferences. Event storage time is provided by Mentat system itself. It is a timestamp of the exact moment the event was stored into the database.') ) - source_addrs = vial.forms.CommaListField( + source_addrs = hawat.forms.CommaListField( lazy_gettext('Source addresses:'), - validators = [ + validators=[ wtforms.validators.Optional(), - vial.forms.check_network_record_list + hawat.forms.check_network_record_list ], - widget = wtforms.widgets.TextArea(), - description = lazy_gettext('Comma separated list of event source IP4/6 addresses, ranges or networks. In this context a source does not necessarily mean a source of the connection, but rather a source of the problem as reported by a detector. Any additional whitespace is ignored and may be used for better readability.') + widget=wtforms.widgets.TextArea(), + description=lazy_gettext( + 'Comma separated list of event source IP4/6 addresses, ranges or networks. In this context a source does not necessarily mean a source of the connection, but rather a source of the problem as reported by a detector. Any additional whitespace is ignored and may be used for better readability.') ) - target_addrs = vial.forms.CommaListField( + target_addrs = hawat.forms.CommaListField( lazy_gettext('Target addresses:'), - validators = [ + validators=[ wtforms.validators.Optional(), - vial.forms.check_network_record_list + hawat.forms.check_network_record_list ], - widget = wtforms.widgets.TextArea(), - description = lazy_gettext('Comma separated list of event target IP4/6 addresses, ranges or networks. In this context a target does not necessarily mean a target of the connection, but rather a victim of the problem as reported by a detector. Any additional whitespace is ignored and may be used for better readability.') + widget=wtforms.widgets.TextArea(), + description=lazy_gettext( + 'Comma separated list of event target IP4/6 addresses, ranges or networks. In this context a target does not necessarily mean a target of the connection, but rather a victim of the problem as reported by a detector. Any additional whitespace is ignored and may be used for better readability.') ) - host_addrs = vial.forms.CommaListField( + host_addrs = hawat.forms.CommaListField( lazy_gettext('Host addresses:'), - validators = [ + validators=[ wtforms.validators.Optional(), - vial.forms.check_network_record_list + hawat.forms.check_network_record_list ], - widget = wtforms.widgets.TextArea(), - description = lazy_gettext('Comma separated list of event source or target IP4/6 addresses, ranges or networks. Any additional whitespace is ignored and may be used for better readability.') + widget=wtforms.widgets.TextArea(), + description=lazy_gettext( + 'Comma separated list of event source or target IP4/6 addresses, ranges or networks. Any additional whitespace is ignored and may be used for better readability.') ) - source_ports = vial.forms.CommaListField( + source_ports = hawat.forms.CommaListField( lazy_gettext('Source ports:'), - validators = [ + validators=[ wtforms.validators.Optional(), - vial.forms.check_port_list + hawat.forms.check_port_list ], - description = lazy_gettext('Comma separated list of source ports as integers. In this context a source does not necessarily mean a source of the connection, but rather a source of the problem as reported by a detector. Any additional whitespace is ignored and may be used for better readability.') + description=lazy_gettext( + 'Comma separated list of source ports as integers. In this context a source does not necessarily mean a source of the connection, but rather a source of the problem as reported by a detector. Any additional whitespace is ignored and may be used for better readability.') ) - target_ports = vial.forms.CommaListField( + target_ports = hawat.forms.CommaListField( lazy_gettext('Target ports:'), - validators = [ + validators=[ wtforms.validators.Optional(), - vial.forms.check_port_list + hawat.forms.check_port_list ], - description = lazy_gettext('Comma separated list of target ports as integers. In this context a target does not necessarily mean a target of the connection, but rather a victim of the problem as reported by a detector. Any additional whitespace is ignored and may be used for better readability.') + description=lazy_gettext( + 'Comma separated list of target ports as integers. In this context a target does not necessarily mean a target of the connection, but rather a victim of the problem as reported by a detector. Any additional whitespace is ignored and may be used for better readability.') ) - host_ports = vial.forms.CommaListField( + host_ports = hawat.forms.CommaListField( lazy_gettext('Host ports:'), - validators = [ + validators=[ wtforms.validators.Optional(), - vial.forms.check_port_list + hawat.forms.check_port_list ], - description = lazy_gettext('Comma separated list of source or target ports as integers. Any additional whitespace is ignored and may be used for better readability.') + description=lazy_gettext( + 'Comma separated list of source or target ports as integers. Any additional whitespace is ignored and may be used for better readability.') ) source_types = wtforms.SelectMultipleField( lazy_gettext('Source types:'), - validators = [ + validators=[ wtforms.validators.Optional() ], - choices = [], - filters = [lambda x: x or []], - description = lazy_gettext('Specification of event source type. Each event source may be optionally assigned one or more labels to better categorize type of a source.') + choices=[], + filters=[lambda x: x or []], + description=lazy_gettext( + 'Specification of event source type. Each event source may be optionally assigned one or more labels to better categorize type of a source.') ) target_types = wtforms.SelectMultipleField( lazy_gettext('Target types:'), - validators = [ + validators=[ wtforms.validators.Optional() ], - choices = [], - filters = [lambda x: x or []], - description = lazy_gettext('Specification of event target type. Each event target may be optionally assigned one or more labels to better categorize type of a target.') + choices=[], + filters=[lambda x: x or []], + description=lazy_gettext( + 'Specification of event target type. Each event target may be optionally assigned one or more labels to better categorize type of a target.') ) host_types = wtforms.SelectMultipleField( lazy_gettext('Host types:'), - validators = [ + validators=[ wtforms.validators.Optional() ], - choices = [], - filters = [lambda x: x or []], - description = lazy_gettext('Specification of event source or target type. Each event source or target may be optionally assigned one or more labels to better categorize type of a source or target.') + choices=[], + filters=[lambda x: x or []], + description=lazy_gettext( + 'Specification of event source or target type. Each event source or target may be optionally assigned one or more labels to better categorize type of a source or target.') ) detectors = wtforms.SelectMultipleField( lazy_gettext('Detectors:'), - validators = [ + validators=[ wtforms.validators.Optional(), ], - choices = [ + choices=[ ('__EMPTY__', lazy_gettext('<< without value >>')), ('__ANY__', lazy_gettext('<< any value >>')) ], - filters = [lambda x: x or []], - description = lazy_gettext('Name of the detector that detected the event.') + filters=[lambda x: x or []], + description=lazy_gettext('Name of the detector that detected the event.') ) not_detectors = wtforms.BooleanField( lazy_gettext('Negate detector selection:'), - validators = [ + validators=[ wtforms.validators.Optional(), ] ) detector_types = wtforms.SelectMultipleField( lazy_gettext('Detector types:'), - validators = [ + validators=[ wtforms.validators.Optional() ], - choices = [ + choices=[ ('__EMPTY__', lazy_gettext('<< without value >>')), ('__ANY__', lazy_gettext('<< any value >>')) ], - filters = [lambda x: x or []], - description = lazy_gettext('Specification of event detector type. Each event detector may be optionally assigned one or more labels to better categorize that detector.') + filters=[lambda x: x or []], + description=lazy_gettext( + 'Specification of event detector type. Each event detector may be optionally assigned one or more labels to better categorize that detector.') ) not_detector_types = wtforms.BooleanField( lazy_gettext('Negate detector_type selection:'), - validators = [ + validators=[ wtforms.validators.Optional(), ] ) categories = wtforms.SelectMultipleField( lazy_gettext('Categories:'), - validators = [ + validators=[ wtforms.validators.Optional(), ], - choices = [ + choices=[ ('__EMPTY__', lazy_gettext('<< without value >>')), ('__ANY__', lazy_gettext('<< any value >>')) ], - filters = [lambda x: x or []], - description = lazy_gettext('Specification of event category. Each event may be optionally assigned one or more labels to better categorize that event, for example as "Recon.Scanning", "Abusive.Spam", "Test" etc.') + filters=[lambda x: x or []], + description=lazy_gettext( + 'Specification of event category. Each event may be optionally assigned one or more labels to better categorize that event, for example as "Recon.Scanning", "Abusive.Spam", "Test" etc.') ) not_categories = wtforms.BooleanField( lazy_gettext('Negate category selection:'), - validators = [ + validators=[ wtforms.validators.Optional(), ] ) severities = wtforms.SelectMultipleField( lazy_gettext('Severities:'), - validators = [ + validators=[ wtforms.validators.Optional(), ], - choices = [ + choices=[ ('__EMPTY__', lazy_gettext('<< without value >>')), ('__ANY__', lazy_gettext('<< any value >>')) ], - filters = [lambda x: x or []], - description = lazy_gettext('Specification of event severity. Each event may be optionally assigned one severity level, which can be then use during incident handling workflows to prioritize events.') + filters=[lambda x: x or []], + description=lazy_gettext( + 'Specification of event severity. Each event may be optionally assigned one severity level, which can be then use during incident handling workflows to prioritize events.') ) not_severities = wtforms.BooleanField( lazy_gettext('Negate severity selection:'), - validators = [ + validators=[ wtforms.validators.Optional(), ] ) classes = wtforms.SelectMultipleField( lazy_gettext('Classes:'), - validators = [ + validators=[ wtforms.validators.Optional(), ], - choices = [ + choices=[ ('__EMPTY__', lazy_gettext('<< without value >>')), ('__ANY__', lazy_gettext('<< any value >>')) ], - filters = [lambda x: x or []], - description = lazy_gettext('Specification of event class. Each event may be optionally assigned one class to better describe the event and group all similar events together for better processing. Event classification in internal feature of Mentat system for better event management.') + filters=[lambda x: x or []], + description=lazy_gettext( + 'Specification of event class. Each event may be optionally assigned one class to better describe the event and group all similar events together for better processing. Event classification in internal feature of Mentat system for better event management.') ) not_classess = wtforms.BooleanField( lazy_gettext('Negate class selection:'), - validators = [ + validators=[ wtforms.validators.Optional(), ] ) description = wtforms.StringField( lazy_gettext('Description:'), - validators = [ + validators=[ wtforms.validators.Optional() ], - description = lazy_gettext('Specification of event description. Each event may be optionally assigned short descriptive string.') + description=lazy_gettext( + 'Specification of event description. Each event may be optionally assigned short descriptive string.') ) protocols = wtforms.SelectMultipleField( lazy_gettext('Protocols:'), - validators = [ + validators=[ wtforms.validators.Optional(), ], - choices = [ + choices=[ ('__EMPTY__', lazy_gettext('<< without value >>')), ('__ANY__', lazy_gettext('<< any value >>')) ], - filters = [lambda x: x or []], - description = lazy_gettext('Specification of one or more communication protocols involved in the event.') + filters=[lambda x: x or []], + description=lazy_gettext('Specification of one or more communication protocols involved in the event.') ) not_protocols = wtforms.BooleanField( lazy_gettext('Negate protocol selection:'), - validators = [ + validators=[ wtforms.validators.Optional(), ] ) groups = QuerySelectMultipleField( lazy_gettext('Abuse group:'), - query_factory = get_available_groups, - allow_blank = False, - get_pk = lambda item: item.name, - description = lazy_gettext('Specification of the abuse group to whose constituency this event belongs based on one of the event source addresses.') + query_factory=get_available_groups, + allow_blank=False, + get_pk=lambda item: item.name, + description=lazy_gettext( + 'Specification of the abuse group to whose constituency this event belongs based on one of the event source addresses.') ) not_groups = wtforms.BooleanField( lazy_gettext('Negate group selection:'), - validators = [ + validators=[ wtforms.validators.Optional(), ] ) inspection_errs = wtforms.SelectMultipleField( lazy_gettext('Inspection errors:'), - validators = [ + validators=[ wtforms.validators.Optional(), ], - choices = [ + choices=[ ('__EMPTY__', lazy_gettext('<< without value >>')), ('__ANY__', lazy_gettext('<< any value >>')) ], - filters = [lambda x: x or []], - description = lazy_gettext('Specification of possible event errors detected during event inspection by real-time event processing inspection daemon.') + filters=[lambda x: x or []], + description=lazy_gettext( + 'Specification of possible event errors detected during event inspection by real-time event processing inspection daemon.') ) not_inspection_errs = wtforms.BooleanField( lazy_gettext('Negate inspection error selection:'), - validators = [ + validators=[ wtforms.validators.Optional(), ] ) sortby = wtforms.SelectField( lazy_gettext('Sort by:'), - validators = [ + validators=[ wtforms.validators.Optional() ], - choices = [ + choices=[ ('time.desc', lazy_gettext('by time descending')), - ('time.asc', lazy_gettext('by time ascending')), + ('time.asc', lazy_gettext('by time ascending')), ('detecttime.desc', lazy_gettext('by detection time descending')), - ('detecttime.asc', lazy_gettext('by detection time ascending')), + ('detecttime.asc', lazy_gettext('by detection time ascending')), ('storagetime.desc', lazy_gettext('by storage time descending')), - ('storagetime.asc', lazy_gettext('by storage time ascending')) + ('storagetime.asc', lazy_gettext('by storage time ascending')) ], - default = 'time.desc' + default='time.desc' ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.source_types.choices = kwargs['choices_source_types'] - self.target_types.choices = kwargs['choices_target_types'] - self.host_types.choices = kwargs['choices_host_types'] + self.source_types.choices = kwargs['choices_source_types'] + self.target_types.choices = kwargs['choices_target_types'] + self.host_types.choices = kwargs['choices_host_types'] - self.detectors.choices[2:] = kwargs['choices_detectors'] - self.detector_types.choices[2:] = kwargs['choices_detector_types'] - self.categories.choices[2:] = kwargs['choices_categories'] - self.severities.choices[2:] = kwargs['choices_severities'] - self.classes.choices[2:] = kwargs['choices_classes'] - self.protocols.choices[2:] = kwargs['choices_protocols'] + self.detectors.choices[2:] = kwargs['choices_detectors'] + self.detector_types.choices[2:] = kwargs['choices_detector_types'] + self.categories.choices[2:] = kwargs['choices_categories'] + self.severities.choices[2:] = kwargs['choices_severities'] + self.classes.choices[2:] = kwargs['choices_classes'] + self.protocols.choices[2:] = kwargs['choices_protocols'] self.inspection_errs.choices[2:] = kwargs['choices_inspection_errs'] @staticmethod @@ -337,7 +355,11 @@ class SimpleEventSearchForm(vial.forms.BaseSearchForm): :return: ``True``, if the field can contain multiple values, ``False`` otherwise. :rtype: bool """ - if field_name in ('source_addrs', 'target_addrs', 'host_addrs', 'source_ports', 'target_ports', 'host_ports', 'source_types', 'target_types', 'host_types', 'detectors', 'detector_types', 'categories', 'severities', 'classess', 'protocols', 'groups', 'inspection_errs'): + if field_name in ( + 'source_addrs', 'target_addrs', 'host_addrs', 'source_ports', 'target_ports', 'host_ports', 'source_types', + 'target_types', 'host_types', 'detectors', 'detector_types', 'categories', 'severities', 'classess', + 'protocols', 'groups', 'inspection_errs' + ): return True return False @@ -346,21 +368,23 @@ class EventDashboardForm(flask_wtf.FlaskForm): """ Class representing event dashboard search form. """ - dt_from = vial.forms.SmartDateTimeField( + dt_from = hawat.forms.SmartDateTimeField( lazy_gettext('From:'), - validators = [ + validators=[ wtforms.validators.Optional() ], - description = lazy_gettext('Lower time boundary for event detection time as provided by event detector. Timestamp is expected to be in the format <code>YYYY-MM-DD hh:mm:ss</code> and in the timezone according to the user`s preferences. Event detectors are usually outside of the control of Mentat system administrators and may sometimes emit events with invalid detection times, for example timestamps in the future.'), - default = lambda: vial.forms.default_dt_with_delta(hawat.const.DEFAULT_RESULT_TIMEDELTA) + description=lazy_gettext( + 'Lower time boundary for event detection time as provided by event detector. Timestamp is expected to be in the format <code>YYYY-MM-DD hh:mm:ss</code> and in the timezone according to the user`s preferences. Event detectors are usually outside of the control of Mentat system administrators and may sometimes emit events with invalid detection times, for example timestamps in the future.'), + default=lambda: hawat.forms.default_dt_with_delta(hawat.const.DEFAULT_RESULT_TIMEDELTA) ) - dt_to = vial.forms.SmartDateTimeField( + dt_to = hawat.forms.SmartDateTimeField( lazy_gettext('To:'), - validators = [ + validators=[ wtforms.validators.Optional() ], - description = lazy_gettext('Upper time boundary for event detection time as provided by event detector. Timestamp is expected to be in the format <code>YYYY-MM-DD hh:mm:ss</code> and in the timezone according to the user`s preferences. Event detectors are usually outside of the control of Mentat system administrators and may sometimes emit events with invalid detection times, for example timestamps in the future.'), - default = vial.forms.default_dt + description=lazy_gettext( + 'Upper time boundary for event detection time as provided by event detector. Timestamp is expected to be in the format <code>YYYY-MM-DD hh:mm:ss</code> and in the timezone according to the user`s preferences. Event detectors are usually outside of the control of Mentat system administrators and may sometimes emit events with invalid detection times, for example timestamps in the future.'), + default=hawat.forms.default_dt ) submit = wtforms.SubmitField( lazy_gettext('Search') diff --git a/lib/hawat/blueprints/events/templates/events/dashboard.html b/lib/hawat/blueprints/events/templates/events/dashboard.html index b283393a4873206454613aaf881e37dde2b50c14..3a4816fd9ef8c74a4e36e8201332ff6d5b5beccc 100644 --- a/lib/hawat/blueprints/events/templates/events/dashboard.html +++ b/lib/hawat/blueprints/events/templates/events/dashboard.html @@ -9,7 +9,7 @@ <!-- Search form - BEGIN ----------------------------------> <div class="jumbotron" style="margin-top: 1em;"> - <h2>{{ vial_current_view.get_view_title() }}</h2> + <h2>{{ hawat_current_view.get_view_title() }}</h2> <hr> <form method="GET" class="form" action="{{ url_for(request.endpoint) }}"> <div class="row"> diff --git a/lib/hawat/blueprints/events/templates/events/show.html b/lib/hawat/blueprints/events/templates/events/show.html index 91dabd4f495c4ae6f2644ec1d9e3db54b169d591..274733f0bd4b31663ea2819033f6e0b552d0276d 100644 --- a/lib/hawat/blueprints/events/templates/events/show.html +++ b/lib/hawat/blueprints/events/templates/events/show.html @@ -17,7 +17,7 @@ </li> </ol> <h2> - {{ vial_current_view.get_view_title() }} + {{ hawat_current_view.get_view_title() }} </h2> <hr> <h3> diff --git a/lib/hawat/blueprints/events/test/__init__.py b/lib/hawat/blueprints/events/test/__init__.py index 77727a9065328a0e5d34f90a3e91f4c23e57bb40..e34c1e5ed2525b5f4d72fbadd8ab92a5020a9f61 100644 --- a/lib/hawat/blueprints/events/test/__init__.py +++ b/lib/hawat/blueprints/events/test/__init__.py @@ -15,14 +15,14 @@ Unit tests for :py:mod:`hawat.blueprints.events`. import unittest -import vial.const -import vial.test -import vial.db -from vial.test import VialTestCase +import hawat.const +import hawat.test +import hawat.db +from hawat.test import HawatTestCase from hawat.test.runner import TestRunnerMixin -class SearchTestCase(TestRunnerMixin, VialTestCase): +class SearchTestCase(TestRunnerMixin, HawatTestCase): """ Class for testing ``events.search`` endpoint. """ @@ -51,28 +51,28 @@ class SearchTestCase(TestRunnerMixin, VialTestCase): """Test access as anonymous user.""" self._attempt_fail_redirect() - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_USER) def test_02_as_user(self): """Test access as user ``user``.""" self._attempt_succeed() - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_DEVELOPER) def test_03_as_developer(self): """Test access as user ``developer``.""" self._attempt_succeed() - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_MAINTAINER) def test_04_as_maintainer(self): """Test access as user ``maintainer``.""" self._attempt_succeed() - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_ADMIN) def test_05_as_admin(self): """Test access as user ``admin``.""" self._attempt_succeed() -class DashboardTestCase(TestRunnerMixin, VialTestCase): +class DashboardTestCase(TestRunnerMixin, HawatTestCase): """ Class for testing ``events.dashboard`` endpoint. """ @@ -101,22 +101,22 @@ class DashboardTestCase(TestRunnerMixin, VialTestCase): """Test access as anonymous user.""" self._attempt_fail_redirect() - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_USER) def test_02_as_user(self): """Test access as user ``user``.""" self._attempt_succeed() - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_DEVELOPER) def test_03_as_developer(self): """Test access as user ``developer``.""" self._attempt_succeed() - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_MAINTAINER) def test_04_as_maintainer(self): """Test access as user ``maintainer``.""" self._attempt_succeed() - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_ADMIN) def test_05_as_admin(self): """Test access as user ``admin``.""" self._attempt_succeed() diff --git a/lib/hawat/blueprints/filters/__init__.py b/lib/hawat/blueprints/filters/__init__.py index 3b95f4f0956c66ec9993c141751f5a4715dcd769..e2b2604910cb594efc9e16957f25e2d123f7c624 100644 --- a/lib/hawat/blueprints/filters/__init__.py +++ b/lib/hawat/blueprints/filters/__init__.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ @@ -19,13 +19,12 @@ features include: * deleting existing reporting filters """ - __author__ = "Jan Mach <jan.mach@cesnet.cz>" __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" - import sys import traceback +import markupsafe import flask import flask_login @@ -45,22 +44,23 @@ from mentat.const import REPORTING_FILTER_BASIC from mentat.datatype.sqldb import FilterModel, GroupModel, ItemChangeLogModel from mentat.idea.internal import Idea, IDEAFilterCompiler -import vial.const -import vial.db -from vial.app import VialBlueprint -from vial.view import RenderableView, ItemListView, ItemShowView, ItemCreateView, ItemCreateForView, ItemUpdateView, ItemDeleteView, ItemEnableView, ItemDisableView -from vial.view.mixin import HTMLMixin, SQLAlchemyMixin +import hawat.const +import hawat.db +import hawat.acl +import hawat.menu +from hawat.base import HawatBlueprint +from hawat.view import RenderableView, ItemListView, ItemShowView, ItemCreateView, ItemCreateForView, ItemUpdateView, \ + ItemDeleteView, ItemEnableView, ItemDisableView +from hawat.view.mixin import HTMLMixin, SQLAlchemyMixin import hawat.events from hawat.blueprints.filters.forms import BaseFilterForm, AdminFilterForm, PlaygroundFilterForm, FilterSearchForm - _PARSER = PynspectFilterParser() _PARSER.build() _COMPILER = IDEAFilterCompiler() _FILTER = DataObjectFilter() - BLUEPRINT_NAME = 'filters' """Name of the blueprint as module global constant.""" @@ -103,6 +103,7 @@ def to_tree(rule): return _PARSER.parse(rule) return None + def tree_compile(rule_tree): """ Compile given filtering rule tree. @@ -111,6 +112,7 @@ def tree_compile(rule_tree): return _COMPILER.compile(rule_tree) return None + def tree_html(rule_tree): """ Render given rule object tree to HTML formatted content. @@ -119,6 +121,7 @@ def tree_html(rule_tree): return rule_tree.traverse(pynspect.traversers.HTMLTreeTraverser()) return None + def tree_check(rule_tree, data): """ Check given event against given rule tree. @@ -134,13 +137,13 @@ class ListView(HTMLMixin, SQLAlchemyMixin, ItemListView): authentication = True - authorization = [vial.acl.PERMISSION_POWER] + authorization = [hawat.acl.PERMISSION_POWER] @classmethod def get_view_title(cls, **kwargs): return lazy_gettext('Filter management') - #--------------------------------------------------------------------------- + # --------------------------------------------------------------------------- @property def dbmodel(self): @@ -148,53 +151,53 @@ class ListView(HTMLMixin, SQLAlchemyMixin, ItemListView): @classmethod def get_action_menu(cls): - action_menu = vial.menu.Menu() + action_menu = hawat.menu.Menu() action_menu.add_entry( 'endpoint', 'create', - endpoint = 'filters.create', - resptitle = True + endpoint='filters.create', + resptitle=True ) action_menu.add_entry( 'endpoint', 'playground', - endpoint = 'filters.playground', - resptitle = True + endpoint='filters.playground', + resptitle=True ) return action_menu @classmethod def get_context_action_menu(cls): - action_menu = vial.menu.Menu() + action_menu = hawat.menu.Menu() action_menu.add_entry( 'endpoint', 'show', - endpoint = 'filters.show', - hidetitle = True + endpoint='filters.show', + hidetitle=True ) action_menu.add_entry( 'endpoint', 'update', - endpoint = 'filters.update', - hidetitle = True + endpoint='filters.update', + hidetitle=True ) action_menu.add_entry( 'endpoint', 'disable', - endpoint = 'filters.disable', - hidetitle = True + endpoint='filters.disable', + hidetitle=True ) action_menu.add_entry( 'endpoint', 'enable', - endpoint = 'filters.enable', - hidetitle = True + endpoint='filters.enable', + hidetitle=True ) action_menu.add_entry( 'endpoint', 'delete', - endpoint = 'filters.delete', - hidetitle = True + endpoint='filters.delete', + hidetitle=True ) return action_menu @@ -206,14 +209,14 @@ class ListView(HTMLMixin, SQLAlchemyMixin, ItemListView): """ return FilterSearchForm( request_args, - meta = {'csrf': False} + meta={'csrf': False} ) @staticmethod def build_query(query, model, form_args): # Adjust query based on text search string. if 'search' in form_args and form_args['search']: - query = query\ + query = query \ .filter( or_( model.name.like('%{}%'.format(form_args['search'])), @@ -238,7 +241,7 @@ class ListView(HTMLMixin, SQLAlchemyMixin, ItemListView): query = query.filter(model.type == form_args['type']) # Adjust query based on user membership selection. if 'group' in form_args and form_args['group']: - query = query\ + query = query \ .filter(model.group_id == form_args['group'].id) if 'sortby' in form_args and form_args['sortby']: sortmap = { @@ -267,7 +270,7 @@ class ShowView(HTMLMixin, SQLAlchemyMixin, ItemShowView): def get_menu_legend(cls, **kwargs): return lazy_gettext( 'View details of reporting filter "%(item)s"', - item = flask.escape(kwargs['item'].name) + item=markupsafe.escape(kwargs['item'].name) ) @classmethod @@ -281,38 +284,38 @@ class ShowView(HTMLMixin, SQLAlchemyMixin, ItemShowView): @classmethod def authorize_item_action(cls, **kwargs): permission_mm = flask_principal.Permission( - vial.acl.MembershipNeed(kwargs['item'].group.id), - vial.acl.ManagementNeed(kwargs['item'].group.id) + hawat.acl.MembershipNeed(kwargs['item'].group.id), + hawat.acl.ManagementNeed(kwargs['item'].group.id) ) - return vial.acl.PERMISSION_POWER.can() or permission_mm.can() + return hawat.acl.PERMISSION_POWER.can() or permission_mm.can() @classmethod def get_action_menu(cls): - action_menu = vial.menu.Menu() + action_menu = hawat.menu.Menu() action_menu.add_entry( 'endpoint', 'update', - endpoint = 'filters.update', + endpoint='filters.update', ) action_menu.add_entry( 'endpoint', 'disable', - endpoint = 'filters.disable', + endpoint='filters.disable', ) action_menu.add_entry( 'endpoint', 'enable', - endpoint = 'filters.enable', + endpoint='filters.enable', ) action_menu.add_entry( 'endpoint', 'delete', - endpoint = 'filters.delete', + endpoint='filters.delete', ) action_menu.add_entry( 'endpoint', 'playground', - endpoint = 'filters.playground', + endpoint='filters.playground', ) return action_menu @@ -321,33 +324,33 @@ class ShowView(HTMLMixin, SQLAlchemyMixin, ItemShowView): filter_tree = to_tree(item.filter) filter_compiled = tree_compile(filter_tree) self.response_context.update( - filter_tree = filter_tree, - filter_compiled = filter_compiled, - filter_preview = tree_html(filter_tree), - filter_compiled_preview = tree_html(filter_compiled) + filter_tree=filter_tree, + filter_compiled=filter_compiled, + filter_preview=tree_html(filter_tree), + filter_compiled_preview=tree_html(filter_compiled) ) - if self.can_access_endpoint('filters.update', item = item) and self.has_endpoint('changelogs.search'): + if self.can_access_endpoint('filters.update', item=item) and self.has_endpoint('changelogs.search'): self.response_context.update( - context_action_menu_changelogs = self.get_endpoint_class( + context_action_menu_changelogs=self.get_endpoint_class( 'changelogs.search' ).get_context_action_menu() ) - item_changelog = self.dbsession.query(ItemChangeLogModel).\ - filter(ItemChangeLogModel.model == item.__class__.__name__).\ - filter(ItemChangeLogModel.model_id == item.id).\ - order_by(ItemChangeLogModel.createtime.desc()).\ - limit(100).\ + item_changelog = self.dbsession.query(ItemChangeLogModel). \ + filter(ItemChangeLogModel.model == item.__class__.__name__). \ + filter(ItemChangeLogModel.model_id == item.id). \ + order_by(ItemChangeLogModel.createtime.desc()). \ + limit(100). \ all() - self.response_context.update(item_changelog = item_changelog) + self.response_context.update(item_changelog=item_changelog) class CreateView(HTMLMixin, SQLAlchemyMixin, ItemCreateView): # pylint: disable=locally-disabled,too-many-ancestors """ View for creating new reporting filters for any groups. """ - methods = ['GET','POST'] + methods = ['GET', 'POST'] authentication = True @@ -369,38 +372,38 @@ class CreateView(HTMLMixin, SQLAlchemyMixin, ItemCreateView): # pylint: disable @classmethod def authorize_item_action(cls, **kwargs): - return vial.acl.PERMISSION_POWER.can() + return hawat.acl.PERMISSION_POWER.can() @staticmethod def get_message_success(**kwargs): return gettext( 'Reporting filter <strong>%(item_id)s</strong> for group <strong>%(parent_id)s</strong> was successfully created.', - item_id = flask.escape(str(kwargs['item'])), - parent_id = flask.escape(str(kwargs['item'].group)) + item_id=markupsafe.escape(str(kwargs['item'])), + parent_id=markupsafe.escape(str(kwargs['item'].group)) ) @staticmethod def get_message_failure(**kwargs): return gettext( 'Unable to create new reporting filter for group <strong>%(parent_id)s</strong>.', - parent_id = flask.escape(str(kwargs['item'].group)) + parent_id=markupsafe.escape(str(kwargs['item'].group)) ) @staticmethod def get_message_cancel(**kwargs): return gettext( 'Canceled creating new reporting filter for group <strong>%(parent_id)s</strong>.', - parent_id = flask.escape(str(kwargs['item'].group)) + parent_id=markupsafe.escape(str(kwargs['item'].group)) ) @staticmethod def get_item_form(item): - detectors = hawat.events.get_event_detectors() + detectors = hawat.events.get_event_detectors() categories = hawat.events.get_event_categories() return AdminFilterForm( - choices_detectors = list(zip(detectors,detectors)), - choices_categories = list(zip(categories,categories)) + choices_detectors=list(zip(detectors, detectors)), + choices_categories=list(zip(categories, categories)) ) def do_before_action(self, item): # pylint: disable=locally-disabled,no-self-use,unused-argument @@ -411,16 +414,17 @@ class CreateView(HTMLMixin, SQLAlchemyMixin, ItemCreateView): # pylint: disable if item: filter_tree = to_tree(item.filter) self.response_context.update( - filter_tree = filter_tree, - filter_preview = tree_html(filter_tree) + filter_tree=filter_tree, + filter_preview=tree_html(filter_tree) ) -class CreateForView(HTMLMixin, SQLAlchemyMixin, ItemCreateForView): # pylint: disable=locally-disabled,too-many-ancestors +class CreateForView(HTMLMixin, SQLAlchemyMixin, + ItemCreateForView): # pylint: disable=locally-disabled,too-many-ancestors """ View for creating new reporting filters for given groups. """ - methods = ['GET','POST'] + methods = ['GET', 'POST'] authentication = True @@ -438,14 +442,14 @@ class CreateForView(HTMLMixin, SQLAlchemyMixin, ItemCreateForView): # pylint: d def get_menu_legend(cls, **kwargs): return lazy_gettext( 'Create reporting filter for group "%(item)s"', - item = flask.escape(str(kwargs['item'])) + item=markupsafe.escape(str(kwargs['item'])) ) @classmethod def get_view_url(cls, **kwargs): return flask.url_for( cls.get_view_endpoint(), - parent_id = kwargs['item'].id + parent_id=kwargs['item'].id ) @classmethod @@ -467,40 +471,40 @@ class CreateForView(HTMLMixin, SQLAlchemyMixin, ItemCreateForView): # pylint: d @classmethod def authorize_item_action(cls, **kwargs): permission_m = flask_principal.Permission( - vial.acl.ManagementNeed(kwargs['item'].id) + hawat.acl.ManagementNeed(kwargs['item'].id) ) - return vial.acl.PERMISSION_POWER.can() or permission_m.can() + return hawat.acl.PERMISSION_POWER.can() or permission_m.can() @staticmethod def get_message_success(**kwargs): return gettext( 'Reporting filter <strong>%(item_id)s</strong> for group <strong>%(parent_id)s</strong> was successfully created.', - item_id = flask.escape(str(kwargs['item'])), - parent_id = flask.escape(str(kwargs['parent'])) + item_id=markupsafe.escape(str(kwargs['item'])), + parent_id=markupsafe.escape(str(kwargs['parent'])) ) @staticmethod def get_message_failure(**kwargs): return gettext( 'Unable to create new reporting filter for group <strong>%(parent_id)s</strong>.', - parent_id = flask.escape(str(kwargs['parent'])) + parent_id=markupsafe.escape(str(kwargs['parent'])) ) @staticmethod def get_message_cancel(**kwargs): return gettext( 'Canceled creating new reporting filter for group <strong>%(parent_id)s</strong>.', - parent_id = flask.escape(str(kwargs['parent'])) + parent_id=markupsafe.escape(str(kwargs['parent'])) ) @staticmethod def get_item_form(item): - detectors = hawat.events.get_event_detectors() + detectors = hawat.events.get_event_detectors() categories = hawat.events.get_event_categories() return BaseFilterForm( - choices_detectors = list(zip(detectors,detectors)), - choices_categories = list(zip(categories,categories)) + choices_detectors=list(zip(detectors, detectors)), + choices_categories=list(zip(categories, categories)) ) @staticmethod @@ -515,8 +519,8 @@ class CreateForView(HTMLMixin, SQLAlchemyMixin, ItemCreateForView): # pylint: d if item: filter_tree = to_tree(item.filter) self.response_context.update( - filter_tree = filter_tree, - filter_preview = tree_html(filter_tree) + filter_tree=filter_tree, + filter_preview=tree_html(filter_tree) ) @@ -524,7 +528,7 @@ class UpdateView(HTMLMixin, SQLAlchemyMixin, ItemUpdateView): # pylint: disable """ View for updating existing reporting filters. """ - methods = ['GET','POST'] + methods = ['GET', 'POST'] authentication = True @@ -532,7 +536,7 @@ class UpdateView(HTMLMixin, SQLAlchemyMixin, ItemUpdateView): # pylint: disable def get_menu_legend(cls, **kwargs): return lazy_gettext( 'Update details of reporting filter "%(item)s"', - item = flask.escape(kwargs['item'].name) + item=markupsafe.escape(kwargs['item'].name) ) @classmethod @@ -550,51 +554,51 @@ class UpdateView(HTMLMixin, SQLAlchemyMixin, ItemUpdateView): # pylint: disable @classmethod def authorize_item_action(cls, **kwargs): permission_m = flask_principal.Permission( - vial.acl.ManagementNeed(kwargs['item'].group.id) + hawat.acl.ManagementNeed(kwargs['item'].group.id) ) - return vial.acl.PERMISSION_POWER.can() or permission_m.can() + return hawat.acl.PERMISSION_POWER.can() or permission_m.can() @staticmethod def get_message_success(**kwargs): return gettext( 'Reporting filter <strong>%(item_id)s</strong> for group <strong>%(parent_id)s</strong> was successfully updated.', - item_id = flask.escape(str(kwargs['item'])), - parent_id = flask.escape(str(kwargs['item'].group)) + item_id=markupsafe.escape(str(kwargs['item'])), + parent_id=markupsafe.escape(str(kwargs['item'].group)) ) @staticmethod def get_message_failure(**kwargs): return gettext( 'Unable to update reporting filter <strong>%(item_id)s</strong> for group <strong>%(parent_id)s</strong>.', - item_id = flask.escape(str(kwargs['item'])), - parent_id = flask.escape(str(kwargs['item'].group)) + item_id=markupsafe.escape(str(kwargs['item'])), + parent_id=markupsafe.escape(str(kwargs['item'].group)) ) @staticmethod def get_message_cancel(**kwargs): return gettext( 'Canceled updating reporting filter <strong>%(item_id)s</strong> for group <strong>%(parent_id)s</strong>.', - item_id = flask.escape(str(kwargs['item'])), - parent_id = flask.escape(str(kwargs['item'].group)) + item_id=markupsafe.escape(str(kwargs['item'])), + parent_id=markupsafe.escape(str(kwargs['item'].group)) ) @staticmethod def get_item_form(item): - detectors = hawat.events.get_event_detectors() + detectors = hawat.events.get_event_detectors() categories = hawat.events.get_event_categories() admin = flask_login.current_user.has_role('admin') if not admin: return BaseFilterForm( - obj = item, - choices_detectors = list(zip(detectors,detectors)), - choices_categories = list(zip(categories,categories)) + obj=item, + choices_detectors=list(zip(detectors, detectors)), + choices_categories=list(zip(categories, categories)) ) return AdminFilterForm( - obj = item, - choices_detectors = list(zip(detectors,detectors)), - choices_categories = list(zip(categories,categories)) + obj=item, + choices_detectors=list(zip(detectors, detectors)), + choices_categories=list(zip(categories, categories)) ) def do_before_action(self, item): # pylint: disable=locally-disabled,no-self-use,unused-argument @@ -604,8 +608,8 @@ class UpdateView(HTMLMixin, SQLAlchemyMixin, ItemUpdateView): # pylint: disable item = self.response_context['item'] filter_tree = to_tree(item.filter) self.response_context.update( - filter_tree = filter_tree, - filter_preview = tree_html(filter_tree) + filter_tree=filter_tree, + filter_preview=tree_html(filter_tree) ) @@ -613,7 +617,7 @@ class EnableView(HTMLMixin, SQLAlchemyMixin, ItemEnableView): # pylint: disable """ View for enabling existing reporting filters. """ - methods = ['GET','POST'] + methods = ['GET', 'POST'] authentication = True @@ -621,7 +625,7 @@ class EnableView(HTMLMixin, SQLAlchemyMixin, ItemEnableView): # pylint: disable def get_menu_legend(cls, **kwargs): return lazy_gettext( 'Enable reporting filter "%(item)s"', - item = flask.escape(kwargs['item'].name) + item=markupsafe.escape(kwargs['item'].name) ) @property @@ -635,32 +639,32 @@ class EnableView(HTMLMixin, SQLAlchemyMixin, ItemEnableView): # pylint: disable @classmethod def authorize_item_action(cls, **kwargs): permission_m = flask_principal.Permission( - vial.acl.ManagementNeed(kwargs['item'].group.id) + hawat.acl.ManagementNeed(kwargs['item'].group.id) ) - return vial.acl.PERMISSION_POWER.can() or permission_m.can() + return hawat.acl.PERMISSION_POWER.can() or permission_m.can() @staticmethod def get_message_success(**kwargs): return gettext( 'Reporting filter <strong>%(item_id)s</strong> for group <strong>%(parent_id)s</strong> was successfully enabled.', - item_id = flask.escape(str(kwargs['item'])), - parent_id = flask.escape(str(kwargs['item'].group)) + item_id=markupsafe.escape(str(kwargs['item'])), + parent_id=markupsafe.escape(str(kwargs['item'].group)) ) @staticmethod def get_message_failure(**kwargs): return gettext( 'Unable to enable reporting filter <strong>%(item_id)s</strong> for group <strong>%(parent_id)s</strong>.', - item_id = flask.escape(str(kwargs['item'])), - parent_id = flask.escape(str(kwargs['item'].group)) + item_id=markupsafe.escape(str(kwargs['item'])), + parent_id=markupsafe.escape(str(kwargs['item'].group)) ) @staticmethod def get_message_cancel(**kwargs): return gettext( 'Canceled enabling reporting filter <strong>%(item_id)s</strong> for group <strong>%(parent_id)s</strong>.', - item_id = flask.escape(str(kwargs['item'])), - parent_id = flask.escape(str(kwargs['item'].group)) + item_id=markupsafe.escape(str(kwargs['item'])), + parent_id=markupsafe.escape(str(kwargs['item'].group)) ) @@ -668,7 +672,7 @@ class DisableView(HTMLMixin, SQLAlchemyMixin, ItemDisableView): # pylint: disab """ View for disabling existing reporting filters. """ - methods = ['GET','POST'] + methods = ['GET', 'POST'] authentication = True @@ -676,10 +680,10 @@ class DisableView(HTMLMixin, SQLAlchemyMixin, ItemDisableView): # pylint: disab def get_menu_legend(cls, **kwargs): return lazy_gettext( 'Disable reporting filter "%(item)s"', - item = flask.escape(kwargs['item'].name) + item=markupsafe.escape(kwargs['item'].name) ) - #--------------------------------------------------------------------------- + # --------------------------------------------------------------------------- @property def dbmodel(self): @@ -692,32 +696,32 @@ class DisableView(HTMLMixin, SQLAlchemyMixin, ItemDisableView): # pylint: disab @classmethod def authorize_item_action(cls, **kwargs): permission_m = flask_principal.Permission( - vial.acl.ManagementNeed(kwargs['item'].group.id) + hawat.acl.ManagementNeed(kwargs['item'].group.id) ) - return vial.acl.PERMISSION_POWER.can() or permission_m.can() + return hawat.acl.PERMISSION_POWER.can() or permission_m.can() @staticmethod def get_message_success(**kwargs): return gettext( 'Reporting filter <strong>%(item_id)s</strong> for group <strong>%(parent_id)s</strong> was successfully disabled.', - item_id = flask.escape(str(kwargs['item'])), - parent_id = flask.escape(str(kwargs['item'].group)) + item_id=markupsafe.escape(str(kwargs['item'])), + parent_id=markupsafe.escape(str(kwargs['item'].group)) ) @staticmethod def get_message_failure(**kwargs): return gettext( 'Unable to disable reporting filter <strong>%(item_id)s</strong> for group <strong>%(parent_id)s</strong>.', - item_id = flask.escape(str(kwargs['item'])), - parent_id = flask.escape(str(kwargs['item'].group)) + item_id=markupsafe.escape(str(kwargs['item'])), + parent_id=markupsafe.escape(str(kwargs['item'].group)) ) @staticmethod def get_message_cancel(**kwargs): return gettext( 'Canceled disabling reporting filter <strong>%(item_id)s</strong> for group <strong>%(parent_id)s</strong>.', - item_id = flask.escape(str(kwargs['item'])), - parent_id = flask.escape(str(kwargs['item'].group)) + item_id=markupsafe.escape(str(kwargs['item'])), + parent_id=markupsafe.escape(str(kwargs['item'].group)) ) @@ -725,7 +729,7 @@ class DeleteView(HTMLMixin, SQLAlchemyMixin, ItemDeleteView): # pylint: disable """ View for deleting existing reporting filters. """ - methods = ['GET','POST'] + methods = ['GET', 'POST'] authentication = True @@ -733,7 +737,7 @@ class DeleteView(HTMLMixin, SQLAlchemyMixin, ItemDeleteView): # pylint: disable def get_menu_legend(cls, **kwargs): return lazy_gettext( 'Delete reporting filter "%(item)s"', - item = flask.escape(kwargs['item'].name) + item=markupsafe.escape(kwargs['item'].name) ) @property @@ -747,32 +751,32 @@ class DeleteView(HTMLMixin, SQLAlchemyMixin, ItemDeleteView): # pylint: disable @classmethod def authorize_item_action(cls, **kwargs): permission_m = flask_principal.Permission( - vial.acl.ManagementNeed(kwargs['item'].group.id) + hawat.acl.ManagementNeed(kwargs['item'].group.id) ) - return vial.acl.PERMISSION_POWER.can() or permission_m.can() + return hawat.acl.PERMISSION_POWER.can() or permission_m.can() @staticmethod def get_message_success(**kwargs): return gettext( 'Reporting filter <strong>%(item_id)s</strong> for group <strong>%(parent_id)s</strong> was successfully and permanently deleted.', - item_id = flask.escape(str(kwargs['item'])), - parent_id = flask.escape(str(kwargs['item'].group)) + item_id=markupsafe.escape(str(kwargs['item'])), + parent_id=markupsafe.escape(str(kwargs['item'].group)) ) @staticmethod def get_message_failure(**kwargs): return gettext( 'Unable to permanently delete reporting filter <strong>%(item_id)s</strong> for group <strong>%(parent_id)s</strong>.', - item_id = flask.escape(str(kwargs['item'])), - parent_id = flask.escape(str(kwargs['item'].group)) + item_id=markupsafe.escape(str(kwargs['item'])), + parent_id=markupsafe.escape(str(kwargs['item'].group)) ) @staticmethod def get_message_cancel(**kwargs): return gettext( 'Canceled deleting reporting filter <strong>%(item_id)s</strong> for group <strong>%(parent_id)s</strong>.', - item_id = flask.escape(str(kwargs['item'])), - parent_id = flask.escape(str(kwargs['item'].group)) + item_id=markupsafe.escape(str(kwargs['item'])), + parent_id=markupsafe.escape(str(kwargs['item'].group)) ) @@ -780,7 +784,7 @@ class PlaygroundView(HTMLMixin, RenderableView): """ Reporting filter playground view. """ - methods = ['GET','POST'] + methods = ['GET', 'POST'] authentication = True @@ -806,21 +810,21 @@ class PlaygroundView(HTMLMixin, RenderableView): @classmethod def get_breadcrumbs_menu(cls): - breadcrumbs_menu = vial.menu.Menu() + breadcrumbs_menu = hawat.menu.Menu() breadcrumbs_menu.add_entry( 'endpoint', 'home', - endpoint = flask.current_app.config['ENDPOINT_HOME'] + endpoint=flask.current_app.config['ENDPOINT_HOME'] ) breadcrumbs_menu.add_entry( 'endpoint', 'list', - endpoint = '{}.{}'.format(cls.module_name, 'list') + endpoint='{}.{}'.format(cls.module_name, 'list') ) breadcrumbs_menu.add_entry( 'endpoint', 'playground', - endpoint = cls.get_view_endpoint() + endpoint=cls.get_view_endpoint() ) return breadcrumbs_menu @@ -835,51 +839,50 @@ class PlaygroundView(HTMLMixin, RenderableView): form_data = form.data try: - event = Idea.from_json(form.event.data) - filter_tree = to_tree(form.filter.data) - filter_preview = tree_html(filter_tree) - filter_compiled = tree_compile(filter_tree) + event = Idea.from_json(form.event.data) + filter_tree = to_tree(form.filter.data) + filter_preview = tree_html(filter_tree) + filter_compiled = tree_compile(filter_tree) filter_compiled_preview = tree_html(filter_compiled) - filter_result = tree_check(filter_compiled, event) + filter_result = tree_check(filter_compiled, event) self.response_context.update( - form_data = form_data, - event = event, - filter_tree = filter_tree, - filter_preview = filter_preview, - filter_compiled = filter_compiled, - filter_compiled_preview = filter_compiled_preview, - filter_result = filter_result, - flag_filtered = True + form_data=form_data, + event=event, + filter_tree=filter_tree, + filter_preview=filter_preview, + filter_compiled=filter_compiled, + filter_compiled_preview=filter_compiled_preview, + filter_result=filter_result, + flag_filtered=True ) except Exception as err: # pylint: disable=locally-disabled,broad-except self.flash( flask.Markup(gettext( '<strong>%(error)s</strong>.', - error = str(err) + error=str(err) )), - vial.const.FLASH_FAILURE + hawat.const.FLASH_FAILURE ) tbexc = traceback.TracebackException(*sys.exc_info()) self.response_context.update( - filter_exception = err, - filter_exception_tb = ''.join(tbexc.format()) + filter_exception=err, + filter_exception_tb=''.join(tbexc.format()) ) - self.response_context.update( - form_url = flask.url_for(self.get_view_endpoint()), - form = form, + form_url=flask.url_for(self.get_view_endpoint()), + form=form, ) return self.generate_response() -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- -class FiltersBlueprint(VialBlueprint): +class FiltersBlueprint(HawatBlueprint): """Pluggable module - reporting filter management (*filters*).""" @classmethod @@ -890,43 +893,43 @@ class FiltersBlueprint(VialBlueprint): app.menu_main.add_entry( 'view', 'admin.{}'.format(BLUEPRINT_NAME), - position = 60, - view = ListView + position=60, + view=ListView ) app.menu_main.add_entry( 'view', 'more.{}_playground'.format(BLUEPRINT_NAME), - position = 1, - group = lazy_gettext('Tools'), - view = PlaygroundView + position=1, + group=lazy_gettext('Tools'), + view=PlaygroundView ) -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- def get_blueprint(): """ - Mandatory interface for :py:mod:`vial.Vial` and factory function. This function - must return a valid instance of :py:class:`vial.app.VialBlueprint` or + Mandatory interface for :py:mod:`hawat.Hawat` and factory function. This function + must return a valid instance of :py:class:`hawat.app.HawatBlueprint` or :py:class:`flask.Blueprint`. """ hbp = FiltersBlueprint( BLUEPRINT_NAME, __name__, - template_folder = 'templates', - url_prefix = '/{}'.format(BLUEPRINT_NAME) + template_folder='templates', + url_prefix='/{}'.format(BLUEPRINT_NAME) ) - hbp.register_view_class(ListView, '/list') - hbp.register_view_class(CreateView, '/create') - hbp.register_view_class(CreateForView, '/createfor/<int:parent_id>') - hbp.register_view_class(ShowView, '/<int:item_id>/show') - hbp.register_view_class(UpdateView, '/<int:item_id>/update') - hbp.register_view_class(EnableView, '/<int:item_id>/enable') - hbp.register_view_class(DisableView, '/<int:item_id>/disable') - hbp.register_view_class(DeleteView, '/<int:item_id>/delete') + hbp.register_view_class(ListView, '/list') + hbp.register_view_class(CreateView, '/create') + hbp.register_view_class(CreateForView, '/createfor/<int:parent_id>') + hbp.register_view_class(ShowView, '/<int:item_id>/show') + hbp.register_view_class(UpdateView, '/<int:item_id>/update') + hbp.register_view_class(EnableView, '/<int:item_id>/enable') + hbp.register_view_class(DisableView, '/<int:item_id>/disable') + hbp.register_view_class(DeleteView, '/<int:item_id>/delete') hbp.register_view_class(PlaygroundView, '/playground') return hbp diff --git a/lib/hawat/blueprints/filters/forms.py b/lib/hawat/blueprints/filters/forms.py index 8fdcbea92f7fd728e910ef9d13a6a5ae9d3c8a8d..6de0c7bf00463abb50e1482f27e4c54a4463ab45 100644 --- a/lib/hawat/blueprints/filters/forms.py +++ b/lib/hawat/blueprints/filters/forms.py @@ -1,22 +1,20 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ This module contains custom reporting filter management forms for Hawat. """ - __author__ = "Jan Mach <jan.mach@cesnet.cz>" __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" - import pynspect.gparser import wtforms @@ -31,8 +29,8 @@ from flask_babel import gettext, lazy_gettext # # Custom modules. # -import vial.db -import vial.forms +import hawat.db +import hawat.forms from mentat.datatype.sqldb import GroupModel from mentat.const import REPORTING_FILTER_BASIC, REPORTING_FILTER_ADVANCED from mentat.idea.internal import Idea @@ -42,10 +40,10 @@ def get_available_groups(): """ Query the database for list of all available groups. """ - return vial.db.db_query(GroupModel).order_by(GroupModel.name).all() + return hawat.db.db_query(GroupModel).order_by(GroupModel.name).all() -def check_filter(form, field): # pylint: disable=locally-disabled,unused-argument +def check_filter(_form, field): # pylint: disable=locally-disabled,unused-argument """ Callback for validating Pynspect filter. """ @@ -57,11 +55,12 @@ def check_filter(form, field): # pylint: disable=locally-disabled,unused-argume raise wtforms.validators.ValidationError( gettext( 'Filtering rule parse error: "%(error)s".', - error = str(err) + error=str(err) ) ) -def check_event(form, field): # pylint: disable=locally-disabled,unused-argument + +def check_event(_form, field): # pylint: disable=locally-disabled,unused-argument """ Callback for validating IDEA event JSON. """ @@ -71,95 +70,95 @@ def check_event(form, field): # pylint: disable=locally-disabled,unused-argumen raise wtforms.validators.ValidationError( gettext( 'Event JSON parse error: "%(error)s".', - error = str(err) + error=str(err) ) ) -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- -class BaseFilterForm(vial.forms.BaseItemForm): +class BaseFilterForm(hawat.forms.BaseItemForm): """ Class representing base reporting filter form. """ name = wtforms.StringField( lazy_gettext('Name:'), - validators = [ + validators=[ wtforms.validators.DataRequired(), - wtforms.validators.Length(min = 3, max = 250) + wtforms.validators.Length(min=3, max=250) ] ) type = wtforms.SelectField( lazy_gettext('Type:'), - validators = [ + validators=[ wtforms.validators.DataRequired(), ], - choices = [ - (REPORTING_FILTER_BASIC, lazy_gettext('Basic')), + choices=[ + (REPORTING_FILTER_BASIC, lazy_gettext('Basic')), (REPORTING_FILTER_ADVANCED, lazy_gettext('Advanced')) ] ) description = wtforms.TextAreaField( lazy_gettext('Description:'), - validators = [ + validators=[ wtforms.validators.DataRequired(), ] ) filter = wtforms.TextAreaField( lazy_gettext('Filter:'), - validators = [ + validators=[ wtforms.validators.Optional(), check_filter ] ) detectors = wtforms.SelectMultipleField( lazy_gettext('Detectors:'), - validators = [ + validators=[ wtforms.validators.Optional(), ], - choices = [('', lazy_gettext('<< no preference >>'))], - filters = [lambda x: x or []] + choices=[('', lazy_gettext('<< no preference >>'))], + filters=[lambda x: x or []] ) categories = wtforms.SelectMultipleField( lazy_gettext('Categories:'), - validators = [ + validators=[ wtforms.validators.Optional(), ], - choices = [('', lazy_gettext('<< no preference >>'))], - filters = [lambda x: x or []] + choices=[('', lazy_gettext('<< no preference >>'))], + filters=[lambda x: x or []] ) - ips = vial.forms.CommaListField( + ips = hawat.forms.CommaListField( lazy_gettext('Source IPs:'), - validators = [ + validators=[ wtforms.validators.Optional(), - vial.forms.check_network_record_list + hawat.forms.check_network_record_list ], - widget = wtforms.widgets.TextArea() + widget=wtforms.widgets.TextArea() ) - valid_from = vial.forms.SmartDateTimeField( + valid_from = hawat.forms.SmartDateTimeField( lazy_gettext('Valid from:'), - validators = [ + validators=[ wtforms.validators.Optional() ] ) - valid_to = vial.forms.SmartDateTimeField( + valid_to = hawat.forms.SmartDateTimeField( lazy_gettext('Valid to:'), - validators = [ + validators=[ wtforms.validators.Optional() ] ) enabled = wtforms.RadioField( lazy_gettext('State:'), - validators = [ + validators=[ wtforms.validators.InputRequired(), ], - choices = [ - (True, lazy_gettext('Enabled')), + choices=[ + (True, lazy_gettext('Enabled')), (False, lazy_gettext('Disabled')) ], - filters = [vial.forms.str_to_bool], - coerce = vial.forms.str_to_bool + filters=[hawat.forms.str_to_bool], + coerce=hawat.forms.str_to_bool ) submit = wtforms.SubmitField( lazy_gettext('Submit') @@ -174,7 +173,7 @@ class BaseFilterForm(vial.forms.BaseItemForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.detectors.choices[1:] = kwargs['choices_detectors'] + self.detectors.choices[1:] = kwargs['choices_detectors'] self.categories.choices[1:] = kwargs['choices_categories'] @@ -184,24 +183,25 @@ class AdminFilterForm(BaseFilterForm): """ group = QuerySelectField( lazy_gettext('Group:'), - query_factory = get_available_groups, - allow_blank = False + query_factory=get_available_groups, + allow_blank=False ) + class PlaygroundFilterForm(flask_wtf.FlaskForm): """ Class representing IP geolocation search form. """ filter = wtforms.TextAreaField( lazy_gettext('Filtering rule:'), - validators = [ + validators=[ wtforms.validators.DataRequired(), check_filter ] ) event = wtforms.TextAreaField( lazy_gettext('IDEA event:'), - validators = [ + validators=[ wtforms.validators.DataRequired(), check_event ] @@ -210,82 +210,86 @@ class PlaygroundFilterForm(flask_wtf.FlaskForm): lazy_gettext('Check') ) -class FilterSearchForm(vial.forms.BaseSearchForm): + +class FilterSearchForm(hawat.forms.BaseSearchForm): """ Class representing simple user search form. """ search = wtforms.StringField( lazy_gettext('Netname, network, description:'), - validators = [ + validators=[ wtforms.validators.Optional(), - wtforms.validators.Length(min = 3, max = 100) + wtforms.validators.Length(min=3, max=100) ], - description = lazy_gettext('Filter`s name, content or description. Search is performed even in the middle of the strings.') + description=lazy_gettext( + 'Filter`s name, content or description. Search is performed even in the middle of the strings.') ) - dt_from = vial.forms.SmartDateTimeField( + dt_from = hawat.forms.SmartDateTimeField( lazy_gettext('Creation time from:'), - validators = [ + validators=[ wtforms.validators.Optional() ], - description = lazy_gettext('Lower time boundary for item creation time. Timestamp is expected to be in the format <code>YYYY-MM-DD hh:mm:ss</code> and in the timezone according to the user`s preferences.') + description=lazy_gettext( + 'Lower time boundary for item creation time. Timestamp is expected to be in the format <code>YYYY-MM-DD hh:mm:ss</code> and in the timezone according to the user`s preferences.') ) - dt_to = vial.forms.SmartDateTimeField( + dt_to = hawat.forms.SmartDateTimeField( lazy_gettext('Creation time to:'), - validators = [ + validators=[ wtforms.validators.Optional() ], - description = lazy_gettext('Upper time boundary for item creation time. Timestamp is expected to be in the format <code>YYYY-MM-DD hh:mm:ss</code> and in the timezone according to the user`s preferences.') + description=lazy_gettext( + 'Upper time boundary for item creation time. Timestamp is expected to be in the format <code>YYYY-MM-DD hh:mm:ss</code> and in the timezone according to the user`s preferences.') ) type = wtforms.SelectField( lazy_gettext('Type:'), - validators = [ + validators=[ wtforms.validators.Optional(), ], - choices = [ + choices=[ ('', lazy_gettext('Nothing selected')), - (REPORTING_FILTER_BASIC, lazy_gettext('Basic')), + (REPORTING_FILTER_BASIC, lazy_gettext('Basic')), (REPORTING_FILTER_ADVANCED, lazy_gettext('Advanced')) ], - default = '', - description = lazy_gettext('Search for filters of particular type.') + default='', + description=lazy_gettext('Search for filters of particular type.') ) state = wtforms.SelectField( lazy_gettext('State:'), - validators = [ + validators=[ wtforms.validators.Optional(), ], - choices = [ + choices=[ ('', lazy_gettext('Nothing selected')), - ('enabled', lazy_gettext('Enabled')), + ('enabled', lazy_gettext('Enabled')), ('disabled', lazy_gettext('Disabled')) ], - default = '', - description = lazy_gettext('Search for filters with particular state.') + default='', + description=lazy_gettext('Search for filters with particular state.') ) group = QuerySelectField( lazy_gettext('Group:'), - query_factory = get_available_groups, - allow_blank = True, - description = lazy_gettext('Search for filters belonging to particular group.') + query_factory=get_available_groups, + allow_blank=True, + description=lazy_gettext('Search for filters belonging to particular group.') ) sortby = wtforms.SelectField( lazy_gettext('Sort by:'), - validators = [ + validators=[ wtforms.validators.Optional() ], - choices = [ + choices=[ ('createtime.desc', lazy_gettext('by creation time descending')), - ('createtime.asc', lazy_gettext('by creation time ascending')), + ('createtime.asc', lazy_gettext('by creation time ascending')), ('name.desc', lazy_gettext('by netname descending')), - ('name.asc', lazy_gettext('by netname ascending')), + ('name.asc', lazy_gettext('by netname ascending')), ('hits.desc', lazy_gettext('by number of hits descending')), - ('hits.asc', lazy_gettext('by number of hits ascending')), + ('hits.asc', lazy_gettext('by number of hits ascending')), ('last_hit.desc', lazy_gettext('by time of last hit descending')), - ('last_hit.asc', lazy_gettext('by time of last hit ascending')) + ('last_hit.asc', lazy_gettext('by time of last hit ascending')) ], - default = 'name.asc' + default='name.asc' ) @staticmethod diff --git a/lib/hawat/blueprints/filters/templates/filters/playground.html b/lib/hawat/blueprints/filters/templates/filters/playground.html index 70c917eaf55361485a753e0353f78864605fc0d9..96f9453bafd43b4e10fcbe45bd3dd4523e1b8f24 100644 --- a/lib/hawat/blueprints/filters/templates/filters/playground.html +++ b/lib/hawat/blueprints/filters/templates/filters/playground.html @@ -7,7 +7,7 @@ {{ macros_page.render_breadcrumbs(item) }} <div class="jumbotron" style="margin-top: 1em;"> - <h2>{{ vial_current_view.get_view_title() }}</h2> + <h2>{{ hawat_current_view.get_view_title() }}</h2> <hr> <form method="POST" class="form-horizontal" id="form-events-simple" action="{{ form_url }}"> {{ macros_form.render_form_item_default(form.filter, placeholder = _('Write your filtering rule here...')) }} diff --git a/lib/hawat/blueprints/filters/templates/filters/show.html b/lib/hawat/blueprints/filters/templates/filters/show.html index b93b1ed1e429fab811b17d21261e442e9c3000bc..bbfade96946ac5f149b8c49e3ce24156042cbf94 100644 --- a/lib/hawat/blueprints/filters/templates/filters/show.html +++ b/lib/hawat/blueprints/filters/templates/filters/show.html @@ -6,7 +6,7 @@ <div class="col-lg-12"> {{ macros_page.render_breadcrumbs(item) }} - <h2>{{ vial_current_view.get_view_title() }}</h2> + <h2>{{ hawat_current_view.get_view_title() }}</h2> <hr> <h3>{{ item.name }}</h3> <div class="pull-right"> diff --git a/lib/hawat/blueprints/filters/test/__init__.py b/lib/hawat/blueprints/filters/test/__init__.py index be5aeb3a05bcff114bd4f8f25b8e16bc14d064b5..5ac1975d1e7735ed13bcea49dd8314da33d31322 100644 --- a/lib/hawat/blueprints/filters/test/__init__.py +++ b/lib/hawat/blueprints/filters/test/__init__.py @@ -17,11 +17,11 @@ import unittest from mentat.datatype.sqldb import FilterModel -import vial.const -import vial.test -import vial.test.fixtures -import vial.db -from vial.test import VialTestCase, ItemCreateVialTestCase +import hawat.const +import hawat.test +import hawat.test.fixtures +import hawat.db +from hawat.test import HawatTestCase, ItemCreateHawatTestCase from hawat.test.runner import TestRunnerMixin @@ -39,20 +39,20 @@ class FilterTestMixin: Get given filter. """ if not with_app_context: - return vial.db.db_session().query(FilterModel).filter(FilterModel.name == filter_name).one_or_none() + return hawat.db.db_session().query(FilterModel).filter(FilterModel.name == filter_name).one_or_none() with self.app.app_context(): - return vial.db.db_session().query(FilterModel).filter(FilterModel.name == filter_name).one_or_none() + return hawat.db.db_session().query(FilterModel).filter(FilterModel.name == filter_name).one_or_none() def filter_save(self, filter_object, with_app_context = False): """ Update given filter. """ if not with_app_context: - vial.db.db_session().add(filter_object) - vial.db.db_session().commit() + hawat.db.db_session().add(filter_object) + hawat.db.db_session().commit() with self.app.app_context(): - vial.db.db_session().add(filter_object) - vial.db.db_session().commit() + hawat.db.db_session().add(filter_object) + hawat.db.db_session().commit() def filter_id(self, filter_type, with_app_context = False): """ @@ -66,7 +66,7 @@ class FilterTestMixin: return fobj.id -class FiltersListTestCase(TestRunnerMixin, VialTestCase): +class FiltersListTestCase(TestRunnerMixin, HawatTestCase): """Class for testing ``filters.list`` endpoint.""" def _attempt_fail(self): @@ -85,28 +85,28 @@ class FiltersListTestCase(TestRunnerMixin, VialTestCase): ] ) - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_USER) def test_01_as_user(self): """Test access as user ``user``.""" self._attempt_fail() - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_DEVELOPER) def test_02_as_developer(self): """Test access as user ``developer``.""" self._attempt_fail() - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_MAINTAINER) def test_03_as_maintainer(self): """Test access as user ``maintainer``.""" self._attempt_succeed() - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_ADMIN) def test_04_as_admin(self): """Test access as user ``admin``.""" self._attempt_succeed() -class FiltersShowTestCase(FilterTestMixin, TestRunnerMixin, VialTestCase): +class FiltersShowTestCase(FilterTestMixin, TestRunnerMixin, HawatTestCase): """Base class for testing ``filters.show`` endpoint.""" def _attempt_fail(self, fname): @@ -127,48 +127,48 @@ class FiltersShowTestCase(FilterTestMixin, TestRunnerMixin, VialTestCase): ] ) - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_USER) def test_01_as_user(self): """ Test access as user 'user'. Only power user is able to view all available filters. """ - self._attempt_succeed(self._fname(vial.test.fixtures.DEMO_GROUP_A)) - self._attempt_fail(self._fname(vial.test.fixtures.DEMO_GROUP_B)) + self._attempt_succeed(self._fname(hawat.test.fixtures.DEMO_GROUP_A)) + self._attempt_fail(self._fname(hawat.test.fixtures.DEMO_GROUP_B)) - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_DEVELOPER) def test_02_as_developer(self): """ Test access as user 'developer'. Only power user is able to view all available filters. """ - self._attempt_succeed(self._fname(vial.test.fixtures.DEMO_GROUP_A)) - self._attempt_fail(self._fname(vial.test.fixtures.DEMO_GROUP_B)) + self._attempt_succeed(self._fname(hawat.test.fixtures.DEMO_GROUP_A)) + self._attempt_fail(self._fname(hawat.test.fixtures.DEMO_GROUP_B)) - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_MAINTAINER) def test_03_as_maintainer(self): """ Test access as user 'maintainer'. Only power user is able to view all available filters. """ - self._attempt_succeed(self._fname(vial.test.fixtures.DEMO_GROUP_A)) - self._attempt_succeed(self._fname(vial.test.fixtures.DEMO_GROUP_B)) + self._attempt_succeed(self._fname(hawat.test.fixtures.DEMO_GROUP_A)) + self._attempt_succeed(self._fname(hawat.test.fixtures.DEMO_GROUP_B)) - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_ADMIN) def test_04_as_admin(self): """ Test access as user 'admin'. Only power user is able to view all available filters. """ - self._attempt_succeed(self._fname(vial.test.fixtures.DEMO_GROUP_A)) - self._attempt_succeed(self._fname(vial.test.fixtures.DEMO_GROUP_B)) + self._attempt_succeed(self._fname(hawat.test.fixtures.DEMO_GROUP_A)) + self._attempt_succeed(self._fname(hawat.test.fixtures.DEMO_GROUP_B)) -class FiltersCreateTestCase(FilterTestMixin, TestRunnerMixin, ItemCreateVialTestCase): +class FiltersCreateTestCase(FilterTestMixin, TestRunnerMixin, ItemCreateHawatTestCase): """Class for testing ``filters.create`` endpoint.""" filter_data_fixture = [ @@ -197,28 +197,28 @@ class FiltersCreateTestCase(FilterTestMixin, TestRunnerMixin, ItemCreateVialTest ] ) - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_USER) def test_01_as_user(self): """Test access as user 'user'.""" self._attempt_fail() - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_DEVELOPER) def test_02_as_developer(self): """Test access as user 'developer'.""" self._attempt_fail() - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_MAINTAINER) def test_03_as_maintainer(self): """Test access as user 'maintainer'.""" self._attempt_succeed() - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_ADMIN) def test_04_as_admin(self): """Test access as user 'admin'.""" self._attempt_succeed() -class FiltersCreateForTestCase(FilterTestMixin, TestRunnerMixin, ItemCreateVialTestCase): +class FiltersCreateForTestCase(FilterTestMixin, TestRunnerMixin, ItemCreateHawatTestCase): """Class for testing ``filters.createfor`` endpoint.""" filter_data_fixture = [ @@ -248,32 +248,32 @@ class FiltersCreateForTestCase(FilterTestMixin, TestRunnerMixin, ItemCreateVialT ] ) - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_USER) def test_01_as_user(self): """Test access as user 'user'.""" - self._attempt_fail(vial.test.fixtures.DEMO_GROUP_A) - self._attempt_fail(vial.test.fixtures.DEMO_GROUP_B) + self._attempt_fail(hawat.test.fixtures.DEMO_GROUP_A) + self._attempt_fail(hawat.test.fixtures.DEMO_GROUP_B) - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_DEVELOPER) def test_02_as_developer(self): """Test access as user 'developer'.""" - self._attempt_succeed(vial.test.fixtures.DEMO_GROUP_A) - self._attempt_fail(vial.test.fixtures.DEMO_GROUP_B) + self._attempt_succeed(hawat.test.fixtures.DEMO_GROUP_A) + self._attempt_fail(hawat.test.fixtures.DEMO_GROUP_B) - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_MAINTAINER) def test_03_as_maintainer(self): """Test access as user 'maintainer'.""" - self._attempt_succeed(vial.test.fixtures.DEMO_GROUP_A) - self._attempt_succeed(vial.test.fixtures.DEMO_GROUP_B) + self._attempt_succeed(hawat.test.fixtures.DEMO_GROUP_A) + self._attempt_succeed(hawat.test.fixtures.DEMO_GROUP_B) - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_ADMIN) def test_04_as_admin(self): """Test access as user 'admin'.""" - self._attempt_succeed(vial.test.fixtures.DEMO_GROUP_A) - self._attempt_succeed(vial.test.fixtures.DEMO_GROUP_B) + self._attempt_succeed(hawat.test.fixtures.DEMO_GROUP_A) + self._attempt_succeed(hawat.test.fixtures.DEMO_GROUP_B) -class FiltersUpdateTestCase(FilterTestMixin, TestRunnerMixin, VialTestCase): +class FiltersUpdateTestCase(FilterTestMixin, TestRunnerMixin, HawatTestCase): """Class for testing ``filters.update`` endpoint.""" def _attempt_fail(self, fname): @@ -293,32 +293,32 @@ class FiltersUpdateTestCase(FilterTestMixin, TestRunnerMixin, VialTestCase): ] ) - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_USER) def test_01_as_user(self): """Test access as user 'user'.""" - self._attempt_fail(self._fname(vial.test.fixtures.DEMO_GROUP_A)) - self._attempt_fail(self._fname(vial.test.fixtures.DEMO_GROUP_B)) + self._attempt_fail(self._fname(hawat.test.fixtures.DEMO_GROUP_A)) + self._attempt_fail(self._fname(hawat.test.fixtures.DEMO_GROUP_B)) - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_DEVELOPER) def test_04_as_developer(self): """Test access as user 'developer'.""" - self._attempt_succeed(self._fname(vial.test.fixtures.DEMO_GROUP_A)) - self._attempt_fail(self._fname(vial.test.fixtures.DEMO_GROUP_B)) + self._attempt_succeed(self._fname(hawat.test.fixtures.DEMO_GROUP_A)) + self._attempt_fail(self._fname(hawat.test.fixtures.DEMO_GROUP_B)) - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_MAINTAINER) def test_05_as_maintainer(self): """Test access as user 'maintainer'.""" - self._attempt_succeed(self._fname(vial.test.fixtures.DEMO_GROUP_A)) - self._attempt_succeed(self._fname(vial.test.fixtures.DEMO_GROUP_B)) + self._attempt_succeed(self._fname(hawat.test.fixtures.DEMO_GROUP_A)) + self._attempt_succeed(self._fname(hawat.test.fixtures.DEMO_GROUP_B)) - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_ADMIN) def test_06_as_admin(self): """Test access as user 'admin'.""" - self._attempt_succeed(self._fname(vial.test.fixtures.DEMO_GROUP_A)) - self._attempt_succeed(self._fname(vial.test.fixtures.DEMO_GROUP_B)) + self._attempt_succeed(self._fname(hawat.test.fixtures.DEMO_GROUP_A)) + self._attempt_succeed(self._fname(hawat.test.fixtures.DEMO_GROUP_B)) -class FiltersEnableDisableTestCase(FilterTestMixin, TestRunnerMixin, VialTestCase): +class FiltersEnableDisableTestCase(FilterTestMixin, TestRunnerMixin, HawatTestCase): """Class for testing ``filters.enable`` and ``filters.disable`` endpoint.""" def _attempt_fail(self, fname): @@ -369,32 +369,32 @@ class FiltersEnableDisableTestCase(FilterTestMixin, TestRunnerMixin, VialTestCas ] ) - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_USER) def test_01_as_user(self): """Test access as user 'user'.""" - self._attempt_fail(self._fname(vial.test.fixtures.DEMO_GROUP_A)) - self._attempt_fail(self._fname(vial.test.fixtures.DEMO_GROUP_B)) + self._attempt_fail(self._fname(hawat.test.fixtures.DEMO_GROUP_A)) + self._attempt_fail(self._fname(hawat.test.fixtures.DEMO_GROUP_B)) - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_DEVELOPER) def test_02_as_developer(self): """Test access as user 'developer'.""" - self._attempt_succeed(self._fname(vial.test.fixtures.DEMO_GROUP_A)) - self._attempt_fail(self._fname(vial.test.fixtures.DEMO_GROUP_B)) + self._attempt_succeed(self._fname(hawat.test.fixtures.DEMO_GROUP_A)) + self._attempt_fail(self._fname(hawat.test.fixtures.DEMO_GROUP_B)) - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_MAINTAINER) def test_03_as_maintainer(self): """Test access as user 'maintainer'.""" - self._attempt_succeed(self._fname(vial.test.fixtures.DEMO_GROUP_A)) - self._attempt_succeed(self._fname(vial.test.fixtures.DEMO_GROUP_B)) + self._attempt_succeed(self._fname(hawat.test.fixtures.DEMO_GROUP_A)) + self._attempt_succeed(self._fname(hawat.test.fixtures.DEMO_GROUP_B)) - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_ADMIN) def test_04_as_admin(self): """Test access as user 'admin'.""" - self._attempt_succeed(self._fname(vial.test.fixtures.DEMO_GROUP_A)) - self._attempt_succeed(self._fname(vial.test.fixtures.DEMO_GROUP_B)) + self._attempt_succeed(self._fname(hawat.test.fixtures.DEMO_GROUP_A)) + self._attempt_succeed(self._fname(hawat.test.fixtures.DEMO_GROUP_B)) -class FiltersDeleteTestCase(FilterTestMixin, TestRunnerMixin, VialTestCase): +class FiltersDeleteTestCase(FilterTestMixin, TestRunnerMixin, HawatTestCase): """Class for testing ``filters.delete`` endpoint.""" def _attempt_fail(self, fname): @@ -424,29 +424,29 @@ class FiltersDeleteTestCase(FilterTestMixin, TestRunnerMixin, VialTestCase): ] ) - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_USER) def test_01_as_user(self): """Test access as user 'user'.""" - self._attempt_fail(self._fname(vial.test.fixtures.DEMO_GROUP_A)) - self._attempt_fail(self._fname(vial.test.fixtures.DEMO_GROUP_B)) + self._attempt_fail(self._fname(hawat.test.fixtures.DEMO_GROUP_A)) + self._attempt_fail(self._fname(hawat.test.fixtures.DEMO_GROUP_B)) - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_DEVELOPER) def test_02_as_developer(self): """Test access as user 'developer'.""" - self._attempt_succeed(self._fname(vial.test.fixtures.DEMO_GROUP_A)) - self._attempt_fail(self._fname(vial.test.fixtures.DEMO_GROUP_B)) + self._attempt_succeed(self._fname(hawat.test.fixtures.DEMO_GROUP_A)) + self._attempt_fail(self._fname(hawat.test.fixtures.DEMO_GROUP_B)) - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_MAINTAINER) def test_03_as_maintainer(self): """Test access as user 'maintainer'.""" - self._attempt_succeed(self._fname(vial.test.fixtures.DEMO_GROUP_A)) - self._attempt_succeed(self._fname(vial.test.fixtures.DEMO_GROUP_B)) + self._attempt_succeed(self._fname(hawat.test.fixtures.DEMO_GROUP_A)) + self._attempt_succeed(self._fname(hawat.test.fixtures.DEMO_GROUP_B)) - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_ADMIN) def test_04_as_admin(self): """Test access as user 'admin'.""" - self._attempt_succeed(self._fname(vial.test.fixtures.DEMO_GROUP_A)) - self._attempt_succeed(self._fname(vial.test.fixtures.DEMO_GROUP_B)) + self._attempt_succeed(self._fname(hawat.test.fixtures.DEMO_GROUP_A)) + self._attempt_succeed(self._fname(hawat.test.fixtures.DEMO_GROUP_B)) #------------------------------------------------------------------------------- diff --git a/lib/hawat/blueprints/geoip/__init__.py b/lib/hawat/blueprints/geoip/__init__.py index 02dd10c8c8809462a7526ab6702e86e9e2a91e6b..643672680c07655450e664a329ca1805f5092ddf 100644 --- a/lib/hawat/blueprints/geoip/__init__.py +++ b/lib/hawat/blueprints/geoip/__init__.py @@ -1,18 +1,18 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ This pluggable module provides access to local IP geolocation service. It is implemented upon custom :py:mod:`mentat.services.geoip` module, which in turn uses the geolocation service `GeoLite2 <https://dev.maxmind.com/geoip/geoip2/geolite2/>`__ -created by `MaxMind <http://www.maxmind.com/>`__. This module requires that GeoLite2 +created by `MaxMind <https://www.maxmind.com/>`__. This module requires that GeoLite2 database files are installed locally on host machine. @@ -43,28 +43,25 @@ Provided endpoints * *Methods:* ``GET``, ``POST`` """ - __author__ = "Jan Mach <jan.mach@cesnet.cz>" __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" - import flask from flask_babel import lazy_gettext import mentat.services.geoip from mentat.const import tr_ -import vial.db -import vial.const -import vial.acl -from vial.app import VialBlueprint -from vial.view import RenderableView -from vial.view.mixin import HTMLMixin, AJAXMixin, SnippetMixin -from vial.utils import URLParamsBuilder +import hawat.db +import hawat.const +import hawat.acl +from hawat.base import HawatBlueprint +from hawat.view import RenderableView +from hawat.view.mixin import HTMLMixin, AJAXMixin, SnippetMixin +from hawat.utils import URLParamsBuilder import hawat.const from hawat.blueprints.geoip.forms import GeoipSearchForm - BLUEPRINT_NAME = 'geoip' """Name of the blueprint as module global constant.""" @@ -91,28 +88,28 @@ class AbstractSearchView(RenderableView): # pylint: disable=locally-disabled,ab Mandatory interface required by the :py:func:`flask.views.View.dispatch_request`. Will be called by the *Flask* framework to service the request. """ - form = GeoipSearchForm(flask.request.args, meta = {'csrf': False}) - if vial.const.FORM_ACTION_SUBMIT in flask.request.args: + form = GeoipSearchForm(flask.request.args, meta={'csrf': False}) + if hawat.const.FORM_ACTION_SUBMIT in flask.request.args: if form.validate(): form_data = form.data geoip_service = mentat.services.geoip.service() self.response_context.update( - search_item = form.search.data, - form_data = form_data + search_item=form.search.data, + form_data=form_data ) try: self.response_context.update( - search_result = geoip_service.lookup( + search_result=geoip_service.lookup( form.search.data ) ) except Exception as exc: - self.flash(str(exc), level = 'error') + self.flash(str(exc), level='error') self.response_context.update( - search_form = form, - request_args = flask.request.args, + search_form=form, + request_args=flask.request.args, ) return self.generate_response() @@ -134,7 +131,7 @@ class APISearchView(AJAXMixin, AbstractSearchView): # pylint: disable=locally-d View responsible for searching IP geolocation service and presenting the results in the form of JSON document. """ - methods = ['GET','POST'] + methods = ['GET', 'POST'] @classmethod def get_view_name(cls): @@ -146,18 +143,20 @@ class SnippetSearchView(SnippetMixin, AbstractSearchView): # pylint: disable=lo View responsible for searching IP geolocation service and presenting the results in the form of JSON document containing ready to use HTML page snippets. """ - methods = ['GET', 'POST'] + methods = ['GET', 'POST'] - renders = ['label', 'full'] + renders = ['label', 'full'] snippets = [ { 'name': 'city', - 'condition': lambda x: x.get('search_result', False) and x['search_result'].get('city', False) and x['search_result']['city'].get('ctr_code', False) + 'condition': lambda x: x.get('search_result', False) and x['search_result'].get('city', False) and + x['search_result']['city'].get('ctr_code', False) }, { 'name': 'asn', - 'condition': lambda x: x.get('search_result', False) and x['search_result'].get('asn', False) and x['search_result']['asn'].get('org', False) + 'condition': lambda x: x.get('search_result', False) and x['search_result'].get('asn', False) and + x['search_result']['asn'].get('org', False) } ] @@ -166,10 +165,10 @@ class SnippetSearchView(SnippetMixin, AbstractSearchView): # pylint: disable=lo return 'sptsearch' -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- -class GeoipBlueprint(VialBlueprint): +class GeoipBlueprint(HawatBlueprint): """Pluggable module - IP geolocation service (*geoip*).""" @classmethod @@ -182,8 +181,8 @@ class GeoipBlueprint(VialBlueprint): app.menu_main.add_entry( 'view', 'more.{}'.format(BLUEPRINT_NAME), - position = 20, - view = SearchView + position=20, + view=SearchView ) # Register context actions provided by this module. @@ -207,24 +206,24 @@ class GeoipBlueprint(VialBlueprint): ) -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- def get_blueprint(): """ - Mandatory interface for :py:mod:`vial.Vial` and factory function. This function - must return a valid instance of :py:class:`vial.app.VialBlueprint` or + Mandatory interface for :py:mod:`hawat.Hawat` and factory function. This function + must return a valid instance of :py:class:`hawat.app.HawatBlueprint` or :py:class:`flask.Blueprint`. """ hbp = GeoipBlueprint( BLUEPRINT_NAME, __name__, - template_folder = 'templates' + template_folder='templates' ) - hbp.register_view_class(SearchView, '/{}/search'.format(BLUEPRINT_NAME)) - hbp.register_view_class(APISearchView, '/api/{}/search'.format(BLUEPRINT_NAME)) + hbp.register_view_class(SearchView, '/{}/search'.format(BLUEPRINT_NAME)) + hbp.register_view_class(APISearchView, '/api/{}/search'.format(BLUEPRINT_NAME)) hbp.register_view_class(SnippetSearchView, '/snippet/{}/search'.format(BLUEPRINT_NAME)) return hbp diff --git a/lib/hawat/blueprints/geoip/forms.py b/lib/hawat/blueprints/geoip/forms.py index 1ab72bbf80f339c2ce3acf84a8111b0fc50f2b74..19a9805a680d3a54bfd543b408307cfb15145027 100644 --- a/lib/hawat/blueprints/geoip/forms.py +++ b/lib/hawat/blueprints/geoip/forms.py @@ -1,27 +1,25 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ This module contains custom internal IP geolocation search form for Hawat. """ - __author__ = "Jan Mach <jan.mach@cesnet.cz>" __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" - import wtforms import flask_wtf from flask_babel import lazy_gettext -import vial.forms +import hawat.forms class GeoipSearchForm(flask_wtf.FlaskForm): @@ -30,9 +28,9 @@ class GeoipSearchForm(flask_wtf.FlaskForm): """ search = wtforms.StringField( lazy_gettext('Search GeoIP:'), - validators = [ + validators=[ wtforms.validators.DataRequired(), - vial.forms.check_ip_record + hawat.forms.check_ip_record ] ) submit = wtforms.SubmitField( diff --git a/lib/hawat/blueprints/geoip/templates/geoip/search.html b/lib/hawat/blueprints/geoip/templates/geoip/search.html index 9fd4069aa36b329d0eec81bd0ff9fad5a9f22401..85268def7bc2e7b0c8056e3fbc74b0f6f258391a 100644 --- a/lib/hawat/blueprints/geoip/templates/geoip/search.html +++ b/lib/hawat/blueprints/geoip/templates/geoip/search.html @@ -7,7 +7,7 @@ <div class="col-lg-12"> <div class="jumbotron" style="margin-top: 1em;"> - <h2>{{ vial_current_view.get_view_title() }}</h2> + <h2>{{ hawat_current_view.get_view_title() }}</h2> <form method="GET" class="form-inline" action="{{ url_for('geoip.search') }}"> <div class="form-group{% if search_form.search.errors %}{{ ' has-error' }}{% endif %}"> {{ search_form.search.label(class_='sr-only') }} diff --git a/lib/hawat/blueprints/geoip/test/__init__.py b/lib/hawat/blueprints/geoip/test/__init__.py index 1c1c894a65bb04f3c79542600ad26cb4f54bd530..0b600e09ee5e69ff3a71b1f3dd68c1bd1ec5bb83 100644 --- a/lib/hawat/blueprints/geoip/test/__init__.py +++ b/lib/hawat/blueprints/geoip/test/__init__.py @@ -1,28 +1,27 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ Unit tests for :py:mod:`hawat.blueprints.geoip`. """ - import unittest -import vial.const -import vial.test -import vial.db -from vial.test import VialTestCase +import hawat.const +import hawat.test +import hawat.db +from hawat.test import HawatTestCase from hawat.test.runner import TestRunnerMixin -class SearchTestCase(TestRunnerMixin, VialTestCase): +class SearchTestCase(TestRunnerMixin, HawatTestCase): """ Class for testing ``geoip.search`` endpoint. """ @@ -35,7 +34,7 @@ class SearchTestCase(TestRunnerMixin, VialTestCase): b'Redirecting...', b'login?next=' ], - follow_redirects = False + follow_redirects=False ) def _attempt_succeed(self): @@ -51,28 +50,28 @@ class SearchTestCase(TestRunnerMixin, VialTestCase): """Test access as anonymous user.""" self._attempt_fail_redirect() - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_USER) def test_02_as_user(self): """Test access as user ``user``.""" self._attempt_succeed() - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_DEVELOPER) def test_03_as_developer(self): """Test access as user ``developer``.""" self._attempt_succeed() - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_MAINTAINER) def test_04_as_maintainer(self): """Test access as user ``maintainer``.""" self._attempt_succeed() - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_ADMIN) def test_05_as_admin(self): """Test access as user ``admin``.""" self._attempt_succeed() -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- if __name__ == "__main__": diff --git a/lib/hawat/blueprints/groups/__init__.py b/lib/hawat/blueprints/groups/__init__.py index 008346575ade3c925e6fd7927822ec04884c74f2..9c20e5b188c6430c160c8f3d21a5065f29261311 100644 --- a/lib/hawat/blueprints/groups/__init__.py +++ b/lib/hawat/blueprints/groups/__init__.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ @@ -24,150 +24,354 @@ related to user group management. These features include: * rejecting group membership requests """ - __author__ = "Jan Mach <jan.mach@cesnet.cz>" __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" - +import markupsafe +import flask import flask_login +import flask_principal from flask_babel import gettext, lazy_gettext + from sqlalchemy import and_, or_ from mentat.datatype.sqldb import SettingsReportingModel, FilterModel, NetworkModel, ItemChangeLogModel from mentat.const import tr_ -import vial.acl -import vial.menu -from vial.utils import URLParamsBuilder -import vial.blueprints.groups -from vial.blueprints.groups import BLUEPRINT_NAME, ListView, EnableView, DisableView, DeleteView, AddMemberView, RejectMemberView, RemoveMemberView, AddManagerView, RemoveManagerView import hawat.const -from hawat.blueprints.groups.forms import AdminCreateGroupForm, AdminUpdateGroupForm,\ - UpdateGroupForm +import hawat.acl +import hawat.menu +from hawat.utils import URLParamsBuilder +from hawat.base import HawatBlueprint +from hawat.view import ItemListView, ItemShowView, ItemCreateView, ItemUpdateView, ItemDeleteView, ItemEnableView, \ + ItemDisableView, ItemObjectRelationView +from hawat.view.mixin import HTMLMixin, SQLAlchemyMixin +from hawat.blueprints.groups.forms import AdminCreateGroupForm, AdminUpdateGroupForm, UpdateGroupForm, GroupSearchForm + +BLUEPRINT_NAME = 'groups' +"""Name of the blueprint as module global constant.""" + + +class ListView(HTMLMixin, SQLAlchemyMixin, ItemListView): + """ + General group listing. + """ + + methods = ['GET'] + + authentication = True + + authorization = [hawat.acl.PERMISSION_POWER] + + @classmethod + def get_view_title(cls, **kwargs): + return lazy_gettext('Group management') + + @property + def dbmodel(self): + return self.get_model(hawat.const.MODEL_GROUP) + + @classmethod + def get_action_menu(cls): + action_menu = hawat.menu.Menu() + action_menu.add_entry( + 'endpoint', + 'create', + endpoint='groups.create', + resptitle=True + ) + return action_menu + + @classmethod + def get_context_action_menu(cls): + action_menu = hawat.menu.Menu() + action_menu.add_entry( + 'endpoint', + 'show', + endpoint='groups.show', + hidetitle=True + ) + action_menu.add_entry( + 'endpoint', + 'update', + endpoint='groups.update', + hidetitle=True + ) + action_menu.add_entry( + 'endpoint', + 'disable', + endpoint='groups.disable', + hidetitle=True + ) + action_menu.add_entry( + 'endpoint', + 'enable', + endpoint='groups.enable', + hidetitle=True + ) + action_menu.add_entry( + 'endpoint', + 'delete', + endpoint='groups.delete', + hidetitle=True + ) + return action_menu + + @staticmethod + def get_search_form(request_args): + """ + Must return instance of :py:mod:`flask_wtf.FlaskForm` appropriate for + searching given type of items. + """ + return GroupSearchForm( + request_args, + meta={'csrf': False} + ) + + @staticmethod + def build_query(query, model, form_args): + # Adjust query based on text search string. + if 'search' in form_args and form_args['search']: + query = query \ + .filter( + or_( + model.name.like('%{}%'.format(form_args['search'])), + model.description.like('%{}%'.format(form_args['search'])), + ) + ) + # Adjust query based on lower time boundary selection. + if 'dt_from' in form_args and form_args['dt_from']: + query = query.filter(model.createtime >= form_args['dt_from']) + # Adjust query based on upper time boundary selection. + if 'dt_to' in form_args and form_args['dt_to']: + query = query.filter(model.createtime <= form_args['dt_to']) + # Adjust query based on user state selection. + if 'state' in form_args and form_args['state']: + if form_args['state'] == 'enabled': + query = query.filter(model.enabled == True) + elif form_args['state'] == 'disabled': + query = query.filter(model.enabled == False) + # Adjust query based on record source selection. + if 'source' in form_args and form_args['source']: + query = query \ + .filter(model.source == form_args['source']) + # Adjust query based on user membership selection. + if 'member' in form_args and form_args['member']: + query = query \ + .join(model.members) \ + .filter(model.members.any(id=form_args['member'].id)) + # Adjust query based on user membership selection. + if 'manager' in form_args and form_args['manager']: + query = query \ + .join(model.managers) \ + .filter(model.managers.any(id=form_args['managers'].id)) + if 'sortby' in form_args and form_args['sortby']: + sortmap = { + 'createtime.desc': lambda x, y: x.order_by(y.createtime.desc()), + 'createtime.asc': lambda x, y: x.order_by(y.createtime.asc()), + 'name.desc': lambda x, y: x.order_by(y.name.desc()), + 'name.asc': lambda x, y: x.order_by(y.name.asc()) + } + query = sortmap[form_args['sortby']](query, model) + return query -class ShowView(vial.blueprints.groups.ShowView): +class ShowView(HTMLMixin, SQLAlchemyMixin, ItemShowView): """ Detailed group view. """ + methods = ['GET'] + + authentication = True + + @classmethod + def get_menu_legend(cls, **kwargs): + if isinstance(kwargs['item'], cls.get_model(hawat.const.MODEL_GROUP)): + return lazy_gettext( + 'View details of group "%(item)s"', + item=markupsafe.escape(str(kwargs['item'])) + ) + return lazy_gettext( + 'View details of group "%(item)s"', + item=markupsafe.escape(str(kwargs['item'].group)) + ) + + @classmethod + def get_view_title(cls, **kwargs): + return lazy_gettext('Show group details') + + @classmethod + def get_view_url(cls, **kwargs): + if isinstance(kwargs['item'], cls.get_model(hawat.const.MODEL_GROUP)): + return flask.url_for( + cls.get_view_endpoint(), + item_id=kwargs['item'].get_id() + ) + return flask.url_for( + cls.get_view_endpoint(), + item_id=kwargs['item'].group.get_id() + ) + + @property + def dbmodel(self): + return self.get_model(hawat.const.MODEL_GROUP) + + @classmethod + def authorize_item_action(cls, **kwargs): + permission_mm = flask_principal.Permission( + hawat.acl.MembershipNeed(kwargs['item'].id), + hawat.acl.ManagementNeed(kwargs['item'].id) + ) + return hawat.acl.PERMISSION_POWER.can() or permission_mm.can() + + @classmethod + def get_action_menu(cls): + action_menu = hawat.menu.Menu() + action_menu.add_entry( + 'endpoint', + 'update', + endpoint='groups.update', + resptitle=True + ) + action_menu.add_entry( + 'endpoint', + 'disable', + endpoint='groups.disable', + resptitle=True + ) + action_menu.add_entry( + 'endpoint', + 'enable', + endpoint='groups.enable', + resptitle=True + ) + action_menu.add_entry( + 'endpoint', + 'delete', + endpoint='groups.delete', + resptitle=True + ) + return action_menu + def do_before_response(self, **kwargs): # pylint: disable=locally-disabled,no-self-use,unused-argument - action_menu = vial.menu.Menu() + action_menu = hawat.menu.Menu() action_menu.add_entry( 'endpoint', 'show', - endpoint = 'users.show', - hidetitle = True, + endpoint='users.show', + hidetitle=True, ) action_menu.add_entry( 'submenu', 'more', - align_right = True, - legend = gettext('More actions') + align_right=True, + legend=gettext('More actions') ) action_menu.add_entry( 'endpoint', 'more.add_membership', - endpoint = 'users.addmembership' + endpoint='users.addmembership' ) action_menu.add_entry( 'endpoint', 'more.reject_membership', - endpoint = 'users.rejectmembership' + endpoint='users.rejectmembership' ) action_menu.add_entry( 'endpoint', 'more.remove_membership', - endpoint = 'users.removemembership' + endpoint='users.removemembership' ) action_menu.add_entry( 'endpoint', 'more.add_management', - endpoint = 'users.addmanagement' + endpoint='users.addmanagement' ) action_menu.add_entry( 'endpoint', 'more.remove_management', - endpoint = 'users.removemanagement' + endpoint='users.removemanagement' ) action_menu.add_entry( 'endpoint', 'more.enable', - endpoint = 'users.enable' + endpoint='users.enable' ) action_menu.add_entry( 'endpoint', 'more.disable', - endpoint = 'users.disable' + endpoint='users.disable' ) action_menu.add_entry( 'endpoint', 'more.update', - endpoint = 'users.update' + endpoint='users.update' ) - self.response_context.update(context_action_menu_users = action_menu) + self.response_context.update(context_action_menu_users=action_menu) - action_menu = vial.menu.Menu() + action_menu = hawat.menu.Menu() action_menu.add_entry( 'endpoint', 'show', - endpoint = 'networks.show', - hidetitle = True, + endpoint='networks.show', + hidetitle=True, ) - self.response_context.update(context_action_menu_networks = action_menu) + self.response_context.update(context_action_menu_networks=action_menu) - action_menu = vial.menu.Menu() + action_menu = hawat.menu.Menu() action_menu.add_entry( 'endpoint', 'show', - endpoint = 'filters.show', - hidetitle = True, + endpoint='filters.show', + hidetitle=True, ) - self.response_context.update(context_action_menu_filters = action_menu) + self.response_context.update(context_action_menu_filters=action_menu) item = self.response_context['item'] - if self.can_access_endpoint('groups.update', item = item) and self.has_endpoint('changelogs.search'): + if self.can_access_endpoint('groups.update', item=item) and self.has_endpoint('changelogs.search'): self.response_context.update( - context_action_menu_changelogs = self.get_endpoint_class( + context_action_menu_changelogs=self.get_endpoint_class( 'changelogs.search' ).get_context_action_menu() ) - item_changelog = self.dbsession.query(ItemChangeLogModel).\ + item_changelog = self.dbsession.query(ItemChangeLogModel). \ filter( - or_( - # Changelogs related directly to group item. - and_( - ItemChangeLogModel.model == item.__class__.__name__, - ItemChangeLogModel.model_id == item.id - ), - # Changelogs related to group reporting settings item. - and_( - ItemChangeLogModel.model == SettingsReportingModel.__name__, - ItemChangeLogModel.model_id.in_( - self.dbsession.query(SettingsReportingModel.id).filter(SettingsReportingModel.group_id == item.id) - ) - ), - # Changelogs related to all group reporting filters. - and_( - ItemChangeLogModel.model == FilterModel.__name__, - ItemChangeLogModel.model_id.in_( - self.dbsession.query(FilterModel.id).filter(FilterModel.group_id == item.id) - ) - ), - # Changelogs related to all group network records. - and_( - ItemChangeLogModel.model == NetworkModel.__name__, - ItemChangeLogModel.model_id.in_( - self.dbsession.query(NetworkModel.id).filter(NetworkModel.group_id == item.id) - ) + or_( + # Changelogs related directly to group item. + and_( + ItemChangeLogModel.model == item.__class__.__name__, + ItemChangeLogModel.model_id == item.id + ), + # Changelogs related to group reporting settings item. + and_( + ItemChangeLogModel.model == SettingsReportingModel.__name__, + ItemChangeLogModel.model_id.in_( + self.dbsession.query(SettingsReportingModel.id).filter( + SettingsReportingModel.group_id == item.id) + ) + ), + # Changelogs related to all group reporting filters. + and_( + ItemChangeLogModel.model == FilterModel.__name__, + ItemChangeLogModel.model_id.in_( + self.dbsession.query(FilterModel.id).filter(FilterModel.group_id == item.id) + ) + ), + # Changelogs related to all group network records. + and_( + ItemChangeLogModel.model == NetworkModel.__name__, + ItemChangeLogModel.model_id.in_( + self.dbsession.query(NetworkModel.id).filter(NetworkModel.group_id == item.id) ) ) - ).\ - order_by(ItemChangeLogModel.createtime.desc()).\ - limit(100).\ + ) + ). \ + order_by(ItemChangeLogModel.createtime.desc()). \ + limit(100). \ all() - self.response_context.update(item_changelog = item_changelog) + self.response_context.update(item_changelog=item_changelog) class ShowByNameView(ShowView): # pylint: disable=locally-disabled,too-many-ancestors @@ -188,65 +392,744 @@ class ShowByNameView(ShowView): # pylint: disable=locally-disabled,too-many-anc return self.dbmodel.name -class CreateView(vial.blueprints.groups.CreateView): # pylint: disable=locally-disabled,too-many-ancestors +class CreateView(HTMLMixin, SQLAlchemyMixin, ItemCreateView): # pylint: disable=locally-disabled,too-many-ancestors """ View for creating new groups. """ + methods = ['GET', 'POST'] + + authentication = True + + authorization = [hawat.acl.PERMISSION_POWER] + + @classmethod + def get_menu_title(cls, **kwargs): + return lazy_gettext('Create group') + + @classmethod + def get_view_title(cls, **kwargs): + return lazy_gettext('Create new group') + + @property + def dbmodel(self): + return self.get_model(hawat.const.MODEL_GROUP) + + @property + def dbchlogmodel(self): + return self.get_model(hawat.const.MODEL_ITEM_CHANGELOG) + + @staticmethod + def get_message_success(**kwargs): + return gettext( + 'Group <strong>%(item_id)s</strong> was successfully created.', + item_id=markupsafe.escape(str(kwargs['item'])) + ) + + @staticmethod + def get_message_failure(**kwargs): + return gettext('Unable to create new group.') + + @staticmethod + def get_message_cancel(**kwargs): + return gettext('Canceled creating new group.') + @staticmethod def get_item_form(item): return AdminCreateGroupForm() def do_before_action(self, item): # Create empty reporting settings object and assign it to the group. - SettingsReportingModel(group = item) + SettingsReportingModel(group=item) -class UpdateView(vial.blueprints.groups.UpdateView): # pylint: disable=locally-disabled,too-many-ancestors +class UpdateView(HTMLMixin, SQLAlchemyMixin, ItemUpdateView): # pylint: disable=locally-disabled,too-many-ancestors """ View for updating existing groups. """ + methods = ['GET', 'POST'] + + authentication = True + + @classmethod + def get_menu_title(cls, **kwargs): + return lazy_gettext('Update') + + @classmethod + def get_menu_legend(cls, **kwargs): + return lazy_gettext( + 'Update details of group "%(item)s"', + item=markupsafe.escape(str(kwargs['item'])) + ) + + @classmethod + def get_view_title(cls, **kwargs): + return lazy_gettext('Update group details') + + @property + def dbmodel(self): + return self.get_model(hawat.const.MODEL_GROUP) + + @property + def dbchlogmodel(self): + return self.get_model(hawat.const.MODEL_ITEM_CHANGELOG) + + @classmethod + def authorize_item_action(cls, **kwargs): + permission_m = flask_principal.Permission( + hawat.acl.ManagementNeed(kwargs['item'].id) + ) + return hawat.acl.PERMISSION_POWER.can() or permission_m.can() + + @staticmethod + def get_message_success(**kwargs): + return gettext( + 'Group <strong>%(item_id)s</strong> was successfully updated.', + item_id=markupsafe.escape(str(kwargs['item'])) + ) + + @staticmethod + def get_message_failure(**kwargs): + return gettext( + 'Unable to update group <strong>%(item_id)s</strong>.', + item_id=markupsafe.escape(str(kwargs['item'])) + ) + + @staticmethod + def get_message_cancel(**kwargs): + return gettext( + 'Canceled updating group <strong>%(item_id)s</strong>.', + item_id=markupsafe.escape(str(kwargs['item'])) + ) + @staticmethod def get_item_form(item): admin = flask_login.current_user.has_role('admin') if not admin: - form = UpdateGroupForm(obj = item) + form = UpdateGroupForm(obj=item) else: - form = AdminUpdateGroupForm(db_item_id = item.id, obj = item) + form = AdminUpdateGroupForm(db_item_id=item.id, obj=item) return form -#------------------------------------------------------------------------------- +class AddMemberView(HTMLMixin, SQLAlchemyMixin, + ItemObjectRelationView): # pylint: disable=locally-disabled,too-many-ancestors + """ + View for adding group members. + """ + methods = ['GET', 'POST'] + + authentication = True + + @classmethod + def get_view_name(cls): + return 'addmember' + + @classmethod + def get_view_title(cls, **kwargs): + return gettext('Add group member') + + @classmethod + def get_view_icon(cls): + return 'action-add-member' + + @classmethod + def get_menu_legend(cls, **kwargs): + return lazy_gettext( + 'Add user "%(user_id)s" to group "%(group_id)s"', + user_id=markupsafe.escape(str(kwargs['other'])), + group_id=markupsafe.escape(str(kwargs['item'])) + ) + + @property + def dbmodel(self): + return self.get_model(hawat.const.MODEL_GROUP) + + @property + def dbchlogmodel(self): + return self.get_model(hawat.const.MODEL_ITEM_CHANGELOG) + + @property + def dbmodel_other(self): + return self.get_model(hawat.const.MODEL_USER) + + @classmethod + def authorize_item_action(cls, **kwargs): + permission_m = flask_principal.Permission( + hawat.acl.ManagementNeed(kwargs['item'].id) + ) + return hawat.acl.PERMISSION_POWER.can() or permission_m.can() + + @classmethod + def validate_item_change(cls, **kwargs): # pylint: disable=locally-disabled,unused-argument + # Reject item change in case given item is already enabled. + if kwargs['other'] in kwargs['item'].members: + return False + return True + + @classmethod + def change_item(cls, **kwargs): + kwargs['item'].members.append(kwargs['other']) + try: + kwargs['item'].members_wanted.remove(kwargs['other']) + except ValueError: + pass + if kwargs['other'].is_state_disabled(): + kwargs['other'].set_state_enabled() + flask.current_app.send_infomail( + 'users.enable', + account=kwargs['other'] + ) + + @staticmethod + def get_message_success(**kwargs): + return gettext( + 'User <strong>%(user_id)s</strong> was successfully added as a member to group <strong>%(group_id)s</strong>.', + user_id=markupsafe.escape(str(kwargs['other'])), + group_id=markupsafe.escape(str(kwargs['item'])) + ) + + @staticmethod + def get_message_failure(**kwargs): + return gettext( + 'Unable to add user <strong>%(user_id)s</strong> as a member to group <strong>%(group_id)s</strong>.', + user_id=markupsafe.escape(str(kwargs['other'])), + group_id=markupsafe.escape(str(kwargs['item'])) + ) + + @staticmethod + def get_message_cancel(**kwargs): + return gettext( + 'Canceled adding user <strong>%(user_id)s</strong> as a member to group <strong>%(group_id)s</strong>.', + user_id=markupsafe.escape(str(kwargs['other'])), + group_id=markupsafe.escape(str(kwargs['item'])) + ) + + +class RejectMemberView(HTMLMixin, SQLAlchemyMixin, + ItemObjectRelationView): # pylint: disable=locally-disabled,too-many-ancestors + """ + View for rejecting group membership reuests. + """ + methods = ['GET', 'POST'] + + authentication = True + + @classmethod + def get_view_name(cls): + return 'rejectmember' + + @classmethod + def get_view_title(cls, **kwargs): + return gettext('Reject group member') + + @classmethod + def get_view_icon(cls): + return 'action-rej-member' + + @classmethod + def get_menu_legend(cls, **kwargs): + return lazy_gettext( + 'Reject user`s "%(user_id)s" membership request for group "%(group_id)s"', + user_id=markupsafe.escape(str(kwargs['other'])), + group_id=markupsafe.escape(str(kwargs['item'])) + ) + + @property + def dbmodel(self): + return self.get_model(hawat.const.MODEL_GROUP) + + @property + def dbchlogmodel(self): + return self.get_model(hawat.const.MODEL_ITEM_CHANGELOG) + + @property + def dbmodel_other(self): + return self.get_model(hawat.const.MODEL_USER) + + @classmethod + def authorize_item_action(cls, **kwargs): + permission_m = flask_principal.Permission( + hawat.acl.ManagementNeed(kwargs['item'].id) + ) + return hawat.acl.PERMISSION_POWER.can() or permission_m.can() + + @classmethod + def validate_item_change(cls, **kwargs): # pylint: disable=locally-disabled,unused-argument + # Reject item change in case given item is already enabled. + if kwargs['other'] not in kwargs['item'].members_wanted: + return False + return True + + @classmethod + def change_item(cls, **kwargs): + kwargs['item'].members_wanted.remove(kwargs['other']) + + # --------------------------------------------------------------------------- + + @staticmethod + def get_message_success(**kwargs): + return gettext( + 'User`s <strong>%(user_id)s</strong> membership request for group <strong>%(group_id)s</strong> was successfully rejected.', + user_id=markupsafe.escape(str(kwargs['other'])), + group_id=markupsafe.escape(str(kwargs['item'])) + ) + + @staticmethod + def get_message_failure(**kwargs): + return gettext( + 'Unable to reject user`s <strong>%(user_id)s</strong> membership request for group <strong>%(group_id)s</strong>.', + user_id=markupsafe.escape(str(kwargs['other'])), + group_id=markupsafe.escape(str(kwargs['item'])) + ) + + @staticmethod + def get_message_cancel(**kwargs): + return gettext( + 'Canceled rejecting user`s <strong>%(user_id)s</strong> membership request for group <strong>%(group_id)s</strong>.', + user_id=markupsafe.escape(str(kwargs['other'])), + group_id=markupsafe.escape(str(kwargs['item'])) + ) + + +class RemoveMemberView(HTMLMixin, SQLAlchemyMixin, + ItemObjectRelationView): # pylint: disable=locally-disabled,too-many-ancestors + """ + View for removing group members. + """ + methods = ['GET', 'POST'] + + authentication = True + + @classmethod + def get_view_name(cls): + return 'removemember' + + @classmethod + def get_view_title(cls, **kwargs): + return gettext('Remove group member') + + @classmethod + def get_view_icon(cls): + return 'action-rem-member' + + @classmethod + def get_menu_legend(cls, **kwargs): + return lazy_gettext( + 'Remove user "%(user_id)s" from group "%(group_id)s"', + user_id=markupsafe.escape(str(kwargs['other'])), + group_id=markupsafe.escape(str(kwargs['item'])) + ) + + @property + def dbmodel(self): + return self.get_model(hawat.const.MODEL_GROUP) + + @property + def dbchlogmodel(self): + return self.get_model(hawat.const.MODEL_ITEM_CHANGELOG) + + @property + def dbmodel_other(self): + return self.get_model(hawat.const.MODEL_USER) + + @classmethod + def authorize_item_action(cls, **kwargs): + permission_m = flask_principal.Permission( + hawat.acl.ManagementNeed(kwargs['item'].id) + ) + return hawat.acl.PERMISSION_POWER.can() or permission_m.can() + + @classmethod + def validate_item_change(cls, **kwargs): # pylint: disable=locally-disabled,unused-argument + # Reject item change in case given item is already enabled. + if kwargs['other'] not in kwargs['item'].members: + return False + return True + + @classmethod + def change_item(cls, **kwargs): + kwargs['item'].members.remove(kwargs['other']) + + @staticmethod + def get_message_success(**kwargs): + return gettext( + 'User <strong>%(user_id)s</strong> was successfully removed as a member from group <strong>%(group_id)s</strong>.', + user_id=markupsafe.escape(str(kwargs['other'])), + group_id=markupsafe.escape(str(kwargs['item'])) + ) + + @staticmethod + def get_message_failure(**kwargs): + return gettext( + 'Unable to remove user <strong>%(user_id)s</strong> as a member from group <strong>%(group_id)s</strong>.', + user_id=markupsafe.escape(str(kwargs['other'])), + group_id=markupsafe.escape(str(kwargs['item'])) + ) + + @staticmethod + def get_message_cancel(**kwargs): + return gettext( + 'Canceled removing user <strong>%(user_id)s</strong> as a member from group <strong>%(group_id)s</strong>.', + user_id=markupsafe.escape(str(kwargs['other'])), + group_id=markupsafe.escape(str(kwargs['item'])) + ) + + +class AddManagerView(HTMLMixin, SQLAlchemyMixin, + ItemObjectRelationView): # pylint: disable=locally-disabled,too-many-ancestors + """ + View for adding group managers. + """ + methods = ['GET', 'POST'] + + authentication = True + + @classmethod + def get_view_name(cls): + return 'addmanager' + + @classmethod + def get_view_title(cls, **kwargs): + return gettext('Add group manager') + + @classmethod + def get_view_icon(cls): + return 'action-add-manager' + + @classmethod + def get_menu_legend(cls, **kwargs): + return lazy_gettext( + 'Add user "%(user_id)s" to group "%(group_id)s" as manager', + user_id=markupsafe.escape(str(kwargs['other'])), + group_id=markupsafe.escape(str(kwargs['item'])) + ) + + @property + def dbmodel(self): + return self.get_model(hawat.const.MODEL_GROUP) + + @property + def dbchlogmodel(self): + return self.get_model(hawat.const.MODEL_ITEM_CHANGELOG) + + @property + def dbmodel_other(self): + return self.get_model(hawat.const.MODEL_USER) + + @classmethod + def authorize_item_action(cls, **kwargs): + permission_m = flask_principal.Permission( + hawat.acl.ManagementNeed(kwargs['item'].id) + ) + return hawat.acl.PERMISSION_POWER.can() or permission_m.can() + + @classmethod + def validate_item_change(cls, **kwargs): # pylint: disable=locally-disabled,unused-argument + # Reject item change in case given item is already manager. + if kwargs['other'] in kwargs['item'].managers: + return False + return True + + @classmethod + def change_item(cls, **kwargs): + kwargs['item'].managers.append(kwargs['other']) + if kwargs['other'].is_state_disabled(): + kwargs['other'].set_state_enabled() + flask.current_app.send_infomail( + 'users.enable', + account=kwargs['other'] + ) + + @staticmethod + def get_message_success(**kwargs): + return gettext( + 'User <strong>%(user_id)s</strong> was successfully added as a manager to group <strong>%(group_id)s</strong>.', + user_id=markupsafe.escape(str(kwargs['other'])), + group_id=markupsafe.escape(str(kwargs['item'])) + ) + + @staticmethod + def get_message_failure(**kwargs): + return gettext( + 'Unable to add user <strong>%(user_id)s</strong> as a manager to group <strong>%(group_id)s</strong>.', + user_id=markupsafe.escape(str(kwargs['other'])), + group_id=markupsafe.escape(str(kwargs['item'])) + ) + + @staticmethod + def get_message_cancel(**kwargs): + return gettext( + 'Canceled adding user <strong>%(user_id)s</strong> as a manager to group <strong>%(group_id)s</strong>.', + user_id=markupsafe.escape(str(kwargs['other'])), + group_id=markupsafe.escape(str(kwargs['item'])) + ) + + +class RemoveManagerView(HTMLMixin, SQLAlchemyMixin, + ItemObjectRelationView): # pylint: disable=locally-disabled,too-many-ancestors + """ + View for removing group managers. + """ + methods = ['GET', 'POST'] + + authentication = True + + @classmethod + def get_view_name(cls): + return 'removemanager' + + @classmethod + def get_view_title(cls, **kwargs): + return gettext('Remove group manager') + + @classmethod + def get_view_icon(cls): + return 'action-rem-manager' + + @classmethod + def get_menu_legend(cls, **kwargs): + return lazy_gettext( + 'Remove user "%(user_id)s" from group "%(group_id)s" as manager', + user_id=markupsafe.escape(str(kwargs['other'])), + group_id=markupsafe.escape(str(kwargs['item'])) + ) + + @property + def dbmodel(self): + return self.get_model(hawat.const.MODEL_GROUP) + + @property + def dbchlogmodel(self): + return self.get_model(hawat.const.MODEL_ITEM_CHANGELOG) + + @property + def dbmodel_other(self): + return self.get_model(hawat.const.MODEL_USER) + + @classmethod + def authorize_item_action(cls, **kwargs): + permission_m = flask_principal.Permission( + hawat.acl.ManagementNeed(kwargs['item'].id) + ) + return hawat.acl.PERMISSION_POWER.can() or permission_m.can() + + @classmethod + def validate_item_change(cls, **kwargs): # pylint: disable=locally-disabled,unused-argument + # Reject item change in case given item is not already manager. + if kwargs['other'] not in kwargs['item'].managers: + return False + return True + + @classmethod + def change_item(cls, **kwargs): + try: + kwargs['item'].managers.remove(kwargs['other']) + except ValueError: + pass + + @staticmethod + def get_message_success(**kwargs): + return gettext( + 'User <strong>%(user_id)s</strong> was successfully removed as a manager from group <strong>%(group_id)s</strong>.', + user_id=markupsafe.escape(str(kwargs['other'])), + group_id=markupsafe.escape(str(kwargs['item'])) + ) + + @staticmethod + def get_message_failure(**kwargs): + return gettext( + 'Unable to remove user <strong>%(user_id)s</strong> as a manager from group <strong>%(group_id)s</strong>.', + user_id=markupsafe.escape(str(kwargs['other'])), + group_id=markupsafe.escape(str(kwargs['item'])) + ) + + @staticmethod + def get_message_cancel(**kwargs): + return gettext( + 'Canceled removing user <strong>%(user_id)s</strong> as a manager from group <strong>%(group_id)s</strong>.', + user_id=markupsafe.escape(str(kwargs['other'])), + group_id=markupsafe.escape(str(kwargs['item'])) + ) + +class EnableView(HTMLMixin, SQLAlchemyMixin, ItemEnableView): # pylint: disable=locally-disabled,too-many-ancestors + """ + View for enabling existing groups. + """ + methods = ['GET', 'POST'] + + authentication = True + + authorization = [hawat.acl.PERMISSION_POWER] + + @classmethod + def get_menu_legend(cls, **kwargs): + return lazy_gettext( + 'Enable group "%(item)s"', + item=markupsafe.escape(str(kwargs['item'])) + ) + + @property + def dbmodel(self): + return self.get_model(hawat.const.MODEL_GROUP) + + @property + def dbchlogmodel(self): + return self.get_model(hawat.const.MODEL_ITEM_CHANGELOG) + + @staticmethod + def get_message_success(**kwargs): + return gettext( + 'Group <strong>%(item_id)s</strong> was successfully enabled.', + item_id=markupsafe.escape(str(kwargs['item'])) + ) + + @staticmethod + def get_message_failure(**kwargs): + return gettext( + 'Unable to enable group <strong>%(item_id)s</strong>.', + item_id=markupsafe.escape(str(kwargs['item'])) + ) + + @staticmethod + def get_message_cancel(**kwargs): + return gettext( + 'Canceled enabling group <strong>%(item_id)s</strong>.', + item_id=markupsafe.escape(str(kwargs['item'])) + ) + + +class DisableView(HTMLMixin, SQLAlchemyMixin, ItemDisableView): # pylint: disable=locally-disabled,too-many-ancestors + """ + View for disabling groups. + """ + methods = ['GET', 'POST'] -class GroupsBlueprint(vial.blueprints.groups.GroupsBlueprint): + authentication = True + + authorization = [hawat.acl.PERMISSION_POWER] + + @classmethod + def get_menu_legend(cls, **kwargs): + return lazy_gettext( + 'Disable group "%(item)s"', + item=markupsafe.escape(str(kwargs['item'])) + ) + + @property + def dbmodel(self): + return self.get_model(hawat.const.MODEL_GROUP) + + @property + def dbchlogmodel(self): + return self.get_model(hawat.const.MODEL_ITEM_CHANGELOG) + + @staticmethod + def get_message_success(**kwargs): + return gettext( + 'Group <strong>%(item_id)s</strong> was successfully disabled.', + item_id=markupsafe.escape(str(kwargs['item'])) + ) + + @staticmethod + def get_message_failure(**kwargs): + return gettext( + 'Unable to disable group <strong>%(item_id)s</strong>.', + item_id=markupsafe.escape(str(kwargs['item'])) + ) + + @staticmethod + def get_message_cancel(**kwargs): + return gettext( + 'Canceled disabling group <strong>%(item_id)s</strong>.', + item_id=markupsafe.escape(str(kwargs['item'])) + ) + + +class DeleteView(HTMLMixin, SQLAlchemyMixin, ItemDeleteView): # pylint: disable=locally-disabled,too-many-ancestors + """ + View for deleting existing groups. + """ + + methods = ['GET', 'POST'] + + authentication = True + + authorization = [hawat.acl.PERMISSION_ADMIN] + + @classmethod + def get_menu_legend(cls, **kwargs): + return lazy_gettext( + 'Delete group "%(item)s"', + item=markupsafe.escape(str(kwargs['item'])) + ) + + @property + def dbmodel(self): + return self.get_model(hawat.const.MODEL_GROUP) + + @property + def dbchlogmodel(self): + return self.get_model(hawat.const.MODEL_ITEM_CHANGELOG) + + @staticmethod + def get_message_success(**kwargs): + return gettext( + 'Group <strong>%(item_id)s</strong> was successfully and permanently deleted.', + item_id=markupsafe.escape(str(kwargs['item'])) + ) + + @staticmethod + def get_message_failure(**kwargs): + return gettext( + 'Unable to delete group <strong>%(item_id)s</strong>.', + item_id=markupsafe.escape(str(kwargs['item'])) + ) + + @staticmethod + def get_message_cancel(**kwargs): + return gettext( + 'Canceled deleting group <strong>%(item_id)s</strong>.', + item_id=markupsafe.escape(str(kwargs['item'])) + ) + + +# ------------------------------------------------------------------------------- + + +class GroupsBlueprint(HawatBlueprint): """Pluggable module - user groups (*groups*).""" - def register_app(self, app): + @classmethod + def get_module_title(cls): + return lazy_gettext('Group management') + def register_app(self, app): def _fetch_my_groups(): groups = {} for i in list(flask_login.current_user.memberships) + list(flask_login.current_user.managements): groups[str(i)] = i - return list(sorted(groups.values(), key = str)) + return list(sorted(groups.values(), key=str)) app.menu_main.add_entry( 'view', 'admin.{}'.format(BLUEPRINT_NAME), - position = 50, - view = ListView + position=50, + view=ListView ) app.menu_auth.add_entry( 'submenudb', 'my_groups', - position = 20, - title = lazy_gettext('My groups'), - resptitle = True, - icon = 'module-groups', - align_right = True, - entry_fetcher = _fetch_my_groups, - entry_builder = lambda x, y: vial.menu.EndpointEntry(x, endpoint = 'groups.show', params = {'item': y}, title = x, icon = 'module-groups') + position=20, + title=lazy_gettext('My groups'), + resptitle=True, + icon='module-groups', + align_right=True, + entry_fetcher=_fetch_my_groups, + entry_builder=lambda x, y: hawat.menu.EndpointEntry(x, endpoint='groups.show', params={'item': y}, title=x, + icon='module-groups') ) # Register context actions provided by this module. @@ -258,35 +1141,35 @@ class GroupsBlueprint(vial.blueprints.groups.GroupsBlueprint): ) -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- def get_blueprint(): """ - Mandatory interface for :py:mod:`vial.Vial` and factory function. This function - must return a valid instance of :py:class:`vial.app.VialBlueprint` or + Mandatory interface for :py:mod:`hawat.Hawat` and factory function. This function + must return a valid instance of :py:class:`hawat.app.HawatBlueprint` or :py:class:`flask.Blueprint`. """ hbp = GroupsBlueprint( BLUEPRINT_NAME, __name__, - template_folder = 'templates', - url_prefix = '/{}'.format(BLUEPRINT_NAME) + template_folder='templates', + url_prefix='/{}'.format(BLUEPRINT_NAME) ) - hbp.register_view_class(ListView, '/list') - hbp.register_view_class(CreateView, '/create') - hbp.register_view_class(ShowView, '/<int:item_id>/show') - hbp.register_view_class(ShowByNameView, '/<item_id>/show_by_name') - hbp.register_view_class(UpdateView, '/<int:item_id>/update') - hbp.register_view_class(AddMemberView, '/<int:item_id>/add_member/<int:other_id>') - hbp.register_view_class(RejectMemberView, '/<int:item_id>/reject_member/<int:other_id>') - hbp.register_view_class(RemoveMemberView, '/<int:item_id>/remove_member/<int:other_id>') - hbp.register_view_class(AddManagerView, '/<int:item_id>/add_manager/<int:other_id>') + hbp.register_view_class(ListView, '/list') + hbp.register_view_class(CreateView, '/create') + hbp.register_view_class(ShowView, '/<int:item_id>/show') + hbp.register_view_class(ShowByNameView, '/<item_id>/show_by_name') + hbp.register_view_class(UpdateView, '/<int:item_id>/update') + hbp.register_view_class(AddMemberView, '/<int:item_id>/add_member/<int:other_id>') + hbp.register_view_class(RejectMemberView, '/<int:item_id>/reject_member/<int:other_id>') + hbp.register_view_class(RemoveMemberView, '/<int:item_id>/remove_member/<int:other_id>') + hbp.register_view_class(AddManagerView, '/<int:item_id>/add_manager/<int:other_id>') hbp.register_view_class(RemoveManagerView, '/<int:item_id>/remove_manager/<int:other_id>') - hbp.register_view_class(EnableView, '/<int:item_id>/enable') - hbp.register_view_class(DisableView, '/<int:item_id>/disable') - hbp.register_view_class(DeleteView, '/<int:item_id>/delete') + hbp.register_view_class(EnableView, '/<int:item_id>/enable') + hbp.register_view_class(DisableView, '/<int:item_id>/disable') + hbp.register_view_class(DeleteView, '/<int:item_id>/delete') return hbp diff --git a/lib/hawat/blueprints/groups/forms.py b/lib/hawat/blueprints/groups/forms.py index 865a31da20d94b05e379dd403d144eaaf30400dd..8566094b93a54c4084f4c9f672a1aa70cede1fa1 100644 --- a/lib/hawat/blueprints/groups/forms.py +++ b/lib/hawat/blueprints/groups/forms.py @@ -1,22 +1,20 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ This module contains custom group management forms for Hawat. """ - __author__ = "Jan Mach <jan.mach@cesnet.cz>" __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" - import sqlalchemy import wtforms from wtforms.ext.sqlalchemy.fields import QuerySelectField, QuerySelectMultipleField @@ -29,18 +27,19 @@ from flask_babel import gettext, lazy_gettext # # Custom modules. # -import vial.db -import vial.forms -from mentat.datatype.sqldb import GroupModel, UserModel +import hawat.db +import hawat.forms +from hawat.forms import get_available_users, get_available_group_sources +from mentat.datatype.sqldb import GroupModel -def check_name_existence(form, field): # pylint: disable=locally-disabled,unused-argument +def check_name_existence(_form, field): # pylint: disable=locally-disabled,unused-argument """ Callback for validating user logins during account create action. """ try: - vial.db.db_get().session.query(GroupModel).\ - filter(GroupModel.name == field.data).\ + hawat.db.db_get().session.query(GroupModel). \ + filter(GroupModel.name == field.data). \ one() except sqlalchemy.orm.exc.NoResultFound: return @@ -53,9 +52,9 @@ def check_name_uniqueness(form, field): """ Callback for validating user logins during account update action. """ - item = vial.db.db_get().session.query(GroupModel).\ - filter(GroupModel.name == field.data).\ - filter(GroupModel.id != form.db_item_id).\ + item = hawat.db.db_get().session.query(GroupModel). \ + filter(GroupModel.name == field.data). \ + filter(GroupModel.id != form.db_item_id). \ all() if not item: return @@ -67,14 +66,8 @@ def check_parent_not_self(form, field): Callback for validating that parent group is not self. """ if field.data and form.db_item_id == field.data.id: - raise wtforms.validators.ValidationError(gettext('You must not select a group as its own parent! Naughty, naughty you!')) - - -def get_available_users(): - """ - Query the database for list of all available user accounts. - """ - return vial.db.db_query(UserModel).order_by(UserModel.fullname).all() + raise wtforms.validators.ValidationError( + gettext('You must not select a group as its own parent! Naughty, naughty you!')) def format_select_option_label_user(item): @@ -88,34 +81,35 @@ def get_available_groups(): """ Query the database for list of all available groups. """ - return vial.db.db_query(GroupModel).order_by(GroupModel.name).all() + return hawat.db.db_query(GroupModel).order_by(GroupModel.name).all() -class BaseGroupForm(vial.forms.BaseItemForm): +class BaseGroupForm(hawat.forms.BaseItemForm): """ Class representing base group form. """ description = wtforms.StringField( lazy_gettext('Description:'), - validators = [ + validators=[ wtforms.validators.DataRequired() ], - description = lazy_gettext('Additional and more extensive group description.') + description=lazy_gettext('Additional and more extensive group description.') ) source = wtforms.HiddenField( - default = 'manual', - validators = [ + default='manual', + validators=[ wtforms.validators.DataRequired(), - wtforms.validators.Length(min = 3, max = 50) + wtforms.validators.Length(min=3, max=50) ], - description = lazy_gettext('Origin of the group record, whether it was added manually, or via some automated mechanism from data from some third party system.') + description=lazy_gettext( + 'Origin of the group record, whether it was added manually, or via some automated mechanism from data from some third party system.') ) members = QuerySelectMultipleField( lazy_gettext('Members:'), - query_factory = get_available_users, - get_label = format_select_option_label_user, - blank_text = lazy_gettext('<< no selection >>'), - description = lazy_gettext('List of group members.') + query_factory=hawat.forms.get_available_users, + get_label=format_select_option_label_user, + blank_text=lazy_gettext('<< no selection >>'), + description=lazy_gettext('List of group members.') ) submit = wtforms.SubmitField( lazy_gettext('Submit') @@ -137,34 +131,37 @@ class AdminBaseGroupForm(BaseGroupForm): """ enabled = wtforms.RadioField( lazy_gettext('State:'), - validators = [ + validators=[ wtforms.validators.InputRequired(), ], - choices = [ - (True, lazy_gettext('Enabled')), + choices=[ + (True, lazy_gettext('Enabled')), (False, lazy_gettext('Disabled')) ], - filters = [vial.forms.str_to_bool], - coerce = vial.forms.str_to_bool, - description = lazy_gettext('Boolean flag whether the group is enabled or disabled. Disabled groups are hidden to the most of the system features.') + filters=[hawat.forms.str_to_bool], + coerce=hawat.forms.str_to_bool, + description=lazy_gettext( + 'Boolean flag whether the group is enabled or disabled. Disabled groups are hidden to the most of the system features.') ) managers = QuerySelectMultipleField( lazy_gettext('Managers:'), - query_factory = get_available_users, - get_label = format_select_option_label_user, - blank_text = lazy_gettext('<< no selection >>'), - description = lazy_gettext('List of users acting as group managers. These users may change various group settings.') + query_factory=hawat.forms.get_available_users, + get_label=format_select_option_label_user, + blank_text=lazy_gettext('<< no selection >>'), + description=lazy_gettext( + 'List of users acting as group managers. These users may change various group settings.') ) parent = QuerySelectField( lazy_gettext('Parent group:'), - validators = [ + validators=[ wtforms.validators.Optional(), check_parent_not_self ], - query_factory = get_available_groups, - allow_blank = True, - blank_text = lazy_gettext('<< no selection >>'), - description = lazy_gettext('Parent group for this group. This feature enables the posibility to create structured group hierarchy.') + query_factory=hawat.forms.get_available_groups, + allow_blank=True, + blank_text=lazy_gettext('<< no selection >>'), + description=lazy_gettext( + 'Parent group for this group. This feature enables the posibility to create structured group hierarchy.') ) @@ -174,12 +171,12 @@ class AdminCreateGroupForm(AdminBaseGroupForm): """ name = wtforms.StringField( lazy_gettext('Name:'), - validators = [ + validators=[ wtforms.validators.DataRequired(), - wtforms.validators.Length(min = 3, max = 100), - check_name_existence + wtforms.validators.Length(min=3, max=100), + hawat.forms.check_unique_group ], - description = lazy_gettext('System-wide unique name for the group.') + description=lazy_gettext('System-wide unique name for the group.') ) def __init__(self, *args, **kwargs): @@ -194,12 +191,12 @@ class AdminUpdateGroupForm(AdminBaseGroupForm): """ name = wtforms.StringField( lazy_gettext('Name:'), - validators = [ + validators=[ wtforms.validators.DataRequired(), - wtforms.validators.Length(min = 3, max = 100), - check_name_uniqueness + wtforms.validators.Length(min=3, max=100), + hawat.forms.check_unique_group ], - description = lazy_gettext('System-wide unique name for the group.') + description=lazy_gettext('System-wide unique name for the group.') ) def __init__(self, *args, **kwargs): @@ -208,3 +205,109 @@ class AdminUpdateGroupForm(AdminBaseGroupForm): # Store the ID of original item in database to enable the ID uniqueness # check with check_name_uniqueness() validator. self.db_item_id = kwargs['db_item_id'] + + +class GroupSearchForm(hawat.forms.BaseSearchForm): + """ + Class representing simple user search form. + """ + search = wtforms.StringField( + lazy_gettext('Name, description:'), + validators=[ + wtforms.validators.Optional(), + wtforms.validators.Length(min=3, max=100) + ], + description=lazy_gettext( + 'Group`s full name or description. Search is performed even in the middle of the strings.') + ) + dt_from = hawat.forms.SmartDateTimeField( + lazy_gettext('Creation time from:'), + validators=[ + wtforms.validators.Optional() + ], + description=lazy_gettext( + 'Lower time boundary for item creation time. Timestamp is expected to be in the format <code>YYYY-MM-DD hh:mm:ss</code> and in the timezone according to the user`s preferences.') + ) + dt_to = hawat.forms.SmartDateTimeField( + lazy_gettext('Creation time to:'), + validators=[ + wtforms.validators.Optional() + ], + description=lazy_gettext( + 'Upper time boundary for item creation time. Timestamp is expected to be in the format <code>YYYY-MM-DD hh:mm:ss</code> and in the timezone according to the user`s preferences.') + ) + + state = wtforms.SelectField( + lazy_gettext('State:'), + validators=[ + wtforms.validators.Optional(), + ], + choices=[ + ('', lazy_gettext('Nothing selected')), + ('enabled', lazy_gettext('Enabled')), + ('disabled', lazy_gettext('Disabled')) + ], + default='', + description=lazy_gettext('Search for groups with particular state.') + ) + source = wtforms.SelectField( + lazy_gettext('Record source:'), + validators=[ + wtforms.validators.Optional() + ], + default='', + description=lazy_gettext('Search for groups coming from particular sources/feeds.') + ) + members = QuerySelectField( + lazy_gettext('Group members:'), + query_factory=get_available_users, + allow_blank=True, + description=lazy_gettext('Search for groups with particular members.') + ) + managers = QuerySelectField( + lazy_gettext('Group managers:'), + query_factory=get_available_users, + allow_blank=True, + description=lazy_gettext('Search for groups with particular managers.') + ) + + sortby = wtforms.SelectField( + lazy_gettext('Sort by:'), + validators=[ + wtforms.validators.Optional() + ], + choices=[ + ('createtime.desc', lazy_gettext('by creation time descending')), + ('createtime.asc', lazy_gettext('by creation time ascending')), + ('name.desc', lazy_gettext('by name descending')), + ('name.asc', lazy_gettext('by name ascending')) + ], + default='name.asc' + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # + # Handle additional custom keywords. + # + + # The list of choices for 'roles' attribute comes from outside of the + # form to provide as loose tie as possible to the outer application. + # Another approach would be to load available choices here with: + # + # roles = flask.current_app.config['ROLES'] + # + # That would mean direct dependency on flask.Flask application. + source_list = get_available_group_sources() + self.source.choices = [('', lazy_gettext('Nothing selected'))] + list(zip(source_list, source_list)) + + @staticmethod + def is_multivalue(field_name): + """ + Check, if given form field is a multivalue field. + + :param str field_name: Name of the form field. + :return: ``True``, if the field can contain multiple values, ``False`` otherwise. + :rtype: bool + """ + return False diff --git a/lib/hawat/blueprints/groups/templates/groups/show.html b/lib/hawat/blueprints/groups/templates/groups/show.html index 84ba2992181bd33a4a3435630df4a1577e808fac..67c390a866fbc8a7b534c7d0808b2880f4b28e0d 100644 --- a/lib/hawat/blueprints/groups/templates/groups/show.html +++ b/lib/hawat/blueprints/groups/templates/groups/show.html @@ -6,7 +6,7 @@ <div class="col-lg-12"> {{ macros_page.render_breadcrumbs(item) }} - <h2>{{ vial_current_view.get_view_title() }}</h2> + <h2>{{ hawat_current_view.get_view_title() }}</h2> <hr> <h3>{{ item.name }}{% if item.description %} <small>{{ item.description }}</small>{% endif %}</h3> <div class="pull-right"> diff --git a/lib/hawat/blueprints/groups/test/__init__.py b/lib/hawat/blueprints/groups/test/__init__.py index a0a507083f15a4031ba87c83ec7e94b500523669..0a38a7cf455805dbea3b166b6c430ab12aed14c4 100644 --- a/lib/hawat/blueprints/groups/test/__init__.py +++ b/lib/hawat/blueprints/groups/test/__init__.py @@ -1,29 +1,28 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ Unit tests for :py:mod:`hawat.blueprints.groups`. """ - import unittest -import vial.const -import vial.test -import vial.test.fixtures -import vial.db -from vial.test import VialTestCase, ItemCreateVialTestCase +import hawat.const +import hawat.test +import hawat.test.fixtures +import hawat.db +from hawat.test import HawatTestCase, ItemCreateHawatTestCase from hawat.test.runner import TestRunnerMixin -class GroupsListTestCase(TestRunnerMixin, VialTestCase): +class GroupsListTestCase(TestRunnerMixin, HawatTestCase): """Class for testing ``groups.list`` endpoint.""" def _attempt_fail(self): @@ -42,32 +41,32 @@ class GroupsListTestCase(TestRunnerMixin, VialTestCase): ] ) - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_USER) def test_01_as_user(self): """Test access as user ``user``.""" self._attempt_fail() - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_DEVELOPER) def test_02_as_developer(self): """Test access as user ``developer``.""" self._attempt_fail() - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_MAINTAINER) def test_03_as_maintainer(self): """Test access as user ``maintainer``.""" self._attempt_succeed() - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_ADMIN) def test_04_as_admin(self): """Test access as user ``admin``.""" self._attempt_succeed() -class GroupsShowTestCase(TestRunnerMixin, VialTestCase): +class GroupsShowTestCase(TestRunnerMixin, HawatTestCase): """Base class for testing ``groups.show`` and ``groups.show_by_name`` endpoints.""" def _attempt_fail(self, gname): - gid = self.group_id(gname, with_app_ctx = True) + gid = self.group_id(gname, with_app_ctx=True) self.assertGetURL( '/groups/{}/show'.format(gid), 403 @@ -78,7 +77,7 @@ class GroupsShowTestCase(TestRunnerMixin, VialTestCase): ) def _attempt_succeed(self, gname): - gid = self.group_id(gname, with_app_ctx = True) + gid = self.group_id(gname, with_app_ctx=True) self.assertGetURL( '/groups/{}/show'.format(gid), 200, @@ -96,48 +95,48 @@ class GroupsShowTestCase(TestRunnerMixin, VialTestCase): ] ) - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_USER) def test_01_as_user(self): """ Test access as user 'user'. Only power user is able to view all available groups. """ - self._attempt_succeed(vial.test.fixtures.DEMO_GROUP_A) - self._attempt_fail(vial.test.fixtures.DEMO_GROUP_B) + self._attempt_succeed(hawat.test.fixtures.DEMO_GROUP_A) + self._attempt_fail(hawat.test.fixtures.DEMO_GROUP_B) - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_DEVELOPER) def test_02_as_developer(self): """ Test access as user 'developer'. Only power user is able to view all available groups. """ - self._attempt_succeed(vial.test.fixtures.DEMO_GROUP_A) - self._attempt_fail(vial.test.fixtures.DEMO_GROUP_B) + self._attempt_succeed(hawat.test.fixtures.DEMO_GROUP_A) + self._attempt_fail(hawat.test.fixtures.DEMO_GROUP_B) - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_MAINTAINER) def test_03_as_maintainer(self): """ Test access as user 'maintainer'. Only power user is able to view all available groups. """ - self._attempt_succeed(vial.test.fixtures.DEMO_GROUP_A) - self._attempt_succeed(vial.test.fixtures.DEMO_GROUP_B) + self._attempt_succeed(hawat.test.fixtures.DEMO_GROUP_A) + self._attempt_succeed(hawat.test.fixtures.DEMO_GROUP_B) - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_ADMIN) def test_04_as_admin(self): """ Test access as user 'admin'. Only power user is able to view all available groups. """ - self._attempt_succeed(vial.test.fixtures.DEMO_GROUP_A) - self._attempt_succeed(vial.test.fixtures.DEMO_GROUP_B) + self._attempt_succeed(hawat.test.fixtures.DEMO_GROUP_A) + self._attempt_succeed(hawat.test.fixtures.DEMO_GROUP_B) -class GroupsCreateTestCase(TestRunnerMixin, ItemCreateVialTestCase): +class GroupsCreateTestCase(TestRunnerMixin, ItemCreateHawatTestCase): """Class for testing ``groups.create`` endpoint.""" group_data_fixture = [ @@ -162,39 +161,39 @@ class GroupsCreateTestCase(TestRunnerMixin, ItemCreateVialTestCase): ] ) - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_USER) def test_01_as_user(self): """Test access as user 'user'.""" self._attempt_fail() - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_DEVELOPER) def test_02_as_developer(self): """Test access as user 'developer'.""" self._attempt_fail() - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_MAINTAINER) def test_03_as_maintainer(self): """Test access as user 'maintainer'.""" self._attempt_succeed() - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_ADMIN) def test_04_as_admin(self): """Test access as user 'admin'.""" self._attempt_succeed() -class GroupsUpdateTestCase(TestRunnerMixin, VialTestCase): +class GroupsUpdateTestCase(TestRunnerMixin, HawatTestCase): """Class for testing ``groups.update`` endpoint.""" def _attempt_fail(self, gname): - gid = self.group_id(gname, with_app_ctx = True) + gid = self.group_id(gname, with_app_ctx=True) self.assertGetURL( '/groups/{}/update'.format(gid), 403 ) def _attempt_succeed(self, gname): - gid = self.group_id(gname, with_app_ctx = True) + gid = self.group_id(gname, with_app_ctx=True) self.assertGetURL( '/groups/{}/update'.format(gid), 200, @@ -203,36 +202,36 @@ class GroupsUpdateTestCase(TestRunnerMixin, VialTestCase): ] ) - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_USER) def test_01_as_user(self): """Test access as user 'user'.""" - self._attempt_fail(vial.test.fixtures.DEMO_GROUP_A) - self._attempt_fail(vial.test.fixtures.DEMO_GROUP_B) + self._attempt_fail(hawat.test.fixtures.DEMO_GROUP_A) + self._attempt_fail(hawat.test.fixtures.DEMO_GROUP_B) - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_DEVELOPER) def test_04_as_developer(self): """Test access as user 'developer'.""" - self._attempt_succeed(vial.test.fixtures.DEMO_GROUP_A) - self._attempt_fail(vial.test.fixtures.DEMO_GROUP_B) + self._attempt_succeed(hawat.test.fixtures.DEMO_GROUP_A) + self._attempt_fail(hawat.test.fixtures.DEMO_GROUP_B) - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_MAINTAINER) def test_05_as_maintainer(self): """Test access as user 'maintainer'.""" - self._attempt_succeed(vial.test.fixtures.DEMO_GROUP_A) - self._attempt_succeed(vial.test.fixtures.DEMO_GROUP_B) + self._attempt_succeed(hawat.test.fixtures.DEMO_GROUP_A) + self._attempt_succeed(hawat.test.fixtures.DEMO_GROUP_B) - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_ADMIN) def test_06_as_admin(self): """Test access as user 'admin'.""" - self._attempt_succeed(vial.test.fixtures.DEMO_GROUP_A) - self._attempt_succeed(vial.test.fixtures.DEMO_GROUP_B) + self._attempt_succeed(hawat.test.fixtures.DEMO_GROUP_A) + self._attempt_succeed(hawat.test.fixtures.DEMO_GROUP_B) -class GroupsEnableDisableTestCase(TestRunnerMixin, VialTestCase): +class GroupsEnableDisableTestCase(TestRunnerMixin, HawatTestCase): """Class for testing ``groups.enable`` and ``groups.disable`` endpoint.""" def _attempt_fail(self, gname): - gid = self.group_id(gname, with_app_ctx = True) + gid = self.group_id(gname, with_app_ctx=True) self.assertGetURL( '/groups/{}/disable'.format(gid), 403 @@ -243,7 +242,7 @@ class GroupsEnableDisableTestCase(TestRunnerMixin, VialTestCase): ) def _attempt_succeed(self, gname): - gid = self.group_id(gname, with_app_ctx = True) + gid = self.group_id(gname, with_app_ctx=True) self.assertGetURL( '/groups/{}/disable'.format(gid), 200, @@ -279,32 +278,32 @@ class GroupsEnableDisableTestCase(TestRunnerMixin, VialTestCase): ] ) - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_USER) def test_01_as_user(self): """Test access as user 'user'.""" - self._attempt_fail(vial.test.fixtures.DEMO_GROUP_A) - self._attempt_fail(vial.test.fixtures.DEMO_GROUP_B) + self._attempt_fail(hawat.test.fixtures.DEMO_GROUP_A) + self._attempt_fail(hawat.test.fixtures.DEMO_GROUP_B) - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_DEVELOPER) def test_02_as_developer(self): """Test access as user 'developer'.""" - self._attempt_fail(vial.test.fixtures.DEMO_GROUP_A) - self._attempt_fail(vial.test.fixtures.DEMO_GROUP_B) + self._attempt_fail(hawat.test.fixtures.DEMO_GROUP_A) + self._attempt_fail(hawat.test.fixtures.DEMO_GROUP_B) - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_MAINTAINER) def test_03_as_maintainer(self): """Test access as user 'maintainer'.""" - self._attempt_succeed(vial.test.fixtures.DEMO_GROUP_A) - self._attempt_succeed(vial.test.fixtures.DEMO_GROUP_B) + self._attempt_succeed(hawat.test.fixtures.DEMO_GROUP_A) + self._attempt_succeed(hawat.test.fixtures.DEMO_GROUP_B) - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_ADMIN) def test_04_as_admin(self): """Test access as user 'admin'.""" - self._attempt_succeed(vial.test.fixtures.DEMO_GROUP_A) - self._attempt_succeed(vial.test.fixtures.DEMO_GROUP_B) + self._attempt_succeed(hawat.test.fixtures.DEMO_GROUP_A) + self._attempt_succeed(hawat.test.fixtures.DEMO_GROUP_B) -class GroupsAddRemRejMemberTestCase(TestRunnerMixin, VialTestCase): +class GroupsAddRemRejMemberTestCase(TestRunnerMixin, HawatTestCase): """Class for testing ``groups.add_member``, ``groups.reject_member`` and ``groups.remove_member`` endpoint.""" def _attempt_fail(self, uname, gname): @@ -324,7 +323,7 @@ class GroupsAddRemRejMemberTestCase(TestRunnerMixin, VialTestCase): 403 ) - def _attempt_succeed(self, uname, gname, print_response = False): + def _attempt_succeed(self, uname, gname, print_response=False): # Additional test preparations. with self.app.app_context(): uid = self.user_id(uname) @@ -429,60 +428,60 @@ class GroupsAddRemRejMemberTestCase(TestRunnerMixin, VialTestCase): ) self.mailbox_monitoring('off') - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_USER) def test_01_as_user(self): """Test access as user 'user'.""" for uname in ( - vial.const.ROLE_USER, - vial.const.ROLE_DEVELOPER, - vial.const.ROLE_MAINTAINER, - vial.const.ROLE_ADMIN - ): - self._attempt_fail(uname, vial.test.fixtures.DEMO_GROUP_A) - - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) + hawat.const.ROLE_USER, + hawat.const.ROLE_DEVELOPER, + hawat.const.ROLE_MAINTAINER, + hawat.const.ROLE_ADMIN + ): + self._attempt_fail(uname, hawat.test.fixtures.DEMO_GROUP_A) + + @hawat.test.do_as_user_decorator(hawat.const.ROLE_DEVELOPER) def test_02_as_developer(self): """Test access as user 'developer'.""" for uname in ( - vial.const.ROLE_USER, - vial.const.ROLE_MAINTAINER, - vial.const.ROLE_ADMIN - ): - self._attempt_succeed(uname, vial.test.fixtures.DEMO_GROUP_A) + hawat.const.ROLE_USER, + hawat.const.ROLE_MAINTAINER, + hawat.const.ROLE_ADMIN + ): + self._attempt_succeed(uname, hawat.test.fixtures.DEMO_GROUP_A) - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_MAINTAINER) def test_03_as_maintainer(self): """Test access as user 'maintainer'.""" for uname in ( - vial.const.ROLE_USER, - vial.const.ROLE_DEVELOPER, - vial.const.ROLE_ADMIN - ): - self._attempt_succeed(uname, vial.test.fixtures.DEMO_GROUP_A) + hawat.const.ROLE_USER, + hawat.const.ROLE_DEVELOPER, + hawat.const.ROLE_ADMIN + ): + self._attempt_succeed(uname, hawat.test.fixtures.DEMO_GROUP_A) - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_ADMIN) def test_04_as_admin(self): """Test access as user 'admin'.""" for uname in ( - vial.const.ROLE_USER, - vial.const.ROLE_DEVELOPER, - vial.const.ROLE_MAINTAINER - ): - self._attempt_succeed(uname, vial.test.fixtures.DEMO_GROUP_A) + hawat.const.ROLE_USER, + hawat.const.ROLE_DEVELOPER, + hawat.const.ROLE_MAINTAINER + ): + self._attempt_succeed(uname, hawat.test.fixtures.DEMO_GROUP_A) -class GroupsDeleteTestCase(TestRunnerMixin, VialTestCase): +class GroupsDeleteTestCase(TestRunnerMixin, HawatTestCase): """Class for testing ``groups.delete`` endpoint.""" def _attempt_fail(self, gname): - gid = self.group_id(gname, with_app_ctx = True) + gid = self.group_id(gname, with_app_ctx=True) self.assertGetURL( '/groups/{}/delete'.format(gid), 403 ) def _attempt_succeed(self, gname): - gid = self.group_id(gname, with_app_ctx = True) + gid = self.group_id(gname, with_app_ctx=True) self.assertGetURL( '/groups/{}/delete'.format(gid), 200, @@ -501,32 +500,32 @@ class GroupsDeleteTestCase(TestRunnerMixin, VialTestCase): ] ) - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_USER) def test_01_as_user(self): """Test access as user 'user'.""" - self._attempt_fail(vial.test.fixtures.DEMO_GROUP_A) - self._attempt_fail(vial.test.fixtures.DEMO_GROUP_B) + self._attempt_fail(hawat.test.fixtures.DEMO_GROUP_A) + self._attempt_fail(hawat.test.fixtures.DEMO_GROUP_B) - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_DEVELOPER) def test_02_as_developer(self): """Test access as user 'developer'.""" - self._attempt_fail(vial.test.fixtures.DEMO_GROUP_A) - self._attempt_fail(vial.test.fixtures.DEMO_GROUP_B) + self._attempt_fail(hawat.test.fixtures.DEMO_GROUP_A) + self._attempt_fail(hawat.test.fixtures.DEMO_GROUP_B) - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_MAINTAINER) def test_03_as_maintainer(self): """Test access as user 'maintainer'.""" - self._attempt_fail(vial.test.fixtures.DEMO_GROUP_A) - self._attempt_fail(vial.test.fixtures.DEMO_GROUP_B) + self._attempt_fail(hawat.test.fixtures.DEMO_GROUP_A) + self._attempt_fail(hawat.test.fixtures.DEMO_GROUP_B) - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_ADMIN) def test_04_as_admin(self): """Test access as user 'admin'.""" - self._attempt_succeed(vial.test.fixtures.DEMO_GROUP_A) - self._attempt_succeed(vial.test.fixtures.DEMO_GROUP_B) + self._attempt_succeed(hawat.test.fixtures.DEMO_GROUP_A) + self._attempt_succeed(hawat.test.fixtures.DEMO_GROUP_B) -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- if __name__ == "__main__": diff --git a/lib/hawat/blueprints/home/__init__.py b/lib/hawat/blueprints/home/__init__.py index 114837490245839526fb9320002434f18cf54b66..ece978fe360ced2ba6f021b5160bd6920b4bc9dd 100644 --- a/lib/hawat/blueprints/home/__init__.py +++ b/lib/hawat/blueprints/home/__init__.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ @@ -22,17 +22,14 @@ Provided endpoints * *Methods:* ``GET`` """ - __author__ = "Jan Mach <jan.mach@cesnet.cz>" __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" - from flask_babel import lazy_gettext -from vial.app import VialBlueprint -from vial.view import SimpleView -from vial.view.mixin import HTMLMixin - +from hawat.base import HawatBlueprint +from hawat.view import SimpleView +from hawat.view.mixin import HTMLMixin BLUEPRINT_NAME = 'home' """Name of the blueprint as module global constant.""" @@ -42,7 +39,7 @@ class IndexView(HTMLMixin, SimpleView): """ View presenting home page. """ - methods = ['GET','POST'] + methods = ['GET', 'POST'] @classmethod def get_view_name(cls): @@ -61,10 +58,10 @@ class IndexView(HTMLMixin, SimpleView): return lazy_gettext('Home') -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- -class HomeBlueprint(VialBlueprint): +class HomeBlueprint(HawatBlueprint): """Pluggable module - home page (*home*).""" @classmethod @@ -72,20 +69,20 @@ class HomeBlueprint(VialBlueprint): return lazy_gettext('Home page') -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- def get_blueprint(): """ - Mandatory interface for :py:mod:`vial.Vial` and factory function. This function - must return a valid instance of :py:class:`vial.app.VialBlueprint` or + Mandatory interface for :py:mod:`hawat.Hawat` and factory function. This function + must return a valid instance of :py:class:`hawat.app.HawatBlueprint` or :py:class:`flask.Blueprint`. """ hbp = HomeBlueprint( BLUEPRINT_NAME, __name__, - template_folder = 'templates' + template_folder='templates' ) hbp.register_view_class(IndexView, '/') diff --git a/lib/hawat/blueprints/home/templates/home/index.html b/lib/hawat/blueprints/home/templates/home/index.html index 746cf4eccfae20dd8886272546dc72fa49291d12..ea99e71446103cf5b9b8ec3bab38668cfdcc07b2 100644 --- a/lib/hawat/blueprints/home/templates/home/index.html +++ b/lib/hawat/blueprints/home/templates/home/index.html @@ -1,13 +1,13 @@ {% extends "_layout.html" %} -{% block title %}{{ vial_current_view.get_view_title() }}{% endblock %} +{% block title %}{{ hawat_current_view.get_view_title() }}{% endblock %} {% block content %} <div class="row"> <div class="col-lg-12"> <div class="jumbotron" style="margin-top: 1em;"> <h1 class="text-center"> - {{ vial_current_view.get_view_title() }} + {{ hawat_current_view.get_view_title() }} </h1> <hr> <br> diff --git a/lib/hawat/blueprints/home/test/__init__.py b/lib/hawat/blueprints/home/test/__init__.py index 2215620406164036042706c138f5f5603781560b..82dbaa7d4da04723a11ce967805767e2cd2cef49 100644 --- a/lib/hawat/blueprints/home/test/__init__.py +++ b/lib/hawat/blueprints/home/test/__init__.py @@ -1,28 +1,27 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ Unit tests for :py:mod:`hawat.blueprints.home`. """ - import unittest -import vial.const -import vial.test -import vial.db -from vial.test import VialTestCase +import hawat.const +import hawat.test +import hawat.db +from hawat.test import HawatTestCase from hawat.test.runner import TestRunnerMixin -class HomeTestCase(TestRunnerMixin, VialTestCase): +class HomeTestCase(TestRunnerMixin, HawatTestCase): """ Class for testing ``home.index`` endpoint. """ @@ -40,28 +39,28 @@ class HomeTestCase(TestRunnerMixin, VialTestCase): """Test access as anonymous user.""" self._attempt_succeed() - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_USER) def test_02_as_user(self): """Test access as user ``user``.""" self._attempt_succeed() - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_DEVELOPER) def test_03_as_developer(self): """Test access as user ``developer``.""" self._attempt_succeed() - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_MAINTAINER) def test_04_as_maintainer(self): """Test access as user ``maintainer``.""" self._attempt_succeed() - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_ADMIN) def test_05_as_admin(self): """Test access as user ``admin``.""" self._attempt_succeed() -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- if __name__ == "__main__": diff --git a/lib/hawat/blueprints/hosts/__init__.py b/lib/hawat/blueprints/hosts/__init__.py index 0e9ef67cda138d701a2fc05b2bb0dc859b5b31b1..4be2da083fdd1ded59f5392c8bf90138bcf6c0be 100644 --- a/lib/hawat/blueprints/hosts/__init__.py +++ b/lib/hawat/blueprints/hosts/__init__.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ @@ -15,11 +15,9 @@ events. This module is currently experimental, because the searching and statist calculations can be very performance demanding. """ - __author__ = "Jan Mach <jan.mach@cesnet.cz>" __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" - import datetime import pytz @@ -31,16 +29,16 @@ import mentat.services.eventstorage from mentat.const import tr_ import hawat.events -import vial.const -import vial.acl -from vial.app import VialBlueprint -from vial.view import BaseSearchView -from vial.view.mixin import HTMLMixin, AJAXMixin -from vial.utils import URLParamsBuilder +import hawat.const +import hawat.acl +import hawat.menu +from hawat.base import HawatBlueprint +from hawat.view import BaseSearchView +from hawat.view.mixin import HTMLMixin, AJAXMixin +from hawat.utils import URLParamsBuilder from hawat.base import PsycopgMixin from hawat.blueprints.hosts.forms import SimpleHostSearchForm - BLUEPRINT_NAME = 'hosts' """Name of the blueprint as module global constant.""" @@ -51,7 +49,7 @@ class AbstractSearchView(PsycopgMixin, BaseSearchView): # pylint: disable=local """ authentication = True - authorization = [vial.acl.PERMISSION_DEVELOPER] + authorization = [hawat.acl.PERMISSION_DEVELOPER] @classmethod def get_menu_title(cls, **kwargs): @@ -69,7 +67,7 @@ class AbstractSearchView(PsycopgMixin, BaseSearchView): # pylint: disable=local def get_search_form(request_args): return SimpleHostSearchForm( request_args, - meta = {'csrf': False} + meta={'csrf': False} ) def do_after_search(self, items): @@ -81,11 +79,11 @@ class AbstractSearchView(PsycopgMixin, BaseSearchView): # pylint: disable=local dt_from = self.response_context['form_data'].get('dt_from', None) if dt_from: dt_from = dt_from.astimezone(pytz.utc) - dt_from = dt_from.replace(tzinfo = None) - dt_to = self.response_context['form_data'].get('dt_to', None) + dt_from = dt_from.replace(tzinfo=None) + dt_to = self.response_context['form_data'].get('dt_to', None) if dt_to: dt_to = dt_to.astimezone(pytz.utc) - dt_to = dt_to.replace(tzinfo = None) + dt_to = dt_to.replace(tzinfo=None) if not dt_from and items: dt_from = self.get_db().search_column_with('detecttime') @@ -93,19 +91,19 @@ class AbstractSearchView(PsycopgMixin, BaseSearchView): # pylint: disable=local dt_to = datetime.datetime.utcnow() self.response_context.update( - statistics = mentat.stats.idea.evaluate_singlehost_events( + statistics=mentat.stats.idea.evaluate_singlehost_events( self.response_context['form_data'].get('host_addr'), items, - dt_from = dt_from, - dt_to = dt_to, - max_count = flask.current_app.config['HAWAT_CHART_TIMELINE_MAXSTEPS'] + dt_from=dt_from, + dt_to=dt_to, + max_count=flask.current_app.config['HAWAT_CHART_TIMELINE_MAXSTEPS'] ) ) self.response_context.pop('items', None) def do_before_response(self, **kwargs): self.response_context.update( - quicksearch_list = self.get_quicksearch_by_time() + quicksearch_list=self.get_quicksearch_by_time() ) @staticmethod @@ -128,16 +126,16 @@ class SearchView(HTMLMixin, AbstractSearchView): # pylint: disable=locally-disa @classmethod def get_breadcrumbs_menu(cls): - breadcrumbs_menu = vial.menu.Menu() + breadcrumbs_menu = hawat.menu.Menu() breadcrumbs_menu.add_entry( 'endpoint', 'home', - endpoint = flask.current_app.config['ENDPOINT_HOME'] + endpoint=flask.current_app.config['ENDPOINT_HOME'] ) breadcrumbs_menu.add_entry( 'endpoint', 'search', - endpoint = '{}.search'.format(cls.module_name) + endpoint='{}.search'.format(cls.module_name) ) return breadcrumbs_menu @@ -147,17 +145,17 @@ class APISearchView(AJAXMixin, AbstractSearchView): # pylint: disable=locally-d View responsible for querying `IDEA <https://idea.cesnet.cz/en/index>`__ event database and presenting the results in the form of JSON document. """ - methods = ['GET','POST'] + methods = ['GET', 'POST'] @classmethod def get_view_name(cls): return 'apisearch' -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- -class HostsBlueprint(VialBlueprint): +class HostsBlueprint(HawatBlueprint): """Pluggable module - Host overview (*hosts*).""" @classmethod @@ -168,8 +166,8 @@ class HostsBlueprint(VialBlueprint): app.menu_main.add_entry( 'view', 'developer.{}'.format(BLUEPRINT_NAME), - position = 80, - view = SearchView + position=80, + view=SearchView ) # Register context actions provided by this module. @@ -177,26 +175,29 @@ class HostsBlueprint(VialBlueprint): hawat.const.CSAG_ADDRESS, tr_('Search for source <strong>%(name)s</strong> in host overview'), SearchView, - URLParamsBuilder({'submit': tr_('Search')}).add_rule('host_addr').add_kwrule('dt_from', False, True).add_kwrule('dt_to', False, True) + URLParamsBuilder({'submit': tr_('Search')}).add_rule('host_addr').add_kwrule('dt_from', False, + True).add_kwrule('dt_to', + False, True) ) -#------------------------------------------------------------------------------- + +# ------------------------------------------------------------------------------- def get_blueprint(): """ - Mandatory interface for :py:mod:`vial.Vial` and factory function. This function - must return a valid instance of :py:class:`vial.app.VialBlueprint` or + Mandatory interface for :py:mod:`hawat.Hawat` and factory function. This function + must return a valid instance of :py:class:`hawat.app.HawatBlueprint` or :py:class:`flask.Blueprint`. """ hbp = HostsBlueprint( BLUEPRINT_NAME, __name__, - template_folder = 'templates' + template_folder='templates' ) - hbp.register_view_class(SearchView, '/{}/search'.format(BLUEPRINT_NAME)) + hbp.register_view_class(SearchView, '/{}/search'.format(BLUEPRINT_NAME)) hbp.register_view_class(APISearchView, '/api/{}/search'.format(BLUEPRINT_NAME)) return hbp diff --git a/lib/hawat/blueprints/hosts/forms.py b/lib/hawat/blueprints/hosts/forms.py index 3aaf2e6a0e0084c1444759f8bc20c3edba0d04b2..3a14adcf53387cdee03cc729a88aeb55094f01d5 100644 --- a/lib/hawat/blueprints/hosts/forms.py +++ b/lib/hawat/blueprints/hosts/forms.py @@ -1,29 +1,27 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ This module contains custom host search form for Hawat. """ - __author__ = "Jan Mach <jan.mach@cesnet.cz>" __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" - import wtforms import flask_wtf from flask_babel import lazy_gettext -import vial.const -import vial.forms +import hawat.const +import hawat.forms import hawat.const @@ -33,24 +31,24 @@ class SimpleHostSearchForm(flask_wtf.FlaskForm): """ host_addr = wtforms.StringField( lazy_gettext('Host address:'), - validators = [ + validators=[ wtforms.validators.DataRequired(), - vial.forms.check_ip_record + hawat.forms.check_ip_record ] ) - dt_from = vial.forms.SmartDateTimeField( + dt_from = hawat.forms.SmartDateTimeField( lazy_gettext('Detection time from:'), - validators = [ + validators=[ wtforms.validators.Optional() ], - default = lambda: vial.forms.default_dt_with_delta(hawat.const.DEFAULT_RESULT_TIMEDELTA) + default=lambda: hawat.forms.default_dt_with_delta(hawat.const.DEFAULT_RESULT_TIMEDELTA) ) - dt_to = vial.forms.SmartDateTimeField( + dt_to = hawat.forms.SmartDateTimeField( lazy_gettext('Detection time to:'), - validators = [ + validators=[ wtforms.validators.Optional() ], - default = vial.forms.default_dt + default=hawat.forms.default_dt ) submit = wtforms.SubmitField( lazy_gettext('Search') diff --git a/lib/hawat/blueprints/hosts/templates/hosts/search.html b/lib/hawat/blueprints/hosts/templates/hosts/search.html index 3d6c64293ed02963e88db4901106a790866ab1cc..954ac5b3ebc19c87489b0018f053695caab2cb6b 100644 --- a/lib/hawat/blueprints/hosts/templates/hosts/search.html +++ b/lib/hawat/blueprints/hosts/templates/hosts/search.html @@ -9,7 +9,7 @@ <!-- Search form - BEGIN ----------------------------------> <div class="jumbotron" style="margin-top: 1em;"> - <h2>{{ vial_current_view.get_view_title() }}</h2> + <h2>{{ hawat_current_view.get_view_title() }}</h2> <hr> <form method="GET" class="form" action="{{ url_for(request.endpoint) }}"> <div class="row"> diff --git a/lib/hawat/blueprints/hosts/test/__init__.py b/lib/hawat/blueprints/hosts/test/__init__.py index 1664923f91aee44cfaf11fc5163ad5e755e71606..511ccf7d5f7593e202d677764572e64dd82cacc4 100644 --- a/lib/hawat/blueprints/hosts/test/__init__.py +++ b/lib/hawat/blueprints/hosts/test/__init__.py @@ -1,28 +1,27 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ Unit tests for :py:mod:`hawat.blueprints.hosts`. """ - import unittest -import vial.const -import vial.test -import vial.db -from vial.test import VialTestCase +import hawat.const +import hawat.test +import hawat.db +from hawat.test import HawatTestCase from hawat.test.runner import TestRunnerMixin -class SearchTestCase(TestRunnerMixin, VialTestCase): +class SearchTestCase(TestRunnerMixin, HawatTestCase): """ Class for testing ``hosts.search`` endpoint. """ @@ -35,7 +34,7 @@ class SearchTestCase(TestRunnerMixin, VialTestCase): b'Redirecting...', b'login?next=' ], - follow_redirects = False + follow_redirects=False ) def _attempt_fail(self): @@ -57,28 +56,28 @@ class SearchTestCase(TestRunnerMixin, VialTestCase): """Test access as anonymous user.""" self._attempt_fail_redirect() - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_USER) def test_02_as_user(self): """Test access as user ``user``.""" self._attempt_fail() - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_DEVELOPER) def test_03_as_developer(self): """Test access as user ``developer``.""" self._attempt_fail() - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_MAINTAINER) def test_04_as_maintainer(self): """Test access as user ``maintainer``.""" self._attempt_succeed() - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_ADMIN) def test_05_as_admin(self): """Test access as user ``admin``.""" self._attempt_succeed() -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- if __name__ == "__main__": diff --git a/lib/hawat/blueprints/nerd/__init__.py b/lib/hawat/blueprints/nerd/__init__.py index 07b9e45080d42ecb696f1ce3d051cef019cc91f1..5f03c22cb5c4da11c8c90f33165246195a67f4b4 100644 --- a/lib/hawat/blueprints/nerd/__init__.py +++ b/lib/hawat/blueprints/nerd/__init__.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ @@ -18,14 +18,14 @@ Provided endpoints ------------------ ``/nerd/search`` - Endpoint providing search form for querying NERD service and formating result + Endpoint providing search form for querying NERD service and formatting result as HTML page. * *Authentication:* login required * *Methods:* ``GET`` ``/api/nerd/search`` - Endpoint providing API search form for querying NERD service and formating + Endpoint providing API search form for querying NERD service and formatting result as JSON document. * *Authentication:* login required @@ -33,7 +33,7 @@ Provided endpoints * *Methods:* ``GET``, ``POST`` ``/snippet/nerd/search`` - Endpoint providing API search form for querying NERD service and formating + Endpoint providing API search form for querying NERD service and formatting result as JSON document containing HTML snippets. * *Authentication:* login required @@ -41,28 +41,25 @@ Provided endpoints * *Methods:* ``GET``, ``POST`` """ - __author__ = "Jan Mach <jan.mach@cesnet.cz>" __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" - import flask from flask_babel import lazy_gettext import mentat.services.nerd from mentat.const import tr_ -import vial.db -import vial.const -import vial.acl -from vial.app import VialBlueprint -from vial.view import RenderableView -from vial.view.mixin import HTMLMixin, AJAXMixin, SnippetMixin -from vial.utils import URLParamsBuilder +import hawat.db +import hawat.const +import hawat.acl +from hawat.base import HawatBlueprint +from hawat.view import RenderableView +from hawat.view.mixin import HTMLMixin, AJAXMixin, SnippetMixin +from hawat.utils import URLParamsBuilder import hawat.const from hawat.blueprints.nerd.forms import NerdSearchForm - BLUEPRINT_NAME = 'nerd' """Name of the blueprint as module global constant.""" @@ -88,27 +85,27 @@ class AbstractSearchView(RenderableView): # pylint: disable=locally-disabled,ab Mandatory interface required by the :py:func:`flask.views.View.dispatch_request`. Will be called by the *Flask* framework to service the request. """ - form = NerdSearchForm(flask.request.args, meta = {'csrf': False}) + form = NerdSearchForm(flask.request.args, meta={'csrf': False}) - if vial.const.FORM_ACTION_SUBMIT in flask.request.args: + if hawat.const.FORM_ACTION_SUBMIT in flask.request.args: if form.validate(): form_data = form.data nerd_service = mentat.services.nerd.service() self.response_context.update( - search_item = form.search.data, - search_url = nerd_service.get_url_lookup_ip(form.search.data), - form_data = form_data + search_item=form.search.data, + search_url=nerd_service.get_url_lookup_ip(form.search.data), + form_data=form_data ) try: self.response_context.update( - search_result = nerd_service.lookup_ip(form.search.data) + search_result=nerd_service.lookup_ip(form.search.data) ) except Exception as exc: - self.flash(str(exc), level = 'error') + self.flash(str(exc), level='error') self.response_context.update( - search_form = form, - request_args = flask.request.args + search_form=form, + request_args=flask.request.args ) return self.generate_response() @@ -130,7 +127,7 @@ class APISearchView(AJAXMixin, AbstractSearchView): # pylint: disable=locally-d View responsible for querying external NERD service and presenting the results in the form of JSON document. """ - methods = ['GET','POST'] + methods = ['GET', 'POST'] @classmethod def get_view_name(cls): @@ -155,14 +152,14 @@ class SnippetSearchView(SnippetMixin, AbstractSearchView): # pylint: disable=lo @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`hawat.view.BaseView.get_view_name`.""" return 'sptsearch' -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- -class NerdBlueprint(VialBlueprint): +class NerdBlueprint(HawatBlueprint): """Pluggable module - NERD service (*nerd*).""" @classmethod @@ -175,8 +172,8 @@ class NerdBlueprint(VialBlueprint): app.menu_main.add_entry( 'view', 'more.{}'.format(BLUEPRINT_NAME), - position = 30, - view = SearchView + position=30, + view=SearchView ) # Register context actions provided by this module. @@ -201,24 +198,24 @@ class NerdBlueprint(VialBlueprint): ) -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- def get_blueprint(): """ - Mandatory interface for :py:mod:`vial.Vial` and factory function. This function - must return a valid instance of :py:class:`vial.app.VialBlueprint` or + Mandatory interface for :py:mod:`hawat.Hawat` and factory function. This function + must return a valid instance of :py:class:`hawat.app.HawatBlueprint` or :py:class:`flask.Blueprint`. """ hbp = NerdBlueprint( BLUEPRINT_NAME, __name__, - template_folder = 'templates' + template_folder='templates' ) - hbp.register_view_class(SearchView, '/{}/search'.format(BLUEPRINT_NAME)) - hbp.register_view_class(APISearchView, '/api/{}/search'.format(BLUEPRINT_NAME)) + hbp.register_view_class(SearchView, '/{}/search'.format(BLUEPRINT_NAME)) + hbp.register_view_class(APISearchView, '/api/{}/search'.format(BLUEPRINT_NAME)) hbp.register_view_class(SnippetSearchView, '/snippet/{}/search'.format(BLUEPRINT_NAME)) return hbp diff --git a/lib/hawat/blueprints/nerd/forms.py b/lib/hawat/blueprints/nerd/forms.py index 17dd98b06df96a739a8b6a09c277d7d3c8c0e25f..d4bb1598d36f4c6361447e273c810fd48119e02c 100644 --- a/lib/hawat/blueprints/nerd/forms.py +++ b/lib/hawat/blueprints/nerd/forms.py @@ -1,27 +1,25 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ This module contains custom external NERD database search form for Hawat. """ - __author__ = "Jan Mach <jan.mach@cesnet.cz>" __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" - import wtforms import flask_wtf from flask_babel import lazy_gettext -import vial.forms +import hawat.forms class NerdSearchForm(flask_wtf.FlaskForm): @@ -30,9 +28,9 @@ class NerdSearchForm(flask_wtf.FlaskForm): """ search = wtforms.StringField( lazy_gettext('Search NERD:'), - validators = [ + validators=[ wtforms.validators.DataRequired(), - vial.forms.check_ip4_record + hawat.forms.check_ip4_record ] ) submit = wtforms.SubmitField( diff --git a/lib/hawat/blueprints/nerd/templates/nerd/search.html b/lib/hawat/blueprints/nerd/templates/nerd/search.html index 1fa6e50a4db718359ee38cb3a7f5f56e25dd5d6d..6a953433cb4477283d7454d7f2b22f7624159715 100644 --- a/lib/hawat/blueprints/nerd/templates/nerd/search.html +++ b/lib/hawat/blueprints/nerd/templates/nerd/search.html @@ -7,7 +7,7 @@ <div class="col-lg-12"> <div class="jumbotron" style="margin-top: 1em;"> - <h2>{{ vial_current_view.get_view_title() }}</h2> + <h2>{{ hawat_current_view.get_view_title() }}</h2> <form method="GET" class="form-inline" action="{{ url_for('nerd.search') }}"> <div class="form-group{% if search_form.search.errors %}{{ ' has-error' }}{% endif %}"> {{ search_form.search.label(class_='sr-only') }} diff --git a/lib/hawat/blueprints/nerd/test/__init__.py b/lib/hawat/blueprints/nerd/test/__init__.py index 4023a0d2405eda95708513f3ad840f070bd830db..fc5437e43e3ffd41e1a16bd097f4645201b6d741 100644 --- a/lib/hawat/blueprints/nerd/test/__init__.py +++ b/lib/hawat/blueprints/nerd/test/__init__.py @@ -1,28 +1,27 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ Unit tests for :py:mod:`hawat.blueprints.nerd`. """ - import unittest -import vial.const -import vial.test -import vial.db -from vial.test import VialTestCase +import hawat.const +import hawat.test +import hawat.db +from hawat.test import HawatTestCase from hawat.test.runner import TestRunnerMixin -class SearchTestCase(TestRunnerMixin, VialTestCase): +class SearchTestCase(TestRunnerMixin, HawatTestCase): """ Class for testing ``nerd.search`` endpoint. """ @@ -43,28 +42,28 @@ class SearchTestCase(TestRunnerMixin, VialTestCase): """Test access as anonymous user.""" self._attempt_fail() - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_USER) def test_02_as_user(self): """Test access as user ``user``.""" self._attempt_succeed() - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_DEVELOPER) def test_03_as_developer(self): """Test access as user ``developer``.""" self._attempt_succeed() - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_MAINTAINER) def test_04_as_maintainer(self): """Test access as user ``maintainer``.""" self._attempt_succeed() - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_ADMIN) def test_05_as_admin(self): """Test access as user ``admin``.""" self._attempt_succeed() -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- if __name__ == "__main__": diff --git a/lib/hawat/blueprints/networks/__init__.py b/lib/hawat/blueprints/networks/__init__.py index 487de9e874f8bed7987d133aaf3d9c2c1aaee9e6..9282752c68cd9920e0903c309f47ee2d0a9a4131 100644 --- a/lib/hawat/blueprints/networks/__init__.py +++ b/lib/hawat/blueprints/networks/__init__.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ @@ -19,11 +19,10 @@ features include: * deleting existing network records """ - __author__ = "Jan Mach <jan.mach@cesnet.cz>" __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" - +import markupsafe import flask import flask_login import flask_principal @@ -33,13 +32,13 @@ from sqlalchemy import or_ from mentat.datatype.sqldb import NetworkModel, GroupModel, ItemChangeLogModel -import vial.acl -from vial.app import VialBlueprint -from vial.view import ItemListView, ItemShowView, ItemCreateView, ItemCreateForView, ItemUpdateView, ItemDeleteView -from vial.view.mixin import HTMLMixin, SQLAlchemyMixin +import hawat.acl +import hawat.menu +from hawat.base import HawatBlueprint +from hawat.view import ItemListView, ItemShowView, ItemCreateView, ItemCreateForView, ItemUpdateView, ItemDeleteView +from hawat.view.mixin import HTMLMixin, SQLAlchemyMixin from hawat.blueprints.networks.forms import BaseNetworkForm, AdminNetworkForm, NetworkSearchForm - BLUEPRINT_NAME = 'networks' """Name of the blueprint as module global constant.""" @@ -52,7 +51,7 @@ class ListView(HTMLMixin, SQLAlchemyMixin, ItemListView): authentication = True - authorization = [vial.acl.PERMISSION_POWER] + authorization = [hawat.acl.PERMISSION_POWER] @classmethod def get_view_title(cls, **kwargs): @@ -64,35 +63,35 @@ class ListView(HTMLMixin, SQLAlchemyMixin, ItemListView): @classmethod def get_action_menu(cls): - action_menu = vial.menu.Menu() + action_menu = hawat.menu.Menu() action_menu.add_entry( 'endpoint', 'create', - endpoint = 'networks.create', - resptitle = True + endpoint='networks.create', + resptitle=True ) return action_menu @classmethod def get_context_action_menu(cls): - action_menu = vial.menu.Menu() + action_menu = hawat.menu.Menu() action_menu.add_entry( 'endpoint', 'show', - endpoint = 'networks.show', - hidetitle = True + endpoint='networks.show', + hidetitle=True ) action_menu.add_entry( 'endpoint', 'update', - endpoint = 'networks.update', - hidetitle = True + endpoint='networks.update', + hidetitle=True ) action_menu.add_entry( 'endpoint', 'delete', - endpoint = 'networks.delete', - hidetitle = True + endpoint='networks.delete', + hidetitle=True ) return action_menu @@ -104,14 +103,14 @@ class ListView(HTMLMixin, SQLAlchemyMixin, ItemListView): """ return NetworkSearchForm( request_args, - meta = {'csrf': False} + meta={'csrf': False} ) @staticmethod def build_query(query, model, form_args): # Adjust query based on text search string. if 'search' in form_args and form_args['search']: - query = query\ + query = query \ .filter( or_( model.netname.like('%{}%'.format(form_args['search'])), @@ -127,11 +126,11 @@ class ListView(HTMLMixin, SQLAlchemyMixin, ItemListView): query = query.filter(model.createtime <= form_args['dt_to']) # Adjust query based on record source selection. if 'source' in form_args and form_args['source']: - query = query\ + query = query \ .filter(model.source == form_args['source']) # Adjust query based on user membership selection. if 'group' in form_args and form_args['group']: - query = query\ + query = query \ .filter(model.group_id == form_args['group'].id) if 'sortby' in form_args and form_args['sortby']: sortmap = { @@ -158,7 +157,7 @@ class ShowView(HTMLMixin, SQLAlchemyMixin, ItemShowView): def get_menu_legend(cls, **kwargs): return lazy_gettext( 'View details of network record "%(item)s"', - item = flask.escape(kwargs['item'].netname) + item=markupsafe.escape(kwargs['item'].netname) ) @classmethod @@ -172,51 +171,51 @@ class ShowView(HTMLMixin, SQLAlchemyMixin, ItemShowView): @classmethod def authorize_item_action(cls, **kwargs): permission_mm = flask_principal.Permission( - vial.acl.MembershipNeed(kwargs['item'].group.id), - vial.acl.ManagementNeed(kwargs['item'].group.id) + hawat.acl.MembershipNeed(kwargs['item'].group.id), + hawat.acl.ManagementNeed(kwargs['item'].group.id) ) - return vial.acl.PERMISSION_POWER.can() or permission_mm.can() + return hawat.acl.PERMISSION_POWER.can() or permission_mm.can() @classmethod def get_action_menu(cls): - action_menu = vial.menu.Menu() + action_menu = hawat.menu.Menu() action_menu.add_entry( 'endpoint', 'update', - endpoint = 'networks.update' + endpoint='networks.update' ) action_menu.add_entry( 'endpoint', 'delete', - endpoint = 'networks.delete' + endpoint='networks.delete' ) return action_menu def do_before_response(self, **kwargs): item = self.response_context['item'] - if self.can_access_endpoint('networks.update', item = item) and self.has_endpoint('changelogs.search'): + if self.can_access_endpoint('networks.update', item=item) and self.has_endpoint('changelogs.search'): self.response_context.update( - context_action_menu_changelogs = self.get_endpoint_class( + context_action_menu_changelogs=self.get_endpoint_class( 'changelogs.search' ).get_context_action_menu() ) - item_changelog = self.dbsession.query(ItemChangeLogModel).\ - filter(ItemChangeLogModel.model == item.__class__.__name__).\ - filter(ItemChangeLogModel.model_id == item.id).\ - order_by(ItemChangeLogModel.createtime.desc()).\ - limit(100).\ + item_changelog = self.dbsession.query(ItemChangeLogModel). \ + filter(ItemChangeLogModel.model == item.__class__.__name__). \ + filter(ItemChangeLogModel.model_id == item.id). \ + order_by(ItemChangeLogModel.createtime.desc()). \ + limit(100). \ all() - self.response_context.update(item_changelog = item_changelog) + self.response_context.update(item_changelog=item_changelog) class CreateView(HTMLMixin, SQLAlchemyMixin, ItemCreateView): # pylint: disable=locally-disabled,too-many-ancestors """ View for creating new network records. """ - methods = ['GET','POST'] + methods = ['GET', 'POST'] authentication = True @@ -238,21 +237,21 @@ class CreateView(HTMLMixin, SQLAlchemyMixin, ItemCreateView): # pylint: disable @classmethod def authorize_item_action(cls, **kwargs): - return vial.acl.PERMISSION_POWER.can() + return hawat.acl.PERMISSION_POWER.can() @staticmethod def get_message_success(**kwargs): return gettext( 'Network record <strong>%(item_id)s</strong> for group <strong>%(parent_id)s</strong> was successfully created.', - item_id = flask.escape(str(kwargs['item'])), - parent_id = flask.escape(str(kwargs['item'].group)) + item_id=markupsafe.escape(str(kwargs['item'])), + parent_id=markupsafe.escape(str(kwargs['item'].group)) ) @staticmethod def get_message_failure(**kwargs): return gettext( 'Unable to create new network record for group <strong>%(parent_id)s</strong>.', - parent_id = flask.escape(str(kwargs['item'].group)) + parent_id=markupsafe.escape(str(kwargs['item'].group)) ) @staticmethod @@ -264,11 +263,12 @@ class CreateView(HTMLMixin, SQLAlchemyMixin, ItemCreateView): # pylint: disable return AdminNetworkForm() -class CreateForView(HTMLMixin, SQLAlchemyMixin, ItemCreateForView): # pylint: disable=locally-disabled,too-many-ancestors +class CreateForView(HTMLMixin, SQLAlchemyMixin, + ItemCreateForView): # pylint: disable=locally-disabled,too-many-ancestors """ View for creating new network records. """ - methods = ['GET','POST'] + methods = ['GET', 'POST'] authentication = True @@ -286,14 +286,14 @@ class CreateForView(HTMLMixin, SQLAlchemyMixin, ItemCreateForView): # pylint: d def get_menu_legend(cls, **kwargs): return lazy_gettext( 'Create network record for group "%(item)s"', - item = flask.escape(str(kwargs['item'])) + item=markupsafe.escape(str(kwargs['item'])) ) @classmethod def get_view_url(cls, **kwargs): return flask.url_for( cls.get_view_endpoint(), - parent_id = kwargs['item'].id + parent_id=kwargs['item'].id ) @classmethod @@ -315,23 +315,23 @@ class CreateForView(HTMLMixin, SQLAlchemyMixin, ItemCreateForView): # pylint: d @classmethod def authorize_item_action(cls, **kwargs): permission_m = flask_principal.Permission( - vial.acl.ManagementNeed(kwargs['item'].id) + hawat.acl.ManagementNeed(kwargs['item'].id) ) - return vial.acl.PERMISSION_POWER.can() or permission_m.can() + return hawat.acl.PERMISSION_POWER.can() or permission_m.can() @staticmethod def get_message_success(**kwargs): return gettext( 'Network record <strong>%(item_id)s</strong> for group <strong>%(parent_id)s</strong> was successfully created.', - item_id = flask.escape(str(kwargs['item'])), - parent_id = flask.escape(str(kwargs['parent'])) + item_id=markupsafe.escape(str(kwargs['item'])), + parent_id=markupsafe.escape(str(kwargs['parent'])) ) @staticmethod def get_message_failure(**kwargs): return gettext( 'Unable to create new network record for group <strong>%(parent_id)s</strong>.', - parent_id = flask.escape(str(kwargs['parent'])) + parent_id=markupsafe.escape(str(kwargs['parent'])) ) @staticmethod @@ -351,7 +351,7 @@ class UpdateView(HTMLMixin, SQLAlchemyMixin, ItemUpdateView): # pylint: disable """ View for updating existing network records. """ - methods = ['GET','POST'] + methods = ['GET', 'POST'] authentication = True @@ -359,7 +359,7 @@ class UpdateView(HTMLMixin, SQLAlchemyMixin, ItemUpdateView): # pylint: disable def get_menu_legend(cls, **kwargs): return lazy_gettext( 'Update details of network record "%(item)s"', - item = flask.escape(kwargs['item'].netname) + item=markupsafe.escape(kwargs['item'].netname) ) @classmethod @@ -377,48 +377,48 @@ class UpdateView(HTMLMixin, SQLAlchemyMixin, ItemUpdateView): # pylint: disable @classmethod def authorize_item_action(cls, **kwargs): permission_m = flask_principal.Permission( - vial.acl.ManagementNeed(kwargs['item'].group.id) + hawat.acl.ManagementNeed(kwargs['item'].group.id) ) - return vial.acl.PERMISSION_POWER.can() or permission_m.can() + return hawat.acl.PERMISSION_POWER.can() or permission_m.can() @staticmethod def get_message_success(**kwargs): return gettext( 'Network record <strong>%(item_id)s</strong> for group <strong>%(parent_id)s</strong> was successfully updated.', - item_id = flask.escape(str(kwargs['item'])), - parent_id = flask.escape(str(kwargs['item'].group)) + item_id=markupsafe.escape(str(kwargs['item'])), + parent_id=markupsafe.escape(str(kwargs['item'].group)) ) @staticmethod def get_message_failure(**kwargs): return gettext( 'Unable to update network record <strong>%(item_id)s</strong> for group <strong>%(parent_id)s</strong>.', - item_id = flask.escape(str(kwargs['item'])), - parent_id = flask.escape(str(kwargs['item'].group)) + item_id=markupsafe.escape(str(kwargs['item'])), + parent_id=markupsafe.escape(str(kwargs['item'].group)) ) @staticmethod def get_message_cancel(**kwargs): return gettext( 'Canceled updating network record <strong>%(item_id)s</strong> for group <strong>%(parent_id)s</strong>.', - item_id = flask.escape(str(kwargs['item'])), - parent_id = flask.escape(str(kwargs['item'].group)) + item_id=markupsafe.escape(str(kwargs['item'])), + parent_id=markupsafe.escape(str(kwargs['item'].group)) ) @staticmethod def get_item_form(item): admin = flask_login.current_user.has_role('admin') if not admin: - return BaseNetworkForm(obj = item) + return BaseNetworkForm(obj=item) - return AdminNetworkForm(obj = item) + return AdminNetworkForm(obj=item) class DeleteView(HTMLMixin, SQLAlchemyMixin, ItemDeleteView): # pylint: disable=locally-disabled,too-many-ancestors """ View for deleting existing network records. """ - methods = ['GET','POST'] + methods = ['GET', 'POST'] authentication = True @@ -426,7 +426,7 @@ class DeleteView(HTMLMixin, SQLAlchemyMixin, ItemDeleteView): # pylint: disable def get_menu_legend(cls, **kwargs): return lazy_gettext( 'Delete network record "%(item)s"', - item = flask.escape(kwargs['item'].netname) + item=markupsafe.escape(kwargs['item'].netname) ) @property @@ -440,39 +440,39 @@ class DeleteView(HTMLMixin, SQLAlchemyMixin, ItemDeleteView): # pylint: disable @classmethod def authorize_item_action(cls, **kwargs): permission_m = flask_principal.Permission( - vial.acl.ManagementNeed(kwargs['item'].group.id) + hawat.acl.ManagementNeed(kwargs['item'].group.id) ) - return vial.acl.PERMISSION_POWER.can() or permission_m.can() + return hawat.acl.PERMISSION_POWER.can() or permission_m.can() @staticmethod def get_message_success(**kwargs): return gettext( 'Network record <strong>%(item_id)s</strong> for group <strong>%(parent_id)s</strong> was successfully and permanently deleted.', - item_id = flask.escape(str(kwargs['item'])), - parent_id = flask.escape(str(kwargs['item'].group)) + item_id=markupsafe.escape(str(kwargs['item'])), + parent_id=markupsafe.escape(str(kwargs['item'].group)) ) @staticmethod def get_message_failure(**kwargs): return gettext( 'Unable to permanently delete network record <strong>%(item_id)s</strong> for group <strong>%(parent_id)s</strong>.', - item_id = flask.escape(str(kwargs['item'])), - parent_id = flask.escape(str(kwargs['item'].group)) + item_id=markupsafe.escape(str(kwargs['item'])), + parent_id=markupsafe.escape(str(kwargs['item'].group)) ) @staticmethod def get_message_cancel(**kwargs): return gettext( 'Canceled deleting network record <strong>%(item_id)s</strong> for group <strong>%(parent_id)s</strong>.', - item_id = flask.escape(str(kwargs['item'])), - parent_id = flask.escape(str(kwargs['item'].group)) + item_id=markupsafe.escape(str(kwargs['item'])), + parent_id=markupsafe.escape(str(kwargs['item'].group)) ) -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- -class NetworksBlueprint(VialBlueprint): +class NetworksBlueprint(HawatBlueprint): """Pluggable module - network management (*networks*).""" @classmethod @@ -483,33 +483,33 @@ class NetworksBlueprint(VialBlueprint): app.menu_main.add_entry( 'view', 'admin.{}'.format(BLUEPRINT_NAME), - position = 70, - view = ListView + position=70, + view=ListView ) -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- def get_blueprint(): """ - Mandatory interface for :py:mod:`vial.Vial` and factory function. This function - must return a valid instance of :py:class:`vial.app.VialBlueprint` or + Mandatory interface for :py:mod:`hawat.Hawat` and factory function. This function + must return a valid instance of :py:class:`hawat.app.HawatBlueprint` or :py:class:`flask.Blueprint`. """ hbp = NetworksBlueprint( BLUEPRINT_NAME, __name__, - template_folder = 'templates', - url_prefix = '/{}'.format(BLUEPRINT_NAME) + template_folder='templates', + url_prefix='/{}'.format(BLUEPRINT_NAME) ) - hbp.register_view_class(ListView, '/list') - hbp.register_view_class(CreateView, '/create') + hbp.register_view_class(ListView, '/list') + hbp.register_view_class(CreateView, '/create') hbp.register_view_class(CreateForView, '/createfor/<int:parent_id>') - hbp.register_view_class(ShowView, '/<int:item_id>/show') - hbp.register_view_class(UpdateView, '/<int:item_id>/update') - hbp.register_view_class(DeleteView, '/<int:item_id>/delete') + hbp.register_view_class(ShowView, '/<int:item_id>/show') + hbp.register_view_class(UpdateView, '/<int:item_id>/update') + hbp.register_view_class(DeleteView, '/<int:item_id>/delete') return hbp diff --git a/lib/hawat/blueprints/networks/forms.py b/lib/hawat/blueprints/networks/forms.py index aadbc266c004ee3d816245ef4c26d2efa0613e54..b4c6689ef0f2449fcdb2da296d7ebc88eb543461 100644 --- a/lib/hawat/blueprints/networks/forms.py +++ b/lib/hawat/blueprints/networks/forms.py @@ -1,22 +1,20 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ This module contains custom network record management forms for Hawat. """ - __author__ = "Jan Mach <jan.mach@cesnet.cz>" __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" - import wtforms from wtforms.ext.sqlalchemy.fields import QuerySelectField @@ -28,10 +26,10 @@ from flask_babel import lazy_gettext # # Custom modules. # -import vial.const -import vial.forms -import vial.db -from vial.forms import get_available_groups +import hawat.const +import hawat.forms +import hawat.db +from hawat.forms import get_available_groups from mentat.datatype.sqldb import NetworkModel @@ -40,43 +38,43 @@ def get_available_sources(): """ Query the database for list of network record sources. """ - result = vial.db.db_query(NetworkModel)\ - .distinct(NetworkModel.source)\ - .order_by(NetworkModel.source)\ + result = hawat.db.db_query(NetworkModel) \ + .distinct(NetworkModel.source) \ + .order_by(NetworkModel.source) \ .all() return [x.source for x in result] -class BaseNetworkForm(vial.forms.BaseItemForm): +class BaseNetworkForm(hawat.forms.BaseItemForm): """ Class representing base network record form. """ netname = wtforms.StringField( lazy_gettext('Netname:'), - validators = [ + validators=[ wtforms.validators.DataRequired(), - wtforms.validators.Length(min = 3, max = 250) + wtforms.validators.Length(min=3, max=250) ] ) source = wtforms.HiddenField( - default = 'manual', - validators = [ + default='manual', + validators=[ wtforms.validators.DataRequired(), - wtforms.validators.Length(min = 3, max = 50) + wtforms.validators.Length(min=3, max=50) ] ) network = wtforms.TextAreaField( lazy_gettext('Network:'), - validators = [ + validators=[ wtforms.validators.DataRequired(), - vial.forms.check_network_record + hawat.forms.check_network_record ] ) rank = wtforms.IntegerField( lazy_gettext('Rank:'), - validators = [ + validators=[ wtforms.validators.Optional(), - wtforms.validators.NumberRange(min = 1, max = 10000) + wtforms.validators.NumberRange(min=1, max=10000) ] ) description = wtforms.TextAreaField( @@ -96,65 +94,68 @@ class AdminNetworkForm(BaseNetworkForm): """ group = QuerySelectField( lazy_gettext('Group:'), - query_factory = get_available_groups, - allow_blank = False + query_factory=get_available_groups, + allow_blank=False ) -class NetworkSearchForm(vial.forms.BaseSearchForm): +class NetworkSearchForm(hawat.forms.BaseSearchForm): """ Class representing simple user search form. """ search = wtforms.StringField( lazy_gettext('Netname, network, description:'), - validators = [ + validators=[ wtforms.validators.Optional(), - wtforms.validators.Length(min = 3, max = 100) + wtforms.validators.Length(min=3, max=100) ], - description = lazy_gettext('Network`s name, address or description. Search is performed even in the middle of the strings.') + description=lazy_gettext( + 'Network`s name, address or description. Search is performed even in the middle of the strings.') ) - dt_from = vial.forms.SmartDateTimeField( + dt_from = hawat.forms.SmartDateTimeField( lazy_gettext('Creation time from:'), - validators = [ + validators=[ wtforms.validators.Optional() ], - description = lazy_gettext('Lower time boundary for item creation time. Timestamp is expected to be in the format <code>YYYY-MM-DD hh:mm:ss</code> and in the timezone according to the user`s preferences.') + description=lazy_gettext( + 'Lower time boundary for item creation time. Timestamp is expected to be in the format <code>YYYY-MM-DD hh:mm:ss</code> and in the timezone according to the user`s preferences.') ) - dt_to = vial.forms.SmartDateTimeField( + dt_to = hawat.forms.SmartDateTimeField( lazy_gettext('Creation time to:'), - validators = [ + validators=[ wtforms.validators.Optional() ], - description = lazy_gettext('Upper time boundary for item creation time. Timestamp is expected to be in the format <code>YYYY-MM-DD hh:mm:ss</code> and in the timezone according to the user`s preferences.') + description=lazy_gettext( + 'Upper time boundary for item creation time. Timestamp is expected to be in the format <code>YYYY-MM-DD hh:mm:ss</code> and in the timezone according to the user`s preferences.') ) group = QuerySelectField( lazy_gettext('Group:'), - query_factory = get_available_groups, - allow_blank = True + query_factory=get_available_groups, + allow_blank=True ) source = wtforms.SelectField( lazy_gettext('Record source:'), - validators = [ + validators=[ wtforms.validators.Optional() ], - default = '' + default='' ) sortby = wtforms.SelectField( lazy_gettext('Sort by:'), - validators = [ + validators=[ wtforms.validators.Optional() ], - choices = [ + choices=[ ('createtime.desc', lazy_gettext('by creation time descending')), - ('createtime.asc', lazy_gettext('by creation time ascending')), + ('createtime.asc', lazy_gettext('by creation time ascending')), ('netname.desc', lazy_gettext('by netname descending')), - ('netname.asc', lazy_gettext('by netname ascending')), + ('netname.asc', lazy_gettext('by netname ascending')), ('network.desc', lazy_gettext('by network descending')), - ('network.asc', lazy_gettext('by network ascending')) + ('network.asc', lazy_gettext('by network ascending')) ], - default = 'netname.asc' + default='netname.asc' ) def __init__(self, *args, **kwargs): diff --git a/lib/hawat/blueprints/networks/templates/networks/show.html b/lib/hawat/blueprints/networks/templates/networks/show.html index 8422897a7ef5f2d74d3c47406cc1e00d31a88ac7..dd97cafb6aef3961e4f90d73f4f574bb82fa6c85 100644 --- a/lib/hawat/blueprints/networks/templates/networks/show.html +++ b/lib/hawat/blueprints/networks/templates/networks/show.html @@ -6,7 +6,7 @@ <div class="col-lg-12"> {{ macros_page.render_breadcrumbs(item) }} - <h2>{{ vial_current_view.get_view_title() }}</h2> + <h2>{{ hawat_current_view.get_view_title() }}</h2> <hr> <h3>{{ item.netname }}</h3> <div class="pull-right"> diff --git a/lib/hawat/blueprints/networks/test/__init__.py b/lib/hawat/blueprints/networks/test/__init__.py index 36b1670427ce103e48e9702594cc1df077eb87d5..547f342e06816f8e1acd5a83a2adc78a98553cbf 100644 --- a/lib/hawat/blueprints/networks/test/__init__.py +++ b/lib/hawat/blueprints/networks/test/__init__.py @@ -1,27 +1,26 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ Unit tests for :py:mod:`hawat.blueprints.networks`. """ - import unittest from mentat.datatype.sqldb import NetworkModel -import vial.const -import vial.test -import vial.test.fixtures -import vial.db -from vial.test import VialTestCase, ItemCreateVialTestCase +import hawat.const +import hawat.test +import hawat.test.fixtures +import hawat.db +from hawat.test import HawatTestCase, ItemCreateHawatTestCase from hawat.test.runner import TestRunnerMixin @@ -34,27 +33,27 @@ class NetworkTestMixin: def _nname(gname): return 'NET_{}'.format(gname) - def network_get(self, network_name, with_app_context = False): + def network_get(self, network_name, with_app_context=False): """ Get given network. """ if not with_app_context: - return vial.db.db_session().query(NetworkModel).filter(NetworkModel.netname == network_name).one_or_none() + return hawat.db.db_session().query(NetworkModel).filter(NetworkModel.netname == network_name).one_or_none() with self.app.app_context(): - return vial.db.db_session().query(NetworkModel).filter(NetworkModel.netname == network_name).one_or_none() + return hawat.db.db_session().query(NetworkModel).filter(NetworkModel.netname == network_name).one_or_none() - def network_save(self, network_object, with_app_context = False): + def network_save(self, network_object, with_app_context=False): """ Update given network. """ if not with_app_context: - vial.db.db_session().add(network_object) - vial.db.db_session().commit() + hawat.db.db_session().add(network_object) + hawat.db.db_session().commit() with self.app.app_context(): - vial.db.db_session().add(network_object) - vial.db.db_session().commit() + hawat.db.db_session().add(network_object) + hawat.db.db_session().commit() - def network_id(self, network_type, with_app_context = False): + def network_id(self, network_type, with_app_context=False): """ Get ID of given network. """ @@ -66,7 +65,7 @@ class NetworkTestMixin: return fobj.id -class NetworksListTestCase(TestRunnerMixin, VialTestCase): +class NetworksListTestCase(TestRunnerMixin, HawatTestCase): """Class for testing ``networks.list`` endpoint.""" def _attempt_fail(self): @@ -85,28 +84,28 @@ class NetworksListTestCase(TestRunnerMixin, VialTestCase): ] ) - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_USER) def test_01_as_user(self): """Test access as user ``user``.""" self._attempt_fail() - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_DEVELOPER) def test_02_as_developer(self): """Test access as user ``developer``.""" self._attempt_fail() - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_MAINTAINER) def test_03_as_maintainer(self): """Test access as user ``maintainer``.""" self._attempt_succeed() - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_ADMIN) def test_04_as_admin(self): """Test access as user ``admin``.""" self._attempt_succeed() -class NetworksShowTestCase(NetworkTestMixin, TestRunnerMixin, VialTestCase): +class NetworksShowTestCase(NetworkTestMixin, TestRunnerMixin, HawatTestCase): """Base class for testing ``networks.show`` endpoint.""" def _attempt_fail(self, nname): @@ -127,48 +126,48 @@ class NetworksShowTestCase(NetworkTestMixin, TestRunnerMixin, VialTestCase): ] ) - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_USER) def test_01_as_user(self): """ Test access as user 'user'. Only power user is able to view all available networks. """ - self._attempt_succeed(self._nname(vial.test.fixtures.DEMO_GROUP_A)) - self._attempt_fail(self._nname(vial.test.fixtures.DEMO_GROUP_B)) + self._attempt_succeed(self._nname(hawat.test.fixtures.DEMO_GROUP_A)) + self._attempt_fail(self._nname(hawat.test.fixtures.DEMO_GROUP_B)) - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_DEVELOPER) def test_02_as_developer(self): """ Test access as user 'developer'. Only power user is able to view all available networks. """ - self._attempt_succeed(self._nname(vial.test.fixtures.DEMO_GROUP_A)) - self._attempt_fail(self._nname(vial.test.fixtures.DEMO_GROUP_B)) + self._attempt_succeed(self._nname(hawat.test.fixtures.DEMO_GROUP_A)) + self._attempt_fail(self._nname(hawat.test.fixtures.DEMO_GROUP_B)) - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_MAINTAINER) def test_03_as_maintainer(self): """ Test access as user 'maintainer'. Only power user is able to view all available networks. """ - self._attempt_succeed(self._nname(vial.test.fixtures.DEMO_GROUP_A)) - self._attempt_succeed(self._nname(vial.test.fixtures.DEMO_GROUP_B)) + self._attempt_succeed(self._nname(hawat.test.fixtures.DEMO_GROUP_A)) + self._attempt_succeed(self._nname(hawat.test.fixtures.DEMO_GROUP_B)) - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_ADMIN) def test_04_as_admin(self): """ Test access as user 'admin'. Only power user is able to view all available networks. """ - self._attempt_succeed(self._nname(vial.test.fixtures.DEMO_GROUP_A)) - self._attempt_succeed(self._nname(vial.test.fixtures.DEMO_GROUP_B)) + self._attempt_succeed(self._nname(hawat.test.fixtures.DEMO_GROUP_A)) + self._attempt_succeed(self._nname(hawat.test.fixtures.DEMO_GROUP_B)) -class NetworksCreateTestCase(NetworkTestMixin, TestRunnerMixin, ItemCreateVialTestCase): +class NetworksCreateTestCase(NetworkTestMixin, TestRunnerMixin, ItemCreateHawatTestCase): """Class for testing ``networks.create`` endpoint.""" network_data_fixture = [ @@ -195,28 +194,28 @@ class NetworksCreateTestCase(NetworkTestMixin, TestRunnerMixin, ItemCreateVialTe ] ) - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_USER) def test_01_as_user(self): """Test access as user 'user'.""" self._attempt_fail() - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_DEVELOPER) def test_02_as_developer(self): """Test access as user 'developer'.""" self._attempt_fail() - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_MAINTAINER) def test_03_as_maintainer(self): """Test access as user 'maintainer'.""" self._attempt_succeed() - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_ADMIN) def test_04_as_admin(self): """Test access as user 'admin'.""" self._attempt_succeed() -class NetworksCreateForTestCase(NetworkTestMixin, TestRunnerMixin, ItemCreateVialTestCase): +class NetworksCreateForTestCase(NetworkTestMixin, TestRunnerMixin, ItemCreateHawatTestCase): """Class for testing ``networks.createfor`` endpoint.""" network_data_fixture = [ @@ -226,14 +225,14 @@ class NetworksCreateForTestCase(NetworkTestMixin, TestRunnerMixin, ItemCreateVia ] def _attempt_fail(self, gname): - gid = self.group_id(gname, with_app_ctx = True) + gid = self.group_id(gname, with_app_ctx=True) self.assertGetURL( '/networks/createfor/{}'.format(gid), 403 ) def _attempt_succeed(self, gname): - gid = self.group_id(gname, with_app_ctx = True) + gid = self.group_id(gname, with_app_ctx=True) self.assertCreate( NetworkModel, '/networks/createfor/{}'.format(gid), @@ -244,32 +243,32 @@ class NetworksCreateForTestCase(NetworkTestMixin, TestRunnerMixin, ItemCreateVia ] ) - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_USER) def test_01_as_user(self): """Test access as user 'user'.""" - self._attempt_fail(vial.test.fixtures.DEMO_GROUP_A) - self._attempt_fail(vial.test.fixtures.DEMO_GROUP_B) + self._attempt_fail(hawat.test.fixtures.DEMO_GROUP_A) + self._attempt_fail(hawat.test.fixtures.DEMO_GROUP_B) - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_DEVELOPER) def test_02_as_developer(self): """Test access as user 'developer'.""" - self._attempt_succeed(vial.test.fixtures.DEMO_GROUP_A) - self._attempt_fail(vial.test.fixtures.DEMO_GROUP_B) + self._attempt_succeed(hawat.test.fixtures.DEMO_GROUP_A) + self._attempt_fail(hawat.test.fixtures.DEMO_GROUP_B) - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_MAINTAINER) def test_03_as_maintainer(self): """Test access as user 'maintainer'.""" - self._attempt_succeed(vial.test.fixtures.DEMO_GROUP_A) - self._attempt_succeed(vial.test.fixtures.DEMO_GROUP_B) + self._attempt_succeed(hawat.test.fixtures.DEMO_GROUP_A) + self._attempt_succeed(hawat.test.fixtures.DEMO_GROUP_B) - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_ADMIN) def test_04_as_admin(self): """Test access as user 'admin'.""" - self._attempt_succeed(vial.test.fixtures.DEMO_GROUP_A) - self._attempt_succeed(vial.test.fixtures.DEMO_GROUP_B) + self._attempt_succeed(hawat.test.fixtures.DEMO_GROUP_A) + self._attempt_succeed(hawat.test.fixtures.DEMO_GROUP_B) -class NetworksUpdateTestCase(NetworkTestMixin, TestRunnerMixin, VialTestCase): +class NetworksUpdateTestCase(NetworkTestMixin, TestRunnerMixin, HawatTestCase): """Class for testing ``networks.update`` endpoint.""" def _attempt_fail(self, nname): @@ -289,32 +288,32 @@ class NetworksUpdateTestCase(NetworkTestMixin, TestRunnerMixin, VialTestCase): ] ) - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_USER) def test_01_as_user(self): """Test access as user 'user'.""" - self._attempt_fail(self._nname(vial.test.fixtures.DEMO_GROUP_A)) - self._attempt_fail(self._nname(vial.test.fixtures.DEMO_GROUP_B)) + self._attempt_fail(self._nname(hawat.test.fixtures.DEMO_GROUP_A)) + self._attempt_fail(self._nname(hawat.test.fixtures.DEMO_GROUP_B)) - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_DEVELOPER) def test_04_as_developer(self): """Test access as user 'developer'.""" - self._attempt_succeed(self._nname(vial.test.fixtures.DEMO_GROUP_A)) - self._attempt_fail(self._nname(vial.test.fixtures.DEMO_GROUP_B)) + self._attempt_succeed(self._nname(hawat.test.fixtures.DEMO_GROUP_A)) + self._attempt_fail(self._nname(hawat.test.fixtures.DEMO_GROUP_B)) - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_MAINTAINER) def test_05_as_maintainer(self): """Test access as user 'maintainer'.""" - self._attempt_succeed(self._nname(vial.test.fixtures.DEMO_GROUP_A)) - self._attempt_succeed(self._nname(vial.test.fixtures.DEMO_GROUP_B)) + self._attempt_succeed(self._nname(hawat.test.fixtures.DEMO_GROUP_A)) + self._attempt_succeed(self._nname(hawat.test.fixtures.DEMO_GROUP_B)) - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_ADMIN) def test_06_as_admin(self): """Test access as user 'admin'.""" - self._attempt_succeed(self._nname(vial.test.fixtures.DEMO_GROUP_A)) - self._attempt_succeed(self._nname(vial.test.fixtures.DEMO_GROUP_B)) + self._attempt_succeed(self._nname(hawat.test.fixtures.DEMO_GROUP_A)) + self._attempt_succeed(self._nname(hawat.test.fixtures.DEMO_GROUP_B)) -class NetworksDeleteTestCase(NetworkTestMixin, TestRunnerMixin, VialTestCase): +class NetworksDeleteTestCase(NetworkTestMixin, TestRunnerMixin, HawatTestCase): """Class for testing ``networks.delete`` endpoint.""" def _attempt_fail(self, nname): @@ -344,32 +343,32 @@ class NetworksDeleteTestCase(NetworkTestMixin, TestRunnerMixin, VialTestCase): ] ) - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_USER) def test_01_as_user(self): """Test access as user 'user'.""" - self._attempt_fail(self._nname(vial.test.fixtures.DEMO_GROUP_A)) - self._attempt_fail(self._nname(vial.test.fixtures.DEMO_GROUP_B)) + self._attempt_fail(self._nname(hawat.test.fixtures.DEMO_GROUP_A)) + self._attempt_fail(self._nname(hawat.test.fixtures.DEMO_GROUP_B)) - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_DEVELOPER) def test_02_as_developer(self): """Test access as user 'developer'.""" - self._attempt_succeed(self._nname(vial.test.fixtures.DEMO_GROUP_A)) - self._attempt_fail(self._nname(vial.test.fixtures.DEMO_GROUP_B)) + self._attempt_succeed(self._nname(hawat.test.fixtures.DEMO_GROUP_A)) + self._attempt_fail(self._nname(hawat.test.fixtures.DEMO_GROUP_B)) - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_MAINTAINER) def test_03_as_maintainer(self): """Test access as user 'maintainer'.""" - self._attempt_succeed(self._nname(vial.test.fixtures.DEMO_GROUP_A)) - self._attempt_succeed(self._nname(vial.test.fixtures.DEMO_GROUP_B)) + self._attempt_succeed(self._nname(hawat.test.fixtures.DEMO_GROUP_A)) + self._attempt_succeed(self._nname(hawat.test.fixtures.DEMO_GROUP_B)) - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_ADMIN) def test_04_as_admin(self): """Test access as user 'admin'.""" - self._attempt_succeed(self._nname(vial.test.fixtures.DEMO_GROUP_A)) - self._attempt_succeed(self._nname(vial.test.fixtures.DEMO_GROUP_B)) + self._attempt_succeed(self._nname(hawat.test.fixtures.DEMO_GROUP_A)) + self._attempt_succeed(self._nname(hawat.test.fixtures.DEMO_GROUP_B)) -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- if __name__ == "__main__": diff --git a/lib/hawat/blueprints/pdnsr/__init__.py b/lib/hawat/blueprints/pdnsr/__init__.py index 83dc4d78f5294c71a763b6417d00e46df2a80309..b0c78781735fd93afdff9df722b10bbf153557c3 100644 --- a/lib/hawat/blueprints/pdnsr/__init__.py +++ b/lib/hawat/blueprints/pdnsr/__init__.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ @@ -41,28 +41,25 @@ Provided endpoints * *Methods:* ``GET``, ``POST`` """ - __author__ = "Jan Mach <jan.mach@cesnet.cz>" __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" - import flask from flask_babel import lazy_gettext import mentat.services.pdnsr from mentat.const import tr_ -import vial.db -import vial.const -import vial.acl -from vial.app import VialBlueprint -from vial.view import RenderableView -from vial.view.mixin import HTMLMixin, AJAXMixin, SnippetMixin -from vial.utils import URLParamsBuilder +import hawat.db +import hawat.const +import hawat.acl +from hawat.base import HawatBlueprint +from hawat.view import RenderableView +from hawat.view.mixin import HTMLMixin, AJAXMixin, SnippetMixin +from hawat.utils import URLParamsBuilder import hawat.const from hawat.blueprints.pdnsr.forms import PDNSRSearchForm - BLUEPRINT_NAME = 'pdnsr' """Name of the blueprint as module global constant.""" @@ -88,32 +85,32 @@ class AbstractSearchView(RenderableView): # pylint: disable=locally-disabled,ab Mandatory interface required by the :py:func:`flask.views.View.dispatch_request`. Will be called by the *Flask* framework to service the request. """ - form = PDNSRSearchForm(flask.request.args, meta = {'csrf': False}) + form = PDNSRSearchForm(flask.request.args, meta={'csrf': False}) - if vial.const.FORM_ACTION_SUBMIT in flask.request.args: + if hawat.const.FORM_ACTION_SUBMIT in flask.request.args: if form.validate(): form_data = form.data pdnsr_service = mentat.services.pdnsr.service() self.response_context.update( - search_item = form.search.data, - search_url = pdnsr_service.get_url_lookup_ip(form.search.data), - form_data = form_data + search_item=form.search.data, + search_url=pdnsr_service.get_url_lookup_ip(form.search.data), + form_data=form_data ) try: self.response_context.update( - search_result = pdnsr_service.lookup_ip( + search_result=pdnsr_service.lookup_ip( form.search.data, form.sortby.data, form.limit.data ) ) except Exception as exc: - self.flash(str(exc), level = 'error') + self.flash(str(exc), level='error') self.response_context.update( - search_form = form, - request_args = flask.request.args + search_form=form, + request_args=flask.request.args ) return self.generate_response() @@ -135,7 +132,7 @@ class APISearchView(AJAXMixin, AbstractSearchView): # pylint: disable=locally-d View responsible for querying PassiveDNS service and presenting the results in the form of JSON document. """ - methods = ['GET','POST'] + methods = ['GET', 'POST'] @classmethod def get_view_name(cls): @@ -163,10 +160,10 @@ class SnippetSearchView(SnippetMixin, AbstractSearchView): # pylint: disable=lo return 'sptsearch' -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- -class PDNSRBlueprint(VialBlueprint): +class PDNSRBlueprint(HawatBlueprint): """Pluggable module - PassiveDNS service (*pdnsr*).""" @classmethod @@ -179,8 +176,8 @@ class PDNSRBlueprint(VialBlueprint): app.menu_main.add_entry( 'view', 'more.{}'.format(BLUEPRINT_NAME), - position = 40, - view = SearchView + position=40, + view=SearchView ) # Register context actions provided by this module. @@ -201,33 +198,37 @@ class PDNSRBlueprint(VialBlueprint): app.set_oads( hawat.const.AODS_IP4, SnippetSearchView, - URLParamsBuilder({'submit': tr_('Search'), 'sortby': 'count.desc'}).add_rule('search').add_kwrule('render', False, True) + URLParamsBuilder({'submit': tr_('Search'), 'sortby': 'count.desc'}).add_rule('search').add_kwrule('render', + False, + True) ) app.set_oads( hawat.const.AODS_IP6, SnippetSearchView, - URLParamsBuilder({'submit': tr_('Search'), 'sortby': 'count.desc'}).add_rule('search').add_kwrule('render', False, True) + URLParamsBuilder({'submit': tr_('Search'), 'sortby': 'count.desc'}).add_rule('search').add_kwrule('render', + False, + True) ) -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- def get_blueprint(): """ - Mandatory interface for :py:mod:`vial.Vial` and factory function. This function - must return a valid instance of :py:class:`vial.app.VialBlueprint` or + Mandatory interface for :py:mod:`hawat.Hawat` and factory function. This function + must return a valid instance of :py:class:`hawat.app.HawatBlueprint` or :py:class:`flask.Blueprint`. """ hbp = PDNSRBlueprint( BLUEPRINT_NAME, __name__, - template_folder = 'templates' + template_folder='templates' ) - hbp.register_view_class(SearchView, '/{}/search'.format(BLUEPRINT_NAME)) - hbp.register_view_class(APISearchView, '/api/{}/search'.format(BLUEPRINT_NAME)) + hbp.register_view_class(SearchView, '/{}/search'.format(BLUEPRINT_NAME)) + hbp.register_view_class(APISearchView, '/api/{}/search'.format(BLUEPRINT_NAME)) hbp.register_view_class(SnippetSearchView, '/snippet/{}/search'.format(BLUEPRINT_NAME)) return hbp diff --git a/lib/hawat/blueprints/pdnsr/forms.py b/lib/hawat/blueprints/pdnsr/forms.py index 7a44eedcd47b6909592575d79fcbc6ca39b950b5..7656dee677abdcb591358802647ee96660b4a8b1 100644 --- a/lib/hawat/blueprints/pdnsr/forms.py +++ b/lib/hawat/blueprints/pdnsr/forms.py @@ -1,27 +1,26 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ This module contains custom external PassiveDNS database search form for Hawat. """ - __author__ = "Jan Mach <jan.mach@cesnet.cz>" __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" - import wtforms import flask_wtf from flask_babel import lazy_gettext -import vial.forms +import hawat.const +import hawat.forms class PDNSRSearchForm(flask_wtf.FlaskForm): @@ -30,37 +29,37 @@ class PDNSRSearchForm(flask_wtf.FlaskForm): """ search = wtforms.StringField( lazy_gettext('Search PassiveDNS:'), - validators = [ + validators=[ wtforms.validators.DataRequired(), - vial.forms.check_ip_record + hawat.forms.check_ip_record ] ) sortby = wtforms.SelectField( lazy_gettext('Sort by:'), - validators = [ + validators=[ wtforms.validators.Optional() ], - choices = [ + choices=[ ('', lazy_gettext('without explicit sorting')), ('domain.desc', lazy_gettext('by domain name descending')), - ('domain.asc', lazy_gettext('by domain name ascending')), + ('domain.asc', lazy_gettext('by domain name ascending')), ('count.desc', lazy_gettext('by hit count descending')), - ('count.asc', lazy_gettext('by hit count ascending')), + ('count.asc', lazy_gettext('by hit count ascending')), ('firstseen.desc', lazy_gettext('by first seen time descending')), - ('firstseen`.asc', lazy_gettext('by first seen time ascending')), + ('firstseen`.asc', lazy_gettext('by first seen time ascending')), ('lastseen.desc', lazy_gettext('by last seen time descending')), - ('lastseen.asc', lazy_gettext('by last seen time ascending')) + ('lastseen.asc', lazy_gettext('by last seen time ascending')) ], - default = '' + default='' ) limit = wtforms.SelectField( lazy_gettext('Pager limit:'), - validators = [ + validators=[ wtforms.validators.Optional() ], - filters = [int], - choices = [(0, lazy_gettext('without explicit limit'))] + vial.const.PAGER_LIMIT_CHOICES, - default = 0 + filters=[int], + choices=[(0, lazy_gettext('without explicit limit'))] + hawat.const.PAGER_LIMIT_CHOICES, + default=0 ) submit = wtforms.SubmitField( lazy_gettext('Search') diff --git a/lib/hawat/blueprints/pdnsr/templates/pdnsr/search.html b/lib/hawat/blueprints/pdnsr/templates/pdnsr/search.html index c1d3c94c650af4250b568766eef30c13a319c995..36bb478ded32b920d7d63ee9f8ae3f18a2ee3d5b 100644 --- a/lib/hawat/blueprints/pdnsr/templates/pdnsr/search.html +++ b/lib/hawat/blueprints/pdnsr/templates/pdnsr/search.html @@ -7,7 +7,7 @@ <div class="col-lg-12"> <div class="jumbotron" style="margin-top: 1em;"> - <h2>{{ vial_current_view.get_view_title() }}</h2> + <h2>{{ hawat_current_view.get_view_title() }}</h2> <form method="GET" class="form-inline" action="{{ url_for('pdnsr.search') }}"> <div class="form-group{% if search_form.search.errors %}{{ ' has-error' }}{% endif %}"> {{ search_form.search.label(class_='sr-only') }} diff --git a/lib/hawat/blueprints/pdnsr/test/__init__.py b/lib/hawat/blueprints/pdnsr/test/__init__.py index bba97a3602475426010aa9a8939a3f7719ac4994..b86409bb7e1313dca3a7314074adb2477a3c18fb 100644 --- a/lib/hawat/blueprints/pdnsr/test/__init__.py +++ b/lib/hawat/blueprints/pdnsr/test/__init__.py @@ -1,28 +1,27 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ Unit tests for :py:mod:`hawat.blueprints.pdnsr`. """ - import unittest -import vial.const -import vial.test -import vial.db -from vial.test import VialTestCase +import hawat.const +import hawat.test +import hawat.db +from hawat.test import HawatTestCase from hawat.test.runner import TestRunnerMixin -class SearchTestCase(TestRunnerMixin, VialTestCase): +class SearchTestCase(TestRunnerMixin, HawatTestCase): """ Class for testing ``pdnsr.search`` endpoint. """ @@ -43,28 +42,28 @@ class SearchTestCase(TestRunnerMixin, VialTestCase): """Test access as anonymous user.""" self._attempt_fail() - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_USER) def test_02_as_user(self): """Test access as user ``user``.""" self._attempt_succeed() - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_DEVELOPER) def test_03_as_developer(self): """Test access as user ``developer``.""" self._attempt_succeed() - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_MAINTAINER) def test_04_as_maintainer(self): """Test access as user ``maintainer``.""" self._attempt_succeed() - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_ADMIN) def test_05_as_admin(self): """Test access as user ``admin``.""" self._attempt_succeed() -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- if __name__ == "__main__": diff --git a/lib/hawat/blueprints/performance/__init__.py b/lib/hawat/blueprints/performance/__init__.py index da0af0808122a57061dd154a5993ef221a1eb339..a74a8d919826c3b5ea4b3609c855e43951f0c6df 100644 --- a/lib/hawat/blueprints/performance/__init__.py +++ b/lib/hawat/blueprints/performance/__init__.py @@ -1,22 +1,20 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ This pluggable module provides access to system performance statistics. """ - __author__ = "Jan Mach <jan.mach@cesnet.cz>" __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" - import collections import flask @@ -25,10 +23,9 @@ from flask_babel import gettext, lazy_gettext import mentat.const import mentat.stats.rrd -from vial.app import VialBlueprint -from vial.view import SimpleView, FileNameView -from vial.view.mixin import HTMLMixin - +from hawat.base import HawatBlueprint +from hawat.view import SimpleView, FileNameView +from hawat.view.mixin import HTMLMixin BLUEPRINT_NAME = 'performance' """Name of the blueprint as module global constant.""" @@ -36,7 +33,7 @@ BLUEPRINT_NAME = 'performance' class ViewView(HTMLMixin, SimpleView): """ - View reponsible for presenting system performance in using RRD charts. + View responsible for presenting system performance in using RRD charts. """ authentication = True @@ -61,11 +58,12 @@ class ViewView(HTMLMixin, SimpleView): try: rrd_stats = mentat.stats.rrd.RrdStats( flask.current_app.mconfig[mentat.const.CKEY_CORE_STATISTICS][mentat.const.CKEY_CORE_STATISTICS_RRDSDIR], - flask.current_app.mconfig[mentat.const.CKEY_CORE_STATISTICS][mentat.const.CKEY_CORE_STATISTICS_REPORTSDIR], + flask.current_app.mconfig[mentat.const.CKEY_CORE_STATISTICS][ + mentat.const.CKEY_CORE_STATISTICS_REPORTSDIR], ) - charts = rrd_stats.lookup() + charts = rrd_stats.lookup() - # Convert list of all existing charts to a structure more apropriate + # Convert list of all existing charts to a structure more appropriate # for display. for chrt in charts: key = chrt['ds_type'] @@ -81,17 +79,17 @@ class ViewView(HTMLMixin, SimpleView): self.flash( gettext("Error when displaying system performance, encountered file not found error.") ) - #flask.current_app.log_exception_with_label( + # flask.current_app.log_exception_with_label( # traceback.TracebackException(*sys.exc_info()), # "Error when displaying system performance" - #) + # ) self.response_context['chartdict'] = chartdict class DataView(FileNameView): """ - View reponsible for accessing raw performance data in RRD databases. + View responsible for accessing raw performance data in RRD databases. """ authentication = True @@ -105,12 +103,13 @@ class DataView(FileNameView): @classmethod def get_directory_path(cls): - return flask.current_app.mconfig[mentat.const.CKEY_CORE_STATISTICS][mentat.const.CKEY_CORE_STATISTICS_REPORTSDIR] + return flask.current_app.mconfig[mentat.const.CKEY_CORE_STATISTICS][ + mentat.const.CKEY_CORE_STATISTICS_REPORTSDIR] class RRDDBView(FileNameView): """ - View reponsible for accessing performance RRD databases. + View responsible for accessing performance RRD databases. """ authentication = True @@ -127,10 +126,10 @@ class RRDDBView(FileNameView): return flask.current_app.mconfig[mentat.const.CKEY_CORE_STATISTICS][mentat.const.CKEY_CORE_STATISTICS_RRDSDIR] -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- -class PerformanceBlueprint(VialBlueprint): +class PerformanceBlueprint(HawatBlueprint): """Pluggable module - system processing performance (*performance*).""" @classmethod @@ -141,32 +140,32 @@ class PerformanceBlueprint(VialBlueprint): app.menu_main.add_entry( 'view', 'more.{}'.format(BLUEPRINT_NAME), - position = 100, - group = lazy_gettext('Status overview'), - view = ViewView + position=100, + group=lazy_gettext('Status overview'), + view=ViewView ) -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- def get_blueprint(): """ - Mandatory interface for :py:mod:`vial.Vial` and factory function. This function - must return a valid instance of :py:class:`vial.app.VialBlueprint` or + Mandatory interface for :py:mod:`hawat.Hawat` and factory function. This function + must return a valid instance of :py:class:`hawat.app.HawatBlueprint` or :py:class:`flask.Blueprint`. """ hbp = PerformanceBlueprint( BLUEPRINT_NAME, __name__, - template_folder = 'templates', - static_folder = 'static', - url_prefix = '/{}'.format(BLUEPRINT_NAME) + template_folder='templates', + static_folder='static', + url_prefix='/{}'.format(BLUEPRINT_NAME) ) - hbp.register_view_class(ViewView, '/view') - hbp.register_view_class(DataView, '/data/<filename>') + hbp.register_view_class(ViewView, '/view') + hbp.register_view_class(DataView, '/data/<filename>') hbp.register_view_class(RRDDBView, '/rrds/<filename>') return hbp diff --git a/lib/hawat/blueprints/performance/templates/performance/view.html b/lib/hawat/blueprints/performance/templates/performance/view.html index 9135eec0076d73ea879a25946bd47856c815d2f0..f243caa17726eec6932eb997775a907c55cd1e87 100644 --- a/lib/hawat/blueprints/performance/templates/performance/view.html +++ b/lib/hawat/blueprints/performance/templates/performance/view.html @@ -4,7 +4,7 @@ <div class="row"> <div class="col-lg-12"> - <h2>{{ vial_current_view.get_view_title() }}</h2> + <h2>{{ hawat_current_view.get_view_title() }}</h2> <hr> <!-- Nav tabs --> diff --git a/lib/hawat/blueprints/performance/test/__init__.py b/lib/hawat/blueprints/performance/test/__init__.py index 64c7c3f9e175b02d0108c299162719ac96732fab..382b0b1d22ed4bbf877622d94bf87f5bb67a160b 100644 --- a/lib/hawat/blueprints/performance/test/__init__.py +++ b/lib/hawat/blueprints/performance/test/__init__.py @@ -1,28 +1,27 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ Unit tests for :py:mod:`hawat.blueprints.performance`. """ - import unittest -import vial.const -import vial.test -import vial.db -from vial.test import VialTestCase +import hawat.const +import hawat.test +import hawat.db +from hawat.test import HawatTestCase from hawat.test.runner import TestRunnerMixin -class SearchTestCase(TestRunnerMixin, VialTestCase): +class SearchTestCase(TestRunnerMixin, HawatTestCase): """ Class for testing ``performance.view`` endpoint. """ @@ -35,7 +34,7 @@ class SearchTestCase(TestRunnerMixin, VialTestCase): b'Redirecting...', b'login?next=' ], - follow_redirects = False + follow_redirects=False ) def _attempt_succeed(self): @@ -51,28 +50,28 @@ class SearchTestCase(TestRunnerMixin, VialTestCase): """Test access as anonymous user.""" self._attempt_fail_redirect() - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_USER) def test_02_as_user(self): """Test access as user ``user``.""" self._attempt_succeed() - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_DEVELOPER) def test_03_as_developer(self): """Test access as user ``developer``.""" self._attempt_succeed() - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_MAINTAINER) def test_04_as_maintainer(self): """Test access as user ``maintainer``.""" self._attempt_succeed() - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_ADMIN) def test_05_as_admin(self): """Test access as user ``admin``.""" self._attempt_succeed() -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- if __name__ == "__main__": diff --git a/lib/hawat/blueprints/reports/__init__.py b/lib/hawat/blueprints/reports/__init__.py index ab03eb87d5a2cadd5d046441f9d0a95c71855210..a528e102546f626a45d0653b66c378806bb0487d 100644 --- a/lib/hawat/blueprints/reports/__init__.py +++ b/lib/hawat/blueprints/reports/__init__.py @@ -1,27 +1,26 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ This pluggable module provides access to periodical event reports. """ - __author__ = "Jan Mach <jan.mach@cesnet.cz>" __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" - import re import os.path import datetime import dateutil.parser import pytz +import markupsafe from jinja2.loaders import ChoiceLoader, FileSystemLoader import flask @@ -39,12 +38,12 @@ from mentat.const import tr_ import hawat.const -import vial.menu -import vial.acl -from vial.app import VialBlueprint -from vial.view import RenderableView, FileIdView, BaseSearchView, ItemShowView, ItemDeleteView -from vial.view.mixin import HTMLMixin, AJAXMixin, SQLAlchemyMixin -from vial.utils import URLParamsBuilder +import hawat.menu +import hawat.acl +from hawat.base import HawatBlueprint +from hawat.view import RenderableView, FileIdView, BaseSearchView, ItemShowView, ItemDeleteView +from hawat.view.mixin import HTMLMixin, AJAXMixin, SQLAlchemyMixin +from hawat.utils import URLParamsBuilder from hawat.blueprints.reports.forms import EventReportSearchForm, ReportingDashboardForm, \ FeedbackForm @@ -74,6 +73,7 @@ def build_related_search_params(item): ) return related_events_search_params + def adjust_query_for_groups(query, groups): """ Adjust given SQLAlchemy query for current user. In case user specified set of @@ -84,16 +84,17 @@ def adjust_query_for_groups(query, groups): # Adjust query to filter only selected groups. if groups: # Naive approach. - #query = query.filter(model.group_id.in_([grp.id for grp in groups])) + # query = query.filter(model.group_id.in_([grp.id for grp in groups])) # "Joined" approach. return query.join(_asoc_groups_reports).join(GroupModel).filter(GroupModel.id.in_([grp.id for grp in groups])) # For non-administrators restrict query only to groups they are member of. - if not flask_login.current_user.has_role(vial.const.ROLE_ADMIN): + if not flask_login.current_user.has_role(hawat.const.ROLE_ADMIN): # Naive approach. - #query = query.filter(model.group.has(GroupModel.members.any(UserModel.id == flask_login.current_user.id))) + # query = query.filter(model.group.has(GroupModel.members.any(UserModel.id == flask_login.current_user.id))) # "Joined" approach. - return query.join(_asoc_groups_reports).join(GroupModel).filter(GroupModel.members.any(UserModel.id == flask_login.current_user.id)) + return query.join(_asoc_groups_reports).join(GroupModel).filter( + GroupModel.members.any(UserModel.id == flask_login.current_user.id)) return query @@ -124,7 +125,7 @@ class SearchView(HTMLMixin, SQLAlchemyMixin, BaseSearchView): # pylint: disable @staticmethod def get_search_form(request_args): - return EventReportSearchForm(request_args, meta = {'csrf': False}) + return EventReportSearchForm(request_args, meta={'csrf': False}) @staticmethod def build_query(query, model, form_args): @@ -152,7 +153,7 @@ class SearchView(HTMLMixin, SQLAlchemyMixin, BaseSearchView): # pylint: disable def do_after_search(self, items): if items: self.response_context.update( - max_evcount_rep = max([x.evcount_rep for x in items]) + max_evcount_rep=max([x.evcount_rep for x in items]) ) @@ -172,7 +173,7 @@ class ShowView(HTMLMixin, SQLAlchemyMixin, ItemShowView): def get_menu_legend(cls, **kwargs): return lazy_gettext( 'View details of event report "%(item)s"', - item = flask.escape(str(kwargs['item'])) + item=markupsafe.escape(str(kwargs['item'])) ) @classmethod @@ -187,72 +188,73 @@ class ShowView(HTMLMixin, SQLAlchemyMixin, ItemShowView): def authorize_item_action(cls, **kwargs): for group in kwargs['item'].groups: permission_mm = flask_principal.Permission( - vial.acl.MembershipNeed(group.id), - vial.acl.ManagementNeed(group.id) + hawat.acl.MembershipNeed(group.id), + hawat.acl.ManagementNeed(group.id) ) if permission_mm.can(): return permission_mm.can() - return vial.acl.PERMISSION_POWER.can() + return hawat.acl.PERMISSION_POWER.can() @classmethod def get_breadcrumbs_menu(cls): # pylint: disable=locally-disabled,unused-argument - action_menu = vial.menu.Menu() + action_menu = hawat.menu.Menu() action_menu.add_entry( 'endpoint', 'home', - endpoint = flask.current_app.config['ENDPOINT_HOME'] + endpoint=flask.current_app.config['ENDPOINT_HOME'] ) action_menu.add_entry( 'endpoint', 'search', - endpoint = '{}.search'.format(cls.module_name) + endpoint='{}.search'.format(cls.module_name) ) action_menu.add_entry( 'endpoint', 'show', - endpoint = '{}.show'.format(cls.module_name), + endpoint='{}.show'.format(cls.module_name), ) return action_menu @classmethod def get_action_menu(cls): - action_menu = vial.menu.Menu() + action_menu = hawat.menu.Menu() action_menu.add_entry( 'endpoint', 'search', - endpoint = 'events.search', - title = lazy_gettext('Search'), - legend = lambda **x: lazy_gettext('Search for all events related to report "%(item)s"', item = flask.escape(x['item'].label)), - url = lambda **x: flask.url_for('events.search', **build_related_search_params(x['item'])) + endpoint='events.search', + title=lazy_gettext('Search'), + legend=lambda **x: lazy_gettext('Search for all events related to report "%(item)s"', + item=markupsafe.escape(x['item'].label)), + url=lambda **x: flask.url_for('events.search', **build_related_search_params(x['item'])) ) action_menu.add_entry( 'endpoint', 'delete', - endpoint = 'reports.delete' + endpoint='reports.delete' ) action_menu.add_entry( 'submenu', 'more', - align_right = True, - legend = lazy_gettext('More actions') + align_right=True, + legend=lazy_gettext('More actions') ) action_menu.add_entry( 'endpoint', 'more.downloadjson', - endpoint = 'reports.data', - title = lazy_gettext('Download data in JSON format'), - url = lambda **x: flask.url_for('reports.data', fileid = x['item'].label, filetype = 'json'), - icon = 'action-download', - hidelegend = True + endpoint='reports.data', + title=lazy_gettext('Download data in JSON format'), + url=lambda **x: flask.url_for('reports.data', fileid=x['item'].label, filetype='json'), + icon='action-download', + hidelegend=True ) action_menu.add_entry( 'endpoint', 'more.downloadjsonzip', - endpoint = 'reports.data', - title = lazy_gettext('Download compressed data in JSON format'), - url = lambda **x: flask.url_for('reports.data', fileid = x['item'].label, filetype = 'jsonzip'), - icon = 'action-download-zip', - hidelegend = True + endpoint='reports.data', + title=lazy_gettext('Download compressed data in JSON format'), + url=lambda **x: flask.url_for('reports.data', fileid=x['item'].label, filetype='jsonzip'), + icon='action-download-zip', + hidelegend=True ) return action_menu @@ -265,7 +267,7 @@ class ShowView(HTMLMixin, SQLAlchemyMixin, ItemShowView): return format_datetime( dateutil.parser.parse(val).replace(tzinfo=pytz.utc).astimezone(tzone), BABEL_RFC3339_FORMAT, - rebase = False + rebase=False ) @staticmethod @@ -277,7 +279,7 @@ class ShowView(HTMLMixin, SQLAlchemyMixin, ItemShowView): return format_datetime( dateutil.parser.parse(val).replace(tzinfo=pytz.utc).astimezone(tzone), format_str, - rebase = False + rebase=False ) @staticmethod @@ -290,21 +292,24 @@ class ShowView(HTMLMixin, SQLAlchemyMixin, ItemShowView): def do_before_response(self, **kwargs): if 'item' in self.response_context and self.response_context['item']: self.response_context.update( - statistics = self.response_context['item'].statistics, - template_vars = flask.current_app.mconfig[mentat.const.CKEY_CORE_REPORTER][mentat.const.CKEY_CORE_REPORTER_TEMPLATEVARS], - form = FeedbackForm(), - format_datetime = ShowView.format_datetime, - format_datetime_wz = ShowView.format_datetime_wz, - tz = pytz.timezone(self.response_context['item'].structured_data["timezone"]) if self.response_context['item'].structured_data else None, - tz_utc = pytz.utc, - event_classes_dir = flask.current_app.mconfig[mentat.const.CKEY_CORE_REPORTER][mentat.const.CKEY_CORE_REPORTER_EVENTCLASSESDIR], - escape_id = ShowView.escape_id + statistics=self.response_context['item'].statistics, + template_vars=flask.current_app.mconfig[mentat.const.CKEY_CORE_REPORTER][ + mentat.const.CKEY_CORE_REPORTER_TEMPLATEVARS], + form=FeedbackForm(), + format_datetime=ShowView.format_datetime, + format_datetime_wz=ShowView.format_datetime_wz, + tz=pytz.timezone(self.response_context['item'].structured_data["timezone"]) if self.response_context[ + 'item'].structured_data else None, + tz_utc=pytz.utc, + event_classes_dir=flask.current_app.mconfig[mentat.const.CKEY_CORE_REPORTER][ + mentat.const.CKEY_CORE_REPORTER_EVENTCLASSESDIR], + escape_id=ShowView.escape_id ) if self.response_context['item'].mail_to: mails = self.response_context['item'].mail_to self.response_context.update( - to_mails = [x[3:] if x.startswith('to:') else x for x in mails if not x.startswith('cc:')], - cc_mails = [x[3:] for x in mails if x.startswith('cc:')] + to_mails=[x[3:] if x.startswith('to:') else x for x in mails if not x.startswith('cc:')], + cc_mails=[x[3:] for x in mails if x.startswith('cc:')] ) @@ -396,7 +401,7 @@ class AbstractDashboardView(SQLAlchemyMixin, BaseSearchView): # pylint: disable @staticmethod def get_search_form(request_args): - return ReportingDashboardForm(request_args, meta = {'csrf': False}) + return ReportingDashboardForm(request_args, meta={'csrf': False}) @staticmethod def build_query(query, model, form_args): @@ -420,10 +425,10 @@ class AbstractDashboardView(SQLAlchemyMixin, BaseSearchView): # pylint: disable if items: dt_from = self.response_context['form_data'].get('dt_from', None) if dt_from: - dt_from = dt_from.replace(tzinfo = None) - dt_to = self.response_context['form_data'].get('dt_to', None) + dt_from = dt_from.replace(tzinfo=None) + dt_to = self.response_context['form_data'].get('dt_to', None) if dt_to: - dt_to = dt_to.replace(tzinfo = None) + dt_to = dt_to.replace(tzinfo=None) if not dt_from: dt_from = self.dbcolumn_min(self.dbmodel.createtime) @@ -443,12 +448,12 @@ class AbstractDashboardView(SQLAlchemyMixin, BaseSearchView): # pylint: disable stats['emails'] = d self.response_context.update( - statistics = stats + statistics=stats ) def do_before_response(self, **kwargs): self.response_context.update( - quicksearch_list = self.get_quicksearch_by_time() + quicksearch_list=self.get_quicksearch_by_time() ) @@ -488,17 +493,17 @@ class DeleteView(HTMLMixin, SQLAlchemyMixin, ItemDeleteView): # pylint: disable """ View for deleting existing user accounts. """ - methods = ['GET','POST'] + methods = ['GET', 'POST'] authentication = True - authorization = [vial.acl.PERMISSION_ADMIN] + authorization = [hawat.acl.PERMISSION_ADMIN] @classmethod def get_menu_legend(cls, **kwargs): return lazy_gettext( 'Delete event report "%(item)s"', - item = flask.escape(str(kwargs['item'])) + item=markupsafe.escape(str(kwargs['item'])) ) def get_url_next(self): @@ -518,21 +523,21 @@ class DeleteView(HTMLMixin, SQLAlchemyMixin, ItemDeleteView): # pylint: disable def get_message_success(**kwargs): return gettext( 'Event report <strong>%(item_id)s</strong> was successfully and permanently deleted.', - item_id = flask.escape(str(kwargs['item'])) + item_id=markupsafe.escape(str(kwargs['item'])) ) @staticmethod def get_message_failure(**kwargs): return gettext( 'Unable to delete event report <strong>%(item_id)s</strong>.', - item_id = flask.escape(str(kwargs['item'])) + item_id=markupsafe.escape(str(kwargs['item'])) ) @staticmethod def get_message_cancel(**kwargs): return gettext( 'Canceled deleting event report <strong>%(item_id)s</strong>.', - item_id = flask.escape(str(kwargs['item'])) + item_id=markupsafe.escape(str(kwargs['item'])) ) @@ -563,8 +568,10 @@ class FeedbackView(AJAXMixin, RenderableView): form = FeedbackForm(flask.request.form) if form.validate(): mail_locale = flask.current_app.config['BABEL_DEFAULT_LOCALE'] - link = flask.current_app.mconfig[mentat.const.CKEY_CORE_REPORTER][mentat.const.CKEY_CORE_REPORTER_TEMPLATEVARS]["report_access_url"] + \ - item_id + "/unauth" + "#" + form.section.data + link = \ + flask.current_app.mconfig[mentat.const.CKEY_CORE_REPORTER][mentat.const.CKEY_CORE_REPORTER_TEMPLATEVARS][ + "report_access_url"] + \ + item_id + "/unauth" + "#" + form.section.data feedback_for = item_id + " (" + form.section.data + ", ip: " + form.ip.data + ")" with force_locale(mail_locale): @@ -587,16 +594,18 @@ class FeedbackView(AJAXMixin, RenderableView): self.response_context["message"] = gettext('Thank you. Your feedback was sent to administrators.') else: self.response_context.update( - form_errors=[(field_name, err) for field_name, error_messages in form.errors.items() for err in error_messages] + form_errors=[(field_name, err) for field_name, error_messages in form.errors.items() for err in + error_messages] ) - self.response_context["message"] = "<br />".join([": ".join(err) for err in self.response_context["form_errors"]]) + self.response_context["message"] = "<br />".join( + [": ".join(err) for err in self.response_context["form_errors"]]) return self.generate_response() -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- -class ReportsBlueprint(VialBlueprint): +class ReportsBlueprint(HawatBlueprint): """Pluggable module - periodical event reports (*reports*).""" @classmethod @@ -607,15 +616,15 @@ class ReportsBlueprint(VialBlueprint): app.menu_main.add_entry( 'view', 'dashboards.reporting', - position = 20, - view = DashboardView + position=20, + view=DashboardView ) app.menu_main.add_entry( 'view', BLUEPRINT_NAME, - position = 120, - view = SearchView, - resptitle = True + position=120, + view=SearchView, + resptitle=True ) # Register context actions provided by this module. @@ -623,14 +632,20 @@ class ReportsBlueprint(VialBlueprint): hawat.const.CSAG_ABUSE, tr_('Search for abuse group <strong>%(name)s</strong> in report database'), SearchView, - URLParamsBuilder({'submit': tr_('Search')}).add_rule('groups', True).add_kwrule('dt_from', False, True).add_kwrule('dt_to', False, True) + URLParamsBuilder({'submit': tr_('Search')}).add_rule('groups', True).add_kwrule('dt_from', False, + True).add_kwrule('dt_to', + False, + True) ) app.set_csag( hawat.const.CSAG_ABUSE, tr_('Search for abuse group <strong>%(name)s</strong> in reporting dashboards'), DashboardView, - URLParamsBuilder({'submit': tr_('Search')}).add_rule('groups', True).add_kwrule('dt_from', False, True).add_kwrule('dt_to', False, True) + URLParamsBuilder({'submit': tr_('Search')}).add_rule('groups', True).add_kwrule('dt_from', False, + True).add_kwrule('dt_to', + False, + True) ) @locked_cached_property @@ -640,16 +655,19 @@ class ReportsBlueprint(VialBlueprint): .. versionadded:: 0.5 """ return ChoiceLoader([FileSystemLoader(os.path.join(self.root_path, self.template_folder)), - FileSystemLoader(flask.current_app.mconfig[mentat.const.CKEY_CORE_REPORTER][mentat.const.CKEY_CORE_REPORTER_TEMPLATESDIR]), - FileSystemLoader(flask.current_app.mconfig[mentat.const.CKEY_CORE_REPORTER][mentat.const.CKEY_CORE_REPORTER_EVENTCLASSESDIR])]) + FileSystemLoader(flask.current_app.mconfig[mentat.const.CKEY_CORE_REPORTER][ + mentat.const.CKEY_CORE_REPORTER_TEMPLATESDIR]), + FileSystemLoader(flask.current_app.mconfig[mentat.const.CKEY_CORE_REPORTER][ + mentat.const.CKEY_CORE_REPORTER_EVENTCLASSESDIR])]) + -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- def get_blueprint(): """ - Mandatory interface for :py:mod:`vial.Vial` and factory function. This function - must return a valid instance of :py:class:`vial.app.VialBlueprint` or + Mandatory interface for :py:mod:`hawat.Hawat` and factory function. This function + must return a valid instance of :py:class:`hawat.app.HawatBlueprint` or :py:class:`flask.Blueprint`. """ diff --git a/lib/hawat/blueprints/reports/forms.py b/lib/hawat/blueprints/reports/forms.py index 99513dad78a0048e41760387fa6d37ab7fd74999..40c24895dd317b58129ce425bc8b107aef757571 100644 --- a/lib/hawat/blueprints/reports/forms.py +++ b/lib/hawat/blueprints/reports/forms.py @@ -1,22 +1,20 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ This module contains custom event report search form for Hawat. """ - __author__ = "Jan Mach <jan.mach@cesnet.cz>" __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" - import wtforms from wtforms.ext.sqlalchemy.fields import QuerySelectMultipleField @@ -26,9 +24,9 @@ from flask_babel import lazy_gettext import mentat.const from mentat.datatype.sqldb import UserModel, GroupModel -import vial.const -import vial.forms -import vial.db +import hawat.const +import hawat.forms +import hawat.db import hawat.const @@ -37,16 +35,17 @@ def get_available_groups(): Query the database for list of all available groups. """ # In case current user is administrator provide list of all groups. - if flask_login.current_user.has_role(vial.const.ROLE_ADMIN): - return vial.db.db_query(GroupModel).\ - order_by(GroupModel.name).\ + if flask_login.current_user.has_role(hawat.const.ROLE_ADMIN): + return hawat.db.db_query(GroupModel). \ + order_by(GroupModel.name). \ all() - # Otherwise provide only list of groups current user is member of. - return vial.db.db_query(GroupModel).\ - filter(GroupModel.members.any(UserModel.id == flask_login.current_user.id)).\ - order_by(GroupModel.name).\ + # Otherwise, provide only list of groups current user is member of. + return hawat.db.db_query(GroupModel). \ + filter(GroupModel.members.any(UserModel.id == flask_login.current_user.id)). \ + order_by(GroupModel.name). \ all() + def get_severity_choices(): """ Return select choices for report severities. @@ -58,6 +57,7 @@ def get_severity_choices(): ) ) + def get_type_choices(): """ Return select choices for report severities. @@ -69,51 +69,52 @@ def get_type_choices(): ) ) -class EventReportSearchForm(vial.forms.BaseSearchForm): + +class EventReportSearchForm(hawat.forms.BaseSearchForm): """ Class representing event report search form. """ label = wtforms.StringField( lazy_gettext('Label:'), - validators = [ + validators=[ wtforms.validators.Optional() ] ) groups = QuerySelectMultipleField( lazy_gettext('Groups:'), - query_factory = get_available_groups, - allow_blank = False, - get_pk = lambda item: item.name + query_factory=get_available_groups, + allow_blank=False, + get_pk=lambda item: item.name ) severities = wtforms.SelectMultipleField( lazy_gettext('Severities:'), - validators = [ + validators=[ wtforms.validators.Optional(), ], - choices = get_severity_choices(), - filters = [lambda x: x or []] + choices=get_severity_choices(), + filters=[lambda x: x or []] ) types = wtforms.SelectMultipleField( lazy_gettext('Types:'), - validators = [ + validators=[ wtforms.validators.Optional(), ], - choices = get_type_choices(), - filters = [lambda x: x or []] + choices=get_type_choices(), + filters=[lambda x: x or []] ) - dt_from = vial.forms.SmartDateTimeField( + dt_from = hawat.forms.SmartDateTimeField( lazy_gettext('From:'), - validators = [ + validators=[ wtforms.validators.Optional() ], - default = lambda: vial.forms.default_dt_with_delta(hawat.const.DEFAULT_RESULT_TIMEDELTA) + default=lambda: hawat.forms.default_dt_with_delta(hawat.const.DEFAULT_RESULT_TIMEDELTA) ) - dt_to = vial.forms.SmartDateTimeField( + dt_to = hawat.forms.SmartDateTimeField( lazy_gettext('To:'), - validators = [ + validators=[ wtforms.validators.Optional() ], - default = vial.forms.default_dt + default=hawat.forms.default_dt ) @staticmethod @@ -121,7 +122,7 @@ class EventReportSearchForm(vial.forms.BaseSearchForm): """ Check, if given form field is a multivalue field. - :param str field_name: Name of the form field. + :param str field_name: Name of the form field :return: ``True``, if the field can contain multiple values, ``False`` otherwise. :rtype: bool """ @@ -136,23 +137,23 @@ class ReportingDashboardForm(flask_wtf.FlaskForm): """ groups = QuerySelectMultipleField( lazy_gettext('Groups:'), - query_factory = get_available_groups, - allow_blank = False, - get_pk = lambda item: item.name + query_factory=get_available_groups, + allow_blank=False, + get_pk=lambda item: item.name ) - dt_from = vial.forms.SmartDateTimeField( + dt_from = hawat.forms.SmartDateTimeField( lazy_gettext('From:'), - validators = [ + validators=[ wtforms.validators.Optional() ], - default = lambda: vial.forms.default_dt_with_delta(vial.const.DEFAULT_RESULT_TIMEDELTA) + default=lambda: hawat.forms.default_dt_with_delta(hawat.const.DEFAULT_RESULT_TIMEDELTA) ) - dt_to = vial.forms.SmartDateTimeField( + dt_to = hawat.forms.SmartDateTimeField( lazy_gettext('To:'), - validators = [ + validators=[ wtforms.validators.Optional() ], - default = vial.forms.default_dt + default=hawat.forms.default_dt ) submit = wtforms.SubmitField( lazy_gettext('Search') @@ -163,7 +164,7 @@ class ReportingDashboardForm(flask_wtf.FlaskForm): """ Check, if given form field is a multivalue field. - :param str field_name: Name of the form field. + :param str field_name: Name of the form field :return: ``True``, if the field can contain multiple values, ``False`` otherwise. :rtype: bool """ @@ -179,7 +180,7 @@ class FeedbackForm(flask_wtf.FlaskForm): ip = wtforms.HiddenField( validators=[ wtforms.validators.DataRequired(), - vial.forms.check_network_record + hawat.forms.check_network_record ] ) text = wtforms.TextAreaField( diff --git a/lib/hawat/blueprints/reports/templates/reports/dashboard.html b/lib/hawat/blueprints/reports/templates/reports/dashboard.html index 935e1d0c6395067f6bd5a86925592157101e762d..292a080f129b8172a5776fa32479c8d63b536acb 100644 --- a/lib/hawat/blueprints/reports/templates/reports/dashboard.html +++ b/lib/hawat/blueprints/reports/templates/reports/dashboard.html @@ -9,7 +9,7 @@ <!-- Search form - BEGIN ----------------------------------> <div class="jumbotron" style="margin-top: 1em;"> - <h2>{{ vial_current_view.get_view_title() }}</h2> + <h2>{{ hawat_current_view.get_view_title() }}</h2> <hr> <form method="GET" class="form" action="{{ url_for(request.endpoint) }}"> <div class="row"> diff --git a/lib/hawat/blueprints/reports/templates/reports/show.html b/lib/hawat/blueprints/reports/templates/reports/show.html index 2215cc4476016aa05f7f2bb88a863c1408fe14a5..9d3db855895fea3af58f93fa2823005324e395f6 100644 --- a/lib/hawat/blueprints/reports/templates/reports/show.html +++ b/lib/hawat/blueprints/reports/templates/reports/show.html @@ -12,7 +12,7 @@ </ol> {%- endif %} - <h2>{{ vial_current_view.get_view_title() }}</h2> + <h2>{{ hawat_current_view.get_view_title() }}</h2> <hr> <h3> {{ item.label }} diff --git a/lib/hawat/blueprints/reports/test/__init__.py b/lib/hawat/blueprints/reports/test/__init__.py index d21d1c245f78b6d77b35f4ffa5b7713d4e9d970f..050af1ae700fdb3268563a392bae88087f100002 100644 --- a/lib/hawat/blueprints/reports/test/__init__.py +++ b/lib/hawat/blueprints/reports/test/__init__.py @@ -1,28 +1,27 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ Unit tests for :py:mod:`hawat.blueprints.reports`. """ - import unittest -import vial.const -import vial.test -import vial.db -from vial.test import VialTestCase +import hawat.const +import hawat.test +import hawat.db +from hawat.test import HawatTestCase from hawat.test.runner import TestRunnerMixin -class SearchTestCase(TestRunnerMixin, VialTestCase): +class SearchTestCase(TestRunnerMixin, HawatTestCase): """ Class for testing ``reports.view`` endpoint. """ @@ -35,7 +34,7 @@ class SearchTestCase(TestRunnerMixin, VialTestCase): b'Redirecting...', b'login?next=' ], - follow_redirects = False + follow_redirects=False ) def _attempt_succeed(self): @@ -51,28 +50,28 @@ class SearchTestCase(TestRunnerMixin, VialTestCase): """Test access as anonymous user.""" self._attempt_fail_redirect() - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_USER) def test_02_as_user(self): """Test access as user ``user``.""" self._attempt_succeed() - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_DEVELOPER) def test_03_as_developer(self): """Test access as user ``developer``.""" self._attempt_succeed() - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_MAINTAINER) def test_04_as_maintainer(self): """Test access as user ``maintainer``.""" self._attempt_succeed() - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_ADMIN) def test_05_as_admin(self): """Test access as user ``admin``.""" self._attempt_succeed() -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- if __name__ == "__main__": diff --git a/lib/hawat/blueprints/settings_reporting/__init__.py b/lib/hawat/blueprints/settings_reporting/__init__.py index 039ee2ffa305b2bd8d94a5cccf6f646b1769b0f0..61f34d1200dbf947b3d033953b11a34093ec91eb 100644 --- a/lib/hawat/blueprints/settings_reporting/__init__.py +++ b/lib/hawat/blueprints/settings_reporting/__init__.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ @@ -18,11 +18,10 @@ These features include: * deleting existing reporting settings """ - __author__ = "Jan Mach <jan.mach@cesnet.cz>" __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" - +import markupsafe import flask import flask_principal from flask_babel import gettext, lazy_gettext @@ -30,14 +29,14 @@ from flask_babel import gettext, lazy_gettext import mentat.reports.utils from mentat.datatype.sqldb import SettingsReportingModel, ItemChangeLogModel -import vial.acl -from vial.app import VialBlueprint -from vial.view import ItemShowView, ItemCreateView, ItemUpdateView -from vial.view.mixin import HTMLMixin, SQLAlchemyMixin -from hawat.blueprints.settings_reporting.forms import CreateSettingsReportingForm,\ +import hawat.acl +import hawat.menu +from hawat.base import HawatBlueprint +from hawat.view import ItemShowView, ItemCreateView, ItemUpdateView +from hawat.view.mixin import HTMLMixin, SQLAlchemyMixin +from hawat.blueprints.settings_reporting.forms import CreateSettingsReportingForm, \ UpdateSettingsReportingForm - BLUEPRINT_NAME = 'settings_reporting' """Name of the blueprint as module global constant.""" @@ -65,51 +64,51 @@ class ShowView(HTMLMixin, SQLAlchemyMixin, ItemShowView): @classmethod def authorize_item_action(cls, **kwargs): permission_mm = flask_principal.Permission( - vial.acl.MembershipNeed(kwargs['item'].group.id), - vial.acl.ManagementNeed(kwargs['item'].group.id) + hawat.acl.MembershipNeed(kwargs['item'].group.id), + hawat.acl.ManagementNeed(kwargs['item'].group.id) ) - return vial.acl.PERMISSION_POWER.can() or permission_mm.can() + return hawat.acl.PERMISSION_POWER.can() or permission_mm.can() @classmethod def get_breadcrumbs_menu(cls): # pylint: disable=locally-disabled,unused-argument - action_menu = vial.menu.Menu() + action_menu = hawat.menu.Menu() action_menu.add_entry( 'endpoint', 'home', - endpoint = flask.current_app.config['ENDPOINT_HOME'] + endpoint=flask.current_app.config['ENDPOINT_HOME'] ) action_menu.add_entry( 'endpoint', 'list', - endpoint = 'groups.list' + endpoint='groups.list' ) action_menu.add_entry( 'endpoint', 'pshow', - endpoint = 'groups.show' + endpoint='groups.show' ) action_menu.add_entry( 'endpoint', 'show', - endpoint = '{}.show'.format(cls.module_name), + endpoint='{}.show'.format(cls.module_name), ) return action_menu @classmethod def get_action_menu(cls): - action_menu = vial.menu.Menu() + action_menu = hawat.menu.Menu() action_menu.add_entry( 'endpoint', 'showgroup', - endpoint = 'groups.show', - title = lazy_gettext('Group') + endpoint='groups.show', + title=lazy_gettext('Group') ) action_menu.add_entry( 'endpoint', 'update', - endpoint = 'settings_reporting.update' + endpoint='settings_reporting.update' ) return action_menu @@ -119,29 +118,30 @@ class ShowView(HTMLMixin, SQLAlchemyMixin, ItemShowView): item.group ) self.response_context.update( - system_default_repsettings = system_default_repsettings + system_default_repsettings=system_default_repsettings ) - if self.can_access_endpoint('settings_reporting.update', item = item) and self.has_endpoint('changelogs.search'): + if self.can_access_endpoint('settings_reporting.update', item=item) and self.has_endpoint('changelogs.search'): self.response_context.update( - context_action_menu_changelogs = self.get_endpoint_class( + context_action_menu_changelogs=self.get_endpoint_class( 'changelogs.search' ).get_context_action_menu() ) - item_changelog = self.dbsession.query(ItemChangeLogModel).\ - filter(ItemChangeLogModel.model == item.__class__.__name__).\ - filter(ItemChangeLogModel.model_id == item.id).\ - order_by(ItemChangeLogModel.createtime.desc()).\ - limit(100).\ + item_changelog = self.dbsession.query(ItemChangeLogModel). \ + filter(ItemChangeLogModel.model == item.__class__.__name__). \ + filter(ItemChangeLogModel.model_id == item.id). \ + order_by(ItemChangeLogModel.createtime.desc()). \ + limit(100). \ all() - self.response_context.update(item_changelog = item_changelog) + self.response_context.update(item_changelog=item_changelog) + class CreateView(HTMLMixin, SQLAlchemyMixin, ItemCreateView): # pylint: disable=locally-disabled,too-many-ancestors """ View for creating new reporting settings. """ - methods = ['GET','POST'] + methods = ['GET', 'POST'] authentication = True @@ -164,30 +164,30 @@ class CreateView(HTMLMixin, SQLAlchemyMixin, ItemCreateView): # pylint: disable @classmethod def authorize_item_action(cls, **kwargs): permission_m = flask_principal.Permission( - vial.acl.ManagementNeed(kwargs['item'].group.id) + hawat.acl.ManagementNeed(kwargs['item'].group.id) ) - return vial.acl.PERMISSION_POWER.can() or permission_m.can() + return hawat.acl.PERMISSION_POWER.can() or permission_m.can() @staticmethod def get_message_success(**kwargs): return gettext( 'Reporting settings <strong>%(item_id)s</strong> for group <strong>%(parent_id)s</strong> were successfully created.', - item_id = flask.escape(str(kwargs['item'])), - parent_id = flask.escape(str(kwargs['item'].group)) + item_id=markupsafe.escape(str(kwargs['item'])), + parent_id=markupsafe.escape(str(kwargs['item'].group)) ) @staticmethod def get_message_failure(**kwargs): return gettext( 'Unable to create new reporting settings for group <strong>%(parent_id)s</strong>.', - parent_id = flask.escape(str(kwargs['item'].group)) + parent_id=markupsafe.escape(str(kwargs['item'].group)) ) @staticmethod def get_message_cancel(**kwargs): return gettext( 'Canceled creating new reporting settings for group <strong>%(parent_id)s</strong>.', - parent_id = flask.escape(str(kwargs['item'].group)) + parent_id=markupsafe.escape(str(kwargs['item'].group)) ) @staticmethod @@ -199,7 +199,7 @@ class UpdateView(HTMLMixin, SQLAlchemyMixin, ItemUpdateView): # pylint: disable """ View for updating existing reporting settings. """ - methods = ['GET','POST'] + methods = ['GET', 'POST'] authentication = True @@ -207,7 +207,7 @@ class UpdateView(HTMLMixin, SQLAlchemyMixin, ItemUpdateView): # pylint: disable def get_menu_legend(cls, **kwargs): return lazy_gettext( 'Update details of reporting settings for group "%(item)s"', - item = flask.escape(kwargs['item'].group.name) + item=markupsafe.escape(kwargs['item'].group.name) ) @classmethod @@ -225,43 +225,43 @@ class UpdateView(HTMLMixin, SQLAlchemyMixin, ItemUpdateView): # pylint: disable @classmethod def authorize_item_action(cls, **kwargs): permission_m = flask_principal.Permission( - vial.acl.ManagementNeed(kwargs['item'].group.id) + hawat.acl.ManagementNeed(kwargs['item'].group.id) ) - return vial.acl.PERMISSION_POWER.can() or permission_m.can() + return hawat.acl.PERMISSION_POWER.can() or permission_m.can() @staticmethod def get_message_success(**kwargs): return gettext( 'Reporting settings <strong>%(item_id)s</strong> for group <strong>%(parent_id)s</strong> were successfully updated.', - item_id = flask.escape(str(kwargs['item'])), - parent_id = flask.escape(str(kwargs['item'].group)) + item_id=markupsafe.escape(str(kwargs['item'])), + parent_id=markupsafe.escape(str(kwargs['item'].group)) ) @staticmethod def get_message_failure(**kwargs): return gettext( 'Unable to update reporting settings <strong>%(item_id)s</strong> for group <strong>%(parent_id)s</strong>.', - item_id = flask.escape(str(kwargs['item'])), - parent_id = flask.escape(str(kwargs['item'].group)) + item_id=markupsafe.escape(str(kwargs['item'])), + parent_id=markupsafe.escape(str(kwargs['item'].group)) ) @staticmethod def get_message_cancel(**kwargs): return gettext( 'Canceled updating reporting settings <strong>%(item_id)s</strong> for group <strong>%(parent_id)s</strong>.', - item_id = flask.escape(str(kwargs['item'])), - parent_id = flask.escape(str(kwargs['item'].group)) + item_id=markupsafe.escape(str(kwargs['item'])), + parent_id=markupsafe.escape(str(kwargs['item'].group)) ) @staticmethod def get_item_form(item): - return UpdateSettingsReportingForm(obj = item) + return UpdateSettingsReportingForm(obj=item) -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- -class SettingsReportingBlueprint(VialBlueprint): +class SettingsReportingBlueprint(HawatBlueprint): """Pluggable module - reporting settings. (*settings_reporting*)""" @classmethod @@ -269,25 +269,25 @@ class SettingsReportingBlueprint(VialBlueprint): return lazy_gettext('Reporting settings management') -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- def get_blueprint(): """ - Mandatory interface for :py:mod:`vial.Vial` and factory function. This function - must return a valid instance of :py:class:`vial.app.VialBlueprint` or + Mandatory interface for :py:mod:`hawat.Hawat` and factory function. This function + must return a valid instance of :py:class:`hawat.app.HawatBlueprint` or :py:class:`flask.Blueprint`. """ hbp = SettingsReportingBlueprint( BLUEPRINT_NAME, __name__, - template_folder = 'templates', - url_prefix = '/{}'.format(BLUEPRINT_NAME) + template_folder='templates', + url_prefix='/{}'.format(BLUEPRINT_NAME) ) hbp.register_view_class(CreateView, '/create') - hbp.register_view_class(ShowView, '/<int:item_id>/show') + hbp.register_view_class(ShowView, '/<int:item_id>/show') hbp.register_view_class(UpdateView, '/<int:item_id>/update') return hbp diff --git a/lib/hawat/blueprints/settings_reporting/forms.py b/lib/hawat/blueprints/settings_reporting/forms.py index fbe98327c8cae883970e377db5c62d470e08b344..78dc81bfdfb957b7c1e9f89baa6846d74339abd5 100644 --- a/lib/hawat/blueprints/settings_reporting/forms.py +++ b/lib/hawat/blueprints/settings_reporting/forms.py @@ -1,22 +1,20 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ This module contains custom reporting settings management forms for Hawat. """ - __author__ = "Jan Mach <jan.mach@cesnet.cz>" __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" - import os import pytz import wtforms @@ -25,14 +23,13 @@ from babel import Locale from flask_babel import lazy_gettext -import vial.const -import vial.forms -import vial.db +import hawat.const +import hawat.forms +import hawat.db import mentat.const from mentat.datatype.sqldb import GroupModel - REPORT_TRANSLATIONS = '/etc/mentat/templates/reporter/translations' @@ -40,7 +37,7 @@ def get_available_groups(): """ Query the database for list of all available groups. """ - return vial.db.db_query(GroupModel).order_by(GroupModel.name).all() + return hawat.db.db_query(GroupModel).order_by(GroupModel.name).all() def get_available_locales(): @@ -56,7 +53,7 @@ def get_available_locales(): if os.path.isdir(os.path.join(REPORT_TRANSLATIONS, translation)): locale_list.append([translation, translation]) - locale_list = sorted(locale_list, key = lambda x: x[0]) + locale_list = sorted(locale_list, key=lambda x: x[0]) for translation in locale_list: locale_obj = Locale.parse(translation[0]) @@ -65,80 +62,80 @@ def get_available_locales(): return locale_list -class BaseSettingsReportingForm(vial.forms.BaseItemForm): +class BaseSettingsReportingForm(hawat.forms.BaseItemForm): """ Class representing base reporting settings form. """ mode = wtforms.SelectField( lazy_gettext('Reporting mode:'), - validators = [ + validators=[ wtforms.validators.Optional(), ], - choices = [ - ('', lazy_gettext('<< system default >>')), + choices=[ + ('', lazy_gettext('<< system default >>')), (mentat.const.REPORTING_MODE_SUMMARY, lazy_gettext('summary')), - (mentat.const.REPORTING_MODE_EXTRA, lazy_gettext('extra')), - (mentat.const.REPORTING_MODE_BOTH, lazy_gettext('both')), - (mentat.const.REPORTING_MODE_NONE, lazy_gettext('none')) + (mentat.const.REPORTING_MODE_EXTRA, lazy_gettext('extra')), + (mentat.const.REPORTING_MODE_BOTH, lazy_gettext('both')), + (mentat.const.REPORTING_MODE_NONE, lazy_gettext('none')) ], - filters = [lambda x: x or None] + filters=[lambda x: x or None] ) - emails_low = vial.forms.CommaListField( + emails_low = hawat.forms.CommaListField( lazy_gettext('Target emails - low severity:'), - validators = [ + validators=[ wtforms.validators.Optional(), - vial.forms.check_email_list + hawat.forms.check_email_list ] ) - emails_medium = vial.forms.CommaListField( + emails_medium = hawat.forms.CommaListField( lazy_gettext('Target emails - medium severity:'), - validators = [ + validators=[ wtforms.validators.Optional(), - vial.forms.check_email_list + hawat.forms.check_email_list ] ) - emails_high = vial.forms.CommaListField( + emails_high = hawat.forms.CommaListField( lazy_gettext('Target emails - high severity:'), - validators = [ + validators=[ wtforms.validators.Optional(), - vial.forms.check_email_list + hawat.forms.check_email_list ] ) - emails_critical = vial.forms.CommaListField( + emails_critical = hawat.forms.CommaListField( lazy_gettext('Target emails - critical severity:'), - validators = [ + validators=[ wtforms.validators.Optional(), - vial.forms.check_email_list + hawat.forms.check_email_list ] ) - redirect = vial.forms.RadioFieldWithNone( + redirect = hawat.forms.RadioFieldWithNone( lazy_gettext('Report redirection:'), - validators = [ + validators=[ wtforms.validators.Optional(), ], - choices = [ - (None, lazy_gettext('System default')), - (True, lazy_gettext('Enabled')), + choices=[ + (None, lazy_gettext('System default')), + (True, lazy_gettext('Enabled')), (False, lazy_gettext('Disabled')) ], - filters = [vial.forms.str_to_bool_with_none], - coerce = vial.forms.str_to_bool_with_none + filters=[hawat.forms.str_to_bool_with_none], + coerce=hawat.forms.str_to_bool_with_none ) locale = wtforms.SelectField( lazy_gettext('Locale:'), - validators = [ + validators=[ wtforms.validators.Optional(), ], - choices = [('', lazy_gettext('<< system default >>'))] + get_available_locales(), - filters = [lambda x: x or None] + choices=[('', lazy_gettext('<< system default >>'))] + get_available_locales(), + filters=[lambda x: x or None] ) timezone = wtforms.SelectField( lazy_gettext('Timezone:'), - validators = [ + validators=[ wtforms.validators.Optional(), ], - choices = [('', lazy_gettext('<< system default >>'))] + list(zip(pytz.common_timezones, pytz.common_timezones)), - filters = [lambda x: x or None] + choices=[('', lazy_gettext('<< system default >>'))] + list(zip(pytz.common_timezones, pytz.common_timezones)), + filters=[lambda x: x or None] ) submit = wtforms.SubmitField( lazy_gettext('Submit') @@ -147,16 +144,18 @@ class BaseSettingsReportingForm(vial.forms.BaseItemForm): lazy_gettext('Cancel') ) + class CreateSettingsReportingForm(BaseSettingsReportingForm): """ Class representing reporting settings create form. """ group = QuerySelectField( lazy_gettext('Group:'), - query_factory = get_available_groups, - allow_blank = False + query_factory=get_available_groups, + allow_blank=False ) + class UpdateSettingsReportingForm(BaseSettingsReportingForm): """ Class representing reporting settings update form. diff --git a/lib/hawat/blueprints/settings_reporting/templates/settings_reporting/creatupdate.html b/lib/hawat/blueprints/settings_reporting/templates/settings_reporting/creatupdate.html index 87d801231d0c717b64ec605b7eaf5b3b5a98bab8..c0cc7c770fbc0549ee5ea837eecb6cbb21d0923c 100644 --- a/lib/hawat/blueprints/settings_reporting/templates/settings_reporting/creatupdate.html +++ b/lib/hawat/blueprints/settings_reporting/templates/settings_reporting/creatupdate.html @@ -16,7 +16,7 @@ <form method="POST" action="{{ form_url }}"> <fieldset> - <legend>{{ vial_current_view.get_view_title() }}</legend> + <legend>{{ hawat_current_view.get_view_title() }}</legend> {%- if item_action == 'create' %} {{ macros_form.render_form_item_default(form.group) }} diff --git a/lib/hawat/blueprints/settings_reporting/templates/settings_reporting/show.html b/lib/hawat/blueprints/settings_reporting/templates/settings_reporting/show.html index 26beac6bc513b13a5c6b343f5f4a3213a9d6ed11..bb4e9dbc2bea47a0b603f95078adf217a629cd7d 100644 --- a/lib/hawat/blueprints/settings_reporting/templates/settings_reporting/show.html +++ b/lib/hawat/blueprints/settings_reporting/templates/settings_reporting/show.html @@ -6,7 +6,7 @@ <div class="col-lg-12"> {{ macros_page.render_breadcrumbs(item) }} - <h2>{{ vial_current_view.get_view_title() }}</h2> + <h2>{{ hawat_current_view.get_view_title() }}</h2> <hr> <h3>{{ _('Reporting settings for group %(item)s', item = item.group.name) }}{% if item.group.description %} <small>{{ item.group.description }}</small>{% endif %}</h3> <div class="pull-right"> diff --git a/lib/hawat/blueprints/skeleton/__init__.py b/lib/hawat/blueprints/skeleton/__init__.py index 6522f3786f6b4107d69796d2714a4f177c2bca06..0b0b9a070c0aa536eeb3dd93b6d913c26af84980 100644 --- a/lib/hawat/blueprints/skeleton/__init__.py +++ b/lib/hawat/blueprints/skeleton/__init__.py @@ -1,29 +1,26 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ This pluggable module is a highly commented skeleton and an example implementation. """ - __author__ = "Jan Mach <jan.mach@cesnet.cz>" __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" - import flask_login from flask_babel import lazy_gettext -from vial.app import VialBlueprint -from vial.view import SimpleView -from vial.view.mixin import HTMLMixin - +from hawat.base import HawatBlueprint +from hawat.view import SimpleView +from hawat.view.mixin import HTMLMixin BLUEPRINT_NAME = 'skeleton' """Name of the blueprint as module global constant.""" @@ -60,10 +57,10 @@ class ExampleView(HTMLMixin, SimpleView): pass -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- -class SkeletonBlueprint(VialBlueprint): +class SkeletonBlueprint(HawatBlueprint): """Pluggable module - skeleton (*skeleton*).""" @classmethod @@ -74,26 +71,26 @@ class SkeletonBlueprint(VialBlueprint): app.menu_main.add_entry( 'view', 'more.{}'.format(BLUEPRINT_NAME), - position = 1000, - view = ExampleView + position=1000, + view=ExampleView ) -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- def get_blueprint(): """ - Mandatory interface for :py:mod:`vial.Vial` and factory function. This function - must return a valid instance of :py:class:`vial.app.VialBlueprint` or + Mandatory interface for :py:mod:`hawat.Hawat` and factory function. This function + must return a valid instance of :py:class:`hawat.app.HawatBlueprint` or :py:class:`flask.Blueprint`. """ hbp = SkeletonBlueprint( BLUEPRINT_NAME, __name__, - template_folder = 'templates', - url_prefix = '/{}'.format(BLUEPRINT_NAME) + template_folder='templates', + url_prefix='/{}'.format(BLUEPRINT_NAME) ) hbp.register_view_class(ExampleView, '/example') diff --git a/lib/hawat/blueprints/skeleton/templates/skeleton/search.html b/lib/hawat/blueprints/skeleton/templates/skeleton/search.html index 9a9676efc1c292eabcd010c0c29159bbaa51cab6..b38b74718a7ce114a06192dc4fada81d138b39b2 100644 --- a/lib/hawat/blueprints/skeleton/templates/skeleton/search.html +++ b/lib/hawat/blueprints/skeleton/templates/skeleton/search.html @@ -4,7 +4,7 @@ <div class="row"> <div class="col-lg-12"> - <h2>{{ vial_current_view.get_view_title() }}</h2> + <h2>{{ hawat_current_view.get_view_title() }}</h2> <form class="form-inline" id="frm-whi" method="get" action="{{ url_for('whois.search') }}"> <div class="form-group"> diff --git a/lib/hawat/blueprints/status/__init__.py b/lib/hawat/blueprints/status/__init__.py index 1c5d34b4ddb597c34a356bd1d2c8172d81ac34b4..200344c857bfd97fec34f4eaa7524da386555090 100644 --- a/lib/hawat/blueprints/status/__init__.py +++ b/lib/hawat/blueprints/status/__init__.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ @@ -27,21 +27,18 @@ Provided endpoints """ - __author__ = "Jan Mach <jan.mach@cesnet.cz>" __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" - import flask from flask_babel import gettext, lazy_gettext import pyzenkit.jsonconf import mentat.system -import vial.acl -from vial.app import VialBlueprint -from vial.view import SimpleView -from vial.view.mixin import HTMLMixin - +import hawat.acl +from hawat.base import HawatBlueprint +from hawat.view import SimpleView +from hawat.view.mixin import HTMLMixin BLUEPRINT_NAME = 'status' """Name of the blueprint as module global constant.""" @@ -53,7 +50,7 @@ class ViewView(HTMLMixin, SimpleView): """ authentication = True - authorization = [vial.acl.PERMISSION_ADMIN] + authorization = [hawat.acl.PERMISSION_ADMIN] @classmethod def get_view_name(cls): @@ -86,7 +83,7 @@ class ViewView(HTMLMixin, SimpleView): self.response_context['mentat_runlogs'] = mentat.system.analyze_runlog_files( flask.current_app.config['MENTAT_PATHS']['path_run'], - limit = 20 + limit=20 ) self.response_context['mentat_status'] = mentat.system.system_status( @@ -102,16 +99,16 @@ class ViewView(HTMLMixin, SimpleView): self.flash( gettext("Error when displaying system status, encountered file not found error.") ) - #flask.current_app.log_exception_with_label( + # flask.current_app.log_exception_with_label( # traceback.TracebackException(*sys.exc_info()), # "Error when displaying system performance" - #) + # ) -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- -class StatusBlueprint(VialBlueprint): +class StatusBlueprint(HawatBlueprint): """Pluggable module - Mentat system status (*status*).""" @classmethod @@ -122,27 +119,27 @@ class StatusBlueprint(VialBlueprint): app.menu_main.add_entry( 'view', 'admin.{}'.format(BLUEPRINT_NAME), - position = 20, - group = lazy_gettext('Status overview'), - view = ViewView + position=20, + group=lazy_gettext('Status overview'), + view=ViewView ) -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- def get_blueprint(): """ - Mandatory interface for :py:mod:`vial.Vial` and factory function. This function - must return a valid instance of :py:class:`vial.app.VialBlueprint` or + Mandatory interface for :py:mod:`hawat.Hawat` and factory function. This function + must return a valid instance of :py:class:`hawat.app.HawatBlueprint` or :py:class:`flask.Blueprint`. """ hbp = StatusBlueprint( BLUEPRINT_NAME, __name__, - template_folder = 'templates', - url_prefix = '/{}'.format(BLUEPRINT_NAME) + template_folder='templates', + url_prefix='/{}'.format(BLUEPRINT_NAME) ) hbp.register_view_class(ViewView, '/view') diff --git a/lib/hawat/blueprints/status/templates/status/view.html b/lib/hawat/blueprints/status/templates/status/view.html index b1cdce0fae44bb787e18673a3c78effbc63b35d4..9d885b74e66eb437e4498dad0bcc86bd1aa6ad53 100644 --- a/lib/hawat/blueprints/status/templates/status/view.html +++ b/lib/hawat/blueprints/status/templates/status/view.html @@ -8,7 +8,7 @@ <li><a href="{{ url_for('home.index') }}">{{ _('Home') }}</a></li> <li class="active">{{ _('System status') }}</li> </ol> - <h2>{{ vial_current_view.get_view_title() }}</h2> + <h2>{{ hawat_current_view.get_view_title() }}</h2> <hr> {%- if mentat_status and mentat_modules and mentat_cronjobs and mentat_runlogs %} <p> diff --git a/lib/hawat/blueprints/status/test/__init__.py b/lib/hawat/blueprints/status/test/__init__.py index 13004f510c987d05eb5a76328f99d227b6d35e96..a25b9688eb796467f5ea26f18cf2723f6741b034 100644 --- a/lib/hawat/blueprints/status/test/__init__.py +++ b/lib/hawat/blueprints/status/test/__init__.py @@ -1,29 +1,28 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ Unit tests for :py:mod:`hawat.blueprints.status`. """ - import unittest -import vial.const -import vial.test -import vial.db +import hawat.const +import hawat.test +import hawat.db -from vial.test import VialTestCase +from hawat.test import HawatTestCase from hawat.test.runner import TestRunnerMixin -class StatusViewTestCase(TestRunnerMixin, VialTestCase): +class StatusViewTestCase(TestRunnerMixin, HawatTestCase): """Class for testing ``status.view`` endpoint.""" def _attempt_fail_redirect(self): @@ -34,7 +33,7 @@ class StatusViewTestCase(TestRunnerMixin, VialTestCase): b'Redirecting...', b'login?next=' ], - follow_redirects = False + follow_redirects=False ) def _attempt_fail(self): @@ -56,28 +55,28 @@ class StatusViewTestCase(TestRunnerMixin, VialTestCase): """Test access as anonymous user.""" self._attempt_fail_redirect() - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_USER) def test_02_as_user(self): """Test access as user ``user``.""" self._attempt_fail() - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_DEVELOPER) def test_03_as_developer(self): """Test access as user ``developer``.""" self._attempt_fail() - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_MAINTAINER) def test_04_as_maintainer(self): """Test access as user ``maintainer``.""" self._attempt_fail() - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_ADMIN) def test_05_as_admin(self): """Test access as user ``admin``.""" self._attempt_succeed() -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- if __name__ == "__main__": diff --git a/lib/hawat/blueprints/timeline/__init__.py b/lib/hawat/blueprints/timeline/__init__.py index 5bf1cb6ee7ea40cb85df1c1724f2e85e5a379d07..d3c0c5adbf93cc1afaf3ce0e31f90787fda06eac 100644 --- a/lib/hawat/blueprints/timeline/__init__.py +++ b/lib/hawat/blueprints/timeline/__init__.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ @@ -14,11 +14,9 @@ related to `IDEA <https://idea.cesnet.cz/en/index>`__ event timeline based visualisations. """ - __author__ = "Jan Mach <jan.mach@cesnet.cz>" __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" - import copy import datetime import pytz @@ -32,37 +30,38 @@ from mentat.const import tr_ from mentat.services.eventstorage import QTYPE_TIMELINE import hawat.events -import vial.const -import vial.acl -from vial.app import VialBlueprint -from vial.view import BaseSearchView, CustomSearchView -from vial.view.mixin import HTMLMixin, AJAXMixin -from vial.utils import URLParamsBuilder +import hawat.const +import hawat.acl +import hawat.menu +import hawat.forms +from hawat.base import HawatBlueprint +from hawat.view import BaseSearchView, CustomSearchView +from hawat.view.mixin import HTMLMixin, AJAXMixin +from hawat.utils import URLParamsBuilder from hawat.base import PsycopgMixin from hawat.blueprints.timeline.forms import SimpleTimelineSearchForm - BLUEPRINT_NAME = 'timeline' """Name of the blueprint as module global constant.""" AGGREGATIONS = ( - (mentat.stats.idea.ST_SKEY_CATEGORIES, {}, {"aggr_set": "category"}), - (mentat.stats.idea.ST_SKEY_IPS, {}, {"aggr_set": "source_ip"}), - #('', {"aggr_set": "target_ip"}), - (mentat.stats.idea.ST_SKEY_SRCPORTS, {}, {"aggr_set": "source_port"}), - (mentat.stats.idea.ST_SKEY_TGTPORTS, {}, {"aggr_set": "target_port"}), - (mentat.stats.idea.ST_SKEY_SRCTYPES, {}, {"aggr_set": "source_type"}), - (mentat.stats.idea.ST_SKEY_TGTTYPES, {}, {"aggr_set": "target_type"}), - (mentat.stats.idea.ST_SKEY_PROTOCOLS, {}, {"aggr_set": "protocol"}), - (mentat.stats.idea.ST_SKEY_DETECTORS, {}, {"aggr_set": "node_name"}), + (mentat.stats.idea.ST_SKEY_CATEGORIES, {}, {"aggr_set": "category"}), + (mentat.stats.idea.ST_SKEY_IPS, {}, {"aggr_set": "source_ip"}), + # ('', {"aggr_set": "target_ip"}), + (mentat.stats.idea.ST_SKEY_SRCPORTS, {}, {"aggr_set": "source_port"}), + (mentat.stats.idea.ST_SKEY_TGTPORTS, {}, {"aggr_set": "target_port"}), + (mentat.stats.idea.ST_SKEY_SRCTYPES, {}, {"aggr_set": "source_type"}), + (mentat.stats.idea.ST_SKEY_TGTTYPES, {}, {"aggr_set": "target_type"}), + (mentat.stats.idea.ST_SKEY_PROTOCOLS, {}, {"aggr_set": "protocol"}), + (mentat.stats.idea.ST_SKEY_DETECTORS, {}, {"aggr_set": "node_name"}), (mentat.stats.idea.ST_SKEY_DETECTORTPS, {}, {"aggr_set": "node_type"}), - (mentat.stats.idea.ST_SKEY_ABUSES, {}, {"aggr_set": "resolvedabuses"}), - (mentat.stats.idea.ST_SKEY_CLASSES, {}, {"aggr_set": "eventclass"}), - (mentat.stats.idea.ST_SKEY_SEVERITIES, {}, {"aggr_set": "eventseverity"}), + (mentat.stats.idea.ST_SKEY_ABUSES, {}, {"aggr_set": "resolvedabuses"}), + (mentat.stats.idea.ST_SKEY_CLASSES, {}, {"aggr_set": "eventclass"}), + (mentat.stats.idea.ST_SKEY_SEVERITIES, {}, {"aggr_set": "eventseverity"}), ) -def _get_search_form(request_args = None): +def _get_search_form(request_args=None): choices = hawat.events.get_event_form_choices() aggrchc = list( zip( @@ -73,24 +72,25 @@ def _get_search_form(request_args = None): form = SimpleTimelineSearchForm( request_args, - meta = {'csrf': False}, - choices_source_types = choices['source_types'], - choices_target_types = choices['target_types'], - choices_host_types = choices['host_types'], - choices_detectors = choices['detectors'], - choices_detector_types = choices['detector_types'], - choices_categories = choices['categories'], - choices_severities = choices['severities'], - choices_classes = choices['classes'], - choices_protocols = choices['protocols'], - choices_inspection_errs = choices['inspection_errs'], - choices_aggregations = aggrchc + meta={'csrf': False}, + choices_source_types=choices['source_types'], + choices_target_types=choices['target_types'], + choices_host_types=choices['host_types'], + choices_detectors=choices['detectors'], + choices_detector_types=choices['detector_types'], + choices_categories=choices['categories'], + choices_severities=choices['severities'], + choices_classes=choices['classes'], + choices_protocols=choices['protocols'], + choices_inspection_errs=choices['inspection_errs'], + choices_aggregations=aggrchc ) # In case no time bounds were set adjust them manually. - if request_args and not ('dt_from' in request_args or 'dt_to' in request_args or 'st_from' in request_args or 'st_to' in request_args): - form.dt_from.process_data(vial.forms.default_dt_with_delta()) - form.dt_to.process_data(vial.forms.default_dt()) + if request_args and not ( + 'dt_from' in request_args or 'dt_to' in request_args or 'st_from' in request_args or 'st_to' in request_args): + form.dt_from.process_data(hawat.forms.default_dt_with_delta()) + form.dt_to.process_data(hawat.forms.default_dt()) return form @@ -122,7 +122,7 @@ class AbstractSearchView(PsycopgMixin, CustomSearchView): # pylint: disable=loc def do_before_search(self, form_data): # pylint: disable=locally-disabled,no-self-use,unused-argument self.response_context.update( - sqlqueries = [] + sqlqueries=[] ) form_data['groups'] = [item.name for item in form_data['groups']] @@ -131,11 +131,11 @@ class AbstractSearchView(PsycopgMixin, CustomSearchView): # pylint: disable=loc dt_from = form_data.get('dt_from', None) if dt_from: dt_from = dt_from.astimezone(pytz.utc) - dt_from = dt_from.replace(tzinfo = None) - dt_to = form_data.get('dt_to', None) + dt_from = dt_from.replace(tzinfo=None) + dt_to = form_data.get('dt_to', None) if dt_to: dt_to = dt_to.astimezone(pytz.utc) - dt_to = dt_to.replace(tzinfo = None) + dt_to = dt_to.replace(tzinfo=None) else: dt_to = datetime.datetime.utcnow() form_data['dt_from'] = dt_from @@ -161,40 +161,40 @@ class AbstractSearchView(PsycopgMixin, CustomSearchView): # pylint: disable=loc self.get_db().cursor.lastquery.decode('utf-8') ) self.response_context.update( - timeline_cfg = timeline_cfg + timeline_cfg=timeline_cfg ) # Put calculated parameters together with other search form parameters. form_data['dt_from'] = timeline_cfg['dt_from'] - form_data['dt_to'] = timeline_cfg['dt_to'] - form_data['step'] = timeline_cfg['step'] + form_data['dt_to'] = timeline_cfg['dt_to'] + form_data['step'] = timeline_cfg['step'] - def _search_events_aggr(self, form_args, qtype, aggr_name, enable_toplist = True): + def _search_events_aggr(self, form_args, qtype, aggr_name, enable_toplist=True): self.mark_time( '{}_{}'.format(qtype, aggr_name), 'begin', - tag = 'search', - label = 'Begin aggregation calculations "{}:{}"'.format( + tag='search', + label='Begin aggregation calculations "{}:{}"'.format( qtype, aggr_name ), - log = True + log=True ) search_result = self.get_db().search_events_aggr( form_args, - qtype = qtype, - dbtoplist = enable_toplist + qtype=qtype, + dbtoplist=enable_toplist ) self.mark_time( '{}_{}'.format(qtype, aggr_name), 'end', - tag = 'search', - label = 'Finished aggregation calculations "{}:{}" [yield {} row(s)]'.format( + tag='search', + label='Finished aggregation calculations "{}:{}" [yield {} row(s)]'.format( qtype, aggr_name, len(search_result) ), - log = True + log=True ) self.response_context['sqlqueries'].append( @@ -204,8 +204,8 @@ class AbstractSearchView(PsycopgMixin, CustomSearchView): # pylint: disable=loc def custom_search(self, form_args): self.response_context.update( - search_result = {}, # Raw database query results (rows). - aggregations = [] # Note all performed aggregations for further processing. + search_result={}, # Raw database query results (rows). + aggregations=[] # Note all performed aggregations for further processing. ) # Always perform total event count timeline calculations. @@ -232,10 +232,9 @@ class AbstractSearchView(PsycopgMixin, CustomSearchView): # pylint: disable=loc self._search_events_aggr(fargs, qtype, aggr_name) self.response_context['aggregations'].append(aggr_name) - def do_after_search(self): self.response_context.update( - statistics = { + statistics={ 'timeline_cfg': self.response_context['timeline_cfg'] } ) @@ -244,9 +243,9 @@ class AbstractSearchView(PsycopgMixin, CustomSearchView): # pylint: disable=loc self.mark_time( 'result_convert', 'begin', - tag = 'calculate', - label = 'Converting result from database rows to statistical dataset', - log = True + tag='calculate', + label='Converting result from database rows to statistical dataset', + log=True ) key = "{}:{}".format(QTYPE_TIMELINE, mentat.stats.idea.ST_SKEY_CNT_EVENTS) mentat.stats.idea.aggregate_dbstats_events( @@ -271,9 +270,9 @@ class AbstractSearchView(PsycopgMixin, CustomSearchView): # pylint: disable=loc self.mark_time( 'result_convert', 'end', - tag = 'calculate', - label = 'Done converting result from database rows to statistical dataset', - log = True + tag='calculate', + label='Done converting result from database rows to statistical dataset', + log=True ) # Calculate aggregation aggregations from timeline aggregations. @@ -281,9 +280,9 @@ class AbstractSearchView(PsycopgMixin, CustomSearchView): # pylint: disable=loc self.mark_time( 'result_calculate', 'begin', - tag = 'calculate', - label = 'Calculating aggregation aggregations from timeline aggregations', - log = True + tag='calculate', + label='Calculating aggregation aggregations from timeline aggregations', + log=True ) mentat.stats.idea.postcalculate_dbstats_events( aggr_name, @@ -298,18 +297,18 @@ class AbstractSearchView(PsycopgMixin, CustomSearchView): # pylint: disable=loc self.mark_time( 'result_calculate', 'end', - tag = 'calculate', - label = 'Done calculating aggregation aggregations from timeline aggregations', - log = True + tag='calculate', + label='Done calculating aggregation aggregations from timeline aggregations', + log=True ) # Truncate all datasets self.mark_time( 'result_truncate', 'begin', - tag = 'calculate', - label = 'Truncating datasets', - log = True + tag='calculate', + label='Truncating datasets', + log=True ) self.response_context['statistics'] = mentat.stats.idea.evaluate_dbstats_events( self.response_context['statistics'] @@ -317,19 +316,19 @@ class AbstractSearchView(PsycopgMixin, CustomSearchView): # pylint: disable=loc self.mark_time( 'result_truncate', 'end', - tag = 'calculate', - label = 'Done truncating datasets', - log = True + tag='calculate', + label='Done truncating datasets', + log=True ) self.response_context.update( - items_count = self.response_context['statistics'].get('cnt_events', 0) + items_count=self.response_context['statistics'].get('cnt_events', 0) ) self.response_context.pop('search_result', None) def do_before_response(self, **kwargs): self.response_context.update( - quicksearch_list = self.get_quicksearch_by_time() + quicksearch_list=self.get_quicksearch_by_time() ) @@ -342,16 +341,16 @@ class SearchView(HTMLMixin, AbstractSearchView): # pylint: disable=locally-disa @classmethod def get_breadcrumbs_menu(cls): - breadcrumbs_menu = vial.menu.Menu() + breadcrumbs_menu = hawat.menu.Menu() breadcrumbs_menu.add_entry( 'endpoint', 'home', - endpoint = flask.current_app.config['ENDPOINT_HOME'] + endpoint=flask.current_app.config['ENDPOINT_HOME'] ) breadcrumbs_menu.add_entry( 'endpoint', 'search', - endpoint = '{}.search'.format(cls.module_name) + endpoint='{}.search'.format(cls.module_name) ) return breadcrumbs_menu @@ -361,14 +360,14 @@ class APISearchView(AJAXMixin, AbstractSearchView): # pylint: disable=locally-d View responsible for querying `IDEA <https://idea.cesnet.cz/en/index>`__ event database and presenting the results in the form of JSON document. """ - methods = ['GET','POST'] + methods = ['GET', 'POST'] @classmethod def get_view_name(cls): return 'apisearch' -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- class AbstractLegacySearchView(PsycopgMixin, BaseSearchView): # pylint: disable=locally-disabled,abstract-method @@ -408,28 +407,28 @@ class AbstractLegacySearchView(PsycopgMixin, BaseSearchView): # pylint: disable dt_from = self.response_context['form_data'].get('dt_from', None) if dt_from: dt_from = dt_from.astimezone(pytz.utc) - dt_from = dt_from.replace(tzinfo = None) - dt_to = self.response_context['form_data'].get('dt_to', None) + dt_from = dt_from.replace(tzinfo=None) + dt_to = self.response_context['form_data'].get('dt_to', None) if dt_to: dt_to = dt_to.astimezone(pytz.utc) - dt_to = dt_to.replace(tzinfo = None) + dt_to = dt_to.replace(tzinfo=None) if not dt_from and items: dt_from = self.get_db().search_column_with('detecttime') if not dt_to and items: dt_to = datetime.datetime.utcnow() self.response_context.update( - statistics = mentat.stats.idea.evaluate_timeline_events( + statistics=mentat.stats.idea.evaluate_timeline_events( items, - dt_from = dt_from, - dt_to = dt_to, - max_count = flask.current_app.config['HAWAT_CHART_TIMELINE_MAXSTEPS'] + dt_from=dt_from, + dt_to=dt_to, + max_count=flask.current_app.config['HAWAT_CHART_TIMELINE_MAXSTEPS'] ) ) self.response_context.pop('items', None) def do_before_response(self, **kwargs): self.response_context.update( - quicksearch_list = self.get_quicksearch_by_time() + quicksearch_list=self.get_quicksearch_by_time() ) @staticmethod @@ -439,6 +438,7 @@ class AbstractLegacySearchView(PsycopgMixin, BaseSearchView): # pylint: disable """ return mentat.services.eventstorage.QTYPE_SELECT_GHOST + class APILegacySearchView(AJAXMixin, AbstractSearchView): # pylint: disable=locally-disabled,too-many-ancestors """ View responsible for querying `IDEA <https://idea.cesnet.cz/en/index>`__ @@ -446,17 +446,17 @@ class APILegacySearchView(AJAXMixin, AbstractSearchView): # pylint: disable=loc *Deprecated legacy implementation, kept only for the purposes of comparison. """ - methods = ['GET','POST'] + methods = ['GET', 'POST'] @classmethod def get_view_name(cls): return 'apilegacysearch' -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- -class TimelineBlueprint(VialBlueprint): +class TimelineBlueprint(HawatBlueprint): """Pluggable module - IDEA event timelines (*timeline*).""" @classmethod @@ -467,9 +467,9 @@ class TimelineBlueprint(VialBlueprint): app.menu_main.add_entry( 'view', BLUEPRINT_NAME, - position = 150, - view = SearchView, - resptitle = True + position=150, + view=SearchView, + resptitle=True ) # Register context actions provided by this module. @@ -477,28 +477,30 @@ class TimelineBlueprint(VialBlueprint): hawat.const.CSAG_ADDRESS, tr_('Search for source <strong>%(name)s</strong> on IDEA event timeline'), SearchView, - URLParamsBuilder({'submit': tr_('Search')}).add_rule('source_addrs', True).add_kwrule('dt_from', False, True).add_kwrule('dt_to', False, True) + URLParamsBuilder({'submit': tr_('Search')}).add_rule('source_addrs', True).add_kwrule('dt_from', False, + True).add_kwrule( + 'dt_to', False, True) ) -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- def get_blueprint(): """ - Mandatory interface for :py:mod:`vial.Vial` and factory function. This function - must return a valid instance of :py:class:`vial.app.VialBlueprint` or + Mandatory interface for :py:mod:`hawat.Hawat` and factory function. This function + must return a valid instance of :py:class:`hawat.app.HawatBlueprint` or :py:class:`flask.Blueprint`. """ hbp = TimelineBlueprint( BLUEPRINT_NAME, __name__, - template_folder = 'templates' + template_folder='templates' ) - hbp.register_view_class(SearchView, '/{}/search'.format(BLUEPRINT_NAME)) - hbp.register_view_class(APISearchView, '/api/{}/search'.format(BLUEPRINT_NAME)) + hbp.register_view_class(SearchView, '/{}/search'.format(BLUEPRINT_NAME)) + hbp.register_view_class(APISearchView, '/api/{}/search'.format(BLUEPRINT_NAME)) hbp.register_view_class(APILegacySearchView, '/api/{}/legacysearch'.format(BLUEPRINT_NAME)) return hbp diff --git a/lib/hawat/blueprints/timeline/forms.py b/lib/hawat/blueprints/timeline/forms.py index 146573a77f2b03e43284eed5a3b55939d3b55b10..52415099ded849d3ae422bdb1766699af5ed3d2b 100644 --- a/lib/hawat/blueprints/timeline/forms.py +++ b/lib/hawat/blueprints/timeline/forms.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ @@ -13,11 +13,9 @@ This module contains custom `IDEA <https://idea.cesnet.cz/en/index>`__ event timeline search form for Hawat application. """ - __author__ = "Jan Mach <jan.mach@cesnet.cz>" __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" - import wtforms from wtforms.ext.sqlalchemy.fields import QuerySelectMultipleField @@ -25,9 +23,9 @@ import flask_wtf from flask_babel import lazy_gettext from mentat.datatype.sqldb import GroupModel -import vial.const -import vial.forms -import vial.db +import hawat.const +import hawat.forms +import hawat.db import hawat.const @@ -35,284 +33,306 @@ def get_available_groups(): """ Query the database for list of all available groups. """ - return vial.db.db_query(GroupModel).order_by(GroupModel.name).all() + return hawat.db.db_query(GroupModel).order_by(GroupModel.name).all() class SimpleTimelineSearchForm(flask_wtf.FlaskForm): """ Class representing simple event timeline search form. """ - dt_from = vial.forms.SmartDateTimeField( + dt_from = hawat.forms.SmartDateTimeField( lazy_gettext('Detection time from:'), - validators = [ + validators=[ wtforms.validators.Optional() ], - description = lazy_gettext('Lower time boundary for event detection time as provided by event detector. Timestamp is expected to be in the format <code>YYYY-MM-DD hh:mm:ss</code> and in the timezone according to the user`s preferences. Event detectors are usually outside of the control of Mentat system administrators and may sometimes emit events with invalid detection times, for example timestamps in the future.'), - default = lambda: vial.forms.default_dt_with_delta(hawat.const.DEFAULT_RESULT_TIMEDELTA) + description=lazy_gettext( + 'Lower time boundary for event detection time as provided by event detector. Timestamp is expected to be in the format <code>YYYY-MM-DD hh:mm:ss</code> and in the timezone according to the user`s preferences. Event detectors are usually outside of the control of Mentat system administrators and may sometimes emit events with invalid detection times, for example timestamps in the future.'), + default=lambda: hawat.forms.default_dt_with_delta(hawat.const.DEFAULT_RESULT_TIMEDELTA) ) - dt_to = vial.forms.SmartDateTimeField( + dt_to = hawat.forms.SmartDateTimeField( lazy_gettext('Detection time to:'), - validators = [ + validators=[ wtforms.validators.Optional() ], - description = lazy_gettext('Upper time boundary for event detection time as provided by event detector. Timestamp is expected to be in the format <code>YYYY-MM-DD hh:mm:ss</code> and in the timezone according to the user`s preferences. Event detectors are usually outside of the control of Mentat system administrators and may sometimes emit events with invalid detection times, for example timestamps in the future.'), - default = vial.forms.default_dt + description=lazy_gettext( + 'Upper time boundary for event detection time as provided by event detector. Timestamp is expected to be in the format <code>YYYY-MM-DD hh:mm:ss</code> and in the timezone according to the user`s preferences. Event detectors are usually outside of the control of Mentat system administrators and may sometimes emit events with invalid detection times, for example timestamps in the future.'), + default=hawat.forms.default_dt ) - st_from = vial.forms.SmartDateTimeField( + st_from = hawat.forms.SmartDateTimeField( lazy_gettext('Storage time from:'), - validators = [ + validators=[ wtforms.validators.Optional() ], - description = lazy_gettext('Lower time boundary for event storage time. Timestamp is expected to be in the format <code>YYYY-MM-DD hh:mm:ss</code> and in the timezone according to the user`s preferences. Event storage time is provided by Mentat system itself. It is a timestamp of the exact moment the event was stored into the database.') + description=lazy_gettext( + 'Lower time boundary for event storage time. Timestamp is expected to be in the format <code>YYYY-MM-DD hh:mm:ss</code> and in the timezone according to the user`s preferences. Event storage time is provided by Mentat system itself. It is a timestamp of the exact moment the event was stored into the database.') ) - st_to = vial.forms.SmartDateTimeField( + st_to = hawat.forms.SmartDateTimeField( lazy_gettext('Storage time to:'), - validators = [ + validators=[ wtforms.validators.Optional() ], - description = lazy_gettext('Upper time boundary for event storage time. Timestamp is expected to be in the format <code>YYYY-MM-DD hh:mm:ss</code> and in the timezone according to the user`s preferences. Event storage time is provided by Mentat system itself. It is a timestamp of the exact moment the event was stored into the database.') + description=lazy_gettext( + 'Upper time boundary for event storage time. Timestamp is expected to be in the format <code>YYYY-MM-DD hh:mm:ss</code> and in the timezone according to the user`s preferences. Event storage time is provided by Mentat system itself. It is a timestamp of the exact moment the event was stored into the database.') ) - source_addrs = vial.forms.CommaListField( + source_addrs = hawat.forms.CommaListField( lazy_gettext('Source addresses:'), - validators = [ + validators=[ wtforms.validators.Optional(), - vial.forms.check_network_record_list + hawat.forms.check_network_record_list ], - widget = wtforms.widgets.TextArea(), - description = lazy_gettext('Comma separated list of event source IP4/6 addresses, ranges or networks. In this context a source does not necessarily mean a source of the connection, but rather a source of the problem as reported by a detector. Any additional whitespace is ignored and may be used for better readability.') + widget=wtforms.widgets.TextArea(), + description=lazy_gettext( + 'Comma separated list of event source IP4/6 addresses, ranges or networks. In this context a source does not necessarily mean a source of the connection, but rather a source of the problem as reported by a detector. Any additional whitespace is ignored and may be used for better readability.') ) - target_addrs = vial.forms.CommaListField( + target_addrs = hawat.forms.CommaListField( lazy_gettext('Target addresses:'), - validators = [ + validators=[ wtforms.validators.Optional(), - vial.forms.check_network_record_list + hawat.forms.check_network_record_list ], - widget = wtforms.widgets.TextArea(), - description = lazy_gettext('Comma separated list of event target IP4/6 addresses, ranges or networks. In this context a target does not necessarily mean a target of the connection, but rather a victim of the problem as reported by a detector. Any additional whitespace is ignored and may be used for better readability.') + widget=wtforms.widgets.TextArea(), + description=lazy_gettext( + 'Comma separated list of event target IP4/6 addresses, ranges or networks. In this context a target does not necessarily mean a target of the connection, but rather a victim of the problem as reported by a detector. Any additional whitespace is ignored and may be used for better readability.') ) - host_addrs = vial.forms.CommaListField( + host_addrs = hawat.forms.CommaListField( lazy_gettext('Host addresses:'), - validators = [ + validators=[ wtforms.validators.Optional(), - vial.forms.check_network_record_list + hawat.forms.check_network_record_list ], - widget = wtforms.widgets.TextArea(), - description = lazy_gettext('Comma separated list of event source or target IP4/6 addresses, ranges or networks. Any additional whitespace is ignored and may be used for better readability.') + widget=wtforms.widgets.TextArea(), + description=lazy_gettext( + 'Comma separated list of event source or target IP4/6 addresses, ranges or networks. Any additional whitespace is ignored and may be used for better readability.') ) - source_ports = vial.forms.CommaListField( + source_ports = hawat.forms.CommaListField( lazy_gettext('Source ports:'), - validators = [ + validators=[ wtforms.validators.Optional(), - vial.forms.check_port_list + hawat.forms.check_port_list ], - description = lazy_gettext('Comma separated list of source ports as integers. In this context a source does not necessarily mean a source of the connection, but rather a source of the problem as reported by a detector. Any additional whitespace is ignored and may be used for better readability.') + description=lazy_gettext( + 'Comma separated list of source ports as integers. In this context a source does not necessarily mean a source of the connection, but rather a source of the problem as reported by a detector. Any additional whitespace is ignored and may be used for better readability.') ) - target_ports = vial.forms.CommaListField( + target_ports = hawat.forms.CommaListField( lazy_gettext('Target ports:'), - validators = [ + validators=[ wtforms.validators.Optional(), - vial.forms.check_port_list + hawat.forms.check_port_list ], - description = lazy_gettext('Comma separated list of target ports as integers. In this context a target does not necessarily mean a target of the connection, but rather a victim of the problem as reported by a detector. Any additional whitespace is ignored and may be used for better readability.') + description=lazy_gettext( + 'Comma separated list of target ports as integers. In this context a target does not necessarily mean a target of the connection, but rather a victim of the problem as reported by a detector. Any additional whitespace is ignored and may be used for better readability.') ) - host_ports = vial.forms.CommaListField( + host_ports = hawat.forms.CommaListField( lazy_gettext('Host ports:'), - validators = [ + validators=[ wtforms.validators.Optional(), - vial.forms.check_port_list + hawat.forms.check_port_list ], - description = lazy_gettext('Comma separated list of source or target ports as integers. Any additional whitespace is ignored and may be used for better readability.') + description=lazy_gettext( + 'Comma separated list of source or target ports as integers. Any additional whitespace is ignored and may be used for better readability.') ) source_types = wtforms.SelectMultipleField( lazy_gettext('Source types:'), - validators = [ + validators=[ wtforms.validators.Optional() ], - choices = [], - filters = [lambda x: x or []], - description = lazy_gettext('Specification of event source type. Each event source may be optionally assigned one or more labels to better categorize type of a source.') + choices=[], + filters=[lambda x: x or []], + description=lazy_gettext( + 'Specification of event source type. Each event source may be optionally assigned one or more labels to better categorize type of a source.') ) target_types = wtforms.SelectMultipleField( lazy_gettext('Target types:'), - validators = [ + validators=[ wtforms.validators.Optional() ], - choices = [], - filters = [lambda x: x or []], - description = lazy_gettext('Specification of event target type. Each event target may be optionally assigned one or more labels to better categorize type of a target.') + choices=[], + filters=[lambda x: x or []], + description=lazy_gettext( + 'Specification of event target type. Each event target may be optionally assigned one or more labels to better categorize type of a target.') ) host_types = wtforms.SelectMultipleField( lazy_gettext('Host types:'), - validators = [ + validators=[ wtforms.validators.Optional() ], - choices = [], - filters = [lambda x: x or []], - description = lazy_gettext('Specification of event source or target type. Each event source or target may be optionally assigned one or more labels to better categorize type of a source or target.') + choices=[], + filters=[lambda x: x or []], + description=lazy_gettext( + 'Specification of event source or target type. Each event source or target may be optionally assigned one or more labels to better categorize type of a source or target.') ) detectors = wtforms.SelectMultipleField( lazy_gettext('Detectors:'), - validators = [ + validators=[ wtforms.validators.Optional(), ], - choices = [ + choices=[ ('__EMPTY__', lazy_gettext('<< without value >>')), ('__ANY__', lazy_gettext('<< any value >>')) ], - filters = [lambda x: x or []], - description = lazy_gettext('Name of the detector that detected the event.') + filters=[lambda x: x or []], + description=lazy_gettext('Name of the detector that detected the event.') ) not_detectors = wtforms.BooleanField( lazy_gettext('Negate detector selection:'), - validators = [ + validators=[ wtforms.validators.Optional(), ] ) detector_types = wtforms.SelectMultipleField( lazy_gettext('Detector types:'), - validators = [ + validators=[ wtforms.validators.Optional() ], - choices = [ + choices=[ ('__EMPTY__', lazy_gettext('<< without value >>')), ('__ANY__', lazy_gettext('<< any value >>')) ], - filters = [lambda x: x or []], - description = lazy_gettext('Specification of event detector type. Each event detector may be optionally assigned one or more labels to better categorize that detector.') + filters=[lambda x: x or []], + description=lazy_gettext( + 'Specification of event detector type. Each event detector may be optionally assigned one or more labels to better categorize that detector.') ) not_detector_types = wtforms.BooleanField( lazy_gettext('Negate detector_type selection:'), - validators = [ + validators=[ wtforms.validators.Optional(), ] ) categories = wtforms.SelectMultipleField( lazy_gettext('Categories:'), - validators = [ + validators=[ wtforms.validators.Optional(), ], - choices = [ + choices=[ ('__EMPTY__', lazy_gettext('<< without value >>')), ('__ANY__', lazy_gettext('<< any value >>')) ], - filters = [lambda x: x or []], - description = lazy_gettext('Specification of event category. Each event may be optionally assigned one or more labels to better categorize that event, for example as "Recon.Scanning", "Abusive.Spam", "Test" etc.') + filters=[lambda x: x or []], + description=lazy_gettext( + 'Specification of event category. Each event may be optionally assigned one or more labels to better categorize that event, for example as "Recon.Scanning", "Abusive.Spam", "Test" etc.') ) not_categories = wtforms.BooleanField( lazy_gettext('Negate category selection:'), - validators = [ + validators=[ wtforms.validators.Optional(), ] ) severities = wtforms.SelectMultipleField( lazy_gettext('Severities:'), - validators = [ + validators=[ wtforms.validators.Optional(), ], - choices = [ + choices=[ ('__EMPTY__', lazy_gettext('<< without value >>')), ('__ANY__', lazy_gettext('<< any value >>')) ], - filters = [lambda x: x or []], - description = lazy_gettext('Specification of event severity. Each event may be optionally assigned one severity level, which can be then use during incident handling workflows to prioritize events.') + filters=[lambda x: x or []], + description=lazy_gettext( + 'Specification of event severity. Each event may be optionally assigned one severity level, which can be then use during incident handling workflows to prioritize events.') ) not_severities = wtforms.BooleanField( lazy_gettext('Negate severity selection:'), - validators = [ + validators=[ wtforms.validators.Optional(), ] ) classes = wtforms.SelectMultipleField( lazy_gettext('Classes:'), - validators = [ + validators=[ wtforms.validators.Optional(), ], - choices = [ + choices=[ ('__EMPTY__', lazy_gettext('<< without value >>')), ('__ANY__', lazy_gettext('<< any value >>')) ], - filters = [lambda x: x or []], - description = lazy_gettext('Specification of event class. Each event may be optionally assigned one class to better describe the event and group all similar events together for better processing. Event classification in internal feature of Mentat system for better event management.') + filters=[lambda x: x or []], + description=lazy_gettext( + 'Specification of event class. Each event may be optionally assigned one class to better describe the event and group all similar events together for better processing. Event classification in internal feature of Mentat system for better event management.') ) not_classess = wtforms.BooleanField( lazy_gettext('Negate class selection:'), - validators = [ + validators=[ wtforms.validators.Optional(), ] ) description = wtforms.StringField( lazy_gettext('Description:'), - validators = [ + validators=[ wtforms.validators.Optional() ], - description = lazy_gettext('Specification of event description. Each event may be optionally assigned short descriptive string.') + description=lazy_gettext( + 'Specification of event description. Each event may be optionally assigned short descriptive string.') ) protocols = wtforms.SelectMultipleField( lazy_gettext('Protocols:'), - validators = [ + validators=[ wtforms.validators.Optional(), ], - choices = [ + choices=[ ('__EMPTY__', lazy_gettext('<< without value >>')), ('__ANY__', lazy_gettext('<< any value >>')) ], - filters = [lambda x: x or []], - description = lazy_gettext('Specification of one or more communication protocols involved in the event.') + filters=[lambda x: x or []], + description=lazy_gettext('Specification of one or more communication protocols involved in the event.') ) not_protocols = wtforms.BooleanField( lazy_gettext('Negate protocol selection:'), - validators = [ + validators=[ wtforms.validators.Optional(), ] ) groups = QuerySelectMultipleField( lazy_gettext('Abuse group:'), - query_factory = get_available_groups, - allow_blank = False, - get_pk = lambda item: item.name, - description = lazy_gettext('Specification of the abuse group to whose constituency this event belongs based on one of the event source addresses.') + query_factory=get_available_groups, + allow_blank=False, + get_pk=lambda item: item.name, + description=lazy_gettext( + 'Specification of the abuse group to whose constituency this event belongs based on one of the event source addresses.') ) not_groups = wtforms.BooleanField( lazy_gettext('Negate group selection:'), - validators = [ + validators=[ wtforms.validators.Optional(), ] ) inspection_errs = wtforms.SelectMultipleField( lazy_gettext('Inspection errors:'), - validators = [ + validators=[ wtforms.validators.Optional(), ], - choices = [ + choices=[ ('__EMPTY__', lazy_gettext('<< without value >>')), ('__ANY__', lazy_gettext('<< any value >>')) ], - filters = [lambda x: x or []], - description = lazy_gettext('Specification of possible event errors detected during event inspection by real-time event processing inspection daemon.') + filters=[lambda x: x or []], + description=lazy_gettext( + 'Specification of possible event errors detected during event inspection by real-time event processing inspection daemon.') ) not_inspection_errs = wtforms.BooleanField( lazy_gettext('Negate inspection error selection:'), - validators = [ + validators=[ wtforms.validators.Optional(), ] ) aggregations = wtforms.SelectMultipleField( lazy_gettext('Restrict only to selected aggregations:'), - validators = [ + validators=[ wtforms.validators.Optional() ], - filters = [lambda x: x or []], - description = lazy_gettext('Choose only which aggregation calculations to perform. When left empty all calculations will be performed.') + filters=[lambda x: x or []], + description=lazy_gettext( + 'Choose only which aggregation calculations to perform. When left empty all calculations will be performed.') ) limit = wtforms.IntegerField( lazy_gettext('Toplist limit:'), - validators = [ + validators=[ wtforms.validators.Optional(), - wtforms.validators.NumberRange(min = 1, max = 1000) + wtforms.validators.NumberRange(min=1, max=1000) ], - default = 100, - description = lazy_gettext('Perform toplisting to given limit for certain calculations like IP addresses and ports.') + default=100, + description=lazy_gettext( + 'Perform toplisting to given limit for certain calculations like IP addresses and ports.') ) submit = wtforms.SubmitField( @@ -324,14 +344,14 @@ class SimpleTimelineSearchForm(flask_wtf.FlaskForm): self.source_types.choices = kwargs['choices_source_types'] self.target_types.choices = kwargs['choices_target_types'] - self.host_types.choices = kwargs['choices_host_types'] + self.host_types.choices = kwargs['choices_host_types'] - self.detectors.choices[2:] = kwargs['choices_detectors'] - self.detector_types.choices[2:] = kwargs['choices_detector_types'] - self.categories.choices[2:] = kwargs['choices_categories'] - self.severities.choices[2:] = kwargs['choices_severities'] - self.classes.choices[2:] = kwargs['choices_classes'] - self.protocols.choices[2:] = kwargs['choices_protocols'] + self.detectors.choices[2:] = kwargs['choices_detectors'] + self.detector_types.choices[2:] = kwargs['choices_detector_types'] + self.categories.choices[2:] = kwargs['choices_categories'] + self.severities.choices[2:] = kwargs['choices_severities'] + self.classes.choices[2:] = kwargs['choices_classes'] + self.protocols.choices[2:] = kwargs['choices_protocols'] self.inspection_errs.choices[2:] = kwargs['choices_inspection_errs'] self.aggregations.choices = kwargs['choices_aggregations'] @@ -345,6 +365,10 @@ class SimpleTimelineSearchForm(flask_wtf.FlaskForm): :return: ``True``, if the field can contain multiple values, ``False`` otherwise. :rtype: bool """ - if field_name in ('source_addrs', 'target_addrs', 'host_addrs', 'source_ports', 'target_ports', 'host_ports', 'source_types', 'target_types', 'host_types', 'detectors', 'detector_types', 'categories', 'severities', 'classess', 'protocols', 'groups', 'inspection_errs', 'aggregations'): + if field_name in ( + 'source_addrs', 'target_addrs', 'host_addrs', 'source_ports', 'target_ports', 'host_ports', 'source_types', + 'target_types', 'host_types', 'detectors', 'detector_types', 'categories', 'severities', 'classess', + 'protocols', 'groups', 'inspection_errs', 'aggregations' + ): return True return False diff --git a/lib/hawat/blueprints/timeline/test/__init__.py b/lib/hawat/blueprints/timeline/test/__init__.py index e4ad642390d115a2ea4b6f2124274ff7c553c472..0d85c163ba6f1f07ba33e7a0a29387528b3521b5 100644 --- a/lib/hawat/blueprints/timeline/test/__init__.py +++ b/lib/hawat/blueprints/timeline/test/__init__.py @@ -1,28 +1,27 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ Unit tests for :py:mod:`hawat.blueprints.timeline`. """ - import unittest -import vial.const -import vial.test -import vial.db -from vial.test import VialTestCase +import hawat.const +import hawat.test +import hawat.db +from hawat.test import HawatTestCase from hawat.test.runner import TestRunnerMixin -class SearchTestCase(TestRunnerMixin, VialTestCase): +class SearchTestCase(TestRunnerMixin, HawatTestCase): """ Class for testing ``timeline.search`` endpoint. """ @@ -35,7 +34,7 @@ class SearchTestCase(TestRunnerMixin, VialTestCase): b'Redirecting...', b'login?next=' ], - follow_redirects = False + follow_redirects=False ) def _attempt_succeed(self): @@ -51,28 +50,28 @@ class SearchTestCase(TestRunnerMixin, VialTestCase): """Test access as anonymous user.""" self._attempt_fail_redirect() - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_USER) def test_02_as_user(self): """Test access as user ``user``.""" self._attempt_succeed() - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_DEVELOPER) def test_03_as_developer(self): """Test access as user ``developer``.""" self._attempt_succeed() - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_MAINTAINER) def test_04_as_maintainer(self): """Test access as user ``maintainer``.""" self._attempt_succeed() - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_ADMIN) def test_05_as_admin(self): """Test access as user ``admin``.""" self._attempt_succeed() -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- if __name__ == "__main__": diff --git a/lib/hawat/blueprints/users/__init__.py b/lib/hawat/blueprints/users/__init__.py index ab21b608c3211b5144d8571bfe24f7b707cdf416..e3e8fd96a2f573598c9625c2e28ff204006ce6f1 100644 --- a/lib/hawat/blueprints/users/__init__.py +++ b/lib/hawat/blueprints/users/__init__.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ @@ -24,26 +24,438 @@ related to user account management. These features include: * rejecting group membership requests """ - __author__ = "Jan Mach <jan.mach@cesnet.cz>" __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" - +import markupsafe import flask import flask_login -from flask_babel import lazy_gettext +import flask_principal +import flask_mail +from flask_babel import gettext, lazy_gettext, force_locale +from sqlalchemy import or_ + +import hawat.const +import hawat.acl +import hawat.menu +from hawat.base import HawatBlueprint +from hawat.view import ItemListView, ItemShowView, ItemCreateView, ItemUpdateView, ItemDeleteView, ItemEnableView, \ + ItemDisableView, ItemObjectRelationView +from hawat.view.mixin import HTMLMixin, SQLAlchemyMixin +from hawat.blueprints.users.forms import CreateUserAccountForm, UpdateUserAccountForm, AdminUpdateUserAccountForm, \ + UserSearchForm + +BLUEPRINT_NAME = 'users' +"""Name of the blueprint as module global constant.""" + + +class ListView(HTMLMixin, SQLAlchemyMixin, ItemListView): + """ + General user account listing. + """ + methods = ['GET'] + + authentication = True + + authorization = [hawat.acl.PERMISSION_POWER] + + @classmethod + def get_view_title(cls, **kwargs): + return lazy_gettext('User management') + + @property + def dbmodel(self): + return self.get_model(hawat.const.MODEL_USER) + + @classmethod + def get_action_menu(cls): + action_menu = hawat.menu.Menu() + action_menu.add_entry( + 'endpoint', + 'create', + endpoint='users.create', + resptitle=True + ) + return action_menu + + @classmethod + def get_context_action_menu(cls): + action_menu = hawat.menu.Menu() + action_menu.add_entry( + 'endpoint', + 'show', + endpoint='users.show', + hidetitle=True + ) + action_menu.add_entry( + 'endpoint', + 'update', + endpoint='users.update', + hidetitle=True + ) + action_menu.add_entry( + 'endpoint', + 'disable', + endpoint='users.disable', + hidetitle=True + ) + action_menu.add_entry( + 'endpoint', + 'enable', + endpoint='users.enable', + hidetitle=True + ) + action_menu.add_entry( + 'endpoint', + 'delete', + endpoint='users.delete', + hidetitle=True + ) + return action_menu + + @staticmethod + def get_search_form(request_args): + """ + Must return instance of :py:mod:`flask_wtf.FlaskForm` appropriate for + searching given type of items. + """ + roles = list(zip(flask.current_app.config['ROLES'], flask.current_app.config['ROLES'])) + return UserSearchForm( + request_args, + meta={'csrf': False}, + choices_roles=roles + ) + + @staticmethod + def build_query(query, model, form_args): + # Adjust query based on text search string. + if 'search' in form_args and form_args['search']: + query = query \ + .filter( + or_( + model.login.like('%{}%'.format(form_args['search'])), + model.fullname.like('%{}%'.format(form_args['search'])), + model.email.like('%{}%'.format(form_args['search'])), + ) + ) + # Adjust query based on lower time boundary selection. + if 'dt_from' in form_args and form_args['dt_from']: + query = query.filter(model.createtime >= form_args['dt_from']) + # Adjust query based on upper time boundary selection. + if 'dt_to' in form_args and form_args['dt_to']: + query = query.filter(model.createtime <= form_args['dt_to']) + # Adjust query based on user state selection. + if 'state' in form_args and form_args['state']: + if form_args['state'] == 'enabled': + query = query.filter(model.enabled == True) + elif form_args['state'] == 'disabled': + query = query.filter(model.enabled == False) + # Adjust query based on user role selection. + if 'role' in form_args and form_args['role']: + if form_args['role'] == hawat.const.NO_ROLE: + query = query.filter(model.roles == []) + else: + query = query.filter(model.roles.any(form_args['role'])) + # Adjust query based on user membership selection. + if 'membership' in form_args and form_args['membership']: + query = query \ + .join(model.memberships) \ + .filter(model.memberships.any(id=form_args['membership'].id)) + # Adjust query based on user management selection. + if 'management' in form_args and form_args['management']: + query = query \ + .join(model.managements) \ + .filter(model.managements.any(id=form_args['management'].id)) + if 'sortby' in form_args and form_args['sortby']: + sortmap = { + 'createtime.desc': lambda x, y: x.order_by(y.createtime.desc()), + 'createtime.asc': lambda x, y: x.order_by(y.createtime.asc()), + 'login.desc': lambda x, y: x.order_by(y.login.desc()), + 'login.asc': lambda x, y: x.order_by(y.login.asc()), + 'fullname.desc': lambda x, y: x.order_by(y.fullname.desc()), + 'fullname.asc': lambda x, y: x.order_by(y.fullname.asc()), + 'email.desc': lambda x, y: x.order_by(y.email.desc()), + 'email.asc': lambda x, y: x.order_by(y.email.asc()), + 'logintime.desc': lambda x, y: x.order_by(y.logintime.desc()), + 'logintime.asc': lambda x, y: x.order_by(y.logintime.asc()), + } + query = sortmap[form_args['sortby']](query, model) + return query + + +class ShowView(HTMLMixin, SQLAlchemyMixin, ItemShowView): + """ + Detailed user account view. + """ + methods = ['GET'] + + authentication = True + + @classmethod + def get_view_icon(cls): + return 'action-show-user' + + @classmethod + def get_menu_legend(cls, **kwargs): + return lazy_gettext( + 'Show details of user account "%(item)s"', + item=markupsafe.escape(kwargs['item'].login) + ) + + @classmethod + def get_view_title(cls, **kwargs): + return lazy_gettext('Show user account details') + + @property + def dbmodel(self): + return self.get_model(hawat.const.MODEL_USER) + + @classmethod + def authorize_item_action(cls, **kwargs): + # Each user must be able to view his/her account. + permission_me = flask_principal.Permission( + flask_principal.UserNeed(kwargs['item'].id) + ) + # Managers of the groups the user is member of may view his/her account. + needs = [hawat.acl.ManagementNeed(x.id) for x in kwargs['item'].memberships] + permission_mngr = flask_principal.Permission(*needs) + return hawat.acl.PERMISSION_POWER.can() or permission_mngr.can() or permission_me.can() + + @classmethod + def get_action_menu(cls): + action_menu = hawat.menu.Menu() + action_menu.add_entry( + 'endpoint', + 'update', + endpoint='users.update', + resptitle=True + ) + action_menu.add_entry( + 'endpoint', + 'disable', + endpoint='users.disable', + resptitle=True + ) + action_menu.add_entry( + 'endpoint', + 'enable', + endpoint='users.enable', + resptitle=True + ) + action_menu.add_entry( + 'endpoint', + 'delete', + endpoint='users.delete', + resptitle=True + ) + return action_menu + + def do_before_response(self, **kwargs): # pylint: disable=locally-disabled,no-self-use,unused-argument + item = self.response_context['item'] + action_menu = hawat.menu.Menu() + action_menu.add_entry( + 'endpoint', + 'show', + endpoint='groups.show', + hidetitle=True + ) + action_menu.add_entry( + 'submenu', + 'more', + align_right=True, + legend=gettext('More actions') + ) + action_menu.add_entry( + 'endpoint', + 'more.add_member', + endpoint='groups.addmember' + ) + action_menu.add_entry( + 'endpoint', + 'more.reject_member', + endpoint='groups.rejectmember' + ) + action_menu.add_entry( + 'endpoint', + 'more.remove_member', + endpoint='groups.removemember' + ) + action_menu.add_entry( + 'endpoint', + 'more.add_manager', + endpoint='groups.addmanager' + ) + action_menu.add_entry( + 'endpoint', + 'more.remove_manager', + endpoint='groups.removemanager' + ) + action_menu.add_entry( + 'endpoint', + 'more.enable', + endpoint='groups.enable' + ) + action_menu.add_entry( + 'endpoint', + 'more.disable', + endpoint='groups.disable' + ) + action_menu.add_entry( + 'endpoint', + 'more.update', + endpoint='groups.update' + ) + self.response_context.update( + context_action_menu_groups=action_menu + ) + + if self.has_endpoint('changelogs.search'): + self.response_context.update( + context_action_menu_changelogs=self.get_endpoint_class( + 'changelogs.search' + ).get_context_action_menu() + ) -import vial.blueprints.users -from vial.blueprints.users import BLUEPRINT_NAME, ListView, ShowView, MeView,\ - EnableView, DisableView, DeleteView, AddMembershipView, RejectMembershipView, RemoveMembershipView, AddManagementView, RemoveManagementView -from hawat.blueprints.users.forms import CreateUserAccountForm, UpdateUserAccountForm,\ - AdminUpdateUserAccountForm + if self.can_access_endpoint('users.update', item=item) and self.has_endpoint('changelogs.search'): + item_changelog_model = self.get_model(hawat.const.MODEL_ITEM_CHANGELOG) + item_changelog = self.dbsession.query(item_changelog_model). \ + filter(item_changelog_model.model == item.__class__.__name__). \ + filter(item_changelog_model.model_id == item.id). \ + order_by(item_changelog_model.createtime.desc()). \ + limit(100). \ + all() + self.response_context.update( + item_changelog=item_changelog + ) + user_changelog = self.dbsession.query(item_changelog_model). \ + filter(item_changelog_model.author_id == item.id). \ + order_by(item_changelog_model.createtime.desc()). \ + limit(100). \ + all() + self.response_context.update( + user_changelog=user_changelog + ) -class CreateView(vial.blueprints.users.CreateView): # pylint: disable=locally-disabled,too-many-ancestors + +class MeView(ShowView): # pylint: disable=locally-disabled,too-many-ancestors + """ + Detailed user account view for currently logged-in user (profile page). + """ + methods = ['GET'] + + authentication = True + + authorization = [hawat.acl.PERMISSION_ANY] + + @classmethod + def get_view_name(cls): + return 'me' + + @classmethod + def get_view_icon(cls): + return 'profile' + + @classmethod + def get_menu_title(cls, **kwargs): + return lazy_gettext('My account') + + @classmethod + def get_view_url(cls, **kwargs): + return flask.url_for(cls.get_view_endpoint()) + + @classmethod + def get_view_title(cls, **kwargs): + return lazy_gettext('My user account') + + @classmethod + def get_view_template(cls): + return '{}/show.html'.format(BLUEPRINT_NAME) + + @classmethod + def authorize_item_action(cls, **kwargs): + return True + + @classmethod + def get_breadcrumbs_menu(cls): # pylint: disable=locally-disabled,unused-argument + action_menu = hawat.menu.Menu() + action_menu.add_entry( + 'endpoint', + 'home', + endpoint=flask.current_app.config['ENDPOINT_HOME'] + ) + action_menu.add_entry( + 'endpoint', + 'show', + endpoint='{}.me'.format(cls.module_name) + ) + return action_menu + + def dispatch_request(self): # pylint: disable=locally-disabled,arguments-differ + """ + Mandatory interface required by the :py:func:`flask.views.View.dispatch_request`. + Will be called by the *Flask* framework to service the request. + + Single item with given unique identifier will be retrieved from database + and injected into template to be displayed to the user. + """ + item_id = flask_login.current_user.get_id() + item = self.dbquery().filter(self.dbmodel.id == item_id).first() + if not item: + self.abort(404) + + self.response_context.update( + item_id=item_id, + item=item, + breadcrumbs_menu=self.get_breadcrumbs_menu(), + action_menu=self.get_action_menu() + ) + + self.do_before_response() + + return self.generate_response() + + +class CreateView(HTMLMixin, SQLAlchemyMixin, ItemCreateView): # pylint: disable=locally-disabled,too-many-ancestors """ View for creating new user accounts. """ + methods = ['GET', 'POST'] + + authentication = True + + authorization = [hawat.acl.PERMISSION_POWER] + + @classmethod + def get_view_icon(cls): + return 'action-create-user' + + @classmethod + def get_view_title(cls, **kwargs): + return lazy_gettext('Create new user account') + + @property + def dbmodel(self): + return self.get_model(hawat.const.MODEL_USER) + + @property + def dbchlogmodel(self): + return self.get_model(hawat.const.MODEL_ITEM_CHANGELOG) + + @staticmethod + def get_message_success(**kwargs): + return gettext( + 'User account <strong>%(item_id)s</strong> was successfully created.', + item_id=markupsafe.escape(str(kwargs['item'])) + ) + + @staticmethod + def get_message_failure(**kwargs): + return gettext('Unable to create new user account.') + + @staticmethod + def get_message_cancel(**kwargs): + return gettext('Canceled creating new user account.') @staticmethod def get_item_form(item): @@ -56,15 +468,69 @@ class CreateView(vial.blueprints.users.CreateView): # pylint: disable=locally-d locales = list(flask.current_app.config['SUPPORTED_LOCALES'].items()) return CreateUserAccountForm( - choices_roles = roles, - choices_locales = locales + choices_roles=roles, + choices_locales=locales ) -class UpdateView(vial.blueprints.users.UpdateView): # pylint: disable=locally-disabled,too-many-ancestors +class UpdateView(HTMLMixin, SQLAlchemyMixin, ItemUpdateView): # pylint: disable=locally-disabled,too-many-ancestors """ View for updating existing user accounts. """ + methods = ['GET', 'POST'] + + authentication = True + + @classmethod + def get_view_icon(cls): + return 'action-update-user' + + @classmethod + def get_view_title(cls, **kwargs): + return lazy_gettext('Update user account details') + + @classmethod + def get_menu_legend(cls, **kwargs): + return lazy_gettext( + 'Update details of user account "%(item)s"', + item=markupsafe.escape(kwargs['item'].login) + ) + + @property + def dbmodel(self): + return self.get_model(hawat.const.MODEL_USER) + + @property + def dbchlogmodel(self): + return self.get_model(hawat.const.MODEL_ITEM_CHANGELOG) + + @classmethod + def authorize_item_action(cls, **kwargs): + permission_me = flask_principal.Permission( + flask_principal.UserNeed(kwargs['item'].id) + ) + return hawat.acl.PERMISSION_ADMIN.can() or permission_me.can() + + @staticmethod + def get_message_success(**kwargs): + return gettext( + 'User account <strong>%(item_id)s</strong> was successfully updated.', + item_id=markupsafe.escape(str(kwargs['item'])) + ) + + @staticmethod + def get_message_failure(**kwargs): + return gettext( + 'Unable to update user account <strong>%(item_id)s</strong>.', + item_id=markupsafe.escape(str(kwargs['item'])) + ) + + @staticmethod + def get_message_cancel(**kwargs): + return gettext( + 'Canceled updating user account <strong>%(item_id)s</strong>.', + item_id=markupsafe.escape(str(kwargs['item'])) + ) @staticmethod def get_item_form(item): @@ -79,74 +545,699 @@ class UpdateView(vial.blueprints.users.UpdateView): # pylint: disable=locally-d admin = flask_login.current_user.has_role('admin') if not admin: form = UpdateUserAccountForm( - choices_roles = roles, - choices_locales = locales, - obj = item + choices_roles=roles, + choices_locales=locales, + obj=item ) else: form = AdminUpdateUserAccountForm( - choices_roles = roles, - choices_locales = locales, - db_item_id = item.id, - obj = item + choices_roles=roles, + choices_locales=locales, + db_item_id=item.id, + obj=item ) return form -#------------------------------------------------------------------------------- +class AddMembershipView(HTMLMixin, SQLAlchemyMixin, + ItemObjectRelationView): # pylint: disable=locally-disabled,too-many-ancestors + """ + View for adding group memberships. + """ + methods = ['GET', 'POST'] + + authentication = True + + @classmethod + def get_view_name(cls): + return 'addmembership' + + @classmethod + def get_view_title(cls, **kwargs): + return gettext('Add group membership') + + @classmethod + def get_view_icon(cls): + return 'action-add-member' + @classmethod + def get_menu_legend(cls, **kwargs): + return lazy_gettext( + 'Add user "%(user_id)s" to group "%(group_id)s"', + user_id=markupsafe.escape(str(kwargs['item'])), + group_id=markupsafe.escape(str(kwargs['other'])) + ) + + @property + def dbmodel(self): + return self.get_model(hawat.const.MODEL_USER) + + @property + def dbmodel_other(self): + return self.get_model(hawat.const.MODEL_GROUP) + + @property + def dbchlogmodel(self): + return self.get_model(hawat.const.MODEL_ITEM_CHANGELOG) + + @classmethod + def authorize_item_action(cls, **kwargs): + permission_m = flask_principal.Permission( + hawat.acl.ManagementNeed(kwargs['other'].id) + ) + return hawat.acl.PERMISSION_POWER.can() or permission_m.can() + + @classmethod + def validate_item_change(cls, **kwargs): # pylint: disable=locally-disabled,unused-argument + # Reject item change in case given item is already enabled. + if kwargs['other'] in kwargs['item'].memberships: + return False + return True + + @classmethod + def change_item(cls, **kwargs): + kwargs['item'].memberships.append(kwargs['other']) + try: + kwargs['item'].memberships_wanted.remove(kwargs['other']) + except ValueError: + pass + if kwargs['item'].is_state_disabled(): + kwargs['item'].set_state_enabled() + flask.current_app.send_infomail( + 'users.enable', + account=kwargs['item'] + ) + + @staticmethod + def get_message_success(**kwargs): + return gettext( + 'User <strong>%(user_id)s</strong> was successfully added as a member to group <strong>%(group_id)s</strong>.', + user_id=markupsafe.escape(str(kwargs['item'])), + group_id=markupsafe.escape(str(kwargs['other'])) + ) + + @staticmethod + def get_message_failure(**kwargs): + return gettext( + 'Unable to add user <strong>%(user_id)s</strong> as a member to group <strong>%(group_id)s</strong>.', + user_id=markupsafe.escape(str(kwargs['item'])), + group_id=markupsafe.escape(str(kwargs['other'])) + ) + + @staticmethod + def get_message_cancel(**kwargs): + return gettext( + 'Canceled adding user <strong>%(user_id)s</strong> as a member to group <strong>%(group_id)s</strong>.', + user_id=markupsafe.escape(str(kwargs['item'])), + group_id=markupsafe.escape(str(kwargs['other'])) + ) + + +class RejectMembershipView(HTMLMixin, SQLAlchemyMixin, + ItemObjectRelationView): # pylint: disable=locally-disabled,too-many-ancestors + """ + View for rejecting group membership requests. + """ + methods = ['GET', 'POST'] + + authentication = True + + @classmethod + def get_view_name(cls): + return 'rejectmembership' + + @classmethod + def get_view_title(cls, **kwargs): + return gettext('Reject group membership') + + @classmethod + def get_view_icon(cls): + return 'action-rej-member' + + @classmethod + def get_menu_legend(cls, **kwargs): + return lazy_gettext( + 'Reject user`s "%(user_id)s" membership request for group "%(group_id)s"', + user_id=markupsafe.escape(str(kwargs['item'])), + group_id=markupsafe.escape(str(kwargs['other'])) + ) + + @property + def dbmodel(self): + return self.get_model(hawat.const.MODEL_USER) + + @property + def dbmodel_other(self): + return self.get_model(hawat.const.MODEL_GROUP) + + @property + def dbchlogmodel(self): + return self.get_model(hawat.const.MODEL_ITEM_CHANGELOG) + + @classmethod + def authorize_item_action(cls, **kwargs): + permission_m = flask_principal.Permission( + hawat.acl.ManagementNeed(kwargs['other'].id) + ) + return hawat.acl.PERMISSION_POWER.can() or permission_m.can() + + @classmethod + def validate_item_change(cls, **kwargs): # pylint: disable=locally-disabled,unused-argument + # Reject item change in case given item is already enabled. + if kwargs['other'] not in kwargs['item'].memberships_wanted: + return False + return True + + @classmethod + def change_item(cls, **kwargs): + kwargs['item'].memberships_wanted.remove(kwargs['other']) + + @staticmethod + def get_message_success(**kwargs): + return gettext( + 'User`s <strong>%(user_id)s</strong> membership request for group <strong>%(group_id)s</strong> was successfully rejected.', + user_id=markupsafe.escape(str(kwargs['item'])), + group_id=markupsafe.escape(str(kwargs['other'])) + ) -class UsersBlueprint(vial.blueprints.users.UsersBlueprint): + @staticmethod + def get_message_failure(**kwargs): + return gettext( + 'Unable to reject user`s <strong>%(user_id)s</strong> membership request for group <strong>%(group_id)s</strong>.', + user_id=markupsafe.escape(str(kwargs['item'])), + group_id=markupsafe.escape(str(kwargs['other'])) + ) + + @staticmethod + def get_message_cancel(**kwargs): + return gettext( + 'Canceled rejecting user`s <strong>%(user_id)s</strong> membership request for group <strong>%(group_id)s</strong>.', + user_id=markupsafe.escape(str(kwargs['item'])), + group_id=markupsafe.escape(str(kwargs['other'])) + ) + + +class RemoveMembershipView(HTMLMixin, SQLAlchemyMixin, + ItemObjectRelationView): # pylint: disable=locally-disabled,too-many-ancestors + """ + View for removing group memberships. + """ + methods = ['GET', 'POST'] + + authentication = True + + @classmethod + def get_view_name(cls): + return 'removemembership' + + @classmethod + def get_view_title(cls, **kwargs): + return gettext('Remove group membership') + + @classmethod + def get_view_icon(cls): + return 'action-rem-member' + + @classmethod + def get_menu_legend(cls, **kwargs): + return lazy_gettext( + 'Remove user "%(user_id)s" from group "%(group_id)s"', + user_id=markupsafe.escape(str(kwargs['item'])), + group_id=markupsafe.escape(str(kwargs['other'])) + ) + + @property + def dbmodel(self): + return self.get_model(hawat.const.MODEL_USER) + + @property + def dbmodel_other(self): + return self.get_model(hawat.const.MODEL_GROUP) + + @property + def dbchlogmodel(self): + return self.get_model(hawat.const.MODEL_ITEM_CHANGELOG) + + @classmethod + def authorize_item_action(cls, **kwargs): + permission_m = flask_principal.Permission( + hawat.acl.ManagementNeed(kwargs['other'].id) + ) + return hawat.acl.PERMISSION_POWER.can() or permission_m.can() + + @classmethod + def validate_item_change(cls, **kwargs): # pylint: disable=locally-disabled,unused-argument + # Reject item change in case given item is already enabled. + if kwargs['other'] not in kwargs['item'].memberships: + return False + return True + + @classmethod + def change_item(cls, **kwargs): + try: + kwargs['item'].memberships.remove(kwargs['other']) + except ValueError: + pass + + @staticmethod + def get_message_success(**kwargs): + return gettext( + 'User <strong>%(user_id)s</strong> was successfully removed as a member from group <strong>%(group_id)s</strong>.', + user_id=markupsafe.escape(str(kwargs['item'])), + group_id=markupsafe.escape(str(kwargs['other'])) + ) + + @staticmethod + def get_message_failure(**kwargs): + return gettext( + 'Unable to remove user <strong>%(user_id)s</strong> as a member from group <strong>%(group_id)s</strong>.', + user_id=markupsafe.escape(str(kwargs['item'])), + group_id=markupsafe.escape(str(kwargs['other'])) + ) + + @staticmethod + def get_message_cancel(**kwargs): + return gettext( + 'Canceled removing user <strong>%(user_id)s</strong> as a member from group <strong>%(group_id)s</strong>.', + user_id=markupsafe.escape(str(kwargs['item'])), + group_id=markupsafe.escape(str(kwargs['other'])) + ) + + +class AddManagementView(HTMLMixin, SQLAlchemyMixin, + ItemObjectRelationView): # pylint: disable=locally-disabled,too-many-ancestors + """ + View for adding group managements. + """ + methods = ['GET', 'POST'] + + authentication = True + + @classmethod + def get_view_name(cls): + return 'addmanagement' + + @classmethod + def get_view_title(cls, **kwargs): + return gettext('Add group management') + + @classmethod + def get_view_icon(cls): + return 'action-add-manager' + + @classmethod + def get_menu_legend(cls, **kwargs): + return lazy_gettext( + 'Add user "%(user_id)s" to group "%(group_id)s" as manager', + user_id=markupsafe.escape(str(kwargs['item'])), + group_id=markupsafe.escape(str(kwargs['other'])) + ) + + @property + def dbmodel(self): + return self.get_model(hawat.const.MODEL_USER) + + @property + def dbmodel_other(self): + return self.get_model(hawat.const.MODEL_GROUP) + + @property + def dbchlogmodel(self): + return self.get_model(hawat.const.MODEL_ITEM_CHANGELOG) + + @classmethod + def authorize_item_action(cls, **kwargs): + permission_m = flask_principal.Permission( + hawat.acl.ManagementNeed(kwargs['other'].id) + ) + return hawat.acl.PERMISSION_POWER.can() or permission_m.can() + + @classmethod + def validate_item_change(cls, **kwargs): # pylint: disable=locally-disabled,unused-argument + # Reject item change in case given item is already manager. + if kwargs['other'] in kwargs['item'].managements: + return False + return True + + @classmethod + def change_item(cls, **kwargs): + kwargs['item'].managements.append(kwargs['other']) + if kwargs['item'].is_state_disabled(): + kwargs['item'].set_state_enabled() + flask.current_app.send_infomail( + 'users.enable', + account=kwargs['item'] + ) + + @staticmethod + def get_message_success(**kwargs): + return gettext( + 'User <strong>%(user_id)s</strong> was successfully added as a manager to group <strong>%(group_id)s</strong>.', + user_id=markupsafe.escape(str(kwargs['item'])), + group_id=markupsafe.escape(str(kwargs['other'])) + ) + + @staticmethod + def get_message_failure(**kwargs): + return gettext( + 'Unable to add user <strong>%(user_id)s</strong> as a manager to group <strong>%(group_id)s</strong>.', + user_id=markupsafe.escape(str(kwargs['item'])), + group_id=markupsafe.escape(str(kwargs['other'])) + ) + + @staticmethod + def get_message_cancel(**kwargs): + return gettext( + 'Canceled adding user <strong>%(user_id)s</strong> as a manager to group <strong>%(group_id)s</strong>.', + user_id=markupsafe.escape(str(kwargs['item'])), + group_id=markupsafe.escape(str(kwargs['other'])) + ) + + +class RemoveManagementView(HTMLMixin, SQLAlchemyMixin, + ItemObjectRelationView): # pylint: disable=locally-disabled,too-many-ancestors + """ + View for removing group managements. + """ + methods = ['GET', 'POST'] + + authentication = True + + @classmethod + def get_view_name(cls): + return 'removemanagement' + + @classmethod + def get_view_title(cls, **kwargs): + return gettext('Remove group management') + + @classmethod + def get_view_icon(cls): + return 'action-rem-manager' + + @classmethod + def get_menu_legend(cls, **kwargs): + return lazy_gettext( + 'Remove user "%(user_id)s" from group "%(group_id)s" as manager', + user_id=markupsafe.escape(str(kwargs['item'])), + group_id=markupsafe.escape(str(kwargs['other'])) + ) + + @property + def dbmodel(self): + return self.get_model(hawat.const.MODEL_USER) + + @property + def dbmodel_other(self): + return self.get_model(hawat.const.MODEL_GROUP) + + @property + def dbchlogmodel(self): + return self.get_model(hawat.const.MODEL_ITEM_CHANGELOG) + + @classmethod + def authorize_item_action(cls, **kwargs): + permission_m = flask_principal.Permission( + hawat.acl.ManagementNeed(kwargs['other'].id) + ) + return hawat.acl.PERMISSION_POWER.can() or permission_m.can() + + @classmethod + def validate_item_change(cls, **kwargs): # pylint: disable=locally-disabled,unused-argument + # Reject item change in case given item is not already manager. + if kwargs['other'] not in kwargs['item'].managements: + return False + return True + + @classmethod + def change_item(cls, **kwargs): + try: + kwargs['item'].managements.remove(kwargs['other']) + except ValueError: + pass + + @staticmethod + def get_message_success(**kwargs): + return gettext( + 'User <strong>%(user_id)s</strong> was successfully removed as a manager from group <strong>%(group_id)s</strong>.', + user_id=markupsafe.escape(str(kwargs['item'])), + group_id=markupsafe.escape(str(kwargs['other'])) + ) + + @staticmethod + def get_message_failure(**kwargs): + return gettext( + 'Unable to remove user <strong>%(user_id)s</strong> as a manager from group <strong>%(group_id)s</strong>.', + user_id=markupsafe.escape(str(kwargs['item'])), + group_id=markupsafe.escape(str(kwargs['other'])) + ) + + @staticmethod + def get_message_cancel(**kwargs): + return gettext( + 'Canceled removing user <strong>%(user_id)s</strong> as a manager from group <strong>%(group_id)s</strong>.', + user_id=markupsafe.escape(str(kwargs['item'])), + group_id=markupsafe.escape(str(kwargs['other'])) + ) + + +class EnableView(HTMLMixin, SQLAlchemyMixin, ItemEnableView): # pylint: disable=locally-disabled,too-many-ancestors + """ + View for enabling existing user accounts. + """ + methods = ['GET', 'POST'] + + authentication = True + + authorization = [hawat.acl.PERMISSION_POWER] + + @classmethod + def get_view_icon(cls): + return 'action-enable-user' + + @classmethod + def get_menu_legend(cls, **kwargs): + return lazy_gettext( + 'Enable user account "%(item)s"', + item=markupsafe.escape(kwargs['item'].login) + ) + + @property + def dbmodel(self): + return self.get_model(hawat.const.MODEL_USER) + + @property + def dbchlogmodel(self): + return self.get_model(hawat.const.MODEL_ITEM_CHANGELOG) + + @staticmethod + def get_message_success(**kwargs): + return gettext( + 'User account <strong>%(item_id)s</strong> was successfully enabled.', + item_id=markupsafe.escape(str(kwargs['item'])) + ) + + @staticmethod + def get_message_failure(**kwargs): + return gettext( + 'Unable to enable user account <strong>%(item_id)s</strong>.', + item_id=markupsafe.escape(str(kwargs['item'])) + ) + + @staticmethod + def get_message_cancel(**kwargs): + return gettext( + 'Canceled enabling user account <strong>%(item_id)s</strong>.', + item_id=markupsafe.escape(str(kwargs['item'])) + ) + + @classmethod + def inform_user(cls, account): + """Send infomail about user account activation.""" + mail_locale = account.locale + if not mail_locale: + mail_locale = flask.current_app.config['BABEL_DEFAULT_LOCALE'] + + with force_locale(mail_locale): + msg = flask_mail.Message( + gettext( + "[%(app_name)s] Account activation - %(item_id)s", + app_name=flask.current_app.config['APPLICATION_NAME'], + item_id=account.login + ), + recipients=[account.email], + bcc=flask.current_app.config['EMAIL_ADMINS'] + ) + msg.body = flask.render_template( + 'users/email_activation.txt', + account=account + ) + flask.current_app.mailer.send(msg) + + def do_after_action(self, item): + self.inform_user(item) + + +class DisableView(HTMLMixin, SQLAlchemyMixin, ItemDisableView): # pylint: disable=locally-disabled,too-many-ancestors + """ + View for disabling user accounts. + """ + methods = ['GET', 'POST'] + + authentication = True + + authorization = [hawat.acl.PERMISSION_POWER] + + @classmethod + def get_view_icon(cls): + return 'action-disable-user' + + @classmethod + def get_menu_legend(cls, **kwargs): + return lazy_gettext( + 'Disable user account "%(item)s"', + item=markupsafe.escape(kwargs['item'].login) + ) + + @property + def dbmodel(self): + return self.get_model(hawat.const.MODEL_USER) + + @property + def dbchlogmodel(self): + return self.get_model(hawat.const.MODEL_ITEM_CHANGELOG) + + @staticmethod + def get_message_success(**kwargs): + return gettext( + 'User account <strong>%(item_id)s</strong> was successfully disabled.', + item_id=markupsafe.escape(str(kwargs['item'])) + ) + + @staticmethod + def get_message_failure(**kwargs): + return gettext( + 'Unable to disable user account <strong>%(item_id)s</strong>.', + item_id=markupsafe.escape(str(kwargs['item'])) + ) + + @staticmethod + def get_message_cancel(**kwargs): + return gettext( + 'Canceled disabling user account <strong>%(item_id)s</strong>.', + item_id=markupsafe.escape(str(kwargs['item'])) + ) + + +class DeleteView(HTMLMixin, SQLAlchemyMixin, ItemDeleteView): # pylint: disable=locally-disabled,too-many-ancestors + """ + View for deleting existing user accounts. + """ + methods = ['GET', 'POST'] + + authentication = True + + authorization = [hawat.acl.PERMISSION_ADMIN] + + @classmethod + def get_view_icon(cls): + return 'action-delete-user' + + @classmethod + def get_menu_legend(cls, **kwargs): + return lazy_gettext( + 'Delete user account "%(item)s"', + item=markupsafe.escape(kwargs['item'].login) + ) + + @property + def dbmodel(self): + return self.get_model(hawat.const.MODEL_USER) + + @property + def dbchlogmodel(self): + return self.get_model(hawat.const.MODEL_ITEM_CHANGELOG) + + @staticmethod + def get_message_success(**kwargs): + return gettext( + 'User account <strong>%(item_id)s</strong> was successfully and permanently deleted.', + item_id=markupsafe.escape(str(kwargs['item'])) + ) + + @staticmethod + def get_message_failure(**kwargs): + return gettext( + 'Unable to delete user account <strong>%(item_id)s</strong>.', + item_id=markupsafe.escape(str(kwargs['item'])) + ) + + @staticmethod + def get_message_cancel(**kwargs): + return gettext( + 'Canceled deleting user account <strong>%(item_id)s</strong>.', + item_id=markupsafe.escape(str(kwargs['item'])) + ) + + +# ------------------------------------------------------------------------------- + + +class UsersBlueprint(HawatBlueprint): """Pluggable module - user account management (*users*).""" + @classmethod + def get_module_title(cls): + return lazy_gettext('User account management') + def register_app(self, app): app.menu_main.add_entry( 'view', 'admin.{}'.format(BLUEPRINT_NAME), - position = 40, - group = lazy_gettext('Object management'), - view = ListView + position=40, + group=lazy_gettext('Object management'), + view=ListView ) app.menu_auth.add_entry( 'view', 'my_account', - position = 10, - view = MeView, - params = lambda: {'item': flask_login.current_user} + position=10, + view=MeView, + params=lambda: {'item': flask_login.current_user} ) app.set_infomailer('users.enable', EnableView.inform_user) -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- def get_blueprint(): """ - Mandatory interface for :py:mod:`vial.Vial` and factory function. This function - must return a valid instance of :py:class:`vial.app.VialBlueprint` or + Mandatory interface for :py:mod:`hawat.Hawat` and factory function. This function + must return a valid instance of :py:class:`hawat.app.HawatBlueprint` or :py:class:`flask.Blueprint`. """ hbp = UsersBlueprint( BLUEPRINT_NAME, __name__, - template_folder = 'templates', - url_prefix = '/{}'.format(BLUEPRINT_NAME) + template_folder='templates', + url_prefix='/{}'.format(BLUEPRINT_NAME) ) - hbp.register_view_class(ListView, '/list') - hbp.register_view_class(CreateView, '/create') - hbp.register_view_class(ShowView, '/<int:item_id>/show') - hbp.register_view_class(MeView, '/me') - hbp.register_view_class(UpdateView, '/<int:item_id>/update') - hbp.register_view_class(AddMembershipView, '/<int:item_id>/add_membership/<int:other_id>') + hbp.register_view_class(ListView, '/list') + hbp.register_view_class(CreateView, '/create') + hbp.register_view_class(ShowView, '/<int:item_id>/show') + hbp.register_view_class(MeView, '/me') + hbp.register_view_class(UpdateView, '/<int:item_id>/update') + hbp.register_view_class(AddMembershipView, '/<int:item_id>/add_membership/<int:other_id>') hbp.register_view_class(RemoveMembershipView, '/<int:item_id>/remove_membership/<int:other_id>') hbp.register_view_class(RejectMembershipView, '/<int:item_id>/reject_membership/<int:other_id>') - hbp.register_view_class(AddManagementView, '/<int:item_id>/add_management/<int:other_id>') + hbp.register_view_class(AddManagementView, '/<int:item_id>/add_management/<int:other_id>') hbp.register_view_class(RemoveManagementView, '/<int:item_id>/remove_management/<int:other_id>') - hbp.register_view_class(EnableView, '/<int:item_id>/enable') - hbp.register_view_class(DisableView, '/<int:item_id>/disable') - hbp.register_view_class(DeleteView, '/<int:item_id>/delete') + hbp.register_view_class(EnableView, '/<int:item_id>/enable') + hbp.register_view_class(DisableView, '/<int:item_id>/disable') + hbp.register_view_class(DeleteView, '/<int:item_id>/delete') return hbp diff --git a/lib/hawat/blueprints/users/forms.py b/lib/hawat/blueprints/users/forms.py index 60fe0a31743484b40d3e8dc5d7d5dc4988e80d0b..58af00e430cfc0551e49889ebc964e2f9e575a64 100644 --- a/lib/hawat/blueprints/users/forms.py +++ b/lib/hawat/blueprints/users/forms.py @@ -1,31 +1,31 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ This module contains custom user account management forms for Hawat. """ - __author__ = "Jan Mach <jan.mach@cesnet.cz>" __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" - import pytz import sqlalchemy import wtforms -from wtforms.ext.sqlalchemy.fields import QuerySelectMultipleField +from wtforms.ext.sqlalchemy.fields import QuerySelectField, QuerySelectMultipleField from flask_babel import gettext, lazy_gettext -import vial.db -import vial.forms -from mentat.datatype.sqldb import UserModel, GroupModel +import hawat.db +import hawat.const +import hawat.forms +from hawat.forms import check_login, check_unique_login, get_available_groups +from mentat.datatype.sqldb import UserModel def check_id_existence(form, field): @@ -33,8 +33,8 @@ def check_id_existence(form, field): Callback for validating user logins during account create action. """ try: - vial.db.db_get().session.query(UserModel).\ - filter(UserModel.login == field.data).\ + hawat.db.db_get().session.query(UserModel). \ + filter(UserModel.login == field.data). \ one() except sqlalchemy.orm.exc.NoResultFound: return @@ -47,65 +47,58 @@ def check_id_uniqueness(form, field): """ Callback for validating user logins during account update action. """ - user = vial.db.db_get().session.query(UserModel).\ - filter(UserModel.login == field.data).\ - filter(UserModel.id != form.db_item_id).\ + user = hawat.db.db_get().session.query(UserModel). \ + filter(UserModel.login == field.data). \ + filter(UserModel.id != form.db_item_id). \ all() if not user: return raise wtforms.validators.ValidationError(gettext('User account with this login already exists.')) -def get_available_groups(): - """ - Query the database for list of all available groups. - """ - return vial.db.db_query(GroupModel).order_by(GroupModel.name).all() - - -class BaseUserAccountForm(vial.forms.BaseItemForm): +class BaseUserAccountForm(hawat.forms.BaseItemForm): """ Class representing base user account form. """ fullname = wtforms.StringField( lazy_gettext('Full name:'), - validators = [ + validators=[ wtforms.validators.DataRequired(), - wtforms.validators.Length(min = 3, max = 100) + wtforms.validators.Length(min=3, max=100) ] ) email = wtforms.StringField( lazy_gettext('Email:'), - validators = [ + validators=[ wtforms.validators.DataRequired(), - wtforms.validators.Length(min = 3, max = 250), + wtforms.validators.Length(min=3, max=250), wtforms.validators.Email() ] ) organization = wtforms.StringField( lazy_gettext('Home organization:'), - validators = [ + validators=[ wtforms.validators.DataRequired(), - wtforms.validators.Length(min = 3, max = 250) + wtforms.validators.Length(min=3, max=250) ] ) - locale = vial.forms.SelectFieldWithNone( + locale = hawat.forms.SelectFieldWithNone( lazy_gettext('Prefered locale:'), - validators = [ + validators=[ wtforms.validators.Optional() ], - choices = [('', lazy_gettext('<< no preference >>'))], - filters = [lambda x: x or None], - default = '' + choices=[('', lazy_gettext('<< no preference >>'))], + filters=[lambda x: x or None], + default='' ) - timezone = vial.forms.SelectFieldWithNone( + timezone = hawat.forms.SelectFieldWithNone( lazy_gettext('Prefered timezone:'), - validators = [ + validators=[ wtforms.validators.Optional(), ], - choices = [('', lazy_gettext('<< no preference >>'))] + list(zip(pytz.common_timezones, pytz.common_timezones)), - filters = [lambda x: x or None], - default = '' + choices=[('', lazy_gettext('<< no preference >>'))] + list(zip(pytz.common_timezones, pytz.common_timezones)), + filters=[lambda x: x or None], + default='' ) submit = wtforms.SubmitField( lazy_gettext('Submit') @@ -134,29 +127,29 @@ class AdminUserAccountForm(BaseUserAccountForm): """ enabled = wtforms.RadioField( lazy_gettext('State:'), - validators = [ + validators=[ wtforms.validators.InputRequired(), ], - choices = [ - (True, lazy_gettext('Enabled')), + choices=[ + (True, lazy_gettext('Enabled')), (False, lazy_gettext('Disabled')) ], - filters = [vial.forms.str_to_bool], - coerce = vial.forms.str_to_bool + filters=[hawat.forms.str_to_bool], + coerce=hawat.forms.str_to_bool ) roles = wtforms.SelectMultipleField( lazy_gettext('Roles:'), - validators = [ + validators=[ wtforms.validators.Optional() ] ) memberships = QuerySelectMultipleField( lazy_gettext('Group memberships:'), - query_factory = get_available_groups + query_factory=get_available_groups ) managements = QuerySelectMultipleField( lazy_gettext('Group managements:'), - query_factory = get_available_groups + query_factory=get_available_groups ) def __init__(self, *args, **kwargs): @@ -181,10 +174,11 @@ class CreateUserAccountForm(AdminUserAccountForm): """ login = wtforms.StringField( lazy_gettext('Login:'), - validators = [ + validators=[ wtforms.validators.DataRequired(), - wtforms.validators.Length(min = 3, max = 50), - vial.forms.check_login, + wtforms.validators.Length(min=3, max=50), + check_login, + check_unique_login, check_id_existence ] ) @@ -202,10 +196,11 @@ class AdminUpdateUserAccountForm(AdminUserAccountForm): """ login = wtforms.StringField( lazy_gettext('Login:'), - validators = [ + validators=[ wtforms.validators.DataRequired(), - wtforms.validators.Length(min = 3, max = 50), - vial.forms.check_login, + wtforms.validators.Length(min=3, max=50), + hawat.forms.check_login, + check_unique_login, check_id_uniqueness ] ) @@ -220,3 +215,117 @@ class AdminUpdateUserAccountForm(AdminUserAccountForm): # Store the ID of original item in database to enable the ID uniqueness # check with check_id_uniqueness() validator. self.db_item_id = kwargs['db_item_id'] + + +class UserSearchForm(hawat.forms.BaseSearchForm): + """ + Class representing simple user search form. + """ + search = wtforms.StringField( + lazy_gettext('Login, name, email:'), + validators=[ + wtforms.validators.Optional(), + wtforms.validators.Length(min=3, max=100) + ], + description=lazy_gettext( + 'User`s login, full name or email address. Search is performed even in the middle of the strings, so for example you may lookup by domain.') + ) + dt_from = hawat.forms.SmartDateTimeField( + lazy_gettext('Creation time from:'), + validators=[ + wtforms.validators.Optional() + ], + description=lazy_gettext( + 'Lower time boundary for item creation time. Timestamp is expected to be in the format <code>YYYY-MM-DD hh:mm:ss</code> and in the timezone according to the user`s preferences.') + ) + dt_to = hawat.forms.SmartDateTimeField( + lazy_gettext('Creation time to:'), + validators=[ + wtforms.validators.Optional() + ], + description=lazy_gettext( + 'Upper time boundary for item creation time. Timestamp is expected to be in the format <code>YYYY-MM-DD hh:mm:ss</code> and in the timezone according to the user`s preferences.') + ) + + state = wtforms.SelectField( + lazy_gettext('State:'), + validators=[ + wtforms.validators.Optional(), + ], + choices=[ + ('', lazy_gettext('Nothing selected')), + ('enabled', lazy_gettext('Enabled')), + ('disabled', lazy_gettext('Disabled')) + ], + default='', + description=lazy_gettext('Search for users with particular account state.') + ) + role = wtforms.SelectField( + lazy_gettext('Role:'), + validators=[ + wtforms.validators.Optional() + ], + default='', + description=lazy_gettext('Search for users with particular role, or without any assigned roles.') + ) + membership = QuerySelectField( + lazy_gettext('Group membership:'), + query_factory=get_available_groups, + allow_blank=True, + description=lazy_gettext('Search for users with membership with particular group.') + ) + management = QuerySelectField( + lazy_gettext('Group management:'), + query_factory=get_available_groups, + allow_blank=True, + description=lazy_gettext('Search for users with management rights to particular group.') + ) + + sortby = wtforms.SelectField( + lazy_gettext('Sort result by:'), + validators=[ + wtforms.validators.Optional() + ], + choices=[ + ('createtime.desc', lazy_gettext('by creation time descending')), + ('createtime.asc', lazy_gettext('by creation time ascending')), + ('login.desc', lazy_gettext('by login descending')), + ('login.asc', lazy_gettext('by login ascending')), + ('fullname.desc', lazy_gettext('by name descending')), + ('fullname.asc', lazy_gettext('by name ascending')), + ('email.desc', lazy_gettext('by email descending')), + ('email.asc', lazy_gettext('by email ascending')), + ('logintime.desc', lazy_gettext('by login time descending')), + ('logintime.asc', lazy_gettext('by login time ascending')), + ], + default='fullname.asc' + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # + # Handle additional custom keywords. + # + + # The list of choices for 'roles' attribute comes from outside of the + # form to provide as loose tie as possible to the outer application. + # Another approach would be to load available choices here with: + # + # roles = flask.current_app.config['ROLES'] + # + # That would mean direct dependency on flask.Flask application. + self.role.choices = [ + ('', lazy_gettext('Nothing selected')), + (hawat.const.NO_ROLE, lazy_gettext('Without any roles')) + ] + kwargs['choices_roles'] + + @staticmethod + def is_multivalue(field_name): + """ + Check, if given form field is a multivalue field. + + :param str field_name: Name of the form field. + :return: ``True``, if the field can contain multiple values, ``False`` otherwise. + :rtype: bool + """ + return False diff --git a/lib/hawat/blueprints/users/templates/users/show.html b/lib/hawat/blueprints/users/templates/users/show.html index d5a3d9bbc0d5c8fd41486b0a824d20dee2a2e9b1..7a50b088e3bffbc838b7b8022251ead860238486 100644 --- a/lib/hawat/blueprints/users/templates/users/show.html +++ b/lib/hawat/blueprints/users/templates/users/show.html @@ -6,7 +6,7 @@ <div class="col-lg-12"> {{ macros_page.render_breadcrumbs(item) }} - <h2>{{ vial_current_view.get_view_title() }}</h2> + <h2>{{ hawat_current_view.get_view_title() }}</h2> <hr> <h3>{{ item.fullname }} ({{ item.login }})</h3> <div class="pull-right"> diff --git a/lib/hawat/blueprints/users/test/__init__.py b/lib/hawat/blueprints/users/test/__init__.py index db6514719d8cf0d57b5a9ba86ed6f5bed73ad489..444b7c03c7a07770bb7732797575a4418c7d484c 100644 --- a/lib/hawat/blueprints/users/test/__init__.py +++ b/lib/hawat/blueprints/users/test/__init__.py @@ -1,32 +1,31 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ Unit tests for :py:mod:`hawat.blueprints.users`. """ - import unittest -import vial.const -import vial.test -import vial.test.fixtures -from vial.test import VialTestCase, ItemCreateVialTestCase +import hawat.const +import hawat.test +import hawat.test.fixtures +from hawat.test import HawatTestCase, ItemCreateHawatTestCase from hawat.test.runner import TestRunnerMixin from hawat.blueprints.users.test.utils import UsersTestCaseMixin -class UsersListTestCase(UsersTestCaseMixin, TestRunnerMixin, VialTestCase): +class UsersListTestCase(UsersTestCaseMixin, TestRunnerMixin, HawatTestCase): """Class for testing ``users.list`` endpoint.""" - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_USER) def test_01_as_user(self): """ Test access as user ``user``. @@ -35,7 +34,7 @@ class UsersListTestCase(UsersTestCaseMixin, TestRunnerMixin, VialTestCase): """ self._attempt_fail_list() - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_DEVELOPER) def test_02_as_developer(self): """ Test access as user ``developer``. @@ -44,7 +43,7 @@ class UsersListTestCase(UsersTestCaseMixin, TestRunnerMixin, VialTestCase): """ self._attempt_fail_list() - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_MAINTAINER) def test_03_as_maintainer(self): """ Test access as user ``maintainer``. @@ -53,7 +52,7 @@ class UsersListTestCase(UsersTestCaseMixin, TestRunnerMixin, VialTestCase): """ self._attempt_succeed_list() - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_ADMIN) def test_04_as_admin(self): """ Test access as user ``admin``. @@ -63,67 +62,67 @@ class UsersListTestCase(UsersTestCaseMixin, TestRunnerMixin, VialTestCase): self._attempt_succeed_list() -class UsersShowOwnTestCase(UsersTestCaseMixin, TestRunnerMixin, VialTestCase): +class UsersShowOwnTestCase(UsersTestCaseMixin, TestRunnerMixin, HawatTestCase): """ Class for testing ``users.show`` endpoint: access to user`s own accounts. Each user must be able to access his own account. """ - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_USER) def test_01_as_user(self): """Test access as user 'user'.""" - self._attempt_succeed_show(vial.const.ROLE_USER) + self._attempt_succeed_show(hawat.const.ROLE_USER) - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_DEVELOPER) def test_02_as_developer(self): """Test access as user 'developer'.""" - self._attempt_succeed_show(vial.const.ROLE_DEVELOPER) + self._attempt_succeed_show(hawat.const.ROLE_DEVELOPER) - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_MAINTAINER) def test_03_as_maintainer(self): """Test access as user 'maintainer'.""" - self._attempt_succeed_show(vial.const.ROLE_MAINTAINER) + self._attempt_succeed_show(hawat.const.ROLE_MAINTAINER) - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_ADMIN) def test_04_as_admin(self): """Test access as user 'admin'.""" - self._attempt_succeed_show(vial.const.ROLE_ADMIN) + self._attempt_succeed_show(hawat.const.ROLE_ADMIN) -class UsersShowOtherTestCase(UsersTestCaseMixin, TestRunnerMixin, VialTestCase): +class UsersShowOtherTestCase(UsersTestCaseMixin, TestRunnerMixin, HawatTestCase): """Class for testing ``users.show`` endpoint: access to other user`s accounts.""" - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_USER) def test_01_as_user_developer(self): """ Test access to 'developer' account as user 'user'. Regular user may view only his own account. """ - self._attempt_fail_show(vial.const.ROLE_DEVELOPER) + self._attempt_fail_show(hawat.const.ROLE_DEVELOPER) - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_USER) def test_02_as_user_maintainer(self): """ Test access to 'maintainer' account as user 'user'. Regular user may view only his own account. """ - self._attempt_fail_show(vial.const.ROLE_MAINTAINER) + self._attempt_fail_show(hawat.const.ROLE_MAINTAINER) - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_USER) def test_03_as_user_admin(self): """ Test access to 'admin' account as user 'user'. Regular user may view only his own account. """ - self._attempt_fail_show(vial.const.ROLE_ADMIN) + self._attempt_fail_show(hawat.const.ROLE_ADMIN) - #-------------------------------------------------------------------------- + # -------------------------------------------------------------------------- - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_DEVELOPER) def test_04_as_developer_user(self): """ Test access to 'user' account as user 'developer'. @@ -131,9 +130,9 @@ class UsersShowOtherTestCase(UsersTestCaseMixin, TestRunnerMixin, VialTestCase): Developer should be able to access because he is a manager of group of which all other users are members. """ - self._attempt_succeed_show(vial.const.ROLE_USER) + self._attempt_succeed_show(hawat.const.ROLE_USER) - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_DEVELOPER) def test_05_as_developer_maintainer(self): """ Test access to 'maintainer' account as user 'developer'. @@ -141,9 +140,9 @@ class UsersShowOtherTestCase(UsersTestCaseMixin, TestRunnerMixin, VialTestCase): Developer should be able to access because he is a manager of group of which all other users are members. """ - self._attempt_succeed_show(vial.const.ROLE_MAINTAINER) + self._attempt_succeed_show(hawat.const.ROLE_MAINTAINER) - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_DEVELOPER) def test_06_as_developer_admin(self): """ Test access to 'admin' account as user 'developer'. @@ -151,56 +150,56 @@ class UsersShowOtherTestCase(UsersTestCaseMixin, TestRunnerMixin, VialTestCase): Developer should be able to access because he is a manager of group of which all other users are members. """ - self._attempt_succeed_show(vial.const.ROLE_ADMIN) + self._attempt_succeed_show(hawat.const.ROLE_ADMIN) - #-------------------------------------------------------------------------- + # -------------------------------------------------------------------------- - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_MAINTAINER) def test_07_as_maintainer_user(self): """ Test access to 'user' account as user 'maintainer'. Maintainer should be allowed access, because he is a power user like admin. """ - self._attempt_succeed_show(vial.const.ROLE_USER) + self._attempt_succeed_show(hawat.const.ROLE_USER) - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_MAINTAINER) def test_08_as_maintainer_developer(self): """ Test access to 'developer' account as user 'maintainer'. Maintainer should be allowed access, because he is a power user like admin. """ - self._attempt_succeed_show(vial.const.ROLE_DEVELOPER) + self._attempt_succeed_show(hawat.const.ROLE_DEVELOPER) - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_MAINTAINER) def test_09_as_maintainer_admin(self): """ Test access to 'maintainer' account as user 'maintainer'. Maintainer should be allowed access, because he is a power user like admin. """ - self._attempt_succeed_show(vial.const.ROLE_MAINTAINER) + self._attempt_succeed_show(hawat.const.ROLE_MAINTAINER) - #-------------------------------------------------------------------------- + # -------------------------------------------------------------------------- - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_ADMIN) def test_10_as_admin_user(self): """Test access to 'user' account as user 'admin'.""" - self._attempt_succeed_show(vial.const.ROLE_USER) + self._attempt_succeed_show(hawat.const.ROLE_USER) - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_ADMIN) def test_11_as_admin_developer(self): """Test access to 'developer' account as user 'admin'.""" - self._attempt_succeed_show(vial.const.ROLE_DEVELOPER) + self._attempt_succeed_show(hawat.const.ROLE_DEVELOPER) - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_ADMIN) def test_12_as_admin_maintainer(self): """Test access to 'maintainer' account as user 'admin'.""" - self._attempt_succeed_show(vial.const.ROLE_MAINTAINER) + self._attempt_succeed_show(hawat.const.ROLE_MAINTAINER) -class UsersCreateTestCase(UsersTestCaseMixin, TestRunnerMixin, ItemCreateVialTestCase): +class UsersCreateTestCase(UsersTestCaseMixin, TestRunnerMixin, ItemCreateHawatTestCase): """Class for testing ``users.create`` endpoint.""" user_data_fixture = [ @@ -211,172 +210,172 @@ class UsersCreateTestCase(UsersTestCaseMixin, TestRunnerMixin, ItemCreateVialTes ('enabled', True) ] - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_USER) def test_01_as_user(self): """Test access as user 'user'.""" self._attempt_fail_create() - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_DEVELOPER) def test_02_as_developer(self): """Test access as user 'developer'.""" self._attempt_fail_create() - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_MAINTAINER) def test_03_as_maintainer(self): """Test access as user 'maintainer'.""" self._attempt_succeed_create(self.user_data_fixture) - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_ADMIN) def test_04_as_admin(self): """Test access as user 'admin'.""" self._attempt_succeed_create(self.user_data_fixture) -class UsersUpdateOwnTestCase(UsersTestCaseMixin, TestRunnerMixin, VialTestCase): +class UsersUpdateOwnTestCase(UsersTestCaseMixin, TestRunnerMixin, HawatTestCase): """Class for testing ``users.update`` endpoint: access to user`s own accounts.""" - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_USER) def test_01_as_user(self): """Test access as user 'user'.""" - self._attempt_succeed_update(vial.const.ROLE_USER) + self._attempt_succeed_update(hawat.const.ROLE_USER) - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_DEVELOPER) def test_02_as_developer(self): """Test access as user 'developer'.""" - self._attempt_succeed_update(vial.const.ROLE_DEVELOPER) + self._attempt_succeed_update(hawat.const.ROLE_DEVELOPER) - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_MAINTAINER) def test_03_as_maintainer(self): """Test access as user 'maintainer'.""" - self._attempt_succeed_update(vial.const.ROLE_MAINTAINER) + self._attempt_succeed_update(hawat.const.ROLE_MAINTAINER) - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_ADMIN) def test_04_as_admin(self): """Test access as user 'admin'.""" - self._attempt_succeed_update(vial.const.ROLE_ADMIN) + self._attempt_succeed_update(hawat.const.ROLE_ADMIN) -class UsersUpdateOtherTestCase(UsersTestCaseMixin, TestRunnerMixin, VialTestCase): +class UsersUpdateOtherTestCase(UsersTestCaseMixin, TestRunnerMixin, HawatTestCase): """Class for testing ``users.update`` endpoint: access to other user`s accounts.""" - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_USER) def test_01_as_user_developer(self): """Test access to 'developer' account as user 'user'.""" - self._attempt_fail_update(vial.const.ROLE_DEVELOPER) + self._attempt_fail_update(hawat.const.ROLE_DEVELOPER) - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_USER) def test_02_as_user_maintainer(self): """Test access to 'maintainer' account as user 'user'.""" - self._attempt_fail_update(vial.const.ROLE_MAINTAINER) + self._attempt_fail_update(hawat.const.ROLE_MAINTAINER) - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_USER) def test_03_as_user_admin(self): """Test access to 'admin' account as user 'user'.""" - self._attempt_fail_update(vial.const.ROLE_ADMIN) + self._attempt_fail_update(hawat.const.ROLE_ADMIN) - #-------------------------------------------------------------------------- + # -------------------------------------------------------------------------- - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_DEVELOPER) def test_04_as_developer_user(self): """Test access to 'user' account as user 'developer'.""" - self._attempt_fail_update(vial.const.ROLE_USER) + self._attempt_fail_update(hawat.const.ROLE_USER) - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_DEVELOPER) def test_05_as_developer_maintainer(self): """Test access to 'maintainer' account as user 'developer'.""" - self._attempt_fail_update(vial.const.ROLE_MAINTAINER) + self._attempt_fail_update(hawat.const.ROLE_MAINTAINER) - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_DEVELOPER) def test_06_as_developer_admin(self): """Test access to 'admin' account as user 'developer'.""" - self._attempt_fail_update(vial.const.ROLE_ADMIN) + self._attempt_fail_update(hawat.const.ROLE_ADMIN) - #-------------------------------------------------------------------------- + # -------------------------------------------------------------------------- - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_MAINTAINER) def test_07_as_maintainer_user(self): """Test access to 'user' account as user 'maintainer'.""" - self._attempt_fail_update(vial.const.ROLE_USER) + self._attempt_fail_update(hawat.const.ROLE_USER) - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_MAINTAINER) def test_08_as_maintainer_developer(self): """Test access to 'developer' account as user 'maintainer'.""" - self._attempt_fail_update(vial.const.ROLE_DEVELOPER) + self._attempt_fail_update(hawat.const.ROLE_DEVELOPER) - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_MAINTAINER) def test_09_as_maintainer_admin(self): """Test access to 'admin' account as user 'maintainer'.""" - self._attempt_fail_update(vial.const.ROLE_ADMIN) + self._attempt_fail_update(hawat.const.ROLE_ADMIN) - #-------------------------------------------------------------------------- + # -------------------------------------------------------------------------- - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_ADMIN) def test_10_as_admin_user(self): """Test access to 'user' account as user 'admin'.""" - self._attempt_succeed_update(vial.const.ROLE_USER) + self._attempt_succeed_update(hawat.const.ROLE_USER) - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_ADMIN) def test_11_as_admin_developer(self): """Test access to 'developer' account as user 'admin'.""" - self._attempt_succeed_update(vial.const.ROLE_DEVELOPER) + self._attempt_succeed_update(hawat.const.ROLE_DEVELOPER) - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_ADMIN) def test_12_as_admin_maintainer(self): """Test access to 'maintainer' account as user 'admin'.""" - self._attempt_succeed_update(vial.const.ROLE_MAINTAINER) + self._attempt_succeed_update(hawat.const.ROLE_MAINTAINER) -class UsersEnableDisableTestCase(UsersTestCaseMixin, TestRunnerMixin, VialTestCase): +class UsersEnableDisableTestCase(UsersTestCaseMixin, TestRunnerMixin, HawatTestCase): """Class for testing ``users.enable`` and ``users.disable`` endpoint.""" - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_USER) def test_01_as_user(self): """Test access as user 'user'.""" for uname in ( - vial.const.ROLE_USER, - vial.const.ROLE_DEVELOPER, - vial.const.ROLE_MAINTAINER, - vial.const.ROLE_ADMIN - ): + hawat.const.ROLE_USER, + hawat.const.ROLE_DEVELOPER, + hawat.const.ROLE_MAINTAINER, + hawat.const.ROLE_ADMIN + ): self._attempt_fail_disable(uname) self._attempt_fail_enable(uname) - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_DEVELOPER) def test_02_as_developer(self): """Test access as user 'developer'.""" for uname in ( - vial.const.ROLE_USER, - vial.const.ROLE_DEVELOPER, - vial.const.ROLE_MAINTAINER, - vial.const.ROLE_ADMIN - ): + hawat.const.ROLE_USER, + hawat.const.ROLE_DEVELOPER, + hawat.const.ROLE_MAINTAINER, + hawat.const.ROLE_ADMIN + ): self._attempt_fail_disable(uname) self._attempt_fail_enable(uname) - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_MAINTAINER) def test_03_as_maintainer(self): """Test access as user 'maintainer'.""" for uname in ( - vial.const.ROLE_USER, - vial.const.ROLE_DEVELOPER, - vial.const.ROLE_ADMIN - ): + hawat.const.ROLE_USER, + hawat.const.ROLE_DEVELOPER, + hawat.const.ROLE_ADMIN + ): self._attempt_succeed_disable(uname) self._attempt_succeed_enable(uname) - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_ADMIN) def test_04_as_admin(self): """Test access as user 'admin'.""" for uname in ( - vial.const.ROLE_USER, - vial.const.ROLE_DEVELOPER, - vial.const.ROLE_MAINTAINER - ): + hawat.const.ROLE_USER, + hawat.const.ROLE_DEVELOPER, + hawat.const.ROLE_MAINTAINER + ): self._attempt_succeed_disable(uname) self._attempt_succeed_enable(uname) -class UsersAddRemRejMembershipTestCase(TestRunnerMixin, VialTestCase): +class UsersAddRemRejMembershipTestCase(TestRunnerMixin, HawatTestCase): """Class for testing ``users.add_membership``, ``users.reject_membership`` and ``users.remove_membership`` endpoint.""" def _attempt_fail(self, uname, gname): @@ -396,7 +395,7 @@ class UsersAddRemRejMembershipTestCase(TestRunnerMixin, VialTestCase): 403 ) - def _attempt_succeed(self, uname, gname, print_response = False): + def _attempt_succeed(self, uname, gname, print_response=False): # Additional test preparations. with self.app.app_context(): uid = self.user_id(uname) @@ -501,96 +500,96 @@ class UsersAddRemRejMembershipTestCase(TestRunnerMixin, VialTestCase): ) self.mailbox_monitoring('off') - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_USER) def test_01_as_user(self): """Test access as user 'user'.""" for uname in ( - vial.const.ROLE_USER, - vial.const.ROLE_DEVELOPER, - vial.const.ROLE_MAINTAINER, - vial.const.ROLE_ADMIN - ): - self._attempt_fail(uname, vial.test.fixtures.DEMO_GROUP_A) - - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) + hawat.const.ROLE_USER, + hawat.const.ROLE_DEVELOPER, + hawat.const.ROLE_MAINTAINER, + hawat.const.ROLE_ADMIN + ): + self._attempt_fail(uname, hawat.test.fixtures.DEMO_GROUP_A) + + @hawat.test.do_as_user_decorator(hawat.const.ROLE_DEVELOPER) def test_02_as_developer(self): """Test access as user 'developer'.""" for uname in ( - vial.const.ROLE_USER, - vial.const.ROLE_MAINTAINER, - vial.const.ROLE_ADMIN - ): - self._attempt_succeed(uname, vial.test.fixtures.DEMO_GROUP_A) + hawat.const.ROLE_USER, + hawat.const.ROLE_MAINTAINER, + hawat.const.ROLE_ADMIN + ): + self._attempt_succeed(uname, hawat.test.fixtures.DEMO_GROUP_A) - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_MAINTAINER) def test_03_as_maintainer(self): """Test access as user 'maintainer'.""" for uname in ( - vial.const.ROLE_USER, - vial.const.ROLE_DEVELOPER, - vial.const.ROLE_ADMIN - ): - self._attempt_succeed(uname, vial.test.fixtures.DEMO_GROUP_A) + hawat.const.ROLE_USER, + hawat.const.ROLE_DEVELOPER, + hawat.const.ROLE_ADMIN + ): + self._attempt_succeed(uname, hawat.test.fixtures.DEMO_GROUP_A) - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_ADMIN) def test_04_as_admin(self): """Test access as user 'admin'.""" for uname in ( - vial.const.ROLE_USER, - vial.const.ROLE_DEVELOPER, - vial.const.ROLE_MAINTAINER - ): - self._attempt_succeed(uname, vial.test.fixtures.DEMO_GROUP_A) + hawat.const.ROLE_USER, + hawat.const.ROLE_DEVELOPER, + hawat.const.ROLE_MAINTAINER + ): + self._attempt_succeed(uname, hawat.test.fixtures.DEMO_GROUP_A) -class UsersDeleteTestCase(UsersTestCaseMixin, TestRunnerMixin, VialTestCase): +class UsersDeleteTestCase(UsersTestCaseMixin, TestRunnerMixin, HawatTestCase): """Class for testing ``users.delete`` endpoint.""" - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_USER) def test_01_as_user(self): """Test access as user 'user'.""" for uname in ( - vial.const.ROLE_USER, - vial.const.ROLE_DEVELOPER, - vial.const.ROLE_MAINTAINER, - vial.const.ROLE_ADMIN - ): + hawat.const.ROLE_USER, + hawat.const.ROLE_DEVELOPER, + hawat.const.ROLE_MAINTAINER, + hawat.const.ROLE_ADMIN + ): self._attempt_fail_delete(uname) - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_DEVELOPER) def test_02_as_developer(self): """Test access as user 'developer'.""" for uname in ( - vial.const.ROLE_USER, - vial.const.ROLE_DEVELOPER, - vial.const.ROLE_MAINTAINER, - vial.const.ROLE_ADMIN - ): + hawat.const.ROLE_USER, + hawat.const.ROLE_DEVELOPER, + hawat.const.ROLE_MAINTAINER, + hawat.const.ROLE_ADMIN + ): self._attempt_fail_delete(uname) - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_MAINTAINER) def test_03_as_maintainer(self): """Test access as user 'maintainer'.""" for uname in ( - vial.const.ROLE_USER, - vial.const.ROLE_DEVELOPER, - vial.const.ROLE_MAINTAINER, - vial.const.ROLE_ADMIN - ): + hawat.const.ROLE_USER, + hawat.const.ROLE_DEVELOPER, + hawat.const.ROLE_MAINTAINER, + hawat.const.ROLE_ADMIN + ): self._attempt_fail_delete(uname) - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_ADMIN) def test_04_as_admin(self): """Test access as user 'admin'.""" for uname in ( - vial.const.ROLE_USER, - vial.const.ROLE_DEVELOPER, - vial.const.ROLE_MAINTAINER - ): + hawat.const.ROLE_USER, + hawat.const.ROLE_DEVELOPER, + hawat.const.ROLE_MAINTAINER + ): self._attempt_succeed_delete(uname) -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- if __name__ == "__main__": diff --git a/lib/hawat/blueprints/users/test/utils.py b/lib/hawat/blueprints/users/test/utils.py index 66a81e800b5487938e098655d810e6496ba85ea2..804605a06bbb7ad092372af32ee1893523762228 100644 --- a/lib/hawat/blueprints/users/test/utils.py +++ b/lib/hawat/blueprints/users/test/utils.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ @@ -23,7 +23,7 @@ class UsersTestCaseMixin: 403 ) - def _attempt_succeed_list(self, content_checks = None): + def _attempt_succeed_list(self, content_checks=None): """Check access to ``users.list`` endpoint and succeed.""" if content_checks is None: content_checks = [ @@ -40,7 +40,7 @@ class UsersTestCaseMixin: def _attempt_fail_show(self, uname): """Check access to ``users.show`` endpoint and fail.""" - uid = self.user_id(uname, with_app_ctx = True) + uid = self.user_id(uname, with_app_ctx=True) self.assertGetURL( '/users/{}/show'.format(uid), 403 @@ -49,8 +49,8 @@ class UsersTestCaseMixin: def _attempt_succeed_show(self, uname): """Check access to ``users.show`` endpoint and succeed.""" with self.app.app_context(): - uobj = self.user_get(uname) - uid = uobj.id + uobj = self.user_get(uname) + uid = uobj.id ufname = uobj.fullname self.assertGetURL( '/users/{}/show'.format(uid), @@ -89,7 +89,7 @@ class UsersTestCaseMixin: def _attempt_fail_update(self, uname): """Check access to ``users.update`` endpoint and fail.""" - uid = self.user_id(uname, with_app_ctx = True) + uid = self.user_id(uname, with_app_ctx=True) self.assertGetURL( '/users/{}/update'.format(uid), 403 @@ -97,7 +97,7 @@ class UsersTestCaseMixin: def _attempt_succeed_update(self, uname): """Check access to ``users.update`` endpoint and succeed.""" - uid = self.user_id(uname, with_app_ctx = True) + uid = self.user_id(uname, with_app_ctx=True) self.assertGetURL( '/users/{}/update'.format(uid), 200, @@ -108,7 +108,7 @@ class UsersTestCaseMixin: def _attempt_fail_enable(self, uname): """Check access to ``users.enable`` endpoint and fail.""" - uid = self.user_id(uname, with_app_ctx = True) + uid = self.user_id(uname, with_app_ctx=True) self.assertGetURL( '/users/{}/enable'.format(uid), 403 @@ -116,7 +116,7 @@ class UsersTestCaseMixin: def _attempt_succeed_enable(self, uname): """Check access to ``users.enable`` endpoint and succeed.""" - uid = self.user_id(uname, with_app_ctx = True) + uid = self.user_id(uname, with_app_ctx=True) self.mailbox_monitoring('on') self.assertGetURL( '/users/{}/enable'.format(uid), @@ -155,7 +155,7 @@ class UsersTestCaseMixin: def _attempt_fail_disable(self, uname): """Check access to ``users.disable`` endpoint and fail.""" - uid = self.user_id(uname, with_app_ctx = True) + uid = self.user_id(uname, with_app_ctx=True) self.assertGetURL( '/users/{}/disable'.format(uid), 403 @@ -163,7 +163,7 @@ class UsersTestCaseMixin: def _attempt_succeed_disable(self, uname): """Check access to ``users.disable`` endpoint and succeed.""" - uid = self.user_id(uname, with_app_ctx = True) + uid = self.user_id(uname, with_app_ctx=True) self.assertGetURL( '/users/{}/disable'.format(uid), 200, @@ -184,7 +184,7 @@ class UsersTestCaseMixin: def _attempt_fail_delete(self, uname): """Check access to ``users.delete`` endpoint and fail.""" - uid = self.user_id(uname, with_app_ctx = True) + uid = self.user_id(uname, with_app_ctx=True) self.assertGetURL( '/users/{}/delete'.format(uid), 403 @@ -192,7 +192,7 @@ class UsersTestCaseMixin: def _attempt_succeed_delete(self, uname): """Check access to ``users.delete`` endpoint and succeed.""" - uid = self.user_id(uname, with_app_ctx = True) + uid = self.user_id(uname, with_app_ctx=True) self.assertGetURL( '/users/{}/delete'.format(uid), 200, diff --git a/lib/hawat/blueprints/whois/__init__.py b/lib/hawat/blueprints/whois/__init__.py index 6d4be189e57bea411268a7b382ea52e93bc440d4..b12887365cf3aa8cdcabddaecf0e6b9ed08ada8f 100644 --- a/lib/hawat/blueprints/whois/__init__.py +++ b/lib/hawat/blueprints/whois/__init__.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ @@ -47,11 +47,9 @@ Provided endpoints * *Methods:* ``GET``, ``POST`` """ - __author__ = "Jan Mach <jan.mach@cesnet.cz>" __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" - import flask from flask_babel import lazy_gettext @@ -59,16 +57,15 @@ import mentat.services.whois from mentat.const import tr_ import hawat.const -import vial.db -import vial.const -import vial.acl -from vial.app import VialBlueprint -from vial.view import RenderableView -from vial.view.mixin import HTMLMixin, AJAXMixin, SnippetMixin -from vial.utils import URLParamsBuilder +import hawat.db +import hawat.const +import hawat.acl +from hawat.base import HawatBlueprint +from hawat.view import RenderableView +from hawat.view.mixin import HTMLMixin, AJAXMixin, SnippetMixin +from hawat.utils import URLParamsBuilder from hawat.blueprints.whois.forms import WhoisSearchForm - BLUEPRINT_NAME = 'whois' """Name of the blueprint as module global constant.""" @@ -94,27 +91,27 @@ class AbstractSearchView(RenderableView): # pylint: disable=locally-disabled,ab Mandatory interface required by the :py:func:`flask.views.View.dispatch_request`. Will be called by the *Flask* framework to service the request. """ - form = WhoisSearchForm(flask.request.args, meta = {'csrf': False}) + form = WhoisSearchForm(flask.request.args, meta={'csrf': False}) - if vial.const.FORM_ACTION_SUBMIT in flask.request.args: + if hawat.const.FORM_ACTION_SUBMIT in flask.request.args: if form.validate(): form_data = form.data whois_manager = mentat.services.whois.WhoisServiceManager(flask.current_app.mconfig) whois_service = whois_manager.service() self.response_context.update( - search_item = form.search.data, - form_data = form_data + search_item=form.search.data, + form_data=form_data ) try: self.response_context.update( - search_result = whois_service.lookup(form.search.data) + search_result=whois_service.lookup(form.search.data) ) except Exception as exc: - self.flash(str(exc), level = 'error') + self.flash(str(exc), level='error') self.response_context.update( - search_form = form, - request_args = flask.request.args, + search_form=form, + request_args=flask.request.args, ) return self.generate_response() @@ -136,7 +133,7 @@ class APISearchView(AJAXMixin, AbstractSearchView): # pylint: disable=locally-d View responsible for querying local WHOIS service and presenting the results in the form of JSON document. """ - methods = ['GET','POST'] + methods = ['GET', 'POST'] @classmethod def get_view_name(cls): @@ -148,9 +145,9 @@ class SnippetSearchView(SnippetMixin, AbstractSearchView): # pylint: disable=lo View responsible for querying local WHOIS service and presenting the results in the form of JSON document containing ready to use HTML page snippets. """ - methods = ['GET', 'POST'] + methods = ['GET', 'POST'] - renders = ['label', 'full'] + renders = ['label', 'full'] snippets = [ { @@ -164,10 +161,10 @@ class SnippetSearchView(SnippetMixin, AbstractSearchView): # pylint: disable=lo return 'sptsearch' -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- -class WhoisBlueprint(VialBlueprint): +class WhoisBlueprint(HawatBlueprint): """Pluggable module - Local WHOIS service (*whois*).""" @classmethod @@ -178,8 +175,8 @@ class WhoisBlueprint(VialBlueprint): app.menu_main.add_entry( 'view', 'more.{}'.format(BLUEPRINT_NAME), - position = 50, - view = SearchView + position=50, + view=SearchView ) # Register context actions provided by this module. @@ -203,24 +200,24 @@ class WhoisBlueprint(VialBlueprint): ) -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- def get_blueprint(): """ - Mandatory interface for :py:mod:`vial.Vial` and factory function. This function - must return a valid instance of :py:class:`vial.app.VialBlueprint` or + Mandatory interface for :py:mod:`hawat.Hawat` and factory function. This function + must return a valid instance of :py:class:`hawat.app.HawatBlueprint` or :py:class:`flask.Blueprint`. """ hbp = WhoisBlueprint( BLUEPRINT_NAME, __name__, - template_folder = 'templates' + template_folder='templates' ) - hbp.register_view_class(SearchView, '/{}/search'.format(BLUEPRINT_NAME)) - hbp.register_view_class(APISearchView, '/api/{}/search'.format(BLUEPRINT_NAME)) + hbp.register_view_class(SearchView, '/{}/search'.format(BLUEPRINT_NAME)) + hbp.register_view_class(APISearchView, '/api/{}/search'.format(BLUEPRINT_NAME)) hbp.register_view_class(SnippetSearchView, '/snippet/{}/search'.format(BLUEPRINT_NAME)) return hbp diff --git a/lib/hawat/blueprints/whois/forms.py b/lib/hawat/blueprints/whois/forms.py index f1f0ad0fa00694b86cacc38a513eb7dc3d5a189e..cb539475a02b03c42128dad8badf2d0e99cca225 100644 --- a/lib/hawat/blueprints/whois/forms.py +++ b/lib/hawat/blueprints/whois/forms.py @@ -1,28 +1,26 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ This module contains custom internal whois resolving search form for Hawat. """ - __author__ = "Jan Mach <jan.mach@cesnet.cz>" __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" - import ipranges import wtforms import flask_wtf from flask_babel import gettext, lazy_gettext -import vial.const +import hawat.const def check_search_data(form, field): # pylint: disable=locally-disabled,unused-argument @@ -36,7 +34,7 @@ def check_search_data(form, field): # pylint: disable=locally-disabled,unused-a except ValueError: pass - if vial.const.CRE_EMAIL.match(field.data): + if hawat.const.CRE_EMAIL.match(field.data): return raise wtforms.validators.ValidationError( @@ -50,7 +48,7 @@ class WhoisSearchForm(flask_wtf.FlaskForm): """ search = wtforms.StringField( lazy_gettext('Search local WHOIS:'), - validators = [ + validators=[ wtforms.validators.DataRequired(), check_search_data ] diff --git a/lib/hawat/blueprints/whois/templates/whois/search.html b/lib/hawat/blueprints/whois/templates/whois/search.html index 4b37a91827a3fa788a9073ae87d6dc72c18f9db6..c743219b2b32a0cb38deb669cafa446630567d52 100644 --- a/lib/hawat/blueprints/whois/templates/whois/search.html +++ b/lib/hawat/blueprints/whois/templates/whois/search.html @@ -6,7 +6,7 @@ <div class="col-lg-12"> <div class="jumbotron" style="margin-top: 1em;"> - <h2>{{ vial_current_view.get_view_title() }}</h2> + <h2>{{ hawat_current_view.get_view_title() }}</h2> <form method="GET" class="form-inline" action="{{ url_for('whois.search') }}"> <div class="form-group{% if search_form.search.errors %}{{ ' has-error' }}{% endif %}"> {{ search_form.search.label(class_='sr-only') }} diff --git a/lib/hawat/blueprints/whois/test/__init__.py b/lib/hawat/blueprints/whois/test/__init__.py index 45a277590b44adbf856ffa995ed71e4d18280b03..1440ee1effe811426852b49b53bbcdf9ac2aae90 100644 --- a/lib/hawat/blueprints/whois/test/__init__.py +++ b/lib/hawat/blueprints/whois/test/__init__.py @@ -1,28 +1,27 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ Unit tests for :py:mod:`hawat.blueprints.whois`. """ - import unittest -import vial.const -import vial.test -import vial.db -from vial.test import VialTestCase +import hawat.const +import hawat.test +import hawat.db +from hawat.test import HawatTestCase from hawat.test.runner import TestRunnerMixin -class SearchTestCase(TestRunnerMixin, VialTestCase): +class SearchTestCase(TestRunnerMixin, HawatTestCase): """ Class for testing ``whois.search`` endpoint. """ @@ -35,7 +34,7 @@ class SearchTestCase(TestRunnerMixin, VialTestCase): b'Redirecting...', b'login?next=' ], - follow_redirects = False + follow_redirects=False ) def _attempt_succeed(self): @@ -51,28 +50,28 @@ class SearchTestCase(TestRunnerMixin, VialTestCase): """Test access as anonymous user.""" self._attempt_fail() - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_USER) def test_02_as_user(self): """Test access as user ``user``.""" self._attempt_succeed() - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_DEVELOPER) def test_03_as_developer(self): """Test access as user ``developer``.""" self._attempt_succeed() - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_MAINTAINER) def test_04_as_maintainer(self): """Test access as user ``maintainer``.""" self._attempt_succeed() - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_ADMIN) def test_05_as_admin(self): """Test access as user ``admin``.""" self._attempt_succeed() -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- if __name__ == "__main__": diff --git a/lib/vial/command.py b/lib/hawat/command.py similarity index 63% rename from lib/vial/command.py rename to lib/hawat/command.py index 8ee109df5b60491970138da5d66279b8d237e2b5..266798d8c5f64413e37786cf893714703d89a9f8 100644 --- a/lib/vial/command.py +++ b/lib/hawat/command.py @@ -1,17 +1,19 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- +# This file is part of Mentat system (https://mentat.cesnet.cz/). +# +# Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ -This module contains custom commands for ``vial-cli`` command line interface. +This module contains custom commands for ``hawat-cli`` command line interface. """ - -__author__ = "Honza Mach <honza.mach.ml@gmail.com>" - +__author__ = "Jan Mach <jan.mach@cesnet.cz>" +__credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" import re import sys @@ -23,14 +25,15 @@ import click import flask from flask.cli import AppGroup -import vial.const -import vial.db +import hawat.const +import hawat.db def account_exists(func): """ - Decorator: Catch SQLAlchemy exceptions for non existing user accounts. + Decorator: Catch SQLAlchemy exceptions for non-existing user accounts. """ + @functools.wraps(func) def wrapper_account_exists(login, *args, **kwargs): try: @@ -38,56 +41,59 @@ def account_exists(func): except sqlalchemy.orm.exc.NoResultFound: click.secho( "[FAIL] User account '{}' was not found.".format(login), - fg = 'red' + fg='red' ) except Exception: # pylint: disable=locally-disabled,broad-except - vial.db.db_session().rollback() + hawat.db.db_session().rollback() click.echo( traceback.TracebackException(*sys.exc_info()) ) + return wrapper_account_exists -def validate_email(ctx, param, value): +def validate_email(_ctx, _param, value): """Validate ``login/email`` command line parameter.""" if value: - if vial.const.CRE_EMAIL.match(value): + if hawat.const.CRE_EMAIL.match(value): return value raise click.BadParameter( "Value '{}' does not look like valid email address.".format(value) ) -def validate_roles(ctx, param, value): + +def validate_roles(_ctx, _param, value): """Validate ``role`` command line parameter.""" if value: for val in value: - if val not in vial.const.ROLES: + if val not in hawat.const.ROLES: raise click.BadParameter( "Value '{}' does not look like valid user role.".format(val) ) return value -user_cli = AppGroup('users', help = "User account management module.") +user_cli = AppGroup('users', help="User account management module.") + @user_cli.command('create') -@click.argument('login', callback = validate_email) +@click.argument('login', callback=validate_email) @click.argument('fullname') -@click.option('--email', callback = validate_email, help = 'Optional email, login will be used as default') +@click.option('--email', callback=validate_email, help='Optional email, login will be used as default') @click.password_option() -@click.option('--enabled/--no-enabled', default = False) -@click.option('--role', callback = validate_roles, help = 'Role to be assigned to the user (multiple)', multiple = True) +@click.option('--enabled/--no-enabled', default=False) +@click.option('--role', callback=validate_roles, help='Role to be assigned to the user (multiple)', multiple=True) def users_create(login, fullname, email, password, enabled, role): """Create new user account.""" - user_model = flask.current_app.get_model(vial.const.MODEL_USER) + user_model = flask.current_app.get_model(hawat.const.MODEL_USER) sqlobj = user_model() - sqlobj.login = login + sqlobj.login = login sqlobj.fullname = fullname - sqlobj.email = email or login - sqlobj.roles = role or [vial.const.ROLE_USER] - sqlobj.enabled = enabled + sqlobj.email = email or login + sqlobj.roles = role or [hawat.const.ROLE_USER] + sqlobj.enabled = enabled if password: sqlobj.set_password(password) @@ -99,15 +105,15 @@ def users_create(login, fullname, email, password, enabled, role): click.echo(" - Enabled: {}".format(sqlobj.enabled)) click.echo(" - Password: {}".format(sqlobj.password)) try: - vial.db.db_session().add(sqlobj) - vial.db.db_session().commit() + hawat.db.db_session().add(sqlobj) + hawat.db.db_session().commit() click.secho( "[OK] User account was successfully created", - fg = 'green' + fg='green' ) except sqlalchemy.exc.IntegrityError as exc: - vial.db.db_session().rollback() + hawat.db.db_session().rollback() match = re.search(r'Key \((\w+)\)=\(([^)]+)\) already exists.', str(exc)) if match: click.secho( @@ -115,32 +121,33 @@ def users_create(login, fullname, email, password, enabled, role): match.group(1), match.group(2) ), - fg = 'red' + fg='red' ) else: click.secho( "[FAIL] There already is an user account with similar data.", - fg = 'red' + fg='red' ) click.secho( "\n{}".format(exc), - fg = 'blue' + fg='blue' ) except Exception: # pylint: disable=locally-disabled,broad-except - vial.db.db_session().rollback() + hawat.db.db_session().rollback() click.echo( traceback.TracebackException(*sys.exc_info()) ) + @user_cli.command('passwd') -@click.argument('login', callback = validate_email) +@click.argument('login', callback=validate_email) @click.password_option() @account_exists def users_passwd(login, password): """Change/set password to given user account.""" - user_model = flask.current_app.get_model(vial.const.MODEL_USER) - item = vial.db.db_session().query( + user_model = flask.current_app.get_model(hawat.const.MODEL_USER) + item = hawat.db.db_session().query( user_model ).filter( user_model.login == login @@ -149,25 +156,25 @@ def users_passwd(login, password): if password: click.echo("Setting password for user account '{}'".format(login)) item.set_password(password) - vial.db.db_session().add(item) - vial.db.db_session().commit() + hawat.db.db_session().add(item) + hawat.db.db_session().commit() click.secho( "[OK] User account was successfully updated", - fg = 'green' + fg='green' ) @user_cli.command('roleadd') -@click.argument('login', callback = validate_email) -@click.argument('role', callback = validate_roles, nargs = -1) +@click.argument('login', callback=validate_email) +@click.argument('role', callback=validate_roles, nargs=-1) @account_exists def users_roleadd(login, role): """Add role(s) to given user account.""" if not role: return click.echo("Adding roles '{}' to user account '{}'".format(','.join(role), login)) - user_model = flask.current_app.get_model(vial.const.MODEL_USER) - item = vial.db.db_session().query( + user_model = flask.current_app.get_model(hawat.const.MODEL_USER) + item = hawat.db.db_session().query( user_model ).filter( user_model.login == login @@ -178,22 +185,23 @@ def users_roleadd(login, role): current_roles.add(i) item.roles = list(current_roles) - vial.db.db_session().add(item) - vial.db.db_session().commit() + hawat.db.db_session().add(item) + hawat.db.db_session().commit() click.secho( "[OK] User account was successfully updated", - fg = 'green' + fg='green' ) + @user_cli.command('roledel') -@click.argument('login', callback = validate_email) -@click.argument('role', callback = validate_roles, nargs = -1) +@click.argument('login', callback=validate_email) +@click.argument('role', callback=validate_roles, nargs=-1) @account_exists def users_roledel(login, role): """Delete role(s) to given user account.""" click.echo("Deleting roles '{}' from user account '{}'".format(','.join(role), login)) - user_model = flask.current_app.get_model(vial.const.MODEL_USER) - item = vial.db.db_session().query( + user_model = flask.current_app.get_model(hawat.const.MODEL_USER) + item = hawat.db.db_session().query( user_model ).filter( user_model.login == login @@ -207,21 +215,22 @@ def users_roledel(login, role): pass item.roles = list(current_roles) - vial.db.db_session().add(item) - vial.db.db_session().commit() + hawat.db.db_session().add(item) + hawat.db.db_session().commit() click.secho( "[OK] User account was successfully updated", - fg = 'green' + fg='green' ) + @user_cli.command('enable') -@click.argument('login', callback = validate_email) +@click.argument('login', callback=validate_email) @account_exists def users_enable(login): """Enable given user account.""" click.echo("Enabling user account '{}'".format(login)) - user_model = flask.current_app.get_model(vial.const.MODEL_USER) - item = vial.db.db_session().query( + user_model = flask.current_app.get_model(hawat.const.MODEL_USER) + item = hawat.db.db_session().query( user_model ).filter( user_model.login == login @@ -230,26 +239,27 @@ def users_enable(login): if not item.enabled: item.enabled = True - vial.db.db_session().add(item) - vial.db.db_session().commit() + hawat.db.db_session().add(item) + hawat.db.db_session().commit() click.secho( "[OK] User account was successfully enabled", - fg = 'green' + fg='green' ) else: click.secho( "[OK] User account was already enabled", - fg = 'green' + fg='green' ) + @user_cli.command('disable') -@click.argument('login', callback = validate_email) +@click.argument('login', callback=validate_email) @account_exists def users_disable(login): """Disable given user account.""" click.echo("Disabling user account '{}'".format(login)) - user_model = flask.current_app.get_model(vial.const.MODEL_USER) - item = vial.db.db_session().query( + user_model = flask.current_app.get_model(hawat.const.MODEL_USER) + item = hawat.db.db_session().query( user_model ).filter( user_model.login == login @@ -258,44 +268,46 @@ def users_disable(login): if item.enabled: item.enabled = False - vial.db.db_session().add(item) - vial.db.db_session().commit() + hawat.db.db_session().add(item) + hawat.db.db_session().commit() click.secho( "[OK] User account was successfully disabled", - fg = 'green' + fg='green' ) else: click.secho( "[OK] User account was already disabled", - fg = 'green' + fg='green' ) + @user_cli.command('delete') -@click.argument('login', callback = validate_email) +@click.argument('login', callback=validate_email) @account_exists def users_delete(login): """Delete existing user account.""" click.echo("Deleting user account '{}'".format(login)) - user_model = flask.current_app.get_model(vial.const.MODEL_USER) - item = vial.db.db_session().query( + user_model = flask.current_app.get_model(hawat.const.MODEL_USER) + item = hawat.db.db_session().query( user_model ).filter( user_model.login == login ).one() - vial.db.db_session().delete(item) - vial.db.db_session().commit() + hawat.db.db_session().delete(item) + hawat.db.db_session().commit() click.secho( "[OK] User account was successfully deleted", - fg = 'green' + fg='green' ) + @user_cli.command('list') def users_list(): """List all available user accounts.""" try: - user_model = flask.current_app.get_model(vial.const.MODEL_USER) - items = vial.db.db_session().query(user_model).all() + user_model = flask.current_app.get_model(hawat.const.MODEL_USER) + items = hawat.db.db_session().query(user_model).all() if items: click.echo("List of existing user accounts:") for item in items: @@ -304,13 +316,13 @@ def users_list(): click.echo("There are currently no user accounts in the database.") except Exception: # pylint: disable=locally-disabled,broad-except - vial.db.db_session().rollback() + hawat.db.db_session().rollback() click.echo( traceback.TracebackException(*sys.exc_info()) ) -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- def setup_cli(app): diff --git a/lib/hawat/config.py b/lib/hawat/config.py index feab16ad2c9ee3d7fcf52384cbd4a9b27266528f..1afefe65d110440e4c3598f039c3423140bc15d1 100644 --- a/lib/hawat/config.py +++ b/lib/hawat/config.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ @@ -33,49 +33,91 @@ names to configuration classess: :py:const:`CONFIG_MAP` -It is used from inside of :py:func:`hawat.app.create_app` factory method to pick +It is used from inside :py:func:`hawat.app.create_app` factory method to pick and apply correct configuration class to application. Please refer to the documentation of :py:func:`hawat.app.create_app` factory function for more details on this process. """ - __author__ = "Jan Mach <jan.mach@cesnet.cz>" __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" - import os import socket +import collections from flask_babel import lazy_gettext import pyzenkit.jsonconf import pyzenkit.utils -import vial.config - import mentat.const from mentat.datatype.sqldb import UserModel, GroupModel, ItemChangeLogModel +import hawat.const + -class Config(vial.config.Config): # pylint: disable=locally-disabled,too-few-public-methods +class Config: # pylint: disable=locally-disabled,too-few-public-methods """ Base class for default configurations of Hawat application. You are free to extend and customize contents of this class to provide better default values for your particular environment. - The configuration keys must be a valid Flask configuration and so they must + The configuration keys must be a valid Flask configuration, and so they must be written in UPPERCASE to be correctly recognized. """ APPLICATION_NAME = "Mentat" - APPLICATION_ID = "mentat" + APPLICATION_ID = "mentat" + + # --------------------------------------------------------------------------- + # Flask internal configurations. Please refer to Flask documentation for + # more information about each configuration key. + # --------------------------------------------------------------------------- + + DEBUG = False + TESTING = False + SECRET_KEY = 'default-secret-key' + + # --------------------------------------------------------------------------- + # Flask extension configurations. Please refer to the documentation of that + # particular Flask extension for more details. + # --------------------------------------------------------------------------- + + # + # Flask-WTF configurations. + # + WTF_CSRF_ENABLED = True # # Flask-Mail configurations. # + MAIL_SERVER = 'localhost' + MAIL_PORT = 25 + MAIL_USERNAME = None + MAIL_PASSWORD = None MAIL_DEFAULT_SENDER = '{}@{}'.format(APPLICATION_ID, socket.getfqdn()) MAIL_SUBJECT_PREFIX = '[{}]'.format(APPLICATION_NAME) + DISABLE_MAIL_LOGGING = False + + # + # Flask-Babel configurations. + # + BABEL_DEFAULT_LOCALE = hawat.const.DEFAULT_LOCALE + BABEL_DEFAULT_TIMEZONE = hawat.const.DEFAULT_TIMEZONE + BABEL_DETECT_LOCALE = True + """Custom configuration, make detection of best possible locale optional to enable forcing default.""" + + # + # Flask-SQLAlchemy configurations. + # + SQLALCHEMY_TRACK_MODIFICATIONS = False + SQLALCHEMY_SETUP_ARGS = { + 'metadata': mentat.datatype.sqldb.MODEL.metadata, + 'model_class': mentat.datatype.sqldb.MODEL, + 'query_class': mentat.services.sqlstorage.RetryingQuery + } + # # Flask-Migrate configurations. # @@ -86,26 +128,32 @@ class Config(vial.config.Config): # pylint: disable=locally-disabled,too-few-pu 'migrations' ) - #--------------------------------------------------------------------------- + # --------------------------------------------------------------------------- # Custom application configurations. - #--------------------------------------------------------------------------- + # --------------------------------------------------------------------------- - ROLES = vial.const.ROLES + ROLES = hawat.const.ROLES """List of all valid user roles supported by the application.""" MODELS = { - vial.const.MODEL_USER: UserModel, - vial.const.MODEL_GROUP: GroupModel, - vial.const.MODEL_ITEM_CHANGELOG: ItemChangeLogModel + hawat.const.MODEL_USER: UserModel, + hawat.const.MODEL_GROUP: GroupModel, + hawat.const.MODEL_ITEM_CHANGELOG: ItemChangeLogModel } """Models to be used within the application.""" + SUPPORTED_LOCALES = collections.OrderedDict([ + ('en', 'English'), + ('cs', 'ÄŒesky') + ]) + """List of all languages (locales) supported by the application.""" + ENABLED_BLUEPRINTS = [ - 'vial.blueprints.auth', - 'vial.blueprints.auth_api', - 'vial.blueprints.design_bs3', - 'vial.blueprints.devtools', - 'vial.blueprints.changelogs', + 'hawat.blueprints.auth', + 'hawat.blueprints.auth_api', + 'hawat.blueprints.design_bs3', + 'hawat.blueprints.devtools', + 'hawat.blueprints.changelogs', 'hawat.blueprints.auth_env', 'hawat.blueprints.auth_pwd', @@ -115,9 +163,9 @@ class Config(vial.config.Config): # pylint: disable=locally-disabled,too-few-pu 'hawat.blueprints.hosts', 'hawat.blueprints.timeline', 'hawat.blueprints.dnsr', - #'hawat.blueprints.pdnsr', + # 'hawat.blueprints.pdnsr', 'hawat.blueprints.geoip', - #'hawat.blueprints.nerd', + # 'hawat.blueprints.nerd', 'hawat.blueprints.whois', 'hawat.blueprints.performance', 'hawat.blueprints.status', @@ -133,6 +181,24 @@ class Config(vial.config.Config): # pylint: disable=locally-disabled,too-few-pu DISABLED_ENDPOINTS = [] """List of endpoints disabled on application level.""" + ENDPOINT_LOGIN = 'auth.login' + """ + Default login view. Users will be redirected to this view in case they are not + authenticated, but the authentication is required for the requested endpoint. + """ + + LOGIN_MSGCAT = 'info' + """Default message category for messages related to user authentication.""" + + ENDPOINT_HOME = 'home.index' + """Homepage endpoint.""" + + ENDPOINT_LOGIN_REDIRECT = 'home.index' + """Default redirection endpoint after login.""" + + ENDPOINT_LOGOUT_REDIRECT = 'home.index' + """Default redirection endpoint after logout.""" + MENU_MAIN_SKELETON = [ { 'entry_type': 'submenu', @@ -173,6 +239,9 @@ class Config(vial.config.Config): # pylint: disable=locally-disabled,too-few-pu ] """Configuration of application menu skeleton.""" + EMAIL_ADMINS = ['root@{}'.format(socket.getfqdn())] + """List of system administrator emails.""" + HAWAT_REPORT_FEEDBACK_MAILS = ['root@{}'.format(socket.getfqdn())] """List of system administrator emails, that receive feedback messages for reports.""" @@ -185,32 +254,56 @@ class Config(vial.config.Config): # pylint: disable=locally-disabled,too-few-pu HAWAT_SEARCH_QUERY_QUOTA = 7 """Event search query quota per each user.""" - SQLALCHEMY_SETUP_ARGS = { - 'metadata': mentat.datatype.sqldb.MODEL.metadata, - 'model_class': mentat.datatype.sqldb.MODEL, - 'query_class': mentat.services.sqlstorage.RetryingQuery - } + LOG_DEFAULT_LEVEL = 'info' + """ + Default logging level, case insensitive. + One of the values ``DEBUG``, ``INFO``, ``WARNING``, ``ERROR``, ``CRITICAL``. + """ + + LOG_FILE_LEVEL = 'info' + """ + File logging level, case insensitive. + One of the values ``DEBUG``, ``INFO``, ``WARNING``, ``ERROR``, ``CRITICAL``. + """ + + LOG_EMAIL_LEVEL = 'error' + """ + File logging level, case insensitive. + One of the values ``DEBUG``, ``INFO``, ``WARNING``, ``ERROR``, ``CRITICAL``. + """ + + ICONS = hawat.const.ICONS -class ProductionConfig(Config, vial.config.ProductionConfig): # pylint: disable=locally-disabled,too-few-public-methods +class ProductionConfig(Config): # pylint: disable=locally-disabled,too-few-public-methods """ Class containing application configurations for *production* environment. """ -class DevelopmentConfig(Config, vial.config.DevelopmentConfig): # pylint: disable=locally-disabled,too-few-public-methods +class DevelopmentConfig(Config): # pylint: disable=locally-disabled,too-few-public-methods """ Class containing application configurations for *development* environment. """ - #EXPLAIN_TEMPLATE_LOADING = True + DEBUG = True + + # EXPLAIN_TEMPLATE_LOADING = True + + # DEBUG_TB_PROFILER_ENABLED = True + + # --------------------------------------------------------------------------- + # Custom application configurations. + # --------------------------------------------------------------------------- + + ENDPOINT_LOGIN = 'auth_dev.login' ENABLED_BLUEPRINTS = [ - 'vial.blueprints.auth', - 'vial.blueprints.auth_api', - 'vial.blueprints.design_bs3', - 'vial.blueprints.devtools', - 'vial.blueprints.changelogs', + 'hawat.blueprints.auth', + 'hawat.blueprints.auth_api', + 'hawat.blueprints.design_bs3', + 'hawat.blueprints.devtools', + 'hawat.blueprints.changelogs', 'hawat.blueprints.auth_env', 'hawat.blueprints.auth_dev', @@ -221,9 +314,9 @@ class DevelopmentConfig(Config, vial.config.DevelopmentConfig): # pylint: disab 'hawat.blueprints.hosts', 'hawat.blueprints.timeline', 'hawat.blueprints.dnsr', - #'hawat.blueprints.pdnsr', + # 'hawat.blueprints.pdnsr', 'hawat.blueprints.geoip', - #'hawat.blueprints.nerd', + # 'hawat.blueprints.nerd', 'hawat.blueprints.whois', 'hawat.blueprints.performance', 'hawat.blueprints.status', @@ -235,17 +328,32 @@ class DevelopmentConfig(Config, vial.config.DevelopmentConfig): # pylint: disab 'hawat.blueprints.networks', ] + LOG_DEFAULT_LEVEL = 'debug' + + LOG_FILE_LEVEL = 'debug' + -class TestingConfig(Config, vial.config.TestingConfig): # pylint: disable=locally-disabled,too-few-public-methods +class TestingConfig(Config): # pylint: disable=locally-disabled,too-few-public-methods """ Class containing *testing* Hawat applications` configurations. """ + + TESTING = True + + EXPLAIN_TEMPLATE_LOADING = False + + # --------------------------------------------------------------------------- + # Custom application configurations. + # --------------------------------------------------------------------------- + + ENDPOINT_LOGIN = 'auth_dev.login' + ENABLED_BLUEPRINTS = [ - 'vial.blueprints.auth', - 'vial.blueprints.auth_api', - 'vial.blueprints.design_bs3', - 'vial.blueprints.devtools', - 'vial.blueprints.changelogs', + 'hawat.blueprints.auth', + 'hawat.blueprints.auth_api', + 'hawat.blueprints.design_bs3', + 'hawat.blueprints.devtools', + 'hawat.blueprints.changelogs', 'hawat.blueprints.auth_env', 'hawat.blueprints.auth_dev', @@ -256,9 +364,9 @@ class TestingConfig(Config, vial.config.TestingConfig): # pylint: disable=local 'hawat.blueprints.hosts', 'hawat.blueprints.timeline', 'hawat.blueprints.dnsr', - #'hawat.blueprints.pdnsr', + # 'hawat.blueprints.pdnsr', 'hawat.blueprints.geoip', - #'hawat.blueprints.nerd', + # 'hawat.blueprints.nerd', 'hawat.blueprints.whois', 'hawat.blueprints.performance', 'hawat.blueprints.status', @@ -273,9 +381,9 @@ class TestingConfig(Config, vial.config.TestingConfig): # pylint: disable=local CONFIG_MAP = { 'development': DevelopmentConfig, - 'production': ProductionConfig, - 'testing': TestingConfig, - 'default': ProductionConfig + 'production': ProductionConfig, + 'testing': TestingConfig, + 'default': ProductionConfig } """Configuration map for easy mapping of configuration aliases to config objects.""" @@ -308,6 +416,7 @@ def get_app_root_relative_config(): ) } + def get_default_config_file(): """ Get path to default configuration file based on the environment. diff --git a/lib/hawat/const.py b/lib/hawat/const.py index 8dd422633e9aac521b2e21dfeba98e89e344dc8c..88823315b846185d57be41a24cb3d6b4ef17ccd7 100644 --- a/lib/hawat/const.py +++ b/lib/hawat/const.py @@ -1,22 +1,173 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ This module contains global application-wide constants for Hawat user interface. """ - __author__ = "Jan Mach <jan.mach@cesnet.cz>" __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" +import re +import datetime + + +def tr_(val): + """ + Method for marking translatable strings according to the documentation + recipe at https://docs.python.org/3/library/gettext.html#deferred-translations. + """ + return val + + +CRE_LOGIN = re.compile('^[-_@.a-zA-Z0-9]+$') +"""Compiled regular expression for login validation.""" + +CRE_EMAIL = re.compile(r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$") +"""Compiled regular expression for email address format validation.""" + +CRE_COUNTRY_CODE = re.compile('^[a-zA-Z]{2,3}$') +"""Compiled regular expression for validating language/country codes.""" + +CRE_LANG_CODE = re.compile('^[a-zA-Z]{2}(_[a-zA-Z]{2})?$') +"""Compiled regular expression for validating language codes.""" + +DEFAULT_LOCALE = 'en' +"""Default application locale.""" + +DEFAULT_TIMEZONE = 'UTC' +"""Default application timezone.""" + +FLASH_INFO = 'info' +"""Class for *info* flash messages.""" + +FLASH_SUCCESS = 'success' +"""Class for *success* flash messages.""" + +FLASH_WARNING = 'warning' +"""Class for *warning* flash messages.""" + +FLASH_FAILURE = 'danger' +"""Class for *failure* flash messages.""" + +FORM_ACTION_SUBMIT = 'submit' +"""Name of the item form *submit* button.""" + +FORM_ACTION_CANCEL = 'cancel' +"""Name of the item form *cancel* button.""" + +ACTION_ITEM_CREATE = 'create' +"""Name of the item *create* action.""" + +ACTION_ITEM_CREATEFOR = 'createfor' +"""Name of the item *createfor* action.""" + +ACTION_ITEM_UPDATE = 'update' +"""Name of the item *update* action.""" + +ACTION_ITEM_ENABLE = 'enable' +"""Name of the item *enable* action.""" + +ACTION_ITEM_DISABLE = 'disable' +"""Name of the item *disable* action.""" + +ACTION_ITEM_DELETE = 'delete' +"""Name of the item *delete* action.""" + +ACTION_USER_LOGIN = 'login' +"""Name of the user *login* action.""" + +ACTION_USER_LOGOUT = 'logout' +"""Name of the user *logout* action.""" + +ACTION_USER_REGISTER = 'register' +"""Name of the user *register* action.""" + +MODEL_USER = 'user' +"""Name of the 'user' model.""" +MODEL_GROUP = 'group' +"""Name of the 'user' model.""" + +MODEL_ITEM_CHANGELOG = 'item_changelog' +"""Name of the 'user' model.""" + +ROLE_USER = 'user' +"""Name of the 'user' role.""" + +ROLE_DEVELOPER = 'developer' +"""Name of the 'developer' role.""" + +ROLE_MAINTAINER = 'maintainer' +"""Name of the 'maintainer' role.""" + +ROLE_ADMIN = 'admin' +"""Name of the 'admin' role.""" + +ROLES = [ + ROLE_USER, + ROLE_DEVELOPER, + ROLE_MAINTAINER, + ROLE_ADMIN +] +"""List of valid user roles.""" + +NO_ROLE = '__NO_ROLE__' +"""Special constant for selecting users with no roles.""" + +CFGKEY_MENU_MAIN_SKELETON = 'MENU_MAIN_SKELETON' +"""Configuration key name: Default application main menu skeleton.""" + +CFGKEY_ENABLED_BLUEPRINTS = 'ENABLED_BLUEPRINTS' +"""Configuration key name: List of all requested blueprints.""" + +CFGKEY_MODELS = 'MODELS' +"""Configuration key name: List of all requested blueprints.""" + +DEFAULT_PAGER_LIMIT = 100 +"""Default page limit for pager/paginator.""" + +PAGER_LIMIT_CHOICES = [ + (5, 5), + (10, 10), + (20, 20), + (25, 25), + (50, 50), + (100, 100), + (200, 200), + (250, 250), + (500, 500), + (1000, 1000), + (2500, 2500), + (5000, 5000), + (10000, 10000), + (25000, 25000), + (50000, 50000), + (100000, 100000) +] +"""List of available valid pager limit choices.""" + +DEFAULT_RESULT_TIMEDELTA = 7 +"""Default result time delta for searching various objects.""" + +RESOURCE_BABEL = 'babel' +"""Name for the ``flask_babel.Babel`` object within the application resources.""" + +RESOURCE_LOGIN_MANAGER = 'login_manager' +"""Name for the ``flask_login.LoginManager`` object within the application resources.""" + +RESOURCE_MIGRATE = 'migrate' +"""Name for the ``flask_migrate.Migrate`` object within the application resources.""" + +RESOURCE_PRINCIPAL = 'principal' +"""Name for the ``flask_principal.Principal`` object within the application resources.""" CFGKEY_MENTAT_CORE = 'MENTAT_CORE' """Configuration key name: Core Mentat configurations.""" @@ -24,20 +175,18 @@ CFGKEY_MENTAT_CORE = 'MENTAT_CORE' CFGKEY_MENTAT_CACHE_DIR = 'MENTAT_CACHE_DIR' """Configuration key name: Path to Mentat cache dir.""" -DEFAULT_RESULT_TIMEDELTA = 7 -"""Default result time delta for searching various objects.""" # # List of all existing Context Search Action Group names (CSAG). # -CSAG_ABUSE = 'abuses' -CSAG_ADDRESS = 'ips' +CSAG_ABUSE = 'abuses' +CSAG_ADDRESS = 'ips' CSAG_CATEGORY = 'categories' -CSAG_CLASS = 'classes' +CSAG_CLASS = 'classes' CSAG_DETECTOR = 'detectors' -CSAG_DETTYPE = 'detector_types' +CSAG_DETTYPE = 'detector_types' CSAG_HOSTTYPE = 'host_types' -CSAG_PORT = 'ports' +CSAG_PORT = 'ports' CSAG_PROTOCOL = 'protocols' CSAG_SEVERITY = 'severities' @@ -47,63 +196,65 @@ CSAG_SEVERITY = 'severities' AODS_IP4 = 'ip4' AODS_IP6 = 'ip6' +ICON_NAME_MISSING_ICON = 'missing-icon' +"""Name of the icon to display instead of missing icons.""" ICONS = { # # General icons. # - 'login': '<i class="fas fa-fw fa-sign-in-alt"></i>', - 'logout': '<i class="fas fa-fw fa-sign-out-alt"></i>', - 'register': '<i class="fas fa-fw fa-user-plus"></i>', - 'help': '<i class="fas fa-fw fa-question-circle"></i>', - 'language': '<i class="fas fa-fw fa-globe"></i>', - - 'role-anonymous': '<i class="fas fa-fw fa-user-secret"></i>', - 'role-user': '<i class="fas fa-fw fa-user"></i>', - 'role-developer': '<i class="fas fa-fw fa-user-md"></i>', + 'login': '<i class="fas fa-fw fa-sign-in-alt"></i>', + 'logout': '<i class="fas fa-fw fa-sign-out-alt"></i>', + 'register': '<i class="fas fa-fw fa-user-plus"></i>', + 'help': '<i class="fas fa-fw fa-question-circle"></i>', + 'language': '<i class="fas fa-fw fa-globe"></i>', + + 'role-anonymous': '<i class="fas fa-fw fa-user-secret"></i>', + 'role-user': '<i class="fas fa-fw fa-user"></i>', + 'role-developer': '<i class="fas fa-fw fa-user-md"></i>', 'role-maintainer': '<i class="fas fa-fw fa-user-tie"></i>', - 'role-admin': '<i class="fas fa-fw fa-user-ninja"></i>', + 'role-admin': '<i class="fas fa-fw fa-user-ninja"></i>', # # Main site section icons. # - 'section-home': '<i class="fas fa-fw fa-home"></i>', - 'section-dashboards': '<i class="fas fa-fw fa-tachometer-alt"></i>', - 'section-more': '<i class="fas fa-fw fa-puzzle-piece"></i>', + 'section-home': '<i class="fas fa-fw fa-home"></i>', + 'section-dashboards': '<i class="fas fa-fw fa-tachometer-alt"></i>', + 'section-more': '<i class="fas fa-fw fa-puzzle-piece"></i>', 'section-administration': '<i class="fas fa-fw fa-cogs"></i>', - 'section-development': '<i class="fas fa-fw fa-bug"></i>', + 'section-development': '<i class="fas fa-fw fa-bug"></i>', # # Built-in module icons. # - 'module-home': '<i class="fas fa-fw fa-home"></i>', - 'module-auth-api': '<i class="fas fa-fw fa-id-card-alt"></i>', - 'module-auth-dev': '<i class="fas fa-fw fa-id-card-alt"></i>', - 'module-auth-env': '<i class="fas fa-fw fa-id-card-alt"></i>', - 'module-changelogs': '<i class="fas fa-fw fa-clipboard-list"></i>', - 'module-dashboards': '<i class="fas fa-fw fa-tachometer-alt"></i>', - 'module-dbstatus': '<i class="fas fa-fw fa-database"></i>', - 'module-design': '<i class="fas fa-fw fa-palette"></i>', - 'module-devtools': '<i class="fas fa-fw fa-bug"></i>', - 'module-dnsr': '<i class="fas fa-fw fa-directions"></i>', - 'module-events': '<i class="fas fa-fw fa-bell"></i>', - 'module-filters': '<i class="fas fa-fw fa-filter"></i>', - 'module-geoip': '<i class="fas fa-fw fa-map-marked-alt"></i>', - 'module-groups': '<i class="fas fa-fw fa-users"></i>', - 'module-help': '<i class="fas fa-fw fa-question-circle"></i>', - 'module-hosts': '<i class="fas fa-fw fa-server"></i>', - 'module-networks': '<i class="fas fa-fw fa-sitemap"></i>', - 'module-nerd': '<i class="fas fa-fw fa-certificate"></i>', - 'module-pdnsr': '<i class="fas fa-fw fa-compass"></i>', - 'module-performance': '<i class="fas fa-fw fa-chart-bar"></i>', - 'module-reports': '<i class="fas fa-fw fa-newspaper"></i>', + 'module-home': '<i class="fas fa-fw fa-home"></i>', + 'module-auth-api': '<i class="fas fa-fw fa-id-card-alt"></i>', + 'module-auth-dev': '<i class="fas fa-fw fa-id-card-alt"></i>', + 'module-auth-env': '<i class="fas fa-fw fa-id-card-alt"></i>', + 'module-changelogs': '<i class="fas fa-fw fa-clipboard-list"></i>', + 'module-dashboards': '<i class="fas fa-fw fa-tachometer-alt"></i>', + 'module-dbstatus': '<i class="fas fa-fw fa-database"></i>', + 'module-design': '<i class="fas fa-fw fa-palette"></i>', + 'module-devtools': '<i class="fas fa-fw fa-bug"></i>', + 'module-dnsr': '<i class="fas fa-fw fa-directions"></i>', + 'module-events': '<i class="fas fa-fw fa-bell"></i>', + 'module-filters': '<i class="fas fa-fw fa-filter"></i>', + 'module-geoip': '<i class="fas fa-fw fa-map-marked-alt"></i>', + 'module-groups': '<i class="fas fa-fw fa-users"></i>', + 'module-help': '<i class="fas fa-fw fa-question-circle"></i>', + 'module-hosts': '<i class="fas fa-fw fa-server"></i>', + 'module-networks': '<i class="fas fa-fw fa-sitemap"></i>', + 'module-nerd': '<i class="fas fa-fw fa-certificate"></i>', + 'module-pdnsr': '<i class="fas fa-fw fa-compass"></i>', + 'module-performance': '<i class="fas fa-fw fa-chart-bar"></i>', + 'module-reports': '<i class="fas fa-fw fa-newspaper"></i>', 'module-settings-reporting': '<i class="fas fa-fw fa-sliders-h"></i>', - 'module-skeleton': '<i class="fas fa-fw fa-skull"></i>', - 'module-status': '<i class="fas fa-fw fa-heartbeat"></i>', - 'module-timeline': '<i class="fas fa-fw fa-chart-line"></i>', - 'module-users': '<i class="fas fa-fw fa-user"></i>', - 'module-whois': '<i class="fas fa-fw fa-map-signs"></i>', + 'module-skeleton': '<i class="fas fa-fw fa-skull"></i>', + 'module-status': '<i class="fas fa-fw fa-heartbeat"></i>', + 'module-timeline': '<i class="fas fa-fw fa-chart-line"></i>', + 'module-users': '<i class="fas fa-fw fa-user"></i>', + 'module-whois': '<i class="fas fa-fw fa-map-signs"></i>', 'profile': '<i class="fas fa-fw fa-id-card"></i>', @@ -114,116 +265,237 @@ ICONS = { # # Action icons. # - 'action-sort': '<i class="fas fa-fw fa-sort"></i>', - 'action-sort-asc': '<i class="fas fa-fw fa-sort-up"></i>', - 'action-sort-desc': '<i class="fas fa-fw fa-sort-down"></i>', - 'action-more': '<i class="fas fa-fw fa-cubes"></i>', - 'action-search': '<i class="fas fa-fw fa-search"></i>', - 'action-show': '<i class="fas fa-fw fa-eye"></i>', - 'action-show-user': '<i class="fas fa-fw fa-user-circle"></i>', - 'action-create': '<i class="fas fa-fw fa-plus-circle"></i>', - 'action-create-user': '<i class="fas fa-fw fa-user-plus"></i>', - 'action-update': '<i class="fas fa-fw fa-edit"></i>', - 'action-update-user': '<i class="fas fa-fw fa-user-edit"></i>', - 'action-enable': '<i class="fas fa-fw fa-unlock"></i>', - 'action-enable-user': '<i class="fas fa-fw fa-user-check"></i>', - 'action-disable': '<i class="fas fa-fw fa-lock"></i>', + 'action-sort': '<i class="fas fa-fw fa-sort"></i>', + 'action-sort-asc': '<i class="fas fa-fw fa-sort-up"></i>', + 'action-sort-desc': '<i class="fas fa-fw fa-sort-down"></i>', + 'action-more': '<i class="fas fa-fw fa-cubes"></i>', + 'action-search': '<i class="fas fa-fw fa-search"></i>', + 'action-show': '<i class="fas fa-fw fa-eye"></i>', + 'action-show-user': '<i class="fas fa-fw fa-user-circle"></i>', + 'action-create': '<i class="fas fa-fw fa-plus-circle"></i>', + 'action-create-user': '<i class="fas fa-fw fa-user-plus"></i>', + 'action-update': '<i class="fas fa-fw fa-edit"></i>', + 'action-update-user': '<i class="fas fa-fw fa-user-edit"></i>', + 'action-enable': '<i class="fas fa-fw fa-unlock"></i>', + 'action-enable-user': '<i class="fas fa-fw fa-user-check"></i>', + 'action-disable': '<i class="fas fa-fw fa-lock"></i>', 'action-disable-user': '<i class="fas fa-fw fa-user-lock"></i>', - 'action-delete': '<i class="fas fa-fw fa-trash"></i>', - 'action-delete-user': '<i class="fas fa-fw fa-user-slash"></i>', - 'action-add-member': '<i class="fas fa-fw fa-user-plus"></i>', - 'action-rej-member': '<i class="fas fa-fw fa-user-minus"></i>', - 'action-rem-member': '<i class="fas fa-fw fa-user-times"></i>', - 'action-save': '<i class="fas fa-fw fa-save"></i>', - 'action-download': '<i class="fas fa-fw fa-file-download"></i>', + 'action-delete': '<i class="fas fa-fw fa-trash"></i>', + 'action-delete-user': '<i class="fas fa-fw fa-user-slash"></i>', + 'action-add-member': '<i class="fas fa-fw fa-user-plus"></i>', + 'action-rej-member': '<i class="fas fa-fw fa-user-minus"></i>', + 'action-rem-member': '<i class="fas fa-fw fa-user-times"></i>', + 'action-save': '<i class="fas fa-fw fa-save"></i>', + 'action-download': '<i class="fas fa-fw fa-file-download"></i>', 'action-download-zip': '<i class="fas fa-fw fa-file-archive"></i>', 'action-download-csv': '<i class="fas fa-fw fa-file-csv"></i>', 'action-download-svg': '<i class="fas fa-fw fa-file-image"></i>', - 'action-download-js': '<i class="fab fa-fw fa-js"></i>', - 'action-mail': '<i class="fas fa-fw fa-envelope"></i>', - 'action-reload': '<i class="fas fa-fw fa-sync-alt"></i>', - 'action-genkey': '<i class="fas fa-fw fa-key"></i>', - 'action-stop': '<i class="fas fa-fw fa-stop-circle"></i>', + 'action-download-js': '<i class="fab fa-fw fa-js"></i>', + 'action-mail': '<i class="fas fa-fw fa-envelope"></i>', + 'action-reload': '<i class="fas fa-fw fa-sync-alt"></i>', + 'action-genkey': '<i class="fas fa-fw fa-key"></i>', + 'action-stop': '<i class="fas fa-fw fa-stop-circle"></i>', 'alert-success': '<i class="fas fa-fw fa-check-circle"></i>', - 'alert-info': '<i class="fas fa-fw fa-info-circle"></i>', + 'alert-info': '<i class="fas fa-fw fa-info-circle"></i>', 'alert-warning': '<i class="fas fa-fw fa-exclamation-circle"></i>', - 'alert-danger': '<i class="fas fa-fw fa-exclamation-triangle"></i>', + 'alert-danger': '<i class="fas fa-fw fa-exclamation-triangle"></i>', - 'item-enabled': '<i class="fas fa-fw fa-toggle-on"></i>', + 'item-enabled': '<i class="fas fa-fw fa-toggle-on"></i>', 'item-disabled': '<i class="fas fa-fw fa-toggle-off"></i>', - 'r-t-summary': '<i class="fas fa-fw fa-archive"></i>', - 'r-t-extra': '<i class="fas fa-fw fa-file-alt"></i>', - 'r-s-unknown': '<i class="fas fa-fw fa-thermometer-empty"></i>', - 'r-s-low': '<i class="fas fa-fw fa-thermometer-quarter"></i>', - 'r-s-medium': '<i class="fas fa-fw fa-thermometer-half"></i>', - 'r-s-high': '<i class="fas fa-fw fa-thermometer-three-quarters"></i>', + 'r-t-summary': '<i class="fas fa-fw fa-archive"></i>', + 'r-t-extra': '<i class="fas fa-fw fa-file-alt"></i>', + 'r-s-unknown': '<i class="fas fa-fw fa-thermometer-empty"></i>', + 'r-s-low': '<i class="fas fa-fw fa-thermometer-quarter"></i>', + 'r-s-medium': '<i class="fas fa-fw fa-thermometer-half"></i>', + 'r-s-high': '<i class="fas fa-fw fa-thermometer-three-quarters"></i>', 'r-s-critical': '<i class="fas fa-fw fa-thermometer-full"></i>', 'report-data-relapsed': '<i class="fas fa-fw fa-sync-alt"></i>', 'report-data-filtered': '<i class="fas fa-fw fa-filter"></i>', - 'report-data-test': '<i class="fas fa-fw fa-bug"></i>', - 'report-data-mailed': '<i class="fas fa-fw fa-envelope"></i>', + 'report-data-test': '<i class="fas fa-fw fa-bug"></i>', + 'report-data-mailed': '<i class="fas fa-fw fa-envelope"></i>', 'ajax-loader': '<i class="fas fa-fw fa-spinner fa-spin fa-4x"></i>', - 'caret-down': '<i class="fas fa-fw fa-caret-square-down"></i>', - 'unassigned': '<i class="fas fa-fw fa-minus"></i>', + 'caret-down': '<i class="fas fa-fw fa-caret-square-down"></i>', + 'unassigned': '<i class="fas fa-fw fa-minus"></i>', 'undisclosed': '<i class="fas fa-fw fa-minus"></i>', - 'calendar': '<i class="fas fa-fw fa-calendar-alt"></i>', - 'stopwatch': '<i class="fas fa-fw fa-stopwatch"></i>', - 'clock': '<i class="fas fa-fw fa-clock"></i>', - 'domain': '<i class="fas fa-fw fa-tag"></i>', - 'time-from': '<i class="fas fa-fw fa-hourglass-start"></i>', - 'time-to': '<i class="fas fa-fw fa-hourglass-end"></i>', - 'debug': '<i class="fas fa-fw fa-bug"></i>', - 'eventclss': '<i class="fas fa-fw fa-book"></i>', - 'reference': '<i class="fas fa-fw fa-external-link-alt"></i>', - 'anchor': '<i class="fas fa-fw fa-anchor"></i>', - 'search': '<i class="fas fa-fw fa-search"></i>', - 'weight': '<i class="fas fa-fw fa-weight"></i>', - 'list': '<i class="fas fa-fw fa-list-ul"></i>', - 'mail': '<i class="fas fa-fw fa-envelope"></i>', - 'redirect': '<i class="fas fa-fw fa-share"></i>', - 'unredirect': '<span class="fa-layers fa-fw"><i class="fas fa-fw fa-share"></i><i class="fas fa-fw fa-ban"></i></span>', - 'mute': '<i class="fas fa-fw fa-volume-off"></i>', - 'unmute': '<i class="fas fa-fw fa-volume-up"></i>', - 'compress': '<i class="fas fa-fw fa-gift"></i>', - 'uncompress': '<span class="fa-layers fa-fw"><i class="fas fa-fw fa-gift"></i><i class="fas fa-fw fa-ban"></i></span>', - 'import': '<i class="fas fa-fw fa-cloud-upload"></i>', - 'export': '<i class="fas fa-fw fa-cloud-download"></i>', - 'validate': '<i class="fas fa-fw fa-check-circle"></i>', - 'min': '<i class="fas fa-fw fa-angle-double-down"></i>', - 'max': '<i class="fas fa-fw fa-angle-double-up"></i>', - 'sum': '<i class="fas fa-fw fa-plus"></i>', - 'cnt': '<i class="fas fa-fw fa-hashtag"></i>', - 'avg': '<i class="fas fa-fw fa-dot-circle"></i>', - 'med': '<i class="fas fa-fw fa-bullseye"></i>', - 'na': '<i class="fas fa-fw fa-times"></i>', - 'stats': '<i class="fas fa-fw fa-bar-chart"></i>', - 'structure': '<i class="fas fa-fw fa-tree"></i>', - 'actions': '<i class="fas fa-fw fa-wrench"></i>', - 'cog': '<i class="fas fa-fw fa-cog"></i>', - 'check': '<i class="fas fa-fw fa-check-square"></i>', + 'calendar': '<i class="fas fa-fw fa-calendar-alt"></i>', + 'stopwatch': '<i class="fas fa-fw fa-stopwatch"></i>', + 'clock': '<i class="fas fa-fw fa-clock"></i>', + 'domain': '<i class="fas fa-fw fa-tag"></i>', + 'time-from': '<i class="fas fa-fw fa-hourglass-start"></i>', + 'time-to': '<i class="fas fa-fw fa-hourglass-end"></i>', + 'debug': '<i class="fas fa-fw fa-bug"></i>', + 'eventclss': '<i class="fas fa-fw fa-book"></i>', + 'reference': '<i class="fas fa-fw fa-external-link-alt"></i>', + 'anchor': '<i class="fas fa-fw fa-anchor"></i>', + 'search': '<i class="fas fa-fw fa-search"></i>', + 'weight': '<i class="fas fa-fw fa-weight"></i>', + 'list': '<i class="fas fa-fw fa-list-ul"></i>', + 'mail': '<i class="fas fa-fw fa-envelope"></i>', + 'redirect': '<i class="fas fa-fw fa-share"></i>', + 'unredirect': '<span class="fa-layers fa-fw"><i class="fas fa-fw fa-share"></i><i class="fas fa-fw fa-ban"></i></span>', + 'mute': '<i class="fas fa-fw fa-volume-off"></i>', + 'unmute': '<i class="fas fa-fw fa-volume-up"></i>', + 'compress': '<i class="fas fa-fw fa-gift"></i>', + 'uncompress': '<span class="fa-layers fa-fw"><i class="fas fa-fw fa-gift"></i><i class="fas fa-fw fa-ban"></i></span>', + 'import': '<i class="fas fa-fw fa-cloud-upload"></i>', + 'export': '<i class="fas fa-fw fa-cloud-download"></i>', + 'validate': '<i class="fas fa-fw fa-check-circle"></i>', + 'min': '<i class="fas fa-fw fa-angle-double-down"></i>', + 'max': '<i class="fas fa-fw fa-angle-double-up"></i>', + 'sum': '<i class="fas fa-fw fa-plus"></i>', + 'cnt': '<i class="fas fa-fw fa-hashtag"></i>', + 'avg': '<i class="fas fa-fw fa-dot-circle"></i>', + 'med': '<i class="fas fa-fw fa-bullseye"></i>', + 'na': '<i class="fas fa-fw fa-times"></i>', + 'stats': '<i class="fas fa-fw fa-bar-chart"></i>', + 'structure': '<i class="fas fa-fw fa-tree"></i>', + 'actions': '<i class="fas fa-fw fa-wrench"></i>', + 'cog': '<i class="fas fa-fw fa-cog"></i>', + 'check': '<i class="fas fa-fw fa-check-square"></i>', 'check_blank': '<i class="far fa-fw fa-square"></i>', - 'ok': '<i class="fas fa-fw fa-check"></i>', - 'ko': '<i class="fas fa-fw fa-times"></i>', - 'sortasc': '<i class="fas fa-fw fa-sort-asc"></i>', - 'sortdesc': '<i class="fas fa-fw fa-sort-desc"></i>', - 'backtotop': '<i class="fas fa-fw fa-level-up-alt"></i>', - 'first': '<i class="fas fa-fw fa-angle-double-left"></i>', - 'previous': '<i class="fas fa-fw fa-angle-left"></i>', - 'next': '<i class="fas fa-fw fa-angle-right"></i>', - 'last': '<i class="fas fa-fw fa-angle-double-right" aria-hidden="true"></i>', - 'liitem': '<i class="fas fa-li fa-asterisk" aria-hidden="true"></i>', - 'expand': '<i class="fas fa-fw fa-angle-left" aria-hidden="true"></i>', - 'collapse': '<i class="fas fa-fw fa-angle-down" aria-hidden="true"></i>', - 'form-error': '<i class="fas fa-fw fa-exclamation-triangle" aria-hidden="true"></i>', - 'table': '<i class="fas fa-fw fa-table"></i>', + 'ok': '<i class="fas fa-fw fa-check"></i>', + 'ko': '<i class="fas fa-fw fa-times"></i>', + 'sortasc': '<i class="fas fa-fw fa-sort-asc"></i>', + 'sortdesc': '<i class="fas fa-fw fa-sort-desc"></i>', + 'backtotop': '<i class="fas fa-fw fa-level-up-alt"></i>', + 'first': '<i class="fas fa-fw fa-angle-double-left"></i>', + 'previous': '<i class="fas fa-fw fa-angle-left"></i>', + 'next': '<i class="fas fa-fw fa-angle-right"></i>', + 'last': '<i class="fas fa-fw fa-angle-double-right" aria-hidden="true"></i>', + 'liitem': '<i class="fas fa-li fa-asterisk" aria-hidden="true"></i>', + 'expand': '<i class="fas fa-fw fa-angle-left" aria-hidden="true"></i>', + 'collapse': '<i class="fas fa-fw fa-angle-down" aria-hidden="true"></i>', + 'form-error': '<i class="fas fa-fw fa-exclamation-triangle" aria-hidden="true"></i>', + 'table': '<i class="fas fa-fw fa-table"></i>', 'quicksearch': '<i class="fab fa-fw fa-searchengin"></i>', - 'playground': '<i class="fas fa-fw fa-gamepad"></i>' + 'playground': '<i class="fas fa-fw fa-gamepad"></i>' } """ -Predefined list of selected `font-awesome <http://fontawesome.io/icons/>`__ icons +Predefined list of selected `font-awesome <https://fontawesome.io/icons/>`__ icons that are used in this application. """ + +TIME_WINDOWS = { + '1h': { + 'current': lambda x: (x - datetime.timedelta(hours=1)).replace(minute=0, second=0, microsecond=0, tzinfo=None), + 'previous': lambda x: (x - datetime.timedelta(hours=1)).replace(minute=0, second=0, microsecond=0, tzinfo=None), + 'next': lambda x: (x + datetime.timedelta(hours=1)).replace(minute=0, second=0, microsecond=0, tzinfo=None) + }, + '2h': { + 'current': lambda x: (x - datetime.timedelta(hours=2)).replace(minute=0, second=0, microsecond=0, tzinfo=None), + 'previous': lambda x: (x - datetime.timedelta(hours=2)).replace(minute=0, second=0, microsecond=0, tzinfo=None), + 'next': lambda x: (x + datetime.timedelta(hours=2)).replace(minute=0, second=0, microsecond=0, tzinfo=None) + }, + '3h': { + 'current': lambda x: (x - datetime.timedelta(hours=3)).replace(minute=0, second=0, microsecond=0, tzinfo=None), + 'previous': lambda x: (x - datetime.timedelta(hours=3)).replace(minute=0, second=0, microsecond=0, tzinfo=None), + 'next': lambda x: (x + datetime.timedelta(hours=3)).replace(minute=0, second=0, microsecond=0, tzinfo=None) + }, + '4h': { + 'current': lambda x: (x - datetime.timedelta(hours=4)).replace(minute=0, second=0, microsecond=0, tzinfo=None), + 'previous': lambda x: (x - datetime.timedelta(hours=4)).replace(minute=0, second=0, microsecond=0, tzinfo=None), + 'next': lambda x: (x + datetime.timedelta(hours=4)).replace(minute=0, second=0, microsecond=0, tzinfo=None) + }, + '6h': { + 'current': lambda x: (x - datetime.timedelta(hours=6)).replace(minute=0, second=0, microsecond=0, tzinfo=None), + 'previous': lambda x: (x - datetime.timedelta(hours=6)).replace(minute=0, second=0, microsecond=0, tzinfo=None), + 'next': lambda x: (x + datetime.timedelta(hours=6)).replace(minute=0, second=0, microsecond=0, tzinfo=None) + }, + '12h': { + 'current': lambda x: (x - datetime.timedelta(hours=12)).replace(minute=0, second=0, microsecond=0, tzinfo=None), + 'previous': lambda x: (x - datetime.timedelta(hours=12)).replace(minute=0, second=0, microsecond=0, + tzinfo=None), + 'next': lambda x: (x + datetime.timedelta(hours=12)).replace(minute=0, second=0, microsecond=0, tzinfo=None) + }, + '1d': { + 'current': lambda x: (x - datetime.timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0, + tzinfo=None), + 'previous': lambda x: (x - datetime.timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0, + tzinfo=None), + 'next': lambda x: (x + datetime.timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0, + tzinfo=None) + }, + '2d': { + 'current': lambda x: (x - datetime.timedelta(days=2)).replace(hour=0, minute=0, second=0, microsecond=0, + tzinfo=None), + 'previous': lambda x: (x - datetime.timedelta(days=2)).replace(hour=0, minute=0, second=0, microsecond=0, + tzinfo=None), + 'next': lambda x: (x + datetime.timedelta(days=2)).replace(hour=0, minute=0, second=0, microsecond=0, + tzinfo=None) + }, + '3d': { + 'current': lambda x: (x - datetime.timedelta(days=3)).replace(hour=0, minute=0, second=0, microsecond=0, + tzinfo=None), + 'previous': lambda x: (x - datetime.timedelta(days=3)).replace(hour=0, minute=0, second=0, microsecond=0, + tzinfo=None), + 'next': lambda x: (x + datetime.timedelta(days=3)).replace(hour=0, minute=0, second=0, microsecond=0, + tzinfo=None) + }, + '1w': { + 'current': lambda x: (x - datetime.timedelta(weeks=1)).replace(hour=0, minute=0, second=0, microsecond=0, + tzinfo=None), + 'previous': lambda x: (x - datetime.timedelta(weeks=1)).replace(hour=0, minute=0, second=0, microsecond=0, + tzinfo=None), + 'next': lambda x: (x + datetime.timedelta(weeks=1)).replace(hour=0, minute=0, second=0, microsecond=0, + tzinfo=None) + }, + '2w': { + 'current': lambda x: (x - datetime.timedelta(weeks=2)).replace(hour=0, minute=0, second=0, microsecond=0, + tzinfo=None), + 'previous': lambda x: (x - datetime.timedelta(weeks=2)).replace(hour=0, minute=0, second=0, microsecond=0, + tzinfo=None), + 'next': lambda x: (x + datetime.timedelta(weeks=2)).replace(hour=0, minute=0, second=0, microsecond=0, + tzinfo=None) + }, + '4w': { + 'current': lambda x: (x - datetime.timedelta(weeks=4)).replace(hour=0, minute=0, second=0, microsecond=0, + tzinfo=None), + 'previous': lambda x: (x - datetime.timedelta(weeks=4)).replace(hour=0, minute=0, second=0, microsecond=0, + tzinfo=None), + 'next': lambda x: (x + datetime.timedelta(weeks=4)).replace(hour=0, minute=0, second=0, microsecond=0, + tzinfo=None) + }, + '12w': { + 'current': lambda x: (x - datetime.timedelta(weeks=12)).replace(hour=0, minute=0, second=0, microsecond=0, + tzinfo=None), + 'previous': lambda x: (x - datetime.timedelta(weeks=12)).replace(hour=0, minute=0, second=0, microsecond=0, + tzinfo=None), + 'next': lambda x: (x + datetime.timedelta(weeks=12)).replace(hour=0, minute=0, second=0, microsecond=0, + tzinfo=None) + }, + + 'td': { + 'current': lambda x: x.replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=None), + 'previous': lambda x: (x - datetime.timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0, + tzinfo=None), + 'next': lambda x: (x + datetime.timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0, + tzinfo=None) + }, + 'tw': { + 'current': lambda x: (x - datetime.timedelta(days=x.weekday())).replace(hour=0, minute=0, second=0, + microsecond=0, tzinfo=None), + 'previous': lambda x: (x - datetime.timedelta(days=7)).replace(hour=0, minute=0, second=0, microsecond=0, + tzinfo=None), + 'next': lambda x: (x + datetime.timedelta(days=7)).replace(hour=0, minute=0, second=0, microsecond=0, + tzinfo=None) + }, + 'tm': { + 'current': lambda x: x.replace(day=1, hour=0, minute=0, second=0, microsecond=0, tzinfo=None), + 'previous': lambda x: (x - datetime.timedelta(days=1)).replace(day=1, hour=0, minute=0, second=0, microsecond=0, + tzinfo=None), + 'next': lambda x: (x + datetime.timedelta(days=32)).replace(day=1, hour=0, minute=0, second=0, microsecond=0, + tzinfo=None) + }, + 'ty': { + 'current': lambda x: x.replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0, tzinfo=None), + 'previous': lambda x: x.replace(year=x.year - 1, month=1, day=1, hour=0, minute=0, second=0, microsecond=0, + tzinfo=None), + 'next': lambda x: x.replace(year=x.year + 1, month=1, day=1, hour=0, minute=0, second=0, microsecond=0, + tzinfo=None) + } +} +"""Default list of time windows for 'by time' quicksearch lists.""" diff --git a/lib/vial/db.py b/lib/hawat/db.py similarity index 64% rename from lib/vial/db.py rename to lib/hawat/db.py index 94b7a5ec960f22673a9491c323a30f61d687252f..6acf16588278b11e26eb44e66229d01e7b56062b 100644 --- a/lib/vial/db.py +++ b/lib/hawat/db.py @@ -1,23 +1,27 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- +# This file is part of Mentat system (https://mentat.cesnet.cz/). +# +# Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ -This module contains database layer for *Vial* application. +This module contains database layer for *Hawat* application. """ +__author__ = "Jan Mach <jan.mach@cesnet.cz>" +__credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" from sqlalchemy.ext.declarative import declarative_base import flask_sqlalchemy from flask_sqlalchemy.model import Model - _DB = None -MODEL = declarative_base(cls = Model) +MODEL = declarative_base(cls=Model) def db_setup(**kwargs): @@ -39,6 +43,7 @@ def db_setup(**kwargs): return _DB + def db_get(): """ Convenience method. diff --git a/lib/vial/errors.py b/lib/hawat/errors.py similarity index 64% rename from lib/vial/errors.py rename to lib/hawat/errors.py index 6de11585057c5278317dc2998c3a4163d66c009d..c830186255d66bce5b5113912b7cbe815f22bc91 100644 --- a/lib/vial/errors.py +++ b/lib/hawat/errors.py @@ -1,35 +1,42 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- +# This file is part of Mentat system (https://mentat.cesnet.cz/). +# +# Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ -This module contains error handling code for *Vial* application. +This module contains error handling code for *Hawat* application. """ +__author__ = "Jan Mach <jan.mach@cesnet.cz>" +__credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" from werkzeug.http import HTTP_STATUS_CODES from flask import request, make_response, render_template, jsonify from flask_babel import gettext from flask_login import current_user -import vial.const +import hawat.const def wants_json_response(): - """Helper method for detecting prefered response in JSON format.""" + """Helper method for detecting preferred response in JSON format.""" return request.accept_mimetypes['application/json'] >= \ - request.accept_mimetypes['text/html'] + request.accept_mimetypes['text/html'] + def error_handler_switch(status_code, exc): """Return correct error response (HTML or JSON) based on client preferences.""" if wants_json_response(): - return api_error_response(status_code, exception = exc) - return error_response(status_code, exception = exc) + return api_error_response(status_code, exception=exc) + return error_response(status_code, exception=exc) + -def _make_payload(status_code, message = None, exception = None): +def _make_payload(status_code, message=None, exception=None): """Prepare the error response payload regardless of the response type.""" payload = { 'status': status_code, @@ -43,11 +50,12 @@ def _make_payload(status_code, message = None, exception = None): if hasattr(exception.__class__, 'description'): payload['message'] = exception.__class__.description # Append the whole exception object for developers to make debugging easier. - if current_user.is_authenticated and current_user.has_role(vial.const.ROLE_DEVELOPER): + if current_user.is_authenticated and current_user.has_role(hawat.const.ROLE_DEVELOPER): payload['exception'] = exception return payload -def error_response(status_code, message = None, exception = None): + +def error_response(status_code, message=None, exception=None): """Generate error response in HTML format.""" status_code = int(status_code) payload = _make_payload(status_code, message, exception) @@ -59,7 +67,8 @@ def error_response(status_code, message = None, exception = None): status_code ) -def api_error_response(status_code, message = None, exception = None): + +def api_error_response(status_code, message=None, exception=None): """Generate error response in JSON format.""" status_code = int(status_code) payload = _make_payload(status_code, message, exception) diff --git a/lib/hawat/events.py b/lib/hawat/events.py index 137364dce9d2aa5c07efcd1a9085af532464f3f3..a94ad6ded7ed4ee1320518a2797bf6d8e93c1ac8 100644 --- a/lib/hawat/events.py +++ b/lib/hawat/events.py @@ -1,22 +1,20 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # This file is part of Mentat system (https://mentat.cesnet.cz/). # # Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ This module contains couple of simple helpers for working with IDEA messages. """ - __author__ = "Jan Mach <jan.mach@cesnet.cz>" __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" - import os # @@ -31,7 +29,6 @@ import pyzenkit.jsonconf import hawat.const import mentat.services.eventstorage - _DB = None @@ -46,66 +43,77 @@ def _get_cached_values(cache_file): ) ) + def _get_values(cache_file, column): try: return _get_cached_values(cache_file) except FileNotFoundError: return db_get().distinct_values(column) + def get_event_source_types(): """ Return list of all available event source types. """ return _get_values('itemset-stat-sourcetypes.json', 'source_type') + def get_event_target_types(): """ Return list of all available event target types. """ return _get_values('itemset-stat-targettypes.json', 'target_type') + def get_event_detector_types(): """ Return list of all available event detector types. """ return _get_values('itemset-stat-detectortypes.json', 'node_type') + def get_event_detectors(): """ Return list of all available event detectors. """ return _get_values('itemset-stat-detectors.json', 'node_name') + def get_event_categories(): """ Return list of all available event categories. """ return _get_values('itemset-stat-categories.json', 'category') + def get_event_severities(): """ Return list of all available event severities. """ return _get_values('itemset-stat-severities.json', 'eventseverity') + def get_event_classes(): """ Return list of all available event classes. """ return _get_values('itemset-stat-classes.json', 'eventclass') + def get_event_protocols(): """ Return list of all available event protocols. """ return _get_values('itemset-stat-protocols.json', 'protocol') + def get_event_inspection_errs(): """ Return list of all available event inspection errors. """ return _get_values('itemset-stat-inspectionerrors.json', 'inspectionerrors') + def db_settings(app): """ Return database settings from Mentat core configurations. @@ -115,25 +123,27 @@ def db_settings(app): """ return app.mconfig + def get_event_enums(): # Get lists of available options for various event search form select fields. enums = {} enums.update( - source_types = get_event_source_types(), - target_types = get_event_target_types(), - detectors = get_event_detectors(), - detector_types = get_event_detector_types(), - categories = get_event_categories(), - severities = get_event_severities(), - classes = get_event_classes(), - protocols = get_event_protocols(), - inspection_errs = get_event_inspection_errs() + source_types=get_event_source_types(), + target_types=get_event_target_types(), + detectors=get_event_detectors(), + detector_types=get_event_detector_types(), + categories=get_event_categories(), + severities=get_event_severities(), + classes=get_event_classes(), + protocols=get_event_protocols(), + inspection_errs=get_event_inspection_errs() ) enums.update( - host_types = sorted(list(set(enums['source_types'] + enums['target_types']))), + host_types=sorted(list(set(enums['source_types'] + enums['target_types']))), ) return enums + def get_event_form_choices(): enums = get_event_enums() choices = {} @@ -141,6 +151,7 @@ def get_event_form_choices(): choices[key] = list(zip(vals, vals)) return choices + def db_init(app): """ Initialize connection to event database. diff --git a/lib/vial/forms.py b/lib/hawat/forms.py similarity index 75% rename from lib/vial/forms.py rename to lib/hawat/forms.py index 3969e784fddcb052d0fa2aa7d0764682627e9830..086c28e777d5e6c93adc6106a8fe8ff4cf8c5553 100644 --- a/lib/vial/forms.py +++ b/lib/hawat/forms.py @@ -1,14 +1,19 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- +# This file is part of Mentat system (https://mentat.cesnet.cz/). +# +# Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ -This module contains usefull form related classes for *Vial* application views. +This module contains usefull form related classes for *Hawat* application views. """ +__author__ = "Jan Mach <jan.mach@cesnet.cz>" +__credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" import datetime import urllib.parse @@ -20,17 +25,19 @@ import flask import flask_wtf from flask_babel import gettext, lazy_gettext -import vial.const +import hawat.const +import hawat.db -def default_dt_with_delta(delta = 7): +def default_dt_with_delta(delta=7): """ Create default timestamp for datetime form values with given time delta in days and adjust the result to whole hours. """ return datetime.datetime.utcnow().replace( - minute = 0, second = 0, microsecond = 0 - ) - datetime.timedelta(days = delta) + minute=0, second=0, microsecond=0 + ) - datetime.timedelta(days=delta) + def default_dt(): """ @@ -38,9 +45,10 @@ def default_dt(): and adjust the result to whole hours. """ return datetime.datetime.utcnow().replace( - minute = 0, second = 0, microsecond = 0 + minute=0, second=0, microsecond=0 ) + def str_to_bool(value): """ Convert given string value to boolean. @@ -62,7 +70,7 @@ def str_to_bool_with_none(value): return False if str(value).lower() == 'none': return None - if str(value).lower() == '': + if str(value).lower() == '': return '' raise ValueError('Invalid string value {} to be converted to boolean'.format(str(value))) @@ -79,27 +87,29 @@ def str_to_int_with_none(value): raise ValueError('Invalid string value {} to be converted to integer'.format(str(value))) -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- def _is_safe_url(target): """ Check, if the URL is safe enough to be redirected to. """ - ref_url = urllib.parse.urlparse(flask.request.host_url) + ref_url = urllib.parse.urlparse(flask.request.host_url) test_url = urllib.parse.urlparse(urllib.parse.urljoin(flask.request.host_url, target)) return test_url.scheme in ('http', 'https') and \ ref_url.netloc == test_url.netloc + def _is_same_path(first, second): """ Check, if both URL point to same path. """ - first_url = urllib.parse.urlparse(first) + first_url = urllib.parse.urlparse(first) second_url = urllib.parse.urlparse(second) return first_url.path == second_url.path -def get_redirect_target(target_url = None, default_url = None, exclude_url = None): + +def get_redirect_target(target_url=None, default_url=None, exclude_url=None): """ Get redirection target, either from GET request variable, or from referrer header. """ @@ -125,77 +135,82 @@ def get_redirect_target(target_url = None, default_url = None, exclude_url = Non raise RuntimeError("Unable to choose apropriate redirection target.") -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- -def check_login(form, field): # pylint: disable=locally-disabled,unused-argument +def check_login(_form, field): # pylint: disable=locally-disabled,unused-argument """ Callback for validating user account logins (usernames). """ - if vial.const.CRE_LOGIN.match(field.data): + if hawat.const.CRE_LOGIN.match(field.data): return raise wtforms.validators.ValidationError( gettext( 'The "%(val)s" value does not look like valid login name.', - val = str(field.data) + val=str(field.data) ) ) -def check_email(form, field): # pylint: disable=locally-disabled,unused-argument + +def check_email(_form, field): # pylint: disable=locally-disabled,unused-argument """ Callback for validating user emails or account logins (usernames). """ - if vial.const.CRE_EMAIL.match(field.data): + if hawat.const.CRE_EMAIL.match(field.data): return raise wtforms.validators.ValidationError( gettext( 'The "%(val)s" value does not look like valid email address.', - val = str(field.data) + val=str(field.data) ) ) -def check_unique_login(form, field): # pylint: disable=locally-disabled,unused-argument + +def check_unique_login(_form, field): # pylint: disable=locally-disabled,unused-argument """ Callback for validating of uniqueness of user login. """ - user_model = flask.current_app.get_model(vial.const.MODEL_USER) - user = vial.db.db_session().query(user_model).filter_by(login = field.data).first() + user_model = flask.current_app.get_model(hawat.const.MODEL_USER) + user = hawat.db.db_session().query(user_model).filter_by(login=field.data).first() if user is not None: raise wtforms.validators.ValidationError( gettext( 'Please use different login, the "%(val)s" is already taken.', - val = str(field.data) + val=str(field.data) ) ) -def check_unique_group(form, field): # pylint: disable=locally-disabled,unused-argument + +def check_unique_group(_form, field): # pylint: disable=locally-disabled,unused-argument """ Callback for validating of uniqueness of group name. """ - group_model = flask.current_app.get_model(vial.const.MODEL_GROUP) - group = vial.db.db_session().query(group_model).filter_by(name = field.data).first() + group_model = flask.current_app.get_model(hawat.const.MODEL_GROUP) + group = hawat.db.db_session().query(group_model).filter_by(name=field.data).first() if group is not None: raise wtforms.validators.ValidationError( gettext( 'Please use different group name, the "%(val)s" is already taken.', - val = str(field.data) + val=str(field.data) ) ) -def check_email_list(form, field): # pylint: disable=locally-disabled,unused-argument + +def check_email_list(_form, field): # pylint: disable=locally-disabled,unused-argument """ Callback for validating list of strings. """ for data in field.data: - if vial.const.CRE_EMAIL.match(data): + if hawat.const.CRE_EMAIL.match(data): continue raise wtforms.validators.ValidationError( gettext( 'The "%(val)s" value does not look like valid email adress.', - val = str(data) + val=str(data) ) ) -def check_ip_record(form, field): # pylint: disable=locally-disabled,unused-argument + +def check_ip_record(_form, field): # pylint: disable=locally-disabled,unused-argument """ Callback for validating IP addresses. """ @@ -210,11 +225,12 @@ def check_ip_record(form, field): # pylint: disable=locally-disabled,unused-arg raise wtforms.validators.ValidationError( gettext( 'The "%(val)s" value does not look like valid IPv4/IPv6 address.', - val = str(field.data) + val=str(field.data) ) ) -def check_ip4_record(form, field): # pylint: disable=locally-disabled,unused-argument + +def check_ip4_record(_form, field): # pylint: disable=locally-disabled,unused-argument """ Callback for validating IP4 addresses. """ @@ -229,11 +245,12 @@ def check_ip4_record(form, field): # pylint: disable=locally-disabled,unused-ar raise wtforms.validators.ValidationError( gettext( 'The "%(val)s" value does not look like valid IPv4 address.', - val = str(field.data) + val=str(field.data) ) ) -def check_ip6_record(form, field): # pylint: disable=locally-disabled,unused-argument + +def check_ip6_record(_form, field): # pylint: disable=locally-disabled,unused-argument """ Callback for validating IP6 addresses. """ @@ -248,11 +265,12 @@ def check_ip6_record(form, field): # pylint: disable=locally-disabled,unused-ar raise wtforms.validators.ValidationError( gettext( 'The "%(val)s" value does not look like valid IPv6 address.', - val = str(field.data) + val=str(field.data) ) ) -def check_network_record(form, field): # pylint: disable=locally-disabled,unused-argument + +def check_network_record(_form, field): # pylint: disable=locally-disabled,unused-argument """ Callback for validating network records. """ @@ -266,11 +284,12 @@ def check_network_record(form, field): # pylint: disable=locally-disabled,unuse raise wtforms.validators.ValidationError( gettext( 'The "%(val)s" value does not look like valid IPv4/IPv6 address/range/network.', - val = str(field.data) + val=str(field.data) ) ) -def check_network_record_list(form, field): # pylint: disable=locally-disabled,unused-argument + +def check_network_record_list(_form, field): # pylint: disable=locally-disabled,unused-argument """ Callback for validating list of network records. """ @@ -281,11 +300,12 @@ def check_network_record_list(form, field): # pylint: disable=locally-disabled, raise wtforms.validators.ValidationError( gettext( 'The "%(val)s" value does not look like valid IPv4/IPv6 address/range/network.', - val = str(value) + val=str(value) ) ) -def check_port_list(form, field): # pylint: disable=locally-disabled,unused-argument + +def check_port_list(_form, field): # pylint: disable=locally-disabled,unused-argument """ Callback for validating list of ports. """ @@ -295,18 +315,19 @@ def check_port_list(form, field): # pylint: disable=locally-disabled,unused-arg raise wtforms.validators.ValidationError( gettext( 'The "%(val)s" value does not look like valid port number.', - val = str(data) + val=str(data) ) ) except ValueError: raise wtforms.validators.ValidationError( gettext( 'The "%(val)s" value does not look like valid port number.', - val = str(data) + val=str(data) ) ) -def check_int_list(form, field): # pylint: disable=locally-disabled,unused-argument + +def check_int_list(_form, field): # pylint: disable=locally-disabled,unused-argument """ Callback for validating list of positive integers. """ @@ -316,14 +337,14 @@ def check_int_list(form, field): # pylint: disable=locally-disabled,unused-argu raise wtforms.validators.ValidationError( gettext( 'The "%(val)s" value does not look like valid positive integer.', - val = str(data) + val=str(data) ) ) except ValueError: raise wtforms.validators.ValidationError( gettext( 'The "%(val)s" value does not look like valid positive integer.', - val = str(data) + val=str(data) ) ) @@ -332,32 +353,35 @@ def get_available_groups(): """ Query the database for list of all available groups. """ - group_model = flask.current_app.get_model(vial.const.MODEL_GROUP) - return vial.db.db_query(group_model).\ - order_by(group_model.name).\ + group_model = flask.current_app.get_model(hawat.const.MODEL_GROUP) + return hawat.db.db_query(group_model). \ + order_by(group_model.name). \ all() + def get_available_users(): """ Query the database for list of users. """ - user_model = flask.current_app.get_model(vial.const.MODEL_USER) - return vial.db.db_query(user_model).\ - order_by(user_model.fullname).\ + user_model = flask.current_app.get_model(hawat.const.MODEL_USER) + return hawat.db.db_query(user_model). \ + order_by(user_model.fullname). \ all() + def get_available_group_sources(): """ Query the database for list of network record sources. """ - group_model = flask.current_app.get_model(vial.const.MODEL_GROUP) - result = vial.db.db_query(group_model)\ - .distinct(group_model.source)\ - .order_by(group_model.source)\ + group_model = flask.current_app.get_model(hawat.const.MODEL_GROUP) + result = hawat.db.db_query(group_model) \ + .distinct(group_model.source) \ + .order_by(group_model.source) \ .all() return [x.source for x in result] -#------------------------------------------------------------------------------- + +# ------------------------------------------------------------------------------- class CommaListField(wtforms.Field): @@ -378,7 +402,8 @@ class CommaListField(wtforms.Field): if val == '': continue self.data.append(val.strip()) - self.data = list(self._remove_duplicates(self.data)) # pylint: disable=locally-disabled,attribute-defined-outside-init + self.data = list( + self._remove_duplicates(self.data)) # pylint: disable=locally-disabled,attribute-defined-outside-init @classmethod def _remove_duplicates(cls, seq): @@ -397,6 +422,7 @@ class DateTimeLocalField(wtforms.DateTimeField): DateTimeField that assumes input is in app-configured timezone and converts to UTC for further processing/storage. """ + def process_data(self, value): """ Process the Python data applied to this field and store the result. @@ -406,7 +432,7 @@ class DateTimeLocalField(wtforms.DateTimeField): """ localtz = pytz.timezone(flask.session['timezone']) if value: - dt_utc = pytz.utc.localize(value, is_dst = None) + dt_utc = pytz.utc.localize(value, is_dst=None) self.data = dt_utc.astimezone(localtz) # pylint: disable=locally-disabled,attribute-defined-outside-init else: self.data = None # pylint: disable=locally-disabled,attribute-defined-outside-init @@ -422,9 +448,10 @@ class DateTimeLocalField(wtforms.DateTimeField): if valuelist: date_str = ' '.join(valuelist) try: - dt_naive = datetime.datetime.strptime(date_str, self.format) - dt_local = localtz.localize(dt_naive, is_dst = None) - self.data = dt_local.astimezone(pytz.utc) # pylint: disable=locally-disabled,attribute-defined-outside-init + dt_naive = datetime.datetime.strptime(date_str, self.format) + dt_local = localtz.localize(dt_naive, is_dst=None) + self.data = dt_local.astimezone( + pytz.utc) # pylint: disable=locally-disabled,attribute-defined-outside-init except ValueError: self.data = None # pylint: disable=locally-disabled,attribute-defined-outside-init raise ValueError(self.gettext('Not a valid datetime value')) @@ -441,7 +468,7 @@ class SmartDateTimeField(wtforms.Field): widget = wtforms.widgets.TextInput() utcisoformat = '%Y-%m-%dT%H:%M:%SZ' - def __init__(self, label = None, validators = None, formats = None, **kwargs): + def __init__(self, label=None, validators=None, formats=None, **kwargs): super(SmartDateTimeField, self).__init__(label, validators, **kwargs) if formats is None: self.formats = [ @@ -472,7 +499,10 @@ class SmartDateTimeField(wtforms.Field): :param value: The python object containing the value to process. """ if value: - self.data = pytz.utc.localize(value, is_dst = None) # pylint: disable=locally-disabled,attribute-defined-outside-init + self.data = pytz.utc.localize( + value, + is_dst=None + ) # pylint: disable=locally-disabled,attribute-defined-outside-init else: self.data = None # pylint: disable=locally-disabled,attribute-defined-outside-init @@ -489,9 +519,10 @@ class SmartDateTimeField(wtforms.Field): # Try all explicitly defined valid datetime formats. for fmt in self.formats: try: - dt_naive = datetime.datetime.strptime(date_str, fmt) - dt_local = localtz.localize(dt_naive, is_dst = None) - self.data = dt_local.astimezone(pytz.utc) # pylint: disable=locally-disabled,attribute-defined-outside-init + dt_naive = datetime.datetime.strptime(date_str, fmt) + dt_local = localtz.localize(dt_naive, is_dst=None) + self.data = dt_local.astimezone( + pytz.utc) # pylint: disable=locally-disabled,attribute-defined-outside-init self.format = fmt print("Received datetime value in format {}, naive: {}, local: {}, utc: {}".format( fmt, @@ -506,8 +537,11 @@ class SmartDateTimeField(wtforms.Field): # In case of failure try UTC ISO format (YYYY-MM-DDTHH:MM:SSZ). if self.data is None: try: - dt_naive = datetime.datetime.strptime(date_str, self.utcisoformat) - self.data = pytz.utc.localize(dt_naive, is_dst = None) # pylint: disable=locally-disabled,attribute-defined-outside-init + dt_naive = datetime.datetime.strptime(date_str, self.utcisoformat) + self.data = pytz.utc.localize( + dt_naive, + is_dst=None + ) # pylint: disable=locally-disabled,attribute-defined-outside-init print("Received UTC ISO datetime value, naive: {}, utc: {}".format( dt_naive.isoformat(), self.data.isoformat(), @@ -524,6 +558,7 @@ class RadioFieldWithNone(wtforms.RadioField): """ RadioField that accepts None as valid choice. """ + def process_formdata(self, valuelist): if valuelist: try: @@ -543,10 +578,12 @@ class SelectFieldWithNone(wtforms.SelectField): """ SelectField that accepts None as valid choice. """ + def process_formdata(self, valuelist): if valuelist: try: - self.data = self.coerce(valuelist[0]) if valuelist[0].lower() != 'none' else None # pylint: disable=locally-disabled,attribute-defined-outside-init + self.data = self.coerce(valuelist[0]) if valuelist[ + 0].lower() != 'none' else None # pylint: disable=locally-disabled,attribute-defined-outside-init except ValueError: raise ValueError(self.gettext("Invalid Choice: could not coerce")) else: @@ -562,7 +599,7 @@ class SelectFieldWithNone(wtforms.SelectField): class BaseItemForm(flask_wtf.FlaskForm): """ - Class representing generic item action (create/update/delete) form for Vial + Class representing generic item action (create/update/delete) form for hawat application. This form contains support for redirection back to original page. @@ -590,7 +627,7 @@ class BaseItemForm(flask_wtf.FlaskForm): class ItemActionConfirmForm(BaseItemForm): """ - Class representing generic item action confirmation form for Vial application. + Class representing generic item action confirmation form for hawat application. This form contains nothing else but two buttons, one for confirmation, one for canceling the delete action. Actual item identifier is passed as part of the URL. @@ -605,27 +642,27 @@ class ItemActionConfirmForm(BaseItemForm): class BaseSearchForm(flask_wtf.FlaskForm): """ - Class representing generic item search form for Vial application. + Class representing generic item search form for hawat application. This form contains support for result limiting and paging. """ limit = wtforms.SelectField( lazy_gettext('Pager limit:'), - validators = [ + validators=[ wtforms.validators.Optional() ], - filters = [int], - choices = vial.const.PAGER_LIMIT_CHOICES, - default = vial.const.DEFAULT_PAGER_LIMIT + filters=[int], + choices=hawat.const.PAGER_LIMIT_CHOICES, + default=hawat.const.DEFAULT_PAGER_LIMIT ) page = wtforms.IntegerField( lazy_gettext('Page number:'), - validators = [ + validators=[ wtforms.validators.Optional(), wtforms.validators.NumberRange(min=1) ], - filters = [int], - default = 1 + filters=[int], + default=1 ) submit = wtforms.SubmitField( lazy_gettext('Search') diff --git a/lib/vial/intl.py b/lib/hawat/intl.py similarity index 71% rename from lib/vial/intl.py rename to lib/hawat/intl.py index 2a906033ed53f058dacfc336f303f0bdcb33275b..9c000ce824ae8e384e382083fc191f5e287284c7 100644 --- a/lib/vial/intl.py +++ b/lib/hawat/intl.py @@ -1,22 +1,28 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- +# This file is part of Mentat system (https://mentat.cesnet.cz/). +# +# Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ -This module contains usefull internationalization utilities for *Vial* application. +This module contains useful internationalization utilities for *Hawat* application. """ +__author__ = "Jan Mach <jan.mach@cesnet.cz>" +__credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" import flask import flask_babel import flask_login from babel import Locale - BABEL = flask_babel.Babel() + + @BABEL.localeselector def get_locale(): # pylint: disable=locally-disabled,unused-variable """ @@ -60,15 +66,15 @@ def get_timezone(): # pylint: disable=locally-disabled,unused-variable return flask.current_app.config['BABEL_DEFAULT_TIMEZONE'] -def babel_format_bytes(size, unit = 'B', step_size = 1024): +def babel_format_bytes(size, unit='B', step_size=1024): """ - Format given numeric value to human readable string describing size in + Format given numeric value to human-readable string describing size in B/KB/MB/GB/TB. - :param int size: Number to be formatted. - :param enum unit: Starting unit, possible values are ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB']. - :param int step_size: Size of the step between units. - :return: Formatted and localized string. + :param int size: Number to be formatted + :param enum unit: Starting unit, possible values are ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB'] + :param int step_size: Size of the step between units + :return: Formatted and localized string :rtype: string """ units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB'] @@ -81,7 +87,7 @@ def babel_format_bytes(size, unit = 'B', step_size = 1024): if size > step_size: if unit == val: size = size / step_size - unit = units[idx+1] + unit = units[idx + 1] else: break return '{} {}'.format( @@ -89,20 +95,22 @@ def babel_format_bytes(size, unit = 'B', step_size = 1024): unit ) -def babel_translate_locale(locale_id, with_current = False): + +def babel_translate_locale(locale_id, with_current=False): """ - Translate given locale language. By default return language in locale`s - language. Optionaly return language in given locale`s language. + Translate given locale language. By default, return language in locale`s + language. Optionally return language in given locale`s language. """ locale_obj = Locale.parse(locale_id) if not with_current: return locale_obj.language_name return locale_obj.get_language_name(flask_babel.get_locale()) -def babel_language_in_locale(locale_id = 'en'): + +def babel_language_in_locale(locale_id='en'): """ - Translate given locale language. By default return language in locale`s - language. Optionaly return language in given locale`s language. + Translate given locale language. By default, return language in locale`s + language. Optionally return language in given locale`s language. """ locale_obj = Locale.parse(flask_babel.get_locale()) return locale_obj.get_language_name(locale_id) diff --git a/lib/vial/jsglue.py b/lib/hawat/jsglue.py similarity index 90% rename from lib/vial/jsglue.py rename to lib/hawat/jsglue.py index bde129dc2eef24fdb8a162390d527be9041f3466..9e3f7587a734cc49d96326c382b98cb10c61f541 100644 --- a/lib/vial/jsglue.py +++ b/lib/hawat/jsglue.py @@ -1,8 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- +# This file is part of Mentat system (https://mentat.cesnet.cz/). +# +# Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- # # This file is part of official Flask-JSGlue project (https://github.com/stewartpark/Flask-JSGlue). @@ -10,11 +13,14 @@ # lacks elegant support for applications that are not running at webserver root. # When the next version is released this file can be removed again. # -# No copyright infrigement intended. +# No copyright infringement intended. # +__author__ = "Jan Mach <jan.mach@cesnet.cz>" +__credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" -import re, json +import re +import json from jinja2 import Markup from flask import make_response, url_for diff --git a/lib/vial/log.py b/lib/hawat/log.py similarity index 78% rename from lib/vial/log.py rename to lib/hawat/log.py index 13f5d2e65f42a110a646a1c4f1beda9bb5ee5e60..51648360c218af15bc862b93a8a272714c8ea1d8 100644 --- a/lib/vial/log.py +++ b/lib/hawat/log.py @@ -1,14 +1,19 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- +# This file is part of Mentat system (https://mentat.cesnet.cz/). +# +# Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ -This module contains logging setup for *Vial* application. +This module contains logging setup for *Hawat* application. """ +__author__ = "Jan Mach <jan.mach@cesnet.cz>" +__credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" import logging from logging.handlers import WatchedFileHandler, SMTPHandler @@ -72,6 +77,7 @@ def setup_logging_file(app): return app + def setup_logging_email(app): """ Setup application logging via email. @@ -98,12 +104,12 @@ def setup_logging_email(app): secure = () mail_handler = SMTPHandler( - mailhost = (app.config['MAIL_SERVER'], app.config['MAIL_PORT']), - fromaddr = app.config['MAIL_DEFAULT_SENDER'], - toaddrs = app.config['EMAIL_ADMINS'], - subject = app.config['MAIL_SUBJECT_PREFIX'] + ' Application Error', - credentials = credentials, - secure = secure + mailhost=(app.config['MAIL_SERVER'], app.config['MAIL_PORT']), + fromaddr=app.config['MAIL_DEFAULT_SENDER'], + toaddrs=app.config['EMAIL_ADMINS'], + subject=app.config['MAIL_SUBJECT_PREFIX'] + ' Application Error', + credentials=credentials, + secure=secure ) mail_handler.setLevel(log_level) mail_handler.setFormatter( diff --git a/lib/vial/mailer.py b/lib/hawat/mailer.py similarity index 54% rename from lib/vial/mailer.py rename to lib/hawat/mailer.py index 0e99db8952561a009e7e5155d7c80d64534ac28f..937b42e08e96725ce11bff7498bd9e8e3eb2f1b9 100644 --- a/lib/vial/mailer.py +++ b/lib/hawat/mailer.py @@ -1,14 +1,19 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- +# This file is part of Mentat system (https://mentat.cesnet.cz/). +# +# Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ -This module contains mailer setup for *Vial* application. +This module contains mailer setup for *Hawat* application. """ +__author__ = "Jan Mach <jan.mach@cesnet.cz>" +__credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" import flask_mail @@ -16,6 +21,7 @@ import flask_mail MAILER = flask_mail.Mail() """Global application resource: :py:mod:`flask_mail` mailer.""" + def on_email_sent(message, app): """ Signal handler for handling :py:func:`flask_mail.email_dispatched` signal. @@ -26,4 +32,6 @@ def on_email_sent(message, app): message.subject, ', '.join(message.recipients) ) + + flask_mail.email_dispatched.connect(on_email_sent) diff --git a/lib/vial/menu.py b/lib/hawat/menu.py similarity index 81% rename from lib/vial/menu.py rename to lib/hawat/menu.py index 691f9bac6de4e44d30de017a5ce0753cf6343a61..3da0a7e50e95795793902e0a7d2b8e1152f63a21 100644 --- a/lib/vial/menu.py +++ b/lib/hawat/menu.py @@ -1,14 +1,19 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- +# This file is part of Mentat system (https://mentat.cesnet.cz/). +# +# Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ -This module contains usefull menu representation *Vial* application. +This module contains useful menu representation *Hawat* application. """ +__author__ = "Jan Mach <jan.mach@cesnet.cz>" +__credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" import re import collections @@ -18,18 +23,16 @@ import flask import flask_login import flask_principal -import vial.acl - +import hawat.acl CRE_STRIP_QUESTION = re.compile(r'\?$') - -ENTRY_SUBMENU = 'submenu' +ENTRY_SUBMENU = 'submenu' ENTRY_SUBMENUDB = 'submenudb' -ENTRY_VIEW = 'view' -ENTRY_ENDPOINT = 'endpoint' -ENTRY_LINK = 'link' -ENTRY_TEST = 'test' +ENTRY_VIEW = 'view' +ENTRY_ENDPOINT = 'endpoint' +ENTRY_LINK = 'link' +ENTRY_TEST = 'test' def _url_segments(path): @@ -38,8 +41,9 @@ def _url_segments(path): parts.pop() return parts + def _is_active(this_url, request): - request_path = request.script_root + request.path + request_path = request.script_root + request.path request_path_full = request.script_root + request.full_path # For some reason in certain cases the '?' is appended to the end of request # path event in case there are no additional parameters. Get rid of that. @@ -71,12 +75,12 @@ def _filter_menu_entries(entries, **kwargs): """ result = collections.OrderedDict() for entry_id, entry in entries.items(): - #print("Processing menu entry '{}'.".format(entry_id)) + # print("Processing menu entry '{}'.".format(entry_id)) # Filter out entries protected with authentication. if entry.authentication: if not flask_login.current_user.is_authenticated: - #print("Hiding menu entry '{}', accessible only to authenticated users.".format(entry_id)) + # print("Hiding menu entry '{}', accessible only to authenticated users.".format(entry_id)) continue # Filter out entries protected with authorization. @@ -90,12 +94,12 @@ def _filter_menu_entries(entries, **kwargs): # Authorization rules may be specified as instances of flask_principal.Permission. if isinstance(authspec, flask_principal.Permission): if not authspec.can(): - #print("Hiding menu entry '{}', accessible only to '{}'.".format(entry_id, str(authspec))) + # print("Hiding menu entry '{}', accessible only to '{}'.".format(entry_id, str(authspec))) hideflag = True - # Authorization rules may be specified as indices to Vial.acl permission dictionary. + # Authorization rules may be specified as indices to hawat.acl permission dictionary. else: - if not vial.acl.PERMISSIONS[authspec].can(): - #print("Hiding menu entry '{}', accessible only to '{}'.".format(entry_id, str(authspec))) + if not hawat.acl.PERMISSIONS[authspec].can(): + # print("Hiding menu entry '{}', accessible only to '{}'.".format(entry_id, str(authspec))) hideflag = True if hideflag: continue @@ -103,7 +107,7 @@ def _filter_menu_entries(entries, **kwargs): if entry.type == ENTRY_SUBMENU: # Filter out empty submenus. if not _filter_menu_entries(entry._entries, **kwargs): # pylint: disable=locally-disabled,protected-access - #print("Hiding menu entry '{}', empty submenu.".format(entry_id)) + # print("Hiding menu entry '{}', empty submenu.".format(entry_id)) continue if entry.type == ENTRY_VIEW: @@ -111,20 +115,21 @@ def _filter_menu_entries(entries, **kwargs): if hasattr(entry.view, 'authorize_item_action'): params = entry._pick_params(kwargs) # pylint: disable=locally-disabled,protected-access if not entry.view.authorize_item_action(**params): - #print("Hiding menu entry '{}', inaccessible item action for item '{}'.".format(entry_id, str(item))) + # print("Hiding menu entry '{}', inaccessible item action for item '{}'.".format(entry_id, str(item))) continue # Check item change validation callback, if exists. if hasattr(entry.view, 'validate_item_change'): params = entry._pick_params(kwargs) # pylint: disable=locally-disabled,protected-access if not entry.view.validate_item_change(**params): - #print("Hiding menu entry '{}', invalid item change for item '{}'.".format(entry_id, str(item))) + # print("Hiding menu entry '{}', invalid item change for item '{}'.".format(entry_id, str(item))) continue result[entry_id] = entry return result + def _get_menu_entries(entries, **kwargs): """ *Helper function*. Return filtered and sorted menu entries for current user. @@ -138,34 +143,35 @@ def _get_menu_entries(entries, **kwargs): list( _filter_menu_entries(entries, **kwargs).values() ), - key = lambda x: x.position + key=lambda x: x.position ) -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- class MenuEntry: # pylint: disable=locally-disabled,too-many-instance-attributes """ Base class for all menu entries. """ + def __init__(self, ident, **kwargs): - self.type = None - self.ident = ident - self.position = kwargs.get('position', 0) - self.group = kwargs.get('group', None) - self._title = kwargs.get('title', None) - self._icon = kwargs.get('icon', None) - self._legend = kwargs.get('legend', None) - self._params = kwargs.get('params', None) - self.paramlist = kwargs.get('paramlist', None) - self.hidetitle = kwargs.get('hidetitle', False) - self.hideicon = kwargs.get('hideicon', False) + self.type = None + self.ident = ident + self.position = kwargs.get('position', 0) + self.group = kwargs.get('group', None) + self._title = kwargs.get('title', None) + self._icon = kwargs.get('icon', None) + self._legend = kwargs.get('legend', None) + self._params = kwargs.get('params', None) + self.paramlist = kwargs.get('paramlist', None) + self.hidetitle = kwargs.get('hidetitle', False) + self.hideicon = kwargs.get('hideicon', False) self.hidelegend = kwargs.get('hidelegend', False) - self.resptitle = kwargs.get('resptitle', False) - self.respicon = kwargs.get('respicon', False) + self.resptitle = kwargs.get('resptitle', False) + self.respicon = kwargs.get('respicon', False) self.resplegend = kwargs.get('resplegend', False) - self.cssclass = kwargs.get('cssclass', None) + self.cssclass = kwargs.get('cssclass', None) def __repr__(self): return '{}<type={},ident={}>'.format( @@ -182,7 +188,7 @@ class MenuEntry: # pylint: disable=locally-disabled,too-many-instance-attribute result = params if self.paramlist is not None: klist = result.keys() & self.paramlist - result = {k:result[k] for k in klist} + result = {k: result[k] for k in klist} return result def get_title(self, **kwargs): @@ -267,11 +273,11 @@ class SubmenuEntry(MenuEntry): def __init__(self, ident, **kwargs): super().__init__(ident, **kwargs) - self.type = ENTRY_SUBMENU - self.align_right = kwargs.get('align_right', False) + self.type = ENTRY_SUBMENU + self.align_right = kwargs.get('align_right', False) self.authentication = kwargs.get('authentication', False) - self.authorization = kwargs.get('authorization', []) - self._entry_list = collections.OrderedDict() + self.authorization = kwargs.get('authorization', []) + self._entry_list = collections.OrderedDict() @property def _entries(self): @@ -433,7 +439,7 @@ class ViewEntry(MenuEntry): ) def is_active(self, request, **kwargs): - #print("Checking if view menu entry '{}' is active.".format(self.ident)) + # print("Checking if view menu entry '{}' is active.".format(self.ident)) params = self._pick_params(kwargs) return _is_active( self.get_url(**params), @@ -458,10 +464,10 @@ class LinkEntry(MenuEntry): def __init__(self, ident, **kwargs): super().__init__(ident, **kwargs) - self.type = ENTRY_LINK + self.type = ENTRY_LINK self.authentication = kwargs.get('authentication', False) - self.authorization = kwargs.get('authorization', []) - self._url = kwargs.get('url') + self.authorization = kwargs.get('authorization', []) + self._url = kwargs.get('url') def get_url(self, **kwargs): """ @@ -488,7 +494,7 @@ class LinkEntry(MenuEntry): ) def is_active(self, request, **kwargs): - #print("Checking if link menu entry '{}' is active.".format(self.ident)) + # print("Checking if link menu entry '{}' is active.".format(self.ident)) params = self._pick_params(kwargs) return _is_active( self.get_url(**params), @@ -503,9 +509,9 @@ class TestEntry(MenuEntry): def __init__(self, ident, **kwargs): super().__init__(ident, **kwargs) - self.type = ENTRY_TEST + self.type = ENTRY_TEST self.authentication = kwargs.get('authentication', False) - self.authorization = kwargs.get('authorization', []) + self.authorization = kwargs.get('authorization', []) def get_entries(self, **kwargs): return [] @@ -527,6 +533,7 @@ class Menu: """ Class for application menu. """ + def __init__(self): self._entries = collections.OrderedDict() @@ -552,7 +559,7 @@ class Menu: :param dict kwargs: Additional arguments, that will be passed to the constructor of the appropriate entry class. """ entry = None - if entry_type == ENTRY_SUBMENU: + if entry_type == ENTRY_SUBMENU: entry = SubmenuEntry(ident, **kwargs) elif entry_type == ENTRY_SUBMENUDB: entry = DbSubmenuEntry(ident, **kwargs) @@ -580,29 +587,28 @@ class Menu: self._entries[path[0]].add_entry(path[1], entry) -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- if __name__ == '__main__': - MENU = Menu() - MENU.add_entry('test', 'test0', position = 10) - MENU.add_entry('test', 'test1', position = 10) - MENU.add_entry('test', 'test2', position = 20) - MENU.add_entry('test', 'test3', position = 40) - MENU.add_entry('test', 'test4', position = 30) - - MENU.add_entry('submenu', 'sub1', position = 50) - MENU.add_entry('submenu', 'sub2', position = 60) - - MENU.add_entry('test', 'sub1.test1', position = 10) - MENU.add_entry('test', 'sub1.test2', position = 20) - MENU.add_entry('test', 'sub1.test3', position = 40) - MENU.add_entry('test', 'sub1.test4', position = 30) - MENU.add_entry('test', 'sub2.test1', position = 10) - MENU.add_entry('test', 'sub2.test2', position = 20) - MENU.add_entry('test', 'sub2.test3', position = 40) - MENU.add_entry('test', 'sub2.test4', position = 30) + MENU.add_entry('test', 'test0', position=10) + MENU.add_entry('test', 'test1', position=10) + MENU.add_entry('test', 'test2', position=20) + MENU.add_entry('test', 'test3', position=40) + MENU.add_entry('test', 'test4', position=30) + + MENU.add_entry('submenu', 'sub1', position=50) + MENU.add_entry('submenu', 'sub2', position=60) + + MENU.add_entry('test', 'sub1.test1', position=10) + MENU.add_entry('test', 'sub1.test2', position=20) + MENU.add_entry('test', 'sub1.test3', position=40) + MENU.add_entry('test', 'sub1.test4', position=30) + MENU.add_entry('test', 'sub2.test1', position=10) + MENU.add_entry('test', 'sub2.test2', position=20) + MENU.add_entry('test', 'sub2.test3', position=40) + MENU.add_entry('test', 'sub2.test4', position=30) import pprint diff --git a/lib/hawat/templates/app-main.js b/lib/hawat/templates/app-main.js index fc16a8516793c3649ccd432c35c63ab194b11b50..d63346b2322130cc5b63958b466c932020236bd4 100644 --- a/lib/hawat/templates/app-main.js +++ b/lib/hawat/templates/app-main.js @@ -224,22 +224,22 @@ var Hawat = (function () { Hawat application configurations. */ var _configs = { - 'APPLICATION_ROOT': '{{ vial_current_app.config['APPLICATION_ROOT'] }}' + 'APPLICATION_ROOT': '{{ hawat_current_app.config['APPLICATION_ROOT'] }}' }; /** Hawat application icon set. */ - var _icons = {{ vial_current_app.icons | tojson | safe }}; + var _icons = {{ hawat_current_app.icons | tojson | safe }}; /** Data structure containing registrations of context search action groups for particular object types. */ var _csag = { -{%- for csag_name in vial_current_app.csag.keys() | sort %} +{%- for csag_name in hawat_current_app.csag.keys() | sort %} '{{ csag_name }}': [ - {%- for csag in vial_current_app.csag[csag_name] %} + {%- for csag in hawat_current_app.csag[csag_name] %} {%- if 'view' in csag %} { 'title': '{{ _(csag.title, name = '{name}') }}', @@ -262,9 +262,9 @@ var Hawat = (function () { for particular object types. */ var _oads = { -{%- for oads_name in vial_current_app.oads.keys() | sort %} +{%- for oads_name in hawat_current_app.oads.keys() | sort %} '{{ oads_name }}': [ - {%- for oads in vial_current_app.oads[oads_name] %} + {%- for oads in hawat_current_app.oads[oads_name] %} {%- if 'view' in oads %} { 'endpoint': '{{ oads.view.get_view_endpoint() }}', diff --git a/lib/hawat/templates/registration/email_admins.txt b/lib/hawat/templates/registration/email_admins.txt index 4ec6751c6248fb055a02b53ef949107cb03e5fc1..5c63ba521fb0e2141b766b2e0109dd26a2350b91 100644 --- a/lib/hawat/templates/registration/email_admins.txt +++ b/lib/hawat/templates/registration/email_admins.txt @@ -1,6 +1,6 @@ {{ _('Dear administrator,') | wordwrap }} -{{ _('a new account "%(item_id)s" was just registered in %(app_name)s. Please review the following request and activate or delete the account:', item_id = account.login, app_name = vial_appname) | wordwrap }} +{{ _('a new account "%(item_id)s" was just registered in %(app_name)s. Please review the following request and activate or delete the account:', item_id = account.login, app_name = hawat_appname) | wordwrap }} {{ '{:16s}'.format(_('Login:')) }} {{ account.login }} {{ '{:16s}'.format(_('Full name:')) }} {{ account.fullname }} @@ -24,4 +24,4 @@ {{ _('Have a nice day') | wordwrap }} --- {{ vial_appname }} +-- {{ hawat_appname }} diff --git a/lib/hawat/templates/registration/email_managers.txt b/lib/hawat/templates/registration/email_managers.txt index c3b79bb78b0aea36f9464cc959db4eedc9cc9c07..2f8ae69b96b3e5739be5031e876e2b3621a42543 100644 --- a/lib/hawat/templates/registration/email_managers.txt +++ b/lib/hawat/templates/registration/email_managers.txt @@ -1,6 +1,6 @@ {{ _('Dear group manager,') | wordwrap }} -{{ _('a new account "%(item_id)s" was just registered in %(app_name)s and user requested membership in your group "%(group_id)s". Please review the following information and approve or reject the request:', item_id = account.login, app_name = vial_appname, group_id = group.name) | wordwrap }} +{{ _('a new account "%(item_id)s" was just registered in %(app_name)s and user requested membership in your group "%(group_id)s". Please review the following information and approve or reject the request:', item_id = account.login, app_name = hawat_appname, group_id = group.name) | wordwrap }} {{ '{:16s}'.format(_('Login:')) }} {{ account.login }} {{ '{:16s}'.format(_('Full name:')) }} {{ account.fullname }} @@ -17,4 +17,4 @@ {{ _('Have a nice day') | wordwrap }} --- {{ vial_appname }} +-- {{ hawat_appname }} diff --git a/lib/hawat/templates/registration/email_user.txt b/lib/hawat/templates/registration/email_user.txt index fd0a4f9051fd07f83c3f0a07324788cd36193f86..0eaccda2562a7a15b514a823ceb411cb92f47834 100644 --- a/lib/hawat/templates/registration/email_user.txt +++ b/lib/hawat/templates/registration/email_user.txt @@ -1,6 +1,6 @@ {{ _('Dear user,') | wordwrap }} -{{ _('this email is a confirmation, that you have successfully registered your new user account "%(item_id)s" in %(app_name)s.', item_id = account.login, app_name = vial_appname) | wordwrap }} +{{ _('this email is a confirmation, that you have successfully registered your new user account "%(item_id)s" in %(app_name)s.', item_id = account.login, app_name = hawat_appname) | wordwrap }} {{ _('During the registration process you have provided following information:') | wordwrap }} @@ -28,4 +28,4 @@ {{ _('Have a nice day') | wordwrap }} --- {{ vial_appname }} +-- {{ hawat_appname }} diff --git a/lib/hawat/test/__init__.py b/lib/hawat/test/__init__.py index 7998eb711b1001140a68b66816fe2b951edd36af..0bbf8c91346a83cf8e66f18a3a37234b12cb0365 100644 --- a/lib/hawat/test/__init__.py +++ b/lib/hawat/test/__init__.py @@ -1,20 +1,455 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- #------------------------------------------------------------------------------- -# This file is part of Mentat system (https://mentat.cesnet.cz/). -# -# Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. #------------------------------------------------------------------------------- """ -Base library for Hawat unit tests. +Base library for web interface unit tests. """ -from vial.test import RegistrationVialTestCase -class RegistrationHawatTestCase(RegistrationVialTestCase): +import io +import unittest +import pprint +import logging +import flask_mail +import hawat +import hawat.app +import hawat.db + + +class do_as_user_decorator: # pylint: disable=locally-disabled,invalid-name,too-few-public-methods + """ + Decorator class for accessing application endpoints as given user. + """ + def __init__(self, user_name, login_func_name = 'login_dev', login_func_params = None): + self.user_name = user_name + self.login_func_name = login_func_name + self.login_func_params = login_func_params or {} + + def __call__(self, func): + def wrapped_f(other_self, *args, **kwargs): + login_func = getattr(other_self, self.login_func_name) + response = login_func(self.user_name, **self.login_func_params) + other_self.assertEqual(response.status_code, 200) + + func(other_self, *args, **kwargs) + + response = other_self.logout() + other_self.assertEqual(response.status_code, 200) + + return wrapped_f + + +def app_context_wrapper_decorator(func): + """ + Decorator class for conditional wrapping of given function with application context. + """ + def wrapped_f(self, *args, **kwargs): + if 'with_app_ctx' not in kwargs or not kwargs['with_app_ctx']: + return func(self, *args, **kwargs) + with self.app.app_context(): + return func(self, *args, **kwargs) + + return wrapped_f + + +class HawatTestCase(unittest.TestCase): + """ + Class for testing :py:class:`hawat.app.Hawat` application. + """ + + logger = logging.getLogger() + logger.level = logging.DEBUG + + def on_email_sent(self, message, app): # pylint: disable=locally-disabled,unused-argument + """ + Signal handler for handling :py:func:`flask_mail.email_dispatched` signal. + Log subject and recipients of all emails that have been sent. + """ + #pprint.pprint(message) + #app.logger.info( + # "Sent email '%s' to '%s'", + # message.subject, + # ', '.join(message.recipients) + #) + self.mailbox.append(message) + + #-------------------------------------------------------------------------- + + def setUp(self): + self.setup_logging() + self.mailbox = [] + self.fixtures_db = [] + + self.app = self.setup_app() + self.client = self.app.test_client() + self.setup_db() + + def setup_logging(self): + """ + Setup logging configuration for testing purposes. + """ + #for hdlr in self.logger.handlers: + # self.logger.removeHandler(hdlr) + self.loghdlr = logging.StreamHandler(io.StringIO()) + self.logger.addHandler(self.loghdlr) + + def setup_app(self): + """ + Setup application object. + """ + raise NotImplementedError() + + def setup_db(self): + """ + Perform database setup. + """ + with self.app.app_context(): + hawat.db.db_get().drop_all() + hawat.db.db_get().create_all() + self.setup_fixtures_db() + + def get_fixtures_db(self, app): + raise NotImplementedError() + + def setup_fixtures_db(self): + """ + Setup general database object fixtures. + """ + fixture_list = self.get_fixtures_db(self.app) + for dbobject in fixture_list: + hawat.db.db_session().add(dbobject) + hawat.db.db_session().commit() + self.fixtures_db.extend(fixture_list) + + #-------------------------------------------------------------------------- + + def tearDown(self): + self.teardown_logging() + self.teardown_db() + + def teardown_logging(self): + """ + Teardown logging configuration for testing purposes. + """ + #print( + # "CAPTURED LOG CONTENTS:\n{}".format( + # self.loghdlr.stream.getvalue() + # ) + #) + self.loghdlr.stream.close() + self.logger.removeHandler(self.loghdlr) + + def teardown_db(self): + with self.app.app_context(): + hawat.db.db_get().drop_all() + + #-------------------------------------------------------------------------- + + def login_dev(self, login): + """ + Login given user with *auth_dev* module. + """ + return self.client.post( + '/auth_dev/login', + data = dict(login = login, submit = 'Login'), + follow_redirects = True + ) + + def login_pwd(self, login, password): + """ + Login given user with *auth_pwd* module. + """ + return self.client.post( + '/auth_pwd/login', + data = dict(login = login, password = password, submit = 'Login'), + follow_redirects = True + ) + + def login_env(self, login, envvar = 'eppn'): + """ + Login given user with *auth_env* module. + """ + return self.client.get( + '/auth_env/login', + environ_base = {envvar: login}, + follow_redirects = True + ) + + def logout(self): + """ + Logout current user. + """ + return self.client.get( + '/logout', + follow_redirects = True + ) + + #-------------------------------------------------------------------------- + + def log_get(self): + """ + Get content written to log so far. + """ + return self.loghdlr.stream.getvalue() + + def log_clear(self): + """ + Clear log content. + """ + self.loghdlr.stream.close() + self.loghdlr.stream = io.StringIO() + + #-------------------------------------------------------------------------- + + def mailbox_clear(self): + """ + Clear internal mailbox. + """ + self.mailbox = [] + + def mailbox_monitoring(self, state): + """ + Enable/disable mailbox monitoring. + """ + if state == 'on': + flask_mail.email_dispatched.connect(self.on_email_sent) + return + + if state == 'off': + flask_mail.email_dispatched.disconnect(self.on_email_sent) + return + + raise ValueError( + 'Invalid parameter for mailbox_monitoring, must be "on" or "off", received {}'.format( + str(state) + ) + ) + + #-------------------------------------------------------------------------- + + def user_model(self): + """ + Get user model class. + """ + return self.app.get_model(hawat.const.MODEL_USER) + + @app_context_wrapper_decorator + def user_get(self, user_name, **kwargs): # pylint: disable=locally-disabled,unused-argument + """ + Get user object according to given user name from database. + """ + user_model = self.user_model() + return hawat.db.db_session().query(user_model).filter(user_model.login == user_name).one_or_none() + + @app_context_wrapper_decorator + def user_save(self, user_object, **kwargs): # pylint: disable=locally-disabled,unused-argument + """ + Update given user object within database. + """ + hawat.db.db_session().add(user_object) + hawat.db.db_session().commit() + + @app_context_wrapper_decorator + def user_id(self, user_name, **kwargs): # pylint: disable=locally-disabled,unused-argument + """ + Get ID of user with given name within database. + """ + uobj = self.user_get(user_name) + return uobj.id + + @app_context_wrapper_decorator + def user_enabled(self, user_name, state, **kwargs): # pylint: disable=locally-disabled,unused-argument + """ + Enable/disable given user within database. + """ + user = self.user_get(user_name) + user.enabled = state + self.user_save(user) + + #-------------------------------------------------------------------------- + + def group_model(self): + """ + Get user model class. + """ + return self.app.get_model(hawat.const.MODEL_GROUP) + + @app_context_wrapper_decorator + def group_get(self, group_name, **kwargs): # pylint: disable=locally-disabled,unused-argument + """ + Get group object according to given group name within database. + """ + group_model = self.group_model() + return hawat.db.db_session().query(group_model).filter(group_model.name == group_name).one_or_none() + + @app_context_wrapper_decorator + def group_save(self, group_object, **kwargs): # pylint: disable=locally-disabled,unused-argument + """ + Update given group object within database. + """ + hawat.db.db_session().add(group_object) + hawat.db.db_session().commit() + + @app_context_wrapper_decorator + def group_id(self, group_name, **kwargs): # pylint: disable=locally-disabled,unused-argument + """ + Get ID of given group within database. + """ + gobj = self.group_get(group_name) + return gobj.id + + @app_context_wrapper_decorator + def group_enabled(self, group_name, state, **kwargs): # pylint: disable=locally-disabled,unused-argument + """ + Enable/disable given group within database. + """ + group = self.group_get(group_name) + group.enabled = state + self.group_save(group) + + #-------------------------------------------------------------------------- + + def assertGetURL(self, url, status_code = 200, content_checks = None, print_response = False, follow_redirects = True): # pylint: disable=locally-disabled,invalid-name + """ + Perform GET request and check some default assertions against the response. + """ + response = self.client.get( + url, + follow_redirects = follow_redirects + ) + if print_response: + print("--------------------------------------------------------------------------------") + print("Response for GET {}: {} ({})".format(url, response.status_code, response.status)) + pprint.pprint(response.headers) + pprint.pprint(response.data) + print("--------------------------------------------------------------------------------") + self.assertEqual(response.status_code, status_code) + if content_checks: + for cch in content_checks: + self.assertTrue(cch in response.data) + return response + + def assertPostURL(self, url, data, status_code = 200, content_checks = None, print_response = False, follow_redirects = True): # pylint: disable=locally-disabled,invalid-name + """ + Perform POST request and check some default assertions against the response. + """ + response = self.client.post( + url, + data = data, + follow_redirects = follow_redirects + ) + if print_response: + print("--------------------------------------------------------------------------------") + print("Response for POST {}, {}: {} ({})".format(url, pprint.pformat(data), response.status_code, response.status)) + pprint.pprint(response.headers) + pprint.pprint(response.data) + print("--------------------------------------------------------------------------------") + self.assertEqual(response.status_code, status_code) + if content_checks: + for cch in content_checks: + self.assertTrue(cch in response.data) + return response + + def assertMailbox(self, checklist): # pylint: disable=locally-disabled,invalid-name + """ + Check internal mailbox. + """ + for attr_name in ('subject', 'sender', 'recipients', 'cc', 'bcc', 'body', 'html'): + if attr_name in checklist: + self.assertEqual( + list( + map( + lambda x: getattr(x, attr_name), + self.mailbox, + ) + ), + checklist[attr_name] + ) + +class ItemCreateHawatTestCase(HawatTestCase): + """ + Class for testing :py:class:`hawat.app.Hawat` application item creation views. + """ + maxDiff = None + + def assertCreate(self, item_model, url, data, content_checks = None, print_response = False): # pylint: disable=locally-disabled,invalid-name + """ + Perform attempt to create given item. + """ + + # Verify, that the item form correctly displays. + response = self.assertGetURL( + url, + 200, + [ + '<form method="POST" action="{}" id="form-{}-create'.format( + url, + item_model.__name__.lower() + ).encode('utf8'), + b'<div class="btn-toolbar" role="toolbar" aria-label="Form submission buttons">' + ], + print_response = print_response + ) + + # Attempt to send empty item form. There is always at least one mandatory + # form field, so we should get some "This field is required." error. + request_data = {'submit': 'Submit'} + response = self.assertPostURL( + url, + request_data, + 200, + [ + b'This field is required.', + b'help-block form-error' + ], + print_response = print_response + ) + + # Attempt to send form with some mandatory fields missing. + #for idx, param in enumerate(data): + # if idx == len(data) - 1: + # break + # response = self.client.post( + # url, + # follow_redirects = True, + # data = { + # i[0]: i[1] for i in data[0:idx+1] + # } + # ) + # self.assertEqual(response.status_code, 200) + # self.assertTrue(b'This field is required.' in response.data) + # self.assertTrue(b'help-block form-error' in response.data) + + # Attempt to send form with valid data. + request_data = { + i[0]: i[1] for i in data + } + request_data['submit'] = 'Submit' + response = self.assertPostURL( + url, + request_data, + 200, + [ + b'<div class="alert alert-success alert-dismissible">' + ], + print_response = print_response + ) + if content_checks: + for cch in content_checks: + self.assertTrue(cch in response.data) + return response + + +class RegistrationHawatTestCase(HawatTestCase): + """ + Class for testing :py:class:`hawat.app.Hawat` application registration views. + """ + maxDiff = None + user_fixture = { 'apikey': None, 'email': 'test.user@domain.org', @@ -31,3 +466,136 @@ class RegistrationHawatTestCase(RegistrationVialTestCase): 'roles': ['user'], 'timezone': None } + + def assertRegisterFail(self, url, data, environ_base = None): # pylint: disable=locally-disabled,invalid-name + response = response = self.client.get( + url, + follow_redirects = True, + environ_base = environ_base, + ) + self.assertEqual(response.status_code, 200) + self.assertTrue(b'User account registration' in response.data) + + for idx, param in enumerate(data): + if idx == len(data) - 1: + break + response = response = self.client.post( + url, + follow_redirects = True, + environ_base = environ_base, + data = { + i[0]: i[1] for i in data[0:idx+1] + } + ) + self.assertEqual(response.status_code, 200) + self.assertTrue(b'This field is required.' in response.data) + self.assertTrue(b'help-block form-error' in response.data) + + response = response = self.client.post( + url, + follow_redirects = True, + environ_base = environ_base, + data = { + i[0]: i[1] for i in data + } + ) + self.assertEqual(response.status_code, 200) + self.assertTrue( + b'Please use different login, the "user" is already taken.' in response.data or \ + b'Please use different login, the "user" is already taken.' in response.data + ) + + + def assertRegister(self, url, data, emails, environ_base = None): # pylint: disable=locally-disabled,invalid-name + uname = 'test' + self.mailbox_monitoring('on') + + response = response = self.client.get( + url, + follow_redirects = True, + environ_base = environ_base + ) + self.assertEqual(response.status_code, 200) + self.assertTrue(b'User account registration' in response.data) + + for idx, param in enumerate(data): + if idx == len(data) - 1: + break + response = response = self.client.post( + url, + follow_redirects = True, + environ_base = environ_base, + data = { + i[0]: i[1] for i in data[0:idx+1] + } + ) + self.assertEqual(response.status_code, 200) + self.assertTrue(b'This field is required.' in response.data) + self.assertTrue(b'help-block form-error' in response.data) + + response = response = self.client.post( + url, + follow_redirects = True, + environ_base = environ_base, + data = { + i[0]: i[1] for i in data + } + ) + self.assertEqual(response.status_code, 200) + self.assertTrue(b'User account <strong>test (Test User)</strong> was successfully registered.' in response.data) + with self.app.app_context(): + uobj = self.user_get(uname) + self.assertTrue(uobj) + self.assertMailbox( + { + 'subject': [ + '[{}] Account registration - {}'.format(self.app.config['APPLICATION_NAME'], uname), + '[{}] Account registration - {}'.format(self.app.config['APPLICATION_NAME'], uname) + ], + 'sender': [ + 'root@unittest', + 'root@unittest' + ], + 'recipients': [ + ['admin@unittest'], + ['test.user@domain.org'] + ], + 'cc': [[],[]], + 'bcc': [[], ['admin@unittest']], + 'body': emails['txt'], + 'html': emails['html'] + } + ) + + self.mailbox_monitoring('off') + + with self.app.app_context(): + user = self.user_get(uname) + user_dict = user.to_dict() + del user_dict['createtime'] + del user_dict['password'] + self.assertEqual( + user_dict, + self.user_fixture + ) + response = self.login_dev(uname) + self.assertEqual(response.status_code, 403) + #self.assertTrue(b'is currently disabled, you are not permitted to log in.' in response.data) + + with self.app.app_context(): + user = self.user_get(uname) + user.set_state_enabled() + self.user_save(user) + + with self.app.app_context(): + user = self.user_get(uname) + user_dict = user.to_dict() + del user_dict['createtime'] + del user_dict['password'] + response = self.login_dev(uname) + self.assertEqual(response.status_code, 200) + self.assertTrue(b'You have been successfully logged in as' in response.data) + + response = self.logout() + self.assertEqual(response.status_code, 200) + self.assertTrue(b'You have been successfully logged out' in response.data) diff --git a/lib/vial/test/fixtures.py b/lib/hawat/test/fixtures.py similarity index 80% rename from lib/vial/test/fixtures.py rename to lib/hawat/test/fixtures.py index 3dcf3dc623db3ae7a4823ff770570d02cfc8effd..1a0b69b06b7edcf4c96a14faa86005367291f634 100644 --- a/lib/vial/test/fixtures.py +++ b/lib/hawat/test/fixtures.py @@ -10,7 +10,7 @@ Base library for web interface unit test database fixtures. """ -import vial.const +import hawat.const DEMO_GROUP_A = 'DEMO_GROUP_A' @@ -23,8 +23,8 @@ def get_fixtures_db(app): """ fixture_list = list() - user_model = app.get_model(vial.const.MODEL_USER) - group_model = app.get_model(vial.const.MODEL_GROUP) + user_model = app.get_model(hawat.const.MODEL_USER) + group_model = app.get_model(hawat.const.MODEL_GROUP) def _gen_user(user_name): user = user_model( @@ -33,7 +33,7 @@ def get_fixtures_db(app): email = '{}@bogus-domain.org'.format(user_name), roles = list( set( - [vial.const.ROLE_USER, user_name] + [hawat.const.ROLE_USER, user_name] ) ), enabled = True, @@ -42,10 +42,10 @@ def get_fixtures_db(app): fixture_list.append(user) return user - account_user = _gen_user(vial.const.ROLE_USER) - account_developer = _gen_user(vial.const.ROLE_DEVELOPER) - account_maintainer = _gen_user(vial.const.ROLE_MAINTAINER) - account_admin = _gen_user(vial.const.ROLE_ADMIN) + account_user = _gen_user(hawat.const.ROLE_USER) + account_developer = _gen_user(hawat.const.ROLE_DEVELOPER) + account_maintainer = _gen_user(hawat.const.ROLE_MAINTAINER) + account_admin = _gen_user(hawat.const.ROLE_ADMIN) def _gen_group(group_name, group_descr): group = group_model( diff --git a/lib/hawat/test/runner.py b/lib/hawat/test/runner.py index 27838f1f8b8d371f5fb14a1a477342078987b45e..0a027bebcad37d1d6b06bc4440c1be6ed7713e56 100644 --- a/lib/hawat/test/runner.py +++ b/lib/hawat/test/runner.py @@ -12,8 +12,8 @@ Base library for Hawat unit tests. """ -import vial.const -import vial.test.fixtures +import hawat.const +import hawat.test.fixtures from mentat.datatype.sqldb import UserModel, GroupModel, ItemChangeLogModel, SettingsReportingModel, FilterModel, NetworkModel from mentat.const import CKEY_CORE_DATABASE, CKEY_CORE_DATABASE_EVENTSTORAGE @@ -42,9 +42,9 @@ def _config_testapp_hawat(app_config): app_config['EMAIL_ADMINS'] = ['admin@unittest'] app_config['DISABLE_MAIL_LOGGING'] = True app_config['MODELS'] = { - vial.const.MODEL_USER: UserModel, - vial.const.MODEL_GROUP: GroupModel, - vial.const.MODEL_ITEM_CHANGELOG: ItemChangeLogModel + hawat.const.MODEL_USER: UserModel, + hawat.const.MODEL_GROUP: GroupModel, + hawat.const.MODEL_ITEM_CHANGELOG: ItemChangeLogModel } app_config[CFGKEY_MENTAT_CORE][CKEY_CORE_DATABASE][CKEY_CORE_DATABASE_EVENTSTORAGE] = { 'dbname': 'mentat_utest', @@ -66,11 +66,11 @@ class TestRunnerMixin: ) def get_fixtures_db(self, app): - fixture_list = vial.test.fixtures.get_fixtures_db(app) + fixture_list = hawat.test.fixtures.get_fixtures_db(app) for fixture in fixture_list: - if isinstance(fixture, app.get_model(vial.const.MODEL_USER)): + if isinstance(fixture, app.get_model(hawat.const.MODEL_USER)): fixture.organization = 'BOGUS DOMAIN, a.l.e.' - elif isinstance(fixture, app.get_model(vial.const.MODEL_GROUP)): + elif isinstance(fixture, app.get_model(hawat.const.MODEL_GROUP)): fixture.source = 'manual' SettingsReportingModel(group = fixture) FilterModel( diff --git a/lib/hawat/test/test_app.py b/lib/hawat/test/test_app.py index 58c5d8efe2de69b564631e9169ad1eb476aa0401..4c8e2320b57380b7782666b01d524306d293e2a7 100644 --- a/lib/hawat/test/test_app.py +++ b/lib/hawat/test/test_app.py @@ -18,17 +18,17 @@ import unittest from mentat.datatype.sqldb import UserModel, GroupModel -import vial.const -import vial.test -import vial.db -from vial.test import VialTestCase +import hawat.const +import hawat.test +import hawat.db +from hawat.test import HawatTestCase from hawat.test.runner import TestRunnerMixin #logging.disable(logging.CRITICAL+1000) -class AppTestCase(TestRunnerMixin, VialTestCase): +class AppTestCase(TestRunnerMixin, HawatTestCase): """ Class for testing :py:class:`hawat.base.HawatApp` application. """ @@ -38,7 +38,7 @@ class AppTestCase(TestRunnerMixin, VialTestCase): Test the test environment. """ with self.app.app_context(): - result_users = vial.db.db_session().query(UserModel).order_by(UserModel.login).all() + result_users = hawat.db.db_session().query(UserModel).order_by(UserModel.login).all() self.assertEqual( len(result_users), 4 @@ -47,7 +47,7 @@ class AppTestCase(TestRunnerMixin, VialTestCase): list(map(lambda x: x.login, result_users)), ['admin', 'developer', 'maintainer', 'user'] ) - result_groups = vial.db.db_session().query(GroupModel).order_by(GroupModel.name).all() + result_groups = hawat.db.db_session().query(GroupModel).order_by(GroupModel.name).all() self.assertEqual( len(result_groups), 2 @@ -69,7 +69,7 @@ class AppTestCase(TestRunnerMixin, VialTestCase): """ Test login/logout with *auth_dev* module. """ - response = self.login_dev(vial.const.ROLE_ADMIN) + response = self.login_dev(hawat.const.ROLE_ADMIN) self.assertEqual(response.status_code, 200) self.assertTrue(b'You have been successfully logged in as' in response.data) @@ -83,7 +83,7 @@ class AppTestCase(TestRunnerMixin, VialTestCase): """ modlist = ('/users/list', '/groups/list', '/filters/list', '/networks/list') - response = self.login_dev(vial.const.ROLE_ADMIN) + response = self.login_dev(hawat.const.ROLE_ADMIN) self.assertEqual(response.status_code, 200) for mod in modlist: response = self.client.get(mod, follow_redirects=True) @@ -91,7 +91,7 @@ class AppTestCase(TestRunnerMixin, VialTestCase): response = self.logout() self.assertEqual(response.status_code, 200) - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_USER) def test_modules_mgmt_user(self): """ Basic tests of various pluggable modules. @@ -101,7 +101,7 @@ class AppTestCase(TestRunnerMixin, VialTestCase): response = self.client.get(mod, follow_redirects=True) self.assertEqual(response.status_code, 403) - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_DEVELOPER) def test_modules_mgmt_developer(self): """ Basic tests of various pluggable modules. @@ -111,7 +111,7 @@ class AppTestCase(TestRunnerMixin, VialTestCase): response = self.client.get(mod, follow_redirects=True) self.assertEqual(response.status_code, 403) - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_MAINTAINER) def test_modules_mgmt_maintainer(self): """ Basic tests of various pluggable modules. @@ -121,7 +121,7 @@ class AppTestCase(TestRunnerMixin, VialTestCase): response = self.client.get(mod, follow_redirects=True) self.assertEqual(response.status_code, 200) - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) + @hawat.test.do_as_user_decorator(hawat.const.ROLE_ADMIN) def test_modules_mgmt_admin(self): """ Basic tests of various pluggable modules. diff --git a/lib/hawat/translations/cs/LC_MESSAGES/messages.po b/lib/hawat/translations/cs/LC_MESSAGES/messages.po index cba0886f547ce43730e9ca16ad3dbe38fe6478d7..e43c6856742e58c26c7ae37059829f72e19eacfe 100644 --- a/lib/hawat/translations/cs/LC_MESSAGES/messages.po +++ b/lib/hawat/translations/cs/LC_MESSAGES/messages.po @@ -38,11 +38,11 @@ msgstr "Dashboardy" msgid "More" msgstr "VÃce" -#: ../hawat/config.py:159 ../vial/config.py:145 +#: ../hawat/config.py:159 ../hawat/config.py:145 msgid "Administration" msgstr "Administrace" -#: ../hawat/config.py:169 ../vial/config.py:155 +#: ../hawat/config.py:169 ../hawat/config.py:155 msgid "Development" msgstr "Vývoj" @@ -65,28 +65,28 @@ msgstr "Vývoj" #: ../hawat/templates/registration/email_admins.txt:5 #: ../hawat/templates/registration/email_managers.txt:5 #: ../hawat/templates/registration/email_user.txt:7 -#: ../vial/blueprints/auth_dev/forms.py:71 -#: ../vial/blueprints/auth_pwd/forms.py:35 -#: ../vial/blueprints/auth_pwd/forms.py:62 -#: ../vial/blueprints/users/forms.py:137 ../vial/blueprints/users/forms.py:158 +#: ../hawat/blueprints/auth_dev/forms.py:71 +#: ../hawat/blueprints/auth_pwd/forms.py:35 +#: ../hawat/blueprints/auth_pwd/forms.py:62 +#: ../hawat/blueprints/users/forms.py:137 ../hawat/blueprints/users/forms.py:158 msgid "Login:" msgstr "Login:" #: ../hawat/blueprints/auth_dev/forms.py:43 #: ../hawat/blueprints/auth_env/forms.py:35 #: ../hawat/blueprints/auth_pwd/forms.py:41 -#: ../vial/blueprints/auth_dev/forms.py:80 -#: ../vial/blueprints/auth_env/forms.py:28 -#: ../vial/blueprints/auth_pwd/forms.py:71 +#: ../hawat/blueprints/auth_dev/forms.py:80 +#: ../hawat/blueprints/auth_env/forms.py:28 +#: ../hawat/blueprints/auth_pwd/forms.py:71 msgid "Requested group memberships:" msgstr "Požadovaná Älenstvà ve skupinách:" #: ../hawat/blueprints/auth_dev/forms.py:47 #: ../hawat/blueprints/auth_env/forms.py:39 #: ../hawat/blueprints/auth_pwd/forms.py:45 -#: ../vial/blueprints/auth_dev/forms.py:84 -#: ../vial/blueprints/auth_env/forms.py:32 -#: ../vial/blueprints/auth_pwd/forms.py:75 +#: ../hawat/blueprints/auth_dev/forms.py:84 +#: ../hawat/blueprints/auth_env/forms.py:32 +#: ../hawat/blueprints/auth_pwd/forms.py:75 msgid "Justification:" msgstr "OdůvodnÄ›nÃ:" @@ -125,7 +125,7 @@ msgstr "" #: ../hawat/templates/registration/email_admins.txt:6 #: ../hawat/templates/registration/email_managers.txt:6 #: ../hawat/templates/registration/email_user.txt:8 -#: ../vial/blueprints/users/forms.py:29 +#: ../hawat/blueprints/users/forms.py:29 msgid "Full name:" msgstr "Celé jméno:" @@ -143,7 +143,7 @@ msgstr "Celé jméno:" #: ../hawat/templates/registration/email_admins.txt:7 #: ../hawat/templates/registration/email_managers.txt:7 #: ../hawat/templates/registration/email_user.txt:9 -#: ../vial/blueprints/users/forms.py:36 +#: ../hawat/blueprints/users/forms.py:36 msgid "Email:" msgstr "Email:" @@ -299,18 +299,18 @@ msgid "" msgstr "Po úspěšné aktivaci se budete moci pÅ™ihlásit a zaÄÃt systém použÃvat:" #: ../hawat/blueprints/auth_env/__init__.py:126 -#: ../vial/blueprints/auth_env/__init__.py:174 +#: ../hawat/blueprints/auth_env/__init__.py:174 msgid "Unable to retrieve account login from your authentication provider." msgstr "Nelze zÃskat uživatelské jméno od VaÅ¡eho poskytovatele identity." #: ../hawat/blueprints/auth_pwd/forms.py:52 -#: ../vial/blueprints/auth_pwd/forms.py:43 -#: ../vial/blueprints/auth_pwd/forms.py:82 +#: ../hawat/blueprints/auth_pwd/forms.py:43 +#: ../hawat/blueprints/auth_pwd/forms.py:82 msgid "Password:" msgstr "Heslo:" #: ../hawat/blueprints/auth_pwd/forms.py:59 -#: ../vial/blueprints/auth_pwd/forms.py:89 +#: ../hawat/blueprints/auth_pwd/forms.py:89 msgid "Repeat Password:" msgstr "Zopakujte heslo:" @@ -365,7 +365,7 @@ msgstr "Zastavenà hledánà <strong>%(item_id)s</strong> bylo pÅ™eruÅ¡eno." #: ../hawat/blueprints/dbstatus/__init__.py:455 #: ../hawat/blueprints/users/__init__.py:107 -#: ../vial/blueprints/users/__init__.py:1185 +#: ../hawat/blueprints/users/__init__.py:1185 msgid "Object management" msgstr "Správa objektů" @@ -383,32 +383,32 @@ msgstr "Zobrazit detaily uživatelského úÄtu "%(item)s"" #: ../hawat/blueprints/dbstatus/__init__.py:633 #: ../hawat/blueprints/groups/__init__.py:66 #: ../hawat/blueprints/reports/__init__.py:237 -#: ../vial/blueprints/changelogs/__init__.py:87 -#: ../vial/blueprints/groups/__init__.py:261 -#: ../vial/blueprints/users/__init__.py:258 +#: ../hawat/blueprints/changelogs/__init__.py:87 +#: ../hawat/blueprints/groups/__init__.py:261 +#: ../hawat/blueprints/users/__init__.py:258 msgid "More actions" msgstr "VÃce akcÃ" #: ../hawat/blueprints/dbstatus/__init__.py:558 -#: ../vial/blueprints/users/__init__.py:478 +#: ../hawat/blueprints/users/__init__.py:478 #, python-format msgid "Update details of user account "%(item)s"" msgstr "Aktualizovat detaily uživatelského úÄtu "%(item)s"" #: ../hawat/blueprints/dbstatus/__init__.py:565 -#: ../vial/blueprints/users/__init__.py:901 +#: ../hawat/blueprints/users/__init__.py:901 #, python-format msgid "Disable user account "%(item)s"" msgstr "Deaktivovat uživatelský úÄet "%(item)s"" #: ../hawat/blueprints/dbstatus/__init__.py:572 -#: ../vial/blueprints/users/__init__.py:824 +#: ../hawat/blueprints/users/__init__.py:824 #, python-format msgid "Enable user account "%(item)s"" msgstr "Aktivovat uživatelský úÄet "%(item)s"" #: ../hawat/blueprints/dbstatus/__init__.py:579 -#: ../vial/blueprints/users/__init__.py:952 +#: ../hawat/blueprints/users/__init__.py:952 #, python-format msgid "Delete user account "%(item)s"" msgstr "Smazat uživatelský úÄet "%(item)s"" @@ -425,32 +425,32 @@ msgstr "Smazat uživatelský úÄet "%(item)s"" #: ../hawat/blueprints/networks/templates/networks/show.html:51 #: ../hawat/blueprints/reports/templates/reports/search.html:62 #: ../hawat/blueprints/reports/templates/reports/show.html:50 -#: ../vial/blueprints/groups/__init__.py:185 -#: ../vial/blueprints/groups/__init__.py:189 +#: ../hawat/blueprints/groups/__init__.py:185 +#: ../hawat/blueprints/groups/__init__.py:189 #, python-format msgid "View details of group "%(item)s"" msgstr "Zobrazit detaily skupiny "%(item)s"" #: ../hawat/blueprints/dbstatus/__init__.py:602 -#: ../vial/blueprints/groups/__init__.py:403 +#: ../hawat/blueprints/groups/__init__.py:403 #, python-format msgid "Update details of group "%(item)s"" msgstr "Aktualizovat detaily skupiny "%(item)s"" #: ../hawat/blueprints/dbstatus/__init__.py:606 -#: ../vial/blueprints/groups/__init__.py:782 +#: ../hawat/blueprints/groups/__init__.py:782 #, python-format msgid "Disable group "%(item)s"" msgstr "Deaktivovat skupinu "%(item)s"" #: ../hawat/blueprints/dbstatus/__init__.py:614 -#: ../vial/blueprints/groups/__init__.py:735 +#: ../hawat/blueprints/groups/__init__.py:735 #, python-format msgid "Enable group "%(item)s"" msgstr "Aktivovat group "%(item)s"" #: ../hawat/blueprints/dbstatus/__init__.py:620 -#: ../vial/blueprints/groups/__init__.py:830 +#: ../hawat/blueprints/groups/__init__.py:830 #, python-format msgid "Delete group "%(item)s"" msgstr "Smazat skupinu "%(item)s"" @@ -814,7 +814,7 @@ msgstr "" #: ../hawat/blueprints/reports/templates/reports/show.html:9 #: ../hawat/blueprints/settings_reporting/templates/settings_reporting/creatupdate.html:8 #: ../hawat/blueprints/status/templates/status/view.html:8 -#: ../vial/blueprints/home/__init__.py:53 +#: ../hawat/blueprints/home/__init__.py:53 msgid "Home" msgstr "Domů" @@ -1093,7 +1093,7 @@ msgstr "Hledat adresu <strong>%(name)s</strong> ve službÄ› DNS" #: ../hawat/blueprints/whois/__init__.py:202 #: ../hawat/blueprints/whois/forms.py:59 #: ../hawat/blueprints/whois/templates/whois/spt_label_abuse.html:10 -#: ../vial/forms.py:631 ../vial/view/__init__.py:772 +#: ../hawat/forms.py:631 ../hawat/view/__init__.py:772 msgid "Search" msgstr "Hledat" @@ -1179,8 +1179,8 @@ msgstr "Události" msgid "Show event" msgstr "Zobrazit událost" -#: ../hawat/blueprints/events/__init__.py:180 ../vial/view/__init__.py:1263 -#: ../vial/view/__init__.py:1282 +#: ../hawat/blueprints/events/__init__.py:180 ../hawat/view/__init__.py:1263 +#: ../hawat/view/__init__.py:1282 msgid "Show" msgstr "Zobrazit" @@ -1668,7 +1668,7 @@ msgstr "Negovat volbu tÅ™Ãdy:" #: ../hawat/blueprints/groups/forms.py:99 #: ../hawat/blueprints/networks/forms.py:76 #: ../hawat/blueprints/timeline/forms.py:244 -#: ../vial/blueprints/groups/forms.py:41 +#: ../hawat/blueprints/groups/forms.py:41 msgid "Description:" msgstr "Popis:" @@ -1742,7 +1742,7 @@ msgstr "Negovat volbu inspekÄnà chyby:" #: ../hawat/blueprints/events/forms.py:301 #: ../hawat/blueprints/filters/forms.py:274 #: ../hawat/blueprints/networks/forms.py:138 -#: ../hawat/blueprints/pdnsr/forms.py:39 ../vial/blueprints/groups/forms.py:232 +#: ../hawat/blueprints/pdnsr/forms.py:39 ../hawat/blueprints/groups/forms.py:232 msgid "Sort by:" msgstr "Řadit dle:" @@ -1773,16 +1773,16 @@ msgstr "dle Äasu uloženà vzestupnÄ›" #: ../hawat/blueprints/events/forms.py:350 #: ../hawat/blueprints/reports/forms.py:105 #: ../hawat/blueprints/reports/forms.py:144 -#: ../vial/blueprints/changelogs/forms.py:86 -#: ../vial/blueprints/changelogs/forms.py:124 +#: ../hawat/blueprints/changelogs/forms.py:86 +#: ../hawat/blueprints/changelogs/forms.py:124 msgid "From:" msgstr "Od:" #: ../hawat/blueprints/events/forms.py:358 #: ../hawat/blueprints/reports/forms.py:112 #: ../hawat/blueprints/reports/forms.py:151 -#: ../vial/blueprints/changelogs/forms.py:93 -#: ../vial/blueprints/changelogs/forms.py:131 +#: ../hawat/blueprints/changelogs/forms.py:93 +#: ../hawat/blueprints/changelogs/forms.py:131 msgid "To:" msgstr "Do:" @@ -2399,8 +2399,8 @@ msgstr "Chyba parsovánà JSONu: \"%(error)s\"." #: ../hawat/blueprints/groups/forms.py:189 #: ../hawat/blueprints/groups/forms.py:209 #: ../hawat/blueprints/groups/templates/groups/creatupdate.html:11 -#: ../vial/blueprints/groups/forms.py:131 -#: ../vial/blueprints/groups/forms.py:151 +#: ../hawat/blueprints/groups/forms.py:131 +#: ../hawat/blueprints/groups/forms.py:151 msgid "Name:" msgstr "Název:" @@ -2426,7 +2426,7 @@ msgstr "Filtr:" #: ../hawat/blueprints/filters/forms.py:121 #: ../hawat/blueprints/filters/forms.py:129 #: ../hawat/blueprints/users/forms.py:97 ../hawat/blueprints/users/forms.py:106 -#: ../vial/blueprints/users/forms.py:48 ../vial/blueprints/users/forms.py:57 +#: ../hawat/blueprints/users/forms.py:48 ../hawat/blueprints/users/forms.py:57 msgid "<< no preference >>" msgstr "<< bez preference >>" @@ -2445,9 +2445,9 @@ msgstr "Platné do:" #: ../hawat/blueprints/filters/forms.py:153 #: ../hawat/blueprints/filters/forms.py:254 #: ../hawat/blueprints/groups/forms.py:152 -#: ../hawat/blueprints/users/forms.py:139 ../vial/blueprints/groups/forms.py:94 -#: ../vial/blueprints/groups/forms.py:196 ../vial/blueprints/users/forms.py:90 -#: ../vial/blueprints/users/forms.py:207 +#: ../hawat/blueprints/users/forms.py:139 ../hawat/blueprints/groups/forms.py:94 +#: ../hawat/blueprints/groups/forms.py:196 ../hawat/blueprints/users/forms.py:90 +#: ../hawat/blueprints/users/forms.py:207 msgid "State:" msgstr "Stav:" @@ -2457,9 +2457,9 @@ msgstr "Stav:" #: ../hawat/blueprints/groups/forms.py:157 #: ../hawat/blueprints/settings_reporting/forms.py:121 #: ../hawat/blueprints/settings_reporting/forms.py:134 -#: ../hawat/blueprints/users/forms.py:144 ../vial/blueprints/groups/forms.py:61 -#: ../vial/blueprints/groups/forms.py:99 ../vial/blueprints/groups/forms.py:202 -#: ../vial/blueprints/users/forms.py:95 ../vial/blueprints/users/forms.py:213 +#: ../hawat/blueprints/users/forms.py:144 ../hawat/blueprints/groups/forms.py:61 +#: ../hawat/blueprints/groups/forms.py:99 ../hawat/blueprints/groups/forms.py:202 +#: ../hawat/blueprints/users/forms.py:95 ../hawat/blueprints/users/forms.py:213 msgid "Enabled" msgstr "AktivnÃ" @@ -2469,10 +2469,10 @@ msgstr "AktivnÃ" #: ../hawat/blueprints/groups/forms.py:158 #: ../hawat/blueprints/settings_reporting/forms.py:122 #: ../hawat/blueprints/settings_reporting/forms.py:135 -#: ../hawat/blueprints/users/forms.py:145 ../vial/blueprints/groups/forms.py:62 -#: ../vial/blueprints/groups/forms.py:100 -#: ../vial/blueprints/groups/forms.py:203 ../vial/blueprints/users/forms.py:96 -#: ../vial/blueprints/users/forms.py:214 +#: ../hawat/blueprints/users/forms.py:145 ../hawat/blueprints/groups/forms.py:62 +#: ../hawat/blueprints/groups/forms.py:100 +#: ../hawat/blueprints/groups/forms.py:203 ../hawat/blueprints/users/forms.py:96 +#: ../hawat/blueprints/users/forms.py:214 msgid "Disabled" msgstr "NeaktivnÃ" @@ -2480,8 +2480,8 @@ msgstr "NeaktivnÃ" #: ../hawat/blueprints/groups/forms.py:134 #: ../hawat/blueprints/networks/forms.py:79 #: ../hawat/blueprints/settings_reporting/forms.py:157 -#: ../hawat/blueprints/users/forms.py:111 ../vial/blueprints/groups/forms.py:76 -#: ../vial/blueprints/users/forms.py:62 +#: ../hawat/blueprints/users/forms.py:111 ../hawat/blueprints/groups/forms.py:76 +#: ../hawat/blueprints/users/forms.py:62 msgid "Submit" msgstr "Odeslat" @@ -2494,10 +2494,10 @@ msgstr "Náhled" #: ../hawat/blueprints/networks/forms.py:82 #: ../hawat/blueprints/settings_reporting/forms.py:160 #: ../hawat/blueprints/users/forms.py:114 -#: ../vial/blueprints/auth_dev/forms.py:42 -#: ../vial/blueprints/auth_pwd/forms.py:53 -#: ../vial/blueprints/groups/forms.py:79 ../vial/blueprints/users/forms.py:65 -#: ../vial/forms.py:602 +#: ../hawat/blueprints/auth_dev/forms.py:42 +#: ../hawat/blueprints/auth_pwd/forms.py:53 +#: ../hawat/blueprints/groups/forms.py:79 ../hawat/blueprints/users/forms.py:65 +#: ../hawat/forms.py:602 msgid "Cancel" msgstr "ZruÅ¡it" @@ -2539,13 +2539,13 @@ msgstr "" #: ../hawat/blueprints/filters/forms.py:226 #: ../hawat/blueprints/networks/forms.py:110 -#: ../vial/blueprints/groups/forms.py:181 ../vial/blueprints/users/forms.py:192 +#: ../hawat/blueprints/groups/forms.py:181 ../hawat/blueprints/users/forms.py:192 msgid "Creation time from:" msgstr "" #: ../hawat/blueprints/filters/forms.py:230 #: ../hawat/blueprints/networks/forms.py:114 -#: ../vial/blueprints/groups/forms.py:185 ../vial/blueprints/users/forms.py:196 +#: ../hawat/blueprints/groups/forms.py:185 ../hawat/blueprints/users/forms.py:196 msgid "" "Lower time boundary for item creation time. Timestamp is expected to be " "in the format <code>YYYY-MM-DD hh:mm:ss</code> and in the timezone " @@ -2554,13 +2554,13 @@ msgstr "" #: ../hawat/blueprints/filters/forms.py:233 #: ../hawat/blueprints/networks/forms.py:117 -#: ../vial/blueprints/groups/forms.py:188 ../vial/blueprints/users/forms.py:199 +#: ../hawat/blueprints/groups/forms.py:188 ../hawat/blueprints/users/forms.py:199 msgid "Creation time to:" msgstr "" #: ../hawat/blueprints/filters/forms.py:237 #: ../hawat/blueprints/networks/forms.py:121 -#: ../vial/blueprints/groups/forms.py:192 ../vial/blueprints/users/forms.py:203 +#: ../hawat/blueprints/groups/forms.py:192 ../hawat/blueprints/users/forms.py:203 msgid "" "Upper time boundary for item creation time. Timestamp is expected to be " "in the format <code>YYYY-MM-DD hh:mm:ss</code> and in the timezone " @@ -2570,10 +2570,10 @@ msgstr "" #: ../hawat/blueprints/filters/forms.py:246 #: ../hawat/blueprints/filters/forms.py:259 #: ../hawat/blueprints/networks/forms.py:167 -#: ../vial/blueprints/changelogs/forms.py:75 -#: ../vial/blueprints/groups/forms.py:201 -#: ../vial/blueprints/groups/forms.py:259 ../vial/blueprints/users/forms.py:212 -#: ../vial/blueprints/users/forms.py:274 +#: ../hawat/blueprints/changelogs/forms.py:75 +#: ../hawat/blueprints/groups/forms.py:201 +#: ../hawat/blueprints/groups/forms.py:259 ../hawat/blueprints/users/forms.py:212 +#: ../hawat/blueprints/users/forms.py:274 msgid "Nothing selected" msgstr "Nic nevybráno" @@ -2591,13 +2591,13 @@ msgstr "" #: ../hawat/blueprints/filters/forms.py:279 #: ../hawat/blueprints/networks/forms.py:143 -#: ../vial/blueprints/groups/forms.py:237 ../vial/blueprints/users/forms.py:246 +#: ../hawat/blueprints/groups/forms.py:237 ../hawat/blueprints/users/forms.py:246 msgid "by creation time descending" msgstr "" #: ../hawat/blueprints/filters/forms.py:280 #: ../hawat/blueprints/networks/forms.py:144 -#: ../vial/blueprints/groups/forms.py:238 ../vial/blueprints/users/forms.py:247 +#: ../hawat/blueprints/groups/forms.py:238 ../hawat/blueprints/users/forms.py:247 msgid "by creation time ascending" msgstr "" @@ -2746,7 +2746,7 @@ msgstr "Stromová struktura filtru" #: ../hawat/blueprints/networks/templates/networks/show.html:33 #: ../hawat/blueprints/settings_reporting/templates/settings_reporting/show.html:33 #: ../hawat/blueprints/users/templates/users/show.html:42 -#: ../vial/blueprints/changelogs/__init__.py:39 +#: ../hawat/blueprints/changelogs/__init__.py:39 msgid "Changelogs" msgstr "Záznamy zmÄ›n" @@ -2925,7 +2925,7 @@ msgid "City resolving" msgstr "Hledánà v databázi mÄ›st" #: ../hawat/blueprints/groups/__init__.py:234 -#: ../vial/blueprints/groups/__init__.py:891 +#: ../hawat/blueprints/groups/__init__.py:891 msgid "My groups" msgstr "Moje skupiny" @@ -2939,17 +2939,17 @@ msgstr "Zobrazit detaily abuse skupiny <strong>%(name)s</strong>" msgid "Group with this name already exists." msgstr "Skupina s tÃmto jménem již existuje." -#: ../hawat/blueprints/groups/forms.py:70 ../vial/blueprints/groups/forms.py:26 +#: ../hawat/blueprints/groups/forms.py:70 ../hawat/blueprints/groups/forms.py:26 msgid "You must not select a group as its own parent! Naughty, naughty you!" msgstr "Nelze zvolit stejnou skupinu jako rodiÄe sama sebe! Zvrhlé, velmi zvrhlé!" #: ../hawat/blueprints/groups/forms.py:103 -#: ../vial/blueprints/groups/forms.py:45 +#: ../hawat/blueprints/groups/forms.py:45 msgid "Additional and more extensive group description." msgstr "DalÅ¡Ã a vÃce obsáhlý popis skupiny." #: ../hawat/blueprints/groups/forms.py:111 -#: ../vial/blueprints/groups/forms.py:53 +#: ../hawat/blueprints/groups/forms.py:53 msgid "" "Origin of the group record, whether it was added manually, or via some " "automated mechanism from data from some third party system." @@ -2958,30 +2958,30 @@ msgstr "" "automatizovaného nástroje z dat služby tÅ™età strany." #: ../hawat/blueprints/groups/forms.py:114 -#: ../vial/blueprints/groups/forms.py:56 +#: ../hawat/blueprints/groups/forms.py:56 msgid "Self management:" msgstr "Samospráva:" #: ../hawat/blueprints/groups/forms.py:127 -#: ../vial/blueprints/groups/forms.py:69 +#: ../hawat/blueprints/groups/forms.py:69 msgid "Members:" msgstr "ÄŒlenové:" #: ../hawat/blueprints/groups/forms.py:130 #: ../hawat/blueprints/groups/forms.py:168 #: ../hawat/blueprints/groups/forms.py:179 -#: ../vial/blueprints/groups/forms.py:72 ../vial/blueprints/groups/forms.py:110 -#: ../vial/blueprints/groups/forms.py:121 +#: ../hawat/blueprints/groups/forms.py:72 ../hawat/blueprints/groups/forms.py:110 +#: ../hawat/blueprints/groups/forms.py:121 msgid "<< no selection >>" msgstr "<< bez volby >>" #: ../hawat/blueprints/groups/forms.py:131 -#: ../vial/blueprints/groups/forms.py:73 +#: ../hawat/blueprints/groups/forms.py:73 msgid "List of group members." msgstr "Seznam Älenů skupiny." #: ../hawat/blueprints/groups/forms.py:162 -#: ../vial/blueprints/groups/forms.py:104 +#: ../hawat/blueprints/groups/forms.py:104 msgid "" "Boolean flag whether the group is enabled or disabled. Disabled groups " "are hidden to the most of the system features." @@ -2990,12 +2990,12 @@ msgstr "" "skryty pro velkou Äást funkcà systému." #: ../hawat/blueprints/groups/forms.py:165 -#: ../vial/blueprints/groups/forms.py:107 +#: ../hawat/blueprints/groups/forms.py:107 msgid "Managers:" msgstr "Správci:" #: ../hawat/blueprints/groups/forms.py:169 -#: ../vial/blueprints/groups/forms.py:111 +#: ../hawat/blueprints/groups/forms.py:111 msgid "" "List of users acting as group managers. These users may change various " "group settings." @@ -3004,12 +3004,12 @@ msgstr "" "Äetná nastavenà skupiny." #: ../hawat/blueprints/groups/forms.py:172 -#: ../vial/blueprints/groups/forms.py:114 +#: ../hawat/blueprints/groups/forms.py:114 msgid "Parent group:" msgstr "NadÅ™azená skupina:" #: ../hawat/blueprints/groups/forms.py:180 -#: ../vial/blueprints/groups/forms.py:122 +#: ../hawat/blueprints/groups/forms.py:122 msgid "" "Parent group for this group. This feature enables the posibility to " "create structured group hierarchy." @@ -3019,8 +3019,8 @@ msgstr "" #: ../hawat/blueprints/groups/forms.py:195 #: ../hawat/blueprints/groups/forms.py:215 -#: ../vial/blueprints/groups/forms.py:137 -#: ../vial/blueprints/groups/forms.py:157 +#: ../hawat/blueprints/groups/forms.py:137 +#: ../hawat/blueprints/groups/forms.py:157 msgid "System-wide unique name for the group." msgstr "Jméno skupiny unikátnà v rámci celého systému." @@ -3148,12 +3148,12 @@ msgid "This group does not have any changelog records at the moment." msgstr "Tato skupina nemá v tuto chvÃli žádné záznamy zmÄ›n." #: ../hawat/blueprints/home/__init__.py:57 -#: ../vial/blueprints/home/__init__.py:49 +#: ../hawat/blueprints/home/__init__.py:49 msgid "Welcome!" msgstr "VÃtejte!" #: ../hawat/blueprints/home/__init__.py:72 -#: ../vial/blueprints/home/__init__.py:64 +#: ../hawat/blueprints/home/__init__.py:64 msgid "Home page" msgstr "" @@ -3489,7 +3489,7 @@ msgid "" msgstr "" #: ../hawat/blueprints/networks/forms.py:130 -#: ../vial/blueprints/groups/forms.py:211 +#: ../hawat/blueprints/groups/forms.py:211 msgid "Record source:" msgstr "" @@ -3579,7 +3579,7 @@ msgstr "podle Äasu poslednÃho záznamu sestupnÄ›" msgid "by last seen time ascending" msgstr "podle Äasu poslednÃho záznamu vzestupnÄ›" -#: ../hawat/blueprints/pdnsr/forms.py:57 ../vial/forms.py:613 +#: ../hawat/blueprints/pdnsr/forms.py:57 ../hawat/forms.py:613 msgid "Pager limit:" msgstr "Limit stránkovaÄe:" @@ -4446,8 +4446,8 @@ msgid "Timezone:" msgstr "ÄŒasová zóna:" #: ../hawat/blueprints/settings_reporting/templates/settings_reporting/creatupdate.html:9 -#: ../vial/blueprints/groups/__init__.py:57 -#: ../vial/blueprints/groups/__init__.py:871 +#: ../hawat/blueprints/groups/__init__.py:57 +#: ../hawat/blueprints/groups/__init__.py:871 msgid "Group management" msgstr "Správa skupin" @@ -4728,23 +4728,23 @@ msgstr "Uživatelský úÄet s tÃmto pÅ™ihlaÅ¡ovacÃm jménem již existuje." msgid "Home organization:" msgstr "Domovská organizace:" -#: ../hawat/blueprints/users/forms.py:93 ../vial/blueprints/users/forms.py:44 +#: ../hawat/blueprints/users/forms.py:93 ../hawat/blueprints/users/forms.py:44 msgid "Prefered locale:" msgstr "Preferovaná lokalizace:" -#: ../hawat/blueprints/users/forms.py:102 ../vial/blueprints/users/forms.py:53 +#: ../hawat/blueprints/users/forms.py:102 ../hawat/blueprints/users/forms.py:53 msgid "Prefered timezone:" msgstr "Preferovaná Äasová zóna:" -#: ../hawat/blueprints/users/forms.py:151 ../vial/blueprints/users/forms.py:102 +#: ../hawat/blueprints/users/forms.py:151 ../hawat/blueprints/users/forms.py:102 msgid "Roles:" msgstr "Role:" -#: ../hawat/blueprints/users/forms.py:157 ../vial/blueprints/users/forms.py:108 +#: ../hawat/blueprints/users/forms.py:157 ../hawat/blueprints/users/forms.py:108 msgid "Group memberships:" msgstr "ÄŒlenstvà ve skupinách:" -#: ../hawat/blueprints/users/forms.py:161 ../vial/blueprints/users/forms.py:112 +#: ../hawat/blueprints/users/forms.py:161 ../hawat/blueprints/users/forms.py:112 msgid "Group managements:" msgstr "Správa skupin:" @@ -4759,10 +4759,10 @@ msgstr "" #: ../hawat/blueprints/users/templates/users/list.html:39 #: ../hawat/blueprints/users/templates/users/show.html:61 -#: ../vial/blueprints/auth/__init__.py:48 -#: ../vial/blueprints/auth_dev/forms.py:39 -#: ../vial/blueprints/auth_pwd/forms.py:50 ../vial/view/__init__.py:557 -#: ../vial/view/__init__.py:561 +#: ../hawat/blueprints/auth/__init__.py:48 +#: ../hawat/blueprints/auth_dev/forms.py:39 +#: ../hawat/blueprints/auth_pwd/forms.py:50 ../hawat/view/__init__.py:557 +#: ../hawat/view/__init__.py:561 msgid "Login" msgstr "PÅ™ihlášenÃ" @@ -4999,125 +4999,125 @@ msgstr "ÄŒas v zónÄ›:" msgid "Period:" msgstr "Perioda:" -#: ../vial/app.py:693 +#: ../hawat/app.py:693 msgid "Please log in to access this page." msgstr "Pro pÅ™Ãstup k této stránce se prosÃm pÅ™ihlaste." -#: ../vial/app.py:719 +#: ../hawat/app.py:719 msgid "You have been successfully logged out." msgstr "Odhlášenà probÄ›hlo úspěšnÄ›." -#: ../vial/app.py:876 +#: ../hawat/app.py:876 #, python-format msgid "Locale was succesfully changed to <strong>%(lcln)s (%(lclc)s)</strong>." msgstr "Lokalizace byla úspěšnÄ› zmÄ›nÄ›na na <strong>%(lcln)s (%(lclc)s)</strong>." -#: ../vial/errors.py:36 +#: ../hawat/errors.py:36 msgid "Unknown error" msgstr "Neznámá chyba" -#: ../vial/forms.py:139 +#: ../hawat/forms.py:139 #, python-format msgid "The \"%(val)s\" value does not look like valid login name." msgstr "Hodnota \"%(val)s\" nevypadá jako validnà uživatelské jméno." -#: ../vial/forms.py:152 +#: ../hawat/forms.py:152 #, python-format msgid "The \"%(val)s\" value does not look like valid email address." msgstr "Hodnota \"%(val)s\" nevypadá jako validnà emailová adresa." -#: ../vial/forms.py:166 +#: ../hawat/forms.py:166 #, python-format msgid "Please use different login, the \"%(val)s\" is already taken." msgstr "ProsÃm použijte jiný login, \"%(val)s\" již existuje." -#: ../vial/forms.py:180 +#: ../hawat/forms.py:180 #, python-format msgid "Please use different group name, the \"%(val)s\" is already taken." msgstr "" -#: ../vial/forms.py:194 +#: ../hawat/forms.py:194 #, python-format msgid "The \"%(val)s\" value does not look like valid email adress." msgstr "Hodnota \"%(val)s\" nevypadá jako validnà emailová adresa." -#: ../vial/forms.py:213 +#: ../hawat/forms.py:213 #, python-format msgid "The \"%(val)s\" value does not look like valid IPv4/IPv6 address." msgstr "Hodnota \"%(val)s\" nevypadá jako validnà IPv4/IPv6 adresa." -#: ../vial/forms.py:232 +#: ../hawat/forms.py:232 #, python-format msgid "The \"%(val)s\" value does not look like valid IPv4 address." msgstr "Hodnota \"%(val)s\" nevypadá jako validnà IPv4 adresa." -#: ../vial/forms.py:251 +#: ../hawat/forms.py:251 #, python-format msgid "The \"%(val)s\" value does not look like valid IPv6 address." msgstr "Hodnota \"%(val)s\" nevypadá jako validnà IPv6 adresa." -#: ../vial/forms.py:269 ../vial/forms.py:284 +#: ../hawat/forms.py:269 ../hawat/forms.py:284 #, python-format msgid "" "The \"%(val)s\" value does not look like valid IPv4/IPv6 " "address/range/network." msgstr "Hodnota \"%(val)s\" nevypadá jako validnà IPv4/IPv6 adresa/rozsah/sÃÅ¥." -#: ../vial/forms.py:298 ../vial/forms.py:305 +#: ../hawat/forms.py:298 ../hawat/forms.py:305 #, python-format msgid "The \"%(val)s\" value does not look like valid port number." msgstr "Hodnota \"%(val)s\" nevypadá jako validnà ÄÃslo portu." -#: ../vial/forms.py:319 ../vial/forms.py:326 +#: ../hawat/forms.py:319 ../hawat/forms.py:326 #, python-format msgid "The \"%(val)s\" value does not look like valid positive integer." msgstr "Hodnota \"%(val)s\" nevypadá jako validnà kladné celé ÄÃslo." -#: ../vial/forms.py:430 +#: ../hawat/forms.py:430 msgid "Not a valid datetime value" msgstr "Neplatná hodnota Äasové znaÄky" -#: ../vial/forms.py:519 +#: ../hawat/forms.py:519 msgid "Value did not match any of datetime formats." msgstr "Hodnota neodpovÃdá žádnému formátu pro Äasové známky." -#: ../vial/forms.py:532 ../vial/forms.py:551 +#: ../hawat/forms.py:532 ../hawat/forms.py:551 msgid "Invalid Choice: could not coerce" msgstr "Neplatná volba: nelze transformovat" -#: ../vial/forms.py:539 ../vial/forms.py:560 +#: ../hawat/forms.py:539 ../hawat/forms.py:560 msgid "Not a valid choice" msgstr "Neplatná volba" -#: ../vial/forms.py:599 +#: ../hawat/forms.py:599 msgid "Confirm" msgstr "Potvrdit" -#: ../vial/forms.py:622 +#: ../hawat/forms.py:622 msgid "Page number:" msgstr "ÄŒÃslo stránky:" -#: ../vial/blueprints/auth/__init__.py:44 +#: ../hawat/blueprints/auth/__init__.py:44 msgid "User login" msgstr "PÅ™ihlášenà uživatele" -#: ../vial/blueprints/auth/__init__.py:85 ../vial/view/__init__.py:1913 +#: ../hawat/blueprints/auth/__init__.py:85 ../hawat/view/__init__.py:1913 msgid "User account registration" msgstr "Registrace uživatelského úÄtu." -#: ../vial/blueprints/auth/__init__.py:89 ../vial/view/__init__.py:1909 +#: ../hawat/blueprints/auth/__init__.py:89 ../hawat/view/__init__.py:1909 msgid "Register" msgstr "Registrace" -#: ../vial/blueprints/auth/__init__.py:108 +#: ../hawat/blueprints/auth/__init__.py:108 msgid "Authentication directional service" msgstr "AutentizaÄnà smÄ›rovacà služba" -#: ../vial/blueprints/auth_api/__init__.py:87 +#: ../hawat/blueprints/auth_api/__init__.py:87 msgid "Generate API key" msgstr "Generovat API klÃÄ" -#: ../vial/blueprints/auth_api/__init__.py:116 +#: ../hawat/blueprints/auth_api/__init__.py:116 #, python-format msgid "" "API key for user account <strong>%(item_id)s</strong> was successfully " @@ -5126,243 +5126,243 @@ msgstr "" "API klÃÄ pro uživatele <strong>%(item_id)s</strong> byl úspěšnÄ› " "vygenerován." -#: ../vial/blueprints/auth_api/__init__.py:123 +#: ../hawat/blueprints/auth_api/__init__.py:123 #, python-format msgid "Unable to generate API key for user account <strong>%(item_id)s</strong>." msgstr "Nelze vygenerovat API klÃÄ pro uživatele <strong>%(item_id)s</strong>." -#: ../vial/blueprints/auth_api/__init__.py:130 +#: ../hawat/blueprints/auth_api/__init__.py:130 #, python-format msgid "Canceled generating API key for user account <strong>%(item_id)s</strong>." msgstr "" "Generovánà API klÃÄe pro uživatele <strong>%(item_id)s</strong> bylo " "zruÅ¡eno." -#: ../vial/blueprints/auth_api/__init__.py:160 +#: ../hawat/blueprints/auth_api/__init__.py:160 msgid "Delete API key" msgstr "Smazat API klÃÄ" -#: ../vial/blueprints/auth_api/__init__.py:189 +#: ../hawat/blueprints/auth_api/__init__.py:189 #, python-format msgid "" "API key for user account <strong>%(item_id)s</strong> was successfully " "deleted." msgstr "API klÃÄ pro uživatele <strong>%(item_id)s</strong> byl úspěšnÄ› smazán." -#: ../vial/blueprints/auth_api/__init__.py:196 +#: ../hawat/blueprints/auth_api/__init__.py:196 #, python-format msgid "Unable to delete API key for user account <strong>%(item_id)s</strong>." msgstr "Nelze smazat API klÃÄ pro uživatele <strong>%(item_id)s</strong>." -#: ../vial/blueprints/auth_api/__init__.py:203 +#: ../hawat/blueprints/auth_api/__init__.py:203 #, python-format msgid "Canceled deleting API key for user account <strong>%(item_id)s</strong>." msgstr "Smazánà API klÃÄe pro uživatele <strong>%(item_id)s</strong> bylo zruÅ¡eno." -#: ../vial/blueprints/auth_api/__init__.py:219 +#: ../hawat/blueprints/auth_api/__init__.py:219 msgid "API key authentication service" msgstr "AutentizaÄnà služba API klÃÄů" -#: ../vial/blueprints/auth_dev/__init__.py:63 +#: ../hawat/blueprints/auth_dev/__init__.py:63 msgid "Developer login" msgstr "PÅ™ihlášenà vývojáře" -#: ../vial/blueprints/auth_dev/__init__.py:67 +#: ../hawat/blueprints/auth_dev/__init__.py:67 msgid "Login (dev)" msgstr "Login (dev)" -#: ../vial/blueprints/auth_dev/__init__.py:95 +#: ../hawat/blueprints/auth_dev/__init__.py:95 msgid "Register (dev)" msgstr "Registrace (dev)" -#: ../vial/blueprints/auth_dev/__init__.py:99 +#: ../hawat/blueprints/auth_dev/__init__.py:99 msgid "User account registration (dev)" msgstr "Registrace uživatelského úÄtu (dev)" -#: ../vial/blueprints/auth_dev/__init__.py:135 +#: ../hawat/blueprints/auth_dev/__init__.py:135 msgid "Developer authentication service" msgstr "AutentizaÄnà služba pro vývojáře" -#: ../vial/blueprints/auth_dev/forms.py:32 +#: ../hawat/blueprints/auth_dev/forms.py:32 msgid "User account:" msgstr "Uživatelský úÄet:" -#: ../vial/blueprints/auth_env/__init__.py:116 +#: ../hawat/blueprints/auth_env/__init__.py:116 msgid "Environment login" msgstr "PÅ™ihlášenÃ" -#: ../vial/blueprints/auth_env/__init__.py:120 +#: ../hawat/blueprints/auth_env/__init__.py:120 msgid "Login (env)" msgstr "PÅ™ihlášenÃ" -#: ../vial/blueprints/auth_env/__init__.py:134 +#: ../hawat/blueprints/auth_env/__init__.py:134 msgid "User login was not received, unable to perform login process." msgstr "Uživatelský login nebyl obdržen, nelze dokonÄit pÅ™ihlaÅ¡ovacà proces." -#: ../vial/blueprints/auth_env/__init__.py:149 +#: ../hawat/blueprints/auth_env/__init__.py:149 msgid "Register (env)" msgstr "Registrace (env)" -#: ../vial/blueprints/auth_env/__init__.py:153 +#: ../hawat/blueprints/auth_env/__init__.py:153 msgid "User account registration (env)" msgstr "Registrace uživatelského úÄtu (env)" -#: ../vial/blueprints/auth_env/__init__.py:262 +#: ../hawat/blueprints/auth_env/__init__.py:262 msgid "Environment authentication service" msgstr "AutentizaÄnà služba prostÅ™edÃ" -#: ../vial/blueprints/auth_pwd/__init__.py:48 +#: ../hawat/blueprints/auth_pwd/__init__.py:48 msgid "Password login" msgstr "PÅ™ihlášenà heslem" -#: ../vial/blueprints/auth_pwd/__init__.py:52 +#: ../hawat/blueprints/auth_pwd/__init__.py:52 msgid "Login (pwd)" msgstr "Login (heslo)" -#: ../vial/blueprints/auth_pwd/__init__.py:85 +#: ../hawat/blueprints/auth_pwd/__init__.py:85 msgid "Register (pwd)" msgstr "Registrace (pwd)" -#: ../vial/blueprints/auth_pwd/__init__.py:89 +#: ../hawat/blueprints/auth_pwd/__init__.py:89 msgid "User account registration (pwd)" msgstr "Registrace uživatelského úÄtu (pwd)" -#: ../vial/blueprints/auth_pwd/__init__.py:125 +#: ../hawat/blueprints/auth_pwd/__init__.py:125 msgid "Password authentication service" msgstr "AutentizaÄnà služba heslem" -#: ../vial/blueprints/changelogs/__init__.py:43 +#: ../hawat/blueprints/changelogs/__init__.py:43 msgid "Search item changelogs" msgstr "Prohledat záznamy zmÄ›n položek" -#: ../vial/blueprints/changelogs/__init__.py:93 +#: ../hawat/blueprints/changelogs/__init__.py:93 msgid "Other changes by the same author" msgstr "DalÅ¡Ã zmÄ›ny se stejným autorem" -#: ../vial/blueprints/changelogs/__init__.py:102 +#: ../hawat/blueprints/changelogs/__init__.py:102 msgid "Other changes of the same item" msgstr "DalÅ¡Ã zmÄ›ny stejné položky" -#: ../vial/blueprints/changelogs/__init__.py:111 +#: ../hawat/blueprints/changelogs/__init__.py:111 msgid "Other changes of the same item by the same author" msgstr "DalÅ¡Ã zmÄ›ny stejné položky se stejným autorem" -#: ../vial/blueprints/changelogs/__init__.py:131 +#: ../hawat/blueprints/changelogs/__init__.py:131 msgid "Show item changelog record" msgstr "Zobrazit záznam zmÄ›n položky" -#: ../vial/blueprints/changelogs/__init__.py:137 +#: ../hawat/blueprints/changelogs/__init__.py:137 #, python-format msgid "View details of item changelog record "%(item)s"" msgstr "Zobrazit detaily záznamu zmÄ›n položky "%(item)s"" -#: ../vial/blueprints/changelogs/__init__.py:142 +#: ../hawat/blueprints/changelogs/__init__.py:142 msgid "Show item changelog record details" msgstr "Zobrazit detaily záznamu zmÄ›n položky" -#: ../vial/blueprints/changelogs/__init__.py:179 +#: ../hawat/blueprints/changelogs/__init__.py:179 msgid "Item changelog record management" msgstr "" -#: ../vial/blueprints/changelogs/forms.py:59 +#: ../hawat/blueprints/changelogs/forms.py:59 msgid "Authors:" msgstr "AutoÅ™i:" -#: ../vial/blueprints/changelogs/forms.py:64 +#: ../hawat/blueprints/changelogs/forms.py:64 msgid "Operations:" msgstr "Operace:" -#: ../vial/blueprints/changelogs/forms.py:71 +#: ../hawat/blueprints/changelogs/forms.py:71 msgid "Item model:" msgstr "Model položky:" -#: ../vial/blueprints/changelogs/forms.py:80 +#: ../hawat/blueprints/changelogs/forms.py:80 msgid "Model ID:" msgstr "Identifikátor modelu:" -#: ../vial/blueprints/changelogs/forms.py:137 +#: ../hawat/blueprints/changelogs/forms.py:137 msgid "Calculate" msgstr "SpoÄÃtat" -#: ../vial/blueprints/design_bs3/__init__.py:54 +#: ../hawat/blueprints/design_bs3/__init__.py:54 msgid "Application design and style template" msgstr "" -#: ../vial/blueprints/devtools/__init__.py:45 +#: ../hawat/blueprints/devtools/__init__.py:45 msgid "System configuration" msgstr "Systémová konfigurace" -#: ../vial/blueprints/devtools/__init__.py:49 +#: ../hawat/blueprints/devtools/__init__.py:49 msgid "View system configuration" msgstr "Zobrazenà systémové konfigurace" -#: ../vial/blueprints/devtools/__init__.py:60 +#: ../hawat/blueprints/devtools/__init__.py:60 msgid "Development tools" msgstr "" -#: ../vial/blueprints/groups/__init__.py:194 +#: ../hawat/blueprints/groups/__init__.py:194 msgid "Show group details" msgstr "Zobrazit detaily skupiny" -#: ../vial/blueprints/groups/__init__.py:352 +#: ../hawat/blueprints/groups/__init__.py:352 msgid "Create group" msgstr "VytvoÅ™it skupinu" -#: ../vial/blueprints/groups/__init__.py:356 +#: ../hawat/blueprints/groups/__init__.py:356 msgid "Create new group" msgstr "VytvoÅ™it novou skupinu" -#: ../vial/blueprints/groups/__init__.py:370 +#: ../hawat/blueprints/groups/__init__.py:370 #, python-format msgid "Group <strong>%(item_id)s</strong> was successfully created." msgstr "Skupina <strong>%(item_id)s</strong> byla úspěšnÄ› vytvoÅ™ena." -#: ../vial/blueprints/groups/__init__.py:375 +#: ../hawat/blueprints/groups/__init__.py:375 msgid "Unable to create new group." msgstr "Nelze vytvoÅ™it skupinu." -#: ../vial/blueprints/groups/__init__.py:379 +#: ../hawat/blueprints/groups/__init__.py:379 msgid "Canceled creating new group." msgstr "VytvoÅ™enà nové skupiny bylo zruÅ¡eno." -#: ../vial/blueprints/groups/__init__.py:397 ../vial/view/__init__.py:2068 -#: ../vial/view/__init__.py:2073 ../vial/view/__init__.py:2184 +#: ../hawat/blueprints/groups/__init__.py:397 ../hawat/view/__init__.py:2068 +#: ../hawat/view/__init__.py:2073 ../hawat/view/__init__.py:2184 msgid "Update" msgstr "Upravit" -#: ../vial/blueprints/groups/__init__.py:408 +#: ../hawat/blueprints/groups/__init__.py:408 msgid "Update group details" msgstr "Aktualizovat detaily skupiny" -#: ../vial/blueprints/groups/__init__.py:429 +#: ../hawat/blueprints/groups/__init__.py:429 #, python-format msgid "Group <strong>%(item_id)s</strong> was successfully updated." msgstr "Skupina <strong>%(item_id)s</strong> byla úspěšnÄ› aktualizována." -#: ../vial/blueprints/groups/__init__.py:436 +#: ../hawat/blueprints/groups/__init__.py:436 #, python-format msgid "Unable to update group <strong>%(item_id)s</strong>." msgstr "Nelze aktualizovat skupinu <strong>%(item_id)s</strong>." -#: ../vial/blueprints/groups/__init__.py:443 +#: ../hawat/blueprints/groups/__init__.py:443 #, python-format msgid "Canceled updating group <strong>%(item_id)s</strong>." msgstr "Aktualizace skupiny <strong>%(item_id)s</strong> byla zruÅ¡ena." -#: ../vial/blueprints/groups/__init__.py:470 +#: ../hawat/blueprints/groups/__init__.py:470 msgid "Add group member" msgstr "PÅ™idat Älena skupiny" -#: ../vial/blueprints/groups/__init__.py:481 -#: ../vial/blueprints/users/__init__.py:569 +#: ../hawat/blueprints/groups/__init__.py:481 +#: ../hawat/blueprints/users/__init__.py:569 #, python-format msgid "Add user "%(user_id)s" to group "%(group_id)s"" msgstr "" "PÅ™idat uživatele "%(user_id)s" do skupiny " ""%(group_id)s"" -#: ../vial/blueprints/groups/__init__.py:529 -#: ../vial/blueprints/users/__init__.py:617 +#: ../hawat/blueprints/groups/__init__.py:529 +#: ../hawat/blueprints/users/__init__.py:617 #, python-format msgid "" "User <strong>%(user_id)s</strong> was successfully added as a member to " @@ -5371,8 +5371,8 @@ msgstr "" "Uživatel <strong>%(user_id)s</strong> byl úspěšnÄ› pÅ™idán jako Älen do " "skupiny <strong>%(group_id)s</strong>." -#: ../vial/blueprints/groups/__init__.py:537 -#: ../vial/blueprints/users/__init__.py:625 +#: ../hawat/blueprints/groups/__init__.py:537 +#: ../hawat/blueprints/users/__init__.py:625 #, python-format msgid "" "Unable to add user <strong>%(user_id)s</strong> as a member to group " @@ -5381,8 +5381,8 @@ msgstr "" "Nelze pÅ™idat uživatele <strong>%(user_id)s</strong> jako Älena do skupiny" " <strong>%(group_id)s</strong>." -#: ../vial/blueprints/groups/__init__.py:545 -#: ../vial/blueprints/users/__init__.py:633 +#: ../hawat/blueprints/groups/__init__.py:545 +#: ../hawat/blueprints/users/__init__.py:633 #, python-format msgid "" "Canceled adding user <strong>%(user_id)s</strong> as a member to group " @@ -5391,12 +5391,12 @@ msgstr "" "PÅ™idánà uživatele <strong>%(user_id)s</strong> jako Älena do skupiny " "<strong>%(group_id)s</strong> bylo zruÅ¡eno." -#: ../vial/blueprints/groups/__init__.py:563 +#: ../hawat/blueprints/groups/__init__.py:563 msgid "Reject group member" msgstr "OdmÃtnout Älena skupiny" -#: ../vial/blueprints/groups/__init__.py:574 -#: ../vial/blueprints/users/__init__.py:662 +#: ../hawat/blueprints/groups/__init__.py:574 +#: ../hawat/blueprints/users/__init__.py:662 #, python-format msgid "" "Reject user`s "%(user_id)s" membership request for group " @@ -5405,8 +5405,8 @@ msgstr "" "OdmÃtnout žádost uživatele "%(user_id)s" o Älenstvà ve skupinÄ› " ""%(group_id)s"" -#: ../vial/blueprints/groups/__init__.py:614 -#: ../vial/blueprints/users/__init__.py:700 +#: ../hawat/blueprints/groups/__init__.py:614 +#: ../hawat/blueprints/users/__init__.py:700 #, python-format msgid "" "User`s <strong>%(user_id)s</strong> membership request for group " @@ -5415,8 +5415,8 @@ msgstr "" "Žádost uživatele <strong>%(user_id)s</strong> o Älenstvà ve skupinÄ› " "<strong>%(group_id)s</strong> byla úspěšnÄ› zamÃtnuta." -#: ../vial/blueprints/groups/__init__.py:622 -#: ../vial/blueprints/users/__init__.py:708 +#: ../hawat/blueprints/groups/__init__.py:622 +#: ../hawat/blueprints/users/__init__.py:708 #, python-format msgid "" "Unable to reject user`s <strong>%(user_id)s</strong> membership request " @@ -5425,8 +5425,8 @@ msgstr "" "Nelze odmÃtnout žádost uživatele <strong>%(user_id)s</strong> o Älenstvà " "ve skupinÄ› <strong>%(group_id)s</strong>." -#: ../vial/blueprints/groups/__init__.py:630 -#: ../vial/blueprints/users/__init__.py:716 +#: ../hawat/blueprints/groups/__init__.py:630 +#: ../hawat/blueprints/users/__init__.py:716 #, python-format msgid "" "Canceled rejecting user`s <strong>%(user_id)s</strong> membership request" @@ -5435,20 +5435,20 @@ msgstr "" "OdmÃtnutà žádosti uživatele <strong>%(user_id)s</strong> o Älenstvà ve " "skupinÄ› <strong>%(group_id)s</strong> bylo zruÅ¡eno." -#: ../vial/blueprints/groups/__init__.py:648 +#: ../hawat/blueprints/groups/__init__.py:648 msgid "Remove group member" msgstr "Odebrat Älena skupiny" -#: ../vial/blueprints/groups/__init__.py:659 -#: ../vial/blueprints/users/__init__.py:745 +#: ../hawat/blueprints/groups/__init__.py:659 +#: ../hawat/blueprints/users/__init__.py:745 #, python-format msgid "Remove user "%(user_id)s" from group "%(group_id)s"" msgstr "" "Odebrat uživatele "%(user_id)s" ze skupiny group " ""%(group_id)s"" -#: ../vial/blueprints/groups/__init__.py:701 -#: ../vial/blueprints/users/__init__.py:786 +#: ../hawat/blueprints/groups/__init__.py:701 +#: ../hawat/blueprints/users/__init__.py:786 #, python-format msgid "" "User <strong>%(user_id)s</strong> was successfully removed as a member " @@ -5457,8 +5457,8 @@ msgstr "" "Uživatel <strong>%(user_id)s</strong> byl úspěšnÄ› odebrán jako Älen ze " "skupiny <strong>%(group_id)s</strong>." -#: ../vial/blueprints/groups/__init__.py:709 -#: ../vial/blueprints/users/__init__.py:794 +#: ../hawat/blueprints/groups/__init__.py:709 +#: ../hawat/blueprints/users/__init__.py:794 #, python-format msgid "" "Unable to remove user <strong>%(user_id)s</strong> as a member from group" @@ -5467,8 +5467,8 @@ msgstr "" "Nelze odebrat uživatele <strong>%(user_id)s</strong> jako Älena ze " "skupiny <strong>%(group_id)s</strong>." -#: ../vial/blueprints/groups/__init__.py:717 -#: ../vial/blueprints/users/__init__.py:802 +#: ../hawat/blueprints/groups/__init__.py:717 +#: ../hawat/blueprints/users/__init__.py:802 #, python-format msgid "" "Canceled removing user <strong>%(user_id)s</strong> as a member from " @@ -5477,380 +5477,380 @@ msgstr "" "Odebránà uživatele <strong>%(user_id)s</strong> jako Älena ze skupiny " "<strong>%(group_id)s</strong> bylo zruÅ¡eno." -#: ../vial/blueprints/groups/__init__.py:741 +#: ../hawat/blueprints/groups/__init__.py:741 msgid "Add group manager" msgstr "" -#: ../vial/blueprints/groups/__init__.py:752 -#: ../vial/blueprints/users/__init__.py:841 +#: ../hawat/blueprints/groups/__init__.py:752 +#: ../hawat/blueprints/users/__init__.py:841 #, python-format msgid "" "Add user "%(user_id)s" to group "%(group_id)s" as " "manager" msgstr "" -#: ../vial/blueprints/groups/__init__.py:796 -#: ../vial/blueprints/users/__init__.py:885 +#: ../hawat/blueprints/groups/__init__.py:796 +#: ../hawat/blueprints/users/__init__.py:885 #, python-format msgid "" "User <strong>%(user_id)s</strong> was successfully added as a manager to " "group <strong>%(group_id)s</strong>." msgstr "" -#: ../vial/blueprints/groups/__init__.py:804 -#: ../vial/blueprints/users/__init__.py:893 +#: ../hawat/blueprints/groups/__init__.py:804 +#: ../hawat/blueprints/users/__init__.py:893 #, python-format msgid "" "Unable to add user <strong>%(user_id)s</strong> as a manager to group " "<strong>%(group_id)s</strong>." msgstr "" -#: ../vial/blueprints/groups/__init__.py:812 -#: ../vial/blueprints/users/__init__.py:901 +#: ../hawat/blueprints/groups/__init__.py:812 +#: ../hawat/blueprints/users/__init__.py:901 #, python-format msgid "" "Canceled adding user <strong>%(user_id)s</strong> as a manager to group " "<strong>%(group_id)s</strong>." msgstr "" -#: ../vial/blueprints/groups/__init__.py:830 +#: ../hawat/blueprints/groups/__init__.py:830 msgid "Remove group manager" msgstr "" -#: ../vial/blueprints/groups/__init__.py:841 -#: ../vial/blueprints/users/__init__.py:930 +#: ../hawat/blueprints/groups/__init__.py:841 +#: ../hawat/blueprints/users/__init__.py:930 #, python-format msgid "" "Remove user "%(user_id)s" from group "%(group_id)s" " "as manager" msgstr "" -#: ../vial/blueprints/groups/__init__.py:882 -#: ../vial/blueprints/users/__init__.py:971 +#: ../hawat/blueprints/groups/__init__.py:882 +#: ../hawat/blueprints/users/__init__.py:971 #, python-format msgid "" "User <strong>%(user_id)s</strong> was successfully removed as a manager " "from group <strong>%(group_id)s</strong>." msgstr "" -#: ../vial/blueprints/groups/__init__.py:890 -#: ../vial/blueprints/users/__init__.py:979 +#: ../hawat/blueprints/groups/__init__.py:890 +#: ../hawat/blueprints/users/__init__.py:979 #, python-format msgid "" "Unable to remove user <strong>%(user_id)s</strong> as a manager from " "group <strong>%(group_id)s</strong>." msgstr "" -#: ../vial/blueprints/groups/__init__.py:898 -#: ../vial/blueprints/users/__init__.py:987 +#: ../hawat/blueprints/groups/__init__.py:898 +#: ../hawat/blueprints/users/__init__.py:987 #, python-format msgid "" "Canceled removing user <strong>%(user_id)s</strong> as a manager from " "group <strong>%(group_id)s</strong>." msgstr "" -#: ../vial/blueprints/groups/__init__.py:931 +#: ../hawat/blueprints/groups/__init__.py:931 #, python-format msgid "Group <strong>%(item_id)s</strong> was successfully enabled." msgstr "Skupina <strong>%(item_id)s</strong> byla úspěšnÄ› aktivována." -#: ../vial/blueprints/groups/__init__.py:757 +#: ../hawat/blueprints/groups/__init__.py:757 #, python-format msgid "Unable to enable group <strong>%(item_id)s</strong>." msgstr "Nelze aktivovat skupinu <strong>%(item_id)s</strong>." -#: ../vial/blueprints/groups/__init__.py:764 +#: ../hawat/blueprints/groups/__init__.py:764 #, python-format msgid "Canceled enabling group <strong>%(item_id)s</strong>." msgstr "Aktivace skupiny <strong>%(item_id)s</strong> byla zruÅ¡ena." -#: ../vial/blueprints/groups/__init__.py:797 +#: ../hawat/blueprints/groups/__init__.py:797 #, python-format msgid "Group <strong>%(item_id)s</strong> was successfully disabled." msgstr "Skupina <strong>%(item_id)s</strong> byla úspěšnÄ› deaktivována." -#: ../vial/blueprints/groups/__init__.py:804 +#: ../hawat/blueprints/groups/__init__.py:804 #, python-format msgid "Unable to disable group <strong>%(item_id)s</strong>." msgstr "Nelze deaktivovat skupinu <strong>%(item_id)s</strong>." -#: ../vial/blueprints/groups/__init__.py:811 +#: ../hawat/blueprints/groups/__init__.py:811 #, python-format msgid "Canceled disabling group <strong>%(item_id)s</strong>." msgstr "Deaktivace skupiny <strong>%(item_id)s</strong> byla zruÅ¡ena." -#: ../vial/blueprints/groups/__init__.py:845 +#: ../hawat/blueprints/groups/__init__.py:845 #, python-format msgid "" "Group <strong>%(item_id)s</strong> was successfully and permanently " "deleted." msgstr "Skupina <strong>%(item_id)s</strong> byla úspěšnÄ› a trvale smazána." -#: ../vial/blueprints/groups/__init__.py:852 +#: ../hawat/blueprints/groups/__init__.py:852 #, python-format msgid "Unable to delete group <strong>%(item_id)s</strong>." msgstr "Nelze smazat skupinu <strong>%(item_id)s</strong>." -#: ../vial/blueprints/groups/__init__.py:859 +#: ../hawat/blueprints/groups/__init__.py:859 #, python-format msgid "Canceled deleting group <strong>%(item_id)s</strong>." msgstr "Mazánà skupiny <strong>%(item_id)s</strong> bylo zruÅ¡eno." -#: ../vial/blueprints/groups/forms.py:173 +#: ../hawat/blueprints/groups/forms.py:173 msgid "Name, description:" msgstr "" -#: ../vial/blueprints/groups/forms.py:178 +#: ../hawat/blueprints/groups/forms.py:178 msgid "" "Group`s full name or description. Search is performed even in the middle " "of the strings." msgstr "" -#: ../vial/blueprints/groups/forms.py:208 +#: ../hawat/blueprints/groups/forms.py:208 msgid "Search for groups with particular state." msgstr "" -#: ../vial/blueprints/groups/forms.py:216 +#: ../hawat/blueprints/groups/forms.py:216 msgid "Search for groups coming from particular sources/feeds." msgstr "" -#: ../vial/blueprints/groups/forms.py:219 +#: ../hawat/blueprints/groups/forms.py:219 msgid "Group members:" msgstr "" -#: ../vial/blueprints/groups/forms.py:222 +#: ../hawat/blueprints/groups/forms.py:222 msgid "Search for groups with particular members." msgstr "" -#: ../vial/blueprints/groups/forms.py:225 +#: ../hawat/blueprints/groups/forms.py:225 msgid "Group managers:" msgstr "" -#: ../vial/blueprints/groups/forms.py:228 +#: ../hawat/blueprints/groups/forms.py:228 msgid "Search for groups with particular managers." msgstr "" -#: ../vial/blueprints/groups/forms.py:239 ../vial/blueprints/users/forms.py:250 +#: ../hawat/blueprints/groups/forms.py:239 ../hawat/blueprints/users/forms.py:250 msgid "by name descending" msgstr "" -#: ../vial/blueprints/groups/forms.py:240 ../vial/blueprints/users/forms.py:251 +#: ../hawat/blueprints/groups/forms.py:240 ../hawat/blueprints/users/forms.py:251 msgid "by name ascending" msgstr "" -#: ../vial/blueprints/users/__init__.py:56 +#: ../hawat/blueprints/users/__init__.py:56 msgid "User management" msgstr "Správa uživatelů" -#: ../vial/blueprints/users/__init__.py:194 +#: ../hawat/blueprints/users/__init__.py:194 #, python-format msgid "Show details of user account "%(item)s"" msgstr "Zobrazit detaily uživatelského úÄtu "%(item)s"" -#: ../vial/blueprints/users/__init__.py:199 +#: ../hawat/blueprints/users/__init__.py:199 msgid "Show user account details" msgstr "Zobrazit detaily uživatelského úÄtu" -#: ../vial/blueprints/users/__init__.py:343 +#: ../hawat/blueprints/users/__init__.py:343 msgid "My account" msgstr "Můj úÄet" -#: ../vial/blueprints/users/__init__.py:351 +#: ../hawat/blueprints/users/__init__.py:351 msgid "My user account" msgstr "Můj uživatelský úÄet" -#: ../vial/blueprints/users/__init__.py:417 +#: ../hawat/blueprints/users/__init__.py:417 msgid "Create new user account" msgstr "VytvoÅ™it nový uživatelský úÄet" -#: ../vial/blueprints/users/__init__.py:431 +#: ../hawat/blueprints/users/__init__.py:431 #, python-format msgid "User account <strong>%(item_id)s</strong> was successfully created." msgstr "Uživatelský úÄet <strong>%(item_id)s</strong> byl úspěšnÄ› vytvoÅ™en." -#: ../vial/blueprints/users/__init__.py:446 +#: ../hawat/blueprints/users/__init__.py:446 msgid "Unable to create new user account." msgstr "Nelze vytvoÅ™it nový uživatelský úÄet." -#: ../vial/blueprints/users/__init__.py:450 +#: ../hawat/blueprints/users/__init__.py:450 msgid "Canceled creating new user account." msgstr "Vytvářenà nového uživatelského úÄtu bylo zruÅ¡eno." -#: ../vial/blueprints/users/__init__.py:482 +#: ../hawat/blueprints/users/__init__.py:482 msgid "Update user account details" msgstr "Aktualizovat detaily uživatelského úÄtu" -#: ../vial/blueprints/users/__init__.py:510 +#: ../hawat/blueprints/users/__init__.py:510 #, python-format msgid "User account <strong>%(item_id)s</strong> was successfully updated." msgstr "Uživatelský úÄet <strong>%(item_id)s</strong> byl úspěšnÄ› aktualizován." -#: ../vial/blueprints/users/__init__.py:517 +#: ../hawat/blueprints/users/__init__.py:517 #, python-format msgid "Unable to update user account <strong>%(item_id)s</strong>." msgstr "Nelze aktualizovat uživatelský úÄet <strong>%(item_id)s</strong>." -#: ../vial/blueprints/users/__init__.py:524 +#: ../hawat/blueprints/users/__init__.py:524 #, python-format msgid "Canceled updating user account <strong>%(item_id)s</strong>." msgstr "Aktualizace uživatelského úÄtu <strong>%(item_id)s</strong> byla zruÅ¡ena." -#: ../vial/blueprints/users/__init__.py:568 +#: ../hawat/blueprints/users/__init__.py:568 msgid "Add group membership" msgstr "PÅ™idat ÄlenstvÃ" -#: ../vial/blueprints/users/__init__.py:661 +#: ../hawat/blueprints/users/__init__.py:661 msgid "Reject group membership" msgstr "OdmÃtnout ÄlenstvÃ" -#: ../vial/blueprints/users/__init__.py:744 +#: ../hawat/blueprints/users/__init__.py:744 msgid "Remove group membership" msgstr "Odebrat ÄlenstvÃ" -#: ../vial/blueprints/users/__init__.py:830 +#: ../hawat/blueprints/users/__init__.py:830 msgid "Add group management" msgstr "" -#: ../vial/blueprints/users/__init__.py:919 +#: ../hawat/blueprints/users/__init__.py:919 msgid "Remove group management" msgstr "" -#: ../vial/blueprints/users/__init__.py:1024 +#: ../hawat/blueprints/users/__init__.py:1024 #, python-format msgid "User account <strong>%(item_id)s</strong> was successfully enabled." msgstr "Uživatelský úÄet <strong>%(item_id)s</strong> byl úspěšnÄ› aktivován." -#: ../vial/blueprints/users/__init__.py:1031 +#: ../hawat/blueprints/users/__init__.py:1031 #, python-format msgid "Unable to enable user account <strong>%(item_id)s</strong>." msgstr "Nelze aktivovat uživatelský úÄet <strong>%(item_id)s</strong>." -#: ../vial/blueprints/users/__init__.py:1038 +#: ../hawat/blueprints/users/__init__.py:1038 #, python-format msgid "Canceled enabling user account <strong>%(item_id)s</strong>." msgstr "Aktivace uživatelského úÄtu <strong>%(item_id)s</strong> byla pÅ™eruÅ¡ena." -#: ../vial/blueprints/users/__init__.py:1050 +#: ../hawat/blueprints/users/__init__.py:1050 #, python-format msgid "[%(app_name)s] Account activation - %(item_id)s" msgstr "" -#: ../vial/blueprints/users/__init__.py:1101 +#: ../hawat/blueprints/users/__init__.py:1101 #, python-format msgid "User account <strong>%(item_id)s</strong> was successfully disabled." msgstr "Uživatelský úÄet <strong>%(item_id)s</strong> byl úspěšnÄ› deaktivován." -#: ../vial/blueprints/users/__init__.py:1108 +#: ../hawat/blueprints/users/__init__.py:1108 #, python-format msgid "Unable to disable user account <strong>%(item_id)s</strong>." msgstr "Nelze deaktivovat uživatelský úÄet <strong>%(item_id)s</strong>." -#: ../vial/blueprints/users/__init__.py:1115 +#: ../hawat/blueprints/users/__init__.py:1115 #, python-format msgid "Canceled disabling user account <strong>%(item_id)s</strong>." msgstr "Deaktivace uživatelského úÄtu <strong>%(item_id)s</strong> byla pÅ™eruÅ¡ena." -#: ../vial/blueprints/users/__init__.py:1152 +#: ../hawat/blueprints/users/__init__.py:1152 #, python-format msgid "" "User account <strong>%(item_id)s</strong> was successfully and " "permanently deleted." msgstr "Uživatelský úÄet <strong>%(item_id)s</strong> byl úspěšnÄ› a trvale smazán." -#: ../vial/blueprints/users/__init__.py:1159 +#: ../hawat/blueprints/users/__init__.py:1159 #, python-format msgid "Unable to delete user account <strong>%(item_id)s</strong>." msgstr "Nelze smazat uživatelský úÄet <strong>%(item_id)s</strong>." -#: ../vial/blueprints/users/__init__.py:1166 +#: ../hawat/blueprints/users/__init__.py:1166 #, python-format msgid "Canceled deleting user account <strong>%(item_id)s</strong>." msgstr "Mazánà uživatelského úÄtu <strong>%(item_id)s</strong> bylo zruÅ¡eno." -#: ../vial/blueprints/users/__init__.py:1178 +#: ../hawat/blueprints/users/__init__.py:1178 msgid "User account management" msgstr "Správa uživatelských úÄtů" -#: ../vial/blueprints/users/forms.py:184 +#: ../hawat/blueprints/users/forms.py:184 msgid "Login, name, email:" msgstr "" -#: ../vial/blueprints/users/forms.py:189 +#: ../hawat/blueprints/users/forms.py:189 msgid "" "User`s login, full name or email address. Search is performed even in the" " middle of the strings, so for example you may lookup by domain." msgstr "" -#: ../vial/blueprints/users/forms.py:217 +#: ../hawat/blueprints/users/forms.py:217 msgid "Search for users with particular account state." msgstr "" -#: ../vial/blueprints/users/forms.py:220 +#: ../hawat/blueprints/users/forms.py:220 msgid "Role:" msgstr "" -#: ../vial/blueprints/users/forms.py:225 +#: ../hawat/blueprints/users/forms.py:225 msgid "Search for users with particular role, or without any assigned roles." msgstr "" -#: ../vial/blueprints/users/forms.py:228 +#: ../hawat/blueprints/users/forms.py:228 msgid "Group membership:" msgstr "" -#: ../vial/blueprints/users/forms.py:231 +#: ../hawat/blueprints/users/forms.py:231 msgid "Search for users with membership with particular group." msgstr "" -#: ../vial/blueprints/users/forms.py:234 +#: ../hawat/blueprints/users/forms.py:234 msgid "Group management:" msgstr "" -#: ../vial/blueprints/users/forms.py:237 +#: ../hawat/blueprints/users/forms.py:237 msgid "Search for users with management rights to particular group." msgstr "" -#: ../vial/blueprints/users/forms.py:241 +#: ../hawat/blueprints/users/forms.py:241 msgid "Sort result by:" msgstr "" -#: ../vial/blueprints/users/forms.py:248 +#: ../hawat/blueprints/users/forms.py:248 msgid "by login descending" msgstr "" -#: ../vial/blueprints/users/forms.py:249 +#: ../hawat/blueprints/users/forms.py:249 msgid "by login ascending" msgstr "" -#: ../vial/blueprints/users/forms.py:252 +#: ../hawat/blueprints/users/forms.py:252 msgid "by email descending" msgstr "" -#: ../vial/blueprints/users/forms.py:253 +#: ../hawat/blueprints/users/forms.py:253 msgid "by email ascending" msgstr "" -#: ../vial/blueprints/users/forms.py:254 +#: ../hawat/blueprints/users/forms.py:254 msgid "by login time descending" msgstr "" -#: ../vial/blueprints/users/forms.py:255 +#: ../hawat/blueprints/users/forms.py:255 msgid "by login time ascending" msgstr "" -#: ../vial/blueprints/users/forms.py:275 +#: ../hawat/blueprints/users/forms.py:275 msgid "Without any roles" msgstr "" -#: ../vial/view/__init__.py:624 ../vial/view/__init__.py:645 +#: ../hawat/view/__init__.py:624 ../hawat/view/__init__.py:645 msgid "You have entered wrong login credentials." msgstr "Byly zadány neplatné pÅ™ihlaÅ¡ovacà údaje." -#: ../vial/view/__init__.py:633 +#: ../hawat/view/__init__.py:633 #, python-format msgid "Unable to perform login as <strong>%(user)s</strong>." msgstr "" -#: ../vial/view/__init__.py:655 +#: ../hawat/view/__init__.py:655 #, python-format msgid "" "Your user account <strong>%(login)s (%(name)s)</strong> is currently " @@ -5859,99 +5859,99 @@ msgstr "" "Váš uživatelský úÄet <strong>%(login)s (%(name)s)</strong> je v tuto " "chvÃli deaktivován, jako tento uživatele se nemůžete bohužel pÅ™ihlásit." -#: ../vial/view/__init__.py:663 +#: ../hawat/view/__init__.py:663 msgid "You have used wrong login credentials." msgstr "Použili jste neplatné pÅ™ihlaÅ¡ovacà údaje." -#: ../vial/view/__init__.py:685 +#: ../hawat/view/__init__.py:685 #, python-format msgid "You have been successfully logged in as <strong>%(user)s</strong>." msgstr "PÅ™ihlášenà bylo úspěšné pod uživatelským úÄtem <strong>%(user)s</strong>." -#: ../vial/view/__init__.py:732 +#: ../hawat/view/__init__.py:732 msgid "Search for last hour" msgstr "Hledat za poslednà hodinu" -#: ../vial/view/__init__.py:733 +#: ../hawat/view/__init__.py:733 msgid "Search for last 2 hours" msgstr "Hledat za poslednà 2 hodiny" -#: ../vial/view/__init__.py:734 +#: ../hawat/view/__init__.py:734 msgid "Search for last 3 hours" msgstr "Hledat za poslednà 3 hodiny" -#: ../vial/view/__init__.py:735 +#: ../hawat/view/__init__.py:735 msgid "Search for last 4 hours" msgstr "Hledat za poslednà 4 hodiny" -#: ../vial/view/__init__.py:736 +#: ../hawat/view/__init__.py:736 msgid "Search for last 6 hours" msgstr "Hledat za poslednà 6 hodin" -#: ../vial/view/__init__.py:737 +#: ../hawat/view/__init__.py:737 msgid "Search for last 12 hours" msgstr "Hledat za poslednà 12 hodin" -#: ../vial/view/__init__.py:738 +#: ../hawat/view/__init__.py:738 msgid "Search for last day" msgstr "Hledat za poslednà den" -#: ../vial/view/__init__.py:739 +#: ../hawat/view/__init__.py:739 msgid "Search for last 2 days" msgstr "Hledat za poslednà 2 dny" -#: ../vial/view/__init__.py:740 +#: ../hawat/view/__init__.py:740 msgid "Search for last 3 days" msgstr "Hledat za poslednà 3 dny" -#: ../vial/view/__init__.py:741 +#: ../hawat/view/__init__.py:741 msgid "Search for last week" msgstr "Hledat za poslednà týden" -#: ../vial/view/__init__.py:742 +#: ../hawat/view/__init__.py:742 msgid "Search for last 2 weeks" msgstr "Hledat za poslednà 2 týdny" -#: ../vial/view/__init__.py:743 +#: ../hawat/view/__init__.py:743 msgid "Search for last 4 weeks" msgstr "Hledat za poslednà 4 týdny" -#: ../vial/view/__init__.py:744 +#: ../hawat/view/__init__.py:744 msgid "Search for last 12 weeks" msgstr "Hledat za poslednÃch 12 týdnů" -#: ../vial/view/__init__.py:746 +#: ../hawat/view/__init__.py:746 msgid "Search for today" msgstr "Hledat za dneÅ¡ek" -#: ../vial/view/__init__.py:747 +#: ../hawat/view/__init__.py:747 msgid "Search for this week" msgstr "Hledat za tento týden" -#: ../vial/view/__init__.py:748 +#: ../hawat/view/__init__.py:748 msgid "Search for this month" msgstr "Hledat za tento mÄ›sÃc" -#: ../vial/view/__init__.py:749 +#: ../hawat/view/__init__.py:749 msgid "Search for this year" msgstr "Hledat za tento rok" -#: ../vial/view/__init__.py:956 ../vial/view/__init__.py:1077 +#: ../hawat/view/__init__.py:956 ../hawat/view/__init__.py:1077 #, python-format msgid "Invalid address value <strong>%(address)s</strong> in search form." msgstr "Neplatná hodnota pro IP adresu <strong>%(address)s</strong> ve formuláři." -#: ../vial/view/__init__.py:1546 ../vial/view/__init__.py:1556 -#: ../vial/view/__init__.py:1681 ../vial/view/__init__.py:1877 +#: ../hawat/view/__init__.py:1546 ../hawat/view/__init__.py:1556 +#: ../hawat/view/__init__.py:1681 ../hawat/view/__init__.py:1877 msgid "Create" msgstr "VytvoÅ™it" -#: ../vial/view/__init__.py:1581 ../vial/view/__init__.py:1782 +#: ../hawat/view/__init__.py:1581 ../hawat/view/__init__.py:1782 #, python-format msgid "Item \"%(item)s\" already exists" msgstr "Položka \"%(item)s\" již existuje" -#: ../vial/view/__init__.py:1921 +#: ../hawat/view/__init__.py:1921 #, python-format msgid "" "User account <strong>%(login)s (%(name)s)</strong> was successfully " @@ -5960,43 +5960,43 @@ msgstr "" "Uživatelský úÄet <strong>%(login)s (%(name)s)</strong> byl úspěšnÄ› " "zaregistrován." -#: ../vial/view/__init__.py:1929 +#: ../hawat/view/__init__.py:1929 msgid "Unable to register new user account." msgstr "Nelze zaregistrovat nový uživatelský úÄet." -#: ../vial/view/__init__.py:1933 +#: ../hawat/view/__init__.py:1933 msgid "Account registration canceled." msgstr "Registrace uživatelského úÄtu byla zruÅ¡ena" -#: ../vial/view/__init__.py:1937 +#: ../hawat/view/__init__.py:1937 #, python-format msgid "Please use different login, the \"%(item)s\" is already taken." msgstr "ProsÃm použijte jiný login, \"%(item)s\" již existuje." -#: ../vial/view/__init__.py:1953 ../vial/view/__init__.py:2001 -#: ../vial/view/__init__.py:2033 +#: ../hawat/view/__init__.py:1953 ../hawat/view/__init__.py:2001 +#: ../hawat/view/__init__.py:2033 #, python-format msgid "[%(app_name)s] Account registration - %(item_id)s" msgstr "[%(app_name)s] Registrace uživatelského úÄtu - %(item_id)s" -#: ../vial/view/__init__.py:2174 ../vial/view/__init__.py:2373 -#: ../vial/view/__init__.py:2576 +#: ../hawat/view/__init__.py:2174 ../hawat/view/__init__.py:2373 +#: ../hawat/view/__init__.py:2576 msgid "No changes detected, no update needed." msgstr "Nebyly zjiÅ¡tÄ›ny žádné zmÄ›ny, aktualizace nenà nutná." -#: ../vial/view/__init__.py:2231 ../vial/view/__init__.py:2236 +#: ../hawat/view/__init__.py:2231 ../hawat/view/__init__.py:2236 msgid "Delete" msgstr "Smazat" -#: ../vial/view/__init__.py:2429 ../vial/view/__init__.py:2434 +#: ../hawat/view/__init__.py:2429 ../hawat/view/__init__.py:2434 msgid "Disable" msgstr "Deaktivovat" -#: ../vial/view/__init__.py:2465 ../vial/view/__init__.py:2470 +#: ../hawat/view/__init__.py:2465 ../hawat/view/__init__.py:2470 msgid "Enable" msgstr "Aktivovat" -#: ../vial/view/mixin.py:259 +#: ../hawat/view/mixin.py:259 #, python-format msgid "Invalid value %(val)s for snippet rendering parameter." msgstr "Neplatná hodnota %(val)s parametru pre renderovánà snippetů." diff --git a/lib/vial/utils.py b/lib/hawat/utils.py similarity index 91% rename from lib/vial/utils.py rename to lib/hawat/utils.py index b0f2e1453e3b9abc3e234f0a760204cf63365ec5..3c84b2cee10a66f6ccb1010f33422444e42e1feb 100644 --- a/lib/vial/utils.py +++ b/lib/hawat/utils.py @@ -1,14 +1,19 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- +# This file is part of Mentat system (https://mentat.cesnet.cz/). +# +# Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- +# ------------------------------------------------------------------------------- """ -This module contains various usefull utilities for *Vial* application. +This module contains various usefull utilities for *Hawat* application. """ +__author__ = "Jan Mach <jan.mach@cesnet.cz>" +__credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea KropáÄová <andrea.kropacova@cesnet.cz>" import os import uuid diff --git a/lib/vial/view/__init__.py b/lib/hawat/view/__init__.py similarity index 92% rename from lib/vial/view/__init__.py rename to lib/hawat/view/__init__.py index dee54ac699bf7f998ba8d632018a9e609a8f4beb..e626d3b02d5ef7ea5112de4621b4dc0f90c74214 100644 --- a/lib/vial/view/__init__.py +++ b/lib/hawat/view/__init__.py @@ -6,7 +6,7 @@ """ -This module contains base classes for all *Vial* application views. They are all +This module contains base classes for all *hawat* application views. They are all based on :py:class:`flask.views.View`. """ @@ -25,12 +25,12 @@ import flask_principal import flask_mail from flask_babel import gettext, force_locale -import vial.const -import vial.menu -import vial.db -import vial.errors -from vial.view.mixin import VialUtils -from vial.forms import ItemActionConfirmForm +import hawat.const +import hawat.menu +import hawat.db +import hawat.errors +from hawat.view.mixin import HawatUtils +from hawat.forms import ItemActionConfirmForm class DecoratedView: @@ -55,7 +55,7 @@ class DecoratedView: class BaseView(flask.views.View): """ - Base class for all custom Vial application views. + Base class for all custom hawat application views. """ module_ref = None @@ -66,7 +66,7 @@ class BaseView(flask.views.View): module_name = None """ Name of the parent module/blueprint. Will be set up during the process - of registering the view into the blueprint in :py:func:`vial.app.VialBlueprint.register_view_class`. + of registering the view into the blueprint in :py:func:`hawat.app.hawatBlueprint.register_view_class`. """ authentication = False @@ -74,7 +74,7 @@ class BaseView(flask.views.View): Similar to the ``decorators`` mechanism in Flask pluggable views, you may use this class variable to specify, that the view is protected by authentication. During the process of registering the view into the blueprint in - :py:func:`vial.app.VialBlueprint.register_view_class` the view will be + :py:func:`hawat.app.hawatBlueprint.register_view_class` the view will be automatically decorated with :py:func:`flask_login.login_required` decorator. The advantage of using this in favor of ``decorators`` is that the application @@ -88,7 +88,7 @@ class BaseView(flask.views.View): Similar to the ``decorators`` mechanism in Flask pluggable views, you may use this class variable to specify, that the view is protected by authorization. During the process of registering the view into the blueprint in - :py:func:`vial.app.VialBlueprint.register_view_class` the view will be + :py:func:`hawat.app.hawatBlueprint.register_view_class` the view will be automatically decorated with given authorization decorators. The advantage of using this in favor of ``decorators`` is that the application @@ -190,7 +190,7 @@ class BaseView(flask.views.View): Return title for the view, that will be displayed in the ``title`` tag of HTML ``head`` element and also as the content of page header in ``h2`` tag. - Default implementation returns the return value of :py:func:`vial.view.BaseView.get_menu_title` + Default implementation returns the return value of :py:func:`hawat.view.BaseView.get_menu_title` method by default. :param dict kwargs: Optional parameters. @@ -204,7 +204,7 @@ class BaseView(flask.views.View): """ Return menu entry title for the view. - Default implementation returns the return value of :py:func:`vial.view.BaseView.get_view_title` + Default implementation returns the return value of :py:func:`hawat.view.BaseView.get_view_title` method by default. :param dict kwargs: Optional parameters. @@ -218,7 +218,7 @@ class BaseView(flask.views.View): """ Return menu entry legend for the view (menu entry hover tooltip). - Default implementation returns the return value of :py:func:`vial.view.BaseView.get_menu_title` + Default implementation returns the return value of :py:func:`hawat.view.BaseView.get_menu_title` method by default. :param dict kwargs: Optional parameters. @@ -302,7 +302,7 @@ class FileNameView(BaseView): @classmethod def get_view_icon(cls): - """*Implementation* of :py:func:`vial.view.BaseView.get_view_icon`.""" + """*Implementation* of :py:func:`hawat.view.BaseView.get_view_icon`.""" return 'action-download' @classmethod @@ -367,7 +367,7 @@ class FileIdView(BaseView): @classmethod def get_view_icon(cls): - """*Implementation* of :py:func:`vial.view.BaseView.get_view_icon`.""" + """*Implementation* of :py:func:`hawat.view.BaseView.get_view_icon`.""" return 'action-download' @classmethod @@ -466,7 +466,7 @@ class RenderableView(BaseView): # pylint: disable=locally-disabled,abstract-met Return Jinja2 template file that should be used for rendering the view content. This default implementation works only in case the view class was properly registered into the parent blueprint/module with - :py:func:`vial.app.VialBlueprint.register_view_class` method. + :py:func:`hawat.app.hawatBlueprint.register_view_class` method. :return: Jinja2 template file to use to render the view. :rtype: str @@ -522,7 +522,7 @@ class SimpleView(RenderableView): # pylint: disable=locally-disabled,abstract-m any user parameters. In most use cases, it should be enough to just enhance the default implementation - of :py:func:`vial.view.RenderableView.get_response_context` to inject + of :py:func:`hawat.view.RenderableView.get_response_context` to inject some additional variables into the template. """ @@ -546,11 +546,11 @@ class BaseLoginView(SimpleView): @classmethod def get_view_name(cls): - return vial.const.ACTION_USER_LOGIN + return hawat.const.ACTION_USER_LOGIN @classmethod def get_view_icon(cls): - return vial.const.ACTION_USER_LOGIN + return hawat.const.ACTION_USER_LOGIN @classmethod def get_view_title(cls, **kwargs): @@ -622,7 +622,7 @@ class BaseLoginView(SimpleView): except sqlalchemy.orm.exc.NoResultFound: self.flash( gettext('You have entered wrong login credentials.'), - vial.const.FLASH_FAILURE + hawat.const.FLASH_FAILURE ) self.abort(403) @@ -632,7 +632,7 @@ class BaseLoginView(SimpleView): "Unable to perform login as <strong>%(user)s</strong>.", user = flask.escape(str(user_login)) )), - vial.const.FLASH_FAILURE + hawat.const.FLASH_FAILURE ) flask.current_app.log_exception_with_label( traceback.TracebackException(*sys.exc_info()), @@ -643,7 +643,7 @@ class BaseLoginView(SimpleView): if not user: self.flash( gettext('You have entered wrong login credentials.'), - vial.const.FLASH_FAILURE + hawat.const.FLASH_FAILURE ) self.abort(403) @@ -654,14 +654,14 @@ class BaseLoginView(SimpleView): login = flask.escape(user.login), name = flask.escape(user.fullname) )), - vial.const.FLASH_FAILURE + hawat.const.FLASH_FAILURE ) self.abort(403) if not self.authenticate_user(user): self.flash( gettext('You have used wrong login credentials.'), - vial.const.FLASH_FAILURE + hawat.const.FLASH_FAILURE ) self.abort(403) @@ -684,7 +684,7 @@ class BaseLoginView(SimpleView): 'You have been successfully logged in as <strong>%(user)s</strong>.', user = flask.escape(str(user)) )), - vial.const.FLASH_SUCCESS + hawat.const.FLASH_SUCCESS ) self.logger.info( "User '{}' successfully logged in with '{}'.".format( @@ -701,18 +701,18 @@ class BaseLoginView(SimpleView): ) self.response_context.update( - next = vial.forms.get_redirect_target() + next = hawat.forms.get_redirect_target() ) return self.generate_response() -class BaseSearchView(RenderableView, VialUtils): +class BaseSearchView(RenderableView, HawatUtils): """ Base class for search views. """ @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`hawat.view.BaseView.get_view_name`.""" return 'search' @classmethod @@ -823,7 +823,7 @@ class BaseSearchView(RenderableView, VialUtils): """ Get breadcrumbs menu. """ - breadcrumbs_menu = vial.menu.Menu() + breadcrumbs_menu = hawat.menu.Menu() breadcrumbs_menu.add_entry( 'endpoint', 'home', @@ -848,8 +848,8 @@ class BaseSearchView(RenderableView, VialUtils): @classmethod def get_context_action_menu(cls): - """*Implementation* of :py:func:`vial.view.ItemListView.get_context_action_menu`.""" - context_action_menu = vial.menu.Menu() + """*Implementation* of :py:func:`hawat.view.ItemListView.get_context_action_menu`.""" + context_action_menu = hawat.menu.Menu() context_action_menu.add_entry( 'endpoint', 'show', @@ -878,7 +878,7 @@ class BaseSearchView(RenderableView, VialUtils): form = self.get_search_form(flask.request.args) flask.g.search_form = form - if vial.const.FORM_ACTION_SUBMIT in flask.request.args: + if hawat.const.FORM_ACTION_SUBMIT in flask.request.args: if form.validate(): form_data = form.data @@ -956,7 +956,7 @@ class BaseSearchView(RenderableView, VialUtils): address = flask.escape(str(match.group(1))) ) ), - vial.const.FLASH_FAILURE + hawat.const.FLASH_FAILURE ) else: raise @@ -1009,7 +1009,7 @@ class CustomSearchView(BaseSearchView): form = self.get_search_form(flask.request.args) flask.g.search_form = form - if vial.const.FORM_ACTION_SUBMIT in flask.request.args: + if hawat.const.FORM_ACTION_SUBMIT in flask.request.args: if form.validate(): form_data = form.data @@ -1077,7 +1077,7 @@ class CustomSearchView(BaseSearchView): address = flask.escape(str(match.group(1))) ) ), - vial.const.FLASH_FAILURE + hawat.const.FLASH_FAILURE ) else: raise @@ -1113,7 +1113,7 @@ class ItemListView(RenderableView): # pylint: disable=locally-disabled,abstract """ @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`hawat.view.BaseView.get_view_name`.""" return 'list' @classmethod @@ -1121,7 +1121,7 @@ class ItemListView(RenderableView): # pylint: disable=locally-disabled,abstract """ Get breadcrumbs menu. """ - action_menu = vial.menu.Menu() + action_menu = hawat.menu.Menu() action_menu.add_entry( 'endpoint', 'home', @@ -1249,22 +1249,22 @@ class ItemShowView(RenderableView): # pylint: disable=locally-disabled,abstract @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`hawat.view.BaseView.get_view_name`.""" return 'show' @classmethod def get_view_icon(cls): - """*Implementation* of :py:func:`vial.view.BaseView.get_view_icon`.""" + """*Implementation* of :py:func:`hawat.view.BaseView.get_view_icon`.""" return 'action-show' @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`hawat.view.BaseView.get_view_title`.""" return gettext('Show') @classmethod def get_view_url(cls, **kwargs): - """*Implementation* of :py:func:`vial.view.BaseView.get_view_url`.""" + """*Implementation* of :py:func:`hawat.view.BaseView.get_view_url`.""" if 'item' not in kwargs or not kwargs['item']: raise ValueError( "Missing item parameter for show URL view '{}'". format( @@ -1278,7 +1278,7 @@ class ItemShowView(RenderableView): # pylint: disable=locally-disabled,abstract @classmethod def get_menu_title(cls, **kwargs): - """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`hawat.view.BaseView.get_menu_title`.""" return gettext('Show') @classmethod @@ -1300,7 +1300,7 @@ class ItemShowView(RenderableView): # pylint: disable=locally-disabled,abstract """ Get breadcrumbs menu. """ - action_menu = vial.menu.Menu() + action_menu = hawat.menu.Menu() action_menu.add_entry( 'endpoint', 'home', @@ -1360,14 +1360,14 @@ class ItemActionView(RenderableView): # pylint: disable=locally-disabled,abstra """ @classmethod def get_view_icon(cls): - """*Implementation* of :py:func:`vial.view.BaseView.get_view_icon`.""" + """*Implementation* of :py:func:`hawat.view.BaseView.get_view_icon`.""" return 'action-{}'.format( cls.get_view_name().replace('_', '-') ) @classmethod def get_view_url(cls, **kwargs): - """*Implementation* of :py:func:`vial.view.BaseView.get_view_url`.""" + """*Implementation* of :py:func:`hawat.view.BaseView.get_view_url`.""" return flask.url_for( cls.get_view_endpoint(), item_id = kwargs['item'].get_id() @@ -1375,7 +1375,7 @@ class ItemActionView(RenderableView): # pylint: disable=locally-disabled,abstra @classmethod def get_view_template(cls): - """*Implementation* of :py:func:`vial.view.RenderableView.get_view_template`.""" + """*Implementation* of :py:func:`hawat.view.RenderableView.get_view_template`.""" return 'form_{}.html'.format( cls.get_view_name().replace('-', '_') ) @@ -1419,11 +1419,11 @@ class ItemActionView(RenderableView): # pylint: disable=locally-disabled,abstra """ *Hook method*. Check the form for *cancel* button press and cancel the action. """ - if hasattr(form, vial.const.FORM_ACTION_CANCEL): - if getattr(form, vial.const.FORM_ACTION_CANCEL).data: + if hasattr(form, hawat.const.FORM_ACTION_CANCEL): + if getattr(form, hawat.const.FORM_ACTION_CANCEL).data: self.flash( flask.Markup(self.get_message_cancel(**kwargs)), - vial.const.FLASH_INFO + hawat.const.FLASH_INFO ) return self.redirect( default_url = self.get_url_next() @@ -1484,7 +1484,7 @@ class ItemActionView(RenderableView): # pylint: disable=locally-disabled,abstra Log item action into changelog. One of the method arguments is permitted to be left out. This enables logging create and delete actions. - :param vial.db.MODEL item: Item that is being changed. + :param hawat.db.MODEL item: Item that is being changed. :param str json_state_before: JSON representation of item state before action. :param str json_state_after: JSON representation of item state after action. """ @@ -1494,7 +1494,7 @@ class ItemActionView(RenderableView): # pylint: disable=locally-disabled,abstra # 'Author' may be an instance of 'AnonymousUserMixin' for actions performed by # anonymous users. In that case store 'Null' into database. author = flask_login.current_user._get_current_object() # pylint: disable=locally-disabled,protected-access - if not isinstance(author, self.get_model(vial.const.MODEL_USER)): + if not isinstance(author, self.get_model(hawat.const.MODEL_USER)): author = None chlog = self.dbchlogmodel( @@ -1520,7 +1520,7 @@ class ItemCreateView(ItemActionView): # pylint: disable=locally-disabled,abstra @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`hawat.view.BaseView.get_view_name`.""" return 'create' @classmethod @@ -1529,7 +1529,7 @@ class ItemCreateView(ItemActionView): # pylint: disable=locally-disabled,abstra Return Jinja2 template file that should be used for rendering the view content. This default implementation works only in case the view class was properly registered into the parent blueprint/module with - :py:func:`vial.app.VialBlueprint.register_view_class` method. + :py:func:`hawat.app.hawatBlueprint.register_view_class` method. :return: Title for the view. :rtype: str @@ -1540,17 +1540,17 @@ class ItemCreateView(ItemActionView): # pylint: disable=locally-disabled,abstra @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`hawat.view.BaseView.get_view_title`.""" return gettext('Create') @classmethod def get_view_url(cls, **kwargs): - """*Implementation* of :py:func:`vial.view.BaseView.get_view_url`.""" + """*Implementation* of :py:func:`hawat.view.BaseView.get_view_url`.""" return flask.url_for(cls.get_view_endpoint()) @classmethod def get_menu_title(cls, **kwargs): - """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`hawat.view.BaseView.get_menu_title`.""" return gettext('Create') def get_item(self): @@ -1581,7 +1581,7 @@ class ItemCreateView(ItemActionView): # pylint: disable=locally-disabled,abstra @classmethod def get_breadcrumbs_menu(cls): - breadcrumbs_menu = vial.menu.Menu() + breadcrumbs_menu = hawat.menu.Menu() breadcrumbs_menu.add_entry( 'endpoint', 'home', @@ -1631,7 +1631,7 @@ class ItemCreateView(ItemActionView): # pylint: disable=locally-disabled,abstra form.populate_obj(item) self.do_before_action(item) - if form_data[vial.const.FORM_ACTION_SUBMIT]: + if form_data[hawat.const.FORM_ACTION_SUBMIT]: try: self.dbsession.add(item) self.dbsession.commit() @@ -1644,7 +1644,7 @@ class ItemCreateView(ItemActionView): # pylint: disable=locally-disabled,abstra flask.Markup( self.get_message_success(item = item) ), - vial.const.FLASH_SUCCESS + hawat.const.FLASH_SUCCESS ) return self.redirect(default_url = self.get_url_next()) @@ -1654,7 +1654,7 @@ class ItemCreateView(ItemActionView): # pylint: disable=locally-disabled,abstra flask.Markup( self.get_message_duplicate(item = item) ), - vial.const.FLASH_FAILURE + hawat.const.FLASH_FAILURE ) return self.redirect(default_url = self.get_url_next()) @@ -1664,7 +1664,7 @@ class ItemCreateView(ItemActionView): # pylint: disable=locally-disabled,abstra flask.Markup( self.get_message_failure() ), - vial.const.FLASH_FAILURE + hawat.const.FLASH_FAILURE ) flask.current_app.log_exception_with_label( traceback.TracebackException(*sys.exc_info()), @@ -1675,7 +1675,7 @@ class ItemCreateView(ItemActionView): # pylint: disable=locally-disabled,abstra self.response_context.update( action_name = gettext('Create'), form_url = flask.url_for(self.get_view_endpoint()), - item_action = vial.const.ACTION_ITEM_CREATE, + item_action = hawat.const.ACTION_ITEM_CREATE, item_type = self.dbmodel.__name__.lower() ) @@ -1693,17 +1693,17 @@ class ItemCreateForView(ItemActionView): # pylint: disable=locally-disabled,abs @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`hawat.view.BaseView.get_view_name`.""" return 'createfor' @classmethod def get_view_icon(cls): - """*Implementation* of :py:func:`vial.view.BaseView.get_view_icon`.""" + """*Implementation* of :py:func:`hawat.view.BaseView.get_view_icon`.""" return 'module-{}'.format(cls.module_name) @classmethod def get_view_url(cls, **kwargs): - """*Implementation* of :py:func:`vial.view.BaseView.get_view_url`.""" + """*Implementation* of :py:func:`hawat.view.BaseView.get_view_url`.""" return flask.url_for( cls.get_view_endpoint(), parent_id = kwargs['parent'].id @@ -1715,7 +1715,7 @@ class ItemCreateForView(ItemActionView): # pylint: disable=locally-disabled,abs Return Jinja2 template file that should be used for rendering the view content. This default implementation works only in case the view class was properly registered into the parent blueprint/module with - :py:func:`vial.app.VialBlueprint.register_view_class` method. + :py:func:`hawat.app.hawatBlueprint.register_view_class` method. :return: Title for the view. :rtype: str @@ -1779,7 +1779,7 @@ class ItemCreateForView(ItemActionView): # pylint: disable=locally-disabled,abs @classmethod def get_breadcrumbs_menu(cls): - breadcrumbs_menu = vial.menu.Menu() + breadcrumbs_menu = hawat.menu.Menu() breadcrumbs_menu.add_entry( 'endpoint', 'home', @@ -1833,7 +1833,7 @@ class ItemCreateForView(ItemActionView): # pylint: disable=locally-disabled,abs self.add_parent_to_item(item, parent) self.do_before_action(item) - if form_data[vial.const.FORM_ACTION_SUBMIT]: + if form_data[hawat.const.FORM_ACTION_SUBMIT]: try: self.dbsession.add(item) self.dbsession.commit() @@ -1844,7 +1844,7 @@ class ItemCreateForView(ItemActionView): # pylint: disable=locally-disabled,abs self.flash( flask.Markup(self.get_message_success(item = item, parent = parent)), - vial.const.FLASH_SUCCESS + hawat.const.FLASH_SUCCESS ) return self.redirect(default_url = self.get_url_next()) @@ -1852,7 +1852,7 @@ class ItemCreateForView(ItemActionView): # pylint: disable=locally-disabled,abs self.dbsession.rollback() self.flash( flask.Markup(self.get_message_duplicate(item = item)), - vial.const.FLASH_FAILURE + hawat.const.FLASH_FAILURE ) return self.redirect(default_url = self.get_url_next()) @@ -1860,7 +1860,7 @@ class ItemCreateForView(ItemActionView): # pylint: disable=locally-disabled,abs self.dbsession.rollback() self.flash( flask.Markup(self.get_message_failure(parent = parent)), - vial.const.FLASH_FAILURE + hawat.const.FLASH_FAILURE ) flask.current_app.log_exception_with_label( traceback.TracebackException(*sys.exc_info()), @@ -1872,7 +1872,7 @@ class ItemCreateForView(ItemActionView): # pylint: disable=locally-disabled,abs action_name = gettext('Create'), form_url = flask.url_for(self.get_view_endpoint(), parent_id = parent_id), form = form, - item_action = vial.const.ACTION_ITEM_CREATEFOR, + item_action = hawat.const.ACTION_ITEM_CREATEFOR, item_type = self.dbmodel.__name__.lower(), item = item, parent_type = self.dbmodel_par.__name__.lower(), @@ -1893,11 +1893,11 @@ class BaseRegisterView(ItemCreateView): # pylint: disable=locally-disabled,abst @classmethod def get_view_name(cls): - return vial.const.ACTION_USER_REGISTER + return hawat.const.ACTION_USER_REGISTER @classmethod def get_view_icon(cls): - return vial.const.ACTION_USER_REGISTER + return hawat.const.ACTION_USER_REGISTER @classmethod def get_menu_title(cls, **kwargs): @@ -2034,7 +2034,7 @@ class BaseRegisterView(ItemCreateView): # pylint: disable=locally-disabled,abst flask.current_app.mailer.send(msg) def do_before_action(self, item): # pylint: disable=locally-disabled,no-self-use,unused-argument - item.roles = [vial.const.ROLE_USER] + item.roles = [hawat.const.ROLE_USER] item.enabled = False def do_after_action(self, item): # pylint: disable=locally-disabled,no-self-use,unused-argument @@ -2052,7 +2052,7 @@ class ItemUpdateView(ItemActionView): # pylint: disable=locally-disabled,abstra @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`hawat.view.BaseView.get_view_name`.""" return 'update' @classmethod @@ -2061,7 +2061,7 @@ class ItemUpdateView(ItemActionView): # pylint: disable=locally-disabled,abstra Return Jinja2 template file that should be used for rendering the view content. This default implementation works only in case the view class was properly registered into the parent blueprint/module with - :py:func:`vial.app.VialBlueprint.register_view_class` method. + :py:func:`hawat.app.hawatBlueprint.register_view_class` method. :return: Title for the view. :rtype: str @@ -2072,12 +2072,12 @@ class ItemUpdateView(ItemActionView): # pylint: disable=locally-disabled,abstra @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`hawat.view.BaseView.get_view_title`.""" return gettext('Update') @classmethod def get_menu_title(cls, **kwargs): - """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`hawat.view.BaseView.get_menu_title`.""" return gettext('Update') @staticmethod @@ -2090,7 +2090,7 @@ class ItemUpdateView(ItemActionView): # pylint: disable=locally-disabled,abstra @classmethod def get_breadcrumbs_menu(cls): - breadcrumbs_menu = vial.menu.Menu() + breadcrumbs_menu = hawat.menu.Menu() breadcrumbs_menu.add_entry( 'endpoint', 'home', @@ -2149,12 +2149,12 @@ class ItemUpdateView(ItemActionView): # pylint: disable=locally-disabled,abstra form.populate_obj(item) self.do_before_action(item) - if form_data[vial.const.FORM_ACTION_SUBMIT]: + if form_data[hawat.const.FORM_ACTION_SUBMIT]: try: if item not in self.dbsession.dirty: self.flash( gettext('No changes detected, no update needed.'), - vial.const.FLASH_INFO + hawat.const.FLASH_INFO ) return self.redirect(default_url = self.get_url_next()) @@ -2166,7 +2166,7 @@ class ItemUpdateView(ItemActionView): # pylint: disable=locally-disabled,abstra self.flash( flask.Markup(self.get_message_success(item = item)), - vial.const.FLASH_SUCCESS + hawat.const.FLASH_SUCCESS ) return self.redirect(default_url = self.get_url_next()) @@ -2174,7 +2174,7 @@ class ItemUpdateView(ItemActionView): # pylint: disable=locally-disabled,abstra self.dbsession.rollback() self.flash( flask.Markup(self.get_message_failure(item = item)), - vial.const.FLASH_FAILURE + hawat.const.FLASH_FAILURE ) flask.current_app.log_exception_with_label( traceback.TracebackException(*sys.exc_info()), @@ -2186,7 +2186,7 @@ class ItemUpdateView(ItemActionView): # pylint: disable=locally-disabled,abstra action_name = gettext('Update'), form_url = flask.url_for(self.get_view_endpoint(), item_id = item_id), form = form, - item_action = vial.const.ACTION_ITEM_UPDATE, + item_action = hawat.const.ACTION_ITEM_UPDATE, item_type = self.dbmodel.__name__.lower(), item_id = item_id, item = item @@ -2204,17 +2204,17 @@ class ItemDeleteView(ItemActionView): # pylint: disable=locally-disabled,abstra @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`hawat.view.BaseView.get_view_name`.""" return 'delete' @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`hawat.view.BaseView.get_view_title`.""" return gettext('Delete') @classmethod def get_menu_title(cls, **kwargs): - """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`hawat.view.BaseView.get_menu_title`.""" return gettext('Delete') def dispatch_request(self, item_id): # pylint: disable=locally-disabled,arguments-differ @@ -2247,7 +2247,7 @@ class ItemDeleteView(ItemActionView): # pylint: disable=locally-disabled,abstra self.do_before_action(item) - if form_data[vial.const.FORM_ACTION_SUBMIT]: + if form_data[hawat.const.FORM_ACTION_SUBMIT]: try: self.dbsession.delete(item) self.dbsession.commit() @@ -2258,7 +2258,7 @@ class ItemDeleteView(ItemActionView): # pylint: disable=locally-disabled,abstra self.flash( flask.Markup(self.get_message_success(item = item)), - vial.const.FLASH_SUCCESS + hawat.const.FLASH_SUCCESS ) return self.redirect( default_url = self.get_url_next(), @@ -2272,7 +2272,7 @@ class ItemDeleteView(ItemActionView): # pylint: disable=locally-disabled,abstra self.dbsession.rollback() self.flash( flask.Markup(self.get_message_failure(item = item)), - vial.const.FLASH_FAILURE + hawat.const.FLASH_FAILURE ) flask.current_app.log_exception_with_label( traceback.TracebackException(*sys.exc_info()), @@ -2347,13 +2347,13 @@ class ItemChangeView(ItemActionView): # pylint: disable=locally-disabled,abstra self.do_before_action(item) - if form_data[vial.const.FORM_ACTION_SUBMIT]: + if form_data[hawat.const.FORM_ACTION_SUBMIT]: try: self.change_item(item = item) if item not in self.dbsession.dirty: self.flash( gettext('No changes detected, no update needed.'), - vial.const.FLASH_INFO + hawat.const.FLASH_INFO ) return self.redirect(default_url = self.get_url_next()) @@ -2365,7 +2365,7 @@ class ItemChangeView(ItemActionView): # pylint: disable=locally-disabled,abstra self.flash( flask.Markup(self.get_message_success(item = item)), - vial.const.FLASH_SUCCESS + hawat.const.FLASH_SUCCESS ) return self.redirect( default_url = self.get_url_next() @@ -2375,7 +2375,7 @@ class ItemChangeView(ItemActionView): # pylint: disable=locally-disabled,abstra self.dbsession.rollback() self.flash( flask.Markup(self.get_message_failure(item = item)), - vial.const.FLASH_FAILURE + hawat.const.FLASH_FAILURE ) flask.current_app.log_exception_with_label( traceback.TracebackException(*sys.exc_info()), @@ -2402,22 +2402,22 @@ class ItemDisableView(ItemChangeView): # pylint: disable=locally-disabled,abstr @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`hawat.view.BaseView.get_view_name`.""" return 'disable' @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`hawat.view.BaseView.get_view_title`.""" return gettext('Disable') @classmethod def get_menu_title(cls, **kwargs): - """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`hawat.view.BaseView.get_menu_title`.""" return gettext('Disable') @classmethod def validate_item_change(cls, **kwargs): # pylint: disable=locally-disabled,unused-argument - """*Implementation* of :py:func:`vial.view.ItemChangeView.validate_item_change`.""" + """*Implementation* of :py:func:`hawat.view.ItemChangeView.validate_item_change`.""" # Reject item change in case given item is already disabled. if not kwargs['item'].enabled: return False @@ -2426,7 +2426,7 @@ class ItemDisableView(ItemChangeView): # pylint: disable=locally-disabled,abstr @classmethod def change_item(cls, **kwargs): """ - *Implementation* of :py:func:`vial.view.ItemChangeView.change_item`. + *Implementation* of :py:func:`hawat.view.ItemChangeView.change_item`. """ kwargs['item'].enabled = False @@ -2438,22 +2438,22 @@ class ItemEnableView(ItemChangeView): # pylint: disable=locally-disabled,abstra @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`hawat.view.BaseView.get_view_name`.""" return 'enable' @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`hawat.view.BaseView.get_view_title`.""" return gettext('Enable') @classmethod def get_menu_title(cls, **kwargs): - """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`hawat.view.BaseView.get_menu_title`.""" return gettext('Enable') @classmethod def validate_item_change(cls, **kwargs): # pylint: disable=locally-disabled,unused-argument - """*Implementation* of :py:func:`vial.view.ItemChangeView.validate_item_change`.""" + """*Implementation* of :py:func:`hawat.view.ItemChangeView.validate_item_change`.""" # Reject item change in case given item is already enabled. if kwargs['item'].enabled: return False @@ -2462,7 +2462,7 @@ class ItemEnableView(ItemChangeView): # pylint: disable=locally-disabled,abstra @classmethod def change_item(cls, **kwargs): """ - *Implementation* of :py:func:`vial.view.ItemChangeView.change_item`. + *Implementation* of :py:func:`hawat.view.ItemChangeView.change_item`. """ kwargs['item'].enabled = True @@ -2473,7 +2473,7 @@ class ItemObjectRelationView(ItemChangeView): # pylint: disable=locally-disable """ @classmethod def get_view_icon(cls): - """*Implementation* of :py:func:`vial.view.BaseView.get_view_icon`.""" + """*Implementation* of :py:func:`hawat.view.BaseView.get_view_icon`.""" return 'module-{}'.format(cls.module_name) @classmethod @@ -2482,7 +2482,7 @@ class ItemObjectRelationView(ItemChangeView): # pylint: disable=locally-disable Return Jinja2 template file that should be used for rendering the view content. This default implementation works only in case the view class was properly registered into the parent blueprint/module with - :py:func:`vial.app.VialBlueprint.register_view_class` method. + :py:func:`hawat.app.hawatBlueprint.register_view_class` method. :return: Title for the view. :rtype: str @@ -2493,7 +2493,7 @@ class ItemObjectRelationView(ItemChangeView): # pylint: disable=locally-disable @classmethod def get_view_url(cls, **kwargs): - """*Implementation* of :py:func:`vial.view.BaseView.get_view_url`.""" + """*Implementation* of :py:func:`hawat.view.BaseView.get_view_url`.""" return flask.url_for( cls.get_view_endpoint(), item_id = kwargs['item'].get_id(), @@ -2550,13 +2550,13 @@ class ItemObjectRelationView(ItemChangeView): # pylint: disable=locally-disable self.response_context.update(form_data = form_data) self.do_before_action(item) - if form_data[vial.const.FORM_ACTION_SUBMIT]: + if form_data[hawat.const.FORM_ACTION_SUBMIT]: try: self.change_item(item = item, other = other) if item not in self.dbsession.dirty: self.flash( gettext('No changes detected, no update needed.'), - vial.const.FLASH_INFO + hawat.const.FLASH_INFO ) return self.redirect(default_url = self.get_url_next()) @@ -2572,7 +2572,7 @@ class ItemObjectRelationView(ItemChangeView): # pylint: disable=locally-disable other = other ) ), - vial.const.FLASH_SUCCESS + hawat.const.FLASH_SUCCESS ) return self.redirect( default_url = self.get_url_next() @@ -2587,7 +2587,7 @@ class ItemObjectRelationView(ItemChangeView): # pylint: disable=locally-disable other = other ) ), - vial.const.FLASH_FAILURE + hawat.const.FLASH_FAILURE ) flask.current_app.log_exception_with_label( traceback.TracebackException(*sys.exc_info()), diff --git a/lib/vial/view/mixin.py b/lib/hawat/view/mixin.py similarity index 94% rename from lib/vial/view/mixin.py rename to lib/hawat/view/mixin.py index c9a36fde0a3c3ba1cb1cfc7d254b6c75b5ae2cfc..886e27bca1fbc71d2e9f28f467076f90ce5237f1 100644 --- a/lib/vial/view/mixin.py +++ b/lib/hawat/view/mixin.py @@ -6,7 +6,7 @@ """ -This module contains usefull view mixin classes for *Vial* application views. +This module contains usefull view mixin classes for *Hawat* application views. """ @@ -18,14 +18,14 @@ import flask.app import flask.views from flask_babel import gettext -import vial.const -import vial.menu -import vial.db -import vial.errors -from vial.forms import get_redirect_target +import hawat.const +import hawat.menu +import hawat.db +import hawat.errors +from hawat.forms import get_redirect_target -class VialUtils: +class HawatUtils: """ Small utility method class to enable use of those methods both in the view classes and in the Jinja2 templates. @@ -39,7 +39,7 @@ class VialUtils: try: if not moment: moment = datetime.datetime.utcnow() - return vial.const.TIME_WINDOWS[tiid][wtype](moment) + return hawat.const.TIME_WINDOWS[tiid][wtype](moment) except: # pylint: disable=locally-disabled,bare-except return None @@ -47,7 +47,7 @@ class VialUtils: class HTMLMixin: """ Mixin class enabling rendering responses as HTML. Use it in your custom view - classess based on :py:class:`vial.view.RenderableView` to provide the + classess based on :py:class:`hawat.view.RenderableView` to provide the ability to render Jinja2 template files into HTML documents. """ @@ -98,7 +98,7 @@ class HTMLMixin: class AJAXMixin: """ Mixin class enabling rendering responses as JSON documents. Use it in your - custom view classess based on based on :py:class:`vial.view.RenderableView` + custom view classess based on based on :py:class:`hawat.view.RenderableView` to provide the ability to generate JSON responses. """ KW_RESP_VIEW_TITLE = 'view_title' @@ -112,7 +112,7 @@ class AJAXMixin: code and optional additional message. Return response as JSON document. """ flask.abort( - vial.errors.api_error_response( + hawat.errors.api_error_response( status_code, message ) @@ -193,7 +193,7 @@ class AJAXMixin: class SnippetMixin(AJAXMixin): """ Mixin class enabling rendering responses as JSON documents. Use it in your - custom view classess based on based on :py:class:`vial.view.RenderableView` + custom view classess based on based on :py:class:`hawat.view.RenderableView` to provide the ability to generate JSON responses. """ KW_RESP_SNIPPETS = 'snippets' @@ -245,7 +245,7 @@ class SnippetMixin(AJAXMixin): def process_response_context(self): """ - Reimplementation of :py:func:`vial.view.mixin.AJAXMixin.process_response_context`. + Reimplementation of :py:func:`hawat.view.mixin.AJAXMixin.process_response_context`. """ self.response_context[self.KW_RESP_VIEW_TITLE] = self.get_view_title() self.response_context[self.KW_RESP_VIEW_ICON] = self.get_view_icon() @@ -316,7 +316,7 @@ class SQLAlchemyMixin: """ This property contains the reference to current *SQLAlchemy* database session. """ - return vial.db.db_get().session + return hawat.db.db_get().session def dbquery(self, dbmodel = None): """ diff --git a/lib/vial/__init__.py b/lib/vial/__init__.py deleted file mode 100644 index 077a2c7a6cbb3c29442c06fb680c89dc8583b07b..0000000000000000000000000000000000000000 --- a/lib/vial/__init__.py +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- -# Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- - - -""" -*Vial* is a lightweight skeleton application with batteries included built on top -of excelent `Flask <http://flask.pocoo.org/>`__ microframework. -""" - - -__author__ = "Honza Mach <honza.mach.ml@gmail.com>" -__version__ = "1.0.0" - - -import os - - -def create_app_full( # pylint: disable=locally-disabled,too-many-arguments - app_class, - app_name, - config_dict = None, - config_object = 'vial.config.ProductionConfig', - config_file = None, - config_env = 'VIAL_CONFIG_FILE', - config_func = None): - """ - Factory function for building Vial application. This function takes number of - optional arguments, that can be used to create a very customized instance of - Vial application. This can be very usefull when extending applications` - capabilities or for purposes of testing. Each of these arguments has default - value for the most common application setup, so for disabling it entirely it - is necessary to provide ``None`` as a value. - - :param class app_class: Flask application class to instantinate. - :param string app_name: Name of the application, identifier in lowercase. - :param dict config_dict: Initial default configurations. - :param str config_object: Name of a class or module containing configurations. - :param str config_file: Name of a file containing configurations. - :param str config_env: Name of an environment variable pointing to a file containing configurations. - :param callable config_func: Callable that will receive app.config as parameter. - :return: Vial application - :rtype: vial.app.VialApp - """ - - app = app_class(app_name) - - if config_dict and isinstance(config_dict, dict): - app.config.update(config_dict) - if config_object: - app.config.from_object(config_object) - if config_file: - app.config.from_pyfile(config_file) - if config_env and os.getenv(config_env, None): - app.config.from_envvar(config_env) - if config_func and callable(config_func): - config_func(app.config) - - app.setup_app() - - return app diff --git a/lib/vial/acl.py b/lib/vial/acl.py deleted file mode 100644 index a5c46db2ed29bb2908461a120c26223734f04d7e..0000000000000000000000000000000000000000 --- a/lib/vial/acl.py +++ /dev/null @@ -1,96 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- -# Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- - - -""" -This module contains default ACL configurations for *Vial* application views. -""" - - -from functools import partial - -import flask_principal - -import vial.const - - -MembershipNeed = partial(flask_principal.Need, 'membership') # pylint: disable=locally-disabled,invalid-name -MembershipNeed.__doc__ = """A need with the method preset to `"membership"`.""" - -ManagementNeed = partial(flask_principal.Need, 'management') # pylint: disable=locally-disabled,invalid-name -ManagementNeed.__doc__ = """A need with the method preset to `"management"`.""" - - -ROLE_NAME_ADMIN = vial.const.ROLE_ADMIN -ROLE_NAME_MAINTAINER = vial.const.ROLE_MAINTAINER -ROLE_NAME_POWER = 'power' -ROLE_NAME_DEVELOPER = vial.const.ROLE_DEVELOPER -ROLE_NAME_USER = vial.const.ROLE_USER -ROLE_NAME_ANY = 'any' - - -PERMISSION_ADMIN = flask_principal.Permission( - flask_principal.RoleNeed(ROLE_NAME_ADMIN) -) -""" -The :py:class:`flask_principal.Permission` permission for users with *admin* role -(ultimate power-user with unrestricted access to the whole system). -""" - -PERMISSION_MAINTAINER = flask_principal.Permission( - flask_principal.RoleNeed(ROLE_NAME_MAINTAINER) -) -""" -The :py:class:`flask_principal.Permission` permission for users with *maintainer* role -(power-users with slightly more restricted access to the system than *admin*). -""" - -PERMISSION_POWER = flask_principal.Permission( - flask_principal.RoleNeed(ROLE_NAME_ADMIN), - flask_principal.RoleNeed(ROLE_NAME_MAINTAINER) -) -""" -The concatenated :py:class:`flask_principal.Permission` permission for any power-user role -(*admin* or *maintainer*). -""" - -PERMISSION_DEVELOPER = flask_principal.Permission( - flask_principal.RoleNeed(ROLE_NAME_DEVELOPER) -) -""" -The :py:class:`flask_principal.Permission` permission for users with *developer* role -(system developers with access to additional development and debugging data output). -""" - -PERMISSION_USER = flask_principal.Permission( - flask_principal.RoleNeed(ROLE_NAME_USER) -) -""" -The :py:class:`flask_principal.Permission` permission for regular users with *user* role. -""" - -PERMISSION_ANY = flask_principal.Permission( - flask_principal.RoleNeed(ROLE_NAME_ADMIN), - flask_principal.RoleNeed(ROLE_NAME_MAINTAINER), - flask_principal.RoleNeed(ROLE_NAME_DEVELOPER), - flask_principal.RoleNeed(ROLE_NAME_USER) -) -""" -The concatenated :py:class:`flask_principal.Permission` permission for any user role -(*admin*, *maintainer*, *developer* or *user*). -""" - -PERMISSIONS = { - ROLE_NAME_ADMIN: PERMISSION_ADMIN, - ROLE_NAME_MAINTAINER: PERMISSION_MAINTAINER, - ROLE_NAME_POWER: PERMISSION_POWER, - ROLE_NAME_DEVELOPER: PERMISSION_DEVELOPER, - ROLE_NAME_USER: PERMISSION_USER, - ROLE_NAME_ANY: PERMISSION_ANY -} -""" -Map for accessing permission objects by name. -""" diff --git a/lib/vial/app.py b/lib/vial/app.py deleted file mode 100644 index 0aeafc2981c9108c553d744c36a6bd4323f1745b..0000000000000000000000000000000000000000 --- a/lib/vial/app.py +++ /dev/null @@ -1,1038 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- -# Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- - - -""" -This module contains extended :py:class:`flask.Flask` and :py:class:`flask.Blueprint` -classes that form the new base of *Vial* application. -""" - - -import sys -import traceback -import datetime -import weakref -import json -import jinja2 - -import werkzeug.routing -import werkzeug.utils -import flask -import flask.app -import flask.views -import flask_babel -import flask_migrate -import flask_login -import flask_principal - -import vial.const -import vial.acl -import vial.log -import vial.mailer -import vial.intl -import vial.errors -import vial.utils -import vial.jsglue -import vial.view -import vial.menu -import vial.command - - -class VialException(Exception): - """ - Custom class for :py:class:`vial.app.Vial` application exceptions. - """ - - -class Vial(flask.Flask): # pylint: disable=locally-disabled,too-many-instance-attributes - """ - Custom implementation of :py:class:`flask.Flask` class. This class extends the - capabilities of the base class with following additional features: - - Configuration based blueprint registration - The application configuration file contains a directive describing list - of requested blueprints/modules, that should be registered into the - application. This enables administrator to very easily fine tune the - application setup for each installation. See the :py:func:`vial.app.Vial.register_blueprints` - for more information on the topic. - - Application main menu management - The application provides three distinct menus, that are at a disposal for - blueprint/module designer. - """ - - def __init__(self, import_name, **kwargs): - super().__init__(import_name, **kwargs) - - self.csrf = None - - self.mailer = None - - self.menu_main = vial.menu.Menu() - self.menu_auth = vial.menu.Menu() - self.menu_anon = vial.menu.Menu() - - self.sign_ins = {} - self.sign_ups = {} - self.resources = {} - self.infomailers = {} - - @property - def icons(self): - """ - Application icon registry. - """ - return self.config.get('ICONS') - - @flask.app.setupmethod - def add_url_rule(self, rule, endpoint = None, view_func = None, provide_automatic_options = None, **options): - """ - Reimplementation of :py:func:`flask.Flask.add_url_rule` method. This method - is capable of disabling selected application endpoints. Keep in mind, that - some URL rules (like application global 'static' endpoint) are created during - the :py:func:`flask.app.Flask.__init__` method and cannot be disabled, - because at that point the configuration of the application is not yet loaded. - """ - if self.config.get('DISABLED_ENDPOINTS', None) and self.config['DISABLED_ENDPOINTS'] and endpoint: - if endpoint in self.config['DISABLED_ENDPOINTS']: - self.logger.warning( # pylint: disable=locally-disabled,no-member - "Application endpoint '%s' is disabled by configuration.", - endpoint - ) - return - #self.logger.debug( # pylint: disable=locally-disabled,no-member - # "Registering URL route %s:%s:%s:%s", - # str(rule), - # str(endpoint), - # str(view_func), - # str(view_func.view_class) if hasattr(view_func, 'view_class') else '---none---', - #) - super().add_url_rule(rule, endpoint, view_func, provide_automatic_options, **options) - - def register_blueprint(self, blueprint, **options): - """ - Reimplementation of :py:func:`flask.Flask.register_blueprint` method. This - method will perform standart blueprint registration and on top of that will - perform following additional tasks: - - * Register blueprint into custom internal registry. The registry lies - within application`s ``config`` under key :py:const:`vial.const.CFGKEY_ENABLED_BLUEPRINTS`. - * Call blueprint`s ``register_app`` method, if available, with ``self`` as only argument. - - :param vial.app.VialBlueprint blueprint: Blueprint to be registered. - :param dict options: Additional options, will be passed down to :py:func:`flask.Flask.register_blueprint`. - """ - super().register_blueprint(blueprint, **options) - - if isinstance(blueprint, VialBlueprint): - if hasattr(blueprint, 'register_app'): - blueprint.register_app(self) - - self.sign_ins.update(blueprint.sign_ins) - self.sign_ups.update(blueprint.sign_ups) - - def register_blueprints(self): - """ - Register all configured application blueprints. The configuration comes - from :py:const:`vial.const.CFGKEY_ENABLED_BLUEPRINTS` configuration - subkey, which must contain list of string names of required blueprints. - The blueprint module must provide ``get_blueprint`` factory method, that - must return valid instance of :py:class:`vial.app.VialBlueprint`. This - method will call the :py:func:`vial.app.Vial.register_blueprint` for - each blueprint, that is being registered into the application. - - :raises vial.app.VialException: In case the factory method ``get_blueprint`` is not provided by loaded module. - """ - for name in self.config[vial.const.CFGKEY_ENABLED_BLUEPRINTS]: - self.logger.debug( # pylint: disable=locally-disabled,no-member - "Loading pluggable module %s", - name - ) - mod = werkzeug.utils.import_string(name) - if hasattr(mod, 'get_blueprint'): - self.register_blueprint(mod.get_blueprint()) - else: - raise VialException( - "Invalid blueprint module '{}', does not provide the 'get_blueprint' factory method.".format(name) - ) - - def log_exception(self, exc_info): - """ - Reimplementation of :py:func:`flask.Flask.log_exception` method. - """ - self.logger.error( # pylint: disable=locally-disabled,no-member - "Exception on %s [%s]" % (flask.request.full_path, flask.request.method), - exc_info = exc_info - ) - - def log_exception_with_label(self, tbexc, label = ''): - """ - Log given exception traceback into application logger. - """ - self.logger.error( # pylint: disable=locally-disabled,no-member - '%s%s', - label, - ''.join(tbexc.format()) - ) - - #-------------------------------------------------------------------------- - - def get_modules(self, filter_func = None): - """ - Get all currently registered application modules. - """ - if not filter_func: - return self.blueprints - return { - k: v for k, v in self.blueprints.items() if filter_func(k, v) - } - - def has_endpoint(self, endpoint): - """ - Check if given routing endpoint is available. - - :param str endpoint: Application routing endpoint. - :return: ``True`` in case endpoint exists, ``False`` otherwise. - :rtype: bool - """ - return endpoint in self.view_functions - - def get_endpoints(self, filter_func = None): - """ - Get all currently registered application endpoints. - """ - if not filter_func: - return { - k: v.view_class for k, v in self.view_functions.items() if hasattr(v, 'view_class') - } - return { - k: v.view_class for k, v in self.view_functions.items() if hasattr(v, 'view_class') and filter_func(k, v.view_class) - } - - def get_endpoint_class(self, endpoint, quiet = False): - """ - Get reference to view class registered to given routing endpoint. - - :param str endpoint: Application routing endpoint. - :param bool quiet: Suppress the exception in case given endpoint does not exist. - :return: Reference to view class. - :rtype: class - """ - if not endpoint in self.view_functions: - if quiet: - return None - raise VialException( - "Unknown endpoint name '{}'.".format(endpoint) - ) - try: - return self.view_functions[endpoint].view_class - except AttributeError: - return vial.view.DecoratedView(self.view_functions[endpoint]) - - def can_access_endpoint(self, endpoint, **kwargs): - """ - Check, that the current user can access given endpoint/view. - - :param str endpoint: Application routing endpoint. - :param dict kwargs: Optional endpoint parameters. - :return: ``True`` in case user can access the endpoint, ``False`` otherwise. - :rtype: bool - """ - try: - view_class = self.get_endpoint_class(endpoint) - - # Reject unauthenticated users in case view requires authentication. - if view_class.authentication: - if not flask_login.current_user.is_authenticated: - return False - - # Check view authorization rules. - if view_class.authorization: - for auth_rule in view_class.authorization: - if not auth_rule.can(): - return False - - # Check item action authorization callback, if exists. - if hasattr(view_class, 'authorize_item_action'): - if not view_class.authorize_item_action(**kwargs): - return False - - return True - - except VialException: - return False - - def get_model(self, name): - """ - Return reference to class of given model. - - :param str name: Name of the model. - """ - return self.config[vial.const.CFGKEY_MODELS][name] - - def get_resource(self, name): - """ - Return reference to given registered resource. - - :param str name: Name of the resource. - """ - return self.resources[name]() - - def set_resource(self, name, resource): - """ - Store reference to given resource. - - :param str name: Name of the resource. - :param resource: Resource to be registered. - """ - self.resources[name] = weakref.ref(resource) - - def set_infomailer(self, name, mailer): - """ - Register mailer handle to be usable by different web interface components. - - :param str name: Name of the informailer. - :param callable mailer: Mailer handle. - """ - self.infomailers.setdefault(name, []).append(mailer) - - def send_infomail(self, name, **kwargs): - """ - Send emails through all registered infomailer handles. - - :param str name: Name of the informailer. - :param **kwargs: Additional mailer arguments. - """ - for mailer in self.infomailers[name]: - mailer(**kwargs) - - #-------------------------------------------------------------------------- - - def setup_app(self): - """ - Perform setup of the whole application. - """ - self._setup_app_logging() - self._setup_app_mailer() - self._setup_app_core() - self._setup_app_db() - self._setup_app_auth() - self._setup_app_acl() - self._setup_app_intl() - self._setup_app_menu() - self._setup_app_blueprints() - self._setup_app_cli() - - def _setup_app_logging(self): - """ - Setup logging to file and via email for given Vial application. Logging - capabilities are adjustable by application configuration. - - :param vial.app.VialApp app: Vial application to be modified. - :return: Modified Vial application - :rtype: vial.app.VialApp - """ - vial.log.setup_logging_default(self) - vial.log.setup_logging_file(self) - if not self.debug: - vial.log.setup_logging_email(self) - - return self - - def _setup_app_mailer(self): - """ - Setup mailer service for Vial application. - - :param vial.app.VialApp app: Vial application to be modified. - :return: Modified Vial application - :rtype: vial.app.VialApp - """ - vial.mailer.MAILER.init_app(self) - self.mailer = vial.mailer.MAILER - - return self - - def _setup_app_core(self): - """ - Setup application core for given Vial application. The application core - contains following features: - - * Error handlers - * Default routes - * Additional custom Jinja template variables - * Additional custom Jinja template macros - - :param vial.app.VialApp app: Vial application to be modified. - :return: Modified Vial application - :rtype: vial.app.VialApp - """ - @self.errorhandler(400) - def eh_badrequest(err): # pylint: disable=locally-disabled,unused-variable - """Flask error handler to be called to service HTTP 400 error.""" - flask.current_app.logger.critical( - "BAD REQUEST\n\nRequest: %s\nTraceback:\n%s", - flask.request.full_path, - ''.join( - traceback.TracebackException( - *sys.exc_info() - ).format() - ) - ) - return vial.errors.error_handler_switch(400, err) - - @self.errorhandler(403) - def eh_forbidden(err): # pylint: disable=locally-disabled,unused-variable - """Flask error handler to be called to service HTTP 403 error.""" - return vial.errors.error_handler_switch(403, err) - - @self.errorhandler(404) - def eh_page_not_found(err): # pylint: disable=locally-disabled,unused-variable - """Flask error handler to be called to service HTTP 404 error.""" - return vial.errors.error_handler_switch(404, err) - - @self.errorhandler(405) - def eh_method_not_allowed(err): # pylint: disable=locally-disabled,unused-variable - """Flask error handler to be called to service HTTP 405 error.""" - return vial.errors.error_handler_switch(405, err) - - @self.errorhandler(410) - def eh_gone(err): # pylint: disable=locally-disabled,unused-variable - """Flask error handler to be called to service HTTP 410 error.""" - return vial.errors.error_handler_switch(410, err) - - @self.errorhandler(500) - def eh_internal_server_error(err): # pylint: disable=locally-disabled,unused-variable - """Flask error handler to be called to service HTTP 500 error.""" - flask.current_app.logger.critical( - "INTERNAL SERVER ERROR\n\nRequest: %s\nTraceback:\n%s", - flask.request.full_path, - ''.join( - traceback.TracebackException( - *sys.exc_info() - ).format() - ), - ) - return vial.errors.error_handler_switch(500, err) - - @self.before_request - def before_request(): # pylint: disable=locally-disabled,unused-variable - """ - Use Flask`s :py:func:`flask.Flask.before_request` hook for performing - various usefull tasks before each request. - """ - flask.g.requeststart = datetime.datetime.utcnow() - - @self.context_processor - def jinja_inject_variables(): # pylint: disable=locally-disabled,unused-variable - """ - Inject additional variables into Jinja2 global template namespace. - """ - return dict( - vial_appname = flask.current_app.config['APPLICATION_NAME'], - vial_appid = flask.current_app.config['APPLICATION_ID'], - vial_current_app = flask.current_app, - vial_current_menu_main = flask.current_app.menu_main, - vial_current_menu_auth = flask.current_app.menu_auth, - vial_current_menu_anon = flask.current_app.menu_anon, - vial_current_view = self.get_endpoint_class(flask.request.endpoint, True), - vial_logger = flask.current_app.logger, - vial_cdt_utc = datetime.datetime.utcnow(), - vial_cdt_local = datetime.datetime.now(), - ) - - @self.context_processor - def jinja2_inject_functions(): # pylint: disable=locally-disabled,unused-variable,too-many-locals - """ - Register additional helpers into Jinja2 global template namespace. - """ - def get_modules_dict(): - """ - Return dictionary of all registered application pluggable modules. - """ - return flask.current_app.blueprints - - def get_endpoints_dict(): - """ - Return dictionary of all registered application view endpoints. - """ - return { k: v.view_class for k, v in flask.current_app.view_functions.items() if hasattr(v, 'view_class') } - - def get_endpoint_class(endpoint, quiet = False): - """ - Return class reference to given view endpoint. - - :param str endpoint: Name of the view endpoint. - :param bool quiet: Suppress the exception in case given endpoint does not exist. - """ - return self.get_endpoint_class(endpoint, quiet) - - def check_endpoint_exists(endpoint): - """ - Check, that given application view endpoint exists and is registered within - the application. - - :param str endpoint: Name of the view endpoint. - :return: ``True`` in case endpoint exists, ``False`` otherwise. - :rtype: bool - """ - return endpoint in self.view_functions - - def get_icon(icon_name, default_icon = 'missing-icon'): - """ - Get HTML icon markup for given icon. - - :param str icon_name: Name of the icon. - :param str default_icon: Name of the default icon. - :return: Icon including HTML markup. - :rtype: flask.Markup - """ - return flask.Markup( - self.config.get('ICONS').get( - icon_name, - self.config.get('ICONS').get(default_icon) - ) - ) - - def get_module_icon(endpoint, default_icon = 'missing-icon'): - """ - Get HTML icon markup for parent module of given view endpoint. - - :param str endpoint: Name of the view endpoint. - :param str default_icon: Name of the default icon. - :return: Icon including HTML markup. - :rtype: flask.Markup - """ - return flask.Markup( - self.config.get('ICONS').get( - self.get_endpoint_class(endpoint).module_ref().get_module_icon(), - self.config.get('ICONS').get(default_icon) - ) - ) - - def get_endpoint_icon(endpoint, default_icon = 'missing-icon'): - """ - Get HTML icon markup for given view endpoint. - - :param str endpoint: Name of the view endpoint. - :return: Icon including HTML markup. - :rtype: flask.Markup - """ - return flask.Markup( - self.config.get('ICONS').get( - self.get_endpoint_class(endpoint).get_view_icon(), - self.config.get('ICONS').get(default_icon) - ) - ) - - def get_country_flag(country): - """ - Get URL to static country flag file. - - :param str country: Name of the icon. - :return: Country including HTML markup. - :rtype: flask.Markup - """ - if not vial.const.CRE_COUNTRY_CODE.match(country): - return get_icon('flag') - - return flask.Markup( - '<img src="{}">'.format( - flask.url_for( - 'static', - filename = 'images/country-flags/flags-iso/shiny/16/{}.png'.format( - country.upper() - ) - ) - ) - ) - - def include_raw(filename): - """ - Include given file in raw form directly into the generated content. - This may be usefull for example for including JavaScript files - directly into the HTML page. - """ - return jinja2.Markup( - self.jinja_loader.get_source(self.jinja_env, filename)[0] - ) - - return dict( - get_modules_dict = get_modules_dict, - get_endpoints_dict = get_endpoints_dict, - get_endpoint_class = get_endpoint_class, - check_endpoint_exists = check_endpoint_exists, - - get_icon = get_icon, - get_module_icon = get_module_icon, - get_endpoint_icon = get_endpoint_icon, - get_country_flag = get_country_flag, - - get_timedelta = vial.utils.get_timedelta, - get_datetime_utc = vial.utils.get_datetime_utc, - get_datetime_local = vial.utils.get_datetime_local, - parse_datetime = vial.utils.parse_datetime, - - get_datetime_window = vial.view.mixin.VialUtils.get_datetime_window, - - check_file_exists = vial.utils.check_file_exists, - - in_query_params = vial.utils.in_query_params, - generate_query_params = vial.utils.generate_query_params, - - current_datetime_utc = datetime.datetime.utcnow(), - - include_raw = include_raw, - json_to_yaml = vial.utils.json_to_yaml, - get_uuid4 = vial.utils.get_uuid4, - load_json_from_file = vial.utils.load_json_from_file, - make_copy_deep = vial.utils.make_copy_deep - ) - - @self.template_filter('tojson_pretty') - def to_pretty_json(value): - return json.dumps( - value, - sort_keys = True, - indent = 4 - ) - - class VialJSONEncoder(flask.json.JSONEncoder): - """ - Custom JSON encoder for converting anything into JSON strings. - """ - def default(self, obj): # pylint: disable=locally-disabled,method-hidden,arguments-differ - try: - if isinstance(obj, datetime.datetime): - return obj.isoformat() + 'Z' - except: # pylint: disable=locally-disabled,bare-except - pass - try: - return obj.to_dict() - except: # pylint: disable=locally-disabled,bare-except - pass - try: - return str(obj) - except: # pylint: disable=locally-disabled,bare-except - pass - return flask.json.JSONEncoder.default(self, obj) - - self.json_encoder = VialJSONEncoder - - @self.route('/app-main.js') - def mainjs(): # pylint: disable=locally-disabled,unused-variable - """ - Default route for main application JavaScript file. - """ - return flask.make_response( - flask.render_template('app-main.js'), - 200, - {'Content-Type': 'text/javascript'} - ) - - # Initialize JSGlue plugin for using `flask.url_for()` method in JavaScript. - #jsglue = flask_jsglue.JSGlue() - jsglue = vial.jsglue.JSGlue() - jsglue.init_app(self) - - @self.template_filter() - def pprint_item(item): # pylint: disable=locally-disabled,unused-variable - """ - Custom Jinja2 filter for full object attribute dump/pretty-print. - """ - res = [] - for key in dir(item): - res.append('%r: %r' % (key, getattr(item, key))) - return '\n'.join(res) - - return self - - def _setup_app_db(self): - """ - Setup application database service for given Vial application. - - :param vial.app.VialApp app: Vial application to be modified. - :return: Modified Vial application - :rtype: vial.app.VialApp - """ - dbh = vial.db.db_setup(**self.config['SQLALCHEMY_SETUP_ARGS']) - dbh.init_app(self) - - # Initialize database migration service and register it among the application - # resources for possible future use. - migrate = flask_migrate.Migrate( - app = self, - db = dbh, - directory = self.config['MIGRATE_DIRECTORY'] - ) - self.set_resource(vial.const.RESOURCE_MIGRATE, migrate) - - self.logger.debug( - "Connected to database via SQLAlchemy ({})".format( - self.config['SQLALCHEMY_DATABASE_URI'] - ) - ) - - return self - - - def _setup_app_auth(self): - """ - Setup application authentication features. - - :param vial.app.VialApp app: Vial application to be modified. - :return: Modified Vial application - :rtype: vial.app.VialApp - """ - - lim = flask_login.LoginManager() - lim.init_app(self) - lim.login_view = self.config['ENDPOINT_LOGIN'] - lim.login_message = flask_babel.gettext("Please log in to access this page.") - lim.login_message_category = self.config['LOGIN_MSGCAT'] - - self.set_resource(vial.const.RESOURCE_LOGIN_MANAGER, lim) - - @lim.user_loader - def load_user(user_id): # pylint: disable=locally-disabled,unused-variable - """ - Flask-Login callback for loading current user`s data. - """ - user_model = self.get_model(vial.const.MODEL_USER) - return vial.db.db_get().session.query(user_model).filter(user_model.id == user_id).one_or_none() - - @self.route('/logout') - @flask_login.login_required - def logout(): # pylint: disable=locally-disabled,unused-variable - """ - Flask-Login callback for logging out current user. - """ - flask.current_app.logger.info( - "User '{}' just logged out.".format( - str(flask_login.current_user) - ) - ) - flask_login.logout_user() - flask.flash( - flask_babel.gettext('You have been successfully logged out.'), - vial.const.FLASH_SUCCESS - ) - - # Remove session keys set by Flask-Principal. - for key in ('identity.name', 'identity.auth_type'): - flask.session.pop(key, None) - - # Tell Flask-Principal the identity changed. - flask_principal.identity_changed.send( - flask.current_app._get_current_object(), # pylint: disable=locally-disabled,protected-access - identity = flask_principal.AnonymousIdentity() - ) - - # Force user to index page. - return flask.redirect( - flask.url_for( - flask.current_app.config['ENDPOINT_LOGOUT_REDIRECT'] - ) - ) - - return self - - def _setup_app_acl(self): - """ - Setup application ACL features. - - :param vial.app.VialApp app: Vial application to be modified. - :return: Modified Vial application - :rtype: vial.app.VialApp - """ - fpp = flask_principal.Principal(self, skip_static = True) - self.set_resource(vial.const.RESOURCE_PRINCIPAL, fpp) - - @flask_principal.identity_loaded.connect_via(self) - def on_identity_loaded(sender, identity): # pylint: disable=locally-disabled,unused-variable,unused-argument - """ - Flask-Principal callback for populating user identity object after login. - """ - # Set the identity user object. - identity.user = flask_login.current_user - - if not flask_login.current_user.is_authenticated: - flask.current_app.logger.debug( - "Loaded ACL identity for anonymous user '{}'.".format( - str(flask_login.current_user) - ) - ) - return - flask.current_app.logger.debug( - "Loading ACL identity for user '{}'.".format( - str(flask_login.current_user) - ) - ) - - # Add the UserNeed to the identity. - if hasattr(flask_login.current_user, 'get_id'): - identity.provides.add( - flask_principal.UserNeed(flask_login.current_user.id) - ) - - # Assuming the User model has a list of roles, update the - # identity with the roles that the user provides. - if hasattr(flask_login.current_user, 'roles'): - for role in flask_login.current_user.roles: - identity.provides.add( - flask_principal.RoleNeed(role) - ) - - # Assuming the User model has a list of group memberships, update the - # identity with the groups that the user is member of. - if hasattr(flask_login.current_user, 'memberships'): - for group in flask_login.current_user.memberships: - identity.provides.add( - vial.acl.MembershipNeed(group.id) - ) - - # Assuming the User model has a list of group managements, update the - # identity with the groups that the user is manager of. - if hasattr(flask_login.current_user, 'managements'): - for group in flask_login.current_user.managements: - identity.provides.add( - vial.acl.ManagementNeed(group.id) - ) - - @self.context_processor - def utility_acl_processor(): # pylint: disable=locally-disabled,unused-variable - """ - Register additional helpers related to authorization into Jinja global - namespace to enable them within the templates. - """ - def can_access_endpoint(endpoint, item = None): - """ - Check if currently logged-in user can access given endpoint/view. - - :param str endpoint: Name of the application endpoint. - :param item: Optional item for additional validations. - :return: ``True`` in case user can access the endpoint, ``False`` otherwise. - :rtype: bool - """ - return flask.current_app.can_access_endpoint(endpoint, item = item) - - def permission_can(permission_name): - """ - Manually check currently logged-in user for given permission. - - :param str permission_name: Name of the permission. - :return: Check result. - :rtype: bool - """ - return vial.acl.PERMISSIONS[permission_name].can() - - def is_it_me(item): - """ - Check if given user account is mine. - """ - return item.id == flask_login.current_user.id - - return dict( - can_access_endpoint = can_access_endpoint, - permission_can = permission_can, - is_it_me = is_it_me - ) - - return self - - def _setup_app_intl(self): - """ - Setup application`s internationalization sybsystem. - - :param vial.app.VialApp app: Vial application to be modified. - :return: Modified Vial application - :rtype: vial.app.VialApp - """ - vial.intl.BABEL.init_app(self) - self.set_resource(vial.const.RESOURCE_BABEL, vial.intl.BABEL) - - @self.route('/locale/<code>') - def locale(code): # pylint: disable=locally-disabled,unused-variable - """ - Application route providing users with the option of changing locale. - """ - if code not in flask.current_app.config['SUPPORTED_LOCALES']: - return flask.abort(404) - - if flask_login.current_user.is_authenticated: - flask_login.current_user.locale = code - # Make sure current user is in SQLAlchemy session. Turns out, this - # step is not necessary and current user is already in session, - # because it was fetched from database few moments ago. - #vial.db.db_session().add(flask_login.current_user) - vial.db.db_session().commit() - - flask.session['locale'] = code - flask_babel.refresh() - - flask.flash( - flask.Markup(flask_babel.gettext( - 'Locale was succesfully changed to <strong>%(lcln)s (%(lclc)s)</strong>.', - lclc = code, - lcln = flask.current_app.config['SUPPORTED_LOCALES'][code] - )), - vial.const.FLASH_SUCCESS - ) - - # Redirect user back to original page. - return flask.redirect( - vial.forms.get_redirect_target( - default_url = flask.url_for( - flask.current_app.config['ENDPOINT_HOME'] - ) - ) - ) - - @self.before_request - def before_request(): # pylint: disable=locally-disabled,unused-variable - """ - Use Flask`s :py:func:`flask.Flask.before_request` hook for storing - currently selected locale and timezone to request`s session storage. - """ - if 'locale' not in flask.session: - flask.session['locale'] = vial.intl.get_locale() - if 'timezone' not in flask.session: - flask.session['timezone'] = vial.intl.get_timezone() - - @self.context_processor - def utility_processor(): # pylint: disable=locally-disabled,unused-variable - """ - Register additional internationalization helpers into Jinja global namespace. - """ - - return dict( - babel_get_locale = vial.intl.get_locale, - babel_get_timezone = vial.intl.get_timezone, - babel_format_datetime = flask_babel.format_datetime, - babel_format_date = flask_babel.format_date, - babel_format_time = flask_babel.format_time, - babel_format_timedelta = flask_babel.format_timedelta, - babel_format_decimal = flask_babel.format_decimal, - babel_format_percent = flask_babel.format_percent, - babel_format_bytes = vial.intl.babel_format_bytes, - babel_translate_locale = vial.intl.babel_translate_locale, - babel_language_in_locale = vial.intl.babel_language_in_locale - ) - - return self - - def _setup_app_menu(self): - """ - Setup default application menu skeleton. - - :param vial.app.VialApp app: Vial application to be modified. - :return: Modified Vial application - :rtype: vial.app.VialApp - """ - for entry in self.config[vial.const.CFGKEY_MENU_MAIN_SKELETON]: - self.menu_main.add_entry(**entry) - - return self - - def _setup_app_blueprints(self): - """ - Setup application blueprints. - - :param vial.app.VialApp app: Vial application to be modified. - :return: Modified Vial application - :rtype: vial.app.VialApp - """ - self.register_blueprints() - - return self - - def _setup_app_cli(app): - """ - Setup application command line interface. - - :param vial.app.VialApp app: Vial application to be modified. - :return: Modified Vial application - :rtype: vial.app.VialApp - """ - vial.command.setup_cli(app) - - return app - - -class VialBlueprint(flask.Blueprint): - """ - Custom implementation of :py:class:`flask.Blueprint` class. This class extends - the capabilities of the base class with additional features: - - * Support for better integration into application and registration of view classes. - * Support for custom tweaking of application object. - * Support for custom style of authentication and authorization decorators - """ - def __init__(self, name, import_name, **kwargs): - super().__init__(name, import_name, **kwargs) - - self.sign_ins = {} - self.sign_ups = {} - - @classmethod - def get_module_title(cls): - """ - Get human readable name for this blueprint/module. - - :return: Name (short summary) of the blueprint/module. - :rtype: str - """ - raise NotImplementedError() - - def get_module_icon(self): - """ - Return icon name for the module. Given name will be used as index to - built-in icon registry. - - :return: Icon for the module. - :rtype: str - """ - return 'module-{}'.format(self.name).replace('_', '-') - - def register_app(self, app): # pylint: disable=locally-disabled,no-self-use,unused-argument - """ - *Hook method:* Custom callback, which will be called from - :py:func:`vial.app.Vial.register_blueprint` method and which can - perform additional tweaking of Vial application object. - - :param vial.app.Vial app: Application object. - """ - return - - def register_view_class(self, view_class, route_spec): - """ - Register given view class into the internal blueprint registry. - - :param vial.view.BaseView view_class: View class (not instance!) - :param str route_spec: Routing information for the view. - """ - view_class.module_ref = weakref.ref(self) - view_class.module_name = self.name - - # Obtain view function. - view_func = view_class.as_view(view_class.get_view_name()) - - # Apply authorization decorators (if requested). - if view_class.authorization: - for auth in view_class.authorization: - view_func = auth.require(403)(view_func) - - # Apply authentication decorators (if requested). - if view_class.authentication: - view_func = flask_login.login_required(view_func) - - # Register endpoint to the application. - self.add_url_rule(route_spec, view_func = view_func) - - # Register SIGN IN and SIGN UP views to enable further special handling. - if hasattr(view_class, 'is_sign_in') and view_class.is_sign_in: - self.sign_ins[view_class.get_view_endpoint()] = view_class - if hasattr(view_class, 'is_sign_up') and view_class.is_sign_up: - self.sign_ups[view_class.get_view_endpoint()] = view_class diff --git a/lib/vial/blueprints/__init__.py b/lib/vial/blueprints/__init__.py deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/lib/vial/blueprints/auth/templates/registration/email_admins.txt b/lib/vial/blueprints/auth/templates/registration/email_admins.txt deleted file mode 100644 index 5a58a233916601c9ee387da660ed227aaba30f82..0000000000000000000000000000000000000000 --- a/lib/vial/blueprints/auth/templates/registration/email_admins.txt +++ /dev/null @@ -1,26 +0,0 @@ -{{ _('Dear administrator,') | wordwrap }} - -{{ _('a new account "%(item_id)s" was just registered in %(app_name)s. Please review the following request and activate or delete the account:', item_id = account.login, app_name = vial_appname) | wordwrap }} - - {{ '{:16s}'.format(_('Login:')) }} {{ account.login }} - {{ '{:16s}'.format(_('Full name:')) }} {{ account.fullname }} - {{ '{:16s}'.format(_('Email:')) }} {{ account.email }} -{%- if account.memberships_wanted %} - -{{ _('User has requested membership in following groups:') | wordwrap }} -{%- for group in account.memberships_wanted %} - * {{ group.name }} -{%- endfor %} -{%- endif %} - -{{ _('User has provided following justification to be given access to the system:') | wordwrap }} - -{{ justification | wordwrap(width=75, break_long_words=False) | indent(width=4, first=True) }} - -{{ _('Account details can be found here:') | wordwrap }} - - {{ url_for('users.show', _external = True, item_id = account.id ) }} - -{{ _('Have a nice day') | wordwrap }} - --- {{ vial_appname }} diff --git a/lib/vial/blueprints/auth/templates/registration/email_managers.txt b/lib/vial/blueprints/auth/templates/registration/email_managers.txt deleted file mode 100644 index 6cc8bf534bb6a22f32d2ff3fb5c64d808c064f14..0000000000000000000000000000000000000000 --- a/lib/vial/blueprints/auth/templates/registration/email_managers.txt +++ /dev/null @@ -1,19 +0,0 @@ -{{ _('Dear group manager,') | wordwrap }} - -{{ _('a new account "%(item_id)s" was just registered in %(app_name)s and user requested membership in your group "%(group_id)s". Please review the following information and approve or reject the request:', item_id = account.login, app_name = vial_appname, group_id = group.name) | wordwrap }} - - {{ '{:16s}'.format(_('Login:')) }} {{ account.login }} - {{ '{:16s}'.format(_('Full name:')) }} {{ account.fullname }} - {{ '{:16s}'.format(_('Email:')) }} {{ account.email }} - -{{ _('User has provided following justification to be given access to the system:') | wordwrap }} - -{{ justification | wordwrap(width=75, break_long_words=False) | indent(width=4, first=True) }} - -{{ _('Management page for your group can be found here:') | wordwrap }} - - {{ url_for('groups.show', _external = True, item_id = group.id ) }} - -{{ _('Have a nice day') | wordwrap }} - --- {{ vial_appname }} diff --git a/lib/vial/blueprints/auth/templates/registration/email_user.txt b/lib/vial/blueprints/auth/templates/registration/email_user.txt deleted file mode 100644 index a3f2d82c948afb81c0bcc7c5e85c8fab05589f90..0000000000000000000000000000000000000000 --- a/lib/vial/blueprints/auth/templates/registration/email_user.txt +++ /dev/null @@ -1,30 +0,0 @@ -{{ _('Dear user,') | wordwrap }} - -{{ _('this email is a confirmation, that you have successfully registered your new user account "%(item_id)s" in %(app_name)s.', item_id = account.login, app_name = vial_appname) | wordwrap }} - -{{ _('During the registration process you have provided following information:') | wordwrap }} - - {{ '{:16s}'.format(_('Login:')) }} {{ account.login }} - {{ '{:16s}'.format(_('Full name:')) }} {{ account.fullname }} - {{ '{:16s}'.format(_('Email:')) }} {{ account.email }} -{%- if account.memberships_wanted %} - -{{ _('You have requested membership in following groups:') | wordwrap }} -{%- for group in account.memberships_wanted %} - * {{ group.name }} -{%- endfor %} -{%- endif %} - -{{ _('You have provided following justification to be given access to the system:') | wordwrap }} - -{{ justification | wordwrap(width=75, break_long_words=False) | indent(width=4, first=True) }} - -{{ _('Administrator was informed about registration of a new account. You will receive email confirmation when your account will be activated.') | wordwrap }} - -{{ _('After successfull activation you will be able to login and start using the system:') | wordwrap }} - - {{ url_for('auth.login', _external = True ) }} - -{{ _('Have a nice day') | wordwrap }} - --- {{ vial_appname }} diff --git a/lib/vial/blueprints/auth/test/__init__.py b/lib/vial/blueprints/auth/test/__init__.py deleted file mode 100644 index 7c8035150c923ab751519e87b549928d0e52d29d..0000000000000000000000000000000000000000 --- a/lib/vial/blueprints/auth/test/__init__.py +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- -# Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- - - -""" -Unit tests for :py:mod:`vial.blueprints.auth`. -""" - - -import sys -import unittest - -from vial.test import VialTestCase -from vial.test.runner import TestRunnerMixin - - -_IS_NOSE = sys.argv[0].endswith('nosetests') - -@unittest.skipIf(_IS_NOSE, "broken under nosetest") -class AuthTestCase(TestRunnerMixin, VialTestCase): - """ - Class for testing :py:mod:`vial.blueprints.auth` blueprint. - """ - - def test_01_login(self): - """ - Test login directional page. - """ - response = self.client.get( - '/auth/login', - follow_redirects = True - ) - self.assertEqual(response.status_code, 200) - self.assertTrue(b'Following is a list of all available user login options. Please choose the one appropriate for you.' in response.data) - - def test_02_register(self): - """ - Test registration directional page. - """ - response = self.client.get( - '/auth/register', - follow_redirects = True - ) - self.assertEqual(response.status_code, 200) - self.assertTrue(b'Following is a list of all available user account registration options. Please choose the one most suitable for your needs.' in response.data) - - -#------------------------------------------------------------------------------- - - -if __name__ == "__main__": - unittest.main() diff --git a/lib/vial/blueprints/auth_dev/__init__.py b/lib/vial/blueprints/auth_dev/__init__.py deleted file mode 100644 index c8b39d23396c5d5d04d9094eb3db48ce23f94b5d..0000000000000000000000000000000000000000 --- a/lib/vial/blueprints/auth_dev/__init__.py +++ /dev/null @@ -1,158 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- -# Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- - - -""" -This Hawat pluggable module provides special authentication method, that is -particularly usable for developers and enables them to impersonate any user. - -After enabling this module special authentication endpoint will be available -and will provide simple authentication form with list of all currently available -user accounts. It will be possible for that user to log in as any other user -without entering password. - -This module is disabled by default in *production* environment and enabled by -default in *development* environment. - -.. warning:: - - This module must never ever be enabled on production systems, because it is - a huge security risk and enables possible access control management violation. - You have been warned! - - -Provided endpoints --------------------------------------------------------------------------------- - -``/auth_dev/login`` - Page providing special developer login form. - - * *Authentication:* no authentication - * *Methods:* ``GET``, ``POST`` -""" - - -import flask -from flask_babel import lazy_gettext - -import vial.const -import vial.forms -import vial.db -from vial.app import VialBlueprint -from vial.view import BaseLoginView, BaseRegisterView -from vial.view.mixin import HTMLMixin, SQLAlchemyMixin - -from vial.blueprints.auth_dev.forms import LoginForm, RegisterUserAccountForm - - -BLUEPRINT_NAME = 'auth_dev' -"""Name of the blueprint as module global constant.""" - - -class LoginView(HTMLMixin, SQLAlchemyMixin, BaseLoginView): - """ - View enabling special developer login. - """ - methods = ['GET', 'POST'] - - @classmethod - def get_view_title(cls, **kwargs): - return lazy_gettext('Developer login') - - @classmethod - def get_menu_title(cls, **kwargs): - return lazy_gettext('Login (dev)') - - @property - def dbmodel(self): - return self.get_model(vial.const.MODEL_USER) - - @property - def search_by(self): - return self.dbmodel.login - - def get_user_login(self): - form = LoginForm() - self.response_context.update( - form = form - ) - if form.validate_on_submit(): - return form.login.data - return None - - -class RegisterView(HTMLMixin, SQLAlchemyMixin, BaseRegisterView): - """ - View responsible for registering new user account into application. - """ - methods = ['GET', 'POST'] - - @classmethod - def get_menu_title(cls, **kwargs): - return lazy_gettext('Register (dev)') - - @classmethod - def get_view_title(cls, **kwargs): - return lazy_gettext('User account registration (dev)') - - @property - def dbmodel(self): - return self.get_model(vial.const.MODEL_USER) - - @property - def dbchlogmodel(self): - return self.get_model(vial.const.MODEL_ITEM_CHANGELOG) - - @staticmethod - def get_item_form(item): - roles = list( - zip( - flask.current_app.config['ROLES'], - flask.current_app.config['ROLES'] - ) - ) - locales = list( - flask.current_app.config['SUPPORTED_LOCALES'].items() - ) - - return RegisterUserAccountForm( - choices_roles = roles, - choices_locales = locales - ) - - -#------------------------------------------------------------------------------- - - -class DevAuthBlueprint(VialBlueprint): - """Pluggable module - developer authentication service (*auth_dev*).""" - - @classmethod - def get_module_title(cls): - return lazy_gettext('Developer authentication service') - - -#------------------------------------------------------------------------------- - - -def get_blueprint(): - """ - Mandatory interface for :py:mod:`vial.Vial` and factory function. This function - must return a valid instance of :py:class:`vial.app.VialBlueprint` or - :py:class:`flask.Blueprint`. - """ - - hbp = DevAuthBlueprint( - BLUEPRINT_NAME, - __name__, - template_folder = 'templates', - url_prefix = '/{}'.format(BLUEPRINT_NAME) - ) - - hbp.register_view_class(LoginView, '/login') - hbp.register_view_class(RegisterView, '/register') - - return hbp diff --git a/lib/vial/blueprints/auth_dev/forms.py b/lib/vial/blueprints/auth_dev/forms.py deleted file mode 100644 index ed55ef4dcebb382d124cefe822ce7599540309f5..0000000000000000000000000000000000000000 --- a/lib/vial/blueprints/auth_dev/forms.py +++ /dev/null @@ -1,86 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- -# Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- - - -""" -This module contains custom developer login form for Hawat. -""" - - -import wtforms -from wtforms.ext.sqlalchemy.fields import QuerySelectMultipleField -import flask -import flask_wtf -from flask_babel import lazy_gettext - -import vial.forms -import vial.db -from vial.forms import check_login, check_unique_login, get_available_groups -from vial.blueprints.users.forms import BaseUserAccountForm - - -class LoginForm(flask_wtf.FlaskForm): - """ - Class representing developer authentication login form. This form provides - list of all currently existing user accounts in simple selectbox, so that - the developer can quickly login as different user. - """ - login = wtforms.SelectField( - lazy_gettext('User account:'), - validators = [ - wtforms.validators.DataRequired(), - check_login - ] - ) - submit = wtforms.SubmitField( - lazy_gettext('Login') - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - self.set_choices() - - def set_choices(self): - """ - Load list of all user accounts and populate the ``choices`` attribute of - the ``login`` selectbox. - """ - dbsess = vial.db.db_get().session - user_model = flask.current_app.get_model(vial.const.MODEL_USER) - users = dbsess.query(user_model).order_by(user_model.login).all() - - choices = [] - for usr in users: - choices.append((usr.login, "{} ({}, #{})".format(usr.fullname, usr.login, usr.id))) - choices = sorted(choices, key=lambda x: x[1]) - self.login.choices = choices - - -class RegisterUserAccountForm(BaseUserAccountForm): - """ - Class representing user account registration form. - """ - login = wtforms.StringField( - lazy_gettext('Login:'), - validators = [ - wtforms.validators.DataRequired(), - wtforms.validators.Length(min = 3, max = 50), - check_login, - check_unique_login - ] - ) - memberships_wanted = QuerySelectMultipleField( - lazy_gettext('Requested group memberships:'), - query_factory = get_available_groups - ) - justification = wtforms.TextAreaField( - lazy_gettext('Justification:'), - validators = [ - wtforms.validators.DataRequired(), - wtforms.validators.Length(min = 10, max = 500) - ] - ) diff --git a/lib/vial/blueprints/auth_dev/templates/auth_dev/login.html b/lib/vial/blueprints/auth_dev/templates/auth_dev/login.html deleted file mode 100644 index fa5ea415333cdace094a9ad35e665773b37a6d3b..0000000000000000000000000000000000000000 --- a/lib/vial/blueprints/auth_dev/templates/auth_dev/login.html +++ /dev/null @@ -1,5 +0,0 @@ -{%- extends "_layout_login.html" %} - -{%- block loginformfields %} - {{ macros_form.render_form_item_select(form.login) }} -{%- endblock loginformfields %} diff --git a/lib/vial/blueprints/auth_dev/templates/auth_dev/registration.html b/lib/vial/blueprints/auth_dev/templates/auth_dev/registration.html deleted file mode 100644 index 021406e954ba5ff6d9f79cf72e41751a07b336ca..0000000000000000000000000000000000000000 --- a/lib/vial/blueprints/auth_dev/templates/auth_dev/registration.html +++ /dev/null @@ -1,19 +0,0 @@ -{%- extends "_layout_registration.html" %} - -{%- block registrationformfields %} - {{ macros_form.render_form_item_default(form.login) }} - {{ macros_form.render_form_item_default(form.fullname) }} - {{ macros_form.render_form_item_default(form.email) }} - {{ macros_form.render_form_item_default(form.justification) }} - - <hr> - - {{ macros_form.render_form_item_select(form.memberships_wanted) }} - - <hr> - - {{ macros_form.render_form_item_select(form.locale) }} - {{ macros_form.render_form_item_select(form.timezone) }} - - <hr> -{%- endblock registrationformfields %} diff --git a/lib/vial/blueprints/auth_dev/templates/registration/email_managers.txt b/lib/vial/blueprints/auth_dev/templates/registration/email_managers.txt deleted file mode 100644 index 6cc8bf534bb6a22f32d2ff3fb5c64d808c064f14..0000000000000000000000000000000000000000 --- a/lib/vial/blueprints/auth_dev/templates/registration/email_managers.txt +++ /dev/null @@ -1,19 +0,0 @@ -{{ _('Dear group manager,') | wordwrap }} - -{{ _('a new account "%(item_id)s" was just registered in %(app_name)s and user requested membership in your group "%(group_id)s". Please review the following information and approve or reject the request:', item_id = account.login, app_name = vial_appname, group_id = group.name) | wordwrap }} - - {{ '{:16s}'.format(_('Login:')) }} {{ account.login }} - {{ '{:16s}'.format(_('Full name:')) }} {{ account.fullname }} - {{ '{:16s}'.format(_('Email:')) }} {{ account.email }} - -{{ _('User has provided following justification to be given access to the system:') | wordwrap }} - -{{ justification | wordwrap(width=75, break_long_words=False) | indent(width=4, first=True) }} - -{{ _('Management page for your group can be found here:') | wordwrap }} - - {{ url_for('groups.show', _external = True, item_id = group.id ) }} - -{{ _('Have a nice day') | wordwrap }} - --- {{ vial_appname }} diff --git a/lib/vial/blueprints/auth_dev/templates/registration/email_user.txt b/lib/vial/blueprints/auth_dev/templates/registration/email_user.txt deleted file mode 100644 index a3f2d82c948afb81c0bcc7c5e85c8fab05589f90..0000000000000000000000000000000000000000 --- a/lib/vial/blueprints/auth_dev/templates/registration/email_user.txt +++ /dev/null @@ -1,30 +0,0 @@ -{{ _('Dear user,') | wordwrap }} - -{{ _('this email is a confirmation, that you have successfully registered your new user account "%(item_id)s" in %(app_name)s.', item_id = account.login, app_name = vial_appname) | wordwrap }} - -{{ _('During the registration process you have provided following information:') | wordwrap }} - - {{ '{:16s}'.format(_('Login:')) }} {{ account.login }} - {{ '{:16s}'.format(_('Full name:')) }} {{ account.fullname }} - {{ '{:16s}'.format(_('Email:')) }} {{ account.email }} -{%- if account.memberships_wanted %} - -{{ _('You have requested membership in following groups:') | wordwrap }} -{%- for group in account.memberships_wanted %} - * {{ group.name }} -{%- endfor %} -{%- endif %} - -{{ _('You have provided following justification to be given access to the system:') | wordwrap }} - -{{ justification | wordwrap(width=75, break_long_words=False) | indent(width=4, first=True) }} - -{{ _('Administrator was informed about registration of a new account. You will receive email confirmation when your account will be activated.') | wordwrap }} - -{{ _('After successfull activation you will be able to login and start using the system:') | wordwrap }} - - {{ url_for('auth.login', _external = True ) }} - -{{ _('Have a nice day') | wordwrap }} - --- {{ vial_appname }} diff --git a/lib/vial/blueprints/auth_dev/test/__init__.py b/lib/vial/blueprints/auth_dev/test/__init__.py deleted file mode 100644 index 664827884909f0752a47692f8beb52c74e7d106d..0000000000000000000000000000000000000000 --- a/lib/vial/blueprints/auth_dev/test/__init__.py +++ /dev/null @@ -1,159 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- -# Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- - - -""" -Unit tests for :py:mod:`vial.blueprints.auth_dev`. -""" - - -import sys -import unittest - -import vial.const -import vial.test -import vial.db -from vial.test import RegistrationVialTestCase -from vial.test.runner import TestRunnerMixin - - -_IS_NOSE = sys.argv[0].endswith('nosetests') - -@unittest.skipIf(_IS_NOSE, "broken under nosetest") -class AuthDevTestCase(TestRunnerMixin, RegistrationVialTestCase): - """ - Class for testing :py:mod:`vial.blueprints.auth_dev` blueprint. - """ - - def test_01_login_user(self): - """ - Test login/logout with *auth_dev* module - user 'user'. - """ - response = self.login_dev(vial.const.ROLE_USER) - self.assertEqual(response.status_code, 200) - self.assertTrue(b'You have been successfully logged in as' in response.data) - - response = self.logout() - self.assertEqual(response.status_code, 200) - self.assertTrue(b'You have been successfully logged out' in response.data) - - def test_02_login_developer(self): - """ - Test login/logout with *auth_dev* module - user 'developer'. - """ - response = self.login_dev(vial.const.ROLE_DEVELOPER) - self.assertEqual(response.status_code, 200) - self.assertTrue(b'You have been successfully logged in as' in response.data) - - response = self.logout() - self.assertEqual(response.status_code, 200) - self.assertTrue(b'You have been successfully logged out' in response.data) - - def test_03_login_admin(self): - """ - Test login/logout with *auth_dev* module - user 'admin'. - """ - response = self.login_dev(vial.const.ROLE_ADMIN) - self.assertEqual(response.status_code, 200) - self.assertTrue(b'You have been successfully logged in as' in response.data) - - response = self.logout() - self.assertEqual(response.status_code, 200) - self.assertTrue(b'You have been successfully logged out' in response.data) - - def test_04_register(self): - """ - Test registration with *auth_dev* module - new user 'test'. - """ - self.assertRegister( - '/auth_dev/register', - [ - ('submit', 'Register'), - ('login', 'test'), - ('fullname', 'Test User'), - ('email', 'test.user@domain.org'), - ('justification', 'I really want in.') - ], - { - 'txt': [ - 'Dear administrator,\n' - '\n' - 'a new account "test" was just registered in Vial. Please review the ' - 'following\n' - 'request and activate or delete the account:\n' - '\n' - ' Login: test\n' - ' Full name: Test User\n' - ' Email: test.user@domain.org\n' - '\n' - 'User has provided following justification to be given access to the system:\n' - '\n' - ' I really want in.\n' - '\n' - 'Account details can be found here:\n' - '\n' - ' http://localhost/users/5/show\n' - '\n' - 'Have a nice day\n' - '\n' - '-- Vial', - 'Dear user,\n' - '\n' - 'this email is a confirmation, that you have successfully registered your ' - 'new\n' - 'user account "test" in Vial.\n' - '\n' - 'During the registration process you have provided following information:\n' - '\n' - ' Login: test\n' - ' Full name: Test User\n' - ' Email: test.user@domain.org\n' - '\n' - 'You have provided following justification to be given access to the system:\n' - '\n' - ' I really want in.\n' - '\n' - 'Administrator was informed about registration of a new account. You will\n' - 'receive email confirmation when your account will be activated.\n' - '\n' - 'After successfull activation you will be able to login and start using the\n' - 'system:\n' - '\n' - '\thttp://localhost/auth/login\n' - '\n' - 'Have a nice day\n' - '\n' - '-- Vial' - ], - 'html': [ - None, - None - ] - } - ) - - def test_05_register_fail(self): - """ - Test registration with *auth_dev* module - existing user 'user'. - """ - self.assertRegisterFail( - '/auth_dev/register', - [ - ('submit', 'Register'), - ('login', 'user'), - ('fullname', 'Demo User'), - ('email', 'demo.user@domain.org'), - ('organization', 'TEST, org.'), - ('justification', 'I really want in.') - ] - ) - - -#------------------------------------------------------------------------------- - - -if __name__ == "__main__": - unittest.main() diff --git a/lib/vial/blueprints/auth_env/__init__.py b/lib/vial/blueprints/auth_env/__init__.py deleted file mode 100644 index 2c440a7f10dc17d6758e2490b6cb55bfc1da4165..0000000000000000000000000000000000000000 --- a/lib/vial/blueprints/auth_env/__init__.py +++ /dev/null @@ -1,290 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- -# Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- - - -""" -This pluggable module provides default authentication service based on server -environment. In this case the burden of performing actual authentication is -on the web server used for serving the web interface. The authentication module -then simply uses selected environment variables set up by the server after -successfull authentication. - -This module also provides interface for automated user account registration. The -registration form is pre-filled with data gathered again from server environment. -The login may not be changed and the value fetched from environment is always used. -Other account attributes like name or email address may be tweaked by user before -submitting the registration form. Administrator and user are both notified via -email about the fact new account was just created. - - -Environment variables --------------------------------------------------------------------------------- - -Currently following environment variables set up by the HTTP server are supported: - -``eppn``,``REMOTE_USER`` (*MANDATORY*) - The ``eppn`` server variable is set up by the _shibd_ daemon implementing the - Shibboleth SSO service. The ``REMOTE_USER`` variable is set up by many - authentication providers. This environment variable is of course mandatory - and it is used as an account username (login). - -``cn``,``givenName``,``sn`` (*OPTIONAL*) - The ``cn`` server variable is used to fill in user`s name, when available. - When not available, user`s name is constructed as contatenation of ``givenName`` - and ``sn`` server variables. When none of the above is available, user has to - input his/her name manually during registration process. - -``perunPreferredMail``,``mail`` (*OPTIONAL*) - The ``perunPreferredMail`` server variable is used to fill in user`s email - address, when available. When not available, the first email address from - ``email`` server variable is used. When none of the above is available, user - has to input his/her email manually during registration process. - -``perunOrganizationName``,``o`` (*OPTIONAL*) - The ``perunOrganizationName`` server variable is used to fill in user`s home - organization name, when available. When not available, the value of ``o`` - server variable is used. When none of the above is available, user - has to input his/her home organization name manually during registration process. - - -Provided endpoints --------------------------------------------------------------------------------- - -``/auth_env/login`` - Page providing login functionality via server set environment variables. - - * *Authentication:* no authentication - * *Methods:* ``GET`` - -``/auth_env/register`` - User account registration using server set environment variables. - - * *Authentication:* no authentication - * *Methods:* ``GET``, ``POST`` -""" - - -import flask -from flask_babel import gettext, lazy_gettext - -import vial.const -import vial.forms -import vial.db -from vial.app import VialBlueprint -from vial.view import BaseLoginView, BaseRegisterView -from vial.view.mixin import HTMLMixin, SQLAlchemyMixin - -from vial.blueprints.auth_env.forms import RegisterUserAccountForm - - -BLUEPRINT_NAME = 'auth_env' -"""Name of the blueprint as module global constant.""" - - -class RegistrationException(Exception): - """ - Exception describing problems with new user account registration. - """ - def __init__(self, description): - super().__init__() - self.description = description - - def __str__(self): - return str(self.description) - - -def get_login_from_environment(): - """ - Get user account login from appropriate environment variable(s). - """ - return flask.request.environ.get( - 'eppn', - flask.request.environ.get('REMOTE_USER', None) - ) - -class LoginView(HTMLMixin, SQLAlchemyMixin, BaseLoginView): - """ - View responsible for user login via application environment. - """ - methods = ['GET'] - - @classmethod - def get_view_title(cls, **kwargs): - return lazy_gettext('Environment login') - - @classmethod - def get_menu_title(cls, **kwargs): - return lazy_gettext('Login (env)') - - @property - def dbmodel(self): - return self.get_model(vial.const.MODEL_USER) - - @property - def search_by(self): - return self.dbmodel.login - - def get_user_login(self): - user_login = get_login_from_environment() - if not user_login: - self.flash( - gettext('User login was not received, unable to perform login process.'), - vial.const.FLASH_FAILURE - ) - self.abort(403) - return user_login - - -class RegisterView(HTMLMixin, SQLAlchemyMixin, BaseRegisterView): - """ - View responsible for registering new user account into application. - """ - methods = ['GET', 'POST'] - - @classmethod - def get_menu_title(cls, **kwargs): - return lazy_gettext('Register (env)') - - @classmethod - def get_view_title(cls, **kwargs): - return lazy_gettext('User account registration (env)') - - @property - def dbmodel(self): - return self.get_model(vial.const.MODEL_USER) - - @property - def dbchlogmodel(self): - return self.get_model(vial.const.MODEL_ITEM_CHANGELOG) - - def get_user_from_env(self): - """ - Get user object populated with information gathered from server environment - variables. - """ - item = self.dbmodel() - - # Fetch login from server authentication headers (mandatory). - item.login = get_login_from_environment() - if not item.login: - raise RegistrationException( - gettext("Unable to retrieve account login from your authentication provider.") - ) - - # Try to fetch name from server authentication headers (optional). - while True: - try: - item.fullname = flask.request.environ['cn'] - break - except (KeyError, AttributeError): - pass - try: - item.fullname = '{} {}'.format( - flask.request.environ['givenName'], - flask.request.environ['sn'] - ) - break - except (KeyError, AttributeError): - pass - break - - # Try to fetch email from server authentication headers (optional). - while True: - try: - item.email = flask.request.environ['perunPreferredMail'] - break - except (KeyError, AttributeError): - pass - try: - item.email = flask.request.environ['mail'].split(';')[0] - break - except (KeyError, AttributeError): - pass - break - - # Try to fetch organization from server authentication headers (optional). - while True: - try: - item.organization = flask.request.environ['perunOrganizationName'] - break - except (KeyError, AttributeError): - pass - try: - item.organization = flask.request.environ['o'] - break - except (KeyError, AttributeError): - pass - break - - return item - - def get_item(self): - # Attempt to create user object from server environment variables. - try: - return self.get_user_from_env() - except RegistrationException as exc: - self.abort(500, exc) - - @staticmethod - def get_item_form(item): - locales = list( - flask.current_app.config['SUPPORTED_LOCALES'].items() - ) - - return RegisterUserAccountForm( - obj = item, - choices_locales = locales - ) - - - def dispatch_request(self): - """ - Mandatory interface required by the :py:func:`flask.views.View.dispatch_request`. - Will be called by the *Flask* framework to service the request. - """ - self.response_context.update( - apacheenv = flask.request.environ - ) - return super().dispatch_request() - - -#------------------------------------------------------------------------------- - - -class EnvAuthBlueprint(VialBlueprint): - """Pluggable module - environment authentication service (*auth_env*).""" - - @classmethod - def get_module_title(cls): - return lazy_gettext('Environment authentication service') - - def register_app(self, app): - app.set_infomailer('auth_env.register', RegisterView.inform_admins) - app.set_infomailer('auth_env.register', RegisterView.inform_managers) - app.set_infomailer('auth_env.register', RegisterView.inform_user) - - -#------------------------------------------------------------------------------- - - -def get_blueprint(): - """ - Mandatory interface for :py:mod:`vial.Vial` and factory function. This function - must return a valid instance of :py:class:`vial.app.VialBlueprint` or - :py:class:`flask.Blueprint`. - """ - - hbp = EnvAuthBlueprint( - BLUEPRINT_NAME, - __name__, - template_folder = 'templates', - url_prefix = '/{}'.format(BLUEPRINT_NAME) - ) - - hbp.register_view_class(LoginView, '/login') - hbp.register_view_class(RegisterView, '/register') - - return hbp diff --git a/lib/vial/blueprints/auth_env/forms.py b/lib/vial/blueprints/auth_env/forms.py deleted file mode 100644 index 8b3610ec8b18245722078b9954f8a8a3e5746bde..0000000000000000000000000000000000000000 --- a/lib/vial/blueprints/auth_env/forms.py +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- -# Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- - - -""" -This module contains custom user account registration form for Hawat. -""" - - -import wtforms -from wtforms.ext.sqlalchemy.fields import QuerySelectMultipleField - -from flask_babel import lazy_gettext - -from vial.forms import get_available_groups - -from vial.blueprints.users.forms import BaseUserAccountForm - - -class RegisterUserAccountForm(BaseUserAccountForm): - """ - Class representing user account registration form. - """ - memberships_wanted = QuerySelectMultipleField( - lazy_gettext('Requested group memberships:'), - query_factory = get_available_groups - ) - justification = wtforms.TextAreaField( - lazy_gettext('Justification:'), - validators = [ - wtforms.validators.DataRequired(), - wtforms.validators.Length(min = 10, max = 500) - ] - ) diff --git a/lib/vial/blueprints/auth_env/templates/auth_env/registration.html b/lib/vial/blueprints/auth_env/templates/auth_env/registration.html deleted file mode 100644 index e6f9ac35be0cf37a6d7afe8c1a8a0a66c9c2c929..0000000000000000000000000000000000000000 --- a/lib/vial/blueprints/auth_env/templates/auth_env/registration.html +++ /dev/null @@ -1,26 +0,0 @@ -{%- extends "_layout_registration.html" %} - -{%- block registrationformfields %} - {{ macros_form.render_form_item_static(_('Login:'), item.login) }} - {{ macros_form.render_form_item_default(form.fullname) }} - {{ macros_form.render_form_item_default(form.email) }} - {{ macros_form.render_form_item_default(form.justification) }} - - <hr> - - {{ macros_form.render_form_item_select(form.memberships_wanted) }} - - <hr> - - {{ macros_form.render_form_item_select(form.locale) }} - {{ macros_form.render_form_item_select(form.timezone) }} - - <hr> - -{%- if permission_can('developer') %} - <pre class="pre-scrollable"> -{{ apacheenv | pprint }} - </pre> - <hr> - {%- endif -%} -{%- endblock registrationformfields %} diff --git a/lib/vial/blueprints/auth_env/templates/auth_env/registration/email_admins.txt b/lib/vial/blueprints/auth_env/templates/auth_env/registration/email_admins.txt deleted file mode 100644 index 5a58a233916601c9ee387da660ed227aaba30f82..0000000000000000000000000000000000000000 --- a/lib/vial/blueprints/auth_env/templates/auth_env/registration/email_admins.txt +++ /dev/null @@ -1,26 +0,0 @@ -{{ _('Dear administrator,') | wordwrap }} - -{{ _('a new account "%(item_id)s" was just registered in %(app_name)s. Please review the following request and activate or delete the account:', item_id = account.login, app_name = vial_appname) | wordwrap }} - - {{ '{:16s}'.format(_('Login:')) }} {{ account.login }} - {{ '{:16s}'.format(_('Full name:')) }} {{ account.fullname }} - {{ '{:16s}'.format(_('Email:')) }} {{ account.email }} -{%- if account.memberships_wanted %} - -{{ _('User has requested membership in following groups:') | wordwrap }} -{%- for group in account.memberships_wanted %} - * {{ group.name }} -{%- endfor %} -{%- endif %} - -{{ _('User has provided following justification to be given access to the system:') | wordwrap }} - -{{ justification | wordwrap(width=75, break_long_words=False) | indent(width=4, first=True) }} - -{{ _('Account details can be found here:') | wordwrap }} - - {{ url_for('users.show', _external = True, item_id = account.id ) }} - -{{ _('Have a nice day') | wordwrap }} - --- {{ vial_appname }} diff --git a/lib/vial/blueprints/auth_env/test/__init__.py b/lib/vial/blueprints/auth_env/test/__init__.py deleted file mode 100644 index e5d8df8196c66e596bc9855cb7d4058bc57d3cd9..0000000000000000000000000000000000000000 --- a/lib/vial/blueprints/auth_env/test/__init__.py +++ /dev/null @@ -1,188 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- -# Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- - - -""" -Unit tests for :py:mod:`vial.blueprints.auth_env`. -""" - -import sys -import unittest - -import vial.const -import vial.test -import vial.db -from vial.test import RegistrationVialTestCase -from vial.test.runner import TestRunnerMixin - - -_IS_NOSE = sys.argv[0].endswith('nosetests') - -@unittest.skipIf(_IS_NOSE, "broken under nosetest") -class AuthEnvTestCase(TestRunnerMixin, RegistrationVialTestCase): - """ - Class for testing :py:mod:`vial.blueprints.auth_env` blueprint. - """ - - def test_01_login_user(self): - """ - Test login/logout with *auth_env* module - user 'user'. - """ - response = self.login_env(vial.const.ROLE_USER) - self.assertEqual(response.status_code, 200) - self.assertTrue(b'You have been successfully logged in as' in response.data) - - response = self.logout() - self.assertEqual(response.status_code, 200) - self.assertTrue(b'You have been successfully logged out' in response.data) - - response = self.login_env(vial.const.ROLE_USER, 'REMOTE_USER') - self.assertEqual(response.status_code, 200) - self.assertTrue(b'You have been successfully logged in as' in response.data) - - response = self.logout() - self.assertEqual(response.status_code, 200) - self.assertTrue(b'You have been successfully logged out' in response.data) - - def test_02_login_developer(self): - """ - Test login/logout with *auth_env* module - user 'developer'. - """ - response = self.login_env(vial.const.ROLE_DEVELOPER) - self.assertEqual(response.status_code, 200) - self.assertTrue(b'You have been successfully logged in as' in response.data) - - response = self.logout() - self.assertEqual(response.status_code, 200) - self.assertTrue(b'You have been successfully logged out' in response.data) - - response = self.login_env(vial.const.ROLE_DEVELOPER, 'REMOTE_USER') - self.assertEqual(response.status_code, 200) - self.assertTrue(b'You have been successfully logged in as' in response.data) - - response = self.logout() - self.assertEqual(response.status_code, 200) - self.assertTrue(b'You have been successfully logged out' in response.data) - - def test_03_login_admin(self): - """ - Test login/logout with *auth_env* module - user 'admin'. - """ - response = self.login_env(vial.const.ROLE_ADMIN) - self.assertEqual(response.status_code, 200) - self.assertTrue(b'You have been successfully logged in as' in response.data) - - response = self.logout() - self.assertEqual(response.status_code, 200) - self.assertTrue(b'You have been successfully logged out' in response.data) - - response = self.login_env(vial.const.ROLE_ADMIN, 'REMOTE_USER') - self.assertEqual(response.status_code, 200) - self.assertTrue(b'You have been successfully logged in as' in response.data) - - response = self.logout() - self.assertEqual(response.status_code, 200) - self.assertTrue(b'You have been successfully logged out' in response.data) - - def test_04_register(self): - """ - Test registration with *auth_env* module - new user 'test'. - """ - self.assertRegister( - '/auth_env/register', - [ - ('submit', 'Register'), - ('justification', 'I really want in.') - ], - { - 'txt': [ - 'Dear administrator,\n' - '\n' - 'a new account "test" was just registered in Vial. Please review the ' - 'following\n' - 'request and activate or delete the account:\n' - '\n' - ' Login: test\n' - ' Full name: Test User\n' - ' Email: test.user@domain.org\n' - '\n' - 'User has provided following justification to be given access to the system:\n' - '\n' - ' I really want in.\n' - '\n' - 'Account details can be found here:\n' - '\n' - ' http://localhost/users/5/show\n' - '\n' - 'Have a nice day\n' - '\n' - '-- Vial', - 'Dear user,\n' - '\n' - 'this email is a confirmation, that you have successfully registered your ' - 'new\n' - 'user account "test" in Vial.\n' - '\n' - 'During the registration process you have provided following information:\n' - '\n' - ' Login: test\n' - ' Full name: Test User\n' - ' Email: test.user@domain.org\n' - '\n' - 'You have provided following justification to be given access to the system:\n' - '\n' - ' I really want in.\n' - '\n' - 'Administrator was informed about registration of a new account. You will\n' - 'receive email confirmation when your account will be activated.\n' - '\n' - 'After successfull activation you will be able to login and start using the\n' - 'system:\n' - '\n' - '\thttp://localhost/auth/login\n' - '\n' - 'Have a nice day\n' - '\n' - '-- Vial' - ], - 'html': [ - None, - None - ] - }, - { - 'eppn': 'test', - 'cn': 'Test User', - 'givenName': 'Test', - 'sn': 'User', - 'perunPreferredMail': 'test.user@domain.org', - } - ) - - def test_05_register_fail(self): - """ - Test registration with *auth_env* module - existing user 'user'. - """ - self.assertRegisterFail( - '/auth_env/register', - [ - ('submit', 'Register'), - ('organization', 'TEST, org.'), - ('justification', 'I really want in.') - ], - { - 'eppn': 'user', - 'cn': 'Test User', - 'perunPreferredMail': 'test.user@domain.org', - } - ) - - -#------------------------------------------------------------------------------- - - -if __name__ == "__main__": - unittest.main() diff --git a/lib/vial/blueprints/auth_pwd/__init__.py b/lib/vial/blueprints/auth_pwd/__init__.py deleted file mode 100644 index 580d290e1401ce3eb0d706882f90aeb54549512b..0000000000000000000000000000000000000000 --- a/lib/vial/blueprints/auth_pwd/__init__.py +++ /dev/null @@ -1,148 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- -# Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- - - -""" -This pluggable module provides classical web login form with password authentication -method. - - -Provided endpoints --------------------------------------------------------------------------------- - -``/auth_pwd/login`` - Page providing classical web login form. - - * *Authentication:* no authentication - * *Methods:* ``GET``, ``POST`` -""" - - -import flask -from flask_babel import lazy_gettext - -import vial.const -import vial.forms -from vial.app import VialBlueprint -from vial.view import BaseLoginView, BaseRegisterView -from vial.view.mixin import HTMLMixin, SQLAlchemyMixin - -from vial.blueprints.auth_pwd.forms import LoginForm, RegisterUserAccountForm - - -BLUEPRINT_NAME = 'auth_pwd' -"""Name of the blueprint as module global constant.""" - - -class LoginView(HTMLMixin, SQLAlchemyMixin, BaseLoginView): - """ - View enabling classical password login. - """ - methods = ['GET', 'POST'] - - @classmethod - def get_view_title(cls, **kwargs): - return lazy_gettext('Password login') - - @classmethod - def get_menu_title(cls, **kwargs): - return lazy_gettext('Login (pwd)') - - @property - def dbmodel(self): - return self.get_model(vial.const.MODEL_USER) - - @property - def search_by(self): - return self.dbmodel.login - - def get_user_login(self): - form = LoginForm() - self.response_context.update( - form = form - ) - if form.validate_on_submit(): - return form.login.data - return None - - def authenticate_user(self, user): - return user.check_password( - self.response_context['form'].password.data - ) - - -class RegisterView(HTMLMixin, SQLAlchemyMixin, BaseRegisterView): - """ - View enabling classical password login. - """ - methods = ['GET', 'POST'] - - @classmethod - def get_menu_title(cls, **kwargs): - return lazy_gettext('Register (pwd)') - - @classmethod - def get_view_title(cls, **kwargs): - return lazy_gettext('User account registration (pwd)') - - @property - def dbmodel(self): - return self.get_model(vial.const.MODEL_USER) - - @property - def dbchlogmodel(self): - return self.get_model(vial.const.MODEL_ITEM_CHANGELOG) - - @property - def search_by(self): - return self.dbmodel.login - - @staticmethod - def get_item_form(item): - locales = list( - flask.current_app.config['SUPPORTED_LOCALES'].items() - ) - return RegisterUserAccountForm( - choices_locales = locales - ) - - def do_before_action(self, item): # pylint: disable=locally-disabled,no-self-use,unused-argument - super().do_before_action(item) - item.set_password(item.password) - - -#------------------------------------------------------------------------------- - - -class PwdAuthBlueprint(VialBlueprint): - """Pluggable module - classical authentication service (*auth_pwd*).""" - - @classmethod - def get_module_title(cls): - return lazy_gettext('Password authentication service') - - -#------------------------------------------------------------------------------- - - -def get_blueprint(): - """ - Mandatory interface for :py:mod:`vial.Vial` and factory function. This function - must return a valid instance of :py:class:`vial.app.VialBlueprint` or - :py:class:`flask.Blueprint`. - """ - - hbp = PwdAuthBlueprint( - BLUEPRINT_NAME, - __name__, - template_folder = 'templates', - url_prefix = '/{}'.format(BLUEPRINT_NAME) - ) - - hbp.register_view_class(LoginView, '/login') - hbp.register_view_class(RegisterView, '/register') - - return hbp diff --git a/lib/vial/blueprints/auth_pwd/forms.py b/lib/vial/blueprints/auth_pwd/forms.py deleted file mode 100644 index c588b75baa6799c826e846c3bdbf8baca1d5deaf..0000000000000000000000000000000000000000 --- a/lib/vial/blueprints/auth_pwd/forms.py +++ /dev/null @@ -1,89 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- -# This file is part of MyDojo package (https://github.com/honzamach/mydojo). -# -# Copyright (C) since 2018 Honza Mach <honza.mach.ml@gmail.com> -# Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- - - -""" -This module contains custom developer login form for Hawat. -""" - - -__author__ = "Honza Mach <honza.mach.ml@gmail.com>" - - -import wtforms -import flask_wtf -from wtforms.ext.sqlalchemy.fields import QuerySelectMultipleField -from flask_babel import lazy_gettext - -from vial.forms import check_login, check_unique_login, get_available_groups -from vial.blueprints.users.forms import BaseUserAccountForm - - -class LoginForm(flask_wtf.FlaskForm): - """ - Class representing classical password authentication login form. - """ - login = wtforms.StringField( - lazy_gettext('Login:'), - validators = [ - wtforms.validators.DataRequired(), - wtforms.validators.Length(min = 3, max = 50), - check_login - ] - ) - password = wtforms.PasswordField( - lazy_gettext('Password:'), - validators = [ - wtforms.validators.DataRequired(), - wtforms.validators.Length(min = 8), - ] - ) - submit = wtforms.SubmitField( - lazy_gettext('Login') - ) - - -class RegisterUserAccountForm(BaseUserAccountForm): - """ - Class representing classical account registration form. - """ - login = wtforms.StringField( - lazy_gettext('Login:'), - validators = [ - wtforms.validators.DataRequired(), - wtforms.validators.Length(min = 3, max = 50), - check_login, - check_unique_login - ] - ) - memberships_wanted = QuerySelectMultipleField( - lazy_gettext('Requested group memberships:'), - query_factory = get_available_groups - ) - justification = wtforms.TextAreaField( - lazy_gettext('Justification:'), - validators = [ - wtforms.validators.DataRequired(), - wtforms.validators.Length(min = 10, max = 500) - ] - ) - password = wtforms.PasswordField( - lazy_gettext('Password:'), - validators = [ - wtforms.validators.DataRequired(), - wtforms.validators.Length(min = 8), - ] - ) - password2 = wtforms.PasswordField( - lazy_gettext('Repeat Password:'), - validators = [ - wtforms.validators.DataRequired(), - wtforms.validators.EqualTo('password'), - ] - ) diff --git a/lib/vial/blueprints/auth_pwd/templates/auth_pwd/login.html b/lib/vial/blueprints/auth_pwd/templates/auth_pwd/login.html deleted file mode 100644 index ba68267a6d4a12b55ca3b509c15ac35416ba962b..0000000000000000000000000000000000000000 --- a/lib/vial/blueprints/auth_pwd/templates/auth_pwd/login.html +++ /dev/null @@ -1,6 +0,0 @@ -{%- extends "_layout_login.html" %} - -{%- block loginformfields %} - {{ macros_form.render_form_item_default(form.login) }} - {{ macros_form.render_form_item_default(form.password) }} -{%- endblock loginformfields %} diff --git a/lib/vial/blueprints/auth_pwd/templates/auth_pwd/registration.html b/lib/vial/blueprints/auth_pwd/templates/auth_pwd/registration.html deleted file mode 100644 index 18bad8fe9106f5dd1b37648156e54abbb559f0de..0000000000000000000000000000000000000000 --- a/lib/vial/blueprints/auth_pwd/templates/auth_pwd/registration.html +++ /dev/null @@ -1,24 +0,0 @@ -{%- extends "_layout_registration.html" %} - -{%- block registrationformfields %} - {{ macros_form.render_form_item_default(form.login) }} - {{ macros_form.render_form_item_default(form.fullname) }} - {{ macros_form.render_form_item_default(form.email) }} - {{ macros_form.render_form_item_default(form.justification) }} - - <hr> - - {{ macros_form.render_form_item_select(form.memberships_wanted) }} - - <hr> - - {{ macros_form.render_form_item_default(form.password) }} - {{ macros_form.render_form_item_default(form.password2) }} - - <hr> - - {{ macros_form.render_form_item_default(form.locale) }} - {{ macros_form.render_form_item_default(form.timezone) }} - - <hr> -{%- endblock registrationformfields %} diff --git a/lib/vial/blueprints/auth_pwd/templates/registration/email_admins.txt b/lib/vial/blueprints/auth_pwd/templates/registration/email_admins.txt deleted file mode 100644 index 5a58a233916601c9ee387da660ed227aaba30f82..0000000000000000000000000000000000000000 --- a/lib/vial/blueprints/auth_pwd/templates/registration/email_admins.txt +++ /dev/null @@ -1,26 +0,0 @@ -{{ _('Dear administrator,') | wordwrap }} - -{{ _('a new account "%(item_id)s" was just registered in %(app_name)s. Please review the following request and activate or delete the account:', item_id = account.login, app_name = vial_appname) | wordwrap }} - - {{ '{:16s}'.format(_('Login:')) }} {{ account.login }} - {{ '{:16s}'.format(_('Full name:')) }} {{ account.fullname }} - {{ '{:16s}'.format(_('Email:')) }} {{ account.email }} -{%- if account.memberships_wanted %} - -{{ _('User has requested membership in following groups:') | wordwrap }} -{%- for group in account.memberships_wanted %} - * {{ group.name }} -{%- endfor %} -{%- endif %} - -{{ _('User has provided following justification to be given access to the system:') | wordwrap }} - -{{ justification | wordwrap(width=75, break_long_words=False) | indent(width=4, first=True) }} - -{{ _('Account details can be found here:') | wordwrap }} - - {{ url_for('users.show', _external = True, item_id = account.id ) }} - -{{ _('Have a nice day') | wordwrap }} - --- {{ vial_appname }} diff --git a/lib/vial/blueprints/auth_pwd/templates/registration/email_managers.txt b/lib/vial/blueprints/auth_pwd/templates/registration/email_managers.txt deleted file mode 100644 index 6cc8bf534bb6a22f32d2ff3fb5c64d808c064f14..0000000000000000000000000000000000000000 --- a/lib/vial/blueprints/auth_pwd/templates/registration/email_managers.txt +++ /dev/null @@ -1,19 +0,0 @@ -{{ _('Dear group manager,') | wordwrap }} - -{{ _('a new account "%(item_id)s" was just registered in %(app_name)s and user requested membership in your group "%(group_id)s". Please review the following information and approve or reject the request:', item_id = account.login, app_name = vial_appname, group_id = group.name) | wordwrap }} - - {{ '{:16s}'.format(_('Login:')) }} {{ account.login }} - {{ '{:16s}'.format(_('Full name:')) }} {{ account.fullname }} - {{ '{:16s}'.format(_('Email:')) }} {{ account.email }} - -{{ _('User has provided following justification to be given access to the system:') | wordwrap }} - -{{ justification | wordwrap(width=75, break_long_words=False) | indent(width=4, first=True) }} - -{{ _('Management page for your group can be found here:') | wordwrap }} - - {{ url_for('groups.show', _external = True, item_id = group.id ) }} - -{{ _('Have a nice day') | wordwrap }} - --- {{ vial_appname }} diff --git a/lib/vial/blueprints/auth_pwd/templates/registration/email_user.txt b/lib/vial/blueprints/auth_pwd/templates/registration/email_user.txt deleted file mode 100644 index a3f2d82c948afb81c0bcc7c5e85c8fab05589f90..0000000000000000000000000000000000000000 --- a/lib/vial/blueprints/auth_pwd/templates/registration/email_user.txt +++ /dev/null @@ -1,30 +0,0 @@ -{{ _('Dear user,') | wordwrap }} - -{{ _('this email is a confirmation, that you have successfully registered your new user account "%(item_id)s" in %(app_name)s.', item_id = account.login, app_name = vial_appname) | wordwrap }} - -{{ _('During the registration process you have provided following information:') | wordwrap }} - - {{ '{:16s}'.format(_('Login:')) }} {{ account.login }} - {{ '{:16s}'.format(_('Full name:')) }} {{ account.fullname }} - {{ '{:16s}'.format(_('Email:')) }} {{ account.email }} -{%- if account.memberships_wanted %} - -{{ _('You have requested membership in following groups:') | wordwrap }} -{%- for group in account.memberships_wanted %} - * {{ group.name }} -{%- endfor %} -{%- endif %} - -{{ _('You have provided following justification to be given access to the system:') | wordwrap }} - -{{ justification | wordwrap(width=75, break_long_words=False) | indent(width=4, first=True) }} - -{{ _('Administrator was informed about registration of a new account. You will receive email confirmation when your account will be activated.') | wordwrap }} - -{{ _('After successfull activation you will be able to login and start using the system:') | wordwrap }} - - {{ url_for('auth.login', _external = True ) }} - -{{ _('Have a nice day') | wordwrap }} - --- {{ vial_appname }} diff --git a/lib/vial/blueprints/auth_pwd/test/__init__.py b/lib/vial/blueprints/auth_pwd/test/__init__.py deleted file mode 100644 index 5d110e2b2bd7f4b733255846e3a8b4c024e1f730..0000000000000000000000000000000000000000 --- a/lib/vial/blueprints/auth_pwd/test/__init__.py +++ /dev/null @@ -1,176 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- -# Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- - - -""" -Unit tests for :py:mod:`vial.blueprints.auth_pwd`. -""" - -import sys -import unittest - -import vial.const -import vial.test -import vial.db -from vial.test import RegistrationVialTestCase -from vial.test.runner import TestRunnerMixin - - -_IS_NOSE = sys.argv[0].endswith('nosetests') - -@unittest.skipIf(_IS_NOSE, "broken under nosetest") -class AuthPwdTestCase(TestRunnerMixin, RegistrationVialTestCase): - """ - Class for testing :py:mod:`vial.blueprints.auth_pwd` blueprint. - """ - - def test_01_login_user(self): - """ - Test login/logout with *auth_pwd* module - user 'user'. - """ - with self.app.app_context(): - user = self.user_get(vial.const.ROLE_USER) - user.set_password('password') - self.user_save(user) - - response = self.login_pwd(vial.const.ROLE_USER, 'password') - self.assertEqual(response.status_code, 200) - self.assertTrue(b'You have been successfully logged in as' in response.data) - - response = self.logout() - self.assertEqual(response.status_code, 200) - self.assertTrue(b'You have been successfully logged out' in response.data) - - def test_02_login_developer(self): - """ - Test login/logout with *auth_pwd* module - user 'developer'. - """ - with self.app.app_context(): - user = self.user_get(vial.const.ROLE_DEVELOPER) - user.set_password('password') - self.user_save(user) - - response = self.login_pwd(vial.const.ROLE_DEVELOPER, 'password') - self.assertEqual(response.status_code, 200) - self.assertTrue(b'You have been successfully logged in as' in response.data) - - response = self.logout() - self.assertEqual(response.status_code, 200) - self.assertTrue(b'You have been successfully logged out' in response.data) - - def test_03_login_admin(self): - """ - Test login/logout with *auth_pwd* module - user 'admin'. - """ - with self.app.app_context(): - user = self.user_get(vial.const.ROLE_ADMIN) - user.set_password('password') - self.user_save(user) - - response = self.login_pwd(vial.const.ROLE_ADMIN, 'password') - self.assertEqual(response.status_code, 200) - self.assertTrue(b'You have been successfully logged in as' in response.data) - - response = self.logout() - self.assertEqual(response.status_code, 200) - self.assertTrue(b'You have been successfully logged out' in response.data) - - def test_04_register(self): - """ - Test registration with *auth_pwd* module - new user 'test'. - """ - self.assertRegister( - '/auth_pwd/register', - [ - ('submit', 'Register'), - ('login', 'test'), - ('fullname', 'Test User'), - ('email', 'test.user@domain.org'), - ('justification', 'I really want in.'), - ('password', 'password'), - ('password2', 'password'), - ], - { - 'txt': [ - 'Dear administrator,\n' - '\n' - 'a new account "test" was just registered in Vial. Please review the ' - 'following\n' - 'request and activate or delete the account:\n' - '\n' - ' Login: test\n' - ' Full name: Test User\n' - ' Email: test.user@domain.org\n' - '\n' - 'User has provided following justification to be given access to the system:\n' - '\n' - ' I really want in.\n' - '\n' - 'Account details can be found here:\n' - '\n' - ' http://localhost/users/5/show\n' - '\n' - 'Have a nice day\n' - '\n' - '-- Vial', - 'Dear user,\n' - '\n' - 'this email is a confirmation, that you have successfully registered your ' - 'new\n' - 'user account "test" in Vial.\n' - '\n' - 'During the registration process you have provided following information:\n' - '\n' - ' Login: test\n' - ' Full name: Test User\n' - ' Email: test.user@domain.org\n' - '\n' - 'You have provided following justification to be given access to the system:\n' - '\n' - ' I really want in.\n' - '\n' - 'Administrator was informed about registration of a new account. You will\n' - 'receive email confirmation when your account will be activated.\n' - '\n' - 'After successfull activation you will be able to login and start using the\n' - 'system:\n' - '\n' - '\thttp://localhost/auth/login\n' - '\n' - 'Have a nice day\n' - '\n' - '-- Vial' - ], - 'html': [ - None, - None - ] - } - ) - - def test_05_register_fail(self): - """ - Test registration with *auth_pwd* module - existing user 'user'. - """ - self.assertRegisterFail( - '/auth_pwd/register', - [ - ('submit', 'Register'), - ('login', 'user'), - ('fullname', 'Demo User'), - ('email', 'demo.user@domain.org'), - ('justification', 'I really want in.'), - ('password', 'password'), - ('password2', 'password'), - ] - ) - - -#------------------------------------------------------------------------------- - - -if __name__ == "__main__": - unittest.main() diff --git a/lib/vial/blueprints/changelogs/test/__init__.py b/lib/vial/blueprints/changelogs/test/__init__.py deleted file mode 100644 index b32ddb9542a2a3b698ed3e3f90c9b84c69f6817c..0000000000000000000000000000000000000000 --- a/lib/vial/blueprints/changelogs/test/__init__.py +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- -# Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- - - -""" -Unit tests for :py:mod:`vial.blueprints.changelogs`. -""" - - -import sys -import unittest - -import vial.const -import vial.test -import vial.db -from vial.test import VialTestCase -from vial.test.runner import TestRunnerMixin - - -_IS_NOSE = sys.argv[0].endswith('nosetests') - -@unittest.skipIf(_IS_NOSE, "broken under nosetest") -class ChangeLogsSearchTestCase(TestRunnerMixin, VialTestCase): - """Class for testing ``changelogs.search`` endpoint.""" - - def _attempt_fail(self): - self.assertGetURL( - '/changelogs/search', - 403 - ) - - def _attempt_succeed(self): - self.assertGetURL( - '/changelogs/search', - 200 - ) - - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) - def test_01_as_user(self): - """Test access as user ``user``.""" - self._attempt_fail() - - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) - def test_02_as_developer(self): - """Test access as user ``developer``.""" - self._attempt_fail() - - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) - def test_03_as_maintainer(self): - """Test access as user ``maintainer``.""" - self._attempt_succeed() - - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) - def test_04_as_admin(self): - """Test access as user ``admin``.""" - self._attempt_succeed() - - -#------------------------------------------------------------------------------- - - -if __name__ == "__main__": - unittest.main() diff --git a/lib/vial/blueprints/groups/__init__.py b/lib/vial/blueprints/groups/__init__.py deleted file mode 100644 index b7d31eb2c624a0cdd7d5f87c7aafbc0d3095d31a..0000000000000000000000000000000000000000 --- a/lib/vial/blueprints/groups/__init__.py +++ /dev/null @@ -1,1108 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- -# Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- - - -""" -This file contains pluggable module for Vial application containing features -related to user group management. These features include: - -* general group listing -* detailed group view -* creating new groups -* updating existing groups -* deleting existing groups -* enabling existing groups -* disabling existing groups -* adding group members -* removing group members -* rejecting group membership requests -""" - - -import flask -import flask_login -import flask_principal -from flask_babel import gettext, lazy_gettext - -from sqlalchemy import and_, or_ - -import vial.acl -import vial.menu -from vial.app import VialBlueprint -from vial.view import ItemListView, ItemShowView, ItemCreateView, ItemUpdateView, ItemDeleteView, ItemEnableView, ItemDisableView, ItemObjectRelationView -from vial.view.mixin import HTMLMixin, SQLAlchemyMixin -from vial.blueprints.groups.forms import AdminCreateGroupForm, AdminUpdateGroupForm, UpdateGroupForm, GroupSearchForm - - -BLUEPRINT_NAME = 'groups' -"""Name of the blueprint as module global constant.""" - - -class ListView(HTMLMixin, SQLAlchemyMixin, ItemListView): - """ - General group listing. - """ - - methods = ['GET'] - - authentication = True - - authorization = [vial.acl.PERMISSION_POWER] - - @classmethod - def get_view_title(cls, **kwargs): - return lazy_gettext('Group management') - - @property - def dbmodel(self): - return self.get_model(vial.const.MODEL_GROUP) - - @classmethod - def get_action_menu(cls): - action_menu = vial.menu.Menu() - action_menu.add_entry( - 'endpoint', - 'create', - endpoint = 'groups.create', - resptitle = True - ) - return action_menu - - @classmethod - def get_context_action_menu(cls): - action_menu = vial.menu.Menu() - action_menu.add_entry( - 'endpoint', - 'show', - endpoint = 'groups.show', - hidetitle = True - ) - action_menu.add_entry( - 'endpoint', - 'update', - endpoint = 'groups.update', - hidetitle = True - ) - action_menu.add_entry( - 'endpoint', - 'disable', - endpoint = 'groups.disable', - hidetitle = True - ) - action_menu.add_entry( - 'endpoint', - 'enable', - endpoint = 'groups.enable', - hidetitle = True - ) - action_menu.add_entry( - 'endpoint', - 'delete', - endpoint = 'groups.delete', - hidetitle = True - ) - return action_menu - - @staticmethod - def get_search_form(request_args): - """ - Must return instance of :py:mod:`flask_wtf.FlaskForm` appropriate for - searching given type of items. - """ - return GroupSearchForm( - request_args, - meta = {'csrf': False} - ) - - @staticmethod - def build_query(query, model, form_args): - # Adjust query based on text search string. - if 'search' in form_args and form_args['search']: - query = query\ - .filter( - or_( - model.name.like('%{}%'.format(form_args['search'])), - model.description.like('%{}%'.format(form_args['search'])), - ) - ) - # Adjust query based on lower time boudary selection. - if 'dt_from' in form_args and form_args['dt_from']: - query = query.filter(model.createtime >= form_args['dt_from']) - # Adjust query based on upper time boudary selection. - if 'dt_to' in form_args and form_args['dt_to']: - query = query.filter(model.createtime <= form_args['dt_to']) - # Adjust query based on user state selection. - if 'state' in form_args and form_args['state']: - if form_args['state'] == 'enabled': - query = query.filter(model.enabled == True) - elif form_args['state'] == 'disabled': - query = query.filter(model.enabled == False) - # Adjust query based on record source selection. - if 'source' in form_args and form_args['source']: - query = query\ - .filter(model.source == form_args['source']) - # Adjust query based on user membership selection. - if 'member' in form_args and form_args['member']: - query = query\ - .join(model.members)\ - .filter(model.members.any(id = form_args['member'].id)) - # Adjust query based on user membership selection. - if 'manager' in form_args and form_args['manager']: - query = query\ - .join(model.managers)\ - .filter(model.managers.any(id = form_args['managers'].id)) - if 'sortby' in form_args and form_args['sortby']: - sortmap = { - 'createtime.desc': lambda x, y: x.order_by(y.createtime.desc()), - 'createtime.asc': lambda x, y: x.order_by(y.createtime.asc()), - 'name.desc': lambda x, y: x.order_by(y.name.desc()), - 'name.asc': lambda x, y: x.order_by(y.name.asc()) - } - query = sortmap[form_args['sortby']](query, model) - return query - -class ShowView(HTMLMixin, SQLAlchemyMixin, ItemShowView): - """ - Detailed group view. - """ - - methods = ['GET'] - - authentication = True - - @classmethod - def get_menu_legend(cls, **kwargs): - if isinstance(kwargs['item'], cls.get_model(vial.const.MODEL_GROUP)): - return lazy_gettext( - 'View details of group "%(item)s"', - item = flask.escape(str(kwargs['item'])) - ) - return lazy_gettext( - 'View details of group "%(item)s"', - item = flask.escape(str(kwargs['item'].group)) - ) - - @classmethod - def get_view_title(cls, **kwargs): - return lazy_gettext('Show group details') - - @classmethod - def get_view_url(cls, **kwargs): - if isinstance(kwargs['item'], cls.get_model(vial.const.MODEL_GROUP)): - return flask.url_for( - cls.get_view_endpoint(), - item_id = kwargs['item'].get_id() - ) - return flask.url_for( - cls.get_view_endpoint(), - item_id = kwargs['item'].group.get_id() - ) - - @property - def dbmodel(self): - return self.get_model(vial.const.MODEL_GROUP) - - @classmethod - def authorize_item_action(cls, **kwargs): - permission_mm = flask_principal.Permission( - vial.acl.MembershipNeed(kwargs['item'].id), - vial.acl.ManagementNeed(kwargs['item'].id) - ) - return vial.acl.PERMISSION_POWER.can() or permission_mm.can() - - @classmethod - def get_action_menu(cls): - action_menu = vial.menu.Menu() - action_menu.add_entry( - 'endpoint', - 'update', - endpoint = 'groups.update', - resptitle = True - ) - action_menu.add_entry( - 'endpoint', - 'disable', - endpoint = 'groups.disable', - resptitle = True - ) - action_menu.add_entry( - 'endpoint', - 'enable', - endpoint = 'groups.enable', - resptitle = True - ) - action_menu.add_entry( - 'endpoint', - 'delete', - endpoint = 'groups.delete', - resptitle = True - ) - return action_menu - - def do_before_response(self, **kwargs): # pylint: disable=locally-disabled,no-self-use,unused-argument - action_menu = vial.menu.Menu() - action_menu.add_entry( - 'endpoint', - 'show', - endpoint = 'users.show', - hidetitle = True, - ) - action_menu.add_entry( - 'submenu', - 'more', - align_right = True, - legend = gettext('More actions') - ) - action_menu.add_entry( - 'endpoint', - 'more.add_membership', - endpoint = 'users.addmembership' - ) - action_menu.add_entry( - 'endpoint', - 'more.reject_membership', - endpoint = 'users.rejectmembership' - ) - action_menu.add_entry( - 'endpoint', - 'more.remove_membership', - endpoint = 'users.removemembership' - ) - action_menu.add_entry( - 'endpoint', - 'more.add_management', - endpoint = 'users.addmanagement' - ) - action_menu.add_entry( - 'endpoint', - 'more.remove_management', - endpoint = 'users.removemanagement' - ) - action_menu.add_entry( - 'endpoint', - 'more.enable', - endpoint = 'users.enable' - ) - action_menu.add_entry( - 'endpoint', - 'more.disable', - endpoint = 'users.disable' - ) - action_menu.add_entry( - 'endpoint', - 'more.update', - endpoint = 'users.update' - ) - self.response_context.update( - context_action_menu_users = action_menu - ) - - item = self.response_context['item'] - if self.can_access_endpoint('groups.update', item = item) and self.has_endpoint('changelogs.search'): - self.response_context.update( - context_action_menu_changelogs = self.get_endpoint_class( - 'changelogs.search' - ).get_context_action_menu() - ) - item_changelog_model = self.get_model(vial.const.MODEL_ITEM_CHANGELOG) - item_changelog = self.dbsession.query(item_changelog_model).\ - filter( - or_( - # Changelogs related directly to group item. - and_( - item_changelog_model.model == item.__class__.__name__, - item_changelog_model.model_id == item.id - ) - ) - ).\ - order_by(item_changelog_model.createtime.desc()).\ - limit(100).\ - all() - self.response_context.update(item_changelog = item_changelog) - - -class ShowByNameView(ShowView): # pylint: disable=locally-disabled,too-many-ancestors - """ - Detailed group view by group name. - """ - - @classmethod - def get_view_name(cls): - return 'show_by_name' - - @classmethod - def get_view_template(cls): - return '{}/show.html'.format(cls.module_name) - - @property - def search_by(self): - return self.dbmodel.name - - -class CreateView(HTMLMixin, SQLAlchemyMixin, ItemCreateView): # pylint: disable=locally-disabled,too-many-ancestors - """ - View for creating new groups. - """ - - methods = ['GET','POST'] - - authentication = True - - authorization = [vial.acl.PERMISSION_POWER] - - @classmethod - def get_menu_title(cls, **kwargs): - return lazy_gettext('Create group') - - @classmethod - def get_view_title(cls, **kwargs): - return lazy_gettext('Create new group') - - @property - def dbmodel(self): - return self.get_model(vial.const.MODEL_GROUP) - - @property - def dbchlogmodel(self): - return self.get_model(vial.const.MODEL_ITEM_CHANGELOG) - - @staticmethod - def get_message_success(**kwargs): - return gettext( - 'Group <strong>%(item_id)s</strong> was successfully created.', - item_id = flask.escape(str(kwargs['item'])) - ) - - @staticmethod - def get_message_failure(**kwargs): - return gettext('Unable to create new group.') - - @staticmethod - def get_message_cancel(**kwargs): - return gettext('Canceled creating new group.') - - @staticmethod - def get_item_form(item): - return AdminCreateGroupForm() - - -class UpdateView(HTMLMixin, SQLAlchemyMixin, ItemUpdateView): # pylint: disable=locally-disabled,too-many-ancestors - """ - View for updating existing groups. - """ - - methods = ['GET','POST'] - - authentication = True - - @classmethod - def get_menu_title(cls, **kwargs): - return lazy_gettext('Update') - - @classmethod - def get_menu_legend(cls, **kwargs): - return lazy_gettext( - 'Update details of group "%(item)s"', - item = flask.escape(str(kwargs['item'])) - ) - - @classmethod - def get_view_title(cls, **kwargs): - return lazy_gettext('Update group details') - - @property - def dbmodel(self): - return self.get_model(vial.const.MODEL_GROUP) - - @property - def dbchlogmodel(self): - return self.get_model(vial.const.MODEL_ITEM_CHANGELOG) - - @classmethod - def authorize_item_action(cls, **kwargs): - permission_m = flask_principal.Permission( - vial.acl.ManagementNeed(kwargs['item'].id) - ) - return vial.acl.PERMISSION_POWER.can() or permission_m.can() - - @staticmethod - def get_message_success(**kwargs): - return gettext( - 'Group <strong>%(item_id)s</strong> was successfully updated.', - item_id = flask.escape(str(kwargs['item'])) - ) - - @staticmethod - def get_message_failure(**kwargs): - return gettext( - 'Unable to update group <strong>%(item_id)s</strong>.', - item_id = flask.escape(str(kwargs['item'])) - ) - - @staticmethod - def get_message_cancel(**kwargs): - return gettext( - 'Canceled updating group <strong>%(item_id)s</strong>.', - item_id = flask.escape(str(kwargs['item'])) - ) - - @staticmethod - def get_item_form(item): - admin = flask_login.current_user.has_role('admin') - if not admin: - form = UpdateGroupForm(obj = item) - else: - form = AdminUpdateGroupForm(db_item_id = item.id, obj = item) - return form - - -class AddMemberView(HTMLMixin, SQLAlchemyMixin, ItemObjectRelationView): # pylint: disable=locally-disabled,too-many-ancestors - """ - View for adding group members. - """ - methods = ['GET','POST'] - - authentication = True - - @classmethod - def get_view_name(cls): - return 'addmember' - - @classmethod - def get_view_title(cls, **kwargs): - return gettext('Add group member') - - @classmethod - def get_view_icon(cls): - return 'action-add-member' - - @classmethod - def get_menu_legend(cls, **kwargs): - return lazy_gettext( - 'Add user "%(user_id)s" to group "%(group_id)s"', - user_id = flask.escape(str(kwargs['other'])), - group_id = flask.escape(str(kwargs['item'])) - ) - - @property - def dbmodel(self): - return self.get_model(vial.const.MODEL_GROUP) - - @property - def dbchlogmodel(self): - return self.get_model(vial.const.MODEL_ITEM_CHANGELOG) - - @property - def dbmodel_other(self): - return self.get_model(vial.const.MODEL_USER) - - @classmethod - def authorize_item_action(cls, **kwargs): - permission_m = flask_principal.Permission( - vial.acl.ManagementNeed(kwargs['item'].id) - ) - return vial.acl.PERMISSION_POWER.can() or permission_m.can() - - @classmethod - def validate_item_change(cls, **kwargs): # pylint: disable=locally-disabled,unused-argument - # Reject item change in case given item is already enabled. - if kwargs['other'] in kwargs['item'].members: - return False - return True - - @classmethod - def change_item(cls, **kwargs): - kwargs['item'].members.append(kwargs['other']) - try: - kwargs['item'].members_wanted.remove(kwargs['other']) - except ValueError: - pass - if kwargs['other'].is_state_disabled(): - kwargs['other'].set_state_enabled() - flask.current_app.send_infomail( - 'users.enable', - account = kwargs['other'] - ) - - @staticmethod - def get_message_success(**kwargs): - return gettext( - 'User <strong>%(user_id)s</strong> was successfully added as a member to group <strong>%(group_id)s</strong>.', - user_id = flask.escape(str(kwargs['other'])), - group_id = flask.escape(str(kwargs['item'])) - ) - - @staticmethod - def get_message_failure(**kwargs): - return gettext( - 'Unable to add user <strong>%(user_id)s</strong> as a member to group <strong>%(group_id)s</strong>.', - user_id = flask.escape(str(kwargs['other'])), - group_id = flask.escape(str(kwargs['item'])) - ) - - @staticmethod - def get_message_cancel(**kwargs): - return gettext( - 'Canceled adding user <strong>%(user_id)s</strong> as a member to group <strong>%(group_id)s</strong>.', - user_id = flask.escape(str(kwargs['other'])), - group_id = flask.escape(str(kwargs['item'])) - ) - - -class RejectMemberView(HTMLMixin, SQLAlchemyMixin, ItemObjectRelationView): # pylint: disable=locally-disabled,too-many-ancestors - """ - View for rejecting group membership reuests. - """ - methods = ['GET','POST'] - - authentication = True - - @classmethod - def get_view_name(cls): - return 'rejectmember' - - @classmethod - def get_view_title(cls, **kwargs): - return gettext('Reject group member') - - @classmethod - def get_view_icon(cls): - return 'action-rej-member' - - @classmethod - def get_menu_legend(cls, **kwargs): - return lazy_gettext( - 'Reject user`s "%(user_id)s" membership request for group "%(group_id)s"', - user_id = flask.escape(str(kwargs['other'])), - group_id = flask.escape(str(kwargs['item'])) - ) - - @property - def dbmodel(self): - return self.get_model(vial.const.MODEL_GROUP) - - @property - def dbchlogmodel(self): - return self.get_model(vial.const.MODEL_ITEM_CHANGELOG) - - @property - def dbmodel_other(self): - return self.get_model(vial.const.MODEL_USER) - - @classmethod - def authorize_item_action(cls, **kwargs): - permission_m = flask_principal.Permission( - vial.acl.ManagementNeed(kwargs['item'].id) - ) - return vial.acl.PERMISSION_POWER.can() or permission_m.can() - - @classmethod - def validate_item_change(cls, **kwargs): # pylint: disable=locally-disabled,unused-argument - # Reject item change in case given item is already enabled. - if kwargs['other'] not in kwargs['item'].members_wanted: - return False - return True - - @classmethod - def change_item(cls, **kwargs): - kwargs['item'].members_wanted.remove(kwargs['other']) - - #--------------------------------------------------------------------------- - - @staticmethod - def get_message_success(**kwargs): - return gettext( - 'User`s <strong>%(user_id)s</strong> membership request for group <strong>%(group_id)s</strong> was successfully rejected.', - user_id = flask.escape(str(kwargs['other'])), - group_id = flask.escape(str(kwargs['item'])) - ) - - @staticmethod - def get_message_failure(**kwargs): - return gettext( - 'Unable to reject user`s <strong>%(user_id)s</strong> membership request for group <strong>%(group_id)s</strong>.', - user_id = flask.escape(str(kwargs['other'])), - group_id = flask.escape(str(kwargs['item'])) - ) - - @staticmethod - def get_message_cancel(**kwargs): - return gettext( - 'Canceled rejecting user`s <strong>%(user_id)s</strong> membership request for group <strong>%(group_id)s</strong>.', - user_id = flask.escape(str(kwargs['other'])), - group_id = flask.escape(str(kwargs['item'])) - ) - - -class RemoveMemberView(HTMLMixin, SQLAlchemyMixin, ItemObjectRelationView): # pylint: disable=locally-disabled,too-many-ancestors - """ - View for removing group members. - """ - methods = ['GET','POST'] - - authentication = True - - @classmethod - def get_view_name(cls): - return 'removemember' - - @classmethod - def get_view_title(cls, **kwargs): - return gettext('Remove group member') - - @classmethod - def get_view_icon(cls): - return 'action-rem-member' - - @classmethod - def get_menu_legend(cls, **kwargs): - return lazy_gettext( - 'Remove user "%(user_id)s" from group "%(group_id)s"', - user_id = flask.escape(str(kwargs['other'])), - group_id = flask.escape(str(kwargs['item'])) - ) - - @property - def dbmodel(self): - return self.get_model(vial.const.MODEL_GROUP) - - @property - def dbchlogmodel(self): - return self.get_model(vial.const.MODEL_ITEM_CHANGELOG) - - @property - def dbmodel_other(self): - return self.get_model(vial.const.MODEL_USER) - - @classmethod - def authorize_item_action(cls, **kwargs): - permission_m = flask_principal.Permission( - vial.acl.ManagementNeed(kwargs['item'].id) - ) - return vial.acl.PERMISSION_POWER.can() or permission_m.can() - - @classmethod - def validate_item_change(cls, **kwargs): # pylint: disable=locally-disabled,unused-argument - # Reject item change in case given item is already enabled. - if kwargs['other'] not in kwargs['item'].members: - return False - return True - - @classmethod - def change_item(cls, **kwargs): - kwargs['item'].members.remove(kwargs['other']) - - @staticmethod - def get_message_success(**kwargs): - return gettext( - 'User <strong>%(user_id)s</strong> was successfully removed as a member from group <strong>%(group_id)s</strong>.', - user_id = flask.escape(str(kwargs['other'])), - group_id = flask.escape(str(kwargs['item'])) - ) - - @staticmethod - def get_message_failure(**kwargs): - return gettext( - 'Unable to remove user <strong>%(user_id)s</strong> as a member from group <strong>%(group_id)s</strong>.', - user_id = flask.escape(str(kwargs['other'])), - group_id = flask.escape(str(kwargs['item'])) - ) - - @staticmethod - def get_message_cancel(**kwargs): - return gettext( - 'Canceled removing user <strong>%(user_id)s</strong> as a member from group <strong>%(group_id)s</strong>.', - user_id = flask.escape(str(kwargs['other'])), - group_id = flask.escape(str(kwargs['item'])) - ) - - -class AddManagerView(HTMLMixin, SQLAlchemyMixin, ItemObjectRelationView): # pylint: disable=locally-disabled,too-many-ancestors - """ - View for adding group managers. - """ - methods = ['GET','POST'] - - authentication = True - - @classmethod - def get_view_name(cls): - return 'addmanager' - - @classmethod - def get_view_title(cls, **kwargs): - return gettext('Add group manager') - - @classmethod - def get_view_icon(cls): - return 'action-add-manager' - - @classmethod - def get_menu_legend(cls, **kwargs): - return lazy_gettext( - 'Add user "%(user_id)s" to group "%(group_id)s" as manager', - user_id = flask.escape(str(kwargs['other'])), - group_id = flask.escape(str(kwargs['item'])) - ) - - @property - def dbmodel(self): - return self.get_model(vial.const.MODEL_GROUP) - - @property - def dbchlogmodel(self): - return self.get_model(vial.const.MODEL_ITEM_CHANGELOG) - - @property - def dbmodel_other(self): - return self.get_model(vial.const.MODEL_USER) - - @classmethod - def authorize_item_action(cls, **kwargs): - permission_m = flask_principal.Permission( - vial.acl.ManagementNeed(kwargs['item'].id) - ) - return vial.acl.PERMISSION_POWER.can() or permission_m.can() - - @classmethod - def validate_item_change(cls, **kwargs): # pylint: disable=locally-disabled,unused-argument - # Reject item change in case given item is already manager. - if kwargs['other'] in kwargs['item'].managers: - return False - return True - - @classmethod - def change_item(cls, **kwargs): - kwargs['item'].managers.append(kwargs['other']) - if kwargs['other'].is_state_disabled(): - kwargs['other'].set_state_enabled() - flask.current_app.send_infomail( - 'users.enable', - account = kwargs['other'] - ) - - @staticmethod - def get_message_success(**kwargs): - return gettext( - 'User <strong>%(user_id)s</strong> was successfully added as a manager to group <strong>%(group_id)s</strong>.', - user_id = flask.escape(str(kwargs['other'])), - group_id = flask.escape(str(kwargs['item'])) - ) - - @staticmethod - def get_message_failure(**kwargs): - return gettext( - 'Unable to add user <strong>%(user_id)s</strong> as a manager to group <strong>%(group_id)s</strong>.', - user_id = flask.escape(str(kwargs['other'])), - group_id = flask.escape(str(kwargs['item'])) - ) - - @staticmethod - def get_message_cancel(**kwargs): - return gettext( - 'Canceled adding user <strong>%(user_id)s</strong> as a manager to group <strong>%(group_id)s</strong>.', - user_id = flask.escape(str(kwargs['other'])), - group_id = flask.escape(str(kwargs['item'])) - ) - - -class RemoveManagerView(HTMLMixin, SQLAlchemyMixin, ItemObjectRelationView): # pylint: disable=locally-disabled,too-many-ancestors - """ - View for removing group managers. - """ - methods = ['GET','POST'] - - authentication = True - - @classmethod - def get_view_name(cls): - return 'removemanager' - - @classmethod - def get_view_title(cls, **kwargs): - return gettext('Remove group manager') - - @classmethod - def get_view_icon(cls): - return 'action-rem-manager' - - @classmethod - def get_menu_legend(cls, **kwargs): - return lazy_gettext( - 'Remove user "%(user_id)s" from group "%(group_id)s" as manager', - user_id = flask.escape(str(kwargs['other'])), - group_id = flask.escape(str(kwargs['item'])) - ) - - @property - def dbmodel(self): - return self.get_model(vial.const.MODEL_GROUP) - - @property - def dbchlogmodel(self): - return self.get_model(vial.const.MODEL_ITEM_CHANGELOG) - - @property - def dbmodel_other(self): - return self.get_model(vial.const.MODEL_USER) - - @classmethod - def authorize_item_action(cls, **kwargs): - permission_m = flask_principal.Permission( - vial.acl.ManagementNeed(kwargs['item'].id) - ) - return vial.acl.PERMISSION_POWER.can() or permission_m.can() - - @classmethod - def validate_item_change(cls, **kwargs): # pylint: disable=locally-disabled,unused-argument - # Reject item change in case given item is not already manager. - if kwargs['other'] not in kwargs['item'].managers: - return False - return True - - @classmethod - def change_item(cls, **kwargs): - try: - kwargs['item'].managers.remove(kwargs['other']) - except ValueError: - pass - - @staticmethod - def get_message_success(**kwargs): - return gettext( - 'User <strong>%(user_id)s</strong> was successfully removed as a manager from group <strong>%(group_id)s</strong>.', - user_id = flask.escape(str(kwargs['other'])), - group_id = flask.escape(str(kwargs['item'])) - ) - - @staticmethod - def get_message_failure(**kwargs): - return gettext( - 'Unable to remove user <strong>%(user_id)s</strong> as a manager from group <strong>%(group_id)s</strong>.', - user_id = flask.escape(str(kwargs['other'])), - group_id = flask.escape(str(kwargs['item'])) - ) - - @staticmethod - def get_message_cancel(**kwargs): - return gettext( - 'Canceled removing user <strong>%(user_id)s</strong> as a manager from group <strong>%(group_id)s</strong>.', - user_id = flask.escape(str(kwargs['other'])), - group_id = flask.escape(str(kwargs['item'])) - ) - - -class EnableView(HTMLMixin, SQLAlchemyMixin, ItemEnableView): # pylint: disable=locally-disabled,too-many-ancestors - """ - View for enabling existing groups. - """ - methods = ['GET','POST'] - - authentication = True - - authorization = [vial.acl.PERMISSION_POWER] - - @classmethod - def get_menu_legend(cls, **kwargs): - return lazy_gettext( - 'Enable group "%(item)s"', - item = flask.escape(str(kwargs['item'])) - ) - - @property - def dbmodel(self): - return self.get_model(vial.const.MODEL_GROUP) - - @property - def dbchlogmodel(self): - return self.get_model(vial.const.MODEL_ITEM_CHANGELOG) - - @staticmethod - def get_message_success(**kwargs): - return gettext( - 'Group <strong>%(item_id)s</strong> was successfully enabled.', - item_id = flask.escape(str(kwargs['item'])) - ) - - @staticmethod - def get_message_failure(**kwargs): - return gettext( - 'Unable to enable group <strong>%(item_id)s</strong>.', - item_id = flask.escape(str(kwargs['item'])) - ) - - @staticmethod - def get_message_cancel(**kwargs): - return gettext( - 'Canceled enabling group <strong>%(item_id)s</strong>.', - item_id = flask.escape(str(kwargs['item'])) - ) - - -class DisableView(HTMLMixin, SQLAlchemyMixin, ItemDisableView): # pylint: disable=locally-disabled,too-many-ancestors - """ - View for disabling groups. - """ - methods = ['GET','POST'] - - authentication = True - - authorization = [vial.acl.PERMISSION_POWER] - - @classmethod - def get_menu_legend(cls, **kwargs): - return lazy_gettext( - 'Disable group "%(item)s"', - item = flask.escape(str(kwargs['item'])) - ) - - @property - def dbmodel(self): - return self.get_model(vial.const.MODEL_GROUP) - - @property - def dbchlogmodel(self): - return self.get_model(vial.const.MODEL_ITEM_CHANGELOG) - - @staticmethod - def get_message_success(**kwargs): - return gettext( - 'Group <strong>%(item_id)s</strong> was successfully disabled.', - item_id = flask.escape(str(kwargs['item'])) - ) - - @staticmethod - def get_message_failure(**kwargs): - return gettext( - 'Unable to disable group <strong>%(item_id)s</strong>.', - item_id = flask.escape(str(kwargs['item'])) - ) - - @staticmethod - def get_message_cancel(**kwargs): - return gettext( - 'Canceled disabling group <strong>%(item_id)s</strong>.', - item_id = flask.escape(str(kwargs['item'])) - ) - - -class DeleteView(HTMLMixin, SQLAlchemyMixin, ItemDeleteView): # pylint: disable=locally-disabled,too-many-ancestors - """ - View for deleting existing groups. - """ - - methods = ['GET','POST'] - - authentication = True - - authorization = [vial.acl.PERMISSION_ADMIN] - - @classmethod - def get_menu_legend(cls, **kwargs): - return lazy_gettext( - 'Delete group "%(item)s"', - item = flask.escape(str(kwargs['item'])) - ) - - @property - def dbmodel(self): - return self.get_model(vial.const.MODEL_GROUP) - - @property - def dbchlogmodel(self): - return self.get_model(vial.const.MODEL_ITEM_CHANGELOG) - - @staticmethod - def get_message_success(**kwargs): - return gettext( - 'Group <strong>%(item_id)s</strong> was successfully and permanently deleted.', - item_id = flask.escape(str(kwargs['item'])) - ) - - @staticmethod - def get_message_failure(**kwargs): - return gettext( - 'Unable to delete group <strong>%(item_id)s</strong>.', - item_id = flask.escape(str(kwargs['item'])) - ) - - @staticmethod - def get_message_cancel(**kwargs): - return gettext( - 'Canceled deleting group <strong>%(item_id)s</strong>.', - item_id = flask.escape(str(kwargs['item'])) - ) - - -#------------------------------------------------------------------------------- - - -class GroupsBlueprint(VialBlueprint): - """Pluggable module - user groups (*groups*).""" - - @classmethod - def get_module_title(cls): - return lazy_gettext('Group management') - - def register_app(self, app): - - def _fetch_my_groups(): - groups = {} - for i in list(flask_login.current_user.memberships) + list(flask_login.current_user.managements): - groups[str(i)] = i - return list(sorted(groups.values(), key = str)) - - app.menu_main.add_entry( - 'view', - 'admin.{}'.format(BLUEPRINT_NAME), - position = 50, - view = ListView - ) - app.menu_auth.add_entry( - 'submenudb', - 'my_groups', - position = 20, - title = lazy_gettext('My groups'), - resptitle = True, - icon = 'module-groups', - align_right = True, - entry_fetcher = _fetch_my_groups, - entry_builder = lambda x, y: vial.menu.EndpointEntry(x, endpoint = 'groups.show', params = {'item': y}, title = x, icon = 'module-groups') - ) - - -#------------------------------------------------------------------------------- - - -def get_blueprint(): - """ - Mandatory interface for :py:mod:`vial.Vial` and factory function. This function - must return a valid instance of :py:class:`vial.app.VialBlueprint` or - :py:class:`flask.Blueprint`. - """ - - hbp = GroupsBlueprint( - BLUEPRINT_NAME, - __name__, - template_folder = 'templates', - url_prefix = '/{}'.format(BLUEPRINT_NAME) - ) - - hbp.register_view_class(ListView, '/list') - hbp.register_view_class(CreateView, '/create') - hbp.register_view_class(ShowView, '/<int:item_id>/show') - hbp.register_view_class(ShowByNameView, '/<item_id>/show_by_name') - hbp.register_view_class(UpdateView, '/<int:item_id>/update') - hbp.register_view_class(AddMemberView, '/<int:item_id>/add_member/<int:other_id>') - hbp.register_view_class(RejectMemberView, '/<int:item_id>/reject_member/<int:other_id>') - hbp.register_view_class(RemoveMemberView, '/<int:item_id>/remove_member/<int:other_id>') - hbp.register_view_class(AddManagerView, '/<int:item_id>/add_manager/<int:other_id>') - hbp.register_view_class(RemoveManagerView, '/<int:item_id>/remove_manager/<int:other_id>') - hbp.register_view_class(EnableView, '/<int:item_id>/enable') - hbp.register_view_class(DisableView, '/<int:item_id>/disable') - hbp.register_view_class(DeleteView, '/<int:item_id>/delete') - - return hbp diff --git a/lib/vial/blueprints/groups/forms.py b/lib/vial/blueprints/groups/forms.py deleted file mode 100644 index 53ce5ff20a17931551741e0048a56e1b521666a7..0000000000000000000000000000000000000000 --- a/lib/vial/blueprints/groups/forms.py +++ /dev/null @@ -1,255 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- -# Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- - - -""" -This module contains custom group management forms for Hawat. -""" - - -import wtforms -from wtforms.ext.sqlalchemy.fields import QuerySelectField, QuerySelectMultipleField -from flask_babel import gettext, lazy_gettext - -import vial.db -import vial.forms -from vial.forms import get_available_users, get_available_group_sources - -def check_parent_not_self(form, field): - """ - Callback for validating that parent group is not self. - """ - if field.data and form.db_item_id == field.data.id: - raise wtforms.validators.ValidationError(gettext('You must not select a group as its own parent! Naughty, naughty you!')) - - -def format_select_option_label_user(item): - """ - Format option for selection of user accounts. - """ - return "{} ({})".format(item.fullname, item.login) - - -class BaseGroupForm(vial.forms.BaseItemForm): - """ - Class representing base group form. - """ - description = wtforms.StringField( - lazy_gettext('Description:'), - validators = [ - wtforms.validators.DataRequired() - ], - description = lazy_gettext('Additional and more extensive group description.') - ) - source = wtforms.HiddenField( - default = 'manual', - validators = [ - wtforms.validators.DataRequired(), - wtforms.validators.Length(min = 3, max = 50) - ], - description = lazy_gettext('Origin of the group record, whether it was added manually, or via some automated mechanism from data from some third party system.') - ) - members = QuerySelectMultipleField( - lazy_gettext('Members:'), - query_factory = vial.forms.get_available_users, - get_label = format_select_option_label_user, - blank_text = lazy_gettext('<< no selection >>'), - description = lazy_gettext('List of group members.') - ) - submit = wtforms.SubmitField( - lazy_gettext('Submit') - ) - cancel = wtforms.SubmitField( - lazy_gettext('Cancel') - ) - - -class UpdateGroupForm(BaseGroupForm): - """ - Class representing group update form for regular users. - """ - - -class AdminBaseGroupForm(BaseGroupForm): - """ - Class representing group create form. - """ - enabled = wtforms.RadioField( - lazy_gettext('State:'), - validators = [ - wtforms.validators.InputRequired(), - ], - choices = [ - (True, lazy_gettext('Enabled')), - (False, lazy_gettext('Disabled')) - ], - filters = [vial.forms.str_to_bool], - coerce = vial.forms.str_to_bool, - description = lazy_gettext('Boolean flag whether the group is enabled or disabled. Disabled groups are hidden to the most of the system features.') - ) - managers = QuerySelectMultipleField( - lazy_gettext('Managers:'), - query_factory = vial.forms.get_available_users, - get_label = format_select_option_label_user, - blank_text = lazy_gettext('<< no selection >>'), - description = lazy_gettext('List of users acting as group managers. These users may change various group settings.') - ) - parent = QuerySelectField( - lazy_gettext('Parent group:'), - validators = [ - wtforms.validators.Optional(), - check_parent_not_self - ], - query_factory = vial.forms.get_available_groups, - allow_blank = True, - blank_text = lazy_gettext('<< no selection >>'), - description = lazy_gettext('Parent group for this group. This feature enables the posibility to create structured group hierarchy.') - ) - - -class AdminCreateGroupForm(AdminBaseGroupForm): - """ - Class representing group create form for administrators. - """ - name = wtforms.StringField( - lazy_gettext('Name:'), - validators = [ - wtforms.validators.DataRequired(), - wtforms.validators.Length(min = 3, max = 100), - vial.forms.check_unique_group - ], - description = lazy_gettext('System-wide unique name for the group.') - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - self.db_item_id = None - - -class AdminUpdateGroupForm(AdminBaseGroupForm): - """ - Class representing group update form for administrators. - """ - name = wtforms.StringField( - lazy_gettext('Name:'), - validators = [ - wtforms.validators.DataRequired(), - wtforms.validators.Length(min = 3, max = 100), - vial.forms.check_unique_group - ], - description = lazy_gettext('System-wide unique name for the group.') - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - # Store the ID of original item in database to enable the ID uniqueness - # check with check_name_uniqueness() validator. - self.db_item_id = kwargs['db_item_id'] - - -class GroupSearchForm(vial.forms.BaseSearchForm): - """ - Class representing simple user search form. - """ - search = wtforms.StringField( - lazy_gettext('Name, description:'), - validators = [ - wtforms.validators.Optional(), - wtforms.validators.Length(min = 3, max = 100) - ], - description = lazy_gettext('Group`s full name or description. Search is performed even in the middle of the strings.') - ) - dt_from = vial.forms.SmartDateTimeField( - lazy_gettext('Creation time from:'), - validators = [ - wtforms.validators.Optional() - ], - description = lazy_gettext('Lower time boundary for item creation time. Timestamp is expected to be in the format <code>YYYY-MM-DD hh:mm:ss</code> and in the timezone according to the user`s preferences.') - ) - dt_to = vial.forms.SmartDateTimeField( - lazy_gettext('Creation time to:'), - validators = [ - wtforms.validators.Optional() - ], - description = lazy_gettext('Upper time boundary for item creation time. Timestamp is expected to be in the format <code>YYYY-MM-DD hh:mm:ss</code> and in the timezone according to the user`s preferences.') - ) - - state = wtforms.SelectField( - lazy_gettext('State:'), - validators = [ - wtforms.validators.Optional(), - ], - choices = [ - ('', lazy_gettext('Nothing selected')), - ('enabled', lazy_gettext('Enabled')), - ('disabled', lazy_gettext('Disabled')) - ], - default = '', - description = lazy_gettext('Search for groups with particular state.') - ) - source = wtforms.SelectField( - lazy_gettext('Record source:'), - validators = [ - wtforms.validators.Optional() - ], - default = '', - description = lazy_gettext('Search for groups coming from particular sources/feeds.') - ) - members = QuerySelectField( - lazy_gettext('Group members:'), - query_factory = get_available_users, - allow_blank = True, - description = lazy_gettext('Search for groups with particular members.') - ) - managers = QuerySelectField( - lazy_gettext('Group managers:'), - query_factory = get_available_users, - allow_blank = True, - description = lazy_gettext('Search for groups with particular managers.') - ) - - sortby = wtforms.SelectField( - lazy_gettext('Sort by:'), - validators = [ - wtforms.validators.Optional() - ], - choices = [ - ('createtime.desc', lazy_gettext('by creation time descending')), - ('createtime.asc', lazy_gettext('by creation time ascending')), - ('name.desc', lazy_gettext('by name descending')), - ('name.asc', lazy_gettext('by name ascending')) - ], - default = 'name.asc' - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - # - # Handle additional custom keywords. - # - - # The list of choices for 'roles' attribute comes from outside of the - # form to provide as loose tie as possible to the outer application. - # Another approach would be to load available choices here with: - # - # roles = flask.current_app.config['ROLES'] - # - # That would mean direct dependency on flask.Flask application. - source_list = get_available_group_sources() - self.source.choices = [('', lazy_gettext('Nothing selected'))] + list(zip(source_list, source_list)) - - @staticmethod - def is_multivalue(field_name): - """ - Check, if given form field is a multivalue field. - - :param str field_name: Name of the form field. - :return: ``True``, if the field can contain multiple values, ``False`` otherwise. - :rtype: bool - """ - return False diff --git a/lib/vial/blueprints/groups/templates/groups/addmember.html b/lib/vial/blueprints/groups/templates/groups/addmember.html deleted file mode 100644 index 183e476c8b296e492e4eafaef984fb5487387db6..0000000000000000000000000000000000000000 --- a/lib/vial/blueprints/groups/templates/groups/addmember.html +++ /dev/null @@ -1,16 +0,0 @@ -{%- extends "_layout_confirmation.html" %} - -{%- block modalcontent %} - <p> - {{ _('Are you really sure you want to add user') }} - </p> - <p class="lead text-center"> - {{ other_name }} - </p> - <p> - {{ _('to abuse group') }} - </p> - <p class="lead text-center"> - {{ item_name }} - </p> -{% endblock modalcontent %} diff --git a/lib/vial/blueprints/groups/templates/groups/creatupdate.html b/lib/vial/blueprints/groups/templates/groups/creatupdate.html deleted file mode 100644 index a08a12bd09fd0b63b775be2111fb05bb305d5272..0000000000000000000000000000000000000000 --- a/lib/vial/blueprints/groups/templates/groups/creatupdate.html +++ /dev/null @@ -1,32 +0,0 @@ -{%- extends "_layout_creatupdate.html" %} - -{%- block itemform_fields %} - - {%- if item_action == 'create' or current_user.has_role('admin') %} - - {{ macros_form.render_form_item_default(form.name) }} - - {%- elif item_action == 'update' %} - - {{ macros_form.render_form_item_static(_('Name:'), item.name) }} - - {%- endif %} - - {{ macros_form.render_form_item_default(form.description) }} - - <hr> - - {%- if form | attr('enabled') %} - {{ macros_form.render_form_item_radiobutton(form.enabled) }} - {%- endif %} - {%- if form | attr('members') %} - {{ macros_form.render_form_item_select(form.members) }} - {%- endif %} - {%- if form | attr('managers') %} - {{ macros_form.render_form_item_select(form.managers) }} - {%- endif %} - {%- if form | attr('parent') %} - {{ macros_form.render_form_item_select(form.parent) }} - {%- endif %} - -{%- endblock itemform_fields %} diff --git a/lib/vial/blueprints/groups/templates/groups/list.html b/lib/vial/blueprints/groups/templates/groups/list.html deleted file mode 100644 index c23eaecceb30ca2cf9dfb628bdf5aab3f20c3b13..0000000000000000000000000000000000000000 --- a/lib/vial/blueprints/groups/templates/groups/list.html +++ /dev/null @@ -1,78 +0,0 @@ -{%- extends "_layout_list.html" %} - -{%- block searchformfields %} - - <div class="row"> - <div class="col-sm-4"> - {{ macros_form.render_form_item_default(g.search_form.search) }} - </div> - <div class="col-sm-4"> - {{ macros_form.render_form_item_datetime(g.search_form.dt_from, 'datetimepicker-hm-from') }} - </div> - <div class="col-sm-4"> - {{ macros_form.render_form_item_datetime(g.search_form.dt_to, 'datetimepicker-hm-to') }} - </div> - </div> - <div class="row"> - <div class="col-sm-3"> - {{ macros_form.render_form_item_select(g.search_form.state) }} - </div> - <div class="col-sm-3"> - {{ macros_form.render_form_item_select(g.search_form.source) }} - </div> - <div class="col-sm-3"> - {{ macros_form.render_form_item_select(g.search_form.members) }} - </div> - <div class="col-sm-3"> - {{ macros_form.render_form_item_select(g.search_form.managers) }} - </div> - </div> - -{%- endblock searchformfields %} - -{%- block contentinner %} - - <table class="table table-bordered table-hover table-striped"> - <thead> - <tr> - <th> - {{ _('Name') }} {{ macros_site.render_sorter(request.endpoint, query_params, 'name') }} - </th> - <th> - {{ _('Source') }} - </th> - <th> - {{ _('Description') }} - </th> - <th> - {{ _('State') }} - </th> - <th data-toggle="tooltip" title="{{ _('Contextual item actions') }}"> - {{ get_icon('actions') }} {{ _('Actions') }} - </th> - </tr> - </thead> - <tbody> - {%- for item in items %} - <tr> - <td> - {{ item.name | default(_('<< unknown >>'), True) }} - </td> - <td> - {{ item.source | default(_('<< unknown >>'), True) }} - </td> - <td> - {{ item.description | default(_('<< unknown >>'), True) | truncate(50) }} - </td> - <td> - {{ macros_site.render_label_item_state(item.enabled, True) }} - </td> - <td class="column-actions"> - {{ macros_page.render_menu_context_actions(item) }} - </td> - </tr> - {%- endfor %} - </tbody> - </table> - -{%- endblock contentinner %} diff --git a/lib/vial/blueprints/groups/templates/groups/rejectmember.html b/lib/vial/blueprints/groups/templates/groups/rejectmember.html deleted file mode 100644 index c28c4a8a3934aa884a606634fc596960f6c536bf..0000000000000000000000000000000000000000 --- a/lib/vial/blueprints/groups/templates/groups/rejectmember.html +++ /dev/null @@ -1,16 +0,0 @@ -{%- extends "_layout_confirmation.html" %} - -{%- block modalcontent %} - <p> - {{ _('Are you really sure you want to reject membership request of user') }} - </p> - <p class="lead text-center"> - {{ other_name }} - </p> - <p> - {{ _('for abuse group') }} - </p> - <p class="lead text-center"> - {{ item_name }} - </p> -{% endblock modalcontent %} diff --git a/lib/vial/blueprints/groups/templates/groups/removemember.html b/lib/vial/blueprints/groups/templates/groups/removemember.html deleted file mode 100644 index 72d449bead97814d032efb500c85aff94573bf59..0000000000000000000000000000000000000000 --- a/lib/vial/blueprints/groups/templates/groups/removemember.html +++ /dev/null @@ -1,16 +0,0 @@ -{%- extends "_layout_confirmation.html" %} - -{%- block modalcontent %} - <p> - {{ _('Are you really sure you want to remove user') }} - </p> - <p class="lead text-center"> - {{ other_name }} - </p> - <p> - {{ _('from abuse group') }} - </p> - <p class="lead text-center"> - {{ item_name }} - </p> -{% endblock modalcontent %} diff --git a/lib/vial/blueprints/groups/templates/groups/show.html b/lib/vial/blueprints/groups/templates/groups/show.html deleted file mode 100644 index fdf42a305d999da0e774b650289bc97f05e13193..0000000000000000000000000000000000000000 --- a/lib/vial/blueprints/groups/templates/groups/show.html +++ /dev/null @@ -1,187 +0,0 @@ -{%- extends "_layout.html" %} - -{%- block content %} - - <div class="row"> - <div class="col-lg-12"> - {{ macros_page.render_breadcrumbs(item) }} - - <h2>{{ vial_current_view.get_view_title() }}</h2> - <hr> - <h3>{{ item.name }}{% if item.description %} <small>{{ item.description }}</small>{% endif %}</h3> - <div class="pull-right"> - {{ macros_page.render_menu_actions(item) }} - - </div> - <p> - <small> - <strong>{{ _('Group created') }}:</strong> {{ babel_format_datetime(item.createtime) }} ({{ _('%(delta)s ago', delta = babel_format_timedelta(current_datetime_utc - item.createtime)) }}) - </small> - </p> - <br> - - <!-- Nav tabs --> - <ul class="nav nav-tabs" role="tablist"> - <li role="presentation" class="active"> - <a href="#tab-general" aria-controls="tab-general" role="tab" data-toggle="tab"> - <strong>{{ get_icon('alert-info') }} {{ _('General information') }}</strong> - </a> - </li> - {%- if can_access_endpoint('groups.update', item) %} - <li role="presentation"> - <a href="#tab-changelog" aria-controls="tab-changelog" role="tab" data-toggle="tab"> - <strong>{{ get_icon('module-changelogs') }} {{ _('Changelogs') }}</strong> <span class="badge">{{ item_changelog | length }}</span> - </a> - </li> - {%- endif %} - </ul> - - <!-- Tab panes --> - <div class="tab-content"> - - <div role="tabpanel" class="tab-pane fade in active" id="tab-general"> - <br> - <div class="row"> - <div class="col-md-4"> - <h4>{{ _('Metadata') }}</h4> - <table class="table table-striped"> - <tbody> - <tr> - <th> - {{ _('Parent group') }}: - </th> - <td> - {%- if item.parent %} - <a data-toggle="tooltip" href="{{ url_for('groups.show', item_id = item.parent.id ) }}" title="{{ _('View details of group "%(item)s"', item = item.parent.name) }}"> - {{ item.parent.name }} - </a> - {%- else %} - {{ _('<< none >>') }} - {%- endif %} - </td> - </tr> - <tr> - <th> - {{ _('State') }}: - </th> - <td> - {{ macros_site.render_label_item_state(item.enabled, True) }} - </td> - </tr> - </tbody> - </table> - </div> - - <div class="col-md-4"> - {%- if item.members_wanted %} - <h4> - {{ _('Membership requests') }} <span class="badge">{{ item.members_wanted | length }}</span> - </h4> - <table class="table table-striped"> - <tbody> - {%- for subitem in item.members_wanted %} - <tr> - <td{%- if not subitem.enabled %} class="text-muted"{% endif %}> - {{ subitem.login }} - </td> - <td{%- if not subitem.enabled %} class="text-muted"{% endif %}> - {{ subitem.fullname }} - </td> - <td> - {{ macros_page.render_menu_context_actions(subitem, context_action_menu_users, kwargs = {'other': item}) }} - </td> - </tr> - {%- endfor %} - </tbody> - </table> - {%- endif %} - <h4> - {{ _('Members') }} <span class="badge">{{ item.members | length }}</span> - </h4> - {%- if item.members %} - <table class="table table-striped"> - <tbody> - {%- for subitem in item.members %} - <tr> - <td{%- if not subitem.enabled %} class="text-muted"{% endif %}> - {{ subitem.login }} - </td> - <td{%- if not subitem.enabled %} class="text-muted"{% endif %}> - {{ subitem.fullname }} - </td> - <td> - {{ macros_page.render_menu_context_actions(subitem, context_action_menu_users, kwargs = {'other': item}) }} - </td> - </tr> - {%- endfor %} - </tbody> - </table> - {%- else %} - {%- call macros_site.render_alert('info', False) %} - {{ _('This group does not have any members defined at the moment.') }} - {%- endcall %} - {%- endif %} - </div> - - <div class="col-md-4"> - <h4>{{ _('Managers') }} <span class="badge">{{ item.managers | length }}</span></h4> - {%- if item.managers %} - <table class="table table-striped"> - <tbody> - {%- for subitem in item.managers %} - <tr> - <td> - {{ subitem.login }} - </td> - <td> - {{ subitem.fullname }} - </td> - <td> - {{ macros_page.render_menu_context_actions(subitem, context_action_menu_users, kwargs = {'other': item}) }} - </td> - </tr> - {%- endfor %} - </tbody> - </table> - {%- else %} - {%- call macros_site.render_alert('info', False) %} - {{ _('This group does not have any managers defined at the moment.') }} - {%- endcall %} - {%- endif %} - </div> - - </div> - </div> - - {%- if can_access_endpoint('groups.update', item) %} - <div role="tabpanel" class="tab-pane fade" id="tab-changelog"> - <br> - {%- if item_changelog %} - {{ macros_page.render_changelog_records(item_changelog, context_action_menu_changelogs) }} - <p> - <small> - {{ _('Displaying only latest %(count)s changelogs', count = 100) }} - </small> - </p> - {%- else %} - {%- call macros_site.render_alert('info', False) %} - {{ _('This group does not have any changelog records at the moment.') }} - {%- endcall %} - {%- endif %} - </div> - {%- endif %} - - </div> - - </div><!-- /.col-lg-12 --> - </div><!-- /.row --> - - {%- if permission_can('developer') %} - - <hr> - - {{ macros_site.render_raw_item_view(item) }} - - {%- endif %} - -{%- endblock content %} diff --git a/lib/vial/blueprints/groups/test/__init__.py b/lib/vial/blueprints/groups/test/__init__.py deleted file mode 100644 index a45d1e9d35de4e63af58bb2d86693987421d1821..0000000000000000000000000000000000000000 --- a/lib/vial/blueprints/groups/test/__init__.py +++ /dev/null @@ -1,541 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- -# Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- - - -""" -Unit tests for :py:mod:`vial.blueprints.groups`. -""" - - -import sys -import unittest - -import vial.const -import vial.db -import vial.test -import vial.test.fixtures -import vial.test.runner -from vial.test import VialTestCase, ItemCreateVialTestCase -from vial.test.runner import TestRunnerMixin - - -_IS_NOSE = sys.argv[0].endswith('nosetests') - -@unittest.skipIf(_IS_NOSE, "broken under nosetest") -class GroupsListTestCase(TestRunnerMixin, VialTestCase): - """Class for testing ``groups.list`` endpoint.""" - - def _attempt_fail(self): - self.assertGetURL( - '/groups/list', - 403 - ) - - def _attempt_succeed(self): - self.assertGetURL( - '/groups/list', - 200, - [ - b'View details of group "DEMO_GROUP_A"', - b'View details of group "DEMO_GROUP_B"', - ] - ) - - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) - def test_01_as_user(self): - """Test access as user ``user``.""" - self._attempt_fail() - - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) - def test_02_as_developer(self): - """Test access as user ``developer``.""" - self._attempt_fail() - - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) - def test_03_as_maintainer(self): - """Test access as user ``maintainer``.""" - self._attempt_succeed() - - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) - def test_04_as_admin(self): - """Test access as user ``admin``.""" - self._attempt_succeed() - - -@unittest.skipIf(_IS_NOSE, "broken under nosetest") -class GroupsShowTestCase(TestRunnerMixin, VialTestCase): - """Base class for testing ``groups.show`` and ``groups.show_by_name`` endpoints.""" - - def _attempt_fail(self, gname): - gid = self.group_id(gname, with_app_ctx = True) - self.assertGetURL( - '/groups/{}/show'.format(gid), - 403 - ) - self.assertGetURL( - '/groups/{}/show_by_name'.format(gname), - 403 - ) - - def _attempt_succeed(self, gname): - gid = self.group_id(gname, with_app_ctx = True) - self.assertGetURL( - '/groups/{}/show'.format(gid), - 200, - [ - '<h3>{} '.format(gname).encode('utf8'), - b'<strong>Group created:</strong>' - ] - ) - self.assertGetURL( - '/groups/{}/show_by_name'.format(gname), - 200, - [ - '<h3>{} '.format(gname).encode('utf8'), - b'<strong>Group created:</strong>' - ] - ) - - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) - def test_01_as_user(self): - """ - Test access as user 'user'. - - Only power user is able to view all available groups. - """ - self._attempt_succeed(vial.test.fixtures.DEMO_GROUP_A) - self._attempt_fail(vial.test.fixtures.DEMO_GROUP_B) - - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) - def test_02_as_developer(self): - """ - Test access as user 'developer'. - - Only power user is able to view all available groups. - """ - self._attempt_succeed(vial.test.fixtures.DEMO_GROUP_A) - self._attempt_fail(vial.test.fixtures.DEMO_GROUP_B) - - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) - def test_03_as_maintainer(self): - """ - Test access as user 'maintainer'. - - Only power user is able to view all available groups. - """ - self._attempt_succeed(vial.test.fixtures.DEMO_GROUP_A) - self._attempt_succeed(vial.test.fixtures.DEMO_GROUP_B) - - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) - def test_04_as_admin(self): - """ - Test access as user 'admin'. - - Only power user is able to view all available groups. - """ - self._attempt_succeed(vial.test.fixtures.DEMO_GROUP_A) - self._attempt_succeed(vial.test.fixtures.DEMO_GROUP_B) - - -@unittest.skipIf(_IS_NOSE, "broken under nosetest") -class GroupsCreateTestCase(TestRunnerMixin, ItemCreateVialTestCase): - """Class for testing ``groups.create`` endpoint.""" - - group_data_fixture = [ - ('name', 'TEST_GROUP'), - ('description', 'Test group for unit testing purposes.'), - ('enabled', True) - ] - - def _attempt_fail(self): - self.assertGetURL( - '/groups/create', - 403 - ) - - def _attempt_succeed(self): - self.assertCreate( - self.group_model(), - '/groups/create', - self.group_data_fixture, - [ - b'Group <strong>TEST_GROUP</strong> was successfully created.' - ] - ) - - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) - def test_01_as_user(self): - """Test access as user 'user'.""" - self._attempt_fail() - - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) - def test_02_as_developer(self): - """Test access as user 'developer'.""" - self._attempt_fail() - - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) - def test_03_as_maintainer(self): - """Test access as user 'maintainer'.""" - self._attempt_succeed() - - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) - def test_04_as_admin(self): - """Test access as user 'admin'.""" - self._attempt_succeed() - - -@unittest.skipIf(_IS_NOSE, "broken under nosetest") -class GroupsUpdateTestCase(TestRunnerMixin, VialTestCase): - """Class for testing ``groups.update`` endpoint.""" - - def _attempt_fail(self, gname): - gid = self.group_id(gname, with_app_ctx = True) - self.assertGetURL( - '/groups/{}/update'.format(gid), - 403 - ) - - def _attempt_succeed(self, gname): - gid = self.group_id(gname, with_app_ctx = True) - self.assertGetURL( - '/groups/{}/update'.format(gid), - 200, - [ - b'Update group details' - ] - ) - - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) - def test_01_as_user(self): - """Test access as user 'user'.""" - self._attempt_fail(vial.test.fixtures.DEMO_GROUP_A) - self._attempt_fail(vial.test.fixtures.DEMO_GROUP_B) - - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) - def test_04_as_developer(self): - """Test access as user 'developer'.""" - self._attempt_succeed(vial.test.fixtures.DEMO_GROUP_A) - self._attempt_fail(vial.test.fixtures.DEMO_GROUP_B) - - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) - def test_05_as_maintainer(self): - """Test access as user 'maintainer'.""" - self._attempt_succeed(vial.test.fixtures.DEMO_GROUP_A) - self._attempt_succeed(vial.test.fixtures.DEMO_GROUP_B) - - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) - def test_06_as_admin(self): - """Test access as user 'admin'.""" - self._attempt_succeed(vial.test.fixtures.DEMO_GROUP_A) - self._attempt_succeed(vial.test.fixtures.DEMO_GROUP_B) - - -@unittest.skipIf(_IS_NOSE, "broken under nosetest") -class GroupsEnableDisableTestCase(TestRunnerMixin, VialTestCase): - """Class for testing ``groups.enable`` and ``groups.disable`` endpoint.""" - - def _attempt_fail(self, gname): - gid = self.group_id(gname, with_app_ctx = True) - self.assertGetURL( - '/groups/{}/disable'.format(gid), - 403 - ) - self.assertGetURL( - '/groups/{}/enable'.format(gid), - 403 - ) - - def _attempt_succeed(self, gname): - gid = self.group_id(gname, with_app_ctx = True) - self.assertGetURL( - '/groups/{}/disable'.format(gid), - 200, - [ - b'Are you really sure you want to disable following item:' - ] - ) - self.assertPostURL( - '/groups/{}/disable'.format(gid), - { - 'submit': 'Confirm' - }, - 200, - [ - b'was successfully disabled.' - ] - ) - self.assertGetURL( - '/groups/{}/enable'.format(gid), - 200, - [ - b'Are you really sure you want to enable following item:' - ] - ) - self.assertPostURL( - '/groups/{}/enable'.format(gid), - { - 'submit': 'Confirm' - }, - 200, - [ - b'was successfully enabled.' - ] - ) - - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) - def test_01_as_user(self): - """Test access as user 'user'.""" - self._attempt_fail(vial.test.fixtures.DEMO_GROUP_A) - self._attempt_fail(vial.test.fixtures.DEMO_GROUP_B) - - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) - def test_02_as_developer(self): - """Test access as user 'developer'.""" - self._attempt_fail(vial.test.fixtures.DEMO_GROUP_A) - self._attempt_fail(vial.test.fixtures.DEMO_GROUP_B) - - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) - def test_03_as_maintainer(self): - """Test access as user 'maintainer'.""" - self._attempt_succeed(vial.test.fixtures.DEMO_GROUP_A) - self._attempt_succeed(vial.test.fixtures.DEMO_GROUP_B) - - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) - def test_04_as_admin(self): - """Test access as user 'admin'.""" - self._attempt_succeed(vial.test.fixtures.DEMO_GROUP_A) - self._attempt_succeed(vial.test.fixtures.DEMO_GROUP_B) - - -@unittest.skipIf(_IS_NOSE, "broken under nosetest") -class GroupsAddRemRejMemberTestCase(TestRunnerMixin, VialTestCase): - """Class for testing ``groups.add_member``, ``groups.reject_member`` and ``groups.remove_member`` endpoint.""" - - def _attempt_fail(self, uname, gname): - with self.app.app_context(): - uid = self.user_id(uname) - gid = self.group_id(gname) - self.assertGetURL( - '/groups/{}/remove_member/{}'.format(gid, uid), - 403 - ) - self.assertGetURL( - '/groups/{}/reject_member/{}'.format(gid, uid), - 403 - ) - self.assertGetURL( - '/groups/{}/add_member/{}'.format(gid, uid), - 403 - ) - - def _attempt_succeed(self, uname, gname, print_response = False): - # Additional test preparations. - with self.app.app_context(): - uid = self.user_id(uname) - gid = self.group_id(gname) - self.user_enabled(uname, False) - self.mailbox_monitoring('on') - - # - # First check the removal of existing membership. - # - self.assertGetURL( - '/groups/{}/remove_member/{}'.format(gid, uid), - 200, - [ - b'Are you really sure you want to remove user' - ], - print_response - ) - self.assertPostURL( - '/groups/{}/remove_member/{}'.format(gid, uid), - { - 'submit': 'Confirm' - }, - 200, - [ - b'was successfully removed as a member from group' - ], - print_response - ) - - # - # Add user back to group. - # - self.assertGetURL( - '/groups/{}/add_member/{}'.format(gid, uid), - 200, - [ - b'Are you really sure you want to add user' - ], - print_response - ) - self.assertPostURL( - '/groups/{}/add_member/{}'.format(gid, uid), - { - 'submit': 'Confirm' - }, - 200, - [ - b'was successfully added as a member to group' - ], - print_response - ) - self.assertMailbox( - { - 'subject': [ - '[{}] Account activation - {}'.format(self.app.config['APPLICATION_NAME'], uname) - ], - 'sender': [ - 'root@unittest' - ], - 'recipients': [ - ['{}@bogus-domain.org'.format(uname)] - ], - 'cc': [[]], - 'bcc': [['admin@unittest']] - } - ) - self.mailbox_clear() - - # Additional test preparations. - with self.app.app_context(): - uobj = self.user_get(uname) - gobj = self.group_get(gname) - uid = uobj.id - gid = gobj.id - uobj.enabled = False - uobj.memberships.remove(gobj) - uobj.memberships_wanted.append(gobj) - self.user_save(uobj) - - # - # Check membership request rejection feature. - # - self.assertGetURL( - '/groups/{}/reject_member/{}'.format(gid, uid), - 200, - [ - b'Are you really sure you want to reject membership request of user' - ], - print_response - ) - self.assertPostURL( - '/groups/{}/reject_member/{}'.format(gid, uid), - { - 'submit': 'Confirm' - }, - 200, - [ - b'was successfully rejected.' - ], - print_response - ) - self.mailbox_monitoring('off') - - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) - def test_01_as_user(self): - """Test access as user 'user'.""" - for uname in ( - vial.const.ROLE_USER, - vial.const.ROLE_DEVELOPER, - vial.const.ROLE_MAINTAINER, - vial.const.ROLE_ADMIN - ): - self._attempt_fail(uname, vial.test.fixtures.DEMO_GROUP_A) - - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) - def test_02_as_developer(self): - """Test access as user 'developer'.""" - for uname in ( - vial.const.ROLE_USER, - vial.const.ROLE_MAINTAINER, - vial.const.ROLE_ADMIN - ): - self._attempt_succeed(uname, vial.test.fixtures.DEMO_GROUP_A) - - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) - def test_03_as_maintainer(self): - """Test access as user 'maintainer'.""" - for uname in ( - vial.const.ROLE_USER, - vial.const.ROLE_DEVELOPER, - vial.const.ROLE_ADMIN - ): - self._attempt_succeed(uname, vial.test.fixtures.DEMO_GROUP_A) - - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) - def test_04_as_admin(self): - """Test access as user 'admin'.""" - for uname in ( - vial.const.ROLE_USER, - vial.const.ROLE_DEVELOPER, - vial.const.ROLE_MAINTAINER - ): - self._attempt_succeed(uname, vial.test.fixtures.DEMO_GROUP_A) - - -@unittest.skipIf(_IS_NOSE, "broken under nosetest") -class GroupsDeleteTestCase(TestRunnerMixin, VialTestCase): - """Class for testing ``groups.delete`` endpoint.""" - - def _attempt_fail(self, gname): - gid = self.group_id(gname, with_app_ctx = True) - self.assertGetURL( - '/groups/{}/delete'.format(gid), - 403 - ) - - def _attempt_succeed(self, gname): - gid = self.group_id(gname, with_app_ctx = True) - self.assertGetURL( - '/groups/{}/delete'.format(gid), - 200, - [ - b'Are you really sure you want to permanently remove following item:' - ] - ) - self.assertPostURL( - '/groups/{}/delete'.format(gid), - { - 'submit': 'Confirm' - }, - 200, - [ - b'was successfully and permanently deleted.' - ] - ) - - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) - def test_01_as_user(self): - """Test access as user 'user'.""" - self._attempt_fail(vial.test.fixtures.DEMO_GROUP_A) - self._attempt_fail(vial.test.fixtures.DEMO_GROUP_B) - - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) - def test_02_as_developer(self): - """Test access as user 'developer'.""" - self._attempt_fail(vial.test.fixtures.DEMO_GROUP_A) - self._attempt_fail(vial.test.fixtures.DEMO_GROUP_B) - - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) - def test_03_as_maintainer(self): - """Test access as user 'maintainer'.""" - self._attempt_fail(vial.test.fixtures.DEMO_GROUP_A) - self._attempt_fail(vial.test.fixtures.DEMO_GROUP_B) - - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) - def test_04_as_admin(self): - """Test access as user 'admin'.""" - self._attempt_succeed(vial.test.fixtures.DEMO_GROUP_A) - self._attempt_succeed(vial.test.fixtures.DEMO_GROUP_B) - - -#------------------------------------------------------------------------------- - - -if __name__ == "__main__": - unittest.main() diff --git a/lib/vial/blueprints/home/__init__.py b/lib/vial/blueprints/home/__init__.py deleted file mode 100644 index 10ecd56563729a6d973c50209c6869c506ba5b44..0000000000000000000000000000000000000000 --- a/lib/vial/blueprints/home/__init__.py +++ /dev/null @@ -1,85 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- -# Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- - -""" -This pluggable module provides default home page. - - -Provided endpoints ------------------- - -``/`` - Page providing home page. - - * *Authentication:* no authentication - * *Methods:* ``GET`` -""" - - -from flask_babel import lazy_gettext - -from vial.app import VialBlueprint -from vial.view import SimpleView -from vial.view.mixin import HTMLMixin - - -BLUEPRINT_NAME = 'home' -"""Name of the blueprint as module global constant.""" - - -class IndexView(HTMLMixin, SimpleView): - """ - View presenting home page. - """ - methods = ['GET','POST'] - - @classmethod - def get_view_name(cls): - return 'index' - - @classmethod - def get_view_icon(cls): - return 'module-home' - - @classmethod - def get_view_title(cls, **kwargs): - return lazy_gettext('Welcome!') - - @classmethod - def get_menu_title(cls, **kwargs): - return lazy_gettext('Home') - - -#------------------------------------------------------------------------------- - - -class HomeBlueprint(VialBlueprint): - """Pluggable module - home page (*home*).""" - - @classmethod - def get_module_title(cls): - return lazy_gettext('Home page') - - -#------------------------------------------------------------------------------- - - -def get_blueprint(): - """ - Mandatory interface for :py:mod:`vial.Vial` and factory function. This function - must return a valid instance of :py:class:`vial.app.VialBlueprint` or - :py:class:`flask.Blueprint`. - """ - - hbp = HomeBlueprint( - BLUEPRINT_NAME, - __name__, - template_folder = 'templates' - ) - - hbp.register_view_class(IndexView, '/') - - return hbp diff --git a/lib/vial/blueprints/home/templates/home/index.html b/lib/vial/blueprints/home/templates/home/index.html deleted file mode 100644 index e44993332f249bf1873456e70b68b579de8fc477..0000000000000000000000000000000000000000 --- a/lib/vial/blueprints/home/templates/home/index.html +++ /dev/null @@ -1,39 +0,0 @@ -{% extends "_layout.html" %} - -{% block title %}{{ vial_current_view.get_view_title() }}{% endblock %} - -{% block content %} -<div class="row"> - <div class="col-lg-12"> - <div class="jumbotron" style="margin-top: 1em;"> - <h1 class="text-center"> - {{ vial_current_view.get_view_title() }} - </h1> - <hr> - <br> - <img src="{{ url_for('design.static', filename='images/app-logo.png') }}" class="welcome-logo img-responsive center-block"> - {% if not current_user.is_authenticated %} - <br> - <br> - <p class="lead text-center"> - {{ _('You have to be authenticated to use most features provided by this application.') }} - </p> - <br> - {%- set endpoint_register = get_endpoint_class('auth.register') %} - {%- set endpoint_login = get_endpoint_class('auth.login') %} - <div class="text-center"> - <div class="btn-group" role="group"> - <a class="btn btn-default btn-lg" href="{{ endpoint_register.get_view_url() }}" role="button"> - {{ get_icon(endpoint_register.get_view_icon()) }} {{ endpoint_register.get_menu_title() }} - </a> - <a class="btn btn-primary btn-lg" href="{{ endpoint_login.get_view_url() }}" role="button"> - {{ get_icon(endpoint_login.get_view_icon()) }} {{ endpoint_login.get_menu_title() }} - </a> - </div> - </div> - {% endif %} - </div> - </div> - <!-- /.col-lg-12 --> -</div> -{% endblock %} diff --git a/lib/vial/blueprints/home/test/__init__.py b/lib/vial/blueprints/home/test/__init__.py deleted file mode 100644 index 22fe835e28dbf3ca58d59b69177a19e944d777eb..0000000000000000000000000000000000000000 --- a/lib/vial/blueprints/home/test/__init__.py +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- -# Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- - - -""" -Unit tests for :py:mod:`vial.blueprints.home`. -""" - - -import sys -import unittest - -import vial.const -import vial.db -import vial.test -import vial.test.runner -from vial.test import VialTestCase -from vial.test.runner import TestRunnerMixin - - -_IS_NOSE = sys.argv[0].endswith('nosetests') - -@unittest.skipIf(_IS_NOSE, "broken under nosetest") -class HomeTestCase(TestRunnerMixin, VialTestCase): - """ - Class for testing ``home.index`` endpoint. - """ - - def _attempt_succeed(self): - self.assertGetURL( - '/', - 200, - [ - b'Welcome!' - ] - ) - - def test_01_as_anonymous(self): - """Test access as anonymous user.""" - self._attempt_succeed() - - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) - def test_02_as_user(self): - """Test access as user ``user``.""" - self._attempt_succeed() - - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) - def test_03_as_developer(self): - """Test access as user ``developer``.""" - self._attempt_succeed() - - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) - def test_04_as_maintainer(self): - """Test access as user ``maintainer``.""" - self._attempt_succeed() - - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) - def test_05_as_admin(self): - """Test access as user ``admin``.""" - self._attempt_succeed() - - -#------------------------------------------------------------------------------- - - -if __name__ == "__main__": - unittest.main() diff --git a/lib/vial/blueprints/users/__init__.py b/lib/vial/blueprints/users/__init__.py deleted file mode 100644 index fd84e20cf89b79a7e246e45ae757eb9107b2774a..0000000000000000000000000000000000000000 --- a/lib/vial/blueprints/users/__init__.py +++ /dev/null @@ -1,1230 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- -# Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- - - -""" -This file contains pluggable module for Vial application containing features -related to user account management. These features include: - -* general user account listing -* detailed user account view -* creating new user accounts -* updating existing user accounts -* deleting existing user accounts -* enabling existing user accounts -* disabling existing user accounts -* adding group memberships -* removing group memberships -* rejecting group membership requests -""" - - -import flask -import flask_login -import flask_principal -import flask_mail -from flask_babel import gettext, lazy_gettext, force_locale -from sqlalchemy import or_ - -import vial.const -import vial.acl -from vial.app import VialBlueprint -from vial.view import ItemListView, ItemShowView, ItemCreateView, ItemUpdateView, ItemDeleteView, ItemEnableView, ItemDisableView, ItemObjectRelationView -from vial.view.mixin import HTMLMixin, SQLAlchemyMixin -from vial.blueprints.users.forms import CreateUserAccountForm, UpdateUserAccountForm, AdminUpdateUserAccountForm, UserSearchForm - - -BLUEPRINT_NAME = 'users' -"""Name of the blueprint as module global constant.""" - - -class ListView(HTMLMixin, SQLAlchemyMixin, ItemListView): - """ - General user account listing. - """ - methods = ['GET'] - - authentication = True - - authorization = [vial.acl.PERMISSION_POWER] - - @classmethod - def get_view_title(cls, **kwargs): - return lazy_gettext('User management') - - @property - def dbmodel(self): - return self.get_model(vial.const.MODEL_USER) - - @classmethod - def get_action_menu(cls): - action_menu = vial.menu.Menu() - action_menu.add_entry( - 'endpoint', - 'create', - endpoint = 'users.create', - resptitle = True - ) - return action_menu - - @classmethod - def get_context_action_menu(cls): - action_menu = vial.menu.Menu() - action_menu.add_entry( - 'endpoint', - 'show', - endpoint = 'users.show', - hidetitle = True - ) - action_menu.add_entry( - 'endpoint', - 'update', - endpoint = 'users.update', - hidetitle = True - ) - action_menu.add_entry( - 'endpoint', - 'disable', - endpoint = 'users.disable', - hidetitle = True - ) - action_menu.add_entry( - 'endpoint', - 'enable', - endpoint = 'users.enable', - hidetitle = True - ) - action_menu.add_entry( - 'endpoint', - 'delete', - endpoint = 'users.delete', - hidetitle = True - ) - return action_menu - - @staticmethod - def get_search_form(request_args): - """ - Must return instance of :py:mod:`flask_wtf.FlaskForm` appropriate for - searching given type of items. - """ - roles = list(zip(flask.current_app.config['ROLES'], flask.current_app.config['ROLES'])) - return UserSearchForm( - request_args, - meta = {'csrf': False}, - choices_roles = roles - ) - - @staticmethod - def build_query(query, model, form_args): - # Adjust query based on text search string. - if 'search' in form_args and form_args['search']: - query = query\ - .filter( - or_( - model.login.like('%{}%'.format(form_args['search'])), - model.fullname.like('%{}%'.format(form_args['search'])), - model.email.like('%{}%'.format(form_args['search'])), - ) - ) - # Adjust query based on lower time boudary selection. - if 'dt_from' in form_args and form_args['dt_from']: - query = query.filter(model.createtime >= form_args['dt_from']) - # Adjust query based on upper time boudary selection. - if 'dt_to' in form_args and form_args['dt_to']: - query = query.filter(model.createtime <= form_args['dt_to']) - # Adjust query based on user state selection. - if 'state' in form_args and form_args['state']: - if form_args['state'] == 'enabled': - query = query.filter(model.enabled == True) - elif form_args['state'] == 'disabled': - query = query.filter(model.enabled == False) - # Adjust query based on user role selection. - if 'role' in form_args and form_args['role']: - if form_args['role'] == vial.const.NO_ROLE: - query = query.filter(model.roles == []) - else: - query = query.filter(model.roles.any(form_args['role'])) - # Adjust query based on user membership selection. - if 'membership' in form_args and form_args['membership']: - query = query\ - .join(model.memberships)\ - .filter(model.memberships.any(id = form_args['membership'].id)) - # Adjust query based on user management selection. - if 'management' in form_args and form_args['management']: - query = query\ - .join(model.managements)\ - .filter(model.managements.any(id = form_args['management'].id)) - if 'sortby' in form_args and form_args['sortby']: - sortmap = { - 'createtime.desc': lambda x, y: x.order_by(y.createtime.desc()), - 'createtime.asc': lambda x, y: x.order_by(y.createtime.asc()), - 'login.desc': lambda x, y: x.order_by(y.login.desc()), - 'login.asc': lambda x, y: x.order_by(y.login.asc()), - 'fullname.desc': lambda x, y: x.order_by(y.fullname.desc()), - 'fullname.asc': lambda x, y: x.order_by(y.fullname.asc()), - 'email.desc': lambda x, y: x.order_by(y.email.desc()), - 'email.asc': lambda x, y: x.order_by(y.email.asc()), - 'logintime.desc': lambda x, y: x.order_by(y.logintime.desc()), - 'logintime.asc': lambda x, y: x.order_by(y.logintime.asc()), - } - query = sortmap[form_args['sortby']](query, model) - return query - - -class ShowView(HTMLMixin, SQLAlchemyMixin, ItemShowView): - """ - Detailed user account view. - """ - methods = ['GET'] - - authentication = True - - @classmethod - def get_view_icon(cls): - return 'action-show-user' - - @classmethod - def get_menu_legend(cls, **kwargs): - return lazy_gettext( - 'Show details of user account "%(item)s"', - item = flask.escape(kwargs['item'].login) - ) - - @classmethod - def get_view_title(cls, **kwargs): - return lazy_gettext('Show user account details') - - @property - def dbmodel(self): - return self.get_model(vial.const.MODEL_USER) - - @classmethod - def authorize_item_action(cls, **kwargs): - # Each user must be able to view his/her account. - permission_me = flask_principal.Permission( - flask_principal.UserNeed(kwargs['item'].id) - ) - # Managers of the groups the user is member of may view his/her account. - needs = [vial.acl.ManagementNeed(x.id) for x in kwargs['item'].memberships] - permission_mngr = flask_principal.Permission(*needs) - return vial.acl.PERMISSION_POWER.can() or permission_mngr.can() or permission_me.can() - - @classmethod - def get_action_menu(cls): - action_menu = vial.menu.Menu() - action_menu.add_entry( - 'endpoint', - 'update', - endpoint = 'users.update', - resptitle = True - ) - action_menu.add_entry( - 'endpoint', - 'disable', - endpoint = 'users.disable', - resptitle = True - ) - action_menu.add_entry( - 'endpoint', - 'enable', - endpoint = 'users.enable', - resptitle = True - ) - action_menu.add_entry( - 'endpoint', - 'delete', - endpoint = 'users.delete', - resptitle = True - ) - return action_menu - - def do_before_response(self, **kwargs): # pylint: disable=locally-disabled,no-self-use,unused-argument - item = self.response_context['item'] - action_menu = vial.menu.Menu() - action_menu.add_entry( - 'endpoint', - 'show', - endpoint = 'groups.show', - hidetitle = True - ) - action_menu.add_entry( - 'submenu', - 'more', - align_right = True, - legend = gettext('More actions') - ) - action_menu.add_entry( - 'endpoint', - 'more.add_member', - endpoint = 'groups.addmember' - ) - action_menu.add_entry( - 'endpoint', - 'more.reject_member', - endpoint = 'groups.rejectmember' - ) - action_menu.add_entry( - 'endpoint', - 'more.remove_member', - endpoint = 'groups.removemember' - ) - action_menu.add_entry( - 'endpoint', - 'more.add_manager', - endpoint = 'groups.addmanager' - ) - action_menu.add_entry( - 'endpoint', - 'more.remove_manager', - endpoint = 'groups.removemanager' - ) - action_menu.add_entry( - 'endpoint', - 'more.enable', - endpoint = 'groups.enable' - ) - action_menu.add_entry( - 'endpoint', - 'more.disable', - endpoint = 'groups.disable' - ) - action_menu.add_entry( - 'endpoint', - 'more.update', - endpoint = 'groups.update' - ) - self.response_context.update( - context_action_menu_groups = action_menu - ) - - if self.has_endpoint('changelogs.search'): - self.response_context.update( - context_action_menu_changelogs = self.get_endpoint_class( - 'changelogs.search' - ).get_context_action_menu() - ) - - if self.can_access_endpoint('users.update', item = item) and self.has_endpoint('changelogs.search'): - item_changelog_model = self.get_model(vial.const.MODEL_ITEM_CHANGELOG) - item_changelog = self.dbsession.query(item_changelog_model).\ - filter(item_changelog_model.model == item.__class__.__name__).\ - filter(item_changelog_model.model_id == item.id).\ - order_by(item_changelog_model.createtime.desc()).\ - limit(100).\ - all() - self.response_context.update( - item_changelog = item_changelog - ) - - user_changelog = self.dbsession.query(item_changelog_model).\ - filter(item_changelog_model.author_id == item.id).\ - order_by(item_changelog_model.createtime.desc()).\ - limit(100).\ - all() - self.response_context.update( - user_changelog = user_changelog - ) - - -class MeView(ShowView): # pylint: disable=locally-disabled,too-many-ancestors - """ - Detailed user account view for currently logged-in user (profile page). - """ - methods = ['GET'] - - authentication = True - - authorization = [vial.acl.PERMISSION_ANY] - - @classmethod - def get_view_name(cls): - return 'me' - - @classmethod - def get_view_icon(cls): - return 'profile' - - @classmethod - def get_menu_title(cls, **kwargs): - return lazy_gettext('My account') - - @classmethod - def get_view_url(cls, **kwargs): - return flask.url_for(cls.get_view_endpoint()) - - @classmethod - def get_view_title(cls, **kwargs): - return lazy_gettext('My user account') - - @classmethod - def get_view_template(cls): - return '{}/show.html'.format(BLUEPRINT_NAME) - - @classmethod - def authorize_item_action(cls, **kwargs): - return True - - @classmethod - def get_breadcrumbs_menu(cls): # pylint: disable=locally-disabled,unused-argument - action_menu = vial.menu.Menu() - action_menu.add_entry( - 'endpoint', - 'home', - endpoint = flask.current_app.config['ENDPOINT_HOME'] - ) - action_menu.add_entry( - 'endpoint', - 'show', - endpoint = '{}.me'.format(cls.module_name) - ) - return action_menu - - def dispatch_request(self): # pylint: disable=locally-disabled,arguments-differ - """ - Mandatory interface required by the :py:func:`flask.views.View.dispatch_request`. - Will be called by the *Flask* framework to service the request. - - Single item with given unique identifier will be retrieved from database - and injected into template to be displayed to the user. - """ - item_id = flask_login.current_user.get_id() - item = self.dbquery().filter(self.dbmodel.id == item_id).first() - if not item: - self.abort(404) - - self.response_context.update( - item_id = item_id, - item = item, - breadcrumbs_menu = self.get_breadcrumbs_menu(), - action_menu = self.get_action_menu() - ) - - self.do_before_response() - - return self.generate_response() - - -class CreateView(HTMLMixin, SQLAlchemyMixin, ItemCreateView): # pylint: disable=locally-disabled,too-many-ancestors - """ - View for creating new user accounts. - """ - methods = ['GET','POST'] - - authentication = True - - authorization = [vial.acl.PERMISSION_POWER] - - @classmethod - def get_view_icon(cls): - return 'action-create-user' - - @classmethod - def get_view_title(cls, **kwargs): - return lazy_gettext('Create new user account') - - @property - def dbmodel(self): - return self.get_model(vial.const.MODEL_USER) - - @property - def dbchlogmodel(self): - return self.get_model(vial.const.MODEL_ITEM_CHANGELOG) - - @staticmethod - def get_message_success(**kwargs): - return gettext( - 'User account <strong>%(item_id)s</strong> was successfully created.', - item_id = flask.escape(str(kwargs['item'])) - ) - - @staticmethod - def get_message_failure(**kwargs): - return gettext('Unable to create new user account.') - - @staticmethod - def get_message_cancel(**kwargs): - return gettext('Canceled creating new user account.') - - @staticmethod - def get_item_form(item): - # - # Inject list of choices for supported locales and roles. Another approach - # would be to let the form get the list on its own, however that would create - # dependency on application object. - # - roles = list(zip(flask.current_app.config['ROLES'], flask.current_app.config['ROLES'])) - locales = list(flask.current_app.config['SUPPORTED_LOCALES'].items()) - - return CreateUserAccountForm( - choices_roles = roles, - choices_locales = locales - ) - - -class UpdateView(HTMLMixin, SQLAlchemyMixin, ItemUpdateView): # pylint: disable=locally-disabled,too-many-ancestors - """ - View for updating existing user accounts. - """ - methods = ['GET','POST'] - - authentication = True - - @classmethod - def get_view_icon(cls): - return 'action-update-user' - - @classmethod - def get_view_title(cls, **kwargs): - return lazy_gettext('Update user account details') - - @classmethod - def get_menu_legend(cls, **kwargs): - return lazy_gettext( - 'Update details of user account "%(item)s"', - item = flask.escape(kwargs['item'].login) - ) - - @property - def dbmodel(self): - return self.get_model(vial.const.MODEL_USER) - - @property - def dbchlogmodel(self): - return self.get_model(vial.const.MODEL_ITEM_CHANGELOG) - - @classmethod - def authorize_item_action(cls, **kwargs): - permission_me = flask_principal.Permission( - flask_principal.UserNeed(kwargs['item'].id) - ) - return vial.acl.PERMISSION_ADMIN.can() or permission_me.can() - - @staticmethod - def get_message_success(**kwargs): - return gettext( - 'User account <strong>%(item_id)s</strong> was successfully updated.', - item_id = flask.escape(str(kwargs['item'])) - ) - - @staticmethod - def get_message_failure(**kwargs): - return gettext( - 'Unable to update user account <strong>%(item_id)s</strong>.', - item_id = flask.escape(str(kwargs['item'])) - ) - - @staticmethod - def get_message_cancel(**kwargs): - return gettext( - 'Canceled updating user account <strong>%(item_id)s</strong>.', - item_id = flask.escape(str(kwargs['item'])) - ) - - @staticmethod - def get_item_form(item): - # - # Inject list of choices for supported locales and roles. Another approach - # would be to let the form get the list on its own, however that would create - # dependency on application object. - # - roles = list(zip(flask.current_app.config['ROLES'], flask.current_app.config['ROLES'])) - locales = list(flask.current_app.config['SUPPORTED_LOCALES'].items()) - - admin = flask_login.current_user.has_role('admin') - if not admin: - form = UpdateUserAccountForm( - choices_roles = roles, - choices_locales = locales, - obj = item - ) - else: - form = AdminUpdateUserAccountForm( - choices_roles = roles, - choices_locales = locales, - db_item_id = item.id, - obj = item - ) - return form - - -class AddMembershipView(HTMLMixin, SQLAlchemyMixin, ItemObjectRelationView): # pylint: disable=locally-disabled,too-many-ancestors - """ - View for adding group memberships. - """ - methods = ['GET','POST'] - - authentication = True - - @classmethod - def get_view_name(cls): - return 'addmembership' - - @classmethod - def get_view_title(cls, **kwargs): - return gettext('Add group membership') - - @classmethod - def get_view_icon(cls): - return 'action-add-member' - - @classmethod - def get_menu_legend(cls, **kwargs): - return lazy_gettext( - 'Add user "%(user_id)s" to group "%(group_id)s"', - user_id = flask.escape(str(kwargs['item'])), - group_id = flask.escape(str(kwargs['other'])) - ) - - @property - def dbmodel(self): - return self.get_model(vial.const.MODEL_USER) - - @property - def dbmodel_other(self): - return self.get_model(vial.const.MODEL_GROUP) - - @property - def dbchlogmodel(self): - return self.get_model(vial.const.MODEL_ITEM_CHANGELOG) - - @classmethod - def authorize_item_action(cls, **kwargs): - permission_m = flask_principal.Permission( - vial.acl.ManagementNeed(kwargs['other'].id) - ) - return vial.acl.PERMISSION_POWER.can() or permission_m.can() - - @classmethod - def validate_item_change(cls, **kwargs): # pylint: disable=locally-disabled,unused-argument - # Reject item change in case given item is already enabled. - if kwargs['other'] in kwargs['item'].memberships: - return False - return True - - @classmethod - def change_item(cls, **kwargs): - kwargs['item'].memberships.append(kwargs['other']) - try: - kwargs['item'].memberships_wanted.remove(kwargs['other']) - except ValueError: - pass - if kwargs['item'].is_state_disabled(): - kwargs['item'].set_state_enabled() - flask.current_app.send_infomail( - 'users.enable', - account = kwargs['item'] - ) - - @staticmethod - def get_message_success(**kwargs): - return gettext( - 'User <strong>%(user_id)s</strong> was successfully added as a member to group <strong>%(group_id)s</strong>.', - user_id = flask.escape(str(kwargs['item'])), - group_id = flask.escape(str(kwargs['other'])) - ) - - @staticmethod - def get_message_failure(**kwargs): - return gettext( - 'Unable to add user <strong>%(user_id)s</strong> as a member to group <strong>%(group_id)s</strong>.', - user_id = flask.escape(str(kwargs['item'])), - group_id = flask.escape(str(kwargs['other'])) - ) - - @staticmethod - def get_message_cancel(**kwargs): - return gettext( - 'Canceled adding user <strong>%(user_id)s</strong> as a member to group <strong>%(group_id)s</strong>.', - user_id = flask.escape(str(kwargs['item'])), - group_id = flask.escape(str(kwargs['other'])) - ) - - -class RejectMembershipView(HTMLMixin, SQLAlchemyMixin, ItemObjectRelationView): # pylint: disable=locally-disabled,too-many-ancestors - """ - View for rejecting group membership requests. - """ - methods = ['GET','POST'] - - authentication = True - - @classmethod - def get_view_name(cls): - return 'rejectmembership' - - @classmethod - def get_view_title(cls, **kwargs): - return gettext('Reject group membership') - - @classmethod - def get_view_icon(cls): - return 'action-rej-member' - - @classmethod - def get_menu_legend(cls, **kwargs): - return lazy_gettext( - 'Reject user`s "%(user_id)s" membership request for group "%(group_id)s"', - user_id = flask.escape(str(kwargs['item'])), - group_id = flask.escape(str(kwargs['other'])) - ) - - @property - def dbmodel(self): - return self.get_model(vial.const.MODEL_USER) - - @property - def dbmodel_other(self): - return self.get_model(vial.const.MODEL_GROUP) - - @property - def dbchlogmodel(self): - return self.get_model(vial.const.MODEL_ITEM_CHANGELOG) - - @classmethod - def authorize_item_action(cls, **kwargs): - permission_m = flask_principal.Permission( - vial.acl.ManagementNeed(kwargs['other'].id) - ) - return vial.acl.PERMISSION_POWER.can() or permission_m.can() - - @classmethod - def validate_item_change(cls, **kwargs): # pylint: disable=locally-disabled,unused-argument - # Reject item change in case given item is already enabled. - if kwargs['other'] not in kwargs['item'].memberships_wanted: - return False - return True - - @classmethod - def change_item(cls, **kwargs): - kwargs['item'].memberships_wanted.remove(kwargs['other']) - - @staticmethod - def get_message_success(**kwargs): - return gettext( - 'User`s <strong>%(user_id)s</strong> membership request for group <strong>%(group_id)s</strong> was successfully rejected.', - user_id = flask.escape(str(kwargs['item'])), - group_id = flask.escape(str(kwargs['other'])) - ) - - @staticmethod - def get_message_failure(**kwargs): - return gettext( - 'Unable to reject user`s <strong>%(user_id)s</strong> membership request for group <strong>%(group_id)s</strong>.', - user_id = flask.escape(str(kwargs['item'])), - group_id = flask.escape(str(kwargs['other'])) - ) - - @staticmethod - def get_message_cancel(**kwargs): - return gettext( - 'Canceled rejecting user`s <strong>%(user_id)s</strong> membership request for group <strong>%(group_id)s</strong>.', - user_id = flask.escape(str(kwargs['item'])), - group_id = flask.escape(str(kwargs['other'])) - ) - - -class RemoveMembershipView(HTMLMixin, SQLAlchemyMixin, ItemObjectRelationView): # pylint: disable=locally-disabled,too-many-ancestors - """ - View for removing group memberships. - """ - methods = ['GET','POST'] - - authentication = True - - @classmethod - def get_view_name(cls): - return 'removemembership' - - @classmethod - def get_view_title(cls, **kwargs): - return gettext('Remove group membership') - - @classmethod - def get_view_icon(cls): - return 'action-rem-member' - - @classmethod - def get_menu_legend(cls, **kwargs): - return lazy_gettext( - 'Remove user "%(user_id)s" from group "%(group_id)s"', - user_id = flask.escape(str(kwargs['item'])), - group_id = flask.escape(str(kwargs['other'])) - ) - - @property - def dbmodel(self): - return self.get_model(vial.const.MODEL_USER) - - @property - def dbmodel_other(self): - return self.get_model(vial.const.MODEL_GROUP) - - @property - def dbchlogmodel(self): - return self.get_model(vial.const.MODEL_ITEM_CHANGELOG) - - @classmethod - def authorize_item_action(cls, **kwargs): - permission_m = flask_principal.Permission( - vial.acl.ManagementNeed(kwargs['other'].id) - ) - return vial.acl.PERMISSION_POWER.can() or permission_m.can() - - @classmethod - def validate_item_change(cls, **kwargs): # pylint: disable=locally-disabled,unused-argument - # Reject item change in case given item is already enabled. - if kwargs['other'] not in kwargs['item'].memberships: - return False - return True - - @classmethod - def change_item(cls, **kwargs): - try: - kwargs['item'].memberships.remove(kwargs['other']) - except ValueError: - pass - - @staticmethod - def get_message_success(**kwargs): - return gettext( - 'User <strong>%(user_id)s</strong> was successfully removed as a member from group <strong>%(group_id)s</strong>.', - user_id = flask.escape(str(kwargs['item'])), - group_id = flask.escape(str(kwargs['other'])) - ) - - @staticmethod - def get_message_failure(**kwargs): - return gettext( - 'Unable to remove user <strong>%(user_id)s</strong> as a member from group <strong>%(group_id)s</strong>.', - user_id = flask.escape(str(kwargs['item'])), - group_id = flask.escape(str(kwargs['other'])) - ) - - @staticmethod - def get_message_cancel(**kwargs): - return gettext( - 'Canceled removing user <strong>%(user_id)s</strong> as a member from group <strong>%(group_id)s</strong>.', - user_id = flask.escape(str(kwargs['item'])), - group_id = flask.escape(str(kwargs['other'])) - ) - - -class AddManagementView(HTMLMixin, SQLAlchemyMixin, ItemObjectRelationView): # pylint: disable=locally-disabled,too-many-ancestors - """ - View for adding group managements. - """ - methods = ['GET','POST'] - - authentication = True - - @classmethod - def get_view_name(cls): - return 'addmanagement' - - @classmethod - def get_view_title(cls, **kwargs): - return gettext('Add group management') - - @classmethod - def get_view_icon(cls): - return 'action-add-manager' - - @classmethod - def get_menu_legend(cls, **kwargs): - return lazy_gettext( - 'Add user "%(user_id)s" to group "%(group_id)s" as manager', - user_id = flask.escape(str(kwargs['item'])), - group_id = flask.escape(str(kwargs['other'])) - ) - - @property - def dbmodel(self): - return self.get_model(vial.const.MODEL_USER) - - @property - def dbmodel_other(self): - return self.get_model(vial.const.MODEL_GROUP) - - @property - def dbchlogmodel(self): - return self.get_model(vial.const.MODEL_ITEM_CHANGELOG) - - @classmethod - def authorize_item_action(cls, **kwargs): - permission_m = flask_principal.Permission( - vial.acl.ManagementNeed(kwargs['other'].id) - ) - return vial.acl.PERMISSION_POWER.can() or permission_m.can() - - @classmethod - def validate_item_change(cls, **kwargs): # pylint: disable=locally-disabled,unused-argument - # Reject item change in case given item is already manager. - if kwargs['other'] in kwargs['item'].managements: - return False - return True - - @classmethod - def change_item(cls, **kwargs): - kwargs['item'].managements.append(kwargs['other']) - if kwargs['item'].is_state_disabled(): - kwargs['item'].set_state_enabled() - flask.current_app.send_infomail( - 'users.enable', - account = kwargs['item'] - ) - - @staticmethod - def get_message_success(**kwargs): - return gettext( - 'User <strong>%(user_id)s</strong> was successfully added as a manager to group <strong>%(group_id)s</strong>.', - user_id = flask.escape(str(kwargs['item'])), - group_id = flask.escape(str(kwargs['other'])) - ) - - @staticmethod - def get_message_failure(**kwargs): - return gettext( - 'Unable to add user <strong>%(user_id)s</strong> as a manager to group <strong>%(group_id)s</strong>.', - user_id = flask.escape(str(kwargs['item'])), - group_id = flask.escape(str(kwargs['other'])) - ) - - @staticmethod - def get_message_cancel(**kwargs): - return gettext( - 'Canceled adding user <strong>%(user_id)s</strong> as a manager to group <strong>%(group_id)s</strong>.', - user_id = flask.escape(str(kwargs['item'])), - group_id = flask.escape(str(kwargs['other'])) - ) - - -class RemoveManagementView(HTMLMixin, SQLAlchemyMixin, ItemObjectRelationView): # pylint: disable=locally-disabled,too-many-ancestors - """ - View for removing group managements. - """ - methods = ['GET','POST'] - - authentication = True - - @classmethod - def get_view_name(cls): - return 'removemanagement' - - @classmethod - def get_view_title(cls, **kwargs): - return gettext('Remove group management') - - @classmethod - def get_view_icon(cls): - return 'action-rem-manager' - - @classmethod - def get_menu_legend(cls, **kwargs): - return lazy_gettext( - 'Remove user "%(user_id)s" from group "%(group_id)s" as manager', - user_id = flask.escape(str(kwargs['item'])), - group_id = flask.escape(str(kwargs['other'])) - ) - - @property - def dbmodel(self): - return self.get_model(vial.const.MODEL_USER) - - @property - def dbmodel_other(self): - return self.get_model(vial.const.MODEL_GROUP) - - @property - def dbchlogmodel(self): - return self.get_model(vial.const.MODEL_ITEM_CHANGELOG) - - @classmethod - def authorize_item_action(cls, **kwargs): - permission_m = flask_principal.Permission( - vial.acl.ManagementNeed(kwargs['other'].id) - ) - return vial.acl.PERMISSION_POWER.can() or permission_m.can() - - @classmethod - def validate_item_change(cls, **kwargs): # pylint: disable=locally-disabled,unused-argument - # Reject item change in case given item is not already manager. - if kwargs['other'] not in kwargs['item'].managements: - return False - return True - - @classmethod - def change_item(cls, **kwargs): - try: - kwargs['item'].managements.remove(kwargs['other']) - except ValueError: - pass - - @staticmethod - def get_message_success(**kwargs): - return gettext( - 'User <strong>%(user_id)s</strong> was successfully removed as a manager from group <strong>%(group_id)s</strong>.', - user_id = flask.escape(str(kwargs['item'])), - group_id = flask.escape(str(kwargs['other'])) - ) - - @staticmethod - def get_message_failure(**kwargs): - return gettext( - 'Unable to remove user <strong>%(user_id)s</strong> as a manager from group <strong>%(group_id)s</strong>.', - user_id = flask.escape(str(kwargs['item'])), - group_id = flask.escape(str(kwargs['other'])) - ) - - @staticmethod - def get_message_cancel(**kwargs): - return gettext( - 'Canceled removing user <strong>%(user_id)s</strong> as a manager from group <strong>%(group_id)s</strong>.', - user_id = flask.escape(str(kwargs['item'])), - group_id = flask.escape(str(kwargs['other'])) - ) - - -class EnableView(HTMLMixin, SQLAlchemyMixin, ItemEnableView): # pylint: disable=locally-disabled,too-many-ancestors - """ - View for enabling existing user accounts. - """ - methods = ['GET','POST'] - - authentication = True - - authorization = [vial.acl.PERMISSION_POWER] - - @classmethod - def get_view_icon(cls): - return 'action-enable-user' - - @classmethod - def get_menu_legend(cls, **kwargs): - return lazy_gettext( - 'Enable user account "%(item)s"', - item = flask.escape(kwargs['item'].login) - ) - - @property - def dbmodel(self): - return self.get_model(vial.const.MODEL_USER) - - @property - def dbchlogmodel(self): - return self.get_model(vial.const.MODEL_ITEM_CHANGELOG) - - @staticmethod - def get_message_success(**kwargs): - return gettext( - 'User account <strong>%(item_id)s</strong> was successfully enabled.', - item_id = flask.escape(str(kwargs['item'])) - ) - - @staticmethod - def get_message_failure(**kwargs): - return gettext( - 'Unable to enable user account <strong>%(item_id)s</strong>.', - item_id = flask.escape(str(kwargs['item'])) - ) - - @staticmethod - def get_message_cancel(**kwargs): - return gettext( - 'Canceled enabling user account <strong>%(item_id)s</strong>.', - item_id = flask.escape(str(kwargs['item'])) - ) - - @classmethod - def inform_user(cls, account): - """Send infomail about user account activation.""" - mail_locale = account.locale - if not mail_locale: - mail_locale = flask.current_app.config['BABEL_DEFAULT_LOCALE'] - - with force_locale(mail_locale): - msg = flask_mail.Message( - gettext( - "[%(app_name)s] Account activation - %(item_id)s", - app_name = flask.current_app.config['APPLICATION_NAME'], - item_id = account.login - ), - recipients = [account.email], - bcc = flask.current_app.config['EMAIL_ADMINS'] - ) - msg.body = flask.render_template( - 'users/email_activation.txt', - account = account - ) - flask.current_app.mailer.send(msg) - - def do_after_action(self, item): - self.inform_user(item) - - -class DisableView(HTMLMixin, SQLAlchemyMixin, ItemDisableView): # pylint: disable=locally-disabled,too-many-ancestors - """ - View for disabling user accounts. - """ - methods = ['GET','POST'] - - authentication = True - - authorization = [vial.acl.PERMISSION_POWER] - - @classmethod - def get_view_icon(cls): - return 'action-disable-user' - - @classmethod - def get_menu_legend(cls, **kwargs): - return lazy_gettext( - 'Disable user account "%(item)s"', - item = flask.escape(kwargs['item'].login) - ) - - @property - def dbmodel(self): - return self.get_model(vial.const.MODEL_USER) - - @property - def dbchlogmodel(self): - return self.get_model(vial.const.MODEL_ITEM_CHANGELOG) - - @staticmethod - def get_message_success(**kwargs): - return gettext( - 'User account <strong>%(item_id)s</strong> was successfully disabled.', - item_id = flask.escape(str(kwargs['item'])) - ) - - @staticmethod - def get_message_failure(**kwargs): - return gettext( - 'Unable to disable user account <strong>%(item_id)s</strong>.', - item_id = flask.escape(str(kwargs['item'])) - ) - - @staticmethod - def get_message_cancel(**kwargs): - return gettext( - 'Canceled disabling user account <strong>%(item_id)s</strong>.', - item_id = flask.escape(str(kwargs['item'])) - ) - - -class DeleteView(HTMLMixin, SQLAlchemyMixin, ItemDeleteView): # pylint: disable=locally-disabled,too-many-ancestors - """ - View for deleting existing user accounts. - """ - methods = ['GET','POST'] - - authentication = True - - authorization = [vial.acl.PERMISSION_ADMIN] - - @classmethod - def get_view_icon(cls): - return 'action-delete-user' - - @classmethod - def get_menu_legend(cls, **kwargs): - return lazy_gettext( - 'Delete user account "%(item)s"', - item = flask.escape(kwargs['item'].login) - ) - - @property - def dbmodel(self): - return self.get_model(vial.const.MODEL_USER) - - @property - def dbchlogmodel(self): - return self.get_model(vial.const.MODEL_ITEM_CHANGELOG) - - @staticmethod - def get_message_success(**kwargs): - return gettext( - 'User account <strong>%(item_id)s</strong> was successfully and permanently deleted.', - item_id = flask.escape(str(kwargs['item'])) - ) - - @staticmethod - def get_message_failure(**kwargs): - return gettext( - 'Unable to delete user account <strong>%(item_id)s</strong>.', - item_id = flask.escape(str(kwargs['item'])) - ) - - @staticmethod - def get_message_cancel(**kwargs): - return gettext( - 'Canceled deleting user account <strong>%(item_id)s</strong>.', - item_id = flask.escape(str(kwargs['item'])) - ) - - -#------------------------------------------------------------------------------- - - -class UsersBlueprint(VialBlueprint): - """Pluggable module - user account management (*users*).""" - - @classmethod - def get_module_title(cls): - return lazy_gettext('User account management') - - def register_app(self, app): - app.menu_main.add_entry( - 'view', - 'admin.{}'.format(BLUEPRINT_NAME), - position = 40, - group = lazy_gettext('Object management'), - view = ListView - ) - app.menu_auth.add_entry( - 'view', - 'my_account', - position = 10, - view = MeView, - params = lambda: {'item': flask_login.current_user} - ) - - app.set_infomailer('users.enable', EnableView.inform_user) - - -#------------------------------------------------------------------------------- - - -def get_blueprint(): - """ - Mandatory interface for :py:mod:`vial.Vial` and factory function. This function - must return a valid instance of :py:class:`vial.app.VialBlueprint` or - :py:class:`flask.Blueprint`. - """ - - hbp = UsersBlueprint( - BLUEPRINT_NAME, - __name__, - template_folder = 'templates', - url_prefix = '/{}'.format(BLUEPRINT_NAME) - ) - - hbp.register_view_class(ListView, '/list') - hbp.register_view_class(CreateView, '/create') - hbp.register_view_class(ShowView, '/<int:item_id>/show') - hbp.register_view_class(MeView, '/me') - hbp.register_view_class(UpdateView, '/<int:item_id>/update') - hbp.register_view_class(AddMembershipView, '/<int:item_id>/add_membership/<int:other_id>') - hbp.register_view_class(RemoveMembershipView, '/<int:item_id>/remove_membership/<int:other_id>') - hbp.register_view_class(RejectMembershipView, '/<int:item_id>/reject_membership/<int:other_id>') - hbp.register_view_class(AddManagementView, '/<int:item_id>/add_management/<int:other_id>') - hbp.register_view_class(RemoveManagementView, '/<int:item_id>/remove_management/<int:other_id>') - hbp.register_view_class(EnableView, '/<int:item_id>/enable') - hbp.register_view_class(DisableView, '/<int:item_id>/disable') - hbp.register_view_class(DeleteView, '/<int:item_id>/delete') - - return hbp diff --git a/lib/vial/blueprints/users/forms.py b/lib/vial/blueprints/users/forms.py deleted file mode 100644 index 0994537eca6768a43951ab04a6f72e2ed49296a2..0000000000000000000000000000000000000000 --- a/lib/vial/blueprints/users/forms.py +++ /dev/null @@ -1,284 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- -# Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- - - -""" -This module contains custom user account management forms for Hawat. -""" - - -import pytz -import wtforms -from wtforms.ext.sqlalchemy.fields import QuerySelectField, QuerySelectMultipleField -from flask_babel import lazy_gettext - -import vial.db -import vial.const -import vial.forms -from vial.forms import check_login, check_unique_login, get_available_groups - - -class BaseUserAccountForm(vial.forms.BaseItemForm): - """ - Class representing base user account form. - """ - fullname = wtforms.StringField( - lazy_gettext('Full name:'), - validators = [ - wtforms.validators.DataRequired(), - wtforms.validators.Length(min = 3, max = 100) - ] - ) - email = wtforms.StringField( - lazy_gettext('Email:'), - validators = [ - wtforms.validators.DataRequired(), - wtforms.validators.Length(min = 3, max = 250), - wtforms.validators.Email() - ] - ) - locale = vial.forms.SelectFieldWithNone( - lazy_gettext('Prefered locale:'), - validators = [ - wtforms.validators.Optional() - ], - choices = [('', lazy_gettext('<< no preference >>'))], - filters = [lambda x: x or None], - default = '' - ) - timezone = vial.forms.SelectFieldWithNone( - lazy_gettext('Prefered timezone:'), - validators = [ - wtforms.validators.Optional(), - ], - choices = [('', lazy_gettext('<< no preference >>'))] + list(zip(pytz.common_timezones, pytz.common_timezones)), - filters = [lambda x: x or None], - default = '' - ) - submit = wtforms.SubmitField( - lazy_gettext('Submit') - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - # - # Handle additional custom keywords. - # - - # The list of choices for 'locale' attribute comes from outside of the - # form to provide as loose tie as possible to the outer application. - # Another approach would be to load available choices here with: - # - # locales = list(flask.current_app.config['SUPPORTED_LOCALES'].items()) - # - # That would mean direct dependency on flask.Flask application. - self.locale.choices[1:] = kwargs['choices_locales'] - - -class AdminUserAccountForm(BaseUserAccountForm): - """ - Class representing base user account form for admins. - """ - enabled = wtforms.RadioField( - lazy_gettext('State:'), - validators = [ - wtforms.validators.InputRequired(), - ], - choices = [ - (True, lazy_gettext('Enabled')), - (False, lazy_gettext('Disabled')) - ], - filters = [vial.forms.str_to_bool], - coerce = vial.forms.str_to_bool - ) - roles = wtforms.SelectMultipleField( - lazy_gettext('Roles:'), - validators = [ - wtforms.validators.Optional() - ] - ) - memberships = QuerySelectMultipleField( - lazy_gettext('Group memberships:'), - query_factory = get_available_groups - ) - managements = QuerySelectMultipleField( - lazy_gettext('Group managements:'), - query_factory = get_available_groups - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - # - # Handle additional custom keywords. - # - - # The list of choices for 'roles' attribute comes from outside of the - # form to provide as loose tie as possible to the outer application. - # Another approach would be to load available choices here with: - # - # roles = flask.current_app.config['ROLES'] - # - # That would mean direct dependency on flask.Flask application. - self.roles.choices = kwargs['choices_roles'] - - -class CreateUserAccountForm(AdminUserAccountForm): - """ - Class representing user account create form. - """ - login = wtforms.StringField( - lazy_gettext('Login:'), - validators = [ - wtforms.validators.DataRequired(), - wtforms.validators.Length(min = 3, max = 50), - check_login, - check_unique_login - ] - ) - - -class UpdateUserAccountForm(BaseUserAccountForm): - """ - Class representing user account update form for regular users. - """ - - -class AdminUpdateUserAccountForm(AdminUserAccountForm): - """ - Class representing user account update form for administrators. - """ - login = wtforms.StringField( - lazy_gettext('Login:'), - validators = [ - wtforms.validators.DataRequired(), - wtforms.validators.Length(min = 3, max = 50), - vial.forms.check_login, - check_unique_login - ] - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - # - # Handle additional custom keywords. - # - - # Store the ID of original item in database to enable the ID uniqueness - # check with check_id_uniqueness() validator. - self.db_item_id = kwargs['db_item_id'] - - -class UserSearchForm(vial.forms.BaseSearchForm): - """ - Class representing simple user search form. - """ - search = wtforms.StringField( - lazy_gettext('Login, name, email:'), - validators = [ - wtforms.validators.Optional(), - wtforms.validators.Length(min = 3, max = 100) - ], - description = lazy_gettext('User`s login, full name or email address. Search is performed even in the middle of the strings, so for example you may lookup by domain.') - ) - dt_from = vial.forms.SmartDateTimeField( - lazy_gettext('Creation time from:'), - validators = [ - wtforms.validators.Optional() - ], - description = lazy_gettext('Lower time boundary for item creation time. Timestamp is expected to be in the format <code>YYYY-MM-DD hh:mm:ss</code> and in the timezone according to the user`s preferences.') - ) - dt_to = vial.forms.SmartDateTimeField( - lazy_gettext('Creation time to:'), - validators = [ - wtforms.validators.Optional() - ], - description = lazy_gettext('Upper time boundary for item creation time. Timestamp is expected to be in the format <code>YYYY-MM-DD hh:mm:ss</code> and in the timezone according to the user`s preferences.') - ) - - state = wtforms.SelectField( - lazy_gettext('State:'), - validators = [ - wtforms.validators.Optional(), - ], - choices = [ - ('', lazy_gettext('Nothing selected')), - ('enabled', lazy_gettext('Enabled')), - ('disabled', lazy_gettext('Disabled')) - ], - default = '', - description = lazy_gettext('Search for users with particular account state.') - ) - role = wtforms.SelectField( - lazy_gettext('Role:'), - validators = [ - wtforms.validators.Optional() - ], - default = '', - description = lazy_gettext('Search for users with particular role, or without any assigned roles.') - ) - membership = QuerySelectField( - lazy_gettext('Group membership:'), - query_factory = get_available_groups, - allow_blank = True, - description = lazy_gettext('Search for users with membership with particular group.') - ) - management = QuerySelectField( - lazy_gettext('Group management:'), - query_factory = get_available_groups, - allow_blank = True, - description = lazy_gettext('Search for users with management rights to particular group.') - ) - - sortby = wtforms.SelectField( - lazy_gettext('Sort result by:'), - validators = [ - wtforms.validators.Optional() - ], - choices = [ - ('createtime.desc', lazy_gettext('by creation time descending')), - ('createtime.asc', lazy_gettext('by creation time ascending')), - ('login.desc', lazy_gettext('by login descending')), - ('login.asc', lazy_gettext('by login ascending')), - ('fullname.desc', lazy_gettext('by name descending')), - ('fullname.asc', lazy_gettext('by name ascending')), - ('email.desc', lazy_gettext('by email descending')), - ('email.asc', lazy_gettext('by email ascending')), - ('logintime.desc', lazy_gettext('by login time descending')), - ('logintime.asc', lazy_gettext('by login time ascending')), - ], - default = 'fullname.asc' - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - # - # Handle additional custom keywords. - # - - # The list of choices for 'roles' attribute comes from outside of the - # form to provide as loose tie as possible to the outer application. - # Another approach would be to load available choices here with: - # - # roles = flask.current_app.config['ROLES'] - # - # That would mean direct dependency on flask.Flask application. - self.role.choices = [ - ('', lazy_gettext('Nothing selected')), - (vial.const.NO_ROLE, lazy_gettext('Without any roles')) - ] + kwargs['choices_roles'] - - @staticmethod - def is_multivalue(field_name): - """ - Check, if given form field is a multivalue field. - - :param str field_name: Name of the form field. - :return: ``True``, if the field can contain multiple values, ``False`` otherwise. - :rtype: bool - """ - return False diff --git a/lib/vial/blueprints/users/templates/users/addmembership.html b/lib/vial/blueprints/users/templates/users/addmembership.html deleted file mode 100644 index 75fc075cff7920ef39e660e1def0987e0233a94c..0000000000000000000000000000000000000000 --- a/lib/vial/blueprints/users/templates/users/addmembership.html +++ /dev/null @@ -1,16 +0,0 @@ -{%- extends "_layout_confirmation.html" %} - -{%- block modalcontent %} - <p> - {{ _('Are you really sure you want to add user') }} - </p> - <p class="lead text-center"> - {{ item_name }} - </p> - <p> - {{ _('to abuse group') }} - </p> - <p class="lead text-center"> - {{ other_name }} - </p> -{% endblock modalcontent %} diff --git a/lib/vial/blueprints/users/templates/users/creatupdate.html b/lib/vial/blueprints/users/templates/users/creatupdate.html deleted file mode 100644 index 2f97a58bb3a8b9f9fa4047236660578ca67c42a6..0000000000000000000000000000000000000000 --- a/lib/vial/blueprints/users/templates/users/creatupdate.html +++ /dev/null @@ -1,38 +0,0 @@ -{%- extends "_layout_creatupdate.html" %} - -{%- block itemform_fields %} - - {%- if item_action == 'create' or current_user.has_role('admin') %} - - {{ macros_form.render_form_item_default(form.login) }} - - {%- elif item_action == 'update' %} - - {{ macros_form.render_form_item_static(_('Login:'), item.login) }} - - {%- endif %} - - {{ macros_form.render_form_item_default(form.fullname) }} - - {{ macros_form.render_form_item_default(form.email) }} - - {%- if item_action == 'create' or current_user.has_role('admin') %} - - <hr> - - {{ macros_form.render_form_item_radiobutton(form.enabled) }} - - {{ macros_form.render_form_item_select(form.roles) }} - - {{ macros_form.render_form_item_select(form.memberships) }} - - {{ macros_form.render_form_item_select(form.managements) }} - {%- endif %} - - <hr> - - {{ macros_form.render_form_item_select(form.locale) }} - - {{ macros_form.render_form_item_select(form.timezone) }} - -{%- endblock itemform_fields %} diff --git a/lib/vial/blueprints/users/templates/users/email_activation.txt b/lib/vial/blueprints/users/templates/users/email_activation.txt deleted file mode 100644 index df918e00e617da3a03fc96cc249b70bc3f246fe2..0000000000000000000000000000000000000000 --- a/lib/vial/blueprints/users/templates/users/email_activation.txt +++ /dev/null @@ -1,9 +0,0 @@ -{{ _('Dear user,') | wordwrap }} - -{{ _('this email is a confirmation, that your account "%(item_id)s" in %(app_name)s was just activated. You may now login and start using the system:', item_id = account.login, app_name = vial_appname) | wordwrap }} - - {{ url_for('home.index', _external = True ) }} - -{{ _('Have a nice day') | wordwrap }} - --- {{ vial_appname }} diff --git a/lib/vial/blueprints/users/templates/users/list.html b/lib/vial/blueprints/users/templates/users/list.html deleted file mode 100644 index 732c1f334acb3056413b659aa061effd05903855..0000000000000000000000000000000000000000 --- a/lib/vial/blueprints/users/templates/users/list.html +++ /dev/null @@ -1,78 +0,0 @@ -{%- extends "_layout_list.html" %} - -{%- block searchformfields %} - - <div class="row"> - <div class="col-sm-4"> - {{ macros_form.render_form_item_default(g.search_form.search) }} - </div> - <div class="col-sm-4"> - {{ macros_form.render_form_item_datetime(g.search_form.dt_from, 'datetimepicker-hm-from') }} - </div> - <div class="col-sm-4"> - {{ macros_form.render_form_item_datetime(g.search_form.dt_to, 'datetimepicker-hm-to') }} - </div> - </div> - <div class="row"> - <div class="col-sm-3"> - {{ macros_form.render_form_item_select(g.search_form.state) }} - </div> - <div class="col-sm-3"> - {{ macros_form.render_form_item_select(g.search_form.role) }} - </div> - <div class="col-sm-3"> - {{ macros_form.render_form_item_select(g.search_form.membership) }} - </div> - <div class="col-sm-3"> - {{ macros_form.render_form_item_select(g.search_form.management) }} - </div> - </div> - -{%- endblock searchformfields %} - -{%- block contentinner %} - - <table class="table table-bordered table-hover"> - <thead> - <tr> - <th> - {{ _('Login') }} - </th> - <th> - {{ _('Name') }} - </th> - <th> - {{ _('Roles') }} - </th> - <th> - {{ _('State') }} - </th> - <th data-toggle="tooltip" title="{{ _('Contextual item actions') }}"> - {{ get_icon('actions') }} {{ _('Actions') }} - </th> - </tr> - </thead> - <tbody> - {%- for item in items %} - <tr{% if item.has_no_role() %} class="warning"{% elif item.has_role('admin') %} class="info"{% endif %}> - <td> - {{ item.login | default(_('<< unknown >>'), True) }} - </td> - <td> - {{ item.fullname | default(_('<< unknown >>'), True) }} - </td> - <td> - {{ macros_site.render_labels_role_list(config['ROLES'], item, True) }} - </td> - <td> - {{ macros_site.render_label_item_state(item.enabled, True) }} - </td> - <td class="column-actions"> - {{ macros_page.render_menu_context_actions(item) }} - </td> - </tr> - {%- endfor %} - </tbody> - </table> - -{%- endblock contentinner %} diff --git a/lib/vial/blueprints/users/templates/users/rejectmembership.html b/lib/vial/blueprints/users/templates/users/rejectmembership.html deleted file mode 100644 index ec04d6b5a1e9a4ea711b7e5091bbecd1b9438f89..0000000000000000000000000000000000000000 --- a/lib/vial/blueprints/users/templates/users/rejectmembership.html +++ /dev/null @@ -1,16 +0,0 @@ -{%- extends "_layout_confirmation.html" %} - -{%- block modalcontent %} - <p> - {{ _('Are you really sure you want to reject membership request of user') }} - </p> - <p class="lead text-center"> - {{ item_name }} - </p> - <p> - {{ _('for abuse group') }} - </p> - <p class="lead text-center"> - {{ other_name }} - </p> -{% endblock modalcontent %} diff --git a/lib/vial/blueprints/users/templates/users/removemembership.html b/lib/vial/blueprints/users/templates/users/removemembership.html deleted file mode 100644 index 1fa014985f2241050c3b21db684971c5694077aa..0000000000000000000000000000000000000000 --- a/lib/vial/blueprints/users/templates/users/removemembership.html +++ /dev/null @@ -1,16 +0,0 @@ -{%- extends "_layout_confirmation.html" %} - -{%- block modalcontent %} - <p> - {{ _('Are you really sure you want to remove user') }} - </p> - <p class="lead text-center"> - {{ item_name }} - </p> - <p> - {{ _('from abuse group') }} - </p> - <p class="lead text-center"> - {{ other_name }} - </p> -{% endblock modalcontent %} diff --git a/lib/vial/blueprints/users/templates/users/show.html b/lib/vial/blueprints/users/templates/users/show.html deleted file mode 100644 index dde19de1d2af91e68043e47b87f4941890644f12..0000000000000000000000000000000000000000 --- a/lib/vial/blueprints/users/templates/users/show.html +++ /dev/null @@ -1,281 +0,0 @@ -{% extends "_layout.html" %} - -{% block content %} - - <div class="row"> - <div class="col-lg-12"> - {{ macros_page.render_breadcrumbs(item) }} - - <h2>{{ vial_current_view.get_view_title() }}</h2> - <hr> - <h3>{{ item.fullname }} ({{ item.login }})</h3> - <div class="pull-right"> - {{ macros_page.render_menu_actions(item) }} - - </div> - <p> - <small> - <strong>{{ _('Account created') }}:</strong> {{ babel_format_datetime(item.createtime) }} ({{ _('%(delta)s ago', delta = babel_format_timedelta(current_datetime_utc - item.createtime)) }}) - | - <strong>{{ _('Last login') }}:</strong> {% if item.logintime %}{{ babel_format_datetime(item.logintime) }} ({{ _('%(delta)s ago', delta = babel_format_timedelta(current_datetime_utc - item.logintime)) }}){% else %}{{ _('never') }}{% endif %} - </small> - </p> - <br> - - <!-- Nav tabs --> - <ul class="nav nav-tabs" role="tablist"> - <li role="presentation" class="active"> - <a href="#tab-general" aria-controls="tab-general" role="tab" data-toggle="tab"> - <strong>{{ get_icon('alert-info') }} {{ _('General information') }}</strong> - </a> - </li> - {%- if can_access_endpoint('users.update', item) %} - <li role="presentation"> - <a href="#tab-actionlog" aria-controls="tab-actionlog" role="tab" data-toggle="tab"> - <strong>{{ get_icon('module-changelogs') }} {{ _('Actionlogs') }}</strong> <span class="badge">{{ user_changelog | length }}</span> - </a> - </li> - {%- endif %} - {%- if can_access_endpoint('users.update', item) %} - <li role="presentation"> - <a href="#tab-changelog" aria-controls="tab-changelog" role="tab" data-toggle="tab"> - <strong>{{ get_icon('module-changelogs') }} {{ _('Changelogs') }}</strong> <span class="badge">{{ item_changelog | length }}</span> - </a> - </li> - {%- endif %} - </ul> - - <!-- Tab panes --> - <div class="tab-content"> - - <div role="tabpanel" class="tab-pane fade in active" id="tab-general"> - <br> - <div class="row"> - - <div class="col-md-4"> - <h4>{{ _('Metadata') }}</h4> - <table class="table table-striped"> - <tbody> - <tr> - <th> - {{ _('Login') }}: - </th> - <td> - {{ item.login | default(_('<< unknown >>'), True) }} - </td> - </tr> - <tr> - <th> - {{ _('Full name') }}: - </th> - <td> - {{ item.fullname | default(_('<< unknown >>'), True) }} - </td> - </tr> - <tr> - <th> - {{ _('Email') }}: - </th> - <td> - {%- if item.email %} - <a href="mailto:{{ item.email }}">{{ item.email }}</a> - {%- else %} - {{ _('<< unknown >>') }} - {%- endif %} - </td> - </tr> - <tr> - <th> - {{ _('State') }}: - </th> - <td> - {{ macros_site.render_label_item_state(item.enabled, True) }} - </td> - </tr> - <tr> - <th> - {{ _('Roles') }}: - </th> - <td> - {{ macros_site.render_labels_role_list(config['ROLES'], item, True) }} - </td> - </tr> - {%- if can_access_endpoint('auth_api.key-generate', item) or item.apikey %} - <tr> - <th> - {{ _('API access key') }}: - </th> - <td> - {%- if item.apikey %} - <div class="input-group"> - {%- if is_it_me(item) %} - <input type="text" value="{{ item.apikey }}" readonly="readonly" class="form-control"> - {%- endif %} - <span class="input-group-btn"> - <a role="button" class="btn btn-default" href="{{ url_for('auth_api.key-delete', item_id = item.id, next = request.url) }}" data-toggle="tooltip" title="{{ get_endpoint_class('auth_api.key-delete').get_menu_legend() }}"> - {{ get_icon(get_endpoint_class('auth_api.key-delete').get_view_icon()) }}{% if not is_it_me(item) %} {{ get_endpoint_class('auth_api.key-delete').get_menu_title()}}{% endif %} - </a> - </span> - </div> - {%- else %} - <div class="input-group"> - <span class="input-group-btn"> - <a role="button" class="btn btn-default btn-sm" href="{{ url_for('auth_api.key-generate', item_id = item.id, next = request.url) }}" data-toggle="tooltip" title="{{ get_endpoint_class('auth_api.key-generate').get_menu_legend() }}"> - {{ get_icon(get_endpoint_class('auth_api.key-generate').get_view_icon()) }} {{ get_endpoint_class('auth_api.key-generate').get_menu_title()}} - </a> - </span> - </div> - {%- endif %} - </td> - </tr> - {%- endif %} - <tr> - <th> - {{ _('Prefered locale') }}: - </th> - <td> - {%- if item.locale %} - {{ get_country_flag(item.locale|upper) }} {{ babel_translate_locale(item.locale, True) }} ({{ item.locale }}) - {%- else %} - {{ _('<< system default >>') }} - {%- endif %} - </td> - </tr> - <tr> - <th> - {{ _('Prefered timezone') }}: - </th> - <td> - {%- if item.timezone %} - {{ item.timezone }} - {%- else %} - {{ _('<< system default >>') }} - {%- endif %} - </td> - </tr> - </tbody> - </table> - </div> - - <div class="col-md-4"> - {%- if item.memberships_wanted %} - <h4> - {{ _('Group membership requests') }} <span class="badge">{{ item.memberships_wanted | length }}</span> - </h4> - <table class="table table-striped"> - <tbody> - {%- for subitem in item.memberships_wanted %} - <tr> - <td> - {{ subitem.name }} - </td> - <td> - {{ macros_page.render_menu_context_actions(subitem, context_action_menu_groups, kwargs = {'other': item}) }} - </td> - </tr> - {%- endfor %} - </tbody> - </table> - {%- endif %} - <h4> - {{ _('Group memberships') }} <span class="badge">{{ item.memberships | length }}</span> - </h4> - {%- if item.memberships %} - <table class="table table-striped"> - <tbody> - {%- for subitem in item.memberships %} - <tr> - <td> - {{ subitem.name }} - </td> - <td> - {{ macros_page.render_menu_context_actions(subitem, context_action_menu_groups, kwargs = {'other': item}) }} - </td> - </tr> - {%- endfor %} - </tbody> - </table> - {%- else %} - {%- call macros_site.render_alert('info', False) %} - {{ _('This user is not member of any group at the moment.') }} - {%- endcall %} - {%- endif %} - </div> - - <div class="col-md-4"> - <h4>{{ _('Group managements') }} <span class="badge">{{ item.managements | length }}</span></h4> - {%- if item.managements %} - <table class="table table-striped"> - <tbody> - {%- for subitem in item.managements %} - <tr> - <td> - {{ subitem.name }} - </td> - <td> - {{ macros_page.render_menu_context_actions(subitem, context_action_menu_groups, kwargs = {'other': item}) }} - </td> - </tr> - {%- endfor %} - </tbody> - </table> - {%- else %} - {%- call macros_site.render_alert('info', False) %} - {{ _('This user is not manager of any group at the moment.') }} - {%- endcall %} - {%- endif %} - </div> - - </div> - </div> - - {%- if can_access_endpoint('users.update', item) %} - <div role="tabpanel" class="tab-pane fade" id="tab-actionlog"> - <br> - {%- if user_changelog %} - {{ macros_page.render_changelog_records(user_changelog, context_action_menu_changelogs, idprefix = 'actionlog', hide_author = True) }} - <p> - <small> - {{ _('Displaying only latest %(count)s actionlogs', count = 100) }} - </small> - </p> - {%- else %} - {%- call macros_site.render_alert('info', False) %} - {{ _('This user does not have any actionlog records at the moment.') }} - {%- endcall %} - {%- endif %} - </div> - {%- endif %} - - {%- if can_access_endpoint('users.update', item) %} - <div role="tabpanel" class="tab-pane fade" id="tab-changelog"> - <br> - {%- if item_changelog %} - {{ macros_page.render_changelog_records(item_changelog, context_action_menu_changelogs, hide_item = True) }} - <p> - <small> - {{ _('Displaying only latest %(count)s changelogs', count = 100) }} - </small> - </p> - {%- else %} - {%- call macros_site.render_alert('info', False) %} - {{ _('This object does not have any changelog records at the moment.') }} - {%- endcall %} - {%- endif %} - </div> - {%- endif %} - - </div> - - </div><!-- /.col-lg-12 --> - </div><!-- /.row --> - - {%- if permission_can('developer') %} - - <hr> - - {{ macros_site.render_raw_item_view(item) }} - - {%- endif %} - -{% endblock content %} diff --git a/lib/vial/blueprints/users/test/__init__.py b/lib/vial/blueprints/users/test/__init__.py deleted file mode 100644 index c86b1b3c9d374010e48be4fa24726b5e3aa67b73..0000000000000000000000000000000000000000 --- a/lib/vial/blueprints/users/test/__init__.py +++ /dev/null @@ -1,606 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- -# Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- - - -""" -Unit tests for :py:mod:`vial.blueprints.users`. -""" - - -import sys -import unittest - -import vial.const -import vial.db -import vial.test -import vial.test.fixtures -import vial.test.runner -from vial.test import VialTestCase, ItemCreateVialTestCase -from vial.test.runner import TestRunnerMixin -from vial.blueprints.users.test.utils import UsersTestCaseMixin - -_IS_NOSE = sys.argv[0].endswith('nosetests') - -@unittest.skipIf(_IS_NOSE, "broken under nosetest") -class UsersListTestCase(UsersTestCaseMixin, TestRunnerMixin, VialTestCase): - """Class for testing ``users.list`` endpoint.""" - - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) - def test_01_as_user(self): - """ - Test access as user ``user``. - - Only power user is able to list all available user accounts. - """ - self._attempt_fail_list() - - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) - def test_02_as_developer(self): - """ - Test access as user ``developer``. - - Only power user is able to list all available user accounts. - """ - self._attempt_fail_list() - - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) - def test_03_as_maintainer(self): - """ - Test access as user ``maintainer``. - - Only power user is able to list all available user accounts. - """ - self._attempt_succeed_list() - - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) - def test_04_as_admin(self): - """ - Test access as user ``admin``. - - Only power user is able to list all available user accounts. - """ - self._attempt_succeed_list() - - -@unittest.skipIf(_IS_NOSE, "broken under nosetest") -class UsersShowOwnTestCase(UsersTestCaseMixin, TestRunnerMixin, VialTestCase): - """ - Class for testing ``users.show`` endpoint: access to user`s own accounts. - - Each user must be able to access his own account. - """ - - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) - def test_01_as_user(self): - """Test access as user 'user'.""" - self._attempt_succeed_show(vial.const.ROLE_USER) - - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) - def test_02_as_developer(self): - """Test access as user 'developer'.""" - self._attempt_succeed_show(vial.const.ROLE_DEVELOPER) - - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) - def test_03_as_maintainer(self): - """Test access as user 'maintainer'.""" - self._attempt_succeed_show(vial.const.ROLE_MAINTAINER) - - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) - def test_04_as_admin(self): - """Test access as user 'admin'.""" - self._attempt_succeed_show(vial.const.ROLE_ADMIN) - - -@unittest.skipIf(_IS_NOSE, "broken under nosetest") -class UsersShowOtherTestCase(UsersTestCaseMixin, TestRunnerMixin, VialTestCase): - """Class for testing ``users.show`` endpoint: access to other user`s accounts.""" - - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) - def test_01_as_user_developer(self): - """ - Test access to 'developer' account as user 'user'. - - Regular user may view only his own account. - """ - self._attempt_fail_show(vial.const.ROLE_DEVELOPER) - - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) - def test_02_as_user_maintainer(self): - """ - Test access to 'maintainer' account as user 'user'. - - Regular user may view only his own account. - """ - self._attempt_fail_show(vial.const.ROLE_MAINTAINER) - - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) - def test_03_as_user_admin(self): - """ - Test access to 'admin' account as user 'user'. - - Regular user may view only his own account. - """ - self._attempt_fail_show(vial.const.ROLE_ADMIN) - - #-------------------------------------------------------------------------- - - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) - def test_04_as_developer_user(self): - """ - Test access to 'user' account as user 'developer'. - - Developer should be able to access because he is a manager of group of - which all other users are members. - """ - self._attempt_succeed_show(vial.const.ROLE_USER) - - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) - def test_05_as_developer_maintainer(self): - """ - Test access to 'maintainer' account as user 'developer'. - - Developer should be able to access because he is a manager of group of - which all other users are members. - """ - self._attempt_succeed_show(vial.const.ROLE_MAINTAINER) - - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) - def test_06_as_developer_admin(self): - """ - Test access to 'admin' account as user 'developer'. - - Developer should be able to access because he is a manager of group of - which all other users are members. - """ - self._attempt_succeed_show(vial.const.ROLE_ADMIN) - - #-------------------------------------------------------------------------- - - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) - def test_07_as_maintainer_user(self): - """ - Test access to 'user' account as user 'maintainer'. - - Maintainer should be allowed access, because he is a power user like admin. - """ - self._attempt_succeed_show(vial.const.ROLE_USER) - - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) - def test_08_as_maintainer_developer(self): - """ - Test access to 'developer' account as user 'maintainer'. - - Maintainer should be allowed access, because he is a power user like admin. - """ - self._attempt_succeed_show(vial.const.ROLE_DEVELOPER) - - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) - def test_09_as_maintainer_admin(self): - """ - Test access to 'maintainer' account as user 'maintainer'. - - Maintainer should be allowed access, because he is a power user like admin. - """ - self._attempt_succeed_show(vial.const.ROLE_MAINTAINER) - - #-------------------------------------------------------------------------- - - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) - def test_10_as_admin_user(self): - """Test access to 'user' account as user 'admin'.""" - self._attempt_succeed_show(vial.const.ROLE_USER) - - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) - def test_11_as_admin_developer(self): - """Test access to 'developer' account as user 'admin'.""" - self._attempt_succeed_show(vial.const.ROLE_DEVELOPER) - - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) - def test_12_as_admin_maintainer(self): - """Test access to 'maintainer' account as user 'admin'.""" - self._attempt_succeed_show(vial.const.ROLE_MAINTAINER) - - -@unittest.skipIf(_IS_NOSE, "broken under nosetest") -class UsersCreateTestCase(UsersTestCaseMixin, TestRunnerMixin, ItemCreateVialTestCase): - """Class for testing ``users.create`` endpoint.""" - - user_data_fixture = [ - ('login', 'test'), - ('fullname', 'Test User'), - ('email', 'test.user@domain.org'), - ('enabled', True) - ] - - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) - def test_01_as_user(self): - """Test access as user 'user'.""" - self._attempt_fail_create() - - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) - def test_02_as_developer(self): - """Test access as user 'developer'.""" - self._attempt_fail_create() - - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) - def test_03_as_maintainer(self): - """Test access as user 'maintainer'.""" - self._attempt_succeed_create(self.user_data_fixture) - - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) - def test_04_as_admin(self): - """Test access as user 'admin'.""" - self._attempt_succeed_create(self.user_data_fixture) - - -@unittest.skipIf(_IS_NOSE, "broken under nosetest") -class UsersUpdateOwnTestCase(UsersTestCaseMixin, TestRunnerMixin, VialTestCase): - """Class for testing ``users.update`` endpoint: access to user`s own accounts.""" - - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) - def test_01_as_user(self): - """Test access as user 'user'.""" - self._attempt_succeed_update(vial.const.ROLE_USER) - - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) - def test_02_as_developer(self): - """Test access as user 'developer'.""" - self._attempt_succeed_update(vial.const.ROLE_DEVELOPER) - - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) - def test_03_as_maintainer(self): - """Test access as user 'maintainer'.""" - self._attempt_succeed_update(vial.const.ROLE_MAINTAINER) - - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) - def test_04_as_admin(self): - """Test access as user 'admin'.""" - self._attempt_succeed_update(vial.const.ROLE_ADMIN) - - -@unittest.skipIf(_IS_NOSE, "broken under nosetest") -class UsersUpdateOtherTestCase(UsersTestCaseMixin, TestRunnerMixin, VialTestCase): - """Class for testing ``users.update`` endpoint: access to other user`s accounts.""" - - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) - def test_01_as_user_developer(self): - """Test access to 'developer' account as user 'user'.""" - self._attempt_fail_update(vial.const.ROLE_DEVELOPER) - - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) - def test_02_as_user_maintainer(self): - """Test access to 'maintainer' account as user 'user'.""" - self._attempt_fail_update(vial.const.ROLE_MAINTAINER) - - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) - def test_03_as_user_admin(self): - """Test access to 'admin' account as user 'user'.""" - self._attempt_fail_update(vial.const.ROLE_ADMIN) - - #-------------------------------------------------------------------------- - - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) - def test_04_as_developer_user(self): - """Test access to 'user' account as user 'developer'.""" - self._attempt_fail_update(vial.const.ROLE_USER) - - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) - def test_05_as_developer_maintainer(self): - """Test access to 'maintainer' account as user 'developer'.""" - self._attempt_fail_update(vial.const.ROLE_MAINTAINER) - - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) - def test_06_as_developer_admin(self): - """Test access to 'admin' account as user 'developer'.""" - self._attempt_fail_update(vial.const.ROLE_ADMIN) - - #-------------------------------------------------------------------------- - - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) - def test_07_as_maintainer_user(self): - """Test access to 'user' account as user 'maintainer'.""" - self._attempt_fail_update(vial.const.ROLE_USER) - - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) - def test_08_as_maintainer_developer(self): - """Test access to 'developer' account as user 'maintainer'.""" - self._attempt_fail_update(vial.const.ROLE_DEVELOPER) - - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) - def test_09_as_maintainer_admin(self): - """Test access to 'admin' account as user 'maintainer'.""" - self._attempt_fail_update(vial.const.ROLE_ADMIN) - - #-------------------------------------------------------------------------- - - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) - def test_10_as_admin_user(self): - """Test access to 'user' account as user 'admin'.""" - self._attempt_succeed_update(vial.const.ROLE_USER) - - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) - def test_11_as_admin_developer(self): - """Test access to 'developer' account as user 'admin'.""" - self._attempt_succeed_update(vial.const.ROLE_DEVELOPER) - - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) - def test_12_as_admin_maintainer(self): - """Test access to 'maintainer' account as user 'admin'.""" - self._attempt_succeed_update(vial.const.ROLE_MAINTAINER) - - -@unittest.skipIf(_IS_NOSE, "broken under nosetest") -class UsersEnableDisableTestCase(UsersTestCaseMixin, TestRunnerMixin, VialTestCase): - """Class for testing ``users.enable`` and ``users.disable`` endpoint.""" - - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) - def test_01_as_user(self): - """Test access as user 'user'.""" - for uname in ( - vial.const.ROLE_USER, - vial.const.ROLE_DEVELOPER, - vial.const.ROLE_MAINTAINER, - vial.const.ROLE_ADMIN - ): - self._attempt_fail_disable(uname) - self._attempt_fail_enable(uname) - - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) - def test_02_as_developer(self): - """Test access as user 'developer'.""" - for uname in ( - vial.const.ROLE_USER, - vial.const.ROLE_DEVELOPER, - vial.const.ROLE_MAINTAINER, - vial.const.ROLE_ADMIN - ): - self._attempt_fail_disable(uname) - self._attempt_fail_enable(uname) - - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) - def test_03_as_maintainer(self): - """Test access as user 'maintainer'.""" - for uname in ( - vial.const.ROLE_USER, - vial.const.ROLE_DEVELOPER, - vial.const.ROLE_ADMIN - ): - self._attempt_succeed_disable(uname) - self._attempt_succeed_enable(uname) - - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) - def test_04_as_admin(self): - """Test access as user 'admin'.""" - for uname in ( - vial.const.ROLE_USER, - vial.const.ROLE_DEVELOPER, - vial.const.ROLE_MAINTAINER - ): - self._attempt_succeed_disable(uname) - self._attempt_succeed_enable(uname) - - -@unittest.skipIf(_IS_NOSE, "broken under nosetest") -class UsersAddRemRejMembershipTestCase(TestRunnerMixin, VialTestCase): - """Class for testing ``users.add_membership``, ``users.reject_membership`` and ``users.remove_membership`` endpoint.""" - - def _attempt_fail(self, uname, gname): - with self.app.app_context(): - uid = self.user_id(uname) - gid = self.group_id(gname) - self.assertGetURL( - '/users/{}/remove_membership/{}'.format(uid, gid), - 403 - ) - self.assertGetURL( - '/users/{}/reject_membership/{}'.format(uid, gid), - 403 - ) - self.assertGetURL( - '/users/{}/add_membership/{}'.format(uid, gid), - 403 - ) - - def _attempt_succeed(self, uname, gname, print_response = False): - # Additional test preparations. - with self.app.app_context(): - uid = self.user_id(uname) - gid = self.group_id(gname) - self.user_enabled(uname, False) - self.mailbox_monitoring('on') - - # - # First check the removal of existing membership. - # - self.assertGetURL( - '/users/{}/remove_membership/{}'.format(uid, gid), - 200, - [ - b'Are you really sure you want to remove user' - ], - print_response - ) - self.assertPostURL( - '/users/{}/remove_membership/{}'.format(uid, gid), - { - 'submit': 'Confirm' - }, - 200, - [ - b'was successfully removed as a member from group' - ], - print_response - ) - - # - # Add user back to group. - # - self.assertGetURL( - '/users/{}/add_membership/{}'.format(uid, gid), - 200, - [ - b'Are you really sure you want to add user' - ], - print_response - ) - self.assertPostURL( - '/users/{}/add_membership/{}'.format(uid, gid), - { - 'submit': 'Confirm' - }, - 200, - [ - b'was successfully added as a member to group' - ], - print_response - ) - self.assertMailbox( - { - 'subject': [ - '[{}] Account activation - {}'.format(self.app.config['APPLICATION_NAME'], uname) - ], - 'sender': [ - 'root@unittest' - ], - 'recipients': [ - ['{}@bogus-domain.org'.format(uname)] - ], - 'cc': [[]], - 'bcc': [['admin@unittest']] - } - ) - self.mailbox_clear() - - # Additional test preparations. - with self.app.app_context(): - uobj = self.user_get(uname) - gobj = self.group_get(gname) - uid = uobj.id - gid = gobj.id - uobj.enabled = False - uobj.memberships.remove(gobj) - uobj.memberships_wanted.append(gobj) - self.user_save(uobj) - - # - # Check membership request rejection feature. - # - self.assertGetURL( - '/users/{}/reject_membership/{}'.format(uid, gid), - 200, - [ - b'Are you really sure you want to reject membership request of user' - ], - print_response - ) - self.assertPostURL( - '/users/{}/reject_membership/{}'.format(uid, gid), - { - 'submit': 'Confirm' - }, - 200, - [ - b'was successfully rejected.' - ], - print_response - ) - self.mailbox_monitoring('off') - - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) - def test_01_as_user(self): - """Test access as user 'user'.""" - for uname in ( - vial.const.ROLE_USER, - vial.const.ROLE_DEVELOPER, - vial.const.ROLE_MAINTAINER, - vial.const.ROLE_ADMIN - ): - self._attempt_fail(uname, vial.test.fixtures.DEMO_GROUP_A) - - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) - def test_02_as_developer(self): - """Test access as user 'developer'.""" - for uname in ( - vial.const.ROLE_USER, - vial.const.ROLE_MAINTAINER, - vial.const.ROLE_ADMIN - ): - self._attempt_succeed(uname, vial.test.fixtures.DEMO_GROUP_A) - - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) - def test_03_as_maintainer(self): - """Test access as user 'maintainer'.""" - for uname in ( - vial.const.ROLE_USER, - vial.const.ROLE_DEVELOPER, - vial.const.ROLE_ADMIN - ): - self._attempt_succeed(uname, vial.test.fixtures.DEMO_GROUP_A) - - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) - def test_04_as_admin(self): - """Test access as user 'admin'.""" - for uname in ( - vial.const.ROLE_USER, - vial.const.ROLE_DEVELOPER, - vial.const.ROLE_MAINTAINER - ): - self._attempt_succeed(uname, vial.test.fixtures.DEMO_GROUP_A) - - -@unittest.skipIf(_IS_NOSE, "broken under nosetest") -class UsersDeleteTestCase(UsersTestCaseMixin, TestRunnerMixin, VialTestCase): - """Class for testing ``users.delete`` endpoint.""" - - @vial.test.do_as_user_decorator(vial.const.ROLE_USER) - def test_01_as_user(self): - """Test access as user 'user'.""" - for uname in ( - vial.const.ROLE_USER, - vial.const.ROLE_DEVELOPER, - vial.const.ROLE_MAINTAINER, - vial.const.ROLE_ADMIN - ): - self._attempt_fail_delete(uname) - - @vial.test.do_as_user_decorator(vial.const.ROLE_DEVELOPER) - def test_02_as_developer(self): - """Test access as user 'developer'.""" - for uname in ( - vial.const.ROLE_USER, - vial.const.ROLE_DEVELOPER, - vial.const.ROLE_MAINTAINER, - vial.const.ROLE_ADMIN - ): - self._attempt_fail_delete(uname) - - @vial.test.do_as_user_decorator(vial.const.ROLE_MAINTAINER) - def test_03_as_maintainer(self): - """Test access as user 'maintainer'.""" - for uname in ( - vial.const.ROLE_USER, - vial.const.ROLE_DEVELOPER, - vial.const.ROLE_MAINTAINER, - vial.const.ROLE_ADMIN - ): - self._attempt_fail_delete(uname) - - @vial.test.do_as_user_decorator(vial.const.ROLE_ADMIN) - def test_04_as_admin(self): - """Test access as user 'admin'.""" - for uname in ( - vial.const.ROLE_USER, - vial.const.ROLE_DEVELOPER, - vial.const.ROLE_MAINTAINER - ): - self._attempt_succeed_delete(uname) - - -#------------------------------------------------------------------------------- - - -if __name__ == "__main__": - unittest.main() diff --git a/lib/vial/blueprints/users/test/utils.py b/lib/vial/blueprints/users/test/utils.py deleted file mode 100644 index 73188a1b8c2f92c5162b9b812490a1d7a9c8c0ee..0000000000000000000000000000000000000000 --- a/lib/vial/blueprints/users/test/utils.py +++ /dev/null @@ -1,209 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- -# Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- - - -""" -Unit test utilities for :py:mod:`vial.blueprints.users`. -""" - - -class UsersTestCaseMixin: - """Mixin class with unit test framework for testing user account management endpoints.""" - - def _attempt_fail_list(self): - """Check access to ``users.list`` endpoint and fail.""" - self.assertGetURL( - '/users/list', - 403 - ) - - def _attempt_succeed_list(self, content_checks = None): - """Check access to ``users.list`` endpoint and succeed.""" - if content_checks is None: - content_checks = [ - b'Show details of user account "user"', - b'Show details of user account "developer"', - b'Show details of user account "maintainer"', - b'Show details of user account "admin"' - ] - self.assertGetURL( - '/users/list', - 200, - content_checks - ) - - def _attempt_fail_show(self, uname): - """Check access to ``users.show`` endpoint and fail.""" - uid = self.user_id(uname, with_app_ctx = True) - self.assertGetURL( - '/users/{}/show'.format(uid), - 403 - ) - - def _attempt_succeed_show(self, uname): - """Check access to ``users.show`` endpoint and succeed.""" - with self.app.app_context(): - uobj = self.user_get(uname) - uid = uobj.id - ufname = uobj.fullname - self.assertGetURL( - '/users/{}/show'.format(uid), - 200, - [ - '<h3>{} ({})</h3>'.format(ufname, uname).encode('utf8'), - b'<strong>Account created:</strong>' - ] - ) - - def _attempt_fail_create(self): - """Check access to ``users.create`` endpoint and fail.""" - self.assertGetURL( - '/users/create', - 403 - ) - - def _attempt_succeed_create(self, data): - """Check access to ``users.create`` endpoint and succeed.""" - self.assertCreate( - self.user_model(), - '/users/create', - data, - [ - 'User account <strong>{}</strong> was successfully created.'.format(data[0][1]).encode('utf8') - ] - ) - self._attempt_succeed_show( - data[0][1] - ) - self._attempt_succeed_list( - [ - 'Show details of user account "{}"'.format(data[0][1]).encode('utf8') - ] - ) - - def _attempt_fail_update(self, uname): - """Check access to ``users.update`` endpoint and fail.""" - uid = self.user_id(uname, with_app_ctx = True) - self.assertGetURL( - '/users/{}/update'.format(uid), - 403 - ) - - def _attempt_succeed_update(self, uname): - """Check access to ``users.update`` endpoint and succeed.""" - uid = self.user_id(uname, with_app_ctx = True) - self.assertGetURL( - '/users/{}/update'.format(uid), - 200, - [ - b'Update user account details' - ] - ) - - def _attempt_fail_enable(self, uname): - """Check access to ``users.enable`` endpoint and fail.""" - uid = self.user_id(uname, with_app_ctx = True) - self.assertGetURL( - '/users/{}/enable'.format(uid), - 403 - ) - - def _attempt_succeed_enable(self, uname): - """Check access to ``users.enable`` endpoint and succeed.""" - uid = self.user_id(uname, with_app_ctx = True) - self.mailbox_monitoring('on') - self.assertGetURL( - '/users/{}/enable'.format(uid), - 200, - [ - b'Are you really sure you want to enable following item:' - ] - ) - self.assertPostURL( - '/users/{}/enable'.format(uid), - { - 'submit': 'Confirm' - }, - 200, - [ - b'was successfully enabled.' - ] - ) - self.assertMailbox( - { - 'subject': [ - '[{}] Account activation - {}'.format(self.app.config['APPLICATION_NAME'], uname) - ], - 'sender': [ - 'root@unittest' - ], - 'recipients': [ - ['{}@bogus-domain.org'.format(uname)] - ], - 'cc': [[]], - 'bcc': [['admin@unittest']] - } - ) - self.mailbox_monitoring('off') - self.mailbox_clear() - - def _attempt_fail_disable(self, uname): - """Check access to ``users.disable`` endpoint and fail.""" - uid = self.user_id(uname, with_app_ctx = True) - self.assertGetURL( - '/users/{}/disable'.format(uid), - 403 - ) - - def _attempt_succeed_disable(self, uname): - """Check access to ``users.disable`` endpoint and succeed.""" - uid = self.user_id(uname, with_app_ctx = True) - self.assertGetURL( - '/users/{}/disable'.format(uid), - 200, - [ - b'Are you really sure you want to disable following item:' - ] - ) - self.assertPostURL( - '/users/{}/disable'.format(uid), - { - 'submit': 'Confirm' - }, - 200, - [ - b'was successfully disabled.' - ] - ) - - def _attempt_fail_delete(self, uname): - """Check access to ``users.delete`` endpoint and fail.""" - uid = self.user_id(uname, with_app_ctx = True) - self.assertGetURL( - '/users/{}/delete'.format(uid), - 403 - ) - - def _attempt_succeed_delete(self, uname): - """Check access to ``users.delete`` endpoint and succeed.""" - uid = self.user_id(uname, with_app_ctx = True) - self.assertGetURL( - '/users/{}/delete'.format(uid), - 200, - [ - b'Are you really sure you want to permanently remove following item:' - ] - ) - self.assertPostURL( - '/users/{}/delete'.format(uid), - { - 'submit': 'Confirm' - }, - 200, - [ - b'was successfully and permanently deleted.' - ] - ) diff --git a/lib/vial/config.py b/lib/vial/config.py deleted file mode 100644 index 9c682397a439b3f8addc63f426534fa792825732..0000000000000000000000000000000000000000 --- a/lib/vial/config.py +++ /dev/null @@ -1,259 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- -# Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- - - -""" -This module contains default *Vial* application configurations. -""" - - -import os -import socket -import collections -from flask_babel import lazy_gettext - -import vial.const - - -class Config: # pylint: disable=locally-disabled,too-few-public-methods - """ - Base class for default configurations of Vial application. You are free to - extend and customize contents of this class to provide better default values - for your particular environment. - - The configuration keys must be a valid Flask configuration and so they must - be written in UPPERCASE to be correctly recognized. - """ - - APPLICATION_NAME = "Vial" - APPLICATION_ID = "vial" - - #--------------------------------------------------------------------------- - # Flask internal configurations. Please refer to Flask documentation for - # more information about each configuration key. - #--------------------------------------------------------------------------- - - DEBUG = False - TESTING = False - SECRET_KEY = 'default-secret-key' - - #--------------------------------------------------------------------------- - # Flask extension configurations. Please refer to the documentation of that - # particular Flask extension for more details. - #--------------------------------------------------------------------------- - - # - # Flask-WTF configurations. - # - WTF_CSRF_ENABLED = True - - # - # Flask-Mail configurations. - # - MAIL_SERVER = 'localhost' - MAIL_PORT = 25 - MAIL_USERNAME = None - MAIL_PASSWORD = None - MAIL_DEFAULT_SENDER = '{}@{}'.format(APPLICATION_ID, socket.getfqdn()) - MAIL_SUBJECT_PREFIX = '[{}]'.format(APPLICATION_NAME) - - DISABLE_MAIL_LOGGING = False - - # - # Flask-Babel configurations. - # - BABEL_DEFAULT_LOCALE = vial.const.DEFAULT_LOCALE - BABEL_DEFAULT_TIMEZONE = vial.const.DEFAULT_TIMEZONE - BABEL_DETECT_LOCALE = True - """Custom configuration, make detection of best possible locale optional to enable forcing default.""" - - # - # Flask-SQLAlchemy configurations. - # - SQLALCHEMY_TRACK_MODIFICATIONS = False - SQLALCHEMY_SETUP_ARGS = {} - - # - # Flask-Migrate configurations. - # - MIGRATE_DIRECTORY = os.path.join( - os.path.dirname( - os.path.abspath(__file__) - ), - 'migrations' - ) - - #--------------------------------------------------------------------------- - # Custom application configurations. - #--------------------------------------------------------------------------- - - ROLES = vial.const.ROLES - """List of all valid user roles supported by the application.""" - - MODELS = {} - """Models to be used within the application.""" - - SUPPORTED_LOCALES = collections.OrderedDict([ - ('en', 'English'), - ('cs', 'ÄŒesky') - ]) - """List of all languages (locales) supported by the application.""" - - ENABLED_BLUEPRINTS = [ - 'vial.blueprints.auth', - 'vial.blueprints.auth_api', - 'vial.blueprints.auth_env', - 'vial.blueprints.auth_pwd', - 'vial.blueprints.devtools', - 'vial.blueprints.design_bs3', - 'vial.blueprints.home', - 'vial.blueprints.users', - 'vial.blueprints.groups', - 'vial.blueprints.changelogs' - ] - """List of requested application blueprints to be loaded during setup.""" - - DISABLED_ENDPOINTS = [] - """List of endpoints disabled on application level.""" - - ENDPOINT_LOGIN = 'auth.login' - """ - Default login view. Users will be redirected to this view in case they are not - authenticated, but the authentication is required for the requested endpoint. - """ - - LOGIN_MSGCAT = 'info' - """Default message category for messages related to user authentication.""" - - ENDPOINT_HOME = 'home.index' - """Homepage endpoint.""" - - ENDPOINT_LOGIN_REDIRECT = 'home.index' - """Default redirection endpoint after login.""" - - ENDPOINT_LOGOUT_REDIRECT = 'home.index' - """Default redirection endpoint after logout.""" - - MENU_MAIN_SKELETON = [ - { - 'entry_type': 'submenu', - 'ident': 'admin', - 'position': 300, - 'authentication': True, - 'authorization': ['power'], - 'title': lazy_gettext('Administration'), - 'resptitle': True, - 'icon': 'section-administration' - }, - { - 'entry_type': 'submenu', - 'ident': 'developer', - 'position': 400, - 'authentication': True, - 'authorization': ['developer'], - 'title': lazy_gettext('Development'), - 'resptitle': True, - 'icon': 'section-development' - } - ] - """Configuration of application menu skeleton.""" - - EMAIL_ADMINS = ['root@{}'.format(socket.getfqdn())] - """List of system administrator emails.""" - - LOG_DEFAULT_LEVEL = 'info' - """Default logging level, case insensitive. One of the values ``DEBUG``, ``INFO``, ``WARNING``, ``ERROR``, ``CRITICAL``.""" - - LOG_FILE_LEVEL = 'info' - """File logging level, case insensitive. One of the values ``DEBUG``, ``INFO``, ``WARNING``, ``ERROR``, ``CRITICAL``.""" - - LOG_EMAIL_LEVEL = 'error' - """File logging level, case insensitive. One of the values ``DEBUG``, ``INFO``, ``WARNING``, ``ERROR``, ``CRITICAL``.""" - - ICONS = vial.const.ICONS - - -class ProductionConfig(Config): # pylint: disable=locally-disabled,too-few-public-methods - """ - Class containing application configurations for *production* environment. - """ - - -class DevelopmentConfig(Config): # pylint: disable=locally-disabled,too-few-public-methods - """ - Class containing application configurations for *development* environment. - """ - - #--------------------------------------------------------------------------- - # Flask internal configurations. Please refer to Flask documentation for - # more information about each configuration key. - #--------------------------------------------------------------------------- - - - DEBUG = True - - #EXPLAIN_TEMPLATE_LOADING = True - - #DEBUG_TB_PROFILER_ENABLED = True - - #--------------------------------------------------------------------------- - # Custom application configurations. - #--------------------------------------------------------------------------- - - ENDPOINT_LOGIN = 'auth_dev.login' - - ENABLED_BLUEPRINTS = [ - 'vial.blueprints.auth', - 'vial.blueprints.auth_api', - 'vial.blueprints.auth_dev', - 'vial.blueprints.auth_env', - 'vial.blueprints.auth_pwd', - 'vial.blueprints.devtools', - 'vial.blueprints.design_bs3', - 'vial.blueprints.home', - 'vial.blueprints.users', - 'vial.blueprints.groups', - 'vial.blueprints.changelogs' - ] - - LOG_DEFAULT_LEVEL = 'debug' - - LOG_FILE_LEVEL = 'debug' - - -class TestingConfig(Config): # pylint: disable=locally-disabled,too-few-public-methods - """ - Class containing *testing* Vial applications` configurations. - """ - - #--------------------------------------------------------------------------- - # Flask internal configurations. Please refer to Flask documentation for - # more information about each configuration key. - #--------------------------------------------------------------------------- - - TESTING = True - - EXPLAIN_TEMPLATE_LOADING = False - - #--------------------------------------------------------------------------- - # Custom application configurations. - #--------------------------------------------------------------------------- - - ENDPOINT_LOGIN = 'auth_dev.login' - - ENABLED_BLUEPRINTS = [ - 'vial.blueprints.auth', - 'vial.blueprints.auth_api', - 'vial.blueprints.auth_dev', - 'vial.blueprints.auth_env', - 'vial.blueprints.auth_pwd', - 'vial.blueprints.devtools', - 'vial.blueprints.design_bs3', - 'vial.blueprints.home', - 'vial.blueprints.users', - 'vial.blueprints.groups', - 'vial.blueprints.changelogs' - ] diff --git a/lib/vial/const.py b/lib/vial/const.py deleted file mode 100644 index 2635720f758ff8dc2a31ecc19710e5926499aa71..0000000000000000000000000000000000000000 --- a/lib/vial/const.py +++ /dev/null @@ -1,452 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- -# Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- - - -""" -This module contains *Vial* application constants. -""" - - -import re -import datetime - - -def tr_(val): - """ - Method for marking translatable strings according to the documentation - recipe at https://docs.python.org/3/library/gettext.html#deferred-translations. - """ - return val - - -CRE_LOGIN = re.compile('^[-_@.a-zA-Z0-9]+$') -"""Compiled regular expression for login validation.""" - -CRE_EMAIL = re.compile(r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$") -"""Compiled regular expression for email address format validation.""" - -CRE_COUNTRY_CODE = re.compile('^[a-zA-Z]{2,3}$') -"""Compiled regular expression for validating language/country codes.""" - -CRE_LANG_CODE = re.compile('^[a-zA-Z]{2}(_[a-zA-Z]{2})?$') -"""Compiled regular expression for validating language codes.""" - - -DEFAULT_LOCALE = 'en' -"""Default application locale.""" - -DEFAULT_TIMEZONE = 'UTC' -"""Default application timezone.""" - - -FLASH_INFO = 'info' -"""Class for *info* flash messages.""" - -FLASH_SUCCESS = 'success' -"""Class for *success* flash messages.""" - -FLASH_WARNING = 'warning' -"""Class for *warning* flash messages.""" - -FLASH_FAILURE = 'danger' -"""Class for *failure* flash messages.""" - - -FORM_ACTION_SUBMIT = 'submit' -"""Name of the item form *submit* button.""" - -FORM_ACTION_CANCEL = 'cancel' -"""Name of the item form *cancel* button.""" - - -ACTION_ITEM_CREATE = 'create' -"""Name of the item *create* action.""" - -ACTION_ITEM_CREATEFOR = 'createfor' -"""Name of the item *createfor* action.""" - -ACTION_ITEM_UPDATE = 'update' -"""Name of the item *update* action.""" - -ACTION_ITEM_ENABLE = 'enable' -"""Name of the item *enable* action.""" - -ACTION_ITEM_DISABLE = 'disable' -"""Name of the item *disable* action.""" - -ACTION_ITEM_DELETE = 'delete' -"""Name of the item *delete* action.""" - - -ACTION_USER_LOGIN = 'login' -"""Name of the user *login* action.""" - -ACTION_USER_LOGOUT = 'logout' -"""Name of the user *logout* action.""" - -ACTION_USER_REGISTER = 'register' -"""Name of the user *register* action.""" - - -MODEL_USER = 'user' -"""Name of the 'user' model.""" - -MODEL_GROUP = 'group' -"""Name of the 'user' model.""" - -MODEL_ITEM_CHANGELOG = 'item_changelog' -"""Name of the 'user' model.""" - - -ROLE_USER = 'user' -"""Name of the 'user' role.""" - -ROLE_DEVELOPER = 'developer' -"""Name of the 'developer' role.""" - -ROLE_MAINTAINER = 'maintainer' -"""Name of the 'maintainer' role.""" - -ROLE_ADMIN = 'admin' -"""Name of the 'admin' role.""" - -ROLES = [ - ROLE_USER, - ROLE_DEVELOPER, - ROLE_MAINTAINER, - ROLE_ADMIN -] -"""List of valid user roles.""" - -NO_ROLE = '__NO_ROLE__' -"""Special constant for selecting users with no roles.""" - -CFGKEY_MENU_MAIN_SKELETON = 'MENU_MAIN_SKELETON' -"""Configuration key name: Default application main menu skeleton.""" - -CFGKEY_ENABLED_BLUEPRINTS = 'ENABLED_BLUEPRINTS' -"""Configuration key name: List of all requested blueprints.""" - -CFGKEY_MODELS = 'MODELS' -"""Configuration key name: List of all requested blueprints.""" - - -DEFAULT_PAGER_LIMIT = 100 -"""Default page limit for pager/paginator.""" - -PAGER_LIMIT_CHOICES = [ - (5,5), - (10,10), - (20,20), - (25,25), - (50,50), - (100,100), - (200,200), - (250,250), - (500,500), - (1000,1000), - (2500,2500), - (5000,5000), - (10000,10000), - (25000,25000), - (50000,50000), - (100000,100000) -] -"""List of available valid pager limit choices.""" - -DEFAULT_RESULT_TIMEDELTA = 7 -"""Default result time delta for searching various objects.""" - - -RESOURCE_BABEL = 'babel' -"""Name for the ``flask_babel.Babel`` object within the application resources.""" - -RESOURCE_LOGIN_MANAGER = 'login_manager' -"""Name for the ``flask_login.LoginManager`` object within the application resources.""" - -RESOURCE_MIGRATE = 'migrate' -"""Name for the ``flask_migrate.Migrate`` object within the application resources.""" - -RESOURCE_PRINCIPAL = 'principal' -"""Name for the ``flask_principal.Principal`` object within the application resources.""" - - -ICON_NAME_MISSING_ICON = 'missing-icon' -"""Name of the icon to display instead of missing icons.""" - - -ICONS = { - - # - # General icons. - # - ACTION_USER_LOGIN: '<i class="fas fa-fw fa-sign-in-alt"></i>', - ACTION_USER_LOGOUT: '<i class="fas fa-fw fa-sign-out-alt"></i>', - ACTION_USER_REGISTER: '<i class="fas fa-fw fa-user-plus"></i>', - - 'help': '<i class="fas fa-fw fa-question-circle"></i>', - 'language': '<i class="fas fa-fw fa-globe"></i>', - - 'role-anonymous': '<i class="fas fa-fw fa-user-secret"></i>', - 'role-user': '<i class="fas fa-fw fa-user"></i>', - 'role-developer': '<i class="fas fa-fw fa-user-md"></i>', - 'role-maintainer': '<i class="fas fa-fw fa-user-tie"></i>', - 'role-admin': '<i class="fas fa-fw fa-user-ninja"></i>', - - # - # Main site section icons. - # - 'section-home': '<i class="fas fa-fw fa-home"></i>', - 'section-dashboards': '<i class="fas fa-fw fa-tachometer-alt"></i>', - 'section-more': '<i class="fas fa-fw fa-puzzle-piece"></i>', - 'section-administration': '<i class="fas fa-fw fa-cogs"></i>', - 'section-development': '<i class="fas fa-fw fa-bug"></i>', - - # - # Built-in module icons. - # - 'module-home': '<i class="fas fa-fw fa-home"></i>', - 'module-auth-api': '<i class="fas fa-fw fa-key"></i>', - 'module-auth-dev': '<i class="fas fa-fw fa-key"></i>', - 'module-auth-env': '<i class="fas fa-fw fa-key"></i>', - 'module-changelogs': '<i class="fas fa-fw fa-clipboard-list"></i>', - 'module-dashboards': '<i class="fas fa-fw fa-tachometer-alt"></i>', - 'module-dbstatus': '<i class="fas fa-fw fa-database"></i>', - 'module-design': '<i class="fas fa-fw fa-palette"></i>', - 'module-devtools': '<i class="fas fa-fw fa-bug"></i>', - 'module-dnsr': '<i class="fas fa-fw fa-directions"></i>', - 'module-events': '<i class="fas fa-fw fa-bell"></i>', - 'module-filters': '<i class="fas fa-fw fa-filter"></i>', - 'module-geoip': '<i class="fas fa-fw fa-map-marked-alt"></i>', - 'module-groups': '<i class="fas fa-fw fa-users"></i>', - 'module-help': '<i class="fas fa-fw fa-question-circle"></i>', - 'module-hosts': '<i class="fas fa-fw fa-server"></i>', - 'module-networks': '<i class="fas fa-fw fa-sitemap"></i>', - 'module-nerd': '<i class="fas fa-fw fa-certificate"></i>', - 'module-pdnsr': '<i class="fas fa-fw fa-compass"></i>', - 'module-performance': '<i class="fas fa-fw fa-chart-bar"></i>', - 'module-reports': '<i class="fas fa-fw fa-newspaper"></i>', - 'module-settings-reporting': '<i class="fas fa-fw fa-sliders-h"></i>', - 'module-skeleton': '<i class="fas fa-fw fa-skull"></i>', - 'module-status': '<i class="fas fa-fw fa-heartbeat"></i>', - 'module-timeline': '<i class="fas fa-fw fa-chart-line"></i>', - 'module-users': '<i class="fas fa-fw fa-user"></i>', - 'module-whois': '<i class="fas fa-fw fa-map-signs"></i>', - - 'profile': '<i class="fas fa-fw fa-id-card"></i>', - - 'modal-question': '<i class="fas fa-fw fa-question-circle"></i>', - - 'missing-icon': '<i class="fas fa-fw fa-question" title="Missing icon"></i>', - - # - # Action icons. - # - 'action-sort': '<i class="fas fa-fw fa-sort"></i>', - 'action-sort-asc': '<i class="fas fa-fw fa-sort-up"></i>', - 'action-sort-desc': '<i class="fas fa-fw fa-sort-down"></i>', - 'action-more': '<i class="fas fa-fw fa-cubes"></i>', - 'action-search': '<i class="fas fa-fw fa-search"></i>', - 'action-show': '<i class="fas fa-fw fa-eye"></i>', - 'action-show-user': '<i class="fas fa-fw fa-user-circle"></i>', - 'action-create': '<i class="fas fa-fw fa-plus-circle"></i>', - 'action-create-user': '<i class="fas fa-fw fa-user-plus"></i>', - 'action-update': '<i class="fas fa-fw fa-edit"></i>', - 'action-update-user': '<i class="fas fa-fw fa-user-edit"></i>', - 'action-enable': '<i class="fas fa-fw fa-unlock"></i>', - 'action-enable-user': '<i class="fas fa-fw fa-user-check"></i>', - 'action-disable': '<i class="fas fa-fw fa-lock"></i>', - 'action-disable-user': '<i class="fas fa-fw fa-user-lock"></i>', - 'action-delete': '<i class="fas fa-fw fa-trash"></i>', - 'action-delete-user': '<i class="fas fa-fw fa-user-slash"></i>', - 'action-add-member': '<i class="fas fa-fw fa-user-plus"></i>', - 'action-rej-member': '<i class="fas fa-fw fa-user-minus"></i>', - 'action-rem-member': '<i class="fas fa-fw fa-user-times"></i>', - 'action-add-manager': '<i class="fas fa-fw fa-user-plus"></i>', - 'action-rem-manager': '<i class="fas fa-fw fa-user-times"></i>', - 'action-save': '<i class="fas fa-fw fa-save"></i>', - 'action-download': '<i class="fas fa-fw fa-file-download"></i>', - 'action-download-zip': '<i class="fas fa-fw fa-file-archive"></i>', - 'action-download-csv': '<i class="fas fa-fw fa-file-csv"></i>', - 'action-download-svg': '<i class="fas fa-fw fa-file-image"></i>', - 'action-download-js': '<i class="fab fa-fw fa-js"></i>', - 'action-mail': '<i class="fas fa-fw fa-envelope"></i>', - 'action-reload': '<i class="fas fa-fw fa-sync-alt"></i>', - 'action-genkey': '<i class="fas fa-fw fa-key"></i>', - 'action-stop': '<i class="fas fa-fw fa-stop-circle"></i>', - - 'alert-success': '<i class="fas fa-fw fa-check-circle"></i>', - 'alert-info': '<i class="fas fa-fw fa-info-circle"></i>', - 'alert-warning': '<i class="fas fa-fw fa-exclamation-circle"></i>', - 'alert-danger': '<i class="fas fa-fw fa-exclamation-triangle"></i>', - - 'item-enabled': '<i class="fas fa-fw fa-toggle-on"></i>', - 'item-disabled': '<i class="fas fa-fw fa-toggle-off"></i>', - - 'r-t-summary': '<i class="fas fa-fw fa-archive"></i>', - 'r-t-extra': '<i class="fas fa-fw fa-file-alt"></i>', - 'r-s-unknown': '<i class="fas fa-fw fa-thermometer-empty"></i>', - 'r-s-low': '<i class="fas fa-fw fa-thermometer-quarter"></i>', - 'r-s-medium': '<i class="fas fa-fw fa-thermometer-half"></i>', - 'r-s-high': '<i class="fas fa-fw fa-thermometer-three-quarters"></i>', - 'r-s-critical': '<i class="fas fa-fw fa-thermometer-full"></i>', - - 'report-data-relapsed': '<i class="fas fa-fw fa-sync-alt"></i>', - 'report-data-filtered': '<i class="fas fa-fw fa-filter"></i>', - 'report-data-test': '<i class="fas fa-fw fa-bug"></i>', - 'report-data-mailed': '<i class="fas fa-fw fa-envelope"></i>', - - 'ajax-loader': '<i class="fas fa-fw fa-spinner fa-spin fa-4x"></i>', - 'caret-down': '<i class="fas fa-fw fa-caret-square-down"></i>', - 'unassigned': '<i class="fas fa-fw fa-minus"></i>', - 'undisclosed': '<i class="fas fa-fw fa-minus"></i>', - 'calendar': '<i class="fas fa-fw fa-calendar-alt"></i>', - 'stopwatch': '<i class="fas fa-fw fa-stopwatch"></i>', - 'clock': '<i class="fas fa-fw fa-clock"></i>', - 'domain': '<i class="fas fa-fw fa-tag"></i>', - 'time-from': '<i class="fas fa-fw fa-hourglass-start"></i>', - 'time-to': '<i class="fas fa-fw fa-hourglass-end"></i>', - 'debug': '<i class="fas fa-fw fa-bug"></i>', - 'eventclss': '<i class="fas fa-fw fa-book"></i>', - 'reference': '<i class="fas fa-fw fa-external-link-alt"></i>', - 'anchor': '<i class="fas fa-fw fa-anchor"></i>', - 'search': '<i class="fas fa-fw fa-search"></i>', - 'weight': '<i class="fas fa-fw fa-weight"></i>', - 'list': '<i class="fas fa-fw fa-list-ul"></i>', - 'mail': '<i class="fas fa-fw fa-envelope"></i>', - 'redirect': '<i class="fas fa-fw fa-share"></i>', - 'unredirect': '<span class="fa-layers fa-fw"><i class="fas fa-fw fa-share"></i><i class="fas fa-fw fa-ban"></i></span>', - 'mute': '<i class="fas fa-fw fa-volume-off"></i>', - 'unmute': '<i class="fas fa-fw fa-volume-up"></i>', - 'compress': '<i class="fas fa-fw fa-gift"></i>', - 'uncompress': '<span class="fa-layers fa-fw"><i class="fas fa-fw fa-gift"></i><i class="fas fa-fw fa-ban"></i></span>', - 'import': '<i class="fas fa-fw fa-cloud-upload"></i>', - 'export': '<i class="fas fa-fw fa-cloud-download"></i>', - 'validate': '<i class="fas fa-fw fa-check-circle"></i>', - 'min': '<i class="fas fa-fw fa-angle-double-down"></i>', - 'max': '<i class="fas fa-fw fa-angle-double-up"></i>', - 'sum': '<i class="fas fa-fw fa-plus"></i>', - 'cnt': '<i class="fas fa-fw fa-hashtag"></i>', - 'avg': '<i class="fas fa-fw fa-dot-circle"></i>', - 'med': '<i class="fas fa-fw fa-bullseye"></i>', - 'na': '<i class="fas fa-fw fa-times"></i>', - 'stats': '<i class="fas fa-fw fa-bar-chart"></i>', - 'structure': '<i class="fas fa-fw fa-tree"></i>', - 'actions': '<i class="fas fa-fw fa-wrench"></i>', - 'cog': '<i class="fas fa-fw fa-cog"></i>', - 'check': '<i class="fas fa-fw fa-check-square"></i>', - 'check_blank': '<i class="far fa-fw fa-square"></i>', - 'ok': '<i class="fas fa-fw fa-check"></i>', - 'ko': '<i class="fas fa-fw fa-times"></i>', - 'sortasc': '<i class="fas fa-fw fa-sort-asc"></i>', - 'sortdesc': '<i class="fas fa-fw fa-sort-desc"></i>', - 'backtotop': '<i class="fas fa-fw fa-level-up-alt"></i>', - 'first': '<i class="fas fa-fw fa-angle-double-left"></i>', - 'previous': '<i class="fas fa-fw fa-angle-left"></i>', - 'next': '<i class="fas fa-fw fa-angle-right"></i>', - 'last': '<i class="fas fa-fw fa-angle-double-right" aria-hidden="true"></i>', - 'liitem': '<i class="fas fa-li fa-asterisk" aria-hidden="true"></i>', - 'expand': '<i class="fas fa-fw fa-angle-left" aria-hidden="true"></i>', - 'collapse': '<i class="fas fa-fw fa-angle-down" aria-hidden="true"></i>', - 'form-error': '<i class="fas fa-fw fa-exclamation-triangle" aria-hidden="true"></i>', - 'table': '<i class="fas fa-fw fa-table"></i>', - 'quicksearch': '<i class="fab fa-fw fa-searchengin"></i>', - 'playground': '<i class="fas fa-fw fa-gamepad"></i>' -} -""" -Predefined list of selected `font-awesome <http://fontawesome.io/icons/>`__ icons -that are used in this application. -""" - -TIME_WINDOWS = { - '1h': { - 'current': lambda x: (x - datetime.timedelta(hours = 1)).replace(minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'previous': lambda x: (x - datetime.timedelta(hours = 1)).replace(minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'next': lambda x: (x + datetime.timedelta(hours = 1)).replace(minute = 0, second = 0, microsecond = 0, tzinfo = None) - }, - '2h': { - 'current': lambda x: (x - datetime.timedelta(hours = 2)).replace(minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'previous': lambda x: (x - datetime.timedelta(hours = 2)).replace(minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'next': lambda x: (x + datetime.timedelta(hours = 2)).replace(minute = 0, second = 0, microsecond = 0, tzinfo = None) - }, - '3h': { - 'current': lambda x: (x - datetime.timedelta(hours = 3)).replace(minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'previous': lambda x: (x - datetime.timedelta(hours = 3)).replace(minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'next': lambda x: (x + datetime.timedelta(hours = 3)).replace(minute = 0, second = 0, microsecond = 0, tzinfo = None) - }, - '4h': { - 'current': lambda x: (x - datetime.timedelta(hours = 4)).replace(minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'previous': lambda x: (x - datetime.timedelta(hours = 4)).replace(minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'next': lambda x: (x + datetime.timedelta(hours = 4)).replace(minute = 0, second = 0, microsecond = 0, tzinfo = None) - }, - '6h': { - 'current': lambda x: (x - datetime.timedelta(hours = 6)).replace(minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'previous': lambda x: (x - datetime.timedelta(hours = 6)).replace(minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'next': lambda x: (x + datetime.timedelta(hours = 6)).replace(minute = 0, second = 0, microsecond = 0, tzinfo = None) - }, - '12h': { - 'current': lambda x: (x - datetime.timedelta(hours = 12)).replace(minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'previous': lambda x: (x - datetime.timedelta(hours = 12)).replace(minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'next': lambda x: (x + datetime.timedelta(hours = 12)).replace(minute = 0, second = 0, microsecond = 0, tzinfo = None) - }, - '1d': { - 'current': lambda x: (x - datetime.timedelta(days = 1)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'previous': lambda x: (x - datetime.timedelta(days = 1)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'next': lambda x: (x + datetime.timedelta(days = 1)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None) - }, - '2d': { - 'current': lambda x: (x - datetime.timedelta(days = 2)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'previous': lambda x: (x - datetime.timedelta(days = 2)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'next': lambda x: (x + datetime.timedelta(days = 2)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None) - }, - '3d': { - 'current': lambda x: (x - datetime.timedelta(days = 3)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'previous': lambda x: (x - datetime.timedelta(days = 3)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'next': lambda x: (x + datetime.timedelta(days = 3)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None) - }, - '1w': { - 'current': lambda x: (x - datetime.timedelta(weeks = 1)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'previous': lambda x: (x - datetime.timedelta(weeks = 1)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'next': lambda x: (x + datetime.timedelta(weeks = 1)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None) - }, - '2w': { - 'current': lambda x: (x - datetime.timedelta(weeks = 2)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'previous': lambda x: (x - datetime.timedelta(weeks = 2)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'next': lambda x: (x + datetime.timedelta(weeks = 2)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None) - }, - '4w': { - 'current': lambda x: (x - datetime.timedelta(weeks = 4)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'previous': lambda x: (x - datetime.timedelta(weeks = 4)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'next': lambda x: (x + datetime.timedelta(weeks = 4)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None) - }, - '12w': { - 'current': lambda x: (x - datetime.timedelta(weeks = 12)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'previous': lambda x: (x - datetime.timedelta(weeks = 12)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'next': lambda x: (x + datetime.timedelta(weeks = 12)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None) - }, - - 'td': { - 'current': lambda x: x.replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'previous': lambda x: (x - datetime.timedelta(days = 1)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'next': lambda x: (x + datetime.timedelta(days = 1)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None) - }, - 'tw': { - 'current': lambda x: (x - datetime.timedelta(days = x.weekday())).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'previous': lambda x: (x - datetime.timedelta(days = 7)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'next': lambda x: (x + datetime.timedelta(days = 7)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None) - }, - 'tm': { - 'current': lambda x: x.replace(day = 1, hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'previous': lambda x: (x - datetime.timedelta(days = 1)).replace(day = 1, hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'next': lambda x: (x + datetime.timedelta(days = 32)).replace(day = 1, hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None) - }, - 'ty': { - 'current': lambda x: x.replace(month = 1, day = 1, hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'previous': lambda x: x.replace(year = x.year - 1, month = 1, day = 1, hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'next': lambda x: x.replace(year = x.year + 1, month = 1, day = 1, hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None) - } -} -"""Default list of time windows for 'by time' quicksearch lists.""" diff --git a/lib/vial/model/__init__.py b/lib/vial/model/__init__.py deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/lib/vial/model/db.py b/lib/vial/model/db.py deleted file mode 100644 index 532478192906d9098c00eaa10563b905680c0efe..0000000000000000000000000000000000000000 --- a/lib/vial/model/db.py +++ /dev/null @@ -1,506 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - - -__author__ = "Honza Mach <honza.mach.ml@gmail.com>" - - -import datetime -import json -import difflib - -from werkzeug.security import generate_password_hash, check_password_hash - -import sqlalchemy -import sqlalchemy.dialects.postgresql -from sqlalchemy.schema import DropTable -from sqlalchemy.ext.compiler import compiles - -from vial.db import MODEL - - -# -# Modify compilation of DROP TABLE for PostgreSQL databases to enable CASCADE feature. -# Otherwise it is not possible to delete the database schema with: -# MODEL.metadata.drop_all(engine) -# -@compiles(DropTable, "postgresql") -def _compile_drop_table(element, compiler, **kwargs): # pylint: disable=locally-disabled,unused-argument - return compiler.visit_drop_table(element) + " CASCADE" - - -class BaseMixin: - """ - Base class providing usefull mixin functionality. - """ - id = sqlalchemy.Column( # pylint: disable=locally-disabled,invalid-name - sqlalchemy.Integer, - primary_key = True - ) - createtime = sqlalchemy.Column( - sqlalchemy.DateTime, - default = datetime.datetime.utcnow - ) - - def get_id(self): - """ - Getter for retrieving current primary ID. - """ - return self.id - - def to_dict(self): - """ - Export object into dictionary containing only primitive data types. - """ - raise NotImplementedError() - - def to_json(self): - """ - Export object into JSON string. - """ - return json.dumps( - self.to_dict(), - indent = 4, - sort_keys = True - ) - -_asoc_group_members = sqlalchemy.Table( # pylint: disable=locally-disabled,invalid-name - 'asoc_group_members', - MODEL.metadata, - sqlalchemy.Column( - 'group_id', - sqlalchemy.ForeignKey('groups.id'), - primary_key = True - ), - sqlalchemy.Column( - 'user_id', - sqlalchemy.ForeignKey('users.id'), - primary_key = True - ) -) -""" -Association table representing user*group relation: group membership. - -What users are members of what groups. -""" - -_asoc_group_members_wanted = sqlalchemy.Table( # pylint: disable=locally-disabled,invalid-name - 'asoc_group_members_wanted', - MODEL.metadata, - sqlalchemy.Column( - 'group_id', - sqlalchemy.ForeignKey('groups.id'), - primary_key = True - ), - sqlalchemy.Column( - 'user_id', - sqlalchemy.ForeignKey('users.id'), - primary_key = True - ), -) -""" -Association table representing user*group relation: wanted group membership. - -What users want to be members of what groups. -""" - -_asoc_group_managers = sqlalchemy.Table( # pylint: disable=locally-disabled,invalid-name - 'asoc_group_managers', - MODEL.metadata, - sqlalchemy.Column( - 'group_id', - sqlalchemy.ForeignKey('groups.id'), - primary_key = True - ), - sqlalchemy.Column( - 'user_id', - sqlalchemy.ForeignKey('users.id'), - primary_key = True - ) -) -""" -Association table representing user*group relation: group management. - -What users can manage what groups. -""" - - -class UserModel(MODEL, BaseMixin): # pylint: disable=locally-disabled,too-many-instance-attributes - """ - Class representing user objects within the SQL database mapped to ``users`` - table. - """ - __tablename__ = 'users' - - login = sqlalchemy.Column( - sqlalchemy.String(50), - unique = True, - index = True - ) - fullname = sqlalchemy.Column( - sqlalchemy.String(100), - nullable = False - ) - email = sqlalchemy.Column( - sqlalchemy.String(250), - nullable = False - ) - roles = sqlalchemy.Column( - sqlalchemy.dialects.postgresql.ARRAY( - sqlalchemy.String(20), - dimensions = 1 - ), - nullable = False, - default = [] - ) - enabled = sqlalchemy.Column( - sqlalchemy.Boolean, - nullable = False, - default = True - ) - - password = sqlalchemy.Column( - sqlalchemy.String - ) - apikey = sqlalchemy.Column( - sqlalchemy.String, - index = True - ) - - locale = sqlalchemy.Column( - sqlalchemy.String(20) - ) - timezone = sqlalchemy.Column( - sqlalchemy.String(50) - ) - - memberships = sqlalchemy.orm.relationship( - 'GroupModel', - secondary = _asoc_group_members, - back_populates = 'members' - ) - memberships_wanted = sqlalchemy.orm.relationship( - 'GroupModel', - secondary = _asoc_group_members_wanted, - back_populates = - 'members_wanted', - order_by = 'GroupModel.name' - ) - managements = sqlalchemy.orm.relationship( - 'GroupModel', - secondary = _asoc_group_managers, - back_populates = 'managers' - ) - - changelogs = sqlalchemy.orm.relationship('ItemChangeLogModel', back_populates = 'author', order_by = 'ItemChangeLogModel.createtime') - - logintime = sqlalchemy.Column( - sqlalchemy.DateTime - ) - - def __repr__(self): - return "<User(login='{}', fullname='{}')>".format(self.login, self.fullname) - - def __str__(self): - return '{}'.format(self.login) - - def to_dict(self): - """ - *Interface implementation:* Implementation of :py:func:`mydojo.db.BaseMixin.to_dict` method. - """ - return { - 'id': self.id, - 'createtime': str(self.createtime), - 'logintime': str(self.logintime), - 'login': self.login, - 'fullname': self.fullname, - 'email': self.email, - 'roles': [ str(x) for x in self.roles], - 'apikey': self.apikey, - 'password': self.password, - 'enabled': bool(self.enabled), - 'locale': self.locale, - 'timezone': self.timezone, - 'memberships': [(x.id, x.name) for x in self.memberships], - 'memberships_wanted': [(x.id, x.name) for x in self.memberships_wanted], - 'managements': [(x.id, x.name) for x in self.managements] - } - - @classmethod - def from_dict(cls, structure, defaults = None): - """ - Convenience method for creating :py:class:`mydojo.db.UserModel` object - from ``dict`` objects. - """ - if not defaults: - defaults = {} - - sqlobj = cls() - sqlobj.login = structure.get('login') - sqlobj.fullname = structure.get('fullname') - sqlobj.email = structure.get('email', structure.get('login')) - sqlobj.roles = [str(i) for i in structure.get('roles', [])] - sqlobj.enabled = structure.get('enabled', None) - sqlobj.password = structure.get('password', None) - sqlobj.apikey = structure.get('apikey', None) - sqlobj.locale = structure.get('locale', None) - sqlobj.timezone = structure.get('timezone', None) - - return sqlobj - - @property - def is_authenticated(self): - """ - Mandatory interface required by the :py:mod:`flask_login` extension. - """ - return True - - @property - def is_active(self): - """ - Mandatory interface required by the :py:mod:`flask_login` extension. - """ - return self.enabled - - @property - def is_anonymous(self): - """ - Mandatory interface required by the :py:mod:`flask_login` extension. - """ - return False - - def get_id(self): - """ - Mandatory interface required by the :py:mod:`flask_login` extension. - """ - try: - return unicode(self.id) # python 2 - except NameError: - return str(self.id) # python 3 - - def has_role(self, role): - """ - Returns ``True`` if the user identifies with the specified role. - - :param str role: A role name. - """ - return role in self.roles - - def has_no_role(self): - """ - Returns ``True`` if the user has no role. - """ - return len(self.roles) == 0 - - def set_password(self, password_plain): - """ - Generate and set password hash from given plain text password. - """ - self.password = generate_password_hash(password_plain) - - def check_password(self, password_plain): - """ - Check given plaintext password agains internal password hash. - """ - return check_password_hash(self.password, password_plain) - - def is_state_enabled(self): - """ - Check if current user account state is enabled. - """ - return self.enabled - - def is_state_disabled(self): - """ - Check if current user account state is disabled. - """ - return not self.enabled - - def set_state_enabled(self): - """ - Set current user account state to enabled. - """ - self.enabled = True - - def set_state_disabled(self): - """ - Set current user account state to disabled. - """ - self.enabled = False - - -class GroupModel(MODEL, BaseMixin): - """ - Class representing group objects within the SQL database mapped to ``groups`` - table. - """ - __tablename__ = 'groups' - - name = sqlalchemy.Column( - sqlalchemy.String(100), - unique = True, - index = True - ) - description = sqlalchemy.Column( - sqlalchemy.String - ) - enabled = sqlalchemy.Column( - sqlalchemy.Boolean, - nullable = False, - default = True - ) - - members = sqlalchemy.orm.relationship( - 'UserModel', - secondary = _asoc_group_members, - back_populates = 'memberships' - ) - members_wanted = sqlalchemy.orm.relationship( - 'UserModel', - secondary = _asoc_group_members_wanted, - back_populates = 'memberships_wanted', - order_by = 'UserModel.fullname' - ) - managers = sqlalchemy.orm.relationship( - 'UserModel', - secondary = _asoc_group_managers, - back_populates = 'managements' - ) - - parent_id = sqlalchemy.Column( - sqlalchemy.Integer, - sqlalchemy.ForeignKey('groups.id') - ) - children = sqlalchemy.orm.relationship( - 'GroupModel', - backref = sqlalchemy.orm.backref( - 'parent', - remote_side = 'GroupModel.id' - ) - ) - - def __repr__(self): - return "<Group(name='{}')>".format(self.name) - - def __str__(self): - return '{}'.format(self.name) - - def to_dict(self): - """ - *Interface implementation:* Implementation of :py:func:`mydojo.db.BaseMixin.to_dict` method. - """ - return { - 'id': int(self.id), - 'createtime': str(self.createtime), - 'name': str(self.name), - 'description': str(self.description), - 'enabled': bool(self.enabled), - 'members': [(x.id, x.login) for x in self.members], - 'managers': [(x.id, x.login) for x in self.managers], - 'parent': str(self.parent), - } - - @classmethod - def from_dict(cls, structure, defaults = None): - """ - Convenience method for creating :py:class:`mydojo.db.GroupModel` object - from ``dict`` objects. - """ - if not defaults: - defaults = {} - - sqlobj = cls() - sqlobj.createtime = structure.get('createtime') - sqlobj.name = structure.get('name') - sqlobj.description = structure.get('description', '-- undisclosed --') - - return sqlobj - - -class ItemChangeLogModel(MODEL, BaseMixin): - """ - Class representing item changelog records within the SQL database mapped to - ``changelogs_items`` table. - """ - __tablename__ = 'changelogs_items' - - author_id = sqlalchemy.Column( - sqlalchemy.Integer, - sqlalchemy.ForeignKey('users.id', onupdate = "CASCADE") - ) - author = sqlalchemy.orm.relationship( - 'UserModel', - back_populates = 'changelogs', - enable_typechecks = False - ) - model_id = sqlalchemy.Column( - sqlalchemy.Integer, - nullable = False - ) - model = sqlalchemy.Column( - sqlalchemy.String, - nullable = False - ) - endpoint = sqlalchemy.Column( - sqlalchemy.String, - nullable = False - ) - module = sqlalchemy.Column( - sqlalchemy.String, - nullable = False - ) - operation = sqlalchemy.Column( - sqlalchemy.String, - nullable = False - ) - before = sqlalchemy.Column( - sqlalchemy.String, - nullable = False - ) - after = sqlalchemy.Column( - sqlalchemy.String, - nullable = False - ) - diff = sqlalchemy.Column( - sqlalchemy.String, - nullable = False - ) - - def __repr__(self): - return "<ItemChangelog(author='%s',operation='%s',model='%s#%s')>" % (str(self.author), self.operation, self.model, self.model_id) - - def __str__(self): - return 'ICL#{:d}:{:s}#{:d}:{:s}'.format(self.id, self.model, self.model_id, self.operation) - - def calculate_diff(self): - """ - Calculate difference between internal ``before`` and ``after`` attributes - and store it internally into ``diff`` attribute. - """ - self.diff = jsondiff(self.before, self.after) - - -#------------------------------------------------------------------------------- - - -def jsondiff(json_obj_a, json_obj_b): - """ - Calculate the difference between two model objects given as JSON strings. - """ - return "\n".join( - difflib.unified_diff(json_obj_a.split("\n"), json_obj_b.split("\n")) - ) - -def dictdiff(dict_obj_a, dict_obj_b): - """ - Calculate the difference between two model objects given as dicts. - """ - json_obj_a = json.dumps(dict_obj_a, indent = 4, sort_keys = True) - json_obj_b = json.dumps(dict_obj_b, indent = 4, sort_keys = True) - return jsondiff(json_obj_a, json_obj_b) - -def diff(obj_a, obj_b): - """ - Calculate the difference between two model objects given as dicts. - """ - return jsondiff(obj_a.to_json(), obj_b.to_json()) diff --git a/lib/vial/test/__init__.py b/lib/vial/test/__init__.py deleted file mode 100644 index 3b06e400ea0f531cdde8c68c16c354aaa1eca6f5..0000000000000000000000000000000000000000 --- a/lib/vial/test/__init__.py +++ /dev/null @@ -1,600 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- -# Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- - - -""" -Base library for web interface unit tests. -""" - - -import io -import unittest -import pprint -import logging -import flask_mail -import vial -import vial.app -import vial.db - - -class do_as_user_decorator: # pylint: disable=locally-disabled,invalid-name,too-few-public-methods - """ - Decorator class for accessing application endpoints as given user. - """ - def __init__(self, user_name, login_func_name = 'login_dev', login_func_params = None): - self.user_name = user_name - self.login_func_name = login_func_name - self.login_func_params = login_func_params or {} - - def __call__(self, func): - def wrapped_f(other_self, *args, **kwargs): - login_func = getattr(other_self, self.login_func_name) - response = login_func(self.user_name, **self.login_func_params) - other_self.assertEqual(response.status_code, 200) - - func(other_self, *args, **kwargs) - - response = other_self.logout() - other_self.assertEqual(response.status_code, 200) - - return wrapped_f - - -def app_context_wrapper_decorator(func): - """ - Decorator class for conditional wrapping of given function with application context. - """ - def wrapped_f(self, *args, **kwargs): - if 'with_app_ctx' not in kwargs or not kwargs['with_app_ctx']: - return func(self, *args, **kwargs) - with self.app.app_context(): - return func(self, *args, **kwargs) - - return wrapped_f - - -class VialTestCase(unittest.TestCase): - """ - Class for testing :py:class:`vial.app.Vial` application. - """ - - logger = logging.getLogger() - logger.level = logging.DEBUG - - def on_email_sent(self, message, app): # pylint: disable=locally-disabled,unused-argument - """ - Signal handler for handling :py:func:`flask_mail.email_dispatched` signal. - Log subject and recipients of all emails that have been sent. - """ - #pprint.pprint(message) - #app.logger.info( - # "Sent email '%s' to '%s'", - # message.subject, - # ', '.join(message.recipients) - #) - self.mailbox.append(message) - - #-------------------------------------------------------------------------- - - def setUp(self): - self.setup_logging() - self.mailbox = [] - self.fixtures_db = [] - - self.app = self.setup_app() - self.client = self.app.test_client() - self.setup_db() - - def setup_logging(self): - """ - Setup logging configuration for testing purposes. - """ - #for hdlr in self.logger.handlers: - # self.logger.removeHandler(hdlr) - self.loghdlr = logging.StreamHandler(io.StringIO()) - self.logger.addHandler(self.loghdlr) - - def setup_app(self): - """ - Setup application object. - """ - raise NotImplementedError() - - def setup_db(self): - """ - Perform database setup. - """ - with self.app.app_context(): - vial.db.db_get().drop_all() - vial.db.db_get().create_all() - self.setup_fixtures_db() - - def get_fixtures_db(self, app): - raise NotImplementedError() - - def setup_fixtures_db(self): - """ - Setup general database object fixtures. - """ - fixture_list = self.get_fixtures_db(self.app) - for dbobject in fixture_list: - vial.db.db_session().add(dbobject) - vial.db.db_session().commit() - self.fixtures_db.extend(fixture_list) - - #-------------------------------------------------------------------------- - - def tearDown(self): - self.teardown_logging() - self.teardown_db() - - def teardown_logging(self): - """ - Teardown logging configuration for testing purposes. - """ - #print( - # "CAPTURED LOG CONTENTS:\n{}".format( - # self.loghdlr.stream.getvalue() - # ) - #) - self.loghdlr.stream.close() - self.logger.removeHandler(self.loghdlr) - - def teardown_db(self): - with self.app.app_context(): - vial.db.db_get().drop_all() - - #-------------------------------------------------------------------------- - - def login_dev(self, login): - """ - Login given user with *auth_dev* module. - """ - return self.client.post( - '/auth_dev/login', - data = dict(login = login, submit = 'Login'), - follow_redirects = True - ) - - def login_pwd(self, login, password): - """ - Login given user with *auth_pwd* module. - """ - return self.client.post( - '/auth_pwd/login', - data = dict(login = login, password = password, submit = 'Login'), - follow_redirects = True - ) - - def login_env(self, login, envvar = 'eppn'): - """ - Login given user with *auth_env* module. - """ - return self.client.get( - '/auth_env/login', - environ_base = {envvar: login}, - follow_redirects = True - ) - - def logout(self): - """ - Logout current user. - """ - return self.client.get( - '/logout', - follow_redirects = True - ) - - #-------------------------------------------------------------------------- - - def log_get(self): - """ - Get content written to log so far. - """ - return self.loghdlr.stream.getvalue() - - def log_clear(self): - """ - Clear log content. - """ - self.loghdlr.stream.close() - self.loghdlr.stream = io.StringIO() - - #-------------------------------------------------------------------------- - - def mailbox_clear(self): - """ - Clear internal mailbox. - """ - self.mailbox = [] - - def mailbox_monitoring(self, state): - """ - Enable/disable mailbox monitoring. - """ - if state == 'on': - flask_mail.email_dispatched.connect(self.on_email_sent) - return - - if state == 'off': - flask_mail.email_dispatched.disconnect(self.on_email_sent) - return - - raise ValueError( - 'Invalid parameter for mailbox_monitoring, must be "on" or "off", received {}'.format( - str(state) - ) - ) - - #-------------------------------------------------------------------------- - - def user_model(self): - """ - Get user model class. - """ - return self.app.get_model(vial.const.MODEL_USER) - - @app_context_wrapper_decorator - def user_get(self, user_name, **kwargs): # pylint: disable=locally-disabled,unused-argument - """ - Get user object according to given user name from database. - """ - user_model = self.user_model() - return vial.db.db_session().query(user_model).filter(user_model.login == user_name).one_or_none() - - @app_context_wrapper_decorator - def user_save(self, user_object, **kwargs): # pylint: disable=locally-disabled,unused-argument - """ - Update given user object within database. - """ - vial.db.db_session().add(user_object) - vial.db.db_session().commit() - - @app_context_wrapper_decorator - def user_id(self, user_name, **kwargs): # pylint: disable=locally-disabled,unused-argument - """ - Get ID of user with given name within database. - """ - uobj = self.user_get(user_name) - return uobj.id - - @app_context_wrapper_decorator - def user_enabled(self, user_name, state, **kwargs): # pylint: disable=locally-disabled,unused-argument - """ - Enable/disable given user within database. - """ - user = self.user_get(user_name) - user.enabled = state - self.user_save(user) - - #-------------------------------------------------------------------------- - - def group_model(self): - """ - Get user model class. - """ - return self.app.get_model(vial.const.MODEL_GROUP) - - @app_context_wrapper_decorator - def group_get(self, group_name, **kwargs): # pylint: disable=locally-disabled,unused-argument - """ - Get group object according to given group name within database. - """ - group_model = self.group_model() - return vial.db.db_session().query(group_model).filter(group_model.name == group_name).one_or_none() - - @app_context_wrapper_decorator - def group_save(self, group_object, **kwargs): # pylint: disable=locally-disabled,unused-argument - """ - Update given group object within database. - """ - vial.db.db_session().add(group_object) - vial.db.db_session().commit() - - @app_context_wrapper_decorator - def group_id(self, group_name, **kwargs): # pylint: disable=locally-disabled,unused-argument - """ - Get ID of given group within database. - """ - gobj = self.group_get(group_name) - return gobj.id - - @app_context_wrapper_decorator - def group_enabled(self, group_name, state, **kwargs): # pylint: disable=locally-disabled,unused-argument - """ - Enable/disable given group within database. - """ - group = self.group_get(group_name) - group.enabled = state - self.group_save(group) - - #-------------------------------------------------------------------------- - - def assertGetURL(self, url, status_code = 200, content_checks = None, print_response = False, follow_redirects = True): # pylint: disable=locally-disabled,invalid-name - """ - Perform GET request and check some default assertions against the response. - """ - response = self.client.get( - url, - follow_redirects = follow_redirects - ) - if print_response: - print("--------------------------------------------------------------------------------") - print("Response for GET {}: {} ({})".format(url, response.status_code, response.status)) - pprint.pprint(response.headers) - pprint.pprint(response.data) - print("--------------------------------------------------------------------------------") - self.assertEqual(response.status_code, status_code) - if content_checks: - for cch in content_checks: - self.assertTrue(cch in response.data) - return response - - def assertPostURL(self, url, data, status_code = 200, content_checks = None, print_response = False, follow_redirects = True): # pylint: disable=locally-disabled,invalid-name - """ - Perform POST request and check some default assertions against the response. - """ - response = self.client.post( - url, - data = data, - follow_redirects = follow_redirects - ) - if print_response: - print("--------------------------------------------------------------------------------") - print("Response for POST {}, {}: {} ({})".format(url, pprint.pformat(data), response.status_code, response.status)) - pprint.pprint(response.headers) - pprint.pprint(response.data) - print("--------------------------------------------------------------------------------") - self.assertEqual(response.status_code, status_code) - if content_checks: - for cch in content_checks: - self.assertTrue(cch in response.data) - return response - - def assertMailbox(self, checklist): # pylint: disable=locally-disabled,invalid-name - """ - Check internal mailbox. - """ - for attr_name in ('subject', 'sender', 'recipients', 'cc', 'bcc', 'body', 'html'): - if attr_name in checklist: - self.assertEqual( - list( - map( - lambda x: getattr(x, attr_name), - self.mailbox, - ) - ), - checklist[attr_name] - ) - -class ItemCreateVialTestCase(VialTestCase): - """ - Class for testing :py:class:`vial.app.Vial` application item creation views. - """ - maxDiff = None - - def assertCreate(self, item_model, url, data, content_checks = None, print_response = False): # pylint: disable=locally-disabled,invalid-name - """ - Perform attempt to create given item. - """ - - # Verify, that the item form correctly displays. - response = self.assertGetURL( - url, - 200, - [ - '<form method="POST" action="{}" id="form-{}-create'.format( - url, - item_model.__name__.lower() - ).encode('utf8'), - b'<div class="btn-toolbar" role="toolbar" aria-label="Form submission buttons">' - ], - print_response = print_response - ) - - # Attempt to send empty item form. There is always at least one mandatory - # form field, so we should get some "This field is required." error. - request_data = {'submit': 'Submit'} - response = self.assertPostURL( - url, - request_data, - 200, - [ - b'This field is required.', - b'help-block form-error' - ], - print_response = print_response - ) - - # Attempt to send form with some mandatory fields missing. - #for idx, param in enumerate(data): - # if idx == len(data) - 1: - # break - # response = self.client.post( - # url, - # follow_redirects = True, - # data = { - # i[0]: i[1] for i in data[0:idx+1] - # } - # ) - # self.assertEqual(response.status_code, 200) - # self.assertTrue(b'This field is required.' in response.data) - # self.assertTrue(b'help-block form-error' in response.data) - - # Attempt to send form with valid data. - request_data = { - i[0]: i[1] for i in data - } - request_data['submit'] = 'Submit' - response = self.assertPostURL( - url, - request_data, - 200, - [ - b'<div class="alert alert-success alert-dismissible">' - ], - print_response = print_response - ) - if content_checks: - for cch in content_checks: - self.assertTrue(cch in response.data) - return response - - -class RegistrationVialTestCase(VialTestCase): - """ - Class for testing :py:class:`vial.app.Vial` application registration views. - """ - maxDiff = None - - user_fixture = { - 'apikey': None, - 'email': 'test.user@domain.org', - 'enabled': False, - 'fullname': 'Test User', - 'id': 5, - 'locale': None, - 'login': 'test', - 'logintime': 'None', - 'managements': [], - 'memberships': [], - 'memberships_wanted': [], - 'roles': ['user'], - 'timezone': None - } - - def assertRegisterFail(self, url, data, environ_base = None): # pylint: disable=locally-disabled,invalid-name - response = response = self.client.get( - url, - follow_redirects = True, - environ_base = environ_base, - ) - self.assertEqual(response.status_code, 200) - self.assertTrue(b'User account registration' in response.data) - - for idx, param in enumerate(data): - if idx == len(data) - 1: - break - response = response = self.client.post( - url, - follow_redirects = True, - environ_base = environ_base, - data = { - i[0]: i[1] for i in data[0:idx+1] - } - ) - self.assertEqual(response.status_code, 200) - self.assertTrue(b'This field is required.' in response.data) - self.assertTrue(b'help-block form-error' in response.data) - - response = response = self.client.post( - url, - follow_redirects = True, - environ_base = environ_base, - data = { - i[0]: i[1] for i in data - } - ) - self.assertEqual(response.status_code, 200) - self.assertTrue( - b'Please use different login, the "user" is already taken.' in response.data or \ - b'Please use different login, the "user" is already taken.' in response.data - ) - - - def assertRegister(self, url, data, emails, environ_base = None): # pylint: disable=locally-disabled,invalid-name - uname = 'test' - self.mailbox_monitoring('on') - - response = response = self.client.get( - url, - follow_redirects = True, - environ_base = environ_base - ) - self.assertEqual(response.status_code, 200) - self.assertTrue(b'User account registration' in response.data) - - for idx, param in enumerate(data): - if idx == len(data) - 1: - break - response = response = self.client.post( - url, - follow_redirects = True, - environ_base = environ_base, - data = { - i[0]: i[1] for i in data[0:idx+1] - } - ) - self.assertEqual(response.status_code, 200) - self.assertTrue(b'This field is required.' in response.data) - self.assertTrue(b'help-block form-error' in response.data) - - response = response = self.client.post( - url, - follow_redirects = True, - environ_base = environ_base, - data = { - i[0]: i[1] for i in data - } - ) - self.assertEqual(response.status_code, 200) - self.assertTrue(b'User account <strong>test (Test User)</strong> was successfully registered.' in response.data) - with self.app.app_context(): - uobj = self.user_get(uname) - self.assertTrue(uobj) - self.assertMailbox( - { - 'subject': [ - '[{}] Account registration - {}'.format(self.app.config['APPLICATION_NAME'], uname), - '[{}] Account registration - {}'.format(self.app.config['APPLICATION_NAME'], uname) - ], - 'sender': [ - 'root@unittest', - 'root@unittest' - ], - 'recipients': [ - ['admin@unittest'], - ['test.user@domain.org'] - ], - 'cc': [[],[]], - 'bcc': [[], ['admin@unittest']], - 'body': emails['txt'], - 'html': emails['html'] - } - ) - - self.mailbox_monitoring('off') - - with self.app.app_context(): - user = self.user_get(uname) - user_dict = user.to_dict() - del user_dict['createtime'] - del user_dict['password'] - self.assertEqual( - user_dict, - self.user_fixture - ) - response = self.login_dev(uname) - self.assertEqual(response.status_code, 403) - #self.assertTrue(b'is currently disabled, you are not permitted to log in.' in response.data) - - with self.app.app_context(): - user = self.user_get(uname) - user.set_state_enabled() - self.user_save(user) - - with self.app.app_context(): - user = self.user_get(uname) - user_dict = user.to_dict() - del user_dict['createtime'] - del user_dict['password'] - response = self.login_dev(uname) - self.assertEqual(response.status_code, 200) - self.assertTrue(b'You have been successfully logged in as' in response.data) - - response = self.logout() - self.assertEqual(response.status_code, 200) - self.assertTrue(b'You have been successfully logged out' in response.data) diff --git a/lib/vial/test/runner.py b/lib/vial/test/runner.py deleted file mode 100644 index 8854446a380a70bb550c6ef1abafde3256644ce3..0000000000000000000000000000000000000000 --- a/lib/vial/test/runner.py +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- -# Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- - - -""" -Base library for web interface unit tests. -""" - - -import vial -import vial.app -import vial.db -import vial.model.db -import vial.test.fixtures - - -#logging.disable(logging.CRITICAL+1000) - - -def _config_testapp_vial(app_config): - """ - Configure and reconfigure application for testing before instantination. - """ - - # Customize configurations for testing purposes. - app_config['TESTING'] = True - app_config['WTF_CSRF_ENABLED'] = False - app_config['DEBUG'] = False - app_config['EXPLAIN_TEMPLATE_LOADING'] = False - app_config['LOG_FILE'] = '/var/tmp/vial-utest.log' - app_config['SQLALCHEMY_DATABASE_URI'] = 'postgresql://mentat:mentat@localhost/mentat_utest' - app_config['MAIL_SERVER'] = 'localhost' - app_config['MAIL_PORT'] = 1025 - app_config['MAIL_DEFAULT_SENDER'] = 'root@unittest' - app_config['EMAIL_ADMINS'] = ['admin@unittest'] - app_config['DISABLE_MAIL_LOGGING'] = True - app_config['MODELS'] = { - vial.const.MODEL_USER: vial.model.db.UserModel, - vial.const.MODEL_GROUP: vial.model.db.GroupModel, - vial.const.MODEL_ITEM_CHANGELOG: vial.model.db.ItemChangeLogModel - } - - -class TestRunnerMixin: - def setup_app(self): - """ - Setup application object. - """ - return vial.create_app_full( - vial.app.Vial, - 'vial', - config_object = 'vial.config.TestingConfig', - config_func = _config_testapp_vial - ) - - def get_fixtures_db(self, app): - return vial.test.fixtures.get_fixtures_db(app) diff --git a/vagrantenv/system/login-banner.sh b/vagrantenv/system/login-banner.sh index 5fa379a69f887df707f74aa99f5d0e75ccf8264c..63f14571183e10486000a81a6590014abde54615 100755 --- a/vagrantenv/system/login-banner.sh +++ b/vagrantenv/system/login-banner.sh @@ -25,7 +25,7 @@ echo " Use command alias 've' to activate project's virtual environment." echo "" echo " Launching:" echo " mentat-controller.py --command start # Mentat backend system" -echo " make run-webui-dev # Flask's web interface (5000)" +echo " make run-webui-vagrant # Flask's web interface (5000)" echo "" echo " Documentation:" echo " https://alchemist.cesnet.cz/mentat/doc/development/html/_doclib/development.html#development-with-vagrant"