New pull list interface taking advantage of flattener for speed,
authorLebbeous Fogle-Weekley <lebbeous@esilibrary.com>
Sat, 31 Mar 2012 16:17:40 +0000 (12:17 -0400)
committerMike Rylander <mrylander@gmail.com>
Fri, 25 May 2012 14:57:22 +0000 (10:57 -0400)
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 <lebbeous@esilibrary.com>
Signed-off-by: Mike Rylander <mrylander@gmail.com>

15 files changed:
Open-ILS/examples/fm_IDL.xml
Open-ILS/src/perlmods/lib/OpenILS/Application/Flattener.pm
Open-ILS/src/perlmods/lib/OpenILS/Utils/Fieldmapper.pm
Open-ILS/src/perlmods/lib/OpenILS/WWW/FlatFielder.pm
Open-ILS/src/sql/Pg/950.data.seed-values.sql
Open-ILS/src/sql/Pg/upgrade/XXXX.schema.simplified-hold-pull-list.sql [new file with mode: 0644]
Open-ILS/src/templates/circ/hold_pull_list.tt2 [new file with mode: 0644]
Open-ILS/web/js/dojo/openils/FlattenerStore.js
Open-ILS/web/js/dojo/openils/widget/FlattenerGrid.js
Open-ILS/web/js/dojo/openils/widget/GridColumnPicker.js
Open-ILS/web/opac/locale/en-US/lang.dtd
Open-ILS/xsl/FlatFielder2HTML.xsl
Open-ILS/xul/staff_client/server/patron/holds.js
Open-ILS/xul/staff_client/server/patron/holds_overlay.xul
docs/RELEASE_NOTES_NEXT/simplified-hold-pull-list.txt [new file with mode: 0644]

index 79b8738..88b1b71 100644 (file)
@@ -2303,7 +2303,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                        <field reporter:label="Notes" name="notes" oils_persist:virtual="true" reporter:datatype="link"/>
                        <field reporter:label="URI Maps" name="uri_maps" oils_persist:virtual="true" reporter:datatype="link"/>
                        <field reporter:label="URIs" name="uris" oils_persist:virtual="true" reporter:datatype="link"/>
-                       <field reporter:label="Sort Key" name="label_sortkey" reporter:datatype="text"/>
+                       <field reporter:label="Call Number Sort Key" name="label_sortkey" reporter:datatype="text"/>
                        <field reporter:label="Classification Scheme" name="label_class" reporter:datatype="link"/>
                        <field reporter:label="Prefix" name="prefix" reporter:datatype="link"/>
                        <field reporter:label="Suffix" name="suffix" reporter:datatype="link"/>
@@ -2649,7 +2649,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                        <link field="billing_location" reltype="has_a" key="id" map="" class="aou"/>
                </links>
        </class>
-       <class id="au" controller="open-ils.cstore" oils_obj:fieldmapper="actor::user" oils_persist:tablename="actor.usr" reporter:core="true" reporter:label="ILS User">
+       <class id="au" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="actor::user" oils_persist:tablename="actor.usr" reporter:core="true" reporter:label="ILS User">
                <fields oils_persist:primary="id" oils_persist:sequence="actor.usr_id_seq">
                        <field reporter:label="All Addresses" name="addresses" oils_persist:virtual="true" reporter:datatype="link"/>
                        <field reporter:label="All Library Cards" name="cards" oils_persist:virtual="true" reporter:datatype="link"/>
@@ -2747,6 +2747,11 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                        <link field="reservations" reltype="has_many" key="usr" map="" class="bresv"/>
                        <link field="usr_activity" reltype="has_many" key="usr" map="" class="auact"/>
                </links>
+               <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+                       <actions>
+                               <retrieve permission="VIEW_USER" context_field="home_ou" />
+                       </actions>
+               </permacrud>
        </class>
        <class id="cuat" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="config::usr_activity_type" oils_persist:tablename="config.usr_activity_type" reporter:label="User Activity Type">
                <fields oils_persist:primary="id" oils_persist:sequence="config.usr_activity_type_id_seq">
@@ -4833,7 +4838,7 @@ SELECT  usr,
         </permacrud>
        </class>
 
