From c8750e242c5daeba50c51c2de36017d67a1639a6 Mon Sep 17 00:00:00 2001 From: Jon Knight Date: Tue, 25 Sep 2018 14:00:04 +0000 Subject: [PATCH] Bug 20028: Export all patron related personal data in one package Add a system preference to determine whether patron data export is permitted, and if it is an extra option in the staff UI to initiate the export. At this stage the API for patron data isn't ready so as a placeholder it just goes to the readingrec.pl script. Testing plan: * Apply patch. * Turn the AllowGDPRPatronExport in the administration sysprefs * Search for a patron (eg Mabel in the standard test data) and then click on the "More" button on the right hand side of the button bar. You should see "Export patron's GDPR data" option. Selecting that will generate and download a JSON file containing the patron's data to your client machine. * Turn off AllowGDPRPatronExport in the administration sysprefs * The "Export patron's GDPR data" option should no long be visible from the "More" button in the patron page. --- installer/data/mysql/sysprefs.sql | 1 + .../prog/en/includes/members-toolbar.inc | 3 + .../prog/en/modules/admin/preferences/patrons.pref | 6 + koha-tmpl/intranet-tmpl/prog/js/members-menu.js | 8 + members/readingrec.pl | 252 ++++++++++++++++++++- 5 files changed, 269 insertions(+), 1 deletion(-) diff --git a/installer/data/mysql/sysprefs.sql b/installer/data/mysql/sysprefs.sql index a08ff3e..d67e100 100644 --- a/installer/data/mysql/sysprefs.sql +++ b/installer/data/mysql/sysprefs.sql @@ -17,6 +17,7 @@ INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` ('AllFinesNeedOverride','1','0','If on, staff will be asked to override every fine, even if it is below noissuescharge.','YesNo'), ('AllowAllMessageDeletion','0','','Allow any Library to delete any message','YesNo'), ('AllowFineOverride','0','0','If on, staff will be able to issue books to patrons with fines greater than noissuescharge.','YesNo'), +('AllowGDPRPatronExport','1','','If set all data for a patron can be exported from the staff interface.','YesNo'), ('AllowHoldDateInFuture','0','','If set a date field is displayed on the Hold screen of the Staff Interface, allowing the hold date to be set in the future.','YesNo'), ('AllowHoldItemTypeSelection','0','','If enabled, patrons and staff will be able to select the itemtype when placing a hold','YesNo'), ('AllowHoldPolicyOverride','0',NULL,'Allow staff to override hold policies when placing holds','YesNo'), 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 3f9a0ea..f7113bf 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/includes/members-toolbar.inc +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/members-toolbar.inc @@ -89,6 +89,9 @@
  • Export today's checked in barcodes
  • [% END %] [% END %] + [% IF Koha.Preference('AllowGDPRPatronExport') %] +
  • Export patron's GDPR data
  • + [% END %] 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 8c8227d4..1075b5f 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,6 +1,12 @@ Patrons: General: - + - pref: AllowGDPRPatronExport + choices: + yes: "Allow export" + no: "Don't allow export" + - of patron data for GDPR compliance. + - - pref: AutoEmailOpacUser choices: yes: Send diff --git a/koha-tmpl/intranet-tmpl/prog/js/members-menu.js b/koha-tmpl/intranet-tmpl/prog/js/members-menu.js index ecc22b1..dfff51b 100644 --- a/koha-tmpl/intranet-tmpl/prog/js/members-menu.js +++ b/koha-tmpl/intranet-tmpl/prog/js/members-menu.js @@ -55,6 +55,11 @@ $(document).ready(function(){ $(".btn-group").removeClass("open"); return false; }); + $("#exportgdprdata").click(function(){ + export_gdpr_data(); + $(".btn-group").removeClass("open"); + return false; + }); $("#printsummary").click(function(){ printx_window("page"); $(".btn-group").removeClass("open"); @@ -108,6 +113,9 @@ function confirm_reregistration() { function export_barcodes() { window.open('/cgi-bin/koha/members/readingrec.pl?borrowernumber=' + borrowernumber + '&op=export_barcodes'); } +function export_gdpr_data() { + window.open('/cgi-bin/koha/members/readingrec.pl?borrowernumber=' + borrowernumber + '&op=export_gdprdata'); +} var slip_re = /slip/; function printx_window(print_type) { var handler = print_type.match(slip_re) ? "printslip" : "summary-print"; diff --git a/members/readingrec.pl b/members/readingrec.pl index 4ce115e..ee7e4a0 100755 --- a/members/readingrec.pl +++ b/members/readingrec.pl @@ -30,9 +30,12 @@ use C4::Members; use List::MoreUtils qw/any uniq/; use Koha::DateUtils; use C4::Members::Attributes qw(GetBorrowerAttributes); +use JSON; +use Data::Dumper; use Koha::Patrons; use Koha::Patron::Categories; +use Koha::Account::Lines; my $input = CGI->new; @@ -54,7 +57,6 @@ if ($input->param('borrowernumber')) { my $borrowernumber = $input->param('borrowernumber'); $patron = Koha::Patrons->find( $borrowernumber ); } - my $logged_in_user = Koha::Patrons->find( $loggedinuser ) or die "Not logged in"; output_and_exit_if_error( $input, $cookie, $template, { module => 'members', logged_in_user => $logged_in_user, current_patron => $patron } ); @@ -92,6 +94,254 @@ if ( $op eq 'export_barcodes' ) { } } +if($op eq 'export_gdprdata') { + my $exportedData = { + 'patron' => { + 'title' => $patron->title, + 'surname' => $patron->surname, + 'firstname' => $patron->firstname, + 'othernames' => $patron->othernames, + 'initials' => $patron->initials, + 'streetnumber' => $patron->streetnumber, + 'streettype' => $patron->streettype, + 'address' => $patron->address, + 'address2' => $patron->address2, + 'city' => $patron->city, + 'state' => $patron->state, + 'zipcode' => $patron->zipcode, + 'country' => $patron->country, + 'category_type' => $patron->category->category_type, + 'email' => $patron->email, + 'phone' => $patron->phone, + 'mobile' => $patron->mobile, + 'fax' => $patron->fax, + 'emailpro' => $patron->emailpro, + 'phonepro' => $patron->phonepro, + 'B_streetnumber' => $patron->B_streetnumber, + 'B_streettype' => $patron->B_streettype, + 'B_address' => $patron->B_address, + 'B_address2' => $patron->B_address2, + 'B_city' => $patron->B_city, + 'B_state' => $patron->B_state, + 'B_zipcode' => $patron->B_zipcode, + 'B_country' => $patron->B_country, + 'B_email' => $patron->B_email, + 'B_phone' => $patron->B_phone, + 'dateoffbirth' => $patron->dateofbirth, + 'categorycode' => $patron->categorycode, + 'dateenrolled' => $patron->dateenrolled, + 'dateexpiry' => $patron->dateexpiry, + 'date_renewed' => $patron->date_renewed, + 'gonenoaddress' => $patron->gonenoaddress, + 'lost' => $patron->lost, + 'debarred' => $patron->debarred, + 'debarredcomment' => $patron->debarredcomment, + 'contactname' => $patron->contactname, + 'contactfirstname' => $patron->contactfirstname, + 'contacttitle' => $patron->contacttitle, + 'borrowernotes' => $patron->borrowernotes, + 'sex' => $patron->sex, + 'userid' => $patron->userid, + 'opacnote' => $patron->opacnote, + 'contactnote' => $patron->contactnote, + 'altcontactfirstname' => $patron->altcontactfirstname, + 'altcontactsurname' => $patron->altcontactsurname, + 'altcontactaddress1' => $patron->altcontactaddress1, + 'altcontactaddress2' => $patron->altcontactaddress2, + 'altcontactaddress3' => $patron->altcontactaddress3, + 'altcontactstate' => $patron->altcontactstate, + 'altcontactzipcode' => $patron->altcontactzipcode, + 'altcontactcountry' => $patron->altcontactcountry, + 'altcontactphone' => $patron->altcontactphone, + 'smsalertnumber' => $patron->smsalertnumber, + 'updated_on' => $patron->updated_on, + 'lastseen' => $patron->lastseen, + 'lang' => $patron->lang, + 'login_attempts' => $patron->login_attempts, + 'account_locked' => $patron->account_locked, + 'age' => $patron->get_age, + 'has_overdues' => $patron->has_overdues, + 'borrowernumber' => $patron->borrowernumber, + 'cardnumber' => $patron->cardnumber, + 'branchcode' => $patron->branchcode, + }, + }; + + my $guarantor = $patron->guarantor(); + if($guarantor) { + $exportedData->{'patron'}->{'guarantor'} = + $guarantor->{'borrowernumber'}; + } + my @guarantees = $patron->guarantees(); + if(@guarantees) { + foreach my $bod (@guarantees) { + push @{$exportedData->{'patron'}->{'guarantees'}}, $bod->borrowernumber; + } + } + + my $image = $patron->image; + if($image) { + $exportedData->{'image'} = { + 'mimetype' => $image->mimetype, + 'imagefile' => $image->imagefile, + }; + } + + my $holds = $patron->holds; + if($holds) { + while(my $hold = $holds->next()) { + push @{$exportedData->{'holds'}}, { + 'reserve_id' => $hold->reserve_id, + 'reservedate' => $hold->reservedate, + 'biblionumber' => $hold->biblionumber, + 'branchcode' => $hold->branchcode, + 'nofificationdate' => $hold->notificationdate, + 'reminderdate' => $hold->reminderdate, + 'cancellationdate' => $hold->cancellationdate, + 'reservenotes' => $hold->reservenotes, + 'priority' => $hold->priority, + 'found' => $hold->found, + 'timestamp' => $hold->timestamp, + 'itemnumber' => $hold->itemnumber, + 'waitingdate' => $hold->waitingdate, + 'expirationdate' => $hold->expirationdate, + 'lowestPriority' => $hold->lowestPriority, + 'suspend' => $hold->suspend, + 'suspend_until' => $hold->suspend_until, + 'itemtype' => $hold->itemtype, + } + } + } + + my $oldHolds = $patron->old_holds; + if($oldHolds) { + while(my $hold = $oldHolds->next()) { + push @{$exportedData->{'holds'}}, { + 'reserve_id' => $hold->reserve_id, + 'reservedate' => $hold->reservedate, + 'biblionumber' => $hold->biblionumber, + 'branchcode' => $hold->branchcode, + 'nofificationdate' => $hold->notificationdate, + 'reminderdate' => $hold->reminderdate, + 'cancellationdate' => $hold->cancellationdate, + 'reservenotes' => $hold->reservenotes, + 'priority' => $hold->priority, + 'found' => $hold->found, + 'timestamp' => $hold->timestamp, + 'itemnumber' => $hold->itemnumber, + 'waitingdate' => $hold->waitingdate, + 'expirationdate' => $hold->expirationdate, + 'lowestPriority' => $hold->lowestPriority, + 'suspend' => $hold->suspend, + 'suspend_until' => $hold->suspend_until, + 'itemtype' => $hold->itemtype, + } + } + } + + my $checkouts = $patron->checkouts; + if($checkouts) { + while(my $checkout = $checkouts->next()) { + push @{$exportedData->{'checkouts'}}, { + 'issue_id' => $checkout->issue_id, + 'itemnumber' => $checkout->itemnumber, + 'date_due' => $checkout->date_due, + 'branchcode' => $checkout->branchcode, + 'returndate' => $checkout->returndate, + 'lastreneweddate' => $checkout->lastreneweddate, + 'renewals' =>$checkout->renewals, + 'auto_renew' => $checkout->auto_renew, + 'auto_renew_error' => $checkout->auto_renew_error, + 'timestamp' => $checkout->timestamp, + 'issuedate' => $checkout->issuedate, + 'onsite_checkout' => $checkout->onsite_checkout, + 'note' => $checkout->note, + 'notedate' => $checkout->notedate, + } + } + } + + my $oldCheckouts = $patron->old_checkouts; + if($oldCheckouts) { + while(my $checkout = $oldCheckouts->next()) { + push @{$exportedData->{'checkouts'}}, { + 'issue_id' => $checkout->issue_id, + 'itemnumber' => $checkout->itemnumber, + 'date_due' => $checkout->date_due, + 'branchcode' => $checkout->branchcode, + 'returndate' => $checkout->returndate, + 'lastreneweddate' => $checkout->lastreneweddate, + 'renewals' =>$checkout->renewals, + 'auto_renew' => $checkout->auto_renew, + 'auto_renew_error' => $checkout->auto_renew_error, + 'timestamp' => $checkout->timestamp, + 'issuedate' => $checkout->issuedate, + 'onsite_checkout' => $checkout->onsite_checkout, + 'note' => $checkout->note, + 'notedate' => $checkout->notedate, + } + } + } + + my $clubs = $patron->get_club_enrollments; + if($clubs) { + while(my $club = $clubs->next()) { + push @{$exportedData->{'clubs'}}, { + id => $club->id, + date_enrolled => $club->date_enrolled, + date_canceled => $club->date_canceled, + date_created => $club->date_created, + date_updated => $club->date_updated, + branchcode => $club->branchcode, + } + } + } + + my $account = $patron->account; + if($account) { + $exportedData->{'account'} = { + 'balance' => $account->balance, + 'non_issues_charges' => $account->non_issues_charges, + }; + my $acclines = Koha::Account::Lines->search( + { + borrowernumber => $patron->borrowernumber, + }, + { + order_by => 'accountno' + } + ); + while(my $thisLine = $acclines->next()) { + push @{$exportedData->{'account'}->{'accountLines'}}, { + 'accountlines_id' => $thisLine->accountlines_id, + 'payment_type' => $thisLine->payment_type, + 'amountoutstanding' => $thisLine->amountoutstanding, + 'accounttype' => $thisLine->accounttype, + 'timestamp' => $thisLine->timestamp, + 'issue_id' => $thisLine->issue_id, + 'description' => $thisLine->description, + 'lastincrement' => $thisLine->lastincrement, + 'date' => $thisLine->date, + 'amount' => $thisLine->amount, + 'manager_id' => $thisLine->manager_id, + 'note' => $thisLine->note, + } + } + } + + # Generate the JSON file and spit it out + my $today = output_pref({ dt => dt_from_string, dateformat => 'iso', dateonly => 1 }); + my $borrowercardnumber = $patron->cardnumber; + print $input->header( + -type => 'application/json', + -charset => 'utf-8', + -attachment => "$today-$borrowercardnumber-GDPR-Export.json" + ); + my $json = new JSON; + print $json->pretty->encode($exportedData); + exit; +} + if (! $limit){ $limit = 'full'; } -- 2.1.4