View | Details | Raw Unified | Return to bug 21520
Collapse All | Expand All

(-)a/C4/OAI/Sets.pm (-30 / +70 lines)
Lines 290-308 sub AddOAISet { Link Here
290
GetOAISetsMappings returns mappings for all OAI Sets.
290
GetOAISetsMappings returns mappings for all OAI Sets.
291
291
292
Mappings define how biblios are categorized in sets.
292
Mappings define how biblios are categorized in sets.
293
A mapping is defined by four properties:
293
A mapping is defined by six properties:
294
294
295
    {
295
    {
296
        marcfield => 'XXX',     # the MARC field to check
296
        marcfield => 'XXX',              # the MARC field to check
297
        marcsubfield => 'Y',    # the MARC subfield to check
297
        marcsubfield => 'Y',             # the MARC subfield to check
298
        operator => 'A',        # the operator 'equal' or 'notequal'; 'equal' if ''
298
        operator => 'A',                 # the operator 'equal' or 'notequal'; 'equal' if ''
299
        marcvalue => 'zzzz',    # the value to check
299
        marcvalue => 'zzzz',             # the value to check
300
        rule_operator => 'and|or|undef', # the operator between the rules
301
        rule_order    => 'n'             # the order of the rule for the mapping
300
    }
302
    }
301
303
302
If defined in a set mapping, a biblio which have at least one 'Y' subfield of
304
If defined in a set mapping, a biblio which have at least one 'Y' subfield of
303
one 'XXX' field equal to 'zzzz' will belong to this set.
305
one 'XXX' field equal to 'zzzz' will belong to this set.
304
If multiple mappings are defined in a set, the biblio will belong to this set
305
if at least one condition is matched.
306
306
307
GetOAISetsMappings returns a hashref of arrayrefs of hashrefs.
307
GetOAISetsMappings returns a hashref of arrayrefs of hashrefs.
308
The first hashref keys are the sets IDs, so it looks like this:
308
The first hashref keys are the sets IDs, so it looks like this:
Lines 313-319 The first hashref keys are the sets IDs, so it looks like this: Link Here
313
                marcfield => 'XXX',
313
                marcfield => 'XXX',
314
                marcsubfield => 'Y',
314
                marcsubfield => 'Y',
315
                operator => 'A',
315
                operator => 'A',
316
                marcvalue => 'zzzz'
316
                marcvalue => 'zzzz',
317
                rule_operator => 'and|or|undef',
318
                rule_order => 'n'
317
            },
319
            },
318
            {
320
            {
319
                ...
321
                ...
Lines 329-335 The first hashref keys are the sets IDs, so it looks like this: Link Here
329
sub GetOAISetsMappings {
331
sub GetOAISetsMappings {
330
    my $dbh = C4::Context->dbh;
332
    my $dbh = C4::Context->dbh;
331
    my $query = qq{
333
    my $query = qq{
332
        SELECT * FROM oai_sets_mappings
334
        SELECT * FROM oai_sets_mappings ORDER BY set_id, rule_order
333
    };
335
    };
334
    my $sth = $dbh->prepare($query);
336
    my $sth = $dbh->prepare($query);
335
    $sth->execute;
337
    $sth->execute;
Lines 340-346 sub GetOAISetsMappings { Link Here
340
            marcfield => $result->{'marcfield'},
342
            marcfield => $result->{'marcfield'},
341
            marcsubfield => $result->{'marcsubfield'},
343
            marcsubfield => $result->{'marcsubfield'},
342
            operator => $result->{'operator'},
344
            operator => $result->{'operator'},
343
            marcvalue => $result->{'marcvalue'}
345
            marcvalue => $result->{'marcvalue'},
346
            rule_operator => $result->{'rule_operator'},
347
            rule_order => $result->{'rule_order'}
344
        };
348
        };
345
    }
349
    }
346
350
Lines 365-370 sub GetOAISetMappings { Link Here
365
        SELECT *
369
        SELECT *
366
        FROM oai_sets_mappings
370
        FROM oai_sets_mappings
367
        WHERE set_id = ?
371
        WHERE set_id = ?
372
        ORDER BY rule_order
368
    };
373
    };
