From 8efed2327dce3cbba7d4eac1fe5e5844993b4623 Mon Sep 17 00:00:00 2001 From: Jan Mach <jan.mach@cesnet.cz> Date: Mon, 21 Mar 2022 17:36:16 +0100 Subject: [PATCH] Merged all of Vial into Hawat Unfinished, unpolished work. (Redmine issue: #7544) --- lib/hawat/app.py | 26 +- lib/hawat/base.py | 994 ++++++++++++- lib/hawat/blueprints/auth/__init__.py | 6 +- .../blueprints/auth_dev/test/__init__.py | 6 +- lib/hawat/blueprints/auth_env/__init__.py | 89 +- lib/hawat/blueprints/auth_env/forms.py | 2 +- .../blueprints/auth_env/test/__init__.py | 6 +- lib/hawat/blueprints/auth_pwd/__init__.py | 93 +- lib/hawat/blueprints/auth_pwd/forms.py | 27 +- .../blueprints/auth_pwd/test/__init__.py | 6 +- .../blueprints/changelogs/__init__.py | 22 +- .../blueprints/changelogs/forms.py | 24 +- .../templates/changelogs/search.html | 0 .../changelogs/templates/changelogs/show.html | 0 .../blueprints/changelogs/test/__init__.py | 20 +- lib/hawat/blueprints/dbstatus/__init__.py | 42 +- .../blueprints/dbstatus/test/__init__.py | 36 +- .../blueprints/design_bs3/__init__.py | 8 +- .../design_bs3/templates/_layout.html | 0 .../templates/_layout_confirmation.html | 0 .../templates/_layout_creatupdate.html | 0 .../templates/_layout_events_search.html | 0 .../design_bs3/templates/_layout_list.html | 0 .../design_bs3/templates/_layout_login.html | 0 .../templates/_layout_registration.html | 0 .../design_bs3/templates/_layout_search.html | 0 .../design_bs3/templates/_macros_chart.html | 0 .../design_bs3/templates/_macros_form.html | 0 .../design_bs3/templates/_macros_page.html | 0 .../design_bs3/templates/_macros_site.html | 0 .../design_bs3/templates/form_delete.html | 0 .../design_bs3/templates/form_disable.html | 0 .../design_bs3/templates/form_enable.html | 0 .../design_bs3/templates/http_error.html | 0 .../templates/spt_flashmessage.html | 0 .../blueprints/devtools/__init__.py | 16 +- .../devtools/templates/devtools/config.html | 0 .../blueprints/devtools/test/__init__.py | 20 +- lib/hawat/blueprints/dnsr/__init__.py | 18 +- lib/hawat/blueprints/dnsr/test/__init__.py | 16 +- lib/hawat/blueprints/events/__init__.py | 26 +- lib/hawat/blueprints/events/forms.py | 52 +- lib/hawat/blueprints/events/test/__init__.py | 26 +- lib/hawat/blueprints/filters/__init__.py | 52 +- lib/hawat/blueprints/filters/forms.py | 26 +- lib/hawat/blueprints/filters/test/__init__.py | 170 +-- lib/hawat/blueprints/geoip/__init__.py | 18 +- lib/hawat/blueprints/geoip/forms.py | 4 +- lib/hawat/blueprints/geoip/test/__init__.py | 16 +- lib/hawat/blueprints/groups/__init__.py | 912 +++++++++++- lib/hawat/blueprints/groups/forms.py | 133 +- lib/hawat/blueprints/groups/test/__init__.py | 150 +- lib/hawat/blueprints/home/__init__.py | 12 +- lib/hawat/blueprints/home/test/__init__.py | 16 +- lib/hawat/blueprints/hosts/__init__.py | 20 +- lib/hawat/blueprints/hosts/forms.py | 12 +- lib/hawat/blueprints/hosts/test/__init__.py | 16 +- lib/hawat/blueprints/nerd/__init__.py | 20 +- lib/hawat/blueprints/nerd/forms.py | 4 +- lib/hawat/blueprints/nerd/test/__init__.py | 16 +- lib/hawat/blueprints/networks/__init__.py | 42 +- lib/hawat/blueprints/networks/forms.py | 18 +- .../blueprints/networks/test/__init__.py | 144 +- lib/hawat/blueprints/pdnsr/__init__.py | 18 +- lib/hawat/blueprints/pdnsr/forms.py | 4 +- lib/hawat/blueprints/pdnsr/test/__init__.py | 16 +- lib/hawat/blueprints/performance/__init__.py | 12 +- .../blueprints/performance/test/__init__.py | 16 +- lib/hawat/blueprints/reports/__init__.py | 30 +- lib/hawat/blueprints/reports/forms.py | 28 +- lib/hawat/blueprints/reports/test/__init__.py | 16 +- .../blueprints/settings_reporting/__init__.py | 32 +- .../blueprints/settings_reporting/forms.py | 30 +- lib/hawat/blueprints/skeleton/__init__.py | 12 +- lib/hawat/blueprints/status/__init__.py | 16 +- lib/hawat/blueprints/status/test/__init__.py | 16 +- lib/hawat/blueprints/timeline/__init__.py | 22 +- lib/hawat/blueprints/timeline/forms.py | 42 +- .../blueprints/timeline/test/__init__.py | 16 +- lib/hawat/blueprints/users/__init__.py | 1107 ++++++++++++++- lib/hawat/blueprints/users/forms.py | 141 +- lib/hawat/blueprints/users/test/__init__.py | 136 +- lib/hawat/blueprints/whois/__init__.py | 18 +- lib/hawat/blueprints/whois/test/__init__.py | 16 +- lib/hawat/test/__init__.py | 580 +++++++- lib/{vial => hawat}/test/fixtures.py | 0 lib/hawat/test/runner.py | 4 +- lib/vial/__init__.py | 63 - lib/vial/app.py | 1038 -------------- lib/vial/blueprints/__init__.py | 0 lib/vial/blueprints/auth_env/__init__.py | 290 ---- lib/vial/blueprints/auth_env/forms.py | 37 - .../templates/auth_env/registration.html | 26 - .../auth_env/registration/email_admins.txt | 26 - .../auth_env/registration/email_managers.txt | 19 - .../auth_env/registration/email_user.txt | 30 - lib/vial/blueprints/auth_env/test/__init__.py | 188 --- lib/vial/blueprints/auth_pwd/__init__.py | 148 -- lib/vial/blueprints/auth_pwd/forms.py | 89 -- .../auth_pwd/templates/auth_pwd/login.html | 6 - .../templates/auth_pwd/registration.html | 24 - .../templates/registration/email_admins.txt | 26 - .../templates/registration/email_managers.txt | 19 - .../templates/registration/email_user.txt | 30 - lib/vial/blueprints/auth_pwd/test/__init__.py | 176 --- lib/vial/blueprints/groups/__init__.py | 1108 --------------- lib/vial/blueprints/groups/forms.py | 255 ---- .../groups/templates/groups/addmember.html | 16 - .../groups/templates/groups/creatupdate.html | 32 - .../groups/templates/groups/list.html | 78 -- .../groups/templates/groups/rejectmember.html | 16 - .../groups/templates/groups/removemember.html | 16 - .../groups/templates/groups/show.html | 187 --- lib/vial/blueprints/groups/test/__init__.py | 541 -------- lib/vial/blueprints/home/__init__.py | 85 -- .../blueprints/home/templates/home/index.html | 39 - lib/vial/blueprints/home/test/__init__.py | 70 - lib/vial/blueprints/users/__init__.py | 1230 ----------------- lib/vial/blueprints/users/forms.py | 284 ---- .../users/templates/users/addmembership.html | 16 - .../users/templates/users/creatupdate.html | 38 - .../templates/users/email_activation.txt | 9 - .../users/templates/users/list.html | 78 -- .../templates/users/rejectmembership.html | 16 - .../templates/users/removemembership.html | 16 - .../users/templates/users/show.html | 281 ---- lib/vial/blueprints/users/test/__init__.py | 606 -------- lib/vial/blueprints/users/test/utils.py | 209 --- lib/vial/test/__init__.py | 600 -------- lib/vial/test/runner.py | 60 - 130 files changed, 4811 insertions(+), 9020 deletions(-) rename lib/{vial => hawat}/blueprints/changelogs/__init__.py (91%) rename lib/{vial => hawat}/blueprints/changelogs/forms.py (84%) rename lib/{vial => hawat}/blueprints/changelogs/templates/changelogs/search.html (100%) rename lib/{vial => hawat}/blueprints/changelogs/templates/changelogs/show.html (100%) rename lib/{vial => hawat}/blueprints/changelogs/test/__init__.py (73%) rename lib/{vial => hawat}/blueprints/design_bs3/__init__.py (88%) rename lib/{vial => hawat}/blueprints/design_bs3/templates/_layout.html (100%) rename lib/{vial => hawat}/blueprints/design_bs3/templates/_layout_confirmation.html (100%) rename lib/{vial => hawat}/blueprints/design_bs3/templates/_layout_creatupdate.html (100%) rename lib/{vial => hawat}/blueprints/design_bs3/templates/_layout_events_search.html (100%) rename lib/{vial => hawat}/blueprints/design_bs3/templates/_layout_list.html (100%) rename lib/{vial => hawat}/blueprints/design_bs3/templates/_layout_login.html (100%) rename lib/{vial => hawat}/blueprints/design_bs3/templates/_layout_registration.html (100%) rename lib/{vial => hawat}/blueprints/design_bs3/templates/_layout_search.html (100%) rename lib/{vial => hawat}/blueprints/design_bs3/templates/_macros_chart.html (100%) rename lib/{vial => hawat}/blueprints/design_bs3/templates/_macros_form.html (100%) rename lib/{vial => hawat}/blueprints/design_bs3/templates/_macros_page.html (100%) rename lib/{vial => hawat}/blueprints/design_bs3/templates/_macros_site.html (100%) rename lib/{vial => hawat}/blueprints/design_bs3/templates/form_delete.html (100%) rename lib/{vial => hawat}/blueprints/design_bs3/templates/form_disable.html (100%) rename lib/{vial => hawat}/blueprints/design_bs3/templates/form_enable.html (100%) rename lib/{vial => hawat}/blueprints/design_bs3/templates/http_error.html (100%) rename lib/{vial => hawat}/blueprints/design_bs3/templates/spt_flashmessage.html (100%) rename lib/{vial => hawat}/blueprints/devtools/__init__.py (84%) rename lib/{vial => hawat}/blueprints/devtools/templates/devtools/config.html (100%) rename lib/{vial => hawat}/blueprints/devtools/test/__init__.py (78%) rename lib/{vial => hawat}/test/fixtures.py (100%) delete mode 100644 lib/vial/__init__.py delete mode 100644 lib/vial/app.py delete mode 100644 lib/vial/blueprints/__init__.py delete mode 100644 lib/vial/blueprints/auth_env/__init__.py delete mode 100644 lib/vial/blueprints/auth_env/forms.py delete mode 100644 lib/vial/blueprints/auth_env/templates/auth_env/registration.html delete mode 100644 lib/vial/blueprints/auth_env/templates/auth_env/registration/email_admins.txt delete mode 100644 lib/vial/blueprints/auth_env/templates/auth_env/registration/email_managers.txt delete mode 100644 lib/vial/blueprints/auth_env/templates/auth_env/registration/email_user.txt delete mode 100644 lib/vial/blueprints/auth_env/test/__init__.py delete mode 100644 lib/vial/blueprints/auth_pwd/__init__.py delete mode 100644 lib/vial/blueprints/auth_pwd/forms.py delete mode 100644 lib/vial/blueprints/auth_pwd/templates/auth_pwd/login.html delete mode 100644 lib/vial/blueprints/auth_pwd/templates/auth_pwd/registration.html delete mode 100644 lib/vial/blueprints/auth_pwd/templates/registration/email_admins.txt delete mode 100644 lib/vial/blueprints/auth_pwd/templates/registration/email_managers.txt delete mode 100644 lib/vial/blueprints/auth_pwd/templates/registration/email_user.txt delete mode 100644 lib/vial/blueprints/auth_pwd/test/__init__.py delete mode 100644 lib/vial/blueprints/groups/__init__.py delete mode 100644 lib/vial/blueprints/groups/forms.py delete mode 100644 lib/vial/blueprints/groups/templates/groups/addmember.html delete mode 100644 lib/vial/blueprints/groups/templates/groups/creatupdate.html delete mode 100644 lib/vial/blueprints/groups/templates/groups/list.html delete mode 100644 lib/vial/blueprints/groups/templates/groups/rejectmember.html delete mode 100644 lib/vial/blueprints/groups/templates/groups/removemember.html delete mode 100644 lib/vial/blueprints/groups/templates/groups/show.html delete mode 100644 lib/vial/blueprints/groups/test/__init__.py delete mode 100644 lib/vial/blueprints/home/__init__.py delete mode 100644 lib/vial/blueprints/home/templates/home/index.html delete mode 100644 lib/vial/blueprints/home/test/__init__.py delete mode 100644 lib/vial/blueprints/users/__init__.py delete mode 100644 lib/vial/blueprints/users/forms.py delete mode 100644 lib/vial/blueprints/users/templates/users/addmembership.html delete mode 100644 lib/vial/blueprints/users/templates/users/creatupdate.html delete mode 100644 lib/vial/blueprints/users/templates/users/email_activation.txt delete mode 100644 lib/vial/blueprints/users/templates/users/list.html delete mode 100644 lib/vial/blueprints/users/templates/users/rejectmembership.html delete mode 100644 lib/vial/blueprints/users/templates/users/removemembership.html delete mode 100644 lib/vial/blueprints/users/templates/users/show.html delete mode 100644 lib/vial/blueprints/users/test/__init__.py delete mode 100644 lib/vial/blueprints/users/test/utils.py delete mode 100644 lib/vial/test/__init__.py delete mode 100644 lib/vial/test/runner.py diff --git a/lib/hawat/app.py b/lib/hawat/app.py index 8f860f579..1f31db67f 100644 --- a/lib/hawat/app.py +++ b/lib/hawat/app.py @@ -65,16 +65,22 @@ def create_app_full( :return: Hawat application :rtype: hawat.base.HawatApp """ - - 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 - ) + app = hawat.base.HawatApp(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 def create_app(): diff --git a/lib/hawat/base.py b/lib/hawat/base.py index 04134ed3a..b0fd3ce94 100644 --- a/lib/hawat/base.py +++ b/lib/hawat/base.py @@ -13,7 +13,34 @@ __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea Kropáčová <andrea 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 + +import hawat.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 # # Flask related modules. @@ -47,7 +74,12 @@ 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:`vial.app.Vial` 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 +102,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 = 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 = {} + self.csag = {} self.oads = {} @@ -80,6 +125,235 @@ 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 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:`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:`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[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 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[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 @@ -153,13 +427,341 @@ class HawatApp(vial.app.Vial): def setup_app(self): - super().setup_app() - + """ + 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 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): - super()._setup_app_core() + """ + 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 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 = 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) @self.context_processor def jinja_inject_variables(): # pylint: disable=locally-disabled,unused-variable @@ -240,7 +842,30 @@ class HawatApp(vial.app.Vial): return self def _setup_app_db(self): - super()._setup_app_db() + """ + 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(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 """ @@ -279,6 +904,290 @@ class HawatApp(vial.app.Vial): mentat.services.sqlstorage.set_manager(StorageServiceManager()) + 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(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 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.'), + 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. + + :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(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( + 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(hawat.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] + )), + hawat.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[hawat.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 + def _setup_app_eventdb(self): """ Setup application database service for given Vial application. @@ -292,6 +1201,81 @@ 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:`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 class PsycopgMixin: """ diff --git a/lib/hawat/blueprints/auth/__init__.py b/lib/hawat/blueprints/auth/__init__.py index a101cbd89..bd3adcbca 100644 --- a/lib/hawat/blueprints/auth/__init__.py +++ b/lib/hawat/blueprints/auth/__init__.py @@ -100,7 +100,7 @@ class RegisterView(HTMLMixin, SimpleView): #------------------------------------------------------------------------------- -class AuthBlueprint(VialBlueprint): +class AuthBlueprint(HawatBlueprint): """Pluggable module - authentication and registration directional service (*auth*).""" @classmethod @@ -129,8 +129,8 @@ class AuthBlueprint(VialBlueprint): def get_blueprint(): """ - Mandatory interface for :py:mod:`hawat.Vial` and factory function. This function - must return a valid instance of :py:class:`hawat.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/auth_dev/test/__init__.py b/lib/hawat/blueprints/auth_dev/test/__init__.py index 370db18fe..191328d9f 100644 --- a/lib/hawat/blueprints/auth_dev/test/__init__.py +++ b/lib/hawat/blueprints/auth_dev/test/__init__.py @@ -16,9 +16,9 @@ Unit tests for :py:mod:`hawat.blueprints.auth_dev`. import unittest import hawat.const -import vial.test -import vial.test.fixtures -import vial.db +import hawat.test +import hawat.test.fixtures +import hawat.db from hawat.test import RegistrationHawatTestCase from hawat.test.runner import TestRunnerMixin diff --git a/lib/hawat/blueprints/auth_env/__init__.py b/lib/hawat/blueprints/auth_env/__init__.py index 192253a6f..6ea0425d0 100644 --- a/lib/hawat/blueprints/auth_env/__init__.py +++ b/lib/hawat/blueprints/auth_env/__init__.py @@ -74,17 +74,24 @@ __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 vial.forms -import vial.db -import vial.blueprints.auth_env -from vial.blueprints.auth_env import BLUEPRINT_NAME, LoginView +import hawat.forms +import hawat.db +from hawat.app import HawatBlueprint +from hawat.view import BaseLoginView, BaseRegisterView +from hawat.view.mixin import HTMLMixin, SQLAlchemyMixin + 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. @@ -106,11 +113,60 @@ def get_login_from_environment(): 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(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(vial.blueprints.auth_env.RegisterView): + +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): """ @@ -192,12 +248,27 @@ class RegisterView(vial.blueprints.auth_env.RegisterView): ) + 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) @@ -209,8 +280,8 @@ class EnvAuthBlueprint(vial.blueprints.auth_env.EnvAuthBlueprint): 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/auth_env/forms.py b/lib/hawat/blueprints/auth_env/forms.py index 2013422aa..82fdf2df5 100644 --- a/lib/hawat/blueprints/auth_env/forms.py +++ b/lib/hawat/blueprints/auth_env/forms.py @@ -22,7 +22,7 @@ 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 diff --git a/lib/hawat/blueprints/auth_env/test/__init__.py b/lib/hawat/blueprints/auth_env/test/__init__.py index 661cc63a5..beb387cb6 100644 --- a/lib/hawat/blueprints/auth_env/test/__init__.py +++ b/lib/hawat/blueprints/auth_env/test/__init__.py @@ -16,9 +16,9 @@ Unit tests for :py:mod:`hawat.blueprints.auth_env`. import unittest import hawat.const -import vial.test -import vial.test.fixtures -import vial.db +import hawat.test +import hawat.test.fixtures +import hawat.db from hawat.test import RegistrationHawatTestCase from hawat.test.runner import TestRunnerMixin diff --git a/lib/hawat/blueprints/auth_pwd/__init__.py b/lib/hawat/blueprints/auth_pwd/__init__.py index b6a17dd9f..cff95837b 100644 --- a/lib/hawat/blueprints/auth_pwd/__init__.py +++ b/lib/hawat/blueprints/auth_pwd/__init__.py @@ -29,19 +29,83 @@ __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea Kropáčová <andrea import flask +from flask_babel import lazy_gettext import hawat.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.forms +from hawat.app import HawatBlueprint +from hawat.view import BaseLoginView, BaseRegisterView +from hawat.view.mixin import HTMLMixin, SQLAlchemyMixin +from hawat.blueprints.auth_pwd.forms import LoginForm, RegisterUserAccountForm -class RegisterView(vial.blueprints.auth_pwd.RegisterView): +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(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): @@ -52,14 +116,29 @@ class RegisterView(vial.blueprints.auth_pwd.RegisterView): 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`. """ diff --git a/lib/hawat/blueprints/auth_pwd/forms.py b/lib/hawat/blueprints/auth_pwd/forms.py index 9d8ac9972..ec553ce68 100644 --- a/lib/hawat/blueprints/auth_pwd/forms.py +++ b/lib/hawat/blueprints/auth_pwd/forms.py @@ -17,13 +17,38 @@ __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 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. diff --git a/lib/hawat/blueprints/auth_pwd/test/__init__.py b/lib/hawat/blueprints/auth_pwd/test/__init__.py index 1b838fce3..da53a34e9 100644 --- a/lib/hawat/blueprints/auth_pwd/test/__init__.py +++ b/lib/hawat/blueprints/auth_pwd/test/__init__.py @@ -16,9 +16,9 @@ Unit tests for :py:mod:`hawat.blueprints.auth_pwd`. import unittest import hawat.const -import vial.test -import vial.test.fixtures -import vial.db +import hawat.test +import hawat.test.fixtures +import hawat.db from hawat.test import RegistrationHawatTestCase from hawat.test.runner import TestRunnerMixin diff --git a/lib/vial/blueprints/changelogs/__init__.py b/lib/hawat/blueprints/changelogs/__init__.py similarity index 91% rename from lib/vial/blueprints/changelogs/__init__.py rename to lib/hawat/blueprints/changelogs/__init__.py index f1109f6ce..7cdd3fcc9 100644 --- a/lib/vial/blueprints/changelogs/__init__.py +++ b/lib/hawat/blueprints/changelogs/__init__.py @@ -13,11 +13,11 @@ This pluggable module provides access to item changelogs. 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.acl +from hawat.app 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' @@ -32,7 +32,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): @@ -124,7 +124,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): @@ -149,7 +149,7 @@ class ShowView(HTMLMixin, SQLAlchemyMixin, ItemShowView): @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', @@ -171,7 +171,7 @@ class ShowView(HTMLMixin, SQLAlchemyMixin, ItemShowView): #------------------------------------------------------------------------------- -class ItemChangeLogsBlueprint(VialBlueprint): +class ItemChangeLogsBlueprint(HawatBlueprint): """Pluggable module - item changelog record management (*changelogs*).""" @classmethod @@ -192,8 +192,8 @@ class ItemChangeLogsBlueprint(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`. """ diff --git a/lib/vial/blueprints/changelogs/forms.py b/lib/hawat/blueprints/changelogs/forms.py similarity index 84% rename from lib/vial/blueprints/changelogs/forms.py rename to lib/hawat/blueprints/changelogs/forms.py index f3093ed0b..72ea33776 100644 --- a/lib/vial/blueprints/changelogs/forms.py +++ b/lib/hawat/blueprints/changelogs/forms.py @@ -17,8 +17,8 @@ import flask_wtf from flask_babel import lazy_gettext import hawat.const -import vial.forms -import vial.db +import hawat.forms +import hawat.db def get_item_operation_choices(): @@ -26,7 +26,7 @@ def get_item_operation_choices(): Return select choices for item changelog operations. """ item_changelog_model = flask.current_app.get_model(hawat.const.MODEL_ITEM_CHANGELOG) - operations_list = vial.db.db_query(item_changelog_model).\ + operations_list = hawat.db.db_query(item_changelog_model).\ distinct(item_changelog_model.operation).\ all() return list( @@ -41,7 +41,7 @@ def get_item_model_choices(): Return select choices for item changelog item models. """ item_changelog_model = flask.current_app.get_model(hawat.const.MODEL_ITEM_CHANGELOG) - models_list = vial.db.db_query(item_changelog_model).\ + models_list = hawat.db.db_query(item_changelog_model).\ distinct(item_changelog_model.model).\ all() return list( @@ -51,13 +51,13 @@ 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, + query_factory = hawat.forms.get_available_users, get_label = lambda x: '{} ({})'.format(x.fullname, x.login) ) operations = wtforms.SelectMultipleField( @@ -82,14 +82,14 @@ class ItemChangeLogSearchForm(vial.forms.BaseSearchForm): wtforms.validators.Optional(), ] ) - dt_from = vial.forms.SmartDateTimeField( + dt_from = hawat.forms.SmartDateTimeField( lazy_gettext('From:'), 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 = [ wtforms.validators.Optional() @@ -120,14 +120,14 @@ 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 = [ 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 = [ 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 100% rename from lib/vial/blueprints/changelogs/templates/changelogs/search.html rename to lib/hawat/blueprints/changelogs/templates/changelogs/search.html diff --git a/lib/vial/blueprints/changelogs/templates/changelogs/show.html b/lib/hawat/blueprints/changelogs/templates/changelogs/show.html similarity index 100% rename from lib/vial/blueprints/changelogs/templates/changelogs/show.html rename to lib/hawat/blueprints/changelogs/templates/changelogs/show.html diff --git a/lib/vial/blueprints/changelogs/test/__init__.py b/lib/hawat/blueprints/changelogs/test/__init__.py similarity index 73% rename from lib/vial/blueprints/changelogs/test/__init__.py rename to lib/hawat/blueprints/changelogs/test/__init__.py index 50af6dc9f..ce9e1917f 100644 --- a/lib/vial/blueprints/changelogs/test/__init__.py +++ b/lib/hawat/blueprints/changelogs/test/__init__.py @@ -6,7 +6,7 @@ """ -Unit tests for :py:mod:`vial.blueprints.changelogs`. +Unit tests for :py:mod:`hawat.blueprints.changelogs`. """ @@ -14,16 +14,16 @@ import sys import unittest import hawat.const -import vial.test -import vial.db -from vial.test import VialTestCase -from vial.test.runner import TestRunnerMixin +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 ChangeLogsSearchTestCase(TestRunnerMixin, VialTestCase): +class ChangeLogsSearchTestCase(TestRunnerMixin, HawatTestCase): """Class for testing ``changelogs.search`` endpoint.""" def _attempt_fail(self): @@ -38,22 +38,22 @@ class ChangeLogsSearchTestCase(TestRunnerMixin, VialTestCase): 200 ) - @vial.test.do_as_user_decorator(hawat.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(hawat.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(hawat.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(hawat.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() diff --git a/lib/hawat/blueprints/dbstatus/__init__.py b/lib/hawat/blueprints/dbstatus/__init__.py index 2a71d6bc7..99f5ebe44 100644 --- a/lib/hawat/blueprints/dbstatus/__init__.py +++ b/lib/hawat/blueprints/dbstatus/__init__.py @@ -66,12 +66,12 @@ 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.menu +import hawat.acl +from hawat.app 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 @@ -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,7 +116,7 @@ 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 @@ -145,7 +145,7 @@ class ViewView(HTMLMixin, PsycopgMixin, SimpleView): database_statistics_events = dbstatistics_events ) - action_menu = vial.menu.Menu() + action_menu = hawat.menu.Menu() action_menu.add_entry( 'endpoint', 'stop', @@ -191,7 +191,7 @@ 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 @@ -206,7 +206,7 @@ class MyQueriesView(HTMLMixin, PsycopgMixin, SimpleView): ) ) - action_menu = vial.menu.Menu() + action_menu = hawat.menu.Menu() action_menu.add_entry( 'endpoint', 'stop', @@ -264,7 +264,7 @@ 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.') @@ -304,7 +304,7 @@ class AbstractQueryStopView(PsycopgMixin, RenderableView): # pylint: disable=lo @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): @@ -445,7 +445,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): @@ -467,7 +467,7 @@ class DashboardView(HTMLMixin, SQLAlchemyMixin, SimpleView): # pylint: disable= def do_before_response(self, **kwargs): - """*Implementation* of :py:func:`vial.view.RenderableView.do_before_response`.""" + """*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()).\ @@ -538,7 +538,7 @@ class DashboardView(HTMLMixin, SQLAlchemyMixin, SimpleView): # pylint: disable= order_by(SettingsReportingModel.createtime.desc()).\ all() - action_menu = vial.menu.Menu() + action_menu = hawat.menu.Menu() action_menu.add_entry( 'endpoint', 'show', @@ -580,7 +580,7 @@ class DashboardView(HTMLMixin, SQLAlchemyMixin, SimpleView): # pylint: disable= ) 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', @@ -619,7 +619,7 @@ class DashboardView(HTMLMixin, SQLAlchemyMixin, SimpleView): # pylint: disable= ) 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', @@ -662,7 +662,7 @@ class DashboardView(HTMLMixin, SQLAlchemyMixin, SimpleView): # pylint: disable= #------------------------------------------------------------------------------- -class DatabaseStatusBlueprint(VialBlueprint): +class DatabaseStatusBlueprint(HawatBlueprint): """Pluggable module - database status overview (*dbstatus*).""" @classmethod @@ -695,8 +695,8 @@ class DatabaseStatusBlueprint(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`. """ diff --git a/lib/hawat/blueprints/dbstatus/test/__init__.py b/lib/hawat/blueprints/dbstatus/test/__init__.py index 057250ae6..8d1a80f11 100644 --- a/lib/hawat/blueprints/dbstatus/test/__init__.py +++ b/lib/hawat/blueprints/dbstatus/test/__init__.py @@ -16,13 +16,13 @@ Unit tests for :py:mod:`hawat.blueprints.dbstatus`. import unittest import hawat.const -import vial.test -import vial.db -from vial.test import VialTestCase +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(hawat.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(hawat.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(hawat.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(hawat.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(hawat.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(hawat.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(hawat.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(hawat.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(hawat.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(hawat.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(hawat.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(hawat.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 88% rename from lib/vial/blueprints/design_bs3/__init__.py rename to lib/hawat/blueprints/design_bs3/__init__.py index d76ceebec..b0a050683 100644 --- a/lib/vial/blueprints/design_bs3/__init__.py +++ b/lib/hawat/blueprints/design_bs3/__init__.py @@ -34,7 +34,7 @@ Module content from flask_babel import lazy_gettext -from vial.app import VialBlueprint +from hawat.app import HawatBlueprint # @@ -46,7 +46,7 @@ BLUEPRINT_NAME = 'design' #------------------------------------------------------------------------------- -class DesignBlueprint(VialBlueprint): +class DesignBlueprint(HawatBlueprint): """Pluggable module - application design and style (*design*).""" @classmethod @@ -58,8 +58,8 @@ class DesignBlueprint(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`. """ diff --git a/lib/vial/blueprints/design_bs3/templates/_layout.html b/lib/hawat/blueprints/design_bs3/templates/_layout.html similarity index 100% rename from lib/vial/blueprints/design_bs3/templates/_layout.html rename to lib/hawat/blueprints/design_bs3/templates/_layout.html diff --git a/lib/vial/blueprints/design_bs3/templates/_layout_confirmation.html b/lib/hawat/blueprints/design_bs3/templates/_layout_confirmation.html similarity index 100% rename from lib/vial/blueprints/design_bs3/templates/_layout_confirmation.html rename to lib/hawat/blueprints/design_bs3/templates/_layout_confirmation.html diff --git a/lib/vial/blueprints/design_bs3/templates/_layout_creatupdate.html b/lib/hawat/blueprints/design_bs3/templates/_layout_creatupdate.html similarity index 100% rename from lib/vial/blueprints/design_bs3/templates/_layout_creatupdate.html rename to lib/hawat/blueprints/design_bs3/templates/_layout_creatupdate.html 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 100% rename from lib/vial/blueprints/design_bs3/templates/_layout_events_search.html rename to lib/hawat/blueprints/design_bs3/templates/_layout_events_search.html diff --git a/lib/vial/blueprints/design_bs3/templates/_layout_list.html b/lib/hawat/blueprints/design_bs3/templates/_layout_list.html similarity index 100% rename from lib/vial/blueprints/design_bs3/templates/_layout_list.html rename to lib/hawat/blueprints/design_bs3/templates/_layout_list.html diff --git a/lib/vial/blueprints/design_bs3/templates/_layout_login.html b/lib/hawat/blueprints/design_bs3/templates/_layout_login.html similarity index 100% rename from lib/vial/blueprints/design_bs3/templates/_layout_login.html rename to lib/hawat/blueprints/design_bs3/templates/_layout_login.html diff --git a/lib/vial/blueprints/design_bs3/templates/_layout_registration.html b/lib/hawat/blueprints/design_bs3/templates/_layout_registration.html similarity index 100% rename from lib/vial/blueprints/design_bs3/templates/_layout_registration.html rename to lib/hawat/blueprints/design_bs3/templates/_layout_registration.html diff --git a/lib/vial/blueprints/design_bs3/templates/_layout_search.html b/lib/hawat/blueprints/design_bs3/templates/_layout_search.html similarity index 100% rename from lib/vial/blueprints/design_bs3/templates/_layout_search.html rename to lib/hawat/blueprints/design_bs3/templates/_layout_search.html 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 100% rename from lib/vial/blueprints/design_bs3/templates/_macros_form.html rename to lib/hawat/blueprints/design_bs3/templates/_macros_form.html diff --git a/lib/vial/blueprints/design_bs3/templates/_macros_page.html b/lib/hawat/blueprints/design_bs3/templates/_macros_page.html similarity index 100% rename from lib/vial/blueprints/design_bs3/templates/_macros_page.html rename to lib/hawat/blueprints/design_bs3/templates/_macros_page.html diff --git a/lib/vial/blueprints/design_bs3/templates/_macros_site.html b/lib/hawat/blueprints/design_bs3/templates/_macros_site.html similarity index 100% rename from lib/vial/blueprints/design_bs3/templates/_macros_site.html rename to lib/hawat/blueprints/design_bs3/templates/_macros_site.html 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 84% rename from lib/vial/blueprints/devtools/__init__.py rename to lib/hawat/blueprints/devtools/__init__.py index 1e7616a10..0c162d02b 100644 --- a/lib/vial/blueprints/devtools/__init__.py +++ b/lib/hawat/blueprints/devtools/__init__.py @@ -13,10 +13,10 @@ This pluggable module provides various utility and development tools. 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.app import HawatBlueprint +from hawat.view import SimpleView +from hawat.view.mixin import HTMLMixin BLUEPRINT_NAME = 'devtools' @@ -30,7 +30,7 @@ class ConfigView(HTMLMixin, SimpleView): authentication = True - authorization = [vial.acl.PERMISSION_DEVELOPER] + authorization = [hawat.acl.PERMISSION_DEVELOPER] @classmethod def get_view_name(cls): @@ -52,7 +52,7 @@ class ConfigView(HTMLMixin, SimpleView): #------------------------------------------------------------------------------- -class DevtoolsBlueprint(VialBlueprint): +class DevtoolsBlueprint(HawatBlueprint): """Pluggable module - development tools (*devtools*).""" @classmethod @@ -75,8 +75,8 @@ class DevtoolsBlueprint(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`. """ diff --git a/lib/vial/blueprints/devtools/templates/devtools/config.html b/lib/hawat/blueprints/devtools/templates/devtools/config.html similarity index 100% rename from lib/vial/blueprints/devtools/templates/devtools/config.html rename to lib/hawat/blueprints/devtools/templates/devtools/config.html diff --git a/lib/vial/blueprints/devtools/test/__init__.py b/lib/hawat/blueprints/devtools/test/__init__.py similarity index 78% rename from lib/vial/blueprints/devtools/test/__init__.py rename to lib/hawat/blueprints/devtools/test/__init__.py index d4ac628a4..c35d8f328 100644 --- a/lib/vial/blueprints/devtools/test/__init__.py +++ b/lib/hawat/blueprints/devtools/test/__init__.py @@ -6,7 +6,7 @@ """ -Unit tests for :py:mod:`vial.blueprints.devtools`. +Unit tests for :py:mod:`hawat.blueprints.devtools`. """ @@ -14,16 +14,16 @@ import sys import unittest import hawat.const -import vial.test -import vial.db -from vial.test import VialTestCase -from vial.test.runner import TestRunnerMixin +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(hawat.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(hawat.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(hawat.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(hawat.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 058eb4189..d58280ace 100644 --- a/lib/hawat/blueprints/dnsr/__init__.py +++ b/lib/hawat/blueprints/dnsr/__init__.py @@ -51,13 +51,13 @@ from flask_babel import lazy_gettext import mentat.services.dnsr from mentat.const import tr_ -import vial.db +import hawat.db import hawat.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.acl +from hawat.app 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 @@ -163,7 +163,7 @@ class SnippetSearchView(SnippetMixin, AbstractSearchView): # pylint: disable=lo #------------------------------------------------------------------------------- -class DnsrBlueprint(VialBlueprint): +class DnsrBlueprint(HawatBlueprint): """Pluggable module - DNS service (*dnsr*).""" @classmethod @@ -207,8 +207,8 @@ 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`. """ diff --git a/lib/hawat/blueprints/dnsr/test/__init__.py b/lib/hawat/blueprints/dnsr/test/__init__.py index d04f83435..c84fb4efc 100644 --- a/lib/hawat/blueprints/dnsr/test/__init__.py +++ b/lib/hawat/blueprints/dnsr/test/__init__.py @@ -16,13 +16,13 @@ Unit tests for :py:mod:`hawat.blueprints.dnsr`. import unittest import hawat.const -import vial.test -import vial.db -from vial.test import VialTestCase +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. """ @@ -51,22 +51,22 @@ class SearchTestCase(TestRunnerMixin, VialTestCase): """Test access as anonymous user.""" self._attempt_fail_redirect() - @vial.test.do_as_user_decorator(hawat.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(hawat.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(hawat.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(hawat.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/events/__init__.py b/lib/hawat/blueprints/events/__init__.py index 2e328c613..9eb61c968 100644 --- a/lib/hawat/blueprints/events/__init__.py +++ b/lib/hawat/blueprints/events/__init__.py @@ -32,11 +32,11 @@ from mentat.const import tr_ import hawat.const import hawat.events import hawat.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.acl +from hawat.app 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 @@ -65,8 +65,8 @@ def _get_search_form(request_args = None): # 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()) + form.dt_from.process_data(hawat.forms.default_dt_with_delta()) + form.dt_to.process_data(hawat.forms.default_dt()) return form @@ -121,7 +121,7 @@ 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', @@ -136,7 +136,7 @@ class SearchView(HTMLMixin, AbstractSearchView): # pylint: disable=locally-disa @classmethod def get_context_action_menu(cls): - action_menu = vial.menu.Menu() + action_menu = hawat.menu.Menu() action_menu.add_entry( 'endpoint', 'show', @@ -198,7 +198,7 @@ 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', @@ -416,7 +416,7 @@ class APIMetadataView(AJAXMixin, SimpleView): #------------------------------------------------------------------------------- -class EventsBlueprint(VialBlueprint): +class EventsBlueprint(HawatBlueprint): """Pluggable module - `IDEA <https://idea.cesnet.cz/en/index>`__ event database (*events*).""" @classmethod @@ -560,8 +560,8 @@ 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`. """ diff --git a/lib/hawat/blueprints/events/forms.py b/lib/hawat/blueprints/events/forms.py index f26986c38..c30221556 100644 --- a/lib/hawat/blueprints/events/forms.py +++ b/lib/hawat/blueprints/events/forms.py @@ -26,8 +26,8 @@ from flask_babel import lazy_gettext from mentat.datatype.sqldb import GroupModel import hawat.const -import vial.forms -import vial.db +import hawat.forms +import hawat.db import hawat.const @@ -35,91 +35,91 @@ 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 = [ 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) + 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 = [ 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 + default = hawat.forms.default_dt ) - st_from = vial.forms.SmartDateTimeField( + st_from = hawat.forms.SmartDateTimeField( lazy_gettext('Storage time from:'), 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.') ) - st_to = vial.forms.SmartDateTimeField( + st_to = hawat.forms.SmartDateTimeField( lazy_gettext('Storage time to:'), 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.') ) - source_addrs = vial.forms.CommaListField( + source_addrs = hawat.forms.CommaListField( lazy_gettext('Source addresses:'), 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.') ) - target_addrs = vial.forms.CommaListField( + target_addrs = hawat.forms.CommaListField( lazy_gettext('Target addresses:'), 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.') ) - host_addrs = vial.forms.CommaListField( + host_addrs = hawat.forms.CommaListField( lazy_gettext('Host addresses:'), 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.') ) - source_ports = vial.forms.CommaListField( + source_ports = hawat.forms.CommaListField( lazy_gettext('Source ports:'), 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.') ) - target_ports = vial.forms.CommaListField( + target_ports = hawat.forms.CommaListField( lazy_gettext('Target ports:'), 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.') ) - host_ports = vial.forms.CommaListField( + host_ports = hawat.forms.CommaListField( lazy_gettext('Host ports:'), 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.') ) @@ -346,21 +346,21 @@ 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 = [ 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) + 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 = [ 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 + default = hawat.forms.default_dt ) submit = wtforms.SubmitField( lazy_gettext('Search') diff --git a/lib/hawat/blueprints/events/test/__init__.py b/lib/hawat/blueprints/events/test/__init__.py index 6dca89f13..e34c1e5ed 100644 --- a/lib/hawat/blueprints/events/test/__init__.py +++ b/lib/hawat/blueprints/events/test/__init__.py @@ -16,13 +16,13 @@ Unit tests for :py:mod:`hawat.blueprints.events`. import unittest import hawat.const -import vial.test -import vial.db -from vial.test import VialTestCase +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(hawat.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(hawat.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(hawat.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(hawat.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(hawat.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(hawat.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(hawat.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(hawat.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 c93362daa..d17fbcdea 100644 --- a/lib/hawat/blueprints/filters/__init__.py +++ b/lib/hawat/blueprints/filters/__init__.py @@ -46,10 +46,10 @@ from mentat.datatype.sqldb import FilterModel, GroupModel, ItemChangeLogModel from mentat.idea.internal import Idea, IDEAFilterCompiler import hawat.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.db +from hawat.app 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 @@ -134,7 +134,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): @@ -148,7 +148,7 @@ 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', @@ -165,7 +165,7 @@ class ListView(HTMLMixin, SQLAlchemyMixin, ItemListView): @classmethod def get_context_action_menu(cls): - action_menu = vial.menu.Menu() + action_menu = hawat.menu.Menu() action_menu.add_entry( 'endpoint', 'show', @@ -281,14 +281,14 @@ 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', @@ -369,7 +369,7 @@ 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): @@ -467,9 +467,9 @@ 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): @@ -550,9 +550,9 @@ 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): @@ -635,9 +635,9 @@ 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): @@ -692,9 +692,9 @@ 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): @@ -747,9 +747,9 @@ 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): @@ -806,7 +806,7 @@ 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', @@ -879,7 +879,7 @@ class PlaygroundView(HTMLMixin, RenderableView): #------------------------------------------------------------------------------- -class FiltersBlueprint(VialBlueprint): +class FiltersBlueprint(HawatBlueprint): """Pluggable module - reporting filter management (*filters*).""" @classmethod @@ -907,8 +907,8 @@ class FiltersBlueprint(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`. """ diff --git a/lib/hawat/blueprints/filters/forms.py b/lib/hawat/blueprints/filters/forms.py index 8fdcbea92..358a13588 100644 --- a/lib/hawat/blueprints/filters/forms.py +++ b/lib/hawat/blueprints/filters/forms.py @@ -31,8 +31,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,7 +42,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 check_filter(form, field): # pylint: disable=locally-disabled,unused-argument @@ -79,7 +79,7 @@ def check_event(form, field): # pylint: disable=locally-disabled,unused-argumen #------------------------------------------------------------------------------- -class BaseFilterForm(vial.forms.BaseItemForm): +class BaseFilterForm(hawat.forms.BaseItemForm): """ Class representing base reporting filter form. """ @@ -129,21 +129,21 @@ class BaseFilterForm(vial.forms.BaseItemForm): choices = [('', lazy_gettext('<< no preference >>'))], filters = [lambda x: x or []] ) - ips = vial.forms.CommaListField( + ips = hawat.forms.CommaListField( lazy_gettext('Source IPs:'), validators = [ wtforms.validators.Optional(), - vial.forms.check_network_record_list + hawat.forms.check_network_record_list ], widget = wtforms.widgets.TextArea() ) - valid_from = vial.forms.SmartDateTimeField( + valid_from = hawat.forms.SmartDateTimeField( lazy_gettext('Valid from:'), validators = [ wtforms.validators.Optional() ] ) - valid_to = vial.forms.SmartDateTimeField( + valid_to = hawat.forms.SmartDateTimeField( lazy_gettext('Valid to:'), validators = [ wtforms.validators.Optional() @@ -158,8 +158,8 @@ class BaseFilterForm(vial.forms.BaseItemForm): (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') @@ -210,7 +210,7 @@ class PlaygroundFilterForm(flask_wtf.FlaskForm): lazy_gettext('Check') ) -class FilterSearchForm(vial.forms.BaseSearchForm): +class FilterSearchForm(hawat.forms.BaseSearchForm): """ Class representing simple user search form. """ @@ -222,14 +222,14 @@ class FilterSearchForm(vial.forms.BaseSearchForm): ], 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 = [ 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( + dt_to = hawat.forms.SmartDateTimeField( lazy_gettext('Creation time to:'), validators = [ wtforms.validators.Optional() diff --git a/lib/hawat/blueprints/filters/test/__init__.py b/lib/hawat/blueprints/filters/test/__init__.py index 0c5de8b1c..5ac1975d1 100644 --- a/lib/hawat/blueprints/filters/test/__init__.py +++ b/lib/hawat/blueprints/filters/test/__init__.py @@ -18,10 +18,10 @@ import unittest from mentat.datatype.sqldb import FilterModel import hawat.const -import vial.test -import vial.test.fixtures -import vial.db -from vial.test import VialTestCase, ItemCreateVialTestCase +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(hawat.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(hawat.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(hawat.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(hawat.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(hawat.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(hawat.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(hawat.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(hawat.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(hawat.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(hawat.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(hawat.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(hawat.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(hawat.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(hawat.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(hawat.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(hawat.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(hawat.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(hawat.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(hawat.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(hawat.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(hawat.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(hawat.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(hawat.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(hawat.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(hawat.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(hawat.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(hawat.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(hawat.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 5e9b8d148..43f52f376 100644 --- a/lib/hawat/blueprints/geoip/__init__.py +++ b/lib/hawat/blueprints/geoip/__init__.py @@ -54,13 +54,13 @@ from flask_babel import lazy_gettext import mentat.services.geoip from mentat.const import tr_ -import vial.db +import hawat.db import hawat.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.acl +from hawat.app 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 @@ -169,7 +169,7 @@ class SnippetSearchView(SnippetMixin, AbstractSearchView): # pylint: disable=lo #------------------------------------------------------------------------------- -class GeoipBlueprint(VialBlueprint): +class GeoipBlueprint(HawatBlueprint): """Pluggable module - IP geolocation service (*geoip*).""" @classmethod @@ -212,8 +212,8 @@ 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`. """ diff --git a/lib/hawat/blueprints/geoip/forms.py b/lib/hawat/blueprints/geoip/forms.py index 1ab72bbf8..ef5984718 100644 --- a/lib/hawat/blueprints/geoip/forms.py +++ b/lib/hawat/blueprints/geoip/forms.py @@ -21,7 +21,7 @@ import wtforms import flask_wtf from flask_babel import lazy_gettext -import vial.forms +import hawat.forms class GeoipSearchForm(flask_wtf.FlaskForm): @@ -32,7 +32,7 @@ class GeoipSearchForm(flask_wtf.FlaskForm): lazy_gettext('Search GeoIP:'), validators = [ wtforms.validators.DataRequired(), - vial.forms.check_ip_record + hawat.forms.check_ip_record ] ) submit = wtforms.SubmitField( diff --git a/lib/hawat/blueprints/geoip/test/__init__.py b/lib/hawat/blueprints/geoip/test/__init__.py index 4b2c8970c..75d915be9 100644 --- a/lib/hawat/blueprints/geoip/test/__init__.py +++ b/lib/hawat/blueprints/geoip/test/__init__.py @@ -16,13 +16,13 @@ Unit tests for :py:mod:`hawat.blueprints.geoip`. import unittest import hawat.const -import vial.test -import vial.db -from vial.test import VialTestCase +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. """ @@ -51,22 +51,22 @@ class SearchTestCase(TestRunnerMixin, VialTestCase): """Test access as anonymous user.""" self._attempt_fail_redirect() - @vial.test.do_as_user_decorator(hawat.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(hawat.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(hawat.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(hawat.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/groups/__init__.py b/lib/hawat/blueprints/groups/__init__.py index 008346575..7532bb5f4 100644 --- a/lib/hawat/blueprints/groups/__init__.py +++ b/lib/hawat/blueprints/groups/__init__.py @@ -29,30 +29,232 @@ __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 +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.app 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 ShowView(vial.blueprints.groups.ShowView): +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 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(hawat.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(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', @@ -107,7 +309,7 @@ class ShowView(vial.blueprints.groups.ShowView): ) 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', @@ -116,7 +318,7 @@ class ShowView(vial.blueprints.groups.ShowView): ) 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', @@ -188,11 +390,48 @@ 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 = 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() @@ -202,11 +441,66 @@ class CreateView(vial.blueprints.groups.CreateView): # pylint: disable=locally- 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 = 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(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 = 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') @@ -217,12 +511,594 @@ class UpdateView(vial.blueprints.groups.UpdateView): # pylint: disable=locally- 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(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 = 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(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 = 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(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 = 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(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 = 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(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 = 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 = [hawat.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(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 = 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 = [hawat.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(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 = 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 = [hawat.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(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 = 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(vial.blueprints.groups.GroupsBlueprint): +class GroupsBlueprint(HawatBlueprint): """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(): @@ -246,7 +1122,7 @@ class GroupsBlueprint(vial.blueprints.groups.GroupsBlueprint): 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') + 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. @@ -263,8 +1139,8 @@ 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`. """ diff --git a/lib/hawat/blueprints/groups/forms.py b/lib/hawat/blueprints/groups/forms.py index 865a31da2..cc7d9e3d0 100644 --- a/lib/hawat/blueprints/groups/forms.py +++ b/lib/hawat/blueprints/groups/forms.py @@ -29,8 +29,9 @@ from flask_babel import gettext, lazy_gettext # # Custom modules. # -import vial.db -import vial.forms +import hawat.db +import hawat.forms +from hawat.forms import get_available_users, get_available_group_sources from mentat.datatype.sqldb import GroupModel, UserModel @@ -39,7 +40,7 @@ def check_name_existence(form, field): # pylint: disable=locally-disabled,unuse Callback for validating user logins during account create action. """ try: - vial.db.db_get().session.query(GroupModel).\ + hawat.db.db_get().session.query(GroupModel).\ filter(GroupModel.name == field.data).\ one() except sqlalchemy.orm.exc.NoResultFound: @@ -53,7 +54,7 @@ def check_name_uniqueness(form, field): """ Callback for validating user logins during account update action. """ - item = vial.db.db_get().session.query(GroupModel).\ + item = hawat.db.db_get().session.query(GroupModel).\ filter(GroupModel.name == field.data).\ filter(GroupModel.id != form.db_item_id).\ all() @@ -74,7 +75,7 @@ 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() + return hawat.db.db_query(UserModel).order_by(UserModel.fullname).all() def format_select_option_label_user(item): @@ -88,10 +89,9 @@ 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. """ @@ -112,7 +112,7 @@ class BaseGroupForm(vial.forms.BaseItemForm): ) members = QuerySelectMultipleField( lazy_gettext('Members:'), - query_factory = get_available_users, + 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.') @@ -144,13 +144,13 @@ class AdminBaseGroupForm(BaseGroupForm): (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, 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, + 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.') @@ -161,7 +161,7 @@ class AdminBaseGroupForm(BaseGroupForm): wtforms.validators.Optional(), check_parent_not_self ], - query_factory = get_available_groups, + 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.') @@ -177,7 +177,7 @@ class AdminCreateGroupForm(AdminBaseGroupForm): validators = [ wtforms.validators.DataRequired(), wtforms.validators.Length(min = 3, max = 100), - check_name_existence + hawat.forms.check_unique_group ], description = lazy_gettext('System-wide unique name for the group.') ) @@ -197,7 +197,7 @@ class AdminUpdateGroupForm(AdminBaseGroupForm): validators = [ wtforms.validators.DataRequired(), wtforms.validators.Length(min = 3, max = 100), - check_name_uniqueness + hawat.forms.check_unique_group ], description = lazy_gettext('System-wide unique name for the group.') ) @@ -208,3 +208,106 @@ 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/test/__init__.py b/lib/hawat/blueprints/groups/test/__init__.py index 124949f05..72de72f97 100644 --- a/lib/hawat/blueprints/groups/test/__init__.py +++ b/lib/hawat/blueprints/groups/test/__init__.py @@ -16,14 +16,14 @@ Unit tests for :py:mod:`hawat.blueprints.groups`. import unittest import hawat.const -import vial.test -import vial.test.fixtures -import vial.db -from vial.test import VialTestCase, ItemCreateVialTestCase +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,28 +42,28 @@ class GroupsListTestCase(TestRunnerMixin, VialTestCase): ] ) - @vial.test.do_as_user_decorator(hawat.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(hawat.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(hawat.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(hawat.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): @@ -96,48 +96,48 @@ class GroupsShowTestCase(TestRunnerMixin, VialTestCase): ] ) - @vial.test.do_as_user_decorator(hawat.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(hawat.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(hawat.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(hawat.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,28 +162,28 @@ class GroupsCreateTestCase(TestRunnerMixin, ItemCreateVialTestCase): ] ) - @vial.test.do_as_user_decorator(hawat.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(hawat.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(hawat.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(hawat.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): @@ -203,32 +203,32 @@ class GroupsUpdateTestCase(TestRunnerMixin, VialTestCase): ] ) - @vial.test.do_as_user_decorator(hawat.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(hawat.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(hawat.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(hawat.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): @@ -279,32 +279,32 @@ class GroupsEnableDisableTestCase(TestRunnerMixin, VialTestCase): ] ) - @vial.test.do_as_user_decorator(hawat.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(hawat.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(hawat.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(hawat.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): @@ -429,7 +429,7 @@ class GroupsAddRemRejMemberTestCase(TestRunnerMixin, VialTestCase): ) self.mailbox_monitoring('off') - @vial.test.do_as_user_decorator(hawat.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 ( @@ -438,9 +438,9 @@ class GroupsAddRemRejMemberTestCase(TestRunnerMixin, VialTestCase): hawat.const.ROLE_MAINTAINER, hawat.const.ROLE_ADMIN ): - self._attempt_fail(uname, vial.test.fixtures.DEMO_GROUP_A) + self._attempt_fail(uname, hawat.test.fixtures.DEMO_GROUP_A) - @vial.test.do_as_user_decorator(hawat.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 ( @@ -448,9 +448,9 @@ class GroupsAddRemRejMemberTestCase(TestRunnerMixin, VialTestCase): hawat.const.ROLE_MAINTAINER, hawat.const.ROLE_ADMIN ): - self._attempt_succeed(uname, vial.test.fixtures.DEMO_GROUP_A) + self._attempt_succeed(uname, hawat.test.fixtures.DEMO_GROUP_A) - @vial.test.do_as_user_decorator(hawat.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 ( @@ -458,9 +458,9 @@ class GroupsAddRemRejMemberTestCase(TestRunnerMixin, VialTestCase): hawat.const.ROLE_DEVELOPER, hawat.const.ROLE_ADMIN ): - self._attempt_succeed(uname, vial.test.fixtures.DEMO_GROUP_A) + self._attempt_succeed(uname, hawat.test.fixtures.DEMO_GROUP_A) - @vial.test.do_as_user_decorator(hawat.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 ( @@ -468,10 +468,10 @@ class GroupsAddRemRejMemberTestCase(TestRunnerMixin, VialTestCase): hawat.const.ROLE_DEVELOPER, hawat.const.ROLE_MAINTAINER ): - self._attempt_succeed(uname, vial.test.fixtures.DEMO_GROUP_A) + 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): @@ -501,29 +501,29 @@ class GroupsDeleteTestCase(TestRunnerMixin, VialTestCase): ] ) - @vial.test.do_as_user_decorator(hawat.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(hawat.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(hawat.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(hawat.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) #------------------------------------------------------------------------------- diff --git a/lib/hawat/blueprints/home/__init__.py b/lib/hawat/blueprints/home/__init__.py index 114837490..1a6478c8c 100644 --- a/lib/hawat/blueprints/home/__init__.py +++ b/lib/hawat/blueprints/home/__init__.py @@ -29,9 +29,9 @@ __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea Kropáčová <andrea from flask_babel import lazy_gettext -from vial.app import VialBlueprint -from vial.view import SimpleView -from vial.view.mixin import HTMLMixin +from hawat.app import HawatBlueprint +from hawat.view import SimpleView +from hawat.view.mixin import HTMLMixin BLUEPRINT_NAME = 'home' @@ -64,7 +64,7 @@ class IndexView(HTMLMixin, SimpleView): #------------------------------------------------------------------------------- -class HomeBlueprint(VialBlueprint): +class HomeBlueprint(HawatBlueprint): """Pluggable module - home page (*home*).""" @classmethod @@ -77,8 +77,8 @@ class HomeBlueprint(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`. """ diff --git a/lib/hawat/blueprints/home/test/__init__.py b/lib/hawat/blueprints/home/test/__init__.py index 7bbe162bb..39f1d42f4 100644 --- a/lib/hawat/blueprints/home/test/__init__.py +++ b/lib/hawat/blueprints/home/test/__init__.py @@ -16,13 +16,13 @@ Unit tests for :py:mod:`hawat.blueprints.home`. import unittest import hawat.const -import vial.test -import vial.db -from vial.test import VialTestCase +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,22 +40,22 @@ class HomeTestCase(TestRunnerMixin, VialTestCase): """Test access as anonymous user.""" self._attempt_succeed() - @vial.test.do_as_user_decorator(hawat.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(hawat.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(hawat.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(hawat.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/hosts/__init__.py b/lib/hawat/blueprints/hosts/__init__.py index 0fa7da4f2..24d02b2a8 100644 --- a/lib/hawat/blueprints/hosts/__init__.py +++ b/lib/hawat/blueprints/hosts/__init__.py @@ -32,11 +32,11 @@ from mentat.const import tr_ import hawat.events import hawat.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.acl +from hawat.app 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 @@ -51,7 +51,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): @@ -128,7 +128,7 @@ 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', @@ -157,7 +157,7 @@ class APISearchView(AJAXMixin, AbstractSearchView): # pylint: disable=locally-d #------------------------------------------------------------------------------- -class HostsBlueprint(VialBlueprint): +class HostsBlueprint(HawatBlueprint): """Pluggable module - Host overview (*hosts*).""" @classmethod @@ -185,8 +185,8 @@ class HostsBlueprint(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`. """ diff --git a/lib/hawat/blueprints/hosts/forms.py b/lib/hawat/blueprints/hosts/forms.py index d0c908ea0..f8b486271 100644 --- a/lib/hawat/blueprints/hosts/forms.py +++ b/lib/hawat/blueprints/hosts/forms.py @@ -23,7 +23,7 @@ import flask_wtf from flask_babel import lazy_gettext import hawat.const -import vial.forms +import hawat.forms import hawat.const @@ -35,22 +35,22 @@ class SimpleHostSearchForm(flask_wtf.FlaskForm): lazy_gettext('Host address:'), 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 = [ 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 = [ 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/test/__init__.py b/lib/hawat/blueprints/hosts/test/__init__.py index d5319402f..64204754e 100644 --- a/lib/hawat/blueprints/hosts/test/__init__.py +++ b/lib/hawat/blueprints/hosts/test/__init__.py @@ -16,13 +16,13 @@ Unit tests for :py:mod:`hawat.blueprints.hosts`. import unittest import hawat.const -import vial.test -import vial.db -from vial.test import VialTestCase +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. """ @@ -57,22 +57,22 @@ class SearchTestCase(TestRunnerMixin, VialTestCase): """Test access as anonymous user.""" self._attempt_fail_redirect() - @vial.test.do_as_user_decorator(hawat.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(hawat.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(hawat.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(hawat.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/nerd/__init__.py b/lib/hawat/blueprints/nerd/__init__.py index f8bff1322..ad1e9f120 100644 --- a/lib/hawat/blueprints/nerd/__init__.py +++ b/lib/hawat/blueprints/nerd/__init__.py @@ -52,13 +52,13 @@ from flask_babel import lazy_gettext import mentat.services.nerd from mentat.const import tr_ -import vial.db +import hawat.db import hawat.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.acl +from hawat.app 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 @@ -155,14 +155,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 @@ -206,8 +206,8 @@ 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`. """ diff --git a/lib/hawat/blueprints/nerd/forms.py b/lib/hawat/blueprints/nerd/forms.py index 17dd98b06..c46222f32 100644 --- a/lib/hawat/blueprints/nerd/forms.py +++ b/lib/hawat/blueprints/nerd/forms.py @@ -21,7 +21,7 @@ import wtforms import flask_wtf from flask_babel import lazy_gettext -import vial.forms +import hawat.forms class NerdSearchForm(flask_wtf.FlaskForm): @@ -32,7 +32,7 @@ class NerdSearchForm(flask_wtf.FlaskForm): lazy_gettext('Search NERD:'), validators = [ wtforms.validators.DataRequired(), - vial.forms.check_ip4_record + hawat.forms.check_ip4_record ] ) submit = wtforms.SubmitField( diff --git a/lib/hawat/blueprints/nerd/test/__init__.py b/lib/hawat/blueprints/nerd/test/__init__.py index 7078e92e2..76a97f6b8 100644 --- a/lib/hawat/blueprints/nerd/test/__init__.py +++ b/lib/hawat/blueprints/nerd/test/__init__.py @@ -16,13 +16,13 @@ Unit tests for :py:mod:`hawat.blueprints.nerd`. import unittest import hawat.const -import vial.test -import vial.db -from vial.test import VialTestCase +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,22 +43,22 @@ class SearchTestCase(TestRunnerMixin, VialTestCase): """Test access as anonymous user.""" self._attempt_fail() - @vial.test.do_as_user_decorator(hawat.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(hawat.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(hawat.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(hawat.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/networks/__init__.py b/lib/hawat/blueprints/networks/__init__.py index 487de9e87..a6540e016 100644 --- a/lib/hawat/blueprints/networks/__init__.py +++ b/lib/hawat/blueprints/networks/__init__.py @@ -33,10 +33,10 @@ 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 +from hawat.app 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 @@ -52,7 +52,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,7 +64,7 @@ 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', @@ -75,7 +75,7 @@ class ListView(HTMLMixin, SQLAlchemyMixin, ItemListView): @classmethod def get_context_action_menu(cls): - action_menu = vial.menu.Menu() + action_menu = hawat.menu.Menu() action_menu.add_entry( 'endpoint', 'show', @@ -172,14 +172,14 @@ 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', @@ -238,7 +238,7 @@ 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): @@ -315,9 +315,9 @@ 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): @@ -377,9 +377,9 @@ 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): @@ -440,9 +440,9 @@ 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): @@ -472,7 +472,7 @@ class DeleteView(HTMLMixin, SQLAlchemyMixin, ItemDeleteView): # pylint: disable #------------------------------------------------------------------------------- -class NetworksBlueprint(VialBlueprint): +class NetworksBlueprint(HawatBlueprint): """Pluggable module - network management (*networks*).""" @classmethod @@ -493,8 +493,8 @@ class NetworksBlueprint(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`. """ diff --git a/lib/hawat/blueprints/networks/forms.py b/lib/hawat/blueprints/networks/forms.py index af07fb57e..2f2723556 100644 --- a/lib/hawat/blueprints/networks/forms.py +++ b/lib/hawat/blueprints/networks/forms.py @@ -29,9 +29,9 @@ from flask_babel import lazy_gettext # Custom modules. # import hawat.const -import vial.forms -import vial.db -from vial.forms import get_available_groups +import hawat.forms +import hawat.db +from hawat.forms import get_available_groups from mentat.datatype.sqldb import NetworkModel @@ -40,14 +40,14 @@ def get_available_sources(): """ Query the database for list of network record sources. """ - result = vial.db.db_query(NetworkModel)\ + 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. """ @@ -69,7 +69,7 @@ class BaseNetworkForm(vial.forms.BaseItemForm): lazy_gettext('Network:'), validators = [ wtforms.validators.DataRequired(), - vial.forms.check_network_record + hawat.forms.check_network_record ] ) rank = wtforms.IntegerField( @@ -101,7 +101,7 @@ class AdminNetworkForm(BaseNetworkForm): ) -class NetworkSearchForm(vial.forms.BaseSearchForm): +class NetworkSearchForm(hawat.forms.BaseSearchForm): """ Class representing simple user search form. """ @@ -113,14 +113,14 @@ class NetworkSearchForm(vial.forms.BaseSearchForm): ], 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 = [ 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( + dt_to = hawat.forms.SmartDateTimeField( lazy_gettext('Creation time to:'), validators = [ wtforms.validators.Optional() diff --git a/lib/hawat/blueprints/networks/test/__init__.py b/lib/hawat/blueprints/networks/test/__init__.py index a7e13c05e..045ea3617 100644 --- a/lib/hawat/blueprints/networks/test/__init__.py +++ b/lib/hawat/blueprints/networks/test/__init__.py @@ -18,10 +18,10 @@ import unittest from mentat.datatype.sqldb import NetworkModel import hawat.const -import vial.test -import vial.test.fixtures -import vial.db -from vial.test import VialTestCase, ItemCreateVialTestCase +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 NetworkTestMixin: 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): """ 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): """ @@ -66,7 +66,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 +85,28 @@ class NetworksListTestCase(TestRunnerMixin, VialTestCase): ] ) - @vial.test.do_as_user_decorator(hawat.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(hawat.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(hawat.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(hawat.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 +127,48 @@ class NetworksShowTestCase(NetworkTestMixin, TestRunnerMixin, VialTestCase): ] ) - @vial.test.do_as_user_decorator(hawat.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(hawat.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(hawat.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(hawat.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 +195,28 @@ class NetworksCreateTestCase(NetworkTestMixin, TestRunnerMixin, ItemCreateVialTe ] ) - @vial.test.do_as_user_decorator(hawat.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(hawat.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(hawat.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(hawat.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 = [ @@ -244,32 +244,32 @@ class NetworksCreateForTestCase(NetworkTestMixin, TestRunnerMixin, ItemCreateVia ] ) - @vial.test.do_as_user_decorator(hawat.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(hawat.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(hawat.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(hawat.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 +289,32 @@ class NetworksUpdateTestCase(NetworkTestMixin, TestRunnerMixin, VialTestCase): ] ) - @vial.test.do_as_user_decorator(hawat.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(hawat.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(hawat.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(hawat.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,29 +344,29 @@ class NetworksDeleteTestCase(NetworkTestMixin, TestRunnerMixin, VialTestCase): ] ) - @vial.test.do_as_user_decorator(hawat.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(hawat.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(hawat.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(hawat.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)) #------------------------------------------------------------------------------- diff --git a/lib/hawat/blueprints/pdnsr/__init__.py b/lib/hawat/blueprints/pdnsr/__init__.py index 043968e48..4bc6fb9ac 100644 --- a/lib/hawat/blueprints/pdnsr/__init__.py +++ b/lib/hawat/blueprints/pdnsr/__init__.py @@ -52,13 +52,13 @@ from flask_babel import lazy_gettext import mentat.services.pdnsr from mentat.const import tr_ -import vial.db +import hawat.db import hawat.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.acl +from hawat.app 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 @@ -166,7 +166,7 @@ class SnippetSearchView(SnippetMixin, AbstractSearchView): # pylint: disable=lo #------------------------------------------------------------------------------- -class PDNSRBlueprint(VialBlueprint): +class PDNSRBlueprint(HawatBlueprint): """Pluggable module - PassiveDNS service (*pdnsr*).""" @classmethod @@ -215,8 +215,8 @@ class PDNSRBlueprint(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`. """ diff --git a/lib/hawat/blueprints/pdnsr/forms.py b/lib/hawat/blueprints/pdnsr/forms.py index dd8bfe4f7..6719b362f 100644 --- a/lib/hawat/blueprints/pdnsr/forms.py +++ b/lib/hawat/blueprints/pdnsr/forms.py @@ -21,7 +21,7 @@ import wtforms import flask_wtf from flask_babel import lazy_gettext -import vial.forms +import hawat.forms class PDNSRSearchForm(flask_wtf.FlaskForm): @@ -32,7 +32,7 @@ class PDNSRSearchForm(flask_wtf.FlaskForm): lazy_gettext('Search PassiveDNS:'), validators = [ wtforms.validators.DataRequired(), - vial.forms.check_ip_record + hawat.forms.check_ip_record ] ) sortby = wtforms.SelectField( diff --git a/lib/hawat/blueprints/pdnsr/test/__init__.py b/lib/hawat/blueprints/pdnsr/test/__init__.py index f88252ddf..cdd5534f5 100644 --- a/lib/hawat/blueprints/pdnsr/test/__init__.py +++ b/lib/hawat/blueprints/pdnsr/test/__init__.py @@ -16,13 +16,13 @@ Unit tests for :py:mod:`hawat.blueprints.pdnsr`. import unittest import hawat.const -import vial.test -import vial.db -from vial.test import VialTestCase +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,22 +43,22 @@ class SearchTestCase(TestRunnerMixin, VialTestCase): """Test access as anonymous user.""" self._attempt_fail() - @vial.test.do_as_user_decorator(hawat.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(hawat.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(hawat.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(hawat.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/performance/__init__.py b/lib/hawat/blueprints/performance/__init__.py index da0af0808..cb87e8b4c 100644 --- a/lib/hawat/blueprints/performance/__init__.py +++ b/lib/hawat/blueprints/performance/__init__.py @@ -25,9 +25,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.app import HawatBlueprint +from hawat.view import SimpleView, FileNameView +from hawat.view.mixin import HTMLMixin BLUEPRINT_NAME = 'performance' @@ -130,7 +130,7 @@ class RRDDBView(FileNameView): #------------------------------------------------------------------------------- -class PerformanceBlueprint(VialBlueprint): +class PerformanceBlueprint(HawatBlueprint): """Pluggable module - system processing performance (*performance*).""" @classmethod @@ -152,8 +152,8 @@ class PerformanceBlueprint(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`. """ diff --git a/lib/hawat/blueprints/performance/test/__init__.py b/lib/hawat/blueprints/performance/test/__init__.py index 9e577c6cb..02eae1787 100644 --- a/lib/hawat/blueprints/performance/test/__init__.py +++ b/lib/hawat/blueprints/performance/test/__init__.py @@ -16,13 +16,13 @@ Unit tests for :py:mod:`hawat.blueprints.performance`. import unittest import hawat.const -import vial.test -import vial.db -from vial.test import VialTestCase +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. """ @@ -51,22 +51,22 @@ class SearchTestCase(TestRunnerMixin, VialTestCase): """Test access as anonymous user.""" self._attempt_fail_redirect() - @vial.test.do_as_user_decorator(hawat.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(hawat.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(hawat.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(hawat.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/reports/__init__.py b/lib/hawat/blueprints/reports/__init__.py index a866a1727..22ad4c529 100644 --- a/lib/hawat/blueprints/reports/__init__.py +++ b/lib/hawat/blueprints/reports/__init__.py @@ -39,12 +39,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.app 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 @@ -187,16 +187,16 @@ 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', @@ -216,7 +216,7 @@ class ShowView(HTMLMixin, SQLAlchemyMixin, ItemShowView): @classmethod def get_action_menu(cls): - action_menu = vial.menu.Menu() + action_menu = hawat.menu.Menu() action_menu.add_entry( 'endpoint', 'search', @@ -464,7 +464,7 @@ class DeleteView(HTMLMixin, SQLAlchemyMixin, ItemDeleteView): # pylint: disable authentication = True - authorization = [vial.acl.PERMISSION_ADMIN] + authorization = [hawat.acl.PERMISSION_ADMIN] @classmethod def get_menu_legend(cls, **kwargs): @@ -568,7 +568,7 @@ class FeedbackView(AJAXMixin, RenderableView): #------------------------------------------------------------------------------- -class ReportsBlueprint(VialBlueprint): +class ReportsBlueprint(HawatBlueprint): """Pluggable module - periodical event reports (*reports*).""" @classmethod @@ -620,8 +620,8 @@ class ReportsBlueprint(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`. """ diff --git a/lib/hawat/blueprints/reports/forms.py b/lib/hawat/blueprints/reports/forms.py index 0803a09c5..0bf10cae9 100644 --- a/lib/hawat/blueprints/reports/forms.py +++ b/lib/hawat/blueprints/reports/forms.py @@ -27,8 +27,8 @@ from flask_babel import lazy_gettext import mentat.const from mentat.datatype.sqldb import UserModel, GroupModel import hawat.const -import vial.forms -import vial.db +import hawat.forms +import hawat.db import hawat.const @@ -38,11 +38,11 @@ def get_available_groups(): """ # In case current user is administrator provide list of all groups. if flask_login.current_user.has_role(hawat.const.ROLE_ADMIN): - return vial.db.db_query(GroupModel).\ + 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).\ + return hawat.db.db_query(GroupModel).\ filter(GroupModel.members.any(UserModel.id == flask_login.current_user.id)).\ order_by(GroupModel.name).\ all() @@ -69,7 +69,7 @@ def get_type_choices(): ) ) -class EventReportSearchForm(vial.forms.BaseSearchForm): +class EventReportSearchForm(hawat.forms.BaseSearchForm): """ Class representing event report search form. """ @@ -101,19 +101,19 @@ class EventReportSearchForm(vial.forms.BaseSearchForm): choices = get_type_choices(), filters = [lambda x: x or []] ) - dt_from = vial.forms.SmartDateTimeField( + dt_from = hawat.forms.SmartDateTimeField( lazy_gettext('From:'), 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 = [ wtforms.validators.Optional() ], - default = vial.forms.default_dt + default = hawat.forms.default_dt ) @staticmethod @@ -140,19 +140,19 @@ class ReportingDashboardForm(flask_wtf.FlaskForm): allow_blank = False, get_pk = lambda item: item.name ) - dt_from = vial.forms.SmartDateTimeField( + dt_from = hawat.forms.SmartDateTimeField( lazy_gettext('From:'), 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 = [ wtforms.validators.Optional() ], - default = vial.forms.default_dt + default = hawat.forms.default_dt ) submit = wtforms.SubmitField( lazy_gettext('Search') @@ -179,7 +179,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/test/__init__.py b/lib/hawat/blueprints/reports/test/__init__.py index e0e79969d..e5022864b 100644 --- a/lib/hawat/blueprints/reports/test/__init__.py +++ b/lib/hawat/blueprints/reports/test/__init__.py @@ -16,13 +16,13 @@ Unit tests for :py:mod:`hawat.blueprints.reports`. import unittest import hawat.const -import vial.test -import vial.db -from vial.test import VialTestCase +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. """ @@ -51,22 +51,22 @@ class SearchTestCase(TestRunnerMixin, VialTestCase): """Test access as anonymous user.""" self._attempt_fail_redirect() - @vial.test.do_as_user_decorator(hawat.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(hawat.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(hawat.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(hawat.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/settings_reporting/__init__.py b/lib/hawat/blueprints/settings_reporting/__init__.py index 039ee2ffa..0afde339a 100644 --- a/lib/hawat/blueprints/settings_reporting/__init__.py +++ b/lib/hawat/blueprints/settings_reporting/__init__.py @@ -30,10 +30,10 @@ 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 +import hawat.acl +from hawat.app 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 @@ -65,14 +65,14 @@ 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', @@ -99,7 +99,7 @@ class ShowView(HTMLMixin, SQLAlchemyMixin, ItemShowView): @classmethod def get_action_menu(cls): - action_menu = vial.menu.Menu() + action_menu = hawat.menu.Menu() action_menu.add_entry( 'endpoint', 'showgroup', @@ -164,9 +164,9 @@ 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): @@ -225,9 +225,9 @@ 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): @@ -261,7 +261,7 @@ class UpdateView(HTMLMixin, SQLAlchemyMixin, ItemUpdateView): # pylint: disable #------------------------------------------------------------------------------- -class SettingsReportingBlueprint(VialBlueprint): +class SettingsReportingBlueprint(HawatBlueprint): """Pluggable module - reporting settings. (*settings_reporting*)""" @classmethod @@ -274,8 +274,8 @@ class SettingsReportingBlueprint(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`. """ diff --git a/lib/hawat/blueprints/settings_reporting/forms.py b/lib/hawat/blueprints/settings_reporting/forms.py index 577235986..5082b7512 100644 --- a/lib/hawat/blueprints/settings_reporting/forms.py +++ b/lib/hawat/blueprints/settings_reporting/forms.py @@ -26,8 +26,8 @@ from babel import Locale from flask_babel import lazy_gettext import hawat.const -import vial.forms -import vial.db +import hawat.forms +import hawat.db import mentat.const from mentat.datatype.sqldb import GroupModel @@ -40,7 +40,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(): @@ -65,7 +65,7 @@ def get_available_locales(): return locale_list -class BaseSettingsReportingForm(vial.forms.BaseItemForm): +class BaseSettingsReportingForm(hawat.forms.BaseItemForm): """ Class representing base reporting settings form. """ @@ -83,35 +83,35 @@ class BaseSettingsReportingForm(vial.forms.BaseItemForm): ], filters = [lambda x: x or None] ) - emails_low = vial.forms.CommaListField( + emails_low = hawat.forms.CommaListField( lazy_gettext('Target emails - low severity:'), 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 = [ 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 = [ 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 = [ 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 = [ wtforms.validators.Optional(), @@ -121,8 +121,8 @@ class BaseSettingsReportingForm(vial.forms.BaseItemForm): (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:'), diff --git a/lib/hawat/blueprints/skeleton/__init__.py b/lib/hawat/blueprints/skeleton/__init__.py index 6522f3786..5f1b2f25d 100644 --- a/lib/hawat/blueprints/skeleton/__init__.py +++ b/lib/hawat/blueprints/skeleton/__init__.py @@ -20,9 +20,9 @@ __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea Kropáčová <andrea 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.app import HawatBlueprint +from hawat.view import SimpleView +from hawat.view.mixin import HTMLMixin BLUEPRINT_NAME = 'skeleton' @@ -63,7 +63,7 @@ class ExampleView(HTMLMixin, SimpleView): #------------------------------------------------------------------------------- -class SkeletonBlueprint(VialBlueprint): +class SkeletonBlueprint(HawatBlueprint): """Pluggable module - skeleton (*skeleton*).""" @classmethod @@ -84,8 +84,8 @@ class SkeletonBlueprint(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`. """ diff --git a/lib/hawat/blueprints/status/__init__.py b/lib/hawat/blueprints/status/__init__.py index 1c5d34b4d..d7effc1dd 100644 --- a/lib/hawat/blueprints/status/__init__.py +++ b/lib/hawat/blueprints/status/__init__.py @@ -37,10 +37,10 @@ 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.app import HawatBlueprint +from hawat.view import SimpleView +from hawat.view.mixin import HTMLMixin BLUEPRINT_NAME = 'status' @@ -53,7 +53,7 @@ class ViewView(HTMLMixin, SimpleView): """ authentication = True - authorization = [vial.acl.PERMISSION_ADMIN] + authorization = [hawat.acl.PERMISSION_ADMIN] @classmethod def get_view_name(cls): @@ -111,7 +111,7 @@ class ViewView(HTMLMixin, SimpleView): #------------------------------------------------------------------------------- -class StatusBlueprint(VialBlueprint): +class StatusBlueprint(HawatBlueprint): """Pluggable module - Mentat system status (*status*).""" @classmethod @@ -133,8 +133,8 @@ class StatusBlueprint(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`. """ diff --git a/lib/hawat/blueprints/status/test/__init__.py b/lib/hawat/blueprints/status/test/__init__.py index 04906c988..84806c8fb 100644 --- a/lib/hawat/blueprints/status/test/__init__.py +++ b/lib/hawat/blueprints/status/test/__init__.py @@ -16,14 +16,14 @@ Unit tests for :py:mod:`hawat.blueprints.status`. import unittest import hawat.const -import vial.test -import vial.db +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): @@ -56,22 +56,22 @@ class StatusViewTestCase(TestRunnerMixin, VialTestCase): """Test access as anonymous user.""" self._attempt_fail_redirect() - @vial.test.do_as_user_decorator(hawat.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(hawat.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(hawat.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(hawat.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/timeline/__init__.py b/lib/hawat/blueprints/timeline/__init__.py index 18655020a..76805df50 100644 --- a/lib/hawat/blueprints/timeline/__init__.py +++ b/lib/hawat/blueprints/timeline/__init__.py @@ -33,11 +33,11 @@ from mentat.services.eventstorage import QTYPE_TIMELINE import hawat.events import hawat.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.acl +from hawat.app 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 @@ -89,8 +89,8 @@ def _get_search_form(request_args = None): # 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()) + form.dt_from.process_data(hawat.forms.default_dt_with_delta()) + form.dt_to.process_data(hawat.forms.default_dt()) return form @@ -342,7 +342,7 @@ 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', @@ -456,7 +456,7 @@ class APILegacySearchView(AJAXMixin, AbstractSearchView): # pylint: disable=loc #------------------------------------------------------------------------------- -class TimelineBlueprint(VialBlueprint): +class TimelineBlueprint(HawatBlueprint): """Pluggable module - IDEA event timelines (*timeline*).""" @classmethod @@ -486,8 +486,8 @@ class TimelineBlueprint(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`. """ diff --git a/lib/hawat/blueprints/timeline/forms.py b/lib/hawat/blueprints/timeline/forms.py index 6cf58bea8..e7adcdb70 100644 --- a/lib/hawat/blueprints/timeline/forms.py +++ b/lib/hawat/blueprints/timeline/forms.py @@ -26,8 +26,8 @@ from flask_babel import lazy_gettext from mentat.datatype.sqldb import GroupModel import hawat.const -import vial.forms -import vial.db +import hawat.forms +import hawat.db import hawat.const @@ -35,91 +35,91 @@ 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 = [ 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) + 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 = [ 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 + default = hawat.forms.default_dt ) - st_from = vial.forms.SmartDateTimeField( + st_from = hawat.forms.SmartDateTimeField( lazy_gettext('Storage time from:'), 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.') ) - st_to = vial.forms.SmartDateTimeField( + st_to = hawat.forms.SmartDateTimeField( lazy_gettext('Storage time to:'), 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.') ) - source_addrs = vial.forms.CommaListField( + source_addrs = hawat.forms.CommaListField( lazy_gettext('Source addresses:'), 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.') ) - target_addrs = vial.forms.CommaListField( + target_addrs = hawat.forms.CommaListField( lazy_gettext('Target addresses:'), 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.') ) - host_addrs = vial.forms.CommaListField( + host_addrs = hawat.forms.CommaListField( lazy_gettext('Host addresses:'), 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.') ) - source_ports = vial.forms.CommaListField( + source_ports = hawat.forms.CommaListField( lazy_gettext('Source ports:'), 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.') ) - target_ports = vial.forms.CommaListField( + target_ports = hawat.forms.CommaListField( lazy_gettext('Target ports:'), 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.') ) - host_ports = vial.forms.CommaListField( + host_ports = hawat.forms.CommaListField( lazy_gettext('Host ports:'), 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.') ) diff --git a/lib/hawat/blueprints/timeline/test/__init__.py b/lib/hawat/blueprints/timeline/test/__init__.py index eb0e5294f..7d1c7b383 100644 --- a/lib/hawat/blueprints/timeline/test/__init__.py +++ b/lib/hawat/blueprints/timeline/test/__init__.py @@ -16,13 +16,13 @@ Unit tests for :py:mod:`hawat.blueprints.timeline`. import unittest import hawat.const -import vial.test -import vial.db -from vial.test import VialTestCase +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. """ @@ -51,22 +51,22 @@ class SearchTestCase(TestRunnerMixin, VialTestCase): """Test access as anonymous user.""" self._attempt_fail_redirect() - @vial.test.do_as_user_decorator(hawat.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(hawat.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(hawat.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(hawat.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/users/__init__.py b/lib/hawat/blueprints/users/__init__.py index ab21b608c..ba70c1aaa 100644 --- a/lib/hawat/blueprints/users/__init__.py +++ b/lib/hawat/blueprints/users/__init__.py @@ -31,19 +31,430 @@ __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea Kropáčová <andrea 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 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 +import hawat.const +import hawat.acl +from hawat.app 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 -class CreateView(vial.blueprints.users.CreateView): # pylint: disable=locally-disabled,too-many-ancestors +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 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'] == 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 = 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(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() + ) + + 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 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 = 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): @@ -61,10 +472,64 @@ class CreateView(vial.blueprints.users.CreateView): # pylint: disable=locally-d ) -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 = flask.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 = 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): @@ -93,12 +558,632 @@ class UpdateView(vial.blueprints.users.UpdateView): # pylint: disable=locally-d 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(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 = 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(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 = 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(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 = 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(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 = 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(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 = 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 = [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 = flask.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 = 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 = [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 = flask.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 = 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 = [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 = flask.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 = 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(vial.blueprints.users.UsersBlueprint): +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', @@ -123,8 +1208,8 @@ class UsersBlueprint(vial.blueprints.users.UsersBlueprint): 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/users/forms.py b/lib/hawat/blueprints/users/forms.py index 60fe0a317..4944ac34d 100644 --- a/lib/hawat/blueprints/users/forms.py +++ b/lib/hawat/blueprints/users/forms.py @@ -20,11 +20,13 @@ __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea Kropáčová <andrea 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 +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, GroupModel @@ -33,7 +35,7 @@ def check_id_existence(form, field): Callback for validating user logins during account create action. """ try: - vial.db.db_get().session.query(UserModel).\ + hawat.db.db_get().session.query(UserModel).\ filter(UserModel.login == field.data).\ one() except sqlalchemy.orm.exc.NoResultFound: @@ -47,7 +49,7 @@ def check_id_uniqueness(form, field): """ Callback for validating user logins during account update action. """ - user = vial.db.db_get().session.query(UserModel).\ + user = hawat.db.db_get().session.query(UserModel).\ filter(UserModel.login == field.data).\ filter(UserModel.id != form.db_item_id).\ all() @@ -60,10 +62,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() -class BaseUserAccountForm(vial.forms.BaseItemForm): +class BaseUserAccountForm(hawat.forms.BaseItemForm): """ Class representing base user account form. """ @@ -89,7 +91,7 @@ class BaseUserAccountForm(vial.forms.BaseItemForm): wtforms.validators.Length(min = 3, max = 250) ] ) - locale = vial.forms.SelectFieldWithNone( + locale = hawat.forms.SelectFieldWithNone( lazy_gettext('Prefered locale:'), validators = [ wtforms.validators.Optional() @@ -98,7 +100,7 @@ class BaseUserAccountForm(vial.forms.BaseItemForm): filters = [lambda x: x or None], default = '' ) - timezone = vial.forms.SelectFieldWithNone( + timezone = hawat.forms.SelectFieldWithNone( lazy_gettext('Prefered timezone:'), validators = [ wtforms.validators.Optional(), @@ -141,8 +143,8 @@ class AdminUserAccountForm(BaseUserAccountForm): (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:'), @@ -184,7 +186,8 @@ class CreateUserAccountForm(AdminUserAccountForm): validators = [ wtforms.validators.DataRequired(), wtforms.validators.Length(min = 3, max = 50), - vial.forms.check_login, + check_login, + check_unique_login, check_id_existence ] ) @@ -205,7 +208,8 @@ class AdminUpdateUserAccountForm(AdminUserAccountForm): validators = [ wtforms.validators.DataRequired(), wtforms.validators.Length(min = 3, max = 50), - vial.forms.check_login, + hawat.forms.check_login, + check_unique_login, check_id_uniqueness ] ) @@ -220,3 +224,114 @@ 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/test/__init__.py b/lib/hawat/blueprints/users/test/__init__.py index 1849bddd7..371fa356e 100644 --- a/lib/hawat/blueprints/users/test/__init__.py +++ b/lib/hawat/blueprints/users/test/__init__.py @@ -16,17 +16,17 @@ Unit tests for :py:mod:`hawat.blueprints.users`. import unittest import hawat.const -import vial.test -import vial.test.fixtures -from vial.test import VialTestCase, ItemCreateVialTestCase +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(hawat.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 +35,7 @@ class UsersListTestCase(UsersTestCaseMixin, TestRunnerMixin, VialTestCase): """ self._attempt_fail_list() - @vial.test.do_as_user_decorator(hawat.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 +44,7 @@ class UsersListTestCase(UsersTestCaseMixin, TestRunnerMixin, VialTestCase): """ self._attempt_fail_list() - @vial.test.do_as_user_decorator(hawat.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 +53,7 @@ class UsersListTestCase(UsersTestCaseMixin, TestRunnerMixin, VialTestCase): """ self._attempt_succeed_list() - @vial.test.do_as_user_decorator(hawat.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,38 +63,38 @@ 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(hawat.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(hawat.const.ROLE_USER) - @vial.test.do_as_user_decorator(hawat.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(hawat.const.ROLE_DEVELOPER) - @vial.test.do_as_user_decorator(hawat.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(hawat.const.ROLE_MAINTAINER) - @vial.test.do_as_user_decorator(hawat.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(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(hawat.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'. @@ -103,7 +103,7 @@ class UsersShowOtherTestCase(UsersTestCaseMixin, TestRunnerMixin, VialTestCase): """ self._attempt_fail_show(hawat.const.ROLE_DEVELOPER) - @vial.test.do_as_user_decorator(hawat.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'. @@ -112,7 +112,7 @@ class UsersShowOtherTestCase(UsersTestCaseMixin, TestRunnerMixin, VialTestCase): """ self._attempt_fail_show(hawat.const.ROLE_MAINTAINER) - @vial.test.do_as_user_decorator(hawat.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'. @@ -123,7 +123,7 @@ class UsersShowOtherTestCase(UsersTestCaseMixin, TestRunnerMixin, VialTestCase): #-------------------------------------------------------------------------- - @vial.test.do_as_user_decorator(hawat.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'. @@ -133,7 +133,7 @@ class UsersShowOtherTestCase(UsersTestCaseMixin, TestRunnerMixin, VialTestCase): """ self._attempt_succeed_show(hawat.const.ROLE_USER) - @vial.test.do_as_user_decorator(hawat.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'. @@ -143,7 +143,7 @@ class UsersShowOtherTestCase(UsersTestCaseMixin, TestRunnerMixin, VialTestCase): """ self._attempt_succeed_show(hawat.const.ROLE_MAINTAINER) - @vial.test.do_as_user_decorator(hawat.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'. @@ -155,7 +155,7 @@ class UsersShowOtherTestCase(UsersTestCaseMixin, TestRunnerMixin, VialTestCase): #-------------------------------------------------------------------------- - @vial.test.do_as_user_decorator(hawat.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'. @@ -164,7 +164,7 @@ class UsersShowOtherTestCase(UsersTestCaseMixin, TestRunnerMixin, VialTestCase): """ self._attempt_succeed_show(hawat.const.ROLE_USER) - @vial.test.do_as_user_decorator(hawat.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'. @@ -173,7 +173,7 @@ class UsersShowOtherTestCase(UsersTestCaseMixin, TestRunnerMixin, VialTestCase): """ self._attempt_succeed_show(hawat.const.ROLE_DEVELOPER) - @vial.test.do_as_user_decorator(hawat.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'. @@ -184,23 +184,23 @@ class UsersShowOtherTestCase(UsersTestCaseMixin, TestRunnerMixin, VialTestCase): #-------------------------------------------------------------------------- - @vial.test.do_as_user_decorator(hawat.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(hawat.const.ROLE_USER) - @vial.test.do_as_user_decorator(hawat.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(hawat.const.ROLE_DEVELOPER) - @vial.test.do_as_user_decorator(hawat.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(hawat.const.ROLE_MAINTAINER) -class UsersCreateTestCase(UsersTestCaseMixin, TestRunnerMixin, ItemCreateVialTestCase): +class UsersCreateTestCase(UsersTestCaseMixin, TestRunnerMixin, ItemCreateHawatTestCase): """Class for testing ``users.create`` endpoint.""" user_data_fixture = [ @@ -211,125 +211,125 @@ class UsersCreateTestCase(UsersTestCaseMixin, TestRunnerMixin, ItemCreateVialTes ('enabled', True) ] - @vial.test.do_as_user_decorator(hawat.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(hawat.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(hawat.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(hawat.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(hawat.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(hawat.const.ROLE_USER) - @vial.test.do_as_user_decorator(hawat.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(hawat.const.ROLE_DEVELOPER) - @vial.test.do_as_user_decorator(hawat.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(hawat.const.ROLE_MAINTAINER) - @vial.test.do_as_user_decorator(hawat.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(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(hawat.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(hawat.const.ROLE_DEVELOPER) - @vial.test.do_as_user_decorator(hawat.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(hawat.const.ROLE_MAINTAINER) - @vial.test.do_as_user_decorator(hawat.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(hawat.const.ROLE_ADMIN) #-------------------------------------------------------------------------- - @vial.test.do_as_user_decorator(hawat.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(hawat.const.ROLE_USER) - @vial.test.do_as_user_decorator(hawat.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(hawat.const.ROLE_MAINTAINER) - @vial.test.do_as_user_decorator(hawat.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(hawat.const.ROLE_ADMIN) #-------------------------------------------------------------------------- - @vial.test.do_as_user_decorator(hawat.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(hawat.const.ROLE_USER) - @vial.test.do_as_user_decorator(hawat.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(hawat.const.ROLE_DEVELOPER) - @vial.test.do_as_user_decorator(hawat.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(hawat.const.ROLE_ADMIN) #-------------------------------------------------------------------------- - @vial.test.do_as_user_decorator(hawat.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(hawat.const.ROLE_USER) - @vial.test.do_as_user_decorator(hawat.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(hawat.const.ROLE_DEVELOPER) - @vial.test.do_as_user_decorator(hawat.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(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(hawat.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 ( @@ -341,7 +341,7 @@ class UsersEnableDisableTestCase(UsersTestCaseMixin, TestRunnerMixin, VialTestCa self._attempt_fail_disable(uname) self._attempt_fail_enable(uname) - @vial.test.do_as_user_decorator(hawat.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 ( @@ -353,7 +353,7 @@ class UsersEnableDisableTestCase(UsersTestCaseMixin, TestRunnerMixin, VialTestCa self._attempt_fail_disable(uname) self._attempt_fail_enable(uname) - @vial.test.do_as_user_decorator(hawat.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 ( @@ -364,7 +364,7 @@ class UsersEnableDisableTestCase(UsersTestCaseMixin, TestRunnerMixin, VialTestCa self._attempt_succeed_disable(uname) self._attempt_succeed_enable(uname) - @vial.test.do_as_user_decorator(hawat.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 ( @@ -376,7 +376,7 @@ class UsersEnableDisableTestCase(UsersTestCaseMixin, TestRunnerMixin, VialTestCa 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): @@ -501,7 +501,7 @@ class UsersAddRemRejMembershipTestCase(TestRunnerMixin, VialTestCase): ) self.mailbox_monitoring('off') - @vial.test.do_as_user_decorator(hawat.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 ( @@ -510,9 +510,9 @@ class UsersAddRemRejMembershipTestCase(TestRunnerMixin, VialTestCase): hawat.const.ROLE_MAINTAINER, hawat.const.ROLE_ADMIN ): - self._attempt_fail(uname, vial.test.fixtures.DEMO_GROUP_A) + self._attempt_fail(uname, hawat.test.fixtures.DEMO_GROUP_A) - @vial.test.do_as_user_decorator(hawat.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 ( @@ -520,9 +520,9 @@ class UsersAddRemRejMembershipTestCase(TestRunnerMixin, VialTestCase): hawat.const.ROLE_MAINTAINER, hawat.const.ROLE_ADMIN ): - self._attempt_succeed(uname, vial.test.fixtures.DEMO_GROUP_A) + self._attempt_succeed(uname, hawat.test.fixtures.DEMO_GROUP_A) - @vial.test.do_as_user_decorator(hawat.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 ( @@ -530,9 +530,9 @@ class UsersAddRemRejMembershipTestCase(TestRunnerMixin, VialTestCase): hawat.const.ROLE_DEVELOPER, hawat.const.ROLE_ADMIN ): - self._attempt_succeed(uname, vial.test.fixtures.DEMO_GROUP_A) + self._attempt_succeed(uname, hawat.test.fixtures.DEMO_GROUP_A) - @vial.test.do_as_user_decorator(hawat.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 ( @@ -540,13 +540,13 @@ class UsersAddRemRejMembershipTestCase(TestRunnerMixin, VialTestCase): hawat.const.ROLE_DEVELOPER, hawat.const.ROLE_MAINTAINER ): - self._attempt_succeed(uname, vial.test.fixtures.DEMO_GROUP_A) + 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(hawat.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 ( @@ -557,7 +557,7 @@ class UsersDeleteTestCase(UsersTestCaseMixin, TestRunnerMixin, VialTestCase): ): self._attempt_fail_delete(uname) - @vial.test.do_as_user_decorator(hawat.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 ( @@ -568,7 +568,7 @@ class UsersDeleteTestCase(UsersTestCaseMixin, TestRunnerMixin, VialTestCase): ): self._attempt_fail_delete(uname) - @vial.test.do_as_user_decorator(hawat.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 ( @@ -579,7 +579,7 @@ class UsersDeleteTestCase(UsersTestCaseMixin, TestRunnerMixin, VialTestCase): ): self._attempt_fail_delete(uname) - @vial.test.do_as_user_decorator(hawat.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 ( diff --git a/lib/hawat/blueprints/whois/__init__.py b/lib/hawat/blueprints/whois/__init__.py index 975338cda..04b027200 100644 --- a/lib/hawat/blueprints/whois/__init__.py +++ b/lib/hawat/blueprints/whois/__init__.py @@ -59,13 +59,13 @@ import mentat.services.whois from mentat.const import tr_ import hawat.const -import vial.db +import hawat.db import hawat.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.acl +from hawat.app 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 @@ -167,7 +167,7 @@ class SnippetSearchView(SnippetMixin, AbstractSearchView): # pylint: disable=lo #------------------------------------------------------------------------------- -class WhoisBlueprint(VialBlueprint): +class WhoisBlueprint(HawatBlueprint): """Pluggable module - Local WHOIS service (*whois*).""" @classmethod @@ -208,8 +208,8 @@ 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`. """ diff --git a/lib/hawat/blueprints/whois/test/__init__.py b/lib/hawat/blueprints/whois/test/__init__.py index 53c0df4a9..8f27bb0b5 100644 --- a/lib/hawat/blueprints/whois/test/__init__.py +++ b/lib/hawat/blueprints/whois/test/__init__.py @@ -16,13 +16,13 @@ Unit tests for :py:mod:`hawat.blueprints.whois`. import unittest import hawat.const -import vial.test -import vial.db -from vial.test import VialTestCase +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. """ @@ -51,22 +51,22 @@ class SearchTestCase(TestRunnerMixin, VialTestCase): """Test access as anonymous user.""" self._attempt_fail() - @vial.test.do_as_user_decorator(hawat.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(hawat.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(hawat.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(hawat.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/test/__init__.py b/lib/hawat/test/__init__.py index 7998eb711..bb036ef4b 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 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(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 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(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 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', @@ -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 100% rename from lib/vial/test/fixtures.py rename to lib/hawat/test/fixtures.py diff --git a/lib/hawat/test/runner.py b/lib/hawat/test/runner.py index 542a71581..0a027bebc 100644 --- a/lib/hawat/test/runner.py +++ b/lib/hawat/test/runner.py @@ -13,7 +13,7 @@ Base library for Hawat unit tests. """ import hawat.const -import vial.test.fixtures +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 @@ -66,7 +66,7 @@ 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(hawat.const.MODEL_USER)): fixture.organization = 'BOGUS DOMAIN, a.l.e.' diff --git a/lib/vial/__init__.py b/lib/vial/__init__.py deleted file mode 100644 index 077a2c7a6..000000000 --- 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/app.py b/lib/vial/app.py deleted file mode 100644 index bd33d7e62..000000000 --- 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 hawat.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:`hawat.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:`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:`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[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 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[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 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 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 = 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(hawat.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(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 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.'), - 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. - - :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(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( - 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(hawat.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] - )), - hawat.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[hawat.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 e69de29bb..000000000 diff --git a/lib/vial/blueprints/auth_env/__init__.py b/lib/vial/blueprints/auth_env/__init__.py deleted file mode 100644 index 4160e87f1..000000000 --- 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 hawat.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(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): - """ - 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 8b3610ec8..000000000 --- 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 e6f9ac35b..000000000 --- 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 5a58a2339..000000000 --- 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/templates/auth_env/registration/email_managers.txt b/lib/vial/blueprints/auth_env/templates/auth_env/registration/email_managers.txt deleted file mode 100644 index 6cc8bf534..000000000 --- a/lib/vial/blueprints/auth_env/templates/auth_env/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_env/templates/auth_env/registration/email_user.txt b/lib/vial/blueprints/auth_env/templates/auth_env/registration/email_user.txt deleted file mode 100644 index a3f2d82c9..000000000 --- a/lib/vial/blueprints/auth_env/templates/auth_env/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_env/test/__init__.py b/lib/vial/blueprints/auth_env/test/__init__.py deleted file mode 100644 index 1f1e9261c..000000000 --- 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 hawat.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(hawat.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(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) - - 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(hawat.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(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) - - 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(hawat.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(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) - - 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 d37cae49d..000000000 --- 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 hawat.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(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): - 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 c588b75ba..000000000 --- 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 ba68267a6..000000000 --- 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 18bad8fe9..000000000 --- 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 5a58a2339..000000000 --- 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 6cc8bf534..000000000 --- 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 a3f2d82c9..000000000 --- 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 8e673ffee..000000000 --- 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 hawat.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(hawat.const.ROLE_USER) - user.set_password('password') - self.user_save(user) - - 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) - - 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(hawat.const.ROLE_DEVELOPER) - user.set_password('password') - self.user_save(user) - - 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) - - 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(hawat.const.ROLE_ADMIN) - user.set_password('password') - self.user_save(user) - - 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) - - 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/groups/__init__.py b/lib/vial/blueprints/groups/__init__.py deleted file mode 100644 index 2556a71f4..000000000 --- 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(hawat.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(hawat.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(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( - 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(hawat.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(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 = 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(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( - 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(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( - 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(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( - 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(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( - 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(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( - 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(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( - 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(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 = 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(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 = 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(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 = 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 53ce5ff20..000000000 --- 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 183e476c8..000000000 --- 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 a08a12bd0..000000000 --- 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 c23eaecce..000000000 --- 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 c28c4a8a3..000000000 --- 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 72d449bea..000000000 --- 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 fdf42a305..000000000 --- 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 ea8f3f694..000000000 --- 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 hawat.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(hawat.const.ROLE_USER) - def test_01_as_user(self): - """Test access as user ``user``.""" - self._attempt_fail() - - @vial.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(hawat.const.ROLE_MAINTAINER) - def test_03_as_maintainer(self): - """Test access as user ``maintainer``.""" - self._attempt_succeed() - - @vial.test.do_as_user_decorator(hawat.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(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) - - @vial.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) - - @vial.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) - - @vial.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) - - -@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(hawat.const.ROLE_USER) - def test_01_as_user(self): - """Test access as user 'user'.""" - self._attempt_fail() - - @vial.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(hawat.const.ROLE_MAINTAINER) - def test_03_as_maintainer(self): - """Test access as user 'maintainer'.""" - self._attempt_succeed() - - @vial.test.do_as_user_decorator(hawat.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(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) - - @vial.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) - - @vial.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) - - @vial.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) - - -@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(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) - - @vial.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) - - @vial.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) - - @vial.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) - - -@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(hawat.const.ROLE_USER) - def test_01_as_user(self): - """Test access as user 'user'.""" - for uname in ( - hawat.const.ROLE_USER, - hawat.const.ROLE_DEVELOPER, - hawat.const.ROLE_MAINTAINER, - hawat.const.ROLE_ADMIN - ): - self._attempt_fail(uname, vial.test.fixtures.DEMO_GROUP_A) - - @vial.test.do_as_user_decorator(hawat.const.ROLE_DEVELOPER) - def test_02_as_developer(self): - """Test access as user 'developer'.""" - for uname in ( - hawat.const.ROLE_USER, - hawat.const.ROLE_MAINTAINER, - hawat.const.ROLE_ADMIN - ): - self._attempt_succeed(uname, vial.test.fixtures.DEMO_GROUP_A) - - @vial.test.do_as_user_decorator(hawat.const.ROLE_MAINTAINER) - def test_03_as_maintainer(self): - """Test access as user 'maintainer'.""" - for uname in ( - hawat.const.ROLE_USER, - hawat.const.ROLE_DEVELOPER, - hawat.const.ROLE_ADMIN - ): - self._attempt_succeed(uname, vial.test.fixtures.DEMO_GROUP_A) - - @vial.test.do_as_user_decorator(hawat.const.ROLE_ADMIN) - def test_04_as_admin(self): - """Test access as user 'admin'.""" - for uname in ( - hawat.const.ROLE_USER, - hawat.const.ROLE_DEVELOPER, - hawat.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(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) - - @vial.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) - - @vial.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) - - @vial.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) - - -#------------------------------------------------------------------------------- - - -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 10ecd5656..000000000 --- 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 e44993332..000000000 --- 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 20b47a395..000000000 --- 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 hawat.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(hawat.const.ROLE_USER) - def test_02_as_user(self): - """Test access as user ``user``.""" - self._attempt_succeed() - - @vial.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(hawat.const.ROLE_MAINTAINER) - def test_04_as_maintainer(self): - """Test access as user ``maintainer``.""" - self._attempt_succeed() - - @vial.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__": - unittest.main() diff --git a/lib/vial/blueprints/users/__init__.py b/lib/vial/blueprints/users/__init__.py deleted file mode 100644 index 5df50e104..000000000 --- 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 hawat.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(hawat.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'] == 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 = 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(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 = [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(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 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(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 = 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(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 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(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( - 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(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( - 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(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( - 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(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( - 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(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( - 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(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 = 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(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 = 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(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 = 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 28d88bb8f..000000000 --- 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 hawat.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')), - (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/vial/blueprints/users/templates/users/addmembership.html b/lib/vial/blueprints/users/templates/users/addmembership.html deleted file mode 100644 index 75fc075cf..000000000 --- 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 2f97a58bb..000000000 --- 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 df918e00e..000000000 --- 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 732c1f334..000000000 --- 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 ec04d6b5a..000000000 --- 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 1fa014985..000000000 --- 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 dde19de1d..000000000 --- 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 7dbd927e0..000000000 --- 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 hawat.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(hawat.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(hawat.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(hawat.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(hawat.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(hawat.const.ROLE_USER) - def test_01_as_user(self): - """Test access as user 'user'.""" - self._attempt_succeed_show(hawat.const.ROLE_USER) - - @vial.test.do_as_user_decorator(hawat.const.ROLE_DEVELOPER) - def test_02_as_developer(self): - """Test access as user 'developer'.""" - self._attempt_succeed_show(hawat.const.ROLE_DEVELOPER) - - @vial.test.do_as_user_decorator(hawat.const.ROLE_MAINTAINER) - def test_03_as_maintainer(self): - """Test access as user 'maintainer'.""" - self._attempt_succeed_show(hawat.const.ROLE_MAINTAINER) - - @vial.test.do_as_user_decorator(hawat.const.ROLE_ADMIN) - def test_04_as_admin(self): - """Test access as user 'admin'.""" - self._attempt_succeed_show(hawat.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(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(hawat.const.ROLE_DEVELOPER) - - @vial.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(hawat.const.ROLE_MAINTAINER) - - @vial.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(hawat.const.ROLE_ADMIN) - - #-------------------------------------------------------------------------- - - @vial.test.do_as_user_decorator(hawat.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(hawat.const.ROLE_USER) - - @vial.test.do_as_user_decorator(hawat.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(hawat.const.ROLE_MAINTAINER) - - @vial.test.do_as_user_decorator(hawat.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(hawat.const.ROLE_ADMIN) - - #-------------------------------------------------------------------------- - - @vial.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(hawat.const.ROLE_USER) - - @vial.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(hawat.const.ROLE_DEVELOPER) - - @vial.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(hawat.const.ROLE_MAINTAINER) - - #-------------------------------------------------------------------------- - - @vial.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(hawat.const.ROLE_USER) - - @vial.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(hawat.const.ROLE_DEVELOPER) - - @vial.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(hawat.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(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(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(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(hawat.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(hawat.const.ROLE_USER) - def test_01_as_user(self): - """Test access as user 'user'.""" - self._attempt_succeed_update(hawat.const.ROLE_USER) - - @vial.test.do_as_user_decorator(hawat.const.ROLE_DEVELOPER) - def test_02_as_developer(self): - """Test access as user 'developer'.""" - self._attempt_succeed_update(hawat.const.ROLE_DEVELOPER) - - @vial.test.do_as_user_decorator(hawat.const.ROLE_MAINTAINER) - def test_03_as_maintainer(self): - """Test access as user 'maintainer'.""" - self._attempt_succeed_update(hawat.const.ROLE_MAINTAINER) - - @vial.test.do_as_user_decorator(hawat.const.ROLE_ADMIN) - def test_04_as_admin(self): - """Test access as user 'admin'.""" - self._attempt_succeed_update(hawat.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(hawat.const.ROLE_USER) - def test_01_as_user_developer(self): - """Test access to 'developer' account as user 'user'.""" - self._attempt_fail_update(hawat.const.ROLE_DEVELOPER) - - @vial.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(hawat.const.ROLE_MAINTAINER) - - @vial.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(hawat.const.ROLE_ADMIN) - - #-------------------------------------------------------------------------- - - @vial.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(hawat.const.ROLE_USER) - - @vial.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(hawat.const.ROLE_MAINTAINER) - - @vial.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(hawat.const.ROLE_ADMIN) - - #-------------------------------------------------------------------------- - - @vial.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(hawat.const.ROLE_USER) - - @vial.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(hawat.const.ROLE_DEVELOPER) - - @vial.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(hawat.const.ROLE_ADMIN) - - #-------------------------------------------------------------------------- - - @vial.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(hawat.const.ROLE_USER) - - @vial.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(hawat.const.ROLE_DEVELOPER) - - @vial.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(hawat.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(hawat.const.ROLE_USER) - def test_01_as_user(self): - """Test access as user 'user'.""" - for uname in ( - 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(hawat.const.ROLE_DEVELOPER) - def test_02_as_developer(self): - """Test access as user 'developer'.""" - for uname in ( - 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(hawat.const.ROLE_MAINTAINER) - def test_03_as_maintainer(self): - """Test access as user 'maintainer'.""" - for uname in ( - 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(hawat.const.ROLE_ADMIN) - def test_04_as_admin(self): - """Test access as user 'admin'.""" - for uname in ( - hawat.const.ROLE_USER, - hawat.const.ROLE_DEVELOPER, - hawat.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(hawat.const.ROLE_USER) - def test_01_as_user(self): - """Test access as user 'user'.""" - for uname in ( - hawat.const.ROLE_USER, - hawat.const.ROLE_DEVELOPER, - hawat.const.ROLE_MAINTAINER, - hawat.const.ROLE_ADMIN - ): - self._attempt_fail(uname, vial.test.fixtures.DEMO_GROUP_A) - - @vial.test.do_as_user_decorator(hawat.const.ROLE_DEVELOPER) - def test_02_as_developer(self): - """Test access as user 'developer'.""" - for uname in ( - hawat.const.ROLE_USER, - hawat.const.ROLE_MAINTAINER, - hawat.const.ROLE_ADMIN - ): - self._attempt_succeed(uname, vial.test.fixtures.DEMO_GROUP_A) - - @vial.test.do_as_user_decorator(hawat.const.ROLE_MAINTAINER) - def test_03_as_maintainer(self): - """Test access as user 'maintainer'.""" - for uname in ( - hawat.const.ROLE_USER, - hawat.const.ROLE_DEVELOPER, - hawat.const.ROLE_ADMIN - ): - self._attempt_succeed(uname, vial.test.fixtures.DEMO_GROUP_A) - - @vial.test.do_as_user_decorator(hawat.const.ROLE_ADMIN) - def test_04_as_admin(self): - """Test access as user 'admin'.""" - for uname in ( - hawat.const.ROLE_USER, - hawat.const.ROLE_DEVELOPER, - hawat.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(hawat.const.ROLE_USER) - def test_01_as_user(self): - """Test access as user 'user'.""" - for uname in ( - 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(hawat.const.ROLE_DEVELOPER) - def test_02_as_developer(self): - """Test access as user 'developer'.""" - for uname in ( - 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(hawat.const.ROLE_MAINTAINER) - def test_03_as_maintainer(self): - """Test access as user 'maintainer'.""" - for uname in ( - 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(hawat.const.ROLE_ADMIN) - def test_04_as_admin(self): - """Test access as user 'admin'.""" - for uname in ( - hawat.const.ROLE_USER, - hawat.const.ROLE_DEVELOPER, - hawat.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 73188a1b8..000000000 --- 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/test/__init__.py b/lib/vial/test/__init__.py deleted file mode 100644 index 2c461b83a..000000000 --- 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(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 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(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 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 bf8c3a02f..000000000 --- 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'] = { - hawat.const.MODEL_USER: vial.model.db.UserModel, - hawat.const.MODEL_GROUP: vial.model.db.GroupModel, - hawat.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) -- GitLab