-       <!-- A note: Please update alhr when updating ahr -->
+       <!-- A note: Please update alhr and ahopl when updating ahr -->
        <class id="ahr" controller="open-ils.cstore" oils_obj:fieldmapper="action::hold_request" oils_persist:tablename="action.hold_request" reporter:core="true" reporter:label="Hold Request">
                <fields oils_persist:primary="id" oils_persist:sequence="action.hold_request_id_seq">
                        <field reporter:label="Status" name="status" oils_persist:virtual="true" />
@@ -4850,7 +4855,7 @@ SELECT  usr,
                        <field reporter:label="Hold ID" name="id" reporter:datatype="id" />
                        <field reporter:label="Notifications Phone Number" name="phone_notify" reporter:datatype="text"/>
                        <field reporter:label="Notifications SMS Number" name="sms_notify" reporter:datatype="text"/>
-                       <field reporter:label="Notifications SMS Carrier" name="sms_carrier" reporter:datatype="text"/>
+                       <field reporter:label="Notifications SMS Carrier" name="sms_carrier" reporter:datatype="link"/>
                        <field reporter:label="Pickup Library" name="pickup_lib" reporter:datatype="org_unit"/>
                        <field reporter:label="Last Targeting Date/Time" name="prev_check_time" reporter:datatype="timestamp"/>
                        <field reporter:label="Requesting Library" name="request_lib" reporter:datatype="org_unit"/>
@@ -4893,9 +4898,118 @@ SELECT  usr,
                        <link field="cancel_cause" reltype="might_have" key="id" map="" class="ahrcc"/>
                        <link field="notes" reltype="has_many" key="hold" map="" class="ahrn"/>
                        <link field="current_shelf_lib" reltype="has_a" key="id" map="" class="aou"/>
-                       <link field="sms_carrier" reltype="might_have" key="code" map="" class="csc"/>
+                       <link field="sms_carrier" reltype="has_a" key="id" map="" class="csc"/>
                </links>
        </class>
