From b3d45facd05ac505b3fde745ac18bf6829a5c4e6 Mon Sep 17 00:00:00 2001 From: Lebbeous Fogle-Weekley Date: Sat, 31 Mar 2012 12:17:40 -0400 Subject: [PATCH] New pull list interface taking advantage of flattener for speed, MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit and advanced sorting. For now, access it by the "Simplifed Pull List" button along the bottom edge of the existing holds pull list interface (but I think when/if this thing is widely accepted, it should replace the existing interface outright). With thanks to Mike Peters for testing an early version. Now including some updates requested by Thomas Berezansky. Specifically, the queue_position column and its relatives fthat come from the same calculation were removed, as they're [very] expensive in computing time to produce and generally useless to pull lists. One exception to the characterization of those fields as "useless" is the "number of potential copies" column, which we should add back later assuming we can find a fast way to do it rather than the previous ways, which were slow. Thanks to Bill Erickson for helping fix my issues in making auto-generated columns coöperate with the column picker (his changes are squashed into this). I think it's finally right. Now with release notes. Signed-off-by: Lebbeous Fogle-Weekley Signed-off-by: Mike Rylander --- Open-ILS/examples/fm_IDL.xml | 140 +++++++- .../lib/OpenILS/Application/Flattener.pm | 2 + .../perlmods/lib/OpenILS/Utils/Fieldmapper.pm | 2 + .../perlmods/lib/OpenILS/WWW/FlatFielder.pm | 25 +- Open-ILS/src/sql/Pg/950.data.seed-values.sql | 20 ++ .../XXXX.schema.simplified-hold-pull-list.sql | 24 ++ .../src/templates/circ/hold_pull_list.tt2 | 99 +++++ .../web/js/dojo/openils/FlattenerStore.js | 273 +++++++++----- .../js/dojo/openils/widget/FlattenerGrid.js | 337 ++++++++++++++---- .../dojo/openils/widget/GridColumnPicker.js | 65 +++- Open-ILS/web/opac/locale/en-US/lang.dtd | 2 + Open-ILS/xsl/FlatFielder2HTML.xsl | 7 + .../xul/staff_client/server/patron/holds.js | 43 +++ .../server/patron/holds_overlay.xul | 2 + .../simplified-hold-pull-list.txt | 30 ++ 15 files changed, 886 insertions(+), 185 deletions(-) create mode 100644 Open-ILS/src/sql/Pg/upgrade/XXXX.schema.simplified-hold-pull-list.sql create mode 100644 Open-ILS/src/templates/circ/hold_pull_list.tt2 create mode 100644 docs/RELEASE_NOTES_NEXT/simplified-hold-pull-list.txt diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml index 79b8738736..88b1b71310 100644 --- a/Open-ILS/examples/fm_IDL.xml +++ b/Open-ILS/examples/fm_IDL.xml @@ -2303,7 +2303,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA - + @@ -2649,7 +2649,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA - + @@ -2747,6 +2747,11 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + + + + + @@ -4833,7 +4838,7 @@ SELECT usr, - + @@ -4850,7 +4855,7 @@ SELECT usr, - + @@ -4893,9 +4898,118 @@ SELECT usr, - + + + ahr.requestor) AS is_staff_hold + FROM action.hold_request ahr + JOIN asset.copy acp ON (acp.id = ahr.current_copy) + JOIN asset.call_number acn ON (acp.call_number = acn.id) + JOIN asset.call_number_prefix acnp ON (acn.prefix = acnp.id) + JOIN asset.call_number_suffix acns ON (acn.suffix = acns.id) + JOIN actor.usr au ON (au.id = ahr.usr) + LEFT JOIN serial.issuance siss + ON (ahr.hold_type = 'I' AND siss.id = ahr.target) + LEFT JOIN asset.copy_location_order acplo + ON (acp.location = acplo.location AND + acp.circ_lib = acplo.org) + WHERE + ahr.capture_time IS NULL AND + ahr.cancel_time IS NULL AND + (ahr.expire_time is NULL OR ahr.expire_time > NOW()) + ]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SELECT ahr.* FROM action.hold_request ahr JOIN (SELECT current_copy, MAX(capture_time) AS capture_time FROM action.hold_request WHERE capture_time IS NOT NULL GROUP BY current_copy)x USING (current_copy, capture_time) @@ -5137,7 +5251,7 @@ SELECT usr, - + @@ -5147,6 +5261,13 @@ SELECT usr, + + + + + + + @@ -7898,7 +8019,7 @@ SELECT usr, - + @@ -7916,6 +8037,11 @@ SELECT usr, + + + + + diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Flattener.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Flattener.pm index a6c77d347e..ff232a9469 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Flattener.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Flattener.pm @@ -89,12 +89,14 @@ sub _flattened_search_single_join_clause { my $new_join; if ($reltype eq "has_a") { $new_join = { + type => "left", class => $hint, fkey => $piece, field => $field }; } elsif ($reltype eq "has_many" or $reltype eq "might_have") { $new_join = { + type => "left", class => $hint, fkey => $last_ident, field => $field diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Utils/Fieldmapper.pm b/Open-ILS/src/perlmods/lib/OpenILS/Utils/Fieldmapper.pm index df00277cbf..eddb944d48 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Utils/Fieldmapper.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Utils/Fieldmapper.pm @@ -139,11 +139,13 @@ sub load_links { my $reltype = get_attribute( $attribute_list, 'reltype' ); my $key = get_attribute( $attribute_list, 'key' ); my $class = get_attribute( $attribute_list, 'class' ); + my $map = get_attribute( $attribute_list, 'map' ); $$fieldmap{$fm}{links}{ $field } = { class => $class, reltype => $reltype, key => $key, + map => $map }; } } diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/FlatFielder.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/FlatFielder.pm index 0e5fc9868d..b80c9e8966 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/FlatFielder.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/FlatFielder.pm @@ -58,8 +58,7 @@ my $_output_handler_dispatch = { "prio" => 0, "code" => sub { $_[0]->content_type("text/html; charset=utf-8"); - print html_ish_output( @_, 'FlatFielder2HTML.xsl' ); - return Apache2::Const::OK; + return html_ish_output( @_, 'FlatFielder2HTML.xsl' ); } }, "application/xml" => { @@ -115,15 +114,29 @@ sub data_to_xml { $fs->setAttribute("FS_key", $args->{key}) if $args->{key}; $dom->setDocumentElement($fs); + my @columns; + my %column_labels; + if (@{$args->{columns}}) { + @columns = @{$args->{columns}}; + if (@{$args->{labels}}) { + my @labels = @{$args->{labels}}; + $column_labels{$columns[$_]} = $labels[$_] for (0..$#labels); + } + } + my $rownum = 1; for my $i (@{$$args{data}}) { my $item = $dom->createElement("row"); $item->setAttribute('ordinal', $rownum); $rownum++; - for my $k (keys %$i) { + @columns = keys %$i unless @columns; + for my $k (@columns) { my $val = $dom->createElement('column'); - $val->setAttribute('name', $k); - $val->appendText($i->{$k}); + my $datum = $i->{$k}; + $datum = join(" ", @$datum) if ref $datum eq 'ARRAY'; + + $val->setAttribute('name', $column_labels{$k} || $k); + $val->appendText($datum); $item->addChild($val); } $fs->addChild($item); @@ -214,6 +227,8 @@ sub handler { $args{key} = $cgi->param('key'); $args{id_field} = $cgi->param('identifier'); $args{label_field} = $cgi->param('label'); + $args{columns} = [ $cgi->param('columns') ]; + $args{labels} = [ $cgi->param('labels') ]; my $fielder = OpenSRF::AppSession->create('open-ils.fielder'); if ($args{map}) { diff --git a/Open-ILS/src/sql/Pg/950.data.seed-values.sql b/Open-ILS/src/sql/Pg/950.data.seed-values.sql index 87c3eff9e2..1dd5b5d7cc 100644 --- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql +++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql @@ -10155,6 +10155,26 @@ INSERT INTO config.usr_setting_type (name,grp,opac_visible,label,description,dat ), 'string' ); + +INSERT INTO config.usr_setting_type (name,grp,opac_visible,label,description,datatype) VALUES ( + 'ui.grid_columns.circ.hold_pull_list', + 'gui', + FALSE, + oils_i18n_gettext( + 'ui.grid_columns.circ.hold_pull_list', + 'Hold Pull List', + 'cust', + 'label' + ), + oils_i18n_gettext( + 'ui.grid_columns.circ.hold_pull_list', + 'Hold Pull List Saved Column Settings', + 'cust', + 'description' + ), + 'string' +); + SELECT setval( 'config.sms_carrier_id_seq', 1000 ); INSERT INTO config.sms_carrier VALUES diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.simplified-hold-pull-list.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.simplified-hold-pull-list.sql new file mode 100644 index 0000000000..eeff3eab04 --- /dev/null +++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.simplified-hold-pull-list.sql @@ -0,0 +1,24 @@ +BEGIN; + +SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version); + +INSERT INTO config.usr_setting_type (name,grp,opac_visible,label,description,datatype) VALUES ( + 'ui.grid_columns.circ.hold_pull_list', + 'gui', + FALSE, + oils_i18n_gettext( + 'ui.grid_columns.circ.hold_pull_list', + 'Hold Pull List', + 'cust', + 'label' + ), + oils_i18n_gettext( + 'ui.grid_columns.circ.hold_pull_list', + 'Hold Pull List Saved Column Settings', + 'cust', + 'description' + ), + 'string' +); + +COMMIT; diff --git a/Open-ILS/src/templates/circ/hold_pull_list.tt2 b/Open-ILS/src/templates/circ/hold_pull_list.tt2 new file mode 100644 index 0000000000..fc497b9225 --- /dev/null +++ b/Open-ILS/src/templates/circ/hold_pull_list.tt2 @@ -0,0 +1,99 @@ +[% WRAPPER base.tt2 %] +[% ctx.page_title = 'Hold Pull List' %] + +
+
+
Hold Pull List
+
+ +
+
+
+ + +
+ + + + + + + + + + + + + + + + + + + +
Shelving LocationAuthorTitlePartsHold NotesPatron BarcodePickup LibraryPickup Library (Shortname)Request LibraryRequest Library (Shortname)Selection LocusSMS Carrier
+
+[% END %] diff --git a/Open-ILS/web/js/dojo/openils/FlattenerStore.js b/Open-ILS/web/js/dojo/openils/FlattenerStore.js index e5cbdd7764..700d3f2845 100644 --- a/Open-ILS/web/js/dojo/openils/FlattenerStore.js +++ b/Open-ILS/web/js/dojo/openils/FlattenerStore.js @@ -29,6 +29,7 @@ if (!dojo._hasResource["openils.FlattenerStore"]) { "offset": 0, "baseSort": null, "defaultSort": null, + "sortFieldReMap": null, "constructor": function(/* object */ args) { dojo.mixin(this, args); @@ -51,7 +52,33 @@ if (!dojo._hasResource["openils.FlattenerStore"]) { ); }, - "_prepare_flattener_params": function(req) { + "_remap_sort": function(prepared_sort) { + if (this.sortFieldReMap) { + return prepared_sort.map( + dojo.hitch( + this, function(exp) { + if (typeof exp == "object") { + var key; + for (key in exp) + break; + var newkey = (key in this.sortFieldReMap) ? + this.sortFieldReMap[key] : key; + var o = {}; + o[newkey] = exp[key]; + return o; + } else { + return (exp in this.sortFieldReMap) ? + this.sortFieldReMap[exp] : exp; + } + } + ) + ); + } else { + return prepared_sort; + } + }, + + "_build_flattener_params": function(req) { var params = { "hint": this.fmClass, "ses": openils.User.authtoken @@ -65,31 +92,38 @@ if (!dojo._hasResource["openils.FlattenerStore"]) { params.where = dojo.toJson(where); } else { - var limit = (!isNaN(req.count) && req.count != Infinity) ? - req.count : this.limit; - var offset = (!isNaN(req.start) && req.start != Infinity) ? - req.start : this.offset; - - dojo.mixin( - params, { - "where": dojo.toJson(req.query), - "slo": dojo.toJson({ - "sort": this._prepare_sort(req.sort), - "limit": limit, - "offset": offset - }) - } - ); + params.where = dojo.toJson(req.query); + + var slo = { + "sort": this._remap_sort(this._prepare_sort(req.sort)) + }; + + if (!req.queryOptions.all) { + slo.limit = + (!isNaN(req.count) && req.count != Infinity) ? + req.count : this.limit; + + slo.offset = + (!isNaN(req.start) && req.start != Infinity) ? + req.start : this.offset; + } + + if (req.queryOptions.columns) + params.columns = req.queryOptions.columns; + if (req.queryOptions.labels) + params.labels = req.queryOptions.labels; + + params.slo = dojo.toJson(slo); } - if (this.mapKey) { /* XXX TODO, get a map key */ + if (this.mapKey) { params.key = this.mapKey; } else { params.map = dojo.toJson(this.mapClause); } - for (var key in params) - console.debug("flattener param " + key + " -> " + params[key]); +// for (var key in params) +// console.debug("flattener param " + key + " -> " + params[key]); return params; }, @@ -114,6 +148,94 @@ if (!dojo._hasResource["openils.FlattenerStore"]) { ); }, + "_on_http_error": function(response, ioArgs, req, retry_method) { + if (response.status == 402) { /* 'Payment Required' stands + in for cache miss */ + if (this._retried_map_key_already) { + var e = new FlattenerStoreError( + "Server won't cache flattener map?" + ); + if (typeof req.onError == "function") + req.onError.call(callback_scope, e); + else + throw e; + } else { + this._retried_map_key_already = true; + delete this.mapKey; + if (retry_method) + return this[retry_method](req); + } + } + }, + + "_fetch_prepare": function(req) { + req.queryOptions = req.queryOptions || {}; + req.abort = function() { console.warn("[unimplemented] abort()"); }; + + if (!this.mapKey) + this._get_map_key(); + + return this._build_flattener_params(req); + }, + + "_fetch_execute": function(params,handle_as,mime_type,onload,onerror) { + dojo.xhrPost({ + "url": this._flattener_url, + "content": params, + "handleAs": handle_as, + "sync": false, + "preventCache": true, + "headers": {"Accept": mime_type}, + "load": onload, + "error": onerror + }); + }, + + /* *** Nonstandard but public API - Please think hard about doing + * things the Dojo Way whenever possible before extending the API + * here. *** */ + + /* fetchToPrint() acts like a lot like fetch(), but doesn't call + * onBegin or onComplete. */ + "fetchToPrint": function(req) { + var callback_scope = req.scope || dojo.global; + var post_params; + + try { + post_params = this._fetch_prepare(req); + } catch (E) { + if (typeof req.onError == "function") + req.onError.call(callback_scope, E); + else + throw E; + } + + var process_fetch_all = dojo.hitch( + this, function(text) { + this._retried_map_key_already = false; + + if (typeof req.onComplete == "function") + req.onComplete.call(callback_scope, text, req); + } + ); + + var process_error = dojo.hitch( + this, function(response, ioArgs) { + this._on_http_error(response, ioArgs, req, "fetchToPrint"); + } + ); + + this._fetch_execute( + post_params, + "text", + "text/html", + process_fetch_all, + process_error + ); + + return req; + }, + /* *** Begin dojo.data.api.Read methods *** */ "getValue": function( @@ -223,35 +345,18 @@ if (!dojo._hasResource["openils.FlattenerStore"]) { // 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(req) + ")"); var self = this; var callback_scope = req.scope || dojo.global; - - if (!this.mapKey) { - try { - this._get_map_key(); - } catch (E) { - if (req.onError) - req.onError.call(callback_scope, E); - else - throw E; - } - } - - var post_params = this._prepare_flattener_params(req); - - if (!post_params) { - if (typeof req.onComplete == "function") - req.onComplete.call(callback_scope, [], req); - return; + var post_params; + + try { + post_params = this._fetch_prepare(req); + } catch (E) { + if (typeof req.onError == "function") + req.onError.call(callback_scope, E); + else + throw E; } var process_fetch = function(obj, when) { @@ -296,41 +401,21 @@ if (!dojo._hasResource["openils.FlattenerStore"]) { req.onComplete.call(callback_scope, obj, req); }; - req.abort = function() { - throw new FlattenerStoreError( - "The 'abort' operation is not supported" - ); - }; + var process_error = dojo.hitch( + this, function(response, ioArgs) { + this._on_http_error(response, ioArgs, req, "fetch"); + } + ); var fetch_time = this._last_fetch = (new Date().getTime()); - dojo.xhrPost({ - "url": this._flattener_url, - "content": post_params, - "handleAs": "json", - "sync": false, - "preventCache": true, - "headers": {"Accept": "application/json"}, - "load": function(obj) { process_fetch(obj, fetch_time); }, - "error": function(response, ioArgs) { - if (response.status == 402) { /* 'Payment Required' stands - in for cache miss */ - if (self._retried_map_key_already) { - var e = new FlattenerStoreError( - "Server won't cache flattener map?" - ); - if (typeof req.onError == "function") - req.onError.call(callback_scope, e); - else - throw e; - } else { - self._retried_map_key_already = true; - delete self.mapKey; - return self.fetch(req); - } - } - } - }); + this._fetch_execute( + post_params, + "json", + "application/json", + function(obj) { process_fetch(obj, fetch_time); }, + process_error + ); return req; }, @@ -368,7 +453,15 @@ if (!dojo._hasResource["openils.FlattenerStore"]) { return; } - var post_params = this._prepare_flattener_params(keywordArgs); + var post_params; + try { + post_params = this._fetch_prepare(keywordArgs); + } catch (E) { + if (typeof keywordArgs.onError == "function") + keywordArgs.onError.call(callback_scope, E); + else + throw E; + } var process_fetch_one = dojo.hitch( this, function(obj, when) { @@ -404,17 +497,23 @@ if (!dojo._hasResource["openils.FlattenerStore"]) { } ); + var process_error = dojo.hitch( + this, function(response, ioArgs) { + this._on_http_error( + response, ioArgs, keywordArgs, "fetchItemByIdentity" + ); + } + ); + var fetch_time = this._last_fetch = (new Date().getTime()); - dojo.xhrPost({ - "url": this._flattener_url, - "content": post_params, - "handleAs": "json", - "sync": false, - "preventCache": true, - "headers": {"Accept": "application/json"}, - "load": function(obj){ process_fetch_one(obj, fetch_time); } - }); + this._fetch_execute( + post_params, + "json", + "application/json", + function(obj) { process_fetch_one(obj, fetch_time); }, + process_error + ); }, /* dojo.data.api.Write - only very partially implemented, because diff --git a/Open-ILS/web/js/dojo/openils/widget/FlattenerGrid.js b/Open-ILS/web/js/dojo/openils/widget/FlattenerGrid.js index a18cd4aba4..0b1914ec85 100644 --- a/Open-ILS/web/js/dojo/openils/widget/FlattenerGrid.js +++ b/Open-ILS/web/js/dojo/openils/widget/FlattenerGrid.js @@ -16,7 +16,10 @@ if (!dojo._hasResource["openils.widget.FlattenerGrid"]) { * FlattenerGrid in their own right */ "columnReordering": true, "columnPersistKey": null, + "autoCoreFields": false, + "autoFieldFields": null, "showLoadFilter": false, /* use FlattenerFilterDialog */ + "fetchLock": false, /* These potential constructor arguments maybe useful to * FlattenerGrid in their own right, and are passed to @@ -24,6 +27,7 @@ if (!dojo._hasResource["openils.widget.FlattenerGrid"]) { "fmClass": null, "fmIdentifier": null, "mapExtras": null, + "sortFieldReMap": null, "defaultSort": null, /* whatever any part of the UI says will /replace/ this */ "baseSort": null, /* will contains what the columnpicker @@ -53,12 +57,12 @@ if (!dojo._hasResource["openils.widget.FlattenerGrid"]) { /* These are the fields defined in thead -> tr -> [th,th,...]. * For purposes of building the map, where each field has * three boolean attributes "display", "sort" and "filter", - * assume "display" and "sort" are always true for these. + * assume "display" is always true for these. * That doesn't mean that at the UI level we can't hide a * column later. * * If you need extra fields in the map for which display - * or sort should *not* be true, use mapExtras. + * should *not* be true, use mapExtras. */ dojo.forEach( fields, function(field) { @@ -68,7 +72,7 @@ if (!dojo._hasResource["openils.widget.FlattenerGrid"]) { map[field.field] = { "display": true, "filter": (field.ffilter || false), - "sort": true, + "sort": field.fsort, "path": field.fpath || field.field }; /* The following attribute is not for the flattener @@ -133,78 +137,96 @@ if (!dojo._hasResource["openils.widget.FlattenerGrid"]) { return clean; }, - /* The FlattenerStore doesn't need this, but it has at least two - * uses: 1) FlattenerFilterDialog, 2) setting column header labels - * to IDL defaults. - * - * To call these 'Terminii' can be misleading. In certain - * (actually probably common) cases, they won't really be the last - * field in a path, but the next-to-last. Read on. */ - "_calculateMapTerminii": function() { - function _fm_is_selector_for_class(hint, field) { - var cl = fieldmapper.IDL.fmclasses[hint]; + /* Given the hint of a class to start at, follow path to the end + * and return information on the last field. */ + "_followPathToEnd": function(hint, path, allow_selector_backoff) { + function _fm_is_selector_for_class(h, field) { + var cl = fieldmapper.IDL.fmclasses[h]; return (cl.field_map[cl.pkey].selector == field); } - function _follow_to_end(hint, path) { - var last_field, last_hint; - var orig_path = dojo.clone(path); - var field; - - while (field = path.shift()) { - /* XXX this assumes we have the whole IDL loaded. I - * guess we could teach this to work by loading classes - * on demand when we don't have the whole IDL loaded. */ - var field_def = - fieldmapper.IDL.fmclasses[hint].field_map[field]; - - if (field_def["class"] && path.length) { - last_field = field; - last_hint = hint; - - hint = field_def["class"]; - } else if (path.length) { - /* There are more fields left but we can't follow - * the chain via IDL any further. */ - throw new Error( - "_calculateMapTerminii can't parse path " + - orig_path + " (at " + field + ")" - ); - } else { - break; /* keeps field defined after loop */ - } + var last_field, last_hint; + var orig_path = dojo.clone(path); + var field, field_def; + + while (field = path.shift()) { + /* XXX this assumes we have the whole IDL loaded. I + * guess we could teach this to work by loading classes + * on demand when we don't have the whole IDL loaded. */ + field_def = + fieldmapper.IDL.fmclasses[hint].field_map[field]; + + if (!field_def) { + /* This can be ok in some cases. Columns following + * IDL paths involving links with a nonempty "map" + * attribute can be used for display only (no + * sort, no filter). */ + console.info( + "Lost our way in IDL at hint " + hint + + ", field " + field + "; may be ok" + ); + return null; } - var datatype = field_def.datatype; - var indirect = false; - /* Back off the last field in the path if it's a selector - * for its class, because the preceding field will be - * a better thing to hand to AutoFieldWidget. - */ - if (orig_path.length > 1 && - _fm_is_selector_for_class(hint, field)) { - hint = last_hint; - field = last_field; - datatype = "link"; - indirect = true; + if (field_def["class"]) { + last_field = field; + last_hint = hint; + + hint = field_def["class"]; + } else if (path.length) { + /* There are more fields left but we can't follow + * the chain via IDL any further. */ + throw new Error( + "_calculateMapTerminii can't parse path " + + orig_path + " (at " + field + ")" + ); } + } - return { - "fmClass": hint, - "name": field, - "label": field_def.label, - "datatype": datatype, - "indirect": indirect - }; + var datatype = field_def.datatype; + var indirect = false; + /* If allowed, back off the last field in the path if it's a + * selector for its class, because the preceding field will be + * a better thing to hand to AutoFieldWidget. + */ + if (orig_path.length > 1 && allow_selector_backoff && + _fm_is_selector_for_class(hint, field_def.name)) { + hint = last_hint; + field = last_field; + datatype = "link"; + indirect = true; + } else { + field = field_def.name; } + return { + "fmClass": hint, + "name": field, + "label": field_def.label, + "datatype": datatype, + "indirect": indirect + }; + }, + + /* The FlattenerStore doesn't need this, but it has at least two + * uses: 1) FlattenerFilterDialog, 2) setting column header labels + * to IDL defaults. + * + * To call these 'Terminii' can be misleading. In certain + * (actually probably common) cases, they won't really be the last + * field in a path, but the next-to-last. Read on. */ + "_calculateMapTerminii": function() { this.mapTerminii = []; for (var column in this.mapClause) { + var end = this._followPathToEnd( + this.fmClass, + this.mapClause[column].path.split(/\./), + true /* allow selector backoff */ + ); + if (!end) + continue; var terminus = dojo.mixin( - _follow_to_end( - this.fmClass, - this.mapClause[column].path.split(/\./) - ), { + end, { "simple_name": column, "isfilter": this.mapClause[column].filter } @@ -217,8 +239,7 @@ if (!dojo._hasResource["openils.widget.FlattenerGrid"]) { }, "_supplementHeaderNames": function() { - /* You'd be surprised how rarely this make sense in Flattener - * use cases, but if we didn't give a particular header cell + /* If we didn't give a particular header cell * () a display name (the innerHTML of that ), then * use the IDL to provide the label of the terminus of the * flattener path for that column. It may be better than using @@ -237,6 +258,122 @@ if (!dojo._hasResource["openils.widget.FlattenerGrid"]) { ); }, + "_columnOrderingAndLabels": function() { + var labels = []; + var columns = []; + + this.views.views[0].structure.cells[0].forEach( + function(c) { + if (!c.field.match(/^\+/)) { + labels.push(c.name); + columns.push(c.field); + } + } + ); + + return {"labels": labels, "columns": columns}; + }, + + "_getAutoFieldFields": function(fmclass) { + return dojo.clone( + fieldmapper.IDL.fmclasses[fmclass].fields) + .filter( + function(field) { + return !field.virtual && field.datatype != "link"; + } + ).sort( + function(a, b) { return a.label > b.label ? 1 : -1; } + ); + }, + + /* Take our core class (this.fmClass) and add table columns for + * any field we don't already have covered by actual hard-coded + * columns. */ + "_addAutoCoreFields": function() { + var cell_list = this.structure[0].cells[0]; + var fields = dojo.clone( + fieldmapper.IDL.fmclasses[this.fmClass].fields + ).sort( + function(a, b) { return a.label > b.label ? 1 : -1; } + ); + + dojo.forEach( + fields, function(f) { + if (f.datatype == "link" || f.virtual) + return; + + if (cell_list.filter( + function(c) { + if (!c.fpath) return false; + return c.fpath.split(/\./)[0] == f.name; + } + ).length) + return; + + cell_list.push({ + "field": f.name, + "name": f.label, + "fsort": true, + "_visible": false + }); + } + ); + }, + + "_addAutoFieldFields": function(paths) { + var self = this; + var n = 0; + + dojo.forEach( + paths, function(path) { + /* The beginning is the end. */ + var beginning = self._followPathToEnd( + self.fmClass, path.split(/\./), false + ); + if (!beginning) { + return; + } else { + dojo.forEach( + self._getAutoFieldFields(beginning.fmClass), + function(field) { + var would_be_path = + path + "." + field.name; + var wbp_re = + new RegExp("^" + would_be_path); + if (!self.structure[0].cells[0].filter( + function(c) { + return c.fpath && + c.fpath.match(wbp_re); + } + ).length) { + console.info("adding auto field" + would_be_path); + self.structure[0].cells[0].push({ + "field": "AUTO_" + beginning.name + + "_" + field.name, + "name": beginning.label + " - " + + field.label, + "fsort": true, + "fpath": would_be_path, + "_visible": false + }); + } + } + ); + } + } + ); + }, + + "_addAutoFields": function() { + if (this.autoCoreFields) + this._addAutoCoreFields(); + + if (dojo.isArray(this.autoFieldFields)) + this._addAutoFieldFields(this.autoFieldFields); + + this.setStructure(this.structure); + }, + "constructor": function(args) { dojo.mixin(this, args); @@ -245,11 +382,15 @@ if (!dojo._hasResource["openils.widget.FlattenerGrid"]) { }, "startup": function() { - /* Save original query for further filtering later */ this._baseQuery = dojo.clone(this.query); + + this._addAutoFields(); + this._startupGridHelperColumns(); + this._generateMap(); + if (!this.columnPicker) { this.columnPicker = new openils.widget.GridColumnPicker( @@ -271,6 +412,17 @@ if (!dojo._hasResource["openils.widget.FlattenerGrid"]) { this.inherited(arguments); }, + "canSort": function(idx, skip_structure /* API abuse */) { + var initial = this.inherited(arguments); + + /* idx is one-based instead of zero-based for a reason. */ + var view_idx = Math.abs(idx) - 1; + return initial && ( + skip_structure || + this.views.views[0].structure.cells[0][view_idx].fsort + ); + }, + /* Maps ColumnPicker sort fields to the correct format. If no sort fields specified, falls back to defaultSort */ "_mapCPSortFields": function(sortFields) { @@ -287,20 +439,24 @@ if (!dojo._hasResource["openils.widget.FlattenerGrid"]) { "_finishStartup": function(sortFields) { - this.setStore( + this._setStore( /* Seriously, let's leave this as _setStore. */ new openils.FlattenerStore({ "fmClass": this.fmClass, "fmIdentifier": this.fmIdentifier, - "mapClause": (this.mapClause || - this._cleanMapForStore(this._generateMap())), + "mapClause": this._cleanMapForStore(this.mapClause), "baseSort": this.baseSort, - "defaultSort": this._mapCPSortFields(sortFields) + "defaultSort": this._mapCPSortFields(sortFields), + "sortFieldReMap": this.sortFieldReMap + }), this.query ); // pick up any column label changes this.columnPicker.reloadStructure(); + if (!this.fetchLock) + this._refresh(true); + this._showing_create_pane = false; this.overrideEditWidgets = {}; @@ -352,6 +508,18 @@ if (!dojo._hasResource["openils.widget.FlattenerGrid"]) { } }, + "refresh": function() { + this.fetchLock = false; + this._refresh(/* isRender */ true); + }, + + "_fetch": function() { + if (this.fetchLock) + return; + else + return this.inherited(arguments); + }, + /* ******** below are methods mostly copied but * slightly changed from AutoGrid ******** */ @@ -662,6 +830,26 @@ if (!dojo._hasResource["openils.widget.FlattenerGrid"]) { ); } ); + }, + + /* Print the same data that the Flattener is feeding to the + * grid, sorted the same way too. remove limit and offset (i.e., + * print it all. */ + "print": function() { + var coal = this._columnOrderingAndLabels(); + var req = { + "query": this.query, + "queryOptions": { + "all": true, + "columns": coal.columns, + "labels": coal.labels + }, + "onComplete": function(text) { + openils.Util.printHtmlString(text); + } + }; + + this.store.fetchToPrint(req); } } ); @@ -683,6 +871,15 @@ if (!dojo._hasResource["openils.widget.FlattenerGrid"]) { cellDef[a] = value; } ); + + /* fsort and _visible are different. Assume true unless defined. */ + dojo.forEach( + ["fsort", "_visible"], function(a) { + var val = dojo.attr(node, a); + cellDef[a] = (typeof val == "undefined" || val === null) ? + true : dojo.fromJson(val); + } + ); }; })(); diff --git a/Open-ILS/web/js/dojo/openils/widget/GridColumnPicker.js b/Open-ILS/web/js/dojo/openils/widget/GridColumnPicker.js index 9cc367d8f4..1d32bbe7d5 100644 --- a/Open-ILS/web/js/dojo/openils/widget/GridColumnPicker.js +++ b/Open-ILS/web/js/dojo/openils/widget/GridColumnPicker.js @@ -58,16 +58,29 @@ if(!dojo._hasResource["openils.widget.GridColumnPicker"]) { }; }, - /** Loads the current grid structure and passes the - * structure back to the grid to force a UI refresh. - * This is necessary if external forces alter the structure. + /** Loads any grid column label changes, clears any + * non-visible fields from the structure, and passes + * the structure back to the grid to force a UI refresh. */ reloadStructure : function() { - this.structure = this.grid.structure; - this.cells = this.structure[0].cells[0].slice(); + + // update our copy of the column labels + var _this = this; + dojo.forEach( + this.grid.structure[0].cells[0], + function(gcell) { + var cell = _this.cells.filter( + function(c) { return c.field == gcell.field } + )[0]; + cell.name = gcell.name; + } + ); + + this.pruneInvisibleFields(); this.grid.setStructure(this.structure); }, + // determine the visible sorting from the // view and update our list of cells to match refreshCells : function() { @@ -114,7 +127,7 @@ if(!dojo._hasResource["openils.widget.GridColumnPicker"]) { "Auto WidthSort Priority" + ""}); - var tDiv = dojo.create('div', {style : 'height:400px; overflow-y:auto;'}); + var tDiv = dojo.create('div'); tDiv.appendChild(table); var bDiv = dojo.create('div', {style : 'text-align:right; width:100%;', @@ -211,16 +224,24 @@ if(!dojo._hasResource["openils.widget.GridColumnPicker"]) { else this.dialogTable.appendChild(tr); - if ( this.grid.canSort(i+1) ) { // column index is 1-based - - // must be added after its parent node is inserted into the DOM. - var ns = new dijit.form.NumberSpinner( - { constraints : {places : 0}, - value : cell._sort || 0, - style : 'width:4em', - name : 'sort', - }, ipt3 - ); + if (this.grid.canSort( + i + 1, /* column index is 1-based */ + true /* skip structure test (API abuse) */ + )) { + + /* Ugly kludge. When using with FlattenerGrid the + * conditional is needed. Shouldn't hurt usage with + * AutoGrid. */ + if (typeof cell.fsort == "undefined" || cell.fsort) { + // must be added after its parent node is inserted into the DOM. + var ns = new dijit.form.NumberSpinner( + { constraints : {places : 0}, + value : cell._sort || 0, + style : 'width:4em', + name : 'sort', + }, ipt3 + ); + } } } }, @@ -366,6 +387,18 @@ if(!dojo._hasResource["openils.widget.GridColumnPicker"]) { this.grid.update(); }, + // *only* call this when no usr setting tells us what columns + // are visible or not. + pruneInvisibleFields : function() { + this.structure[0].cells[0] = dojo.filter( + this.structure[0].cells[0], + dojo.hitch(this, function(c) { + // keep true or undef, lose false + return typeof c._visible == "undefined" || c._visible; + }) + ); + }, + load : function() { var _this = this; diff --git a/Open-ILS/web/opac/locale/en-US/lang.dtd b/Open-ILS/web/opac/locale/en-US/lang.dtd index fd3e752b38..8839740103 100644 --- a/Open-ILS/web/opac/locale/en-US/lang.dtd +++ b/Open-ILS/web/opac/locale/en-US/lang.dtd @@ -3253,6 +3253,8 @@ + + diff --git a/Open-ILS/xsl/FlatFielder2HTML.xsl b/Open-ILS/xsl/FlatFielder2HTML.xsl index c988ba8fcd..eaa7e37d4a 100644 --- a/Open-ILS/xsl/FlatFielder2HTML.xsl +++ b/Open-ILS/xsl/FlatFielder2HTML.xsl @@ -9,6 +9,13 @@ + diff --git a/Open-ILS/xul/staff_client/server/patron/holds.js b/Open-ILS/xul/staff_client/server/patron/holds.js index f461acf0d3..bf4bdad7db 100644 --- a/Open-ILS/xul/staff_client/server/patron/holds.js +++ b/Open-ILS/xul/staff_client/server/patron/holds.js @@ -391,6 +391,44 @@ patron.holds.prototype = { } } ], + 'cmd_simplified_pull_list' : [ + ['command'], + function() { + try { + var content_params = { + "session": ses(), + "authtime": ses("authtime"), + "no_xulG": false, + "show_nav_buttons": true, + "show_print_button": true + }; + ["url_prefix", "new_tab", "set_tab", + "close_tab", "new_patron_tab", + "set_patron_tab", "volume_item_creator", + "get_new_session", + "holdings_maintenance_tab", "set_tab_name", + "open_chrome_window", "url_prefix", + "network_meter", "page_meter", + "set_statusbar", "set_help_context" + ].forEach(function(k) { + content_params[k] = xulG[k]; + }); + + var loc = urls.XUL_BROWSER + "?url=" + window.escape( + xulG.url_prefix("/eg/circ/hold_pull_list").replace("http:","https:") + ); + xulG.new_tab( + loc, { + "tab_name": "Simplified Pull List", /* XXX i18n */ + "browser": false, + "show_print_button": false + }, content_params + ); + } catch (E) { + g.error.sdump("D_ERROR", E); + } + } + ], 'cmd_holds_print' : [ ['command'], function() { @@ -1488,6 +1526,7 @@ patron.holds.prototype = { var x_expired_checkbox = document.getElementById('expired_checkbox'); var x_print_full_pull_list = document.getElementById('print_full_btn'); var x_print_full_pull_list_alt = document.getElementById('print_alt_btn'); + var x_simplified_pull_list = document.getElementById('simplified_pull_list_btn'); switch(obj.hold_interface_type) { case 'shelf': obj.render_lib_menus({'pickup_lib':true}); @@ -1496,6 +1535,7 @@ patron.holds.prototype = { if (x_lib_menu_placeholder) x_lib_menu_placeholder.hidden = false; if (x_clear_shelf_widgets) x_clear_shelf_widgets.hidden = false; if (x_print_full_pull_list_alt) x_print_full_pull_list_alt.hidden = true; + if (x_simplified_pull_list) x_simplified_pull_list.hidden = true; break; case 'pull' : if (x_fetch_more) x_fetch_more.hidden = false; @@ -1503,6 +1543,7 @@ patron.holds.prototype = { if (x_print_full_pull_list_alt) x_print_full_pull_list_alt.hidden = false; if (x_lib_type_menu) x_lib_type_menu.hidden = true; if (x_lib_menu_placeholder) x_lib_menu_placeholder.hidden = true; + if (x_simplified_pull_list) x_simplified_pull_list.hidden = false; break; case 'record' : obj.render_lib_menus({'pickup_lib':true,'request_lib':true}); @@ -1510,6 +1551,7 @@ patron.holds.prototype = { if (x_lib_type_menu) x_lib_type_menu.hidden = false; if (x_print_full_pull_list_alt) x_print_full_pull_list_alt.hidden = true; if (x_lib_menu_placeholder) x_lib_menu_placeholder.hidden = false; + if (x_simplified_pull_list) x_simplified_pull_list.hidden = true; break; default: if (obj.controller.view.cmd_search_opac) obj.controller.view.cmd_search_opac.setAttribute('hidden', false); @@ -1518,6 +1560,7 @@ patron.holds.prototype = { if (x_lib_menu_placeholder) x_lib_menu_placeholder.hidden = true; if (x_show_cancelled_deck) x_show_cancelled_deck.hidden = false; if (x_print_full_pull_list_alt) x_print_full_pull_list_alt.hidden = true; + if (x_simplified_pull_list) x_simplified_pull_list.hidden = true; break; } setTimeout( // We do this because render_lib_menus above creates and appends a DOM node, but until this thread exits, it doesn't really happen diff --git a/Open-ILS/xul/staff_client/server/patron/holds_overlay.xul b/Open-ILS/xul/staff_client/server/patron/holds_overlay.xul index 1e050026dd..2326cdfcb0 100644 --- a/Open-ILS/xul/staff_client/server/patron/holds_overlay.xul +++ b/Open-ILS/xul/staff_client/server/patron/holds_overlay.xul @@ -20,6 +20,7 @@ + @@ -200,6 +201,7 @@