You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@allura.apache.org by ke...@apache.org on 2018/03/14 18:46:03 UTC

allura git commit: [#8196] Remember text in large form inputs before submitting

Repository: allura
Updated Branches:
  refs/heads/kt/8196 [created] 9c27942b1


[#8196] Remember text in large form inputs before submitting


Project: http://git-wip-us.apache.org/repos/asf/allura/repo
Commit: http://git-wip-us.apache.org/repos/asf/allura/commit/9c27942b
Tree: http://git-wip-us.apache.org/repos/asf/allura/tree/9c27942b
Diff: http://git-wip-us.apache.org/repos/asf/allura/diff/9c27942b

Branch: refs/heads/kt/8196
Commit: 9c27942b1e376c7c1c79a104f47d862b82f44919
Parents: b3027e3
Author: Kenton Taylor <kt...@slashdotmedia.com>
Authored: Wed Mar 14 14:35:48 2018 -0400
Committer: Kenton Taylor <kt...@slashdotmedia.com>
Committed: Wed Mar 14 14:35:48 2018 -0400

----------------------------------------------------------------------
 Allura/allura/controllers/discuss.py            |   9 +-
 Allura/allura/lib/decorators.py                 |  50 ++++
 Allura/allura/lib/widgets/form_fields.py        |   2 +-
 .../lib/widgets/resources/js/memorable.js       | 236 +++++++++++++++++++
 .../lib/widgets/resources/js/sf_markitup.js     |   2 +
 .../allura/templates/jinja_master/master.html   |   3 +
 ForgeBlog/forgeblog/main.py                     |   3 +-
 .../forgediscussion/controllers/root.py         |   3 +-
 ForgeTracker/forgetracker/tracker_main.py       |   4 +-
 .../forgetracker/widgets/ticket_form.py         |   2 +-
 ForgeWiki/forgewiki/wiki_main.py                |   3 +-
 11 files changed, 308 insertions(+), 9 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/allura/blob/9c27942b/Allura/allura/controllers/discuss.py
----------------------------------------------------------------------
diff --git a/Allura/allura/controllers/discuss.py b/Allura/allura/controllers/discuss.py
index 100ce44..d1a798f 100644
--- a/Allura/allura/controllers/discuss.py
+++ b/Allura/allura/controllers/discuss.py
@@ -19,8 +19,10 @@ from urllib import unquote
 from datetime import datetime
 import logging
 
-from tg import expose, redirect, validate, request, flash
-from tg.decorators import with_trailing_slash
+from tg import expose, redirect, validate, request, flash, response
+from tg.decorators import with_trailing_slash, before_render, before_validate
+from decorator import decorator
+
 from pylons import tmpl_context as c, app_globals as g
 from webob import exc
 
@@ -32,7 +34,7 @@ from allura import model as M
 from base import BaseController
 from allura.lib import utils
 from allura.lib import helpers as h
-from allura.lib.decorators import require_post
+from allura.lib.decorators import require_post, memorable_forget
 from allura.lib.security import require_access
 
 from allura.lib.widgets import discuss as DW
@@ -202,6 +204,7 @@ class ThreadController(BaseController, FeedController):
     def error_handler(self, *args, **kwargs):
         redirect(request.referer)
 
+    @memorable_forget()
     @h.vardec
     @expose()
     @require_post()

http://git-wip-us.apache.org/repos/asf/allura/blob/9c27942b/Allura/allura/lib/decorators.py
----------------------------------------------------------------------
diff --git a/Allura/allura/lib/decorators.py b/Allura/allura/lib/decorators.py
index 70e5833..214491f 100644
--- a/Allura/allura/lib/decorators.py
+++ b/Allura/allura/lib/decorators.py
@@ -32,6 +32,8 @@ from tg import request, redirect, session, config
 from tg.render import render
 from webob import exc
 from pylons import tmpl_context as c
+from pylons import response
+from webob.exc import HTTPFound
 
 from allura.lib import helpers as h
 from allura.lib import utils
@@ -246,3 +248,51 @@ def memoize(func, *args):
         result = func(*args)
         dic[args] = result
         return result
+
+
+def memorable_forget():
+    """
+    Decorator to mark a controller action as needing to "forget" remembered input values on the next
+    page render, if we detect that the form post was processed successfully
+    """
+
+    def _ok_to_forget(response, controller_result, raised):
+        """
+        Look for signals that say it's probably ok to forget remembered inputs for the current form.
+        Checks here will need to be expanded for controller actions that behave differently
+        than others upon successful processing of their particular request
+        """
+        # if there is a flash message with type "ok", then we can forget.
+        if response.headers:
+            set_cookie = response.headers.get('Set-Cookie', '')
+            if 'status%22%3A%20%22ok' in set_cookie:
+                return True
+
+        # if the controller raised a 302, we can assume the value will be remembered by the app
+        # if needed, and forget.
+        if raised and isinstance(raised, HTTPFound):
+            return True
+
+        return False
+
+    def forget(controller_result, raised=None):
+        """
+        Check if the form's inputs can be forgotten, and set the cookie to forget if so.
+        :param res: the result of the controller action
+        :param raised: any error (redirect or exception) raised by the controller action
+        """
+        if _ok_to_forget(response, controller_result, raised):
+            response.set_cookie('memorable_forget', request.path)
+
+    @decorator
+    def _inner(func, *args, **kwargs):
+        res, raised = (None, None)
+        try:
+            res = func(*args, **kwargs)
+            forget(res)
+            return res
+        except Exception as ex:
+            forget(None, ex)
+            raise ex
+
+    return _inner
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/allura/blob/9c27942b/Allura/allura/lib/widgets/form_fields.py
----------------------------------------------------------------------
diff --git a/Allura/allura/lib/widgets/form_fields.py b/Allura/allura/lib/widgets/form_fields.py
index 52d7e10..04bd7c5 100644
--- a/Allura/allura/lib/widgets/form_fields.py
+++ b/Allura/allura/lib/widgets/form_fields.py
@@ -280,7 +280,7 @@ class MarkdownEdit(ew.TextArea):
         yield ew.CSSLink('css/markitup_sf.css')
         yield ew.JSLink('js/simplemde.min.js')
         yield ew.JSLink('js/sf_markitup.js')
-
+        yield ew.JSLink('js/memorable.js')
 
 class PageList(ew_core.Widget):
     template = 'jinja:allura:templates/widgets/page_list.html'

http://git-wip-us.apache.org/repos/asf/allura/blob/9c27942b/Allura/allura/lib/widgets/resources/js/memorable.js
----------------------------------------------------------------------
diff --git a/Allura/allura/lib/widgets/resources/js/memorable.js b/Allura/allura/lib/widgets/resources/js/memorable.js
new file mode 100644
index 0000000..3ddf892
--- /dev/null
+++ b/Allura/allura/lib/widgets/resources/js/memorable.js
@@ -0,0 +1,236 @@
+/*global $, SF, console, jQuery, localStorage */
+window.SF = window.SF || {};
+SF.Memorable = {};
+
+/**
+ * Class that describes the management of a memorable input - identifying, watching, saving, restoring, and forgetting.
+ */
+SF.Memorable.InputManager = (function(){
+
+    /**
+     * @param inputObj - the InputBasic or InputMDE object representing the input to be tracked
+     * @constructor
+     */
+    function InputManager(inputObj){
+        this.inputObj = inputObj;
+        this.$form = this.inputObj.getForm();
+
+        this.inputObj.watchObj.on(this.inputObj.watchEvent, this.handleSave.bind(this));
+
+        this.forget();
+        this.restore();
+    }
+
+    /**
+     * Builds a unique key to use when persisting the input's value
+     * @returns {string}
+     */
+    InputManager.prototype.getStorageKey = function(){
+        function isUsableName($el){
+            var name = $el.attr('name');
+            if (name && name.length != 28){
+                return true;
+            }
+        }
+        function getRelativeAction($f){
+            var action = $f[0].action;
+            var list = action.split('/');
+            var relativeAction = "";
+            for (i = 3; i < list.length; i++) {
+              relativeAction += "/";
+              relativeAction += list[i];
+            }
+            return relativeAction;
+        }
+
+        var key = '';
+        var $f = this.$form;
+        var keySeparator = '__';
+        if ($f.attr('action')){
+            var relativeAction = getRelativeAction($f);
+            key += relativeAction;
+        }
+        if (isUsableName(this.inputObj.$el)) {
+            key += keySeparator + this.inputObj.$el.attr('name');
+        } else if (this.inputObj.$el.attr('class')) {
+            // id can't be relied upon, because of EW.  We can key off class, if it's the only one in the form.
+            var klass = this.inputObj.$el.attr('class');
+            if ($('.' + klass, $f).length == 1) {
+                key += keySeparator + klass;
+            } else {
+                throw "Element isn't memorable, it has no unique class";
+            }
+        } else {
+            throw "Element isn't memorable, it has no identifiable traits";
+        }
+        return key;
+    };
+
+    /**
+     * Gets the value of the tracked input field
+     */
+    InputManager.prototype.getValue = function(){
+        return this.inputObj.getValue();
+    };
+
+    /**
+     * Saves the input's value to local storage, and registers it as part of the form for later removal
+     */
+    InputManager.prototype.save = function(){
+        localStorage[this.getStorageKey()] = this.getValue();
+    };
+
+    /**
+     * Event handler for invoke the safe
+     * @param e
+     * @returns {boolean}
+     */
+    InputManager.prototype.handleSave = function(e){
+        if (e.preventDefault){
+            e.preventDefault();
+        }
+        this.save();
+        return false;
+    };
+
+    /**
+     * Fetches the tracked input's persisted value from storage
+     * @returns {string}
+     */
+    InputManager.prototype.storedValue = function(){
+        return localStorage[this.getStorageKey()];
+    };
+
+    /**
+     * Fetches the input's remembered value and restores it to the target field
+     */
+    InputManager.prototype.restore = function(){
+        if (!this.storedValue()){
+            return;
+        }
+        this.inputObj.setValue(this.storedValue());
+    };
+
+    /**
+     * Forgets any successfully processed inputs from user
+     */
+    InputManager.prototype.forget = function(){
+        var key_prefix = $.cookie('memorable_forget');
+        if (key_prefix) {
+            for (var i = localStorage.length -1; i >=0; i--) {
+                if(localStorage.key(i).indexOf(key_prefix) == 0){
+                    localStorage.removeItem(localStorage.key(i));
+                }
+            }
+            $.removeCookie('memorable_forget', { path: '/' });
+        }
+    };
+
+    return InputManager;
+})();
+
+
+/**
+ * Class describing a simple input field, as identified by a selector or DOM element, with specific methods for
+ * getting & setting the value, and finding it's parent form
+ *
+ * @property obj: the raw object representing the field to be tracked; a standard jquery object
+ * @property watchEvent: the name of the event to watch to detect when changes have been made
+ * @property watchObj: the object instance to watch for events on. same as this.obj
+ * @property $el: the jquery object representing the actual input field on the page. same as this.obj
+ */
+SF.Memorable.InputBasic = (function() {
+    /**
+     * @param obj: a selector or DOM Element identifying the basic input field to be remembered
+     * @constructor
+     */
+    function InputBasic(obj) {
+        this.obj = $(obj);
+        this.watchEvent = 'change';
+        this.watchObj = this.obj;
+        this.$el = this.obj;
+    }
+    InputBasic.prototype.getValue = function () {
+        return this.obj.val();
+    };
+    InputBasic.prototype.setValue = function (val) {
+        this.$el.val(val);
+    };
+    InputBasic.prototype.getForm = function () {
+        return this.$el.parents('form').eq(0);
+    };
+    return InputBasic;
+})();
+
+
+/**
+ * Class describing a field backed by SimpleMDE, as identified by the passed instance of `SimpleMDE` provided, with specific methods for
+ * getting & setting the value, and finding it's parent form
+ *
+ * @property obj: the SimpleMDE object describing the field to be tracked
+ * @property watchEvent: the name of the event to watch to detect when changes have been made
+ * @property watchObj: the object instance to watch for events on; editor.codemirror per their docs
+ * @property $el: the jquery object representing the actual input field on the page
+ */
+SF.Memorable.InputMDE = (function() {
+    /**
+     * @param obj: A SimpleMDE object representing the input field
+     * @constructor
+     */
+    function InputMDE(obj) {
+        this.obj = obj;
+        this.watchEvent = 'change';
+        this.watchObj = this.obj.codemirror;
+        this.$el= $(this.obj.element);
+    }
+    InputMDE.prototype.getValue = function () {
+        return this.obj.value();
+    };
+    InputMDE.prototype.setValue = function (val) {
+        this.obj.value(val);
+    };
+    InputMDE.prototype.getForm = function () {
+        return this.$el.parents('form').eq(0);
+    };
+    return InputMDE;
+})();
+
+
+/**
+ * Takes an arbitrary object, and determines the best Input class to represent it
+ */
+SF.Memorable.inputFactory = function(obj) {
+    if (obj.codemirror){
+        return SF.Memorable.InputMDE;
+    } else {
+        return SF.Memorable.InputBasic;
+    }
+};
+
+
+/**
+ * Convenience method to find any classes decorated with `.memorable` and create a related Input object for it
+ * @param selector - use to override the selector used to find all fields to be remembered
+ */
+SF.Memorable.initialize = function(selector){
+    var s = selector || '.memorable';
+    SF.Memorable.items = [];
+    $(s).each(function(){
+        var cls = SF.Memorable.inputFactory(this);
+        SF.Memorable.items.push(new SF.Memorable.InputManager(new cls(this)));
+    });
+};
+
+
+/**
+ * Creates a new Input object to remember changes to an individual field
+ * @param obj - the raw object representing the field to be tracked
+ */
+SF.Memorable.add = function(obj){
+    var cls = SF.Memorable.inputFactory(obj);
+    SF.Memorable.items.push(new SF.Memorable.InputManager(new cls(obj)));
+};
+
+
+// Initialize
+$(function(){SF.Memorable.initialize();});
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/allura/blob/9c27942b/Allura/allura/lib/widgets/resources/js/sf_markitup.js
----------------------------------------------------------------------
diff --git a/Allura/allura/lib/widgets/resources/js/sf_markitup.js b/Allura/allura/lib/widgets/resources/js/sf_markitup.js
index 1e9a69d..09fe07b 100644
--- a/Allura/allura/lib/widgets/resources/js/sf_markitup.js
+++ b/Allura/allura/lib/widgets/resources/js/sf_markitup.js
@@ -57,6 +57,7 @@ $(window).load(function() {
               autofocus: false,
               spellChecker: false, // https://forge-allura.apache.org/p/allura/tickets/7954/
               indentWithTabs: false,
+              autosave: true,
               tabSize: 4,
               toolbar: toolbar,
               previewRender: previewRender,
@@ -74,6 +75,7 @@ $(window).load(function() {
                   "toggleUnorderedList": null, // default is cmd-l, used to activate location bar
               }
             });
+            SF.Memorable.add(editor);
             // https://github.com/codemirror/CodeMirror/issues/1576#issuecomment-19146595
             // can't use simplemde's shortcuts settings, since those only hook into bindings set up for each button
             editor.codemirror.options.extraKeys.Home = "goLineLeft";

http://git-wip-us.apache.org/repos/asf/allura/blob/9c27942b/Allura/allura/templates/jinja_master/master.html
----------------------------------------------------------------------
diff --git a/Allura/allura/templates/jinja_master/master.html b/Allura/allura/templates/jinja_master/master.html
index c9e185a..9fb165e 100644
--- a/Allura/allura/templates/jinja_master/master.html
+++ b/Allura/allura/templates/jinja_master/master.html
@@ -29,6 +29,7 @@
 {% do g.register_forge_js('js/twemoji.min.js') %}
 {% do g.register_forge_js('js/pb.transformie.min.js') %}
 {% do g.register_forge_js('js/allura-base.js') %}
+{#{% do g.register_forge_js('js/sisyphus.js') %}#}
 {% do g.register_forge_css('css/forge/hilite.css') %}
 {% do g.register_forge_css('css/forge/tooltipster.css') %}
 {% do g.register_forge_css('css/font-awesome.min.css', compress=False) %}
@@ -186,6 +187,8 @@
         }).blur(function () {
             $(this).tooltipster('hide');
         });
+
+        {#$('form').sisyphus();#}
     });
 </script>
 </body>

http://git-wip-us.apache.org/repos/asf/allura/blob/9c27942b/ForgeBlog/forgeblog/main.py
----------------------------------------------------------------------
diff --git a/ForgeBlog/forgeblog/main.py b/ForgeBlog/forgeblog/main.py
index 578432c..d20658d 100644
--- a/ForgeBlog/forgeblog/main.py
+++ b/ForgeBlog/forgeblog/main.py
@@ -40,7 +40,7 @@ from allura.app import DefaultAdminController
 from allura.lib import helpers as h
 from allura.lib.utils import JSONForExport
 from allura.lib.search import search_app
-from allura.lib.decorators import require_post
+from allura.lib.decorators import require_post, memorable_forget
 from allura.lib.security import has_access, require_access
 from allura.lib import widgets as w
 from allura.lib import exceptions as forge_exc
@@ -386,6 +386,7 @@ class PostController(BaseController, FeedController):
         result = h.diff_text(p1.text, p2.text)
         return dict(p1=p1, p2=p2, edits=result)
 
+    @memorable_forget()
     @expose()
     @require_post()
     @validate(form=W.edit_post_form, error_handler=edit)

http://git-wip-us.apache.org/repos/asf/allura/blob/9c27942b/ForgeDiscussion/forgediscussion/controllers/root.py
----------------------------------------------------------------------
diff --git a/ForgeDiscussion/forgediscussion/controllers/root.py b/ForgeDiscussion/forgediscussion/controllers/root.py
index 176156f..641e757 100644
--- a/ForgeDiscussion/forgediscussion/controllers/root.py
+++ b/ForgeDiscussion/forgediscussion/controllers/root.py
@@ -35,7 +35,7 @@ from allura.lib.security import require_access, has_access, require_authenticate
 from allura.lib.search import search_app
 from allura.lib import helpers as h
 from allura.lib.utils import AntiSpam
-from allura.lib.decorators import require_post
+from allura.lib.decorators import require_post, memorable_forget
 from allura.controllers import BaseController, DispatchIndex
 from allura.controllers.rest import AppRestControllerMixin
 from allura.controllers.feed import FeedArgs, FeedController
@@ -117,6 +117,7 @@ class RootController(BaseController, DispatchIndex, FeedController):
                 my_forums.append(f)
         return dict(forums=my_forums, current_forum=current_forum)
 
+    @memorable_forget()
     @h.vardec
     @expose()
     @require_post()

http://git-wip-us.apache.org/repos/asf/allura/blob/9c27942b/ForgeTracker/forgetracker/tracker_main.py
----------------------------------------------------------------------
diff --git a/ForgeTracker/forgetracker/tracker_main.py b/ForgeTracker/forgetracker/tracker_main.py
index 8c8e626..8262986 100644
--- a/ForgeTracker/forgetracker/tracker_main.py
+++ b/ForgeTracker/forgetracker/tracker_main.py
@@ -55,7 +55,7 @@ from allura.app import (
 )
 from allura.lib.search import search_artifact, SearchError
 from allura.lib.solr import escape_solr_arg
-from allura.lib.decorators import require_post
+from allura.lib.decorators import require_post, memorable_forget
 from allura.lib.security import (require_access, has_access, require,
                                  require_authenticated)
 from allura.lib import widgets as w
@@ -916,6 +916,7 @@ class RootController(BaseController, FeedController):
         """Static page explaining markdown."""
         return dict()
 
+    @memorable_forget()
     @expose()
     @h.vardec
     @require_post()
@@ -1429,6 +1430,7 @@ class TicketController(BaseController, FeedController):
             post_data['labels'] = []
         self._update_ticket(post_data)
 
+    @memorable_forget()
     @expose()
     @require_post()
     @h.vardec

http://git-wip-us.apache.org/repos/asf/allura/blob/9c27942b/ForgeTracker/forgetracker/widgets/ticket_form.py
----------------------------------------------------------------------
diff --git a/ForgeTracker/forgetracker/widgets/ticket_form.py b/ForgeTracker/forgetracker/widgets/ticket_form.py
index 476d9b3..24b97cb 100644
--- a/ForgeTracker/forgetracker/widgets/ticket_form.py
+++ b/ForgeTracker/forgetracker/widgets/ticket_form.py
@@ -101,7 +101,7 @@ class GenericTicketForm(ew.SimpleForm):
     def fields(self):
         fields = [
             ew.TextField(name='summary', label='Title',
-                         attrs={'style': 'width: 425px',
+                         attrs={'style': 'width: 425px', 'class':'memorable',
                                 'placeholder': 'Title'},
                          validator=fev.UnicodeString(
                              not_empty=True, messages={

http://git-wip-us.apache.org/repos/asf/allura/blob/9c27942b/ForgeWiki/forgewiki/wiki_main.py
----------------------------------------------------------------------
diff --git a/ForgeWiki/forgewiki/wiki_main.py b/ForgeWiki/forgewiki/wiki_main.py
index 377c50c..12e2e6e 100644
--- a/ForgeWiki/forgewiki/wiki_main.py
+++ b/ForgeWiki/forgewiki/wiki_main.py
@@ -36,7 +36,7 @@ from allura import model as M
 from allura.lib import helpers as h
 from allura.app import Application, SitemapEntry, DefaultAdminController, ConfigOption
 from allura.lib.search import search_app
-from allura.lib.decorators import require_post
+from allura.lib.decorators import require_post, memorable_forget
 from allura.lib.security import require_access, has_access
 from allura.lib.utils import is_ajax, JSONForExport
 from allura.lib import exceptions as forge_exc
@@ -699,6 +699,7 @@ class PageController(BaseController, FeedController):
         self.page.commit()
         return dict(location='.')
 
+    @memorable_forget()
     @without_trailing_slash
     @h.vardec
     @expose()