From 32f372b0a59f5fdab8838e139b08f17514c4bee7 Mon Sep 17 00:00:00 2001 From: Aurélien Bompard Date: Jan 27 2017 15:30:02 +0000 Subject: [PATCH 1/4] Add a way for plugins to register their own routes --- diff --git a/docs/dev-guide.rst b/docs/dev-guide.rst index 35b1871..a69004b 100644 --- a/docs/dev-guide.rst +++ b/docs/dev-guide.rst @@ -381,6 +381,42 @@ If you want to try making a new widget: Destroy your database, rebuild it, and re-run the app. Your widget should show up. +A widget may also register additional routes by declaring a ``ROUTES`` list in +the widget module. This list will contain dictionnaries that represent the +arguments passed to Flask's `add_url_rule +`_ function. +There are some changes from the basic ``add_url_rule`` function: + +- The view function will be passed the ``session`` and ``widget`` instances as + first arguments, and then the URL kwargs. +- The endpoint will be prefixed with ``_``, for example + ``meetings_``. Remember that when you want to reverse the URL with + ``url_for``. +- When reversing the URL, you need to pass the ``hub`` and ``idx`` kwargs. + +Example: consider this additional method that needs to be exported as a view +in the ``meetings`` plugin:: + + def search(session, widget, requester): + # Do something, probably using the "requester" variable. + return flask.jsonify({"hope_sources": ["Obi-Wan Kenobi"]}) + +It will be registered as a view using the following module variable:: + + ROUTES = [ + { + "rule": "search//", + "endpoint": "search", + "view_func": search, + "methods": ["GET", "POST"], # if needed + }, + ] + +And a valid call to ``url_for`` to reverse this URL would look like:: + + url_for("meetings_search", hub=widget.hub.name, idx=widget.idx, requester="Leia") + + A proposal, client-side templates ================================= diff --git a/hubs/app.py b/hubs/app.py index a2a48a4..1990f58 100755 --- a/hubs/app.py +++ b/hubs/app.py @@ -797,3 +797,25 @@ def plus_plus_update(user): return flask.jsonify(req.json()) else: return req.text, req.status_code + + +# +# Add widget-specific routes +# + +def widget_view_decorator(func): + @functools.wraps(func) + def inner(*args, **kwargs): + hubname = kwargs.pop("hub") + widgetidx = kwargs.pop("idx") + widget = get_widget(session, hubname, widgetidx) + return func(session, widget, *args, **kwargs) + return inner + +for widget in session.query(hubs.models.Widget): + for params in getattr(widget.module, 'ROUTES', []): + params["rule"] = "///widget/" \ + + params["rule"].lstrip("/") + params["endpoint"] = "%s_%s" % (widget.plugin, params["endpoint"]) + params["view_func"] = widget_view_decorator(params["view_func"]) + app.add_url_rule(**params) From 2b95bdb2ca6bde903df160a38f20d8ffc20028da Mon Sep 17 00:00:00 2001 From: Aurélien Bompard Date: Jan 27 2017 15:30:02 +0000 Subject: [PATCH 2/4] Fix typo --- diff --git a/docs/dev-guide.rst b/docs/dev-guide.rst index a69004b..25680ac 100644 --- a/docs/dev-guide.rst +++ b/docs/dev-guide.rst @@ -382,7 +382,7 @@ If you want to try making a new widget: Destroy your database, rebuild it, and re-run the app. Your widget should show up. A widget may also register additional routes by declaring a ``ROUTES`` list in -the widget module. This list will contain dictionnaries that represent the +the widget module. This list will contain dictionaries that represent the arguments passed to Flask's `add_url_rule `_ function. There are some changes from the basic ``add_url_rule`` function: From 921f4950e1c389de651b6d7d66a2cccea40deb1b Mon Sep 17 00:00:00 2001 From: Aurélien Bompard Date: Jan 27 2017 15:30:02 +0000 Subject: [PATCH 3/4] Switch to a decorator system for the widget routes It makes them much easier to declare. --- diff --git a/docs/dev-guide.rst b/docs/dev-guide.rst index 25680ac..1d742f5 100644 --- a/docs/dev-guide.rst +++ b/docs/dev-guide.rst @@ -381,11 +381,11 @@ If you want to try making a new widget: Destroy your database, rebuild it, and re-run the app. Your widget should show up. -A widget may also register additional routes by declaring a ``ROUTES`` list in -the widget module. This list will contain dictionaries that represent the -arguments passed to Flask's `add_url_rule -`_ function. -There are some changes from the basic ``add_url_rule`` function: +Widget-specific views +--------------------- +A widget may also register additional routes by using the +``hubs.widgets.base.widget_route`` decorator. There are some differences from +the usual ``route`` decorator or the ``add_url_rule`` function: - The view function will be passed the ``session`` and ``widget`` instances as first arguments, and then the URL kwargs. @@ -397,24 +397,25 @@ There are some changes from the basic ``add_url_rule`` function: Example: consider this additional method that needs to be exported as a view in the ``meetings`` plugin:: + from hubs.widgets.base import widget_route + @widget_route(rule="search//", methods=["GET", "POST"]) def search(session, widget, requester): # Do something, probably using the "requester" variable. return flask.jsonify({"hope_sources": ["Obi-Wan Kenobi"]}) -It will be registered as a view using the following module variable:: +Then, a valid call to ``url_for`` to reverse this URL would look like:: - ROUTES = [ - { - "rule": "search//", - "endpoint": "search", - "view_func": search, - "methods": ["GET", "POST"], # if needed - }, - ] + url_for("meetings_search", hub=widget.hub.name, idx=widget.idx, requester="Leia") -And a valid call to ``url_for`` to reverse this URL would look like:: +Behind the scenes, the ``widget_route`` decorator adds a ``ROUTES`` global +variable in the widget module, that contains dictionaries representing the +arguments passed to Flask's `add_url_rule +`_ function. - url_for("meetings_search", hub=widget.hub.name, idx=widget.idx, requester="Leia") +If you want more control, you can edit this ``ROUTES`` list directly, but there +are a difference from the basic ``add_url_rule`` function: the view function +name must be given with the dict key ``view_func_name``, and not ``view_func`` +as in the ``add_url_rule`` function. A proposal, client-side templates diff --git a/hubs/app.py b/hubs/app.py index 1990f58..07ef281 100755 --- a/hubs/app.py +++ b/hubs/app.py @@ -816,6 +816,9 @@ for widget in session.query(hubs.models.Widget): for params in getattr(widget.module, 'ROUTES', []): params["rule"] = "///widget/" \ + params["rule"].lstrip("/") + if not params.get("endpoint"): + params["endpoint"] = params["view_func_name"] params["endpoint"] = "%s_%s" % (widget.plugin, params["endpoint"]) - params["view_func"] = widget_view_decorator(params["view_func"]) + params["view_func"] = widget_view_decorator( + getattr(widget.module, params.pop("view_func_name"))) app.add_url_rule(**params) diff --git a/hubs/widgets/base.py b/hubs/widgets/base.py index 91d3931..bde7bb1 100755 --- a/hubs/widgets/base.py +++ b/hubs/widgets/base.py @@ -3,6 +3,7 @@ import datetime import functools import hashlib import json +import sys import dogpile.cache import flask @@ -96,3 +97,13 @@ def wraps(original): subsequent.widget_arguments = getattr(original, 'widget_arguments', []) return subsequent return decorator + + +def widget_route(**options): + def decorator(func): + options["view_func_name"] = func.__name__ + mdict = sys.modules[func.__module__].__dict__ + mroutes = mdict.setdefault('ROUTES', []) + mroutes.append(options) + return func + return decorator From f24845b5644fe59de24eb51f5908355b726a37e9 Mon Sep 17 00:00:00 2001 From: Aurélien Bompard Date: Jan 27 2017 15:30:02 +0000 Subject: [PATCH 4/4] Add docstrings --- diff --git a/docs/dev-guide.rst b/docs/dev-guide.rst index 1d742f5..715315b 100644 --- a/docs/dev-guide.rst +++ b/docs/dev-guide.rst @@ -387,8 +387,8 @@ A widget may also register additional routes by using the ``hubs.widgets.base.widget_route`` decorator. There are some differences from the usual ``route`` decorator or the ``add_url_rule`` function: -- The view function will be passed the ``session`` and ``widget`` instances as - first arguments, and then the URL kwargs. +- The view function will be passed the database ``session`` and ``widget`` + instances as first arguments, and then the URL kwargs. - The endpoint will be prefixed with ``_``, for example ``meetings_``. Remember that when you want to reverse the URL with ``url_for``. diff --git a/hubs/app.py b/hubs/app.py index 07ef281..f352afd 100755 --- a/hubs/app.py +++ b/hubs/app.py @@ -803,7 +803,14 @@ def plus_plus_update(user): # Add widget-specific routes # -def widget_view_decorator(func): +def _widget_view_decorator(func): + """ + This internal decorator will edit the view function arguments. + + It will: + - remove the hub name and the widget primary key + - add the database session and the widget instance + """ @functools.wraps(func) def inner(*args, **kwargs): hubname = kwargs.pop("hub") @@ -819,6 +826,6 @@ for widget in session.query(hubs.models.Widget): if not params.get("endpoint"): params["endpoint"] = params["view_func_name"] params["endpoint"] = "%s_%s" % (widget.plugin, params["endpoint"]) - params["view_func"] = widget_view_decorator( + params["view_func"] = _widget_view_decorator( getattr(widget.module, params.pop("view_func_name"))) app.add_url_rule(**params) diff --git a/hubs/widgets/base.py b/hubs/widgets/base.py index bde7bb1..f3cf198 100755 --- a/hubs/widgets/base.py +++ b/hubs/widgets/base.py @@ -100,6 +100,12 @@ def wraps(original): def widget_route(**options): + """Register a view for the current widget. + + This decorator can be used to expose a specific function below a widget's + URL endpoint. Refer to the "Widget-specific views" section in the + documentation to learn how to construct the corresponding URL. + """ def decorator(func): options["view_func_name"] = func.__name__ mdict = sys.modules[func.__module__].__dict__