LP#1704873 webstaff: label printing
authorJason Etheridge <jason@esilibrary.com>
Sun, 5 Mar 2017 21:07:17 +0000 (16:07 -0500)
committerBill Erickson <berickxx@gmail.com>
Mon, 7 Aug 2017 18:34:31 +0000 (14:34 -0400)
...initial hook
...better stock template for labels, and a | wrap filter
...pull in some Library Settings for Print Labels
...Reset to Default button for templates for both receipt and item print labels
...toward tabs for Print Label interface
...template management for print labels
...bundle the Call Number Template in with saved templates
...manual editing of cn's for print labels
...And affixes in the stock CN template.
...Settings tab for print labels
...add Print Labels action to Copy Buckets
...and some other cosmetic tweaks to the Actions menu
...add Print Labels to Holdings View
...provide MVR's for use with Print Labels
...better than super simple record, but Display Fields will be better yet later on
...new org unit settings for print labels
...and fix to stock call number template for affixes
...and cn_wrap filter for call numbers
...handle null location affixes
...implement Print Item Labels on Save & Exit in vol/copy editor

Signed-off-by: Jason Etheridge <jason@EquinoxInitiative.org>
Signed-off-by: Bill Erickson <berickxx@gmail.com>

20 files changed:
Open-ILS/src/perlmods/lib/OpenILS/Application/Cat.pm
Open-ILS/src/sql/Pg/950.data.seed-values.sql
Open-ILS/src/sql/Pg/upgrade/XXXX.data.webstaff_print_label_ou_settings.sql [new file with mode: 0644]
Open-ILS/src/templates/staff/admin/workstation/t_print_templates.tt2
Open-ILS/src/templates/staff/cat/bucket/copy/t_view.tt2
Open-ILS/src/templates/staff/cat/catalog/t_holdings.tt2
Open-ILS/src/templates/staff/cat/item/t_list.tt2
Open-ILS/src/templates/staff/cat/printlabels/index.tt2 [new file with mode: 0644]
Open-ILS/src/templates/staff/cat/printlabels/t_view.tt2 [new file with mode: 0644]
Open-ILS/src/templates/staff/cat/volcopy/t_attr_edit.tt2
Open-ILS/src/templates/staff/cat/volcopy/t_defaults.tt2
Open-ILS/src/templates/staff/share/print_templates/t_item_label.tt2 [new file with mode: 0644]
Open-ILS/src/templates/staff/share/print_templates/t_item_label_cn.tt2 [new file with mode: 0644]
Open-ILS/web/js/ui/default/staff/admin/workstation/app.js
Open-ILS/web/js/ui/default/staff/cat/bucket/copy/app.js
Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
Open-ILS/web/js/ui/default/staff/cat/item/app.js
Open-ILS/web/js/ui/default/staff/cat/printlabels/app.js [new file with mode: 0644]
Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
Open-ILS/web/js/ui/default/staff/services/print.js

index e083c3b..19c0b6a 100644 (file)
@@ -1103,6 +1103,7 @@ sub fleshed_volume_update {
     my $retarget_holds = [];
     my $auto_merge_vols = $options->{auto_merge_vols};
     my $create_parts = $options->{create_parts};
+    my $copy_ids = [];
 
     for my $vol (@$volumes) {
         $logger->info("vol-update: investigating volume ".$vol->id);
@@ -1148,12 +1149,17 @@ sub fleshed_volume_update {
             $evt = $assetcom->update_fleshed_copies(
                 $editor, $oargs, $vol, $copies, $delete_stats, $retarget_holds, undef, $create_parts);
             return $evt if $evt;
+            push( @$copy_ids, $_->id ) for @$copies;
         }
     }
 
     $editor->finish;
     reset_hold_list($auth, $retarget_holds);
-    return scalar(@$volumes);
+    if ($options->{return_copy_ids}) {
+        return $copy_ids;
+    } else {
+        return scalar(@$volumes);
+    }
 }
 
 
index eef768d..c40c86d 100644 (file)
@@ -16642,6 +16642,207 @@ VALUES
      'coust', 'description'),
  'bool');
 