369
    my $sth = $dbh->prepare($query);
374
    my $sth = $dbh->prepare($query);
370
    $sth->execute($set_id);
375
    $sth->execute($set_id);
Lines 375-381 sub GetOAISetMappings { Link Here
375
            marcfield => $result->{'marcfield'},
380
            marcfield => $result->{'marcfield'},
376
            marcsubfield => $result->{'marcsubfield'},
381
            marcsubfield => $result->{'marcsubfield'},
377
            operator => $result->{'operator'},
382
            operator => $result->{'operator'},
378
            marcvalue => $result->{'marcvalue'}
383
            marcvalue => $result->{'marcvalue'},
384
            rule_operator => $result->{'rule_operator'},
385
            rule_order => $result->{'rule_order'}
379
        };
386
        };
380
    }
387
    }
381
388
Lines 411-425 sub ModOAISetMappings { Link Here
411
    };
418
    };
412
    my $sth = $dbh->prepare($query);
419
    my $sth = $dbh->prepare($query);
413
    $sth->execute($set_id);
420
    $sth->execute($set_id);
414
415
    if(scalar @$mappings > 0) {
421
    if(scalar @$mappings > 0) {
416
        $query = qq{
422
        $query = qq{
417
            INSERT INTO oai_sets_mappings (set_id, marcfield, marcsubfield, operator, marcvalue)
423
            INSERT INTO oai_sets_mappings (set_id, marcfield, marcsubfield, operator, marcvalue, rule_operator, rule_order)
418
            VALUES (?,?,?,?,?)
424
            VALUES (?,?,?,?,?,?,?)
419
        };
425
        };
420
        $sth = $dbh->prepare($query);
426
        $sth = $dbh->prepare($query);
421
        foreach (@$mappings) {
427
        foreach (@$mappings) {
422
            $sth->execute($set_id, $_->{'marcfield'}, $_->{'marcsubfield'}, $_->{'operator'}, $_->{'marcvalue'});
428
            $sth->execute($set_id, $_->{'marcfield'}, $_->{'marcsubfield'}, $_->{'operator'}, $_->{'marcvalue'}, $_->{'rule_operator'}, $_->{'rule_order'});
423
        }
429
        }
424
    }
430
    }
