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

(-)a/C4/Breeding.pm (-103 / +1 lines)
Lines 39-45 BEGIN { Link Here
39
    $VERSION = 3.07.00.049;
39
    $VERSION = 3.07.00.049;
40
	require Exporter;
40
	require Exporter;
41
	@ISA = qw(Exporter);
41
	@ISA = qw(Exporter);
42
    @EXPORT = qw(&BreedingSearch &Z3950Search &Z3950SearchAuth &RunZ3950);
42
    @EXPORT = qw(&BreedingSearch &Z3950Search &Z3950SearchAuth);
43
}
43
}
44
44
45
=head1 NAME
45
=head1 NAME
Lines 327-336 sub _add_rowdata { Link Here
327
    my ($row, $record)=@_;
327
    my ($row, $record)=@_;
328
    my %fetch= (
328
    my %fetch= (
329
        title => 'biblio.title',
329
        title => 'biblio.title',
330
        seriestitle => 'biblio.seriestitle',
331
        author => 'biblio.author',
330
        author => 'biblio.author',
332
        isbn =>'biblioitems.isbn',
331
        isbn =>'biblioitems.isbn',
333
        issn =>'biblioitems.issn',
334
        lccn =>'biblioitems.lccn', #LC control number (not call number)
332
        lccn =>'biblioitems.lccn', #LC control number (not call number)
335
        edition =>'biblioitems.editionstatement',
333
        edition =>'biblioitems.editionstatement',
336
        date => 'biblio.copyrightdate', #MARC21
334
        date => 'biblio.copyrightdate', #MARC21
Lines 714-819 sub Z3950SearchAuth { Link Here
714
    );
712
    );
715
}
713
}
716
714
717
sub RunZ3950 {
718
    my ( $server_ids, $query, $options ) = @_;
719
720
    $options = {
721
        offset => 0,
722
        fetch => 20,
723
        on_error => sub {},
724
        on_hit => sub {},
725
        %{ $options || {} }
726
    };
727
728
    my $schema = Koha::Database->new->schema;
729
    my $stats = {
730
        num_fetched => {
731
            map { $_ => 0 } @$server_ids
732
        },
733
        num_hits => {
734
            map { $_ => 0 } @$server_ids
735
        },
736
        total_fetched => 0,
737
        total_hits => 0,
738
    };
739
    my $start = clock_gettime( CLOCK_MONOTONIC );
740
    my @servers;
741
742
    foreach my $server ( $schema->resultset('Z3950server')->search( { id => $server_ids } )->all ) {
743
        my $zoptions = ZOOM::Options->new();
744
        $zoptions->option( 'async', 1 );
745
        $zoptions->option( 'elementSetName', 'F' );
746
        $zoptions->option( 'databaseName',   $server->db );
747
        $zoptions->option( 'user', $server->userid ) if $server->userid;
748
        $zoptions->option( 'password', $server->password ) if $server->password;
749
        $zoptions->option( 'preferredRecordSyntax', $server->syntax );
750
        $zoptions->option( 'timeout', $server->timeout ) if $server->timeout;
751
752
        my $connection = ZOOM::Connection->create($zoptions);
753
        $connection->connect( $server->host, $server->port );
754
755
        push @servers, {
756
            connection => $connection,
757
            id => $server->id,
758
            host => $server->host,
759
            name => $server->name,
760
            encoding => ( $server->encoding ? $server->encoding : "iso-5426" ),
761
            results => $connection->search_pqf( $query ), # Starts the search
762
        };
763
    }
764
765
    my $servers_left = scalar @servers;
766
    my $total_raw_size = 0;
767
768
    while ( $servers_left ) {
769
        my $i;
770
771
        # Read pending events from servers until one finishes
772
        while ( ( $i = ZOOM::event( [ map { $_->{connection} } @servers ] ) ) != 0 ) {
773
            last if $servers[ $i - 1 ]->{connection}->last_event() == ZOOM::Event::ZEND;
774
        }
775
776
        $servers_left--;
777
        my $server = $servers[ --$i ];
778
        my $exception = $server->{connection}->exception(); #ignores errmsg, addinfo, diagset
779
780
        if ($exception) {
781
            $options->{on_error}->( $server, $exception );
782
        } else {
783
            my $num_results = $stats->{num_hits}->{ $server->{id} } = $server->{results}->size;
784
            my $num_fetched = $stats->{num_fetched}->{ $server->{id} } = ( $options->{offset} + $options->{fetch} ) < $num_results ? $options->{fetch} : $num_results;
785
786
            $stats->{total_hits} += $num_results;
787
            $stats->{total_fetched} += $num_fetched;
788
789
            next if ( !$num_results );
790
791
            my $hits = $server->{results}->records( $options->{offset}, $num_fetched, 1 );
792
793
            if ( !@$hits ) {
794
                $options->{on_error}->( $server, $server->{connection}->exception() ) if ( $server->{connection}->exception() );
795
                next;
796
            }
797
798
            foreach my $j ( 0..$#$hits ) {
799
                $total_raw_size += length $hits->[$j]->raw();
800
                my ($marcrecord) = MarcToUTF8Record( $hits->[$j]->raw(), C4::Context->preference('marcflavour'), $server->{encoding} ); #ignores charset return values
801
                my $metadata = {};
802
                _add_rowdata( $metadata, $marcrecord );
803
                $options->{on_hit}->( $server, {
804
                    index => $options->{offset} + $j,
805
                    record => $marcrecord,
806
                    metadata => $metadata,
807
                } );
808
            }
809
        }
810
    }
811
812
    $stats->{time} = clock_gettime( CLOCK_MONOTONIC ) - $start;
813
814
    return $stats;
815
}
816
817
1;
715
1;
818
__END__
716
__END__
819
717
(-)a/C4/Service.pm (+1 lines)
Lines 57-62 our ( $query, $cookie ); Link Here
57
57
58
sub _output {
58
sub _output {
59
    my ( $response, $status ) = @_;
59
    my ( $response, $status ) = @_;
60
    binmode STDOUT, ':encoding(UTF-8)';
60
61
61
    if ( $query->param( 'callback' ) ) {
62
    if ( $query->param( 'callback' ) ) {
62
        output_with_http_headers $query, $cookie, $query->param( 'callback' ) . '(' . $response->output . ');', 'js';
63
        output_with_http_headers $query, $cookie, $query->param( 'callback' ) . '(' . $response->output . ');', 'js';
(-)a/Koha/MetaSearcher.pm (+351 lines)
Line 0 Link Here
1
package Koha::MetaSearcher;
2
3
# Copyright 2014 ByWater Solutions
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
22
use base 'Class::Accessor';
23
24
use C4::Charset qw( MarcToUTF8Record );
25
use C4::Search qw(); # Purely for new_record_from_zebra
26
use DBIx::Class::ResultClass::HashRefInflator;
27
use IO::Select;
28
use Koha::Cache;
29
use Koha::Database;
30
use Koha::MetadataRecord;
31
use MARC::File::XML;
32
use Storable qw( store_fd fd_retrieve );
33
use Time::HiRes qw( clock_gettime CLOCK_MONOTONIC );
34
use UUID;
35
use ZOOM;
36
37
use sort 'stable';
38
39
__PACKAGE__->mk_accessors( qw( fetch offset on_error resultset ) );
40
41
sub new {
42
    my ( $class, $options ) = @_;
43
44
    my ( $uuid, $uuidstring );
45
    UUID::generate($uuid);
46
    UUID::unparse( $uuid, $uuidstring );
47
48
    return bless {
49
        offset => 0,
50
        fetch => 100,
51
        on_error => sub {},
52
        results => [],
53
        resultset => $uuidstring,
54
        %{ $options || {} }
55
    }, $class;
56
}
57
58
sub handle_hit {
59
    my ( $self, $index, $server, $marcrecord ) = @_;
60
61
    my $record = Koha::MetadataRecord->new( { schema => 'marc', record => $marcrecord } );
62
63
    my %fetch = (
64
        title => 'biblio.title',
65
        seriestitle => 'biblio.seriestitle',
66
        author => 'biblio.author',
67
        isbn =>'biblioitems.isbn',
68
        issn =>'biblioitems.issn',
69
        lccn =>'biblioitems.lccn', #LC control number (not call number)
70
        edition =>'biblioitems.editionstatement',
71
        date => 'biblio.copyrightdate', #MARC21
72
        date2 => 'biblioitems.publicationyear', #UNIMARC
73
    );
74
75
    my $metadata = {};
76
    while ( my ( $key, $kohafield ) = each %fetch ) {
77
        $metadata->{$key} = $record->getKohaField($kohafield);
78
    }
79
    $metadata->{date} //= $metadata->{date2};
80
81
    push @{ $self->{results} }, {
82
        server => $server,
83
        index => $index,
84
        record => $marcrecord,
85
        metadata => $metadata,
86
    };
87
}
88
89
sub search {
90
    my ( $self, $server_ids, $query ) = @_;
91
92
    my $resultset_expiry = 300;
93
94
    my $cache;
95
    eval { $cache = Koha::Cache->new(); };
96
    my $schema = Koha::Database->new->schema;
97
    my $stats = {
98
        num_fetched => {
99
            map { $_ => 0 } @$server_ids
100
        },
101
        num_hits => {
102
            map { $_ => 0 } @$server_ids
103
        },
104
        total_fetched => 0,
105
        total_hits => 0,
106
    };
107
    my $start = clock_gettime( CLOCK_MONOTONIC );
108
    my $select = IO::Select->new;
109
    my @worker_fhs;
110
111
    my @cached_sets;
112
    my @servers;
113
114
    foreach my $server_id ( @$server_ids ) {
115
        if ( $server_id =~ /^\d+$/ ) {
116
            # Z39.50 server
117
            my $server = $schema->resultset('Z3950server')->find(
118
                { id => $server_id },
119
                { result_class => 'DBIx::Class::ResultClass::HashRefInflator' },
120
            );
121
            $server->{type} = 'z3950';
122
123
            push @servers, $server;
124
        } elsif ( $server_id =~ /(\w+)(?::(\w+))?/ ) {
125
            # Special server
126
            push @servers, {
127
                type => $1,
128
                extra => $2,
129
                id => $server_id,
130
                host => $server_id,
131
                name => $server_id,
132
            };
133
        }
134
    }
135
136
    # HashRefInflator is used so that the information will survive into the fork
137
    foreach my $server ( @servers ) {
138
        if ( $cache ) {
139
            my $set = $cache->get_from_cache( 'z3950-resultset-' . $self->resultset . '-' . $server->{id} );
140
            if ( ref($set) eq 'HASH' ) {
141
                $set->{server} = $server;
142
                push @cached_sets, $set;
143
                next;
144
            }
145
        }
146
147
        $select->add( $self->_start_worker( $server, $query ) );
148
    }
149
150
    # Handle these while the servers are searching
151
    foreach my $set ( @cached_sets ) {
152
        $self->_handle_hits( $stats, $set );
153
    }
154
155
    while ( $select->count ) {
156
        foreach my $readfh ( $select->can_read() ) {
157
            my $result = fd_retrieve( $readfh );
158
159
            $select->remove( $readfh );
160
            close $readfh;
161
            wait;
162
163
            next if ( ref $result ne 'HASH' );
164
165
            if ( $result->{error} ) {
166
                $self->{on_error}->( $result->{server}, $result->{error} );
167
                next;
168
            }
169
170
            $self->_handle_hits( $stats, $result );
171
172
            if ( $cache ) {
173
                $cache->set_in_cache( 'z3950-resultset-' . $self->resultset . '-' . $result->{server}->{id}, {
174
                    hits => $result->{hits},
175
                    num_fetched => $result->{num_fetched},
176
                    num_hits => $result->{num_hits},
177
                }, $resultset_expiry );
178
            }
179
        }
180
    }
181
182
    $stats->{time} = clock_gettime( CLOCK_MONOTONIC ) - $start;
183
184
    return $stats;
185
}
186
187
sub _start_worker {
188
    my ( $self, $server, $query ) = @_;
189
    pipe my $readfh, my $writefh;
190
191
    # Accessing the cache or Koha database after the fork is risky, so get any resources we need
192
    # here.
193
    my $pid;
194
    my $marcflavour = C4::Context->preference('marcflavour');
195
196
    if ( ( $pid = fork ) ) {
197
        # Parent process
198
        close $writefh;
199
200
        return $readfh;
201
    } elsif ( !defined $pid ) {
202
        # Error
203
204
        $self->{on_error}->( $server, 'Failed to fork' );
205
        return;
206
    }
207
208
    close $readfh;
209
    my $connection;
210
    my ( $num_hits, $num_fetched, $hits, $results );
211
212
    eval {
213
        if ( $server->{type} eq 'z3950' ) {
214
            my $zoptions = ZOOM::Options->new();
215
            $zoptions->option( 'elementSetName', 'F' );
216
            $zoptions->option( 'databaseName',   $server->{db} );
217
            $zoptions->option( 'user', $server->{userid} ) if $server->{userid};
218
            $zoptions->option( 'password', $server->{password} ) if $server->{password};
219
            $zoptions->option( 'preferredRecordSyntax', $server->{syntax} );
220
            $zoptions->option( 'timeout', $server->{timeout} ) if $server->{timeout};
221
222
            $connection = ZOOM::Connection->create($zoptions);
223
224
            $connection->connect( $server->{host}, $server->{port} );
225
            $results = $connection->search_pqf( $query ); # Starts the search
226
        } elsif ( $server->{type} eq 'koha' ) {
227
            $connection = C4::Context->Zconn( $server->{extra} );
228
            $results = $connection->search_pqf( $query ); # Starts the search
229
        } elsif ( $server->{type} eq 'batch' )  {
230
            $server->{encoding} = 'utf-8';
231
        }
232
    };
233
    if ($@) {
234
        store_fd {
235
            error => $connection ? $connection->exception() : $@,
236
            server => $server,
237
        }, $writefh;
238
        exit;
239
    }
240
241
    if ( $server->{type} eq 'batch' ) {
242
        # TODO: actually handle PQF
243
        $query =~ s/@\w+ (?:\d+=\d+ )?//g;
244
        $query =~ s/"//g;
245
246
        my $schema = Koha::Database->new->schema;
247
        $schema->storage->debug(1);
248
        my $match_condition = [ map +{ -like => '%' . $_ . '%' }, split( /\s+/, $query ) ];
249
        $hits = [ $schema->resultset('ImportRecord')->search(
250
            {
251
                import_batch_id => $server->{extra},
252
                -or => [
253
                    { 'import_biblios.title' => $match_condition },
254
                    { 'import_biblios.author' => $match_condition },
255
                    { 'import_biblios.isbn' => $match_condition },
256
                    { 'import_biblios.issn' => $match_condition },
257
                ],
258
            },
259
            {
260
                join => [ qw( import_biblios ) ],
261
                rows => $self->{fetch},
262
            }
263
        )->get_column( 'marc' )->all ];
264
265
        $num_hits = $num_fetched = scalar @$hits;
266
    } else {
267
        $num_hits = $results->size;
268
        $num_fetched = ( $self->{offset} + $self->{fetch} ) < $num_hits ? $self->{fetch} : $num_hits;
269
270
        $hits = [ map { $_->raw() } @{ $results->records( $self->{offset}, $num_fetched, 1 ) } ];
271
    }
272
273
    if ( !@$hits && $connection && $connection->exception() ) {
274
        store_fd {
275
            error => $connection->exception(),
276
            server => $server,
277
        }, $writefh;
278
        exit;
279
    }
280
281
    if ( $server->{type} eq 'koha' ) {
282
        $hits = [ map { C4::Search::new_record_from_zebra( $server->{extra}, $_ ) } @$hits ];
283
    } else {
284
        $hits = [ map { $self->_import_record( $_, $marcflavour, $server->{encoding} ? $server->{encoding} : "iso-5426" ) } @$hits ];
285
    }
286
287
    store_fd {
288
        hits => $hits,
289
        num_fetched => $num_fetched,
290
        num_hits => $num_hits,
291
        server => $server,
292
    }, $writefh;
293
294
    exit;
295
}
296
297
sub _import_record {
298
    my ( $self, $raw, $marcflavour, $encoding ) = @_;
299
300
    my ( $marcrecord ) = MarcToUTF8Record( $raw, $marcflavour, $encoding ); #ignores charset return values
301
302
    return $marcrecord;
303
}
304
305
sub _handle_hits {
306
    my ( $self, $stats, $set ) = @_;
307
308
    my $server = $set->{server};
309
310
    my $num_hits = $stats->{num_hits}->{ $server->{id} } = $set->{num_hits};
311
    my $num_fetched = $stats->{num_fetched}->{ $server->{id} } = $set->{num_fetched};
312
313
    $stats->{total_hits} += $num_hits;
314
    $stats->{total_fetched} += $num_fetched;
315
316
    foreach my $j ( 0..$#{ $set->{hits} } ) {
317
        $self->handle_hit( $self->{offset} + $j, $server, $set->{hits}->[$j] );
318
    }
319
}
320
321
sub sort {
322
    my ( $self, $key, $direction ) = @_;
323
324
    my $empty_flip = -1; # Determines the flip of ordering for records with empty sort keys.
325
326
    foreach my $hit ( @{ $self->{results} } ) {
327
        ( $hit->{sort_key} = $hit->{metadata}->{$key} || '' ) =~ s/\W//g;
328
    }
329
330
    $self->{results} = [ sort {
331
        # Sort empty records at the end
332
        return -$empty_flip unless $a->{sort_key};
333
        return $empty_flip unless $b->{sort_key};
334
335
        $direction * ( $a->{sort_key} cmp $b->{sort_key} );
336
    } @{ $self->{results} } ];
337
}
338
339
sub results {
340
    my ( $self, $offset, $length ) = @_;
341
342
    my @subset;
343
344
    foreach my $i ( $offset..( $offset + $length - 1 ) ) {
345
        push @subset, $self->{results}->[$i] if $self->{results}->[$i];
346
    }
347
348
    return @subset;
349
}
350
351
1;
(-)a/Koha/MetadataRecord.pm (+25 lines)
Lines 56-59 sub createMergeHash { Link Here
56
    }
56
    }
57
}
57
}
58
58
59
sub getKohaField {
60
    my ($self, $kohafield) = @_;
61
62
    if ($self->schema =~ m/marc/) {
63
        my $relations = C4::Context->marcfromkohafield->{''};
64
        my $tagfield = $relations->{$kohafield};
65
66
        return '' if ref($tagfield) ne 'ARRAY';
67
68
        my ($tag, $subfield) = @$tagfield;
69
        my @kohafield;
70
        foreach my $field ( $self->record->field($tag) ) {
71
            if ( $field->tag() < 10 ) {
72
                push @kohafield, $field->data();
73
            } else {
74
                foreach my $contents ( $field->subfield($subfield) ) {
75
                    push @kohafield, $contents;
76
                }
77
            }
78
        }
79
80
        return join ' | ', @kohafield;
81
    }
82
}
83
59
1;
84
1;
(-)a/cataloguing/editor.pl (+13 lines)
Lines 27-32 use C4::Auth; Link Here
27
use C4::Biblio;
27
use C4::Biblio;
28
use C4::Context;
28
use C4::Context;
29
use C4::Output;
29
use C4::Output;
30
use DBIx::Class::ResultClass::HashRefInflator;
31
use Koha::Database;
30
32
31
my $input = CGI->new;
33
my $input = CGI->new;
32
34
Lines 40-45 my ( $template, $loggedinuser, $cookie ) = get_template_and_user( Link Here
40
    }
42
    }
