Bugzilla – Attachment 191469 Details for
Bug 41622
Make patron export use Koha::CSV
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 41622: Make patron export use Koha::CSV
f712a9d.patch (text/plain), 11.72 KB, created by
Tomás Cohen Arazi (tcohen)
on 2026-01-14 20:24:01 UTC
(
hide
)
Description:
Bug 41622: Make patron export use Koha::CSV
Filename:
MIME Type:
Creator:
Tomás Cohen Arazi (tcohen)
Created:
2026-01-14 20:24:01 UTC
Size:
11.72 KB
patch
obsolete
>From f712a9db995196bebaf2904e605850d7c78d4c84 Mon Sep 17 00:00:00 2001 >From: =?UTF-8?q?Tom=C3=A1s=20Cohen=20Arazi?= <tomascohen@theke.io> >Date: Wed, 14 Jan 2026 20:21:58 +0000 >Subject: [PATCH] Bug 41622: Make patron export use Koha::CSV > >This patch introduces Koha::CSV::Patrons and updates misc/export_borrowers.pl >to use it, providing consistent CSV generation with proper quoting and >delimiter handling. > >Changes: >- Add Koha::CSV::Patrons with dynamic field support >- Update misc/export_borrowers.pl to use Koha::CSV::Patrons >- Simplify export logic by removing manual CSV handling >- Maintain all existing functionality (field selection, custom separator) > >The new class supports configurable field lists, making it flexible for >different patron export scenarios while ensuring consistent CSV formatting. > >Test plan: >1. Apply patch >2. Run: > $ ktd --shell > k$ prove t/db_dependent/Koha/CSV/Patrons.t >=> SUCCESS: Tests pass! >3. Test patron export: > k$ misc/export_borrowers.pl --show-header > patrons.csv >=> SUCCESS: CSV generated with all fields >4. Test with specific fields: > k$ misc/export_borrowers.pl -H -f cardnumber -f surname -f firstname > patrons.csv >=> SUCCESS: Only specified fields exported >5. Test with custom separator: > k$ misc/export_borrowers.pl -H -s=";" > patrons.csv >=> SUCCESS: Semicolon separator used >6. Open CSV in Excel >=> SUCCESS: Fields display correctly without quote issues >7. Sign off :-D >--- > Koha/CSV/Patrons.pm | 134 +++++++++++++++++++++++++++++ > misc/export_borrowers.pl | 34 +++----- > t/db_dependent/Koha/CSV/Patrons.t | 138 ++++++++++++++++++++++++++++++ > 3 files changed, 285 insertions(+), 21 deletions(-) > create mode 100644 Koha/CSV/Patrons.pm > create mode 100755 t/db_dependent/Koha/CSV/Patrons.t > >diff --git a/Koha/CSV/Patrons.pm b/Koha/CSV/Patrons.pm >new file mode 100644 >index 0000000000..cf56604d60 >--- /dev/null >+++ b/Koha/CSV/Patrons.pm >@@ -0,0 +1,134 @@ >+package Koha::CSV::Patrons; >+ >+# Copyright 2026 Theke Solutions >+# >+# This file is part of Koha. >+# >+# 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 base 'Koha::CSV'; >+ >+=head1 NAME >+ >+Koha::CSV::Patrons - CSV generator for patron exports >+ >+=head1 SYNOPSIS >+ >+ my $csv = Koha::CSV::Patrons->new({ fields => \@fields }); >+ >+ # Print headers >+ $csv->print_headers($fh); >+ >+ # Print patron rows >+ foreach my $patron (@patrons) { >+ $csv->print_patron($fh, $patron); >+ } >+ >+=head1 DESCRIPTION >+ >+Specialized CSV generator for patron exports with configurable fields. >+ >+=cut >+ >+=head2 new >+ >+ my $csv = Koha::CSV::Patrons->new({ fields => \@fields }); >+ >+Creates a new Koha::CSV::Patrons object. Accepts optional fields parameter >+to specify which patron fields to export. >+ >+=cut >+ >+sub new { >+ my ( $class, $params ) = @_; >+ my $self = $class->SUPER::new($params); >+ >+ $self->{_fields} = $params->{fields} || []; >+ >+ return $self; >+} >+ >+=head2 fields >+ >+ my $fields = $csv->fields(); >+ $csv->fields(\@new_fields); >+ >+Get or set the list of fields to export. >+ >+=cut >+ >+sub fields { >+ my ( $self, $fields ) = @_; >+ >+ if ($fields) { >+ $self->{_fields} = $fields; >+ } >+ >+ return $self->{_fields}; >+} >+ >+=head2 print_headers >+ >+ $csv->print_headers($fh); >+ >+Prints the CSV headers (field names) to a filehandle. >+ >+=cut >+ >+sub print_headers { >+ my ( $self, $fh ) = @_; >+ return $self->print( $fh, $self->{_fields} ); >+} >+ >+=head2 format_patron >+ >+ my $row = $csv->format_patron($patron); >+ >+Formats a Koha::Patron object into an arrayref of CSV fields based on >+the configured field list. >+ >+=cut >+ >+sub format_patron { >+ my ( $self, $patron ) = @_; >+ >+ my $category = $patron->category; >+ my $member = $patron->unblessed; >+ >+ # Add category fields >+ $member->{description} = $category->description; >+ $member->{category_type} = $category->category_type; >+ >+ # Extract requested fields, handling undefined and reference values >+ my @row = map { ( defined $member->{$_} && !ref $member->{$_} ) ? $member->{$_} : '' } @{ $self->{_fields} }; >+ >+ return \@row; >+} >+ >+=head2 print_patron >+ >+ $csv->print_patron($fh, $patron); >+ >+Formats and prints a Koha::Patron to a filehandle as a CSV row. >+ >+=cut >+ >+sub print_patron { >+ my ( $self, $fh, $patron ) = @_; >+ return $self->print( $fh, $self->format_patron($patron) ); >+} >+ >+1; >diff --git a/misc/export_borrowers.pl b/misc/export_borrowers.pl >index 26609adb1f..eb8f14d84e 100755 >--- a/misc/export_borrowers.pl >+++ b/misc/export_borrowers.pl >@@ -20,11 +20,12 @@ > # Script to export borrowers > > use Modern::Perl; >-use Text::CSV; >+ > use Getopt::Long qw( GetOptions :config no_ignore_case ); > > use Koha::Script; > use C4::Context; >+use Koha::CSV::Patrons; > use Koha::Patrons; > > binmode STDOUT, ":encoding(UTF-8)"; >@@ -94,38 +95,29 @@ unless ($separator) { > $separator = C4::Context->csv_delimiter; > } > >-my $csv = Text::CSV->new( { sep_char => $separator, binary => 1, formula => 'empty' } ); >+my $csv = Koha::CSV::Patrons->new( { sep_char => $separator } ); > > # If the user did not specify any field to export, we assume they want them all > # We retrieve the first borrower information to get field names > my ($borrowernumber) = $sth->fetchrow_array or die "No borrower to export"; >-my $patron = Koha::Patrons->find($borrowernumber); # FIXME Now is_expired is no longer available >- # We will have to use Koha::Patron and allow method calls >-my $category = $patron->category; >-my $member = $patron->unblessed; >+my $patron = Koha::Patrons->find($borrowernumber); >+my $category = $patron->category; >+my $member = $patron->unblessed; > $member->{description} = $category->description; > $member->{category_type} = $category->category_type; > > @fields = keys %$member unless (@fields); > >+# Set fields for CSV export >+$csv->fields( \@fields ); >+ > if ($show_header) { >- $csv->combine(@fields); >- print $csv->string . "\n"; >+ $csv->print_headers(*STDOUT); > } > >-$csv->combine( map { ( defined $member->{$_} and !ref $member->{$_} ) ? $member->{$_} : '' } @fields ); >-die "Invalid character at borrower $borrowernumber: [" . $csv->error_input . "]\n" >- if ( !defined( $csv->string ) ); >-print $csv->string . "\n"; >+$csv->print_patron( *STDOUT, $patron ); > > while ( my $borrowernumber = $sth->fetchrow_array ) { >- my $patron = Koha::Patrons->find($borrowernumber); >- my $category = $patron->category; >- my $member = $patron->unblessed; >- $member->{description} = $category->description; >- $member->{category_type} = $category->category_type; >- $csv->combine( map { ( defined $member->{$_} and !ref $member->{$_} ) ? $member->{$_} : '' } @fields ); >- die "Invalid character at borrower $borrowernumber: [" . $csv->error_input . "]\n" >- if ( !defined( $csv->string ) ); >- print $csv->string . "\n"; >+ my $patron = Koha::Patrons->find($borrowernumber); >+ $csv->print_patron( *STDOUT, $patron ); > } >diff --git a/t/db_dependent/Koha/CSV/Patrons.t b/t/db_dependent/Koha/CSV/Patrons.t >new file mode 100755 >index 0000000000..06e918d285 >--- /dev/null >+++ b/t/db_dependent/Koha/CSV/Patrons.t >@@ -0,0 +1,138 @@ >+#!/usr/bin/perl >+ >+# Copyright 2026 Theke Solutions >+# >+# This file is part of Koha. >+# >+# 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 Test::More tests => 7; >+use Test::NoWarnings; >+ >+use t::lib::TestBuilder; >+use t::lib::Mocks; >+ >+BEGIN { >+ use_ok('Koha::CSV::Patrons'); >+} >+ >+my $schema = Koha::Database->new->schema; >+my $builder = t::lib::TestBuilder->new; >+ >+subtest 'new() tests' => sub { >+ plan tests => 2; >+ >+ my $csv = Koha::CSV::Patrons->new(); >+ isa_ok( $csv, 'Koha::CSV::Patrons', 'Object created' ); >+ >+ $csv = Koha::CSV::Patrons->new( { fields => [qw( cardnumber surname firstname )] } ); >+ is_deeply( $csv->fields(), [qw( cardnumber surname firstname )], 'Fields set correctly' ); >+}; >+ >+subtest 'fields() tests' => sub { >+ plan tests => 2; >+ >+ my $csv = Koha::CSV::Patrons->new(); >+ is_deeply( $csv->fields(), [], 'Empty fields by default' ); >+ >+ $csv->fields( [qw( email phone )] ); >+ is_deeply( $csv->fields(), [qw( email phone )], 'Fields updated' ); >+}; >+ >+subtest 'format_patron() tests' => sub { >+ plan tests => 4; >+ >+ $schema->storage->txn_begin; >+ >+ t::lib::Mocks::mock_preference( 'CSVDelimiter', ',' ); >+ >+ my $category = $builder->build_object( { class => 'Koha::Patron::Categories' } ); >+ my $patron = $builder->build_object( >+ { >+ class => 'Koha::Patrons', >+ value => { >+ categorycode => $category->categorycode, >+ surname => 'Test', >+ firstname => 'Patron', >+ email => 'test@example.com', >+ } >+ } >+ ); >+ >+ my $csv = >+ Koha::CSV::Patrons->new( { fields => [qw( cardnumber surname firstname email description category_type )] } ); >+ >+ my $row = $csv->format_patron($patron); >+ >+ isa_ok( $row, 'ARRAY', 'format_patron returns arrayref' ); >+ is( scalar @$row, 6, 'Correct number of fields' ); >+ is( $row->[1], 'Test', 'Surname field correct' ); >+ is( $row->[2], 'Patron', 'Firstname field correct' ); >+ >+ $schema->storage->txn_rollback; >+}; >+ >+subtest 'print_headers() tests' => sub { >+ plan tests => 2; >+ >+ t::lib::Mocks::mock_preference( 'CSVDelimiter', ',' ); >+ >+ my $csv = Koha::CSV::Patrons->new( { fields => [qw( cardnumber surname firstname )] } ); >+ >+ my $output = ''; >+ open my $fh, '>', \$output or die "Cannot open string as file: $!"; >+ >+ my $status = $csv->print_headers($fh); >+ ok( $status, 'print_headers returns true' ); >+ >+ close $fh; >+ >+ is( $output, "cardnumber,surname,firstname\n", 'Headers printed correctly' ); >+}; >+ >+subtest 'print_patron() tests' => sub { >+ plan tests => 2; >+ >+ $schema->storage->txn_begin; >+ >+ t::lib::Mocks::mock_preference( 'CSVDelimiter', ',' ); >+ >+ my $category = $builder->build_object( { class => 'Koha::Patron::Categories' } ); >+ my $patron = $builder->build_object( >+ { >+ class => 'Koha::Patrons', >+ value => { >+ categorycode => $category->categorycode, >+ surname => 'Smith', >+ firstname => 'John', >+ } >+ } >+ ); >+ >+ my $csv = Koha::CSV::Patrons->new( { fields => [qw( surname firstname )] } ); >+ >+ my $output = ''; >+ open my $fh, '>', \$output or die "Cannot open string as file: $!"; >+ >+ my $status = $csv->print_patron( $fh, $patron ); >+ ok( $status, 'print_patron returns true' ); >+ >+ close $fh; >+ >+ like( $output, qr/Smith,John/, 'Patron data printed correctly' ); >+ >+ $schema->storage->txn_rollback; >+}; >-- >2.50.1 (Apple Git-155) >
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 41622
: 191469