Bugzilla – Attachment 36248 Details for
Bug 11559
Professional cataloger's interface
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 11559 followup - refactoring and cleanup
Bug-11559-followup---refactoring-and-cleanup.patch (text/plain), 134.72 KB, created by
Jesse Weaver
on 2015-03-01 07:46:43 UTC
(
hide
)
Description:
Bug 11559 followup - refactoring and cleanup
Filename:
MIME Type:
Creator:
Jesse Weaver
Created:
2015-03-01 07:46:43 UTC
Size:
134.72 KB
patch
obsolete
>From 3a397468eb51acbc67e24ccbca46473fb9104318 Mon Sep 17 00:00:00 2001 >From: Jesse Weaver <pianohacker@gmail.com> >Date: Wed, 11 Jun 2014 01:41:43 -0600 >Subject: [PATCH] Bug 11559 followup - refactoring and cleanup > >Added license headers to all JS files, moved metasearcher code into a >new service that largely lives in Koha::, and massively cleaned up the >internals of the textual MARC machinery. Also, fixed some encoding and >saving issues. >--- > C4/Breeding.pm | 104 +--- > C4/Service.pm | 1 + > Koha/MetaSearcher.pm | 351 +++++++++++ > Koha/MetadataRecord.pm | 25 + > cataloguing/editor.pl | 13 + > .../lib/koha/cateditor/koha-backend.js | 21 +- > .../intranet-tmpl/lib/koha/cateditor/macros.js | 220 +------ > .../intranet-tmpl/lib/koha/cateditor/macros/its.js | 208 +++++++ > .../lib/koha/cateditor/macros/rancor.js | 277 +++++++++ > .../lib/koha/cateditor/marc-editor.js | 662 +++++++++++++++------ > .../intranet-tmpl/lib/koha/cateditor/marc-mode.js | 53 +- > .../lib/koha/cateditor/marc-record.js | 19 + > .../lib/koha/cateditor/preferences.js | 20 + > .../intranet-tmpl/lib/koha/cateditor/resources.js | 19 + > .../intranet-tmpl/lib/koha/cateditor/search.js | 42 +- > .../intranet-tmpl/lib/koha/cateditor/text-marc.js | 19 + > .../intranet-tmpl/lib/koha/cateditor/widget.js | 22 + > koha-tmpl/intranet-tmpl/prog/en/css/cateditor.css | 38 +- > .../prog/en/includes/cateditor-ui.inc | 313 ++++++++-- > .../prog/en/modules/cataloguing/editor.tt | 129 ++-- > svc/bib | 2 +- > svc/cataloguing/framework | 38 +- > svc/cataloguing/metasearch | 76 +++ > svc/z3950 | 102 ---- > 24 files changed, 2071 insertions(+), 703 deletions(-) > create mode 100644 Koha/MetaSearcher.pm > create mode 100644 koha-tmpl/intranet-tmpl/lib/koha/cateditor/macros/its.js > create mode 100644 koha-tmpl/intranet-tmpl/lib/koha/cateditor/macros/rancor.js > create mode 100755 svc/cataloguing/metasearch > delete mode 100755 svc/z3950 > >diff --git a/C4/Breeding.pm b/C4/Breeding.pm >index 9848cb0..d0466d7 100644 >--- a/C4/Breeding.pm >+++ b/C4/Breeding.pm >@@ -39,7 +39,7 @@ BEGIN { > $VERSION = 3.07.00.049; > require Exporter; > @ISA = qw(Exporter); >- @EXPORT = qw(&BreedingSearch &Z3950Search &Z3950SearchAuth &RunZ3950); >+ @EXPORT = qw(&BreedingSearch &Z3950Search &Z3950SearchAuth); > } > > =head1 NAME >@@ -327,10 +327,8 @@ sub _add_rowdata { > my ($row, $record)=@_; > my %fetch= ( > title => 'biblio.title', >- seriestitle => 'biblio.seriestitle', > author => 'biblio.author', > isbn =>'biblioitems.isbn', >- issn =>'biblioitems.issn', > lccn =>'biblioitems.lccn', #LC control number (not call number) > edition =>'biblioitems.editionstatement', > date => 'biblio.copyrightdate', #MARC21 >@@ -714,106 +712,6 @@ sub Z3950SearchAuth { > ); > } > >-sub RunZ3950 { >- my ( $server_ids, $query, $options ) = @_; >- >- $options = { >- offset => 0, >- fetch => 20, >- on_error => sub {}, >- on_hit => sub {}, >- %{ $options || {} } >- }; >- >- my $schema = Koha::Database->new->schema; >- my $stats = { >- num_fetched => { >- map { $_ => 0 } @$server_ids >- }, >- num_hits => { >- map { $_ => 0 } @$server_ids >- }, >- total_fetched => 0, >- total_hits => 0, >- }; >- my $start = clock_gettime( CLOCK_MONOTONIC ); >- my @servers; >- >- foreach my $server ( $schema->resultset('Z3950server')->search( { id => $server_ids } )->all ) { >- my $zoptions = ZOOM::Options->new(); >- $zoptions->option( 'async', 1 ); >- $zoptions->option( 'elementSetName', 'F' ); >- $zoptions->option( 'databaseName', $server->db ); >- $zoptions->option( 'user', $server->userid ) if $server->userid; >- $zoptions->option( 'password', $server->password ) if $server->password; >- $zoptions->option( 'preferredRecordSyntax', $server->syntax ); >- $zoptions->option( 'timeout', $server->timeout ) if $server->timeout; >- >- my $connection = ZOOM::Connection->create($zoptions); >- $connection->connect( $server->host, $server->port ); >- >- push @servers, { >- connection => $connection, >- id => $server->id, >- host => $server->host, >- name => $server->name, >- encoding => ( $server->encoding ? $server->encoding : "iso-5426" ), >- results => $connection->search_pqf( $query ), # Starts the search >- }; >- } >- >- my $servers_left = scalar @servers; >- my $total_raw_size = 0; >- >- while ( $servers_left ) { >- my $i; >- >- # Read pending events from servers until one finishes >- while ( ( $i = ZOOM::event( [ map { $_->{connection} } @servers ] ) ) != 0 ) { >- last if $servers[ $i - 1 ]->{connection}->last_event() == ZOOM::Event::ZEND; >- } >- >- $servers_left--; >- my $server = $servers[ --$i ]; >- my $exception = $server->{connection}->exception(); #ignores errmsg, addinfo, diagset >- >- if ($exception) { >- $options->{on_error}->( $server, $exception ); >- } else { >- my $num_results = $stats->{num_hits}->{ $server->{id} } = $server->{results}->size; >- my $num_fetched = $stats->{num_fetched}->{ $server->{id} } = ( $options->{offset} + $options->{fetch} ) < $num_results ? $options->{fetch} : $num_results; >- >- $stats->{total_hits} += $num_results; >- $stats->{total_fetched} += $num_fetched; >- >- next if ( !$num_results ); >- >- my $hits = $server->{results}->records( $options->{offset}, $num_fetched, 1 ); >- >- if ( !@$hits ) { >- $options->{on_error}->( $server, $server->{connection}->exception() ) if ( $server->{connection}->exception() ); >- next; >- } >- >- foreach my $j ( 0..$#$hits ) { >- $total_raw_size += length $hits->[$j]->raw(); >- my ($marcrecord) = MarcToUTF8Record( $hits->[$j]->raw(), C4::Context->preference('marcflavour'), $server->{encoding} ); #ignores charset return values >- my $metadata = {}; >- _add_rowdata( $metadata, $marcrecord ); >- $options->{on_hit}->( $server, { >- index => $options->{offset} + $j, >- record => $marcrecord, >- metadata => $metadata, >- } ); >- } >- } >- } >- >- $stats->{time} = clock_gettime( CLOCK_MONOTONIC ) - $start; >- >- return $stats; >-} >- > 1; > __END__ > >diff --git a/C4/Service.pm b/C4/Service.pm >index 3decfe9..e41f755 100644 >--- a/C4/Service.pm >+++ b/C4/Service.pm >@@ -57,6 +57,7 @@ our ( $query, $cookie ); > > sub _output { > my ( $response, $status ) = @_; >+ binmode STDOUT, ':encoding(UTF-8)'; > > if ( $query->param( 'callback' ) ) { > output_with_http_headers $query, $cookie, $query->param( 'callback' ) . '(' . $response->output . ');', 'js'; >diff --git a/Koha/MetaSearcher.pm b/Koha/MetaSearcher.pm >new file mode 100644 >index 0000000..7c4c87a >--- /dev/null >+++ b/Koha/MetaSearcher.pm >@@ -0,0 +1,351 @@ >+package Koha::MetaSearcher; >+ >+# Copyright 2014 ByWater Solutions >+# >+# This file is part of Koha. >+# >+# Koha is free software; you can redistribute it and/or modify it >+# under the terms of the GNU General Public License as published by >+# the Free Software Foundation; either version 3 of the License, or >+# (at your option) any later version. >+# >+# Koha is distributed in the hope that it will be useful, but >+# WITHOUT ANY WARRANTY; without even the implied warranty of >+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the >+# GNU General Public License for more details. >+# >+# You should have received a copy of the GNU General Public License >+# along with Koha; if not, see <http://www.gnu.org/licenses>. >+ >+use Modern::Perl; >+ >+use base 'Class::Accessor'; >+ >+use C4::Charset qw( MarcToUTF8Record ); >+use C4::Search qw(); # Purely for new_record_from_zebra >+use DBIx::Class::ResultClass::HashRefInflator; >+use IO::Select; >+use Koha::Cache; >+use Koha::Database; >+use Koha::MetadataRecord; >+use MARC::File::XML; >+use Storable qw( store_fd fd_retrieve ); >+use Time::HiRes qw( clock_gettime CLOCK_MONOTONIC ); >+use UUID; >+use ZOOM; >+ >+use sort 'stable'; >+ >+__PACKAGE__->mk_accessors( qw( fetch offset on_error resultset ) ); >+ >+sub new { >+ my ( $class, $options ) = @_; >+ >+ my ( $uuid, $uuidstring ); >+ UUID::generate($uuid); >+ UUID::unparse( $uuid, $uuidstring ); >+ >+ return bless { >+ offset => 0, >+ fetch => 100, >+ on_error => sub {}, >+ results => [], >+ resultset => $uuidstring, >+ %{ $options || {} } >+ }, $class; >+} >+ >+sub handle_hit { >+ my ( $self, $index, $server, $marcrecord ) = @_; >+ >+ my $record = Koha::MetadataRecord->new( { schema => 'marc', record => $marcrecord } ); >+ >+ my %fetch = ( >+ title => 'biblio.title', >+ seriestitle => 'biblio.seriestitle', >+ author => 'biblio.author', >+ isbn =>'biblioitems.isbn', >+ issn =>'biblioitems.issn', >+ lccn =>'biblioitems.lccn', #LC control number (not call number) >+ edition =>'biblioitems.editionstatement', >+ date => 'biblio.copyrightdate', #MARC21 >+ date2 => 'biblioitems.publicationyear', #UNIMARC >+ ); >+ >+ my $metadata = {}; >+ while ( my ( $key, $kohafield ) = each %fetch ) { >+ $metadata->{$key} = $record->getKohaField($kohafield); >+ } >+ $metadata->{date} //= $metadata->{date2}; >+ >+ push @{ $self->{results} }, { >+ server => $server, >+ index => $index, >+ record => $marcrecord, >+ metadata => $metadata, >+ }; >+} >+ >+sub search { >+ my ( $self, $server_ids, $query ) = @_; >+ >+ my $resultset_expiry = 300; >+ >+ my $cache; >+ eval { $cache = Koha::Cache->new(); }; >+ my $schema = Koha::Database->new->schema; >+ my $stats = { >+ num_fetched => { >+ map { $_ => 0 } @$server_ids >+ }, >+ num_hits => { >+ map { $_ => 0 } @$server_ids >+ }, >+ total_fetched => 0, >+ total_hits => 0, >+ }; >+ my $start = clock_gettime( CLOCK_MONOTONIC ); >+ my $select = IO::Select->new; >+ my @worker_fhs; >+ >+ my @cached_sets; >+ my @servers; >+ >+ foreach my $server_id ( @$server_ids ) { >+ if ( $server_id =~ /^\d+$/ ) { >+ # Z39.50 server >+ my $server = $schema->resultset('Z3950server')->find( >+ { id => $server_id }, >+ { result_class => 'DBIx::Class::ResultClass::HashRefInflator' }, >+ ); >+ $server->{type} = 'z3950'; >+ >+ push @servers, $server; >+ } elsif ( $server_id =~ /(\w+)(?::(\w+))?/ ) { >+ # Special server >+ push @servers, { >+ type => $1, >+ extra => $2, >+ id => $server_id, >+ host => $server_id, >+ name => $server_id, >+ }; >+ } >+ } >+ >+ # HashRefInflator is used so that the information will survive into the fork >+ foreach my $server ( @servers ) { >+ if ( $cache ) { >+ my $set = $cache->get_from_cache( 'z3950-resultset-' . $self->resultset . '-' . $server->{id} ); >+ if ( ref($set) eq 'HASH' ) { >+ $set->{server} = $server; >+ push @cached_sets, $set; >+ next; >+ } >+ } >+ >+ $select->add( $self->_start_worker( $server, $query ) ); >+ } >+ >+ # Handle these while the servers are searching >+ foreach my $set ( @cached_sets ) { >+ $self->_handle_hits( $stats, $set ); >+ } >+ >+ while ( $select->count ) { >+ foreach my $readfh ( $select->can_read() ) { >+ my $result = fd_retrieve( $readfh ); >+ >+ $select->remove( $readfh ); >+ close $readfh; >+ wait; >+ >+ next if ( ref $result ne 'HASH' ); >+ >+ if ( $result->{error} ) { >+ $self->{on_error}->( $result->{server}, $result->{error} ); >+ next; >+ } >+ >+ $self->_handle_hits( $stats, $result ); >+ >+ if ( $cache ) { >+ $cache->set_in_cache( 'z3950-resultset-' . $self->resultset . '-' . $result->{server}->{id}, { >+ hits => $result->{hits}, >+ num_fetched => $result->{num_fetched}, >+ num_hits => $result->{num_hits}, >+ }, $resultset_expiry ); >+ } >+ } >+ } >+ >+ $stats->{time} = clock_gettime( CLOCK_MONOTONIC ) - $start; >+ >+ return $stats; >+} >+ >+sub _start_worker { >+ my ( $self, $server, $query ) = @_; >+ pipe my $readfh, my $writefh; >+ >+ # Accessing the cache or Koha database after the fork is risky, so get any resources we need >+ # here. >+ my $pid; >+ my $marcflavour = C4::Context->preference('marcflavour'); >+ >+ if ( ( $pid = fork ) ) { >+ # Parent process >+ close $writefh; >+ >+ return $readfh; >+ } elsif ( !defined $pid ) { >+ # Error >+ >+ $self->{on_error}->( $server, 'Failed to fork' ); >+ return; >+ } >+ >+ close $readfh; >+ my $connection; >+ my ( $num_hits, $num_fetched, $hits, $results ); >+ >+ eval { >+ if ( $server->{type} eq 'z3950' ) { >+ my $zoptions = ZOOM::Options->new(); >+ $zoptions->option( 'elementSetName', 'F' ); >+ $zoptions->option( 'databaseName', $server->{db} ); >+ $zoptions->option( 'user', $server->{userid} ) if $server->{userid}; >+ $zoptions->option( 'password', $server->{password} ) if $server->{password}; >+ $zoptions->option( 'preferredRecordSyntax', $server->{syntax} ); >+ $zoptions->option( 'timeout', $server->{timeout} ) if $server->{timeout}; >+ >+ $connection = ZOOM::Connection->create($zoptions); >+ >+ $connection->connect( $server->{host}, $server->{port} ); >+ $results = $connection->search_pqf( $query ); # Starts the search >+ } elsif ( $server->{type} eq 'koha' ) { >+ $connection = C4::Context->Zconn( $server->{extra} ); >+ $results = $connection->search_pqf( $query ); # Starts the search >+ } elsif ( $server->{type} eq 'batch' ) { >+ $server->{encoding} = 'utf-8'; >+ } >+ }; >+ if ($@) { >+ store_fd { >+ error => $connection ? $connection->exception() : $@, >+ server => $server, >+ }, $writefh; >+ exit; >+ } >+ >+ if ( $server->{type} eq 'batch' ) { >+ # TODO: actually handle PQF >+ $query =~ s/@\w+ (?:\d+=\d+ )?//g; >+ $query =~ s/"//g; >+ >+ my $schema = Koha::Database->new->schema; >+ $schema->storage->debug(1); >+ my $match_condition = [ map +{ -like => '%' . $_ . '%' }, split( /\s+/, $query ) ]; >+ $hits = [ $schema->resultset('ImportRecord')->search( >+ { >+ import_batch_id => $server->{extra}, >+ -or => [ >+ { 'import_biblios.title' => $match_condition }, >+ { 'import_biblios.author' => $match_condition }, >+ { 'import_biblios.isbn' => $match_condition }, >+ { 'import_biblios.issn' => $match_condition }, >+ ], >+ }, >+ { >+ join => [ qw( import_biblios ) ], >+ rows => $self->{fetch}, >+ } >+ )->get_column( 'marc' )->all ]; >+ >+ $num_hits = $num_fetched = scalar @$hits; >+ } else { >+ $num_hits = $results->size; >+ $num_fetched = ( $self->{offset} + $self->{fetch} ) < $num_hits ? $self->{fetch} : $num_hits; >+ >+ $hits = [ map { $_->raw() } @{ $results->records( $self->{offset}, $num_fetched, 1 ) } ]; >+ } >+ >+ if ( !@$hits && $connection && $connection->exception() ) { >+ store_fd { >+ error => $connection->exception(), >+ server => $server, >+ }, $writefh; >+ exit; >+ } >+ >+ if ( $server->{type} eq 'koha' ) { >+ $hits = [ map { C4::Search::new_record_from_zebra( $server->{extra}, $_ ) } @$hits ]; >+ } else { >+ $hits = [ map { $self->_import_record( $_, $marcflavour, $server->{encoding} ? $server->{encoding} : "iso-5426" ) } @$hits ]; >+ } >+ >+ store_fd { >+ hits => $hits, >+ num_fetched => $num_fetched, >+ num_hits => $num_hits, >+ server => $server, >+ }, $writefh; >+ >+ exit; >+} >+ >+sub _import_record { >+ my ( $self, $raw, $marcflavour, $encoding ) = @_; >+ >+ my ( $marcrecord ) = MarcToUTF8Record( $raw, $marcflavour, $encoding ); #ignores charset return values >+ >+ return $marcrecord; >+} >+ >+sub _handle_hits { >+ my ( $self, $stats, $set ) = @_; >+ >+ my $server = $set->{server}; >+ >+ my $num_hits = $stats->{num_hits}->{ $server->{id} } = $set->{num_hits}; >+ my $num_fetched = $stats->{num_fetched}->{ $server->{id} } = $set->{num_fetched}; >+ >+ $stats->{total_hits} += $num_hits; >+ $stats->{total_fetched} += $num_fetched; >+ >+ foreach my $j ( 0..$#{ $set->{hits} } ) { >+ $self->handle_hit( $self->{offset} + $j, $server, $set->{hits}->[$j] ); >+ } >+} >+ >+sub sort { >+ my ( $self, $key, $direction ) = @_; >+ >+ my $empty_flip = -1; # Determines the flip of ordering for records with empty sort keys. >+ >+ foreach my $hit ( @{ $self->{results} } ) { >+ ( $hit->{sort_key} = $hit->{metadata}->{$key} || '' ) =~ s/\W//g; >+ } >+ >+ $self->{results} = [ sort { >+ # Sort empty records at the end >+ return -$empty_flip unless $a->{sort_key}; >+ return $empty_flip unless $b->{sort_key}; >+ >+ $direction * ( $a->{sort_key} cmp $b->{sort_key} ); >+ } @{ $self->{results} } ]; >+} >+ >+sub results { >+ my ( $self, $offset, $length ) = @_; >+ >+ my @subset; >+ >+ foreach my $i ( $offset..( $offset + $length - 1 ) ) { >+ push @subset, $self->{results}->[$i] if $self->{results}->[$i]; >+ } >+ >+ return @subset; >+} >+ >+1; >diff --git a/Koha/MetadataRecord.pm b/Koha/MetadataRecord.pm >index 865dc0d..7b802e6 100644 >--- a/Koha/MetadataRecord.pm >+++ b/Koha/MetadataRecord.pm >@@ -56,4 +56,29 @@ sub createMergeHash { > } > } > >+sub getKohaField { >+ my ($self, $kohafield) = @_; >+ >+ if ($self->schema =~ m/marc/) { >+ my $relations = C4::Context->marcfromkohafield->{''}; >+ my $tagfield = $relations->{$kohafield}; >+ >+ return '' if ref($tagfield) ne 'ARRAY'; >+ >+ my ($tag, $subfield) = @$tagfield; >+ my @kohafield; >+ foreach my $field ( $self->record->field($tag) ) { >+ if ( $field->tag() < 10 ) { >+ push @kohafield, $field->data(); >+ } else { >+ foreach my $contents ( $field->subfield($subfield) ) { >+ push @kohafield, $contents; >+ } >+ } >+ } >+ >+ return join ' | ', @kohafield; >+ } >+} >+ > 1; >diff --git a/cataloguing/editor.pl b/cataloguing/editor.pl >index 85a1191..4f4f380 100755 >--- a/cataloguing/editor.pl >+++ b/cataloguing/editor.pl >@@ -27,6 +27,8 @@ use C4::Auth; > use C4::Biblio; > use C4::Context; > use C4::Output; >+use DBIx::Class::ResultClass::HashRefInflator; >+use Koha::Database; > > my $input = CGI->new; > >@@ -40,6 +42,17 @@ my ( $template, $loggedinuser, $cookie ) = get_template_and_user( > } > ); > >+my $schema = Koha::Database->new->schema; >+ >+# Available import batches >+$template->{VARS}->{editable_batches} = [ $schema->resultset('ImportBatch')->search( >+ { >+ batch_type => [ 'batch', 'webservice' ], >+ import_status => 'staged', >+ }, >+ { result_class => 'DBIx::Class::ResultClass::HashRefInflator' }, >+) ]; >+ > # Needed information for cataloging plugins > $template->{VARS}->{DefaultLanguageField008} = pack( 'A3', C4::Context->preference('DefaultLanguageField008') || 'eng' ); > >diff --git a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/koha-backend.js b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/koha-backend.js >index c70c3bf..7827f60 100644 >--- a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/koha-backend.js >+++ b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/koha-backend.js >@@ -1,3 +1,22 @@ >+/** >+ * Copyright 2015 ByWater Solutions >+ * >+ * This file is part of Koha. >+ * >+ * Koha is free software; you can redistribute it and/or modify it >+ * under the terms of the GNU General Public License as published by >+ * the Free Software Foundation; either version 3 of the License, or >+ * (at your option) any later version. >+ * >+ * Koha is distributed in the hope that it will be useful, but >+ * WITHOUT ANY WARRANTY; without even the implied warranty of >+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the >+ * GNU General Public License for more details. >+ * >+ * You should have received a copy of the GNU General Public License >+ * along with Koha; if not, see <http://www.gnu.org/licenses>. >+ */ >+ > define( [ '/cgi-bin/koha/svc/cataloguing/framework?frameworkcode=&callback=define', 'marc-record' ], function( defaultFramework, MARC ) { > var _authorised_values = defaultFramework.authorised_values; > var _frameworks = {}; >@@ -61,7 +80,7 @@ define( [ '/cgi-bin/koha/svc/cataloguing/framework?frameworkcode=&callback=defin > record.loadMARCXML(data); > callback(record); > } ).fail( function( data ) { >- callback( { data: { error: data } } ); >+ callback( { error: data } ); > } ); > }, > >diff --git a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/macros.js b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/macros.js >index 8aaa2f3..4d747f9 100644 >--- a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/macros.js >+++ b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/macros.js >@@ -1,194 +1,36 @@ >-define( [ 'widget' ], function( Widget ) { >- function _setIndicators( editor, ind1, ind2 ) { >- var info = editor.getLineInfo( editor.cm.getCursor() ); >- if (!info || !info.subfields) return false; >- >- var cur = editor.cm.getCursor(); >- >- var indicators = [ ind1 || info.contents.substring(4, 5) || '_', ind2 || info.contents.substring(6, 7) || '_' ]; >- >- editor.cm.replaceRange( >- info.tagNumber + ' ' + indicators.join(' ') + ' ' + info.contents.substring(8), >- { line: cur.line, ch: 0 }, >- { line: cur.line }, >- 'marcAware' >- ); >- >- return true; >- } >- >- var _commandGenerators = [ >- [ /^copy field data$/i, function() { >- return function( editor, state ) { >- var info = editor.getLineInfo( editor.cm.getCursor() ); >- if (!info) return false; >- >- if (info.subfields) { >- state.clipboard = info.contents.substring(4); >- } else { >- state.clipboard = info.contents.substring(8); >- } >- }; >- } ], >- [ /^copy subfield data$/i, function() { >- return function( editor, state ) { >- var info = editor.getLineInfo( editor.cm.getCursor() ); >- if (!info) return false; >- >- var cur = editor.cm.getCursor(); >- >- if (info.subfields) { >- for (var i = 0; i < info.subfields.length; i++) { >- var end = i == info.subfields.length - 1 ? info.contents.length : info.subfields[i+1].ch; >- if (cur.ch > end) continue; >- >- state.clipboard = info.contents.substring(info.subfields[i].ch + 3, end); >- return; >- } >- } >- >- return false; >- } >- } ], >- [ /^delete field$/i, function() { >- return function( editor, state ) { >- var cur = editor.cm.getCursor(); >- >- editor.cm.replaceRange( "", { line: cur.line, ch: 0 }, { line: cur.line + 1, ch: 0 }, 'marcAware' ); >- } >- } ], >- [ /^goto field end$/i, function() { >- return function( editor, state ) { >- editor.cm.setCursor( { line: editor.cm.lastLine() } ); >- } >- } ], >- [ /^goto field (\w{3})$/i, function(field) { >- var matcher = new RegExp('^' + field + ' '); >- return function( editor, state ) { >- for ( var line = 0, contents; (contents = editor.cm.getLine(line)); line++ ) { >- if ( matcher.exec( contents ) ) { >- editor.cm.setCursor( { line: line, ch: 0 } ); >- return; >- } >- } >- >- return false; >- } >- } ], >- [ /^goto subfield end$/i, function() { >- return function( editor, state ) { >- var cur = editor.cm.getCursor(); >- >- editor.cm.setCursor( { line: cur.line } ); >- } >- } ], >- [ /^goto subfield (\w)$/i, function( code ) { >- return function( editor, state ) { >- var info = editor.getLineInfo( editor.cm.getCursor() ); >- if (!info || !info.subfields) return false; >- >- var cur = editor.cm.getCursor(); >- >- for (var i = 0; i < info.subfields.length; i++) { >- if ( info.subfields[i].code != code ) continue; >- >- var end = i == info.subfields.length - 1 ? info.contents.length : info.subfields[i+1].ch; >- editor.cm.setCursor( { line: cur.line, ch: end } ); >- return; >- } >- >- return false; >- } >- } ], >- [ /^insert (new )?field (\w{3}) data=(.*)/i, function(undef, field, data) { >- var new_contents = field + ( field < '100' ? ' ' : ' _ _ ' ) + data.replace(/\\([0-9a-z])/g, '$$$1 '); >- return function( editor, state ) { >- var line, contents; >- >- for ( line = 0; (contents = editor.cm.getLine(line)); line++ ) { >- if ( contents && contents[0] > field[0] ) break; >- } >- >- if ( line > editor.cm.lastLine() ) { >- new_contents = '\n' + new_contents; >- } else { >- new_contents = new_contents + '\n'; >- } >- >- editor.cm.replaceRange( new_contents, { line: line, ch: 0 }, null, 'marcAware' ); >- editor.cm.setCursor( { line: line, ch: 0 } ); >- } >- } ], >- [ /^insert (new )?subfield (\w) data=(.*)/i, function(undef, subfield, data) { >- return function( editor, state ) { >- editor.cm.replaceRange( '$' + subfield + ' ' + data, { line: editor.cm.getCursor().line }, null, 'marcAware' ); >- } >- } ], >- [ /^paste$/i, function() { >- return function( editor, state ) { >- var cur = editor.cm.getCursor(); >- >- editor.cm.replaceRange( state.clipboard, cur, null, 'marcAware' ); >- } >- } ], >- [ /^set indicator([12])=([ _0-9])$/i, function( ind, value ) { >- return function( editor, state ) { >- return ind == '1' ? _setIndicators( editor, ind1, null ) : _setIndicators( editor, null, ind2 ); >- } >- } ], >- [ /^set indicators=([ _0-9])([ _0-9])$/i, function( ind1, ind2 ) { >- return function( editor, state ) { >- return _setIndicators( editor, ind1, ind2 ); >- } >- } ], >- ]; >- >+/** >+ * Copyright 2015 ByWater Solutions >+ * >+ * This file is part of Koha. >+ * >+ * Koha is free software; you can redistribute it and/or modify it >+ * under the terms of the GNU General Public License as published by >+ * the Free Software Foundation; either version 3 of the License, or >+ * (at your option) any later version. >+ * >+ * Koha is distributed in the hope that it will be useful, but >+ * WITHOUT ANY WARRANTY; without even the implied warranty of >+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the >+ * GNU General Public License for more details. >+ * >+ * You should have received a copy of the GNU General Public License >+ * along with Koha; if not, see <http://www.gnu.org/licenses>. >+ */ >+ >+define( [ 'macros/its', 'macros/rancor' ], function( ITSMacro, RancorMacro ) { > var Macros = { >- Compile: function( macro ) { >- var result = { commands: [], errors: [] }; >- >- $.each( macro.split(/\r\n|\n/), function( line, contents ) { >- var command; >- >- if ( contents.match(/^\s*$/) ) return; >- >- $.each( _commandGenerators, function( undef, gen ) { >- var match; >- >- if ( !( match = gen[0].exec( contents ) ) ) return; >- >- command = gen[1].apply(null, match.slice(1)); >- return false; >- } ); >- >- if ( !command ) { >- result.errors.push( { line: line, error: 'unrecognized' } ); >- } >- >- result.commands.push( { func: command, orig: contents, line: line } ); >- } ); >- >- return result; >+ formats: { >+ its: { >+ description: 'TLC® ITS', >+ Run: ITSMacro.Run, >+ }, >+ rancor: { >+ description: 'Rancor', >+ Run: RancorMacro.Run, >+ }, > }, >- Run: function( editor, macro ) { >- var compiled = Macros.Compile(macro); >- if ( compiled.errors.length ) return { errors: compiled.errors }; >- var state = { >- clipboard: '', >- }; >- >- var result = { errors: [] }; >- >- editor.cm.operation( function() { >- $.each( compiled.commands, function( undef, command ) { >- if ( command.func( editor, state ) === false ) { >- result.errors.push( { line: command.line, error: 'failed' } ); >- return false; >- } >- } ); >- } ); >- >- return result; >+ Run: function( editor, format, macro ) { >+ return Macros.formats[format].Run( editor, macro ); > }, > }; > >diff --git a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/macros/its.js b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/macros/its.js >new file mode 100644 >index 0000000..07bfbeb >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/macros/its.js >@@ -0,0 +1,208 @@ >+/** >+ * Copyright 2015 ByWater Solutions >+ * >+ * This file is part of Koha. >+ * >+ * Koha is free software; you can redistribute it and/or modify it >+ * under the terms of the GNU General Public License as published by >+ * the Free Software Foundation; either version 3 of the License, or >+ * (at your option) any later version. >+ * >+ * Koha is distributed in the hope that it will be useful, but >+ * WITHOUT ANY WARRANTY; without even the implied warranty of >+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the >+ * GNU General Public License for more details. >+ * >+ * You should have received a copy of the GNU General Public License >+ * along with Koha; if not, see <http://www.gnu.org/licenses>. >+ */ >+ >+define( function() { >+ var NAV_FAILED = new Object(); >+ var NAV_SUCCEEDED = new Object(); >+ >+ var _commandGenerators = [ >+ [ /^copy field data$/i, function() { >+ return function( editor, state ) { >+ if ( state.field == null ) return; >+ >+ return state.field.getText(); >+ }; >+ } ], >+ [ /^copy subfield data$/i, function() { >+ return function( editor, state ) { >+ if ( state.field == null ) return; >+ >+ var cur = editor.getCursor(); >+ var subfields = state.field.getSubfields(); >+ >+ for (var i = 0; i < subfields.length; i++) { >+ if ( cur.ch > subfields[i].end ) continue; >+ >+ state.clipboard = subfields[i].text; >+ return; >+ } >+ >+ return false; >+ } >+ } ], >+ [ /^del(ete)? field$/i, function() { >+ return function( editor, state ) { >+ if ( state.field == null ) return; >+ >+ state.field.delete(); >+ return NAV_FAILED; >+ } >+ } ], >+ [ /^goto field end$/i, function() { >+ return function( editor, state ) { >+ if ( state.field == null ) return NAV_FAILED; >+ var cur = editor.cm.getCursor(); >+ >+ editor.cm.setCursor( { line: cur.line } ); >+ return NAV_SUCCEEDED; >+ } >+ } ], >+ [ /^goto field (\w{3})$/i, function(tag) { >+ return function( editor, state ) { >+ var field = editor.getFirstField(tag); >+ if ( field == null ) return NAV_FAILED; >+ >+ field.focus(); >+ return NAV_SUCCEEDED; >+ } >+ } ], >+ [ /^goto subfield end$/i, function() { >+ return function( editor, state ) { >+ if ( state.field == null ) return NAV_FAILED; >+ >+ var cur = editor.getCursor(); >+ var subfields = state.field.getSubfields(); >+ >+ for (var i = 0; i < subfields.length; i++) { >+ if ( cur.ch > subfields[i].end ) continue; >+ >+ subfield.focusEnd(); >+ return NAV_SUCCEEDED; >+ } >+ >+ return NAV_FAILED; >+ } >+ } ], >+ [ /^goto subfield (\w)$/i, function( code ) { >+ return function( editor, state ) { >+ if ( state.field == null ) return NAV_FAILED; >+ >+ var subfield = state.field.getFirstSubfield( code ); >+ if ( subfield == null ) return NAV_FAILED; >+ >+ subfield.focus(); >+ return NAV_SUCCEEDED; >+ } >+ } ], >+ [ /^insert (new )?field (\w{3}) data=(.*)/i, function(undef, tag, text) { >+ text = text.replace(/\\([0-9a-z])/g, '$$$1 '); >+ return function( editor, state ) { >+ editor.createFieldGrouped(tag).setText(text).focus(); >+ return NAV_SUCCEEDED; >+ } >+ } ], >+ [ /^insert (new )?subfield (\w) data=(.*)/i, function(undef, code, text) { >+ return function( editor, state ) { >+ if ( state.field == null ) return; >+ >+ state.field.appendSubfield(code).setText(text); >+ } >+ } ], >+ [ /^paste$/i, function() { >+ return function( editor, state ) { >+ var cur = editor.cm.getCursor(); >+ >+ editor.cm.replaceRange( state.clipboard, cur, null, 'marcAware' ); >+ } >+ } ], >+ [ /^set indicator([12])=([ _0-9])$/i, function( ind, value ) { >+ return function( editor, state ) { >+ if ( state.field == null ) return; >+ if ( state.field.isControlField ) return false; >+ >+ if ( ind == '1' ) { >+ state.field.setIndicator1(value); >+ return true; >+ } else if ( ind == '2' ) { >+ state.field.setIndicator2(value); >+ return true; >+ } else { >+ return false; >+ } >+ } >+ } ], >+ [ /^set indicators=([ _0-9])([ _0-9])?$/i, function( ind1, ind2 ) { >+ return function( editor, state ) { >+ if ( state.field == null ) return; >+ if ( state.field.isControlField ) return false; >+ >+ state.field.setIndicator1(ind1); >+ state.field.setIndicator2(ind2 || '_'); >+ } >+ } ], >+ ]; >+ >+ var ITSMacro = { >+ Compile: function( macro ) { >+ var result = { commands: [], errors: [] }; >+ >+ $.each( macro.split(/\r\n|\n/), function( line, contents ) { >+ var command; >+ >+ if ( contents.match(/^\s*$/) ) return; >+ >+ $.each( _commandGenerators, function( undef, gen ) { >+ var match; >+ >+ if ( !( match = gen[0].exec( contents ) ) ) return; >+ >+ command = gen[1].apply(null, match.slice(1)); >+ return false; >+ } ); >+ >+ if ( !command ) { >+ result.errors.push( { line: line, error: 'unrecognized' } ); >+ } >+ >+ result.commands.push( { func: command, orig: contents, line: line } ); >+ } ); >+ >+ return result; >+ }, >+ Run: function( editor, macro ) { >+ var compiled = ITSMacro.Compile(macro); >+ if ( compiled.errors.length ) return { errors: compiled.errors }; >+ var state = { >+ clipboard: '', >+ field: null, >+ }; >+ >+ var run_result = { errors: [] }; >+ >+ editor.cm.operation( function() { >+ $.each( compiled.commands, function( undef, command ) { >+ var result = command.func( editor, state ); >+ >+ if ( result == NAV_FAILED ) { >+ state.field = null; >+ } else if ( result == NAV_SUCCEEDED ) { >+ state.field = editor.getCurrentField(); >+ } else if ( result === false ) { >+ run_result.errors.push( { line: command.line, error: 'failed' } ); >+ return false; >+ } >+ } ); >+ } ); >+ >+ return run_result; >+ }, >+ }; >+ >+ return ITSMacro; >+} ); >diff --git a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/macros/rancor.js b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/macros/rancor.js >new file mode 100644 >index 0000000..e9b484e >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/macros/rancor.js >@@ -0,0 +1,277 @@ >+/** >+ * Copyright 2015 ByWater Solutions >+ * >+ * This file is part of Koha. >+ * >+ * Koha is free software; you can redistribute it and/or modify it >+ * under the terms of the GNU General Public License as published by >+ * the Free Software Foundation; either version 3 of the License, or >+ * (at your option) any later version. >+ * >+ * Koha is distributed in the hope that it will be useful, but >+ * WITHOUT ANY WARRANTY; without even the implied warranty of >+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the >+ * GNU General Public License for more details. >+ * >+ * You should have received a copy of the GNU General Public License >+ * along with Koha; if not, see <http://www.gnu.org/licenses>. >+ */ >+ >+define( [ 'marc-editor' ], function( MARCEditor ) { >+ // These are the generators for targets that appear on the left-hand side of an assignment. >+ var _lhsGenerators = [ >+ // Field; will replace the entire contents of the tag except for indicators. >+ // Examples: >+ // * 245 - will return the first 245 tag it finds, or create a new one >+ // * new 245 - will always create a new 245 >+ // * new 245 grouped - will always create a new 245, and insert it at the end of the 2xx >+ // block >+ [ /^(new )?(\w{3})( (grouped))?$/, function( forceCreate, tag, position, positionGrouped ) { >+ if ( !forceCreate && positionGrouped ) return null; >+ >+ // The extra argument allows the delete command to prevent this from needlessly creating >+ // a tag that it is about to delete. >+ return function( editor, state, extra ) { >+ extra = extra || {}; >+ >+ if ( !forceCreate ) { >+ var result = editor.getFirstField(tag); >+ >+ if ( result != null || extra.dontCreate ) return result; >+ } >+ >+ if ( positionGrouped ) { >+ return editor.createFieldGrouped(tag); >+ } else { >+ return editor.createFieldOrdered(tag); >+ } >+ } >+ } ], >+ >+ // This regex is a little complicated, but allows for the following possibilities: >+ // * 245a - Finds the first 245 field, then tries to find an a subfield within it. If none >+ // exists, it is created. Will still fail if there is no 245 field. >+ // * new 245a - always creates a new a subfield. >+ // * new 245a at end - does the same as the above. >+ // * $a or new $a - does the same as the above, but for the last-used tag. >+ // * new 245a after b - creates a new subfield, placing it after the first subfield $b. >+ [ /^(new )?(\w{3}|\$)(\w)( (at end)| after (\w))?$/, function( forceCreate, tag, code, position, positionAtEnd, positionAfterSubfield ) { >+ if ( tag != '$' && tag < '010' ) return null; >+ if ( !forceCreate && position ) return null; >+ >+ return function( editor, state, extra ) { >+ extra = extra || {}; >+ >+ var field; >+ >+ if ( tag == '$' ) { >+ field = state.field; >+ } else { >+ field = editor.getFirstField(tag); >+ } >+ if ( field == null || field.isControlField ) return null; >+ >+ if ( !forceCreate ) { >+ var subfield = field.getFirstSubfield(code) >+ >+ if ( subfield != null || extra.dontCreate ) return subfield; >+ } >+ >+ if ( !position || position == ' at end' ) { >+ return field.appendSubfield(code); >+ } else if ( positionAfterSubfield ) { >+ var afterSubfield = field.getFirstSubfield(positionAfterSubfield); >+ >+ if ( afterSubfield == null ) return null; >+ >+ return field.insertSubfield( code, afterSubfield.index + 1 ); >+ } >+ } >+ } ], >+ >+ // Can set indicatators either for a particular field or the last-used tag. >+ [ /^((\w{3}) )?indicators$/, function( undef, tag ) { >+ if ( tag && tag < '010' ) return null; >+ >+ return function( editor, state ) { >+ var field; >+ >+ if ( tag == null ) { >+ field = state.field; >+ } else { >+ field = editor.getFirstField(tag); >+ } >+ if ( field == null || field.isControlField ) return null; >+ >+ return { >+ field: field, >+ setText: function( text ) { >+ field.setIndicator1( text.substr( 0, 1 ) ); >+ field.setIndicator2( text.substr( 1, 1 ) ); >+ } >+ }; >+ } >+ } ], >+ ]; >+ >+ // These patterns, on the other hand, appear inside interpolations on the right hand side. >+ var _rhsGenerators = [ >+ [ /^(\w{3})$/, function( tag ) { >+ return function( editor, state, extra ) { >+ return editor.getFirstField(tag); >+ } >+ } ], >+ [ /^(\w{3})(\w)$/, function( tag, code ) { >+ if ( tag < '010' ) return null; >+ >+ return function( editor, state, extra ) { >+ extra = extra || {}; >+ >+ var field = editor.getFirstField(tag); >+ if ( field == null ) return null; >+ >+ return field.getFirstSubfield(code); >+ } >+ } ], >+ ]; >+ >+ var _commandGenerators = [ >+ [ /^delete (.+)$/, function( target ) { >+ var target_closure = _generate( _lhsGenerators, target ); >+ if ( !target_closure ) return null; >+ >+ return function( editor, state ) { >+ var target = target_closure( editor, state, { dontCreate: true } ); >+ if ( target == null ) return; >+ if ( !target.delete ) return false; >+ >+ state.field = null; // As other fields may have been invalidated >+ target.delete(); >+ } >+ } ], >+ [ /^([^=]+)=([^=]*)$/, function( lhs_desc, rhs_desc ) { >+ var lhs_closure = _generate( _lhsGenerators, lhs_desc ); >+ if ( !lhs_closure ) return null; >+ >+ var rhs_closure = _generateInterpolation( _rhsGenerators, rhs_desc ); >+ if ( !rhs_closure ) return null; >+ >+ return function( editor, state ) { >+ var lhs = lhs_closure( editor, state ); >+ if ( lhs == null ) return; >+ >+ state.field = lhs.field || lhs; >+ >+ try { >+ return lhs.setText( rhs_closure( editor, state ) ); >+ } catch (e) { >+ if ( e instanceof MARCEditor.FieldError ) { >+ return false; >+ } else { >+ throw e; >+ } >+ } >+ }; >+ } ], >+ ]; >+ >+ function _generate( set, contents ) { >+ var closure; >+ >+ if ( contents.match(/^\s*$/) ) return; >+ >+ $.each( set, function( undef, gen ) { >+ var match; >+ >+ if ( !( match = gen[0].exec( contents ) ) ) return; >+ >+ closure = gen[1].apply(null, match.slice(1)); >+ return false; >+ } ); >+ >+ return closure; >+ } >+ >+ function _generateInterpolation( set, contents ) { >+ // While this regex will not match at all for an empty string, that just leaves an empty >+ // parts array which yields an empty string (which is what we want.) >+ var matcher = /\{([^}]+)\}|([^{]+)/g; >+ var match; >+ >+ var parts = []; >+ >+ while ( ( match = matcher.exec(contents) ) ) { >+ var closure; >+ if ( match[1] ) { >+ // Found an interpolation >+ var rhs_closure = _generate( set, match[1] ); >+ if ( rhs_closure == null ) return null; >+ >+ closure = ( function(rhs_closure) { return function( editor, state ) { >+ var rhs = rhs_closure( editor, state ); >+ >+ return rhs ? rhs.getText() : ''; >+ } } )( rhs_closure ); >+ } else { >+ // Plain text (needs artificial closure to keep match) >+ closure = ( function(text) { return function() { return text }; } )( match[2] ); >+ } >+ >+ parts.push( closure ); >+ } >+ >+ return function( editor, state ) { >+ var result = ''; >+ $.each( parts, function( i, part ) { >+ result += part( editor, state ); >+ } ); >+ >+ return result; >+ }; >+ } >+ >+ var RancorMacro = { >+ Compile: function( macro ) { >+ var result = { commands: [], errors: [] }; >+ >+ $.each( macro.split(/\r\n|\n/), function( line, contents ) { >+ contents = contents.replace( /#.*$/, '' ); >+ if ( contents.match(/^\s*$/) ) return; >+ >+ var command = _generate( _commandGenerators, contents ); >+ >+ if ( !command ) { >+ result.errors.push( { line: line, error: 'unrecognized' } ); >+ } >+ >+ result.commands.push( { func: command, orig: contents, line: line } ); >+ } ); >+ >+ return result; >+ }, >+ Run: function( editor, macro ) { >+ var compiled = RancorMacro.Compile(macro); >+ if ( compiled.errors.length ) return { errors: compiled.errors }; >+ var state = { >+ field: null, >+ }; >+ >+ var run_result = { errors: [] }; >+ >+ editor.cm.operation( function() { >+ $.each( compiled.commands, function( undef, command ) { >+ var result = command.func( editor, state ); >+ >+ if ( result === false ) { >+ run_result.errors.push( { line: command.line, error: 'failed' } ); >+ return false; >+ } >+ } ); >+ } ); >+ >+ return run_result; >+ }, >+ }; >+ >+ return RancorMacro; >+} ); >diff --git a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/marc-editor.js b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/marc-editor.js >index 3c6992b..025d10d 100644 >--- a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/marc-editor.js >+++ b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/marc-editor.js >@@ -1,38 +1,43 @@ >+/** >+ * Copyright 2015 ByWater Solutions >+ * >+ * This file is part of Koha. >+ * >+ * Koha is free software; you can redistribute it and/or modify it >+ * under the terms of the GNU General Public License as published by >+ * the Free Software Foundation; either version 3 of the License, or >+ * (at your option) any later version. >+ * >+ * Koha is distributed in the hope that it will be useful, but >+ * WITHOUT ANY WARRANTY; without even the implied warranty of >+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the >+ * GNU General Public License for more details. >+ * >+ * You should have received a copy of the GNU General Public License >+ * along with Koha; if not, see <http://www.gnu.org/licenses>. >+ */ >+ > define( [ 'marc-record', 'koha-backend', 'preferences', 'text-marc', 'widget' ], function( MARC, KohaBackend, Preferences, TextMARC, Widget ) { > var NOTIFY_TIMEOUT = 250; > > function editorCursorActivity( cm ) { > var editor = cm.marceditor; >- if ( editor.textMode ) return; >- >- $('#status-tag-info').empty(); >- $('#status-subfield-info').empty(); >- >- var info = editor.getLineInfo( cm.getCursor() ); >- >- if ( !info ) return; // No tag at all on this line >- >- var taginfo = KohaBackend.GetTagInfo( '', info.tagNumber ); >- $('#status-tag-info').html( '<strong>' + info.tagNumber + ':</strong> ' ); >- >- if ( taginfo ) { >- $('#status-tag-info').append( taginfo.lib ); >+ var field = editor.getCurrentField(); >+ if ( !field ) return; > >- if ( !info.currentSubfield ) return; // No current subfield >- >- var subfieldinfo = taginfo.subfields[info.currentSubfield]; >- $('#status-subfield-info').html( '<strong>$' + info.currentSubfield + ':</strong> ' ); >- >- if ( subfieldinfo ) { >- $('#status-subfield-info').append( subfieldinfo.lib ); >- } else { >- $('#status-subfield-info').append( '<em>' + _("Unknown subfield") + '</em>' ); >- } >+ // Set overwrite mode for tag numbers/indicators and contents of fixed fields >+ if ( field.isControlField || cm.getCursor().ch < 8 ) { >+ cm.toggleOverwrite(true); > } else { >- $('#status-tag-info').append( '<em>' + _("Unknown tag") + '</em>' ); >+ cm.toggleOverwrite(false); > } >+ >+ editor.onCursorActivity(); > } > >+ // This function exists to prevent inserting or partially deleting text that belongs to a >+ // widget. The 'marcAware' change source exists for other parts of the editor code to bypass >+ // this check. > function editorBeforeChange( cm, change ) { > var editor = cm.marceditor; > if ( editor.textMode || change.origin == 'marcAware' ) return; >@@ -62,15 +67,15 @@ define( [ 'marc-record', 'koha-backend', 'preferences', 'text-marc', 'widget' ], > } > > var startLine, endLine; >- if ( change.text.length == 2 && from.line == to.line && from.ch == to.ch) { >+ if ( change.text.length == 2 && from.line == to.line && from.ch == to.ch ) { > if ( from.ch == 0 ) { > startLine = endLine = from.line; > } else if ( from.ch == cm.getLine(from.line).length ){ > startLine = endLine = from.line + 1; > } > } else { >- startLine = (from.ch == cm.getLine(from.line).length && from.line < to.line) ? Math.min(cm.lastLine(), from.line + 1) : from.line; >- endLine = ((to.ch == 0 && from.line < to.line) ? Math.max(to.line - 1, 0) : to.line) + change.text.length - 1; >+ startLine = ( from.ch == cm.getLine( from.line ).length && from.line < to.line ) ? Math.min( cm.lastLine(), from.line + 1 ) : from.line; >+ endLine = ( ( to.ch == 0 && from.line < to.line ) ? Math.max( to.line - 1, 0 ) : to.line ) + change.text.length - 1; > } > > for ( var line = startLine; line <= endLine; line++ ) { >@@ -87,25 +92,49 @@ define( [ 'marc-record', 'koha-backend', 'preferences', 'text-marc', 'widget' ], > } > > // Editor helper functions >- function activateTabPosition( cm, cur, idx ) { >- cm.setCursor( cur ); >- Widget.ActivateAt( cm, cur, idx ); >+ function activateTabPosition( cm, pos, idx ) { >+ // Allow tabbing to as-yet-nonexistent positions >+ var lenDiff = pos.ch - cm.getLine( pos.line ).length; >+ if ( lenDiff > 0 ) { >+ var extra = ''; >+ while ( lenDiff-- > 0 ) extra += ' '; >+ if ( pos.prefill ) extra += pos.prefill; >+ cm.replaceRange( extra, { line: pos.line } ); >+ } >+ >+ cm.setCursor( pos ); >+ Widget.ActivateAt( cm, pos, idx ); > } > > function getTabPositions( editor, cur ) { >- var info = editor.getLineInfo( cur || editor.cm.getCursor() ); >+ cur = cur || editor.cm.getCursor(); >+ var field = editor.getFieldAt( cur.line ); > >- if ( info ) { >- if ( info.subfields ) { >- var positions = [ 0, 4, 6 ]; >+ if ( field ) { >+ if ( field.isControlField ) { >+ var positions = [ { ch: 0 }, { ch: 4 } ]; > >- $.each( info.subfields, function( undef, subfield ) { >- positions.push( subfield.ch + 3 ); >+ $.each( positions, function( undef, pos ) { >+ pos.line = cur.line; > } ); > > return positions; > } else { >- return [ 0, 4 ]; >+ var positions = [ { ch: 0 }, { ch: 4, prefill: '_' }, { ch: 6, prefill: '_' } ]; >+ >+ $.each( positions, function( undef, pos ) { >+ pos.line = cur.line; >+ } ); >+ $.each( field.getSubfields(), function( undef, subfield ) { >+ positions.push( { line: cur.line, ch: subfield.contentsStart } ); >+ } ); >+ >+ // Allow to tab to start of empty field >+ if ( field.getSubfields().length == 0 ) { >+ positions.push( { line: cur.line, ch: 8 } ); >+ } >+ >+ return positions; > } > } else { > return []; >@@ -119,34 +148,21 @@ define( [ 'marc-record', 'koha-backend', 'preferences', 'text-marc', 'widget' ], > cm.setCursor( { line: cursor.line + 1, ch: 0 } ); > }, > >- 'Shift-Enter': function( cm ) { >- var cursor = cm.getCursor(); >- cm.replaceRange( '\n', { line: cursor.line, ch: 0 }, null, 'marcAware' ); >- cm.setCursor( { line: cursor.line, ch: 0 } ); >- }, >- > 'Ctrl-X': function( cm ) { >- // Delete subfield (or cut) >+ // Delete line (or cut) > if ( cm.somethingSelected() ) return true; > >- var cur = cm.getCursor(); >- var info = cm.marceditor.getLineInfo( cur ); >- if ( !info || !info.subfields ) return true; >- >- for (var i = 0; i < info.subfields.length; i++) { >- var end = i == info.subfields.length - 1 ? info.contents.length : info.subfields[i+1].ch; >- if (cur.ch > end) continue; >- >- cm.replaceRange( "", { line: cur.line, ch: info.subfields[i].ch }, { line: cur.line, ch: end }, 'marcAware' ); >- return; >- } >+ var field = cm.marceditor.getCurrentField(); >+ if ( field ) field.delete(); > }, > > 'Shift-Ctrl-X': function( cm ) { >- // Delete line >- var cur = cm.getCursor(); >+ // Delete subfield >+ var field = cm.marceditor.getCurrentField(); >+ if ( !field ) return; > >- cm.replaceRange( "", { line: cur.line, ch: 0 }, { line: cur.line + 1, ch: 0 }, 'marcAware' ); >+ var subfield = field.getSubfieldAt( cm.getCursor().ch ); >+ if ( subfield ) subfield.delete(); > }, > > Tab: function( cm ) { >@@ -155,8 +171,8 @@ define( [ 'marc-record', 'koha-backend', 'preferences', 'text-marc', 'widget' ], > var cur = cm.getCursor(); > > for ( var i = 0; i < positions.length; i++ ) { >- if ( positions[i] > cur.ch ) { >- activateTabPosition( cm, { line: cur.line, ch: positions[i] } ); >+ if ( positions[i].ch > cur.ch ) { >+ activateTabPosition( cm, positions[i] ); > return false; > } > } >@@ -170,27 +186,267 @@ define( [ 'marc-record', 'koha-backend', 'preferences', 'text-marc', 'widget' ], > var cur = cm.getCursor(); > > for ( var i = positions.length - 1; i >= 0; i-- ) { >- if ( positions[i] < cur.ch ) { >- activateTabPosition( cm, { line: cur.line, ch: positions[i] } ); >+ if ( positions[i].ch < cur.ch ) { >+ activateTabPosition( cm, positions[i] ); > return false; > } > } > > if ( cur.line == 0 ) return; > >- var prevPositions = getTabPositions( cm, { line: cur.line - 1, ch: cm.getLine( cur.line - 1 ).length } ); >+ var prevPositions = getTabPositions( cm.marceditor, { line: cur.line - 1, ch: cm.getLine( cur.line - 1 ).length } ); > > if ( prevPositions.length ) { >- activateTabPosition( cm, { line: cur.line - 1, ch: prevPositions[ prevPositions.length - 1 ] }, -1 ); >+ activateTabPosition( cm, prevPositions[ prevPositions.length - 1 ], -1 ); > } else { > cm.setCursor( { line: cur.line - 1, ch: 0 } ); > } > }, >+ >+ 'Ctrl-D': function( cm ) { >+ // Insert subfield delimiter >+ // This will be extended later to allow either a configurable subfield delimiter or just >+ // make it be the double cross. >+ var cur = cm.getCursor(); >+ >+ cm.replaceRange( "$", cur, null ); >+ }, >+ }; >+ >+ // The objects below are part of a field/subfield manipulation API, accessed through the base >+ // editor object. >+ // >+ // Each one is tied to a particular line; this means that using a field or subfield object after >+ // any other changes to the record will cause entertaining explosions. The objects are meant to >+ // be temporary, and should only be reused with great care. The macro code does this only >+ // because it is careful to dispose of the object after any other updates. >+ // >+ // Note, however, tha you can continue to use a field object after changing subfields. It's just >+ // the subfield objects that become invalid. >+ >+ // This is an exception raised by the EditorSubfield and EditorField when an invalid change is >+ // attempted. >+ function FieldError(line, message) { >+ this.line = line; >+ this.message = message; >+ }; >+ >+ FieldError.prototype.toString = function() { >+ return 'FieldError(' + this.line + ', "' + this.message + '")'; > }; > >- function MARCEditor( position ) { >+ // This is the temporary object for a particular subfield in a field. Any change to any other >+ // subfields will invalidate this subfield object. >+ function EditorSubfield( field, index, start, end ) { >+ this.field = field; >+ this.index = index; >+ this.start = start; >+ this.end = end; >+ >+ if ( this.field.isControlField ) { >+ this.contentsStart = start; >+ this.code = '@'; >+ } else { >+ this.contentsStart = start + 3; >+ this.code = this.field.contents.substr( this.start + 1, 1 ); >+ } >+ >+ this.cm = field.cm; >+ >+ var marks = this.cm.findMarksAt( { line: field.line, ch: this.contentsStart } ); >+ if ( marks[0] && marks[0].widget ) { >+ this.widget = marks[0].widget; >+ >+ this.text = this.widget.text; >+ this.setText = this.widget.setText; >+ this.getFixed = this.widget.getFixed; >+ this.setFixed = this.widget.setFixed; >+ } else { >+ this.widget = null; >+ this.text = this.field.contents.substr( this.contentsStart, end - this.contentsStart ); >+ } >+ }; >+ >+ $.extend( EditorSubfield.prototype, { >+ _invalid: function() { >+ return this.field._subfieldsInvalid(); >+ }, >+ >+ focus: function() { >+ this.cm.setCursor( { line: this.field.line, ch: this.contentsStart } ); >+ }, >+ focusEnd: function() { >+ this.cm.setCursor( { line: this.field.line, ch: this.end } ); >+ }, >+ getText: function() { >+ return this.text; >+ }, >+ setText: function( text ) { >+ if ( !this._invalid() ) throw new FieldError( this.field.line, 'subfield invalid' ); >+ this.cm.replaceRange( text, { line: this.field.line, ch: this.contentsStart }, { line: this.field.line, ch: this.end }, 'marcAware' ); >+ this.field._invalidateSubfields(); >+ }, >+ } ); >+ >+ function EditorField( editor, line ) { >+ this.editor = editor; >+ this.line = line; >+ >+ this.cm = editor.cm; >+ >+ this._updateInfo(); >+ this.tag = this.contents.substr( 0, 3 ); >+ this.isControlField = ( this.tag < '010' ); >+ >+ if ( this.isControlField ) { >+ this._ind1 = this.contents.substr( 4, 1 ); >+ this._ind2 = this.contents.substr( 6, 1 ); >+ } else { >+ this._ind1 = null; >+ this._ind2 = null; >+ } >+ >+ this.subfields = null; >+ } >+ >+ $.extend( EditorField.prototype, { >+ _subfieldsInvalid: function() { >+ return !this.subfields; >+ }, >+ _invalidateSubfields: function() { >+ this._subfields = null; >+ }, >+ >+ _updateInfo: function() { >+ this.info = this.editor.getLineInfo( { line: this.line, ch: 0 } ); >+ if ( this.info == null ) throw new FieldError( 'Invalid field' ); >+ this.contents = this.info.contents; >+ }, >+ _scanSubfields: function() { >+ this._updateInfo(); >+ >+ if ( this.isControlField ) { >+ this._subfields = [ new EditorSubfield( this, 0, 4, this.contents.length ) ]; >+ } else { >+ var field = this; >+ var subfields = this.info.subfields; >+ this._subfields = []; >+ >+ for (var i = 0; i < this.info.subfields.length; i++) { >+ var end = i == subfields.length - 1 ? this.contents.length : subfields[i+1].ch; >+ >+ this._subfields.push( new EditorSubfield( this, i, subfields[i].ch, end ) ); >+ } >+ } >+ }, >+ >+ delete: function() { >+ this.cm.replaceRange( "", { line: this.line, ch: 0 }, { line: this.line + 1, ch: 0 }, 'marcAware' ); >+ }, >+ focus: function() { >+ this.cm.setCursor( { line: this.line, ch: 0 } ); >+ >+ return this; >+ }, >+ >+ getText: function() { >+ var result = ''; >+ >+ $.each( this.getSubfields(), function() { >+ if ( this.code != '@' ) result += '$' + this.code + ' '; >+ >+ result += this.getText(); >+ } ); >+ >+ return result; >+ }, >+ setText: function( text ) { >+ var indicator_match = /^([_ 0-9])([_ 0-9])\$/.exec( text ); >+ if ( indicator_match ) { >+ text = text.substr(2); >+ this.setIndicator1( indicator_match[1] ); >+ this.setIndicator2( indicator_match[2] ); >+ } >+ >+ this.cm.replaceRange( text, { line: this.line, ch: this.isControlField ? 4 : 8 }, { line: this.line }, 'marcAware' ); >+ this._invalidateSubfields(); >+ >+ return this; >+ }, >+ >+ getIndicator1: function() { >+ return this._ind1; >+ }, >+ getIndicator2: function() { >+ return this._ind2; >+ }, >+ setIndicator1: function(val) { >+ if ( this.isControlField ) throw new FieldError('Cannot set indicators on control field'); >+ >+ this._ind1 = ( !val || val == ' ' ) ? '_' : val; >+ this.cm.replaceRange( this._ind1, { line: this.line, ch: 4 }, { line: this.line, ch: 5 }, 'marcAware' ); >+ >+ return this; >+ }, >+ setIndicator2: function(val) { >+ if ( this.isControlField ) throw new FieldError('Cannot set indicators on control field'); >+ >+ this._ind2 = ( !val || val == ' ' ) ? '_' : val; >+ this.cm.replaceRange( this._ind2, { line: this.line, ch: 6 }, { line: this.line, ch: 7 }, 'marcAware' ); >+ >+ return this; >+ }, >+ >+ appendSubfield: function( code ) { >+ if ( this.isControlField ) throw new FieldError('Cannot add subfields to control field'); >+ >+ this._invalidateSubfields(); >+ this.cm.replaceRange( '$' + code + ' ', { line: this.line }, null, 'marcAware' ); >+ var subfields = this.getSubfields(); >+ >+ return subfields[ subfields.length - 1 ]; >+ }, >+ insertSubfield: function( code, position ) { >+ if ( this.isControlField ) throw new FieldError('Cannot add subfields to control field'); >+ >+ position = position || 0; >+ >+ var subfields = this.getSubfields(); >+ this._invalidateSubfields(); >+ this.cm.replaceRange( '$' + code + ' ', { line: this.line, ch: subfields[position] ? subfields[position].start : null }, null, 'marcAware' ); >+ subfields = this.getSubfields(); >+ >+ return subfields[ position ]; >+ }, >+ getSubfields: function( code ) { >+ if ( !this._subfields ) this._scanSubfields(); >+ if ( code == null ) return this._subfields; >+ >+ var result = []; >+ >+ $.each( this._subfields, function() { >+ if ( code == null || this.code == code ) result.push(this); >+ } ); >+ >+ return result; >+ }, >+ getFirstSubfield: function( code ) { >+ var result = this.getSubfields( code ); >+ >+ return ( result && result.length ) ? result[0] : null; >+ }, >+ getSubfieldAt: function( ch ) { >+ var subfields = this.getSubfields(); >+ >+ for (var i = 0; i < subfields.length; i++) { >+ if ( subfields[i].start < ch && subfields[i].end >= ch ) return subfields[i]; >+ } >+ }, >+ } ); >+ >+ function MARCEditor( options ) { > this.cm = CodeMirror( >- position, >+ options.position, > { > extraKeys: _editorKeys, > gutters: [ >@@ -210,149 +466,221 @@ define( [ 'marc-record', 'koha-backend', 'preferences', 'text-marc', 'widget' ], > this.cm.on( 'change', editorChange ); > this.cm.on( 'cursorActivity', editorCursorActivity ); > >+ this.onCursorActivity = options.onCursorActivity; >+ > this.subscribers = []; > this.subscribe( function( marceditor ) { > Widget.Notify( marceditor ); > } ); > } > >- MARCEditor.prototype.setUseWidgets = function( val ) { >- if ( val ) { >- for ( var line = 0; line <= this.cm.lastLine(); line++ ) { >- Widget.UpdateLine( this, line ); >+ MARCEditor.FieldError = FieldError; >+ >+ $.extend( MARCEditor.prototype, { >+ setUseWidgets: function( val ) { >+ if ( val ) { >+ for ( var line = 0; line <= this.cm.lastLine(); line++ ) { >+ Widget.UpdateLine( this, line ); >+ } >+ } else { >+ $.each( this.cm.getAllMarks(), function( undef, mark ) { >+ if ( mark.widget ) mark.widget.clearToText(); >+ } ); > } >- } else { >+ }, >+ >+ focus: function() { >+ this.cm.focus(); >+ }, >+ >+ getCursor: function() { >+ return this.cm.getCursor(); >+ }, >+ >+ refresh: function() { >+ this.cm.refresh(); >+ }, >+ >+ displayRecord: function( record ) { >+ this.cm.setValue( TextMARC.RecordToText(record) ); >+ }, >+ >+ getRecord: function() { >+ this.textMode = true; >+ > $.each( this.cm.getAllMarks(), function( undef, mark ) { > if ( mark.widget ) mark.widget.clearToText(); > } ); >- } >- }; >+ var record = TextMARC.TextToRecord( this.cm.getValue() ); >+ for ( var line = 0; line <= this.cm.lastLine(); line++ ) { >+ if ( Preferences.user.fieldWidgets ) Widget.UpdateLine( this, line ); >+ } > >- MARCEditor.prototype.focus = function() { >- this.cm.focus(); >- }; >+ this.textMode = false; > >- MARCEditor.prototype.refresh = function() { >- this.cm.refresh(); >- }; >+ return record; >+ }, > >- MARCEditor.prototype.displayRecord = function( record ) { >- this.cm.setValue( TextMARC.RecordToText(record) ); >- }; >+ getLineInfo: function( pos ) { >+ var contents = this.cm.getLine( pos.line ); >+ if ( contents == null ) return {}; > >- MARCEditor.prototype.getRecord = function() { >- this.textMode = true; >+ var tagNumber = contents.match( /^([A-Za-z0-9]{3})/ ); > >- $.each( this.cm.getAllMarks(), function( undef, mark ) { >- if ( mark.widget ) mark.widget.clearToText(); >- } ); >- var record = TextMARC.TextToRecord( this.cm.getValue() ); >- for ( var line = 0; line <= this.cm.lastLine(); line++ ) { >- if ( Preferences.user.fieldWidgets ) Widget.UpdateLine( this, line ); >- } >+ if ( !tagNumber ) return null; // No tag at all on this line >+ tagNumber = tagNumber[1]; > >- this.textMode = false; >+ if ( tagNumber < '010' ) return { tagNumber: tagNumber, contents: contents }; // No current subfield > >- return record; >- }; >+ var matcher = /[$|Çâ¡]([a-z0-9%]) /g; >+ var match; > >- MARCEditor.prototype.getLineInfo = function( pos ) { >- var contents = this.cm.getLine( pos.line ); >- if ( contents == null ) return {}; >+ var subfields = []; >+ var currentSubfield; > >- var tagNumber = contents.match( /^([A-Za-z0-9]{3}) / ); >+ while ( ( match = matcher.exec(contents) ) ) { >+ subfields.push( { code: match[1], ch: match.index } ); >+ if ( match.index < pos.ch ) currentSubfield = match[1]; >+ } > >- if ( !tagNumber ) return null; // No tag at all on this line >- tagNumber = tagNumber[1]; >+ return { tagNumber: tagNumber, subfields: subfields, currentSubfield: currentSubfield, contents: contents }; >+ }, > >- if ( tagNumber < '010' ) return { tagNumber: tagNumber, contents: contents }; // No current subfield >+ addError: function( line, error ) { >+ var found = false; >+ var options = {}; > >- var matcher = /[$|Çâ¡]([a-z0-9%]) /g; >- var match; >+ if ( line == null ) { >+ line = 0; >+ options.above = true; >+ } > >- var subfields = []; >- var currentSubfield; >+ $.each( this.cm.getLineHandle(line).widgets || [], function( undef, widget ) { >+ if ( !widget.isErrorMarker ) return; > >- while ( ( match = matcher.exec(contents) ) ) { >- subfields.push( { code: match[1], ch: match.index } ); >- if ( match.index < pos.ch ) currentSubfield = match[1]; >- } >+ found = true; > >- return { tagNumber: tagNumber, subfields: subfields, currentSubfield: currentSubfield, contents: contents }; >- }; >- >- MARCEditor.prototype.addError = function( line, error ) { >- var found = false; >- var options = {}; >+ $( widget.node ).append( '; ' + error ); >+ widget.changed(); > >- if ( line == null ) { >- line = 0; >- options.above = true; >- } >+ return false; >+ } ); > >- $.each( this.cm.getLineHandle(line).widgets || [], function( undef, widget ) { >- if ( !widget.isErrorMarker ) return; >+ if ( found ) return; > >- found = true; >+ var node = $( '<div class="structure-error"><i class="icon-remove"></i> ' + error + '</div>' )[0]; >+ var widget = this.cm.addLineWidget( line, node, options ); > >- $( widget.node ).append( '; ' + error ); >- widget.changed(); >+ widget.node = node; >+ widget.isErrorMarker = true; >+ }, > >- return false; >- } ); >+ removeErrors: function() { >+ for ( var line = 0; line < this.cm.lineCount(); line++ ) { >+ $.each( this.cm.getLineHandle( line ).widgets || [], function( undef, lineWidget ) { >+ if ( lineWidget.isErrorMarker ) lineWidget.clear(); >+ } ); >+ } >+ }, > >- if ( found ) return; >+ startNotify: function() { >+ if ( this.notifyTimeout ) clearTimeout( this.notifyTimeout ); >+ this.notifyTimeout = setTimeout( $.proxy( function() { >+ this.notifyAll(); > >- var node = $( '<div class="structure-error"><i class="icon-remove"></i> ' + error + '</div>' )[0]; >- var widget = this.cm.addLineWidget( line, node, options ); >+ this.notifyTimeout = null; >+ }, this ), NOTIFY_TIMEOUT ); >+ }, > >- widget.node = node; >- widget.isErrorMarker = true; >- }; >+ notifyAll: function() { >+ $.each( this.subscribers, $.proxy( function( undef, subscriber ) { >+ subscriber(this); >+ }, this ) ); >+ }, > >- MARCEditor.prototype.removeErrors = function() { >- for ( var line = 0; line < this.cm.lineCount(); line++ ) { >- $.each( this.cm.getLineHandle( line ).widgets || [], function( undef, lineWidget ) { >- if ( lineWidget.isErrorMarker ) lineWidget.clear(); >- } ); >- } >- }; >+ subscribe: function( subscriber ) { >+ this.subscribers.push( subscriber ); >+ }, > >- MARCEditor.prototype.getFixedField = function(field) { >- field += ' '; >- for ( var line = 0; line < this.cm.lineCount(); line++ ) { >- var contents = this.cm.getLine(line); >- if ( contents.substr( 0, 4 ) != field ) continue; >+ createField: function( tag, line ) { >+ var contents = tag + ( tag < '010' ? ' ' : ' _ _ ' ); > >- var marks = this.cm.findMarksAt( { line: line, ch: 4 } ); >- if ( marks[0] && marks[0].widget ) { >- return marks[0].widget.text; >+ if ( line > this.cm.lastLine() ) { >+ contents = '\n' + contents; > } else { >- return contents.substr(4); >+ contents = contents + '\n'; > } >- } > >- return null; >- }; >+ this.cm.replaceRange( contents, { line: line, ch: 0 }, null, 'marcAware' ); > >- MARCEditor.prototype.startNotify = function() { >- if ( this.notifyTimeout ) clearTimeout( this.notifyTimeout ); >- this.notifyTimeout = setTimeout( $.proxy( function() { >- this.notifyAll(); >+ return new EditorField( this, line ); >+ }, > >- this.notifyTimeout = null; >- }, this ), NOTIFY_TIMEOUT ); >- }; >+ createFieldOrdered: function( tag ) { >+ var line, contents; > >- MARCEditor.prototype.notifyAll = function() { >- $.each( this.subscribers, $.proxy( function( undef, subscriber ) { >- subscriber(this); >- }, this ) ); >- }; >+ for ( line = 0; (contents = this.cm.getLine(line)); line++ ) { >+ if ( contents && contents.substr(0, 3) > tag ) break; >+ } > >- MARCEditor.prototype.subscribe = function( subscriber ) { >- this.subscribers.push( subscriber ); >- }; >+ return this.createField( tag, line ); >+ }, >+ >+ createFieldGrouped: function( tag ) { >+ // Control fields should be inserted in actual order, whereas other fields should be >+ // inserted grouped >+ if ( tag < '010' ) return this.createFieldOrdered( tag ); >+ >+ var line, contents; >+ >+ for ( line = 0; (contents = this.cm.getLine(line)); line++ ) { >+ if ( contents && contents[0] > tag[0] ) break; >+ } >+ >+ return this.createField( tag, line ); >+ }, >+ >+ getFieldAt: function( line ) { >+ try { >+ return new EditorField( this, line ); >+ } catch (e) { >+ return null; >+ } >+ }, >+ >+ getCurrentField: function() { >+ return this.getFieldAt( this.cm.getCursor().line ); >+ }, >+ >+ getFields: function( tag ) { >+ var result = []; >+ >+ if ( tag != null ) tag += ' '; >+ >+ for ( var line = 0; line < this.cm.lineCount(); line++ ) { >+ if ( tag && this.cm.getLine(line).substr( 0, 4 ) != tag ) continue; >+ >+ // If this throws a FieldError, pretend it doesn't exist >+ try { >+ result.push( new EditorField( this, line ) ); >+ } catch (e) { >+ if ( !( e instanceof FieldError ) ) throw e; >+ } >+ } >+ >+ return result; >+ }, >+ >+ getFirstField: function( tag ) { >+ var result = this.getFields( tag ); >+ >+ return ( result && result.length ) ? result[0] : null; >+ }, >+ >+ getAllFields: function( tag ) { >+ return this.getFields( null ); >+ }, >+ } ); > > return MARCEditor; > } ); >diff --git a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/marc-mode.js b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/marc-mode.js >index bfa0174..f9bf862 100644 >--- a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/marc-mode.js >+++ b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/marc-mode.js >@@ -1,6 +1,20 @@ > /** >- * Textual MARC mode for CodeMirror. >- * Copyright (c) 2013 ByWater >+ * Copyright 2015 ByWater Solutions >+ * >+ * This file is part of Koha. >+ * >+ * Koha is free software; you can redistribute it and/or modify it >+ * under the terms of the GNU General Public License as published by >+ * the Free Software Foundation; either version 3 of the License, or >+ * (at your option) any later version. >+ * >+ * Koha is distributed in the hope that it will be useful, but >+ * WITHOUT ANY WARRANTY; without even the implied warranty of >+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the >+ * GNU General Public License for more details. >+ * >+ * You should have received a copy of the GNU General Public License >+ * along with Koha; if not, see <http://www.gnu.org/licenses>. > */ > > // Expected format: 245 _ 1 $a Pizza |c 34ars >@@ -34,6 +48,7 @@ CodeMirror.defineMode( 'marc', function( config, modeConfig ) { > }, > token: function( stream, state ) { > var match; >+ // First, try to match some kind of valid tag > if ( stream.sol() ) { > this.startState( state ); > if ( match = stream.match( /[0-9A-Za-z]+/ ) ) { >@@ -50,7 +65,7 @@ CodeMirror.defineMode( 'marc', function( config, modeConfig ) { > > state.tagNumber = match; > if ( state.tagNumber < '010' ) { >- // Control field >+ // Control field, no subfields or indicators > state.subAllowed = false; > } > >@@ -66,26 +81,29 @@ CodeMirror.defineMode( 'marc', function( config, modeConfig ) { > } > } > >+ // Don't need to do anything > if ( stream.eol() ) { > return; > } > >+ // Check for the correct space after the tag number for a control field > if ( !state.subAllowed && stream.pos == 3 ) { > if ( stream.next() == ' ' ) { >- return 'reqspace'; >+ return 'required-space'; > } else { > stream.skipToEnd(); > return 'error'; > } > } > >+ // For a normal field, check for correct indicators and spacing > if ( stream.pos < 8 && state.subAllowed ) { > switch ( stream.pos ) { > case 3: > case 5: > case 7: > if ( stream.next() == ' ' ) { >- return 'reqspace'; >+ return 'required-space'; > } else { > stream.skipToEnd(); > return 'error'; >@@ -101,8 +119,21 @@ CodeMirror.defineMode( 'marc', function( config, modeConfig ) { > } > } > >+ // Otherwise, we're after the start of the line. > if ( state.subAllowed ) { >- if ( stream.pos != 8 && stream.match( /[^$|Çâ¡]+/ ) ) return; >+ // If we don't have to match a subfield, try to consume text. >+ if ( stream.pos != 8 ) { >+ // Try to match space at the end of the line, then everything but spaces, and as >+ // a final fallback, only spaces. >+ // >+ // This is required to keep the contents matching from stepping on the end-space >+ // matching. >+ if ( stream.match( /[ \t]+$/ ) ) { >+ return 'end-space'; >+ } else if ( stream.match( /[^ \t$|Çâ¡]+/ ) || stream.match( /[ \t]+/ ) ) { >+ return; >+ } >+ } > > if ( stream.eat( /[$|Çâ¡]/ ) ) { > var subfieldCode; >@@ -122,7 +153,15 @@ CodeMirror.defineMode( 'marc', function( config, modeConfig ) { > return 'error'; > } > } else { >- stream.skipToEnd(); >+ // Match space at end of line >+ if ( stream.match( /[ \t]+$/ ) ) { >+ return 'end-space'; >+ } else { >+ stream.match( /[ \t]+/ ); >+ } >+ >+ stream.match( /[^ \t]+/ ); >+ return; > } > } > }; >diff --git a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/marc-record.js b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/marc-record.js >index 95adf85..f745533 100644 >--- a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/marc-record.js >+++ b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/marc-record.js >@@ -1,4 +1,23 @@ > /** >+ * Copyright 2015 ByWater Solutions >+ * >+ * This file is part of Koha. >+ * >+ * Koha is free software; you can redistribute it and/or modify it >+ * under the terms of the GNU General Public License as published by >+ * the Free Software Foundation; either version 3 of the License, or >+ * (at your option) any later version. >+ * >+ * Koha is distributed in the hope that it will be useful, but >+ * WITHOUT ANY WARRANTY; without even the implied warranty of >+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the >+ * GNU General Public License for more details. >+ * >+ * You should have received a copy of the GNU General Public License >+ * along with Koha; if not, see <http://www.gnu.org/licenses>. >+ */ >+ >+/** > * Adapted and cleaned up from biblios.net, which is purportedly under the GPL. > * Source: http://git.librarypolice.com/?p=biblios.git;a=blob_plain;f=plugins/marc21editor/marcrecord.js;hb=master > * >diff --git a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/preferences.js b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/preferences.js >index 6e324e6..5cf81da 100644 >--- a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/preferences.js >+++ b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/preferences.js >@@ -1,3 +1,22 @@ >+/** >+ * Copyright 2015 ByWater Solutions >+ * >+ * This file is part of Koha. >+ * >+ * Koha is free software; you can redistribute it and/or modify it >+ * under the terms of the GNU General Public License as published by >+ * the Free Software Foundation; either version 3 of the License, or >+ * (at your option) any later version. >+ * >+ * Koha is distributed in the hope that it will be useful, but >+ * WITHOUT ANY WARRANTY; without even the implied warranty of >+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the >+ * GNU General Public License for more details. >+ * >+ * You should have received a copy of the GNU General Public License >+ * along with Koha; if not, see <http://www.gnu.org/licenses>. >+ */ >+ > define( function() { > var Preferences = { > Load: function( borrowernumber ) { >@@ -14,6 +33,7 @@ define( function() { > font: 'monospace', > fontSize: '1em', > macros: {}, >+ selected_search_targets: {}, > }, saved_prefs ); > }, > >diff --git a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/resources.js b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/resources.js >index 24806ce..5be97d6 100644 >--- a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/resources.js >+++ b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/resources.js >@@ -1,3 +1,22 @@ >+/** >+ * Copyright 2015 ByWater Solutions >+ * >+ * This file is part of Koha. >+ * >+ * Koha is free software; you can redistribute it and/or modify it >+ * under the terms of the GNU General Public License as published by >+ * the Free Software Foundation; either version 3 of the License, or >+ * (at your option) any later version. >+ * >+ * Koha is distributed in the hope that it will be useful, but >+ * WITHOUT ANY WARRANTY; without even the implied warranty of >+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the >+ * GNU General Public License for more details. >+ * >+ * You should have received a copy of the GNU General Public License >+ * along with Koha; if not, see <http://www.gnu.org/licenses>. >+ */ >+ > define( [ 'module' ], function( module ) { > var _allResources = []; > >diff --git a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/search.js b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/search.js >index e2e248c..341da39 100644 >--- a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/search.js >+++ b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/search.js >@@ -1,3 +1,22 @@ >+/** >+ * Copyright 2015 ByWater Solutions >+ * >+ * This file is part of Koha. >+ * >+ * Koha is free software; you can redistribute it and/or modify it >+ * under the terms of the GNU General Public License as published by >+ * the Free Software Foundation; either version 3 of the License, or >+ * (at your option) any later version. >+ * >+ * Koha is distributed in the hope that it will be useful, but >+ * WITHOUT ANY WARRANTY; without even the implied warranty of >+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the >+ * GNU General Public License for more details. >+ * >+ * You should have received a copy of the GNU General Public License >+ * along with Koha; if not, see <http://www.gnu.org/licenses>. >+ */ >+ > define( [ 'marc-record' ], function( MARC ) { > var _options; > var _records = {}; >@@ -39,6 +58,11 @@ define( [ 'marc-record' ], function( MARC ) { > return q; > }, > Run: function( servers, q, options ) { >+ options = $.extend( { >+ offset: 0, >+ page_size: 20, >+ }, _options, options ); >+ > Search.includedServers = []; > _records = {}; > _last = { >@@ -47,25 +71,24 @@ define( [ 'marc-record' ], function( MARC ) { > options: options, > }; > >- options = $.extend( { >- offset: 0, >- page_size: 20, >- }, _options, options ); >- > $.each( servers, function ( id, info ) { > if ( info.checked ) Search.includedServers.push( id ); > } ); > > $.get( >- '/cgi-bin/koha/svc/z3950', >+ '/cgi-bin/koha/svc/cataloguing/metasearch', > { > q: q, > servers: Search.includedServers.join( ',' ), > offset: options.offset, >- page_size: options.page_size >+ page_size: options.page_size, >+ sort_direction: options.sort_direction, >+ sort_key: options.sort_key, >+ resultset: options.resultset, > } > ) > .done( function( data ) { >+ _last.options.resultset = data.resultset; > $.each( data.hits, function( undef, hit ) { > var record = new MARC.Record(); > record.loadMARCXML( hit.record ); >@@ -80,9 +103,10 @@ define( [ 'marc-record' ], function( MARC ) { > > return true; > }, >- Fetch: function( offset ) { >+ Fetch: function( options ) { > if ( !_last ) return; >- Search.Run( _last.servers, _last.q, $.extend( {}, _last.options, { offset: offset } ) ); >+ $.extend( _last.options, options ); >+ Search.Run( _last.servers, _last.q, _last.options ); > } > }; > >diff --git a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/text-marc.js b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/text-marc.js >index f522f2c..ba471e6 100644 >--- a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/text-marc.js >+++ b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/text-marc.js >@@ -1,3 +1,22 @@ >+/** >+ * Copyright 2015 ByWater Solutions >+ * >+ * This file is part of Koha. >+ * >+ * Koha is free software; you can redistribute it and/or modify it >+ * under the terms of the GNU General Public License as published by >+ * the Free Software Foundation; either version 3 of the License, or >+ * (at your option) any later version. >+ * >+ * Koha is distributed in the hope that it will be useful, but >+ * WITHOUT ANY WARRANTY; without even the implied warranty of >+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the >+ * GNU General Public License for more details. >+ * >+ * You should have received a copy of the GNU General Public License >+ * along with Koha; if not, see <http://www.gnu.org/licenses>. >+ */ >+ > define( [ 'marc-record' ], function( MARC ) { > // Convert any characters for display > function _sanitize( text ) { >diff --git a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/widget.js b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/widget.js >index 5e7bb48..188a027 100644 >--- a/koha-tmpl/intranet-tmpl/lib/koha/cateditor/widget.js >+++ b/koha-tmpl/intranet-tmpl/lib/koha/cateditor/widget.js >@@ -1,3 +1,22 @@ >+/** >+ * Copyright 2015 ByWater Solutions >+ * >+ * This file is part of Koha. >+ * >+ * Koha is free software; you can redistribute it and/or modify it >+ * under the terms of the GNU General Public License as published by >+ * the Free Software Foundation; either version 3 of the License, or >+ * (at your option) any later version. >+ * >+ * Koha is distributed in the hope that it will be useful, but >+ * WITHOUT ANY WARRANTY; without even the implied warranty of >+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the >+ * GNU General Public License for more details. >+ * >+ * You should have received a copy of the GNU General Public License >+ * along with Koha; if not, see <http://www.gnu.org/licenses>. >+ */ >+ > define( [ 'resources' ], function( Resources ) { > var _widgets = {}; > >@@ -233,6 +252,9 @@ define( [ 'resources' ], function( Resources ) { > var next = ( i < info.subfields.length - 1 ) ? info.subfields[i + 1].ch : end; > subfields.push( { code: info.subfields[i].code, from: info.subfields[i].ch + 3, to: next } ); > } >+ // If not a fixed field, and we didn't find any subfields, we need to throw in the >+ // '@' subfield so we can properly remove it >+ if ( subfields.length == 0 ) subfields.push( { code: '@', from: 4, to: end } ); > } > > $.each( subfields, function ( undef, subfield ) { >diff --git a/koha-tmpl/intranet-tmpl/prog/en/css/cateditor.css b/koha-tmpl/intranet-tmpl/prog/en/css/cateditor.css >index ad9f9cb..04258a5 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/css/cateditor.css >+++ b/koha-tmpl/intranet-tmpl/prog/en/css/cateditor.css >@@ -90,6 +90,10 @@ body { > font-weight: bold; > } > >+.cm-end-space { >+ background-color: #DDDDBB; >+} >+ > #editor .modified-line-gutter { > width: 10px; > } >@@ -202,7 +206,7 @@ body { > } > > .modal-body { >- height: 400px; >+ max-height: none; > padding: 0; > } > >@@ -237,7 +241,7 @@ body { > #quicksearch input, #quicksearch a { > font-size: 1.2em; > padding: 3px 0; >- width: 96%; // I have no idea why this is necessary >+ width: 96%; /* I have no idea why this is necessary */ > } > > #show-advanced-search { >@@ -246,12 +250,15 @@ body { > } > > #advanced-search-fields { >+ -moz-column-width: 26em; >+ -webkit-column-width: 26em; >+ column-width: 26em; > margin: 0; > padding: 0; > } > > #advanced-search-fields li { >- display: inline-block; >+ display: block; > list-style-type: none; > } > >@@ -280,6 +287,14 @@ body { > > /*> Search results */ > >+#search-serversinfo li { >+ list-style-type: none; >+} >+ >+#search-serversinfo .search-toggle-server { >+ margin-right: 5px; >+} >+ > #searchresults table { > width: 100%; > } >@@ -400,3 +415,20 @@ body { > #macro-list .active .macro-info { > display: block; > } >+ >+.btn-toolbar label, .btn-toolbar select { >+ font-size: 13px; >+ vertical-align: middle; >+} >+ >+.btn-toolbar label { >+ margin-left: 1em; >+} >+ >+.btn-toolbar select { >+ padding: 2px; >+} >+ >+#macro-editor .CodeMirror { >+ height: 100%; >+} >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/cateditor-ui.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/cateditor-ui.inc >index 673aa2f..fcc8136 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/includes/cateditor-ui.inc >+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/cateditor-ui.inc >@@ -10,6 +10,7 @@ require.config( { > themelang: '[% themelang %]', > }, > }, >+ waitSeconds: 30, > } ); > </script> > >@@ -22,19 +23,32 @@ require.config( { > <script> > require( [ 'koha-backend', 'search', 'macros', 'marc-editor', 'marc-record', 'preferences', 'resources', 'text-marc', 'widget' ], function( KohaBackend, Search, Macros, MARCEditor, MARC, Preferences, Resources, TextMARC, Widget ) { > var z3950Servers = { >- [% FOREACH server = z3950_servers %] >+ 'koha:biblioserver': { >+ name: _("Local catalog"), >+ recordtype: 'biblio', >+ checked: false, >+ }, >+ [%- FOREACH batch = editable_batches -%] >+ 'batch:[% batch.import_batch_id %]': { >+ name: _("Batch: ") + '[% batch.file_name %]', >+ recordtype: 'biblio', >+ checked: false, >+ }, >+ [%- END -%] >+ [%- FOREACH server = z3950_servers -%] > [% server.id %]: { >- 'name': '[% server.name %]', >- 'kohasyntax': '[% server.syntax == 'USMARC' ? 'MARC21' : server.syntax %]', >- 'checked': [% server.checked ? 'true' : 'false' %], >+ name: '[% server.name %]', >+ recordtype: '[% server.recordtype %]', >+ checked: [% server.checked ? 'true' : 'false' %], > }, >- [% END %] >+ [%- END -%] > }; > > // The columns that should show up in a search, in order, and keyed by the corresponding <metadata> tag in the XSL and Pazpar2 config > var z3950Labels = [ >+ [ "local_number", _("Local number") ], > [ "title", _("Title") ], >- [ "series", _("Series Title") ], >+ [ "series", _("Series title") ], > [ "author", _("Author") ], > [ "lccn", _("LCCN") ], > [ "isbn", _("ISBN") ], >@@ -121,6 +135,14 @@ require( [ 'koha-backend', 'search', 'macros', 'marc-editor', 'marc-record', 'pr > return false; > } ); > >+ shortcut.add( 'ctrl+h', function() { >+ var field = editor.getCurrentField(); >+ >+ if ( !field ) return; >+ >+ window.open( getFieldHelpURL( field.tag ) ); >+ } ); >+ > $('#quicksearch .search-box').each( function() { > shortcut.add( 'enter', $.proxy( function() { > var terms = []; >@@ -143,6 +165,34 @@ require( [ 'koha-backend', 'search', 'macros', 'marc-editor', 'marc-record', 'pr > } ); > } > >+ function getFieldHelpURL( tag ) { >+ [% IF ( marcflavour == 'MARC21' ) %] >+ if ( tag == '000' ) { >+ return "http://www.loc.gov/marc/bibliographic/bdleader.html"; >+ } else if ( tag < '900' ) { >+ return "http://www.loc.gov/marc/bibliographic/bd" + tag + ".html"; >+ } else { >+ return "http://www.loc.gov/marc/bibliographic/bd9xx.html"; >+ } >+ [% ELSIF ( marcflavour == 'UNIMARC' ) %] >+ /* http://archive.ifla.org/VI/3/p1996-1/ is an outdated version of UNIMARC, but >+ seems to be the only version available that can be linked to per tag. More recent >+ versions of the UNIMARC standard are available on the IFLA website only as >+ PDFs! >+ */ >+ if ( tag == '000' ) { >+ return "http://archive.ifla.org/VI/3/p1996-1/uni.htm"; >+ } else { >+ var first = tag[0]; >+ var url = "http://archive.ifla.org/VI/3/p1996-1/uni" + first + ".htm#"; >+ if ( first == '0' ) url += "b"; >+ if ( first != '9' ) url += field; >+ >+ return url; >+ } >+ [% END %] >+ } >+ > // Record loading > var backends = { > 'new': { >@@ -207,6 +257,10 @@ require( [ 'koha-backend', 'search', 'macros', 'marc-editor', 'marc-record', 'pr > titleForRecord: _("Editing search result"), > get: function( id, callback ) { > if ( !id ) return false; >+ if ( !backends.search.records[ id ] ) { >+ callback( { error: _( "Invalid record" ) } ); >+ return false; >+ } > > callback( backends.search.records[ id ] ); > }, >@@ -214,6 +268,14 @@ require( [ 'koha-backend', 'search', 'macros', 'marc-editor', 'marc-record', 'pr > }, > }; > >+ var editable_batches = { >+ [%- FOREACH batch = editable_batches -%] >+ [% batch.import_batch_id %]: { >+ 'name': '[% batch.file_name %]', >+ }, >+ [%- END -%] >+ }; >+ > function setSource(parts) { > state.backend = parts[0]; > state.recordID = parts[1]; >@@ -222,7 +284,7 @@ require( [ 'koha-backend', 'search', 'macros', 'marc-editor', 'marc-record', 'pr > > var backend = backends[state.backend]; > >- document.location.hash = '#' + parts[0] + ':' + parts[1]; >+ document.location.hash = '#' + parts[0] + '/' + parts[1]; > > $('#title').text( backend.titleForRecord.replace( '{ID}', parts[1] ) ); > >@@ -234,7 +296,7 @@ require( [ 'koha-backend', 'search', 'macros', 'marc-editor', 'marc-record', 'pr > } > > function saveRecord( recid, editor, callback ) { >- var parts = recid.split(':'); >+ var parts = recid.split('/'); > if ( parts.length != 2 ) return false; > > if ( !backends[ parts[0] ] || !backends[ parts[0] ].save ) return false; >@@ -266,7 +328,7 @@ require( [ 'koha-backend', 'search', 'macros', 'marc-editor', 'marc-record', 'pr > > if (data.newId) { > setSource(data.newId); >- } else { >+ } else { > setSource( [ state.backend, state.recordID ] ); > } > >@@ -275,14 +337,16 @@ require( [ 'koha-backend', 'search', 'macros', 'marc-editor', 'marc-record', 'pr > } > > function loadRecord( recid, editor, callback ) { >- var parts = recid.split(':'); >+ var parts = recid.split('/'); > if ( parts.length != 2 ) return false; > > if ( !backends[ parts[0] ] || !backends[ parts[0] ].get ) return false; > > backends[ parts[0] ].get( parts[1], function( record ) { >- editor.displayRecord( record ); >- editor.focus(); >+ if ( !record.error ) { >+ editor.displayRecord( record ); >+ editor.focus(); >+ } > > if (callback) callback(record); > } ); >@@ -292,7 +356,7 @@ require( [ 'koha-backend', 'search', 'macros', 'marc-editor', 'marc-record', 'pr > > function openRecord( recid, editor, callback ) { > return loadRecord( recid, editor, function ( record ) { >- setSource( recid.split(':') ); >+ setSource( recid.split('/') ); > > if (callback) callback( record ); > } ); >@@ -300,23 +364,26 @@ require( [ 'koha-backend', 'search', 'macros', 'marc-editor', 'marc-record', 'pr > > // Search functions > function showAdvancedSearch() { >+ $('#advanced-search-servers').empty(); >+ $.each( z3950Servers, function( server_id, server ) { >+ $('#advanced-search-servers').append( '<li data-server-id="' + server_id + '"><input class="search-toggle-server" type="checkbox"' + ( server.checked ? ' checked="checked">' : '>' ) + server.name + '</li>' ); >+ } ); > $('#advanced-search-ui').modal('show'); > } > > function startAdvancedSearch() { >- var search = ''; >+ var terms = []; > >- $('#advanced-search-ui input').each( function() { >- if (!this.value) return; >- if (search) search += ' and '; >- search += $(this).data('qualifier') + this.value; >- } ); >+ $('#advanced-search-ui .search-box').each( function() { >+ if ( !this.value ) return; > >- if (!search) return; >+ terms.push( [ $(this).data('qualifier'), this.value ] ); >+ } ); > >- $('#advanced-search-ui').modal('hide'); >+ if ( !terms.length ) return; > >- if ( Search.Run( z3950Servers, search, 20 ) ) { >+ if ( Search.Run( z3950Servers, Search.JoinTerms(terms) ) ) { >+ $('#advanced-search-ui').modal('hide'); > $("#search-overlay").show(); > showResultsBox(); > } >@@ -330,6 +397,19 @@ require( [ 'koha-backend', 'search', 'macros', 'marc-editor', 'marc-record', 'pr > $('#search-results-ui').modal('show'); > } > >+ function showSearchSorting( sort_key, sort_direction ) { >+ var $th = $('#searchresults thead tr th[data-sort-label="' + sort_key + '"]'); >+ $th.parent().find( 'th[data-sort-label]' ).attr( 'class', 'sorting' ); >+ >+ if ( sort_direction == 'asc' ) { >+ direction = 'asc'; >+ $th.attr( 'class', 'sorting_asc' ); >+ } else { >+ direction = 'desc'; >+ $th.attr( 'class', 'sorting_desc' ); >+ } >+ } >+ > function showSearchResults( editor, data ) { > backends.search.records = {}; > >@@ -337,12 +417,18 @@ require( [ 'koha-backend', 'search', 'macros', 'marc-editor', 'marc-record', 'pr > $('#searchresults tbody').empty(); > $('#search-serversinfo').empty(); > >- $.each( data.num_fetched, function( server_id, num_fetched ) { >- if ( num_fetched < data.num_hits[server_id] ) { >+ $.each( z3950Servers, function( server_id, server ) { >+ var num_fetched = data.num_fetched[server_id]; >+ >+ if ( data.errors[server_id] ) { >+ num_fetched = data.errors[server_id]; >+ } else if ( num_fetched == null ) { >+ num_fetched = '-'; >+ } else if ( num_fetched < data.num_hits[server_id] ) { > num_fetched += '+'; > } > >- $('#search-serversinfo').append( '<li>' + z3950Servers[server_id].name + ' (' + num_fetched + ')' + '</li>' ); >+ $('#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>' ); > } ); > > var seenColumns = {}; >@@ -357,15 +443,17 @@ require( [ 'koha-backend', 'search', 'macros', 'marc-editor', 'marc-record', 'pr > > $.each( z3950Labels, function( undef, label ) { > if ( seenColumns[ label[0] ] ) { >- $('#searchresults thead tr').append( '<th>' + label[1] + '</th>' ); >+ $('#searchresults thead tr').append( '<th class="sorting" data-sort-label="' + label[0] + '">' + label[1] + '</th>' ); > } > } ); > >+ showSearchSorting( data.sort_key, data.sort_direction ); >+ > $('#searchresults thead tr').append('<th>' + _("Tools") + '</th>'); > > $.each( data.hits, function( undef, hit ) { >- backends.search.records[ hit.server + '-' + hit.index ] = hit.record; >- hit.id = 'search:' + hit.server + '-' + hit.index; >+ backends.search.records[ hit.server + ':' + hit.index ] = hit.record; >+ hit.id = 'search/' + hit.server + ':' + hit.index; > > var result = '<tr>'; > result += '<td class="sourcecol">' + z3950Servers[ hit.server ].name + '</td>'; >@@ -496,6 +584,13 @@ require( [ 'koha-backend', 'search', 'macros', 'marc-editor', 'marc-record', 'pr > case 'macros': > showSavedMacros(); > break; >+ case 'selected_search_targets': >+ $.each( z3950Servers, function( server_id, server ) { >+ var saved_val = Preferences.user.selected_search_targets[server_id]; >+ >+ if ( saved_val != null ) server.checked = saved_val; >+ } ); >+ break; > } > } > >@@ -525,6 +620,13 @@ require( [ 'koha-backend', 'search', 'macros', 'marc-editor', 'marc-record', 'pr > Preferences.user.fontSize = $( e.target ).css( 'font-size' ); > } ); > break; >+ case 'selected_search_targets': >+ $( document ).on( 'change', 'input.search-toggle-server', function() { >+ var server_id = $( this ).parent().data('server-id'); >+ Preferences.user.selected_search_targets[server_id] = this.checked; >+ Preferences.Save( [% USER_INFO.0.borrowernumber %] ); >+ } ); >+ break; > } > } > >@@ -548,6 +650,7 @@ require( [ 'koha-backend', 'search', 'macros', 'marc-editor', 'marc-record', 'pr > $( '#macro-list li[data-name="' + name + '"]' ).addClass( 'active' ); > var macro = Preferences.user.macros[name]; > macroEditor.setValue( macro.contents ); >+ $( '#macro-format' ).val( macro.format || 'its' ); > if ( macro.history ) macroEditor.setHistory( macro.history ); > } > >@@ -604,14 +707,46 @@ require( [ 'koha-backend', 'search', 'macros', 'marc-editor', 'marc-record', 'pr > if ( !name || macroEditor.savedGeneration == macroEditor.changeGeneration() ) return; > > macroEditor.savedGeneration = macroEditor.changeGeneration(); >- storeMacro( name, { contents: macroEditor.getValue(), modified: (new Date()).valueOf(), history: macroEditor.getHistory() } ); >+ storeMacro( name, { contents: macroEditor.getValue(), modified: (new Date()).valueOf(), history: macroEditor.getHistory(), format: $('#macro-format').val() } ); > $('#macro-save-message').text(_("Saved")); > showSavedMacros(); > } > > $(document).ready( function() { > // Editor setup >- editor = new MARCEditor( function (elt) { $(elt).insertAfter('#toolbar') } ); >+ editor = new MARCEditor( { >+ onCursorActivity: function() { >+ $('#status-tag-info').empty(); >+ $('#status-subfield-info').empty(); >+ >+ var field = editor.getCurrentField(); >+ var cur = editor.getCursor(); >+ >+ if ( !field ) return; >+ >+ var taginfo = KohaBackend.GetTagInfo( '', field.tag ); >+ $('#status-tag-info').html( '<strong>' + field.tag + ':</strong> ' ); >+ >+ if ( taginfo ) { >+ $('#status-tag-info').append( '<a href="' + getFieldHelpURL( field.tag ) + '" target="_blank" class="show-field-help" title="' + _("Show help for this tag") + '">[?]</a> ' + taginfo.lib ); >+ >+ var subfield = field.getSubfieldAt( cur.ch ); >+ if ( !subfield ) return; >+ >+ var subfieldinfo = taginfo.subfields[ subfield.code ]; >+ $('#status-subfield-info').html( '<strong>$' + subfield.code + ':</strong> ' ); >+ >+ if ( subfieldinfo ) { >+ $('#status-subfield-info').append( subfieldinfo.lib ); >+ } else { >+ $('#status-subfield-info').append( '<em>' + _("Unknown subfield") + '</em>' ); >+ } >+ } else { >+ $('#status-tag-info').append( '<em>' + _("Unknown tag") + '</em>' ); >+ } >+ }, >+ position: function (elt) { $(elt).insertAfter('#toolbar') }, >+ } ); > > macroEditor = CodeMirror( > $('#macro-editor')[0], >@@ -621,14 +756,26 @@ require( [ 'koha-backend', 'search', 'macros', 'marc-editor', 'marc-record', 'pr > } > ); > >+ // Automatically detect resizes and change the height of the editor and position of modals. > var resizeTimer = null; > $( window ).resize( function() { > if ( resizeTimer == null ) resizeTimer = setTimeout( function() { > resizeTimer = null; > > var pos = $('#editor .CodeMirror').position(); >- $('#editor .CodeMirror').height( $(window).height() - pos.top - 24 ); >+ $('#editor .CodeMirror').height( $(window).height() - pos.top - 24 - $('#changelanguage').height() ); // 24 is hardcoded value but works well >+ >+ $('.modal-body').each( function() { >+ $(this).height( $(window).height() * .8 - $(this).prevAll('.modal-header').height() ); >+ } ); > }, 100); >+ >+ $("#advanced-search-ui, #search-results-ui, #macro-ui").css( { >+ marginLeft: function() { >+ return -($(this).width() / 2); >+ } >+ } ); >+ > } ).resize(); > > var saveableBackends = []; >@@ -640,6 +787,16 @@ require( [ 'koha-backend', 'search', 'macros', 'marc-editor', 'marc-record', 'pr > $( '#save-dropdown' ).append( '<li><a href="#" data-backend="' + backend[1] + '">' + backend[0] + '</a></li>' ); > } ); > >+ var macro_format_list = $.map( Macros.formats, function( format, name ) { >+ return $.extend( { name: name }, format ); >+ } ); >+ macro_format_list.sort( function( a, b ) { >+ return a.description.localeCompare(b.description); >+ } ); >+ $.each( macro_format_list, function() { >+ $('#macro-format').append( '<option value="' + this.name + '">' + this.description + '</option>' ); >+ } ); >+ > // Click bindings > $( '#save-record, #save-dropdown a' ).click( function() { > $( '#save-record' ).find('i').attr( 'class', 'icon-loading' ).siblings( 'span' ).text( _("Saving...") ); >@@ -690,9 +847,9 @@ require( [ 'koha-backend', 'search', 'macros', 'marc-editor', 'marc-record', 'pr > > var backend = $( this ).data( 'backend' ) || ( state.saveBackend ); > if ( state.backend == backend ) { >- saveRecord( backend + ':' + state.recordID, editor, finishCb ); >+ saveRecord( backend + '/' + state.recordID, editor, finishCb ); > } else { >- saveRecord( backend + ':', editor, finishCb ); >+ saveRecord( backend + '/', editor, finishCb ); > } > > return false; >@@ -709,17 +866,55 @@ require( [ 'koha-backend', 'search', 'macros', 'marc-editor', 'marc-record', 'pr > > reader.onload = function() { > var record = new MARC.Record(); >+ $("#search-overlay").show(); >+ Search.includedServers = [1]; >+ showResultsBox(); > > if ( /\.mrc$/.test( file.name ) ) { >- record.loadISO2709( reader.result ); >+ /*var data = { >+ errors: {}, >+ num_fetched: {}, >+ hits: [], >+ sort_direction: 'asc', >+ sort_key: 'title', >+ }; >+ data.hits = []; >+ $.each( reader.result.split('\035'), function( i, blob ) { >+ if ( !blob ) return; >+ >+ var record = new MARC.Record(); >+ record.loadISO2709( blob ); >+ >+ var hit = { >+ server: 1, >+ record: record, >+ index: i, >+ metadata: {}, >+ }; >+ >+ var _addField = function(field, tag, subf) { >+ var f = record.field(tag); >+ if (!f) return; >+ var s = f.subfield(subf); >+ if (s!==false) hit.metadata[field] = s; >+ } >+ >+ _addField('local_number', '001', '@'); >+ _addField('isbn', '020', 'a'); >+ _addField('author', '100', 'a'); >+ _addField('title', '245', 'a'); >+ >+ data.hits.push(hit); >+ } ); >+ showSearchResults(editor, data); >+ $("#search-overlay").hide();*/ >+ record.loadISO2709( blob ); > } else if ( /\.xml$/.test( file.name ) ) { > record.loadMARCXML( reader.result ); > } else { > humanMsg.displayAlert( _("Unknown record type, cannot import"), { className: 'humanError' } ); > return; > } >- >- editor.displayRecord( record ); > }; > > reader.readAsText( file ); >@@ -736,7 +931,7 @@ require( [ 'koha-backend', 'search', 'macros', 'marc-editor', 'marc-record', 'pr > } ); > > $('#run-macro').click( function() { >- var result = Macros.Run( editor, macroEditor.getValue() ); >+ var result = Macros.Run( editor, $('#macro-format').val(), macroEditor.getValue() ); > > if ( !result.errors.length ) return false; > >@@ -808,20 +1003,42 @@ require( [ 'koha-backend', 'search', 'macros', 'marc-editor', 'marc-record', 'pr > > $( document ).on( 'click', 'a.search-nav', function() { > $("#search-overlay").show(); >- Search.Fetch( $( this ).data( 'offset' ) ); >+ Search.Fetch( { offset: $( this ).data( 'offset' ) } ); >+ return false; >+ }); >+ >+ $( document ).on( 'click', 'th[data-sort-label]', function() { >+ $("#search-overlay").show(); >+ var direction; >+ >+ if ( $( this ).hasClass( 'sorting_asc' ) ) { >+ direction = 'desc'; >+ } else { >+ direction = 'asc'; >+ } >+ >+ showSearchSorting( $( this ).data( 'sort-label' ), direction ); >+ >+ Search.Fetch( { sort_key: $( this ).data( 'sort-label' ), sort_direction: direction } ); > return false; > }); > >+ $( document ).on( 'change', 'input.search-toggle-server', function() { >+ var server = z3950Servers[ $( this ).parent().data('server-id') ]; >+ server.checked = this.checked; >+ >+ if ( $('#search-results-ui').is( ':visible' ) ) { >+ $("#search-overlay").show(); >+ Search.Fetch(); >+ } >+ } ); >+ > // Key bindings > bindGlobalKeys(); > > // Setup UI > $("#advanced-search-ui, #search-results-ui, #macro-ui").each( function() { >- $(this).modal({ show: false }).css({ >- marginLeft: function() { >- return -($(this).width() / 2); >- } >- }); >+ $(this).modal({ show: false }); > } ); > > var $quicksearch = $('#quicksearch fieldset'); >@@ -847,6 +1064,10 @@ require( [ 'koha-backend', 'search', 'macros', 'marc-editor', 'marc-record', 'pr > return '<div id="alerts-container"><ul>' + prevAlerts.join('') + '</ul></div>'; > }, > }); >+ $('#new-record' ).click( function() { >+ openRecord( 'new/', editor ); >+ return false; >+ } ); > > // Start editor > Preferences.Load( [% USER_INFO.0.borrowernumber || 0 %] ); >@@ -858,7 +1079,9 @@ require( [ 'koha-backend', 'search', 'macros', 'marc-editor', 'marc-record', 'pr > onerror: handleSearchError, > } ); > >- function finishCb() { >+ function finishCb( data ) { >+ if ( data.error ) openRecord( 'new/', editor, finishCb ); >+ > Resources.GetAll().done( function() { > $("#loading").hide(); > editor.focus(); >@@ -870,7 +1093,7 @@ require( [ 'koha-backend', 'search', 'macros', 'marc-editor', 'marc-record', 'pr > } > > if ( !document.location.hash || !openRecord( document.location.hash.slice(1), editor, finishCb ) ) { >- openRecord( 'new:', editor, finishCb ); >+ openRecord( 'new/', editor, finishCb ); > } > } ); > } )(); >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/editor.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/editor.tt >index 70d0c38..0991dde 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/editor.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/editor.tt >@@ -1,9 +1,10 @@ > [% INCLUDE 'doc-head-open.inc' %] > <title>Koha › Cataloging › Editor</title> > [% INCLUDE 'doc-head-close.inc' %] >-<link type="text/css" rel="stylesheet" href="[% themelang %]/css/cateditor.css" /> >-<link type="text/css" rel="stylesheet" href="/intranet-tmpl/lib/codemirror/codemirror.css" /> >-<link rel="stylesheet" type="text/css" href="[% themelang %]/css/humanmsg.css" /> >+<link rel="stylesheet" href="[% themelang %]/css/cateditor.css" /> >+<link rel="stylesheet" href="[% themelang %]/css/datatables.css" /> >+<link rel="stylesheet" href="/intranet-tmpl/lib/codemirror/codemirror.css" /> >+<link rel="stylesheet" href="[% themelang %]/css/humanmsg.css" /> > <script src="[% interface %]/lib/jquery/plugins/humanmsg.js" type="text/javascript"></script> > [% IF ( bidi ) %] > <link rel="stylesheet" type="text/css" href="[% themelang %]/css/right-to-left.css" /> >@@ -29,6 +30,7 @@ > <div id="editor"> > <input id="import-records-input" type="file" style="display: none"> > <div id="toolbar" class="btn-toolbar"> >+ <button class="btn btn-small" id="new-record" title="Open fresh record"><i class="icon-plus"></i> <span>New record</span></button> > <div class="btn-group"> > <button class="btn btn-small" id="save-record" title="Save current record (Ctrl-S)"><i class="icon-hdd"></i> <span>Save</span></button> > <button class="btn btn-small dropdown-toggle" data-toggle="dropdown"> >@@ -99,60 +101,69 @@ > </div> > > <form id="advanced-search" class="modal-body"> >- <div id="toolbar" class="btn-toolbar"> >- <button class="btn btn-small" type="submit"><i class="icon-search"></i> <span>Search</span></button> >- <button class="btn btn-small" type="reset"><i class="icon-remove"></i> <span>Clear</span></button> >+ <div class="span3"> >+ <div id="search-facets"> >+ <ul> >+ <li>Servers:<ul id="advanced-search-servers"></ul></li> >+ </ul> >+ </div> >+ </div> >+ <div class="span9"> >+ <div id="toolbar" class="btn-toolbar"> >+ <button class="btn btn-small" type="submit"><i class="icon-search"></i> <span>Search</span></button> >+ <button class="btn btn-small" type="reset"><i class="icon-remove"></i> <span>Clear</span></button> >+ </div> >+ <ul id="advanced-search-fields"> >+ <li> >+ <label for="advanced-search-by-author">Author:</label> >+ <input class="search-box" data-qualifier="author" id="advanced-search-by-author" /> >+ </li> >+ <li> >+ <label for="advanced-search-by-control-number">Control number:</label> >+ <input class="search-box" data-qualifier="local_number" id="advanced-search-by-control-number" /> >+ </li> >+ <li> >+ <label for="advanced-search-by-dewey">Dewey number:</label> >+ <input class="search-box" data-qualifier="cn_dewey" id="advanced-search-by-dewey" /> >+ </li> >+ <li> >+ <label for="advanced-search-by-isbn">ISBN:</label> >+ <input class="search-box" data-qualifier="isbn" id="advanced-search-by-isbn" /> >+ </li> >+ <li> >+ <label for="advanced-search-by-issn">ISSN:</label> >+ <input class="search-box" data-qualifier="issn" id="advanced-search-by-issn" /> >+ </li> >+ <li> >+ <label for="advanced-search-by-lccn">LCCN:</label> >+ <input class="search-box" data-qualifier="lccn" id="advanced-search-by-lccn" /> >+ </li> >+ <li> >+ <label for="advanced-search-by-lc-number">LC call number:</label> >+ <input class="search-box" data-qualifier="cn_lc" id="advanced-search-by-lc-number" /> >+ </li> >+ <li> >+ <label for="advanced-search-by-publisher-number">Publisher number:</label> >+ <input class="search-box" data-qualifier="music_identifier" id="advanced-search-by-publisher-number" /> >+ </li> >+ <li> >+ <label for="advanced-search-by-standard-number">Standard number:</label> >+ <input class="search-box" data-qualifier="standard_identifier" id="advanced-search-by-standard-number" /> >+ </li> >+ <li> >+ <label for="advanced-search-by-subject">Subject:</label> >+ <input class="search-box" data-qualifier="subject" id="advanced-search-by-subject" /> >+ </li> >+ <li> >+ <label for="advanced-search-by-publication-date">Publication date:</label> >+ <input class="search-box" data-qualifier="date" id="advanced-search-by-publication-date" /> >+ </li> >+ <li> >+ <label for="advanced-search-by-title">Title:</label> >+ <input class="search-box" data-qualifier="title" id="advanced-search-by-title" /> >+ </li> >+ </ul> > </div> >- <ul id="advanced-search-fields"> >- <li> >- <label for="advanced-search-by-author">Author:</label> >- <input class="search-box" data-qualifier="author" id="advanced-search-by-author" /> >- </li> >- <li> >- <label for="advanced-search-by-control-number">Control number:</label> >- <input class="search-box" data-qualifier="local_number" id="advanced-search-by-control-number" /> >- </li> >- <li> >- <label for="advanced-search-by-dewey">Dewey number:</label> >- <input class="search-box" data-qualifier="cn_dewey" id="advanced-search-by-dewey" /> >- </li> >- <li> >- <label for="advanced-search-by-isbn">ISBN:</label> >- <input class="search-box" data-qualifier="isbn" id="advanced-search-by-isbn" /> >- </li> >- <li> >- <label for="advanced-search-by-issn">ISSN:</label> >- <input class="search-box" data-qualifier="issn" id="advanced-search-by-issn" /> >- </li> >- <li> >- <label for="advanced-search-by-lccn">LCCN:</label> >- <input class="search-box" data-qualifier="lccn" id="advanced-search-by-lccn" /> >- </li> >- <li> >- <label for="advanced-search-by-lc-number">LC call number:</label> >- <input class="search-box" data-qualifier="cn_lc" id="advanced-search-by-lc-number" /> >- </li> >- <li> >- <label for="advanced-search-by-publisher-number">Publisher number:</label> >- <input class="search-box" data-qualifier="music_identifier" id="advanced-search-by-publisher-number" /> >- </li> >- <li> >- <label for="advanced-search-by-standard-number">Standard number:</label> >- <input class="search-box" data-qualifier="standard_identifier" id="advanced-search-by-standard-number" /> >- </li> >- <li> >- <label for="advanced-search-by-subject">Subject:</label> >- <input class="search-box" data-qualifier="subject" id="advanced-search-by-subject" /> >- </li> >- <li> >- <label for="advanced-search-by-publication-date">Publication date:</label> >- <input class="search-box" data-qualifier="date" id="advanced-search-by-publication-date" /> >- </li> >- <li> >- <label for="advanced-search-by-title">Title:</label> >- <input class="search-box" data-qualifier="title" id="advanced-search-by-title" /> >- </li> >- </ul> > </form> > > </div> >@@ -190,10 +201,9 @@ > <div class="pagination pagination-small"> > </div> > </div> >- >- <div id="search-overlay"><span>Loading...</span><div class="progress progress-striped active"><div class="bar" style="width: 100%"></div></div></div> > </div> > </div> >+ <div id="search-overlay"><span>Loading...</span><div class="progress progress-striped active"><div class="bar" style="width: 100%"></div></div></div> > </div> > > </div> >@@ -209,8 +219,9 @@ > <div class="span3"><ul id="macro-list"></ul></div> > <div class="span9" id="macro-editor"> > <div id="macro-toolbar" class="btn-toolbar"> >- <button class="btn btn-small" id="run-macro"itle="Run and edit macros"><i class="icon-play"></i> Run macro</button> >+ <button class="btn btn-small" id="run-macro" title="Run and edit macros"><i class="icon-play"></i> Run macro</button> > <button class="btn btn-small" id="delete-macro" title="Delete macro"><i class="icon-remove"></i> Delete macro</button> >+ <label for="macro-format">Format: </label> <select id="macro-format"></select> > <div id="macro-save-message"></div> > </div> > </div> >diff --git a/svc/bib b/svc/bib >index a360429..3a1fd11 100755 >--- a/svc/bib >+++ b/svc/bib >@@ -86,7 +86,7 @@ sub update_bib { > > my $result = {}; > my $inxml = $query->param('POSTDATA'); >- print $query->header(-type => 'text/xml'); >+ print $query->header(-type => 'text/xml', -charset => 'utf-8'); > > my $record = eval {MARC::Record::new_from_xml( $inxml, "utf8", C4::Context->preference('marcflavour'))}; > my $do_not_escape = 0; >diff --git a/svc/cataloguing/framework b/svc/cataloguing/framework >index 39b1719..49d341d 100755 >--- a/svc/cataloguing/framework >+++ b/svc/cataloguing/framework >@@ -8,6 +8,7 @@ use C4::ClassSource; > use C4::Context; > use C4::Biblio; > use C4::Service; >+use Koha::Database; > > my ( $query, $response ) = C4::Service->init( editcatalogue => 'edit_catalogue' ); > >@@ -25,7 +26,7 @@ foreach my $tag ( sort keys %$tagslib ) { > push @tags, [ $tag, $taginfo ]; > } > >-my $dbh = C4::Context->dbh; >+my $schema = Koha::Database->new->schema; > my $authorised_values = {}; > > $authorised_values->{branches} = []; >@@ -38,9 +39,11 @@ foreach my $thisbranch ( sort keys %$branches ) { > push @{ $authorised_values->{branches} }, { value => $thisbranch, lib => $branches->{$thisbranch}->{'branchname'} }; > } > >-$authorised_values->{itemtypes} = $dbh->selectall_arrayref( q{ >- SELECT itemtype AS value, description AS lib FROM itemtypes ORDER BY description >-}, { Slice => {} } ); >+$authorised_values->{itemtypes} = [ $schema->resultset( "Itemtype" )->search( undef, { >+ columns => [ { value => 'itemtype' }, { lib => "description" } ], >+ order_by => "description", >+ result_class => 'DBIx::Class::ResultClass::HashRefInflator' >+} ) ]; > > my $class_sources = GetClassSources(); > >@@ -53,19 +56,20 @@ foreach my $class_source (sort keys %$class_sources) { > } > > my $branch_limit = C4::Context->userenv ? C4::Context->userenv->{"branch"} : ""; >-my $auth_query = "SELECT category, authorised_value, lib >- FROM authorised_values"; >-$auth_query .= qq{ LEFT JOIN authorised_values_branches ON ( id = av_id )} if $branch_limit; >-$auth_query .= " AND ( branchcode = ? OR branchcode IS NULL )" if $branch_limit; >-$auth_query .= " GROUP BY lib ORDER BY lib, lib_opac"; >-my $authorised_values_sth = $dbh->prepare( $auth_query ); >-$authorised_values_sth->execute( >- $branch_limit ? $branch_limit : (), >-); >- >-while ( my ( $category, $value, $lib ) = $authorised_values_sth->fetchrow_array ) { >- $authorised_values->{$category} ||= []; >- push @{ $authorised_values->{$category} }, { value => $lib, lib => $lib }; >+my $results; >+if( $branch_limit ) { >+ $results = $schema->resultset( "AuthorisedValue" )->search( >+ { "authorised_values_branches.branchcode" => { "=", [ $branch_limit, undef ] } }, >+ { join => "authorised_values_branches", order_by => "lib" } ); >+} else { >+ $results = $schema->resultset( "AuthorisedValue" )->search( >+ undef, >+ { order_by => "lib" } ); >+} >+ >+foreach my $result ( $results->all ) { >+ $authorised_values->{$result->category} ||= []; >+ push @{ $authorised_values->{$result->category} }, { value => $result->authorised_value, lib => $result->lib }; > } > > $response->param( framework => \@tags, authorised_values => $authorised_values ); >diff --git a/svc/cataloguing/metasearch b/svc/cataloguing/metasearch >new file mode 100755 >index 0000000..110732f >--- /dev/null >+++ b/svc/cataloguing/metasearch >@@ -0,0 +1,76 @@ >+#!/usr/bin/perl >+# >+# Copyright 2014 ByWater Solutions >+# >+# This file is part of Koha. >+# >+# Koha is free software; you can redistribute it and/or modify it >+# under the terms of the GNU General Public License as published by >+# the Free Software Foundation; either version 3 of the License, or >+# (at your option) any later version. >+# >+# Koha is distributed in the hope that it will be useful, but >+# WITHOUT ANY WARRANTY; without even the implied warranty of >+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the >+# GNU General Public License for more details. >+# >+# You should have received a copy of the GNU General Public License >+# along with Koha; if not, see <http://www.gnu.org/licenses>. >+ >+use Modern::Perl; >+ >+use C4::Service; >+use Encode qw( encode_utf8 ); >+use Koha::MetaSearcher; >+ >+my ( $query, $response ) = C4::Service->init( catalogue => 1 ); >+ >+my ( $query_string, $servers ) = C4::Service->require_params( 'q', 'servers' ); >+ >+my $server_errors = {}; >+ >+my $sort_key = $query->param( 'sort_key' ) || 'title'; >+my $sort_direction = $query->param( 'sort_direction' ) || 'asc'; >+my $offset = $query->param( 'offset' ) || 0; >+my $page_size = $query->param( 'page_size' ) || 20; >+my $fetched = $query->param( 'fetched' ) || 100; >+ >+my $searcher = Koha::MetaSearcher->new( { >+ fetched => $fetched, >+ on_error => sub { >+ my ( $server, $exception ) = @_; >+ >+ $server_errors->{ $server->{id} } = $exception->message; >+ }, >+} ); >+ >+$searcher->resultset( $query->param('resultset') ) if ( $query->param('resultset') ); >+ >+my @server_ids = split( /,/, $servers ); >+my $stats = $searcher->search( \@server_ids, $query_string ); >+ >+$searcher->sort( $sort_key, $sort_direction eq 'desc' ? -1 : 1 ); >+ >+my @hits; >+ >+foreach my $hit ( $searcher->results( $offset, $page_size ) ) { >+ push @hits, { >+ server => $hit->{server}->{id}, >+ index => $hit->{index}, >+ record => $hit->{record}->as_xml_record(), >+ metadata => $hit->{metadata} >+ }; >+} >+ >+$response->param( >+ resultset => $searcher->resultset, >+ sort_key => $sort_key, >+ sort_direction => $sort_direction, >+ offset => $offset, >+ page_size => $page_size, >+ errors => $server_errors, >+ hits => \@hits, >+ %$stats >+); >+ >+C4::Service->return_success( $response ); >diff --git a/svc/z3950 b/svc/z3950 >deleted file mode 100755 >index e0c07e3..0000000 >--- a/svc/z3950 >+++ /dev/null >@@ -1,102 +0,0 @@ >-#!/usr/bin/perl >-# >-# Copyright 2014 ByWater Solutions >- >-# >-# This file is part of Koha. >-# >-# Koha is free software; you can redistribute it and/or modify it under the >-# terms of the GNU General Public License as published by the Free Software >-# Foundation; either version 3 of the License, or (at your option) any later >-# version. >-# >-# Koha is distributed in the hope that it will be useful, but WITHOUT ANY >-# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR >-# A PARTICULAR PURPOSE. See the GNU General Public License for more details. >-# >-# You should have received a copy of the GNU General Public License along >-# with Koha; if not, write to the Free Software Foundation, Inc., >-# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. >- >-use Modern::Perl; >- >-use C4::Breeding qw( RunZ3950 ); >-use C4::Service; >-use Encode qw( encode_utf8 ); >- >-use sort 'stable'; >- >-my ( $query, $response ) = C4::Service->init( catalogue => 1 ); >- >-my ( $query_string, $servers ) = C4::Service->require_params( 'q', 'servers' ); >- >-my $server_hits = {}; >-my $server_errors = {}; >- >-my $offset = $query->param( 'offset' ) || 0; >-my $page_size = $query->param( 'page_size' ) || 20; >-my $fetched = $query->param( 'fetched' ) || 100; >-my $empty_flip = -1; # Determines the flip of ordering for records with empty sort keys. >- >-my @server_ids = split( /,/, $servers ); >- >-my $stats = RunZ3950( \@server_ids, $query_string, { >- on_error => sub { >- my ( $server, $exception ) = @_; >- >- $server_errors->{ $server->{id} } = $exception->message; >- }, >- on_hit => sub { >- my ( $server, $hit ) = @_; >- >- push @{ $server_hits->{ $server->{id} } }, { server => $server->{id}, >- index => $hit->{index}, >- record => encode_utf8( $hit->{record}->as_xml_record() ), >- metadata => $hit->{metadata} >- }; >- }, >- >- offset => 0, >- fetch => $fetched, >-} ); >- >-my @hits; >- >-foreach my $id ( @server_ids ) { >- warn scalar @{ $server_hits->{$id} ||= [] } . ' hits from ' . $id; >-} >- >-# Interleave hits; should be replaced by actual relevance ranking at some point >-foreach my $i ( 0..$fetched ) { >- foreach my $id ( @server_ids ) { >- my $hit = shift @{ $server_hits->{ $id } }; >- next unless ( $hit ); >- >- ( $hit->{sort_key} = $hit->{metadata}->{title} || '' ) =~ s/\W//g; >- push @hits, $hit; >- } >-} >- >-@hits = sort { >- # Sort empty records at the end >- return -$empty_flip unless $a->{sort_key}; >- return $empty_flip unless $b->{sort_key}; >- >- $a->{sort_key} cmp $b->{sort_key}; >-} @hits; >- >-my @hits_subset; >- >-foreach my $i ( $offset..( $offset + $page_size - 1 ) ) { >- push @hits_subset, $hits[$i] if $hits[$i]; >-} >- >-$response->param( >- offset => $offset, >- page_size => $page_size, >- errors => $server_errors, >- hits => \@hits_subset, >- %$stats >-); >- >-C4::Service->return_success( $response ); >-- >2.1.4
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
|
Splinter Review
Attachments on
bug 11559
:
26069
|
26070
|
26071
|
26104
|
26653
|
26654
|
27140
|
27141
|
27142
|
27757
|
27758
|
27759
|
27760
|
27761
|
27900
|
27901
|
27902
|
27993
|
27995
|
34458
|
34459
|
34460
|
34461
|
34462
|
34463
|
36243
|
36244
|
36245
|
36246
|
36247
|
36248
|
36455
|
38533
|
38534
|
38535
|
38536
|
38537
|
38538
|
38539
|
40153
|
40154
|
40155
|
40543
|
40555
|
40556
|
40557
|
40558
|
40811
|
40812
|
40813
|
40814
|
40815
|
41425
|
41453
|
41476
|
42373
|
43261
|
43547
|
43614
|
43615
|
43616
|
43617
|
43618
|
43619
|
43620
|
43621
|
43622
|
43623
|
43703
|
43704
|
43705
|
43706
|
43707
|
43708
|
43709
|
43710
|
43711
|
43712
|
43713
|
43772
|
43790
|
43883
|
43969
|
44016
|
44018
|
44022
|
44023
|
44024
|
44025
|
44026
|
44027
|
44028
|
44029
|
44030
|
44031
|
44032
|
44033
|
44034
|
44035
|
44036