whoo! have a ui much of the way there based on dijit.form.FilteringSelect
authorLebbeous Fogle-Weekley <lebbeous@esilibrary.com>
Wed, 18 Jan 2012 23:18:20 +0000 (18:18 -0500)
committerLebbeous Fogle-Weekley <lebbeous@esilibrary.com>
Mon, 23 Jan 2012 17:26:58 +0000 (12:26 -0500)
not done though:
    * still need elegant way to decode html entities, and should probably
        just have the apache module return JSON so we don't have to
    * need to get the widget to never try to validate
    * need to load all cmf objects so we can do class|field in the label
        instead of just id
    * when you click on an entry that's not going to be the first
        suggest result even when it is the exact search term you use,
        the dijit changes your selection to whatever is the first result.
        May need to override some dijit methods to overcome. Not sure yet.
    * possibly something still wrong when dealing with numerical ranges
    * more things I'm forgetting just now.

Signed-off-by: Lebbeous Fogle-Weekley <lebbeous@esilibrary.com>
Open-ILS/src/perlmods/lib/OpenILS/WWW/AutoSuggest.pm
Open-ILS/src/sql/Pg/upgrade/YYYY.schema.bib_autosuggest.sql
Open-ILS/web/dijittest.2.html [new file with mode: 0644]
Open-ILS/web/js/dojo/openils/AutoSuggestStore.js [new file with mode: 0644]
Open-ILS/web/js/dojo/openils/BibTemplate.js
Open-ILS/web/js/dojo/openils/Util.js

index 7c1636d..79fff88 100644 (file)
@@ -46,7 +46,6 @@ sub get_suggestions {
     my $highlight_min = int(shift || 0);
     my $highlight_max = int(shift || 0);
     my $limit = int(shift || 10);
-    my $restrict = int(shift || 0);
 
     $limit = 10 unless $limit > 0;
 
@@ -60,8 +59,7 @@ sub get_suggestions {
             prepare_for_tsquery($query),
             $search_class,
             $headline_opts,
-            $org_unit,
-            $restrict
+            $org_unit
         ]
     });
 }
@@ -103,7 +101,6 @@ sub handler {
             highlight_min
             highlight_max
             limit
-            restrict
         )
     );
 
index 8f35bdf..5f9a7c2 100644 (file)
@@ -21,6 +21,7 @@ ALTER TABLE config.metabib_field ADD COLUMN browse_xpath TEXT;
 
 ALTER TABLE config.metabib_class ADD COLUMN bouyant BOOLEAN DEFAULT FALSE NOT NULL;
 ALTER TABLE config.metabib_class ADD COLUMN restrict BOOLEAN DEFAULT FALSE NOT NULL;
+ALTER TABLE config.metabib_field ADD COLUMN restrict BOOLEAN DEFAULT FALSE NOT NULL;
 
 CREATE OR REPLACE FUNCTION metabib.browse_normalize(facet_text TEXT, mapped_field INT) RETURNS TEXT AS $$
 DECLARE
@@ -346,8 +347,7 @@ CREATE OR REPLACE
         query_text      TEXT,   -- 'foo' or 'foo & ba:*',ready for to_tsquery()
         search_class    TEXT,   -- 'alias' or 'class' or 'class|field..', etc
         headline_opts   TEXT,   -- markup options for ts_headline()
