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

(-)a/misc/maintenance/marc_framework_diff.pl (-1 / +492 lines)
Line 0 Link Here
0
- 
1
#!/usr/bin/perl
2
3
# Copyright 2024 Hypernova Oy
4
#
5
# This file is part of Koha.
6
#
7
# Koha is free software; you can redistribute it and/or modify it
8
# under the terms of the GNU General Public License as published by
9
# the Free Software Foundation; either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# Koha is distributed in the hope that it will be useful, but
13
# WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20
use Modern::Perl;
21
use strict;
22
use warnings;
23
24
use Getopt::Long qw( GetOptions :config no_ignore_case bundling);
25
use Data::Printer;
26
use Pod::Usage qw( pod2usage );
27
28
use Koha::Database;
29
30
=head1 NAME
31
32
marc_framework_diff.pl - Detect differences in MARC frameworks and reconcile them
33
34
=head1 SYNOPSIS
35
36
marc_framework_diff.pl
37
38
 Options:
39
  --help or -h          Brief usage message
40
  --verbose or -v       Integer, 0-4, Be more verbose
41
42
 Utility commands
43
  --list-frameworkcodes List all framework codes in Koha as a csv, with more
44
                        verbosity dumps object attributes.
45
46
 Diff commands
47
  --diff                Activate diffing output. By default selects all
48
                        frameworks for diffing.
49
  --attributes-exclude  CSV String, eg. "display_order,maxlength".
50
                        Exclude these marc framework keys from comparison.
51
  --frameworks-include  CSV String, eg. "ACQ,,FA,BKS"
52
                        (Empty ,, selects the default framework).
53
                        Include these frameworks.
54
  --frameworks-exclude  CSV String, eg. "ACQ,FA"
55
                        (Empty ,, selects the default framework).
56
                        Exclude these frameworks. Can be used with
57
                        --frameworks-include
58
  --ignore-undef        When comparing values, undefined and "" and 0 compare
59
                        as equally undefined/unset/false.
60
  --perly-false         When comparing values, "" and 0 compare
61
                        as equally undefined/unset/false.
62
63
 Update commands
64
  --update              Activate the update feature.
65
  --field               String, eg. "245". MARC field within the framework.
66
  --subfield            String, eg. "a". MARC subfield within the framework.
67
  --attribute           String, eg. "hidden". Framework attribute to update.
68
  --value               String, eg. "yso_finto_finaf.pl".
69
                        Value to set to all of the selected frameworks.
70
71
=head1 EXAMPLES
72
73
 marc_framework_diff.pl --list-frameworkcodes -v 1
74
75
 # Update marc frameworks "KI,,KA,EK" field "810$t" attribute "hidden" with "0"
76
 marc_framework_diff.pl --diff --attributes-exclude "display_order,maxlength" \
77
  --frameworks-include "KI,,KA,EK" --ignore-undef --perly-false \
78
  --update --field 810 --subfield t --attribute hidden --value 0
