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 );
40 use C4::Installer::PerlModules;
43 use Koha::DateUtils qw( dt_from_string output_pref );
44 use Koha::Acquisition::Currencies;
45 use Koha::Authorities;
46 use Koha::BackgroundJob;
47 use Koha::BiblioFrameworks;
50 use Koha::Patron::Categories;
53 use Koha::Config::SysPrefs;
54 use Koha::Illrequest::Config;
55 use Koha::SearchEngine::Elasticsearch;
57 use Koha::Filter::MARC::ViewPolicy;
59 use C4::Members::Statistics;
62 my ( $template, $loggedinuser, $cookie ) = get_template_and_user(
64 template_name => "about.tt",
67 flagsrequired => { catalogue => 1 },
71 my $config_timezone = C4::Context->config('timezone') // '';
72 my $config_invalid = !DateTime::TimeZone->is_valid_name( $config_timezone );
73 my $env_timezone = $ENV{TZ} // '';
74 my $env_invalid = !DateTime::TimeZone->is_valid_name( $env_timezone );
75 my $actual_bad_tz_fallback = 0;
77 if ( $config_timezone ne '' &&
80 $actual_bad_tz_fallback = 1;
82 elsif ( $config_timezone eq '' &&
83 $env_timezone ne '' &&
85 # No config, but bad ENV{TZ}
86 $actual_bad_tz_fallback = 1;
90 actual => C4::Context->tz->name,
91 actual_bad_tz_fallback => $actual_bad_tz_fallback,
92 config => $config_timezone,
93 config_invalid => $config_invalid,
94 environment => $env_timezone,
95 environment_invalid => $env_invalid
99 my $log4perl_config = C4::Context->config("log4perl_conf");
101 if ( ! $log4perl_config ) {
102 push @log4perl_errors, 'missing_config_entry'
105 my @lines = read_file($log4perl_config) or push @log4perl_errors, 'cannot_read_config_file';
106 for my $line ( @lines ) {
107 next unless $line =~ m|log4perl.appender.\w+.filename=(.*)|;
108 push @log4perl_errors, 'logfile_not_writable' unless -w $1;
111 eval {Koha::Logger->get};
112 push @log4perl_errors, 'cannot_init_module' and warn $@ if $@;
113 $template->param( log4perl_errors => @log4perl_errors );
117 time_zone => $time_zone,
118 current_date_and_time => output_pref({ dt => dt_from_string(), dateformat => 'iso' })
123 $perl_path .= $Config{_exe} unless $perl_path =~ m/$Config{_exe}$/i;
126 my $zebraVersion = `zebraidx -V`;
128 # Check running PSGI env
129 if ( C4::Context->psgi_env ) {
132 psgi_server => ($ENV{ PLACK_ENV }) ? "Plack ($ENV{PLACK_ENV})" :
133 ($ENV{ MOD_PERL }) ? "mod_perl ($ENV{MOD_PERL})" :
138 # Memcached configuration
139 my $memcached_servers = $ENV{MEMCACHED_SERVERS} || C4::Context->config('memcached_servers');
140 my $memcached_namespace = $ENV{MEMCACHED_NAMESPACE} || C4::Context->config('memcached_namespace') // 'koha';
142 my $cache = Koha::Caches->get_instance;
143 my $effective_caching_method = ref($cache->cache);
144 # Memcached may have been running when plack has been initialized but could have been stopped since
145 # FIXME What are the consequences of that??
146 my $is_memcached_still_active = $cache->set_in_cache('test_for_about_page', "just a simple value");
148 my $where_is_memcached_config = 'nowhere';
149 if ( $ENV{MEMCACHED_SERVERS} and C4::Context->config('memcached_servers') ) {
150 $where_is_memcached_config = 'both';
151 } elsif ( $ENV{MEMCACHED_SERVERS} and not C4::Context->config('memcached_servers') ) {
152 $where_is_memcached_config = 'ENV_only';
153 } elsif ( C4::Context->config('memcached_servers') ) {
154 $where_is_memcached_config = 'config_only';
158 effective_caching_method => $effective_caching_method,
159 memcached_servers => $memcached_servers,
160 memcached_namespace => $memcached_namespace,
161 is_memcached_still_active => $is_memcached_still_active,
162 where_is_memcached_config => $where_is_memcached_config,
163 memcached_running => Koha::Caches->get_instance->memcached_cache,
166 # Additional system information for warnings
168 my $warnStatisticsFieldsError;
169 my $prefStatisticsFields = C4::Context->preference('StatisticsFields');
170 if ($prefStatisticsFields) {
171 $warnStatisticsFieldsError = $prefStatisticsFields
172 unless ( $prefStatisticsFields eq C4::Members::Statistics->get_fields() );
175 my $prefAutoCreateAuthorities = C4::Context->preference('AutoCreateAuthorities');
176 my $prefRequireChoosingExistingAuthority = C4::Context->preference('RequireChoosingExistingAuthority');
177 my $warnPrefRequireChoosingExistingAuthority = ( !$prefAutoCreateAuthorities && ( !$prefRequireChoosingExistingAuthority) );
179 my $prefEasyAnalyticalRecords = C4::Context->preference('EasyAnalyticalRecords');
180 my $prefUseControlNumber = C4::Context->preference('UseControlNumber');
181 my $warnPrefEasyAnalyticalRecords = ( $prefEasyAnalyticalRecords && $prefUseControlNumber );
183 my $AnonymousPatron = C4::Context->preference('AnonymousPatron');
184 my $warnPrefAnonymousPatronOPACPrivacy = (
185 C4::Context->preference('OPACPrivacy')
186 and not $AnonymousPatron
188 my $warnPrefAnonymousPatronAnonSuggestions = (
189 C4::Context->preference('AnonSuggestions')
190 and not $AnonymousPatron
193 my $anonymous_patron = Koha::Patrons->find( $AnonymousPatron );
194 my $warnPrefAnonymousPatronAnonSuggestions_PatronDoesNotExist = ( $AnonymousPatron && C4::Context->preference('AnonSuggestions') && not $anonymous_patron );
196 my $warnPrefAnonymousPatronOPACPrivacy_PatronDoesNotExist = ( not $anonymous_patron and Koha::Patrons->search({ privacy => 2 })->count );
198 my $warnPrefKohaAdminEmailAddress = !Koha::Email->is_valid(C4::Context->preference('KohaAdminEmailAddress'));
200 my $c = Koha::Items->filter_by_visible_in_opac->count;
201 my @warnings = C4::Context->dbh->selectrow_array('SHOW WARNINGS');
202 my $warnPrefOpacHiddenItems = $warnings[2];
204 my $invalid_yesno = Koha::Config::SysPrefs->search(
207 value => { -or => { 'is' => undef, -not_in => [ "1", "0" ] } }
210 $template->param( invalid_yesno => $invalid_yesno );
212 my $errZebraConnection = C4::Context->Zconn("biblioserver",0)->errcode();
214 my $warnIsRootUser = (! $loggedinuser);
216 my $warnNoActiveCurrency = (! defined Koha::Acquisition::Currencies->get_active);
218 my @xml_config_warnings;
220 if ( C4::Context->config('zebra_bib_index_mode')
221 and C4::Context->config('zebra_bib_index_mode') eq 'grs1' )
223 push @xml_config_warnings, { error => 'zebra_bib_index_mode_is_grs1' };
226 if ( C4::Context->config('zebra_auth_index_mode')
227 and C4::Context->config('zebra_auth_index_mode') eq 'grs1' )
229 push @xml_config_warnings, { error => 'zebra_auth_index_mode_is_grs1' };
232 my $authorityserver = C4::Context->zebraconfig('authorityserver');
233 if( ( C4::Context->config('zebra_auth_index_mode')
234 and C4::Context->config('zebra_auth_index_mode') eq 'dom' )
235 && ( $authorityserver->{config} !~ /zebra-authorities-dom.cfg/ ) )
237 push @xml_config_warnings, {
238 error => 'zebra_auth_index_mode_mismatch_warn'
242 if ( ! defined C4::Context->config('log4perl_conf') ) {
243 push @xml_config_warnings, {
244 error => 'log4perl_entry_missing'
248 if ( ! defined C4::Context->config('lockdir') ) {
249 push @xml_config_warnings, {
250 error => 'lockdir_entry_missing'
254 unless ( -w C4::Context->config('lockdir') ) {
255 push @xml_config_warnings, {
256 error => 'lockdir_not_writable',
257 lockdir => C4::Context->config('lockdir')
262 if ( ! defined C4::Context->config('upload_path') ) {
263 if ( Koha::Config::SysPrefs->find('OPACBaseURL')->value ) {
264 # OPACBaseURL seems to be set
265 push @xml_config_warnings, {
266 error => 'uploadpath_entry_missing'
269 push @xml_config_warnings, {
270 error => 'uploadpath_and_opacbaseurl_entry_missing'
275 if ( ! C4::Context->config('tmp_path') ) {
276 my $temporary_directory = C4::Context::temporary_directory;
277 push @xml_config_warnings, {
278 error => 'tmp_path_missing',
279 effective_tmp_dir => $temporary_directory,
283 if( ! C4::Context->config('encryption_key') ) {
284 push @xml_config_warnings, { error => 'encryption_key_missing' };
287 # Test Zebra facets configuration
288 if ( !defined C4::Context->config('use_zebra_facets') ) {
289 push @xml_config_warnings, { error => 'use_zebra_facets_entry_missing' };
293 if ( C4::Context->preference('ILLModule') ) {
294 my $warnILLConfiguration = 0;
295 my $ill_config_from_file = C4::Context->config("interlibrary_loans");
296 my $ill_config = Koha::Illrequest::Config->new;
298 my $available_ill_backends =
299 ( scalar @{ $ill_config->available_backends } > 0 );
302 if ( !$available_ill_backends ) {
303 $template->param( no_ill_backends => 1 );
304 $warnILLConfiguration = 1;
308 if ( !Koha::Patron::Categories->find($ill_config->partner_code) ) {
309 $template->param( ill_partner_code_doesnt_exist => $ill_config->partner_code );
310 $warnILLConfiguration = 1;
313 if ( !$ill_config_from_file->{partner_code} ) {
314 # partner code not defined
315 $template->param( ill_partner_code_not_defined => 1 );
316 $warnILLConfiguration = 1;
320 if ( !$ill_config_from_file->{branch} ) {
322 $template->param( ill_branch_not_defined => 1 );
323 $warnILLConfiguration = 1;
326 $template->param( warnILLConfiguration => $warnILLConfiguration );
328 unless ( can_run('weasyprint') ) {
329 $template->param( weasyprint_missing => 1 );
335 OPACXSLTDetailsDisplay
337 OPACXSLTResultsDisplay
343 for my $p ( @xslt_prefs ) {
344 my $xsl_filename = C4::XSLT::get_xsl_filename( $p );
345 next if -e $xsl_filename;
349 value => C4::Context->preference("$p"),
350 filename => $xsl_filename
354 $template->param( warnXSLT => \@warnXSLT ) if @warnXSLT;
357 if ( C4::Context->preference('SearchEngine') eq 'Elasticsearch' ) {
358 # Check ES configuration health and runtime status
363 my $es_has_missing = 0;
367 $es_conf = Koha::SearchEngine::Elasticsearch::_read_configuration();
370 if ( ref($_) eq 'Koha::Exceptions::Config::MissingEntry' ) {
371 $template->param( elasticsearch_fatal_config_error => $_->message );
372 $es_config_error = 1;
375 if ( !$es_config_error ) {
377 my $biblios_index_name = $es_conf->{index_name} . "_" . $Koha::SearchEngine::BIBLIOS_INDEX;
378 my $authorities_index_name = $es_conf->{index_name} . "_" . $Koha::SearchEngine::AUTHORITIES_INDEX;
380 my @indexes = ($biblios_index_name, $authorities_index_name);
381 # TODO: When new indexes get added, we could have other ways to
382 # fetch the list of available indexes (e.g. plugins, etc)
383 $es_status->{nodes} = $es_conf->{nodes};
384 my $es = Search::Elasticsearch->new({ nodes => $es_conf->{nodes} });
385 my $es_status->{version} = $es->info->{version}->{number};
387 foreach my $index ( @indexes ) {
390 $index_count = $es->indices->stats( index => $index )
391 ->{_all}{primaries}{docs}{count};
394 if ( ref($_) eq 'Search::Elasticsearch::Error::Missing' ) {
395 push @{ $es_status->{errors} }, "Index not found ($index)";
398 elsif ( ref($_) eq 'Search::Elasticsearch::Error::NoNodes' ) {
402 # TODO: when time comes, we will cover more use cases
408 my $missing_count = 0;
409 if ( $index eq $biblios_index_name ) {
410 $db_count = Koha::Biblios->search->count;
411 } elsif ( $index eq $authorities_index_name ) {
412 $db_count = Koha::Authorities->search->count;
414 if ( $db_count != -1 && $index_count != -1 ) {
415 $missing_count = $db_count - $index_count;
416 $es_has_missing = 1 if $missing_count > 0;
418 push @{ $es_status->{indexes} },
420 index_name => $index,
421 index_count => $index_count,
422 db_count => $db_count,
423 missing_count => $missing_count,
426 $es_status->{running} = $es_running;
429 elasticsearch_status => $es_status,
430 elasticsearch_has_missing => $es_has_missing,
435 if ( C4::Context->preference('RESTOAuth2ClientCredentials') ) {
436 # Do we have the required deps?
437 unless ( can_load( modules => { 'Net::OAuth2::AuthorizationServer' => undef }) ) {
438 $template->param( oauth2_missing_deps => 1 );
442 # Sco Patron should not contain any other perms than circulate => self_checkout
443 if ( C4::Context->preference('WebBasedSelfCheck')
444 and C4::Context->preference('AutoSelfCheckAllowed')
446 my $userid = C4::Context->preference('AutoSelfCheckID');
447 my $all_permissions = C4::Auth::get_user_subpermissions( $userid );
448 my ( $has_self_checkout_perm, $has_other_permissions );
449 while ( my ( $module, $permissions ) = each %$all_permissions ) {
450 if ( $module eq 'self_check' ) {
451 while ( my ( $permission, $flag ) = each %$permissions ) {
452 if ( $permission eq 'self_checkout_module' ) {
453 $has_self_checkout_perm = 1;
455 $has_other_permissions = 1;
459 $has_other_permissions = 1;
463 AutoSelfCheckPatronDoesNotHaveSelfCheckPerm => not ( $has_self_checkout_perm ),
464 AutoSelfCheckPatronHasTooManyPerm => $has_other_permissions,
468 # Test YAML system preferences
469 # FIXME: This is list of current YAML formatted prefs, should by type of preference
471 "BibtexExportAdditionalFields",
472 "ItemsDeniedRenewal",
474 "MarcItemFieldsToOrder",
476 "RisExportAdditionalFields",
477 "UpdateitemLocationOnCheckin",
478 "UpdateItemWhenLostFromHoldList",
479 "UpdateNotForLoanStatusOnCheckin",
480 "UpdateNotForLoanStatusOnCheckout",
483 foreach my $syspref (@yaml_prefs) {
484 my $yaml = C4::Context->preference( $syspref );
486 eval { YAML::XS::Load( Encode::encode_utf8("$yaml\n\n") ); };
488 push @bad_yaml_prefs, $syspref;
492 $template->param( 'bad_yaml_prefs' => \@bad_yaml_prefs ) if @bad_yaml_prefs;
495 my $dbh = C4::Context->dbh;
496 my $patrons = $dbh->selectall_arrayref(
497 q|select b.borrowernumber from borrowers b join deletedborrowers db on b.borrowernumber=db.borrowernumber|,
500 my $biblios = $dbh->selectall_arrayref(
501 q|select b.biblionumber from biblio b join deletedbiblio db on b.biblionumber=db.biblionumber|,
504 my $items = $dbh->selectall_arrayref(
505 q|select i.itemnumber from items i join deleteditems di on i.itemnumber=di.itemnumber|,
508 my $checkouts = $dbh->selectall_arrayref(
509 q|select i.issue_id from issues i join old_issues oi on i.issue_id=oi.issue_id|,
512 my $holds = $dbh->selectall_arrayref(
513 q|select r.reserve_id from reserves r join old_reserves o on r.reserve_id=o.reserve_id|,
516 if ( @$patrons or @$biblios or @$items or @$checkouts or @$holds ) {
519 ai_patrons => $patrons,
520 ai_biblios => $biblios,
522 ai_checkouts => $checkouts,
530 my $dbh = C4::Context->dbh;
531 my $units = Koha::CirculationRules->search({ rule_name => 'lengthunit', rule_value => { -not_in => ['days', 'hours'] } });
533 if ( $units->count ) {
535 warnIssuingRules => 1,
541 # Guarantor relationships warnings
543 my $dbh = C4::Context->dbh;
544 my ($bad_relationships_count) = $dbh->selectall_arrayref(q{
547 SELECT relationship FROM borrower_relationships WHERE relationship='_bad_data'
549 SELECT relationship FROM borrowers WHERE relationship='_bad_data') a
552 $bad_relationships_count = $bad_relationships_count->[0]->[0];
554 my $existing_relationships = $dbh->selectall_arrayref(q{
555 SELECT DISTINCT(relationship)
557 SELECT relationship FROM borrower_relationships WHERE relationship IS NOT NULL
559 SELECT relationship FROM borrowers WHERE relationship IS NOT NULL) a
562 my %valid_relationships = map { $_ => 1 } split( /,|\|/, C4::Context->preference('borrowerRelationship') );
563 $valid_relationships{ _bad_data } = 1; # we handle this case in another way
565 my $wrong_relationships = [ grep { !$valid_relationships{ $_->[0] } } @{$existing_relationships} ];
566 if ( @$wrong_relationships or $bad_relationships_count ) {
569 warnRelationships => 1,
572 if ( $wrong_relationships ) {
574 wrong_relationships => $wrong_relationships
577 if ($bad_relationships_count) {
579 bad_relationships_count => $bad_relationships_count,
586 # Test 'bcrypt_settings' config for Pseudonymization
587 $template->param( config_bcrypt_settings_no_set => 1 )
588 if C4::Context->preference('Pseudonymization')
589 and not C4::Context->config('bcrypt_settings');
593 my @frameworkcodes = Koha::BiblioFrameworks->search->get_column('frameworkcode');
594 my @hidden_biblionumbers;
595 push @frameworkcodes, ""; # it's not in the biblio_frameworks table!
596 my $no_FA_framework = 1;
597 for my $frameworkcode ( @frameworkcodes ) {
598 $no_FA_framework = 0 if $frameworkcode eq 'FA';
599 my $shouldhidemarc_opac = Koha::Filter::MARC::ViewPolicy->should_hide_marc(
601 frameworkcode => $frameworkcode,
605 push @hidden_biblionumbers, { frameworkcode => $frameworkcode, interface => 'opac' }
606 if $shouldhidemarc_opac->{biblionumber};
608 my $shouldhidemarc_intranet = Koha::Filter::MARC::ViewPolicy->should_hide_marc(
610 frameworkcode => $frameworkcode,
611 interface => "intranet"
614 push @hidden_biblionumbers, { frameworkcode => $frameworkcode, interface => 'intranet' }
615 if $shouldhidemarc_intranet->{biblionumber};
617 $template->param( warnHiddenBiblionumbers => \@hidden_biblionumbers );
618 $template->param( warnFastCataloging => $no_FA_framework );
622 # BackgroundJob - test connection to message broker
624 Koha::BackgroundJob->connect;
628 $template->param( warnConnectBroker => $@ );
632 my %versions = C4::Context::get_versions();
635 kohaVersion => $versions{'kohaVersion'},
636 osVersion => $versions{'osVersion'},
637 perlPath => $perl_path,
638 perlVersion => $versions{'perlVersion'},
639 perlIncPath => [ map { perlinc => $_ }, @INC ],
640 mysqlVersion => $versions{'mysqlVersion'},
641 apacheVersion => $versions{'apacheVersion'},
642 zebraVersion => $zebraVersion,
643 prefRequireChoosingExistingAuthority => $prefRequireChoosingExistingAuthority,
644 prefAutoCreateAuthorities => $prefAutoCreateAuthorities,
645 warnPrefRequireChoosingExistingAuthority => $warnPrefRequireChoosingExistingAuthority,
646 warnPrefEasyAnalyticalRecords => $warnPrefEasyAnalyticalRecords,
647 warnPrefAnonymousPatronOPACPrivacy => $warnPrefAnonymousPatronOPACPrivacy,
648 warnPrefAnonymousPatronAnonSuggestions => $warnPrefAnonymousPatronAnonSuggestions,
649 warnPrefAnonymousPatronOPACPrivacy_PatronDoesNotExist => $warnPrefAnonymousPatronOPACPrivacy_PatronDoesNotExist,
650 warnPrefAnonymousPatronAnonSuggestions_PatronDoesNotExist => $warnPrefAnonymousPatronAnonSuggestions_PatronDoesNotExist,
651 warnPrefKohaAdminEmailAddress => $warnPrefKohaAdminEmailAddress,
652 warnPrefOpacHiddenItems => $warnPrefOpacHiddenItems,
653 errZebraConnection => $errZebraConnection,
654 warnIsRootUser => $warnIsRootUser,
655 warnNoActiveCurrency => $warnNoActiveCurrency,
656 warnNoTemplateCaching => ( C4::Context->config('template_cache_dir') ? 0 : 1 ),
657 xml_config_warnings => \@xml_config_warnings,
658 warnStatisticsFieldsError => $warnStatisticsFieldsError,
663 my $perl_modules = C4::Installer::PerlModules->new;
664 $perl_modules->versions_info;
666 my @pm_types = qw(missing_pm upgrade_pm current_pm);
668 foreach my $pm_type(@pm_types) {
669 my $modules = $perl_modules->get_attr($pm_type);
670 foreach (@$modules) {
671 my ($module, $stats) = each %$_;
676 version => $stats->{'cur_ver'},
677 missing => ($pm_type eq 'missing_pm' ? 1 : 0),
678 upgrade => ($pm_type eq 'upgrade_pm' ? 1 : 0),
679 current => ($pm_type eq 'current_pm' ? 1 : 0),
680 require => $stats->{'required'},
681 reqversion => $stats->{'min_ver'},
682 maxversion => $stats->{'max_ver'},
683 excversion => $stats->{'exc_ver'}
689 @components = sort {$a->{'name'} cmp $b->{'name'}} @components;
694 foreach (@components) {
696 unless (++$counter % 4) {
697 push (@$table, {row => $row});
701 # Processing the last line (if there are any modules left)
702 if (scalar(@$row) > 0) {
703 # Extending $row to the table size
705 # Pushing the last line
706 push (@$table, {row => $row});
710 $template->param( table => $table );
713 ## ------------------------------------------
714 ## Koha contributions
716 if ( defined C4::Context->config('docdir') ) {
717 $docdir = C4::Context->config('docdir');
719 # if no <docdir> is defined in koha-conf.xml, use the default location
720 # this is a work-around to stop breakage on upgraded Kohas, bug 8911
721 $docdir = C4::Context->config('intranetdir') . '/docs';
726 -e "$docdir" . "/teams.yaml"
727 ? YAML::XS::LoadFile( "$docdir" . "/teams.yaml" )
729 my $dev_team = (sort {$b <=> $a} (keys %{$teams->{team}}))[0];
730 my $short_version = substr($versions{'kohaVersion'},0,5);
731 my $minor = substr($versions{'kohaVersion'},3,2);
732 my $development_version = ( $minor eq '05' || $minor eq '11' ) ? 0 : 1;
734 $template->param( short_version => $short_version );
735 $template->param( development_version => $development_version );
739 -e "$docdir" . "/contributors.yaml"
740 ? YAML::XS::LoadFile( "$docdir" . "/contributors.yaml" )
742 delete $contributors->{_others_};
743 for my $version ( sort { $a <=> $b } keys %{$teams->{team}} ) {
744 for my $role ( keys %{ $teams->{team}->{$version} } ) {
745 my $normalized_role = "$role";
746 $normalized_role =~ s/s$//;
747 if ( ref( $teams->{team}->{$version}->{$role} ) eq 'ARRAY' ) {
748 for my $contributor ( @{ $teams->{team}->{$version}->{$role} } ) {
749 my $name = $contributor->{name};
750 # Add role to contributors
751 push @{ $contributors->{$name}->{roles}->{$normalized_role} },
753 # Add openhub to teams
754 if ( exists( $contributors->{$name}->{openhub} ) ) {
755 $contributor->{openhub} = $contributors->{$name}->{openhub};
759 elsif ( $role eq 'release_date' ) {
760 $teams->{team}->{$version}->{$role} = DateTime->from_epoch( epoch => $teams->{team}->{$version}->{$role});
762 elsif ( $role eq 'codename' ) {
763 if ( $version == $short_version ) {
764 $codename = $teams->{team}->{$version}->{$role};
769 my $name = $teams->{team}->{$version}->{$role}->{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 $teams->{team}->{$version}->{$role}->{openhub} =
776 $contributors->{$name}->{openhub};
782 ## Create last name ordered array of people from contributors
784 { name => $_, ( $contributors->{$_} ? %{ $contributors->{$_} } : () ) }
786 my ($alast) = $a =~ /(\S+)$/;
787 my ($blast) = $b =~ /(\S+)$/;
788 my $cmp = lc($alast||"") cmp lc($blast||"");
791 my ($a2last) = $a =~ /(\S+)\s\S+$/;
792 my ($b2last) = $b =~ /(\S+)\s\S+$/;
793 lc($a2last||"") cmp lc($b2last||"");
794 } keys %$contributors;
796 $template->param( kohaCodename => $codename);
797 $template->param( contributors => \@people );
798 $template->param( maintenance_team => $teams->{team}->{$dev_team} );
799 $template->param( release_team => $teams->{team}->{$short_version} );
802 if ( open( my $file, "<:encoding(UTF-8)", "$docdir" . "/history.txt" ) ) {
812 shift @lines; #remove header row
815 my ( $epoch, $date, $desc, $tag ) = split(/\t/);
816 if(!$desc && $date=~ /(?<=\d{4})\s+/) {
817 ($date, $desc)= ($`, $');
829 #foreach my $row2 (@rows2) {
832 push( @$table2, { row2 => $row2 } );
836 $template->param( table2 => $table2 );
838 $template->param( timeline_read_error => 1 );
841 output_html_with_http_headers $query, $cookie, $template->output;