-        visibility_org  INTEGER,-- null if you don't want opac visibility test
-        strict_match    BOOL    -- actually limit to what matches search_class
+        visibility_org  INTEGER -- null if you don't want opac visibility test
     ) RETURNS TABLE (
         value                   TEXT,   -- plain
         match                   TEXT,   -- marked up
@@ -362,6 +362,7 @@ DECLARE
     query                   TSQUERY;
     opac_visibility_join    TEXT;
     search_class_join       TEXT;
+    r_fields                RECORD;
 BEGIN
     query := TO_TSQUERY(query_text);
 
@@ -375,7 +376,28 @@ BEGIN
         opac_visibility_join := '';
     END IF;
 
-    IF strict_match THEN
+    -- The following determines whether we only provide suggestsons matching
+    -- the user's selected search_class, or whether we show other suggestions
+    -- too. The reason for MIN() is that for search_classes like
+    -- 'title|proper|uniform' you would otherwise get multiple rows.  The
+    -- implication is that if title as a class doesn't have restrict,
+    -- nor does the proper field, but the uniform field does, you're going
+    -- to get 'false' for your overall evaluation of 'should we restrict?'
+    -- To invert that, change from MIN() to MAX().
+
+    SELECT
+        INTO r_fields
+            MIN(cmc.restrict::INT) AS restrict_class,
+            MIN(cmf.restrict::INT) AS restrict_field
+        FROM metabib.search_class_to_registered_components(search_class)
+            AS _registered (field_class TEXT, field INT)
+        JOIN
+            config.metabib_class cmc ON (cmc.name = _registered.field_class)
+        LEFT JOIN
+            config.metabib_field cmf ON (cmf.id = _registered.field);
+
+    -- evaluate 'should we restrict?'
+    IF r_fields.restrict_field::BOOL OR r_fields.restrict_class::BOOL THEN
         search_class_join := '
     JOIN
         metabib.search_class_to_registered_components($2)
@@ -413,7 +435,7 @@ BEGIN
     ORDER BY 4 DESC, 5 DESC NULLS LAST, 6 DESC, 7 DESC, 8 DESC
     ' USING query, search_class, headline_opts, visibility_org;
 
-    -- sort order by after talking to mike:
+    -- sort order:
     --  bouyant AND chosen class = match class
     --  chosen field = match field
     --  field weight
diff --git a/Open-ILS/web/dijittest.2.html b/Open-ILS/web/dijittest.2.html
new file mode 100644 (file)
index 0000000..d98f137
--- /dev/null
@@ -0,0 +1,54 @@
+<html>
+       <head>
+        <style type="text/css">
+            @import "/js/dojo/dojo/resources/dojo.css";
+            @import "/js/dojo/dijit/themes/tundra/tundra.css";
+            .oils_AS { font-weight: bold; color: red; }
+            .one { text-align: left; }
+            .two { font-size: 75%; padding: 0.65em 0; text-align: right;}
+        </style>
+               <script src="/js/dojo/dojo/dojo.js" type="text/javascript"></script>
+        <script type="text/javascript">
+            dojo.require("dijit.form.Button");
+            dojo.require("dijit.form.FilteringSelect");
+//            dojo.require("dojo.data.ItemFileReadStore");
+            dojo.require("openils.AutoSuggestStore");
+
+            function go() {
+//                var store = new dojo.data.ItemFileReadStore(
+//                    {
+//                        "data": {
+//                            "identifier": "name",
+//                            "items": [
+//                                {"name": "one", "label": "1", "odd": true},
+//                                {"name": "two", "label": "2", "prime": true, "odd": false},
+//                                {"name": "three", "label": "3", "odd": true, "prime": true},
+//                                {"name": "four", "label": "4", "odd": false},
+//                                {"name": "five", "label": "5", "odd": true, "prime": true},
+//                                {"name": "six", "label": "6", "odd": false}
+//                            ]
+//                        }
+//                    }
+//                );
+
+                var store = new openils.AutoSuggestStore();
+
+                var widg = new dijit.form.FilteringSelect({
+                    "store": store,
+                    "labelAttr": "match",
+                    "labelType": "html",
+                    "searchAttr": "term",
+                    "hasDownArrow": false,
+                    "autoComplete": false,
+                    "style": {"width": "20em"}
+                }, "here");
+            }
+
+            dojo.addOnLoad(go);
+        </script>
+       </head>
+    <body class="tundra">
+        <h1>autosuggest dijit.form.FilteringSelect test</h1>
+        <div id="here"></div>
+    </body>
+</html>
diff --git a/Open-ILS/web/js/dojo/openils/AutoSuggestStore.js b/Open-ILS/web/js/dojo/openils/AutoSuggestStore.js
new file mode 100644 (file)
index 0000000..a296adf
--- /dev/null
@@ -0,0 +1,348 @@
+if (!dojo._hasResource["openils.AutoSuggestStore"]) {
+    dojo._hasResource["openils.AutoSuggestStore"] = true;
+    dojo.provide("openils.AutoSuggestStore");
+    dojo.require("dojox.html");
+    dojo.require("openils.Util");
+
+    /* an exception class specific to openils.AutoSuggestStore */
+    function AutoSuggestStoreError(message) { this.message = message; }
+    AutoSuggestStoreError.prototype.toString = function() {
+        return "openils.AutoSuggestStore: " + this.message;
+    };
+
+    /* XXX TODO make this smarter */
+    var _autosuggest_fields = ["id", "match", "term", "field"];
+
+    dojo.declare(
+        "openils.AutoSuggestStore", null, {
+
+        "constructor": function(/* object */ args) {
+            this._current_items = {};
+        },
+
+        /* req will have attribute 'query' and possibly 'limit', for now */
+        "_prepare_autosuggest_url": function(req) {
+            var term = req.query.term;
+
+            if (!term || term.length < 1 || term == "*")
+                return null;
+
+            if (term.match(/[^\s*]$/))
+                term += " ";
+            term = term.replace(/\*$/, "");
+
+//            console.log("transformed query: '" + term + "'");
+
+            /* XXX limit! org_unit! */
+            return "/opac/extras/autosuggest?query=" +
+                encodeURI(term) + "&search_class=title";
+        },
+
+        /* *** Begin dojo.data.api.Read methods *** */
+
+        "getValue": function(
+            /* object */ item,
+            /* string */ attribute,
+            /* anything */ defaultValue) {
+            //  summary:
+            //      Given an *item* and the name of an *attribute* on that item,
+            //      return that attribute's value.
+//            console.log("getValue(" + item + ", " + attribute + ")");
+            if (!this.isItem(item))
+                throw new AutoSuggestStoreError("getValue(): bad item: " + item);
+            else if (typeof attribute != "string")
+                throw new AutoSuggestStoreError("getValue(): bad attribute");
+
+            var value = item[attribute];
+
+            return (typeof value == "undefined") ? defaultValue : value;
+        },
+
+        "getValues": function(/* object */ item, /* string */ attribute) {
+            //  summary:
+            //      Same as getValue(), except the result is always an array
+            //      and there is no way to specify a default value.
+//            console.log("getValues()");
+            if (!this.isItem(item) || typeof attribute != "string")
+                throw new AutoSuggestStoreError("bad arguments");
+
+            var result = this.getValue(item, attribute, []);
+            return dojo.isArray(result) ? result : [result];
+        },
+
+        "getAttributes": function(/* object */ item) {
+            //  summary:
+            //      Return an array of all of the given *item*'s *attribute*s.
+            //      This is done by consulting fieldmapper.
+//            console.log("getAttributes()");
+            if (!this.isItem(item))
+                throw new AutoSuggestStoreError("getAttributes(): bad arguments");
+            else
+                return _autosuggest_fields;
+        },
+
+        "hasAttribute": function(/* object */ item, /* string */ attribute) {
+            //  summary:
+            //      Return true or false based on whether *item* has an
+            //      attribute by the name specified in *attribute*.
+//            console.log("hasAttribute()");
+            if (!this.isItem(item) || typeof attribute != "string") {
+                throw new AutoSuggestStoreError("hasAttribute(): bad arguments");
+            } else {
+                return (_autosuggest_fields.indexOf(attribute) >= 0);
+            }
+        },
+
+        "containsValue": function(
+            /* object */ item,
+            /* string */ attribute,
+            /* anything */ value) {
+            //  summary:
+            //      Return true or false based on whether *item* has any value
+            //      matching *value* for *attribute*.
+//            console.log("containsValue(" + item + ", " + attribute + ", " + value + ")");
+            if (!this.isItem(item) || typeof attribute != "string")
+                throw new AutoSuggestStoreError("bad data");
+            else
+                return (
+                    dojo.indexOf(this.getValues(item, attribute), value) != -1
+                );
+        },
+
+        "isItem": function(/* anything */ something) {
+            //  summary:
+            //      Return true if *something* is an item (loaded or not
+            //      because to use everything is loaded, really.
+//            console.log("isItem(" + something + ")");
+            if (typeof something != "object" || something === null)
+                return false;
+
+            for (var i = 0; i < _autosuggest_fields.length; i++) {
+                var cur = _autosuggest_fields[i];
+                if (typeof something[cur] == "undefined")
+                    return false;
+            }
+            return true
+        },
+
+        "isItemLoaded": function(/* anything */ something) {
+            //  summary:
+            //      Return true if *something* is an item.  It's always
+            //      "loaded" in this store.
+//            console.log("isItemLoaded()");
+            return this.isItem(something);
+        },
+
+        "close": function(/* object */ request) {
+            //  summary:
+            //      This is a no-op.
+            return;
+        },
+
+        "getLabel": function(/* object */ item) {
+            //  summary:
+            //      Return the name of the attribute that should serve as the
+            //      label for objects of the same class as *item*.
+//            console.log("getLabel(" + item + ")");
+            return "match";
+        },
+
+        "getLabelAttributes": function(/* object */ item) {
+            //  summary:
+            //      XXX !?
+//            console.log("getLabelAttributes(" + item + ")");
+            return ["match"];
+        },
+
+        "loadItem": function(/* object */ keywordArgs) {
+            //  summary:
+            //      Fully load the item specified in the *item* property of
+            //      *keywordArgs*
+            //
+            //  description:
+            //      This ultimately just returns the same object it's given.
+            //      That's because everything fetched is always fully loaded
+            //      with this store implementation.  So this method only
+            //      exists for API completeness.
+//            console.log("loadItem(" + dojo.toJson(keywordArgs) + ")");
+            if (!this.isItem(keywordArgs.item))
+                throw new AutoSuggestStoreError("that's not an item; can't load it");
+
+            keywordArgs.identity = this.getIdentity(item);
+            return this.fetchItemByIdentity(keywordArgs);
+        },
+
+        "fetch": function(/* request-object */ req) {
+            //  summary:
+            //      Basically, fetch objects matching the *query* property of
+            //      the *req* parameter.
+            //
+            //  description:
+            //      Translate the *query* into a call we make to the
+            //      autosuggest webserver module, which yields XML for us,
+            //      and translate those results into items, storing them
+            //      in our internal cache.
+            //
+            //      We also respect the following properties of the *req*
+            //      object (all optional):
+            //
+            //          limit    an int
+            //          onBegin  a callback that takes the number of items
+            //                      that this call to fetch() will return, but
+            //                      we always give it -1 (i.e. unknown)
+            //          onItem   a callback that takes each item as we get it
+            //          onComplete  a callback that takes the list of items
+            //                          after they're all fetched
+            //
+            //      The onError callback is ignored for now (haven't thought
+            //      of anything useful to do with it yet).
+            //
+            //      The Read API also charges this method with adding an abort
+            //      callback to the *req* object for the caller's use, but
+            //      the one we provide does nothing but issue an alert().
+//            console.log("fetch(" + dojo.toJson(openils.Util.objectProperties(req)) + ")");
+
+            var callback_scope = req.scope || dojo.global;
+            var url = this._prepare_autosuggest_url(req);
+
+            if (!url) {
+//                console.log("what happens if we do nothing?");
+                if (typeof req.onComplete == "function") {
+//                    console.log("  except calling onComplete!");
+                    var results = openils.Util.objectValues(this._current_items);
+//                    console.log(" results are " + results.length + " item(s)");
+                    req.onComplete.call(callback_scope, results, req);
+                }
+
+                return;
+            }
+
+            this._current_items = {};
+
+            /* set up some closures... */
+            var self = this;
+
+            var process_fetch = function(xmldoc) {
+//                console.log("inside process_fetch");
+                dojo.query("as val", xmldoc).forEach(
+                    function(val) {
+                        var item = {};
+                        item.field = val.getAttribute("field");
+                        item.term = val.getAttribute("term");
+
+                        /* XXX obv this next bit needs improvement, but it
+                         * shows that what we want to do is plenty possible */
+                        item.match =
+                            val.textContent.replace(/&gt;/, ">").
+                            replace(/&lt;/, "<").
+                            replace(/&amp;/, "&");
+                        item.match = "<div><div class='one'>" + item.match +
+                            "</div><div class='two'>" + item.field + "</div></div>";
+                        item.id = item.field + "_" + item.term;
+
+                        self._current_items[item.id] = item;
+
+                        if (typeof req.onItem == "function")
+                            req.onItem.call(callback_scope, item, req);
+                    }
+                );
+
+                if (typeof req.onComplete == "function") {
+                    req.onComplete.call(
+                        callback_scope,
+                        openils.Util.objectValues(self._current_items),
+                        req
+                    );
+                }
+            };
+
+            req.abort = function() {
+                alert("The 'abort' operation is not supported");
+            };
+
+            /* ... and proceed. */
+
+            if (typeof req.onBegin == "function")
+                req.onBegin.call(callback_scope, -1, req);
+
+            dojo.xhrGet(
+                {
+                    "url": url,
+                    "handleAs": "xml",
+                    "sync": false,
+                    "preventCache": false,
+                    "load": process_fetch
+                }
+            );
+
+            /* as for onError: what to do? */
+
+            return req;
+        },
+
+        /* *** Begin dojo.data.api.Identity methods *** */
+
+        "getIdentity": function(/* object */ item) {
+            //  summary:
+            //      Given an *item* return its unique identifier (the value
+            //      of its primary key).
+//            console.log("getIdentity(" + item + " [" + item.id + "])");
+            if (!this.isItem(item))
+                throw new AutoSuggestStoreError("not an item");
+
+            return item.id;
+        },
+
+        "getIdentityAttributes": function(/* object */ item) {
+            //  summary:
+            //      Given an *item* return the list of the name of the fields
+            //      that constitute the item's unique identifier. 
+//            console.log("getIdentityAttributes()");
+            return ["id"];
+        },
+
+        "fetchItemByIdentity": function(/* object */ keywordArgs) {
+            //  summary:
+            //      Given an *identity* property in the *keywordArgs* object,
+            //      retrieve an item, unless we already have the fully loaded
+            //      item in the store's internal memory.
+            //
+            //  description:
+            //      Once we've have the item we want one way or another, issue
+            //      the *onItem* callback from the *keywordArgs* object.  If we
+            //      tried to retrieve the item with pcrud but didn't get an item
+            //      back, issue the *onError* callback.
+//            console.log("fetchItemByIdentity(" + dojo.toJson(keywordArgs) + ")");
+            if (keywordArgs.identity == undefined)
+                return null; // Identity API spec unclear whether error callback
+                             // would need to be run, so we won't.
+            var callback_scope = keywordArgs.scope || dojo.global;
+
+            var item;
+            if (item = this._current_items[keywordArgs.identity]) {
+//                console.log(
+//                    "fetchItemByIdentity(): already have " +
+//                    keywordArgs.identity
+//                );
+                if (typeof keywordArgs.onItem == "function")
+                    keywordArgs.onItem.call(callback_scope, item);
+
+                return item;
+            } else {
+                if (typeof keywordArgs.onError == "function")
+                    keywordArgs.onError.call(callback_scope, E);
+
+                return null;
+            }
+        },
+
+        /* *** This last method is for classes implementing any dojo APIs *** */
+
+        "getFeatures": function() {
+            return {
+                "dojo.data.api.Read": true,
+                "dojo.data.api.Identity": true
+            };
+        }
+    });
+}
index c130dd1..8c81447 100644 (file)
@@ -267,21 +267,16 @@ if(!dojo._hasResource["openils.BibTemplate"]) {
             var me = this;
 
             var process_feed = function (xmldoc) {
-                console.log("in process_feed()");
-                if (parseInt(me.horizon) >= parseInt(me.root.getAttribute('horizon'))) {
+               if (parseInt(me.horizon) >= parseInt(me.root.getAttribute('horizon'))) {
                     if (me.empty == true) dojo.empty(me.place);
                     me.root.setAttribute('horizon', this.horizon);
-                    console.log("about to do bibtemplate loop, item_query is " + me.item_query);
                     dojo.query( me.item_query, xmldoc ).forEach(
                         function (item) {
                             var template = me.root.cloneNode(true);
                             dojo.place( template, me.place, me.relativePosition );
-                            console.log("instantiating a bibtemplate");
                             new openils.BibTemplate({ delay : false, xml : item, root : template });
                         }
                     );
-                } else {
-                    console.log("horizon test failed");
                 }
             };
 
index 077ff5a..5745fad 100644 (file)
@@ -372,6 +372,16 @@ if(!dojo._hasResource["openils.Util"]) {
         return K;
     }
 
+    /**
+     * Return the values of an object as a list. There may be a Dojo
+     * idiom or something that makes this redundant. Check into that.
+     */
+    openils.Util.objectValues = function(obj) {
+        var V = [];
+        for (var k in obj) V.push(obj[k]);
+        return V;
+    }
+
     openils.Util.uniqueElements = function(L) {
         var o = {};
         for (var k in L) o[L[k]] = true;