From def768a3a816e8bc1fc10eae4f9e83b886d07f12 Mon Sep 17 00:00:00 2001 From: Aurélien Bompard Date: Jan 31 2017 18:25:49 +0000 Subject: [PATCH 1/3] Split off the views in their own module Fixes #305 --- diff --git a/hubs/app.py b/hubs/app.py index 89b5271..006edea 100755 --- a/hubs/app.py +++ b/hubs/app.py @@ -1,23 +1,17 @@ -from __future__ import print_function, unicode_literals +from __future__ import unicode_literals import datetime -import functools import json import logging import os -import six -from six.moves.urllib import parse as urlparse import flask import flask.json import munch from flask.ext.oidc import OpenIDConnect -import requests import hubs.models -import hubs.widgets -import hubs.stream import datanommer.models @@ -36,11 +30,6 @@ app.template_filter('avatar')(username2avatar) logging.basicConfig() -# TODO - put this in config so we can migrate to pagure -# TODO - instead of 'develop', use the version from pkg_resources to figure out -# the right tag to link people to. AGPL ftw. -SOURCE_URL = 'https://pagure.io/fedora-hubs/blob/develop/f' # /hubs/widgets/badges.py' - app.config.from_object('hubs.default_config') if 'HUBS_CONFIG' in os.environ: app.config.from_envvar('HUBS_CONFIG') @@ -54,6 +43,10 @@ fedmsg.meta.make_processors(**fedmsg_config) session = hubs.models.init(fedmsg_config['hubs.sqlalchemy.uri']) datanommer.models.init(fedmsg_config['datanommer.sqlalchemy.uri']) +@app.before_request +def store_db(): + flask.g.db = session + class CustomJSONEncoder(flask.json.JSONEncoder): def default(self, o): @@ -69,24 +62,6 @@ class CustomJSONEncoder(flask.json.JSONEncoder): app.json_encoder = CustomJSONEncoder -def authenticated(): - """ Utility function checking if the current auth is set or not.""" - return hasattr(flask.g, 'auth') \ - and flask.g.auth is not None \ - and flask.g.auth.logged_in - - -def is_safe_url(target): - """ Checks that the target url is safe and sending to the current - website not some other malicious one. - """ - ref_url = urlparse.urlparse(flask.request.host_url) - test_url = urlparse.urlparse( - urlparse.urljoin(flask.request.host_url, target)) - return test_url.scheme in ('http', 'https') and \ - ref_url.netloc == test_url.netloc - - @app.template_filter('commas') def commas(numeric): return "{:,.2f}".format(numeric) @@ -98,490 +73,10 @@ def shutdown_session(exception=None): session.remove() -@app.route('/') -def index(): - if not authenticated(): - return flask.redirect(flask.url_for('login')) - - return flask.redirect(flask.url_for('stream', name=flask.g.auth.nickname)) - - -@app.route('/groups') -def groups(): - if not authenticated(): - return flask.redirect(flask.url_for('login')) - - # Get the list of promoted and non-promoted group hubs from the DB - promoted_names = app.config.get('PROMOTED_GROUPS') - groups = hubs.models.Hub.all_group_hubs(session) - promoted = [g for g in groups if g.name in promoted_names] - secondary = [g for g in groups if g.name not in promoted_names] - - name_of_the_month = app.config.get('HUB_OF_THE_MONTH') - hub_of_the_month = hubs.models.Hub.by_name(session, name_of_the_month) - - return flask.render_template( - 'groups.html', - promoted=promoted, - secondary=secondary, - hub_of_the_month=hub_of_the_month, - session=session, - ) - - -@app.route('/') -@app.route('//') -def hub(name): - hub = get_hub(session, name) - return flask.render_template( - 'hubs.html', hub=hub, session=session, edit=False) - - -@app.route('//json/') -@app.route('//json') -def hub_json(name): - hub = get_hub(session, name) - response = flask.jsonify(hub.__json__(session)) - # TODO -- modify headers with response.headers['X-fedora-hubs-wat'] = 'foo' - return response - - -@app.route('//edit/', methods=['GET', 'POST']) -@app.route('//edit', methods=['GET', 'POST']) -def hub_edit(name): - if flask.request.method == 'POST': - return hub_edit_post(name) - else: - return hub_edit_get(name) - - -def hub_edit_get(name): - hub = get_hub(session, name) - return flask.render_template( - 'hubs.html', hub=hub, session=session, edit=True) - - -def hub_edit_post(name): - hub = get_hub(session, name) - is_js = flask.request.form.get('js', False) - - error = False - - # Check the input submitted - # Right side - r_widget_ids = [ - w.strip().replace('widget-', '') - for w in flask.request.form.getlist('right_widgets[]') - if w.strip() - ] - try: - r_widget_ids = [int(w) for w in r_widget_ids] - except: - flask.flash('Invalid widget identifiers submitted', 'error') - error = True - - r_indexes = [ - i.strip() for i in flask.request.form.getlist('right_indexes[]') - if i.strip() - ] - - try: - r_indexes = [int(i) for i in r_indexes] - except: - if not is_js: - flask.flash('Invalid indexes submitted', 'error') - error = True - - if len(r_widget_ids) != len(r_indexes): - if not is_js: - flask.flash( - 'The number of indexes and the number of widgets are not of ' - 'the same length', 'error') - error = True - - # Left side - l_widget_ids = [ - w.strip().replace('widget-', '') - for w in flask.request.form.getlist('left_widgets[]') - if w.strip() - ] - try: - l_widget_ids = [int(w) for w in l_widget_ids] - except: - flask.flash('Invalid widget identifiers submitted', 'error') - error = True - - l_indexes = [ - i.strip() for i in flask.request.form.getlist('left_indexes[]') - if i.strip() - ] - - try: - l_indexes = [int(i) for i in l_indexes] - except: - if not is_js: - flask.flash('Invalid indexes submitted', 'error') - error = True - - if len(l_widget_ids) != len(l_indexes): - if not is_js: - flask.flash( - 'The number of indexes and the number of widgets are not of ' - 'the same length', 'error') - error = True - - # If all good, update the database - if not error: - for cnt, wid in enumerate(r_widget_ids): - widget = hubs.models.Widget.get(session, wid) - if widget.index != r_indexes[cnt]: - widget.index = r_indexes[cnt] - session.add(widget) - for cnt, wid in enumerate(l_widget_ids): - widget = hubs.models.Widget.get(session, wid) - if widget.index != l_indexes[cnt]: - widget.index = l_indexes[cnt] - session.add(widget) - hub.last_edited = datetime.datetime.utcnow() - session.add(hub) - try: - session.commit() - except Exception as err: - error = True - if not is_js: - flask.flash( - 'Could not save your changes to the database ' - 'if the error persists, please warn an admin', - 'error') - if is_js: - if error: - flask.abort(400) - else: - return ('ok', 200) - else: - return flask.redirect(flask.url_for('hub', name=name)) - - -@app.route('//add/', methods=['GET', 'POST']) -@app.route('//add', methods=['GET', 'POST']) -def hub_add_widgets(name): - if flask.request.method == 'POST': - return hub_add_widget_post(name) - else: - return hub_add_widget_get(name) - - -@app.route('//add//', methods=['GET']) -@app.route('//add/', methods=['GET']) -def hub_add_widget(name, widget_name): - hub = get_hub(session, name) - side = str(flask.request.args.get('position')).lower() - if side not in ['right', 'left']: - flask.abort(400, 'Invalid position provided') - - widget = None - w_name = None - for widgt in hubs.widgets.registry: - if hubs.widgets.registry[widgt].position in ['both', side] \ - and widgt == widget_name: - w_name = widgt - widget = hubs.widgets.registry[widgt] - - return flask.render_template( - 'add_widget.html', - hub=hub, - widget=widget, - widget_name=w_name, - side=side, - url_to=flask.url_for('hub_add_widgets', name=name), - ) - - -def hub_add_widget_get(name): - hub = get_hub(session, name) - side = str(flask.request.args.get('position')).lower() - if side not in ['right', 'left']: - flask.abort(400, 'Invalid position provided') - - widgets = [ - widget - for widget in hubs.widgets.registry - if hubs.widgets.registry[widget].position in ['both', side] - ] - return flask.render_template( - 'add_widget.html', - hub=hub, - widgets=widgets, - side=side, - ) - - -def hub_add_widget_post(name): - widget_name = flask.request.form.get('widget_name') - position = flask.request.form.get('position', '').lower() - if not widget_name: - flask.abort(400, 'Invalid request sent') - if widget_name not in hubs.widgets.registry: - flask.abort(404, 'Unknown widget called') - - hub = get_hub(session, name) - - widget = widget = hubs.models.Widget( - plugin=widget_name, index=-1, left=position == 'left') - error = False - config = {} - for arg in widget.module.data.widget_arguments: - val = flask.request.form.get(arg.name) - if not val: - flask.flash( - 'You must provide a value for: %s' % arg.name, 'error') - error = True - break - try: - arg.validator(session, val) - config[arg.name] = val - except Exception as err: - flask.flash('Invalid data provided, error: %s' % err, 'error') - error = True - if not error: - widget.hub = hub - widget.config = config - try: - session.add(widget) - session.flush() - widget.hub.last_edited = datetime.datetime.utcnow() - session.commit() - except Exception as err: - print(err) - flask.flash( - 'Could not save the configuration to the database ' - 'if the error persists, please warn an admin', - 'error') - - return flask.render_template( - 'hubs.html', hub=hub, session=session, edit=True) - - -@app.route('///') -@app.route('//') -def widget_render(hub, idx): - widget = get_widget(session, hub, idx) - return widget.render(session) # , edit=False) - # was blocking all widgets from working, sorry! - - -@app.route('///json') -@app.route('///json/') -def widget_json(hub, idx): - widget = get_widget(session, hub, idx) - response = flask.jsonify(widget.__json__(session)) - # TODO -- modify headers with response.headers['X-fedora-hubs-wat'] = 'foo' - return response - - -@app.route('///edit/', methods=['GET', 'POST']) -@app.route('///edit', methods=['GET', 'POST']) -def widget_edit(hub, idx): - if flask.request.method == 'POST': - return widget_edit_post(hub, idx) - else: - return widget_edit_get(hub, idx) - - -def widget_edit_get(hub, idx): - widget = get_widget(session, hub, idx) - return flask.render_template( - 'edit.html', - hub=hub, - widget=widget, - url_to=flask.url_for('widget_edit', hub=hub, idx=idx) - ) - - -def widget_edit_post(hub, idx): - widget = get_widget(session, hub, idx) - error = False - config = {} - for arg in widget.module.data.widget_arguments: - val = flask.request.form.get(arg.name) - if not val: - flask.flash( - 'You must provide a value for: %s' % arg.name, 'error') - error = True - break - try: - val = arg.validator(session, val) - config[arg.name] = val - except Exception as err: - flask.flash('Invalid data provided, error: %s' % err, 'error') - error = True - if not error: - cur_config = widget.config - cur_config.update(config) - widget.config = cur_config - widget.hub.last_edited = datetime.datetime.utcnow() - session.add(widget) - try: - session.commit() - except Exception as err: - flask.flash( - 'Could not save the configuration to the database ' - 'if the error persists, please warn an admin', - 'error') - return flask.redirect(flask.url_for('hub', name=hub)) - - -@app.route('///delete/', methods=['POST']) -@app.route('///delete', methods=['POST']) -def widget_edit_delete(hub, idx): - ''' Remove a widget from a hub. ''' - widget = get_widget(session, hub, idx) - session.delete(widget) - try: - session.commit() - except Exception as err: - flask.flash( - 'Could not delete this widget from this hub in the database ' - 'if the error persists, please warn an admin', - 'error') - return flask.redirect(flask.url_for('hub_edit', name=hub)) - - -@app.route('/source//') -@app.route('/source/') -def widget_source(name): - from hubs.widgets import registry - base = '/hubs/' - fname = '' - - try: - fname = base + registry[name].__file__.split(base)[-1] - except KeyError: - flask.abort(404) - - fname = fname.replace('.pyc', '.py') - return flask.redirect(SOURCE_URL + fname) - - # Set up OpenIDConnect OIDC = OpenIDConnect(app, credentials_store=flask.session ) -@app.route('/login/', methods=('GET', 'POST')) -@app.route('/login', methods=('GET', 'POST')) -@OIDC.require_login -def login(): - default = flask.url_for('index') - next_url = flask.request.args.get('next', default) - if is_safe_url(next_url): - return_point = next_url - else: - return_point = default - - hubs.models.User.get_or_create( - session, username=flask.g.auth.nickname, fullname=flask.g.auth.fullname) - - return flask.redirect(return_point) - - -@app.route('/logout/') -@app.route('/logout') -def logout(): - next_url = flask.url_for('index') - if 'next' in flask.request.values: # pragma: no cover - if is_safe_url(flask.request.args['next']): - next_url = flask.request.values['next'] - - if next_url == flask.url_for('login'): # pragma: no cover - next_url = flask.url_for('index') - - if authenticated(): - OIDC.logout() - flask.session.auth = None - flask.flash('You have been logged out') - - return flask.redirect(next_url) - - -def login_required(function): - """ Flask decorator to restrict access to logged-in users. """ - - @functools.wraps(function) - def decorated_function(*args, **kwargs): - """ Decorated function, actually does the work. """ - if not authenticated(): - flask.flash('Login required', 'errors') - return flask.redirect(flask.url_for( - 'login', next=flask.request.url)) - - return function(*args, **kwargs) - - return decorated_function - -@app.route('//stream') -@app.route('//stream/') -@login_required -def stream(name): - hub = get_hub(session, name) - saved = hubs.models.SavedNotification.by_username(session, name) - saved = [n.__json__() for n in saved] - - stream = hubs.stream.Stream() - actions = stream.get_json() - - return flask.render_template( - 'stream.html', - hub=hub, - saved=json.dumps(saved), - actions=actions - ) - - -@app.route('//notifications', methods=['GET', 'POST']) -@app.route('//notifications/', methods=['GET', 'POST']) -@login_required -def notifications(user): - if flask.request.method == 'GET': - notifications = hubs.models.SavedNotification.by_username(session, user) - notifications = [n.__json__() for n in notifications] - return flask.jsonify(notifications) - - if flask.request.method == 'POST': - data = flask.request.get_json() - user = hubs.models.User.by_username(session, user) - if not user: - return flask.abort(400) - try: - markup = data['markup'] - link = data['link'] - icon = data['secondary_icon'] - dom_id = data['dom_id'] - except: - return flask.abort(400) - notification = hubs.models.SavedNotification( - username=user.username, - markup=markup, - link=link, - secondary_icon=icon, - dom_id=dom_id - ) - session.add(notification) - session.commit() - return flask.jsonify( - dict(notification=notification.__json__(), success=True) - ) - -@app.route('//notifications/', methods=['DELETE']) -@app.route('//notifications//', methods=['DELETE']) -@login_required -def delete_notifications(user, idx): - notification = session.query(hubs.models.SavedNotification).filter_by(idx=idx).first() - if not notification: - return flask.abort(400) - session.delete(notification) - session.commit() - return flask.jsonify(dict(status_code=200)) - @app.before_request def check_auth(): flask.session.permanent = True @@ -610,223 +105,4 @@ def check_auth(): flask.g.auth = munch.Munch(logged_in=False) -def get_hub(session, name): - """ Utility shorthand to get a hub and 404 if not found. """ - hub = session.query(hubs.models.Hub) \ - .filter(hubs.models.Hub.name == name) \ - .first() - - if not hub: - flask.abort(404) - - return hub - - -@app.route('/visit//', methods=['GET', 'POST']) -@app.route('/visit/', methods=['GET', 'POST']) -@login_required -def increment_counter(visited_hub): - nickname = flask.g.auth.nickname - - if str(visited_hub) != str(nickname): - try: - vc = hubs.models.VisitCounter.get_or_create(session=session, - username=nickname, - visited_hub=visited_hub) - except ValueError: # this should never trip - # flask will 405 if visited_hub is blank - # @login_required forces flask.g.auth to be sets - flask.abort(404) - if flask.request.method == 'POST': - vc.increment_visits(session=session, - username=nickname, - visited_hub=visited_hub) - - return flask.jsonify({'count': vc.count}) - - elif flask.request.method == 'GET': - return flask.jsonify({'count': vc.count}) - else: - return flask.abort(403) - - -def get_widget(session, hub, idx): - """ Utility shorthand to get a widget and 404 if not found. """ - try: - idx = int(idx) - except (TypeError, ValueError): - flask.abort(404) - - hub = get_hub(session, hub) - - for widget in hub.widgets: - if widget.idx == idx: - return widget - - flask.abort(404) - - -# Here are a bunch of API methods that should probably be broken out into -# their own file -@app.route('/api/hub//subscribe', methods=['POST']) -@login_required -def hub_subscribe(hub): - hub = get_hub(session, hub) - user = hubs.models.User.by_username(session, flask.g.auth.nickname) - hub.subscribe(session, user) - session.commit() - return flask.redirect(flask.url_for('hub', name=hub.name)) - - -@app.route('/api/hub//unsubscribe', methods=['POST']) -@login_required -def hub_unsubscribe(hub): - hub = get_hub(session, hub) - user = hubs.models.User.by_username(session, flask.g.auth.nickname) - try: - hub.unsubscribe(session, user) - except KeyError: - return flask.abort(400) - session.commit() - return flask.redirect(flask.url_for('hub', name=hub.name)) - - -@app.route('/api/hub//star', methods=['POST']) -@login_required -def hub_star(hub): - hub = get_hub(session, hub) - user = hubs.models.User.by_username(session, flask.g.auth.nickname) - hub.subscribe(session, user, role='stargazer') - session.commit() - return flask.redirect(flask.url_for('hub', name=hub.name)) - - -@app.route('/api/hub//unstar', methods=['POST']) -@login_required -def hub_unstar(hub): - hub = get_hub(session, hub) - user = hubs.models.User.by_username(session, flask.g.auth.nickname) - try: - hub.unsubscribe(session, user, role='stargazer') - except KeyError: - return flask.abort(400) - session.commit() - return flask.redirect(flask.url_for('hub', name=hub.name)) - - -@app.route('/api/hub//join', methods=['POST']) -@login_required -def hub_join(hub): - hub = get_hub(session, hub) - user = hubs.models.User.by_username(session, flask.g.auth.nickname) - hub.subscribe(session, user, role='member') - session.commit() - return flask.redirect(flask.url_for('hub', name=hub.name)) - - -@app.route('/api/hub//leave', methods=['POST']) -@login_required -def hub_leave(hub): - hub = get_hub(session, hub) - user = hubs.models.User.by_username(session, flask.g.auth.nickname) - try: - hub.unsubscribe(session, user, role='member') - except KeyError: - return flask.abort(400) - session.commit() - return flask.redirect(flask.url_for('hub', name=hub.name)) - - -@app.route('/plus_plus//status', methods=['GET']) -def plus_plus_status(user): - receiver = hubs.models.User.by_username(session, user) - if not receiver: - return 'User does not exist', 403 - pp_url = app.config['PLUS_PLUS_URL'] + str(receiver.username) - req = None - try: - req = requests.get(pp_url) - except: - flask.abort(500) - if req.status_code == 200: - return flask.jsonify(req.json()) - else: - return req.text, req.status_code - - -def plus_plus_update_bool_helper(val): - if isinstance(val, bool): - return val - elif isinstance(val, six.string_types): - fmt_str = str(val).replace("'", "").replace('"', '').lower() - return fmt_str in ("yes", "true", "t", "1") - else: - raise ValueError - - -@app.route('/plus_plus//update', methods=['POST']) -@login_required -def plus_plus_update(user): - receiver = hubs.models.User.by_username(session, user) - - if not receiver: - return 'User does not exist', 403 - - if user == flask.g.auth.nickname: - return 'You may not modify your own karma.', 403 - - if 'decrement' not in flask.request.form \ - and 'increment' not in flask.request.form: - return "You must set 'decrement' or 'increment' " \ - "with a boolean value in the body", 403 - - update = 'increment' if 'increment' in flask.request.form else 'decrement' - - update_bool_val = plus_plus_update_bool_helper(flask.request.form[update]) - pp_url = app.config['PLUS_PLUS_URL'] + str(receiver.username) - sender = hubs.models.User.by_username(session, flask.g.auth.nickname) - pp_token = app.config['PLUS_PLUS_TOKEN'] - auth_header = {'Authorization': 'token {}'.format(pp_token)} - data = {'sender': sender.username, update: update_bool_val} - req = None - try: - req = requests.post(url=pp_url, headers=auth_header, data=data) - except: - flask.abort(500) - - if req.status_code == 200: - return flask.jsonify(req.json()) - else: - return req.text, req.status_code - - -# -# Add widget-specific routes -# - -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") - 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("/") - 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( - getattr(widget.module, params.pop("view_func_name"))) - app.add_url_rule(**params) +import hubs.views diff --git a/hubs/default_config.py b/hubs/default_config.py index 769f8ad..085ec5f 100755 --- a/hubs/default_config.py +++ b/hubs/default_config.py @@ -24,3 +24,7 @@ OIDC_ID_TOKEN_COOKIE_SECURE = True OIDC_REQUIRE_VERIFIED_EMAIL = False OIDC_OPENID_REALM = 'http://localhost:5000/oidc_callback' OIDC_SCOPES = ['openid', 'email', 'profile', 'fedora'] + +# TODO - instead of 'develop', use the version from pkg_resources to figure out +# the right tag to link people to. AGPL ftw. +SOURCE_URL = 'https://pagure.io/fedora-hubs/blob/develop/f' # /hubs/widgets/badges.py' diff --git a/hubs/tests/__init__.py b/hubs/tests/__init__.py index 7f8aa8c..b1e182f 100644 --- a/hubs/tests/__init__.py +++ b/hubs/tests/__init__.py @@ -33,6 +33,7 @@ class APPTest(unittest.TestCase): hubs.app.fedmsg_config['hubs.sqlalchemy.uri'], create=True, ) + hubs.app.app.config['TESTING'] = True self.app = hubs.app.app.test_client() self.app.testing = True self.session = hubs.app.session @@ -106,11 +107,13 @@ def auth_set(APP, auth): :param auth: A FakeAuthorization instance to set as g.auth. """ - # Hack used to remove the before_request function set by - # flask.ext.fas_openid.FAS which otherwise kills our effort to set a - # flask.g.auth. + # Hack used to remove the check_auth function set on `before_request` + # which otherwise kills our effort to set a flask.g.auth. from flask import appcontext_pushed, g - APP.before_request_funcs[None] = [] + APP.before_request_funcs[None] = [ + f for f in APP.before_request_funcs[None] + if f.__name__ != 'check_auth' + ] def handler(sender, **kwargs): g.auth = auth diff --git a/hubs/tests/test_api/__init__.py b/hubs/tests/test_api/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/hubs/tests/test_api/__init__.py diff --git a/hubs/views/__init__.py b/hubs/views/__init__.py new file mode 100644 index 0000000..13fb79c --- /dev/null +++ b/hubs/views/__init__.py @@ -0,0 +1,46 @@ +from __future__ import absolute_import + +from .root import * +from .hub import * +from .widget import * +from .user import * +from .api import * +from .plus_plus import * + +# +# Add widget-specific routes +# + +import flask +import functools +import hubs.models +from sqlalchemy.orm.exc import NoResultFound +from hubs.app import app, session +from .utils import get_widget + +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") + widgetidx = kwargs.pop("idx") + widget = get_widget(hubname, widgetidx, session=session) + 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("/") + 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( + getattr(widget.module, params.pop("view_func_name"))) + app.add_url_rule(**params) diff --git a/hubs/views/api.py b/hubs/views/api.py new file mode 100644 index 0000000..18ca0ba --- /dev/null +++ b/hubs/views/api.py @@ -0,0 +1,78 @@ +from __future__ import unicode_literals, absolute_import + + +import flask +import hubs.models + +from hubs.app import app +from .utils import get_hub, login_required + + +@app.route('/api/hub//subscribe', methods=['POST']) +@login_required +def hub_subscribe(hub): + hub = get_hub(hub) + user = hubs.models.User.by_username(flask.g.db, flask.g.auth.nickname) + hub.subscribe(flask.g.db, user) + flask.g.db.commit() + return flask.redirect(flask.url_for('hub', name=hub.name)) + + +@app.route('/api/hub//unsubscribe', methods=['POST']) +@login_required +def hub_unsubscribe(hub): + hub = get_hub(hub) + user = hubs.models.User.by_username(flask.g.db, flask.g.auth.nickname) + try: + hub.unsubscribe(flask.g.db, user) + except KeyError: + return flask.abort(400) + flask.g.db.commit() + return flask.redirect(flask.url_for('hub', name=hub.name)) + + +@app.route('/api/hub//star', methods=['POST']) +@login_required +def hub_star(hub): + hub = get_hub(hub) + user = hubs.models.User.by_username(flask.g.db, flask.g.auth.nickname) + hub.subscribe(flask.g.db, user, role='stargazer') + flask.g.db.commit() + return flask.redirect(flask.url_for('hub', name=hub.name)) + + +@app.route('/api/hub//unstar', methods=['POST']) +@login_required +def hub_unstar(hub): + hub = get_hub(hub) + user = hubs.models.User.by_username(flask.g.db, flask.g.auth.nickname) + try: + hub.unsubscribe(flask.g.db, user, role='stargazer') + except KeyError: + return flask.abort(400) + flask.g.db.commit() + return flask.redirect(flask.url_for('hub', name=hub.name)) + + +@app.route('/api/hub//join', methods=['POST']) +@login_required +def hub_join(hub): + hub = get_hub(hub) + user = hubs.models.User.by_username(flask.g.db, flask.g.auth.nickname) + hub.subscribe(flask.g.db, user, role='member') + flask.g.db.commit() + return flask.redirect(flask.url_for('hub', name=hub.name)) + + +@app.route('/api/hub//leave', methods=['POST']) +@login_required +def hub_leave(hub): + hub = get_hub(hub) + user = hubs.models.User.by_username(flask.g.db, flask.g.auth.nickname) + try: + hub.unsubscribe(flask.g.db, user, role='member') + except KeyError: + return flask.abort(400) + flask.g.db.commit() + return flask.redirect(flask.url_for('hub', name=hub.name)) + diff --git a/hubs/views/hub.py b/hubs/views/hub.py new file mode 100644 index 0000000..12b75e9 --- /dev/null +++ b/hubs/views/hub.py @@ -0,0 +1,241 @@ +from __future__ import unicode_literals, absolute_import + +import datetime +import flask +import hubs.models + +from hubs.app import app, session +from .utils import get_hub + + +@app.route('/') +@app.route('//') +def hub(name): + hub = get_hub(name) + return flask.render_template( + 'hubs.html', hub=hub, edit=False) + + +@app.route('//json/') +@app.route('//json') +def hub_json(name): + hub = get_hub(name) + response = flask.jsonify(hub.__json__(flask.g.db)) + # TODO -- modify headers with response.headers['X-fedora-hubs-wat'] = 'foo' + return response + + +@app.route('//edit/', methods=['GET', 'POST']) +@app.route('//edit', methods=['GET', 'POST']) +def hub_edit(name): + if flask.request.method == 'POST': + return hub_edit_post(name) + else: + return hub_edit_get(name) + + +def hub_edit_get(name): + hub = get_hub(name) + return flask.render_template( + 'hubs.html', hub=hub, edit=True) + + +def hub_edit_post(name): + hub = get_hub(name) + is_js = flask.request.form.get('js', False) + + error = False + + # Check the input submitted + # Right side + r_widget_ids = [ + w.strip().replace('widget-', '') + for w in flask.request.form.getlist('right_widgets[]') + if w.strip() + ] + try: + r_widget_ids = [int(w) for w in r_widget_ids] + except: + flask.flash('Invalid widget identifiers submitted', 'error') + error = True + + r_indexes = [ + i.strip() for i in flask.request.form.getlist('right_indexes[]') + if i.strip() + ] + + try: + r_indexes = [int(i) for i in r_indexes] + except: + if not is_js: + flask.flash('Invalid indexes submitted', 'error') + error = True + + if len(r_widget_ids) != len(r_indexes): + if not is_js: + flask.flash( + 'The number of indexes and the number of widgets are not of ' + 'the same length', 'error') + error = True + + # Left side + l_widget_ids = [ + w.strip().replace('widget-', '') + for w in flask.request.form.getlist('left_widgets[]') + if w.strip() + ] + try: + l_widget_ids = [int(w) for w in l_widget_ids] + except: + flask.flash('Invalid widget identifiers submitted', 'error') + error = True + + l_indexes = [ + i.strip() for i in flask.request.form.getlist('left_indexes[]') + if i.strip() + ] + + try: + l_indexes = [int(i) for i in l_indexes] + except: + if not is_js: + flask.flash('Invalid indexes submitted', 'error') + error = True + + if len(l_widget_ids) != len(l_indexes): + if not is_js: + flask.flash( + 'The number of indexes and the number of widgets are not of ' + 'the same length', 'error') + error = True + + # If all good, update the database + if not error: + for cnt, wid in enumerate(r_widget_ids): + widget = flask.g.db.query(hubs.models.Widget).get(wid) + if widget.index != r_indexes[cnt]: + widget.index = r_indexes[cnt] + flask.g.db.add(widget) + for cnt, wid in enumerate(l_widget_ids): + widget = flask.g.db.query(hubs.models.Widget).get(wid) + if widget.index != l_indexes[cnt]: + widget.index = l_indexes[cnt] + flask.g.db.add(widget) + hub.last_edited = datetime.datetime.utcnow() + flask.g.db.add(hub) + try: + flask.g.db.commit() + except Exception as err: + error = True + if not is_js: + flask.flash( + 'Could not save your changes to the database ' + 'if the error persists, please warn an admin', + 'error') + if is_js: + if error: + flask.abort(400) + else: + return ('ok', 200) + else: + return flask.redirect(flask.url_for('hub', name=name)) + + +@app.route('//add/', methods=['GET', 'POST']) +@app.route('//add', methods=['GET', 'POST']) +def hub_add_widgets(name): + if flask.request.method == 'POST': + return hub_add_widget_post(name) + else: + return hub_add_widget_get(name) + + +@app.route('//add//', methods=['GET']) +@app.route('//add/', methods=['GET']) +def hub_add_widget(name, widget_name): + hub = get_hub(name) + side = str(flask.request.args.get('position')).lower() + if side not in ['right', 'left']: + flask.abort(400, 'Invalid position provided') + + widget = None + w_name = None + for widgt in hubs.widgets.registry: + if hubs.widgets.registry[widgt].position in ['both', side] \ + and widgt == widget_name: + w_name = widgt + widget = hubs.widgets.registry[widgt] + + return flask.render_template( + 'add_widget.html', + hub=hub, + widget=widget, + widget_name=w_name, + side=side, + url_to=flask.url_for('hub_add_widgets', name=name), + ) + + +def hub_add_widget_get(name): + hub = get_hub(name) + side = str(flask.request.args.get('position')).lower() + if side not in ['right', 'left']: + flask.abort(400, 'Invalid position provided') + + widgets = [ + widget + for widget in hubs.widgets.registry + if hubs.widgets.registry[widget].position in ['both', side] + ] + return flask.render_template( + 'add_widget.html', + hub=hub, + widgets=widgets, + side=side, + ) + + +def hub_add_widget_post(name): + widget_name = flask.request.form.get('widget_name') + position = flask.request.form.get('position', '').lower() + if not widget_name: + flask.abort(400, 'Invalid request sent') + if widget_name not in hubs.widgets.registry: + flask.abort(404, 'Unknown widget called') + + hub = get_hub(name) + + widget = widget = hubs.models.Widget( + plugin=widget_name, index=-1, left=position == 'left') + error = False + config = {} + for arg in widget.module.data.widget_arguments: + val = flask.request.form.get(arg.name) + if not val: + flask.flash( + 'You must provide a value for: %s' % arg.name, 'error') + error = True + break + try: + arg.validator(flask.g.db, val) + config[arg.name] = val + except Exception as err: + flask.flash('Invalid data provided, error: %s' % err, 'error') + error = True + if not error: + widget.hub = hub + widget.config = config + try: + flask.g.db.add(widget) + flask.g.db.flush() + widget.hub.last_edited = datetime.datetime.utcnow() + flask.g.db.commit() + except Exception as err: + print(err) + flask.flash( + 'Could not save the configuration to the database ' + 'if the error persists, please warn an admin', + 'error') + + return flask.render_template( + 'hubs.html', hub=hub, edit=True) diff --git a/hubs/views/plus_plus.py b/hubs/views/plus_plus.py new file mode 100644 index 0000000..266c2ea --- /dev/null +++ b/hubs/views/plus_plus.py @@ -0,0 +1,77 @@ +from __future__ import unicode_literals, absolute_import + +import flask +import requests +import six +import hubs.models + +from hubs.app import app, session +from .utils import login_required + + +# These views should be converted to widget-specific views, now that we can do +# that. + + +@app.route('/plus_plus//status', methods=['GET']) +def plus_plus_status(user): + receiver = hubs.models.User.by_username(flask.g.db, user) + if not receiver: + return 'User does not exist', 403 + pp_url = app.config['PLUS_PLUS_URL'] + str(receiver.username) + req = None + try: + req = requests.get(pp_url) + except: + flask.abort(500) + if req.status_code == 200: + return flask.jsonify(req.json()) + else: + return req.text, req.status_code + + +def plus_plus_update_bool_helper(val): + if isinstance(val, bool): + return val + elif isinstance(val, six.string_types): + fmt_str = str(val).replace("'", "").replace('"', '').lower() + return fmt_str in ("yes", "true", "t", "1") + else: + raise ValueError + + +@app.route('/plus_plus//update', methods=['POST']) +@login_required +def plus_plus_update(user): + receiver = hubs.models.User.by_username(flask.g.db, user) + + if not receiver: + return 'User does not exist', 403 + + if user == flask.g.auth.nickname: + return 'You may not modify your own karma.', 403 + + if 'decrement' not in flask.request.form \ + and 'increment' not in flask.request.form: + return "You must set 'decrement' or 'increment' " \ + "with a boolean value in the body", 403 + + update = 'increment' if 'increment' in flask.request.form else 'decrement' + + update_bool_val = plus_plus_update_bool_helper(flask.request.form[update]) + pp_url = app.config['PLUS_PLUS_URL'] + str(receiver.username) + sender = hubs.models.User.by_username(flask.g.db, flask.g.auth.nickname) + pp_token = app.config['PLUS_PLUS_TOKEN'] + auth_header = {'Authorization': 'token {}'.format(pp_token)} + data = {'sender': sender.username, update: update_bool_val} + req = None + try: + req = requests.post(url=pp_url, headers=auth_header, data=data) + except: + flask.abort(500) + + if req.status_code == 200: + return flask.jsonify(req.json()) + else: + return req.text, req.status_code + diff --git a/hubs/views/root.py b/hubs/views/root.py new file mode 100644 index 0000000..cc03161 --- /dev/null +++ b/hubs/views/root.py @@ -0,0 +1,73 @@ +from __future__ import unicode_literals, absolute_import + +import flask +import hubs.models + +from hubs.app import app, session, OIDC +from .utils import authenticated, is_safe_url + + +@app.route('/') +def index(): + if not authenticated(): + return flask.redirect(flask.url_for('login')) + + return flask.redirect(flask.url_for('stream', name=flask.g.auth.nickname)) + + +@app.route('/groups') +def groups(): + if not authenticated(): + return flask.redirect(flask.url_for('login')) + + # Get the list of promoted and non-promoted group hubs from the DB + promoted_names = app.config.get('PROMOTED_GROUPS') + groups = hubs.models.Hub.all_group_hubs(flask.g.db) + promoted = [g for g in groups if g.name in promoted_names] + secondary = [g for g in groups if g.name not in promoted_names] + + name_of_the_month = app.config.get('HUB_OF_THE_MONTH') + hub_of_the_month = hubs.models.Hub.by_name(flask.g.db, name_of_the_month) + + return flask.render_template( + 'groups.html', + promoted=promoted, + secondary=secondary, + hub_of_the_month=hub_of_the_month, + ) + + +@app.route('/login/', methods=('GET', 'POST')) +@app.route('/login', methods=('GET', 'POST')) +@OIDC.require_login +def login(): + default = flask.url_for('index') + next_url = flask.request.args.get('next', default) + if is_safe_url(next_url): + return_point = next_url + else: + return_point = default + + hubs.models.User.get_or_create( + flask.g.db, username=flask.g.auth.nickname, fullname=flask.g.auth.fullname) + + return flask.redirect(return_point) + + +@app.route('/logout/') +@app.route('/logout') +def logout(): + next_url = flask.url_for('index') + if 'next' in flask.request.values: # pragma: no cover + if is_safe_url(flask.request.args['next']): + next_url = flask.request.values['next'] + + if next_url == flask.url_for('login'): # pragma: no cover + next_url = flask.url_for('index') + + if authenticated(): + OIDC.logout() + flask.session.auth = None + flask.flash('You have been logged out') + + return flask.redirect(next_url) diff --git a/hubs/views/user.py b/hubs/views/user.py new file mode 100644 index 0000000..cab84ed --- /dev/null +++ b/hubs/views/user.py @@ -0,0 +1,100 @@ +from __future__ import unicode_literals, absolute_import + +import flask +import json +import hubs.models +import hubs.stream + +from hubs.app import app, session +from .utils import login_required, get_hub + + +@app.route('//stream') +@app.route('//stream/') +@login_required +def stream(name): + hub = get_hub(name) + saved = hubs.models.SavedNotification.by_username(flask.g.db, name) + saved = [n.__json__() for n in saved] + + stream = hubs.stream.Stream() + actions = stream.get_json() + + return flask.render_template( + 'stream.html', + hub=hub, + saved=json.dumps(saved), + actions=actions + ) + + +@app.route('//notifications', methods=['GET', 'POST']) +@app.route('//notifications/', methods=['GET', 'POST']) +@login_required +def notifications(user): + if flask.request.method == 'GET': + notifications = hubs.models.SavedNotification.by_username(flask.g.db, user) + notifications = [n.__json__() for n in notifications] + return flask.jsonify(notifications) + + if flask.request.method == 'POST': + data = flask.request.get_json() + user = hubs.models.User.by_username(flask.g.db, user) + if not user: + return flask.abort(400) + try: + markup = data['markup'] + link = data['link'] + icon = data['secondary_icon'] + dom_id = data['dom_id'] + except: + return flask.abort(400) + notification = hubs.models.SavedNotification( + username=user.username, + markup=markup, + link=link, + secondary_icon=icon, + dom_id=dom_id + ) + flask.g.db.add(notification) + flask.g.db.commit() + return flask.jsonify( + dict(notification=notification.__json__(), success=True) + ) + +@app.route('//notifications/', methods=['DELETE']) +@app.route('//notifications//', methods=['DELETE']) +@login_required +def delete_notifications(user, idx): + notification = flask.g.db.query(hubs.models.SavedNotification).filter_by(idx=idx).first() + if not notification: + return flask.abort(400) + flask.g.db.delete(notification) + flask.g.db.commit() + return flask.jsonify(dict(status_code=200)) +@app.route('/visit//', methods=['GET', 'POST']) +@app.route('/visit/', methods=['GET', 'POST']) +@login_required +def increment_counter(visited_hub): + nickname = flask.g.auth.nickname + + if str(visited_hub) != str(nickname): + try: + vc = hubs.models.VisitCounter.get_or_create(session=flask.g.db, + username=nickname, + visited_hub=visited_hub) + except ValueError: # this should never trip + # flask will 405 if visited_hub is blank + # @login_required forces flask.g.auth to be sets + flask.abort(404) + if flask.request.method == 'POST': + vc.increment_visits(session=flask.g.db, + username=nickname, + visited_hub=visited_hub) + + return flask.jsonify({'count': vc.count}) + + elif flask.request.method == 'GET': + return flask.jsonify({'count': vc.count}) + else: + return flask.abort(403) diff --git a/hubs/views/utils.py b/hubs/views/utils.py new file mode 100644 index 0000000..e2d4bd2 --- /dev/null +++ b/hubs/views/utils.py @@ -0,0 +1,74 @@ +from __future__ import unicode_literals + +import flask +import functools +import hubs.models +import hubs.widgets + +from six.moves.urllib import parse as urlparse +from sqlalchemy.orm.exc import NoResultFound + + +def get_hub(name, session=None): + """ Utility shorthand to get a hub and 404 if not found. """ + if session is None: + session = flask.g.db + try: + return session.query(hubs.models.Hub).filter( + hubs.models.Hub.name == name).one() + except NoResultFound: + flask.abort(404) + + +def get_widget(hub, idx, session=None): + """ Utility shorthand to get a widget and 404 if not found. """ + if session is None: + session = flask.g.db + try: + idx = int(idx) + except (TypeError, ValueError): + flask.abort(400) + try: + return session.query( + hubs.models.Widget + ).join(hubs.models.Hub).filter( + hubs.models.Hub.name == hub, + hubs.models.Widget.idx == idx, + ).one() + except NoResultFound: + flask.abort(404) + + +def authenticated(): + """ Utility function checking if the current auth is set or not.""" + return hasattr(flask.g, 'auth') \ + and flask.g.auth is not None \ + and flask.g.auth.logged_in + + +def login_required(function): + """ Flask decorator to restrict access to logged-in users. """ + + @functools.wraps(function) + def decorated_function(*args, **kwargs): + """ Decorated function, actually does the work. """ + if not authenticated(): + flask.flash('Login required', 'errors') + return flask.redirect(flask.url_for( + 'login', next=flask.request.url)) + + return function(*args, **kwargs) + + return decorated_function + + +def is_safe_url(target): + """ Checks that the target url is safe and sending to the current + website not some other malicious one. + """ + ref_url = urlparse.urlparse(flask.request.host_url) + test_url = urlparse.urlparse( + urlparse.urljoin(flask.request.host_url, target)) + return test_url.scheme in ('http', 'https') and \ + ref_url.netloc == test_url.netloc + diff --git a/hubs/views/widget.py b/hubs/views/widget.py new file mode 100644 index 0000000..d40ff3f --- /dev/null +++ b/hubs/views/widget.py @@ -0,0 +1,109 @@ +from __future__ import unicode_literals, absolute_import + +import datetime +import flask + +from hubs.app import app, session +from .utils import get_widget + + +@app.route('///') +@app.route('//') +def widget_render(hub, idx): + widget = get_widget(hub, idx) + return widget.render(flask.g.db) # , edit=False) + # was blocking all widgets from working, sorry! + + +@app.route('///json') +@app.route('///json/') +def widget_json(hub, idx): + widget = get_widget(hub, idx) + response = flask.jsonify(widget.__json__(flask.g.db)) + # TODO -- modify headers with response.headers['X-fedora-hubs-wat'] = 'foo' + return response + + +@app.route('///edit/', methods=['GET', 'POST']) +@app.route('///edit', methods=['GET', 'POST']) +def widget_edit(hub, idx): + if flask.request.method == 'POST': + return widget_edit_post(hub, idx) + else: + return widget_edit_get(hub, idx) + + +def widget_edit_get(hub, idx): + widget = get_widget(hub, idx) + return flask.render_template( + 'edit.html', + hub=hub, + widget=widget, + url_to=flask.url_for('widget_edit', hub=hub, idx=idx) + ) + + +def widget_edit_post(hub, idx): + widget = get_widget(hub, idx) + error = False + config = {} + for arg in widget.module.data.widget_arguments: + val = flask.request.form.get(arg.name) + if not val: + flask.flash( + 'You must provide a value for: %s' % arg.name, 'error') + error = True + break + try: + val = arg.validator(flask.g.db, val) + config[arg.name] = val + except Exception as err: + flask.flash('Invalid data provided, error: %s' % err, 'error') + error = True + if not error: + cur_config = widget.config + cur_config.update(config) + widget.config = cur_config + widget.hub.last_edited = datetime.datetime.utcnow() + flask.g.db.add(widget) + try: + flask.g.db.commit() + except Exception as err: + flask.flash( + 'Could not save the configuration to the database ' + 'if the error persists, please warn an admin', + 'error') + return flask.redirect(flask.url_for('hub', name=hub)) + + +@app.route('///delete/', methods=['POST']) +@app.route('///delete', methods=['POST']) +def widget_edit_delete(hub, idx): + ''' Remove a widget from a hub. ''' + widget = get_widget(hub, idx) + flask.g.db.delete(widget) + try: + flask.g.db.commit() + except Exception as err: + flask.flash( + 'Could not delete this widget from this hub in the database ' + 'if the error persists, please warn an admin', + 'error') + return flask.redirect(flask.url_for('hub_edit', name=hub)) + + +@app.route('/source//') +@app.route('/source/') +def widget_source(name): + from hubs.widgets import registry + base = '/hubs/' + fname = '' + + try: + fname = base + registry[name].__file__.split(base)[-1] + except KeyError: + flask.abort(404) + + fname = fname.replace('.pyc', '.py') + return flask.redirect(app.config.get('SOURCE_URL') + fname) + From 8bd1aa1d4d6af5616977bb4b7f9a9f47e942c921 Mon Sep 17 00:00:00 2001 From: Aurélien Bompard Date: Jan 31 2017 18:25:56 +0000 Subject: [PATCH 2/3] Protect widget views against uninitialized DB --- diff --git a/hubs/views/__init__.py b/hubs/views/__init__.py index 13fb79c..f39cc0c 100644 --- a/hubs/views/__init__.py +++ b/hubs/views/__init__.py @@ -14,6 +14,7 @@ from .plus_plus import * import flask import functools import hubs.models +from sqlalchemy.exc import OperationalError from sqlalchemy.orm.exc import NoResultFound from hubs.app import app, session from .utils import get_widget @@ -34,13 +35,19 @@ def _widget_view_decorator(func): 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("/") - 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( - getattr(widget.module, params.pop("view_func_name"))) - app.add_url_rule(**params) +def _load_widget_views(): + 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( + getattr(widget.module, params.pop("view_func_name"))) + app.add_url_rule(**params) + +try: + _load_widget_views() +except OperationalError: + pass # Maybe the DB is not yet initialized (tests). From 37bd81d6edc500b8904d3463d0044c5b402e5042 Mon Sep 17 00:00:00 2001 From: Aurélien Bompard Date: Feb 01 2017 17:30:36 +0000 Subject: [PATCH 3/3] Use the registry to discover widget routes --- diff --git a/hubs/tests/test_widget_routes.py b/hubs/tests/test_widget_routes.py new file mode 100644 index 0000000..be03bd8 --- /dev/null +++ b/hubs/tests/test_widget_routes.py @@ -0,0 +1,67 @@ +from __future__ import unicode_literals + +import hubs.widgets + +from mock import Mock +from hubs.models import Hub, Widget +from hubs.tests import APPTest + +class WidgetRoutesTest(APPTest): + + def setUp(self): + super(WidgetRoutesTest, self).setUp() + # Backup the URL map + self._old_url_map = ( + self.app.application.url_map._rules.copy(), + self.app.application.url_map._rules_by_endpoint.copy() + ) + + def tearDown(self): + for module in hubs.widgets.registry.values(): + if hasattr(module, 'ROUTES'): + delattr(module, 'ROUTES') + # Restore the URL map + self.app.application.url_map._rules = self._old_url_map[0] + self.app.application.url_map._rules_by_endpoint = self._old_url_map[1] + super(WidgetRoutesTest, self).tearDown() + + def test_routes_variable(self): + mock_view = Mock(return_value='') + hubs.widgets.contact.mock_view = mock_view + hubs.widgets.contact.ROUTES = [{ + 'rule': 'dummy-url', + 'endpoint': 'dummy-endpoint', + 'view_func_name': 'mock_view', + }] + hubs.views._load_widget_views() + added_rules = list( + self.app.application.url_map.iter_rules( + endpoint='contact_dummy-endpoint') + ) + self.assertEqual(len(added_rules), 1) + self.assertEqual( + added_rules[0].rule, + '///widget/dummy-url' + ) + widget = self.session.query(Widget).filter_by( + plugin="contact").first() + self.app.get('/%s/%s/widget/dummy-url' + % (widget.hub.name, widget.idx)) + self.assertTrue(mock_view.called) + call = mock_view.call_args_list[0] + # Check arguments: session and widget + self.assertEqual(call[0][0].__class__, self.session.__class__) + self.assertEqual(call[0][1], widget) + + def test_decorator(self): + mock_view = Mock(return_value='') + mock_view.__name__ = 'mock_view' + mock_view.__module__ = 'hubs.widgets.feed' + hubs.widgets.feed.mock_view = hubs.widgets.base.widget_route( + rule='dummy-url', endpoint='dummy-endpoint')(mock_view) + self.assertEqual( + hubs.widgets.feed.ROUTES, + [{'view_func_name': 'mock_view', + 'endpoint': 'dummy-endpoint', + 'rule': 'dummy-url'}] + ) diff --git a/hubs/views/__init__.py b/hubs/views/__init__.py index f39cc0c..f388470 100644 --- a/hubs/views/__init__.py +++ b/hubs/views/__init__.py @@ -14,7 +14,7 @@ from .plus_plus import * import flask import functools import hubs.models -from sqlalchemy.exc import OperationalError +import hubs.widgets from sqlalchemy.orm.exc import NoResultFound from hubs.app import app, session from .utils import get_widget @@ -31,23 +31,21 @@ def _widget_view_decorator(func): def inner(*args, **kwargs): hubname = kwargs.pop("hub") widgetidx = kwargs.pop("idx") - widget = get_widget(hubname, widgetidx, session=session) + widget = get_widget(hubname, widgetidx) return func(session, widget, *args, **kwargs) return inner def _load_widget_views(): - for widget in session.query(hubs.models.Widget): - for params in getattr(widget.module, 'ROUTES', []): + for name, module in hubs.widgets.registry.items(): + #print(module, hasattr(module, 'ROUTES')) + for params in getattr(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["endpoint"] = "%s_%s" % (name, params["endpoint"]) params["view_func"] = _widget_view_decorator( - getattr(widget.module, params.pop("view_func_name"))) + getattr(module, params.pop("view_func_name"))) app.add_url_rule(**params) -try: - _load_widget_views() -except OperationalError: - pass # Maybe the DB is not yet initialized (tests). +_load_widget_views()