+       <class id="ahopl" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="action::hold_on_pull_list" reporter:label="Hold On Pull List" oils_persist:readonly="true">
+               <oils_persist:source_definition><![CDATA[
+               SELECT
+                       ahr.*,
+                       COALESCE(acplo.position, 999) AS
+                               copy_location_order_position,
+                       CASE WHEN au.alias IS NOT NULL THEN
+                               au.alias
+                       ELSE
+                               REGEXP_REPLACE(ARRAY_TO_STRING(ARRAY[
+                                       COALESCE(au.family_name, ''),
+                                       COALESCE(au.suffix, ''),
+                                       ', ',
+                                       COALESCE(au.prefix, ''),
+                                       COALESCE(au.first_given_name, ''),
+                                       COALESCE(au.second_given_name, '')
+                               ], ' '), E'\\s+,', ',')
+                       END AS usr_display_name,
+                       TRIM(acnp.label || ' ' || acn.label || ' ' || acns.label)
+                               AS call_number_label,
+                       siss.label AS issuance_label,
+                       (ahr.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())
+               ]]></oils_persist:source_definition>
+               <fields oils_persist:primary="id">
+                       <field reporter:label="Status" name="status" oils_persist:virtual="true" />
+                       <field reporter:label="Transit" name="transit" oils_persist:virtual="true" />
+                       <field reporter:label="Capture Date/Time" name="capture_time" reporter:datatype="timestamp"/>
+                       <field reporter:label="Currently Targeted Copy" name="current_copy" />
+                       <field reporter:label="Notify by Email?" name="email_notify" reporter:datatype="bool"/>
+                       <field reporter:label="Hold Expire Date/Time" name="expire_time" reporter:datatype="timestamp"/>
+                       <field reporter:label="Fulfilling Library" name="fulfillment_lib" reporter:datatype="org_unit"/>
+                       <field reporter:label="Fulfilling Staff" name="fulfillment_staff" />
+                       <field reporter:label="Fulfillment Date/Time" name="fulfillment_time" reporter:datatype="timestamp"/>
+                       <field reporter:label="Hold Type" name="hold_type" reporter:datatype="text"/>
+                       <field reporter:label="Holdable Formats (for M-type hold)" name="holdable_formats" reporter:datatype="text"/>
+                       <field reporter:label="Hold ID" name="id" reporter:datatype="id" />
+                       <field reporter:label="Notifications Phone Number" name="phone_notify" reporter:datatype="text"/>
+                       <field reporter:label="Notifications SMS Number" name="sms_notify" reporter:datatype="text"/>
+                       <field reporter:label="Notifications SMS Carrier" name="sms_carrier" reporter:datatype="link"/>
+                       <field reporter:label="Pickup Library" name="pickup_lib" reporter:datatype="org_unit"/>
+                       <field reporter:label="Last Targeting Date/Time" name="prev_check_time" reporter:datatype="timestamp"/>
+                       <field reporter:label="Requesting Library" name="request_lib" reporter:datatype="org_unit"/>
+                       <field reporter:label="Request Date/Time" name="request_time" reporter:datatype="timestamp"/>
+                       <field reporter:label="Requesting User" name="requestor" reporter:datatype="link"/>
+                       <field reporter:label="Item Selection Depth" name="selection_depth" />
+                       <field reporter:label="Selection Locus" name="selection_ou" reporter:datatype="org_unit"/>
+                       <field reporter:label="Target Object ID" name="target" reporter:datatype="link"/>
+                       <field reporter:label="Hold User" name="usr" reporter:datatype="link"/>
+                       <field reporter:label="Hold Cancel Date/Time" name="cancel_time" reporter:datatype="timestamp"/>
+                       <field reporter:label="Notify Time" name="notify_time" oils_persist:virtual="true" reporter:datatype="timestamp"/>
+                       <field reporter:label="Notify Count" name="notify_count" oils_persist:virtual="true" reporter:datatype="int" />
+                       <field reporter:label="Notifications" name="notifications" oils_persist:virtual="true" reporter:datatype="link"/>
+                       <field reporter:label="Bib Record link" name="bib_rec" oils_persist:virtual="true" reporter:datatype="link"/>
+                       <field reporter:label="Eligible Copies" name="eligible_copies" oils_persist:virtual="true" reporter:datatype="link"/>
+                       <field reporter:label="Currently Frozen" name="frozen" reporter:datatype="bool"/>
+                       <field reporter:label="Thaw Date (if frozen)" name="thaw_date" reporter:datatype="timestamp"/>
+                       <field reporter:label="Shelf Time" name="shelf_time" reporter:datatype="timestamp"/>
+                       <field reporter:label="Cancelation cause" name="cancel_cause" reporter:datatype="link" />
+                       <field reporter:label="Cancelation note" name="cancel_note" reporter:datatype="text" />
+                       <field reporter:label="Top of Queue" name="cut_in_line" reporter:datatype="bool" />
+                       <field reporter:label="Is Mint Condition" name="mint_condition" reporter:datatype="bool" />
+                       <field reporter:label="Shelf Expire Time" name="shelf_expire_time" reporter:datatype="timestamp"/>
+                       <field reporter:label="Notes" name="notes" reporter:datatype="link" oils_persist:virtual="true"/>
+                       <field reporter:label="Current Shelf Lib" name="current_shelf_lib" reporter:datatype="org_unit"/>
+                       <field reporter:label="Copy Location Sort Order" name="copy_location_order_position" reporter:datatype="int" />
+                       <field reporter:label="User Display Name" name="usr_display_name" reporter:datatype="text" />
+                       <field reporter:label="Call Number Label" name="call_number_label" reporter:datatype="text" />
+                       <field reporter:label="Issuance Label" name="issuance_label" reporter:datatype="text" />
+                       <field reporter:label="Is Staff Hold?" name="is_staff_hold" reporter:datatype="bool" />
+               </fields>
+               <links>
+                       <link field="fulfillment_lib" reltype="has_a" key="id" map="" class="aou"/>
+                       <link field="fulfillment_staff" reltype="has_a" key="id" map="" class="au"/>
+                       <link field="pickup_lib" reltype="has_a" key="id" map="" class="aou"/>
+                       <link field="selection_ou" reltype="has_a" key="id" map="" class="aou"/>
+                       <link field="requestor" reltype="has_a" key="id" map="" class="au"/>
+                       <link field="current_copy" reltype="has_a" key="id" map="" class="acp"/>
+                       <link field="usr" reltype="has_a" key="id" map="" class="au"/>
+                       <link field="request_lib" reltype="has_a" key="id" map="" class="aou"/>
+                       <link field="transit" reltype="might_have" key="hold" map="" class="ahtc"/>
+                       <link field="notifications" reltype="has_many" key="hold" map="" class="ahn"/>
+                       <link field="eligible_copies" reltype="has_many" key="hold" map="target_copy" class="ahcm"/>
+                       <link field="bib_rec" reltype="might_have" key="id" map="" class="rhrr"/>
+                       <link field="cancel_cause" reltype="might_have" key="id" map="" class="ahrcc"/>
+                       <link field="notes" reltype="has_many" key="hold" map="" class="ahrn"/>
+                       <link field="current_shelf_lib" reltype="has_a" key="id" map="" class="aou"/>
+                       <link field="sms_carrier" reltype="has_a" key="id" map="" class="csc"/>
+               </links>
+               <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+                       <actions>
+                               <retrieve permission="VIEW_HOLD" context_field="pickup_lib" />
+                       </actions>
+               </permacrud>
+       </class>
        <class id="alhr" controller="open-ils.cstore" oils_obj:fieldmapper="action::last_hold_request" reporter:label="Last Captured Hold Request" oils_persist:readonly="true">
                <oils_persist:source_definition>
                        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,
                        <link field="entries" reltype="has_many" key="stat_cat" map="" class="asce"/>
                </links>
        </class>
