@@ -, +, @@ -OPACShowPlaceItemLevelHold or -ShowPlaceItemLevelHold --- C4/Items.pm | 44 ++++++++++++---------- catalogue/detail.pl | 6 +++ installer/data/mysql/sysprefs.sql | 2 + .../en/modules/admin/preferences/cataloguing.pref | 7 ++++ .../prog/en/modules/admin/preferences/opac.pref | 7 ++++ .../prog/en/modules/catalogue/detail.tt | 8 ++++ .../prog/en/modules/reserve/request.tt | 17 +++++++-- koha-tmpl/opac-tmpl/prog/en/modules/opac-detail.tt | 27 ++++++++----- .../opac-tmpl/prog/en/modules/opac-reserve.tt | 16 +++++--- opac/opac-detail.pl | 39 ++++++++----------- opac/opac-reserve.pl | 14 +++++-- reserve/request.pl | 22 ++++++++++- t/db_dependent/Items.t | 21 +++++++---- 13 files changed, 158 insertions(+), 72 deletions(-) --- a/C4/Items.pm +++ a/C4/Items.pm @@ -1219,7 +1219,8 @@ $filter->{fromDate} = '01/01/2013'; #Filters only serial issues by the serialite $filter->{toDate} = '31/12/2014'; #Filters only serial issues by the serialitems.publisheddate #The ending date in C4::Context->preference('dateformat') format $filter->{limit} = 15; #Limits the returning set to this many items - +$filter->{itemnumber} = 165; #Returns the item with this itemnumber + Filters are expected to be validated! If a filter is not defined, that filter is not present in the $filter-HASH =cut @@ -1227,20 +1228,20 @@ Filters are expected to be validated! If a filter is not defined, that filter is sub GetItemsInfo { my ( $biblionumber, $filter ) = @_; my $dbh = C4::Context->dbh; - + #Prepare the filter my $filterEnumchron = 0; my $enumchronSQLRegexp; if (defined $filter && ref $filter eq 'HASH') { - + #Items enumchron can be filtered by volume or number or both. - #Because the format of enumchron + #Because the format of enumchron #For performance reasons regexp's need to be as simple as possible. ## It is entirely possible to search with just volume or just number or just issue. ## We don't know which filters are used so it is safer and more efficient to just ## prepare the enumeration parsing SQL every time. $enumchronSQLRegexp = C4::Context->preference('NumberingFormulaParsingRegexp'); - + if (exists $filter->{volume}) { $filterEnumchron = 1; $enumchronSQLRegexp =~ s/volume/$filter->{volume}/; @@ -1261,14 +1262,14 @@ sub GetItemsInfo { } else { $enumchronSQLRegexp =~ s/issue/[0-9]*/; - } + } } #If we know that this item is a serial, we can better optimize our big SQL. # This is especially useful when we want to filter based on the publication date. # SELECTing a huge blob of serials just to remove unnecessary ones will be really sloooow. my $search = C4::SQLHelper::SearchInTable("biblio",{biblionumber => $biblionumber}, undef, undef, ['serial'], undef, "exact"); my $serial = $search->[0]->{serial}; - + # note biblioitems.* must be avoided to prevent large marc and marcxml fields from killing performance. #Because it is uncertain how many parameters the SQL query needs, we need to build the parameters dynamically # This is because we cannot predict what filters our users use. @@ -1294,8 +1295,8 @@ sub GetItemsInfo { holding.branchname, holding.opac_info as branch_opac_info "; if ($serial) { - $query .= ", - serial.serialseq, + $query .= ", + serial.serialseq, serial.publisheddate "; } $query .= " @@ -1306,15 +1307,15 @@ sub GetItemsInfo { LEFT JOIN biblioitems ON biblioitems.biblioitemnumber = items.biblioitemnumber LEFT JOIN itemtypes ON itemtypes.itemtype = " . (C4::Context->preference('item-level_itypes') ? 'items.itype' : 'biblioitems.itemtype'); - + if ($serial) { - $query .= " - LEFT JOIN serialitems ON serialitems.itemnumber = items.itemnumber - LEFT JOIN serial ON serialitems.serialid = serial.serialid "; + $query .= " + LEFT JOIN serialitems ON serialitems.itemnumber = items.itemnumber + LEFT JOIN serial ON serialitems.serialid = serial.serialid "; } - + $query .= " WHERE items.biblionumber = ? "; - + if (exists $filter->{branch}) { $query .= " AND items.holdingbranch = ?"; push @$queryParams, $filter->{branch}; @@ -1341,13 +1342,18 @@ sub GetItemsInfo { } push @$queryParams, $filter->{toDate}; } - + if (exists $filter->{itemnumber}) { + push @$queryParams, $filter->{itemnumber}; + $query .= " AND items.itemnumber = ?"; + } + + $query .= "ORDER BY home.branchname, items.enumchron, LPAD( items.copynumber, 8, '0' ), items.dateaccessioned DESC" ; if (exists $filter->{limit}) { push @$queryParams, $filter->{limit}; $query .= " LIMIT ?"; } - + my $sth = $dbh->prepare($query); $sth->execute(@$queryParams); my $i = 0; @@ -1359,7 +1365,7 @@ sub GetItemsInfo { WHERE itemnumber = ?" ); - + while ( my $data = $sth->fetchrow_hashref ) { my $datedue = ''; $isth->execute( $data->{'itemnumber'} ); @@ -1372,7 +1378,7 @@ sub GetItemsInfo { $datedue = $idata->{'date_due'}; if (C4::Context->preference("IndependentBranches")){ my $userenv = C4::Context->userenv; - if ( ($userenv) && ( $userenv->{flags} % 2 != 1 ) ) { + if ( ($userenv) && ( $userenv->{flags} % 2 != 1 ) ) { $data->{'NOTSAMEBRANCH'} = 1 if ($idata->{'bcode'} ne $userenv->{branch}); } } --- a/catalogue/detail.pl +++ a/catalogue/detail.pl @@ -143,6 +143,10 @@ my $dat = &GetBiblioData($biblionumber); my $subscriptionsnumber = CountSubscriptionFromBiblionumber($biblionumber); my @subscriptions = GetSubscriptions( $dat->{title}, $dat->{issn}, undef, $biblionumber ); my @subs; +if (scalar(@subscriptions)) { + $template->param('isSerial',1); +} + foreach my $subscription (@subscriptions) { my %cell; @@ -412,4 +416,6 @@ if (C4::Context->preference('TagsEnabled') and $tag_quantity = C4::Context->pref my ( $holdcount, $holds ) = C4::Reserves::GetReservesFromBiblionumber($biblionumber,1); $template->param( holdcount => $holdcount, holds => $holds ); +$template->param('ShowPlaceItemLevelHold', C4::Context->preference('ShowPlaceItemLevelHold')); + output_html_with_http_headers $query, $cookie, $template->output; --- a/installer/data/mysql/sysprefs.sql +++ a/installer/data/mysql/sysprefs.sql @@ -270,6 +270,7 @@ INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` ('OpacShowLibrariesPulldownMobile','1',NULL,'Show the libraries pulldown on the mobile version of the OPAC.','YesNo'), ('OpacShowRecentComments','0',NULL,'If ON a link to recent comments will appear in the OPAC masthead','YesNo'), ('OPACShowUnusedAuthorities','1','','Show authorities that are not being used in the OPAC.','YesNo'), +('OPACShowPlaceItemLevelHold','0',NULL,'Allow placing a item-level hold from the detailed biblio view.','YesNo'), ('opacsmallimage','','','Enter a complete URL to an image to replace the default Koha logo','free'), ('OpacStarRatings','all','disable|all|details',NULL,'Choice'), ('OpacSuppression','0','','Turn ON the OPAC Suppression feature, requires further setup, ask your system administrator for details','YesNo'), @@ -341,6 +342,7 @@ INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` ('ShowPatronImageInWebBasedSelfCheck','0','','If ON, displays patron image when a patron uses web-based self-checkout','YesNo'), ('ShowReviewer','full','none|full|first|surname|firstandinitial|username','Choose how a commenter\'s identity is presented alongside comments in the OPAC','Choice'), ('ShowReviewerPhoto','1','','If ON, photo of reviewer will be shown beside comments in OPAC','YesNo'), +('ShowPlaceItemLevelHold','0',NULL,'Allow placing a item-level hold from the detailed biblio view.','YesNo'), ('singleBranchMode','0',NULL,'Operate in Single-branch mode, hide branch selection in the OPAC','YesNo'), ('SlipCSS','',NULL,'Slips CSS url.','free'), ('SMSSendDriver','','','Sets which SMS::Send driver is used to send SMS messages.','free'), --- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/cataloguing.pref +++ a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/cataloguing.pref @@ -18,6 +18,13 @@ Cataloging: yes: Display no: "Don't Display" - easy ways to create analytical record relationships + - + - pref: ShowPlaceItemLevelHold + default: 0 + choices: + yes: "Allow" + no : "Don't allow" + - placing a item-level hold from the detailed biblio view. This is always enabled for serials because serials need only item-level holds. Spine Labels: - - When using the quick spine label printer, --- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/opac.pref +++ a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/opac.pref @@ -131,6 +131,13 @@ OPAC: no: "Don't show" - the item's barcode on the holdings tab. - + - pref: OPACShowPlaceItemLevelHold + default: 0 + choices: + yes: "Allow" + no : "Don't allow" + - placing a item-level hold from the detailed biblio view. This is always enabled for serials because serials need only item-level holds. + - - pref: OpacHighlightedWords choices: yes: Highlight --- a/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/detail.tt +++ a/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/detail.tt @@ -422,6 +422,7 @@ function verify_images() { [% IF ( hostrecords ) %]Host records[% END %] [% IF ( analyze ) %]Used in[% END %] [% IF ( ShowCourseReserves ) %]Course Reserves[% END %] + [% IF ( ShowPlaceItemLevelHold || isSerial ) %]Reserve[% END %] @@ -590,6 +591,13 @@ function verify_images() { [% END %] [% END %] + [% IF ( ShowPlaceItemLevelHold || isSerial ) %] + + + Place hold + + + [% END %] [% END %] --- a/koha-tmpl/intranet-tmpl/prog/en/modules/reserve/request.tt +++ a/koha-tmpl/intranet-tmpl/prog/en/modules/reserve/request.tt @@ -191,7 +191,9 @@ function checkMultiHold() { [% END %] [% UNLESS ( multi_hold ) %] -

