How can a flask Addon overwrite a template? - python

I created a flask addon using "flask fab create-addon".
I would like to change the template appbuilder/general/security/login_oauth.html so I have:
templates
appbuilder
general
security
login_oauth.html
But when I load the host application my version of login_oauth.html is not loaded. I tried registering a blueprint as in this post with the following code:
from flask import Blueprint
bp = Blueprint('fab_addon_fslogin', __name__, template_folder='templates')
class MyAddOnManager(BaseManager):
def __init__(self, appbuilder):
"""
Use the constructor to setup any config keys specific for your app.
"""
super(MyAddOnManager, self).__init__(appbuilder)
self.appbuilder.get_app.config.setdefault("MYADDON_KEY", "SOME VALUE")
self.appbuilder.register_blueprint(bp)
def register_views(self):
"""
This method is called by AppBuilder when initializing, use it to add you views
"""
pass
def pre_process(self):
pass
def post_process(self):
pass
But register_blueprint(bp) return:
File "/home/cquiros/data/projects2017/personal/software/superset/addons/fab_addon_fslogin/fab_addon_fslogin/manager.py", line 24, in __init__
self.appbuilder.register_blueprint(bp)
File "/home/cquiros/data/projects2017/personal/software/superset/env_superset/lib/python3.8/site-packages/Flask_AppBuilder-3.3.0-py3.8.egg/flask_appbuilder/base.py", line 643, in register_blueprint
baseview.create_blueprint(
AttributeError: 'Blueprint' object has no attribute 'create_blueprint'
Not much information out there on how to do this. Any clue is appreciated

If you want to customize login_oauth.html, The easiest way is that adding it into your app directly not addon.
That means the login_oauth.html should be put in this path.
YourApp
- app
-- templates
--- appbuilder
---- general
----- security
------ login_oauth.html
For solution of addons, the function self.appbuilder.register_blueprint is for views not for the object which Blueprint fuction returns. It should be replaced with
self.appbuilder.get_app.register_blueprint(Blueprint('fab_addon_fslogin', __name__, template_folder='templates'))
Keep the blueprint registration and
try to rename the login_oauth.html to login_oauth_xxxx.html which at
{your python packages root path}\site-packages\flask_appbuilder\templates\appbuilder\general\security
That will let the template be overwrote as you need. I guess that the template searching order of app-builder package is greater than addons. The searching order of blueprints depend on order of registrations
Finally, I have found a trick without renaming the file, you could try the following
class MyAddOnManager(BaseManager):
def __init__(self, appbuilder):
"""
Use the constructor to setup any config keys specific for your app.
"""
super(MyAddOnManager, self).__init__(appbuilder)
self.appbuilder.get_app.config.setdefault('MYADDON_KEY', 'SOME VALUE')
self.static_bp = Blueprint('fab_addon_fslogin', __name__, template_folder='templates')
def register_views(self):
"""
This method is called by AppBuilder when initializing, use it to add you views
"""
pass
def pre_process(self):
self.appbuilder.get_app.register_blueprint(self.static_bp)
blueprint_order = self.appbuilder.get_app._blueprint_order
# move blueprint order of addon to top
blueprint_order.insert(0, blueprint_order.pop())
def post_process(self):
pass
Reference:
flask-appbuilder Customizing

Related

How to override python pyramid app config settings?

I am a newbie at Python Pyramid and working on improving an existing app that we have.
I have an app main function defined like below:
def web_main(global_config, **settings):
config = Configurator(settings=settings, root_factory=RootFactory)
...
...
config.add_request_method(get_user, "user", reify=True)
config.set_authentication_policy(authn_policy)
config.set_authorization_policy(authz_policy)
...
app = config.make_wsgi_app()
return app
I want to override get_user request method with my implementation and also want to use my own authentication policy.
With that I was thinking to do write a function like below:
def my_web_main(global_config, **settings):
app = web_main(global_config, **settings)
<Set Overrides here>
return app
Inside the config.ini file I will call my_web_main to start this app.
I have not been able to figure out how to set the overrides. Would appreciate some inputs on this.
The configurator is where you should perform overrides. So the answer is to modify web_main or define your own. Pyramid has an override mechanism via config.include(), but it does not work one level higher where you're attempting to override things with a built wsgi-app. You have to do it at the config level.

Nestable Blueprint not working

I would like to use Blueprint nesting so that similar blueprints can have similar #similar.before_request, etc (along with nice url name spacing).
I am aware that flask doesnt native support this feature.
However, this issue mentions a way to do it:
class NestableBlueprint(Blueprint):
"""
Hacking in support for nesting blueprints, until hopefully https://github.com/mitsuhiko/flask/issues/593 will be resolved
"""
def register_blueprint(self, blueprint, **options):
def deferred(state):
url_prefix = (state.url_prefix or u"") + (options.get('url_prefix', blueprint.url_prefix) or u"")
if 'url_prefix' in options:
del options['url_prefix']
state.app.register_blueprint(blueprint, url_prefix=url_prefix, **options)
self.record(deferred)
Trying the above fails to work, and gives me 404.
I have a file, which defines a blueprint like :
base_blueprint = Blueprint("BASE", __name__, url_prefix='/v0.1')
At the bottom, I have :
from .Admin import admin_base_blueprint
admin_base_blueprint.register_blueprint(base_blueprint,
url_prefix='/admin')
And Admin.py is defined as:
admin_base_blueprint = NestableBlueprint('admin', __name__)
#admin_base_blueprint.route('/', methods=['GET'])
def admin_echo_time():
"""
Just a function to echo current time.
May be useful for testing if servers are up.
:return: current date
"""
return datetime.now()
When I try to visit GET /v0.1/admin, it give me a 404.
I also tried https://stackoverflow.com/a/36326234/2670775. In this case, I am able to get url name spacing to work , but blueprint functions like before_request dont get called.
my flask version is 0.12.2

How to import custom jinja2 filters from another file (and using Flask)?

I have a jinja_filters.py file with a few dozen custom filters I've written. Now I have multiple Flask apps that need to use these filters. (I'm not sure if my problem is Flask-specific or not.)
One hacky way to accomplish what I want is to do:
app = Flask(__name__)
import jinja_filters
#app.template_filter('filter_name1')
def filter_name1(arg):
return jinja_filters.filter_name1(arg)
#app.template_filter('filter_name2')
def filter_name2(arg):
return jinja_filters.filter_name2(arg)
...
What's the "right" way to do this?
EDIT: Ideally, I wouldn't have to list each filter name. So when I add a new filter to jinja_filters.py I don't have to update any other code -- all my apps would be able to use it right away.
There is a recommended way of doing this using Flask blueprints. One of it's use cases is this functionality specifically:
Provide template filters, static files, templates, and other utilities through blueprints. A blueprint does not have to implement applications or view functions.
You just need to create a flask.Blueprint object and use it to register your filters in a similar way as you would with flask.Flask app object, using the Blueprint.app_template_filter decorator or Blueprint.add_app_template_filter method.
# filters.py
import jinja2
import flask
blueprint = flask.Blueprint('filters', __name__)
# using the decorator
#jinja2.contextfilter
#blueprint.app_template_filter()
def filter1(context, value):
return 1
# using the method
#jinja2.contextfilter
def filter2(context, value):
return 2
blueprint.add_app_template_filter(filter2)
Then you just need to register the blueprint on your app object:
# app.py
import flask
import filters
app = flask.Flask(__name__)
app.register_blueprint(filters.blueprint)
And voilĂ , the filters are registered.
Where ever you're setting up your app object (app.py, perhaps), you only need to import your custom filters and then modify the Jinja environment attribute.
import jinja_filters
app = Flask(__name__)
app.jinja_env.filters['filter_name1'] = jinja_filters.filter_name1
app.jinja_env.filters['filter_name2'] = jinja_filters.filter_name2
and so on.
Another possibility is to use the inspect module to find all the methods in jinja_filters like so:
from inspect import getmembers, isfunction
import jinja_filters
app = Flask(__name__)
my_filters = {name: function
for name, function in getmembers(jinja_filters)
if isfunction(function)}
app.jinja_env.filters.update(my_filters)
That code is untested, but the idea is to build a dictionary of function names and functions that exist in your jinja_filters files and then update the Jinja environment's filters dictionary with your filters.

