diff --git a/conf/mentat-hawat.py.conf b/conf/mentat-hawat.py.conf index b24cec91876fb83e924b33da2d3b3e411494cd5c..e60fbf301091345e3322881b0a20f95da1060d13 100644 --- a/conf/mentat-hawat.py.conf +++ b/conf/mentat-hawat.py.conf @@ -8,7 +8,7 @@ SECRET_KEY = '!!!-CHANGE-ME-ASAP-!!!-local-secret-key' # Define list of Hawat administrator accounts. -#HAWAT_ADMINS = ['boss@domain.org', 'admin@domain.org'] +#EMAIL_ADMINS = ['boss@domain.org', 'admin@domain.org'] # Define list of Hawat administrator accounts, that receive feedback messages for reports. #HAWAT_REPORT_FEEDBACK_MAILS = ['admin@domain.org'] diff --git a/lib/hawat/acl.py b/lib/hawat/acl.py index 11cfea977da5864e705c24e9b336487d82cd7e2b..7c143607454e991ffa004a6c096548dac7846009 100644 --- a/lib/hawat/acl.py +++ b/lib/hawat/acl.py @@ -21,7 +21,7 @@ from functools import partial import flask_principal -import hawat.const +import vial.const MembershipNeed = partial(flask_principal.Need, 'membership') # pylint: disable=locally-disabled,invalid-name @@ -31,11 +31,11 @@ ManagementNeed = partial(flask_principal.Need, 'management') # pylint: disable ManagementNeed.__doc__ = """A need with the method preset to `"management"`.""" -ROLE_NAME_ADMIN = hawat.const.ROLE_ADMIN -ROLE_NAME_MAINTAINER = hawat.const.ROLE_MAINTAINER +ROLE_NAME_ADMIN = vial.const.ROLE_ADMIN +ROLE_NAME_MAINTAINER = vial.const.ROLE_MAINTAINER ROLE_NAME_POWER = 'power' -ROLE_NAME_DEVELOPER = hawat.const.ROLE_DEVELOPER -ROLE_NAME_USER = hawat.const.ROLE_USER +ROLE_NAME_DEVELOPER = vial.const.ROLE_DEVELOPER +ROLE_NAME_USER = vial.const.ROLE_USER ROLE_NAME_ANY = 'any' diff --git a/lib/hawat/app.py b/lib/hawat/app.py index 3e387e4540fd6547f3118bc4a87903fbe2cd8396..3cb5f26f97462d02f07e9145bf92150ef1e792d4 100644 --- a/lib/hawat/app.py +++ b/lib/hawat/app.py @@ -22,47 +22,17 @@ __author__ = "Jan Mach <jan.mach@cesnet.cz>" __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea Kropáčová <andrea.kropacova@cesnet.cz>" -import sys -import traceback import os -import uuid -import copy -import datetime -import json -import jinja2 -import yaml -# -# Flask related modules. -# -import flask -import flask_babel -#import flask_jsglue -import flask_migrate -import flask_login -import flask_principal +from mentat.const import CKEY_CORE_DATABASE, CKEY_CORE_DATABASE_SQLSTORAGE -# -# Custom modules. -# -import mentat -import mentat._buildmeta -import mentat.idea.internal -import mentat.idea.jsondict -from mentat.datatype.sqldb import UserModel +import vial +from hawat.const import CFGKEY_MENTAT_CORE import hawat.base -import hawat.const import hawat.config -import hawat.acl -import hawat.log -import hawat.mailer import hawat.db -import hawat.intl import hawat.events -import hawat.errors -import hawat.utils -import hawat.jsglue APP_NAME = 'hawat' @@ -76,7 +46,7 @@ def create_app_full( config_dict = None, config_object = 'hawat.config.ProductionConfig', config_file = None, - config_env = 'FLASK_CONFIG_FILE'): + config_env = 'HAWAT_CONFIG_FILE'): """ Factory function for building Hawat application. This function takes number of optional arguments, that can be used to create a very customized instance of @@ -90,35 +60,19 @@ def create_app_full( :param str config_file: Name of the file containing additional configurations. :param str config_env: Name of the environment variable pointing to file containing configurations. :return: Hawat application - :rtype: hawat.base.Vial + :rtype: hawat.base.HawatApp """ - app = hawat.base.Vial(APP_NAME) - - if config_dict and isinstance(config_dict, dict): - app.config.update(config_dict) - app.config.update( - hawat.config.get_app_root_relative_config() + 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_app ) - 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) - _setup_app_logging(app) - _setup_app_mailer(app) - _setup_app_core(app) - _setup_app_db(app) - _setup_app_eventdb(app) - _setup_app_auth(app) - _setup_app_acl(app) - _setup_app_intl(app) - _setup_app_menu(app) - _setup_app_blueprints(app) - - return app def create_app(): """ @@ -127,7 +81,7 @@ def create_app(): variables. :return: Hawat application - :rtype: hawat.base.Vial + :rtype: hawat.base.HawatApp """ config_name = os.getenv('FLASK_CONFIG', 'default') config_file = hawat.config.get_default_config_file() @@ -139,780 +93,24 @@ def create_app(): ) -#------------------------------------------------------------------------------- - - -def _setup_app_logging(app): - """ - Setup logging to file and via email for given Hawat application. Logging - capabilities are adjustable by application configuration. - - :param hawat.base.Vial app: Hawat application to be modified. - :return: Modified Hawat application - :rtype: hawat.base.Vial - """ - hawat.log.setup_logging_default(app) - hawat.log.setup_logging_file(app) - if not app.debug: - hawat.log.setup_logging_email(app) - - return app - - -def _setup_app_mailer(app): - """ - Setup mailer service for Hawat application. - - :param hawat.base.Vial app: Hawat application to be modified. - :return: Modified Hawat application - :rtype: hawat.base.Vial - """ - hawat.mailer.MAILER.init_app(app) - app.mailer = hawat.mailer.MAILER - - return app - - -def _setup_app_core(app): - """ - Setup application core for given Hawat application. The application core - contains following features: - - * Error handlers - * Default routes - * Additional custom Jinja template variables - * Additional custom Jinja template macros - - :param hawat.base.Vial app: Hawat application to be modified. - :return: Modified Hawat application - :rtype: hawat.base.Vial - """ - @app.errorhandler(400) - def eh_badrequest(err): # pylint: disable=locally-disabled,unused-variable - """Flask error handler to be called to service HTTP 400 error.""" - flask.current_app.logger.critical( - "BAD REQUEST\n\nRequest: %s\nTraceback:\n%s", - flask.request.full_path, - ''.join( - traceback.TracebackException( - *sys.exc_info() - ).format() - ) - ) - return hawat.errors.error_handler_switch(400, err) - - @app.errorhandler(403) - def eh_forbidden(err): # pylint: disable=locally-disabled,unused-variable - """Flask error handler to be called to service HTTP 403 error.""" - return hawat.errors.error_handler_switch(403, err) - - @app.errorhandler(404) - def eh_page_not_found(err): # pylint: disable=locally-disabled,unused-variable - """Flask error handler to be called to service HTTP 404 error.""" - return hawat.errors.error_handler_switch(404, err) - - @app.errorhandler(405) - def eh_method_not_allowed(err): # pylint: disable=locally-disabled,unused-variable - """Flask error handler to be called to service HTTP 405 error.""" - return hawat.errors.error_handler_switch(405, err) - - @app.errorhandler(410) - def eh_gone(err): # pylint: disable=locally-disabled,unused-variable - """Flask error handler to be called to service HTTP 410 error.""" - return hawat.errors.error_handler_switch(410, err) - - @app.errorhandler(500) - def eh_internal_server_error(err): # pylint: disable=locally-disabled,unused-variable - """Flask error handler to be called to service HTTP 500 error.""" - flask.current_app.logger.critical( - "INTERNAL SERVER ERROR\n\nRequest: %s\nTraceback:\n%s", - flask.request.full_path, - ''.join( - traceback.TracebackException( - *sys.exc_info() - ).format() - ), - ) - return hawat.errors.error_handler_switch(500, err) - - @app.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() - - @app.context_processor - def jinja_inject_variables(): # pylint: disable=locally-disabled,unused-variable - """ - Inject additional variables into Jinja2 global template namespace. - """ - return dict( - hawat_version = mentat.__version__, - hawat_bversion = mentat._buildmeta.__bversion__, # pylint: disable=locally-disabled,protected-access - hawat_bversion_full = mentat._buildmeta.__bversion_full__, # pylint: disable=locally-disabled,protected-access - hawat_current_app = flask.current_app, - hawat_current_menu_main = flask.current_app.menu_main, - hawat_current_menu_auth = flask.current_app.menu_auth, - hawat_current_menu_anon = flask.current_app.menu_anon, - hawat_current_view = app.get_endpoint_class(flask.request.endpoint, True), - hawat_chart_dimensions = 'height:700px', - hawat_logger = flask.current_app.logger - ) - - @app.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_endpoints_dict(): - """ - Return dictionary of all registered application view endpoints. - """ - return flask.current_app.view_functions - - 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 app.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 app.view_functions - - def get_icon(icon_name, default_icon = 'missing-icon'): - """ - Get HTML icon markup for given icon. The icon will be looked up in - the :py:const:`hawat.const.FA_ICONS` lookup table. - - :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( - hawat.const.FA_ICONS.get( - icon_name, - hawat.const.FA_ICONS.get(default_icon) - ) - ) - - def get_module_icon(endpoint): - """ - Get HTML icon markup for parent module of given view endpoint. - - :param str endpoint: Name of the view endpoint. - :return: Icon including HTML markup. - :rtype: flask.Markup - """ - return flask.Markup( - hawat.const.FA_ICONS[app.get_endpoint_class(endpoint).module_ref().get_module_icon()] - ) - - def get_endpoint_icon(endpoint): - """ - 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( - hawat.const.FA_ICONS[app.get_endpoint_class(endpoint).get_view_icon()] - ) - - def get_csag(group): - """ - Return list of all registered context search actions under given group. - - :param str group: Name of the group. - :return: List of all registered context search actions. - :rtype: list - """ - return app.get_csag(group) - - 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( - 'design.static', - filename = 'images/country-flags/flags-iso/shiny/16/{}.png'.format( - country.upper() - ) - ) - ) - ) - - def get_timedelta(tstamp): - """ - Get timedelta from current UTC time and given datetime object. - - :param datetime.datetime: Datetime of the lower timedelta boundary. - :return: Timedelta object. - :rtype: datetime.timedelta - """ - return datetime.datetime.utcnow() - tstamp - - def get_datetime_utc(aware = False): - """ - Get current UTC datetime. - - :return: Curent UTC datetime. - :rtype: datetime.datetime - """ - if aware: - return datetime.datetime.now(datetime.timezone.utc) - return datetime.datetime.utcnow() - - def parse_datetime(dtstring): - """ - Parse given datetime string. - - :param str dtstring: Datetime string in ISON format to parse. - :return: Curent UTC datetime. - :rtype: datetime.datetime - """ - return datetime.datetime.fromisoformat(dtstring) - - def get_datetime_local(): - """ - Get current local timestamp. - - :return: Curent local timestamp. - :rtype: datetime.datetime - """ - return datetime.datetime.now() - - def get_reporting_interval_name(seconds): - """ - Get a name of reporting interval for given time delta. - - :param int seconds: Time interval delta in seconds. - :return: Name of the reporting interval. - :rtype: str - """ - return mentat.const.REPORTING_INTERVALS_INV[seconds] - - def check_file_exists(filename): - """ - Check, that given file exists in the filesystem. - - :param str filename: Name of the file to check. - :return: Existence flag as ``True`` or ``False``. - :rtype: bool - """ - return os.path.isfile(filename) - - def in_query_params(haystack, needles, on_true = True, on_false = False, on_empty = False): - """ - Utility method for checking that any needle from given list of needles is - present in given haystack. - """ - if not haystack: - return on_empty - for needle in needles: - if needle in haystack: - return on_true - return on_false - - def generate_query_params(baseparams, updates): - """ - Generate query parameters for GET method form. - - :param dict baseparams: Original query parameters. - :param dict updates: Updates for query parameters. - :return: Deep copy of original parameters modified with given updates. - :rtype: dict - """ - result = copy.deepcopy(baseparams) - result.update(updates) - return result - - 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( - app.jinja_loader.get_source(app.jinja_env, filename)[0] - ) - - def json_to_yaml(json_data): - """ - 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 yaml.dump( - yaml.load( - json_data - ), - default_flow_style=False - ) - - def get_uuid4(): - """ - Generate random UUID identifier. - """ - return uuid.uuid4() - - def get_limit_counter(limit = None): - """ - Get fresh instance of limit counter. - """ - if not limit: - limit = flask.current_app.config['HAWAT_LIMIT_AODS'] - return hawat.utils.LimitCounter(limit) - - def load_json_from_file(filename): - with open(filename) as fhnd: - res = json.load(fhnd) - return res - - def make_copy_deep(data): - """ - Make a deep copy of given data structure. - """ - return copy.deepcopy(data) - - return 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_csag = get_csag, - get_country_flag = get_country_flag, - - get_timedelta = get_timedelta, - get_datetime_utc = get_datetime_utc, - get_datetime_local = get_datetime_local, - parse_datetime = parse_datetime, - - get_datetime_window = hawat.base.VialUtils.get_datetime_window, - - get_reporting_interval_name = get_reporting_interval_name, - - check_file_exists = check_file_exists, - - in_query_params = in_query_params, - generate_query_params = generate_query_params, - - current_datetime_utc = datetime.datetime.utcnow(), - - include_raw = include_raw, - json_to_yaml = json_to_yaml, - get_uuid4 = get_uuid4, - get_limit_counter = get_limit_counter, - load_json_from_file = load_json_from_file, - make_copy_deep = make_copy_deep - ) - - class HawatJSONEncoder(flask.json.JSONEncoder): - """ - Custom JSON encoder for converting anything into JSON strings. - """ - def default(self, obj): # pylint: disable=locally-disabled,method-hidden,arguments-differ - try: - if isinstance(obj, mentat.idea.internal.Idea): - return mentat.idea.jsondict.Idea(obj).data - except: # pylint: disable=locally-disabled,bare-except - pass - 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) - - app.json_encoder = HawatJSONEncoder - - @app.route('/hawat-main.js') - def mainjs(): # pylint: disable=locally-disabled,unused-variable - """ - Default route for main application JavaScript file. - """ - return flask.make_response( - flask.render_template('hawat-main.js'), - 200, - {'Content-Type': 'text/javascript'} - ) - - # Initialize JSGlue plugin for using `flask.url_for()` method in JavaScript. - #jsglue = flask_jsglue.JSGlue() - jsglue = hawat.jsglue.JSGlue() - jsglue.init_app(app) - - @app.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 app - - -def _setup_app_db(app): - """ - Setup application database service for given Hawat application. - - :param hawat.base.Vial app: Hawat application to be modified. - :return: Modified Hawat application - :rtype: hawat.base.Vial - """ - dbcfg = hawat.db.db_settings(app) - app.config['SQLALCHEMY_DATABASE_URI'] = dbcfg['url'] - app.config['SQLALCHEMY_ECHO'] = dbcfg['echo'] - - dbh = hawat.db.db_get() - dbh.init_app(app) - - # Initialize database migration service and register it among the application - # resources for possible future use. - migrate = flask_migrate.Migrate( - app = app, - db = dbh, - directory = os.path.realpath( - os.path.join( - os.path.dirname( - os.path.abspath(__file__) - ), - 'migrations' - ) - ) +def _config_app(app_config): + app_config.update( + hawat.config.get_app_root_relative_config() ) - app.set_resource(hawat.const.RESOURCE_MIGRATE, migrate) - - app.logger.info("Connected to database via SQLAlchemy") - - return app - - -def _setup_app_eventdb(app): - """ - Setup application database service for given Hawat application. - - :param hawat.base.Vial app: Hawat application to be modified. - :return: Modified Hawat application - :rtype: hawat.base.Vial - """ - hawat.events.db_init(app) - app.logger.info("Connected to event database") - - return app - - -def _setup_app_auth(app): - """ - Setup application authentication features. - - :param hawat.base.Vial app: Hawat application to be modified. - :return: Modified Hawat application - :rtype: hawat.base.Vial - """ - - lim = flask_login.LoginManager() - lim.init_app(app) - lim.login_view = app.config['HAWAT_LOGIN_VIEW'] - lim.login_message = flask_babel.gettext("Please log in to access this page.") - lim.login_message_category = app.config['HAWAT_LOGIN_MSGCAT'] - - app.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. - """ - return hawat.db.db_get().session.query(UserModel).filter(UserModel.id == user_id).one_or_none() - - @app.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['HAWAT_LOGOUT_REDIRECT'] - ) - ) - - return app - -def _setup_app_acl(app): - """ - Setup application ACL features. - - :param hawat.base.Vial app: Hawat application to be modified. - :return: Modified Hawat application - :rtype: hawat.base.Vial - """ - fpp = flask_principal.Principal(app, skip_static = True) - app.set_resource(hawat.const.RESOURCE_PRINCIPAL, fpp) - @flask_principal.identity_loaded.connect_via(app) - 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 + dbcfg = app_config[CFGKEY_MENTAT_CORE][CKEY_CORE_DATABASE][CKEY_CORE_DATABASE_SQLSTORAGE] + app_config['SQLALCHEMY_DATABASE_URI'] = dbcfg['url'] + app_config['SQLALCHEMY_ECHO'] = dbcfg['echo'] - if not flask_login.current_user.is_authenticated: - flask.current_app.logger.debug( - "Loaded ACL identity for anonymous user '{}'.".format( - str(flask_login.current_user) - ) - ) - return - flask.current_app.logger.debug( - "Loading ACL identity for user '{}'.".format( - str(flask_login.current_user) - ) - ) - - # Add the UserNeed to the identity. - if hasattr(flask_login.current_user, 'get_id'): - identity.provides.add( - flask_principal.UserNeed(flask_login.current_user.id) - ) - - # Assuming the User model has a list of roles, update the - # identity with the roles that the user provides. - if hasattr(flask_login.current_user, 'roles'): - for role in flask_login.current_user.roles: - identity.provides.add( - flask_principal.RoleNeed(role) - ) - - # Assuming the User model has a list of group memberships, update the - # identity with the groups that the user is member of. - if hasattr(flask_login.current_user, 'memberships'): - for group in flask_login.current_user.memberships: - identity.provides.add( - hawat.acl.MembershipNeed(group.id) - ) - - # Assuming the User model has a list of group managements, update the - # identity with the groups that the user is manager of. - if hasattr(flask_login.current_user, 'managements'): - for group in flask_login.current_user.managements: - identity.provides.add( - hawat.acl.ManagementNeed(group.id) - ) - - @app.context_processor - def utility_acl_processor(): # pylint: disable=locally-disabled,unused-variable - """ - Register additional helpers related to authorization into Jinja global - namespace to enable them within the templates. - """ - def can_access_endpoint(endpoint, item = None): - """ - Check if currently logged-in user can access given endpoint/view. - - :param str endpoint: Name of the application endpoint. - :param item: Optional item for additional validations. - :return: ``True`` in case user can access the endpoint, ``False`` otherwise. - :rtype: bool - """ - return flask.current_app.can_access_endpoint(endpoint, item = item) - - def permission_can(permission_name): - """ - Manually check currently logged-in user for given permission. - - :param str permission_name: Name of the permission. - :return: Check result. - :rtype: bool - """ - return hawat.acl.PERMISSIONS[permission_name].can() - - def is_it_me(item): - """ - Check if given user account is mine. - """ - return item.id == flask_login.current_user.id - - return dict( - can_access_endpoint = can_access_endpoint, - permission_can = permission_can, - is_it_me = is_it_me - ) - - return app - - -def _setup_app_intl(app): - """ - Setup application`s internationalization sybsystem. - - :param hawat.base.Vial app: Hawat application to be modified. - :return: Modified Hawat application - :rtype: hawat.base.Vial - """ - app.config["BABEL_TRANSLATION_DIRECTORIES"] = "translations;" - app.config["BABEL_TRANSLATION_DIRECTORIES"] += os.path.join(app.config["MENTAT_CORE"]["__core__reporter"]["templates_dir"], "translations;") - for i in os.listdir(app.config["MENTAT_CORE"]["__core__reporter"]["event_classes_dir"]): - if os.path.isdir(os.path.join(app.config["MENTAT_CORE"]["__core__reporter"]["event_classes_dir"], i)): - app.config["BABEL_TRANSLATION_DIRECTORIES"] += os.path.join(app.config["MENTAT_CORE"]["__core__reporter"]["event_classes_dir"], i, "translations;") - - hawat.intl.BABEL.init_app(app) - app.set_resource(hawat.const.RESOURCE_BABEL, hawat.intl.BABEL) - app.cli.add_command(hawat.intl.WINTL_CLI) - app.cli.add_command(hawat.intl.RINTL_CLI) - - @app.route('/locale/<code>') - def locale(code): # pylint: disable=locally-disabled,unused-variable - """ - Application route providing users with the option of changing locale. - """ - if code not in flask.current_app.config['SUPPORTED_LOCALES']: - return flask.abort(404) - - if flask_login.current_user.is_authenticated: - flask_login.current_user.locale = code - # Make sure current user is in SQLAlchemy session. Turns out, this - # step is not necessary and current user is already in session, - # because it was fetched from database few moments ago. - #hawat.db.db_session().add(flask_login.current_user) - hawat.db.db_session().commit() - - flask.session['locale'] = code - flask_babel.refresh() - - flask.flash( - flask.Markup(flask_babel.gettext( - 'Locale was succesfully changed to <strong>%(lcln)s (%(lclc)s)</strong>.', - lclc = code, - lcln = flask.current_app.config['SUPPORTED_LOCALES'][code] - )), - hawat.const.FLASH_SUCCESS - ) - - # Redirect user back to original page. - return flask.redirect( - hawat.forms.get_redirect_target( - default_url = flask.url_for( - flask.current_app.config['HAWAT_ENDPOINT_HOME'] - ) + app_config['BABEL_TRANSLATION_DIRECTORIES'] = 'translations;' + app_config['BABEL_TRANSLATION_DIRECTORIES'] += os.path.join( + app_config['MENTAT_CORE']['__core__reporter']['templates_dir'], + 'translations;' + ) + for i in os.listdir(app_config['MENTAT_CORE']['__core__reporter']['event_classes_dir']): + if os.path.isdir(os.path.join(app_config['MENTAT_CORE']['__core__reporter']['event_classes_dir'], i)): + app_config['BABEL_TRANSLATION_DIRECTORIES'] += os.path.join( + app_config['MENTAT_CORE']['__core__reporter']['event_classes_dir'], + i, + 'translations;' ) - ) - - @app.before_request - def before_request(): # pylint: disable=locally-disabled,unused-variable - """ - Use Flask`s :py:func:`flask.Flask.before_request` hook for storing - currently selected locale and timezone to request`s session storage. - """ - if 'locale' not in flask.session: - flask.session['locale'] = hawat.intl.get_locale() - if 'timezone' not in flask.session: - flask.session['timezone'] = hawat.intl.get_timezone() - - @app.context_processor - def utility_processor(): # pylint: disable=locally-disabled,unused-variable - """ - Register additional internationalization helpers into Jinja global namespace. - """ - - return dict( - babel_get_locale = hawat.intl.get_locale, - babel_get_timezone = hawat.intl.get_timezone, - babel_format_datetime = flask_babel.format_datetime, - babel_format_timedelta = flask_babel.format_timedelta, - babel_format_decimal = flask_babel.format_decimal, - babel_format_percent = flask_babel.format_percent, - babel_format_bytes = hawat.intl.babel_format_bytes, - babel_translate_locale = hawat.intl.babel_translate_locale, - babel_language_in_locale = hawat.intl.babel_language_in_locale - ) - - return app - - -def _setup_app_menu(app): - """ - Setup default application menu skeleton. - - :param hawat.base.Vial app: Hawat application to be modified. - :return: Modified Hawat application - :rtype: hawat.base.Vial - """ - for entry in app.config[hawat.const.CFGKEY_HAWAT_MENU_SKELETON]: - app.menu_main.add_entry(**entry) - - return app - - -def _setup_app_blueprints(app): - """ - Setup application blueprints. - - :param hawat.base.Vial app: Hawat application to be modified. - :return: Modified Hawat application - :rtype: hawat.base.Vial - """ - app.register_blueprints() - - return app diff --git a/lib/hawat/base.py b/lib/hawat/base.py index 923b6c16bdc07810ad3bc2e2b7103cfd97ede3e1..f261a7f0232bf53e4cfc8ded64ec0e61fc6492a1 100644 --- a/lib/hawat/base.py +++ b/lib/hawat/base.py @@ -8,69 +8,16 @@ #------------------------------------------------------------------------------- -""" -This module contains implementations of base classes for Hawat application pluggable -modules. Since the Hawat application is based on excelent `Flask <http://flask.pocoo.org/>`__ -microframework, the modularity and extendability of the application is already -built-in as `blueprint <http://flask.pocoo.org/docs/0.12/blueprints/>`__ -feature. However this module provides customized classes for application, -blueprint and view, that provide some additional features that are out of the -scope of bare Flask microframework. - -Module contents ---------------- - -* :py:class:`Vial` -* :py:class:`VialBlueprint` -* :py:class:`HTMLMixin` -* :py:class:`AJAXMixin` - - * :py:class:`SnippetMixin` - -* :py:class:`SQLAlchemyMixin` -* :py:class:`PsycopgMixin` -* :py:class:`DecoratedView` -* :py:class:`BaseView` - - * :py:class:`FileNameView` - * :py:class:`FileIdView` - * :py:class:`RenderableView` - - * :py:class:`SimpleView` - * :py:class:`BaseSearchView` - * :py:class:`ItemListView` - * :py:class:`ItemShowView` - * :py:class:`ItemActionView` - - * :py:class:`ItemCreateView` - * :py:class:`ItemCreateForView` - * :py:class:`ItemUpdateView` - * :py:class:`ItemDeleteView` - * :py:class:`ItemChangeView` - - * :py:class:`ItemEnableView` - * :py:class:`ItemDisableView` - * :py:class:`ItemObjectRelationView` -""" - - __author__ = "Jan Mach <jan.mach@cesnet.cz>" __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea Kropáčová <andrea.kropacova@cesnet.cz>" import re -import sys -import copy import datetime -import weakref -import traceback -import sqlalchemy # # Flask related modules. # -import werkzeug.routing -import werkzeug.utils import flask import flask.app import flask.views @@ -80,118 +27,26 @@ from flask_babel import gettext # # Custom modules. # -import hawat.const -import hawat.menu +import vial.app +import vial.const +import vial.menu +import vial.errors import hawat.db -import hawat.errors -from hawat.forms import get_redirect_target, ItemActionConfirmForm +import mentat +import mentat._buildmeta import mentat.const +import mentat.services.sqlstorage import mentat.services.eventstorage -from mentat.datatype.sqldb import ItemChangeLogModel +import mentat.idea.internal +import mentat.datatype.sqldb + CRE_QNAME = re.compile(r'^([\d]+)_([a-z]{6})$') RE_UQUERY = ' AS "_mentatq\\({:d}_[^)]+\\)_"' -class VialException(Exception): - """ - Custom class for :py:class:`hawat.base.Vial` application exceptions. - """ - - -class URLParamsBuilder: - """ - Small utility class for building URL parameter dictionaries for various view - endpoints. - - .. note:: - - This class is still proof of concept and work in progress. - """ - def __init__(self, skeleton = None): - self.rules = [] - self.kwrules = {} - self.skeleton = skeleton or {} - - @staticmethod - def _add_scalar(dst, key, val): - if val is not None: - dst[key] = val - - @staticmethod - def _add_vector(dst, key, val): - if val is not None: - dst.setdefault(key, []).append(val) - - def add_rule(self, key, as_list = False, optional = False): - """ - Add new rule to URL parameter builder. - - :param str key: Name of the rule key. - :param bool as_list: Indication that the rule parameter is a list of multiple values. - :param bool optional: Indication that the rule parameter is optional. - """ - if as_list: - rule = [key, self._add_vector, True, optional] - self.rules.append(rule) - else: - rule = [key, self._add_scalar, False, optional] - self.rules.append(rule) - return self - - def add_kwrule(self, key, as_list = False, optional = False): - """ - Add new keyword rule to URL parameter builder. - - :param str key: Name of the rule key. - :param bool as_list: Indication that the rule parameter is a list of multiple values. - :param bool optional: Indication that the rule parameter is optional. - """ - if as_list: - rule = [key, self._add_vector, True, optional] - self.kwrules[key] = rule - else: - rule = [key, self._add_scalar, False, optional] - self.kwrules[key] = rule - return self - - def get_params(self, *args, **kwargs): - """ - Get URL parameters as dictionary with filled-in values. - """ - tmp = copy.deepcopy(self.skeleton) - for idx, rule in enumerate(self.rules): - try: - rule[1](tmp, rule[0], args[idx]) - except IndexError: - if not rule[3]: - raise - for key, rule in self.kwrules.items(): - if key in kwargs: - rule[1](tmp, rule[0], kwargs[key]) - return tmp - - -class VialUtils: - """ - Small utility method class to enable use of those methods both in the view - classes and in the Jinja2 templates. - """ - @staticmethod - def get_datetime_window(tiid, wtype, moment = None): - """ - Get timestamp of given type ('current', 'previous', 'next') for given time - window and optional time moment. - """ - try: - if not moment: - moment = datetime.datetime.utcnow() - return hawat.const.TIME_WINDOWS[tiid][wtype](moment) - except: # pylint: disable=locally-disabled,bare-except - return None - -class Vial(flask.Flask): +class HawatApp(vial.app.Vial): """ Custom implementation of :py:class:`flask.Flask` class. This class extends the capabilities of the base class with following additional features: @@ -200,7 +55,7 @@ class Vial(flask.Flask): 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:`hawat.base.Vial.register_blueprints` + application setup for each installation. See the :py:func:`hawat.base.HawatApp.register_blueprints` for more information on the topic. Application main menu management @@ -214,18 +69,8 @@ class Vial(flask.Flask): def __init__(self, import_name, **kwargs): super().__init__(import_name, **kwargs) - self.csrf = None - - self.menu_main = hawat.menu.Menu() - self.menu_auth = hawat.menu.Menu() - self.menu_anon = hawat.menu.Menu() - - self.sign_ins = {} - self.sign_ups = {} - self.csag = {} - self.oads = {} - self.resources = {} - self.infomailers = {} + self.csag = {} + self.oads = {} @property def mconfig(self): @@ -234,173 +79,6 @@ class Vial(flask.Flask): """ return self.config[hawat.const.CFGKEY_MENTAT_CORE] - @property - def icons(self): - """ - Application icon registry. - """ - return hawat.const.FA_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 - 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_HAWAT_BLUEPRINTS`. - * Call blueprint`s ``register_app`` method, if available, with ``self`` as only argument. - - :param hawat.base.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:`hawat.base.VialBlueprint`. This - method will call the :py:func:`hawat.base.Vial.register_blueprint` for - each blueprint, that is being registered into the application. - - :raises hawat.base.VialException: In case the factory method ``get_blueprint`` is not provided by loaded module. - """ - for name in self.config[hawat.const.CFGKEY_ENABLED_BLUEPRINTS]: - 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.path_full, 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 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_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 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_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 get_csag(self, group_name): """ Return list of all registered context search actions for given group name @@ -469,431 +147,149 @@ class Vial(flask.Flask): 'params': params_builder }) - 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) - - -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:`hawat.base.Vial.register_blueprint` method and which can - perform additional tweaking of Hawat application object. - - :param hawat.base.Vial app: Application object. - """ - return - - def register_view_class(self, view_class, route_spec): - """ - Register given view class into the internal blueprint registry. - - :param hawat.base.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 authentication decorators (if requested). - if view_class.authentication: - view_func = flask_login.login_required(view_func) - - # Apply authorization decorators (if requested). - if view_class.authorization: - for auth in view_class.authorization: - view_func = auth.require(403)(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 HTMLMixin: - """ - Mixin class enabling rendering responses as HTML. Use it in your custom view - classess based on :py:class:`hawat.base.RenderableView` to provide the - ability to render Jinja2 template files into HTML documents. - """ - - @staticmethod - def abort(status_code, message = None): # pylint: disable=locally-disabled,unused-argument - """ - Abort request processing with ``flask.abort`` function and custom status - code and optional additional message. Return response as HTML error document. - """ - flask.abort(status_code, message) - - def flash(self, message, level = 'info'): # pylint: disable=locally-disabled,no-self-use - """ - Display a one time message to the user. This implementation uses the - :py:func:`flask.flash` method. - - :param str message: Message text. - :param str level: Severity level of the flash message. - """ - flask.flash(message, level) - - def redirect(self, target_url = None, default_url = None, exclude_url = None): # pylint: disable=locally-disabled,no-self-use - """ - Redirect user to different page. This implementation uses the - :py:func:`flask.redirect` method to return valid HTTP redirection response. - - :param str target_url: Explicit redirection target, if possible. - :param str default_url: Default redirection URL to use in case it cannot be autodetected from the response. - :param str exclude_url: URL to which to never redirect (for example never redirect back to the item detail after the item deletion). - """ - return flask.redirect( - get_redirect_target(target_url, default_url, exclude_url) - ) - - def generate_response(self, view_template = None): - """ - Generate the response appropriate for this view class, in this case HTML - page. - - :param str view_template: Override internally preconfigured page template. - """ - return flask.render_template( - view_template or self.get_view_template(), - **self.response_context - ) - - -class AJAXMixin: - """ - Mixin class enabling rendering responses as JSON documents. Use it in your - custom view classess based on based on :py:class:`hawat.base.RenderableView` - to provide the ability to generate JSON responses. - """ - KW_RESP_VIEW_TITLE = 'view_title' - KW_RESP_VIEW_ICON = 'view_icon' - KW_RESP_FLASH_MESSAGES = 'flash_messages' - - @staticmethod - def abort(status_code, message = None): - """ - Abort request processing with ``flask.abort`` function and custom status - code and optional additional message. Return response as JSON document. - """ - flask.abort( - hawat.errors.api_error_response( - status_code, - message - ) - ) - - def flash(self, message, level = 'info'): # pylint: disable=locally-disabled,no-self-use - """ - Display a one time message to the user. This implementation uses the - ``flash_messages`` subkey in returned JSON document to store the messages. - - :param str message: Message text. - :param str level: Severity level of the flash message. - """ - self.response_context.\ - setdefault(self.KW_RESP_FLASH_MESSAGES, {}).\ - setdefault(level, []).\ - append(message) - - def redirect(self, target_url = None, default_url = None, exclude_url = None): - """ - Redirect user to different page. This implementation stores the redirection - target to the JSON response. - - :param str target_url: Explicit redirection target, if possible. - :param str default_url: Default redirection URL to use in case it cannot be autodetected from the response. - :param str exclude_url: URL to which to never redirect (for example never redirect back to the item detail after the item deletion). - """ - self.response_context.update( - redirect = get_redirect_target( - target_url, - default_url, - exclude_url - ) - ) - self.process_response_context() - return flask.jsonify(self.response_context) - - def process_response_context(self): - """ - Perform additional mangling with the response context before generating - the response. This method can be useful to delete some context keys, that - should not leave the server. - - :return: Possibly updated response context. - :rtype: dict - """ - self.response_context[self.KW_RESP_VIEW_TITLE] = self.get_view_title() - self.response_context[self.KW_RESP_VIEW_ICON] = self.get_view_icon() - - flashed_messages = flask.get_flashed_messages(with_categories = True) - if flashed_messages: - for category, message in flashed_messages: - self.response_context.\ - setdefault(self.KW_RESP_FLASH_MESSAGES, {}).\ - setdefault(category, []).\ - append(message) - - # Prevent certain response context keys to appear in final response. - for key in ('search_form', 'item_form'): - try: - del self.response_context[key] - except KeyError: - pass - - return self.response_context - - def generate_response(self, view_template = None): # pylint: disable=locally-disabled,unused-argument - """ - Generate the response appropriate for this view class, in this case JSON - document. - - :param str view_template: Override internally preconfigured page template. - """ - self.process_response_context() - return flask.jsonify(self.response_context) + #-------------------------------------------------------------------------- -class SnippetMixin(AJAXMixin): - """ - Mixin class enabling rendering responses as JSON documents. Use it in your - custom view classess based on based on :py:class:`hawat.base.RenderableView` - to provide the ability to generate JSON responses. - """ - KW_RESP_SNIPPETS = 'snippets' - KW_RESP_RENDER = '_render' - renders = [] - snippets = [] + def setup_app(self): + super().setup_app() - def _render_snippet(self, snippet, snippet_file = None): - if 'condition' in snippet and not snippet['condition'](self.response_context): - return + self._setup_app_eventdb() - if not 'file' in snippet: - snippet['file'] = '{mod}/spt_{rdr}_{spt}.html'.format( - mod = self.module_name, - rdr = self.response_context[self.KW_RESP_RENDER], - spt = snippet['name'] - ) - if snippet_file: - snippet['file'] = snippet_file - - self.response_context.setdefault( - self.KW_RESP_SNIPPETS, - {} - )[snippet['name']] = flask.render_template( - snippet['file'], - **self.response_context - ) - def flash(self, message, level = 'info'): # pylint: disable=locally-disabled,no-self-use - """ - Display a one time message to the user. This implementation uses the - ``flash_messages`` subkey in returned JSON document to store the messages. + def _setup_app_core(self): + super()._setup_app_core() - :param str message: Message text. - :param str level: Severity level of the flash message. - """ - self.response_context.\ - setdefault(self.KW_RESP_SNIPPETS, {}).\ - setdefault(self.KW_RESP_FLASH_MESSAGES, {}).\ - setdefault(level, []).\ - append( - flask.render_template( - 'spt_flashmessage.html', - level = level, - message = message - ) + @self.context_processor + def jinja_inject_variables(): # pylint: disable=locally-disabled,unused-variable + """ + Inject additional variables into Jinja2 global template namespace. + """ + return dict( + hawat_version = mentat.__version__, + hawat_bversion = mentat._buildmeta.__bversion__, # pylint: disable=locally-disabled,protected-access + hawat_bversion_full = mentat._buildmeta.__bversion_full__, # pylint: disable=locally-disabled,protected-access + hawat_chart_dimensions = 'height:700px', ) - def process_response_context(self): - """ - Reimplementation of :py:func:`hawat.base.AJAXMixin.process_response_context`. - """ - self.response_context[self.KW_RESP_VIEW_TITLE] = self.get_view_title() - self.response_context[self.KW_RESP_VIEW_ICON] = self.get_view_icon() - self.response_context[self.KW_RESP_RENDER] = flask.request.args.get( - 'render', - self.renders[0] - ) or self.renders[0] - if self.response_context[self.KW_RESP_RENDER] not in self.renders: - self.abort( - 400, - gettext( - 'Invalid value %(val)s for snippet rendering parameter.', - val = self.response_context[self.KW_RESP_RENDER] - ) + @self.context_processor + def jinja2_inject_functions(): # pylint: disable=locally-disabled,unused-variable,too-many-locals + + def get_csag(group): + """ + Return list of all registered context search actions under given group. + + :param str group: Name of the group. + :return: List of all registered context search actions. + :rtype: list + """ + return self.get_csag(group) + + def get_reporting_interval_name(seconds): + """ + Get a name of reporting interval for given time delta. + + :param int seconds: Time interval delta in seconds. + :return: Name of the reporting interval. + :rtype: str + """ + return mentat.const.REPORTING_INTERVALS_INV[seconds] + + def get_limit_counter(limit = None): + """ + Get fresh instance of limit counter. + """ + if not limit: + limit = flask.current_app.config['HAWAT_LIMIT_AODS'] + return vial.utils.LimitCounter(limit) + + return dict( + get_csag = get_csag, + get_reporting_interval_name = get_reporting_interval_name, + get_limit_counter = get_limit_counter, ) - flashed_messages = flask.get_flashed_messages(with_categories = True) - if flashed_messages: - for category, message in flashed_messages: - self.response_context.\ - setdefault(self.KW_RESP_SNIPPETS, {}).\ - setdefault(self.KW_RESP_FLASH_MESSAGES, {}).\ - setdefault(category, []).\ - append( - flask.render_template( - 'spt_flashmessage.html', - level = category, - message = message - ) - ) - - for snippet in self.snippets: - self._render_snippet(snippet) - - # Prevent certain response context keys to appear in final response. - for key in ('search_form', 'item_form'): - try: - del self.response_context[key] - except KeyError: - pass - - return self.response_context - - -class SQLAlchemyMixin: - """ - Mixin class providing generic interface for interacting with SQL database - backend through SQLAlchemy library. - """ - - @property - def dbmodel(self): - """ - This property must be implemented in each subclass to return reference to - appropriate model class based on *SQLAlchemy* declarative base. - """ - raise NotImplementedError() - - @property - def search_by(self): - """ - Return model`s attribute (column) according to which to search for a single item. - """ - return self.dbmodel.id - - @property - def dbsession(self): - """ - This property contains the reference to current *SQLAlchemy* database session. - """ - return hawat.db.db_get().session - - def dbquery(self, dbmodel = None): - """ - This property contains the reference to *SQLAlchemy* query object appropriate - for particular ``dbmodel`` property. - """ - return self.dbsession.query(dbmodel or self.dbmodel) - - def dbcolumn_min(self, dbcolumn): - """ - Find and return the minimal value for given table column. - """ - result = self.dbsession.query(sqlalchemy.func.min(dbcolumn)).one_or_none() - if result: - return result[0] - return None - - def dbcolumn_max(self, dbcolumn): - """ - Find and return the maximal value for given table column. - """ - result = self.dbsession.query(sqlalchemy.func.max(dbcolumn)).one_or_none() - if result: - return result[0] - return None - - @staticmethod - def build_query(query, model, form_args): # pylint: disable=locally-disabled,unused-argument - """ - *Hook method*. Modify given query according to the given arguments. - """ - return query + class HawatJSONEncoder(flask.json.JSONEncoder): + """ + Custom JSON encoder for converting anything into JSON strings. + """ + def default(self, obj): # pylint: disable=locally-disabled,method-hidden,arguments-differ + try: + if isinstance(obj, mentat.idea.internal.Idea): + return mentat.idea.jsondict.Idea(obj).data + except: # pylint: disable=locally-disabled,bare-except + pass + 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) - def fetch(self, item_id): - """ - Fetch item with given primary identifier from the database. - """ - return self.dbquery().filter(self.search_by == item_id).first() + self.json_encoder = HawatJSONEncoder - def search(self, form_args): - """ - Perform actual search with given query. - """ - query = self.build_query(self.dbquery(), self.dbmodel, form_args) + return self - # Adjust the query according to the paging parameters. - if 'limit' in form_args and form_args['limit']: - query = query.limit(int(form_args['limit'])) - if 'page' in form_args and form_args['page'] and int(form_args['page']) > 1: - query = query.offset((int(form_args['page']) - 1) * int(form_args['limit'])) + def _setup_app_db(self): + super()._setup_app_db() + + class StorageService: # pylint: disable=locally-disabled,too-few-public-methods + """ + This is a thin proxy class, that can be used in place of :py:class:`mentat.services.sqlstorage.StorageService`. + This is necessary for certain services like :py:mod:`mentat.services.whois`, that require + some access to database storage service and are hardcoded to use :py:class:`mentat.services.sqlstorage.StorageService`. + This is necessary when using the services from Flask framework, because there + is another storage service management feature in place using the py:mod:`flask_sqlalchemy` + module. + """ + @property + def session(self): + """ + Thin proxy property for retrieving reference to current database session. + """ + return vial.db.db_session() + + + class StorageServiceManager: # pylint: disable=locally-disabled,too-few-public-methods + """ + This is a thin proxy class, that can be used in place of :py:class:`mentat.services.sqlstorage.StorageServiceManager`. + This is necessary for certain services like :py:mod:`mentat.services.whois`, that require + some access to database storage service manager and are hardcoded to use :py:class:`mentat.services.sqlstorage.StorageServiceManager`. + This is necessary when using the services from Flask framework, because there + is another storage service management feature in place using the py:mod:`flask_sqlalchemy` + module. + """ + @staticmethod + def service(): + """ + Thin proxy property for retrieving reference to current database storage + service. + """ + return StorageService() + + + mentat.services.sqlstorage.set_manager(StorageServiceManager()) + + def _setup_app_eventdb(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 + """ + hawat.events.db_init(self) + self.logger.info("Connected to event database") - return query.all() + return self class PsycopgMixin: @@ -986,1871 +382,3 @@ class PsycopgMixin: sqlquery_name = query_name ) return items - - -#------------------------------------------------------------------------------- - - -class DecoratedView: - """ - Wrapper class for classical decorated view functions. - """ - def __init__(self, view_function): - self.view_function = view_function - - def get_view_name(self): - """Simple adapter method to enable support of classical decorated views.""" - return self.view_function.__name__ - - def get_view_endpoint(self): - """Simple adapter method to enable support of classical decorated views.""" - return self.get_view_name() - - def get_view_icon(self): - """Simple adapter method to enable support of classical decorated views.""" - return 'view-{}'.format(self.get_view_name()) - - -class BaseView(flask.views.View): - """ - Base class for all custom Vial application views. - """ - - module_ref = None - """ - Weak reference to parent module of this view. - """ - - module_name = None - """ - Name of the parent module (blueprint). Will be set up during the process - of registering the view into the blueprint in :py:func:`hawat.base.VialBlueprint.register_view_class`. - """ - - authentication = False - """ - Similar to the ``decorators`` mechanism in Flask pluggable views, you may use - this class variable to specify, that the view is protected by authentication. - During the process of registering the view into the blueprint in - :py:func:`hawat.base.VialBlueprint.register_view_class` the view will be - automatically decorated with :py:func:`flask_login.login_required` decorator. - - The advantage of using this in favor of ``decorators`` is that the application - menu can automatically hide/show items inaccessible to current user. - - This is a scalar variable that must contain boolean ``True`` or ``False``. - """ - - authorization = () - """ - Similar to the ``decorators`` mechanism in Flask pluggable views, you may use - this class variable to specify, that the view is protected by authorization. - During the process of registering the view into the blueprint in - :py:func:`hawat.base.VialBlueprint.register_view_class` the view will be - automatically decorated with given authorization decorators. - - The advantage of using this in favor of ``decorators`` is that the application - menu can automatically hide/show items inaccessible to current user. - - This is a list variable that must contain list of desired decorators. - """ - - url_params_unsupported = () - """ - List of URL parameters, that are not supported by this view and should be removed - before generating the URL. - """ - - @classmethod - def get_view_name(cls): - """ - Return unique name for the view. Name must be unique in the namespace of - parent blueprint/module and should contain only characters ``[a-z0-9]``. - It will be used for generating endpoint name for the view. - - *This method does not have any default implementation and must be overridden - by a subclass.* - - :return: Name for the view. - :rtype: str - """ - raise NotImplementedError() - - @classmethod - def get_view_endpoint(cls): - """ - Return name of the routing endpoint for the view within the whole application. - - Default implementation generates the endpoint name by concatenating the - module name and view name. - - :return: Routing endpoint for the view within the whole application. - :rtype: str - """ - return '{}.{}'.format(cls.module_name, cls.get_view_name()) - - @classmethod - def get_view_url(cls, **kwargs): - """ - Return view URL. - - :param dict kwargs: Optional parameters. - :return: URL for the view. - :rtype: str - """ - # Filter out unsupported URL parameters. - params = { - k: v for k, v in filter( - lambda x: x[0] not in cls.url_params_unsupported, - kwargs.items() - ) - } - return flask.url_for( - cls.get_view_endpoint(), - **params - ) - - @classmethod - def get_view_icon(cls): - """ - Return menu entry icon name for the view. Given name will be used as index - to built-in icon registry. - - Default implementation generates the icon name by concatenating the prefix - ``module-`` with module name. - - :return: View icon. - :rtype: str - """ - return 'module-{}'.format(cls.module_name) - - @classmethod - def get_view_title(cls, **kwargs): - """ - Return title for the view, that will be displayed in the ``title`` tag of - HTML ``head`` element and also as the content of page header in ``h2`` tag. - - Default implementation returns the return value of :py:func:`hawat.base.BaseView.get_menu_title` - method by default. - - :param dict kwargs: Optional parameters. - :return: Title for the view. - :rtype: str - """ - raise NotImplementedError() - - @classmethod - def get_menu_title(cls, **kwargs): - """ - Return menu entry title for the view. - - Default implementation returns the return value of :py:func:`hawat.base.BaseView.get_view_title` - method by default. - - :param dict kwargs: Optional parameters. - :return: Menu entry title for the view. - :rtype: str - """ - return cls.get_view_title(**kwargs) - - @classmethod - def get_menu_legend(cls, **kwargs): - """ - Return menu entry legend for the view (menu entry hover tooltip). - - Default implementation returns the return value of :py:func:`hawat.base.BaseView.get_menu_title` - method by default. - - :param dict kwargs: Optional parameters. - :return: Menu entry legend for the view. - :rtype: str - """ - return cls.get_menu_title(**kwargs) - - #--------------------------------------------------------------------------- - - @staticmethod - def can_access_endpoint(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 - """ - return flask.current_app.can_access_endpoint(endpoint, **kwargs) - - @staticmethod - def has_endpoint(endpoint): - """ - Check if given routing endpoint is available within the application. - - :param str endpoint: Application routing endpoint. - :return: ``True`` in case endpoint exists, ``False`` otherwise. - :rtype: bool - """ - return flask.current_app.has_endpoint(endpoint) - - @staticmethod - def get_endpoint_class(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 - """ - return flask.current_app.get_endpoint_class(endpoint, quiet) - - #--------------------------------------------------------------------------- - - @property - def logger(self): - """ - Return current application`s logger object. - """ - return flask.current_app.logger - - -class FileNameView(BaseView): - """ - Base class for direct file access views. These views can be used to access - and serve files from arbitrary filesystem directories (that are accessible to - application process). This can be very usefull for serving files like charts, - that are periodically generated into configurable and changeable location. - """ - - @classmethod - def get_view_icon(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_icon`.""" - return 'action-download' - - @classmethod - def get_directory_path(cls): - """ - Return absolute path to the directory, that will be used as a base path - for serving files. - - *This method does not have any default implementation and must be overridden - by a subclass.* - - :return: Absolute path to the directory for serving files. - :rtype: str - """ - raise NotImplementedError() - - @classmethod - def validate_filename(cls, filename): - """ - Validate given file name to prevent user from accessing restricted files. - - In default implementation all files pass the validation. - - :param str filename: Name of the file to be validated/filtered. - :return: ``True`` in case file name is allowed, ``False`` otherwise. - :rtype: bool - """ - return bool(filename) - - def dispatch_request(self, filename): # 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. - """ - if not self.validate_filename(filename): - flask.abort(400) - - self.logger.info( - "Serving file '{}' from directory '{}'.".format( - filename, - self.get_directory_path() - ) - ) - return flask.send_from_directory( - self.get_directory_path(), - filename, - as_attachment = True - ) - - -class FileIdView(BaseView): - """ - Base class for indirrect file access views. These views can be used to access - and serve files from arbitrary filesystem directories (that are accessible to - application process). This can be very usefull for serving files like charts, - that are periodically generated into configurable and changeable location. - The difference between this view class and :py:class:`FileNameView` is, - that is this case some kind of identifier is used to access the file and - provided class method is responsible for translating this identifier into - real file name. - """ - - @classmethod - def get_view_icon(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_icon`.""" - return 'action-download' - - @classmethod - def get_directory_path(cls, fileid, filetype): - """ - This method must return absolute path to the directory, that will be - used as a base path for serving files. Parameter ``fileid`` may be used - internally to further customize the base directory, for example when - serving some files places into subdirectories based on some part of the - file name (for example to reduce total number of files in base directory). - - *This method does not have any default implementation and must be overridden - by a subclass.* - - :param str fileid: Identifier of the requested file. - :param str filetype: Type of the requested file. - :return: Absolute path to the directory for serving files. - :rtype: str - """ - raise NotImplementedError() - - @classmethod - def get_filename(cls, fileid, filetype): - """ - This method must return actual name of the file based on given identifier - and type. - - *This method does not have any default implementation and must be overridden - by a subclass.* - - :param str fileid: Identifier of the requested file. - :param str filetype: Type of the requested file. - :return: Translated name of the file. - :rtype: str - """ - raise NotImplementedError() - - def dispatch_request(self, fileid, filetype): # 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. - """ - basedirpath = self.get_directory_path(fileid, filetype) - filename = self.get_filename(fileid, filetype) - if not basedirpath or not filename: - flask.abort(400) - - self.logger.info( - "Serving file '{}' from directory '{}'.".format( - filename, - basedirpath - ) - ) - return flask.send_from_directory( - basedirpath, - filename, - as_attachment = True - ) - - -class RenderableView(BaseView): # pylint: disable=locally-disabled,abstract-method - """ - Base class for all views, that are rendering content based on Jinja2 templates - or returning JSON/XML data. - """ - - def __init__(self): - self.response_context = {} - - def mark_time(self, ident, tag = 'default', label = 'Time mark', log = False): - """ - Mark current time with given identifier and label for further analysis. - This method can be usefull for measuring durations of various operations. - """ - mark = [datetime.datetime.utcnow(), ident, tag, label] - marks = self.response_context.setdefault('time_marks', []) - marks.append(mark) - - if log: - if len(marks) <= 1: - self.logger.info( - 'Mark {}:{} ({})'.format(*mark[1:]) - ) - else: - self.logger.info( - 'Mark {}:{} ({});delta={};delta0={}'.format( - *mark[1:], - (marks[-1][0]-marks[-2][0]).__str__(), # Time delta from last mark. - (marks[-1][0]-marks[0][0]).__str__() # Time delta from first mark. - ) - ) - - @classmethod - def get_view_template(cls): - """ - Return Jinja2 template file that should be used for rendering the view - content. This default implementation works only in case the view class - was properly registered into the parent blueprint/module with - :py:func:`hawat.base.VialBlueprint.register_view_class` method. - - :return: Jinja2 template file to use to render the view. - :rtype: str - """ - if cls.module_name: - return '{}/{}.html'.format( - cls.module_name, - cls.get_view_name() - ) - raise RuntimeError("Unable to guess default view template, because module name was not yet set.") - - def do_before_response(self, **kwargs): # pylint: disable=locally-disabled,no-self-use,unused-argument - """ - This method will be called just before generating the response. By providing - some meaningfull implementation you can use it for some simple item and - response context mangling tasks. - - :param kwargs: Custom additional arguments. - """ - - def generate_response(self): - """ - Generate the appropriate response from given response context. - - :param dict response_context: Response context as a dictionary - """ - raise NotImplementedError() - - @staticmethod - def abort(status_code, message = None): - """ - Abort request processing with HTTP status code. - """ - raise NotImplementedError() - - def flash(self, message, level = 'info'): - """ - Flash information to the user. - """ - raise NotImplementedError() - - def redirect(self, default_url = None, exclude_url = None): - """ - Redirect user to different location. - """ - raise NotImplementedError() - - -class SimpleView(RenderableView): # pylint: disable=locally-disabled,abstract-method - """ - Base class for simple views. These are the most, well, simple views, that are - rendering single template file or directly returning some JSON/XML data without - any user parameters. - - In most use cases, it should be enough to just enhance the default implementation - of :py:func:`hawat.base.RenderableView.get_response_context` to inject - some additional variables into the template. - """ - - 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. - """ - self.do_before_response() - return self.generate_response() - - -class BaseSearchView(RenderableView, VialUtils): - """ - Base class for search views. - """ - @classmethod - def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" - return 'search' - - @classmethod - def get_view_icon(cls): - """*Implementation* of :py:func:`mydojo.base.BaseView.get_view_name`.""" - return 'action-search' - - #--------------------------------------------------------------------------- - - @classmethod - def get_quicksearch_by_time(cls): - """ - Get default list of 'by time' quickseach items. - """ - quicksearch_list = [] - for item in ( - [ '1h', gettext('Search for last hour')], - [ '2h', gettext('Search for last 2 hours')], - [ '3h', gettext('Search for last 3 hours')], - [ '4h', gettext('Search for last 4 hours')], - [ '6h', gettext('Search for last 6 hours')], - ['12h', gettext('Search for last 12 hours')], - [ '1d', gettext('Search for last day')], - [ '2d', gettext('Search for last 2 days')], - [ '3d', gettext('Search for last 3 days')], - [ '1w', gettext('Search for last week')], - [ '2w', gettext('Search for last 2 weeks')], - [ '4w', gettext('Search for last 4 weeks')], - ['12w', gettext('Search for last 12 weeks')], - - [ 'td', gettext('Search for today')], - [ 'tw', gettext('Search for this week')], - [ 'tm', gettext('Search for this month')], - [ 'ty', gettext('Search for this year')], - ): - try: - dt_from = cls.get_datetime_window( - item[0], - 'current' - ) - dt_to = cls.get_datetime_window( - item[0], - 'next', - dt_from - ) - quicksearch_list.append( - { - 'label': item[1], - 'params': { - 'dt_from': dt_from.isoformat( - sep = ' ' - ), - 'dt_to': dt_to.isoformat( - sep = ' ' - ), - 'tiid': item[0], - 'submit': gettext('Search') - } - } - ) - except: # pylint: disable=locally-disabled,bare-except - pass - - return quicksearch_list - - @staticmethod - def get_search_form(request_args): - """ - Must return instance of :py:mod:`flask_wtf.FlaskForm` appropriate for - searching given type of items. - """ - raise NotImplementedError() - - @staticmethod - def get_query_parameters(form, request_args): - """ - Get query parameters by comparing contents of processed form data and - original request arguments. Result of this method can be used for generating - modified URLs back to current request. One of the use cases is the result - pager/paginator. - """ - params = {} - for arg in request_args: - if getattr(form, arg, None) and arg in request_args: - # Handle multivalue request arguments separately - # Resources: - # http://flask.pocoo.org/docs/1.0/api/#flask.Request.args - # http://werkzeug.pocoo.org/docs/0.14/datastructures/#werkzeug.datastructures.MultiDict - try: - if form.is_multivalue(arg): - params[arg] = request_args.getlist(arg) - else: - params[arg] = request_args[arg] - except AttributeError: - params[arg] = request_args[arg] - return params - - def search(self, form_args): - """ - Perform actual search with given query. - """ - raise NotImplementedError() - - #--------------------------------------------------------------------------- - - @classmethod - def get_breadcrumbs_menu(cls): - """ - Get breadcrumbs menu. - """ - breadcrumbs_menu = hawat.menu.Menu() - breadcrumbs_menu.add_entry( - 'endpoint', - 'home', - endpoint = flask.current_app.config['HAWAT_ENDPOINT_HOME'] - ) - breadcrumbs_menu.add_entry( - 'endpoint', - cls.get_view_name(), - endpoint = '{}.{}'.format( - cls.module_name, - cls.get_view_name() - ) - ) - return breadcrumbs_menu - - @classmethod - def get_action_menu(cls): - """ - Get action menu for all items. - """ - return None - - @classmethod - def get_context_action_menu(cls): - """*Implementation* of :py:func:`hawat.base.ItemListView.get_context_action_menu`.""" - context_action_menu = hawat.menu.Menu() - context_action_menu.add_entry( - 'endpoint', - 'show', - endpoint = '{}.show'.format(cls.module_name), - hidetitle = True - ) - return context_action_menu - - #--------------------------------------------------------------------------- - - def do_before_search(self, form_data): # pylint: disable=locally-disabled,no-self-use,unused-argument - """ - This hook method will be called before search attempt. - """ - - def do_after_search(self, items): # pylint: disable=locally-disabled,no-self-use,unused-argument - """ - This hook method will be called after successfull search. - """ - - 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. - """ - form = self.get_search_form(flask.request.args) - flask.g.search_form = form - - if hawat.const.FORM_ACTION_SUBMIT in flask.request.args: - if form.validate(): - form_data = form.data - - self.mark_time( - 'preprocess_begin', - tag = 'search', - label = 'Begin preprocessing for "{}"'.format(flask.request.full_path), - log = True - ) - self.do_before_search(form_data) - self.mark_time( - 'preprocess_end', - tag = 'search', - label = 'Done preprocessing for "{}"'.format(flask.request.full_path), - log = True - ) - - try: - self.mark_time( - 'search_begin', - tag = 'search', - label = 'Begin searching for "{}"'.format(flask.request.full_path), - log = True - ) - items = self.search(form_data) - self.mark_time( - 'search_end', - tag = 'search', - label = 'Done searching for "{}", items found: {}'.format(flask.request.full_path, len(items)), - log = True - ) - - self.response_context.update( - searched = True, - items = items, - items_count = len(items), - form_data = form_data - ) - - # Not all search forms support result paging. - if 'page' in form_data: - self.response_context.update( - pager_index_low = ((form_data['page'] - 1) * form_data['limit']) + 1, - pager_index_high = ((form_data['page'] - 1) * form_data['limit']) + len(items), - pager_index_limit = ((form_data['page'] - 1) * form_data['limit']) + form_data['limit'] - ) - - self.mark_time( - 'postprocess_begin', - tag = 'search', - label = 'Begin postprocessing for "{}"'.format(flask.request.full_path), - log = True - ) - self.do_after_search(items) - self.mark_time( - 'postprocess_end', - tag = 'search', - label = 'Done postprocessing for "{}"'.format(flask.request.full_path), - log = True - ) - - except Exception as err: # pylint: disable=locally-disabled,broad-except - match = re.match('invalid IP4R value: "([^"]+)"', str(err)) - if match: - self.flash( - flask.Markup( - gettext( - 'Invalid address value <strong>%(address)s</strong> in search form.', - address = flask.escape(str(match.group(1))) - ) - ), - hawat.const.FLASH_FAILURE - ) - else: - raise - - else: - self.response_context.update( - form_errors = [(field_name, err) for field_name, error_messages in form.errors.items() for err in error_messages] - ) - - self.response_context.update( - query_params = self.get_query_parameters( - form, - flask.request.args - ), - search_widget_item_limit = 3 - ) - self.do_before_response() - return self.generate_response() - - -class ItemListView(RenderableView): # pylint: disable=locally-disabled,abstract-method - """ - Base class for item *list* views. These views provide quick and simple access - to lists of all objects. - """ - - @classmethod - def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" - return 'list' - - @classmethod - def get_breadcrumbs_menu(cls): - """ - Get breadcrumbs menu. - """ - action_menu = hawat.menu.Menu() - action_menu.add_entry( - 'endpoint', - 'home', - endpoint = flask.current_app.config['HAWAT_ENDPOINT_HOME'] - ) - action_menu.add_entry( - 'endpoint', - 'list', - endpoint = '{}.list'.format(cls.module_name) - ) - return action_menu - - @classmethod - def get_action_menu(cls): - """ - Get action menu for all listed items. - """ - return None - - @classmethod - def get_context_action_menu(cls): - """ - Get context action menu for particular single item. - """ - return None - - def search(self, form_args): - """ - Perform actual search with given form arguments. - """ - raise NotImplementedError() - - 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. - - List of all items will be retrieved from database and injected into template - to be displayed to the user. - """ - items = self.search({}) - - self.response_context.update( - items = items - ) - - self.do_before_response() - return self.generate_response() - - -class ItemShowView(RenderableView): # pylint: disable=locally-disabled,abstract-method - """ - Base class for item *show* views. These views expect unique item identifier - as parameter and are supposed to display specific information about single - item. - """ - - @classmethod - def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" - return 'show' - - @classmethod - def get_view_icon(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_icon`.""" - return 'action-show' - - @classmethod - def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" - return gettext('Show') - - @classmethod - def get_view_url(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_url`.""" - return flask.url_for( - cls.get_view_endpoint(), - item_id = kwargs['item'].get_id() - ) - - @classmethod - def get_menu_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" - return gettext('Show') - - @classmethod - def authorize_item_action(cls, **kwargs): # pylint: disable=locally-disabled,unused-argument - """ - Perform access authorization for current user to particular item. - """ - return True - - @classmethod - def get_action_menu(cls): # pylint: disable=locally-disabled,unused-argument - """ - Get action menu for particular item. - """ - return None - - @classmethod - def get_breadcrumbs_menu(cls): # pylint: disable=locally-disabled,unused-argument - """ - Get breadcrumbs menu. - """ - action_menu = hawat.menu.Menu() - action_menu.add_entry( - 'endpoint', - 'home', - endpoint = flask.current_app.config['HAWAT_ENDPOINT_HOME'] - ) - action_menu.add_entry( - 'endpoint', - 'list', - endpoint = '{}.list'.format(cls.module_name), - paramlist = [] - ) - action_menu.add_entry( - 'endpoint', - 'show', - endpoint = '{}.show'.format(cls.module_name) - ) - return action_menu - - def fetch(self, item_id): - """ - Fetch item with given ID. - """ - raise NotImplementedError() - - def dispatch_request(self, item_id): # 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 = self.fetch(item_id) - if not item: - self.abort(404) - - if not self.authorize_item_action(item = item): - self.abort(403) - - self.response_context.update( - item_id = item_id, - item = item, - search_widget_item_limit = 100 - ) - - self.do_before_response() - return self.generate_response() - - -class ItemActionView(RenderableView): # pylint: disable=locally-disabled,abstract-method - """ - Base class for item action views. These views perform various actions - (create/update/delete) with given item class. - """ - @classmethod - def get_view_icon(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_icon`.""" - return 'action-{}'.format( - cls.get_view_name().replace('_', '-') - ) - - @classmethod - def get_view_url(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_url`.""" - return flask.url_for( - cls.get_view_endpoint(), - item_id = kwargs['item'].get_id() - ) - - @classmethod - def get_view_template(cls): - """*Implementation* of :py:func:`hawat.base.RenderableView.get_view_template`.""" - return 'form_{}.html'.format( - cls.get_view_name().replace('-', '_') - ) - - @staticmethod - def get_message_success(**kwargs): - """ - *Hook method*. Must return text for flash message in case of action *success*. - The text may contain HTML characters and will be passed to :py:class:`flask.Markup` - before being used, so to certain extend you may emphasize and customize the output. - """ - raise NotImplementedError() - - @staticmethod - def get_message_failure(**kwargs): - """ - *Hook method*. Must return text for flash message in case of action *failure*. - The text may contain HTML characters and will be passed to :py:class:`flask.Markup` - before being used, so to certain extend you may emphasize and customize the output. - """ - raise NotImplementedError() - - @staticmethod - def get_message_cancel(**kwargs): - """ - *Hook method*. Must return text for flash message in case of action *cancel*. - The text may contain HTML characters and will be passed to :py:class:`flask.Markup` - before being used, so to certain extend you may emphasize and customize the output. - """ - raise NotImplementedError() - - def get_url_next(self): - """ - *Hook method*. Must return URL for redirection after action *success*. In - most cases there should be call for :py:func:`flask.url_for` function - somewhere in this method. - """ - try: - return flask.url_for( - '{}.{}'.format(self.module_name, 'list') - ) - except werkzeug.routing.BuildError: - return flask.url_for( - flask.current_app.config['HAWAT_ENDPOINT_HOME'] - ) - - def check_action_cancel(self, form, **kwargs): - """ - Check the form for *cancel* button press and cancel the action. - """ - if getattr(form, hawat.const.FORM_ACTION_CANCEL).data: - self.flash( - flask.Markup(self.get_message_cancel(**kwargs)), - hawat.const.FLASH_INFO - ) - return self.redirect( - default_url = self.get_url_next() - ) - - return None - - def do_before_action(self, item): # pylint: disable=locally-disabled,no-self-use,unused-argument - """ - *Hook method*. Will be called before any action handling tasks. - """ - - def do_after_action(self, item): # pylint: disable=locally-disabled,no-self-use,unused-argument - """ - *Hook method*. Will be called after successfull action handling tasks. - """ - - @classmethod - def authorize_item_action(cls, **kwargs): # pylint: disable=locally-disabled,unused-argument - """ - Perform access authorization for current user to particular item. - """ - return True - - @property - def dbsession(self): - """ - This property contains the reference to current *SQLAlchemy* database session. - """ - raise NotImplementedError() - - @property - def dbmodel(self): - """ - This property must be implemented in each subclass to - return reference to appropriate model class based on *SQLAlchemy* declarative - base. - """ - raise NotImplementedError() - - def fetch(self, item_id, fetch_by = None): - """ - Perform actual search with given query. - """ - raise NotImplementedError() - - def changelog_log(self, item, json_state_before = '', json_state_after = ''): - """ - Log item action into changelog. One of the method arguments is permitted - to be left out. This enables logging create and delete actions. - - :param mentat.datatype.sqldb.MODEL item: Item that is being changed. - :param str json_state_before: JSON representation of item state before action. - :param str json_state_after: JSON representation of item state after action. - """ - if not json_state_before and not json_state_after: - raise ValueError("Invalid use of changelog_log() method, both of the arguments are null.") - - chlog = ItemChangeLogModel( - author = flask_login.current_user._get_current_object(), # pylint: disable=locally-disabled,protected-access - model = item.__class__.__name__, - model_id = item.id, - endpoint = self.get_view_endpoint(), - module = self.module_name, - operation = self.get_view_name(), - before = json_state_before, - after = json_state_after - ) - chlog.calculate_diff() - self.dbsession.add(chlog) - self.dbsession.commit() - - -class ItemCreateView(ItemActionView): # pylint: disable=locally-disabled,abstract-method - """ - Base class for item *create* action views. These views create new items in - database. - """ - - @classmethod - def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" - return 'create' - - @classmethod - def get_view_template(cls): - """ - Return Jinja2 template file that should be used for rendering the view - content. This default implementation works only in case the view class - was properly registered into the parent blueprint/module with - :py:func:`hawat.base.VialBlueprint.register_view_class` method. - - :return: Title for the view. - :rtype: str - """ - if cls.module_name: - return '{}/creatupdate.html'.format(cls.module_name) - raise RuntimeError("Unable to guess default view template, because module name was not yet set.") - - @classmethod - def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" - return gettext('Create') - - @classmethod - def get_view_url(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_url`.""" - return flask.url_for(cls.get_view_endpoint()) - - @classmethod - def get_menu_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" - return gettext('Create') - - @staticmethod - def get_item_form(): - """ - *Hook method*. Must return instance of :py:mod:`flask_wtf.FlaskForm` - appropriate for given item class. - """ - raise NotImplementedError() - - 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. - - This method will attempt to validate the submitted form and create new - instance of appropriate item from form data and finally store the item - into the database. - """ - if not self.authorize_item_action(): - self.abort(403) - - item = self.dbmodel() - - form = self.get_item_form() - - cancel_response = self.check_action_cancel(form) - if cancel_response: - return cancel_response - - if form.validate_on_submit(): - form_data = form.data - form.populate_obj(item) - - self.do_before_action(item) - - if form_data[hawat.const.FORM_ACTION_SUBMIT]: - try: - self.dbsession.add(item) - self.dbsession.commit() - self.do_after_action(item) - - # Log the item creation into changelog. - self.changelog_log(item, '', item.to_json()) - - self.flash( - flask.Markup(self.get_message_success(item = item)), - hawat.const.FLASH_SUCCESS - ) - return self.redirect(default_url = self.get_url_next()) - - except Exception: # pylint: disable=locally-disabled,broad-except - self.dbsession.rollback() - self.flash( - flask.Markup(self.get_message_failure()), - hawat.const.FLASH_FAILURE - ) - flask.current_app.log_exception_with_label( - traceback.TracebackException(*sys.exc_info()), - self.get_message_failure() - ) - return self.redirect(default_url = self.get_url_next()) - - self.response_context.update( - action_name = gettext('Create'), - form_url = flask.url_for(self.get_view_endpoint()), - form = form, - item_action = hawat.const.ACTION_ITEM_CREATE, - item = item - ) - - self.do_before_response() - return self.generate_response() - - -class ItemCreateForView(ItemActionView): # pylint: disable=locally-disabled,abstract-method - """ - Base class for item *createfor* action views. These views differ a little bit - from *create* action views. They are used to create new items within database, - but only for particular defined parent item. One example use case is creating - network records for particular abuse group. - """ - - @classmethod - def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" - return 'createfor' - - @classmethod - def get_view_icon(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_icon`.""" - return 'module-{}'.format(cls.module_name) - - @classmethod - def get_view_template(cls): - """ - Return Jinja2 template file that should be used for rendering the view - content. This default implementation works only in case the view class - was properly registered into the parent blueprint/module with - :py:func:`hawat.base.VialBlueprint.register_view_class` method. - - :return: Title for the view. - :rtype: str - """ - if cls.module_name: - return '{}/creatupdate.html'.format(cls.module_name) - raise RuntimeError("Unable to guess default view template, because module name was not yet set.") - - @property - def dbmodel_par(self): - """ - *Hook property*. This property must be implemented in each subclass to - return reference to appropriate model class for parent objects and that - is based on *SQLAlchemy* declarative base. - """ - raise NotImplementedError() - - @property - def dbquery_par(self): - """ - This property contains the reference to *SQLAlchemy* query object appropriate - for particular ``dbmodel_par`` property. - """ - return self.dbsession.query(self.dbmodel_par) - - @staticmethod - def get_item_form(): - """ - *Hook method*. Must return instance of :py:mod:`flask_wtf.FlaskForm` - appropriate for given item class. - """ - raise NotImplementedError() - - @staticmethod - def add_parent_to_item(item, parent): - """ - *Hook method*. Use given parent object for given item object. The actual - operation to realize this relationship is highly dependent on current - circumstance. It is up to the developer to perform correct set of actions - to implement parent - child relationship for particular object types. - """ - raise NotImplementedError() - - def dispatch_request(self, parent_id): # 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. - - This method will attempt to validate the submitted form and create new - instance of appropriate item from form data and finally store the item - into the database. - """ - parent = self.dbquery_par.filter(self.dbmodel_par.id == parent_id).one_or_none() - if not parent: - self.abort(404) - - if not self.authorize_item_action(item = parent): - self.abort(403) - - self.response_context.update( - parent_id = parent_id, - parent = parent - ) - - item = self.dbmodel() - form = self.get_item_form() - - cancel_response = self.check_action_cancel(form, parent = parent) - if cancel_response: - return cancel_response - - if form.validate_on_submit(): - form_data = form.data - form.populate_obj(item) - - self.do_before_action(item) - self.add_parent_to_item(item, parent) - - if form_data[hawat.const.FORM_ACTION_SUBMIT]: - try: - self.dbsession.add(item) - self.dbsession.commit() - self.do_after_action(item) - - # Log the item creation into changelog. - self.changelog_log(item, '', item.to_json()) - - self.flash( - flask.Markup(self.get_message_success(item = item, parent = parent)), - hawat.const.FLASH_SUCCESS - ) - return self.redirect(default_url = self.get_url_next()) - - except Exception: # pylint: disable=locally-disabled,broad-except - self.dbsession.rollback() - self.flash( - flask.Markup(self.get_message_failure(parent = parent)), - hawat.const.FLASH_FAILURE - ) - flask.current_app.log_exception_with_label( - traceback.TracebackException(*sys.exc_info()), - self.get_message_failure(parent = parent) - ) - return self.redirect(default_url = self.get_url_next()) - - self.response_context.update( - action_name = gettext('Create'), - form_url = flask.url_for('{}.{}'.format(self.module_name, self.get_view_name()), parent_id = parent_id), - form = form, - item_action = hawat.const.ACTION_ITEM_CREATEFOR, - item = item - ) - - self.do_before_response() - return self.generate_response() - - -class ItemUpdateView(ItemActionView): # pylint: disable=locally-disabled,abstract-method - """ - Base class for item *update* action views. These views update existing items - in database. - """ - - @classmethod - def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" - return 'update' - - @classmethod - def get_view_template(cls): - """ - Return Jinja2 template file that should be used for rendering the view - content. This default implementation works only in case the view class - was properly registered into the parent blueprint/module with - :py:func:`hawat.base.VialBlueprint.register_view_class` method. - - :return: Title for the view. - :rtype: str - """ - if cls.module_name: - return '{}/creatupdate.html'.format(cls.module_name) - raise RuntimeError("Unable to guess default view template, because module name was not yet set.") - - @classmethod - def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" - return gettext('Update') - - @classmethod - def get_menu_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" - return gettext('Update') - - @staticmethod - def get_item_form(item): - """ - *Hook method*. Must return instance of :py:mod:`flask_wtf.FlaskForm` - appropriate for given item class. - """ - raise NotImplementedError() - - def dispatch_request(self, item_id): # 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. - - This method will attempt to validate the submitted form and update the - instance of appropriate item from form data and finally store the item - back into the database. - """ - item = self.fetch(item_id) - if not item: - self.abort(404) - - if not self.authorize_item_action(item = item): - self.abort(403) - - self.dbsession.add(item) - - form = self.get_item_form(item) - - cancel_response = self.check_action_cancel(form, item = item) - if cancel_response: - return cancel_response - - item_json_before = item.to_json() - - if form.validate_on_submit(): - form_data = form.data - form.populate_obj(item) - - self.do_before_action(item) - - if form_data[hawat.const.FORM_ACTION_SUBMIT]: - try: - if item not in self.dbsession.dirty: - self.flash( - gettext('No changes detected, no update needed.'), - hawat.const.FLASH_INFO - ) - return self.redirect(default_url = self.get_url_next()) - - self.dbsession.commit() - self.do_after_action(item) - - # Log the item update into changelog. - self.changelog_log(item, item_json_before, item.to_json()) - - self.flash( - flask.Markup(self.get_message_success(item = item)), - hawat.const.FLASH_SUCCESS - ) - return self.redirect(default_url = self.get_url_next()) - - except Exception: # pylint: disable=locally-disabled,broad-except - self.dbsession.rollback() - self.flash( - flask.Markup(self.get_message_failure(item = item)), - hawat.const.FLASH_FAILURE - ) - flask.current_app.log_exception_with_label( - traceback.TracebackException(*sys.exc_info()), - self.get_message_failure(item = item) - ) - return self.redirect(default_url = self.get_url_next()) - - self.response_context.update( - action_name = gettext('Update'), - form_url = flask.url_for(self.get_view_endpoint(), item_id = item_id), - form = form, - item_action = hawat.const.ACTION_ITEM_UPDATE, - item_id = item_id, - item = item - ) - - self.do_before_response() - return self.generate_response() - - -class ItemDeleteView(ItemActionView): # pylint: disable=locally-disabled,abstract-method - """ - Base class for item *delete* action views. These views delete existing items - from database. - """ - - @classmethod - def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" - return 'delete' - - @classmethod - def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" - return gettext('Delete') - - @classmethod - def get_menu_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" - return gettext('Delete') - - def dispatch_request(self, item_id): # 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. - - This method will attempt to validate the submitted form and delete the - instance of appropriate item from database in case user agreed to the - item removal action. - """ - item = self.fetch(item_id) - if not item: - self.abort(404) - - if not self.authorize_item_action(item = item): - self.abort(403) - - form = ItemActionConfirmForm() - - cancel_response = self.check_action_cancel(form, item = item) - if cancel_response: - return cancel_response - - item_json_before = item.to_json() - - if form.validate_on_submit(): - form_data = form.data - - self.do_before_action(item) - - if form_data[hawat.const.FORM_ACTION_SUBMIT]: - try: - self.dbsession.delete(item) - self.dbsession.commit() - self.do_after_action(item) - - # Log the item deletion into changelog. - self.changelog_log(item, item_json_before, '') - - self.flash( - flask.Markup(self.get_message_success(item = item)), - hawat.const.FLASH_SUCCESS - ) - return self.redirect( - default_url = self.get_url_next(), - exclude_url = flask.url_for( - '{}.{}'.format(self.module_name, 'show'), - item_id = item.id - ) - ) - - except Exception: # pylint: disable=locally-disabled,broad-except - self.dbsession.rollback() - self.flash( - flask.Markup(self.get_message_failure(item = item)), - hawat.const.FLASH_FAILURE - ) - flask.current_app.log_exception_with_label( - traceback.TracebackException(*sys.exc_info()), - self.get_message_failure(item = item) - ) - return self.redirect(default_url = self.get_url_next()) - - self.response_context.update( - confirm_form = form, - confirm_url = flask.url_for(self.get_view_endpoint(), item_id = item_id), - item_name = str(item), - item_id = item_id, - item = item - ) - - self.do_before_response() - return self.generate_response() - - -class ItemChangeView(ItemActionView): # pylint: disable=locally-disabled,abstract-method - """ - Base class for single item change views, that are doing some simple modification - of item attribute, like enable/disable item, etc. - """ - - @classmethod - def validate_item_change(cls, **kwargs): # pylint: disable=locally-disabled,unused-argument - """ - Perform validation of particular change to given item. - """ - return True - - @classmethod - def change_item(cls, **kwargs): - """ - *Hook method*: Change given item in any desired way. - - :param item: Item to be changed/modified. - """ - raise NotImplementedError() - - def dispatch_request(self, item_id): # 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. - - This method will attempt to validate the submitted form, then perform - arbitrary mangling action with the item and submit the changes to the - database. - """ - item = self.fetch(item_id) - if not item: - self.abort(404) - - if not self.authorize_item_action(item = item): - self.abort(403) - - if not self.validate_item_change(item = item): - self.abort(400) - - form = ItemActionConfirmForm() - - cancel_response = self.check_action_cancel(form, item = item) - if cancel_response: - return cancel_response - - item_json_before = item.to_json() - - if form.validate_on_submit(): - form_data = form.data - - self.do_before_action(item) - - if form_data[hawat.const.FORM_ACTION_SUBMIT]: - try: - self.change_item(item = item) - if item not in self.dbsession.dirty: - self.flash( - gettext('No changes detected, no update needed.'), - hawat.const.FLASH_INFO - ) - return self.redirect(default_url = self.get_url_next()) - - self.dbsession.commit() - self.do_after_action(item) - - # Log the item change into changelog. - self.changelog_log(item, item_json_before, item.to_json()) - - self.flash( - flask.Markup(self.get_message_success(item = item)), - hawat.const.FLASH_SUCCESS - ) - try: - exclude_url = flask.url_for( - '{}.{}'.format(self.module_name, 'show'), - item_id = item.id - ) - except werkzeug.routing.BuildError: - exclude_url = None - return self.redirect( - default_url = self.get_url_next(), - exclude_url = exclude_url - ) - - except Exception: # pylint: disable=locally-disabled,broad-except - self.dbsession.rollback() - self.flash( - flask.Markup(self.get_message_failure(item = item)), - hawat.const.FLASH_FAILURE - ) - flask.current_app.log_exception_with_label( - traceback.TracebackException(*sys.exc_info()), - self.get_message_failure(item = item) - ) - return self.redirect(default_url = self.get_url_next()) - - self.response_context.update( - confirm_form = form, - confirm_url = flask.url_for(self.get_view_endpoint(), item_id = item_id), - item_name = str(item), - item_id = item_id, - item = item - ) - - self.do_before_response() - return self.generate_response() - - -class ItemDisableView(ItemChangeView): # pylint: disable=locally-disabled,abstract-method - """ - Base class for item disabling views. - """ - - @classmethod - def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" - return 'disable' - - @classmethod - def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" - return gettext('Disable') - - @classmethod - def get_menu_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" - return gettext('Disable') - - @classmethod - def validate_item_change(cls, **kwargs): # pylint: disable=locally-disabled,unused-argument - """*Implementation* of :py:func:`hawat.base.ItemChangeView.validate_item_change`.""" - # Reject item change in case given item is already disabled. - if not kwargs['item'].enabled: - return False - return True - - @classmethod - def change_item(cls, **kwargs): - """ - *Implementation* of :py:func:`hawat.base.ItemChangeView.change_item`. - """ - kwargs['item'].enabled = False - - -class ItemEnableView(ItemChangeView): # pylint: disable=locally-disabled,abstract-method - """ - Base class for item enabling views. - """ - - @classmethod - def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" - return 'enable' - - @classmethod - def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" - return gettext('Enable') - - @classmethod - def get_menu_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" - return gettext('Enable') - - @classmethod - def validate_item_change(cls, **kwargs): # pylint: disable=locally-disabled,unused-argument - """*Implementation* of :py:func:`hawat.base.ItemChangeView.validate_item_change`.""" - # Reject item change in case given item is already enabled. - if kwargs['item'].enabled: - return False - return True - - @classmethod - def change_item(cls, **kwargs): - """ - *Implementation* of :py:func:`hawat.base.ItemChangeView.change_item`. - """ - kwargs['item'].enabled = True - - -class ItemObjectRelationView(ItemChangeView): # pylint: disable=locally-disabled,abstract-method - """ - Base class for item object relation action views. - """ - @classmethod - def get_view_icon(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_icon`.""" - return 'module-{}'.format(cls.module_name) - - @classmethod - def get_view_template(cls): - """ - Return Jinja2 template file that should be used for rendering the view - content. This default implementation works only in case the view class - was properly registered into the parent blueprint/module with - :py:func:`hawat.base.VialBlueprint.register_view_class` method. - - :return: Title for the view. - :rtype: str - """ - if cls.module_name: - return '{}/{}.html'.format(cls.module_name, cls.get_view_name()) - raise RuntimeError("Unable to guess default view template, because module name was not yet set.") - - @classmethod - def get_view_url(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_url`.""" - return flask.url_for( - cls.get_view_endpoint(), - item_id = kwargs['item'].get_id(), - other_id = kwargs['other'].get_id() - ) - - @property - def dbmodel_other(self): - """ - *Hook property*. This property must be implemented in each subclass to - return reference to appropriate model class for other objects and that - is based on *SQLAlchemy* declarative base. - """ - raise NotImplementedError() - - @property - def dbquery_other(self): - """ - This property contains the reference to *SQLAlchemy* query object appropriate - for particular ``dbmodel_other`` property. - """ - return self.dbsession.query(self.dbmodel_other) - - def dispatch_request(self, item_id, other_id): # 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. - - This method will attempt to validate the submitted form and create new - instance of appropriate item from form data and finally store the item - into the database. - """ - item = self.fetch(item_id) - if not item: - self.abort(404) - other = self.dbquery_other.filter(self.dbmodel_other.id == other_id).first() - if not other: - self.abort(404) - - if not self.authorize_item_action(item = item, other = other): - self.abort(403) - - if not self.validate_item_change(item = item, other = other): - self.abort(400) - - form = ItemActionConfirmForm() - - cancel_response = self.check_action_cancel(form, item = item, other = other) - if cancel_response: - return cancel_response - - item_json_before = item.to_json() - - if form.validate_on_submit(): - form_data = form.data - - self.do_before_action(item) - - if form_data[hawat.const.FORM_ACTION_SUBMIT]: - try: - self.change_item(item = item, other = other) - if item not in self.dbsession.dirty: - self.flash( - gettext('No changes detected, no update needed.'), - hawat.const.FLASH_INFO - ) - return self.redirect(default_url = self.get_url_next()) - - self.dbsession.commit() - self.do_after_action(item) - - # Log the item change into changelog. - self.changelog_log(item, item_json_before, item.to_json()) - - self.flash( - flask.Markup( - self.get_message_success( - item = item, - other = other - ) - ), - hawat.const.FLASH_SUCCESS - ) - try: - exclude_url = flask.url_for( - '{}.{}'.format(self.module_name, 'show'), - item_id = item.id - ) - except werkzeug.routing.BuildError: - exclude_url = None - return self.redirect( - default_url = self.get_url_next(), - exclude_url = exclude_url - ) - - except Exception: # pylint: disable=locally-disabled,broad-except - self.dbsession.rollback() - self.flash( - flask.Markup( - self.get_message_failure(item = item, other = other) - ), - hawat.const.FLASH_FAILURE - ) - flask.current_app.log_exception_with_label( - traceback.TracebackException(*sys.exc_info()), - self.get_message_failure(item = item, other = other) - ) - return self.redirect(default_url = self.get_url_next()) - - self.response_context.update( - confirm_form = form, - confirm_url = flask.url_for( - '{}.{}'.format( - self.module_name, - self.get_view_name() - ), - item_id = item_id, - other_id = other_id - ), - item_name = str(item), - item_id = item_id, - item = item, - other_name = str(other), - other_id = other_id, - other = other - ) - - self.do_before_response() - return self.generate_response() diff --git a/lib/hawat/blueprints/auth_api/__init__.py b/lib/hawat/blueprints/auth_api/__init__.py index 05456c7c2848b1d0e3a4f9c0fb3d9533d7dde4a0..713faaec794df6ce1b31cd739b17a0972851faed 100644 --- a/lib/hawat/blueprints/auth_api/__init__.py +++ b/lib/hawat/blueprints/auth_api/__init__.py @@ -71,11 +71,15 @@ from flask_babel import gettext, lazy_gettext # # Custom modules. # -from mentat.datatype.sqldb import UserModel -import hawat.const +from mentat.datatype.sqldb import UserModel, ItemChangeLogModel + +import vial.const +import vial.forms +from vial.app import VialBlueprint +from vial.view import ItemChangeView +from vial.view.mixin import HTMLMixin, SQLAlchemyMixin + import hawat.db -import hawat.forms -from hawat.base import HTMLMixin, SQLAlchemyMixin, ItemChangeView, VialBlueprint BLUEPRINT_NAME = 'auth_api' @@ -90,40 +94,45 @@ class GenerateKeyView(HTMLMixin, SQLAlchemyMixin, ItemChangeView): # pylint: di authentication = True - authorization = [hawat.acl.PERMISSION_ADMIN] + authorization = [vial.acl.PERMISSION_ADMIN] @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" return 'key-generate' @classmethod def get_view_icon(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_icon`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_icon`.""" return 'action-genkey' @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" return lazy_gettext('Generate API key') @classmethod def get_view_template(cls): - """*Implementation* of :py:func:`hawat.base.RenderableView.get_view_template`.""" + """*Implementation* of :py:func:`vial.view.RenderableView.get_view_template`.""" return 'auth_api/key-generate.html' #--------------------------------------------------------------------------- @property def dbmodel(self): - """*Implementation* of :py:func:`hawat.base.SQLAlchemyMixin.dbmodel`.""" + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" return UserModel + @property + def dbchlogmodel(self): + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" + return ItemChangeLogModel + #--------------------------------------------------------------------------- @staticmethod def get_message_success(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_success`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_success`.""" return gettext( 'API key for user account <strong>%(item_id)s</strong> was successfully generated.', item_id = str(kwargs['item']) @@ -131,7 +140,7 @@ class GenerateKeyView(HTMLMixin, SQLAlchemyMixin, ItemChangeView): # pylint: di @staticmethod def get_message_failure(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_failure`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_failure`.""" return gettext( 'Unable to generate API key for user account <strong>%(item_id)s</strong>.', item_id = str(kwargs['item']) @@ -139,22 +148,22 @@ class GenerateKeyView(HTMLMixin, SQLAlchemyMixin, ItemChangeView): # pylint: di @staticmethod def get_message_cancel(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_cancel`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_cancel`.""" return gettext( 'Canceled generating API key for user account <strong>%(item_id)s</strong>.', item_id = str(kwargs['item']) ) @classmethod - def change_item(cls, item): + def change_item(cls, **kwargs): """ - *Interface implementation* of :py:func:`hawat.base.ItemChangeView.change_item`. + *Interface implementation* of :py:func:`vial.view.ItemChangeView.change_item`. """ serializer = itsdangerous.URLSafeTimedSerializer( flask.current_app.config['SECRET_KEY'], salt = 'apikey-user' ) - item.apikey = serializer.dumps(item.id) + kwargs['item'].apikey = serializer.dumps(kwargs['item'].id) class DeleteKeyView(HTMLMixin, SQLAlchemyMixin, ItemChangeView): # pylint: disable=locally-disabled,too-many-ancestors @@ -165,40 +174,45 @@ class DeleteKeyView(HTMLMixin, SQLAlchemyMixin, ItemChangeView): # pylint: disa authentication = True - authorization = [hawat.acl.PERMISSION_ADMIN] + authorization = [vial.acl.PERMISSION_ADMIN] @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" return 'key-delete' @classmethod def get_view_icon(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_icon`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_icon`.""" return 'action-delete' @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" return lazy_gettext('Delete API key') @classmethod def get_view_template(cls): - """*Implementation* of :py:func:`hawat.base.RenderableView.get_view_template`.""" + """*Implementation* of :py:func:`vial.view.RenderableView.get_view_template`.""" return 'auth_api/key-delete.html' #--------------------------------------------------------------------------- @property def dbmodel(self): - """*Implementation* of :py:func:`hawat.base.SQLAlchemyMixin.dbmodel`.""" + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" return UserModel + @property + def dbchlogmodel(self): + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" + return ItemChangeLogModel + #--------------------------------------------------------------------------- @staticmethod def get_message_success(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_success`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_success`.""" return gettext( 'API key for user account <strong>%(item_id)s</strong> was successfully deleted.', item_id = str(kwargs['item']) @@ -206,7 +220,7 @@ class DeleteKeyView(HTMLMixin, SQLAlchemyMixin, ItemChangeView): # pylint: disa @staticmethod def get_message_failure(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_failure`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_failure`.""" return gettext( 'Unable to delete API key for user account <strong>%(item_id)s</strong>.', item_id = str(kwargs['item']) @@ -214,18 +228,18 @@ class DeleteKeyView(HTMLMixin, SQLAlchemyMixin, ItemChangeView): # pylint: disa @staticmethod def get_message_cancel(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_cancel`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_cancel`.""" return gettext( 'Canceled deleting API key for user account <strong>%(item_id)s</strong>.', item_id = str(kwargs['item']) ) @classmethod - def change_item(cls, item): + def change_item(cls, **kwargs): """ - *Interface implementation* of :py:func:`hawat.base.ItemChangeView.change_item`. + *Interface implementation* of :py:func:`vial.view.ItemChangeView.change_item`. """ - item.apikey = None + kwargs['item'].apikey = None #------------------------------------------------------------------------------- @@ -238,21 +252,21 @@ class APIAuthBlueprint(VialBlueprint): @classmethod def get_module_title(cls): - """*Implementation* of :py:func:`hawat.base.VialBlueprint.get_module_title`.""" + """*Implementation* of :py:func:`vial.app.VialBlueprint.get_module_title`.""" return gettext('API key authentication service') def register_app(self, app): """ - *Callback method*. Will be called from :py:func:`hawat.base.Vial.register_blueprint` + *Callback method*. Will be called from :py:func:`hawat.base.HawatApp.register_blueprint` method and can be used to customize the Flask application object. Possible use cases: * application menu customization - :param hawat.base.Vial app: Flask application to be customized. + :param hawat.base.HawatApp app: Flask application to be customized. """ - login_manager = app.get_resource(hawat.const.RESOURCE_LOGIN_MANAGER) - principal = app.get_resource(hawat.const.RESOURCE_PRINCIPAL) + login_manager = app.get_resource(vial.const.RESOURCE_LOGIN_MANAGER) + principal = app.get_resource(vial.const.RESOURCE_PRINCIPAL) @login_manager.request_loader def load_user_from_request(request): # pylint: disable=locally-disabled,unused-variable @@ -326,7 +340,7 @@ class APIAuthBlueprint(VialBlueprint): def get_blueprint(): """ Mandatory interface and factory function. This function must return a valid - instance of :py:class:`hawat.base.VialBlueprint` or :py:class:`flask.Blueprint`. + instance of :py:class:`vial.app.VialBlueprint` or :py:class:`flask.Blueprint`. """ hbp = APIAuthBlueprint( diff --git a/lib/hawat/blueprints/auth_api/templates/auth_api/key-delete.html b/lib/hawat/blueprints/auth_api/templates/auth_api/key-delete.html index 3b307270dc12539f9f8d4274538d15193ccc71aa..41af6269d7949e2037a734f63d34d9875949c5d6 100644 --- a/lib/hawat/blueprints/auth_api/templates/auth_api/key-delete.html +++ b/lib/hawat/blueprints/auth_api/templates/auth_api/key-delete.html @@ -10,7 +10,7 @@ <div class="modal-content"> <div class="modal-header"> - <h4 class="modal-title">{{ get_icon('modal-question') }} {{ hawat_current_view.get_view_title() }}</h4> + <h4 class="modal-title">{{ get_icon('modal-question') }} {{ vial_current_view.get_view_title() }}</h4> </div> <div class="modal-body"> diff --git a/lib/hawat/blueprints/auth_api/templates/auth_api/key-generate.html b/lib/hawat/blueprints/auth_api/templates/auth_api/key-generate.html index 0faaac0974854f45d43cb60a80a5ee6942f73869..b1895a3ecfad7975be4628678b0562c189062dc3 100644 --- a/lib/hawat/blueprints/auth_api/templates/auth_api/key-generate.html +++ b/lib/hawat/blueprints/auth_api/templates/auth_api/key-generate.html @@ -10,7 +10,7 @@ <div class="modal-content"> <div class="modal-header"> - <h4 class="modal-title">{{ get_icon('modal-question') }} {{ hawat_current_view.get_view_title() }}</h4> + <h4 class="modal-title">{{ get_icon('modal-question') }} {{ vial_current_view.get_view_title() }}</h4> </div> <div class="modal-body"> diff --git a/lib/hawat/blueprints/auth_dev/__init__.py b/lib/hawat/blueprints/auth_dev/__init__.py index 5b22170d59c4295650a9279b713b965309a1c0a9..717456d8717e239bab3bd6cba89b7565f1b8e35a 100644 --- a/lib/hawat/blueprints/auth_dev/__init__.py +++ b/lib/hawat/blueprints/auth_dev/__init__.py @@ -62,9 +62,14 @@ from flask_babel import gettext, lazy_gettext, force_locale # Custom modules. # from mentat.datatype.sqldb import UserModel, ItemChangeLogModel -import hawat.const -import hawat.forms -from hawat.base import HTMLMixin, SQLAlchemyMixin, SimpleView, RenderableView, VialBlueprint + +import vial.const +import vial.forms +from vial.app import VialBlueprint +from vial.view import RenderableView, SimpleView +from vial.view.mixin import HTMLMixin, SQLAlchemyMixin + +import hawat.db from hawat.blueprints.auth_dev.forms import LoginForm, RegisterUserAccountForm @@ -80,22 +85,22 @@ class LoginView(HTMLMixin, SQLAlchemyMixin, SimpleView): @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" return 'login' @classmethod def get_view_icon(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_icon`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_icon`.""" return 'login' @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('Developer login') @property def dbmodel(self): - """*Implementation* of :py:func:`hawat.base.SQLAlchemyMixin.dbmodel`.""" + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" return UserModel def dispatch_request(self): @@ -117,7 +122,7 @@ class LoginView(HTMLMixin, SQLAlchemyMixin, SimpleView): login = user.login, name = user.fullname )), - hawat.const.FLASH_FAILURE + vial.const.FLASH_FAILURE ) flask_login.login_user(user) @@ -135,7 +140,7 @@ class LoginView(HTMLMixin, SQLAlchemyMixin, SimpleView): 'You have been successfully logged in as <strong>%(user)s</strong>.', user = str(user) )), - hawat.const.FLASH_SUCCESS + vial.const.FLASH_SUCCESS ) self.logger.info( "User '{}' successfully logged in with 'auth_dev'.".format( @@ -146,7 +151,7 @@ class LoginView(HTMLMixin, SQLAlchemyMixin, SimpleView): # Redirect user back to original page. return self.redirect( default_url = flask.url_for( - flask.current_app.config['HAWAT_LOGIN_REDIRECT'] + flask.current_app.config['ENDPOINT_LOGIN_REDIRECT'] ) ) @@ -161,7 +166,7 @@ class LoginView(HTMLMixin, SQLAlchemyMixin, SimpleView): except sqlalchemy.orm.exc.NoResultFound: self.flash( gettext('You have entered wrong login credentials.'), - hawat.const.FLASH_FAILURE + vial.const.FLASH_FAILURE ) except Exception: # pylint: disable=locally-disabled,broad-except @@ -170,7 +175,7 @@ class LoginView(HTMLMixin, SQLAlchemyMixin, SimpleView): "Unable to perform developer login as <strong>%(user)s</strong>.", user = str(form.login.data) )), - hawat.const.FLASH_FAILURE + vial.const.FLASH_FAILURE ) flask.current_app.log_exception_with_label( traceback.TracebackException(*sys.exc_info()), @@ -179,7 +184,7 @@ class LoginView(HTMLMixin, SQLAlchemyMixin, SimpleView): self.response_context.update( form = form, - next = hawat.forms.get_redirect_target() + next = vial.forms.get_redirect_target() ) return self.generate_response() @@ -192,22 +197,22 @@ class RegisterView(HTMLMixin, SQLAlchemyMixin, RenderableView): @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" return 'register' @classmethod def get_view_icon(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_icon`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_icon`.""" return 'register' @classmethod def get_menu_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('Register (dev)') @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" return lazy_gettext('User account registration (dev)') @classmethod @@ -216,7 +221,7 @@ class RegisterView(HTMLMixin, SQLAlchemyMixin, RenderableView): @property def dbmodel(self): - """*Implementation* of :py:func:`hawat.base.SQLAlchemyMixin.dbmodel`.""" + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" return UserModel @staticmethod @@ -270,7 +275,7 @@ class RegisterView(HTMLMixin, SQLAlchemyMixin, RenderableView): "[Mentat] Account registration - %(item_id)s", item_id = account.login ), - recipients = flask.current_app.config['HAWAT_ADMINS'] + recipients = flask.current_app.config['EMAIL_ADMINS'] ) msg.body = flask.render_template( 'auth_env/email_registration_admins.txt', @@ -308,7 +313,7 @@ class RegisterView(HTMLMixin, SQLAlchemyMixin, RenderableView): item_id = account.login ), recipients = [manager.email], - bcc = flask.current_app.config['HAWAT_ADMINS'] + bcc = flask.current_app.config['EMAIL_ADMINS'] ) msg.body = flask.render_template( 'auth_env/email_registration_managers.txt', @@ -334,7 +339,7 @@ class RegisterView(HTMLMixin, SQLAlchemyMixin, RenderableView): item_id = account.login ), recipients = [account.email], - bcc = flask.current_app.config['HAWAT_ADMINS'] + bcc = flask.current_app.config['EMAIL_ADMINS'] ) msg.body = flask.render_template( 'auth_env/email_registration_user.txt', @@ -353,24 +358,24 @@ class RegisterView(HTMLMixin, SQLAlchemyMixin, RenderableView): if form.validate_on_submit(): form_data = form.data - if form_data[hawat.const.FORM_ACTION_CANCEL]: + if form_data[vial.const.FORM_ACTION_CANCEL]: self.flash( gettext('Account registration canceled.'), - hawat.const.FLASH_INFO + vial.const.FLASH_INFO ) return self.redirect( default_url = flask.url_for( - flask.current_app.config['HAWAT_ENDPOINT_HOME'] + flask.current_app.config['ENDPOINT_HOME'] ) ) - if form_data[hawat.const.FORM_ACTION_SUBMIT]: + if form_data[vial.const.FORM_ACTION_SUBMIT]: try: # Populate the user object from form data and make sure the # account has default 'user' role and is disabled by default. item = self.dbmodel() form.populate_obj(item) - item.roles = [hawat.const.ROLE_USER] + item.roles = [vial.const.ROLE_USER] item.enabled = False self.dbsession.add(item) @@ -389,18 +394,18 @@ class RegisterView(HTMLMixin, SQLAlchemyMixin, RenderableView): login = item.login, name = item.fullname )), - hawat.const.FLASH_SUCCESS + vial.const.FLASH_SUCCESS ) return self.redirect( default_url = flask.url_for( - flask.current_app.config['HAWAT_ENDPOINT_HOME'] + flask.current_app.config['ENDPOINT_HOME'] ) ) except Exception: # pylint: disable=locally-disabled,broad-except self.flash( gettext('Unable to register new user account.'), - hawat.const.FLASH_FAILURE + vial.const.FLASH_FAILURE ) flask.current_app.log_exception_with_label( traceback.TracebackException(*sys.exc_info()), @@ -427,18 +432,18 @@ class DevAuthBlueprint(VialBlueprint): @classmethod def get_module_title(cls): - """*Implementation* of :py:func:`hawat.base.VialBlueprint.get_module_title`.""" + """*Implementation* of :py:func:`vial.app.VialBlueprint.get_module_title`.""" return gettext('Developer authentication service') def register_app(self, app): """ - *Callback method*. Will be called from :py:func:`hawat.base.Vial.register_blueprint` + *Callback method*. Will be called from :py:func:`hawat.base.HawatApp.register_blueprint` method and can be used to customize the Flask application object. Possible use cases: * application menu customization - :param hawat.base.Vial app: Flask application to be customized. + :param hawat.base.HawatApp app: Flask application to be customized. """ app.menu_anon.add_entry( 'view', @@ -476,7 +481,7 @@ class DevAuthBlueprint(VialBlueprint): def get_blueprint(): """ Mandatory interface and factory function. This function must return a valid - instance of :py:class:`hawat.base.VialBlueprint` or :py:class:`flask.Blueprint`. + instance of :py:class:`vial.app.VialBlueprint` or :py:class:`flask.Blueprint`. """ hbp = DevAuthBlueprint( diff --git a/lib/hawat/blueprints/auth_dev/forms.py b/lib/hawat/blueprints/auth_dev/forms.py index 10ed57fe4fb5e0c12e21008f2bf0e65aab4e01d8..cf6ffc9f9cc8988720ede4d33d7e83d1dc06c35d 100644 --- a/lib/hawat/blueprints/auth_dev/forms.py +++ b/lib/hawat/blueprints/auth_dev/forms.py @@ -30,6 +30,8 @@ from flask_babel import lazy_gettext # from mentat.datatype.sqldb import UserModel, GroupModel +import vial.forms + import hawat.db from hawat.blueprints.users.forms import check_id_existence, BaseUserAccountForm @@ -89,7 +91,7 @@ class RegisterUserAccountForm(BaseUserAccountForm): validators = [ wtforms.validators.DataRequired(), wtforms.validators.Length(min = 3, max = 50), - hawat.forms.check_login, + vial.forms.check_login, check_id_existence ] ) diff --git a/lib/hawat/blueprints/auth_dev/templates/auth_dev/login.html b/lib/hawat/blueprints/auth_dev/templates/auth_dev/login.html index 6d2a09333691640e91248001e3a234d980ad0fa5..ce0cdafb8837c4edc3d7ef37a4a8af51ea6131ff 100644 --- a/lib/hawat/blueprints/auth_dev/templates/auth_dev/login.html +++ b/lib/hawat/blueprints/auth_dev/templates/auth_dev/login.html @@ -8,7 +8,7 @@ <form method="POST" action="{{ url_for('auth_dev.login', next = next) }}"> <fieldset> - <legend>{{ get_icon('login') }} {{ hawat_current_view.get_view_title() }}</legend> + <legend>{{ get_icon('login') }} {{ vial_current_view.get_view_title() }}</legend> {{ macros_form.render_form_item_select(form.login) }} diff --git a/lib/hawat/blueprints/auth_dev/templates/auth_dev/registration.html b/lib/hawat/blueprints/auth_dev/templates/auth_dev/registration.html index c578e998951e1ddb98436889a2bbc16a20fbf492..49bf36927f7907865b84d75d5656406a5ae91d7e 100644 --- a/lib/hawat/blueprints/auth_dev/templates/auth_dev/registration.html +++ b/lib/hawat/blueprints/auth_dev/templates/auth_dev/registration.html @@ -13,7 +13,7 @@ <form method="POST" action="{{ form_url }}"> <fieldset> - <legend>{{ hawat_current_view.get_view_title() }}</legend> + <legend>{{ vial_current_view.get_view_title() }}</legend> {{ macros_form.render_form_item_default(form.login) }} diff --git a/lib/hawat/blueprints/auth_env/__init__.py b/lib/hawat/blueprints/auth_env/__init__.py index cd2cca4deec542167d2fb6d5862d9d35b67e5491..aefc2700097c94e2235fbebd221e00b4b88093c2 100644 --- a/lib/hawat/blueprints/auth_env/__init__.py +++ b/lib/hawat/blueprints/auth_env/__init__.py @@ -96,10 +96,12 @@ from flask_babel import gettext, lazy_gettext, force_locale # from mentat.datatype.sqldb import UserModel, ItemChangeLogModel -import hawat.const import hawat.db -import hawat.forms -from hawat.base import HTMLMixin, SQLAlchemyMixin, RenderableView, VialBlueprint +import vial.const +import vial.forms +from vial.app import VialBlueprint +from vial.view import RenderableView +from vial.view.mixin import HTMLMixin, SQLAlchemyMixin from hawat.blueprints.auth_env.forms import RegisterUserAccountForm @@ -136,22 +138,22 @@ class LoginView(HTMLMixin, RenderableView): @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" return 'login' @classmethod def get_view_icon(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_icon`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_icon`.""" return 'login' @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" return lazy_gettext('Environment login') @classmethod def get_menu_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('Login (env)') def dispatch_request(self): @@ -163,7 +165,7 @@ class LoginView(HTMLMixin, RenderableView): if not user_login: self.flash( gettext('User login was not received, unable to perform login process.'), - hawat.const.FLASH_FAILURE + vial.const.FLASH_FAILURE ) self.abort(403) @@ -182,7 +184,7 @@ class LoginView(HTMLMixin, RenderableView): except sqlalchemy.orm.exc.NoResultFound: self.flash( gettext('You have entered wrong login credentials.'), - hawat.const.FLASH_FAILURE + vial.const.FLASH_FAILURE ) self.abort(403) @@ -193,7 +195,7 @@ class LoginView(HTMLMixin, RenderableView): login = user.login, name = user.fullname )), - hawat.const.FLASH_FAILURE + vial.const.FLASH_FAILURE ) self.abort(403) @@ -217,7 +219,7 @@ class LoginView(HTMLMixin, RenderableView): login = user.login, name = user.fullname )), - hawat.const.FLASH_SUCCESS + vial.const.FLASH_SUCCESS ) self.logger.info( "User '{}' successfully logged in with 'auth_env'.".format( @@ -228,7 +230,7 @@ class LoginView(HTMLMixin, RenderableView): # Redirect user back to original page. return self.redirect( default_url = flask.url_for( - flask.current_app.config['HAWAT_LOGIN_REDIRECT'] + flask.current_app.config['ENDPOINT_LOGIN_REDIRECT'] ) ) @@ -241,22 +243,22 @@ class RegisterView(HTMLMixin, SQLAlchemyMixin, RenderableView): @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" return 'register' @classmethod def get_view_icon(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_icon`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_icon`.""" return 'register' @classmethod def get_menu_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('Register') @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" return lazy_gettext('User account registration') @classmethod @@ -265,7 +267,7 @@ class RegisterView(HTMLMixin, SQLAlchemyMixin, RenderableView): @property def dbmodel(self): - """*Implementation* of :py:func:`hawat.base.SQLAlchemyMixin.dbmodel`.""" + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" return UserModel def get_user_from_env(self): @@ -364,7 +366,7 @@ class RegisterView(HTMLMixin, SQLAlchemyMixin, RenderableView): "[Mentat] Account registration - %(item_id)s", item_id = account.login ), - recipients = flask.current_app.config['HAWAT_ADMINS'] + recipients = flask.current_app.config['EMAIL_ADMINS'] ) msg.body = flask.render_template( 'auth_env/email_registration_admins.txt', @@ -402,7 +404,7 @@ class RegisterView(HTMLMixin, SQLAlchemyMixin, RenderableView): item_id = account.login ), recipients = [manager.email], - bcc = flask.current_app.config['HAWAT_ADMINS'] + bcc = flask.current_app.config['EMAIL_ADMINS'] ) msg.body = flask.render_template( 'auth_env/email_registration_managers.txt', @@ -428,7 +430,7 @@ class RegisterView(HTMLMixin, SQLAlchemyMixin, RenderableView): item_id = account.login ), recipients = [account.email], - bcc = flask.current_app.config['HAWAT_ADMINS'] + bcc = flask.current_app.config['EMAIL_ADMINS'] ) msg.body = flask.render_template( 'auth_env/email_registration_user.txt', @@ -463,7 +465,7 @@ class RegisterView(HTMLMixin, SQLAlchemyMixin, RenderableView): 'User account <strong>%(item_id)s</strong> already exists.', item_id = str(user) )), - hawat.const.FLASH_FAILURE + vial.const.FLASH_FAILURE ) self.response_context.update(item = user) return self.generate_response( @@ -475,23 +477,23 @@ class RegisterView(HTMLMixin, SQLAlchemyMixin, RenderableView): if form.validate_on_submit(): form_data = form.data - if form_data[hawat.const.FORM_ACTION_CANCEL]: + if form_data[vial.const.FORM_ACTION_CANCEL]: self.flash( gettext('Account registration canceled.'), - hawat.const.FLASH_INFO + vial.const.FLASH_INFO ) return self.redirect( default_url = flask.url_for( - flask.current_app.config['HAWAT_ENDPOINT_HOME'] + flask.current_app.config['ENDPOINT_HOME'] ) ) - if form_data[hawat.const.FORM_ACTION_SUBMIT]: + if form_data[vial.const.FORM_ACTION_SUBMIT]: try: # Populate the user object from form data and make sure the # account has default 'user' role and is disabled by default. form.populate_obj(item) - item.roles = [hawat.const.ROLE_USER] + item.roles = [vial.const.ROLE_USER] item.enabled = False self.dbsession.add(item) @@ -510,7 +512,7 @@ class RegisterView(HTMLMixin, SQLAlchemyMixin, RenderableView): login = item.login, name = item.fullname )), - hawat.const.FLASH_SUCCESS + vial.const.FLASH_SUCCESS ) return self.generate_response( view_template = 'auth_env/registration_show.html' @@ -519,7 +521,7 @@ class RegisterView(HTMLMixin, SQLAlchemyMixin, RenderableView): except Exception: # pylint: disable=locally-disabled,broad-except self.flash( gettext('Unable to register new user account.'), - hawat.const.FLASH_FAILURE + vial.const.FLASH_FAILURE ) flask.current_app.log_exception_with_label( traceback.TracebackException(*sys.exc_info()), @@ -546,18 +548,18 @@ class EnvAuthBlueprint(VialBlueprint): @classmethod def get_module_title(cls): - """*Implementation* of :py:func:`hawat.base.VialBlueprint.get_module_title`.""" + """*Implementation* of :py:func:`vial.app.VialBlueprint.get_module_title`.""" return gettext('Environment authentication service') def register_app(self, app): """ - *Callback method*. Will be called from :py:func:`hawat.base.Vial.register_blueprint` + *Callback method*. Will be called from :py:func:`hawat.base.HawatApp.register_blueprint` method and can be used to customize the Flask application object. Possible use cases: * application menu customization - :param hawat.base.Vial app: Flask application to be customized. + :param hawat.base.HawatApp app: Flask application to be customized. """ app.menu_anon.add_entry( 'view', @@ -585,7 +587,7 @@ class EnvAuthBlueprint(VialBlueprint): def get_blueprint(): """ Mandatory interface and factory function. This function must return a valid - instance of :py:class:`hawat.base.VialBlueprint` or :py:class:`flask.Blueprint`. + instance of :py:class:`vial.app.VialBlueprint` or :py:class:`flask.Blueprint`. """ hbp = EnvAuthBlueprint( diff --git a/lib/hawat/blueprints/auth_env/templates/auth_env/registration.html b/lib/hawat/blueprints/auth_env/templates/auth_env/registration.html index 8dd9d9d18e13b0815c8aebaa5b633d8f3d9d6aa0..94ff3eed9129f7e8a7592b28c7c9953eeafb1632 100644 --- a/lib/hawat/blueprints/auth_env/templates/auth_env/registration.html +++ b/lib/hawat/blueprints/auth_env/templates/auth_env/registration.html @@ -13,7 +13,7 @@ <form method="POST" action="{{ form_url }}"> <fieldset> - <legend>{{ hawat_current_view.get_view_title() }}</legend> + <legend>{{ vial_current_view.get_view_title() }}</legend> {{ macros_form.render_form_item_static(gettext('Login:'), item.login) }} diff --git a/lib/hawat/blueprints/auth_pwd/__init__.py b/lib/hawat/blueprints/auth_pwd/__init__.py index f9aa20a159c7ec36cb6c36c7a53b3a5676b48a5c..ce95e45468e589979f0fba46152b5959f95e9ecf 100644 --- a/lib/hawat/blueprints/auth_pwd/__init__.py +++ b/lib/hawat/blueprints/auth_pwd/__init__.py @@ -48,9 +48,11 @@ from flask_babel import gettext, lazy_gettext # Custom modules. # from mentat.datatype.sqldb import UserModel -import hawat.const -import hawat.forms -from hawat.base import HTMLMixin, SQLAlchemyMixin, SimpleView, VialBlueprint +import vial.const +import vial.forms +from vial.app import VialBlueprint +from vial.view import SimpleView +from vial.view.mixin import HTMLMixin, SQLAlchemyMixin from hawat.blueprints.auth_pwd.forms import LoginForm @@ -68,32 +70,32 @@ class LoginView(HTMLMixin, SQLAlchemyMixin, SimpleView): @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" return 'login' @classmethod def get_view_icon(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_icon`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_icon`.""" return 'login' @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" return lazy_gettext('Password login') @classmethod def get_menu_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('Login (pwd)') @property def dbmodel(self): - """*Implementation* of :py:func:`hawat.base.SQLAlchemyMixin.dbmodel`.""" + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" return UserModel @property def search_by(self): - """*Implementation* of :py:func:`hawat.base.SQLAlchemyMixin.search_by`.""" + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.search_by`.""" return self.dbmodel.login def dispatch_request(self): @@ -104,7 +106,7 @@ class LoginView(HTMLMixin, SQLAlchemyMixin, SimpleView): if flask_login.current_user.is_authenticated: return self.redirect( default_url = flask.url_for( - flask.current_app.config['hawat_LOGIN_REDIRECT'] + flask.current_app.config['ENDPOINT_LOGIN_REDIRECT'] ) ) @@ -124,7 +126,7 @@ class LoginView(HTMLMixin, SQLAlchemyMixin, SimpleView): login = user.login, name = user.fullname )), - hawat.const.FLASH_FAILURE + vial.const.FLASH_FAILURE ) self.abort(403) @@ -148,7 +150,7 @@ class LoginView(HTMLMixin, SQLAlchemyMixin, SimpleView): login = user.login, name = user.fullname )), - hawat.const.FLASH_SUCCESS + vial.const.FLASH_SUCCESS ) self.logger.info( "User '{}' successfully logged in with 'auth_pwd'.".format( @@ -159,7 +161,7 @@ class LoginView(HTMLMixin, SQLAlchemyMixin, SimpleView): # Redirect user back to original page. return self.redirect( default_url = flask.url_for( - flask.current_app.config['hawat_LOGIN_REDIRECT'] + flask.current_app.config['ENDPOINT_LOGIN_REDIRECT'] ) ) @@ -167,7 +169,7 @@ class LoginView(HTMLMixin, SQLAlchemyMixin, SimpleView): # not say specifically it was password, that was invalid. self.flash( gettext('You have entered wrong login credentials.'), - hawat.const.FLASH_FAILURE + vial.const.FLASH_FAILURE ) except sqlalchemy.orm.exc.MultipleResultsFound: @@ -181,7 +183,7 @@ class LoginView(HTMLMixin, SQLAlchemyMixin, SimpleView): except sqlalchemy.orm.exc.NoResultFound: self.flash( gettext('You have entered wrong login credentials.'), - hawat.const.FLASH_FAILURE + vial.const.FLASH_FAILURE ) except Exception: # pylint: disable=locally-disabled,broad-except @@ -190,7 +192,7 @@ class LoginView(HTMLMixin, SQLAlchemyMixin, SimpleView): "Unable to perform password login as <strong>%(user)s</strong>.", user = str(form.login.data) )), - hawat.const.FLASH_FAILURE + vial.const.FLASH_FAILURE ) flask.current_app.log_exception_with_label( traceback.TracebackException(*sys.exc_info()), @@ -199,7 +201,7 @@ class LoginView(HTMLMixin, SQLAlchemyMixin, SimpleView): self.response_context.update( form = form, - next = hawat.forms.get_redirect_target() + next = vial.forms.get_redirect_target() ) return self.generate_response() @@ -214,18 +216,18 @@ class PwdAuthBlueprint(VialBlueprint): @classmethod def get_module_title(cls): - """*Implementation* of :py:func:`hawat.base.VialBlueprint.get_module_title`.""" + """*Implementation* of :py:func:`vial.app.VialBlueprint.get_module_title`.""" return gettext('Password authentication service') def register_app(self, app): """ - *Callback method*. Will be called from :py:func:`hawat.base.Vial.register_blueprint` + *Callback method*. Will be called from :py:func:`hawat.base.HawatApp.register_blueprint` method and can be used to customize the Flask application object. Possible use cases: * application menu customization - :param hawat.base.Vial app: Flask application to be customized. + :param hawat.base.HawatApp app: Flask application to be customized. """ app.menu_anon.add_entry( 'view', @@ -242,7 +244,7 @@ class PwdAuthBlueprint(VialBlueprint): def get_blueprint(): """ Mandatory interface and factory function. This function must return a valid - instance of :py:class:`hawat.base.hawatBlueprint` or :py:class:`flask.Blueprint`. + instance of :py:class:`vial.app.VialBlueprint` or :py:class:`flask.Blueprint`. """ hbp = PwdAuthBlueprint( diff --git a/lib/hawat/blueprints/changelogs/__init__.py b/lib/hawat/blueprints/changelogs/__init__.py index aaa04a7a6e16b45e74bf417bceccab02e7aa27a8..65041f461e5ac863172e7eab17297a1f01af7ccf 100644 --- a/lib/hawat/blueprints/changelogs/__init__.py +++ b/lib/hawat/blueprints/changelogs/__init__.py @@ -31,9 +31,10 @@ from flask_babel import lazy_gettext # from mentat.datatype.sqldb import ItemChangeLogModel -import hawat.acl -from hawat.base import HTMLMixin, SQLAlchemyMixin, BaseSearchView,\ - ItemShowView, VialBlueprint +import vial.acl +from vial.app import VialBlueprint +from vial.view import BaseSearchView, ItemShowView +from vial.view.mixin import HTMLMixin, SQLAlchemyMixin from hawat.blueprints.changelogs.forms import ItemChangeLogSearchForm @@ -49,36 +50,36 @@ class SearchView(HTMLMixin, SQLAlchemyMixin, BaseSearchView): # pylint: disable authentication = True - authorization = [hawat.acl.PERMISSION_POWER] + authorization = [vial.acl.PERMISSION_POWER] @classmethod def get_menu_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('Changelogs') @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" return lazy_gettext('Search item changelogs') #--------------------------------------------------------------------------- @property def dbmodel(self): - """*Implementation* of :py:func:`hawat.base.SQLAlchemyMixin.dbmodel`.""" + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" return ItemChangeLogModel @staticmethod def get_search_form(request_args): """ - *Interface implementation* of :py:func:`hawat.base.SearchView.get_search_form`. + *Interface implementation* of :py:func:`vial.view.BaseSearchView.get_search_form`. """ return ItemChangeLogSearchForm(request_args, meta = {'csrf': False}) @staticmethod def build_query(query, model, form_args): """ - *Interface implementation* of :py:func:`hawat.base.SearchView.build_query`. + *Interface implementation* of :py:func:`vial.view.BaseSearchView.build_query`. """ # Adjust query based on lower time boudary selection. if 'dt_from' in form_args and form_args['dt_from']: @@ -105,7 +106,7 @@ class SearchView(HTMLMixin, SQLAlchemyMixin, BaseSearchView): # pylint: disable @classmethod def get_context_action_menu(cls): - """*Implementation* of :py:func:`hawat.base.ItemListView.get_context_action_menu`.""" + """*Implementation* of :py:func:`vial.view.ItemListView.get_context_action_menu`.""" context_action_menu = super().get_context_action_menu() context_action_menu.add_entry( 'submenu', @@ -151,30 +152,30 @@ class ShowView(HTMLMixin, SQLAlchemyMixin, ItemShowView): authentication = True - authorization = [hawat.acl.PERMISSION_POWER] + authorization = [vial.acl.PERMISSION_POWER] @classmethod def get_menu_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('Show item changelog record') @classmethod def get_menu_legend(cls, **kwargs): """ - *Interface implementation* of :py:func:`hawat.base.BaseView.get_menu_legend`. + *Interface implementation* of :py:func:`vial.view.BaseView.get_menu_legend`. """ return lazy_gettext('View details of item changelog record "%(item)s"', item = str(kwargs['item'])) @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" return lazy_gettext('Show item changelog record details') #--------------------------------------------------------------------------- @property def dbmodel(self): - """*Implementation* of :py:func:`hawat.base.SQLAlchemyMixin.dbmodel`.""" + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" return ItemChangeLogModel @classmethod @@ -182,11 +183,11 @@ class ShowView(HTMLMixin, SQLAlchemyMixin, ItemShowView): """ Get breadcrumbs menu. """ - action_menu = hawat.menu.Menu() + action_menu = vial.menu.Menu() action_menu.add_entry( 'endpoint', 'home', - endpoint = flask.current_app.config['HAWAT_ENDPOINT_HOME'] + endpoint = flask.current_app.config['ENDPOINT_HOME'] ) action_menu.add_entry( 'endpoint', @@ -211,18 +212,18 @@ class ItemChangeLogsBlueprint(VialBlueprint): @classmethod def get_module_title(cls): - """*Implementation* of :py:func:`hawat.base.VialBlueprint.get_module_title`.""" + """*Implementation* of :py:func:`vial.app.VialBlueprint.get_module_title`.""" return lazy_gettext('Item changelog record pluggable module') def register_app(self, app): """ - *Callback method*. Will be called from :py:func:`hawat.base.Vial.register_blueprint` + *Callback method*. Will be called from :py:func:`hawat.base.HawatApp.register_blueprint` method and can be used to customize the Flask application object. Possible use cases: * application menu customization - :param hawat.base.Vial app: Flask application to be customized. + :param hawat.base.HawatApp app: Flask application to be customized. """ app.menu_main.add_entry( 'view', @@ -238,7 +239,7 @@ class ItemChangeLogsBlueprint(VialBlueprint): def get_blueprint(): """ Mandatory interface and factory function. This function must return a valid - instance of :py:class:`hawat.base.VialBlueprint` or :py:class:`flask.Blueprint`. + instance of :py:class:`vial.app.VialBlueprint` or :py:class:`flask.Blueprint`. """ hbp = ItemChangeLogsBlueprint( diff --git a/lib/hawat/blueprints/changelogs/forms.py b/lib/hawat/blueprints/changelogs/forms.py index 6ad83b4ec49df54cf771a5a4aa02d88c5f64da2b..3bd0c3e132c09d6d1cfbc8ac20e5d7a9bc81d8da 100644 --- a/lib/hawat/blueprints/changelogs/forms.py +++ b/lib/hawat/blueprints/changelogs/forms.py @@ -23,9 +23,11 @@ from wtforms.ext.sqlalchemy.fields import QuerySelectMultipleField import flask_wtf from flask_babel import lazy_gettext -import hawat.const +import vial.const +import vial.forms + import hawat.db -import hawat.forms + from mentat.datatype.sqldb import UserModel, ItemChangeLogModel @@ -65,7 +67,7 @@ def get_item_model_choices(): ) ) -class ItemChangeLogSearchForm(hawat.forms.BaseSearchForm): +class ItemChangeLogSearchForm(vial.forms.BaseSearchForm): """ Class representing item changelog search form. """ @@ -96,14 +98,14 @@ class ItemChangeLogSearchForm(hawat.forms.BaseSearchForm): wtforms.validators.Optional(), ] ) - dt_from = hawat.forms.SmartDateTimeField( + dt_from = vial.forms.SmartDateTimeField( lazy_gettext('From:'), validators = [ wtforms.validators.Optional() ], - default = lambda: hawat.forms.default_dt_with_delta(hawat.const.DEFAULT_RESULT_TIMEDELTA) + default = lambda: vial.forms.default_dt_with_delta(vial.const.DEFAULT_RESULT_TIMEDELTA) ) - dt_to = hawat.forms.SmartDateTimeField( + dt_to = vial.forms.SmartDateTimeField( lazy_gettext('To:'), validators = [ wtforms.validators.Optional() @@ -134,14 +136,14 @@ class ItemChangeLogDashboardForm(flask_wtf.FlaskForm): """ Class representing item changelog dashboard search form. """ - dt_from = hawat.forms.SmartDateTimeField( + dt_from = vial.forms.SmartDateTimeField( lazy_gettext('From:'), validators = [ wtforms.validators.Optional() ], - default = lambda: hawat.forms.default_dt_with_delta(hawat.const.DEFAULT_RESULT_TIMEDELTA) + default = lambda: vial.forms.default_dt_with_delta(vial.const.DEFAULT_RESULT_TIMEDELTA) ) - dt_to = hawat.forms.SmartDateTimeField( + dt_to = vial.forms.SmartDateTimeField( lazy_gettext('To:'), validators = [ wtforms.validators.Optional() diff --git a/lib/hawat/blueprints/changelogs/templates/changelogs/search.html b/lib/hawat/blueprints/changelogs/templates/changelogs/search.html index 83ff8f70a4d7451e23825186807d9489452e2035..60b5b4918a2b0a731f304f73dba05cf6154eaf41 100644 --- a/lib/hawat/blueprints/changelogs/templates/changelogs/search.html +++ b/lib/hawat/blueprints/changelogs/templates/changelogs/search.html @@ -31,6 +31,6 @@ {%- block contentsearchresult %} - {{ macros_page.render_changelog_records(items, hawat_current_view.get_context_action_menu()) }} + {{ macros_page.render_changelog_records(items, vial_current_view.get_context_action_menu()) }} {%- endblock contentsearchresult %} diff --git a/lib/hawat/blueprints/changelogs/templates/changelogs/show.html b/lib/hawat/blueprints/changelogs/templates/changelogs/show.html index b1c0d54216b4dd3e18b0f10016c91439c50431d9..7e13cdb510f4006bfd43dbb0515f5758d54f27fc 100644 --- a/lib/hawat/blueprints/changelogs/templates/changelogs/show.html +++ b/lib/hawat/blueprints/changelogs/templates/changelogs/show.html @@ -6,7 +6,7 @@ <div class="col-lg-12"> {{ macros_page.render_breadcrumbs(item) }} - <h2>{{ hawat_current_view.get_view_title() }}</h2> + <h2>{{ vial_current_view.get_view_title() }}</h2> <hr> <h3>{{ item.__str__() }}</h3> <br> diff --git a/lib/hawat/blueprints/dbstatus/__init__.py b/lib/hawat/blueprints/dbstatus/__init__.py index 80c4b4a0cc6747f9618469769b43798ac823ce5b..c38c08ab598b45828877532b6a77724cebd76511 100644 --- a/lib/hawat/blueprints/dbstatus/__init__.py +++ b/lib/hawat/blueprints/dbstatus/__init__.py @@ -55,12 +55,14 @@ __author__ = "Jan Mach <jan.mach@cesnet.cz>" __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea Kropáčová <andrea.kropacova@cesnet.cz>" +import sys import datetime import traceback # # Flask related modules. # +import werkzeug.routing import flask import flask_login from flask_babel import gettext, lazy_gettext @@ -73,11 +75,15 @@ import mentat.const import mentat.system from mentat.datatype.sqldb import UserModel, GroupModel, FilterModel, SettingsReportingModel -import hawat.menu -import hawat.acl +import vial.menu +import vial.acl import hawat.events -from hawat.forms import ItemActionConfirmForm -from hawat.base import RenderableView, SimpleView, PsycopgMixin, SQLAlchemyMixin, AJAXMixin, HTMLMixin, VialBlueprint, RE_UQUERY +from vial.app import VialBlueprint +from vial.view import RenderableView, SimpleView +from vial.view.mixin import HTMLMixin, AJAXMixin, SQLAlchemyMixin +from vial.forms import ItemActionConfirmForm +from hawat.base import PsycopgMixin +from hawat.base import RE_UQUERY BLUEPRINT_NAME = 'dbstatus' @@ -90,26 +96,26 @@ class ViewView(HTMLMixin, PsycopgMixin, SimpleView): """ authentication = True - authorization = [hawat.acl.PERMISSION_ADMIN] + authorization = [vial.acl.PERMISSION_ADMIN] @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" return 'view' @classmethod def get_view_icon(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_icon`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_icon`.""" return 'module-{}'.format(BLUEPRINT_NAME) @classmethod def get_menu_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('Database status') @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" return lazy_gettext('Database status') def _enrich_result_queries(self, result): @@ -129,7 +135,7 @@ class ViewView(HTMLMixin, PsycopgMixin, SimpleView): return result def do_before_response(self, **kwargs): - """*Implementation* of :py:func:`hawat.base.RenderableView.do_before_response`.""" + """*Implementation* of :py:func:`vial.view.RenderableView.do_before_response`.""" self.response_context.update( query_status_events = self._enrich_result_queries(self.get_db().queries_status()), database_status_events = self.get_db().database_status(), @@ -154,7 +160,7 @@ class ViewView(HTMLMixin, PsycopgMixin, SimpleView): database_statistics_events = dbstatistics_events ) - action_menu = hawat.menu.Menu() + action_menu = vial.menu.Menu() action_menu.add_entry( 'endpoint', 'stop', @@ -172,26 +178,26 @@ class MyQueriesView(HTMLMixin, PsycopgMixin, SimpleView): """ authentication = True - authorization = [hawat.acl.PERMISSION_ANY] + authorization = [vial.acl.PERMISSION_ANY] @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" return 'queries_my' @classmethod def get_view_icon(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_icon`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_icon`.""" return 'module-{}'.format(BLUEPRINT_NAME) @classmethod def get_menu_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('My queries') @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" return lazy_gettext('My currently running queries') def _enrich_result_queries(self, result): @@ -211,7 +217,7 @@ class MyQueriesView(HTMLMixin, PsycopgMixin, SimpleView): return result def do_before_response(self, **kwargs): - """*Implementation* of :py:func:`hawat.base.RenderableView.do_before_response`.""" + """*Implementation* of :py:func:`vial.view.RenderableView.do_before_response`.""" self.response_context.update( query_status_events = self._enrich_result_queries( self.get_db().queries_status( @@ -222,7 +228,7 @@ class MyQueriesView(HTMLMixin, PsycopgMixin, SimpleView): ) ) - action_menu = hawat.menu.Menu() + action_menu = vial.menu.Menu() action_menu.add_entry( 'endpoint', 'stop', @@ -240,38 +246,38 @@ class QueryStatusView(AJAXMixin, PsycopgMixin, RenderableView): """ authentication = True - authorization = [hawat.acl.PERMISSION_ANY] + authorization = [vial.acl.PERMISSION_ANY] @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" return 'query-status' @classmethod def get_view_icon(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_icon`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_icon`.""" return 'module-{}'.format(BLUEPRINT_NAME) @classmethod def get_menu_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('Query status') @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" return lazy_gettext('Query status') @classmethod def get_view_url(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_url`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_url`.""" return flask.url_for( cls.get_view_endpoint(), item_id = kwargs['item']['query_name'] ) def do_before_response(self, **kwargs): - """*Implementation* of :py:func:`hawat.base.RenderableView.do_before_response`.""" + """*Implementation* of :py:func:`vial.view.RenderableView.do_before_response`.""" query_status = self.get_db().query_status(kwargs['item_id']) if not query_status: self.abort(404) @@ -288,7 +294,7 @@ class QueryStatusView(AJAXMixin, PsycopgMixin, RenderableView): Will be called by the **Flask** framework to service the request. """ user_id, query_id = self.parse_qname(item_id) - if flask_login.current_user.get_id() != user_id and not hawat.acl.PERMISSION_POWER.can(): + if flask_login.current_user.get_id() != user_id and not vial.acl.PERMISSION_POWER.can(): self.abort( 403, gettext('You are not allowed to view status of this query.') @@ -306,26 +312,26 @@ class AbstractQueryStopView(PsycopgMixin, RenderableView): authentication = True - authorization = [hawat.acl.PERMISSION_ANY] + authorization = [vial.acl.PERMISSION_ANY] @classmethod def get_view_icon(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_icon`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_icon`.""" return 'action-stop' @classmethod def get_menu_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('Stop query') @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" return lazy_gettext('Stop query') @classmethod def get_view_url(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_url`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_url`.""" return flask.url_for( cls.get_view_endpoint(), item_id = kwargs['item']['query_name'] @@ -337,21 +343,21 @@ class AbstractQueryStopView(PsycopgMixin, RenderableView): Perform access authorization for current user to particular item. """ user_id, query_id = cls.parse_qname(kwargs['item']['query_name']) - return hawat.acl.PERMISSION_POWER.can() or flask_login.current_user.get_id() == user_id + return vial.acl.PERMISSION_POWER.can() or flask_login.current_user.get_id() == user_id @staticmethod def get_message_success(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_success`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_success`.""" return gettext('Query <strong>%(item_id)s</strong> was successfully stopped.', item_id = str(kwargs['item']['query_name'])) @staticmethod def get_message_failure(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_failure`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_failure`.""" return gettext('Unable to stop query <strong>%(item_id)s</strong>.', item_id = str(kwargs['item']['query_name'])) @staticmethod def get_message_cancel(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_cancel`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_cancel`.""" return gettext('Canceled stopping query <strong>%(item_id)s</strong>.', item_id = str(kwargs['item']['query_name'])) def get_url_next(self): @@ -366,17 +372,17 @@ class AbstractQueryStopView(PsycopgMixin, RenderableView): ) except werkzeug.routing.BuildError: return flask.url_for( - flask.current_app.config['HAWAT_ENDPOINT_HOME'] + flask.current_app.config['ENDPOINT_HOME'] ) def check_action_cancel(self, form, **kwargs): """ Check the form for *cancel* button press and cancel the action. """ - if getattr(form, hawat.const.FORM_ACTION_CANCEL).data: + if getattr(form, vial.const.FORM_ACTION_CANCEL).data: self.flash( flask.Markup(self.get_message_cancel(**kwargs)), - hawat.const.FLASH_INFO + vial.const.FLASH_INFO ) return self.redirect(default_url = self.get_url_next()) @@ -403,18 +409,18 @@ class AbstractQueryStopView(PsycopgMixin, RenderableView): if form.validate_on_submit(): form_data = form.data - if form_data[hawat.const.FORM_ACTION_SUBMIT]: + if form_data[vial.const.FORM_ACTION_SUBMIT]: try: action_status = self.get_db().query_cancel(item_id) if action_status: self.flash( flask.Markup(self.get_message_success(item = item)), - hawat.const.FLASH_SUCCESS + vial.const.FLASH_SUCCESS ) else: self.flash( flask.Markup(self.get_message_failure(item = item)), - hawat.const.FLASH_FAILURE + vial.const.FLASH_FAILURE ) self.get_db().commit() return self.redirect(default_url = self.get_url_next()) @@ -423,7 +429,7 @@ class AbstractQueryStopView(PsycopgMixin, RenderableView): self.get_db().commit() self.flash( flask.Markup(self.get_message_failure(item = item)), - hawat.const.FLASH_FAILURE + vial.const.FLASH_FAILURE ) flask.current_app.log_exception_with_label( traceback.TracebackException(*sys.exc_info()), @@ -448,12 +454,12 @@ class QueryStopView(HTMLMixin, AbstractQueryStopView): """ @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" return 'query-stop' @classmethod def get_view_template(cls): - """*Implementation* of :py:func:`hawat.base.RenderableView.get_view_template`.""" + """*Implementation* of :py:func:`vial.view.RenderableView.get_view_template`.""" return '{}/query_stop.html'.format(cls.module_name) @@ -463,7 +469,7 @@ class ApiQueryStopView(AJAXMixin, AbstractQueryStopView): """ @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" return 'api-query-stop' @@ -473,33 +479,33 @@ class DashboardView(HTMLMixin, SQLAlchemyMixin, SimpleView): # pylint: disable= """ authentication = True - authorization = [hawat.acl.PERMISSION_ADMIN] + authorization = [vial.acl.PERMISSION_ADMIN] @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" return 'dashboard' @classmethod def get_menu_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('Object management') @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" return lazy_gettext('Object management dashboards') @classmethod def get_view_template(cls): - """*Implementation* of :py:func:`hawat.base.RenderableView.get_view_template`.""" + """*Implementation* of :py:func:`vial.view.RenderableView.get_view_template`.""" return '{}/dashboard.html'.format(cls.module_name) #--------------------------------------------------------------------------- def do_before_response(self, **kwargs): - """*Implementation* of :py:func:`hawat.base.RenderableView.do_before_response`.""" + """*Implementation* of :py:func:`vial.view.RenderableView.do_before_response`.""" self.response_context['users_disabled'] = self.dbquery(UserModel).\ filter(UserModel.enabled == False).\ order_by(UserModel.createtime.desc()).\ @@ -581,7 +587,7 @@ class DashboardView(HTMLMixin, SQLAlchemyMixin, SimpleView): # pylint: disable= order_by(SettingsReportingModel.createtime.desc()).\ all() - action_menu = hawat.menu.Menu() + action_menu = vial.menu.Menu() action_menu.add_entry( 'endpoint', 'show', @@ -623,7 +629,7 @@ class DashboardView(HTMLMixin, SQLAlchemyMixin, SimpleView): # pylint: disable= ) self.response_context['context_action_menu_user'] = action_menu - action_menu = hawat.menu.Menu() + action_menu = vial.menu.Menu() action_menu.add_entry( 'endpoint', 'show', @@ -662,7 +668,7 @@ class DashboardView(HTMLMixin, SQLAlchemyMixin, SimpleView): # pylint: disable= ) self.response_context['context_action_menu_group'] = action_menu - action_menu = hawat.menu.Menu() + action_menu = vial.menu.Menu() action_menu.add_entry( 'endpoint', 'show', @@ -712,18 +718,18 @@ class DatabaseStatusBlueprint(VialBlueprint): @classmethod def get_module_title(cls): - """*Implementation* of :py:func:`hawat.base.VialBlueprint.get_module_title`.""" + """*Implementation* of :py:func:`vial.app.VialBlueprint.get_module_title`.""" return lazy_gettext('Database status overview pluggable module') def register_app(self, app): """ - *Callback method*. Will be called from :py:func:`hawat.base.Vial.register_blueprint` + *Callback method*. Will be called from :py:func:`hawat.base.HawatApp.register_blueprint` method and can be used to customize the Flask application object. Possible use cases: * application menu customization - :param hawat.base.Vial app: Flask application to be customized. + :param hawat.base.HawatApp app: Flask application to be customized. """ app.menu_main.add_entry( 'view', @@ -751,7 +757,7 @@ class DatabaseStatusBlueprint(VialBlueprint): def get_blueprint(): """ Mandatory interface and factory function. This function must return a valid - instance of :py:class:`hawat.base.VialBlueprint` or :py:class:`flask.Blueprint`. + instance of :py:class:`vial.app.VialBlueprint` or :py:class:`flask.Blueprint`. """ hbp = DatabaseStatusBlueprint( diff --git a/lib/hawat/blueprints/dbstatus/templates/dbstatus/queries_my.html b/lib/hawat/blueprints/dbstatus/templates/dbstatus/queries_my.html index ac363e1d4fa33405f2c96f8e4f8849fe4b31fb62..ecae88fe3b516ee32f9053d2f059a0f17d577b16 100644 --- a/lib/hawat/blueprints/dbstatus/templates/dbstatus/queries_my.html +++ b/lib/hawat/blueprints/dbstatus/templates/dbstatus/queries_my.html @@ -6,15 +6,15 @@ <div class="col-lg-12"> <ol class="breadcrumb"> <li><a href="{{ url_for('home.index') }}">{{ _('Home') }}</a></li> - <li class="active">{{ hawat_current_view.get_menu_title() }}</li> + <li class="active">{{ vial_current_view.get_menu_title() }}</li> </ol> - <h2>{{ hawat_current_view.get_view_title() }}</h2> + <h2>{{ vial_current_view.get_view_title() }}</h2> <hr> </div> <!-- /.col-lg-12 --> </div> <!-- /.row --> - + {%- if query_status_events %} <table class="table table-condensed table-striped"> <tr> @@ -86,11 +86,11 @@ {% endif %} {% if 'query_name' in query_data and query_data.query_name %} <td> - {{ + {{ macros_page.render_menu_context_actions( - query_data, + query_data, action_menu = context_action_menu_query - ) + ) }} </td> {% else %} diff --git a/lib/hawat/blueprints/dbstatus/templates/dbstatus/view.html b/lib/hawat/blueprints/dbstatus/templates/dbstatus/view.html index 90e7fc9d912c7f96f6454d2d0e6f34818533ce64..9171d762e99ff5288d634b5833625ae96a8bd28f 100644 --- a/lib/hawat/blueprints/dbstatus/templates/dbstatus/view.html +++ b/lib/hawat/blueprints/dbstatus/templates/dbstatus/view.html @@ -8,7 +8,7 @@ <li><a href="{{ url_for('home.index') }}">{{ _('Home') }}</a></li> <li class="active">{{ _('Database status') }}</li> </ol> - <h2>{{ hawat_current_view.get_view_title() }}</h2> + <h2>{{ vial_current_view.get_view_title() }}</h2> <hr> </div> <!-- /.col-lg-12 --> @@ -165,11 +165,11 @@ {% endif %} {% if 'query_name' in query_data and query_data.query_name %} <td> - {{ + {{ macros_page.render_menu_context_actions( - query_data, + query_data, action_menu = context_action_menu_query - ) + ) }} </td> {% else %} diff --git a/lib/hawat/blueprints/design/__init__.py b/lib/hawat/blueprints/design/__init__.py index f48c6814381c8c7095c76e7ef74c46a720f952b1..567329c5e87d45a021d0791949bac5db487b89f8 100644 --- a/lib/hawat/blueprints/design/__init__.py +++ b/lib/hawat/blueprints/design/__init__.py @@ -44,7 +44,7 @@ __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea Kropáčová <andrea from flask_babel import lazy_gettext -import hawat.base +from vial.app import VialBlueprint # @@ -53,14 +53,14 @@ import hawat.base BLUEPRINT_NAME = 'design' -class DesignBlueprint(hawat.base.VialBlueprint): +class DesignBlueprint(VialBlueprint): """ Hawat pluggable module - application design and style. """ @classmethod def get_module_title(cls): - """*Implementation* of :py:func:`hawat.base.VialBlueprint.get_module_title`.""" + """*Implementation* of :py:func:`vial.app.VialBlueprint.get_module_title`.""" return lazy_gettext('Hawat default design pluggable module') #------------------------------------------------------------------------------- @@ -69,7 +69,7 @@ class DesignBlueprint(hawat.base.VialBlueprint): def get_blueprint(): """ Mandatory interface and factory function. This function must return a valid - instance of :py:class:`hawat.base.VialBlueprint` or :py:class:`flask.Blueprint`. + instance of :py:class:`vial.app.VialBlueprint` or :py:class:`flask.Blueprint`. """ hbp = DesignBlueprint( diff --git a/lib/hawat/blueprints/design/templates/_layout.html b/lib/hawat/blueprints/design/templates/_layout.html index 7a921c496b84c6414d90b43dd74a34b1f3b3c659..167e3e450480a755083183fd9098588c52481fd5 100644 --- a/lib/hawat/blueprints/design/templates/_layout.html +++ b/lib/hawat/blueprints/design/templates/_layout.html @@ -184,7 +184,7 @@ -----------------------------------------------------------------------> <link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}"> - <title>{% block title %}{{ hawat_current_view.get_view_title() }}{% endblock %} - Mentat</title> + <title>{% block title %}{{ vial_current_view.get_view_title() }}{% endblock %} - Mentat</title> {%- endblock head %} </head> @@ -214,7 +214,7 @@ <span class="icon-bar"></span> <span class="icon-bar"></span> </button> - <a class="navbar-brand" href="{{ url_for(config['HAWAT_ENDPOINT_HOME']) }}"> + <a class="navbar-brand" href="{{ url_for(config['ENDPOINT_HOME']) }}"> <img src="{{ url_for('design.static', filename='images/app-logo-mini.png') }}" class="mentat-logo-mini" alt="Mentat logo"> Mentat </a> @@ -224,7 +224,7 @@ <ul class="nav navbar-nav navbar-right"> {{ macros_site.render_submenu_auth() }} {{ macros_site.render_locale_switcher() }} - {%- if hawat_current_view.has_help %} + {%- if vial_current_view.has_help %} <li> {{ macros_page.render_help_link(icon = 'help', text = '') }} </li> diff --git a/lib/hawat/blueprints/design/templates/_layout_confirmation.html b/lib/hawat/blueprints/design/templates/_layout_confirmation.html index 3c60a769f0b9a977c218d0e694b73c862e29b4ca..7acbe08cb51a741258be31ecda8770b55210b0f7 100644 --- a/lib/hawat/blueprints/design/templates/_layout_confirmation.html +++ b/lib/hawat/blueprints/design/templates/_layout_confirmation.html @@ -11,7 +11,7 @@ <div class="modal-header"> <h4 class="modal-title"> - {{ get_icon('modal-question') }} {{ hawat_current_view.get_view_title() }} + {{ get_icon('modal-question') }} {{ vial_current_view.get_view_title() }} </h4> </div> diff --git a/lib/hawat/blueprints/design/templates/_layout_events_search.html b/lib/hawat/blueprints/design/templates/_layout_events_search.html index b4c86bf3ffbad012e1aeaa850c9f1c442ca0269d..51196325e9f8fd58d55de88eb708b875943b76f2 100644 --- a/lib/hawat/blueprints/design/templates/_layout_events_search.html +++ b/lib/hawat/blueprints/design/templates/_layout_events_search.html @@ -8,7 +8,7 @@ <!-- Search form - BEGIN ----------------------------------> <div class="jumbotron" style="margin-top: 1em;"> - <h2>{{ hawat_current_view.get_view_title() }}</h2> + <h2>{{ vial_current_view.get_view_title() }}</h2> <hr> <form method="GET" class="form-horizontal" id="form-events-simple" action="{{ url_for(request.endpoint) }}"> <div> diff --git a/lib/hawat/blueprints/design/templates/_layout_list.html b/lib/hawat/blueprints/design/templates/_layout_list.html index bf3661df5572b1f4e99b9081d7de1f3450829bcf..d0a156dc4762bef6d7041d960be877aec6a6260f 100644 --- a/lib/hawat/blueprints/design/templates/_layout_list.html +++ b/lib/hawat/blueprints/design/templates/_layout_list.html @@ -38,7 +38,7 @@ {{ macros_page.render_breadcrumbs() }} - <h2>{{ hawat_current_view.get_view_title() }}</h2> + <h2>{{ vial_current_view.get_view_title() }}</h2> <div class="pull-right"> {{ macros_page.render_menu_actions() }} </div> diff --git a/lib/hawat/blueprints/design/templates/_layout_search.html b/lib/hawat/blueprints/design/templates/_layout_search.html index 23a89a803b55ebb02d576a9226529c89b3dba565..64fff59a85099c11bc9a1d5c17e097d77da8e9fc 100644 --- a/lib/hawat/blueprints/design/templates/_layout_search.html +++ b/lib/hawat/blueprints/design/templates/_layout_search.html @@ -9,7 +9,7 @@ <!-- Search form - BEGIN ----------------------------------> <div class="jumbotron" style="margin-top: 1em;"> - <h2>{{ hawat_current_view.get_view_title() }}</h2> + <h2>{{ vial_current_view.get_view_title() }}</h2> <hr> <form method="GET" class="form" action="{{ url_for(request.endpoint) }}"> {% block contentsearchform %}{% endblock contentsearchform %} diff --git a/lib/hawat/blueprints/design/templates/_macros_form.html b/lib/hawat/blueprints/design/templates/_macros_form.html index 6cb67be767452aff4f00fb465c5a309fe0f0c1ff..ea5ba52c6265d815d7efff9850b0a44d07751942 100644 --- a/lib/hawat/blueprints/design/templates/_macros_form.html +++ b/lib/hawat/blueprints/design/templates/_macros_form.html @@ -182,7 +182,7 @@ </p> {%- endif %} {{ caller() }} - {%- if hawat_current_view.has_help %} + {%- if vial_current_view.has_help %} <p> {{ macros_page.render_help_link(icon = 'reference', text = _('more help'), tooltip_text = '') }} </p> diff --git a/lib/hawat/blueprints/design/templates/_macros_page.html b/lib/hawat/blueprints/design/templates/_macros_page.html index 2fab9f7f94cbc3cd9bffef8ffef3e98ecac6a57c..9b28e0da3b9945569d9c2b4bddc92df03767086b 100644 --- a/lib/hawat/blueprints/design/templates/_macros_page.html +++ b/lib/hawat/blueprints/design/templates/_macros_page.html @@ -5,7 +5,7 @@ ----------------------------------------------------------------------------- #} {%- macro render_breadcrumbs(context_item = None) %} - {%- set breadcrumbs_menu = hawat_current_view.get_breadcrumbs_menu() %} + {%- set breadcrumbs_menu = vial_current_view.get_breadcrumbs_menu() %} {%- if breadcrumbs_menu %} <!-- Breadcrumbs menu widget - BEGIN ----------------------> <ol class="breadcrumb"> @@ -36,7 +36,7 @@ {%- macro render_menu_actions(context_item = None, action_menu = None) %} {%- if not action_menu %} - {%- set action_menu = hawat_current_view.get_action_menu() %} + {%- set action_menu = vial_current_view.get_action_menu() %} {%- endif %} {%- if action_menu %} {%- set menu_item_list = action_menu.get_entries(item = context_item) %} @@ -82,7 +82,7 @@ {%- macro render_menu_context_actions(context_item, action_menu = None, cssclass = '', kwargs = {}) %} {%- if not action_menu %} - {%- set action_menu = hawat_current_view.get_context_action_menu() %} + {%- set action_menu = vial_current_view.get_context_action_menu() %} {%- endif %} {%- if action_menu %} {%- set menu_item_list = action_menu.get_entries(item = context_item, **kwargs) %} @@ -204,5 +204,5 @@ {%- macro render_help_link(icon = 'reference', text = _('Help'), tooltip_text = _('View full online documentation'), tooltip_placement = 'bottom', url_suffix = '') %} -<a href="https://alchemist.cesnet.cz/mentat/doc/production/html/_doclib/hawat_plugin_{{ hawat_current_view.module_name }}.html#section-hawat-plugin-{{ hawat_current_view.module_name }}-features-{{ hawat_current_view.get_view_name() }}{{ url_suffix }}" target="_blank"{% if tooltip_text %} data-toggle="tooltip" data-placement="{{ tooltip_placement }}" title="{{ tooltip_text }}"{% endif %}>{% if icon %}{{ get_icon(icon) }}{% endif %}{% if text %} {{ text }}{% endif %}</a> +<a href="https://alchemist.cesnet.cz/mentat/doc/production/html/_doclib/hawat_plugin_{{ vial_current_view.module_name }}.html#section-hawat-plugin-{{ vial_current_view.module_name }}-features-{{ vial_current_view.get_view_name() }}{{ url_suffix }}" target="_blank"{% if tooltip_text %} data-toggle="tooltip" data-placement="{{ tooltip_placement }}" title="{{ tooltip_text }}"{% endif %}>{% if icon %}{{ get_icon(icon) }}{% endif %}{% if text %} {{ text }}{% endif %}</a> {%- endmacro %} diff --git a/lib/hawat/blueprints/design/templates/_macros_site.html b/lib/hawat/blueprints/design/templates/_macros_site.html index 33e1b387532e0569ed1925290f0d1e21ec8a9923..4051027605755eaeb94682665dfb3b2c93d00d9e 100644 --- a/lib/hawat/blueprints/design/templates/_macros_site.html +++ b/lib/hawat/blueprints/design/templates/_macros_site.html @@ -11,7 +11,7 @@ <!-- Main menu widget - BEGIN -----------------------------> <ul class="nav navbar-nav"> - {%- for menu_item in hawat_current_menu_main.get_entries() recursive %} + {%- for menu_item in vial_current_menu_main.get_entries() recursive %} {%- set menu_item_submenu = menu_item.get_entries() %} {%- if menu_item.group %} <li role="separator" class="divider"></li> @@ -54,7 +54,7 @@ {%- if current_user.has_role('admin') %}{{ get_icon('role-admin') }}{% elif current_user.has_role('maintainer') %}{{ get_icon('role-maintainer') }}{% elif current_user.has_role('developer') %}{{ get_icon('role-developer') }}{% else %}{{ get_icon('role-user') }}{% endif %}<span class="hidden-sm hidden-md"> {{ current_user.login }}</span> <span class="caret"></span> </a> <ul class="dropdown-menu"> - {%- for menu_item in hawat_current_menu_auth.get_entries() recursive %} + {%- for menu_item in vial_current_menu_auth.get_entries() recursive %} {%- set menu_item_submenu = menu_item.get_entries() %} {%- if menu_item.group %} <li role="separator" class="divider"></li> @@ -91,7 +91,7 @@ {{ get_icon('role-anonymous') }}<span class="hidden-sm hidden-md"> {{ _('Anonymous') }}</span> <span class="caret"></span> </a> <ul class="dropdown-menu"> - {%- for menu_item in hawat_current_menu_anon.get_entries() recursive %} + {%- for menu_item in vial_current_menu_anon.get_entries() recursive %} {%- set menu_item_submenu = menu_item.get_entries() %} {%- if menu_item.group %} <li role="separator" class="divider"></li> diff --git a/lib/hawat/blueprints/devtools/__init__.py b/lib/hawat/blueprints/devtools/__init__.py index 4b6659ca953bf80b2b071244b396036f95ea59fe..6b04fd6ad4df37ad8befd0ef3b0a3e5c11d3441f 100644 --- a/lib/hawat/blueprints/devtools/__init__.py +++ b/lib/hawat/blueprints/devtools/__init__.py @@ -31,8 +31,10 @@ from flask_babel import lazy_gettext # # Custom modules. # -import hawat.acl -from hawat.base import HTMLMixin, SimpleView, VialBlueprint +import vial.acl +from vial.app import VialBlueprint +from vial.view import SimpleView +from vial.view.mixin import HTMLMixin BLUEPRINT_NAME = 'devtools' @@ -46,26 +48,26 @@ class ConfigView(HTMLMixin, SimpleView): authentication = True - authorization = [hawat.acl.PERMISSION_DEVELOPER] + authorization = [vial.acl.PERMISSION_DEVELOPER] @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" return 'config' @classmethod def get_view_icon(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_icon`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_icon`.""" return 'module-{}'.format(BLUEPRINT_NAME) @classmethod def get_menu_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('System configuration') @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" return lazy_gettext('View system configuration') @@ -79,18 +81,18 @@ class DevtoolsBlueprint(VialBlueprint): @classmethod def get_module_title(cls): - """*Implementation* of :py:func:`hawat.base.VialBlueprint.get_module_title`.""" + """*Implementation* of :py:func:`vial.app.VialBlueprint.get_module_title`.""" return lazy_gettext('Development tools pluggable module') def register_app(self, app): """ - *Callback method*. Will be called from :py:func:`hawat.base.Vial.register_blueprint` + *Callback method*. Will be called from :py:func:`hawat.base.HawatApp.register_blueprint` method and can be used to customize the Flask application object. Possible use cases: * application menu customization - :param hawat.base.Vial app: Flask application to be customized. + :param hawat.base.HawatApp app: Flask application to be customized. """ self.developer_toolbar.init_app(app) @@ -108,7 +110,7 @@ class DevtoolsBlueprint(VialBlueprint): def get_blueprint(): """ Mandatory interface and factory function. This function must return a valid - instance of :py:class:`hawat.base.VialBlueprint` or :py:class:`flask.Blueprint`. + instance of :py:class:`vial.app.VialBlueprint` or :py:class:`flask.Blueprint`. """ hbp = DevtoolsBlueprint( @@ -119,36 +121,36 @@ def get_blueprint(): ) @hbp.route('/admin') - @hawat.acl.PERMISSION_ADMIN.require(403) + @vial.acl.PERMISSION_ADMIN.require(403) def admin(): # pylint: disable=locally-disabled,unused-variable """ Simple utility view for testing 'admin' permissions. """ - return flask.render_template('devtools/permission_test.html', hawat_acl_policy = str(hawat.acl.PERMISSION_ADMIN)) + return flask.render_template('devtools/permission_test.html', hawat_acl_policy = str(vial.acl.PERMISSION_ADMIN)) @hbp.route('/maintainer') - @hawat.acl.PERMISSION_MAINTAINER.require(403) + @vial.acl.PERMISSION_MAINTAINER.require(403) def maintainer(): # pylint: disable=locally-disabled,unused-variable """ Simple utility view for testing 'maintainer' permissions. """ - return flask.render_template('devtools/permission_test.html', hawat_acl_policy = str(hawat.acl.PERMISSION_MAINTAINER)) + return flask.render_template('devtools/permission_test.html', hawat_acl_policy = str(vial.acl.PERMISSION_MAINTAINER)) @hbp.route('/developer') - @hawat.acl.PERMISSION_DEVELOPER.require(403) + @vial.acl.PERMISSION_DEVELOPER.require(403) def developer(): # pylint: disable=locally-disabled,unused-variable """ Simple utility view for testing 'developer' permissions. """ - return flask.render_template('devtools/permission_test.html', hawat_acl_policy = str(hawat.acl.PERMISSION_DEVELOPER)) + return flask.render_template('devtools/permission_test.html', hawat_acl_policy = str(vial.acl.PERMISSION_DEVELOPER)) @hbp.route('/power') - @hawat.acl.PERMISSION_POWER.require(403) + @vial.acl.PERMISSION_POWER.require(403) def power(): # pylint: disable=locally-disabled,unused-variable """ Simple utility view for testing 'power' permissions (admin or maintainer). """ - return flask.render_template('devtools/permission_test.html', hawat_acl_policy = str(hawat.acl.PERMISSION_POWER)) + return flask.render_template('devtools/permission_test.html', hawat_acl_policy = str(vial.acl.PERMISSION_POWER)) hbp.developer_toolbar = flask_debugtoolbar.DebugToolbarExtension() # pylint: disable=locally-disabled,attribute-defined-outside-init diff --git a/lib/hawat/blueprints/devtools/templates/devtools/config.html b/lib/hawat/blueprints/devtools/templates/devtools/config.html index 1a16089c5cda82f3e541773b27610135ccf2f0c6..8307c2592a1ec1c17f7d0fa342a16bc8bd7cfbf9 100644 --- a/lib/hawat/blueprints/devtools/templates/devtools/config.html +++ b/lib/hawat/blueprints/devtools/templates/devtools/config.html @@ -4,7 +4,7 @@ <div class="row"> <div class="col-lg-12"> - <h2>{{ hawat_current_view.get_view_title() }}</h2> + <h2>{{ vial_current_view.get_view_title() }}</h2> <h3>Permissions</h3> <table class="table"> @@ -69,21 +69,21 @@ <h3>Menu - main</h3> <pre class="pre-scrollable"> -{{ hawat_current_menu_main|pprint }} +{{ vial_current_menu_main|pprint }} --- -{{ hawat_current_menu_main.get_entries()|pprint }}</pre> +{{ vial_current_menu_main.get_entries()|pprint }}</pre> <h3>Menu - auth</h3> <pre class="pre-scrollable"> -{{ hawat_current_menu_auth|pprint }} +{{ vial_current_menu_auth|pprint }} --- -{{ hawat_current_menu_auth.get_entries()|pprint }}</pre> +{{ vial_current_menu_auth.get_entries()|pprint }}</pre> <h3>Menu - anon</h3> <pre class="pre-scrollable"> -{{ hawat_current_menu_anon|pprint }} +{{ vial_current_menu_anon|pprint }} --- -{{ hawat_current_menu_anon.get_entries()|pprint }}</pre> +{{ vial_current_menu_anon.get_entries()|pprint }}</pre> </div><!-- /.col-lg-12 --> </div><!-- /.row --> diff --git a/lib/hawat/blueprints/dnsr/__init__.py b/lib/hawat/blueprints/dnsr/__init__.py index 05fc1338971aa718517685519306597830104644..baf4a10d55831b6dd4ea618305878218755ec5e8 100644 --- a/lib/hawat/blueprints/dnsr/__init__.py +++ b/lib/hawat/blueprints/dnsr/__init__.py @@ -60,10 +60,13 @@ from flask_babel import lazy_gettext import mentat.services.dnsr from mentat.const import tr_ -import hawat.const import hawat.db -import hawat.acl -from hawat.base import HTMLMixin, AJAXMixin, SnippetMixin, RenderableView, VialBlueprint, URLParamsBuilder +import vial.const +import vial.acl +from vial.app import VialBlueprint +from vial.view import RenderableView +from vial.view.mixin import HTMLMixin, AJAXMixin, SnippetMixin +from vial.utils import URLParamsBuilder from hawat.blueprints.dnsr.forms import DnsrSearchForm @@ -79,16 +82,16 @@ class AbstractSearchView(RenderableView): # pylint: disable=locally-disabled,ab """ authentication = True - authorization = [hawat.acl.PERMISSION_ANY] + authorization = [vial.acl.PERMISSION_ANY] @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" return lazy_gettext('Search DNS') @classmethod def get_menu_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('Search DNS') #--------------------------------------------------------------------------- @@ -100,7 +103,7 @@ class AbstractSearchView(RenderableView): # pylint: disable=locally-disabled,ab """ form = DnsrSearchForm(flask.request.args, meta = {'csrf': False}) - if hawat.const.FORM_ACTION_SUBMIT in flask.request.args: + if vial.const.FORM_ACTION_SUBMIT in flask.request.args: if form.validate(): form_data = form.data dnsr_service = mentat.services.dnsr.service() @@ -133,7 +136,7 @@ class SearchView(HTMLMixin, AbstractSearchView): # pylint: disable=locally-disa @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" return 'search' @@ -146,7 +149,7 @@ class APISearchView(AJAXMixin, AbstractSearchView): # pylint: disable=locally-d @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" return 'apisearch' @@ -166,7 +169,7 @@ class SnippetSearchView(SnippetMixin, AbstractSearchView): # pylint: disable=lo @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" return 'sptsearch' @@ -180,18 +183,18 @@ class DnsrBlueprint(VialBlueprint): @classmethod def get_module_title(cls): - """*Implementation* of :py:func:`hawat.base.VialBlueprint.get_module_title`.""" + """*Implementation* of :py:func:`vial.app.VialBlueprint.get_module_title`.""" return lazy_gettext('DNS service') def register_app(self, app): """ - *Callback method*. Will be called from :py:func:`hawat.base.Vial.register_blueprint` + *Callback method*. Will be called from :py:func:`hawat.base.HawatApp.register_blueprint` method and can be used to customize the Flask application object. Possible use cases: * application menu customization - :param hawat.base.Vial app: Flask application to be customized. + :param hawat.base.HawatApp app: Flask application to be customized. """ mentat.services.dnsr.init(app.mconfig) @@ -231,7 +234,7 @@ class DnsrBlueprint(VialBlueprint): def get_blueprint(): """ Mandatory interface and factory function. This function must return a valid - instance of :py:class:`hawat.base.VialBlueprint` or :py:class:`flask.Blueprint`. + instance of :py:class:`vial.app.VialBlueprint` or :py:class:`flask.Blueprint`. """ hbp = DnsrBlueprint( diff --git a/lib/hawat/blueprints/dnsr/templates/dnsr/search.html b/lib/hawat/blueprints/dnsr/templates/dnsr/search.html index 852620068784611d1194645239161cd902c81814..964d1ed2be07906ffe835b496d8ca3b5b9a4356d 100644 --- a/lib/hawat/blueprints/dnsr/templates/dnsr/search.html +++ b/lib/hawat/blueprints/dnsr/templates/dnsr/search.html @@ -6,7 +6,7 @@ <div class="col-lg-12"> <div class="jumbotron" style="margin-top: 1em;"> - <h2>{{ hawat_current_view.get_view_title() }}</h2> + <h2>{{ vial_current_view.get_view_title() }}</h2> <form method="GET" class="form-inline" action="{{ url_for('dnsr.search') }}"> <div class="form-group{% if search_form.search.errors %}{{ ' has-error' }}{% endif %}"> {{ search_form.search.label(class_='sr-only') }} diff --git a/lib/hawat/blueprints/events/__init__.py b/lib/hawat/blueprints/events/__init__.py index 718a19ca8b7c0ece4603a0346feadb1440f010cd..1b8bf446de9269b16c8eacf8e3752a9e0a307051 100644 --- a/lib/hawat/blueprints/events/__init__.py +++ b/lib/hawat/blueprints/events/__init__.py @@ -37,10 +37,13 @@ from mentat.const import tr_ import hawat.const import hawat.events -import hawat.acl -from hawat.base import HTMLMixin, PsycopgMixin, AJAXMixin, SQLAlchemyMixin,\ - BaseView, BaseSearchView, ItemShowView, SimpleView, VialBlueprint,\ - URLParamsBuilder +import vial.const +import vial.acl +from vial.app import VialBlueprint +from vial.view import BaseView, SimpleView, BaseSearchView, ItemShowView +from vial.view.mixin import HTMLMixin, AJAXMixin, SQLAlchemyMixin +from vial.utils import URLParamsBuilder +from hawat.base import PsycopgMixin from hawat.blueprints.events.forms import SimpleEventSearchForm, EventDashboardForm @@ -68,7 +71,7 @@ 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(hawat.forms.default_dt_with_delta()) + form.dt_from.process_data(vial.forms.default_dt_with_delta()) return form @@ -80,33 +83,38 @@ class AbstractSearchView(PsycopgMixin, BaseSearchView): # pylint: disable=local """ authentication = True - authorization = [hawat.acl.PERMISSION_ANY] + authorization = [vial.acl.PERMISSION_ANY] @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" return lazy_gettext('Search event database') + @classmethod + def get_view_icon(cls): + """*Implementation* of :py:func:`vial.view.BaseView.get_view_icon`.""" + return 'module-{}'.format(cls.module_name) + @classmethod def get_menu_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('Events') @staticmethod def get_search_form(request_args): """ - *Interface implementation* of :py:func:`hawat.base.SearchView.get_search_form`. + *Interface implementation* of :py:func:`vial.view.BaseSearchView.get_search_form`. """ return _get_search_form(request_args) def do_before_search(self, form_data): # pylint: disable=locally-disabled,no-self-use,unused-argument """ - *Interface implementation* of :py:func:`hawat.base.SearchView.do_before_search`. + *Interface implementation* of :py:func:`vial.view.BaseSearchView.do_before_search`. """ form_data['groups'] = [item.name for item in form_data['groups']] def do_before_response(self, **kwargs): - """*Implementation* of :py:func:`hawat.base.RenderableView.do_before_response`.""" + """*Implementation* of :py:func:`vial.view.RenderableView.do_before_response`.""" self.response_context.update( quicksearch_list = self.get_quicksearch_by_time() ) @@ -131,13 +139,13 @@ class SearchView(HTMLMixin, AbstractSearchView): # pylint: disable=locally-disa @classmethod def get_breadcrumbs_menu(cls): """ - *Interface implementation* of :py:func:`hawat.base.SearchView.get_breadcrumbs_menu`. + *Interface implementation* of :py:func:`vial.view.BaseSearchView.get_breadcrumbs_menu`. """ - breadcrumbs_menu = hawat.menu.Menu() + breadcrumbs_menu = vial.menu.Menu() breadcrumbs_menu.add_entry( 'endpoint', 'home', - endpoint = flask.current_app.config['HAWAT_ENDPOINT_HOME'] + endpoint = flask.current_app.config['ENDPOINT_HOME'] ) breadcrumbs_menu.add_entry( 'endpoint', @@ -149,9 +157,9 @@ class SearchView(HTMLMixin, AbstractSearchView): # pylint: disable=locally-disa @classmethod def get_context_action_menu(cls): """ - *Interface implementation* of :py:func:`hawat.base.SearchView.get_context_action_menu`. + *Interface implementation* of :py:func:`vial.view.BaseSearchView.get_context_action_menu`. """ - action_menu = hawat.menu.Menu() + action_menu = vial.menu.Menu() action_menu.add_entry( 'endpoint', 'show', @@ -176,7 +184,7 @@ class APISearchView(AJAXMixin, AbstractSearchView): # pylint: disable=locally-d @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" return 'apisearch' @@ -187,22 +195,22 @@ class AbstractShowView(PsycopgMixin, ItemShowView): # pylint: disable=locally-d """ authentication = True - authorization = [hawat.acl.PERMISSION_ANY] + authorization = [vial.acl.PERMISSION_ANY] @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('Show event') @classmethod def get_menu_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('Show') @classmethod def get_menu_legend(cls, **kwargs): """ - *Interface implementation* of :py:func:`hawat.base.BaseView.get_menu_legend`. + *Interface implementation* of :py:func:`vial.view.BaseView.get_menu_legend`. """ return lazy_gettext('View details of event "%(item)s"', item = kwargs['item'].get_id()) @@ -219,9 +227,9 @@ class ShowView(HTMLMixin, AbstractShowView): # pylint: disable=locally-disabled @classmethod def get_action_menu(cls): # pylint: disable=locally-disabled,unused-argument """ - *Interface implementation* of :py:func:`hawat.base.SearchView.get_action_menu`. + *Interface implementation* of :py:func:`vial.view.BaseSearchView.get_action_menu`. """ - action_menu = hawat.menu.Menu() + action_menu = vial.menu.Menu() action_menu.add_entry( 'endpoint', 'download', @@ -239,7 +247,7 @@ class APIShowView(AJAXMixin, AbstractShowView): # pylint: disable=locally-disab @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" return 'apishow' @@ -251,32 +259,32 @@ class DownloadView(PsycopgMixin, BaseView): authentication = True - authorization = [hawat.acl.PERMISSION_ANY] + authorization = [vial.acl.PERMISSION_ANY] @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" return 'download' @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('Download event') @classmethod def get_view_url(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_url`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_url`.""" return flask.url_for(cls.get_view_endpoint(), item_id = kwargs['item'].get_id()) @classmethod def get_menu_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('Download') @classmethod def get_menu_legend(cls, **kwargs): """ - *Interface implementation* of :py:func:`hawat.base.BaseView.get_menu_legend`. + *Interface implementation* of :py:func:`vial.view.BaseView.get_menu_legend`. """ return lazy_gettext('Download event "%(item)s"', item = kwargs['item'].get_id()) @@ -314,46 +322,46 @@ class AbstractDashboardView(SQLAlchemyMixin, BaseSearchView): # pylint: disable """ authentication = True - authorization = [hawat.acl.PERMISSION_ANY] + authorization = [vial.acl.PERMISSION_ANY] @classmethod def get_view_icon(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_icon`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_icon`.""" return 'module-{}'.format(BLUEPRINT_NAME) @classmethod def get_menu_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('Events') @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" return lazy_gettext('Overall event dashboards') @classmethod def get_view_template(cls): - """*Implementation* of :py:func:`hawat.base.RenderableView.get_view_template`.""" + """*Implementation* of :py:func:`vial.view.RenderableView.get_view_template`.""" return '{}/{}.html'.format(cls.module_name, cls.get_view_name()) #--------------------------------------------------------------------------- @property def dbmodel(self): - """*Implementation* of :py:func:`hawat.base.SQLAlchemyMixin.dbmodel`.""" + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" return EventStatisticsModel @staticmethod def get_search_form(request_args): """ - *Interface implementation* of :py:func:`hawat.base.SearchView.get_search_form`. + *Interface implementation* of :py:func:`vial.view.BaseSearchView.get_search_form`. """ return EventDashboardForm(request_args, meta = {'csrf': False}) @staticmethod def build_query(query, model, form_args): """ - *Interface implementation* of :py:func:`hawat.base.SQLAlchemyMixin.build_query`. + *Interface implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.build_query`. """ # Adjust query based on lower time boudary selection. if 'dt_from' in form_args and form_args['dt_from']: @@ -367,7 +375,7 @@ class AbstractDashboardView(SQLAlchemyMixin, BaseSearchView): # pylint: disable def do_after_search(self, items): """ - *Interface implementation* of :py:func:`hawat.base.SearchView.do_after_search`. + *Interface implementation* of :py:func:`vial.view.BaseSearchView.do_after_search`. """ self.logger.debug( "Calculating event dashboard overview from %d records.", @@ -397,7 +405,7 @@ class AbstractDashboardView(SQLAlchemyMixin, BaseSearchView): # pylint: disable ) def do_before_response(self, **kwargs): - """*Implementation* of :py:func:`hawat.base.RenderableView.do_before_response`.""" + """*Implementation* of :py:func:`vial.view.RenderableView.do_before_response`.""" self.response_context.update( quicksearch_list = self.get_quicksearch_by_time() ) @@ -412,7 +420,7 @@ class DashboardView(HTMLMixin, AbstractDashboardView): # pylint: disable=locall @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" return 'dashboard' @@ -425,11 +433,11 @@ class APIDashboardView(AJAXMixin, AbstractDashboardView): # pylint: disable=loc @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" return 'apidashboard' def process_response_context(self): - """*Implementation* of :py:func:`hawat.base.AJAXMixin.process_response_context`.""" + """*Implementation* of :py:func:`vial.view.mixin.AJAXMixin.process_response_context`.""" super().process_response_context() # Prevent certain response context keys to appear in final response. for key in ('items', 'quicksearch_list'): @@ -446,22 +454,22 @@ class APIMetadataView(AJAXMixin, SimpleView): """ authentication = True - authorization = [hawat.acl.PERMISSION_ANY] + authorization = [vial.acl.PERMISSION_ANY] methods = ['GET','POST'] @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" return 'metadata' @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('Event metadata') def do_before_response(self, **kwargs): - """*Implementation* of :py:func:`hawat.base.RenderableView.do_before_response`.""" + """*Implementation* of :py:func:`vial.view.RenderableView.do_before_response`.""" self.response_context.update(**hawat.events.get_event_enums()) @@ -475,18 +483,18 @@ class EventsBlueprint(VialBlueprint): @classmethod def get_module_title(cls): - """*Implementation* of :py:func:`hawat.base.VialBlueprint.get_module_title`.""" + """*Implementation* of :py:func:`vial.app.VialBlueprint.get_module_title`.""" return lazy_gettext('Event database') def register_app(self, app): """ - *Callback method*. Will be called from :py:func:`hawat.base.Vial.register_blueprint` + *Callback method*. Will be called from :py:func:`hawat.base.HawatApp.register_blueprint` method and can be used to customize the Flask application object. Possible use cases: * application menu customization - :param hawat.base.Vial app: Flask application to be customized. + :param hawat.base.HawatApp app: Flask application to be customized. """ app.menu_main.add_entry( 'view', @@ -625,7 +633,7 @@ class EventsBlueprint(VialBlueprint): def get_blueprint(): """ Mandatory interface and factory function. This function must return a valid - instance of :py:class:`hawat.base.VialBlueprint` or :py:class:`flask.Blueprint`. + instance of :py:class:`vial.app.VialBlueprint` or :py:class:`flask.Blueprint`. """ hbp = EventsBlueprint( diff --git a/lib/hawat/blueprints/events/forms.py b/lib/hawat/blueprints/events/forms.py index 192861772d6800e8d2ee9191dcda59bf9505f210..998f1b1ba722054e7f606578f3c3b47b15b6ce92 100644 --- a/lib/hawat/blueprints/events/forms.py +++ b/lib/hawat/blueprints/events/forms.py @@ -24,9 +24,11 @@ from wtforms.ext.sqlalchemy.fields import QuerySelectMultipleField import flask_wtf from flask_babel import lazy_gettext -import hawat.const +import vial.const +import vial.forms + import hawat.db -import hawat.forms + from mentat.datatype.sqldb import GroupModel @@ -37,86 +39,86 @@ def get_available_groups(): return hawat.db.db_query(GroupModel).order_by(GroupModel.name).all() -class SimpleEventSearchForm(hawat.forms.BaseSearchForm): +class SimpleEventSearchForm(vial.forms.BaseSearchForm): """ Class representing simple event search form. """ - dt_from = hawat.forms.SmartDateTimeField( + dt_from = vial.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.') ) - dt_to = hawat.forms.SmartDateTimeField( + dt_to = vial.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.') ) - st_from = hawat.forms.SmartDateTimeField( + st_from = vial.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 = hawat.forms.SmartDateTimeField( + st_to = vial.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 = hawat.forms.CommaListField( + source_addrs = vial.forms.CommaListField( lazy_gettext('Source addresses:'), validators = [ wtforms.validators.Optional(), - hawat.forms.check_network_record_list + vial.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 = hawat.forms.CommaListField( + target_addrs = vial.forms.CommaListField( lazy_gettext('Target addresses:'), validators = [ wtforms.validators.Optional(), - hawat.forms.check_network_record_list + vial.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 = hawat.forms.CommaListField( + host_addrs = vial.forms.CommaListField( lazy_gettext('Host addresses:'), validators = [ wtforms.validators.Optional(), - hawat.forms.check_network_record_list + vial.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 = hawat.forms.CommaListField( + source_ports = vial.forms.CommaListField( lazy_gettext('Source ports:'), validators = [ wtforms.validators.Optional(), - hawat.forms.check_port_list + vial.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 = hawat.forms.CommaListField( + target_ports = vial.forms.CommaListField( lazy_gettext('Target ports:'), validators = [ wtforms.validators.Optional(), - hawat.forms.check_port_list + vial.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 = hawat.forms.CommaListField( + host_ports = vial.forms.CommaListField( lazy_gettext('Host ports:'), validators = [ wtforms.validators.Optional(), - hawat.forms.check_port_list + vial.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.') ) @@ -343,15 +345,15 @@ class EventDashboardForm(flask_wtf.FlaskForm): """ Class representing event dashboard search form. """ - dt_from = hawat.forms.SmartDateTimeField( + dt_from = vial.forms.SmartDateTimeField( lazy_gettext('From:'), validators = [ wtforms.validators.Optional() ], - default = lambda: hawat.forms.default_dt_with_delta(hawat.const.DEFAULT_RESULT_TIMEDELTA), + default = lambda: vial.forms.default_dt_with_delta(vial.const.DEFAULT_RESULT_TIMEDELTA), description = lazy_gettext('Lower time boundary for event detection time as provided by event detector. Timestamp is expected to be in the format <code>YYYY-MM-DD hh:mm:ss</code> and in the timezone according to the user`s preferences. Event detectors are usually outside of the control of Mentat system administrators and may sometimes emit events with invalid detection times, for example timestamps in the future.') ) - dt_to = hawat.forms.SmartDateTimeField( + dt_to = vial.forms.SmartDateTimeField( lazy_gettext('To:'), validators = [ wtforms.validators.Optional() diff --git a/lib/hawat/blueprints/events/templates/events/dashboard.html b/lib/hawat/blueprints/events/templates/events/dashboard.html index 3a4816fd9ef8c74a4e36e8201332ff6d5b5beccc..b283393a4873206454613aaf881e37dde2b50c14 100644 --- a/lib/hawat/blueprints/events/templates/events/dashboard.html +++ b/lib/hawat/blueprints/events/templates/events/dashboard.html @@ -9,7 +9,7 @@ <!-- Search form - BEGIN ----------------------------------> <div class="jumbotron" style="margin-top: 1em;"> - <h2>{{ hawat_current_view.get_view_title() }}</h2> + <h2>{{ vial_current_view.get_view_title() }}</h2> <hr> <form method="GET" class="form" action="{{ url_for(request.endpoint) }}"> <div class="row"> diff --git a/lib/hawat/blueprints/events/templates/events/show.html b/lib/hawat/blueprints/events/templates/events/show.html index 80fe9cbccf538162b55ea834bb25569d7ab440cb..470e077166af678def3276ec08cd12294550b44c 100644 --- a/lib/hawat/blueprints/events/templates/events/show.html +++ b/lib/hawat/blueprints/events/templates/events/show.html @@ -17,7 +17,7 @@ </li> </ol> <h2> - {{ hawat_current_view.get_view_title() }} + {{ vial_current_view.get_view_title() }} </h2> <hr> <h3> diff --git a/lib/hawat/blueprints/filters/__init__.py b/lib/hawat/blueprints/filters/__init__.py index cd4134b0e1eab2f441455001f079108151128879..90cd4c9adace1fe76c3a64c9024b275ec3e0c8af 100644 --- a/lib/hawat/blueprints/filters/__init__.py +++ b/lib/hawat/blueprints/filters/__init__.py @@ -52,12 +52,12 @@ from mentat.const import REPORTING_FILTER_BASIC from mentat.datatype.sqldb import FilterModel, GroupModel, ItemChangeLogModel from mentat.idea.internal import Idea, IDEAFilterCompiler -import hawat.const +import vial.const import hawat.db import hawat.events -from hawat.base import HTMLMixin, SQLAlchemyMixin, ItemListView,\ - ItemShowView, ItemCreateView, ItemCreateForView, ItemUpdateView,\ - ItemEnableView, ItemDisableView, ItemDeleteView, RenderableView, VialBlueprint +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 from hawat.blueprints.filters.forms import BaseFilterForm, AdminFilterForm, PlaygroundFilterForm @@ -141,24 +141,24 @@ class ListView(HTMLMixin, SQLAlchemyMixin, ItemListView): authentication = True - authorization = [hawat.acl.PERMISSION_POWER] + authorization = [vial.acl.PERMISSION_POWER] @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('Filter management') #--------------------------------------------------------------------------- @property def dbmodel(self): - """*Implementation* of :py:func:`hawat.base.SQLAlchemyMixin.dbmodel`.""" + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" return FilterModel @classmethod def get_action_menu(cls): - """*Implementation* of :py:func:`hawat.base.ItemListView.get_action_menu`.""" - action_menu = hawat.menu.Menu() + """*Implementation* of :py:func:`vial.view.ItemListView.get_action_menu`.""" + action_menu = vial.menu.Menu() action_menu.add_entry( 'endpoint', 'create', @@ -175,8 +175,8 @@ class ListView(HTMLMixin, SQLAlchemyMixin, ItemListView): @classmethod def get_context_action_menu(cls): - """*Implementation* of :py:func:`hawat.base.ItemListView.get_context_action_menu`.""" - action_menu = hawat.menu.Menu() + """*Implementation* of :py:func:`vial.view.ItemListView.get_context_action_menu`.""" + action_menu = vial.menu.Menu() action_menu.add_entry( 'endpoint', 'show', @@ -220,19 +220,19 @@ class ShowView(HTMLMixin, SQLAlchemyMixin, ItemShowView): @classmethod def get_menu_legend(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('View details of reporting filter "%(item)s"', item = kwargs['item'].name) @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" return lazy_gettext('Show reporting filter details') #--------------------------------------------------------------------------- @property def dbmodel(self): - """*Implementation* of :py:func:`hawat.base.SQLAlchemyMixin.dbmodel`.""" + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" return FilterModel @classmethod @@ -241,17 +241,17 @@ class ShowView(HTMLMixin, SQLAlchemyMixin, ItemShowView): Perform access authorization for current user to particular item. """ permission_mm = flask_principal.Permission( - hawat.acl.MembershipNeed(kwargs['item'].group.id), - hawat.acl.ManagementNeed(kwargs['item'].group.id) + vial.acl.MembershipNeed(kwargs['item'].group.id), + vial.acl.ManagementNeed(kwargs['item'].group.id) ) - return hawat.acl.PERMISSION_POWER.can() or permission_mm.can() + return vial.acl.PERMISSION_POWER.can() or permission_mm.can() @classmethod def get_action_menu(cls): """ Get action menu for particular item. """ - action_menu = hawat.menu.Menu() + action_menu = vial.menu.Menu() action_menu.add_entry( 'endpoint', 'update', @@ -317,48 +317,53 @@ class CreateView(HTMLMixin, SQLAlchemyMixin, ItemCreateView): # pylint: disable @classmethod def get_menu_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('Create reporting filter') @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" return lazy_gettext('Create new reporting filter') #--------------------------------------------------------------------------- @property def dbmodel(self): - """*Implementation* of :py:func:`hawat.base.SQLAlchemyMixin.dbmodel`.""" + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" return FilterModel + @property + def dbchlogmodel(self): + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" + return ItemChangeLogModel + @classmethod def authorize_item_action(cls, **kwargs): """ Perform access authorization for current user to particular item. """ - return hawat.acl.PERMISSION_POWER.can() + return vial.acl.PERMISSION_POWER.can() #--------------------------------------------------------------------------- @staticmethod def get_message_success(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_success`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_success`.""" return gettext('Reporting filter <strong>%(item_id)s</strong> for group <strong>%(parent_id)s</strong> was successfully created.', item_id = str(kwargs['item']), parent_id = str(kwargs['item'].group)) @staticmethod def get_message_failure(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_failure`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_failure`.""" return gettext('Unable to create new reporting filter for group <strong>%(parent_id)s</strong>.', parent_id = str(kwargs['item'].group)) @staticmethod def get_message_cancel(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_cancel`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_cancel`.""" return gettext('Canceled creating new reporting filter for group <strong>%(parent_id)s</strong>.', parent_id = str(kwargs['item'].group)) @staticmethod def get_item_form(): - """*Implementation* of :py:func:`hawat.base.ItemCreateView.get_item_form`.""" + """*Implementation* of :py:func:`vial.view.ItemCreateView.get_item_form`.""" detectors = hawat.events.get_event_detectors() categories = hawat.events.get_event_categories() @@ -369,7 +374,7 @@ class CreateView(HTMLMixin, SQLAlchemyMixin, ItemCreateView): # pylint: disable def do_before_action(self, item): # pylint: disable=locally-disabled,no-self-use,unused-argument """ - *Hook method*. Implementation of :py:func:`hawat.base.ItemActionView.do_before_action` interface. + *Hook method*. Implementation of :py:func:`vial.view.ItemActionView.do_before_action` interface. """ process_rule(item) @@ -396,73 +401,78 @@ class CreateForView(HTMLMixin, SQLAlchemyMixin, ItemCreateForView): # pylint: d @classmethod def get_view_icon(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_icon`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_icon`.""" return 'module-{}'.format(BLUEPRINT_NAME) @classmethod def get_menu_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('Create reporting filter') @classmethod def get_menu_legend(cls, **kwargs): """ - *Interface implementation* of :py:func:`hawat.base.BaseView.get_menu_legend`. + *Interface implementation* of :py:func:`vial.view.BaseView.get_menu_legend`. """ return lazy_gettext('Create reporting filter for group "%(item)s"', item = str(kwargs['item'])) @classmethod def get_view_url(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_url`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_url`.""" return flask.url_for(cls.get_view_endpoint(), parent_id = kwargs['item'].id) @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" return lazy_gettext('Create new reporting filter for group') #--------------------------------------------------------------------------- @property def dbmodel(self): - """*Implementation* of :py:func:`hawat.base.SQLAlchemyMixin.dbmodel`.""" + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" return FilterModel @property def dbmodel_par(self): - """*Implementation* of :py:func:`hawat.base.SQLAlchemyMixin.dbmodel`.""" + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" return GroupModel + @property + def dbchlogmodel(self): + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" + return ItemChangeLogModel + @classmethod def authorize_item_action(cls, **kwargs): """ Perform access authorization for current user to particular item. """ permission_m = flask_principal.Permission( - hawat.acl.ManagementNeed(kwargs['item'].id) + vial.acl.ManagementNeed(kwargs['item'].id) ) - return hawat.acl.PERMISSION_POWER.can() or permission_m.can() + return vial.acl.PERMISSION_POWER.can() or permission_m.can() #--------------------------------------------------------------------------- @staticmethod def get_message_success(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_success`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_success`.""" return gettext('Reporting filter <strong>%(item_id)s</strong> for group <strong>%(parent_id)s</strong> was successfully created.', item_id = str(kwargs['item']), parent_id = str(kwargs['parent'])) @staticmethod def get_message_failure(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_failure`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_failure`.""" return gettext('Unable to create new reporting filter for group <strong>%(parent_id)s</strong>.', parent_id = str(kwargs['parent'])) @staticmethod def get_message_cancel(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_cancel`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_cancel`.""" return gettext('Canceled creating new reporting filter for group <strong>%(parent_id)s</strong>.', parent_id = str(kwargs['parent'])) @staticmethod def get_item_form(): - """*Implementation* of :py:func:`hawat.base.ItemCreateView.get_item_form`.""" + """*Implementation* of :py:func:`vial.view.ItemCreateView.get_item_form`.""" detectors = hawat.events.get_event_detectors() categories = hawat.events.get_event_categories() @@ -474,13 +484,13 @@ class CreateForView(HTMLMixin, SQLAlchemyMixin, ItemCreateForView): # pylint: d @staticmethod def add_parent_to_item(item, parent): """ - *Hook method*. Implementation of :py:func:`hawat.base.ItemCreateForView.add_parent_to_item` interface. + *Hook method*. Implementation of :py:func:`vial.view.ItemCreateForView.add_parent_to_item` interface. """ item.group = parent def do_before_action(self, item): # pylint: disable=locally-disabled,no-self-use,unused-argument """ - *Hook method*. Implementation of :py:func:`hawat.base.ItemActionView.do_before_action` interface. + *Hook method*. Implementation of :py:func:`vial.view.ItemActionView.do_before_action` interface. """ process_rule(item) @@ -507,51 +517,56 @@ class UpdateView(HTMLMixin, SQLAlchemyMixin, ItemUpdateView): # pylint: disable @classmethod def get_menu_legend(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('Update details of reporting filter "%(item)s"', item = kwargs['item'].name) @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" return lazy_gettext('Update reporting filter details') #--------------------------------------------------------------------------- @property def dbmodel(self): - """*Implementation* of :py:func:`hawat.base.SQLAlchemyMixin.dbmodel`.""" + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" return FilterModel + @property + def dbchlogmodel(self): + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" + return ItemChangeLogModel + @classmethod def authorize_item_action(cls, **kwargs): """ Perform access authorization for current user to particular item. """ permission_m = flask_principal.Permission( - hawat.acl.ManagementNeed(kwargs['item'].group.id) + vial.acl.ManagementNeed(kwargs['item'].group.id) ) - return hawat.acl.PERMISSION_POWER.can() or permission_m.can() + return vial.acl.PERMISSION_POWER.can() or permission_m.can() #--------------------------------------------------------------------------- @staticmethod def get_message_success(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_success`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_success`.""" return gettext('Reporting filter <strong>%(item_id)s</strong> for group <strong>%(parent_id)s</strong> was successfully updated.', item_id = str(kwargs['item']), parent_id = str(kwargs['item'].group)) @staticmethod def get_message_failure(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_failure`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_failure`.""" return gettext('Unable to update reporting filter <strong>%(item_id)s</strong> for group <strong>%(parent_id)s</strong>.', item_id = str(kwargs['item']), parent_id = str(kwargs['item'].group)) @staticmethod def get_message_cancel(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_cancel`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_cancel`.""" return gettext('Canceled updating reporting filter <strong>%(item_id)s</strong> for group <strong>%(parent_id)s</strong>.', item_id = str(kwargs['item']), parent_id = str(kwargs['item'].group)) @staticmethod def get_item_form(item): - """*Implementation* of :py:func:`hawat.base.ItemUpdateView.get_item_form`.""" + """*Implementation* of :py:func:`vial.view.ItemUpdateView.get_item_form`.""" detectors = hawat.events.get_event_detectors() categories = hawat.events.get_event_categories() @@ -571,7 +586,7 @@ class UpdateView(HTMLMixin, SQLAlchemyMixin, ItemUpdateView): # pylint: disable def do_before_action(self, item): # pylint: disable=locally-disabled,no-self-use,unused-argument """ - *Hook method*. Implementation of :py:func:`hawat.base.ItemActionView.do_before_action` interface. + *Hook method*. Implementation of :py:func:`vial.view.ItemActionView.do_before_action` interface. """ process_rule(item) @@ -597,41 +612,46 @@ class EnableView(HTMLMixin, SQLAlchemyMixin, ItemEnableView): # pylint: disable @classmethod def get_menu_legend(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('Enable reporting filter "%(item)s"', item = kwargs['item'].name) #--------------------------------------------------------------------------- @property def dbmodel(self): - """*Implementation* of :py:func:`hawat.base.SQLAlchemyMixin.dbmodel`.""" + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" return FilterModel + @property + def dbchlogmodel(self): + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" + return ItemChangeLogModel + @classmethod def authorize_item_action(cls, **kwargs): """ Perform access authorization for current user to particular item. """ permission_m = flask_principal.Permission( - hawat.acl.ManagementNeed(kwargs['item'].group.id) + vial.acl.ManagementNeed(kwargs['item'].group.id) ) - return hawat.acl.PERMISSION_POWER.can() or permission_m.can() + return vial.acl.PERMISSION_POWER.can() or permission_m.can() #--------------------------------------------------------------------------- @staticmethod def get_message_success(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_success`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_success`.""" return gettext('Reporting filter <strong>%(item_id)s</strong> for group <strong>%(parent_id)s</strong> was successfully enabled.', item_id = str(kwargs['item']), parent_id = str(kwargs['item'].group)) @staticmethod def get_message_failure(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_failure`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_failure`.""" return gettext('Unable to enable reporting filter <strong>%(item_id)s</strong> for group <strong>%(parent_id)s</strong>.', item_id = str(kwargs['item']), parent_id = str(kwargs['item'].group)) @staticmethod def get_message_cancel(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_cancel`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_cancel`.""" return gettext('Canceled enabling reporting filter <strong>%(item_id)s</strong> for group <strong>%(parent_id)s</strong>.', item_id = str(kwargs['item']), parent_id = str(kwargs['item'].group)) @@ -645,41 +665,46 @@ class DisableView(HTMLMixin, SQLAlchemyMixin, ItemDisableView): # pylint: disab @classmethod def get_menu_legend(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('Disable reporting filter "%(item)s"', item = kwargs['item'].name) #--------------------------------------------------------------------------- @property def dbmodel(self): - """*Implementation* of :py:func:`hawat.base.SQLAlchemyMixin.dbmodel`.""" + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" return FilterModel + @property + def dbchlogmodel(self): + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" + return ItemChangeLogModel + @classmethod def authorize_item_action(cls, **kwargs): """ Perform access authorization for current user to particular item. """ permission_m = flask_principal.Permission( - hawat.acl.ManagementNeed(kwargs['item'].group.id) + vial.acl.ManagementNeed(kwargs['item'].group.id) ) - return hawat.acl.PERMISSION_POWER.can() or permission_m.can() + return vial.acl.PERMISSION_POWER.can() or permission_m.can() #--------------------------------------------------------------------------- @staticmethod def get_message_success(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_success`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_success`.""" return gettext('Reporting filter <strong>%(item_id)s</strong> for group <strong>%(parent_id)s</strong> was successfully disabled.', item_id = str(kwargs['item']), parent_id = str(kwargs['item'].group)) @staticmethod def get_message_failure(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_failure`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_failure`.""" return gettext('Unable to disable reporting filter <strong>%(item_id)s</strong> for group <strong>%(parent_id)s</strong>.', item_id = str(kwargs['item']), parent_id = str(kwargs['item'].group)) @staticmethod def get_message_cancel(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_cancel`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_cancel`.""" return gettext('Canceled disabling reporting filter <strong>%(item_id)s</strong> for group <strong>%(parent_id)s</strong>.', item_id = str(kwargs['item']), parent_id = str(kwargs['item'].group)) @@ -693,41 +718,46 @@ class DeleteView(HTMLMixin, SQLAlchemyMixin, ItemDeleteView): # pylint: disable @classmethod def get_menu_legend(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('Delete reporting filter "%(item)s"', item = kwargs['item'].name) #--------------------------------------------------------------------------- @property def dbmodel(self): - """*Implementation* of :py:func:`hawat.base.SQLAlchemyMixin.dbmodel`.""" + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" return FilterModel + @property + def dbchlogmodel(self): + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" + return ItemChangeLogModel + @classmethod def authorize_item_action(cls, **kwargs): """ Perform access authorization for current user to particular item. """ permission_m = flask_principal.Permission( - hawat.acl.ManagementNeed(kwargs['item'].group.id) + vial.acl.ManagementNeed(kwargs['item'].group.id) ) - return hawat.acl.PERMISSION_POWER.can() or permission_m.can() + return vial.acl.PERMISSION_POWER.can() or permission_m.can() #--------------------------------------------------------------------------- @staticmethod def get_message_success(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_success`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_success`.""" return gettext('Reporting filter <strong>%(item_id)s</strong> for group <strong>%(parent_id)s</strong> was successfully and permanently deleted.', item_id = str(kwargs['item']), parent_id = str(kwargs['item'].group)) @staticmethod def get_message_failure(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_failure`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_failure`.""" return gettext('Unable to permanently delete reporting filter <strong>%(item_id)s</strong> for group <strong>%(parent_id)s</strong>.', item_id = str(kwargs['item']), parent_id = str(kwargs['item'].group)) @staticmethod def get_message_cancel(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_cancel`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_cancel`.""" return gettext('Canceled deleting reporting filter <strong>%(item_id)s</strong> for group <strong>%(parent_id)s</strong>.', item_id = str(kwargs['item']), parent_id = str(kwargs['item'].group)) @@ -741,27 +771,27 @@ class PlaygroundView(HTMLMixin, RenderableView): @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" return 'playground' @classmethod def get_menu_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('Filter playground') @classmethod def get_view_icon(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_icon`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_icon`.""" return 'playground' @classmethod def get_menu_legend(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('Reporting filter playground') @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" return lazy_gettext('Reporting filter rule playground') @classmethod @@ -769,11 +799,11 @@ class PlaygroundView(HTMLMixin, RenderableView): """ Get breadcrumbs menu. """ - breadcrumbs_menu = hawat.menu.Menu() + breadcrumbs_menu = vial.menu.Menu() breadcrumbs_menu.add_entry( 'endpoint', 'home', - endpoint = flask.current_app.config['HAWAT_ENDPOINT_HOME'] + endpoint = flask.current_app.config['ENDPOINT_HOME'] ) breadcrumbs_menu.add_entry( 'endpoint', @@ -826,7 +856,7 @@ class PlaygroundView(HTMLMixin, RenderableView): '<strong>%(error)s</strong>.', error = str(err) )), - hawat.const.FLASH_FAILURE + vial.const.FLASH_FAILURE ) tbexc = traceback.TracebackException(*sys.exc_info()) @@ -853,18 +883,18 @@ class FiltersBlueprint(VialBlueprint): @classmethod def get_module_title(cls): - """*Implementation* of :py:func:`hawat.base.VialBlueprint.get_module_title`.""" + """*Implementation* of :py:func:`vial.app.VialBlueprint.get_module_title`.""" return lazy_gettext('Reporting filter management pluggable module') def register_app(self, app): """ - *Callback method*. Will be called from :py:func:`hawat.base.Vial.register_blueprint` + *Callback method*. Will be called from :py:func:`hawat.base.HawatApp.register_blueprint` method and can be used to customize the Flask application object. Possible use cases: * application menu customization - :param hawat.base.Vial app: Flask application to be customized. + :param hawat.base.HawatApp app: Flask application to be customized. """ app.menu_main.add_entry( 'view', @@ -887,7 +917,7 @@ class FiltersBlueprint(VialBlueprint): def get_blueprint(): """ Mandatory interface and factory function. This function must return a valid - instance of :py:class:`hawat.base.VialBlueprint` or :py:class:`flask.Blueprint`. + instance of :py:class:`vial.app.VialBlueprint` or :py:class:`flask.Blueprint`. """ hbp = FiltersBlueprint( diff --git a/lib/hawat/blueprints/filters/forms.py b/lib/hawat/blueprints/filters/forms.py index f33a34075c862ce6cf0ea68deb1c96ab86b04269..18af85cf1f4e6599a533d0860be6537c86444bee 100644 --- a/lib/hawat/blueprints/filters/forms.py +++ b/lib/hawat/blueprints/filters/forms.py @@ -32,7 +32,7 @@ from flask_babel import gettext, lazy_gettext # Custom modules. # import hawat.db -import hawat.forms +import vial.forms from mentat.datatype.sqldb import GroupModel from mentat.const import REPORTING_FILTER_BASIC, REPORTING_FILTER_ADVANCED from mentat.idea.internal import Idea @@ -79,7 +79,7 @@ def check_event(form, field): # pylint: disable=locally-disabled,unused-argumen #------------------------------------------------------------------------------- -class BaseFilterForm(hawat.forms.BaseItemForm): +class BaseFilterForm(vial.forms.BaseItemForm): """ Class representing base reporting filter form. """ @@ -129,21 +129,21 @@ class BaseFilterForm(hawat.forms.BaseItemForm): choices = [('', lazy_gettext('<< no preference >>'))], filters = [lambda x: x or []] ) - ips = hawat.forms.CommaListField( + ips = vial.forms.CommaListField( lazy_gettext('Source IPs:'), validators = [ wtforms.validators.Optional(), - hawat.forms.check_network_record_list + vial.forms.check_network_record_list ], widget = wtforms.widgets.TextArea() ) - valid_from = hawat.forms.SmartDateTimeField( + valid_from = vial.forms.SmartDateTimeField( lazy_gettext('Valid from:'), validators = [ wtforms.validators.Optional() ] ) - valid_to = hawat.forms.SmartDateTimeField( + valid_to = vial.forms.SmartDateTimeField( lazy_gettext('Valid to:'), validators = [ wtforms.validators.Optional() @@ -158,8 +158,8 @@ class BaseFilterForm(hawat.forms.BaseItemForm): (True, lazy_gettext('Enabled')), (False, lazy_gettext('Disabled')) ], - filters = [hawat.forms.str_to_bool], - coerce = hawat.forms.str_to_bool + filters = [vial.forms.str_to_bool], + coerce = vial.forms.str_to_bool ) submit = wtforms.SubmitField( lazy_gettext('Submit') diff --git a/lib/hawat/blueprints/filters/templates/filters/creatupdate.html b/lib/hawat/blueprints/filters/templates/filters/creatupdate.html index 53b1fac9080eb8ee11650bb7702b6ea4d5737c5a..53d0b6858e5c85f2f80eb7989661269d1f19086f 100644 --- a/lib/hawat/blueprints/filters/templates/filters/creatupdate.html +++ b/lib/hawat/blueprints/filters/templates/filters/creatupdate.html @@ -17,7 +17,7 @@ <form method="POST" action="{{ form_url }}"> <fieldset> - <legend>{{ hawat_current_view.get_view_title() }}</legend> + <legend>{{ vial_current_view.get_view_title() }}</legend> {%- if item_action == 'createfor' %} diff --git a/lib/hawat/blueprints/filters/templates/filters/playground.html b/lib/hawat/blueprints/filters/templates/filters/playground.html index 96f9453bafd43b4e10fcbe45bd3dd4523e1b8f24..70c917eaf55361485a753e0353f78864605fc0d9 100644 --- a/lib/hawat/blueprints/filters/templates/filters/playground.html +++ b/lib/hawat/blueprints/filters/templates/filters/playground.html @@ -7,7 +7,7 @@ {{ macros_page.render_breadcrumbs(item) }} <div class="jumbotron" style="margin-top: 1em;"> - <h2>{{ hawat_current_view.get_view_title() }}</h2> + <h2>{{ vial_current_view.get_view_title() }}</h2> <hr> <form method="POST" class="form-horizontal" id="form-events-simple" action="{{ form_url }}"> {{ macros_form.render_form_item_default(form.filter, placeholder = _('Write your filtering rule here...')) }} diff --git a/lib/hawat/blueprints/filters/templates/filters/show.html b/lib/hawat/blueprints/filters/templates/filters/show.html index 5b9855dc14c231e9a4c201026834d670c9f14c78..bf465587d83e5ad2caa6a6fc949a60c50c05c931 100644 --- a/lib/hawat/blueprints/filters/templates/filters/show.html +++ b/lib/hawat/blueprints/filters/templates/filters/show.html @@ -6,7 +6,7 @@ <div class="col-lg-12"> {{ macros_page.render_breadcrumbs(item) }} - <h2>{{ hawat_current_view.get_view_title() }}</h2> + <h2>{{ vial_current_view.get_view_title() }}</h2> <hr> <h3>{{ item.name }}</h3> <div class="pull-right"> diff --git a/lib/hawat/blueprints/geoip/__init__.py b/lib/hawat/blueprints/geoip/__init__.py index b2733641a9498b66fe15382c903de05e4693c5f1..72bd723bf9cb3ce59da25708082fe33af08c6b80 100644 --- a/lib/hawat/blueprints/geoip/__init__.py +++ b/lib/hawat/blueprints/geoip/__init__.py @@ -63,10 +63,13 @@ from flask_babel import lazy_gettext import mentat.services.geoip from mentat.const import tr_ -import hawat.const import hawat.db -import hawat.acl -from hawat.base import HTMLMixin, AJAXMixin, SnippetMixin, RenderableView, VialBlueprint, URLParamsBuilder +import vial.const +import vial.acl +from vial.app import VialBlueprint +from vial.view import RenderableView +from vial.view.mixin import HTMLMixin, AJAXMixin, SnippetMixin +from vial.utils import URLParamsBuilder from hawat.blueprints.geoip.forms import GeoipSearchForm @@ -83,16 +86,16 @@ class AbstractSearchView(RenderableView): # pylint: disable=locally-disabled,ab """ authentication = True - authorization = [hawat.acl.PERMISSION_ANY] + authorization = [vial.acl.PERMISSION_ANY] @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" return lazy_gettext('Search GeoIP') @classmethod def get_menu_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('Search GeoIP') #--------------------------------------------------------------------------- @@ -103,7 +106,7 @@ class AbstractSearchView(RenderableView): # pylint: disable=locally-disabled,ab Will be called by the *Flask* framework to service the request. """ form = GeoipSearchForm(flask.request.args, meta = {'csrf': False}) - if hawat.const.FORM_ACTION_SUBMIT in flask.request.args: + if vial.const.FORM_ACTION_SUBMIT in flask.request.args: if form.validate(): form_data = form.data geoip_service = mentat.services.geoip.service() @@ -137,7 +140,7 @@ class SearchView(HTMLMixin, AbstractSearchView): # pylint: disable=locally-disa @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" return 'search' @@ -150,7 +153,7 @@ class APISearchView(AJAXMixin, AbstractSearchView): # pylint: disable=locally-d @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" return 'apisearch' @@ -174,7 +177,7 @@ class SnippetSearchView(SnippetMixin, AbstractSearchView): # pylint: disable=lo @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" return 'sptsearch' @@ -188,18 +191,18 @@ class GeoipBlueprint(VialBlueprint): @classmethod def get_module_title(cls): - """*Implementation* of :py:func:`hawat.base.VialBlueprint.get_module_title`.""" + """*Implementation* of :py:func:`vial.app.VialBlueprint.get_module_title`.""" return lazy_gettext('IP geolocation service') def register_app(self, app): """ - *Callback method*. Will be called from :py:func:`hawat.base.Vial.register_blueprint` + *Callback method*. Will be called from :py:func:`hawat.base.HawatApp.register_blueprint` method and can be used to customize the Flask application object. Possible use cases: * application menu customization - :param hawat.base.Vial app: Flask application to be customized. + :param hawat.base.HawatApp app: Flask application to be customized. """ mentat.services.geoip.init(app.mconfig) @@ -237,7 +240,7 @@ class GeoipBlueprint(VialBlueprint): def get_blueprint(): """ Mandatory interface and factory function. This function must return a valid - instance of :py:class:`hawat.base.VialBlueprint` or :py:class:`flask.Blueprint`. + instance of :py:class:`vial.app.VialBlueprint` or :py:class:`flask.Blueprint`. """ hbp = GeoipBlueprint( diff --git a/lib/hawat/blueprints/geoip/forms.py b/lib/hawat/blueprints/geoip/forms.py index ef5984718a970a51d73294b9b6e544d4dad19bbc..1ab72bbf80f339c2ce3acf84a8111b0fc50f2b74 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 hawat.forms +import vial.forms class GeoipSearchForm(flask_wtf.FlaskForm): @@ -32,7 +32,7 @@ class GeoipSearchForm(flask_wtf.FlaskForm): lazy_gettext('Search GeoIP:'), validators = [ wtforms.validators.DataRequired(), - hawat.forms.check_ip_record + vial.forms.check_ip_record ] ) submit = wtforms.SubmitField( diff --git a/lib/hawat/blueprints/geoip/templates/geoip/search.html b/lib/hawat/blueprints/geoip/templates/geoip/search.html index 85268def7bc2e7b0c8056e3fbc74b0f6f258391a..9fd4069aa36b329d0eec81bd0ff9fad5a9f22401 100644 --- a/lib/hawat/blueprints/geoip/templates/geoip/search.html +++ b/lib/hawat/blueprints/geoip/templates/geoip/search.html @@ -7,7 +7,7 @@ <div class="col-lg-12"> <div class="jumbotron" style="margin-top: 1em;"> - <h2>{{ hawat_current_view.get_view_title() }}</h2> + <h2>{{ vial_current_view.get_view_title() }}</h2> <form method="GET" class="form-inline" action="{{ url_for('geoip.search') }}"> <div class="form-group{% if search_form.search.errors %}{{ ' has-error' }}{% endif %}"> {{ search_form.search.label(class_='sr-only') }} diff --git a/lib/hawat/blueprints/groups/__init__.py b/lib/hawat/blueprints/groups/__init__.py index 7bbc1d5484d462b5294c4b3f27bd85f6644d38ba..56d4a58c903359a770448a011d0c868323c30920 100644 --- a/lib/hawat/blueprints/groups/__init__.py +++ b/lib/hawat/blueprints/groups/__init__.py @@ -45,14 +45,13 @@ from mentat.datatype.sqldb import UserModel, GroupModel, SettingsReportingModel, FilterModel, NetworkModel, ItemChangeLogModel from mentat.const import tr_ -import hawat.acl import hawat.db -import hawat.menu -from hawat.base import HTMLMixin, SQLAlchemyMixin, ItemListView,\ - ItemShowView, ItemCreateView, ItemUpdateView, ItemEnableView,\ - ItemDisableView, ItemObjectRelationView, ItemDeleteView, VialBlueprint,\ - URLParamsBuilder - +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.utils import URLParamsBuilder from hawat.blueprints.groups.forms import AdminCreateGroupForm, AdminUpdateGroupForm,\ UpdateGroupForm @@ -70,24 +69,24 @@ class ListView(HTMLMixin, SQLAlchemyMixin, ItemListView): authentication = True - authorization = [hawat.acl.PERMISSION_POWER] + authorization = [vial.acl.PERMISSION_POWER] @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('Group management') #--------------------------------------------------------------------------- @property def dbmodel(self): - """*Implementation* of :py:func:`hawat.base.SQLAlchemyMixin.dbmodel`.""" + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" return GroupModel @classmethod def get_action_menu(cls): - """*Implementation* of :py:func:`hawat.base.ItemListView.get_action_menu`.""" - action_menu = hawat.menu.Menu() + """*Implementation* of :py:func:`vial.view.ItemListView.get_action_menu`.""" + action_menu = vial.menu.Menu() action_menu.add_entry( 'endpoint', 'create', @@ -98,8 +97,8 @@ class ListView(HTMLMixin, SQLAlchemyMixin, ItemListView): @classmethod def get_context_action_menu(cls): - """*Implementation* of :py:func:`hawat.base.ItemListView.get_context_action_menu`.""" - action_menu = hawat.menu.Menu() + """*Implementation* of :py:func:`vial.view.ItemListView.get_context_action_menu`.""" + action_menu = vial.menu.Menu() action_menu.add_entry( 'endpoint', 'show', @@ -144,19 +143,19 @@ class ShowView(HTMLMixin, SQLAlchemyMixin, ItemShowView): @classmethod def get_menu_legend(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" if isinstance(kwargs['item'], GroupModel): return lazy_gettext('View details of group "%(item)s"', item = str(kwargs['item'])) return lazy_gettext('View details of group "%(item)s"', item = str(kwargs['item'].group)) @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" return lazy_gettext('Show group details') @classmethod def get_view_url(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_url`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_url`.""" if isinstance(kwargs['item'], GroupModel): 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()) @@ -165,7 +164,7 @@ class ShowView(HTMLMixin, SQLAlchemyMixin, ItemShowView): @property def dbmodel(self): - """*Implementation* of :py:func:`hawat.base.SQLAlchemyMixin.dbmodel`.""" + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" return GroupModel @classmethod @@ -174,17 +173,17 @@ class ShowView(HTMLMixin, SQLAlchemyMixin, ItemShowView): Perform access authorization for current user to particular item. """ permission_mm = flask_principal.Permission( - hawat.acl.MembershipNeed(kwargs['item'].id), - hawat.acl.ManagementNeed(kwargs['item'].id) + vial.acl.MembershipNeed(kwargs['item'].id), + vial.acl.ManagementNeed(kwargs['item'].id) ) - return hawat.acl.PERMISSION_POWER.can() or permission_mm.can() + return vial.acl.PERMISSION_POWER.can() or permission_mm.can() @classmethod def get_action_menu(cls): """ Get action menu for particular item. """ - action_menu = hawat.menu.Menu() + action_menu = vial.menu.Menu() action_menu.add_entry( 'endpoint', 'update', @@ -228,8 +227,8 @@ class ShowView(HTMLMixin, SQLAlchemyMixin, ItemShowView): return action_menu def do_before_response(self, **kwargs): # pylint: disable=locally-disabled,no-self-use,unused-argument - """*Implementation* of :py:func:`hawat.base.RenderableView.do_before_response`.""" - action_menu = hawat.menu.Menu() + """*Implementation* of :py:func:`vial.view.RenderableView.do_before_response`.""" + action_menu = vial.menu.Menu() action_menu.add_entry( 'endpoint', 'show', @@ -274,7 +273,7 @@ class ShowView(HTMLMixin, SQLAlchemyMixin, ItemShowView): ) self.response_context.update(context_action_menu_users = action_menu) - action_menu = hawat.menu.Menu() + action_menu = vial.menu.Menu() action_menu.add_entry( 'endpoint', 'show', @@ -283,7 +282,7 @@ class ShowView(HTMLMixin, SQLAlchemyMixin, ItemShowView): ) self.response_context.update(context_action_menu_networks = action_menu) - action_menu = hawat.menu.Menu() + action_menu = vial.menu.Menu() action_menu.add_entry( 'endpoint', 'show', @@ -342,18 +341,18 @@ class ShowByNameView(ShowView): # pylint: disable=locally-disabled,too-many-anc @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" return 'show_by_name' @classmethod def get_view_template(cls): - """*Implementation* of :py:func:`hawat.base.RenderableView.get_view_template`.""" + """*Implementation* of :py:func:`vial.view.RenderableView.get_view_template`.""" return '{}/show.html'.format(cls.module_name) @property def search_by(self): """ - *Interface implementation* of :py:func:`hawat.base.ItemShowView.search_by`. + *Interface implementation* of :py:func:`vial.view.ItemShowView.search_by`. """ return self.dbmodel.name @@ -367,45 +366,50 @@ class CreateView(HTMLMixin, SQLAlchemyMixin, ItemCreateView): # pylint: disable authentication = True - authorization = [hawat.acl.PERMISSION_POWER] + authorization = [vial.acl.PERMISSION_POWER] @classmethod def get_menu_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('Create group') @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" return lazy_gettext('Create new group') #--------------------------------------------------------------------------- @property def dbmodel(self): - """*Implementation* of :py:func:`hawat.base.SQLAlchemyMixin.dbmodel`.""" + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" return GroupModel + @property + def dbchlogmodel(self): + """*Implementation* of :py:func:`vial.view.ItemActionView.dbchlogmodel`.""" + return ItemChangeLogModel + #--------------------------------------------------------------------------- @staticmethod def get_message_success(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_success`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_success`.""" return gettext('Group <strong>%(item_id)s</strong> was successfully created.', item_id = str(kwargs['item'])) @staticmethod def get_message_failure(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_failure`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_failure`.""" return gettext('Unable to create new group.') @staticmethod def get_message_cancel(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_cancel`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_cancel`.""" return gettext('Canceled creating new group.') @staticmethod def get_item_form(): - """*Implementation* of :py:func:`hawat.base.ItemCreateView.get_item_form`.""" + """*Implementation* of :py:func:`vial.view.ItemCreateView.get_item_form`.""" return AdminCreateGroupForm() def do_before_action(self, item): @@ -428,56 +432,61 @@ class UpdateView(HTMLMixin, SQLAlchemyMixin, ItemUpdateView): # pylint: disable @classmethod def get_menu_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('Update') @classmethod def get_menu_legend(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('Update details of group "%(item)s"', item = str(kwargs['item'])) @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" return lazy_gettext('Update group details') #--------------------------------------------------------------------------- @property def dbmodel(self): - """*Implementation* of :py:func:`hawat.base.SQLAlchemyMixin.dbmodel`.""" + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" return GroupModel + @property + def dbchlogmodel(self): + """*Implementation* of :py:func:`vial.view.ItemActionView.dbchlogmodel`.""" + return ItemChangeLogModel + @classmethod def authorize_item_action(cls, **kwargs): """ Perform access authorization for current user to particular item. """ permission_m = flask_principal.Permission( - hawat.acl.ManagementNeed(kwargs['item'].id) + vial.acl.ManagementNeed(kwargs['item'].id) ) - return hawat.acl.PERMISSION_POWER.can() or permission_m.can() + return vial.acl.PERMISSION_POWER.can() or permission_m.can() #--------------------------------------------------------------------------- @staticmethod def get_message_success(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_success`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_success`.""" return gettext('Group <strong>%(item_id)s</strong> was successfully updated.', item_id = str(kwargs['item'])) @staticmethod def get_message_failure(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_failure`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_failure`.""" return gettext('Unable to update group <strong>%(item_id)s</strong>.', item_id = str(kwargs['item'])) @staticmethod def get_message_cancel(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_cancel`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_cancel`.""" return gettext('Canceled updating group <strong>%(item_id)s</strong>.', item_id = str(kwargs['item'])) @staticmethod def get_item_form(item): - """*Implementation* of :py:func:`hawat.base.ItemUpdateView.get_item_form`.""" + """*Implementation* of :py:func:`vial.view.ItemUpdateView.get_item_form`.""" admin = flask_login.current_user.has_role('admin') if not admin: form = UpdateGroupForm(obj = item) @@ -500,17 +509,17 @@ class AddMemberView(HTMLMixin, SQLAlchemyMixin, ItemObjectRelationView): # pyli @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" return gettext('Add group member') @classmethod def get_view_icon(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_icon`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_icon`.""" return 'action-add-member' @classmethod def get_menu_legend(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext( 'Add user "%(user_id)s" to group "%(group_id)s"', user_id = str(kwargs['other']), @@ -521,9 +530,14 @@ class AddMemberView(HTMLMixin, SQLAlchemyMixin, ItemObjectRelationView): # pyli @property def dbmodel(self): - """*Implementation* of :py:func:`hawat.base.SQLAlchemyMixin.dbmodel`.""" + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" return GroupModel + @property + def dbchlogmodel(self): + """*Implementation* of :py:func:`vial.view.ItemActionView.dbchlogmodel`.""" + return ItemChangeLogModel + @property def dbmodel_other(self): """*Implementation* of :py:func:`hawat.base.AddMemberView.dbmodel_other`.""" @@ -537,9 +551,9 @@ class AddMemberView(HTMLMixin, SQLAlchemyMixin, ItemObjectRelationView): # pyli Perform access authorization for current user to particular item. """ permission_m = flask_principal.Permission( - hawat.acl.ManagementNeed(kwargs['item'].id) + vial.acl.ManagementNeed(kwargs['item'].id) ) - return hawat.acl.PERMISSION_POWER.can() or permission_m.can() + return vial.acl.PERMISSION_POWER.can() or permission_m.can() @classmethod def validate_item_change(cls, **kwargs): # pylint: disable=locally-disabled,unused-argument @@ -554,7 +568,7 @@ class AddMemberView(HTMLMixin, SQLAlchemyMixin, ItemObjectRelationView): # pyli @classmethod def change_item(cls, **kwargs): """ - *Interface implementation* of :py:func:`hawat.base.ItemChangeView.change_item`. + *Interface implementation* of :py:func:`vial.view.ItemChangeView.change_item`. """ kwargs['item'].members.append(kwargs['other']) try: @@ -569,7 +583,7 @@ class AddMemberView(HTMLMixin, SQLAlchemyMixin, ItemObjectRelationView): # pyli @staticmethod def get_message_success(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_success`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_success`.""" return gettext( 'User <strong>%(user_id)s</strong> was successfully added as a member to group <strong>%(group_id)s</strong>.', user_id = str(kwargs['other']), @@ -578,7 +592,7 @@ class AddMemberView(HTMLMixin, SQLAlchemyMixin, ItemObjectRelationView): # pyli @staticmethod def get_message_failure(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_failure`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_failure`.""" return gettext( 'Unable to add user <strong>%(user_id)s</strong> as a member to group <strong>%(group_id)s</strong>.', user_id = str(kwargs['other']), @@ -587,7 +601,7 @@ class AddMemberView(HTMLMixin, SQLAlchemyMixin, ItemObjectRelationView): # pyli @staticmethod def get_message_cancel(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_cancel`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_cancel`.""" return gettext( 'Canceled adding user <strong>%(user_id)s</strong> as a member to group <strong>%(group_id)s</strong>.', user_id = str(kwargs['other']), @@ -609,17 +623,17 @@ class RejectMemberView(HTMLMixin, SQLAlchemyMixin, ItemObjectRelationView): # p @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" return gettext('Reject group member') @classmethod def get_view_icon(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_icon`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_icon`.""" return 'action-rej-member' @classmethod def get_menu_legend(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext( 'Reject user`s "%(user_id)s" membership request for group "%(group_id)s"', user_id = str(kwargs['other']), @@ -630,9 +644,14 @@ class RejectMemberView(HTMLMixin, SQLAlchemyMixin, ItemObjectRelationView): # p @property def dbmodel(self): - """*Implementation* of :py:func:`hawat.base.SQLAlchemyMixin.dbmodel`.""" + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" return GroupModel + @property + def dbchlogmodel(self): + """*Implementation* of :py:func:`vial.view.ItemActionView.dbchlogmodel`.""" + return ItemChangeLogModel + @property def dbmodel_other(self): """*Implementation* of :py:func:`hawat.base.AddMemberView.dbmodel_other`.""" @@ -646,9 +665,9 @@ class RejectMemberView(HTMLMixin, SQLAlchemyMixin, ItemObjectRelationView): # p Perform access authorization for current user to particular item. """ permission_m = flask_principal.Permission( - hawat.acl.ManagementNeed(kwargs['item'].id) + vial.acl.ManagementNeed(kwargs['item'].id) ) - return hawat.acl.PERMISSION_POWER.can() or permission_m.can() + return vial.acl.PERMISSION_POWER.can() or permission_m.can() @classmethod def validate_item_change(cls, **kwargs): # pylint: disable=locally-disabled,unused-argument @@ -663,7 +682,7 @@ class RejectMemberView(HTMLMixin, SQLAlchemyMixin, ItemObjectRelationView): # p @classmethod def change_item(cls, **kwargs): """ - *Interface implementation* of :py:func:`hawat.base.ItemChangeView.change_item`. + *Interface implementation* of :py:func:`vial.view.ItemChangeView.change_item`. """ kwargs['item'].members_wanted.remove(kwargs['other']) @@ -671,7 +690,7 @@ class RejectMemberView(HTMLMixin, SQLAlchemyMixin, ItemObjectRelationView): # p @staticmethod def get_message_success(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_success`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_success`.""" return gettext( 'User`s <strong>%(user_id)s</strong> membership request for group <strong>%(group_id)s</strong> was successfully rejected.', user_id = str(kwargs['other']), @@ -680,7 +699,7 @@ class RejectMemberView(HTMLMixin, SQLAlchemyMixin, ItemObjectRelationView): # p @staticmethod def get_message_failure(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_failure`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_failure`.""" return gettext( 'Unable to reject user`s <strong>%(user_id)s</strong> membership request for group <strong>%(group_id)s</strong>.', user_id = str(kwargs['other']), @@ -689,7 +708,7 @@ class RejectMemberView(HTMLMixin, SQLAlchemyMixin, ItemObjectRelationView): # p @staticmethod def get_message_cancel(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_cancel`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_cancel`.""" return gettext( 'Canceled rejecting user`s <strong>%(user_id)s</strong> membership request for group <strong>%(group_id)s</strong>.', user_id = str(kwargs['other']), @@ -711,17 +730,17 @@ class RemoveMemberView(HTMLMixin, SQLAlchemyMixin, ItemObjectRelationView): # p @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" return gettext('Remove group member') @classmethod def get_view_icon(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_icon`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_icon`.""" return 'action-rem-member' @classmethod def get_menu_legend(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext( 'Remove user "%(user_id)s" from group "%(group_id)s"', user_id = str(kwargs['other']), @@ -732,9 +751,14 @@ class RemoveMemberView(HTMLMixin, SQLAlchemyMixin, ItemObjectRelationView): # p @property def dbmodel(self): - """*Implementation* of :py:func:`hawat.base.SQLAlchemyMixin.dbmodel`.""" + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" return GroupModel + @property + def dbchlogmodel(self): + """*Implementation* of :py:func:`vial.view.ItemActionView.dbchlogmodel`.""" + return ItemChangeLogModel + @property def dbmodel_other(self): """*Implementation* of :py:func:`hawat.base.AddMemberView.dbmodel_other`.""" @@ -748,9 +772,9 @@ class RemoveMemberView(HTMLMixin, SQLAlchemyMixin, ItemObjectRelationView): # p Perform access authorization for current user to particular item. """ permission_m = flask_principal.Permission( - hawat.acl.ManagementNeed(kwargs['item'].id) + vial.acl.ManagementNeed(kwargs['item'].id) ) - return hawat.acl.PERMISSION_POWER.can() or permission_m.can() + return vial.acl.PERMISSION_POWER.can() or permission_m.can() @classmethod def validate_item_change(cls, **kwargs): # pylint: disable=locally-disabled,unused-argument @@ -765,7 +789,7 @@ class RemoveMemberView(HTMLMixin, SQLAlchemyMixin, ItemObjectRelationView): # p @classmethod def change_item(cls, **kwargs): """ - *Interface implementation* of :py:func:`hawat.base.ItemChangeView.change_item`. + *Interface implementation* of :py:func:`vial.view.ItemChangeView.change_item`. """ kwargs['item'].members.remove(kwargs['other']) @@ -773,7 +797,7 @@ class RemoveMemberView(HTMLMixin, SQLAlchemyMixin, ItemObjectRelationView): # p @staticmethod def get_message_success(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_success`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_success`.""" return gettext( 'User <strong>%(user_id)s</strong> was successfully removed as a member from group <strong>%(group_id)s</strong>.', user_id = str(kwargs['other']), @@ -782,7 +806,7 @@ class RemoveMemberView(HTMLMixin, SQLAlchemyMixin, ItemObjectRelationView): # p @staticmethod def get_message_failure(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_failure`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_failure`.""" return gettext( 'Unable to remove user <strong>%(user_id)s</strong> as a member from group <strong>%(group_id)s</strong>.', user_id = str(kwargs['other']), @@ -791,7 +815,7 @@ class RemoveMemberView(HTMLMixin, SQLAlchemyMixin, ItemObjectRelationView): # p @staticmethod def get_message_cancel(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_cancel`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_cancel`.""" return gettext( 'Canceled removing user <strong>%(user_id)s</strong> as a member from group <strong>%(group_id)s</strong>.', user_id = str(kwargs['other']), @@ -807,35 +831,40 @@ class EnableView(HTMLMixin, SQLAlchemyMixin, ItemEnableView): # pylint: disable authentication = True - authorization = [hawat.acl.PERMISSION_POWER] + authorization = [vial.acl.PERMISSION_POWER] @classmethod def get_menu_legend(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('Enable group "%(item)s"', item = str(kwargs['item'])) #--------------------------------------------------------------------------- @property def dbmodel(self): - """*Implementation* of :py:func:`hawat.base.SQLAlchemyMixin.dbmodel`.""" + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" return GroupModel + @property + def dbchlogmodel(self): + """*Implementation* of :py:func:`vial.view.ItemActionView.dbchlogmodel`.""" + return ItemChangeLogModel + #--------------------------------------------------------------------------- @staticmethod def get_message_success(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_success`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_success`.""" return gettext('Group <strong>%(item_id)s</strong> was successfully enabled.', item_id = str(kwargs['item'])) @staticmethod def get_message_failure(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_failure`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_failure`.""" return gettext('Unable to enable group <strong>%(item_id)s</strong>.', item_id = str(kwargs['item'])) @staticmethod def get_message_cancel(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_cancel`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_cancel`.""" return gettext('Canceled enabling group <strong>%(item_id)s</strong>.', item_id = str(kwargs['item'])) @@ -847,35 +876,40 @@ class DisableView(HTMLMixin, SQLAlchemyMixin, ItemDisableView): # pylint: disab authentication = True - authorization = [hawat.acl.PERMISSION_POWER] + authorization = [vial.acl.PERMISSION_POWER] @classmethod def get_menu_legend(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('Disable group "%(item)s"', item = str(kwargs['item'])) #--------------------------------------------------------------------------- @property def dbmodel(self): - """*Implementation* of :py:func:`hawat.base.SQLAlchemyMixin.dbmodel`.""" + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" return GroupModel + @property + def dbchlogmodel(self): + """*Implementation* of :py:func:`vial.view.ItemActionView.dbchlogmodel`.""" + return ItemChangeLogModel + #--------------------------------------------------------------------------- @staticmethod def get_message_success(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_success`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_success`.""" return gettext('Group <strong>%(item_id)s</strong> was successfully disabled.', item_id = str(kwargs['item'])) @staticmethod def get_message_failure(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_failure`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_failure`.""" return gettext('Unable to disable group <strong>%(item_id)s</strong>.', item_id = str(kwargs['item'])) @staticmethod def get_message_cancel(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_cancel`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_cancel`.""" return gettext('Canceled disabling group <strong>%(item_id)s</strong>.', item_id = str(kwargs['item'])) @@ -888,12 +922,12 @@ class DeleteView(HTMLMixin, SQLAlchemyMixin, ItemDeleteView): # pylint: disable authentication = True - authorization = [hawat.acl.PERMISSION_ADMIN] + authorization = [vial.acl.PERMISSION_ADMIN] @classmethod def get_menu_legend(cls, **kwargs): """ - *Interface implementation* of :py:func:`hawat.base.BaseView.get_menu_legend`. + *Interface implementation* of :py:func:`vial.view.BaseView.get_menu_legend`. """ return lazy_gettext('Delete group "%(item)s"', item = str(kwargs['item'])) @@ -901,24 +935,29 @@ class DeleteView(HTMLMixin, SQLAlchemyMixin, ItemDeleteView): # pylint: disable @property def dbmodel(self): - """*Implementation* of :py:func:`hawat.base.SQLAlchemyMixin.dbmodel`.""" + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" return GroupModel + @property + def dbchlogmodel(self): + """*Implementation* of :py:func:`vial.view.ItemActionView.dbchlogmodel`.""" + return ItemChangeLogModel + #--------------------------------------------------------------------------- @staticmethod def get_message_success(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_success`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_success`.""" return gettext('Group <strong>%(item_id)s</strong> was successfully and permanently deleted.', item_id = str(kwargs['item'])) @staticmethod def get_message_failure(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_failure`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_failure`.""" return gettext('Unable to delete group <strong>%(item_id)s</strong>.', item_id = str(kwargs['item'])) @staticmethod def get_message_cancel(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_cancel`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_cancel`.""" return gettext('Canceled deleting group <strong>%(item_id)s</strong>.', item_id = str(kwargs['item'])) @@ -932,18 +971,18 @@ class GroupsBlueprint(VialBlueprint): @classmethod def get_module_title(cls): - """*Implementation* of :py:func:`hawat.base.VialBlueprint.get_module_title`.""" + """*Implementation* of :py:func:`vial.app.VialBlueprint.get_module_title`.""" return lazy_gettext('Group management pluggable module') def register_app(self, app): """ - *Callback method*. Will be called from :py:func:`hawat.base.Vial.register_blueprint` + *Callback method*. Will be called from :py:func:`hawat.base.HawatApp.register_blueprint` method and can be used to customize the Flask application object. Possible use cases: * application menu customization - :param hawat.base.Vial app: Flask application to be customized. + :param hawat.base.HawatApp app: Flask application to be customized. """ def _fetch_my_groups(): @@ -967,7 +1006,7 @@ class GroupsBlueprint(VialBlueprint): icon = 'module-groups', align_right = True, entry_fetcher = _fetch_my_groups, - entry_builder = lambda x, y: hawat.menu.EndpointEntry(x, endpoint = 'groups.show', params = {'item': y}, title = x, icon = 'module-groups') + entry_builder = lambda x, y: vial.menu.EndpointEntry(x, endpoint = 'groups.show', params = {'item': y}, title = x, icon = 'module-groups') ) # Register context actions provided by this module. @@ -985,7 +1024,7 @@ class GroupsBlueprint(VialBlueprint): def get_blueprint(): """ Mandatory interface and factory function. This function must return a valid - instance of :py:class:`hawat.base.VialBlueprint` or :py:class:`flask.Blueprint`. + instance of :py:class:`vial.app.VialBlueprint` or :py:class:`flask.Blueprint`. """ hbp = GroupsBlueprint( diff --git a/lib/hawat/blueprints/groups/forms.py b/lib/hawat/blueprints/groups/forms.py index 65f1f75d687208aabf1dfb94e3e2e4118d259ff3..f3436f6a86de005479f8be812c9890a9b2110a51 100644 --- a/lib/hawat/blueprints/groups/forms.py +++ b/lib/hawat/blueprints/groups/forms.py @@ -30,7 +30,7 @@ from flask_babel import gettext, lazy_gettext # Custom modules. # import hawat.db -import hawat.forms +import vial.forms from mentat.datatype.sqldb import GroupModel, UserModel @@ -91,7 +91,7 @@ def get_available_groups(): return hawat.db.db_query(GroupModel).order_by(GroupModel.name).all() -class BaseGroupForm(hawat.forms.BaseItemForm): +class BaseGroupForm(vial.forms.BaseItemForm): """ Class representing base group form. """ @@ -119,8 +119,8 @@ class BaseGroupForm(hawat.forms.BaseItemForm): (True, lazy_gettext('Enabled')), (False, lazy_gettext('Disabled')) ], - filters = [hawat.forms.str_to_bool], - coerce = hawat.forms.str_to_bool, + filters = [vial.forms.str_to_bool], + coerce = vial.forms.str_to_bool, description = lazy_gettext('Boolean flag whether the group is self managed by group managers. When enabled group managers are expected to take care of the group management tasks and they get notifications about important events like group membership requests, etc.') ) members = QuerySelectMultipleField( @@ -158,8 +158,8 @@ class AdminBaseGroupForm(BaseGroupForm): (True, lazy_gettext('Enabled')), (False, lazy_gettext('Disabled')) ], - filters = [hawat.forms.str_to_bool], - coerce = hawat.forms.str_to_bool, + 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( diff --git a/lib/hawat/blueprints/groups/templates/groups/creatupdate.html b/lib/hawat/blueprints/groups/templates/groups/creatupdate.html index 505f0f4054d9c1de47982bf9f27d93a32d1f4988..b9f1c7b1951925c2fe1d523ca02b4a3257a56bcd 100644 --- a/lib/hawat/blueprints/groups/templates/groups/creatupdate.html +++ b/lib/hawat/blueprints/groups/templates/groups/creatupdate.html @@ -18,7 +18,7 @@ <form method="POST" action="{{ form_url }}"> <fieldset> - <legend>{{ hawat_current_view.get_view_title() }}</legend> + <legend>{{ vial_current_view.get_view_title() }}</legend> {%- if item_action == 'create' or current_user.has_role('admin') %} {{ macros_form.render_form_item_default(form.name) }} diff --git a/lib/hawat/blueprints/groups/templates/groups/show.html b/lib/hawat/blueprints/groups/templates/groups/show.html index 7439892f64e48b0e1a0a0f8920b6b4f777272a06..6f1ad1bea0e0065ecc2f7c0a007d81d712574dc7 100644 --- a/lib/hawat/blueprints/groups/templates/groups/show.html +++ b/lib/hawat/blueprints/groups/templates/groups/show.html @@ -6,7 +6,7 @@ <div class="col-lg-12"> {{ macros_page.render_breadcrumbs(item) }} - <h2>{{ hawat_current_view.get_view_title() }}</h2> + <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"> diff --git a/lib/hawat/blueprints/home/__init__.py b/lib/hawat/blueprints/home/__init__.py index f0ad33f2460623fe8cafdd91f3a0fe7ab665d996..41090dfe9ff4bb3812400ba52a1bfd274592d785 100644 --- a/lib/hawat/blueprints/home/__init__.py +++ b/lib/hawat/blueprints/home/__init__.py @@ -38,7 +38,9 @@ from flask_babel import lazy_gettext # # Custom modules. # -from hawat.base import HTMLMixin, SimpleView, VialBlueprint +from vial.app import VialBlueprint +from vial.view import SimpleView +from vial.view.mixin import HTMLMixin BLUEPRINT_NAME = 'home' @@ -53,22 +55,22 @@ class IndexView(HTMLMixin, SimpleView): @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" return 'index' @classmethod def get_view_icon(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_icon`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_icon`.""" return 'module-home' @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" return lazy_gettext('Welcome to Mentat!') @classmethod def get_menu_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('Home') @@ -82,7 +84,7 @@ class HomeBlueprint(VialBlueprint): @classmethod def get_module_title(cls): - """*Implementation* of :py:func:`hawat.base.VialBlueprint.get_module_title`.""" + """*Implementation* of :py:func:`vial.app.VialBlueprint.get_module_title`.""" return lazy_gettext('Home page pluggable module') @@ -92,7 +94,7 @@ class HomeBlueprint(VialBlueprint): def get_blueprint(): """ Mandatory interface and factory function. This function must return a valid - instance of :py:class:`hawat.base.VialBlueprint` or :py:class:`flask.Blueprint`. + instance of :py:class:`vial.app.VialBlueprint` or :py:class:`flask.Blueprint`. """ hbp = HomeBlueprint( diff --git a/lib/hawat/blueprints/home/templates/home/index.html b/lib/hawat/blueprints/home/templates/home/index.html index 5688fee5c6f2f2c519485debd4391316bc3ca1f0..77c24e8de4d1ebc0b329a7d90c31b1ede97ff269 100644 --- a/lib/hawat/blueprints/home/templates/home/index.html +++ b/lib/hawat/blueprints/home/templates/home/index.html @@ -1,13 +1,13 @@ {% extends "_layout.html" %} -{% block title %}{{ hawat_current_view.get_view_title() }}{% endblock %} +{% 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"> - {{ hawat_current_view.get_view_title() }} + {{ vial_current_view.get_view_title() }} </h1> <hr> <br> diff --git a/lib/hawat/blueprints/hosts/__init__.py b/lib/hawat/blueprints/hosts/__init__.py index aeed2af526057fc48a6e48e6b03693a4b089d043..90225cd9d474c7fbc895aae8b8fb97ff183f616b 100644 --- a/lib/hawat/blueprints/hosts/__init__.py +++ b/lib/hawat/blueprints/hosts/__init__.py @@ -36,11 +36,14 @@ import mentat.stats.idea import mentat.services.eventstorage from mentat.const import tr_ -import hawat.const import hawat.events -import hawat.acl -from hawat.base import HTMLMixin, PsycopgMixin, AJAXMixin,\ - BaseSearchView, VialBlueprint, URLParamsBuilder +import vial.const +import vial.acl +from vial.app import VialBlueprint +from vial.view import BaseSearchView +from vial.view.mixin import HTMLMixin, AJAXMixin +from vial.utils import URLParamsBuilder +from hawat.base import PsycopgMixin from hawat.blueprints.hosts.forms import SimpleHostSearchForm @@ -54,22 +57,27 @@ class AbstractSearchView(PsycopgMixin, BaseSearchView): # pylint: disable=local """ authentication = True - authorization = [hawat.acl.PERMISSION_POWER] + authorization = [vial.acl.PERMISSION_POWER] @classmethod def get_menu_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('Hosts') @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" return lazy_gettext('Search hosts') + @classmethod + def get_view_icon(cls): + """*Implementation* of :py:func:`vial.view.BaseView.get_view_icon`.""" + return 'module-{}'.format(cls.module_name) + @staticmethod def get_search_form(request_args): """ - *Interface implementation* of :py:func:`hawat.base.SearchView.get_search_form`. + *Interface implementation* of :py:func:`vial.view.BaseSearchView.get_search_form`. """ # Get lists of available options for various event search form select fields. @@ -80,7 +88,7 @@ class AbstractSearchView(PsycopgMixin, BaseSearchView): # pylint: disable=local def do_after_search(self, items): """ - *Interface implementation* of :py:func:`hawat.base.SearchView.do_after_search`. + *Interface implementation* of :py:func:`vial.view.BaseSearchView.do_after_search`. """ self.logger.debug( "Calculating host statistics from %d records.", @@ -113,7 +121,7 @@ class AbstractSearchView(PsycopgMixin, BaseSearchView): # pylint: disable=local self.response_context.pop('items', None) def do_before_response(self, **kwargs): - """*Implementation* of :py:func:`hawat.base.RenderableView.do_before_response`.""" + """*Implementation* of :py:func:`vial.view.RenderableView.do_before_response`.""" self.response_context.update( quicksearch_list = self.get_quicksearch_by_time() ) @@ -124,7 +132,7 @@ class AbstractSearchView(PsycopgMixin, BaseSearchView): # pylint: disable=local @staticmethod def get_event_columns(): - columns = list(mentat.services.eventstorage.EVENT_COLUMNS) + columns = list(mentat.services.eventstorage.EVENTS_COLUMNS) columns.remove('event') return columns @@ -139,13 +147,13 @@ class SearchView(HTMLMixin, AbstractSearchView): # pylint: disable=locally-disa @classmethod def get_breadcrumbs_menu(cls): """ - *Interface implementation* of :py:func:`hawat.base.SearchView.get_breadcrumbs_menu`. + *Interface implementation* of :py:func:`vial.view.BaseSearchView.get_breadcrumbs_menu`. """ - breadcrumbs_menu = hawat.menu.Menu() + breadcrumbs_menu = vial.menu.Menu() breadcrumbs_menu.add_entry( 'endpoint', 'home', - endpoint = flask.current_app.config['HAWAT_ENDPOINT_HOME'] + endpoint = flask.current_app.config['ENDPOINT_HOME'] ) breadcrumbs_menu.add_entry( 'endpoint', @@ -164,7 +172,7 @@ class APISearchView(AJAXMixin, AbstractSearchView): # pylint: disable=locally-d @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" return 'apisearch' @@ -178,18 +186,18 @@ class HostsBlueprint(VialBlueprint): @classmethod def get_module_title(cls): - """*Implementation* of :py:func:`hawat.base.VialBlueprint.get_module_title`.""" + """*Implementation* of :py:func:`vial.app.VialBlueprint.get_module_title`.""" return lazy_gettext('Host overview pluggable module') def register_app(self, app): """ - *Callback method*. Will be called from :py:func:`hawat.base.Vial.register_blueprint` + *Callback method*. Will be called from :py:func:`hawat.base.HawatApp.register_blueprint` method and can be used to customize the Flask application object. Possible use cases: * application menu customization - :param hawat.base.Vial app: Flask application to be customized. + :param hawat.base.HawatApp app: Flask application to be customized. """ app.menu_main.add_entry( 'view', @@ -213,7 +221,7 @@ class HostsBlueprint(VialBlueprint): def get_blueprint(): """ Mandatory interface and factory function. This function must return a valid - instance of :py:class:`hawat.base.VialBlueprint` or :py:class:`flask.Blueprint`. + instance of :py:class:`vial.app.VialBlueprint` or :py:class:`flask.Blueprint`. """ hbp = HostsBlueprint( diff --git a/lib/hawat/blueprints/hosts/forms.py b/lib/hawat/blueprints/hosts/forms.py index 06bc78151d0f3b0e05b13127ff1b7ae46907ca60..81c934ccacc7117a0b7c371783aa36099ac04d19 100644 --- a/lib/hawat/blueprints/hosts/forms.py +++ b/lib/hawat/blueprints/hosts/forms.py @@ -22,9 +22,8 @@ import wtforms import flask_wtf from flask_babel import lazy_gettext -import hawat.const -import hawat.db -import hawat.forms +import vial.const +import vial.forms class SimpleHostSearchForm(flask_wtf.FlaskForm): @@ -35,16 +34,16 @@ class SimpleHostSearchForm(flask_wtf.FlaskForm): lazy_gettext('Host address:'), validators = [ wtforms.validators.Required(), - hawat.forms.check_ip_record + vial.forms.check_ip_record ] ) - dt_from = hawat.forms.SmartDateTimeField( + dt_from = vial.forms.SmartDateTimeField( lazy_gettext('Detection time from:'), validators = [ wtforms.validators.Optional() ] ) - dt_to = hawat.forms.SmartDateTimeField( + dt_to = vial.forms.SmartDateTimeField( lazy_gettext('Detection time to:'), validators = [ wtforms.validators.Optional() diff --git a/lib/hawat/blueprints/hosts/templates/hosts/search.html b/lib/hawat/blueprints/hosts/templates/hosts/search.html index 954ac5b3ebc19c87489b0018f053695caab2cb6b..3d6c64293ed02963e88db4901106a790866ab1cc 100644 --- a/lib/hawat/blueprints/hosts/templates/hosts/search.html +++ b/lib/hawat/blueprints/hosts/templates/hosts/search.html @@ -9,7 +9,7 @@ <!-- Search form - BEGIN ----------------------------------> <div class="jumbotron" style="margin-top: 1em;"> - <h2>{{ hawat_current_view.get_view_title() }}</h2> + <h2>{{ vial_current_view.get_view_title() }}</h2> <hr> <form method="GET" class="form" action="{{ url_for(request.endpoint) }}"> <div class="row"> diff --git a/lib/hawat/blueprints/nerd/__init__.py b/lib/hawat/blueprints/nerd/__init__.py index f6e5fa7e06239eae13c02bc1414bd7553f4a71f8..843c80df19702638ab6c7313de8b3cfa6362e726 100644 --- a/lib/hawat/blueprints/nerd/__init__.py +++ b/lib/hawat/blueprints/nerd/__init__.py @@ -61,10 +61,13 @@ from flask_babel import lazy_gettext import mentat.services.nerd from mentat.const import tr_ -import hawat.const import hawat.db -import hawat.acl -from hawat.base import HTMLMixin, AJAXMixin, SnippetMixin, RenderableView, VialBlueprint, URLParamsBuilder +import vial.const +import vial.acl +from vial.app import VialBlueprint +from vial.view import RenderableView +from vial.view.mixin import HTMLMixin, AJAXMixin, SnippetMixin +from vial.utils import URLParamsBuilder from hawat.blueprints.nerd.forms import NerdSearchForm @@ -80,16 +83,16 @@ class AbstractSearchView(RenderableView): # pylint: disable=locally-disabled,ab """ authentication = True - authorization = [hawat.acl.PERMISSION_ANY] + authorization = [vial.acl.PERMISSION_ANY] @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" return lazy_gettext('Search NERD') @classmethod def get_menu_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('Search NERD') #--------------------------------------------------------------------------- @@ -101,7 +104,7 @@ class AbstractSearchView(RenderableView): # pylint: disable=locally-disabled,ab """ form = NerdSearchForm(flask.request.args, meta = {'csrf': False}) - if hawat.const.FORM_ACTION_SUBMIT in flask.request.args: + if vial.const.FORM_ACTION_SUBMIT in flask.request.args: if form.validate(): form_data = form.data nerd_service = mentat.services.nerd.service() @@ -133,7 +136,7 @@ class SearchView(HTMLMixin, AbstractSearchView): # pylint: disable=locally-disa @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" return 'search' @@ -146,7 +149,7 @@ class APISearchView(AJAXMixin, AbstractSearchView): # pylint: disable=locally-d @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" return 'apisearch' @@ -166,7 +169,7 @@ class SnippetSearchView(SnippetMixin, AbstractSearchView): # pylint: disable=lo @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" return 'sptsearch' @@ -180,18 +183,18 @@ class NerdBlueprint(VialBlueprint): @classmethod def get_module_title(cls): - """*Implementation* of :py:func:`hawat.base.VialBlueprint.get_module_title`.""" + """*Implementation* of :py:func:`vial.app.VialBlueprint.get_module_title`.""" return lazy_gettext('NERD service') def register_app(self, app): """ - *Callback method*. Will be called from :py:func:`hawat.base.Vial.register_blueprint` + *Callback method*. Will be called from :py:func:`hawat.base.HawatApp.register_blueprint` method and can be used to customize the Flask application object. Possible use cases: * application menu customization - :param hawat.base.Vial app: Flask application to be customized. + :param hawat.base.HawatApp app: Flask application to be customized. """ mentat.services.nerd.init(app.mconfig) @@ -231,7 +234,7 @@ class NerdBlueprint(VialBlueprint): def get_blueprint(): """ Mandatory interface and factory function. This function must return a valid - instance of :py:class:`hawat.base.VialBlueprint` or :py:class:`flask.Blueprint`. + instance of :py:class:`vial.app.VialBlueprint` or :py:class:`flask.Blueprint`. """ hbp = NerdBlueprint( diff --git a/lib/hawat/blueprints/nerd/forms.py b/lib/hawat/blueprints/nerd/forms.py index c46222f329cc0c7b3906981a2b0f174494ffcbcd..17dd98b06df96a739a8b6a09c277d7d3c8c0e25f 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 hawat.forms +import vial.forms class NerdSearchForm(flask_wtf.FlaskForm): @@ -32,7 +32,7 @@ class NerdSearchForm(flask_wtf.FlaskForm): lazy_gettext('Search NERD:'), validators = [ wtforms.validators.DataRequired(), - hawat.forms.check_ip4_record + vial.forms.check_ip4_record ] ) submit = wtforms.SubmitField( diff --git a/lib/hawat/blueprints/nerd/templates/nerd/search.html b/lib/hawat/blueprints/nerd/templates/nerd/search.html index 6a953433cb4477283d7454d7f2b22f7624159715..1fa6e50a4db718359ee38cb3a7f5f56e25dd5d6d 100644 --- a/lib/hawat/blueprints/nerd/templates/nerd/search.html +++ b/lib/hawat/blueprints/nerd/templates/nerd/search.html @@ -7,7 +7,7 @@ <div class="col-lg-12"> <div class="jumbotron" style="margin-top: 1em;"> - <h2>{{ hawat_current_view.get_view_title() }}</h2> + <h2>{{ vial_current_view.get_view_title() }}</h2> <form method="GET" class="form-inline" action="{{ url_for('nerd.search') }}"> <div class="form-group{% if search_form.search.errors %}{{ ' has-error' }}{% endif %}"> {{ search_form.search.label(class_='sr-only') }} diff --git a/lib/hawat/blueprints/networks/__init__.py b/lib/hawat/blueprints/networks/__init__.py index 29c07acedc89e3f10a0d13540026a92ac90847b6..20c7b8914ee54017ede81f30827fb5fb03498c4c 100644 --- a/lib/hawat/blueprints/networks/__init__.py +++ b/lib/hawat/blueprints/networks/__init__.py @@ -40,11 +40,10 @@ from flask_babel import gettext, lazy_gettext # from mentat.datatype.sqldb import NetworkModel, GroupModel, ItemChangeLogModel -import hawat.base -import hawat.db -from hawat.base import HTMLMixin, SQLAlchemyMixin, ItemListView,\ - ItemShowView, ItemCreateView, ItemCreateForView, ItemUpdateView,\ - ItemDeleteView, VialBlueprint +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 from hawat.blueprints.networks.forms import BaseNetworkForm, AdminNetworkForm @@ -60,24 +59,24 @@ class ListView(HTMLMixin, SQLAlchemyMixin, ItemListView): authentication = True - authorization = [hawat.acl.PERMISSION_POWER] + authorization = [vial.acl.PERMISSION_POWER] @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('Network management') #--------------------------------------------------------------------------- @property def dbmodel(self): - """*Implementation* of :py:func:`hawat.base.SQLAlchemyMixin.dbmodel`.""" + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" return NetworkModel @classmethod def get_action_menu(cls): - """*Implementation* of :py:func:`hawat.base.ItemListView.get_action_menu`.""" - action_menu = hawat.menu.Menu() + """*Implementation* of :py:func:`vial.view.ItemListView.get_action_menu`.""" + action_menu = vial.menu.Menu() action_menu.add_entry( 'endpoint', 'create', @@ -88,8 +87,8 @@ class ListView(HTMLMixin, SQLAlchemyMixin, ItemListView): @classmethod def get_context_action_menu(cls): - """*Implementation* of :py:func:`hawat.base.ItemListView.get_context_action_menu`.""" - action_menu = hawat.menu.Menu() + """*Implementation* of :py:func:`vial.view.ItemListView.get_context_action_menu`.""" + action_menu = vial.menu.Menu() action_menu.add_entry( 'endpoint', 'show', @@ -121,19 +120,19 @@ class ShowView(HTMLMixin, SQLAlchemyMixin, ItemShowView): @classmethod def get_menu_legend(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('View details of network record "%(item)s"', item = kwargs['item'].netname) @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" return lazy_gettext('Show network record details') #--------------------------------------------------------------------------- @property def dbmodel(self): - """*Implementation* of :py:func:`hawat.base.SQLAlchemyMixin.dbmodel`.""" + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" return NetworkModel @classmethod @@ -142,17 +141,17 @@ class ShowView(HTMLMixin, SQLAlchemyMixin, ItemShowView): Perform access authorization for current user to particular item. """ permission_mm = flask_principal.Permission( - hawat.acl.MembershipNeed(kwargs['item'].group.id), - hawat.acl.ManagementNeed(kwargs['item'].group.id) + vial.acl.MembershipNeed(kwargs['item'].group.id), + vial.acl.ManagementNeed(kwargs['item'].group.id) ) - return hawat.acl.PERMISSION_POWER.can() or permission_mm.can() + return vial.acl.PERMISSION_POWER.can() or permission_mm.can() @classmethod def get_action_menu(cls): """ Get action menu for particular item. """ - action_menu = hawat.menu.Menu() + action_menu = vial.menu.Menu() action_menu.add_entry( 'endpoint', @@ -196,48 +195,53 @@ class CreateView(HTMLMixin, SQLAlchemyMixin, ItemCreateView): # pylint: disable @classmethod def get_menu_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('Create network record') @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" return lazy_gettext('Create new network record') #--------------------------------------------------------------------------- @property def dbmodel(self): - """*Implementation* of :py:func:`hawat.base.SQLAlchemyMixin.dbmodel`.""" + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" return NetworkModel + @property + def dbchlogmodel(self): + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" + return ItemChangeLogModel + @classmethod def authorize_item_action(cls, **kwargs): """ Perform access authorization for current user to particular item. """ - return hawat.acl.PERMISSION_POWER.can() + return vial.acl.PERMISSION_POWER.can() #--------------------------------------------------------------------------- @staticmethod def get_message_success(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_success`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_success`.""" return gettext('Network record <strong>%(item_id)s</strong> for group <strong>%(parent_id)s</strong> was successfully created.', item_id = str(kwargs['item']), parent_id = str(kwargs['item'].group)) @staticmethod def get_message_failure(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_failure`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_failure`.""" return gettext('Unable to create new network record for group <strong>%(parent_id)s</strong>.', parent_id = str(kwargs['item'].group)) @staticmethod def get_message_cancel(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_cancel`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_cancel`.""" return gettext('Canceled creating new network record for group <strong>%(parent_id)s</strong>.', parent_id = str(kwargs['item'].group)) @staticmethod def get_item_form(): - """*Implementation* of :py:func:`hawat.base.ItemCreateView.get_item_form`.""" + """*Implementation* of :py:func:`vial.view.ItemCreateView.get_item_form`.""" return AdminNetworkForm() @@ -251,79 +255,84 @@ class CreateForView(HTMLMixin, SQLAlchemyMixin, ItemCreateForView): # pylint: d @classmethod def get_view_icon(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_icon`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_icon`.""" return 'module-{}'.format(BLUEPRINT_NAME) @classmethod def get_menu_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('Create network record') @classmethod def get_menu_legend(cls, **kwargs): """ - *Interface implementation* of :py:func:`hawat.base.BaseView.get_menu_legend`. + *Interface implementation* of :py:func:`vial.view.BaseView.get_menu_legend`. """ return lazy_gettext('Create network record for group "%(item)s"', item = str(kwargs['item'])) @classmethod def get_view_url(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_url`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_url`.""" return flask.url_for(cls.get_view_endpoint(), parent_id = kwargs['item'].id) @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" return lazy_gettext('Create new network record for group') #--------------------------------------------------------------------------- @property def dbmodel(self): - """*Implementation* of :py:func:`hawat.base.SQLAlchemyMixin.dbmodel`.""" + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" return NetworkModel @property def dbmodel_par(self): - """*Implementation* of :py:func:`hawat.base.SQLAlchemyMixin.dbmodel`.""" + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" return GroupModel + @property + def dbchlogmodel(self): + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" + return ItemChangeLogModel + @classmethod def authorize_item_action(cls, **kwargs): """ Perform access authorization for current user to particular item. """ permission_m = flask_principal.Permission( - hawat.acl.ManagementNeed(kwargs['item'].id) + vial.acl.ManagementNeed(kwargs['item'].id) ) - return hawat.acl.PERMISSION_POWER.can() or permission_m.can() + return vial.acl.PERMISSION_POWER.can() or permission_m.can() #--------------------------------------------------------------------------- @staticmethod def get_message_success(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_success`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_success`.""" return gettext('Network record <strong>%(item_id)s</strong> for group <strong>%(parent_id)s</strong> was successfully created.', item_id = str(kwargs['item']), parent_id = str(kwargs['parent'])) @staticmethod def get_message_failure(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_failure`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_failure`.""" return gettext('Unable to create new network record for group <strong>%(parent_id)s</strong>.', parent_id = str(kwargs['parent'])) @staticmethod def get_message_cancel(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_cancel`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_cancel`.""" return gettext('Canceled creating new network record for group <strong>%(parent_id)s</strong>.', parent_id = str(kwargs['parent'])) @staticmethod def get_item_form(): - """*Implementation* of :py:func:`hawat.base.ItemCreateView.get_item_form`.""" + """*Implementation* of :py:func:`vial.view.ItemCreateView.get_item_form`.""" return BaseNetworkForm() @staticmethod def add_parent_to_item(item, parent): """ - *Hook method*. Implementation of :py:func:`hawat.base.ItemCreateForView.add_parent_to_item` interface. + *Hook method*. Implementation of :py:func:`vial.view.ItemCreateForView.add_parent_to_item` interface. """ item.group = parent @@ -338,51 +347,56 @@ class UpdateView(HTMLMixin, SQLAlchemyMixin, ItemUpdateView): # pylint: disable @classmethod def get_menu_legend(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('Update details of network record "%(item)s"', item = kwargs['item'].netname) @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" return lazy_gettext('Update network record details') #--------------------------------------------------------------------------- @property def dbmodel(self): - """*Implementation* of :py:func:`hawat.base.SQLAlchemyMixin.dbmodel`.""" + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" return NetworkModel + @property + def dbchlogmodel(self): + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" + return ItemChangeLogModel + @classmethod def authorize_item_action(cls, **kwargs): """ Perform access authorization for current user to particular item. """ permission_m = flask_principal.Permission( - hawat.acl.ManagementNeed(kwargs['item'].group.id) + vial.acl.ManagementNeed(kwargs['item'].group.id) ) - return hawat.acl.PERMISSION_POWER.can() or permission_m.can() + return vial.acl.PERMISSION_POWER.can() or permission_m.can() #--------------------------------------------------------------------------- @staticmethod def get_message_success(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_success`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_success`.""" return gettext('Network record <strong>%(item_id)s</strong> for group <strong>%(parent_id)s</strong> was successfully updated.', item_id = str(kwargs['item']), parent_id = str(kwargs['item'].group)) @staticmethod def get_message_failure(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_failure`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_failure`.""" return gettext('Unable to update network record <strong>%(item_id)s</strong> for group <strong>%(parent_id)s</strong>.', item_id = str(kwargs['item']), parent_id = str(kwargs['item'].group)) @staticmethod def get_message_cancel(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_cancel`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_cancel`.""" return gettext('Canceled updating network record <strong>%(item_id)s</strong> for group <strong>%(parent_id)s</strong>.', item_id = str(kwargs['item']), parent_id = str(kwargs['item'].group)) @staticmethod def get_item_form(item): - """*Implementation* of :py:func:`hawat.base.ItemUpdateView.get_item_form`.""" + """*Implementation* of :py:func:`vial.view.ItemUpdateView.get_item_form`.""" admin = flask_login.current_user.has_role('admin') if not admin: return BaseNetworkForm(obj = item) @@ -401,7 +415,7 @@ class DeleteView(HTMLMixin, SQLAlchemyMixin, ItemDeleteView): # pylint: disable @classmethod def get_menu_legend(cls, **kwargs): """ - *Interface implementation* of :py:func:`hawat.base.BaseView.get_menu_legend`. + *Interface implementation* of :py:func:`vial.view.BaseView.get_menu_legend`. """ return lazy_gettext('Delete network record "%(item)s"', item = kwargs['item'].netname) @@ -409,34 +423,39 @@ class DeleteView(HTMLMixin, SQLAlchemyMixin, ItemDeleteView): # pylint: disable @property def dbmodel(self): - """*Implementation* of :py:func:`hawat.base.SQLAlchemyMixin.dbmodel`.""" + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" return NetworkModel + @property + def dbchlogmodel(self): + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" + return ItemChangeLogModel + @classmethod def authorize_item_action(cls, **kwargs): """ Perform access authorization for current user to particular item. """ permission_m = flask_principal.Permission( - hawat.acl.ManagementNeed(kwargs['item'].group.id) + vial.acl.ManagementNeed(kwargs['item'].group.id) ) - return hawat.acl.PERMISSION_POWER.can() or permission_m.can() + return vial.acl.PERMISSION_POWER.can() or permission_m.can() #--------------------------------------------------------------------------- @staticmethod def get_message_success(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_success`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_success`.""" return gettext('Network record <strong>%(item_id)s</strong> for group <strong>%(parent_id)s</strong> was successfully and permanently deleted.', item_id = str(kwargs['item']), parent_id = str(kwargs['item'].group)) @staticmethod def get_message_failure(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_failure`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_failure`.""" return gettext('Unable to permanently delete network record <strong>%(item_id)s</strong> for group <strong>%(parent_id)s</strong>.', item_id = str(kwargs['item']), parent_id = str(kwargs['item'].group)) @staticmethod def get_message_cancel(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_cancel`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_cancel`.""" return gettext('Canceled deleting network record <strong>%(item_id)s</strong> for group <strong>%(parent_id)s</strong>.', item_id = str(kwargs['item']), parent_id = str(kwargs['item'].group)) @@ -450,18 +469,18 @@ class NetworksBlueprint(VialBlueprint): @classmethod def get_module_title(cls): - """*Implementation* of :py:func:`hawat.base.VialBlueprint.get_module_title`.""" + """*Implementation* of :py:func:`vial.app.VialBlueprint.get_module_title`.""" return lazy_gettext('Network record management pluggable module') def register_app(self, app): """ - *Callback method*. Will be called from :py:func:`hawat.base.Vial.register_blueprint` + *Callback method*. Will be called from :py:func:`hawat.base.HawatApp.register_blueprint` method and can be used to customize the Flask application object. Possible use cases: * application menu customization - :param hawat.base.Vial app: Flask application to be customized. + :param hawat.base.HawatApp app: Flask application to be customized. """ app.menu_main.add_entry( 'view', @@ -477,7 +496,7 @@ class NetworksBlueprint(VialBlueprint): def get_blueprint(): """ Mandatory interface and factory function. This function must return a valid - instance of :py:class:`hawat.base.VialBlueprint` or :py:class:`flask.Blueprint`. + instance of :py:class:`vial.app.VialBlueprint` or :py:class:`flask.Blueprint`. """ hbp = NetworksBlueprint( diff --git a/lib/hawat/blueprints/networks/forms.py b/lib/hawat/blueprints/networks/forms.py index 1a524d2c82dcbde9309c110ac571912458af4ae6..d3d7cafc4365846f1e5c45da6af12aabe5db2e6d 100644 --- a/lib/hawat/blueprints/networks/forms.py +++ b/lib/hawat/blueprints/networks/forms.py @@ -28,9 +28,11 @@ from flask_babel import lazy_gettext # # Custom modules. # -import hawat.const +import vial.const +import vial.forms + import hawat.db -import hawat.forms + from mentat.datatype.sqldb import GroupModel @@ -41,7 +43,7 @@ def get_available_groups(): return hawat.db.db_query(GroupModel).order_by(GroupModel.name).all() -class BaseNetworkForm(hawat.forms.BaseItemForm): +class BaseNetworkForm(vial.forms.BaseItemForm): """ Class representing base network record form. """ @@ -63,7 +65,7 @@ class BaseNetworkForm(hawat.forms.BaseItemForm): lazy_gettext('Network:'), validators = [ wtforms.validators.DataRequired(), - hawat.forms.check_network_record + vial.forms.check_network_record ] ) description = wtforms.TextAreaField( diff --git a/lib/hawat/blueprints/networks/templates/networks/creatupdate.html b/lib/hawat/blueprints/networks/templates/networks/creatupdate.html index 25dbdc2abd6e8e196711b383dda9aed4c21c0885..9cab06b30ed4632001f1f2cad3a3d7ebc4a64407 100644 --- a/lib/hawat/blueprints/networks/templates/networks/creatupdate.html +++ b/lib/hawat/blueprints/networks/templates/networks/creatupdate.html @@ -17,7 +17,7 @@ <form method="POST" action="{{ form_url }}"> <fieldset> - <legend>{{ hawat_current_view.get_view_title() }}</legend> + <legend>{{ vial_current_view.get_view_title() }}</legend> {%- if item_action == 'createfor' %} diff --git a/lib/hawat/blueprints/networks/templates/networks/show.html b/lib/hawat/blueprints/networks/templates/networks/show.html index 4db6bc0b7fe5f4a602699c4c5fbd45cd6414b750..2ebb83533435bac3c31b8289e3369c9864c014ac 100644 --- a/lib/hawat/blueprints/networks/templates/networks/show.html +++ b/lib/hawat/blueprints/networks/templates/networks/show.html @@ -6,7 +6,7 @@ <div class="col-lg-12"> {{ macros_page.render_breadcrumbs(item) }} - <h2>{{ hawat_current_view.get_view_title() }}</h2> + <h2>{{ vial_current_view.get_view_title() }}</h2> <hr> <h3>{{ item.netname }}</h3> <div class="pull-right"> diff --git a/lib/hawat/blueprints/pdnsr/__init__.py b/lib/hawat/blueprints/pdnsr/__init__.py index 25cd1ba2415dccdec15765cfca27fcb185a4a422..5ff3c4d0b8d388a04b344a4e26b218c06b82cd1e 100644 --- a/lib/hawat/blueprints/pdnsr/__init__.py +++ b/lib/hawat/blueprints/pdnsr/__init__.py @@ -61,10 +61,13 @@ from flask_babel import lazy_gettext import mentat.services.pdnsr from mentat.const import tr_ -import hawat.const import hawat.db -import hawat.acl -from hawat.base import HTMLMixin, AJAXMixin, SnippetMixin, RenderableView, VialBlueprint, URLParamsBuilder +import vial.const +import vial.acl +from vial.app import VialBlueprint +from vial.view import RenderableView +from vial.view.mixin import HTMLMixin, AJAXMixin, SnippetMixin +from vial.utils import URLParamsBuilder from hawat.blueprints.pdnsr.forms import PDNSRSearchForm @@ -80,16 +83,16 @@ class AbstractSearchView(RenderableView): # pylint: disable=locally-disabled,ab """ authentication = True - authorization = [hawat.acl.PERMISSION_ANY] + authorization = [vial.acl.PERMISSION_ANY] @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" return lazy_gettext('Search PassiveDNS') @classmethod def get_menu_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('Search PassiveDNS') #--------------------------------------------------------------------------- @@ -101,7 +104,7 @@ class AbstractSearchView(RenderableView): # pylint: disable=locally-disabled,ab """ form = PDNSRSearchForm(flask.request.args, meta = {'csrf': False}) - if hawat.const.FORM_ACTION_SUBMIT in flask.request.args: + if vial.const.FORM_ACTION_SUBMIT in flask.request.args: if form.validate(): form_data = form.data pdnsr_service = mentat.services.pdnsr.service() @@ -138,7 +141,7 @@ class SearchView(HTMLMixin, AbstractSearchView): # pylint: disable=locally-disa @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" return 'search' @@ -151,7 +154,7 @@ class APISearchView(AJAXMixin, AbstractSearchView): # pylint: disable=locally-d @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" return 'apisearch' @@ -171,7 +174,7 @@ class SnippetSearchView(SnippetMixin, AbstractSearchView): # pylint: disable=lo @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" return 'sptsearch' @@ -185,18 +188,18 @@ class PDNSRBlueprint(VialBlueprint): @classmethod def get_module_title(cls): - """*Implementation* of :py:func:`hawat.base.VialBlueprint.get_module_title`.""" + """*Implementation* of :py:func:`vial.app.VialBlueprint.get_module_title`.""" return lazy_gettext('PassiveDNS service') def register_app(self, app): """ - *Callback method*. Will be called from :py:func:`hawat.base.Vial.register_blueprint` + *Callback method*. Will be called from :py:func:`hawat.base.HawatApp.register_blueprint` method and can be used to customize the Flask application object. Possible use cases: * application menu customization - :param hawat.base.Vial app: Flask application to be customized. + :param hawat.base.HawatApp app: Flask application to be customized. """ mentat.services.pdnsr.init(app.mconfig) @@ -241,7 +244,7 @@ class PDNSRBlueprint(VialBlueprint): def get_blueprint(): """ Mandatory interface and factory function. This function must return a valid - instance of :py:class:`hawat.base.VialBlueprint` or :py:class:`flask.Blueprint`. + instance of :py:class:`vial.app.VialBlueprint` or :py:class:`flask.Blueprint`. """ hbp = PDNSRBlueprint( diff --git a/lib/hawat/blueprints/pdnsr/forms.py b/lib/hawat/blueprints/pdnsr/forms.py index 6e72db34ea4f96e64ef9b7a23f99ecda293780e5..677224dc81eb0b8ab17d124f4aac4d5c2c5e2760 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 hawat.forms +import vial.forms class PDNSRSearchForm(flask_wtf.FlaskForm): @@ -32,7 +32,7 @@ class PDNSRSearchForm(flask_wtf.FlaskForm): lazy_gettext('Search PassiveDNS:'), validators = [ wtforms.validators.DataRequired(), - hawat.forms.check_ip_record + vial.forms.check_ip_record ] ) sortby = wtforms.SelectField( @@ -53,13 +53,13 @@ class PDNSRSearchForm(flask_wtf.FlaskForm): ], default = '' ) - limit = hawat.forms.SelectFieldWithNone( + limit = vial.forms.SelectFieldWithNone( lazy_gettext('Pager limit:'), validators = [ wtforms.validators.Optional() ], filters = [int], - choices = [(0, lazy_gettext('without explicit limit'))] + hawat.const.PAGER_LIMIT_CHOICES, + choices = [(0, lazy_gettext('without explicit limit'))] + vial.const.PAGER_LIMIT_CHOICES, default = 0 ) submit = wtforms.SubmitField( diff --git a/lib/hawat/blueprints/pdnsr/templates/pdnsr/search.html b/lib/hawat/blueprints/pdnsr/templates/pdnsr/search.html index 36bb478ded32b920d7d63ee9f8ae3f18a2ee3d5b..c1d3c94c650af4250b568766eef30c13a319c995 100644 --- a/lib/hawat/blueprints/pdnsr/templates/pdnsr/search.html +++ b/lib/hawat/blueprints/pdnsr/templates/pdnsr/search.html @@ -7,7 +7,7 @@ <div class="col-lg-12"> <div class="jumbotron" style="margin-top: 1em;"> - <h2>{{ hawat_current_view.get_view_title() }}</h2> + <h2>{{ vial_current_view.get_view_title() }}</h2> <form method="GET" class="form-inline" action="{{ url_for('pdnsr.search') }}"> <div class="form-group{% if search_form.search.errors %}{{ ' has-error' }}{% endif %}"> {{ search_form.search.label(class_='sr-only') }} diff --git a/lib/hawat/blueprints/performance/__init__.py b/lib/hawat/blueprints/performance/__init__.py index 46fa5fd7d8d0994caee72dc72de7607bda7dba2a..6752edd85bfe9e45604ef8074863c3665364d6af 100644 --- a/lib/hawat/blueprints/performance/__init__.py +++ b/lib/hawat/blueprints/performance/__init__.py @@ -25,8 +25,12 @@ import collections from flask_babel import lazy_gettext import mentat.stats.rrd -import hawat.acl -from hawat.base import HTMLMixin, SimpleView, FileNameView, VialBlueprint + +import vial.acl +from vial.app import VialBlueprint +from vial.view import SimpleView, FileNameView +from vial.view.mixin import HTMLMixin + RRD_DB_DIR = '/var/mentat/rrds' RRD_REPORTS_DIR = '/var/mentat/reports/statistician' @@ -42,31 +46,31 @@ class ViewView(HTMLMixin, SimpleView): """ authentication = True - authorization = [hawat.acl.PERMISSION_ANY] + authorization = [vial.acl.PERMISSION_ANY] @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" return 'view' @classmethod def get_view_icon(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_icon`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_icon`.""" return 'module-{}'.format(BLUEPRINT_NAME) @classmethod def get_menu_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('System performance') @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" return lazy_gettext('System processing performance') def do_before_response(self, **kwargs): - """*Implementation* of :py:func:`hawat.base.RenderableView.do_before_response`.""" + """*Implementation* of :py:func:`vial.view.RenderableView.do_before_response`.""" rrd_stats = mentat.stats.rrd.RrdStats(RRD_DB_DIR, RRD_REPORTS_DIR) charts = rrd_stats.lookup() chartdict = collections.OrderedDict() @@ -92,22 +96,22 @@ class DataView(FileNameView): """ authentication = True - authorization = [hawat.acl.PERMISSION_ANY] + authorization = [vial.acl.PERMISSION_ANY] @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" return 'data' @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('System performance data') @classmethod def get_directory_path(cls): """ - *Interface implementation* of :py:func:`hawat.base.FileNameView.get_directory_path`. + *Interface implementation* of :py:func:`vial.view.FileNameView.get_directory_path`. """ return RRD_REPORTS_DIR @@ -118,22 +122,22 @@ class RRDDBView(FileNameView): """ authentication = True - authorization = [hawat.acl.PERMISSION_ANY] + authorization = [vial.acl.PERMISSION_ANY] @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" return 'rrds' @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('System performance databases') @classmethod def get_directory_path(cls): """ - *Interface implementation* of :py:func:`hawat.base.FileNameView.get_directory_path`. + *Interface implementation* of :py:func:`vial.view.FileNameView.get_directory_path`. """ return RRD_DB_DIR @@ -148,18 +152,18 @@ class PerformanceBlueprint(VialBlueprint): @classmethod def get_module_title(cls): - """*Implementation* of :py:func:`hawat.base.VialBlueprint.get_module_title`.""" + """*Implementation* of :py:func:`vial.app.VialBlueprint.get_module_title`.""" return lazy_gettext('System performance pluggable module') def register_app(self, app): """ - *Callback method*. Will be called from :py:func:`hawat.base.Vial.register_blueprint` + *Callback method*. Will be called from :py:func:`hawat.base.HawatApp.register_blueprint` method and can be used to customize the Flask application object. Possible use cases: * application menu customization - :param hawat.base.Vial app: Flask application to be customized. + :param hawat.base.HawatApp app: Flask application to be customized. """ app.menu_main.add_entry( 'view', @@ -176,7 +180,7 @@ class PerformanceBlueprint(VialBlueprint): def get_blueprint(): """ Mandatory interface and factory function. This function must return a valid - instance of :py:class:`hawat.base.VialBlueprint` or :py:class:`flask.Blueprint`. + instance of :py:class:`vial.app.VialBlueprint` or :py:class:`flask.Blueprint`. """ hbp = PerformanceBlueprint( diff --git a/lib/hawat/blueprints/performance/templates/performance/view.html b/lib/hawat/blueprints/performance/templates/performance/view.html index 62e6c32feefe0a9154834b7269206e44e2963d7a..c0abd579a4f6ff85047f854a9ed1e3d2982155e4 100644 --- a/lib/hawat/blueprints/performance/templates/performance/view.html +++ b/lib/hawat/blueprints/performance/templates/performance/view.html @@ -4,7 +4,7 @@ <div class="row"> <div class="col-lg-12"> - <h2>{{ hawat_current_view.get_view_title() }}</h2> + <h2>{{ vial_current_view.get_view_title() }}</h2> <hr> <!-- Nav tabs --> diff --git a/lib/hawat/blueprints/reports/__init__.py b/lib/hawat/blueprints/reports/__init__.py index 2a0edee71d7482cb49ae03290c4f8e63cacf92ae..6e8ee575fc1ba0e4e1efc844a726bfa5745928ba 100644 --- a/lib/hawat/blueprints/reports/__init__.py +++ b/lib/hawat/blueprints/reports/__init__.py @@ -21,11 +21,11 @@ __author__ = "Jan Mach <jan.mach@cesnet.cz>" __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea Kropáčová <andrea.kropacova@cesnet.cz>" +import re import os.path import datetime import dateutil.parser import pytz -import re # # Flask related modules. @@ -43,14 +43,17 @@ from flask_babel import gettext, lazy_gettext, force_locale, format_datetime # import mentat.const import mentat.stats.idea -from mentat.datatype.sqldb import EventReportModel, GroupModel, UserModel +from mentat.datatype.sqldb import EventReportModel, GroupModel, UserModel, ItemChangeLogModel from mentat.const import tr_ -import hawat.menu -import hawat.acl -from hawat.base import HTMLMixin, SQLAlchemyMixin, AJAXMixin, BaseSearchView, \ - ItemShowView, ItemDeleteView, FileIdView, VialBlueprint,\ - URLParamsBuilder, RenderableView +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 from hawat.blueprints.reports.forms import EventReportSearchForm, ReportingDashboardForm, \ FeedbackForm @@ -95,7 +98,7 @@ def adjust_query_for_groups(query, groups): return query.join(GroupModel).filter(GroupModel.id.in_([grp.id for grp in groups])) # For non-administrators restrict query only to groups they are member of. - if not flask_login.current_user.has_role(hawat.const.ROLE_ADMIN): + if not flask_login.current_user.has_role(vial.const.ROLE_ADMIN): # Naive approach. #query = query.filter(model.group.has(GroupModel.members.any(UserModel.id == flask_login.current_user.id))) # "Joined" approach. @@ -112,36 +115,41 @@ class SearchView(HTMLMixin, SQLAlchemyMixin, BaseSearchView): # pylint: disable authentication = True - authorization = [hawat.acl.PERMISSION_ANY] + authorization = [vial.acl.PERMISSION_ANY] @classmethod - def get_menu_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" - return lazy_gettext('Reports') + def get_view_icon(cls): + """*Implementation* of :py:func:`vial.view.BaseView.get_view_icon`.""" + return 'module-{}'.format(cls.module_name) @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" return lazy_gettext('Search event reports') + @classmethod + def get_menu_title(cls, **kwargs): + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" + return lazy_gettext('Reports') + #--------------------------------------------------------------------------- @property def dbmodel(self): - """*Implementation* of :py:func:`hawat.base.SQLAlchemyMixin.dbmodel`.""" + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" return EventReportModel @staticmethod def get_search_form(request_args): """ - *Interface implementation* of :py:func:`hawat.base.SearchView.get_search_form`. + *Interface implementation* of :py:func:`vial.view.BaseSearchView.get_search_form`. """ return EventReportSearchForm(request_args, meta = {'csrf': False}) @staticmethod def build_query(query, model, form_args): """ - *Interface implementation* of :py:func:`hawat.base.SQLAlchemyMixin.build_query`. + *Interface implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.build_query`. """ # Adjust query based on group selection. query = adjust_query_for_groups(query, form_args.get('groups', None)) @@ -166,7 +174,7 @@ class SearchView(HTMLMixin, SQLAlchemyMixin, BaseSearchView): # pylint: disable def do_after_search(self, items): """ - *Interface implementation* of :py:func:`hawat.base.SearchView.do_after_search`. + *Interface implementation* of :py:func:`vial.view.BaseSearchView.do_after_search`. """ if items: self.response_context.update( @@ -184,49 +192,49 @@ class ShowView(HTMLMixin, SQLAlchemyMixin, ItemShowView): @classmethod def get_menu_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('Show report') @classmethod def get_menu_legend(cls, **kwargs): """ - *Interface implementation* of :py:func:`hawat.base.BaseView.get_menu_legend`. + *Interface implementation* of :py:func:`vial.view.BaseView.get_menu_legend`. """ return lazy_gettext('View details of event report "%(item)s"', item = str(kwargs['item'])) @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" return lazy_gettext('Show report') #--------------------------------------------------------------------------- @property def dbmodel(self): - """*Implementation* of :py:func:`hawat.base.SQLAlchemyMixin.dbmodel`.""" + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" return EventReportModel @classmethod def authorize_item_action(cls, **kwargs): """ - *Interface implementation* of :py:func:`hawat.base.ItemShowView.authorize_item_action`. + *Interface implementation* of :py:func:`vial.view.ItemShowView.authorize_item_action`. """ permission_mm = flask_principal.Permission( - hawat.acl.MembershipNeed(kwargs['item'].group.id), - hawat.acl.ManagementNeed(kwargs['item'].group.id) + vial.acl.MembershipNeed(kwargs['item'].group.id), + vial.acl.ManagementNeed(kwargs['item'].group.id) ) - return hawat.acl.PERMISSION_POWER.can() or permission_mm.can() + return vial.acl.PERMISSION_POWER.can() or permission_mm.can() @classmethod def get_breadcrumbs_menu(cls): # pylint: disable=locally-disabled,unused-argument """ - *Interface implementation* of :py:func:`hawat.base.ItemShowView.get_breadcrumbs_menu`. + *Interface implementation* of :py:func:`vial.view.ItemShowView.get_breadcrumbs_menu`. """ - action_menu = hawat.menu.Menu() + action_menu = vial.menu.Menu() action_menu.add_entry( 'endpoint', 'home', - endpoint = flask.current_app.config['HAWAT_ENDPOINT_HOME'] + endpoint = flask.current_app.config['ENDPOINT_HOME'] ) action_menu.add_entry( 'endpoint', @@ -243,9 +251,9 @@ class ShowView(HTMLMixin, SQLAlchemyMixin, ItemShowView): @classmethod def get_action_menu(cls): """ - *Interface implementation* of :py:func:`hawat.base.ItemShowView.get_action_menu`. + *Interface implementation* of :py:func:`vial.view.ItemShowView.get_action_menu`. """ - action_menu = hawat.menu.Menu() + action_menu = vial.menu.Menu() action_menu.add_entry( 'endpoint', 'search', @@ -315,26 +323,26 @@ class ShowView(HTMLMixin, SQLAlchemyMixin, ItemShowView): ) @staticmethod - def format_datetime(val, format, tzone): + def format_datetime(val, format_str, tzone): """ Static method that take string with isoformat datetime in utc and return string with BABEL_RFC3339_FORMAT formated datetime in tz timezone """ return format_datetime( dateutil.parser.parse(val).replace(tzinfo=pytz.utc).astimezone(tzone), - format, + format_str, rebase = False ) @staticmethod - def escape_id(id): + def escape_id(ident): """ Escape id for use in bootstrap """ - return re.sub(r"[^A-Za-z0-9-_]", (lambda x: '\{:X} '.format(ord(x.group()))), id) + return re.sub(r"[^A-Za-z0-9-_]", (lambda x: '\{:X} '.format(ord(x.group()))), ident) def do_before_response(self, **kwargs): - """*Implementation* of :py:func:`hawat.base.RenderableView.do_before_response`.""" + """*Implementation* of :py:func:`vial.view.RenderableView.do_before_response`.""" if 'item' in self.response_context and self.response_context['item']: self.response_context.update( statistics = self.response_context['item'].statistics, @@ -357,25 +365,25 @@ class UnauthShowView(ShowView): # pylint: disable=locally-disabled,too-many-anc @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" return 'unauth' @classmethod def get_view_template(cls): - """*Implementation* of :py:func:`hawat.base.RenderableView.get_view_template`.""" + """*Implementation* of :py:func:`vial.view.RenderableView.get_view_template`.""" return '{}/show.html'.format(cls.module_name) @classmethod def authorize_item_action(cls, **kwargs): """ - *Interface implementation* of :py:func:`hawat.base.ItemShowView.authorize_item_action`. + *Interface implementation* of :py:func:`vial.view.ItemShowView.authorize_item_action`. """ return True @property def search_by(self): """ - *Interface implementation* of :py:func:`hawat.base.SQLAlchemyMixin.search_by`. + *Interface implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.search_by`. """ return self.dbmodel.label @@ -390,18 +398,18 @@ class DataView(FileIdView): @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" return 'data' @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('Event report data') @classmethod def get_directory_path(cls, fileid, filetype): """ - *Interface implementation* of :py:func:`hawat.base.FileIdView.get_directory_path`. + *Interface implementation* of :py:func:`vial.view.FileIdView.get_directory_path`. """ return mentat.const.construct_report_dirpath( flask.current_app.mconfig[mentat.const.CKEY_CORE_REPORTER][mentat.const.CKEY_CORE_REPORTER_REPORTSDIR], @@ -411,7 +419,7 @@ class DataView(FileIdView): def get_filename(self, fileid, filetype): """ - *Interface implementation* of :py:func:`hawat.base.FileIdView.get_filename`. + *Interface implementation* of :py:func:`vial.view.FileIdView.get_filename`. """ fileext = '' if filetype == 'json': @@ -433,46 +441,51 @@ class DashboardView(HTMLMixin, SQLAlchemyMixin, BaseSearchView): # pylint: disa """ authentication = True - authorization = [hawat.acl.PERMISSION_ANY] + authorization = [vial.acl.PERMISSION_ANY] @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" return 'dashboard' + @classmethod + def get_view_icon(cls): + """*Implementation* of :py:func:`vial.view.BaseView.get_view_icon`.""" + return 'module-{}'.format(cls.module_name) + @classmethod def get_menu_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('Reporting') @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" return lazy_gettext('Event reporting dashboards') @classmethod def get_view_template(cls): - """*Implementation* of :py:func:`hawat.base.RenderableView.get_view_template`.""" + """*Implementation* of :py:func:`vial.view.RenderableView.get_view_template`.""" return '{}/dashboard.html'.format(cls.module_name) #--------------------------------------------------------------------------- @property def dbmodel(self): - """*Implementation* of :py:func:`hawat.base.SQLAlchemyMixin.dbmodel`.""" + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" return EventReportModel @staticmethod def get_search_form(request_args): """ - *Interface implementation* of :py:func:`hawat.base.SearchView.get_search_form`. + *Interface implementation* of :py:func:`vial.view.BaseSearchView.get_search_form`. """ return ReportingDashboardForm(request_args, meta = {'csrf': False}) @staticmethod def build_query(query, model, form_args): """ - *Interface implementation* of :py:func:`hawat.base.SQLAlchemyMixin.build_query`. + *Interface implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.build_query`. """ # Adjust query based on group selection. query = adjust_query_for_groups(query, form_args.get('groups', None)) @@ -488,7 +501,7 @@ class DashboardView(HTMLMixin, SQLAlchemyMixin, BaseSearchView): # pylint: disa def do_after_search(self, items): """ - *Interface implementation* of :py:func:`hawat.base.SearchView.do_after_search`. + *Interface implementation* of :py:func:`vial.view.BaseSearchView.do_after_search`. """ self.logger.debug( "Calculating event reporting dashboard overview from %d records.", @@ -516,7 +529,7 @@ class DashboardView(HTMLMixin, SQLAlchemyMixin, BaseSearchView): # pylint: disa ) def do_before_response(self, **kwargs): - """*Implementation* of :py:func:`hawat.base.RenderableView.do_before_response`.""" + """*Implementation* of :py:func:`vial.view.RenderableView.do_before_response`.""" self.response_context.update( quicksearch_list = self.get_quicksearch_by_time() ) @@ -530,18 +543,18 @@ class DeleteView(HTMLMixin, SQLAlchemyMixin, ItemDeleteView): # pylint: disable authentication = True - authorization = [hawat.acl.PERMISSION_ADMIN] + authorization = [vial.acl.PERMISSION_ADMIN] @classmethod def get_menu_legend(cls, **kwargs): """ - *Interface implementation* of :py:func:`hawat.base.BaseView.get_menu_legend`. + *Interface implementation* of :py:func:`vial.view.BaseView.get_menu_legend`. """ return lazy_gettext('Delete event report "%(item)s"', item = str(kwargs['item'])) def get_url_next(self): """ - *Interface implementation* of :py:func:`hawat.base.ItemActionView.get_url_next`. + *Interface implementation* of :py:func:`vial.view.ItemActionView.get_url_next`. """ return flask.url_for('{}.{}'.format(self.module_name, 'search')) @@ -549,24 +562,29 @@ class DeleteView(HTMLMixin, SQLAlchemyMixin, ItemDeleteView): # pylint: disable @property def dbmodel(self): - """*Implementation* of :py:func:`hawat.base.SQLAlchemyMixin.dbmodel`.""" + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" return EventReportModel + @property + def dbchlogmodel(self): + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" + return ItemChangeLogModel + #--------------------------------------------------------------------------- @staticmethod def get_message_success(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_success`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_success`.""" return gettext('Event report <strong>%(item_id)s</strong> was successfully and permanently deleted.', item_id = str(kwargs['item'])) @staticmethod def get_message_failure(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_failure`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_failure`.""" return gettext('Unable to delete event report <strong>%(item_id)s</strong>.', item_id = str(kwargs['item'])) @staticmethod def get_message_cancel(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_cancel`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_cancel`.""" return gettext('Canceled deleting event report <strong>%(item_id)s</strong>.', item_id = str(kwargs['item'])) @@ -578,16 +596,16 @@ class FeedbackView(AJAXMixin, RenderableView): authentication = True - authorization = [hawat.acl.PERMISSION_ANY] + authorization = [vial.acl.PERMISSION_ANY] @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" return 'feedback' @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('Report feedback') #--------------------------------------------------------------------------- @@ -642,18 +660,18 @@ class ReportsBlueprint(VialBlueprint): @classmethod def get_module_title(cls): - """*Implementation* of :py:func:`hawat.base.VialBlueprint.get_module_title`.""" + """*Implementation* of :py:func:`vial.app.VialBlueprint.get_module_title`.""" return lazy_gettext('Event reports') def register_app(self, app): """ - *Callback method*. Will be called from :py:func:`hawat.base.Vial.register_blueprint` + *Callback method*. Will be called from :py:func:`hawat.base.HawatApp.register_blueprint` method and can be used to customize the Flask application object. Possible use cases: * application menu customization - :param hawat.base.Vial app: Flask application to be customized. + :param hawat.base.HawatApp app: Flask application to be customized. """ app.menu_main.add_entry( 'view', @@ -700,7 +718,7 @@ class ReportsBlueprint(VialBlueprint): def get_blueprint(): """ Mandatory interface and factory function. This function must return a valid - instance of :py:class:`hawat.base.VialBlueprint` or :py:class:`flask.Blueprint`. + instance of :py:class:`vial.app.VialBlueprint` or :py:class:`flask.Blueprint`. """ hbp = ReportsBlueprint( diff --git a/lib/hawat/blueprints/reports/forms.py b/lib/hawat/blueprints/reports/forms.py index 3a2f55050fb400863ec5aa42919f9b50d1ee9e32..2b31c12fa897feeaa822e625d14eab50d68d2d26 100644 --- a/lib/hawat/blueprints/reports/forms.py +++ b/lib/hawat/blueprints/reports/forms.py @@ -24,8 +24,11 @@ import flask_login import flask_wtf from flask_babel import lazy_gettext -import hawat.const -import hawat.forms +import vial.const +import vial.forms + +import hawat.db + import mentat.const from mentat.datatype.sqldb import UserModel, GroupModel @@ -35,7 +38,7 @@ def get_available_groups(): Query the database for list of all available groups. """ # In case current user is administrator provide list of all groups. - if flask_login.current_user.has_role(hawat.const.ROLE_ADMIN): + if flask_login.current_user.has_role(vial.const.ROLE_ADMIN): return hawat.db.db_query(GroupModel).\ order_by(GroupModel.name).\ all() @@ -67,7 +70,7 @@ def get_type_choices(): ) ) -class EventReportSearchForm(hawat.forms.BaseSearchForm): +class EventReportSearchForm(vial.forms.BaseSearchForm): """ Class representing event report search form. """ @@ -99,14 +102,14 @@ class EventReportSearchForm(hawat.forms.BaseSearchForm): choices = get_type_choices(), filters = [lambda x: x or []] ) - dt_from = hawat.forms.SmartDateTimeField( + dt_from = vial.forms.SmartDateTimeField( lazy_gettext('From:'), validators = [ wtforms.validators.Optional() ], formats = ['%Y-%m-%d %H:%M', '%Y-%m-%dT%H:%M'] ) - dt_to = hawat.forms.SmartDateTimeField( + dt_to = vial.forms.SmartDateTimeField( lazy_gettext('To:'), validators = [ wtforms.validators.Optional() @@ -138,14 +141,14 @@ class ReportingDashboardForm(flask_wtf.FlaskForm): allow_blank = False, get_pk = lambda item: item.name ) - dt_from = hawat.forms.SmartDateTimeField( + dt_from = vial.forms.SmartDateTimeField( lazy_gettext('From:'), validators = [ wtforms.validators.Optional() ], - default = lambda: hawat.forms.default_dt_with_delta(hawat.const.DEFAULT_RESULT_TIMEDELTA) + default = lambda: vial.forms.default_dt_with_delta(vial.const.DEFAULT_RESULT_TIMEDELTA) ) - dt_to = hawat.forms.SmartDateTimeField( + dt_to = vial.forms.SmartDateTimeField( lazy_gettext('To:'), validators = [ wtforms.validators.Optional() @@ -176,7 +179,7 @@ class FeedbackForm(flask_wtf.FlaskForm): ip = wtforms.HiddenField( validators=[ wtforms.validators.DataRequired(), - hawat.forms.check_network_record + vial.forms.check_network_record ] ) text = wtforms.TextAreaField( diff --git a/lib/hawat/blueprints/reports/templates/reports/dashboard.html b/lib/hawat/blueprints/reports/templates/reports/dashboard.html index 292a080f129b8172a5776fa32479c8d63b536acb..935e1d0c6395067f6bd5a86925592157101e762d 100644 --- a/lib/hawat/blueprints/reports/templates/reports/dashboard.html +++ b/lib/hawat/blueprints/reports/templates/reports/dashboard.html @@ -9,7 +9,7 @@ <!-- Search form - BEGIN ----------------------------------> <div class="jumbotron" style="margin-top: 1em;"> - <h2>{{ hawat_current_view.get_view_title() }}</h2> + <h2>{{ vial_current_view.get_view_title() }}</h2> <hr> <form method="GET" class="form" action="{{ url_for(request.endpoint) }}"> <div class="row"> diff --git a/lib/hawat/blueprints/reports/templates/reports/show.html b/lib/hawat/blueprints/reports/templates/reports/show.html index 19385de2a783ca0cee8313c219f60dcd451a0a5a..99d01e071d87644252b69b32aa3ce969cfa10dc5 100644 --- a/lib/hawat/blueprints/reports/templates/reports/show.html +++ b/lib/hawat/blueprints/reports/templates/reports/show.html @@ -12,7 +12,7 @@ </ol> {%- endif %} - <h2>{{ hawat_current_view.get_view_title() }}</h2> + <h2>{{ vial_current_view.get_view_title() }}</h2> <hr> <h3> {{ item.label }} diff --git a/lib/hawat/blueprints/settings_reporting/__init__.py b/lib/hawat/blueprints/settings_reporting/__init__.py index 8fefb57bcbf8b340f9a0ac6ee01b3f342ae0bfdd..109acfe92f54a812522d58c63f95a17a0a6c174a 100644 --- a/lib/hawat/blueprints/settings_reporting/__init__.py +++ b/lib/hawat/blueprints/settings_reporting/__init__.py @@ -39,9 +39,10 @@ from flask_babel import gettext, lazy_gettext import mentat.reports.utils from mentat.datatype.sqldb import SettingsReportingModel, ItemChangeLogModel -import hawat.db -from hawat.base import HTMLMixin, SQLAlchemyMixin, ItemShowView,\ - ItemCreateView, ItemUpdateView, VialBlueprint +import vial.acl +from vial.app import VialBlueprint +from vial.view import ItemShowView, ItemCreateView, ItemUpdateView +from vial.view.mixin import HTMLMixin, SQLAlchemyMixin from hawat.blueprints.settings_reporting.forms import CreateSettingsReportingForm,\ UpdateSettingsReportingForm @@ -60,19 +61,19 @@ class ShowView(HTMLMixin, SQLAlchemyMixin, ItemShowView): @classmethod def get_view_icon(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_icon`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_icon`.""" return 'module-settings-reporting' @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('Show reporting settings') #--------------------------------------------------------------------------- @property def dbmodel(self): - """*Implementation* of :py:func:`hawat.base.SQLAlchemyMixin.dbmodel`.""" + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" return SettingsReportingModel @classmethod @@ -81,22 +82,22 @@ class ShowView(HTMLMixin, SQLAlchemyMixin, ItemShowView): Perform access authorization for current user to particular item. """ permission_mm = flask_principal.Permission( - hawat.acl.MembershipNeed(kwargs['item'].group.id), - hawat.acl.ManagementNeed(kwargs['item'].group.id) + vial.acl.MembershipNeed(kwargs['item'].group.id), + vial.acl.ManagementNeed(kwargs['item'].group.id) ) - return hawat.acl.PERMISSION_POWER.can() or permission_mm.can() + return vial.acl.PERMISSION_POWER.can() or permission_mm.can() @classmethod def get_breadcrumbs_menu(cls): # pylint: disable=locally-disabled,unused-argument """ Get breadcrumbs menu. """ - action_menu = hawat.menu.Menu() + action_menu = vial.menu.Menu() action_menu.add_entry( 'endpoint', 'home', - endpoint = flask.current_app.config['HAWAT_ENDPOINT_HOME'] + endpoint = flask.current_app.config['ENDPOINT_HOME'] ) action_menu.add_entry( 'endpoint', @@ -121,7 +122,7 @@ class ShowView(HTMLMixin, SQLAlchemyMixin, ItemShowView): """ Get action menu for particular item. """ - action_menu = hawat.menu.Menu() + action_menu = vial.menu.Menu() action_menu.add_entry( 'endpoint', 'showgroup', @@ -177,51 +178,56 @@ class CreateView(HTMLMixin, SQLAlchemyMixin, ItemCreateView): # pylint: disable @classmethod def get_menu_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('Create reporting settings') @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" return lazy_gettext('Create new reporting settings') #--------------------------------------------------------------------------- @property def dbmodel(self): - """*Implementation* of :py:func:`hawat.base.SQLAlchemyMixin.dbmodel`.""" + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" return SettingsReportingModel + @property + def dbchlogmodel(self): + """*Implementation* of :py:func:`vial.view.ItemActionView.dbchlogmodel`.""" + return ItemChangeLogModel + @classmethod def authorize_item_action(cls, **kwargs): """ Perform access authorization for current user to particular item. """ permission_m = flask_principal.Permission( - hawat.acl.ManagementNeed(kwargs['item'].group.id) + vial.acl.ManagementNeed(kwargs['item'].group.id) ) - return hawat.acl.PERMISSION_POWER.can() or permission_m.can() + return vial.acl.PERMISSION_POWER.can() or permission_m.can() #--------------------------------------------------------------------------- @staticmethod def get_message_success(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_success`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_success`.""" return gettext('Reporting settings <strong>%(item_id)s</strong> for group <strong>%(parent_id)s</strong> were successfully created.', item_id = str(kwargs['item']), parent_id = str(kwargs['item'].group)) @staticmethod def get_message_failure(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_failure`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_failure`.""" return gettext('Unable to create new reporting settings for group <strong>%(parent_id)s</strong>.', parent_id = str(kwargs['item'].group)) @staticmethod def get_message_cancel(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_cancel`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_cancel`.""" return gettext('Canceled creating new reporting settings for group <strong>%(parent_id)s</strong>.', parent_id = str(kwargs['item'].group)) @staticmethod def get_item_form(): - """*Implementation* of :py:func:`hawat.base.ItemCreateView.get_item_form`.""" + """*Implementation* of :py:func:`vial.view.ItemCreateView.get_item_form`.""" return CreateSettingsReportingForm() def do_before_response(self, **kwargs): @@ -241,56 +247,61 @@ class UpdateView(HTMLMixin, SQLAlchemyMixin, ItemUpdateView): # pylint: disable @classmethod def get_menu_legend(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('Update details of reporting settings for group "%(item)s"', item = kwargs['item'].group.name) @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" return lazy_gettext('Update reporting settings details') #--------------------------------------------------------------------------- @property def dbmodel(self): - """*Implementation* of :py:func:`hawat.base.SQLAlchemyMixin.dbmodel`.""" + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" return SettingsReportingModel + @property + def dbchlogmodel(self): + """*Implementation* of :py:func:`vial.view.ItemActionView.dbchlogmodel`.""" + return ItemChangeLogModel + @classmethod def authorize_item_action(cls, **kwargs): """ Perform access authorization for current user to particular item. """ permission_m = flask_principal.Permission( - hawat.acl.ManagementNeed(kwargs['item'].group.id) + vial.acl.ManagementNeed(kwargs['item'].group.id) ) - return hawat.acl.PERMISSION_POWER.can() or permission_m.can() + return vial.acl.PERMISSION_POWER.can() or permission_m.can() #--------------------------------------------------------------------------- @staticmethod def get_message_success(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_success`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_success`.""" return gettext('Reporting settings <strong>%(item_id)s</strong> for group <strong>%(parent_id)s</strong> were successfully updated.', item_id = str(kwargs['item']), parent_id = str(kwargs['item'].group)) @staticmethod def get_message_failure(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_failure`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_failure`.""" return gettext('Unable to update reporting settings <strong>%(item_id)s</strong> for group <strong>%(parent_id)s</strong>.', item_id = str(kwargs['item']), parent_id = str(kwargs['item'].group)) @staticmethod def get_message_cancel(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_cancel`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_cancel`.""" return gettext('Canceled updating reporting settings <strong>%(item_id)s</strong> for group <strong>%(parent_id)s</strong>.', item_id = str(kwargs['item']), parent_id = str(kwargs['item'].group)) @staticmethod def get_item_form(item): - """*Implementation* of :py:func:`hawat.base.ItemUpdateView.get_item_form`.""" + """*Implementation* of :py:func:`vial.view.ItemUpdateView.get_item_form`.""" return UpdateSettingsReportingForm(obj = item) def get_url_next(self): """ - *Hook method*. Implementation of :py:func:`hawat.base.ItemUpdateView.get_url_next` interface. + *Hook method*. Implementation of :py:func:`vial.view.ItemUpdateView.get_url_next` interface. """ return flask.url_for('groups.list') @@ -311,7 +322,7 @@ class SettingsReportingBlueprint(VialBlueprint): @classmethod def get_module_title(cls): - """*Implementation* of :py:func:`hawat.base.VialBlueprint.get_module_title`.""" + """*Implementation* of :py:func:`vial.app.VialBlueprint.get_module_title`.""" return lazy_gettext('Reporting settings management pluggable module') @@ -321,7 +332,7 @@ class SettingsReportingBlueprint(VialBlueprint): def get_blueprint(): """ Mandatory interface and factory function. This function must return a valid - instance of :py:class:`hawat.base.VialBlueprint` or :py:class:`flask.Blueprint`. + instance of :py:class:`vial.app.VialBlueprint` or :py:class:`flask.Blueprint`. """ hbp = SettingsReportingBlueprint( diff --git a/lib/hawat/blueprints/settings_reporting/forms.py b/lib/hawat/blueprints/settings_reporting/forms.py index c6e8c2ac413808245753a1ac5d3514c44b958332..12b5dc9e2eb47f5d835ae4410dc8753d25e6a09d 100644 --- a/lib/hawat/blueprints/settings_reporting/forms.py +++ b/lib/hawat/blueprints/settings_reporting/forms.py @@ -31,9 +31,11 @@ from flask_babel import gettext, lazy_gettext # # Custom modules. # -import hawat.const +import vial.const +import vial.forms + import hawat.db -import hawat.forms + import mentat.const from mentat.datatype.sqldb import GroupModel @@ -85,7 +87,7 @@ def check_defined_when_custom(form, field): # pylint: disable=locally-disabled, ) -class BaseSettingsReportingForm(hawat.forms.BaseItemForm): +class BaseSettingsReportingForm(vial.forms.BaseItemForm): """ Class representing base reporting settings form. """ @@ -117,14 +119,14 @@ class BaseSettingsReportingForm(hawat.forms.BaseItemForm): ], filters = [lambda x: x or None] ) - emails = hawat.forms.CommaListField( + emails = vial.forms.CommaListField( lazy_gettext('Target emails:'), validators = [ wtforms.validators.Optional(), - hawat.forms.check_email_list + vial.forms.check_email_list ] ) - mute = hawat.forms.RadioFieldWithNone( + mute = vial.forms.RadioFieldWithNone( lazy_gettext('Mute reporting:'), validators = [ wtforms.validators.Optional(), @@ -134,10 +136,10 @@ class BaseSettingsReportingForm(hawat.forms.BaseItemForm): (True, lazy_gettext('Enabled')), (False, lazy_gettext('Disabled')) ], - filters = [hawat.forms.str_to_bool_with_none], - coerce = hawat.forms.str_to_bool_with_none + filters = [vial.forms.str_to_bool_with_none], + coerce = vial.forms.str_to_bool_with_none ) - redirect = hawat.forms.RadioFieldWithNone( + redirect = vial.forms.RadioFieldWithNone( lazy_gettext('Report redirection:'), validators = [ wtforms.validators.Optional(), @@ -147,10 +149,10 @@ class BaseSettingsReportingForm(hawat.forms.BaseItemForm): (True, lazy_gettext('Enabled')), (False, lazy_gettext('Disabled')) ], - filters = [hawat.forms.str_to_bool_with_none], - coerce = hawat.forms.str_to_bool_with_none + filters = [vial.forms.str_to_bool_with_none], + coerce = vial.forms.str_to_bool_with_none ) - compress = hawat.forms.RadioFieldWithNone( + compress = vial.forms.RadioFieldWithNone( lazy_gettext('Attachment compression:'), validators = [ wtforms.validators.Optional(), @@ -160,8 +162,8 @@ class BaseSettingsReportingForm(hawat.forms.BaseItemForm): (True, lazy_gettext('Enabled')), (False, lazy_gettext('Disabled')) ], - filters = [hawat.forms.str_to_bool_with_none], - coerce = hawat.forms.str_to_bool_with_none + filters = [vial.forms.str_to_bool_with_none], + coerce = vial.forms.str_to_bool_with_none ) template = wtforms.StringField( lazy_gettext('Template:'), @@ -186,19 +188,19 @@ class BaseSettingsReportingForm(hawat.forms.BaseItemForm): choices = [('', lazy_gettext('<< system default >>'))] + list(zip(pytz.common_timezones, pytz.common_timezones)), filters = [lambda x: x or None] ) - max_attachment_size = hawat.forms.SelectFieldWithNone( + max_attachment_size = vial.forms.SelectFieldWithNone( lazy_gettext('Attachment size limit:'), validators = [ wtforms.validators.Optional(), ], - coerce = hawat.forms.str_to_int_with_none, + coerce = vial.forms.str_to_int_with_none, choices = [ (None, lazy_gettext('<< system default >>')), (0, lazy_gettext('<< no limit >>')) ] + list(reversed(sorted(mentat.const.REPORT_ATTACHMENT_SIZES.items(), key = lambda x: x[0]))), default = mentat.const.DFLT_REPORTING_MAXATTACHSIZE ) - timing = hawat.forms.RadioFieldWithNone( + timing = vial.forms.RadioFieldWithNone( lazy_gettext('Reporting timing:'), validators = [ wtforms.validators.Optional(), diff --git a/lib/hawat/blueprints/settings_reporting/templates/settings_reporting/creatupdate.html b/lib/hawat/blueprints/settings_reporting/templates/settings_reporting/creatupdate.html index 4115906931a38b5a4aa59e82fba8e559bb9ba481..5626e749d006489dc67d65107f694509654d7f31 100644 --- a/lib/hawat/blueprints/settings_reporting/templates/settings_reporting/creatupdate.html +++ b/lib/hawat/blueprints/settings_reporting/templates/settings_reporting/creatupdate.html @@ -16,7 +16,7 @@ <form method="POST" action="{{ form_url }}"> <fieldset> - <legend>{{ hawat_current_view.get_view_title() }}</legend> + <legend>{{ vial_current_view.get_view_title() }}</legend> {%- if item_action == 'create' %} {{ macros_form.render_form_item_default(form.group) }} diff --git a/lib/hawat/blueprints/settings_reporting/templates/settings_reporting/show.html b/lib/hawat/blueprints/settings_reporting/templates/settings_reporting/show.html index fb3ac2b54cec9d4caa914f9064e4b3079c680292..fae17fab2473fe3de84f38dad42a9e5686dc42f6 100644 --- a/lib/hawat/blueprints/settings_reporting/templates/settings_reporting/show.html +++ b/lib/hawat/blueprints/settings_reporting/templates/settings_reporting/show.html @@ -6,7 +6,7 @@ <div class="col-lg-12"> {{ macros_page.render_breadcrumbs(item) }} - <h2>{{ hawat_current_view.get_view_title() }}</h2> + <h2>{{ vial_current_view.get_view_title() }}</h2> <hr> <h3>{{ gettext('Reporting settings for group %(item)s', item = item.group.name) }}{% if item.group.description %} <small>{{ item.group.description }}</small>{% endif %}</h3> <div class="pull-right"> diff --git a/lib/hawat/blueprints/skeleton/__init__.py b/lib/hawat/blueprints/skeleton/__init__.py index c009673deb039d0a52da0f5c704f71501d78b3e3..5dde44c3b222ebf417cebd4038e4a96cf1f341e5 100644 --- a/lib/hawat/blueprints/skeleton/__init__.py +++ b/lib/hawat/blueprints/skeleton/__init__.py @@ -24,15 +24,16 @@ __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea Kropáčová <andrea import flask_login from flask_babel import lazy_gettext -import hawat.base -import hawat.db +from vial.app import VialBlueprint +from vial.view import SimpleView +from vial.view.mixin import HTMLMixin BLUEPRINT_NAME = 'skeleton' """Name of the blueprint as module global constant.""" -class ExampleView(hawat.base.HTMLMixin, hawat.base.SimpleView): +class ExampleView(HTMLMixin, SimpleView): """ Example simple view. """ @@ -41,22 +42,22 @@ class ExampleView(hawat.base.HTMLMixin, hawat.base.SimpleView): @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" return 'example' @classmethod def get_view_icon(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_icon`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_icon`.""" return 'example' @classmethod def get_menu_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return 'Example view' @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" return 'Example view' @classmethod @@ -64,31 +65,31 @@ class ExampleView(hawat.base.HTMLMixin, hawat.base.SimpleView): return '{}/example.html'.format(BLUEPRINT_NAME) def do_before_response(self, **kwargs): - """*Implementation* of :py:func:`hawat.base.RenderableView.do_before_response`.""" + """*Implementation* of :py:func:`vial.view.RenderableView.do_before_response`.""" #------------------------------------------------------------------------------- -class SkeletonBlueprint(hawat.base.VialBlueprint): +class SkeletonBlueprint(VialBlueprint): """ Hawat pluggable module - skeleton. """ @classmethod def get_module_title(cls): - """*Implementation* of :py:func:`hawat.base.VialBlueprint.get_module_title`.""" + """*Implementation* of :py:func:`vial.app.VialBlueprint.get_module_title`.""" return lazy_gettext('Skeleton module') def register_app(self, app): """ - *Callback method*. Will be called from :py:func:`hawat.base.Vial.register_blueprint` + *Callback method*. Will be called from :py:func:`hawat.base.HawatApp.register_blueprint` method and can be used to customize the Flask application object. Possible use cases: * application menu customization - :param hawat.base.Vial app: Flask application to be customized. + :param hawat.base.HawatApp app: Flask application to be customized. """ app.menu_main.add_entry( 'view', @@ -104,7 +105,7 @@ class SkeletonBlueprint(hawat.base.VialBlueprint): def get_blueprint(): """ Mandatory interface and factory function. This function must return a valid - instance of :py:class:`hawat.base.VialBlueprint` or :py:class:`flask.Blueprint`. + instance of :py:class:`vial.app.VialBlueprint` or :py:class:`flask.Blueprint`. """ hbp = SkeletonBlueprint( diff --git a/lib/hawat/blueprints/skeleton/templates/skeleton/search.html b/lib/hawat/blueprints/skeleton/templates/skeleton/search.html index b38b74718a7ce114a06192dc4fada81d138b39b2..9a9676efc1c292eabcd010c0c29159bbaa51cab6 100644 --- a/lib/hawat/blueprints/skeleton/templates/skeleton/search.html +++ b/lib/hawat/blueprints/skeleton/templates/skeleton/search.html @@ -4,7 +4,7 @@ <div class="row"> <div class="col-lg-12"> - <h2>{{ hawat_current_view.get_view_title() }}</h2> + <h2>{{ vial_current_view.get_view_title() }}</h2> <form class="form-inline" id="frm-whi" method="get" action="{{ url_for('whois.search') }}"> <div class="form-group"> diff --git a/lib/hawat/blueprints/status/__init__.py b/lib/hawat/blueprints/status/__init__.py index f42db6e46c83b17ad5a42122ea72469dc5a0acfe..59f2d0a04e464353c5f1fc7bd5c6796d6da7c77b 100644 --- a/lib/hawat/blueprints/status/__init__.py +++ b/lib/hawat/blueprints/status/__init__.py @@ -46,8 +46,10 @@ from flask_babel import lazy_gettext # import pyzenkit.jsonconf import mentat.system -import hawat.acl -from hawat.base import HTMLMixin, SimpleView, VialBlueprint +import vial.acl +from vial.app import VialBlueprint +from vial.view import SimpleView +from vial.view.mixin import HTMLMixin BLUEPRINT_NAME = 'status' @@ -60,30 +62,30 @@ class ViewView(HTMLMixin, SimpleView): """ authentication = True - authorization = [hawat.acl.PERMISSION_ADMIN] + authorization = [vial.acl.PERMISSION_ADMIN] @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" return 'view' @classmethod def get_view_icon(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_icon`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_icon`.""" return 'module-{}'.format(BLUEPRINT_NAME) @classmethod def get_menu_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('System status') @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" return lazy_gettext('System status') def do_before_response(self, **kwargs): - """*Implementation* of :py:func:`hawat.base.RenderableView.do_before_response`.""" + """*Implementation* of :py:func:`vial.view.RenderableView.do_before_response`.""" controller_cfg = pyzenkit.jsonconf.json_load( flask.current_app.config['MENTAT_CONTROLLER_CFG'], ) @@ -120,18 +122,18 @@ class StatusBlueprint(VialBlueprint): @classmethod def get_module_title(cls): - """*Implementation* of :py:func:`hawat.base.VialBlueprint.get_module_title`.""" + """*Implementation* of :py:func:`vial.app.VialBlueprint.get_module_title`.""" return lazy_gettext('System status pluggable module') def register_app(self, app): """ - *Callback method*. Will be called from :py:func:`hawat.base.Vial.register_blueprint` + *Callback method*. Will be called from :py:func:`hawat.base.HawatApp.register_blueprint` method and can be used to customize the Flask application object. Possible use cases: * application menu customization - :param hawat.base.Vial app: Flask application to be customized. + :param hawat.base.HawatApp app: Flask application to be customized. """ app.menu_main.add_entry( 'view', @@ -148,7 +150,7 @@ class StatusBlueprint(VialBlueprint): def get_blueprint(): """ Mandatory interface and factory function. This function must return a valid - instance of :py:class:`hawat.base.VialBlueprint` or :py:class:`flask.Blueprint`. + instance of :py:class:`vial.app.VialBlueprint` or :py:class:`flask.Blueprint`. """ hbp = StatusBlueprint( diff --git a/lib/hawat/blueprints/status/templates/status/view.html b/lib/hawat/blueprints/status/templates/status/view.html index 888a3001ad9136891c6aa9d0151e5e2ebc2a8920..8f0fc79f1e907598893ce78617f3c9bfd114a3f9 100644 --- a/lib/hawat/blueprints/status/templates/status/view.html +++ b/lib/hawat/blueprints/status/templates/status/view.html @@ -8,7 +8,7 @@ <li><a href="{{ url_for('home.index') }}">{{ gettext('Home') }}</a></li> <li class="active">{{ gettext('System status') }}</li> </ol> - <h2>{{ hawat_current_view.get_view_title() }}</h2> + <h2>{{ vial_current_view.get_view_title() }}</h2> <hr> <p> <strong>{{ gettext('Overall status:')}}</strong> diff --git a/lib/hawat/blueprints/timeline/__init__.py b/lib/hawat/blueprints/timeline/__init__.py index 12219c057ad3d7a7304fc3b68712bc8c0b5daef8..2e6a0e14d243baf9e480e53ad05ef05034b35a36 100644 --- a/lib/hawat/blueprints/timeline/__init__.py +++ b/lib/hawat/blueprints/timeline/__init__.py @@ -35,11 +35,14 @@ import mentat.stats.idea import mentat.services.eventstorage from mentat.const import tr_ -import hawat.const import hawat.events -import hawat.acl -from hawat.base import HTMLMixin, PsycopgMixin, AJAXMixin,\ - BaseSearchView, VialBlueprint, URLParamsBuilder +import vial.const +import vial.acl +from vial.app import VialBlueprint +from vial.view import BaseSearchView +from vial.view.mixin import HTMLMixin, AJAXMixin +from vial.utils import URLParamsBuilder +from hawat.base import PsycopgMixin from hawat.blueprints.timeline.forms import SimpleTimelineSearchForm @@ -67,7 +70,7 @@ 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(hawat.forms.default_dt_with_delta()) + form.dt_from.process_data(vial.forms.default_dt_with_delta()) return form @@ -79,36 +82,41 @@ class AbstractSearchView(PsycopgMixin, BaseSearchView): # pylint: disable=local """ authentication = True - authorization = [hawat.acl.PERMISSION_ANY] + authorization = [vial.acl.PERMISSION_ANY] url_params_unsupported = ('page', 'limit', 'sortby') @classmethod - def get_menu_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" - return lazy_gettext('Timeline') + def get_view_icon(cls): + """*Implementation* of :py:func:`vial.view.BaseView.get_view_icon`.""" + return 'module-{}'.format(cls.module_name) @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" return lazy_gettext('Search event timeline') + @classmethod + def get_menu_title(cls, **kwargs): + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" + return lazy_gettext('Timeline') + @staticmethod def get_search_form(request_args): """ - *Interface implementation* of :py:func:`hawat.base.SearchView.get_search_form`. + *Interface implementation* of :py:func:`vial.view.BaseSearchView.get_search_form`. """ return _get_search_form(request_args) def do_before_search(self, form_data): # pylint: disable=locally-disabled,no-self-use,unused-argument """ - *Interface implementation* of :py:func:`hawat.base.SearchView.do_before_search`. + *Interface implementation* of :py:func:`vial.view.BaseSearchView.do_before_search`. """ form_data['groups'] = [item.name for item in form_data['groups']] def do_after_search(self, items): """ - *Interface implementation* of :py:func:`hawat.base.SearchView.do_after_search`. + *Interface implementation* of :py:func:`vial.view.BaseSearchView.do_after_search`. """ self.logger.debug( "Calculating IDEA event timeline from %d records.", @@ -140,7 +148,7 @@ class AbstractSearchView(PsycopgMixin, BaseSearchView): # pylint: disable=local self.response_context.pop('items', None) def do_before_response(self, **kwargs): - """*Implementation* of :py:func:`hawat.base.RenderableView.do_before_response`.""" + """*Implementation* of :py:func:`vial.view.RenderableView.do_before_response`.""" self.response_context.update( quicksearch_list = self.get_quicksearch_by_time() ) @@ -163,13 +171,13 @@ class SearchView(HTMLMixin, AbstractSearchView): # pylint: disable=locally-disa @classmethod def get_breadcrumbs_menu(cls): """ - *Interface implementation* of :py:func:`hawat.base.SearchView.get_breadcrumbs_menu`. + *Interface implementation* of :py:func:`vial.view.BaseSearchView.get_breadcrumbs_menu`. """ - breadcrumbs_menu = hawat.menu.Menu() + breadcrumbs_menu = vial.menu.Menu() breadcrumbs_menu.add_entry( 'endpoint', 'home', - endpoint = flask.current_app.config['HAWAT_ENDPOINT_HOME'] + endpoint = flask.current_app.config['ENDPOINT_HOME'] ) breadcrumbs_menu.add_entry( 'endpoint', @@ -188,7 +196,7 @@ class APISearchView(AJAXMixin, AbstractSearchView): # pylint: disable=locally-d @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" return 'apisearch' @@ -202,18 +210,18 @@ class TimelineBlueprint(VialBlueprint): @classmethod def get_module_title(cls): - """*Implementation* of :py:func:`hawat.base.VialBlueprint.get_module_title`.""" + """*Implementation* of :py:func:`vial.app.VialBlueprint.get_module_title`.""" return lazy_gettext('IDEA event timelines pluggable module') def register_app(self, app): """ - *Callback method*. Will be called from :py:func:`hawat.base.Vial.register_blueprint` + *Callback method*. Will be called from :py:func:`hawat.base.HawatApp.register_blueprint` method and can be used to customize the Flask application object. Possible use cases: * application menu customization - :param hawat.base.Vial app: Flask application to be customized. + :param hawat.base.HawatApp app: Flask application to be customized. """ app.menu_main.add_entry( 'view', @@ -237,7 +245,7 @@ class TimelineBlueprint(VialBlueprint): def get_blueprint(): """ Mandatory interface and factory function. This function must return a valid - instance of :py:class:`hawat.base.VialBlueprint` or :py:class:`flask.Blueprint`. + instance of :py:class:`vial.app.VialBlueprint` or :py:class:`flask.Blueprint`. """ hbp = TimelineBlueprint( diff --git a/lib/hawat/blueprints/timeline/forms.py b/lib/hawat/blueprints/timeline/forms.py index 89478abb7f4001af9a359eefc6406cb33f6bf2d8..aecdfe1a4f8facff0688ecda1efe054f0b8a0480 100644 --- a/lib/hawat/blueprints/timeline/forms.py +++ b/lib/hawat/blueprints/timeline/forms.py @@ -24,9 +24,9 @@ from wtforms.ext.sqlalchemy.fields import QuerySelectMultipleField import flask_wtf from flask_babel import lazy_gettext -import hawat.const +import vial.const +import vial.forms import hawat.db -import hawat.forms from mentat.datatype.sqldb import GroupModel @@ -41,82 +41,82 @@ class SimpleTimelineSearchForm(flask_wtf.FlaskForm): """ Class representing simple event timeline search form. """ - dt_from = hawat.forms.SmartDateTimeField( + dt_from = vial.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.') ) - dt_to = hawat.forms.SmartDateTimeField( + dt_to = vial.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.') ) - st_from = hawat.forms.SmartDateTimeField( + st_from = vial.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 = hawat.forms.SmartDateTimeField( + st_to = vial.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 = hawat.forms.CommaListField( + source_addrs = vial.forms.CommaListField( lazy_gettext('Source addresses:'), validators = [ wtforms.validators.Optional(), - hawat.forms.check_network_record_list + vial.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 = hawat.forms.CommaListField( + target_addrs = vial.forms.CommaListField( lazy_gettext('Target addresses:'), validators = [ wtforms.validators.Optional(), - hawat.forms.check_network_record_list + vial.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 = hawat.forms.CommaListField( + host_addrs = vial.forms.CommaListField( lazy_gettext('Host addresses:'), validators = [ wtforms.validators.Optional(), - hawat.forms.check_network_record_list + vial.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 = hawat.forms.CommaListField( + source_ports = vial.forms.CommaListField( lazy_gettext('Source ports:'), validators = [ wtforms.validators.Optional(), - hawat.forms.check_port_list + vial.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 = hawat.forms.CommaListField( + target_ports = vial.forms.CommaListField( lazy_gettext('Target ports:'), validators = [ wtforms.validators.Optional(), - hawat.forms.check_port_list + vial.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 = hawat.forms.CommaListField( + host_ports = vial.forms.CommaListField( lazy_gettext('Host ports:'), validators = [ wtforms.validators.Optional(), - hawat.forms.check_port_list + vial.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/users/__init__.py b/lib/hawat/blueprints/users/__init__.py index 54e2b58398fd00b110022072ce6224b1cf7b23a9..036fd6a186ee9716de8ab8c28075cff66415158a 100644 --- a/lib/hawat/blueprints/users/__init__.py +++ b/lib/hawat/blueprints/users/__init__.py @@ -43,12 +43,10 @@ from flask_babel import gettext, lazy_gettext, force_locale # from mentat.datatype.sqldb import UserModel, GroupModel, ItemChangeLogModel -import hawat.const -import hawat.db -import hawat.acl -from hawat.base import HTMLMixin, SQLAlchemyMixin, ItemListView,\ - ItemShowView, ItemCreateView, ItemUpdateView, ItemEnableView,\ - ItemDisableView, ItemDeleteView, ItemObjectRelationView, VialBlueprint +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 hawat.blueprints.users.forms import CreateUserAccountForm, UpdateUserAccountForm,\ AdminUpdateUserAccountForm @@ -65,24 +63,24 @@ class ListView(HTMLMixin, SQLAlchemyMixin, ItemListView): authentication = True - authorization = [hawat.acl.PERMISSION_POWER] + authorization = [vial.acl.PERMISSION_POWER] @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('User management') #--------------------------------------------------------------------------- @property def dbmodel(self): - """*Implementation* of :py:func:`hawat.base.SQLAlchemyMixin.dbmodel`.""" + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" return UserModel @classmethod def get_action_menu(cls): - """*Implementation* of :py:func:`hawat.base.ItemListView.get_action_menu`.""" - action_menu = hawat.menu.Menu() + """*Implementation* of :py:func:`vial.view.ItemListView.get_action_menu`.""" + action_menu = vial.menu.Menu() action_menu.add_entry( 'endpoint', 'create', @@ -93,8 +91,8 @@ class ListView(HTMLMixin, SQLAlchemyMixin, ItemListView): @classmethod def get_context_action_menu(cls): - """*Implementation* of :py:func:`hawat.base.ItemListView.get_context_action_menu`.""" - action_menu = hawat.menu.Menu() + """*Implementation* of :py:func:`vial.view.ItemListView.get_context_action_menu`.""" + action_menu = vial.menu.Menu() action_menu.add_entry( 'endpoint', 'show', @@ -138,24 +136,24 @@ class ShowView(HTMLMixin, SQLAlchemyMixin, ItemShowView): @classmethod def get_view_icon(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_icon`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_icon`.""" return 'action-show-user' @classmethod def get_menu_legend(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('Show details of user account "%(item)s"', item = kwargs['item'].login) @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" return lazy_gettext('Show user account details') #--------------------------------------------------------------------------- @property def dbmodel(self): - """*Implementation* of :py:func:`hawat.base.SQLAlchemyMixin.dbmodel`.""" + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" return UserModel @classmethod @@ -168,16 +166,16 @@ class ShowView(HTMLMixin, SQLAlchemyMixin, ItemShowView): 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] + needs = [vial.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() + return vial.acl.PERMISSION_POWER.can() or permission_mngr.can() or permission_me.can() @classmethod def get_action_menu(cls): """ Get action menu for particular item. """ - action_menu = hawat.menu.Menu() + action_menu = vial.menu.Menu() action_menu.add_entry( 'endpoint', 'update', @@ -205,9 +203,9 @@ class ShowView(HTMLMixin, SQLAlchemyMixin, ItemShowView): return action_menu def do_before_response(self, **kwargs): # pylint: disable=locally-disabled,no-self-use,unused-argument - """*Implementation* of :py:func:`hawat.base.RenderableView.do_before_response`.""" + """*Implementation* of :py:func:`vial.view.RenderableView.do_before_response`.""" item = self.response_context['item'] - action_menu = hawat.menu.Menu() + action_menu = vial.menu.Menu() action_menu.add_entry( 'endpoint', 'show', @@ -285,32 +283,32 @@ class MeView(ShowView): # pylint: disable=locally-disabled,too-many-ancestors @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" return 'me' @classmethod def get_view_icon(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_icon`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_icon`.""" return 'profile' @classmethod def get_menu_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('My account') @classmethod def get_view_url(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_url`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_url`.""" return flask.url_for(cls.get_view_endpoint()) @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" return lazy_gettext('My user account') @classmethod def get_view_template(cls): - """*Implementation* of :py:func:`hawat.base.RenderableView.get_view_template`.""" + """*Implementation* of :py:func:`vial.view.RenderableView.get_view_template`.""" return '{}/show.html'.format(BLUEPRINT_NAME) @classmethod @@ -325,11 +323,11 @@ class MeView(ShowView): # pylint: disable=locally-disabled,too-many-ancestors """ Get breadcrumbs menu. """ - action_menu = hawat.menu.Menu() + action_menu = vial.menu.Menu() action_menu.add_entry( 'endpoint', 'home', - endpoint = flask.current_app.config['HAWAT_ENDPOINT_HOME'] + endpoint = flask.current_app.config['ENDPOINT_HOME'] ) action_menu.add_entry( 'endpoint', @@ -373,45 +371,50 @@ class CreateView(HTMLMixin, SQLAlchemyMixin, ItemCreateView): # pylint: disable authentication = True - authorization = [hawat.acl.PERMISSION_POWER] + authorization = [vial.acl.PERMISSION_POWER] @classmethod def get_view_icon(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_icon`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_icon`.""" return 'action-create-user' @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" return lazy_gettext('Create new user account') #--------------------------------------------------------------------------- @property def dbmodel(self): - """*Implementation* of :py:func:`hawat.base.SQLAlchemyMixin.dbmodel`.""" + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" return UserModel + @property + def dbchlogmodel(self): + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" + return ItemChangeLogModel + #--------------------------------------------------------------------------- @staticmethod def get_message_success(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_success`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_success`.""" return gettext('User account <strong>%(item_id)s</strong> was successfully created.', item_id = str(kwargs['item'])) @staticmethod def get_message_failure(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_failure`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_failure`.""" return gettext('Unable to create new user account.') @staticmethod def get_message_cancel(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_cancel`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_cancel`.""" return gettext('Canceled creating new user account.') @staticmethod def get_item_form(): - """*Implementation* of :py:func:`hawat.base.ItemCreateView.get_item_form`.""" + """*Implementation* of :py:func:`vial.view.ItemCreateView.get_item_form`.""" # # 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 @@ -436,26 +439,31 @@ class UpdateView(HTMLMixin, SQLAlchemyMixin, ItemUpdateView): # pylint: disable @classmethod def get_view_icon(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_icon`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_icon`.""" return 'action-update-user' @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" return lazy_gettext('Update user account details') @classmethod def get_menu_legend(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('Update details of user account "%(item)s"', item = kwargs['item'].login) #--------------------------------------------------------------------------- @property def dbmodel(self): - """*Implementation* of :py:func:`hawat.base.SQLAlchemyMixin.dbmodel`.""" + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" return UserModel + @property + def dbchlogmodel(self): + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" + return ItemChangeLogModel + @classmethod def authorize_item_action(cls, **kwargs): """ @@ -464,28 +472,28 @@ class UpdateView(HTMLMixin, SQLAlchemyMixin, ItemUpdateView): # pylint: disable permission_me = flask_principal.Permission( flask_principal.UserNeed(kwargs['item'].id) ) - return hawat.acl.PERMISSION_ADMIN.can() or permission_me.can() + return vial.acl.PERMISSION_ADMIN.can() or permission_me.can() #--------------------------------------------------------------------------- @staticmethod def get_message_success(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_success`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_success`.""" return gettext('User account <strong>%(item_id)s</strong> was successfully updated.', item_id = str(kwargs['item'])) @staticmethod def get_message_failure(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_failure`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_failure`.""" return gettext('Unable to update user account <strong>%(item_id)s</strong>.', item_id = str(kwargs['item'])) @staticmethod def get_message_cancel(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_cancel`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_cancel`.""" return gettext('Canceled updating user account <strong>%(item_id)s</strong>.', item_id = str(kwargs['item'])) @staticmethod def get_item_form(item): - """*Implementation* of :py:func:`hawat.base.ItemUpdateView.get_item_form`.""" + """*Implementation* of :py:func:`vial.view.ItemUpdateView.get_item_form`.""" # # Inject list of choices for supported locales and roles. Another approach @@ -526,17 +534,17 @@ class AddMembershipView(HTMLMixin, SQLAlchemyMixin, ItemObjectRelationView): # @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" return gettext('Add group membership') @classmethod def get_view_icon(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_icon`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_icon`.""" return 'action-add-member' @classmethod def get_menu_legend(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext( 'Add user "%(user_id)s" to group "%(group_id)s"', user_id = str(kwargs['item']), @@ -547,7 +555,7 @@ class AddMembershipView(HTMLMixin, SQLAlchemyMixin, ItemObjectRelationView): # @property def dbmodel(self): - """*Implementation* of :py:func:`hawat.base.SQLAlchemyMixin.dbmodel`.""" + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" return UserModel @property @@ -555,6 +563,11 @@ class AddMembershipView(HTMLMixin, SQLAlchemyMixin, ItemObjectRelationView): # """*Implementation* of :py:func:`hawat.base.AddMemberView.dbmodel_other`.""" return GroupModel + @property + def dbchlogmodel(self): + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" + return ItemChangeLogModel + #--------------------------------------------------------------------------- @classmethod @@ -563,9 +576,9 @@ class AddMembershipView(HTMLMixin, SQLAlchemyMixin, ItemObjectRelationView): # Perform access authorization for current user to particular item. """ permission_m = flask_principal.Permission( - hawat.acl.ManagementNeed(kwargs['other'].id) + vial.acl.ManagementNeed(kwargs['other'].id) ) - return hawat.acl.PERMISSION_POWER.can() or permission_m.can() + return vial.acl.PERMISSION_POWER.can() or permission_m.can() @classmethod def validate_item_change(cls, **kwargs): # pylint: disable=locally-disabled,unused-argument @@ -580,7 +593,7 @@ class AddMembershipView(HTMLMixin, SQLAlchemyMixin, ItemObjectRelationView): # @classmethod def change_item(cls, **kwargs): """ - *Interface implementation* of :py:func:`hawat.base.ItemChangeView.change_item`. + *Interface implementation* of :py:func:`vial.view.ItemChangeView.change_item`. """ kwargs['item'].memberships.append(kwargs['other']) try: @@ -595,7 +608,7 @@ class AddMembershipView(HTMLMixin, SQLAlchemyMixin, ItemObjectRelationView): # @staticmethod def get_message_success(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_success`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_success`.""" return gettext( 'User <strong>%(user_id)s</strong> was successfully added as a member to group <strong>%(group_id)s</strong>.', user_id = str(kwargs['item']), @@ -604,7 +617,7 @@ class AddMembershipView(HTMLMixin, SQLAlchemyMixin, ItemObjectRelationView): # @staticmethod def get_message_failure(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_failure`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_failure`.""" return gettext( 'Unable to add user <strong>%(user_id)s</strong> as a member to group <strong>%(group_id)s</strong>.', user_id = str(kwargs['item']), @@ -613,7 +626,7 @@ class AddMembershipView(HTMLMixin, SQLAlchemyMixin, ItemObjectRelationView): # @staticmethod def get_message_cancel(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_cancel`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_cancel`.""" return gettext( 'Canceled adding user <strong>%(user_id)s</strong> as a member to group <strong>%(group_id)s</strong>.', user_id = str(kwargs['item']), @@ -635,17 +648,17 @@ class RejectMembershipView(HTMLMixin, SQLAlchemyMixin, ItemObjectRelationView): @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" return gettext('Reject group membership') @classmethod def get_view_icon(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_icon`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_icon`.""" return 'action-rej-member' @classmethod def get_menu_legend(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext( 'Reject user`s "%(user_id)s" membership request for group "%(group_id)s"', user_id = str(kwargs['item']), @@ -656,7 +669,7 @@ class RejectMembershipView(HTMLMixin, SQLAlchemyMixin, ItemObjectRelationView): @property def dbmodel(self): - """*Implementation* of :py:func:`hawat.base.SQLAlchemyMixin.dbmodel`.""" + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" return UserModel @property @@ -664,6 +677,11 @@ class RejectMembershipView(HTMLMixin, SQLAlchemyMixin, ItemObjectRelationView): """*Implementation* of :py:func:`hawat.base.AddMemberView.dbmodel_other`.""" return GroupModel + @property + def dbchlogmodel(self): + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" + return ItemChangeLogModel + #--------------------------------------------------------------------------- @classmethod @@ -672,9 +690,9 @@ class RejectMembershipView(HTMLMixin, SQLAlchemyMixin, ItemObjectRelationView): Perform access authorization for current user to particular item. """ permission_m = flask_principal.Permission( - hawat.acl.ManagementNeed(kwargs['other'].id) + vial.acl.ManagementNeed(kwargs['other'].id) ) - return hawat.acl.PERMISSION_POWER.can() or permission_m.can() + return vial.acl.PERMISSION_POWER.can() or permission_m.can() @classmethod def validate_item_change(cls, **kwargs): # pylint: disable=locally-disabled,unused-argument @@ -689,7 +707,7 @@ class RejectMembershipView(HTMLMixin, SQLAlchemyMixin, ItemObjectRelationView): @classmethod def change_item(cls, **kwargs): """ - *Interface implementation* of :py:func:`hawat.base.ItemChangeView.change_item`. + *Interface implementation* of :py:func:`vial.view.ItemChangeView.change_item`. """ kwargs['item'].memberships_wanted.remove(kwargs['other']) @@ -697,7 +715,7 @@ class RejectMembershipView(HTMLMixin, SQLAlchemyMixin, ItemObjectRelationView): @staticmethod def get_message_success(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_success`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_success`.""" return gettext( 'User`s <strong>%(user_id)s</strong> membership request for group <strong>%(group_id)s</strong> was successfully rejected.', user_id = str(kwargs['item']), @@ -706,7 +724,7 @@ class RejectMembershipView(HTMLMixin, SQLAlchemyMixin, ItemObjectRelationView): @staticmethod def get_message_failure(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_failure`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_failure`.""" return gettext( 'Unable to reject user`s <strong>%(user_id)s</strong> membership request for group <strong>%(group_id)s</strong>.', user_id = str(kwargs['item']), @@ -715,7 +733,7 @@ class RejectMembershipView(HTMLMixin, SQLAlchemyMixin, ItemObjectRelationView): @staticmethod def get_message_cancel(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_cancel`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_cancel`.""" return gettext( 'Canceled rejecting user`s <strong>%(user_id)s</strong> membership request for group <strong>%(group_id)s</strong>.', user_id = str(kwargs['item']), @@ -737,17 +755,17 @@ class RemoveMembershipView(HTMLMixin, SQLAlchemyMixin, ItemObjectRelationView): @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" return gettext('Remove group membership') @classmethod def get_view_icon(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_icon`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_icon`.""" return 'action-rem-member' @classmethod def get_menu_legend(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext( 'Remove user "%(user_id)s" from group "%(group_id)s"', user_id = str(kwargs['item']), @@ -758,7 +776,7 @@ class RemoveMembershipView(HTMLMixin, SQLAlchemyMixin, ItemObjectRelationView): @property def dbmodel(self): - """*Implementation* of :py:func:`hawat.base.SQLAlchemyMixin.dbmodel`.""" + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" return UserModel @property @@ -766,6 +784,11 @@ class RemoveMembershipView(HTMLMixin, SQLAlchemyMixin, ItemObjectRelationView): """*Implementation* of :py:func:`hawat.base.AddMemberView.dbmodel_other`.""" return GroupModel + @property + def dbchlogmodel(self): + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" + return ItemChangeLogModel + #--------------------------------------------------------------------------- @classmethod @@ -774,9 +797,9 @@ class RemoveMembershipView(HTMLMixin, SQLAlchemyMixin, ItemObjectRelationView): Perform access authorization for current user to particular item. """ permission_m = flask_principal.Permission( - hawat.acl.ManagementNeed(kwargs['other'].id) + vial.acl.ManagementNeed(kwargs['other'].id) ) - return hawat.acl.PERMISSION_POWER.can() or permission_m.can() + return vial.acl.PERMISSION_POWER.can() or permission_m.can() @classmethod def validate_item_change(cls, **kwargs): # pylint: disable=locally-disabled,unused-argument @@ -791,7 +814,7 @@ class RemoveMembershipView(HTMLMixin, SQLAlchemyMixin, ItemObjectRelationView): @classmethod def change_item(cls, **kwargs): """ - *Interface implementation* of :py:func:`hawat.base.ItemChangeView.change_item`. + *Interface implementation* of :py:func:`vial.view.ItemChangeView.change_item`. """ try: kwargs['item'].memberships.remove(kwargs['other']) @@ -802,7 +825,7 @@ class RemoveMembershipView(HTMLMixin, SQLAlchemyMixin, ItemObjectRelationView): @staticmethod def get_message_success(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_success`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_success`.""" return gettext( 'User <strong>%(user_id)s</strong> was successfully removed as a member from group <strong>%(group_id)s</strong>.', user_id = str(kwargs['item']), @@ -811,7 +834,7 @@ class RemoveMembershipView(HTMLMixin, SQLAlchemyMixin, ItemObjectRelationView): @staticmethod def get_message_failure(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_failure`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_failure`.""" return gettext( 'Unable to remove user <strong>%(user_id)s</strong> as a member from group <strong>%(group_id)s</strong>.', user_id = str(kwargs['item']), @@ -820,7 +843,7 @@ class RemoveMembershipView(HTMLMixin, SQLAlchemyMixin, ItemObjectRelationView): @staticmethod def get_message_cancel(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_cancel`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_cancel`.""" return gettext( 'Canceled removing user <strong>%(user_id)s</strong> as a member from group <strong>%(group_id)s</strong>.', user_id = str(kwargs['item']), @@ -836,40 +859,45 @@ class EnableView(HTMLMixin, SQLAlchemyMixin, ItemEnableView): # pylint: disable authentication = True - authorization = [hawat.acl.PERMISSION_POWER] + authorization = [vial.acl.PERMISSION_POWER] @classmethod def get_view_icon(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_icon`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_icon`.""" return 'action-enable-user' @classmethod def get_menu_legend(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('Enable user account "%(item)s"', item = kwargs['item'].login) #--------------------------------------------------------------------------- @property def dbmodel(self): - """*Implementation* of :py:func:`hawat.base.SQLAlchemyMixin.dbmodel`.""" + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" return UserModel + @property + def dbchlogmodel(self): + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" + return ItemChangeLogModel + #--------------------------------------------------------------------------- @staticmethod def get_message_success(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_success`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_success`.""" return gettext('User account <strong>%(item_id)s</strong> was successfully enabled.', item_id = str(kwargs['item'])) @staticmethod def get_message_failure(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_failure`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_failure`.""" return gettext('Unable to enable user account <strong>%(item_id)s</strong>.', item_id = str(kwargs['item'])) @staticmethod def get_message_cancel(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_cancel`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_cancel`.""" return gettext('Canceled enabling user account <strong>%(item_id)s</strong>.', item_id = str(kwargs['item'])) #--------------------------------------------------------------------------- @@ -888,7 +916,7 @@ class EnableView(HTMLMixin, SQLAlchemyMixin, ItemEnableView): # pylint: disable item_id = account.login ), recipients = [account.email], - bcc = flask.current_app.config['HAWAT_ADMINS'] + bcc = flask.current_app.config['EMAIL_ADMINS'] ) msg.body = flask.render_template( 'users/email_activation.txt', @@ -897,7 +925,7 @@ class EnableView(HTMLMixin, SQLAlchemyMixin, ItemEnableView): # pylint: disable flask.current_app.mailer.send(msg) def do_after_action(self, item): - """*Implementation* of :py:func:`hawat.base.ItemActionView.do_after_action`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.do_after_action`.""" self.inform_user(item) @@ -909,40 +937,45 @@ class DisableView(HTMLMixin, SQLAlchemyMixin, ItemDisableView): # pylint: disab authentication = True - authorization = [hawat.acl.PERMISSION_POWER] + authorization = [vial.acl.PERMISSION_POWER] @classmethod def get_view_icon(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_icon`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_icon`.""" return 'action-disable-user' @classmethod def get_menu_legend(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('Disable user account "%(item)s"', item = kwargs['item'].login) #--------------------------------------------------------------------------- @property def dbmodel(self): - """*Implementation* of :py:func:`hawat.base.SQLAlchemyMixin.dbmodel`.""" + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" return UserModel + @property + def dbchlogmodel(self): + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" + return ItemChangeLogModel + #--------------------------------------------------------------------------- @staticmethod def get_message_success(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_success`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_success`.""" return gettext('User account <strong>%(item_id)s</strong> was successfully disabled.', item_id = str(kwargs['item'])) @staticmethod def get_message_failure(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_failure`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_failure`.""" return gettext('Unable to disable user account <strong>%(item_id)s</strong>.', item_id = str(kwargs['item'])) @staticmethod def get_message_cancel(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_cancel`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_cancel`.""" return gettext('Canceled disabling user account <strong>%(item_id)s</strong>.', item_id = str(kwargs['item'])) @@ -954,40 +987,45 @@ class DeleteView(HTMLMixin, SQLAlchemyMixin, ItemDeleteView): # pylint: disable authentication = True - authorization = [hawat.acl.PERMISSION_ADMIN] + authorization = [vial.acl.PERMISSION_ADMIN] @classmethod def get_view_icon(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_icon`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_icon`.""" return 'action-delete-user' @classmethod def get_menu_legend(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('Delete user account "%(item)s"', item = kwargs['item'].login) #--------------------------------------------------------------------------- @property def dbmodel(self): - """*Implementation* of :py:func:`hawat.base.SQLAlchemyMixin.dbmodel`.""" + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" return UserModel + @property + def dbchlogmodel(self): + """*Implementation* of :py:func:`vial.view.mixin.SQLAlchemyMixin.dbmodel`.""" + return ItemChangeLogModel + #--------------------------------------------------------------------------- @staticmethod def get_message_success(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_success`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_success`.""" return gettext('User account <strong>%(item_id)s</strong> was successfully and permanently deleted.', item_id = str(kwargs['item'])) @staticmethod def get_message_failure(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_failure`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_failure`.""" return gettext('Unable to delete user account <strong>%(item_id)s</strong>.', item_id = str(kwargs['item'])) @staticmethod def get_message_cancel(**kwargs): - """*Implementation* of :py:func:`hawat.base.ItemActionView.get_message_cancel`.""" + """*Implementation* of :py:func:`vial.view.ItemActionView.get_message_cancel`.""" return gettext('Canceled deleting user account <strong>%(item_id)s</strong>.', item_id = str(kwargs['item'])) @@ -1001,18 +1039,18 @@ class UsersBlueprint(VialBlueprint): @classmethod def get_module_title(cls): - """*Implementation* of :py:func:`hawat.base.VialBlueprint.get_module_title`.""" + """*Implementation* of :py:func:`vial.app.VialBlueprint.get_module_title`.""" return lazy_gettext('User account management') def register_app(self, app): """ - *Callback method*. Will be called from :py:func:`hawat.base.Vial.register_blueprint` + *Callback method*. Will be called from :py:func:`hawat.base.HawatApp.register_blueprint` method and can be used to customize the Flask application object. Possible use cases: * application menu customization - :param hawat.base.Vial app: Flask application to be customized. + :param hawat.base.HawatApp app: Flask application to be customized. """ app.menu_main.add_entry( 'view', @@ -1038,7 +1076,7 @@ class UsersBlueprint(VialBlueprint): def get_blueprint(): """ Mandatory interface and factory function. This function must return a valid - instance of :py:class:`hawat.base.VialBlueprint` or :py:class:`flask.Blueprint`. + instance of :py:class:`vial.app.VialBlueprint` or :py:class:`flask.Blueprint`. """ hbp = UsersBlueprint( diff --git a/lib/hawat/blueprints/users/forms.py b/lib/hawat/blueprints/users/forms.py index 32a6af78921735e1227f45e3efb832dc784044ac..73d055a87efd2f32664bf0b826fc79fca8a87cc2 100644 --- a/lib/hawat/blueprints/users/forms.py +++ b/lib/hawat/blueprints/users/forms.py @@ -31,7 +31,7 @@ from flask_babel import gettext, lazy_gettext # Custom modules. # import hawat.db -import hawat.forms +import vial.forms from mentat.datatype.sqldb import UserModel, GroupModel @@ -70,7 +70,7 @@ def get_available_groups(): return hawat.db.db_query(GroupModel).order_by(GroupModel.name).all() -class BaseUserAccountForm(hawat.forms.BaseItemForm): +class BaseUserAccountForm(vial.forms.BaseItemForm): """ Class representing base user account form. """ @@ -149,8 +149,8 @@ class AdminUserAccountForm(BaseUserAccountForm): (True, lazy_gettext('Enabled')), (False, lazy_gettext('Disabled')) ], - filters = [hawat.forms.str_to_bool], - coerce = hawat.forms.str_to_bool + filters = [vial.forms.str_to_bool], + coerce = vial.forms.str_to_bool ) roles = wtforms.SelectMultipleField( lazy_gettext('Roles:'), @@ -192,7 +192,7 @@ class CreateUserAccountForm(AdminUserAccountForm): validators = [ wtforms.validators.DataRequired(), wtforms.validators.Length(min = 3, max = 50), - hawat.forms.check_login, + vial.forms.check_login, check_id_existence ] ) @@ -213,7 +213,7 @@ class AdminUpdateUserAccountForm(AdminUserAccountForm): validators = [ wtforms.validators.DataRequired(), wtforms.validators.Length(min = 3, max = 50), - hawat.forms.check_login, + vial.forms.check_login, check_id_uniqueness ] ) diff --git a/lib/hawat/blueprints/users/templates/users/creatupdate.html b/lib/hawat/blueprints/users/templates/users/creatupdate.html index 210e2f2b18633bfc21ab3716279f919dd9dda30d..46fe84e1c75abea87ae454446e4e7e5cc434d064 100644 --- a/lib/hawat/blueprints/users/templates/users/creatupdate.html +++ b/lib/hawat/blueprints/users/templates/users/creatupdate.html @@ -18,7 +18,7 @@ <form method="POST" action="{{ form_url }}"> <fieldset> - <legend>{{ hawat_current_view.get_view_title() }}</legend> + <legend>{{ vial_current_view.get_view_title() }}</legend> {%- if item_action == 'create' or current_user.has_role('admin') %} {{ macros_form.render_form_item_default(form.login) }} diff --git a/lib/hawat/blueprints/users/templates/users/show.html b/lib/hawat/blueprints/users/templates/users/show.html index 039fe6dbd8a4c92af4927e96ce2642b96f876397..78fb27fb0906aa4e7eedfd54ad5f1b2d9255176b 100644 --- a/lib/hawat/blueprints/users/templates/users/show.html +++ b/lib/hawat/blueprints/users/templates/users/show.html @@ -6,7 +6,7 @@ <div class="col-lg-12"> {{ macros_page.render_breadcrumbs(item) }} - <h2>{{ hawat_current_view.get_view_title() }}</h2> + <h2>{{ vial_current_view.get_view_title() }}</h2> <hr> <h3>{{ item.fullname }} ({{ item.login }})</h3> <div class="pull-right"> diff --git a/lib/hawat/blueprints/whois/__init__.py b/lib/hawat/blueprints/whois/__init__.py index 2fa5c0a7d95d1004f37bed8f4b513271e4123900..46f38bbaf43c9a1fe09d742b342edc8fafb95e6d 100644 --- a/lib/hawat/blueprints/whois/__init__.py +++ b/lib/hawat/blueprints/whois/__init__.py @@ -68,9 +68,14 @@ from flask_babel import lazy_gettext import mentat.services.whois from mentat.const import tr_ +import hawat.const import hawat.db -import hawat.acl -from hawat.base import HTMLMixin, AJAXMixin, SnippetMixin, RenderableView, VialBlueprint, URLParamsBuilder +import vial.const +import vial.acl +from vial.app import VialBlueprint +from vial.view import RenderableView +from vial.view.mixin import HTMLMixin, AJAXMixin, SnippetMixin +from vial.utils import URLParamsBuilder from hawat.blueprints.whois.forms import WhoisSearchForm @@ -86,16 +91,16 @@ class AbstractSearchView(RenderableView): # pylint: disable=locally-disabled,ab """ authentication = True - authorization = [hawat.acl.PERMISSION_ANY] + authorization = [vial.acl.PERMISSION_ANY] @classmethod def get_view_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" return lazy_gettext('Search local WHOIS') @classmethod def get_menu_title(cls, **kwargs): - """*Implementation* of :py:func:`hawat.base.BaseView.get_menu_title`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" return lazy_gettext('Search local WHOIS') #--------------------------------------------------------------------------- @@ -107,7 +112,7 @@ class AbstractSearchView(RenderableView): # pylint: disable=locally-disabled,ab """ form = WhoisSearchForm(flask.request.args, meta = {'csrf': False}) - if hawat.const.FORM_ACTION_SUBMIT in flask.request.args: + if vial.const.FORM_ACTION_SUBMIT in flask.request.args: if form.validate(): form_data = form.data whois_manager = mentat.services.whois.WhoisServiceManager(flask.current_app.mconfig) @@ -139,7 +144,7 @@ class SearchView(HTMLMixin, AbstractSearchView): # pylint: disable=locally-disa @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" return 'search' @@ -152,7 +157,7 @@ class APISearchView(AJAXMixin, AbstractSearchView): # pylint: disable=locally-d @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" return 'apisearch' @@ -172,7 +177,7 @@ class SnippetSearchView(SnippetMixin, AbstractSearchView): # pylint: disable=lo @classmethod def get_view_name(cls): - """*Implementation* of :py:func:`hawat.base.BaseView.get_view_name`.""" + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" return 'sptsearch' @@ -186,18 +191,18 @@ class WhoisBlueprint(VialBlueprint): @classmethod def get_module_title(cls): - """*Implementation* of :py:func:`hawat.base.VialBlueprint.get_module_title`.""" + """*Implementation* of :py:func:`vial.app.VialBlueprint.get_module_title`.""" return lazy_gettext('Local WHOIS service') def register_app(self, app): """ - *Callback method*. Will be called from :py:func:`hawat.base.Vial.register_blueprint` + *Callback method*. Will be called from :py:func:`hawat.base.HawatApp.register_blueprint` method and can be used to customize the Flask application object. Possible use cases: * application menu customization - :param hawat.base.Vial app: Flask application to be customized. + :param hawat.base.HawatApp app: Flask application to be customized. """ app.menu_main.add_entry( 'view', @@ -233,7 +238,7 @@ class WhoisBlueprint(VialBlueprint): def get_blueprint(): """ Mandatory interface and factory function. This function must return a valid - instance of :py:class:`hawat.base.VialBlueprint` or :py:class:`flask.Blueprint`. + instance of :py:class:`vial.app.VialBlueprint` or :py:class:`flask.Blueprint`. """ hbp = WhoisBlueprint( diff --git a/lib/hawat/blueprints/whois/forms.py b/lib/hawat/blueprints/whois/forms.py index a858f68aa21728c3b28aa429dab3c9d9a2afbcb9..f1f0ad0fa00694b86cacc38a513eb7dc3d5a189e 100644 --- a/lib/hawat/blueprints/whois/forms.py +++ b/lib/hawat/blueprints/whois/forms.py @@ -22,7 +22,7 @@ import wtforms import flask_wtf from flask_babel import gettext, lazy_gettext -import hawat.const +import vial.const def check_search_data(form, field): # pylint: disable=locally-disabled,unused-argument @@ -36,7 +36,7 @@ def check_search_data(form, field): # pylint: disable=locally-disabled,unused-a except ValueError: pass - if hawat.const.CRE_EMAIL.match(field.data): + if vial.const.CRE_EMAIL.match(field.data): return raise wtforms.validators.ValidationError( diff --git a/lib/hawat/blueprints/whois/templates/whois/search.html b/lib/hawat/blueprints/whois/templates/whois/search.html index c743219b2b32a0cb38deb669cafa446630567d52..4b37a91827a3fa788a9073ae87d6dc72c18f9db6 100644 --- a/lib/hawat/blueprints/whois/templates/whois/search.html +++ b/lib/hawat/blueprints/whois/templates/whois/search.html @@ -6,7 +6,7 @@ <div class="col-lg-12"> <div class="jumbotron" style="margin-top: 1em;"> - <h2>{{ hawat_current_view.get_view_title() }}</h2> + <h2>{{ vial_current_view.get_view_title() }}</h2> <form method="GET" class="form-inline" action="{{ url_for('whois.search') }}"> <div class="form-group{% if search_form.search.errors %}{{ ' has-error' }}{% endif %}"> {{ search_form.search.label(class_='sr-only') }} diff --git a/lib/hawat/config.py b/lib/hawat/config.py index 713c271770d71fe0a53cf236ae34e85a5d161ae5..4e05a3a1703c244622312a84c6bfe9cec6b5bb18 100644 --- a/lib/hawat/config.py +++ b/lib/hawat/config.py @@ -45,24 +45,19 @@ __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea Kropáčová <andrea import os import socket -import collections -# -# Flask related modules. -# from flask_babel import lazy_gettext -# -# Custom modules. -# import pyzenkit.jsonconf import pyzenkit.utils +import vial.config + import mentat.const -import hawat.const +from mentat.datatype.sqldb import UserModel -class Config: # pylint: disable=locally-disabled,too-few-public-methods +class Config(vial.config.Config): # pylint: disable=locally-disabled,too-few-public-methods """ Base class for default configurations of Hawat application. You are free to extend and customize contents of this class to provide better default values @@ -72,60 +67,24 @@ class Config: # pylint: disable=locally-disabled,too-few-public-methods be written in UPPERCASE to be correctly recognized. """ - #--------------------------------------------------------------------------- - # Flask internal configurations. Please refer to Flask documentation for - # more information about each configuration key. - #--------------------------------------------------------------------------- - - DEBUG = False - TESTING = False - SECRET_KEY = 'default-secret-key' - - #--------------------------------------------------------------------------- - # Flask extension configurations. Please refer to the documentation of that - # particular Flask extension for more details. - #--------------------------------------------------------------------------- - - # - # Flask-WTF configurations. - # - WTF_CSRF_ENABLED = True + APPLICATION_NAME = "Mentat" + APPLICATION_ID = "mentat" # # Flask-Mail configurations. # - MAIL_SERVER = 'localhost' - MAIL_PORT = 25 - MAIL_USERNAME = None - MAIL_PASSWORD = None - MAIL_DEFAULT_SENDER = 'mentat@{}'.format(socket.getfqdn()) - MAIL_SUBJECT_PREFIX = '[Mentat]' - - # - # Flask-Babel configurations. - # - BABEL_DEFAULT_LOCALE = hawat.const.DEFAULT_LOCALE - BABEL_DEFAULT_TIMEZONE = hawat.const.DEFAULT_TIMEZONE - - # - # Flask-SQLAlchemy configurations. - # Note: do not put 'SQLALCHEMY_DATABASE_URI' and 'SQLALCHEMY_ECHO' here. - # These will be overwritten with the values in Mentat core database configurations. - # - SQLALCHEMY_TRACK_MODIFICATIONS = False + MAIL_DEFAULT_SENDER = '{}@{}'.format(APPLICATION_ID, socket.getfqdn()) + MAIL_SUBJECT_PREFIX = '[{}]'.format(APPLICATION_NAME) #--------------------------------------------------------------------------- # Custom application configurations. #--------------------------------------------------------------------------- - ROLES = hawat.const.ROLES + ROLES = vial.const.ROLES """List of all valid user roles supported by the application.""" - SUPPORTED_LOCALES = collections.OrderedDict([ - ('en', 'English'), - ('cs', 'Česky') - ]) - """List of all languages (locales) supported by the application.""" + MODEL_USER = UserModel + """User model to be used within the application.""" ENABLED_BLUEPRINTS = [ 'hawat.blueprints.auth_api', @@ -157,25 +116,7 @@ class Config: # pylint: disable=locally-disabled,too-few-public-methods DISABLED_ENDPOINTS = [] """List of endpoints disabled on application level.""" - HAWAT_LOGIN_VIEW = 'auth_env.login' - """ - Default login view. Users will be redirected to this view in case they are not - authenticated, but the authentication is required for the requested endpoint. - """ - - HAWAT_LOGIN_MSGCAT = 'info' - """Default message category for messages related to user authentication.""" - - HAWAT_ENDPOINT_HOME = 'home.index' - """Homepage endpoint.""" - - HAWAT_LOGIN_REDIRECT = 'home.index' - """Default redirection endpoint after login.""" - - HAWAT_LOGOUT_REDIRECT = 'home.index' - """Default redirection endpoint after logout.""" - - HAWAT_MENU_SKELETON = [ + MENU_MAIN_SKELETON = [ { 'entry_type': 'submenu', 'ident': 'dashboards', @@ -215,24 +156,9 @@ class Config: # pylint: disable=locally-disabled,too-few-public-methods ] """Configuration of application menu skeleton.""" - HAWAT_ADMINS = ['root@{}'.format(socket.getfqdn())] - """List of system administrator emails.""" - HAWAT_REPORT_FEEDBACK_MAILS = ['root@{}'.format(socket.getfqdn())] """List of system administrator emails, that receive feedback messages for reports.""" - HAWAT_LOG_DEFAULT_LEVEL = 'info' - """Default logging level, case insensitive. One of the values ``DEBUG``, ``INFO``, ``WARNING``, ``ERROR``, ``CRITICAL``.""" - - HAWAT_LOG_FILE_LEVEL = 'info' - """File logging level, case insensitive. One of the values ``DEBUG``, ``INFO``, ``WARNING``, ``ERROR``, ``CRITICAL``.""" - - HAWAT_LOG_EMAIL_LEVEL = 'error' - """File logging level, case insensitive. One of the values ``DEBUG``, ``INFO``, ``WARNING``, ``ERROR``, ``CRITICAL``.""" - - HAWAT_LOCAL_DEVELOPER_LOGIN = 'account@local' - """Developer account for local development login.""" - HAWAT_CHART_TIMELINE_MAXSTEPS = 200 """Maximal number of steps (bars) displayed in timeline chart.""" @@ -242,31 +168,24 @@ class Config: # pylint: disable=locally-disabled,too-few-public-methods HAWAT_SEARCH_QUERY_QUOTA = 3 """Event search query quota per each user.""" + SQLALCHEMY_SETUP_ARGS = { + 'metadata': mentat.datatype.sqldb.MODEL.metadata, + 'model_class': mentat.datatype.sqldb.MODEL, + 'query_class': mentat.services.sqlstorage.RetryingQuery + } + -class ProductionConfig(Config): # pylint: disable=locally-disabled,too-few-public-methods +class ProductionConfig(Config, vial.config.ProductionConfig): # pylint: disable=locally-disabled,too-few-public-methods """ Class containing application configurations for *production* environment. """ -class DevelopmentConfig(Config): # pylint: disable=locally-disabled,too-few-public-methods +class DevelopmentConfig(Config, vial.config.DevelopmentConfig): # pylint: disable=locally-disabled,too-few-public-methods """ Class containing application configurations for *development* environment. """ - #--------------------------------------------------------------------------- - # Flask internal configurations. Please refer to Flask documentation for - # more information about each configuration key. - #--------------------------------------------------------------------------- - - - DEBUG = True - """Overwrite default :py:const:`hawat.config.Config.DEBUG`.""" - - #--------------------------------------------------------------------------- - # Custom application configurations. - #--------------------------------------------------------------------------- - ENABLED_BLUEPRINTS = [ 'hawat.blueprints.auth_api', 'hawat.blueprints.auth_dev', @@ -295,25 +214,13 @@ class DevelopmentConfig(Config): # pylint: disable=locally-disabled,too-few-pub ] """Overwrite default :py:const:`hawat.config.Config.ENABLED_BLUEPRINTS`.""" - HAWAT_LOGIN_VIEW = 'auth_dev.login' - """Overwrite default :py:const:`hawat.config.Config.HAWAT_LOGIN_VIEW`.""" -class TestingConfig(Config): # pylint: disable=locally-disabled,too-few-public-methods +class TestingConfig(Config, vial.config.TestingConfig): # pylint: disable=locally-disabled,too-few-public-methods """ Class containing *testing* Hawat applications` configurations. """ - #--------------------------------------------------------------------------- - # Flask internal configurations. Please refer to Flask documentation for - # more information about each configuration key. - #--------------------------------------------------------------------------- - - - TESTING = True - """Overwrite default :py:const:`hawat.config.Config.TESTING`.""" - - CONFIG_MAP = { 'development': DevelopmentConfig, 'production': ProductionConfig, @@ -345,7 +252,7 @@ def get_app_root_relative_config(): 'MENTAT_CONTROLLER_CFG': pyzenkit.utils.get_resource_path( os.path.join(mentat.const.PATH_CFG, 'mentat-controller.py.conf') ), - 'HAWAT_LOG_FILE': pyzenkit.utils.get_resource_path( + 'LOG_FILE': pyzenkit.utils.get_resource_path( os.path.join(mentat.const.PATH_LOG, 'mentat-hawat.py.log') ) } diff --git a/lib/hawat/const.py b/lib/hawat/const.py index 5318a115fa80fc7715991c6ed003c67ab82817b5..2a72ae805a38ab5573bb59186e1602b9b55e9e92 100644 --- a/lib/hawat/const.py +++ b/lib/hawat/const.py @@ -17,89 +17,6 @@ __author__ = "Jan Mach <jan.mach@cesnet.cz>" __credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea Kropáčová <andrea.kropacova@cesnet.cz>" -import re -import datetime - - -CRE_LOGIN = re.compile('^[-_@.a-zA-Z0-9]+$') -"""Compiled regular expression for login validation.""" - -CRE_EMAIL = re.compile(r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$") -"""Compiled regular expression for email address format validation.""" - -CRE_COUNTRY_CODE = re.compile('^[a-zA-Z]{2,3}$') -"""Compiled regular expression for validating language/country codes.""" - -CRE_LANG_CODE = re.compile('^[a-zA-Z]{2}(_[a-zA-Z]{2})?$') -"""Compiled regular expression for validating language codes.""" - - -DEFAULT_LOCALE = 'en' -"""Default application locale.""" - -DEFAULT_TIMEZONE = 'UTC' -"""Default application timezone.""" - - -FLASH_INFO = 'info' -"""Class for *info* flash messages.""" - -FLASH_SUCCESS = 'success' -"""Class for *success* flash messages.""" - -FLASH_WARNING = 'warning' -"""Class for *warning* flash messages.""" - -FLASH_FAILURE = 'danger' -"""Class for *failure* flash messages.""" - - -FORM_ACTION_SUBMIT = 'submit' -"""Name of the item form *submit* button.""" - -FORM_ACTION_CANCEL = 'cancel' -"""Name of the item form *cancel* button.""" - - -ACTION_ITEM_CREATE = 'create' -"""Name of the item *create* action.""" - -ACTION_ITEM_CREATEFOR = 'createfor' -"""Name of the item *createfor* action.""" - -ACTION_ITEM_UPDATE = 'update' -"""Name of the item *update* action.""" - -ACTION_ITEM_ENABLE = 'enable' -"""Name of the item *enable* action.""" - -ACTION_ITEM_DISABLE = 'disable' -"""Name of the item *disable* action.""" - -ACTION_ITEM_DELETE = 'delete' -"""Name of the item *delete* action.""" - - -ROLE_USER = 'user' -"""Name of the 'user' role.""" - -ROLE_DEVELOPER = 'developer' -"""Name of the 'developer' role.""" - -ROLE_MAINTAINER = 'maintainer' -"""Name of the 'maintainer' role.""" - -ROLE_ADMIN = 'admin' -"""Name of the 'admin' role.""" - -ROLES = [ - ROLE_USER, - ROLE_DEVELOPER, - ROLE_MAINTAINER, - ROLE_ADMIN -] -"""List of valid user roles.""" - CFGKEY_MENTAT_CORE = 'MENTAT_CORE' """Configuration key name: Core Mentat configurations.""" @@ -107,55 +24,6 @@ CFGKEY_MENTAT_CORE = 'MENTAT_CORE' CFGKEY_MENTAT_CACHE_DIR = 'MENTAT_CACHE_DIR' """Configuration key name: Path to Mentat cache dir.""" -CFGKEY_HAWAT_MENU_SKELETON = 'HAWAT_MENU_SKELETON' -"""Configuration key name: Default application main menu skeleton.""" - -CFGKEY_HAWAT_BLUEPRINTS = 'HAWAT_BLUEPRINTS' -"""Configuration key name: Registry of all successfully loaded blueprints.""" - -CFGKEY_ENABLED_BLUEPRINTS = 'ENABLED_BLUEPRINTS' -"""Configuration key name: List of all requested blueprints.""" - - -DEFAULT_PAGER_LIMIT = 100 -"""Default page limit for pager/paginator.""" - -PAGER_LIMIT_CHOICES = [ - (5,5), - (10,10), - (20,20), - (25,25), - (50,50), - (100,100), - (200,200), - (250,250), - (500,500), - (1000,1000), - (2500,2500), - (5000,5000), - (10000,10000), - (25000,25000), - (50000,50000), - (100000,100000) -] -"""List of available valid pager limit choices.""" - -DEFAULT_RESULT_TIMEDELTA = 7 -"""Default result time delta for searching various objects.""" - - -RESOURCE_BABEL = 'babel' -"""Name for the ``flask_babel.Babel`` object within the application resources.""" - -RESOURCE_LOGIN_MANAGER = 'login_manager' -"""Name for the ``flask_login.LoginManager`` object within the application resources.""" - -RESOURCE_MIGRATE = 'migrate' -"""Name for the ``flask_migrate.Migrate`` object within the application resources.""" - -RESOURCE_PRINCIPAL = 'principal' -"""Name for the ``flask_principal.Principal`` object within the application resources.""" - # # List of all existing Context Search Action Group names (CSAG). @@ -177,9 +45,6 @@ CSAG_SEVERITY = 'severities' AODS_IP4 = 'ip4' AODS_IP6 = 'ip6' -ICON_NAME_MISSING_ICON = 'missing-icon' -"""Name of the icon to display instead of missing icons.""" - FA_ICONS = { @@ -356,93 +221,3 @@ FA_ICONS = { Predefined list of selected `font-awesome <http://fontawesome.io/icons/>`__ icons that are used in this application. """ - -TIME_WINDOWS = { - '1h': { - 'current': lambda x: (x - datetime.timedelta(hours = 1)).replace(minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'previous': lambda x: (x - datetime.timedelta(hours = 1)).replace(minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'next': lambda x: (x + datetime.timedelta(hours = 1)).replace(minute = 0, second = 0, microsecond = 0, tzinfo = None) - }, - '2h': { - 'current': lambda x: (x - datetime.timedelta(hours = 2)).replace(minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'previous': lambda x: (x - datetime.timedelta(hours = 2)).replace(minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'next': lambda x: (x + datetime.timedelta(hours = 2)).replace(minute = 0, second = 0, microsecond = 0, tzinfo = None) - }, - '3h': { - 'current': lambda x: (x - datetime.timedelta(hours = 3)).replace(minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'previous': lambda x: (x - datetime.timedelta(hours = 3)).replace(minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'next': lambda x: (x + datetime.timedelta(hours = 3)).replace(minute = 0, second = 0, microsecond = 0, tzinfo = None) - }, - '4h': { - 'current': lambda x: (x - datetime.timedelta(hours = 4)).replace(minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'previous': lambda x: (x - datetime.timedelta(hours = 4)).replace(minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'next': lambda x: (x + datetime.timedelta(hours = 4)).replace(minute = 0, second = 0, microsecond = 0, tzinfo = None) - }, - '6h': { - 'current': lambda x: (x - datetime.timedelta(hours = 6)).replace(minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'previous': lambda x: (x - datetime.timedelta(hours = 6)).replace(minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'next': lambda x: (x + datetime.timedelta(hours = 6)).replace(minute = 0, second = 0, microsecond = 0, tzinfo = None) - }, - '12h': { - 'current': lambda x: (x - datetime.timedelta(hours = 12)).replace(minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'previous': lambda x: (x - datetime.timedelta(hours = 12)).replace(minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'next': lambda x: (x + datetime.timedelta(hours = 12)).replace(minute = 0, second = 0, microsecond = 0, tzinfo = None) - }, - '1d': { - 'current': lambda x: (x - datetime.timedelta(days = 1)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'previous': lambda x: (x - datetime.timedelta(days = 1)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'next': lambda x: (x + datetime.timedelta(days = 1)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None) - }, - '2d': { - 'current': lambda x: (x - datetime.timedelta(days = 2)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'previous': lambda x: (x - datetime.timedelta(days = 2)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'next': lambda x: (x + datetime.timedelta(days = 2)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None) - }, - '3d': { - 'current': lambda x: (x - datetime.timedelta(days = 3)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'previous': lambda x: (x - datetime.timedelta(days = 3)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'next': lambda x: (x + datetime.timedelta(days = 3)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None) - }, - '1w': { - 'current': lambda x: (x - datetime.timedelta(weeks = 1)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'previous': lambda x: (x - datetime.timedelta(weeks = 1)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'next': lambda x: (x + datetime.timedelta(weeks = 1)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None) - }, - '2w': { - 'current': lambda x: (x - datetime.timedelta(weeks = 2)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'previous': lambda x: (x - datetime.timedelta(weeks = 2)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'next': lambda x: (x + datetime.timedelta(weeks = 2)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None) - }, - '4w': { - 'current': lambda x: (x - datetime.timedelta(weeks = 4)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'previous': lambda x: (x - datetime.timedelta(weeks = 4)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'next': lambda x: (x + datetime.timedelta(weeks = 4)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None) - }, - '12w': { - 'current': lambda x: (x - datetime.timedelta(weeks = 12)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'previous': lambda x: (x - datetime.timedelta(weeks = 12)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'next': lambda x: (x + datetime.timedelta(weeks = 12)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None) - }, - - 'td': { - 'current': lambda x: x.replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'previous': lambda x: (x - datetime.timedelta(days = 1)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'next': lambda x: (x + datetime.timedelta(days = 1)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None) - }, - 'tw': { - 'current': lambda x: (x - datetime.timedelta(days = x.weekday())).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'previous': lambda x: (x - datetime.timedelta(days = 7)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'next': lambda x: (x + datetime.timedelta(days = 7)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None) - }, - 'tm': { - 'current': lambda x: x.replace(day = 1, hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'previous': lambda x: (x - datetime.timedelta(days = 1)).replace(day = 1, hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'next': lambda x: (x + datetime.timedelta(days = 32)).replace(day = 1, hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None) - }, - 'ty': { - 'current': lambda x: x.replace(month = 1, day = 1, hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'previous': lambda x: x.replace(year = x.year - 1, month = 1, day = 1, hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), - 'next': lambda x: x.replace(year = x.year + 1, month = 1, day = 1, hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None) - } -} -"""Default list of time windows for 'by time' quicksearch lists.""" diff --git a/lib/hawat/intl.py b/lib/hawat/intl.py deleted file mode 100644 index 86738dc57b74c78021fa0263ce7248d2ce067bf6..0000000000000000000000000000000000000000 --- a/lib/hawat/intl.py +++ /dev/null @@ -1,365 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- -# This file is part of Mentat system (https://mentat.cesnet.cz/). -# -# Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) -# Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- - - -""" -This module contains very thin database abstraction layer and access functions for -Hawat. It is a wrapper around `SQLAlchemy <http://www.sqlalchemy.org/>`__ library. -""" - - -__author__ = "Jan Mach <jan.mach@cesnet.cz>" -__credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea Kropáčová <andrea.kropacova@cesnet.cz>" - - -import os - -# -# Flask related modules. -# -import click -import flask -from flask.cli import AppGroup -import flask_babel -import flask_login -from babel import Locale - - -import pyzenkit.utils -import hawat.const - - -BABEL = flask_babel.Babel() -@BABEL.localeselector -def get_locale(): # pylint: disable=locally-disabled,unused-variable - """ - Implementation of locale selector for :py:mod:`flask_babel`. - """ - # If a user is logged in, try to use the locale from the user settings. - if flask_login.current_user.is_authenticated: - if hasattr(flask_login.current_user, 'locale') and flask_login.current_user.locale: - flask.session['locale'] = flask_login.current_user.locale - - # Store the best locale selection into the session. - if 'locale' not in flask.session or not flask.session['locale']: - flask.session['locale'] = flask.request.accept_languages.best_match( - flask.current_app.config['SUPPORTED_LOCALES'].keys() - ) - - if 'locale' in flask.session and flask.session['locale']: - return flask.session['locale'] - return flask.current_app.config['BABEL_DEFAULT_LOCALE'] - -@BABEL.timezoneselector -def get_timezone(): # pylint: disable=locally-disabled,unused-variable - """ - Implementation of timezone selector for :py:mod:`flask_babel`. - """ - # If a user is logged in, try to use the timezone from the user settings. - if flask_login.current_user.is_authenticated: - if hasattr(flask_login.current_user, 'timezone') and flask_login.current_user.timezone: - flask.session['timezone'] = flask_login.current_user.timezone - - # Store the default timezone selection into the session. - if 'timezone' not in flask.session or not flask.session['timezone']: - flask.session['timezone'] = flask.current_app.config['BABEL_DEFAULT_TIMEZONE'] - - return flask.session['timezone'] - - -def babel_format_bytes(size, unit = 'B', step_size = 1024): - """ - Format given numeric value to human readable string describing size in - B/KB/MB/GB/TB. - - :param int size: Number to be formatted. - :param enum unit: Starting unit, possible values are ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB']. - :param int step_size: Size of the step between units. - :return: Formatted and localized string. - :rtype: string - """ - units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB'] - idx_max = len(units) - 1 - unit = unit.upper() - for idx, val in enumerate(units): - # Skip the last step, there is no next unit defined after exabyte. - if idx == idx_max: - break - if size > step_size: - if unit == val: - size = size / step_size - unit = units[idx+1] - else: - break - return '{} {}'.format( - flask_babel.format_decimal(size), - unit - ) - -def babel_translate_locale(locale_id, with_current = False): - """ - Translate given locale language. By default return language in locale`s - language. Optionaly return language in given locale`s language. - """ - locale_obj = Locale.parse(locale_id) - if not with_current: - return locale_obj.language_name - return locale_obj.get_language_name(flask_babel.get_locale()) - -def babel_language_in_locale(locale_id = 'en'): - """ - Translate given locale language. By default return language in locale`s - language. Optionaly return language in given locale`s language. - """ - locale_obj = Locale.parse(flask_babel.get_locale()) - return locale_obj.get_language_name(locale_id) - - -#------------------------------------------------------------------------------- - - -TRANSLATIONS_ROOT = os.path.relpath(os.path.dirname(__file__)) -TRANSLATIONS_CFG = os.path.join(TRANSLATIONS_ROOT, 'babel.cfg') -TRANSLATIONS_POT = os.path.join(TRANSLATIONS_ROOT, 'messages.pot') -TRANSLATIONS_DIR = os.path.join(TRANSLATIONS_ROOT, 'translations') - -CMD_BABEL_EXTRACT = 'pybabel extract -F {} -o {} -k lazy_gettext -k tr_ {}'.format( - TRANSLATIONS_CFG, - TRANSLATIONS_POT, - TRANSLATIONS_ROOT -) -CMD_BABEL_UPDATE = 'pybabel update -i {} -d {}'.format( - TRANSLATIONS_POT, - TRANSLATIONS_DIR, -) -CMD_BABEL_COMPILE = 'pybabel compile -d {}'.format( - TRANSLATIONS_DIR -) -CMD_BABEL_INIT = 'pybabel init -i {} -d {} -l {{}}'.format( - TRANSLATIONS_POT, - TRANSLATIONS_DIR, -) - - -WINTL_CLI = AppGroup('webintl', help = "Web interface translation module.") - - -@WINTL_CLI.command('update') -def intl_update(): - """Update all existing translation message catalogs.""" - _extract() - _update() - _clean() - - -@WINTL_CLI.command('compile') -def intl_compile(): - """Compile all existing message translation catalogs.""" - _compile() - - -def validate_lang(ctx, param, value): - """Validate ``login/email`` command line parameter.""" - if value: - if hawat.const.CRE_LANG_CODE.match(value): - return value - raise click.BadParameter( - "Value '{}' does not look like valid language code.".format(value) - ) - return value - -@WINTL_CLI.command() -@click.argument('lang', callback = validate_lang) -def init(lang): - """Initialize a new language translation.""" - _extract() - _init(lang) - _clean() - - -def _extract(): - click.secho("\n***Extracting web interface translations ***\n", fg = 'yellow') - click.echo("$ {}\n".format(CMD_BABEL_EXTRACT)) - if os.system(CMD_BABEL_EXTRACT): - raise RuntimeError('pybabel extract command failed') - click.secho("[OK] Successfully extracted translations", fg = 'green') - -def _update(): - click.secho("\n*** Updating all web interface message translation catalogs ***\n", fg = 'yellow') - click.echo("$ {}\n".format(CMD_BABEL_UPDATE)) - if os.system(CMD_BABEL_UPDATE): - raise RuntimeError('pybabel update command failed') - click.secho("[OK] Successfully updated all message translation catalogs", fg = 'green') - -def _compile(): - click.secho("\n*** Compiling all web interface message translation catalogs ***\n", fg = 'yellow') - click.echo("$ {}\n".format(CMD_BABEL_COMPILE)) - if os.system(CMD_BABEL_COMPILE): - raise RuntimeError('pybabel compile command failed') - click.secho("[OK] Successfully compiled all message translation catalogs", fg = 'green') - -def _init(lang): - click.secho("\n*** Initializing new web interface translation ***\n", fg = 'yellow') - click.echo("Locale name: {}\n".format(lang)) - click.echo("$ {}\n".format(CMD_BABEL_INIT.format(lang))) - if os.system(CMD_BABEL_INIT.format(lang)): - raise RuntimeError('pybabel init command failed') - click.secho( - "[OK] Successfully initialized translations for '{}' language.".format(lang), - fg = 'green' - ) - -def _clean(): - os.remove(TRANSLATIONS_POT) - - -#------------------------------------------------------------------------------- - - -RINTL_CLI = AppGroup('repintl', help = "Reporting translation module.") -RINTL_COMPS = ('informant', 'reporter') - -@RINTL_CLI.command('update') -def rintl_update(): - """Update all existing reporting translation message catalogs.""" - _rep_extract() - _rep_update() - _rep_clean() - - -@RINTL_CLI.command('compile') -def rintl_compile(): - """Compile all existing reporting message translation catalogs.""" - _rep_compile() - -@RINTL_CLI.command() -@click.argument('lang', callback = validate_lang) -def rinit(lang): - """Initialize a new reporting language translation.""" - _rep_extract() - _rep_init(lang) - _rep_clean() - - -def _rep_extract(): - click.secho("\n***Extracting reporting translations ***\n", fg = 'yellow') - for component in RINTL_COMPS: - cmd = _get_cmd_rep_extract(component) - click.echo("$ {}\n".format(cmd)) - if os.system(cmd): - raise RuntimeError('pybabel extract command failed') - click.secho( - "[OK] Successfully extracted translations", - fg = 'green' - ) - -def _rep_update(): - click.secho("\n*** Updating all reporting message translation catalogs ***\n", fg = 'yellow') - for component in RINTL_COMPS: - cmd = _get_cmd_rep_update(component) - click.echo("$ {}\n".format(cmd)) - if os.system(cmd): - raise RuntimeError('pybabel update command failed') - click.secho( - "[OK] Successfully updated all message translation catalogs", - fg = 'green' - ) - -def _rep_compile(): - click.secho("\n*** Compiling all reporting message translation catalogs ***\n", fg = 'yellow') - for component in RINTL_COMPS: - cmd = _get_cmd_rep_compile(component) - click.echo("$ {}\n".format(cmd)) - if os.system(cmd): - raise RuntimeError('pybabel compile command failed') - click.secho( - "[OK] Successfully compiled all message translation catalogs", - fg = 'green' - ) - -def _rep_init(lang): - click.secho("\n*** Initializing new reporting translation ***\n", fg = 'yellow') - click.echo("Locale name: {}\n".format(lang)) - for component in RINTL_COMPS: - cmd = _get_cmd_rep_init(component, lang) - click.echo("$ {}\n".format(cmd)) - if os.system(cmd): - raise RuntimeError('pybabel init command failed') - click.secho( - "[OK] Successfully initialized translations for '{}' language.".format(lang), - fg = 'green' - ) - -def _rep_clean(): - for component in RINTL_COMPS: - os.remove( - os.path.join( - '/etc/mentat/templates', - component, - 'messages.pot' - ) - ) - -# --- - -def _get_cmd_rep_extract(component): - return 'pybabel extract -F {} -o {} -k lazy_gettext -k tr_ --no-location {} /var/mentat/venv/lib/python3.5/site-packages'.format( - os.path.join( - '/etc/mentat/templates', - component, - 'babel-venv.cfg' - ), - os.path.join( - '/etc/mentat/templates', - component, - 'messages.pot' - ), - os.path.join( - '/etc/mentat/templates', - component - ) - ) - -def _get_cmd_rep_update(component): - return 'pybabel update -i {} -d {}'.format( - os.path.join( - '/etc/mentat/templates', - component, - 'messages.pot' - ), - os.path.join( - '/etc/mentat/templates', - component, - 'translations' - ) - ) - -def _get_cmd_rep_compile(component): - return 'pybabel compile -d {}'.format( - os.path.join( - '/etc/mentat/templates', - component, - 'translations' - ) - ) - -def _get_cmd_rep_init(component, lang): - return 'pybabel init -i {} -d {} -l {}'.format( - os.path.join( - '/etc/mentat/templates', - component, - 'messages.pot' - ), - os.path.join( - '/etc/mentat/templates', - component, - 'translations' - ), - lang - ) diff --git a/lib/hawat/templates/hawat-main.js b/lib/hawat/templates/app-main.js similarity index 97% rename from lib/hawat/templates/hawat-main.js rename to lib/hawat/templates/app-main.js index d63346b2322130cc5b63958b466c932020236bd4..fc16a8516793c3649ccd432c35c63ab194b11b50 100644 --- a/lib/hawat/templates/hawat-main.js +++ b/lib/hawat/templates/app-main.js @@ -224,22 +224,22 @@ var Hawat = (function () { Hawat application configurations. */ var _configs = { - 'APPLICATION_ROOT': '{{ hawat_current_app.config['APPLICATION_ROOT'] }}' + 'APPLICATION_ROOT': '{{ vial_current_app.config['APPLICATION_ROOT'] }}' }; /** Hawat application icon set. */ - var _icons = {{ hawat_current_app.icons | tojson | safe }}; + var _icons = {{ vial_current_app.icons | tojson | safe }}; /** Data structure containing registrations of context search action groups for particular object types. */ var _csag = { -{%- for csag_name in hawat_current_app.csag.keys() | sort %} +{%- for csag_name in vial_current_app.csag.keys() | sort %} '{{ csag_name }}': [ - {%- for csag in hawat_current_app.csag[csag_name] %} + {%- for csag in vial_current_app.csag[csag_name] %} {%- if 'view' in csag %} { 'title': '{{ _(csag.title, name = '{name}') }}', @@ -262,9 +262,9 @@ var Hawat = (function () { for particular object types. */ var _oads = { -{%- for oads_name in hawat_current_app.oads.keys() | sort %} +{%- for oads_name in vial_current_app.oads.keys() | sort %} '{{ oads_name }}': [ - {%- for oads in hawat_current_app.oads[oads_name] %} + {%- for oads in vial_current_app.oads[oads_name] %} {%- if 'view' in oads %} { 'endpoint': '{{ oads.view.get_view_endpoint() }}', diff --git a/lib/hawat/utils.py b/lib/hawat/utils.py deleted file mode 100644 index 09c22269cd4cc5f101427121a97dcc20d4ead65e..0000000000000000000000000000000000000000 --- a/lib/hawat/utils.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -#------------------------------------------------------------------------------- -# This file is part of Mentat system (https://mentat.cesnet.cz/). -# -# Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) -# Use of this source is governed by the MIT license, see LICENSE file. -#------------------------------------------------------------------------------- - - -""" -This module contains various utilities. -""" - - -__author__ = "Jan Mach <jan.mach@cesnet.cz>" -__credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea Kropáčová <andrea.kropacova@cesnet.cz>" - - -class LimitCounter: - def __init__(self, limit): - self.counters = {} - self.limit = limit - - def count_and_check(self, key): - self.counters[key] = self.counters.get(key, 0) + 1 - return self.counters[key] <= self.limit diff --git a/lib/vial/__init__.py b/lib/vial/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..077a2c7a6cbb3c29442c06fb680c89dc8583b07b --- /dev/null +++ b/lib/vial/__init__.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +#------------------------------------------------------------------------------- +# Use of this source is governed by the MIT license, see LICENSE file. +#------------------------------------------------------------------------------- + + +""" +*Vial* is a lightweight skeleton application with batteries included built on top +of excelent `Flask <http://flask.pocoo.org/>`__ microframework. +""" + + +__author__ = "Honza Mach <honza.mach.ml@gmail.com>" +__version__ = "1.0.0" + + +import os + + +def create_app_full( # pylint: disable=locally-disabled,too-many-arguments + app_class, + app_name, + config_dict = None, + config_object = 'vial.config.ProductionConfig', + config_file = None, + config_env = 'VIAL_CONFIG_FILE', + config_func = None): + """ + Factory function for building Vial application. This function takes number of + optional arguments, that can be used to create a very customized instance of + Vial application. This can be very usefull when extending applications` + capabilities or for purposes of testing. Each of these arguments has default + value for the most common application setup, so for disabling it entirely it + is necessary to provide ``None`` as a value. + + :param class app_class: Flask application class to instantinate. + :param string app_name: Name of the application, identifier in lowercase. + :param dict config_dict: Initial default configurations. + :param str config_object: Name of a class or module containing configurations. + :param str config_file: Name of a file containing configurations. + :param str config_env: Name of an environment variable pointing to a file containing configurations. + :param callable config_func: Callable that will receive app.config as parameter. + :return: Vial application + :rtype: vial.app.VialApp + """ + + app = app_class(app_name) + + if config_dict and isinstance(config_dict, dict): + app.config.update(config_dict) + if config_object: + app.config.from_object(config_object) + if config_file: + app.config.from_pyfile(config_file) + if config_env and os.getenv(config_env, None): + app.config.from_envvar(config_env) + if config_func and callable(config_func): + config_func(app.config) + + app.setup_app() + + return app diff --git a/lib/vial/acl.py b/lib/vial/acl.py new file mode 100644 index 0000000000000000000000000000000000000000..a5c46db2ed29bb2908461a120c26223734f04d7e --- /dev/null +++ b/lib/vial/acl.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +#------------------------------------------------------------------------------- +# Use of this source is governed by the MIT license, see LICENSE file. +#------------------------------------------------------------------------------- + + +""" +This module contains default ACL configurations for *Vial* application views. +""" + + +from functools import partial + +import flask_principal + +import vial.const + + +MembershipNeed = partial(flask_principal.Need, 'membership') # pylint: disable=locally-disabled,invalid-name +MembershipNeed.__doc__ = """A need with the method preset to `"membership"`.""" + +ManagementNeed = partial(flask_principal.Need, 'management') # pylint: disable=locally-disabled,invalid-name +ManagementNeed.__doc__ = """A need with the method preset to `"management"`.""" + + +ROLE_NAME_ADMIN = vial.const.ROLE_ADMIN +ROLE_NAME_MAINTAINER = vial.const.ROLE_MAINTAINER +ROLE_NAME_POWER = 'power' +ROLE_NAME_DEVELOPER = vial.const.ROLE_DEVELOPER +ROLE_NAME_USER = vial.const.ROLE_USER +ROLE_NAME_ANY = 'any' + + +PERMISSION_ADMIN = flask_principal.Permission( + flask_principal.RoleNeed(ROLE_NAME_ADMIN) +) +""" +The :py:class:`flask_principal.Permission` permission for users with *admin* role +(ultimate power-user with unrestricted access to the whole system). +""" + +PERMISSION_MAINTAINER = flask_principal.Permission( + flask_principal.RoleNeed(ROLE_NAME_MAINTAINER) +) +""" +The :py:class:`flask_principal.Permission` permission for users with *maintainer* role +(power-users with slightly more restricted access to the system than *admin*). +""" + +PERMISSION_POWER = flask_principal.Permission( + flask_principal.RoleNeed(ROLE_NAME_ADMIN), + flask_principal.RoleNeed(ROLE_NAME_MAINTAINER) +) +""" +The concatenated :py:class:`flask_principal.Permission` permission for any power-user role +(*admin* or *maintainer*). +""" + +PERMISSION_DEVELOPER = flask_principal.Permission( + flask_principal.RoleNeed(ROLE_NAME_DEVELOPER) +) +""" +The :py:class:`flask_principal.Permission` permission for users with *developer* role +(system developers with access to additional development and debugging data output). +""" + +PERMISSION_USER = flask_principal.Permission( + flask_principal.RoleNeed(ROLE_NAME_USER) +) +""" +The :py:class:`flask_principal.Permission` permission for regular users with *user* role. +""" + +PERMISSION_ANY = flask_principal.Permission( + flask_principal.RoleNeed(ROLE_NAME_ADMIN), + flask_principal.RoleNeed(ROLE_NAME_MAINTAINER), + flask_principal.RoleNeed(ROLE_NAME_DEVELOPER), + flask_principal.RoleNeed(ROLE_NAME_USER) +) +""" +The concatenated :py:class:`flask_principal.Permission` permission for any user role +(*admin*, *maintainer*, *developer* or *user*). +""" + +PERMISSIONS = { + ROLE_NAME_ADMIN: PERMISSION_ADMIN, + ROLE_NAME_MAINTAINER: PERMISSION_MAINTAINER, + ROLE_NAME_POWER: PERMISSION_POWER, + ROLE_NAME_DEVELOPER: PERMISSION_DEVELOPER, + ROLE_NAME_USER: PERMISSION_USER, + ROLE_NAME_ANY: PERMISSION_ANY +} +""" +Map for accessing permission objects by name. +""" diff --git a/lib/vial/app.py b/lib/vial/app.py new file mode 100644 index 0000000000000000000000000000000000000000..a6be78c96283ebe098783701e9a17c61bdc53e09 --- /dev/null +++ b/lib/vial/app.py @@ -0,0 +1,968 @@ +#!/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 os +import datetime +import weakref +import jinja2 + +import werkzeug.routing +import werkzeug.utils +import flask +import flask.app +import flask.views +import flask_babel +import flask_migrate +import flask_login +import flask_principal + +import vial.const +import vial.acl +import vial.log +import vial.mailer +import vial.intl +import vial.errors +import vial.utils +import vial.jsglue +import vial.view +import vial.menu + + +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 vial.const.FA_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 + super().add_url_rule(rule, endpoint, view_func, provide_automatic_options, **options) + + def register_blueprint(self, blueprint, **options): + """ + Reimplementation of :py:func:`flask.Flask.register_blueprint` method. This + method will perform standart blueprint registration and on top of that will + perform following additional tasks: + + * Register blueprint into custom internal registry. The registry lies + within application`s ``config`` under key :py:const:`vial.const.CFGKEY_ENABLED_BLUEPRINTS`. + * Call blueprint`s ``register_app`` method, if available, with ``self`` as only argument. + + :param vial.app.VialBlueprint blueprint: Blueprint to be registered. + :param dict options: Additional options, will be passed down to :py:func:`flask.Flask.register_blueprint`. + """ + super().register_blueprint(blueprint, **options) + + if isinstance(blueprint, VialBlueprint): + if hasattr(blueprint, 'register_app'): + blueprint.register_app(self) + + self.sign_ins.update(blueprint.sign_ins) + self.sign_ups.update(blueprint.sign_ups) + + def register_blueprints(self): + """ + Register all configured application blueprints. The configuration comes + from :py:const:`vial.const.CFGKEY_ENABLED_BLUEPRINTS` configuration + subkey, which must contain list of string names of required blueprints. + The blueprint module must provide ``get_blueprint`` factory method, that + must return valid instance of :py:class:`vial.app.VialBlueprint`. This + method will call the :py:func:`vial.app.Vial.register_blueprint` for + each blueprint, that is being registered into the application. + + :raises vial.app.VialException: In case the factory method ``get_blueprint`` is not provided by loaded module. + """ + for name in self.config[vial.const.CFGKEY_ENABLED_BLUEPRINTS]: + 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.path_full, 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 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_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_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() + + 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_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 flask.current_app.view_functions + + 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. The icon will be looked up in + the :py:const:`vial.const.FA_ICONS` lookup table. + + :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( + vial.const.FA_ICONS.get( + icon_name, + vial.const.FA_ICONS.get(default_icon) + ) + ) + + def get_module_icon(endpoint): + """ + Get HTML icon markup for parent module of given view endpoint. + + :param str endpoint: Name of the view endpoint. + :return: Icon including HTML markup. + :rtype: flask.Markup + """ + return flask.Markup( + vial.const.FA_ICONS[self.get_endpoint_class(endpoint).module_ref().get_module_icon()] + ) + + def get_endpoint_icon(endpoint): + """ + 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( + vial.const.FA_ICONS[self.get_endpoint_class(endpoint).get_view_icon()] + ) + + def get_country_flag(country): + """ + Get URL to static country flag file. + + :param str country: Name of the icon. + :return: Country including HTML markup. + :rtype: flask.Markup + """ + if not vial.const.CRE_COUNTRY_CODE.match(country): + return get_icon('flag') + + return flask.Markup( + '<img src="{}">'.format( + flask.url_for( + 'design.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 + ) + + 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 = os.path.realpath( + os.path.join( + os.path.dirname( + os.path.abspath(__file__) + ), + 'migrations' + ) + ) + ) + self.set_resource(vial.const.RESOURCE_MIGRATE, migrate) + + self.logger.info("Connected to database via SQLAlchemy") + + return self + + + def _setup_app_auth(self): + """ + Setup application authentication features. + + :param vial.app.VialApp app: Vial application to be modified. + :return: Modified Vial application + :rtype: vial.app.VialApp + """ + + lim = flask_login.LoginManager() + lim.init_app(self) + lim.login_view = self.config['ENDPOINT_LOGIN'] + lim.login_message = flask_babel.gettext("Please log in to access this page.") + lim.login_message_category = self.config['LOGIN_MSGCAT'] + + self.set_resource(vial.const.RESOURCE_LOGIN_MANAGER, lim) + + @lim.user_loader + def load_user(user_id): # pylint: disable=locally-disabled,unused-variable + """ + Flask-Login callback for loading current user`s data. + """ + user_model = self.config['MODEL_USER'] + return vial.db.db_get().session.query(user_model).filter(user_model.id == user_id).one_or_none() + + @self.route('/logout') + @flask_login.login_required + def logout(): # pylint: disable=locally-disabled,unused-variable + """ + Flask-Login callback for logging out current user. + """ + flask.current_app.logger.info( + "User '{}' just logged out.".format( + str(flask_login.current_user) + ) + ) + flask_login.logout_user() + flask.flash( + flask_babel.gettext('You have been successfully logged out.'), + vial.const.FLASH_SUCCESS + ) + + # Remove session keys set by Flask-Principal. + for key in ('identity.name', 'identity.auth_type'): + flask.session.pop(key, None) + + # Tell Flask-Principal the identity changed. + flask_principal.identity_changed.send( + flask.current_app._get_current_object(), # pylint: disable=locally-disabled,protected-access + identity = flask_principal.AnonymousIdentity() + ) + + # Force user to index page. + return flask.redirect( + flask.url_for( + flask.current_app.config['ENDPOINT_LOGOUT_REDIRECT'] + ) + ) + + return self + + def _setup_app_acl(self): + """ + Setup application ACL features. + + :param vial.app.VialApp app: Vial application to be modified. + :return: Modified Vial application + :rtype: vial.app.VialApp + """ + fpp = flask_principal.Principal(self, skip_static = True) + self.set_resource(vial.const.RESOURCE_PRINCIPAL, fpp) + + @flask_principal.identity_loaded.connect_via(self) + def on_identity_loaded(sender, identity): # pylint: disable=locally-disabled,unused-variable,unused-argument + """ + Flask-Principal callback for populating user identity object after login. + """ + # Set the identity user object. + identity.user = flask_login.current_user + + if not flask_login.current_user.is_authenticated: + flask.current_app.logger.debug( + "Loaded ACL identity for anonymous user '{}'.".format( + str(flask_login.current_user) + ) + ) + return + flask.current_app.logger.debug( + "Loading ACL identity for user '{}'.".format( + str(flask_login.current_user) + ) + ) + + # Add the UserNeed to the identity. + if hasattr(flask_login.current_user, 'get_id'): + identity.provides.add( + flask_principal.UserNeed(flask_login.current_user.id) + ) + + # Assuming the User model has a list of roles, update the + # identity with the roles that the user provides. + if hasattr(flask_login.current_user, 'roles'): + for role in flask_login.current_user.roles: + identity.provides.add( + flask_principal.RoleNeed(role) + ) + + # Assuming the User model has a list of group memberships, update the + # identity with the groups that the user is member of. + if hasattr(flask_login.current_user, 'memberships'): + for group in flask_login.current_user.memberships: + identity.provides.add( + vial.acl.MembershipNeed(group.id) + ) + + # Assuming the User model has a list of group managements, update the + # identity with the groups that the user is manager of. + if hasattr(flask_login.current_user, 'managements'): + for group in flask_login.current_user.managements: + identity.provides.add( + vial.acl.ManagementNeed(group.id) + ) + + @self.context_processor + def utility_acl_processor(): # pylint: disable=locally-disabled,unused-variable + """ + Register additional helpers related to authorization into Jinja global + namespace to enable them within the templates. + """ + def can_access_endpoint(endpoint, item = None): + """ + Check if currently logged-in user can access given endpoint/view. + + :param str endpoint: Name of the application endpoint. + :param item: Optional item for additional validations. + :return: ``True`` in case user can access the endpoint, ``False`` otherwise. + :rtype: bool + """ + return flask.current_app.can_access_endpoint(endpoint, item = item) + + def permission_can(permission_name): + """ + Manually check currently logged-in user for given permission. + + :param str permission_name: Name of the permission. + :return: Check result. + :rtype: bool + """ + return vial.acl.PERMISSIONS[permission_name].can() + + def is_it_me(item): + """ + Check if given user account is mine. + """ + return item.id == flask_login.current_user.id + + return dict( + can_access_endpoint = can_access_endpoint, + permission_can = permission_can, + is_it_me = is_it_me + ) + + return self + + def _setup_app_intl(self): + """ + Setup application`s internationalization sybsystem. + + :param vial.app.VialApp app: Vial application to be modified. + :return: Modified Vial application + :rtype: vial.app.VialApp + """ + vial.intl.BABEL.init_app(self) + self.set_resource(vial.const.RESOURCE_BABEL, vial.intl.BABEL) + self.cli.add_command(vial.intl.WINTL_CLI) + self.cli.add_command(vial.intl.RINTL_CLI) + + @self.route('/locale/<code>') + def locale(code): # pylint: disable=locally-disabled,unused-variable + """ + Application route providing users with the option of changing locale. + """ + if code not in flask.current_app.config['SUPPORTED_LOCALES']: + return flask.abort(404) + + if flask_login.current_user.is_authenticated: + flask_login.current_user.locale = code + # Make sure current user is in SQLAlchemy session. Turns out, this + # step is not necessary and current user is already in session, + # because it was fetched from database few moments ago. + #vial.db.db_session().add(flask_login.current_user) + vial.db.db_session().commit() + + flask.session['locale'] = code + flask_babel.refresh() + + flask.flash( + flask.Markup(flask_babel.gettext( + 'Locale was succesfully changed to <strong>%(lcln)s (%(lclc)s)</strong>.', + lclc = code, + lcln = flask.current_app.config['SUPPORTED_LOCALES'][code] + )), + vial.const.FLASH_SUCCESS + ) + + # Redirect user back to original page. + return flask.redirect( + vial.forms.get_redirect_target( + default_url = flask.url_for( + flask.current_app.config['ENDPOINT_HOME'] + ) + ) + ) + + @self.before_request + def before_request(): # pylint: disable=locally-disabled,unused-variable + """ + Use Flask`s :py:func:`flask.Flask.before_request` hook for storing + currently selected locale and timezone to request`s session storage. + """ + if 'locale' not in flask.session: + flask.session['locale'] = vial.intl.get_locale() + if 'timezone' not in flask.session: + flask.session['timezone'] = vial.intl.get_timezone() + + @self.context_processor + def utility_processor(): # pylint: disable=locally-disabled,unused-variable + """ + Register additional internationalization helpers into Jinja global namespace. + """ + + return dict( + babel_get_locale = vial.intl.get_locale, + babel_get_timezone = vial.intl.get_timezone, + babel_format_datetime = flask_babel.format_datetime, + babel_format_timedelta = flask_babel.format_timedelta, + babel_format_decimal = flask_babel.format_decimal, + babel_format_percent = flask_babel.format_percent, + babel_format_bytes = vial.intl.babel_format_bytes, + babel_translate_locale = vial.intl.babel_translate_locale, + babel_language_in_locale = vial.intl.babel_language_in_locale + ) + + return self + + def _setup_app_menu(self): + """ + Setup default application menu skeleton. + + :param vial.app.VialApp app: Vial application to be modified. + :return: Modified Vial application + :rtype: vial.app.VialApp + """ + for entry in self.config[vial.const.CFGKEY_MENU_MAIN_SKELETON]: + self.menu_main.add_entry(**entry) + + return self + + def _setup_app_blueprints(self): + """ + Setup application blueprints. + + :param vial.app.VialApp app: Vial application to be modified. + :return: Modified Vial application + :rtype: vial.app.VialApp + """ + self.register_blueprints() + + return self + + +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 authentication decorators (if requested). + if view_class.authentication: + view_func = flask_login.login_required(view_func) + + # Apply authorization decorators (if requested). + if view_class.authorization: + for auth in view_class.authorization: + view_func = auth.require(403)(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/config.py b/lib/vial/config.py new file mode 100644 index 0000000000000000000000000000000000000000..2abd5c9c07d2532c729fd3194b4f102578bd7c7b --- /dev/null +++ b/lib/vial/config.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +#------------------------------------------------------------------------------- +# Use of this source is governed by the MIT license, see LICENSE file. +#------------------------------------------------------------------------------- + + +""" +This module contains default *Vial* application configurations. +""" + + +import socket +import collections + +import vial.const + + +class Config: # pylint: disable=locally-disabled,too-few-public-methods + """ + Base class for default configurations of Vial application. You are free to + extend and customize contents of this class to provide better default values + for your particular environment. + + The configuration keys must be a valid Flask configuration and so they must + be written in UPPERCASE to be correctly recognized. + """ + + APPLICATION_NAME = "Vial" + APPLICATION_ID = "vial" + + #--------------------------------------------------------------------------- + # Flask internal configurations. Please refer to Flask documentation for + # more information about each configuration key. + #--------------------------------------------------------------------------- + + DEBUG = False + TESTING = False + SECRET_KEY = 'default-secret-key' + + #--------------------------------------------------------------------------- + # Flask extension configurations. Please refer to the documentation of that + # particular Flask extension for more details. + #--------------------------------------------------------------------------- + + # + # Flask-WTF configurations. + # + WTF_CSRF_ENABLED = True + + # + # Flask-Mail configurations. + # + MAIL_SERVER = 'localhost' + MAIL_PORT = 25 + MAIL_USERNAME = None + MAIL_PASSWORD = None + MAIL_DEFAULT_SENDER = '{}@{}'.format(APPLICATION_ID, socket.getfqdn()) + MAIL_SUBJECT_PREFIX = '[{}]'.format(APPLICATION_NAME) + + # + # Flask-Babel configurations. + # + BABEL_DEFAULT_LOCALE = vial.const.DEFAULT_LOCALE + BABEL_DEFAULT_TIMEZONE = vial.const.DEFAULT_TIMEZONE + + # + # Flask-SQLAlchemy configurations. + # + SQLALCHEMY_TRACK_MODIFICATIONS = False + + #--------------------------------------------------------------------------- + # Custom application configurations. + #--------------------------------------------------------------------------- + + ROLES = vial.const.ROLES + """List of all valid user roles supported by the application.""" + + MODEL_USER = None + """User model to be used within the application.""" + + SUPPORTED_LOCALES = collections.OrderedDict([ + ('en', 'English'), + ('cs', 'Česky') + ]) + """List of all languages (locales) supported by the application.""" + + ENABLED_BLUEPRINTS = [] + """List of requested application blueprints to be loaded during setup.""" + + DISABLED_ENDPOINTS = [] + """List of endpoints disabled on application level.""" + + ENDPOINT_LOGIN = 'auth_env.login' + """ + Default login view. Users will be redirected to this view in case they are not + authenticated, but the authentication is required for the requested endpoint. + """ + + LOGIN_MSGCAT = 'info' + """Default message category for messages related to user authentication.""" + + ENDPOINT_HOME = 'home.index' + """Homepage endpoint.""" + + ENDPOINT_LOGIN_REDIRECT = 'home.index' + """Default redirection endpoint after login.""" + + ENDPOINT_LOGOUT_REDIRECT = 'home.index' + """Default redirection endpoint after logout.""" + + MENU_MAIN_SKELETON = [] + """Configuration of application menu skeleton.""" + + EMAIL_ADMINS = ['root@{}'.format(socket.getfqdn())] + """List of system administrator emails.""" + + LOG_DEFAULT_LEVEL = 'info' + """Default logging level, case insensitive. One of the values ``DEBUG``, ``INFO``, ``WARNING``, ``ERROR``, ``CRITICAL``.""" + + LOG_FILE_LEVEL = 'info' + """File logging level, case insensitive. One of the values ``DEBUG``, ``INFO``, ``WARNING``, ``ERROR``, ``CRITICAL``.""" + + LOG_EMAIL_LEVEL = 'error' + """File logging level, case insensitive. One of the values ``DEBUG``, ``INFO``, ``WARNING``, ``ERROR``, ``CRITICAL``.""" + + +class ProductionConfig(Config): # pylint: disable=locally-disabled,too-few-public-methods + """ + Class containing application configurations for *production* environment. + """ + + +class DevelopmentConfig(Config): # pylint: disable=locally-disabled,too-few-public-methods + """ + Class containing application configurations for *development* environment. + """ + + #--------------------------------------------------------------------------- + # Flask internal configurations. Please refer to Flask documentation for + # more information about each configuration key. + #--------------------------------------------------------------------------- + + + DEBUG = True + """Overwrite default :py:const:`vial.config.Config.DEBUG`.""" + + #--------------------------------------------------------------------------- + # Custom application configurations. + #--------------------------------------------------------------------------- + + ENDPOINT_LOGIN = 'auth_dev.login' + """Overwrite default :py:const:`vial.config.Config.ENDPOINT_LOGIN`.""" + + +class TestingConfig(Config): # pylint: disable=locally-disabled,too-few-public-methods + """ + Class containing *testing* Vial applications` configurations. + """ + + #--------------------------------------------------------------------------- + # Flask internal configurations. Please refer to Flask documentation for + # more information about each configuration key. + #--------------------------------------------------------------------------- + + + TESTING = True + """Overwrite default :py:const:`vial.config.Config.TESTING`.""" diff --git a/lib/vial/const.py b/lib/vial/const.py new file mode 100644 index 0000000000000000000000000000000000000000..fb1bbd22555c5e1a79dcad15cc1d8cc6087e4830 --- /dev/null +++ b/lib/vial/const.py @@ -0,0 +1,412 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +#------------------------------------------------------------------------------- +# Use of this source is governed by the MIT license, see LICENSE file. +#------------------------------------------------------------------------------- + + +""" +This module contains *Vial* application constants. +""" + + +import re +import datetime + + +CRE_LOGIN = re.compile('^[-_@.a-zA-Z0-9]+$') +"""Compiled regular expression for login validation.""" + +CRE_EMAIL = re.compile(r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$") +"""Compiled regular expression for email address format validation.""" + +CRE_COUNTRY_CODE = re.compile('^[a-zA-Z]{2,3}$') +"""Compiled regular expression for validating language/country codes.""" + +CRE_LANG_CODE = re.compile('^[a-zA-Z]{2}(_[a-zA-Z]{2})?$') +"""Compiled regular expression for validating language codes.""" + + +DEFAULT_LOCALE = 'en' +"""Default application locale.""" + +DEFAULT_TIMEZONE = 'UTC' +"""Default application timezone.""" + + +FLASH_INFO = 'info' +"""Class for *info* flash messages.""" + +FLASH_SUCCESS = 'success' +"""Class for *success* flash messages.""" + +FLASH_WARNING = 'warning' +"""Class for *warning* flash messages.""" + +FLASH_FAILURE = 'danger' +"""Class for *failure* flash messages.""" + + +FORM_ACTION_SUBMIT = 'submit' +"""Name of the item form *submit* button.""" + +FORM_ACTION_CANCEL = 'cancel' +"""Name of the item form *cancel* button.""" + + +ACTION_ITEM_CREATE = 'create' +"""Name of the item *create* action.""" + +ACTION_ITEM_CREATEFOR = 'createfor' +"""Name of the item *createfor* action.""" + +ACTION_ITEM_UPDATE = 'update' +"""Name of the item *update* action.""" + +ACTION_ITEM_ENABLE = 'enable' +"""Name of the item *enable* action.""" + +ACTION_ITEM_DISABLE = 'disable' +"""Name of the item *disable* action.""" + +ACTION_ITEM_DELETE = 'delete' +"""Name of the item *delete* action.""" + + +ROLE_USER = 'user' +"""Name of the 'user' role.""" + +ROLE_DEVELOPER = 'developer' +"""Name of the 'developer' role.""" + +ROLE_MAINTAINER = 'maintainer' +"""Name of the 'maintainer' role.""" + +ROLE_ADMIN = 'admin' +"""Name of the 'admin' role.""" + +ROLES = [ + ROLE_USER, + ROLE_DEVELOPER, + ROLE_MAINTAINER, + ROLE_ADMIN +] +"""List of valid user roles.""" + + +CFGKEY_MENU_MAIN_SKELETON = 'MENU_MAIN_SKELETON' +"""Configuration key name: Default application main menu skeleton.""" + +CFGKEY_ENABLED_BLUEPRINTS = 'ENABLED_BLUEPRINTS' +"""Configuration key name: List of all requested blueprints.""" + + +DEFAULT_PAGER_LIMIT = 100 +"""Default page limit for pager/paginator.""" + +PAGER_LIMIT_CHOICES = [ + (5,5), + (10,10), + (20,20), + (25,25), + (50,50), + (100,100), + (200,200), + (250,250), + (500,500), + (1000,1000), + (2500,2500), + (5000,5000), + (10000,10000), + (25000,25000), + (50000,50000), + (100000,100000) +] +"""List of available valid pager limit choices.""" + +DEFAULT_RESULT_TIMEDELTA = 7 +"""Default result time delta for searching various objects.""" + + +RESOURCE_BABEL = 'babel' +"""Name for the ``flask_babel.Babel`` object within the application resources.""" + +RESOURCE_LOGIN_MANAGER = 'login_manager' +"""Name for the ``flask_login.LoginManager`` object within the application resources.""" + +RESOURCE_MIGRATE = 'migrate' +"""Name for the ``flask_migrate.Migrate`` object within the application resources.""" + +RESOURCE_PRINCIPAL = 'principal' +"""Name for the ``flask_principal.Principal`` object within the application resources.""" + + +ICON_NAME_MISSING_ICON = 'missing-icon' +"""Name of the icon to display instead of missing icons.""" + + +FA_ICONS = { + + # + # General icons. + # + 'login': '<i class="fas fa-fw fa-sign-in-alt"></i>', + 'logout': '<i class="fas fa-fw fa-sign-out-alt"></i>', + 'register': '<i class="fas fa-fw fa-user-plus"></i>', + 'help': '<i class="fas fa-fw fa-question-circle"></i>', + 'language': '<i class="fas fa-fw fa-globe"></i>', + + 'role-anonymous': '<i class="fas fa-fw fa-user-secret"></i>', + 'role-user': '<i class="fas fa-fw fa-user"></i>', + 'role-developer': '<i class="fas fa-fw fa-user-md"></i>', + 'role-maintainer': '<i class="fas fa-fw fa-user-tie"></i>', + 'role-admin': '<i class="fas fa-fw fa-user-ninja"></i>', + + # + # Main site section icons. + # + 'section-home': '<i class="fas fa-fw fa-home"></i>', + 'section-dashboards': '<i class="fas fa-fw fa-tachometer-alt"></i>', + 'section-more': '<i class="fas fa-fw fa-puzzle-piece"></i>', + 'section-administration': '<i class="fas fa-fw fa-cogs"></i>', + 'section-development': '<i class="fas fa-fw fa-bug"></i>', + + # + # Built-in module icons. + # + 'module-auth-api': '<i class="fas fa-fw fa-key"></i>', + 'module-auth-dev': '<i class="fas fa-fw fa-key"></i>', + 'module-auth-env': '<i class="fas fa-fw fa-key"></i>', + 'module-changelogs': '<i class="fas fa-fw fa-clipboard-list"></i>', + 'module-dashboards': '<i class="fas fa-fw fa-tachometer-alt"></i>', + 'module-dbstatus': '<i class="fas fa-fw fa-database"></i>', + 'module-design': '<i class="fas fa-fw fa-palette"></i>', + 'module-devtools': '<i class="fas fa-fw fa-bug"></i>', + 'module-dnsr': '<i class="fas fa-fw fa-directions"></i>', + 'module-events': '<i class="fas fa-fw fa-bell"></i>', + 'module-filters': '<i class="fas fa-fw fa-filter"></i>', + 'module-geoip': '<i class="fas fa-fw fa-map-marked-alt"></i>', + 'module-groups': '<i class="fas fa-fw fa-users"></i>', + 'module-help': '<i class="fas fa-fw fa-question-circle"></i>', + 'module-hosts': '<i class="fas fa-fw fa-server"></i>', + 'module-networks': '<i class="fas fa-fw fa-sitemap"></i>', + 'module-nerd': '<i class="fas fa-fw fa-certificate"></i>', + 'module-pdnsr': '<i class="fas fa-fw fa-compass"></i>', + 'module-performance': '<i class="fas fa-fw fa-chart-bar"></i>', + 'module-reports': '<i class="fas fa-fw fa-newspaper"></i>', + 'module-settings-reporting': '<i class="fas fa-fw fa-sliders-h"></i>', + 'module-skeleton': '<i class="fas fa-fw fa-skull"></i>', + 'module-status': '<i class="fas fa-fw fa-heartbeat"></i>', + 'module-timeline': '<i class="fas fa-fw fa-chart-line"></i>', + 'module-users': '<i class="fas fa-fw fa-user"></i>', + 'module-whois': '<i class="fas fa-fw fa-map-signs"></i>', + + 'profile': '<i class="fas fa-fw fa-id-card"></i>', + + 'modal-question': '<i class="fas fa-fw fa-question-circle"></i>', + + 'missing-icon': '<i class="fas fa-fw fa-question" title="Missing icon"></i>', + + # + # Action icons. + # + 'action-more': '<i class="fas fa-fw fa-cubes"></i>', + 'action-search': '<i class="fas fa-fw fa-search"></i>', + 'action-show': '<i class="fas fa-fw fa-eye"></i>', + 'action-show-user': '<i class="fas fa-fw fa-user-circle"></i>', + 'action-create': '<i class="fas fa-fw fa-plus-circle"></i>', + 'action-create-user': '<i class="fas fa-fw fa-user-plus"></i>', + 'action-update': '<i class="fas fa-fw fa-edit"></i>', + 'action-update-user': '<i class="fas fa-fw fa-user-edit"></i>', + 'action-enable': '<i class="fas fa-fw fa-unlock"></i>', + 'action-enable-user': '<i class="fas fa-fw fa-user-check"></i>', + 'action-disable': '<i class="fas fa-fw fa-lock"></i>', + 'action-disable-user': '<i class="fas fa-fw fa-user-lock"></i>', + 'action-delete': '<i class="fas fa-fw fa-trash"></i>', + 'action-delete-user': '<i class="fas fa-fw fa-user-slash"></i>', + 'action-add-member': '<i class="fas fa-fw fa-user-plus"></i>', + 'action-rej-member': '<i class="fas fa-fw fa-user-minus"></i>', + 'action-rem-member': '<i class="fas fa-fw fa-user-times"></i>', + 'action-save': '<i class="fas fa-fw fa-save"></i>', + 'action-download': '<i class="fas fa-fw fa-file-download"></i>', + 'action-download-zip': '<i class="fas fa-fw fa-file-archive"></i>', + 'action-download-csv': '<i class="fas fa-fw fa-file-csv"></i>', + 'action-download-svg': '<i class="fas fa-fw fa-file-image"></i>', + 'action-download-js': '<i class="fab fa-fw fa-js"></i>', + 'action-mail': '<i class="fas fa-fw fa-envelope"></i>', + 'action-reload': '<i class="fas fa-fw fa-sync-alt"></i>', + 'action-genkey': '<i class="fas fa-fw fa-key"></i>', + 'action-stop': '<i class="fas fa-fw fa-stop-circle"></i>', + + 'alert-success': '<i class="fas fa-fw fa-check-circle"></i>', + 'alert-info': '<i class="fas fa-fw fa-info-circle"></i>', + 'alert-warning': '<i class="fas fa-fw fa-exclamation-circle"></i>', + 'alert-danger': '<i class="fas fa-fw fa-exclamation-triangle"></i>', + + 'item-enabled': '<i class="fas fa-fw fa-toggle-on"></i>', + 'item-disabled': '<i class="fas fa-fw fa-toggle-off"></i>', + + 'r-t-summary': '<i class="fas fa-fw fa-archive"></i>', + 'r-t-extra': '<i class="fas fa-fw fa-file-alt"></i>', + 'r-s-unknown': '<i class="fas fa-fw fa-thermometer-empty"></i>', + 'r-s-low': '<i class="fas fa-fw fa-thermometer-quarter"></i>', + 'r-s-medium': '<i class="fas fa-fw fa-thermometer-half"></i>', + 'r-s-high': '<i class="fas fa-fw fa-thermometer-three-quarters"></i>', + 'r-s-critical': '<i class="fas fa-fw fa-thermometer-full"></i>', + + 'report-data-relapsed': '<i class="fas fa-fw fa-sync-alt"></i>', + 'report-data-filtered': '<i class="fas fa-fw fa-filter"></i>', + 'report-data-test': '<i class="fas fa-fw fa-bug"></i>', + 'report-data-mailed': '<i class="fas fa-fw fa-envelope"></i>', + + 'ajax-loader': '<i class="fas fa-fw fa-spinner fa-spin fa-4x"></i>', + 'caret-down': '<i class="fas fa-fw fa-caret-square-down"></i>', + 'unassigned': '<i class="fas fa-fw fa-minus"></i>', + 'undisclosed': '<i class="fas fa-fw fa-minus"></i>', + 'calendar': '<i class="fas fa-fw fa-calendar-alt"></i>', + 'stopwatch': '<i class="fas fa-fw fa-stopwatch"></i>', + 'clock': '<i class="fas fa-fw fa-clock"></i>', + 'domain': '<i class="fas fa-fw fa-tag"></i>', + 'time-from': '<i class="fas fa-fw fa-hourglass-start"></i>', + 'time-to': '<i class="fas fa-fw fa-hourglass-end"></i>', + 'debug': '<i class="fas fa-fw fa-bug"></i>', + 'eventclss': '<i class="fas fa-fw fa-book"></i>', + 'reference': '<i class="fas fa-fw fa-external-link-alt"></i>', + 'anchor': '<i class="fas fa-fw fa-anchor"></i>', + 'search': '<i class="fas fa-fw fa-search"></i>', + 'weight': '<i class="fas fa-fw fa-weight"></i>', + 'list': '<i class="fas fa-fw fa-list-ul"></i>', + 'mail': '<i class="fas fa-fw fa-envelope"></i>', + 'redirect': '<i class="fas fa-fw fa-share"></i>', + 'unredirect': '<span class="fa-layers fa-fw"><i class="fas fa-fw fa-share"></i><i class="fas fa-fw fa-ban"></i></span>', + 'mute': '<i class="fas fa-fw fa-volume-off"></i>', + 'unmute': '<i class="fas fa-fw fa-volume-up"></i>', + 'compress': '<i class="fas fa-fw fa-gift"></i>', + 'uncompress': '<span class="fa-layers fa-fw"><i class="fas fa-fw fa-gift"></i><i class="fas fa-fw fa-ban"></i></span>', + 'import': '<i class="fas fa-fw fa-cloud-upload"></i>', + 'export': '<i class="fas fa-fw fa-cloud-download"></i>', + 'validate': '<i class="fas fa-fw fa-check-circle"></i>', + 'min': '<i class="fas fa-fw fa-angle-double-down"></i>', + 'max': '<i class="fas fa-fw fa-angle-double-up"></i>', + 'sum': '<i class="fas fa-fw fa-plus"></i>', + 'cnt': '<i class="fas fa-fw fa-hashtag"></i>', + 'avg': '<i class="fas fa-fw fa-dot-circle"></i>', + 'med': '<i class="fas fa-fw fa-bullseye"></i>', + 'na': '<i class="fas fa-fw fa-times"></i>', + 'stats': '<i class="fas fa-fw fa-bar-chart"></i>', + 'structure': '<i class="fas fa-fw fa-tree"></i>', + 'actions': '<i class="fas fa-fw fa-wrench"></i>', + 'cog': '<i class="fas fa-fw fa-cog"></i>', + 'check': '<i class="fas fa-fw fa-check-square"></i>', + 'check_blank': '<i class="far fa-fw fa-square"></i>', + 'ok': '<i class="fas fa-fw fa-check"></i>', + 'ko': '<i class="fas fa-fw fa-times"></i>', + 'sortasc': '<i class="fas fa-fw fa-sort-asc"></i>', + 'sortdesc': '<i class="fas fa-fw fa-sort-desc"></i>', + 'backtotop': '<i class="fas fa-fw fa-level-up-alt"></i>', + 'first': '<i class="fas fa-fw fa-angle-double-left"></i>', + 'previous': '<i class="fas fa-fw fa-angle-left"></i>', + 'next': '<i class="fas fa-fw fa-angle-right"></i>', + 'last': '<i class="fas fa-fw fa-angle-double-right" aria-hidden="true"></i>', + 'liitem': '<i class="fas fa-li fa-asterisk" aria-hidden="true"></i>', + 'expand': '<i class="fas fa-fw fa-angle-left" aria-hidden="true"></i>', + 'collapse': '<i class="fas fa-fw fa-angle-down" aria-hidden="true"></i>', + 'form-error': '<i class="fas fa-fw fa-exclamation-triangle" aria-hidden="true"></i>', + 'table': '<i class="fas fa-fw fa-table"></i>', + 'quicksearch': '<i class="fab fa-fw fa-searchengin"></i>', + 'playground': '<i class="fas fa-fw fa-gamepad"></i>' +} +""" +Predefined list of selected `font-awesome <http://fontawesome.io/icons/>`__ icons +that are used in this application. +""" + +TIME_WINDOWS = { + '1h': { + 'current': lambda x: (x - datetime.timedelta(hours = 1)).replace(minute = 0, second = 0, microsecond = 0, tzinfo = None), + 'previous': lambda x: (x - datetime.timedelta(hours = 1)).replace(minute = 0, second = 0, microsecond = 0, tzinfo = None), + 'next': lambda x: (x + datetime.timedelta(hours = 1)).replace(minute = 0, second = 0, microsecond = 0, tzinfo = None) + }, + '2h': { + 'current': lambda x: (x - datetime.timedelta(hours = 2)).replace(minute = 0, second = 0, microsecond = 0, tzinfo = None), + 'previous': lambda x: (x - datetime.timedelta(hours = 2)).replace(minute = 0, second = 0, microsecond = 0, tzinfo = None), + 'next': lambda x: (x + datetime.timedelta(hours = 2)).replace(minute = 0, second = 0, microsecond = 0, tzinfo = None) + }, + '3h': { + 'current': lambda x: (x - datetime.timedelta(hours = 3)).replace(minute = 0, second = 0, microsecond = 0, tzinfo = None), + 'previous': lambda x: (x - datetime.timedelta(hours = 3)).replace(minute = 0, second = 0, microsecond = 0, tzinfo = None), + 'next': lambda x: (x + datetime.timedelta(hours = 3)).replace(minute = 0, second = 0, microsecond = 0, tzinfo = None) + }, + '4h': { + 'current': lambda x: (x - datetime.timedelta(hours = 4)).replace(minute = 0, second = 0, microsecond = 0, tzinfo = None), + 'previous': lambda x: (x - datetime.timedelta(hours = 4)).replace(minute = 0, second = 0, microsecond = 0, tzinfo = None), + 'next': lambda x: (x + datetime.timedelta(hours = 4)).replace(minute = 0, second = 0, microsecond = 0, tzinfo = None) + }, + '6h': { + 'current': lambda x: (x - datetime.timedelta(hours = 6)).replace(minute = 0, second = 0, microsecond = 0, tzinfo = None), + 'previous': lambda x: (x - datetime.timedelta(hours = 6)).replace(minute = 0, second = 0, microsecond = 0, tzinfo = None), + 'next': lambda x: (x + datetime.timedelta(hours = 6)).replace(minute = 0, second = 0, microsecond = 0, tzinfo = None) + }, + '12h': { + 'current': lambda x: (x - datetime.timedelta(hours = 12)).replace(minute = 0, second = 0, microsecond = 0, tzinfo = None), + 'previous': lambda x: (x - datetime.timedelta(hours = 12)).replace(minute = 0, second = 0, microsecond = 0, tzinfo = None), + 'next': lambda x: (x + datetime.timedelta(hours = 12)).replace(minute = 0, second = 0, microsecond = 0, tzinfo = None) + }, + '1d': { + 'current': lambda x: (x - datetime.timedelta(days = 1)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), + 'previous': lambda x: (x - datetime.timedelta(days = 1)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), + 'next': lambda x: (x + datetime.timedelta(days = 1)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None) + }, + '2d': { + 'current': lambda x: (x - datetime.timedelta(days = 2)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), + 'previous': lambda x: (x - datetime.timedelta(days = 2)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), + 'next': lambda x: (x + datetime.timedelta(days = 2)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None) + }, + '3d': { + 'current': lambda x: (x - datetime.timedelta(days = 3)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), + 'previous': lambda x: (x - datetime.timedelta(days = 3)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), + 'next': lambda x: (x + datetime.timedelta(days = 3)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None) + }, + '1w': { + 'current': lambda x: (x - datetime.timedelta(weeks = 1)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), + 'previous': lambda x: (x - datetime.timedelta(weeks = 1)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), + 'next': lambda x: (x + datetime.timedelta(weeks = 1)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None) + }, + '2w': { + 'current': lambda x: (x - datetime.timedelta(weeks = 2)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), + 'previous': lambda x: (x - datetime.timedelta(weeks = 2)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), + 'next': lambda x: (x + datetime.timedelta(weeks = 2)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None) + }, + '4w': { + 'current': lambda x: (x - datetime.timedelta(weeks = 4)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), + 'previous': lambda x: (x - datetime.timedelta(weeks = 4)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), + 'next': lambda x: (x + datetime.timedelta(weeks = 4)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None) + }, + '12w': { + 'current': lambda x: (x - datetime.timedelta(weeks = 12)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), + 'previous': lambda x: (x - datetime.timedelta(weeks = 12)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), + 'next': lambda x: (x + datetime.timedelta(weeks = 12)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None) + }, + + 'td': { + 'current': lambda x: x.replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), + 'previous': lambda x: (x - datetime.timedelta(days = 1)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), + 'next': lambda x: (x + datetime.timedelta(days = 1)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None) + }, + 'tw': { + 'current': lambda x: (x - datetime.timedelta(days = x.weekday())).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), + 'previous': lambda x: (x - datetime.timedelta(days = 7)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), + 'next': lambda x: (x + datetime.timedelta(days = 7)).replace(hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None) + }, + 'tm': { + 'current': lambda x: x.replace(day = 1, hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), + 'previous': lambda x: (x - datetime.timedelta(days = 1)).replace(day = 1, hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), + 'next': lambda x: (x + datetime.timedelta(days = 32)).replace(day = 1, hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None) + }, + 'ty': { + 'current': lambda x: x.replace(month = 1, day = 1, hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), + 'previous': lambda x: x.replace(year = x.year - 1, month = 1, day = 1, hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None), + 'next': lambda x: x.replace(year = x.year + 1, month = 1, day = 1, hour = 0, minute = 0, second = 0, microsecond = 0, tzinfo = None) + } +} +"""Default list of time windows for 'by time' quicksearch lists.""" diff --git a/lib/vial/db.py b/lib/vial/db.py new file mode 100644 index 0000000000000000000000000000000000000000..3b772014a8570c3a41c0354f0883e694ca5c5c15 --- /dev/null +++ b/lib/vial/db.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +#------------------------------------------------------------------------------- +# Use of this source is governed by the MIT license, see LICENSE file. +#------------------------------------------------------------------------------- + + +""" +This module contains database layer for *Vial* application. +""" + + +import flask_sqlalchemy + + +_DB = None + + +def db_setup(**kwargs): + """ + Opens a new database connection if there is none yet for the + current application context. + + :return: Database storage handler. + :rtype: flask_sqlalchemy.SQLAlchemy + """ + global _DB # pylint: disable=locally-disabled,global-statement + if not _DB: + _DB = flask_sqlalchemy.SQLAlchemy(**kwargs) + + return _DB + +def db_get(): + """ + Convenience method. + """ + return _DB + + +def db_session(): + """ + Convenience method. + """ + return db_get().session + + +def db_query(dbmodel): + """ + Convenience method. + """ + return db_session().query(dbmodel) diff --git a/lib/hawat/errors.py b/lib/vial/errors.py similarity index 85% rename from lib/hawat/errors.py rename to lib/vial/errors.py index 174d7fc36d6e5bde69783f66e6012d3c3a0352d6..6de11585057c5278317dc2998c3a4163d66c009d 100644 --- a/lib/hawat/errors.py +++ b/lib/vial/errors.py @@ -1,28 +1,21 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- #------------------------------------------------------------------------------- -# This file is part of Mentat system (https://mentat.cesnet.cz/). -# -# Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. #------------------------------------------------------------------------------- """ -This module contains error handling features for Hawat application. +This module contains error handling code for *Vial* application. """ -__author__ = "Jan Mach <jan.mach@cesnet.cz>" -__credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea Kropáčová <andrea.kropacova@cesnet.cz>" - - from werkzeug.http import HTTP_STATUS_CODES from flask import request, make_response, render_template, jsonify from flask_babel import gettext from flask_login import current_user -import hawat.const +import vial.const def wants_json_response(): @@ -50,7 +43,7 @@ def _make_payload(status_code, message = None, exception = None): if hasattr(exception.__class__, 'description'): payload['message'] = exception.__class__.description # Append the whole exception object for developers to make debugging easier. - if current_user.is_authenticated and current_user.has_role(hawat.const.ROLE_DEVELOPER): + if current_user.is_authenticated and current_user.has_role(vial.const.ROLE_DEVELOPER): payload['exception'] = exception return payload diff --git a/lib/hawat/forms.py b/lib/vial/forms.py similarity index 94% rename from lib/hawat/forms.py rename to lib/vial/forms.py index 8ef15f943e1cfa90e01c2be63e31b328f3e0175c..14eaf2ecadbc823adbfc761324bd62d94557422e 100644 --- a/lib/hawat/forms.py +++ b/lib/vial/forms.py @@ -1,42 +1,26 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- #------------------------------------------------------------------------------- -# This file is part of Mentat system (https://mentat.cesnet.cz/). -# -# Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. #------------------------------------------------------------------------------- """ -This module contains common forms for Hawat application. - -List of available forms: -^^^^^^^^^^^^^^^^^^^^^^^^ - -:py:class:`hawat.forms.ItemDeleteForm` - Generic item deletion confirmation form. +This module contains usefull form related classes for *Vial* application views. """ -__author__ = "Jan Mach <jan.mach@cesnet.cz>" -__credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea Kropáčová <andrea.kropacova@cesnet.cz>" - - import datetime import urllib.parse import pytz import wtforms import ipranges -# -# Flask related modules. -# import flask import flask_wtf from flask_babel import gettext, lazy_gettext -import hawat.const +import vial.const def default_dt_with_delta(delta = 7): @@ -135,7 +119,7 @@ def check_login(form, field): # pylint: disable=locally-disabled,unused-argumen """ Callback for validating user account logins (usernames). """ - if hawat.const.CRE_LOGIN.match(field.data): + if vial.const.CRE_LOGIN.match(field.data): return raise wtforms.validators.ValidationError( gettext( @@ -149,15 +133,14 @@ def check_email_list(form, field): # pylint: disable=locally-disabled,unused-ar Callback for validating list of strings. """ for data in field.data: - if hawat.const.CRE_EMAIL.match(data): + if vial.const.CRE_EMAIL.match(data): continue - else: - raise wtforms.validators.ValidationError( - gettext( - 'The "%(val)s" value does not look like valid email adress.', - val = str(data) - ) + raise wtforms.validators.ValidationError( + gettext( + 'The "%(val)s" value does not look like valid email adress.', + val = str(data) ) + ) def check_ip_record(form, field): # pylint: disable=locally-disabled,unused-argument """ @@ -495,7 +478,7 @@ class SelectFieldWithNone(wtforms.SelectField): class BaseItemForm(flask_wtf.FlaskForm): """ - Class representing generic item action (create/update/delete) form for Hawat + Class representing generic item action (create/update/delete) form for Vial application. This form contains support for redirection back to original page. @@ -523,7 +506,7 @@ class BaseItemForm(flask_wtf.FlaskForm): class ItemActionConfirmForm(BaseItemForm): """ - Class representing generic item action confirmation form for Hawat application. + Class representing generic item action confirmation form for Vial application. This form contains nothing else but two buttons, one for confirmation, one for canceling the delete action. Actual item identifier is passed as part of the URL. @@ -538,7 +521,7 @@ class ItemActionConfirmForm(BaseItemForm): class BaseSearchForm(flask_wtf.FlaskForm): """ - Class representing generic item search form for Hawat application. + Class representing generic item search form for Vial application. This form contains support for result limiting and paging. """ @@ -548,8 +531,8 @@ class BaseSearchForm(flask_wtf.FlaskForm): wtforms.validators.Optional() ], filters = [int], - choices = hawat.const.PAGER_LIMIT_CHOICES, - default = hawat.const.DEFAULT_PAGER_LIMIT + choices = vial.const.PAGER_LIMIT_CHOICES, + default = vial.const.DEFAULT_PAGER_LIMIT ) page = wtforms.IntegerField( lazy_gettext('Page number:'), diff --git a/lib/vial/intl.py b/lib/vial/intl.py new file mode 100644 index 0000000000000000000000000000000000000000..485cb62fd68b922b7e5d95e450a4795f09a19512 --- /dev/null +++ b/lib/vial/intl.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +#------------------------------------------------------------------------------- +# Use of this source is governed by the MIT license, see LICENSE file. +#------------------------------------------------------------------------------- + + +""" +This module contains usefull internationalization utilities for *Vial* application. +""" + + +import os + +import click +import flask +from flask.cli import AppGroup +import flask_babel +import flask_login +from babel import Locale + +import vial.const + + +BABEL = flask_babel.Babel() +@BABEL.localeselector +def get_locale(): # pylint: disable=locally-disabled,unused-variable + """ + Implementation of locale selector for :py:mod:`flask_babel`. + """ + # If a user is logged in, try to use the locale from the user settings. + if flask_login.current_user.is_authenticated: + if hasattr(flask_login.current_user, 'locale') and flask_login.current_user.locale: + flask.session['locale'] = flask_login.current_user.locale + + # Store the best locale selection into the session. + if 'locale' not in flask.session or not flask.session['locale']: + flask.session['locale'] = flask.request.accept_languages.best_match( + flask.current_app.config['SUPPORTED_LOCALES'].keys() + ) + + if 'locale' in flask.session and flask.session['locale']: + return flask.session['locale'] + return flask.current_app.config['BABEL_DEFAULT_LOCALE'] + +@BABEL.timezoneselector +def get_timezone(): # pylint: disable=locally-disabled,unused-variable + """ + Implementation of timezone selector for :py:mod:`flask_babel`. + """ + # If a user is logged in, try to use the timezone from the user settings. + if flask_login.current_user.is_authenticated: + if hasattr(flask_login.current_user, 'timezone') and flask_login.current_user.timezone: + flask.session['timezone'] = flask_login.current_user.timezone + + # Store the default timezone selection into the session. + if 'timezone' not in flask.session or not flask.session['timezone']: + flask.session['timezone'] = flask.current_app.config['BABEL_DEFAULT_TIMEZONE'] + + return flask.session['timezone'] + + +def babel_format_bytes(size, unit = 'B', step_size = 1024): + """ + Format given numeric value to human readable string describing size in + B/KB/MB/GB/TB. + + :param int size: Number to be formatted. + :param enum unit: Starting unit, possible values are ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB']. + :param int step_size: Size of the step between units. + :return: Formatted and localized string. + :rtype: string + """ + units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB'] + idx_max = len(units) - 1 + unit = unit.upper() + for idx, val in enumerate(units): + # Skip the last step, there is no next unit defined after exabyte. + if idx == idx_max: + break + if size > step_size: + if unit == val: + size = size / step_size + unit = units[idx+1] + else: + break + return '{} {}'.format( + flask_babel.format_decimal(size), + unit + ) + +def babel_translate_locale(locale_id, with_current = False): + """ + Translate given locale language. By default return language in locale`s + language. Optionaly return language in given locale`s language. + """ + locale_obj = Locale.parse(locale_id) + if not with_current: + return locale_obj.language_name + return locale_obj.get_language_name(flask_babel.get_locale()) + +def babel_language_in_locale(locale_id = 'en'): + """ + Translate given locale language. By default return language in locale`s + language. Optionaly return language in given locale`s language. + """ + locale_obj = Locale.parse(flask_babel.get_locale()) + return locale_obj.get_language_name(locale_id) diff --git a/lib/hawat/jsglue.py b/lib/vial/jsglue.py similarity index 94% rename from lib/hawat/jsglue.py rename to lib/vial/jsglue.py index 7cdf6059e0f3828e7fd5ca75668d52a4008f8ffd..87118d98086ef8bf18a40585a727b764ea0ea512 100644 --- a/lib/hawat/jsglue.py +++ b/lib/vial/jsglue.py @@ -1,3 +1,9 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +#------------------------------------------------------------------------------- +# Use of this source is governed by the MIT license, see LICENSE file. +#------------------------------------------------------------------------------- + # # This file is part of official Flask-JSGlue project (https://github.com/stewartpark/Flask-JSGlue). # However at this point (June 2019) the latest version available from PyPI (0.3.1) @@ -7,10 +13,13 @@ # No copyright infrigement intended. # -from flask import current_app, make_response, url_for -from jinja2 import Markup + import re, json +from jinja2 import Markup +from flask import current_app, make_response, url_for + + JSGLUE_JS_PATH = '/jsglue.js' JSGLUE_NAMESPACE = 'Flask' rule_parser = re.compile(r'<(.+?)>') diff --git a/lib/hawat/log.py b/lib/vial/log.py similarity index 74% rename from lib/hawat/log.py rename to lib/vial/log.py index 2b3695bfa875c7ac5875f800a4b4c8d5a9088cb8..4c20e0223d37e7d9214b45405e2bc6786670c187 100644 --- a/lib/hawat/log.py +++ b/lib/vial/log.py @@ -1,22 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- #------------------------------------------------------------------------------- -# This file is part of Mentat system (https://mentat.cesnet.cz/). -# -# Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. #------------------------------------------------------------------------------- """ -This module contains logging functions for Hawat application. +This module contains logging setup for *Vial* application. """ -__author__ = "Jan Mach <jan.mach@cesnet.cz>" -__credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea Kropáčová <andrea.kropacova@cesnet.cz>" - - import logging from logging.handlers import WatchedFileHandler, SMTPHandler @@ -25,7 +18,7 @@ def setup_logging_default(app): """ Setup default application logging features. """ - log_level_str = app.config['HAWAT_LOG_DEFAULT_LEVEL'].upper() + log_level_str = app.config['LOG_DEFAULT_LEVEL'].upper() log_level = getattr( logging, log_level_str, @@ -38,7 +31,8 @@ def setup_logging_default(app): app.logger.setLevel(log_level) app.logger.debug( - 'Hawat: Default logging services successfully started with level %s', + '%s: Default logging services successfully started with level %s', + app.config['APPLICATION_NAME'], log_level_str ) @@ -49,7 +43,7 @@ def setup_logging_file(app): """ Setup application logging via watched file (rotated by external command). """ - log_level_str = app.config['HAWAT_LOG_FILE_LEVEL'].upper() + log_level_str = app.config['LOG_FILE_LEVEL'].upper() log_level = getattr( logging, log_level_str, @@ -60,7 +54,7 @@ def setup_logging_file(app): 'Invalid log file level: %s' % log_level_str ) - file_handler = WatchedFileHandler(app.config['HAWAT_LOG_FILE']) + file_handler = WatchedFileHandler(app.config['LOG_FILE']) file_handler.setLevel(log_level) file_handler.setFormatter( logging.Formatter( @@ -70,8 +64,9 @@ def setup_logging_file(app): app.logger.addHandler(file_handler) app.logger.debug( - 'Hawat: File logging services successfully started to file %s with level %s', - app.config['HAWAT_LOG_FILE'], + '%s: File logging services successfully started to file %s with level %s', + app.config['APPLICATION_NAME'], + app.config['LOG_FILE'], log_level_str ) @@ -81,7 +76,7 @@ def setup_logging_email(app): """ Setup application logging via email. """ - log_level_str = app.config['HAWAT_LOG_EMAIL_LEVEL'].upper() + log_level_str = app.config['LOG_EMAIL_LEVEL'].upper() log_level = getattr( logging, log_level_str, @@ -102,7 +97,7 @@ def setup_logging_email(app): mail_handler = SMTPHandler( mailhost = (app.config['MAIL_SERVER'], app.config['MAIL_PORT']), fromaddr = app.config['MAIL_DEFAULT_SENDER'], - toaddrs = app.config['HAWAT_ADMINS'], + toaddrs = app.config['EMAIL_ADMINS'], subject = app.config['MAIL_SUBJECT_PREFIX'] + ' Application Error', credentials = credentials, secure = secure @@ -123,7 +118,8 @@ Message: app.logger.addHandler(mail_handler) app.logger.debug( - 'Hawat: Email logging services successfully started with level %s', + '%s: Email logging services successfully started with level %s', + app.config['APPLICATION_NAME'], log_level_str ) diff --git a/lib/hawat/mailer.py b/lib/vial/mailer.py similarity index 71% rename from lib/hawat/mailer.py rename to lib/vial/mailer.py index 95a29d88374441e63f72056175196213a3d196a1..0e99db8952561a009e7e5155d7c80d64534ac28f 100644 --- a/lib/hawat/mailer.py +++ b/lib/vial/mailer.py @@ -1,21 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- #------------------------------------------------------------------------------- -# This file is part of Mentat system (https://mentat.cesnet.cz/). -# -# Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. #------------------------------------------------------------------------------- + """ -This module contains mailing functions for Hawat application. They are built on -top of :py:mod:`flask_mail` extension. +This module contains mailer setup for *Vial* application. """ -__author__ = "Honza Mach <honza.mach.ml@gmail.com>" - - import flask_mail diff --git a/lib/hawat/menu.py b/lib/vial/menu.py similarity index 97% rename from lib/hawat/menu.py rename to lib/vial/menu.py index 4168328ae68442898e50b4e2bf2f514b445f470d..f77ae0f607dd8b188079231445a268d157585828 100644 --- a/lib/hawat/menu.py +++ b/lib/vial/menu.py @@ -1,22 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- #------------------------------------------------------------------------------- -# This file is part of Mentat system (https://mentat.cesnet.cz/). -# -# Copyright (C) since 2011 CESNET, z.s.p.o (http://www.ces.net/) # Use of this source is governed by the MIT license, see LICENSE file. #------------------------------------------------------------------------------- """ -This module contains application menu model for Hawat user interface. +This module contains usefull menu representation *Vial* application. """ -__author__ = "Jan Mach <jan.mach@cesnet.cz>" -__credits__ = "Pavel Kácha <pavel.kacha@cesnet.cz>, Andrea Kropáčová <andrea.kropacova@cesnet.cz>" - - import re import collections @@ -24,7 +17,7 @@ import flask import flask_login import flask_principal -import hawat.acl +import vial.acl CRE_STRIP_QUESTION = re.compile(r'\?$') @@ -94,9 +87,9 @@ def _filter_menu_entries(entries, **kwargs): if not authspec.can(): #print("Hiding menu entry '{}', accessible only to '{}'.".format(entry_id, str(authspec))) hideflag = True - # Authorization rules may be specified as indices to hawat.acl permission dictionary. + # Authorization rules may be specified as indices to Vial.acl permission dictionary. else: - if not hawat.acl.PERMISSIONS[authspec].can(): + if not vial.acl.PERMISSIONS[authspec].can(): #print("Hiding menu entry '{}', accessible only to '{}'.".format(entry_id, str(authspec))) hideflag = True if hideflag: @@ -147,7 +140,7 @@ def _get_menu_entries(entries, **kwargs): #------------------------------------------------------------------------------- -class MenuEntry: +class MenuEntry: # pylint: disable=locally-disabled,too-many-instance-attributes """ Base class for all menu entries. """ diff --git a/lib/vial/utils.py b/lib/vial/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..b0f2e1453e3b9abc3e234f0a760204cf63365ec5 --- /dev/null +++ b/lib/vial/utils.py @@ -0,0 +1,219 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +#------------------------------------------------------------------------------- +# Use of this source is governed by the MIT license, see LICENSE file. +#------------------------------------------------------------------------------- + + +""" +This module contains various usefull utilities for *Vial* application. +""" + + +import os +import uuid +import copy +import datetime +import json +import yaml + + +class URLParamsBuilder: + """ + Small utility class for building URL parameter dictionaries for various view + endpoints. + + .. note:: + + This class is still proof of concept and work in progress. + """ + def __init__(self, skeleton = None): + self.rules = [] + self.kwrules = {} + self.skeleton = skeleton or {} + + @staticmethod + def _add_scalar(dst, key, val): + if val is not None: + dst[key] = val + + @staticmethod + def _add_vector(dst, key, val): + if val is not None: + dst.setdefault(key, []).append(val) + + def add_rule(self, key, as_list = False, optional = False): + """ + Add new rule to URL parameter builder. + + :param str key: Name of the rule key. + :param bool as_list: Indication that the rule parameter is a list of multiple values. + :param bool optional: Indication that the rule parameter is optional. + """ + if as_list: + rule = [key, self._add_vector, True, optional] + self.rules.append(rule) + else: + rule = [key, self._add_scalar, False, optional] + self.rules.append(rule) + return self + + def add_kwrule(self, key, as_list = False, optional = False): + """ + Add new keyword rule to URL parameter builder. + + :param str key: Name of the rule key. + :param bool as_list: Indication that the rule parameter is a list of multiple values. + :param bool optional: Indication that the rule parameter is optional. + """ + if as_list: + rule = [key, self._add_vector, True, optional] + self.kwrules[key] = rule + else: + rule = [key, self._add_scalar, False, optional] + self.kwrules[key] = rule + return self + + def get_params(self, *args, **kwargs): + """ + Get URL parameters as dictionary with filled-in values. + """ + tmp = copy.deepcopy(self.skeleton) + for idx, rule in enumerate(self.rules): + try: + rule[1](tmp, rule[0], args[idx]) + except IndexError: + if not rule[3]: + raise + for key, rule in self.kwrules.items(): + if key in kwargs: + rule[1](tmp, rule[0], kwargs[key]) + return tmp + + +class LimitCounter: + """ + Simple configurable limit counter with support for multiple keys. + """ + def __init__(self, limit): + self.counters = {} + self.limit = limit + + def count_and_check(self, key, increment = 1): + """ + Increment key counter and check against internal limit. + """ + self.counters[key] = self.counters.get(key, 0) + increment + return self.counters[key] <= self.limit + + +#------------------------------------------------------------------------------ + + +def get_timedelta(tstamp): + """ + Get timedelta from current UTC time and given datetime object. + + :param datetime.datetime: Datetime of the lower timedelta boundary. + :return: Timedelta object. + :rtype: datetime.timedelta + """ + return datetime.datetime.utcnow() - tstamp + +def get_datetime_utc(aware = False): + """ + Get current UTC datetime. + + :return: Curent UTC datetime. + :rtype: datetime.datetime + """ + if aware: + return datetime.datetime.now(datetime.timezone.utc) + return datetime.datetime.utcnow() + +def parse_datetime(dtstring): + """ + Parse given datetime string. + + :param str dtstring: Datetime string in ISON format to parse. + :return: Curent UTC datetime. + :rtype: datetime.datetime + """ + return datetime.datetime.fromisoformat(dtstring) + +def get_datetime_local(): + """ + Get current local timestamp. + + :return: Curent local timestamp. + :rtype: datetime.datetime + """ + return datetime.datetime.now() + +def check_file_exists(filename): + """ + Check, that given file exists in the filesystem. + + :param str filename: Name of the file to check. + :return: Existence flag as ``True`` or ``False``. + :rtype: bool + """ + return os.path.isfile(filename) + +def in_query_params(haystack, needles, on_true = True, on_false = False, on_empty = False): + """ + Utility method for checking that any needle from given list of needles is + present in given haystack. + """ + if not haystack: + return on_empty + for needle in needles: + if needle in haystack: + return on_true + return on_false + +def generate_query_params(baseparams, updates): + """ + Generate query parameters for GET method form. + + :param dict baseparams: Original query parameters. + :param dict updates: Updates for query parameters. + :return: Deep copy of original parameters modified with given updates. + :rtype: dict + """ + result = copy.deepcopy(baseparams) + result.update(updates) + return result + +def json_to_yaml(json_data): + """ + 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 yaml.dump( + yaml.safe_load( + json_data + ), + default_flow_style=False + ) + +def get_uuid4(): + """ + Generate random UUID identifier. + """ + return uuid.uuid4() + +def load_json_from_file(filename): + """ + Load JSON from given file. + """ + with open(filename) as fhnd: + res = json.load(fhnd) + return res + +def make_copy_deep(data): + """ + Make a deep copy of given data structure. + """ + return copy.deepcopy(data) diff --git a/lib/vial/view/__init__.py b/lib/vial/view/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..ccce06dfd9ccc7de663fc36ed84c2ec0b74c41ea --- /dev/null +++ b/lib/vial/view/__init__.py @@ -0,0 +1,1906 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +#------------------------------------------------------------------------------- +# Use of this source is governed by the MIT license, see LICENSE file. +#------------------------------------------------------------------------------- + + +""" +This module contains base classes for all *Vial* application views. They are all +based on :py:class:`flask.views.View`. +""" + + +import re +import sys +import datetime +import traceback + +import werkzeug.routing +import werkzeug.utils +import flask +import flask.app +import flask.views +import flask_login +from flask_babel import gettext + +import vial.const +import vial.menu +import vial.db +import vial.errors +from vial.view.mixin import VialUtils +from vial.forms import ItemActionConfirmForm + + +class DecoratedView: + """ + Wrapper class for classical decorated view functions. + """ + def __init__(self, view_function): + self.view_function = view_function + + def get_view_name(self): + """Simple adapter method to enable support of classical decorated views.""" + return self.view_function.__name__ + + def get_view_endpoint(self): + """Simple adapter method to enable support of classical decorated views.""" + return self.get_view_name() + + def get_view_icon(self): + """Simple adapter method to enable support of classical decorated views.""" + return 'view-{}'.format(self.get_view_name()) + + +class BaseView(flask.views.View): + """ + Base class for all custom Vial application views. + """ + + module_ref = None + """ + Weak reference to parent module/blueprint of this view. + """ + + module_name = None + """ + Name of the parent module/blueprint. Will be set up during the process + of registering the view into the blueprint in :py:func:`vial.app.VialBlueprint.register_view_class`. + """ + + authentication = False + """ + Similar to the ``decorators`` mechanism in Flask pluggable views, you may use + this class variable to specify, that the view is protected by authentication. + During the process of registering the view into the blueprint in + :py:func:`vial.app.VialBlueprint.register_view_class` the view will be + automatically decorated with :py:func:`flask_login.login_required` decorator. + + The advantage of using this in favor of ``decorators`` is that the application + menu can automatically hide/show items inaccessible to current user. + + This is a scalar variable that must contain boolean ``True`` or ``False``. + """ + + authorization = () + """ + Similar to the ``decorators`` mechanism in Flask pluggable views, you may use + this class variable to specify, that the view is protected by authorization. + During the process of registering the view into the blueprint in + :py:func:`vial.app.VialBlueprint.register_view_class` the view will be + automatically decorated with given authorization decorators. + + The advantage of using this in favor of ``decorators`` is that the application + menu can automatically hide/show items inaccessible to current user. + + This is a list variable that must contain list of desired decorators. + """ + + url_params_unsupported = () + """ + List of URL parameters, that are not supported by this view and should be removed + before generating the URL. + """ + + @classmethod + def get_view_name(cls): + """ + Return unique name for the view. Name must be unique in the namespace of + parent blueprint/module and should contain only characters ``[a-z0-9]``. + It will be used for generating endpoint name for the view. + + *This method does not have any default implementation and must be overridden + by a subclass.* + + :return: Name for the view. + :rtype: str + """ + raise NotImplementedError() + + @classmethod + def get_view_endpoint(cls): + """ + Return name of the routing endpoint for the view within the whole application. + + Default implementation generates the endpoint name by concatenating the + module name and view name. + + :return: Routing endpoint for the view within the whole application. + :rtype: str + """ + return '{}.{}'.format(cls.module_name, cls.get_view_name()) + + @classmethod + def get_view_url(cls, **kwargs): + """ + Return view URL. + + :param dict kwargs: Optional parameters. + :return: URL for the view. + :rtype: str + """ + # Filter out unsupported URL parameters. + params = { + k: v for k, v in filter( + lambda x: x[0] not in cls.url_params_unsupported, + kwargs.items() + ) + } + return flask.url_for( + cls.get_view_endpoint(), + **params + ) + + @classmethod + def get_view_icon(cls): + """ + Return menu entry icon name for the view. Given name will be used as index + to built-in icon registry. + + Default implementation generates the icon name by concatenating the prefix + ``module-`` with module name. + + :return: View icon. + :rtype: str + """ + return 'module-{}'.format(cls.module_name) + + @classmethod + def get_view_title(cls, **kwargs): + """ + Return title for the view, that will be displayed in the ``title`` tag of + HTML ``head`` element and also as the content of page header in ``h2`` tag. + + Default implementation returns the return value of :py:func:`vial.view.BaseView.get_menu_title` + method by default. + + :param dict kwargs: Optional parameters. + :return: Title for the view. + :rtype: str + """ + raise NotImplementedError() + + @classmethod + def get_menu_title(cls, **kwargs): + """ + Return menu entry title for the view. + + Default implementation returns the return value of :py:func:`vial.view.BaseView.get_view_title` + method by default. + + :param dict kwargs: Optional parameters. + :return: Menu entry title for the view. + :rtype: str + """ + return cls.get_view_title(**kwargs) + + @classmethod + def get_menu_legend(cls, **kwargs): + """ + Return menu entry legend for the view (menu entry hover tooltip). + + Default implementation returns the return value of :py:func:`vial.view.BaseView.get_menu_title` + method by default. + + :param dict kwargs: Optional parameters. + :return: Menu entry legend for the view. + :rtype: str + """ + return cls.get_menu_title(**kwargs) + + #--------------------------------------------------------------------------- + + @staticmethod + def can_access_endpoint(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 + """ + return flask.current_app.can_access_endpoint(endpoint, **kwargs) + + @staticmethod + def has_endpoint(endpoint): + """ + Check if given routing endpoint is available within the application. + + :param str endpoint: Application routing endpoint. + :return: ``True`` in case endpoint exists, ``False`` otherwise. + :rtype: bool + """ + return flask.current_app.has_endpoint(endpoint) + + @staticmethod + def get_endpoint_class(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 + """ + return flask.current_app.get_endpoint_class(endpoint, quiet) + + #--------------------------------------------------------------------------- + + @property + def logger(self): + """ + Return current application`s logger object. + """ + return flask.current_app.logger + + +class FileNameView(BaseView): + """ + Base class for direct file access views. These views can be used to access + and serve files from arbitrary filesystem directories (that are accessible to + application process). This can be very usefull for serving files like charts, + that are periodically generated into configurable and changeable location. + """ + + @classmethod + def get_view_icon(cls): + """*Implementation* of :py:func:`vial.view.BaseView.get_view_icon`.""" + return 'action-download' + + @classmethod + def get_directory_path(cls): + """ + Return absolute path to the directory, that will be used as a base path + for serving files. + + *This method does not have any default implementation and must be overridden + by a subclass.* + + :return: Absolute path to the directory for serving files. + :rtype: str + """ + raise NotImplementedError() + + @classmethod + def validate_filename(cls, filename): + """ + Validate given file name to prevent user from accessing restricted files. + + In default implementation all files pass the validation. + + :param str filename: Name of the file to be validated/filtered. + :return: ``True`` in case file name is allowed, ``False`` otherwise. + :rtype: bool + """ + return bool(filename) + + def dispatch_request(self, filename): # 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. + """ + if not self.validate_filename(filename): + flask.abort(400) + + self.logger.info( + "Serving file '{}' from directory '{}'.".format( + filename, + self.get_directory_path() + ) + ) + return flask.send_from_directory( + self.get_directory_path(), + filename, + as_attachment = True + ) + + +class FileIdView(BaseView): + """ + Base class for indirrect file access views. These views can be used to access + and serve files from arbitrary filesystem directories (that are accessible to + application process). This can be very usefull for serving files like charts, + that are periodically generated into configurable and changeable location. + The difference between this view class and :py:class:`FileNameView` is, + that is this case some kind of identifier is used to access the file and + provided class method is responsible for translating this identifier into + real file name. + """ + + @classmethod + def get_view_icon(cls): + """*Implementation* of :py:func:`vial.view.BaseView.get_view_icon`.""" + return 'action-download' + + @classmethod + def get_directory_path(cls, fileid, filetype): + """ + This method must return absolute path to the directory, that will be + used as a base path for serving files. Parameter ``fileid`` may be used + internally to further customize the base directory, for example when + serving some files places into subdirectories based on some part of the + file name (for example to reduce total number of files in base directory). + + *This method does not have any default implementation and must be overridden + by a subclass.* + + :param str fileid: Identifier of the requested file. + :param str filetype: Type of the requested file. + :return: Absolute path to the directory for serving files. + :rtype: str + """ + raise NotImplementedError() + + @classmethod + def get_filename(cls, fileid, filetype): + """ + This method must return actual name of the file based on given identifier + and type. + + *This method does not have any default implementation and must be overridden + by a subclass.* + + :param str fileid: Identifier of the requested file. + :param str filetype: Type of the requested file. + :return: Translated name of the file. + :rtype: str + """ + raise NotImplementedError() + + def dispatch_request(self, fileid, filetype): # 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. + """ + basedirpath = self.get_directory_path(fileid, filetype) + filename = self.get_filename(fileid, filetype) + if not basedirpath or not filename: + flask.abort(400) + + self.logger.info( + "Serving file '{}' from directory '{}'.".format( + filename, + basedirpath + ) + ) + return flask.send_from_directory( + basedirpath, + filename, + as_attachment = True + ) + + +class RenderableView(BaseView): # pylint: disable=locally-disabled,abstract-method + """ + Base class for all views, that are rendering content based on Jinja2 templates + or returning JSON/XML data. + """ + + def __init__(self): + self.response_context = {} + + def mark_time(self, ident, tag = 'default', label = 'Time mark', log = False): + """ + Mark current time with given identifier and label for further analysis. + This method can be usefull for measuring durations of various operations. + """ + mark = [datetime.datetime.utcnow(), ident, tag, label] + marks = self.response_context.setdefault('time_marks', []) + marks.append(mark) + + if log: + if len(marks) <= 1: + self.logger.info( + 'Mark {}:{} ({})'.format(*mark[1:]) + ) + else: + self.logger.info( + 'Mark {}:{} ({});delta={};delta0={}'.format( + *mark[1:], + (marks[-1][0]-marks[-2][0]).__str__(), # Time delta from last mark. + (marks[-1][0]-marks[0][0]).__str__() # Time delta from first mark. + ) + ) + + @classmethod + def get_view_template(cls): + """ + Return Jinja2 template file that should be used for rendering the view + content. This default implementation works only in case the view class + was properly registered into the parent blueprint/module with + :py:func:`vial.app.VialBlueprint.register_view_class` method. + + :return: Jinja2 template file to use to render the view. + :rtype: str + """ + if cls.module_name: + return '{}/{}.html'.format( + cls.module_name, + cls.get_view_name() + ) + raise RuntimeError("Unable to guess default view template, because module name was not yet set.") + + def do_before_response(self, **kwargs): # pylint: disable=locally-disabled,no-self-use,unused-argument + """ + This method will be called just before generating the response. By providing + some meaningfull implementation you can use it for some simple item and + response context mangling tasks. + + :param kwargs: Custom additional arguments. + """ + + def generate_response(self): + """ + Generate the appropriate response from given response context. + + :param dict response_context: Response context as a dictionary + """ + raise NotImplementedError() + + @staticmethod + def abort(status_code, message = None): + """ + Abort request processing with HTTP status code. + """ + raise NotImplementedError() + + def flash(self, message, level = 'info'): + """ + Flash information to the user. + """ + raise NotImplementedError() + + def redirect(self, default_url = None, exclude_url = None): + """ + Redirect user to different location. + """ + raise NotImplementedError() + + +class SimpleView(RenderableView): # pylint: disable=locally-disabled,abstract-method + """ + Base class for simple views. These are the most, well, simple views, that are + rendering single template file or directly returning some JSON/XML data without + any user parameters. + + In most use cases, it should be enough to just enhance the default implementation + of :py:func:`vial.view.RenderableView.get_response_context` to inject + some additional variables into the template. + """ + + 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. + """ + self.do_before_response() + return self.generate_response() + + +class BaseSearchView(RenderableView, VialUtils): + """ + Base class for search views. + """ + @classmethod + def get_view_name(cls): + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" + return 'search' + + @classmethod + def get_view_icon(cls): + """*Implementation* of :py:func:`mydojo.base.BaseView.get_view_name`.""" + return 'action-search' + + #--------------------------------------------------------------------------- + + @classmethod + def get_quicksearch_by_time(cls): + """ + Get default list of 'by time' quickseach items. + """ + quicksearch_list = [] + for item in ( + [ '1h', gettext('Search for last hour')], + [ '2h', gettext('Search for last 2 hours')], + [ '3h', gettext('Search for last 3 hours')], + [ '4h', gettext('Search for last 4 hours')], + [ '6h', gettext('Search for last 6 hours')], + ['12h', gettext('Search for last 12 hours')], + [ '1d', gettext('Search for last day')], + [ '2d', gettext('Search for last 2 days')], + [ '3d', gettext('Search for last 3 days')], + [ '1w', gettext('Search for last week')], + [ '2w', gettext('Search for last 2 weeks')], + [ '4w', gettext('Search for last 4 weeks')], + ['12w', gettext('Search for last 12 weeks')], + + [ 'td', gettext('Search for today')], + [ 'tw', gettext('Search for this week')], + [ 'tm', gettext('Search for this month')], + [ 'ty', gettext('Search for this year')], + ): + try: + dt_from = cls.get_datetime_window( + item[0], + 'current' + ) + dt_to = cls.get_datetime_window( + item[0], + 'next', + dt_from + ) + quicksearch_list.append( + { + 'label': item[1], + 'params': { + 'dt_from': dt_from.isoformat( + sep = ' ' + ), + 'dt_to': dt_to.isoformat( + sep = ' ' + ), + 'tiid': item[0], + 'submit': gettext('Search') + } + } + ) + except: # pylint: disable=locally-disabled,bare-except + pass + + return quicksearch_list + + @staticmethod + def get_search_form(request_args): + """ + Must return instance of :py:mod:`flask_wtf.FlaskForm` appropriate for + searching given type of items. + """ + raise NotImplementedError() + + @staticmethod + def get_query_parameters(form, request_args): + """ + Get query parameters by comparing contents of processed form data and + original request arguments. Result of this method can be used for generating + modified URLs back to current request. One of the use cases is the result + pager/paginator. + """ + params = {} + for arg in request_args: + if getattr(form, arg, None) and arg in request_args: + # Handle multivalue request arguments separately + # Resources: + # http://flask.pocoo.org/docs/1.0/api/#flask.Request.args + # http://werkzeug.pocoo.org/docs/0.14/datastructures/#werkzeug.datastructures.MultiDict + try: + if form.is_multivalue(arg): + params[arg] = request_args.getlist(arg) + else: + params[arg] = request_args[arg] + except AttributeError: + params[arg] = request_args[arg] + return params + + def search(self, form_args): + """ + Perform actual search with given query. + """ + raise NotImplementedError() + + #--------------------------------------------------------------------------- + + @classmethod + def get_breadcrumbs_menu(cls): + """ + Get breadcrumbs menu. + """ + breadcrumbs_menu = vial.menu.Menu() + breadcrumbs_menu.add_entry( + 'endpoint', + 'home', + endpoint = flask.current_app.config['ENDPOINT_HOME'] + ) + breadcrumbs_menu.add_entry( + 'endpoint', + cls.get_view_name(), + endpoint = '{}.{}'.format( + cls.module_name, + cls.get_view_name() + ) + ) + return breadcrumbs_menu + + @classmethod + def get_action_menu(cls): + """ + Get action menu for all items. + """ + return None + + @classmethod + def get_context_action_menu(cls): + """*Implementation* of :py:func:`vial.view.ItemListView.get_context_action_menu`.""" + context_action_menu = vial.menu.Menu() + context_action_menu.add_entry( + 'endpoint', + 'show', + endpoint = '{}.show'.format(cls.module_name), + hidetitle = True + ) + return context_action_menu + + #--------------------------------------------------------------------------- + + def do_before_search(self, form_data): # pylint: disable=locally-disabled,no-self-use,unused-argument + """ + This hook method will be called before search attempt. + """ + + def do_after_search(self, items): # pylint: disable=locally-disabled,no-self-use,unused-argument + """ + This hook method will be called after successfull search. + """ + + 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. + """ + form = self.get_search_form(flask.request.args) + flask.g.search_form = form + + if vial.const.FORM_ACTION_SUBMIT in flask.request.args: + if form.validate(): + form_data = form.data + + self.mark_time( + 'preprocess_begin', + tag = 'search', + label = 'Begin preprocessing for "{}"'.format(flask.request.full_path), + log = True + ) + self.do_before_search(form_data) + self.mark_time( + 'preprocess_end', + tag = 'search', + label = 'Done preprocessing for "{}"'.format(flask.request.full_path), + log = True + ) + + try: + self.mark_time( + 'search_begin', + tag = 'search', + label = 'Begin searching for "{}"'.format(flask.request.full_path), + log = True + ) + items = self.search(form_data) + self.mark_time( + 'search_end', + tag = 'search', + label = 'Done searching for "{}", items found: {}'.format(flask.request.full_path, len(items)), + log = True + ) + + self.response_context.update( + searched = True, + items = items, + items_count = len(items), + form_data = form_data + ) + + # Not all search forms support result paging. + if 'page' in form_data: + self.response_context.update( + pager_index_low = ((form_data['page'] - 1) * form_data['limit']) + 1, + pager_index_high = ((form_data['page'] - 1) * form_data['limit']) + len(items), + pager_index_limit = ((form_data['page'] - 1) * form_data['limit']) + form_data['limit'] + ) + + self.mark_time( + 'postprocess_begin', + tag = 'search', + label = 'Begin postprocessing for "{}"'.format(flask.request.full_path), + log = True + ) + self.do_after_search(items) + self.mark_time( + 'postprocess_end', + tag = 'search', + label = 'Done postprocessing for "{}"'.format(flask.request.full_path), + log = True + ) + + except Exception as err: # pylint: disable=locally-disabled,broad-except + match = re.match('invalid IP4R value: "([^"]+)"', str(err)) + if match: + self.flash( + flask.Markup( + gettext( + 'Invalid address value <strong>%(address)s</strong> in search form.', + address = flask.escape(str(match.group(1))) + ) + ), + vial.const.FLASH_FAILURE + ) + else: + raise + + else: + self.response_context.update( + form_errors = [(field_name, err) for field_name, error_messages in form.errors.items() for err in error_messages] + ) + + self.response_context.update( + query_params = self.get_query_parameters( + form, + flask.request.args + ), + search_widget_item_limit = 3 + ) + self.do_before_response() + return self.generate_response() + + +class ItemListView(RenderableView): # pylint: disable=locally-disabled,abstract-method + """ + Base class for item *list* views. These views provide quick and simple access + to lists of all objects. + """ + + @classmethod + def get_view_name(cls): + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" + return 'list' + + @classmethod + def get_breadcrumbs_menu(cls): + """ + Get breadcrumbs menu. + """ + action_menu = vial.menu.Menu() + action_menu.add_entry( + 'endpoint', + 'home', + endpoint = flask.current_app.config['ENDPOINT_HOME'] + ) + action_menu.add_entry( + 'endpoint', + 'list', + endpoint = '{}.list'.format(cls.module_name) + ) + return action_menu + + @classmethod + def get_action_menu(cls): + """ + Get action menu for all listed items. + """ + return None + + @classmethod + def get_context_action_menu(cls): + """ + Get context action menu for particular single item. + """ + return None + + def search(self, form_args): + """ + Perform actual search with given form arguments. + """ + raise NotImplementedError() + + 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. + + List of all items will be retrieved from database and injected into template + to be displayed to the user. + """ + items = self.search({}) + + self.response_context.update( + items = items + ) + + self.do_before_response() + return self.generate_response() + + +class ItemShowView(RenderableView): # pylint: disable=locally-disabled,abstract-method + """ + Base class for item *show* views. These views expect unique item identifier + as parameter and are supposed to display specific information about single + item. + """ + + @classmethod + def get_view_name(cls): + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" + return 'show' + + @classmethod + def get_view_icon(cls): + """*Implementation* of :py:func:`vial.view.BaseView.get_view_icon`.""" + return 'action-show' + + @classmethod + def get_view_title(cls, **kwargs): + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" + return gettext('Show') + + @classmethod + def get_view_url(cls, **kwargs): + """*Implementation* of :py:func:`vial.view.BaseView.get_view_url`.""" + return flask.url_for( + cls.get_view_endpoint(), + item_id = kwargs['item'].get_id() + ) + + @classmethod + def get_menu_title(cls, **kwargs): + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" + return gettext('Show') + + @classmethod + def authorize_item_action(cls, **kwargs): # pylint: disable=locally-disabled,unused-argument + """ + Perform access authorization for current user to particular item. + """ + return True + + @classmethod + def get_action_menu(cls): # pylint: disable=locally-disabled,unused-argument + """ + Get action menu for particular item. + """ + return None + + @classmethod + def get_breadcrumbs_menu(cls): # pylint: disable=locally-disabled,unused-argument + """ + Get breadcrumbs menu. + """ + action_menu = vial.menu.Menu() + action_menu.add_entry( + 'endpoint', + 'home', + endpoint = flask.current_app.config['ENDPOINT_HOME'] + ) + action_menu.add_entry( + 'endpoint', + 'list', + endpoint = '{}.list'.format(cls.module_name), + paramlist = [] + ) + action_menu.add_entry( + 'endpoint', + 'show', + endpoint = '{}.show'.format(cls.module_name) + ) + return action_menu + + def fetch(self, item_id): + """ + Fetch item with given ID. + """ + raise NotImplementedError() + + def dispatch_request(self, item_id): # 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 = self.fetch(item_id) + if not item: + self.abort(404) + + if not self.authorize_item_action(item = item): + self.abort(403) + + self.response_context.update( + item_id = item_id, + item = item, + search_widget_item_limit = 100 + ) + + self.do_before_response() + return self.generate_response() + + +class ItemActionView(RenderableView): # pylint: disable=locally-disabled,abstract-method + """ + Base class for item action views. These views perform various actions + (create/update/delete) with given item class. + """ + @classmethod + def get_view_icon(cls): + """*Implementation* of :py:func:`vial.view.BaseView.get_view_icon`.""" + return 'action-{}'.format( + cls.get_view_name().replace('_', '-') + ) + + @classmethod + def get_view_url(cls, **kwargs): + """*Implementation* of :py:func:`vial.view.BaseView.get_view_url`.""" + return flask.url_for( + cls.get_view_endpoint(), + item_id = kwargs['item'].get_id() + ) + + @classmethod + def get_view_template(cls): + """*Implementation* of :py:func:`vial.view.RenderableView.get_view_template`.""" + return 'form_{}.html'.format( + cls.get_view_name().replace('-', '_') + ) + + @staticmethod + def get_message_success(**kwargs): + """ + *Hook method*. Must return text for flash message in case of action *success*. + The text may contain HTML characters and will be passed to :py:class:`flask.Markup` + before being used, so to certain extend you may emphasize and customize the output. + """ + raise NotImplementedError() + + @staticmethod + def get_message_failure(**kwargs): + """ + *Hook method*. Must return text for flash message in case of action *failure*. + The text may contain HTML characters and will be passed to :py:class:`flask.Markup` + before being used, so to certain extend you may emphasize and customize the output. + """ + raise NotImplementedError() + + @staticmethod + def get_message_cancel(**kwargs): + """ + *Hook method*. Must return text for flash message in case of action *cancel*. + The text may contain HTML characters and will be passed to :py:class:`flask.Markup` + before being used, so to certain extend you may emphasize and customize the output. + """ + raise NotImplementedError() + + def get_url_next(self): + """ + *Hook method*. Must return URL for redirection after action *success*. In + most cases there should be call for :py:func:`flask.url_for` function + somewhere in this method. + """ + try: + return flask.url_for( + '{}.{}'.format(self.module_name, 'list') + ) + except werkzeug.routing.BuildError: + return flask.url_for( + flask.current_app.config['ENDPOINT_HOME'] + ) + + def check_action_cancel(self, form, **kwargs): + """ + Check the form for *cancel* button press and cancel the action. + """ + if getattr(form, vial.const.FORM_ACTION_CANCEL).data: + self.flash( + flask.Markup(self.get_message_cancel(**kwargs)), + vial.const.FLASH_INFO + ) + return self.redirect( + default_url = self.get_url_next() + ) + + return None + + def do_before_action(self, item): # pylint: disable=locally-disabled,no-self-use,unused-argument + """ + *Hook method*. Will be called before any action handling tasks. + """ + + def do_after_action(self, item): # pylint: disable=locally-disabled,no-self-use,unused-argument + """ + *Hook method*. Will be called after successfull action handling tasks. + """ + + @classmethod + def authorize_item_action(cls, **kwargs): # pylint: disable=locally-disabled,unused-argument + """ + Perform access authorization for current user to particular item. + """ + return True + + @property + def dbsession(self): + """ + This property contains the reference to current *SQLAlchemy* database session. + """ + raise NotImplementedError() + + @property + def dbmodel(self): + """ + This property must be implemented in each subclass to + return reference to appropriate model class based on *SQLAlchemy* declarative + base. + """ + raise NotImplementedError() + + @property + def dbchlogmodel(self): + """ + This property must be implemented in each subclass to + return reference to appropriate model class based on *SQLAlchemy* declarative + base. + """ + raise NotImplementedError() + + def fetch(self, item_id, fetch_by = None): + """ + Perform actual search with given query. + """ + raise NotImplementedError() + + def changelog_log(self, item, json_state_before = '', json_state_after = ''): + """ + Log item action into changelog. One of the method arguments is permitted + to be left out. This enables logging create and delete actions. + + :param mentat.datatype.sqldb.MODEL item: Item that is being changed. + :param str json_state_before: JSON representation of item state before action. + :param str json_state_after: JSON representation of item state after action. + """ + if not json_state_before and not json_state_after: + raise ValueError("Invalid use of changelog_log() method, both of the arguments are null.") + + chlog = self.dbchlogmodel( + author = flask_login.current_user._get_current_object(), # pylint: disable=locally-disabled,protected-access + model = item.__class__.__name__, + model_id = item.id, + endpoint = self.get_view_endpoint(), + module = self.module_name, + operation = self.get_view_name(), + before = json_state_before, + after = json_state_after + ) + chlog.calculate_diff() + self.dbsession.add(chlog) + self.dbsession.commit() + + +class ItemCreateView(ItemActionView): # pylint: disable=locally-disabled,abstract-method + """ + Base class for item *create* action views. These views create new items in + database. + """ + + @classmethod + def get_view_name(cls): + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" + return 'create' + + @classmethod + def get_view_template(cls): + """ + Return Jinja2 template file that should be used for rendering the view + content. This default implementation works only in case the view class + was properly registered into the parent blueprint/module with + :py:func:`vial.app.VialBlueprint.register_view_class` method. + + :return: Title for the view. + :rtype: str + """ + if cls.module_name: + return '{}/creatupdate.html'.format(cls.module_name) + raise RuntimeError("Unable to guess default view template, because module name was not yet set.") + + @classmethod + def get_view_title(cls, **kwargs): + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" + return gettext('Create') + + @classmethod + def get_view_url(cls, **kwargs): + """*Implementation* of :py:func:`vial.view.BaseView.get_view_url`.""" + return flask.url_for(cls.get_view_endpoint()) + + @classmethod + def get_menu_title(cls, **kwargs): + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" + return gettext('Create') + + @staticmethod + def get_item_form(): + """ + *Hook method*. Must return instance of :py:mod:`flask_wtf.FlaskForm` + appropriate for given item class. + """ + raise NotImplementedError() + + 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. + + This method will attempt to validate the submitted form and create new + instance of appropriate item from form data and finally store the item + into the database. + """ + if not self.authorize_item_action(): + self.abort(403) + + item = self.dbmodel() + + form = self.get_item_form() + + cancel_response = self.check_action_cancel(form) + if cancel_response: + return cancel_response + + if form.validate_on_submit(): + form_data = form.data + form.populate_obj(item) + + self.do_before_action(item) + + if form_data[vial.const.FORM_ACTION_SUBMIT]: + try: + self.dbsession.add(item) + self.dbsession.commit() + self.do_after_action(item) + + # Log the item creation into changelog. + self.changelog_log(item, '', item.to_json()) + + self.flash( + flask.Markup(self.get_message_success(item = item)), + vial.const.FLASH_SUCCESS + ) + return self.redirect(default_url = self.get_url_next()) + + except Exception: # pylint: disable=locally-disabled,broad-except + self.dbsession.rollback() + self.flash( + flask.Markup(self.get_message_failure()), + vial.const.FLASH_FAILURE + ) + flask.current_app.log_exception_with_label( + traceback.TracebackException(*sys.exc_info()), + self.get_message_failure() + ) + return self.redirect(default_url = self.get_url_next()) + + self.response_context.update( + action_name = gettext('Create'), + form_url = flask.url_for(self.get_view_endpoint()), + form = form, + item_action = vial.const.ACTION_ITEM_CREATE, + item = item + ) + + self.do_before_response() + return self.generate_response() + + +class ItemCreateForView(ItemActionView): # pylint: disable=locally-disabled,abstract-method + """ + Base class for item *createfor* action views. These views differ a little bit + from *create* action views. They are used to create new items within database, + but only for particular defined parent item. One example use case is creating + network records for particular abuse group. + """ + + @classmethod + def get_view_name(cls): + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" + return 'createfor' + + @classmethod + def get_view_icon(cls): + """*Implementation* of :py:func:`vial.view.BaseView.get_view_icon`.""" + return 'module-{}'.format(cls.module_name) + + @classmethod + def get_view_template(cls): + """ + Return Jinja2 template file that should be used for rendering the view + content. This default implementation works only in case the view class + was properly registered into the parent blueprint/module with + :py:func:`vial.app.VialBlueprint.register_view_class` method. + + :return: Title for the view. + :rtype: str + """ + if cls.module_name: + return '{}/creatupdate.html'.format(cls.module_name) + raise RuntimeError("Unable to guess default view template, because module name was not yet set.") + + @property + def dbmodel_par(self): + """ + *Hook property*. This property must be implemented in each subclass to + return reference to appropriate model class for parent objects and that + is based on *SQLAlchemy* declarative base. + """ + raise NotImplementedError() + + @property + def dbquery_par(self): + """ + This property contains the reference to *SQLAlchemy* query object appropriate + for particular ``dbmodel_par`` property. + """ + return self.dbsession.query(self.dbmodel_par) + + @staticmethod + def get_item_form(): + """ + *Hook method*. Must return instance of :py:mod:`flask_wtf.FlaskForm` + appropriate for given item class. + """ + raise NotImplementedError() + + @staticmethod + def add_parent_to_item(item, parent): + """ + *Hook method*. Use given parent object for given item object. The actual + operation to realize this relationship is highly dependent on current + circumstance. It is up to the developer to perform correct set of actions + to implement parent - child relationship for particular object types. + """ + raise NotImplementedError() + + def dispatch_request(self, parent_id): # 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. + + This method will attempt to validate the submitted form and create new + instance of appropriate item from form data and finally store the item + into the database. + """ + parent = self.dbquery_par.filter(self.dbmodel_par.id == parent_id).one_or_none() + if not parent: + self.abort(404) + + if not self.authorize_item_action(item = parent): + self.abort(403) + + self.response_context.update( + parent_id = parent_id, + parent = parent + ) + + item = self.dbmodel() + form = self.get_item_form() + + cancel_response = self.check_action_cancel(form, parent = parent) + if cancel_response: + return cancel_response + + if form.validate_on_submit(): + form_data = form.data + form.populate_obj(item) + + self.do_before_action(item) + self.add_parent_to_item(item, parent) + + if form_data[vial.const.FORM_ACTION_SUBMIT]: + try: + self.dbsession.add(item) + self.dbsession.commit() + self.do_after_action(item) + + # Log the item creation into changelog. + self.changelog_log(item, '', item.to_json()) + + self.flash( + flask.Markup(self.get_message_success(item = item, parent = parent)), + vial.const.FLASH_SUCCESS + ) + return self.redirect(default_url = self.get_url_next()) + + except Exception: # pylint: disable=locally-disabled,broad-except + self.dbsession.rollback() + self.flash( + flask.Markup(self.get_message_failure(parent = parent)), + vial.const.FLASH_FAILURE + ) + flask.current_app.log_exception_with_label( + traceback.TracebackException(*sys.exc_info()), + self.get_message_failure(parent = parent) + ) + return self.redirect(default_url = self.get_url_next()) + + self.response_context.update( + action_name = gettext('Create'), + form_url = flask.url_for('{}.{}'.format(self.module_name, self.get_view_name()), parent_id = parent_id), + form = form, + item_action = vial.const.ACTION_ITEM_CREATEFOR, + item = item + ) + + self.do_before_response() + return self.generate_response() + + +class ItemUpdateView(ItemActionView): # pylint: disable=locally-disabled,abstract-method + """ + Base class for item *update* action views. These views update existing items + in database. + """ + + @classmethod + def get_view_name(cls): + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" + return 'update' + + @classmethod + def get_view_template(cls): + """ + Return Jinja2 template file that should be used for rendering the view + content. This default implementation works only in case the view class + was properly registered into the parent blueprint/module with + :py:func:`vial.app.VialBlueprint.register_view_class` method. + + :return: Title for the view. + :rtype: str + """ + if cls.module_name: + return '{}/creatupdate.html'.format(cls.module_name) + raise RuntimeError("Unable to guess default view template, because module name was not yet set.") + + @classmethod + def get_view_title(cls, **kwargs): + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" + return gettext('Update') + + @classmethod + def get_menu_title(cls, **kwargs): + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" + return gettext('Update') + + @staticmethod + def get_item_form(item): + """ + *Hook method*. Must return instance of :py:mod:`flask_wtf.FlaskForm` + appropriate for given item class. + """ + raise NotImplementedError() + + def dispatch_request(self, item_id): # 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. + + This method will attempt to validate the submitted form and update the + instance of appropriate item from form data and finally store the item + back into the database. + """ + item = self.fetch(item_id) + if not item: + self.abort(404) + + if not self.authorize_item_action(item = item): + self.abort(403) + + self.dbsession.add(item) + + form = self.get_item_form(item) + + cancel_response = self.check_action_cancel(form, item = item) + if cancel_response: + return cancel_response + + item_json_before = item.to_json() + + if form.validate_on_submit(): + form_data = form.data + form.populate_obj(item) + + self.do_before_action(item) + + if form_data[vial.const.FORM_ACTION_SUBMIT]: + try: + if item not in self.dbsession.dirty: + self.flash( + gettext('No changes detected, no update needed.'), + vial.const.FLASH_INFO + ) + return self.redirect(default_url = self.get_url_next()) + + self.dbsession.commit() + self.do_after_action(item) + + # Log the item update into changelog. + self.changelog_log(item, item_json_before, item.to_json()) + + self.flash( + flask.Markup(self.get_message_success(item = item)), + vial.const.FLASH_SUCCESS + ) + return self.redirect(default_url = self.get_url_next()) + + except Exception: # pylint: disable=locally-disabled,broad-except + self.dbsession.rollback() + self.flash( + flask.Markup(self.get_message_failure(item = item)), + vial.const.FLASH_FAILURE + ) + flask.current_app.log_exception_with_label( + traceback.TracebackException(*sys.exc_info()), + self.get_message_failure(item = item) + ) + return self.redirect(default_url = self.get_url_next()) + + self.response_context.update( + action_name = gettext('Update'), + form_url = flask.url_for(self.get_view_endpoint(), item_id = item_id), + form = form, + item_action = vial.const.ACTION_ITEM_UPDATE, + item_id = item_id, + item = item + ) + + self.do_before_response() + return self.generate_response() + + +class ItemDeleteView(ItemActionView): # pylint: disable=locally-disabled,abstract-method + """ + Base class for item *delete* action views. These views delete existing items + from database. + """ + + @classmethod + def get_view_name(cls): + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" + return 'delete' + + @classmethod + def get_view_title(cls, **kwargs): + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" + return gettext('Delete') + + @classmethod + def get_menu_title(cls, **kwargs): + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" + return gettext('Delete') + + def dispatch_request(self, item_id): # 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. + + This method will attempt to validate the submitted form and delete the + instance of appropriate item from database in case user agreed to the + item removal action. + """ + item = self.fetch(item_id) + if not item: + self.abort(404) + + if not self.authorize_item_action(item = item): + self.abort(403) + + form = ItemActionConfirmForm() + + cancel_response = self.check_action_cancel(form, item = item) + if cancel_response: + return cancel_response + + item_json_before = item.to_json() + + if form.validate_on_submit(): + form_data = form.data + + self.do_before_action(item) + + if form_data[vial.const.FORM_ACTION_SUBMIT]: + try: + self.dbsession.delete(item) + self.dbsession.commit() + self.do_after_action(item) + + # Log the item deletion into changelog. + self.changelog_log(item, item_json_before, '') + + self.flash( + flask.Markup(self.get_message_success(item = item)), + vial.const.FLASH_SUCCESS + ) + return self.redirect( + default_url = self.get_url_next(), + exclude_url = flask.url_for( + '{}.{}'.format(self.module_name, 'show'), + item_id = item.id + ) + ) + + except Exception: # pylint: disable=locally-disabled,broad-except + self.dbsession.rollback() + self.flash( + flask.Markup(self.get_message_failure(item = item)), + vial.const.FLASH_FAILURE + ) + flask.current_app.log_exception_with_label( + traceback.TracebackException(*sys.exc_info()), + self.get_message_failure(item = item) + ) + return self.redirect(default_url = self.get_url_next()) + + self.response_context.update( + confirm_form = form, + confirm_url = flask.url_for(self.get_view_endpoint(), item_id = item_id), + item_name = str(item), + item_id = item_id, + item = item + ) + + self.do_before_response() + return self.generate_response() + + +class ItemChangeView(ItemActionView): # pylint: disable=locally-disabled,abstract-method + """ + Base class for single item change views, that are doing some simple modification + of item attribute, like enable/disable item, etc. + """ + + @classmethod + def validate_item_change(cls, **kwargs): # pylint: disable=locally-disabled,unused-argument + """ + Perform validation of particular change to given item. + """ + return True + + @classmethod + def change_item(cls, **kwargs): + """ + *Hook method*: Change given item in any desired way. + + :param item: Item to be changed/modified. + """ + raise NotImplementedError() + + def dispatch_request(self, item_id): # 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. + + This method will attempt to validate the submitted form, then perform + arbitrary mangling action with the item and submit the changes to the + database. + """ + item = self.fetch(item_id) + if not item: + self.abort(404) + + if not self.authorize_item_action(item = item): + self.abort(403) + + if not self.validate_item_change(item = item): + self.abort(400) + + form = ItemActionConfirmForm() + + cancel_response = self.check_action_cancel(form, item = item) + if cancel_response: + return cancel_response + + item_json_before = item.to_json() + + if form.validate_on_submit(): + form_data = form.data + + self.do_before_action(item) + + if form_data[vial.const.FORM_ACTION_SUBMIT]: + try: + self.change_item(item = item) + if item not in self.dbsession.dirty: + self.flash( + gettext('No changes detected, no update needed.'), + vial.const.FLASH_INFO + ) + return self.redirect(default_url = self.get_url_next()) + + self.dbsession.commit() + self.do_after_action(item) + + # Log the item change into changelog. + self.changelog_log(item, item_json_before, item.to_json()) + + self.flash( + flask.Markup(self.get_message_success(item = item)), + vial.const.FLASH_SUCCESS + ) + try: + exclude_url = flask.url_for( + '{}.{}'.format(self.module_name, 'show'), + item_id = item.id + ) + except werkzeug.routing.BuildError: + exclude_url = None + return self.redirect( + default_url = self.get_url_next(), + exclude_url = exclude_url + ) + + except Exception: # pylint: disable=locally-disabled,broad-except + self.dbsession.rollback() + self.flash( + flask.Markup(self.get_message_failure(item = item)), + vial.const.FLASH_FAILURE + ) + flask.current_app.log_exception_with_label( + traceback.TracebackException(*sys.exc_info()), + self.get_message_failure(item = item) + ) + return self.redirect(default_url = self.get_url_next()) + + self.response_context.update( + confirm_form = form, + confirm_url = flask.url_for(self.get_view_endpoint(), item_id = item_id), + item_name = str(item), + item_id = item_id, + item = item + ) + + self.do_before_response() + return self.generate_response() + + +class ItemDisableView(ItemChangeView): # pylint: disable=locally-disabled,abstract-method + """ + Base class for item disabling views. + """ + + @classmethod + def get_view_name(cls): + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" + return 'disable' + + @classmethod + def get_view_title(cls, **kwargs): + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" + return gettext('Disable') + + @classmethod + def get_menu_title(cls, **kwargs): + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" + return gettext('Disable') + + @classmethod + def validate_item_change(cls, **kwargs): # pylint: disable=locally-disabled,unused-argument + """*Implementation* of :py:func:`vial.view.ItemChangeView.validate_item_change`.""" + # Reject item change in case given item is already disabled. + if not kwargs['item'].enabled: + return False + return True + + @classmethod + def change_item(cls, **kwargs): + """ + *Implementation* of :py:func:`vial.view.ItemChangeView.change_item`. + """ + kwargs['item'].enabled = False + + +class ItemEnableView(ItemChangeView): # pylint: disable=locally-disabled,abstract-method + """ + Base class for item enabling views. + """ + + @classmethod + def get_view_name(cls): + """*Implementation* of :py:func:`vial.view.BaseView.get_view_name`.""" + return 'enable' + + @classmethod + def get_view_title(cls, **kwargs): + """*Implementation* of :py:func:`vial.view.BaseView.get_view_title`.""" + return gettext('Enable') + + @classmethod + def get_menu_title(cls, **kwargs): + """*Implementation* of :py:func:`vial.view.BaseView.get_menu_title`.""" + return gettext('Enable') + + @classmethod + def validate_item_change(cls, **kwargs): # pylint: disable=locally-disabled,unused-argument + """*Implementation* of :py:func:`vial.view.ItemChangeView.validate_item_change`.""" + # Reject item change in case given item is already enabled. + if kwargs['item'].enabled: + return False + return True + + @classmethod + def change_item(cls, **kwargs): + """ + *Implementation* of :py:func:`vial.view.ItemChangeView.change_item`. + """ + kwargs['item'].enabled = True + + +class ItemObjectRelationView(ItemChangeView): # pylint: disable=locally-disabled,abstract-method + """ + Base class for item object relation action views. + """ + @classmethod + def get_view_icon(cls): + """*Implementation* of :py:func:`vial.view.BaseView.get_view_icon`.""" + return 'module-{}'.format(cls.module_name) + + @classmethod + def get_view_template(cls): + """ + Return Jinja2 template file that should be used for rendering the view + content. This default implementation works only in case the view class + was properly registered into the parent blueprint/module with + :py:func:`vial.app.VialBlueprint.register_view_class` method. + + :return: Title for the view. + :rtype: str + """ + if cls.module_name: + return '{}/{}.html'.format(cls.module_name, cls.get_view_name()) + raise RuntimeError("Unable to guess default view template, because module name was not yet set.") + + @classmethod + def get_view_url(cls, **kwargs): + """*Implementation* of :py:func:`vial.view.BaseView.get_view_url`.""" + return flask.url_for( + cls.get_view_endpoint(), + item_id = kwargs['item'].get_id(), + other_id = kwargs['other'].get_id() + ) + + @property + def dbmodel_other(self): + """ + *Hook property*. This property must be implemented in each subclass to + return reference to appropriate model class for other objects and that + is based on *SQLAlchemy* declarative base. + """ + raise NotImplementedError() + + @property + def dbquery_other(self): + """ + This property contains the reference to *SQLAlchemy* query object appropriate + for particular ``dbmodel_other`` property. + """ + return self.dbsession.query(self.dbmodel_other) + + def dispatch_request(self, item_id, other_id): # 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. + + This method will attempt to validate the submitted form and create new + instance of appropriate item from form data and finally store the item + into the database. + """ + item = self.fetch(item_id) + if not item: + self.abort(404) + other = self.dbquery_other.filter(self.dbmodel_other.id == other_id).first() + if not other: + self.abort(404) + + if not self.authorize_item_action(item = item, other = other): + self.abort(403) + + if not self.validate_item_change(item = item, other = other): + self.abort(400) + + form = ItemActionConfirmForm() + + cancel_response = self.check_action_cancel(form, item = item, other = other) + if cancel_response: + return cancel_response + + item_json_before = item.to_json() + + if form.validate_on_submit(): + form_data = form.data + + self.do_before_action(item) + + if form_data[vial.const.FORM_ACTION_SUBMIT]: + try: + self.change_item(item = item, other = other) + if item not in self.dbsession.dirty: + self.flash( + gettext('No changes detected, no update needed.'), + vial.const.FLASH_INFO + ) + return self.redirect(default_url = self.get_url_next()) + + self.dbsession.commit() + self.do_after_action(item) + + # Log the item change into changelog. + self.changelog_log(item, item_json_before, item.to_json()) + + self.flash( + flask.Markup( + self.get_message_success( + item = item, + other = other + ) + ), + vial.const.FLASH_SUCCESS + ) + try: + exclude_url = flask.url_for( + '{}.{}'.format(self.module_name, 'show'), + item_id = item.id + ) + except werkzeug.routing.BuildError: + exclude_url = None + return self.redirect( + default_url = self.get_url_next(), + exclude_url = exclude_url + ) + + except Exception: # pylint: disable=locally-disabled,broad-except + self.dbsession.rollback() + self.flash( + flask.Markup( + self.get_message_failure(item = item, other = other) + ), + vial.const.FLASH_FAILURE + ) + flask.current_app.log_exception_with_label( + traceback.TracebackException(*sys.exc_info()), + self.get_message_failure(item = item, other = other) + ) + return self.redirect(default_url = self.get_url_next()) + + self.response_context.update( + confirm_form = form, + confirm_url = flask.url_for( + '{}.{}'.format( + self.module_name, + self.get_view_name() + ), + item_id = item_id, + other_id = other_id + ), + item_name = str(item), + item_id = item_id, + item = item, + other_name = str(other), + other_id = other_id, + other = other + ) + + self.do_before_response() + return self.generate_response() diff --git a/lib/vial/view/mixin.py b/lib/vial/view/mixin.py new file mode 100644 index 0000000000000000000000000000000000000000..c9a36fde0a3c3ba1cb1cfc7d254b6c75b5ae2cfc --- /dev/null +++ b/lib/vial/view/mixin.py @@ -0,0 +1,371 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +#------------------------------------------------------------------------------- +# Use of this source is governed by the MIT license, see LICENSE file. +#------------------------------------------------------------------------------- + + +""" +This module contains usefull view mixin classes for *Vial* application views. +""" + + +import datetime +import sqlalchemy + +import flask +import flask.app +import flask.views +from flask_babel import gettext + +import vial.const +import vial.menu +import vial.db +import vial.errors +from vial.forms import get_redirect_target + + +class VialUtils: + """ + Small utility method class to enable use of those methods both in the view + classes and in the Jinja2 templates. + """ + @staticmethod + def get_datetime_window(tiid, wtype, moment = None): + """ + Get timestamp of given type ('current', 'previous', 'next') for given time + window and optional time moment. + """ + try: + if not moment: + moment = datetime.datetime.utcnow() + return vial.const.TIME_WINDOWS[tiid][wtype](moment) + except: # pylint: disable=locally-disabled,bare-except + return None + + +class HTMLMixin: + """ + Mixin class enabling rendering responses as HTML. Use it in your custom view + classess based on :py:class:`vial.view.RenderableView` to provide the + ability to render Jinja2 template files into HTML documents. + """ + + @staticmethod + def abort(status_code, message = None): # pylint: disable=locally-disabled,unused-argument + """ + Abort request processing with ``flask.abort`` function and custom status + code and optional additional message. Return response as HTML error document. + """ + flask.abort(status_code, message) + + def flash(self, message, level = 'info'): # pylint: disable=locally-disabled,no-self-use + """ + Display a one time message to the user. This implementation uses the + :py:func:`flask.flash` method. + + :param str message: Message text. + :param str level: Severity level of the flash message. + """ + flask.flash(message, level) + + def redirect(self, target_url = None, default_url = None, exclude_url = None): # pylint: disable=locally-disabled,no-self-use + """ + Redirect user to different page. This implementation uses the + :py:func:`flask.redirect` method to return valid HTTP redirection response. + + :param str target_url: Explicit redirection target, if possible. + :param str default_url: Default redirection URL to use in case it cannot be autodetected from the response. + :param str exclude_url: URL to which to never redirect (for example never redirect back to the item detail after the item deletion). + """ + return flask.redirect( + get_redirect_target(target_url, default_url, exclude_url) + ) + + def generate_response(self, view_template = None): + """ + Generate the response appropriate for this view class, in this case HTML + page. + + :param str view_template: Override internally preconfigured page template. + """ + return flask.render_template( + view_template or self.get_view_template(), + **self.response_context + ) + + +class AJAXMixin: + """ + Mixin class enabling rendering responses as JSON documents. Use it in your + custom view classess based on based on :py:class:`vial.view.RenderableView` + to provide the ability to generate JSON responses. + """ + KW_RESP_VIEW_TITLE = 'view_title' + KW_RESP_VIEW_ICON = 'view_icon' + KW_RESP_FLASH_MESSAGES = 'flash_messages' + + @staticmethod + def abort(status_code, message = None): + """ + Abort request processing with ``flask.abort`` function and custom status + code and optional additional message. Return response as JSON document. + """ + flask.abort( + vial.errors.api_error_response( + status_code, + message + ) + ) + + def flash(self, message, level = 'info'): # pylint: disable=locally-disabled,no-self-use + """ + Display a one time message to the user. This implementation uses the + ``flash_messages`` subkey in returned JSON document to store the messages. + + :param str message: Message text. + :param str level: Severity level of the flash message. + """ + self.response_context.\ + setdefault(self.KW_RESP_FLASH_MESSAGES, {}).\ + setdefault(level, []).\ + append(message) + + def redirect(self, target_url = None, default_url = None, exclude_url = None): + """ + Redirect user to different page. This implementation stores the redirection + target to the JSON response. + + :param str target_url: Explicit redirection target, if possible. + :param str default_url: Default redirection URL to use in case it cannot be autodetected from the response. + :param str exclude_url: URL to which to never redirect (for example never redirect back to the item detail after the item deletion). + """ + self.response_context.update( + redirect = get_redirect_target( + target_url, + default_url, + exclude_url + ) + ) + self.process_response_context() + return flask.jsonify(self.response_context) + + def process_response_context(self): + """ + Perform additional mangling with the response context before generating + the response. This method can be useful to delete some context keys, that + should not leave the server. + + :return: Possibly updated response context. + :rtype: dict + """ + self.response_context[self.KW_RESP_VIEW_TITLE] = self.get_view_title() + self.response_context[self.KW_RESP_VIEW_ICON] = self.get_view_icon() + + flashed_messages = flask.get_flashed_messages(with_categories = True) + if flashed_messages: + for category, message in flashed_messages: + self.response_context.\ + setdefault(self.KW_RESP_FLASH_MESSAGES, {}).\ + setdefault(category, []).\ + append(message) + + # Prevent certain response context keys to appear in final response. + for key in ('search_form', 'item_form'): + try: + del self.response_context[key] + except KeyError: + pass + + return self.response_context + + def generate_response(self, view_template = None): # pylint: disable=locally-disabled,unused-argument + """ + Generate the response appropriate for this view class, in this case JSON + document. + + :param str view_template: Override internally preconfigured page template. + """ + self.process_response_context() + return flask.jsonify(self.response_context) + + +class SnippetMixin(AJAXMixin): + """ + Mixin class enabling rendering responses as JSON documents. Use it in your + custom view classess based on based on :py:class:`vial.view.RenderableView` + to provide the ability to generate JSON responses. + """ + KW_RESP_SNIPPETS = 'snippets' + KW_RESP_RENDER = '_render' + + renders = [] + snippets = [] + + def _render_snippet(self, snippet, snippet_file = None): + if 'condition' in snippet and not snippet['condition'](self.response_context): + return + + if not 'file' in snippet: + snippet['file'] = '{mod}/spt_{rdr}_{spt}.html'.format( + mod = self.module_name, + rdr = self.response_context[self.KW_RESP_RENDER], + spt = snippet['name'] + ) + if snippet_file: + snippet['file'] = snippet_file + + self.response_context.setdefault( + self.KW_RESP_SNIPPETS, + {} + )[snippet['name']] = flask.render_template( + snippet['file'], + **self.response_context + ) + + def flash(self, message, level = 'info'): # pylint: disable=locally-disabled,no-self-use + """ + Display a one time message to the user. This implementation uses the + ``flash_messages`` subkey in returned JSON document to store the messages. + + :param str message: Message text. + :param str level: Severity level of the flash message. + """ + self.response_context.\ + setdefault(self.KW_RESP_SNIPPETS, {}).\ + setdefault(self.KW_RESP_FLASH_MESSAGES, {}).\ + setdefault(level, []).\ + append( + flask.render_template( + 'spt_flashmessage.html', + level = level, + message = message + ) + ) + + def process_response_context(self): + """ + Reimplementation of :py:func:`vial.view.mixin.AJAXMixin.process_response_context`. + """ + self.response_context[self.KW_RESP_VIEW_TITLE] = self.get_view_title() + self.response_context[self.KW_RESP_VIEW_ICON] = self.get_view_icon() + self.response_context[self.KW_RESP_RENDER] = flask.request.args.get( + 'render', + self.renders[0] + ) or self.renders[0] + if self.response_context[self.KW_RESP_RENDER] not in self.renders: + self.abort( + 400, + gettext( + 'Invalid value %(val)s for snippet rendering parameter.', + val = self.response_context[self.KW_RESP_RENDER] + ) + ) + + flashed_messages = flask.get_flashed_messages(with_categories = True) + if flashed_messages: + for category, message in flashed_messages: + self.response_context.\ + setdefault(self.KW_RESP_SNIPPETS, {}).\ + setdefault(self.KW_RESP_FLASH_MESSAGES, {}).\ + setdefault(category, []).\ + append( + flask.render_template( + 'spt_flashmessage.html', + level = category, + message = message + ) + ) + + for snippet in self.snippets: + self._render_snippet(snippet) + + # Prevent certain response context keys to appear in final response. + for key in ('search_form', 'item_form'): + try: + del self.response_context[key] + except KeyError: + pass + + return self.response_context + + +class SQLAlchemyMixin: + """ + Mixin class providing generic interface for interacting with SQL database + backend through SQLAlchemy library. + """ + + @property + def dbmodel(self): + """ + This property must be implemented in each subclass to return reference to + appropriate model class based on *SQLAlchemy* declarative base. + """ + raise NotImplementedError() + + @property + def search_by(self): + """ + Return model`s attribute (column) according to which to search for a single item. + """ + return self.dbmodel.id + + @property + def dbsession(self): + """ + This property contains the reference to current *SQLAlchemy* database session. + """ + return vial.db.db_get().session + + def dbquery(self, dbmodel = None): + """ + This property contains the reference to *SQLAlchemy* query object appropriate + for particular ``dbmodel`` property. + """ + return self.dbsession.query(dbmodel or self.dbmodel) + + def dbcolumn_min(self, dbcolumn): + """ + Find and return the minimal value for given table column. + """ + result = self.dbsession.query(sqlalchemy.func.min(dbcolumn)).one_or_none() + if result: + return result[0] + return None + + def dbcolumn_max(self, dbcolumn): + """ + Find and return the maximal value for given table column. + """ + result = self.dbsession.query(sqlalchemy.func.max(dbcolumn)).one_or_none() + if result: + return result[0] + return None + + @staticmethod + def build_query(query, model, form_args): # pylint: disable=locally-disabled,unused-argument + """ + *Hook method*. Modify given query according to the given arguments. + """ + return query + + def fetch(self, item_id): + """ + Fetch item with given primary identifier from the database. + """ + return self.dbquery().filter(self.search_by == item_id).first() + + def search(self, form_args): + """ + Perform actual search with given query. + """ + query = self.build_query(self.dbquery(), self.dbmodel, form_args) + + # Adjust the query according to the paging parameters. + if 'limit' in form_args and form_args['limit']: + query = query.limit(int(form_args['limit'])) + if 'page' in form_args and form_args['page'] and int(form_args['page']) > 1: + query = query.offset((int(form_args['page']) - 1) * int(form_args['limit'])) + + return query.all()