425
}
431
}
Lines 492-521 sub CalcOAISetsBiblio { Link Here
492
498
493
    my @biblio_sets;
499
    my @biblio_sets;
494
    foreach my $set_id (keys %$oai_sets_mappings) {
500
    foreach my $set_id (keys %$oai_sets_mappings) {
501
502
        my $rules = [];
495
        foreach my $mapping (@{ $oai_sets_mappings->{$set_id} }) {
503
        foreach my $mapping (@{ $oai_sets_mappings->{$set_id} }) {
496
            next if not $mapping;
504
            next if not $mapping;
497
            my $field = $mapping->{'marcfield'};
505
            my $rule_operator = $mapping->{'rule_operator'};
498
            my $subfield = $mapping->{'marcsubfield'};
506
            my $result = _evalRule($record, $mapping);
499
            my $operator = $mapping->{'operator'};
507
500
            my $value = $mapping->{'marcvalue'};
508
            # First rule or 'or' rule is always pushed
501
            my @subfield_values = $record->subfield($field, $subfield);
509
            if (!@$rules || $rule_operator eq 'or') {
502
            if ($operator eq 'notequal') {
510
                push @$rules, [$result];
503
                if(0 == grep /^$value$/, @subfield_values) {
511
                next;
504
                    push @biblio_sets, $set_id;
505
                    last;
506
                }
507
            }
512
            }
508
            else {
513
509
                if(0 < grep /^$value$/, @subfield_values) {
514
            # 'and' rule is pushed in the last 'or' rule
510
                    push @biblio_sets, $set_id;
515
            push @{$rules->[-1]}, $result;
511
                    last;
516
        }
512
                }
517
518
        my @evaluated_and;
519
        foreach my $ruleset (@$rules) {
520
           if (0 < grep /0/, @{$ruleset}) {
521
                push @evaluated_and, 0;
522
            } else {
523
                push @evaluated_and, 1;
513
            }
524
            }
514
        }
525
        }
526
527
        if (grep /1/, @evaluated_and) {
528
            push @biblio_sets, $set_id;
529
        }
530
515
    }
531
    }
516
    return @biblio_sets;
532
    return @biblio_sets;
517
}
533
}
518
534
535
# Does the record match a given mapping rule?
536
sub _evalRule {
537
    my $record = shift;
538
    my $mapping = shift;
539
540
    my $field = $mapping->{'marcfield'};
541
    my $subfield = $mapping->{'marcsubfield'};
542
    my $operator = $mapping->{'operator'};
543
    my $value = $mapping->{'marcvalue'};
544
    my @subfield_values = $record->subfield($field, $subfield);
545
    if ($operator eq 'notequal') {
546
        if(0 == grep /^$value$/, @subfield_values) {
547
            return 1;
548
        }
549
    }
550
    else {
551
        if(0 < grep /^$value$/, @subfield_values) {
552
            return 1;
553
        }
554
    }
555
    return 0;
556
}
557
558
519
=head2 ModOAISetsBiblios
559
=head2 ModOAISetsBiblios
520
560
521
    my $oai_sets_biblios = {
561
    my $oai_sets_biblios = {
(-)a/admin/oai_set_mappings.pl (-4 / +9 lines)
Lines 57-76 if($op && $op eq "save") { Link Here
57
    my @marcsubfields = $input->multi_param('marcsubfield');
57
    my @marcsubfields = $input->multi_param('marcsubfield');
58
    my @operators = $input->multi_param('operator');
58
    my @operators = $input->multi_param('operator');
59
    my @marcvalues = $input->multi_param('marcvalue');
59
    my @marcvalues = $input->multi_param('marcvalue');
60
    my @ruleoperators = $input->multi_param('rule_operator');
61
    my @ruleorders = $input->multi_param('rule_order');
60
62
61
    my @mappings;
63
    my @mappings;
62
    my $i = 0;
64
    my $i = 0;
63
    while($i < @marcfields and $i < @marcsubfields and $i < @marcvalues) {
65
    while($i < @marcfields and $i < @marcsubfields and $i < @marcvalues) {
64
        if($marcfields[$i] and $marcsubfields[$i]) {
66
        if($marcfields[$i] and $marcsubfields[$i]) {
65
            push @mappings, {
67
            push @mappings, {
66
                marcfield    => $marcfields[$i],
68
                marcfield     => $marcfields[$i],
67
                marcsubfield => $marcsubfields[$i],
69
                marcsubfield  => $marcsubfields[$i],
68
                operator     => $operators[$i],
70
                operator      => $operators[$i],
69
                marcvalue    => $marcvalues[$i]
71
                marcvalue     => $marcvalues[$i],
72
                rule_operator => $ruleoperators[$i],
73
                rule_order    => $i
70
            };
74
            };
71
        }
75
        }
72
        $i++;
76
        $i++;
73
    }
77
    }
78
    $mappings[0]{'rule_operator'} = undef if (@mappings);
74
    ModOAISetMappings($id, \@mappings);
79
    ModOAISetMappings($id, \@mappings);
75
    $template->param(mappings_saved => 1);
80
    $template->param(mappings_saved => 1);
76
}
81
}
(-)a/installer/data/mysql/atomicupdate/bug_21520.perl (+9 lines)
Line 0 Link Here
1
$DBversion = 'XXX'; # will be replaced by the RM
2
if( CheckVersion( $DBversion ) ) {
3
    $dbh->do( "ALTER TABLE oai_sets_mappings ADD COLUMN rule_order INT AFTER set_id, ADD COLUMN rule_operator VARCHAR(3) AFTER rule_order" );
4
    $dbh->do( "UPDATE oai_sets_mappings SET rule_operator='or'" );
5
6
    # Always end with this (adjust the bug info)
7
    SetVersion( $DBversion );
8
    print "Upgrade to $DBversion done (Bug 21520 - Add rule_order and rule_operator fields to oai_sets_mappings table)\n";
9
}
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/oai_set_mappings.tt (-9 / +23 lines)
Lines 31-36 Link Here
31
        <table id="mappings">
31
        <table id="mappings">
32
          <thead>
32
          <thead>
33
            <tr>
33
            <tr>
34
              <th>Rule operator</th>
34
              <th>Field</th>
35
              <th>Field</th>
35
              <th>Subfield</th>
36
              <th>Subfield</th>
36
              <th>Operator</th>
37
              <th>Operator</th>
Lines 43-48 Link Here
43
            [% IF ( mappings ) %]
44
            [% IF ( mappings ) %]
44
              [% FOREACH mapping IN mappings %]
45
              [% FOREACH mapping IN mappings %]
45
                <tr>
46
                <tr>
47
                  <td>
48
                  <select name="rule_operator">
49
                    <option value="and" [% IF (mapping.rule_operator == 'and') %] selected="selected" [% END %]>and</option>
50
                    <option value="or" [% IF (mapping.rule_operator == 'or') %] selected="selected" [% END %]>or</option>
51
                  </select>
52
                  </td>
46
                  <td><input type="text" name="marcfield" size="3" value="[% mapping.marcfield | html %]" /></td>
53
                  <td><input type="text" name="marcfield" size="3" value="[% mapping.marcfield | html %]" /></td>
47
                  <td style="text-align:center"><input type="text" name="marcsubfield" size="1" value="[% mapping.marcsubfield | html %]" /></td>
54
                  <td style="text-align:center"><input type="text" name="marcsubfield" size="1" value="[% mapping.marcsubfield | html %]" /></td>
48
                  <td><select name=operator>
55
                  <td><select name=operator>
Lines 57-65 Link Here
57
                  <td><input type="text" name="marcvalue" value="[% mapping.marcvalue | html %]" /></td>
64
                  <td><input type="text" name="marcvalue" value="[% mapping.marcvalue | html %]" /></td>
58
                  <td style="text-align:center">
65
                  <td style="text-align:center">
59
                    [% IF ( loop.last ) %]
66
                    [% IF ( loop.last ) %]
60
                      <input type="button" id="ORbutton" value="OR" title="Add another condition" />
67
                      <input type="button" id="new_rule_button" value="add" title="Add another condition" />
61
                    [% ELSE %]
62
                      OR
63
                    [% END %]
68
                    [% END %]
64
                  </td>
69
                  </td>
65
                  <td><button class="btn btn-default btn-xs clear-field" type="button"><i class="fa fa-trash"></i> Delete</button></td>
70
                  <td><button class="btn btn-default btn-xs clear-field" type="button"><i class="fa fa-trash"></i> Delete</button></td>
Lines 67-72 Link Here
67
              [% END %]
72
              [% END %]
68
            [% ELSE %]
73
            [% ELSE %]
69
              <tr>
74
              <tr>
75
                <td>
76
                  <select name="rule_operator">
77
                    <option value="and" [% IF (mapping.rule_operator == 'and') %] selected="selected" [% END %]>and</option>
78
                    <option value="or" [% IF (mapping.rule_operator == 'or') %] selected="selected" [% END %]>or</option>
79
                  </select>
80
                </td>
70
                <td><input type="text" name="marcfield" size="3" /></td>
81
                <td><input type="text" name="marcfield" size="3" /></td>
71
                <td style="text-align:center"><input type="text" name="marcsubfield" size="1" /></td>
82
                <td style="text-align:center"><input type="text" name="marcsubfield" size="1" /></td>
72
                <td><select name=operator>
83
                <td><select name=operator>
Lines 74-80 Link Here
74
                    <option value="notequal">not equal to</option>
85
                    <option value="notequal">not equal to</option>
75
                    </select></td>
86
                    </select></td>
76
                <td><input type="text" name="marcvalue" /></td>
87
                <td><input type="text" name="marcvalue" /></td>
77
                <td><input type="button" id="ORbutton" value="OR" /></td>
88
                <td><input type="button" id="new_rule_button" value="Add" /></td>
78
                <td><button class="btn btn-default btn-xs clear-field" type="button"><i class="fa fa-trash"></i> Delete</button></td>
89
                <td><button class="btn btn-default btn-xs clear-field" type="button"><i class="fa fa-trash"></i> Delete</button></td>
79
              </tr>
90
              </tr>
80
            [% END %]
91
            [% END %]
Lines 104-110 Link Here
104
            $("#mappingform").submit(function(){
115
            $("#mappingform").submit(function(){
105
              hideDialogBox();
116
              hideDialogBox();
106
            });
117
            });
107
            $("body").on("click","#ORbutton", function(e){
118
            $("body").on("click","#new_rule_button", function(e){
108
                e.preventDefault();
119
                e.preventDefault();
109
                newCondition();
120
                newCondition();
110
            });
121
            });
Lines 112-133 Link Here
112
                e.preventDefault();
123
                e.preventDefault();
113
                clearRow(e.target);
124
                clearRow(e.target);
114
            });
