From 7ec3a49cb685fe91da852815c53345ee56cb92a5 Mon Sep 17 00:00:00 2001 From: Taylor Schmidt Date: Mon, 19 May 2014 12:27:22 -0600 Subject: [PATCH] Bug 13630 - Basic Angular circulation client --- Koha/Service.pm | 66 +- Koha/Service/Authentication.pm | 8 +- Koha/Service/Bib.pm | 24 +- Koha/Service/BibProfile.pm | 12 +- Koha/Service/Config/SystemPreferences.pm | 1 - Koha/Service/Patrons.pm | 245 ++++++ circ/checkout.pl | 98 +++ circ/circulation.pl | 3 + .../prog/en/includes/doc-head-open.inc | 14 +- .../prog/en/includes/members-toolbar.inc | 15 + koha-tmpl/intranet-tmpl/prog/en/js/ajax.js | 6 +- .../intranet-tmpl/prog/en/modules/circ/checkout.tt | 945 +++++++++++++++++++++ svc/patrons | 23 + t/db_dependent/Members.t | 18 +- t/db_dependent/Reserves.t | 16 +- 15 files changed, 1448 insertions(+), 46 deletions(-) create mode 100644 Koha/Service/Patrons.pm create mode 100755 circ/checkout.pl create mode 100644 koha-tmpl/intranet-tmpl/prog/en/modules/circ/checkout.tt create mode 100755 svc/patrons diff --git a/Koha/Service.pm b/Koha/Service.pm index f194298..a4d9799 100644 --- a/Koha/Service.pm +++ b/Koha/Service.pm @@ -59,8 +59,10 @@ use Modern::Perl; use base 'Class::Accessor'; use C4::Auth qw( check_api_auth ); +use C4::Context; use C4::Output qw( :ajax ); use CGI; +use DateTime; use JSON; our $debug; @@ -109,6 +111,35 @@ sub new { }, $class; } +=head2 test + + $service->test( $request_method, $path_info, \%params ); + +Sets up a fake CGI context for unit tests. + +=cut + +sub test { + my ( $self, $request_method, $path_info, $params ) = @_; + + $ENV{REQUEST_METHOD} = $request_method; + $ENV{PATH_INFO} = $path_info; + $ENV{HTTP_CONTENT_LENGTH} = "0"; + $self->query(CGI->new); + + foreach my $key ( keys %$params ) { + $self->query->param( $key, $params->{ $key } ); + } + + my $user = $ENV{KOHA_USER} || C4::Context->config("user"); + my $password = $ENV{KOHA_PASS} || C4::Context->config("pass"); + + $self->query->param( 'userid', $user ); + $self->query->param( 'password', $password ); + + $self->authenticate; +} + =head2 authenticate my ( $query, $cookie ) = $self->authenticate(); @@ -123,14 +154,31 @@ This must be called before the C or C methods. sub authenticate { my ( $self ) = @_; - $self->query(CGI->new); + unless ( defined( $self->auth_status ) ) { + $self->query(CGI->new) unless ( $self->query ); - my ( $status, $cookie, $sessionID ) = check_api_auth( $self->query, $self->{needed_flags} ); - $self->cookie($cookie); - $self->auth_status($status); - $self->croak( 'auth', $status ) if ( $status ne 'ok' && !$self->{authnotrequired} ); + my ( $status, $cookie, $sessionID ) = check_api_auth( $self->query, $self->{needed_flags} ); + $self->cookie($cookie); + $self->auth_status($status); + $self->handle_auth_failure() if ( $status ne 'ok' ); + } - return ( $self->query, $cookie ); + return ( $self->query, $self->cookie ); +} + +=head2 handle_auth_failure + + $self->handle_auth_failure(); + +Called when C fails (C<$self->auth_status> not 'ok'). By default, if +C<$self->{authnotrequired}> is not set, croaks and outputs an auth error. + +=cut + +sub handle_auth_failure { + my ( $self ) = @_; + + $self->croak( 'auth', $self->auth_status ) if ( !$self->{authnotrequired} ); } =head2 output @@ -157,6 +205,8 @@ is given, outputs JSONP. =cut +*DateTime::TO_JSON = sub { shift->_stringify; }; + sub output { my ( $self, $response, $options ) = @_; @@ -170,10 +220,10 @@ sub output { }; if ( $options->{type} eq 'json' ) { - $response = encode_json($response); + $response = JSON->new->convert_blessed->encode($response); if ( $self->query->param( 'callback' ) ) { - $response = $self->query->param( 'callback' ) . '(' . encode_json($response) . ');'; + $response = $self->query->param( 'callback' ) . '(' . $response . ');'; $options->{status} = '200 OK'; $options->{type} = 'js'; } diff --git a/Koha/Service/Authentication.pm b/Koha/Service/Authentication.pm index a337eba..352b391 100644 --- a/Koha/Service/Authentication.pm +++ b/Koha/Service/Authentication.pm @@ -20,6 +20,7 @@ package Koha::Service::Authentication; use Modern::Perl; +# Handles authentication and output manually, so no reason to inherit from Koha::Service::XML use base 'Koha::Service'; use C4::Auth qw/check_api_auth/; @@ -29,13 +30,15 @@ use XML::Simple; sub new { my ( $class ) = @_; - # Authentication is handled manually below return $class->SUPER::new( { - authnotrequired => 1, needed_flags => { editcatalogue => 'edit_catalogue'}, } ); } +sub handle_auth_failure { + # Stub, to allow run() to output XML itself. +} + sub run { my ( $self ) = @_; # The authentication strategy for the biblios web @@ -55,6 +58,7 @@ sub run { $self->authenticate; + # Can't reuse Koha::Service::XML, as result node has different name. $self->output( XMLout({ status => $self->auth_status }, NoAttr => 1, RootName => 'response', XMLDecl => 1), { type => 'xml' } ); } diff --git a/Koha/Service/Bib.pm b/Koha/Service/Bib.pm index 1d00f98..8f683f5 100644 --- a/Koha/Service/Bib.pm +++ b/Koha/Service/Bib.pm @@ -21,7 +21,7 @@ package Koha::Service::Bib; use Modern::Perl; -use base 'Koha::Service'; +use base 'Koha::Service::XML'; use C4::Biblio; use C4::Items; @@ -30,10 +30,8 @@ use XML::Simple; sub new { my ( $class ) = @_; - # Authentication is handled manually below return $class->SUPER::new( { - authnotrequired => 1, - needed_flags => { editcatalogue => 'edit_catalogue'}, + needed_flags => { editcatalogue => 'edit_catalogue' }, routes => [ [ qr'GET /(\d+)', 'fetch_bib' ], [ qr'POST /(\d+)', 'update_bib' ], @@ -41,26 +39,13 @@ sub new { } ); } -sub run { - my ( $self ) = @_; - - $self->authenticate; - - unless ( $self->auth_status eq "ok" ) { - $self->output( XMLout( { auth_status => $self->auth_status }, NoAttr => 1, RootName => 'response', XMLDecl => 1 ), { type => 'xml', status => '403 Forbidden' } ); - exit; - } - - $self->dispatch; -} - sub fetch_bib { my ( $self, $biblionumber ) = @_; my $record = GetMarcBiblio( $biblionumber, $self->query->url_param('items') ); if (defined $record) { - $self->output( $record->as_xml_record(), { type => 'xml' } ); + return $record->as_xml_record(); } else { $self->output( '', { status => '404 Not Found', type => 'xml' } ); } @@ -77,7 +62,6 @@ sub update_bib { my $result = {}; my $inxml = $self->query->param('POSTDATA'); - use Data::Dumper; warn Dumper($self->query); my $record = eval {MARC::Record::new_from_xml( $inxml, "utf8", C4::Context->preference('marcflavour'))}; my $do_not_escape = 0; @@ -115,7 +99,7 @@ sub update_bib { $do_not_escape = 1; } - $self->output( XMLout($result, NoAttr => 1, RootName => 'response', XMLDecl => 1, NoEscape => $do_not_escape), { type => 'xml' } ); + return XMLout($result, NoAttr => 1, RootName => 'response', XMLDecl => 1, NoEscape => $do_not_escape); } 1; diff --git a/Koha/Service/BibProfile.pm b/Koha/Service/BibProfile.pm index db74635..1fefbbf 100644 --- a/Koha/Service/BibProfile.pm +++ b/Koha/Service/BibProfile.pm @@ -22,7 +22,7 @@ package Koha::Service::BibProfile; use Modern::Perl; -use base 'Koha::Service'; +use base 'Koha::Service::XML'; use C4::Context; use C4::Koha; @@ -33,8 +33,7 @@ sub new { # Authentication is handled manually below return $class->SUPER::new( { - authnotrequired => 1, - needed_flags => { editcatalogue => 'edit_catalogue'}, + needed_flags => { editcatalogue => 'edit_catalogue' }, routes => [ [ qr'GET /(\d+)', 'fetch_bib' ], [ qr'POST /(\d+)', 'update_bib' ], @@ -47,11 +46,6 @@ sub run { $self->authenticate; - unless ( $self->auth_status eq "ok" ) { - $self->output( XMLout( { auth_status => $self->auth_status }, NoAttr => 1, RootName => 'response', XMLDecl => 1 ), { type => 'xml', status => '403 Forbidden' } ); - exit; - } - # get list of required tags my $result = {}; $result->{'auth_status'} = $self->auth_status; @@ -69,7 +63,7 @@ sub run { XMLDecl => 1, GroupTags => {mandatory_tags => 'tag', mandatory_subfields => 'subfield', reserved_tags => 'tag', valid_values => 'value'} ), - { type => 'xml', status => '403 Forbidden' } + { type => 'xml' } ); } diff --git a/Koha/Service/Config/SystemPreferences.pm b/Koha/Service/Config/SystemPreferences.pm index e3494da..ecf26fa 100644 --- a/Koha/Service/Config/SystemPreferences.pm +++ b/Koha/Service/Config/SystemPreferences.pm @@ -47,7 +47,6 @@ use C4::Log; sub new { my ( $class ) = @_; - # Authentication is handled manually below return $class->SUPER::new( { needed_flags => { parameters => 1 }, routes => [ diff --git a/Koha/Service/Patrons.pm b/Koha/Service/Patrons.pm new file mode 100644 index 0000000..ec315fb --- /dev/null +++ b/Koha/Service/Patrons.pm @@ -0,0 +1,245 @@ +#!/usr/bin/perl +package Koha::Service::Patrons; + +# This file is part of Koha. +# +# Copyright (C) 2014 ByWater Solutions +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +=head1 NAME + +svc/patrons - Web service for getting patron information + +=head1 SYNOPSIS + + GET /svc/patrons/BORROWERNUMBER + +=head1 DESCRIPTION + +This service is used to query and change patron information. + +=head1 METHODS + +=cut + +use Modern::Perl; + +use base 'Koha::Service'; + +use C4::Biblio; +use C4::Circulation; +use C4::Context; +use C4::Dates; +use C4::Items; +use C4::Members; +use C4::Reserves; +use C4::Search qw( SimpleSearch ); +use Koha::DateUtils; + +sub new { + my ( $class ) = @_; + + # Authentication is handled manually below + return $class->SUPER::new( { + needed_flags => { circulate => 'circulate_remaining_permissions' }, + routes => [ + [ qr'POST /(\d+)/checkouts', 'add_checkout'], + [ qr'GET /(\d+)/checkouts', 'get_checkouts' ], + [ qr'GET /(\d+)/holds', 'get_holds' ], + [ qr'GET /(\d+)/patronInfo', 'get_patron_info' ], + [ qr'POST /(\d+)/checkouts/(\d+(?:,\d+)*)', 'renew_checkouts', [ 'renewed' ] ], + ] + } ); +} + +=head2 add_checkout + +=over 4 + +POST /svc/patrons/BORROWERNUMBER/checkouts + +=back + +Checks out an item + +=cut + +sub add_checkout { + my ( $self, $borrowernumber ) = @_; + + my $datedue; + my $duedatespec = $self->query->param('duedate'); + + if ( C4::Context->preference('SpecifyDueDate') && $duedatespec ){ + if ($duedatespec =~ C4::Dates->regexp('syspref')) { + $datedue = dt_from_string($duedatespec); + } else { + return {errors => {INVALID_DATE=>$duedatespec}}; + } + } + + my ($barcode) = $self->require_params('barcode'); + + my $borrower = GetMember( borrowernumber => $borrowernumber ); + + my ( $errors, $questions, $alerts ) = + CanBookBeIssued( $borrower, $barcode, $datedue ); + + if ( $errors->{'UNKNOWN_BARCODE'} && C4::Context->preference("itemBarcodeFallbackSearch") ) { + my $query = "kw=" . $barcode; + my ( $searcherror, $results, $total_hits ) = SimpleSearch($query); + + # if multiple hits, offer options to librarian + if ( $total_hits > 0 ) { + my @options = (); + foreach my $hit ( @{$results} ) { + my $biblionumber = C4::Biblio::get_koha_field_from_marc( + 'biblio', + 'biblionumber', + C4::Search::new_record_from_zebra('biblioserver',$hit) + ); + + next unless ( $biblionumber ); + + # offer all items with barcodes individually + foreach my $item ( GetItemsInfo( $biblionumber ) ) { + $item->{available} = !( $item->{itemnotforloan} || $item->{onloan} || $item->{itemlost} || $item->{withdrawn} || $item->{damaged} || $item->{transfertwhen} || $item->{reservedate} ); + + push @options, $item if ( $item->{barcode} ); + } + } + + $errors->{fallback_choices} = \@options; + } + } + + if ( %$errors || ( %$questions && !$self->query->param('confirmed') ) ) { + return { + item => GetBiblioFromItemNumber( undef, $barcode ), + errors => $errors, + questions => $questions, + alerts => $alerts + }; + } + + AddIssue( $borrower, $barcode, $datedue ); + + return {}; +} + +=head2 get_holds + +=over 4 + +GET /svc/patrons/BORROWERNUMBER/holds + +=back + +Retrieves information on the holds for a patron. + +=cut + +sub get_holds { + my ( $self, $borrowernumber ) = @_; + + my @holds = GetReservesFromBorrowernumber($borrowernumber); + foreach my $hold (@holds) { + my $getiteminfo = GetBiblioFromItemNumber( $hold->{'itemnumber'} ); + $hold->{title} = $getiteminfo->{title}; + $hold->{author} = $getiteminfo->{author}; + $hold->{barcode} = $getiteminfo->{barcode}; + } + + return { holds => \@holds }; +} + +=head2 get_checkouts + +=over 4 + +GET /svc/patrons/BORROWERNUMBER/checkouts + +=back + +Retrieves information on the checkouts for a patron. + +=cut + +sub get_checkouts { + my ( $self, $borrowernumber ) = @_; + + return { checkouts => GetPendingIssues( $borrowernumber ) }; +} + +=head2 get_patron_info + +=over 4 + +GET /svc/patrons/BORROWERNUMBER/patronInfo + +=back + +Retrieves information on a patron. + +=cut + +sub get_patron_info { + my ( $self, $borrowernumber ) = @_; + + return { patronInfo => GetMemberDetails( $borrowernumber, 0 ) }; +} + +=head2 renew_checkouts + +=over 4 + +POST /svc/patrons/BORROWERNUMBER/checkouts/ITEMNUMBER,ITEMNUMBER,.../?renewed=1 + +=back + +Renews several checkouts. + +=cut + +sub renew_checkouts { + my ( $self, $borrowernumber, $itemnumbers ) = @_; + + my @items = split /,/, $itemnumbers; + + my $branch = C4::Context->userenv ? C4::Context->userenv->{'branch'} : ''; + my $datedue; + if ( $self->query->param('newduedate') ) { + $datedue = dt_from_string( $self->query->param('newduedate') ); + $datedue->set_hour(23); + $datedue->set_minute(59); + } + + my $override_limit = $self->query->param("override_limit") || 0; + my @responses; + foreach my $itemno (@items) { + # check status before renewing issue + my ( $renewokay, $error ) = + CanBookBeRenewed( $borrowernumber, $itemno, $override_limit ); + if ($renewokay) { + push @responses, { itemnumber => $itemno, datedue => AddRenewal( $borrowernumber, $itemno, $branch, $datedue ) }; + } else { + push @responses, { itemnumber => $itemno, error => $error }; + } + } + + return { responses => \@responses }; +} + +1; diff --git a/circ/checkout.pl b/circ/checkout.pl new file mode 100755 index 0000000..1260c69 --- /dev/null +++ b/circ/checkout.pl @@ -0,0 +1,98 @@ +#!/usr/bin/perl + +# This file is part of Koha. +# +# Copyright (C) 2014 ByWater Solutions +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use Modern::Perl; + +use C4::Auth; +use C4::Branch; +use C4::ClassSource; +use C4::Context; +use C4::Output; +use C4::Members; +use CGI; +use Koha::Database; + +my $query = CGI->new; + +my ( $template, $loggedinuser, $cookie ) = get_template_and_user ( + { + template_name => 'circ/checkout.tt', + query => $query, + type => "intranet", + authnotrequired => 0, + flagsrequired => { circulate => 'circulate_remaining_permissions' }, + } +); + +my $borrowernumber = $query->param( 'borrowernumber' ); +$template->{ VARS }->{ borrowernumber }=$borrowernumber; +$template->{ VARS }->{ circview }=1; +$template->param( %{ GetMemberDetails( $borrowernumber, 0 ) } ); + +my $schema = Koha::Database->new->schema; +my $authorised_values = {}; + +$authorised_values->{branches} = []; +my $onlymine=C4::Context->preference('IndependentBranches') && + C4::Context->userenv && + C4::Context->userenv->{flags} % 2 == 0 && + C4::Context->userenv->{branch}; +my $branches = GetBranches($onlymine); +foreach my $thisbranch ( sort keys %$branches ) { + push @{ $authorised_values->{branches} }, { value => $thisbranch, lib => $branches->{$thisbranch}->{'branchname'} }; +} + +$authorised_values->{itemtypes} = [ $schema->resultset( "Itemtype" )->search( undef, { + columns => [ { value => 'itemtype' }, { lib => "description" } ], + order_by => "description", + result_class => 'DBIx::Class::ResultClass::HashRefInflator' +} ) ]; + +my $class_sources = GetClassSources(); + +my $default_source = C4::Context->preference("DefaultClassificationSource"); + +foreach my $class_source (sort keys %$class_sources) { + next unless $class_sources->{$class_source}->{'used'} or + ($class_source eq $default_source); + push @{ $authorised_values->{cn_source} }, { value => $class_source, lib => $class_sources->{$class_source}->{'description'} }; +} + +my $branch_limit = C4::Context->userenv ? C4::Context->userenv->{"branch"} : ""; +my $results; +if( $branch_limit ) { + $results = $schema->resultset( "AuthorisedValue" )->search( + { "authorised_values_branches.branchcode" => { "=", [ $branch_limit, undef ] } }, + { join => "authorised_values_branches", order_by => "lib" } ); +} else { + $results = $schema->resultset( "AuthorisedValue" )->search( + undef, + { order_by => "lib" } ); +} + +foreach my $result ( $results->all ) { + $authorised_values->{$result->category} ||= []; + push @{ $authorised_values->{$result->category} }, { value => $result->authorised_value, lib => $result->lib }; +} + +$template->{VARS}->{authorised_values} = $authorised_values; + +$template->{VARS}->{authvalcode_notforloan} = C4::Koha::GetAuthValCode('items.notforloan', '' ); + +output_html_with_http_headers $query, $cookie, $template->output; diff --git a/circ/circulation.pl b/circ/circulation.pl index 982669c..9573399 100755 --- a/circ/circulation.pl +++ b/circ/circulation.pl @@ -230,6 +230,9 @@ if ($findborrower) { # get the borrower information..... my $borrower; if ($borrowernumber) { + if ( $query->cookie( 'checkout_client' ) eq 'beta' ) { + print $query->redirect( '/cgi-bin/koha/circ/checkout.pl?borrowernumber=' . $borrowernumber ); + } $borrower = GetMemberDetails( $borrowernumber, 0 ); my ( $od, $issue, $fines ) = GetMemberIssuesAndFines( $borrowernumber ); diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/doc-head-open.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/doc-head-open.inc index 23449a5..08a215f 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/includes/doc-head-open.inc +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/doc-head-open.inc @@ -1,4 +1,16 @@ -[% IF ( bidi ) %][% ELSE %][% END %] +[% IF ( bidi ) %] +[% IF ( angular_app ) %] + +[% END %] +[% ELSE %] +[% IF ( angular_app ) %] + +[% ELSE %] + +[% END %] +[% END %] diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/members-toolbar.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/members-toolbar.inc index 828c564..050a7b0 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/includes/members-toolbar.inc +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/members-toolbar.inc @@ -62,7 +62,21 @@ $(document).ready(function(){ searchToHold(); return false; }) + $("#switchclient").click( function() { + var borrowernumber = [% borrowernumber || "null" %]; + + if ( $.cookie('checkout_client' ) == 'beta' ) { + $.cookie( 'checkout_client', 'standard', { expires: 365, path: '/' } ); + window.location = '/cgi-bin/koha/circ/circulation.pl?borrowernumber=' + borrowernumber; + } + else { + $.cookie( 'checkout_client', 'beta', { expires: 365, path: '/' } ); + window.location = '/cgi-bin/koha/circ/checkout.pl?borrowernumber=' + borrowernumber; + } + return false; + } ); }); + function confirm_deletion() { var is_confirmed = window.confirm(_("Are you sure you want to delete this patron? This cannot be undone.")); if (is_confirmed) { @@ -195,6 +209,7 @@ function searchToHold(){
  • Update child to adult patron
  • [% END %]
  • Export today's checked in barcodes
  • +
  • Switch checkout client
  • diff --git a/koha-tmpl/intranet-tmpl/prog/en/js/ajax.js b/koha-tmpl/intranet-tmpl/prog/en/js/ajax.js index a8a9241..3691d3b 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/js/ajax.js +++ b/koha-tmpl/intranet-tmpl/prog/en/js/ajax.js @@ -19,14 +19,14 @@ KOHA.AJAX = { return; } - var error = eval( '(' + xhr.responseText + ')' ); + var data = eval( '(' + xhr.responseText + ')' ); - if ( error.type == 'auth' ) { + if ( data.error == 'auth' ) { humanMsg.displayMsg( MSG_SESSION_TIMED_OUT ); } if ( callback ) { - callback( error ); + callback( data ); } else { humanMsg.displayAlert( MSG_DATA_NOT_SAVED ); } diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/checkout.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/circ/checkout.tt new file mode 100644 index 0000000..2c7e160 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/circ/checkout.tt @@ -0,0 +1,945 @@ +[% USE Koha %] +[% USE Branches %] +[% USE KohaDates %] +[% IF ( export_remove_fields OR export_with_csv_profile ) %] + [% SET exports_enabled = 1 %] +[% END %] +[% USE AuthorisedValues %] +[% INCLUDE 'doc-head-open.inc' angular_app = "checkoutApp" %] +[% SET destination = "circ" %] +Koha › Circulation +[% IF borrowernumber %] + › Checking out to [% INCLUDE 'patron-title.inc' invert_name = 1 %] +[% END %] + +[% INCLUDE 'doc-head-close.inc' %] +[% INCLUDE 'calendar.inc' %] +[% IF ( UseTablesortForCirc ) %] +[% INCLUDE 'datatables.inc' %][% END %] + + +[% INCLUDE 'timepicker.inc' %] +[% INCLUDE 'doc-head-angular.inc' %] + + + + +
    +
    Loading, please wait...
    +
    + +[% INCLUDE 'header.inc' %] +[% INCLUDE 'circ-search.inc' %] + + +
    + +
    +
    +