3 # Copyright Pat Eyler 2003
4 # Copyright Biblibre 2006
5 # Parts Copyright Liblime 2008
6 # Parts Copyright Chris Nighswonger 2010
8 # This file is part of Koha.
10 # Koha is free software; you can redistribute it and/or modify it
11 # under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 3 of the License, or
13 # (at your option) any later version.
15 # Koha is distributed in the hope that it will be useful, but
16 # WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 # GNU General Public License for more details.
20 # You should have received a copy of the GNU General Public License
21 # along with Koha; if not, see <http://www.gnu.org/licenses>.
26 use DateTime::TimeZone;
27 use File::Slurp qw( read_file );
28 use IPC::Cmd qw(can_run);
29 use List::MoreUtils qw( any );
30 use Module::Load::Conditional qw( can_load );
31 use Config qw( %Config );
32 use Search::Elasticsearch;
33 use Try::Tiny qw( catch try );
37 use C4::Output qw( output_html_with_http_headers );
38 use C4::Auth qw( get_template_and_user get_user_subpermissions );
41 use C4::Installer::PerlModules;
44 use Koha::DateUtils qw( dt_from_string output_pref );
45 use Koha::Acquisition::Currencies;
46 use Koha::Authorities;
47 use Koha::BackgroundJob;
48 use Koha::BiblioFrameworks;
51 use Koha::Patron::Categories;
54 use Koha::Config::SysPrefs;
55 use Koha::Illrequest::Config;
56 use Koha::SearchEngine::Elasticsearch;
58 use Koha::Filter::MARC::ViewPolicy;
60 use C4::Members::Statistics;
63 my ( $template, $loggedinuser, $cookie ) = get_template_and_user(
65 template_name => "about.tt",
68 flagsrequired => { catalogue => 1 },
72 my $config_timezone = C4::Context->config('timezone') // '';
73 my $config_invalid = !DateTime::TimeZone->is_valid_name( $config_timezone );
74 my $env_timezone = $ENV{TZ} // '';
75 my $env_invalid = !DateTime::TimeZone->is_valid_name( $env_timezone );
76 my $actual_bad_tz_fallback = 0;
78 if ( $config_timezone ne '' &&
81 $actual_bad_tz_fallback = 1;
83 elsif ( $config_timezone eq '' &&
84 $env_timezone ne '' &&
86 # No config, but bad ENV{TZ}
87 $actual_bad_tz_fallback = 1;
91 actual => C4::Context->tz->name,
92 actual_bad_tz_fallback => $actual_bad_tz_fallback,
93 config => $config_timezone,
94 config_invalid => $config_invalid,
95 environment => $env_timezone,
96 environment_invalid => $env_invalid
100 my $log4perl_config = C4::Context->config("log4perl_conf");
102 if ( ! $log4perl_config ) {
103 push @log4perl_errors, 'missing_config_entry'
106 my @lines = read_file($log4perl_config) or push @log4perl_errors, 'cannot_read_config_file';
107 for my $line ( @lines ) {
108 next unless $line =~ m|log4perl.appender.\w+.filename=(.*)|;
109 push @log4perl_errors, 'logfile_not_writable' unless -w $1;
112 eval {Koha::Logger->get};
113 push @log4perl_errors, 'cannot_init_module' and warn $@ if $@;
114 $template->param( log4perl_errors => @log4perl_errors );
118 time_zone => $time_zone,
119 current_date_and_time => output_pref({ dt => dt_from_string(), dateformat => 'iso' })
124 $perl_path .= $Config{_exe} unless $perl_path =~ m/$Config{_exe}$/i;
127 my $zebraVersion = `zebraidx -V`;
129 # Check running PSGI env
130 if ( C4::Context->psgi_env ) {
133 psgi_server => ($ENV{ PLACK_ENV }) ? "Plack ($ENV{PLACK_ENV})" :
134 ($ENV{ MOD_PERL }) ? "mod_perl ($ENV{MOD_PERL})" :
139 # Memcached configuration
140 my $memcached_servers = $ENV{MEMCACHED_SERVERS} || C4::Context->config('memcached_servers');
141 my $memcached_namespace = $ENV{MEMCACHED_NAMESPACE} || C4::Context->config('memcached_namespace') // 'koha';
143 my $cache = Koha::Caches->get_instance;
144 my $effective_caching_method = ref($cache->cache);
145 # Memcached may have been running when plack has been initialized but could have been stopped since
146 # FIXME What are the consequences of that??
147 my $is_memcached_still_active = $cache->set_in_cache('test_for_about_page', "just a simple value");
149 my $where_is_memcached_config = 'nowhere';
150 if ( $ENV{MEMCACHED_SERVERS} and C4::Context->config('memcached_servers') ) {
151 $where_is_memcached_config = 'both';
152 } elsif ( $ENV{MEMCACHED_SERVERS} and not C4::Context->config('memcached_servers') ) {
153 $where_is_memcached_config = 'ENV_only';
154 } elsif ( C4::Context->config('memcached_servers') ) {
155 $where_is_memcached_config = 'config_only';
159 effective_caching_method => $effective_caching_method,
160 memcached_servers => $memcached_servers,
161 memcached_namespace => $memcached_namespace,
162 is_memcached_still_active => $is_memcached_still_active,
163 where_is_memcached_config => $where_is_memcached_config,
164 memcached_running => Koha::Caches->get_instance->memcached_cache,
167 # Additional system information for warnings
169 my $warnStatisticsFieldsError;
170 my $prefStatisticsFields = C4::Context->preference('StatisticsFields');
171 if ($prefStatisticsFields) {
172 $warnStatisticsFieldsError = $prefStatisticsFields
173 unless ( $prefStatisticsFields eq C4::Members::Statistics->get_fields() );
176 my $prefAutoCreateAuthorities = C4::Context->preference('AutoCreateAuthorities');
177 my $prefRequireChoosingExistingAuthority = C4::Context->preference('RequireChoosingExistingAuthority');
178 my $warnPrefRequireChoosingExistingAuthority = ( !$prefAutoCreateAuthorities && ( !$prefRequireChoosingExistingAuthority) );
180 my $prefEasyAnalyticalRecords = C4::Context->preference('EasyAnalyticalRecords');
181 my $prefUseControlNumber = C4::Context->preference('UseControlNumber');
182 my $warnPrefEasyAnalyticalRecords = ( $prefEasyAnalyticalRecords && $prefUseControlNumber );
184 my $AnonymousPatron = C4::Context->preference('AnonymousPatron');
185 my $warnPrefAnonymousPatronOPACPrivacy = (
186 C4::Context->preference('OPACPrivacy')
187 and not $AnonymousPatron
189 my $warnPrefAnonymousPatronAnonSuggestions = (
190 C4::Context->preference('AnonSuggestions')
191 and not $AnonymousPatron
194 my $anonymous_patron = Koha::Patrons->find( $AnonymousPatron );
195 my $warnPrefAnonymousPatronAnonSuggestions_PatronDoesNotExist = ( $AnonymousPatron && C4::Context->preference('AnonSuggestions') && not $anonymous_patron );
197 my $warnPrefAnonymousPatronOPACPrivacy_PatronDoesNotExist = ( not $anonymous_patron and Koha::Patrons->search({ privacy => 2 })->count );
199 my $warnPrefKohaAdminEmailAddress = !Koha::Email->is_valid(C4::Context->preference('KohaAdminEmailAddress'));
201 my $c = Koha::Items->filter_by_visible_in_opac->count;
202 my @warnings = C4::Context->dbh->selectrow_array('SHOW WARNINGS');
203 my $warnPrefOpacHiddenItems = $warnings[2];
205 my $invalid_yesno = Koha::Config::SysPrefs->search(
208 value => { -or => { 'is' => undef, -not_in => [ "1", "0" ] } }
211 $template->param( invalid_yesno => $invalid_yesno );
213 my $errZebraConnection = C4::Context->Zconn("biblioserver",0)->errcode();
215 my $warnIsRootUser = (! $loggedinuser);
217 my $warnNoActiveCurrency = (! defined Koha::Acquisition::Currencies->get_active);
219 my @xml_config_warnings;
221 if ( C4::Context->config('zebra_bib_index_mode')
222 and C4::Context->config('zebra_bib_index_mode') eq 'grs1' )
224 push @xml_config_warnings, { error => 'zebra_bib_index_mode_is_grs1' };
227 if ( C4::Context->config('zebra_auth_index_mode')
228 and C4::Context->config('zebra_auth_index_mode') eq 'grs1' )
230 push @xml_config_warnings, { error => 'zebra_auth_index_mode_is_grs1' };
233 my $authorityserver = C4::Context->zebraconfig('authorityserver');
234 if( ( C4::Context->config('zebra_auth_index_mode')
235 and C4::Context->config('zebra_auth_index_mode') eq 'dom' )
236 && ( $authorityserver->{config} !~ /zebra-authorities-dom.cfg/ ) )
238 push @xml_config_warnings, {
239 error => 'zebra_auth_index_mode_mismatch_warn'
243 if ( ! defined C4::Context->config('log4perl_conf') ) {
244 push @xml_config_warnings, {
245 error => 'log4perl_entry_missing'
249 if ( ! defined C4::Context->config('lockdir') ) {
250 push @xml_config_warnings, {
251 error => 'lockdir_entry_missing'
255 unless ( -w C4::Context->config('lockdir') ) {
256 push @xml_config_warnings, {
257 error => 'lockdir_not_writable',
258 lockdir => C4::Context->config('lockdir')
263 if ( ! defined C4::Context->config('upload_path') ) {
264 if ( Koha::Config::SysPrefs->find('OPACBaseURL')->value ) {
265 # OPACBaseURL seems to be set
266 push @xml_config_warnings, {
267 error => 'uploadpath_entry_missing'
270 push @xml_config_warnings, {
271 error => 'uploadpath_and_opacbaseurl_entry_missing'
276 if ( ! C4::Context->config('tmp_path') ) {
277 my $temporary_directory = C4::Context::temporary_directory;
278 push @xml_config_warnings, {
279 error => 'tmp_path_missing',
280 effective_tmp_dir => $temporary_directory,
284 my $encryption_key = C4::Context->config('encryption_key');
285 if ( !$encryption_key || $encryption_key eq '__ENCRYPTION_KEY__') {
286 push @xml_config_warnings, { error => 'encryption_key_missing' };
289 # Test Zebra facets configuration
290 if ( !defined C4::Context->config('use_zebra_facets') ) {
291 push @xml_config_warnings, { error => 'use_zebra_facets_entry_missing' };
295 if ( C4::Context->preference('ILLModule') ) {
296 my $warnILLConfiguration = 0;
297 my $ill_config_from_file = C4::Context->config("interlibrary_loans");
298 my $ill_config = Koha::Illrequest::Config->new;
300 my $available_ill_backends =
301 ( scalar @{ $ill_config->available_backends } > 0 );
304 if ( !$available_ill_backends ) {
305 $template->param( no_ill_backends => 1 );
306 $warnILLConfiguration = 1;
309 # Check ILLPartnerCode sys pref
310 if ( !Koha::Patron::Categories->find( C4::Context->preference('ILLPartnerCode') ) ) {
311 $template->param( ill_partner_code_doesnt_exist => C4::Context->preference('ILLPartnerCode') );
312 $warnILLConfiguration = 1;
313 } elsif ( !Koha::Patrons->search( { categorycode => C4::Context->preference('ILLPartnerCode') } )->count ) {
314 $template->param( ill_partner_code_no_patrons => C4::Context->preference('ILLPartnerCode') );
315 $warnILLConfiguration = 1;
318 if ( !C4::Context->preference('ILLPartnerCode') ) {
319 # partner code not defined
320 $template->param( ill_partner_code_not_defined => 1 );
321 $warnILLConfiguration = 1;
325 if ( !$ill_config_from_file->{branch} ) {
327 $template->param( ill_branch_not_defined => 1 );
328 $warnILLConfiguration = 1;
331 $template->param( warnILLConfiguration => $warnILLConfiguration );
333 unless ( can_run('weasyprint') ) {
334 $template->param( weasyprint_missing => 1 );
340 OPACXSLTDetailsDisplay
342 OPACXSLTResultsDisplay
348 for my $p ( @xslt_prefs ) {
349 my $xsl_filename = C4::XSLT::get_xsl_filename( $p );
350 next if -e $xsl_filename;
354 value => C4::Context->preference("$p"),
355 filename => $xsl_filename
359 $template->param( warnXSLT => \@warnXSLT ) if @warnXSLT;
362 if ( C4::Context->preference('SearchEngine') eq 'Elasticsearch' ) {
363 # Check ES configuration health and runtime status
368 my $es_has_missing = 0;
372 $es_conf = Koha::SearchEngine::Elasticsearch::_read_configuration();
375 if ( ref($_) eq 'Koha::Exceptions::Config::MissingEntry' ) {
376 $template->param( elasticsearch_fatal_config_error => $_->message );
377 $es_config_error = 1;
380 if ( !$es_config_error ) {
382 my $biblios_index_name = $es_conf->{index_name} . "_" . $Koha::SearchEngine::BIBLIOS_INDEX;
383 my $authorities_index_name = $es_conf->{index_name} . "_" . $Koha::SearchEngine::AUTHORITIES_INDEX;
385 my @indexes = ($biblios_index_name, $authorities_index_name);
386 # TODO: When new indexes get added, we could have other ways to
387 # fetch the list of available indexes (e.g. plugins, etc)
388 $es_status->{nodes} = $es_conf->{nodes};
389 my $es = Search::Elasticsearch->new({ nodes => $es_conf->{nodes} });
390 my $es_status->{version} = $es->info->{version}->{number};
392 foreach my $index ( @indexes ) {
395 $index_count = $es->indices->stats( index => $index )
396 ->{_all}{primaries}{docs}{count};
399 if ( ref($_) eq 'Search::Elasticsearch::Error::Missing' ) {
400 push @{ $es_status->{errors} }, "Index not found ($index)";
403 elsif ( ref($_) eq 'Search::Elasticsearch::Error::NoNodes' ) {
407 # TODO: when time comes, we will cover more use cases
413 my $missing_count = 0;
414 if ( $index eq $biblios_index_name ) {
415 $db_count = Koha::Biblios->search->count;
416 } elsif ( $index eq $authorities_index_name ) {
417 $db_count = Koha::Authorities->search->count;
419 if ( $db_count != -1 && $index_count != -1 ) {
420 $missing_count = $db_count - $index_count;
421 $es_has_missing = 1 if $missing_count > 0;
423 push @{ $es_status->{indexes} },
425 index_name => $index,
426 index_count => $index_count,
427 db_count => $db_count,
428 missing_count => $missing_count,
431 $es_status->{running} = $es_running;
434 elasticsearch_status => $es_status,
435 elasticsearch_has_missing => $es_has_missing,
440 if ( C4::Context->preference('RESTOAuth2ClientCredentials') ) {
441 # Do we have the required deps?
442 unless ( can_load( modules => { 'Net::OAuth2::AuthorizationServer' => undef }) ) {
443 $template->param( oauth2_missing_deps => 1 );
447 # Sco Patron should not contain any other perms than circulate => self_checkout
448 if ( C4::Context->preference('WebBasedSelfCheck')
449 and C4::Context->preference('AutoSelfCheckAllowed')
451 my $userid = C4::Context->preference('AutoSelfCheckID');
452 my $all_permissions = C4::Auth::get_user_subpermissions( $userid );
453 my ( $has_self_checkout_perm, $has_other_permissions );
454 while ( my ( $module, $permissions ) = each %$all_permissions ) {
455 if ( $module eq 'self_check' ) {
456 while ( my ( $permission, $flag ) = each %$permissions ) {
457 if ( $permission eq 'self_checkout_module' ) {
458 $has_self_checkout_perm = 1;
460 $has_other_permissions = 1;
464 $has_other_permissions = 1;
468 AutoSelfCheckPatronDoesNotHaveSelfCheckPerm => not ( $has_self_checkout_perm ),
469 AutoSelfCheckPatronHasTooManyPerm => $has_other_permissions,
473 if ( C4::Context->preference('PatronSelfRegistration') ) {
474 $template->param( warnPrefPatronSelfRegistrationDefaultCategory => 1 )
475 unless Koha::Patron::Categories->find(C4::Context->preference('PatronSelfRegistrationDefaultCategory'));
478 # Test YAML system preferences
479 # FIXME: This is list of current YAML formatted prefs, should by type of preference
481 "BibtexExportAdditionalFields",
482 "ItemsDeniedRenewal",
484 "MarcItemFieldsToOrder",
486 "RisExportAdditionalFields",
487 "UpdateitemLocationOnCheckin",
488 "UpdateItemWhenLostFromHoldList",
489 "UpdateNotForLoanStatusOnCheckin",
490 "UpdateNotForLoanStatusOnCheckout",
493 foreach my $syspref (@yaml_prefs) {
494 my $yaml = C4::Context->preference( $syspref );
496 eval { YAML::XS::Load( Encode::encode_utf8("$yaml\n\n") ); };
498 push @bad_yaml_prefs, $syspref;
502 $template->param( 'bad_yaml_prefs' => \@bad_yaml_prefs ) if @bad_yaml_prefs;
505 my $dbh = C4::Context->dbh;
506 my $patrons = $dbh->selectall_arrayref(
507 q|select b.borrowernumber from borrowers b join deletedborrowers db on b.borrowernumber=db.borrowernumber|,
510 my $biblios = $dbh->selectall_arrayref(
511 q|select b.biblionumber from biblio b join deletedbiblio db on b.biblionumber=db.biblionumber|,
514 my $biblioitems = $dbh->selectall_arrayref(
515 q|select bi.biblioitemnumber from biblioitems bi join deletedbiblioitems dbi on bi.biblionumber=dbi.biblionumber|,
518 my $items = $dbh->selectall_arrayref(
519 q|select i.itemnumber from items i join deleteditems di on i.itemnumber=di.itemnumber|,
522 my $checkouts = $dbh->selectall_arrayref(
523 q|select i.issue_id from issues i join old_issues oi on i.issue_id=oi.issue_id|,
526 my $holds = $dbh->selectall_arrayref(
527 q|select r.reserve_id from reserves r join old_reserves o on r.reserve_id=o.reserve_id|,
530 if ( @$patrons or @$biblios or @$biblioitems or @$items or @$checkouts or @$holds ) {
533 ai_patrons => $patrons,
534 ai_biblios => $biblios,
535 ai_biblioitems => $biblioitems,
537 ai_checkouts => $checkouts,
545 my $dbh = C4::Context->dbh;
546 my $units = Koha::CirculationRules->search({ rule_name => 'lengthunit', rule_value => { -not_in => ['days', 'hours'] } });
548 if ( $units->count ) {
550 warnIssuingRules => 1,
556 # Guarantor relationships warnings
558 my $dbh = C4::Context->dbh;
559 my ($bad_relationships_count) = $dbh->selectall_arrayref(q{
562 SELECT relationship FROM borrower_relationships WHERE relationship='_bad_data'
564 SELECT relationship FROM borrowers WHERE relationship='_bad_data') a
567 $bad_relationships_count = $bad_relationships_count->[0]->[0];
569 my $existing_relationships = $dbh->selectall_arrayref(q{
570 SELECT DISTINCT(relationship)
572 SELECT relationship FROM borrower_relationships WHERE relationship IS NOT NULL
574 SELECT relationship FROM borrowers WHERE relationship IS NOT NULL) a
577 my %valid_relationships = map { $_ => 1 } split( /,|\|/, C4::Context->preference('borrowerRelationship') );
578 $valid_relationships{ _bad_data } = 1; # we handle this case in another way
580 my $wrong_relationships = [ grep { !$valid_relationships{ $_->[0] } } @{$existing_relationships} ];
581 if ( @$wrong_relationships or $bad_relationships_count ) {
584 warnRelationships => 1,
587 if ( $wrong_relationships ) {
589 wrong_relationships => $wrong_relationships
592 if ($bad_relationships_count) {
594 bad_relationships_count => $bad_relationships_count,
601 # Test 'bcrypt_settings' config for Pseudonymization
602 $template->param( config_bcrypt_settings_no_set => 1 )
603 if C4::Context->preference('Pseudonymization')
604 and not C4::Context->config('bcrypt_settings');
608 my @frameworkcodes = Koha::BiblioFrameworks->search->get_column('frameworkcode');
609 my @hidden_biblionumbers;
610 push @frameworkcodes, ""; # it's not in the biblio_frameworks table!
611 my $no_FA_framework = 1;
612 for my $frameworkcode ( @frameworkcodes ) {
613 $no_FA_framework = 0 if $frameworkcode eq 'FA';
614 my $shouldhidemarc_opac = Koha::Filter::MARC::ViewPolicy->should_hide_marc(
616 frameworkcode => $frameworkcode,
620 push @hidden_biblionumbers, { frameworkcode => $frameworkcode, interface => 'opac' }
621 if $shouldhidemarc_opac->{biblionumber};
623 my $shouldhidemarc_intranet = Koha::Filter::MARC::ViewPolicy->should_hide_marc(
625 frameworkcode => $frameworkcode,
626 interface => "intranet"
629 push @hidden_biblionumbers, { frameworkcode => $frameworkcode, interface => 'intranet' }
630 if $shouldhidemarc_intranet->{biblionumber};
632 $template->param( warnHiddenBiblionumbers => \@hidden_biblionumbers );
633 $template->param( warnFastCataloging => $no_FA_framework );
637 # BackgroundJob - test connection to message broker
639 Koha::BackgroundJob->connect;
643 $template->param( warnConnectBroker => $@ );
647 #BZ 28267: Warn administrators if there are database rows with a format other than 'DYNAMIC'
649 $template->param( warnDbRowFormat => C4::Installer->has_non_dynamic_row_format );
652 my %versions = C4::Context::get_versions();
655 kohaVersion => $versions{'kohaVersion'},
656 osVersion => $versions{'osVersion'},
657 perlPath => $perl_path,
658 perlVersion => $versions{'perlVersion'},
659 perlIncPath => [ map { perlinc => $_ }, @INC ],
660 mysqlVersion => $versions{'mysqlVersion'},
661 apacheVersion => $versions{'apacheVersion'},
662 zebraVersion => $zebraVersion,
663 prefRequireChoosingExistingAuthority => $prefRequireChoosingExistingAuthority,
664 prefAutoCreateAuthorities => $prefAutoCreateAuthorities,
665 warnPrefRequireChoosingExistingAuthority => $warnPrefRequireChoosingExistingAuthority,
666 warnPrefEasyAnalyticalRecords => $warnPrefEasyAnalyticalRecords,
667 warnPrefAnonymousPatronOPACPrivacy => $warnPrefAnonymousPatronOPACPrivacy,
668 warnPrefAnonymousPatronAnonSuggestions => $warnPrefAnonymousPatronAnonSuggestions,
669 warnPrefAnonymousPatronOPACPrivacy_PatronDoesNotExist => $warnPrefAnonymousPatronOPACPrivacy_PatronDoesNotExist,
670 warnPrefAnonymousPatronAnonSuggestions_PatronDoesNotExist => $warnPrefAnonymousPatronAnonSuggestions_PatronDoesNotExist,
671 warnPrefKohaAdminEmailAddress => $warnPrefKohaAdminEmailAddress,
672 warnPrefOpacHiddenItems => $warnPrefOpacHiddenItems,
673 errZebraConnection => $errZebraConnection,
674 warnIsRootUser => $warnIsRootUser,
675 warnNoActiveCurrency => $warnNoActiveCurrency,
676 warnNoTemplateCaching => ( C4::Context->config('template_cache_dir') ? 0 : 1 ),
677 xml_config_warnings => \@xml_config_warnings,
678 warnStatisticsFieldsError => $warnStatisticsFieldsError,
683 my $perl_modules = C4::Installer::PerlModules->new;
684 $perl_modules->versions_info;
686 my @pm_types = qw(missing_pm upgrade_pm current_pm);
688 foreach my $pm_type(@pm_types) {
689 my $modules = $perl_modules->get_attr($pm_type);
690 foreach (@$modules) {
691 my ($module, $stats) = each %$_;
696 version => $stats->{'cur_ver'},
697 missing => ($pm_type eq 'missing_pm' ? 1 : 0),
698 upgrade => ($pm_type eq 'upgrade_pm' ? 1 : 0),
699 current => ($pm_type eq 'current_pm' ? 1 : 0),
700 require => $stats->{'required'},
701 reqversion => $stats->{'min_ver'},
702 maxversion => $stats->{'max_ver'},
703 excversion => $stats->{'exc_ver'}
709 @components = sort {$a->{'name'} cmp $b->{'name'}} @components;
714 foreach (@components) {
716 unless (++$counter % 4) {
717 push (@$table, {row => $row});
721 # Processing the last line (if there are any modules left)
722 if (scalar(@$row) > 0) {
723 # Extending $row to the table size
725 # Pushing the last line
726 push (@$table, {row => $row});
730 $template->param( table => $table );
733 ## ------------------------------------------
734 ## Koha contributions
736 if ( defined C4::Context->config('docdir') ) {
737 $docdir = C4::Context->config('docdir');
739 # if no <docdir> is defined in koha-conf.xml, use the default location
740 # this is a work-around to stop breakage on upgraded Kohas, bug 8911
741 $docdir = C4::Context->config('intranetdir') . '/docs';
746 -e "$docdir" . "/teams.yaml"
747 ? YAML::XS::LoadFile( "$docdir" . "/teams.yaml" )
749 my $dev_team = (sort {$b <=> $a} (keys %{$teams->{team}}))[0];
750 my $short_version = substr($versions{'kohaVersion'},0,5);
751 my $minor = substr($versions{'kohaVersion'},3,2);
752 my $development_version = ( $minor eq '05' || $minor eq '11' ) ? 0 : 1;
754 $template->param( short_version => $short_version );
755 $template->param( development_version => $development_version );
759 -e "$docdir" . "/contributors.yaml"
760 ? YAML::XS::LoadFile( "$docdir" . "/contributors.yaml" )
762 delete $contributors->{_others_};
763 for my $version ( sort { $a <=> $b } keys %{$teams->{team}} ) {
764 for my $role ( keys %{ $teams->{team}->{$version} } ) {
765 my $normalized_role = "$role";
766 $normalized_role =~ s/s$//;
767 if ( ref( $teams->{team}->{$version}->{$role} ) eq 'ARRAY' ) {
768 for my $contributor ( @{ $teams->{team}->{$version}->{$role} } ) {
769 my $name = $contributor->{name};
770 # Add role to contributors
771 push @{ $contributors->{$name}->{roles}->{$normalized_role} },
773 # Add openhub to teams
774 if ( exists( $contributors->{$name}->{openhub} ) ) {
775 $contributor->{openhub} = $contributors->{$name}->{openhub};
779 elsif ( $role eq 'release_date' ) {
780 $teams->{team}->{$version}->{$role} = DateTime->from_epoch( epoch => $teams->{team}->{$version}->{$role});
782 elsif ( $role eq 'codename' ) {
783 if ( $version == $short_version ) {
784 $codename = $teams->{team}->{$version}->{$role};
789 my $name = $teams->{team}->{$version}->{$role}->{name};
790 # Add role to contributors
791 push @{ $contributors->{$name}->{roles}->{$normalized_role} },
793 # Add openhub to teams
794 if ( exists( $contributors->{$name}->{openhub} ) ) {
795 $teams->{team}->{$version}->{$role}->{openhub} =
796 $contributors->{$name}->{openhub};
802 ## Create last name ordered array of people from contributors
804 { name => $_, ( $contributors->{$_} ? %{ $contributors->{$_} } : () ) }
806 my ($alast) = $a =~ /(\S+)$/;
807 my ($blast) = $b =~ /(\S+)$/;
808 my $cmp = lc($alast||"") cmp lc($blast||"");
811 my ($a2last) = $a =~ /(\S+)\s\S+$/;
812 my ($b2last) = $b =~ /(\S+)\s\S+$/;
813 lc($a2last||"") cmp lc($b2last||"");
814 } keys %$contributors;
816 $template->param( kohaCodename => $codename);
817 $template->param( contributors => \@people );
818 $template->param( maintenance_team => $teams->{team}->{$dev_team} );
819 $template->param( release_team => $teams->{team}->{$short_version} );
822 if ( open( my $file, "<:encoding(UTF-8)", "$docdir" . "/history.txt" ) ) {
832 shift @lines; #remove header row
835 my ( $epoch, $date, $desc, $tag ) = split(/\t/);
836 if(!$desc && $date=~ /(?<=\d{4})\s+/) {
837 ($date, $desc)= ($`, $');
849 #foreach my $row2 (@rows2) {
852 push( @$table2, { row2 => $row2 } );
856 $template->param( table2 => $table2 );
858 $template->param( timeline_read_error => 1 );
861 output_html_with_http_headers $query, $cookie, $template->output;