125
            });
126
            $("#mappings tbody tr:first-child td:first-child select").hide();
115
        });
127
        });
116
128
117
        function newCondition() {
129
        function newCondition() {
118
            var tr = $('#ORbutton').parents('tr');
130
            var tr = $('#new_rule_button').parents('tr');
119
            var clone = $(tr).clone();
131
            var clone = $(tr).clone();
120
            $("#ORbutton").parent('td').replaceWith('<td style="text-align:center">OR</td>');
132
            $("#new_rule_button").parent('td').find("#new_rule_button").remove();
133
            $(clone).find("select").show();
121
            $(tr).parent('tbody').append(clone);
134
            $(tr).parent('tbody').append(clone);
122
        }
135
        }
123
        function clearRow(link){
136
        function clearRow(link){
124
            var tr = $(link).parent().parent();
137
            var tr = $(link).parent().parent();
125
            var found = tr.find('#ORbutton');
138
            var found = tr.find('#new_rule_button');
126
            if( found.length ){
139
            if( found.length ){
127
              tr.find('input[type="text"]').attr("value","");
140
              tr.find('input[type="text"]').attr("value","");
128
            } else {
141
            } else {
129
              tr.find('input[type="text"]').attr("value","").end().hide();
142
              tr.remove();
130
            }
143
            }
144
            $("#mappings tbody tr:first-child td:first-child select").hide();
131
        }
145
        }
