Quipu eCard Integration for PINES
authorChris Sharp <csharp@georgialibraries.org>
Fri, 14 Aug 2020 13:11:30 +0000 (09:11 -0400)
committerChris Sharp <csharp@georgialibraries.org>
Wed, 25 Nov 2020 16:43:06 +0000 (11:43 -0500)
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 <csharp@georgialibraries.org>
Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm
Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Ecard.pm
Open-ILS/src/sql/Pg/005.schema.actors.sql
Open-ILS/src/sql/Pg/950.data.seed-values.sql
Open-ILS/src/sql/Pg/upgrade/XXXX-quipu-ecard-integration.sql [new file with mode: 0644]

index ac4104d..a6e82ce 100644 (file)
@@ -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?
index eda9c13..1e47c2a 100644 (file)
@@ -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;
 
index 33ae628..5d61b4a 100644 (file)
@@ -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,
index bf9b329..9ba4bbb 100644 (file)
@@ -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 (file)
index 0000000..378856c
--- /dev/null
@@ -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;