[open-ils-commits] [GIT] Evergreen ILS branch master updated. 986a2d011726f254e6a73f06382a76d91ce88018

Evergreen Git git at git.evergreen-ils.org
Wed Feb 21 21:04:55 EST 2018


This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "Evergreen ILS".

The branch, master has been updated
       via  986a2d011726f254e6a73f06382a76d91ce88018 (commit)
       via  6dbb6c76aa5277c7acc397ca7d02451465e86996 (commit)
       via  04bf77f7feace19817dc7c1e0efa75c6112558a8 (commit)
       via  7e76706a0b34f87797f09b9b3218bce476c067dc (commit)
       via  cac2694533baae42cf5bad1434287ca69c72116b (commit)
       via  89eb96b8eaae3f0074c1bf69c1408a2dc9fa9073 (commit)
       via  a61f5bb7dc7606c758cc2a91cb46db7fd654a720 (commit)
       via  1438791250621e5f089b2b1ed9292862d6c4fbcd (commit)
       via  e70d5917f50e195b2f3b456c9d420a8c06e94823 (commit)
      from  0a81e0d2bf2b107a46307e7ab9164f54355bff66 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit 986a2d011726f254e6a73f06382a76d91ce88018
Author: Kathy Lussier <klussier at masslnc.org>
Date:   Wed Feb 21 21:01:22 2018 -0500

    LP#1694058: Stamping upgrade script for duplicate holds coust
    
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/src/sql/Pg/002.schema.config.sql b/Open-ILS/src/sql/Pg/002.schema.config.sql
index 04bc915..1d074f1 100644
--- a/Open-ILS/src/sql/Pg/002.schema.config.sql
+++ b/Open-ILS/src/sql/Pg/002.schema.config.sql
@@ -92,7 +92,7 @@ CREATE TRIGGER no_overlapping_deps
     BEFORE INSERT OR UPDATE ON config.db_patch_dependencies
     FOR EACH ROW EXECUTE PROCEDURE evergreen.array_overlap_check ('deprecates');
 