+
+INSERT into config.org_unit_setting_type (
+     name
+    ,grp
+    ,label
+    ,description
+    ,datatype
+) VALUES ( ----------------------------------------
+     'webstaff.cat.label.font.family'
+    ,'cat'
+    ,oils_i18n_gettext(
+         'webstaff.cat.label.font.family'
+        ,'Item Print Label Font Family'
+        ,'coust'
+        ,'label'
+    )
+    ,oils_i18n_gettext(
+         'webstaff.cat.label.font.family'
+        ,'Set the preferred font family for item print labels. You can specify a list of CSS fonts, separated by commas, in order of preference; the system will use the first font it finds with a matching name. For example, "Arial, Helvetica, serif"'
+        ,'coust'
+        ,'description'
+    )
+    ,'string'
+), ( ----------------------------------------
+     'webstaff.cat.label.font.size'
+    ,'cat'
+    ,oils_i18n_gettext(
+         'webstaff.cat.label.font.size'
+        ,'Item Print Label Font Size'
+        ,'coust'
+        ,'label'
+    )
+    ,oils_i18n_gettext(
+         'webstaff.cat.label.font.size'
+        ,'Set the default font size for item print labels. Please include a unit of measurement that is valid CSS. For example, "12pt" or "16px" or "1em"'
+        ,'coust'
+        ,'description'
+    )
+    ,'string'
+), ( ----------------------------------------
+     'webstaff.cat.label.font.weight'
+    ,'cat'
+    ,oils_i18n_gettext(
+         'webstaff.cat.label.font.weight'
+        ,'Item Print Label Font Weight'
+        ,'coust'
+        ,'label'
+    )
+    ,oils_i18n_gettext(
+         'webstaff.cat.label.font.weight'
+        ,'Set the default font weight for item print labels. Please use the CSS specification for values for font-weight.  For example, "normal", "bold", "bolder", or "lighter"'
+        ,'coust'
+        ,'description'
+    )
+    ,'string'
+), ( ----------------------------------------
+     'webstaff.cat.label.left_label.left_margin'
+    ,'cat'
+    ,oils_i18n_gettext(
+         'webstaff.cat.label.left_label.left_margin'
+        ,'Item Print Label - Left Margin for Left Label'
+        ,'coust'
+        ,'label'
+    )
+    ,oils_i18n_gettext(
+         'webstaff.cat.label.left_label.left_margin'
+        ,'Set the default left margin for the leftmost item print Label. Please include a unit of measurement that is valid CSS. For example, "1in" or "2.5cm"'
+        ,'coust'
+        ,'description'
+    )
+    ,'string'
+), ( ----------------------------------------
+     'webstaff.cat.label.right_label.left_margin'
+    ,'cat'
+    ,oils_i18n_gettext(
+         'webstaff.cat.label.right_label.left_margin'
+        ,'Item Print Label - Left Margin for Right Label'
+        ,'coust'
+        ,'label'
+    )
+    ,oils_i18n_gettext(
+         'webstaff.cat.label.right_label.left_margin'
+        ,'Set the default left margin for the rightmost item print label (or in other words, the desired space between the two labels). Please include a unit of measurement that is valid CSS. For example, "1in" or "2.5cm"'
+        ,'coust'
+        ,'description'
+    )
+    ,'string'
+), ( ----------------------------------------
+     'webstaff.cat.label.left_label.height'
+    ,'cat'
+    ,oils_i18n_gettext(
+         'webstaff.cat.label.left_label.height'
+        ,'Item Print Label - Height for Left Label'
+        ,'coust'
+        ,'label'
+    )
+    ,oils_i18n_gettext(
+         'webstaff.cat.label.left_label.height'
+        ,'Set the default height for the leftmost item print label. Please include a unit of measurement that is valid CSS. For example, "1in" or "2.5cm"'
+        ,'coust'
+        ,'description'
+    )
+    ,'string'
+), ( ----------------------------------------
+     'webstaff.cat.label.left_label.width'
+    ,'cat'
+    ,oils_i18n_gettext(
+         'webstaff.cat.label.left_label.width'
+        ,'Item Print Label - Width for Left Label'
+        ,'coust'
+        ,'label'
+    )
+    ,oils_i18n_gettext(
+         'webstaff.cat.label.left_label.width'
+        ,'Set the default width for the leftmost item print label. Please include a unit of measurement that is valid CSS. For example, "1in" or "2.5cm"'
+        ,'coust'
+        ,'description'
+    )
+    ,'string'
+), ( ----------------------------------------
+     'webstaff.cat.label.right_label.height'
+    ,'cat'
+    ,oils_i18n_gettext(
+         'webstaff.cat.label.right_label.height'
+        ,'Item Print Label - Height for Right Label'
+        ,'coust'
+        ,'label'
+    )
+    ,oils_i18n_gettext(
+         'webstaff.cat.label.right_label.height'
+        ,'Set the default height for the rightmost item print label. Please include a unit of measurement that is valid CSS. For example, "1in" or "2.5cm"'
+        ,'coust'
+        ,'description'
+    )
+    ,'string'
+), ( ----------------------------------------
+     'webstaff.cat.label.right_label.width'
+    ,'cat'
+    ,oils_i18n_gettext(
+         'webstaff.cat.label.right_label.width'
+        ,'Item Print Label - Width for Right Label'
+        ,'coust'
+        ,'label'
+    )
+    ,oils_i18n_gettext(
+         'webstaff.cat.label.right_label.width'
+        ,'Set the default width for the rightmost item print label. Please include a unit of measurement that is valid CSS. For example, "1in" or "2.5cm"'
+        ,'coust'
+        ,'description'
+    )
+    ,'string'
+), (
+     'webstaff.cat.label.inline_css'
+    ,'cat'
+    ,oils_i18n_gettext(
+         'webstaff.cat.label.inline_css'
+        ,'Item Print Label - Inline CSS'
+        ,'coust'
+        ,'label'
+    )
+    ,oils_i18n_gettext(
+         'webstaff.cat.label.inline_css'
+        ,'This setting allows you to inject arbitrary CSS into the item print label template.  For example, ".printlabel { text-transform: uppercase; }"'
+        ,'coust'
+        ,'description'
+    )
+    ,'string'
+), (
+     'webstaff.cat.label.call_number_wrap_filter_height'
+    ,'cat'
+    ,oils_i18n_gettext(
+         'webstaff.cat.label.call_number_wrap_filter_height'
+        ,'Item Print Label - Call Number Wrap Filter Height'
+        ,'coust'
+        ,'label'
+    )
+    ,oils_i18n_gettext(
+         'webstaff.cat.label.call_number_wrap_filter_height'
+        ,'This setting is used to set the default height (in number of lines) to use for call number wrapping in the left print label.'
+        ,'coust'
+        ,'description'
+    )
+    ,'integer'
+), (
+     'webstaff.cat.label.call_number_wrap_filter_width'
+    ,'cat'
+    ,oils_i18n_gettext(
+         'webstaff.cat.label.call_number_wrap_filter_width'
+        ,'Item Print Label - Call Number Wrap Filter Width'
+        ,'coust'
+        ,'label'
+    )
+    ,oils_i18n_gettext(
+         'webstaff.cat.label.call_number_wrap_filter_width'
+        ,'This setting is used to set the default width (in number of characters) to use for call number wrapping in the left print label.'
+        ,'coust'
+        ,'description'
+    )
+    ,'integer'
+);
+
 INSERT INTO config.global_flag (name, label, value, enabled) VALUES (
     'circ.holds.retarget_interval',
     oils_i18n_gettext(
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.data.webstaff_print_label_ou_settings.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.data.webstaff_print_label_ou_settings.sql
new file mode 100644 (file)
index 0000000..10e7ca7
--- /dev/null
@@ -0,0 +1,252 @@
+BEGIN;
+
+--SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+
+INSERT into config.org_unit_setting_type (
+     name
+    ,grp
+    ,label
+    ,description
+    ,datatype
+) VALUES ( ----------------------------------------
+     'webstaff.cat.label.font.family'
+    ,'cat'
+    ,oils_i18n_gettext(
+         'webstaff.cat.label.font.family'
+        ,'Item Print Label Font Family'
+        ,'coust'
+        ,'label'
+    )
+    ,oils_i18n_gettext(
+         'webstaff.cat.label.font.family'
+        ,'Set the preferred font family for item print labels. You can specify a list of CSS fonts, separated by commas, in order of preference; the system will use the first font it finds with a matching name. For example, "Arial, Helvetica, serif"'
+        ,'coust'
+        ,'description'
+    )
+    ,'string'
+), ( ----------------------------------------
+     'webstaff.cat.label.font.size'
+    ,'cat'
+    ,oils_i18n_gettext(
+         'webstaff.cat.label.font.size'
+        ,'Item Print Label Font Size'
+        ,'coust'
+        ,'label'
+    )
+    ,oils_i18n_gettext(
+         'webstaff.cat.label.font.size'
+        ,'Set the default font size for item print labels. Please include a unit of measurement that is valid CSS. For example, "12pt" or "16px" or "1em"'
+        ,'coust'
+        ,'description'
+    )
+    ,'string'
+), ( ----------------------------------------
+     'webstaff.cat.label.font.weight'
+    ,'cat'
+    ,oils_i18n_gettext(
+         'webstaff.cat.label.font.weight'
+        ,'Item Print Label Font Weight'
+        ,'coust'
+        ,'label'
+    )
+    ,oils_i18n_gettext(
+         'webstaff.cat.label.font.weight'
+        ,'Set the default font weight for item print labels. Please use the CSS specification for values for font-weight.  For example, "normal", "bold", "bolder", or "lighter"'
+        ,'coust'
+        ,'description'
+    )
+    ,'string'
+), ( ----------------------------------------
+     'webstaff.cat.label.left_label.left_margin'
+    ,'cat'
+    ,oils_i18n_gettext(
+         'webstaff.cat.label.left_label.left_margin'
+        ,'Item Print Label - Left Margin for Left Label'
+        ,'coust'
+        ,'label'
+    )
+    ,oils_i18n_gettext(
+         'webstaff.cat.label.left_label.left_margin'
+        ,'Set the default left margin for the leftmost item print Label. Please include a unit of measurement that is valid CSS. For example, "1in" or "2.5cm"'
+        ,'coust'
+        ,'description'
+    )
+    ,'string'
+), ( ----------------------------------------
+     'webstaff.cat.label.right_label.left_margin'
+    ,'cat'
+    ,oils_i18n_gettext(
+         'webstaff.cat.label.right_label.left_margin'
+        ,'Item Print Label - Left Margin for Right Label'
+        ,'coust'
+        ,'label'
+    )
+    ,oils_i18n_gettext(
+         'webstaff.cat.label.right_label.left_margin'
+        ,'Set the default left margin for the rightmost item print label (or in other words, the desired space between the two labels). Please include a unit of measurement that is valid CSS. For example, "1in" or "2.5cm"'
+        ,'coust'
+        ,'description'
+    )
+    ,'string'
+), ( ----------------------------------------
+     'webstaff.cat.label.left_label.height'
+    ,'cat'
+    ,oils_i18n_gettext(
+         'webstaff.cat.label.left_label.height'
+        ,'Item Print Label - Height for Left Label'
+        ,'coust'
+        ,'label'
+    )
+    ,oils_i18n_gettext(
+         'webstaff.cat.label.left_label.height'
+        ,'Set the default height for the leftmost item print label. Please include a unit of measurement that is valid CSS. For example, "1in" or "2.5cm"'
+        ,'coust'
+        ,'description'
+    )
+    ,'string'
+), ( ----------------------------------------
+     'webstaff.cat.label.left_label.width'
+    ,'cat'
+    ,oils_i18n_gettext(
+         'webstaff.cat.label.left_label.width'
+        ,'Item Print Label - Width for Left Label'
+        ,'coust'
+        ,'label'
+    )
+    ,oils_i18n_gettext(
+         'webstaff.cat.label.left_label.width'
+        ,'Set the default width for the leftmost item print label. Please include a unit of measurement that is valid CSS. For example, "1in" or "2.5cm"'
+        ,'coust'
+        ,'description'
+    )
+    ,'string'
+), ( ----------------------------------------
+     'webstaff.cat.label.right_label.height'
+    ,'cat'
+    ,oils_i18n_gettext(
+         'webstaff.cat.label.right_label.height'
+        ,'Item Print Label - Height for Right Label'
+        ,'coust'
+        ,'label'
+    )
+    ,oils_i18n_gettext(
+         'webstaff.cat.label.right_label.height'
+        ,'Set the default height for the rightmost item print label. Please include a unit of measurement that is valid CSS. For example, "1in" or "2.5cm"'
+        ,'coust'
+        ,'description'
+    )
+    ,'string'
+), ( ----------------------------------------
+     'webstaff.cat.label.right_label.width'
+    ,'cat'
+    ,oils_i18n_gettext(
+         'webstaff.cat.label.right_label.width'
+        ,'Item Print Label - Width for Right Label'
+        ,'coust'
+        ,'label'
+    )
+    ,oils_i18n_gettext(
+         'webstaff.cat.label.right_label.width'
+        ,'Set the default width for the rightmost item print label. Please include a unit of measurement that is valid CSS. For example, "1in" or "2.5cm"'
+        ,'coust'
+        ,'description'
+    )
+    ,'string'
+), (
+     'webstaff.cat.label.inline_css'
+    ,'cat'
+    ,oils_i18n_gettext(
+         'webstaff.cat.label.inline_css'
+        ,'Item Print Label - Inline CSS'
+        ,'coust'
+        ,'label'
+    )
+    ,oils_i18n_gettext(
+         'webstaff.cat.label.inline_css'
+        ,'This setting allows you to inject arbitrary CSS into the item print label template.  For example, ".printlabel { text-transform: uppercase; }"'
+        ,'coust'
+        ,'description'
+    )
+    ,'string'
+), (
+     'webstaff.cat.label.call_number_wrap_filter_height'
+    ,'cat'
+    ,oils_i18n_gettext(
+         'webstaff.cat.label.call_number_wrap_filter_height'
+        ,'Item Print Label - Call Number Wrap Filter Height'
+        ,'coust'
+        ,'label'
+    )
+    ,oils_i18n_gettext(
+         'webstaff.cat.label.call_number_wrap_filter_height'
+        ,'This setting is used to set the default height (in number of lines) to use for call number wrapping in the left print label.'
+        ,'coust'
+        ,'description'
+    )
+    ,'integer'
+), (
+     'webstaff.cat.label.call_number_wrap_filter_width'
+    ,'cat'
+    ,oils_i18n_gettext(
+         'webstaff.cat.label.call_number_wrap_filter_width'
+        ,'Item Print Label - Call Number Wrap Filter Width'
+        ,'coust'
+        ,'label'
+    )
+    ,oils_i18n_gettext(
+         'webstaff.cat.label.call_number_wrap_filter_width'
+        ,'This setting is used to set the default width (in number of characters) to use for call number wrapping in the left print label.'
+        ,'coust'
+        ,'description'
+    )
+    ,'integer'
+
+
+);
+
+-- for testing, setting removal:
+--DELETE FROM actor.org_unit_setting WHERE name IN (
+--     'webstaff.cat.label.font.family'
+--    ,'webstaff.cat.label.font.size'
+--    ,'webstaff.cat.label.font.weight'
+--    ,'webstaff.cat.label.left_label.height'
+--    ,'webstaff.cat.label.left_label.width'
+--    ,'webstaff.cat.label.left_label.left_margin'
+--    ,'webstaff.cat.label.right_label.height'
+--    ,'webstaff.cat.label.right_label.width'
+--    ,'webstaff.cat.label.right_label.left_margin'
+--    ,'webstaff.cat.label.inline_css'
+--    ,'webstaff.cat.label.call_number_wrap_filter_height'
+--    ,'webstaff.cat.label.call_number_wrap_filter_width'
+--);
+--DELETE FROM config.org_unit_setting_type_log WHERE field_name IN (
+--     'webstaff.cat.label.font.family'
+--    ,'webstaff.cat.label.font.size'
+--    ,'webstaff.cat.label.font.weight'
+--    ,'webstaff.cat.label.left_label.height'
+--    ,'webstaff.cat.label.left_label.width'
+--    ,'webstaff.cat.label.left_label.left_margin'
+--    ,'webstaff.cat.label.right_label.height'
+--    ,'webstaff.cat.label.right_label.width'
+--    ,'webstaff.cat.label.right_label.left_margin'
+--    ,'webstaff.cat.label.inline_css'
+--    ,'webstaff.cat.label.call_number_wrap_filter_height'
+--    ,'webstaff.cat.label.call_number_wrap_filter_width'
+--);
+--DELETE FROM config.org_unit_setting_type WHERE name IN (
+--     'webstaff.cat.label.font.family'
+--    ,'webstaff.cat.label.font.size'
+--    ,'webstaff.cat.label.font.weight'
+--    ,'webstaff.cat.label.left_label.height'
+--    ,'webstaff.cat.label.left_label.width'
+--    ,'webstaff.cat.label.left_label.left_margin'
+--    ,'webstaff.cat.label.right_label.height'
+--    ,'webstaff.cat.label.right_label.width'
+--    ,'webstaff.cat.label.right_label.left_margin'
+--    ,'webstaff.cat.label.inline_css'
+--    ,'webstaff.cat.label.call_number_wrap_filter_height'
+--    ,'webstaff.cat.label.call_number_wrap_filter_width'
+--);
+
+
+COMMIT;
index b049c56..064bfff 100644 (file)
@@ -46,6 +46,7 @@
     </div>
   </div>
   <div class="col-md-7">
+    <button class="btn btn-default pull-left" ng-click="reset_to_default()">[% l('Reset to Default') %]</button>
     <button class="btn btn-default pull-left" ng-click="save_locally()">[% l('Save Locally') %]</button>
     <div class="btn-group pull-right">
       <span class="btn btn-default btn-file">
index c2daf27..98308cc 100644 (file)
 
   [% INCLUDE 'staff/cat/bucket/copy/t_grid_menu.tt2' %]
 
-  <eg-grid-action label="[% l('Request Selected Copies') %]" 
+  <eg-grid-action label="[% l('Remove Selected Copies from Bucket') %]" group="[% l('Bucket') %]"
+    handler="detachCopies"></eg-grid-action>
+  <eg-grid-action label="[% l('Request Selected Copies') %]" group="[% l('Items') %]"
     handler="requestItems"></eg-grid-action>
-  <eg-grid-action label="[% l('Edit Selected Copies') %]" 
+  <eg-grid-action label="[% l('Edit Selected Copies') %]" group="[% l('Items') %]"
     handler="spawnHoldingsEdit"></eg-grid-action>
-  <eg-grid-action label="[% l('Remove Selected Copies from Bucket') %]" 
-    handler="detachCopies"></eg-grid-action>
-  <eg-grid-action label="[% l('Transfer Selected Copies to Marked Volume') %]" 
+  <eg-grid-action handler="print_labels" group="[% l('Show') %]"
+    label="[% l('Print Labels') %]"></eg-grid-action>
+  <eg-grid-action label="[% l('Transfer Selected Copies to Marked Volume') %]" group="[% l('Items') %]"
     handler="transferCopies"></eg-grid-action>
-  <eg-grid-action label="[% l('Delete Selected Copies from Catalog') %]" 
+  <eg-grid-action label="[% l('Delete Selected Copies from Catalog') %]" group="[% l('Items') %]"
     handler="deleteCopiesFromCatalog"></eg-grid-action>
   <eg-grid-action label="[% l('Apply Tags') %]" 
     handler="applyTags"></eg-grid-action>
index c4ca14b..b506f3f 100644 (file)
@@ -55,6 +55,8 @@
       label="[% l('Triggered Events') %]"></eg-grid-action>
     <eg-grid-action handler="selectedHoldingsItemStatusHolds" group="[% l('Show') %]"
       label="[% l('Item Holds') %]"></eg-grid-action>
+    <eg-grid-action handler="selectedHoldingsPrintLabels" group="[% l('Show') %]"
+      label="[% l('Print Labels') %]"></eg-grid-action>
 
     <eg-grid-action handler="selectedHoldingsDamaged" group="[% l('Mark') %]"
       label="[% l('Item as Damaged') %]"></eg-grid-action>
index a481d0d..6afb87a 100644 (file)
@@ -34,6 +34,8 @@
     label="[% l('Item Holds') %]"></eg-grid-action>
   <eg-grid-action handler="showBibHolds" group="[% l('Show') %]"
     label="[% l('Record Holds') %]"></eg-grid-action>
+  <eg-grid-action handler="print_labels" group="[% l('Show') %]"
+    label="[% l('Print Labels') %]"></eg-grid-action>
     
   <eg-grid-action handler="selectedHoldingsDamaged" group="[% l('Mark') %]"
     label="[% l('Item as Damaged') %]"></eg-grid-action>
diff --git a/Open-ILS/src/templates/staff/cat/printlabels/index.tt2 b/Open-ILS/src/templates/staff/cat/printlabels/index.tt2
new file mode 100644 (file)
index 0000000..ac89fc5
--- /dev/null
@@ -0,0 +1,30 @@
+[%
+  WRAPPER "staff/base.tt2";
+  ctx.page_title = l("Print Item Labels"); 
+  ctx.page_app = "egPrintLabels";
+%]
+
+[% BLOCK APP_JS %]
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/grid.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/ui.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/file.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/eframe.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/cat/printlabels/app.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/cat/services/record.js"></script>
+<script>
+angular.module('egCoreMod').run(['egStrings', function(s) {
+    s.KEY_EXPIRED = "[% l('Key expired, please close this window; it no longer remembers which items you are printing labels for.') %]";
+    s.PRINT_LABEL_TEMPLATE_SUCCESS_SAVE = "[% l('Saved print label template(s)') %]";
+    s.PRINT_LABEL_TEMPLATE_SUCCESS_DELETE = "[% l('Deleted print label template') %]";
+}]);
+</script>
+[% END %]
+
+<style>
+</style>
+
+<div ng-view></div>
+
+[% END %]
+
+
diff --git a/Open-ILS/src/templates/staff/cat/printlabels/t_view.tt2 b/Open-ILS/src/templates/staff/cat/printlabels/t_view.tt2
new file mode 100644 (file)
index 0000000..418d4e8
--- /dev/null
@@ -0,0 +1,161 @@
+<style>
+  /* TODO: move me */
+  .print-template-text {
+    height: 36em;
+    width: 100%;
+  }
+  .cn-template-text {
+    height: 12em;
+    width: 100%;
+  }
+</style>
+
+<h2>[% l('Print Item Labels') %]</h2>
+
+<div class="row bg-info">
+    <div class="col-md-6">
+        <div class="row">
+            <div class="col-md-1">
+                <span class="h4">[% l('Template') %]</span>
+            </div>
+            <div class="col-md-5">
+                <eg-basic-combo-box list="template_name_list" selected="template_name"></eg-basic-combo-box>
+            </div>
+            <div class="col-md-1">
+                <button class="btn btn-default" ng-click="applyTemplate(template_name)">[% l('Apply') %]</button>
+            </div>
+            <div class="col-md-1">
+                <span class="h4">[% l('Printer') %]</span>
+            </div>
+            <div class="col-md-4">
+                <select class="form-control" ng-model="print.template_context">
+                  <option value="default">[% l('Default') %]</option>
+                  <option value="receipt">[% l('Receipt') %]</option>
+                  <option value="label">[% l('Label') %]</option>
+                  <option value="mail">[% l('Mail') %]</option>
+                  <option value="offline">[% l('Offline') %]</option>
+                </select>
+            </div>
+        </div>
+    </div>
+    <div class="col-md-2">
+        <div class="btn-group">
+            <button class="btn btn-default" ng-click="saveTemplate(template_name)">[% l('Save') %]</button>
+            <button class="btn btn-default" ng-click="deleteTemplate(template_name)">[% l('Delete') %]</button>
+        </div>
+    </div>
+    <div class="col-md-3">
+        <div class="btn-group">
+            <span class="btn btn-default btn-file">
+                [% l('Import') %]
+                <input type="file" eg-file-reader container="imported_templates.data">
+            </span>
+            <label class="btn btn-default"
+                eg-json-exporter container="templates"
+                default-file-name="'[% l('exported_label_templates.json') %]'">
+                [% l('Export') %]
+            </label>
+            <label class="btn btn-default" ng-click="reset_to_default()">[% l('Default') %]</button>
+        </div>
+    </div>
+    <div class="col-md-1 pull-right">
+        <button class="btn btn-default" ng-click="print_labels()">[% l('Print') %]</button>
+    </div>
+</div>
+
+<hr/>
+
+<div class="row">
+  <div class="col-md-5">
+    <ul class="nav nav-tabs">
+        <li ng-class="{active : current_tab == 'cn_template'}">
+            <a ng-click="set_tab('cn_template')">
+                [% l('Call Number Template') %]
+            </a>
+        </li>
+        <li ng-class="{active : current_tab == 'call_numbers'}">
+            <a ng-click="set_tab('call_numbers')">
+                [% l('Call Numbers') %]
+            </a>
+        </li>
+        <li ng-class="{active : current_tab == 'settings'}">
+            <a ng-click="set_tab('settings')">
+                [% l('Settings') %]
+            </a>
+        </li>
+        <li ng-class="{active : current_tab == 'template'}">
+            <a ng-click="set_tab('template')">
+                [% l('Label Template') %]
+            </a>
+        </li>
+    </ul>
+    <div class="tab-content">
+        <div class="tab-pane active">
+            <div ng-show="current_tab == 'cn_template'">
+                <h4>
+                    [% l('Call Number Preview') %]
+                </h4>
+                <div eg-print-template-output ng-show="true"
+                    content="print.cn_template_content"
+                    context="{ copy : preview_scope.copies[0], get_cn_and_location_prefix : preview_scope.get_cn_and_location_prefix, get_cn_and_location_suffix : preview_scope.get_cn_and_location_suffix, settings : preview_scope.settings }"></div>
+                <h4>
+                    [% l('Call Number Template') %]
+                </h4>
+                <div><span>[% l('Changes here will wipe out manual changes in the Call Numbers tab.') %]<br/></span></div>
+                <textarea ng-model="print.cn_template_content" class="print-template-text">
+                </textarea>
+                <div ng-repeat="copy in preview_scope.copies">
+                    <div id="cn_for_copy_{{copy.id}}" eg-print-template-output ng-show="false"
+                        content="print.cn_template_content"
+                        context="{ copy : copy, get_cn_and_location_prefix : preview_scope.get_cn_and_location_prefix, get_cn_and_location_suffix : preview_scope.get_cn_and_location_suffix, settings : preview_scope.settings }"></div>
+                </div>
+            </div>
+            <div ng-show="current_tab == 'call_numbers'">
+                <h4>
+                    [% l('Formatted Call Numbers') %]
+                </h4>
+                <div><span>[% l('Manual adjustments may be made here. These do not get saved with templates.') %]<br/></span></div>
+                <div ng-repeat="cn in rendered_call_number_set">
+                    <textarea ng-model="cn.value" class="cn-template-text">
+                    </textarea>
+                </div>
+            </div>
+            <div ng-show="current_tab == 'settings'">
+                <div><span>[% l('These settings do get saved with templates and will override corresponding Library Settings.') %]<br/></span></div>
+                <div ng-repeat="s in org_unit_settings">
+                    <div class="row" style="margin-top: 5mm; border-top: solid thin black">
+                        <div class="col-md-6" style="font-weight: bold">{{s.label}}</div>
+                        <div class="col-md-6"><input type="text" ng-model="preview_scope.settings[s.name]"></input></div>
+                    </div>
+
+                    <div class="row">
+                        <div>{{s.description}}</div>
+                    </div>
+                </div>
+
+            </div>
+            <div ng-show="current_tab == 'template'">
+                <div ng-if="print.load_failed" class="alert alert-danger">
+                  [% l(
+                    "Unable to load template '[_1]'.  The web server returned an error.",
+                    '{{print.template_name}}')
+                  %]
+                </div>
+                <div>
+                  <textarea ng-model="print.template_content" class="print-template-text">
+                  </textarea>
+                </div>
+            </div>
+        </div>
+    </div>
+  </div>
+  <div class="col-md-7">
+    <h3>
+        [% l('Label Preview') %]
+    </h3>
+    <div eg-print-template-output
+      content="print.template_content"
+      context="preview_scope"></div>
+  </div> <!-- col -->
+</div>
+
index fc2c5e6..70fac46 100644 (file)
@@ -41,7 +41,7 @@
             </div>
         </div>
         <div class="col-md-2">
-            <button class="btn btn-default pull-right" ng-click="clearWorking()" type="button">Clear</button>
+            <button class="btn btn-default pull-right" ng-click="clearWorking()" type="button">[% l('Clear') %]</button>
         </div>
     </div>
 
index 507205d..8d20cf2 100644 (file)
@@ -69,8 +69,8 @@
             <div class="row pad-vert">
                 <div class="col-xs-12">
                     <label>
-                        <input type="checkbox" ng-change="saveDefaults()" ng-model="defaults.print_spine_labels"/>
-                        [% l('Print Spine Labels') %]
+                        <input type="checkbox" ng-change="saveDefaults()" ng-model="defaults.print_item_labels"/>
+                        [% l('Print Item Labels on Save &amp; Exit') %]
                     </label>
                 </div>
             </div>
diff --git a/Open-ILS/src/templates/staff/share/print_templates/t_item_label.tt2 b/Open-ILS/src/templates/staff/share/print_templates/t_item_label.tt2
new file mode 100644 (file)
index 0000000..263e1b0
--- /dev/null
@@ -0,0 +1,285 @@
+<!--
+Template for printing item labels (spine, pocket, etc.).
+
+Like receipt slip templates, this template uses AngularJS
+which fuses HTML and code together.  The code portion is
+mostly used to bind data for the item labels being printed
+to various HTML elements, but you can also use code to
+perform conditional logic, transform the data, etc.
+
+-->
+<!--
+
+The <style> section is for holding most of our CSS, which
+is a language for controlling the appearance of HTML markup.
+
+The {{ and }} markers are similar to HTML's < and > characters,
+but are for separating AngularJS variables and code from the
+HTML itself.
+
+-->
+<style>
+    /* The .spine here defines a CSS "class", which in this case, is used by our
+       left label */
+    .spine {
+
+        /* this pulls from the "Item Print Label Font Family" setting, but will
+           default to 'monospace' */
+        font-family: {{settings['webstaff.cat.label.font.family'] || 'monospace'}};
+
+        /* this pulls from the "Item Print Label Font Size" setting, but will
+           default to 'normal' */
+        font-size: {{settings['webstaff.cat.label.font.size'] || '10'}};
+
+        /* this pulls from the "Item Print Label Font Weight" setting, but will
+           default to 'normal' */
+        font-weight: {{settings['webstaff.cat.label.font.weight'] || 'normal'}};
+
+        /* this pulls from the "Item Print Label - Height for Left Label"
+           setting, but will default to '1.25in' */
+        height: {{settings['webstaff.cat.label.left_label.height'] || '1.25in'}};
+        min-height: {{settings['webstaff.cat.label.left_label.height'] || '1.25in'}};
+        max-height: {{settings['webstaff.cat.label.left_label.height'] || '1.25in'}};
+
+        /* this pulls from the "Item Print Label - Width for Left Label"
+           setting, but will default to '1in' */
+        width: {{settings['webstaff.cat.label.left_label.width'] || '1in'}};
+        min-width: {{settings['webstaff.cat.label.left_label.width'] || '1in'}};
+        max-width: {{settings['webstaff.cat.label.left_label.width'] || '1in'}};
+
+        /* this pulls from the "Item Print Label - Left Margin for Left Label"
+           setting, but will default to '0in' */
+        margin-left: {{settings['webstaff.cat.label.left_label.left_margin'] || '0in'}};
+    }
+
+    /* This CSS class is used by our right label */
+    .pocket {
+
+        /* this pulls from the "Item Print Label Font Family" setting, but will
+           default to 'monospace' */
+        font-family: {{settings['webstaff.cat.label.font.family'] || 'monospace'}};
+
+        /* this pulls from the "Item Print Label Font Size" setting, but will
+           default to 'normal' */
+        font-size: {{settings['webstaff.cat.label.font.size'] || '10'}};
+
+        /* this pulls from the "Item Print Label Font Weight" setting, but will
+           default to 'normal' */
+        font-weight: {{settings['webstaff.cat.label.font.weight'] || 'normal'}};
+
+        /* this pulls from the "Item Print Label - Height for Left Label"
+           setting, but will default to '1.25in' */
+        height: {{settings['webstaff.cat.label.right_label.height'] || '1.25in'}};
+        min-height: {{settings['webstaff.cat.label.right_label.height'] || '1.25in'}};
+        max-height: {{settings['webstaff.cat.label.right_label.height'] || '1.25in'}};
+
+        /* this pulls from the "Item Print Label - Width for Left Label"
+           setting, but will default to '2.625in' */
+        width: {{settings['webstaff.cat.label.right_label.width'] || '2.625in'}};
+        min-width: {{settings['webstaff.cat.label.right_label.width'] || '2.625in'}};
+        max-width: {{settings['webstaff.cat.label.right_label.width'] || '2.625in'}};
+
+        /* this pulls from the "Item Print Label - Left Margin for Left Label"
+           setting, but will default to '0in' */
+        margin-left: {{settings['webstaff.cat.label.right_label.left_margin'] || '0in'}};
+
+    }
+
+    /* This is to help mitigate artifacts in the Preview pane which don't
+       actually print */
+    .labels ::-webkit-scrollbar { 
+        display: none; 
+    }
+
+    /* This pulls from the "Item Print Label - Inline CSS" setting */
+    {{settings['webstaff.cat.label.inline_css']||''}}
+
+</style>
+
+<!-- Here we are implementing our combined label as 2-column HTML <table>, with
+     each table containing a <pre> tag for preserving whitespace and linefeeds.
+
+     The ng-repeat attribute is from AngularJS, and in this case is looping
+     through a list of all the items that were selected when the Print Label
+     interface was invoked.  Each iteration puts item data into the variable
+     "copy", which may be referenced in the HTML by escaping it with {{ and }}
+
+     The following are available, and you may treat these similar to the
+     "macros" in earlier versions of Evergreen:
+
+                copy['active_date']
+                copy['age_protect']
+                copy['alert_message']
+                copy['barcode']
+                copy['call_number.copies']
+                copy['call_number.create_date']
+                copy['call_number.deleted']
+                copy['call_number.edit_date']
+                copy['call_number.id']
+                copy['call_number.label']
+                copy['call_number.label_class']
+                copy['call_number.label_sortkey']
+                copy['call_number.notes']
+                copy['call_number.owning_lib']
+                copy['call_number.prefix.id']
+                copy['call_number.prefix.label']
+                copy['call_number.prefix.label_sortkey']
+                copy['call_number.prefix.owning_lib']
+                copy['call_number.record.active']
+                copy['call_number.record.author_field_entries']
+                copy['call_number.record.call_numbers']
+                copy['call_number.record.create_date']
+                copy['call_number.record.creator.ws_ou']
+                copy['call_number.record.deleted']
+                copy['call_number.record.edit_date']
+                copy['call_number.record.fingerprint']
+                copy['call_number.record.fixed_fields']
+                copy['call_number.record.full_record_entries']
+                copy['call_number.record.id']
+                copy['call_number.record.identifier_field_entries']
+                copy['call_number.record.keyword_field_entries']
+                copy['call_number.record.language']
+                copy['call_number.record.last_xact_id']
+                copy['call_number.record.marc']
+                copy['call_number.record.metarecord']
+                copy['call_number.record.notes']
+                copy['call_number.record.owner']
+                copy['call_number.record.quality']
+                copy['call_number.record.series_field_entries']
+                copy['call_number.record.share_depth']
+                copy['call_number.record.simple_record.author']
+                copy['call_number.record.simple_record.fingerprint']
+                copy['call_number.record.simple_record.id']
+                copy['call_number.record.simple_record.isbn']
+                copy['call_number.record.simple_record.issn']
+                copy['call_number.record.simple_record.pubdate']
+                copy['call_number.record.simple_record.publisher']
+                copy['call_number.record.simple_record.quality']
+                copy['call_number.record.simple_record.tcn_source']
+                copy['call_number.record.simple_record.tcn_value']
+                copy['call_number.record.simple_record.title']
+                copy['call_number.record.source']
+                copy['call_number.record.subject_field_entries']
+                copy['call_number.record.tcn_source']
+                copy['call_number.record.tcn_value']
+                copy['call_number.record.title_field_entries']
+                copy['call_number.suffix.id']
+                copy['call_number.suffix.label']
+                copy['call_number.suffix.label_sortkey']
+                copy['call_number.suffix.owning_lib']
+                copy['call_number.uri_maps']
+                copy['call_number.uris']
+                copy['circ_as_type']
+                copy['circ_lib']
+                copy['circ_modifier']
+                copy['circulate']
+                copy['copy_number']
+                copy['cost']
+                copy['create_date']
+                copy['deleted']
+                copy['deposit']
+                copy['deposit_amount']
+                copy['dummy_author']
+                copy['dummy_isbn']
+                copy['dummy_title']
+                copy['edit_date']
+                copy['fine_level']
+                copy['floating']
+                copy['holdable']
+                copy['id']
+                copy['loan_duration']
+                copy['location.checkin_alert']
+                copy['location.circulate']
+                copy['location.copies']
+                copy['location.deleted']
+                copy['location.holdable']
+                copy['location.hold_verify']
+                copy['location.id']
+                copy['location.label_prefix']
+                copy['location.label_suffix']
+                copy['location.name']
+                copy['location.opac_visible']
+                copy['location.orders']
+                copy['location.owning_lib']
+                copy['mint_condition']
+                copy['opac_visible']
+                copy['price']
+                copy['ref']
+                copy['status_changed_time']
+                copy['status.copy_active']
+                copy['status.holdable']
+                copy['status.id']
+                copy['status.is_available']
+                copy['status.name']
+                copy['status.opac_visible']
+                copy['status.restrict_copy_delete']
+
+    There are also two functions available.  The following pulls manually
+    edited call numbers for a given item:
+
+                get_cn_for(copy)
+
+    And this one pulls bibliographic data for a given item:
+
+                get_bib_for(copy)
+
+    It returns a data structure similar to "copy" that has the following fields:
+
+                get_bib_for(copy)['author']
+                get_bib_for(copy)['copy_count']
+                get_bib_for(copy)['doc_id']
+                get_bib_for(copy)['doc_type']
+                get_bib_for(copy)['edition']
+                get_bib_for(copy)['isbn']
+                get_bib_for(copy)['physical_description']
+                get_bib_for(copy)['pubdate']
+                get_bib_for(copy)['publisher']
+                get_bib_for(copy)['series.0']
+                get_bib_for(copy)['synopsis']
+                get_bib_for(copy)['tcn']
+                get_bib_for(copy)['title']
+                get_bib_for(copy)['toc']
+                get_bib_for(copy)['types_of_resource.0']
+
+    Finally, there are filter functions which can be used to modify or transform
+    data being outputted.  For example, consider the following:
+
+                get_bib_for(copy)['title'] | limitTo:28
+
+    This would show up to the first 28 characters for an item's title and
+    truncate the rest.  Now, this:
+
+                get_bib_for(copy)['title'] | wrap:28
+
+    This would try to the wrap the item's title every 28 characters.
+
+                get_bib_for(copy)['title'] | wrap:28:multi:'  '
+
+    This does the same thing but indents subsequent lines with 2 spaces each.
+    
+                get_bib_for(copy)['title'] | wrap:28:once:'  '
+
+    This wraps the title just once, prefixes the second line with two spaces,
+    and truncates anything after the 2nd line.
+
+-->
+
+<table class="labels" style="page-break-after: always;" ng-repeat="copy in copies"><tr valign="top"><td>
+
+<!-- Spine Label contents -->
+<pre class="spine" style="border:none" ng-show="true">
+{{get_cn_for(copy)}}
+</pre>
+
+</td><td>
+
+<!-- Pocket Label contents -->
+<pre class="pocket" style="border:none" ng-show="true">
+{{copy.barcode}}
+{{copy['call_number.label']}}
+{{get_bib_for(copy).author }}
+{{get_bib_for(copy).title | wrap:28:'once':' '}}
+</pre>
+
+</td></tr></table>
+
diff --git a/Open-ILS/src/templates/staff/share/print_templates/t_item_label_cn.tt2 b/Open-ILS/src/templates/staff/share/print_templates/t_item_label_cn.tt2
new file mode 100644 (file)
index 0000000..85d201c
--- /dev/null
@@ -0,0 +1,10 @@
+<pre>
+{{
+    [
+         get_cn_and_location_prefix(copy)
+        ,copy['call_number.label']
+        ,get_cn_and_location_suffix(copy)
+    ]
+    | cn_wrap:settings['webstaff.cat.label.call_number_wrap_filter_width']:settings['webstaff.cat.label.call_number_wrap_filter_height']:copy['call_number.label_class']
+}}
+</pre>
index 15c1fcb..06a908d 100644 (file)
@@ -555,6 +555,16 @@ function($scope , $q , egCore , ngToast) {
         });
     }
 