How to load from more then one template_folder for Flask blueprint?

I learned how to create Flask Blueprints and can create blueprint for non flask products that uses Jinja2 templates and use them inside flask projects. I do something like this:
# blueprint code
from flask import Blueprint
from pkg_resources import resource_filename
app = Blueprint('formgear', __name__,
template_folder=resource_filename('formgear', 'templates'))
And now I want to add another set of templates, which is logically connected with my non-Flask project but are Flask-only specific. I'm completely not sure is it good desing, but is there any way to propagate both templates folders from one blueprint? And make both templates set available for whole Flask project?
Note: formgear is name of my non-Flask project.
Here is the trail of investigations.
From flask.blueprints
class Blueprint(_PackageBoundObject):
....
def __init__(self, name, import_name, static_folder=None,
static_url_path=None, template_folder=None,
...)
_PackageBoundObject.__init__(self, import_name, template_folder)
....
From flask.helpers
#locked_cached_property
def jinja_loader(self):
"""The Jinja loader for this package bound object.
.. versionadded:: 0.5
"""
if self.template_folder is not None:
return FileSystemLoader(os.path.join(self.root_path,
self.template_folder))
Investigation Result:
template_folder which is passed to Blueprints is subsequently to class _PackageBoundObject
It is treated as a single string and not as a list of folders
Result:
You can not pass multiple paths for template_folder

