Flask makes a various things in Indico much nicer. Here's a quick overview of them.
URL Routing
You should not touch or even create *.py files in indico/htdocs. All of them obsolete, unused and will be removed eventually.
NOTE: For any new developments the following paragraph is not relevant!
Why are they still there? When integrating something from the era before Flask chances are good that something is added there. In this case add appropriate URL rules (with pretty URLs) for the new RHs and update the UHs to point to the new endpoint instead of the relativeURL. The bin/utils/generateLegacyBlueprint.py script can be used to create a legacy rule in the legacy blueprint. This will not have a pretty URL but you can copy&paste it into a proper blueprint and then update the URL rule. Running the script again afterwards should remove the entry from legacy.py assuming the endpoint name stayed the same.
Where to put Blueprints and URL rules?
Plugins
blueprint.py inside your plugin's package. The module name is not a requirement (any global holding an instance of IndicoBlueprint is loaded) but highly suggested. Note that you need to reload plugins after adding a new blueprint and restart your webserver afterwards (since the URL map is only loaded on startup)
Core
In the indico.web.flask.blueprints package. Check for a subpackage/module in there that fits your purpose. Unless you develop something totally new chances are good that this is the case. For example, anything that's part of the public view of an event is in indico.web.flask.blueprints.event.display (you can create a submodule in there though which uses the event blueprint). Event management is likewise in .event.management.
For *very* small things (such as the "about" page) there's no need to create a new blueprint either. Use the misc blueprint for them. But please don't abuse it and dump lots of things in there.
How to create a proper URL rule?
The URL should be pretty. Look at the already existing URL schema and at this wiki page for the logic behind the current structure. About trailing slashes: If /foo/ contains lots of things that will actually show up in the user's address bar, use the trailing slash. Otherwise just use /foo.
The first argument of add_url_rule() is the actual URL rule, prefixed with the blueprint's url_prefix if set. If you want to override that prefix, prefix your rule with ! (e.g. '!/foo/' instead of '/foo/'). Use this sparingly - if you have a new blueprint where you using it all the time either your URL structure is bad or you shouldn't be using url_prefix at all.
The second argument (the endpoint name) should be short but meaningful. Most existing endpoint names resemble the old *.py file structure since those endpoint names are used to generate the compatibility blueprint. You do not need to care about this for anything new - new endpoint names will not be taken into account by the compatibility rule generation.
The third argument is the view function. You can actually pass a RH directly - it'll be processed to be used as a view function automatically. Besides that, you can also pass a simple callable, e.g. generated by indico.web.flask.util.redirect_view. For details on how the argument is processed see indico.web.flask.util.make_view_func
The fourth argument, ALWAYS a keyword argument, is OPTIONAL. If a rule should only accept GET requests, simply omit it. If you want to support another HTTP verb such as POST specify a tuple containing the allowed verbs, e.g. methods=('GET', 'POST') or methods=('POST',) (don't forget the trailing comma). Always be specific: If the RH only modifies data you usually do not want to accept GET requests. However, if a RH contains separate process functions for GET and POST it's very appropriate (and actually necessary ;)) to allow both methods.
Building URLs
Import the url_for wrapper from indico.web.flask.util. The function's docstring pretty much explains everything you need to know but here are the basics:
The first argument is like in flask's native url_for, i.e. the endpoint. This is 'blueprintname.endpointname'. DO NOT use the '.endpointname' shortcut unless you put a very small view function in the same file as the Blueprint itself (you usually do not do this!). While it would usually work it greatly reduces the readability since you don't see immediately from which blueprint the endpoint is taken.
The second argument is optional and can be any object that has a getLocator() method. It automatically adds the dict returned by that method to the URL values.
Any other arguments are keyword arguments containing the URL values which either end up in the URL itself or in the query string. Some special ones are supported, too:
- _external - set this to True if you want an absolute URL e.g. for an email.
- _secure - set this to True or False if you want to enforce HTTPS or HTTP. Only works with _external=True
- _scheme - allows you to set the protocol manually. Does not work when _secure is present and should not be needed at all (unless you want to build e.g. an xmpp:// URL)
- _anchor - sets the anchor/fragment part of the URL, e.g. the part after the #
The url_for function is available in all templates without importing it.
If you want to build URLs using JavaScript, you can do so, too:
var url_template = ${ url_rule_to_js('blueprintname.endpoint') | j,n }; var url = build_url(url_template, {whatever: 'arguments', you: 'have'});
build_url() supports both the objects returned by url_rule_to_js and plain URLs. So you can use this function whenever you need to build/extend an URL with query string arguments. It is much cleaner than using string operations! When using it with a js_router it adds any argument not used in the URL rule to the query string. If you need a #anchor, you can specify it as a third argument.
URLHandlers (not needed for new stuff)
They now use _endpoint instead of _relativeURL. The syntax is _endpoint = 'blueprintname.endpointname', e.g. 'misc.about'.
If you need to build an URL in JavaScript and have an URLHandler you can use the js_router property of the class returned by the getURL method:
var url_template = ${ someUH.getURL().js_router | j,n }; var url = build_url(url_template, {whatever: 'arguments', you: 'have'});
See the previous section for details.
Request arguments (URI, GET, POST)
The _checkParams method of each RH still has a params argument because of legacy code that receives a version where all arguments have been merged and converted to UTF-8. Do not use this for new development! After from flask import request you can use request.view_args for URL arguments (e.g. <foo> in the URL rule gives you request.view_args['foo']), request.args for GET arguments and request.form for POST arguments. request.files contains all uploaded files.
view_args is a normal dict, the others are ImmutableMultiDict objects. This is usually not relevant - you can use them like normal dicts. The only case is if an argument is present more than once. In this case, whatever['key'] gives you only the first value; to get the list containing all of them use whatever.getlist('key'). This allows you to use the simple version in most cases where you don't expect/support multiple values and .getlist() in other cases where you *always* want a list, even if there's only one value present.
Remember to use .encode('utf-8') on the values you retrieve from those arrays unless a unicode object is fine or you never have any non-ASCII data in the argument (for a confId it wouldn't matter for example since those are always plain ASCII).
If an argument is required, you can simply read it using the whatever['key'] notation. If it does not exist a special KeyError (BadRequestKeyError) is raised. It will be shown to the user like a FormValuesError with the message Required argument missing: <key>.
HTTP Verbs in RHs (not final yet - to discuss)
Besides the normal _checkParams and _checkProtection methods which are always called a RH class MAY contain additional methods named e.g. _checkParams_POST or _checkProtection_GET which are invoked AFTER the normal method without any arguments. This allows you e.g. to check/require data that is only available/needed when the user submitted a form easily by putting it into _checkParams_POST.
The _process method behaves a bit different since it's pointless to call more than one in the same request. If a custom _process method exists only this one is called no matter which request method is used. If no such method exists the RH class MUST implement a _process_VERB for each supported method. If e.g. only _process_POST is implemented but a GET request is sent, it will fail with a HTTP 405 error. However, your routing rule MUST already specify the allowed methods anyway if anything but GET is allowed, so the 405 failing on the RH layer is just for your convenience during development.
Look at RHResetPasswordBase if you want an example.
Sending files to the client
While flask has its own send_file function we have a wrapper for it that performs some indico-specific tasks. Always use our function: from indico.web.flask.util import send_file
It is documented well in its docstring, but the most simple usage is this:
# Physical file send_file('foo.png', '/path/to/the/file', 'image/png') # Generated data from cStringIO import StringIO send_file('foo.pdf', StringIO(pdf_data), 'PDF')
Session
The session interface is much more comfortable now. All you need to do to use it is from flask import session. It has a dict-like interface which you SHOULD use. The session object tracks modification so if you modify it (e.g. by assigning or deleting a value) it will be automatically saved. However, a session can contain mutable objects. If you modify such an object you need to set session.modified = True or it won't be saved.
Example:
# setdefault only marks the session as modified if it actually modified it (i.e. if the key did not exist yet) selected_items = session.setdefault('selected_items', set()) selected_items.add(foo) session.modified = True
Another important thing to know is that the session is pickled and stored in Indico's cache backend (i.e. preferably redis). This means it is not stored in the ZODB which in turn means that you MUST NOT store a Persistent object in the session - otherwise loading the session will fail. Store the ID or Locator of the object instead and retrieve the database object when necessary.
The session contains some indico-specific properties. See indico.web.flask.session.IndicoSession for them. TLDR: session.user is the current user's Avatar or None; session.lang, session.timezone, session.csrf_protected, and session.csrf_token should be self-explanatory. Besides the CSRF ones all of them can be modified.
Please do not add your own properties to the session class unless you REALLY need to do so. The only reason to do so is something in the Indico core that needs getter/setter logic and thus cannot be cleanly realized using the dict interface. Make sure to read the comment right above the IndicoSession class.
Templates
Still Mako. Sorry if you hoped to read about Jinja2 here. ;)
The various Flask proxy objects are available in templates, prefixed with an underscore to avoid collisions with custom variables: _session, _request, _g, _app. This makes it easy to show e.g. a success message after a redirect:
% if 'fooDone' in _request.args: Foo has been done. % endif
However, we should consider using flask's flash() for this instead!
The functions url_for and url_rule_to_js are also available in all templates.
Miscellaneous things
- request.remote_addr takes the "Use Proxy" setting of Indico into account.
- app.debug uses the "Debug" setting from Indico