1 # ---------------------------------------------------------------
2 # Copyright © 2014 Jason Stephenson <jason@sigio.com>
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 2 of the License, or
7 # (at your option) any later version.
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 # ---------------------------------------------------------------
14 package NCIP::ILS::Evergreen;
17 use Object::Tiny qw/name/;
18 use XML::LibXML::Simple qw(XMLin);
20 use DateTime::Format::ISO8601;
21 use Digest::MD5 qw/md5_hex/;
23 use OpenSRF::Utils qw/:datetime/;
24 use OpenSRF::Utils::SettingsClient;
25 use OpenILS::Utils::Fieldmapper;
26 use OpenILS::Utils::CStoreEditor qw/:funcs/;
27 use OpenILS::Application::AppUtils;
28 use OpenILS::Const qw/:const/;
33 # Default values we define for things that might be missing in our
34 # runtime environment or configuration file that absolutely must have
37 # OILS_NCIP_CONFIG_DEFAULT is the default location to find our
38 # driver's configuration file. This location can be overridden by
39 # setting the path in the OILS_NCIP_CONFIG environment variable.
41 # BIB_SOURCE_DEFAULT is the config.bib_source.id to use when creating
42 # "short" bibs. It is used only if no entry is supplied in the
43 # configuration file. The provided default is 2, the id of the
44 # "System Local" source that comes with a default Evergreen
47 OILS_NCIP_CONFIG_DEFAULT => '/openils/conf/oils_ncip.xml',
48 BIB_SOURCE_DEFAULT => 2
51 # A common Evergreen code shortcut to use AppUtils:
52 my $U = 'OpenILS::Application::AppUtils';
54 # The usual constructor:
57 $class = ref $class or $class;
59 # Instantiate our Object::Tiny parent with the rest of the
60 # arguments. It creates a blessed hashref.
61 my $self = $class->SUPER::new(@_);
63 # Look for our configuration file, load, and parse it:
66 # Bootstrap OpenSRF and prepare some OpenILS components.
69 # Initialize the rest of our internal state.
75 # Subroutines required by the NCIPServer interface:
82 # Check our session and login if necessary.
83 $self->login() unless ($self->checkauth());
85 # Initialize the hashref we need to return to the caller.
101 # Look up our patron by barcode:
102 my $user = $U->simplereq(
104 'open-ils.actor.user.fleshed.retrieve_by_barcode',
109 # Check for a failure, or a deleted, inactive, or expired user,
110 # and if so, return empty userdata.
111 if (!$user || $U->event_code($user) || $user->deleted() || !$user->active()
112 || _expired($user) || !$user->card()->active()) {
113 # We'll return the empty userdata hashref to indicate a patron
115 return ($userdata, 'Borrower not found');
118 # We also need to check if the barcode used to retrieve the patron
119 # is an active barcode.
120 if (!grep {$_->barcode() eq $barcode && $_->active()} @{$user->cards()}) {
121 return ($userdata, 'Borrower not found');
124 # We got the information, so lets fill in our userdata.
125 $userdata->{borrowernumber} = $user->id();
126 $userdata->{cardnumber} = $user->card()->barcode();
127 $userdata->{firstname} = $user->first_given_name();
128 $userdata->{surname} = $user->family_name();
129 # Use the first address in the array that is valid and not
130 # pending, since no one said whether or not to use billing or
132 my @addrs = grep {$_->valid() && !$_->pending()} @{$user->addresses()};
134 $userdata->{city} = $addrs[0]->city();
135 $userdata->{country} = $addrs[0]->country();
136 $userdata->{zipcode} = $addrs[0]->post_code();
137 $userdata->{state} = $addrs[0]->state();
138 $userdata->{address} = $addrs[0]->street1();
139 $userdata->{address2} = $addrs[0]->street2();
142 # Check for barred patron.
143 if ($user->barred()) {
144 $userdata->{blocked} = "Patron account barred.";
147 # Check if the patron's profile is blocked from ILL.
148 if (!$userdata->{blocked} &&
149 grep {$_->id() == $user->profile()} @{$self->{block_profiles}}) {
150 $userdata->{blocked} = "Patron group blocked from ILL.";
153 # Check for penalties that block CIRC, HOLD, or RENEW.
154 unless ($userdata->{blocked}) {
155 foreach my $penalty (@{$user->standing_penalties()}) {
156 if ($penalty->standing_penalty->block_list()) {
157 my @blocks = split /\|/,
158 $penalty->standing_penalty->block_list();
159 if (grep /(?:CIRC|HOLD|RENEW)/, @blocks) {
160 $userdata->{blocked} = $penalty->standing_penalty->label();
167 return ($userdata, $userdata->{blocked});
182 # Implementation functions that might be useful to a subclass.
184 # Get a CStoreEditor:
188 # If we have an editor, check the validity of the auth session, then
189 # invalidate the editor if the session is not valid.
190 if ($self->{editor}) {
191 undef($self->{editor}) unless ($self->checkauth());
194 # If we don't have an editor, make a new one.
195 unless (defined($self->{editor})) {
196 $self->login() unless ($self->checkauth());
197 $self->{editor} = new_editor(authtoken=>$self->{session}->{authtoken});
200 return $self->{editor};
203 # Login via OpenSRF to Evergreen.
207 # Get the authentication seed.
208 my $seed = $U->simplereq(
210 'open-ils.auth.authenticate.init',
211 $self->{config}->{username}
216 my $response = $U->simplereq(
218 'open-ils.auth.authenticate.complete',
220 username => $self->{config}->{username},
222 $seed . md5_hex($self->{config}->{password})
225 workstation => $self->{config}->{workstation}
229 $self->{session}->{authtoken} = $response->{payload}->{authtoken};
230 $self->{session}->{authtime} = $response->{payload}->{authtime};
235 # Return 1 if we have a 'valid' authtoken, 0 if not.
239 # We implement our own version of this function, rather than rely
240 # on CStoreEditor, because we may want to check this at times that
241 # we don't have a CStoreEditor.
243 # We use AppUtils to do the heavy lifting.
244 if (defined($self->{session})) {
245 if ($U->check_user_session($self->{session}->{authtoken})) {
252 # If we reach here, we don't have a session, so we are definitely
257 # private subroutines not meant to be used directly by subclasses.
258 # Most have to do with setup and/or state checking of implementation
261 # Find, load, and parse our configuration file:
265 # Find the configuration file via variables:
266 my $file = OILS_NCIP_CONFIG_DEFAULT;
267 $file = $ENV{OILS_NCIP_CONFIG} if ($ENV{OILS_NCIP_CONFIG});
269 $self->{config} = XMLin($file, NormaliseSpace => 2,
270 ForceArray => ['block_profile', 'stat_cat_entry']);
273 # Bootstrap OpenSRF::System, load the IDL, and initialize the
274 # CStoreEditor module.
278 my $bootstrap_config = $self->{config}->{bootstrap};
279 OpenSRF::System->bootstrap_client(config_file => $bootstrap_config);
281 my $idl = OpenSRF::Utils::SettingsClient->new->config_value("IDL");
282 Fieldmapper->import(IDL => $idl);
284 OpenILS::Utils::CStoreEditor->init;
287 # Login and then initialize some object data based on the
292 # Login to Evergreen.
296 my $e = $self->editor();
298 # Retrieve the work_ou as an object.
299 my $work_ou = $e->search_actor_org_unit(
300 {shortname => $self->{config}->{credentials}->{work_ou}}
302 $self->{work_ou} = $work_ou->[0] if ($work_ou && @$work_ou);
304 # Load the barred groups as pgt objects into a blocked_profiles
306 $self->{blocked_profiles} = [];
307 foreach (@{$self->{config}->{patrons}->{block_profile}}) {
309 my $pgt = $e->retrieve_permission_grp_tree($_->{grp});
310 push(@{$blocked_profiles}, $pgt) if ($pgt);
312 my $result = $e->search_permission_grp_tree({name => $_});
313 if ($result && @$result) {
314 map {push(@{$self->{blocked_profiles}}, $_)} @$result;
319 # Load the bib source if we're not using precats.
320 unless ($self->{config}->{items}->{use_precats}) {
321 # Retrieve the default
322 my $cbs = $e->retrieve_config_bib_source(BIB_SOURCE_DEFAULT);
323 my $data = $self->{config}->{items}->{bib_source};
325 $data = $data->[0] if (ref($data) eq 'ARRAY');
327 my $result = $e->retrieve_config_bib_source($data->{cbs});
328 $cbs = $result if ($result);
330 my $result = $e->search_config_bib_source({source => $data-});
331 if ($result && @$result) {
332 $cbs = $result->[0]; # Use the first one.
336 $self->{bib_source} = $cbs;
339 # Load the required asset.stat_cat_entries:
340 $self->{stat_cat_entries} = [];
341 foreach (@{$self->{config}->{items}->{stat_cat_entry}}) {
342 # Must have the stat_cat attr and the name, so we must have a
345 # We want to limit the search to the work org and its
347 my $ancestors = $U->get_org_ancestors($self->{work_ou}->id());
348 my $result = $e->search_asset_stat_cat_entry(
350 stat_cat => $_->{stat_cat},
351 value => $_->{content},
355 if ($result && @$result) {
356 map {push(@{$self->{stat_cat_entries}}, $_)} @$result;
361 # Standalone, "helper" functions. These do not take an object or
364 # Strip leading and trailing whitespace (incl. newlines) from a string
375 # Check if a user is past their expiration date.
380 # Users might not expire. If so, they have no expire_date.
381 if ($user->expire_date()) {
382 my $expires = DateTime::Format::ISO8601->parse_datetime(
383 cleanse_ISO8601($user->expire_date())
385 my $now = DateTime->now()->epoch();
386 $expired = $now > $expires;