# # OILSRedirectDepth defaults to the depth of the branch that the OPAC was directed to
# #PerlSetVar OILSRedirectDepth "0"
# #PerlSetVar OILSRedirectLocale "en-US"
+# # Use the template-toolkit opac
+# #PerlSetVar OILSRedirectTpac "true"
# allow from all
#</LocationMatch>
#SetEnv OILS_CHILIFRESH_ACCOUNT
#SetEnv OILS_CHILIFRESH_PROFILE
#SetEnv OILS_CHILIFRESH_URL http://chilifresh.com/on-site/js/evergreen.js
+ #SetEnv OILS_CHILIFRESH_HTTPS_URL https://secure.chilifresh.com/on-site/js/evergreen.js
# Specify the initial script URL for Novelist (containing account credentials, etc.)
#SetEnv OILS_NOVELIST_URL
-
+ #
# Uncomment to force SSL any time a patron is logged in. This protects
# authentication tokens. Left commented out for backwards compat for now.
#SetEnv OILS_OPAC_FORCE_LOGIN_SSL 1
-
# If set, the skin uses the combined JS file at $SKINDIR/js/combined.js
#SetEnv OILS_OPAC_COMBINED_JS 1
</Location>
-<Location /js/>
- # ----------------------------------------------------------------------------------
- # Some mod_deflate fun
- # ----------------------------------------------------------------------------------
- <IfModule mod_deflate.c>
- SetOutputFilter DEFLATE
-
- BrowserMatch ^Mozilla/4 gzip-only-text/html
- BrowserMatch ^Mozilla/4\.0[678] no-gzip
- BrowserMatch \bMSI[E] !no-gzip !gzip-only-text/html
-
- SetEnvIfNoCase Request_URI \.(?:gif|jpe?g|png)$ no-gzip dont-vary
-
- <IfModule mod_headers.c>
- Header append Vary User-Agent env=!dont-vary
- </IfModule>
- </IfModule>
-
-</Location>
-
# ----------------------------------------------------------------------------------
# Force SSL on the OPAC's "My Account" page
# ----------------------------------------------------------------------------------
Options +ExecCGI
PerlSendHeader On
allow from all
+
+ PerlSetVar OILSWebBasePath "/eg"
+ PerlSetVar OILSWebWebDir "/openils/var/web"
+ PerlSetVar OILSWebDefaultTemplateExtension "tt2"
+ PerlSetVar OILSWebDebugTemplate "true"
+
+ # -------------------------------------------------------
+ # Media Prefix. In the 3rd example, the protocol (http) is enforced
+ #PerlSetVar OILSWebMediaPrefix "/media"
+ #PerlSetVar OILSWebMediaPrefix "static.example.com/media"
+ #PerlSetVar OILSWebMediaPrefix "http://static.example.com/media"
+
+ # Locale messages files
+ PerlAddVar OILSWebLocale "en"
+ PerlAddVar OILSWebLocale "/openils/var/data/locale/messages.en.po"
+ PerlAddVar OILSWebLocale "en_ca"
+ PerlAddVar OILSWebLocale "/openils/var/data/locale/messages.en_ca.po"
+ PerlAddVar OILSWebLocale "fr_ca"
+ PerlAddVar OILSWebLocale "/openils/var/data/locale/messages.fr_ca.po"
+
+ # Templates will be loaded from the following paths in reverse order.
+ PerlAddVar OILSWebTemplatePath "/openils/var/templates"
+ PerlAddVar OILSWebTemplatePath "/openils/var/templates_localskin"
+
+ <IfModule mod_deflate.c>
+ SetOutputFilter DEFLATE
+ BrowserMatch ^Mozilla/4 gzip-only-text/html
+ BrowserMatch ^Mozilla/4\.0[678] no-gzip
+ BrowserMatch \bMSI[E] !no-gzip !gzip-only-text/html
+ SetEnvIfNoCase Request_URI \.(?:gif|jpe?g|png)$ no-gzip dont-vary
+ <IfModule mod_headers.c>
+ Header append Cache-Control "public"
+ Header append Vary User-Agent env=!dont-vary
+ </IfModule>
+ </IfModule>
+</Location>
+<LocationMatch ^/(images|css|js)/>
+ # should pick up the default expire time from eg.conf...
+ <IfModule mod_deflate.c>
+ SetOutputFilter DEFLATE
+ BrowserMatch ^Mozilla/4 gzip-only-text/html
+ BrowserMatch ^Mozilla/4\.0[678] no-gzip
+ BrowserMatch \bMSI[E] !no-gzip !gzip-only-text/html
+ SetEnvIfNoCase Request_URI \.(?:gif|jpe?g|png)$ no-gzip dont-vary
+ <IfModule mod_headers.c>
+ Header append Cache-Control "public"
+ Header append Vary User-Agent env=!dont-vary
+ </IfModule>
+ </IfModule>
+</LocationMatch>
+<Location /eg/opac>
+ PerlSetVar OILSWebContextLoader "OpenILS::WWW::EGCatLoader"
+ # Expire the HTML quickly since we're loading dynamic data for each page
+ ExpiresActive On
+ ExpiresByType text/html "access plus 5 seconds"
+
+ # For use with embedded Content Cafe content
+ #SetEnv OILS_CONTENT_CAFE_USER 123
+ #SetEnv OILS_CONTENT_CAFE_PASS 456
+ # Consider copying/moving other added content configs
+ # (e.g. NOVELIST) into here or to an outer container shared by
+ # both /opac and /eg/opac since some are used in both places
</Location>
+
+
# Note: the template processor will decline handling anything it does not
# have an explicit configuration for, which means it will fall back to
# Apache to serve the file. However, in the interest of speed, go ahead
<field name="xact" oils_persist:virtual="true" reporter:datatype="link"/>
<field name="grocery" oils_persist:virtual="true" reporter:datatype="link"/>
<field name="circulation" oils_persist:virtual="true" reporter:datatype="link"/>
+ <field name="reservation" oils_persist:virtual="true" reporter:datatype="link"/>
<field name="billing_location" reporter:datatype="link"/>
</fields>
<links>
<link field="xact" reltype="might_have" key="id" map="" class="mbt"/>
<link field="circulation" reltype="might_have" key="id" map="" class="circ"/>
<link field="grocery" reltype="might_have" key="id" map="" class="mg"/>
+ <link field="reservation" reltype="might_have" key="id" map="" class="bresv"/>
<link field="billing_location" reltype="has_a" key="id" map="" class="aou"/>
</links>
</class>
+++ /dev/null
-<oils_web>
-
- <!-- This should match the Apache Directory/Location[Match] configuration path -->
- <base_path>/eg</base_path>
-
- <web_dir>/openils/var/web</web_dir>
-
- <!-- when locating files that don't have explicit handlers defined, assume the
- files have the following filename extension -->
- <default_template_extension>tt2</default_template_extension>
-
- <!-- media_prefix can be a remote server.
- E.g. <media_prefix>http://static.example.com/media</media_prefix> -->
- <media_prefix/>
-
- <!-- If set to true, all output will be parsed as XML before delivery to the client.
- If XML parsing fails, the error message, with HTML included, will be output as text/plain.
- XML parsing adds overhead, so this should only be used for debugging -->
- <force_valid_xml>false</force_valid_xml>
-
- <!-- Where templates can be found. Paths will be checked in the order entered here.
- It's possible to override individual or sets of templates by putting them into
- a path in front of the default template path -->
- <template_paths>
- <!-- XXX we should really move these out of the default web root -->
- <path>/openils/var/web/templates</path>
- </template_paths>
-
- <handlers>
- <!-- add custom handlers here. These are for templates that live in non-obvious locations.
- In other words, if the path + default extension does not map directly to a template file -->
- <handler path='acq/fund/list' template='acq/financial/list_funds.tt2'/>
- <handler path='acq/fund/view' template='acq/financial/view_fund.tt2'/>
- <handler path='acq/funding_source/list' template='acq/financial/list_funding_sources.tt2'/>
- <handler path='acq/funding_source/view' template='acq/financial/view_funding_source.tt2'/>
- <handler path='acq/currency_type/list' template='acq/financial/list_currency_types.tt2'/>
- <handler path='acq/currency_type/view' template='acq/financial/view_currency_type.tt2'/>
- <handler path='acq/provider/list' template='acq/financial/list_providers.tt2'/>
- <handler path='acq/provider/view' template='acq/financial/view_provider.tt2'/>
- <handler path='vandelay/vandelay' template='vandelay/vandelay.tt2' as_xml='true'/>
- </handlers>
-</oils_web>
$(examples)/opensrf_core.xml.example \
$(examples)/fm_IDL.xml \
$(examples)/oils_sip.xml.example \
- $(examples)/oils_web.xml.example \
$(examples)/lib_ips.txt.example \
$(examples)/oils_yaz.xml.example \
$(examples)/oils_z3950.xml.example
ilscore-install:
@echo $@
$(MKDIR_P) $(DESTDIR)$(TEMPLATEDIR)
- cp -r @srcdir@/templates/marc $(DESTDIR)$(TEMPLATEDIR)
- cp -r @srcdir@/templates/password-reset $(DESTDIR)$(TEMPLATEDIR)
- @echo "Installing string templates to $(DESTDIR)$(TEMPLATEDIR)"
- $(MKDIR_P) $(DESTDIR)$(TEMPLATEDIR)
+ @echo "Installing templates to $(DESTDIR)$(TEMPLATEDIR)"
+ cp -r @srcdir@/templates/* $(DESTDIR)$(TEMPLATEDIR)
$(MKDIR_P) $(DESTDIR)$(datadir)/overdue/
- cp -r @srcdir@/templates/strings $(DESTDIR)$(TEMPLATEDIR)
sed -i 's|LOCALSTATEDIR|@localstatedir@|g' '$(DESTDIR)@sysconfdir@/oils_sip.xml.example'
sed -i 's|SYSCONFDIR|@sysconfdir@|g' '$(DESTDIR)@sysconfdir@/oils_sip.xml.example'
sed -i 's|LIBDIR|@libdir@|g' '$(DESTDIR)@sysconfdir@/oils_sip.xml.example'
libtext-csv-perl\
libuniversal-require-perl\
libnet-ip-perl\
+ liblocale-maketext-lexicon-perl\
libunix-syslog-perl
# Debian Lenny and Ubuntu Intrepid bundle recent versions of yaz
Business::EDI \
Library::CallNumber::LC \
Net::Z3950::Simple2ZOOM \
+ Template::Plugin::POSIX \
SRU
# More chronically unpackaged CPAN modules (available in Squeeze though)
lib/OpenILS/SIP/Transaction/Checkin.pm
lib/OpenILS/SIP/Transaction/Checkout.pm
lib/OpenILS/SIP/Transaction/Renew.pm
+lib/Template/Plugin/ResolverResolver.pm
lib/OpenILS/Template/Plugin/Unicode.pm
lib/OpenILS/Template/Plugin/WebSession.pm
lib/OpenILS/Template/Plugin/WebUtils.pm
);
sub hold_request_count {
- my( $self, $client, $login_session, $userid ) = @_;
-
- my( $user_obj, $target, $evt ) = $apputils->checkses_requestor(
- $login_session, $userid, 'VIEW_HOLD' );
- return $evt if $evt;
-
-
- my $holds = $apputils->simple_scalar_request(
- "open-ils.cstore",
- "open-ils.cstore.direct.action.hold_request.search.atomic",
- {
- usr => $userid,
- fulfillment_time => {"=" => undef },
- cancel_time => undef,
- }
- );
+ my( $self, $client, $authtoken, $user_id ) = @_;
+ my $e = new_editor(authtoken => $authtoken);
+ return $e->event unless $e->checkauth;
- my @ready;
- for my $h (@$holds) {
- next unless $h->capture_time and $h->current_copy;
+ $user_id = $e->requestor->id unless defined $user_id;
- my $copy = $apputils->simple_scalar_request(
- "open-ils.cstore",
- "open-ils.cstore.direct.asset.copy.retrieve",
- $h->current_copy
- );
+ if($e->requestor->id ne $user_id) {
+ my $user = $e->retrieve_actor_user($user_id);
+ return $e->event unless $e->allowed('VIEW_HOLD', $user->home_ou);
+ }
- if ($copy and $copy->status == 8) {
- push @ready, $h;
- }
- }
+ my $holds = $e->json_query({
+ select => {ahr => ['shelf_time']},
+ from => 'ahr',
+ where => {
+ usr => $user_id,
+ fulfillment_time => {"=" => undef },
+ cancel_time => undef,
+ }
+ });
- return { total => scalar(@$holds), ready => scalar(@ready) };
+ return {
+ total => scalar(@$holds),
+ ready => scalar(grep { $_->{shelf_time} } @$holds)
+ };
}
__PACKAGE__->register_method(
return $obj;
}
+
+# Returns "mra" attribute key/value pairs for a set of bre's
+# Takes a list of bre IDs, returns a hash of hashes,
+# {bre_id1 => {key1 => {code => value1, label => label1}, ...}...}
+my $ccvm_cache;
+sub get_bre_attrs {
+ my ($class, $bre_ids, $e) = @_;
+ $e = $e || OpenILS::Utils::CStoreEditor->new;
+
+ my $attrs = {};
+ return $attrs unless defined $bre_ids;
+ $bre_ids = [$bre_ids] unless ref $bre_ids;
+
+ my $mra = $e->json_query({
+ select => {
+ mra => [
+ {
+ column => 'id',
+ alias => 'bre'
+ }, {
+ column => 'attrs',
+ transform => 'each',
+ result_field => 'key',
+ alias => 'key'
+ },{
+ column => 'attrs',
+ transform => 'each',
+ result_field => 'value',
+ alias => 'value'
+ }
+ ]
+ },
+ from => 'mra',
+ where => {id => $bre_ids}
+ });
+
+ return $attrs unless $mra;
+
+ $ccvm_cache = $ccvm_cache || $e->search_config_coded_value_map({id => {'!=' => undef}});
+
+ for my $id (@$bre_ids) {
+ $attrs->{$id} = {};
+ for my $mra (grep { $_->{bre} eq $id } @$mra) {
+ my $ctype = $mra->{key};
+ my $code = $mra->{value};
+ $attrs->{$id}->{$ctype} = {code => $code};
+ if($code) {
+ my ($ccvm) = grep { $_->ctype eq $ctype and $_->code eq $code } @$ccvm_cache;
+ $attrs->{$id}->{$ctype}->{label} = $ccvm->value if $ccvm;
+ }
+ }
+ }
+
+ return $attrs;
+}
+
1;
'available at the library where the user is placing the hold (or, alternatively, '.
'at the pickup library) to encourage bypassing the hold placement and just ' .
'checking out the item.' ,
- params => {
+ params => [
{ desc => 'Authentication Token', type => 'string' },
{ desc => 'Method Arguments. Options include: hold_type, hold_target, org_unit. '
. 'hold_type is the hold type code (T, V, C, M, ...). '
. 'hold_target is the identifier of the hold target object. '
. 'org_unit is org unit ID.',
type => 'object'
- },
- },
+ }
+ ],
return => {
desc => q/Result hash like { "copy" : copy_id, "location" : location_name }, empty hash on misses, event on error./,
type => 'object'
--- /dev/null
+package OpenILS::WWW::EGCatLoader;
+use strict; use warnings;
+use XML::LibXML;
+use URI::Escape;
+use Digest::MD5 qw(md5_hex);
+use Apache2::Const -compile => qw(OK DECLINED FORBIDDEN HTTP_INTERNAL_SERVER_ERROR REDIRECT HTTP_BAD_REQUEST);
+use OpenSRF::AppSession;
+use OpenSRF::EX qw/:try/;
+use OpenSRF::Utils qw/:datetime/;
+use OpenSRF::Utils::JSON;
+use OpenSRF::Utils::Logger qw/$logger/;
+use OpenILS::Application::AppUtils;
+use OpenILS::Utils::CStoreEditor qw/:funcs/;
+use OpenILS::Utils::Fieldmapper;
+use DateTime::Format::ISO8601;
+use CGI qw(:all -utf8);
+
+# EGCatLoader sub-modules
+use OpenILS::WWW::EGCatLoader::Util;
+use OpenILS::WWW::EGCatLoader::Account;
+use OpenILS::WWW::EGCatLoader::Search;
+use OpenILS::WWW::EGCatLoader::Record;
+use OpenILS::WWW::EGCatLoader::Container;
+
+my $U = 'OpenILS::Application::AppUtils';
+
+use constant COOKIE_SES => 'ses';
+use constant COOKIE_ORIG_LOC => 'eg_orig_loc';
+
+sub new {
+ my($class, $apache, $ctx) = @_;
+
+ my $self = bless({}, ref($class) || $class);
+
+ $self->apache($apache);
+ $self->ctx($ctx);
+ $self->cgi(new CGI);
+
+ OpenILS::Utils::CStoreEditor->init; # just in case
+ $self->editor(new_editor());
+
+ return $self;
+}
+
+
+# current Apache2::RequestRec;
+sub apache {
+ my($self, $apache) = @_;
+ $self->{apache} = $apache if $apache;
+ return $self->{apache};
+}
+
+# runtime / template context
+sub ctx {
+ my($self, $ctx) = @_;
+ $self->{ctx} = $ctx if $ctx;
+ return $self->{ctx};
+}
+
+# cstore editor
+sub editor {
+ my($self, $editor) = @_;
+ $self->{editor} = $editor if $editor;
+ return $self->{editor};
+}
+
+# CGI handle
+sub cgi {
+ my($self, $cgi) = @_;
+ $self->{cgi} = $cgi if $cgi;
+ return $self->{cgi};
+}
+
+
+# -----------------------------------------------------------------------------
+# Perform initial setup, load common data, then load page data
+# -----------------------------------------------------------------------------
+sub load {
+ my $self = shift;
+
+ $self->init_ro_object_cache;
+
+ my $stat = $self->load_common;
+ return $stat unless $stat == Apache2::Const::OK;
+
+ my $path = $self->apache->path_info;
+
+ (undef, $self->ctx->{mylist}) = $self->fetch_mylist unless
+ $path =~ /opac\/my(opac\/lists|list)/;
+
+ return $self->load_simple("home") if $path =~ m|opac/home|;
+ return $self->load_simple("advanced") if
+ $path =~ m:opac/(advanced|numeric|expert):;
+
+ return $self->load_rresults if $path =~ m|opac/results|;
+ return $self->load_record if $path =~ m|opac/record|;
+ return $self->load_cnbrowse if $path =~ m|opac/cnbrowse|;
+
+ return $self->load_mylist_add if $path =~ m|opac/mylist/add|;
+ return $self->load_mylist_delete if $path =~ m|opac/mylist/delete|;
+ return $self->load_mylist_move if $path =~ m|opac/mylist/move|;
+ return $self->load_mylist if $path =~ m|opac/mylist|;
+ return $self->load_cache_clear if $path =~ m|opac/cache/clear|;
+
+ # ----------------------------------------------------------------
+ # Logout and login require SSL
+ # ----------------------------------------------------------------
+ if($path =~ m|opac/login|) {
+ return $self->redirect_ssl unless $self->cgi->https;
+ return $self->load_login unless $self->editor->requestor; # already logged in?
+
+ # This will be less confusing to users than to be shown a login form
+ # when they're already logged in.
+ return $self->generic_redirect(
+ sprintf(
+ "https://%s%s/myopac/main",
+ $self->apache->hostname, $self->ctx->{opac_root}
+ )
+ );
+ }
+
+ if($path =~ m|opac/logout|) {
+ #return Apache2::Const::FORBIDDEN unless $self->cgi->https;
+ $self->apache->log->warn("catloader: logout called in non-secure context from " .
+ ($self->ctx->{referer} || '<no referer>')) unless $self->cgi->https;
+ return $self->load_logout;
+ }
+
+ # ----------------------------------------------------------------
+ # Everything below here requires SSL + authentication
+ # ----------------------------------------------------------------
+ return $self->redirect_auth
+ unless $self->cgi->https and $self->editor->requestor;
+
+ return $self->load_place_hold if $path =~ m|opac/place_hold|;
+ return $self->load_myopac_holds if $path =~ m|opac/myopac/holds|;
+ return $self->load_myopac_circs if $path =~ m|opac/myopac/circs|;
+ return $self->load_myopac_payment_form if $path =~ m|opac/myopac/main_payment_form|;
+ return $self->load_myopac_payments if $path =~ m|opac/myopac/main_payments|;
+ return $self->load_myopac_pay if $path =~ m|opac/myopac/main_pay|;
+ return $self->load_myopac_main if $path =~ m|opac/myopac/main|;
+ return $self->load_myopac_receipt_email if $path =~ m|opac/myopac/receipt_email|;
+ return $self->load_myopac_receipt_print if $path =~ m|opac/myopac/receipt_print|;
+ return $self->load_myopac_update_email if $path =~ m|opac/myopac/update_email|;
+ return $self->load_myopac_update_password if $path =~ m|opac/myopac/update_password|;
+ return $self->load_myopac_update_username if $path =~ m|opac/myopac/update_username|;
+ return $self->load_myopac_bookbags if $path =~ m|opac/myopac/lists|;
+ return $self->load_myopac_bookbag_update if $path =~ m|opac/myopac/list/update|;
+ return $self->load_myopac_circ_history if $path =~ m|opac/myopac/circ_history|;
+ return $self->load_myopac_hold_history if $path =~ m|opac/myopac/hold_history|;
+ return $self->load_myopac_prefs_notify if $path =~ m|opac/myopac/prefs_notify|;
+ return $self->load_myopac_prefs_settings if $path =~ m|opac/myopac/prefs_settings|;
+ return $self->load_myopac_prefs if $path =~ m|opac/myopac/prefs|;
+
+ return Apache2::Const::OK;
+}
+
+
+# -----------------------------------------------------------------------------
+# Redirect to SSL equivalent of a given page
+# -----------------------------------------------------------------------------
+sub redirect_ssl {
+ my $self = shift;
+ my $new_page = sprintf('https://%s%s', $self->apache->hostname, $self->apache->unparsed_uri);
+ return $self->generic_redirect($new_page);
+}
+
+# -----------------------------------------------------------------------------
+# If an authnticated resource is requested w/o auth, redirect to the login page,
+# then return to the originally requrested resource upon successful login.
+# -----------------------------------------------------------------------------
+sub redirect_auth {
+ my $self = shift;
+ my $login_page = sprintf('https://%s%s/login', $self->apache->hostname, $self->ctx->{opac_root});
+ my $redirect_to = uri_escape($self->apache->unparsed_uri);
+ return $self->generic_redirect("$login_page?redirect_to=$redirect_to");
+}
+
+# -----------------------------------------------------------------------------
+# Fall-through for loading a basic page
+# -----------------------------------------------------------------------------
+sub load_simple {
+ my ($self, $page) = @_;
+ $self->ctx->{page} = $page;
+
+ if (my $patron_barcode = $self->cgi->param("patron_barcode")) {
+ # Special CGI variable from staff client; propagate henceforth as cookie
+ $self->apache->headers_out->add(
+ "Set-Cookie" => $self->cgi->cookie(
+ -name => "patron_barcode",
+ -path => "/",
+ -secure => 1,
+ -value => $patron_barcode,
+ -expires => undef
+ )
+ );
+ }
+ return Apache2::Const::OK;
+}
+
+# -----------------------------------------------------------------------------
+# Tests to see if the user is authenticated and sets some common context values
+# -----------------------------------------------------------------------------
+sub load_common {
+ my $self = shift;
+
+ my $e = $self->editor;
+ my $ctx = $self->ctx;
+
+ $ctx->{referer} = $self->cgi->referer;
+ $ctx->{path_info} = $self->cgi->path_info;
+ $ctx->{full_path} = $ctx->{base_path} . $self->cgi->path_info;
+ $ctx->{unparsed_uri} = $self->apache->unparsed_uri;
+ $ctx->{opac_root} = $ctx->{base_path} . "/opac"; # absolute base url
+ $ctx->{is_staff} = ($self->apache->headers_in->get('User-Agent') =~ /oils_xulrunner/);
+ $ctx->{orig_loc} = $self->get_orig_loc;
+
+ # capture some commonly accessed pages
+ $ctx->{home_page} = 'http://' . $self->apache->hostname . $self->ctx->{opac_root} . "/home";
+ $ctx->{logout_page} = 'https://' . $self->apache->hostname . $self->ctx->{opac_root} . "/logout";
+
+ if($e->authtoken($self->cgi->cookie(COOKIE_SES))) {
+
+ if($e->checkauth) {
+
+ $ctx->{authtoken} = $e->authtoken;
+ $ctx->{authtime} = $e->authtime;
+ $ctx->{user} = $e->requestor;
+
+ $ctx->{user_stats} = $U->simplereq(
+ 'open-ils.actor',
+ 'open-ils.actor.user.opac.vital_stats',
+ $e->authtoken, $e->requestor->id);
+
+ } else {
+
+ # if we encounter a stale authtoken, call load_logout
+ # to clean up the cookie, then redirect the user to the
+ # originally requested page
+ return $self->load_logout($self->apache->unparsed_uri);
+ }
+ }
+
+ return Apache2::Const::OK;
+}
+
+# orig_loc (i.e. "original location") passed in as a URL
+# param will replace any existing orig_loc stored as a cookie.
+sub get_orig_loc {
+ my $self = shift;
+
+ if(my $orig_loc = $self->cgi->param('orig_loc')) {
+ $self->apache->headers_out->add(
+ "Set-Cookie" => $self->cgi->cookie(
+ -name => COOKIE_ORIG_LOC,
+ -path => $self->ctx->{base_path},
+ -value => $orig_loc,
+ -expires => undef
+ )
+ );
+ return $orig_loc;
+ }
+
+ return $self->cgi->cookie(COOKIE_ORIG_LOC);
+}
+
+
+
+# -----------------------------------------------------------------------------
+# Log in and redirect to the redirect_to URL (or home)
+# -----------------------------------------------------------------------------
+sub load_login {
+ my $self = shift;
+ my $cgi = $self->cgi;
+ my $ctx = $self->ctx;
+
+ $ctx->{page} = 'login';
+
+ my $username = $cgi->param('username');
+ my $password = $cgi->param('password');
+ my $org_unit = $cgi->param('loc') || $ctx->{aou_tree}->()->id;
+ my $persist = $cgi->param('persist');
+
+ # initial log form only
+ return Apache2::Const::OK unless $username and $password;
+
+ my $seed = $U->simplereq(
+ 'open-ils.auth',
+ 'open-ils.auth.authenticate.init', $username);
+
+ my $args = {
+ username => $username,
+ password => md5_hex($seed . md5_hex($password)),
+ type => ($persist) ? 'persist' : 'opac'
+ };
+
+ my $bc_regex = $ctx->{get_org_setting}->($org_unit, 'opac.barcode_regex');
+
+ $args->{barcode} = delete $args->{username}
+ if $bc_regex and ($username =~ /$bc_regex/);
+
+ my $response = $U->simplereq(
+ 'open-ils.auth', 'open-ils.auth.authenticate.complete', $args);
+
+ if($U->event_code($response)) {
+ # login failed, report the reason to the template
+ $ctx->{login_failed_event} = $response;
+ return Apache2::Const::OK;
+ }
+
+ # login succeeded, redirect as necessary
+
+ my $acct = $self->apache->unparsed_uri;
+ $acct =~ s|/login|/myopac/main|;
+
+ return $self->generic_redirect(
+ $cgi->param('redirect_to') || $acct,
+ $cgi->cookie(
+ -name => COOKIE_SES,
+ -path => '/',
+ -secure => 1,
+ -value => $response->{payload}->{authtoken},
+ -expires => ($persist) ? CORE::time + $response->{payload}->{authtime} : undef
+ )
+ );
+}
+
+# -----------------------------------------------------------------------------
+# Log out and redirect to the home page
+# -----------------------------------------------------------------------------
+sub load_logout {
+ my $self = shift;
+ my $redirect_to = shift;
+
+ # If the user was adding anyting to an anonymous cache
+ # while logged in, go ahead and clear it out.
+ $self->clear_anon_cache;
+
+ return $self->generic_redirect(
+ $redirect_to || $self->ctx->{home_page},
+ $self->cgi->cookie(
+ -name => COOKIE_SES,
+ -path => '/',
+ -value => '',
+ -expires => '-1h'
+ )
+ );
+}
+
+1;
+
--- /dev/null
+package OpenILS::WWW::EGCatLoader;
+use strict; use warnings;
+use Apache2::Const -compile => qw(OK DECLINED FORBIDDEN HTTP_INTERNAL_SERVER_ERROR REDIRECT HTTP_BAD_REQUEST);
+use OpenSRF::Utils::Logger qw/$logger/;
+use OpenILS::Utils::CStoreEditor qw/:funcs/;
+use OpenILS::Utils::Fieldmapper;
+use OpenILS::Application::AppUtils;
+use OpenILS::Event;
+use OpenSRF::Utils::JSON;
+use Data::Dumper;
+$Data::Dumper::Indent = 0;
+use DateTime;
+my $U = 'OpenILS::Application::AppUtils';
+
+sub prepare_extended_user_info {
+ my $self = shift;
+ my @extra_flesh = @_;
+
+ $self->ctx->{user} = $self->editor->retrieve_actor_user([
+ $self->ctx->{user}->id,
+ {
+ flesh => 1,
+ flesh_fields => {
+ au => [qw/card home_ou addresses ident_type billing_address/, @extra_flesh]
+ # ...
+ }
+ }
+ ]) or return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
+
+ return;
+}
+
+# Given an event returned by a failed attempt to create a hold, do we have
+# permission to override? XXX Should the permission check be scoped to a
+# given org_unit context?
+sub test_could_override {
+ my ($self, $event) = @_;
+
+ return 0 unless $event;
+ return 1 if $self->editor->allowed($event->{textcode} . ".override");
+ return 1 if $event->{"fail_part"} and
+ $self->editor->allowed($event->{"fail_part"} . ".override");
+ return 0;
+}
+
+# Find out whether we care that local copies are available
+sub local_avail_concern {
+ my ($self, $hold_target, $hold_type, $pickup_lib) = @_;
+
+ my $would_block = $self->ctx->{get_org_setting}->
+ ($pickup_lib, "circ.holds.hold_has_copy_at.block");
+ my $would_alert = (
+ $self->ctx->{get_org_setting}->
+ ($pickup_lib, "circ.holds.hold_has_copy_at.alert") and
+ not $self->cgi->param("override")
+ ) unless $would_block;
+
+ if ($would_block or $would_alert) {
+ my $args = {
+ "hold_target" => $hold_target,
+ "hold_type" => $hold_type,
+ "org_unit" => $pickup_lib
+ };
+ my $local_avail = $U->simplereq(
+ "open-ils.circ",
+ "open-ils.circ.hold.has_copy_at", $self->editor->authtoken, $args
+ );
+ $logger->info(
+ "copy availability information for " . Dumper($args) .
+ " is " . Dumper($local_avail)
+ );
+ if (%$local_avail) { # if hash not empty
+ $self->ctx->{hold_copy_available} = $local_avail;
+ return ($would_block, $would_alert);
+ }
+ }
+
+ return (0, 0);
+}
+
+# context additions:
+# user : au object, fleshed
+sub load_myopac_prefs {
+ my $self = shift;
+ my $cgi = $self->cgi;
+ my $e = $self->editor;
+ my $pending_addr = $cgi->param('pending_addr');
+ my $replace_addr = $cgi->param('replace_addr');
+ my $delete_pending = $cgi->param('delete_pending');
+
+ $self->prepare_extended_user_info;
+ my $user = $self->ctx->{user};
+
+ return Apache2::Const::OK unless
+ $pending_addr or $replace_addr or $delete_pending;
+
+ my @form_fields = qw/address_type street1 street2 city county state country post_code/;
+
+ my $paddr;
+ if( $pending_addr ) { # update an existing pending address
+
+ ($paddr) = grep { $_->id == $pending_addr } @{$user->addresses};
+ return Apache2::Const::HTTP_BAD_REQUEST unless $paddr;
+ $paddr->$_( $cgi->param($_) ) for @form_fields;
+
+ } elsif( $replace_addr ) { # create a new pending address for 'replace_addr'
+
+ $paddr = Fieldmapper::actor::user_address->new;
+ $paddr->isnew(1);
+ $paddr->usr($user->id);
+ $paddr->pending('t');
+ $paddr->replaces($replace_addr);
+ $paddr->$_( $cgi->param($_) ) for @form_fields;
+
+ } elsif( $delete_pending ) {
+ $paddr = $e->retrieve_actor_user_address($delete_pending);
+ return Apache2::Const::HTTP_BAD_REQUEST unless
+ $paddr and $paddr->usr == $user->id and $U->is_true($paddr->pending);
+ $paddr->isdeleted(1);
+ }
+
+ my $resp = $U->simplereq(
+ 'open-ils.actor',
+ 'open-ils.actor.user.address.pending.cud',
+ $e->authtoken, $paddr);
+
+ if( $U->event_code($resp) ) {
+ $logger->error("Error updating pending address: $resp");
+ return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
+ }
+
+ # in light of these changes, re-fetch latest data
+ $e->xact_begin;
+ $self->prepare_extended_user_info;
+ $e->rollback;
+
+ return Apache2::Const::OK;
+}
+
+sub load_myopac_prefs_notify {
+ my $self = shift;
+ my $e = $self->editor;
+
+ my $user_prefs = $self->fetch_optin_prefs;
+ $user_prefs = $self->update_optin_prefs($user_prefs)
+ if $self->cgi->request_method eq 'POST';
+
+ $self->ctx->{opt_in_settings} = $user_prefs;
+
+ return Apache2::Const::OK;
+}
+
+sub fetch_optin_prefs {
+ my $self = shift;
+ my $e = $self->editor;
+
+ # fetch all of the opt-in settings the user has access to
+ # XXX: user's should in theory have options to opt-in to notices
+ # for remote locations, but that opens the door for a large
+ # set of generally un-used opt-ins.. needs discussion
+ my $opt_ins = $U->simplereq(
+ 'open-ils.actor',
+ 'open-ils.actor.event_def.opt_in.settings.atomic',
+ $e->authtoken, $e->requestor->home_ou);
+
+ # some opt-ins are staff-only
+ $opt_ins = [ grep { $U->is_true($_->opac_visible) } @$opt_ins ];
+
+ # fetch user setting values for each of the opt-in settings
+ my $user_set = $U->simplereq(
+ 'open-ils.actor',
+ 'open-ils.actor.patron.settings.retrieve',
+ $e->authtoken,
+ $e->requestor->id,
+ [map {$_->name} @$opt_ins]
+ );
+
+ return [map { {cust => $_, value => $user_set->{$_->name} } } @$opt_ins];
+}
+
+sub update_optin_prefs {
+ my $self = shift;
+ my $user_prefs = shift;
+ my $e = $self->editor;
+ my @settings = $self->cgi->param('setting');
+ my %newsets;
+
+ # apply now-true settings
+ for my $applied (@settings) {
+ # see if setting is already applied to this user
+ next if grep { $_->{cust}->name eq $applied and $_->{value} } @$user_prefs;
+ $newsets{$applied} = OpenSRF::Utils::JSON->true;
+ }
+
+ # remove now-false settings
+ for my $pref (grep { $_->{value} } @$user_prefs) {
+ $newsets{$pref->{cust}->name} = undef
+ unless grep { $_ eq $pref->{cust}->name } @settings;
+ }
+
+ $U->simplereq(
+ 'open-ils.actor',
+ 'open-ils.actor.patron.settings.update',
+ $e->authtoken, $e->requestor->id, \%newsets);
+
+ # update the local prefs to match reality
+ for my $pref (@$user_prefs) {
+ $pref->{value} = $newsets{$pref->{cust}->name}
+ if exists $newsets{$pref->{cust}->name};
+ }
+
+ return $user_prefs;
+}
+
+sub _load_user_with_prefs {
+ my $self = shift;
+ my $stat = $self->prepare_extended_user_info('settings');
+ return $stat if $stat; # not-OK
+
+ $self->ctx->{user_setting_map} = {
+ map { $_->name => OpenSRF::Utils::JSON->JSON2perl($_->value) }
+ @{$self->ctx->{user}->settings}
+ };
+
+ return undef;
+}
+
+sub load_myopac_prefs_settings {
+ my $self = shift;
+
+ my $stat = $self->_load_user_with_prefs;
+ return $stat if $stat;
+
+ return Apache2::Const::OK
+ unless $self->cgi->request_method eq 'POST';
+
+ # some setting values from the form don't match the
+ # required value/format for the db, so they have to be
+ # individually translated.
+
+ my %settings;
+ my $set_map = $self->ctx->{user_setting_map};
+
+ my $key = 'opac.hits_per_page';
+ my $val = $self->cgi->param($key);
+ $settings{$key}= $val unless $$set_map{$key} eq $val;
+
+ my $now = DateTime->now->strftime('%F');
+ for $key (qw/history.circ.retention_start history.hold.retention_start/) {
+ $val = $self->cgi->param($key);
+ if($val and $val eq 'on') {
+ # Set the start time to 'now' unless a start time already exists for the user
+ $settings{$key} = $now unless $$set_map{$key};
+ } else {
+ # clear the start time if one previously existed for the user
+ $settings{$key} = undef if $$set_map{$key};
+ }
+ }
+
+ # Send the modified settings off to be saved
+ $U->simplereq(
+ 'open-ils.actor',
+ 'open-ils.actor.patron.settings.update',
+ $self->editor->authtoken, undef, \%settings);
+
+ # re-fetch user prefs
+ $self->ctx->{updated_user_settings} = \%settings;
+ return $self->_load_user_with_prefs || Apache2::Const::OK;
+}
+
+sub fetch_user_holds {
+ my $self = shift;
+ my $hold_ids = shift;
+ my $ids_only = shift;
+ my $flesh = shift;
+ my $available = shift;
+ my $limit = shift;
+ my $offset = shift;
+
+ my $e = $self->editor;
+
+ if(!$hold_ids) {
+ my $circ = OpenSRF::AppSession->create('open-ils.circ');
+
+ $hold_ids = $circ->request(
+ 'open-ils.circ.holds.id_list.retrieve.authoritative',
+ $e->authtoken,
+ $e->requestor->id
+ )->gather(1);
+ $circ->kill_me;
+
+ $hold_ids = [ grep { defined $_ } @$hold_ids[$offset..($offset + $limit - 1)] ] if $limit or $offset;
+ }
+
+
+ return $hold_ids if $ids_only or @$hold_ids == 0;
+
+ my $args = {
+ suppress_notices => 1,
+ suppress_transits => 1,
+ suppress_mvr => 1,
+ suppress_patron_details => 1,
+ include_bre => $flesh ? 1 : 0
+ };
+
+ # ----------------------------------------------------------------
+ # Collect holds in batches of $batch_size for faster retrieval
+
+ my $batch_size = 8;
+ my $batch_idx = 0;
+ my $mk_req_batch = sub {
+ my @ses;
+ my $top_idx = $batch_idx + $batch_size;
+ while($batch_idx < $top_idx) {
+ my $hold_id = $hold_ids->[$batch_idx++];
+ last unless $hold_id;
+ my $ses = OpenSRF::AppSession->create('open-ils.circ');
+ my $req = $ses->request(
+ 'open-ils.circ.hold.details.retrieve',
+ $e->authtoken, $hold_id, $args);
+ push(@ses, {ses => $ses, req => $req});
+ }
+ return @ses;
+ };
+
+ my $first = 1;
+ my(@collected, @holds, @ses);
+
+ while(1) {
+ @ses = $mk_req_batch->() if $first;
+ last if $first and not @ses;
+
+ if(@collected) {
+ # If desired by the caller, filter any holds that are not available.
+ if ($available) {
+ @collected = grep { $_->{hold}->{status} == 4 } @collected;
+ }
+ while(my $blob = pop(@collected)) {
+ $blob->{marc_xml} = XML::LibXML->new->parse_string($blob->{hold}->{bre}->marc) if $flesh;
+ push(@holds, $blob);
+ }
+ }
+
+ for my $req_data (@ses) {
+ push(@collected, {hold => $req_data->{req}->gather(1)});
+ $req_data->{ses}->kill_me;
+ }
+
+ @ses = $mk_req_batch->();
+ last unless @collected or @ses;
+ $first = 0;
+ }
+
+ # put the holds back into the original server sort order
+ my @sorted;
+ for my $id (@$hold_ids) {
+ push @sorted, grep { $_->{hold}->{hold}->id == $id } @holds;
+ }
+
+ return \@sorted;
+}
+
+sub handle_hold_update {
+ my $self = shift;
+ my $action = shift;
+ my $hold_ids = shift;
+ my $e = $self->editor;
+ my $url;
+
+ my @hold_ids = ($hold_ids) ? @$hold_ids : $self->cgi->param('hold_id'); # for non-_all actions
+ @hold_ids = @{$self->fetch_user_holds(undef, 1)} if $action =~ /_all/;
+
+ my $circ = OpenSRF::AppSession->create('open-ils.circ');
+
+ if($action =~ /cancel/) {
+
+ for my $hold_id (@hold_ids) {
+ my $resp = $circ->request(
+ 'open-ils.circ.hold.cancel', $e->authtoken, $hold_id, 6 )->gather(1); # 6 == patron-cancelled-via-opac
+ }
+
+ } elsif ($action =~ /activate|suspend/) {
+
+ my $vlist = [];
+ for my $hold_id (@hold_ids) {
+ my $vals = {id => $hold_id};
+
+ if($action =~ /activate/) {
+ $vals->{frozen} = 'f';
+ $vals->{thaw_date} = undef;
+
+ } elsif($action =~ /suspend/) {
+ $vals->{frozen} = 't';
+ # $vals->{thaw_date} = TODO;
+ }
+ push(@$vlist, $vals);
+ }
+
+ $circ->request('open-ils.circ.hold.update.batch.atomic', $e->authtoken, undef, $vlist)->gather(1);
+ } elsif ($action eq 'edit') {
+
+ my @vals = map {
+ my $val = {"id" => $_};
+ $val->{"frozen"} = $self->cgi->param("frozen");
+ $val->{"pickup_lib"} = $self->cgi->param("pickup_lib");
+
+ for my $field (qw/expire_time thaw_date/) {
+ # XXX TODO make this support other date formats, not just
+ # MM/DD/YYYY.
+ next unless $self->cgi->param($field) =~
+ m:^(\d{2})/(\d{2})/(\d{4})$:;
+ $val->{$field} = "$3-$1-$2";
+ }
+ $val;
+ } @hold_ids;
+
+ $circ->request(
+ 'open-ils.circ.hold.update.batch.atomic',
+ $e->authtoken, undef, \@vals
+ )->gather(1); # LFW XXX test for failure
+ $url = 'https://' . $self->apache->hostname . $self->ctx->{opac_root} . '/myopac/holds';
+ }
+
+ $circ->kill_me;
+ return defined($url) ? $self->generic_redirect($url) : undef;
+}
+
+sub load_myopac_holds {
+ my $self = shift;
+ my $e = $self->editor;
+ my $ctx = $self->ctx;
+
+ my $limit = $self->cgi->param('limit') || 0;
+ my $offset = $self->cgi->param('offset') || 0;
+ my $action = $self->cgi->param('action') || '';
+ my $hold_id = $self->cgi->param('id');
+ my $available = int($self->cgi->param('available') || 0);
+
+ my $hold_handle_result;
+ $hold_handle_result = $self->handle_hold_update($action) if $action;
+
+ $ctx->{holds} = $self->fetch_user_holds($hold_id ? [$hold_id] : undef, 0, 1, $available, $limit, $offset);
+
+ return defined($hold_handle_result) ? $hold_handle_result : Apache2::Const::OK;
+}
+
+sub load_place_hold {
+ my $self = shift;
+ my $ctx = $self->ctx;
+ my $gos = $ctx->{get_org_setting};
+ my $e = $self->editor;
+ my $cgi = $self->cgi;
+
+ $self->ctx->{page} = 'place_hold';
+ my @targets = $cgi->param('hold_target');
+ $ctx->{hold_type} = $cgi->param('hold_type');
+ $ctx->{default_pickup_lib} = $e->requestor->home_ou; # unless changed below
+
+ return $self->post_hold_redirect unless @targets;
+
+ $logger->info("Looking at hold targets: @targets");
+
+ # if the staff client provides a patron barcode, fetch the patron
+ if (my $bc = $self->cgi->cookie("patron_barcode")) {
+ $ctx->{patron_recipient} = $U->simplereq(
+ "open-ils.actor", "open-ils.actor.user.fleshed.retrieve_by_barcode",
+ $self->editor->authtoken, $bc
+ ) or return Apache2::Const::HTTP_BAD_REQUEST;
+
+ $ctx->{default_pickup_lib} = $ctx->{patron_recipient}->home_ou;
+ }
+
+ my $request_lib = $e->requestor->ws_ou;
+ my @hold_data;
+ $ctx->{hold_data} = \@hold_data;
+
+ my $type_dispatch = {
+ T => sub {
+ my $recs = $e->batch_retrieve_biblio_record_entry(\@targets, {substream => 1});
+ for my $id (@targets) { # force back into the correct order
+ my ($rec) = grep {$_->id eq $id} @$recs;
+ push(@hold_data, {target => $rec, record => $rec});
+ }
+ },
+ V => sub {
+ my $vols = $e->batch_retrieve_asset_call_number([
+ \@targets, {
+ "flesh" => 1,
+ "flesh_fields" => {"acn" => ["record"]}
+ }
+ ], {substream => 1});
+
+ for my $id (@targets) {
+ my ($vol) = grep {$_->id eq $id} @$vols;
+ push(@hold_data, {target => $vol, record => $vol->record});
+ }
+ },
+ C => sub {
+ my $copies = $e->batch_retrieve_asset_copy([
+ \@targets, {
+ "flesh" => 2,
+ "flesh_fields" => {
+ "acn" => ["record"],
+ "acp" => ["call_number"]
+ }
+ }
+ ], {substream => 1});
+
+ for my $id (@targets) {
+ my ($copy) = grep {$_->id eq $id} @$copies;
+ push(@hold_data, {target => $copy, record => $copy->call_number->record});
+ }
+ },
+ I => sub {
+ my $isses = $e->batch_retrieve_serial_issuance([
+ \@targets, {
+ "flesh" => 2,
+ "flesh_fields" => {
+ "siss" => ["subscription"], "ssub" => ["record_entry"]
+ }
+ }
+ ], {substream => 1});
+
+ for my $id (@targets) {
+ my ($iss) = grep {$_->id eq $id} @$isses;
+ push(@hold_data, {target => $iss, record => $iss->subscription->record_entry});
+ }
+ }
+ # ...
+
+ }->{$ctx->{hold_type}}->();
+
+ # caller sent bad target IDs or the wrong hold type
+ return Apache2::Const::HTTP_BAD_REQUEST unless @hold_data;
+
+ # generate the MARC xml for each record
+ $_->{marc_xml} = XML::LibXML->new->parse_string($_->{record}->marc) for @hold_data;
+
+ my $pickup_lib = $cgi->param('pickup_lib');
+ # no pickup lib means no holds placement
+ return Apache2::Const::OK unless $pickup_lib;
+
+ $ctx->{hold_attempt_made} = 1;
+
+ # Give the original CGI params back to the user in case they
+ # want to try to override something.
+ $ctx->{orig_params} = $cgi->Vars;
+ delete $ctx->{orig_params}{submit};
+ delete $ctx->{orig_params}{hold_target};
+
+ my $usr = $e->requestor->id;
+
+ if ($ctx->{is_staff} and !$cgi->param("hold_usr_is_requestor")) {
+ # find the real hold target
+
+ $usr = $U->simplereq(
+ 'open-ils.actor',
+ "open-ils.actor.user.retrieve_id_by_barcode_or_username",
+ $e->authtoken, $cgi->param("hold_usr"));
+
+ if (defined $U->event_code($usr)) {
+ $ctx->{hold_failed} = 1;
+ $ctx->{hold_failed_event} = $usr;
+ }
+ }
+
+ # First see if we should warn/block for any holds that
+ # might have locally available items.
+ for my $hdata (@hold_data) {
+ my ($local_block, $local_alert) = $self->local_avail_concern(
+ $hdata->{target}->id, $ctx->{hold_type}, $pickup_lib);
+
+ if ($local_block) {
+ $hdata->{hold_failed} = 1;
+ $hdata->{hold_local_block} = 1;
+ } elsif ($local_alert) {
+ $hdata->{hold_failed} = 1;
+ $hdata->{hold_local_alert} = 1;
+ }
+ }
+
+
+ my $method = 'open-ils.circ.holds.test_and_create.batch';
+ $method .= '.override' if $cgi->param('override');
+
+ my @create_targets = map {$_->{target}->id} (grep { !$_->{hold_failed} } @hold_data);
+
+ if(@create_targets) {
+
+ my $bses = OpenSRF::AppSession->create('open-ils.circ');
+ my $breq = $bses->request(
+ $method,
+ $e->authtoken,
+ { patronid => $usr,
+ pickup_lib => $pickup_lib,
+ hold_type => $ctx->{hold_type}
+ },
+ \@create_targets
+ );
+
+ while (my $resp = $breq->recv) {
+
+ $resp = $resp->content;
+ $logger->info('batch hold placement result: ' . OpenSRF::Utils::JSON->perl2JSON($resp));
+
+ if ($U->event_code($resp)) {
+ $ctx->{general_hold_error} = $resp;
+ last;
+ }
+
+ my ($hdata) = grep {$_->{target}->id eq $resp->{target}} @hold_data;
+ my $result = $resp->{result};
+
+ if ($U->event_code($result)) {
+ # e.g. permission denied
+ $hdata->{hold_failed} = 1;
+ $hdata->{hold_failed_event} = $result;
+
+ } else {
+
+ if(not ref $result and $result > 0) {
+ # successul hold returns the hold ID
+
+ $hdata->{hold_success} = $result;
+
+ } else {
+ # hold-specific failure event
+ $hdata->{hold_failed} = 1;
+ $hdata->{hold_failed_event} = $result->{last_event};
+ $hdata->{could_override} = $self->test_could_override($hdata->{hold_failed_event});
+ }
+ }
+ }
+
+ $bses->kill_me;
+ }
+
+ # stay on the current page and display the results
+ return Apache2::Const::OK if
+ (grep {$_->{hold_failed}} @hold_data) or $ctx->{general_hold_error};
+
+ # if successful, do some cleanup and return the
+ # user to the requesting page.
+
+ return $self->post_hold_redirect;
+}
+
+sub post_hold_redirect {
+ my $self = shift;
+
+ # XXX: Leave the barcode cookie in place. Otherwise, it's not
+ # possible to place more than one hold for the patron within
+ # a staff/patron session. This does leave the barcode to linger
+ # longer than is ideal, but normal staff work flow will cause the
+ # cookie to be replaced with each new patron anyway.
+ # TODO: See about getting the staff client to clear the cookie
+ return $self->generic_redirect;
+
+ # We also clear the patron_barcode (from the staff client)
+ # cookie at this point (otherwise it haunts the staff user
+ # later). XXX todo make sure this is best; also see that
+ # template when staff mode calls xulG.opac_hold_placed()
+
+ return $self->generic_redirect(
+ undef,
+ $self->cgi->cookie(
+ -name => "patron_barcode",
+ -path => "/",
+ -secure => 1,
+ -value => "",
+ -expires => "-1h"
+ )
+ );
+}
+
+
+sub fetch_user_circs {
+ my $self = shift;
+ my $flesh = shift; # flesh bib data, etc.
+ my $circ_ids = shift;
+ my $limit = shift;
+ my $offset = shift;
+
+ my $e = $self->editor;
+
+ my @circ_ids;
+
+ if($circ_ids) {
+ @circ_ids = @$circ_ids;
+
+ } else {
+
+ my $circ_data = $U->simplereq(
+ 'open-ils.actor',
+ 'open-ils.actor.user.checked_out',
+ $e->authtoken,
+ $e->requestor->id
+ );
+
+ @circ_ids = ( @{$circ_data->{overdue}}, @{$circ_data->{out}} );
+
+ if($limit or $offset) {
+ @circ_ids = grep { defined $_ } @circ_ids[0..($offset + $limit - 1)];
+ }
+ }
+
+ return [] unless @circ_ids;
+
+ my $qflesh = {
+ flesh => 3,
+ flesh_fields => {
+ circ => ['target_copy'],
+ acp => ['call_number'],
+ acn => ['record']
+ }
+ };
+
+ $e->xact_begin;
+ my $circs = $e->search_action_circulation(
+ [{id => \@circ_ids}, ($flesh) ? $qflesh : {}], {substream => 1});
+
+ my @circs;
+ for my $circ (@$circs) {
+ push(@circs, {
+ circ => $circ,
+ marc_xml => ($flesh and $circ->target_copy->call_number->id != -1) ?
+ XML::LibXML->new->parse_string($circ->target_copy->call_number->record->marc) :
+ undef # pre-cat copy, use the dummy title/author instead
+ });
+ }
+ $e->xact_rollback;
+
+ # make sure the final list is in the correct order
+ my @sorted_circs;
+ for my $id (@circ_ids) {
+ push(
+ @sorted_circs,
+ (grep { $_->{circ}->id == $id } @circs)
+ );
+ }
+
+ return \@sorted_circs;
+}
+
+
+sub handle_circ_renew {
+ my $self = shift;
+ my $action = shift;
+ my $ctx = $self->ctx;
+
+ my @renew_ids = $self->cgi->param('circ');
+
+ my $circs = $self->fetch_user_circs(0, ($action eq 'renew') ? [@renew_ids] : undef);
+
+ # TODO: fire off renewal calls in batches to speed things up
+ my @responses;
+ for my $circ (@$circs) {
+
+ my $evt = $U->simplereq(
+ 'open-ils.circ',
+ 'open-ils.circ.renew',
+ $self->editor->authtoken,
+ {
+ patron_id => $self->editor->requestor->id,
+ copy_id => $circ->{circ}->target_copy,
+ opac_renewal => 1
+ }
+ );
+
+ # TODO return these, then insert them into the circ data
+ # blob that is shoved into the template for each circ
+ # so the template won't have to match them
+ push(@responses, {copy => $circ->{circ}->target_copy, evt => $evt});
+ }
+
+ return @responses;
+}
+
+
+sub load_myopac_circs {
+ my $self = shift;
+ my $e = $self->editor;
+ my $ctx = $self->ctx;
+
+ $ctx->{circs} = [];
+ my $limit = $self->cgi->param('limit') || 0; # 0 == unlimited
+ my $offset = $self->cgi->param('offset') || 0;
+ my $action = $self->cgi->param('action') || '';
+
+ # perform the renewal first if necessary
+ my @results = $self->handle_circ_renew($action) if $action =~ /renew/;
+
+ $ctx->{circs} = $self->fetch_user_circs(1, undef, $limit, $offset);
+
+ my $success_renewals = 0;
+ my $failed_renewals = 0;
+ for my $data (@{$ctx->{circs}}) {
+ my ($resp) = grep { $_->{copy} == $data->{circ}->target_copy->id } @results;
+
+ if($resp) {
+ my $evt = ref($resp->{evt}) eq 'ARRAY' ? $resp->{evt}->[0] : $resp->{evt};
+ $data->{renewal_response} = $evt;
+ $success_renewals++ if $evt->{textcode} eq 'SUCCESS';
+ $failed_renewals++ if $evt->{textcode} ne 'SUCCESS';
+ }
+ }
+
+ $ctx->{success_renewals} = $success_renewals;
+ $ctx->{failed_renewals} = $failed_renewals;
+
+ return Apache2::Const::OK;
+}
+
+sub load_myopac_circ_history {
+ my $self = shift;
+ my $e = $self->editor;
+ my $ctx = $self->ctx;
+ my $limit = $self->cgi->param('limit') || 15;
+ my $offset = $self->cgi->param('offset') || 0;
+
+ $ctx->{circ_history_limit} = $limit;
+ $ctx->{circ_history_offset} = $offset;
+
+ my $circ_ids = $e->json_query({
+ select => {
+ au => [{
+ column => 'id',
+ transform => 'action.usr_visible_circs',
+ result_field => 'id'
+ }]
+ },
+ from => 'au',
+ where => {id => $e->requestor->id},
+ limit => $limit,
+ offset => $offset
+ });
+
+ $ctx->{circs} = $self->fetch_user_circs(1, [map { $_->{id} } @$circ_ids]);
+ return Apache2::Const::OK;
+}
+
+# TODO: action.usr_visible_holds does not return cancelled holds. Should it?
+sub load_myopac_hold_history {
+ my $self = shift;
+ my $e = $self->editor;
+ my $ctx = $self->ctx;
+ my $limit = $self->cgi->param('limit') || 15;
+ my $offset = $self->cgi->param('offset') || 0;
+ $ctx->{hold_history_limit} = $limit;
+ $ctx->{hold_history_offset} = $offset;
+
+ my $hold_ids = $e->json_query({
+ select => {
+ au => [{
+ column => 'id',
+ transform => 'action.usr_visible_holds',
+ result_field => 'id'
+ }]
+ },
+ from => 'au',
+ where => {id => $e->requestor->id},
+ limit => $limit,
+ offset => $offset
+ });
+
+ $ctx->{holds} = $self->fetch_user_holds([map { $_->{id} } @$hold_ids], 0, 1, 0);
+ return Apache2::Const::OK;
+}
+
+sub load_myopac_payment_form {
+ my $self = shift;
+ my $r;
+
+ $r = $self->prepare_fines(undef, undef, [$self->cgi->param('xact'), $self->cgi->param('xact_misc')]) and return $r;
+ $r = $self->prepare_extended_user_info and return $r;
+
+ return Apache2::Const::OK;
+}
+
+# TODO: add other filter options as params/configs/etc.
+sub load_myopac_payments {
+ my $self = shift;
+ my $limit = $self->cgi->param('limit') || 20;
+ my $offset = $self->cgi->param('offset') || 0;
+ my $e = $self->editor;
+
+ $self->ctx->{payment_history_limit} = $limit;
+ $self->ctx->{payment_history_offset} = $offset;
+
+ my $args = {};
+ $args->{limit} = $limit if $limit;
+ $args->{offset} = $offset if $offset;
+
+ if (my $max_age = $self->ctx->{get_org_setting}->(
+ $e->requestor->home_ou, "opac.payment_history_age_limit"
+ )) {
+ my $min_ts = DateTime->now(
+ "time_zone" => DateTime::TimeZone->new("name" => "local"),
+ )->subtract("seconds" => interval_to_seconds($max_age))->iso8601();
+
+ $logger->info("XXX min_ts: $min_ts");
+ $args->{"where"} = {"payment_ts" => {">=" => $min_ts}};
+ }
+
+ $self->ctx->{payments} = $U->simplereq(
+ 'open-ils.actor',
+ 'open-ils.actor.user.payments.retrieve.atomic',
+ $e->authtoken, $e->requestor->id, $args);
+
+ return Apache2::Const::OK;
+}
+
+sub load_myopac_pay {
+ my $self = shift;
+ my $r;
+
+ $r = $self->prepare_fines(undef, undef, [$self->cgi->param('xact'), $self->cgi->param('xact_misc')]) and
+ return $r;
+
+ # balance_owed is computed specifically from the fines we're trying
+ # to pay in this case.
+ if ($self->ctx->{fines}->{balance_owed} <= 0) {
+ $self->apache->log->info(
+ sprintf("Can't pay non-positive balance. xacts selected: (%s)",
+ join(", ", map(int, $self->cgi->param("xact"), $self->cgi->param('xact_misc'))))
+ );
+ return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
+ }
+
+ my $cc_args = {"where_process" => 1};
+
+ $cc_args->{$_} = $self->cgi->param($_) for (qw/
+ number cvv2 expire_year expire_month billing_first
+ billing_last billing_address billing_city billing_state
+ billing_zip
+ /);
+
+ my $args = {
+ "cc_args" => $cc_args,
+ "userid" => $self->ctx->{user}->id,
+ "payment_type" => "credit_card_payment",
+ "payments" => $self->prepare_fines_for_payment # should be safe after self->prepare_fines
+ };
+
+ my $resp = $U->simplereq("open-ils.circ", "open-ils.circ.money.payment",
+ $self->editor->authtoken, $args, $self->ctx->{user}->last_xact_id
+ );
+
+ $self->ctx->{"payment_response"} = $resp;
+
+ unless ($resp->{"textcode"}) {
+ $self->ctx->{printable_receipt} = $U->simplereq(
+ "open-ils.circ", "open-ils.circ.money.payment_receipt.print",
+ $self->editor->authtoken, $resp->{payments}
+ );
+ }
+
+ return Apache2::Const::OK;
+}
+
+sub load_myopac_receipt_print {
+ my $self = shift;
+
+ $self->ctx->{printable_receipt} = $U->simplereq(
+ "open-ils.circ", "open-ils.circ.money.payment_receipt.print",
+ $self->editor->authtoken, [$self->cgi->param("payment")]
+ );
+
+ return Apache2::Const::OK;
+}
+
+sub load_myopac_receipt_email {
+ my $self = shift;
+
+ # The following ML method doesn't actually check whether the user in
+ # question has an email address, so we do.
+ if ($self->ctx->{user}->email) {
+ $self->ctx->{email_receipt_result} = $U->simplereq(
+ "open-ils.circ", "open-ils.circ.money.payment_receipt.email",
+ $self->editor->authtoken, [$self->cgi->param("payment")]
+ );
+ } else {
+ $self->ctx->{email_receipt_result} =
+ new OpenILS::Event("PATRON_NO_EMAIL_ADDRESS");
+ }
+
+ return Apache2::Const::OK;
+}
+
+sub prepare_fines {
+ my ($self, $limit, $offset, $id_list) = @_;
+
+ # XXX TODO: check for failure after various network calls
+
+ # It may be unclear, but this result structure lumps circulation and
+ # reservation fines together, and keeps grocery fines separate.
+ $self->ctx->{"fines"} = {
+ "circulation" => [],
+ "grocery" => [],
+ "total_paid" => 0,
+ "total_owed" => 0,
+ "balance_owed" => 0
+ };
+
+ my $cstore = OpenSRF::AppSession->create('open-ils.cstore');
+
+ # TODO: This should really be a ML call, but the existing calls
+ # return an excessive amount of data and don't offer streaming
+
+ my %paging = ($limit or $offset) ? (limit => $limit, offset => $offset) : ();
+
+ my $req = $cstore->request(
+ 'open-ils.cstore.direct.money.open_billable_transaction_summary.search',
+ {
+ usr => $self->editor->requestor->id,
+ balance_owed => {'!=' => 0},
+ ($id_list && @$id_list ? ("id" => $id_list) : ()),
+ },
+ {
+ flesh => 4,
+ flesh_fields => {
+ mobts => [qw/grocery circulation reservation/],
+ bresv => ['target_resource_type'],
+ brt => ['record'],
+ mg => ['billings'],
+ mb => ['btype'],
+ circ => ['target_copy'],
+ acp => ['call_number'],
+ acn => ['record']
+ },
+ order_by => { mobts => 'xact_start' },
+ %paging
+ }
+ );
+
+ my @total_keys = qw/total_paid total_owed balance_owed/;
+ $self->ctx->{"fines"}->{@total_keys} = (0, 0, 0);
+
+ while(my $resp = $req->recv) {
+ my $mobts = $resp->content;
+ my $circ = $mobts->circulation;
+
+ my $last_billing;
+ if($mobts->grocery) {
+ my @billings = sort { $a->billing_ts cmp $b->billing_ts } @{$mobts->grocery->billings};
+ $last_billing = pop(@billings);
+ }
+
+ # XXX TODO confirm that the following, and the later division by 100.0
+ # to get a floating point representation once again, is sufficiently
+ # "money-safe" math.
+ $self->ctx->{"fines"}->{$_} += int($mobts->$_ * 100) for (@total_keys);
+
+ my $marc_xml = undef;
+ if ($mobts->xact_type eq 'reservation' and
+ $mobts->reservation->target_resource_type->record) {
+ $marc_xml = XML::LibXML->new->parse_string(
+ $mobts->reservation->target_resource_type->record->marc
+ );
+ } elsif ($mobts->xact_type eq 'circulation' and
+ $circ->target_copy->call_number->id != -1) {
+ $marc_xml = XML::LibXML->new->parse_string(
+ $circ->target_copy->call_number->record->marc
+ );
+ }
+
+ push(
+ @{$self->ctx->{"fines"}->{$mobts->grocery ? "grocery" : "circulation"}},
+ {
+ xact => $mobts,
+ last_grocery_billing => $last_billing,
+ marc_xml => $marc_xml
+ }
+ );
+ }
+
+ $cstore->kill_me;
+
+ $self->ctx->{"fines"}->{$_} /= 100.0 for (@total_keys);
+ return;
+}
+
+sub prepare_fines_for_payment {
+ # This assumes $self->prepare_fines has already been run
+ my ($self) = @_;
+
+ my @results = ();
+ if ($self->ctx->{fines}) {
+ push @results, [$_->{xact}->id, $_->{xact}->balance_owed] foreach (
+ @{$self->ctx->{fines}->{circulation}},
+ @{$self->ctx->{fines}->{grocery}}
+ );
+ }
+
+ return \@results;
+}
+
+sub load_myopac_main {
+ my $self = shift;
+ my $limit = $self->cgi->param('limit') || 0;
+ my $offset = $self->cgi->param('offset') || 0;
+
+ return $self->prepare_fines($limit, $offset) || Apache2::Const::OK;
+}
+
+sub load_myopac_update_email {
+ my $self = shift;
+ my $e = $self->editor;
+ my $ctx = $self->ctx;
+ my $email = $self->cgi->param('email') || '';
+
+ # needed for most up-to-date email address
+ if (my $r = $self->prepare_extended_user_info) { return $r };
+
+ return Apache2::Const::OK
+ unless $self->cgi->request_method eq 'POST';
+
+ unless($email =~ /.+\@.+\..+/) { # TODO better regex?
+ $ctx->{invalid_email} = $email;
+ return Apache2::Const::OK;
+ }
+
+ my $stat = $U->simplereq(
+ 'open-ils.actor',
+ 'open-ils.actor.user.email.update',
+ $e->authtoken, $email);
+
+ unless ($self->cgi->param("redirect_to")) {
+ my $url = $self->apache->unparsed_uri;
+ $url =~ s/update_email/prefs/;
+
+ return $self->generic_redirect($url);
+ }
+
+ return $self->generic_redirect;
+}
+
+sub load_myopac_update_username {
+ my $self = shift;
+ my $e = $self->editor;
+ my $ctx = $self->ctx;
+ my $username = $self->cgi->param('username') || '';
+
+ return Apache2::Const::OK
+ unless $self->cgi->request_method eq 'POST';
+
+ unless($username and $username !~ /\s/) { # any other username restrictions?
+ $ctx->{invalid_username} = $username;
+ return Apache2::Const::OK;
+ }
+
+ if($username ne $e->requestor->usrname) {
+
+ my $evt = $U->simplereq(
+ 'open-ils.actor',
+ 'open-ils.actor.user.username.update',
+ $e->authtoken, $username);
+
+ if($U->event_equals($evt, 'USERNAME_EXISTS')) {
+ $ctx->{username_exists} = $username;
+ return Apache2::Const::OK;
+ }
+ }
+
+ my $url = $self->apache->unparsed_uri;
+ $url =~ s/update_username/prefs/;
+
+ return $self->generic_redirect($url);
+}
+
+sub load_myopac_update_password {
+ my $self = shift;
+ my $e = $self->editor;
+ my $ctx = $self->ctx;
+
+ return Apache2::Const::OK
+ unless $self->cgi->request_method eq 'POST';
+
+ my $current_pw = $self->cgi->param('current_pw') || '';
+ my $new_pw = $self->cgi->param('new_pw') || '';
+ my $new_pw2 = $self->cgi->param('new_pw2') || '';
+
+ unless($new_pw eq $new_pw2) {
+ $ctx->{password_nomatch} = 1;
+ return Apache2::Const::OK;
+ }
+
+ my $pw_regex = $ctx->{get_org_setting}->($e->requestor->home_ou, 'global.password_regex');
+
+ if($pw_regex and $new_pw !~ /$pw_regex/) {
+ $ctx->{password_invalid} = 1;
+ return Apache2::Const::OK;
+ }
+
+ my $evt = $U->simplereq(
+ 'open-ils.actor',
+ 'open-ils.actor.user.password.update',
+ $e->authtoken, $new_pw, $current_pw);
+
+
+ if($U->event_equals($evt, 'INCORRECT_PASSWORD')) {
+ $ctx->{password_incorrect} = 1;
+ return Apache2::Const::OK;
+ }
+
+ my $url = $self->apache->unparsed_uri;
+ $url =~ s/update_password/prefs/;
+
+ return $self->generic_redirect($url);
+}
+
+sub load_myopac_bookbags {
+ my $self = shift;
+ my $e = $self->editor;
+ my $ctx = $self->ctx;
+
+ $e->xact_begin; # replication...
+
+ my $rv = $self->load_mylist;
+ unless($rv eq Apache2::Const::OK) {
+ $e->rollback;
+ return $rv;
+ }
+
+ my $args = {
+ order_by => {cbreb => 'name'},
+ limit => $self->cgi->param('limit') || 10,
+ offset => $self->cgi->param('offset') || 0
+ };
+
+ $ctx->{bookbags} = $e->search_container_biblio_record_entry_bucket(
+ [
+ {owner => $self->editor->requestor->id, btype => 'bookbag'},
+ {"flesh" => 1, "flesh_fields" => {"cbreb" => ["items"]}, %$args}
+ ],
+ {substream => 1}
+ );
+
+ if(!$ctx->{bookbags}) {
+ $e->rollback;
+ return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
+ }
+
+ # get unique record IDs
+ my %rec_ids = ();
+ foreach my $bbag (@{$ctx->{bookbags}}) {
+ foreach my $rec_id (
+ map { $_->target_biblio_record_entry } @{$bbag->items}
+ ) {
+ $rec_ids{$rec_id} = 1;
+ }
+ }
+
+ $ctx->{bookbags_marc_xml} = $self->fetch_marc_xml_by_id([keys %rec_ids]);
+
+ $e->rollback;
+ return Apache2::Const::OK;
+}
+
+
+# actions are create, delete, show, hide, rename, add_rec, delete_item, place_hold
+# CGI is action, list=list_id, add_rec/record=bre_id, del_item=bucket_item_id, name=new_bucket_name
+sub load_myopac_bookbag_update {
+ my ($self, $action, $list_id, @hold_recs) = @_;
+ my $e = $self->editor;
+ my $cgi = $self->cgi;
+
+ $action ||= $cgi->param('action');
+ $list_id ||= $cgi->param('list');
+
+ my @add_rec = $cgi->param('add_rec') || $cgi->param('record');
+ my @selected_item = $cgi->param('selected_item');
+ my $shared = $cgi->param('shared');
+ my $name = $cgi->param('name');
+ my $success = 0;
+ my $list;
+
+ if($action eq 'create') {
+ $list = Fieldmapper::container::biblio_record_entry_bucket->new;
+ $list->name($name);
+ $list->owner($e->requestor->id);
+ $list->btype('bookbag');
+ $list->pub($shared ? 't' : 'f');
+ $success = $U->simplereq('open-ils.actor',
+ 'open-ils.actor.container.create', $e->authtoken, 'biblio', $list)
+
+ } elsif($action eq 'place_hold') {
+
+ # @hold_recs comes from anon lists redirect; selected_itesm comes from existing buckets
+ unless (@hold_recs) {
+ if (@selected_item) {
+ my $items = $e->search_container_biblio_record_entry_bucket_item({id => \@selected_item});
+ @hold_recs = map { $_->target_biblio_record_entry } @$items;
+ }
+ }
+
+ return Apache2::Const::OK unless @hold_recs;
+ $logger->info("placing holds from list page on: @hold_recs");
+
+ my $url = $self->ctx->{opac_root} . '/place_hold?hold_type=T';
+ $url .= ';hold_target=' . $_ for @hold_recs;
+ return $self->generic_redirect($url);
+
+ } else {
+
+ $list = $e->retrieve_container_biblio_record_entry_bucket($list_id);
+
+ return Apache2::Const::HTTP_BAD_REQUEST unless
+ $list and $list->owner == $e->requestor->id;
+ }
+
+ if($action eq 'delete') {
+ $success = $U->simplereq('open-ils.actor',
+ 'open-ils.actor.container.full_delete', $e->authtoken, 'biblio', $list_id);
+
+ } elsif($action eq 'show') {
+ unless($U->is_true($list->pub)) {
+ $list->pub('t');
+ $success = $U->simplereq('open-ils.actor',
+ 'open-ils.actor.container.update', $e->authtoken, 'biblio', $list);
+ }
+
+ } elsif($action eq 'hide') {
+ if($U->is_true($list->pub)) {
+ $list->pub('f');
+ $success = $U->simplereq('open-ils.actor',
+ 'open-ils.actor.container.update', $e->authtoken, 'biblio', $list);
+ }
+
+ } elsif($action eq 'rename') {
+ if($name) {
+ $list->name($name);
+ $success = $U->simplereq('open-ils.actor',
+ 'open-ils.actor.container.update', $e->authtoken, 'biblio', $list);
+ }
+
+ } elsif($action eq 'add_rec') {
+ foreach my $add_rec (@add_rec) {
+ my $item = Fieldmapper::container::biblio_record_entry_bucket_item->new;
+ $item->bucket($list_id);
+ $item->target_biblio_record_entry($add_rec);
+ $success = $U->simplereq('open-ils.actor',
+ 'open-ils.actor.container.item.create', $e->authtoken, 'biblio', $item);
+ last unless $success;
+ }
+
+ } elsif($action eq 'del_item') {
+ foreach (@selected_item) {
+ $success = $U->simplereq(
+ 'open-ils.actor',
+ 'open-ils.actor.container.item.delete', $e->authtoken, 'biblio', $_
+ );
+ last unless $success;
+ }
+ }
+
+ return $self->generic_redirect if $success;
+
+ $self->ctx->{bucket_action} = $action;
+ $self->ctx->{bucket_action_failed} = 1;
+ return Apache2::Const::OK;
+}
+
+1
--- /dev/null
+package OpenILS::WWW::EGCatLoader;
+use strict; use warnings;
+use Apache2::Const -compile => qw(OK DECLINED FORBIDDEN HTTP_INTERNAL_SERVER_ERROR REDIRECT HTTP_BAD_REQUEST);
+use OpenSRF::Utils::Logger qw/$logger/;
+use OpenILS::Utils::CStoreEditor qw/:funcs/;
+use OpenILS::Utils::Fieldmapper;
+use OpenILS::Application::AppUtils;
+my $U = 'OpenILS::Application::AppUtils';
+
+use constant COOKIE_ANON_CACHE => 'anoncache';
+use constant ANON_CACHE_MYLIST => 'mylist';
+
+# Retrieve the users cached records AKA 'My List'
+# Returns an empty list if there are no cached records
+sub fetch_mylist {
+ my ($self, $with_marc_xml) = @_;
+
+ my $list = [];
+ my $cache_key = $self->cgi->cookie(COOKIE_ANON_CACHE);
+
+ if($cache_key) {
+
+ $list = $U->simplereq(
+ 'open-ils.actor',
+ 'open-ils.actor.anon_cache.get_value',
+ $cache_key, ANON_CACHE_MYLIST);
+
+ if(!$list) {
+ $cache_key = undef;
+ $list = [];
+ }
+ }
+
+ my $marc_xml;
+ if ($with_marc_xml) {
+ $marc_xml = $self->fetch_marc_xml_by_id($list);
+ }
+
+ return ($cache_key, $list, $marc_xml);
+}
+
+
+# Adds a record (by id) to My List, creating a new anon cache + list if necessary.
+sub load_mylist_add {
+ my $self = shift;
+ my $rec_id = $self->cgi->param('record');
+
+ my ($cache_key, $list) = $self->fetch_mylist;
+ push(@$list, $rec_id);
+
+ $cache_key = $U->simplereq(
+ 'open-ils.actor',
+ 'open-ils.actor.anon_cache.set_value',
+ $cache_key, ANON_CACHE_MYLIST, $list);
+
+ return $self->mylist_action_redirect($cache_key);
+}
+
+sub load_mylist_delete {
+ my $self = shift;
+ my $rec_id = $self->cgi->param('record');
+
+ my ($cache_key, $list) = $self->fetch_mylist;
+ $list = [ grep { $_ ne $rec_id } @$list ];
+
+ $cache_key = $U->simplereq(
+ 'open-ils.actor',
+ 'open-ils.actor.anon_cache.set_value',
+ $cache_key, ANON_CACHE_MYLIST, $list);
+
+ return $self->mylist_action_redirect($cache_key);
+}
+
+sub load_mylist_move {
+ my $self = shift;
+ my @rec_ids = $self->cgi->param('record');
+ my $action = $self->cgi->param('action') || '';
+
+ return $self->load_myopac_bookbag_update('place_hold', undef, @rec_ids)
+ if $action eq 'place_hold';
+
+ my ($cache_key, $list) = $self->fetch_mylist;
+ return $self->mylist_action_redirect unless $cache_key;
+
+ my @keep;
+ foreach my $id (@$list) { push(@keep, $id) unless grep { $id eq $_ } @rec_ids; }
+
+ $cache_key = $U->simplereq(
+ 'open-ils.actor',
+ 'open-ils.actor.anon_cache.set_value',
+ $cache_key, ANON_CACHE_MYLIST, \@keep
+ );
+
+ if ($self->ctx->{user} and $action =~ /^\d+$/) {
+ # in this case, action becomes list_id
+ $self->load_myopac_bookbag_update('add_rec', $self->cgi->param('action'));
+ # XXX TODO: test for failure of above
+ }
+
+ return $self->mylist_action_redirect($cache_key);
+}
+
+sub load_cache_clear {
+ my $self = shift;
+ $self->clear_anon_cache;
+ return $self->mylist_action_redirect;
+}
+
+# Wipes the entire anonymous cache, including My List
+sub clear_anon_cache {
+ my $self = shift;
+ my $field = shift;
+
+ my $cache_key = $self->cgi->cookie(COOKIE_ANON_CACHE) or return;
+
+ $U->simplereq(
+ 'open-ils.actor',
+ 'open-ils.actor.anon_cache.delete_session', $cache_key)
+ if $cache_key;
+
+}
+
+# Called after an anon-cache / My List action occurs. Redirect
+# to the redirect_url (cgi param) or referrer or home.
+sub mylist_action_redirect {
+ my $self = shift;
+ my $cache_key = shift;
+
+ my $url;
+ if( my $anchor = $self->cgi->param('anchor') ) {
+ # on the results page, we want to redirect
+ # back to record that was affected
+ $url = $self->ctx->{referer};
+ $url =~ s/#.*|$/#$anchor/g;
+ }
+
+ return $self->generic_redirect(
+ $url,
+ $self->cgi->cookie(
+ -name => COOKIE_ANON_CACHE,
+ -path => '/',
+ -value => ($cache_key) ? $cache_key : '',
+ -expires => ($cache_key) ? undef : '-1h'
+ )
+ );
+}
+
+sub load_mylist {
+ my ($self) = shift;
+ (undef, $self->ctx->{mylist}, $self->ctx->{mylist_marc_xml}) =
+ $self->fetch_mylist(1);
+
+ return Apache2::Const::OK;
+}
+
+1;
--- /dev/null
+package OpenILS::WWW::EGCatLoader;
+use strict; use warnings;
+use Apache2::Const -compile => qw(OK DECLINED FORBIDDEN HTTP_INTERNAL_SERVER_ERROR REDIRECT HTTP_BAD_REQUEST);
+use OpenSRF::Utils::Logger qw/$logger/;
+use OpenILS::Utils::CStoreEditor qw/:funcs/;
+use OpenILS::Utils::Fieldmapper;
+use OpenILS::Application::AppUtils;
+my $U = 'OpenILS::Application::AppUtils';
+
+# context additions:
+# record : bre object
+sub load_record {
+ my $self = shift;
+ my $ctx = $self->ctx;
+ $ctx->{page} = 'record';
+
+ my $org = $self->cgi->param('loc') || $ctx->{aou_tree}->()->id;
+ my $depth = $self->cgi->param('depth') || 0;
+ my $copy_limit = int($self->cgi->param('copy_limit') || 10);
+ my $copy_offset = int($self->cgi->param('copy_offset') || 0);
+
+ my $rec_id = $ctx->{page_args}->[0]
+ or return Apache2::Const::HTTP_BAD_REQUEST;
+
+ # run copy retrieval in parallel to bib retrieval
+ # XXX unapi
+ my $cstore = OpenSRF::AppSession->create('open-ils.cstore');
+ my $copy_rec = $cstore->request(
+ 'open-ils.cstore.json_query.atomic',
+ $self->mk_copy_query($rec_id, $org, $depth, $copy_limit, $copy_offset)
+ );
+
+ my (undef, @rec_data) = $self->get_records_and_facets([$rec_id], undef, {flesh => '{holdings_xml,mra,acp}'});
+ $ctx->{bre_id} = $rec_data[0]->{id};
+ $ctx->{marc_xml} = $rec_data[0]->{marc_xml};
+
+ $ctx->{copies} = $copy_rec->gather(1);
+ $ctx->{copy_limit} = $copy_limit;
+ $ctx->{copy_offset} = $copy_offset;
+
+ $ctx->{have_holdings_to_show} = 0;
+ $self->get_hold_copy_summary($rec_id, $org);
+
+ $cstore->kill_me;
+
+ # XXX TODO we'll also need conditional logic to show MFHD-based holdings
+ if (
+ $ctx->{get_org_setting}->
+ ($org, "opac.fully_compressed_serial_holdings")
+ ) {
+ $ctx->{holding_summaries} =
+ $self->get_holding_summaries($rec_id, $org, $depth);
+
+ $ctx->{have_holdings_to_show} =
+ scalar(@{$ctx->{holding_summaries}->{basic}}) ||
+ scalar(@{$ctx->{holding_summaries}->{index}}) ||
+ scalar(@{$ctx->{holding_summaries}->{supplement}});
+ }
+
+ my %expandies = (
+ marchtml => sub {
+ $ctx->{marchtml} = $self->mk_marc_html($rec_id);
+ },
+ issues => sub {
+ $ctx->{expanded_holdings} =
+ $self->get_expanded_holdings($rec_id, $org, $depth)
+ if $ctx->{have_holdings_to_show};
+ },
+ cnbrowse => sub {
+ $self->prepare_browse_call_numbers();
+ }
+ );
+
+ my @expand = $self->cgi->param('expand');
+ if (grep {$_ eq 'all'} @expand) {
+ $ctx->{expand_all} = 1;
+ $expandies{$_}->() for keys %expandies;
+
+ } else {
+ for my $exp (@expand) {
+ $ctx->{"expand_$exp"} = 1;
+ $expandies{$exp}->() if exists $expandies{$exp};
+ }
+ }
+
+ return Apache2::Const::OK;
+}
+
+sub mk_copy_query {
+ my $self = shift;
+ my $rec_id = shift;
+ my $org = shift;
+ my $depth = shift;
+ my $copy_limit = shift;
+ my $copy_offset = shift;
+
+ my $query = {
+ select => {
+ acp => ['id', 'barcode', 'circ_lib', 'create_date', 'age_protect', 'holdable'],
+ acpl => [
+ {column => 'name', alias => 'copy_location'},
+ {column => 'holdable', alias => 'location_holdable'}
+ ],
+ ccs => [
+ {column => 'name', alias => 'copy_status'},
+ {column => 'holdable', alias => 'status_holdable'}
+ ],
+ acn => [
+ {column => 'label', alias => 'call_number_label'},
+ {column => 'id', alias => 'call_number'}
+ ],
+ circ => ['due_date'],
+ },
+
+ from => {
+ acp => {
+ acn => {
+ join => {bre => {filter => {id => $rec_id }}},
+ filter => {deleted => 'f'}
+ },
+ circ => { # If the copy is circulating, retrieve the open circ
+ type => 'left',
+ filter => {checkin_time => undef}
+ },
+ acpl => {},
+ ccs => {},
+ aou => {}
+ }
+ },
+
+ where => {'+acp' => {deleted => 'f' }},
+
+ order_by => [
+ {class => 'aou', field => 'name'},
+ {class => 'acn', field => 'label'}
+ ],
+
+ limit => $copy_limit,
+ offset => $copy_offset
+ };
+
+ # XXX In the future, $sort_org should be understood to be an abstration
+ # that refers to something configurable, not necessariyl orig_loc.
+
+ if (my $sort_org = $self->ctx->{orig_loc}) {
+ unshift @{$query->{order_by}}, {
+ class => 'acp', field => 'circ_lib', transform => 'numeric_eq',
+ params => [$sort_org], direction => 'desc'
+ };
+ }
+
+ if($org != $self->ctx->{aou_tree}->()->id) {
+ # no need to add the org join filter if we're not actually filtering
+ $query->{from}->{acp}->{aou} = {
+ fkey => 'circ_lib',
+ field => 'id',
+ filter => {
+ id => {
+ in => {
+ select => {aou => [{
+ column => 'id',
+ transform => 'actor.org_unit_descendants',
+ result_field => 'id',
+ params => [$depth]
+ }]},
+ from => 'aou',
+ where => {id => $org}
+ }
+ }
+ }
+ }
+ };
+
+ # Filter hidden items if this is the public catalog
+ unless($self->ctx->{is_staff}) {
+ $query->{where}->{'+acp'}->{opac_visible} = 't';
+ $query->{from}->{'acp'}->{'acpl'}->{filter} = {opac_visible => 't'};
+ $query->{from}->{'acp'}->{'ccs'}->{filter} = {opac_visible => 't'};
+ }
+
+ return $query;
+}
+
+sub mk_marc_html {
+ my($self, $rec_id) = @_;
+
+ # could be optimized considerably by performing the xslt on the already fetched record
+ return $U->simplereq(
+ 'open-ils.search',
+ 'open-ils.search.biblio.record.html', $rec_id, 1);
+}
+
+sub get_holding_summaries {
+ my ($self, $rec_id, $org, $depth) = @_;
+
+ my $serial = create OpenSRF::AppSession("open-ils.serial");
+ my $result = $serial->request(
+ "open-ils.serial.bib.summary_statements",
+ $rec_id, {"org_id" => $org, "depth" => $depth}
+ )->gather(1);
+
+ $serial->kill_me;
+ return $result;
+}
+
+sub get_expanded_holdings {
+ my ($self, $rec_id, $org, $depth) = @_;
+
+ my $holding_limit = int($self->cgi->param("holding_limit") || 10);
+ my $holding_offset = int($self->cgi->param("holding_offset") || 0);
+ my $type = $self->cgi->param("expand_holding_type");
+
+ my $serial = create OpenSRF::AppSession("open-ils.serial");
+ my $result = $serial->request(
+ "open-ils.serial.received_siss.retrieve.by_bib.atomic",
+ $rec_id, {
+ "ou" => $org, "depth" => $depth,
+ "limit" => $holding_limit, "offset" => $holding_offset,
+ "type" => $type
+ }
+ )->gather(1);
+
+ $serial->kill_me;
+ return $result;
+}
+
+sub any_call_number_label {
+ my ($self) = @_;
+
+ if ($self->ctx->{copies} and @{$self->ctx->{copies}}) {
+ return $self->ctx->{copies}->[0]->{call_number_label};
+ } else {
+ return;
+ }
+}
+
+sub prepare_browse_call_numbers {
+ my ($self) = @_;
+
+ my $cn = ($self->cgi->param("cn") || $self->any_call_number_label) or
+ return [];
+
+ my $org_unit = $self->ctx->{get_aou}->($self->cgi->param('loc')) ||
+ $self->ctx->{aou_tree}->();
+
+ my $supercat = create OpenSRF::AppSession("open-ils.supercat");
+ my $results = $supercat->request(
+ "open-ils.supercat.call_number.browse",
+ $cn, $org_unit->shortname, 9, $self->cgi->param("cnoffset")
+ )->gather(1) || [];
+
+ $supercat->kill_me;
+
+ $self->ctx->{browsed_call_numbers} = [
+ map {
+ $_->record->marc(
+ (new XML::LibXML)->parse_string($_->record->marc)
+ );
+ $_;
+ } @$results
+ ];
+ $self->ctx->{browsing_ou} = $org_unit;
+}
+
+sub get_hold_copy_summary {
+ my ($self, $rec_id, $org) = @_;
+
+ my $search = OpenSRF::AppSession->create('open-ils.search');
+ my $req1 = $search->request(
+ 'open-ils.search.biblio.record.copy_count', $org, $rec_id);
+
+ $self->ctx->{record_hold_count} = $U->simplereq(
+ 'open-ils.circ', 'open-ils.circ.bre.holds.count', $rec_id);
+
+ $self->ctx->{copy_summary} = $req1->recv->content;
+
+ $search->kill_me;
+}
+
+1;
--- /dev/null
+package OpenILS::WWW::EGCatLoader;
+use strict; use warnings;
+use Apache2::Const -compile => qw(OK DECLINED FORBIDDEN HTTP_INTERNAL_SERVER_ERROR REDIRECT HTTP_BAD_REQUEST);
+use OpenSRF::Utils::Logger qw/$logger/;
+use OpenILS::Utils::CStoreEditor qw/:funcs/;
+use OpenILS::Utils::Fieldmapper;
+use OpenILS::Application::AppUtils;
+use OpenSRF::Utils::JSON;
+use Data::Dumper;
+$Data::Dumper::Indent = 0;
+my $U = 'OpenILS::Application::AppUtils';
+
+
+sub _prepare_biblio_search_basics {
+ my ($cgi) = @_;
+
+ return $cgi->param('query') unless $cgi->param('qtype');
+
+ my %parts;
+ my @part_names = qw/qtype contains query bool/;
+ $parts{$_} = [ $cgi->param($_) ] for (@part_names);
+
+ my $full_query = '';
+ for (my $i = 0; $i < scalar @{$parts{'qtype'}}; $i++) {
+ my ($qtype, $contains, $query, $bool) = map { $parts{$_}->[$i] } @part_names;
+
+ next unless $query =~ /\S/;
+
+ # This stuff probably will need refined or rethought to better handle
+ # the weird things Real Users will surely type in.
+ $contains = "" unless defined $contains; # silence warning
+ if ($contains eq 'nocontains') {
+ $query =~ s/"//g;
+ $query = ('"' . $query . '"') if index $query, ' ';
+ $query = '-' . $query;
+ } elsif ($contains eq 'phrase') {
+ $query =~ s/"//g;
+ $query = ('"' . $query . '"') if index $query, ' ';
+ } elsif ($contains eq 'exact') {
+ $query =~ s/[\^\$]//g;
+ $query = '^' . $query . '$';
+ }
+ $query = "$qtype:$query" unless $qtype eq 'keyword' and $i == 0;
+
+ $bool = ($bool and $bool eq 'or') ? '||' : '&&';
+ $full_query = $full_query ? "($full_query $bool $query)" : $query;
+ }
+
+ return $full_query;
+}
+
+sub _prepare_biblio_search {
+ my ($cgi, $ctx) = @_;
+
+ my $query = _prepare_biblio_search_basics($cgi) || '';
+
+ foreach ($cgi->param('modifier')) {
+ # The unless bit is to avoid stacking modifiers.
+ $query = ('#' . $_ . ' ' . $query) unless $query =~ qr/\#\Q$_/;
+ }
+
+ # filters
+ foreach (grep /^fi:/, $cgi->param) {
+ /:(-?\w+)$/ or next;
+ my $term = join(",", $cgi->param($_));
+ $query .= " $1($term)" if length $term;
+ }
+
+ # sort is treated specially, even though it's actually a filter
+ if ($cgi->param('sort')) {
+ $query =~ s/sort\([^\)]*\)//g; # override existing sort(). no stacking.
+ my ($axis, $desc) = split /\./, $cgi->param('sort');
+ $query .= " sort($axis)";
+ if ($desc and not $query =~ /\#descending/) {
+ $query .= '#descending';
+ } elsif (not $desc) {
+ $query =~ s/\#descending//;
+ }
+ }
+
+ if ($cgi->param('pubdate') && $cgi->param('date1')) {
+ if ($cgi->param('pubdate') eq 'between') {
+ $query .= ' between(' . $cgi->param('date1');
+ $query .= ',' . $cgi->param('date2') if $cgi->param('date2');
+ $query .= ')';
+ } elsif ($cgi->param('pubdate') eq 'is') {
+ $query .= ' between(' . $cgi->param('date1') .
+ ',' . $cgi->param('date1') . ')'; # sic, date1 twice
+ } else {
+ $query .= ' ' . $cgi->param('pubdate') .
+ '(' . $cgi->param('date1') . ')';
+ }
+ }
+
+ my $site;
+ my $org = $cgi->param('loc');
+ if (defined($org) and $org ne '' and ($org ne $ctx->{aou_tree}->()->id) and not $query =~ /site\(\S+\)/) {
+ $site = $ctx->{get_aou}->($org)->shortname;
+ $query .= " site($site)";
+ }
+
+ if(!$site) {
+ ($site) = ($query =~ /site\(([^\)]+)\)/);
+ $site ||= $ctx->{aou_tree}->()->shortname;
+ }
+
+
+ my $depth;
+ if (defined($cgi->param('depth')) and not $query =~ /depth\(\d+\)/) {
+ $depth = defined $cgi->param('depth') ?
+ $cgi->param('depth') : $ctx->{get_aou}->($site)->ou_type->depth;
+ $query .= " depth($depth)";
+ }
+
+ return ($query, $site, $depth);
+}
+
+sub _get_search_limit {
+ my $self = shift;
+
+ # param takes precedence
+ my $limit = $self->cgi->param('limit');
+ return $limit if $limit;
+
+ if($self->editor->requestor) {
+ # See if the user has a hit count preference
+ my $lset = $self->editor->search_actor_user_setting({
+ usr => $self->editor->requestor->id,
+ name => 'opac.hits_per_page'
+ })->[0];
+ return OpenSRF::Utils::JSON->JSON2perl($lset->value) if $lset;
+ }
+
+ return 10; # default
+}
+
+# context additions:
+# page_size
+# hit_count
+# records : list of bre's and copy-count objects
+sub load_rresults {
+ my $self = shift;
+ my $cgi = $self->cgi;
+ my $ctx = $self->ctx;
+ my $e = $self->editor;
+
+ $ctx->{page} = 'rresult';
+
+ # Special alternative searches here. This could all stand to be cleaner.
+ if ($cgi->param("_special")) {
+ return $self->marc_expert_search if scalar($cgi->param("tag"));
+ return $self->item_barcode_shortcut if (
+ $cgi->param("qtype") and ($cgi->param("qtype") eq "item_barcode")
+ );
+ return $self->call_number_browse_standalone if (
+ $cgi->param("qtype") and ($cgi->param("qtype") eq "cnbrowse")
+ );
+ }
+
+ my $page = $cgi->param('page') || 0;
+ my $facet = $cgi->param('facet');
+ my $limit = $self->_get_search_limit;
+ my $loc = $cgi->param('loc') || $ctx->{aou_tree}->()->id;
+ my $offset = $page * $limit;
+ my $metarecord = $cgi->param('metarecord');
+ my $results;
+
+ my ($query, $site, $depth) = _prepare_biblio_search($cgi, $ctx);
+
+ if ($metarecord) {
+
+ # TODO: other limits, like SVF/format, etc.
+ $results = $U->simplereq(
+ 'open-ils.search',
+ 'open-ils.search.biblio.metarecord_to_records',
+ $metarecord, {org => $loc, depth => $depth}
+ );
+
+ # force the metarecord result blob to match the format of regular search results
+ $results->{ids} = [map { [$_] } @{$results->{ids}}];
+
+ } else {
+
+ return $self->generic_redirect unless $query;
+
+ # Limit and offset will stay here. Everything else should be part of
+ # the query string, not special args.
+ my $args = {'limit' => $limit, 'offset' => $offset};
+
+ # Stuff these into the TT context so that templates can use them in redrawing forms
+ $ctx->{processed_search_query} = $query;
+
+ $query = "$query $facet" if $facet; # TODO
+
+ $logger->activity("EGWeb: [search] $query");
+
+ try {
+
+ my $method = 'open-ils.search.biblio.multiclass.query';
+ $method .= '.staff' if $ctx->{is_staff};
+ $results = $U->simplereq('open-ils.search', $method, $args, $query, 1);
+
+ } catch Error with {
+ my $err = shift;
+ $logger->error("multiclass search error: $err");
+ $results = {count => 0, ids => []};
+ };
+ }
+
+ my $rec_ids = [map { $_->[0] } @{$results->{ids}}];
+
+ $ctx->{records} = [];
+ $ctx->{search_facets} = {};
+ $ctx->{page_size} = $limit;
+ $ctx->{hit_count} = $results->{count};
+ $ctx->{parsed_query} = $results->{parsed_query};
+
+ return Apache2::Const::OK if @$rec_ids == 0;
+
+ my ($facets, @data) = $self->get_records_and_facets(
+ $rec_ids, $results->{facet_key},
+ {
+ flesh => '{holdings_xml,mra,acp}',
+ site => $site,
+ depth => $depth
+ }
+ );
+
+ # shove recs into context in search results order
+ for my $rec_id (@$rec_ids) {
+ push(
+ @{$ctx->{records}},
+ grep { $_->{id} == $rec_id } @data
+ );
+ }
+
+ $ctx->{search_facets} = $facets;
+
+ return Apache2::Const::OK;
+}
+
+# Searching by barcode is a special search that does /not/ respect any other
+# of the usual search parameters, not even the ones for sorting and paging!
+sub item_barcode_shortcut {
+ my ($self) = @_;
+
+ my $method = "open-ils.search.multi_home.bib_ids.by_barcode";
+ if (my $search = create OpenSRF::AppSession("open-ils.search")) {
+ my $rec_ids = $search->request(
+ $method, $self->cgi->param("query")
+ )->gather(1);
+ $search->kill_me;
+
+ if (ref $rec_ids ne 'ARRAY') {
+
+ if($U->event_equals($rec_ids, 'ASSET_COPY_NOT_FOUND')) {
+ $rec_ids = [];
+
+ } else {
+ if (defined $U->event_code($rec_ids)) {
+ $self->apache->log->warn(
+ "$method returned event: " . $U->event_code($rec_ids)
+ );
+ } else {
+ $self->apache->log->warn(
+ "$method returned something unexpected: $rec_ids"
+ );
+ }
+ return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
+ }
+ }
+
+ my ($facets, @data) = $self->get_records_and_facets(
+ $rec_ids, undef, {flesh => "{holdings_xml,mra}"}
+ );
+
+ $self->ctx->{records} = [@data];
+ $self->ctx->{search_facets} = {};
+ $self->ctx->{hit_count} = scalar @data;
+ $self->ctx->{page_size} = $self->ctx->{hit_count};
+
+ return Apache2::Const::OK;
+ } {
+ $self->apache->log->warn("couldn't connect to open-ils.search");
+ return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
+ }
+}
+
+# like item_barcode_search, this can't take all the usual search params, but
+# this one will at least do site, limit and page
+sub marc_expert_search {
+ my ($self) = @_;
+
+ my @tags = $self->cgi->param("tag");
+ my @subfields = $self->cgi->param("subfield");
+ my @terms = $self->cgi->param("term");
+
+ my $query = [];
+ for (my $i = 0; $i < scalar @tags; $i++) {
+ next if ($tags[$i] eq "" || $terms[$i] eq "");
+ $subfields[$i] = '_' unless $subfields[$i];
+ push @$query, {
+ "term" => $terms[$i],
+ "restrict" => [{"tag" => $tags[$i], "subfield" => $subfields[$i]}]
+ };
+ }
+
+ $logger->info("query for expert search: " . Dumper($query));
+
+ # loc, limit and offset
+ my $page = $self->cgi->param("page") || 0;
+ my $limit = $self->_get_search_limit;
+ my $org_unit = $self->cgi->param("loc") || $self->ctx->{aou_tree}->()->id;
+ my $offset = $page * $limit;
+
+ $self->ctx->{records} = [];
+ $self->ctx->{search_facets} = {};
+ $self->ctx->{page_size} = $limit;
+ $self->ctx->{hit_count} = 0;
+
+ # nothing to do
+ return Apache2::Const::OK if @$query == 0;
+
+ my $results = $U->simplereq(
+ 'open-ils.search',
+ 'open-ils.search.biblio.marc',
+ {searches => $query, org_unit => $org_unit}, $limit, $offset);
+
+ if (defined $U->event_code($results)) {
+ $self->apache->log->warn(
+ "open-ils.search.biblio.marc returned event: " .
+ $U->event_code($results)
+ );
+ return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
+ }
+
+ my ($facets, @data) = $self->get_records_and_facets(
+ # filter out nulls that will turn up here
+ [ grep { $_ } @{$results->{ids}} ],
+ undef, {flesh => "{holdings_xml,mra}"}
+ );
+
+ $self->ctx->{records} = [@data];
+ $self->ctx->{page_size} = $limit;
+ $self->ctx->{hit_count} = $results->{count};
+
+ return Apache2::Const::OK;
+}
+
+sub call_number_browse_standalone {
+ my ($self) = @_;
+
+ if (my $cnfrag = $self->cgi->param("query")) {
+ my $url = sprintf(
+ 'http%s://%s%s/cnbrowse?cn=%s',
+ $self->cgi->https ? "s" : "",
+ $self->apache->hostname,
+ $self->ctx->{opac_root},
+ $cnfrag # XXX some kind of escaping needed here?
+ );
+ return $self->generic_redirect($url);
+ } else {
+ return $self->generic_redirect; # return to search page
+ }
+}
+
+sub load_cnbrowse {
+ my ($self) = @_;
+
+ $self->prepare_browse_call_numbers();
+
+ return Apache2::Const::OK;
+}
+
+1;
--- /dev/null
+package OpenILS::WWW::EGCatLoader;
+use strict; use warnings;
+use Apache2::Const -compile => qw(OK DECLINED FORBIDDEN HTTP_INTERNAL_SERVER_ERROR REDIRECT HTTP_BAD_REQUEST);
+use OpenSRF::Utils::Logger qw/$logger/;
+use OpenILS::Utils::CStoreEditor qw/:funcs/;
+use OpenILS::Utils::Fieldmapper;
+use OpenILS::Application::AppUtils;
+use OpenSRF::MultiSession;
+my $U = 'OpenILS::Application::AppUtils';
+
+my $ro_object_subs; # cached subs
+our %cache = ( # cached data
+ map => {aou => {}}, # others added dynamically as needed
+ list => {},
+ search => {},
+ org_settings => {}
+);
+
+sub init_ro_object_cache {
+ my $self = shift;
+ my $e = $self->editor;
+ my $ctx = $self->ctx;
+
+ if($ro_object_subs) {
+ # subs have been built. insert into the context then move along.
+ $ctx->{$_} = $ro_object_subs->{$_} for keys %$ro_object_subs;
+ return;
+ }
+
+ # make all "field_safe" classes accesible by default in the template context
+ my @classes = grep {
+ ($Fieldmapper::fieldmap->{$_}->{field_safe} || '') =~ /true/i
+ } keys %{ $Fieldmapper::fieldmap };
+
+ for my $class (@classes) {
+
+ my $hint = $Fieldmapper::fieldmap->{$class}->{hint};
+ next if $hint eq 'aou'; # handled separately
+
+ my $ident_field = $Fieldmapper::fieldmap->{$class}->{identity};
+ (my $eclass = $class) =~ s/Fieldmapper:://o;
+ $eclass =~ s/::/_/g;
+
+ my $list_key = "${hint}_list";
+ my $get_key = "get_$hint";
+ my $search_key = "search_$hint";
+
+ # Retrieve the full set of objects with class $hint
+ $ro_object_subs->{$list_key} = sub {
+ my $method = "retrieve_all_$eclass";
+ $cache{list}{$hint} = $e->$method() unless $cache{list}{$hint};
+ return $cache{list}{$hint};
+ };
+
+ # locate object of class $hint with Ident field $id
+ $cache{map}{$hint} = {};
+ $ro_object_subs->{$get_key} = sub {
+ my $id = shift;
+ return $cache{map}{$hint}{$id} if $cache{map}{$hint}{$id};
+ ($cache{map}{$hint}{$id}) = grep { $_->$ident_field eq $id } @{$ro_object_subs->{$list_key}->()};
+ return $cache{map}{$hint}{$id};
+ };
+
+ # search for objects of class $hint where field=value
+ $cache{search}{$hint} = {};
+ $ro_object_subs->{$search_key} = sub {
+ my ($field, $val) = @_;
+ my $method = "search_$eclass";
+ $cache{search}{$hint}{$field} = {} unless $cache{search}{$hint}{$field};
+ $cache{search}{$hint}{$field}{$val} = $e->$method({$field => $val})
+ unless $cache{search}{$hint}{$field}{$val};
+ return $cache{search}{$hint}{$field}{$val};
+ };
+ }
+
+ $ro_object_subs->{aou_tree} = sub {
+
+ # fetch the org unit tree
+ unless($cache{aou_tree}) {
+ my $tree = $e->search_actor_org_unit([
+ { parent_ou => undef},
+ { flesh => -1,
+ flesh_fields => {aou => ['children']},
+ order_by => {aou => 'name'}
+ }
+ ])->[0];
+
+ # flesh the org unit type for each org unit
+ # and simultaneously set the id => aou map cache
+ sub flesh_aout {
+ my $node = shift;
+ my $ro_object_subs = shift;
+ $node->ou_type( $ro_object_subs->{get_aout}->($node->ou_type) );
+ $cache{map}{aou}{$node->id} = $node;
+ flesh_aout($_, $ro_object_subs) foreach @{$node->children};
+ };
+ flesh_aout($tree, $ro_object_subs);
+
+ $cache{aou_tree} = $tree;
+ }
+
+ return $cache{aou_tree};
+ };
+
+ # Add a special handler for the tree-shaped org unit cache
+ $ro_object_subs->{get_aou} = sub {
+ my $org_id = shift;
+ return undef unless defined $org_id;
+ $ro_object_subs->{aou_tree}->(); # force the org tree to load
+ return $cache{map}{aou}{$org_id};
+ };
+
+ # turns an ISO date into something TT can understand
+ $ro_object_subs->{parse_datetime} = sub {
+ my $date = shift;
+ $date = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($date));
+ return sprintf(
+ "%0.2d:%0.2d:%0.2d %0.2d-%0.2d-%0.4d",
+ $date->hour,
+ $date->minute,
+ $date->second,
+ $date->day,
+ $date->month,
+ $date->year
+ );
+ };
+
+ # retrieve and cache org unit setting values
+ $ro_object_subs->{get_org_setting} = sub {
+ my($org_id, $setting) = @_;
+
+ $cache{org_settings}{$org_id} = {}
+ unless $cache{org_settings}{$org_id};
+
+ $cache{org_settings}{$org_id}{$setting} =
+ $U->ou_ancestor_setting_value($org_id, $setting)
+ unless exists $cache{org_settings}{$org_id}{$setting};
+
+ return $cache{org_settings}{$org_id}{$setting};
+ };
+
+ $ctx->{$_} = $ro_object_subs->{$_} for keys %$ro_object_subs;
+}
+
+sub generic_redirect {
+ my $self = shift;
+ my $url = shift;
+ my $cookie = shift; # can be an array of cgi.cookie's
+
+ $self->apache->print(
+ $self->cgi->redirect(
+ -url => $url ||
+ $self->cgi->param('redirect_to') ||
+ $self->ctx->{referer} ||
+ $self->ctx->{home_page},
+ -cookie => $cookie
+ )
+ );
+
+ return Apache2::Const::REDIRECT;
+}
+
+sub get_records_and_facets {
+ my ($self, $rec_ids, $facet_key, $unapi_args) = @_;
+
+ $unapi_args ||= {};
+ $unapi_args->{site} ||= $self->ctx->{aou_tree}->()->shortname;
+ $unapi_args->{depth} ||= $self->ctx->{aou_tree}->()->ou_type->depth;
+ $unapi_args->{flesh_depth} ||= 5;
+
+ my @data;
+ my $ses = OpenSRF::MultiSession->new(
+ app => 'open-ils.cstore',
+ cap => 10, # XXX config
+ success_handler => sub {
+ my($self, $req) = @_;
+ my $data = $req->{response}->[0]->content;
+ my $xml = XML::LibXML->new->parse_string($data->{'unapi.bre'})->documentElement;
+ my $bre_id = $xml->find('*[@tag="901"]/*[@code="c"]')->[0]->textContent;
+ push(@data, {id => $bre_id, marc_xml => $xml});
+ }
+ );
+
+ $ses->request(
+ 'open-ils.cstore.json_query',
+ {from => [
+ 'unapi.bre', $_, 'marcxml','record',
+ $unapi_args->{flesh},
+ $unapi_args->{site},
+ $unapi_args->{depth},
+ $unapi_args->{flesh_depth},
+ ]}
+ ) for @$rec_ids;
+
+ # collect the facet data
+ my $search = OpenSRF::AppSession->create('open-ils.search');
+ my $facet_req = $search->request(
+ 'open-ils.search.facet_cache.retrieve', $facet_key, 10
+ ) if $facet_key;
+
+ # gather up the unapi recs
+ $ses->session_wait(1);
+
+ my $facets;
+ if ($facet_key) {
+ $facets = $facet_req->gather(1);
+ $facets->{$_} = {
+ cmf => $self->ctx->{get_cmf}->($_),
+ data => $facets->{$_}
+ } for keys %$facets; # quick-n-dirty
+ } else {
+ $facets = undef;
+ }
+
+ $search->kill_me;
+
+ return ($facets, @data);
+}
+
+# TODO: blend this code w/ ^-- get_records_and_facets
+sub fetch_marc_xml_by_id {
+ my ($self, $id_list) = @_;
+ $id_list = [$id_list] unless ref($id_list);
+
+ {
+ no warnings qw/numeric/;
+ $id_list = [map { int $_ } @$id_list];
+ $id_list = [grep { $_ > 0} @$id_list];
+ };
+
+ return {} if scalar(@$id_list) < 1;
+
+ # I'm just sure there needs to be some more efficient way to get all of
+ # this.
+ my $results = $self->editor->json_query({
+ "select" => {"bre" => ["id", "marc"]},
+ "from" => {"bre" => {}},
+ "where" => {"id" => $id_list}
+ }, {substream => 1}) or return $self->editor->die_event;
+
+ my $marc_xml = {};
+ for my $r (@$results) {
+ $marc_xml->{$r->{"id"}} =
+ (new XML::LibXML)->parse_string($r->{"marc"});
+ }
+
+ return $marc_xml;
+}
+
+1;
use XML::Simple;
use XML::LibXML;
use File::stat;
+use Encode;
use Apache2::Const -compile => qw(OK DECLINED HTTP_INTERNAL_SERVER_ERROR);
use Apache2::Log;
use OpenSRF::EX qw(:try);
+use OpenILS::Utils::CStoreEditor;
-use constant OILS_HTTP_COOKIE_SKIN => 'oils:skin';
-use constant OILS_HTTP_COOKIE_THEME => 'oils:theme';
-use constant OILS_HTTP_COOKIE_LOCALE => 'oils:locale';
-
-my $web_config;
-my $web_config_file;
-my $web_config_edit_time;
-
-sub import {
- my $self = shift;
- $web_config_file = shift || '';
- unless(-r $web_config_file) {
- warn "Invalid web config $web_config_file";
- return;
- }
- check_web_config();
-}
+use constant OILS_HTTP_COOKIE_SKIN => 'eg_skin';
+use constant OILS_HTTP_COOKIE_THEME => 'eg_theme';
+use constant OILS_HTTP_COOKIE_LOCALE => 'eg_locale';
+# cache string bundles
+my @registered_locales;
sub handler {
my $r = shift;
- check_web_config($r); # option to disable this
my $ctx = load_context($r);
my $base = $ctx->{base_path};
+
+ $r->content_type('text/html; encoding=utf8');
+
my($template, $page_args, $as_xml) = find_template($r, $base, $ctx);
+ $ctx->{page_args} = $page_args;
+
+ my $stat = run_context_loader($r, $ctx);
+
+ return $stat unless $stat == Apache2::Const::OK;
return Apache2::Const::DECLINED unless $template;
- $template = $ctx->{skin} . "/$template";
- $ctx->{page_args} = $page_args;
- $r->content_type('text/html; encoding=utf8');
+ my $text_handler = set_text_handler($ctx, $r);
my $tt = Template->new({
OUTPUT => ($as_xml) ? sub { parse_as_xml($r, $ctx, @_); } : $r,
INCLUDE_PATH => $ctx->{template_paths},
+ DEBUG => $ctx->{debug_template},
+ PLUGINS => {
+ EGI18N => 'OpenILS::WWW::EGWeb::I18NFilter',
+ CGI_utf8 => 'OpenILS::WWW::EGWeb::CGI_utf8'
+ },
+ FILTERS => {
+ # Register a dynamic filter factory for our locale::maketext generator
+ l => [
+ sub {
+ my($ctx, @args) = @_;
+ return sub { $text_handler->(shift(), @args); }
+ }, 1
+ ]
+ }
});
- unless($tt->process($template, {ctx => $ctx})) {
- $r->log->warn('Template error: ' . $tt->error);
+ if (!$tt) {
+ $r->log->error("Error creating template processor: $@");
+ return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
+ }
+
+ $ctx->{encode_utf8} = sub {return encode_utf8(shift())};
+
+ unless($tt->process($template, {ctx => $ctx, ENV => \%ENV, l => $text_handler})) {
+ $r->log->warn('egweb: template error: ' . $tt->error);
return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
}
return Apache2::Const::OK;
}
+sub set_text_handler {
+ my $ctx = shift;
+ my $r = shift;
+
+ my $locale = $ctx->{locale};
+
+ $r->log->debug("egweb: messages locale = $locale");
+
+ return sub {
+ my $lh = OpenILS::WWW::EGWeb::I18N->get_handle($locale);
+ return $lh->maketext(@_);
+ };
+}
+
+
+
+sub run_context_loader {
+ my $r = shift;
+ my $ctx = shift;
+
+ my $stat = Apache2::Const::OK;
+
+ my $loader = $r->dir_config('OILSWebContextLoader');
+ return $stat unless $loader;
+
+ eval {
+ $loader->use;
+ $stat = $loader->new($r, $ctx)->load;
+ };
+
+ if($@) {
+ $r->log->error("egweb: Context Loader error: $@");
+ return Apache2::Const::HTTP_INTERNAL_SERVER_ERROR;
+ }
+
+ $r->log->debug("egweb: context loader resulted in status $stat");
+ return $stat;
+}
+
sub parse_as_xml {
my $r = shift;
my $ctx = shift;
$data = $ctx->{final_dtd} . "\n" . $data;
$success = 1;
} otherwise {
- my $e = shift;
+ my $e = shift;
my $err = "Invalid XML: $e";
- $r->log->error($err);
+ $r->log->error("egweb: $err");
$r->content_type('text/plain; encoding=utf8');
$r->print("\n$err\n\n$data");
};
$r->print($data) if ($success);
}
-
sub load_context {
my $r = shift;
my $cgi = CGI->new;
- my $ctx = $web_config->{ctx};
+ my $ctx = {}; # new context for each page load
+
+ $ctx->{base_path} = $r->dir_config('OILSWebBasePath');
+ $ctx->{web_dir} = $r->dir_config('OILSWebWebDir');
+ $ctx->{debug_template} = ($r->dir_config('OILSWebDebugTemplate') =~ /true/io);
+ $ctx->{media_prefix} = $r->dir_config('OILSWebMediaPrefix');
$ctx->{hostname} = $r->hostname;
$ctx->{base_url} = $cgi->url(-base => 1);
$ctx->{skin} = $cgi->cookie(OILS_HTTP_COOKIE_SKIN) || 'default';
$ctx->{theme} = $cgi->cookie(OILS_HTTP_COOKIE_THEME) || 'default';
+ $ctx->{proto} = $cgi->https ? 'https' : 'http';
+
+ my @template_paths = $r->dir_config->get('OILSWebTemplatePath');
+ $ctx->{template_paths} = [ reverse @template_paths ];
+
+ my %locales = $r->dir_config->get('OILSWebLocale');
+ load_locale_handlers($ctx, %locales);
+
$ctx->{locale} =
$cgi->cookie(OILS_HTTP_COOKIE_LOCALE) ||
- parse_accept_lang($r->headers_in->get('Accept-Language')) || 'en-US';
- $r->log->debug('skin = ' . $ctx->{skin} . ' : theme = ' .
- $ctx->{theme} . ' : locale = ' . $ctx->{locale});
+ parse_accept_lang($r->headers_in->get('Accept-Language')) || 'en_us';
+
+ my $mprefix = $ctx->{media_prefix};
+ if($mprefix and $mprefix !~ /^http/ and $mprefix !~ /^\//) {
+ # if a hostname is provided /w no protocol, match the protocol to the current page
+ $ctx->{media_prefix} = ($cgi->https) ? "https://$mprefix" : "http://$mprefix";
+ }
+
return $ctx;
}
# turn Accept-Language into sometihng EG can understand
+# TODO: try all langs, not just the first
sub parse_accept_lang {
my $al = shift;
return undef unless $al;
my ($locale) = split(/,/, $al);
($locale) = split(/;/, $locale);
return undef unless $locale;
- $locale =~ s/-(.*)/eval '-'.uc("$1")/e;
+ $locale =~ s/-/_/og;
return $locale;
}
my $r = shift;
my $base = shift;
my $ctx = shift;
- my $skin = $ctx->{skin};
my $path = $r->uri;
- $path =~ s/$base//og;
- my @parts = split('/', $path);
+ $path =~ s/$base\/?//og;
my $template = '';
my $page_args = [];
- my $as_xml = $ctx->{force_valid_xml};
- my $handler = $web_config->{handlers};
+ my $as_xml = $r->dir_config('OILSWebForceValidXML');
+ my $ext = $r->dir_config('OILSWebDefaultTemplateExtension');
+ my @parts = split('/', $path);
+ my $localpath = $path;
+ my @args;
while(@parts) {
- my $part = shift @parts;
- next unless $part;
- my $t = $handler->{$part};
- if(ref($t) eq 'PathConfig') {
- $template = $t->{template};
- $as_xml = ($t->{as_xml} and $t->{as_xml} =~ /true/io) || $as_xml;
- $page_args = [@parts];
- last;
- } else {
- $handler = $t;
- }
- }
-
- unless($template) { # no template configured
-
- # see if we can magically find the template based on the path and default extension
- my $ext = $ctx->{default_template_extension};
-
- my @parts = split('/', $path);
- my $localpath = $path;
- my @args;
- while(@parts) {
- last unless $localpath;
- for my $tpath (@{$ctx->{template_paths}}) {
- my $fpath = "$tpath/$skin/$localpath.$ext";
- $r->log->debug("looking at possible template $fpath");
- if(-r $fpath) {
- $template = "$localpath.$ext";
- last;
- }
+ last unless $localpath;
+ for my $tpath (@{$ctx->{template_paths}}) {
+ my $fpath = "$tpath/$localpath.$ext";
+ $r->log->debug("egweb: looking at possible template $fpath");
+ if(-r $fpath) {
+ $template = "$localpath.$ext";
+ last;
}
- last if $template;
- push(@args, pop @parts);
- $localpath = '/'.join('/', @parts);
- }
+ }
+ last if $template;
+ push(@args, pop @parts);
+ $localpath = join('/', @parts);
+ }
- $page_args = [@args];
+ $page_args = [@args];
- # no template configured or found
- unless($template) {
- $r->log->warn("No template configured for path $path");
- return ();
- }
+ # no template configured or found
+ unless($template) {
+ $r->log->debug("egweb: No template configured for path $path");
+ return ();
}
- $r->log->debug("template = $template : page args = @$page_args");
+ $r->log->debug("egweb: template = $template : page args = @$page_args");
return ($template, $page_args, $as_xml);
}
-# if the web configuration file has never been loaded or has
-# changed since the last load, reload it
-sub check_web_config {
- my $r = shift;
- my $epoch = stat($web_config_file)->mtime;
- unless($web_config_edit_time and $web_config_edit_time == $epoch) {
- $r->log->debug("Reloading web config after edit...") if $r;
- $web_config_edit_time = $epoch;
- $web_config = parse_config($web_config_file);
- }
-}
+# Create an I18N sub-module for each supported locale
+# Each module creates its own MakeText lexicon by parsing .po/.mo files
+sub load_locale_handlers {
+ my $ctx = shift;
+ my %locales = @_;
+
+ my @locale_tags = sort { length($a) <=> length($b) } keys %locales;
+
+ # If no locales are defined, fall back to en_us so that at least 1 handler exists
+ push(@locale_tags, 'en_us') unless @registered_locales or @locale_tags;
+
+ for my $idx (0..$#locale_tags) {
+
+ my $tag = $locale_tags[$idx];
+ next if grep { $_ eq $tag } @registered_locales;
-sub parse_config {
- my $cfg_file = shift;
- my $data = XML::Simple->new->XMLin($cfg_file);
- my $ctx = {};
- my $handlers = {};
-
- $ctx->{media_prefix} = (ref $data->{media_prefix}) ? '' : $data->{media_prefix};
- $ctx->{base_path} = (ref $data->{base_path}) ? '' : $data->{base_path};
- $ctx->{template_paths} = [];
- $ctx->{force_valid_xml} = ($data->{force_valid_xml} =~ /true/io) ? 1 : 0;
- $ctx->{default_template_extension} = $data->{default_template_extension} || 'tt2';
- $ctx->{web_dir} = $data->{web_dir};
-
- my $tpaths = $data->{template_paths}->{path};
- $tpaths = [$tpaths] unless ref $tpaths;
- push(@{$ctx->{template_paths}}, $_) for @$tpaths;
-
- for my $handler (@{$data->{handlers}->{handler}}) {
- my @parts = split('/', $handler->{path});
- my $h = $handlers;
- my $pcount = scalar(@parts);
- for(my $i = 0; $i < $pcount; $i++) {
- my $p = $parts[$i];
- unless(defined $h->{$p}) {
- if($i == $pcount - 1) {
- $h->{$p} = PathConfig->new(%$handler);
- last;
+ my $parent_tag = '';
+ my $sub_idx = $idx;
+
+ # find the parent locale if possible. It will be
+ # longest left-anchored substring of the current tag
+ while( --$sub_idx >= 0 ) {
+ my $ptag = $locale_tags[$sub_idx];
+ if( substr($tag, 0, length($ptag)) eq $ptag ) {
+ $parent_tag = "::$ptag";
+ last;
+ }
+ }
+
+ my $messages = $locales{$tag} || '';
+
+ # TODO Can we do this without eval?
+ my $eval = <<" EVAL";
+ package OpenILS::WWW::EGWeb::I18N::$tag;
+ use base 'OpenILS::WWW::EGWeb::I18N$parent_tag';
+ if(\$messages) {
+ use Locale::Maketext::Lexicon::Gettext;
+ if(open F, '$messages') {
+ our %Lexicon = (%Lexicon, %{ Locale::Maketext::Lexicon::Gettext->parse(<F>) });
+ close F;
} else {
- $h->{$p} = {};
+ warn "EGWeb: unable to open messages file: $messages";
}
}
- $h = $h->{$p};
+ EVAL
+ eval $eval;
+
+ if ($@) {
+ warn "$@\n" if $@;
+ } else {
+ push(@registered_locales, $tag);
}
}
-
- return {ctx => $ctx, handlers => $handlers};
}
-package PathConfig;
-sub new {
- my($class, %args) = @_;
- return bless(\%args, $class);
-}
+# base class for all supported locales
+package OpenILS::WWW::EGWeb::I18N;
+use base 'Locale::Maketext';
+our %Lexicon = (_AUTO => 1);
1;
--- /dev/null
+package OpenILS::WWW::EGWeb::CGI_utf8;
+
+# The code in this module is copied from (except for a tiny modification)
+# Template::Plugin::CGI, which is written by:
+#
+# Andy Wardley E<lt>abw@wardley.orgE<gt> L<http://wardley.org/>
+#
+# Copyright (C) 1996-2007 Andy Wardley. All Rights Reserved.
+#
+# This module is free software; you can redistribute it and/or
+# modify it under the same terms as Perl itself.
+
+use strict;
+use warnings;
+use base 'Template::Plugin';
+use CGI qw(:all -utf8);
+
+sub new {
+ my $class = shift;
+ my $context = shift;
+ new CGI(@_);
+}
+
+# monkeypatch CGI::params() method to Do The Right Thing in TT land
+
+sub CGI::params {
+ my $self = shift;
+ local $" = ', ';
+
+ return $self->{ _TT_PARAMS } ||= do {
+ # must call Vars() in a list context to receive
+ # plain list of key/vals rather than a tied hash
+ my $params = { $self->Vars() };
+
+ # convert any null separated values into lists
+ @$params{ keys %$params } = map {
+ /\0/ ? [ split /\0/ ] : $_
+ } values %$params;
+
+ $params;
+ };
+}
+
+1;
--- /dev/null
+package OpenILS::WWW::EGWeb::I18NFilter;
+use Template::Plugin::Filter;
+use base qw(Template::Plugin::Filter);
+our $DYNAMIC = 1;
+
+sub filter {
+ my ($self, $text, $args) = @_;
+ return $maketext->($text, @$args);
+}
+
+1;
+
}
+my %org_cache;
sub handler {
+ my $apache = shift;
+ my $cgi = CGI->new( $apache );
+ my $port = $cgi->server_port();
+ my $hostname = $cgi->server_name();
+ my $proto = ($cgi->https) ? 'https' : 'http';
my $user_ip = $ENV{REMOTE_ADDR};
- my $apache_obj = shift;
- my $cgi = CGI->new( $apache_obj );
+ # Apache config values
+ my $skin = $apache->dir_config('OILSRedirectSkin') || 'default';
+ my $depth = $apache->dir_config('OILSRedirectDepth');
+ my $locale = $apache->dir_config('OILSRedirectLocale') || 'en-US';
+ my $use_tt = ($apache->dir_config('OILSRedirectTpac') || '') =~ /true/i;
+ my $orig_loc;
- my $skin = $apache_obj->dir_config('OILSRedirectSkin') || 'default';
- my $depth = $apache_obj->dir_config('OILSRedirectDepth');
- my $locale = $apache_obj->dir_config('OILSRedirectLocale') || 'en-US';
+ $apache->log->debug("Redirector sees client frim $user_ip");
- my $hostname = $cgi->server_name();
- my $port = $cgi->server_port();
+ # parse the IP file
+ my ($shortname, $nskin, $nhostname) = redirect_libs($user_ip);
- my $proto = "http";
- if($cgi->https) { $proto = "https"; }
+ if ($shortname) { # we have a config
- my $url = "$proto://$hostname:$port/opac/$locale/skin/$skin/xml/index.xml";
- my $path = $apache_obj->path_info();
+ # Read any override vars from the ips txt file
+ if ($nskin =~ m/[^\s]/) { $skin = $nskin; }
+ if ($nhostname =~ m/[^\s]/) { $hostname = $nhostname; }
- $logger->debug("Apache client connecting from $user_ip");
+ if($org_cache{$shortname}) {
+ $orig_loc = $org_cache{$shortname};
- my ($shortname, $nskin, $nhostname) = redirect_libs($user_ip);
- if ($shortname) {
+ } else {
- if ($nskin =~ m/[^\s]/) { $skin = $nskin; }
- if ($nhostname =~ m/[^\s]/) { $hostname = $nhostname; }
+ my $session = OpenSRF::AppSession->create("open-ils.actor");
+ my $org = $session->request(
+ 'open-ils.actor.org_unit.retrieve_by_shortname',
+ $shortname)->gather(1);
+
+ $org_cache{$shortname} = $orig_loc = $org->id if $org;
+ }
+ }
- $logger->info("Apache redirecting $user_ip to $shortname with skin $skin and host $hostname");
- my $session = OpenSRF::AppSession->create("open-ils.actor");
+ my $url = "$proto://$hostname:$port";
- $url = "$proto://$hostname:$port/opac/$locale/skin/$skin/xml/index.xml";
+ if($use_tt) {
- my $org = $session->request(
- 'open-ils.actor.org_unit.retrieve_by_shortname',
- $shortname)->gather(1);
+ $url .= "/eg/opac/home";
+ $url .= "?orig_loc=$orig_loc" if $orig_loc;
- if($org) {
- $url .= "?ol=" . $org->id;
+=head potential locale/skin implementation
+ if($locale ne 'en-US') {
+ $apache->headers_out->add(
+ "Set-Cookie" => $cgi->cookie(
+ -name => "oils:locale", # see EGWeb.pm
+ -path => "/eg",
+ -value => $locale,
+ -expires => undef
+ )
+ );
+ }
+
+ if($skin ne 'default') {
+ $apache->headers_out->add(
+ "Set-Cookie" => $cgi->cookie(
+ -name => "oils:skin", # see EGWeb.pm
+ -path => "/eg",
+ -value => $skin,
+ -expires => undef
+ )
+ );
+ }
+=cut
+
+ } else {
+ $url .= "/opac/$locale/skin/$skin/xml/index.xml";
+ if($orig_loc) {
+ $url .= "?ol=" . $orig_loc;
$url .= "&d=$depth" if defined $depth;
}
- }
+ }
- print "Location: $url\n\n";
+ $logger->info("Apache redirecting $user_ip to $url");
+ $apache->headers_out->add('Location' => "$url");
return Apache2::Const::REDIRECT;
-
- return print_page($url);
}
sub redirect_libs {
my $range = new Net::IP( $block->[0] . ' - ' . $block->[1] );
if( $source_ip->overlaps($range)==$IP_A_IN_B_OVERLAP ||
$source_ip->overlaps($range)==$IP_IDENTICAL ) {
- return ($shortname, $block->[2], $block->[3]);
+ return ($shortname, $block->[2] || '', $block->[3] || '');
}
}
}
return 0;
}
-
-sub print_page {
-
- my $url = shift;
-
- print "Content-type: text/html; charset=utf-8\n\n";
- print <<" HTML";
- <html>
- <head>
- <meta HTTP-EQUIV='Refresh' CONTENT="0; URL=$url"/>
- <style TYPE="text/css">
- .loading_div {
- text-align:center;
- margin-top:30px;
- font-weight:bold;
- background: lightgrey;
- color:black;
- width:100%;
- }
- </style>
- </head>
- <body>
- <br/><br/>
- <div class="loading_div">
- <h4>Loading...</h4>
- </div>
- <br/><br/>
- <center><img src='/opac/images/main_logo.jpg'/></center>
- </body>
- </html>
- HTML
-
- return Apache2::Const::OK;
-}
-
-
1;
--- /dev/null
+#
+# OpenILS::Template::Plugin::ResolverResolver
+#
+# DESCRIPTION
+#
+# Simple Template Toolkit Plugin which hooks into Dan Scott's Resolver
+#
+# AUTHOR
+# Art Rhyno <http://projectconifer.ca>
+#
+# COPYRIGHT
+# Copyright (C) 2011
+#
+# LICENSE
+# GNU General Public License v2 or later
+#
+#============================================================================
+
+package Template::Plugin::ResolverResolver;
+
+use strict;
+use warnings;
+use base 'Template::Plugin';
+use OpenILS::Application::ResolverResolver;
+use OpenILS::Application;
+use base qw/OpenILS::Application/;
+use OpenSRF::AppSession;
+
+
+our $VERSION = 0.9;
+
+sub load {
+ my ( $class, $context ) = @_;
+ return $class;
+}
+
+sub new {
+ my ( $class, $context, @params ) = @_;
+
+ bless { _CONTEXT => $context, }, $class;
+}
+
+# monkeypatch ResolverResolver::params() method to Do The Right Thing in TT land
+
+sub ResolverResolver::params {
+ my $self = shift;
+ local $" = ', ';
+
+ return $self->{ _TT_PARAMS } ||= do {
+ # must call Vars() in a list context to receive
+ # plain list of key/vals rather than a tied hash
+ my $params = { $self->Vars() };
+
+ # convert any null separated values into lists
+ @$params{ keys %$params } = map {
+ /\0/ ? [ split /\0/ ] : $_
+ } values %$params;
+
+ $params;
+ };
+}
+
+sub resolve_issn
+{
+ my ($class, $c, $baseurl) = @_;
+
+ if (length($c) <= 9) {
+ my $session = OpenSRF::AppSession->create("open-ils.resolver");
+
+ my $request = $session->request("open-ils.resolver.resolve_holdings.raw", "issn", $c, $baseurl)->gather();
+ if ($request) {
+ return $request;
+ }
+ $session->disconnect();
+ }
+
+ return "";
+}
+
+sub resolve_isbn
+{
+ my ($class, $c, $baseurl) = @_;
+
+ my $session = OpenSRF::AppSession->create("open-ils.resolver");
+
+ my $request = $session->request("open-ils.resolver.resolve_holdings.raw", "isbn", $c, $baseurl)->gather();
+
+ if ($request) {
+ return $request;
+ }
+ $session->disconnect();
+
+ return "";
+}
+
+
+1;
+
( 'org.patron_opt_default',
oils_i18n_gettext( 'org.patron_opt_default', 'Circ: Patron Opt-In Default', 'coust', 'label'),
oils_i18n_gettext( 'org.patron_opt_default', 'This is the default depth at which a patron is opted in; it is calculated as an org unit relative to the current workstation.', 'coust', 'label'),
- 'integer')
-,(
+ 'integer'),
+
+( 'opac.payment_history_age_limit',
+ oils_i18n_gettext( 'opac.payment_history_age_limit', 'OPAC: Payment History Age Limit', 'coust', 'label'),
+ oils_i18n_gettext( 'opac.payment_history_age_limit', 'The OPAC should not display payments by patrons that are older than any interval defined here.', 'coust', 'label'),
+ 'interval'),
+
+(
'ui.circ.billing.uncheck_bills_and_unfocus_payment_box',
oils_i18n_gettext(
'ui.circ.billing.uncheck_bills_and_unfocus_payment_box',
--- /dev/null
+-- Evergreen DB patch XXXX.data.opac_payment_history_age_limit.sql
+
+BEGIN;
+
+SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+
+INSERT into config.org_unit_setting_type (name, label, description, datatype)
+VALUES (
+ 'opac.payment_history_age_limit',
+ oils_i18n_gettext('opac.payment_history_age_limit',
+ 'OPAC: Payment History Age Limit', 'coust', 'label'),
+ oils_i18n_gettext('opac.payment_history_age_limit',
+ 'The OPAC should not display payments by patrons that are older than any interval defined here.', 'coust', 'label'),
+ 'interval'
+);
+
+COMMIT;
--- /dev/null
+#!/usr/bin/perl
+require '../oils_header.pl';
+use strict; use warnings;
+use Time::HiRes qw/time usleep/;
+use Data::Dumper;
+use OpenSRF::Utils::JSON;
+use OpenILS::Utils::CStoreEditor;
+use XML::LibXML;
+
+#-----------------------------------------------------------------------------
+# Does a checkout, renew, and checkin
+#-----------------------------------------------------------------------------
+
+my @recs = (1,2,3,4,5,6,7,8,9,10);
+
+osrf_connect(shift() || '/openils/conf/opensrf_core.xml');
+
+my $e = OpenILS::Utils::CStoreEditor->new;
+
+sub xptext {
+ my($node, $path) = @_;
+ #my $res = $node->findnodes($path);
+ my $res = $node->find($path);
+ return '' unless $res and $res->[0];
+ return $res->[0]->textContent;
+}
+
+sub get_bib_attrs {
+ my $xml = shift;
+ return {
+ isbn => xptext($xml, '*[@tag="020"]/*[@code="a"]'),
+ upc => xptext($xml,'*[@tag="024"]/*[@code="a"]'),
+ issn => xptext($xml,'*[@tag="022"]/*[@code="a"]'),
+ title => xptext($xml,'*[@tag="245"]/*[@code="a"]'),
+ author => xptext($xml,'*[@tag="100"]/*[@code="a"]'),
+ publisher => xptext($xml,'*[@tag="260"]/*[@code="b"]'),
+ pubdate => xptext($xml,'*[@tag="260"]/*[@code="c"]'),
+ edition => xptext($xml,'*[@tag="250"]/*[@code="a"]'),
+ };
+}
+
+sub unapi {
+ my @recs = @_;
+ my $start = time();
+
+ my $ses1 = OpenSRF::AppSession->create('open-ils.cstore');
+ my $ses2 = OpenSRF::AppSession->create('open-ils.cstore');
+ my $ses3 = OpenSRF::AppSession->create('open-ils.cstore');
+ my ($req1, $req2, $req3);
+
+ my %records;
+ while(@recs) {
+ my ($id1, $id2, $id3) = (pop @recs, pop @recs, pop @recs);
+
+ for my $r ($req1, $req2, $req3) {
+ if($r) {
+ my $data = $r->gather(1);
+ my $xml = XML::LibXML->new->parse_string($data->{'unapi.bre'});
+ $xml = $xml->documentElement;
+ my $attrs = get_bib_attrs($xml);
+ my $rec_id = xptext($xml,'*[@tag="901"]/*[@code="c"]');
+ $records{$rec_id}{$_} = $attrs->{$_} for keys %$attrs;
+
+ my $rvols = [];
+ for my $volnode ($xml->findnodes('//*[local-name()="volumes"]/*[local-name()="volume"]')) {
+ my $vol = {};
+ $vol->{copies} = [];
+ $vol->{label} = $volnode->getAttribute('label');
+ for my $copynode ($volnode->getElementsByLocalName('copy')) {
+ my $copy = {};
+ $copy->{barcode} = $copynode->getAttribute('barcode');
+ push(@{$vol->{copies}}, $copy);
+ }
+ push(@{$records{$rec_id}->{volumes}}, $vol);
+ }
+
+ }
+ }
+
+ $req1 = ($id1) ? $ses1->request('open-ils.cstore.json_query', {from => ['unapi.bre', $id1, 'marcxml', 'record', '{holdings_xml,acp}', 'CONS']}) : undef;
+ $req2 = ($id2) ? $ses1->request('open-ils.cstore.json_query', {from => ['unapi.bre', $id2, 'marcxml', 'record', '{holdings_xml,acp}', 'CONS']}) : undef;
+ $req3 = ($id3) ? $ses1->request('open-ils.cstore.json_query', {from => ['unapi.bre', $id3, 'marcxml', 'record', '{holdings_xml,acp}', 'CONS']}) : undef;
+ }
+
+
+ for my $r ($req1, $req2, $req3) {
+ if($r) {
+ my $data = $r->gather(1);
+ my $xml = XML::LibXML->new->parse_string($data->{'unapi.bre'});
+ $xml = $xml->documentElement;
+ my $attrs = get_bib_attrs($xml);
+ my $rec_id = xptext($xml,'*[@tag="901"]/*[@code="c"]');
+ $records{$rec_id}{$_} = $attrs->{$_} for keys %$attrs;
+
+ my $rvols = [];
+ for my $volnode ($xml->findnodes('//*[local-name()="volumes"]/*[local-name()="volume"]')) {
+ my $vol = {};
+ $vol->{copies} = [];
+ $vol->{label} = $volnode->getAttribute('label');
+ for my $copynode ($volnode->getElementsByLocalName('copy')) {
+ my $copy = {};
+ $copy->{barcode} = $copynode->getAttribute('barcode');
+ push(@{$vol->{copies}}, $copy);
+ }
+ push(@{$records{$rec_id}->{volumes}}, $vol);
+ }
+
+ }
+ }
+
+ my $duration = time() - $start;
+
+ for my $rec_id (keys %records) {
+ my $rec = $records{$rec_id};
+ print sprintf("%d [%s] has %d volumes and %d copies\n",
+ $rec_id, $rec->{title},
+ scalar(@{$rec->{volumes}}),
+ scalar(map { @{$_->{copies}} } @{$rec->{volumes}}));
+ }
+
+ #note, unapi.biblio_record_entry_feed per record performs the same as unapi.bre pre record
+ print "\nunapi 'unapi.bre' duration is $duration\n\n";
+}
+
+sub unapi_spread {
+ my @recs = @_;
+ my %records;
+ my $start = time();
+
+ my @reqs;
+ for my $rec_id (@recs) {
+
+ my $ses = OpenSRF::AppSession->create('open-ils.cstore');
+ my $req = $ses->request(
+ 'open-ils.cstore.json_query',
+ {from => ['unapi.bre', $rec_id, 'marcxml', 'record', '{holdings_xml,acp}', 'CONS']});
+
+ push(@reqs, $req);
+ }
+
+ for my $req (@reqs) {
+
+ my $data = $req->gather(1);
+ my $xml = XML::LibXML->new->parse_string($data->{'unapi.bre'});
+ $xml = $xml->documentElement;
+ my $attrs = get_bib_attrs($xml);
+ my $rec_id = xptext($xml,'*[@tag="901"]/*[@code="c"]');
+ $records{$rec_id}{$_} = $attrs->{$_} for keys %$attrs;
+
+ my $rvols = [];
+ for my $volnode ($xml->findnodes('//*[local-name()="volumes"]/*[local-name()="volume"]')) {
+ my $vol = {};
+ $vol->{copies} = [];
+ $vol->{label} = $volnode->getAttribute('label');
+ for my $copynode ($volnode->getElementsByLocalName('copy')) {
+ my $copy = {};
+ $copy->{barcode} = $copynode->getAttribute('barcode');
+ push(@{$vol->{copies}}, $copy);
+ }
+ push(@{$records{$rec_id}->{volumes}}, $vol);
+ }
+ }
+
+ my $duration = time() - $start;
+
+ for my $rec_id (keys %records) {
+ my $rec = $records{$rec_id};
+ print sprintf("%d [%s] has %d volumes and %d copies\n",
+ $rec_id, $rec->{title},
+ scalar(@{$rec->{volumes}}),
+ scalar(map { @{$_->{copies}} } @{$rec->{volumes}}));
+ }
+
+ #note, unapi.biblio_record_entry_feed per record performs the same as unapi.bre pre record
+ print "\nunapi 'unapi.bre' spread duration is $duration\n\n";
+}
+
+
+
+sub unapi_batch {
+ my @recs = @_;
+ my $start = time();
+
+ my $data = $e->json_query({from => ['unapi.biblio_record_entry_feed', "{".join(',',@recs)."}", 'marcxml', '{holdings_xml,acp}', 'CONS']})->[0];
+ my $xml = XML::LibXML->new->parse_string($data->{'unapi.biblio_record_entry_feed'});
+
+ my %records;
+ for my $rec_xml ($xml->documentElement->getElementsByLocalName('record')) {
+
+ my $attrs = get_bib_attrs($rec_xml);
+ my $rec_id = xptext($rec_xml,'*[@tag="901"]/*[@code="c"]');
+ #print "REC = $rec_xml : $rec_id : " . $attrs->{title} . "\n" . $rec_xml->toString . "\n";
+ $records{$rec_id}{$_} = $attrs->{$_} for keys %$attrs;
+
+ my $rvols = [];
+ for my $volnode ($rec_xml->findnodes('//*[local-name()="volumes"]/*[local-name()="volume"]')) {
+ my $vol = {};
+ $vol->{copies} = [];
+ $vol->{label} = $volnode->getAttribute('label');
+ for my $copynode ($volnode->getElementsByLocalName('copy')) {
+ my $copy = {};
+ $copy->{barcode} = $copynode->getAttribute('barcode');
+ push(@{$vol->{copies}}, $copy);
+ }
+ push(@{$records{$rec_id}->{volumes}}, $vol);
+ }
+ }
+
+ my $duration = time() - $start;
+
+ for my $rec_id (keys %records) {
+ my $rec = $records{$rec_id};
+ print sprintf("%d [%s] has %d volumes and %d copies\n",
+ $rec_id, $rec->{title},
+ scalar(@{$rec->{volumes}}),
+ scalar(map { @{$_->{copies}} } @{$rec->{volumes}}));
+ }
+ print "\nunapi 'batch feed' duration is $duration\n\n";
+}
+
+sub direct_spread {
+ my @recs = @_;
+ my %records;
+ my $start = time();
+
+ my $query = {
+ flesh => 4,
+ flesh_fields => {
+ bre => ['call_numbers'],
+ acn => ['copies', 'uris'],
+ acp => ['location', 'stat_cat_entries', 'parts'],
+ ascecm => ['stat_cat', 'stat_cat_entry'],
+ acpm => ['part']
+ }
+ };
+
+ my @reqs;
+ for my $rec_id (@recs) {
+ my $ses = OpenSRF::AppSession->create('open-ils.cstore');
+ my $req = $ses->request(
+ 'open-ils.cstore.direct.biblio.record_entry.search', {id => $rec_id}, $query);
+ push(@reqs, $req);
+ }
+
+ $records{$_}{counts} = $e->json_query({from => ['asset.record_copy_count', 1, $_, 0]})->[0] for @recs;
+ for my $req (@reqs) {
+ my $bre = $req->gather(1);
+ my $xml = XML::LibXML->new->parse_string($bre->marc)->documentElement;
+ my $attrs = get_bib_attrs($xml);
+ $records{$bre->id}{record} = $bre;
+ $records{$bre->id}{$_} = $attrs->{$_} for keys %$attrs;
+ }
+
+ my $duration = time() - $start;
+
+ for my $rec_id (keys %records) {
+ my $rec = $records{$rec_id};
+ print sprintf("%d [%s] has %d volumes and %d copies\n",
+ $rec_id, $rec->{title},
+ scalar(@{$rec->{record}->call_numbers}),
+ scalar(map { @{$_->copies} } @{$rec->{record}->call_numbers}));
+ }
+
+ print "\n'direct' spread calls processing duration is $duration\n\n";
+}
+
+
+sub direct {
+ my @recs = @_;
+ my %records;
+
+ my $start = time();
+
+ my $ses1 = OpenSRF::AppSession->create('open-ils.cstore');
+ my $ses2 = OpenSRF::AppSession->create('open-ils.cstore');
+ my $ses3 = OpenSRF::AppSession->create('open-ils.cstore');
+ my ($req1, $req2, $req3);
+
+ my $query = {
+ flesh => 5,
+ flesh_fields => {
+ bre => ['call_numbers'],
+ acn => ['copies', 'uris'],
+ acp => ['location', 'stat_cat_entries', 'parts'],
+ ascecm => ['stat_cat', 'stat_cat_entry'],
+ acpm => ['part']
+ }
+ };
+
+ my $first = 1;
+ while(@recs) {
+ my ($id1, $id2, $id3) = (pop @recs, pop @recs, pop @recs);
+
+ for my $r ($req1, $req2, $req3) {
+ last unless $r;
+ my $bre = $r->gather(1);
+ my $xml = XML::LibXML->new->parse_string($bre->marc)->documentElement;
+ my $attrs = get_bib_attrs($xml);
+ $records{$bre->id}{record} = $bre;
+ $records{$bre->id}{$_} = $attrs->{$_} for keys %$attrs;
+ }
+
+ $req1 = ($id1) ? $ses1->request('open-ils.cstore.direct.biblio.record_entry.search', {id => $id1}, $query) : undef;
+ $req2 = ($id2) ? $ses1->request('open-ils.cstore.direct.biblio.record_entry.search', {id => $id2}, $query) : undef;
+ $req3 = ($id3) ? $ses1->request('open-ils.cstore.direct.biblio.record_entry.search', {id => $id3}, $query) : undef;
+
+ if($first) {
+ $records{$_}{counts} = $e->json_query({from => ['asset.record_copy_count', 1, $_, 0]})->[0] for @recs;
+ $first = 0;
+ }
+ }
+
+ for my $r ($req1, $req2, $req3) {
+ last unless $r;
+ my $bre = $r->gather(1);
+ my $xml = XML::LibXML->new->parse_string($bre->marc)->documentElement;
+ my $attrs = get_bib_attrs($xml);
+ $records{$bre->id}{record} = $bre;
+ $records{$bre->id}{$_} = $attrs->{$_} for keys %$attrs;
+ }
+
+
+ my $duration = time() - $start;
+
+ for my $rec_id (keys %records) {
+ my $rec = $records{$rec_id};
+ print sprintf("%d [%s] has %d volumes and %d copies\n",
+ $rec_id, $rec->{title},
+ scalar(@{$rec->{record}->call_numbers}),
+ scalar(map { @{$_->copies} } @{$rec->{record}->call_numbers}));
+ }
+
+ print "\n'direct' calls processing duration is $duration\n\n";
+}
+
+for (0..1) { direct(@recs); unapi(@recs); unapi_batch(@recs); unapi_spread(@recs); direct_spread(@recs); }
--- /dev/null
+<div style="width: 300px; height: 300px; overflow: auto;">
+ <script
+ src="[% ctx.media_prefix %]/js/ui/default/acq/common/claim_dialog.js">
+ </script>
+ <div><big>Claims</big></div>
+ <div>Against item:
+ <span id="acq-lit-li-claim-dia-li-title"></span>
+ (<span id="acq-lit-li-claim-dia-li-id"></span>)
+ </div>
+ <div id="acq-lit-li-claim-dia-show" class="hidden">
+ <ul id="acq-lit-li-claim-dia-lid-list">
+ <li name="lid">
+ <span name="barcode"></span> /
+ <span name="recvd"></span>
+ <ul name="claims">
+ <li name="claim">
+ <span name="type"></span>
+ <a name="voucher"
+ href="javascript:void(0);">Show Voucher</a>
+ </li>
+ </ul>
+ </li>
+ </ul>
+ <hr />
+ </div>
+ <div id="acq-lit-li-claim-dia-initiate" class="hidden">
+ <div><big>Initiate New Claims</big></div>
+ <div id="acq-lit-li-claim-dia-lid-list-init">
+ <div name="lid_to_claim">
+ <input type="checkbox" name="claimable_lid" />
+ <label name="claimable_lid_label">
+ <span name="barcode"></span> /
+ <span name="recvd"></span>
+ </label>
+ </div>
+ </div>
+ <hr />
+ <div id="acqclet-display" class="hidden">
+ <div><big>Select Claim Action</big></div>
+ <table>
+ <tbody id="acqclet-tbody">
+ <tr name="acqclet-template">
+ <td>
+ <input type="checkbox" name="acqclet-checkbox" />
+ </td>
+ <td style="padding-left: 1em;">
+ <label name="acqclet-label">
+ (${ou}) ${code} <em>${description}</em>
+ <span style="color: #069;">
+ ${library_initiated}</span>
+ </label>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ <hr />
+ </div>
+ <button id="acq-lit-li-claim-dia-claim">Claim selected</button>
+ </div>
+</div>
--- /dev/null
+<div id="acq-eligible-claim-controls">
+ <label for="acq-eligible-claim-type">Claim type:</label>
+ <span id="acq-eligible-claim-type"></span>
+ <label for="acq-eligible-claim-note">Note:</label>
+ <input dojoType="dijit.form.TextBox" id="acq-eligible-claim-note" />
+ <button id="acq-eligible-claim-submit">Claim</button>
+</div>
+
--- /dev/null
+[% which_lc = which | lower %]
+ <div id="acq-[% which_lc %]-info-div" class="hidden">
+ <div class="acq-menu-bar">
+ <div dojoType="dijit.form.Button" id="acq-[% which_lc %]-info-back-button">↖ [% IF which == "Lit" %]Return[% ELSE %]Hide[% END %]</div>
+ </div>
+ <table>
+ <tbody id="acq-[% which_lc %]-info-tbody">
+ <tr id="acq-[% which_lc %]-info-row"><td name="label"/><td name="value"/></tr>
+ </tbody>
+ </table>
+[% IF which == "Lit" %]
+ <div class="hidden" id="acq-[% which_lc %]-info-related">
+ Show the <a name="rel_link" href="#"><span name="related_number"></span> lineitem(s)</a> related to the same bibliographic record.
+ </div>
+ <div style="margin-top:40px;">
+ <h3 id="acq-[% which_lc %]-marc-order-record-label">MARC Order Record</h3>
+ <h3 id="acq-[% which_lc %]-marc-real-record-label">MARC ILS Record</h3>
+ <div>
+ <div dojoType="dijit.form.Button" jsId="acq[% which %]EditOrderMarc" class="hidden">Edit MARC Order Record</div>
+ </div>
+ <div id="acq-[% which_lc %]-marc-div" style="margin-top:20px;"> </div>
+ </div>
+[% END %]
+ </div>
--- /dev/null
+<script type="text/javascript" src="[% ctx.media_prefix %]/js/ui/default/acq/common/inv_dialog.js">
+</script>
+<big><strong>Choose invoice</strong></big>
+<table class="acq-link-invoice-dialog">
+ <tr>
+ <th>
+ <label for="acq-[% which %]-link-invoice-inv_ident">
+ Invoice #
+ </label>
+ </th>
+ <td>
+ <input id="acq-[% which %]-link-invoice-inv_ident"
+ dojoType="dijit.form.TextBox" />
+ </td>
+ </tr>
+ <tr>
+ <th>
+ <label for="acq-[% which %]-link-invoice-provider">Provider</label>
+ </th>
+ <td>
+ <span id="acq-[% which %]-link-invoice-provider"></span>
+ </td>
+ </tr>
+ <tr>
+ <td colspan="2" style="text-align: center;">
+ <button id="acq-[% which %]-link-invoice-link"
+ dojoType="dijit.form.Button" type="submit">Link</button>
+ </td>
+ </tr>
+</table>
--- /dev/null
+[%#-
+This template creates a split screen Dojo layout. The top frame
+of the screen holds a list of of JUBs, or titles. Clicking on a
+title in the top frame will load the purchase details for all the
+copies on order for that title into the bottom frame.
+
+To create a display for a set of JUBs, create a Dojo store and
+model for the set of JUBs, then place the following lines in your
+HTML where you want the display to appear:
+
+ <%namespace file='/oils/default/common/jubgrid.html' name='jubgrid'/>
+ ${jubgrid.jubgrid('dom_prefix', 'grid_jsid')}
+
+where 'dom_prefix' is a string that will be used as the prefix
+for the DOM notes that are created by this template, and
+'grid_jsid' is a valid JavaScript identifier that will name the
+DOM node to which the list of JUBs will be attached. For example
+
+ ${jubgrid.jubgrid('oils-acq-picklist', 'pickListGrid', hideDetails)}
+
+will create a Dojo grid with the DOM id of
+
+ 'oils-acq-picklist-JUB-grid'
+
+and a jsid of
+
+ pickListGrid
+
+To fill the grid with data, call the javascript function
+
+ JUBGrid.populate(grid_jsid, model)
+
+'grid_jsid' is the same javascript id that was used to
+instantiate the template, and model is a javascript variable
+pointing to the JUB model (and store) that you have created.
+-#%]
+
+[% UNLESS hide_details %]
+<div dojoType='dijit.layout.ContentPane' style='height:100%;'>
+[% END %]
+
+ <style type='text/css'>
+ .grid_container {width: 100%; height: 100%;}
+ </style>
+
+ <script type="text/javascript" src='[% ctx.media_prefix %]/js/ui/default/acq/common/jubgrid.js'> </script>
+ <script type="text/javascript" src='[% ctx.media_prefix %]/js/dojo/openils/CopyLocation.js'> </script>
+ <script type="text/javascript">
+ JUBGrid.getPO = function(rowIndex) {
+ var data = JUBGrid.jubGrid.model.getRow(rowIndex);
+ if (!(data && data.purchase_order)) return '';
+ return "<a href='[% ctx.base_path %]/acq/po/view/" + data.purchase_order+"'>"+data.purchase_order+"</a>";
+ }
+ JUBGrid.jubGridLayout = [{
+ //noscroll: true,
+ cells: [[
+ {name: 'ID', field: 'id', width:'auto'},
+ {name: 'Title', width: "180px", get:JUBGrid.getJUBTitle},
+ {name: 'Author', get:JUBGrid.getJUBAuthor, width:'auto'},
+ {name: 'ISBN', get:JUBGrid.getJUBIsbn, width:'auto'},
+ {name: 'Pubdate', get:JUBGrid.getJUBPubdate, width:'auto'},
+ {name: 'Actual Price',
+ field:'actual_price',
+ get:JUBGrid.getJUBActualPrice,
+ editor:dojox.grid.editors.Dijit, width:'auto',
+ editorClass: "dijit.form.CurrencyTextBox"
+ },
+ {name: 'Estimated Price',
+ field:'estimated_price',
+ get:JUBGrid.getJUBEstimatedPrice, width:'auto',
+ editor:dojox.grid.editors.Dijit,
+ editorClass: "dijit.form.CurrencyTextBox"
+ },
+ {name: 'Vendor', width:'auto',
+ field: 'provider', get:JUBGrid.getProvider,
+ editor:openils.editors.ProviderSelectEditor,
+ },
+ {name: 'No. Copies', field: 'item_count', width:'auto'},
+ {name: 'State', field: 'state', width:'auto'},
+ {name: 'PO', get:JUBGrid.getPO, width:'auto'}
+ ]]
+ }];
+
+ JUBGrid.jubDetailGridLayout = [{
+ cells: [[
+ {name:"ID", field:"id"},
+ {name:"Fund", field:"fund",
+ get:JUBGrid.getLIDFundCode,
+ editor: openils.editors.FundSelectEditor,
+ },
+ {name:"Branch", field:"owning_lib",
+ get:JUBGrid.getLIDLibName,
+ editor: openils.editors.OrgUnitSelectEditor
+ },
+ {name:"Barcode", field:"barcode", width:'auto',
+ editor:dojox.grid.editors.Dijit,
+ editorClass: "dijit.form.TextBox"
+ },
+ {name:"Call Number", field:"cn_label", width:'auto',
+ editor:dojox.grid.editors.Dijit,
+ editorClass: "dijit.form.TextBox"
+ },
+ {name:"Shelving Location", field:"location", width:'auto',
+ editor:openils.editors.CopyLocationSelectEditor,
+ get:JUBGrid.getCopyLocation
+ },
+ {name:"Receive Time", width:'auto',
+ get:JUBGrid.getRecvTime
+ },
+ ]]
+ }];
+
+ JUBGrid.jubDetailGridLayoutReadOnly = [{
+ cells: [[
+ {name:'ID', field:"id"},
+ {name:'Fund', field:"fund",
+ get:JUBGrid.getLIDFundCode,
+ },
+ {name:'Branch', field:"owning_lib",
+ get:JUBGrid.getLIDLibName,
+ },
+ {name:'Barcode', field:"barcode", width:'auto'},
+ {name:'Call Number', field:"cn_label", width:'auto'},
+ {name:'Shelving Location', field:"location",
+ width:'auto', get:JUBGrid.getCopyLocation},
+ ]]
+ }];
+ </script>
+
+[% UNLESS hide_details %]
+ <!-- button bar for lineitems -->
+ <script type="text/javascript">JUBGrid.showDetails = true;</script>
+ <div id="[% domprefix %]-container" class='container'
+ dojoType="dijit.layout.ContentPane" sizeMin="" sizeShare="">
+ <div dojoType="dijit.layout.ContentPane"
+ id='[% domprefix %]-jub-buttonbar'>
+ <button dojoType="dijit.form.Button" onclick="JUBGrid.approveJUB">
+ Approve Selected Titles
+ </button>
+ <button dojoType="dijit.form.Button" onclick="JUBGrid.removeSelectedJUBs">
+ Remove Selected Titles
+ </button>
+ </div>
+ </div>
+ <div style='height:40%;'>
+[% ELSE %]
+ <div style='height:100%;'>
+[% END %]
+ <div structure='JUBGrid.jubGridLayout' jsid='[% grid_jsid %]' class='grid_container'
+ dojoType='dojox.Grid' id="[% domprefix %]-JUB-grid">
+ </div>
+ </div>
+[% UNLESS hide_details %]
+ <!-- button bar for lineitem details -->
+ <div dojoType="dijit.layout.ContentPane" sizeMin="" sizeShare="" class='container'>
+ <div dojoType="dijit.layout.ContentPane" id='[% domprefix %]-details-buttonbar'>
+ <div dojoType="dijit.form.DropDownButton">
+ <span>New Copy</span>
+ <div dojoType="dijit.TooltipDialog" execute="JUBGrid.createLID(arguments[0]);">
+ <script type='dojo/connect' event='onOpen'>
+ new openils.User().buildPermOrgSelector('MANAGE_FUND', copyOwnerSelect);
+ openils.acq.Fund.buildPermFundSelector('MANAGE_FUND', acqlidFund);
+ </script>
+ <table class="dijitTooltipTable">
+ <tr>
+ <td><label for="fund">Fund: </label></td>
+ <td>
+ <input dojoType="openils.widget.FundSelector"
+ jsId="acqlidFund" searchAttr="name" autocomplete="true" name="fund"></input>
+ </td>
+ </tr>
+ <tr>
+ <td><label for="owning_lib">Location: </label></td>
+ <td><input dojoType="openils.widget.OrgUnitFilteringSelect"
+ jsId="copyOwnerSelect"
+ searchAttr="shortname"
+ name="owning_lib" autocomplete="true"
+ labelAttr="shortname"></input>
+ </td>
+ </tr>
+ <tr>
+ <td colspan="2" align="center">
+ <button dojotype="dijit.form.Button" type="submit">
+ Create
+ </button>
+ </td>
+ </tr>
+ </table>
+ </div>
+ </div>
+ <button dojoType='dijit.form.Button' onclick='JUBGrid.deleteLID'>
+ Delete Selected Copy
+ </button>
+ <button dojoType='dijit.form.Button' onclick='JUBGrid.receiveLID'>
+ Mark Selected Copies Received
+ </button>
+ </div>
+ </div>
+ <!-- end button bar -->
+
+ <div style='height:40%;'>
+ <div class='grid_container'>
+ <div structure='JUBGrid.jubDetailGridLayout' jsid="JUBGrid.jubDetailGrid" dojoType="dojox.Grid"
+ id='[% domprefix %]-details-grid'>
+ </div>
+ </div>
+ </div>
+</div>
+[% END %]
--- /dev/null
+<script type="text/javascript" src="[% ctx.media_prefix %]/js/ui/default/acq/common/base64.js"> </script>
+<script type="text/javascript" src='[% ctx.media_prefix %]/js/ui/default/acq/common/li_table.js'> </script>
+<script type="text/javascript" src='[% ctx.media_prefix %]/js/ui/default/acq/financial/claim_voucher.js'> </script>
+<div id='acq-lit-table-container'>
+ <div id='acq-lit-table-div' class='hidden'>
+
+ <!-- Lineitem (bib record) list -->
+ <table id='acq-lit-table' class='oils-generic-table'>
+ <thead>
+ <tr>
+ <th colspan='0'>
+ <table style='width:100%;'>
+ <tr>
+ <td>
+ <span>
+ <select id="acq-lit-li-actions-selector">
+ <option mask='*' value='_'>--Actions--</option>
+ <option mask='sr' value='save_picklist'>Save Items To Selection List</option>
+ <option mask='pl' value='selector_ready'>Mark Ready for Selector</option>
+ <option mask='pl' value='order_ready'>Mark Ready for Order</option>
+ <option mask='*' value='delete_selected'>Delete Selected Items</option>
+ <option mask='*' value='add_brief_record'>Add Brief Record</option>
+ <option mask='*' value='export_attr_list'>Export Single Attribute List</option>
+ <option mask='*' value='batch_apply_funds'>Apply Funds to Selected Items</option>
+ <option mask='po' value='' disabled='disabled'>----PO----</option>
+ <option mask='sr|pl' value='create_order'>Create Purchase Order</option>
+ <option mask='po' value='create_assets'>Load Bibs and Items</option>
+ <option mask='po' value='cancel_lineitems'>Cancel Selected Lineitems</option>
+ <option mask='po' value='change_claim_policy'>Change Claim Policy</option>
+ <option mask='po' value='receive_po'>Mark Purchase Order as Received</option>
+ <option mask='po' value='rollback_receive_po'>Un-Receive Purchase Order</option>
+ <option mask='po' value='print_po'>Print Purchase Order</option>
+ <option mask='po' value='po_history'>View PO History</option>
+ </select>
+ <span id="acq-lit-export-attr-holder" class="hidden">
+ <input dojoType="dijit.form.FilteringSelect" id="acq-lit-export-attr" jsId="acqLitExportAttrSelector" labelAttr="description" searchAttr="description" />
+ <span dojoType="dijit.form.Button" jsId="acqLitExportAttrButton">Export List</span>
+ </span>
+ <span id="acq-lit-cancel-reason" class="hidden">
+ <span id="acq-lit-cancel-reason-selector"></span>
+ <span dojoType="dijit.form.Button" jsId="acqLitCancelLineitemsButton">Cancel Line Items</span>
+ </span>
+ </span>
+ <span id='acq-lit-generic-progress' class='hidden'>
+ <span dojoType="dijit.ProgressBar" style="width:300px" jsId="litGenericProgress"></span>
+ </span>
+ </td>
+ <td>
+ <div style='width:100%;text-align:right;'>
+ <span style='padding-right:15px;'>
+ <a href='javascript:void(0);' id='acq-lit-prev' style='visibility:hidden'>« Previous</a>
+ <a href='javascript:void(0);' id='acq-lit-next' style='visibility:hidden'>Next »</a>
+ </span>
+ </div>
+ </td>
+ </tr>
+ </table>
+ </th>
+ </tr>
+ </thead>
+ <tbody><tr><td colspan='0' style='height:20px;'/></tr></tbody>
+ <tbody style='font-weight:bold;border:1px solid #aaa;'>
+ <tr>
+ <td><span><a id='acq-lit-select-toggle' href='javascript:void(0);'>✓</a></span></td>
+ <td>Line Items</td>
+ <td>Items</td>
+ <td>Notes</td>
+ <td>Actions</td>
+ <td>Status</td>
+ <td>Estimated Price</td>
+ </tr>
+ </tbody>
+ <tbody id='acq-lit-tbody'>
+ <tr id='acq-lit-row' class='acq-lit-row'>
+ <td name='selector'><input type='checkbox' name='selectbox'/></td>
+ <td style='width:75%;'>
+ <table style='width:100%;'>
+ <tbody>
+ <tr>
+ <td rowspan='3' style='width:43px;'><img style='width:40px;height:65px;' name='jacket'></td>
+ <td style='width:70%;font-weight:bold;'>
+ <span name="bib_origin" class="hidden">
+ <img src="/opac/images/book-icon.png" />
+ </span><a attr='title' href='javascript:void(0);'></a>
+ </td>
+ <td rowspan='2' style='text-align:right'>
+ </td>
+ </tr>
+ <tr class='acq-lit-alt-row'>
+ <td colspan='0'>
+ <span attr='author'></span>
+ <span attr='isbn'></span>
+ <span attr='issn'></span>
+ <span attr='edition'></span>
+ <span attr='pubdate'></span>
+ <span attr='publisher'></span>
+ <span name='source_label'></span>
+ </td>
+ </tr>
+ <tr>
+ <td colspan='0'>
+ <span name="liid"># </span>
+ <span name="catalog" class='hidden'> | <a title='Show In Catalog' name="catalog_link" href="javascript:void(0);">➟ catalog</a></span>
+ <span name="link_to_catalog" class='hidden'> | <a title='Link To Catalog Record' name="link_to_catalog_link" href="javascript:void(0);">➾ link to catalog</a></span>
+ <span name="worksheet"> | <a title='Generate Worksheet' name="worksheet_link" href="javascript:void(0);">✍ worksheet</a></span>
+ <span name='pl' class='hidden'> | <a title='Select List' name='pl_link' href='javascript:void(0);'>❖ </a></span>
+ <span name='po' class='hidden'> | <a title='Purchase Order' name='po_link' href='javascript:void(0);'>⌘ </a></span>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </td>
+ <td><a title='FOOOBAR' name='copieslink' href='javascript:void(0);'>Copies(<span name='count'>0</span>)</a></td>
+ <td>
+ <a name='noteslink' href='javascript:void(0);'>Notes(<span name='notes_count'>0</span>)</a><span name="notes_alert_flag"></span>
+ </td>
+ <td>
+ <select name='actions'>
+ <option name='action_none'>-- Actions --</option>
+ <option name='action_mark_recv' disabled='disabled'>Mark Received</option>
+ <option name='action_mark_unrecv' disabled='disabled'>Un-Receive</option>
+ <option name='action_update_barcodes' disabled='disabled'>Update Barcodes</option>
+ <option name='action_holdings_maint' disabled='disabled'>Holdings Maint.</option>
+ <option name='action_new_invoice' disabled='disabled'>New Invoice</option>
+ <option name='action_link_invoice' disabled='disabled'>Link to Invoice</option>
+ <option name='action_view_invoice' disabled='disabled'>View Invoice(s)</option>
+ <option name='action_view_claim_policy'>Apply Claim Policy</option>
+ <option name='action_manage_claims' disabled='disabled'>Claims</option>
+ <option name='action_view_history'>View History</option>
+ </select>
+ </td>
+ <td><span name='li_state'></span></td>
+ <td><input type='text' size='8' name='price'/></td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+
+ <!-- Bib record / Lineitem info table -->
+ [% INCLUDE "acq/common/info.tt2" which = "Lit" %]
+
+ <!-- Lineitem notes table -->
+ [% INCLUDE "acq/common/notes.tt2" which = "Lit" %]
+
+ <!-- Copies table -->
+ <div id='acq-lit-li-details' class='hidden'>
+
+ <div id='acq-lit-copies-li-summary'></div>
+
+ <h3>Add/Edit Items</h3>
+ <hr/>
+
+ <div class='acq-lit-li-menu-bar'>
+ <table style='width:100%'>
+ <tr>
+ <td class="acq-lit-li-menu-left">
+ <div dojoType='dijit.form.Button' id='acq-lit-copies-back-button' scrollOnFocus='false'>↖ Return</div>
+ <span style='margin-left:10px;padding-left:10px;border-left:2px solid #aaa;'>
+ Item Count:
+ <input dojoType='dijit.form.NumberTextBox' jsId='acqLitCopyCountInput'
+ constraints="{min:0,max:1000,places:0}" style='width:40px' value='0'></input>
+ <div dojoType='dijit.form.Button' jsId='acqLitAddCopyCount' scrollOnFocus='false'>Go</div>
+ </span>
+ <span style='margin-left:10px;padding-left:10px;border-left:2px solid #aaa;'>
+ <div dojoType='dijit.form.Button' jsId='acqLitSaveCopies' scrollOnFocus='false'>Save Changes</div>
+ </span>
+ <span id='acq-lit-update-copies-progress' class='hidden'>
+ <span dojoType="dijit.ProgressBar" style="width:300px" jsId="litUpdateCopiesProgress"></span>
+ </span>
+ </td>
+ </tr>
+ </table>
+ </div>
+
+ <hr/>
+ <table id='acq-lit-distrib-formula-table'>
+ <tbody id='acq-lit-distrib-formula-tbody'>
+ <tr id='acq-lit-distrib-form-row'>
+ <td colspan='0'>
+ <span>Distribution Formulas</span>
+ <div name='selector'></div>
+ <div name='set_button'></div>
+ <div name="reset_button"></div>
+ </td>
+ </tr>
+ </tbody>
+ <tbody id="acq-lit-distrib-applied-tbody" class="hidden">
+ <tr>
+ <td colspan="5" id="acq-lit-distrib-applied-heading">
+ Distribution formulas applied to this lineitem:
+ </td>
+ </tr>
+ <tr id="acq-lit-distrib-applied-row" class="acq-lit-distrib-applied-row">
+ <th></th>
+ <td colspan="4"></td>
+ </tr>
+ </tbody>
+ <table>
+
+ <table id='acq-lit-li-details-table'>
+ <tbody><tr><td class='acq-lit-table-spacer' colspan='0'/></tr></tbody>
+ <tbody style='font-weight:bold;'>
+ <tr>
+ <td style='margin-top:30px;'>Owning Branch</td>
+ <td>Shelving Location</td>
+ <td>Collection Code</td>
+ <td>Fund</td>
+ <td>Circ Modifier</td>
+ <td>Callnumber</td>
+ <td colspan='0'></td>
+ </tr>
+ </tbody>
+ <tbody style='background-color:#ddd;'>
+ <tr id='acq-lit-li-details-batch-row'>
+ <td><div name='owning_lib'></div></td>
+ <td><div name='location'></div></td>
+ <td><div name='collection_code'></div></td>
+ <td><div name='fund'></div></td>
+ <td><div name='circ_modifier'></div></td>
+ <td><div name='cn_label'></div></td>
+ <td colspan='3' style='text-align:left;'>
+ <div dojoType='dijit.form.Button' jsId='acqLitBatchUpdateCopies' scrollOnFocus='false'>Batch Update</div>
+ </td>
+ </tr>
+ </tbody>
+
+
+ <tbody><tr><td class='acq-lit-table-spacer' colspan='0'></td></tr></tbody>
+ <tbody style='font-weight:bold;'>
+ <tr>
+ <td style='margin-top:30px;'>Owning Branch</td>
+ <td>Shelving Location</td>
+ <td>Collection Code</td>
+ <td>Fund</td>
+ <td>Circ Modifier</td>
+ <td>Callnumber</td>
+ <td>Barcode</td>
+ <td>Notes</td>
+ <td>Receiver</td>
+ <td colspan='0'></td>
+ </tr>
+ </tbody>
+ <tbody id='acq-lit-li-details-tbody' class='oils-generic-table'>
+ <tr id='acq-lit-li-details-row'>
+ <td><div name='owning_lib'></div></td>
+ <td><div name='location'></div></td>
+ <td><div name='collection_code'></div></td>
+ <td><div name='fund'></div></td>
+ <td><div name='circ_modifier'></div></td>
+ <td><div name='cn_label'></div></td>
+ <td><div name='barcode'></div></td>
+ <td><div name='note'></div></td>
+ <td><div name='receiver'></div></td>
+ <td><a href="javascript:void(0);" name="receive">Mark Received</a><a href="javascript:void(0);" name="unreceive">Un-Receive</a> <a href="javascript:void(0);" name="cancel">Cancel</a><span class="hidden" name="cancel_reason"></span> <a href="javascript:void(0);" name="claim">Claim</a></td>
+ <td><div name='delete' dojoType='dijit.form.Button' style='color:red;' scrollOnFocus='false'>X</div></td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+
+
+ <!-- Copies table -->
+ <div id='acq-lit-real-copies-div' class='hidden'>
+ <h2>Copies</h2>
+
+ <div class='acq-lit-li-menu-bar'>
+ <table style='width:100%'>
+ <tr>
+ <td style='text-align:left;'>
+ <div dojoType='dijit.form.Button' id='acq-lit-real-copies-back-button' scrollOnFocus='false'>↖ Return</div>
+ </td>
+ <td style='text-align:right;'>
+ <span>
+ <div dojoType='dijit.form.Button' jsId='acqLitSaveRealCopies' scrollOnFocus='false'>Save Changes</div>
+ </span>
+ </td>
+ </tr>
+ </table>
+ </div>
+
+ <table id='acq-lit-real-copies-table'>
+ <tbody style='font-weight:bold;'>
+ <tr>
+ <td style='margin-top:30px;'>Owning Branch</td>
+ <td>Shelving Location</td>
+ <td>Circ Modifier</td>
+ <td>Callnumber</td>
+ <td>Barcode</td>
+ <td colspan='0'></td>
+ </tr>
+ </tbody>
+ <tbody id='acq-lit-real-copies-tbody' class='oils-generic-table'>
+ <tr id='acq-lit-real-copies-row'>
+ <td><div name='owning_lib'></div></td>
+ <td><div name='location'></div></td>
+ <td><div name='circ_modifier'></div></td>
+ <td><div name='label'></div></td>
+ <td><div name='barcode'></div></td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+
+ <div class="hidden">
+ <div jsId="acqLitLinkInvoiceDialog" dojoType="dijit.Dialog">
+ [% INCLUDE "acq/common/inv_dialog.tt2" which = "li" %]
+ </div>
+ </div>
+
+ <div class='hidden' id='acq-lit-progress-numbers'>
+ <table class='oils-generic-table'>
+ <tbody>
+ <tr>
+ <td>Lineitems Processed</td>
+ <td><span id='acq-pl-lit-li-processed'>0</span></td>
+ </tr>
+ <tr>
+ <td>Items Processed</td>
+ <td><span id='acq-pl-lit-lid-processed'>0</span></td>
+ </tr>
+ <tr>
+ <td>Debits Encumbered</td>
+ <td><span id='acq-pl-lit-debits-processed'>0</span></td>
+ </tr>
+ <tr>
+ <td>Bib Records Imported</td>
+ <td><span id='acq-pl-lit-bibs-processed'>0</span></td>
+ </tr>
+ <tr>
+ <td>Bib Records Indexed</td>
+ <td><span id='acq-pl-lit-indexed-processed'>0</span></td>
+ </tr>
+ <tr>
+ <td>Copies Processed</td>
+ <td><span id='acq-pl-lit-copies-processed'>0</span></td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+
+ <div class='hidden'>
+ <div dojoType='dijit.Dialog' jsId='acqLitPoCreateDialog'>
+ <table class='oils-generic-table'>
+ <tr>
+ <td>Ordering Agency</td>
+ <td><div name='ordering_agency' id='acq-lit-po-agency'></div></td>
+ </tr>
+ <tr>
+ <td>Provider</td>
+ <td><div name='provider' id='acq-lit-po-provider'></div></td>
+ </tr>
+ <tr>
+ <td>Prepayment Required</td>
+ <td><input id="acq-lit-po-prepay" name="prepayment_required" dojoType="dijit.form.CheckBox"/></td>
+ </tr>
+ <tr>
+ <td>All Lineitems</td>
+ <td><input checked='checked' name='create_from' value='all' dojoType='dijit.form.RadioButton'/></td>
+ </tr>
+ <tr>
+ <td>Selected Lineitems</td>
+ <td><input name='create_from' value='selected' dojoType='dijit.form.RadioButton'/></td>
+ </tr>
+ <tr>
+ <td>Import Bibs and Create Copies</td>
+ <td><input name='create_assets' dojoType='dijit.form.CheckBox'/></td>
+ </tr>
+ <tr>
+ <td colspan='2'>
+ <div dojoType='dijit.form.Button' type='submit' jsId='acqLitCreatePoSubmit'>Submit</div>
+ </td>
+ </tr>
+ </table>
+ </div>
+ </div>
+
+ <div class="hidden">
+ <div dojoType="dijit.Dialog" jsId='acqLitSavePlDialog'>
+ <table class='dijitTooltipTable'>
+ <tr>
+ <td colspan='2'>
+ <input dojoType="dijit.form.RadioButton" name="which" type='radio' checked='checked' value='selected'/>
+ <label for="name">Save selected</label>
+ <input dojoType="dijit.form.RadioButton" name="which" type='radio' value='all'/>
+ <label for="name">Save all</label>
+ </td>
+ </tr>
+ <tr><td colspan='2'><hr/></td></tr>
+ <tr>
+ <td><label for="new_name">Save as Selection List: </label></td>
+ <td><input dojoType="dijit.form.TextBox" name="new_name"/></td>
+ </tr>
+ <tr>
+ <td><label for="existing_pl">Add to Selection List: </label></td>
+ <td>
+ <input jsId="acqLitAddExistingSelect" dojoType="openils.widget.PCrudAutocompleteBox" fmclass="acqpl" searchAttr="name" name="existing_pl" />
+ </td>
+ </tr>
+ <tr>
+ <td colspan='2' align='center'>
+ <button dojoType='dijit.form.Button' type="submit" jsId='acqLitSavePlButton'>Save</button>
+ </td>
+ </tr>
+ </table>
+ </div>
+
+ <div dojoType="dijit.Dialog" jsId="lidCancelDialog">
+ <label for="acq-lit-lid-cancel-reason">Reason:</label>
+ <span id="acq-lit-lid-cancel-reason"></span>
+ <span dojoType="dijit.form.Button"
+ jsId="acqLidCancelButton">Cancel Copy</span>
+ </div>
+ <div dojoType="dijit.Dialog" jsId="liClaimPolicyDialog">
+ <label for="acq-lit-li-claim-policy">Claim policy:</label>
+ <span id="acq-lit-li-claim-policy"></span>
+ <sp