Bugzilla – Attachment 189028 Details for
Bug 34069
Add ability to restore a recently deleted borrower from deletedborrowers
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 34069: Implement tool for restoring deleted patrons
Bug-34069-Implement-tool-for-restoring-deleted-pat.patch (text/plain), 20.91 KB, created by
Lucas Gass (lukeg)
on 2025-11-04 15:36:36 UTC
(
hide
)
Description:
Bug 34069: Implement tool for restoring deleted patrons
Filename:
MIME Type:
Creator:
Lucas Gass (lukeg)
Created:
2025-11-04 15:36:36 UTC
Size:
20.91 KB
patch
obsolete
>From 362e33214e566f568a94c6eb1fe3ef488ba5ff9f Mon Sep 17 00:00:00 2001 >From: Lucas Gass <lucas@bywatersolutions.com> >Date: Wed, 22 Oct 2025 19:13:07 +0000 >Subject: [PATCH] Bug 34069: Implement tool for restoring deleted patrons > >--- > Koha/Old/Patron.pm | 39 +++ > .../tools/restore_deleted_borrowers.tt | 241 ++++++++++++++++++ > .../prog/en/modules/tools/tools-home.tt | 5 + > tools/restore_deleted_borrowers.pl | 154 +++++++++++ > 4 files changed, 439 insertions(+) > create mode 100644 koha-tmpl/intranet-tmpl/prog/en/modules/tools/restore_deleted_borrowers.tt > create mode 100755 tools/restore_deleted_borrowers.pl > >diff --git a/Koha/Old/Patron.pm b/Koha/Old/Patron.pm >index 6e9b899cc6a..88ef74f0057 100644 >--- a/Koha/Old/Patron.pm >+++ b/Koha/Old/Patron.pm >@@ -29,6 +29,45 @@ Koha::Old::Patron - Koha Old::Patron Object class > > =cut > >+=head3 restore_deleted_borrower >+ >+ my $patron = $old_patron->restore_deleted_borrower; >+ >+Restores a deleted patron from the deletedborrowers table back to the borrowers table. >+This only restores the patron account record itself, not any associated historical data. >+The patron will retain their original borrowernumber. >+ >+Returns the restored Koha::Patron object on success. >+Throws an exception on failure. >+ >+=cut >+ >+sub restore_deleted_borrower { >+ my ($self) = @_; >+ >+ my $schema = Koha::Database->new->schema; >+ my $restored_patron; >+ >+ $schema->txn_do( >+ sub { >+ # Retrive all the data about this patron from deleteborrowers table >+ my $patron_data = $self->unblessed; >+ >+ # Create the Koha::Patron object >+ my $patron = Koha::Patron->new($patron_data); >+ >+ # Create the "new" patron using SUPER::store to bypass any Koha::Patron->store() and restore the patron as it was >+ $restored_patron = $patron->SUPER::store(); >+ >+ # Delete the entry from deletedborrowers >+ Koha::Old::Patrons->search( { borrowernumber => $patron_data->{borrowernumber} } )->delete; >+ >+ } >+ ); >+ >+ return $restored_patron; >+} >+ > =head2 Internal methods > > =head3 _type >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/restore_deleted_borrowers.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/restore_deleted_borrowers.tt >new file mode 100644 >index 00000000000..f3b70e98af1 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/restore_deleted_borrowers.tt >@@ -0,0 +1,241 @@ >+[% USE raw %] >+[% USE Asset %] >+[% SET footerjs = 1 %] >+[% USE Branches %] >+[% USE Categories %] >+[% USE KohaDates %] >+[% PROCESS 'html_helpers.inc' %] >+[% PROCESS 'i18n.inc' %] >+[% INCLUDE 'doc-head-open.inc' %] >+[% FILTER collapse %] >+ <title>[% t("Restore deleted patrons") | html %] › [% t("Tools") | html %] › [% t("Koha") | html %]</title> >+[% END %] >+[% INCLUDE 'doc-head-close.inc' %] >+</head> >+ >+<body id="tools_restore_deleted_borrowers" class="tools"> >+[% WRAPPER 'header.inc' %] >+ [% INCLUDE 'patron-search-header.inc' %] >+[% END %] >+[% WRAPPER 'sub-header.inc' %] >+ [% WRAPPER breadcrumbs %] >+ [% WRAPPER breadcrumb_item %] >+ <a href="/cgi-bin/koha/tools/tools-home.pl">Tools</a> >+ [% END %] >+ [% IF ( view == 'restored' ) %] >+ [% WRAPPER breadcrumb_item %] >+ <a href="/cgi-bin/koha/tools/restore_deleted_borrowers.pl">Restore deleted patrons</a> >+ [% END %] >+ [% WRAPPER breadcrumb_item bc_active=1 %] >+ <span>Restored patrons</span> >+ [% END %] >+ [% ELSE %] >+ [% WRAPPER breadcrumb_item bc_active=1 %] >+ <span>Restore deleted patrons</span> >+ [% END %] >+ [% END %] >+ [% END #/ WRAPPER breadcrumbs %] >+[% END #/ WRAPPER sub-header.inc %] >+ >+<div class="main container-fluid"> >+ <div class="row"> >+ <div class="col-sm-10 col-sm-push-2 order-sm-1"> >+ <main> >+ [% IF view == "search" %] >+ <h1>Restore deleted patrons</h1> >+ >+ <div id="search_deleted_borrowers"> >+ <form method="get" action="/cgi-bin/koha/tools/restore_deleted_borrowers.pl" id="search_form"> >+ <fieldset class="rows"> >+ <legend>Search for deleted patrons:</legend> >+ <ol> >+ <li> >+ <label for="cardnumber">Cardnumber:</label> >+ <input type="text" id="cardnumber" name="cardnumber" value="[% cardnumber | html %]" /> >+ </li> >+ <li> >+ <label for="borrowernumber">Borrowernumber:</label> >+ <input type="text" id="borrowernumber" name="borrowernumber" value="[% borrowernumber | html %]" /> >+ </li> >+ <li> >+ <label for="firstname">First name:</label> >+ <input type="text" id="firstname" name="firstname" value="[% firstname | html %]" /> >+ </li> >+ <li> >+ <label for="surname">Surname:</label> >+ <input type="text" id="surname" name="surname" value="[% surname | html %]" /> >+ </li> >+ <li> >+ <label for="categorycode">Patron category:</label> >+ <select name="categorycode" id="categorycode"> >+ <option value="">All categories</option> >+ [% FOREACH cat IN Categories.all() %] >+ <option value="[% cat.categorycode | html %]"> [% cat.description | html %] </option> >+ [% END %] >+ </select> >+ </li> >+ <li> >+ <label for="branchcode">Library:</label> >+ <select name="branchcode" id="branchcode"> >+ <option value="">All libraries</option> >+ [% PROCESS options_for_libraries libraries => Branches.all() %] >+ </select> >+ </li> >+ </ol> >+ </fieldset> >+ <fieldset class="action"> >+ <input type="hidden" name="op" value="search" /> >+ <input type="submit" class="btn btn-primary" value="Search" /> >+ </fieldset> >+ </form> >+ </div> >+ [% ELSIF view == "results" %] >+ <h1>Restore deleted patrons</h1> >+ >+ [% IF deleted_patrons.size > 0 %] >+ <div id="results_wrapper" class="page-section"> >+ <h3>Deleted patrons found: [% deleted_patrons.size | html %]</h3> >+ >+ <form method="post" action="/cgi-bin/koha/tools/restore_deleted_borrowers.pl" id="restore_form"> >+ [% INCLUDE 'csrf-token.inc' %] >+ <input type="hidden" name="op" value="cud-restore" /> >+ >+ <table id="deleted_patrons_table"> >+ <thead> >+ <tr> >+ <th><input type="checkbox" id="select_all" /></th> >+ <th>Card number</th> >+ <th>Borrowernumber</th> >+ <th>Surname</th> >+ <th>First name</th> >+ <th>Category</th> >+ <th>Library</th> >+ <th>Date deleted</th> >+ </tr> >+ </thead> >+ <tbody> >+ [% FOREACH patron IN deleted_patrons %] >+ <tr> >+ <td> >+ <input type="checkbox" name="borrowernumber" value="[% patron.borrowernumber | html %]" /> >+ </td> >+ <td>[% patron.cardnumber | html %]</td> >+ <td>[% patron.borrowernumber | html %]</td> >+ <td>[% patron.surname | html %]</td> >+ <td>[% patron.firstname | html %]</td> >+ <td>[% patron.category.description | html %]</td> >+ <td>[% patron.library.branchname | html %]</td> >+ <td>[% patron.updated_on | $KohaDates %]</td> >+ </tr> >+ [% END %] >+ </tbody> >+ </table> >+ >+ <fieldset class="action"> >+ <input type="submit" class="btn btn-primary" value="Restore selected patrons" id="restore_button" /> >+ </fieldset> >+ </form> >+ </div> >+ [% ELSE %] >+ <div class="dialog message"> No deleted patrons found matching your search criteria. </div> >+ <fieldset class="action"> >+ <a class="btn btn-default" href="/cgi-bin/koha/tools/restore_deleted_borrowers.pl">New search</a> >+ </fieldset> >+ [% END %] >+ [% ELSIF view == "restored" %] >+ <h1>Patrons restored</h1> >+ >+ [% IF restored_patrons.size > 0 %] >+ <div class="dialog message"> >+ <p>Successfully restored [% restored_patrons.size | html %] patron(s).</p> >+ </div> >+ >+ <div id="restored_results" class="page-section"> >+ <table id="restored_patrons_table"> >+ <thead> >+ <tr> >+ <th>Card number</th> >+ <th>Borrowernumber</th> >+ <th>Name</th> >+ <th>Category</th> >+ <th>Library</th> >+ <th>Actions</th> >+ </tr> >+ </thead> >+ <tbody> >+ [% FOREACH patron IN restored_patrons %] >+ <tr> >+ <td>[% patron.cardnumber | html %]</td> >+ <td>[% patron.borrowernumber | html %]</td> >+ <td>[% patron.firstname | html %] [% patron.surname | html %]</td> >+ <td>[% patron.category.description | html %]</td> >+ <td>[% patron.library.branchname | html %]</td> >+ <td> >+ <a href="/cgi-bin/koha/members/moremember.pl?borrowernumber=[% patron.borrowernumber | uri %]" class="btn btn-default btn-xs"> <i class="fa fa-user"></i> View patron </a> >+ </td> >+ </tr> >+ [% END %] >+ </tbody> >+ </table> >+ </div> >+ [% END %] >+ >+ [% IF errors.size > 0 %] >+ <div class="dialog alert"> >+ <h3>Errors occurred:</h3> >+ <ul> >+ [% FOREACH error IN errors %] >+ <li>[% error | html %]</li> >+ [% END %] >+ </ul> >+ </div> >+ [% END %] >+ >+ <fieldset class="action"> >+ <a class="btn btn-default" href="/cgi-bin/koha/tools/restore_deleted_borrowers.pl">Restore more patrons</a> >+ </fieldset> >+ [% END %] >+ </main> >+ </div> >+ <!-- /.col-sm-10.col-sm-push-2 --> >+ >+ <div class="col-sm-2 col-sm-pull-10"> >+ <aside> [% INCLUDE 'tools-menu.inc' %] </aside> >+ </div> >+ <!-- /.col-sm-2.col-sm-pull-10 --> >+ </div> >+ <!-- /.row --> >+</div> >+<!-- /.main.container-fluid --> >+ >+[% MACRO jsinclude BLOCK %] >+ [% Asset.js("js/tools-menu.js") | $raw %] >+ [% INCLUDE 'datatables.inc' %] >+ <script> >+ $(document).ready(function() { >+ // Select all checkbox functionality >+ $("#select_all").on("click", function() { >+ $("#deleted_patrons_table tbody input[type='checkbox']").prop('checked', this.checked); >+ }); >+ >+ // Initialize #deleted_patrons_table table >+ [% IF view == "results" && deleted_patrons.size > 0 %] >+ $("#deleted_patrons_table").kohaTable({ >+ "paging": true, >+ "order": [[1, 'asc']] >+ }); >+ [% END %] >+ >+ // Initialize #restored_patrons_table table >+ [% IF view == "restored" && restored_patrons.size > 0 %] >+ $("#restored_patrons_table").kohaTable({ >+ "paging": true, >+ "order": [[1, 'asc']] >+ }); >+ [% END %] >+ >+ }); >+ </script> >+[% END %] >+ >+[% INCLUDE 'intranet-bottom.inc' %] >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/tools-home.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/tools-home.tt >index 6f12d5fc69a..8219370ca09 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/tools-home.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/tools-home.tt >@@ -43,6 +43,11 @@ > <dd>Manage patron clubs</dd> > [% END %] > >+ [% IF (CAN_user_borrowers_restore_deleted_borrowers) %] >+ <dt><a href="/cgi-bin/koha/tools/restore_deleted_borrowers.pl">Restore deleted patrons</a></dt> >+ <dd>Restore deleted patrons from the deletedborrowers table</dd> >+ [% END %] >+ > [% IF ( CAN_user_tools_moderate_comments ) %] > <dt> > <a href="/cgi-bin/koha/reviews/reviewswaiting.pl">Comments</a> >diff --git a/tools/restore_deleted_borrowers.pl b/tools/restore_deleted_borrowers.pl >new file mode 100755 >index 00000000000..1a8e1b675f6 >--- /dev/null >+++ b/tools/restore_deleted_borrowers.pl >@@ -0,0 +1,154 @@ >+#!/usr/bin/perl >+ >+# This file is part of Koha. >+# >+# Copyright 2024 Koha Development Team >+# >+# 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 >+# <https://www.gnu.org/licenses> >+ >+use Modern::Perl; >+use CGI qw ( -utf8 ); >+use Try::Tiny; >+use Scalar::Util qw( blessed ); >+ >+use C4::Auth qw( get_template_and_user ); >+use C4::Output qw( output_html_with_http_headers ); >+use C4::Context; >+ >+use Koha::Old::Patrons; >+use Koha::Patron::Categories; >+use Koha::Libraries; >+ >+my $input = CGI->new; >+my $op = $input->param('op') || 'search'; >+ >+my ( $template, $loggedinuser, $cookie ) = get_template_and_user( >+ { >+ template_name => "tools/restore_deleted_borrowers.tt", >+ query => $input, >+ type => "intranet", >+ flagsrequired => { tools => 'restore_deleted_patrons' }, >+ } >+); >+ >+if ( $op eq 'search' ) { >+ >+ # Get search parameters >+ my $cardnumber = $input->param('cardnumber'); >+ my $borrowernumber = $input->param('borrowernumber'); >+ my $surname = $input->param('surname'); >+ my $firstname = $input->param('firstname'); >+ my $email = $input->param('email'); >+ my $categorycode = $input->param('categorycode'); >+ my $branchcode = $input->param('branchcode'); >+ >+ # No empty searches, if no search critrea is added, do nothing. >+ if ( $cardnumber || $borrowernumber || $surname || $firstname || $email || $categorycode || $branchcode ) { >+ >+ # empty search parameters >+ my %search_params; >+ >+ # these params should be exact matches >+ $search_params{cardnumber} = $cardnumber if $cardnumber; >+ $search_params{borrowernumber} = $borrowernumber if $borrowernumber; >+ $search_params{categorycode} = $categorycode if $categorycode; >+ $search_params{branchcode} = $branchcode if $branchcode; >+ >+ # these params don't necessarily have to be exact matches >+ $search_params{surname} = { 'like' => "%$surname%" } if $surname; >+ $search_params{firstname} = { 'like' => "%$firstname%" } if $firstname; >+ $search_params{email} = { 'like' => "%$email%" } if $email; >+ >+ my $deleted_patrons_rs = Koha::Old::Patrons->search( >+ \%search_params, >+ { >+ order_by => { -desc => 'updated_on' }, >+ rows => 100, >+ } >+ ); >+ >+ my @deleted_patrons; >+ >+ while ( my $patron = $deleted_patrons_rs->next ) { >+ >+ #collect patron data to use in the results table >+ my $patron_data = { >+ borrowernumber => $patron->borrowernumber, >+ cardnumber => $patron->cardnumber, >+ surname => $patron->surname, >+ firstname => $patron->firstname, >+ email => $patron->email, >+ branchcode => $patron->branchcode, >+ updated_on => $patron->updated_on, >+ }; >+ >+ push @deleted_patrons, $patron_data; >+ } >+ >+ $template->param( >+ view => 'results', >+ deleted_patrons => \@deleted_patrons, >+ cardnumber => $cardnumber, >+ borrowernumber => $borrowernumber, >+ surname => $surname, >+ firstname => $firstname, >+ email => $email, >+ ); >+ } else { >+ $template->param( view => 'search' ); >+ } >+ >+} elsif ( $op eq 'cud-restore' ) { >+ >+ my @borrowernumbers = $input->multi_param('borrowernumber'); >+ >+ my @restored_patrons; >+ my @errors; >+ >+ foreach my $borrowernumber (@borrowernumbers) { >+ try { >+ # Find the deleted patron >+ my $deleted_patron = Koha::Old::Patrons->search( { borrowernumber => $borrowernumber } )->next; >+ >+ unless ($deleted_patron) { >+ push @errors, "Borrowernumber $borrowernumber not found in deleted patrons"; >+ next; >+ } >+ >+ # Attempt to restore >+ my $restored_patron = $deleted_patron->restore_deleted_borrower; >+ >+ push @restored_patrons, { >+ borrowernumber => $restored_patron->borrowernumber, >+ cardnumber => $restored_patron->cardnumber, >+ firstname => $restored_patron->firstname, >+ surname => $restored_patron->surname, >+ }; >+ } catch { >+ push @errors, "Error restoring borrower $borrowernumber: $_"; >+ }; >+ } >+ >+ $template->param( >+ view => 'restored', >+ restored_patrons => \@restored_patrons, >+ errors => \@errors, >+ ); >+ >+} else { >+ $template->param( view => 'search' ); >+} >+ >+output_html_with_http_headers $input, $cookie, $template->output; >-- >2.39.5
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
|
Splinter Review
Attachments on
bug 34069
:
188299
|
188302
|
188988
|
188989
|
188990
|
189027
|
189028
|
189029
|
189030
|
189037
|
189066
|
189067
|
189370
|
189371
|
189372
|
189373
|
189374
|
189375
|
189376
|
189377
|
189642
|
189645
|
189718
|
189773
|
189774
|
189779
|
189780
|
189781
|
189782
|
189783
|
189784
|
189785
|
189786
|
189787
|
189788
|
189789
|
189790
|
189791
|
189792
|
189793
|
189794
|
189795
|
189940
|
189941
|
189942
|
189943
|
189944
|
189945
|
189946
|
189947
|
189948
|
189949
|
189950
|
189951
|
189952
|
189953
|
189954