Bug 11559: Rancor: advanced cataloging interface

Full test plan is posted on bug. Test plan for system preference:

  1. Apply patch, clear cookies.
  2. Go to "Cataloging."
  3. Add new record, verify that basic editor is used.
  4. Navigate to existing record, click on "Edit record", verify that
     basic editor is used.
  5. Inside basic editor, verify that no button appears to switch to the
     advanced editor.
  6. Enable the "EnableAdvancedCatalogingEditor" syspref.
  7. Repeat above steps, should still go to basic editor, but button
     should appear to switch to the advanced editor; click it.
  8. Now, adding new records and editing existing records should go to
     the advanced editor.

Signed-off-by: Nick Clemens <nick@quecheelibrary.org>

Signed-off-by: Katrin Fischer <katrin.fischer.83@web.de>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>
This commit is contained in:
Jesse Weaver 2015-06-15 17:09:27 -04:00 committed by Tomas Cohen Arazi
parent ac05c178b0
commit edd64d3018
29 changed files with 5102 additions and 4 deletions

351
Koha/MetaSearcher.pm Normal file
View File

@ -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;

View File

@ -117,4 +117,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;

View File

@ -746,8 +746,14 @@ if ($frameworkcode eq 'FA'){
'stickyduedate' => $fa_stickyduedate,
'duedatespec' => $fa_duedatespec,
);
} elsif ( C4::Context->preference('EnableAdvancedCatalogingEditor') && $input->cookie( 'catalogue_editor_' . $loggedinuser ) eq 'advanced' && !$breedingid ) {
# Only use the advanced editor for non-fast-cataloging.
# breedingid is not handled because those would only come off a Z39.50
# search initiated by the basic editor.
print $input->redirect( '/cgi-bin/koha/cataloguing/editor.pl' . ( $biblionumber ? ( '#catalog/' . $biblionumber ) : '' ) );
}
# Getting the list of all frameworks
# get framework list
my $frameworks = getframeworks;

67
cataloguing/editor.pl Executable file
View File

@ -0,0 +1,67 @@
#!/usr/bin/perl
#
# Copyright 2013 ByWater
#
# 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 2 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 '2009';
use CGI;
use MARC::Record;
use C4::Auth;
use C4::Biblio;
use C4::Context;
use C4::Output;
use DBIx::Class::ResultClass::HashRefInflator;
use Koha::Database;
my $input = CGI->new;
my ( $template, $loggedinuser, $cookie ) = get_template_and_user(
{
template_name => 'cataloguing/editor.tt',
query => $input,
type => 'intranet',
authnotrequired => 0,
flagsrequired => { editcatalogue => 'edit_catalogue' },
}
);
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' );
# Z39.50 servers
my $dbh = C4::Context->dbh;
$template->{VARS}->{z3950_servers} = $dbh->selectall_arrayref( q{
SELECT * FROM z3950servers
WHERE recordtype != 'authority' AND servertype = 'zed'
ORDER BY servername
}, { Slice => {} } );
output_html_with_http_headers $input, $cookie, $template->output;

View File

@ -0,0 +1 @@
INSERT IGNORE INTO systempreferences ( `variable`, `value`, `options`, `explanation`, `type` ) VALUES ('EnableAdvancedCatalogingEditor','0','','Enable the Rancor advanced cataloging editor','YesNo');

View File