-INSERT INTO config.upgrade_log (version, applied_to) VALUES ('1088', :eg_version); -- miker/dyrcona
+INSERT INTO config.upgrade_log (version, applied_to) VALUES ('1089', :eg_version); -- dyrcona/kmlussier
 
 CREATE TABLE config.bib_source (
 	id		SERIAL	PRIMARY KEY,
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.data.max_duplicate_holds_coust.sql b/Open-ILS/src/sql/Pg/upgrade/1089.data.max_duplicate_holds_coust.sql
similarity index 86%
rename from Open-ILS/src/sql/Pg/upgrade/XXXX.data.max_duplicate_holds_coust.sql
rename to Open-ILS/src/sql/Pg/upgrade/1089.data.max_duplicate_holds_coust.sql
index 93e4a4f..c4bf1a7 100644
--- a/Open-ILS/src/sql/Pg/upgrade/XXXX.data.max_duplicate_holds_coust.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/1089.data.max_duplicate_holds_coust.sql
@@ -1,6 +1,6 @@
 BEGIN;
 
--- INSERT INTO config.upgrade_log (version, applied_to) VALUES ('XXXX', :eg_version);
+SELECT evergreen.upgrade_deps_block_check('1089', :eg_version);
 
 -- Add the circ.holds.max_duplicate_holds org. unit setting type.
 INSERT into config.org_unit_setting_type

commit 6dbb6c76aa5277c7acc397ca7d02451465e86996
Author: Kathy Lussier <klussier at masslnc.org>
Date:   Wed Feb 21 20:55:31 2018 -0500

    LP# 1694058: Release notes entry for placing multiple holds
    
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/docs/RELEASE_NOTES_NEXT/Circulation/multiple_hold_placement.adoc b/docs/RELEASE_NOTES_NEXT/Circulation/multiple_hold_placement.adoc
new file mode 100644
index 0000000..49cedbe
--- /dev/null
+++ b/docs/RELEASE_NOTES_NEXT/Circulation/multiple_hold_placement.adoc
@@ -0,0 +1,20 @@
+Place Multiple Holds At Once
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Users with the appropriate permissions now have the ability to place multiple
+title/metarecords holds at once. This feature is especially beneficial for book
+clubs and reading groups, which need to place holds on multiple copies of a 
+title.
+
+In order to use the feature:
+  * Set the _Maximum number of duplicate holds allowed_ Library Setting (
+circ.holds.max_duplicate_holds) to a number higher than 1
+  * Log in as a user with the CREATE_DUPLICATE_HOLDS
+
+When placing a title or metarecord hold, a _Number of copies_ field will 
+display for these users. This field is not available when placing part, volume
+or copy holds. 
+
+This feature does not change the way in which the system fills holds. The
+multiple holds will fill in the same way that they would if the user had placed
+multiple holds separately.
+

commit 04bf77f7feace19817dc7c1e0efa75c6112558a8
Author: Jason Stephenson <jason at sigio.com>
Date:   Sun Oct 29 15:14:41 2017 -0400

    Lp 1694058: Fix Issue With Place Holds Reported in Testing
    
    When staff did have to override in order to place the hold, and the
    "Place another hold for this title" link was subsequnetly used, the
    inputs for the patron barcodes and some other fields on the place
    holds page were duplicated.  To avoid this, we now use uniq from the
    List::MoreUtils library when retrieving the hold targets list from the
    CGI parameters.
    
    Signed-off-by: Jason Stephenson <jason at sigio.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm
index 6095f85..b630517 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm
@@ -14,6 +14,7 @@ $Data::Dumper::Indent = 0;
 use DateTime;
 use DateTime::Format::ISO8601;
 my $U = 'OpenILS::Application::AppUtils';
+use List::MoreUtils qw/uniq/;
 
 sub prepare_extended_user_info {
     my $self = shift;
@@ -984,7 +985,7 @@ sub load_place_hold {
     my $cgi = $self->cgi;
 
     $self->ctx->{page} = 'place_hold';
-    my @targets = $cgi->param('hold_target');
+    my @targets = uniq $cgi->param('hold_target');
     my @parts = $cgi->param('part');
 
     $ctx->{hold_type} = $cgi->param('hold_type');

commit 7e76706a0b34f87797f09b9b3218bce476c067dc
Author: Jason Stephenson <jason at sigio.com>
Date:   Sun Oct 1 12:03:26 2017 -0400

    LP 1694058: Final OPAC Changes for Multiple Hold Placement
    
    Add JavaScript code to disable the number of copies selection when a
    part is chosen for a record that has both parts and non parts holds
    available.  If a part is chosen, the number of copies select should be
    disabled and reset to 1.  It should be enabled again if the All Parts
    option is chosen.  It does not appear if the hold requires a part.
    
    Test Plan for the OPAC changes:
    
    1. Set circ.holds.max_duplicate_holds to 5 for BR1.
    
    2. Login to the OPAC as any patron from BR1.
    
    3. Perform any search.
    
    4. Select a title and click Place Hold.
    
    5. See that the selection box for placing multiple holds does not appear on the place holds screen.
    
    6. Place the hold to see that hold placement still works as expected.
    
    8. Go to the patron’s list of holds in My Opac to see that the hold appears.
    
    9. Cancel the hold so it doesn’t interfere with later testing.
    
    10. Logout.
    
    11. Login to the OPAC as any patron from BR2 or any branch other than BR1.
    
    12. Perform any search.
    
    13. Select a title and click Place Hold.
    
    14. See that the selection box for placing multiple holds does not appear on the place holds screen.
    
    15. Place the hold to see that hold placement still works as expected.
    
    16. Go to the patron’s list of holds in My Opac to see that the hold appears.
    
    17. Cancel the hold so it doesn’t interfere with later testing.
    
    18. Logout.
    
    19. Login to the web staff client as a circulator for BR1 at BR1.
    
    20. Select a patron barcode for any BR1 patron.
    
    21. Perform any search.
    
    22. Choose a title without parts and click place holds.
    
    23. See that the selection box for placing multiple holds appears with 1 as the default.
    
    24. Click the selection box to see that the values go from 1 to 5.
    
    25. Place any number of holds for the patron whose barcode was chosen in step 15.
    
    26. Open the patron in the staff client and check the patron’s holds list to see that they have the correct number of holds for this title listed.
    
    27. Repeat steps 21 – 26 with metarecord holds on one of the metarecord titles.
    
    28. Perform a search and select a title.
    
    29. Choose an item to place a copy hold for the same patron.
    
    30. Notice that the multiple hold number selection box does not appear.
    
    31. Place the hold as normal (if you like) to test that copy hold placement still works.
    
    32. Search for a title with parts.
    
    33. Select a part in the parts drop down or radio button.
    
    34. Notice that the number of copies select box is reset to 1 and disabled when a part is selected.
    
    35. Repeat the above steps for a BR2 circulation account logged in at BR2 (or any other branch staff).
    
    36. Notice that the selection box never appears when placing holds where the org. unit setting does not apply.
    
    37. Clear the org. unit setting for BR1 and repeat the above steps, if desired.
    
    The web staff client tests should work in the XUL staff client as well.
    
    Signed-off-by: Jason Stephenson <jason at sigio.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/src/templates/opac/parts/place_hold.tt2 b/Open-ILS/src/templates/opac/parts/place_hold.tt2
index b40c34c..099208c 100644
--- a/Open-ILS/src/templates/opac/parts/place_hold.tt2
+++ b/Open-ILS/src/templates/opac/parts/place_hold.tt2
@@ -24,6 +24,28 @@ function toggleActivationDate() {
     // Prevent the href from being followed, thus overriding the CSS.
     return false;
 }
+
+// Maybe enable or disable the num_copies select when the user selects
+// or deselects a part.
+function maybeToggleNumCopies(obj) {
+    var numCopies = document.getElementById("num_copies");
+    // Only if numCopies exists.
+    if (numCopies) {
+        var objValue;
+        if (obj.type == 'radio') {
+            if (obj.checked) objValue = obj.value;
+            else return;
+        } else {
+            objValue = obj.value;
+        }
+        if (objValue && objValue != '') {
+            if (numCopies.value != '1') numCopies.value = '1';
+            if (!numCopies.disabled) numCopies.disabled = true;
+        } else {
+            if (numCopies.disabled) numCopies.disabled = false;
+        }
+    }
+}
 </script>
 <div id='holds_box' class='canvas' style='margin-top: 6px;'>
     <h1>[% l('Place Hold') %]</h1>
@@ -132,10 +154,10 @@ function toggleActivationDate() {
                              <div class='radio-parts-selection'>
                              [% IF !hdata.part_required %]
                                 <span class='parts-radio-option'>
-                                 <input type='radio' name='part' value='' required>[% l('All Parts') %]</span>
+                                 <input type='radio' name='part' value='' onchange='maybeToggleNumCopies(this);' required>[% l('All Parts') %]</span>
                               [% END %]
                                [% FOR part IN hdata.parts %]
-                                 <span class='parts-radio-option'><input type='radio' name='part' id=[% part.id %] value=[% part.id %] required>
+                                 <span class='parts-radio-option'><input type='radio' name='part' id=[% part.id %] value=[% part.id %] onchange='maybeToggleNumCopies(this);' required>
                                   <label for=[% part.id %]>[% part.label | html %]</label></span>
                               [% END %]
                               </div>
@@ -143,7 +165,7 @@ function toggleActivationDate() {
                             <span style='font-weight: bold;'><label for='select_hold_part'>[%
                                 hdata.part_required ? l('Select a Part:') : l('Select a Part (optional):')
                             %]</label></span>
-                            <select id='select_hold_part' name='part'>
+                            <select id='select_hold_part' name='part' onchange='maybeToggleNumCopies(this);'>
                                 [% IF !hdata.part_required %]
                                 <option selected='selected' value=''>[% l('- All Parts -') %]</option>
                                 [% END %]

commit cac2694533baae42cf5bad1434287ca69c72116b
Author: Jason Stephenson <jason at sigio.com>
Date:   Sat Sep 30 15:30:22 2017 -0400

    LP 1694058: Add confirmation dialog for multiple title holds.
    
    Add a dialog to confirm that the user really wants to place the
    requested number of title or metarecord holds to the
    validateHoldForm() function.
    
    Along the way, we add a format() function to the JS String prototype
    in the i18n_strings.tt2 so that we can have translated strings with
    placeholders in JavaScript.
    
    Signed-off-by: Jason Stephenson <jason at sigio.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/src/templates/opac/i18n_strings.tt2 b/Open-ILS/src/templates/opac/i18n_strings.tt2
index d92c2b5..1617e29 100644
--- a/Open-ILS/src/templates/opac/i18n_strings.tt2
+++ b/Open-ILS/src/templates/opac/i18n_strings.tt2
@@ -3,7 +3,21 @@ This file allows us to bring TT2 i18n'ized strings
 to js source files, via js blob.
 -->
 <script>
+    // Add a boost-style format function to JavaScript string.
+    // Implementation stolen from StackOverflow:
+    // https://stackoverflow.com/questions/610406/javascript-equivalent-to-printf-string-format
+    String.prototype.format = function() {
+        var args = arguments;
+        return this.replace(/{(\d+)}/g, function(match, number) {
+        return typeof args[number] != 'undefined'
+            ? args[number]
+            : match;
+        });
+    };
+
     var eg_opac_i18n = {};
 
     eg_opac_i18n.EG_MISSING_REQUIRED_INPUT = "[% l('Please fill out all required fields') %]";
+    // For multiple holds placement confirmation dialog. {0} is replaced by number of copies requested.
+    eg_opac_i18n.EG_MULTIHOLD_MESSAGE = "[% l('Do you really want to place {0} holds for this title?') %]";
 </script>
diff --git a/Open-ILS/web/js/ui/default/opac/holds-validation.js b/Open-ILS/web/js/ui/default/opac/holds-validation.js
index ef3c4cd..7345e3f 100644
--- a/Open-ILS/web/js/ui/default/opac/holds-validation.js
+++ b/Open-ILS/web/js/ui/default/opac/holds-validation.js
@@ -60,11 +60,22 @@ function validateMethodSelections (alertMethodCboxes) {
     return { isValid: isFormOK, culpritNames : culprits };
 }
 
+function confirmMultipleHolds() {
+    var result = true;
+    var numSelect = document.getElementById("num_copies");
+    if (numSelect) {
+        var num = parseInt(numSelect.value);
+        if (num > 1) {
+            result = window.confirm(eg_opac_i18n.EG_MULTIHOLD_MESSAGE.format(num));
+        }
+    }
+    return result;
+}
+
 function validateHoldForm() {
     var res = validateMethodSelections(document.getElementsByClassName("hold-alert-method"));
-    if (res.isValid)
-    {
-        return true;
+    if (res.isValid) {
+        return confirmMultipleHolds();
     } else {
         alert(eg_opac_i18n.EG_MISSING_REQUIRED_INPUT);
         res.culpritNames.forEach(function(n){

commit 89eb96b8eaae3f0074c1bf69c1408a2dc9fa9073
Author: Jason Stephenson <jason at sigio.com>
Date:   Sat Sep 23 14:45:41 2017 -0400

    LP 1694058: OPAC changes for multiple title and metarecord holds.
    
    We add the num_copies select box to the place_holds.tt2 whe appropriate.
    
    The new select list generator has its own tt2 file:
    Open-ILS/src/templates/opac/parts/multi_hold_select.tt2
    
    Modify WWW/EGCatLoader/Account.pm to properly process multiple hold
    requests for the same title or metarecord.
    
    Signed-off-by: Jason Stephenson <jason at sigio.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm
index 8295e12..6095f85 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm
@@ -1000,6 +1000,23 @@ sub load_place_hold {
 
     return $self->generic_redirect unless @targets;
 
+    # Check for multiple hold placement via the num_copies widget.
+    my $num_copies = int($cgi->param('num_copies')); # if undefined, we get 0.
+    if ($num_copies > 1) {
+        # Only if we have 1 hold target and no parts.
+        if (scalar(@targets) == 1 && !$parts[0]) {
+            # Also, only for M and T holds.
+            if ($ctx->{hold_type} eq 'M' || $ctx->{hold_type} eq 'T') {
+                # Add the extra holds to @targets. NOTE: We start with
+                # 1 and go to < $num_copies to account for the
+                # existing target.
+                for (my $i = 1; $i < $num_copies; $i++) {
+                    push(@targets, $targets[0]);
+                }
+            }
+        }
+    }
+
     $logger->info("Looking at hold_type: " . $ctx->{hold_type} . " and targets: @targets");
 
     $ctx->{staff_recipient} = $self->editor->retrieve_actor_user([
@@ -1241,15 +1258,23 @@ sub load_place_hold {
         # like a real P-type hold.
         my (@p_holds, @t_holds);
 
-        for my $idx (0..$#parts) {
-            my $hdata = $hold_data[$idx];
-            if (my $part = $parts[$idx]) {
-                $hdata->{target_id} = $part;
-                $hdata->{selected_part} = $part;
-                push(@p_holds, $hdata);
-            } else {
-                push(@t_holds, $hdata);
+        # Now that we have the num_copies field for mutliple title and
+        # metarecord hold placement, the number of holds and parts
+        # arrays can get out of sync.  We only want to parse out parts
+        # if the numbers are equal.
+        if ($#hold_data == $#parts) {
+            for my $idx (0..$#parts) {
+                my $hdata = $hold_data[$idx];
+                if (my $part = $parts[$idx]) {
+                    $hdata->{target_id} = $part;
+                    $hdata->{selected_part} = $part;
+                    push(@p_holds, $hdata);
+                } else {
+                    push(@t_holds, $hdata);
+                }
             }
+        } else {
+            @t_holds = @hold_data;
         }
 
         $self->apache->log->warn("$#parts : @t_holds");
@@ -1342,7 +1367,8 @@ sub attempt_hold_placement {
                 last;
             }
 
-            my ($hdata) = grep {$_->{target_id} eq $resp->{target}} @hold_data;
+            # Skip those that had the hold_success or hold_failed fields set for duplicate holds placement.
+            my ($hdata) = grep {$_->{target_id} eq $resp->{target} && !($_->{hold_failed} || $_->{hold_success})} @hold_data;
             my $result = $resp->{result};
 
             if ($U->event_code($result)) {
diff --git a/Open-ILS/src/templates/opac/parts/multi_hold_select.tt2 b/Open-ILS/src/templates/opac/parts/multi_hold_select.tt2
new file mode 100644
index 0000000..020245b
--- /dev/null
+++ b/Open-ILS/src/templates/opac/parts/multi_hold_select.tt2
@@ -0,0 +1,15 @@
+[%  # Check if we need to do anything.
+    hold_type = CGI.param('hold_type');
+    max_holds = ctx.get_org_setting(ctx.default_pickup_lib, 'circ.holds.max_duplicate_holds');
+    can_dup = ctx.has_perm('CREATE_DUPLICATE_HOLDS', ctx.default_pickup_lib);
+    IF ctx.hold_data.size == 1 && (hold_type == 'M' || hold_type == 'T') && max_holds && max_holds > 1 && can_dup;
+%]
+<p>
+<label for="num_copies">[% l('Number of copies') %]</label>
+<select id="num_copies" name="num_copies" title="[% l('Number of copies') %]">
+[% FOR num IN [1..max_holds] %]
+<option value="[% num %]">[% num %]</option>
+[% END %]
+</select>
+</p>
+[% END %]
diff --git a/Open-ILS/src/templates/opac/parts/place_hold.tt2 b/Open-ILS/src/templates/opac/parts/place_hold.tt2
index 76d8d9e..b40c34c 100644
--- a/Open-ILS/src/templates/opac/parts/place_hold.tt2
+++ b/Open-ILS/src/templates/opac/parts/place_hold.tt2
@@ -157,6 +157,7 @@ function toggleActivationDate() {
                         <input type='hidden' name='part' value=''/>
                         [% END %]
                     [% END %]
+		    [% INCLUDE "opac/parts/multi_hold_select.tt2" IF NOT (this_hold_disallowed AND hdata.part_required); %]
                     [% IF NOT metarecords.disabled %]
                         [% IF CGI.param('hold_type') == 'T' AND hdata.record.metarecord AND !hdata.part_required %]
                         <!-- Grab the bre_id so that we can restore it if user accidentally clicks advanced options -->

commit a61f5bb7dc7606c758cc2a91cb46db7fd654a720
Author: Jason Stephenson <jason at sigio.com>
Date:   Sun Aug 13 09:42:34 2017 -0400

    LP 1694058: Perl tests for backend multiple hold placement changes.
    
    Signed-off-by: Jason Stephenson <jason at sigio.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/src/perlmods/live_t/25-lp1694058-multiple-hold-placement.t b/Open-ILS/src/perlmods/live_t/25-lp1694058-multiple-hold-placement.t
new file mode 100644
index 0000000..942aab2
--- /dev/null
+++ b/Open-ILS/src/perlmods/live_t/25-lp1694058-multiple-hold-placement.t
@@ -0,0 +1,545 @@
+#!perl
+use strict; use warnings;
+use Test::More tests => 30;
+use OpenILS::Utils::TestUtils;
+use OpenILS::Const qw(:const);
+
+diag("Test LP 1694058 multiple hold placement.");
+
+my $script = OpenILS::Utils::TestUtils->new();
+my $U = 'OpenILS::Application::AppUtils';
+
+use constant {
+    BR1_WORKSTATION => 'BR1-test-lp1694058-multiple-hold-placement.t',
+    BR1_ID => 4,
+    BR2_ID => 5,
+    PATRON1_BARCODE => '99999376864',
+    PATRON2_BARCODE => '99999342948',
+    RECORD_ID => 3,
+    METARECORD_ID => 13,
+    COPY_ID => 2503,
+};
+
+# Because this may run multiple times, without a DB reload, we search
+# for the workstation before registering it.  Takes an authtoken, the
+# id of the workstation lib, and the name of the workstation.
+sub find_or_register_workstation {
+    my ($authtoken, $lib, $workstation) = @_;
+    my $ws;
+    my $r = $U->simplereq(
+        'open-ils.actor',
+        'open-ils.actor.workstation.list',
+        $authtoken,
+        $lib
+    );
+    if ($r && $r->{$lib}) {
+        $ws = grep {$_->name() eq $workstation} @{$r->{$lib}};
+    }
+    unless ($ws) {
+        $ws = $script->register_workstation($workstation, $lib);
+    }
+    return $ws;
+}
+
+
+# Keep track of hold ids, so we can cancel them later.
+my @holds = ();
+
+# Login as admin at BR1.
+my $authtoken = $script->authenticate({
+    username=>'admin',
+    password=>'demo123',
+    type=>'staff'
+});
+ok(
+    $script->authtoken,
+    'Have an authtoken'
+);
+
+# Register workstation.
+my $ws = find_or_register_workstation($authtoken, BR1_ID, BR1_WORKSTATION);
+ok(
+    ! ref $ws,
+    'Found or registered workstation'
+);
+
+# Logout.
+$script->logout();
+ok(
+    ! $script->authtoken,
+    'Successfully logged out'
+);
+
+# Login as admin at BR1 using the workstation.
+$authtoken = $script->authenticate({
+    username=>'admin',
+    password=>'demo123',
+    type=>'staff',
+    workstation => BR1_WORKSTATION
+});
+ok(
+    $script->authtoken,
+    'Have an authtoken'
+);
+
+# Check that OILS_SETTING_MAX_DUPLICATE_HOLDS is not set at BR1 and ancestors.
+my $setting_value = $U->ou_ancestor_setting_value(BR1_ID, OILS_SETTING_MAX_DUPLICATE_HOLDS);
+ok(
+    ! $setting_value,
+    'circ.holds.max_duplicate_holds is not set for BR1'
+);
+
+# Check that OILS_SETTING_MAX_DUPLICATE_HOLDS is not set at BR2 and ancestors.
+$setting_value = $U->ou_ancestor_setting_value(BR2_ID, OILS_SETTING_MAX_DUPLICATE_HOLDS);
+ok(
+    ! $setting_value,
+    'circ.holds.max_duplicate_holds is not set for BR2'
+);
+
+# Set OILS_SETTING_MAX_DUPLICATE_HOLDS to 5 at BR1.
+$setting_value = $U->simplereq(
+    'open-ils.actor',
+    'open-ils.actor.org_unit.settings.update',
+    $authtoken,
+    BR1_ID,
+    {OILS_SETTING_MAX_DUPLICATE_HOLDS, 5}
+);
+ok(
+    ! ref $setting_value,
+    'circ.holds.max_duplicate_holds set to 5 for BR1'
+);
+
+# Retrieve PATRON1.
+my $patron1 = $U->simplereq(
+    'open-ils.actor',
+    'open-ils.actor.user.fleshed.retrieve_by_barcode',
+    $authtoken,
+    PATRON1_BARCODE
+);
+isa_ok(
+    ref $patron1,
+    'Fieldmapper::actor::user',
+    'Got patron 1'
+) or BAIL_OUT('Need Patron1');
+
+# Create a circ session for holds placement.
+my $circ_session = $script->session('open-ils.circ');
+
+# Place 5 holds for RECORD_ID for PATRON1. Expect success.
+my $request = $circ_session->request(
+    'open-ils.circ.holds.test_and_create.batch',
+    $authtoken,
+    {
+        hold_type => 'T',
+        patronid => $patron1->id(),
+        pickup_lib => $patron1->home_ou()
+    },
+    [RECORD_ID, RECORD_ID, RECORD_ID, RECORD_ID, RECORD_ID]
+);
+my $success = 0;
+while (my $response = $request->recv()) {
+    my $result = $response->content();
+    if ($result->{result} && !ref $result->{result}) {
+        $success++;
+        push(@holds, $result->{result});
+    }
+}
+$request->finish();
+is(
+    $success,
+    5,
+    'Placed 5 title holds for Patron 1'
+);
+
+# Place 1 hold for RECORD_ID for PATRON1. Expect HOLD_EXISTS.
+$request = $circ_session->request(
+    'open-ils.circ.holds.test_and_create.batch',
+    $authtoken,
+    {
+        hold_type => 'T',
+        patronid => $patron1->id(),
+        pickup_lib => $patron1->home_ou()
+    },
+    [RECORD_ID]
+);
+my $textcode;
+while (my $response = $request->recv()) {
+    my $result = $response->content();
+    if ($result->{result} && ref($result->{result}) eq 'ARRAY') {
+        if (grep {$_->{textcode} eq 'HOLD_EXISTS'} @{$result->{result}}) {
+            $textcode = 'HOLD_EXISTS';
+        }
+    }
+}
+$request->finish();
+is(
+    $textcode,
+    'HOLD_EXISTS',
+    'Got HOLD_EXISTS placing 6th title hold for patron 1'
+);
+
+# Place 5 holds for METARECORD_ID for PATRON1. Expect success.
+$request = $circ_session->request(
+    'open-ils.circ.holds.test_and_create.batch',
+    $authtoken,
+    {
+        hold_type => 'M',
+        patronid => $patron1->id(),
+        pickup_lib => $patron1->home_ou()
+    },
+    [METARECORD_ID, METARECORD_ID, METARECORD_ID, METARECORD_ID, METARECORD_ID]
+);
+$success = 0;
+while (my $response = $request->recv()) {
+    my $result = $response->content();
+    if ($result->{result} && !ref $result->{result}) {
+        $success++;
+        push(@holds, $result->{result});
+    }
+}
+$request->finish();
+is(
+    $success,
+    5,
+    'Placed 5 metarecord holds for Patron 1'
+);
+
+# Place 1 hold for METARECORD_ID for PATRON1. Expect HOLD_EXISTS.
+$request = $circ_session->request(
+    'open-ils.circ.holds.test_and_create.batch',
+    $authtoken,
+    {
+        hold_type => 'M',
+        patronid => $patron1->id(),
+        pickup_lib => $patron1->home_ou()
+    },
+    [METARECORD_ID]
+);
+$textcode = '';
+while (my $response = $request->recv()) {
+    my $result = $response->content();
+    if ($result->{result} && ref($result->{result}) eq 'ARRAY') {
+        if (grep {$_->{textcode} eq 'HOLD_EXISTS'} @{$result->{result}}) {
+            $textcode = 'HOLD_EXISTS';
+        }
+    }
+}
+$request->finish();
+is(
+    $textcode,
+    'HOLD_EXISTS',
+    'Got HOLD_EXISTS placing 6th metarecord hold for patron 1'
+);
+
+# Place 5 holds for COPY_ID for PATRON1. Expect 1 success and 4 HOLD_EXISTS.
+$request = $circ_session->request(
+    'open-ils.circ.holds.test_and_create.batch',
+    $authtoken,
+    {
+        hold_type => 'C',
+        patronid => $patron1->id(),
+        pickup_lib => $patron1->home_ou()
+    },
+    [COPY_ID, COPY_ID, COPY_ID, COPY_ID, COPY_ID]
+);
+$success = 0;
+$textcode = 0; # Using textcode as int this time.
+while (my $response = $request->recv()) {
+    my $result = $response->content();
+    if ($result->{result} && ref($result->{result}) eq 'ARRAY') {
+        if (grep {$_->{textcode} eq 'HOLD_EXISTS'} @{$result->{result}}) {
+            $textcode++;
+        }
+    } elsif ($result->{result}) {
+        $success++;
+        push(@holds, $result->{result});
+    }
+}
+$request->finish();
+is(
+    $success,
+    1,
+    'Placed 1 copy hold for patron 1'
+);
+is(
+    $textcode,
+    4,
+    'Got 4 HOLD_EXISTS on copy holds for patron 1'
+);
+
+# Retrieve PATRON2.
+my $patron2 = $U->simplereq(
+    'open-ils.actor',
+    'open-ils.actor.user.fleshed.retrieve_by_barcode',
+    $authtoken,
+    PATRON2_BARCODE
+);
+isa_ok(
+    ref $patron2,
+    'Fieldmapper::actor::user',
+    'Got patron 2'
+) or BAIL_OUT('Need Patron 2');
+
+# Place 5 holds for RECORD_ID for PATRON2. Expect 1 success and 4 HOLD_EXISTS.
+$request = $circ_session->request(
+    'open-ils.circ.holds.test_and_create.batch',
+    $authtoken,
+    {
+        hold_type => 'T',
+        patronid => $patron2->id(),
+        pickup_lib => $patron2->home_ou()
+    },
+    [RECORD_ID, RECORD_ID, RECORD_ID, RECORD_ID, RECORD_ID]
+);
+$success = 0;
+$textcode = 0; # Using textcode as int this time.
+while (my $response = $request->recv()) {
+    my $result = $response->content();
+    if ($result->{result} && ref($result->{result}) eq 'ARRAY') {
+        if (grep {$_->{textcode} eq 'HOLD_EXISTS'} @{$result->{result}}) {
+            $textcode++;
+        }
+    } elsif ($result->{result}) {
+        $success++;
+        push(@holds, $result->{result});
+    }
+}
+$request->finish();
+is(
+    $success,
+    1,
+    'Placed 1 title hold for patron 2'
+);
+is(
+    $textcode,
+    4,
+    'Got 4 HOLD_EXISTS on title holds for patron 2'
+);
+
+# Place 5 holds for METARECORD_ID for PATRON2. Expect 1 success and 4 HOLD_EXISTS.
+$request = $circ_session->request(
+    'open-ils.circ.holds.test_and_create.batch',
+    $authtoken,
+    {
+        hold_type => 'M',
+        patronid => $patron2->id(),
+        pickup_lib => $patron2->home_ou()
+    },
+    [METARECORD_ID, METARECORD_ID, METARECORD_ID, METARECORD_ID, METARECORD_ID]
+);
+$success = 0;
+$textcode = 0; # Using textcode as int this time.
+while (my $response = $request->recv()) {
+    my $result = $response->content();
+    if ($result->{result} && ref($result->{result}) eq 'ARRAY') {
+        if (grep {$_->{textcode} eq 'HOLD_EXISTS'} @{$result->{result}}) {
+            $textcode++;
+        }
+    } elsif ($result->{result}) {
+        $success++;
+        push(@holds, $result->{result});
+    }
+}
+$request->finish();
+is(
+    $success,
+    1,
+    'Placed 1 metarecord hold for patron 2'
+);
+is(
+    $textcode,
+    4,
+    'Got 4 HOLD_EXISTS on metarecord holds for patron 2'
+);
+
+# Place 5 holds for COPY_ID for PATRON2. Expect 1 success and 4 HOLD_EXISTS.
+$request = $circ_session->request(
+    'open-ils.circ.holds.test_and_create.batch',
+    $authtoken,
+    {
+        hold_type => 'C',
+        patronid => $patron2->id(),
+        pickup_lib => $patron2->home_ou()
+    },
+    [COPY_ID, COPY_ID, COPY_ID, COPY_ID, COPY_ID]
+);
+$success = 0;
+$textcode = 0; # Using textcode as int this time.
+while (my $response = $request->recv()) {
+    my $result = $response->content();
+    if ($result->{result} && ref($result->{result}) eq 'ARRAY') {
+        if (grep {$_->{textcode} eq 'HOLD_EXISTS'} @{$result->{result}}) {
+            $textcode++;
+        }
+    } elsif ($result->{result}) {
+        $success++;
+        push(@holds, $result->{result});
+    }
+}
+$request->finish();
+is(
+    $success,
+    1,
+    'Placed 1 copy hold for patron 2'
+);
+is(
+    $textcode,
+    4,
+    'Got 4 HOLD_EXISTS on copy holds for patron 2'
+);
+
+# Cancel all of the holds placed.
+# How many successes we expect.
+my $expect = scalar(@holds);
+$success = 0;
+foreach my $hold (@holds) {
+    my $result = $circ_session->request(
+        'open-ils.circ.hold.cancel',
+        $authtoken,
+        $hold,
+        5,
+        'LP 1694058 perl test'
+    )->gather(1);
+    if ($result && ! ref $result) {
+        $success++;
+    }
+}
+is(
+    $success,
+    $expect,
+    "Cancelled $expect holds"
+);
+
+# Reset @holds
+ at holds = ();
+
+# Test the permission by logging in as patron 1 and placing a title and metarecord hold.
+
+# Login as patron1.
+my $patron_auth = $script->authenticate({
+    username => $patron1->usrname(),
+    password => 'leona1234',
+    type => 'opac'
+});
+ok(
+    $patron_auth,
+    'Logged in as patron 1'
+);
+
+# Place 5 holds for RECORD_ID as PATRON1. Expect 1 success and 4 HOLD_EXISTS.
+$request = $circ_session->request(
+    'open-ils.circ.holds.test_and_create.batch',
+    $patron_auth,
+    {
+        hold_type => 'T',
+        patronid => $patron1->id(),
+        pickup_lib => $patron1->home_ou()
+    },
+    [RECORD_ID, RECORD_ID, RECORD_ID, RECORD_ID, RECORD_ID]
+);
+$success = 0;
+$textcode = 0; # Using textcode as int this time.
+while (my $response = $request->recv()) {
+    my $result = $response->content();
+    if ($result->{result} && ref($result->{result}) eq 'ARRAY') {
+        if (grep {$_->{textcode} eq 'HOLD_EXISTS'} @{$result->{result}}) {
+            $textcode++;
+        }
+    } elsif ($result->{result}) {
+        $success++;
+        push(@holds, $result->{result});
+    }
+}
+$request->finish();
+is(
+    $success,
+    1,
+    'Patron 1 placed 1 title hold'
+);
+is(
+    $textcode,
+    4,
+    'Patron 1 got 4 HOLD_EXISTS on title holds'
+);
+
+# Ditto for metarecord holds:
+$request = $circ_session->request(
+    'open-ils.circ.holds.test_and_create.batch',
+    $patron_auth,
+    {
+        hold_type => 'T',
+        patronid => $patron1->id(),
+        pickup_lib => $patron1->home_ou()
+    },
+    [METARECORD_ID, METARECORD_ID, METARECORD_ID, METARECORD_ID, METARECORD_ID]
+);
+$success = 0;
+$textcode = 0; # Using textcode as int this time.
+while (my $response = $request->recv()) {
+    my $result = $response->content();
+    if ($result->{result} && ref($result->{result}) eq 'ARRAY') {
+        if (grep {$_->{textcode} eq 'HOLD_EXISTS'} @{$result->{result}}) {
+            $textcode++;
+        }
+    } elsif ($result->{result}) {
+        $success++;
+        push(@holds, $result->{result});
+    }
+}
+$request->finish();
+is(
+    $success,
+    1,
+    'Patron 1 placed 1 metarecord hold'
+);
+is(
+    $textcode,
+    4,
+    'Patron 1 got 4 HOLD_EXISTS on metarecord holds'
+);
+
+# Cancel the patron-placed holds.
+$expect = scalar(@holds);
+$success = 0;
+foreach my $hold (@holds) {
+    my $result = $circ_session->request(
+        'open-ils.circ.hold.cancel',
+        $patron_auth,
+        $hold,
+        6,
+        'LP 1694058 perl test'
+    )->gather(1);
+    if ($result && ! ref $result) {
+        $success++;
+    }
+}
+is(
+    $success,
+    $expect,
+    "Cancelled $expect patron holds"
+);
+
+# Reset @holds
+ at holds = ();
+
+# Unset OILS_SETTING_MAX_DUPLICATE_HOLDS at BR1.
+$setting_value = $U->simplereq(
+    'open-ils.actor',
+    'open-ils.actor.org_unit.settings.update',
+    $authtoken,
+    BR1_ID,
+    {OILS_SETTING_MAX_DUPLICATE_HOLDS, undef}
+);
+ok(
+    ! ref $setting_value,
+    'circ.holds.max_duplicate_holds unset for BR1'
+);
+
+# Logout. Because of a "bug" in Cronscript.pm, we need to log out in the order that we logged in.
+$script->logout($authtoken);
+$script->logout($patron_auth);
+ok(
+    ! $script->authtoken,
+    'Successfully logged out'
+);
+

commit 1438791250621e5f089b2b1ed9292862d6c4fbcd
Author: Jason Stephenson <jason at sigio.com>
Date:   Tue Aug 8 21:23:44 2017 -0400

    LP 1694058: Add backend code to allow multiple hold placement.
    
    We add a constant for the circ.holds.max_duplicate_holds setting.
    
    We modify Holds.pm to check if we're placing a title or metarecord
    hold, that we have the CREATE_DUPLICATE_HOLDS permission, and that we
    haven't placed more than the maximum allowed number of duplicate holds
    before returning the HOLD_EXISTS event.
    
    Signed-off-by: Jason Stephenson <jason at sigio.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm
index c46ad21..350fde2 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm
@@ -294,7 +294,15 @@ sub create_hold {
     $sargs->{holdable_formats} = $hold->holdable_formats if $t eq 'M';
 
     my $existing = $e->search_action_hold_request($sargs);
-    push( @events, OpenILS::Event->new('HOLD_EXISTS')) if @$existing;
+    if (@$existing) {
+        # See if the requestor has the CREATE_DUPLICATE_HOLDS perm.
+        my $can_dup = $e->allowed('CREATE_DUPLICATE_HOLDS', $recipient->home_ou);
+        # How many are allowed.
+        my $num_dups = $U->ou_ancestor_setting_value($recipient->home_ou, OILS_SETTING_MAX_DUPLICATE_HOLDS, $e) || 0;
+        push( @events, OpenILS::Event->new('HOLD_EXISTS'))
+            unless (($t eq 'T' || $t eq 'M') && $can_dup && scalar(@$existing) < $num_dups);
+        # Note: We check for @$existing < $num_dups because we're adding a hold with this call.
+    }
 
     my $checked_out = hold_item_is_checked_out($e, $recipient->id, $hold->hold_type, $hold->target);
     push( @events, OpenILS::Event->new('HOLD_ITEM_CHECKED_OUT')) if $checked_out;
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Const.pm b/Open-ILS/src/perlmods/lib/OpenILS/Const.pm
index c568d89..7c5fb7b 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Const.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Const.pm
@@ -96,6 +96,7 @@ econst OILS_SETTING_RESTORE_OVERDUE_ON_LOST_RETURN      => 'circ.restore_overdue
 econst OILS_SETTING_LOST_IMMEDIATELY_AVAILABLE          => 'circ.lost_immediately_available';
 econst OILS_SETTING_BLOCK_HOLD_FOR_EXPIRED_PATRON       => 'circ.holds.expired_patron_block';
 econst OILS_SETTING_GENERATE_OVERDUE_ON_LOST_RETURN     => 'circ.lost.generate_overdue_on_checkin';
+econst OILS_SETTING_MAX_DUPLICATE_HOLDS => 'circ.holds.max_duplicate_holds';
 
 
 

commit e70d5917f50e195b2f3b456c9d420a8c06e94823
Author: Jason Stephenson <jason at sigio.com>
Date:   Sun Jul 30 15:52:25 2017 -0400

    LP 1694058: Add org. unit setting for multiple hold placement.
    
    Add config.org_unit_setting_type circ.hold.max_duplicate_holds to set
    the maximum duplicate title or metarecord holds allowed per patron.
    
    Add pgtap test to make sure the new setting exists.
    
    Signed-off-by: Jason Stephenson <jason at sigio.com>
    Signed-off-by: Kathy Lussier <klussier at masslnc.org>

diff --git a/Open-ILS/src/sql/Pg/950.data.seed-values.sql b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
index 03f992f..e4379df 100644
--- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql
+++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
@@ -5243,6 +5243,16 @@ INSERT into config.org_unit_setting_type
       'In the Patron Bills interface, a payment attempt will warn if the amount exceeds the value of this setting.',
       'coust', 'description'),
     'currency', null)
+,( 'circ.holds.max_duplicate_holds', 'holds',
+   oils_i18n_gettext(
+     'circ.holds.max_duplicate_holds',
+     'Maximum number of duplicate holds allowed.',
+     'coust', 'label'),
+   oils_i18n_gettext(
+     'circ.holds.max_duplicate_holds',
+     'Maximum number of duplicate title or metarecord holds allowed per patron.',
+     'coust', 'description'),
+   'integer', null)
 ;
 
 UPDATE config.org_unit_setting_type
diff --git a/Open-ILS/src/sql/Pg/t/lp1694058_multiple_hold_placement.pg b/Open-ILS/src/sql/Pg/t/lp1694058_multiple_hold_placement.pg
new file mode 100644
index 0000000..6a65862
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/t/lp1694058_multiple_hold_placement.pg
@@ -0,0 +1,13 @@
+BEGIN;
+
+SELECT plan(1);
+
+-- Check that config.org_unit_setting_type of circ.holds.max_duplicate_holds exists.
+SELECT isnt_empty(
+    'SELECT * FROM config.org_unit_setting_type WHERE name = $$circ.holds.max_duplicate_holds$$',
+    'config.org_unit_setting_type circ.holds.max_duplicate_holds exists'
+);
+
+SELECT * FROM finish();
+
+ROLLBACK;
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.data.max_duplicate_holds_coust.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.data.max_duplicate_holds_coust.sql
new file mode 100644
index 0000000..93e4a4f
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.data.max_duplicate_holds_coust.sql
@@ -0,0 +1,20 @@
+BEGIN;
+
+-- INSERT INTO config.upgrade_log (version, applied_to) VALUES ('XXXX', :eg_version);
+
+-- Add the circ.holds.max_duplicate_holds org. unit setting type.
+INSERT into config.org_unit_setting_type
+( name, grp, label, description, datatype, fm_class )
+VALUES
+( 'circ.holds.max_duplicate_holds', 'holds',
+   oils_i18n_gettext(
+     'circ.holds.max_duplicate_holds',
+     'Maximum number of duplicate holds allowed.',
+     'coust', 'label'),
+   oils_i18n_gettext(
+     'circ.holds.max_duplicate_holds',
+     'Maximum number of duplicate title or metarecord holds allowed per patron.',
+     'coust', 'description'),
+   'integer', null );
+
+COMMIT;

-----------------------------------------------------------------------

Summary of changes:
 .../perlmods/lib/OpenILS/Application/Circ/Holds.pm |   10 +-
 Open-ILS/src/perlmods/lib/OpenILS/Const.pm         |    1 +
 .../lib/OpenILS/WWW/EGCatLoader/Account.pm         |   47 ++-
 .../live_t/25-lp1694058-multiple-hold-placement.t  |  545 ++++++++++++++++++++
 Open-ILS/src/sql/Pg/002.schema.config.sql          |    2 +-
 Open-ILS/src/sql/Pg/950.data.seed-values.sql       |   10 +
 .../sql/Pg/t/lp1694058_multiple_hold_placement.pg  |   13 +
 .../1089.data.max_duplicate_holds_coust.sql        |   20 +
 Open-ILS/src/templates/opac/i18n_strings.tt2       |   14 +
 .../src/templates/opac/parts/multi_hold_select.tt2 |   15 +
 Open-ILS/src/templates/opac/parts/place_hold.tt2   |   29 +-
 .../web/js/ui/default/opac/holds-validation.js     |   17 +-
 .../Circulation/multiple_hold_placement.adoc       |   20 +
 13 files changed, 725 insertions(+), 18 deletions(-)
 create mode 100644 Open-ILS/src/perlmods/live_t/25-lp1694058-multiple-hold-placement.t
 create mode 100644 Open-ILS/src/sql/Pg/t/lp1694058_multiple_hold_placement.pg
 create mode 100644 Open-ILS/src/sql/Pg/upgrade/1089.data.max_duplicate_holds_coust.sql
 create mode 100644 Open-ILS/src/templates/opac/parts/multi_hold_select.tt2
 create mode 100644 docs/RELEASE_NOTES_NEXT/Circulation/multiple_hold_placement.adoc


hooks/post-receive
-- 
Evergreen ILS




More information about the open-ils-commits mailing list