79
80
=cut
81
82
binmode( STDOUT, ":encoding(UTF-8)" );
83
84
my %a;
85
GetOptions(
86
  \%a,
87
  'help|h',
88
  'verbose|v:i',
89
90
  #Utility commands
91
  'list-frameworkcodes',
92
93
  #Diff commands
94
  'diff',
95
  'attributes-exclude:s',
96
  'frameworks-include:s',
97
  'frameworks-exclude:s',
98
  'ignore-undef',
99
  'perly-false',
100
101
  #Update commands
102
  'update',
103
  'field:s',
104
  'subfield:s',
105
  'attribute:s',
106
  'value:s',
107
) or pod2usage(2);
108
$a{verbose} = 0 unless exists $a{verbose};
109
p(%a);
110
111
pod2usage(1) if $a{help};
112
113
my $dbh = Koha::Database->schema->storage->dbh(); # Use DBD::MySQL for minimum headache
114
$dbh->{ShowErrorStatement} = 1;
115
$dbh->{RaiseError} = 1;
116
117
my @colWidths = (3, 1, 16, 30);
118
sub col {
119
  my ($position, $value) = @_;
120
  my $width = $colWidths[$position];
121
  my $offset = $width - length($value);
122
  return $value . (" " x $offset) if $offset > 0;
123
  return substr($value, 0, $width);
124
}
125
126
sub handle_cli_param_include_exclude {
127
  my ($biblio_framework_codes) = @_;
128
129
  if ($a{'attributes-exclude'}) {
130
    $a{'attributes-exclude'} =~ s/\s//gsm;
131
    $a{'attributes-exclude'} = ','.$a{'attributes-exclude'}.',';
132
    if ($a{verbose} > 1) {
133
      print "- Attributes to exclude: $a{'attributes-exclude'}\n";
134
    }
135
  }
136
137
  if ($a{'frameworks-include'}) {
138
    $a{'frameworks-include'} =~ s/\s//gsm;
139
    $a{'frameworks-include'} = ','.$a{'frameworks-include'}.',';
140
    @$biblio_framework_codes = grep {$a{'frameworks-include'} =~ /,$_,/} @$biblio_framework_codes;
141
    if ($a{verbose} > 1) {
142
      print "- Included: @$biblio_framework_codes\n";
143
    }
144
  }
145
146
  if ($a{'frameworks-exclude'}) {
147
    $a{'frameworks-exclude'} =~ s/\s//gsm;
148
    $a{'frameworks-exclude'} = ','.$a{'frameworks-exclude'}.',';
149
    @$biblio_framework_codes = grep {$a{'frameworks-exclude'} !~ /,$_,/} @$biblio_framework_codes;
150
    if ($a{verbose} > 1) {
151
      print "- Excluded: @$biblio_framework_codes\n";
152
    }
153
  }
154
155
  return $biblio_framework_codes;
156
}
157
158
sub _check_where_filter {
159
  my ($sql_where_filter) = @_;
160
  $sql_where_filter = "" unless $sql_where_filter;
161
  $sql_where_filter = "WHERE $sql_where_filter" if $sql_where_filter and $sql_where_filter !~ /WHERE/;
162
  return $sql_where_filter;
163
}
164
165
sub fetch_biblio_framework_codes {
166
  my $sql = "SELECT DISTINCT(frameworkcode) FROM biblio_framework ORDER BY frameworkcode ASC;";
167
  print "SQL:> " . sql_dump($sql) . "\n" if $a{verbose};
168
  my $biblio_framework_codes = $dbh->selectcol_arrayref($sql);
169
  push(@$biblio_framework_codes, ''); #The default framework is no longer in the biblio_frameworks-table
170
  return $biblio_framework_codes;
171
}
172
sub fetch_biblio_frameworks {
173
  my ($sql_where_filter) = @_;
174
  $sql_where_filter = _check_where_filter($sql_where_filter);
175
  my $sql = "SELECT * FROM biblio_framework $sql_where_filter ORDER BY frameworkcode ASC;";
176
  print "SQL:> " . sql_dump($sql) . "\n" if $a{verbose};
177
  my $biblio_frameworks = $dbh->selectall_arrayref($sql, { Slice => {} });
178
  push(@$biblio_frameworks, {frameworkcode => '', frameworktext => 'Default'}) if $sql_where_filter =~ /''/; #The default framework is no longer in the biblio_frameworks-table
179
  return $biblio_frameworks;
180
}
181
sub fetch_marc_tag_structure {
182
  my ($sql_where_filter) = @_;
183
  $sql_where_filter = _check_where_filter($sql_where_filter);
184
  my $sql = "SELECT * FROM marc_tag_structure $sql_where_filter GROUP BY frameworkcode ASC, tagfield ASC;";
185
  print "SQL:> " . sql_dump($sql) . "\n" if $a{verbose};
186
  return $dbh->selectall_arrayref($sql, { Slice => {} });
187
}
188
sub fetch_marc_subfield_structure {
189
  my ($sql_where_filter) = @_;
190
  $sql_where_filter = _check_where_filter($sql_where_filter);
191
  my $sql = "SELECT * FROM marc_subfield_structure $sql_where_filter GROUP BY frameworkcode ASC, tagfield ASC, tagsubfield ASC;";
192
  print "SQL:> " . sql_dump($sql) . "\n" if $a{verbose};
193
  return $dbh->selectall_arrayref($sql, { Slice => {} });
194
}
195
sub fetch_data {
196
  my $biblio_framework_codes = fetch_biblio_framework_codes();
197
  $biblio_framework_codes = handle_cli_param_include_exclude($biblio_framework_codes);
198
  my $sql_where_filter = "WHERE frameworkcode IN ('" . join("','", @$biblio_framework_codes) . "')";
199
200
  my $biblio_frameworks = fetch_biblio_frameworks($sql_where_filter);
201
  if ($a{verbose} > 1) {
202
    print "Biblio frameworks:\n";
203
    p($biblio_frameworks);
204
  }
205
206
  my $marc_field_structure = fetch_marc_tag_structure($sql_where_filter);
207
  if ($a{verbose} > 3) { # This is too massive info dump
208
    print "MARC field structure:\n";
209
    p($marc_field_structure);
210
  }
211
212
  my $marc_subfield_structure = fetch_marc_subfield_structure($sql_where_filter);
213
  if ($a{verbose} > 3) { # This is too massive info dump
214
    print "MARC subfield structure:\n";
215
    p($marc_subfield_structure);
216
  }
217
218
  my $distinct_fields_and_subfields = $dbh->selectall_arrayref("SELECT tagfield, tagsubfield FROM marc_subfield_structure $sql_where_filter GROUP BY tagfield, tagsubfield ORDER BY tagfield ASC, tagsubfield ASC", { Slice => {} });
219
  if ($a{verbose} > 3) {
220
    print "distinct_fields_and_subfields:\n";
221
    p($distinct_fields_and_subfields);
222
  }
223
224
  return ($biblio_frameworks, $marc_field_structure, $marc_subfield_structure, $distinct_fields_and_subfields);
225
}
226
227
sub prepare_data_for_comparison {
228
  my ($biblio_frameworks, $marc_field_structure, $marc_subfield_structure, $distinct_fields_and_subfields) = @_;
229
  my $d = MFWDiff->new();
230
231
  for my $fw (@$biblio_frameworks) {
232
    $d->add_framework($fw);
233
  }
234
235
  for my $dfsfs (@$distinct_fields_and_subfields) {
236
    $d->push_comparison_slots($dfsfs->{tagfield}, $dfsfs->{tagsubfield});
237
  }
238
239
  for my $mfs (@$marc_field_structure) {
240
    $d->push_field_structure($mfs->{tagfield}, $mfs);
241
  }
242
243
  for my $mss (@$marc_subfield_structure) {
244
    $d->push_subfield_structure($mss->{tagfield}, $mss->{tagsubfield}, $mss);
245
  }
246
247
  return $d;
248
}
249
250
sub diff_comparison {
251
  my ($d) = @_;
252
253
  for my $field_code (sort keys %{$d->{fields}}) {
254
    for my $attr (sort keys %{$d->{fields}->{$field_code}->{attr}}) {
255
      for my $fw_seek ($d->get_framework_codes()) {
256
        for my $fw_comp ($d->get_framework_codes()) {
257
          next if ($fw_seek eq $fw_comp);
258
          next if ($attr eq 'frameworkcode');
259
          next if ($a{'attributes-exclude'} && $a{'attributes-exclude'} =~ /$attr/);
260
          if ($d->field_values_diff($field_code, $attr, $fw_seek, $fw_comp)) {
261
            $d->add_field_diff($field_code, $attr, $fw_seek, $fw_comp);
262
          }
263
        }
264
      }
265
    }
266
267
    for my $subfield_code (sort keys %{$d->{fields}->{$field_code}->{subs}}) {
268
      for my $attr (sort keys %{$d->{fields}->{$field_code}->{subs}->{$subfield_code}}) {
269
        for my $fw_seek ($d->get_framework_codes()) {
270
          for my $fw_comp ($d->get_framework_codes()) {
271
            next if ($fw_seek eq $fw_comp);
272
            next if ($attr eq 'frameworkcode');
273
            next if ($a{'attributes-exclude'} && $a{'attributes-exclude'} =~ /$attr/);
274
            if ($d->subfield_values_diff($field_code, $subfield_code, $attr, $fw_seek, $fw_comp)) {
275
              $d->add_subfield_diff($field_code, $subfield_code, $attr, $fw_seek, $fw_comp);
276
            }
277
          }
278
        }
279
      }
280
    }
281
  }
282
  return $d;
283
}
284
285
sub print_diff {
286
  my ($d) = @_;
287
288
  print "|" . join("|", col(0, "FLD"), col(1, "S"), col(2, "Attribute")) . "|";
289
  for my $fw ($d->get_framework_codes()) {
290
    print col(3, ($fw ? $fw : 'Default')) . "|";
291
  }
292
  print "\n+---+-+-----------+-------------------------------\n";
293
294
  for my $field_code (sort keys %{$d->{fields}}) {
295
    for my $attr (sort keys %{$d->{fields}->{$field_code}->{attr}}) {
296
      my $show_diff = 0;
297
      my @sb;
298
      for my $fw ($d->get_framework_codes()) {
299
        if ($d->{fields}->{$field_code}->{attr}->{$attr}->{$fw}->{diff}) {
300
          $show_diff++;
301
          push(@sb, col(3, scalar(@{$d->{fields}->{$field_code}->{attr}->{$attr}->{$fw}->{diff}}) . '*' . $d->field_value($field_code, $attr, $fw)));
302
        }
303
        else {
304
          push(@sb, col(3, ""));
305
        }
306
      }
307
      print "|" . join("|", col(0, $field_code), col(1, " "), col(2, $attr)) . "|" . join("|", @sb) . "|\n" if $show_diff;
308
    }
309
310
    for my $subfield_code (sort keys %{$d->{fields}->{$field_code}->{subs}}) {
311
      for my $attr (sort keys %{$d->{fields}->{$field_code}->{subs}->{$subfield_code}}) {
312
        my $show_diff = 0;
313
        my @sb;
314
        for my $fw ($d->get_framework_codes()) {
315
          if ($d->{fields}->{$field_code}->{subs}->{$subfield_code}->{$attr}->{$fw}->{diff}) {
316
            $show_diff++;
317
            push(@sb, col(3, scalar(@{$d->{fields}->{$field_code}->{subs}->{$subfield_code}->{$attr}->{$fw}->{diff}}) . '*' . $d->subfield_value($field_code, $subfield_code, $attr, $fw)));
318
          }
319
          else {
320
            push(@sb, col(3, ""));
321
          }
322
        }
323
        print "|" . join("|", col(0, $field_code), col(1, $subfield_code), col(2, $attr)) . "|" . join("|", @sb) . "|\n" if $show_diff;
324
      }
325
    }
326
  }
327
}
328
329
sub list_frameworkcodes {
330
  if ($a{verbose} == 0) {
331
    my $biblio_framework_codes = fetch_biblio_framework_codes();
332
    print join(",", @$biblio_framework_codes)."\n";
333
  }
334
  else {
335
    my $biblio_frameworks = fetch_biblio_frameworks("");
336
    p($biblio_frameworks);
337
  }
338
}
339
340
sub sql_dump {
341
  my ($sql, @params) = @_;
342
  $sql . ' "' . join('","', @params) . '"';
343
}
344
345
sub update {
346
  my $biblio_framework_codes = fetch_biblio_framework_codes();
347
  $biblio_framework_codes = handle_cli_param_include_exclude($biblio_framework_codes);
348
  my $sql_frameworkcode_filter = "frameworkcode IN ('" . join("','", @$biblio_framework_codes) . "')";
349
350
  my $rv;
351
  my $sql;
352
  if ($a{field} && not($a{subfield})) {
353
    $sql = "UPDATE marc_tag_structure SET ".$dbh->quote_identifier($a{attribute})." = ? WHERE $sql_frameworkcode_filter AND tagfield = ?;";
354
    print "SQL:> " . sql_dump($sql, $a{value}, $a{field}) . "\n" if $a{verbose};
355
    $rv = $dbh->do($sql, undef, $a{value}, $a{field});
356
  }
357
  elsif ($a{field} && $a{subfield}) {
358
    $sql = "UPDATE marc_subfield_structure SET ".$dbh->quote_identifier($a{attribute})." = ? WHERE $sql_frameworkcode_filter AND tagfield = ? AND tagsubfield = ?;";
359
    print "SQL:> " . sql_dump($sql, $a{value}, $a{field}, $a{subfield}) . "\n" if $a{verbose};
360
    $rv = $dbh->do($sql, undef, $a{value}, $a{field}, $a{subfield});
361
  }
362
  print "SQL:> $rv rows affected\n" if ($rv >= 0);
363
}
364
list_frameworkcodes() if $a{'list-frameworkcodes'};
365
update() if $a{update};
366
print_diff(diff_comparison(prepare_data_for_comparison(fetch_data()))) if $a{diff};
367
368
package MFWDiff;
369
370
=head2 MFWDiff
371
372
Data structure to present all the frameworks in a easily comparable fashion.
373
374
field -> 'attr' -> attribute -> framework -> 'value' -> value
375
                                          -> 'diff'  -> [framework, framework, ...]
