latest updates user/csharp/lp1902939_student_card_integration
authorChris Sharp <csharp@georgialibraries.org>
Mon, 25 Oct 2021 17:26:29 +0000 (13:26 -0400)
committerChris Sharp <csharp@georgialibraries.org>
Fri, 5 Nov 2021 18:07:31 +0000 (14:07 -0400)
Open-ILS/src/support-scripts/import_student_data.pl

index 42cd8db..0b9b27d 100755 (executable)
 # This script is for importing student data from a CSV
 # exported from a student information system (SIS).
 #
+
+#TODO: STFP delete perms, create Exceptions dir
+
 use warnings;
 use strict;
-use File::Rsync;
+use Getopt::Long;
 use Text::CSV qw/ csv /;
 use OpenILS::Utils::Cronscript;
-use OpenILS::Application::Actor;
+use OpenSRF::Utils::Logger qw/ $logger /;
+use Date::Parse;
+# something to do rsync/file transfer
+use File::Rsync;
+use Data::Dumper;
+
+# TODO: setup command line options to 
+# control things like remote server, etc.
+
 
+# initiate Cronscript
 my $cscript = OpenILS::Utils::Cronscript->new();
+# set up a cstore editor
 my $editor = $cscript->editor(xact=>1);
+# set up AppUtils
+my $U = 'OpenILS::Application::AppUtils';
 
-# 0 = no stdout messages
-# 1 = basic
-# 2 = detailed
-my $debug = 1;
-# the script can be run manually outside of cron, just
-# append <filename> <school code> to the script invocation
+# set up variables
+my $work_dir_prefix = "/tmp";
+my $in_dir_prefix = "/home/opensrf/districts";
 my $prefix = $ARGV[1] if $ARGV[1];
 my @retrieved_files = $ARGV[0] if $ARGV[0];
-my $district_id = 0;
-my $error_message;
+my $is_opt_out;
 my $imports;
 my $updates;
 my $deletes;
-my $opt_out;
 my @exceptions;
+my @required_fields = qw(
+    school_id
+    student_id
+    student_fname
+    student_lname
+    student_dob 
+    address_street1
+    address_city
+    address_state
+    address_postal
+    grade
+);
+
+# we start by getting the active school districts
+# via cstore
+sub get_active_districts {
+    my $districts = $editor->search_student_card_district(
+        {active => 't'});
+    return $districts;
+}
+
+
+sub get_files {
+    # for each active district, check the FTP server for files
+    # and copy them over 
+    my $district = shift;
+    my $code = $district->code;
+    my $prev_files = get_prev_files($district);
+    
+    # TODO: define the directory structure above
+    my $work_dir = "$work_dir_prefix/$code/";
+    mkdir $work_dir unless -d $work_dir;
+    my $in_dir = "$in_dir_prefix/$code/In/";
+    $logger->info("StudentCard: rsync-ing from $in_dir to $work_dir");
+    my $rsync = File::Rsync->new(
+        archive      => 1,
+        compress     => 1);
+    my $try = $rsync->exec(
+        src => $in_dir, 
+        dest => $work_dir,
+        exclude => $prev_files);
+    my $error = $!;
+    return $error unless $try;
+    my @new_files;
+    opendir(DIR, $work_dir) or die $!;
+    while (my $file = readdir(DIR)) {
+        next if (grep(/$file/, @$prev_files)) || ($file =~ m/^\./);
+        push(@new_files, "$work_dir/$file");
+        $logger->info("StudentCard: $file is a new file");
+    }
+    closedir(DIR);
+    my $new_files = \@new_files;
+    return $new_files;
+}
+
+sub get_schools {
+    my $district = shift;
+    my $district_id = $district->id;
+    my $district_name = $district->name;
+    $logger->info("StudentCard: Getting schools for $district_name");
+    my $schools = $editor->search_student_card_school(
+       {district_id => $district_id}
+    );
+    return $schools;
+}
 
