Browse Source

Bug 15836: Add the ability to defined custom methods to split call number in labels

Currently the call number splitting seems to be mostly implemented for
DDC and LC classifications.
Those are both not very common in some countries.
A lot of libraries use their own custom classification schemes so the call number
plitting is something that should be individually configurable.

This enhancement adds the ability to define custom splitting rules based
on regular expressions.

How does it work so far?
From C4/Labels/Label.pm there are 3 differents splitting methods defined, depending on items.cn_source.
  if cn_source is "lcc' or 'nlm' we split using Library::CallNumber::LC
  if cn_source is 'ddc' we split using a in-house method
Finally there is a fallback method to split on space
And nothing else is done for other cn_source

The idea of this patch is to mimick what was done for the "filing rules" and add
the ability to define "splitting rules" that will be used by the "Classification sources".
A classification source will then have:
  * a filing rule used to sort items by callnumbers
  * a splitting rule used to print labels

To acchieve this goal this enhancement will do the following
modifications at DB level:
* New table class_split_rules
* New column class_sources.class_split_rule

Test plan:
* Execute the update database entry to create the new table and
column.
I. UI Changes
a) Create/modify/delete a filing rule
b) Create/modify/delete a splitting rule
c) Create/modify/delete a classification source
=> A filing rule or splitting rule cannot be removed if used by a
classification source

II. Splitting rule using regular expressions
a) Create a splitting rule using the "Splitting routine" "RegEx"
b) Define several regular expressions, they will be applied one after
the other in the same order you define them.
Something like:
  s/\s/\n/g         # Break on spaces
  s/(\s?=)/\n=/g    # Break on = (unless it's done already)
  s/^(J|K)\n/$1 /   # Remove the first break if callnumber starts with J or K
c) You can test the regular expressions using filling the textarea with
a list of callnumbers. Then click "Test" and confirm the callnumbers are
split how you expected.
d) Finally create a new classification source that will use this new
splitting rule.

III. Print the label!
a) Create a layout. It should have the "Split call numbers" checkbox
ticked, and display itemcallnumber
b) Use this layout to export labels, use items with different
classification source ('lcc', 'ddc', but also the new one you have
create)
=> The callnumbers should have been split according to the regex you
defined earlier!

Notes:
* The update database entry fill the class_sources.class_split_rule
with the value of class_sources.class_sort_rule
If default rules exist it will not work, we should add a note in the
release notes (would be enough?)
* C4::ClassSplitRoutine::* should be moved to Koha::ClassSplitRule,
but it sounded better to keep the same pattern as ClassSortRoutines
* Should not we use a LONGTEXT for class_split_rules.split_regex instead
of VARCHAR(255)?

* class_sources.sql should be filled for other languages before pushed
to master!

