1 dojo.require("dojo.date.stamp");
2 dojo.require("dojox.encoding.base64");
3 dojo.require("openils.widget.AutoGrid");
4 dojo.require("openils.widget.AutoWidget");
5 dojo.require("openils.widget.XULTermLoader");
6 dojo.require("openils.PermaCrud");
8 var termSelectorFactory;
12 var pcrud = new openils.PermaCrud();
13 var cgi = new openils.CGI();
15 /* typing save: add {get,set}Value() to all HTML <select> elements */
16 HTMLSelectElement.prototype.getValue = function() {
17 return this.options[this.selectedIndex].value;
20 /* only sets the selected value if such an option is actually available */
21 HTMLSelectElement.prototype.setValue = function(s) {
22 for (var i = 0; i < this.options.length; i++) {
23 if (s == this.options[i].value) {
24 this.selectedIndex = i;
30 /* minor formatting function used by autogrids in unified.tt2 */
31 function getName(rowIndex, item) {
34 "name": this.grid.store.getValue(item, "name") ||
35 localeStrings.UNNAMED,
36 "id": this.grid.store.getValue(item, "id")
41 /* quickly find elements by the value of a "name" attribute */
42 function nodeByName(name, root) {
43 return dojo.query("[name='" + name + "']", root)[0];
47 openils.Util.hide("acq-unified-hide-form");
48 openils.Util.show("acq-unified-reveal-form", "inline");
49 openils.Util.hide("acq-unified-form");
52 function revealForm() {
53 openils.Util.hide("acq-unified-reveal-form");
54 openils.Util.show("acq-unified-hide-form", "inline");
55 openils.Util.show("acq-unified-form");
58 /* The TermSelectorFactory will be instantiated by the TermManager. It
59 * provides HTML select controls whose options are all the searchable
60 * fields. Selecting a field from one of these controls will create the
61 * appopriate type of corresponding widget for the user to enter a search
62 * term against the selected field.
64 function TermSelectorFactory(terms) {
67 this.onlyBibFriendly = false;
69 this.template = dojo.create("select");
70 this.template.appendChild(
71 dojo.create("option", {
72 "disabled": "disabled",
73 "selected": "selected",
75 "innerHTML": localeStrings.SELECT_SEARCH_FIELD
79 /* Create abbreviations for class names to make field categories
80 * more readable in field selector control. */
81 this._abbreviate = function(s) {
83 for (var i = 0; i < s.length; i++) {
85 if (!i) result = s[i];
86 else if (last == " ") result += s[i];
93 var selectorMethods = {
94 /* Important: within the following functions, "this" refers to one
95 * HTMLSelect object, and "self" refers to the TermSelectorFactory. */
96 "getTerm": function() { return this.valueToTerm(this.getValue()); },
97 "valueToTerm": function(value) {
98 var parts = value.split(":");
99 if (!parts || parts.length != 2) return null;
101 self.terms[parts[0]][parts[1]],
102 {"hint": parts[0], "field": parts[1]}
105 "onlyBibFriendly": function(yes) {
107 for (var i = 0; i < this.options.length; i++) {
108 if (this.options[i].value) {
109 var term = this.valueToTerm(this.options[i].value);
110 this.options[i].disabled = !term.bib_friendly;
114 for (var i = 0; i < this.options.length; i++) {
115 if (this.options[i].value)
116 this.options[i].disabled = false;
120 "makeWidget": function(
121 parentNode, wStore, matchHow, value, noFocus, callback
123 var term = this.getTerm();
124 var widgetKey = this.uniq;
125 if (matchHow.getValue() == "__in") {
126 new openils.widget.XULTermLoader({
127 "parentNode": parentNode
130 wStore[widgetKey] = w;
131 if (typeof(callback) == "function")
132 callback(term, widgetKey);
133 if (typeof(value) != "undefined")
134 w.attr("value", value);
135 /* I would love for the following call not to be
136 * necessary, so that updating the value of the dijit
137 * would lead to this automatically, but I can't yet
138 * figure out the correct way to do this in Dojo.
143 } else if (term.hint == "acqlia" ||
144 (term.hint == "jub" && term.field == "eg_bib_id")) {
145 /* The test for jub.eg_bib_id is a special case to prevent
146 * AutoFieldWidget from trying to render a ridiculous dropdown
147 * of every bib record ID in the system. */
148 wStore[widgetKey] = dojo.create(
149 "input", {"type": "text"}, parentNode, "only"
151 if (typeof(value) != "undefined")
152 wStore[widgetKey].value = value;
154 wStore[widgetKey].focus();
155 if (typeof(callback) == "function")
156 callback(term, widgetKey);
158 new openils.widget.AutoFieldWidget({
159 "fmClass": term.hint,
160 "fmField": term.field,
161 "noDisablePkey": true,
162 "parentNode": dojo.create("span", null, parentNode, "only")
165 wStore[widgetKey] = w;
166 if (typeof(value) != "undefined") {
167 if (w.declaredClass.match(/Check/))
168 w.attr("checked", value == "t");
170 w.attr("value", value);
174 if (typeof(callback) == "function")
175 callback(term, widgetKey);
178 openils.Util.registerEnterHandler(w.domNode,
180 resultManager.go(termManager.buildSearchObject());
189 for (var hint in this.terms) {
190 var optgroup = dojo.create(
191 "optgroup", {"value": "", "label": this.terms[hint].__label}
193 var prefix = this._abbreviate(this.terms[hint].__label);
195 for (var field in this.terms[hint]) {
196 if (!/^__/.test(field)) {
197 optgroup.appendChild(
198 dojo.create("option", {
199 "class": "acq-unified-option-regular",
200 "value": hint + ":" + field,
201 "innerHTML": prefix + " - " +
202 this.terms[hint][field].label
208 this.template.appendChild(optgroup);
211 this.make = function(n) {
212 var node = dojo.clone(this.template);
214 dojo.attr(node, "id", "term-" + n);
215 for (var name in selectorMethods)
216 node[name] = selectorMethods[name];
217 if (this.onlyBibFriendly)
218 node.onlyBibFriendly(true);
223 /* The term manager retrieves information from the IDL about all the fields
224 * in the classes that we consider searchable for our purpose. It maintains
225 * a dynamic HTML table of search terms, using the TermSelectorFactory
226 * to generate search field selectors, which in turn provide appropriate
227 * widgets for entering search terms. The TermManager provides search term
228 * modifiers (fuzzy searching, not searching). The TermManager also handles
229 * adding and removing rows of search terms, as well as building the search
230 * query to pass to the middle layer from the search term widgets.
232 function TermManager() {
235 /* All the keys in this object are bib-search-friendly attributes, but the
236 * boolean values indicate whether they should be searched by their
237 * field name as such, or simply mapped to "keyword". */
238 this.bibFriendlyAttrNames = {
239 "author": true, "title": true,
240 "isbn": false, "issn": false, "upc": false
244 ["jub", "acqpl", "acqpo", "acqinv"].forEach(
247 o.__label = fieldmapper.IDL.fmclasses[hint].label;
248 fieldmapper.IDL.fmclasses[hint].fields.forEach(
250 if (!field.virtual) {
252 "label": field.label, "datatype": field.datatype
257 self.terms[hint] = o;
261 this.terms.acqlia = {"__label": fieldmapper.IDL.fmclasses.acqlia.label};
262 pcrud.retrieveAll("acqliad", {"order_by": {"acqliad": "id"}}).forEach(
264 self.terms.acqlia[def.id()] = {
265 "label": def.description(),
268 (typeof(self.bibFriendlyAttrNames[def.code()]) !=
271 self.bibFriendlyAttrNames[def.code()] ?
272 def.code() : "keyword"
277 this.selectorFactory = new TermSelectorFactory(this.terms);
278 this.template = dojo.byId("acq-unified-terms-tbody").
279 removeChild(dojo.byId("acq-unified-terms-row-tmpl"));
280 dojo.attr(this.template, "id");
282 this.lastResultType = null;
287 dojo.byId("acq-unified-result-type").onchange = function() {
288 self.resultTypeChange(this.getValue());
291 this.allRowIds = function() {
292 return dojo.query("tr[id^='term-row-']", "acq-unified-terms-tbody").
293 map(function(o) { return o.id.match(/^term-row-(\d+)$/)[1]; });
296 this._row = function(id) { return dojo.byId("term-row-" + id); };
297 this._selector = function(id) { return dojo.byId("term-" + id); };
298 this._match_how = function(id) { return dojo.byId("term-match-" + id); };
300 this._updateMatchHowForField = function(term, key) {
301 /* NOTE important to use self, not this, in this function.
303 * Based on the selected field (its datatype and the kind of widget
304 * that AutoFieldWidget provides for it) we update the possible
305 * choices in the mach_how selector.
307 var w = self.widgets[key];
308 var can_do_fuzzy, can_do_in;
309 if (term.datatype == "id") {
310 can_do_fuzzy = false;
312 } else if (term.datatype == "link") {
313 var target = self.getLinkTarget(term);
314 can_do_fuzzy = (target == "au");
315 can_do_in = (target == "bre"); /* XXX might revise later */
316 } else if (typeof(w.declaredClass) != "undefined") {
317 can_do_fuzzy = can_do_in =
318 Boolean(w.declaredClass.match(/form\.Text|XULT/));
320 var type = dojo.attr(w, "type");
322 can_do_fuzzy = can_do_in = (type == "text");
324 can_do_fuzzy = can_do_in = false;
327 self.matchHowAllow(key, "__fuzzy", can_do_fuzzy);
328 self.matchHowAllow(key, "__in", can_do_in);
330 var inequalities = (term.datatype == "timestamp");
331 self.matchHowAllow(key, "__gte", inequalities);
332 self.matchHowAllow(key, "__lte", inequalities);
335 this.removerButton = function(n) {
336 return dojo.create("button", {
338 "class": "acq-unified-remover",
339 "onclick": function() { self.removeRow(n); }
343 this.matchHowAllow = function(where, what, which, exact) {
345 "option[value" + (exact ? "" : "*") + "='" + what + "']",
346 typeof(where) == "object" ? where : this._match_how(where)
347 ).forEach(function(o) { o.disabled = !which; });
350 this.getLinkTarget = function(term) {
351 return fieldmapper.IDL.fmclasses[term.hint].
352 field_map[term.field]["class"];
355 this.updateRowWidget = function(id, value, noFocus) {
356 var where = nodeByName("widget", this._row(id));
358 delete this.widgets[id];
361 this._selector(id).makeWidget(
362 where, this.widgets, this._match_how(id), value, noFocus,
363 this._updateMatchHowForField
367 this.resultTypeChange = function(resultType) {
369 this.lastResultType == "lineitem_and_bib" &&
370 resultType != "lineitem_and_bib"
372 /* Re-enable all non-bib-friendly fields in all search term
373 * field selectors. */
374 this.allRowIds().forEach(
376 self._selector(id).onlyBibFriendly(false);
377 self.matchHowAllow(id, "", true, /* exact */ true);
378 self.matchHowAllow(id, "__not", true, /* exact */ true);
381 /* Tell the selector factory to create new search term field
382 * selectors with all fields, not just bib-friendly ones. */
383 this.selectorFactory.onlyBibFriendly = false;
385 this.lastResultType != "lineitem_and_bib" &&
386 resultType == "lineitem_and_bib"
388 /* Remove all search term rows set to non-bib-friendly fields. */
389 this.allRowIds().forEach(
391 var term = self._selector(id).getTerm();
393 !self.terms[term.hint][term.field].bib_friendly) {
398 /* Disable all non-bib-friendly fields in all remaining search term
399 * field selectors. */
400 this.allRowIds().forEach(
402 self._selector(id).onlyBibFriendly(true);
403 self.matchHowAllow(id, "", false, /* exact */ true);
404 self.matchHowAllow(id, "__not", false, /* exact */ true);
407 /* Tell the selector factory to create new search term field
408 * selectors with only bib friendly options. */
409 this.selectorFactory.onlyBibFriendly = true;
411 this.lastResultType = resultType;
414 /* this method is particularly kludgy... puts back together a string
415 * based on object properties that might arrive in indeterminate order. */
416 this._term_reverse_match_how = function(term) {
417 /* only two-key combination we use */
418 if (term.__not && term.__fuzzy)
419 return "__not,__fuzzy";
421 /* only other possibilities are single-key or no key */
422 for (var key in term) {
431 this._term_reverse_selector_field = function(term) {
432 for (var key in term) {
433 if (!/^__/.test(key))
439 this._term_reverse_selector_value = function(term) {
440 for (var key in term) {
441 if (!/^__/.test(key))
447 this.addRow = function(term, hint) {
448 var uniq = (this.rowId)++;
450 var row = dojo.clone(this.template);
451 dojo.attr(row, "id", "term-row-" + uniq);
453 var selector = this.selectorFactory.make(uniq);
455 selector, "onchange", function() { self.updateRowWidget(uniq); }
458 var match_how = dojo.query("select", nodeByName("match", row))[0];
459 dojo.attr(match_how, "id", "term-match-" + uniq);
460 dojo.attr(match_how, "selectedIndex", 0);
462 match_how, "onchange",
464 if (this.getValue() == "__in") {
465 self.updateRowWidget(uniq);
467 } else if (this.was_in) {
468 self.updateRowWidget(uniq);
471 if (self.widgets[uniq]) self.widgets[uniq].focus();
475 /* Kind of inelegant; could be improved: this section turns off
476 * match-type options that don't apply to bib searching. */
479 !this.selectorFactory.onlyBibFriendly, /* exact */ true
483 !this.selectorFactory.onlyBibFriendly, /* exact */ true
485 if (this.selectorFactory.onlyBibFriendly)
486 match_how.setValue("__fuzzy");
488 nodeByName("selector", row).appendChild(selector);
489 nodeByName("remove", row).appendChild(this.removerButton(uniq));
491 dojo.place(row, "acq-unified-terms-tbody", "last");
494 var attr = this._term_reverse_selector_field(term);
495 var field = hint + ":" + attr;
496 selector.setValue(field);
498 var match_how_value = this._term_reverse_match_how(term);
500 match_how.setValue(match_how_value);
502 var value = this._term_reverse_selector_value(term);
503 if (this.terms[hint][attr].datatype == "timestamp")
504 value = dojo.date.stamp.fromISOString(value);
505 this.updateRowWidget(uniq, value, /* noFocus */ true);
510 this.removeRow = function(id) {
511 delete this.widgets[id];
512 dojo.destroy(this._row(id));
515 this.reflect = function(search_object) {
516 for (var hint in search_object) {
517 search_object[hint].forEach(
518 function(term) { self.addRow(term, hint); }
523 this.buildSearchObject = function() {
526 for (var id in this.widgets) {
527 var kvlist = this._selector(id).getValue().split(":");
528 var hint = kvlist[0];
529 var attr = kvlist[1];
530 if (!(hint && attr)) continue;
533 this._match_how(id).getValue().split(",").filter(Boolean);
536 if (typeof(this.widgets[id].declaredClass) != "undefined") {
537 if (this.widgets[id].declaredClass.match(/Date/)) {
539 dojo.date.stamp.toISOString(this.widgets[id].value).
542 value = this.widgets[id].attr("value");
543 if (this.widgets[id].declaredClass.match(/Check/))
544 value = (value == "on") ? "t" : "f";
547 value = this.widgets[id].value;
555 match_how.forEach(function(key) { unit[key] = true; });
556 if (this.terms[hint][attr].datatype == "timestamp")
557 unit.__castdate = true;
564 this.buildBibSearchString = function() {
565 var conj = {"and": " ", "or": " || "}[
566 dojo.byId("acq-unified-conjunction").getValue()
570 /* Notice that below we use conj in two places and a constant " || "
571 * in one. That constant " || " is applied for the "file of terms"
572 * search term type, which is in itself always an or search. */
573 for (var id in this.widgets) {
574 var term = this._selector(id).getTerm();
575 var attr = term.bib_attr_name;
576 var match_how = this._match_how(id).getValue();
577 var widget = this.widgets[id];
579 if (!sso[attr]) sso[attr] = [];
581 typeof(widget.attr) == "function" ?
582 widget.attr("value") : widget.value
584 if (typeof(value) != "string")
585 value = value.join(" || ");
587 (match_how.indexOf("__not") == -1 ? "" : "-") + value
591 for (var attr in sso)
592 ssa.push(attr + ": " + sso[attr].join(conj));
593 return "(" + ssa.join(conj) + ")";
597 /* The result manager is used primarily when the users submits a search. It
598 * consults the termManager to get the search query to send to the middl
599 * layer, and it chooses which ML method to call as well as what widgets to use
600 * to display the results.
602 function ResultManager(liPager, poGrid, plGrid, invGrid) {
605 this.liPager = liPager;
606 this.liPager.liTable.isUni = true;
608 this.poGrid = poGrid;
609 this.plGrid = plGrid;
610 this.invGrid = invGrid;
615 this.result_types = {
619 "flesh_cancel_reason": true,
622 "revealer": function() {
624 progressDialog.show(true);
626 "finisher": function() {
627 self.liPager.batch_length = self.count_results;
628 self.liPager.relabelControls();
629 self.liPager.enableControls(true);
630 progressDialog.hide();
632 "adder": function(li) {
633 self.liPager.liTable.addLineitem(li);
635 "interface": self.liPager
639 "no_flesh_cancel_reason": true
641 "revealer": function() {
642 self.poGrid.resetStore();
643 self.poGrid.showLoadProgressIndicator();
646 "finisher": function() {
647 self.poGrid.hideLoadProgressIndicator();
649 "adder": function(po) {
650 self.poCache[po.id()] = po;
651 self.poGrid.store.newItem(acqpo.toStoreItem(po));
653 "interface": self.poGrid
657 "flesh_lineitem_count": true,
660 "revealer": function() {
661 self.plGrid.resetStore();
662 self.plGrid.showLoadProgressIndicator();
665 "finisher": function() {
666 self.plGrid.hideLoadProgressIndicator();
668 "adder": function(pl) {
669 self.plCache[pl.id()] = pl;
670 self.plGrid.store.newItem(acqpl.toStoreItem(pl));
672 "interface": self.plGrid
676 "no_flesh_misc": true
678 "finisher": function() {
679 self.invGrid.hideLoadProgressIndicator();
681 "revealer": function() {
682 self.invGrid.resetStore();
685 "adder": function(inv) {
686 self.invCache[inv.id()] = inv;
687 self.invGrid.store.newItem(acqinv.toStoreItem(inv));
689 "interface": self.invGrid
692 "revealer": function() { alert(localeStrings.NO_RESULTS); }
696 this._dataLoader = function() {
697 /* This function must contain references to "self" only, not "this." */
698 var grid = self.result_types[self.result_type].interface;
699 self.count_results = 0;
700 self.params[4].offset = grid.displayOffset;
701 self.params[4].limit = grid.displayLimit;
703 fieldmapper.standardRequest(
704 ["open-ils.acq", self.method_name], {
705 "params": self.params,
707 "onresponse": function(r) {
708 if (r = openils.Util.readResponse(r)) {
709 if (!self.count_results++)
710 self.show(self.result_type);
711 self.add(self.result_type, r);
714 "oncomplete": function() { self.resultsComplete(); }
719 this.add = function(which, what) {
720 var f = this.result_types[which].adder;
724 this.finish = function(which) {
725 var f = this.result_types[which].finisher;
729 this.show = function(which) {
730 openils.Util.objectProperties(this.result_types).forEach(
732 openils.Util[rt == which ? "show" : "hide"](
733 "acq-unified-results-" + rt
737 this.result_types[which].revealer();
740 this.resultsComplete = function() {
741 if (!this.count_results)
742 this.show("no_results");
743 else this.finish(this.result_type);
746 this.go = function(search_object) {
747 location.href = oilsBasePath + "/acq/search/unified?" +
748 "so=" + base64Encode(search_object) +
749 "&rt=" + dojo.byId("acq-unified-result-type").getValue() +
750 "&c=" + dojo.byId("acq-unified-conjunction").getValue();
753 this.search = function(uriManager, termManager) {
754 var bib_search_string = null;
755 this.count_results = 0;
756 this.result_type = dojo.byId("acq-unified-result-type").getValue();
758 /* lineitem_and_bib: a special case */
759 if (this.result_type == "lineitem_and_bib") {
760 this.result_type = "lineitem";
761 bib_search_string = termManager.buildBibSearchString();
764 this.method_name = "open-ils.acq." + this.result_type +
766 /* Except for building the API method name that we want to call,
767 * we want to treat lineitem_and_bib the same way as lineitem from
771 openils.User.authtoken,
773 this.result_types[this.result_type].search_options
777 dojo.byId("acq-unified-conjunction").getValue() == "and" ? 1 : 2
778 ] = uriManager.search_object;
779 if (uriManager.order_by)
780 this.params[4].order_by = uriManager.order_by;
782 var interface = this.result_types[this.result_type].interface;
783 interface.dataLoader = this._dataLoader;
785 if (bib_search_string) {
786 /* Have the ML do the bib search first, which incidentally has the
787 * side effect of creating line items that will show up when
788 * we do the LI part of the search (so we don't actually want
789 * to display these results directly). */
790 fieldmapper.standardRequest(
791 ["open-ils.acq", "open-ils.acq.biblio.wrapped_search.atomic"], {
793 openils.User.authtoken, bib_search_string, {
797 "onresponse": function(r) {
798 r = openils.Util.readResponse(r, false, true);
804 interface.dataLoader();
808 function URIManager() {
810 this.cannedSearches = {
814 {"ordering_agency": openils.User.user.ws_ou()},
815 {"state": "on-order"}
819 "result_type": "purchase_order",
820 "conjunction": "and",
822 {"class": "acqpo", "field": "edit_time", "direction": "desc"}
828 {"owner": openils.User.user.usrname()}
831 "result_type": "picklist",
832 "conjunction": "and",
834 {"class": "acqpl", "field": "edit_time", "direction": "desc"}
841 {"receiver": openils.User.user.ws_ou()}
845 "result_type": "invoice",
846 "conjunction": "and",
848 {"class": "acqinv", "field": "recv_date", "direction": "desc"}
853 if (this.canned = cgi.param("ca")) { /* assignment */
854 dojo.mixin(this, this.cannedSearches[this.canned]);
855 dojo.byId("acq-unified-result-type").setValue(this.result_type);
856 dojo.byId("acq-unified-result-type").onchange();
857 dojo.byId("acq-unified-conjunction").setValue(this.conjunction);
859 this.search_object = cgi.param("so");
860 if (this.search_object)
861 this.search_object = base64Decode(this.search_object);
863 this.result_type = cgi.param("rt");
864 if (this.result_type) {
865 dojo.byId("acq-unified-result-type").setValue(this.result_type);
866 dojo.byId("acq-unified-result-type").onchange();
869 this.conjunction = cgi.param("c");
870 if (this.conjunction)
871 dojo.byId("acq-unified-conjunction").setValue(this.conjunction);
876 openils.Util.addOnLoad(
878 termManager = new TermManager();
879 resultManager = new ResultManager(
880 new LiTablePager(null, new AcqLiTable()),
881 dijit.byId("acq-unified-po-grid"),
882 dijit.byId("acq-unified-pl-grid"),
883 dijit.byId("acq-unified-inv-grid")
886 uriManager = new URIManager();
887 if (uriManager.search_object) {
888 if (!uriManager.half_search)
890 openils.Util.show("acq-unified-body");
891 termManager.reflect(uriManager.search_object);
893 if (!uriManager.half_search)
894 resultManager.search(uriManager, termManager);
896 termManager.addRow();
897 openils.Util.show("acq-unified-body");