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! ''' [[BR]] 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 [wiki:Dev/Technical/Flask/URL_Structure 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. `` 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: `. = 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