IMPORTANT NOTES: The regular expressions are stored as it, and eval is
used to evaluate it (perlcritic raises a warning about it (Expression
form of "eval"). It can lead to serious security issues (execution of
arbitrary code on the server), especially if the modifier 'e' is used.
We could then remedy the situation with one of these following points:
- Assume that this DB data is safe (We can add a new permission?)
- Assume that the data is not safe and deal with possible attack
Cons: how be sure we are exhaustive? Making sure it matches ^s///[^e/]*$
would be enough?
- Use Template Toolkit syntax instead (Really safer?)
  [% callnumber.replace('\s', '\n').replace ... %]
- Cut the regex parts: find, replace, modifiers
like we already do for Marc modification template. Cons: we are going to
have escape problems, the "find" and "replace" parts should not be
handle the same way (think "\n", "\\n", "\1", "\s", etc.)
I did not manage to implement this one easily.

Sponsored-by: Goethe-Institut

Signed-off-by: Christian Stelzenmüller <christian.stelzenmueller@bsz-bw.de>
Signed-off-by: Chris Cormack <chrisc@catalyst.net.nz>

Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
18.11.x
Jonathan Druart 6 years ago
committed by Nick Clemens
parent
commit
65c4d8019e
  1. 63
      C4/ClassSplitRoutine.pm
  2. 79
      C4/ClassSplitRoutine/Dewey.pm
  3. 72
      C4/ClassSplitRoutine/Generic.pm
  4. 63
      C4/ClassSplitRoutine/LCC.pm
  5. 59
      C4/ClassSplitRoutine/RegEx.pm
  6. 100
      C4/Labels/Label.pm
  7. 78
      Koha/ClassSplitRule.pm
  8. 56
      Koha/ClassSplitRules.pm
  9. 23
      admin/classsources.pl
  10. 2
      koha-tmpl/intranet-tmpl/prog/en/modules/admin/admin-home.tt
  11. 90
      koha-tmpl/intranet-tmpl/prog/en/modules/admin/classsources.tt
  12. 3
      t/Labels.t
  13. 38
      t/Labels_split_Regex.t
  14. 5
      t/Labels_split_ddcn.t
  15. 3
      t/Labels_split_lccn.t

63
C4/ClassSplitRoutine.pm

@ -0,0 +1,63 @@
package C4::ClassSplitRoutine;
# Copyright 2018 Koha Development Team
#
# 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 <http://www.gnu.org/licenses>.
use Modern::Perl;
require Exporter;
use Class::Factory::Util;
use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
=head1 NAME
C4::ClassSplitRoutine - base object for creation of classification splitting routines
=head1 SYNOPSIS
use C4::ClassSplitRoutine;
=head1 FUNCTIONS
=cut
@ISA = qw(Exporter);
@EXPORT = qw(
&GetSplitRoutineNames
);
=head2 GetSplitRoutineNames
my @routines = GetSplitRoutineNames();
Get names of all modules under C4::ClassSplitRoutine::*.
=cut
sub GetSplitRoutineNames {
return C4::ClassSplitRoutine->subclasses();
}
1;
=head1 AUTHOR
Koha Development Team <http://koha-community.org/>
=cut

79
C4/ClassSplitRoutine/Dewey.pm

@ -0,0 +1,79 @@
package C4::ClassSplitRoutine::Dewey;
# Copyright 2018 Koha Development Team
#
# 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 <http://www.gnu.org/licenses>.
use Modern::Perl;
use C4::Debug;
=head1 NAME
C4::ClassSplitRoutine::Dewey - Dewey call number split method
=head1 SYNOPSIS
use C4::ClassSplitRoutine;
my $cn_split = C4::ClassSplitRoutine::Dewey::split_callnumber($cn_item);
=head1 FUNCTIONS
=head2 split_callnumber
my $cn_split = C4::ClassSplitRoutine::Dewey::split_callnumber($cn_item);
=cut
sub split_callnumber {
my ($cn_item) = @_;
my $possible_decimal =
qr/\d{3,}(?:\.\d+)?/; # at least three digits for a DDCN
$cn_item =~ s/\///g
; # in theory we should be able to simply remove all segmentation markers and arrive at the correct call number...
my (@lines) = $cn_item =~ m/
^([-a-zA-Z]*\s?(?:$possible_decimal)?) # R220.3 CD-ROM 787.87 # will require extra splitting
\s+
(.+) # H2793Z H32 c.2 EAS # everything else (except bracketing spaces)
\s*
/x;
unless (@lines) {
warn sprintf( 'regexp failed to match string: %s', $cn_item );
push @lines, $cn_item; # if no match, just push the whole string.
}
if ( $lines[0] =~ /^([-a-zA-Z]+)\s?($possible_decimal)$/ ) {
shift @lines; # pull off the mathching first element, like example 1
unshift @lines, $1, $2; # replace it with the two pieces
}
push @lines, split /\s+/,
pop @lines
; # split the last piece into an arbitrary number of pieces at spaces
$debug and print STDERR "split_ddcn array: ", join( " | ", @lines ), "\n";
return @lines;
}
1;
=head1 AUTHOR
Koha Development Team <http://koha-community.org/>
=cut

72
C4/ClassSplitRoutine/Generic.pm

@ -0,0 +1,72 @@
package C4::ClassSplitRoutine::Generic;
# Copyright 2018 Koha Development Team
#
# 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 <http://www.gnu.org/licenses>.
use Modern::Perl;
use C4::Debug;
=head1 NAME
C4::ClassSplitRoutine::Generic - generic call number sorting key routine
=head1 SYNOPSIS
use C4::ClassSplitRoutine;
=head1 FUNCTIONS
=head2 split_callnumber
my $cn_split = C4::ClassSplitRoutine::Generic::split_callnumber($cn_item);
NOTE: Custom call number types go here. It may be necessary to create additional
splitting algorithms if some custom call numbers cannot be made to work here.
Presently this splits standard non-ddcn, non-lccn fiction and biography call numbers.
=cut
sub split_callnumber {
my ($cn_item) = @_;
my @lines;
# Split call numbers based on spaces
push @lines, split /\s+/, $cn_item
; # split the call number into an arbitrary number of pieces at spaces
if ( $lines[-1] !~ /^.*\d-\d.*$/ && $lines[-1] =~ /^(.*\d+)(\D.*)$/ ) {
pop @lines; # pull off the matching last element
push @lines, $1, $2; # replace it with the two pieces
}
unless ( scalar @lines ) {
warn sprintf( 'regexp failed to match string: %s', $cn_item );
push( @lines, $cn_item );
}
$debug and print STDERR "split_ccn array: ", join( " | ", @lines ), "\n";
return @lines;
}
1;
=head1 AUTHOR
Koha Development Team <http://koha-community.org/>
=cut

63
C4/ClassSplitRoutine/LCC.pm

@ -0,0 +1,63 @@
package C4::ClassSplitRoutine::LCC;
# Copyright 2018 Koha Development Team
#
# 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 <http://www.gnu.org/licenses>.
use Modern::Perl;
use Library::CallNumber::LC;
use C4::Debug;
=head1 NAME
C4::ClassSplitRoutine::LCC - LCC call number split method
=head1 SYNOPSIS
use C4::ClassSplitRoutine;
my $cn_split = C4::ClassSplitRoutine::LCC::split_callnumber($cn_item);
=head1 FUNCTIONS
=head2 split_callnumber
my $cn_split = C4::ClassSplitRoutine::LCC::split_callnumber($cn_item);
=cut
sub split_callnumber {
my ($cn_item) = @_;
# lccn examples: 'HE8700.7 .P6T44 1983', 'BS2545.E8 H39 1996';
my @lines = Library::CallNumber::LC->new($cn_item)->components();
unless (scalar @lines && defined $lines[0]) {
$debug and warn sprintf('regexp failed to match string: %s', $cn_item);
@lines = $cn_item; # if no match, just use the whole string.
}
my $LastPiece = pop @lines;
push @lines, split /\s+/, $LastPiece if $LastPiece; # split the last piece into an arbitrary number of pieces at spaces
$debug and warn "split LCC array: ", join(" | ", @lines), "\n";
return @lines;
}
1;
=head1 AUTHOR
Koha Development Team <http://koha-community.org/>
=cut

59
C4/ClassSplitRoutine/RegEx.pm

@ -0,0 +1,59 @@
package C4::ClassSplitRoutine::RegEx;
# Copyright 2018 Koha Development Team
#
# 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 <http://www.gnu.org/licenses>.
use Modern::Perl;
use C4::Debug;
=head1 NAME
C4::ClassSplitRoutine::RegEx - regex call number sorting key routine
=head1 SYNOPSIS
use C4::ClassSplitRoutine;
my $cn_sort = C4::ClassSplitRoutine::RegEx::split_callnumber($cn_item, $regexs);
=head1 FUNCTIONS
=head2 split_callnumber
my $cn_split = C4::ClassSplitRoutine::RegEx::split_callnumber($cn_item, $regexs);
=cut
sub split_callnumber {
my ($cn_item, $regexs) = @_;
for my $regex ( @$regexs ) {
eval "\$cn_item =~ $regex";
}
my @lines = split "\n", $cn_item;
return @lines;
}
1;
=head1 AUTHOR
Koha Development Team <http://koha-community.org/>
=cut

100
C4/Labels/Label.pm

@ -7,15 +7,18 @@ use Text::Wrap;
use Algorithm::CheckDigits; use Algorithm::CheckDigits;
use Text::CSV_XS; use Text::CSV_XS;
use Data::Dumper; use Data::Dumper;
use Library::CallNumber::LC;
use Text::Bidi qw( log2vis ); use Text::Bidi qw( log2vis );
use C4::Context; use C4::Context;
use C4::Debug; use C4::Debug;
use C4::Biblio; use C4::Biblio;
use Koha::ClassSources;
use Koha::ClassSortRules;
my $possible_decimal = qr/\d{3,}(?:\.\d+)?/; # at least three digits for a DDCN use Koha::ClassSplitRules;
use C4::ClassSplitRoutine::Dewey;
use C4::ClassSplitRoutine::LCC;
use C4::ClassSplitRoutine::Generic;
use C4::ClassSplitRoutine::RegEx;
sub _check_params { sub _check_params {
my $given_params = {}; my $given_params = {};
@ -121,67 +124,6 @@ sub _get_text_fields {
return \@sorted_fields; return \@sorted_fields;
} }
sub _split_lccn {
my ($lccn) = @_;
$_ = $lccn;
# lccn examples: 'HE8700.7 .P6T44 1983', 'BS2545.E8 H39 1996';
my @parts = Library::CallNumber::LC->new($lccn)->components();
unless (scalar @parts && defined $parts[0]) {
$debug and warn sprintf('regexp failed to match string: %s', $_);
@parts = $_; # if no match, just use the whole string.
}
my $LastPiece = pop @parts;
push @parts, split /\s+/, $LastPiece if $LastPiece; # split the last piece into an arbitrary number of pieces at spaces
$debug and warn "split_lccn array: ", join(" | ", @parts), "\n";
return @parts;
}
sub _split_ddcn {
my ($ddcn) = @_;
$_ = $ddcn;
s/\///g; # in theory we should be able to simply remove all segmentation markers and arrive at the correct call number...
my (@parts) = m/
^([-a-zA-Z]*\s?(?:$possible_decimal)?) # R220.3 CD-ROM 787.87 # will require extra splitting
\s+
(.+) # H2793Z H32 c.2 EAS # everything else (except bracketing spaces)
\s*
/x;
unless (scalar @parts) {
warn sprintf('regexp failed to match string: %s', $_);
push @parts, $_; # if no match, just push the whole string.
}
if ($parts[0] =~ /^([-a-zA-Z]+)\s?($possible_decimal)$/) {
shift @parts; # pull off the mathching first element, like example 1
unshift @parts, $1, $2; # replace it with the two pieces
}
push @parts, split /\s+/, pop @parts; # split the last piece into an arbitrary number of pieces at spaces
$debug and print STDERR "split_ddcn array: ", join(" | ", @parts), "\n";
return @parts;
}
## NOTE: Custom call number types go here. It may be necessary to create additional splitting algorithms if some custom call numbers
## cannot be made to work here. Presently this splits standard non-ddcn, non-lccn fiction and biography call numbers.
sub _split_ccn {
my ($fcn) = @_;
my @parts = ();
# Split call numbers based on spaces
push @parts, split /\s+/, $fcn; # split the call number into an arbitrary number of pieces at spaces
if ($parts[-1] !~ /^.*\d-\d.*$/ && $parts[-1] =~ /^(.*\d+)(\D.*)$/) {
pop @parts; # pull off the matching last element
push @parts, $1, $2; # replace it with the two pieces
}
unless (scalar @parts) {
warn sprintf('regexp failed to match string: %s', $_);
push (@parts, $_);
}
$debug and print STDERR "split_ccn array: ", join(" | ", @parts), "\n";
return @parts;
}
sub _get_barcode_data { sub _get_barcode_data {
my ( $f, $item, $record ) = @_; my ( $f, $item, $record ) = @_;
my $kohatables = _desc_koha_tables(); my $kohatables = _desc_koha_tables();
@ -400,6 +342,14 @@ sub draw_label_text {
# FIXME - returns all items, so you can't get data from an embedded holdings field. # FIXME - returns all items, so you can't get data from an embedded holdings field.
# TODO - add a GetMarcBiblio1item(bibnum,itemnum) or a GetMarcItem(itemnum). # TODO - add a GetMarcBiblio1item(bibnum,itemnum) or a GetMarcItem(itemnum).
my $cn_source = ($item->{'cn_source'} ? $item->{'cn_source'} : C4::Context->preference('DefaultClassificationSource')); my $cn_source = ($item->{'cn_source'} ? $item->{'cn_source'} : C4::Context->preference('DefaultClassificationSource'));
my $class_source = Koha::ClassSources->find( $cn_source );
my ( $split_routine, $regexs );
if ($class_source) {
my $class_split_rule = Koha::ClassSplitRules->find( $class_source->class_split_rule );
$split_routine = $class_split_rule->split_routine;
$regexs = $class_split_rule->regexs;
}
else { $split_routine = $cn_source }
LABEL_FIELDS: # process data for requested fields on current label LABEL_FIELDS: # process data for requested fields on current label
for my $field (@$label_fields) { for my $field (@$label_fields) {
if ($field->{'code'} eq 'itemtype') { if ($field->{'code'} eq 'itemtype') {
@ -429,14 +379,18 @@ sub draw_label_text {
# Fields which hold call number data FIXME: ( 060? 090? 092? 099? ) # Fields which hold call number data FIXME: ( 060? 090? 092? 099? )
my @callnumber_list = qw(itemcallnumber 050a 050b 082a 952o 995k); my @callnumber_list = qw(itemcallnumber 050a 050b 082a 952o 995k);
if ((grep {$field->{'code'} =~ m/$_/} @callnumber_list) and ($self->{'printing_type'} eq 'BIB') and ($self->{'callnum_split'})) { # If the field contains the call number, we do some sp if ((grep {$field->{'code'} =~ m/$_/} @callnumber_list) and ($self->{'printing_type'} eq 'BIB') and ($self->{'callnum_split'})) { # If the field contains the call number, we do some sp
if ($cn_source eq 'lcc' || $cn_source eq 'nlm') { # NLM and LCC should be split the same way if ($split_routine eq 'LCC' || $split_routine eq 'nlm') { # NLM and LCC should be split the same way
@label_lines = _split_lccn($field_data); @label_lines = C4::ClassSplitRoutine::LCC::split_callnumber($field_data);
@label_lines = _split_ccn($field_data) if !@label_lines; # If it was not a true lccn, try it as a custom call number @label_lines = C4::ClassSplitRoutine::Generic::split_callnumber($field_data) unless @label_lines; # If it was not a true lccn, try it as a custom call number
push (@label_lines, $field_data) if !@label_lines; # If it was not that, send it on unsplit push (@label_lines, $field_data) unless @label_lines; # If it was not that, send it on unsplit
} elsif ($cn_source eq 'ddc') { } elsif ($split_routine eq 'Dewey') {
@label_lines = _split_ddcn($field_data); @label_lines = C4::ClassSplitRoutine::Dewey::split_callnumber($field_data);
@label_lines = _split_ccn($field_data) if !@label_lines; @label_lines = C4::ClassSplitRoutine::Generic::split_callnumber($field_data) unless @label_lines;
push (@label_lines, $field_data) if !@label_lines; push (@label_lines, $field_data) unless @label_lines;
} elsif ($split_routine eq 'RegEx' ) {
@label_lines = C4::ClassSplitRoutine::RegEx::split_callnumber($field_data, $regexs);
@label_lines = C4::ClassSplitRoutine::Generic::split_callnumber($field_data) unless @label_lines;
push (@label_lines, $field_data) unless @label_lines;
} else { } else {
warn sprintf('Call number splitting failed for: %s. Please add this call number to bug #2500 at bugs.koha-community.org', $field_data); warn sprintf('Call number splitting failed for: %s. Please add this call number to bug #2500 at bugs.koha-community.org', $field_data);
push @label_lines, $field_data; push @label_lines, $field_data;

78
Koha/ClassSplitRule.pm

@ -0,0 +1,78 @@
package Koha::ClassSplitRule;
# 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, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
use Modern::Perl;
use JSON qw(to_json from_json);
use Koha::Database;
use base qw(Koha::Object);
=head1 NAME
Koha::ClassSplitRule Koha Classfication Spliting Rule Object class
=head1 API
=head2 Class Methods
=cut
=head3 new
Accept 'regexs' as a valid attribute.
It should be an arrayref that will be serialized in JSON before stored in DB.
=cut
sub new {
my ($class, $attributes) = @_;
if ( exists $attributes->{regexs} ) {
$attributes->{split_regex} = to_json($attributes->{regexs});
delete $attributes->{regexs};
}
return $class->SUPER::new($attributes);
}
=head3 regexs
my $regexs = $rule->regexs
$rule->regex(\@regexs);
Getter or setter for split_regex
=cut
sub regexs {
my ( $self, $regexs ) = @_;
return $regexs
? $self->split_regex( to_json($regexs) )
: from_json( $self->split_regex );
}
=head3 type
=cut
sub _type {
return 'ClassSplitRule';
}
1;

56
Koha/ClassSplitRules.pm

@ -0,0 +1,56 @@
package Koha::ClassSplitRules;
# 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, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
use Modern::Perl;
use Koha::Database;
use Koha::ClassSplitRule;
use base qw(Koha::Objects);
=head1 NAME
Koha::ClassSplitRules - Koha Classification Split Rules Object set class
=head1 API
=head2 Class Methods
=cut
=head3 _type
Returns name of corresponding DBIC resultset
=cut
sub _type {
return 'ClassSplitRule';
}
=head3 object_class
Returns name of corresponding Koha object class
=cut
sub object_class {
return 'Koha::ClassSplitRule';
}
1;

23
admin/classsources.pl

@ -27,6 +27,7 @@ use C4::Output;
use C4::Koha; use C4::Koha;
use C4::ClassSource; use C4::ClassSource;
use C4::ClassSortRoutine; use C4::ClassSortRoutine;
use C4::ClassSplitRoutine;
use Koha::ClassSources; use Koha::ClassSources;
use Koha::ClassSortRules; use Koha::ClassSortRules;
use Koha::ClassSplitRules; use Koha::ClassSplitRules;
@ -40,7 +41,7 @@ my $class_sort_rule = $input->param('class_sort_rule');
my $class_split_rule = $input->param('class_split_rule'); my $class_split_rule = $input->param('class_split_rule');
my $sort_routine = $input->param('sort_routine'); my $sort_routine = $input->param('sort_routine');
my $split_routine = $input->param('split_routine'); my $split_routine = $input->param('split_routine');
my $split_regex = $input->param('split_regex'); my @split_regex = $input->multi_param('split_regex');
my $description = $input->param('description'); my $description = $input->param('description');
my $used = $input->param('used'); my $used = $input->param('used');
@ -194,16 +195,20 @@ elsif ( $op eq "add_split_rule" ) {
} }
elsif ( $op eq "add_split_rule_validate" ) { elsif ( $op eq "add_split_rule_validate" ) {
my $split_rule = Koha::ClassSplitRules->find($class_split_rule); my $split_rule = Koha::ClassSplitRules->find($class_split_rule);
@split_regex = grep {!/^$/} @split_regex; # Remove empty
if ($split_rule) { if ($split_rule) {
$split_rule->set( $split_rule->set(
{ {
description => $description, description => $description,
split_routine => $split_routine, split_routine => $split_routine,
split_regex =>
( $split_routine eq 'RegEx' ? $split_regex : '' ),
} }
); );
eval { $split_rule->store; }; eval {
$split_rule->regexs(\@split_regex)
if $split_routine eq 'RegEx';
$split_rule->store;
};
if ($@) { if ($@) {
push @messages, push @messages,
{ type => 'error', code => 'error_on_update_split_rule' }; { type => 'error', code => 'error_on_update_split_rule' };
@ -220,8 +225,7 @@ elsif ( $op eq "add_split_rule_validate" ) {
class_split_rule => $class_split_rule, class_split_rule => $class_split_rule,
description => $description, description => $description,
split_routine => $split_routine, split_routine => $split_routine,
split_regex => regexs => \@split_regex,
( $split_routine eq 'RegEx' ? $split_regex : '' ),
} }
); );
eval { $split_rule->store; }; eval { $split_rule->store; };
@ -273,11 +277,6 @@ sub get_class_sort_routines {
} }
sub get_class_split_routines { sub get_class_split_routines {
my @split_routines = qw( my @split_routines = GetSplitRoutineNames();
Dewey
Generic
LCC
RegEx
);
return \@split_routines; return \@split_routines;
} }

2
koha-tmpl/intranet-tmpl/prog/en/modules/admin/admin-home.tt

@ -113,7 +113,7 @@
[% END %] [% END %]
[% IF ( CAN_user_parameters_manage_classifications ) %] [% IF ( CAN_user_parameters_manage_classifications ) %]
<dt><a href="/cgi-bin/koha/admin/classsources.pl">Classification sources</a></dt> <dt><a href="/cgi-bin/koha/admin/classsources.pl">Classification sources</a></dt>
<dd>Define classification sources (i.e., call number schemes) used by your collection. Also define filing rules used for sorting call numbers.</dd> <dd>Define classification sources (i.e., call number schemes) used by your collection. Also define filing rules used for sorting call numbers and splitting rules for splitting them.</dd>
[% END %] [% END %]
[% IF ( CAN_user_parameters_manage_matching_rules ) %] [% IF ( CAN_user_parameters_manage_matching_rules ) %]
<dt><a href="/cgi-bin/koha/admin/matching-rules.pl">Record matching rules</a></dt> <dt><a href="/cgi-bin/koha/admin/matching-rules.pl">Record matching rules</a></dt>

90
koha-tmpl/intranet-tmpl/prog/en/modules/admin/classsources.tt

@ -16,6 +16,14 @@
</title> </title>
[% INCLUDE 'doc-head-close.inc' %] [% INCLUDE 'doc-head-close.inc' %]
<style type="text/css">
#button_add,
.split_regex,
.split_callnumber
{
padding-left: 10em;
}
</style>
</head> </head>
<body id="admin_classsources" class="admin"> <body id="admin_classsources" class="admin">
@ -233,12 +241,33 @@
</select> </select>
<span class="required">Required</span> <span class="required">Required</span>
</li> </li>
<li id="regex_block"> <li id="regex_block">
<label for="split_regex" class="required">Regular expression: </label> <label for="split_regex" class="required">Regular expression: </label>
<input type="text" name="split_regex" id="split_regex" value="[% split_rule.split_regex | html %]" /> <div>
<span class="required">Required</span> [% IF split_rule %]
</li> [% FOR re IN split_rule.regexs %]
<div class="split_regex">
<input type="text" name="split_regex" id="split_regex" value="[% re | html %]" />
<a href="#" title="Delete" class="del_regex"><i class="fa fa-fw fa-trash"></i>Delete</a>
</div>
[% END %]
[% ELSE %]
<div class="split_regex">
<input type="text" name="split_regex" id="split_regex" value="" />
</div>
[% END %]
</div>
<div id="button_add">
<a href="#" class="add_regex" title="Add a new regular expression"><i class="fa fa-fw fa-plus"></i> New</a>
<span class="required">Required</span>
</div>
<label for="callnumbers">Test the regular expressions:</label>
<div id="test_split">
<textarea name="callnumbers"></textarea>
<button name="test_split_cns">Test</button>
<div id="result_split_callnumbers"></div>
</div>
</li>
</ol> </ol>
</fieldset> </fieldset>
<fieldset class="action"> <fieldset class="action">
@ -349,6 +378,26 @@
$("#split_regex").removeAttr('required'); $("#split_regex").removeAttr('required');
} }
}; };
function update_delete_links_visibility() {
if( $("div.split_regex").length > 1 ) {
$("div.split_regex").find('a.del_regex').show();
} else {
$("div.split_regex").find('a.del_regex').hide();
}
}
function add_regex() {
var div_node = $("div.split_regex:last");
var div_clone = $(div_node).clone(true);
$(div_clone).find("input").val("");
$(div_node).after(div_clone);
update_delete_links_visibility();
}
function del_regex(a_node) {
$(a_node).parent('div').remove();
update_delete_links_visibility();
}
$(document).ready(function(){ $(document).ready(function(){
update_regex_block(); update_regex_block();
$("#split_routine").on("change", function(){ $("#split_routine").on("change", function(){
@ -364,6 +413,35 @@
$(".delete_split_rule").on("click", function(e){ $(".delete_split_rule").on("click", function(e){
return confirm(_("Are you sure you want to delete this splitting rule?")); return confirm(_("Are you sure you want to delete this splitting rule?"));
}); });
$(".add_regex").on("click", function(e){
e.preventDefault();
add_regex();
});
$(".del_regex").on("click", function(e){
e.preventDefault();
del_regex(this);
});
$("button[name='test_split_cns']").on("click", function(e){
e.preventDefault();
var regexs = [];
$("input[name='split_regex']").each(function(){
regexs.push($(this).val());
});
var callnumbers = $("textarea[name='callnumbers']").val();
$.getJSON('/cgi-bin/koha/svc/split_callnumbers', {regexs: JSON.stringify(regexs), callnumbers: callnumbers}, function(answer){
var callnumbers = answer['split_callnumbers'];
$("#result_split_callnumbers").html('');
var list_node = $('<ol></ol>');
$(callnumbers).each(function(){
var split_cn = this['split'].join('<br/>');
var li_node = $('<li><label>'+this['inline']+'</label><div class="split_callnumber">'+split_cn+'</div></li>');
$(list_node).append(li_node);
});
$("#result_split_callnumbers").append(list_node);
}).fail(function(){alert("Something went wrong.")});
});
}); });
</script> </script>

3
t/Labels.t

@ -20,6 +20,7 @@
use strict; use strict;
use warnings; use warnings;
use C4::ClassSplitRoutine::LCC;
use Test::More tests => 11; use Test::More tests => 11;
BEGIN { BEGIN {
@ -62,4 +63,4 @@ Q
ok(C4::Labels::Label::_get_text_fields(), 'test getting textx fields'); ok(C4::Labels::Label::_get_text_fields(), 'test getting textx fields');
is(C4::Labels::Label::_split_lccn(),"0", 'test when _split_lccn is null'); is(C4::ClassSplitRoutine::LCC::split_callnumber(),"0", 'test when split LCC is null');

38
t/Labels_split_Regex.t

@ -0,0 +1,38 @@
#!/usr/bin/perl
#
# 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 <http://www.gnu.org/licenses>.
use Modern::Perl;
use Test::More tests => 5;
use C4::ClassSplitRoutine::RegEx;
my $callnumbers = {
'830 Han' => [qw{830 Han}],
'159.9 (091) Gesh' => [qw{159.9 (091) Gesh}],
'J 3 Kin =774' => ['J 3', 'Kin', '=774'],
'830 Hil =774 4' => [qw{830 Hil =774 4}],
'830 Las=20 4' => [qw{830 Las =20 4}],
};
# Split on spaces and before =
# If starts with J or K then do not split the first 2 groups
my @regexs = ('s/\s/\n/g', 's/(\s?=)/\n=/g', 's/^(J|K)\n/$1 /');
foreach my $cn (sort keys %$callnumbers) {
my @parts = C4::ClassSplitRoutine::RegEx::split_callnumber($cn, \@regexs);
my @expected = @{$callnumbers->{$cn}};
is_deeply(\@parts, \@expected);
}

5
t/Labels_split_ddcn.t

@ -42,7 +42,7 @@ BEGIN {
$test_num += 4; $test_num += 4;
} }
plan tests => $test_num; plan tests => $test_num;
use_ok('C4::Labels::Label'); use_ok('C4::ClassSplitRoutine::Dewey');
use vars qw($ddcns); use vars qw($ddcns);
} }
@ -50,7 +50,8 @@ foreach my $ddcn (sort keys %$ddcns) {
my (@parts, @expected); my (@parts, @expected);
ok($ddcn, "ddcn: $ddcn"); ok($ddcn, "ddcn: $ddcn");
ok(@expected = @{$ddcns->{$ddcn}}, "split expected to produce " . scalar(@expected) . " pieces"); ok(@expected = @{$ddcns->{$ddcn}}, "split expected to produce " . scalar(@expected) . " pieces");
ok(@parts = C4::Labels::Label::_split_ddcn($ddcn), "C4::Labels::Label::_split_ddcn($ddcn)"); ok(@parts = C4::ClassSplitRoutine::Dewey::split_callnumber($ddcn), "Dewey::split_callnumber($ddcn)");
ok(scalar(@expected) == scalar(@parts), sprintf("%d of %d pieces produced", scalar(@parts), scalar(@expected))); ok(scalar(@expected) == scalar(@parts), sprintf("%d of %d pieces produced", scalar(@parts), scalar(@expected)));
my $i = 0; my $i = 0;
foreach my $unit (@expected) { foreach my $unit (@expected) {

3
t/Labels_split_lccn.t

@ -20,6 +20,7 @@
use strict; use strict;
use warnings; use warnings;
use C4::ClassSplitRoutine::LCC;
use Test::More; use Test::More;
BEGIN { BEGIN {
@ -52,7 +53,7 @@ foreach my $lccn (sort keys %$lccns) {
my (@parts, @expected); my (@parts, @expected);
ok($lccn, "lccn: $lccn"); ok($lccn, "lccn: $lccn");
ok(@expected = @{$lccns->{$lccn}}, "split expected to produce " . scalar(@expected) . " pieces"); ok(@expected = @{$lccns->{$lccn}}, "split expected to produce " . scalar(@expected) . " pieces");
ok(@parts = C4::Labels::Label::_split_lccn($lccn), "C4::Labels::Label::_split_lccn($lccn)"); ok(@parts = C4::ClassSplitRoutine::LCC::split_callnumber($lccn), "split LCC ($lccn)");
ok(scalar(@expected) == scalar(@parts), sprintf("%d of %d pieces produced", scalar(@parts), scalar(@expected))); ok(scalar(@expected) == scalar(@parts), sprintf("%d of %d pieces produced", scalar(@parts), scalar(@expected)));
my $i = 0; my $i = 0;
foreach my $unit (@expected) { foreach my $unit (@expected) {

Loading…
Cancel
Save