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;
52 use Koha::Patron::Categories;
55 use Koha::Config::SysPrefs;
56 use Koha::Illrequest::Config;
57 use Koha::SearchEngine::Elasticsearch;
59 use Koha::Filter::MARC::ViewPolicy;
61 use C4::Members::Statistics;
64 my ( $template, $loggedinuser, $cookie ) = get_template_and_user(
66 template_name => "about.tt",
69 flagsrequired => { catalogue => 1 },
73 my $config_timezone = C4::Context->config('timezone') // '';
74 my $config_invalid = !DateTime::TimeZone->is_valid_name( $config_timezone );
75 my $env_timezone = $ENV{TZ} // '';
76 my $env_invalid = !DateTime::TimeZone->is_valid_name( $env_timezone );
77 my $actual_bad_tz_fallback = 0;
79 if ( $config_timezone ne '' &&
82 $actual_bad_tz_fallback = 1;
84 elsif ( $config_timezone eq '' &&
85 $env_timezone ne '' &&
87 # No config, but bad ENV{TZ}
88 $actual_bad_tz_fallback = 1;
92 actual => C4::Context->tz->name,
93 actual_bad_tz_fallback => $actual_bad_tz_fallback,
94 config => $config_timezone,
95 config_invalid => $config_invalid,
96 environment => $env_timezone,
97 environment_invalid => $env_invalid
101 my $log4perl_config = C4::Context->config("log4perl_conf");
103 if ( ! $log4perl_config ) {
104 push @log4perl_errors, 'missing_config_entry'
107 my @lines = read_file($log4perl_config) or push @log4perl_errors, 'cannot_read_config_file';
108 for my $line ( @lines ) {
109 next unless $line =~ m|log4perl.appender.\w+.filename=(.*)|;
110 push @log4perl_errors, 'logfile_not_writable' unless -w $1;
113 eval {Koha::Logger->get};
114 push @log4perl_errors, 'cannot_init_module' and warn $@ if $@;
115 $template->param( log4perl_errors => @log4perl_errors );
119 time_zone => $time_zone,
120 current_date_and_time => output_pref({ dt => dt_from_string(), dateformat => 'iso' })
125 $perl_path .= $Config{_exe} unless $perl_path =~ m/$Config{_exe}$/i;
128 my $zebraVersion = `zebraidx -V`;
130 # Check running PSGI env
131 if ( C4::Context->psgi_env ) {
134 psgi_server => ($ENV{ PLACK_ENV }) ? "Plack ($ENV{PLACK_ENV})" :
135 ($ENV{ MOD_PERL }) ? "mod_perl ($ENV{MOD_PERL})" :
140 # Memcached configuration
141 my $memcached_servers = $ENV{MEMCACHED_SERVERS} || C4::Context->config('memcached_servers');
142 my $memcached_namespace = $ENV{MEMCACHED_NAMESPACE} || C4::Context->config('memcached_namespace') // 'koha';
144 my $cache = Koha::Caches->get_instance;
145 my $effective_caching_method = ref($cache->cache);
146 # Memcached may have been running when plack has been initialized but could have been stopped since
147 # FIXME What are the consequences of that??
148 my $is_memcached_still_active = $cache->set_in_cache('test_for_about_page', "just a simple value");
150 my $where_is_memcached_config = 'nowhere';
151 if ( $ENV{MEMCACHED_SERVERS} and C4::Context->config('memcached_servers') ) {
152 $where_is_memcached_config = 'both';
153 } elsif ( $ENV{MEMCACHED_SERVERS} and not C4::Context->config('memcached_servers') ) {
154 $where_is_memcached_config = 'ENV_only';
155 } elsif ( C4::Context->config('memcached_servers') ) {
156 $where_is_memcached_config = 'config_only';
160 effective_caching_method => $effective_caching_method,
161 memcached_servers => $memcached_servers,
162 memcached_namespace => $memcached_namespace,
163 is_memcached_still_active => $is_memcached_still_active,
164 where_is_memcached_config => $where_is_memcached_config,
165 memcached_running => Koha::Caches->get_instance->memcached_cache,
168 # Additional system information for warnings
170 my $warnStatisticsFieldsError;
171 my $prefStatisticsFields = C4::Context->preference('StatisticsFields');
172 if ($prefStatisticsFields) {
173 $warnStatisticsFieldsError = $prefStatisticsFields
174 unless ( $prefStatisticsFields eq C4::Members::Statistics->get_fields() );
177 my $prefAutoCreateAuthorities = C4::Context->preference('AutoCreateAuthorities');
178 my $prefRequireChoosingExistingAuthority = C4::Context->preference('RequireChoosingExistingAuthority');
179 my $warnPrefRequireChoosingExistingAuthority = ( !$prefAutoCreateAuthorities && ( !$prefRequireChoosingExistingAuthority) );
181 my $prefEasyAnalyticalRecords = C4::Context->preference('EasyAnalyticalRecords');
182 my $prefUseControlNumber = C4::Context->preference('UseControlNumber');
183 my $warnPrefEasyAnalyticalRecords = ( $prefEasyAnalyticalRecords && $prefUseControlNumber );
185 my $AnonymousPatron = C4::Context->preference('AnonymousPatron');
186 my $warnPrefAnonymousPatronOPACPrivacy = (
187 C4::Context->preference('OPACPrivacy')
188 and not $AnonymousPatron
190 my $warnPrefAnonymousPatronAnonSuggestions = (
191 C4::Context->preference('AnonSuggestions')
192 and not $AnonymousPatron
195 my $anonymous_patron = Koha::Patrons->find( $AnonymousPatron );
196 my $warnPrefAnonymousPatronAnonSuggestions_PatronDoesNotExist = ( $AnonymousPatron && C4::Context->preference('AnonSuggestions') && not $anonymous_patron );
198 my $warnPrefAnonymousPatronOPACPrivacy_PatronDoesNotExist = ( not $anonymous_patron and Koha::Patrons->search({ privacy => 2 })->count );
200 my $warnPrefKohaAdminEmailAddress = !Koha::Email->is_valid(C4::Context->preference('KohaAdminEmailAddress'));
202 my $c = Koha::Items->filter_by_visible_in_opac->count;
203 my @warnings = C4::Context->dbh->selectrow_array('SHOW WARNINGS');
204 my $warnPrefOpacHiddenItems = $warnings[2];
206 my $invalid_yesno = Koha::Config::SysPrefs->search(
209 value => { -or => { 'is' => undef, -not_in => [ "1", "0" ] } }
212 $template->param( invalid_yesno => $invalid_yesno );
214 my $errZebraConnection = C4::Context->Zconn("biblioserver",0)->errcode();
216 my $warnIsRootUser = (! $loggedinuser);
218 my $warnNoActiveCurrency = (! defined Koha::Acquisition::Currencies->get_active);
220 my @xml_config_warnings;
222 if ( C4::Context->config('zebra_bib_index_mode')
223 and C4::Context->config('zebra_bib_index_mode') eq 'grs1' )
225 push @xml_config_warnings, { error => 'zebra_bib_index_mode_is_grs1' };
228 if ( C4::Context->config('zebra_auth_index_mode')
229 and C4::Context->config('zebra_auth_index_mode') eq 'grs1' )
231 push @xml_config_warnings, { error => 'zebra_auth_index_mode_is_grs1' };
234 my $authorityserver = C4::Context->zebraconfig('authorityserver');
235 if( ( C4::Context->config('zebra_auth_index_mode')
236 and C4::Context->config('zebra_auth_index_mode') eq 'dom' )
237 && ( $authorityserver->{config} !~ /zebra-authorities-dom.cfg/ ) )
239 push @xml_config_warnings, {
240 error => 'zebra_auth_index_mode_mismatch_warn'
244 if ( ! defined C4::Context->config('log4perl_conf') ) {
245 push @xml_config_warnings, {
246 error => 'log4perl_entry_missing'
250 if ( ! defined C4::Context->config('lockdir') ) {
251 push @xml_config_warnings, {
252 error => 'lockdir_entry_missing'
256 unless ( -w C4::Context->config('lockdir') ) {
257 push @xml_config_warnings, {
258 error => 'lockdir_not_writable',
259 lockdir => C4::Context->config('lockdir')
264 if ( ! defined C4::Context->config('upload_path') ) {
265 if ( Koha::Config::SysPrefs->find('OPACBaseURL')->value ) {
266 # OPACBaseURL seems to be set
267 push @xml_config_warnings, {
268 error => 'uploadpath_entry_missing'
271 push @xml_config_warnings, {
272 error => 'uploadpath_and_opacbaseurl_entry_missing'
277 if ( ! C4::Context->config('tmp_path') ) {
278 my $temporary_directory = C4::Context::temporary_directory;
279 push @xml_config_warnings, {
280 error => 'tmp_path_missing',
281 effective_tmp_dir => $temporary_directory,
285 my $encryption_key = C4::Context->config('encryption_key');
286 if ( !$encryption_key || $encryption_key eq '__ENCRYPTION_KEY__') {
287 push @xml_config_warnings, { error => 'encryption_key_missing' };
290 # Test Zebra facets configuration
291 if ( !defined C4::Context->config('use_zebra_facets') ) {
292 push @xml_config_warnings, { error => 'use_zebra_facets_entry_missing' };
295 unless ( Koha::I18N->_base_directory ) {
296 $template->param( warnI18nMissing => 1 );
300 if ( C4::Context->preference('ILLModule') ) {
301 my $warnILLConfiguration = 0;
302 my $ill_config_from_file = C4::Context->config("interlibrary_loans");
303 my $ill_config = Koha::Illrequest::Config->new;
305 my $available_ill_backends =
306 ( scalar @{ $ill_config->available_backends } > 0 );
309 if ( !$available_ill_backends ) {
310 $template->param( no_ill_backends => 1 );
311 $warnILLConfiguration = 1;
314 # Check ILLPartnerCode sys pref
315 if ( !Koha::Patron::Categories->find( C4::Context->preference('ILLPartnerCode') ) ) {
316 $template->param( ill_partner_code_doesnt_exist => C4::Context->preference('ILLPartnerCode') );
317 $warnILLConfiguration = 1;
318 } elsif ( !Koha::Patrons->search( { categorycode => C4::Context->preference('ILLPartnerCode') } )->count ) {
319 $template->param( ill_partner_code_no_patrons => C4::Context->preference('ILLPartnerCode') );
320 $warnILLConfiguration = 1;
323 if ( !C4::Context->preference('ILLPartnerCode') ) {
324 # partner code not defined
325 $template->param( ill_partner_code_not_defined => 1 );
326 $warnILLConfiguration = 1;
330 if ( !$ill_config_from_file->{branch} ) {
332 $template->param( ill_branch_not_defined => 1 );
333 $warnILLConfiguration = 1;
336 $template->param( warnILLConfiguration => $warnILLConfiguration );
338 unless ( can_run('weasyprint') ) {
339 $template->param( weasyprint_missing => 1 );
345 OPACXSLTDetailsDisplay
347 OPACXSLTResultsDisplay
353 for my $p ( @xslt_prefs ) {
354 my $xsl_filename = C4::XSLT::get_xsl_filename( $p );
355 next if -e $xsl_filename;
359 value => C4::Context->preference("$p"),
360 filename => $xsl_filename
364 $template->param( warnXSLT => \@warnXSLT ) if @warnXSLT;
367 if ( C4::Context->preference('SearchEngine') eq 'Elasticsearch' ) {
368 # Check ES configuration health and runtime status
373 my $es_has_missing = 0;
377 $es_conf = Koha::SearchEngine::Elasticsearch::_read_configuration();
380 if ( ref($_) eq 'Koha::Exceptions::Config::MissingEntry' ) {
381 $template->param( elasticsearch_fatal_config_error => $_->message );
382 $es_config_error = 1;
385 if ( !$es_config_error ) {
387 my $biblios_index_name = $es_conf->{index_name} . "_" . $Koha::SearchEngine::BIBLIOS_INDEX;
388 my $authorities_index_name = $es_conf->{index_name} . "_" . $Koha::SearchEngine::AUTHORITIES_INDEX;
390 my @indexes = ($biblios_index_name, $authorities_index_name);
391 # TODO: When new indexes get added, we could have other ways to
392 # fetch the list of available indexes (e.g. plugins, etc)
393 $es_status->{nodes} = $es_conf->{nodes};
394 my $es = Search::Elasticsearch->new( $es_conf );
395 my $es_status->{version} = $es->info->{version}->{number};
397 foreach my $index ( @indexes ) {
400 $index_count = $es->indices->stats( index => $index )
401 ->{_all}{primaries}{docs}{count};
404 if ( ref($_) eq 'Search::Elasticsearch::Error::Missing' ) {
405 push @{ $es_status->{errors} }, "Index not found ($index)";
408 elsif ( ref($_) eq 'Search::Elasticsearch::Error::NoNodes' ) {
412 # TODO: when time comes, we will cover more use cases
418 my $missing_count = 0;
419 if ( $index eq $biblios_index_name ) {
420 $db_count = Koha::Biblios->search->count;
421 } elsif ( $index eq $authorities_index_name ) {
422 $db_count = Koha::Authorities->search->count;
424 if ( $db_count != -1 && $index_count != -1 ) {
425 $missing_count = $db_count - $index_count;
426 $es_has_missing = 1 if $missing_count > 0;
428 push @{ $es_status->{indexes} },
430 index_name => $index,
431 index_count => $index_count,
432 db_count => $db_count,
433 missing_count => $missing_count,
436 $es_status->{running} = $es_running;
439 elasticsearch_status => $es_status,
440 elasticsearch_has_missing => $es_has_missing,
445 if ( C4::Context->preference('RESTOAuth2ClientCredentials') ) {
446 # Do we have the required deps?
447 unless ( can_load( modules => { 'Net::OAuth2::AuthorizationServer' => undef }) ) {
448 $template->param( oauth2_missing_deps => 1 );
452 # Sco Patron should not contain any other perms than circulate => self_checkout
453 if ( C4::Context->preference('WebBasedSelfCheck')
454 and C4::Context->preference('AutoSelfCheckAllowed')
456 my $userid = C4::Context->preference('AutoSelfCheckID');
457 my $all_permissions = C4::Auth::get_user_subpermissions( $userid );
458 my ( $has_self_checkout_perm, $has_other_permissions );
459 while ( my ( $module, $permissions ) = each %$all_permissions ) {
460 if ( $module eq 'self_check' ) {
461 while ( my ( $permission, $flag ) = each %$permissions ) {
462 if ( $permission eq 'self_checkout_module' ) {
463 $has_self_checkout_perm = 1;
465 $has_other_permissions = 1;
469 $has_other_permissions = 1;
473 AutoSelfCheckPatronDoesNotHaveSelfCheckPerm => not ( $has_self_checkout_perm ),
474 AutoSelfCheckPatronHasTooManyPerm => $has_other_permissions,
478 if ( C4::Context->preference('PatronSelfRegistration') ) {
479 $template->param( warnPrefPatronSelfRegistrationDefaultCategory => 1 )
480 unless Koha::Patron::Categories->find(C4::Context->preference('PatronSelfRegistrationDefaultCategory'));
483 # Test YAML system preferences
484 # FIXME: This is list of current YAML formatted prefs, should by type of preference
486 "BibtexExportAdditionalFields",
487 "ItemsDeniedRenewal",
489 "MarcItemFieldsToOrder",
491 "RisExportAdditionalFields",
492 "UpdateitemLocationOnCheckin",
493 "UpdateItemWhenLostFromHoldList",
494 "UpdateNotForLoanStatusOnCheckin",
495 "UpdateNotForLoanStatusOnCheckout",
498 foreach my $syspref (@yaml_prefs) {
499 my $yaml = C4::Context->preference( $syspref );
501 eval { YAML::XS::Load( Encode::encode_utf8("$yaml\n\n") ); };
503 push @bad_yaml_prefs, $syspref;
507 $template->param( 'bad_yaml_prefs' => \@bad_yaml_prefs ) if @bad_yaml_prefs;
510 my $dbh = C4::Context->dbh;
511 my $patrons = $dbh->selectall_arrayref(
512 q|select b.borrowernumber from borrowers b join deletedborrowers db on b.borrowernumber=db.borrowernumber|,
515 my $biblios = $dbh->selectall_arrayref(
516 q|select b.biblionumber from biblio b join deletedbiblio db on b.biblionumber=db.biblionumber|,
519 my $biblioitems = $dbh->selectall_arrayref(
520 q|select bi.biblioitemnumber from biblioitems bi join deletedbiblioitems dbi on bi.biblionumber=dbi.biblionumber|,
523 my $items = $dbh->selectall_arrayref(
524 q|select i.itemnumber from items i join deleteditems di on i.itemnumber=di.itemnumber|,
527 my $checkouts = $dbh->selectall_arrayref(
528 q|select i.issue_id from issues i join old_issues oi on i.issue_id=oi.issue_id|,
531 my $holds = $dbh->selectall_arrayref(
532 q|select r.reserve_id from reserves r join old_reserves o on r.reserve_id=o.reserve_id|,
535 if ( @$patrons or @$biblios or @$biblioitems or @$items or @$checkouts or @$holds ) {
538 ai_patrons => $patrons,
539 ai_biblios => $biblios,
540 ai_biblioitems => $biblioitems,
542 ai_checkouts => $checkouts,
550 my $dbh = C4::Context->dbh;
551 my $units = Koha::CirculationRules->search({ rule_name => 'lengthunit', rule_value => { -not_in => ['days', 'hours'] } });
553 if ( $units->count ) {
555 warnIssuingRules => 1,
561 # Guarantor relationships warnings
563 my $dbh = C4::Context->dbh;
564 my ($bad_relationships_count) = $dbh->selectall_arrayref(q{
567 SELECT relationship FROM borrower_relationships WHERE relationship='_bad_data'
569 SELECT relationship FROM borrowers WHERE relationship='_bad_data') a
572 $bad_relationships_count = $bad_relationships_count->[0]->[0];
574 my $existing_relationships = $dbh->selectall_arrayref(q{
575 SELECT DISTINCT(relationship)
577 SELECT relationship FROM borrower_relationships WHERE relationship IS NOT NULL
579 SELECT relationship FROM borrowers WHERE relationship IS NOT NULL) a
582 my %valid_relationships = map { $_ => 1 } split( /,|\|/, C4::Context->preference('borrowerRelationship') );
583 $valid_relationships{ _bad_data } = 1; # we handle this case in another way
585 my $wrong_relationships = [ grep { !$valid_relationships{ $_->[0] } } @{$existing_relationships} ];
586 if ( @$wrong_relationships or $bad_relationships_count ) {
589 warnRelationships => 1,
592 if ( $wrong_relationships ) {
594 wrong_relationships => $wrong_relationships
597 if ($bad_relationships_count) {
599 bad_relationships_count => $bad_relationships_count,
606 # Test 'bcrypt_settings' config for Pseudonymization
607 $template->param( config_bcrypt_settings_no_set => 1 )
608 if C4::Context->preference('Pseudonymization')
609 and not C4::Context->config('bcrypt_settings');
613 my @frameworkcodes = Koha::BiblioFrameworks->search->get_column('frameworkcode');
614 my @hidden_biblionumbers;
615 push @frameworkcodes, ""; # it's not in the biblio_frameworks table!
616 my $no_FA_framework = 1;
617 for my $frameworkcode ( @frameworkcodes ) {
618 $no_FA_framework = 0 if $frameworkcode eq 'FA';
619 my $shouldhidemarc_opac = Koha::Filter::MARC::ViewPolicy->should_hide_marc(
621 frameworkcode => $frameworkcode,
625 push @hidden_biblionumbers, { frameworkcode => $frameworkcode, interface => 'opac' }
626 if $shouldhidemarc_opac->{biblionumber};
628 my $shouldhidemarc_intranet = Koha::Filter::MARC::ViewPolicy->should_hide_marc(
630 frameworkcode => $frameworkcode,
631 interface => "intranet"
634 push @hidden_biblionumbers, { frameworkcode => $frameworkcode, interface => 'intranet' }
635 if $shouldhidemarc_intranet->{biblionumber};
637 $template->param( warnHiddenBiblionumbers => \@hidden_biblionumbers );
638 $template->param( warnFastCataloging => $no_FA_framework );
642 # BackgroundJob - test connection to message broker
644 Koha::BackgroundJob->connect;
648 $template->param( warnConnectBroker => $@ );
652 #BZ 28267: Warn administrators if there are database rows with a format other than 'DYNAMIC'
654 $template->param( warnDbRowFormat => C4::Installer->has_non_dynamic_row_format );
657 my %versions = C4::Context::get_versions();
660 kohaVersion => $versions{'kohaVersion'},
661 osVersion => $versions{'osVersion'},
662 perlPath => $perl_path,
663 perlVersion => $versions{'perlVersion'},
664 perlIncPath => [ map { perlinc => $_ }, @INC ],
665 mysqlVersion => $versions{'mysqlVersion'},
666 apacheVersion => $versions{'apacheVersion'},
667 zebraVersion => $zebraVersion,
668 prefRequireChoosingExistingAuthority => $prefRequireChoosingExistingAuthority,
669 prefAutoCreateAuthorities => $prefAutoCreateAuthorities,
670 warnPrefRequireChoosingExistingAuthority => $warnPrefRequireChoosingExistingAuthority,
671 warnPrefEasyAnalyticalRecords => $warnPrefEasyAnalyticalRecords,
672 warnPrefAnonymousPatronOPACPrivacy => $warnPrefAnonymousPatronOPACPrivacy,
673 warnPrefAnonymousPatronAnonSuggestions => $warnPrefAnonymousPatronAnonSuggestions,
674 warnPrefAnonymousPatronOPACPrivacy_PatronDoesNotExist => $warnPrefAnonymousPatronOPACPrivacy_PatronDoesNotExist,
675 warnPrefAnonymousPatronAnonSuggestions_PatronDoesNotExist => $warnPrefAnonymousPatronAnonSuggestions_PatronDoesNotExist,
676 warnPrefKohaAdminEmailAddress => $warnPrefKohaAdminEmailAddress,
677 warnPrefOpacHiddenItems => $warnPrefOpacHiddenItems,
678 errZebraConnection => $errZebraConnection,
679 warnIsRootUser => $warnIsRootUser,
680 warnNoActiveCurrency => $warnNoActiveCurrency,
681 warnNoTemplateCaching => ( C4::Context->config('template_cache_dir') ? 0 : 1 ),
682 xml_config_warnings => \@xml_config_warnings,
683 warnStatisticsFieldsError => $warnStatisticsFieldsError,
688 my $perl_modules = C4::Installer::PerlModules->new;
689 $perl_modules->versions_info;
691 my @pm_types = qw(missing_pm upgrade_pm current_pm);
693 foreach my $pm_type(@pm_types) {
694 my $modules = $perl_modules->get_attr($pm_type);
695 foreach (@$modules) {
696 my ($module, $stats) = each %$_;
701 version => $stats->{'cur_ver'},
702 missing => ($pm_type eq 'missing_pm' ? 1 : 0),
703 upgrade => ($pm_type eq 'upgrade_pm' ? 1 : 0),
704 current => ($pm_type eq 'current_pm' ? 1 : 0),
705 require => $stats->{'required'},
706 reqversion => $stats->{'min_ver'},
707 maxversion => $stats->{'max_ver'},
708 excversion => $stats->{'exc_ver'}
714 @components = sort {$a->{'name'} cmp $b->{'name'}} @components;
719 foreach (@components) {
721 unless (++$counter % 4) {
722 push (@$table, {row => $row});
726 # Processing the last line (if there are any modules left)
727 if (scalar(@$row) > 0) {
728 # Extending $row to the table size
730 # Pushing the last line
731 push (@$table, {row => $row});
735 $template->param( table => $table );
738 ## ------------------------------------------
739 ## Koha contributions
741 if ( defined C4::Context->config('docdir') ) {
742 $docdir = C4::Context->config('docdir');
744 # if no <docdir> is defined in koha-conf.xml, use the default location
745 # this is a work-around to stop breakage on upgraded Kohas, bug 8911
746 $docdir = C4::Context->config('intranetdir') . '/docs';
751 -e "$docdir" . "/teams.yaml"
752 ? YAML::XS::LoadFile( "$docdir" . "/teams.yaml" )
754 my $dev_team = (sort {$b <=> $a} (keys %{$teams->{team}}))[0];
755 my $short_version = substr($versions{'kohaVersion'},0,5);
756 my $minor = substr($versions{'kohaVersion'},3,2);
757 my $development_version = ( $minor eq '05' || $minor eq '11' ) ? 0 : 1;
759 $template->param( short_version => $short_version );
760 $template->param( development_version => $development_version );
764 -e "$docdir" . "/contributors.yaml"
765 ? YAML::XS::LoadFile( "$docdir" . "/contributors.yaml" )
767 delete $contributors->{_others_};
768 for my $version ( sort { $a <=> $b } keys %{$teams->{team}} ) {
769 for my $role ( keys %{ $teams->{team}->{$version} } ) {
770 my $detail = $teams->{team}->{$version}->{$role};
771 my $normalized_role = "$role";
772 $normalized_role =~ s/s$//;
773 if ( ref( $detail ) eq 'ARRAY' ) {
774 for my $contributor ( @{ $detail } ) {
776 my $localized_role = $normalized_role;
777 if ( my $subversion = $contributor->{version} ) {
778 $localized_role .= ':' . $subversion;
780 if ( my $area = $contributor->{area} ) {
781 $localized_role .= ':' . $area;
784 my $name = $contributor->{name};
785 # Add role to contributors
786 push @{ $contributors->{$name}->{roles}->{$localized_role} },
788 # Add openhub to teams
789 if ( exists( $contributors->{$name}->{openhub} ) ) {
790 $contributor->{openhub} = $contributors->{$name}->{openhub};
794 elsif ( $role eq 'release_date' ) {
795 $teams->{team}->{$version}->{$role} = DateTime->from_epoch( epoch => $teams->{team}->{$version}->{$role} );
797 elsif ( $role eq 'codename' ) {
798 if ( $version == $short_version ) {
804 if ( my $subversion = $detail->{version} ) {
805 $normalized_role .= ':' . $subversion;
807 if ( my $area = $detail->{area} ) {
808 $normalized_role .= ':' . $area;
811 my $name = $detail->{name};
812 # Add role to contributors
813 push @{ $contributors->{$name}->{roles}->{$normalized_role} },
815 # Add openhub to teams
816 if ( exists( $contributors->{$name}->{openhub} ) ) {
818 $contributors->{$name}->{openhub};
824 ## Create last name ordered array of people from contributors
826 { name => $_, ( $contributors->{$_} ? %{ $contributors->{$_} } : () ) }
828 my ($alast) = $a =~ /(\S+)$/;
829 my ($blast) = $b =~ /(\S+)$/;
830 my $cmp = lc($alast||"") cmp lc($blast||"");
833 my ($a2last) = $a =~ /(\S+)\s\S+$/;
834 my ($b2last) = $b =~ /(\S+)\s\S+$/;
835 lc($a2last||"") cmp lc($b2last||"");
836 } keys %$contributors;
838 $template->param( kohaCodename => $codename);
839 $template->param( contributors => \@people );
840 $template->param( maintenance_team => $teams->{team}->{$dev_team} );
841 $template->param( release_team => $teams->{team}->{$short_version} );
844 if ( open( my $file, "<:encoding(UTF-8)", "$docdir" . "/history.txt" ) ) {
854 shift @lines; #remove header row
857 my ( $epoch, $date, $desc, $tag ) = split(/\t/);
858 if(!$desc && $date=~ /(?<=\d{4})\s+/) {
859 ($date, $desc)= ($`, $');
871 #foreach my $row2 (@rows2) {
874 push( @$table2, { row2 => $row2 } );
878 $template->param( table2 => $table2 );
880 $template->param( timeline_read_error => 1 );
883 output_html_with_http_headers $query, $cookie, $template->output;