Bugzilla – Attachment 187134 Details for
Bug 15248
Make it possible to configure duplicate check on bibliographic records
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 15248: Use record matching rules to find duplicate while cataloging
Bug-15248-Use-record-matching-rules-to-find-duplic.patch (text/plain), 47.63 KB, created by
Andrew Fuerste-Henry
on 2025-09-30 20:18:58 UTC
(
hide
)
Description:
Bug 15248: Use record matching rules to find duplicate while cataloging
Filename:
MIME Type:
Creator:
Andrew Fuerste-Henry
Created:
2025-09-30 20:18:58 UTC
Size:
47.63 KB
patch
obsolete
>From 9488660b1a7e770df4bf47803959ca8580d646f9 Mon Sep 17 00:00:00 2001 >From: Julian Maurice <julian.maurice@biblibre.com> >Date: Wed, 13 Nov 2024 16:55:25 +0100 >Subject: [PATCH] Bug 15248: Use record matching rules to find duplicate while > cataloging > >This patch allows to configure how Koha searches for duplicates when >cataloging manually by using a record matching rule. > >For each framework we can now set a record matching rule to use when >searching for duplicates. >This allow to use different matching rules while in acquisition for >instance (ACQ framework). > >If a matching rule is selected, we also have the ability to forbid the >creation of duplicate: if a duplicate is found and this option is >enabled, the usual option to create a duplicate will be hidden. > >If no matching rule is selected, then the default matching mechanism is >used. > >Test plan: >1. Create a record matching rule matching on ISBN > Threshold: 100 > Record type: biblio > Match point: > Search index: "isbn" > Score: 100 > Tag: "020" (or "010" for UNIMARC) > Subfields: "a" > Offset: 0 > Length: 0 > Normalization rule: Remove spaces > No match checks >2. Edit the default MARC framework and select the new matching rule and > enable "Forbid duplicate creation" >3. Create a biblio with an ISBN >4. Make sure your new biblio is indexed in Zebra or ES >5. Try to create a biblio with the same ISBN. You should see a warning > about the duplicate with no option to create the duplicate biblio >6. In the acquisitions module, in a basket add a new order from an > external source (requires a configured Z39.50/SRU server) >7. Choose a record with an ISBN >8. Once the order is created, try to create a new order from an external > source and choose the same record. You should see a warning about the > duplicate. >9. Edit the record matching rule and disable "Forbid duplicate creation" >10. Redo step 5. Now there should be an option to create the duplicate > record. >11. Redo step 8. Now there should be an option to create the duplicate > record. >12. Run the following test scripts: > - t/db_dependent/Search.t > - t/db_dependent/Matcher.t > >Sponsored-by: SCD Lyon2 >Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com> > >Signed-off-by: Jeanne Maurielle <jeanne.mauriello@mainlib.org> >--- > C4/Matcher.pm | 40 +++--- > C4/Search.pm | 43 ++++++ > Koha/BiblioFramework.pm | 17 +++ > Koha/BiblioFrameworkMarcMatcher.pm | 43 ++++++ > Koha/BiblioFrameworkMarcMatchers.pm | 49 +++++++ > Koha/REST/V1/Biblios.pm | 11 +- > acqui/neworderempty.pl | 55 ++++---- > admin/biblio_framework.pl | 77 +++++++---- > cataloguing/addbiblio.pl | 12 +- > .../data/mysql/atomicupdate/bug-15248.pl | 29 ++++ > installer/data/mysql/kohastructure.sql | 17 +++ > .../modules/acqui/neworderempty_duplicate.tt | 30 +++-- > .../prog/en/modules/admin/biblio_framework.tt | 38 +++++- > .../prog/en/modules/admin/matching-rules.tt | 2 + > .../prog/en/modules/cataloguing/addbiblio.tt | 126 +++++++++++------- > t/db_dependent/Matcher.t | 6 +- > t/db_dependent/Search.t | 75 ++++++++++- > 17 files changed, 521 insertions(+), 149 deletions(-) > create mode 100644 Koha/BiblioFrameworkMarcMatcher.pm > create mode 100644 Koha/BiblioFrameworkMarcMatchers.pm > create mode 100644 installer/data/mysql/atomicupdate/bug-15248.pl > >diff --git a/C4/Matcher.pm b/C4/Matcher.pm >index 0aaaf818d9b..41d2c6512a9 100644 >--- a/C4/Matcher.pm >+++ b/C4/Matcher.pm >@@ -19,6 +19,7 @@ package C4::Matcher; > > use Modern::Perl; > >+use Koha::Database; > use Koha::SearchEngine; > use Koha::SearchEngine::Search; > use Koha::SearchEngine::QueryBuilder; >@@ -70,7 +71,7 @@ C4::Matcher - find MARC records matching another one > > =head2 GetMatcherList > >- my @matchers = C4::Matcher::GetMatcherList(); >+ my @matchers = C4::Matcher::GetMatcherList($filters); > > Returns an array of hashrefs list all matchers > present in the database. Each hashref includes: >@@ -78,19 +79,32 @@ present in the database. Each hashref includes: > * matcher_id > * code > * description >+ * record_type >+ >+C<$filters> is an optional hashref parameter that allows to filter the result. Useful keys are: >+ >+=over >+ >+=item * C<record_type> >+ >+=back >+ >+See L<DBIx::Class::ResultSet/search> for more info >+ >+=head3 Examples >+ >+ @matchers = C4::Matcher::GetMatcherList(); >+ @matchers = C4::Matcher::GetMatcherList({ record_type => 'biblio' }); >+ @matchers = C4::Matcher::GetMatcherList({ record_type => 'authority' }); > > =cut > > sub GetMatcherList { >- my $dbh = C4::Context->dbh; >+ my ($filters) = @_; > >- my $sth = $dbh->prepare_cached("SELECT matcher_id, code, description FROM marc_matchers ORDER BY matcher_id"); >- $sth->execute(); >- my @results = (); >- while ( my $row = $sth->fetchrow_hashref ) { >- push @results, $row; >- } >- return @results; >+ my $rs = Koha::Database->schema->resultset('MarcMatcher'); >+ >+ return $rs->search($filters // {}, { result_class => 'DBIx::Class::ResultClass::HashRefInflator' })->all; > } > > =head2 GetMatcherId >@@ -171,12 +185,8 @@ sub fetch { > $sth->finish(); > return unless defined $row; > >- my $self = {}; >- $self->{'id'} = $row->{'matcher_id'}; >- $self->{'record_type'} = $row->{'record_type'}; >- $self->{'code'} = $row->{'code'}; >- $self->{'description'} = $row->{'description'}; >- $self->{'threshold'} = int( $row->{'threshold'} ); >+ my $self = { %$row }; >+ $self->{id} = delete $self->{matcher_id}; > bless $self, $class; > > # matchpoints >diff --git a/C4/Search.pm b/C4/Search.pm >index 12ba64f60fe..4cb9e121daa 100644 >--- a/C4/Search.pm >+++ b/C4/Search.pm >@@ -28,7 +28,9 @@ use XML::Simple; > use C4::XSLT qw( XSLTParse4Display ); > use C4::Reserves qw( GetReserveStatus ); > use C4::Charset qw( SetUTF8Flag ); >+use C4::Matcher; > use Koha::AuthorisedValues; >+use Koha::BiblioFrameworkMarcMatchers; > use Koha::ItemTypes; > use Koha::Libraries; > use Koha::Logger; >@@ -146,6 +148,47 @@ sub FindDuplicate { > return @results; > } > >+sub FindDuplicateWithMatchingRules { >+ my ($record, $frameworkcode) = @_; >+ >+ $frameworkcode //= ''; >+ >+ my $biblio_framework_marc_matcher = Koha::BiblioFrameworkMarcMatchers->find($frameworkcode); >+ if ($biblio_framework_marc_matcher) { >+ my $matcher = C4::Matcher->fetch($biblio_framework_marc_matcher->marc_matcher_id); >+ if ($matcher) { >+ my @duplicates; >+ my $max_matches = 1; >+ my @matches = $matcher->get_matches($record, $max_matches); >+ foreach my $match (@matches) { >+ my $biblio = Koha::Biblios->find($match->{record_id}); >+ if ($biblio) { >+ push @duplicates, { >+ biblionumber => $biblio->biblionumber, >+ title => $biblio->title, >+ forbid_duplicate_creation => $biblio_framework_marc_matcher->forbid_duplicate_creation, >+ }; >+ } >+ } >+ >+ return @duplicates; >+ } >+ } >+ >+ # If no matcher can be used, default to FindDuplicate >+ my @results = FindDuplicate($record); >+ my @duplicates; >+ while ((my $biblionumber = shift @results) && (my $title = shift @results)) { >+ push @duplicates, { >+ biblionumber => $biblionumber, >+ title => $title, >+ forbid_duplicate_creation => 0, >+ } >+ } >+ >+ return @duplicates; >+} >+ > =head2 SimpleSearch > > ( $error, $results, $total_hits ) = SimpleSearch( $query, $offset, $max_results, [@servers], [%options] ); >diff --git a/Koha/BiblioFramework.pm b/Koha/BiblioFramework.pm >index 02d08c98325..bc661234cb3 100644 >--- a/Koha/BiblioFramework.pm >+++ b/Koha/BiblioFramework.pm >@@ -31,6 +31,23 @@ Koha::BiblioFramework - Koha BiblioFramework Object class > > =cut > >+=head3 delete >+ >+See L<Koha::Object/delete> >+ >+=cut >+ >+sub delete { >+ my ($self) = @_; >+ >+ my $biblio_framework_marc_matcher = Koha::BiblioFrameworkMarcMatchers->find($self->frameworkcode); >+ if ($biblio_framework_marc_matcher) { >+ $biblio_framework_marc_matcher->delete(); >+ } >+ >+ $self->SUPER::delete(); >+} >+ > =head3 type > > =cut >diff --git a/Koha/BiblioFrameworkMarcMatcher.pm b/Koha/BiblioFrameworkMarcMatcher.pm >new file mode 100644 >index 00000000000..c02c09a61f9 >--- /dev/null >+++ b/Koha/BiblioFrameworkMarcMatcher.pm >@@ -0,0 +1,43 @@ >+package Koha::BiblioFrameworkMarcMatcher; >+ >+# 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 Koha::Database; >+ >+use base qw(Koha::Object); >+ >+=head1 NAME >+ >+Koha::BiblioFrameworkMarcMatcher >+ >+=head1 API >+ >+=head2 Class Methods >+ >+=cut >+ >+=head3 type >+ >+=cut >+ >+sub _type { >+ return 'BiblioFrameworkMarcMatcher'; >+} >+ >+1; >diff --git a/Koha/BiblioFrameworkMarcMatchers.pm b/Koha/BiblioFrameworkMarcMatchers.pm >new file mode 100644 >index 00000000000..32bef344b27 >--- /dev/null >+++ b/Koha/BiblioFrameworkMarcMatchers.pm >@@ -0,0 +1,49 @@ >+package Koha::BiblioFrameworkMarcMatchers; >+ >+# 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 Koha::Database; >+ >+use Koha::BiblioFrameworkMarcMatcher; >+ >+use base qw(Koha::Objects); >+ >+=head1 NAME >+ >+Koha::BiblioFrameworkMarcMatchers >+ >+=head1 API >+ >+=head2 Class Methods >+ >+=cut >+ >+=head3 type >+ >+=cut >+ >+sub _type { >+ return 'BiblioFrameworkMarcMatcher'; >+} >+ >+sub object_class { >+ return 'Koha::BiblioFrameworkMarcMatcher'; >+} >+ >+1; >diff --git a/Koha/REST/V1/Biblios.pm b/Koha/REST/V1/Biblios.pm >index 94bca50e83b..3bbb6387a55 100644 >--- a/Koha/REST/V1/Biblios.pm >+++ b/Koha/REST/V1/Biblios.pm >@@ -23,7 +23,7 @@ use Koha::Biblios; > use Koha::DateUtils; > use Koha::Ratings; > use C4::Biblio qw( DelBiblio AddBiblio ModBiblio ); >-use C4::Search qw( FindDuplicate ); >+use C4::Search; > > use C4::Auth qw( haspermission ); > use C4::Barcodes::ValueBuilder; >@@ -667,15 +667,14 @@ sub add { > > my $confirm_not_duplicate = $headers->header('x-confirm-not-duplicate'); > >- if ( !$confirm_not_duplicate ) { >- my ( $duplicatebiblionumber, $duplicatetitle ) = FindDuplicate($record); >- >+ my ($duplicate) = C4::Search::FindDuplicateWithMatchingRules($record, $frameworkcode); >+ if ( $duplicate && ( $duplicate->{forbid_duplicate_creation} || !$confirm_not_duplicate ) ) { > return $c->render( > status => 400, > openapi => { >- error => "Duplicate biblio $duplicatebiblionumber", >+ error => "Duplicate biblio $duplicate->{biblionumber}", > } >- ) if $duplicatebiblionumber; >+ ); > } > > my ($biblio_id) = C4::Biblio::AddBiblio( $record, $frameworkcode, { record_source_id => $record_source_id } ); >diff --git a/acqui/neworderempty.pl b/acqui/neworderempty.pl >index c4db7f4044b..25b0ad6edba 100755 >--- a/acqui/neworderempty.pl >+++ b/acqui/neworderempty.pl >@@ -83,7 +83,7 @@ use C4::Biblio qw( > ); > use C4::Output qw( output_and_exit output_html_with_http_headers ); > use C4::Members; >-use C4::Search qw( FindDuplicate ); >+use C4::Search; > > #needed for z3950 import: > use C4::ImportBatch qw( SetImportRecordStatus SetMatchedBiblionumber GetImportRecordMarc ); >@@ -172,15 +172,31 @@ if ( $ordernumber eq '' and defined $breedingid ) { > $marcrecord->delete_field($item); > } > >- my $duplicatetitle; >- >- #look for duplicates >- ( $biblionumber, $duplicatetitle ) = FindDuplicate($marcrecord); >- if ( $biblionumber && $op ne 'cud-use_external_source' ) { >- >+ # look for duplicates >+ my ($duplicate) = C4::Search::FindDuplicateWithMatchingRules($marcrecord, $frameworkcode); >+ if ( $duplicate && $op ne 'cud-use_external_source' ) { > #if duplicate record found and user did not decide yet, first warn user > #and let them choose between using a new record or an existing record >- Load_Duplicate($duplicatetitle); >+ #(if the matching rule does not forbid it) >+ my ( $template, $loggedinuser, $cookie ) = get_template_and_user( >+ { >+ template_name => "acqui/neworderempty_duplicate.tt", >+ query => $input, >+ type => "intranet", >+ flagsrequired => { acquisition => 'order_manage' }, >+ } >+ ); >+ >+ my $marcflavour = uc C4::Context->preference("marcflavour"); >+ $template->param( >+ basketno => $basketno, >+ booksellerid => $basket->{'booksellerid'}, >+ breedingid => $breedingid, >+ duplicate => $duplicate, >+ $marcflavour => 1 >+ ); >+ >+ output_html_with_http_headers $input, $cookie, $template->output; > exit; > } > >@@ -608,26 +624,3 @@ sub MARCfindbreeding { > } > return -1; > } >- >-sub Load_Duplicate { >- my ($duplicatetitle) = @_; >- ( $template, $loggedinuser, $cookie ) = get_template_and_user( >- { >- template_name => "acqui/neworderempty_duplicate.tt", >- query => $input, >- type => "intranet", >- flagsrequired => { acquisition => 'order_manage' }, >- } >- ); >- >- $template->param( >- biblionumber => $biblionumber, >- basketno => $basketno, >- booksellerid => $basket->{'booksellerid'}, >- breedingid => $breedingid, >- duplicatetitle => $duplicatetitle, >- ( uc( C4::Context->preference("marcflavour") ) ) => 1 >- ); >- >- output_html_with_http_headers $input, $cookie, $template->output; >-} >diff --git a/admin/biblio_framework.pl b/admin/biblio_framework.pl >index ecad5c2bb2b..eb7709c8b37 100755 >--- a/admin/biblio_framework.pl >+++ b/admin/biblio_framework.pl >@@ -23,13 +23,17 @@ use CGI qw ( -utf8 ); > use C4::Context; > use C4::Auth qw( get_template_and_user ); > use C4::Output qw( output_html_with_http_headers ); >+use C4::Matcher; > use Koha::Biblios; > use Koha::BiblioFramework; > use Koha::BiblioFrameworks; >+use Koha::BiblioFrameworkMarcMatcher; >+use Koha::BiblioFrameworkMarcMatchers; > use Koha::Caches; >+use Koha::I18N; > > my $input = CGI->new; >-my $frameworkcode = $input->param('frameworkcode') || q||; >+my $frameworkcode = $input->param('frameworkcode'); > my $op = $input->param('op') || q|list|; > my $cache = Koha::Caches->get_instance(); > my @messages; >@@ -46,38 +50,59 @@ my ( $template, $borrowernumber, $cookie ) = get_template_and_user( > my $dbh = C4::Context->dbh; > if ( $op eq 'add_form' ) { > my $framework; >- if ($frameworkcode) { >- $framework = Koha::BiblioFrameworks->find($frameworkcode); >+ my $biblio_framework_marc_matcher; >+ if (defined $frameworkcode) { >+ if ($frameworkcode ne '') { >+ $framework = Koha::BiblioFrameworks->find($frameworkcode); >+ } else { >+ $framework = Koha::BiblioFramework->new({ frameworkcode => '', frameworktext => __('Default framework') }) >+ } >+ >+ $biblio_framework_marc_matcher = Koha::BiblioFrameworkMarcMatchers->find($frameworkcode); > } > $template->param( framework => $framework ); >+ $template->param( biblio_framework_marc_matcher => $biblio_framework_marc_matcher ); >+ >+ my @matchers = C4::Matcher::GetMatcherList(); >+ $template->param('marc_matchers' => \@matchers); > } elsif ( $op eq 'cud-add_validate' ) { >- my $frameworkcode = $input->param('frameworkcode'); >- my $frameworktext = $input->param('frameworktext'); >- my $is_a_modif = $input->param('is_a_modif'); >- >- if ($is_a_modif) { >- my $framework = Koha::BiblioFrameworks->find($frameworkcode); >- $framework->frameworktext($frameworktext); >- eval { $framework->store; }; >- if ($@) { >- push @messages, { type => 'error', code => 'error_on_update' }; >- } else { >- push @messages, { type => 'message', code => 'success_on_update' }; >- } >+ my $frameworkcode = $input->param('frameworkcode'); >+ my $frameworktext = $input->param('frameworktext'); >+ my $marc_matcher_id = $input->param('marc_matcher_id'); >+ my $forbid_duplicate_creation = $input->param('forbid_duplicate_creation') // 0; >+ my $is_a_modif = $input->param('is_a_modif'); >+ >+ my $framework; >+ if ( $is_a_modif && $frameworkcode ne '' ) { >+ $framework = Koha::BiblioFrameworks->find($frameworkcode); > } else { >- my $framework = Koha::BiblioFramework->new( >- { >- frameworkcode => $frameworkcode, >- frameworktext => $frameworktext, >+ $framework = Koha::BiblioFramework->new( { frameworkcode => $frameworkcode } ); >+ } >+ >+ $framework->frameworktext($frameworktext); >+ eval { >+ $framework->store() unless $framework->frameworkcode eq ''; >+ >+ my $biblio_framework_marc_matcher = Koha::BiblioFrameworkMarcMatchers->find($frameworkcode); >+ if ($marc_matcher_id) { >+ unless ($biblio_framework_marc_matcher) { >+ $biblio_framework_marc_matcher = >+ Koha::BiblioFrameworkMarcMatcher->new( { frameworkcode => $frameworkcode } ); > } >- ); >- eval { $framework->store; }; >- if ($@) { >- push @messages, { type => 'error', code => 'error_on_insert' }; >- } else { >- push @messages, { type => 'message', code => 'success_on_insert' }; >+ $biblio_framework_marc_matcher->marc_matcher_id($marc_matcher_id); >+ $biblio_framework_marc_matcher->forbid_duplicate_creation($forbid_duplicate_creation); >+ $biblio_framework_marc_matcher->store(); >+ } elsif ($biblio_framework_marc_matcher) { >+ $biblio_framework_marc_matcher->delete(); > } >+ }; >+ >+ if ($@) { >+ push @messages, { type => 'error', code => $is_a_modif ? 'error_on_update' : 'error_on_insert' }; >+ } else { >+ push @messages, { type => 'message', code => $is_a_modif ? 'success_on_update' : 'success_on_insert' }; > } >+ > $cache->clear_from_cache("MarcStructure-0-$frameworkcode"); > $cache->clear_from_cache("MarcStructure-1-$frameworkcode"); > $cache->clear_from_cache("MarcSubfieldStructure-$frameworkcode"); >diff --git a/cataloguing/addbiblio.pl b/cataloguing/addbiblio.pl >index 46f28d19dfb..453c21b484b 100755 >--- a/cataloguing/addbiblio.pl >+++ b/cataloguing/addbiblio.pl >@@ -37,7 +37,7 @@ use C4::Biblio qw( > TransformHtmlToMarc > ApplyMarcOverlayRules > ); >-use C4::Search qw( FindDuplicate enabled_staff_search_views ); >+use C4::Search qw( enabled_staff_search_views ); > use C4::Auth qw( get_template_and_user haspermission ); > use C4::Context; > use MARC::Record; >@@ -694,14 +694,14 @@ if ( $op eq "cud-addbiblio" ) { > $record = TransformHtmlToMarc( $input, 1 ); > > # check for a duplicate >- my ( $duplicatebiblionumber, $duplicatetitle ); >+ my $duplicate; > if ( !$is_a_modif ) { >- ( $duplicatebiblionumber, $duplicatetitle ) = FindDuplicate($record); >+ ($duplicate) = C4::Search::FindDuplicateWithMatchingRules($record, $frameworkcode); > } > my $confirm_not_duplicate = $input->param('confirm_not_duplicate'); > > # it is not a duplicate (determined either by Koha itself or by user checking it's not a duplicate) >- if ( !$duplicatebiblionumber or $confirm_not_duplicate ) { >+ if ( !$duplicate or ( !$duplicate->{forbid_duplicate_creation} and $confirm_not_duplicate ) ) { > my $oldbibitemnum; > if ($is_a_modif) { > ModBiblio( >@@ -796,9 +796,7 @@ if ( $op eq "cud-addbiblio" ) { > $template->param( > biblionumber => $biblionumber, > biblioitemnumber => $biblioitemnumber, >- duplicatebiblionumber => $duplicatebiblionumber, >- duplicatebibid => $duplicatebiblionumber, >- duplicatetitle => $duplicatetitle, >+ duplicate => $duplicate, > ); > } > >diff --git a/installer/data/mysql/atomicupdate/bug-15248.pl b/installer/data/mysql/atomicupdate/bug-15248.pl >new file mode 100644 >index 00000000000..3daa0e394e4 >--- /dev/null >+++ b/installer/data/mysql/atomicupdate/bug-15248.pl >@@ -0,0 +1,29 @@ >+use Modern::Perl; >+ >+return { >+ bug_number => "15248", >+ description => "Add biblio_framework_marc_matcher table", >+ up => sub { >+ my ($args) = @_; >+ my ( $dbh, $out ) = @$args{qw(dbh out)}; >+ >+ unless (TableExists('biblio_framework_marc_matcher')) { >+ $dbh->do( >+ q{ >+ CREATE TABLE biblio_framework_marc_matcher ( >+ frameworkcode VARCHAR(4) NOT NULL COMMENT 'MARC framework code', >+ marc_matcher_id INT(11) NOT NULL COMMENT 'MARC matcher id', >+ forbid_duplicate_creation TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Do not show an option to create a duplicate record when a duplicate is found', >+ PRIMARY KEY (frameworkcode), >+ CONSTRAINT biblio_framework_marc_matcher_ibfk_1 >+ FOREIGN KEY (marc_matcher_id) REFERENCES marc_matchers (matcher_id) >+ ON DELETE CASCADE >+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci >+ } >+ ); >+ say $out "Created new table biblio_framework_marc_matcher"; >+ } else { >+ say $out "Table biblio_framework_marc_matcher already exists"; >+ } >+ }, >+}; >diff --git a/installer/data/mysql/kohastructure.sql b/installer/data/mysql/kohastructure.sql >index e391dc68f5d..bd6674bb3f2 100644 >--- a/installer/data/mysql/kohastructure.sql >+++ b/installer/data/mysql/kohastructure.sql >@@ -1131,6 +1131,23 @@ CREATE TABLE `biblio_framework` ( > ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; > /*!40101 SET character_set_client = @saved_cs_client */; > >+-- >+-- Table structure for table `biblio_framework_marc_matcher` >+-- >+ >+DROP TABLE IF EXISTS `biblio_framework_marc_matcher`; >+/*!40101 SET @saved_cs_client = @@character_set_client */; >+/*!40101 SET character_set_client = utf8 */; >+CREATE TABLE `biblio_framework_marc_matcher` ( >+ `frameworkcode` varchar(4) NOT NULL COMMENT 'MARC framework code', >+ `marc_matcher_id` int(11) NOT NULL COMMENT 'MARC matcher id', >+ `forbid_duplicate_creation` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'Do not show an option to create a duplicate record when a duplicate is found', >+ PRIMARY KEY (`frameworkcode`), >+ KEY `biblio_framework_marc_matcher_ibfk_1` (`marc_matcher_id`), >+ CONSTRAINT `biblio_framework_marc_matcher_ibfk_1` FOREIGN KEY (`marc_matcher_id`) REFERENCES `marc_matchers` (`matcher_id`) ON DELETE CASCADE >+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; >+/*!40101 SET character_set_client = @saved_cs_client */; >+ > -- > -- Table structure for table `biblio_metadata` > -- >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/acqui/neworderempty_duplicate.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/acqui/neworderempty_duplicate.tt >index 78cb23489db..a3af870a765 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/acqui/neworderempty_duplicate.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/acqui/neworderempty_duplicate.tt >@@ -35,7 +35,7 @@ > <h1>Duplicate warning</h1> > <p > >You selected a record from an external source that matches an existing record in your catalog: >- <a target="_blank" title="Open in new window" href="[% PROCESS biblio_a_href biblionumber => biblionumber %]"><i class="fa-solid fa-window-restore"></i> [% duplicatetitle | html %]</a></p >+ <a target="_blank" title="Open in new window" href="[% PROCESS biblio_a_href biblionumber => duplicate.biblionumber %]"><i class="fa-solid fa-window-restore"></i> [% duplicate.title | html %]</a></p > > > </div> > >@@ -64,20 +64,22 @@ > </div> > </div> > >- <div class="col-sm-4"> >- <div style="border: 1px solid #DDD; padding:1em;"> >- <form method="post" action="/cgi-bin/koha/acqui/neworderempty.pl"> >- [% INCLUDE 'csrf-token.inc' %] >- <h4>Create new record</h4> >- <p>Create a new record by importing the external (duplicate) record.</p> >- <input type="hidden" name="booksellerid" value="[% booksellerid | html %]" /> >- <input type="hidden" name="basketno" value="[% basketno | html %]" /> >- <input type="hidden" name="breedingid" value="[% breedingid | html %]" /> >- <input type="hidden" name="op" value="cud-use_external_source" /> >- <input type="submit" class="btn btn-primary" value="Create new" /> >- </form> >+ [% UNLESS duplicate.forbid_duplicate_creation %] >+ <div class="col-sm-4"> >+ <div style="border: 1px solid #DDD; padding:1em;"> >+ <form method="post" action="/cgi-bin/koha/acqui/neworderempty.pl"> >+ [% INCLUDE 'csrf-token.inc' %] >+ <h4>Create new record</h4> >+ <p>Create a new record by importing the external (duplicate) record.</p> >+ <input type="hidden" name="booksellerid" value="[% booksellerid | html %]" /> >+ <input type="hidden" name="basketno" value="[% basketno | html %]" /> >+ <input type="hidden" name="breedingid" value="[% breedingid | html %]" /> >+ <input type="hidden" name="op" value="cud-use_external_source" /> >+ <input type="submit" class="btn btn-primary" value="Create new" /> >+ </form> >+ </div> > </div> >- </div> >+ [% END %] > </div> > [% END %] > >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/biblio_framework.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/biblio_framework.tt >index b4a85fe0217..b8f16e3dd82 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/biblio_framework.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/biblio_framework.tt >@@ -131,10 +131,39 @@ > </li> > [% END %] > <li> >- <label for="description" class="required">Description: </label> >- <input type="text" name="frameworktext" id="description" size="40" maxlength="80" value="[% framework.frameworktext | html %]" required="required" class="required" /> >- <span class="required">Required</span> >+ [% IF framework && frameworkcode == '' %] >+ <label for="description">Description: </label> >+ <span>[% framework.frameworktext | html %]</span> >+ [% ELSE %] >+ <label for="description" class="required">Description: </label> >+ <input type="text" name="frameworktext" id="description" size="40" maxlength="80" value="[% framework.frameworktext | html %]" required="required" class="required" /> >+ <span class="required">Required</span> >+ [% END %] > </li> >+ >+ [% IF marc_matchers.size > 0 %] >+ <li> >+ <label for="marc_matcher_id">[% t('Matching rules used to find duplicates:') | html %]</label> >+ <select name="marc_matcher_id" id="marc_matcher_id"> >+ <option value="">[% t('None (use the default duplicates finding mechanism)') | html %]</option> >+ >+ [% FOREACH marc_matcher IN marc_matchers %] >+ [% SET selected = marc_matcher.matcher_id == biblio_framework_marc_matcher.marc_matcher_id %] >+ <option value="[% marc_matcher.matcher_id | html %]" [% IF selected %]selected[% END %]> >+ [% marc_matcher.description | html %] >+ </option> >+ [% END %] >+ </select> >+ </li> >+ [% END %] >+ >+ <li> >+ <label for="forbid_duplicate_creation">[% t('Forbid creation of duplicate') | html %]</label> >+ <input type="checkbox" id="forbid_duplicate_creation" name="forbid_duplicate_creation" value="1" [% IF biblio_framework_marc_matcher.forbid_duplicate_creation %]checked[% END %]> >+ <div class="hint"> >+ [% t('If the selected matching rule finds a duplicate and this option is enabled, it will not be possible to create a duplicate. This has no effect if no matching rule is selected.') | html %] >+ </div> >+ </li> > </ol> > </fieldset> > <fieldset class="action"> >@@ -191,6 +220,9 @@ > <li > ><a class="dropdown-item" href="marctagstructure.pl?frameworkcode="><i class="fa-solid fa-eye"></i> MARC structure</a></li > > >+ <li >+ ><a class="dropdown-item" href="/cgi-bin/koha/admin/biblio_framework.pl?op=add_form&frameworkcode="><i class="fa-solid fa-pencil"></i> Edit</a></li >+ > > <!-- Trigger modal --> > <li > ><a class="dropdown-item" href="#" data-bs-toggle="modal" data-bs-target="#exportModal_default" title="Export framework structure (fields, subfields) to a spreadsheet file (.csv or .ods)" >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/matching-rules.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/matching-rules.tt >index 551b893c510..ac812fc80a1 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/matching-rules.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/matching-rules.tt >@@ -590,6 +590,7 @@ > <th>#</th> > <th>Code</th> > <th>Description</th> >+ <th>Record type</th> > <th class="no-export">Actions</th> > </tr> > [% FOREACH available_matching_rule IN available_matching_rules %] >@@ -597,6 +598,7 @@ > <td>[% available_matching_rule.matcher_id | html %]</td> > <td>[% available_matching_rule.code | html %]</td> > <td>[% available_matching_rule.description | html %]</td> >+ <td>[% available_matching_rule.record_type | html %]</td> > <td class="actions"> > <a class="btn btn-default btn-xs" href="/cgi-bin/koha/admin/matching-rules.pl?op=edit_matching_rule&matcher_id=[% available_matching_rule.matcher_id | uri %]" > ><i class="fa-solid fa-pencil" aria-hidden="true"></i> Edit</a >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/addbiblio.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/addbiblio.tt >index 7d75f7970cd..c9621d7a140 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/addbiblio.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/addbiblio.tt >@@ -831,55 +831,91 @@ > > [% UNLESS ( number ) %] > <!-- show duplicate warning on tab 0 only --> >- [% IF ( duplicatebiblionumber ) %] >+ [% IF ( duplicate ) %] > <div class="alert alert-warning"> >- <h3>Duplicate record suspected</h3> >- <p >- >Is this a duplicate of >- <a >- href="/cgi-bin/koha/catalogue/MARCdetail.pl?biblionumber=[% duplicatebiblionumber | uri %]" >- onclick="openWindow('/cgi-bin/koha/catalogue/MARCdetail.pl?biblionumber=[% duplicatebiblionumber | uri %]&popup=1', 'DuplicateBiblio','800','600'); return false;" >- >[% duplicatetitle | html %] <i class="fa-solid fa-window-restore"></i >- ></a> >- ?</p >- > >- [% IF ( CAN_user_editcatalogue_edit_items ) %] >- <form action="/cgi-bin/koha/cataloguing/additem.pl" method="get"> >- [% IF ( circborrowernumber ) # It is possible we have come from fast cataloging - include the fields %] >- <input type="hidden" name="barcode" value="[% barcode | html %]" /> >- <input type="hidden" name="branch" value="[% branch | html %]" /> >- <input type="hidden" name="circborrowernumber" value="[% circborrowernumber | html %]" /> >- <input type="hidden" name="stickyduedate" value="[% stickyduedate | html %]" /> >- <input type="hidden" name="duedatespec" value="[% duedatespec | html %]" /> >- [% END %] >- <input type="hidden" name="biblionumber" value="[% duplicatebiblionumber | html %]" /> >- <button type="submit" class="new"><i class="fa-fw fa-solid fa-pencil" aria-hidden="true"></i> Yes, edit existing items</button> >- </form> >- [% ELSIF ( circborrowernumber ) # Coming from fast cataloging %] >- <form action="/cgi-bin/koha/catalogue/detail.pl" method="get"> >- <input type="hidden" name="biblionumber" value="[% duplicatebiblionumber | html %]" /> >- <input type="submit" value="Yes: View existing items (you may not add items to existing records)" /> >- </form> >+ [% IF duplicate.forbid_duplicate_creation %] >+ <h3>Duplicate record detected</h3> >+ <p >+ >This is a duplicate of >+ <a >+ href="/cgi-bin/koha/catalogue/MARCdetail.pl?biblionumber=[% duplicate.biblionumber | uri %]" >+ onclick="openWindow('/cgi-bin/koha/catalogue/MARCdetail.pl?biblionumber=[% duplicate.biblionumber | uri %]&popup=1', 'DuplicateBiblio','800','600'); return false;" >+ >[% duplicate.title | html %] <i class="fa-solid fa-window-restore"></i >+ ></a> >+ </p >+ > >+ [% IF ( CAN_user_editcatalogue_edit_items ) %] >+ <form action="/cgi-bin/koha/cataloguing/additem.pl" method="get"> >+ [% IF ( circborrowernumber ) # It is possible we have come from fast cataloging - include the fields %] >+ <input type="hidden" name="barcode" value="[% barcode | html %]" /> >+ <input type="hidden" name="branch" value="[% branch | html %]" /> >+ <input type="hidden" name="circborrowernumber" value="[% circborrowernumber | html %]" /> >+ <input type="hidden" name="stickyduedate" value="[% stickyduedate | html %]" /> >+ <input type="hidden" name="duedatespec" value="[% duedatespec | html %]" /> >+ [% END %] >+ <input type="hidden" name="biblionumber" value="[% duplicate.biblionumber | html %]" /> >+ <button type="submit" class="new"><i class="fa-fw fa-solid fa-pencil" aria-hidden="true"></i> Edit existing items</button> >+ </form> >+ [% ELSIF ( circborrowernumber ) # Coming from fast cataloging %] >+ <form action="/cgi-bin/koha/catalogue/detail.pl" method="get"> >+ <input type="hidden" name="biblionumber" value="[% duplicate.biblionumber | html %]" /> >+ <input type="submit" value="View existing items (you may not add items to existing records)" /> >+ </form> >+ [% ELSE %] >+ <form action="/cgi-bin/koha/catalogue/detail.pl" method="get"> >+ <input type="hidden" name="biblionumber" value="[% duplicate.biblionumber | html %]" /> >+ <input type="submit" value="View existing items" /> >+ </form> >+ [% END %] > [% ELSE %] >- <form action="/cgi-bin/koha/catalogue/detail.pl" method="get"> >- <input type="hidden" name="biblionumber" value="[% duplicatebiblionumber | html %]" /> >- <input type="submit" value="Yes: View existing items" /> >- </form> >- [% END %] >- <form action="/cgi-bin/koha/cataloguing/addbiblio.pl" method="get"> >- [% IF ( CAN_user_editcatalogue_edit_items || CAN_user_editcatalogue_fast_cataloging ) %] >- [% IF ( circborrowernumber ) # It is possible we have come from fast cataloging - include the fields %] >- <input type="hidden" name="barcode" value="[% barcode | html %]" /> >- <input type="hidden" name="branch" value="[% branch | html %]" /> >- <input type="hidden" name="circborrowernumber" value="[% circborrowernumber | html %]" /> >- <input type="hidden" name="stickyduedate" value="[% stickyduedate | html %]" /> >- <input type="hidden" name="duedatespec" value="[% duedatespec | html %]" /> >- [% END %] >- <button type="submit" class="new" onclick="confirmnotdup('items'); return false;"><i class="fa fa-fw fa-save"></i> No, save as new record</button> >+ <h3>Duplicate record suspected</h3> >+ <p >+ >Is this a duplicate of >+ <a >+ href="/cgi-bin/koha/catalogue/MARCdetail.pl?biblionumber=[% duplicate.biblionumber | uri %]" >+ onclick="openWindow('/cgi-bin/koha/catalogue/MARCdetail.pl?biblionumber=[% duplicate.biblionumber | uri %]&popup=1', 'DuplicateBiblio','800','600'); return false;" >+ >[% duplicate.title | html %] <i class="fa-solid fa-window-restore"></i >+ ></a> >+ ?</p >+ > >+ [% IF ( CAN_user_editcatalogue_edit_items ) %] >+ <form action="/cgi-bin/koha/cataloguing/additem.pl" method="get"> >+ [% IF ( circborrowernumber ) # It is possible we have come from fast cataloging - include the fields %] >+ <input type="hidden" name="barcode" value="[% barcode | html %]" /> >+ <input type="hidden" name="branch" value="[% branch | html %]" /> >+ <input type="hidden" name="circborrowernumber" value="[% circborrowernumber | html %]" /> >+ <input type="hidden" name="stickyduedate" value="[% stickyduedate | html %]" /> >+ <input type="hidden" name="duedatespec" value="[% duedatespec | html %]" /> >+ [% END %] >+ <input type="hidden" name="biblionumber" value="[% duplicate.biblionumber | html %]" /> >+ <button type="submit" class="new"><i class="fa-fw fa-solid fa-pencil" aria-hidden="true"></i> Yes, edit existing items</button> >+ </form> >+ [% ELSIF ( circborrowernumber ) # Coming from fast cataloging %] >+ <form action="/cgi-bin/koha/catalogue/detail.pl" method="get"> >+ <input type="hidden" name="biblionumber" value="[% duplicate.biblionumber | html %]" /> >+ <input type="submit" value="Yes: View existing items (you may not add items to existing records)" /> >+ </form> > [% ELSE %] >- <button type="submit" class="new" onclick="confirmnotdup('view'); return false;"><i class="fa fa-fw fa-save"></i> No, save as new record</button> >+ <form action="/cgi-bin/koha/catalogue/detail.pl" method="get"> >+ <input type="hidden" name="biblionumber" value="[% duplicate.biblionumber | html %]" /> >+ <input type="submit" value="Yes: View existing items" /> >+ </form> > [% END %] >- </form> >+ <form action="/cgi-bin/koha/cataloguing/addbiblio.pl" method="get"> >+ [% IF ( CAN_user_editcatalogue_edit_items || CAN_user_editcatalogue_fast_cataloging ) %] >+ [% IF ( circborrowernumber ) # It is possible we have come from fast cataloging - include the fields %] >+ <input type="hidden" name="barcode" value="[% barcode | html %]" /> >+ <input type="hidden" name="branch" value="[% branch | html %]" /> >+ <input type="hidden" name="circborrowernumber" value="[% circborrowernumber | html %]" /> >+ <input type="hidden" name="stickyduedate" value="[% stickyduedate | html %]" /> >+ <input type="hidden" name="duedatespec" value="[% duedatespec | html %]" /> >+ [% END %] >+ <button type="submit" class="new" onclick="confirmnotdup('items'); return false;"><i class="fa fa-fw fa-save"></i> No, save as new record</button> >+ [% ELSE %] >+ <button type="submit" class="new" onclick="confirmnotdup('view'); return false;"><i class="fa fa-fw fa-save"></i> No, save as new record</button> >+ [% END %] >+ </form> >+ [% END %] > </div> > <!-- /.dialog.alert --> > [% END # /IF duplicatebiblionumber %] >diff --git a/t/db_dependent/Matcher.t b/t/db_dependent/Matcher.t >index 00840843d13..c9587668891 100755 >--- a/t/db_dependent/Matcher.t >+++ b/t/db_dependent/Matcher.t >@@ -32,7 +32,7 @@ my $builder = t::lib::TestBuilder->new; > $schema->storage->txn_begin; > > subtest 'GetMatcherList' => sub { >- plan tests => 9; >+ plan tests => 11; > > $schema->resultset('MarcMatcher')->delete_all; > my $matcher1 = $builder->build( >@@ -70,6 +70,10 @@ subtest 'GetMatcherList' => sub { > > $testmatcher->description('match on ISSN'); > is( $testmatcher->description(), 'match on ISSN', 'testing code accessor' ); >+ >+ @matchers = C4::Matcher::GetMatcherList({ record_type => 'blue' }); >+ is(scalar @matchers, 1, 'Filtering works'); >+ is($matchers[0]->{record_type}, 'blue', 'Filtering on record type works'); > }; > > subtest '_get_match_keys() tests' => sub { >diff --git a/t/db_dependent/Search.t b/t/db_dependent/Search.t >index 8ebcd4e2b2a..6d64beeef34 100755 >--- a/t/db_dependent/Search.t >+++ b/t/db_dependent/Search.t >@@ -20,6 +20,7 @@ use Modern::Perl; > use utf8; > > use C4::AuthoritiesMarc qw( SearchAuthorities ); >+use C4::Matcher; > use C4::XSLT; > require C4::Context; > >@@ -27,13 +28,14 @@ require C4::Context; > use open ':std', ':encoding(utf8)'; > > use Test::NoWarnings; >-use Test::More tests => 5; >+use Test::More tests => 6; > use Test::MockModule; > use Test::Warn; > use t::lib::Mocks; > use t::lib::Mocks::Zebra; > > use Koha::Caches; >+use Koha::Database; > > use MARC::Record; > use File::Spec; >@@ -42,6 +44,7 @@ use File::Find; > > use File::Temp qw/ tempdir /; > use File::Path; >+use DateTime; > > # Fall back to make sure that the Zebra process > # and files get cleaned up >@@ -1369,6 +1372,76 @@ subtest 'FindDuplicate' => sub { > > }; > >+subtest 'FindDuplicateWithMatchingRules' => sub { >+ plan tests => 3; >+ >+ my $marcflavour = 'MARC21'; >+ my $mock_zebra = t::lib::Mocks::Zebra->new({marcflavour => $marcflavour}); >+ push @cleanup, $mock_zebra; >+ >+ mock_GetMarcSubfieldStructure($marcflavour); >+ >+ my $sourcedir = dirname(__FILE__) . "/data"; >+ $mock_zebra->load_records( >+ sprintf( "%s/%s/zebraexport/biblio", $sourcedir, lc($marcflavour) ), >+ 'iso2709', 'biblios', 1 ); >+ $mock_zebra->launch_zebra; >+ t::lib::Mocks::mock_preference('SearchEngine', 'Zebra' ); >+ >+ my $schema = Koha::Database->schema; >+ >+ # Unlike with FindDuplicate, we need the biblio to exist >+ my $biblio = Koha::Biblios->find(51); >+ unless ($biblio) { >+ $schema->resultset('Biblio')->create( >+ { >+ biblionumber => 51, >+ frameworkcode => '', >+ title => 'Administração da produção /', >+ datecreated => DateTime->now->ymd, >+ } >+ ); >+ } >+ >+ $schema->txn_begin; >+ >+ $schema->resultset('MarcMatcher')->delete; >+ >+ my $record = MARC::Record->new; >+ $record->add_fields( >+ [ '020', ' ', ' ', a => '9788522421718' ], >+ [ '245', '0', '0', a => 'Administração da produção /' ] >+ ); >+ my ($duplicate) = C4::Search::FindDuplicateWithMatchingRules($record); >+ is($duplicate && $duplicate->{biblionumber}, 51, 'Without any matcher, same result than FindDuplicate'); >+ >+ # Make sure the matcher is used by configuring it badly first, it should return no duplicate >+ my $matcher = C4::Matcher->new('biblio', 1000); >+ $matcher->add_matchpoint('issn', 1000, [ { tag => '020', subfields => 'a', offset => 0 } ]); >+ $matcher->store(); >+ >+ my $biblio_framework_marc_matcher = Koha::BiblioFrameworkMarcMatcher->new( >+ { >+ frameworkcode => '', >+ marc_matcher_id => $matcher->{id}, >+ } >+ ); >+ $biblio_framework_marc_matcher->store(); >+ >+ ($duplicate) = C4::Search::FindDuplicateWithMatchingRules($record); >+ ok(!defined $duplicate, 'Matcher is used and returns no duplicates'); >+ >+ # Now fix the matcher and make sure it returns a duplicate >+ $matcher->{matchpoints} = []; >+ $matcher->add_matchpoint('isbn', 1000, [ { tag => '020', subfields => 'a', offset => 0 } ]); >+ $matcher->store(); >+ >+ ($duplicate) = C4::Search::FindDuplicateWithMatchingRules($record); >+ is($duplicate && $duplicate->{biblionumber}, 51, 'Matcher is used and returns a duplicate'); >+ >+ $schema->txn_rollback; >+}; >+ > # Make sure that following tests are not using our config settings > Koha::Caches->get_instance('config')->flush_all; > >-- >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 15248
:
187049
|
187050
|
187105
|
187119
| 187134