This is an automated email from the ASF dual-hosted git repository. bbovenzi pushed a commit to branch revert-appbuilder-changes in repository https://gitbox.apache.org/repos/asf/airflow.git
commit 1d2bda0d10e8db0bdd9de0f7c587023ce5469023 Author: Brent Bovenzi <[email protected]> AuthorDate: Wed Nov 17 12:56:38 2021 -0600 Revert "Copy AppBuilder Base class verbatim from FAB to Airflow (with attribution) (#19322)" This reverts commit 37a12e9c278209d7e8ea914012a31a91a6c6ccff. --- airflow/www/extensions/init_appbuilder.py | 608 +----------------------------- 1 file changed, 11 insertions(+), 597 deletions(-) diff --git a/airflow/www/extensions/init_appbuilder.py b/airflow/www/extensions/init_appbuilder.py index 5c81f0c..036315b 100644 --- a/airflow/www/extensions/init_appbuilder.py +++ b/airflow/www/extensions/init_appbuilder.py @@ -15,607 +15,11 @@ # specific language governing permissions and limitations # under the License. -# This product contains a modified portion of 'Flask App Builder' developed by Daniel Vaz Gaspar. -# (https://github.com/dpgaspar/Flask-AppBuilder). -# Copyright 2013, Daniel Vaz Gaspar - - -import logging -from typing import Dict - -from flask import Blueprint, current_app, url_for -from flask_appbuilder import __version__ -from flask_appbuilder.api.manager import OpenApiManager -from flask_appbuilder.babel.manager import BabelManager -from flask_appbuilder.base import dynamic_class_import -from flask_appbuilder.const import ( - LOGMSG_ERR_FAB_ADD_PERMISSION_MENU, - LOGMSG_ERR_FAB_ADD_PERMISSION_VIEW, - LOGMSG_ERR_FAB_ADDON_PROCESS, - LOGMSG_INF_FAB_ADD_VIEW, - LOGMSG_INF_FAB_ADDON_ADDED, - LOGMSG_WAR_FAB_VIEW_EXISTS, -) -from flask_appbuilder.filters import TemplateFilters -from flask_appbuilder.menu import Menu, MenuApiManager -from flask_appbuilder.views import IndexView, UtilView +from flask_appbuilder import AppBuilder from airflow import settings from airflow.configuration import conf -log = logging.getLogger(__name__) - - -class AirflowAppBuilder: - """ - This is the base class for all the framework. - This is were you will register all your views - and create the menu structure. - Will hold your flask app object, all your views, and security classes. - initialize your application like this for SQLAlchemy:: - from flask import Flask - from flask_appbuilder import SQLA, AirflowAppBuilder - app = Flask(__name__) - app.config.from_object('config') - db = SQLA(app) - appbuilder = AirflowAppBuilder(app, db.session) - When using MongoEngine:: - from flask import Flask - from flask_appbuilder import AirflowAppBuilder - from flask_appbuilder.security.mongoengine.manager import SecurityManager - from flask_mongoengine import MongoEngine - app = Flask(__name__) - app.config.from_object('config') - dbmongo = MongoEngine(app) - appbuilder = AirflowAppBuilder(app, security_manager_class=SecurityManager) - You can also create everything as an application factory. - """ - - baseviews = [] - security_manager_class = None - # Flask app - app = None - # Database Session - session = None - # Security Manager Class - sm = None - # Babel Manager Class - bm = None - # OpenAPI Manager Class - openapi_manager = None - # dict with addon name has key and intantiated class has value - addon_managers = None - # temporary list that hold addon_managers config key - _addon_managers = None - - menu = None - indexview = None - - static_folder = None - static_url_path = None - - template_filters = None - - def __init__( - self, - app=None, - session=None, - menu=None, - indexview=None, - base_template="appbuilder/baselayout.html", - static_folder="static/appbuilder", - static_url_path="/appbuilder", - security_manager_class=None, - update_perms=True, - ): - """ - Class constructor - :param app: - The flask app object - :param session: - The SQLAlchemy session object - :param menu: - optional, a previous constructed menu - :param indexview: - optional, your customized indexview - :param static_folder: - optional, your override for the global static folder - :param static_url_path: - optional, your override for the global static url path - :param security_manager_class: - optional, pass your own security manager class - :param update_perms: - optional, update permissions flag (Boolean) you can use - FAB_UPDATE_PERMS config key also - """ - self.baseviews = [] - self._addon_managers = [] - self.addon_managers = {} - self.menu = menu - self.base_template = base_template - self.security_manager_class = security_manager_class - self.indexview = indexview - self.static_folder = static_folder - self.static_url_path = static_url_path - self.app = app - self.update_perms = update_perms - - if app is not None: - self.init_app(app, session) - - def init_app(self, app, session): - """ - Will initialize the Flask app, supporting the app factory pattern. - :param app: - :param session: The SQLAlchemy session - """ - app.config.setdefault("APP_NAME", "F.A.B.") - app.config.setdefault("APP_THEME", "") - app.config.setdefault("APP_ICON", "") - app.config.setdefault("LANGUAGES", {"en": {"flag": "gb", "name": "English"}}) - app.config.setdefault("ADDON_MANAGERS", []) - app.config.setdefault("FAB_API_MAX_PAGE_SIZE", 100) - app.config.setdefault("FAB_BASE_TEMPLATE", self.base_template) - app.config.setdefault("FAB_STATIC_FOLDER", self.static_folder) - app.config.setdefault("FAB_STATIC_URL_PATH", self.static_url_path) - - self.app = app - - self.base_template = app.config.get("FAB_BASE_TEMPLATE", self.base_template) - self.static_folder = app.config.get("FAB_STATIC_FOLDER", self.static_folder) - self.static_url_path = app.config.get("FAB_STATIC_URL_PATH", self.static_url_path) - _index_view = app.config.get("FAB_INDEX_VIEW", None) - if _index_view is not None: - self.indexview = dynamic_class_import(_index_view) - else: - self.indexview = self.indexview or IndexView - _menu = app.config.get("FAB_MENU", None) - if _menu is not None: - self.menu = dynamic_class_import(_menu) - else: - self.menu = self.menu or Menu() - - if self.update_perms: # default is True, if False takes precedence from config - self.update_perms = app.config.get("FAB_UPDATE_PERMS", True) - _security_manager_class_name = app.config.get("FAB_SECURITY_MANAGER_CLASS", None) - if _security_manager_class_name is not None: - self.security_manager_class = dynamic_class_import(_security_manager_class_name) - if self.security_manager_class is None: - from flask_appbuilder.security.sqla.manager import SecurityManager - - self.security_manager_class = SecurityManager - - self._addon_managers = app.config["ADDON_MANAGERS"] - self.session = session - self.sm = self.security_manager_class(self) - self.bm = BabelManager(self) - self.openapi_manager = OpenApiManager(self) - self.menuapi_manager = MenuApiManager(self) - self._add_global_static() - self._add_global_filters() - app.before_request(self.sm.before_request) - self._add_admin_views() - self._add_addon_views() - if self.app: - self._add_menu_permissions() - else: - self.post_init() - self._init_extension(app) - - def _init_extension(self, app): - app.appbuilder = self - if not hasattr(app, "extensions"): - app.extensions = {} - app.extensions["appbuilder"] = self - - def post_init(self): - for baseview in self.baseviews: - # instantiate the views and add session - self._check_and_init(baseview) - # Register the views has blueprints - if baseview.__class__.__name__ not in self.get_app.blueprints.keys(): - self.register_blueprint(baseview) - # Add missing permissions where needed - self.add_permissions() - - @property - def get_app(self): - """ - Get current or configured flask app - :return: Flask App - """ - if self.app: - return self.app - else: - return current_app - - @property - def get_session(self): - """ - Get the current sqlalchemy session. - :return: SQLAlchemy Session - """ - return self.session - - @property - def app_name(self): - """ - Get the App name - :return: String with app name - """ - return self.get_app.config["APP_NAME"] - - @property - def app_theme(self): - """ - Get the App theme name - :return: String app theme name - """ - return self.get_app.config["APP_THEME"] - - @property - def app_icon(self): - """ - Get the App icon location - :return: String with relative app icon location - """ - return self.get_app.config["APP_ICON"] - - @property - def languages(self): - return self.get_app.config["LANGUAGES"] - - @property - def version(self): - """ - Get the current F.A.B. version - :return: String with the current F.A.B. version - """ - return __version__ - - def _add_global_filters(self): - self.template_filters = TemplateFilters(self.get_app, self.sm) - - def _add_global_static(self): - bp = Blueprint( - "appbuilder", - __name__, - url_prefix="/static", - template_folder="templates", - static_folder=self.static_folder, - static_url_path=self.static_url_path, - ) - self.get_app.register_blueprint(bp) - - def _add_admin_views(self): - """Registers indexview, utilview (back function), babel views and Security views.""" - self.indexview = self._check_and_init(self.indexview) - self.add_view_no_menu(self.indexview) - self.add_view_no_menu(UtilView()) - self.bm.register_views() - self.sm.register_views() - self.openapi_manager.register_views() - self.menuapi_manager.register_views() - - def _add_addon_views(self): - """Registers declared addon's""" - for addon in self._addon_managers: - addon_class = dynamic_class_import(addon) - if addon_class: - # Instantiate manager with appbuilder (self) - addon_class = addon_class(self) - try: - addon_class.pre_process() - addon_class.register_views() - addon_class.post_process() - self.addon_managers[addon] = addon_class - log.info(LOGMSG_INF_FAB_ADDON_ADDED.format(str(addon))) - except Exception as e: - log.exception(e) - log.error(LOGMSG_ERR_FAB_ADDON_PROCESS.format(addon, e)) - - def _check_and_init(self, baseview): - if hasattr(baseview, 'datamodel'): - # Delete sessions if initiated previously to limit side effects. We want to use - # the current session in the current application. - baseview.datamodel.session = self.session - if hasattr(baseview, "__call__"): - baseview = baseview() - return baseview - - def add_view( - self, - baseview, - name, - href="", - icon="", - label="", - category="", - category_icon="", - category_label="", - menu_cond=None, - ): - """ - Add your views associated with menus using this method. - :param baseview: - A BaseView type class instantiated or not. - This method will instantiate the class for you if needed. - :param name: - The string name that identifies the menu. - :param href: - Override the generated href for the menu. - You can use an url string or an endpoint name - if non provided default_view from view will be set as href. - :param icon: - Font-Awesome icon name, optional. - :param label: - The label that will be displayed on the menu, - if absent param name will be used - :param category: - The menu category where the menu will be included, - if non provided the view will be accessible as a top menu. - :param category_icon: - Font-Awesome icon name for the category, optional. - :param category_label: - The label that will be displayed on the menu, - if absent param name will be used - :param menu_cond: - If a callable, :code:`menu_cond` will be invoked when - constructing the menu items. If it returns :code:`True`, - then this link will be a part of the menu. Otherwise, it - will not be included in the menu items. Defaults to - :code:`None`, meaning the item will always be present. - Examples:: - appbuilder = AirflowAppBuilder(app, db) - # Register a view, rendering a top menu without icon. - appbuilder.add_view(MyModelView(), "My View") - # or not instantiated - appbuilder.add_view(MyModelView, "My View") - # Register a view, a submenu "Other View" from "Other" with a phone icon. - appbuilder.add_view( - MyOtherModelView, - "Other View", - icon='fa-phone', - category="Others" - ) - # Register a view, with category icon and translation. - appbuilder.add_view( - YetOtherModelView, - "Other View", - icon='fa-phone', - label=_('Other View'), - category="Others", - category_icon='fa-envelop', - category_label=_('Other View') - ) - # Register a view whose menu item will be conditionally displayed - appbuilder.add_view( - YourFeatureView, - "Your Feature", - icon='fa-feature', - label=_('Your Feature'), - menu_cond=lambda: is_feature_enabled("your-feature"), - ) - # Add a link - appbuilder.add_link("google", href="www.google.com", icon = "fa-google-plus") - """ - baseview = self._check_and_init(baseview) - log.info(LOGMSG_INF_FAB_ADD_VIEW.format(baseview.__class__.__name__, name)) - - if not self._view_exists(baseview): - baseview.appbuilder = self - self.baseviews.append(baseview) - self._process_inner_views() - if self.app: - self.register_blueprint(baseview) - self._add_permission(baseview) - self.add_link( - name=name, - href=href, - icon=icon, - label=label, - category=category, - category_icon=category_icon, - category_label=category_label, - baseview=baseview, - cond=menu_cond, - ) - return baseview - - def add_link( - self, - name, - href, - icon="", - label="", - category="", - category_icon="", - category_label="", - baseview=None, - cond=None, - ): - """ - Add your own links to menu using this method - :param name: - The string name that identifies the menu. - :param href: - Override the generated href for the menu. - You can use an url string or an endpoint name - :param icon: - Font-Awesome icon name, optional. - :param label: - The label that will be displayed on the menu, - if absent param name will be used - :param category: - The menu category where the menu will be included, - if non provided the view will be accessible as a top menu. - :param category_icon: - Font-Awesome icon name for the category, optional. - :param category_label: - The label that will be displayed on the menu, - if absent param name will be used - :param cond: - If a callable, :code:`cond` will be invoked when - constructing the menu items. If it returns :code:`True`, - then this link will be a part of the menu. Otherwise, it - will not be included in the menu items. Defaults to - :code:`None`, meaning the item will always be present. - """ - self.menu.add_link( - name=name, - href=href, - icon=icon, - label=label, - category=category, - category_icon=category_icon, - category_label=category_label, - baseview=baseview, - cond=cond, - ) - if self.app: - self._add_permissions_menu(name) - if category: - self._add_permissions_menu(category) - - def add_separator(self, category, cond=None): - """ - Add a separator to the menu, you will sequentially create the menu - :param category: - The menu category where the separator will be included. - :param cond: - If a callable, :code:`cond` will be invoked when - constructing the menu items. If it returns :code:`True`, - then this separator will be a part of the menu. Otherwise, - it will not be included in the menu items. Defaults to - :code:`None`, meaning the separator will always be present. - """ - self.menu.add_separator(category, cond=cond) - - def add_view_no_menu(self, baseview, endpoint=None, static_folder=None): - """ - Add your views without creating a menu. - :param baseview: - A BaseView type class instantiated. - """ - baseview = self._check_and_init(baseview) - log.info(LOGMSG_INF_FAB_ADD_VIEW.format(baseview.__class__.__name__, "")) - - if not self._view_exists(baseview): - baseview.appbuilder = self - self.baseviews.append(baseview) - self._process_inner_views() - if self.app: - self.register_blueprint(baseview, endpoint=endpoint, static_folder=static_folder) - self._add_permission(baseview) - else: - log.warning(LOGMSG_WAR_FAB_VIEW_EXISTS.format(baseview.__class__.__name__)) - return baseview - - def add_api(self, baseview): - """ - Add a BaseApi class or child to AirflowAppBuilder - :param baseview: A BaseApi type class - :return: The instantiated base view - """ - return self.add_view_no_menu(baseview) - - def security_cleanup(self): - """ - This method is useful if you have changed - the name of your menus or classes, - changing them will leave behind permissions - that are not associated with anything. - You can use it always or just sometimes to - perform a security cleanup. Warning this will delete any permission - that is no longer part of any registered view or menu. - Remember invoke ONLY AFTER YOU HAVE REGISTERED ALL VIEWS - """ - self.sm.security_cleanup(self.baseviews, self.menu) - - def security_converge(self, dry=False) -> Dict: - """ - This method is useful when you use: - - `class_permission_name` - - `previous_class_permission_name` - - `method_permission_name` - - `previous_method_permission_name` - migrates all permissions to the new names on all the Roles - :param dry: If True will not change DB - :return: Dict with all computed necessary operations - """ - return self.sm.security_converge(self.baseviews, self.menu, dry) - - @property - def get_url_for_login(self): - return url_for(f"{self.sm.auth_view.endpoint}.login") - - @property - def get_url_for_logout(self): - return url_for(f"{self.sm.auth_view.endpoint}.logout") - - @property - def get_url_for_index(self): - return url_for(f"{self.indexview.endpoint}.{self.indexview.default_view}") - - @property - def get_url_for_userinfo(self): - return url_for(f"{self.sm.user_view.endpoint}.userinfo") - - def get_url_for_locale(self, lang): - return url_for( - f"{self.bm.locale_view.endpoint}.{self.bm.locale_view.default_view}", - locale=lang, - ) - - def add_permissions(self, update_perms=False): - if self.update_perms or update_perms: - for baseview in self.baseviews: - self._add_permission(baseview, update_perms=update_perms) - self._add_menu_permissions(update_perms=update_perms) - - def _add_permission(self, baseview, update_perms=False): - if self.update_perms or update_perms: - try: - self.sm.add_permissions_view(baseview.base_permissions, baseview.class_permission_name) - except Exception as e: - log.exception(e) - log.error(LOGMSG_ERR_FAB_ADD_PERMISSION_VIEW.format(str(e))) - - def _add_permissions_menu(self, name, update_perms=False): - if self.update_perms or update_perms: - try: - self.sm.add_permissions_menu(name) - except Exception as e: - log.exception(e) - log.error(LOGMSG_ERR_FAB_ADD_PERMISSION_MENU.format(str(e))) - - def _add_menu_permissions(self, update_perms=False): - if self.update_perms or update_perms: - for category in self.menu.get_list(): - self._add_permissions_menu(category.name, update_perms=update_perms) - for item in category.childs: - # don't add permission for menu separator - if item.name != "-": - self._add_permissions_menu(item.name, update_perms=update_perms) - - def register_blueprint(self, baseview, endpoint=None, static_folder=None): - self.get_app.register_blueprint( - baseview.create_blueprint(self, endpoint=endpoint, static_folder=static_folder) - ) - - def _view_exists(self, view): - for baseview in self.baseviews: - if baseview.__class__ == view.__class__: - return True - return False - - def _process_inner_views(self): - for view in self.baseviews: - for inner_class in view.get_uninit_inner_views(): - for v in self.baseviews: - if isinstance(v, inner_class) and v not in view.get_init_inner_views(): - view.get_init_inner_views().append(v) - def init_appbuilder(app): """Init `Flask App Builder <https://flask-appbuilder.readthedocs.io/en/latest/>`__.""" @@ -629,6 +33,16 @@ def init_appbuilder(app): not FAB's security manager.""" ) + class AirflowAppBuilder(AppBuilder): + """Custom class to prevent side effects of the session.""" + + def _check_and_init(self, baseview): + if hasattr(baseview, 'datamodel'): + # Delete sessions if initiated previously to limit side effects. We want to use + # the current session in the current application. + baseview.datamodel.session = None + return super()._check_and_init(baseview) + AirflowAppBuilder( app=app, session=settings.Session,
