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/;
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 our patron up by barcode:
102 my $user = $U->simplereq(
104 'open-ils.actor.user.fleshed.retrieve_by_barcode',
109 # Check for a failure deleted, inactive, or expired user, and if
110 # 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 $userdate->{address} = $addrs[0]->street1();
139 $userdata->{address2} = $addrs[0]->street2();
142 # Check for barred patron.
143 if ($user->barred()) {
144 $userdata->{blocked} = 1;
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} = 1;
153 # Check for penalties that block CIRC or HOLD.
154 unless ($userdata->{blocked}) {
155 foreach my $penalty (@{$user->standing_penalties()}) {
156 if ($penalty->stand_penalty->block_list()) {
157 my @blocks = split /\|/,
158 $penalty->standing_penalty->block_list();
159 if (grep /(?:CIRC|HOLD)/, @blocks) {
160 $userdata->{blocked} = 1;
167 return ($userdata, '');
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 # Load our configuration with XML::XPath.
270 my $xpath = XML::XPath->new(filename => $file);
271 # Load configuration into $self:
272 $self->{config}->{bootstrap} =
273 _strip($xpath->findvalue("/ncip/bootstrap")->value());
274 $self->{config}->{username} =
275 _strip($xpath->findvalue("/ncip/credentials/username")->value());
276 $self->{config}->{password} =
277 _strip($xpath->findvalue("/ncip/credentials/password")->value());
278 $self->{config}->{work_ou} =
279 _strip($xpath->findvalue("/ncip/credentials/work_ou")->value());
280 $self->{config}->{workstation} =
281 _strip($xpath->findvalue("/ncip/credentials/workstation")->value());
282 # Look for a list of patron profiles to treat as blocked. This is
283 # useful if you have a patron group or groups that are not
284 # permitted to do ILL.
285 $self->{config}->{barred_groups} = [];
286 my $nodes = $xpath->findnodes('/ncip/patrons/block_profile');
288 foreach my $node ($nodes->get_nodelist()) {
289 my $data = {id => 0, name => ""};
290 my $attr = $xpath->findvalue('@pgt', $node);
294 $data->{name} = _strip($node->string_value());
295 push(@{$self->{config}->{barred_groups}}, $data)
296 if ($data->{id} || $data->{name});
299 # Check for the use_precats setting for acceptitem. This should
300 # only be set if you are using 2.7.0-alpha or later of Evergreen.
301 $self->{config}->{use_precats} = 0;
303 $nodes = $xpath->find('/ncip/items/use_precats');
304 $self->{config}->{use_precats} = 1 if ($nodes);
305 # If we're not using precats, we will be making "short" bibs. We
306 # need to look up and see if a special bib source has been
307 # configured for these.
309 $nodes = $xpath->findnodes('/ncip/items/bib_source');
311 my $node = $nodes->get_node(1);
312 my $attr = $xpath->findvalue('@cbs', $node);
314 $self->{config}->{cbs}->{id} = $attr;
316 $self->{config}->{cbs}->{name} = _strip($node->string_value());
318 # Look for any required asset.copy.stat_cat_entry entries.
319 $self->{config}->{asces} = [];
321 $nodes = $xpath->findnodes('/ncip/items/stat_cat_entry');
323 foreach my $node ($nodes->get_nodelist()) {
324 my $data = {asc => 0, id => 0, name => ''};
325 my $asc = $xpath->findvalue('@asc', $node);
326 $data->{asc} = $asc if ($asc);
327 my $asce = $xpath->findvalue('@asce', $node);
328 $data->{id} = $asce if ($asce);
329 $data->{name} = _strip($node->string_value());
330 push(@{$self->{config}->{asces}}, $data)
331 if ($data->{id} || ($data->{name} && $data->{asc}));
336 # Bootstrap OpenSRF::System, load the IDL, and initialize the
337 # CStoreEditor module.
341 my $bootstrap_config = $self->{config}->{bootstrap};
342 OpenSRF::System->bootstrap_client(config_file => $bootstrap_config);
344 my $idl = OpenSRF::Utils::SettingsClient->new->config_value("IDL");
345 Fieldmapper->import(IDL => $idl);
347 OpenILS::Utils::CStoreEditor->init;
350 # Login and then initialize some object data based on the
355 # Login to Evergreen.
359 my $e = $self->editor();
361 # Load the barred groups as pgt objects into a blocked_profiles
363 $self->{blocked_profiles} = [];
364 foreach (@{$self->{config}->{barred_groups}}) {
366 my $pgt = $e->retrieve_permission_grp_tree($_->{id});
367 push(@{$self->{blocked_profiles}}, $pgt) if ($pgt);
369 my $result = $e->search_permission_grp_tree(
372 if ($result && @$result) {
373 map {push(@{$self->{blocked_profiles}}, $_)} @$result;
378 # Load the bib source if we're not using precats.
379 unless ($self->{config}->{use_precats}) {
380 # Retrieve the default
381 my $cbs = $e->retrieve_config_bib_source(BIB_SOURCE_DEFAULT);
382 my $data = $self->{config}->{cbs};
385 my $result = $e->retrieve_config_bib_source($data->{id});
386 $cbs = $result if ($result);
388 my $result = $e->search_config_bib_source(
389 {source => $data->{name}}
391 if ($result && @$result) {
392 $cbs = $result->[0]; # Use the first one.
396 $self->{bib_source} = $cbs;
399 # Load the required asset.stat_cat_entries:
401 foreach (@{$self->{config}->{asces}}) {
403 my $asce = $e->retrieve_asset_stat_cat_entry($_->{id});
404 push(@{$self->{asces}}, $asce) if ($asce);
405 } elsif ($_->{asc} && $_->{name}) {
406 # We want to limit the search to the work org and its
408 my $ancestors = $U->get_org_ancestors($self->{config}->{work_ou});
409 my $result = $e->search_asset_stat_cat_entry(
411 stat_cat => $_->{asc},
416 if ($result && @$result) {
417 map {push(@{$self->{asces}}, $_)} @$result;
423 # Standalone, "helper" functions. These do not take an object or
426 # Strip leading and trailing whitespace (incl. newlines) from a string
437 # Check if a user is past their expiration date.
442 # Users might not expire. If so, they have no expire_date.
443 if ($user->expire_date()) {
444 my $expires = DateTime::Format::ISO8601->parse_datetime(
445 cleanse_ISO8601($user->expire_date())
447 my $now = DateTime->now()->epoch();
448 $expired = $now > $expires;