← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~rvb/maas/maas-bug-941751-js into lp:maas

 

Raphaël Badin has proposed merging lp:~rvb/maas/maas-bug-941751-js into lp:maas with lp:~rvb/maas/maas-bug-941751 as a prerequisite.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~rvb/maas/maas-bug-941751-js/+merge/96112

This branch adds a Widget to maas.utils to be able to edit the MaaS' title "in place".  I choose not to put it in the homepage js because we might want to reuse that widget and also I figured the dashboard js module will soon be crowded.  The only trick is that we want the suffix " MaaS" to be appended to the title: the javascript has to take care of that when the user starts/stop to edit the title.

I had to introduce flash_{red, green} utilities to signal the success/failure of the background save.
-- 
https://code.launchpad.net/~rvb/maas/maas-bug-941751-js/+merge/96112
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~rvb/maas/maas-bug-941751-js into lp:maas.
=== modified file 'src/maasserver/static/js/testing/testing.js'
--- src/maasserver/static/js/testing/testing.js	2012-02-15 19:27:11 +0000
+++ src/maasserver/static/js/testing/testing.js	2012-03-06 12:35:34 +0000
@@ -29,8 +29,7 @@
 
    /**
     * Mock the '_io' field of the provided module.  This assumes that
-    * the module has a internal reference to its io module named '_io'
-    * and that all its io is done via module._io.io(...).
+    * the module has a internal reference to its io module named '_io'.
     *
     * @method mockIO
     * @param mock the mock object that should replace the module's io
@@ -43,6 +42,26 @@
     },
 
    /**
+    * Mock the '_io' field of the provided module with a silent method that
+    * simply records the call to 'send'.  Whether or not 'send' was called
+    * will be stored in 'self.fired'.
+    * This assumes that the module has a internal reference to its io module
+    * named '_io' and that all its io is done via module._io.send(...).
+    *
+    * @method mockIO
+    * @param module the module to monkey patch
+    */
+    mockIOFired: function(module) {
+        this.fired = false;
+        var mockXhr = new Y.Base();
+        var self = this;
+        mockXhr.send = function(url, cfg) {
+            self.fired = true;
+        };
+        this.mockIO(mockXhr, module);
+    },
+
+   /**
     * Register a method to be fired when the event 'name' is triggered on
     * 'source'.  The handle will be cleaned up when the test finishes.
     *

=== modified file 'src/maasserver/static/js/tests/test_utils.html'
--- src/maasserver/static/js/tests/test_utils.html	2012-02-27 10:20:20 +0000
+++ src/maasserver/static/js/tests/test_utils.html	2012-03-06 12:35:34 +0000
@@ -4,16 +4,30 @@
     <title>Test maas.utils</title>
 
     <!-- YUI and test setup -->
-    <script type="text/javascript" src="../yui/tests/yui/yui-min.js"></script>
+    <script type="text/javascript" src="../yui/tests/yui/yui.js"></script>
     <script type="text/javascript" src="../testing/testrunner.js"></script>
     <script type="text/javascript" src="../testing/testing.js"></script>
     <!-- The module under test -->
     <script type="text/javascript" src="../utils.js"></script>
     <!-- The test suite -->
     <script type="text/javascript" src="test_utils.js"></script>
+    <script type="text/javascript">
+    <!--
+    var MaaS_config = {
+      uris: {
+        maas_handler: '/api/maas/'
+      }
+    };
+    // -->
+    </script>
   </head>
   <body>
   <span id="suite">maas.utils.tests</span>
+  <script type="text/x-template" id="title-form">
+    <form class="page-title-form">
+      <input type="text" value="Test Name MaaS"></input>
+    </form>
+  </script>
   <div id="placeholder"></div>
   </body>
 </html>

=== modified file 'src/maasserver/static/js/tests/test_utils.js'
--- src/maasserver/static/js/tests/test_utils.js	2012-02-27 10:20:20 +0000
+++ src/maasserver/static/js/tests/test_utils.js	2012-03-06 12:35:34 +0000
@@ -10,13 +10,233 @@
 var module = Y.maas.utils;
 var suite = new Y.Test.Suite("maas.utils Tests");
 
-suite.add(new Y.maas.testing.TestCase({
-    name: 'test-utils'
+var title_form = Y.one('#title-form').getContent();
+
+suite.add(new Y.maas.testing.TestCase({
+    name: 'test-utils-flash',
+
+    createNodeWithBaseColor: function(base_color) {
+        var node = Y.Node.create('<span />')
+            .set('text', "sample text");
+        node.setStyle('backgroundColor', base_color);
+        Y.one('#placeholder').empty().append(node);
+        return node;
+    },
+
+    assertAnimRestoresBackground: function(anim, node, base_color) {
+        var self = this;
+        anim.on("end", function(){
+            self.resume(function(){
+                Y.Assert.areEqual(
+                    base_color, node.getStyle('backgroundColor'));
+            });
+        });
+        anim.run();
+        // Make sure we wait long enough (duration is in seconds, wait uses
+        // milliseconds (!)).
+        this.wait(module.FLASH_DURATION*1000*1.5);
+    },
+
+    test_base_flash_restores_background_color: function() {
+        var base_color = 'rgb(12, 11, 12)';
+        var from_color = 'rgb(122, 211, 142)';
+        var node = this.createNodeWithBaseColor(base_color);
+        var anim = module.base_flash(node, from_color);
+        this.assertAnimRestoresBackground(anim, node, base_color);
+    },
+
+    test_red_flash_restores_background_color: function() {
+        var base_color = 'rgb(12, 11, 12)';
+        var node = this.createNodeWithBaseColor(base_color);
+        var anim = module.red_flash(node);
+        this.assertAnimRestoresBackground(anim, node, base_color);
+    },
+
+    test_green_flash_restores_background_color: function() {
+        var base_color = 'rgb(12, 11, 12)';
+        var node = this.createNodeWithBaseColor(base_color);
+        var anim = module.green_flash(node);
+        this.assertAnimRestoresBackground(anim, node, base_color);
+    }
+
+}));
+
+
+suite.add(new Y.maas.testing.TestCase({
+    name: 'test-utils-titleeditwidget',
+
+    setUp: function() {
+        Y.one('#placeholder').empty().append(Y.Node.create(title_form));
+    },
+
+    createWidget: function() {
+        var widget = new module.TitleEditWidget({srcNode: '#placeholder'});
+        widget.render();
+        return widget;
+    },
+
+    test_getInput_returns_input: function() {
+        var widget = this.createWidget();
+        input = widget.get('srcNode').one('input');
+        Y.Assert.areEqual(input, widget.get('input'));
+    },
+
+    test_getTitle_returns_title: function() {
+        var widget = this.createWidget();
+        widget.get('srcNode').one('input').set('value', "Test value");
+        Y.Assert.areEqual("Test value", widget.get('title'));
+    },
+
+    test_setTitle_changes_title: function() {
+        var widget = this.createWidget();
+        widget.set('title', "Another value");
+        Y.Assert.areEqual(
+            "Another value",
+            widget.get('srcNode').one('input').get('value'));
+    },
+
+    test_hasSuffix_returns_true_if_suffix: function() {
+        var widget = this.createWidget();
+        widget.set('title', "Sample Title MaaS");
+        Y.Assert.isTrue(widget.hasSuffix());
+    },
+
+    test_hasSuffix_returns_false_if_not_suffix: function() {
+        var widget = this.createWidget();
+        widget.set('title', "Sample Title");
+        Y.Assert.isFalse(widget.hasSuffix());
+    },
+
+    test_removeSuffix_removes_suffix: function() {
+        var widget = this.createWidget();
+        widget.set('title', "Sample Title MaaS");
+        widget.removeSuffix();
+        Y.Assert.areEqual(
+            "Sample Title", widget.get('title'));
+    },
+
+    test_removeSuffix_doesnothing_if_suffix_not_present: function() {
+        var widget = this.createWidget();
+        widget.set('title', "Sample Title");
+        widget.removeSuffix();
+        Y.Assert.areEqual(
+            "Sample Title", widget.get('title'));
+    },
+
+    test_addSuffix_add_suffix: function() {
+        var widget = this.createWidget();
+        widget.set('title', "Sample Title");
+        widget.addSuffix();
+        Y.Assert.areEqual(
+            "Sample Title MaaS", widget.get('title'));
+    },
+
+    test_titleEditStart_starts_editing: function() {
+        var widget = this.createWidget();
+        widget._editing = false;
+        widget.set('title', "Sample Title MaaS");
+        widget.titleEditStart();
+        Y.Assert.isTrue(widget._editing);
+        Y.Assert.areEqual(
+            "Sample Title", widget.get('title'));
+    },
+
+    test_titleEditStart_does_nothing_if_already_editing: function() {
+        var widget = this.createWidget();
+        widget._editing = true;
+        widget.set('title', "Sample Title MaaS");
+        widget.titleEditStart();
+        Y.Assert.isTrue(widget._editing);
+        Y.Assert.areEqual(
+            "Sample Title MaaS", widget.get('title'));
+    },
+
+    test_titleEditEnd_stops_editing_if_currently_editing: function() {
+        var widget = this.createWidget();
+        widget._editing = true;
+        widget.set('title', "SampleTitle");
+        var mockXhr = new Y.Base();
+        var fired = false;
+        mockXhr.send = function(url, cfg) {
+            fired = true;
+            Y.Assert.areEqual(MaaS_config.uris.maas_handler, url);
+            Y.Assert.areEqual(
+                "op=set_config&name=maas_name&value=SampleTitle",
+                cfg.data);
+        };
+        this.mockIO(mockXhr, module);
+        widget.titleEditEnd();
+        Y.Assert.isTrue(fired);
+        Y.Assert.isFalse(widget._editing);
+        Y.Assert.areEqual(
+            "SampleTitle MaaS", widget.get('title'));
+    },
+
+    test_titleEditEnd_does_nothing_if_not_editing: function() {
+        var widget = this.createWidget();
+        widget._editing = false;
+        widget.set('title', "Sample Title");
+        this.mockIOFired(module);
+        widget.titleEditEnd();
+        Y.Assert.isFalse(this.fired);
+        Y.Assert.isFalse(widget._editing);
+        Y.Assert.areEqual(
+            "Sample Title", widget.get('title'));
+    },
+
+    test_input_click_starts_edition: function() {
+        var widget = this.createWidget();
+        widget._editing = false;
+        this.mockIOFired(module); // Silent io.
+        var input = widget.get('srcNode').one('input');
+        input.simulate('click');
+        Y.Assert.isTrue(widget._editing);
+    },
+
+    test_input_onchange_stops_editing: function() {
+        var widget = this.createWidget();
+        widget._editing = true;
+        this.mockIOFired(module); // Silent io.
+        var input = widget.get('srcNode').one('input');
+        input.simulate('change');
+        Y.Assert.isFalse(widget._editing);
+    },
+
+    test_input_enter_pressed_stops_editing: function() {
+        var widget = this.createWidget();
+        widget._editing = true;
+        this.mockIOFired(module); // Silent io.
+        var input = widget.get('srcNode').one('input');
+        // Simulate 'Enter' being pressed.
+        input.simulate("keypress", { keyCode: 13 });
+        Y.Assert.isFalse(widget._editing);
+    },
+
+    xtestDeleteTokenCall: function() {
+        // A click on the delete link calls the API to delete a token.
+        var mockXhr = new Y.Base();
+        var fired = false;
+        mockXhr.send = function(url, cfg) {
+            fired = true;
+            Y.Assert.areEqual(MaaS_config.uris.account_handler, url);
+            Y.Assert.areEqual(
+                "op=delete_authorisation_token&token_key=tokenkey1",
+                cfg.data);
+        };
+        this.mockIO(mockXhr, module);
+        var widget = this.createWidget();
+        this.addCleanup(function() { widget.destroy(); });
+        widget.render();
+        var link = widget.get('srcNode').one('.delete-link');
+        link.simulate('click');
+        Y.Assert.isTrue(fired);
+    }
+
 
 }));
 
 namespace.suite = suite;
 
 }, '0.1', {'requires': [
-    'node', 'test', 'maas.testing', 'maas.utils']}
+    'node', 'test', 'maas.testing', 'maas.utils', 'node-event-simulate']}
 );

=== modified file 'src/maasserver/static/js/utils.js'
--- src/maasserver/static/js/utils.js	2012-02-27 10:20:20 +0000
+++ src/maasserver/static/js/utils.js	2012-03-06 12:35:34 +0000
@@ -11,5 +11,219 @@
 Y.log('loading mass.utils');
 var module = Y.namespace('maas.utils');
 
-}, '0.1', {'requires': ['base']}
+// Only used to mockup io in tests.
+module._io = new Y.IO();
+
+var FLASH_DURATION = 1;
+
+module.FLASH_DURATION = FLASH_DURATION;
+
+base_flash = function(obj, from_color) {
+    old_bg_color = Y.one(obj).getStyle('backgroundColor');
+
+    return new Y.Anim({
+        node: obj,
+        duration: FLASH_DURATION,
+        from: { backgroundColor: from_color},
+        to: { backgroundColor: old_bg_color}
+    });
+};
+
+module.base_flash = base_flash;
+
+/**
+ * @function green_flash
+ * @description A green flash and fade.
+ * @return Y.Anim instance
+ */
+var green_flash;
+
+green_flash = function(obj) {
+    return base_flash(obj, '#00FF00');
+};
+
+module.green_flash = green_flash;
+
+
+/**
+ * @function red_flash
+ * @description A red flash and fade, used to indicate errors.
+ * @return Y.Anim instance
+ */
+
+red_flash = function(obj) {
+    return base_flash(obj, '#FF0000');
+};
+
+module.red_flash = red_flash;
+
+
+var TitleEditWidget = function() {
+    TitleEditWidget.superclass.constructor.apply(this, arguments);
+};
+
+TitleEditWidget.NAME = 'title-edit-widget';
+
+TITLE_SUFFIX = ' MaaS';
+
+TitleEditWidget.ATTRS = {
+
+   /**
+    * MaaS's title input node.
+    *
+    * @attribute input
+    * @type Node
+    */
+    input: {
+        getter: function() {
+            return this.get('srcNode').one('input');
+        }
+    },
+
+   /**
+    * MaaS's title.
+    *
+    * @attribute title
+    * @type string
+    */
+    title: {
+        getter: function() {
+            return this.get('input').get('value');
+        },
+        setter: function(value) {
+            this.get('input').set('value', value);
+        }
+    }
+
+};
+
+
+Y.extend(TitleEditWidget, Y.Widget, {
+
+   initializer: function() {
+        // A boolean indicating whether or not the user is currently editing
+        // the title.
+        this._editing = false;
+    },
+
+   /**
+    * Does the input contain the suffix?
+    *
+    * @method hasSuffix
+    */
+    hasSuffix: function() {
+        var title = this.get('title');
+        var suffix = title.substring(
+            title.length - TITLE_SUFFIX.length, title.length);
+        return (suffix === TITLE_SUFFIX);
+    },
+
+   /**
+    * Add the suffix to the input's content.
+    *
+    * @method addSuffix
+    */
+    addSuffix: function() {
+        this.set('title', this.get('title') + TITLE_SUFFIX);
+    },
+
+   /**
+    * Remove the suffix to the input's content.
+    *
+    * @method removeSuffix
+    */
+    removeSuffix: function() {
+        if (this.hasSuffix()) {
+            var title = this.get('title');
+            var new_title = title.substring(
+                0, title.length - TITLE_SUFFIX.length);
+            this.set('title', new_title);
+        }
+        else {
+            Y.log("Error: suffix not present in title.");
+        }
+    },
+
+    bindUI: function() {
+        var self = this;
+        var input = this.get('input');
+        // Click on the input node: start title edition.
+        input.on('click', function(e) {
+            e.preventDefault();
+            self.titleEditStart(e.rangeOffset);
+        });
+        // Change is fired when the input text as changed and the focus is now
+        // set another element.
+        input.on('change', function(e) {
+            e.preventDefault();
+            self.titleEditEnd();
+        });
+        // Form submitted (Enter pressed in the input Node).
+        this.get('srcNode').on('submit', function(e) {
+            e.preventDefault();
+            self.titleEditEnd();
+        });
+    },
+
+   /**
+    * Start of title edition: remove suffix and focus.
+    *
+    * @method titleEditStart
+    */
+    titleEditStart: function(rangeOffset) {
+        if (!this._editing) {
+            this._editing = true;
+            this.removeSuffix();
+            this.get('input').focus();
+        }
+    },
+
+   /**
+    * End of title edition: add suffix, persist the title and blur.
+    *
+    * @method titleEditEnd
+    */
+    titleEditEnd: function() {
+        if (this._editing) {
+            this.titlePersist();
+            this.addSuffix();
+            this._editing = false;
+            this.get('input').blur();
+        }
+    },
+
+   /**
+    * Call the API to make the title persist.
+    *
+    * @method titlePersist
+    */
+    titlePersist: function() {
+        var title = this.get('title');
+        var input = this.get('input');
+        var cfg = {
+            method: 'POST',
+            sync: false,
+            data: Y.QueryString.stringify({
+                op: 'set_config',
+                name: 'maas_name',
+                value: title
+                }),
+            on: {
+                success: function(id, out) {
+                    green_flash(input).run();
+                },
+                failure: function(id, out) {
+                    red_flash(input).run();
+                    Y.log(out);
+                }
+            }
+        };
+        var request = module._io.send(
+            MaaS_config.uris.maas_handler, cfg);
+    }
+});
+
+module.TitleEditWidget = TitleEditWidget;
+
+}, '0.1', {'requires': ['base', 'widget', 'io', 'anim']}
 );