132
        function hideDialogBox() {
146
        function hideDialogBox() {
133
            $('div.dialog').remove();
147
            $('div.dialog').remove();
(-)a/t/db_dependent/OAI/AndSets.t (-1 / +181 lines)
Line 0 Link Here
0
- 
1
#!/usr/bin/perl
2
3
# Copyright 2019 BibLibre
4
#
5
# This file is part of Koha.
6
#
7
# Koha is free software; you can redistribute it and/or modify it under the
8
# terms of the GNU General Public License as published by the Free Software
9
# Foundation; either version 3 of the License, or (at your option) any later
10
# version.
11
#
12
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
13
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License along
17
# with Koha; if not, see <http://www.gnu.org/licenses>.
18
19
use Modern::Perl;
20
21
use Test::More tests => 4;
22
use Test::MockModule;
23
use Test::Warn;
24
use MARC::Record;
25
use Data::Dumper;
26
27
use Koha::Database;
28
use C4::Biblio;
29
use C4::OAI::Sets;
30
31
use t::lib::TestBuilder;
32
33
my $schema  = Koha::Database->new->schema;
34
$schema->storage->txn_begin;
35
my $dbh = C4::Context->dbh;
36
37
$dbh->do('DELETE FROM oai_sets');
38
$dbh->do('DELETE FROM oai_sets_descriptions');
39
$dbh->do('DELETE FROM oai_sets_mappings');
40
$dbh->do('DELETE FROM oai_sets_biblios');
41
42
my $builder = t::lib::TestBuilder->new;
43
44
my $set1 = {
45
    'spec' => 'specSet1',
46
    'name' => 'nameSet1',
47
};
48
my $set1_id = AddOAISet($set1);
49
50
my $marcflavour = C4::Context->preference('marcflavour');
51
my $mapping1;
52
53
if ($marcflavour eq 'UNIMARC' ){
54
    $mapping1 = [
55
        {
56
            rule_order => 1,
57
            marcfield => '200',
58
            marcsubfield => 'a',
59
            operator => 'equal',
60
            marcvalue => 'myTitle'
61
        },
62
        {
63
            rule_order => 2,
64
            rule_operator => 'and',
65
            marcfield => '200',
66
            marcsubfield => 'f',
67
            operator => 'equal',
68
            marcvalue => 'myAuthor'
69
        },
70
    ];
71
} else {
72
    $mapping1 = [
73
        {
74
            rule_order => 1,
75
            marcfield => '245',
76
            marcsubfield => 'a',
77
            operator => 'equal',
78
            marcvalue => 'myTitle'
79
        },
80
        {
81
            rule_order => 2,
82
            rule_operator => 'and',
83
            marcfield => '100',
84
            marcsubfield => 'a',
85
            operator => 'equal',
86
            marcvalue => 'myAuthor'
87
        },
88
    ];
89
}
90
91
#Add 1st mapping for set1
92
ModOAISetMappings($set1_id, $mapping1);
93
94
my $biblio_1 = $builder->build_sample_biblio({ title => 'myTitle' });
95
my $biblio_2 = $builder->build_sample_biblio({ title => 'myTitle', author => 'myAuthor' });
96
97
my $biblionumber1 = $biblio_1->biblionumber;
98
my $biblionumber2 = $biblio_2->biblionumber;
99
100
101
my $record = GetMarcBiblio({ biblionumber => $biblionumber1 });
102
my @setsEq = CalcOAISetsBiblio($record);
103
ok(!@setsEq, 'If only one condition is true, the record does not belong to the set');
104
105
$record = GetMarcBiblio({ biblionumber => $biblionumber2 });
106
@setsEq = CalcOAISetsBiblio($record);
107
is_deeply(@setsEq, $set1_id, 'If all conditions are true, the record belongs to the set');
108
109
if ($marcflavour eq 'UNIMARC' ){
110
    $mapping1 = [
111
        {
112
            rule_order => 1,
113
            marcfield => '200',
114
            marcsubfield => 'a',
115
            operator => 'equal',
116
            marcvalue => 'myTitle'
117
        },
118
        {
119
            rule_order => 2,
120
            rule_operator => 'or',
121
            marcfield => '200',
122
            marcsubfield => 'f',
123
            operator => 'equal',
124
            marcvalue => 'myAuthor'
125
        },
126
        {
127
            rule_order => 3,
128
            rule_operator => 'and',
129
            marcfield => '995',
130
            marcsubfield => 'r',
131
            operator => 'equal',
132
            marcvalue => 'myItemType'
133
        },
134
135
    ];
136
} else {
137
    $mapping1 = [
138
        {
139
            rule_order => 1,
140
            marcfield => '245',
141
            marcsubfield => 'a',
142
            operator => 'equal',
143
            marcvalue => 'myTitle'
144
        },
145
        {
146
            rule_order => 2,
147
            rule_operator => 'or',
148
            marcfield => '100',
149
            marcsubfield => 'a',
150
            operator => 'equal',
151
            marcvalue => 'myAuthor'
152
        },
153
        {
154
            rule_order => 3,
155
            rule_operator => 'and',
156
            marcfield => '942',
157
            marcsubfield => 'c',
158
            operator => 'equal',
159
            marcvalue => 'myItemType'
160
        },
161
    ];
162
}
163
164
ModOAISetMappings($set1_id, $mapping1);
165
166
$biblio_1 = $builder->build_sample_biblio({ title => 'myTitle' });
167
$biblio_2 = $builder->build_sample_biblio({ author => 'myAuthor', itemtype => 'myItemType' });
168
169
$biblionumber1 = $biblio_1->biblionumber;
170
$biblionumber2 = $biblio_2->biblionumber;
171
172
$record = GetMarcBiblio({ biblionumber => $biblionumber1 });
173
@setsEq = CalcOAISetsBiblio($record);
174
175
is_deeply(@setsEq, $set1_id, 'Boolean operators precedence is respected, the record with only the title belongs to the set');
176
177
$record = GetMarcBiblio({ biblionumber => $biblionumber2 });
178
@setsEq = CalcOAISetsBiblio($record);
179
is_deeply(@setsEq, $set1_id, 'Boolean operators precedence is respected, the record with author and itemtype belongs to the set');
180
181
$schema->storage->txn_rollback;

Return to bug 21520