From tomascohen at gmail.com Wed Aug 1 15:35:30 2012 From: tomascohen at gmail.com (Tomas Cohen Arazi) Date: Wed, 1 Aug 2012 10:35:30 -0300 Subject: [Koha-patches] [PATCH] Bug 8485 - Make koha_perl_deps.pl batch friendly Added a -b flag for brief which outputs only the perl library name (Foo::BaR), and added a -r flag for required which filters the list to required=Yes perl libraries. Message-ID: <1343828130-6270-1-git-send-email-tomascohen@gmail.com> From: Mark Tompsett Signed-off-by: Tomas Cohen Arazi --- koha_perl_deps.pl | 60 ++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 46 insertions(+), 14 deletions(-) diff --git a/koha_perl_deps.pl b/koha_perl_deps.pl index 01e17d5..123bcd2 100755 --- a/koha_perl_deps.pl +++ b/koha_perl_deps.pl @@ -18,6 +18,8 @@ my $installed = 0; my $upgrade = 0; my $all = 0; my $color = 0; +my $brief = 0; +my $req = 0; GetOptions( 'h|help|?' => \$help, @@ -25,6 +27,8 @@ GetOptions( 'i|installed' => \$installed, 'u|upgrade' => \$upgrade, 'a|all' => \$all, + 'b|brief' => \$brief, + 'r|required' => \$req, 'c|color' => \$color, ); @@ -39,12 +43,14 @@ push @pm, 'missing_pm' if $missing || $all; push @pm, 'upgrade_pm' if $upgrade || $all; push @pm, 'current_pm' if $installed || $all; -print color 'bold blue' if $color; -print" +if (!$brief) { + print color 'bold blue' if $color; + print" Installed Required Module is Module Name Version Version Required -------------------------------------------------------------------------------------------- "; +} my $count = 0; foreach my $type (@pm) { @@ -60,28 +66,40 @@ foreach my $type (@pm) { my $required = ($_->{$pm}->{'required'}?'Yes':'No'); my $current_version = ($color ? $_->{$pm}->{'cur_ver'} : $type eq 'missing_pm' || $type eq 'upgrade_pm' ? $_->{$pm}->{'cur_ver'}." *" : $_->{$pm}->{'cur_ver'}); + if (!$brief) { + if (($req && $required eq 'Yes') || !$req) { format = @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< @<<<<<<<<<<<<<<<< @<<<<<<<<<<<<<<<< @<<<<< $pm, $current_version, $_->{$pm}->{'min_ver'}, $required . write; + } + } + else { + if (($req && $required eq 'Yes') || !$req) { + print "$pm\n"; + } + } } } } -print color 'bold blue' if $color; -my $footer = " + +if (!$brief) { + print color 'bold blue' if $color; + my $footer = " -------------------------------------------------------------------------------------------- Total modules reported: $count "; -if ($color) { - $footer .= "\n\n"; -} -else { - $footer .= "* Module is missing or requires an upgrade.\n\n"; -} + if ($color) { + $footer .= "\n\n"; + } + else { + $footer .= "* Module is missing or requires an upgrade.\n\n"; + } -print $footer; -print color 'reset' if $color; + print $footer; + print color 'reset' if $color; +} 1; @@ -93,7 +111,12 @@ koha_perl_deps.pl =head1 SYNOPSIS -./koha_perl_deps.pl -m + At least one of -a, -m, -i, or -u flags must specified to not trigger help. + ./koha_perl_deps.pl -m [-b] [-r] [-c] + ./koha_perl_deps.pl -u [-b] [-r] [-c] + ./koha_perl_deps.pl -i [-b] [-r] [-c] + ./koha_perl_deps.pl -a [-b] [-r] [-c] + ./koha_perl_deps.pl [-[h?]] =head1 OPTIONS @@ -113,7 +136,16 @@ lists all perl modules needing to be upgraded relative to Koha =item B<-a|--all> -lists all koha perl dependencies + lists all koha perl dependencies + This is equivalent to '-m -i -u'. + +=item B<-b|--brief> + +lists only the perl dependency name. + +=item B<-r|--required> + +filters list to only required perl dependencies. =item B<-c|--color> -- 1.7.9.5 From jcamins at cpbibliography.com Wed Aug 1 18:27:09 2012 From: jcamins at cpbibliography.com (Jared Camins-Esakov) Date: Wed, 1 Aug 2012 12:27:09 -0400 Subject: [Koha-patches] [PATCH] Bug 8524: Add a non-automatic barcode plugin Message-ID: <1343838429-13146-1-git-send-email-jcamins@cpbibliography.com> In addition to adding a new barcode plugin, this commit begins refactoring the barcode generation code using a new module, C4::Barcodes::ValueBuilder. From the POD: This module is intended as a shim to ease the eventual transition from having all barcode-related code in the value builder plugin barcode.pl file to using C4::Barcodes. Since the shift will require a rather significant amount of refactoring, this module will return value builder-formatted results, at first by merely running the code that was formerly in the barcodes.pl value builder, but later by using C4::Barcodes. --- C4/Barcodes/ValueBuilder.pm | 109 ++++++++++++++++++++++ cataloguing/value_builder/barcode.pl | 53 ++--------- cataloguing/value_builder/barcode_manual.pl | 132 +++++++++++++++++++++++++++ t/Barcodes_ValueBuilder.t | 78 ++++++++++++++++ 4 files changed, 327 insertions(+), 45 deletions(-) create mode 100644 C4/Barcodes/ValueBuilder.pm create mode 100755 cataloguing/value_builder/barcode_manual.pl create mode 100644 t/Barcodes_ValueBuilder.t diff --git a/C4/Barcodes/ValueBuilder.pm b/C4/Barcodes/ValueBuilder.pm new file mode 100644 index 0000000..10cd504 --- /dev/null +++ b/C4/Barcodes/ValueBuilder.pm @@ -0,0 +1,109 @@ +#!/usr/bin/perl +# +# Copyright 2008-2010 Foundations Bible College +# Parts copyright 2012 C & P Bibliography Services +# +# 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. + +package C4::Barcodes::ValueBuilder::incremental; +use C4::Context; +my $DEBUG = 0; + +sub get_barcode { + my ($args) = @_; + my $nextnum; + # not the best, two catalogers could add the same barcode easily this way :/ + my $query = "select max(abs(barcode)) from items"; + my $sth = C4::Context->dbh->prepare($query); + $sth->execute(); + while (my ($count)= $sth->fetchrow_array) { + $nextnum = $count; + } + $nextnum++; + return $nextnum; +} + +1; + +package C4::Barcodes::ValueBuilder::hbyymmincr; +use C4::Context; +my $DEBUG = 0; + +sub get_barcode { + my ($args) = @_; + my $nextnum; + my $year = substr($args->{year}, -2); + my $query = "SELECT MAX(CAST(SUBSTRING(barcode,-4) AS signed)) AS number FROM items WHERE barcode REGEXP ?"; + my $sth = C4::Context->dbh->prepare($query); + $sth->execute("^[-a-zA-Z]{1,}$year"); + while (my ($count)= $sth->fetchrow_array) { + $nextnum = $count if $count; + $nextnum = 0 if $nextnum == 9999; # this sequence only allows for cataloging 10000 books per month + warn "Existing incremental number = $nextnum" if $DEBUG; + } + $nextnum++; + $nextnum = sprintf("%0*d", "4",$nextnum); + $nextnum = $year . $args->{mon} . $nextnum; + warn "New hbyymmincr Barcode = $nextnum" if $DEBUG; + my $scr = " + for (i=0 ; i{loctag}' && document.f.subfield[i].value == '$args->{locsubfield}') { + fnum = i; + } + } + if (\$('#' + id).val() == '' || force) { + \$('#' + id).val(document.f.field_value[fnum].value + '$nextnum'); + } + "; + return $nextnum, $scr; +} + + +package C4::Barcodes::ValueBuilder::annual; +use C4::Context; +my $DEBUG = 0; + +sub get_barcode { + my ($args) = @_; + my $nextnum; + my $query = "select max(cast( substring_index(barcode, '-',-1) as signed)) from items where barcode like ?"; + my $sth=C4::Context->dbh->prepare($query); + $sth->execute("$args->{year}%"); + while (my ($count)= $sth->fetchrow_array) { + warn "Examining Record: $count" if $DEBUG; + $nextnum = $count if $count; + } + $nextnum++; + $nextnum = sprintf("%0*d", "4",$nextnum); + $nextnum = "$args->{year}-$nextnum"; + return $nextnum; +} + +1; + + +=head1 Barcodes::ValueBuilder + +This module is intended as a shim to ease the eventual transition from +having all barcode-related code in the value builder plugin .pl file +to using C4::Barcodes. Since the shift will require a rather significant +amount of refactoring, this module will return value builder-formatted +results, at first by merely running the code that was formerly in the +barcodes.pl value builder, but later by using C4::Barcodes. + +=cut + +1; diff --git a/cataloguing/value_builder/barcode.pl b/cataloguing/value_builder/barcode.pl index c5e1fd3..cb89a32 100755 --- a/cataloguing/value_builder/barcode.pl +++ b/cataloguing/value_builder/barcode.pl @@ -22,6 +22,7 @@ use warnings; no warnings 'redefine'; # otherwise loading up multiple plugins fills the log with subroutine redefine warnings use C4::Context; +require C4::Barcodes::ValueBuilder; require C4::Dates; my $DEBUG = 0; @@ -55,14 +56,14 @@ the 3 scripts are inserted after the in the html code sub plugin_javascript { my ($dbh,$record,$tagslib,$field_number,$tabloop) = @_; my $function_name= "barcode".(int(rand(100000))+1); + my %args; # find today's date - my ($year, $mon, $day) = split('-', C4::Dates->today('iso')); - my ($tag,$subfield) = GetMarcFromKohaField("items.barcode", ''); - my ($loctag,$locsubfield) = GetMarcFromKohaField("items.homebranch", ''); + ($args{year}, $args{mon}, $args{day}) = split('-', C4::Dates->today('iso')); + ($args{tag},$args{subfield}) = GetMarcFromKohaField("items.barcode", ''); + ($args{loctag},$args{locsubfield}) = GetMarcFromKohaField("items.homebranch", ''); my $nextnum; - my $query; my $scr; my $autoBarcodeType = C4::Context->preference("autoBarcode"); warn "Barcode type = $autoBarcodeType" if $DEBUG; @@ -77,51 +78,13 @@ sub plugin_javascript { "); } if ($autoBarcodeType eq 'annual') { - $query = "select max(cast( substring_index(barcode, '-',-1) as signed)) from items where barcode like ?"; - my $sth=$dbh->prepare($query); - $sth->execute("$year%"); - while (my ($count)= $sth->fetchrow_array) { - warn "Examining Record: $count" if $DEBUG; - $nextnum = $count if $count; - } - $nextnum++; - $nextnum = sprintf("%0*d", "4",$nextnum); - $nextnum = "$year-$nextnum"; + ($nextnum, $scr) = C4::Barcodes::ValueBuilder::annual::get_barcode(\%args); } elsif ($autoBarcodeType eq 'incremental') { - # not the best, two catalogers could add the same barcode easily this way :/ - $query = "select max(abs(barcode)) from items"; - my $sth = $dbh->prepare($query); - $sth->execute(); - while (my ($count)= $sth->fetchrow_array) { - $nextnum = $count; - } - $nextnum++; + ($nextnum, $scr) = C4::Barcodes::ValueBuilder::incremental::get_barcode(\%args); } elsif ($autoBarcodeType eq 'hbyymmincr') { # Generates a barcode where hb = home branch Code, yymm = year/month catalogued, incr = incremental number, reset yearly -fbcit - $year = substr($year, -2); - $query = "SELECT MAX(CAST(SUBSTRING(barcode,-4) AS signed)) AS number FROM items WHERE barcode REGEXP ?"; - my $sth = $dbh->prepare($query); - $sth->execute("^[-a-zA-Z]{1,}$year"); - while (my ($count)= $sth->fetchrow_array) { - $nextnum = $count if $count; - $nextnum = 0 if $nextnum == 9999; # this sequence only allows for cataloging 10000 books per month - warn "Existing incremental number = $nextnum" if $DEBUG; - } - $nextnum++; - $nextnum = sprintf("%0*d", "4",$nextnum); - $nextnum = $year . $mon . $nextnum; - warn "New hbyymmincr Barcode = $nextnum" if $DEBUG; - $scr = " - for (i=0 ; i) named ClicXXX + +returns : +* XXX +* a variable containing the 3 scripts. +the 3 scripts are inserted after the in the html code + +=cut + +sub plugin_javascript { + my ($dbh,$record,$tagslib,$field_number,$tabloop) = @_; + my $function_name= "barcode".(int(rand(100000))+1); + my %args; + + $args{dbh} = $dbh; + +# find today's date + ($args{year}, $args{mon}, $args{day}) = split('-', C4::Dates->today('iso')); + ($args{tag},$args{subfield}) = GetMarcFromKohaField("items.barcode", ''); + ($args{loctag},$args{locsubfield}) = GetMarcFromKohaField("items.homebranch", ''); + + my $nextnum; + my $scr; + my $autoBarcodeType = C4::Context->preference("autoBarcode"); + warn "Barcode type = $autoBarcodeType" if $DEBUG; + if ((not $autoBarcodeType) or $autoBarcodeType eq 'OFF') { +# don't return a value unless we have the appropriate syspref set + return ($function_name, + ""); + } + if ($autoBarcodeType eq 'annual') { + ($nextnum, $scr) = C4::Barcodes::ValueBuilder::annual::get_barcode(\%args); + } + elsif ($autoBarcodeType eq 'incremental') { + ($nextnum, $scr) = C4::Barcodes::ValueBuilder::incremental::get_barcode(\%args); + } + elsif ($autoBarcodeType eq 'hbyymmincr') { # Generates a barcode where hb = home branch Code, yymm = year/month catalogued, incr = incremental number, reset yearly -fbcit + ($nextnum, $scr) = C4::Barcodes::ValueBuilder::hbyymmincr::get_barcode(\%args); + } + +# default js body (if not filled by hbyymmincr) + $scr or $scr = < + // + +END_OF_JS + return ($function_name, $js); +} + +=head1 + +plugin: useless here + +=cut + +sub plugin { +# my ($input) = @_; + return ""; +} + +1; diff --git a/t/Barcodes_ValueBuilder.t b/t/Barcodes_ValueBuilder.t new file mode 100644 index 0000000..c3d816e --- /dev/null +++ b/t/Barcodes_ValueBuilder.t @@ -0,0 +1,78 @@ +#!/usr/bin/perl +use strict; +use warnings; +use DBI; +use Test::More tests => 15; +use Test::MockModule; + +BEGIN { + use_ok('C4::Barcodes::ValueBuilder'); +} + + +my $module = new Test::MockModule('C4::Context'); +$module->mock('_new_dbh', sub { + my $dbh = DBI->connect( 'DBI:Mock:', '', '' ) + || die "Cannot create handle: $DBI::errstr\n"; + return $dbh }); + +# Mock data +my $incrementaldata = [ + ['max(abs(barcode))'], + ['33333074344563'], +]; + + +my $dbh = C4::Context->dbh(); + +my %args = ( + year => '2012', + mon => '07', + day => '30', + tag => '952', + subfield => 'p', + loctag => '952', + locsubfield => 'a' +); + +$dbh->{mock_add_resultset} = $incrementaldata; +my ($nextnum, $scr, $history); + +($nextnum, $scr) = C4::Barcodes::ValueBuilder::incremental::get_barcode(\%args); +is($nextnum, 33333074344564, 'incremental barcode'); +ok(length($scr) == 0, 'incremental javascript'); + +# This should run exactly one query so we can test +$history = $dbh->{mock_all_history}; +is(scalar(@{$history}), 1, 'Correct number of statements executed for incremental barcode') ; + +my $hbyymmincrdata = [ + ['number'], + ['890'], +]; + +$dbh->{mock_add_resultset} = $hbyymmincrdata; +$dbh->{mock_clear_history} = 1; +($nextnum, $scr) = C4::Barcodes::ValueBuilder::hbyymmincr::get_barcode(\%args); +is($nextnum, '12070891', 'hbyymmincr barcode'); +ok(length($scr) > 0, 'hbyymmincr javascript'); + +# This should run exactly one query so we can test +$history = $dbh->{mock_all_history}; +is(scalar(@{$history}), 1, 'Correct number of statements executed for hbyymmincr barcode') ; + +my $annualdata = [ + ['max(cast( substring_index(barcode, \'-\',-1) as signed))'], + ['34'], +]; + +$dbh->{mock_add_resultset} = $annualdata; +$dbh->{mock_clear_history} = 1; +($nextnum, $scr) = C4::Barcodes::ValueBuilder::annual::get_barcode(\%args); +is($nextnum, '2012-0035', 'annual barcode'); +ok(length($scr) == 0, 'annual javascript'); + +# This should run exactly one query so we can test +$history = $dbh->{mock_all_history}; +is(scalar(@{$history}), 1, 'Correct number of statements executed for hbyymmincr barcode') ; + -- 1.7.2.5 From tomascohen at gmail.com Wed Aug 1 20:27:40 2012 From: tomascohen at gmail.com (Tomas Cohen Arazi) Date: Wed, 1 Aug 2012 15:27:40 -0300 Subject: [Koha-patches] [PATCH] Bug 8520: fix authority display in staff client Message-ID: <1343845660-13503-1-git-send-email-tomascohen@gmail.com> From: Jared Camins-Esakov Signed-off-by: Tomas Cohen Arazi --- .../en/includes/authorities-search-results.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/authorities-search-results.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/authorities-search-results.inc index 92314b1..3defe48 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/includes/authorities-search-results.inc +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/authorities-search-results.inc @@ -30,7 +30,7 @@ [% IF ( summary.summary ) %][% summary.summary %]:[% END %] [% UNLESS ( summary.summaryonly ) %] [% FOREACH authorize IN summary.authorized %] - [% authorize %] + [% authorize.heading %] [% END %] [% IF ( marcflavour == 'UNIMARC' ) %] [% FOREACH note IN summary.notes %] -- 1.7.9.5 From fridolyn.somers at biblibre.com Fri Aug 3 17:32:41 2012 From: fridolyn.somers at biblibre.com (Fridolyn SOMERS) Date: Fri, 3 Aug 2012 17:32:41 +0200 Subject: [Koha-patches] [PATCH 2/2] Bug 7455: Authority subfields are cloned in the wrong field (follow-up 1) Message-ID: <1344007961-19369-1-git-send-email-fridolyn.somers@biblibre.com> --- authorities/authorities.pl | 21 ++++++++++++--------- cataloguing/addbiblio.pl | 5 +++-- koha-tmpl/intranet-tmpl/prog/en/js/cataloging.js | 4 ++++ 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/authorities/authorities.pl b/authorities/authorities.pl index 23653f6..de79238 100755 --- a/authorities/authorities.pl +++ b/authorities/authorities.pl @@ -152,9 +152,15 @@ sub create_input { $value =~ s/DD/$day/g; } my $dbh = C4::Context->dbh; + + # map '@' as "subfield" label for fixed fields + # to something that's allowed in a div id. + my $id_subfield = $subfield; + $id_subfield = "00" if $id_subfield eq "@"; + my %subfield_data = ( tag => $tag, - subfield => $subfield, + subfield => $id_subfield, marc_lib => substr( $tagslib->{$tag}->{$subfield}->{lib}, 0, 22 ), marc_lib_plain => $tagslib->{$tag}->{$subfield}->{lib}, tag_mandatory => $tagslib->{$tag}->{mandatory}, @@ -162,14 +168,11 @@ sub create_input { repeatable => $tagslib->{$tag}->{$subfield}->{repeatable}, kohafield => $tagslib->{$tag}->{$subfield}->{kohafield}, index => $index_tag, - id => "tag_".$tag."_subfield_".$subfield."_".$index_tag."_".$index_subfield, + id => "tag_".$tag."_subfield_".$id_subfield."_".$index_tag."_".$index_subfield, value => $value, + random => CreateKey(), + fixedfield => $tag < 10 ? 1 : 0, ); - if($subfield eq '@'){ - $subfield_data{id} = "tag_".$tag."_subfield_00_".$index_tag."_".$index_subfield; - } else { - $subfield_data{id} = "tag_".$tag."_subfield_".$subfield."_".$index_tag."_".$index_subfield; - } if(exists $mandatory_z3950->{$tag.$subfield}){ $subfield_data{z3950_mandatory} = $mandatory_z3950->{$tag.$subfield}; @@ -441,7 +444,7 @@ sub build_tabs { repeatable => $tagslib->{$tag}->{repeatable}, mandatory => $tagslib->{$tag}->{mandatory}, subfield_loop => \@subfields_data, - fixedfield => ($tag < 10)?(1):(0), + fixedfield => $tag < 10 ? 1 : 0, random => CreateKey, ); if ($tag >= 10){ # no indicator for theses tag @@ -481,7 +484,7 @@ sub build_tabs { indicator2 => $indicator2, subfield_loop => \@subfields_data, tagfirstsubfield => $subfields_data[0], - fixedfield => ($tag < 10)?(1):(0) + fixedfield => $tag < 10 ? 1 : 0, ); push @loop_data, \%tag_data ; diff --git a/cataloguing/addbiblio.pl b/cataloguing/addbiblio.pl index 3db1a65..a509f31 100755 --- a/cataloguing/addbiblio.pl +++ b/cataloguing/addbiblio.pl @@ -327,6 +327,7 @@ sub create_input { value => $value, maxlength => $tagslib->{$tag}->{$subfield}->{maxlength}, random => CreateKey(), + fixedfield => $tag < 10 ? 1 : 0, ); if(exists $mandatory_z3950->{$tag.$subfield}){ @@ -648,7 +649,7 @@ sub build_tabs { repeatable => $tagslib->{$tag}->{repeatable}, mandatory => $tagslib->{$tag}->{mandatory}, subfield_loop => \@subfields_data, - fixedfield => $tag < 10?1:0, + fixedfield => $tag < 10 ? 1 : 0, random => CreateKey, ); if ($tag >= 10){ # no indicator for 00x tags @@ -697,7 +698,7 @@ sub build_tabs { indicator2 => $indicator2, subfield_loop => \@subfields_data, tagfirstsubfield => $subfields_data[0], - fixedfield => $tag < 10?1:0, + fixedfield => $tag < 10 ? 1 : 0, ); push @loop_data, \%tag_data ; diff --git a/koha-tmpl/intranet-tmpl/prog/en/js/cataloging.js b/koha-tmpl/intranet-tmpl/prog/en/js/cataloging.js index 29ab1eb..96cdea0 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/js/cataloging.js +++ b/koha-tmpl/intranet-tmpl/prog/en/js/cataloging.js @@ -211,6 +211,10 @@ function CloneField(index, hideMarc, advancedMARCEditor) { } else { // it's a indicator div if(divs[i].getAttribute('id').match(/^div_indicator/)){ + + // setting a new id for the indicator div + divs[i].setAttribute('id',divs[i].getAttribute('id')+new_key); + var inputs = divs[i].getElementsByTagName('input'); inputs[0].setAttribute('id',inputs[0].getAttribute('id')+new_key); inputs[1].setAttribute('id',inputs[1].getAttribute('id')+new_key); -- 1.7.9.5 From jcamins at cpbibliography.com Sat Aug 4 23:18:03 2012 From: jcamins at cpbibliography.com (Jared Camins-Esakov) Date: Sat, 4 Aug 2012 17:18:03 -0400 Subject: [Koha-patches] =?utf-8?q?=5BPATCH=5D_Bug_8523=3A_Display_auth_hie?= =?utf-8?q?rarchies_w/all_marcflavours?= Message-ID: <1344115083-7454-1-git-send-email-jcamins@cpbibliography.com> This commit adds support for displaying authority hierarchies for all flavours of MARC, not just UNIMARC. Display now uses the jQuery jstree plugin, selected with the help of Owen Leonard, resulting in a much faster experience for users. Be aware that the jstree file uses tabs rather than 4-space indentation, which I left as-is so as to make it easier to integrate upstream releases in the future. To test: 1) Enable the AuthDisplayHierarchy syspref 2) Create authority records with a hierarchy of see also fields (in MARC21/NORMARC, you'll be using 5xx fields for this, with a subfield $w=g for broader terms and subfield $w=h for narrower terms) 3) View the authorities in the OPAC, noting the hierarchical view at the top of the page. --- C4/AuthoritiesMarc.pm | 208 +- authorities/detail.pl | 23 +- installer/data/mysql/sysprefs.sql | 1 + installer/data/mysql/updatedatabase.pl | 7 + .../intranet-tmpl/prog/en/includes/authorities.inc | 19 + .../prog/en/lib/jquery/plugins/jquery.jstree.js | 4551 ++++++++++++++++++++ .../en/lib/jquery/plugins/themes/classic/d.gif | Bin 0 -> 3003 bytes .../en/lib/jquery/plugins/themes/classic/d.png | Bin 0 -> 7535 bytes .../jquery/plugins/themes/classic/dot_for_ie.gif | Bin 0 -> 43 bytes .../en/lib/jquery/plugins/themes/classic/style.css | 77 + .../lib/jquery/plugins/themes/classic/throbber.gif | Bin 0 -> 1849 bytes .../en/modules/admin/preferences/authorities.pref | 7 + .../prog/en/modules/authorities/detail.tt | 64 +- .../prog/en/includes/opac-authorities.inc | 53 + .../prog/en/lib/jquery/plugins/jquery.jstree.js | 4551 ++++++++++++++++++++ .../en/lib/jquery/plugins/themes/classic/d.gif | Bin 0 -> 3003 bytes .../en/lib/jquery/plugins/themes/classic/d.png | Bin 0 -> 7535 bytes .../jquery/plugins/themes/classic/dot_for_ie.gif | Bin 0 -> 43 bytes .../en/lib/jquery/plugins/themes/classic/style.css | 77 + .../lib/jquery/plugins/themes/classic/throbber.gif | Bin 0 -> 1849 bytes .../prog/en/modules/opac-auth-MARCdetail.tt | 64 +- .../opac-tmpl/prog/en/modules/opac-auth-detail.tt | 97 +- opac/opac-authoritiesdetail.pl | 30 +- t/AuthoritiesMarc.t | 86 +- 24 files changed, 9613 insertions(+), 302 deletions(-) create mode 100644 koha-tmpl/intranet-tmpl/prog/en/includes/authorities.inc create mode 100644 koha-tmpl/intranet-tmpl/prog/en/lib/jquery/plugins/jquery.jstree.js create mode 100644 koha-tmpl/intranet-tmpl/prog/en/lib/jquery/plugins/themes/classic/d.gif create mode 100644 koha-tmpl/intranet-tmpl/prog/en/lib/jquery/plugins/themes/classic/d.png create mode 100644 koha-tmpl/intranet-tmpl/prog/en/lib/jquery/plugins/themes/classic/dot_for_ie.gif create mode 100644 koha-tmpl/intranet-tmpl/prog/en/lib/jquery/plugins/themes/classic/style.css create mode 100644 koha-tmpl/intranet-tmpl/prog/en/lib/jquery/plugins/themes/classic/throbber.gif create mode 100644 koha-tmpl/opac-tmpl/prog/en/includes/opac-authorities.inc create mode 100644 koha-tmpl/opac-tmpl/prog/en/lib/jquery/plugins/jquery.jstree.js create mode 100644 koha-tmpl/opac-tmpl/prog/en/lib/jquery/plugins/themes/classic/d.gif create mode 100644 koha-tmpl/opac-tmpl/prog/en/lib/jquery/plugins/themes/classic/d.png create mode 100644 koha-tmpl/opac-tmpl/prog/en/lib/jquery/plugins/themes/classic/dot_for_ie.gif create mode 100644 koha-tmpl/opac-tmpl/prog/en/lib/jquery/plugins/themes/classic/style.css create mode 100644 koha-tmpl/opac-tmpl/prog/en/lib/jquery/plugins/themes/classic/throbber.gif diff --git a/C4/AuthoritiesMarc.pm b/C4/AuthoritiesMarc.pm index 21ebd94..6d0ac24 100644 --- a/C4/AuthoritiesMarc.pm +++ b/C4/AuthoritiesMarc.pm @@ -52,8 +52,9 @@ BEGIN { &SearchAuthorities &BuildSummary - &BuildUnimarcHierarchies - &BuildUnimarcHierarchy + &BuildAuthHierarchies + &BuildAuthHierarchy + &GenerateHierarchy &merge &FindDuplicateAuthority @@ -1142,9 +1143,9 @@ sub BuildSummary { return \%summary; } -=head2 BuildUnimarcHierarchies +=head2 BuildAuthHierarchies - $text= &BuildUnimarcHierarchies( $authid, $force) + $text= &BuildAuthHierarchies( $authid, $force) return text containing trees for hierarchies for them to be stored in auth_header @@ -1154,54 +1155,59 @@ Example of text: =cut -sub BuildUnimarcHierarchies{ - my $authid = shift @_; +sub BuildAuthHierarchies{ + my $authid = shift @_; # warn "authid : $authid"; - my $force = shift @_; - my @globalresult; - my $dbh=C4::Context->dbh; - my $hierarchies; - my $data = GetHeaderAuthority($authid); - if ($data->{'authtrees'} and not $force){ - return $data->{'authtrees'}; + my $force = shift @_; + my @globalresult; + my $dbh=C4::Context->dbh; + my $hierarchies; + my $data = GetHeaderAuthority($authid); + if ($data->{'authtrees'} and not $force){ + return $data->{'authtrees'}; # } elsif ($data->{'authtrees'}){ # $hierarchies=$data->{'authtrees'}; - } else { - my $record = GetAuthority($authid); - my $found; - return unless $record; - foreach my $field ($record->field('5..')){ - if ($field->subfield('5') && $field->subfield('5') eq 'g'){ - my $subfauthid=_get_authid_subfield($field); - next if ($subfauthid eq $authid); - my $parentrecord = GetAuthority($subfauthid); - my $localresult=$hierarchies; - my $trees; - $trees = BuildUnimarcHierarchies($subfauthid); - my @trees; - if ($trees=~/;/){ - @trees = split(/;/,$trees); - } else { - push @trees, $trees; - } - foreach (@trees){ - $_.= ",$authid"; + } else { + my $record = GetAuthority($authid); + my $found; + return unless $record; + foreach my $field ($record->field('5..')){ + my $broader = 0; + $broader = 1 if ( + (C4::Context->preference('marcflavour') eq 'UNIMARC' && $field->subfield('5') && $field->subfield('5') eq 'g') || + (C4::Context->preference('marcflavour') eq 'MARC21' && $field->subfield('w') && substr($field->subfield('w'), 0, 1) eq 'g')); + if ($broader) { + my $subfauthid=_get_authid_subfield($field) || ''; + next if ($subfauthid eq $authid); + my $parentrecord = GetAuthority($subfauthid); + next unless $parentrecord; + my $localresult=$hierarchies; + my $trees; + $trees = BuildAuthHierarchies($subfauthid); + my @trees; + if ($trees=~/;/){ + @trees = split(/;/,$trees); + } else { + push @trees, $trees; + } + foreach (@trees){ + $_.= ",$authid"; + } + @globalresult = (@globalresult, at trees); + $found=1; + } + $hierarchies=join(";", at globalresult); } - @globalresult = (@globalresult, at trees); - $found=1; - } - $hierarchies=join(";", at globalresult); +#Unless there is no ancestor, I am alone. + $hierarchies="$authid" unless ($hierarchies); } - #Unless there is no ancestor, I am alone. - $hierarchies="$authid" unless ($hierarchies); - } - AddAuthorityTrees($authid,$hierarchies); - return $hierarchies; + AddAuthorityTrees($authid,$hierarchies); + return $hierarchies; } -=head2 BuildUnimarcHierarchy +=head2 BuildAuthHierarchy - $ref= &BuildUnimarcHierarchy( $record, $class,$authid) + $ref= &BuildAuthHierarchy( $record, $class,$authid) return a hashref in order to display hierarchy for record and final Authid $authid @@ -1212,42 +1218,90 @@ return a hashref in order to display hierarchy for record and final Authid $auth "current_value" "value" -"ifparents" -"ifchildren" -Those two latest ones should disappear soon. +=cut + +sub BuildAuthHierarchy{ + my $record = shift @_; + my $class = shift @_; + my $authid_constructed = shift @_; + return undef unless ($record && $record->field('001')); + my $authid=$record->field('001')->data(); + my %cell; + my $parents=""; my $children=""; + my (@loopparents, at loopchildren); + my $marcflavour = C4::Context->preference('marcflavour'); + my $relationshipsf = $marcflavour eq 'UNIMARC' ? '5' : 'w'; + foreach my $field ($record->field('5..')){ + my $subfauthid=_get_authid_subfield($field); + if ($subfauthid && $field->subfield($relationshipsf) && $field->subfield('a')){ + my $relationship = substr($field->subfield($relationshipsf), 0, 1); + if ($relationship eq 'h'){ + push @loopchildren, { "authid"=>$subfauthid,"value"=>$field->subfield('a')}; + } + elsif ($relationship eq 'g'){ + push @loopparents, { "authid"=>$subfauthid,"value"=>$field->subfield('a')}; + } +# brothers could get in there with an else + } + } + $cell{"parents"}=\@loopparents; + $cell{"children"}=\@loopchildren; + $cell{"class"}=$class; + $cell{"authid"}=$authid; + $cell{"current_value"} =1 if $authid eq $authid_constructed; + $cell{"value"}=C4::Context->preference('marcflavour') eq 'UNIMARC' ? $record->subfield('2..',"a") : $record->subfield('1..', 'a'); + return \%cell; +} + +=head2 BuildAuthHierarchyBranch + + $branch = &BuildAuthHierarchyBranch( $tree, $authid[, $cnt]) + +Return a data structure representing an authority hierarchy +given a list of authorities representing a single branch in +an authority hierarchy tree. $authid is the current node in +the tree (which may or may not be somewhere in the middle). +$cnt represents the level of the upper-most item, and is only +used when BuildAuthHierarchyBranch is called recursively (i.e., +don't ever pass in anything but zero to it). =cut -sub BuildUnimarcHierarchy{ - my $record = shift @_; - my $class = shift @_; - my $authid_constructed = shift @_; - return undef unless ($record); - my $authid=$record->field('001')->data(); - my %cell; - my $parents=""; my $children=""; - my (@loopparents, at loopchildren); - foreach my $field ($record->field('5..')){ - my $subfauthid=_get_authid_subfield($field); - if ($subfauthid && $field->subfield('5') && $field->subfield('a')){ - if ($field->subfield('5') eq 'h'){ - push @loopchildren, { "childauthid"=>$field->subfield('3'),"childvalue"=>$field->subfield('a')}; - } - elsif ($field->subfield('5') eq 'g'){ - push @loopparents, { "parentauthid"=>$field->subfield('3'),"parentvalue"=>$field->subfield('a')}; - } - # brothers could get in there with an else - } - } - $cell{"ifparents"}=1 if (scalar(@loopparents)>0); - $cell{"ifchildren"}=1 if (scalar(@loopchildren)>0); - $cell{"loopparents"}=\@loopparents if (scalar(@loopparents)>0); - $cell{"loopchildren"}=\@loopchildren if (scalar(@loopchildren)>0); - $cell{"class"}=$class; - $cell{"loopauthid"}=$authid; - $cell{"current_value"} =1 if $authid eq $authid_constructed; - $cell{"value"}=$record->subfield('2..',"a"); - return \%cell; +sub BuildAuthHierarchyBranch { + my ($tree, $authid, $cnt) = @_; + $cnt |= 0; + my $elementdata = GetAuthority(shift @$tree); + my $branch = BuildAuthHierarchy($elementdata,"child".$cnt, $authid); + if (scalar @$tree > 0) { + my $nextBranch = BuildAuthHierarchyBranch($tree, $authid, ++$cnt); + my $nextAuthid = $nextBranch->{authid}; + push @{$branch->{children}}, $nextBranch unless grep {$_->{authid} == $nextAuthid} @{$branch->{children}}; + } + return $branch; +} + +=head2 GenerateHierarchy + + $hierarchy = &GenerateHierarchy($authid); + +Return an arrayref holding one or more "trees" representing +authority hierarchies. + +=cut + +sub GenerateHierarchy { + my ($authid) = @_; + my $trees = BuildAuthHierarchies($authid); + my @trees = split /;/,$trees ; + push @trees,$trees unless (@trees); + my @loophierarchies; + foreach my $tree (@trees){ + my @tree=split /,/,$tree; + push @tree, $tree unless (@tree); + my $branch = BuildAuthHierarchyBranch(\@tree, $authid); + push @loophierarchies, [ $branch ]; + } + return \@loophierarchies; } sub _get_authid_subfield{ diff --git a/authorities/detail.pl b/authorities/detail.pl index 458746c..5287e06 100755 --- a/authorities/detail.pl +++ b/authorities/detail.pl @@ -199,27 +199,8 @@ if (not defined $record) { } if (C4::Context->preference("AuthDisplayHierarchy")){ - my $trees=BuildUnimarcHierarchies($authid); - my @trees = split /;/,$trees ; - push @trees,$trees unless (@trees); - my @loophierarchies; - foreach my $tree (@trees){ - my @tree=split /,/,$tree; - push @tree,$tree unless (@tree); - my $cnt=0; - my @loophierarchy; - foreach my $element (@tree){ - my $elementdata = GetAuthority($element); - $record= $elementdata if ($authid==$element); - push @loophierarchy, BuildUnimarcHierarchy($elementdata,"child".$cnt, $authid); - $cnt++; - } - push @loophierarchies, { 'loopelement' =>\@loophierarchy}; - } - $template->param( - 'displayhierarchy' =>C4::Context->preference("AuthDisplayHierarchy"), - 'loophierarchies' =>\@loophierarchies, - ); + $template->{VARS}->{'displayhierarchy'} = C4::Context->preference("AuthDisplayHierarchy"); + $template->{VARS}->{'loophierarchies'} = GenerateHierarchy($authid); } my $count = CountUsage($authid); diff --git a/installer/data/mysql/sysprefs.sql b/installer/data/mysql/sysprefs.sql index f5a18b2..0978d74 100644 --- a/installer/data/mysql/sysprefs.sql +++ b/installer/data/mysql/sysprefs.sql @@ -374,3 +374,4 @@ INSERT INTO systempreferences (variable,value,options,explanation,type) VALUES ( INSERT INTO systempreferences (variable,value,explanation,type) VALUES('EnableBorrowerFiles','0','If enabled, allows librarians to upload and attach arbitrary files to a borrower record.','YesNo'); INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES('UpdateTotalIssuesOnCirc','0','Whether to update the totalissues field in the biblio on each circ.',NULL,'YesNo'); INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES('IntranetSlipPrinterJS','','Use this JavaScript for printing slips. Define at least function printThenClose(). For use e.g. with Firefox PlugIn jsPrintSetup, see http://jsprintsetup.mozdev.org/','','Free'); +INSERT IGNORE INTO systempreferences (variable,value,explanation,options,type) VALUES('AuthDisplayHierarchy','0','Display authority hierarchies','','YesNo'); diff --git a/installer/data/mysql/updatedatabase.pl b/installer/data/mysql/updatedatabase.pl index ed0c674..fc40197 100755 --- a/installer/data/mysql/updatedatabase.pl +++ b/installer/data/mysql/updatedatabase.pl @@ -5635,6 +5635,13 @@ if(C4::Context->preference("Version") < TransformToNum($DBversion) ) { SetVersion($DBversion); } +$DBversion ="3.09.00.XXX"; +if ( C4::Context->preference("Version") < TransformToNum($DBversion) ) { + $dbh->do("INSERT IGNORE INTO systempreferences (variable,value,explanation,options,type) VALUES('AuthDisplayHierarchy','0','Display authority hierarchies','','YesNo')"); + print "Upgrade to $DBversion done (Add system preference AuthDisplayHierarchy))\n"; + SetVersion($DBversion); +} + =head1 FUNCTIONS =head2 TableExists($table) diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/authorities.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/authorities.inc new file mode 100644 index 0000000..5bf67f4 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/authorities.inc @@ -0,0 +1,19 @@ +[% BLOCK showhierarchy %] + [% FOREACH tree IN trees %] +
    + [% FOREACH node IN tree %] +
  • + [% IF ( node.current_value ) %] + [% node.value %] + [% ELSE %] + [% node.value %] + [% END %] + [% IF ( node.children.size > 0 ) %] + [% PROCESS showhierarchy trees = [ node.children ] %] + [% END %] +
  • + [% END %] +
+ [% END %] +[% END %] + diff --git a/koha-tmpl/intranet-tmpl/prog/en/lib/jquery/plugins/jquery.jstree.js b/koha-tmpl/intranet-tmpl/prog/en/lib/jquery/plugins/jquery.jstree.js new file mode 100644 index 0000000..c94e61f --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/en/lib/jquery/plugins/jquery.jstree.js @@ -0,0 +1,4551 @@ +/* + * jsTree 1.0-rc3 + * http://jstree.com/ + * + * Copyright (c) 2010 Ivan Bozhanov (vakata.com) + * + * Licensed same as jquery - under the terms of either the MIT License or the GPL Version 2 License + * http://www.opensource.org/licenses/mit-license.php + * http://www.gnu.org/licenses/gpl.html + * + * $Date: 2011-02-09 01:17:14 +0200 (??, 09 ???? 2011) $ + * $Revision: 236 $ + */ + +/*jslint browser: true, onevar: true, undef: true, bitwise: true, strict: true */ +/*global window : false, clearInterval: false, clearTimeout: false, document: false, setInterval: false, setTimeout: false, jQuery: false, navigator: false, XSLTProcessor: false, DOMParser: false, XMLSerializer: false*/ + +"use strict"; + +// top wrapper to prevent multiple inclusion (is this OK?) +(function () { if(jQuery && jQuery.jstree) { return; } + var is_ie6 = false, is_ie7 = false, is_ff2 = false; + +/* + * jsTree core + */ +(function ($) { + // Common functions not related to jsTree + // decided to move them to a `vakata` "namespace" + $.vakata = {}; + // CSS related functions + $.vakata.css = { + get_css : function(rule_name, delete_flag, sheet) { + rule_name = rule_name.toLowerCase(); + var css_rules = sheet.cssRules || sheet.rules, + j = 0; + do { + if(css_rules.length && j > css_rules.length + 5) { return false; } + if(css_rules[j].selectorText && css_rules[j].selectorText.toLowerCase() == rule_name) { + if(delete_flag === true) { + if(sheet.removeRule) { sheet.removeRule(j); } + if(sheet.deleteRule) { sheet.deleteRule(j); } + return true; + } + else { return css_rules[j]; } + } + } + while (css_rules[++j]); + return false; + }, + add_css : function(rule_name, sheet) { + if($.jstree.css.get_css(rule_name, false, sheet)) { return false; } + if(sheet.insertRule) { sheet.insertRule(rule_name + ' { }', 0); } else { sheet.addRule(rule_name, null, 0); } + return $.vakata.css.get_css(rule_name); + }, + remove_css : function(rule_name, sheet) { + return $.vakata.css.get_css(rule_name, true, sheet); + }, + add_sheet : function(opts) { + var tmp = false, is_new = true; + if(opts.str) { + if(opts.title) { tmp = $("style[id='" + opts.title + "-stylesheet']")[0]; } + if(tmp) { is_new = false; } + else { + tmp = document.createElement("style"); + tmp.setAttribute('type',"text/css"); + if(opts.title) { tmp.setAttribute("id", opts.title + "-stylesheet"); } + } + if(tmp.styleSheet) { + if(is_new) { + document.getElementsByTagName("head")[0].appendChild(tmp); + tmp.styleSheet.cssText = opts.str; + } + else { + tmp.styleSheet.cssText = tmp.styleSheet.cssText + " " + opts.str; + } + } + else { + tmp.appendChild(document.createTextNode(opts.str)); + document.getElementsByTagName("head")[0].appendChild(tmp); + } + return tmp.sheet || tmp.styleSheet; + } + if(opts.url) { + if(document.createStyleSheet) { + try { tmp = document.createStyleSheet(opts.url); } catch (e) { } + } + else { + tmp = document.createElement('link'); + tmp.rel = 'stylesheet'; + tmp.type = 'text/css'; + tmp.media = "all"; + tmp.href = opts.url; + document.getElementsByTagName("head")[0].appendChild(tmp); + return tmp.styleSheet; + } + } + } + }; + + // private variables + var instances = [], // instance array (used by $.jstree.reference/create/focused) + focused_instance = -1, // the index in the instance array of the currently focused instance + plugins = {}, // list of included plugins + prepared_move = {}; // for the move_node function + + // jQuery plugin wrapper (thanks to jquery UI widget function) + $.fn.jstree = function (settings) { + var isMethodCall = (typeof settings == 'string'), // is this a method call like $().jstree("open_node") + args = Array.prototype.slice.call(arguments, 1), + returnValue = this; + + // if a method call execute the method on all selected instances + if(isMethodCall) { + if(settings.substring(0, 1) == '_') { return returnValue; } + this.each(function() { + var instance = instances[$.data(this, "jstree_instance_id")], + methodValue = (instance && $.isFunction(instance[settings])) ? instance[settings].apply(instance, args) : instance; + if(typeof methodValue !== "undefined" && (settings.indexOf("is_") === 0 || (methodValue !== true && methodValue !== false))) { returnValue = methodValue; return false; } + }); + } + else { + this.each(function() { + // extend settings and allow for multiple hashes and $.data + var instance_id = $.data(this, "jstree_instance_id"), + a = [], + b = settings ? $.extend({}, true, settings) : {}, + c = $(this), + s = false, + t = []; + a = a.concat(args); + if(c.data("jstree")) { a.push(c.data("jstree")); } + b = a.length ? $.extend.apply(null, [true, b].concat(a)) : b; + + // if an instance already exists, destroy it first + if(typeof instance_id !== "undefined" && instances[instance_id]) { instances[instance_id].destroy(); } + // push a new empty object to the instances array + instance_id = parseInt(instances.push({}),10) - 1; + // store the jstree instance id to the container element + $.data(this, "jstree_instance_id", instance_id); + // clean up all plugins + b.plugins = $.isArray(b.plugins) ? b.plugins : $.jstree.defaults.plugins.slice(); + b.plugins.unshift("core"); + // only unique plugins + b.plugins = b.plugins.sort().join(",,").replace(/(,|^)([^,]+)(,,\2)+(,|$)/g,"$1$2$4").replace(/,,+/g,",").replace(/,$/,"").split(","); + + // extend defaults with passed data + s = $.extend(true, {}, $.jstree.defaults, b); + s.plugins = b.plugins; + $.each(plugins, function (i, val) { + if($.inArray(i, s.plugins) === -1) { s[i] = null; delete s[i]; } + else { t.push(i); } + }); + s.plugins = t; + + // push the new object to the instances array (at the same time set the default classes to the container) and init + instances[instance_id] = new $.jstree._instance(instance_id, $(this).addClass("jstree jstree-" + instance_id), s); + // init all activated plugins for this instance + $.each(instances[instance_id]._get_settings().plugins, function (i, val) { instances[instance_id].data[val] = {}; }); + $.each(instances[instance_id]._get_settings().plugins, function (i, val) { if(plugins[val]) { plugins[val].__init.apply(instances[instance_id]); } }); + // initialize the instance + setTimeout(function() { if(instances[instance_id]) { instances[instance_id].init(); } }, 0); + }); + } + // return the jquery selection (or if it was a method call that returned a value - the returned value) + return returnValue; + }; + // object to store exposed functions and objects + $.jstree = { + defaults : { + plugins : [] + }, + _focused : function () { return instances[focused_instance] || null; }, + _reference : function (needle) { + // get by instance id + if(instances[needle]) { return instances[needle]; } + // get by DOM (if still no luck - return null + var o = $(needle); + if(!o.length && typeof needle === "string") { o = $("#" + needle); } + if(!o.length) { return null; } + return instances[o.closest(".jstree").data("jstree_instance_id")] || null; + }, + _instance : function (index, container, settings) { + // for plugins to store data in + this.data = { core : {} }; + this.get_settings = function () { return $.extend(true, {}, settings); }; + this._get_settings = function () { return settings; }; + this.get_index = function () { return index; }; + this.get_container = function () { return container; }; + this.get_container_ul = function () { return container.children("ul:eq(0)"); }; + this._set_settings = function (s) { + settings = $.extend(true, {}, settings, s); + }; + }, + _fn : { }, + plugin : function (pname, pdata) { + pdata = $.extend({}, { + __init : $.noop, + __destroy : $.noop, + _fn : {}, + defaults : false + }, pdata); + plugins[pname] = pdata; + + $.jstree.defaults[pname] = pdata.defaults; + $.each(pdata._fn, function (i, val) { + val.plugin = pname; + val.old = $.jstree._fn[i]; + $.jstree._fn[i] = function () { + var rslt, + func = val, + args = Array.prototype.slice.call(arguments), + evnt = new $.Event("before.jstree"), + rlbk = false; + + if(this.data.core.locked === true && i !== "unlock" && i !== "is_locked") { return; } + + // Check if function belongs to the included plugins of this instance + do { + if(func && func.plugin && $.inArray(func.plugin, this._get_settings().plugins) !== -1) { break; } + func = func.old; + } while(func); + if(!func) { return; } + + // context and function to trigger events, then finally call the function + if(i.indexOf("_") === 0) { + rslt = func.apply(this, args); + } + else { + rslt = this.get_container().triggerHandler(evnt, { "func" : i, "inst" : this, "args" : args, "plugin" : func.plugin }); + if(rslt === false) { return; } + if(typeof rslt !== "undefined") { args = rslt; } + + rslt = func.apply( + $.extend({}, this, { + __callback : function (data) { + this.get_container().triggerHandler( i + '.jstree', { "inst" : this, "args" : args, "rslt" : data, "rlbk" : rlbk }); + }, + __rollback : function () { + rlbk = this.get_rollback(); + return rlbk; + }, + __call_old : function (replace_arguments) { + return func.old.apply(this, (replace_arguments ? Array.prototype.slice.call(arguments, 1) : args ) ); + } + }), args); + } + + // return the result + return rslt; + }; + $.jstree._fn[i].old = val.old; + $.jstree._fn[i].plugin = pname; + }); + }, + rollback : function (rb) { + if(rb) { + if(!$.isArray(rb)) { rb = [ rb ]; } + $.each(rb, function (i, val) { + instances[val.i].set_rollback(val.h, val.d); + }); + } + } + }; + // set the prototype for all instances + $.jstree._fn = $.jstree._instance.prototype = {}; + + // load the css when DOM is ready + $(function() { + // code is copied from jQuery ($.browser is deprecated + there is a bug in IE) + var u = navigator.userAgent.toLowerCase(), + v = (u.match( /.+?(?:rv|it|ra|ie)[\/: ]([\d.]+)/ ) || [0,'0'])[1], + css_string = '' + + '.jstree ul, .jstree li { display:block; margin:0 0 0 0; padding:0 0 0 0; list-style-type:none; } ' + + '.jstree li { display:block; min-height:18px; line-height:18px; white-space:nowrap; margin-left:18px; min-width:18px; } ' + + '.jstree-rtl li { margin-left:0; margin-right:18px; } ' + + '.jstree > ul > li { margin-left:0px; } ' + + '.jstree-rtl > ul > li { margin-right:0px; } ' + + '.jstree ins { display:inline-block; text-decoration:none; width:18px; height:18px; margin:0 0 0 0; padding:0; } ' + + '.jstree a { display:inline-block; line-height:16px; height:16px; color:black; white-space:nowrap; text-decoration:none; padding:1px 2px; margin:0; } ' + + '.jstree a:focus { outline: none; } ' + + '.jstree a > ins { height:16px; width:16px; } ' + + '.jstree a > .jstree-icon { margin-right:3px; } ' + + '.jstree-rtl a > .jstree-icon { margin-left:3px; margin-right:0; } ' + + 'li.jstree-open > ul { display:block; } ' + + 'li.jstree-closed > ul { display:none; } '; + // Correct IE 6 (does not support the > CSS selector) + if(/msie/.test(u) && parseInt(v, 10) == 6) { + is_ie6 = true; + + // fix image flicker and lack of caching + try { + document.execCommand("BackgroundImageCache", false, true); + } catch (err) { } + + css_string += '' + + '.jstree li { height:18px; margin-left:0; margin-right:0; } ' + + '.jstree li li { margin-left:18px; } ' + + '.jstree-rtl li li { margin-left:0px; margin-right:18px; } ' + + 'li.jstree-open ul { display:block; } ' + + 'li.jstree-closed ul { display:none !important; } ' + + '.jstree li a { display:inline; border-width:0 !important; padding:0px 2px !important; } ' + + '.jstree li a ins { height:16px; width:16px; margin-right:3px; } ' + + '.jstree-rtl li a ins { margin-right:0px; margin-left:3px; } '; + } + // Correct IE 7 (shifts anchor nodes onhover) + if(/msie/.test(u) && parseInt(v, 10) == 7) { + is_ie7 = true; + css_string += '.jstree li a { border-width:0 !important; padding:0px 2px !important; } '; + } + // correct ff2 lack of display:inline-block + if(!/compatible/.test(u) && /mozilla/.test(u) && parseFloat(v, 10) < 1.9) { + is_ff2 = true; + css_string += '' + + '.jstree ins { display:-moz-inline-box; } ' + + '.jstree li { line-height:12px; } ' + // WHY?? + '.jstree a { display:-moz-inline-box; } ' + + '.jstree .jstree-no-icons .jstree-checkbox { display:-moz-inline-stack !important; } '; + /* this shouldn't be here as it is theme specific */ + } + // the default stylesheet + $.vakata.css.add_sheet({ str : css_string, title : "jstree" }); + }); + + // core functions (open, close, create, update, delete) + $.jstree.plugin("core", { + __init : function () { + this.data.core.locked = false; + this.data.core.to_open = this.get_settings().core.initially_open; + this.data.core.to_load = this.get_settings().core.initially_load; + }, + defaults : { + html_titles : false, + animation : 500, + initially_open : [], + initially_load : [], + open_parents : true, + notify_plugins : true, + rtl : false, + load_open : false, + strings : { + loading : "Loading ...", + new_node : "New node", + multiple_selection : "Multiple selection" + } + }, + _fn : { + init : function () { + this.set_focus(); + if(this._get_settings().core.rtl) { + this.get_container().addClass("jstree-rtl").css("direction", "rtl"); + } + this.get_container().html(""); + this.data.core.li_height = this.get_container_ul().find("li.jstree-closed, li.jstree-leaf").eq(0).height() || 18; + + this.get_container() + .delegate("li > ins", "click.jstree", $.proxy(function (event) { + var trgt = $(event.target); + // if(trgt.is("ins") && event.pageY - trgt.offset().top < this.data.core.li_height) { this.toggle_node(trgt); } + this.toggle_node(trgt); + }, this)) + .bind("mousedown.jstree", $.proxy(function () { + this.set_focus(); // This used to be setTimeout(set_focus,0) - why? + }, this)) + .bind("dblclick.jstree", function (event) { + var sel; + if(document.selection && document.selection.empty) { document.selection.empty(); } + else { + if(window.getSelection) { + sel = window.getSelection(); + try { + sel.removeAllRanges(); + sel.collapse(); + } catch (err) { } + } + } + }); + if(this._get_settings().core.notify_plugins) { + this.get_container() + .bind("load_node.jstree", $.proxy(function (e, data) { + var o = this._get_node(data.rslt.obj), + t = this; + if(o === -1) { o = this.get_container_ul(); } + if(!o.length) { return; } + o.find("li").each(function () { + var th = $(this); + if(th.data("jstree")) { + $.each(th.data("jstree"), function (plugin, values) { + if(t.data[plugin] && $.isFunction(t["_" + plugin + "_notify"])) { + t["_" + plugin + "_notify"].call(t, th, values); + } + }); + } + }); + }, this)); + } + if(this._get_settings().core.load_open) { + this.get_container() + .bind("load_node.jstree", $.proxy(function (e, data) { + var o = this._get_node(data.rslt.obj), + t = this; + if(o === -1) { o = this.get_container_ul(); } + if(!o.length) { return; } + o.find("li.jstree-open:not(:has(ul))").each(function () { + t.load_node(this, $.noop, $.noop); + }); + }, this)); + } + this.__callback(); + this.load_node(-1, function () { this.loaded(); this.reload_nodes(); }); + }, + destroy : function () { + var i, + n = this.get_index(), + s = this._get_settings(), + _this = this; + + $.each(s.plugins, function (i, val) { + try { plugins[val].__destroy.apply(_this); } catch(err) { } + }); + this.__callback(); + // set focus to another instance if this one is focused + if(this.is_focused()) { + for(i in instances) { + if(instances.hasOwnProperty(i) && i != n) { + instances[i].set_focus(); + break; + } + } + } + // if no other instance found + if(n === focused_instance) { focused_instance = -1; } + // remove all traces of jstree in the DOM (only the ones set using jstree*) and cleans all events + this.get_container() + .unbind(".jstree") + .undelegate(".jstree") + .removeData("jstree_instance_id") + .find("[class^='jstree']") + .andSelf() + .attr("class", function () { return this.className.replace(/jstree[^ ]*|$/ig,''); }); + $(document) + .unbind(".jstree-" + n) + .undelegate(".jstree-" + n); + // remove the actual data + instances[n] = null; + delete instances[n]; + }, + + _core_notify : function (n, data) { + if(data.opened) { + this.open_node(n, false, true); + } + }, + + lock : function () { + this.data.core.locked = true; + this.get_container().children("ul").addClass("jstree-locked").css("opacity","0.7"); + this.__callback({}); + }, + unlock : function () { + this.data.core.locked = false; + this.get_container().children("ul").removeClass("jstree-locked").css("opacity","1"); + this.__callback({}); + }, + is_locked : function () { return this.data.core.locked; }, + save_opened : function () { + var _this = this; + this.data.core.to_open = []; + this.get_container_ul().find("li.jstree-open").each(function () { + if(this.id) { _this.data.core.to_open.push("#" + this.id.toString().replace(/^#/,"").replace(/\\\//g,"/").replace(/\//g,"\\\/").replace(/\\\./g,".").replace(/\./g,"\\.").replace(/\:/g,"\\:")); } + }); + this.__callback(_this.data.core.to_open); + }, + save_loaded : function () { }, + reload_nodes : function (is_callback) { + var _this = this, + done = true, + current = [], + remaining = []; + if(!is_callback) { + this.data.core.reopen = false; + this.data.core.refreshing = true; + this.data.core.to_open = $.map($.makeArray(this.data.core.to_open), function (n) { return "#" + n.toString().replace(/^#/,"").replace(/\\\//g,"/").replace(/\//g,"\\\/").replace(/\\\./g,".").replace(/\./g,"\\.").replace(/\:/g,"\\:"); }); + this.data.core.to_load = $.map($.makeArray(this.data.core.to_load), function (n) { return "#" + n.toString().replace(/^#/,"").replace(/\\\//g,"/").replace(/\//g,"\\\/").replace(/\\\./g,".").replace(/\./g,"\\.").replace(/\:/g,"\\:"); }); + if(this.data.core.to_open.length) { + this.data.core.to_load = this.data.core.to_load.concat(this.data.core.to_open); + } + } + if(this.data.core.to_load.length) { + $.each(this.data.core.to_load, function (i, val) { + if(val == "#") { return true; } + if($(val).length) { current.push(val); } + else { remaining.push(val); } + }); + if(current.length) { + this.data.core.to_load = remaining; + $.each(current, function (i, val) { + if(!_this._is_loaded(val)) { + _this.load_node(val, function () { _this.reload_nodes(true); }, function () { _this.reload_nodes(true); }); + done = false; + } + }); + } + } + if(this.data.core.to_open.length) { + $.each(this.data.core.to_open, function (i, val) { + _this.open_node(val, false, true); + }); + } + if(done) { + // TODO: find a more elegant approach to syncronizing returning requests + if(this.data.core.reopen) { clearTimeout(this.data.core.reopen); } + this.data.core.reopen = setTimeout(function () { _this.__callback({}, _this); }, 50); + this.data.core.refreshing = false; + this.reopen(); + } + }, + reopen : function () { + var _this = this; + if(this.data.core.to_open.length) { + $.each(this.data.core.to_open, function (i, val) { + _this.open_node(val, false, true); + }); + } + this.__callback({}); + }, + refresh : function (obj) { + var _this = this; + this.save_opened(); + if(!obj) { obj = -1; } + obj = this._get_node(obj); + if(!obj) { obj = -1; } + if(obj !== -1) { obj.children("UL").remove(); } + else { this.get_container_ul().empty(); } + this.load_node(obj, function () { _this.__callback({ "obj" : obj}); _this.reload_nodes(); }); + }, + // Dummy function to fire after the first load (so that there is a jstree.loaded event) + loaded : function () { + this.__callback(); + }, + // deal with focus + set_focus : function () { + if(this.is_focused()) { return; } + var f = $.jstree._focused(); + if(f) { f.unset_focus(); } + + this.get_container().addClass("jstree-focused"); + focused_instance = this.get_index(); + this.__callback(); + }, + is_focused : function () { + return focused_instance == this.get_index(); + }, + unset_focus : function () { + if(this.is_focused()) { + this.get_container().removeClass("jstree-focused"); + focused_instance = -1; + } + this.__callback(); + }, + + // traverse + _get_node : function (obj) { + var $obj = $(obj, this.get_container()); + if($obj.is(".jstree") || obj == -1) { return -1; } + $obj = $obj.closest("li", this.get_container()); + return $obj.length ? $obj : false; + }, + _get_next : function (obj, strict) { + obj = this._get_node(obj); + if(obj === -1) { return this.get_container().find("> ul > li:first-child"); } + if(!obj.length) { return false; } + if(strict) { return (obj.nextAll("li").size() > 0) ? obj.nextAll("li:eq(0)") : false; } + + if(obj.hasClass("jstree-open")) { return obj.find("li:eq(0)"); } + else if(obj.nextAll("li").size() > 0) { return obj.nextAll("li:eq(0)"); } + else { return obj.parentsUntil(".jstree","li").next("li").eq(0); } + }, + _get_prev : function (obj, strict) { + obj = this._get_node(obj); + if(obj === -1) { return this.get_container().find("> ul > li:last-child"); } + if(!obj.length) { return false; } + if(strict) { return (obj.prevAll("li").length > 0) ? obj.prevAll("li:eq(0)") : false; } + + if(obj.prev("li").length) { + obj = obj.prev("li").eq(0); + while(obj.hasClass("jstree-open")) { obj = obj.children("ul:eq(0)").children("li:last"); } + return obj; + } + else { var o = obj.parentsUntil(".jstree","li:eq(0)"); return o.length ? o : false; } + }, + _get_parent : function (obj) { + obj = this._get_node(obj); + if(obj == -1 || !obj.length) { return false; } + var o = obj.parentsUntil(".jstree", "li:eq(0)"); + return o.length ? o : -1; + }, + _get_children : function (obj) { + obj = this._get_node(obj); + if(obj === -1) { return this.get_container().children("ul:eq(0)").children("li"); } + if(!obj.length) { return false; } + return obj.children("ul:eq(0)").children("li"); + }, + get_path : function (obj, id_mode) { + var p = [], + _this = this; + obj = this._get_node(obj); + if(obj === -1 || !obj || !obj.length) { return false; } + obj.parentsUntil(".jstree", "li").each(function () { + p.push( id_mode ? this.id : _this.get_text(this) ); + }); + p.reverse(); + p.push( id_mode ? obj.attr("id") : this.get_text(obj) ); + return p; + }, + + // string functions + _get_string : function (key) { + return this._get_settings().core.strings[key] || key; + }, + + is_open : function (obj) { obj = this._get_node(obj); return obj && obj !== -1 && obj.hasClass("jstree-open"); }, + is_closed : function (obj) { obj = this._get_node(obj); return obj && obj !== -1 && obj.hasClass("jstree-closed"); }, + is_leaf : function (obj) { obj = this._get_node(obj); return obj && obj !== -1 && obj.hasClass("jstree-leaf"); }, + correct_state : function (obj) { + obj = this._get_node(obj); + if(!obj || obj === -1) { return false; } + obj.removeClass("jstree-closed jstree-open").addClass("jstree-leaf").children("ul").remove(); + this.__callback({ "obj" : obj }); + }, + // open/close + open_node : function (obj, callback, skip_animation) { + obj = this._get_node(obj); + if(!obj.length) { return false; } + if(!obj.hasClass("jstree-closed")) { if(callback) { callback.call(); } return false; } + var s = skip_animation || is_ie6 ? 0 : this._get_settings().core.animation, + t = this; + if(!this._is_loaded(obj)) { + obj.children("a").addClass("jstree-loading"); + this.load_node(obj, function () { t.open_node(obj, callback, skip_animation); }, callback); + } + else { + if(this._get_settings().core.open_parents) { + obj.parentsUntil(".jstree",".jstree-closed").each(function () { + t.open_node(this, false, true); + }); + } + if(s) { obj.children("ul").css("display","none"); } + obj.removeClass("jstree-closed").addClass("jstree-open").children("a").removeClass("jstree-loading"); + if(s) { obj.children("ul").stop(true, true).slideDown(s, function () { this.style.display = ""; t.after_open(obj); }); } + else { t.after_open(obj); } + this.__callback({ "obj" : obj }); + if(callback) { callback.call(); } + } + }, + after_open : function (obj) { this.__callback({ "obj" : obj }); }, + close_node : function (obj, skip_animation) { + obj = this._get_node(obj); + var s = skip_animation || is_ie6 ? 0 : this._get_settings().core.animation, + t = this; + if(!obj.length || !obj.hasClass("jstree-open")) { return false; } + if(s) { obj.children("ul").attr("style","display:block !important"); } + obj.removeClass("jstree-open").addClass("jstree-closed"); + if(s) { obj.children("ul").stop(true, true).slideUp(s, function () { this.style.display = ""; t.after_close(obj); }); } + else { t.after_close(obj); } + this.__callback({ "obj" : obj }); + }, + after_close : function (obj) { this.__callback({ "obj" : obj }); }, + toggle_node : function (obj) { + obj = this._get_node(obj); + if(obj.hasClass("jstree-closed")) { return this.open_node(obj); } + if(obj.hasClass("jstree-open")) { return this.close_node(obj); } + }, + open_all : function (obj, do_animation, original_obj) { + obj = obj ? this._get_node(obj) : -1; + if(!obj || obj === -1) { obj = this.get_container_ul(); } + if(original_obj) { + obj = obj.find("li.jstree-closed"); + } + else { + original_obj = obj; + if(obj.is(".jstree-closed")) { obj = obj.find("li.jstree-closed").andSelf(); } + else { obj = obj.find("li.jstree-closed"); } + } + var _this = this; + obj.each(function () { + var __this = this; + if(!_this._is_loaded(this)) { _this.open_node(this, function() { _this.open_all(__this, do_animation, original_obj); }, !do_animation); } + else { _this.open_node(this, false, !do_animation); } + }); + // so that callback is fired AFTER all nodes are open + if(original_obj.find('li.jstree-closed').length === 0) { this.__callback({ "obj" : original_obj }); } + }, + close_all : function (obj, do_animation) { + var _this = this; + obj = obj ? this._get_node(obj) : this.get_container(); + if(!obj || obj === -1) { obj = this.get_container_ul(); } + obj.find("li.jstree-open").andSelf().each(function () { _this.close_node(this, !do_animation); }); + this.__callback({ "obj" : obj }); + }, + clean_node : function (obj) { + obj = obj && obj != -1 ? $(obj) : this.get_container_ul(); + obj = obj.is("li") ? obj.find("li").andSelf() : obj.find("li"); + obj.removeClass("jstree-last") + .filter("li:last-child").addClass("jstree-last").end() + .filter(":has(li)") + .not(".jstree-open").removeClass("jstree-leaf").addClass("jstree-closed"); + obj.not(".jstree-open, .jstree-closed").addClass("jstree-leaf").children("ul").remove(); + this.__callback({ "obj" : obj }); + }, + // rollback + get_rollback : function () { + this.__callback(); + return { i : this.get_index(), h : this.get_container().children("ul").clone(true), d : this.data }; + }, + set_rollback : function (html, data) { + this.get_container().empty().append(html); + this.data = data; + this.__callback(); + }, + // Dummy functions to be overwritten by any datastore plugin included + load_node : function (obj, s_call, e_call) { this.__callback({ "obj" : obj }); }, + _is_loaded : function (obj) { return true; }, + + // Basic operations: create + create_node : function (obj, position, js, callback, is_loaded) { + obj = this._get_node(obj); + position = typeof position === "undefined" ? "last" : position; + var d = $("
  • "), + s = this._get_settings().core, + tmp; + + if(obj !== -1 && !obj.length) { return false; } + if(!is_loaded && !this._is_loaded(obj)) { this.load_node(obj, function () { this.create_node(obj, position, js, callback, true); }); return false; } + + this.__rollback(); + + if(typeof js === "string") { js = { "data" : js }; } + if(!js) { js = {}; } + if(js.attr) { d.attr(js.attr); } + if(js.metadata) { d.data(js.metadata); } + if(js.state) { d.addClass("jstree-" + js.state); } + if(!js.data) { js.data = this._get_string("new_node"); } + if(!$.isArray(js.data)) { tmp = js.data; js.data = []; js.data.push(tmp); } + $.each(js.data, function (i, m) { + tmp = $(""); + if($.isFunction(m)) { m = m.call(this, js); } + if(typeof m == "string") { tmp.attr('href','#')[ s.html_titles ? "html" : "text" ](m); } + else { + if(!m.attr) { m.attr = {}; } + if(!m.attr.href) { m.attr.href = '#'; } + tmp.attr(m.attr)[ s.html_titles ? "html" : "text" ](m.title); + if(m.language) { tmp.addClass(m.language); } + } + tmp.prepend(" "); + if(!m.icon && js.icon) { m.icon = js.icon; } + if(m.icon) { + if(m.icon.indexOf("/") === -1) { tmp.children("ins").addClass(m.icon); } + else { tmp.children("ins").css("background","url('" + m.icon + "') center center no-repeat"); } + } + d.append(tmp); + }); + d.prepend(" "); + if(obj === -1) { + obj = this.get_container(); + if(position === "before") { position = "first"; } + if(position === "after") { position = "last"; } + } + switch(position) { + case "before": obj.before(d); tmp = this._get_parent(obj); break; + case "after" : obj.after(d); tmp = this._get_parent(obj); break; + case "inside": + case "first" : + if(!obj.children("ul").length) { obj.append("