@ -123,6 +123,7 @@ INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, `
('DumpTemplateVarsOpac', '0', NULL , 'If enabled, dump all Template Toolkit variable to a comment in the html source for the opac.', 'YesNo'),
('EasyAnalyticalRecords','0','','If on, display in the catalogue screens tools to easily setup analytical record relationships','YesNo'),
('emailLibrarianWhenHoldIsPlaced','0',NULL,'If ON, emails the librarian whenever a hold is placed','YesNo'),
('EnableAdvancedCatalogingEditor','0','','Enable the Rancor advanced cataloging editor','YesNo'),
('EnableBorrowerFiles','0',NULL,'If enabled, allows librarians to upload and attach arbitrary files to a borrower record.','YesNo'),
('EnableOpacSearchHistory','1','YesNo','Enable or disable opac search history',''),
('EnableSearchHistory','0','','Enable or disable search history','YesNo'),

View File

@ -0,0 +1,220 @@
/**
* 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 = {};
var _framework_mappings = {};
function _fromXMLStruct( data ) {
result = {};
$(data).children().eq(0).children().each( function() {
var $contents = $(this).contents();
if ( $contents.length == 1 && $contents[0].nodeType == Node.TEXT_NODE ) {
result[ this.localName ] = $contents[0].data;
} else {
result[ this.localName ] = $contents.filter( function() { return this.nodeType != Node.TEXT_NODE || !this.data.match( /^\s+$/ ) } ).toArray();
}
} );
return result;
}
function _importFramework( frameworkcode, frameworkinfo ) {
_frameworks[frameworkcode] = frameworkinfo;
_framework_mappings[frameworkcode] = {};
$.each( frameworkinfo, function( i, tag ) {
var tagnum = tag[0], taginfo = tag[1];
var subfields = {};
$.each( taginfo.subfields, function( i, subfield ) {
subfields[ subfield[0] ] = subfield[1];
} );
_framework_mappings[frameworkcode][tagnum] = $.extend( {}, taginfo, { subfields: subfields } );
} );
}
_importFramework( '', defaultFramework.framework );
var KohaBackend = {
NOT_EMPTY: {}, // Sentinel value
GetAllTagsInfo: function( frameworkcode, tagnumber ) {
return _framework_mappings[frameworkcode];
},
GetAuthorisedValues: function( category ) {
return _authorised_values[category];
},
GetTagInfo: function( frameworkcode, tagnumber ) {
if ( !_framework_mappings[frameworkcode] ) return undefined;
return _framework_mappings[frameworkcode][tagnumber];
},
GetRecord: function( id, callback ) {
$.get(
'/cgi-bin/koha/svc/bib/' + id
).done( function( data ) {
var record = new MARC.Record();
record.loadMARCXML(data);
callback(record);
} ).fail( function( data ) {
callback( { error: data } );
} );
},
CreateRecord: function( record, callback ) {
$.ajax( {
type: 'POST',
url: '/cgi-bin/koha/svc/new_bib',
data: record.toXML(),
contentType: 'text/xml'
} ).done( function( data ) {
callback( _fromXMLStruct( data ) );
} ).fail( function( data ) {
callback( { error: data } );
} );
},
SaveRecord: function( id, record, callback ) {
$.ajax( {
type: 'POST',
url: '/cgi-bin/koha/svc/bib/' + id,
data: record.toXML(),
contentType: 'text/xml'
} ).done( function( data ) {
callback( _fromXMLStruct( data ) );
} ).fail( function( data ) {
callback( { data: { error: data } } );
} );
},
GetTagsBy: function( frameworkcode, field, value ) {
var result = {};
$.each( _frameworks[frameworkcode], function( undef, tag ) {
var tagnum = tag[0], taginfo = tag[1];
if ( taginfo[field] == value ) result[tagnum] = true;
} );
return result;
},
GetSubfieldsBy: function( frameworkcode, field, value ) {
var result = {};
$.each( _frameworks[frameworkcode], function( undef, tag ) {
var tagnum = tag[0], taginfo = tag[1];
$.each( taginfo.subfields, function( undef, subfield ) {
var subfieldcode = subfield[0], subfieldinfo = subfield[1];
if ( subfieldinfo[field] == value ) {
if ( !result[tagnum] ) result[tagnum] = {};
result[tagnum][subfieldcode] = true;
}
} );
} );
return result;
},
FillRecord: function( frameworkcode, record, allTags ) {
$.each( _frameworks[frameworkcode], function( undef, tag ) {
var tagnum = tag[0], taginfo = tag[1];
if ( taginfo.mandatory != "1" && !allTags ) return;
var fields = record.fields(tagnum);
if ( fields.length == 0 ) {
var newField = new MARC.Field( tagnum, ' ', ' ', [] );
fields.push( newField );
record.addFieldGrouped( newField );
if ( tagnum < '010' ) {
newField.addSubfield( [ '@', '' ] );
return;
}
}
$.each( taginfo.subfields, function( undef, subfield ) {
var subfieldcode = subfield[0], subfieldinfo = subfield[1];
if ( subfieldinfo.mandatory != "1" && !allTags ) return;
$.each( fields, function( undef, field ) {
if ( !field.hasSubfield(subfieldcode) ) field.addSubfieldGrouped( [ subfieldcode, '' ] );
} );
} );
} );
},
ValidateRecord: function( frameworkcode, record ) {
var errors = [];
var mandatoryTags = KohaBackend.GetTagsBy( '', 'mandatory', '1' );
var mandatorySubfields = KohaBackend.GetSubfieldsBy( '', 'mandatory', '1' );
var nonRepeatableTags = KohaBackend.GetTagsBy( '', 'repeatable', '0' );
var nonRepeatableSubfields = KohaBackend.GetSubfieldsBy( '', 'repeatable', '0' );
$.each( mandatoryTags, function( tag ) {
if ( !record.hasField( tag ) ) errors.push( { type: 'missingTag', tag: tag } );
} );
var seenTags = {};
$.each( record.fields(), function( undef, field ) {
if ( seenTags[ field.tagnumber() ] && nonRepeatableTags[ field.tagnumber() ] ) {
errors.push( { type: 'unrepeatableTag', line: field.sourceLine, tag: field.tagnumber() } );
return;
}
seenTags[ field.tagnumber() ] = true;
var seenSubfields = {};
$.each( field.subfields(), function( undef, subfield ) {
if ( seenSubfields[ subfield[0] ] != null && nonRepeatableSubfields[ field.tagnumber() ][ subfield[0] ] ) {
errors.push( { type: 'unrepeatableSubfield', subfield: subfield[0], line: field.sourceLine } );
} else {
seenSubfields[ subfield[0] ] = subfield[1];
}
} );
$.each( mandatorySubfields[ field.tagnumber() ] || {}, function( subfield ) {
if ( !seenSubfields[ subfield ] ) {
errors.push( { type: 'missingSubfield', subfield: subfield[0], line: field.sourceLine } );
}
} );
} );
return errors;
},
};
return KohaBackend;
} );

View File

@ -0,0 +1,38 @@
/**
* 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 = {
formats: {
its: {
description: 'TLC® ITS',
Run: ITSMacro.Run,
},
rancor: {
description: 'Rancor',
Run: RancorMacro.Run,
},
},
Run: function( editor, format, macro ) {
return Macros.formats[format].Run( editor, macro );
},
};
return Macros;
} );

View File

@ -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;
} );

View File

@ -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;
} );

View File

@ -0,0 +1,671 @@
/**
* 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;
var field = editor.getCurrentField();
if ( !field ) return;
// Set overwrite mode for tag numbers/indicators and contents of fixed fields
if ( field.isControlField || cm.getCursor().ch < 8 ) {
cm.toggleOverwrite(true);
} else {
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' || change.origin == 'widget.clearToText' ) return;
// FIXME: Should only cancel changes if this is a control field/subfield widget
if ( change.from.line !== change.to.line || Math.abs( change.from.ch - change.to.ch ) > 1 || change.text.length != 1 || change.text[0].length != 0 ) return; // Not single-char change
if ( change.from.ch == change.to.ch - 1 && cm.findMarksAt( { line: change.from.line, ch: change.from.ch + 1 } ).length ) {
change.cancel();
} else if ( change.from.ch == change.to.ch && cm.findMarksAt(change.from).length && !change.text[0].match(/^[$|ǂ‡]$/) ) {
change.cancel();
}
}
function editorChanges( cm, changes ) {
var editor = cm.marceditor;
if ( editor.textMode ) return;
for (var i = 0; i < changes.length; i++) {
var change = changes[i];
var origin = change.from.line;
var newTo = CodeMirror.changeEnd(change);
for (var delLine = origin; delLine <= change.to.line; delLine++) {
// Line deleted; currently nothing to do
}
for (var line = origin; line <= newTo.line; line++) {
if ( Preferences.user.fieldWidgets ) Widget.UpdateLine( cm.marceditor, line );
if ( change.origin != 'setValue' && change.origin != 'marcWidgetPrefill' && change.origin != 'widget.clearToText' ) cm.addLineClass( line, 'wrapper', 'modified-line' );
}
}
Widget.ActivateAt( cm, cm.getCursor() );
cm.marceditor.startNotify();
}
// Editor helper functions
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 ) {
cur = cur || editor.cm.getCursor();
var field = editor.getFieldAt( cur.line );
if ( field ) {
if ( field.isControlField ) {
var positions = [ { ch: 0 }, { ch: 4 } ];
$.each( positions, function( undef, pos ) {
pos.line = cur.line;
} );
return positions;
} else {
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 [];
}
}
var _editorKeys = {
Enter: function( cm ) {
var cursor = cm.getCursor();
cm.replaceRange( '\n', { line: cursor.line }, null, 'marcAware' );
cm.setCursor( { line: cursor.line + 1, ch: 0 } );
},
'Ctrl-X': function( cm ) {
// Delete line (or cut)
if ( cm.somethingSelected() ) return true;
var field = cm.marceditor.getCurrentField();
if ( field ) field.delete();
},
'Shift-Ctrl-X': function( cm ) {
// Delete subfield
var field = cm.marceditor.getCurrentField();
if ( !field ) return;
var subfield = field.getSubfieldAt( cm.getCursor().ch );
if ( subfield ) subfield.delete();
},
Tab: function( cm ) {
// Move through parts of tag/fixed fields
var positions = getTabPositions( cm.marceditor );
var cur = cm.getCursor();
for ( var i = 0; i < positions.length; i++ ) {
if ( positions[i].ch > cur.ch ) {
activateTabPosition( cm, positions[i] );
return false;
}
}
cm.setCursor( { line: cur.line + 1, ch: 0 } );
},
'Shift-Tab': function( cm ) {
// Move backwards through parts of tag/fixed fields
var positions = getTabPositions( cm.marceditor );
var cur = cm.getCursor();
for ( var i = positions.length - 1; i >= 0; i-- ) {
if ( positions[i].ch < cur.ch ) {
activateTabPosition( cm, positions[i] );
return false;
}
}
if ( cur.line == 0 ) return;
var prevPositions = getTabPositions( cm.marceditor, { line: cur.line - 1, ch: cm.getLine( cur.line - 1 ).length } );
if ( prevPositions.length ) {
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 + '")';
};
// 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(
options.position,
{
extraKeys: _editorKeys,
gutters: [
'modified-line-gutter',
],
lineWrapping: true,
mode: {
name: 'marc',
nonRepeatableTags: KohaBackend.GetTagsBy( '', 'repeatable', '0' ),
nonRepeatableSubfields: KohaBackend.GetSubfieldsBy( '', 'repeatable', '0' )
}
}
);
this.cm.marceditor = this;
this.cm.on( 'beforeChange', editorBeforeChange );
this.cm.on( 'changes', editorChanges );
this.cm.on( 'cursorActivity', editorCursorActivity );
this.onCursorActivity = options.onCursorActivity;
this.subscribers = [];
this.subscribe( function( marceditor ) {
Widget.Notify( marceditor );
} );
}
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();
} );
}
},
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 );
}
this.textMode = false;
return record;
},
getLineInfo: function( pos ) {
var contents = this.cm.getLine( pos.line );
if ( contents == null ) return {};
var tagNumber = contents.match( /^([A-Za-z0-9]{3})/ );
if ( !tagNumber ) return null; // No tag at all on this line
tagNumber = tagNumber[1];
if ( tagNumber < '010' ) return { tagNumber: tagNumber, contents: contents }; // No current subfield
var matcher = /[$|ǂ‡]([a-z0-9%]) /g;
var match;
var subfields = [];
var currentSubfield;
while ( ( match = matcher.exec(contents) ) ) {
subfields.push( { code: match[1], ch: match.index } );
if ( match.index < pos.ch ) currentSubfield = match[1];
}
return { tagNumber: tagNumber, subfields: subfields, currentSubfield: currentSubfield, contents: contents };
},
addError: function( line, error ) {
var found = false;
var options = {};
if ( line == null ) {
line = 0;
options.above = true;
}
$.each( this.cm.getLineHandle(line).widgets || [], function( undef, widget ) {
if ( !widget.isErrorMarker ) return;
found = true;
$( widget.node ).append( '; ' + error );
widget.changed();
return false;
} );
if ( found ) return;
var node = $( '<div class="structure-error"><i class="icon-remove"></i> ' + error + '</div>' )[0];
var widget = this.cm.addLineWidget( line, node, options );
widget.node = node;
widget.isErrorMarker = true;
},
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();
} );
}
},
startNotify: function() {
if ( this.notifyTimeout ) clearTimeout( this.notifyTimeout );
this.notifyTimeout = setTimeout( $.proxy( function() {
this.notifyAll();
this.notifyTimeout = null;
}, this ), NOTIFY_TIMEOUT );
},
notifyAll: function() {
$.each( this.subscribers, $.proxy( function( undef, subscriber ) {
subscriber(this);
}, this ) );
},
subscribe: function( subscriber ) {
this.subscribers.push( subscriber );
},
createField: function( tag, line ) {
var contents = tag + ( tag < '010' ? ' ' : ' _ _ ' );
if ( line > this.cm.lastLine() ) {
contents = '\n' + contents;
} else {
contents = contents + '\n';
}
this.cm.replaceRange( contents, { line: line, ch: 0 }, null, 'marcAware' );
return new EditorField( this, line );
},
createFieldOrdered: function( tag ) {
var line, contents;
for ( line = 0; (contents = this.cm.getLine(line)); line++ ) {
if ( contents && contents.substr(0, 3) > tag ) break;
}
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;
} );

View File

@ -0,0 +1,168 @@
/**
* 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
CodeMirror.defineMode( 'marc', function( config, modeConfig ) {
modeConfig.nonRepeatableTags = modeConfig.nonRepeatableTags || {};
modeConfig.nonRepeatableSubfields = modeConfig.nonRepeatableSubfields || {};
return {
startState: function( prevState ) {
var state = prevState || {};
if ( !prevState ) {
state.seenTags = {};
}
state.indicatorNeeded = false;
state.subAllowed = true;
state.subfieldCode = undefined;
state.tagNumber = undefined;
state.seenSubfields = {};
return state;
},
copyState: function( prevState ) {
var result = $.extend( {}, prevState );
result.seenTags = $.extend( {}, prevState.seenTags );
result.seenSubfields = $.extend( {}, prevState.seenSubfields );
return result;
},
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]+/ ) ) {
match = match[0];
if ( match.length != 3 ) {
if ( stream.eol() && match.length < 3 ) {
// Don't show error for incomplete number
return 'tagnumber';
} else {
stream.skipToEnd();
return 'error';
}
}
state.tagNumber = match;
if ( state.tagNumber < '010' ) {
// Control field, no subfields or indicators
state.subAllowed = false;
}
if ( state.seenTags[state.tagNumber] && modeConfig.nonRepeatableTags[state.tagNumber] ) {
return 'bad-tagnumber';
} else {
state.seenTags[state.tagNumber] = true;
return 'tagnumber';
}
} else {
stream.skipToEnd();
return 'error';
}
}
// 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 '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 'required-space';
} else {
stream.skipToEnd();
return 'error';
}
case 4:
case 6:
if ( /[0-9A-Za-z_]/.test( stream.next() ) ) {
return 'indicator';
} else {
stream.skipToEnd();
return 'error';
}
}
}
// Otherwise, we're after the start of the line.
if ( state.subAllowed ) {
// 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;
if ( ( subfieldCode = stream.eat( /[a-z0-9%]/ ) ) && stream.eat( ' ' ) ) {
state.subfieldCode = subfieldCode;
if ( state.seenSubfields[state.subfieldCode] && ( modeConfig.nonRepeatableSubfields[state.tagNumber] || {} )[state.subfieldCode] ) {
return 'bad-subfieldcode';
} else {
state.seenSubfields[state.subfieldCode] = true;
return 'subfieldcode';
}
}
}
if ( stream.pos < 11 && ( !stream.eol() || stream.pos == 8 ) ) {
stream.skipToEnd();
return 'error';
}
} else {
// Match space at end of line
if ( stream.match( /[ \t]+$/ ) ) {
return 'end-space';
} else {
stream.match( /[ \t]+/ );
}
stream.match( /[^ \t]+/ );
return;
}
}
};
} );

View File

@ -0,0 +1,370 @@
/**
* 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
*
* ISO2709 import/export is cribbed from marcjs, which is under the MIT license.
* Source: https://github.com/fredericd/marcjs/blob/master/lib/marcjs.js
*/
define( function() {
var MARC = {};
var _escape_map = {
"<": "&lt;",
"&": "&amp;",
"\"": "&quot;"
};
function _escape(str) {
return str.replace( /[<&"]/, function (c) { return _escape_map[c] } );
}
function _intpadded(i, digits) {
i = i + '';
while (i.length < digits) {
i = '0' + i;
}
return i;
}
MARC.Record = function (fieldlist) {
this._fieldlist = fieldlist || [];
}
$.extend( MARC.Record.prototype, {
leader: function(val) {
var field = this.field('000');
if (val) {
if (field) {
field.subfield( '@', val );
} else {
field = new MARC.Field( '000', '', '', [ [ '@', val ] ] );
this.addFieldGrouped(field);
}
} else {
return ( field && field.subfield('@') ) || ' nam a22 7a 4500';
}
},
/**
* If a tagnumber is given, returns all fields with that tagnumber.
* Otherwise, returns all fields.
*/
fields: function(fieldno) {
if (!fieldno) return this._fieldlist;
var results = [];
for(var i=0; i<this._fieldlist.length; i++){
if( this._fieldlist[i].tagnumber() == fieldno ) {
results.push(this._fieldlist[i]);
}
}
return results;
},
/**
* Returns the first field with the given tagnumber, or false.
*/
field: function(fieldno) {
for(var i=0; i<this._fieldlist.length; i++){
if( this._fieldlist[i].tagnumber() == fieldno ) {
return this._fieldlist[i];
}
}
return false;
},
/**
* Adds the given MARC.Field to the record, at the end.
*/
addField: function(field) {
this._fieldlist.push(field);
return true;
},
/**
* Adds the given MARC.Field to the record, at the end of the matching
* x00 group. If a record has a 100, 245 and 300 field, for instance, a
* 260 field would be added after the 245 field.
*/
addFieldGrouped: function(field) {
for ( var i = this._fieldlist.length - 1; i >= 0; i-- ) {
if ( this._fieldlist[i].tagnumber()[0] <= field.tagnumber()[0] ) {
this._fieldlist.splice(i+1, 0, field);
return true;
}
}
this._fieldlist.push(field);
return true;
},
/**
* Removes the first field with the given tagnumber. Returns false if no
* such field was found.
*/
removeField: function(fieldno) {
for(var i=0; i<this._fieldlist.length; i++){
if( this._fieldlist[i].tagnumber() == fieldno ) {
this._fieldlist.splice(i, 1);
return true;
}
}
return false;
},
/**
* Check to see if this record contains a field with the given
* tagnumber.
*/
hasField: function(fieldno) {
for(var i=0; i<this._fieldlist.length; i++){
if( this._fieldlist[i].tagnumber() == fieldno ) {
return true;
}
}
return false;
},
toXML: function() {
var xml = '<record xmlns="http://www.loc.gov/MARC21/slim">';
for(var i=0; i<this._fieldlist.length; i++){
xml += this._fieldlist[i].toXML();
}
xml += '</record>';
return xml;
},
/**
* Truncates this record, and loads in the data from the given MARCXML
* document.
*/
loadMARCXML: function(xmldoc) {
var record = this;
record.xmlSource = xmldoc;
this._fieldlist.length = 0;
this.leader( $('leader', xmldoc).text() );
$('controlfield', xmldoc).each( function(i) {
val = $(this).text();
tagnum = $(this).attr('tag');
record._fieldlist.push( new MARC.Field(tagnum, '', '', [ [ '@', val ] ]) );
});
$('datafield', xmldoc).each(function(i) {
var value = $(this).text();
var tagnum = $(this).attr('tag');
var ind1 = $(this).attr('ind1') || ' ';
var ind2 = $(this).attr('ind2') || ' ';
var subfields = new Array();
$('subfield', this).each(function(j) {
var sfval = $(this).text();
var sfcode = $(this).attr('code');
subfields.push( [ sfcode, sfval ] );
});
record._fieldlist.push( new MARC.Field(tagnum, ind1, ind2, subfields) );
});
},
toISO2709: function() {
var FT = '\x1e', RT = '\x1d', DE = '\x1f';
var directory = '',
from = 0,
chunks = ['', ''];
$.each( this._fieldlist, function( undef, element ) {
var chunk = '';
var tag = element.tagnumber();
if (tag == '000') {
return;
} else if (tag < '010') {
chunk = element.subfields()[0][1];
} else {
chunk = element.indicators().join('');
$.each( element.subfields(), function( undef, subfield ) {
chunk += DE + subfield[0] + subfield[1];
} );
}
chunk += FT;
chunks.push(chunk);
directory += _intpadded(tag,3) + _intpadded(chunk.length,4) + _intpadded(from,5);
from += chunk.length;
});
chunks.push(RT);
directory += FT;
var offset = 24 + 12 * (this._fieldlist.length - 1) + 1;
var length = offset + from + 1;
var leader = this.leader();
leader = _intpadded(length,5) + leader.substr(5,7) + _intpadded(offset,5) +
leader.substr(17);
chunks[0] = leader;
chunks[1] = directory;
return chunks.join('');
},
loadISO2709: function(data) {
this._fieldlist.length = 0;
this.leader(data.substr(0, 24));
var directory_len = parseInt(data.substring(12, 17), 0) - 25,
number_of_tag = directory_len / 12;
for (var i = 0; i < number_of_tag; i++) {
var off = 24 + i * 12,
tag = data.substring(off, off+3),
len = parseInt(data.substring(off+3, off+7), 0) - 1,
pos = parseInt(data.substring(off+7, off+12), 0) + 25 + directory_len,
value = data.substring(pos, pos+len);
if ( parseInt(tag) < 10 ) {
this.addField( new MARC.Field( tag, '', '', [ [ '@', value ] ] ) );
} else {
if ( value.indexOf('\x1F') ) { // There are some letters
var ind1 = value.substr(0, 1), ind2 = value.substr(1, 1);
var subfields = [];
$.each( value.substr(3).split('\x1f'), function( undef, v ) {
if (v.length < 2) return;
subfields.push([v.substr(0, 1), v.substr(1)]);
} );
this.addField( new MARC.Field( tag, ind1, ind2, subfields ) );
}
}
}
}
} );
MARC.Field = function(tagnumber, indicator1, indicator2, subfields) {
this._tagnumber = tagnumber;
this._indicators = [ indicator1, indicator2 ];
this._subfields = subfields;
};
$.extend( MARC.Field.prototype, {
tagnumber: function() {
return this._tagnumber;
},
isControlField: function() {
return this._tagnumber < '010';
},
indicator: function(num, val) {
if( val != null ) {
this._indicators[num] = val;
}
return this._indicators[num];
},
indicators: function() {
return this._indicators;
},
hasSubfield: function(code) {
for(var i = 0; i<this._subfields.length; i++) {
if( this._subfields[i][0] == code ) {
return true;
}
}
return false;
},
removeSubfield: function(code) {
for(var i = 0; i<this._subfields.length; i++) {
if( this._subfields[i][0] == code ) {
this._subfields.splice(i,1);
return true;
}
}
return false;
},
subfields: function() {
return this._subfields;
},
addSubfield: function(sf) {
this._subfields.push(sf);
return true;
},
addSubfieldGrouped: function(sf) {
function _kind( sc ) {
if ( /[a-z]/.test( sc ) ) {
return 0;
} else if ( /[0-9]/.test( sc ) ) {
return 1;
} else {
return 2;
}
}
for ( var i = this._subfields.length - 1; i >= 0; i-- ) {
if ( i == 0 && _kind( sf[0] ) < _kind( this._subfields[i][0] ) ) {
this._subfields.splice( 0, 0, sf );
return true;
} else if ( _kind( this._subfields[i][0] ) <= _kind( sf[0] ) ) {
this._subfields.splice( i + 1, 0, sf );
return true;
}
}
this._subfields.push(sf);
return true;
},
subfield: function(code, val) {
var sf = '';
for(var i = 0; i<this._subfields.length; i++) {
if( this._subfields[i][0] == code ) {
sf = this._subfields[i];
if( val != null ) {
sf[1] = val;
}
return sf[1];
}
}
return false;
},
toXML: function() {
// decide if it's controlfield of datafield
if( this._tagnumber == '000') {
return '<leader>' + _escape( this._subfields[0][1] ) + '</leader>';
} else if ( this._tagnumber < '010' ) {
return '<controlfield tag="' + this._tagnumber + '">' + _escape( this._subfields[0][1] ) + '</controlfield>';
} else {
var result = '<datafield tag="' + this._tagnumber + '"';
result += ' ind1="' + this._indicators[0] + '"';
result += ' ind2="' + this._indicators[1] + '">';
for( var i = 0; i< this._subfields.length; i++) {
result += '<subfield code="' + this._subfields[i][0] + '">';
result += _escape( this._subfields[i][1] );
result += '</subfield>';
}
result += '</datafield>';
return result;
}
}
} );
return MARC;
} );

View File

@ -0,0 +1,49 @@
/**
* 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 ) {
if ( borrowernumber == null ) return;
var saved_prefs;
try {
saved_prefs = JSON.parse( localStorage[ 'cateditor_preferences_' + borrowernumber ] );
} catch (e) {}
Preferences.user = $.extend( {
// Preference defaults
fieldWidgets: true,
font: 'monospace',
fontSize: '1em',
macros: {},
selected_search_targets: {},
}, saved_prefs );
},
Save: function( borrowernumber ) {
if ( !borrowernumber ) return;
if ( !Preferences.user ) Preferences.Load(borrowernumber);
localStorage[ 'cateditor_preferences_' + borrowernumber ] = JSON.stringify(Preferences.user);
},
};
return Preferences;
} );

View File

@ -0,0 +1,38 @@
/**
* 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 = [];
var Resources = {
GetAll: function() {
return $.when.call( null, _allResources );
}
};
function _res( name, deferred ) {
Resources[name] = deferred;
_allResources.push(deferred);
}
_res( 'marc21/xml/006', $.get( module.config().themelang + '/data/marc21_field_006.xml' ) );
_res( 'marc21/xml/008', $.get( module.config().themelang + '/data/marc21_field_008.xml' ) );
return Resources;
} );

View File

@ -0,0 +1,114 @@
/**
* 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 = {};
var _last;
var _pqfMapping = {
author: '1=1004', // s=al',
cn_dewey: '1=13',
cn_lc: '1=16',
date: '1=30', // r=r',
isbn: '1=7',
issn: '1=8',
lccn: '1=9',
local_number: '1=12',
music_identifier: '1=51',
standard_identifier: '1=1007',
subject: '1=21', // s=al',
term: '1=1016', // t=l,r s=al',
title: '1=4', // s=al',
}
var Search = {
Init: function( options ) {
_options = options;
},
JoinTerms: function( terms ) {
var q = '';
$.each( terms, function( i, term ) {
var term = '@attr ' + _pqfMapping[ term[0] ] + ' "' + term[1].replace( '"', '\\"' ) + '"'
if ( q ) {
q = '@and ' + q + ' ' + term;
} else {
q = term;
}
} );
return q;
},
Run: function( servers, q, options ) {
options = $.extend( {
offset: 0,
page_size: 20,
}, _options, options );
Search.includedServers = [];
_records = {};
_last = {
servers: servers,
q: q,
options: options,
};
$.each( servers, function ( id, info ) {
if ( info.checked ) Search.includedServers.push( id );
} );
$.get(
'/cgi-bin/koha/svc/cataloguing/metasearch',
{
q: q,
servers: Search.includedServers.join( ',' ),
offset: options.offset,
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 );
hit.record = record;
} );
_options.onresults( data );
} )
.fail( function( error ) {
_options.onerror( error );
} );
return true;
},
Fetch: function( options ) {
if ( !_last ) return;
$.extend( _last.options, options );
Search.Run( _last.servers, _last.q, _last.options );
}
};
return Search;
} );

View File

@ -0,0 +1,106 @@
/**
* 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 ) {
return text.replace( '$', '{dollar}' );
}
// Undo conversion
function _desanitize( text ) {
return text.replace( '{dollar}', '$' );
}
return {
RecordToText: function( record ) {
var lines = [];
var fields = record.fields();
for ( var i = 0; i < fields.length; i++ ) {
var field = fields[i];
if ( field.isControlField() ) {
lines.push( field.tagnumber() + ' ' + _sanitize( field.subfield( '@' ) ) );
} else {
var result = [ field.tagnumber() + ' ' ];
result.push( field.indicator(0) == ' ' ? '_' : field.indicator(0), ' ' );
result.push( field.indicator(1) == ' ' ? '_' : field.indicator(1), ' ' );
$.each( field.subfields(), function( i, subfield ) {
result.push( '$' + subfield[0] + ' ' + _sanitize( subfield[1] ) );
} );
lines.push( result.join('') );
}
}
return lines.join('\n');
},
TextToRecord: function( text ) {
var record = new MARC.Record();
var errors = [];
$.each( text.split('\n'), function( i, line ) {
var tagNumber = line.match( /^([A-Za-z0-9]{3}) / );
if ( !tagNumber ) {
errors.push( { type: 'noTag', line: i } );
return;
}
tagNumber = tagNumber[1];
if ( tagNumber < '010' ) {
var field = new MARC.Field( tagNumber, ' ', ' ', [ [ '@', _desanitize( line.substring( 4 ) ) ] ] );
field.sourceLine = i;
record.addField( field );
} else {
var indicators = line.match( /^... ([0-9A-Za-z_]) ([0-9A-Za-z_])/ );
if ( !indicators ) {
errors.push( { type: 'noIndicators', line: i } );
return;
}
var field = new MARC.Field( tagNumber, ( indicators[1] == '_' ? ' ' : indicators[1] ), ( indicators[2] == '_' ? ' ' : indicators[2] ), [] );
var matcher = /[$|ǂ‡]([a-z0-9%]) /g;
var match;
var subfields = [];
while ( ( match = matcher.exec(line) ) ) {
subfields.push( { code: match[1], ch: match.index } );
}
$.each( subfields, function( i, subfield ) {
var next = subfields[ i + 1 ];
field.addSubfield( [ subfield.code, _desanitize( line.substring( subfield.ch + 3, next ? next.ch : line.length ) ) ] );
} );
field.sourceLine = i;
record.addField( field );
}
} );
return errors.length ? { errors: errors } : record;
}
};
} );

View File

@ -0,0 +1,310 @@
/**
* 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 = {};
var Widget = {
Register: function( tagfield, widget ) {
_widgets[tagfield] = widget;
},
PadNum: function( number, length ) {
var result = number.toString();
while ( result.length < length ) result = '0' + result;
return result;
},
PadString: function( result, length ) {
while ( result.length < length ) result = ' ' + result;
return result;
},
PadStringRight: function( result, length ) {
result = '' + result;
while ( result.length < length ) result += ' ';
return result;
},
Base: {
// Marker utils
clearToText: function() {
var range = this.mark.find();
if ( this.text == null ) throw new Error('Tried to clear widget with no text');
this.mark.doc.replaceRange( this.text, range.from, range.to, 'widget.clearToText' );
},
reCreate: function() {
this.postCreate( this.node, this.mark );
},
// Fixed field utils
bindFixed: function( sel, start, end ) {
var $node = $( this.node ).find( sel );
$node.val( this.getFixed( start, end ) );
var widget = this;
var $collapsed = $( '<span class="fixed-collapsed" title="' + $node.attr('title') + '">' + $node.val() + '</span>' ).insertAfter( $node );
function show() {
$collapsed.hide();
$node.val( widget.getFixed( start, end ).replace(/\s+$/, '') );
$node.show();
$node[0].focus();
}
function hide() {
$node.hide();
$collapsed.text( Widget.PadStringRight( $node.val(), end - start ) ).show();
}
$node.on( 'change keyup', function() {
widget.setFixed( start, end, $node.val(), '+input' );
} ).focus( show ).blur( hide );
hide();
$collapsed.click( show );
},
getFixed: function( start, end ) {
return this.text.substring( start, end );
},
setFixed: function( start, end, value, source ) {
this.setText( this.text.substring( 0, start ) + Widget.PadStringRight( value.toString().substr( 0, end - start ), end - start ) + this.text.substring( end ), source );
},
setText: function( text, source ) {
if ( source == '+input' ) this.mark.doc.cm.addLineClass( this.mark.find().from.line, 'wrapper', 'modified-line' );
this.text = text;
this.editor.startNotify();
},
createFromXML: function( resourceId ) {
var widget = this;
Resources[resourceId].done( function( xml ) {
$(widget.node).find('.widget-loading').remove();
var $matSelect = $('<select class="material-select"></select>').appendTo(widget.node);
var $contents = $('<span class="material-contents"/>').appendTo(widget.node);
var materialInfo = {};
$('Tagfield', xml).children('Material').each( function() {
$matSelect.append( '<option value="' + $(this).attr('id') + '">' + $(this).attr('id') + ' - ' + $(this).children('name').text() + '</option>' );
materialInfo[ $(this).attr('id') ] = this;
} );
$matSelect.change( function() {
widget.loadXMLMaterial( materialInfo[ $matSelect.val() ] );
} ).change();
} );
},
loadXMLMaterial: function( materialInfo ) {
var $contents = $(this.node).children('.material-contents');
$contents.empty();
var widget = this;
$(materialInfo).children('Position').each( function() {
var match = $(this).attr('pos').match(/(\d+)(?:-(\d+))?/);
if (!match) return;
var start = parseInt(match[1]);
var end = ( match[2] ? parseInt(match[2]) : start ) + 1;
var $input;
var $values = $(this).children('Value');
if ($values.length == 0) {
$contents.append( '<span title="' + $(this).children('name').text() + '">' + widget.getFixed(start, end) + '</span>' );
return;
}
if ( match[2] ) {
$input = $( '<input name="f' + Widget.PadNum(start, 2) + '" title="' + $(this).children('name').text() + '" maxlength="' + (end - start) + '" />' );
} else {
$input = $( '<select name="f' + Widget.PadNum(start, 2) + '" title="' + $(this).children('name').text() + '"></select>' );
$values.each( function() {
$input.append( '<option value="' + $(this).attr('code') + '">' + $(this).attr('code') + ' - ' + $(this).children('description').text() + '</option>' );
} );
}
$contents.append( $input );
widget.bindFixed( $input, start, end );
} );
},
nodeChanged: function() {
this.mark.changed();
var widget = this;
var $inputs = $(this.node).find('input, select');
if ( !$inputs.length ) return;
$inputs.off('keydown.marc-tab');
var editor = widget.editor;
$inputs.each( function( i ) {
$(this).on( 'keydown.marc-tab', function( e ) {
if ( e.which != 9 ) return; // Tab
var span = widget.mark.find();
var cur = editor.cm.getCursor();
if ( e.shiftKey ) {
if ( i > 0 ) {
$inputs.eq(i - 1).trigger( 'focus' );
} else {
editor.cm.setCursor( span.from );
// FIXME: ugly hack
editor.cm.options.extraKeys['Shift-Tab']( editor.cm );
editor.focus();
}
} else {
if ( i < $inputs.length - 1 ) {
$inputs.eq(i + 1).trigger( 'focus' );
} else {
editor.cm.setCursor( span.to );
editor.focus();
}
}
return false;
} );
} );
},
// Template utils
insertTemplate: function( sel ) {
var wsOnly = /^\s*$/;
$( sel ).contents().clone().each( function() {
if ( this.nodeType == Node.TEXT_NODE ) {
this.data = this.data.replace( /^\s+|\s+$/g, '' );
}
} ).appendTo( this.node );
},
},
ActivateAt: function( editor, cur, idx ) {
var marks = editor.findMarksAt( cur );
if ( !marks.length ) return false;
var $input = $(marks[0].widget.node).find('input, select').eq(idx || 0);
if ( !$input.length ) return false;
$input.focus();
return true;
},
Notify: function( editor ) {
$.each( editor.cm.getAllMarks(), function( undef, mark ) {
if ( mark.widget && mark.widget.notify ) mark.widget.notify();
} );
},
UpdateLine: function( editor, line ) {
var info = editor.getLineInfo( { line: line, ch: 0 } );
var lineh = editor.cm.getLineHandle( line );
if ( !lineh ) return;
if ( !info ) {
if ( lineh.markedSpans ) {
$.each( lineh.markedSpans, function ( undef, span ) {
var mark = span.marker;
if ( !mark.widget ) return;
mark.widget.clearToText();
} );
}
return;
}
var subfields = [];
var end = editor.cm.getLine( line ).length;
if ( info.tagNumber < '010' ) {
if ( end >= 4 ) subfields.push( { code: '@', from: 4, to: end } );
} else {
for ( var i = 0; i < info.subfields.length; i++ ) {
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 ) {
var id = info.tagNumber + subfield.code;
var marks = editor.cm.findMarksAt( { line: line, ch: subfield.from } );
if ( marks.length ) {
if ( marks[0].id == id ) {
return;
} else {
marks[0].widget.clearToText();
}
}
if ( !_widgets[id] ) return;
var fullBase = $.extend( Object.create( Widget.Base ), _widgets[id] );
var widget = Object.create( fullBase );
if ( subfield.from == subfield.to ) {
editor.cm.replaceRange( widget.makeTemplate ? widget.makeTemplate() : '<empty>', { line: line, ch: subfield.from }, null, 'marcWidgetPrefill' );
return; // We'll do the actual work when the change event is triggered again
}
var text = editor.cm.getRange( { line: line, ch: subfield.from }, { line: line, ch: subfield.to } );
widget.text = text;
var node = widget.init();
var mark = editor.cm.markText( { line: line, ch: subfield.from }, { line: line, ch: subfield.to }, {
atomic: true,
inclusiveLeft: false,
inclusiveRight: false,
replacedWith: node,
} );
mark.id = id;
mark.widget = widget;
widget.node = node;
widget.mark = mark;
widget.editor = editor;
if ( widget.postCreate ) {
widget.postCreate();
}
widget.nodeChanged();
} );
},
};
return Widget;
} );

View File

@ -0,0 +1,434 @@
/*> Infrastructure */
body {
padding: 0;
}
#loading {
background-color: #FFF;
cursor: wait;
height: 100%;
left: 0;
opacity: .7;
position: fixed;
top: 0;
width: 100%;
z-index: 1000;
}
#loading div {
background : transparent url(../../img/loading.gif) top left no-repeat;
font-size : 175%;
font-weight: bold;
height: 2em;
left: 50%;
margin: -1em 0 0 -2.5em;
padding-left : 50px;
position: absolute;
top: 50%;
width: 15em;
}
#alerts-container {
font-size: 12px;
}
#alerts-container h3 {
font-size: inherit;
}
#alerts-container > ul {
padding: 0;
}
#alerts-container > ul > li {
border-bottom: 1px solid #DDD;
display: block;
padding: 4px 0;
}
#alerts-container > ul > li:first-child {
padding-top: 0;
}
#alerts-container > ul > li:last-child {
border-bottom: none;
padding-bottom: 0;
}
/*> MARC editor */
#editor .CodeMirror {
line-height: 1.2;
}
.cm-tagnumber {
color: #080;
font-weight: bold;
}
.cm-bad-tagnumber {
color: #A20;
font-weight: bold;
}
.cm-indicator {
color: #884;
}
.cm-subfieldcode {
background-color: #F4F4F4;
color: #187848;
border-radius: 3px 8px 8px 3px;
border-right: 2px solid white;
font-weight: bold;
margin-right: -2px;
}
.cm-bad-subfieldcode {
background-color: #FFD9D9;
color: #482828;
border-radius: 3px 8px 8px 3px;
font-weight: bold;
}
.cm-end-space {
background-color: #DDDDBB;
}
#editor .modified-line-gutter {
width: 10px;
}
#editor .modified-line {
background: #F8F8F8;
border-left: 5px solid black;
margin-left: -10px;
padding-left: 5px;
}
#editor .CodeMirror-gutters {
background: transparent;
border-right: none;
}
/*> MARC editor widgets */
#editor .subfield-widget {
color: #538200;
border: solid 2px #538200;
border-radius: 6px;
font-family: inherit;
line-height: 2.75;
margin: 3px 0;
padding: 4px;
}
#editor .subfield-widget select, #editor .subfield-widget input {
height: 1.5em;
vertical-align: middle;
}
#editor .subfield-widget select:focus {
outline: 2px #83A230 solid;
}
#editor .fixed-widget input {
width: 4em;
}
#editor .fixed-widget select {
width: 3em;
}
#editor .fixed-widget .material-select {
width: 4.5em;
margin-right: .5em;
}
#editor .fixed-collapsed {
display: inline-block;
margin: 0 .25em;
text-align: center;
text-decoration: underline;
}
#editor .hidden-widget {
color: #999999;
border: solid 2px #AAAAAA;
line-height: 2;
padding: 2px;
}
.structure-error {
background: #FFEEEE;
font-size: 0.9em;
line-height: 1.5;
margin: .5em;
padding: 0 .5em;
}
.structure-error i {
vertical-align: text-bottom;
}
#statusbar {
background-color: #F4F8F9;
border: solid 2px #b9d8d9;
border-bottom-style: none;
border-radius: 6px 6px 0 0;
height: 18px;
margin-bottom: -32px;
overflow: auto;
padding: 4px;
padding-bottom: 0;
}
#statusbar #status-tag-info, #statusbar #status-subfield-info {
float: left;
overflow: hidden;
padding-right: 2%;
width: 48%;
}
#record-info .label {
float: none;
}
#record-info .label + span {
display: block;
padding-left: 1em;
}
/*> Search */
#advanced-search-ui, #search-results-ui, #macro-ui {
padding: 5px;
width: 90%;
}
.modal-body {
max-height: none;
padding: 0;
}
#quicksearch-overlay {
background: rgba(255, 255, 255, .9);
border: 2px solid #CC8877;
border-radius: 5px;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
color: #664444;
position: relative;
vertical-align: middle;
}
#quicksearch-overlay h3 {
font-size: 1.5em%;
margin: 0;
text-align: center;
padding: 50px 5px;
}
#quicksearch-overlay p {
bottom: 0;
font-size: .8em;
overflow: hidden;
padding: 8px 15px;
position: absolute;
text-align: center;
}
#quicksearch input, #quicksearch a {
font-size: 1.2em;
padding: 3px 0;
width: 96%; /* I have no idea why this is necessary */
}
#show-advanced-search {
display: block;
margin-top: .3em;
}
#advanced-search-fields {
-moz-column-width: 26em;
-webkit-column-width: 26em;
column-width: 26em;
margin: 0;
padding: 0;
}
#advanced-search-fields li {
display: block;
list-style-type: none;
}
#advanced-search-fields label {
display: inline-block;
font-weight: bold;
padding: 1em 1em 1em 0;
width: 10em;
text-align: right;
}
#advanced-search-fields input {
display: inline-block;
margin: 0px auto;
width: 14em;
}
.icon-loading {
display: inline-block;
height: 16px;
width: 16px;
background: transparent url("../../img/spinner-small.gif") top left no-repeat;
padding: -1px;
vertical-align: text-top;
}
/*> Search results */
#search-serversinfo li {
list-style-type: none;
}
#search-serversinfo .search-toggle-server {
margin-right: 5px;
}
#searchresults table {
width: 100%;
}
.sourcecol {
width: 50px;
}
.results-info {
height: 100px;
overflow: auto;
}
.toolscol {
padding: 0;
width: 100px;
}
.toolscol ul {
margin: 0;
padding: 0;
}
#searchresults .toolscol li {
list-style-type: none;
list-style-image: none;
}
.toolscol a {
border-bottom: 1px solid #BCBCBC;
display: block;
padding: 0 1em;
line-height: 24px;
}
.marccol {
font-family: monospace;
height: auto;
white-space: pre-wrap;
}
#searchresults {
position: relative;
}
#search-overlay {
background: white;
bottom: 0;
font-size: 2em;
left: 0;
opacity: .7;
padding: 2em;
position: absolute;
right: 0;
text-align: center;
top: 0;
z-index: 9001;
}
/*> Macros */
#macro-ui .CodeMirror {
width: 100%;
}
#macro-save-message {
color: #666;
font-size: 13px;
float: right;
line-height: 26px;
}
#macro-list > li {
border: 2px solid #F0F0F0;
border-radius: 6px;
display: block;
font-size: 115%;
}
#macro-list > li + li {
margin-top: -2px;
}
#macro-list .active {
background: #EDF4F6;
border-color: none;
}
#macro-list a {
display: block;
padding: 6px;
}
#macro-list a:focus {
outline: none;
}
.macro-info {
background-color: #F4F4F4;
display: none;
margin: 0;
padding: 10px;
text-align: right;
}
.macro-info li {
color: #666;
font-size: 75%;
list-style-type: none;
}
.macro-info .label {
clear: left;
font-weight: bold;
float: left;
}
#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%;
}

View File

@ -418,13 +418,18 @@ dd {
font-weight : normal;
}
div#toolbar {
.btn-toolbar {
background-color : #EDF4F6;
padding: 5px 5px 5px 5px;
border-radius: 5px 5px 0 0;
border: 1px solid #E6F0F2;
}
.btn-toolbar .yui-menu-button button,
.btn-toolbar .yui-button-button button {
line-height : 1.7em;
}
ul.toolbar {
padding-left : 0;
}
@ -2412,8 +2417,8 @@ video {
background-position:-48px -166px;
}
#toolbar .btn,
#toolbar .dropdown-menu {
.btn-toolbar .btn,
.btn-toolbar .dropdown-menu {
font-size: 13px;
}
a.btn:link,

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,158 @@
<div id="editor-widget-templates" style="display:none">
<div id="widget-leader">
Leader:&nbsp;<span title="Record length (autogenerated)">#####</span>
<select name="f5" title="Record status">
<option value="a">a - Increase in encoding level</option>
<option value="c">c - Corrected or revised</option>
<option value="d">d - Deleted</option>
<option value="n">n - New</option>
<option value="p">p - Increase in encoding level from prepublication</option>
</select>
<select name="f6" title="Type of record">
<option value="a">a - Language material</option>
<option value="c">c - Notated music</option>
<option value="d">d - Manuscript notated music</option>
<option value="e">e - Cartographic material</option>
<option value="f">f - Manuscript cartographic material</option>
<option value="g">g - Projected medium</option>
<option value="i">i - Nonmusical sound recording</option>
<option value="j">j - Musical sound recording</option>
<option value="k">k - Two-dimensional nonprojectable graphic</option>
<option value="m">m - Computer file</option>
<option value="o">o - Kit</option>
<option value="p">p - Mixed materials</option>
<option value="r">r - Three-dimensional artifact or naturally occurring object</option>
<option value="t">t - Manuscript language material</option>
</select>
<select name="f7" title="Bibliographic level">
<option value="a">a - Monographic component part</option>
<option value="b">b - Serial component part</option>
<option value="c">c - Collection</option>
<option value="d">d - Subunit</option>
<option value="i">i - Integrating resource</option>
<option value="m">m - Monograph/item</option>
<option value="s">s - Serial</option>
</select>
<select name="f8" title="Type of control">
<option value=" ">_ - No specific type</option>
<option value="a">a - Archival</option>
</select>
<span title="Encoding (forced Unicode)">a</span>
<span title="Indicator/subfield lengths">22</span>
<span title="Data base address (autogenerated)">#####</span>
<select name="f17" title="Encoding level">
<option value=" ">_ - Full level</option>
<option value="1">1 - Full level, material not examined</option>
<option value="2">2 - Less-than-full level, material not examined</option>
<option value="3">3 - Abbreviated level</option>
<option value="4">4 - Core level</option>
<option value="5">5 - Partial (preliminary) level</option>
<option value="7">7 - Minimal level</option>
<option value="8">8 - Prepublication level</option>
<option value="u">u - Unknown</option>
<option value="z">z - Not applicable</option>
</select>
<select name="f18" title="Descriptive cataloging form">
<option value=" ">_ - Non-ISBD</option>
<option value="a">a - AACR 2</option>
<option value="c">c - ISBD punctuation omitted</option>
<option value="i">i - ISBD punctuation included</option>
<option value="u">u - Unknown</option>
</select>
<select name="f19" title="Multipart record resource level">
<option value=" ">_ - Not specified or not applicable</option>
<option value="a">a - Set</option>
<option value="b">b - Part with independent title</option>
<option value="c">c - Part with dependent title</option>
</select>
<span title="Length of directory elements">4500</span>
</div>
</div>
<script>
/**
* Each widget should provide one to three methods:
* init( text ): Returns the DOM node for this widget.
* postCreate( node, mark ): Optional, called once the mark has been created
* and the node shown. Bind event handlers here.
* makeTemplate(): Optional, should return some sane default contents for a
* newly created field/subfield. '<empty>' will be used if this
* method is unset.
*
* Following the Koha convention, control fields are defined as tags with a
* single subfield, '@'.
*/
require( [ 'widget' ], function( Widget ) {
Widget.Register( '000@', {
makeTemplate: function() {
return ' nam a22 7a 4500';
},
init: function() {
var $result = $( '<span class="subfield-widget fixed-widget"></span>' );
return $result[0];
},
postCreate: function() {
// Clear the length and directory start fields; these are unnecessary for MARCXML and will be filled in upon USMARC export
this.setFixed( 0, 5, ' ' );
this.setFixed( 9, 17, 'a22 ' );
this.setFixed( 20, 24, '4500' );
this.insertTemplate( '#widget-leader' );
this.bindFixed( '[name=f5]', 5, 6 );
this.bindFixed( '[name=f6]', 6, 7 );
this.bindFixed( '[name=f7]', 7, 8 );
this.bindFixed( '[name=f8]', 8, 9 );
this.bindFixed( '[name=f17]', 17, 18 );
this.bindFixed( '[name=f18]', 18, 19 );
this.bindFixed( '[name=f19]', 19, 20 );
},
} );
Widget.Register( '005@', {
init: function() {
var $result = $( '<span class="subfield-widget fixed-widget">Updated: </span>' );
return $result[0];
},
postCreate: function( node, mark ) {
var parts = this.text.match( /(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})\.(\d)/ );
if ( parts ) {
var dateVal = new Date(
parseInt( parts[1] ), // Year
parseInt( parts[2] ) - 1, // Month (0-11)
parseInt( parts[3] ), // Day
parseInt( parts[4] ), // Hour
parseInt( parts[5] ), // Minute
parseInt( parts[6] ), // Second
parseInt( parts[7] ) * 100 // Millisecond
);
$( this.node ).append( dateVal.toLocaleString() );
} else {
$( this.node ).append( '<span class="hint">unset</span>' );
}
}
} );
Widget.Register( '008@', {
makeTemplate: function() {
var now = new Date();
return Widget.PadNum( now.getYear() % 100, 2 ) + Widget.PadNum( now.getMonth() + 1, 2 ) + Widget.PadNum( now.getDate(), 2 ) + "b xxu||||| |||| 00| 0 [% DefaultLanguageField008 %] d";
},
init: function() {
var $result = $( '<span class="subfield-widget fixed-widget">Fixed data:<span class="hint widget-loading">Loading...</span></span>' );
return $result[0];
},
postCreate: function( node, mark ) {
this.createFromXML( 'marc21/xml/008' );
}
} );
} );
</script>

View File

@ -7,6 +7,7 @@
[% IF ( circulation ) %]<li class="active">[% ELSE %]<li>[% END %]<a title="Circulation" href="/cgi-bin/koha/admin/preferences.pl?tab=circulation">Circulation</a></li>
[% IF ( enhanced_content ) %]<li class="active">[% ELSE %]<li>[% END %]<a title="Enhanced content settings" href="/cgi-bin/koha/admin/preferences.pl?tab=enhanced_content">Enhanced content</a></li>
[% IF ( i18n_l10n ) %]<li class="active">[% ELSE %]<li>[% END %]<a title="Internationalization and localization" href="/cgi-bin/koha/admin/preferences.pl?tab=i18n_l10n">I18N/L10N</a></li>
[% IF ( labs ) %]<li class="active">[% ELSE %]<li>[% END %]<a title="Experimental features" href="/cgi-bin/koha/admin/preferences.pl?tab=labs">Labs</a></li>
[% IF ( local_use ) %]<li class="active">[% ELSE %]<li>[% END %]<a href="/cgi-bin/koha/admin/systempreferences.pl">Local use</a></li>
[% IF ( logs ) %]<li class="active">[% ELSE %]<li>[% END %]<a title="Transaction logs" href="/cgi-bin/koha/admin/preferences.pl?tab=logs">Logs</a></li>
[% IF ( opac ) %]<li class="active">[% ELSE %]<li>[% END %]<a title="Online Public Access Catalog" href="/cgi-bin/koha/admin/preferences.pl?tab=opac">OPAC</a></li>

View File

@ -0,0 +1,11 @@
Labs:
All:
-
- pref: EnableAdvancedCatalogingEditor
default: 0
choices:
yes: Enable
no: "Don't enable"
- the advanced cataloging editor.
- "<br/> NOTE:"
- This feature is currently experimental, and may have bugs that cause corruption of records. Please help us test it and report any bugs, but do so at your own risk.

View File

@ -105,7 +105,26 @@
redirect("just_save", tab);
return false;
});
});
$( '#switcheditor' ).click( function() {
var breedingid = [% breedingid || "null" %];
if ( !confirm( breedingid ? _("This record cannot be transferred to the advanced editor. Continue?") : _("Any changes will not be saved. Continue?") ) ) return false;
$.cookie( 'catalogue_editor_[% USER_INFO.0.borrowernumber %]', 'advanced', { expires: 365, path: '/' } );
var biblionumber = [% biblionumber || "null" %];
if ( biblionumber ) {
window.location = '/cgi-bin/koha/cataloguing/editor.pl#catalog:' + biblionumber;
} else {
window.location = '/cgi-bin/koha/cataloguing/editor.pl';
}
return false;
} );
});
function redirect(dest){
$("#redirect").attr("value",dest);
@ -462,6 +481,9 @@ function Changefwk(FwkList) {
[% UNLESS (circborrowernumber) %][%# Hide in fast cataloging %]
<div class="btn-group"><a class="btn btn-small" href="#" id="z3950search"><i class="fa fa-search"></i> Z39.50/SRU search</a></div>
[% IF Koha.Preference( 'EnableAdvancedCatalogingEditor' ) == 1 %]
<div class="btn-group"><a href="#" id="switcheditor" class="btn btn-small">Switch to advanced editor</a></div>
[% END %]
[% IF (biblionumber) %]
[% IF ( BiblioDefaultViewmarc ) %]
<div class="btn-group">

View File

@ -1,3 +1,4 @@
[% USE Koha %]
[% INCLUDE 'doc-head-open.inc' %]
<title>Koha &rsaquo; Cataloging</title>
[% INCLUDE 'greybox.inc' %]
@ -22,6 +23,11 @@
e.preventDefault();
MergeItems();
});
$("#useadvanced").click(function(){
$.cookie( 'catalogue_editor_[% USER_INFO.0.borrowernumber %]', 'advanced', { expires: 365, path: '/' } );
return true;
});
});
/* this function open a popup to search on z3950 server. */
@ -70,6 +76,9 @@
[% IF ( CAN_user_editcatalogue_edit_catalogue ) %]
<div id="toolbar" class="btn-toolbar">
[% IF Koha.Preference( 'EnableAdvancedCatalogingEditor' ) == 1 %]
<a id="useadvanced" href="/cgi-bin/koha/cataloguing/editor.pl" class="btn btn-small"><i class="icon-edit"></i> Advanced editor</a>
[% END %]
<div class="btn-group">
<button class="btn btn-small dropdown-toggle" data-toggle="dropdown"><i class="fa fa-plus"></i> New record <span class="caret"></span></button>
<ul class="dropdown-menu">

View File

@ -0,0 +1,234 @@
[% INCLUDE 'doc-head-open.inc' %]
<title>Koha &rsaquo; Cataloging &rsaquo; Editor</title>
[% INCLUDE 'doc-head-close.inc' %]
<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" />
[% END %]
</head>
<body id="cat_addbiblio" class="cat">
<div id="loading">
<div>Loading, please wait...</div>
</div>
[% INCLUDE 'header.inc' %]
<div id="breadcrumbs"><a href="/cgi-bin/koha/mainpage.pl">Home</a> &rsaquo; <a href="/cgi-bin/koha/cataloguing/addbooks.pl">Cataloging</a> &rsaquo; Editor</div>
<div id="doc3" class="yui-t2">
<div id="bd">
<h1 id="title">Cataloging editor</h1>
<div id="yui-main"><div class="yui-b">
<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">
<span class="caret"></span>
</button>
<ul class="dropdown-menu" id="save-dropdown">
</ul>
</div>
<button class="btn btn-small" id="import-records" title="Import an ISO2709 or MARCXML record"><i class="icon-upload"></i> <span>Import record...</span></button>
<button class="btn btn-small" id="open-macros" title="Run and edit macros"><i class="icon-play"></i> <span>Macros...</span></button>
<div class="btn-group">
<button class="btn btn-small dropdown-toggle" data-toggle="dropdown"><i class="icon-cog"></i> Settings <span class="caret"></span></button>
<ul id="prefs-menu" class="dropdown-menu">
<li><a id="switch-editor" href="#">Switch to basic editor</a></li>
<li><a id="set-field-widgets" href="#"></a></li>
<li class="divider"></li>
<li><a class="set-fontSize" style="font-size: .92em" href="#">Small text</a></li>
<li><a class="set-fontSize" style="font-size: 1em" href="#">Normal text</a></li>
<li><a class="set-fontSize" style="font-size: 1.08em" href="#">Large text</a></li>
<li><a class="set-fontSize" style="font-size: 1.18em" href="#">Huge text</a></li>
<li class="divider"></li>
<li><a class="set-font" style="font-family: monospace" href="#">Default font</a></li>
<li><a class="set-font" style="font-family: 'Courier New'" href="#">Courier New</a></li>
<li><a class="set-font" style="font-family: peep" href="#">peep</a></li>
</ul>
</div>
<button class="btn btn-small" id="show-alerts" title="Previous alerts"><i class="icon-info-sign"></i> Alerts <span class="caret"></span></button>
</div>
[%# CodeMirror instance will be inserted here %]
<div id="statusbar">
<div id="status-tag-info">
</div>
<div id="status-subfield-info">
</div>
</div>
</div>
</div></div>
<div class="yui-b" id="sidebar">
<h3>Search</h3>
<form id="quicksearch">
<fieldset class="brief">
<ol>
<li><label for="search-by-keywords">Keywords:</label></li>
<li><input class="search-box" data-qualifier="term" id="search-by-keywords" placeholder="(Ctrl-Alt-K)" /></li>
<li><label for="search-by-author">Author:</label></li>
<li><input class="search-box" data-qualifier="author" id="search-by-author" placeholder="(Ctrl-Alt-A)" /></li>
<li><label for="search-by-isbn">ISBN:</label></li>
<li><input class="search-box" data-qualifier="isbn" id="search-by-isbn" placeholder="(Ctrl-Alt-I)" /></li>
<li><label for="search-by-title">Title:</label></li>
<li><input class="search-box" data-qualifier="title" id="search-by-title" placeholder="(Ctrl-Alt-T)" /></li>
<li><a href="#" id="show-advanced-search" title="Show advanced search (Ctrl-Alt-S)">Advanced &raquo;</a></li>
</fieldset>
</form>
</div>
</div>
</div>
<div id="advanced-search-ui" class="modal hide fade" tabindex="-1" role="dialog" aria-labelledby="advanced-search-title" aria-hidden="true">
<div class="modal-header">
<button type="button" class="closebtn" data-dismiss="modal" aria-hidden="true">×</button>
<h3 id="advanced-search-title">Advanced search</h3>
</div>
<form id="advanced-search" class="modal-body">
<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>
</form>
</div>
<div id="search-results-ui" class="modal hide fade" tabindex="-1" role="dialog" aria-labelledby="search-results-title" aria-hidden="true">
<div class="modal-header">
<button type="button" class="closebtn" data-dismiss="modal" aria-hidden="true">×</button>
<h3 id="search-results-title">Results</h3>
</div>
<div class="modal-body row-fluid">
<div class="span3">
<div id="search-facets">
<ul>
<li>Servers:<ul id="search-serversinfo"></ul></li>
</ul>
</div>
</div>
<div class="span9">
<div id="searchresults">
<div id="search-top-pages">
<div class="pagination pagination-small">
</div>
</div>
<table>
<thead>
<tr></tr>
</thead>
<tbody></tbody>
</table>
<div id="search-bottom-pages">
<div class="pagination pagination-small">
</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>
<div id="macro-ui" class="modal hide fade" tabindex="-1" role="dialog" aria-labelledby="macro-title" aria-hidden="true">
<div class="modal-header">
<button type="button" class="closebtn" data-dismiss="modal" aria-hidden="true">×</button>
<h3 id="macro-title">Macros</h3>
</div>
<div class="modal-body row-fluid">
<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" 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>
</div>
</div>
[% PROCESS 'cateditor-ui.inc' %]
[% INCLUDE 'intranet-bottom.inc' %]

77
svc/cataloguing/framework Executable file
View File

@ -0,0 +1,77 @@
#!/usr/bin/perl
use Modern::Perl '2009';
use CGI;
use C4::Branch;
use C4::ClassSource;
use C4::Context;
use C4::Biblio;
use C4::Service;
use Koha::Database;
my ( $query, $response ) = C4::Service->init( editcatalogue => 'edit_catalogue' );
my $frameworkcode = $query->param( 'frameworkcode' ) // '';
my $tagslib = GetMarcStructure( 1, $frameworkcode );
my @tags;
foreach my $tag ( sort keys %$tagslib ) {
my $taglib = $tagslib->{$tag};
my $taginfo = { map { $_, $taglib->{$_} } grep { length $_ > 1 } keys %$taglib };
$taginfo->{subfields} = [ map { [ $_, $taglib->{$_} ] } grep { length $_ == 1 } sort keys %$taglib ];
push @tags, [ $tag, $taginfo ];
}
my $schema = Koha::Database->new->schema;
my $authorised_values = {};
$authorised_values->{branches} = [];
my $onlymine=C4::Context->preference('IndependentBranches') &&
C4::Context->userenv &&
C4::Context->userenv->{flags} % 2 == 0 &&
C4::Context->userenv->{branch};
my $branches = GetBranches($onlymine);
foreach my $thisbranch ( sort keys %$branches ) {
push @{ $authorised_values->{branches} }, { value => $thisbranch, lib => $branches->{$thisbranch}->{'branchname'} };
}
$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();
my $default_source = C4::Context->preference("DefaultClassificationSource");
foreach my $class_source (sort keys %$class_sources) {
next unless $class_sources->{$class_source}->{'used'} or
($class_source eq $default_source);
push @{ $authorised_values->{cn_source} }, { value => $class_source, lib => $class_sources->{$class_source}->{'description'} };
}
my $branch_limit = C4::Context->userenv ? C4::Context->userenv->{"branch"} : "";
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 );
C4::Service->return_success( $response );

76
svc/cataloguing/metasearch Executable file
View File

@ -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 );