Acq: Several improvements to the LI search interface
authorsenator <senator@dcc99617-32d9-48b4-a31d-7c20da2025e4>
Tue, 16 Feb 2010 15:00:34 +0000 (15:00 +0000)
committersenator <senator@dcc99617-32d9-48b4-a31d-7c20da2025e4>
Tue, 16 Feb 2010 15:00:34 +0000 (15:00 +0000)
- You can export a file of ISBN numbers from any interface using an
    LI table.
- You can use multiple search terms in the LI search interface.
- The LI search interface no longer returns too many results in tested cases.
- You can make a brief bib record out of your LI search terms.
- Little bit of misc cleanup.

git-svn-id: svn://svn.open-ils.org/ILS/trunk@15545 dcc99617-32d9-48b4-a31d-7c20da2025e4

Open-ILS/src/perlmods/OpenILS/Application/Acq/Lineitem.pm
Open-ILS/web/css/skin/default/acq.css
Open-ILS/web/js/dojo/openils/XUL.js
Open-ILS/web/js/dojo/openils/acq/nls/acq.js
Open-ILS/web/js/ui/default/acq/common/li_table.js
Open-ILS/web/js/ui/default/acq/lineitem/search.js
Open-ILS/web/js/ui/default/acq/picklist/brief_record.js
Open-ILS/web/templates/default/acq/common/li_table.tt2
Open-ILS/web/templates/default/acq/lineitem/search.tt2

