diff --git a/invenio_config_tuw/startup.py b/invenio_config_tuw/startup.py deleted file mode 100644 index d656ba5c7822077d112cb9f4aa09ad9427301af9..0000000000000000000000000000000000000000 --- a/invenio_config_tuw/startup.py +++ /dev/null @@ -1,219 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2022-2024 TU Wien. -# -# Invenio-Config-TUW is free software; you can redistribute it and/or modify -# it under the terms of the MIT License; see LICENSE file for more details. - -"""Invenio-Config-TUW hacks and overrides to be applied on application startup. - -This module provides a blueprint whose sole purpose is to execute some code exactly -once during application startup (via ``bp.record_once()``). -These functions will be executed after the Invenio modules' extensions have been -initialized, and thus we can rely on them being already available. -""" - -from logging import ERROR -from logging.handlers import SMTPHandler - -import importlib_metadata -from flask.config import Config -from invenio_rdm_records.services.search_params import MyDraftsParam -from invenio_requests.proxies import current_request_type_registry - -from .curations import TUWCurationRequest -from .logs import DetailedFormatter - - -class TUWConfig(Config): - """Override for the Flask config that evaluates the SITE_{API,UI}_URL proxies.""" - - @classmethod - def from_flask_config(cls, config): - """Create a clone of the given config.""" - if isinstance(config, TUWConfig): - return config - - return cls(config.root_path, config) - - def __getitem__(self, key): - """Return config[key], or str(config[key]) if key is 'SITE_{UI,API}_URL'.""" - value = super().__getitem__(key) - - # give special treatment to the URL configuration items: - # enforce their evaluation as strings - if key in ("SITE_UI_URL", "SITE_API_URL"): - value = str(value) - - return value - - -def register_smtp_error_handler(app): - """Register email error handler to the application.""" - handler_name = "invenio-config-tuw-smtp-error-handler" - - # check reasons to skip handler registration - error_mail_disabled = app.config.get("CONFIG_TUW_DISABLE_ERROR_MAILS", False) - if app.debug or app.testing or error_mail_disabled: - # email error handling should occur only in production mode, if not disabled - return - - elif any([handler.name == handler_name for handler in app.logger.handlers]): - # we don't want to register duplicate handlers - return - - elif "invenio-mail" not in app.extensions: - app.logger.warning( - ( - "The Invenio-Mail extension is not loaded! " - "Skipping registration of SMTP error handler." - ) - ) - return - - # check if mail server and admin email(s) are present in the config - # if not raise a warning - if app.config.get("MAIL_SERVER") and app.config.get("MAIL_ADMIN"): - # configure auth - username = app.config.get("MAIL_USERNAME") - password = app.config.get("MAIL_PASSWORD") - auth = (username, password) if username and password else None - - # configure TLS - secure = None - if app.config.get("MAIL_USE_TLS"): - secure = () - - # initialize SMTP Handler - mail_handler = SMTPHandler( - mailhost=(app.config["MAIL_SERVER"], app.config.get("MAIL_PORT", 25)), - fromaddr=app.config["SECURITY_EMAIL_SENDER"], - toaddrs=app.config["MAIL_ADMIN"], - subject=app.config["THEME_SITENAME"] + " - Failure", - credentials=auth, - secure=secure, - ) - mail_handler.name = handler_name - mail_handler.setLevel(ERROR) - mail_handler.setFormatter(DetailedFormatter()) - - # attach to the application - app.logger.addHandler(mail_handler) - - else: - app.logger.warning( - "Mail configuration missing: SMTP error handler not registered!" - ) - - -def override_search_drafts_options(app): - """Override the "search drafts" options to show all accessible drafts.""" - # doing this via config is (currently) not possible, as the `search_drafts` - # property can't be overridden with a config item (unlike `search`, above it) - # cf. https://github.com/inveniosoftware/invenio-rdm-records/blob/maint-10.x/invenio_rdm_records/services/config.py#L327-L332 - try: - service = app.extensions["invenio-rdm-records"].records_service - service.config.search_drafts.params_interpreters_cls.remove(MyDraftsParam) - except ValueError: - pass - - -def register_menu_entries(app): - """Register the curation setting endpoint in Flask-Menu.""" - menu = app.extensions["menu"].root() - menu.submenu("settings.curation").register( - "invenio_config_tuw_settings.curation_settings_view", - '<i class="file icon"></i> Curation', - order=1, - ) - - -def customize_curation_request_type(app): - """Override the rdm-curations request type with our own version.""" - current_request_type_registry.register_type(TUWCurationRequest(), force=True) - - -def override_flask_config(app): - """Replace the app's config with our own override. - - This evaluates the ``LocalProxy`` objects used for ``SITE_{API,UI}_URL`` by - casting them into strings (which is their expected type). - """ - app.config = TUWConfig.from_flask_config(app.config) - - # we need to override the "config" global, as that might still be the - # old "normal" Flask config, and thus have different content - app.add_template_global(app.config, "config") - - -def override_prefixed_config(app): - """Override config items with their prefixed siblings' values. - - The prefix is determined via the config item "CONFIG_TUW_CONFIG_OVERRIDE_PREFIX". - Configuration items with this prefix will override the values for - - If the prefix is set to `None` (the default), then this feature will be disabled. - """ - prefix = app.config.get("CONFIG_TUW_CONFIG_OVERRIDE_PREFIX", None) - if prefix is None: - return - - prefix_len = len(prefix) - pairs = [(k, v) for k, v in app.config.items() if k.startswith(prefix)] - - for key, value in pairs: - key = key[prefix_len:] - app.config[key] = value - - -def patch_flask_create_url_adapter(app): - """Patch Flask's {host,subdomain}_matching with 3.1 behavior. - - See: https://github.com/pallets/flask/pull/5634 - - This can be removed once we get Flask 3.1+ in. - """ - flask_version = importlib_metadata.version("flask").split(".", 2) - major = int(flask_version[0]) - minor = int(flask_version[1]) - if (major == 3 and minor >= 1) or (major > 3): - app.logger.info( - f"Flask version is {flask_version} (>= 3.1). " - "Skipping monkey patching app.create_url_adapter()." - ) - return - - def create_url_adapter(self, request): - """Create a URL adapter for the given request. - - This function is from Flask 3.1, which is licensed under the 3-clause BSD. - """ - if request is not None: - subdomain = None - server_name = self.config["SERVER_NAME"] - - if self.url_map.host_matching: - # Don't pass SERVER_NAME, otherwise it's used and the actual - # host is ignored, which breaks host matching. - server_name = None - elif not self.subdomain_matching: - # Werkzeug doesn't implement subdomain matching yet. Until then, - # disable it by forcing the current subdomain to the default, or - # the empty string. - subdomain = self.url_map.default_subdomain or "" - - return self.url_map.bind_to_environ( - request.environ, server_name=server_name, subdomain=subdomain - ) - - # Need at least SERVER_NAME to match/build outside a request. - if self.config["SERVER_NAME"] is not None: - return self.url_map.bind( - self.config["SERVER_NAME"], - script_name=self.config["APPLICATION_ROOT"], - url_scheme=self.config["PREFERRED_URL_SCHEME"], - ) - - return None - - app.create_url_adapter = create_url_adapter.__get__(app, type(app)) diff --git a/invenio_config_tuw/startup/__init__.py b/invenio_config_tuw/startup/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..d25e9f3b5c77cee36419c7ac7cc3a45527beaf7e --- /dev/null +++ b/invenio_config_tuw/startup/__init__.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2025 TU Wien. +# +# Invenio-Config-TUW is free software; you can redistribute it and/or modify +# it under the terms of the MIT License; see LICENSE file for more details. + +"""Invenio-Config-TUW hacks and overrides to be applied on application startup. + +This module provides a blueprint whose sole purpose is to execute some code exactly +once during application startup (via ``bp.record_once()``). +These functions will be executed after the Invenio modules' extensions have been +initialized, and thus we can rely on them being already available. +""" + + +from .config import finalize_config, patch_flask_create_url_adapter +from .misc import ( + customize_curation_request_type, + override_search_drafts_options, + register_menu_entries, + register_smtp_error_handler, +) + +__all__ = ( + "customize_curation_request_type", + "finalize_config", + "override_search_drafts_options", + "patch_flask_create_url_adapter", + "register_menu_entries", + "register_smtp_error_handler", +) diff --git a/invenio_config_tuw/startup/config.py b/invenio_config_tuw/startup/config.py new file mode 100644 index 0000000000000000000000000000000000000000..90781d8bc045b8294f3adf9f209a8172eef14056 --- /dev/null +++ b/invenio_config_tuw/startup/config.py @@ -0,0 +1,359 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2022-2025 TU Wien. +# +# Invenio-Config-TUW is free software; you can redistribute it and/or modify +# it under the terms of the MIT License; see LICENSE file for more details. + +"""Invenio-Config-TUW hacks and overrides to be applied on application startup. + +This module provides a blueprint whose sole purpose is to execute some code exactly +once during application startup (via ``bp.record_once()``). +These functions will be executed after the Invenio modules' extensions have been +initialized, and thus we can rely on them being already available. +""" + +from functools import partial + +import importlib_metadata +from flask import current_app, request +from flask.config import Config +from werkzeug.local import LocalProxy + +from ..auth.settings import TUWSSOSettingsHelper + + +class TUWConfig(Config): + """Override for the Flask config that evaluates the SITE_{API,UI}_URL proxies.""" + + @classmethod + def from_flask_config(cls, config): + """Create a clone of the given config.""" + if isinstance(config, TUWConfig): + return config + + return cls(config.root_path, config) + + def __getitem__(self, key): + """Return config[key], or str(config[key]) if key is 'SITE_{UI,API}_URL'.""" + value = super().__getitem__(key) + + # give special treatment to the URL configuration items: + # enforce their evaluation as strings + if key in ("SITE_UI_URL", "SITE_API_URL"): + value = str(value) + + return value + + +def _get_config(app, key, prefixes=None, default=None): + """Get the config item, preferably with the longest matching prefix.""" + prefixes = [p for p in (prefixes or []) if p is not None] + for prefix in sorted(prefixes, key=len, reverse=True): + prefixed_key = prefix + key + if prefixed_key in app.config: + return app.config[prefixed_key] + + if key in app.config: + return app.config[key] + else: + return default + + +def _make_site_url(suffix): + """Create a URL with the given suffix from contextual information. + + If available, use the request's Host URL as base for the URL. + Otherwise, look at the configuration value for `THEME_SITEURL`. + """ + url = None + + try: + if request and request.host_url and request.host_url.startswith("http"): + url = request.host_url + + except RuntimeError: + # this will be hit if we're working outside of a request context + pass + + # use THEME_SITEURL or relative URLs as fallback + if url is None: + url = current_app.config.get("THEME_SITEURL", "") + + # do a little dance to make sure there's no extra slashes + return (url.rstrip("/") + "/" + suffix.lstrip("/")).rstrip("/") + + +def assemble_db_uri_from_parts(app): + """Assemble the DB connection string from its parts.""" + prefixes = ["SQLALCHEMY_"] + db_uri = _get_config(app, "DATABASE_URI", prefixes) + + db_driver = _get_config( + app, "DATABASE_DRIVER", prefixes, default="postgresql+psycopg2" + ) + db_user = _get_config(app, "DATABASE_USER", prefixes) + db_pw = _get_config(app, "DATABASE_PASSWORD", prefixes) + db_host = _get_config(app, "DATABASE_HOST", prefixes, default="localhost") + db_db = _get_config(app, "DATABASE_DB", prefixes, default=db_user) + + if all((v is not None for v in [db_driver, db_user, db_pw, db_host, db_db])): + db_uri = f"{db_driver}://{db_user}:{db_pw}@{db_host}/{db_db}" + + if db_uri is not None: + app.config["SQLALCHEMY_DATABASE_URI"] = db_uri + else: + app.logger.warn("Warning: No DB conection string set") + + +def assemble_broker_uri_from_parts(app): + """Assemble the broker URI from its parts.""" + rabbitmq_user = _get_config(app, "RABBITMQ_USER") + rabbitmq_password = _get_config(app, "RABBITMQ_PASSWORD") + broker_url = _get_config(app, "BROKER_URL") + broker_host = _get_config(app, "BROKER_HOST", default="localhost") + broker_protocol = _get_config(app, "BROKER_PROTOCOL", default="amqp") + broker_user = _get_config(app, "BROKER_USER", default=rabbitmq_user) + broker_password = _get_config(app, "BROKER_PASSWORD", default=rabbitmq_password) + + if broker_url is None: + if None not in [broker_protocol, broker_user, broker_password, broker_host]: + broker_url = ( + f"{broker_protocol}://{broker_user}:{broker_password}@{broker_host}/" + ) + else: + broker_url = "amqp://guest:guest@localhost:5672/" + + app.config.setdefault("BROKER_URL", broker_url) + app.config.setdefault("CELERY_BROKER_URL", broker_url) + + +def assemble_cache_uri_from_parts(app): + """Assemble the various cache URIs from their parts.""" + redis_user = _get_config(app, "CACHE_REDIS_USER") + redis_password = _get_config(app, "CACHE_REDIS_PASSWORD") + redis_host = _get_config(app, "CACHE_REDIS_HOST", default="localhost") + redis_port = _get_config(app, "CACHE_REDIS_PORT", default="6379") + redis_protocol = _get_config(app, "CACHE_REDIS_PROTOCOL", default="redis") + redis_db = _get_config(app, "CACHE_REDIS_DB", default="0") + + # set the redis database names that should be used for various parts + account_sessions_db = _get_config(app, "ACCOUNTS_SESSION_REDIS_DB", default="1") + celery_results_db = _get_config(app, "CELERY_RESULT_BACKEND_DB", default="2") + ratelimit_storage_db = _get_config(app, "RATELIMIT_STORAGE_DB", default="3") + communities_identities_db = _get_config( + app, "COMMUNITIES_IDENTITIES_STORAGE_DB", default="4" + ) + + if redis_user is None and redis_password is not None: + # the default user in redis is named 'default' + redis_user = "default" + + def _make_redis_url(db): + """Create redis URL from the given DB name.""" + if redis_password is not None: + return f"{redis_protocol}://{redis_user}:{redis_password}@{redis_host}:{redis_port}/{db}" + else: + return f"{redis_protocol}://{redis_host}:{redis_port}/{db}" + + cache_redis_url = _make_redis_url(redis_db) + accounts_session_redis_url = _make_redis_url(account_sessions_db) + celery_results_backend_url = _make_redis_url(celery_results_db) + ratelimit_storage_url = _make_redis_url(ratelimit_storage_db) + communities_identities_cache_url = _make_redis_url(communities_identities_db) + + app.config.setdefault("CACHE_TYPE", "redis") + app.config.setdefault("CACHE_REDIS_URL", cache_redis_url) + app.config.setdefault("IIIF_CACHE_REDIS_URL", cache_redis_url) + app.config.setdefault("ACCOUNTS_SESSION_REDIS_URL", accounts_session_redis_url) + app.config.setdefault("CELERY_RESULT_BACKEND", celery_results_backend_url) + app.config.setdefault("RATELIMIT_STORAGE_URL", ratelimit_storage_url) + app.config.setdefault( + "COMMUNITIES_IDENTITIES_CACHE_REDIS_URL", communities_identities_cache_url + ) + + +def assemble_site_urls_from_parts(app): + """Create `LocalProxy` objects for the `SITE_{API,UI}_URL` items.""" + # prefer the `SERVER_NAME` config item to build URLs + server_name = _get_config(app, "SERVER_NAME") + theme_siteurl = _get_config( + app, "THEME_SITEURL", default=f"https://{server_name or 'localhost'}" + ) + if server_name: + hostname = server_name + else: + hostname = theme_siteurl.lstrip("http://").lstrip("https://").split("/")[0] + + # note: 'invenio-cli run' likes to populate INVENIO_SITE_{UI,API}_URL... + app.config["SITE_UI_URL"] = LocalProxy(partial(_make_site_url, "")) + app.config["SITE_API_URL"] = LocalProxy(partial(_make_site_url, "/api")) + app.config.setdefault("OAISERVER_ID_PREFIX", hostname) + + +def assemble_keycloak_config_from_parts(app): + """Assemble the Keycloak remote app from its parts.""" + consumer_key = app.config.get("OAUTHCLIENT_KEYCLOAK_CONSUMER_KEY") + consumer_secret = app.config.get("OAUTHCLIENT_KEYCLOAK_CONSUMER_SECRET") + + if consumer_key is not None and consumer_secret is not None: + app_credentials = { + "consumer_key": consumer_key, + "consumer_secret": consumer_secret, + } + app.config.setdefault("OAUTHCLIENT_KEYCLOAK_APP_CREDENTIALS", app_credentials) + app.config.setdefault("KEYCLOAK_APP_CREDENTIALS", app_credentials) + + base_url = app.config.get("OAUTHCLIENT_KEYCLOAK_BASE_URL") + realm = app.config.get("OAUTHCLIENT_KEYCLOAK_REALM") + app_title = app.config.get("OAUTHCLIENT_KEYCLOAK_APP_TITLE") + app_description = app.config.get("OAUTHCLIENT_KEYCLOAK_APP_DESCRIPTION") + + if base_url is not None and realm is not None: + helper = TUWSSOSettingsHelper( + title=app_title or "TU Wien SSO", + description=app_description or "TU Wien Single Sign-On", + base_url=base_url, + realm=realm, + ) + remote_app = helper.remote_app + + if app_title is not None: + remote_app["title"] = app_title + + # ensure that this remote app is listed as "keycloak" in the app config + remote_apps = { + **(app.config.get("OAUTHCLIENT_REMOTE_APPS") or {}), + "keycloak": remote_app, + } + app.config.setdefault("OAUTHCLIENT_KEYCLOAK_REALM_URL", helper.realm_url) + app.config.setdefault( + "OAUTHCLIENT_KEYCLOAK_USER_INFO_URL", helper.user_info_url + ) + app.config["OAUTHCLIENT_REMOTE_APPS"] = remote_apps + + +def populate_unset_salt_values(app): + """Populate the salt values if they're not set yet.""" + secret_key = app.config.get("SECRET_KEY", None) + if secret_key is None: + raise RuntimeError("SECRET_KEY configuration is unset! Aborting.") + + app.config.setdefault("CSRF_SECRET_SALT", secret_key) + app.config.setdefault("SECURITY_RESET_SALT", secret_key) + app.config.setdefault("SECURITY_LOGIN_SALT", secret_key) + app.config.setdefault("SECURITY_PASSWORD_SALT", secret_key) + app.config.setdefault("SECURITY_CONFIRM_SALT", secret_key) + app.config.setdefault("SECURITY_CHANGE_SALT", secret_key) + app.config.setdefault("SECURITY_REMEMBER_SALT", secret_key) + + +def assemble_and_populate_config(app): + """Assemble some config from their parts and populate some unset config.""" + assemble_db_uri_from_parts(app) + assemble_broker_uri_from_parts(app) + assemble_cache_uri_from_parts(app) + assemble_site_urls_from_parts(app) + assemble_keycloak_config_from_parts(app) + populate_unset_salt_values(app) + + +def override_prefixed_config(app): + """Override config items with their prefixed siblings' values. + + The prefix is determined via the config item "CONFIG_TUW_CONFIG_OVERRIDE_PREFIX". + Configuration items with this prefix will override the values for + + If the prefix is set to `None` (the default), then this feature will be disabled. + """ + prefix = app.config.get("CONFIG_TUW_CONFIG_OVERRIDE_PREFIX", None) + if prefix is None: + return + + prefix_len = len(prefix) + pairs = [(k, v) for k, v in app.config.items() if k.startswith(prefix)] + + for key, value in pairs: + key = key[prefix_len:] + app.config[key] = value + + +def override_flask_config(app): + """Replace the app's config with our own override. + + This evaluates the ``LocalProxy`` objects used for ``SITE_{API,UI}_URL`` by + casting them into strings (which is their expected type). + """ + app.config = TUWConfig.from_flask_config(app.config) + + # we need to override the "config" global, as that might still be the + # old "normal" Flask config, and thus have different content + app.add_template_global(app.config, "config") + + +def finalize_config(app): + """Finalize the application configuration with a few tweaks. + + First, override the Flask configuration with the ``TUWConfig`` class. + Then, use prefixed configuration items to override non-prefixed ones + if a prefix is configured. + Lastly, assemble some configuration items from their parts (like building + the DB connection string from its pieces), and populate some unset + values like salts. + """ + override_flask_config(app) + override_prefixed_config(app) + assemble_and_populate_config(app) + + +def patch_flask_create_url_adapter(app): + """Patch Flask's {host,subdomain}_matching with 3.1 behavior. + + See: https://github.com/pallets/flask/pull/5634 + + This can be removed once we get Flask 3.1+ in. + """ + flask_version = importlib_metadata.version("flask").split(".", 2) + major = int(flask_version[0]) + minor = int(flask_version[1]) + if (major == 3 and minor >= 1) or (major > 3): + app.logger.info( + f"Flask version is {flask_version} (>= 3.1). " + "Skipping monkey patching app.create_url_adapter()." + ) + return + + def create_url_adapter(self, request): + """Create a URL adapter for the given request. + + This function is from Flask 3.1, which is licensed under the 3-clause BSD. + """ + if request is not None: + subdomain = None + server_name = self.config["SERVER_NAME"] + + if self.url_map.host_matching: + # Don't pass SERVER_NAME, otherwise it's used and the actual + # host is ignored, which breaks host matching. + server_name = None + elif not self.subdomain_matching: + # Werkzeug doesn't implement subdomain matching yet. Until then, + # disable it by forcing the current subdomain to the default, or + # the empty string. + subdomain = self.url_map.default_subdomain or "" + + return self.url_map.bind_to_environ( + request.environ, server_name=server_name, subdomain=subdomain + ) + + # Need at least SERVER_NAME to match/build outside a request. + if self.config["SERVER_NAME"] is not None: + return self.url_map.bind( + self.config["SERVER_NAME"], + script_name=self.config["APPLICATION_ROOT"], + url_scheme=self.config["PREFERRED_URL_SCHEME"], + ) + + return None + + app.create_url_adapter = create_url_adapter.__get__(app, type(app)) diff --git a/invenio_config_tuw/startup/misc.py b/invenio_config_tuw/startup/misc.py new file mode 100644 index 0000000000000000000000000000000000000000..cec10ed137bebd04110d782401a0cc73586ac410 --- /dev/null +++ b/invenio_config_tuw/startup/misc.py @@ -0,0 +1,108 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2022-2025 TU Wien. +# +# Invenio-Config-TUW is free software; you can redistribute it and/or modify +# it under the terms of the MIT License; see LICENSE file for more details. + +"""Invenio-Config-TUW hacks and overrides to be applied on application startup. + +This module provides a blueprint whose sole purpose is to execute some code exactly +once during application startup (via ``bp.record_once()``). +These functions will be executed after the Invenio modules' extensions have been +initialized, and thus we can rely on them being already available. +""" + +from logging import ERROR +from logging.handlers import SMTPHandler + +from invenio_rdm_records.services.search_params import MyDraftsParam +from invenio_requests.proxies import current_request_type_registry + +from ..curations import TUWCurationRequest +from ..logs import DetailedFormatter + + +def register_smtp_error_handler(app): + """Register email error handler to the application.""" + handler_name = "invenio-config-tuw-smtp-error-handler" + + # check reasons to skip handler registration + error_mail_disabled = app.config.get("CONFIG_TUW_DISABLE_ERROR_MAILS", False) + if app.debug or app.testing or error_mail_disabled: + # email error handling should occur only in production mode, if not disabled + return + + elif any([handler.name == handler_name for handler in app.logger.handlers]): + # we don't want to register duplicate handlers + return + + elif "invenio-mail" not in app.extensions: + app.logger.warning( + ( + "The Invenio-Mail extension is not loaded! " + "Skipping registration of SMTP error handler." + ) + ) + return + + # check if mail server and admin email(s) are present in the config + # if not raise a warning + if app.config.get("MAIL_SERVER") and app.config.get("MAIL_ADMIN"): + # configure auth + username = app.config.get("MAIL_USERNAME") + password = app.config.get("MAIL_PASSWORD") + auth = (username, password) if username and password else None + + # configure TLS + secure = None + if app.config.get("MAIL_USE_TLS"): + secure = () + + # initialize SMTP Handler + mail_handler = SMTPHandler( + mailhost=(app.config["MAIL_SERVER"], app.config.get("MAIL_PORT", 25)), + fromaddr=app.config["SECURITY_EMAIL_SENDER"], + toaddrs=app.config["MAIL_ADMIN"], + subject=app.config["THEME_SITENAME"] + " - Failure", + credentials=auth, + secure=secure, + ) + mail_handler.name = handler_name + mail_handler.setLevel(ERROR) + mail_handler.setFormatter(DetailedFormatter()) + + # attach to the application + app.logger.addHandler(mail_handler) + + else: + app.logger.warning( + "Mail configuration missing: SMTP error handler not registered!" + ) + + +def override_search_drafts_options(app): + """Override the "search drafts" options to show all accessible drafts.""" + # doing this via config is (currently) not possible, as the `search_drafts` + # property can't be overridden with a config item (unlike `search`, above it) + # cf. https://github.com/inveniosoftware/invenio-rdm-records/blob/maint-10.x/invenio_rdm_records/services/config.py#L327-L332 + try: + service = app.extensions["invenio-rdm-records"].records_service + service.config.search_drafts.params_interpreters_cls.remove(MyDraftsParam) + except ValueError: + pass + + +def register_menu_entries(app): + """Register the curation setting endpoint in Flask-Menu.""" + menu = app.extensions["menu"].root() + menu.submenu("settings.curation").register( + "invenio_config_tuw_settings.curation_settings_view", + '<i class="file icon"></i> Curation', + order=1, + ) + + +def customize_curation_request_type(app): + """Override the rdm-curations request type with our own version.""" + current_request_type_registry.register_type(TUWCurationRequest(), force=True) diff --git a/pyproject.toml b/pyproject.toml index 426f619d797fecf81d98347e63f05b8d328bca5f..6cb4e70e10ba41abb24c7c53577af804305eac5d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,16 +81,14 @@ invenio_config_tuw_mail_handler = "invenio_config_tuw.startup:register_smtp_erro invenio_config_tuw_search_drafts = "invenio_config_tuw.startup:override_search_drafts_options" invenio_config_tuw_curation_settings = "invenio_config_tuw.startup:register_menu_entries" invenio_config_tuw_curation_request = "invenio_config_tuw.startup:customize_curation_request_type" -invenio_config_tuw_flask_config = "invenio_config_tuw.startup:override_flask_config" -invenio_config_tuw_override_prefixed_config = "invenio_config_tuw.startup:override_prefixed_config" +invenio_config_tuw_finalize_config = "invenio_config_tuw.startup:finalize_config" invenio_config_tuw_patch_flask = "invenio_config_tuw.startup:patch_flask_create_url_adapter" [project.entry-points."invenio_base.api_finalize_app"] invenio_config_tuw_mail_handler = "invenio_config_tuw.startup:register_smtp_error_handler" invenio_config_tuw_search_drafts = "invenio_config_tuw.startup:override_search_drafts_options" invenio_config_tuw_curation_request = "invenio_config_tuw.startup:customize_curation_request_type" -invenio_config_tuw_flask_config = "invenio_config_tuw.startup:override_flask_config" -invenio_config_tuw_override_prefixed_config = "invenio_config_tuw.startup:override_prefixed_config" +invenio_config_tuw_finalize_config = "invenio_config_tuw.startup:finalize_config" invenio_config_tuw_patch_flask = "invenio_config_tuw.startup:patch_flask_create_url_adapter" [project.entry-points."invenio_i18n.translations"]