#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
</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
+ <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"
+</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>
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. Allows static files to be served from an alternate domain/server
+
+ Examples:
+
+ # local URL path
+ <media_prefix>/media</media_prefix>
+
+ # server w/ path.
+ <media_prefix>static.example.com/media</media_prefix>
+
+ ===
+ In the first 2 examples, the request protocol (http vs https) will
+ match the protocol of the current page
+ ===
+
+ # full-qualified with static protocol
+ <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.
XML parsing adds overhead, so this should only be used for debugging -->
<force_valid_xml>false</force_valid_xml>
+ <!-- Turn on template-toolkit debugging, which reports on undefined blocks, macros, etc. -->
+ <debug_template>false</debug_template>
+
+ <!--
+ Supported locales. Locales with no message catalog will use the native template strings.
+ All locales will fall back to native strings when a given string is not in the catalog
+ -->
+ <locales>
+ <en_US/>
+ <en_CA>/openils/var/data/locale/messages.en_CA.po</en_CA>
+ <fr_CA>/openils/var/data/locale/messages.fr_CA.po</fr_CA>
+ </locales>
+
<!-- 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 -->
);
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;
--- /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';
+
+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|;
+ return $self->load_rresults if $path =~ m|opac/results|;
+ return $self->load_record if $path =~ m|opac/record|;
+
+ return $self->load_mylist_add if $path =~ m|opac/mylist/add|;
+ 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;
+ }
+
+ 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;
+ 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/);
+
+ # 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;
+}
+
+
+# -----------------------------------------------------------------------------
+# 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 = {
+ barcode => $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->{username} = delete $args->{barcode}
+ 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 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;
+}
+
+# context additions:
+# user : au object, fleshed
+sub load_myopac_prefs {
+ my $self = shift;
+ return $self->prepare_extended_user_info || 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);
+
+ # 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;
+
+ my $circ = OpenSRF::AppSession->create('open-ils.circ');
+
+ if(!$hold_ids) {
+
+ $hold_ids = $circ->request(
+ 'open-ils.circ.holds.id_list.retrieve.authoritative',
+ $e->authtoken,
+ $e->requestor->id
+ )->gather(1);
+
+ $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 $e = $self->editor;
+ my $url;
+
+ my @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 $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(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 $e = $self->editor;
+ my $cgi = $self->cgi;
+ $self->ctx->{page} = 'place_hold';
+
+ $ctx->{hold_target} = $cgi->param('hold_target');
+ $ctx->{hold_type} = $cgi->param('hold_type');
+ $ctx->{default_pickup_lib} = $e->requestor->home_ou; # XXX staff
+
+ if ($ctx->{hold_type} eq 'T') {
+ $ctx->{record} = $e->retrieve_biblio_record_entry($ctx->{hold_target});
+ } elsif ($ctx->{hold_type} eq 'I') {
+ my $iss = $e->retrieve_serial_issuance([
+ $ctx->{hold_target}, {
+ "flesh" => 2,
+ "flesh_fields" => {
+ "siss" => ["subscription"], "ssub" => ["record_entry"]
+ }
+ }
+ ]);
+ $ctx->{record} = $iss->subscription->record_entry;
+ }
+ # ...
+
+ $ctx->{marc_xml} = XML::LibXML->new->parse_string($ctx->{record}->marc);
+
+ if(my $pickup_lib = $cgi->param('pickup_lib')) {
+
+ my $args = {
+ patronid => $e->requestor->id,
+ titleid => $ctx->{hold_target}, # XXX
+ pickup_lib => $pickup_lib,
+ depth => 0, # XXX
+ };
+
+ my $allowed = $U->simplereq(
+ 'open-ils.circ',
+ 'open-ils.circ.title_hold.is_possible',
+ $e->authtoken, $args
+ );
+
+ if($allowed->{success} == 1) {
+ my $hold = Fieldmapper::action::hold_request->new;
+
+ $hold->pickup_lib($pickup_lib);
+ $hold->requestor($e->requestor->id);
+ $hold->usr($e->requestor->id); # XXX staff
+ $hold->target($ctx->{hold_target});
+ $hold->hold_type($ctx->{hold_type});
+ # frozen, expired, etc..
+
+ my $stat = $U->simplereq(
+ 'open-ils.circ',
+ 'open-ils.circ.holds.create',
+ $e->authtoken, $hold
+ );
+
+ if($stat and $stat > 0) {
+ # if successful, return the user to the requesting page
+ $self->apache->log->info("Redirecting back to " . $cgi->param('redirect_to'));
+ return $self->generic_redirect;
+
+ } else {
+ $ctx->{hold_failed} = 1;
+ }
+ } else { # hold *check* failed
+ $ctx->{hold_failed} = 1; # XXX process the events, etc
+ $ctx->{hold_failed_event} = $allowed->{last_event};
+ }
+
+ # hold permit failed
+ $logger->info('hold permit result ' . OpenSRF::Utils::JSON->perl2JSON($allowed));
+ }
+
+ return Apache2::Const::OK;
+}
+
+
+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 $cstore = OpenSRF::AppSession->create('open-ils.cstore');
+
+ 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 $circs = $e->json_query({
+ from => ['action.usr_visible_circs', $e->requestor->id],
+ #limit => $limit || 25,
+ #offset => $offset || 0,
+ });
+
+ # XXX: order-by in the json_query above appears to do nothing, so in-query
+ # paging is not reallly an option. do the sorting/paging here
+
+ # sort newest to oldest
+ $circs = [ sort { $b->{xact_start} cmp $a->{xact_start} } @$circs ];
+ my @ids = map { $_->{id} } @$circs;
+
+ # find the selected page and trim cruft
+ @ids = @ids[$offset..($offset + $limit - 1)] if $limit;
+ @ids = grep { defined $_ } @ids;
+
+ $ctx->{circs} = $self->fetch_user_circs(1, \@ids);
+ #$ctx->{circs} = $self->fetch_user_circs(1, [map { $_->{id} } @$circs], $limit, $offset);
+
+ 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 $holds = $e->json_query({
+ from => ['action.usr_visible_holds', $e->requestor->id],
+ limit => $limit || 25,
+ offset => $offset || 0
+ });
+
+ $ctx->{holds} = $self->fetch_user_holds([map { $_->{id} } @$holds], 0, 1, 0, $limit, $offset);
+
+ return Apache2::Const::OK;
+}
+
+sub load_myopac_payment_form {
+ my $self = shift;
+ my $r;
+
+ $r = $self->prepare_fines(undef, undef, [$self->cgi->param('xact')]) 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;
+
+ $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')]) 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"))))
+ );
+ 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
+ }
+ );
+ }
+
+ $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') || '';
+
+ 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);
+
+ my $url = $self->apache->unparsed_uri;
+ $url =~ s/update_email/prefs/;
+
+ return $self->generic_redirect($url);
+}
+
+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'},
+ # XXX what to do about the possibility of really large bookbags here?
+ {"flesh" => 1, "flesh_fields" => {"cbreb" => ["items"]}, %$args}
+ ]);
+
+ 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
+# 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) = @_;
+ 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 @del_item = $cgi->param('del_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)
+
+ } 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 (@del_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);
+}
+
+# Removes a record ID from My List, or moves to an actual bookbag
+sub load_mylist_move {
+ my $self = shift;
+ my @rec_ids = $self->cgi->param('record');
+
+ 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 $self->cgi->param('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;
+
+ return $self->generic_redirect(
+ undef,
+ $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 $copy_rec = OpenSRF::AppSession->create('open-ils.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}'});
+ $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);
+
+ # 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}});
+ }
+
+ for my $expand ($self->cgi->param('expand')) {
+ $ctx->{"expand_$expand"} = 1;
+ if ($expand eq 'marchtml') {
+ $ctx->{marchtml} = $self->mk_marc_html($rec_id);
+ } elsif ($expand eq 'issues' and $ctx->{have_holdings_to_show}) {
+ $ctx->{expanded_holdings} =
+ $self->get_expanded_holdings($rec_id, $org, $depth);
+ }
+ }
+
+ 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 => {},
+ acpl => {},
+ ccs => {},
+ circ => {type => 'left'},
+ aou => {}
+ }
+ },
+ where => {
+ '+acp' => {
+ deleted => 'f',
+ call_number => {
+ in => {
+ select => {acn => ['id']},
+ from => 'acn',
+ where => {record => $rec_id}
+ }
+ },
+ circ_lib => {
+ in => {
+ select => {aou => [{
+ column => 'id',
+ transform => 'actor.org_unit_descendants',
+ result_field => 'id',
+ params => [$depth]
+ }]},
+ from => 'aou',
+ where => {id => $org}
+ }
+ }
+ },
+ '+acn' => {deleted => 'f'},
+ '+circ' => {checkin_time => undef}
+ },
+
+ # Order is: copies with circ_lib=org, followed by circ_lib name, followed by call_number label
+ order_by => [
+ {class => 'aou', field => 'name'},
+ {class => 'acn', field => 'label'}
+ ],
+
+ limit => $copy_limit,
+ offset => $copy_offset
+ };
+
+ # Filter hidden items if this is the public catalog
+ unless($self->ctx->{is_staff}) {
+ $query->{where}->{'+acp'}->{opac_visible} = 't';
+ $query->{where}->{'+acpl'}->{opac_visible} = 't';
+ $query->{where}->{'+ccs'}->{opac_visible} = 't';
+ }
+
+ return $query;
+ #return $self->editor->json_query($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) = @_;
+
+ return (
+ create OpenSRF::AppSession("open-ils.serial")->request(
+ "open-ils.serial.bib.summary_statements",
+ $rec_id, {"org_id" => $org, "depth" => $depth}
+ )->gather(1)
+ );
+}
+
+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");
+
+ return create OpenSRF::AppSession("open-ils.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);
+}
+
+
+sub get_hold_copy_summary {
+ my ($self, $rec_id, $org) = @_;
+
+ my $req1 = OpenSRF::AppSession->create('open-ils.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;
+}
+
+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;
+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/;
+ $parts{$_} = [ $cgi->param($_) ] for (@part_names);
+
+ my @chunks = ();
+ for (my $i = 0; $i < scalar @{$parts{'qtype'}}; $i++) {
+ my ($qtype, $contains, $query) = map { $parts{$_}->[$i] } @part_names;
+
+ next unless $query =~ /\S/;
+ push(@chunks, $qtype . ':') unless $qtype eq 'keyword' and $i == 0;
+
+ # 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 . '$';
+ }
+ push @chunks, $query;
+ }
+
+ return join(' ', @chunks);
+}
+
+sub _prepare_biblio_search {
+ my ($cgi, $ctx) = @_;
+
+ my $query = _prepare_biblio_search_basics($cgi);
+
+ $query = ('#' . $_ . ' ' . $query) foreach ($cgi->param('modifier'));
+
+ # filters
+ foreach (grep /^fi:/, $cgi->param) {
+ /:(-?\w+)$/ or next;
+ my $term = join(",", $cgi->param($_));
+ $query .= " $1($term)" if length $term;
+ }
+
+ if ($cgi->param('sort')) {
+ my ($axis, $desc) = split /\./, $cgi->param('sort');
+ $query .= " sort($axis)";
+ $query .= '#descending' if $desc;
+ }
+
+ 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';
+ my $page = $cgi->param('page') || 0;
+ my $facet = $cgi->param('facet');
+ my $limit = $self->_get_search_limit;
+ my $loc = $cgi->param('loc');
+ my $offset = $page * $limit;
+
+ my ($query, $site, $depth) = _prepare_biblio_search($cgi, $ctx);
+
+ 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");
+
+ my $results;
+
+ 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};
+
+ 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}',
+ 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;
+}
+
+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;
+ $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;
+ }
+
+ 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}
+ }) 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 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';
my $web_config;
my $web_config_file;
my $web_config_edit_time;
+my %lh_cache; # locale handlers
sub import {
my $self = shift;
$web_config_file = shift || '';
unless(-r $web_config_file) {
- warn "Invalid web config $web_config_file";
+ warn "Invalid web config $web_config_file\n";
return;
}
check_web_config();
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 $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'
+ }
});
- unless($tt->process($template, {ctx => $ctx})) {
- $r->log->warn('Template error: ' . $tt->error);
+ unless($tt->process($template, {ctx => $ctx, ENV => \%ENV, l => set_text_handler($ctx, $r)})) {
+ $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};
+ $locale =~ s/-/_/g;
+
+ $r->log->debug("egweb: messages locale = $locale");
+
+ unless($lh_cache{$locale}) {
+ $r->log->info("egweb: Unsupported locale: $locale");
+ $lh_cache{$locale} = $lh_cache{'en_US'};
+ }
+
+ return $OpenILS::WWW::EGWeb::I18NFilter::maketext =
+ sub { return $lh_cache{$locale}->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");
};
sub load_context {
my $r = shift;
my $cgi = CGI->new;
- my $ctx = $web_config->{ctx};
+ my $ctx = {}; # new context for each page load
+ $ctx->{$_} = $web_config->{base_ctx}->{$_} for keys %{$web_config->{base_ctx}};
$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->{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});
+
+ 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;
}
last unless $localpath;
for my $tpath (@{$ctx->{template_paths}}) {
my $fpath = "$tpath/$skin/$localpath.$ext";
- $r->log->debug("looking at possible template $fpath");
+ $r->log->debug("egweb: looking at possible template $fpath");
if(-r $fpath) {
$template = "$localpath.$ext";
last;
# no template configured or found
unless($template) {
- $r->log->warn("No template configured for path $path");
+ $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);
}
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;
+ $r->log->debug("egweb: 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 = $ctx->{locales};
+
+ $locales->{en_US} = {} unless exists $locales->{en_US};
+
+ for my $lang (keys %$locales) {
+ my $messages = $locales->{$lang};
+ $messages = '' if ref $messages; # empty {}
+
+ # TODO Can we do this without eval?
+ my $eval = <<EVAL;
+ package OpenILS::WWW::EGWeb::I18N::$lang;
+ use base 'OpenILS::WWW::EGWeb::I18N';
+ if(\$messages) {
+ use Locale::Maketext::Lexicon::Gettext;
+ if(open F, '$messages') {
+ our %Lexicon = (%Lexicon, %{ Locale::Maketext::Lexicon::Gettext->parse(<F>) });
+ close F;
+ } else {
+ warn "EGWeb: unable to open messages file: $messages";
+ }
+ }
+EVAL
+ eval $eval;
+ warn "$@\n" if $@; # TODO better logging
+ $lh_cache{$lang} = "OpenILS::WWW::EGWeb::I18N::$lang"->new;
+ }
+}
+
+
+
sub parse_config {
my $cfg_file = shift;
my $data = XML::Simple->new->XMLin($cfg_file);
$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->{force_valid_xml} = ( ($data->{force_valid_xml}||'') =~ /true/io) ? 1 : 0;
+ $ctx->{debug_template} = ( ($data->{debug_template}||'') =~ /true/io) ? 1 : 0;
$ctx->{default_template_extension} = $data->{default_template_extension} || 'tt2';
$ctx->{web_dir} = $data->{web_dir};
+ $ctx->{locales} = $data->{locales};
+ load_locale_handlers($ctx);
my $tpaths = $data->{template_paths}->{path};
$tpaths = [$tpaths] unless ref $tpaths;
}
}
- return {ctx => $ctx, handlers => $handlers};
+ return {base_ctx => $ctx, handlers => $handlers};
}
package PathConfig;
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;
+our $maketext;
+
+sub filter {
+ my ($self, $text, $args) = @_;
+ return $maketext->($text, @$args);
+}
+
+sub init {
+ my $self = shift;
+ $self->install_filter('l');
+ return $self;
+}
+
+1;
+
--- /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
+.sliderwrapper{
+position: relative; /*leave as is*/
+overflow: hidden; /*leave as is*/
+width: 675px; /*width of featured content slider*/
+height: 213px;
+}
+
+
+
+.sliderwrapper .contentdiv{
+visibility: hidden; /*leave as is*/
+position: absolute; /*leave as is*/
+left: 0; /*leave as is*/
+top: 0; /*leave as is*/
+width: 675px; /*width of content DIVs within slider. Total width should equal slider's inner width (390+5+5=400) */
+height: 100%;
+filter:progid:DXImageTransform.Microsoft.alpha(opacity=100);
+-moz-opacity: 1;
+opacity: 1;
+}
+
+.pagination{
+width: 400px; /*Width of pagination DIV. Total width should equal slider's outer width (400+10+10=420)*/
+text-align: right;
+background-color: navy;
+padding: 5px 10px;
+}
+
+.pagination a{
+padding: 0 5px;
+text-decoration: none;
+color: #00007D;
+background: white;
+}
+
+.pagination a:hover, .pagination a.selected{
+color: #000;
+background-color: #FEE496;
+}
\ No newline at end of file
--- /dev/null
+/* once done renaming and everything, combine with style.css */
+
+/* Verfied in-use classes --------------------------- */
+.opac-auto-004 { background: #E0F0E0; }
+.opac-auto-009 { border: 3px solid #E0E0E0; }
+.opac-auto-010 { border-bottom: 1px dotted #ccc; padding-top: 10px; }
+.opac-auto-011 { border-bottom: 1px dotted #ccc; padding-top: 6px; }
+.opac-auto-012 { border-bottom: none; }
+.opac-auto-013 { border-bottom: none; *height: 0px; }
+.opac-auto-015 { border-left: 1px solid #e9ebf3; padding-right: 27px; }
+.opac-auto-017 { border: none; width: 100%; }
+.opac-auto-018 { border-top: 1px dotted #ccc; padding-top: 17px; }
+.clear-both { clear: both; }
+.common-no-pad { clear: both; height: 0px; margin: 0px; padding: 0px; }
+.common-full-pad { clear: both; height: 15px; }
+.opac-auto-029 { color: #333; font-weight: bold; font-size: 13px; }
+.opac-auto-030 { color: #545454; }
+.opac-auto-031 { color: #9999FF; padding-left: 10px; font-size: 7pt; font-weight: 300; }
+.red { color: red; }
+.bold-red { color: red; font-weight: bold }
+.pointer { cursor: pointer; }
+/* ------------------------------------------- */
+
+
+#lib_selector_span { display: none }
+.float-left { float: left; }
+.bookbag-share { float: left; padding: 5px 0; }
+.bookbag-controls { float: left; padding: 5px 0px 0px 10px; }
+.left-corner { float: left; width: 163px; height: 30px; background: url('/images/utils-corner-mid.png') repeat-x top; }
+.float-right { float: right; }
+.opac-auto-045 { float: right; margin-right: 17px; }
+.opac-auto-046 { float: right; width: 214px; }
+.opac-auto-047 { float: right; width: 353px; background: #ccc; padding: 10px; margin-top: 7px; }
+.opac-auto-048 { float: right; width: 65px; }
+#they_said_dont_touch { float: right; width: 675px; height: 213px; color: green; overflow: hidden; }
+.opac-auto-050 { float: right; width: 85px; }
+.ten-px { font-size: 10px; }
+.eleven-px { font-size: 11px; }
+.eight-pt { font-size: 8pt; }
+.opac-auto-054 { font-size: 8pt; padding-left: 20px; }
+.bold { font-weight: bold; }
+.opac-auto-057 { font-weight: bold; padding: 5px; margin: 5px; width: 100%; }
+.opac-auto-058 { font-weight: bold; padding-left: 10px; }
+#anon_list_name { font-weight: bold; padding-right: 10px; }
+.opac-auto-060 { font-weight: normal; }
+.opac-auto-061 { height: 0px; border-top: 1px solid #b7b7b7; border-bottom: 1px solid #d4d4d4; margin: 15px 0px; }
+.small-height { height: 10px; }
+.normal-height { height: 15px; }
+.big-height { height: 20px; }
+.very-big-height { height: 30px; }
+#gold-links-holder { height: 24px; background: #252525; }
+.opac-auto-067 { margin: 3px; width: 100%; }
+.big-block { margin: auto; width: 974px; height: 0px; }
+.opac-auto-069 { margin-bottom: 10px; }
+.opac-auto-070 { margin-bottom: 20px; }
+.opac-auto-071 { margin-bottom: 5px; }
+.opac-auto-072 { margin-left: 20px; }
+.bookbag-list { margin-left: 5px; margin-top: 5px; width: 91%; border: 0;}
+#cn_browse_where { margin-left: 6px; }
+.opac-auto-075 { margin-right: 20px; }
+.opac-auto-076 { margin-right: 3px; }
+.opac-auto-077 { margin-right: 4px; position: relative; top: -10px; }
+.opac-auto-078 { margin-right: 7px; }
+.opac-auto-079 { margin-top: 10px; }
+.opac-auto-080 { margin-top: 10px; margin-bottom: 10px; }
+.opac-auto-081 { margin-top: 13px; }
+.opac-auto-082 { margin-top: 29px; }
+.opac-auto-083 { margin-top: 2px; }
+.opac-auto-084 { margin-top: 5px; }
+.opac-auto-085 { margin-top: 6px; margin-left: 20px; width: 250px; padding: 5px; }
+.opac-auto-086 { margin-top: 8px; }
+.opac-auto-087 { max-width: 11em; }
+.opac-auto-088 { padding: 0px; }
+.opac-auto-089 { padding: 0px 10px; }
+.opac-auto-090 { padding: 10px; }
+.opac-auto-091 { padding: 10px 0px; }
+.opac-auto-092 { padding: 4px; text-align: center; }
+.pad-bottom-five { padding: 5px; }
+.opac-auto-094 { padding: 5px 7px 0px 0px; }
+.opac-auto-095 { padding: 5px 7px 0px 0px; white-space: nowrap; }
+.opac-auto-096 { padding: 6px }
+.opac-auto-097 { padding: 8px 0px 6px 0px; width: 100%; border: 0; }
+.opac-auto-097b { padding: 8px 0px 6px 0px; border: 0; }
+.opac-auto-098 { padding-bottom: 10px; }
+.opac-auto-099 { padding-bottom: 12px; color: #666; }
+.opac-auto-100 { padding-bottom: 16px; }
+.opac-auto-101 { padding-bottom: 1px; }
+.opac-auto-102 { padding-bottom: 7px; }
+.opac-auto-103 { padding-left: 10px; }
+.opac-auto-104 { padding-left: 11px; padding-right: 11px; }
+.opac-auto-105 { padding-left: 11px; padding-right: 13px; }
+.opac-auto-106 { padding-left: 15px; }
+.opac-auto-107 { padding-left: 15px; padding-bottom: 10px; }
+.opac-auto-108 { padding-left: 5px; }
+.opac-auto-109 { padding-left: 5px; padding-bottom: 10px; }
+.opac-auto-110 { padding-left: 6px; }
+.opac-auto-111 { padding-left: 8px; }
+.opac-auto-112 { padding-left: 9px; }
+.opac-auto-113 { padding-right: 10px; }
+.opac-auto-114 { padding-right: 15px; padding-left: 15px; }
+.opac-auto-115 { padding-right: 20px; }
+.opac-auto-116 { padding-right: 5px; }
+.opac-auto-117 { padding-right: 7px; }
+.pad-top-ten { padding-top: 10px; }
+.opac-auto-119 { padding-top: 14px; }
+.opac-auto-120 { padding-top: 5px; }
+.opac-auto-121 { padding-top: 6px; }
+.opac-auto-122 { padding-top: 7px; }
+.opac-auto-123 { padding-top: 8px; }
+.pos-abs { position: absolute; }
+#new_cat_link_holder { position: absolute; z-index: 101; }
+#new_cat_link_holder a { display: block; width: 675px; height: 213px; }
+.pos-rel { position: relative; }
+#search-box table { position: relative; left: -10px; }
+.opac-auto-129 { position: relative; left: -19px; }
+.opac-auto-130 { position: relative; left: 80px; }
+.opac-auto-131 { position: relative; top: 0px; left: 55px; }
+.opac-auto-132 { position: relative; top: 13px; }
+.opac-auto-133 { position: relative; top: -13px; left: 2px; font-size: 10px; }
+.opac-auto-134 { position: relative; top: -15px; left: 172px; }
+.opac-auto-135 { position: relative; top: -15px; left: -23px; }
+.opac-auto-136 { position: relative; top: 161px; left: 172px; }
+.opac-auto-137 { position: relative; top: 161px; left: -23px; }
+#home_adv_search_link { position: relative; top: -1px; left: 10px; }
+#util_back_btn { position: relative; top: 1px; left: 10px; }
+.opac-auto-140 { position: relative; top: -2px; }
+#util_help_btn { position: relative; top: 2px; left: 40px; }
+#util_forw_btn { position: relative; top: 2px; left: 50px; }
+.opac-auto-143 { position: relative; top: -3px; }
+#util_home_btn { position: relative; top: 3px; left: 20px; }
+.opac-auto-145 { position: relative; top: -3px; left: 3px; }
+.opac-auto-146 { position: relative; top: -3px; left: -5px; }
+.opac-auto-147 { position: relative; top: -5px; }
+.pos-rel-top-5 { position: relative; top: 5px; }
+.opac-auto-149 { position: relative; top: 5px; left: 25px; }
+#util_print_btn { position: relative; top: 5px; left: 30px; }
+.opac-auto-151 { position: relative; top: 75px; }
+#adv_reset { position: relative; top: -9px; }
+.opac-auto-153 { position: relative; z-index: 100; }
+.text-center { text-align: center; }
+.opac-auto-156 { text-align: center; font-weight: bold; }
+#adv_quick_search_sidebar { text-align: center; margin-top: 20px; width: 400px; }
+.opac-auto-158 { text-align: center; margin-top: 6px; margin-bottom: 6px }
+.opac-auto-159 { text-align: center; padding: 20px; width: 100% }
+.opac-auto-160 { text-align: center; padding-bottom: 8px; }
+.opac-auto-161 { text-align: right; padding-right: 7px; width: 62px; }
+.opac-auto-162 { vertical-align: top; }
+.nowrap { white-space: nowrap; }
+.opac-auto-164 { white-space: nowrap; padding-left: 5px; }
+.full-width { width: 100%; }
+.opac-auto-167 { width: 100%; border: 1px solid black; padding: 6px; margin-top: 5px; }
+.opac-auto-168 { width: 100%; border: 2px solid #E0F0E0; margin-bottom: 20px; }
+.opac-auto-169 { width: 100%; height: 100%; }
+.opac-auto-170 { width: 100%; margin-top: 20px; border-top: 1px dotted #ccc; padding-top: 8px; }
+.opac-auto-171 { font-size: 120%; text-align: center; font-style: italic; }
+.opac-auto-172 { width: 100%; text-align: center; padding-bottom: 5px; }
+.opac-auto-173 { width: 10px; }
+.opac-auto-174 { width: 111px; height: 25px; }
+.opac-auto-175 { width: 125px; height: 21px; }
+.opac-auto-176 { width: 129px; }
+#search_box { width: 162px; }
+.opac-auto-178 { width: 174px; }
+.opac-auto-179 { width: 175px; margin-right: 11px; }
+.opac-auto-179 optgroup { margin-left: 1em; font-weight: normal; font-style: italic; }
+.opac-auto-179-inner-option { margin-left: 2em; } /* XXX ".opac-auto-179 optgroup option" doesn't work!? */
+.opac-auto-180 { width: 182px; color: black; padding: 5px 25px; }
+.opac-auto-181 { width: 195px; }
+.opac-auto-182 { width: 230px; text-align: left; margin-top: 3px; }
+.opac-auto-183 { width: 250px; text-align: left; }
+.opac-auto-184 { width: 26px; height: 23px; margin-top: 6px; margin-bottom: 6px; }
+.opac-auto-185 { width: 324px; }
+.opac-auto-186 { width: 400px; margin-top: 20px; }
+.opac-auto-187 { width: 662px; }
+.opac-auto-188 { width: 700px; height: 30px; border: 1px solid red; }
+.opac-auto-189 { width: 742px; float: left; }
+.opac-auto-190 { width: 88px; }
+.opac-auto-191 { width: 90px; }
+#cn_browse_div > div { width: 90%; text-align: center; margin: 10px; }
+.opac-auto-193 { width: 91px; }
+#cn_browse { width: 95%; text-align: center; padding: 15px; }
+.opac-auto-195 { width: 99%; text-align: center }
+#homesearch_thing { width: 664px; height: 117px; background: #bda964; }
+#mystery_thing { width: 664px; height: 35px; background: #ffffff; }
--- /dev/null
+body {
+ margin:0;
+ font-family: Arial, Helvetica, sans-serif;
+ font-size: 12px;
+ background:#333;
+}
+
+img {
+ border: none;
+}
+
+a {
+ color: #003399;
+ text-decoration: none;
+}
+
+a:hover {
+ text-decoration: underline;
+}
+
+#search-wrapper input[type=text] {
+ border:none;
+ margin:0;
+ padding:0;
+}
+
+#search-wrapper select {
+ border:1px solid #e9ebf3;
+ margin:0;
+ padding:0;
+ width:168px;
+}
+
+/*
+#search-wrapper select {
+ border:0px solid black;
+ filter:alpha(opacity=0);
+ -moz-opacity:0;
+ -khtml-opacity:0;
+ opacity:0;
+ padding:0;
+ margin:0;
+ height:18px;
+ font-size: 12px;
+}
+*/
+h1 {
+ margin:0;
+ margin-bottom: 5px;
+ font-size: 20px;
+ font-weight:normal;
+}
+
+h2 {
+ margin:0;
+ margin-bottom: 5px;
+ font-size: 14px;
+ font-weight:bold;
+}
+
+.hide_me, .hidden {
+ display: none;
+ visibility: hidden;
+}
+
+div.select-box-wrapper {
+ position:absolute;
+ padding-top:2px;
+ padding-left:3px;
+ overflow:hidden;
+ text-align:left;
+}
+
+div.select-wrapper {
+ border:1px solid #4C8AB0;
+ display:inline-block;
+ position:relative;
+ z-index:2;
+ background:url('/images/dropdown.gif') no-repeat right center;
+}
+
+div.select-wrapper:hover {
+ background:url('/images/dropdown-hover.gif') no-repeat right center;
+}
+
+#dash_wrapper {
+ width:500px;
+ position:relative;
+ top:-26px;
+}
+
+#dashboard {
+ clear:both;
+ float:right;
+ width:384px;
+}
+
+#dashboard span {
+ font-weight:bold;
+ position:relative;
+ left:-1px;
+}
+
+#dash_user {
+ font-weight: bold;
+ text-transform: capitalize;
+ position: relative;
+ top: 10px;
+}
+
+#dash_corner_mid1a {
+ vertical-align: top;
+ background: url('/images/dash-corner-mid1.png') repeat-x;
+ padding-left: 8px;
+}
+#dash_corner_mid1b {
+ background: url('/images/dash-corner-mid1.png') repeat-x;
+ padding: 0px 8px 0px 10px;
+}
+#dash_corner_mid1b img { position: relative; top: -1px; }
+#dash_corner_mid1c {
+ background: url('/images/dash-corner-mid1.png') repeat-x;
+ vertical-align: top;
+}
+#dash_corner_mid2a {
+ vertical-align: top;
+ width: 372px;
+ background: url('/images/dash-corner-mid2.png') repeat-x;
+}
+.dash-pos-out { position: relative; left: 3px; }
+.dash-pos-holds { position: relative; left: 100px; }
+.dash-align-out { text-align: right; width: 86px; }
+.dash-align-holds { text-align: right; width: 62px; }
+.dash-pos-pickup { position: relative; left: 170px; }
+.dash-align-pickup { text-align: right; width: 111px; }
+.dash-pos-fines { position: relative; left: 284px; }
+.dash-align-fines { text-align: right; width: 76px; }
+.pos-rel-top4 { position: relative; top: 4px; }
+#dash_number_row { position: relative; top: 6px; }
+#logout_link { left: 1px; }
+
+#dash_checked { color: #ffcc33; }
+#dash_holds { color: #ffcc33; }
+#dash_pickup { color: #1dd93c; }
+#dash_fines { color: #f41d36; }
+#header {
+ color: #fff;
+ padding: 26px 0px 26px 0px;
+ width: 974px;
+ margin: auto;
+ font-size:11px;
+}
+
+#header a {
+ color: #fff;
+}
+
+#header a:hover {
+ color: white;
+ text-decoration: none;
+}
+
+#header-links {
+ color: #afafaf;
+ font-size: 11px;
+ font-weight: bold;
+ position: relative;
+ top:4px;
+
+}
+
+#header-links a {
+ color: #afafaf;
+ display: block;
+ float:left;
+ margin-right:22px;
+}
+
+#header-links a:hover {
+ color: white;
+ text-decoration: none;
+}
+
+#header #header-links2 {
+ position:relative;
+ top:-8px;
+ color: white;
+ padding-bottom: 15px;
+}
+
+#header #header-links2 a {
+ color: white;
+}
+
+#header #header-links2 a:hover {
+ text-decoration: underline;
+}
+
+#header #your-acct-login {
+ padding-top:10px;
+}
+
+#gold-links {
+ margin:auto;
+ width:974px;
+ padding-left:0px;
+}
+
+#gold-links-home {
+ margin:auto;
+ width:694px;
+ padding-left:0px;
+}
+
+#util-bar {
+ margin:auto;
+ width:974px;
+ padding-left:0px;
+ height:0px;
+}
+
+#search-wrapper {
+ border-bottom: 1px solid #e9ebf3;
+ padding-bottom: 5px;
+ background: white;
+}
+
+#search_box_wrapper {
+ border:1px solid #e9ebf3;
+ padding: 1px;
+ padding-left: 3px;
+}
+
+#search-wrapper #breadcrumb {
+ margin-top:0px;
+ font-size: 10px;
+ float:left;
+}
+
+#search-wrapper #search-within {
+ margin-top:10px;
+ float:right;
+ position:relative;
+ left:-173px;
+}
+
+#search-wrapper #breadcrumb a {
+ color: black;
+}
+
+#search-wrapper #search_frm label {
+ font-size: 10px;
+}
+
+#search-wrapper #search-box {
+ width:974px;
+ margin:auto;
+ padding-left: 0px;
+}
+
+#utils {
+ float:right;
+ z-index:1;
+ width:150px;
+ height:30px;
+ background:url('/images/utils-corner-left.png') no-repeat left top;
+ padding-left: 3px;
+ color: white;
+ position:relative;
+}
+
+#utils a {
+ color: white;
+ font-size: 10px;
+}
+#adv_search_tabs {
+ height:33px;
+ width:974px;
+ margin:auto;
+}
+
+#adv_search_tabs a {
+ float: left;
+ display: block;
+ height:33px;
+ margin-right:7px;
+}
+
+#adv_search {
+ width:156px;
+ background:url('/images/adv_search_on.gif') no-repeat bottom;
+}
+
+#num_search {
+ width:156px;
+ background:url('/images/num_search_off.gif') no-repeat bottom;
+}
+
+#expert_search {
+ width:156px;
+ background:url('/images/expert_search_off.gif') no-repeat bottom;
+}
+
+#acct_tabs, #acct_fines_tabs {
+ height:33px;
+ width:974px;
+ margin:auto;
+}
+
+#acct_tabs a, #acct_fines_tabs a {
+ float: left;
+ display: block;
+ height:33px;
+ margin-right:7px;
+}
+
+.acct-tab {
+ background-repeat: no-repeat;
+ background-position: bottom;
+ width:156px;
+}
+
+.acct-main-off {
+ background-image:url('/images/acct_summary_off.gif');
+}
+.acct-main-on {
+ background-image:url('/images/acct_summary_on.gif');
+}
+
+.acct-circs-off {
+ background-image:url('/images/acct_checked_out_off.gif');
+}
+.acct-circs-on {
+ background-image:url('/images/acct_checked_out_on.gif');
+}
+
+.acct-holds-off {
+ background-image:url('/images/acct_holds_off.gif');
+}
+.acct-holds-on {
+ background-image:url('/images/acct_holds_on.gif');
+}
+
+.acct-prefs-off {
+ background-image:url('/images/acct_prefs_off.gif');
+}
+.acct-prefs-on {
+ background-image:url('/images/acct_prefs_on.gif');
+}
+
+.acct-lists-off {
+ background-image:url('/images/acct_lists_off.gif');
+}
+.acct-lists-on {
+ background-image:url('/images/acct_lists_on.gif');
+}
+
+#rdetail_header {
+ font-size:14px;
+ font-weight:bold;
+ color:#074079;
+ padding: 5px 7px 6px 0px;
+ border-bottom: 1px dotted #ccc;
+}
+
+#rdetail_result_count {
+ color: black;
+ font-size: 11px;
+ font-weight: normal;
+}
+
+#rdetail_result_nav {
+ float:right;
+ font-size: 11px;
+ font-weight:normal;
+}
+
+#rdetail_details_table {
+ margin-top: 15px;
+}
+
+#rdetail_title {
+ font-size: 18px;
+}
+
+#rdetail_image { border: none; }
+#rdetail_image_cell {
+ padding-top: 3px;
+ padding-right: 10px;
+}
+
+.rdetail_aux_utils {
+ border-left:1px dotted #ccc;
+ padding-left: 17px;
+ padding-bottom: 6px;
+ padding-right: 70px;
+}
+
+.results_aux_utils {
+ border-left:1px dotted #ccc;
+ padding-left: 17px;
+ padding-bottom: 6px;
+ padding-right: 50px;
+}
+
+#rdetails_status td, #rdetails_status2 td {
+ white-space:nowrap !important;
+ padding: 7px 0px 3px 13px;
+}
+
+#rdetails_status thead td {
+ background-color: #d8d8d8;
+ padding: 13px 0px 13px 13px;
+ font-size: 10px;
+ text-transform: uppercase;
+ font-weight: bold;
+}
+
+#rdetails_status tbody td {
+ padding-left: 13px;;
+}
+
+.rdetail_extras {
+ height: 29px;
+ background: #9ad0f1;
+ padding-top:1px;
+ margin-bottom: 10px;
+ margin-top: 10px;
+ clear:both;
+}
+
+.rdetail_extras_hr {
+ height: 1px;
+ background: #b7def5;
+ margin-left: 1px;
+ margin-right: 1px;
+}
+
+.rdetail_extras_link {
+ padding-top: 4px;
+ padding-left: 12px;
+ font-size: 10px;
+ text-transform: uppercase;
+ font-weight: bold;
+}
+
+.rdetail_extras_lbl {
+ position: relative;
+ top: -4px;
+ left: 7px;
+}
+
+#paginate-homebanner a.toc {
+ display:block;
+ width:20px;
+ height:20px;
+ background:gray;
+ float:left;
+ margin-left:2px;
+ margin-right:2px;
+ margin-top:2px;
+}
+
+#rdetail_extras_expand, #rdetail_extras_collapse, #rdetail_locs_collapse {
+ margin-left: 13px;
+}
+
+#rdetail_locs_expand, #rdetail_locs_collapse {
+ padding-bottom:3px;
+ margin-top:15px;
+ margin-left:13px;
+}
+
+#rdetail_anotes_div .biography {
+ margin:0;
+}
+
+#paginate-homebanner a.selected {
+ border: 2px solid black;
+ margin-top:0px;
+ margin-left:0px;
+ margin-right:0px;
+}
+
+#hp-buttons {
+ margin: auto;
+ margin-top: 6px;
+ width: 694px; /* 974px; */
+}
+
+#hp-welcome {
+ position:absolute;
+ width:295px;
+ height:192px;
+ background: url('/images/banner-bg.png') no-repeat;
+ color: #fff;
+ padding-left: 33px;
+ padding-top: 21px;
+ z-index:9999999999;
+}
+
+#hp-welcome h1 {
+ font-size: 25px;
+ margin-bottom:15px;
+}
+
+#hp-welcome a {
+ color: #fff;
+ text-decoration: underline;
+}
+
+#hp-banner {
+ margin: auto;
+ width: 694px; /* formerly 974px */
+ height: 213px;
+}
+
+#hp-ql-table {
+ margin-left: 2px;
+ padding-top: 3px;
+}
+
+#hp-ql-table a {
+ color: #333;
+ font-weight: bold;
+ font-size: 13px;
+ text-transform: uppercase;
+ text-decoration: none;
+ display: block;
+ width: 144px;
+ height: 25px;
+ padding-top: 9px;
+ padding-left: 15px;
+ background: url('/images/button-bg.png') no-repeat;
+}
+
+#hp-ql-bottom {
+ width: 640px;
+ height: 31px;
+ padding-left: 24px;
+ padding-top: 13px;
+ background: url('/images/hp-links-mid.jpg') repeat-x;
+}
+
+#hp-ql-bottom a {
+ display: inline-block;
+ text-decoration: none;
+ color: white;
+ font-size: 15px;
+ font-weight: bold;
+}
+
+#hp-ql-bottom img {
+ position:relative;
+ top:-1px;
+ left:2px;
+}
+
+.almost-content-wrapper {
+ background: white;
+}
+
+#content-wrapper {
+ background: white;
+ min-height: 260px;
+ border-bottom: 1px solid black;
+}
+
+.content-wrapper-record-page { top: -15px; position: relative; }
+
+#main-content-home { width: 694px; margin: auto; padding-left: 17px; }
+#main-content { width: 974px; margin:auto; padding-left: 0px; }
+
+#main-content .login_boxes {
+ border: 1px solid #dedede;
+ background:url('/images/login-bg.jpg') top repeat-x;
+ color: #333;
+}
+
+#main-content .login_boxes h1 {
+ font-weight: normal;
+ font-size: 25px;
+ margin:0;
+}
+
+#main-content .left_brain {
+ padding-left:28px;
+ padding-top:25px;
+}
+
+#main-content .left_brain input[type=text], #main-content .left_brain input[type=password] {
+ width:167px;
+ height:18px;
+ margin:0;
+ padding:0;
+ border:none;
+ background: none;
+ font-size: 15px;
+ color: #666;
+}
+
+#main-content .left_brain .input_bg {
+ padding:10px 10px 0px 13px;
+ background: url('/images/login-box-bg.jpg') no-repeat;
+ width:167px;
+ height:29px;
+}
+
+#home-buttons-inner {
+ width:664px;
+ height:117px;
+ background:#bda964;
+}
+
+#holds_temp_parent td {
+ border-bottom:1px solid #dcdbdb;
+}
+
+#holds_temp_parent input, #holds_temp_parent select {
+ margin:0;
+}
+
+
+
+
+#results_header_bar {
+ background: #929292;
+ border-top:1px solid #8b8b8b;
+}
+
+#results_header_inner {
+ height:32px;
+ width:974px;
+ margin:auto;
+ padding-top:6px;
+}
+
+.results_header_btns {
+ float:left;
+ margin-right: 6px;
+}
+
+.cached_list_div { width: 111px; height: 25px; }
+
+.results_header_div {
+ float: left;
+ width: 0px;
+ height: 25px;
+ border-left: 1px solid #7c7c7c;
+ border-right: 1px solid #9c9c9c;
+ margin: 0px 13px;
+}
+
+.results_header_lbl {
+ font-weight: bold;
+ float: left;
+ font-size: 11px;
+ color: #191919;
+ position: relative;
+ top: 5px;
+ margin-right: 6px;
+}
+
+.results_header_sel {
+ /* width: 88px; */
+ float:left;
+ position: relative;
+ top: 2px;
+ margin:0;
+}
+
+.results_header_nav1 {
+ padding: 5px 7px 6px 0px;
+ border-bottom: 1px dotted #ccc;
+}
+
+.results_header_nav1 .h1 {
+ font-size:14px;
+ font-weight:bold;
+ color:#074079;
+}
+
+.start_end_links_span {
+ font-size: 11px;
+}
+
+.nav_arrow_fix {
+ font-size:8px;
+ position:relative;
+ top:-1px;
+}
+
+#result_table_div {
+ margin-top: 20px;
+}
+
+.result_numbers {
+ font-size: 11px; padding-left:15px; white-space: nowrap; width: 320px;
+}
+
+.result_table_subtable { width: 100%; border-collapse: collapse; border: 0; }
+
+
+
+
+.icon_text {
+ text-transform:capitalize;
+}
+
+.result_table_title_cell {
+ padding-left: 7px;
+}
+
+#myopac_summary_div p {
+ margin:0;
+ margin-bottom: 10px;
+}
+
+#acct_sum_checked_table td {
+ padding-bottom:5px;
+}
+
+#zero_search_hits div { float:left;width:300px;margin-top:20px; }
+
+#zero_search_hits p {
+ margin-top:0;
+}
+
+#zero_hits_term {
+ text-transform: uppercase;
+ font-weight: bold;
+}
+
+#zero_search_hits #spell_check_link {
+ text-transform: uppercase;
+}
+
+#zero_search_hits #zero_hits_suggestions {
+ text-transform: uppercase;
+}
+
+.results_info_table td {
+ padding-right: 10px;
+}
+
+#myopac_holds_main_table {
+ border-collapse: collapse;
+}
+
+#myopac_holds_main_table td {
+ border: 1px solid black;
+
+}
+
+#myopac_prefs_div .data_grid {
+ border-collapse:collapse;
+}
+
+#myopac_prefs_div .data_grid td {
+ background:#f0f0f0;
+ border-bottom:3px solid white;
+ padding:6px 0px 7px 17px;
+}
+
+.header_middle {
+ height:22px;
+ font-size:14px;
+ font-weight:bold;
+ color:#074079;
+ padding: 0px 7px 0px 0px;
+ border-bottom: 1px dotted #ccc;
+}
+
+.header_middle a {
+ font-size: 12px;
+ font-weight: normal;
+}
+
+.acct_sum_row {
+ padding: 7px 15px;
+ width: 712px;
+ background: #f0f0f0;
+ margin-bottom: 2px;
+ font-size: 10px;
+ font-weight: bold;
+ text-transform: uppercase;
+}
+
+.acct_sum_row a {
+ text-transform: none;
+ font-size: 12px;
+ position:relative;
+ top:-1px;
+}
+
+.acct_sum_row .view_link {
+ font-weight: normal;
+ font-size:12px;
+}
+
+#myopac_sum_fines {
+ float:right;
+ padding: 15px 0px 0px 23px;
+ background: #f0f0f0;
+ width: 177px;
+ height: 166px;
+}
+
+#myopac_sum_fines_placehold { float: right; width: 177px; height: 166px; }
+
+.acct_holds_temp td {
+ text-align: left;
+}
+
+#acct_checked_tabs .align, #acct_holds_tabs .align, #acct_prefs_tabs .align {
+ float:left;
+ /*padding-left:10px;*/
+}
+
+#acct_checked_tabs .selected, #acct_holds_tabs .selected, #acct_prefs_tabs .selected {
+ /*background:url('/images/gray-arrow.png') left 3px no-repeat;*/
+}
+
+#acct_checked_main_header, #acct_holds_main_header, #acct_checked_hist_header, #acct_list_header, #acct_list_header_anon, #temp_list_holds {
+ font-weight:bold;
+ text-transform:uppercase;
+ font-size: 10px;
+}
+
+#acct_checked_main_header td, #acct_holds_main_header td, #acct_checked_hist_header td, #acct_list_header td, #acct_list_header_anon td, #temp_list_holds td {
+ background: #d8d8d8;
+ padding: 8px 0px 7px 0px;
+}
+
+#acct_list_header select, #acct_list_header_anon select {
+ font-weight:normal;
+ text-transform:none;
+}
+
+#acct_holds_activates_table label {
+ font-weight: bold;
+ font-size: 11px;
+ text-transform:uppercase;
+ padding-right: 5px;
+}
+
+.adv_search_font {
+ font-size: 10px;
+}
+
+.search_catalog_lbl {
+ font-size: 14px;
+}
+
+.lbl1 {
+ font-size:14px;
+ font-weight:bold;
+}
+
+.lbl2 {
+ font-size:10px;
+ font-weight:normal;
+ position:relative;
+ top:3px;
+}
+
+#myopac_tabs, #adv_search_parent, #fines_payments_wrapper {
+ background: #929292;
+ border-top:1px solid #8b8b8b;
+ padding-top:5px;
+ margin-bottom:20px;
+}
+
+#fines_payments_wrapper {
+ padding-left: 5px;
+ padding-right: 5px;
+}
+
+#myopac_payments_table th { text-align: left; }
+#myopac_payments_table tbody tr:nth-child(odd) { background-color: #ddd; }
+#myopac_payments_table form { display: inline; }
+#myopac_payments_table input[type="submit"] { padding: 1px; }
+
+.payment-error {
+ font-weight: bold; color: red;
+ padding: 10px; border: 1px solid #888;
+}
+
+#adv_search_parent {
+ margin-bottom:0px;
+}
+
+#myopac_loading {
+ width:100%;
+ text-align:center;
+ padding-top:20px;
+ font-size:16px;
+ font-weight:bold;
+}
+
+.chili_link {
+ width:100px !important;
+ text-align: center !important;
+}
+
+.chili_review div.chili_link div {
+ margin: auto;
+}
+
+/* some facet styling */
+.facetClassContainer { margin: 2px; border: 1px solid #CCC; }
+.facetClassLabelContainer { border: 1px solid #CCC; }
+.facetClassLabel { font-weight: bold; text-align: center; }
+.facetFieldContainer { }
+.facetFieldLabel { padding-left: 2px; margin-top: 5px; margin-bottom: 5px; font-weight: bold; text-align: left; }
+.extraFacetFieldsWrapper { }
+.toggleExtraFacetFieldsButton { float: right; margin: 0px; padding: 0px; }
+.facetFieldLineCount { display: inline-block; border-right: 1px solid #CCC; color: gray; width: 3em; margin-right: 3px }
+.facetField { border-top: 1px solid #CCC; }
+.facetFields { padding-left: 5px; }
+.facetFieldLineValue { overflow: hidden; text-overflow: ellipsis; }
+
+#footer {
+ padding-top:5px;
+ padding-bottom: 10px;
+ color: white;
+ margin: auto;
+ width: 974px;
+ color: #afafaf;
+ font-size: 11px;
+}
+
+#footer a {
+ color: white;
+ color: #afafaf;
+}
+
+.color_4 {
+ text-transform: uppercase;
+ font-weight: bold;
+ font-size: 10px;
+}
+
+.advanced_div { padding-top: 15px; }
+#adv_global_search select { width: 13em; }
+#adv_global_input_table select { width: 7em; }
+.adv_adv_link { font-size: 8pt; color: red; }
+#acct_prefs_header { float: left; }
+#limit_to_available {
+ float: left; position: relative; top: 2px;
+ left: -2px; margin-right: 4px;
+}
+#rdetail_copy_info_table { font-size: 8pt; }
+#rdetail_copy_info_table td { padding: 3px; }
+.search_page_nav_link { cursor: pointer; }
+#opac.result.sort { width: 160px; }
+.renew-summary { font-size: 125%; font-style: italic; margin: 0.5ex 0; }
+.failure-text { margin-left: 4em; font-style: italic; color: #ff0000; }
+.refine-controls { font-size: 125%; padding: 0.5ex 0; }
+#adv_search_refine input[type=text] { border: 1px inset #ccc !important; }
+#adv_search_refine select { border: 1px inset #ccc !important; }
+#adv_search_refine {
+ padding-left: 5em; background-color: #d7d7d7; margin: 2ex 0;
+}
+.row-remover { position: relative; top: 1px; vertical-align: middle; }
+.subtle-button {
+ background-color: #ffffff;
+ color: #003399; text-decoration: none;
+ font-size: 12px;
+ padding: 0; border: 0; margin: 0;
+ vertical-align: middle;
+}
+.subtle-button:hover { text-decoration: underline; cursor: pointer; }
+.no-dec:hover { text-decoration: none; }
+.pending-addr td { background-color: #ffcccc !important; border: 0 !important; }
+
+#account-update-email table { text-align: center; padding: 20px; margin-top: 30px; border-collapse: collapse; }
+#account-update-email table td { padding: 5px 15px 5px 15px; border-bottom: 1px solid #ddd; text-align: left;}
+#account-update-email-error { font-size: 1.5em; padding: 10px; border:1px solid #e9ebf3;}
+a.dash-link:hover { text-decoration: underline !important; }
+#list_create_table td { vertical-align: middle; padding: 0 8px; }
+#list_create_table {
+ background-color: #ccc;
+ padding-bottom: 4px;
+ margin-bottom: 10px;
+ border-bottom: 1px dotted #666;
+}
+.list-create-table-buttons input[type=image] { margin-top: 2px; }
+.result_table_format_cell { padding: 0px 10px; text-align: center; }
+#hold_editor h1 { font-size: 120%; font-weight: bold; }
+#hold_editor h2 { font-size: 111%; font-weight: normal; text-indent: 2em; font-style: italic; }
+#hold_editor h1, #hold_editor h2 { margin: 2px 0; }
+#hold_editor_table { background-color: #ddd; padding: 0.5em; }
+#hold_editor_table th { text-align: right; padding-right: 1em; }
+#hold_editor_table td { padding: 0.25em 0; }
+.fmt-note { vertical-align: middle; padding-left: 1em !important; }
+.hold-editor-controls { text-align: center; padding-top: 1em !important; }
+.hold-editor-controls a { padding-left: 2em; }
+
+.text-right { text-align: right; }
+.rdetail-author-div { padding-bottom: 10px; }
+
+.invisible { visibility: hidden; }
--- /dev/null
+/* Keep this dead simple. No dojo. */
+function $(s) { return document.getElementById(s); }
+function removeClass(node, cls) {
+ if (!node || !node.className) return;
+ node.className =
+ node.className.replace(new RegExp("\\b" + cls + "\\b", "g"), "");
+}
+function addClass(node, cls) {
+ if (!node) return;
+ removeClass(node, cls);
+ if (!node.className) node.className = cls;
+ else node.className += ' ' + cls;
+}
+function unHideMe(node) { removeClass(node, "hide_me"); }
+function hideMe(node) { addClass(node, "hide_me"); }
+
+var _search_row_template;
+function addSearchRow() {
+ if (!_search_row_template) {
+ t = $("adv_global_row").cloneNode(true);
+ t.id = null;
+ _search_row_template = t;
+ }
+
+ $("adv_global_tbody").insertBefore(
+ _search_row_template.cloneNode(true),
+ $("adv_global_addrow")
+ );
+}
+function print_node(node_id) {
+ var iframe = document.createElement("iframe");
+ var source_node = document.getElementById(node_id);
+ source_node.parentNode.appendChild(iframe);
+
+ var iwin = iframe.contentWindow;
+
+ /* These next three statements are only needed by IE, but they don't
+ * hurt FF/Chrome. */
+ iwin.document.open();
+ iwin.document.write( /* XXX make better/customizable? */
+ "<html><head><title>Recipt</title></head><body></body></html>"
+ );
+ iwin.document.close();
+
+ iwin.document.body.innerHTML = source_node.innerHTML;
+ iframe.focus();
+
+ try { iframe.print(); } catch (e) { iwin.print(); }
+ setTimeout(function() { iframe.style.display = "none"; }, 3500);
+}
--- /dev/null
+/* staff client integration functions */
+function debug(msg){dump(msg+'\n')}
+var eventCache={};
+function attachEvt(scope, name, action) {
+ if(!eventCache[scope]) eventCache[scope] = {};
+ if(!eventCache[scope][name]) eventCache[scope][name] = [];
+ eventCache[scope][name].push(action);
+}
+function runEvt(scope, name) {
+ debug('running event '+scope+':'+name);
+ var args = Array.prototype.slice.call(arguments).slice(2);
+ if(eventCache[scope]) {
+ var evt = eventCache[scope][name];
+ for(var i in evt) {evt[i].apply(evt[i], args);}
+ }
+}
+window.onload = function() {
+ // record details page events
+ var rec = location.href.match(/\/opac\/record\/(\d+)/);
+ if(rec && rec[1]) { runEvt('rdetail', 'recordRetrieved', rec[1]); }
+ // fire other events the staff client is expecting...
+}
--- /dev/null
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns='http://www.w3.org/1999/xhtml' lang='[% ctx.locale %]' xml:lang='[% ctx.locale %]'>
+ <head>
+ <title>[% ctx.page_title %]</title>
+ <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
+ [% BLOCK html_head; END; # provide a default that can be overridden %]
+ [% PROCESS html_head %]
+ </head>
+ <body>
+ [% content %]
+ </body>
+</html>
--- /dev/null
+[%
+ # Org Unit Selector Widget :
+ # PROCESS build_org_selector id='selector-id' name='selector-name'
+ BLOCK build_org_selector;
+ first_run = 0;
+ IF !org_unit;
+ org_unit = ctx.aou_tree;
+ first_run = 1;
+%]
+ <select id='[% id %]' name='[% name %]'>
+ [% END %]
+ <option value='[% org_unit.id %]' [% IF org_unit.id == value %] selected='selected' [% END %]>
+ [%
+ pad = org_unit.ou_type.depth * 2;
+ FOR idx IN [0..pad]; ' '; END;
+ org_unit.name;
+ %]
+ </option>
+ [% FOR child IN org_unit.children; PROCESS build_org_selector org_unit = child; END %]
+ [% IF first_run %]
+ </select>
+ [% END %]
+[% END %]
+
+[% PROCESS 'default/opac/marc_attrs.tt2' %]
+[% USE date;
+ USE money = format('$%.2f');
+ icon_by_mattype = { # XXX KCLS-specific
+ "a" => "media_book.jpg",
+ "b" => "media_magazines.jpg",
+ "c" => "media_printedmusic.jpg",
+ "d" => "media_microform.jpg",
+ "e" => "media_equipment.jpg",
+ "f" => "media_films.jpg",
+ "g" => "",
+ "h" => "media_dvd.jpg",
+ "i" => "media_bookoncassette.jpg",
+ "j" => "media_musiccd.jpg",
+ "k" => "media_musiccassette.jpg",
+ "l" => "media_musicrecord.jpg",
+ "m" => "media_software.jpg",
+ "n" => "media_bookoncd.jpg",
+ "o" => "media_kit.jpg",
+ "p" => "media_newspaper.jpg",
+ "q" => "media_largeprint.jpg",
+ "r" => "media_3dobject.jpg",
+ "s" => "media_slide.jpg",
+ "t" => "media_online.jpg",
+ "u" => "media_eaudio.jpg",
+ "v" => "media_ebooktext.jpg",
+ "w" => "media_eaudio.jpg",
+ "x" => "media_downloadmusic.jpg",
+ "y" => "media_downloadvideo.jpg",
+ "z" => "media_map.jpg",
+ "2" => "media_cassettewithbook.jpg",
+ "5" => "media_cdwithbook.jpg"
+ };
+%]
--- /dev/null
+[% ctx.page_title = "Home" %]
+
+[% BLOCK html_head %]
+<style>
+ #home_div { text-align: center; width: 100%; margin-top: 30px;}
+</style>
+[% END %]
+
+[% WRAPPER "default/opac/base.tt2" %]
+[% PROCESS "default/opac/common.tt2" %]
+
+<div id='home_div'>
+ <img src='/images/eg_logo.jpg'/>
+ <br/><br/>
+ <form action='./results' method='GET'>
+ <input type='text' name='query' size='50' value='[% query %]'/>
+ [% PROCESS build_org_selector name='loc' %]
+ <input type='submit' value='[% l('Go!') %]'/>
+ <input type='hidden' name='page' value='0'/>
+ </form>
+</div>
+
+
+[% END %]
--- /dev/null
+[% BLOCK html_head %]
+<style>
+</style>
+[% END %]
+
+[%
+ USE CGI;
+ WRAPPER "default/opac/base.tt2";
+ ctx.page_title = "Login";
+%]
+
+<div style='width:400px; text-align:center; border: 1px solid #888'>
+ <form method='POST'>
+ <table>
+ <tr>
+ <td>Username or Barcode</td>
+ <td><input name='username' type='text'/></td>
+ </tr>
+ <tr>
+ <td>Password</td>
+ <td><input name='password' type='password'/></td>
+ </tr>
+ <tr>
+ <td colspan='2' style='text-align:center'>
+ <input type='submit'/>
+ </td>
+ </tr>
+ </table>
+ <input type='hidden' name='redirect_to' value='[% CGI.param('redirect_to') || ctx.referer | replace('^http:', 'https:') %]'/>
+ </form>
+</div>
+[% END %]
--- /dev/null
+[%
+ # Extract MARC fields from XML
+ # get_marc_attrs( { marc_xml => doc } )
+ BLOCK get_marc_attrs;
+ xml = args.marc_xml;
+ args.isbn = xml.findnodes('//*[@tag="020"]/*[@code="a"]').textContent;
+ args.upc = xml.findnodes('//*[@tag="024"]/*[@code="a"]').textContent;
+ args.issn = xml.findnodes('//*[@tag="022"]/*[@code="a"]').textContent;
+ args.title = xml.findnodes('//*[@tag="245"]/*[@code="a"]').textContent;
+ args.author = xml.findnodes('//*[@tag="100"]/*[@code="a"]').textContent;
+ args.publisher = xml.findnodes('//*[@tag="260"]/*[@code="b"]').textContent;
+ args.pubdate = xml.findnodes('//*[@tag="260"]/*[@code="c"]').textContent;
+ args.mattype = xml.findnodes('//*[@tag="998"]/*[@code="d"]').textContent; # XXX this is KCLS-specific and will need to change
+
+ # clean up the ISBN
+ args.isbn_clean = args.isbn.replace('\ .*', '');
+ END;
+%]
--- /dev/null
+<div>[%
+pages = [
+ {url => "main", name => "My Account"},
+ {url => "circs", name => "Items Out"},
+ {url => "holds", name => "Items on Hold"},
+ {url => "fines", name => "Fines"},
+ {url => "prefs", name => "Account Preferences"},
+ {url => "bookbags", name => "My Bookbags"}
+];
+FOREACH page IN pages %]
+ <span style="margin: 0 0.5em;">
+ [% IF page.url != myopac_page %]<a href="[% page.url %]">[% END;
+ page.name;
+ IF page.url != myopac_page %]</a>
+ [% ELSE; ctx.page_title = page.name; END %]</span>
+[% END %]</div>
+<ul>
+ <li><a href="../home">Home</a></li>
+ <li><a href="../logout">Logout</a></li>
+</ul>
--- /dev/null
+[% BLOCK html_head %]
+<style>
+ table { width: 100%; text-align: center; padding: 20px; margin-top: 30px; }
+ table { border-collapse: collapse; }
+ table { padding: 3px; border-bottom: 1px solid #ddd; text-align: left;}
+ table tr:nth-child(odd) { background-color:#ded; }
+ #action_div { width: 95%; }
+ #action-buttons { float:right; }
+</style>
+[% END %]
+
+[% WRAPPER "default/opac/base.tt2" %]
+[% INCLUDE "default/opac/myopac/_links.tt2" myopac_page = "bookbags" %]
+
+[% IF ctx.bookbags.size == 0 %]
+<b>No Bookbags</b>
+[% STOP; END %]
+
+<form method='POST' id='bbag-form'>
+ <div id='action_div'>
+ <div id='action-buttons'>
+ <select name='action'>
+ <option value='delete'>Delete Selected</option>
+ <option value='delete_all'>Delete All</option>
+ </select>
+ <input type='submit' value='Go'/>
+ </div>
+ </div>
+ <table>
+ <thead>
+ <tr>
+ <th>Name</th>
+ <th>Public</th>
+ <th>Created On</th>
+ <th>Items</th>
+ </tr>
+ </thead>
+ <tbody>
+ [% FOR bbag IN ctx.bookbags %]
+ <tr [% IF loop.count % 2 == 1 %] class='bbag-table-odd' [% END %]>
+ <td>[% bbag.name %]</td>
+ <td>[% bbag.pub == 't' ? 'Yes' : 'No' %]</td>
+ <td>[% date.format(ctx.parse_datetime(bbag.create_time),'%Y-%m-%d') %]</td>
+ <td>XXX</td>
+ </tr>
+ [% END %]
+ </tbody>
+ </table>
+</form>
+
+
+[% END %]
--- /dev/null
+[% BLOCK html_head %]
+<style>
+ table { width: 100%; text-align: center; padding: 20px; margin-top: 30px; }
+ table { border-collapse: collapse; }
+ table { padding: 3px; border-bottom: 1px solid #ddd; text-align: left;}
+ #action_div { width: 95%; }
+ .renew-summary { float:left; padding-right: 10px;}
+ #action-buttons { float:right; }
+ .circ-table-odd { background-color:#ded; }
+ .failure-text { font-weight: bold; color: red; }
+ #circ-form { margin-top: 20px; }
+</style>
+[% END %]
+
+[% PROCESS "default/opac/common.tt2" %]
+[% WRAPPER "default/opac/base.tt2" %]
+[% INCLUDE "default/opac/myopac/_links.tt2" myopac_page = "circs" %]
+
+[% IF ctx.circs.size == 0 %]
+<b>No Items Checked Out</b>
+[% STOP; END %]
+
+<form method='POST' id='circ-form'>
+ <div id='action_div'>
+ [% IF ctx.success_renewals > 0 %]
+ <div class='renew-summary'><b>Successfully renewed [% ctx.success_renewals %] items.</b></div>
+ [% END %]
+ [% IF ctx.failed_renewals > 0 %]
+ <div class='renew-summary'><b>Failed to renew [% ctx.failed_renewals %] items.</b></div>
+ [% END %]
+ <div id='action-buttons'>
+ <button type='submit' value='renew' name='action'>Renew Selected</button>
+ <button type='submit' value='renew_all' name='action'>Renew All</button>
+ </div>
+ </div>
+ <table>
+ <thead>
+ <tr>
+ <th>Title</th>
+ <th>Author</th>
+ <th>Due Date</th>
+ <th>Renewals Remaining</th>
+ <th>Select</th>
+ </tr>
+ </thead>
+ <tbody>
+ [% FOR circ IN ctx.circs %]
+ [% attrs = {marc_xml => circ.marc_xml}; %]
+ [% PROCESS get_marc_attrs args=attrs; %]
+
+ <tr [% IF loop.count % 2 == 1 %] class='circ-table-odd' [% END %]>
+ <td><a href='../record/[% circ.circ.target_copy.call_number.record.id %]'>[% attrs.title %]</a></td>
+ <td><a href='../results?query=au:[% attrs.author | url %]'>[% attrs.author %]</a></td>
+ <td>[% date.format(ctx.parse_datetime(circ.circ.due_date),'%Y-%m-%d') %]</td>
+ <td><em>[% circ.circ.renewal_remaining %]</em></td>
+ <td><input name='circ' value='[% circ.circ.id %]' type='checkbox'
+ [% IF circ.circ.renewal_remaining < 1 %] disabled='disabled' [% END %]/></td>
+ </tr>
+
+ [% IF circ.renewal_response and circ.renewal_response.textcode != 'SUCCESS' %]
+ <tr [% IF loop.count % 2 == 1 %] class='circ-table-odd' [% END %]>
+ <td colspan='0'>
+ <div class='failure-text'>
+ [% circ.renewal_response.textcode %]
+ [% IF circ.renewal_response.payload.fail_part and circ.renewal_response.payload.fail_part != circ.renewal_response.textcode %]
+ [% circ.renewal_response.payload.fail_part %]
+ [% END %]
+ </div>
+ </td>
+ </tr>
+ [% END %]
+ [% END %]
+ </tbody>
+ </table>
+</form>
+
+[% END %]
--- /dev/null
+[% BLOCK html_head %]
+<style>
+ table { width: 100%; text-align: center; padding: 20px; margin-top: 30px; }
+ table { border-collapse: collapse; }
+ table { padding: 3px; border-bottom: 1px solid #ddd; text-align: left;}
+ table tr:nth-child(odd) { background-color:#ded; }
+ h2 { margin-bottom: 0; }
+</style>
+[% END %]
+
+[% PROCESS "default/opac/common.tt2" %]
+[% WRAPPER "default/opac/base.tt2" %]
+[% INCLUDE "default/opac/myopac/_links.tt2" myopac_page = "fines" %]
+<h2>Summary</h2>
+<table>
+ <thead>
+ <tr>
+ <th>Total Owed</th>
+ <th>Total Paid</th>
+ <th>Balance Owed</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>[% money(ctx.fines.total_owed) %]</td>
+ <td>[% money(ctx.fines.total_paid) %]</td>
+ <td>[% money(ctx.fines.balance_owed) %]</td>
+ </tr>
+ </tbody>
+</table>
+
+[% IF ctx.fines.circulation.size > 0 %]
+<h2>Overdue materials</h2>
+<table>
+ <thead>
+ <tr>
+ <th>Title</th>
+ <th>Author</th>
+ <th>Checkout Date</th>
+ <th>Due Date</th>
+ <th>Date Returned</th>
+ <th>Balance Owed</th>
+ </tr>
+ </thead>
+ <tbody>
+ [% FOR f IN ctx.fines.circulation %]
+ [% attrs = {marc_xml => f.marc_xml}; %]
+ [% PROCESS get_marc_attrs args=attrs; %]
+ <tr>
+ <td>[% attrs.title %]</td>
+ <td>[% attrs.author %]</td>
+ <td>[% date.format(
+ ctx.parse_datetime(f.xact.circulation.xact_start), "%Y-%m-%d"
+ ) %]</td>
+ <td>[% date.format(
+ ctx.parse_datetime(f.xact.circulation.due_date), "%Y-%m-%d"
+ ) %]</td>
+ <td>[%
+ IF f.xact.circulation.checkin_time;
+ date.format(
+ ctx.parse_datetime(f.xact.circulation.checkin_time),
+ "%Y-%m-%d"
+ );
+ END %]</td><!-- XXX TODO display stop_fines_time if set? Display something instead of blank like "fines accruing" ? -->
+ <td>[% money(f.xact.balance_owed) %]</td>
+ </tr>
+ [% END %]
+ </tbody>
+</table>
+[% END %]
+
+[% IF ctx.fines.grocery.size > 0 %]
+<h2>Other Fees</h2>
+<table>
+ <thead>
+ <tr>
+ <th>Transaction Start Time</th>
+ <th>Last Payment Time</th>
+ <th>Initial Amount Owed</th>
+ <th>Total Amount Paid</th>
+ <th>Balance Owed</th>
+ <th>Billing Type</th>
+ </tr>
+ </thead>
+ <tbody>
+ [% FOR f IN ctx.fines.grocery %]
+ <tr>
+ <td>[%
+ date.format(
+ ctx.parse_datetime(f.xact.xact_start), "%Y-%m-%d"
+ ) %]</td>
+ <td>[% IF f.xact.last_payment_ts;
+ date.format(
+ ctx.parse_datetime(f.xact.last_payment_ts), "%Y-%m-%d"
+ );
+ END %]</td>
+ <td>[% money(f.xact.total_owed) %]</td>
+ <td>[% money(f.xact.total_paid) %]</td>
+ <td>[% money(f.xact.balance_owed) %]</td>
+ <td>[% f.xact.last_billing_type %]</td>
+ </tr>
+ [% END %]
+ </tbody>
+</table>
+[% END %]
+
+[% END %]
--- /dev/null
+[% BLOCK html_head %]
+<style>
+ table { width: 100%; text-align: center; padding: 20px; margin-top: 30px; }
+ table { border-collapse: collapse; }
+ table { padding: 3px; border-bottom: 1px solid #ddd; text-align: left;}
+ table tr:nth-child(odd) { background-color:#ded; }
+ #action_div { width: 95%; }
+ #action-buttons { float:right; }
+</style>
+[% END %]
+
+[% PROCESS "default/opac/common.tt2";
+ WRAPPER "default/opac/base.tt2";
+ INCLUDE "default/opac/myopac/_links.tt2" myopac_page = "holds" %]
+
+[% IF ctx.holds.size == 0 %]
+<b>No Items On Hold</b>
+[% STOP; END %]
+
+<form method='POST'>
+
+ <div id='action_div'>
+ <div id='action-buttons'>
+ <select name='action'>
+ <option value='cancel'>Cancel Selected</option>
+ <option value='cancel_all'>Cancel All</option>
+ <option value='suspend'>Suspend Selected</option>
+ <option value='suspend_all'>Suspend All</option>
+ <option value='activate'>Activate Selected</option>
+ <option value='activate_all'>Activate All</option>
+ </select>
+ <input type='Submit' value='Go'/>
+ </div>
+ </div>
+ <table>
+ <thead>
+ <tr>
+ <th>Title</th>
+ <th>Author</th>
+ <th>Formats</th>
+ <th>Pickup Location</th>
+ <th>Status</th>
+ <th>Active</th>
+ <th>Activate on...</th>
+ <th>Expiration Date</th>
+ <th>Select</th>
+ </tr>
+ </thead>
+ <tbody>
+ [% FOR hold IN ctx.holds %]
+ [% attrs = {marc_xml => hold.marc_xml}; %]
+ [% PROCESS get_marc_attrs args=attrs; %]
+ <tr>
+ <td><a href='../record/[% hold.hold.bre.id %]'>[% attrs.title %]</a></td>
+ <td><a href='../results?query=au:[% attrs.author | url %]'>[% attrs.author %]</a></td>
+ <td>
+ [%
+ key = attrs.mattype;
+ format_desc = ctx.find_citm(key).value;
+ icon_filename = icon_by_mattype.$key;
+ IF icon_filename;
+ %]<!-- XXX in situations where we might show M-type holds, this won't be good enough -->
+ <img alt="[% format_desc %]" title="[% format_desc %]"
+ src="/images/mattype/[% icon_filename %]" />
+ [% END %]
+ </td>
+ <td>[% ctx.find_aou(hold.hold.hold.pickup_lib).name %]</td>
+ <td>[%
+ IF hold.hold.status == 4;
+ "Available";
+ ELSIF hold.hold.estimated_wait;
+ "Estimated wait (days): "; hold.hold.estimated_wait;
+ ELSIF hold.hold.status == 3;
+ "In Transit";
+ ELSIF hold.hold.status < 3;
+ "Waiting for copy";
+ END;
+ %]</td>
+ <td>[% IF hold.hold.hold.frozen == 't' %] No [% ELSE %] Yes [% END %]</td>
+ <td>[%
+ IF hold.hold.hold.frozen == 't' AND hold.hold.hold.thaw_date;
+ date.format(ctx.parse_datetime(hold.hold.hold.thaw_date),'%Y-%m-%d');
+ ELSE;
+ '-';
+ END
+ %]</td>
+ <td>[% hold.hold.hold.expire_time ? hold.hold.hold.expire_time : '-' %]</td>
+ <td><input type='checkbox' name='hold_id' value='[% hold.hold.hold.id %]'/></td>
+ </tr>
+ [% END %]
+ </tbody>
+ </table>
+</form>
+[% END %]
--- /dev/null
+[% BLOCK html_head %]
+<style>
+ table { text-align: center; padding: 20px; margin-top: 30px; border-collapse: collapse; }
+ table td { padding: 5px 15px 5px 15px; border-bottom: 1px solid #ddd; text-align: left;}
+ table tr:nth-child(odd) { background-color:#ded; }
+</style>
+[% END %]
+
+[% WRAPPER "default/opac/base.tt2" %]
+[% INCLUDE "default/opac/myopac/_links.tt2" myopac_page = "main" %]
+<table>
+ <tr>
+ <td>First Name</td>
+ <td>[% ctx.user.first_given_name %]</td>
+ </tr>
+ <tr>
+ <td>Middle Name</td>
+ <td>[% ctx.user.second_given_name %]</td>
+ </tr>
+ <tr>
+ <td>Last Name</td>
+ <td>[% ctx.user.family_name %]</td>
+ </tr>
+ <tr>
+ <td>Library Card</td>
+ <td>[% ctx.user.card.barcode %]</td>
+ </tr>
+ <tr>
+ <td>Email Address</td>
+ <td>[% ctx.user.email %]</td>
+ <td><a href='update_email'>Change</a></td>
+ </tr>
+ <tr>
+ <td>Phone</td>
+ <td>[% ctx.user.day_phone %]</td>
+ </tr>
+</table>
+
+[% END %]
--- /dev/null
+[% BLOCK html_head %]
+<style>
+ table { width: 100%; text-align: center; padding: 20px; margin-top: 30px; }
+ table { border-collapse: collapse; }
+ table { padding: 3px; border-bottom: 1px solid #ddd; text-align: left;}
+ table tr:nth-child(odd) { background-color:#ded; }
+</style>
+[% END %]
+
+[% WRAPPER "default/opac/base.tt2" %]
+[% INCLUDE "default/opac/myopac/_links.tt2" myopac_page = "prefs" %]
+<p><em>XXX TODO Need to find out whether the list of pref fields can/should be generated
+ from db entries or something.</em></p>
+
+[% END %]
--- /dev/null
+[% BLOCK html_head %]
+<style>
+ table { text-align: center; padding: 20px; margin-top: 30px; border-collapse: collapse; }
+ table td { padding: 5px 15px 5px 15px; border-bottom: 1px solid #ddd; text-align: left;}
+ table tr:nth-child(odd) { background-color:#ded; }
+</style>
+[% END %]
+
+[% WRAPPER "default/opac/base.tt2" %]
+[% INCLUDE "default/opac/myopac/_links.tt2" %]
+
+<form method='POST'>
+ <table>
+ <tr><td>Current Email</td><td>[% ctx.user.email %]</td></tr>
+ <tr><td>New Email</td><td><input type='text' name='email'/></td></tr>
+ </table>
+ <input type='submit'/>
+</form>
+
+[% END %]
--- /dev/null
+[% BLOCK html_head %]
+<style>
+</style>
+[% END %]
+
+[%
+ USE CGI;
+ PROCESS "default/opac/common.tt2";
+ WRAPPER "default/opac/base.tt2";
+ ctx.page_title = "Place Hold";
+ attrs = {marc_xml => ctx.marc_xml};
+ PROCESS get_marc_attrs args=attrs;
+%]
+
+
+<div>
+ <div>Placing hold on [% attrs.title %], by [% attrs.author %]</div>
+ [% IF ctx.hold_success %]
+ <div>Succeeded</div>
+ [% ELSIF ctx.hold_failed %]
+ <div>Failed...</div>
+ [% ELSE %]
+ <form action='place_hold' method='POST'>
+ Choose a pickup Library [% PROCESS build_org_selector name='pickup_lib' value=ctx.default_pickup_lib %]
+ <input type='Submit'/>
+ <input type='hidden' name='hold_target' value='[% CGI.param('hold_target') | html %]'/>
+ <input type='hidden' name='hold_type' value='[% CGI.param('hold_type') | html %]'/>
+ <input type='hidden' name='redirect_to' value='[% ctx.referer | html %]'/>
+ </form>
+ [% END %]
+</div>
+
+[% END %]
--- /dev/null
+[% BLOCK html_head %]
+<style>
+ table { width: 100%; padding: 20px; margin-top: 30px; }
+ table { border-collapse: collapse; }
+ table td { padding: 3px; border-bottom: 1px solid #ddd; text-align: left;}
+ table th { padding: 3px; border-bottom: 1px solid #ddd; text-align: left;}
+ table tr:nth-child(even) { background-color:#ded; }
+ #record_table td { padding-left: 15px; padding-right: 15px; }
+</style>
+[% END %]
+
+[%
+ WRAPPER "default/opac/base.tt2";
+ PROCESS "default/opac/common.tt2";
+ ctx.page_title = "Details";
+ record = ctx.record;
+ attrs = {marc_xml => ctx.marc_xml};
+ PROCESS get_marc_attrs args=attrs;
+%]
+
+<div id='detail_div'>
+ <table id='record_table' style='width:auto'>
+ <tr>
+ <td rowspan='10' style='width:55px; vertical-align:top; padding-right:4px;'>
+ [% IF attrs.isbn_clean || attrs.upc %]
+ <img width='50' height='70' src='[% ctx.media_prefix %]/opac/extras/ac/jacket/small/[% attrs.isbn_clean || attrs.upc %]'/>
+ [% END %]
+ </td>
+ </tr>
+ [% IF attrs.title %]<tr><td>Title</td><td>[% attrs.title %]</td></tr>[% END %]
+ [% IF attrs.author %]<tr><td>Author</td><td><a href='../results?query=au:[% attrs.author | uri %]'>[% attrs.author %]</a></td></tr>[% END %]
+ [% IF attrs.isbn %]<tr><td>ISBN</td><td>[% attrs.isbn %]</td></tr>[% END %]
+ [% IF attrs.issn %]<tr><td>ISSN</td><td>[% attrs.issn %]</td></tr>[% END %]
+ [% IF attrs.upc %]<tr><td>UPC</td><td>[% attrs.upc %]</td></tr>[% END %]
+ [% IF attrs.pubdate %]<tr><td>Publication Date</td><td>[% attrs.pubdate %]</td></tr>[% END %]
+ [% IF attrs.publisher %]<tr><td>Publishere</td><td>[% attrs.publisher %]</td></tr>[% END %]
+ <tr>
+ <td>Subjects</td>
+ <td>
+ [% FOR node IN ctx.marc_xml.findnodes('//*[@tag="650"]') %]
+ [%
+ s0 = node.childNodes.0.textContent;
+ s1 = node.childNodes.1.textContent;
+ %]
+ [% IF s0 %]
+ <a href='../results?query=su:[% s0 | url %]'>[% s0 %]</a>
+ [% IF s1 %]
+ <span>--</span>
+ <a href='../results?query=su:[% s1 | url %]'>[% s1 %]</a>
+ [% END %]
+ <br/>
+ [% END %]
+ [% END %]
+ </td>
+ </tr>
+ </table>
+ <table id='copy_table'>
+ <thead>
+ <tr>
+ <th>Owning Lib</th>
+ <th>Call Number</th>
+ <th>Barcode</th>
+ <th>Status</th>
+ </tr>
+ </thead>
+ <tbody>
+ [% FOR acn IN record.call_numbers %]
+ [% FOR acp IN acn.copies %]
+ <tr>
+ <td>[% ctx.find_aou(acn.owning_lib).shortname %]</td>
+ <td>[% acn.label %]</td>
+ <td>[% acp.barcode %]</td>
+ <td>[% ctx.find_ccs(acp.status).name %]</td>
+ </tr>
+ [% END %]
+ [% END %]
+ </tbody>
+ </table>
+</div>
+
+
+[% END %]
--- /dev/null
+[% BLOCK html_head %]
+<style>
+ #body_table { width: 100%; margin-top: 20px; }
+ #left_block { width: 15%; vertical-align: top; }
+ #right_block { width: auto; vertical-align: top;}
+ #record_table { border-collapse: collapse; width:100%; }
+ #record_table td { padding: 3px; border-bottom: 1px solid #ddd; }
+ #record_table tr:nth-child(odd) { background-color:#ded; }
+ .record-table-odd { background-color:#ded; }
+ #form_div { text-align: center; width: 100%; margin-top: 10px;}
+ #links_div { margin-bottom: 10px; padding: 5px;}
+</style>
+[% END %]
+
+[%
+ USE CGI;
+ USE POSIX;
+ WRAPPER "default/opac/base.tt2";
+ PROCESS "default/opac/common.tt2";
+ ctx.page_title = "Results";
+ page = CGI.param('page') || 0;
+ query = CGI.param('query');
+ page_count = POSIX.ceil(ctx.hit_count / ctx.page_size);
+ loc = CGI.param('loc');
+%]
+
+<div id='form_div'>
+ <form action='./results' method='GET'>
+ <input type='text' name='query' size='50' value='[% query %]'/>
+ [% PROCESS build_org_selector name='loc' value=loc %]
+ <input type='submit'/>
+ <input type='hidden' name='page' value='0'/>
+ </form>
+</div>
+
+<table id='body_table'>
+ <tr>
+ <td id='left_block'>
+ [% IF ctx.user; %]
+ <div id='links_div'>
+ <div><a href='home'>Home</a></div>
+ <div><a href='myopac/main'>Account</a></div>
+ <div><a href='logout'>Logout</a></div>
+ </div>
+ <hr/>
+ <table>
+ <tr><td colspan='2' style='border-bottom:1px solid #9A9'>Signed in as [% ctx.user.usrname %]</td></tr>
+ <tr><td>Total Holds</td><td>[% ctx.user_stats.holds.total %]</td></tr>
+ <tr><td>Ready Holds</td><td>[% ctx.user_stats.holds.ready %]</td></tr>
+ <tr><td>Items Out</td><td>[% ctx.user_stats.checkouts.out %]</td></tr>
+ <tr><td>Fines</td><td>$[% ctx.user_stats.fines.balance_owed %]</td></tr>
+ </table>
+ [% ELSE %]
+ [%
+ login = CGI.url("-path" => 1).replace('^http:', 'https:').replace('/results','/login');
+ %]
+ <a href='[% login %]'>Login</a>
+ [% END %]
+ </div>
+ <div>
+ [% FOR facet_type IN ctx.search_facets.keys %]
+ [% cmf = ctx.search_facets.$facet_type.cmf %]
+ <b>[% cmf.label %]</b>
+ <ul>
+ [% FOR facet IN ctx.search_facets.$facet_type.data.keys %]
+ [% facet_count = ctx.search_facets.$facet_type.data.$facet %]
+ <li><a href='results?query=[% query | url %]&facet=[% cmf.field_class %]|[% cmf.name %][[% facet | url %]]'>[% facet_count %] / [% facet %]</a></li>
+ [% END %]
+ </ul>
+ [% END %]
+ </div>
+ </td>
+ <td id='right_block'>
+ <div>
+ <span>[% l('Hits: [_1] / Page [_2] of [_3]', ctx.hit_count, page + 1, page_count) %]</span>
+ [%
+ q = query | url;
+ np_link = '?query=' _ q;
+ IF loc; np_link = np_link _ "&loc=" _ loc; END;
+ IF depth or depth == 0; np_link = np_link _ "&depth=" _ depth; END;
+ %]
+ <a [% IF page > 0 %] href='[% np_link %]&page=[% page - 1 %]' [% END %]>Prev</a>
+ <a [% IF (page + 1) < page_count %] href='[% np_link %]&page=[% page + 1 %]' [% END %]>Next</a>
+ </div>
+ <table id='record_table'>
+ [%
+ FOR rec IN ctx.records;
+ attrs = {marc_xml => rec.marc_xml};
+ PROCESS get_marc_attrs args=attrs;
+ %]
+ <tr [% IF loop.count % 2 == 1 %] class='record-table-odd' [% END %]>
+ <td style='width:52px;height:72px'>
+ [% IF attrs.isbn %]
+ <img width='50' height='70' src='[% ctx.media_prefix %]/opac/extras/ac/jacket/small/[% attrs.isbn_clean || attrs.upc %]'/>
+ [% END %]
+ </td>
+ <td width='auto'>
+ <div width='99%'>
+ <div style='float:left'>
+ <a href='record/[% rec.bre.id %]'>[% attrs.title %]</a>
+ </div>
+ <div style='float:right'>
+ <span>[% rec.copy_counts.available %] / [% rec.copy_counts.visible %]</span>
+ <span style='padding-left:10px;'><a href='place_hold?hold_target=[% rec.bre.id %]&hold_type=T'>Hold</a></span>
+ </div>
+ </div><br/>
+ <div>[% attrs.author %]</div>
+ <div>[% attrs.isbn || attrs.issn || attrs.upc %] [% attrs.publisher %] [% attrs.pubdate %]</div>
+ </td>
+ </tr>
+ [% END %]
+ </table>
+ </td>
+ </tr>
+
+</table>
+[% END %]
--- /dev/null
+[% PROCESS "default/opac/parts/header.tt2";
+ WRAPPER "default/opac/parts/base.tt2";
+ INCLUDE "default/opac/parts/topnav.tt2";
+ ctx.page_title = l("Advanced Search") %]
+ <div id="search-wrapper">
+ [% INCLUDE "default/opac/parts/printnav.tt2" %]
+ <div id="adv_search_parent">
+ <div id="adv_search_tabs">
+ <a href="#" alt="[% l('Advanced Search') %]" id="adv_search"></a>
+<!-- <a href="#" alt="[% l('Numeric Search') %]" id="num_search"></a>
+ <a href="#" alt="[% l('Expert Search') %]" id="expert_search"></a> -->
+ </div>
+ </div>
+ </div>
+ <div id="content-wrapper">
+ <div id="main-content">
+ <div class="advanced_div">
+ [% INCLUDE "default/opac/parts/advanced/search.tt2" %]
+ </div>
+ <div class="common-full-pad"></div>
+ </div>
+ </div>
+[% END %]
--- /dev/null
+[% PROCESS "default/opac/parts/header.tt2";
+ WRAPPER "default/opac/parts/base.tt2";
+ INCLUDE "default/opac/parts/topnav.tt2";
+ ctx.page_title = l("Home") %]
+ <div id="search-wrapper">
+ [% INCLUDE "default/opac/parts/printnav.tt2" %]
+ [% INCLUDE "default/opac/parts/searchbar.tt2" %]
+ </div>
+ <div id="content-wrapper">
+ <div id="main-content-home">
+ <div class="common-full-pad"></div>
+ [% INCLUDE "default/opac/parts/homesearch.tt2" %]
+ <div class="common-full-pad"></div>
+ </div>
+ </div>
+[% END %]
--- /dev/null
+[% PROCESS "default/opac/parts/header.tt2";
+ WRAPPER "default/opac/parts/base.tt2";
+ INCLUDE "default/opac/parts/topnav.tt2";
+ ctx.page_title = l("Account Login") %]
+ <div id="search-wrapper">
+ [% INCLUDE "default/opac/parts/printnav.tt2" %]
+ [% INCLUDE "default/opac/parts/searchbar.tt2" %]
+ </div>
+ <div id="content-wrapper">
+ <div id="main-content">
+ [% INCLUDE "default/opac/parts/login/form.tt2" %]
+ <div class="clear-both very-big-height"></div>
+ <script type="text/javascript">
+ /* Note: when common browsers suppor HTML5 "autofocus", we can remove this */
+ var _onload = window.onload;
+ window.onload = function() {
+ try {
+ document.getElementById("username_field").focus();
+ if (_onload) _onload();
+ } catch (E) {
+ void(0);
+ }
+ };
+ </script>
+ </div>
+ </div>
+[% END %]
--- /dev/null
+[% PROCESS "default/opac/parts/header.tt2";
+ PROCESS "default/opac/parts/misc_util.tt2";
+ WRAPPER "default/opac/parts/base.tt2";
+ INCLUDE "default/opac/parts/topnav.tt2";
+ ctx.page_title = l("Record Detail") %]
+ <div id="search-wrapper">
+ [% INCLUDE "default/opac/parts/printnav.tt2" %]
+ [% INCLUDE "default/opac/parts/searchbar.tt2" %]
+ </div>
+ <div id="content-wrapper">
+ <div id="main-content">
+ [% IF ctx.mylist.size;
+ INCLUDE "default/opac/parts/anon_list.tt2";
+ ELSE %]
+ <div class="opac-auto-171 opac-auto-097">[% l("You have not created a list yet."); %]</div>
+ [% END %]
+ <div class="common-full-pad"></div>
+ </div>
+ </div>
+[% END %]
--- /dev/null
+[% PROCESS "default/opac/parts/header.tt2";
+ PROCESS "default/opac/parts/misc_util.tt2";
+ WRAPPER "default/opac/parts/myopac/base.tt2";
+ myopac_page = "circs"
+ limit = ctx.circ_history_limit;
+ offset = ctx.circ_history_offset;
+%]
+
+<div style="padding:0px;">
+
+ <div id="acct_checked_tabs" style="padding-bottom: 12px;color:#666;">
+ <div class="align selected">
+ <a href='circs'><img src="[% ctx.media_prefix %]/images/sub_checked_out_off.jpg"/></a>
+ </div>
+ <div class="align">
+ <img src="[% ctx.media_prefix %]/images/sub_checked_hist_on.jpg"/>
+ </div>
+ <div class="clear-both"></div>
+ </div>
+
+ <div class="header_middle">
+ <span class="float-left">[% l('Previously Checked Out Items') %]</span>
+ <span class='float-left' style='padding-left: 10px;'>
+ <a href='circ_history?limit=[% limit %]&offset=[% offset - limit %]'
+ [% IF offset == 0 %] class='invisible' [% END %]><span class="nav_arrow_fix">◄</span>[% l('Previous') %]</a>
+ [%# TODO: get total to prevent paging off then end of the list.. %]
+ <a href='circ_history?limit=[% limit %]&offset=[% offset + limit %]'
+ [% IF ctx.circs.size < limit %] class='invisible' [% END %] >[% l('Next') %]<span class="nav_arrow_fix">►</span></a>
+ </span>
+ <span class="float-right">
+ <a class="hide_me" href="#">[% l('Export List') %]</a>
+ </span>
+ </div>
+ <div class="clear-both"></div>
+
+ [% IF ctx.circs.size < 1 %]
+ <div class="opac-auto-079">
+ <big><strong>[% l('There are no items in your circulation history.') %]</strong></big>
+ </div>
+ [% ELSE %]
+
+ <div id='checked_main'>
+ <table style='border-collapse:collapse;'>
+ <thead id="acct_checked_main_header">
+ <tr>
+ <td width="32%" style="padding-left:5px;">
+ <span title="Click to sort" class='pointer'>[% l('Title') %]</span> /
+ <span title="Click to sort" class='pointer'>[% l('Author') %]</span>
+ </td>
+ <td width="10%" style="padding-left:5px;">
+ <span title="Click to sort" class='pointer'>[% l('Checkout Date') %]</span>
+ </td>
+ <td width="10%" style="padding-left:5px;">
+ <span title="Click to sort" class='pointer'>[% l('Due Date') %]</span>
+ </td>
+ <td width="10%" style="padding-left:5px;">
+ <span title="Click to sort" class='pointer'>[% l('Date Returned') %]</span>
+ </td>
+ <td width="16%">
+ <span title="Click to sort" class='pointer'>[% l('Barcode') %]</span>
+ </td>
+ <td width="22%">
+ <span title="Click to sort" class='pointer'>[% l('Call Number') %]</span>
+ </td>
+ </tr>
+ </thead>
+ <tbody>
+ [% FOR circ IN ctx.circs;
+ attrs = {marc_xml => circ.marc_xml};
+ PROCESS get_marc_attrs args=attrs; %]
+ <tr>
+ <td style="padding-left:5px;padding-bottom:10px;">
+ <a href="[% ctx.opac_root %]/record/[% circ.circ.target_copy.call_number.record.id %]"
+ name="[% l('Catalog record') %]">[% attrs.title %]</a>
+ [% IF attrs.author %] /
+ <a href="[% ctx.opac_root %]/results?qtype=author&query=[% attrs.author | replace('[,\.:;]', '') | url %]">[% attrs.author %]</a>
+ [% END %]
+ </td>
+ <td style="padding-left:5px;">
+ [% date.format(ctx.parse_datetime(circ.circ.xact_start),DATE_FORMAT); %]
+ </td>
+ <td style="padding-left:5px;">
+ [% date.format(ctx.parse_datetime(circ.circ.due_date),DATE_FORMAT); %]
+ </td>
+ <td style="padding-left:5px;">
+ [%
+ IF circ.circ.checkin_time;
+ date.format(ctx.parse_datetime(circ.circ.checkin_time),DATE_FORMAT);
+ ELSE; %]
+ <span style='color:blue;'>*</span><!-- meh -->
+ [% END; %]
+ </td>
+ <td>[% circ.circ.target_copy.barcode %]</td>
+ <td>[% circ.circ.target_copy.call_number.label %]</td>
+ </tr>
+ [% END %]
+ </tbody>
+ </table>
+ </div>
+ [% END %]
+</div>
+[% END %]
--- /dev/null
+[% PROCESS "default/opac/parts/header.tt2";
+ PROCESS "default/opac/parts/misc_util.tt2";
+ WRAPPER "default/opac/parts/myopac/base.tt2";
+ myopac_page = "circs" %]
+<div id='myopac_checked_div' style="padding:0px;">
+
+ <div id="acct_checked_tabs" style="padding-bottom: 12px;color:#666;">
+ <div class="align selected">
+ <img src="[% ctx.media_prefix %]/images/sub_checked_out_on.jpg" />
+ </div>
+ <div class="align">
+ <a href="circ_history"><img
+ src="[% ctx.media_prefix %]/images/sub_checked_hist_off.jpg" /></a>
+ </div>
+ <div class="clear-both"></div>
+ </div>
+
+ <div class="header_middle">
+ <span class="float-left">[% l('Current Items Checked Out') %]</span>
+ <span class="float-right">
+ <a class="hide_me" href="#">[% l('Export List') %]</a>
+ </span>
+ </div>
+ <div class="clear-both"></div>
+ [% IF ctx.circs.size < 1 %]
+ <div class="opac-auto-079">
+ <big><strong>[% l('You have no items checked out.') %]</strong></big>
+ </div>
+ [% ELSE %]
+ [% IF ctx.success_renewals %]
+ <div class="renew-summary">
+ [% l("Successfully renewed [_1] item(s)", ctx.success_renewals) %]
+ </div>
+ [% END %]
+ [% IF ctx.failed_renewals %]
+ <div class="renew-summary red">
+ [% l("Failed to renew [_1] item(s)", ctx.failed_renewals) %]
+ </div>
+ [% END %]
+ <div id='checked_main'>
+ <form method="POST" id="circ-form"
+ onsubmit="return confirm('[% l("Are you sure you wish to renew the selected item(s)?") %]');">
+ <table cellpadding='0' cellspacing='0' class="opac-auto-097b">
+ <tr>
+ <td>
+ <select name="action">
+ <option value="renew">[% l('Renew Selected Titles') %]</option>
+ </select>
+ </td>
+ <td style="padding-left:9px;">
+ <input type="image"
+ alt="[% l('Go') %]" title="[% l('Go') %]"
+ src="[% ctx.media_prefix %]/images/go-btn.png" /></a>
+ </td>
+ <td style="padding-left:5px;">
+ <a href="#"><img alt="Renewing Help"
+ src="[% ctx.media_prefix %]/images/question-mark.png" /></a>
+ </td>
+ </tr>
+ </table>
+ <table id="acct_checked_main_header" cellpadding='0' cellspacing='0'
+ border='0'>
+ <tr>
+ <td width="1%" style="padding-left:10px;">
+ <input type="checkbox" id="check_all_checked"
+ onclick="var inputs=document.getElementsByTagName('input'); for (i = 0; i < inputs.length; i++) { if (inputs[i].name == 'circ' && !inputs[i].disabled) inputs[i].checked = this.checked;}"
+ />
+ </td>
+ <td width="40%" style="padding-left:5px;">
+ <span title="Click to sort" class='pointer'>
+ Title
+ </span> /
+ <span title="Click to sort" class='pointer'>
+ Author
+ </span>
+ </td>
+ <td width="8%" style="padding-right:5px;" align="center">
+ <span title="Click to sort" class='pointer'>Renews<br />Left
+ </span>
+ </td>
+ <td width="13%" style="padding-left:5px;">
+ <span title="Click to sort" class='pointer'>Due Date</span>
+ </td>
+ <td width="16%">
+ <span title="Click to sort" class='pointer'>barcode</span>
+ </td>
+ <td width="22%">
+ <span title="Click to sort" class='pointer'>call number</span>
+ </td>
+ </tr>
+ </table>
+
+ <div id="checked_temp_parent">
+ <div id="acct_checked_temp">
+ <table cellpadding='0' cellspacing='0' border='0'
+ style="margin-top:5px;">
+ [% FOR circ IN ctx.circs;
+ attrs = {marc_xml => circ.marc_xml};
+ PROCESS get_marc_attrs args=attrs; %]
+ <tr>
+ <td width="1%" style="padding-left:10px;" valign="top">
+ <input type="checkbox" name="circ"
+ [% IF circ.circ.renewal_remaining < 1; l('disabled="disabled"'); END %]
+ value="[% circ.circ.id %]" />
+ </td>
+ <td width="40%"
+ style="padding-left:5px;padding-bottom:10px;"
+ name="author">
+ <a href="[% ctx.opac_root %]/record/[% circ.circ.target_copy.call_number.record.id %]" name="[% l('Catalog record') %]">[% attrs.title %]</a>
+ [% IF attrs.author %] /
+ <a href="[% ctx.opac_root %]/results?qtype=author&query=[% attrs.author | replace('[,\.:;]', '') | url %]">[% attrs.author %]</a>
+ [% END %]
+ </td>
+ <td width="8%" name="renewals" align="center">
+ [% circ.circ.renewal_remaining %]
+ </td>
+ <td width="13%" style="padding-left:5px;"
+ name="due_date">
+ [% date.format(ctx.parse_datetime(circ.circ.due_date),DATE_FORMAT) %]
+ </td>
+ <td width="16%" name="barcode">
+ [% circ.circ.target_copy.barcode %]
+ </td>
+ <td width="22%" name="call_number">
+ [% circ.circ.target_copy.call_number.label %]
+ </td>
+ </tr>
+ [% IF circ.renewal_response AND
+ circ.renewal_response.textcode != 'SUCCESS' %]
+ <tr>
+ <td colspan="6">[%# XXX colspan="0" does not work in IE %]
+ <span class="failure-text" title="[% circ.renewal_response.textcode %] / [% circ.renewal_response.payload.fail_part %]">
+ [% circ.renewal_response.desc || circ.renewal_response.payload.fail_part || circ.renewal_response.textcode %]
+ </span>
+ </td>
+ </tr>
+ [% END;
+ END %]
+ </table>
+ </div>
+ </div>
+ </form>
+ </div>
+ [% END %]
+ <div id='non_cat_circs_div' class='hide_me'>
+ <br/>
+ <div style='text-align: center'><b>[% l("Other Circulations") %]</b></div>
+ <table class='data_grid' width='100%'>
+ <thead>
+ <tr>
+ <td>[% l("Circulating Library") %]</td>
+ <td>[% l("Circulation Type") %]</td>
+ <td>[% l("Please return by ...") %]</td>
+ </tr>
+ </thead>
+ <tbody id='non_cat_circs_tbody'>
+ <tr id='non_cat_circs_row'>
+ <td name='circ_lib'/>
+ <td name='item_type'/>
+ <td name='circ_time'/>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+</div>
+[% END %]
--- /dev/null
+[% PROCESS "default/opac/parts/header.tt2";
+ PROCESS "default/opac/parts/misc_util.tt2";
+ WRAPPER "default/opac/parts/myopac/base.tt2";
+ myopac_page = "holds"
+ limit = ctx.hold_history_limit;
+ offset = ctx.hold_history_offset;
+%]
+
+<div id='myopac_holds_div'>
+
+ <div id="acct_holds_tabs" style="padding-bottom: 12px;color:#666;">
+ <div class="align selected">
+ <a href='holds'><img src="[% ctx.media_prefix %]/images/sub_holds_off.jpg"/></a>
+ </div>
+ <div class="align">
+ <img src="[% ctx.media_prefix %]/images/sub_holds_hist_on.jpg"/>
+ </div>
+ <div class="clear-both"></div>
+ </div>
+
+ <div class="header_middle">
+ <span style="float:left;">[% l("Previously Held Items") %]</span>
+ <span class='float-left' style='padding-left: 10px;'>
+ <a href='hold_history?limit=[% limit %]&offset=[% offset - limit %]'
+ [% IF offset == 0 %] class='invisible' [% END %]><span class="nav_arrow_fix">◄</span>[% l('Previous') %]</a>
+ [%# TODO: get total to prevent paging off then end of the list.. %]
+ <a href='hold_history?limit=[% limit %]&offset=[% offset + limit %]'
+ [% IF ctx.holds.size < limit %] class='invisible' [% END %] >[% l('Next') %]<span class="nav_arrow_fix">►</span></a>
+ </span>
+ <span style="float:right;">
+ <a class="hide_me" href="#">Export List</a>
+ </span>
+ </div>
+ <div class="clear-both"></div>
+
+ <div id='holds_main'>
+ [% IF ctx.holds.size < 1 %]
+ <div class="opac-auto-079">
+ <big><strong>[% l('No holds found.') %]</strong></big>
+ </div>
+ [% ELSE %]
+ <table style='border-collapse:collapse; width: 100%;'>
+ <thead id='acct_holds_main_header'>
+ <tr>
+ <td width="138"><span>[% l('Title') %]</span></td>
+ <td width="123"><span>[% l('Author') %]</span></td>
+ <td width="64"> <span>[% l('Format') %]</span></td>
+ <td width="136"><span>[% l('Pickup Location') %]</span> </td>
+ <td width="104">[% l('Active On') %]</td>
+ <td width="95">[% l('Active') %]</td>
+ <td width="106">[% l('Date Fulfilled') %]</td>
+ <td width="172"><span>[% l('Status') %]</span></td>
+ </tr>
+ </thead>
+ <tbody>
+ [% FOR hold IN ctx.holds;
+ attrs = {marc_xml => hold.marc_xml};
+ PROCESS get_marc_attrs args=attrs;
+ ahr = hold.hold.hold %]
+
+ <tr id="acct_holds_temp" name="acct_holds_temp" class="acct_holds_temp">
+
+ <td width="138">
+ <div style="margin-top:10px;margin-bottom:10px;">
+ <a href="[% ctx.opac_root %]/record/[% hold.hold.bre.id %]">[% attrs.title | html %]</a>
+ </div>
+ </td>
+ <td width="123">
+ <div style="margin-top:10px;margin-bottom:10px;">
+ <a href="[% ctx.opac_root %]/results?qtype=author&query=[% attrs.author | replace('[,\.:;]', '') | url %]">[% attrs.author | html %]</a>
+ </div>
+ </td>
+ <td width="64">
+ <div style="width:26px;height:23px;margin-top:6px;margin-bottom:6px;">
+ [% IF attrs.format_icon %]
+ <img src="[% ctx.media_prefix %]/images/[% attrs.form_icon %]"
+ title="[% attrs.format %]" alt="[% attrs.format %]" />
+ [% ELSE;
+ attrs.format;
+ END %]
+ </div>
+ </td>
+ <td width="136">
+ [% ctx.get_aou(ahr.pickup_lib).name %]
+ </td>
+ <td width="104">
+ [% IF ahr.frozen == 't' AND ahr.thaw_date;
+ date.format(ctx.parse_datetime(ahr.thaw_date), DATE_FORMAT);
+ END %]
+ </td>
+ <td width="95">
+ [% l(ahr.frozen == 'f' ? 'Active' : 'Suspended') %]
+ </td>
+ <td width="106">
+ [% IF ahr.fulfillment_time;
+ date.format(ctx.parse_datetime(ahr.fulfillment_time), DATE_FORMAT);
+ END %]
+ </td>
+ <td width="110">
+ <div name="acct_holds_status"
+ style="margin-top:10px;margin-bottom:10px;">
+ [%
+ IF hold.hold.status == 4;
+ l("Available");
+ IF ahr.shelf_expire_time;
+ l('<br/>Expires [_1]',
+ date.format(ctx.parse_datetime(ahr.shelf_expire_time), DATE_FORMAT));
+ END;
+ ELSIF hold.hold.estimated_wait AND hold.hold.estimated_wait > 0;
+ # estimated wait is delivered as seconds.
+ SET hwait = POSIX.ceil(hold.hold.estimated_wait / 86400);
+ l("Estimated wait: [quant,_1,day,days]", hwait);
+ ELSIF hold.hold.status == 3;
+ l("In Transit");
+ ELSIF hold.hold.status < 3;
+ l("Waiting for copy");
+ END;
+ %]
+ </div>
+ </td>
+ </tr>
+ [% END %]
+ </tbody>
+ </table>
+ [% END %]
+ </div>
+[% END %]
--- /dev/null
+[% PROCESS "default/opac/parts/header.tt2";
+ PROCESS "default/opac/parts/misc_util.tt2";
+ WRAPPER "default/opac/parts/myopac/base.tt2";
+ myopac_page = "holds" %]
+<div id='myopac_holds_div'>
+
+ <div id="acct_holds_tabs" style="padding-bottom: 12px;color:#666;">
+ <div class="align selected">
+ <img src="[% ctx.media_prefix %]/images/sub_holds_on.jpg"/>
+ </div>
+ <div class="align">
+ <a href='hold_history'><img src="[% ctx.media_prefix %]/images/sub_holds_hist_off.jpg"/></a>
+ </div>
+ <div class="clear-both"></div>
+ </div>
+
+ <div class="header_middle">
+ <span id="acct_holds_header" style="float:left;">
+ [% IF CGI.param("available");
+ l("Items Ready for Pickup");
+ ELSE;
+ l("Current Items on Hold");
+ END
+ %]
+ </span>
+ <span style="float:right;">
+ <a class="hide_me" href="#">Export List</a>
+ </span>
+ </div>
+ <div class="clear-both"></div>
+ <div id='holds_main'>
+ <form method="POST">
+ <table cellpadding='0' cellspacing='0' class="opac-auto-097">
+ <tr>
+ <td width="1">
+ <select name="action" id="acct_holds_actions">
+ <option id='myopac_holds_actions_none' value=''>
+ -- [% l("Actions for selected holds") %] --
+ </option>
+ <option value='suspend'>
+ [% l("Suspend") %]
+ </option>
+ <option value='activate'>
+ [% l("Activate") %]
+ </option>
+ <!-- XXX maybe later <option value='thaw_date'>
+ [% l("Set Active Date") %]
+ </option> -->
+ <option value='cancel'>
+ [% l("Cancel") %]
+ </option>
+ </select>
+ </td>
+ <td width="1" style="padding-left:9px;">
+ <input type="image"
+ alt="[% l('Go') %]" title="[% l('Go') %]"
+ src="[% ctx.media_prefix %]/images/go-btn.png" />
+ </td>
+ <td width="1" style="padding-left:5px;">
+ <a href="#"><img
+ alt="Holds Help"
+ src="[% ctx.media_prefix %]/images/question-mark.png" /></a>
+ </td>
+ <td align="right">
+ [% l("Show") %]
+ [% IF CGI.param("available") %]
+ <a href="holds">[% l('all') %]</a> |
+ <strong>[% l("only available") %]</strong>
+ [% ELSE %]
+ <strong>[% l("all") %]</strong> |
+ <a href="holds?available=1">[% l("only available") %]</a>
+ [% END %]
+ [% l("holds") %]
+ <select class="hide_me" id="holds_sort">
+ <option value="">-- Sort By --</option>
+ <option value="title">Title</option>
+ <option value="pickup">PickUp Location</option>
+ <option value="status">Status</option>
+ </select>
+ </td>
+ </tr>
+ </table>
+ [% IF ctx.holds.size < 1 %]
+ <div class="opac-auto-079">
+ <big><strong>[% l('No holds found.') %]</strong></big>
+ </div>
+ [% ELSE %]
+ <table id="acct_holds_main_header" cellpadding='0' cellspacing='0'
+ border='0' width="100%">
+ <tr>
+ <td width="36" align="center">
+ <input type="checkbox" id="check_all_holds" />
+ </td>
+ <td width="138">
+ <span title="Click to sort"
+ style="cursor:pointer;">
+ Title
+ </span>
+ </td>
+ <td width="123">
+ <span title="Click to sort" style="cursor:pointer;">Author</span>
+ </td>
+ <td width="64">
+ <span title="Click to sort"
+ style="cursor:pointer;">Format</span>
+ </td>
+ <td width="136">
+ <span title="Click to sort"
+ style="cursor:pointer;">Pickup Location</span>
+ </td>
+ <td width="104">Activate</td>
+ <td width="106">Cancel if not<br />filled by</td>
+ <td width="95">Active</td>
+ <td width="172">
+ <span title="Click to sort"
+ style="cursor:pointer;">Status</span>
+ </td>
+ </tr>
+ </table>
+ <div class="hide_me">
+ <select id="hold_pickup_lib_temp" name="hold_pickup_lib_sel"
+ class="hide_me" style="width:125px;height:21px;"></select>
+ </div>
+
+ <table cellpadding='0' cellspacing='0' border='0' width="100%">
+ <tbody id="holds_temp_parent">
+ [% FOR hold IN ctx.holds;
+ attrs = {marc_xml => hold.marc_xml};
+ PROCESS get_marc_attrs args=attrs;
+ ahr = hold.hold.hold %]
+ <tr id="acct_holds_temp" name="acct_holds_temp"
+ class="acct_holds_temp">
+ <td width="36" align="center" style="text-align:center;">
+ <input type="checkbox" name="hold_id" value="[% ahr.id %]" />
+ </td>
+ <td width="138">
+ <div style="margin-top:10px;margin-bottom:10px;">
+ <a href="[% ctx.opac_root %]/record/[% hold.hold.bre.id %]">[% attrs.title | html %]</a>
+ </div>
+ </td>
+ <td width="123">
+ <div style="margin-top:10px;margin-bottom:10px;">
+ <a href="[% ctx.opac_root %]/results?qtype=author&query=[% attrs.author | replace('[,\.:;]', '') | url %]">[% attrs.author | html %]</a>
+ </div>
+ </td>
+ <td width="64">
+ <div style="width:26px;height:23px;margin-top:6px;margin-bottom:6px;">
+ [% IF attrs.format_icon %]
+ <img src="[% ctx.media_prefix %]/images/[% attrs.form_icon %]"
+ title="[% attrs.format %]" alt="[% attrs.format %]" />
+ [% ELSE;
+ attrs.format;
+ END %]
+ </div>
+ </td>
+ <td width="136">
+ [% ctx.get_aou(ahr.pickup_lib).name %]
+ </td>
+ <td width="104">
+ <!-- <input
+ title="Enter a date (e.g. 10/21/2010)"
+ class="hide_me" style="width:91px;"
+ name="activate_box" type="text" /> -->
+ [% IF ahr.frozen == 't' AND
+ ahr.thaw_date;
+ date.format(ctx.parse_datetime(ahr.thaw_date), DATE_FORMAT);
+ END %]
+ </td>
+ <td width="106">
+ <!-- <input title="[% l('