index d6f0076..499fb14 100644 (file)
@@ -310,7 +310,9 @@ sub lineitem_search_by_attributes {
     my $po_agencies = $search->{po_agencies}; # XXX if none, base it on perms
 
     my $query = {
-        "select" => {"acqlia" => ["lineitem"]},
+        "select" => {"acqlia" =>
+            [{"column" => "lineitem", "transform" => "distinct"}]
+        },
         "from" => {
             "acqlia" => {
                 "acqliad" => {"field" => "id", "fkey" => "definition"},
index 9c1c250..78f6a11 100644 (file)
@@ -118,6 +118,8 @@ input.oils-acq-li-search { margin: 0 12px; }
 label.oils-acq-li-search { margin: 0 12px; }
 span.oils-acq-li-search { margin: 0 12px; }
 span#records-up { font-weight: bold; }
+label[for="attr_search_type_scalar"] { vertical-align: top; }
+.oils-acq-li-search-scalar { margin: 8px 0; }
 
 #acq-lit-table {width:100%}
 #acq-lit-table th {padding:5px; font-weight: bold; text-align:left;}
index 6a39d19..c93cca2 100644 (file)
@@ -66,6 +66,10 @@ if(!dojo._hasResource["openils.XUL"]) {
             "iface": Components.interfaces.nsIScriptableInputStream,
             "cls": "@mozilla.org/scriptableinputstream;1"
         },
+        "FOS": {
+            "iface": Components.interfaces.nsIFileOutputStream,
+            "cls": "@mozilla.org/network/file-output-stream;1"
+        },
         "create": function(key) {
             return Components.classes[this[key].cls].
                 createInstance(this[key].iface);
@@ -75,33 +79,50 @@ if(!dojo._hasResource["openils.XUL"]) {
         }
     };
 
-    openils.XUL.contentFromFileOpenDialog = function(windowTitle) {
-        try {
-            var api = new openils.XUL.SimpleXPCOM();
+    openils.XUL.contentFromFileOpenDialog = function(windowTitle, sizeLimit) {
+        var api = new openils.XUL.SimpleXPCOM();
 
-            /* The following enablePrivilege() call must happen at this exact
-             * level of scope -- not wrapped in another function -- otherwise
-             * it doesn't work. */
-            api.getPrivilegeManager().enablePrivilege("UniversalXPConnect");
+        /* The following enablePrivilege() call must happen at this exact
+         * level of scope -- not wrapped in another function -- otherwise
+         * it doesn't work. */
+        api.getPrivilegeManager().enablePrivilege("UniversalXPConnect");
 
-            var picker = api.create("FP");
-            picker.init(
-                window, windowTitle || "Upload File", api.FP.iface.modeOpen
-            );
-            if (picker.show() == api.FP.iface.returnOK && picker.file) {
-                var fis = api.create("FIS");
-                var sis = api.create("SIS");
+        var picker = api.create("FP");
+        picker.init(
+            window, windowTitle || "Upload File", api.FP.iface.modeOpen
+        );
+        if (picker.show() == api.FP.iface.returnOK && picker.file) {
+            var fis = api.create("FIS");
+            var sis = api.create("SIS");
 
-                fis.init(picker.file, 1 /* MODE_RDONLY */, 0, 0);
-                sis.init(fis);
+            fis.init(picker.file, 1 /* MODE_RDONLY */, 0, 0);
+            sis.init(fis);
 
-                return sis.read(-1);
-            } else {
-                return null;
-            }
-        } catch(E) {
-            alert(E);
+            return sis.read(sizeLimit || -1);
+        } else {
             return null;
         }
     };
+
+    openils.XUL.contentToFileSaveDialog = function(content, windowTitle) {
+        var api = new openils.XUL.SimpleXPCOM();
+        api.getPrivilegeManager().enablePrivilege("UniversalXPConnect");
+
+        var picker = api.create("FP");
+        picker.init(
+            window, windowTitle || "Save File", api.FP.iface.modeSave
+        );
+        var result = picker.show();
+        if (picker.file &&
+                (result == api.FP.iface.returnOK ||
+                    result == api.FP.iface.returnReplace)) {
+            if (!picker.file.exists())
+                picker.file.create(0, 0644); /* XXX hardcoded = bad */
+            var fos = api.create("FOS");
+            fos.init(picker.file, 42 /* WRONLY | CREAT | TRUNCATE */, 0644, 0);
+            return fos.write(content, content.length);
+        } else {
+            return 0;
+        }
+    };
 }
index 9077dfa..f3f903f 100644 (file)
@@ -14,5 +14,8 @@
     'LI_ATTR_SEARCH_CHOOSE_FILE': "Select file with search terms",
     'LI_ATTR_SEARCH_TOO_LARGE': "That file is too large for this operation.",
     'SELECT_AN_LI_ATTRIBUTE': "You must select an LI attribute.",
-    'NO_RESULTS': "No results."
+    'NO_RESULTS': "No results.",
+    'ISBN_SAVE_DIALOG_TITLE': "Save ISBNs to a file",
+    'ISBN_SHORT_LIST': "Not all of the selected items had ISBNs.\nChoose OK to save the ISBNs that could be found.",
+    'ISBN_EMPTY_LIST': "No ISBNs found."
 }
index 03ba892..bed0e8f 100644 (file)
@@ -1023,6 +1023,10 @@ function AcqLiTable() {
                 this.createAssets();
                 break;
 
+            case 'export_isbn_list':
+                this.exportISBNList();
+                break;
+
             case 'add_brief_record':
                 if(this.isPO)
                     location.href = oilsBasePath + '/acq/picklist/brief_record?po=' + this.isPO;
@@ -1048,6 +1052,36 @@ function AcqLiTable() {
         );
     }
 
+    /* Should really think about generalizing this to do more than ISBN #s */
+    this.exportISBNList = function() {
+        var selected = this.getSelected();
+        var isbn_list = selected.map(
+            function(li) {
+                return (new openils.acq.Lineitem({"lineitem": li})).findAttr(
+                    "isbn", "lineitem_marc_attr_definition"
+                );
+            }
+        ).filter(function(attr) { return Boolean(attr); });
+
+        if (isbn_list.length > 0) {
+            if (isbn_list.length < selected.length) {
+                if (!confirm(localeStrings.ISBN_SHORT_LIST)) {
+                    return;
+                }
+            }
+            try {
+                openils.XUL.contentToFileSaveDialog(
+                    isbn_list.join("\n"),
+                    localeStrings.ISBN_SAVE_DIALOG_TITLE
+                );
+            } catch (E) {
+                alert(E);
+            }
+        } else {
+            alert(localeStrings.ISBN_EMPTY_LIST);
+        }
+    };
+
     this.printPO = function() {
         if(!this.isPO) return;
         progressDialog.show(true);
index 00f4ba4..e1aedf8 100644 (file)
@@ -12,6 +12,7 @@ dojo.require("openils.widget.AutoFieldWidget");
 
 var _searchable_by_array = ["issn", "isbn", "upc"];
 var combinedAttrValueArray = [];
+var scalarAttrSearchManager;
 var liTable;
 
 function prepareStateStore(pcrud) {
@@ -30,11 +31,6 @@ function prepareStateStore(pcrud) {
 }
 
 function prepareScalarSearchStore(pcrud) {
-    attrScalarDefSelector.store = new dojo.data.ItemFileReadStore({
-        "data": acqliad.toStoreData(
-            pcrud.search("acqliad", {"id": {"!=": null}})
-        )
-    });
 }
 
 function prepareArraySearchStore(pcrud) {
@@ -55,25 +51,14 @@ function prepareAgencySelector() {
     }).build();
 }
 
-function load() {
-    var pcrud = new openils.PermaCrud();
-
-    prepareStateStore(pcrud);
-    prepareScalarSearchStore(pcrud);
-    prepareArraySearchStore(pcrud);
-
-    prepareAgencySelector();
-
-    liTable = new AcqLiTable();
-    openils.Util.show("oils-acq-li-search-form-holder");
-}
-
 function toggleAttrSearchType(which, checked) {
     /* This would be cooler with a slick dispatch table instead of branchy
      * logic, but whatever... */
     if (checked) {
         if (which == "scalar") {
-            openils.Util.show("oils-acq-li-search-attr-scalar", "inline");
+            if (scalarAttrSearchManager.index < 1)
+                scalarAttrSearchManager.add();
+            openils.Util.show("oils-acq-li-search-attr-scalar", "inline-block");
             openils.Util.hide("oils-acq-li-search-attr-array");
         } else if (which == "array") {
             openils.Util.hide("oils-acq-li-search-attr-scalar");
@@ -96,16 +81,14 @@ var buildAttrSearchClause = {
         };
     },
     "scalar": function(v) {
-        if (!v.scalar_def) {
+        var r = scalarAttrSearchManager.buildSearchClause();
+        if (r.attr_value_pairs.length < 1) {
             throw new Error(localeStrings.SELECT_AN_LI_ATTRIBUTE);
+        } else {
+            return r;
         }
-        return {
-            "attr_value_pairs":
-                [[Number(v.scalar_def), v.scalar_value]] /* [[sic]] */
-        };
     },
     "none": function(v) {
-        //return {"attr_value_pairs": [[1, ""]]};
         return {};
     }
 };
@@ -120,18 +103,19 @@ function clearTerms() {
 }
 
 function loadTermsFromFile() {
-    var rawdata = openils.XUL.contentFromFileOpenDialog(
-        localeStrings.LI_ATTR_SEARCH_CHOOSE_FILE
-    );
-    if (!rawdata) {
-        return;
-    } else if (rawdata.length > 1024 * 128) {
+    var rawdata;
+
+    try {
         /* FIXME 128k is completely arbitrary; needs researched for
-         * a sane limit and should also be made configurable. Further, if
-         * there's going to be a size limit, it'd be much better to apply
-         * it before reading in the file at all, not now. */
-        alert(localeStrings.LI_ATTR_SEARCH_TOO_LARGE);
-    } else {
+         * a sane limit and should also be made configurable. */
+        rawdata = openils.XUL.contentFromFileOpenDialog(
+            localeStrings.LI_ATTR_SEARCH_CHOOSE_FILE, 1024 * 128
+        );
+    } catch (E) {
+        alert(E);
+    }
+
+    if (rawdata) {
         try {
             combinedAttrValueArray =
                 combinedAttrValueArray.concat(naivelyParse(rawdata));
@@ -187,4 +171,87 @@ function doSearch(values) {
     }
 }
 
+function myScalarAttrSearchManager(template_id, pcrud) {
+    this.template = dojo.byId(template_id);
+    this.store = new dojo.data.ItemFileReadStore({
+        "data": acqliad.toStoreData(
+            pcrud.search("acqliad", {"id": {"!=": null}})
+        )
+    });
+    this.rows = {};
+    this.index = 0;
+};
+myScalarAttrSearchManager.prototype.remove = function(n) {
+    dojo.destroy("scalar_attr_holder_" + n);
+    delete this.rows[n];
+};
+myScalarAttrSearchManager.prototype.add = function() {
+    var self = this;
+    var n = this.index;
+    var clone = dojo.clone(this.template);
+    var def = dojo.query('input[name="def"]', clone)[0];
+    var value = dojo.query('input[name="value"]', clone)[0];
+    var a = dojo.query('a', clone)[0];
+
+    clone.id = "scalar_attr_holder_" + n;
+    a.onclick = function() { self.remove(n); };
+
+    this.rows[n] = [
+        new dijit.form.FilteringSelect({
+            "id": "scalar_def_" + n,
+            "name": "scalar_def_" + n,
+            "store": this.store,
+            "labelAttr": "description",
+            "searchAttr": "description"
+        }, def),
+        new dijit.form.TextBox({
+            "id": "scalar_value_" + n,
+            "name": "scalar_value_" + n
+        }, value)
+    ];
+
+    this.index++;
+
+    dojo.place(clone, "oils-acq-li-search-scalar-adder", "before");
+    openils.Util.show(clone);
+};
+myScalarAttrSearchManager.prototype.buildSearchClause = function() {
+    var list = [];
+    for (var k in this.rows) {
+        var def = this.rows[k][0].attr("value");
+        var val = this.rows[k][1].attr("value");
+        if (def != "" && val != "")
+            list.push([Number(def), val]);
+    }
+    return {"attr_value_pairs": list};
+};
+myScalarAttrSearchManager.prototype.simplifiedPairs = function() {
+    var result = {};
+    for (var k in this.rows) {
+        result[this.rows[k][0].attr("value")] = this.rows[k][1].attr("value");
+    }
+    return result;
+};
+myScalarAttrSearchManager.prototype.newBrief = function() {
+    location.href = oilsBasePath + "/acq/picklist/brief_record?prepop=" +
+        encodeURIComponent(js2JSON(this.simplifiedPairs()));
+};
+
+
+function load() {
+    var pcrud = new openils.PermaCrud();
+
+    prepareStateStore(pcrud);
+    prepareArraySearchStore(pcrud);
+
+    prepareAgencySelector();
+
+    liTable = new AcqLiTable();
+    scalarAttrSearchManager = new myScalarAttrSearchManager(
+        "oils-acq-li-search-scalar-template", pcrud
+    );
+
+    openils.Util.show("oils-acq-li-search-form-holder");
+}
+
 openils.Util.addOnLoad(load);
index 66ebc67..d5ea700 100644 (file)
@@ -21,6 +21,7 @@ function drawBriefRecordForm(fields) {
     var cgi = new openils.CGI();
     paramPL = cgi.param('pl');
     paramPO = cgi.param('po');
+    prepop = JSON2js(cgi.param('prepop'));
 
 
     if(paramPL) {
@@ -63,9 +64,11 @@ function drawBriefRecordForm(fields) {
     }
 
 
+    /*
     marcEditButton.onClick = function(fields) {
         saveBriefRecord(fields, true);
     }
+    */
 
     fieldmapper.standardRequest(
         ['open-ils.acq', 'open-ils.acq.lineitem_attr_definition.retrieve.all'],
@@ -90,7 +93,12 @@ function drawBriefRecordForm(fields) {
                             attrDefs[def.code()] = xpathParser.parse(def.xpath());
                             var row = rowTmpl.cloneNode(true);
                             dojo.query('[name=name]', row)[0].innerHTML = def.description();
-                            new dijit.form.TextBox({name : def.code()}, dojo.query('[name=widget]', row)[0]);
+                            var textbox = new dijit.form.TextBox(
+                                {"name": def.code()},
+                                dojo.query('[name=widget]', row)[0]
+                            );
+                            if (prepop && prepop[def.id()])
+                                textbox.attr("value", prepop[def.id()]);
                             tbody.appendChild(row);
                         }
                     );
index 0787eb3..5b8fcef 100644 (file)
@@ -18,6 +18,7 @@
                                             <option mask='pl' value='order_ready'>Mark Ready for Order</option>
                                             <option mask='*'  value='delete_selected'>Delete Selected Items</option>
                                             <option mask='*'  value='add_brief_record'>Add Brief Record</option>
+                                            <option mask='*'  value='export_isbn_list'>Export ISBN List</option>
                                             <option mask='po' value='' disabled='disabled'>----PO----</option>
                                             <option mask='sr|pl' value='create_order'>Create Purchase Order</option>
                                             <option mask='po' value='create_assets'>Load Bibs and Items</option>
index 1c2739b..0195817 100644 (file)
             </label>
         </div>
         <div class="oils-acq-li-search-form-row">
+            <!-- the "style" attribute on this input seems to be necessary as
+                I can't get the same effect from CSS for some reason -->
             <input class="oils-acq-li-search" dojoType="dijit.form.RadioButton"
                 name="attr_search_type" jsId="attrSearchTypeScalar"
                 id="attr_search_type_scalar" value="scalar"
+                style="vertical-align: top;"
                 onChange="toggleAttrSearchType(this.value, this.checked);" />
             <label for="attr_search_type_scalar" class="oils-acq-li-search">
-                Search by one attribute value
+                Search by attribute values
             </label>
             <div id="oils-acq-li-search-attr-scalar" class="hidden">
-                <input class="oils-acq-li-search"
-                    name="scalar_def" dojoType="dijit.form.FilteringSelect"
-                    jsId="attrScalarDefSelector"
-                    labelAttr="description" searchAttr="description" />
-                <input class="oils-acq-li-search" name="scalar_value"
-                    dojoType="dijit.form.TextBox"/>
+<!--            <div class="oils-acq-li-search-conjunction">
+                    <em>Show results for which:</em><br />
+                    <input dojoType="dijit.form.RadioButton"
+                        id="scalar_search_conjuction_or"
+                        name="scalar_search_conjunction" value="or"
+                        checked="checked" class="oils-acq-li-search" />
+                    <label for="scalar_search_conjuction_or"
+                        class="oils-acq-li-search">ANY of the following terms match</label>
+                    <br />
+                    <input dojoType="dijit.form.RadioButton"
+                        id="scalar_search_conjuction_and"
+                        name="scalar_search_conjunction" value="and"
+                        class="oils-acq-li-search" />
+                    <label for="scalar_search_conjuction_and"
+                        class="oils-acq-li-search">ALL of the following terms match</label>
+                </div> -->
+                <div class="oils-acq-li-search-scalar hidden"
+                    id="oils-acq-li-search-scalar-template">
+                    <input class="oils-acq-li-search" name="def" />
+                    <input class="oils-acq-li-search" name="value" />
+                    <a class="oils-acq-li-search" title="Remove this row"
+                        href="javascript:void(0);" />(X)</a>
+                </div>
+                <div id="oils-acq-li-search-scalar-adder">
+                    <span dojoType="dijit.form.Button"
+                        class="oils-acq-li-search"
+                        onclick="scalarAttrSearchManager.add();">
+                        Add more search terms
+                    </span>
+                    <span dojoType="dijit.form.Button"
+                        class="oils-acq-li-search"
+                        onclick="scalarAttrSearchManager.newBrief();">
+                        New brief record like this
+                    </span>
+                </div>
             </div>
         </div>
         <div class="oils-acq-li-search-form-row">