376
         'subs' -> subfield -> attribute -> framework -> 'value' -> value
377
                                                      -> 'diff'  -> [framework, framework, ...]
378
379
=cut
380
381
sub new {
382
  my $self = {};
383
  return bless($self, $_[0]);
384
}
385
386
sub field {
387
  return $_[0]->{fields}->{$_[1]};
388
}
389
390
sub field_value {
391
  my ($s, $field_code, $attribute, $frameworkcode) = @_;
392
  #return '_mssin_' unless $s->{fields}->{$field_code}->{attr}->{$attribute}->{$frameworkcode};
393
  return '_mssin_' unless $s->{fields}->{$field_code}->{attr}->{$attribute}->{$frameworkcode}->{value};
394
  return $s->{fields}->{$field_code}->{attr}->{$attribute}->{$frameworkcode}->{value};
395
}
396
397
sub subfield_value {
398
  my ($s, $field_code, $subfield_code, $attribute, $frameworkcode) = @_;
399
  #return '_mssin_' unless $s->{fields}->{$field_code}->{subs}->{$subfield_code}->{$attribute}->{$frameworkcode};
400
  return '_mssin_' unless $s->{fields}->{$field_code}->{subs}->{$subfield_code}->{$attribute}->{$frameworkcode}->{value};
401
  return $s->{fields}->{$field_code}->{subs}->{$subfield_code}->{$attribute}->{$frameworkcode}->{value};
402
}
403
404
sub add_field_diff {
405
  my ($s, $field_code, $attribute, $frameworkcode_seeker, $frameworkcode_comparator) = @_;
406
  $s->{fields}->{$field_code}->{attr}->{$attribute}->{$frameworkcode_seeker}->{diff} = [] unless $s->{fields}->{$field_code}->{attr}->{$attribute}->{$frameworkcode_seeker}->{diff};
407
  push(@{$s->{fields}->{$field_code}->{attr}->{$attribute}->{$frameworkcode_seeker}->{diff}}, $frameworkcode_comparator);
408
  print("Diff: '$field_code'->'attr'->'$attribute'->'$frameworkcode_seeker' ne '$frameworkcode_comparator'\n") if $a{verbose} > 1;
409
}
410
411
sub add_subfield_diff {
412
  my ($s, $field_code, $subfield_code, $attribute, $frameworkcode_seeker, $frameworkcode_comparator) = @_;
413
  $s->{fields}->{$field_code}->{subs}->{$subfield_code}->{$attribute}->{$frameworkcode_seeker}->{diff} = [] unless $s->{fields}->{$field_code}->{subs}->{$subfield_code}->{$attribute}->{$frameworkcode_seeker}->{diff};
414
  push(@{$s->{fields}->{$field_code}->{subs}->{$subfield_code}->{$attribute}->{$frameworkcode_seeker}->{diff}}, $frameworkcode_comparator);
415
  print("Diff: '$field_code\$$subfield_code'->'attr'->'$attribute'->'$frameworkcode_seeker' ne '$frameworkcode_comparator'\n") if $a{verbose} > 1;
416
}
417
418
sub field_values_diff {
419
  my ($s, $field_code, $attr, $fw_seek, $fw_comp) = @_;
420
421
  if ($a{'perly-false'}) {
422
    return 0 if (not($s->{fields}->{$field_code}->{attr}->{$attr}->{$fw_seek}->{value}) && not($s->{fields}->{$field_code}->{attr}->{$attr}->{$fw_comp}->{value}));
423
  }
424
425
  return $s->field_value($field_code, $attr, $fw_seek) ne $s->field_value($field_code, $attr, $fw_comp);
426
}
427
428
sub subfield_values_diff {
429
  my ($s, $field_code, $subfield_code, $attr, $fw_seek, $fw_comp) = @_;
430
431
  if ($a{'perly-false'}) {
432
    return 0 if (not($s->{fields}->{$field_code}->{subs}->{$subfield_code}->{$attr}->{$fw_seek}->{value}) && not($s->{fields}->{$field_code}->{subs}->{$subfield_code}->{$attr}->{$fw_comp}->{value}));
433
  }
434
435
  return $s->subfield_value($field_code, $subfield_code, $attr, $fw_seek) ne $s->subfield_value($field_code, $subfield_code, $attr, $fw_comp);
436
}
437
438
sub push_comparison_slots {
439
  my ($s, $field_code, $subfield_code) = @_;
440
  $s->{fields}->{$field_code} = {} unless $s->{fields}->{$field_code};
441
  $s->{fields}->{$field_code}->{attr} = {} unless $s->{fields}->{$field_code}->{attr};
442
  $s->{fields}->{$field_code}->{subs} = {} unless $s->{fields}->{$field_code}->{subs};
443
  $s->{fields}->{$field_code}->{subs}->{$subfield_code} = {};
444
}
445
446
sub push_field_structure {
447
  my ($s, $field_code, $mfs) = @_;
448
  unless ($s->{fields}->{$field_code}) {
449
    warn "push_field_structure($field_code fw='".$mfs->{frameworkcode}."'):> Missing field_code?";
450
  }
451
452
  while (my ($k, $v) = each(%$mfs)) {
453
    $s->{fields}->{$field_code}->{attr}->{$k} = {} unless $s->{fields}->{$field_code}->{attr}->{$k};
454
    $s->{fields}->{$field_code}->{attr}->{$k}->{$mfs->{frameworkcode}} = {} unless $s->{fields}->{$field_code}->{attr}->{$k}->{$mfs->{frameworkcode}};
455
    if ($a{'ignore-undef'} || $a{'perly-false'}) {
456
      $s->{fields}->{$field_code}->{attr}->{$k}->{$mfs->{frameworkcode}}->{value} = (defined $v) ? $v : '';
457
    }
458
    else {
459
      $s->{fields}->{$field_code}->{attr}->{$k}->{$mfs->{frameworkcode}}->{value} = (defined $v) ? $v : '_undef_';
460
    }
461
  }
462
}
463
464
sub push_subfield_structure {
465
  my ($s, $field_code, $subfield_code, $mss) = @_;
466
  unless ($s->{fields}->{$field_code}) {
467
    warn "push_subfield_structure($field_code\$$subfield_code fw='".$mss->{frameworkcode}."'):> Missing field_code?";
468
  }
469
  unless ($s->{fields}->{$field_code}->{subs}->{$subfield_code}) {
470
    warn "push_subfield_structure($field_code\$$subfield_code fw='".$mss->{frameworkcode}."'):> Missing subfield_code?";
471
  }
472
  while (my ($k, $v) = each(%$mss)) {
473
    $s->{fields}->{$field_code}->{subs}->{$subfield_code}->{$k} = {} unless $s->{fields}->{$field_code}->{subs}->{$subfield_code}->{$k};
474
    $s->{fields}->{$field_code}->{subs}->{$subfield_code}->{$k}->{$mss->{frameworkcode}} = {} unless $s->{fields}->{$field_code}->{subs}->{$subfield_code}->{$k}->{$mss->{frameworkcode}};
475
    if ($a{'ignore-undef'} || $a{'perly-false'}) {
476
      $s->{fields}->{$field_code}->{subs}->{$subfield_code}->{$k}->{$mss->{frameworkcode}}->{value} = (defined $v) ? $v : '';
477
    }
478
    else {
479
      $s->{fields}->{$field_code}->{subs}->{$subfield_code}->{$k}->{$mss->{frameworkcode}}->{value} = (defined $v) ? $v : '_undef_';
480
    }
481
  }
482
}
483
484
sub add_framework {
485
  my ($s, $framework) = @_;
486
  $s->{frameworks} = {} unless $s->{frameworks};
487
  $s->{frameworks}->{$framework->{frameworkcode}} = $framework;
488
}
489
# Use en external frameworks list, because not all frameworks have all fields/subfields
490
sub get_framework_codes {
491
  return sort keys %{$_[0]->{frameworks}};
492
}

Return to bug 37738