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 if( ! C4::Context->config('encryption_key') ) {
285 push @xml_config_warnings, { error => 'encryption_key_missing' };
288 # Test Zebra facets configuration
289 if ( !defined C4::Context->config('use_zebra_facets') ) {
290 push @xml_config_warnings, { error => 'use_zebra_facets_entry_missing' };
294 if ( C4::Context->preference('ILLModule') ) {
295 my $warnILLConfiguration = 0;
296 my $ill_config_from_file = C4::Context->config("interlibrary_loans");
297 my $ill_config = Koha::Illrequest::Config->new;
299 my $available_ill_backends =
300 ( scalar @{ $ill_config->available_backends } > 0 );
303 if ( !$available_ill_backends ) {
304 $template->param( no_ill_backends => 1 );
305 $warnILLConfiguration = 1;
309 if ( !Koha::Patron::Categories->find($ill_config->partner_code) ) {
310 $template->param( ill_partner_code_doesnt_exist => $ill_config->partner_code );
311 $warnILLConfiguration = 1;
314 if ( !$ill_config_from_file->{partner_code} ) {
315 # partner code not defined
316 $template->param( ill_partner_code_not_defined => 1 );
317 $warnILLConfiguration = 1;
321 if ( !$ill_config_from_file->{branch} ) {
323 $template->param( ill_branch_not_defined => 1 );
324 $warnILLConfiguration = 1;
327 $template->param( warnILLConfiguration => $warnILLConfiguration );
329 unless ( can_run('weasyprint') ) {
330 $template->param( weasyprint_missing => 1 );
336 OPACXSLTDetailsDisplay
338 OPACXSLTResultsDisplay
344 for my $p ( @xslt_prefs ) {
345 my $xsl_filename = C4::XSLT::get_xsl_filename( $p );
346 next if -e $xsl_filename;
350 value => C4::Context->preference("$p"),
351 filename => $xsl_filename
355 $template->param( warnXSLT => \@warnXSLT ) if @warnXSLT;
358 if ( C4::Context->preference('SearchEngine') eq 'Elasticsearch' ) {
359 # Check ES configuration health and runtime status
364 my $es_has_missing = 0;
368 $es_conf = Koha::SearchEngine::Elasticsearch::_read_configuration();
371 if ( ref($_) eq 'Koha::Exceptions::Config::MissingEntry' ) {
372 $template->param( elasticsearch_fatal_config_error => $_->message );
373 $es_config_error = 1;
376 if ( !$es_config_error ) {
378 my $biblios_index_name = $es_conf->{index_name} . "_" . $Koha::SearchEngine::BIBLIOS_INDEX;
379 my $authorities_index_name = $es_conf->{index_name} . "_" . $Koha::SearchEngine::AUTHORITIES_INDEX;
381 my @indexes = ($biblios_index_name, $authorities_index_name);
382 # TODO: When new indexes get added, we could have other ways to
383 # fetch the list of available indexes (e.g. plugins, etc)
384 $es_status->{nodes} = $es_conf->{nodes};
385 my $es = Search::Elasticsearch->new({ nodes => $es_conf->{nodes} });
386 my $es_status->{version} = $es->info->{version}->{number};
388 foreach my $index ( @indexes ) {
391 $index_count = $es->indices->stats( index => $index )
392 ->{_all}{primaries}{docs}{count};
395 if ( ref($_) eq 'Search::Elasticsearch::Error::Missing' ) {
396 push @{ $es_status->{errors} }, "Index not found ($index)";
399 elsif ( ref($_) eq 'Search::Elasticsearch::Error::NoNodes' ) {
403 # TODO: when time comes, we will cover more use cases
409 my $missing_count = 0;
410 if ( $index eq $biblios_index_name ) {
411 $db_count = Koha::Biblios->search->count;
412 } elsif ( $index eq $authorities_index_name ) {
413 $db_count = Koha::Authorities->search->count;
415 if ( $db_count != -1 && $index_count != -1 ) {
416 $missing_count = $db_count - $index_count;
417 $es_has_missing = 1 if $missing_count > 0;
419 push @{ $es_status->{indexes} },
421 index_name => $index,
422 index_count => $index_count,
423 db_count => $db_count,
424 missing_count => $missing_count,
427 $es_status->{running} = $es_running;
430 elasticsearch_status => $es_status,
431 elasticsearch_has_missing => $es_has_missing,
436 if ( C4::Context->preference('RESTOAuth2ClientCredentials') ) {
437 # Do we have the required deps?
438 unless ( can_load( modules => { 'Net::OAuth2::AuthorizationServer' => undef }) ) {
439 $template->param( oauth2_missing_deps => 1 );
443 # Sco Patron should not contain any other perms than circulate => self_checkout
444 if ( C4::Context->preference('WebBasedSelfCheck')
445 and C4::Context->preference('AutoSelfCheckAllowed')
447 my $userid = C4::Context->preference('AutoSelfCheckID');
448 my $all_permissions = C4::Auth::get_user_subpermissions( $userid );
449 my ( $has_self_checkout_perm, $has_other_permissions );
450 while ( my ( $module, $permissions ) = each %$all_permissions ) {
451 if ( $module eq 'self_check' ) {
452 while ( my ( $permission, $flag ) = each %$permissions ) {
453 if ( $permission eq 'self_checkout_module' ) {
454 $has_self_checkout_perm = 1;
456 $has_other_permissions = 1;
460 $has_other_permissions = 1;
464 AutoSelfCheckPatronDoesNotHaveSelfCheckPerm => not ( $has_self_checkout_perm ),
465 AutoSelfCheckPatronHasTooManyPerm => $has_other_permissions,
469 # Test YAML system preferences
470 # FIXME: This is list of current YAML formatted prefs, should by type of preference
472 "BibtexExportAdditionalFields",
473 "ItemsDeniedRenewal",
475 "MarcItemFieldsToOrder",
477 "RisExportAdditionalFields",
478 "UpdateitemLocationOnCheckin",
479 "UpdateItemWhenLostFromHoldList",
480 "UpdateNotForLoanStatusOnCheckin",
481 "UpdateNotForLoanStatusOnCheckout",
484 foreach my $syspref (@yaml_prefs) {
485 my $yaml = C4::Context->preference( $syspref );
487 eval { YAML::XS::Load( Encode::encode_utf8("$yaml\n\n") ); };
489 push @bad_yaml_prefs, $syspref;
493 $template->param( 'bad_yaml_prefs' => \@bad_yaml_prefs ) if @bad_yaml_prefs;
496 my $dbh = C4::Context->dbh;
497 my $patrons = $dbh->selectall_arrayref(
498 q|select b.borrowernumber from borrowers b join deletedborrowers db on b.borrowernumber=db.borrowernumber|,
501 my $biblios = $dbh->selectall_arrayref(
502 q|select b.biblionumber from biblio b join deletedbiblio db on b.biblionumber=db.biblionumber|,
505 my $items = $dbh->selectall_arrayref(
506 q|select i.itemnumber from items i join deleteditems di on i.itemnumber=di.itemnumber|,
509 my $checkouts = $dbh->selectall_arrayref(
510 q|select i.issue_id from issues i join old_issues oi on i.issue_id=oi.issue_id|,
513 my $holds = $dbh->selectall_arrayref(
514 q|select r.reserve_id from reserves r join old_reserves o on r.reserve_id=o.reserve_id|,
517 if ( @$patrons or @$biblios or @$items or @$checkouts or @$holds ) {
520 ai_patrons => $patrons,
521 ai_biblios => $biblios,
523 ai_checkouts => $checkouts,
531 my $dbh = C4::Context->dbh;
532 my $units = Koha::CirculationRules->search({ rule_name => 'lengthunit', rule_value => { -not_in => ['days', 'hours'] } });
534 if ( $units->count ) {
536 warnIssuingRules => 1,
542 # Guarantor relationships warnings
544 my $dbh = C4::Context->dbh;
545 my ($bad_relationships_count) = $dbh->selectall_arrayref(q{
548 SELECT relationship FROM borrower_relationships WHERE relationship='_bad_data'
550 SELECT relationship FROM borrowers WHERE relationship='_bad_data') a
553 $bad_relationships_count = $bad_relationships_count->[0]->[0];
555 my $existing_relationships = $dbh->selectall_arrayref(q{
556 SELECT DISTINCT(relationship)
558 SELECT relationship FROM borrower_relationships WHERE relationship IS NOT NULL
560 SELECT relationship FROM borrowers WHERE relationship IS NOT NULL) a
563 my %valid_relationships = map { $_ => 1 } split( /,|\|/, C4::Context->preference('borrowerRelationship') );
564 $valid_relationships{ _bad_data } = 1; # we handle this case in another way
566 my $wrong_relationships = [ grep { !$valid_relationships{ $_->[0] } } @{$existing_relationships} ];
567 if ( @$wrong_relationships or $bad_relationships_count ) {
570 warnRelationships => 1,
573 if ( $wrong_relationships ) {
575 wrong_relationships => $wrong_relationships
578 if ($bad_relationships_count) {
580 bad_relationships_count => $bad_relationships_count,
587 # Test 'bcrypt_settings' config for Pseudonymization
588 $template->param( config_bcrypt_settings_no_set => 1 )
589 if C4::Context->preference('Pseudonymization')
590 and not C4::Context->config('bcrypt_settings');
594 my @frameworkcodes = Koha::BiblioFrameworks->search->get_column('frameworkcode');
595 my @hidden_biblionumbers;
596 push @frameworkcodes, ""; # it's not in the biblio_frameworks table!
597 my $no_FA_framework = 1;
598 for my $frameworkcode ( @frameworkcodes ) {
599 $no_FA_framework = 0 if $frameworkcode eq 'FA';
600 my $shouldhidemarc_opac = Koha::Filter::MARC::ViewPolicy->should_hide_marc(
602 frameworkcode => $frameworkcode,
606 push @hidden_biblionumbers, { frameworkcode => $frameworkcode, interface => 'opac' }
607 if $shouldhidemarc_opac->{biblionumber};
609 my $shouldhidemarc_intranet = Koha::Filter::MARC::ViewPolicy->should_hide_marc(
611 frameworkcode => $frameworkcode,
612 interface => "intranet"
615 push @hidden_biblionumbers, { frameworkcode => $frameworkcode, interface => 'intranet' }
616 if $shouldhidemarc_intranet->{biblionumber};
618 $template->param( warnHiddenBiblionumbers => \@hidden_biblionumbers );
619 $template->param( warnFastCataloging => $no_FA_framework );
623 # BackgroundJob - test connection to message broker
625 Koha::BackgroundJob->connect;
629 $template->param( warnConnectBroker => $@ );
633 #BZ 28267: Warn administrators if there are database rows with a format other than 'DYNAMIC'
635 $template->param( warnDbRowFormat => C4::Installer->has_non_dynamic_row_format );
638 my %versions = C4::Context::get_versions();
641 kohaVersion => $versions{'kohaVersion'},
642 osVersion => $versions{'osVersion'},
643 perlPath => $perl_path,
644 perlVersion => $versions{'perlVersion'},
645 perlIncPath => [ map { perlinc => $_ }, @INC ],
646 mysqlVersion => $versions{'mysqlVersion'},
647 apacheVersion => $versions{'apacheVersion'},
648 zebraVersion => $zebraVersion,
649 prefRequireChoosingExistingAuthority => $prefRequireChoosingExistingAuthority,
650 prefAutoCreateAuthorities => $prefAutoCreateAuthorities,
651 warnPrefRequireChoosingExistingAuthority => $warnPrefRequireChoosingExistingAuthority,
652 warnPrefEasyAnalyticalRecords => $warnPrefEasyAnalyticalRecords,
653 warnPrefAnonymousPatronOPACPrivacy => $warnPrefAnonymousPatronOPACPrivacy,
654 warnPrefAnonymousPatronAnonSuggestions => $warnPrefAnonymousPatronAnonSuggestions,
655 warnPrefAnonymousPatronOPACPrivacy_PatronDoesNotExist => $warnPrefAnonymousPatronOPACPrivacy_PatronDoesNotExist,
656 warnPrefAnonymousPatronAnonSuggestions_PatronDoesNotExist => $warnPrefAnonymousPatronAnonSuggestions_PatronDoesNotExist,
657 warnPrefKohaAdminEmailAddress => $warnPrefKohaAdminEmailAddress,
658 warnPrefOpacHiddenItems => $warnPrefOpacHiddenItems,
659 errZebraConnection => $errZebraConnection,
660 warnIsRootUser => $warnIsRootUser,
661 warnNoActiveCurrency => $warnNoActiveCurrency,
662 warnNoTemplateCaching => ( C4::Context->config('template_cache_dir') ? 0 : 1 ),
663 xml_config_warnings => \@xml_config_warnings,
664 warnStatisticsFieldsError => $warnStatisticsFieldsError,
669 my $perl_modules = C4::Installer::PerlModules->new;
670 $perl_modules->versions_info;
672 my @pm_types = qw(missing_pm upgrade_pm current_pm);
674 foreach my $pm_type(@pm_types) {
675 my $modules = $perl_modules->get_attr($pm_type);
676 foreach (@$modules) {
677 my ($module, $stats) = each %$_;
682 version => $stats->{'cur_ver'},
683 missing => ($pm_type eq 'missing_pm' ? 1 : 0),
684 upgrade => ($pm_type eq 'upgrade_pm' ? 1 : 0),
685 current => ($pm_type eq 'current_pm' ? 1 : 0),
686 require => $stats->{'required'},
687 reqversion => $stats->{'min_ver'},
688 maxversion => $stats->{'max_ver'},
689 excversion => $stats->{'exc_ver'}
695 @components = sort {$a->{'name'} cmp $b->{'name'}} @components;
700 foreach (@components) {
702 unless (++$counter % 4) {
703 push (@$table, {row => $row});
707 # Processing the last line (if there are any modules left)
708 if (scalar(@$row) > 0) {
709 # Extending $row to the table size
711 # Pushing the last line
712 push (@$table, {row => $row});
716 $template->param( table => $table );
719 ## ------------------------------------------
720 ## Koha contributions
722 if ( defined C4::Context->config('docdir') ) {
723 $docdir = C4::Context->config('docdir');
725 # if no <docdir> is defined in koha-conf.xml, use the default location
726 # this is a work-around to stop breakage on upgraded Kohas, bug 8911
727 $docdir = C4::Context->config('intranetdir') . '/docs';
732 -e "$docdir" . "/teams.yaml"
733 ? YAML::XS::LoadFile( "$docdir" . "/teams.yaml" )
735 my $dev_team = (sort {$b <=> $a} (keys %{$teams->{team}}))[0];
736 my $short_version = substr($versions{'kohaVersion'},0,5);
737 my $minor = substr($versions{'kohaVersion'},3,2);
738 my $development_version = ( $minor eq '05' || $minor eq '11' ) ? 0 : 1;
740 $template->param( short_version => $short_version );
741 $template->param( development_version => $development_version );
745 -e "$docdir" . "/contributors.yaml"
746 ? YAML::XS::LoadFile( "$docdir" . "/contributors.yaml" )
748 delete $contributors->{_others_};
749 for my $version ( sort { $a <=> $b } keys %{$teams->{team}} ) {
750 for my $role ( keys %{ $teams->{team}->{$version} } ) {
751 my $normalized_role = "$role";
752 $normalized_role =~ s/s$//;
753 if ( ref( $teams->{team}->{$version}->{$role} ) eq 'ARRAY' ) {
754 for my $contributor ( @{ $teams->{team}->{$version}->{$role} } ) {
755 my $name = $contributor->{name};
756 # Add role to contributors
757 push @{ $contributors->{$name}->{roles}->{$normalized_role} },
759 # Add openhub to teams
760 if ( exists( $contributors->{$name}->{openhub} ) ) {
761 $contributor->{openhub} = $contributors->{$name}->{openhub};
765 elsif ( $role eq 'release_date' ) {
766 $teams->{team}->{$version}->{$role} = DateTime->from_epoch( epoch => $teams->{team}->{$version}->{$role});
768 elsif ( $role eq 'codename' ) {
769 if ( $version == $short_version ) {
770 $codename = $teams->{team}->{$version}->{$role};
775 my $name = $teams->{team}->{$version}->{$role}->{name};
776 # Add role to contributors
777 push @{ $contributors->{$name}->{roles}->{$normalized_role} },
779 # Add openhub to teams
780 if ( exists( $contributors->{$name}->{openhub} ) ) {
781 $teams->{team}->{$version}->{$role}->{openhub} =
782 $contributors->{$name}->{openhub};
788 ## Create last name ordered array of people from contributors
790 { name => $_, ( $contributors->{$_} ? %{ $contributors->{$_} } : () ) }
792 my ($alast) = $a =~ /(\S+)$/;
793 my ($blast) = $b =~ /(\S+)$/;
794 my $cmp = lc($alast||"") cmp lc($blast||"");
797 my ($a2last) = $a =~ /(\S+)\s\S+$/;
798 my ($b2last) = $b =~ /(\S+)\s\S+$/;
799 lc($a2last||"") cmp lc($b2last||"");
800 } keys %$contributors;
802 $template->param( kohaCodename => $codename);
803 $template->param( contributors => \@people );
804 $template->param( maintenance_team => $teams->{team}->{$dev_team} );
805 $template->param( release_team => $teams->{team}->{$short_version} );
808 if ( open( my $file, "<:encoding(UTF-8)", "$docdir" . "/history.txt" ) ) {
818 shift @lines; #remove header row
821 my ( $epoch, $date, $desc, $tag ) = split(/\t/);
822 if(!$desc && $date=~ /(?<=\d{4})\s+/) {
823 ($date, $desc)= ($`, $');
835 #foreach my $row2 (@rows2) {
838 push( @$table2, { row2 => $row2 } );
842 $template->param( table2 => $table2 );
844 $template->param( timeline_read_error => 1 );
847 output_html_with_http_headers $query, $cookie, $template->output;