From c929f9e79d043ebc79e0a47d0473c55946c86aeb Mon Sep 17 00:00:00 2001 From: Magnus Enger <digitalutvikling@gmail.com> Date: Wed, 17 Sep 2014 10:16:45 +0200 Subject: [PATCH] Bug 11401 - Add support for Norwegian national library card This patch makes it possible to sync patron data between Koha and the Norwegian national patron database, in both directions. In order to use this, the following information is necessary: - a username/password from the Norwegian national database of libraries ("Base Bibliotek"), available to all Norwegian libraries - a special key in order to decrypt and encrypt PIN-codes/passwords, which is only available to Norwegian library system vendors - a norwegian library vendor username/password See http://www.lanekortet.no/ for more information (in Norwegian). While this is of course an implementation of a specific synchronization scheme for borrower data, attempts have been made to prepare the ground for other sync schemes that might be implemented later. Especially the structure of the new borrower_sync table might be reviewed with an eye to how it might fit other schemes. To test: Since the password and cryptographic key needed to use this functionality is only available to Norwegian library system vendors, only regression testing can be done on the submitted code. Suggested things to check: - Apply the patch and make sure the database update is done. This should add the new "borrower_sync" table and five new systmpreferences under the "Patrons" > "Norwegian patron database" category: - NorwegianPatronDBEnable - NorwegianPatronDBEndpoint - NorwegianPatronDBUsername - NorwegianPatronDBPassword - NorwegianPatronDBSearchNLAfterLocalHit - Check that patrons can be created, edited and deleted as usual, when NorwegianPatronDBEnable is set to "Disable" - Check that the new tests in t/NorwegianPatronDB.pm run ok, e.g. on a gitified setup: $ sudo koha-shell -c "PERL5LIB=/path/to/kohaclone prove -v t/NorwegianPatronDB.t" instancename - Check that all the other tests still run ok - Check that the POD in the new files itroduced by this patch looks ok: - Koha/NorwegianPatronDB.pm - members/nl-search.pl - misc/cronjobs/nl-sync-from-koha.pl - misc/cronjobs/nl-sync-to-koha.pl - t/NorwegianPatronDB.t Note: koha-qa.pl complains about tabs in deletemem.tt, but the lines it complains about have not been touched by this patch. Update 2014-09-18: Rebase on master, split out changes to Koha::Schema, incorporate new way of authenticating with NL Signed-off-by: Chris Cormack <chrisc@catalyst.net.nz> Have tested for regressions, none found, will sign off. The only suggestion I have is that we could perhaps use Module::Load::Conditional in C4/Members.pm, so that we don't have to load the module unless we need it. --- C4/Installer/PerlDependencies.pm | 25 + C4/Members.pm | 41 +- Koha/NorwegianPatronDB.pm | 657 +++++++++++++++++++++ installer/data/mysql/kohastructure.sql | 18 + installer/data/mysql/sysprefs.sql | 5 + installer/data/mysql/updatedatabase.pl | 25 + .../prog/en/includes/members-toolbar.inc | 48 +- .../prog/en/includes/nl-search-form.tt | 9 + .../prog/en/modules/admin/preferences/patrons.pref | 22 + .../prog/en/modules/members/deletemem.tt | 34 +- .../prog/en/modules/members/member.tt | 6 + .../prog/en/modules/members/memberentrygen.tt | 13 + .../prog/en/modules/members/moremember.tt | 10 + .../prog/en/modules/members/nl-search.tt | 176 ++++++ members/deletemem.pl | 25 +- members/memberentry.pl | 16 +- members/moremember.pl | 11 + members/nl-search.pl | 161 +++++ misc/cronjobs/nl-sync-from-koha.pl | 184 ++++++ misc/cronjobs/nl-sync-to-koha.pl | 192 ++++++ t/NorwegianPatronDB.t | 583 ++++++++++++++++++ 21 files changed, 2242 insertions(+), 19 deletions(-) create mode 100644 Koha/NorwegianPatronDB.pm create mode 100644 koha-tmpl/intranet-tmpl/prog/en/includes/nl-search-form.tt create mode 100644 koha-tmpl/intranet-tmpl/prog/en/modules/members/nl-search.tt create mode 100755 members/nl-search.pl create mode 100644 misc/cronjobs/nl-sync-from-koha.pl create mode 100644 misc/cronjobs/nl-sync-to-koha.pl create mode 100644 t/NorwegianPatronDB.t diff --git a/C4/Installer/PerlDependencies.pm b/C4/Installer/PerlDependencies.pm index ad1f5a6..58862db 100644 --- a/C4/Installer/PerlDependencies.pm +++ b/C4/Installer/PerlDependencies.pm @@ -742,6 +742,31 @@ our $PERL_DEPS = { 'required' => '0', 'min_ver' => '5.836', }, + 'SOAP::Lite' => { + 'usage' => 'Norwegian national library card', + 'required' => '0', + 'min_ver' => '0.712', + }, + 'Crypt::GCrypt' => { + 'usage' => 'Norwegian national library card', + 'required' => '0', + 'min_ver' => '1.24', + }, + 'Convert::BaseN' => { + 'usage' => 'Norwegian national library card', + 'required' => '0', + 'min_ver' => '0.01', + }, + 'Test::DBIx::Class' => { + 'usage' => 'Testing suite', + 'required' => '0', + 'min_ver' => '0.41', + }, + 'Digest::SHA' => { + 'usage' => 'Norwegian national library card', + 'required' => '0', + 'min_ver' => '5.84_01', + }, }; 1; diff --git a/C4/Members.pm b/C4/Members.pm index e3a5538..1f427d6 100644 --- a/C4/Members.pm +++ b/C4/Members.pm @@ -33,15 +33,17 @@ use C4::Accounts; use C4::Biblio; use C4::Letters; use C4::SQLHelper qw(InsertInTable UpdateInTable SearchInTable); -use C4::Members::Attributes qw(SearchIdMatchingAttribute); +use C4::Members::Attributes qw(SearchIdMatchingAttribute UpdateBorrowerAttribute); use C4::NewsChannels; #get slip news use DateTime; use DateTime::Format::DateParse; +use Koha::Database; use Koha::DateUtils; use Koha::Borrower::Debarments qw(IsDebarred); use Text::Unaccent qw( unac_string ); use Koha::AuthUtils qw(hash_password); use Koha::Database; +use Koha::NorwegianPatronDB qw( NLUpdateHashedPIN NLEncryptPIN NLSync ); our ($VERSION,@ISA,@EXPORT,@EXPORT_OK,$debug); @@ -783,6 +785,10 @@ sub ModMember { if ($data{password} eq '****' or $data{password} eq '') { delete $data{password}; } else { + if ( C4::Context->preference('NorwegianPatronDBEnable') == 1 ) { + # Update the hashed PIN in borrower_sync.hashed_pin, before Koha hashes it + NLUpdateHashedPIN( $data{'borrowernumber'}, $data{password} ); + } $data{password} = hash_password($data{password}); } } @@ -803,6 +809,25 @@ sub ModMember { AddEnrolmentFeeIfNeeded( $data{categorycode}, $data{borrowernumber} ); } + # If NorwegianPatronDBEnable is enabled, we set syncstatus to something that a + # cronjob will use for syncing with NL + if ( C4::Context->preference('NorwegianPatronDBEnable') == 1 ) { + my $borrowersync = Koha::Database->new->schema->resultset('BorrowerSync')->find({ + 'synctype' => 'norwegianpatrondb', + 'borrowernumber' => $data{'borrowernumber'} + }); + # Do not set to "edited" if syncstatus is "new". We need to sync as new before + # we can sync as changed. And the "new sync" will pick up all changes since + # the patron was created anyway. + if ( $borrowersync->syncstatus ne 'new' && $borrowersync->syncstatus ne 'delete' ) { + $borrowersync->update( { 'syncstatus' => 'edited' } ); + } + # Set the value of 'sync' + $borrowersync->update( { 'sync' => $data{'sync'} } ); + # Try to do the live sync + NLSync({ 'borrowernumber' => $data{'borrowernumber'} }); + } + logaction("MEMBERS", "MODIFY", $data{'borrowernumber'}, "UPDATE (executed w/ arg: $data{'borrowernumber'})") if C4::Context->preference("BorrowersLog"); } return $execute_success; @@ -845,11 +870,25 @@ sub AddMember { : $patron_category->default_privacy() eq 'never' ? 2 : $patron_category->default_privacy() eq 'forever' ? 0 : undef; + # Make a copy of the plain text password for later use + my $plain_text_password = $data{'password'}; # create a disabled account if no password provided $data{'password'} = ($data{'password'})? hash_password($data{'password'}) : '!'; $data{'borrowernumber'}=InsertInTable("borrowers",\%data); + # If NorwegianPatronDBEnable is enabled, we set syncstatus to something that a + # cronjob will use for syncing with NL + if ( exists $data{'borrowernumber'} && C4::Context->preference('NorwegianPatronDBEnable') == 1 ) { + Koha::Database->new->schema->resultset('BorrowerSync')->create({ + 'borrowernumber' => $data{'borrowernumber'}, + 'synctype' => 'norwegianpatrondb', + 'sync' => 1, + 'syncstatus' => 'new', + 'hashed_pin' => NLEncryptPIN( $plain_text_password ), + }); + } + # mysql_insertid is probably bad. not necessarily accurate and mysql-specific at best. logaction("MEMBERS", "CREATE", $data{'borrowernumber'}, "") if C4::Context->preference("BorrowersLog"); diff --git a/Koha/NorwegianPatronDB.pm b/Koha/NorwegianPatronDB.pm new file mode 100644 index 0000000..9d94034 --- /dev/null +++ b/Koha/NorwegianPatronDB.pm @@ -0,0 +1,657 @@ +package Koha::NorwegianPatronDB; + +# Copyright 2014 Oslo Public Library +# +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation; either version 3 of the License, or (at your option) any later +# version. +# +# Koha is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with Koha; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +=head1 NAME + +Koha::NorwegianPatronDB + +=head1 SYNOPSIS + + use Koha::NorwegianPatronDB; + +=head1 FUNCTIONS + +=cut + +use Modern::Perl; +use C4::Context; +use C4::Members::Attributes qw( UpdateBorrowerAttribute ); +use SOAP::Lite; +use Crypt::GCrypt; +use Digest::SHA qw( sha256_hex ); +use Convert::BaseN; +use DateTime; + +use base 'Exporter'; +use version; our $VERSION = qv('1.0.0'); + +our %EXPORT_TAGS = ( all => [qw( + NLCheckSysprefs + NLSearch + NLSync + NLGetChanged + NLMarkForDeletion + NLDecodePin + NLEncryptPIN + NLUpdateHashedPIN + NLGetFirstname + NLGetSurname + NLGetSyncDataFromBorrowernumber +)] ); +Exporter::export_ok_tags('all'); + +my $nl_uri = 'http://lanekortet.no'; +my $nl_proxy = C4::Context->preference("NorwegianPatronDBEndpoint"); + +=head2 SOAP::Transport::HTTP::Client::get_basic_credentials + +This is included to set the username and password used by SOAP::Lite. + +=cut + +sub SOAP::Transport::HTTP::Client::get_basic_credentials { + # Library username and password from Base Bibliotek (stored as system preferences) + my $library_username = C4::Context->preference("NorwegianPatronDBUsername"); + my $library_password = C4::Context->preference("NorwegianPatronDBPassword"); + # Vendor username and password (stored in koha-conf.xml) + my $vendor_username = C4::Context->config( 'nlvendoruser' ); + my $vendor_password = C4::Context->config( 'nlvendorpass' ); + # Combine usernames and passwords, and encrypt with SHA256 + my $combined_username = "$vendor_username-$library_username"; + my $combined_password = sha256_hex( "$library_password-$vendor_password" ); + warn "$combined_username => $combined_password"; + return $combined_username => $combined_password; +} + +=head2 NLCheckSysprefs + +Check that sysprefs relevant to NL are set. + +=cut + +sub NLCheckSysprefs { + + my $response = { + 'error' => 0, + 'nlenabled' => 0, + 'endpoint' => 0, + 'userpass' => 0, + }; + + # Check that the Norwegian national paron database is enabled + if ( C4::Context->preference("NorwegianPatronDBEnable") == 1 ) { + $response->{ 'nlenabled' } = 1; + } else { + $response->{ 'error' } = 1; + } + + # Check that an endpoint is specified + if ( C4::Context->preference("NorwegianPatronDBEndpoint") ne '' ) { + $response->{ 'endpoint' } = 1; + } else { + $response->{ 'error' } = 1; + } + + # Check that the username and password for the patron database is set + if ( C4::Context->preference("NorwegianPatronDBUsername") ne '' && C4::Context->preference("NorwegianPatronDBPassword") ne '' ) { + $response->{ 'userpass' } = 1; + } else { + $response->{ 'error' } = 1; + } + + return $response; + +} + +=head2 NLSearch + +Search the NL patron database. + +SOAP call: "hent" (fetch) + +=cut + +sub NLSearch { + + my ( $identifier ) = @_; + + my $client = SOAP::Lite + ->on_action( sub { return '""';}) + ->uri( $nl_uri ) + ->proxy( $nl_proxy ); + + my $id = SOAP::Data->type('string'); + $id->name('identifikator'); + $id->value( $identifier ); + my $som = $client->hent( $id ); + + return $som; + +} + +=head2 NLSync + +Sync a patron that has been changed or created in Koha "upstream" to NL. + +Input is a hashref with one of two possible elements, either a patron retrieved +from the database: + + my $result = NLSync({ 'patron' => $borrower_from_dbic }); + +or a plain old borrowernumber: + + my $result = NLSync({ 'borrowernumber' => $borrowernumber }); + +In the latter case, this function will retrieve the patron record from the +database using DBIC. + +Which part of the API is called depends on the value of the "syncstatus" column: + +=over 4 + +=item * B<new> = The I<nyPost> ("new record") method is called. + +=item * B<edited> = The I<endre> ("change/update") method is called. + +=item * B<delete> = The I<slett> ("delete") method is called. + +=back + +Required values for B<new> and B<edited>: + +=over 4 + +=item * sist_endret (last updated) + +=item * adresse, postnr eller sted (address, zip or city) + +=item * fdato (birthdate) + +=item * fnr_hash (social security number, but not hashed...) + +=item * kjonn (gender, M/F) + +=back + +=cut + +sub NLSync { + + my ( $input ) = @_; + + my $patron; + if ( defined $input->{'borrowernumber'} ) { + $patron = Koha::Database->new->schema->resultset('Borrower')->find( $input->{'borrowernumber'} ); + } elsif ( defined $input->{'patron'} ) { + $patron = $input->{'patron'}; + } + + # There should only be one sync, so we use the first one + my @syncs = $patron->borrower_syncs; + my $sync; + foreach my $this_sync ( @syncs ) { + if ( $this_sync->synctype eq 'norwegianpatrondb' ) { + $sync = $this_sync; + } + } + + my $client = SOAP::Lite + ->on_action( sub { return '""';}) + ->uri( $nl_uri ) + ->proxy( $nl_proxy ); + + my $cardnumber = SOAP::Data->name( 'lnr' => $patron->cardnumber ); + + # Call the appropriate method based on syncstatus + my $response; + if ( $sync->syncstatus eq 'edited' || $sync->syncstatus eq 'new' ) { + my $soap_patron = _koha_patron_to_soap( $patron ); + if ( $sync->syncstatus eq 'edited' ) { + $response = $client->endre( $cardnumber, $soap_patron ); + } elsif ( $sync->syncstatus eq 'new' ) { + $response = $client->nyPost( $soap_patron ); + } + } + if ( $sync->syncstatus eq 'delete' ) { + $response = $client->slett( $cardnumber ); + } + + # Update the sync data according to the results + if ( $response->{'status'} && $response->{'status'} == 1 ) { + if ( $sync->syncstatus eq 'delete' ) { + # Turn off any further syncing + $sync->update( { 'sync' => 0 } ); + } + # Update the syncstatus to 'synced' + $sync->update( { 'syncstatus' => 'synced' } ); + # Update the 'synclast' attribute with the "server time" ("server_tid") returned by the method + $sync->update( { 'lastsync' => $response->{'server_tid'} } ); + } + return $response; + +} + +=head2 NLGetChanged + +Fetches patrons from NL that have been changed since a given timestamp. This includes +patrons that have been changed by the library that runs the sync, so we have to +check which library was the last one to change a patron, before we update patrons +locally. + +This is supposed to be executed once per night. + +SOAP call: soekEndret + +=cut + +sub NLGetChanged { + + my ( $from_arg ) = @_; + + my $client = SOAP::Lite + ->on_action( sub { return '""';}) + ->uri( $nl_uri ) + ->proxy( $nl_proxy ); + + my $from_string; + if ( $from_arg && $from_arg ne '' ) { + $from_string = $from_arg; + } else { + # Calculate 1 second past midnight of the day before + my $dt = DateTime->now( time_zone => 'Europe/Oslo' ); + $dt->subtract( days => 1 ); + my $from = DateTime->new( + year => $dt->year(), + month => $dt->month(), + day => $dt->day(), + hour => 00, + minute => 00, + second => 01, + time_zone => 'Europe/Oslo', + ); + $from_string = $from->ymd . "T" . $from->hms; + } + + my $timestamp = SOAP::Data->name( 'tidspunkt' => $from_string ); + my $max_results = SOAP::Data->name( 'max_antall' => 0 ); # 0 = no limit + my $start_index = SOAP::Data->name( 'start_indeks' => 0 ); # 1 is the first record + + # Call the appropriate method based on syncstatus + my $som = $client->soekEndret( $timestamp, $max_results, $start_index ); + + # Extract and massage patron data + my $result = $som->result; + foreach my $patron ( @{ $result->{'respons_poster'} } ) { + # Only handle patrons that have lnr (barcode) and fnr_hash (social security number) + # Patrons that lack these two have been deleted from NL + if ( $patron->{'lnr'} && $patron->{'fnr_hash'} ) { + push @{ $result->{'kohapatrons'} }, _soap_to_kohapatron( $patron ); + } + } + return $result; + +} + +=head2 NLMarkForDeletion + +Mark a borrower for deletion, but do not do the actual deletion. Deleting the +borrower from NL will be done later by the nl-sync-from-koha.pl script. + +=cut + +sub NLMarkForDeletion { + + my ( $borrowernumber ) = @_; + + my $borrowersync = Koha::Database->new->schema->resultset('BorrowerSync')->find({ + 'synctype' => 'norwegianpatrondb', + 'borrowernumber' => $borrowernumber, + }); + return $borrowersync->update( { 'syncstatus' => 'delete' } ); + +} + +=head2 NLDecodePin + +Takes a string encoded with AES/ECB/PKCS5PADDING and a 128-bits key, and returns +the decoded string as plain text. + +The key needs to be stored in koha-conf.xml, like so: + +<yazgfs> + ... + <config> + ... + <nlkey>xyz</nlkey> + </config> +</yazgfs> + +=cut + +sub NLDecodePin { + + my ( $hash ) = @_; + my $key = C4::Context->config( 'nlkey' ); + + # Convert the hash from Base16 + my $cb = Convert::BaseN->new( base => 16 ); + my $decoded_hash = $cb->decode( $hash ); + + # Do the decryption + my $cipher = Crypt::GCrypt->new( + type => 'cipher', + algorithm => 'aes', + mode => 'ecb', + padding => 'standard', # "This is also known as PKCS#5" + ); + $cipher->start( 'decrypting' ); + $cipher->setkey( $key ); # Must be called after start() + my $plaintext = $cipher->decrypt( $decoded_hash ); + $plaintext .= $cipher->finish; + + return $plaintext; + +} + +=head2 NLEncryptPIN + +Takes a plain text PIN as argument, returns the encrypted PIN, according to the +NL specs. + + my $encrypted_pin = NLEncryptPIN( $plain_text_pin ); + +=cut + +sub NLEncryptPIN { + + my ( $pin ) = @_; + return _encrypt_pin( $pin ); + +} + +=head2 NLUpdateHashedPIN + +Takes two arguments: + +=over 4 + +=item * Borrowernumber + +=item * Clear text PIN code + +=back + +Hashes the password and saves it in borrower_sync.hashed_pin. + +=cut + +sub NLUpdateHashedPIN { + + my ( $borrowernumber, $pin ) = @_; + my $borrowersync = Koha::Database->new->schema->resultset('BorrowerSync')->find({ + 'synctype' => 'norwegianpatrondb', + 'borrowernumber' => $borrowernumber, + }); + return $borrowersync->update({ 'hashed_pin', _encrypt_pin( $pin ) }); + +} + +=head2 _encrypt_pin + +Takes a plain text PIN and returns the encrypted version, according to the NL specs. + +=cut + +sub _encrypt_pin { + + my ( $pin ) = @_; + my $key = C4::Context->config( 'nlkey' ); + + # Do the encryption + my $cipher = Crypt::GCrypt->new( + type => 'cipher', + algorithm => 'aes', + mode => 'ecb', + padding => 'standard', # "This is also known as PKCS#5" + ); + $cipher->start( 'encrypting' ); + $cipher->setkey( $key ); # Must be called after start() + my $ciphertext = $cipher->encrypt( $pin ); + $ciphertext .= $cipher->finish; + + # Encode as Bas16 + my $cb = Convert::BaseN->new( base => 16 ); + my $encoded_ciphertext = $cb->encode( $ciphertext ); + + return $encoded_ciphertext; + +} + +=head2 NLGetSyncDataFromBorrowernumber + +Takes a borrowernumber as argument, returns a Koha::Schema::Result::BorrowerSync +object. + + my $syncdata = NLGetSyncDataFromBorrowernumber( $borrowernumber ); + +=cut + +sub NLGetSyncDataFromBorrowernumber { + + my ( $borrowernumber ) = @_; + my $data = Koha::Database->new->schema->resultset('BorrowerSync')->find({ + 'synctype' => 'norwegianpatrondb', + 'borrowernumber' => $borrowernumber, + }); + return $data; + +} + +=head2 NLGetFirstname + +Takes a string like "Surname, Firstname" and returns the "Firstname" part. + +If there is no comma, the string is returned unaltered. + + my $firstname = NLGetFirstname( $name ); + +=cut + +sub NLGetFirstname { + + my ( $s ) = @_; + my ( $surname, $firstname ) = _split_name( $s ); + if ( $surname eq $s ) { + return $s; + } else { + return $firstname; + } + +} + +=head2 NLGetSurname + +Takes a string like "Surname, Firstname" and returns the "Surname" part. + +If there is no comma, the string is returned unaltered. + + my $surname = NLGetSurname( $name ); + +=cut + +sub NLGetSurname { + + my ( $s ) = @_; + my ( $surname, $firstname ) = _split_name( $s ); + return $surname; + +} + +=head2 _split_name + +Takes a string like "Surname, Firstname" and returns a list of surname and firstname. + +If there is no comma, the string is returned unaltered. + + my ( $surname, $firstname ) = _split_name( $name ); + +=cut + +sub _split_name { + + my ( $s ) = @_; + + # Return the string if there is no comma + unless ( $s =~ m/,/ ) { + return $s; + } + + my ( $surname, $firstname ) = split /, /, $s; + + return ( $surname, $firstname ); + +} + +=head2 _format_soap_error + +Takes a soap result object as input and returns a formatted string containing SOAP error data. + +=cut + +sub _format_soap_error { + + my ( $result ) = @_; + if ( $result ) { + return join ', ', $result->faultcode, $result->faultstring, $result->faultdetail; + } else { + return 'No result'; + } + +} + +=head2 _soap_to_koha_patron + +Convert a SOAP object of type "Laaner" into a hash that can be sent to AddMember or ModMember. + +=cut + +sub _soap_to_kohapatron { + + my ( $soap ) = @_; + + return { + 'cardnumber' => $soap->{ 'lnr' }, + 'surname' => NLGetSurname( $soap->{ 'navn' } ), + 'firstname' => NLGetFirstname( $soap->{ 'navn' } ), + 'sex' => $soap->{ 'kjonn' }, + 'dateofbirth' => $soap->{ 'fdato' }, + 'address' => $soap->{ 'p_adresse1' }, + 'address2' => $soap->{ 'p_adresse2' }, + 'zipcode' => $soap->{ 'p_postnr' }, + 'city' => $soap->{ 'p_sted' }, + 'country' => $soap->{ 'p_land' }, + 'b_address' => $soap->{ 'm_adresse1' }, + 'b_address2' => $soap->{ 'm_adresse2' }, + 'b_zipcode' => $soap->{ 'm_postnr' }, + 'b_city' => $soap->{ 'm_sted' }, + 'b_country' => $soap->{ 'm_land' }, + 'password' => $soap->{ 'pin' }, + 'dateexpiry' => $soap->{ 'gyldig_til' }, + 'email' => $soap->{ 'epost' }, + 'mobile' => $soap->{ 'tlf_mobil' }, + 'phone' => $soap->{ 'tlf_hjemme' }, + 'phonepro' => $soap->{ 'tlf_jobb' }, + '_extra' => { # Data that should not go in the borrowers table + 'socsec' => $soap->{ 'fnr_hash' }, + 'created' => $soap->{ 'opprettet' }, + 'created_by' => $soap->{ 'opprettet_av' }, + 'last_change' => $soap->{ 'sist_endret' }, + 'last_change_by' => $soap->{ 'sist_endret_av' }, + }, + }; + +} + +=head2 _koha_patron_to_soap + +Convert a patron (in the form of a Koha::Schema::Result::Borrower) into a SOAP +object that can be sent to NL. + +=cut + +sub _koha_patron_to_soap { + + my ( $patron ) = @_; + + # Extract attributes + my $patron_attributes = {}; + foreach my $attribute ( $patron->borrower_attributes ) { + $patron_attributes->{ $attribute->code->code } = $attribute->attribute; + } + + # There should only be one sync, so we use the first one + my @syncs = $patron->borrower_syncs; + my $sync = $syncs[0]; + + # Create SOAP::Data object + my $soap_patron = SOAP::Data->name( + 'post' => \SOAP::Data->value( + SOAP::Data->name( 'lnr' => $patron->cardnumber ), + SOAP::Data->name( 'fnr_hash' => $patron_attributes->{ 'fnr' } )->type( 'string' )->type( 'string' ), + SOAP::Data->name( 'navn' => $patron->surname . ', ' . $patron->firstname )->type( 'string' ), + SOAP::Data->name( 'sist_endret' => $sync->lastsync )->type( 'string' ), + SOAP::Data->name( 'kjonn' => $patron->sex )->type( 'string' ), + SOAP::Data->name( 'fdato' => $patron->dateofbirth )->type( 'string' ), + SOAP::Data->name( 'p_adresse1' => $patron->address )->type( 'string' ), + SOAP::Data->name( 'p_adresse2' => $patron->address2 )->type( 'string' ), + SOAP::Data->name( 'p_postnr' => $patron->zipcode )->type( 'string' ), + SOAP::Data->name( 'p_sted' => $patron->city )->type( 'string' ), + SOAP::Data->name( 'p_land' => $patron->country )->type( 'string' ), + SOAP::Data->name( 'm_adresse1' => $patron->b_address )->type( 'string' ), + SOAP::Data->name( 'm_adresse2' => $patron->b_address2 )->type( 'string' ), + SOAP::Data->name( 'm_postnr' => $patron->b_zipcode )->type( 'string' ), + SOAP::Data->name( 'm_sted' => $patron->b_city )->type( 'string' ), + SOAP::Data->name( 'm_land' => $patron->b_country )->type( 'string' ), + # Do not send the PIN code as it has been hashed by Koha, but use the version hashed according to NL + SOAP::Data->name( 'pin' => $sync->hashed_pin )->type( 'string' ), + SOAP::Data->name( 'gyldig_til' => $patron->dateexpiry )->type( 'string' ), + SOAP::Data->name( 'epost' => $patron->email )->type( 'string' ), + SOAP::Data->name( 'tlf_mobil' => $patron->mobile )->type( 'string' ), + SOAP::Data->name( 'tlf_hjemme' => $patron->phone )->type( 'string' ), + SOAP::Data->name( 'tlf_jobb' => $patron->phonepro )->type( 'string' ), + ), + )->type("Laaner"); + + return $soap_patron; + +} + +=head1 EXPORT + +None by default. + +=head1 AUTHOR + +Magnus Enger <digitalutvikling@gmail.com> + +=cut + +1; + +__END__ diff --git a/installer/data/mysql/kohastructure.sql b/installer/data/mysql/kohastructure.sql index aeeba17..2d5e1d6 100644 --- a/installer/data/mysql/kohastructure.sql +++ b/installer/data/mysql/kohastructure.sql @@ -357,6 +357,24 @@ CREATE TABLE `branch_item_rules` ( -- information entered in the circulation and ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- +-- Table structure for table borrower_sync +-- + +CREATE TABLE borrower_sync ( + borrowersyncid int(11) NOT NULL AUTO_INCREMENT, -- Primary key, unique identifier + borrowernumber int(11) NOT NULL, -- Connects data about synchronisations to a borrower + synctype varchar(32) NOT NULL, -- There could potentially be more than one kind of syncing going on, a text string here can be used to tell them apart. E.g.: The Norwegian national patron database uses 'norwegianpatrondb' in this column + sync tinyint(1) NOT NULL DEFAULT '0', -- A boolean (1/0) for turning syncing off and on for individual borrowers + syncstatus varchar(10) DEFAULT NULL, -- The sync status for any given borrower. Could be text strings like 'new', 'edited', 'synced' etc. The values used here will depend on the actual syncing being done. + lastsync varchar(50) DEFAULT NULL, -- Date of the last successfull sync. The date format might be different depending on the service that is being used, so no special date format is being enforced here. + hashed_pin varchar(64) DEFAULT NULL, -- Perhaps specific to The Norwegian national patron database, this column holds a hashed PIN code + PRIMARY KEY (borrowersyncid), + KEY borrowernumber (borrowernumber), + CONSTRAINT borrower_sync_ibfk_1 FOREIGN KEY (borrowernumber) REFERENCES borrowers (borrowernumber) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8 + + +-- -- Table structure for table `branchcategories` -- diff --git a/installer/data/mysql/sysprefs.sql b/installer/data/mysql/sysprefs.sql index b06b163..88622ea 100644 --- a/installer/data/mysql/sysprefs.sql +++ b/installer/data/mysql/sysprefs.sql @@ -197,6 +197,11 @@ INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` ('noissuescharge','5','','Define maximum amount withstanding before check outs are blocked','Integer'), ('noItemTypeImages','0',NULL,'If ON, disables item-type images','YesNo'), ('NoLoginInstructions', '', '60|10', 'Instructions to display on the OPAC login form when a patron is not logged in', 'Textarea'), +('NorwegianPatronDBEnable','0',NULL,'Enable communication with the Norwegian national patron database.', 'YesNo'), +('NorwegianPatronDBEndpoint','',NULL,'Which NL endpoint to use.', 'Free'), +('NorwegianPatronDBUsername','',NULL,'Username for communication with the Norwegian national patron database.','Free'), +('NorwegianPatronDBPassword','',NULL,'Password for communication with the Norwegian national patron database.','Free'), +('NorwegianPatronDBSearchNLAfterLocalHit','0',NULL,'Search NL if a search has already given one or more local hits?.','YesNo'), ('NotesBlacklist','',NULL,'List of notes fields that should not appear in the title notes/description separator of details','free'), ('NotHighlightedWords','and|or|not',NULL,'List of words to NOT highlight when OpacHitHighlight is enabled','free'), ('NoticeCSS','',NULL,'Notices CSS url.','free'), diff --git a/installer/data/mysql/updatedatabase.pl b/installer/data/mysql/updatedatabase.pl index aae80f1..753d98c 100755 --- a/installer/data/mysql/updatedatabase.pl +++ b/installer/data/mysql/updatedatabase.pl @@ -8776,6 +8776,31 @@ if ( CheckVersion($DBversion) ) { SetVersion($DBversion); } +$DBversion = "3.17.00.XXX"; +if ( C4::Context->preference("Version") < TransformToNum($DBversion) ) { + $dbh->do("INSERT IGNORE INTO systempreferences (variable,value,options,explanation,type) VALUES ('NorwegianPatronDBEnable', '0', NULL, 'Enable communication with the Norwegian national patron database.', 'YesNo')"); + $dbh->do("INSERT IGNORE INTO systempreferences (variable,value,options,explanation,type) VALUES ('NorwegianPatronDBEndpoint', '', NULL, 'Which NL endpoint to use.', 'Free')"); + $dbh->do("INSERT IGNORE INTO systempreferences (variable,value,options,explanation,type) VALUES ('NorwegianPatronDBUsername', '', NULL, 'Username for communication with the Norwegian national patron database.', 'Free')"); + $dbh->do("INSERT IGNORE INTO systempreferences (variable,value,options,explanation,type) VALUES ('NorwegianPatronDBPassword', '', NULL, 'Password for communication with the Norwegian national patron database.', 'Free')"); + $dbh->do("INSERT IGNORE INTO systempreferences (variable,value,options,explanation,type) VALUES ('NorwegianPatronDBSearchNLAfterLocalHit','0',NULL,'Search NL if a search has already given one or more local hits?.','YesNo')"); + $dbh->do(" +CREATE TABLE borrower_sync ( + borrowersyncid int(11) NOT NULL AUTO_INCREMENT, + borrowernumber int(11) NOT NULL, + synctype varchar(32) NOT NULL, + sync tinyint(1) NOT NULL DEFAULT '0', + syncstatus varchar(10) DEFAULT NULL, + lastsync varchar(50) DEFAULT NULL, + hashed_pin varchar(64) DEFAULT NULL, + PRIMARY KEY (borrowersyncid), + KEY borrowernumber (borrowernumber), + CONSTRAINT borrower_sync_ibfk_1 FOREIGN KEY (borrowernumber) REFERENCES borrowers (borrowernumber) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8" +); + print "Upgrade to $DBversion done (Bug 11401 - Add support for Norwegian national library card)\n"; + SetVersion($DBversion); +} + =head1 FUNCTIONS =head2 TableExists($table) diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/members-toolbar.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/members-toolbar.inc index caa2fbf..870f254 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/includes/members-toolbar.inc +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/members-toolbar.inc @@ -1,12 +1,32 @@ +[% USE Koha %] +[% SET NorwegianPatronDBEnable = Koha.Preference( 'NorwegianPatronDBEnable' ) %] <script type="text/javascript"> //<![CDATA[ $(document).ready(function(){ [% IF ( CAN_user_borrowers ) %] - $("#deletepatron").click(function(){ + [% IF ( NorwegianPatronDBEnable == 1 ) %] + $("#deletepatronlocal").click(function(){ + confirm_local_deletion(); + $(".btn-group").removeClass("open"); + return false; + }); + $("#deletepatronremote").click(function(){ + confirm_remote_deletion(); + $(".btn-group").removeClass("open"); + return false; + }); + $("#deletepatronboth").click(function(){ + confirm_both_deletion(); + $(".btn-group").removeClass("open"); + return false; + }); + [% ELSE %] + $("#deletepatron").click(function(){ confirm_deletion(); $(".btn-group").removeClass("open"); return false; }); + [% END %] $("#renewpatron").click(function(){ confirm_reregistration(); $(".btn-group").removeClass("open"); @@ -49,6 +69,24 @@ function confirm_deletion() { window.location='/cgi-bin/koha/members/deletemem.pl?member=[% borrowernumber %]'; } } +function confirm_local_deletion() { + var is_confirmed = window.confirm(_("Are you sure you want to delete this patron from the local database? This cannot be undone.")); + if (is_confirmed) { + window.location='/cgi-bin/koha/members/deletemem.pl?member=[% borrowernumber %]&deletelocal=true&deleteremote=false'; + } +} +function confirm_remote_deletion() { + var is_confirmed = window.confirm(_("Are you sure you want to delete this patron from the Norwegian national patron database? This cannot be undone.")); + if (is_confirmed) { + window.location='/cgi-bin/koha/members/deletemem.pl?member=[% borrowernumber %]&deletelocal=false&deleteremote=true'; + } +} +function confirm_both_deletion() { + var is_confirmed = window.confirm(_("Are you sure you want to delete this patron both from the local database and from the Norwegian national patron database? This cannot be undone.")); + if (is_confirmed) { + window.location='/cgi-bin/koha/members/deletemem.pl?member=[% borrowernumber %]&deletelocal=true&deleteremote=true'; + } +} [% IF ( is_child ) %]function confirm_updatechild() { var is_confirmed = window.confirm(_("Are you sure you want to update this child to an Adult category? This cannot be undone.")); @@ -141,7 +179,13 @@ function searchToHold(){ <li class="disabled"><a data-toggle="tooltip" data-placement="left" title="You are not authorized to set permissions" id="patronflags" href="#">Set permissions</a></li> [% END %] [% IF ( CAN_user_borrowers ) %] - <li><a id="deletepatron" href="#">Delete</a></li> + [% IF ( NorwegianPatronDBEnable == 1 ) %] + <li><a id="deletepatronlocal" href="#">Delete local</a></li> + <li><a id="deletepatronremote" href="#">Delete remote</a></li> + <li><a id="deletepatronboth" href="#">Delete local and remote</a></li> + [% ELSE %] + <li><a id="deletepatron" href="#">Delete</a></li> + [% END %] [% ELSE %] <li class="disabled"><a data-toggle="tooltip" data-placement="left" title="You are not authorized to delete patrons" id="deletepatron" href="#">Delete</a></li> [% END %] diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/nl-search-form.tt b/koha-tmpl/intranet-tmpl/prog/en/includes/nl-search-form.tt new file mode 100644 index 0000000..a8eeef5 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/nl-search-form.tt @@ -0,0 +1,9 @@ +<form method="POST" action="nl-search.pl" autocomplete="off"> <!-- This form will be used for things like social security numbers, so it makes sense not to remember them. --> +<input type="hidden" name="op" value="search" /> + <fieldset> + <legend>[% nl_search_form_title %]</legend> + <label for="q">Social security or card number: </label> + <input type="text" name="q" value="[% q %]"> + <input type="submit" value="Search"> + </fieldset> +</form> diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/patrons.pref b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/patrons.pref index 2469431..b245d1a 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/patrons.pref +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/patrons.pref @@ -1,4 +1,5 @@ Patrons: + General: - - List - pref: AddPatronLists @@ -138,3 +139,24 @@ Patrons: - pref: CardnumberLength - "characters long. The length can be a single number to specify an exact length, a range separated by a comma (i.e., 'Min,Max'), or a maximum with no minimum (i.e., ',Max')." - "If 'cardnumber' is included in the BorrowerMandatoryField list, the minimum length, if not specified here, defaults to one." + Norwegian patron database: + - + - pref: NorwegianPatronDBEnable + choices: + yes: Enable + no: Disable + - the ability to communicate with the Norwegian national patron database via the + - pref: NorwegianPatronDBEndpoint + - endpoint. + - + - Communicate with the Norwegian national patron database using the username + - pref: NorwegianPatronDBUsername + - and the password + - pref: NorwegianPatronDBPassword + - . You can get these from "Base Bibliotek", which is maintained by the Norwegian National Library. + - + - pref: NorwegianPatronDBSearchNLAfterLocalHit + choices: + yes: Do + no: "Don't" + - search the Norwegian national patron database after a local search result was found. diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/members/deletemem.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/members/deletemem.tt index 082b066..b100361 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/members/deletemem.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/members/deletemem.tt @@ -13,18 +13,28 @@ <div id="yui-main"> <div class="yui-b"> [% INCLUDE 'members-toolbar.inc' %] - <div class="dialog alert"> - <h3>Cannot delete patron</h3> - <ul>[% IF ( ItemsOnIssues ) %] - <li>Patron has [% ItemsOnIssues %] item(s) checked out.</li> - [% END %] - [% IF ( charges ) %] - <li>Patron has [% charges %] in fines.</li> - [% END %] - [% IF ( guarantees ) %] - <li>Patron's record has guaranteed accounts attached.</li> - [% END %]</ul> + [% IF ( ItemsOnIssues || charges || guarantees ) %] + <div class="dialog alert"> + <h3>Cannot delete patron</h3> + <ul> + [% IF ( ItemsOnIssues ) %] + <li>Patron has [% ItemsOnIssues %] item(s) checked out.</li> + [% END %] + [% IF ( charges ) %] + <li>Patron has [% charges %] in fines.</li> + [% END %] + [% IF ( guarantees ) %] + <li>Patron's record has guaranteed accounts attached.</li> + [% END %] + </ul> </div> + [% END %] + [% IF ( keeplocal ) %] + <div class="dialog message"> + <h3>Remote record deleted, local record kept</h3> + <p>Patron was marked for deletion from Norwegian national patron database, but the local record was kept.</p> + </div> + [% END %] </div> </div> @@ -32,4 +42,4 @@ [% INCLUDE 'circ-menu.inc' %] </div> </div> -[% INCLUDE 'intranet-bottom.inc' %] \ No newline at end of file +[% INCLUDE 'intranet-bottom.inc' %] diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/members/member.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/members/member.tt index f167e32..34d0b7d 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/members/member.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/members/member.tt @@ -1,3 +1,4 @@ +[% USE Koha %] [% INCLUDE 'doc-head-open.inc' %] <title>Koha › Patrons [% IF ( searching ) %]› Search results[% END %]</title> [% INCLUDE 'doc-head-close.inc' %] @@ -293,6 +294,11 @@ function filterByFirstLetterSurname(letter) { </div> [% END %] + [% IF Koha.Preference( 'NorwegianPatronDBEnable' ) == 1 %] + [% SET nl_search_form_title='Search the Norwegian national patron database' %] + [% INCLUDE 'nl-search-form.tt' %] + [% END %] + [% INCLUDE 'patron-toolbar.inc' %] [% IF ( no_add ) %] <div class="dialog alert"> diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/members/memberentrygen.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/members/memberentrygen.tt index 56cc570..96777e1 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/members/memberentrygen.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/members/memberentrygen.tt @@ -1,4 +1,5 @@ [% IF ( opduplicate ) %][% SET focusAction = "clearDupe" %][% END %] +[% USE Koha %] [% USE KohaDates %] [% INCLUDE 'doc-head-open.inc' %] <title>Koha › Patrons › @@ -973,6 +974,18 @@ [% IF ( mandatorysort2 ) %]<span class="required">Required</span>[% END %] </li> [% END %] + [% IF ( Koha.Preference( 'NorwegianPatronDBEnable' ) == 1 ) %] + <li> + <label for="sort2">Sync with the Norwegian national patron database:</label> + [% IF ( sync == 0 ) %] + <input type="radio" id="sync" name="sync" value="1"> Yes + <input type="radio" id="sync" name="sync" value="0" checked> No + [% ELSE %] + <input type="radio" id="sync" name="sync" value="1" checked> Yes + <input type="radio" id="sync" name="sync" value="0"> No + [% END %] + </li> + [% END %] </ol> </fieldset> [% UNLESS nodateenrolled && noopacnote && noborrowernotes %] diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/members/moremember.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/members/moremember.tt index 64f1fe3..454651c 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/members/moremember.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/members/moremember.tt @@ -1,3 +1,4 @@ +[% USE Koha %] [% USE KohaDates %] [% INCLUDE 'doc-head-open.inc' %] <title>Koha › Patrons › @@ -350,6 +351,15 @@ function validate1(date) { </li> [% IF ( borrowernotes ) %]<li><span class="label">Circulation note: </span>[% borrowernotes %]</li>[% END %] [% IF ( opacnote ) %]<li><span class="label">OPAC note:</span>[% opacnote %]</li>[% END %] + [% IF Koha.Preference( 'NorwegianPatronDBEnable' ) == 1 %] + [% IF ( sync == 1 ) %] + <li><span class="label">Activate sync: </span>Yes</li> + [% IF ( syncstatus ) %]<li><span class="label">Sync status: </span>[% syncstatus %]</li>[% END %] + [% IF ( lastsync ) %]<li><span class="label">Last sync: </span>[% lastsync | $KohaDates %]</li>[% END %] + [% ELSE %] + <li><span class="label">Activate sync: </span>No</li> + [% END %] + [% END %] </ol> </div> </div> diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/members/nl-search.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/members/nl-search.tt new file mode 100644 index 0000000..56cb76e --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/members/nl-search.tt @@ -0,0 +1,176 @@ +[% USE KohaDates %] +[% INCLUDE 'doc-head-open.inc' %] +<title>Search the Norwegian national patron database</title> +[% INCLUDE 'doc-head-close.inc' %] +</head> +<body> +[% INCLUDE 'header.inc' %] +[% INCLUDE 'patron-search.inc' %] + +<div id="breadcrumbs"><a href="/cgi-bin/koha/mainpage.pl">Home</a> › <a href="/cgi-bin/koha/members/members-home.pl">Patrons</a> › Search the Norwegian national patron database</div> + +<div id="doc3" class="yui-t2"> + <div id="bd"> + <div id="yui-main"> + <div class="yui-b"> + + <h1>Search the Norwegian national patron database</h1> + + [% IF (error) %] + <div class="dialog alert"> + [% IF ( error.nlenabled == 0 ) %]<p>You need to activate this function with the NorwegianPatronDBEnable system preference in order to use it.</p>[% END %] + [% IF ( error.endpoint == 0 ) %]<p>You need to specify an endpoint with the NorwegianPatronDBEndpoint system preference.</p>[% END %] + [% IF ( error.userpass == 0 ) %]<p>You need to fill in the NorwegianPatronDBUsername and NorwegianPatronDBPassword system preferences in order to use this function.</p>[% END %] + [% IF ( error == 'COULD_NOT_ADD_PATRON' ) %]<p>Could not add a new patron.</p>[% END %] + </div> + [% ELSE %] + [% SET nl_search_form_title='Search' %] + [% INCLUDE 'nl-search-form.tt' %] + [% END %] + + [% IF ( local_result ) %] + <h3>Existing patrons</h3> + <ul> + [% FOREACH patron IN local_result %] + [%# Find the category_type %] + [% FOREACH category IN categories %] + [% IF category.categorycode == patron.categorycode %] + [% patron.category_type = category.category_type %] + [% END %] + [% END %] + <li>[% patron.firstname %] [% patron.surname %] [% patron.cardnumber %] | + <a href="/cgi-bin/koha/members/moremember.pl?borrowernumber=[% patron.borrowernumber %]">Details</a> | + <a href="/cgi-bin/koha/members/memberentry.pl?op=modify&destination=circ&borrowernumber=[% patron.borrowernumber %]&category_type=[% patron.category_type %]">Edit</a> | + <a href="/cgi-bin/koha/circ/circulation.pl?borrowernumber=[% patron.borrowernumber %]">Check out</a> + </li> + [% END %] + </ul> + [% END %] + + [% IF ( result ) %] + + [% IF result.antall_poster_returnert == 0 %] + + <div class="dialog alert"> + <p>No results found in the Norwegian national patron database. Message: "[% result.melding %]"</p> + </div> + + [% ELSE %] + + <h3>Results from the Norwegian national patron database</h3> + <div class="yui-g"> + <div class="yui-u first"> + [% PROCESS patron_detail p=result.respons_poster.0 %] + </div> + [% IF ( result.respons_poster.1 ) %] + <div class="yui-u"> + [% PROCESS patron_detail p=result.respons_poster.1 %] + </div> + [% END %] + </div> + + [% END %] + + [% END %] + + </div> + </div> + + <div class="yui-b"> + [% INCLUDE 'circ-menu.inc' %] + </div> + </div> +[% INCLUDE 'intranet-bottom.inc' %] + +[% BLOCK patron_detail %] +<div class="rows"> +<h4>[% p.navn | html_entity %]</h4> +<ol> +[% IF ( p.kjonn ) %]<li><span class="label">kjonn: </span>[% p.kjonn | html_entity %]</li>[% END %] +[% IF ( p.fdato ) %]<li><span class="label">fdato: </span>[% p.fdato | html_entity %]</li>[% END %] +[% IF ( p.lnr ) %]<li><span class="label">lnr: </span>[% p.lnr | html_entity %]</li>[% END %] +[% IF ( p.fnr_hash ) %]<li><span class="label">fnr_hash: </span>[% p.fnr_hash | html_entity %]</li>[% END %] + +[% IF ( p.epost ) %]<li><span class="label">epost: </span>[% p.epost | html_entity %]</li>[% END %] +[% IF ( p.epost_sjekk ) %]<li><span class="label">epost_sjekk: </span>[% p.epost_sjekk | html_entity %]</li>[% END %] + +[% IF ( p.tlf_mobil ) %]<li><span class="label">tlf_mobil: </span>[% p.tlf_mobil | html_entity %]</li>[% END %] +[% IF ( p.tlf_hjemme ) %]<li><span class="label">tlf_hjemme: </span>[% p.tlf_hjemme | html_entity %]</li>[% END %] +[% IF ( p.tlf_jobb ) %]<li><span class="label">tlf_jobb: </span>[% p.tlf_jobb | html_entity %]</li>[% END %] + +[% IF ( p.p_adresse1 ) %]<li><span class="label">p_adresse1: </span>[% p.p_adresse1 | html_entity %]</li>[% END %] +[% IF ( p.p_adresse2 ) %]<li><span class="label">p_adresse2: </span>[% p.p_adresse2 | html_entity %]</li>[% END %] +[% IF ( p.p_postnr ) %]<li><span class="label">p_postnr: </span>[% p.p_postnr | html_entity %]</li>[% END %] +[% IF ( p.p_sted ) %]<li><span class="label">p_sted: </span>[% p.p_sted | html_entity %]</li>[% END %] +[% IF ( p.p_land ) %]<li><span class="label">p_land: </span>[% p.p_land | html_entity %]</li>[% END %] +[% IF ( p.p_sjekk ) %]<li><span class="label">p_sjekk: </span>[% p.p_sjekk | html_entity %]</li>[% END %] + +[% IF ( p.m_adresse1 ) %]<li><span class="label">m_adresse1: </span>[% p.m_adresse1 | html_entity %]</li>[% END %] +[% IF ( p.m_adresse2 ) %]<li><span class="label">m_adresse2: </span>[% p.m_adresse2 | html_entity %]</li>[% END %] +[% IF ( p.m_postnr ) %]<li><span class="label">m_postnr: </span>[% p.m_postnr | html_entity %]</li>[% END %] +[% IF ( p.m_sted ) %]<li><span class="label">m_sted: </span>[% p.m_sted | html_entity %]</li>[% END %] +[% IF ( p.m_land ) %]<li><span class="label">m_land: </span>[% p.m_land | html_entity %]</li>[% END %] +[% IF ( p.m_sjek ) %]<li><span class="label">m_sjekk: </span>[% p.m_sjekk | html_entity %]</li>[% END %] +[% IF ( p.m_gyldig_til ) %]<li><span class="label">m_gyldig_til: </span>[% p.m_gyldig_til | html_entity %]</li>[% END %] + +[% IF ( p.pin ) %]<li><span class="label">pin: </span>[% p.pin | html_entity %]</li>[% END %] +[% IF ( p.passord ) %]<li><span class="label">passsord: </span>[% p.passord | html_entity %]</li>[% END %] +[% IF ( p.feide ) %]<li><span class="label">feide: </span>[% p.feide | html_entity %]</li>[% END %] +[% IF ( p.folkeregsjekk_dato ) %]<li><span class="label">folkeregsjekk_dato: </span>[% p.folkeregsjekk_dato | html_entity %]</li>[% END %] + +[% IF ( p.hjemmebibliotek ) %]<li><span class="label">hjemmebibliotek: </span>[% p.hjemmebibliotek | html_entity %]</li>[% END %] +[% IF ( p.opprettet ) %]<li><span class="label">opprettet: </span>[% p.opprettet | html_entity %]</li>[% END %] +[% IF ( p.opprettet_av ) %]<li><span class="label">opprettet_av: </span>[% p.opprettet_av | html_entity %]</li>[% END %] +[% IF ( p.sist_endret ) %]<li><span class="label">sist_endret: </span>[% p.sist_endret | html_entity %]</li>[% END %] +[% IF ( p.sist_endret_av ) %]<li><span class="label">sist_endret_av: </span>[% p.sist_endret_av | html_entity %]</li>[% END %] +[% IF ( p.gyldig_til ) %]<li><span class="label">gyldig_til: </span>[% p.gyldig_til | html_entity %]</li>[% END %] + +[% IF ( p.prim_kontakt ) %]<li><span class="label">prim_kontakt: </span>[% p.prim_kontakt | html_entity %]</li>[% END %] +</ol> + +<form action="nl-search.pl" method="POST"> +<input type="hidden" name="op" value="save" /> +<input type="hidden" name="navn" value="[% p.navn | html_entity %]" /> +<input type="hidden" name="kjonn" value="[% p.kjonn | html_entity %]" /> +<input type="hidden" name="fdato" value="[% p.fdato | html_entity %]" /> +<input type="hidden" name="lnr" value="[% p.lnr | html_entity %]" /> +<input type="hidden" name="fnr_hash" value="[% p.fnr_hash | html_entity %]" /> +<input type="hidden" name="p_adresse1" value="[% p.p_adresse1 | html_entity %]" /> +<input type="hidden" name="p_adresse2" value="[% p.p_adresse2 | html_entity %]" /> +<input type="hidden" name="p_postnr" value="[% p.p_postnr | html_entity %]" /> +<input type="hidden" name="p_sted" value="[% p.p_sted | html_entity %]" /> +<input type="hidden" name="p_land" value="[% p.p_land | html_entity %]" /> +<input type="hidden" name="p_sjekk" value="[% p.p_sjekk | html_entity %]" /> +<input type="hidden" name="m_adresse1" value="[% p.m_adresse1 | html_entity %]" /> +<input type="hidden" name="m_adresse2" value="[% p.m_adresse2 | html_entity %]" /> +<input type="hidden" name="m_postnr" value="[% p.m_postnr | html_entity %]" /> +<input type="hidden" name="m_sted" value="[% p.m_sted | html_entity %]" /> +<input type="hidden" name="m_land" value="[% p.m_land | html_entity %]" /> +<input type="hidden" name="m_sjekk" value="[% p.m_sjekk | html_entity %]" /> +<input type="hidden" name="m_gyldig_til" value="[% p.m_gyldig_til | html_entity %]" /> +<input type="hidden" name="pin" value="[% p.pin %]" /> +<input type="hidden" name="passord" value="[% p.passord | html_entity %]" /> +<input type="hidden" name="feide" value="[% p.feide | html_entity %]" /> +<input type="hidden" name="folkeregsjekk_dato" value="[% p.folkeregsjekk_dato | html_entity %]" /> +<input type="hidden" name="hjemmebibliotek" value="[% p.hjemmebibliotek | html_entity %]" /> +<input type="hidden" name="opprettet" value="[% p.opprettet | html_entity %]" /> +<input type="hidden" name="opprettet_av" value="[% p.opprettet_av | html_entity %]" /> +<input type="hidden" name="sist_endret" value="[% p.sist_endret | html_entity %]" /> +<input type="hidden" name="sist_endret_av" value="[% p.sist_endret_av | html_entity %]" /> +<input type="hidden" name="gyldig_til" value="[% p.gyldig_til | html_entity %]" /> +<input type="hidden" name="epost" value="[% p.epost | html_entity %]" /> +<input type="hidden" name="epost_sjekk" value="[% p.epost_sjekk | html_entity %]" /> +<input type="hidden" name="tlf_mobil" value="[% p.tlf_mobil | html_entity %]" /> +<input type="hidden" name="tlf_hjemme" value="[% p.tlf_hjemme | html_entity %]" /> +<input type="hidden" name="tlf_jobb" value="[% p.tlf_jobb | html_entity %]" /> +<input type="hidden" name="prim_kontakt" value="[% p.prim_kontakt | html_entity %]" /> +<input type="submit" value="Import this patron" /> +as +<select name="categorycode"> +[% FOREACH c IN categories %] + <option value="[% c.categorycode %]">[% c.description %]</option> +[% END %] +</select> +</form> +</div> +[% END %] diff --git a/members/deletemem.pl b/members/deletemem.pl index 32801e0..dec9005 100755 --- a/members/deletemem.pl +++ b/members/deletemem.pl @@ -31,6 +31,7 @@ use C4::Auth; use C4::Members; use C4::Branch; # GetBranches use C4::VirtualShelves (); #no import +use Koha::NorwegianPatronDB qw( NLMarkForDeletion NLSync ); my $input = new CGI; @@ -44,7 +45,22 @@ my ($template, $borrowernumber, $cookie) }); #print $input->header; -my $member=$input->param('member'); +my $member = $input->param('member'); + +# Handle deletion from the Norwegian national patron database, if it is enabled +# If the "deletelocal" parameter is set to "false", the regular deletion will be +# short circuited, and only a deletion from the national database can be carried +# out. If "deletelocal" is set to "true", or not set to anything normal +# deletion will be done. +my $deletelocal = $input->param('deletelocal') eq 'false' ? 0 : 1; # Deleting locally is the default +if ( C4::Context->preference('NorwegianPatronDBEnable') == 1 ) { + if ( $input->param('deleteremote') eq 'true' ) { + # Mark for deletion, then try a live sync + NLMarkForDeletion( $member ); + NLSync({ 'borrowernumber' => $member }); + } +} + my $issues = GetPendingIssues($member); # FIXME: wasteful call when really, we only want the count my $countissues = scalar(@$issues); @@ -80,7 +96,7 @@ my $dbh = C4::Context->dbh; my $sth=$dbh->prepare("Select * from borrowers where guarantorid=?"); $sth->execute($member); my $data=$sth->fetchrow_hashref; -if ($countissues > 0 or $flags->{'CHARGES'} or $data->{'borrowernumber'}){ +if ($countissues > 0 or $flags->{'CHARGES'} or $data->{'borrowernumber'} or $deletelocal == 0){ # print $input->header; my ($picture, $dberror) = GetPatronImage($bor->{'borrowernumber'}); @@ -112,9 +128,12 @@ if ($countissues > 0 or $flags->{'CHARGES'} or $data->{'borrowernumber'}){ if ($flags->{'CHARGES'} ne '') { $template->param(charges => $flags->{'CHARGES'}->{'amount'}); } - if ($data) { + if ($data->{'borrowernumber'}) { $template->param(guarantees => 1); } + if ($deletelocal == 0) { + $template->param(keeplocal => 1); + } output_html_with_http_headers $input, $cookie, $template->output; } else { diff --git a/members/memberentry.pl b/members/memberentry.pl index 3c7f648..b887cb2 100755 --- a/members/memberentry.pl +++ b/members/memberentry.pl @@ -43,6 +43,7 @@ use C4::Branch; # GetBranches use C4::Form::MessagingPreferences; use Koha::Borrower::Debarments; use Koha::DateUtils; +use Koha::NorwegianPatronDB qw(NLGetSyncDataFromBorrowernumber); use vars qw($debug); @@ -416,12 +417,16 @@ if ((!$nok) and $nodouble and ($op eq 'insert' or $op eq 'save')){ if (C4::Context->preference('EnhancedMessagingPreferences') and $input->param('setting_messaging_prefs')) { C4::Form::MessagingPreferences::handle_form_action($input, { borrowernumber => $borrowernumber }, $template, 1, $newdata{'categorycode'}); } + # Try to do the live sync with the Norwegian national patron database, if it is enabled + if ( exists $data{'borrowernumber'} && C4::Context->preference('NorwegianPatronDBEnable') == 1 ) { + NLSync({ 'borrowernumber' => $borrowernumber }); + } } elsif ($op eq 'save'){ if ($NoUpdateLogin) { delete $newdata{'password'}; delete $newdata{'userid'}; } - &ModMember(%newdata) unless scalar(keys %newdata) <= 1; # bug 4508 - avoid crash if we're not + &ModMember(%newdata) unless scalar(keys %newdata) <= 1; # bug 4508 - avoid crash if we're not # updating any columns in the borrowers table, # which can happen if we're only editing the # patron attributes or messaging preferences sections @@ -470,6 +475,15 @@ if ($op eq "modify") { if ( $step == 4 ) { $template->param( categorycode => $borrower_data->{'categorycode'} ); } + # Add sync data to the user data + if ( C4::Context->preference('NorwegianPatronDBEnable') == 1 ) { + my $sync = NLGetSyncDataFromBorrowernumber( $borrowernumber ); + if ( $sync ) { + $template->param( + sync => $sync->sync, + ); + } + } } if ( $op eq "duplicate" ) { $template->param( updtype => 'I' ); diff --git a/members/moremember.pl b/members/moremember.pl index b1df1dd..80a4cae 100755 --- a/members/moremember.pl +++ b/members/moremember.pl @@ -53,6 +53,7 @@ use C4::Form::MessagingPreferences; use List::MoreUtils qw/uniq/; use C4::Members::Attributes qw(GetBorrowerAttributes); use Koha::Borrower::Debarments qw(GetDebarments IsDebarred); +use Koha::NorwegianPatronDB qw(NLGetSyncDataFromBorrowernumber); #use Smart::Comments; #use Data::Dumper; use DateTime; @@ -290,6 +291,16 @@ else { } } +# Add sync data to the user data +if ( C4::Context->preference('NorwegianPatronDBEnable') == 1 ) { + my $sync = NLGetSyncDataFromBorrowernumber( $borrowernumber ); + if ( $sync ) { + $data->{'sync'} = $sync->sync; + $data->{'syncstatus'} = $sync->syncstatus; + $data->{'lastsync'} = $sync->lastsync; + } +} + # check to see if patron's image exists in the database # basically this gives us a template var to condition the display of # patronimage related interface on diff --git a/members/nl-search.pl b/members/nl-search.pl new file mode 100755 index 0000000..e9bee0b --- /dev/null +++ b/members/nl-search.pl @@ -0,0 +1,161 @@ +#!/usr/bin/perl + +# Copyright 2013 Oslo Public Library +# +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see <http://www.gnu.org/licenses>. + +=head1 NAME + +nl-search.pl - Script for searching the Norwegian national patron database + +=head1 DESCRIPTION + +This script will search the Norwegian national patron database, and let staff +import patrons from the natial database into the local database. + +In order to use this, a username/password from the Norwegian national database +of libraries ("Base Bibliotek") is needed. A special key is also needed, in +order to decrypt and encrypt PIN-codes/passwords. + +See http://www.lanekortet.no/ for more information (in Norwegian). + +=cut + +use Modern::Perl; +use CGI; +use C4::Auth; +use C4::Category; +use C4::Context; +use C4::Output; +use C4::Members; +use C4::Members::Attributes qw( SetBorrowerAttributes ); +use Koha::NorwegianPatronDB qw( NLCheckSysprefs NLSearch NLDecodePin NLGetFirstname NLGetSurname NLSync ); +use Koha::Database; +use Koha::DateUtils; + +my $cgi = CGI->new; +my $dbh = C4::Context->dbh; +my $op = $cgi->param('op'); + +my ( $template, $loggedinuser, $cookie ) = get_template_and_user( + { + template_name => "members/nl-search.tt", + query => $cgi, + type => "intranet", + authnotrequired => 0, + flagsrequired => { borrowers => 1 }, + debug => 1, + } +); + +my $userenv = C4::Context->userenv; + +# Check sysprefs +my $check_result = NLCheckSysprefs(); +if ( $check_result->{'error'} == 1 ) { + $template->param( 'error' => $check_result ); + output_html_with_http_headers $cgi, $cookie, $template->output; + exit 0; +} + +if ( $op && $op eq 'search' ) { + + # Get the string we are searching for + my $identifier = $cgi->param('q'); + if ( $identifier ) { + # Local search + my @local_results = Search( $identifier ); + $template->param( 'local_result' => @local_results ); + # Search NL, unless we got at least one hit and further searching is + # disabled + if ( scalar @local_results == 0 || C4::Context->preference("NorwegianPatronDBSearchNLAfterLocalHit") == 1 ) { + # TODO Check the format of the identifier before searching NL + my $result = NLSearch( $identifier ); + unless ($result->fault) { + my $r = $result->result(); + # Send the data to the template + my @categories = C4::Category->all; + $template->param( + 'result' => $r, + 'categories' => \@categories, + ); + } else { + $template->param( 'error' => join ', ', $result->faultcode, $result->faultstring, $result->faultdetail ); + } + } + $template->param( 'q' => $identifier ); + } + +} elsif ( $op && $op eq 'save' ) { + + # This is where we map from fields in NL to fields in Koha + my %borrower = ( + 'surname' => NLGetSurname( $cgi->param('navn') ), + 'firstname' => NLGetFirstname( $cgi->param('navn') ), + 'sex' => $cgi->param('kjonn'), + 'dateofbirth' => $cgi->param('fdato'), + 'cardnumber' => $cgi->param('lnr'), + 'userid' => $cgi->param('lnr'), + 'address' => $cgi->param('p_adresse1'), + 'address2' => $cgi->param('p_adresse2'), + 'zipcode' => $cgi->param('p_postnr'), + 'city' => $cgi->param('p_sted'), + 'country' => $cgi->param('p_land'), + 'B_address' => $cgi->param('m_adresse1'), + 'B_address2' => $cgi->param('m_adresse2'), + 'B_zipcode' => $cgi->param('m_postnr'), + 'B_city' => $cgi->param('m_sted'), + 'B_country' => $cgi->param('m_land'), + 'password' => NLDecodePin( $cgi->param('pin') ), + 'dateexpiry' => $cgi->param('gyldig_til'), + 'email' => $cgi->param('epost'), + 'mobile' => $cgi->param('tlf_mobil'), + 'phone' => $cgi->param('tlf_hjemme'), + 'phonepro' => $cgi->param('tlf_jobb'), + 'branchcode' => $userenv->{'branch'}, + 'categorycode' => $cgi->param('categorycode'), + ); + # Add the new patron + my $borrowernumber = &AddMember(%borrower); + if ( $borrowernumber ) { + # Add extended patron attributes + SetBorrowerAttributes($borrowernumber, [ + { code => 'fnr', value => $cgi->param('fnr_hash') }, + ] ); + # Override the default sync data created by AddMember + my $borrowersync = Koha::Database->new->schema->resultset('BorrowerSync')->find({ + 'synctype' => 'norwegianpatrondb', + 'borrowernumber' => $borrowernumber, + }); + $borrowersync->update({ 'syncstatus', 'synced' }); + $borrowersync->update({ 'lastsync', $cgi->param('sist_endret') }); + $borrowersync->update({ 'hashed_pin', $cgi->param('pin') }); + # Try to sync in real time. If this fails it will be picked up by the cronjob + NLSync({ 'borrowernumber' => $borrowernumber }); + # Redirect to the edit screen + print $cgi->redirect( "/cgi-bin/koha/members/memberentry.pl?op=modify&destination=circ&borrowernumber=$borrowernumber" ); + } else { + $template->param( 'error' => 'COULD_NOT_ADD_PATRON' ); + } +} + +output_html_with_http_headers $cgi, $cookie, $template->output; + +=head1 AUTHOR + +Magnus Enger <digitalutvikling@gmail.com> + +=cut diff --git a/misc/cronjobs/nl-sync-from-koha.pl b/misc/cronjobs/nl-sync-from-koha.pl new file mode 100644 index 0000000..f62fd7a --- /dev/null +++ b/misc/cronjobs/nl-sync-from-koha.pl @@ -0,0 +1,184 @@ +#!/usr/bin/perl + +# Copyright 2014 Oslo Public Library + +=head1 NAME + +nl-sync-from-koha.pl - Sync patrons from Koha to the Norwegian national patron database (NL). + +=head1 SYNOPSIS + + perl nl-sync-from-koha.pl -v --run + +=cut + +use Koha::NorwegianPatronDB qw( NLCheckSysprefs NLSync ); +use Koha::Database; +use Getopt::Long; +use Pod::Usage; +use Modern::Perl; + +# Get options +my ( $run, $verbose, $debug ) = get_options(); + +=head1 ACTIONS + +=head2 + +Find local patrons that have been changed and need to be sent upstream to NL. +These patrons will be distinguished by two borrower attributes: + +=over 4 + +=item * The "nlstatus" attribute will have a value of "needsync". (Which means +that the patron has been changed in Koha, but not yet successfully synced +upstream.) + +=item * The "nlsync" attribute will have a value of 1. (Which means that this +patron has accepted to be synced with NL, as opposed to a value of 0 which +would indicate that the patron has asked not to be synced with NL.) + +=back + +=head1 STEPS + +This script performs the following steps: + +=head2 Check sysprefs + +=cut + +my $check_result = NLCheckSysprefs(); +if ( $check_result->{'error'} == 1 ) { + if ( $check_result->{'nlenabled'} == 0 ) { say "* Please activate this function with the NorwegianPatronDBEnable system preference." }; + if ( $check_result->{'endpoint'} == 0 ) { say "* Please specify an endpoint with the NorwegianPatronDBEndpoint system preference." }; + if ( $check_result->{'userpass'} == 0 ) { say "* Please fill in the NorwegianPatronDBUsername and NorwegianPatronDBPassword system preferences." }; + exit 0; +} + +unless ( $run ) { + say "* You have not specified --run, no real syncing will be done."; +} + +=head2 Find patrons that need to be synced + +=cut + +my @needs_sync = Koha::Database->new->schema->resultset('BorrowerSync')->search({ + -and => [ + sync => 1, + synctype => 'norwegianpatrondb', + -or => [ + syncstatus => 'edited', + syncstatus => 'new', + syncstatus => 'delete', + ], + ], +}); + +=head2 Do the actual sync + +=cut + +my $sync_success = 0; +my $sync_failed = 0; +foreach my $borrower ( @needs_sync ) { + my $cardnumber = $borrower->borrowernumber->cardnumber; + my $firstname = $borrower->borrowernumber->firstname; + my $surname = $borrower->borrowernumber->surname; + my $syncstatus = $borrower->syncstatus; + say "*** Syncing patron: $cardnumber - $firstname $surname ($syncstatus)" if $verbose; + if ( $run ) { + my $response = NLSync({ 'patron' => $borrower->borrowernumber }); + if ( $response ) { + my $result = $response->result; + if ( $result->{'status'} && $result->{'status'} == 1 ) { + $sync_success++; + } else { + $sync_failed++; + } + if ( $result->{'melding'} && $verbose ) { + say $result->{'melding'}; + } + } + } +} + +=head2 Summarize if verbose mode is enabled + +=cut + +if ( $verbose ) { + say "-----------------------------"; + say "Sync succeeded: $sync_success"; + say "Sync failed : $sync_failed"; +} + +=head1 OPTIONS + +=over 4 + +=item B<-r, --run> + +Actually carry out syncing operations. Without this option, the script will +only report what it would have done, but not change any data, locally or +remotely. + +=item B<-v --verbose> + +Report on the progress of the script. + +=item B<-d --debug> + +Even more output. + +=item B<-h, -?, --help> + +Prints this help message and exits. + +=back + +=cut + +sub get_options { + + # Options + my $run = '', + my $verbose = ''; + my $debug = ''; + my $help = ''; + + GetOptions ( + 'r|run' => \$run, + 'v|verbose' => \$verbose, + 'd|debug' => \$debug, + 'h|?|help' => \$help + ); + + pod2usage( -exitval => 0 ) if $help; + + return ( $run, $verbose, $debug ); + +} + +=head1 AUTHOR + +Magnus Enger <digitalutvikling@gmail.com> + +=head1 COPYRIGHT + +Copyright 2014 Oslo Public Library + +=head1 LICENSE + +This file is part of Koha. + +Koha is free software; you can redistribute it and/or modify it under the terms +of the GNU General Public License as published by the Free Software Foundation; +either version 3 of the License, or (at your option) any later version. + +You should have received a copy of the GNU General Public License along with +Koha; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, +Fifth Floor, Boston, MA 02110-1301 USA. + +=cut diff --git a/misc/cronjobs/nl-sync-to-koha.pl b/misc/cronjobs/nl-sync-to-koha.pl new file mode 100644 index 0000000..84e2378 --- /dev/null +++ b/misc/cronjobs/nl-sync-to-koha.pl @@ -0,0 +1,192 @@ +#!/usr/bin/perl + +# Copyright 2014 Oslo Public Library + +=head1 NAME + +nl-sync-to-koha.pl - Sync patrons from the Norwegian national patron database (NL) to Koha. + +=head1 SYNOPSIS + + perl nl-sync-to-koha.pl -v --run + +=cut + +use C4::Members; +use C4::Members::Attributes qw( UpdateBorrowerAttribute ); +use Koha::NorwegianPatronDB qw( NLCheckSysprefs NLGetChanged ); +use Koha::Database; +use Getopt::Long; +use Pod::Usage; +use Modern::Perl; + +# Get options +my ( $run, $from, $verbose, $debug ) = get_options(); + +my $check_result = NLCheckSysprefs(); +if ( $check_result->{'error'} == 1 ) { + if ( $check_result->{'nlenabled'} == 0 ) { say "* Please activate this function with the NorwegianPatronDBEnable system preference." }; + if ( $check_result->{'endpoint'} == 0 ) { say "* Please specify an endpoint with the NorwegianPatronDBEndpoint system preference." }; + if ( $check_result->{'userpass'} == 0 ) { say "* Please fill in the NorwegianPatronDBUsername and NorwegianPatronDBPassword system preferences." }; + exit 0; +} + +unless ( $run ) { + say "* You have not specified --run, no real syncing will be done."; +} + +# Do the sync +my $sync_success = 0; +my $sync_failed = 0; +my $skipped_local_change = 0; + +# Get the borrowers that have been changed +my $result = NLGetChanged( $from ); + +if ( $verbose ) { + say 'Number of records: ' . $result->{'antall_poster_returnert'}; + say 'Number of hits: ' . $result->{'antall_treff'}; + say 'Message: ' . $result->{'melding'}; + say 'Status: ' . $result->{'status'}; + say 'Server time: ' . $result->{'server_tid'}; + say "-----------------------------"; +} + +# Loop through the patrons +foreach my $patron ( @{ $result->{'kohapatrons'} } ) { + if ( $verbose ) { + if ( $patron->{'surname'} ) { + say "*** Name: " . $patron->{'surname'}; + } else { + say "*** No name"; + } + say 'Created by: ' . $patron->{'_extra'}->{'created_by'}; + say 'Last change by: ' . $patron->{'_extra'}->{'last_change_by'}; + } + # Only sync in changes made by other libraries + if ( C4::Context->preference("NorwegianPatronDBUsername") ne $patron->{'_extra'}->{'last_change_by'} ) { + # Make a copy of the data in the hashref and store it as a hash + my %clean_patron = %$patron; + # Delete the extra data from the copy of the hashref + delete $clean_patron{'_extra'}; + # Find the borrowernumber based on cardnumber + my $stored_patron = Koha::Database->new->schema->resultset('Borrower')->find({ + 'cardnumber' => $patron->{'cardnumber'} + }); + my $borrowernumber = $stored_patron->borrowernumber; + if ( $run ) { + # Call ModMember + my $success = ModMember( + 'borrowernumber' => $borrowernumber, + %clean_patron, + ); + if ( $success ) { + # Get the sync object + my $sync = Koha::Database->new->schema->resultset('BorrowerSync')->find({ + 'synctype' => 'norwegianpatrondb', + 'borrowernumber' => $borrowernumber, + }); + # Update the syncstatus to 'synced' + $sync->update( { 'syncstatus' => 'synced' } ); + # Update the 'synclast' attribute with the "server time" ("server_tid") returned by the method + $sync->update( { 'lastsync' => $result->{'result'}->{'server_tid'} } ); + # Save social security number as attribute + UpdateBorrowerAttribute( + $borrowernumber, + { code => 'fnr', attribute => $patron->{'_extra'}->{'socsec'} }, + ); + $sync_success++; + } else { + $sync_failed++; + } + } + } else { + say "Skipped, local change" if $verbose; + $skipped_local_change++; + } +} + +if ( $verbose ) { + say "-----------------------------"; + say "Sync succeeded: $sync_success"; + say "Sync failed : $sync_failed"; + say "Skipped local change: $skipped_local_change"; +} + +=head1 OPTIONS + +=over 4 + +=item B<-r, --run> + +Actually carry out syncing operations. Without this option, the script will +only report what it would have done, but not change any data, locally or +remotely. + +=item B<-v --verbose> + +Report on the progress of the script. + +=item B<-f --from> + +Date and time to sync from, if this should be different from "1 second past +midnight of the day before". The date should be in this format: + + 2014-06-03T00:00:01 + +=item B<-d --debug> + +Even more output. + +=item B<-h, -?, --help> + +Prints this help message and exits. + +=back + +=cut + +sub get_options { + + # Options + my $run = '', + my $from = '', + my $verbose = ''; + my $debug = ''; + my $help = ''; + + GetOptions ( + 'r|run' => \$run, + 'f|from=s' => \$from, + 'v|verbose' => \$verbose, + 'd|debug' => \$debug, + 'h|?|help' => \$help + ); + + pod2usage( -exitval => 0 ) if $help; + + return ( $run, $from, $verbose, $debug ); + +} + +=head1 AUTHOR + +Magnus Enger <digitalutvikling@gmail.com> + +=head1 COPYRIGHT + +Copyright 2014 Oslo Public Library + +=head1 LICENSE + +This file is part of Koha. + +Koha is free software; you can redistribute it and/or modify it under the terms +of the GNU General Public License as published by the Free Software Foundation; +either version 3 of the License, or (at your option) any later version. + +You should have received a copy of the GNU General Public License along with +Koha; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, +Fifth Floor, Boston, MA 02110-1301 USA. + +=cut diff --git a/t/NorwegianPatronDB.t b/t/NorwegianPatronDB.t new file mode 100644 index 0000000..483fe2a --- /dev/null +++ b/t/NorwegianPatronDB.t @@ -0,0 +1,583 @@ +#!/usr/bin/perl + +use Modern::Perl; +use Test::More; +use Test::MockModule; +use t::lib::Mocks; +use Data::Dumper; +use SOAP::Lite; + +# Check that all the modules we need are installed, or bail out +BEGIN { + eval { + require Test::DBIx::Class; + 1; + } or do { + plan skip_all => "Test::DBIx::Class is not available"; + }; +} +BEGIN { + eval { + require SOAP::Lite; + 1; + } or do { + plan skip_all => "SOAP::Lite is not available"; + }; +} +BEGIN { + eval { + require Crypt::GCrypt; + 1; + } or do { + plan skip_all => "Crypt::GCrypt is not available"; + }; +} +BEGIN { + eval { + require Convert::BaseN; + 1; + } or do { + plan skip_all => "Convert::BaseN is not available"; + }; +} + +use Test::DBIx::Class { + schema_class => 'Koha::Schema', + connect_info => ['dbi:SQLite:dbname=:memory:','',''], + connect_opts => { name_sep => '.', quote_char => '`', }, + fixture_class => '::Populate', +}, 'Borrower', 'BorrowerSync'; + +# Make the code in the module use our mocked Koha::Schema/Koha::Database +my $db = Test::MockModule->new('Koha::Database'); +$db->mock( + # Schema() gives us the DB connection set up by Test::DBIx::Class + _new_schema => sub { return Schema(); } +); + +fixtures_ok [ + 'Borrower' => [ + [qw/firstname surname borrowernumber address city/], + ['Test', 'Borrower', 1, 'Test road', 'Test city'], + ['Test', 'Borrower', 2, 'Test road', 'Test city'], + ['Test', 'Borrower', 3, 'Test road', 'Test city'], + ['Test', 'Borrower', 4, 'Test road', 'Test city'], + ], + 'BorrowerSync' => [ + [qw/borrowernumber sync syncstatus lastsync hashed_pin synctype/], + [1, 1, 'new', '2014-03-31T12:35:14', 'abc', 'norwegianpatrondb' ], + [2, 1, 'edited', '2014-03-31T12:35:14', 'abc', 'norwegianpatrondb' ], + [3, 1, 'new', '2014-03-31T12:35:14', 'abc', 'norwegianpatrondb' ], + [4, 1, 'new', '2014-03-31T12:35:14', 'abc', 'norwegianpatrondb' ], + ], +], 'installed some fixtures'; + +=head1 LOADING THE MODULE + +=cut + +BEGIN { use_ok( 'Koha::NorwegianPatronDB', ':all' ) } + + +=head1 UTILITY SUBROUTINES + +=head2 NLCheckSysprefs + +Relevant sysprefs: + +=over 4 + +=item * NorwegianPatronDBEnable + +=item * NorwegianPatronDBEndpoint + +=item * NorwegianPatronDBUsername + +=item * NorwegianPatronDBPassword + +=back + +=cut + +t::lib::Mocks::mock_preference('NorwegianPatronDBEnable', 0); +t::lib::Mocks::mock_preference('NorwegianPatronDBEndpoint', ''); +t::lib::Mocks::mock_preference('NorwegianPatronDBUsername', ''); +t::lib::Mocks::mock_preference('NorwegianPatronDBPassword', ''); +ok( my $result = NLCheckSysprefs(), 'call NLCheckSysprefs() ok' ); +is( $result->{ 'error' }, 1, 'error detected' ); +is( $result->{ 'nlenabled' }, 0, 'NL is not enabled' ); +is( $result->{ 'endpoint' }, 0, 'an endpoint is not specified' ); +is( $result->{ 'userpass' }, 0, 'username and/or password is missing' ); + +t::lib::Mocks::mock_preference('NorwegianPatronDBEnable', 1); +ok( $result = NLCheckSysprefs(), 'call NLCheckSysprefs() ok' ); +is( $result->{ 'error' }, 1, 'error detected' ); +is( $result->{ 'nlenabled' }, 1, 'NL is enabled' ); +is( $result->{ 'endpoint' }, 0, 'an endpoint is not specified' ); +is( $result->{ 'userpass' }, 0, 'username and/or password is missing' ); + +t::lib::Mocks::mock_preference('NorwegianPatronDBEnable', 0); +t::lib::Mocks::mock_preference('NorwegianPatronDBUsername', 'user'); +t::lib::Mocks::mock_preference('NorwegianPatronDBPassword', 'pass'); +ok( $result = NLCheckSysprefs(), 'call NLCheckSysprefs() ok' ); +is( $result->{ 'error' }, 1, 'error detected' ); +is( $result->{ 'nlenabled' }, 0, 'NL is not enabled' ); +is( $result->{ 'endpoint' }, 0, 'an endpoint is not specified' ); +is( $result->{ 'userpass' }, 1, 'username and/or password is present' ); + +t::lib::Mocks::mock_preference('NorwegianPatronDBEnable', 1); +t::lib::Mocks::mock_preference('NorwegianPatronDBEndpoint', 'http://example.com/'); +ok( $result = NLCheckSysprefs(), 'call NLCheckSysprefs() ok' ); +is( $result->{ 'error' }, 0, 'no error detected' ); +is( $result->{ 'nlenabled' }, 1, 'NL is enabled' ); +is( $result->{ 'endpoint' }, 1, 'an endpoint is specified' ); +is( $result->{ 'userpass' }, 1, 'username and/or password is present' ); + +=head2 NLGetFirstname and NLGetSurname + +=cut + +my $firstname = 'Firstname'; +my $surname = 'Surname'; +my $fullname = "$surname, $firstname"; +my $wrongname = "$surname $firstname"; + +is( NLGetFirstname( $fullname ), $firstname, 'can get firstname from name' ); +is( NLGetSurname( $fullname ), $surname, 'can get surname from name' ); +is( NLGetFirstname( $wrongname ), $wrongname, 'returns full string when name misses comma' ); +is( NLGetSurname( $wrongname ), $wrongname, 'returns full string when name misses comma' ); + +=head2 NLDecodePin and NLEncryptPIN + +=cut + +my $pin = '1234'; +my $hash = NLEncryptPIN( $pin ); + +is( NLEncryptPIN( $pin ), $hash, 'NLEncryptPIN works' ); +is( NLDecodePin( $hash ), $pin, 'NLDecodePin works' ); + +=head2 NLUpdateHashedPIN + +TODO + +=cut + +is ( BorrowerSync->find({ 'borrowernumber' => 1 })->get_column('hashed_pin'), 'abc', 'hashed_pin is "abc"' ); +# Set a new pin +my $new_pin = 'bcd'; +ok( NLUpdateHashedPIN( 1, $new_pin ), 'NLUpdateHashedPIN runs ok' ); +# Hash the new pin and compare it to the one stored in the database +my $hashed_pin = Koha::NorwegianPatronDB::_encrypt_pin( $new_pin ); +is ( BorrowerSync->find({ 'borrowernumber' => 1 })->get_column('hashed_pin'), $hashed_pin, 'hashed_pin was updated' ); + +=head2 NLMarkForDeletion + +=cut + +is ( BorrowerSync->find({ 'borrowernumber' => 3 })->get_column('syncstatus'), 'new', 'syncstatus is "new"' ); +ok( NLMarkForDeletion( 3 ), 'NLMarkForDeletion runs ok' ); +# Check that the syncstatus was updated. Note: We will use this status later, to check syncing of deleted borrowers +is ( BorrowerSync->find({ 'borrowernumber' => 3 })->get_column('syncstatus'), 'delete', 'syncstatus is "delete"' ); + +=head2 NLGetSyncDataFromBorrowernumber + +=cut + +ok( my $sync_data = NLGetSyncDataFromBorrowernumber( 1 ), 'NLGetSyncDataFromBorrowernumber runs ok' ); +isa_ok( $sync_data, 'Koha::Schema::Result::BorrowerSync' ); +is( $sync_data->sync, 1, 'the sync is on' ); +is( $sync_data->syncstatus, 'new', 'syncstatus is "new"' ); +is( $sync_data->lastsync, '2014-03-31T12:35:14', 'lastsync is ok' ); +is( $sync_data->hashed_pin, $hashed_pin, 'hashed_pin is ok' ); + +=head1 SUBROUTINES THAT TALK TO SOAP + +=head2 NLSearch + +=cut + +my $lite = Test::MockModule->new('SOAP::Lite'); + +# Mock a successfull call to the "hent" method +$lite->mock( + hent => sub { return SOAP::Deserializer->deserialize( hent_success() )->result; } +); +ok( my $res = NLSearch( '12345678910' ), 'successfull call to NLSearch' ); +is( $res->{'antall_poster_returnert'}, 1, 'got 1 record' ); +isa_ok( $res, "Resultat" ); +isa_ok( $res->{'respons_poster'}, "LaanerListe" ); +isa_ok( $res->{'respons_poster'}[0], "Laaner" ); + +# Mock an unsuccessfull call to the "hent" method +$lite->mock( + hent => sub { return SOAP::Deserializer->deserialize( hent_failure() )->result; } +); +ok( $res = NLSearch( '12345678910' ), 'call to NLSearch with an illegal argument' ); +is( $res->{'antall_poster_returnert'}, 0, 'got 0 records' ); +isa_ok( $res, "Resultat" ); +like( $res->{'melding'}, qr/Ulovlig argument: hverken LNR eller FNR_HASH/, "got expected error message for an illegal identifier" ); + +=head2 NLSync + +=head3 New patron + +=cut + +my $borrower = Borrower->find({ 'borrowernumber' => 1 }); +$lite->mock( + nyPost => sub { return SOAP::Deserializer->deserialize( nyPost_success() )->result; } +); +is ( BorrowerSync->find({ 'borrowernumber' => 1 })->get_column('syncstatus'), 'new', 'patron is new' ); +ok ( $result = NLSync({ 'patron' => $borrower }), 'successfull call to NLSync via patron ("nyPost")' ); +is ( BorrowerSync->find({ 'borrowernumber' => 1 })->get_column('syncstatus'), 'synced', 'patron is synced' ); + +# Now do the same test, but pass in a borrowernumber, not a Koha::Schema::Result::Borrower +is ( BorrowerSync->find({ 'borrowernumber' => 4 })->get_column('syncstatus'), 'new', 'patron is new' ); +ok ( $result = NLSync({ 'borrowernumber' => 4 }), 'successfull call to NLSync via borrowernumber ("nyPost")' ); +is ( BorrowerSync->find({ 'borrowernumber' => 4 })->get_column('syncstatus'), 'synced', 'patron is synced' ); + +=head3 Edited patron + +=cut + +ok ( $borrower = Borrower->find({ 'borrowernumber' => 2 }), 'find our "edited" mock patron' ); +$lite->mock( + endre => sub { return SOAP::Deserializer->deserialize( endre_success() )->result; } +); +is ( BorrowerSync->find({ 'borrowernumber' => 2 })->get_column('syncstatus'), 'edited', 'patron is edited' ); +ok ( $result = NLSync({ 'patron' => $borrower }), 'successfull call to NLSync ("endre")' ); +is ( BorrowerSync->find({ 'borrowernumber' => 2 })->get_column('syncstatus'), 'synced', 'patron is synced' ); + +=head3 Deleted patron + +=cut + +ok ( $borrower = Borrower->find({ 'borrowernumber' => 3 }), 'find our "deleted" mock patron' ); +$lite->mock( + slett => sub { return SOAP::Deserializer->deserialize( endre_success() )->result; } +); +is ( BorrowerSync->find({ 'borrowernumber' => 3 })->get_column('syncstatus'), 'delete', 'patron is marked for deletion' ); +ok ( $result = NLSync({ 'patron' => $borrower }), 'successfull call to NLSync ("slett")' ); +is ( BorrowerSync->find({ 'borrowernumber' => 3 })->get_column('sync'), 0, 'sync is now disabled' ); + +=head2 NLGetChanged + +=cut + +# Mock a successfull call to the "soekEndret" method +$lite->mock( + soekEndret => sub { return SOAP::Deserializer->deserialize( soekEndret_success() ); } +); +ok( $res = NLGetChanged(), 'successfull call to NLGetChanged - 2 results' ); +is( $res->{'melding'}, 'OK', 'got "OK"' ); +is( $res->{'antall_poster_returnert'}, 2, 'got 2 records' ); +isa_ok( $res, "Resultat" ); +isa_ok( $res->{'respons_poster'}, "LaanerListe" ); +isa_ok( $res->{'respons_poster'}[0], "Laaner" ); + + +# Mock a successfull call to the "soekEndret" method, but with zero new records +$lite->mock( + soekEndret => sub { return SOAP::Deserializer->deserialize( soekEndret_zero_new() ); } +); +ok( $res = NLGetChanged(), 'successfull call to NLGetChanged - 0 results' ); +is( $res->{'melding'}, 'ingen treff', 'got "ingen treff"' ); +is( $res->{'antall_poster_returnert'}, 0, 'got 0 records' ); +is( $res->{'antall_treff'}, 0, 'got 0 records' ); + +done_testing(); + +=head1 SAMPLE SOAP XML RESPONSES + +These responses can be gathered by setting "outputxml()" to true on the SOAP +client: + + my $client = SOAP::Lite + ->on_action( sub { return '""';}) + ->uri('http://lanekortet.no') + ->proxy('https://fl.lanekortet.no/laanekort/fl_test.php') + ->outputxml(1); + my $response = $client->slett( $x ); + say $response; + +Pretty formatting can be achieved by piping the output from a test script +through xmllint: + + perl my_test_script.pl > xmllint --format - + +=cut + +sub slett_success { + + return <<'ENDRESPONSE'; +<?xml version="1.0" encoding="UTF-8"?> +<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="http://lanekortet.no" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> + <SOAP-ENV:Body> + <ns1:slettResponse> + <return xsi:type="ns1:Svar"> + <status xsi:type="xsd:boolean">true</status> + <melding xsi:type="xsd:string">Test Testersen (1973-08-11) er slettet fra registeret</melding> + <lnr xsi:type="xsd:string">N000106188</lnr> + <server_tid xsi:type="xsd:string">2014-06-02T16:51:58</server_tid> + </return> + </ns1:slettResponse> + </SOAP-ENV:Body> +</SOAP-ENV:Envelope> +ENDRESPONSE + +} + +sub endre_success { + + return <<'ENDRESPONSE'; +<?xml version="1.0" encoding="UTF-8"?> +<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="http://lanekortet.no" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> + <SOAP-ENV:Body> + <ns1:endreResponse> + <return xsi:type="ns1:Svar"> + <status xsi:type="xsd:boolean">true</status> + <melding xsi:type="xsd:string">Oppdaterte felt: navn, p_adresse1, p_postnr, p_sted, p_land, fdato, fnr_hash, kjonn, pin, sist_endret, sist_endret_av</melding> + <lnr xsi:type="xsd:string">N000106188</lnr> + <server_tid xsi:type="xsd:string">2014-06-02T16:42:32</server_tid> + </return> + </ns1:endreResponse> + </SOAP-ENV:Body> +</SOAP-ENV:Envelope> +ENDRESPONSE + +} + +sub nyPost_success { + + return <<'ENDRESPONSE'; +<?xml version="1.0" encoding="UTF-8"?> +<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="http://lanekortet.no" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> + <SOAP-ENV:Body> + <ns1:nyPostResponse> + <return xsi:type="ns1:Svar"> + <status xsi:type="xsd:boolean">true</status> + <melding xsi:type="xsd:string">Ny post er opprettet</melding> + <lnr xsi:type="xsd:string">N000106188</lnr> + <server_tid xsi:type="xsd:string">2014-06-02T14:10:09</server_tid> + </return> + </ns1:nyPostResponse> + </SOAP-ENV:Body> +</SOAP-ENV:Envelope> +ENDRESPONSE + +} + +sub soekEndret_success { + +return <<'ENDRESPONSE'; +<?xml version="1.0" encoding="UTF-8"?> +<SOAP-ENV:Envelope + xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" + xmlns:ns1="http://lanekortet.no" + xmlns:xsd="http://www.w3.org/2001/XMLSchema" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" + SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> + <SOAP-ENV:Body> + <ns1:soekEndretResponse> + <return xsi:type="ns1:Resultat"> + <status xsi:type="xsd:boolean">true</status> + <melding xsi:type="xsd:string">OK</melding> + <antall_treff xsi:type="xsd:int">2</antall_treff> + <antall_poster_returnert xsi:type="xsd:int">2</antall_poster_returnert> + <neste_indeks xsi:type="xsd:int">0</neste_indeks> + <respons_poster SOAP-ENC:arrayType="ns1:Laaner[2]" xsi:type="ns1:LaanerListe"> + <item xsi:type="ns1:Laaner"> + <lnr xsi:type="xsd:string">N000106186</lnr> + <navn xsi:type="xsd:string">Hansen, Hanne</navn> + <p_adresse1 xsi:type="xsd:string"/> + <p_adresse2 xsi:type="xsd:string"/> + <p_postnr xsi:type="xsd:string"/> + <p_sted xsi:type="xsd:string">BØDØ</p_sted> + <p_land xsi:type="xsd:string">no</p_land> + <p_sjekk xsi:type="xsd:string">0</p_sjekk> + <m_adresse1 xsi:type="xsd:string"/> + <m_adresse2 xsi:type="xsd:string"/> + <m_postnr xsi:type="xsd:string"/> + <m_sted xsi:type="xsd:string"/> + <m_land xsi:type="xsd:string"/> + <m_sjekk xsi:type="xsd:string">0</m_sjekk> + <m_gyldig_til xsi:type="xsd:string">0000-00-00</m_gyldig_til> + <tlf_hjemme xsi:type="xsd:string"/> + <tlf_jobb xsi:type="xsd:string"/> + <tlf_mobil xsi:type="xsd:string"/> + <epost xsi:type="xsd:string"/> + <epost_sjekk xsi:type="xsd:string"/> + <prim_kontakt xsi:type="xsd:string"/> + <hjemmebibliotek xsi:type="xsd:string">5180401</hjemmebibliotek> + <fdato xsi:type="xsd:string">1994-04-08</fdato> + <fnr_hash xsi:type="xsd:string">11087395628</fnr_hash> + <kjonn xsi:type="xsd:string">F</kjonn> + <pin xsi:type="xsd:string">89308dfc85ee7a5826ae14e2d8efad1e</pin> + <passord xsi:type="xsd:string"/> + <feide xsi:type="xsd:string">0</feide> + <opprettet xsi:type="xsd:string">2014-04-28T15:20:38</opprettet> + <opprettet_av xsi:type="xsd:string">5180401</opprettet_av> + <sist_endret xsi:type="xsd:string">2014-04-28T15:20:38</sist_endret> + <sist_endret_av xsi:type="xsd:string">5180401</sist_endret_av> + <folkeregsjekk_dato xsi:type="xsd:string">0000-00-00</folkeregsjekk_dato> + </item> + <item xsi:type="ns1:Laaner"> + <lnr xsi:type="xsd:string">N000106184</lnr> + <navn xsi:type="xsd:string">Enger, Magnus</navn> + <p_adresse1 xsi:type="xsd:string">Svarthammarveien 633333</p_adresse1> + <p_adresse2 xsi:type="xsd:string"/> + <p_postnr xsi:type="xsd:string">8015</p_postnr> + <p_sted xsi:type="xsd:string">Bodø</p_sted> + <p_land xsi:type="xsd:string">no</p_land> + <p_sjekk xsi:type="xsd:string">0</p_sjekk> + <m_adresse1 xsi:type="xsd:string"/> + <m_adresse2 xsi:type="xsd:string"/> + <m_postnr xsi:type="xsd:string"/> + <m_sted xsi:type="xsd:string"/> + <m_land xsi:type="xsd:string">no</m_land> + <m_sjekk xsi:type="xsd:string">0</m_sjekk> + <m_gyldig_til xsi:type="xsd:string">0000-00-00</m_gyldig_til> + <tlf_hjemme xsi:type="xsd:string">95158548</tlf_hjemme> + <tlf_jobb xsi:type="xsd:string"/> + <tlf_mobil xsi:type="xsd:string"/> + <epost xsi:type="xsd:string">magnus@enger.priv.no</epost> + <epost_sjekk xsi:type="xsd:string"/> + <prim_kontakt xsi:type="xsd:string"/> + <hjemmebibliotek xsi:type="xsd:string">5180401</hjemmebibliotek> + <fdato xsi:type="xsd:string">1973-08-11</fdato> + <fnr_hash xsi:type="xsd:string">11087345795</fnr_hash> + <kjonn xsi:type="xsd:string">M</kjonn> + <pin xsi:type="xsd:string">a632c504b8c4fba3149115cb07e0796c</pin> + <passord xsi:type="xsd:string"/> + <feide xsi:type="xsd:string">0</feide> + <opprettet xsi:type="xsd:string">2014-04-28T14:52:02</opprettet> + <opprettet_av xsi:type="xsd:string">5180401</opprettet_av> + <sist_endret xsi:type="xsd:string">2014-05-13T11:01:33</sist_endret> + <sist_endret_av xsi:type="xsd:string">5180401</sist_endret_av> + <folkeregsjekk_dato xsi:type="xsd:string">0000-00-00</folkeregsjekk_dato> + </item> + </respons_poster> + <server_tid xsi:type="xsd:string">2014-05-16T14:44:44</server_tid> + </return> + </ns1:soekEndretResponse> + </SOAP-ENV:Body> +</SOAP-ENV:Envelope> +ENDRESPONSE +} + +sub soekEndret_zero_new { + return <<'ENDRESPONSE'; +<?xml version="1.0" encoding="UTF-8"?> + <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="http://lanekortet.no" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> + <SOAP-ENV:Body> + <ns1:soekEndretResponse> + <return xsi:type="ns1:Resultat"> + <status xsi:type="xsd:boolean">false</status> + <melding xsi:type="xsd:string">ingen treff</melding> + <antall_treff xsi:type="xsd:int">0</antall_treff> + <antall_poster_returnert xsi:type="xsd:int">0</antall_poster_returnert> + <neste_indeks xsi:type="xsd:int">0</neste_indeks> + <respons_poster SOAP-ENC:arrayType="ns1:Laaner[0]" xsi:type="ns1:LaanerListe"/> + <server_tid xsi:type="xsd:string">2014-05-20T13:02:02</server_tid> + </return> + </ns1:soekEndretResponse> + </SOAP-ENV:Body> + </SOAP-ENV:Envelope> +ENDRESPONSE +} + +sub hent_failure { + return <<'ENDRESPONSE'; +<?xml version="1.0" encoding="UTF-8"?> +<SOAP-ENV:Envelope + xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" + xmlns:ns1="http://lanekortet.no" + xmlns:xsd="http://www.w3.org/2001/XMLSchema" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" + SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> + <SOAP-ENV:Body> + <ns1:hentResponse> + <return xsi:type="ns1:Resultat"> + <status xsi:type="xsd:boolean">false</status> + <melding xsi:type="xsd:string">hent: Ulovlig argument: hverken LNR eller FNR_HASH</melding> + <antall_treff xsi:type="xsd:int">0</antall_treff> + <antall_poster_returnert xsi:type="xsd:int">0</antall_poster_returnert> + <neste_indeks xsi:type="xsd:int">0</neste_indeks> + <respons_poster SOAP-ENC:arrayType="ns1:Laaner[0]" xsi:type="ns1:LaanerListe"/> + <server_tid xsi:type="xsd:string">2014-05-15T10:56:24</server_tid> + </return> + </ns1:hentResponse> + </SOAP-ENV:Body> +</SOAP-ENV:Envelope> +ENDRESPONSE + +} + +sub hent_success { + +return <<'ENDRESPONSE'; +<?xml version="1.0" encoding="UTF-8"?> +<SOAP-ENV:Envelope + xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" + xmlns:ns1="http://lanekortet.no" + xmlns:xsd="http://www.w3.org/2001/XMLSchema" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" + SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> + <SOAP-ENV:Body> + <ns1:hentResponse> + <return xsi:type="ns1:Resultat"> + <status xsi:type="xsd:boolean">true</status> + <melding xsi:type="xsd:string">OK</melding> + <antall_treff xsi:type="xsd:int">1</antall_treff> + <antall_poster_returnert xsi:type="xsd:int">1</antall_poster_returnert> + <neste_indeks xsi:type="xsd:int">0</neste_indeks> + <respons_poster SOAP-ENC:arrayType="ns1:Laaner[1]" xsi:type="ns1:LaanerListe"> + <item xsi:type="ns1:Laaner"> + <lnr xsi:type="xsd:string">N000123456</lnr> + <navn xsi:type="xsd:string">Test, Testersen</navn> + <p_adresse1 xsi:type="xsd:string">Bibliotekveien 6</p_adresse1> + <p_adresse2 xsi:type="xsd:string"/> + <p_postnr xsi:type="xsd:string">1234</p_postnr> + <p_sted xsi:type="xsd:string">Lillevik</p_sted> + <p_land xsi:type="xsd:string">no</p_land> + <p_sjekk xsi:type="xsd:string">0</p_sjekk> + <m_adresse1 xsi:type="xsd:string"/> + <m_adresse2 xsi:type="xsd:string"/> + <m_postnr xsi:type="xsd:string"/> + <m_sted xsi:type="xsd:string"/> + <m_land xsi:type="xsd:string">no</m_land> + <m_sjekk xsi:type="xsd:string">0</m_sjekk> + <m_gyldig_til xsi:type="xsd:string">0000-00-00</m_gyldig_til> + <tlf_hjemme xsi:type="xsd:string"/> + <tlf_jobb xsi:type="xsd:string"/> + <tlf_mobil xsi:type="xsd:string">12345678</tlf_mobil> + <epost xsi:type="xsd:string">test@example.com</epost> + <epost_sjekk xsi:type="xsd:string">0</epost_sjekk> + <prim_kontakt xsi:type="xsd:string"/> + <hjemmebibliotek xsi:type="xsd:string">2060000</hjemmebibliotek> + <fdato xsi:type="xsd:string">1964-05-22</fdato> + <fnr_hash xsi:type="xsd:string">22056412345</fnr_hash> + <kjonn xsi:type="xsd:string">F</kjonn> + <pin xsi:type="xsd:string">g345abc123dab567abc78900abc123ab</pin> + <passord xsi:type="xsd:string"/> + <feide xsi:type="xsd:string"/> + <opprettet xsi:type="xsd:string">2005-10-20</opprettet> + <opprettet_av xsi:type="xsd:string">2060000</opprettet_av> + <sist_endret xsi:type="xsd:string">2013-05-13T13:51:24</sist_endret> + <sist_endret_av xsi:type="xsd:string">2060000</sist_endret_av> + <gyldig_til xsi:type="xsd:string"/> + <folkeregsjekk_dato xsi:type="xsd:string">0000-00-00</folkeregsjekk_dato> + </item> + </respons_poster> + <server_tid xsi:type="xsd:string">2014-01-07T14:43:18</server_tid> + </return> + </ns1:hentResponse> + </SOAP-ENV:Body> +</SOAP-ENV:Envelope> +ENDRESPONSE + +} -- 2.1.0