-       <class id="ac" controller="open-ils.cstore" oils_obj:fieldmapper="actor::card" oils_persist:tablename="actor.card" reporter:label="Library Card">
+       <class id="ac" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="actor::card" oils_persist:tablename="actor.card" reporter:label="Library Card">
                <fields oils_persist:primary="id" oils_persist:sequence="actor.card_id_seq">
                        <field reporter:label="IsActive?" name="active" reporter:datatype="bool"/>
                        <field reporter:label="Barcode" name="barcode" reporter:datatype="text"/>
@@ -5147,6 +5261,13 @@ SELECT  usr,
                <links>
                        <link field="usr" reltype="has_a" key="id" map="" class="au"/>
                </links>
+               <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+                       <actions>
+                               <retrieve permission="VIEW_USER">
+                                       <context link="usr" field="home_ou" />
+                               </retrieve>
+                       </actions>
+               </permacrud>
        </class>
     <class id="actscsf" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="actor::stat_cat_sip_fields" oils_persist:tablename="actor.stat_cat_sip_fields" reporter:label="SIP Statistical Category Field Identifier">
         <fields oils_persist:primary="field">
@@ -7898,7 +8019,7 @@ SELECT  usr,
                        <link field="folder" reltype="has_a" key="id" map="" class="rof"/>
                </links>
        </class>
-       <class id="rmsr" controller="open-ils.reporter-store open-ils.cstore" oils_obj:fieldmapper="reporter::materialized_simple_record" oils_persist:tablename="reporter.materialized_simple_record" reporter:label="Fast Simple Record Extracts">
+       <class id="rmsr" controller="open-ils.reporter-store open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="reporter::materialized_simple_record" oils_persist:tablename="reporter.materialized_simple_record" reporter:label="Fast Simple Record Extracts">
                <fields oils_persist:primary="id">
                        <field reporter:label="Record ID" name="id" reporter:datatype="id" />
                        <field reporter:label="Fingerprint" name="fingerprint" reporter:datatype="text"/>
@@ -7916,6 +8037,11 @@ SELECT  usr,
                <links>
                        <link field="biblio_record" reltype="might_have" key="id" map="" class="bre"/>
                </links>
+               <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+                       <actions>
+                               <retrieve />
+                       </actions>
+               </permacrud>
        </class>
        <class id="rssr" controller="open-ils.reporter-store" oils_obj:fieldmapper="reporter::super_simple_record" oils_persist:tablename="reporter.super_simple_record" reporter:label="Simple Record Extracts">
                <fields oils_persist:primary="id">
index a6c77d3..ff232a9 100644 (file)
@@ -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
index df00277..eddb944 100644 (file)
@@ -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
                                };
                }
        }
