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;
315 if ( !Koha::Patron::Categories->find($ill_config->partner_code) ) {
316 $template->param( ill_partner_code_doesnt_exist => $ill_config->partner_code );
317 $warnILLConfiguration = 1;
320 if ( !$ill_config_from_file->{partner_code} ) {
321 # partner code not defined
322 $template->param( ill_partner_code_not_defined => 1 );
323 $warnILLConfiguration = 1;
327 if ( !$ill_config_from_file->{branch} ) {
329 $template->param( ill_branch_not_defined => 1 );
330 $warnILLConfiguration = 1;
333 $template->param( warnILLConfiguration => $warnILLConfiguration );
335 unless ( can_run('weasyprint') ) {
336 $template->param( weasyprint_missing => 1 );
342 OPACXSLTDetailsDisplay
344 OPACXSLTResultsDisplay
350 for my $p ( @xslt_prefs ) {
351 my $xsl_filename = C4::XSLT::get_xsl_filename( $p );
352 next if -e $xsl_filename;
356 value => C4::Context->preference("$p"),
357 filename => $xsl_filename
361 $template->param( warnXSLT => \@warnXSLT ) if @warnXSLT;
364 if ( C4::Context->preference('SearchEngine') eq 'Elasticsearch' ) {
365 # Check ES configuration health and runtime status
370 my $es_has_missing = 0;
374 $es_conf = Koha::SearchEngine::Elasticsearch::_read_configuration();
377 if ( ref($_) eq 'Koha::Exceptions::Config::MissingEntry' ) {
378 $template->param( elasticsearch_fatal_config_error => $_->message );
379 $es_config_error = 1;
382 if ( !$es_config_error ) {
384 my $biblios_index_name = $es_conf->{index_name} . "_" . $Koha::SearchEngine::BIBLIOS_INDEX;
385 my $authorities_index_name = $es_conf->{index_name} . "_" . $Koha::SearchEngine::AUTHORITIES_INDEX;
387 my @indexes = ($biblios_index_name, $authorities_index_name);
388 # TODO: When new indexes get added, we could have other ways to
389 # fetch the list of available indexes (e.g. plugins, etc)
390 $es_status->{nodes} = $es_conf->{nodes};
391 my $es = Search::Elasticsearch->new({ nodes => $es_conf->{nodes} });
392 my $es_status->{version} = $es->info->{version}->{number};
394 foreach my $index ( @indexes ) {
397 $index_count = $es->indices->stats( index => $index )
398 ->{_all}{primaries}{docs}{count};
401 if ( ref($_) eq 'Search::Elasticsearch::Error::Missing' ) {
402 push @{ $es_status->{errors} }, "Index not found ($index)";
405 elsif ( ref($_) eq 'Search::Elasticsearch::Error::NoNodes' ) {
409 # TODO: when time comes, we will cover more use cases
415 my $missing_count = 0;
416 if ( $index eq $biblios_index_name ) {
417 $db_count = Koha::Biblios->search->count;
418 } elsif ( $index eq $authorities_index_name ) {
419 $db_count = Koha::Authorities->search->count;
421 if ( $db_count != -1 && $index_count != -1 ) {
422 $missing_count = $db_count - $index_count;
423 $es_has_missing = 1 if $missing_count > 0;
425 push @{ $es_status->{indexes} },
427 index_name => $index,
428 index_count => $index_count,
429 db_count => $db_count,
430 missing_count => $missing_count,
433 $es_status->{running} = $es_running;
436 elasticsearch_status => $es_status,
437 elasticsearch_has_missing => $es_has_missing,
442 if ( C4::Context->preference('RESTOAuth2ClientCredentials') ) {
443 # Do we have the required deps?
444 unless ( can_load( modules => { 'Net::OAuth2::AuthorizationServer' => undef }) ) {
445 $template->param( oauth2_missing_deps => 1 );
449 # Sco Patron should not contain any other perms than circulate => self_checkout
450 if ( C4::Context->preference('WebBasedSelfCheck')
451 and C4::Context->preference('AutoSelfCheckAllowed')
453 my $userid = C4::Context->preference('AutoSelfCheckID');
454 my $all_permissions = C4::Auth::get_user_subpermissions( $userid );
455 my ( $has_self_checkout_perm, $has_other_permissions );
456 while ( my ( $module, $permissions ) = each %$all_permissions ) {
457 if ( $module eq 'self_check' ) {
458 while ( my ( $permission, $flag ) = each %$permissions ) {
459 if ( $permission eq 'self_checkout_module' ) {
460 $has_self_checkout_perm = 1;
462 $has_other_permissions = 1;
466 $has_other_permissions = 1;
470 AutoSelfCheckPatronDoesNotHaveSelfCheckPerm => not ( $has_self_checkout_perm ),
471 AutoSelfCheckPatronHasTooManyPerm => $has_other_permissions,
475 # Test YAML system preferences
476 # FIXME: This is list of current YAML formatted prefs, should by type of preference
478 "BibtexExportAdditionalFields",
479 "ItemsDeniedRenewal",
481 "MarcItemFieldsToOrder",
483 "RisExportAdditionalFields",
484 "UpdateitemLocationOnCheckin",
485 "UpdateItemWhenLostFromHoldList",
486 "UpdateNotForLoanStatusOnCheckin",
487 "UpdateNotForLoanStatusOnCheckout",
490 foreach my $syspref (@yaml_prefs) {
491 my $yaml = C4::Context->preference( $syspref );
493 eval { YAML::XS::Load( Encode::encode_utf8("$yaml\n\n") ); };
495 push @bad_yaml_prefs, $syspref;
499 $template->param( 'bad_yaml_prefs' => \@bad_yaml_prefs ) if @bad_yaml_prefs;
502 my $dbh = C4::Context->dbh;
503 my $patrons = $dbh->selectall_arrayref(
504 q|select b.borrowernumber from borrowers b join deletedborrowers db on b.borrowernumber=db.borrowernumber|,
507 my $biblios = $dbh->selectall_arrayref(
508 q|select b.biblionumber from biblio b join deletedbiblio db on b.biblionumber=db.biblionumber|,
511 my $biblioitems = $dbh->selectall_arrayref(
512 q|select bi.biblioitemnumber from biblioitems bi join deletedbiblioitems dbi on bi.biblionumber=dbi.biblionumber|,
515 my $items = $dbh->selectall_arrayref(
516 q|select i.itemnumber from items i join deleteditems di on i.itemnumber=di.itemnumber|,
519 my $checkouts = $dbh->selectall_arrayref(
520 q|select i.issue_id from issues i join old_issues oi on i.issue_id=oi.issue_id|,
523 my $holds = $dbh->selectall_arrayref(
524 q|select r.reserve_id from reserves r join old_reserves o on r.reserve_id=o.reserve_id|,
527 if ( @$patrons or @$biblios or @$biblioitems or @$items or @$checkouts or @$holds ) {
530 ai_patrons => $patrons,
531 ai_biblios => $biblios,
532 ai_biblioitems => $biblioitems,
534 ai_checkouts => $checkouts,
542 my $dbh = C4::Context->dbh;
543 my $units = Koha::CirculationRules->search({ rule_name => 'lengthunit', rule_value => { -not_in => ['days', 'hours'] } });
545 if ( $units->count ) {
547 warnIssuingRules => 1,
553 # Guarantor relationships warnings
555 my $dbh = C4::Context->dbh;
556 my ($bad_relationships_count) = $dbh->selectall_arrayref(q{
559 SELECT relationship FROM borrower_relationships WHERE relationship='_bad_data'
561 SELECT relationship FROM borrowers WHERE relationship='_bad_data') a
564 $bad_relationships_count = $bad_relationships_count->[0]->[0];
566 my $existing_relationships = $dbh->selectall_arrayref(q{
567 SELECT DISTINCT(relationship)
569 SELECT relationship FROM borrower_relationships WHERE relationship IS NOT NULL
571 SELECT relationship FROM borrowers WHERE relationship IS NOT NULL) a
574 my %valid_relationships = map { $_ => 1 } split( /,|\|/, C4::Context->preference('borrowerRelationship') );
575 $valid_relationships{ _bad_data } = 1; # we handle this case in another way
577 my $wrong_relationships = [ grep { !$valid_relationships{ $_->[0] } } @{$existing_relationships} ];
578 if ( @$wrong_relationships or $bad_relationships_count ) {
581 warnRelationships => 1,
584 if ( $wrong_relationships ) {
586 wrong_relationships => $wrong_relationships
589 if ($bad_relationships_count) {
591 bad_relationships_count => $bad_relationships_count,
598 # Test 'bcrypt_settings' config for Pseudonymization
599 $template->param( config_bcrypt_settings_no_set => 1 )
600 if C4::Context->preference('Pseudonymization')
601 and not C4::Context->config('bcrypt_settings');
605 my @frameworkcodes = Koha::BiblioFrameworks->search->get_column('frameworkcode');
606 my @hidden_biblionumbers;
607 push @frameworkcodes, ""; # it's not in the biblio_frameworks table!
608 my $no_FA_framework = 1;
609 for my $frameworkcode ( @frameworkcodes ) {
610 $no_FA_framework = 0 if $frameworkcode eq 'FA';
611 my $shouldhidemarc_opac = Koha::Filter::MARC::ViewPolicy->should_hide_marc(
613 frameworkcode => $frameworkcode,
617 push @hidden_biblionumbers, { frameworkcode => $frameworkcode, interface => 'opac' }
618 if $shouldhidemarc_opac->{biblionumber};
620 my $shouldhidemarc_intranet = Koha::Filter::MARC::ViewPolicy->should_hide_marc(
622 frameworkcode => $frameworkcode,
623 interface => "intranet"
626 push @hidden_biblionumbers, { frameworkcode => $frameworkcode, interface => 'intranet' }
627 if $shouldhidemarc_intranet->{biblionumber};
629 $template->param( warnHiddenBiblionumbers => \@hidden_biblionumbers );
630 $template->param( warnFastCataloging => $no_FA_framework );
634 # BackgroundJob - test connection to message broker
636 Koha::BackgroundJob->connect;
640 $template->param( warnConnectBroker => $@ );
644 #BZ 28267: Warn administrators if there are database rows with a format other than 'DYNAMIC'
646 $template->param( warnDbRowFormat => C4::Installer->has_non_dynamic_row_format );
649 my %versions = C4::Context::get_versions();
652 kohaVersion => $versions{'kohaVersion'},
653 osVersion => $versions{'osVersion'},
654 perlPath => $perl_path,
655 perlVersion => $versions{'perlVersion'},
656 perlIncPath => [ map { perlinc => $_ }, @INC ],
657 mysqlVersion => $versions{'mysqlVersion'},
658 apacheVersion => $versions{'apacheVersion'},
659 zebraVersion => $zebraVersion,
660 prefRequireChoosingExistingAuthority => $prefRequireChoosingExistingAuthority,
661 prefAutoCreateAuthorities => $prefAutoCreateAuthorities,
662 warnPrefRequireChoosingExistingAuthority => $warnPrefRequireChoosingExistingAuthority,
663 warnPrefEasyAnalyticalRecords => $warnPrefEasyAnalyticalRecords,
664 warnPrefAnonymousPatronOPACPrivacy => $warnPrefAnonymousPatronOPACPrivacy,
665 warnPrefAnonymousPatronAnonSuggestions => $warnPrefAnonymousPatronAnonSuggestions,
666 warnPrefAnonymousPatronOPACPrivacy_PatronDoesNotExist => $warnPrefAnonymousPatronOPACPrivacy_PatronDoesNotExist,
667 warnPrefAnonymousPatronAnonSuggestions_PatronDoesNotExist => $warnPrefAnonymousPatronAnonSuggestions_PatronDoesNotExist,
668 warnPrefKohaAdminEmailAddress => $warnPrefKohaAdminEmailAddress,
669 warnPrefOpacHiddenItems => $warnPrefOpacHiddenItems,
670 errZebraConnection => $errZebraConnection,
671 warnIsRootUser => $warnIsRootUser,
672 warnNoActiveCurrency => $warnNoActiveCurrency,
673 warnNoTemplateCaching => ( C4::Context->config('template_cache_dir') ? 0 : 1 ),
674 xml_config_warnings => \@xml_config_warnings,
675 warnStatisticsFieldsError => $warnStatisticsFieldsError,
680 my $perl_modules = C4::Installer::PerlModules->new;
681 $perl_modules->versions_info;
683 my @pm_types = qw(missing_pm upgrade_pm current_pm);
685 foreach my $pm_type(@pm_types) {
686 my $modules = $perl_modules->get_attr($pm_type);
687 foreach (@$modules) {
688 my ($module, $stats) = each %$_;
693 version => $stats->{'cur_ver'},
694 missing => ($pm_type eq 'missing_pm' ? 1 : 0),
695 upgrade => ($pm_type eq 'upgrade_pm' ? 1 : 0),
696 current => ($pm_type eq 'current_pm' ? 1 : 0),
697 require => $stats->{'required'},
698 reqversion => $stats->{'min_ver'},
699 maxversion => $stats->{'max_ver'},
700 excversion => $stats->{'exc_ver'}
706 @components = sort {$a->{'name'} cmp $b->{'name'}} @components;
711 foreach (@components) {
713 unless (++$counter % 4) {
714 push (@$table, {row => $row});
718 # Processing the last line (if there are any modules left)
719 if (scalar(@$row) > 0) {
720 # Extending $row to the table size
722 # Pushing the last line
723 push (@$table, {row => $row});
727 $template->param( table => $table );
730 ## ------------------------------------------
731 ## Koha contributions
733 if ( defined C4::Context->config('docdir') ) {
734 $docdir = C4::Context->config('docdir');
736 # if no <docdir> is defined in koha-conf.xml, use the default location
737 # this is a work-around to stop breakage on upgraded Kohas, bug 8911
738 $docdir = C4::Context->config('intranetdir') . '/docs';
743 -e "$docdir" . "/teams.yaml"
744 ? YAML::XS::LoadFile( "$docdir" . "/teams.yaml" )
746 my $dev_team = (sort {$b <=> $a} (keys %{$teams->{team}}))[0];
747 my $short_version = substr($versions{'kohaVersion'},0,5);
748 my $minor = substr($versions{'kohaVersion'},3,2);
749 my $development_version = ( $minor eq '05' || $minor eq '11' ) ? 0 : 1;
751 $template->param( short_version => $short_version );
752 $template->param( development_version => $development_version );
756 -e "$docdir" . "/contributors.yaml"
757 ? YAML::XS::LoadFile( "$docdir" . "/contributors.yaml" )
759 delete $contributors->{_others_};
760 for my $version ( sort { $a <=> $b } keys %{$teams->{team}} ) {
761 for my $role ( keys %{ $teams->{team}->{$version} } ) {
762 my $detail = $teams->{team}->{$version}->{$role};
763 my $normalized_role = "$role";
764 $normalized_role =~ s/s$//;
765 if ( ref( $detail ) eq 'ARRAY' ) {
766 for my $contributor ( @{ $detail } ) {
768 my $localized_role = $normalized_role;
769 if ( my $subversion = $contributor->{version} ) {
770 $localized_role .= ':' . $subversion;
772 if ( my $area = $contributor->{area} ) {
773 $localized_role .= ':' . $area;
776 my $name = $contributor->{name};
777 # Add role to contributors
778 push @{ $contributors->{$name}->{roles}->{$localized_role} },
780 # Add openhub to teams
781 if ( exists( $contributors->{$name}->{openhub} ) ) {
782 $contributor->{openhub} = $contributors->{$name}->{openhub};
786 elsif ( $role eq 'release_date' ) {
787 $teams->{team}->{$version}->{$role} = DateTime->from_epoch( epoch => $teams->{team}->{$version}->{$role} );
789 elsif ( $role eq 'codename' ) {
790 if ( $version == $short_version ) {
796 if ( my $subversion = $detail->{version} ) {
797 $normalized_role .= ':' . $subversion;
799 if ( my $area = $detail->{area} ) {
800 $normalized_role .= ':' . $area;
803 my $name = $detail->{name};
804 # Add role to contributors
805 push @{ $contributors->{$name}->{roles}->{$normalized_role} },
807 # Add openhub to teams
808 if ( exists( $contributors->{$name}->{openhub} ) ) {
810 $contributors->{$name}->{openhub};
816 ## Create last name ordered array of people from contributors
818 { name => $_, ( $contributors->{$_} ? %{ $contributors->{$_} } : () ) }
820 my ($alast) = $a =~ /(\S+)$/;
821 my ($blast) = $b =~ /(\S+)$/;
822 my $cmp = lc($alast||"") cmp lc($blast||"");
825 my ($a2last) = $a =~ /(\S+)\s\S+$/;
826 my ($b2last) = $b =~ /(\S+)\s\S+$/;
827 lc($a2last||"") cmp lc($b2last||"");
828 } keys %$contributors;
830 $template->param( kohaCodename => $codename);
831 $template->param( contributors => \@people );
832 $template->param( maintenance_team => $teams->{team}->{$dev_team} );
833 $template->param( release_team => $teams->{team}->{$short_version} );
836 if ( open( my $file, "<:encoding(UTF-8)", "$docdir" . "/history.txt" ) ) {
846 shift @lines; #remove header row
849 my ( $epoch, $date, $desc, $tag ) = split(/\t/);
850 if(!$desc && $date=~ /(?<=\d{4})\s+/) {
851 ($date, $desc)= ($`, $');
863 #foreach my $row2 (@rows2) {
866 push( @$table2, { row2 => $row2 } );
870 $template->param( table2 => $table2 );
872 $template->param( timeline_read_error => 1 );
875 output_html_with_http_headers $query, $cookie, $template->output;