41
);
43
);
42
44
45
my $schema = Koha::Database->new->schema;
46
47
# Available import batches
48
$template->{VARS}->{editable_batches} = [ $schema->resultset('ImportBatch')->search(
49
    {
50
        batch_type => [ 'batch', 'webservice' ],
51
        import_status => 'staged',
52
    },
53
    { result_class => 'DBIx::Class::ResultClass::HashRefInflator' },
54
) ];
55
43
# Needed information for cataloging plugins
56
# Needed information for cataloging plugins
44
$template->{VARS}->{DefaultLanguageField008} = pack( 'A3', C4::Context->preference('DefaultLanguageField008') || 'eng' );
57
$template->{VARS}->{DefaultLanguageField008} = pack( 'A3', C4::Context->preference('DefaultLanguageField008') || 'eng' );
45
58
(-)a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/koha-backend.js (-1 / +20 lines)
Lines 1-3 Link Here
1
/**
2
 * Copyright 2015 ByWater Solutions
3
 *
4
 * This file is part of Koha.
5
 *
6
 * Koha is free software; you can redistribute it and/or modify it
7
 * under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation; either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * Koha is distributed in the hope that it will be useful, but
12
 * WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License
17
 * along with Koha; if not, see <http://www.gnu.org/licenses>.
18
 */
19
1
define( [ '/cgi-bin/koha/svc/cataloguing/framework?frameworkcode=&callback=define', 'marc-record' ], function( defaultFramework, MARC ) {
20
define( [ '/cgi-bin/koha/svc/cataloguing/framework?frameworkcode=&callback=define', 'marc-record' ], function( defaultFramework, MARC ) {
2
    var _authorised_values = defaultFramework.authorised_values;
21
    var _authorised_values = defaultFramework.authorised_values;
3
    var _frameworks = {};
22
    var _frameworks = {};
Lines 61-67 define( [ '/cgi-bin/koha/svc/cataloguing/framework?frameworkcode=&callback=defin Link Here
61
                record.loadMARCXML(data);
80
                record.loadMARCXML(data);
62
                callback(record);
81
                callback(record);
63
            } ).fail( function( data ) {
82
            } ).fail( function( data ) {
64
                callback( { data: { error: data } } );
83
                callback( { error: data } );
65
            } );
84
            } );
66
        },
85
        },