+sub get_prev_files {
+    my $district = shift;
+    my $district_id = $district->id;
+    my $district_name = $district->name;
+    my $prev_files = $editor->search_student_card_import(
+        {district_id => $district_id}
+    );
+    my @prev_files;
+    for my $file (@$prev_files) {
+        my $filename = $file->filename;
+        push(@prev_files, $filename);
+    }
+    my $filelist = join(' ', @prev_files);
+    $logger->info("StudentCard: Previous files for $district_name: $filelist");
+    return \@prev_files;
+}
+
+sub import_csv {
+    my $csv_file = shift;
+    $is_opt_out = 1 if $csv_file =~ /OptOut/;
+    my $student_entries = csv (
+        in => "$csv_file",
+        headers => "lc", #lc = "lowercase"
+        empty_is_undef => 1
+    ) or die Text::CSV->error_diag;
+    return $student_entries; # this is an array of hashes
+}
+
+sub check_required_fields {
+    my $student = shift;
+    for my $field (@required_fields) {
+        unless ( $student->{$field} ) {
+            my $id = $student->{student_id};
+            $logger->info("StudentCard: no $field for student ID $id");
+            add_exception($student, "Missing $field");
+            return 0;
+        }
+    }
+    return 1;   
+}
+
+sub lookup_school {
+    my ($schools, $school_id) = @_;
+    for my $school (@$schools) {
+        my $state_id = $school->state_id;
+        if (int($school_id) == $state_id) {
+            return $school;
+        }
+    }
+    $logger->warn("StudentCard: No school entered for $school_id");
+    return 0;
+}
 
 sub calculate_expire_date {
     my @now = localtime();
     my ($now_year, $now_month) = ($now[5] + 1900, $now[4] + 1);
     my $expire_year = $now_year;
-    if ($now_month > 6) { # any student registered before end of June gets expire date the following 9/15
+    if ($now_month > 6) { # any student registered before end of June gets expire date the following 10/15
         $expire_year += 1;
     }
     my $expire_date = $expire_year . "-10-15";
     return $expire_date;
 }
 
-# TODO: we need to alter this to accommodate format-agnostic dates
-sub calculate_password_from_dob {
+sub calculate_password {
     my $dob = shift;
-    my @elements = split('-', $dob);
-    my ($dob_month, $dob_year) = ($elements[1], $elements[0]);
-    my $password = $dob_month . $dob_year;
+    return 0 unless $dob;
+    my ($ss,$mm,$hh,$day,$month,$year,$zone) = strptime($dob);
+    if ($year =~ /(\d{3})/) {
+        $year += 1900;
+    } else{
+        $year += 2000;
+    }
+    $month += 1;
+    if ($month < 10) {
+            $month = "0"."$month";
+    }
+    my $password = $month . $year;
     return $password;
 }
 
-
-if (@retrieved_files) {
-    # we're in single-file mode, so get the district ID for later use
-    # TODO: get the district ID via cstore
-} else {
-    # go out and get the files we need
-        my @files;
-        if ($debug) {
-            print "Now processing files for " . $district->{code} . "\n";
-        }
-    
-        ## walk through the remote files and filter out any that do
-        ## not meet our naming requirements
-        foreach my $file (@files) {
-            # XXX hard-coding the "OptOut" string here - maybe make it a setting? 
-            if ($file =~ /${prefix}(OptOut)?_\d{12}/) { 
-                # TODO: add OpenSRF-y way to retrieve previous files
-                my $previous =  ;
-                # skip files we have already retreived
-                unless (grep /$file/, @$previous) {
-                    # get the file from the local FTP server
-                } else {
-                    if ($debug) {
-                        print "$file already retreived\n";
-                    }
-                }
-            }
+sub get_name_keywords {
+    my $grade = shift;
+    my $name_keywords;
+    if ($grade && $grade =~ /^\d+$/) {
+        if ($grade == 0) {
+            $name_keywords = "GradeK";
+        } elsif ($grade < 0) {
+            $name_keywords = "GradePK";
+        } elsif ($grade < 10) {
+            $name_keywords = "Grade0" . $grade;
+        } else {
+            $name_keywords = "Grade" . $grade;
         }
+    } elsif ($grade) {
+        $name_keywords = $grade;
     }
+    return $name_keywords;
 }
 
+# copied from Ecard.pm...
+# Create actor.usr perl object and populate column data
+sub make_user {
+    my ($student, $school, $district_code) = @_;
 
-foreach my $csv_file (@retrieved_files) {
-    $imports = 0;
-    $updates = 0;
-    $deletes = 0;
-    $opt_out = 1 if $csv_file =~ /OptOut/;
-    # a couple of options here - we can assume that the headers are correct
-    # and use headers => "auto" here, which will fail to work if there's a
-    # typo in the header names, or we can do headers => "skip" and trust
-    # that the export follows our prescribed order.
-    my $student_entries = csv (in => "$csv_file", headers => "auto", empty_is_undef => 1)
-        or die Text::CSV->error_diag;
-    foreach my $student (@$student_entries) {
-        # set up variables
-        # TODO: make these assignment use the hash we just created with col names
-        my $school_id       = @$student[0]; 
-        my $student_id      = @$student[1]; 
-        my $first_name      = @$student[2]; 
-        my $middle_name     = @$student[3]; 
-        my $last_name       = @$student[4]; 
-        my $dob             = @$student[5]; 
-        my $phone           = @$student[6]; 
-        my $email           = @$student[7]; 
-        my $street1         = @$student[8]; 
-        my $street2         = @$student[9]; 
-        my $city            = @$student[10]; 
-        my $county          = @$student[11];
-        my $state           = @$student[12]; 
-        my $post_code       = @$student[13]; 
-        my $parent_guardian = @$student[14]; 
-        my $grade           = @$student[15];
-        my $barcode         = $prefix . $student_id;
-        my $username        = $barcode;
-        my $password        = calculate_password_from_dob($dob);         
-        my $ident_type      = 3; # Other
-        my $ident_value     = $student_id;
-        my $expire_date     = calculate_expire_date;
-        my $juvenile        = "true";
-        my $name_keywords;
-        if ($grade && $grade =~ /^\d+$/) {
-            if ($grade == 0) {
-                $name_keywords = "GradeK";
-            } elsif ($grade < 0) {
-                $name_keywords = "GradePK";
-            } elsif ($grade < 10) {
-                $name_keywords = "Grade0" . $grade;
-            } else {
-                $name_keywords = "Grade" . $grade;
-            }
-        } elsif ($grade) {
-            $name_keywords = $grade;
-        }
-        # figure out how to get the home ou from the DB based
-        # on the school ID
-        # TODO: get these values via OpenSRF
-        my $home_branch = $school_result[0];
-        my $school_name = $school_result[1];
-        my $school_county = $school_result[2];
-        my $ident2_value = $school_name;
-        # make sure we have a usable county entry
-        # if not, add from the school data
-        if (!$county || $county =~ m/\d/) {
-            $county = $school_county;
-        }
-        # default to school for parent/guardian
-        if (!$parent_guardian) {
-            $parent_guardian = $school_name
-        }
-        # check that we have the required data for the student
-        my $no_import = 0;
-        unless (
-            $school_id && 
-            $student_id &&
-            $first_name &&
-            $last_name &&
-            $dob &&
-            $street1 &&
-            $city &&
-            $state &&
-            $post_code &&
-            $home_branch ) {
-            if ($debug == 2) {
-                print "Student $student_id is an exception.\n";
-            }
-            push @exceptions, $student;
-            $no_import = 1;
-        }
-        unless ($no_import) {
-            # check if we already have the student account
-            # TODO: retrieve these with OpenSRF
-            my $sth_check_barcode = $dbh->prepare($check_barcode_sql);
-            $sth_check_barcode->execute($barcode);
-            my $already_imported = $sth_check_barcode->fetchrow_array;
-            $sth_check_barcode->finish;
-            my $sth_user_id = $dbh->prepare($get_user_id_sql);
-            $sth_user_id->execute($barcode);
-            my @user_result = $sth_user_id->fetchrow_array;
-            $sth_user_id->finish;
-            my $user_id = $user_result[0];
-
-            if ($already_imported && !$opt_out) {
-                # assume that we're doing an update
-                my $sth_addr = $dbh->prepare($get_addr_sql);
-                $sth_addr->execute($user_id);
-                my @addr_result = $sth_addr->fetchrow_array;
-                $sth_addr->finish;
-                my $addr_id = $addr_result[0];
-                my @update_user_data = (
-                    $first_name,
-                    $middle_name,
-                    $last_name,
-                    $dob,
-                    $phone,
-                    $parent_guardian,
-                    $name_keywords,
-                    $ident_type,
-                    $ident_value,
-                    $ident2_value,
-                    $home_branch,
-                    $expire_date,
-                    $user_id
-                );
-                my @update_addr_data = (
-                    $street1,
-                    $street2,
-                    $city,
-                    $county,
-                    $state,
-                    $post_code,
-                    $addr_id
-                );
-                # do the update
-                if ($debug) {
-                    print "Updating $barcode\n";
-                }
-                # TODO: see if we still need the try/catch stuff
-                try {
-                    # update user
-                    if ($debug == 2) {
-                        print "Update user data: @update_user_data\n";
-                    }
-                    my $sth_update_user = $dbh->prepare($update_user_sql);
-                    $sth_update_user->execute(@update_user_data);
-                    # update address
-                    if ($debug == 2) {
-                        print "Update address data: @update_addr_data\n";
-                    }
-                    my $sth_update_addr = $dbh->prepare($update_address_sql);
-                    $sth_update_addr->execute(@update_addr_data);
-                    $dbh->commit;
-                    $sth_update_user->finish;
-                    $sth_update_addr->finish;
-                    $updates++;
-                } catch {
-                    warn "Transaction aborted because $_";
-                    eval { $dbh->rollback };
-                };
-            } elsif ($already_imported && $opt_out ) {
-                if ($debug) {
-                    print "Deleting $barcode\n";
-                }
-                try {
-                    # delete user
-                    my $sth_delete_user = $dbh->prepare($delete_user_sql);
-                    $sth_delete_user->execute($user_id);
-                    $dbh->commit;
-                    $sth_delete_user->finish;
-                    $deletes++
-                } catch {
-                    warn "Transaction aborted because $_";
-                    eval { $dbh->rollback };
-                };
-            } elsif ($opt_out) {
-                # do nothing
-                next;
-            } else {
-                # import user
-                my @user_data = (
-                    $profile, 
-                    $username, 
-                    $email,
-                    $password,
-                    $ident_type,
-                    $ident_value,
-                    $ident_type,
-                    $ident2_value,
-                    $first_name,
-                    $middle_name,
-                    $last_name,
-                    $phone,
-                    $home_branch,
-                    $dob,
-                    $expire_date,   
-                    $juvenile,
-                    $parent_guardian,
-                    $name_keywords
-                );
-                my @addr_data = (
-                    $username,
-                    $street1,
-                    $street2,
-                    $city,
-                    $county,
-                    $state,
-                    $post_code,
-                );
-                my @card_data = (
-                    $username,
-                    $barcode
-                );
-                my @link_card_data = (
-                    $barcode,
-                    $username
-                );
-                my @link_addr_data = (
-                    $username,
-                    $username
-                );
-                    
-               # do the import here
-                if ($debug) {
-                    print "Importing $barcode\n";
-                    }
-                    try {
-                        # insert user
-                        if ($debug == 2) {
-                            print "Inserting user data: @user_data\n";
-                        }
-                        my $sth_insert_user = $dbh->prepare($insert_user_sql);
-                        $sth_insert_user->execute(@user_data);
-                        # insert address
-                        if ($debug == 2) {
-                            print "Inserting address data: @addr_data\n";
-                        }
-                        my $sth_insert_addr = $dbh->prepare($insert_address_sql);
-                        $sth_insert_addr->execute(@addr_data);
-                        # link address back to the user
-                        my $sth_link_addr = $dbh->prepare($link_addr_sql);
-                        $sth_link_addr->execute(@link_addr_data);
-                        # insert card
-                        if ($debug == 2) {
-                            print "Inserting card data: @card_data\n";
-                        }
-                        my $sth_insert_card = $dbh->prepare($insert_card_sql);
-                        $sth_insert_card->execute(@card_data);
-                        # link card back to the user
-                        my $sth_link_card = $dbh->prepare($link_card_sql);
-                        $sth_link_card->execute(@link_card_data);
-                        $dbh->commit;
-                        $sth_insert_user->finish;
-                        $sth_insert_addr->finish;
-                        $sth_link_addr->finish;
-                        $sth_insert_card->finish;
-                        $sth_link_card->finish;
-                    } catch {
-                        warn "Transaction aborted because $_";
-                        eval { $dbh->rollback };
-                    };
-                    $imports++;
-                }
-            
-            }
-        }
+    my $au = Fieldmapper::actor::user->new;
+
+    print Dumper($au);
+
+    # set up variables
+    my $student_id      = $student->{student_id};
+    my $grade           = $student->{grade};
+    my $provided_pass   = $student->{student_password};
+    my $dob             = $student->{student_dob};
+
+    # populate fields 
+    $au->isnew(1);
+    $au->first_given_name($student->{student_fname});
+    $au->second_given_name($student->{student_mname});
+    $au->family_name($student->{student_lname});
+    $au->dob($dob);
+    $au->day_phone($student->{student_phone});
+    $au->email($student->{student_email});
+    $au->passwd($provided_pass ? $provided_pass : calculate_password($dob));
+    $au->guardian($student->{parent_guardian} ? $student->{parent_guardian} : $school->name);
+    $au->ident_type(3); # Other
+    $au->ident_value($student_id);
+    $au->expire_date(calculate_expire_date);
+    $au->juvenile(1);
+    $au->name_keywords(get_name_keywords($grade));
+    $au->net_access_level(1); # Filtered
+    $au->profile($school->eg_perm_group);
+    $au->home_ou($school->home_ou);
+    $au->usrname($district_code.$student_id);
+
+    return $au;
 
-    # record the import in the DB
-    my @file_path = split('/', $csv_file);
-    my $filename = $file_path[-1]; # bare filename
-    try {     
-        my $sth_insert_import = $dbh->prepare($insert_import_sql);
-        #TODO: error handling that gets pushed to the DB
-        $sth_insert_import->execute($district_id, $filename, $error_message);
-        $dbh->commit;
-        $sth_insert_import->finish;
-    } catch {
-        warn "Transaction aborted because $_";
-        eval { $dbh->rollback };
-    };
-    # write exceptions to a file
-    my $exceptions_file = "/tmp/$filename.exceptions";
-    my $exceptions_ref = \@exceptions;
-    csv (in => $exceptions_ref, out => "$exceptions_file")
-        or die Text::CSV->error_diag;
-    print "\n\nImport statistics:\n\n\tImports: $imports\n\tUpdates: $updates\n\tOpt-Outs: $deletes\n\tExceptions: " . scalar(@exceptions) . "\n\n\tCreated exceptions file $exceptions_file.\n";
-    shift @retrieved_files;
 }
 
+sub add_addresses {
+    my ($student, $user) = @_;
+
+    # address fields
+
+    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');
+
+    for my $type ($mailing_addr, $physical_addr) {
+        $type->street1($student->{address_street1});
+        $type->street2($student->{address_street2});
+        $type->city($student->{address_city});
+        $type->county($student->{address_county});
+        $type->state($student->{address_state});
+        $type->post_code($student->{address_postal});
+        $type->country('USA');
+    }
+
+    # Use as both billing and mailing via virtual ID.
+    $physical_addr->id(-1);
+    $mailing_addr->id(-1);
+    $user->billing_address(-1);
+    $user->mailing_address(-1);
+
+    $user->billing_address($physical_addr);
+    $user->mailing_address($mailing_addr);
+    $user->addresses([$physical_addr, $mailing_addr]);
+
+    return 1;
+}
+
+
+sub add_card {
+    my ($student, $user, $prefix) = @_;
+    my $id = $student->{student_id};
+    my $barcode = "$prefix"."$id";
+
+    my $card = Fieldmapper::actor::card->new;
+    $card->id(-1);
+    $card->isnew(1);
+    $card->usr($user->id);
+    $card->barcode($barcode);
+
+    # username defaults to barcode
+    $user->usrname($barcode);
+    $user->card($card);
+    $user->cards([$card]);
+
+    return 1;
+}
+
+
+sub do_login {
+    my $auth = $U->simplereq(
+    'open-ils.auth_internal',
+    'open-ils.auth_internal.session.create',
+    {user_id => 1, org_unit => 394, login_type => 'temp'}
+    );
+    
+    return unless $auth && $auth->{textcode} eq 'SUCCESS';
+
+    my $authtoken = $auth->{payload}->{authtoken};
+    
+    return $authtoken;
+}
+
+
+sub user_exists {
+    my ($student) = @_;
+    my $lname = $student->{lname};
+    my $ident = $student->{student_id};
+    my $user = $editor->search_actor_user({
+        family_name => $lname,
+        ident_value => $ident
+    });
+    return $user if $user;
+    return 0;
+
+}
+
+
+sub save_user {
+    my ($user, $authtoken) = @_;
+
+    print Dumper($user);
+    print $authtoken;
+    
+    my $save = $U->simplereq(
+        'open-ils.actor',
+        'open-ils.actor.patron.update',
+        $authtoken, $user
+    );
+
+    if ($U->is_event($save)) {
+        my $msg = "Unable to save account: " . $save->{textcode};
+        $logger->error("StudentCard: $msg");
+        return 0;
+    }
+    return 1;
+}
+
+
+sub add_exception {
+    my ($student, $error) = @_;
+    $student->{error} = $error;
+    push (@exceptions, $student);
+}
+
+# We've built everything, now get to work!
+
+my $districts = get_active_districts;
+my $authtoken = do_login;
+die "No authtoken available, cannot continue\n" unless $authtoken;
+
+# for each active district,
+for my $district(@$districts) {
+    my $schools = get_schools($district);
+    my $district_name = $district->name;
+    my $district_code = $district->code;
+    
+    my $retrieved_files = get_files($district);
+
+    if (scalar(@$retrieved_files) > 0) {
+        $logger->info("StudentCard: Copying files from $district_name");
+    } else {
+        $logger->info("StudentCard: No new files for $district_name");
+        next;
+    }
+
+    # for each retrieved file for the current district,
+    for my $csv_file (@$retrieved_files) {
+
+        #import the file
+        $logger->info("Importing $csv_file for $district_name");
+        my $student_entries = import_csv($csv_file);
+    
+        #for each student row in the current file,
+        for my $student (@$student_entries) {
+            next unless check_required_fields($student);
+            #my $existing_user = user_exists($student, $district_code); 
+            #$logger->info("CSHARP: \$existing_user = " . Dumper($existing_user));
+            my $school = lookup_school($schools, $student->{school_id});
+            add_exception($student, "No school found with State ID $student->{school_id}") && next unless $school;
+            my $user = make_user($student, $school, $district_code);
+            add_addresses($student, $user);
+            save_user($user, $authtoken);
+            add_card($student, $user, $district_code);
+            save_user($user, $authtoken); # not sure this is right...
+        } # student
+    } # csv_file
+} # district