From 572a3fb3260c8e8535844ebe5861d9d50ac42952 Mon Sep 17 00:00:00 2001 From: Aurélien Bompard Date: Feb 22 2017 17:21:45 +0000 Subject: [PATCH 1/10] Protect the edit views to logged-in users only --- diff --git a/hubs/tests/test_fedora_hubs_flask_api.py b/hubs/tests/test_fedora_hubs_flask_api.py index 2a53ba5..a682d85 100644 --- a/hubs/tests/test_fedora_hubs_flask_api.py +++ b/hubs/tests/test_fedora_hubs_flask_api.py @@ -214,14 +214,18 @@ class HubsAPITest(hubs.tests.APPTest): "/openidc/Authorization") def test_hub_add_widget_get_no_args(self): - result = self.app.get('/ralph/add', follow_redirects=False) + user = tests.FakeAuthorization('ralph') + with tests.auth_set(app, user): + result = self.app.get('/ralph/add', follow_redirects=False) self.assertEqual(result.status_code, 400) expected_str = 'Invalid position provided' self.assertIn(expected_str, result.get_data(as_text=True)) def test_hub_add_widget_get_with_args(self): - result = self.app.get('/ralph/add?position=right', - follow_redirects=True) + user = tests.FakeAuthorization('ralph') + with tests.auth_set(app, user): + result = self.app.get('/ralph/add?position=right', + follow_redirects=True) self.assertEqual(result.status_code, 200) expected_str = 'Adding a widget to hub: ralph' self.assertIn(expected_str, result.get_data(as_text=True)) @@ -230,14 +234,18 @@ class HubsAPITest(hubs.tests.APPTest): def test_hub_add_widget_post_no_widget_name(self): data = {} - result = self.app.post('/ralph/add', data=data, follow_redirects=False) + user = tests.FakeAuthorization('ralph') + with tests.auth_set(app, user): + result = self.app.post('/ralph/add', data=data, follow_redirects=False) self.assertEqual(result.status_code, 400) expected_str = 'Invalid request sent' self.assertIn(expected_str, result.get_data(as_text=True)) def test_hub_add_widget_post_invalid_widget_name(self): data = {'widget_name': 'invalid_widget_name'} - result = self.app.post('/ralph/add', data=data, follow_redirects=False) + user = tests.FakeAuthorization('ralph') + with tests.auth_set(app, user): + result = self.app.post('/ralph/add', data=data, follow_redirects=False) self.assertEqual(result.status_code, 404) expected_str = 'Unknown widget called' self.assertIn(expected_str, result.get_data(as_text=True)) diff --git a/hubs/views/hub.py b/hubs/views/hub.py index 3da39d3..a0247f4 100644 --- a/hubs/views/hub.py +++ b/hubs/views/hub.py @@ -5,7 +5,7 @@ import flask import hubs.models from hubs.app import app, session -from .utils import get_hub +from .utils import get_hub, login_required @app.route('/') @@ -25,8 +25,8 @@ def hub_json(name): return response -@app.route('//edit/', methods=['GET', 'POST']) @app.route('//edit', methods=['GET', 'POST']) +@login_required def hub_edit(name): if flask.request.method == 'POST': return hub_edit_post(name) diff --git a/hubs/views/root.py b/hubs/views/root.py index 6b9102f..386f233 100644 --- a/hubs/views/root.py +++ b/hubs/views/root.py @@ -50,6 +50,7 @@ def login(): hubs.models.User.get_or_create( flask.g.db, username=flask.g.auth.nickname, fullname=flask.g.auth.fullname) + flask.flash('Login successful', 'success') return flask.redirect(return_point) diff --git a/hubs/views/utils.py b/hubs/views/utils.py index 94113e3..32b544b 100644 --- a/hubs/views/utils.py +++ b/hubs/views/utils.py @@ -52,7 +52,6 @@ def login_required(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)) diff --git a/hubs/views/widget.py b/hubs/views/widget.py index adc857a..773e357 100644 --- a/hubs/views/widget.py +++ b/hubs/views/widget.py @@ -5,7 +5,7 @@ import flask from hubs.app import app from pkg_resources import resource_isdir -from .utils import get_widget_instance +from .utils import get_widget_instance, login_required @app.route('///json') @@ -19,6 +19,7 @@ def widget_json(hub, idx): @app.route('///edit/', methods=['GET', 'POST']) @app.route('///edit', methods=['GET', 'POST']) +@login_required def widget_edit(hub, idx): if flask.request.method == 'POST': return widget_edit_post(hub, idx) @@ -71,6 +72,7 @@ def widget_edit_post(hub, idx): @app.route('///delete/', methods=['POST']) @app.route('///delete', methods=['POST']) +@login_required def widget_edit_delete(hub, idx): ''' Remove a widget from a hub. ''' widget = get_widget_instance(hub, idx) From 69c3e12d1c701db768355ef93445774e069e7a37 Mon Sep 17 00:00:00 2001 From: Aurélien Bompard Date: Feb 22 2017 17:21:45 +0000 Subject: [PATCH 2/10] Let widgets declare their add and edit config urls This way, widgets can have a more complex configuration panel. --- diff --git a/hubs/models.py b/hubs/models.py index 2c155b9..3464f5b 100644 --- a/hubs/models.py +++ b/hubs/models.py @@ -360,6 +360,10 @@ class Widget(BASE): def module(self): return hubs.widgets.registry[self.plugin] + @property + def edit_url(self): + return self.module.get_edit_url(self.hub, self) + class User(BASE): __tablename__ = 'users' diff --git a/hubs/templates/edit.html b/hubs/templates/edit.html deleted file mode 100644 index de4613f..0000000 --- a/hubs/templates/edit.html +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - - diff --git a/hubs/templates/hubs.html b/hubs/templates/hubs.html index f5628f0..f096d30 100644 --- a/hubs/templates/hubs.html +++ b/hubs/templates/hubs.html @@ -281,10 +281,10 @@ $('#save_edits_btn').click(function() { function setup_edit_btns() { $("body").on("click", ".edit_widget", function(e) { e.preventDefault(); - var _idx = $(this).attr('data-idx'); + var url = $(this).attr('data-url'); $.ajax({ - url: _idx + '/edit', + url: url, dataType: 'html', success: function(html) { $('#edit_modal_content').html(html); diff --git a/hubs/templates/widget_add.html b/hubs/templates/widget_add.html new file mode 100644 index 0000000..7eae842 --- /dev/null +++ b/hubs/templates/widget_add.html @@ -0,0 +1,36 @@ + diff --git a/hubs/templates/widget_edit.html b/hubs/templates/widget_edit.html new file mode 100644 index 0000000..3139d5d --- /dev/null +++ b/hubs/templates/widget_edit.html @@ -0,0 +1,43 @@ + diff --git a/hubs/views/widget.py b/hubs/views/widget.py index 773e357..60b1326 100644 --- a/hubs/views/widget.py +++ b/hubs/views/widget.py @@ -4,8 +4,10 @@ import datetime import flask from hubs.app import app +from hubs.models import Widget +from hubs.widgets import registry from pkg_resources import resource_isdir -from .utils import get_widget_instance, login_required +from .utils import get_hub, get_widget_instance, login_required @app.route('///json') @@ -17,7 +19,59 @@ def widget_json(hub, idx): return response -@app.route('///edit/', methods=['GET', 'POST']) +@app.route('//add/', methods=['GET', 'POST']) +def widget_add(hub, widget): + hub = get_hub(hub) + position = flask.request.values.get('position', '') + if position not in ['right', 'left']: + flask.abort(400, 'Invalid position provided') + try: + widget = registry[widget] + except KeyError: + flask.abort(404, 'Unknown widget: %s' % widget) + if flask.request.method == 'POST': + create_widget_instance(hub, widget, position) + return flask.redirect(flask.url_for('hub_edit', name=hub.name)) + return flask.render_template( + 'widget_add.html', + hub=hub, + widget=widget, + position=position, + ) + +def create_widget_instance(hub, widget, position): + """View helper to instanciate a widget on a hub.""" + widget_instance = Widget( + hub=hub, plugin=widget.name, index=-1, + left=(position == 'left')) + config = {} + for param in widget.get_parameters(): + val = flask.request.form.get(param.name) + if not val: + flask.flash( + 'You must provide a value for: %s' % param.name, 'error') + return + try: + config[param.name] = param.validator.from_string(val) + except Exception as err: + flask.flash('Invalid data provided, error: %s' % err, 'error') + return + widget_instance.config = config + try: + flask.g.db.add(widget_instance) + flask.g.db.flush() + # TODO: use a SQLAlchemy signal here + hub.last_edited = datetime.datetime.utcnow() + 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') + else: + flask.flash("The widget has been added.") + + @app.route('///edit', methods=['GET', 'POST']) @login_required def widget_edit(hub, idx): @@ -28,20 +82,19 @@ def widget_edit(hub, idx): def widget_edit_get(hub, idx): - widget = get_widget_instance(hub, idx) + widget_instance = get_widget_instance(hub, idx) return flask.render_template( - 'edit.html', - hub=hub, - widget=widget, - url_to=flask.url_for('widget_edit', hub=hub, idx=idx) + 'widget_edit.html', + hub=widget_instance.hub, + widget_instance=widget_instance, ) def widget_edit_post(hub, idx): - widget = get_widget_instance(hub, idx) + widget_instance = get_widget_instance(hub, idx) error = False config = {} - for param in widget.module.get_parameters(): + for param in widget_instance.module.get_parameters(): val = flask.request.form.get(param.name) if not val: flask.flash( @@ -49,17 +102,17 @@ def widget_edit_post(hub, idx): error = True break try: - val = param.validator.from_string(val) - config[param.name] = val + config[param.name] = param.validator.from_string(val) except Exception as err: flask.flash('Invalid data provided, error: %s' % err, 'error') error = True if not error: - cur_config = widget.config + # Updating in-place is not supported, it's a class property. + cur_config = widget_instance.config.copy() cur_config.update(config) - widget.config = cur_config - widget.hub.last_edited = datetime.datetime.utcnow() - flask.g.db.add(widget) + widget_instance.config = cur_config + widget_instance.hub.last_edited = datetime.datetime.utcnow() + flask.g.db.add(widget_instance) try: flask.g.db.commit() except Exception as err: @@ -67,7 +120,7 @@ def widget_edit_post(hub, idx): '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)) + return flask.redirect(flask.url_for('hub_edit', name=hub)) @app.route('///delete/', methods=['POST']) @@ -90,8 +143,6 @@ def widget_edit_delete(hub, idx): @app.route('/source//') @app.route('/source/') def widget_source(name): - from hubs.widgets import registry - try: widget_path = registry[name].__module__ except KeyError: diff --git a/hubs/widgets/base.py b/hubs/widgets/base.py index 38da840..8ae225a 100644 --- a/hubs/widgets/base.py +++ b/hubs/widgets/base.py @@ -52,7 +52,7 @@ class Widget(object): """ The main widget class, you must subclass it to create a widget. - Arguments: + Attributes: name (str): The widget name. It will not be displayed in the UI, but will appear in some URLs, so be careful to only use simple, URL-compatible characters. @@ -194,6 +194,39 @@ class Widget(object): self.name, url_rule.lstrip("/")) app.add_url_rule(rule, view_func=view_func) + def get_add_url(self, hub, position): + """Returns the URL to the configuration panel to add this widget. + + By default, it returns the URL to the common widget configuration panel + that will simply display the widget's parameters in a form. Widgets + override this method to implement a more complex configuration view. + + If a more complex configuration view is implemented, the resulting HTML + code will be wrapped in a `` From 175f521bc2d5cc370b16f12efd8c72578212220b Mon Sep 17 00:00:00 2001 From: Aurélien Bompard Date: Feb 22 2017 17:21:45 +0000 Subject: [PATCH 3/10] The add_widget panel is only used to choose the widget name Then it delegates the configuration to the widget's add_url. --- diff --git a/hubs/templates/add_widget.html b/hubs/templates/add_widget.html index cf9d686..ce20d36 100644 --- a/hubs/templates/add_widget.html +++ b/hubs/templates/add_widget.html @@ -1,40 +1,21 @@ - -{% if widgets %} - -{% else %} - - - - -{% endif %} diff --git a/hubs/templates/hubs.html b/hubs/templates/hubs.html index f096d30..fda7f95 100644 --- a/hubs/templates/hubs.html +++ b/hubs/templates/hubs.html @@ -249,6 +249,39 @@ function setup_add_btns() { }); return false; }); + // Setup events for the "add widget" form using delegated events because it's + // not in the DOM yet. + $("body").on("submit", "#adding_form form", function(e) { + var form = $(this); + e.preventDefault(); + $.ajax({ + type: "POST", + url: form.attr("action"), + dataType: "json", + data: form.serialize(), + success: function(data) { + if (data.status === "CONFIGURE") { + $("#adding_form").load(data.url); + } else if (data.status === "ADDED") { + // no config necessary, reload the page + window.location = window.location; + } else { + $('#adding_form .modal-body').html( + "An error occured when trying to add a new widget." + ); + console.log('error'); + console.trace(); + } + }, + error: function() { + $('#adding_form .modal-body').html( + "An error occured when trying to add a new widget." + ); + console.log('error'); + console.trace(); + } + }); + }); } $('#save_edits_btn').click(function() { diff --git a/hubs/tests/test_fedora_hubs_flask_api.py b/hubs/tests/test_fedora_hubs_flask_api.py index a682d85..dc3da86 100644 --- a/hubs/tests/test_fedora_hubs_flask_api.py +++ b/hubs/tests/test_fedora_hubs_flask_api.py @@ -350,7 +350,7 @@ class HubsAPITest(hubs.tests.APPTest): with tests.auth_set(app, user): url = '/ralph/add/about?position=right' result = self.app.get(url) - self.assertIn('Adding widget "about" to hub: ralph', + self.assertIn('Adding widget "about" to hub ralph', result.get_data(as_text=True)) def test_hub_add_widget_invalid_side(self): diff --git a/hubs/views/hub.py b/hubs/views/hub.py index a0247f4..a4a8b19 100644 --- a/hubs/views/hub.py +++ b/hubs/views/hub.py @@ -139,96 +139,3 @@ def hub_edit_post(name): 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 = hubs.widgets.registry[widget_name] - if widget.position not in ['both', side]: - flask.abort(400, "This widget can't be placed here") - - return flask.render_template( - 'add_widget.html', - hub=hub, - widget=widget, - 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 param in widget.module.get_parameters(): - val = flask.request.form.get(param.name) - if not val: - flask.flash( - 'You must provide a value for: %s' % param.name, 'error') - error = True - break - try: - val = param.validator.from_string(val) - config[param.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.redirect(flask.url_for('hub_edit', name=hub.name)) diff --git a/hubs/views/widget.py b/hubs/views/widget.py index 60b1326..02c06c8 100644 --- a/hubs/views/widget.py +++ b/hubs/views/widget.py @@ -19,6 +19,40 @@ def widget_json(hub, idx): return response +@app.route('//add', methods=['GET', 'POST']) +def hub_add_widget(name): + hub = get_hub(name) + position = flask.request.values.get('position', '') + if position not in ['right', 'left']: + flask.abort(400, 'Invalid position provided') + widgets = [ + widget + for widget in registry + if registry[widget].position in ['both', position] + ] + + if flask.request.method == 'POST': + widget_name = flask.request.form.get('widget') + if not widget_name: + flask.abort(400, 'Invalid request sent') + if widget_name not in widgets: + flask.abort(404, 'Unknown widget called') + widget = registry[widget_name] + if widget.get_parameters(): + return flask.jsonify({ + "status": "CONFIGURE", + "url": widget.get_add_url(hub, position), + }) + create_widget_instance(hub, widget, position) + return flask.jsonify({"status": "ADDED"}) + return flask.render_template( + 'add_widget.html', + hub=hub, + widgets=widgets, + position=position, + ) + + @app.route('//add/', methods=['GET', 'POST']) def widget_add(hub, widget): hub = get_hub(hub) From 4198845c1b7cd5e0ab11ae73f6fc1562be5ff5e4 Mon Sep 17 00:00:00 2001 From: Aurélien Bompard Date: Feb 22 2017 17:21:45 +0000 Subject: [PATCH 4/10] Fix unit tests --- diff --git a/hubs/tests/test_fedora_hubs_flask_api.py b/hubs/tests/test_fedora_hubs_flask_api.py index dc3da86..97c4911 100644 --- a/hubs/tests/test_fedora_hubs_flask_api.py +++ b/hubs/tests/test_fedora_hubs_flask_api.py @@ -227,13 +227,14 @@ class HubsAPITest(hubs.tests.APPTest): result = self.app.get('/ralph/add?position=right', follow_redirects=True) self.assertEqual(result.status_code, 200) - expected_str = 'Adding a widget to hub: ralph' - self.assertIn(expected_str, result.get_data(as_text=True)) - expected_str = "url: 'add/' + $('#widget').val() + '?position=right'," - self.assertIn(expected_str, result.get_data(as_text=True)) + page_html = result.get_data(as_text=True) + self.assertIn('Adding a widget to hub: ralph', page_html) + self.assertIn('
', page_html) + self.assertIn('', + page_html) def test_hub_add_widget_post_no_widget_name(self): - data = {} + data = {"position": "left"} user = tests.FakeAuthorization('ralph') with tests.auth_set(app, user): result = self.app.post('/ralph/add', data=data, follow_redirects=False) @@ -242,7 +243,8 @@ class HubsAPITest(hubs.tests.APPTest): self.assertIn(expected_str, result.get_data(as_text=True)) def test_hub_add_widget_post_invalid_widget_name(self): - data = {'widget_name': 'invalid_widget_name'} + data = {'widget': 'invalid_widget_name', + 'position': 'right'} user = tests.FakeAuthorization('ralph') with tests.auth_set(app, user): result = self.app.post('/ralph/add', data=data, follow_redirects=False) @@ -253,31 +255,36 @@ class HubsAPITest(hubs.tests.APPTest): def test_hub_add_widget_post_valid_widget_name_no_args(self): user = tests.FakeAuthorization('ralph') with tests.auth_set(app, user): - data = {'widget_name': 'about'} + data = { + 'widget': 'memberships', + 'position': 'right', + } result = self.app.post('/ralph/add', data=data) - self.assertEqual(result.status_code, 302) - self.assertEqual(urlparse(result.location).path, "/ralph/edit") + self.assertEqual(result.status_code, 200) + self.assertEqual( + json.loads(result.get_data(as_text=True)), + {"status": "ADDED"}) + #self.assertEqual(urlparse(result.location).path, "/ralph/edit") result = self.app.get('/ralph/edit') self.assertEqual(result.status_code, 200) - expected_str = '' - self.assertIn(expected_str, result.get_data(as_text=True)) - expected_str = 'Full Name: fullname: ralph' - self.assertIn(expected_str, result.get_data(as_text=True)) + page_html = result.get_data(as_text=True) + self.assertIn('data-url="/ralph/w/memberships/', page_html) def test_hub_add_widget_post_valid_widget_name_with_args(self): user = tests.FakeAuthorization('ralph') with tests.auth_set(app, user): - data = {'widget_name': 'about', 'text': 'text of widget'} - result = self.app.post('/ralph/add', data=data, + data = { + 'text': 'text of widget', + 'position': 'right', + } + result = self.app.post('/ralph/add/about', data=data, follow_redirects=False) self.assertEqual(result.status_code, 302) self.assertEqual(urlparse(result.location).path, "/ralph/edit") result = self.app.get('/ralph/edit') self.assertEqual(result.status_code, 200) - expected_str = '' - self.assertIn(expected_str, result.get_data(as_text=True)) - expected_str = 'Full Name: fullname: ralph' - self.assertIn(expected_str, result.get_data(as_text=True)) + page_html = result.get_data(as_text=True) + self.assertIn('data-url="/ralph/w/about/', page_html) def test_hub_edit_widget_get_logged_in(self): user = tests.FakeAuthorization('ralph') @@ -299,7 +306,7 @@ class HubsAPITest(hubs.tests.APPTest): url = '/ralph/37/edit' result = self.app.post(url, data=data, follow_redirects=False) self.assertEqual(result.status_code, 302) - self.assertEqual(urlparse(result.location).path, '/ralph/') + self.assertEqual(urlparse(result.location).path, '/ralph/edit') def test_hub_visit_counter_logged_in(self): user = tests.FakeAuthorization('ralph') From addb6b553444e9ea79f633d49cd9d23b64f961bd Mon Sep 17 00:00:00 2001 From: Aurélien Bompard Date: Feb 22 2017 17:21:45 +0000 Subject: [PATCH 5/10] Don't hardcode URLs in Javascript code --- diff --git a/hubs/templates/hubs.html b/hubs/templates/hubs.html index fda7f95..2e4ccb3 100644 --- a/hubs/templates/hubs.html +++ b/hubs/templates/hubs.html @@ -146,7 +146,7 @@

- + Add a widget

@@ -164,7 +164,7 @@

- + Add a widget

@@ -223,17 +223,14 @@ function make_widget_sortable() { }; function setup_add_btns() { - $(".add_widget").unbind(); - $(".add_widget").click(function() { - console.log($(this)); - var _pos = $(this).attr('data-position'); - + // Setup events for the "add widget" button. + $(".add_widget").click(function(e) { + e.preventDefault(); $.ajax({ - url: 'add?position=' + _pos, + url: $(this).attr('href'), dataType: 'html', success: function(html) { $('#edit_modal_content').html(html); - $('#edit_modal').modal(); }, error: function() { $('#edit_modal_content').html( @@ -244,8 +241,10 @@ function setup_add_btns() { ); console.log('error'); console.trace(); - $('#edit_modal').modal(); }, + complete: function() { + $('#edit_modal').modal(); + } }); return false; }); From e8bb053df384c1ddc18fa87f1b96aea895e0c07d Mon Sep 17 00:00:00 2001 From: Aurélien Bompard Date: Feb 22 2017 17:24:04 +0000 Subject: [PATCH 6/10] Move a function in the view utils module And other minor cleanups --- diff --git a/hubs/templates/hubs.html b/hubs/templates/hubs.html index 2e4ccb3..b1b013d 100644 --- a/hubs/templates/hubs.html +++ b/hubs/templates/hubs.html @@ -232,7 +232,7 @@ function setup_add_btns() { success: function(html) { $('#edit_modal_content').html(html); }, - error: function() { + error: function(html) { $('#edit_modal_content').html( '