=== modified file 'src/maasserver/templates/maasserver/index.html'
--- src/maasserver/templates/maasserver/index.html	2012-03-01 06:22:38 +0000
+++ src/maasserver/templates/maasserver/index.html	2012-03-06 12:35:34 +0000
@@ -10,7 +10,9 @@
 {% block head %}
   <script type="text/javascript">
   <!--
-  YUI().use('maas.node_add', 'maas.node','maas.node_views', function (Y) {
+  YUI().use(
+    'maas.node_add', 'maas.node','maas.node_views', 'maas.utils',
+    function (Y) {
     Y.on('load', function() {
       // Create Dashboard view.
       var view_container = Y.Node.create('<div />')
@@ -27,6 +29,12 @@
           .addClass('button');
       Y.one('#content').append(add_node_link);
       add_node_link.on('click', Y.maas.node_add.showAddNodeWidget);
+
+      // Setup TitleEditWidget.
+      var title_widget = new Y.maas.utils.TitleEditWidget(
+	  {srcNode: '.page-title-form'});
+      title_widget.render();
+
     });
   });
   // -->
@@ -35,9 +43,9 @@
 
 {% block page-title-block %}
   {% if user.is_superuser %}
-    {% comment %}TODO: wire this up so that it changes the MaaS title{% endcomment %}
     <form action="" method="" class="page-title-form">
-      <input type="text" value="{{ global_options.site_name }}" title="Edit the name of this MaaS" />
+      <input type="text" value="{{ global_options.site_name }} MaaS"
+             title="Edit the name of this MaaS" />
     </form>
   {% else %}
     <h1 id="page-title">{% include "maasserver/site_title.html" %}</h1>

=== modified file 'src/maasserver/templates/maasserver/js-conf.html'
--- src/maasserver/templates/maasserver/js-conf.html	2012-02-24 17:37:01 +0000
+++ src/maasserver/templates/maasserver/js-conf.html	2012-03-06 12:35:34 +0000
@@ -9,6 +9,7 @@
     uris: {
         login: '{% url "login" %}',
         statics: '{{ STATIC_URL }}',
+        maas_handler: '{% url "maas_handler" %}',
         nodes_handler: '{% url "nodes_handler" %}',
         account_handler: '{% url "account_handler" %}'
         },