Place a hold on [% INCLUDE 'biblio-default-view.inc' %][% title |html %]

+

Place a hold on [% INCLUDE 'biblio-default-view.inc' %][% title |html %] + [% IF targetItemTitle %] - [% targetItemTitle %][% END %] +

[% ELSE %]

Confirm Holds

[% END %] @@ -208,6 +210,7 @@ function checkMultiHold() { + [% IF targetItemnumber %][% END %] [% ELSE %]
@@ -221,6 +224,7 @@ function checkMultiHold() { [% END %]
+ [% IF targetItemnumber %][% END %] [% END %] [% IF ( multi_hold ) %] @@ -344,8 +348,11 @@ function checkMultiHold() { [% UNLESS ( multi_hold ) %] -
  • +
  • + [% UNLESS targetItemnumber %] + + [% END %]
  • @@ -395,7 +402,11 @@ function checkMultiHold() { [% IF ( itemloo.available ) %] - + [% IF targetItemnumber %] + + [% ELSE %] + + [% END %] [% ELSIF ( itemloo.override ) %] Requires override of hold policy --- a/koha-tmpl/opac-tmpl/prog/en/modules/opac-detail.tt +++ a/koha-tmpl/opac-tmpl/prog/en/modules/opac-detail.tt @@ -68,7 +68,7 @@ $(".highlight_toggle").toggle(); } [% END %] -// ------------------------------ // +// ------------------------------ // //>>> Document READY starts here! // // ------------------------------ // $(document).ready(function() { @@ -132,12 +132,13 @@ null, [% END %] [% IF ( ShowCourseReservesHeader ) %]null,[% END %] + [% IF ( isSerial ) %]null,[% END %] ] })); //Bind the datepicker $('.datepicker').datepicker(); - + [% IF ( query_desc ) %][% IF ( OpacHighlightedWords ) %]var query_desc = "[% query_desc |replace("'", "\'") |replace('\n', '\\n') |replace('\r', '\\r') |html %]"; q_array = query_desc.split(" "); highlightOn(); @@ -1053,17 +1054,17 @@ YAHOO.util.Event.onContentReady("furtherm", function () {
    - + [% IF ( showFilter ) %] [%# Display the items filtering form used to filter the shown items. See the end of this file! %] [% INCLUDE filter_form tab="holdings" %] [% END %] - -[% IF ( itemloop.size ) %] - + +[% IF ( itemloop.size ) %] + [% IF ( lotsofitems ) %]

    - [% IF noBranchItems %] + [% IF userBranchNotDefined %] Limiting items to the most recent received last year.
    [% ELSE %] Showing only the most recent items from your branch.
    @@ -1550,6 +1551,7 @@ YAHOO.util.Event.onContentReady("furtherm", function () { Item hold queue priority [% END %] [% IF ( ShowCourseReservesHeader ) %]Course reserves[% END %] + [% IF ( isSerial ) %]Reserve[% END %] [% FOREACH ITEM_RESULT IN items %] [% IF ITEM_RESULT.this_branch %] @@ -1630,6 +1632,13 @@ YAHOO.util.Event.onContentReady("furtherm", function () { [% END %] [% END %] + [% IF OPACShowPlaceItemLevelHold || isSerial %] + + + Place hold + + + [% END %] [% END %] @@ -1654,7 +1663,7 @@ YAHOO.util.Event.onContentReady("furtherm", function () {

    - +
    +
    @@ -1686,7 +1695,7 @@ YAHOO.util.Event.onContentReady("furtherm", function () { -
    [% IF ( itemLoo.available ) %] + value="[% itemLoo.itemnumber %]" + [% IF singleItemLevelHold %] checked="checked" [% END %] + /> [% ELSE %] Cannot be put on hold @@ -540,12 +545,11 @@
    - - [% END %] - [% END %] + [% END %] + [% END %] [% END %] --- a/opac/opac-detail.pl +++ a/opac/opac-detail.pl @@ -77,7 +77,7 @@ $biblionumber = int($biblionumber); ##>> Handling the Serial issue filter parameters from the user ## # We can filter issues based on these five values. -my $filterBranchLimiter = $query->param('filterBranchLimiter') ? $query->param('filterBranchLimiter') : undef; +my $filterBranchLimiter = $query->param('filterBranchLimiter') ? $query->param('filterBranchLimiter') : '_ShowAll'; my $filterVolume = $query->param('filterVolume') ? $query->param('filterVolume') : undef; my $filterNumber = $query->param('filterNumber') ? $query->param('filterNumber') : undef; my $filterIssue = $query->param('filterIssue') ? $query->param('filterIssue') : undef; @@ -153,7 +153,7 @@ my $itemsCount = GetItemsCount($biblionumber); my $lotsofitems = 0; if ($itemsCount > C4::Context->preference('OPACmaxItemsOnDetail') && !($query->param('viewallitems')) ) { - + $lotsofitems = 1; $template->param('lotsofitems' => $lotsofitems); } @@ -162,35 +162,22 @@ my @all_items; if ($lotsofitems) { #Spare the poor old DB from this possibly excruciatinly huge read operation. if (! scalar(%$filter)) { #Is the $filter empty? - - #Limit the amount of results to the system preference - $filter->{limit} = C4::Context->preference('OPACmaxItemsOnDetail'); - #Limit display of Items based on the users branch if (C4::Context->userenv){ $filter->{branch} = C4::Context->userenv->{branch}; $filterBranchLimiter = $filter->{branch}; #Making the branches loop react to this! - - @all_items = GetItemsInfo($biblionumber, $filter); } - #Or if branch cannot be defined or has no items, then display Items only from the past year - if( ! (scalar(@all_items)) ) { - delete $filter->{branch}; #Remove the branch filter as it gave no results - undef $filterBranchLimiter; - + #Or if branch cannot be defined, then display Items only from the past year + else { my $now = DateTime->now(); $now->set_year( ($now->year() - 1) ); $filter->{fromDate} = Koha::DateUtils::output_pref({ dt => $now, dateformat => 'iso'}); - $template->param('noBranchItems' => 1); - @all_items = GetItemsInfo($biblionumber, $filter); + $template->param('userBranchNotDefined' => 1); } - + $filter->{limit} = C4::Context->preference('OPACmaxItemsOnDetail'); } } -#If we haven't found our items yet, then pull them using whatever we've got. -if( ! (scalar(@all_items)) ) { - @all_items = GetItemsInfo($biblionumber, $filter); -} +@all_items = GetItemsInfo($biblionumber, $filter); ##Prepare the custom branches loop containing the _ShowAll entry to show issues from all libraries. @@ -306,7 +293,6 @@ if ($session->param('busc')) { my $expanded_facet = $arrParamsBusc->{'expand'}; my $branches = GetBranches(); - my $itemtypes = GetItemTypes; my @servers; @servers = @{$arrParamsBusc->{'server'}} if $arrParamsBusc->{'server'}; @servers = ("biblioserver") unless (@servers); @@ -317,7 +303,7 @@ if ($session->param('busc')) { $sort_by[0] = $default_sort_by if !$sort_by[0] && defined($default_sort_by); my ($error, $results_hashref, $facets); eval { - ($error, $results_hashref, $facets) = getRecords($arrParamsBusc->{'query'},$arrParamsBusc->{'simple_query'},\@sort_by,\@servers,$results_per_page,$offset,$expanded_facet,$branches,$itemtypes,$arrParamsBusc->{'query_type'},$arrParamsBusc->{'scan'}); + ($error, $results_hashref, $facets) = getRecords($arrParamsBusc->{'query'},$arrParamsBusc->{'simple_query'},\@sort_by,\@servers,$results_per_page,$offset,$expanded_facet,$branches,$arrParamsBusc->{'query_type'},$arrParamsBusc->{'scan'}); }; my $hits; my @newresults; @@ -705,7 +691,7 @@ if ( $show_holds_count || $show_priority) { } $template->param( show_priority => $has_hold ) ; -my $norequests = 1; +my $norequests = 1; #"No requests"-flag hides the "Place hold"-link in opac-detail-sidebar.inc my %itemfields; my (@itemloop, @otheritemloop); my $currentbranch = C4::Context->userenv ? C4::Context->userenv->{branch} : undef; @@ -770,6 +756,11 @@ for my $itm (@items) { } } +#There is no point in enabling "Place hold" for Serials. +# Placing a title-level hold for a Serial is crazy since we don't want "any" item but instead the specific volume and number. +# Also choosing an item-level hold from opac-reserve.pl is problematic since there could be thousands of items. +$norequests = 1 if $template->{VARS}->{'isSerial'}; + # Display only one tab if one items list is empty if (scalar(@itemloop) == 0 || scalar(@otheritemloop) == 0) { $template->param(SeparateHoldings => 0); @@ -1217,4 +1208,6 @@ if ( length C4::Context->preference('NumberingFormulaParsingRegexp') < 3 ) { } $template->{VARS}->{useFilterIssueInput} = 1 if (C4::Context->preference('FilterSerialsByIssue')); +$template->param('OPACShowPlaceItemLevelHold', C4::Context->preference('OPACShowPlaceItemLevelHold')); + output_html_with_http_headers $query, $cookie, $template->output; --- a/opac/opac-reserve.pl +++ a/opac/opac-reserve.pl @@ -73,8 +73,10 @@ if ($borr->{reservefee} > 0){ my $branches = GetBranches(); my $itemTypes = GetItemTypes(); -# There are two ways of calling this script, with a single biblio num -# or multiple biblio nums. +# There are three ways of calling this script, with a single biblio num +# or multiple biblio nums or as a itemhold, where the target item has already been chosen. +my $targetItemnumber = $query->param('itemnumber') ? $query->param('itemnumber') : undef; +$template->param('singleItemLevelHold' => 1); my $biblionumbers = $query->param('biblionumbers'); my $reserveMode = $query->param('reserve_mode'); if ($reserveMode && ($reserveMode eq 'single')) { @@ -127,8 +129,14 @@ foreach my $biblioNumber (@biblionumbers) { my $biblioData = GetBiblioData($biblioNumber); $biblioDataHash{$biblioNumber} = $biblioData; + my @itemInfos; - my @itemInfos = GetItemsInfo($biblioNumber); + if ($targetItemnumber) { #We just want the one item so use the filters to find it! + @itemInfos = GetItemsInfo($biblioNumber, {itemnumber => $targetItemnumber}); + } + else { + @itemInfos = GetItemsInfo($biblioNumber); + } my $marcrecord= GetMarcBiblio($biblioNumber); --- a/reserve/request.pl +++ a/reserve/request.pl @@ -57,10 +57,27 @@ my ( $template, $borrowernumber, $cookie, $flags ) = get_template_and_user( } ); +my $targetItemnumber = $input->param('itemnumber') ? $input->param('itemnumber') : undef; +if ($targetItemnumber) { + $template->param('targetItemnumber', $targetItemnumber); + my $targetItem = C4::Items::GetItem($targetItemnumber); + if ($targetItem->{enumchron}) { + $template->param('targetItemTitle', $targetItem->{enumchron}); + } + elsif ($targetItem->{barcode}) { + $template->param('targetItemTitle', $targetItem->{barcode}); + } + else { + $template->param('targetItemTitle', $targetItem->{itemnumber}); + } +} + + my $multihold = $input->param('multi_hold'); $template->param(multi_hold => $multihold); my $showallitems = $input->param('showallitems'); + # get Branches and Itemtypes my $branches = GetBranches(); my $itemtypes = GetItemTypes(); @@ -243,7 +260,10 @@ foreach my $biblionumber (@biblionumbers) { my @itemnumbers; ## $items is array of 'item' table numbers - if (my $items = get_itemnumbers_of($biblionumber)->{$biblionumber}){ + if ($targetItemnumber) { #If we just want to place a hold on one item + push @itemnumbers, $targetItemnumber; + } + elsif (my $items = get_itemnumbers_of($biblionumber)->{$biblionumber}){ @itemnumbers = @$items; } my @hostitems = get_hostitemnumbers_of($biblionumber); --- a/t/db_dependent/Items.t +++ a/t/db_dependent/Items.t @@ -144,8 +144,8 @@ subtest 'GetHiddenItemnumbers tests' => sub { subtest 'Filter items tests' => sub { - - plan tests => 3; + + plan tests => 2; # Start transaction $dbh->{AutoCommit} = 0; @@ -165,18 +165,23 @@ subtest 'Filter items tests' => sub { holdingbranch => 'MPL', }, $biblionumber ); - + my ($item3_bibnum, $item3_bibitemnum, $item3_itemnumber) = AddItem( + { homebranch => 'IPT', + holdingbranch => 'IPT', }, + $biblionumber + ); + # Testing the branch filter my @shouldBeItem2 = C4::Items::GetItemsInfo($biblionumber, {branch => 'MPL'}); is( $shouldBeItem2[0]->{itemnumber}, $item2_itemnumber, "Filtering by branch"); - + # Testing the dates filter my @shouldBeEmpty = C4::Items::GetItemsInfo($biblionumber, {toDate => '01/01/1933'}); is( scalar(@shouldBeEmpty), 0, "Filtering by date"); - - # Testing the limit filter - @shouldBeEmpty = C4::Items::GetItemsInfo($biblionumber, {limit => 0}); - is( scalar(@shouldBeEmpty), 0, "Filtering by limit"); + + # Testing the itemnumber filter + my @shouldBeItem3 = C4::Items::GetItemsInfo($biblionumber, {itemnumber => $item3_itemnumber}); + is( $shouldBeItem3[0]->{itemnumber}, $item3_itemnumber, "Filtering by itemnumber"); }; # Helper method to set up a Biblio. --