From d7b4441cab840b1225ef01269c5fac6f8de78357 Mon Sep 17 00:00:00 2001 From: Chris Sharp Date: Fri, 14 Aug 2020 09:11:30 -0400 Subject: [PATCH] Quipu eCard Integration for PINES Altering KCLS's Quipu eCard implementation to align with PINES's needs, with an eye towards a generic feature that could be submitted to Evergreen master. Signed-off-by: Chris Sharp --- .../src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm | 7 +- .../perlmods/lib/OpenILS/WWW/EGCatLoader/Ecard.pm | 249 +++++++++++---------- Open-ILS/src/sql/Pg/005.schema.actors.sql | 21 ++ Open-ILS/src/sql/Pg/950.data.seed-values.sql | 23 +- .../Pg/upgrade/XXXX-quipu-ecard-integration.sql | 51 +++++ 5 files changed, 223 insertions(+), 128 deletions(-) create mode 100644 Open-ILS/src/sql/Pg/upgrade/XXXX-quipu-ecard-integration.sql diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm index ac4104db75..a6e82ceaeb 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm @@ -182,9 +182,10 @@ sub load { $self->load_simple("myopac") if $path =~ m:opac/myopac:; # A default page for myopac parts - return $self->load_ecard_form if $path =~ m|opac/ecard/form|; - return $self->load_ecard_submit if $path =~ m|opac/ecard/submit|; - return $self->load_ecard_verify if $path =~ m|opac/ecard/verify|; + # maybe make these optional parts of load_patron_reg? + #return $self->load_ecard_form if $path =~ m|opac/ecard/form|; + return $self->load_ecard_submit if $path =~ m|opac/ecard/submit|; + return $self->load_ecard_verify if $path =~ m|opac/ecard/verify|; if($path =~ m|opac/login|) { return $self->load_login unless $self->editor->requestor; # already logged in? diff --git a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Ecard.pm b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Ecard.pm index eda9c13871..1e47c2ad32 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Ecard.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Ecard.pm @@ -10,49 +10,58 @@ use OpenILS::Utils::CStoreEditor qw/:funcs/; use OpenILS::Event; use Data::Dumper; use LWP::UserAgent; -use OpenILS::Utils::KCLSNormalize; use DateTime; use Digest::MD5 qw(md5_hex); $Data::Dumper::Indent = 0; my $U = 'OpenILS::Application::AppUtils'; - -my $PROVISIONAL_ECARD_GRP = 951; -my $FULL_ECARD_GRP = 952; -my $ECARD_VERIFY_IDENT = 102; - -my $HEADER_FOOTER_URL = - 'https://kcls.bibliocommons.com/widgets/external_templates.json'; -my $HEADER_FOOTER_TIMEOUT = 5; - my @api_fields = ( {name => 'vendor_username', required => 1}, {name => 'vendor_password', required => 1}, {name => 'first_given_name', class => 'au', required => 1}, {name => 'second_given_name', class => 'au'}, {name => 'family_name', class => 'au', required => 1}, + {name => 'suffix', class => 'au'}, {name => 'email', class => 'au', required => 1}, {name => 'passwd', class => 'au', required => 1}, {name => 'day_phone', class => 'au', required => 1}, {name => 'dob', class => 'au', required => 1}, {name => 'home_ou', class => 'au', required => 1}, - {name => 'ident_value2', + {name => 'ident_type', class => 'au', required => 1}, + {name => 'ident_value', class => 'au', required => 1}, + {name => 'guardian', class => 'au', notes => "AKA parent/guardian", required_if => 'Patron is less than 18 years old' }, - {name => 'billing_street1', class => 'aua', required => 1}, - {name => 'billing_street1_name'}, - {name => 'billing_street2', class => 'aua'}, - {name => 'billing_city', class => 'aua', required => 1}, - {name => 'billing_post_code', class => 'aua', required => 1}, - {name => 'billing_county', class => 'aua', required => 1}, - {name => 'billing_state', class => 'aua', required => 1}, - {name => 'billing_country', class => 'aua', required => 1}, - {name => 'events_mailing', class => 'asc'}, - {name => 'foundation_mailing', class => 'asc'} + {name => 'pref_first_given_name', class => 'au'}, + {name => 'pref_second_given_name', class => 'au'}, + {name => 'pref_family_name', class => 'au'}, + {name => 'pref_suffix', class => 'au'}, + {name => 'physical_street1', class => 'aua', required => 1}, + {name => 'physical_street1_name'}, + {name => 'physical_street2', class => 'aua'}, + {name => 'physical_city', class => 'aua', required => 1}, + {name => 'physical_post_code', class => 'aua', required => 1}, + {name => 'physical_county', class => 'aua', required => 1}, + {name => 'physical_state', class => 'aua', required => 1}, + {name => 'physical_country', class => 'aua', required => 1}, + {name => 'mailing_street1', class => 'aua', required => 1}, + {name => 'mailing_street1_name'}, + {name => 'mailing_street2', class => 'aua'}, + {name => 'mailing_city', class => 'aua', required => 1}, + {name => 'mailing_post_code', class => 'aua', required => 1}, + {name => 'mailing_county', class => 'aua', required => 1}, + {name => 'mailing_state', class => 'aua', required => 1}, + {name => 'mailing_country', class => 'aua', required => 1}, + {name => 'voter_registration', class => 'asvr', required => 1}, + {name => 'in_house_registration', required => 1}, ); + +# TODO: wrap the following in a check for a library setting as to whether or not +# to require emailed verification + # Random 6-character alpha-numeric code that avoids look-alike characters # https://ux.stackexchange.com/questions/53341/are-there-any-letters-numbers-that-should-be-avoided-in-an-id # Also exclude vowels to avoid creating any real (potentially offensive) words. @@ -63,16 +72,8 @@ sub generate_verify_code { return $string; } -sub load_ecard_form { - my $self = shift; - my $ctx = $self->ctx; - my $cgi = $self->cgi; - - $self->collect_header_footer; - return Apache2::Const::OK; -} - +# only if we're verifying the card via email sub load_ecard_verify { my $self = shift; my $cgi = $self->cgi; @@ -81,7 +82,7 @@ sub load_ecard_verify { # Loading the form. return Apache2::Const::OK if $cgi->request_method eq 'GET'; - $self->verify_ecard; + #$self->verify_ecard; return Apache2::Const::OK; } @@ -204,7 +205,6 @@ sub handle_datamode_api { return $self->compile_response; } - sub load_ecard_submit { my $self = shift; my $ctx = $self->ctx; @@ -239,10 +239,11 @@ sub load_ecard_submit { return $self->compile_response unless $self->make_user; return $self->compile_response unless $self->add_addresses; - return $self->compile_response unless $self->add_stat_cats; return $self->compile_response unless $self->check_dupes; return $self->compile_response unless $self->add_card; + return $self->compile_response unless $self->add_survey_responses; return $self->compile_response unless $self->save_user; + return $self->compile_response unless $self->add_usr_settings; return $self->compile_response if $ctx->{response}->{status}; $U->create_events_for_hook( @@ -273,7 +274,7 @@ sub login_vendor { my $auth = $U->simplereq( 'open-ils.auth_internal', 'open-ils.auth_internal.session.create', - {user_id => 1, org_unit => 4, login_type => 'temp'} + {user_id => 1, org_unit => 394, login_type => 'temp'} ); return unless $auth && $auth->{textcode} eq 'SUCCESS'; @@ -291,6 +292,7 @@ sub verify_vendor_host { return 1; } + sub compile_response { my $self = shift; my $ctx = $self->ctx; @@ -311,7 +313,6 @@ sub upperclense { return $value; } - # Create actor.usr perl object and populate column data sub make_user { my $self = shift; @@ -319,14 +320,20 @@ sub make_user { my $cgi = $self->cgi; my $au = Fieldmapper::actor::user->new; + my $in_house = $cgi->param('in_house_registration'); $au->isnew(1); - $au->ident_type($ECARD_VERIFY_IDENT); # Ecard Verification - $au->net_access_level(101); # No Access - $au->ident_value(generate_verify_code()); + $au->net_access_level(1); # Filtered + $au->name_keywords($in_house ? 'quipu_inhouse' : 'quipu_remote'); + my $home_ou = $cgi->param('home_ou'); + + my $perm_grp = $U->ou_ancestor_setting_value( + $home_ou, + 'lib.ecard_patron_profile' + ); - $au->profile($PROVISIONAL_ECARD_GRP); - my $grp = new_editor()->retrieve_permission_grp_tree($PROVISIONAL_ECARD_GRP); + $au->profile($perm_grp); + my $grp = new_editor()->retrieve_permission_grp_tree($perm_grp); $au->expire_date( DateTime->now(time_zone => 'local')->add( @@ -339,8 +346,8 @@ sub make_user { my $val = $cgi->param($field); - # Map to guardian field on the actor.usr object. - $field = 'guardian' if $field eq 'ident_value2'; + $au->juvenile(1) if $field eq 'guardian' && $val; + $au->day_phone(undef) if $field eq 'day_phone' && $val eq '--'; if ($field_info->{required} && !$val) { my $msg = "Value required for field: '$field'"; @@ -350,7 +357,7 @@ sub make_user { } $self->verify_dob($val) if $field eq 'dob' && $val; - $au->$field($self->upperclense($field, $val)); + $au->$field($val); } # Usename defaults to the user barcode @@ -362,12 +369,18 @@ sub make_user { sub add_card { my $self = shift; my $ctx = $self->ctx; + my $cgi = $self->cgi; my $user = $ctx->{user}; + my $home_ou = $cgi->param('home_ou'); + my $prefix = $U->ou_ancestor_setting_value( + $home_ou, + 'lib.ecard_barcode_prefix' + ) || 'AUTO'; my $bc = new_editor()->json_query({from => [ 'actor.generate_barcode', - '934', # ecard prefix - 7, # length of autogenated portion + $prefix, # ecard prefix + 8, # length of autogenated portion 'actor.auto_barcode_ecard_seq' # base sequence for autogeneration. ]})->[0]; @@ -389,7 +402,6 @@ sub add_card { return 1; } - # Returns 1 on success, undef on error. sub verify_dob { my $self = shift; @@ -451,38 +463,30 @@ sub add_addresses { my $e = $ctx->{editor}; my $user = $ctx->{user}; - my $bill_addr = Fieldmapper::actor::user_address->new; - $bill_addr->isnew(1); - $bill_addr->usr($user->id); - $bill_addr->address_type('RESIDENTIAL'); - $bill_addr->within_city_limits('f'); - - # Use as both billing and mailing via virtual ID. - $bill_addr->id(-1); + my $physical_addr = Fieldmapper::actor::user_address->new; + $physical_addr->isnew(1); + $physical_addr->usr($user->id); + $physical_addr->address_type('PHYSICAL'); + $physical_addr->within_city_limits('f'); + + my $mailing_addr = Fieldmapper::actor::user_address->new; + $mailing_addr->isnew(1); + $mailing_addr->usr($user->id); + $mailing_addr->address_type('MAILING'); + $mailing_addr->within_city_limits('f'); + + # Use as both billing and mailing via virtual ID. + $physical_addr->id(-1); + $mailing_addr->id(-2); $user->billing_address(-1); - $user->mailing_address(-1); - - my ($s1, $s2) = - OpenILS::Utils::KCLSNormalize::normalize_address_street( - $cgi->param('billing_street1'), - $cgi->param('billing_street2') - ); - - # Toss the normalized values back into CGI to simplify the steps below. - $cgi->param('billing_street1', $s1); - - if ($s2) { - $cgi->param('billing_street2', $s2); - } else { - $cgi->delete('billing_street2'); - } + $user->mailing_address(-2); # Confirm we have values for all of the required fields. # Apply values to our in-progress address object. for my $field_info (@api_fields) { my $field = $field_info->{name}; - next unless $field =~ /billing/; - next if $field =~ /billing_street1_/; + next unless $field =~ /physical|mailing/; + next if $field =~ /street1_/; my $val = $cgi->param($field); @@ -493,19 +497,63 @@ sub add_addresses { $logger->error("ECARD $msg"); } - (my $col_field = $field) =~ s/billing_//g; - $bill_addr->$col_field($self->upperclense($col_field, $val)); + if ($field =~ /physical/) { + (my $col_field = $field) =~ s/physical_//g; + $physical_addr->$col_field($val); + } else { + (my $col_field = $field) =~ s/mailing_//g; + $mailing_addr->$col_field($val); + } + } # exit if there were any errors above. return undef if $ctx->{response}->{status}; - $user->billing_address($bill_addr); - $user->addresses([$bill_addr]); + $user->billing_address($physical_addr); + $user->mailing_address($mailing_addr); + $user->addresses([$physical_addr, $mailing_addr]); return 1; } +sub add_usr_settings { + my $self = shift; + my $cgi = $self->cgi; + my $ctx = $self->ctx; + my $user = $ctx->{user}; + my %settings = ( + 'opac.hold_notify' => 'email' + ); + + $U->simplereq( + 'open-ils.actor', + 'open-ils.actor.patron.settings.update', + $self->ctx->{authtoken}, $user->id, \%settings); + + return 1; +} + +sub add_survey_responses { + my $self = shift; + my $cgi = $self->cgi; + my $user = $self->ctx->{user}; + my $answer = $cgi->param('voter_registration'); + + my $survey_response = Fieldmapper::action::survey_response->new; + $survey_response->id(-1); + $survey_response->isnew(1); + $survey_response->survey(1); # voter registration survey + $survey_response->question(1); + $survey_response->answer($answer); + + $user->survey_responses([$survey_response]); + return 1; +} + +# TODO: this is KCLS-specific, but maybe we can make it something +# generic for adding stat cats to the patron + sub add_stat_cats { my $self = shift; my $cgi = $self->cgi; @@ -565,7 +613,7 @@ sub check_dupes { $logger->info("ECARD found potential duplicate patrons: @$ids"); - if (my $streetname = $self->cgi->param('billing_street1_name')) { + if (my $streetname = $self->cgi->param('physical_street1_name')) { # We found matching patrons. Perform a secondary check on the # address street name only. @@ -602,7 +650,7 @@ sub check_dupes { $ctx->{response}->{status} = 'DUPLICATE'; $ctx->{response}->{messages} = ['first_given_name', - 'familiy_name', 'dob_year', 'billing_street1_name']; + 'family_name', 'dob_year', 'billing_street1_name']; return undef; } @@ -636,50 +684,5 @@ sub save_user { return 1; } -my %bc_parts; # cache -my @bc_part_keys = qw/css screen_reader_navigation header footer js/; -sub collect_header_footer { - my $self = shift; - - # kiosk == no header/footer - return if $self->cgi->param('kiosk'); - - if ($bc_parts{header}) { - $self->ctx->{"bc_$_"} = $bc_parts{$_} for @bc_part_keys; - return; - } - - my $agent = LWP::UserAgent->new(timeout => 5); - my $res = $agent->get($HEADER_FOOTER_URL); - $logger->info("Self-reg header/footer request returned code ".$res->code); - - if (!$res->is_success) { - $logger->error("Self-reg header/footer request ". - "[$HEADER_FOOTER_URL] failed with error " . $res->status_line); - - return; - } - - my $json = $res->content; - - if (!$json) { - $logger->error("Self-reg header/footer ". - "[$HEADER_FOOTER_URL] returned an empty response"); - return; - } - - - my $blob; - eval { $blob = OpenSRF::Utils::JSON->JSON2perl($json) }; - - if ($@) { - $logger->error("Self-reg header/footer ". - "[$HEADER_FOOTER_URL] returned invalid JSON : $@"); - return; - } - - $self->ctx->{"bc_$_"} = $bc_parts{$_} = $blob->{$_} for @bc_part_keys; -} - 1; diff --git a/Open-ILS/src/sql/Pg/005.schema.actors.sql b/Open-ILS/src/sql/Pg/005.schema.actors.sql index 33ae6282a1..5d61b4ac64 100644 --- a/Open-ILS/src/sql/Pg/005.schema.actors.sql +++ b/Open-ILS/src/sql/Pg/005.schema.actors.sql @@ -220,6 +220,27 @@ $$; CREATE INDEX actor_usr_setting_usr_idx ON actor.usr_setting (usr); +-- Start at 100 to avoid barcodes with long stretches of zeros early on. +-- eCard barcodes have 7 auto-generated digits. +CREATE SEQUENCE actor.auto_barcode_ecard_seq START 100 MAXVALUE 9999999; + +CREATE OR REPLACE FUNCTION actor.generate_barcode + (prefix TEXT, numchars INTEGER, seqname TEXT) RETURNS TEXT AS +$$ +/* +Generate a barcode starting with 'prefix' and followed by 'numchars' +numbers. The auto portion numbers are generated from the provided +sequence, guaranteeing uniquness across all barcodes generated with +the same sequence. The number is left-padded with zeros to meet the +numchars size requirement. Returns NULL if the sequnce value is +higher than numchars can accommodate .*/ + SELECT NEXTVAL($3); -- bump the sequence up 1 + SELECT CASE + WHEN LENGTH(CURRVAL($3)::TEXT) > $2 THEN NULL + ELSE $1 || LPAD(CURRVAL($3)::TEXT, $2, '0') + END; +$$ LANGUAGE SQL; + CREATE TABLE actor.stat_cat_sip_fields ( field CHAR(2) PRIMARY KEY, name TEXT NOT NULL, diff --git a/Open-ILS/src/sql/Pg/950.data.seed-values.sql b/Open-ILS/src/sql/Pg/950.data.seed-values.sql index 690c5d5464..24edcd236b 100644 --- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql +++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql @@ -2864,8 +2864,9 @@ INSERT INTO permission.grp_perm_map (grp, perm, depth, grantable) -- Admin user account INSERT INTO actor.passwd_type - (code, name, login, crypt_algo, iter_count) - VALUES ('main', 'Main Login Password', TRUE, 'bf', 10); + (code, name, login, crypt_algo, iter_count) VALUES + ('main', 'Main Login Password', TRUE, 'bf', 10) + ,('ecard_vendor', 'eCard Vendor Password', TRUE, 'bf', 10); INSERT INTO actor.usr ( profile, card, usrname, passwd, first_given_name, family_name, dob, master_account, super_user, ident_type, ident_value, home_ou ) VALUES ( 1, 1, md5(random()::text), md5(random()::text), 'Administrator', 'System Account', '1979-01-22', TRUE, TRUE, 1, 'identification', 1 ); @@ -4412,6 +4413,24 @@ INSERT into config.org_unit_setting_type 'coust', 'description'), 'string', null) +,( 'lib.ecard_barcode_prefix', 'lib', + oils_i18n_gettext('lib.ecard_barcode_prefix', + 'Barcode prefix for Quipu eCard feature', + 'coust', 'label'), + oils_i18n_gettext('lib.ecard_barcode_prefix', + 'Set the barcode prefix for new Quipu eCard users', + 'coust', 'description'), + 'string', null) + +,( 'lib.ecard_patron_profile', 'lib', + oils_i18n_gettext('lib.ecard_patron_profile', + 'Patron permission profile for Quipu eCard feature', + 'coust', 'label'), + oils_i18n_gettext('lib.ecard_barcode_prefix', + 'Patron permission profile for Quipu eCard feature', + 'coust', 'description'), + 'link', 'pgt') + ,( 'lib.info_url', 'lib', oils_i18n_gettext('lib.info_url', 'Library information URL (such as "http://example.com/about.html")', diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX-quipu-ecard-integration.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX-quipu-ecard-integration.sql new file mode 100644 index 0000000000..378856c2c6 --- /dev/null +++ b/Open-ILS/src/sql/Pg/upgrade/XXXX-quipu-ecard-integration.sql @@ -0,0 +1,51 @@ +BEGIN; + +-- Thank you, berick :-) +-- Start at 100 to avoid barcodes with long stretches of zeros early on. +-- eCard barcodes have 7 auto-generated digits. +CREATE SEQUENCE actor.auto_barcode_ecard_seq START 100 MAXVALUE 9999999; + +CREATE OR REPLACE FUNCTION actor.generate_barcode + (prefix TEXT, numchars INTEGER, seqname TEXT) RETURNS TEXT AS +$$ +/* +Generate a barcode starting with 'prefix' and followed by 'numchars' +numbers. The auto portion numbers are generated from the provided +sequence, guaranteeing uniquness across all barcodes generated with +the same sequence. The number is left-padded with zeros to meet the +numchars size requirement. Returns NULL if the sequnce value is +higher than numchars can accommodate .*/ + SELECT NEXTVAL($3); -- bump the sequence up 1 + SELECT CASE + WHEN LENGTH(CURRVAL($3)::TEXT) > $2 THEN NULL + ELSE $1 || LPAD(CURRVAL($3)::TEXT, $2, '0') + END; +$$ LANGUAGE SQL; + +INSERT INTO actor.passwd_type + (code, name, login, crypt_algo, iter_count) + VALUES ('ecard_vendor', 'eCard Vendor Password', TRUE, 'bf', 10); + +INSERT into config.org_unit_setting_type +( name, grp, label, description, datatype, fm_class ) VALUES + +( 'lib.ecard_barcode_prefix', 'lib', + oils_i18n_gettext('lib.ecard_barcode_prefix', + 'Barcode prefix for Quipu eCard feature', + 'coust', 'label'), + oils_i18n_gettext('lib.ecard_barcode_prefix', + 'Set the barcode prefix for new Quipu eCard users', + 'coust', 'description'), + 'string', null) + +,( 'lib.ecard_patron_profile', 'lib', + oils_i18n_gettext('lib.ecard_patron_profile', + 'Patron permission profile for Quipu eCard feature', + 'coust', 'label'), + oils_i18n_gettext('lib.ecard_barcode_prefix', + 'Patron permission profile for Quipu eCard feature', + 'coust', 'description'), + 'link', 'pgt') +; + +COMMIT; -- 2.11.0