index 0e5fc98..b80c9e8 100644 (file)
@@ -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}) {
index 87c3eff..1dd5b5d 100644 (file)
@@ -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 (file)
index 0000000..eeff3ea
--- /dev/null
@@ -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 (file)
index 0000000..fc497b9
--- /dev/null
@@ -0,0 +1,99 @@
+[% WRAPPER base.tt2 %]
+[% ctx.page_title = 'Hold Pull List' %]
+<script type="text/javascript">
+    dojo.require("dijit.form.Button");
+    dojo.require("openils.widget.OrgUnitFilteringSelect");
+    dojo.require("openils.widget.FlattenerGrid");
+
+    var map_extras = {
+        "copy_circ_lib": {
+            "path": "current_copy.circ_lib",
+            "filter": true
+        },
+        "call_number_sort_key": {
+            "path": "current_copy.call_number.label_sortkey",
+            "sort" :true
+        }
+    };
+
+    function set_grid_query_from_org_selector() {
+        grid.query = {
+            "copy_circ_lib": org_selector.attr("value")
+        };
+        grid.refresh();
+    }
+
+    function prepare_org_selector(perm) {
+        new openils.User().buildPermOrgSelector(
+            perm, org_selector, null,
+            function() {
+                dojo.connect(
+                    org_selector, "onChange", set_grid_query_from_org_selector
+                );
+                set_grid_query_from_org_selector();
+            }
+        );
+    }
+
+    openils.Util.addOnLoad(
+        function() {
+            prepare_org_selector("VIEW_HOLD");
+        }
+    );
+
+</script>
+<div dojoType="dijit.layout.ContentPane" layoutAlign="client">
+    <div dojoType="dijit.layout.ContentPane"
+         layoutAlign="top" class="oils-header-panel">
+        <div>Hold Pull List</div>
+        <div>
+            <button dojoType="dijit.form.Button"
+                onClick="grid.print();">Print Pull List</button>
+        </div>
+    </div>
+    <div class="oils-acq-basic-roomy">
+        <label for="org_selector">Show the pull list for:</label>
+        <select
+            id="org_selector" jsId="org_selector"
+            dojoType="openils.widget.OrgUnitFilteringSelect"
+            searchAttr="name" labelAttr="name">
+        </select>
+    </div>
+    <table
+        jsid="grid"
+        dojoType="openils.widget.FlattenerGrid"
+        columnPersistKey='"circ.hold_pull_list"'
+        autoHeight="10"
+        editOnEnter="false"
+        hideSelector="true"
+        autoCoreFields="true"
+        autoFieldFields="['current_copy','current_copy.call_number.record.simple_record']"
+        editStyle="pane"
+        showLoadFilter="true"
+        fmClass="'ahopl'"
+        defaultSort="['copy_location_sort_order','call_number_sort_key']"
+        mapExtras="map_extras"
+        sortFieldReMap="{call_number_label: 'call_number_sort_key'}"
+        fetchLock="true"
+        query="{}">
+        <thead>
+            <tr>
+                <th field="shelving_loc" fpath="current_copy.location.name" ffilter="true">Shelving Location</th>
+                <th field="call_number_label" fpath="call_number_label"></th>
+                <th field="author" fpath="current_copy.call_number.record.simple_record.author">Author</th>
+                <th field="title" fpath="current_copy.call_number.record.simple_record.title">Title</th>
+                <th field="barcode" fpath="current_copy.barcode"></th>
+                <th field="parts" fpath="current_copy.parts.label" fsort="false">Parts</th>
+                <th field="notes" fpath="notes.body" fsort="false" _visible="false">Hold Notes</th>
+                <th field="patron_barcode" fpath="usr.card.barcode" _visible="false">Patron Barcode</th>
+                <th field="pickup_lib_name" fpath="pickup_lib.name" _visible="false">Pickup Library</th>
+                <th field="pickup_lib_shortname" fpath="pickup_lib.shortname" _visible="false">Pickup Library (Shortname)</th>
+                <th field="request_lib_name" fpath="request_lib.name" _visible="false">Request Library</th>
+                <th field="request_lib_shortname" fpath="request_lib.shortname" _visible="false">Request Library (Shortname)</th>
+                <th field="selection_ou" fpath="selection_ou.shortname" _visible="false">Selection Locus</th>
+                <th field="sms_carrier_name" fpath="sms_carrier.name" _visible="false">SMS Carrier</th>
+            </tr>
+        </thead>
+    </table>
+</div>
+[% END %]
index e5cbdd7..700d3f2 100644 (file)
@@ -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
index a18cd4a..0b1914e 100644 (file)
@@ -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
                  * (<th>) a display name (the innerHTML of that <th>), 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
+             * <th> 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);
+                }
+            );
         };
     })();
 