+    $scope.reset_to_default = function() {
+        egCore.print.removePrintTemplate(
+            $scope.print.template_name
+        );
+        egCore.print.removePrintTemplateContext(
+            $scope.print.template_name
+        );
+        $scope.template_changed();
+    }
+
     $scope.save_locally = function() {
         egCore.print.storePrintTemplate(
             $scope.print.template_name,
index 7eea044..d99e70e 100644 (file)
@@ -485,6 +485,28 @@ function($scope,  $q , $routeParams , $timeout , $window , $uibModal , bucketSvc
         });
     }
 
+    $scope.print_labels = function() {
+        var cp_list = []
+        angular.forEach($scope.gridControls.selectedItems(), function (i) {
+            cp_list.push(i.id);
+        })
+
+        egCore.net.request(
+            'open-ils.actor',
+            'open-ils.actor.anon_cache.set_value',
+            null, 'print-labels-these-copies', {
+                copies : cp_list
+            }
+        ).then(function(key) {
+            if (key) {
+                var url = egCore.env.basePath + 'cat/printlabels/' + key;
+                $timeout(function() { $window.open(url, '_blank') });
+            } else {
+                alert('Could not create anonymous cache key!');
+            }
+        });
+    }
+
     $scope.requestItems = function() {
         var copy_list = $scope.gridControls.selectedItems().map(
             function (i) {
index e71f74f..d73224a 100644 (file)
@@ -1416,6 +1416,23 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
         );
     }
 
+    $scope.selectedHoldingsPrintLabels = function() {
+        egCore.net.request(
+            'open-ils.actor',
+            'open-ils.actor.anon_cache.set_value',
+            null, 'print-labels-these-copies', {
+                copies : gatherSelectedHoldingsIds()
+            }
+        ).then(function(key) {
+            if (key) {
+                var url = egCore.env.basePath + 'cat/printlabels/' + key;
+                $timeout(function() { $window.open(url, '_blank') });
+            } else {
+                alert('Could not create anonymous cache key!');
+            }
+        });
+    }
+
     $scope.selectedHoldingsDamaged = function () {
         egCirc.mark_damaged(gatherSelectedHoldingsIds()).then(function() {
             holdingsSvcInst.fetchAgain().then(function() {
index 002061e..865a842 100644 (file)
@@ -1285,6 +1285,23 @@ function($scope , $q , $routeParams , $location , $timeout , $window , egCore ,
         itemSvc.transferItems(copyGrid.selectedItems());
     }
 
+    $scope.print_labels = function() {
+        egCore.net.request(
+            'open-ils.actor',
+            'open-ils.actor.anon_cache.set_value',
+            null, 'print-labels-these-copies', {
+                copies : gatherSelectedHoldingsIds()
+            }
+        ).then(function(key) {
+            if (key) {
+                var url = egCore.env.basePath + 'cat/printlabels/' + key;
+                $timeout(function() { $window.open(url, '_blank') });
+            } else {
+                alert('Could not create anonymous cache key!');
+            }
+        });
+    }
+
     $scope.print_list = function() {
         var print_data = { copies : copyGrid.allItems() };
 
diff --git a/Open-ILS/web/js/ui/default/staff/cat/printlabels/app.js b/Open-ILS/web/js/ui/default/staff/cat/printlabels/app.js
new file mode 100644 (file)
index 0000000..1fd5296
--- /dev/null
@@ -0,0 +1,633 @@
+/**
+ * Vol/Copy Editor
+ */
+
+angular.module('egPrintLabels',
+    ['ngRoute', 'ui.bootstrap', 'egCoreMod', 'egUiMod', 'egGridMod'])
+
+.config(function($routeProvider, $locationProvider, $compileProvider) {
+    $locationProvider.html5Mode(true);
+    $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|blob):/); // grid export
+
+    var resolver = {
+        delay : ['egStartup', function(egStartup) { return egStartup.go(); }]
+    };
+
+    $routeProvider.when('/cat/printlabels/:dataKey', {
+        templateUrl: './cat/printlabels/t_view',
+        controller: 'LabelCtrl',
+        resolve : resolver
+    });
+
+})
+
+.factory('itemSvc', 
+       ['egCore',
+function(egCore) {
+
+    var service = {
+        copies : [], // copy barcode search results
+        index : 0 // search grid index
+    };
+
+    service.flesh = {   
+        flesh : 3, 
+        flesh_fields : {
+            acp : ['call_number','location','status','location','floating','circ_modifier','age_protect'],
+            acn : ['record','prefix','suffix'],
+            bre : ['simple_record','creator','editor']
+        },
+        select : { 
+            // avoid fleshing MARC on the bre
+            // note: don't add simple_record.. not sure why
+            bre : ['id','tcn_value','creator','editor'],
+        } 
+    }
+
+    // resolved with the last received copy
+    service.fetch = function(barcode, id, noListDupes) {
+        var promise;
+
+        if (barcode) {
+            promise = egCore.pcrud.search('acp', 
+                {barcode : barcode, deleted : 'f'}, service.flesh);
+        } else {
+            promise = egCore.pcrud.retrieve('acp', id, service.flesh);
+        }
+
+        var lastRes;
+        return promise.then(
+            function() {return lastRes},
+            null, // error
+
+            // notify reads the stream of copies, one at a time.
+            function(copy) {
+
+                var flatCopy;
+                if (noListDupes) {
+                    // use the existing copy if possible
+                    flatCopy = service.copies.filter(
+                        function(c) {return c.id == copy.id()})[0];
+                }
+
+                if (!flatCopy) {
+                    flatCopy = egCore.idl.toHash(copy, true);
+                    flatCopy.index = service.index++;
+                    service.copies.unshift(flatCopy);
+                }
+
+                return lastRes = {
+                    copy : copy, 
+                    index : flatCopy.index
+                }
+            }
+        );
+    }
+
+    return service;
+}])
+
+/**
+ * Label controller!
+ */
+.controller('LabelCtrl', 
+       ['$scope','$q','$window','$routeParams','$location','$timeout','egCore','egNet','ngToast','itemSvc',
+function($scope , $q , $window , $routeParams , $location , $timeout , egCore , egNet , ngToast , itemSvc ) {
+
+    var dataKey = $routeParams.dataKey;
+    console.debug('dataKey: ' + dataKey);
+
+    $scope.print = {
+        template_name : 'item_label',
+        template_output : '',
+        template_context : 'default'
+    };
+
+
+    if (dataKey && dataKey.length > 0) {
+
+        egNet.request(
+            'open-ils.actor',
+            'open-ils.actor.anon_cache.get_value',
+            dataKey, 'print-labels-these-copies'
+        ).then(function (data) {
+
+            if (data) {
+
+                $scope.preview_scope = {
+                     'copies' : []
+                    ,'settings' : {}
+                    ,'get_cn_for' : function(copy) {
+                        var key = $scope.rendered_cn_key_by_copy_id[copy.id];
+                        if (key) {
+                            var manual_cn = $scope.rendered_call_number_set[key];
+                            if (manual_cn && manual_cn.value) {
+                                return manual_cn.value;
+                            } else {
+                                return '..';
+                            }
+                        } else {
+                            return '...';
+                        }
+                    }
+                    ,'get_bib_for' : function(copy) {
+                        return $scope.record_details[copy['call_number.record.id']];
+                    }
+                    ,'get_cn_prefix' : function(copy) {
+                        return copy['call_number.prefix.label'];
+                    }
+                    ,'get_cn_suffix' : function(copy) {
+                        return copy['call_number.suffix.label'];
+                    }
+                    ,'get_location_prefix' : function(copy) {
+                        return copy['location.label_prefix'];
+                    }
+                    ,'get_location_suffix' : function(copy) {
+                        return copy['location.label_suffix'];
+                    }
+                    ,'get_cn_and_location_prefix' : function(copy,separator) {
+                        var acpl_prefix = copy['location.label_prefix'] || '';
+                        var cn_prefix = copy['call_number.prefix.label'] || '';
+                        var prefix = acpl_prefix + ' ' + cn_prefix;
+                        prefix = prefix.trim();
+                        if (separator && prefix != '') { prefix += separator; }
+                        return prefix;
+                    }
+                    ,'get_cn_and_location_suffix' : function(copy,separator) {
+                        var acpl_suffix = copy['location.label_suffix'] || '';
+                        var cn_suffix = copy['call_number.suffix.label'] || '';
+                        var suffix = cn_suffix + ' ' + acpl_suffix;
+                        suffix = suffix.trim();
+                        if (separator && suffix != '') { suffix = separator + suffix; }
+                        return suffix;
+                    }
+                };
+                $scope.record_details = {};
+                $scope.org_unit_settings = {};
+
+                var promises = [];
+                $scope.org_unit_setting_list = [
+                     'webstaff.cat.label.font.family'
+                    ,'webstaff.cat.label.font.size'
+                    ,'webstaff.cat.label.font.weight'
+                    ,'webstaff.cat.label.inline_css'
+                    ,'webstaff.cat.label.left_label.height'
+                    ,'webstaff.cat.label.left_label.left_margin'
+                    ,'webstaff.cat.label.left_label.width'
+                    ,'webstaff.cat.label.right_label.height'
+                    ,'webstaff.cat.label.right_label.left_margin'
+                    ,'webstaff.cat.label.right_label.width'
+                    ,'webstaff.cat.label.call_number_wrap_filter_height'
+                    ,'webstaff.cat.label.call_number_wrap_filter_width'
+                ];
+
+                promises.push(
+                    egCore.pcrud.search('coust',{name:$scope.org_unit_setting_list}).then(
+                         null
+                        ,null
+                        ,function(yaous) {
+                            $scope.org_unit_settings[yaous.name()] = egCore.idl.toHash(yaous, true);
+                        }
+                    )
+                );
+
+                promises.push(
+                    egCore.org.settings($scope.org_unit_setting_list).then(function(res) {
+                        $scope.preview_scope.settings = res;
+                        egCore.hatch.getItem('cat.printlabels.last_settings').then(function(last_settings) {
+                            if (last_settings) {
+                                for (s in last_settings) {
+                                    $scope.preview_scope.settings[s] = last_settings[s];
+                                }
+                            }
+                        });
+                    })
+                );
+
+                angular.forEach(data.copies, function(copy) {
+                    promises.push(
+                        itemSvc.fetch(null,copy).then(function(res) {
+                            var flat_copy = egCore.idl.toHash(res.copy, true);
+                            $scope.preview_scope.copies.push(flat_copy);
+                            $scope.record_details[ flat_copy['call_number.record.id'] ] = 1;
+                        })
+                    )
+                });
+
+                $q.all(promises).then(function() {
+
+                    var promises2 = [];
+                    angular.forEach($scope.record_details, function(el,k,obj) {
+                        promises2.push(
+                            egNet.request(
+                                'open-ils.search',
+                                'open-ils.search.biblio.record.mods_slim.retrieve.authoritative',
+                                k
+                            ).then(function (data) {
+                                obj[k] = egCore.idl.toHash(data, true);
+                            })
+                        );
+                    });
+
+                    $q.all(promises2).then(function() {
+                        // today, staff, current_location, etc.
+                        egCore.print.fleshPrintScope($scope.preview_scope);
+                        $scope.template_changed(); // load the default
+                        $scope.rebuild_cn_set();
+                    });
+
+                });
+            } else {
+                ngToast.danger(egCore.strings.KEY_EXPIRED);
+            }
+
+        });
+
+    }
+
+    $scope.fetchTemplates = function () {
+        egCore.hatch.getItem('cat.printlabels.templates').then(function(t) {
+            if (t) {
+                $scope.templates = t;
+                $scope.template_name_list = Object.keys(t);
+            }
+        });
+    }
+    $scope.fetchTemplates();
+
+    $scope.applyTemplate = function (n) {
+        $scope.print.cn_template_content = $scope.templates[n].cn_content;
+        $scope.print.template_content = $scope.templates[n].content;
+        $scope.print.template_context = $scope.templates[n].context;
+        for (var s in $scope.templates[n].settings) {
+            $scope.preview_scope.settings[s] = $scope.templates[n].settings[s];
+        }
+    }
+
+    $scope.deleteTemplate = function (n) {
+        if (n) {
+            delete $scope.templates[n]
+            $scope.template_name_list = Object.keys($scope.templates);
+            $scope.template_name = '';
+            egCore.hatch.setItem('cat.printlabels.templates', $scope.templates);
+            $scope.fetchTemplates();
+            ngToast.create(egCore.strings.PRINT_LABEL_TEMPLATE_SUCCESS_DELETE);
+        }
+    }
+
+    $scope.saveTemplate = function (n) {
+        if (n) {
+
+            $scope.templates[n] = {
+                 content : $scope.print.template_content
+                ,context : $scope.print.template_context
+                ,cn_content : $scope.print.cn_template_content
+                ,settings : $scope.preview_scope.settings
+            };
+            $scope.template_name_list = Object.keys($scope.templates);
+
+            egCore.hatch.setItem('cat.printlabels.templates', $scope.templates);
+            $scope.fetchTemplates();
+
+            $scope.dirty = false;
+        } else {
+            // save all templates, as we might do after an import
+            egCore.hatch.setItem('cat.printlabels.templates', $scope.templates);
+            $scope.fetchTemplates();
+        }
+        ngToast.create(egCore.strings.PRINT_LABEL_TEMPLATE_SUCCESS_SAVE);
+    }
+
+    $scope.templates = {};
+    $scope.imported_templates = { data : '' };
+    $scope.template_name = '';
+    $scope.template_name_list = [];
+
+    $scope.print_labels = function() {
+        $scope.save_locally();
+        return egCore.print.print({
+            context : $scope.print.template_context,
+            template : $scope.print.template_name,
+            scope : $scope.preview_scope,
+        });
+    }
+
+    $scope.template_changed = function() {
+        $scope.print.load_failed = false;
+        egCore.print.getPrintTemplate('item_label')
+        .then(
+            function(html) { 
+                $scope.print.template_content = html;
+            },
+            function() {
+                $scope.print.template_content = '';
+                $scope.print.load_failed = true;
+            }
+        );
+        egCore.print.getPrintTemplateContext('item_label')
+        .then(function(template_context) {
+            $scope.print.template_context = template_context;
+        });
+        egCore.print.getPrintTemplate('item_label_cn')
+        .then(
+            function(html) {
+                $scope.print.cn_template_content = html;
+            },
+            function() {
+                $scope.print.cn_template_content = '';
+                $scope.print.load_failed = true;
+            }
+        );
+        egCore.hatch.getItem('cat.printlabels.last_settings').then(function(s) {
+            if (s) {
+                $scope.preview_scope.settings = s;
+            }
+        });
+
+    }
+
+    $scope.reset_to_default = function() {
+        egCore.print.removePrintTemplate(
+            'item_label'
+        );
+        egCore.print.removePrintTemplateContext(
+            'item_label'
+        );
+        egCore.print.removePrintTemplate(
+            'item_label_cn'
+        );
+        egCore.hatch.removeItem('cat.printlabels.last_settings');
+        for (s in $scope.preview_scope.settings) {
+            $scope.preview_scope.settings[s] = undefined;
+        }
+        $scope.preview_scope.settings = {};
+        egCore.org.settings($scope.org_unit_setting_list).then(function(res) {
+            $scope.preview_scope.settings = res;
+        });
+
+        $scope.template_changed();
+    }
+
+    $scope.save_locally = function() {
+        egCore.print.storePrintTemplate(
+            'item_label',
+            $scope.print.template_content
+        );
+        egCore.print.storePrintTemplateContext(
+            'item_label',
+            $scope.print.template_context
+        );
+        egCore.print.storePrintTemplate(
+            'item_label_cn',
+            $scope.print.cn_template_content
+        );
+        egCore.hatch.setItem('cat.printlabels.last_settings', $scope.preview_scope.settings);
+    }
+
+    $scope.imported_print_templates = { data : '' };
+    $scope.$watch('imported_templates.data', function(newVal, oldVal) {
+        if (newVal && newVal != oldVal) {
+            try {
+                var data = JSON.parse(newVal);
+                angular.forEach(data, function(el,k) {
+                    $scope.templates[k] = {
+                         content : el.content
+                        ,context : el.context
+                        ,cn_content : el.cn_content
+                        ,settings : el.settings
+                    };
+                });
+                $scope.saveTemplate();
+                $scope.template_changed(); // refresh
+                ngToast.create(egCore.strings.PRINT_TEMPLATES_SUCCESS_IMPORT);
+            } catch (E) {
+                ngToast.warning(egCore.strings.PRINT_TEMPLATES_FAIL_IMPORT);
+            }
+        }
+    });
+
+    $scope.rendered_call_number_set = {};
+    $scope.rendered_cn_key_by_copy_id = {};
+    $scope.rebuild_cn_set = function() {
+        $timeout(function(){
+            $scope.rendered_call_number_set = {};
+            $scope.rendered_cn_key_by_copy_id = {};
+            for (var i = 0; i < $scope.preview_scope.copies.length; i++) {
+                var copy = $scope.preview_scope.copies[i];
+                var rendered_cn = document.getElementById('cn_for_copy_'+copy.id);
+                if (rendered_cn && rendered_cn.textContent) {
+                    var key = rendered_cn.textContent;
+                    if (typeof $scope.rendered_call_number_set[key] == 'undefined') {
+                        $scope.rendered_call_number_set[key] = {
+                            value : key
+                        };
+                    }
+                    $scope.rendered_cn_key_by_copy_id[copy.id] = key;
+                }
+            }
+            $scope.preview_scope.tickle = Date() + ' ' + Math.random();
+        });
+    }
+
+    $scope.$watch('print.cn_template_content', function(newVal, oldVal) {
+        if (newVal && newVal != oldVal) {
+            $scope.rebuild_cn_set();
+        }
+    });
+
+    $scope.$watch("preview_scope.settings['webstaff.cat.label.call_number_wrap_filter_height']", function(newVal, oldVal) {
+        if (newVal && newVal != oldVal) {
+            $scope.rebuild_cn_set();
+        }
+    });
+
+    $scope.$watch("preview_scope.settings['webstaff.cat.label.call_number_wrap_filter_width']", function(newVal, oldVal) {
+        if (newVal && newVal != oldVal) {
+            $scope.rebuild_cn_set();
+        }
+    });
+
+    $scope.current_tab = 'call_numbers';
+    $scope.set_tab = function(tab) {
+        $scope.current_tab = tab;
+    }
+
+}])
+
+// 
+.directive('egPrintTemplateOutput', ['$compile',function($compile) {
+    return function(scope, element, attrs) {
+        scope.$watch(
+            function(scope) {
+                return scope.$eval(attrs.content);
+            },
+            function(value) {
+                // create an isolate scope and copy the print context
+                // data into the new scope.
+                // TODO: see also print security concerns in egHatch
+                var result = element.html(value);
+                var context = scope.$eval(attrs.context);
+                var print_scope = scope.$new(true);
+                angular.forEach(context, function(val, key) {
+                    print_scope[key] = val;
+                })
+                $compile(element.contents())(print_scope);
+            }
+        );
+    };
+}])
+
+.filter('cn_wrap', function() {
+    return function(input, w, h, wrap_type) {
+        var names;
+        var prefix = input[0];
+        var callnum = input[1];
+        var suffix = input[2];
+
+        if (!w) { w = 8; }
+        if (!h) { h = 9; }
+
+        /* handle spine labels differently if using LC */
+        if (wrap_type == 'lc' || wrap_type == 3) {
+            /* Establish a pattern where every return value should be isolated on its own line 
+               on the spine label: subclass letters, subclass numbers, cutter numbers, trailing stuff (date) */
+            var patt1 = /^([A-Z]{1,3})\s*(\d+(?:\.\d+)?)\s*(\.[A-Z]\d*)\s*([A-Z]\d*)?\s*(\d\d\d\d(?:-\d\d\d\d)?)?\s*(.*)$/i;
+            var result = callnum.match(patt1);
+            if (result) { 
+                callnum = result.slice(1).join('\t');  
+            } else {
+                callnum = callnum.split(/\s+/).join('\t');
+            } 
+
+            /* If result is null, leave callnum alone. Can't parse this malformed call num */
+        } else {
+            callnum = callnum.split(/\s+/).join('\t');
+        }
+
+        if (prefix) {
+            callnum = prefix + '\t' + callnum;
+        }
+        if (suffix) {
+            callnum += '\t' + suffix;
+        }
+
+        /* At this point, the call number pieces are separated by tab characters.  This allows
+        *  some space-containing constructs like "v. 1" to appear on one line
+        */
+        callnum = callnum.replace(/\t\t/g,'\t');  /* Squeeze out empties */ 
+        names = callnum.split('\t');
+        var j = 0; var tb = [];
+        while (j < h) {
+            
+            /* spine */
+            if (j < w) {
+
+                var name = names.shift();
+                if (name) {
+                    name = String( name );
+
+                    /* if the name is greater than the label width... */
+                    if (name.length > w) {
+                        /* then try to split it on periods */
+                        var sname = name.split(/\./);
+                        if (sname.length > 1) {
+                            /* if we can, then put the periods back in on each splitted element */
+                            if (name.match(/^\./)) sname[0] = '.' + sname[0];
+                            for (var k = 1; k < sname.length; k++) sname[k] = '.' + sname[k];
+                            /* and put all but the first one back into the names array */
+                            names = sname.slice(1).concat( names );
+                            /* if the name fragment is still greater than the label width... */
+                            if (sname[0].length > w) {
+                                /* then just truncate and throw the rest back into the names array */
+                                tb[j] = sname[0].substr(0,w);
+                                names = [ sname[0].substr(w) ].concat( names );
+                            } else {
+                                /* otherwise we're set */
+                                tb[j] = sname[0];
+                            }
+                        } else {
+                            /* if we can't split on periods, then just truncate and throw the rest back into the names array */
+                            tb[j] = name.substr(0,w);
+                            names = [ name.substr(w) ].concat( names );
+                        }
+                    } else {
+                        /* otherwise we're set */
+                        tb[j] = name;
+                    }
+                }
+            }
+            j++;
+        }
+        return tb.join('\n');
+    }
+})
+
+.filter('wrap', function() {
+    return function(input, w, wrap_type, indent) {
+        var output;
+
+        if (!w) return input;
+        if (!indent) indent = '';
+
+        function wrap_on_space(
+                text,
+                length,
+                wrap_just_once,
+                if_cant_wrap_then_truncate,
+                idx
+        ) {
+            if (idx>10) {
+                console.log('possible infinite recursion, aborting');
+                return '';
+            }
+            if (String(text).length <= length) {
+                return text;
+            } else {
+                var truncated_text = String(text).substr(0,length);
+                var pivot_pos = truncated_text.lastIndexOf(' ');
+                var left_chunk = text.substr(0,pivot_pos).replace(/\s*$/,'');
+                var right_chunk = String(text).substr(pivot_pos+1);
+
+                var wrapped_line;
+                if (left_chunk.length == 0) {
+                    if (if_cant_wrap_then_truncate) {
+                        wrapped_line = truncated_text;
+                    } else {
+                        wrapped_line = text;
+                    }
+                } else {
+                    wrapped_line =
+                        left_chunk + '\n'
+                        + indent + (
+                            wrap_just_once
+                            ? right_chunk
+                            : (
+                                right_chunk.length > length
+                                ? wrap_on_space(
+                                    right_chunk,
+                                    length,
+                                    false,
+                                    if_cant_wrap_then_truncate,
+                                    idx+1)
+                                : right_chunk
+                            )
+                        )
+                    ;
+                }
+                return wrapped_line;
+            }
+        }
+
+        switch(wrap_type) {
+            case 'once':
+                output = wrap_on_space(input,w,true,false,0);
+            break;
+            default:
+                output = wrap_on_space(input,w,false,false,0);
+            break;
+        }
+
+        return output;
+    }
+})
+
index fce1e7a..8399f72 100644 (file)
@@ -1586,11 +1586,30 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
             egNet.request(
                 'open-ils.cat',
                 'open-ils.cat.asset.volume.fleshed.batch.update.override',
-                egCore.auth.token(), cnList, 1, { auto_merge_vols : 1, create_parts : 1 }
-            ).then(function(update_count) {
+                egCore.auth.token(), cnList, 1, { auto_merge_vols : 1, create_parts : 1, return_copy_ids : 1 }
+            ).then(function(copy_ids) {
                 if (and_exit) {
                     $scope.dirty = false;
-                    $timeout(function(){$window.close()});
+                    if ($scope.defaults.print_item_labels) {
+                        egCore.net.request(
+                            'open-ils.actor',
+                            'open-ils.actor.anon_cache.set_value',
+                            null, 'print-labels-these-copies', {
+                                copies : copy_ids
+                            }
+                        ).then(function(key) {
+                            if (key) {
+                                var url = egCore.env.basePath + 'cat/printlabels/' + key;
+                                $timeout(function() { $window.open(url, '_blank') }).then(
+                                    function() { $timeout(function(){$window.close()}); }
+                                );
+                            } else {
+                                alert('Could not create anonymous cache key!');
+                            }
+                        });
+                    } else {
+                        $timeout(function(){$window.close()});
+                    }
                 }
             });
         }
index de6e766..03ea6b8 100644 (file)
@@ -235,6 +235,10 @@ function($q , $window , $timeout , $http , egHatch , egAuth , egIDL , egOrg , eg
         return egHatch.setItem('eg.print.template.' + name, html);
     }
 
+    service.removePrintTemplate = function(name) {
+        return egHatch.removeItem('eg.print.template.' + name);
+    }
+
     service.getPrintTemplateContext = function(name) {
         var deferred = $q.defer();
 
@@ -249,6 +253,9 @@ function($q , $window , $timeout , $http , egHatch , egAuth , egIDL , egOrg , eg
     service.storePrintTemplateContext = function(name, context) {
         return egHatch.setItem('eg.print.template_context.' + name, context);
     }
+    service.removePrintTemplateContext = function(name) {
+        return egHatch.removeItem('eg.print.template_context.' + name);
+    }
 
     return service;
 }])