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;
310 if ( !Koha::Patron::Categories->find($ill_config->partner_code) ) {
311 $template->param( ill_partner_code_doesnt_exist => $ill_config->partner_code );
312 $warnILLConfiguration = 1;
315 if ( !$ill_config_from_file->{partner_code} ) {
316 # partner code not defined
317 $template->param( ill_partner_code_not_defined => 1 );
318 $warnILLConfiguration = 1;
322 if ( !$ill_config_from_file->{branch} ) {
324 $template->param( ill_branch_not_defined => 1 );
325 $warnILLConfiguration = 1;
328 $template->param( warnILLConfiguration => $warnILLConfiguration );
330 unless ( can_run('weasyprint') ) {
331 $template->param( weasyprint_missing => 1 );
337 OPACXSLTDetailsDisplay
339 OPACXSLTResultsDisplay
345 for my $p ( @xslt_prefs ) {
346 my $xsl_filename = C4::XSLT::get_xsl_filename( $p );
347 next if -e $xsl_filename;
351 value => C4::Context->preference("$p"),
352 filename => $xsl_filename
356 $template->param( warnXSLT => \@warnXSLT ) if @warnXSLT;
359 if ( C4::Context->preference('SearchEngine') eq 'Elasticsearch' ) {
360 # Check ES configuration health and runtime status
365 my $es_has_missing = 0;
369 $es_conf = Koha::SearchEngine::Elasticsearch::_read_configuration();
372 if ( ref($_) eq 'Koha::Exceptions::Config::MissingEntry' ) {
373 $template->param( elasticsearch_fatal_config_error => $_->message );
374 $es_config_error = 1;
377 if ( !$es_config_error ) {
379 my $biblios_index_name = $es_conf->{index_name} . "_" . $Koha::SearchEngine::BIBLIOS_INDEX;
380 my $authorities_index_name = $es_conf->{index_name} . "_" . $Koha::SearchEngine::AUTHORITIES_INDEX;
382 my @indexes = ($biblios_index_name, $authorities_index_name);
383 # TODO: When new indexes get added, we could have other ways to
384 # fetch the list of available indexes (e.g. plugins, etc)
385 $es_status->{nodes} = $es_conf->{nodes};
386 my $es = Search::Elasticsearch->new({ nodes => $es_conf->{nodes} });
387 my $es_status->{version} = $es->info->{version}->{number};
389 foreach my $index ( @indexes ) {
392 $index_count = $es->indices->stats( index => $index )
393 ->{_all}{primaries}{docs}{count};
396 if ( ref($_) eq 'Search::Elasticsearch::Error::Missing' ) {
397 push @{ $es_status->{errors} }, "Index not found ($index)";
400 elsif ( ref($_) eq 'Search::Elasticsearch::Error::NoNodes' ) {
404 # TODO: when time comes, we will cover more use cases
410 my $missing_count = 0;
411 if ( $index eq $biblios_index_name ) {
412 $db_count = Koha::Biblios->search->count;
413 } elsif ( $index eq $authorities_index_name ) {
414 $db_count = Koha::Authorities->search->count;
416 if ( $db_count != -1 && $index_count != -1 ) {
417 $missing_count = $db_count - $index_count;
418 $es_has_missing = 1 if $missing_count > 0;
420 push @{ $es_status->{indexes} },
422 index_name => $index,
423 index_count => $index_count,
424 db_count => $db_count,
425 missing_count => $missing_count,
428 $es_status->{running} = $es_running;
431 elasticsearch_status => $es_status,
432 elasticsearch_has_missing => $es_has_missing,
437 if ( C4::Context->preference('RESTOAuth2ClientCredentials') ) {
438 # Do we have the required deps?
439 unless ( can_load( modules => { 'Net::OAuth2::AuthorizationServer' => undef }) ) {
440 $template->param( oauth2_missing_deps => 1 );
444 # Sco Patron should not contain any other perms than circulate => self_checkout
445 if ( C4::Context->preference('WebBasedSelfCheck')
446 and C4::Context->preference('AutoSelfCheckAllowed')
448 my $userid = C4::Context->preference('AutoSelfCheckID');
449 my $all_permissions = C4::Auth::get_user_subpermissions( $userid );
450 my ( $has_self_checkout_perm, $has_other_permissions );
451 while ( my ( $module, $permissions ) = each %$all_permissions ) {
452 if ( $module eq 'self_check' ) {
453 while ( my ( $permission, $flag ) = each %$permissions ) {
454 if ( $permission eq 'self_checkout_module' ) {
455 $has_self_checkout_perm = 1;
457 $has_other_permissions = 1;
461 $has_other_permissions = 1;
465 AutoSelfCheckPatronDoesNotHaveSelfCheckPerm => not ( $has_self_checkout_perm ),
466 AutoSelfCheckPatronHasTooManyPerm => $has_other_permissions,
470 # Test YAML system preferences
471 # FIXME: This is list of current YAML formatted prefs, should by type of preference
473 "BibtexExportAdditionalFields",
474 "ItemsDeniedRenewal",
476 "MarcItemFieldsToOrder",
478 "RisExportAdditionalFields",
479 "UpdateitemLocationOnCheckin",
480 "UpdateItemWhenLostFromHoldList",
481 "UpdateNotForLoanStatusOnCheckin",
482 "UpdateNotForLoanStatusOnCheckout",
485 foreach my $syspref (@yaml_prefs) {
486 my $yaml = C4::Context->preference( $syspref );
488 eval { YAML::XS::Load( Encode::encode_utf8("$yaml\n\n") ); };
490 push @bad_yaml_prefs, $syspref;
494 $template->param( 'bad_yaml_prefs' => \@bad_yaml_prefs ) if @bad_yaml_prefs;
497 my $dbh = C4::Context->dbh;
498 my $patrons = $dbh->selectall_arrayref(
499 q|select b.borrowernumber from borrowers b join deletedborrowers db on b.borrowernumber=db.borrowernumber|,
502 my $biblios = $dbh->selectall_arrayref(
503 q|select b.biblionumber from biblio b join deletedbiblio db on b.biblionumber=db.biblionumber|,
506 my $biblioitems = $dbh->selectall_arrayref(
507 q|select bi.biblioitemnumber from biblioitems bi join deletedbiblioitems dbi on bi.biblionumber=dbi.biblionumber|,
510 my $items = $dbh->selectall_arrayref(
511 q|select i.itemnumber from items i join deleteditems di on i.itemnumber=di.itemnumber|,
514 my $checkouts = $dbh->selectall_arrayref(
515 q|select i.issue_id from issues i join old_issues oi on i.issue_id=oi.issue_id|,
518 my $holds = $dbh->selectall_arrayref(
519 q|select r.reserve_id from reserves r join old_reserves o on r.reserve_id=o.reserve_id|,
522 if ( @$patrons or @$biblios or @$biblioitems or @$items or @$checkouts or @$holds ) {
525 ai_patrons => $patrons,
526 ai_biblios => $biblios,
527 ai_biblioitems=> $biblioitems,
529 ai_checkouts => $checkouts,
537 my $dbh = C4::Context->dbh;
538 my $units = Koha::CirculationRules->search({ rule_name => 'lengthunit', rule_value => { -not_in => ['days', 'hours'] } });
540 if ( $units->count ) {
542 warnIssuingRules => 1,
548 # Guarantor relationships warnings
550 my $dbh = C4::Context->dbh;
551 my ($bad_relationships_count) = $dbh->selectall_arrayref(q{
554 SELECT relationship FROM borrower_relationships WHERE relationship='_bad_data'
556 SELECT relationship FROM borrowers WHERE relationship='_bad_data') a
559 $bad_relationships_count = $bad_relationships_count->[0]->[0];
561 my $existing_relationships = $dbh->selectall_arrayref(q{
562 SELECT DISTINCT(relationship)
564 SELECT relationship FROM borrower_relationships WHERE relationship IS NOT NULL
566 SELECT relationship FROM borrowers WHERE relationship IS NOT NULL) a
569 my %valid_relationships = map { $_ => 1 } split( /,|\|/, C4::Context->preference('borrowerRelationship') );
570 $valid_relationships{ _bad_data } = 1; # we handle this case in another way
572 my $wrong_relationships = [ grep { !$valid_relationships{ $_->[0] } } @{$existing_relationships} ];
573 if ( @$wrong_relationships or $bad_relationships_count ) {
576 warnRelationships => 1,
579 if ( $wrong_relationships ) {
581 wrong_relationships => $wrong_relationships
584 if ($bad_relationships_count) {
586 bad_relationships_count => $bad_relationships_count,
593 # Test 'bcrypt_settings' config for Pseudonymization
594 $template->param( config_bcrypt_settings_no_set => 1 )
595 if C4::Context->preference('Pseudonymization')
596 and not C4::Context->config('bcrypt_settings');
600 my @frameworkcodes = Koha::BiblioFrameworks->search->get_column('frameworkcode');
601 my @hidden_biblionumbers;
602 push @frameworkcodes, ""; # it's not in the biblio_frameworks table!
603 my $no_FA_framework = 1;
604 for my $frameworkcode ( @frameworkcodes ) {
605 $no_FA_framework = 0 if $frameworkcode eq 'FA';
606 my $shouldhidemarc_opac = Koha::Filter::MARC::ViewPolicy->should_hide_marc(
608 frameworkcode => $frameworkcode,
612 push @hidden_biblionumbers, { frameworkcode => $frameworkcode, interface => 'opac' }
613 if $shouldhidemarc_opac->{biblionumber};
615 my $shouldhidemarc_intranet = Koha::Filter::MARC::ViewPolicy->should_hide_marc(
617 frameworkcode => $frameworkcode,
618 interface => "intranet"
621 push @hidden_biblionumbers, { frameworkcode => $frameworkcode, interface => 'intranet' }
622 if $shouldhidemarc_intranet->{biblionumber};
624 $template->param( warnHiddenBiblionumbers => \@hidden_biblionumbers );
625 $template->param( warnFastCataloging => $no_FA_framework );
629 # BackgroundJob - test connection to message broker
631 Koha::BackgroundJob->connect;
635 $template->param( warnConnectBroker => $@ );
639 #BZ 28267: Warn administrators if there are database rows with a format other than 'DYNAMIC'
641 $template->param( warnDbRowFormat => C4::Installer->has_non_dynamic_row_format );
644 my %versions = C4::Context::get_versions();
647 kohaVersion => $versions{'kohaVersion'},
648 osVersion => $versions{'osVersion'},
649 perlPath => $perl_path,
650 perlVersion => $versions{'perlVersion'},
651 perlIncPath => [ map { perlinc => $_ }, @INC ],
652 mysqlVersion => $versions{'mysqlVersion'},
653 apacheVersion => $versions{'apacheVersion'},
654 zebraVersion => $zebraVersion,
655 prefRequireChoosingExistingAuthority => $prefRequireChoosingExistingAuthority,
656 prefAutoCreateAuthorities => $prefAutoCreateAuthorities,
657 warnPrefRequireChoosingExistingAuthority => $warnPrefRequireChoosingExistingAuthority,
658 warnPrefEasyAnalyticalRecords => $warnPrefEasyAnalyticalRecords,
659 warnPrefAnonymousPatronOPACPrivacy => $warnPrefAnonymousPatronOPACPrivacy,
660 warnPrefAnonymousPatronAnonSuggestions => $warnPrefAnonymousPatronAnonSuggestions,
661 warnPrefAnonymousPatronOPACPrivacy_PatronDoesNotExist => $warnPrefAnonymousPatronOPACPrivacy_PatronDoesNotExist,
662 warnPrefAnonymousPatronAnonSuggestions_PatronDoesNotExist => $warnPrefAnonymousPatronAnonSuggestions_PatronDoesNotExist,
663 warnPrefKohaAdminEmailAddress => $warnPrefKohaAdminEmailAddress,
664 warnPrefOpacHiddenItems => $warnPrefOpacHiddenItems,
665 errZebraConnection => $errZebraConnection,
666 warnIsRootUser => $warnIsRootUser,
667 warnNoActiveCurrency => $warnNoActiveCurrency,
668 warnNoTemplateCaching => ( C4::Context->config('template_cache_dir') ? 0 : 1 ),
669 xml_config_warnings => \@xml_config_warnings,
670 warnStatisticsFieldsError => $warnStatisticsFieldsError,
675 my $perl_modules = C4::Installer::PerlModules->new;
676 $perl_modules->versions_info;
678 my @pm_types = qw(missing_pm upgrade_pm current_pm);
680 foreach my $pm_type(@pm_types) {
681 my $modules = $perl_modules->get_attr($pm_type);
682 foreach (@$modules) {
683 my ($module, $stats) = each %$_;
688 version => $stats->{'cur_ver'},
689 missing => ($pm_type eq 'missing_pm' ? 1 : 0),
690 upgrade => ($pm_type eq 'upgrade_pm' ? 1 : 0),
691 current => ($pm_type eq 'current_pm' ? 1 : 0),
692 require => $stats->{'required'},
693 reqversion => $stats->{'min_ver'},
694 maxversion => $stats->{'max_ver'},
695 excversion => $stats->{'exc_ver'}
701 @components = sort {$a->{'name'} cmp $b->{'name'}} @components;
706 foreach (@components) {
708 unless (++$counter % 4) {
709 push (@$table, {row => $row});
713 # Processing the last line (if there are any modules left)
714 if (scalar(@$row) > 0) {
715 # Extending $row to the table size
717 # Pushing the last line
718 push (@$table, {row => $row});
722 $template->param( table => $table );
725 ## ------------------------------------------
726 ## Koha contributions
728 if ( defined C4::Context->config('docdir') ) {
729 $docdir = C4::Context->config('docdir');
731 # if no <docdir> is defined in koha-conf.xml, use the default location
732 # this is a work-around to stop breakage on upgraded Kohas, bug 8911
733 $docdir = C4::Context->config('intranetdir') . '/docs';
738 -e "$docdir" . "/teams.yaml"
739 ? YAML::XS::LoadFile( "$docdir" . "/teams.yaml" )
741 my $dev_team = (sort {$b <=> $a} (keys %{$teams->{team}}))[0];
742 my $short_version = substr($versions{'kohaVersion'},0,5);
743 my $minor = substr($versions{'kohaVersion'},3,2);
744 my $development_version = ( $minor eq '05' || $minor eq '11' ) ? 0 : 1;
746 $template->param( short_version => $short_version );
747 $template->param( development_version => $development_version );
751 -e "$docdir" . "/contributors.yaml"
752 ? YAML::XS::LoadFile( "$docdir" . "/contributors.yaml" )
754 delete $contributors->{_others_};
755 for my $version ( sort { $a <=> $b } keys %{$teams->{team}} ) {
756 for my $role ( keys %{ $teams->{team}->{$version} } ) {
757 my $normalized_role = "$role";
758 $normalized_role =~ s/s$//;
759 if ( ref( $teams->{team}->{$version}->{$role} ) eq 'ARRAY' ) {
760 for my $contributor ( @{ $teams->{team}->{$version}->{$role} } ) {
761 my $name = $contributor->{name};
762 # Add role to contributors
763 push @{ $contributors->{$name}->{roles}->{$normalized_role} },
765 # Add openhub to teams
766 if ( exists( $contributors->{$name}->{openhub} ) ) {
767 $contributor->{openhub} = $contributors->{$name}->{openhub};
771 elsif ( $role eq 'release_date' ) {
772 $teams->{team}->{$version}->{$role} = DateTime->from_epoch( epoch => $teams->{team}->{$version}->{$role});
774 elsif ( $role eq 'codename' ) {
775 if ( $version == $short_version ) {
776 $codename = $teams->{team}->{$version}->{$role};
781 my $name = $teams->{team}->{$version}->{$role}->{name};
782 # Add role to contributors
783 push @{ $contributors->{$name}->{roles}->{$normalized_role} },
785 # Add openhub to teams
786 if ( exists( $contributors->{$name}->{openhub} ) ) {
787 $teams->{team}->{$version}->{$role}->{openhub} =
788 $contributors->{$name}->{openhub};
794 ## Create last name ordered array of people from contributors
796 { name => $_, ( $contributors->{$_} ? %{ $contributors->{$_} } : () ) }
798 my ($alast) = $a =~ /(\S+)$/;
799 my ($blast) = $b =~ /(\S+)$/;
800 my $cmp = lc($alast||"") cmp lc($blast||"");
803 my ($a2last) = $a =~ /(\S+)\s\S+$/;
804 my ($b2last) = $b =~ /(\S+)\s\S+$/;
805 lc($a2last||"") cmp lc($b2last||"");
806 } keys %$contributors;
808 $template->param( kohaCodename => $codename);
809 $template->param( contributors => \@people );
810 $template->param( maintenance_team => $teams->{team}->{$dev_team} );
811 $template->param( release_team => $teams->{team}->{$short_version} );
814 if ( open( my $file, "<:encoding(UTF-8)", "$docdir" . "/history.txt" ) ) {
824 shift @lines; #remove header row
827 my ( $epoch, $date, $desc, $tag ) = split(/\t/);
828 if(!$desc && $date=~ /(?<=\d{4})\s+/) {
829 ($date, $desc)= ($`, $');
841 #foreach my $row2 (@rows2) {
844 push( @$table2, { row2 => $row2 } );
848 $template->param( table2 => $table2 );
850 $template->param( timeline_read_error => 1 );
853 output_html_with_http_headers $query, $cookie, $template->output;