index 9cc367d..1d32bbe 100644 (file)
@@ -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"]) {
                 "<th width='23%'>Auto Width</th><th width='23%'>Sort Priority</th></tr></thead>" +
                 "<tbody />"});
 
-            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;
 
index fd3e752..8839740 100644 (file)
 <!ENTITY staff.patron.holds_overlay.print_full_pull_list.accesskey "u">
 <!ENTITY staff.patron.holds_overlay.print_alt_pull_list.label "Print Full Pull List (Alternate strategy)">
 <!ENTITY staff.patron.holds_overlay.print_alt_pull_list.accesskey "y">
+<!ENTITY staff.patron.holds_overlay.simplified_pull_list.label "Simplified Pull List Interface">
+<!ENTITY staff.patron.holds_overlay.simplified_pull_list.accesskey "i">
 <!ENTITY staff.patron.holds_overlay.place_hold.label "Place Hold">
 <!ENTITY staff.patron.holds_overlay.place_hold.accesskey "H">
 <!ENTITY staff.patron.holds_overlay.show_cancelled_holds.label "Show Cancelled Holds">
index c988ba8..eaa7e37 100644 (file)
@@ -9,6 +9,13 @@
     <html>
         <head>
             <meta http-equiv="Content-Type" content="text/html" charset="utf-8"/>
+            <style type="text/css">
+                /* This CSS controls whether data printed from an interface
+                based on FlattenerGrid has visible table cell borders. */
+
+                table { border-collapse: collapse; }
+                td, th { border: 1px solid black; }
+            </style>
         </head>
         <body>
             <table>
index f461acf..bf4bdad 100644 (file)
@@ -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
index 1e05002..2326cdf 100644 (file)
@@ -20,6 +20,7 @@
         <command id="cmd_holds_print" />
         <command id="cmd_holds_print_full" />
         <command id="cmd_holds_print_alt" />
+        <command id="cmd_simplified_pull_list" />
         <command id="cmd_show_catalog" />
         <command id="cmd_retrieve_patron" />
         <command id="cmd_holds_edit_desire_mint_condition" />
         <button id="holds_print" label="&staff.patron.holds_overlay.print.label;" command="cmd_holds_print" accesskey="&staff.patron.holds_overlay.print.accesskey;" />
         <button id="print_full_btn" hidden="true" label="&staff.patron.holds_overlay.print_full_pull_list.label;" command="cmd_holds_print_full" accesskey="&staff.patron.holds_overlay.print_full_pull_list.accesskey;" />
         <button id="print_alt_btn" hidden="true" label="&staff.patron.holds_overlay.print_alt_pull_list.label;" command="cmd_holds_print_alt" accesskey="&staff.patron.holds_overlay.print_alt_pull_list.accesskey;" />
+        <button id="simplified_pull_list_btn" hidden="true" label="&staff.patron.holds_overlay.simplified_pull_list.label;" command="cmd_simplified_pull_list" accesskey="&staff.patron.holds_overlay.simplified_pull_list.accesskey;" />
         <spacer flex="1"/>
     </hbox>
 
diff --git a/docs/RELEASE_NOTES_NEXT/simplified-hold-pull-list.txt b/docs/RELEASE_NOTES_NEXT/simplified-hold-pull-list.txt
new file mode 100644 (file)
index 0000000..53b92e6
--- /dev/null
@@ -0,0 +1,30 @@
+Simplified Hold Pull List
+-------------------------
+
+There is a new hold pull list interface based on the Flattener service that's
+designed to perform faster than existing pull list interfaces, both in staff
+client display and printing.
+
+Sorting
+~~~~~~~
+
+You can sort on any one column by clicking on it.  Click again to reverse
+direction.  This is typical of similar interfaces.
+
+Now you can also sort by multiple columns.  Right click the column headers of
+the grid in the pull list interface to get a dialog that allows you to sort
+by multiple columns, in any order.
+
+Column Picking
+~~~~~~~~~~~~~~
+
+The same dialog that allows you to choose multiple sort columns (accessed by
+right clicking any column header) also allows you to toggle the display of any
+column available to the pull list on or off.
+
+Persistence
+~~~~~~~~~~~
+
+Once saved, your changes in this dialog persist for your user account.  Column
+display, display order, and `sorting choices affect printing as well as
+displayed output.