67
86
(-)a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/macros.js (-189 / +31 lines)
Lines 1-194 Link Here
1
define( [ 'widget' ], function( Widget ) {
1
/**
2
    function _setIndicators( editor, ind1, ind2 ) {
2
 * Copyright 2015 ByWater Solutions
3
        var info = editor.getLineInfo( editor.cm.getCursor() );
3
 *
4
        if (!info || !info.subfields) return false;
4
 * This file is part of Koha.
5
5
 *
6
        var cur = editor.cm.getCursor();
6
 * Koha is free software; you can redistribute it and/or modify it
7
7
 * under the terms of the GNU General Public License as published by
8
        var indicators = [ ind1 || info.contents.substring(4, 5) || '_', ind2 || info.contents.substring(6, 7) || '_' ];
8
 * the Free Software Foundation; either version 3 of the License, or
9
9
 * (at your option) any later version.
10
        editor.cm.replaceRange(
10
 *
11
            info.tagNumber + ' ' + indicators.join(' ') + ' ' + info.contents.substring(8),
11
 * Koha is distributed in the hope that it will be useful, but
12
            { line: cur.line, ch: 0 },
12
 * WITHOUT ANY WARRANTY; without even the implied warranty of
13
            { line: cur.line },
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
            'marcAware'
14
 * GNU General Public License for more details.
15
        );
15
 *
16
16
 * You should have received a copy of the GNU General Public License
17
        return true;
17
 * along with Koha; if not, see <http://www.gnu.org/licenses>.
18
    }
18
 */
19
19
20
    var _commandGenerators = [
20
define( [ 'macros/its', 'macros/rancor' ], function( ITSMacro, RancorMacro ) {
21
        [ /^copy field data$/i, function() {
22
            return function( editor, state ) {
23
                var info = editor.getLineInfo( editor.cm.getCursor() );
24
                if (!info) return false;
25
26
                if (info.subfields) {
27
                    state.clipboard = info.contents.substring(4);
28
                } else {
29
                    state.clipboard = info.contents.substring(8);
30
                }
31
            };
32
        } ],
33
        [ /^copy subfield data$/i, function() {
34
            return function( editor, state ) {
35
                var info = editor.getLineInfo( editor.cm.getCursor() );
36
                if (!info) return false;
37
38
                var cur = editor.cm.getCursor();
39
40
                if (info.subfields) {
41
                    for (var i = 0; i < info.subfields.length; i++) {
42
                        var end = i == info.subfields.length - 1 ? info.contents.length : info.subfields[i+1].ch;
43
                        if (cur.ch > end) continue;
44
45
                        state.clipboard = info.contents.substring(info.subfields[i].ch + 3, end);
46
                        return;
47
                    }
48
                }
49
50
                return false;
51
            }
52
        } ],
53
        [ /^delete field$/i, function() {
54
            return function( editor, state ) {
55
                var cur = editor.cm.getCursor();
56
57
                editor.cm.replaceRange( "", { line: cur.line, ch: 0 }, { line: cur.line + 1, ch: 0 }, 'marcAware' );
58
            }
59
        } ],
60
        [ /^goto field end$/i, function() {
61
            return function( editor, state ) {
62
                editor.cm.setCursor( { line: editor.cm.lastLine() } );
63
            }
64
        } ],
65
        [ /^goto field (\w{3})$/i, function(field) {
66
            var matcher = new RegExp('^' + field + ' ');
67
            return function( editor, state ) {
68
                for ( var line = 0, contents; (contents = editor.cm.getLine(line)); line++ ) {
69
                    if ( matcher.exec( contents ) ) {
70
                        editor.cm.setCursor( { line: line, ch: 0 } );
71
                        return;
72
                    }
73
                }
74
75
                return false;
76
            }
77
        } ],
78
        [ /^goto subfield end$/i, function() {
79
            return function( editor, state ) {
80
                var cur = editor.cm.getCursor();
81
82
                editor.cm.setCursor( { line: cur.line } );
83
            }
84
        } ],
85
        [ /^goto subfield (\w)$/i, function( code ) {
86
            return function( editor, state ) {
87
                var info = editor.getLineInfo( editor.cm.getCursor() );
88
                if (!info || !info.subfields) return false;
89
90
                var cur = editor.cm.getCursor();
91
92
                for (var i = 0; i < info.subfields.length; i++) {
93
                    if ( info.subfields[i].code != code ) continue;
94
95
                    var end = i == info.subfields.length - 1 ? info.contents.length : info.subfields[i+1].ch;
96
                    editor.cm.setCursor( { line: cur.line, ch: end } );
97
                    return;
98
                }
99
100
                return false;
101
            }
102
        } ],
103
        [ /^insert (new )?field (\w{3}) data=(.*)/i, function(undef, field, data) {
104
            var new_contents = field + ( field < '100' ? ' ' : ' _ _ ' ) + data.replace(/\\([0-9a-z])/g, '$$$1 ');
105
            return function( editor, state ) {
106
                var line, contents;
107
108
                for ( line = 0; (contents = editor.cm.getLine(line)); line++ ) {
109
                    if ( contents && contents[0] > field[0] ) break;
110
                }
111
112
                if ( line > editor.cm.lastLine() ) {
113
                    new_contents = '\n' + new_contents;
114
                } else {
115
                    new_contents = new_contents + '\n';
116
                }
117
118
                editor.cm.replaceRange( new_contents, { line: line, ch: 0 }, null, 'marcAware' );
119
                editor.cm.setCursor( { line: line, ch: 0 } );
120
            }
121
        } ],
122
        [ /^insert (new )?subfield (\w) data=(.*)/i, function(undef, subfield, data) {
123
            return function( editor, state ) {
124
                editor.cm.replaceRange( '$' + subfield + ' ' + data, { line: editor.cm.getCursor().line }, null, 'marcAware' );
125
            }
126
        } ],
127
        [ /^paste$/i, function() {
128
            return function( editor, state ) {
129
                var cur = editor.cm.getCursor();
130
131
                editor.cm.replaceRange( state.clipboard, cur, null, 'marcAware' );
132
            }
133
        } ],
134
        [ /^set indicator([12])=([ _0-9])$/i, function( ind, value ) {
135
            return function( editor, state ) {
136
                return ind == '1' ? _setIndicators( editor, ind1, null ) : _setIndicators( editor, null, ind2 );
137
            }
138
        } ],
139
        [ /^set indicators=([ _0-9])([ _0-9])$/i, function( ind1, ind2 ) {
140
            return function( editor, state ) {
141
                return _setIndicators( editor, ind1, ind2 );
142
            }
143
        } ],
144
    ];
145
146
    var Macros = {
21
    var Macros = {
147
        Compile: function( macro ) {
22
        formats: {
148
            var result = { commands: [], errors: [] };
23
            its: {
149
24
                description: 'TLC® ITS',
150
            $.each( macro.split(/\r\n|\n/), function( line, contents ) {
25
                Run: ITSMacro.Run,
151
                var command;
26
            },
152
27
            rancor: {
153
                if ( contents.match(/^\s*$/) ) return;
28
                description: 'Rancor',
154
29
                Run: RancorMacro.Run,
155
                $.each( _commandGenerators, function( undef, gen ) {
30
            },
156
                    var match;
157
158
                    if ( !( match = gen[0].exec( contents ) ) ) return;
159
160
                    command = gen[1].apply(null, match.slice(1));
161
                    return false;
162
                } );
163
164
                if ( !command ) {
165
                    result.errors.push( { line: line, error: 'unrecognized' } );
166
                }
167
168
                result.commands.push( { func: command, orig: contents, line: line } );
169
            } );
170
171
            return result;
172
        },
31
        },
173
        Run: function( editor, macro ) {
32
        Run: function( editor, format, macro ) {
174
            var compiled = Macros.Compile(macro);
33
            return Macros.formats[format].Run( editor, macro );
175
            if ( compiled.errors.length ) return { errors: compiled.errors };
176
            var state = {
177
                clipboard: '',
178
            };
179
180
            var result = { errors: [] };
181
182
            editor.cm.operation( function() {
183
                $.each( compiled.commands, function( undef, command ) {
184
                    if ( command.func( editor, state ) === false ) {
185
                        result.errors.push( { line: command.line, error: 'failed' } );
186
                        return false;
187
                    }
188
                } );
189
            } );
190
191
            return result;
192
        },
34
        },
193
    };
35
    };
194
36
(-)a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/macros/its.js (+208 lines)
Line 0 Link Here
1
/**
2
 * Copyright 2015 ByWater Solutions
3
 *
4
 * This file is part of Koha.
5
 *
6
 * Koha is free software; you can redistribute it and/or modify it
7
 * under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation; either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * Koha is distributed in the hope that it will be useful, but
12
 * WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License
17
 * along with Koha; if not, see <http://www.gnu.org/licenses>.
18
 */
19
20
define( function() {
21
    var NAV_FAILED = new Object();
22
    var NAV_SUCCEEDED = new Object();
23
24
    var _commandGenerators = [
25
        [ /^copy field data$/i, function() {
26
            return function( editor, state ) {
27
                if ( state.field == null ) return;
28
29
                return state.field.getText();
30
            };
31
        } ],
32
        [ /^copy subfield data$/i, function() {
33
            return function( editor, state ) {
34
                if ( state.field == null ) return;
35
36
                var cur = editor.getCursor();
37
                var subfields = state.field.getSubfields();
38
39
                for (var i = 0; i < subfields.length; i++) {
40
                    if ( cur.ch > subfields[i].end ) continue;
41
42
                    state.clipboard = subfields[i].text;
43
                    return;
44
                }
45
46
                return false;
47
            }
48
        } ],
49
        [ /^del(ete)? field$/i, function() {
50
            return function( editor, state ) {
51
                if ( state.field == null ) return;
52
53
                state.field.delete();
54
                return NAV_FAILED;
55
            }
56
        } ],
57
        [ /^goto field end$/i, function() {
58
            return function( editor, state ) {
59
                if ( state.field == null ) return NAV_FAILED;
60
                var cur = editor.cm.getCursor();
61
62
                editor.cm.setCursor( { line: cur.line } );
63
                return NAV_SUCCEEDED;
64
            }
65
        } ],
66
        [ /^goto field (\w{3})$/i, function(tag) {
67
            return function( editor, state ) {
68
                var field = editor.getFirstField(tag);
69
                if ( field == null ) return NAV_FAILED;
70
71
                field.focus();
72
                return NAV_SUCCEEDED;
73
            }
74
        } ],
75
        [ /^goto subfield end$/i, function() {
76
            return function( editor, state ) {
77
                if ( state.field == null ) return NAV_FAILED;
78
79
                var cur = editor.getCursor();
80
                var subfields = state.field.getSubfields();
81
82
                for (var i = 0; i < subfields.length; i++) {
83
                    if ( cur.ch > subfields[i].end ) continue;
84
85
                    subfield.focusEnd();
86
                    return NAV_SUCCEEDED;
87
                }
88
89
                return NAV_FAILED;
90
            }
91
        } ],
92
        [ /^goto subfield (\w)$/i, function( code ) {
93
            return function( editor, state ) {
94
                if ( state.field == null ) return NAV_FAILED;
95
96
                var subfield = state.field.getFirstSubfield( code );
97
                if ( subfield == null ) return NAV_FAILED;
98
99
                subfield.focus();
100
                return NAV_SUCCEEDED;
101
            }
102
        } ],
103
        [ /^insert (new )?field (\w{3}) data=(.*)/i, function(undef, tag, text) {
104
            text = text.replace(/\\([0-9a-z])/g, '$$$1 ');
105
            return function( editor, state ) {
106
                editor.createFieldGrouped(tag).setText(text).focus();
107
                return NAV_SUCCEEDED;
108
            }
109
        } ],
110
        [ /^insert (new )?subfield (\w) data=(.*)/i, function(undef, code, text) {
111
            return function( editor, state ) {
112
                if ( state.field == null ) return;
113
114
                state.field.appendSubfield(code).setText(text);
115
            }
116
        } ],
117
        [ /^paste$/i, function() {
118
            return function( editor, state ) {
119
                var cur = editor.cm.getCursor();
120
121
                editor.cm.replaceRange( state.clipboard, cur, null, 'marcAware' );
122
            }
123
        } ],
124
        [ /^set indicator([12])=([ _0-9])$/i, function( ind, value ) {
125
            return function( editor, state ) {
126
                if ( state.field == null ) return;
127
                if ( state.field.isControlField ) return false;
128
129
                if ( ind == '1' ) {
130
                    state.field.setIndicator1(value);
131
                    return true;
132
                } else if ( ind == '2' ) {
133
                    state.field.setIndicator2(value);
134
                    return true;
135
                } else {
136
                    return false;
137
                }
138
            }
139
        } ],
140
        [ /^set indicators=([ _0-9])([ _0-9])?$/i, function( ind1, ind2 ) {
141
            return function( editor, state ) {
142
                if ( state.field == null ) return;
143
                if ( state.field.isControlField ) return false;
144
145
                state.field.setIndicator1(ind1);
146
                state.field.setIndicator2(ind2 || '_');
147
            }
148
        } ],
149
    ];
150
151
    var ITSMacro = {
152
        Compile: function( macro ) {
153
            var result = { commands: [], errors: [] };
154
155
            $.each( macro.split(/\r\n|\n/), function( line, contents ) {
156
                var command;
157
158
                if ( contents.match(/^\s*$/) ) return;
159
160
                $.each( _commandGenerators, function( undef, gen ) {
161
                    var match;
162
163
                    if ( !( match = gen[0].exec( contents ) ) ) return;
164
165
                    command = gen[1].apply(null, match.slice(1));
166
                    return false;
167
                } );
168
169
                if ( !command ) {
170
                    result.errors.push( { line: line, error: 'unrecognized' } );
171
                }
172
173
                result.commands.push( { func: command, orig: contents, line: line } );
174
            } );
175
176
            return result;
177
        },
178
        Run: function( editor, macro ) {
179
            var compiled = ITSMacro.Compile(macro);
180
            if ( compiled.errors.length ) return { errors: compiled.errors };
181
            var state = {
182
                clipboard: '',
183
                field: null,
184
            };
185
186
            var run_result = { errors: [] };
187
188
            editor.cm.operation( function() {
189
                $.each( compiled.commands, function( undef, command ) {
190
                    var result = command.func( editor, state );
191
192
                    if ( result == NAV_FAILED ) {
193
                        state.field = null;
194
                    } else if ( result == NAV_SUCCEEDED ) {
195
                        state.field = editor.getCurrentField();
196
                    } else if ( result === false ) {
197
                        run_result.errors.push( { line: command.line, error: 'failed' } );
198
                        return false;
199
                    }
200
                } );
201
            } );
202
203
            return run_result;
204
        },
205
    };
206
207
    return ITSMacro;
208
} );
(-)a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/macros/rancor.js (+277 lines)
Line 0 Link Here
1
/**
2
 * Copyright 2015 ByWater Solutions
3
 *
4
 * This file is part of Koha.
5
 *
6
 * Koha is free software; you can redistribute it and/or modify it
7
 * under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation; either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * Koha is distributed in the hope that it will be useful, but
12
 * WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License
17
 * along with Koha; if not, see <http://www.gnu.org/licenses>.
18
 */
19
20
define( [ 'marc-editor' ], function( MARCEditor ) {
21
    // These are the generators for targets that appear on the left-hand side of an assignment.
22
    var _lhsGenerators = [
23
        // Field; will replace the entire contents of the tag except for indicators.
24
        // Examples:
25
        //   * 245 - will return the first 245 tag it finds, or create a new one
26
        //   * new 245 - will always create a new 245
27
        //   * new 245 grouped - will always create a new 245, and insert it at the end of the 2xx
28
        //     block
29
        [ /^(new )?(\w{3})( (grouped))?$/, function( forceCreate, tag, position, positionGrouped ) {
30
            if ( !forceCreate && positionGrouped ) return null;
31
32
            // The extra argument allows the delete command to prevent this from needlessly creating
33
            // a tag that it is about to delete.
34
            return function( editor, state, extra ) {
35
                extra = extra || {};
36
37
                if ( !forceCreate ) {
38
                    var result = editor.getFirstField(tag);
39
40
                    if ( result != null || extra.dontCreate ) return result;
41
                }
42
43
                if ( positionGrouped ) {
44
                    return editor.createFieldGrouped(tag);
45
                } else {
46
                    return editor.createFieldOrdered(tag);
47
                }
48
            }
49
        } ],
50
51
        // This regex is a little complicated, but allows for the following possibilities:
52
        //   * 245a - Finds the first 245 field, then tries to find an a subfield within it. If none
53
        //            exists, it is created. Will still fail if there is no 245 field.
54
        //   * new 245a - always creates a new a subfield.
55
        //   * new 245a at end - does the same as the above.
56
        //   * $a or new $a - does the same as the above, but for the last-used tag.
57
        //   * new 245a after b - creates a new subfield, placing it after the first subfield $b.
58
        [ /^(new )?(\w{3}|\$)(\w)( (at end)| after (\w))?$/, function( forceCreate, tag, code, position, positionAtEnd, positionAfterSubfield ) {
59
            if ( tag != '$' && tag < '010' ) return null;
60
            if ( !forceCreate && position ) return null;
61
62
            return function( editor, state, extra ) {
63
                extra = extra || {};
64
65
                var field;
66
67
                if ( tag == '$' ) {
68
                    field = state.field;
69
                } else {
70
                    field = editor.getFirstField(tag);
71
                }
72
                if ( field == null || field.isControlField ) return null;
73
74
                if ( !forceCreate ) {
75
                    var subfield = field.getFirstSubfield(code)
76
77
                    if ( subfield != null || extra.dontCreate ) return subfield;
78
                }
79
80
                if ( !position || position == ' at end' ) {
81
                    return field.appendSubfield(code);
82
                } else if ( positionAfterSubfield ) {
83
                    var afterSubfield = field.getFirstSubfield(positionAfterSubfield);
84
85
                    if ( afterSubfield == null ) return null;
86
87
                    return field.insertSubfield( code, afterSubfield.index + 1 );
88
                }
89
            }
90
        } ],
91
92
        // Can set indicatators either for a particular field or the last-used tag.
93
        [ /^((\w{3}) )?indicators$/, function( undef, tag ) {
94
            if ( tag && tag < '010' ) return null;
95
96
            return function( editor, state ) {
97
                var field;
98
99
                if ( tag == null ) {
100
                    field = state.field;
101
                } else {
102
                    field = editor.getFirstField(tag);
103
                }
104
                if ( field == null || field.isControlField ) return null;
105
106
                return {
107
                    field: field,
108
                    setText: function( text ) {
109
                        field.setIndicator1( text.substr( 0, 1 ) );
110
                        field.setIndicator2( text.substr( 1, 1 ) );
111
                    }
112
                };
113
            }
114
        } ],
115
    ];
116
117
    // These patterns, on the other hand, appear inside interpolations on the right hand side.
118
    var _rhsGenerators = [
119
        [ /^(\w{3})$/, function( tag ) {
120
            return function( editor, state, extra ) {
121
                return editor.getFirstField(tag);
122
            }
123
        } ],
124
        [ /^(\w{3})(\w)$/, function( tag, code ) {
125
            if ( tag < '010' ) return null;
126
127
            return function( editor, state, extra ) {
128
                extra = extra || {};
129
130
                var field = editor.getFirstField(tag);
131
                if ( field == null ) return null;
132
133
                return field.getFirstSubfield(code);
134
            }
135
        } ],
136
    ];
137
138
    var _commandGenerators = [
139
        [ /^delete (.+)$/, function( target ) {
140
            var target_closure = _generate( _lhsGenerators, target );
141
            if ( !target_closure ) return null;
142
143
            return function( editor, state ) {
144
                var target = target_closure( editor, state, { dontCreate: true } );
145
                if ( target == null ) return;
146
                if ( !target.delete ) return false;
147
148
                state.field = null; // As other fields may have been invalidated
149
                target.delete();
150
            }
151
        } ],
152
        [ /^([^=]+)=([^=]*)$/, function( lhs_desc, rhs_desc ) {
153
            var lhs_closure = _generate( _lhsGenerators, lhs_desc );
154
            if ( !lhs_closure ) return null;
155
156
            var rhs_closure = _generateInterpolation( _rhsGenerators, rhs_desc );
157
            if ( !rhs_closure ) return null;
158
159
            return function( editor, state ) {
160
                var lhs = lhs_closure( editor, state );
161
                if ( lhs == null ) return;
162
163
                state.field = lhs.field || lhs;
164
165
                try {
166
                    return lhs.setText( rhs_closure( editor, state ) );
167
                } catch (e) {
168
                    if ( e instanceof MARCEditor.FieldError ) {
169
                        return false;
170
                    } else {
171
                        throw e;
172
                    }
173
                }
174
            };
175
        } ],
176
    ];
177
178
    function _generate( set, contents ) {
179
        var closure;
180
181
        if ( contents.match(/^\s*$/) ) return;
182
183
        $.each( set, function( undef, gen ) {
184
            var match;
185
186
            if ( !( match = gen[0].exec( contents ) ) ) return;
187
188
            closure = gen[1].apply(null, match.slice(1));
189
            return false;
190
        } );
191
192
        return closure;
193
    }
194
195
    function _generateInterpolation( set, contents ) {
196
        // While this regex will not match at all for an empty string, that just leaves an empty
197
        // parts array which yields an empty string (which is what we want.)
198
        var matcher = /\{([^}]+)\}|([^{]+)/g;
199
        var match;
200
201
        var parts = [];
202
203
        while ( ( match = matcher.exec(contents) ) ) {
204
            var closure;
205
            if ( match[1] ) {
206
                // Found an interpolation
207
                var rhs_closure = _generate( set, match[1] );
208
                if ( rhs_closure == null ) return null;
209
210
                closure = ( function(rhs_closure) { return function( editor, state ) {
211
                    var rhs = rhs_closure( editor, state );
212
213
                    return rhs ? rhs.getText() : '';
214
                } } )( rhs_closure );
215
            } else {
216
                // Plain text (needs artificial closure to keep match)
217
                closure = ( function(text) { return function() { return text }; } )( match[2] );
218
            }
219
220
            parts.push( closure );
221
        }
222
223
        return function( editor, state ) {
224
            var result = '';
225
            $.each( parts, function( i, part ) {
226
                result += part( editor, state );
227
            } );
228
229
            return result;
230
        };
231
    }
232
233
    var RancorMacro = {
234
        Compile: function( macro ) {
235
            var result = { commands: [], errors: [] };
236
237
            $.each( macro.split(/\r\n|\n/), function( line, contents ) {
238
                contents = contents.replace( /#.*$/, '' );
239
                if ( contents.match(/^\s*$/) ) return;
240
241
                var command = _generate( _commandGenerators, contents );
242
243
                if ( !command ) {
244
                    result.errors.push( { line: line, error: 'unrecognized' } );
245
                }
246
247
                result.commands.push( { func: command, orig: contents, line: line } );
248
            } );
249
250
            return result;
251
        },
252
        Run: function( editor, macro ) {
253
            var compiled = RancorMacro.Compile(macro);
254
            if ( compiled.errors.length ) return { errors: compiled.errors };
255
            var state = {
256
                field: null,
257
            };
258
259
            var run_result = { errors: [] };
260
261
            editor.cm.operation( function() {
262
                $.each( compiled.commands, function( undef, command ) {
263
                    var result = command.func( editor, state );
264
265
                    if ( result === false ) {
266
                        run_result.errors.push( { line: command.line, error: 'failed' } );
267
                        return false;
268
                    }
269
                } );
270
            } );
271
272
            return run_result;
273
        },
274
    };
275
276
    return RancorMacro;
277
} );
(-)a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/marc-editor.js (-167 / +495 lines)
Lines 1-38 Link Here
1
/**
2
 * Copyright 2015 ByWater Solutions
3
 *
4
 * This file is part of Koha.
5
 *
6
 * Koha is free software; you can redistribute it and/or modify it
7
 * under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation; either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * Koha is distributed in the hope that it will be useful, but
12
 * WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License
17
 * along with Koha; if not, see <http://www.gnu.org/licenses>.
18
 */
19
1
define( [ 'marc-record', 'koha-backend', 'preferences', 'text-marc', 'widget' ], function( MARC, KohaBackend, Preferences, TextMARC, Widget ) {
20
define( [ 'marc-record', 'koha-backend', 'preferences', 'text-marc', 'widget' ], function( MARC, KohaBackend, Preferences, TextMARC, Widget ) {
2
    var NOTIFY_TIMEOUT = 250;
21
    var NOTIFY_TIMEOUT = 250;
3
22
4
    function editorCursorActivity( cm ) {
23
    function editorCursorActivity( cm ) {
5
        var editor = cm.marceditor;
24
        var editor = cm.marceditor;
6
        if ( editor.textMode ) return;
25
        var field = editor.getCurrentField();
7
26
        if ( !field ) return;
8
        $('#status-tag-info').empty();
9
        $('#status-subfield-info').empty();
10
11
        var info = editor.getLineInfo( cm.getCursor() );
12
13
        if ( !info ) return; // No tag at all on this line
14
15
        var taginfo = KohaBackend.GetTagInfo( '', info.tagNumber );
16
        $('#status-tag-info').html( '<strong>' + info.tagNumber + ':</strong> ' );
17
18
        if ( taginfo ) {
19
            $('#status-tag-info').append( taginfo.lib );
20
27
21
            if ( !info.currentSubfield ) return; // No current subfield
28
        // Set overwrite mode for tag numbers/indicators and contents of fixed fields
22
29
        if ( field.isControlField || cm.getCursor().ch < 8 ) {
23
            var subfieldinfo = taginfo.subfields[info.currentSubfield];
30
            cm.toggleOverwrite(true);
24
            $('#status-subfield-info').html( '<strong>$' + info.currentSubfield + ':</strong> ' );
25
26
            if ( subfieldinfo ) {
27
                $('#status-subfield-info').append( subfieldinfo.lib );
28
            } else {
29
                $('#status-subfield-info').append( '<em>' + _("Unknown subfield") + '</em>' );
30
            }
31
        } else {
31
        } else {
32
            $('#status-tag-info').append( '<em>' + _("Unknown tag") + '</em>' );
32
            cm.toggleOverwrite(false);
33
        }
33
        }
34
35
        editor.onCursorActivity();
34
    }
36
    }
35
37
38
    // This function exists to prevent inserting or partially deleting text that belongs to a
39
    // widget. The 'marcAware' change source exists for other parts of the editor code to bypass
40
    // this check.
36
    function editorBeforeChange( cm, change ) {
41
    function editorBeforeChange( cm, change ) {
37
        var editor = cm.marceditor;
42
        var editor = cm.marceditor;
38
        if ( editor.textMode || change.origin == 'marcAware' ) return;
43
        if ( editor.textMode || change.origin == 'marcAware' ) return;
Lines 62-76 define( [ 'marc-record', 'koha-backend', 'preferences', 'text-marc', 'widget' ], Link Here
62
            }
67
            }
63
68
64
            var startLine, endLine;
69
            var startLine, endLine;
65
            if ( change.text.length == 2 && from.line == to.line && from.ch == to.ch) {
70
            if ( change.text.length == 2 && from.line == to.line && from.ch == to.ch ) {
66
                if ( from.ch == 0 ) {
71
                if ( from.ch == 0 ) {
67
                    startLine = endLine = from.line;
72
                    startLine = endLine = from.line;
68
                } else if ( from.ch == cm.getLine(from.line).length ){
73
                } else if ( from.ch == cm.getLine(from.line).length ){
69
                    startLine = endLine = from.line + 1;
74
                    startLine = endLine = from.line + 1;
70
                }
75
                }
71
            } else {
76
            } else {
72
                startLine = (from.ch == cm.getLine(from.line).length && from.line < to.line) ? Math.min(cm.lastLine(), from.line + 1) : from.line;
77
                startLine = ( from.ch == cm.getLine( from.line ).length && from.line < to.line ) ? Math.min( cm.lastLine(), from.line + 1 ) : from.line;
73
                endLine = ((to.ch == 0 && from.line < to.line) ? Math.max(to.line - 1, 0) : to.line) + change.text.length - 1;
78
                endLine = ( ( to.ch == 0 && from.line < to.line ) ? Math.max( to.line - 1, 0 ) : to.line ) + change.text.length - 1;
74
            }
79
            }
75
80
76
            for ( var line = startLine; line <= endLine; line++ ) {
81
            for ( var line = startLine; line <= endLine; line++ ) {
Lines 87-111 define( [ 'marc-record', 'koha-backend', 'preferences', 'text-marc', 'widget' ], Link Here
87
    }
92
    }
88
93
89
    // Editor helper functions
94
    // Editor helper functions
90
    function activateTabPosition( cm, cur, idx ) {
95
    function activateTabPosition( cm, pos, idx ) {
91
        cm.setCursor( cur );
96
        // Allow tabbing to as-yet-nonexistent positions
92
        Widget.ActivateAt( cm, cur, idx );
97
        var lenDiff = pos.ch - cm.getLine( pos.line ).length;
98
        if ( lenDiff > 0 ) {
99
            var extra = '';
100
            while ( lenDiff-- > 0 ) extra += ' ';
101
            if ( pos.prefill ) extra += pos.prefill;
102
            cm.replaceRange( extra, { line: pos.line } );
103
        }
104
105
        cm.setCursor( pos );
106
        Widget.ActivateAt( cm, pos, idx );
93
    }
107
    }
94
108
95
    function getTabPositions( editor, cur ) {
109
    function getTabPositions( editor, cur ) {
96
        var info = editor.getLineInfo( cur || editor.cm.getCursor() );
110
        cur = cur || editor.cm.getCursor();
111
        var field = editor.getFieldAt( cur.line );
97
112
98
        if ( info ) {
113
        if ( field ) {
99
            if ( info.subfields ) {
114
            if ( field.isControlField ) {
100
                var positions = [ 0, 4, 6 ];
115
                var positions = [ { ch: 0 }, { ch: 4 } ];
101
116
102
                $.each( info.subfields, function( undef, subfield ) {
117
                $.each( positions, function( undef, pos ) {
103
                    positions.push( subfield.ch + 3 );
118
                    pos.line = cur.line;
104
                } );
119
                } );
105
120
106
                return positions;
121
                return positions;
107
            } else {
122
            } else {
108
                return [ 0, 4 ];
123
                var positions = [ { ch: 0 }, { ch: 4, prefill: '_' }, { ch: 6, prefill: '_' } ];
124
125
                $.each( positions, function( undef, pos ) {
126
                    pos.line = cur.line;
127
                } );
128
                $.each( field.getSubfields(), function( undef, subfield ) {
129
                    positions.push( { line: cur.line, ch: subfield.contentsStart } );
130
                } );
131
132
                // Allow to tab to start of empty field
133
                if ( field.getSubfields().length == 0 ) {
134
                    positions.push( { line: cur.line, ch: 8 } );
135
                }
136
137
                return positions;
109
            }
138
            }
110
        } else {
139
        } else {
111
            return [];
140
            return [];
Lines 119-152 define( [ 'marc-record', 'koha-backend', 'preferences', 'text-marc', 'widget' ], Link Here
119
            cm.setCursor( { line: cursor.line + 1, ch: 0 } );
148
            cm.setCursor( { line: cursor.line + 1, ch: 0 } );
120
        },
149
        },
121
150
122
        'Shift-Enter': function( cm ) {
123
            var cursor = cm.getCursor();
124
            cm.replaceRange( '\n', { line: cursor.line, ch: 0 }, null, 'marcAware' );
125
            cm.setCursor( { line: cursor.line, ch: 0 } );
126
        },
127
128
        'Ctrl-X': function( cm ) {
151
        'Ctrl-X': function( cm ) {
129
            // Delete subfield (or cut)
152
            // Delete line (or cut)
130
            if ( cm.somethingSelected() ) return true;
153
            if ( cm.somethingSelected() ) return true;
131
154
132
            var cur = cm.getCursor();
155
            var field = cm.marceditor.getCurrentField();
133
            var info = cm.marceditor.getLineInfo( cur );
156
            if ( field ) field.delete();
134
            if ( !info || !info.subfields ) return true;
135
136
            for (var i = 0; i < info.subfields.length; i++) {
137
                var end = i == info.subfields.length - 1 ? info.contents.length : info.subfields[i+1].ch;
138
                if (cur.ch > end) continue;
139
140
                cm.replaceRange( "", { line: cur.line, ch: info.subfields[i].ch }, { line: cur.line, ch: end }, 'marcAware' );
141
                return;
142
            }
143
        },
157
        },
144
158
145
        'Shift-Ctrl-X': function( cm ) {
159
        'Shift-Ctrl-X': function( cm ) {
146
            // Delete line
160
            // Delete subfield
147
            var cur = cm.getCursor();
161
            var field = cm.marceditor.getCurrentField();
162
            if ( !field ) return;
148
163
149
            cm.replaceRange( "", { line: cur.line, ch: 0 }, { line: cur.line + 1, ch: 0 }, 'marcAware' );
164
            var subfield = field.getSubfieldAt( cm.getCursor().ch );
165
            if ( subfield ) subfield.delete();
150
        },
166
        },
151
167
152
        Tab: function( cm ) {
168
        Tab: function( cm ) {
Lines 155-162 define( [ 'marc-record', 'koha-backend', 'preferences', 'text-marc', 'widget' ], Link Here
155
            var cur = cm.getCursor();
171
            var cur = cm.getCursor();
156
172
157
            for ( var i = 0; i < positions.length; i++ ) {
173
            for ( var i = 0; i < positions.length; i++ ) {
158
                if ( positions[i] > cur.ch ) {
174
                if ( positions[i].ch > cur.ch ) {
159
                    activateTabPosition( cm, { line: cur.line, ch: positions[i] } );
175
                    activateTabPosition( cm, positions[i] );
160
                    return false;
176
                    return false;
161
                }
177
                }
162
            }
178
            }
Lines 170-196 define( [ 'marc-record', 'koha-backend', 'preferences', 'text-marc', 'widget' ], Link Here
170
            var cur = cm.getCursor();
186
            var cur = cm.getCursor();
171
187
172
            for ( var i = positions.length - 1; i >= 0; i-- ) {
188
            for ( var i = positions.length - 1; i >= 0; i-- ) {
173
                if ( positions[i] < cur.ch ) {
189
                if ( positions[i].ch < cur.ch ) {
174
                    activateTabPosition( cm, { line: cur.line, ch: positions[i] } );
190
                    activateTabPosition( cm, positions[i] );
175
                    return false;
191
                    return false;
176
                }
192
                }
177
            }
193
            }
178
194
179
            if ( cur.line == 0 ) return;
195
            if ( cur.line == 0 ) return;
180
196
181
            var prevPositions = getTabPositions( cm, { line: cur.line - 1, ch: cm.getLine( cur.line - 1 ).length } );
197
            var prevPositions = getTabPositions( cm.marceditor, { line: cur.line - 1, ch: cm.getLine( cur.line - 1 ).length } );
182
198
183
            if ( prevPositions.length ) {
199
            if ( prevPositions.length ) {
184
                activateTabPosition( cm, { line: cur.line - 1, ch: prevPositions[ prevPositions.length - 1 ] }, -1 );
200
                activateTabPosition( cm, prevPositions[ prevPositions.length - 1 ], -1 );
185
            } else {
201
            } else {
186
                cm.setCursor( { line: cur.line - 1, ch: 0 } );
202
                cm.setCursor( { line: cur.line - 1, ch: 0 } );
187
            }
203
            }
188
        },
204
        },
205
206
        'Ctrl-D': function( cm ) {
207
            // Insert subfield delimiter
208
            // This will be extended later to allow either a configurable subfield delimiter or just
209
            // make it be the double cross.
210
            var cur = cm.getCursor();
211
212
            cm.replaceRange( "$", cur, null );
213
        },
214
    };
215
216
    // The objects below are part of a field/subfield manipulation API, accessed through the base
217
    // editor object.
218
    //
219
    // Each one is tied to a particular line; this means that using a field or subfield object after
220
    // any other changes to the record will cause entertaining explosions. The objects are meant to
221
    // be temporary, and should only be reused with great care. The macro code does this only
222
    // because it is careful to dispose of the object after any other updates.
223
    //
224
    // Note, however, tha you can continue to use a field object after changing subfields. It's just
225
    // the subfield objects that become invalid.
226
227
    // This is an exception raised by the EditorSubfield and EditorField when an invalid change is
228
    // attempted.
229
    function FieldError(line, message) {
230
        this.line = line;
231
        this.message = message;
232
    };
233
234
    FieldError.prototype.toString = function() {
235
        return 'FieldError(' + this.line + ', "' + this.message + '")';
189
    };
236
    };
190
237
191
    function MARCEditor( position ) {
238
    // This is the temporary object for a particular subfield in a field. Any change to any other
239
    // subfields will invalidate this subfield object.
240
    function EditorSubfield( field, index, start, end ) {
241
        this.field = field;
242
        this.index = index;
243
        this.start = start;
244
        this.end = end;
245
246
        if ( this.field.isControlField ) {
247
            this.contentsStart = start;
248
            this.code = '@';
249
        } else {
250
            this.contentsStart = start + 3;
251
            this.code =  this.field.contents.substr( this.start + 1, 1 );
252
        }
253
254
        this.cm = field.cm;
255
256
        var marks = this.cm.findMarksAt( { line: field.line, ch: this.contentsStart } );
257
        if ( marks[0] && marks[0].widget ) {
258
            this.widget = marks[0].widget;
259
260
            this.text = this.widget.text;
261
            this.setText = this.widget.setText;
262
            this.getFixed = this.widget.getFixed;
263
            this.setFixed = this.widget.setFixed;
264
        } else {
265
            this.widget = null;
266
            this.text = this.field.contents.substr( this.contentsStart, end - this.contentsStart );
267
        }
268
    };
269
270
    $.extend( EditorSubfield.prototype, {
271
        _invalid: function() {
272
            return this.field._subfieldsInvalid();
273
        },
274
275
        focus: function() {
276
            this.cm.setCursor( { line: this.field.line, ch: this.contentsStart } );
277
        },
278
        focusEnd: function() {
279
            this.cm.setCursor( { line: this.field.line, ch: this.end } );
280
        },
281
        getText: function() {
282
            return this.text;
283
        },
284
        setText: function( text ) {
285
            if ( !this._invalid() ) throw new FieldError( this.field.line, 'subfield invalid' );
286
            this.cm.replaceRange( text, { line: this.field.line, ch: this.contentsStart }, { line: this.field.line, ch: this.end }, 'marcAware' );
287
            this.field._invalidateSubfields();
288
        },
289
    } );
290
291
    function EditorField( editor, line ) {
292
        this.editor = editor;
293
        this.line = line;
294
295
        this.cm = editor.cm;
296
297
        this._updateInfo();
298
        this.tag = this.contents.substr( 0, 3 );
299
        this.isControlField = ( this.tag < '010' );
300
301
        if ( this.isControlField ) {
302
            this._ind1 = this.contents.substr( 4, 1 );
303
            this._ind2 = this.contents.substr( 6, 1 );
304
        } else {
305
            this._ind1 = null;
306
            this._ind2 = null;
307
        }
308
309
        this.subfields = null;
310
    }
311
312
    $.extend( EditorField.prototype, {
313
        _subfieldsInvalid: function() {
314
            return !this.subfields;
315
        },
316
        _invalidateSubfields: function() {
317
            this._subfields = null;
318
        },
319
320
        _updateInfo: function() {
321
            this.info = this.editor.getLineInfo( { line: this.line, ch: 0 } );
322
            if ( this.info == null ) throw new FieldError( 'Invalid field' );
323
            this.contents = this.info.contents;
324
        },
325
        _scanSubfields: function() {
326
            this._updateInfo();
327
328
            if ( this.isControlField ) {
329
                this._subfields = [ new EditorSubfield( this, 0, 4, this.contents.length ) ];
330
            } else {
331
                var field = this;
332
                var subfields = this.info.subfields;
333
                this._subfields = [];
334
335
                for (var i = 0; i < this.info.subfields.length; i++) {
336
                    var end = i == subfields.length - 1 ? this.contents.length : subfields[i+1].ch;
337
338
                    this._subfields.push( new EditorSubfield( this, i, subfields[i].ch, end ) );
339
                }
340
            }
341
        },
342
343
        delete: function() {
344
            this.cm.replaceRange( "", { line: this.line, ch: 0 }, { line: this.line + 1, ch: 0 }, 'marcAware' );
345
        },
346
        focus: function() {
347
            this.cm.setCursor( { line: this.line, ch: 0 } );
348
349
            return this;
350
        },
351
352
        getText: function() {
353
            var result = '';
354
355
            $.each( this.getSubfields(), function() {
356
                if ( this.code != '@' ) result += '$' + this.code + ' ';
357
358
                result += this.getText();
359
            } );
360
361
            return result;
362
        },
363
        setText: function( text ) {
364
            var indicator_match = /^([_ 0-9])([_ 0-9])\$/.exec( text );
365
            if ( indicator_match ) {
366
                text = text.substr(2);
367
                this.setIndicator1( indicator_match[1] );
368
                this.setIndicator2( indicator_match[2] );
369
            }
370
371
            this.cm.replaceRange( text, { line: this.line, ch: this.isControlField ? 4 : 8 }, { line: this.line }, 'marcAware' );
372
            this._invalidateSubfields();
373
374
            return this;
375
        },
376
377
        getIndicator1: function() {
378
            return this._ind1;
379
        },
380
        getIndicator2: function() {
381
            return this._ind2;
382
        },
383
        setIndicator1: function(val) {
384
            if ( this.isControlField ) throw new FieldError('Cannot set indicators on control field');
385
386
            this._ind1 = ( !val || val == ' ' ) ? '_' : val;
387
            this.cm.replaceRange( this._ind1, { line: this.line, ch: 4 }, { line: this.line, ch: 5 }, 'marcAware' );
388
389
            return this;
390
        },
391
        setIndicator2: function(val) {
392
            if ( this.isControlField ) throw new FieldError('Cannot set indicators on control field');
393
394
            this._ind2 = ( !val || val == ' ' ) ? '_' : val;
395
            this.cm.replaceRange( this._ind2, { line: this.line, ch: 6 }, { line: this.line, ch: 7 }, 'marcAware' );
396
397
            return this;
398
        },
399
400
        appendSubfield: function( code ) {
401
            if ( this.isControlField ) throw new FieldError('Cannot add subfields to control field');
402
403
            this._invalidateSubfields();
404
            this.cm.replaceRange( '$' + code + ' ', { line: this.line }, null, 'marcAware' );
405
            var subfields = this.getSubfields();
406
407
            return subfields[ subfields.length - 1 ];
408
        },
409
        insertSubfield: function( code, position ) {
410
            if ( this.isControlField ) throw new FieldError('Cannot add subfields to control field');
411
412
            position = position || 0;
413
414
            var subfields = this.getSubfields();
415
            this._invalidateSubfields();
416
            this.cm.replaceRange( '$' + code + ' ', { line: this.line, ch: subfields[position] ? subfields[position].start : null }, null, 'marcAware' );
417
            subfields = this.getSubfields();
418
419
            return subfields[ position ];
420
        },
421
        getSubfields: function( code ) {
422
            if ( !this._subfields ) this._scanSubfields();
423
            if ( code == null ) return this._subfields;
424
425
            var result = [];
426
427
            $.each( this._subfields, function() {
428
                if ( code == null || this.code == code ) result.push(this);
429
            } );
430
431
            return result;
432
        },
433
        getFirstSubfield: function( code ) {
434
            var result = this.getSubfields( code );
435
436
            return ( result && result.length ) ? result[0] : null;
437
        },
438
        getSubfieldAt: function( ch ) {
439
            var subfields = this.getSubfields();
440
441
            for (var i = 0; i < subfields.length; i++) {
442
                if ( subfields[i].start < ch && subfields[i].end >= ch ) return subfields[i];
443
            }
444
        },
445
    } );
446
447
    function MARCEditor( options ) {
192
        this.cm = CodeMirror(
448
        this.cm = CodeMirror(
193
            position,
449
            options.position,
194
            {
450
            {
195
                extraKeys: _editorKeys,
451
                extraKeys: _editorKeys,
196
                gutters: [
452
                gutters: [
Lines 210-358 define( [ 'marc-record', 'koha-backend', 'preferences', 'text-marc', 'widget' ], Link Here
210
        this.cm.on( 'change', editorChange );
466
        this.cm.on( 'change', editorChange );
211
        this.cm.on( 'cursorActivity', editorCursorActivity );
467
        this.cm.on( 'cursorActivity', editorCursorActivity );
212
468
469
        this.onCursorActivity = options.onCursorActivity;
470
213
        this.subscribers = [];
471
        this.subscribers = [];
214
        this.subscribe( function( marceditor ) {
472
        this.subscribe( function( marceditor ) {
215
            Widget.Notify( marceditor );
473
            Widget.Notify( marceditor );
216
        } );
474
        } );
217
    }
475
    }
218
476
219
    MARCEditor.prototype.setUseWidgets = function( val ) {
477
    MARCEditor.FieldError = FieldError;
220
        if ( val ) {
478
221
            for ( var line = 0; line <= this.cm.lastLine(); line++ ) {
479
    $.extend( MARCEditor.prototype, {
222
                Widget.UpdateLine( this, line );
480
        setUseWidgets: function( val ) {
481
            if ( val ) {
482
                for ( var line = 0; line <= this.cm.lastLine(); line++ ) {
483
                    Widget.UpdateLine( this, line );
484
                }
485
            } else {
486
                $.each( this.cm.getAllMarks(), function( undef, mark ) {
487
                    if ( mark.widget ) mark.widget.clearToText();
488
                } );
223
            }
489
            }
224
        } else {
490
        },
491
492
        focus: function() {
493
            this.cm.focus();
494
        },
495
496
        getCursor: function() {
497
            return this.cm.getCursor();
498
        },
499
500
        refresh: function() {
501
            this.cm.refresh();
502
        },
503
504
        displayRecord: function( record ) {
505
            this.cm.setValue( TextMARC.RecordToText(record) );
506
        },
507
508
        getRecord: function() {
509
            this.textMode = true;
510
225
            $.each( this.cm.getAllMarks(), function( undef, mark ) {
511
            $.each( this.cm.getAllMarks(), function( undef, mark ) {
226
                if ( mark.widget ) mark.widget.clearToText();
512
                if ( mark.widget ) mark.widget.clearToText();
227
            } );
513
            } );
228
        }
514
            var record = TextMARC.TextToRecord( this.cm.getValue() );
229
    };
515
            for ( var line = 0; line <= this.cm.lastLine(); line++ ) {
516
                if ( Preferences.user.fieldWidgets ) Widget.UpdateLine( this, line );
517
            }
230
518
231
    MARCEditor.prototype.focus = function() {
519
            this.textMode = false;
232
        this.cm.focus();
233
    };
234
520
235
    MARCEditor.prototype.refresh = function() {
521
            return record;
236
        this.cm.refresh();
522
        },
237
    };
238
523
239
    MARCEditor.prototype.displayRecord = function( record ) {
524
        getLineInfo: function( pos ) {
240
        this.cm.setValue( TextMARC.RecordToText(record) );
525
            var contents = this.cm.getLine( pos.line );
241
    };
526
            if ( contents == null ) return {};
242
527
243
    MARCEditor.prototype.getRecord = function() {
528
            var tagNumber = contents.match( /^([A-Za-z0-9]{3})/ );
244
        this.textMode = true;
245
529
246
        $.each( this.cm.getAllMarks(), function( undef, mark ) {
530
            if ( !tagNumber ) return null; // No tag at all on this line
247
            if ( mark.widget ) mark.widget.clearToText();
531
            tagNumber = tagNumber[1];
248
        } );
249
        var record = TextMARC.TextToRecord( this.cm.getValue() );
250
        for ( var line = 0; line <= this.cm.lastLine(); line++ ) {
251
            if ( Preferences.user.fieldWidgets ) Widget.UpdateLine( this, line );
252
        }
253
532
254
        this.textMode = false;
533
            if ( tagNumber < '010' ) return { tagNumber: tagNumber, contents: contents }; // No current subfield
255
534
256
        return record;
535
            var matcher = /[$|ǂ‡]([a-z0-9%]) /g;
257
    };
536
            var match;
258
537
259
    MARCEditor.prototype.getLineInfo = function( pos ) {
538
            var subfields = [];
260
        var contents = this.cm.getLine( pos.line );
539
            var currentSubfield;
261
        if ( contents == null ) return {};
262
540
263
        var tagNumber = contents.match( /^([A-Za-z0-9]{3}) / );
541
            while ( ( match = matcher.exec(contents) ) ) {
542
                subfields.push( { code: match[1], ch: match.index } );
543
                if ( match.index < pos.ch ) currentSubfield = match[1];
544
            }
264
545
265
        if ( !tagNumber ) return null; // No tag at all on this line
546
            return { tagNumber: tagNumber, subfields: subfields, currentSubfield: currentSubfield, contents: contents };
266
        tagNumber = tagNumber[1];
547
        },
267
548
268
        if ( tagNumber < '010' ) return { tagNumber: tagNumber, contents: contents }; // No current subfield
549
        addError: function( line, error ) {
550
            var found = false;
551
            var options = {};
269
552
270
        var matcher = /[$|ǂ‡]([a-z0-9%]) /g;
553
            if ( line == null ) {
271
        var match;
554
                line = 0;
555
                options.above = true;
556
            }
272
557
273
        var subfields = [];
558
            $.each( this.cm.getLineHandle(line).widgets || [], function( undef, widget ) {
274
        var currentSubfield;
559
                if ( !widget.isErrorMarker ) return;
275
560
276
        while ( ( match = matcher.exec(contents) ) ) {
561
                found = true;
277
            subfields.push( { code: match[1], ch: match.index } );
278
            if ( match.index < pos.ch ) currentSubfield = match[1];
279
        }
280
562
281
        return { tagNumber: tagNumber, subfields: subfields, currentSubfield: currentSubfield, contents: contents };
563
                $( widget.node ).append( '; ' + error );
282
    };
564
                widget.changed();
283
284
    MARCEditor.prototype.addError = function( line, error ) {
285
        var found = false;
286
        var options = {};
287
565
288
        if ( line == null ) {
566
                return false;
289
            line = 0;
567
            } );
290
            options.above = true;
291
        }
292
568
293
        $.each( this.cm.getLineHandle(line).widgets || [], function( undef, widget ) {
569
            if ( found ) return;
294
            if ( !widget.isErrorMarker ) return;
295
570
296
            found = true;
571
            var node = $( '<div class="structure-error"><i class="icon-remove"></i> ' + error + '</div>' )[0];
572
            var widget = this.cm.addLineWidget( line, node, options );
297
573
298
            $( widget.node ).append( '; ' + error );
574
            widget.node = node;
299
            widget.changed();
575
            widget.isErrorMarker = true;
576
        },
300
577
301
            return false;
578
        removeErrors: function() {
302
        } );
579
            for ( var line = 0; line < this.cm.lineCount(); line++ ) {
580
                $.each( this.cm.getLineHandle( line ).widgets || [], function( undef, lineWidget ) {
581
                    if ( lineWidget.isErrorMarker ) lineWidget.clear();
582
                } );
583
            }
584
        },
303
585
304
        if ( found ) return;
586
        startNotify: function() {
587
            if ( this.notifyTimeout ) clearTimeout( this.notifyTimeout );
588
            this.notifyTimeout = setTimeout( $.proxy( function() {
589
                this.notifyAll();
305
590
306
        var node = $( '<div class="structure-error"><i class="icon-remove"></i> ' + error + '</div>' )[0];
591
                this.notifyTimeout = null;
307
        var widget = this.cm.addLineWidget( line, node, options );
592
            }, this ), NOTIFY_TIMEOUT );
593
        },
308
594
309
        widget.node = node;
595
        notifyAll: function() {
310
        widget.isErrorMarker = true;
596
            $.each( this.subscribers, $.proxy( function( undef, subscriber ) {
311
    };
597
                subscriber(this);
598
            }, this ) );
599
        },
312
600
313
    MARCEditor.prototype.removeErrors = function() {
601
        subscribe: function( subscriber ) {
314
        for ( var line = 0; line < this.cm.lineCount(); line++ ) {
602
            this.subscribers.push( subscriber );
315
            $.each( this.cm.getLineHandle( line ).widgets || [], function( undef, lineWidget ) {
603
        },
316
                if ( lineWidget.isErrorMarker ) lineWidget.clear();
317
            } );
318
        }
319
    };
320
604
321
    MARCEditor.prototype.getFixedField = function(field) {
605
        createField: function( tag, line ) {
322
        field += ' ';
606
            var contents = tag + ( tag < '010' ? ' ' : ' _ _ ' );
323
        for ( var line = 0; line < this.cm.lineCount(); line++ ) {
324
            var contents = this.cm.getLine(line);
325
            if ( contents.substr( 0, 4 ) != field ) continue;
326
607
327
            var marks = this.cm.findMarksAt( { line: line, ch: 4 } );
608
            if ( line > this.cm.lastLine() ) {
328
            if ( marks[0] && marks[0].widget ) {
609
                contents = '\n' + contents;
329
                return marks[0].widget.text;
330
            } else {
610
            } else {
331
                return contents.substr(4);
611
                contents = contents + '\n';
332
            }
612
            }
333
        }
334
613
335
        return null;
614
            this.cm.replaceRange( contents, { line: line, ch: 0 }, null, 'marcAware' );
336
    };
337
615
338
    MARCEditor.prototype.startNotify = function() {
616
            return new EditorField( this, line );
339
        if ( this.notifyTimeout ) clearTimeout( this.notifyTimeout );
617
        },
340
        this.notifyTimeout = setTimeout( $.proxy( function() {
341
            this.notifyAll();
342
618
343
            this.notifyTimeout = null;
619
        createFieldOrdered: function( tag ) {
344
        }, this ), NOTIFY_TIMEOUT );
620
            var line, contents;
345
    };
346
621
347
    MARCEditor.prototype.notifyAll = function() {
622
            for ( line = 0; (contents = this.cm.getLine(line)); line++ ) {
348
        $.each( this.subscribers, $.proxy( function( undef, subscriber ) {
623
                if ( contents && contents.substr(0, 3) > tag ) break;
349
            subscriber(this);
624
            }
350
        }, this ) );
351
    };
352
625
353
    MARCEditor.prototype.subscribe = function( subscriber ) {
626
            return this.createField( tag, line );
354
        this.subscribers.push( subscriber );
627
        },
355
    };
628
629
        createFieldGrouped: function( tag ) {
630
            // Control fields should be inserted in actual order, whereas other fields should be
631
            // inserted grouped
632
            if ( tag < '010' ) return this.createFieldOrdered( tag );
633
634
            var line, contents;
635
636
            for ( line = 0; (contents = this.cm.getLine(line)); line++ ) {
637
                if ( contents && contents[0] > tag[0] ) break;
638
            }
639
640
            return this.createField( tag, line );
641
        },
642
643
        getFieldAt: function( line ) {
644
            try {
645
                return new EditorField( this, line );
646
            } catch (e) {
647
                return null;
648
            }
649
        },
650
651
        getCurrentField: function() {
652
            return this.getFieldAt( this.cm.getCursor().line );
653
        },
654
655
        getFields: function( tag ) {
656
            var result = [];
657
658
            if ( tag != null ) tag += ' ';
659
660
            for ( var line = 0; line < this.cm.lineCount(); line++ ) {
661
                if ( tag && this.cm.getLine(line).substr( 0, 4 ) != tag ) continue;
662
663
                // If this throws a FieldError, pretend it doesn't exist
664
                try {
665
                    result.push( new EditorField( this, line ) );
666
                } catch (e) {
667
                    if ( !( e instanceof FieldError ) ) throw e;
668
                }
669
            }
670
671
            return result;
672
        },
673
674
        getFirstField: function( tag ) {
675
            var result = this.getFields( tag );
676
677
            return ( result && result.length ) ? result[0] : null;
678
        },
679
680
        getAllFields: function( tag ) {
681
            return this.getFields( null );
682
        },
683
    } );
356
684
357
    return MARCEditor;
685
    return MARCEditor;
358
} );
686
} );
(-)a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/marc-mode.js (-7 / +46 lines)
Lines 1-6 Link Here
1
/**
1
/**
2
 * Textual MARC mode for CodeMirror.
2
 * Copyright 2015 ByWater Solutions
3
 * Copyright (c) 2013 ByWater
3
 *
4
 * This file is part of Koha.
5
 *
6
 * Koha is free software; you can redistribute it and/or modify it
7
 * under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation; either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * Koha is distributed in the hope that it will be useful, but
12
 * WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License
17
 * along with Koha; if not, see <http://www.gnu.org/licenses>.
4
 */
18
 */
5
19
6
// Expected format: 245 _ 1 $a Pizza |c 34ars
20
// Expected format: 245 _ 1 $a Pizza |c 34ars
Lines 34-39 CodeMirror.defineMode( 'marc', function( config, modeConfig ) { Link Here
34
        },
48
        },
35
        token: function( stream, state ) {
49
        token: function( stream, state ) {
36
            var match;
50
            var match;
51
            // First, try to match some kind of valid tag
37
            if ( stream.sol() ) {
52
            if ( stream.sol() ) {
38
                this.startState( state );
53
                this.startState( state );
39
                if ( match = stream.match( /[0-9A-Za-z]+/ ) ) {
54
                if ( match = stream.match( /[0-9A-Za-z]+/ ) ) {
Lines 50-56 CodeMirror.defineMode( 'marc', function( config, modeConfig ) { Link Here
50
65
51
                    state.tagNumber = match;
66
                    state.tagNumber = match;
52
                    if ( state.tagNumber < '010' ) {
67
                    if ( state.tagNumber < '010' ) {
53
                        // Control field
68
                        // Control field, no subfields or indicators
54
                        state.subAllowed = false;
69
                        state.subAllowed = false;
55
                    }
70
                    }
56
71
Lines 66-91 CodeMirror.defineMode( 'marc', function( config, modeConfig ) { Link Here
66
                }
81
                }
67
            }
82
            }
68
83
84
            // Don't need to do anything
69
            if ( stream.eol() ) {
85
            if ( stream.eol() ) {
70
                return;
86
                return;
71
            }
87
            }
72
88
89
            // Check for the correct space after the tag number for a control field
73
            if ( !state.subAllowed && stream.pos == 3 ) {
90
            if ( !state.subAllowed && stream.pos == 3 ) {
74
                if ( stream.next() == ' ' ) {
91
                if ( stream.next() == ' ' ) {
75
                    return 'reqspace';
92
                    return 'required-space';
76
                } else {
93
                } else {
77
                    stream.skipToEnd();
94
                    stream.skipToEnd();
78
                    return 'error';
95
                    return 'error';
79
                }
96
                }
80
            }
97
            }
81
98
99
            // For a normal field, check for correct indicators and spacing
82
            if ( stream.pos < 8 && state.subAllowed ) {
100
            if ( stream.pos < 8 && state.subAllowed ) {
83
                switch ( stream.pos ) {
101
                switch ( stream.pos ) {
84
                    case 3:
102
                    case 3:
85
                    case 5:
103
                    case 5:
86
                    case 7:
104
                    case 7:
87
                        if ( stream.next() == ' ' ) {
105
                        if ( stream.next() == ' ' ) {
88
                            return 'reqspace';
106
                            return 'required-space';
89
                        } else {
107
                        } else {
90
                            stream.skipToEnd();
108
                            stream.skipToEnd();
91
                            return 'error';
109
                            return 'error';
Lines 101-108 CodeMirror.defineMode( 'marc', function( config, modeConfig ) { Link Here
101
                }
119
                }
102
            }
120
            }
103
121
122
            // Otherwise, we're after the start of the line.
104
            if ( state.subAllowed ) {
123
            if ( state.subAllowed ) {
105
                if ( stream.pos != 8 && stream.match( /[^$|ǂ‡]+/ ) ) return;
124
                // If we don't have to match a subfield, try to consume text.
125
                if ( stream.pos != 8 ) {
126
                    // Try to match space at the end of the line, then everything but spaces, and as
127
                    // a final fallback, only spaces.
128
                    //
129
                    // This is required to keep the contents matching from stepping on the end-space
130
                    // matching.
131
                    if ( stream.match( /[ \t]+$/ ) ) {
132
                        return 'end-space';
133
                    } else if ( stream.match( /[^ \t$|ǂ‡]+/ ) || stream.match( /[ \t]+/ ) ) {
134
                        return;
135
                    }
136
                }
106
137
107
                if ( stream.eat( /[$|ǂ‡]/ ) ) {
138
                if ( stream.eat( /[$|ǂ‡]/ ) ) {
108
                    var subfieldCode;
139
                    var subfieldCode;
Lines 122-128 CodeMirror.defineMode( 'marc', function( config, modeConfig ) { Link Here
122
                    return 'error';
153
                    return 'error';
123
                }
154
                }
124
            } else {
155
            } else {
125
                stream.skipToEnd();
156
                // Match space at end of line
157
                if ( stream.match( /[ \t]+$/ ) ) {
158
                    return 'end-space';
159
                } else {
160
                    stream.match( /[ \t]+/ );
161
                }
162
163
                stream.match( /[^ \t]+/ );
164
                return;
126
            }
165
            }
127
        }
166
        }
128
    };
167
    };
(-)a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/marc-record.js (+19 lines)
Lines 1-4 Link Here
1
/**
1
/**
2
 * Copyright 2015 ByWater Solutions
3
 *
4
 * This file is part of Koha.
5
 *
6
 * Koha is free software; you can redistribute it and/or modify it
7
 * under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation; either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * Koha is distributed in the hope that it will be useful, but
12
 * WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License
17
 * along with Koha; if not, see <http://www.gnu.org/licenses>.
18
 */
19
20
/**
2
 * Adapted and cleaned up from biblios.net, which is purportedly under the GPL.
21
 * Adapted and cleaned up from biblios.net, which is purportedly under the GPL.
3
 * Source: http://git.librarypolice.com/?p=biblios.git;a=blob_plain;f=plugins/marc21editor/marcrecord.js;hb=master
22
 * Source: http://git.librarypolice.com/?p=biblios.git;a=blob_plain;f=plugins/marc21editor/marcrecord.js;hb=master
4
 *
23
 *
(-)a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/preferences.js (+20 lines)
Lines 1-3 Link Here
1
/**
2
 * Copyright 2015 ByWater Solutions
3
 *
4
 * This file is part of Koha.
5
 *
6
 * Koha is free software; you can redistribute it and/or modify it
7
 * under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation; either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * Koha is distributed in the hope that it will be useful, but
12
 * WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License
17
 * along with Koha; if not, see <http://www.gnu.org/licenses>.
18
 */
19
1
define( function() {
20
define( function() {
2
    var Preferences = {
21
    var Preferences = {
3
        Load: function( borrowernumber ) {
22
        Load: function( borrowernumber ) {
Lines 14-19 define( function() { Link Here
14
                font: 'monospace',
33
                font: 'monospace',
15
                fontSize: '1em',
34
                fontSize: '1em',
16
                macros: {},
35
                macros: {},
36
                selected_search_targets: {},
17
            }, saved_prefs );
37
            }, saved_prefs );
18
        },
38
        },
19
39
(-)a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/resources.js (+19 lines)
Lines 1-3 Link Here
1
/**
2
 * Copyright 2015 ByWater Solutions
3
 *
4
 * This file is part of Koha.
5
 *
6
 * Koha is free software; you can redistribute it and/or modify it
7
 * under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation; either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * Koha is distributed in the hope that it will be useful, but
12
 * WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License
17
 * along with Koha; if not, see <http://www.gnu.org/licenses>.
18
 */
19
1
define( [ 'module' ], function( module ) {
20
define( [ 'module' ], function( module ) {
2
    var _allResources = [];
21
    var _allResources = [];
3
22
(-)a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/search.js (-9 / +33 lines)
Lines 1-3 Link Here
1
/**
2
 * Copyright 2015 ByWater Solutions
3
 *
4
 * This file is part of Koha.
5
 *
6
 * Koha is free software; you can redistribute it and/or modify it
7
 * under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation; either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * Koha is distributed in the hope that it will be useful, but
12
 * WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License
17
 * along with Koha; if not, see <http://www.gnu.org/licenses>.
18
 */
19
1
define( [ 'marc-record' ], function( MARC ) {
20
define( [ 'marc-record' ], function( MARC ) {
2
    var _options;
21
    var _options;
3
    var _records = {};
22
    var _records = {};
Lines 39-44 define( [ 'marc-record' ], function( MARC ) { Link Here
39
            return q;
58
            return q;
40
        },
59
        },
41
        Run: function( servers, q, options ) {
60
        Run: function( servers, q, options ) {
61
            options = $.extend( {
62
                offset: 0,
63
                page_size: 20,
64
            }, _options, options );
65
42
            Search.includedServers = [];
66
            Search.includedServers = [];
43
            _records = {};
67
            _records = {};
44
            _last = {
68
            _last = {
Lines 47-71 define( [ 'marc-record' ], function( MARC ) { Link Here
47
                options: options,
71
                options: options,
48
            };
72
            };
49
73
50
            options = $.extend( {
51
                offset: 0,
52
                page_size: 20,
53
            }, _options, options );
54
55
            $.each( servers, function ( id, info ) {
74
            $.each( servers, function ( id, info ) {
56
                if ( info.checked ) Search.includedServers.push( id );
75
                if ( info.checked ) Search.includedServers.push( id );
57
            } );
76
            } );
58
77
59
            $.get(
78
            $.get(
60
                '/cgi-bin/koha/svc/z3950',
79
                '/cgi-bin/koha/svc/cataloguing/metasearch',
61
                {
80
                {
62
                    q: q,
81
                    q: q,
63
                    servers: Search.includedServers.join( ',' ),
82
                    servers: Search.includedServers.join( ',' ),
64
                    offset: options.offset,
83
                    offset: options.offset,
65
                    page_size: options.page_size
84
                    page_size: options.page_size,
85
                    sort_direction: options.sort_direction,
86
                    sort_key: options.sort_key,
87
                    resultset: options.resultset,
66
                }
88
                }
67
            )
89
            )
68
                .done( function( data ) {
90
                .done( function( data ) {
91
                    _last.options.resultset = data.resultset;
69
                    $.each( data.hits, function( undef, hit ) {
92
                    $.each( data.hits, function( undef, hit ) {
70
                        var record = new MARC.Record();
93
                        var record = new MARC.Record();
71
                        record.loadMARCXML( hit.record );
94
                        record.loadMARCXML( hit.record );
Lines 80-88 define( [ 'marc-record' ], function( MARC ) { Link Here
80
103
81
            return true;
104
            return true;
82
        },
105
        },
83
        Fetch: function( offset ) {
106
        Fetch: function( options ) {
84
            if ( !_last ) return;
107
            if ( !_last ) return;
85
            Search.Run( _last.servers, _last.q, $.extend( {}, _last.options, { offset: offset } ) );
108
            $.extend( _last.options, options );
109
            Search.Run( _last.servers, _last.q, _last.options );
86
        }
110
        }
87
    };
111
    };
88
112
(-)a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/text-marc.js (+19 lines)
Lines 1-3 Link Here
1
/**
2
 * Copyright 2015 ByWater Solutions
3
 *
4
 * This file is part of Koha.
5
 *
6
 * Koha is free software; you can redistribute it and/or modify it
7
 * under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation; either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * Koha is distributed in the hope that it will be useful, but
12
 * WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License
17
 * along with Koha; if not, see <http://www.gnu.org/licenses>.
18
 */
19
1
define( [ 'marc-record' ], function( MARC ) {
20
define( [ 'marc-record' ], function( MARC ) {
2
    // Convert any characters for display
21
    // Convert any characters for display
3
    function _sanitize( text ) {
22
    function _sanitize( text ) {
(-)a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/widget.js (+22 lines)
Lines 1-3 Link Here
1
/**
2
 * Copyright 2015 ByWater Solutions
3
 *
4
 * This file is part of Koha.
5
 *
6
 * Koha is free software; you can redistribute it and/or modify it
7
 * under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation; either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * Koha is distributed in the hope that it will be useful, but
12
 * WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License
17
 * along with Koha; if not, see <http://www.gnu.org/licenses>.
18
 */
19
1
define( [ 'resources' ], function( Resources ) {
20
define( [ 'resources' ], function( Resources ) {
2
    var _widgets = {};
21
    var _widgets = {};
3
22
Lines 233-238 define( [ 'resources' ], function( Resources ) { Link Here
233
                    var next = ( i < info.subfields.length - 1 ) ? info.subfields[i + 1].ch : end;
252
                    var next = ( i < info.subfields.length - 1 ) ? info.subfields[i + 1].ch : end;
234
                    subfields.push( { code: info.subfields[i].code, from: info.subfields[i].ch + 3, to: next } );
253
                    subfields.push( { code: info.subfields[i].code, from: info.subfields[i].ch + 3, to: next } );
235
                }
254
                }
255
                // If not a fixed field, and we didn't find any subfields, we need to throw in the
256
                // '@' subfield so we can properly remove it
257
                if ( subfields.length == 0 ) subfields.push( { code: '@', from: 4, to: end } );
236
            }
258
            }
237
259
238
            $.each( subfields, function ( undef, subfield ) {
260
            $.each( subfields, function ( undef, subfield ) {
(-)a/koha-tmpl/intranet-tmpl/prog/en/css/cateditor.css (-3 / +35 lines)
Lines 90-95 body { Link Here
90
    font-weight: bold;
90
    font-weight: bold;
91
}
91
}
92
92
93
.cm-end-space {
94
    background-color: #DDDDBB;
95
}
96
93
#editor .modified-line-gutter {
97
#editor .modified-line-gutter {
94
    width: 10px;
98
    width: 10px;
95
}
99
}
Lines 202-208 body { Link Here
202
}
206
}
203
207
204
.modal-body {
208
.modal-body {
205
    height: 400px;
209
    max-height: none;
206
    padding: 0;
210
    padding: 0;
207
}
211
}
208
212
Lines 237-243 body { Link Here
237
#quicksearch input, #quicksearch a {
241
#quicksearch input, #quicksearch a {
238
    font-size: 1.2em;
242
    font-size: 1.2em;
239
    padding: 3px 0;
243
    padding: 3px 0;
240
    width: 96%; // I have no idea why this is necessary
244
    width: 96%; /* I have no idea why this is necessary */
241
}
245
}
242
246
243
#show-advanced-search {
247
#show-advanced-search {
Lines 246-257 body { Link Here
246
}
250
}
247
251
248
#advanced-search-fields {
252
#advanced-search-fields {
253
    -moz-column-width: 26em;
254
    -webkit-column-width: 26em;
255
    column-width: 26em;
249
    margin: 0;
256
    margin: 0;
250
    padding: 0;
257
    padding: 0;
251
}
258
}
252
259
253
#advanced-search-fields li {
260
#advanced-search-fields li {
254
    display: inline-block;
261
    display: block;
255
    list-style-type: none;
262
    list-style-type: none;
256
}
263
}
257
264
Lines 280-285 body { Link Here
280
287
281
/*> Search results */
288
/*> Search results */
282
289
290
#search-serversinfo li {
291
    list-style-type: none;
292
}
293
294
#search-serversinfo .search-toggle-server {
295
    margin-right: 5px;
296
}
297
283
#searchresults table {
298
#searchresults table {
284
    width: 100%;
299
    width: 100%;
285
}
300
}
Lines 400-402 body { Link Here
400
#macro-list .active .macro-info {
415
#macro-list .active .macro-info {
401
    display: block;
416
    display: block;
402
}
417
}
418
419
.btn-toolbar label, .btn-toolbar select {
420
    font-size: 13px;
421
    vertical-align: middle;
422
}
423
424
.btn-toolbar label {
425
    margin-left: 1em;
426
}
427
428
.btn-toolbar select {
429
    padding: 2px;
430
}
431
432
#macro-editor .CodeMirror {
433
    height: 100%;
434
}
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/cateditor-ui.inc (-45 / +268 lines)
Lines 10-15 require.config( { Link Here
10
            themelang: '[% themelang %]',
10
            themelang: '[% themelang %]',
11
        },
11
        },
12
    },
12
    },
13
    waitSeconds: 30,
13
} );
14
} );
14
</script>
15
</script>
15
16
Lines 22-40 require.config( { Link Here
22
<script>
23
<script>
23
require( [ 'koha-backend', 'search', 'macros', 'marc-editor', 'marc-record', 'preferences', 'resources', 'text-marc', 'widget' ], function( KohaBackend, Search, Macros, MARCEditor, MARC, Preferences, Resources, TextMARC, Widget ) {
24
require( [ 'koha-backend', 'search', 'macros', 'marc-editor', 'marc-record', 'preferences', 'resources', 'text-marc', 'widget' ], function( KohaBackend, Search, Macros, MARCEditor, MARC, Preferences, Resources, TextMARC, Widget ) {
24
    var z3950Servers = {
25
    var z3950Servers = {
25
        [% FOREACH server = z3950_servers %]
26
        'koha:biblioserver': {
27
            name: _("Local catalog"),
28
            recordtype: 'biblio',
29
            checked: false,
30
        },
31
        [%- FOREACH batch = editable_batches -%]
32
            'batch:[% batch.import_batch_id %]': {
33
                name: _("Batch: ") + '[% batch.file_name %]',
34
                recordtype: 'biblio',
35
                checked: false,
36
            },
37
        [%- END -%]
38
        [%- FOREACH server = z3950_servers -%]
26
            [% server.id %]: {
39
            [% server.id %]: {
27
                'name': '[% server.name %]',
40
                name: '[% server.name %]',
28
                'kohasyntax': '[% server.syntax == 'USMARC' ? 'MARC21' : server.syntax %]',
41
                recordtype: '[% server.recordtype %]',
29
                'checked': [% server.checked ? 'true' : 'false' %],
42
                checked: [% server.checked ? 'true' : 'false' %],
30
            },
43
            },
31
        [% END %]
44
        [%- END -%]
32
    };
45
    };
33
46
34
    // The columns that should show up in a search, in order, and keyed by the corresponding <metadata> tag in the XSL and Pazpar2 config
47
    // The columns that should show up in a search, in order, and keyed by the corresponding <metadata> tag in the XSL and Pazpar2 config
35
    var z3950Labels = [
48
    var z3950Labels = [
49
		[ "local_number", _("Local number") ],
36
		[ "title", _("Title") ],
50
		[ "title", _("Title") ],
37
		[ "series", _("Series Title") ],
51
		[ "series", _("Series title") ],
38
		[ "author", _("Author") ],
52
		[ "author", _("Author") ],
39
		[ "lccn", _("LCCN") ],
53
		[ "lccn", _("LCCN") ],
40
		[ "isbn", _("ISBN") ],
54
		[ "isbn", _("ISBN") ],
Lines 121-126 require( [ 'koha-backend', 'search', 'macros', 'marc-editor', 'marc-record', 'pr Link Here
121
            return false;
135
            return false;
122
        } );
136
        } );
123
137
138
        shortcut.add( 'ctrl+h', function() {
139
            var field = editor.getCurrentField();
140
141
            if ( !field ) return;
142
143
            window.open( getFieldHelpURL( field.tag ) );
144
        } );
145
124
        $('#quicksearch .search-box').each( function() {
146
        $('#quicksearch .search-box').each( function() {
125
            shortcut.add( 'enter', $.proxy( function() {
147
            shortcut.add( 'enter', $.proxy( function() {
126
                var terms = [];
148
                var terms = [];
Lines 143-148 require( [ 'koha-backend', 'search', 'macros', 'marc-editor', 'marc-record', 'pr Link Here
143
        } );
165
        } );
144
    }
166
    }
145
167
168
    function getFieldHelpURL( tag ) {
169
        [% IF ( marcflavour == 'MARC21' ) %]
170
            if ( tag == '000' ) {
171
                return "http://www.loc.gov/marc/bibliographic/bdleader.html";
172
            } else if ( tag < '900' ) {
173
                return "http://www.loc.gov/marc/bibliographic/bd" + tag + ".html";
174
            } else {
175
                return "http://www.loc.gov/marc/bibliographic/bd9xx.html";
176
            }
177
        [% ELSIF ( marcflavour == 'UNIMARC' ) %]
178
            /* http://archive.ifla.org/VI/3/p1996-1/ is an outdated version of UNIMARC, but
179
               seems to be the only version available that can be linked to per tag.  More recent
180
               versions of the UNIMARC standard are available on the IFLA website only as
181
               PDFs!
182
            */
183
            if ( tag == '000' ) {
184
               return  "http://archive.ifla.org/VI/3/p1996-1/uni.htm";
185
            } else {
186
                var first = tag[0];
187
                var url = "http://archive.ifla.org/VI/3/p1996-1/uni" + first + ".htm#";
188
                if ( first == '0' ) url += "b";
189
                if ( first != '9' ) url += field;
190
191
                return url;
192
            }
193
        [% END %]
194
    }
195
146
    // Record loading
196
    // Record loading
147
    var backends = {
197
    var backends = {
148
       'new': {
198
       'new': {
Lines 207-212 require( [ 'koha-backend', 'search', 'macros', 'marc-editor', 'marc-record', 'pr Link Here
207
            titleForRecord: _("Editing search result"),
257
            titleForRecord: _("Editing search result"),
208
            get: function( id, callback ) {
258
            get: function( id, callback ) {
209
                if ( !id ) return false;
259
                if ( !id ) return false;
260
                if ( !backends.search.records[ id ] ) {
261
                    callback( { error: _( "Invalid record" ) } );
262
                    return false;
263
                }
210
264
211
                callback( backends.search.records[ id ] );
265
                callback( backends.search.records[ id ] );
212
            },
266
            },
Lines 214-219 require( [ 'koha-backend', 'search', 'macros', 'marc-editor', 'marc-record', 'pr Link Here
214
        },
268
        },
215
    };
269
    };
216
270
271
    var editable_batches = {
272
        [%- FOREACH batch = editable_batches -%]
273
            [% batch.import_batch_id %]: {
274
                'name': '[% batch.file_name %]',
275
            },
276
        [%- END -%]
277
    };
278
217
    function setSource(parts) {
279
    function setSource(parts) {
218
        state.backend = parts[0];
280
        state.backend = parts[0];
219
        state.recordID = parts[1];
281
        state.recordID = parts[1];
Lines 222-228 require( [ 'koha-backend', 'search', 'macros', 'marc-editor', 'marc-record', 'pr Link Here
222
284
223
        var backend = backends[state.backend];
285
        var backend = backends[state.backend];
224
286
225
        document.location.hash = '#' + parts[0] + ':' + parts[1];
287
        document.location.hash = '#' + parts[0] + '/' + parts[1];
226
288
227
        $('#title').text( backend.titleForRecord.replace( '{ID}', parts[1] ) );
289
        $('#title').text( backend.titleForRecord.replace( '{ID}', parts[1] ) );
228
290
Lines 234-240 require( [ 'koha-backend', 'search', 'macros', 'marc-editor', 'marc-record', 'pr Link Here
234
    }
296
    }
235
297
236
    function saveRecord( recid, editor, callback ) {
298
    function saveRecord( recid, editor, callback ) {
237
        var parts = recid.split(':');
299
        var parts = recid.split('/');
238
        if ( parts.length != 2 ) return false;
300
        if ( parts.length != 2 ) return false;
239
301
240
        if ( !backends[ parts[0] ] || !backends[ parts[0] ].save ) return false;
302
        if ( !backends[ parts[0] ] || !backends[ parts[0] ].save ) return false;
Lines 266-272 require( [ 'koha-backend', 'search', 'macros', 'marc-editor', 'marc-record', 'pr Link Here
266
328
267
            if (data.newId) {
329
            if (data.newId) {
268
                setSource(data.newId);
330
                setSource(data.newId);
269
                } else {
331
            } else {
270
                setSource( [ state.backend, state.recordID ] );
332
                setSource( [ state.backend, state.recordID ] );
271
            }
333
            }
272
334
Lines 275-288 require( [ 'koha-backend', 'search', 'macros', 'marc-editor', 'marc-record', 'pr Link Here
275
    }
337
    }
276
338
277
    function loadRecord( recid, editor, callback ) {
339
    function loadRecord( recid, editor, callback ) {
278
        var parts = recid.split(':');
340
        var parts = recid.split('/');
279
        if ( parts.length != 2 ) return false;
341
        if ( parts.length != 2 ) return false;
280
342
281
        if ( !backends[ parts[0] ] || !backends[ parts[0] ].get ) return false;
343
        if ( !backends[ parts[0] ] || !backends[ parts[0] ].get ) return false;
282
344
283
        backends[ parts[0] ].get( parts[1], function( record ) {
345
        backends[ parts[0] ].get( parts[1], function( record ) {
284
            editor.displayRecord( record );
346
            if ( !record.error ) {
285
            editor.focus();
347
                editor.displayRecord( record );
348
                editor.focus();
349
            }
286
350
287
            if (callback) callback(record);
351
            if (callback) callback(record);
288
        } );
352
        } );
Lines 292-298 require( [ 'koha-backend', 'search', 'macros', 'marc-editor', 'marc-record', 'pr Link Here
292
356
293
    function openRecord( recid, editor, callback ) {
357
    function openRecord( recid, editor, callback ) {
294
        return loadRecord( recid, editor, function ( record ) {
358
        return loadRecord( recid, editor, function ( record ) {
295
            setSource( recid.split(':') );
359
            setSource( recid.split('/') );
296
360
297
            if (callback) callback( record );
361
            if (callback) callback( record );
298
        } );
362
        } );
Lines 300-322 require( [ 'koha-backend', 'search', 'macros', 'marc-editor', 'marc-record', 'pr Link Here
300
364
301
    // Search functions
365
    // Search functions
302
    function showAdvancedSearch() {
366
    function showAdvancedSearch() {
367
        $('#advanced-search-servers').empty();
368
        $.each( z3950Servers, function( server_id, server ) {
369
            $('#advanced-search-servers').append( '<li data-server-id="' + server_id + '"><input class="search-toggle-server" type="checkbox"' + ( server.checked ? ' checked="checked">' : '>' ) + server.name + '</li>' );
370
        } );
303
        $('#advanced-search-ui').modal('show');
371
        $('#advanced-search-ui').modal('show');
304
    }
372
    }
305
373
306
    function startAdvancedSearch() {
374
    function startAdvancedSearch() {
307
        var search = '';
375
        var terms = [];
308
376
309
        $('#advanced-search-ui input').each( function() {
377
        $('#advanced-search-ui .search-box').each( function() {
310
            if (!this.value) return;
378
            if ( !this.value ) return;
311
            if (search) search += ' and ';
312
            search += $(this).data('qualifier') + this.value;
313
        } );
314
379
315
        if (!search) return;
380
            terms.push( [ $(this).data('qualifier'), this.value ] );
381
        } );
316
382
317
        $('#advanced-search-ui').modal('hide');
383
        if ( !terms.length ) return;
318
384
319
        if ( Search.Run( z3950Servers, search, 20 ) ) {
385
        if ( Search.Run( z3950Servers, Search.JoinTerms(terms) ) ) {
386
            $('#advanced-search-ui').modal('hide');
320
            $("#search-overlay").show();
387
            $("#search-overlay").show();
321
            showResultsBox();
388
            showResultsBox();
322
        }
389
        }
Lines 330-335 require( [ 'koha-backend', 'search', 'macros', 'marc-editor', 'marc-record', 'pr Link Here
330
        $('#search-results-ui').modal('show');
397
        $('#search-results-ui').modal('show');
331
    }
398
    }
332
399
400
    function showSearchSorting( sort_key, sort_direction ) {
401
        var $th = $('#searchresults thead tr th[data-sort-label="' + sort_key + '"]');
402
        $th.parent().find( 'th[data-sort-label]' ).attr( 'class', 'sorting' );
403
404
        if ( sort_direction == 'asc' ) {
405
            direction = 'asc';
406
            $th.attr( 'class', 'sorting_asc' );
407
        } else {
408
            direction = 'desc';
409
            $th.attr( 'class', 'sorting_desc' );
410
        }
411
    }
412
333
    function showSearchResults( editor, data ) {
413
    function showSearchResults( editor, data ) {
334
        backends.search.records = {};
414
        backends.search.records = {};
335
415
Lines 337-348 require( [ 'koha-backend', 'search', 'macros', 'marc-editor', 'marc-record', 'pr Link Here
337
        $('#searchresults tbody').empty();
417
        $('#searchresults tbody').empty();
338
        $('#search-serversinfo').empty();
418
        $('#search-serversinfo').empty();
339
419
340
        $.each( data.num_fetched, function( server_id, num_fetched ) {
420
        $.each( z3950Servers, function( server_id, server ) {
341
            if ( num_fetched < data.num_hits[server_id] ) {
421
            var num_fetched = data.num_fetched[server_id];
422
423
            if ( data.errors[server_id] ) {
424
                num_fetched = data.errors[server_id];
425
            } else if ( num_fetched == null ) {
426
                num_fetched = '-';
427
            } else if ( num_fetched < data.num_hits[server_id] ) {
342
                num_fetched += '+';
428
                num_fetched += '+';
343
            }
429
            }
344
430
345
            $('#search-serversinfo').append( '<li>' + z3950Servers[server_id].name + ' (' + num_fetched + ')' + '</li>' );
431
            $('#search-serversinfo').append( '<li data-server-id="' + server_id + '"><input class="search-toggle-server" type="checkbox"' + ( server.checked ? ' checked="checked">' : '>' ) + server.name + ' (' + num_fetched + ')' + '</li>' );
346
        } );
432
        } );
347
433
348
        var seenColumns = {};
434
        var seenColumns = {};
Lines 357-371 require( [ 'koha-backend', 'search', 'macros', 'marc-editor', 'marc-record', 'pr Link Here
357
443
358
        $.each( z3950Labels, function( undef, label ) {
444
        $.each( z3950Labels, function( undef, label ) {
359
            if ( seenColumns[ label[0] ] ) {
445
            if ( seenColumns[ label[0] ] ) {
360
                $('#searchresults thead tr').append( '<th>' + label[1] + '</th>' );
446
                $('#searchresults thead tr').append( '<th class="sorting" data-sort-label="' + label[0] + '">' + label[1] + '</th>' );
361
            }
447
            }
362
        } );
448
        } );
363
449
450
        showSearchSorting( data.sort_key, data.sort_direction );
451
364
        $('#searchresults thead tr').append('<th>' + _("Tools") + '</th>');
452
        $('#searchresults thead tr').append('<th>' + _("Tools") + '</th>');
365
453
366
        $.each( data.hits, function( undef, hit ) {
454
        $.each( data.hits, function( undef, hit ) {
367
            backends.search.records[ hit.server + '-' + hit.index ] = hit.record;
455
            backends.search.records[ hit.server + ':' + hit.index ] = hit.record;
368
            hit.id = 'search:' + hit.server + '-' + hit.index;
456
            hit.id = 'search/' + hit.server + ':' + hit.index;
369
457
370
            var result = '<tr>';
458
            var result = '<tr>';
371
            result += '<td class="sourcecol">' + z3950Servers[ hit.server ].name + '</td>';
459
            result += '<td class="sourcecol">' + z3950Servers[ hit.server ].name + '</td>';
Lines 496-501 require( [ 'koha-backend', 'search', 'macros', 'marc-editor', 'marc-record', 'pr Link Here
496
            case 'macros':
584
            case 'macros':
497
                showSavedMacros();
585
                showSavedMacros();
498
                break;
586
                break;
587
            case 'selected_search_targets':
588
                $.each( z3950Servers, function( server_id, server ) {
589
                    var saved_val = Preferences.user.selected_search_targets[server_id];
590
591
                    if ( saved_val != null ) server.checked = saved_val;
592
                } );
593
                break;
499
        }
594
        }
500
    }
595
    }
501
596
Lines 525-530 require( [ 'koha-backend', 'search', 'macros', 'marc-editor', 'marc-record', 'pr Link Here
525
                    Preferences.user.fontSize = $( e.target ).css( 'font-size' );
620
                    Preferences.user.fontSize = $( e.target ).css( 'font-size' );
526
                } );
621
                } );
527
                break;
622
                break;
623
            case 'selected_search_targets':
624
                $( document ).on( 'change', 'input.search-toggle-server', function() {
625
                    var server_id = $( this ).parent().data('server-id');
626
                    Preferences.user.selected_search_targets[server_id] = this.checked;
627
                    Preferences.Save( [% USER_INFO.0.borrowernumber %] );
628
                } );
629
                break;
528
        }
630
        }
529
    }
631
    }
530
632
Lines 548-553 require( [ 'koha-backend', 'search', 'macros', 'marc-editor', 'marc-record', 'pr Link Here
548
        $( '#macro-list li[data-name="' + name + '"]' ).addClass( 'active' );
650
        $( '#macro-list li[data-name="' + name + '"]' ).addClass( 'active' );
549
        var macro = Preferences.user.macros[name];
651
        var macro = Preferences.user.macros[name];
550
        macroEditor.setValue( macro.contents );
652
        macroEditor.setValue( macro.contents );
653
        $( '#macro-format' ).val( macro.format || 'its' );
551
        if ( macro.history ) macroEditor.setHistory( macro.history );
654
        if ( macro.history ) macroEditor.setHistory( macro.history );
552
    }
655
    }
553
656
Lines 604-617 require( [ 'koha-backend', 'search', 'macros', 'marc-editor', 'marc-record', 'pr Link Here
604
        if ( !name || macroEditor.savedGeneration == macroEditor.changeGeneration() ) return;
707
        if ( !name || macroEditor.savedGeneration == macroEditor.changeGeneration() ) return;
605
708
606
        macroEditor.savedGeneration = macroEditor.changeGeneration();
709
        macroEditor.savedGeneration = macroEditor.changeGeneration();
607
        storeMacro( name, { contents: macroEditor.getValue(), modified: (new Date()).valueOf(), history: macroEditor.getHistory() } );
710
        storeMacro( name, { contents: macroEditor.getValue(), modified: (new Date()).valueOf(), history: macroEditor.getHistory(), format: $('#macro-format').val() } );
608
        $('#macro-save-message').text(_("Saved"));
711
        $('#macro-save-message').text(_("Saved"));
609
        showSavedMacros();
712
        showSavedMacros();
610
    }
713
    }
611
714
612
    $(document).ready( function() {
715
    $(document).ready( function() {
613
        // Editor setup
716
        // Editor setup
614
        editor = new MARCEditor( function (elt) { $(elt).insertAfter('#toolbar') } );
717
        editor = new MARCEditor( {
718
            onCursorActivity: function() {
719
                $('#status-tag-info').empty();
720
                $('#status-subfield-info').empty();
721
722
                var field = editor.getCurrentField();
723
                var cur = editor.getCursor();
724
725
                if ( !field ) return;
726
727
                var taginfo = KohaBackend.GetTagInfo( '', field.tag );
728
                $('#status-tag-info').html( '<strong>' + field.tag + ':</strong> ' );
729
730
                if ( taginfo ) {
731
                    $('#status-tag-info').append( '<a href="' + getFieldHelpURL( field.tag ) + '" target="_blank" class="show-field-help" title="' + _("Show help for this tag") + '">[?]</a> '  + taginfo.lib );
732
733
                    var subfield = field.getSubfieldAt( cur.ch );
734
                    if ( !subfield ) return;
735
736
                    var subfieldinfo = taginfo.subfields[ subfield.code ];
737
                    $('#status-subfield-info').html( '<strong>$' + subfield.code + ':</strong> ' );
738
739
                    if ( subfieldinfo ) {
740
                        $('#status-subfield-info').append( subfieldinfo.lib );
741
                    } else {
742
                        $('#status-subfield-info').append( '<em>' + _("Unknown subfield") + '</em>' );
743
                    }
744
                } else {
745
                    $('#status-tag-info').append( '<em>' + _("Unknown tag") + '</em>' );
746
                }
747
            },
748
            position: function (elt) { $(elt).insertAfter('#toolbar') },
749
        } );
615
750
616
        macroEditor = CodeMirror(
751
        macroEditor = CodeMirror(
617
            $('#macro-editor')[0],
752
            $('#macro-editor')[0],
Lines 621-634 require( [ 'koha-backend', 'search', 'macros', 'marc-editor', 'marc-record', 'pr Link Here
621
            }
756
            }
622
        );
757
        );
623
758
759
        // Automatically detect resizes and change the height of the editor and position of modals.
624
        var resizeTimer = null;
760
        var resizeTimer = null;
625
        $( window ).resize( function() {
761
        $( window ).resize( function() {
626
            if ( resizeTimer == null ) resizeTimer = setTimeout( function() {
762
            if ( resizeTimer == null ) resizeTimer = setTimeout( function() {
627
                resizeTimer = null;
763
                resizeTimer = null;
628
764
629
                var pos = $('#editor .CodeMirror').position();
765
                var pos = $('#editor .CodeMirror').position();
630
                $('#editor .CodeMirror').height( $(window).height() - pos.top - 24 );
766
                $('#editor .CodeMirror').height( $(window).height() - pos.top - 24 - $('#changelanguage').height() ); // 24 is hardcoded value but works well
767
768
                $('.modal-body').each( function() {
769
                    $(this).height( $(window).height() * .8 - $(this).prevAll('.modal-header').height() );
770
                } );
631
            }, 100);
771
            }, 100);
772
773
            $("#advanced-search-ui, #search-results-ui, #macro-ui").css( {
774
                marginLeft: function() {
775
                    return -($(this).width() / 2);
776
                }
777
            } );
778
632
        } ).resize();
779
        } ).resize();
633
780
634
        var saveableBackends = [];
781
        var saveableBackends = [];
Lines 640-645 require( [ 'koha-backend', 'search', 'macros', 'marc-editor', 'marc-record', 'pr Link Here
640
            $( '#save-dropdown' ).append( '<li><a href="#" data-backend="' + backend[1] + '">' + backend[0] + '</a></li>' );
787
            $( '#save-dropdown' ).append( '<li><a href="#" data-backend="' + backend[1] + '">' + backend[0] + '</a></li>' );
641
        } );
788
        } );
642
789
790
        var macro_format_list = $.map( Macros.formats, function( format, name ) {
791
            return $.extend( { name: name }, format );
792
        } );
793
        macro_format_list.sort( function( a, b ) {
794
            return a.description.localeCompare(b.description);
795
        } );
796
        $.each( macro_format_list, function() {
797
            $('#macro-format').append( '<option value="' + this.name + '">' + this.description + '</option>' );
798
        } );
799
643
        // Click bindings
800
        // Click bindings
644
        $( '#save-record, #save-dropdown a' ).click( function() {
801
        $( '#save-record, #save-dropdown a' ).click( function() {
645
            $( '#save-record' ).find('i').attr( 'class', 'icon-loading' ).siblings( 'span' ).text( _("Saving...") );
802
            $( '#save-record' ).find('i').attr( 'class', 'icon-loading' ).siblings( 'span' ).text( _("Saving...") );
Lines 690-698 require( [ 'koha-backend', 'search', 'macros', 'marc-editor', 'marc-record', 'pr Link Here
690
847
691
            var backend = $( this ).data( 'backend' ) || ( state.saveBackend );
848
            var backend = $( this ).data( 'backend' ) || ( state.saveBackend );
692
            if ( state.backend == backend ) {
849
            if ( state.backend == backend ) {
693
                saveRecord( backend + ':' + state.recordID, editor, finishCb );
850
                saveRecord( backend + '/' + state.recordID, editor, finishCb );
694
            } else {
851
            } else {
695
                saveRecord( backend + ':', editor, finishCb );
852
                saveRecord( backend + '/', editor, finishCb );
696
            }
853
            }
697
854
698
            return false;
855
            return false;
Lines 709-725 require( [ 'koha-backend', 'search', 'macros', 'marc-editor', 'marc-record', 'pr Link Here
709
866
710
                    reader.onload = function() {
867
                    reader.onload = function() {
711
                        var record = new MARC.Record();
868
                        var record = new MARC.Record();
869
                        $("#search-overlay").show();
870
                        Search.includedServers = [1];
871
                        showResultsBox();
712
872
713
                        if ( /\.mrc$/.test( file.name ) ) {
873
                        if ( /\.mrc$/.test( file.name ) ) {
714
                            record.loadISO2709( reader.result );
874
                            /*var data = {
875
                                errors: {},
876
                                num_fetched: {},
877
                                hits: [],
878
                                sort_direction: 'asc',
879
                                sort_key: 'title',
880
                            };
881
                            data.hits = [];
882
                            $.each( reader.result.split('\035'), function( i, blob ) {
883
                                if ( !blob ) return;
884
885
                                var record = new MARC.Record();
886
                                record.loadISO2709( blob );
887
888
                                var hit = {
889
                                    server: 1,
890
                                    record: record,
891
                                    index: i,
892
                                    metadata: {},
893
                                };
894
895
                                var _addField = function(field, tag, subf) {
896
                                    var f = record.field(tag);
897
                                    if (!f) return;
898
                                    var s = f.subfield(subf);
899
                                    if (s!==false) hit.metadata[field] = s;
900
                                }
901
902
                                _addField('local_number', '001', '@');
903
                                _addField('isbn', '020', 'a');
904
                                _addField('author', '100', 'a');
905
                                _addField('title', '245', 'a');
906
907
                                data.hits.push(hit);
908
                            } );
909
                            showSearchResults(editor, data);
910
                            $("#search-overlay").hide();*/
911
                            record.loadISO2709( blob );
715
                        } else if ( /\.xml$/.test( file.name ) ) {
912
                        } else if ( /\.xml$/.test( file.name ) ) {
716
                            record.loadMARCXML( reader.result );
913
                            record.loadMARCXML( reader.result );
717
                        } else {
914
                        } else {
718
                            humanMsg.displayAlert( _("Unknown record type, cannot import"), { className: 'humanError' } );
915
                            humanMsg.displayAlert( _("Unknown record type, cannot import"), { className: 'humanError' } );
719
                            return;
916
                            return;
720
                        }
917
                        }
721
722
                       editor.displayRecord( record );
723
                    };
918
                    };
724
919
725
                    reader.readAsText( file );
920
                    reader.readAsText( file );
Lines 736-742 require( [ 'koha-backend', 'search', 'macros', 'marc-editor', 'marc-record', 'pr Link Here
736
        } );
931
        } );
737
932
738
        $('#run-macro').click( function() {
933
        $('#run-macro').click( function() {
739
            var result = Macros.Run( editor, macroEditor.getValue() );
934
            var result = Macros.Run( editor, $('#macro-format').val(), macroEditor.getValue() );
740
935
741
            if ( !result.errors.length ) return false;
936
            if ( !result.errors.length ) return false;
742
937
Lines 808-827 require( [ 'koha-backend', 'search', 'macros', 'marc-editor', 'marc-record', 'pr Link Here
808
1003
809
        $( document ).on( 'click', 'a.search-nav', function() {
1004
        $( document ).on( 'click', 'a.search-nav', function() {
810
            $("#search-overlay").show();
1005
            $("#search-overlay").show();
811
            Search.Fetch( $( this ).data( 'offset' ) );
1006
            Search.Fetch( { offset: $( this ).data( 'offset' ) } );
1007
            return false;
1008
        });
1009
1010
        $( document ).on( 'click', 'th[data-sort-label]', function() {
1011
            $("#search-overlay").show();
1012
            var direction;
1013
1014
            if ( $( this ).hasClass( 'sorting_asc' ) ) {
1015
                direction = 'desc';
1016
            } else {
1017
                direction = 'asc';
1018
            }
1019
1020
            showSearchSorting( $( this ).data( 'sort-label' ), direction );
1021
1022
            Search.Fetch( { sort_key: $( this ).data( 'sort-label' ), sort_direction: direction } );
812
            return false;
1023
            return false;
813
        });
1024
        });
814
1025
1026
        $( document ).on( 'change', 'input.search-toggle-server', function() {
1027
            var server = z3950Servers[ $( this ).parent().data('server-id') ];
1028
            server.checked = this.checked;
1029
1030
            if ( $('#search-results-ui').is( ':visible' ) ) {
1031
                $("#search-overlay").show();
1032
                Search.Fetch();
1033
            }
1034
        } );
1035
815
        // Key bindings
1036
        // Key bindings
816
        bindGlobalKeys();
1037
        bindGlobalKeys();
817
1038
818
        // Setup UI
1039
        // Setup UI
819
        $("#advanced-search-ui, #search-results-ui, #macro-ui").each( function() {
1040
        $("#advanced-search-ui, #search-results-ui, #macro-ui").each( function() {
820
            $(this).modal({ show: false }).css({
1041
            $(this).modal({ show: false });
821
                marginLeft: function() {
822
                    return -($(this).width() / 2);
823
                }
824
            });
825
        } );
1042
        } );
826
1043
827
        var $quicksearch = $('#quicksearch fieldset');
1044
        var $quicksearch = $('#quicksearch fieldset');
Lines 847-852 require( [ 'koha-backend', 'search', 'macros', 'marc-editor', 'marc-record', 'pr Link Here
847
                return '<div id="alerts-container"><ul>' + prevAlerts.join('') + '</ul></div>';
1064
                return '<div id="alerts-container"><ul>' + prevAlerts.join('') + '</ul></div>';
848
            },
1065
            },
849
        });
1066
        });
1067
        $('#new-record' ).click( function() {
1068
            openRecord( 'new/', editor );
1069
            return false;
1070
        } );
850
1071
851
        // Start editor
1072
        // Start editor
852
        Preferences.Load( [% USER_INFO.0.borrowernumber || 0 %] );
1073
        Preferences.Load( [% USER_INFO.0.borrowernumber || 0 %] );
Lines 858-864 require( [ 'koha-backend', 'search', 'macros', 'marc-editor', 'marc-record', 'pr Link Here
858
            onerror: handleSearchError,
1079
            onerror: handleSearchError,
859
        } );
1080
        } );
860
1081
861
        function finishCb() {
1082
        function finishCb( data ) {
1083
            if ( data.error ) openRecord( 'new/', editor, finishCb );
1084
862
            Resources.GetAll().done( function() {
1085
            Resources.GetAll().done( function() {
863
                $("#loading").hide();
1086
                $("#loading").hide();
864
                editor.focus();
1087
                editor.focus();
Lines 870-876 require( [ 'koha-backend', 'search', 'macros', 'marc-editor', 'marc-record', 'pr Link Here
870
        }
1093
        }
871
1094
872
        if ( !document.location.hash || !openRecord( document.location.hash.slice(1), editor, finishCb ) ) {
1095
        if ( !document.location.hash || !openRecord( document.location.hash.slice(1), editor, finishCb ) ) {
873
            openRecord( 'new:', editor, finishCb );
1096
            openRecord( 'new/', editor, finishCb );
874
        }
1097
        }
875
    } );
1098
    } );
876
} )();
1099
} )();
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/editor.tt (-59 / +70 lines)
Lines 1-9 Link Here
1
[% INCLUDE 'doc-head-open.inc' %]
1
[% INCLUDE 'doc-head-open.inc' %]
2
<title>Koha &rsaquo; Cataloging &rsaquo; Editor</title>
2
<title>Koha &rsaquo; Cataloging &rsaquo; Editor</title>
3
[% INCLUDE 'doc-head-close.inc' %]
3
[% INCLUDE 'doc-head-close.inc' %]
4
<link type="text/css" rel="stylesheet" href="[% themelang %]/css/cateditor.css" />
4
<link rel="stylesheet" href="[% themelang %]/css/cateditor.css" />
5
<link type="text/css" rel="stylesheet" href="/intranet-tmpl/lib/codemirror/codemirror.css" />
5
<link rel="stylesheet" href="[% themelang %]/css/datatables.css" />
6
<link rel="stylesheet" type="text/css" href="[% themelang %]/css/humanmsg.css" />
6
<link rel="stylesheet" href="/intranet-tmpl/lib/codemirror/codemirror.css" />
7
<link rel="stylesheet" href="[% themelang %]/css/humanmsg.css" />
7
<script src="[% interface %]/lib/jquery/plugins/humanmsg.js" type="text/javascript"></script>
8
<script src="[% interface %]/lib/jquery/plugins/humanmsg.js" type="text/javascript"></script>
8
[% IF ( bidi ) %]
9
[% IF ( bidi ) %]
9
   <link rel="stylesheet" type="text/css" href="[% themelang %]/css/right-to-left.css" />
10
   <link rel="stylesheet" type="text/css" href="[% themelang %]/css/right-to-left.css" />
Lines 29-34 Link Here
29
<div id="editor">
30
<div id="editor">
30
    <input id="import-records-input" type="file" style="display: none">
31
    <input id="import-records-input" type="file" style="display: none">
31
    <div id="toolbar" class="btn-toolbar">
32
    <div id="toolbar" class="btn-toolbar">
33
        <button class="btn btn-small" id="new-record" title="Open fresh record"><i class="icon-plus"></i> <span>New record</span></button>
32
        <div class="btn-group">
34
        <div class="btn-group">
33
            <button class="btn btn-small" id="save-record" title="Save current record (Ctrl-S)"><i class="icon-hdd"></i> <span>Save</span></button>
35
            <button class="btn btn-small" id="save-record" title="Save current record (Ctrl-S)"><i class="icon-hdd"></i> <span>Save</span></button>
34
            <button class="btn btn-small dropdown-toggle" data-toggle="dropdown">
36
            <button class="btn btn-small dropdown-toggle" data-toggle="dropdown">
Lines 99-158 Link Here
99
</div>
101
</div>
100
102
101
<form id="advanced-search" class="modal-body">
103
<form id="advanced-search" class="modal-body">
102
    <div id="toolbar" class="btn-toolbar">
104
    <div class="span3">
103
        <button class="btn btn-small" type="submit"><i class="icon-search"></i> <span>Search</span></button>
105
        <div id="search-facets">
104
        <button class="btn btn-small" type="reset"><i class="icon-remove"></i> <span>Clear</span></button>
106
            <ul>
107
                <li>Servers:<ul id="advanced-search-servers"></ul></li>
108
            </ul>
109
        </div>
110
    </div>
111
    <div class="span9">
112
        <div id="toolbar" class="btn-toolbar">
113
            <button class="btn btn-small" type="submit"><i class="icon-search"></i> <span>Search</span></button>
114
            <button class="btn btn-small" type="reset"><i class="icon-remove"></i> <span>Clear</span></button>
115
        </div>
116
        <ul id="advanced-search-fields">
117
            <li>
118
                <label for="advanced-search-by-author">Author:</label>
119
                <input class="search-box" data-qualifier="author" id="advanced-search-by-author" />
120
            </li>
121
            <li>
122
                <label for="advanced-search-by-control-number">Control number:</label>
123
                <input class="search-box" data-qualifier="local_number" id="advanced-search-by-control-number" />
124
            </li>
125
            <li>
126
                <label for="advanced-search-by-dewey">Dewey number:</label>
127
                <input class="search-box" data-qualifier="cn_dewey" id="advanced-search-by-dewey" />
128
            </li>
129
            <li>
130
                <label for="advanced-search-by-isbn">ISBN:</label>
131
                <input class="search-box" data-qualifier="isbn" id="advanced-search-by-isbn" />
132
            </li>
133
            <li>
134
                <label for="advanced-search-by-issn">ISSN:</label>
135
                <input class="search-box" data-qualifier="issn" id="advanced-search-by-issn" />
136
            </li>
137
            <li>
138
                <label for="advanced-search-by-lccn">LCCN:</label>
139
                <input class="search-box" data-qualifier="lccn" id="advanced-search-by-lccn" />
140
            </li>
141
            <li>
142
                <label for="advanced-search-by-lc-number">LC call number:</label>
143
                <input class="search-box" data-qualifier="cn_lc" id="advanced-search-by-lc-number" />
144
            </li>
145
            <li>
146
                <label for="advanced-search-by-publisher-number">Publisher number:</label>
147
                <input class="search-box" data-qualifier="music_identifier" id="advanced-search-by-publisher-number" />
148
            </li>
149
            <li>
150
                <label for="advanced-search-by-standard-number">Standard number:</label>
151
                <input class="search-box" data-qualifier="standard_identifier" id="advanced-search-by-standard-number" />
152
            </li>
153
            <li>
154
                <label for="advanced-search-by-subject">Subject:</label>
155
                <input class="search-box" data-qualifier="subject" id="advanced-search-by-subject" />
156
            </li>
157
            <li>
158
                <label for="advanced-search-by-publication-date">Publication date:</label>
159
                <input class="search-box" data-qualifier="date" id="advanced-search-by-publication-date" />
160
            </li>
161
            <li>
162
                <label for="advanced-search-by-title">Title:</label>
163
                <input class="search-box" data-qualifier="title" id="advanced-search-by-title" />
164
            </li>
165
        </ul>
105
    </div>
166
    </div>
106
    <ul id="advanced-search-fields">
107
        <li>
108
            <label for="advanced-search-by-author">Author:</label>
109
            <input class="search-box" data-qualifier="author" id="advanced-search-by-author" />
110
        </li>
111
        <li>
112
            <label for="advanced-search-by-control-number">Control number:</label>
113
            <input class="search-box" data-qualifier="local_number" id="advanced-search-by-control-number" />
114
        </li>
115
        <li>
116
            <label for="advanced-search-by-dewey">Dewey number:</label>
117
            <input class="search-box" data-qualifier="cn_dewey" id="advanced-search-by-dewey" />
118
        </li>
119
        <li>
120
            <label for="advanced-search-by-isbn">ISBN:</label>
121
            <input class="search-box" data-qualifier="isbn" id="advanced-search-by-isbn" />
122
        </li>
123
        <li>
124
            <label for="advanced-search-by-issn">ISSN:</label>
125
            <input class="search-box" data-qualifier="issn" id="advanced-search-by-issn" />
126
        </li>
127
        <li>
128
            <label for="advanced-search-by-lccn">LCCN:</label>
129
            <input class="search-box" data-qualifier="lccn" id="advanced-search-by-lccn" />
130
        </li>
131
        <li>
132
            <label for="advanced-search-by-lc-number">LC call number:</label>
133
            <input class="search-box" data-qualifier="cn_lc" id="advanced-search-by-lc-number" />
134
        </li>
135
        <li>
136
            <label for="advanced-search-by-publisher-number">Publisher number:</label>
137
            <input class="search-box" data-qualifier="music_identifier" id="advanced-search-by-publisher-number" />
138
        </li>
139
        <li>
140
            <label for="advanced-search-by-standard-number">Standard number:</label>
141
            <input class="search-box" data-qualifier="standard_identifier" id="advanced-search-by-standard-number" />
142
        </li>
143
        <li>
144
            <label for="advanced-search-by-subject">Subject:</label>
145
            <input class="search-box" data-qualifier="subject" id="advanced-search-by-subject" />
146
        </li>
147
        <li>
148
            <label for="advanced-search-by-publication-date">Publication date:</label>
149
            <input class="search-box" data-qualifier="date" id="advanced-search-by-publication-date" />
150
        </li>
151
        <li>
152
            <label for="advanced-search-by-title">Title:</label>
153
            <input class="search-box" data-qualifier="title" id="advanced-search-by-title" />
154
        </li>
155
    </ul>
156
</form>
167
</form>
157
168
158
</div>
169
</div>
Lines 190-199 Link Here
190
                <div class="pagination pagination-small">
201
                <div class="pagination pagination-small">
191
                </div>
202
                </div>
192
            </div>
203
            </div>
193
194
            <div id="search-overlay"><span>Loading...</span><div class="progress progress-striped active"><div class="bar" style="width: 100%"></div></div></div>
195
        </div>
204
        </div>
196
    </div>
205
    </div>
206
    <div id="search-overlay"><span>Loading...</span><div class="progress progress-striped active"><div class="bar" style="width: 100%"></div></div></div>
197
</div>
207
</div>
198
208
199
</div>
209
</div>
Lines 209-216 Link Here
209
    <div class="span3"><ul id="macro-list"></ul></div>
219
    <div class="span3"><ul id="macro-list"></ul></div>
210
    <div class="span9" id="macro-editor">
220
    <div class="span9" id="macro-editor">
211
        <div id="macro-toolbar" class="btn-toolbar">
221
        <div id="macro-toolbar" class="btn-toolbar">
212
            <button class="btn btn-small" id="run-macro"itle="Run and edit macros"><i class="icon-play"></i> Run macro</button>
222
            <button class="btn btn-small" id="run-macro" title="Run and edit macros"><i class="icon-play"></i> Run macro</button>
213
            <button class="btn btn-small" id="delete-macro" title="Delete macro"><i class="icon-remove"></i> Delete macro</button>
223
            <button class="btn btn-small" id="delete-macro" title="Delete macro"><i class="icon-remove"></i> Delete macro</button>
224
            <label for="macro-format">Format: </label> <select id="macro-format"></select>
214
            <div id="macro-save-message"></div>
225
            <div id="macro-save-message"></div>
215
        </div>
226
        </div>
216
    </div>
227
    </div>
(-)a/svc/bib (-1 / +1 lines)
Lines 86-92 sub update_bib { Link Here
86
86
87
    my $result = {};
87
    my $result = {};
88
    my $inxml = $query->param('POSTDATA');
88
    my $inxml = $query->param('POSTDATA');
89
    print $query->header(-type => 'text/xml');
89
    print $query->header(-type => 'text/xml', -charset => 'utf-8');
90
90
91
    my $record = eval {MARC::Record::new_from_xml( $inxml, "utf8", C4::Context->preference('marcflavour'))};
91
    my $record = eval {MARC::Record::new_from_xml( $inxml, "utf8", C4::Context->preference('marcflavour'))};
92
    my $do_not_escape = 0;
92
    my $do_not_escape = 0;
(-)a/svc/cataloguing/framework (-17 / +21 lines)
Lines 8-13 use C4::ClassSource; Link Here
8
use C4::Context;
8
use C4::Context;
9
use C4::Biblio;
9
use C4::Biblio;
10
use C4::Service;
10
use C4::Service;
11
use Koha::Database;
11
12
12
my ( $query, $response ) = C4::Service->init( editcatalogue => 'edit_catalogue' );
13
my ( $query, $response ) = C4::Service->init( editcatalogue => 'edit_catalogue' );
13
14
Lines 25-31 foreach my $tag ( sort keys %$tagslib ) { Link Here
25
    push @tags, [ $tag, $taginfo ];
26
    push @tags, [ $tag, $taginfo ];
26
}
27
}
27
28
28
my $dbh = C4::Context->dbh;
29
my $schema = Koha::Database->new->schema;
29
my $authorised_values = {};
30
my $authorised_values = {};
30
31
31
$authorised_values->{branches} = [];
32
$authorised_values->{branches} = [];
Lines 38-46 foreach my $thisbranch ( sort keys %$branches ) { Link Here
38
    push @{ $authorised_values->{branches} }, { value => $thisbranch, lib => $branches->{$thisbranch}->{'branchname'} };
39
    push @{ $authorised_values->{branches} }, { value => $thisbranch, lib => $branches->{$thisbranch}->{'branchname'} };
39
}
40
}
40
41
41
$authorised_values->{itemtypes} = $dbh->selectall_arrayref( q{
42
$authorised_values->{itemtypes} = [ $schema->resultset( "Itemtype" )->search( undef, {
42
    SELECT itemtype AS value, description AS lib FROM itemtypes ORDER BY description
43
    columns => [ { value => 'itemtype' }, { lib => "description" } ],
43
}, { Slice => {} } );
44
    order_by => "description",
45
    result_class => 'DBIx::Class::ResultClass::HashRefInflator'
46
} ) ];
44
47
45
my $class_sources = GetClassSources();
48
my $class_sources = GetClassSources();
46
49
Lines 53-71 foreach my $class_source (sort keys %$class_sources) { Link Here
53
}
56
}
54
57
55
my $branch_limit = C4::Context->userenv ? C4::Context->userenv->{"branch"} : "";
58
my $branch_limit = C4::Context->userenv ? C4::Context->userenv->{"branch"} : "";
56
my $auth_query = "SELECT category, authorised_value, lib
59
my $results;
57
            FROM authorised_values";
60
if( $branch_limit ) {
58
$auth_query .= qq{ LEFT JOIN authorised_values_branches ON ( id = av_id )} if $branch_limit;
61
    $results = $schema->resultset( "AuthorisedValue" )->search(
59
$auth_query .= " AND ( branchcode = ? OR branchcode IS NULL )" if $branch_limit;
62
    { "authorised_values_branches.branchcode" => { "=", [ $branch_limit, undef ] } },
60
$auth_query .= " GROUP BY lib ORDER BY lib, lib_opac";
63
    { join => "authorised_values_branches", order_by => "lib" } );
61
my $authorised_values_sth = $dbh->prepare( $auth_query );
64
} else {
62
$authorised_values_sth->execute(
65
    $results = $schema->resultset( "AuthorisedValue" )->search(
63
    $branch_limit ? $branch_limit : (),
66
    undef,
64
);
67
    { order_by => "lib" } );
65
68
}
66
while ( my ( $category, $value, $lib ) = $authorised_values_sth->fetchrow_array ) {
69
67
    $authorised_values->{$category} ||= [];
70
foreach my $result ( $results->all ) {
68
    push @{ $authorised_values->{$category} }, { value => $lib, lib => $lib };
71
    $authorised_values->{$result->category} ||= [];
72
    push @{ $authorised_values->{$result->category} }, { value => $result->authorised_value, lib => $result->lib };
69
}
73
}
70
74
71
$response->param( framework => \@tags, authorised_values => $authorised_values );
75
$response->param( framework => \@tags, authorised_values => $authorised_values );
(-)a/svc/cataloguing/metasearch (+76 lines)
Line 0 Link Here
1
#!/usr/bin/perl
2
#
3
# Copyright 2014 ByWater Solutions
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
22
use C4::Service;
23
use Encode qw( encode_utf8 );
24
use Koha::MetaSearcher;
25
26
my ( $query, $response ) = C4::Service->init( catalogue => 1 );
27
28
my ( $query_string, $servers ) = C4::Service->require_params( 'q', 'servers' );
29
30
my $server_errors = {};
31
32
my $sort_key = $query->param( 'sort_key' ) || 'title';
33
my $sort_direction = $query->param( 'sort_direction' ) || 'asc';
34
my $offset = $query->param( 'offset' ) || 0;
35
my $page_size = $query->param( 'page_size' ) || 20;
36
my $fetched = $query->param( 'fetched' ) || 100;
37
38
my $searcher = Koha::MetaSearcher->new( {
39
    fetched => $fetched,
40
    on_error => sub {
41
        my ( $server, $exception ) = @_;
42
43
        $server_errors->{ $server->{id} } = $exception->message;
44
    },
45
} );
46
47
$searcher->resultset( $query->param('resultset') ) if ( $query->param('resultset') );
48
49
my @server_ids = split( /,/, $servers );
50
my $stats = $searcher->search( \@server_ids, $query_string );
51
52
$searcher->sort( $sort_key, $sort_direction eq 'desc' ? -1 : 1 );
53
54
my @hits;
55
56
foreach my $hit ( $searcher->results( $offset, $page_size ) ) {
57
    push @hits, {
58
        server => $hit->{server}->{id},
59
        index => $hit->{index},
60
        record => $hit->{record}->as_xml_record(),
61
        metadata => $hit->{metadata}
62
    };
63
}
64
65
$response->param(
66
    resultset => $searcher->resultset,
67
    sort_key => $sort_key,
68
    sort_direction => $sort_direction,
69
    offset => $offset,
70
    page_size => $page_size,
71
    errors => $server_errors,
72
    hits => \@hits,
73
    %$stats
74
);
75
76
C4::Service->return_success( $response );
(-)a/svc/z3950 (-103 lines)
Lines 1-102 Link Here
1
#!/usr/bin/perl
2
#
3
# Copyright 2014 ByWater Solutions
4
5
# 
6
# This file is part of Koha.
7
# 
8
# Koha is free software; you can redistribute it and/or modify it under the
9
# terms of the GNU General Public License as published by the Free Software
10
# Foundation; either version 3 of the License, or (at your option) any later
11
# version.
12
# 
13
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
14
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
15
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
16
# 
17
# You should have received a copy of the GNU General Public License along
18
# with Koha; if not, write to the Free Software Foundation, Inc.,
19
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20
21
use Modern::Perl;
22
23
use C4::Breeding qw( RunZ3950 );
24
use C4::Service;
25
use Encode qw( encode_utf8 );
26
27
use sort 'stable';
28
29
my ( $query, $response ) = C4::Service->init( catalogue => 1 );
30
31
my ( $query_string, $servers ) = C4::Service->require_params( 'q', 'servers' );
32
33
my $server_hits = {};
34
my $server_errors = {};
35
36
my $offset = $query->param( 'offset' ) || 0;
37
my $page_size = $query->param( 'page_size' ) || 20;
38
my $fetched = $query->param( 'fetched' ) || 100;
39
my $empty_flip = -1; # Determines the flip of ordering for records with empty sort keys.
40
41
my @server_ids = split( /,/, $servers );
42
43
my $stats = RunZ3950( \@server_ids, $query_string, {
44
    on_error => sub {
45
        my ( $server, $exception ) = @_;
46
47
        $server_errors->{ $server->{id} } = $exception->message;
48
    },
49
    on_hit => sub {
50
        my ( $server, $hit ) = @_;
51
52
        push @{ $server_hits->{ $server->{id} } }, { server => $server->{id},
53
            index => $hit->{index},
54
            record => encode_utf8( $hit->{record}->as_xml_record() ),
55
            metadata => $hit->{metadata}
56
        };
57
    },
58
59
    offset => 0,
60
    fetch => $fetched,
61
} );
62
63
my @hits;
64
65
foreach my $id ( @server_ids ) {
66
    warn scalar @{ $server_hits->{$id} ||= [] } . ' hits from ' . $id;
67
}
68
69
# Interleave hits; should be replaced by actual relevance ranking at some point
70
foreach my $i ( 0..$fetched ) {
71
    foreach my $id ( @server_ids ) {
72
        my $hit = shift @{ $server_hits->{ $id } };
73
        next unless ( $hit );
74
75
        ( $hit->{sort_key} = $hit->{metadata}->{title} || '' ) =~ s/\W//g;
76
        push @hits, $hit;
77
    }
78
}
79
80
@hits = sort {
81
    # Sort empty records at the end
82
    return -$empty_flip unless $a->{sort_key};
83
    return $empty_flip unless $b->{sort_key};
84
85
    $a->{sort_key} cmp $b->{sort_key};
86
} @hits;
87
88
my @hits_subset;
89
90
foreach my $i ( $offset..( $offset + $page_size - 1 ) ) {
91
    push @hits_subset, $hits[$i] if $hits[$i];
92
}
93
94
$response->param(
95
    offset => $offset,
96
    page_size => $page_size,
97
    errors => $server_errors,
98
    hits => \@hits_subset,
99
    %$stats
100
);
101
102
C4::Service->return_success( $response );
103
- 

Return to bug 11559