Open rcubarocha opened 8 years ago
I did something similar and here's how I solved it:
class MicrositeApp(Flask):
def send_static_file(self, filename, subdomain=None):
response = super(MicrositeApp, self).send_static_file(filename)
#response.headers['Access-Control-Allow-Origin'] = '*%s' % self.config['SERVER_NAME']
response.headers['Access-Control-Allow-Origin'] = '*'
return response
app = MicrositeApp(__name__, static_folder=None)
app.add_url_rule('/static/<path:filename>',
endpoint='static',
view_func=app.send_static_file)
app.add_url_rule('/static/<path:filename>',
endpoint='static',
subdomain='<subdomain>',
view_func=app.send_static_file)
Static files will be sent from the "root" website. If you want to send static resources from current subdomain, you can add your own function that'll get current subdomain from the request and generate the URL. For example:
def static_url(path, _external=False, subdomain=None):
if subdomain is None:
# Implement your application-specific _get_subdomain() logic
subdomain = _get_subdomain()
return url_for('static',
filename=path,
subdomain=subdomain,
_external=_external)
@app.context_processor
def inject_context():
return {
'static_url': static_url
}
I also use static_url
to inject file hash, so I don't have to worry about versioning. Hash is calculated only once and stored in a dictionary.
Thank you very much for the reply. Though I would appreciate a bit more clarity.
Your sample seems to override Flask's default static route. I have two questions:
1) How do the two rules work simultaneously? I guess the one without subdomain allows for the situation where url_for
does not include the subdomain parameter (which is why subdomain has a default value in the handler)?
2) I'm not sure how this would also take effect for Flask-Admin's own static attributes. Doesn't it have it's own path and endpoint?
1) Works just fine - it'll use one that's provided by the user. And yes, it's backward compatible - allows using url_for
without the subdomain
argument. The second rule uses Flask subdomain machinery (pass subdomain as argument to the view).
2) Default Flask-Admin url_for
will generate links to the root of the website. But because code adds CORS headers, everything will work as expected. Alternatively, you can override header and footer in your base Flask-Admin template and use static_url
helper.
In the production you'll have to setup nginx (or whatever that's in front of the Flask app) to serve files from subdomains without sending these requests to the Flask app.
I only now had the chance to test what you proposed.
First of all I noticed that without changing anything at all, static files on the non-Admin side are already being served from root domain, while all other routes (which I have defined) do pay attention to the subdomain. The only static file requests I see fail are only the ones generated client-side with relative paths, so instead of the url being domain.com/static/
Not sure if this behavior is expected.
Anyway, my real problem is that Flask-Admin is unable to load stuff like the Bootstrap css from its own static path.
I implemented your subclass to override the static url handler but now all I get is this error non-Admin routes (which were working before):
The referenced blueprint <SubdomainApp 'app'> has no static folder.
I get the same error as before for Admin-side routes.
And this for direct static files:
RuntimeError: No static folder for this object
The stack trace for the latter one shows the exception being raised on the call to the original Flask send_static_file from the code you provided:
response = super(SubdomainApp, self).send_static_file(filename)
I am not sure where to go from here.
It is actually useful being able to serve static files from both the root domain and any subdomain, so I looked a little further.
Since the suggested code is really not creating its own logic, instead leveraging the existing Flask function, setting the static_folder
on the app doesn't make sense as the function still expects a value in it. So I have refactored it to this:
class MicrositeApp(Flask):
def send_static_file(self, filename, subdomain=None):
response = super(MicrositeApp, self).send_static_file(filename)
#response.headers['Access-Control-Allow-Origin'] = '*%s' % self.config['SERVER_NAME']
response.headers['Access-Control-Allow-Origin'] = '*'
return response
app = MicrositeApp(__name__)
app.add_url_rule('/static/<path:filename>',
endpoint='static',
subdomain='<subdomain>',
view_func=app.send_static_file)
This keeps the standard Flask static
rule but adds one that can use subdomains.
This, however, still does nothing for my issue with static files for the templates in Flask-Admin.
What's the error with Flask-Admin static files? Can you post full traceback here?
It simply seems that setting Flask-Admin up to use subdomains extends that requirement to its own static rule, and so the url_for
requesting files used in the templates (such as the bootstrap css) fails without that subdomain parameter.
Here it is:
File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1836, in __call__
return self.wsgi_app(environ, start_response)
File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1820, in wsgi_app
response = self.make_response(self.handle_exception(e))
File "/usr/local/lib/python2.7/dist-packages/flask_restful/__init__.py", line 145, in error_router
return original_handler(e)
File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1403, in handle_exception
reraise(exc_type, exc_value, tb)
File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1817, in wsgi_app
response = self.full_dispatch_request()
File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1477, in full_dispatch_request
rv = self.handle_user_exception(e)
File "/usr/local/lib/python2.7/dist-packages/flask_restful/__init__.py", line 145, in error_router
return original_handler(e)
File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1381, in handle_user_exception
reraise(exc_type, exc_value, tb)
File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1475, in full_dispatch_request
rv = self.dispatch_request()
File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1461, in dispatch_request
return self.view_functions[rule.endpoint](**req.view_args)
File "/usr/local/lib/python2.7/dist-packages/flask_admin/base.py", line 68, in inner
return self._run_view(f, *args, **kwargs)
File "/usr/local/lib/python2.7/dist-packages/flask_admin/base.py", line 367, in _run_view
return fn(self, *args, **kwargs)
File "/home/rcubarocha/Documents/App/multitenant/admin.py", line 52, in index
return super(MyAdminIndexView, self).index()
File "/usr/local/lib/python2.7/dist-packages/flask_admin/base.py", line 68, in inner
return self._run_view(f, *args, **kwargs)
File "/usr/local/lib/python2.7/dist-packages/flask_admin/base.py", line 367, in _run_view
return fn(self, *args, **kwargs)
File "/usr/local/lib/python2.7/dist-packages/flask_admin/base.py", line 451, in index
return self.render(self._template)
File "/usr/local/lib/python2.7/dist-packages/flask_admin/base.py", line 307, in render
return render_template(template, **kwargs)
File "/usr/local/lib/python2.7/dist-packages/flask/templating.py", line 128, in render_template
context, ctx.app)
File "/usr/local/lib/python2.7/dist-packages/flask/templating.py", line 110, in _render
rv = template.render(context)
File "/usr/local/lib/python2.7/dist-packages/jinja2/environment.py", line 989, in render
return self.environment.handle_exception(exc_info, True)
File "/usr/local/lib/python2.7/dist-packages/jinja2/environment.py", line 754, in handle_exception
reraise(exc_type, exc_value, tb)
File "/usr/local/lib/python2.7/dist-packages/flask_admin/templates/bootstrap3/admin/index.html", line 1, in top-level template code
{% extends 'admin/master.html' %}
File "/usr/local/lib/python2.7/dist-packages/flask_admin/templates/bootstrap3/admin/master.html", line 1, in top-level template code
{% extends admin_base_template %}
File "/home/rcubarocha/Documents/App/multitenant/templates/layout.html", line 2, in top-level template code
{% extends 'admin/base.html' %}
File "/usr/local/lib/python2.7/dist-packages/flask_admin/templates/bootstrap3/admin/base.html", line 14, in top-level template code
{% block head_css %}
File "/usr/local/lib/python2.7/dist-packages/flask_admin/templates/bootstrap3/admin/base.html", line 15, in block "head_css"
<link href="{{ admin_static.url(filename='bootstrap/bootstrap3/css/bootstrap.min.css', v='3.3.5') }}" rel="stylesheet">
File "/usr/local/lib/python2.7/dist-packages/flask_admin/templates/bootstrap3/admin/static.html", line 2, in template
{{ url_for('admin.static', *varargs, **kwargs) }}
File "/usr/local/lib/python2.7/dist-packages/flask/helpers.py", line 312, in url_for
return appctx.app.handle_url_build_error(error, endpoint, values)
File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1641, in handle_url_build_error
reraise(exc_type, exc_value, tb)
File "/usr/local/lib/python2.7/dist-packages/flask/helpers.py", line 305, in url_for
force_external=external)
File "/usr/local/lib/python2.7/dist-packages/werkzeug/routing.py", line 1758, in build
raise BuildError(endpoint, values, method, self)
BuildError: Could not build url for endpoint 'admin.static' with values ['filename', 'v']. Did you forget to specify values ['subdomain']?
Error says that it can't find the endpoint with required subdomain
argument. I'm not sure where it is coming from, assuming that you added both rules.
Well, if you don't know, then I am hopeless.
But let me ask you something. I just want to understand this better.
You said that Flask's default behavior for url_for
is to generate links at the root of the application. Which is why the code you suggested changes the behavior of Flask's static
endpoint.
However, the file referenced in the error for which the template is trying to build a link is not in the app's static folder, but is part of Flask-Admin's package, right? Isn't that why the endpoint is admin.static
? I am not sure of the whole mechanism, though.
In a version of the working app before I started refactoring it for multi-tenancy, I see that the path created for that file is /admin/static/bootstrap/bootstrap3/css/bootstrap.min.css.
This is definitely not a straight up file path (there is no admin/static/ folder in the project), so I assume the admin.static
endpoint is set up to serve a route like /admin/static/<path:filename>
, but where does it actually serve the file from?
I'm sorry for all the questions, it's just that I am really not finding these answers looking through the repository.
url_for
logic is pretty simple:
In your case, it found the static folder for admin.static
, but failed to find a version that does not accept subdomain
argument.
So I have a question - did you do any other changes to support multiple subdomains? Why subdomain
is required argument for all endpoints?
Sure, easy way would be to add_rule
for admin.static
and point it to the send_static_file
like you did for global resources. But that's a hack and I'm not sure why it requires subdomain
for every other endpoint.
Regarding your other questions: Flask allows having multiple static folders per appliction. Each blueprint might have one and they can point to different physical directories. Flask-Admin is distributed with few CSS/JS files that live along with the package.
Here's what I have done so far regarding multi-tenancy through subdomains.
On the non-Admin side:
app.config['SERVER_NAME'] = 'local.dev:5000' #(1)
...
@app.route('/about/', subdomain='<subdomain>') #(2)
def about(subdomain): #(3)
...
return render_template(template, **template_vars)
(1) Enables Flask subdomain options
(2) Capture the dynamic subdomain. If the subdomain
parameter is not set, then the route will only respond to the root domain
(3) Required if (2) is in place since Flask will invoke the function passing it the subdomain argument or you get an unexpected keyword argument error
The above altogether, makes it so that when creating links to these routes, url_for
requires passing a subdomain parameter (just like it is being requested in the error I am trying to resolve), and I had to go to my templates and pass them the subdomain and add it to every url_for
for everything to work.
Now on the Admin side:
class CustomAdminIndexView(AdminIndexView):
@expose('/')
def index(self, subdomain): #(1)
return super(CustomAdminIndexView, self).index()
...
#(2)
admin = Admin(app, subdomain='<subdomain>', name='Admin Panel', base_template='layout.html', index_view = CustomAdminIndexView(), template_mode='bootstrap3')
(1) Similar to the non-Admin side, it's required or else end up with an unexpected keyword argument error
(2) I didn't see this documented but rather just in the code for Flask-Admin that I could pass a dynamic subdomain argument like for the routes in the non-Admin side. This seemed to enable subdomains for Admin routes and there requirement for the parameter in the exposed routes as in (1)
I guess the same mechanism causing the requirement of (1) is applying the same to the admin.static
route?
After that. I added the code you suggested (with the fix of not setting the static_folder
to None), and nothing has changed except that now on the non-Admin side I can now retrieve static files from both the root domain (as before) and any subdomain.
Yeah, subdomain
argument passed to the Flask-Admin makes all routes (including static route) require subdomain
to generate URLs.
So I see few options:
admin.static
rule explicitly without the subdomain
argument to fix the issue. I was looking into how to implement any of the options you suggested (which I was finding hard to do), and it seems that the issue is more systemic than I thought and not just about the static route.
Upon testing a ModelView route, another issue with the subdomain comes up but this time even before it gets to the template render stage. Here is the trace:
File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1836, in __call__
return self.wsgi_app(environ, start_response)
File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1820, in wsgi_app
response = self.make_response(self.handle_exception(e))
File "/usr/local/lib/python2.7/dist-packages/flask_restful/__init__.py", line 145, in error_router
return original_handler(e)
File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1403, in handle_exception
reraise(exc_type, exc_value, tb)
File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1817, in wsgi_app
response = self.full_dispatch_request()
File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1477, in full_dispatch_request
rv = self.handle_user_exception(e)
File "/usr/local/lib/python2.7/dist-packages/flask_restful/__init__.py", line 145, in error_router
return original_handler(e)
File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1381, in handle_user_exception
reraise(exc_type, exc_value, tb)
File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1475, in full_dispatch_request
rv = self.dispatch_request()
File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1461, in dispatch_request
return self.view_functions[rule.endpoint](**req.view_args)
File "/usr/local/lib/python2.7/dist-packages/flask_admin/base.py", line 68, in inner
return self._run_view(f, *args, **kwargs)
File "/usr/local/lib/python2.7/dist-packages/flask_admin/base.py", line 367, in _run_view
return fn(self, *args, **kwargs)
TypeError: index_view() got an unexpected keyword argument 'subdomain'
Similar issues happen with other views such as AdminIndexView.
There are multiple things at play that need to be adapted it seems.
First, all the routes implemented in the base view classes (ModelView, AdminIndexView, etc...) need to be re-implemented with the added subdomain parameter.
Second, the subdomain needs to be added to the ViewArgs used within these routes as they are eventually used in more url_for
calls.
Third, the subdomain needs to be passed to the render function in these routes so that it is available in the templates.
These three changes are exemplified here (I have snipped out the majority of the code since it is exactly as the original implementation of the function):
class SubdomainModelView(ModelView):
@expose('/')
def index_view(self, subdomain): #(1)
...
#(2)
view_args = self._get_list_extra_args().clone(extra_args=dict(subdomain=subdomain))
...
return self.render(
...
subdomain=subdomain #(3)
)
These manages to get the list route of ModelViews to the render stage (with the same issue for the static files).
I was trying to override the https://github.com/flask-admin/flask-admin/blob/master/flask_admin/templates/bootstrap3/admin/static.html template, but I have been unable to.
I thought that having the replacement inside the app's template folder with relative path would be enough. In other words, I put the replacement at /templates/bootstrap/admin/static.html
, but it didn't work.
OK, I'll make small gist to illustrate it today (or tomorrow).
Here you go - https://gist.github.com/mrjoes/75dc5a1d65ce305406311e98abe1155b
Try it with: http://test.demo.dev:5000/ http://test.demo.dev:5000/admin/ http://foo.demo.dev:5000/
You can access current subdomain using subdomain
property that's provided by the mixin.
There's better way to send static files, but for purpose of demo method I used - works.
Also forgot to mention - please use current master from https://github.com/flask-admin/flask-admin/
I made small change in the static.html template - it now uses get_url
that's overridable instead of url_for
.
I am making a multi-tenant application that includes an Admin side for each tenant.
I added the
subdomain
config in the Admin initialization to capture the dynamic subdomain and added the parameter to each exposed route, like so:The problem is that at render time, in the templates (and likely elsewhere, too) where there are calls to
url_for
for static files (theadmin.static
endpoint), it fails because it is now expecting the subdomain parameter. The immediate Error I am getting is from aurl_for
in/usr/local/lib/python2.7/dist-packages/flask_admin/templates/bootstrap3/admin/static.html
:BuildError: Could not build url for endpoint 'admin.static' with values ['filename', 'v']. Did you forget to specify values ['subdomain']?
I assume there is a way to override the file mentioned (though not sure how), but I also assume that is probably not the only place where this would need to be fixed.
How could I handle this so for anywhere where it may happen?