flask blueprint template folder

My flask app layout is:
myapp/
run.py
admin/
__init__.py
views.py
pages/
index.html
main/
__init__.py
views.py
pages/
index.html
_init_.py files are empty. admin/views.py content is:
from flask import Blueprint, render_template
admin = Blueprint('admin', __name__, template_folder='pages')
#admin.route('/')
def index():
return render_template('index.html')
main/views.py is similar to admin/views.py:
from flask import Blueprint, render_template
main = Blueprint('main', __name__, template_folder='pages')
#main.route('/')
def index():
return render_template('index.html')
run.py is:
from flask import Flask
from admin.views import admin
from main.views import main
app = Flask(__name__)
app.register_blueprint(admin, url_prefix='/admin')
app.register_blueprint(main, url_prefix='/main')
print app.url_map
app.run()
Now, if I access http://127.0.0.1:5000/admin/, it correctly displays admin/index.html.
However, http://127.0.0.1:5000/main/ shows still admin/index.html instead of main/index.html. I checked app.url_map:
<Rule 'admin' (HEAD, OPTIONS, GET) -> admin.index,
<Rule 'main' (HEAD, OPTIONS, GET) -> main.index,
Also, I verified that index function in main/views.py is called as expected.
If I rename main/index.html to something different then it works. So, without
renaming, how can achieve that 1http://127.0.0.1:5000/main/1 shows main/index.html?
As of Flask 0.8, blueprints add the specified template_folder to the app's searchpath, rather than treating each of the directories as separate entities. This means that if you have two templates with the same filename, the first one found in the searchpath is the one used. This is admittedly confusing, and is poorly documented at this time (see this bug). It seems that you weren't the only one that was confused by this behavior.
The design reason for this behavior is so that blueprint templates can be easily overriden from the main app's templates, which are first-in-line in Flask's template searchpath.
Two options come to mind.
Rename each of the index.html files to be unique (e.g. admin.html
and main.html).
In each of the template folders, put each of the
templates in a subdirectory of the blueprint folder and then call
the template using that subdirectory. Your admin template, for example, would be yourapp/admin/pages/admin/index.html, and then called from within
the blueprint as render_template('admin/index.html').
In addition to linqq's good suggestions above, you can also override the default functionality if needed. There are a couple ways:
One can override create_global_jinja_loader in a subclassed Flask application (which returns a DispatchingJinjaLoader defined in flask/templating.py). This is not recommended, but would work. The reason that this is discouraged is that the DispatchingJinjaLoader has enough flexiblity to support the injection of custom loaders. And if you screw your own loader up, it'll be able to lean on default, sane functionality.
So, what is recommended is that one "override the jinja_loader function" instead. This is where lack of documentation comes in. Patching Flask's loading strategy requires some knowledge that doesn't seem to be documented, as well as a good understanding of Jinja2.
There are two components you need to understand:
The Jinja2 environment
The Jinja2 template loader
These are created by Flask, with sensible defaults, automatically. (You can specify your own Jinja2 options, by the way, by overriding app.jinja_options -- but bear in mind that you'll lose two extensions which Flask includes by default -- autoescape and with -- unless you specify them yourself. Take a look at flask/app.py to see how they reference those.)
The environment contains all of those context processors (e.g., so you can do var|tojson in a template), helper functions (url_for, etc) and variables (g, session, app). It also contains a reference to a template loader, in this case the aforementioned and auto-instantiated DispatchingJinjaLoader. So when you call render_template in your app, it finds or creates the Jinja2 environment, sets up all those goodies, and calls get_template on it, which in turn calls get_source inside of the DispatchingJinjaLoader, which tries a few strategies described later.
If all goes according to plan, that chain will resolve in finding a file and will return its contents (and some other data). Also, note that this is the same execution path that {% extend 'foo.htm' %} takes.
DispatchingJinjaLoader does two things: First it checks if the app's global loader, which is app.jinja_loader can locate the file. Failing that, it checks all application blueprints (in order of registration, AFAIK) for blueprint.jinja_loader in an attempt to locate the file. Tracing that chain to the very end, here is definition of jinja_loader (in flask/helpers.py, _PackageBoundObject, the base class of both the Flask application and Blueprints):
def jinja_loader(self):
"""The Jinja loader for this package bound object.
.. versionadded:: 0.5
"""
if self.template_folder is not None:
return FileSystemLoader(os.path.join(self.root_path,
self.template_folder))
Ah! So now we see. Obviously, the namespaces of both will conflict over the same directory names. Since the global loader is called first, it will always win. (FileSystemLoader is one of several standard Jinja2 loaders.) However, what this means is that there's no truly simple way to reorder the Blueprint and the application-wide template loader.
So, we need to modify the behavior of DispatchingJinjaLoader. For a while, I thought there was no good non-discouraged and efficient way of going about this. However, apparently if you override app.jinja_options['loader'] itself, we can get the behavior we want. So, if we subclass DispatchingJinjaLoader, and modify one small function (I suppose it might be better to reimplement it entirely, but this works for now), we have the behavior we want. In total, a reasonable strategy would be the following (untested, but should work with modern Flask applications):
from flask.templating import DispatchingJinjaLoader
from flask.globals import _request_ctx_stack
class ModifiedLoader(DispatchingJinjaLoader):
def _iter_loaders(self, template):
bp = _request_ctx_stack.top.request.blueprint
if bp is not None and bp in self.app.blueprints:
loader = self.app.blueprints[bp].jinja_loader
if loader is not None:
yield loader, template
loader = self.app.jinja_loader
if loader is not None:
yield loader, template
This modifies the strategy of the original loader in two ways: Attempt to load from the blueprint (and ONLY the currently executing blueprint, not all blueprints) first, and if that fails, only then load from the application. If you like the all-blueprint behavior, you can do some copy-pasta from flask/templating.py.
To tie it all together, you have to set jinja_options on the Flask object:
app = Flask(__name__)
# jinja_options is an ImmutableDict, so we have to do this song and dance
app.jinja_options = Flask.jinja_options.copy()
app.jinja_options['loader'] = ModifiedLoader(app)
The first time a template environment is needed (and thus instantiated), meaning the first time render_template is called, your loader should be used.
twooster's answer is interesting, but another problem is that Jinja by default caches a template based on its name. Because both templates are named "index.html", the loader won't run for subsequent blueprints.
Besides linqq's two suggestions, a third option is to ignore the blueprint's templates_folder option all together and place the templates in respective folders in the application's templates directory.
ie:
myapp/templates/admin/index.html
myapp/templates/main/index.html
Tks #linqq, your method really works well here, besides I made a better solution by the decorator.
Attention here, don't import the render_template function like this:
from flask import render_template
You should import the flask module like this:
import flask
Then, make this block of code at the top of your router file:
def render_decorate(path_prefix):
def decorate(func):
def dec_func(*args, **kw):
arg_list = list(args)
arg_list[0] = path_prefix + str(arg_list[0])
arg_tuple = tuple(arg_list)
return func(*arg_tuple, **kw)
return dec_func
return decorate
#render_decorate("%YOUR_DIRECTORY_NAME%/")
def render_template(template_name_or_list, **context):
return flask.render_template(template_name_or_list, **context)
Replace the %YOUR_DIRECTORY_NAME% with your actual path, and ensure your templates folder is like this:
Folder Structure
And all done! Just use the render_template function as usual.
I'm using something like this on fypress and fybb because I have a theme system.
# utils.templates
from jinja2 import Environment, PackageLoader
from flask.templating import _default_template_ctx_processor
from flask import current_app, url_for, get_flashed_messages
admin_env = Environment(
loader=PackageLoader('fypress', '/templates/admin/'),
extensions=['jinja2.ext.autoescape', 'jinja2.ext.with_'],
autoescape=True
)
def render_template(template, **kwargs):
kwargs.update(_default_template_ctx_processor())
kwargs.update({
'url_for': url_for,
'get_flashed_messages': get_flashed_messages # etc...
})
kwargs.update(dict(debug=current_app.config.get('DEBUG'), flask_config=current_app.config))
template = admin_env.get_template(template)
return template.render(**kwargs)
And then
# routes.admin.
from flask import Blueprint
from utils.templates import render_template
admin_bp = Blueprint('admin', __name__, url_prefix='/admin')
#admin_bp.route('/')
def root():
return render_template('index.html', title='Admin')
Currently this is what works for me. First search the template in the blueprint templates folder, if not found search in the app templates folder (for layout, etc.).
from jinja2 import BaseLoader, TemplateNotFound
from flask import Flask, current_app, request
class BlueprintLoader(BaseLoader):
def get_source(self, environment, template):
for loader in (current_app.blueprints[request.blueprint].jinja_loader, current_app.jinja_loader):
try:
if loader:
return loader.get_source(environment, template)
except TemplateNotFound:
pass
raise TemplateNotFound(template)
app = Flask(__name__)
app.jinja_env.loader = BlueprintLoader()
An easy fix would be to specify the template_folder = "templates" and then on rendering the template you would then specify the blueprint name as the parent directory as shown below
#users.route("/")
def users_index():
return render_template('users/index.html')
Note that the above solution works only if you have created a sub-folder with the name of the blueprint first inside the templates folder which will be under your blueprint

Categories

Resources