# Translator memcache server. Default is localhost
# OSRFTranslatorCacheServer 127.0.0.1:11211
+# ----------------------------------------------------------------------------------
+# Log redaction
+# ----------------------------------------------------------------------------------
+<Location />
+ # handler for redacting URL query parameters from
+ # the access log
+ PerlLogHandler OpenILS::WWW::EGWeb::log_handler
+ PerlAddVar OILSUrlParamToRedact "geographic-location"
+</Location>
# ----------------------------------------------------------------------------------
# Added content plugin
<max_spare_children>5</max_spare_children>
</unix_config>
<app_settings>
+ <cache_timeout>300</cache_timeout>
</app_settings>
</open-ils.geo>
<service>open-ils.courses</service>
<service>open-ils.curbside</service>
<service>open-ils.fielder</service>
- <service>open-ils.geo</service>
<service>open-ils.pcrud</service>
<service>open-ils.permacrud</service>
<service>open-ils.reporter</service>
<match_string>open-ils.cstore.direct.actor.user.update</match_string>
<match_string>open-ils.cstore.direct.actor.user.delete</match_string>
<match_string>open-ils.search.z3950.apply_credentials</match_string>
+ <match_string>open-ils.geo</match_string>
+ <match_string>open-ils.actor.geo</match_string>
</log_protect>
</shared>
</config>
use OpenILS::Application;
use base qw/OpenILS::Application/;
+use OpenSRF::Utils::SettingsClient;
use OpenILS::Utils::CStoreEditor qw/:funcs/;
use OpenILS::Utils::Fieldmapper;
+use OpenSRF::Utils::Cache;
use OpenILS::Application::AppUtils;
my $U = "OpenILS::Application::AppUtils";
use Geo::Coder::Google;
use Math::Trig qw(great_circle_distance deg2rad);
+use Digest::SHA qw(sha256_base64);
+
+my $cache;
+my $cache_timeout;
+
+sub initialize {
+ my $conf = OpenSRF::Utils::SettingsClient->new;
+
+ $cache_timeout = $conf->config_value(
+ "apps", "open-ils.geo", "app_settings", "cache_timeout" ) || 300;
+}
+sub child_init {
+ $cache = OpenSRF::Utils::Cache->new('global');
+}
sub calculate_distance {
my ($self, $conn, $pointA, $pointB) = @_;
return new OpenILS::Event("BAD_PARAMS", "desc" => "Malformed coordinates") unless scalar(@{ $pointA }) == 2;
return new OpenILS::Event("BAD_PARAMS", "desc" => "Malformed coordinates") unless scalar(@{ $pointB }) == 2;
- sub NESW { deg2rad($_[0]), deg2rad(90 - $_[1]) }
+ sub NESW { deg2rad($_[1]), deg2rad(90 - $_[0]) } # longitude, latitude
my @A = NESW( $pointA->[0], $pointA->[1] );
my @B = NESW( $pointB->[0], $pointB->[1] );
my $km = great_circle_distance(@A, @B, 6378);
my $service = $e->retrieve_config_geolocation_service($service_id);
return new OpenILS::Event("GEOCODING_NOT_ALLOWED") unless ($U->is_true($service));
+ $address =~ s/^\s+//;
+ $address =~ s/\s+$//;
return new OpenILS::Event("BAD_PARAMS", "desc" => "No address supplied") unless $address;
+
+ # Return cached coordinates if available. We're assuming that any
+ # geolocation service will give roughly equivalent results, so we're
+ # using a hash of the user-supplied address as the cache key, not
+ # address + OU.
+ my $cache_key = 'geo.address.' . sha256_base64($address);
+ my $coords = OpenSRF::Utils::JSON->JSON2perl($cache->get_cache($cache_key));
+ return $coords if $coords;
+
my $geo_coder;
eval {
if ($service->service_code eq 'Free') {
$latitude = $location->{lat};
$longitude = $location->{lon};
}
+ $coords = { latitude => $latitude, longitude => $longitude };
+ $cache->put_cache($cache_key, OpenSRF::Utils::JSON->perl2JSON($coords), $cache_timeout);
- return { latitude => $latitude, longitude => $longitude }
+ return $coords;
}
__PACKAGE__->register_method(
method => "retrieve_coordinates",
use OpenILS::Application::AppUtils;
use Net::HTTP::NB;
use IO::Select;
+use List::MoreUtils qw(uniq);
my $U = 'OpenILS::Application::AppUtils';
our $ac_types = ['toc', 'anotes', 'excerpt', 'summary', 'reviews'];
$self->timelog("load user lists and settings");
}
+ # fetch geographic coordinates if user supplied an
+ # address
+ my $gl = $self->cgi->param('geographic-location');
+ my $coords;
+ if ($gl) {
+ my $geo = OpenSRF::AppSession->create("open-ils.geo");
+ $coords = $geo
+ ->request('open-ils.geo.retrieve_coordinates', $org, scalar $gl)
+ ->gather(1);
+ $geo->kill_me;
+ }
+ $ctx->{has_valid_coords} = 0;
+ if ($coords
+ && ref($coords)
+ && $$coords{latitude}
+ && $$coords{longitude}
+ ) {
+ $ctx->{has_valid_coords} = 1;
+ }
+
# run copy retrieval in parallel to bib retrieval
# XXX unapi
my $cstore = OpenSRF::AppSession->create('open-ils.cstore');
my $copy_rec = $cstore->request(
'open-ils.cstore.json_query.atomic',
- $self->mk_copy_query($rec_id, $org, $copy_depth, $copy_limit, $copy_offset, $pref_ou)
+ $self->mk_copy_query($rec_id, $org, $copy_depth, $copy_limit, $copy_offset, $pref_ou, $coords)
);
if ($self->cgi->param('badges')) {
$ctx->{course_module_opt_in} = 1;
}
+ $ctx->{ou_distances} = {};
+ if ($ctx->{has_valid_coords}) {
+ my $circ_libs = [ uniq map { $_->{circ_lib} } @{$ctx->{copies}} ];
+ my $foreign_copy_circ_libs = [
+ map { $_->target_copy()->circ_lib() }
+ map { @{ $_->foreign_copy_maps() } }
+ @{ $ctx->{foreign_copies} }
+ ];
+ push @{ $circ_libs }, @$foreign_copy_circ_libs; # some overlap is OK here
+ my $ou_distance_list = $U->simplereq(
+ 'open-ils.geo',
+ 'open-ils.geo.sort_orgs_by_distance_from_coordinate.include_distances',
+ [ $coords->{latitude}, $coords->{longitude} ],
+ $circ_libs
+ );
+ $ctx->{ou_distances} = { map { $_->[0] => $_->[1] } @$ou_distance_list };
+ }
+
# Add public copy notes to each copy - and while we're in there, grab peer bib records
# and copy tags. Oh and if we're working with course materials, those too.
my %cached_bibs = ();
my $copy_limit = shift;
my $copy_offset = shift;
my $pref_ou = shift;
+ my $coords = shift;
my $query = $U->basic_opac_copy_query(
$rec_id, undef, undef, $copy_limit, $copy_offset, $self->ctx->{is_staff}
}};
};
- my $ou_sort_param = [$org, $pref_ou ];
- my $gl = $self->cgi->param('geographic-location');
- if ($gl) {
- my $geo = OpenSRF::AppSession->create("open-ils.geo");
- my $coords = $geo
- ->request('open-ils.geo.retrieve_coordinates', $org, scalar $gl)
- ->gather(1);
- if ($coords
- && ref($coords)
- && $$coords{latitude}
- && $$coords{longitude}
- ) {
- push(@$ou_sort_param, $$coords{latitude}, $$coords{longitude});
- }
- }
+ my $ou_sort_param = [$org, $pref_ou ];
+ if ($coords
+ && ref($coords)
+ && $$coords{latitude}
+ && $$coords{longitude}
+ ) {
+ push(@$ou_sort_param, $$coords{latitude}, $$coords{longitude});
+ }
# Unsure if we want these in the shared function, leaving here for now
unshift(@{$query->{order_by}},
return Apache2::Const::OK;
}
+sub log_handler {
+ my $r = shift;
+
+ my @params_to_redact = uniq $r->dir_config->get('OILSUrlParamToRedact');
+ my $re = '('. join('|', map { quotemeta($_) } @params_to_redact) . ')=(?:[^&;]*)';
+
+ my $args = $r->args();
+ $args =~ s/$re/$1=[REDACTED]/g;
+ $r->args($args);
+ my $req = $r->the_request(); # munging args doesn't update the
+ # original requested URI
+ $req =~ s/$re/$1=[REDACTED]/g;
+ $r->the_request($req);
+
+ if ($r->headers_in->{Referer}) {
+ $r->headers_in->{Referer} =~ s/$re/$1=[REDACTED]/g;
+ }
+
+ return Apache2::Const::OK;
+}
+
sub handler {
my $r = shift;
my $stat = handler_guts($r);
.copyTable td:nth-of-type(4):before { content: "Shelving Location"; display: flex;}
.copyTable td:nth-of-type(5):before { content: "Status"; display: flex;}
.copyTable td:nth-of-type(6):before { content: "Due Date"; display: flex;}
+ .copyTable td:nth-of-type(7):before { content: "[% l('Distance') %]"; display: flex;}
.holdingsTable tr:nth-of-type(1):before { content: "Copy #1"; display: block; text-align:center; }
.holdingsTable tr:nth-of-type(2):before { content: "Copy #2"; display: block; text-align:center;}
.copyTable td:nth-of-type(4):before { content: "Shelving Location"; display: flex;}
.copyTable td:nth-of-type(5):before { content: "Status"; display: flex;}
.copyTable td:nth-of-type(6):before { content: "Due Date"; display: flex;}
+ .copyTable td:nth-of-type(7):before { content: "[% l('Distance') %]"; display: flex;}
.holdingsTable tr:nth-of-type(1):before { content: "Copy #1"; display: block; text-align:center; }
.holdingsTable tr:nth-of-type(2):before { content: "Copy #2"; display: block; text-align:center;}
END;
END;
-%]
+[%- MACRO display_ou_distance(ou) BLOCK;
+ km = ctx.ou_distances.$ou;
+ IF km && km != '-1';
+ IF ctx.get_org_setting(ctx.physical_loc || ctx.search_ou, 'opac.geographic_proximity_in_miles');
+ distance = l('[_1] mi', POSIX.sprintf('%.01f', km / 1.609));
+ ELSE;
+ distance = l('[_1] km', POSIX.sprintf('%.01f', km));
+ END;
+ ELSE;
+ distance = '-';
+ END;
+%]
+[% distance %]
+[%- END %]
[%-
IF has_copies or ctx.foreign_copies;
depth = CGI.param('copy_depth').defined ? CGI.param('copy_depth') : CGI.param('depth').defined ? CGI.param('depth') : ctx.copy_summary.last.depth;
<input type="text" id="geographic-location-box" name="geographic-location" aria-label="[% l('Enter address or postal code') %]" placeholder="[% l('Enter address/postal code') %]" class="search-box" x-webkit-speech="" value="[% p = 'geographic-location'; CGI.params.$p %]"></input>
<button type="submit" class="btn btn-confirm">[% l('Go') %]</button>
</span>
+[% p = 'geographic-location'; IF CGI.params.$p && !ctx.has_valid_coords %]
+<span class="opac-alert">[% l('Sorry, your address is not recognized') %]</span>
+[% END %]
+</form>
+[% p = 'geographic-location'; IF CGI.params.$p && ctx.has_valid_coords %]
+<form method="GET">
+[% FOREACH p IN CGI.params.keys; NEXT IF p == 'geographic-location' %]
+ <input type="hidden" name="[% p | html %]" value="[% CGI.params.$p | html %]"/>
+[% END %]
+ <button type="submit" class="opac-button">[% l('Use default item sort') %]</button>
</form>
[% END %]
+[% END %]
<table class="container-fluid table table-hover mt-4 miniTable copyTable w-100" >
<thead>
<tr>
<th scope='col'>[% l("Due Date") %]</th>
[%- IF ctx.use_courses %]
<th scope='col'>[% l("Courses") %]</th>
+ [%- IF ctx.geo_sort && ctx.has_valid_coords %]
+ <th scope='col'>[% l("Distance") %]</th>
[%- END %]
</tr>
</thead>
<td>[% bib.target_copy.location.name | html %]</td>
<td>[% bib.target_copy.status.name | html %]</td>
<td>[% date.format(ctx.parse_datetime(copy_info.due_date, copy_info.circ_circ_lib),DATE_FORMAT) %]</td>
+ [%- IF ctx.geo_sort && ctx.has_valid_coords %]
+ <td>[% display_ou_distance(bib.target_copy.circ_lib) %]</td>
+ [%- END %]
</tr>
[%- END; # FOREACH peer
END; # FOREACH bib
<div>[% course.course_number %]</div>
[% END %]</td>
[% END %]
+ [%- IF ctx.geo_sort && ctx.has_valid_coords %]
+ <td>[% display_ou_distance(copy_info.circ_lib) %]</td>
+ [%- END %]
</tr>
[% IF copy_info.notes; %]
END;
END;
-%]
+[%- MACRO display_ou_distance(ou) BLOCK;
+ km = ctx.ou_distances.$ou;
+ IF km && km != '-1';
+ IF ctx.get_org_setting(ctx.physical_loc || ctx.search_ou, 'opac.geographic_proximity_in_miles');
+ distance = l('[_1] mi', POSIX.sprintf('%.01f', km / 1.609));
+ ELSE;
+ distance = l('[_1] km', POSIX.sprintf('%.01f', km));
+ END;
+ ELSE;
+ distance = '-';
+ END;
+%]
+[% distance %]
+[%- END %]
[%-
IF has_copies or ctx.foreign_copies;
depth = CGI.param('copy_depth').defined ? CGI.param('copy_depth') : CGI.param('depth').defined ? CGI.param('depth') : ctx.copy_summary.last.depth;
<input type="text" id="geographic-location-box" name="geographic-location" aria-label="[% l('Enter address or postal code') %]" placeholder="[% l('Enter address/postal code') %]" class="search-box" x-webkit-speech="" value="[% p = 'geographic-location'; CGI.params.$p %]"></input>
<button type="submit" class="opac-button">[% l('Go') %]</button>
</span>
+[% p = 'geographic-location'; IF CGI.params.$p && !ctx.has_valid_coords %]
+<span class="alert">[% l('Sorry, your address is not recognized') %]</span>
+[% END %]
</form>
+[% p = 'geographic-location'; IF CGI.params.$p && ctx.has_valid_coords %]
+<form method="GET">
+[% FOREACH p IN CGI.params.keys; NEXT IF p == 'geographic-location' %]
+ <input type="hidden" name="[% p | html %]" value="[% CGI.params.$p | html %]"/>
+[% END %]
+ <button type="submit" class="opac-button">[% l('Use default item sort') %]</button>
+</form>
+[% END %]
[% END %]
<table class="table_no_border_space table_no_cell_pad table_no_border" width="100%" id="rdetails_status">
<thead>
[%- IF use_courses %]
<th scope='col'>[% l("Courses") %]</th>
[%- END %]
+ [%- IF ctx.geo_sort && ctx.has_valid_coords %]
+ <th scope='col'>[% l("Distance") %]</th>
+ [%- END %]
</tr>
</thead>
<tbody class="copy_details_table">
OR use_courses %]
<td></td>
[%- END %]
+ [%- IF ctx.geo_sort && ctx.has_valid_coords %]
+ <td>[% display_ou_distance(bib.target_copy.circ_lib) %]</td>
+ [%- END %]
</tr>
[%- END; # FOREACH peer
END; # FOREACH bib
<div>[% course.course_number %]</div>
[% END %]</td>
[% END %]
+ [%- IF ctx.geo_sort && ctx.has_valid_coords %]
+ <td>[% display_ou_distance(copy_info.circ_lib) %]</td>
+ [%- END %]
</tr>
[% IF copy_info.notes; %]