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
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
query TSQUERY;
opac_visibility_join TEXT;
search_class_join TEXT;
+ r_fields RECORD;
BEGIN
query := TO_TSQUERY(query_text);
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)
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
--- /dev/null
+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(/>/, ">").
+ replace(/</, "<").
+ replace(/&/, "&");
+ 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
+ };
+ }
+ });
+}