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::ILL::Request::Config;
57 use Koha::SearchEngine::Elasticsearch;
59 use Koha::Filter::MARC::ViewPolicy;
61 use C4::Members::Statistics;
64 my $params = $query->Vars();
66 my ( $template, $loggedinuser, $cookie ) = get_template_and_user(
68 template_name => "about.tt",
71 flagsrequired => { catalogue => 1 },
75 my $tab = $params->{tab} || 'about';
82 if ( defined C4::Context->config('docdir') ) {
83 $docdir = C4::Context->config('docdir');
86 # if no <docdir> is defined in koha-conf.xml, use the default location
87 # this is a work-around to stop breakage on upgraded Kohas, bug 8911
88 $docdir = C4::Context->config('intranetdir') . '/docs';
91 my %versions = C4::Context::get_versions();
93 if ( $tab eq 'about' ) {
95 my $config_timezone = C4::Context->config('timezone') // '';
96 my $config_invalid = !DateTime::TimeZone->is_valid_name( $config_timezone );
97 my $env_timezone = $ENV{TZ} // '';
98 my $env_invalid = !DateTime::TimeZone->is_valid_name( $env_timezone );
99 my $actual_bad_tz_fallback = 0;
101 if ( $config_timezone ne '' &&
104 $actual_bad_tz_fallback = 1;
106 elsif ( $config_timezone eq '' &&
107 $env_timezone ne '' &&
109 # No config, but bad ENV{TZ}
110 $actual_bad_tz_fallback = 1;
114 actual => C4::Context->tz->name,
115 actual_bad_tz_fallback => $actual_bad_tz_fallback,
116 config => $config_timezone,
117 config_invalid => $config_invalid,
118 environment => $env_timezone,
119 environment_invalid => $env_invalid
123 my $log4perl_config = C4::Context->config("log4perl_conf");
125 if ( ! $log4perl_config ) {
126 push @log4perl_errors, 'missing_config_entry'
129 my @lines = read_file($log4perl_config) or push @log4perl_errors, 'cannot_read_config_file';
130 for my $line ( @lines ) {
131 next unless $line =~ m|log4perl.appender.\w+.filename=(.*)|;
132 push @log4perl_errors, 'logfile_not_writable' unless -w $1;
135 eval {Koha::Logger->get};
136 push @log4perl_errors, 'cannot_init_module' and warn $@ if $@;
137 $template->param( log4perl_errors => @log4perl_errors );
141 time_zone => $time_zone,
142 current_date_and_time => output_pref({ dt => dt_from_string(), dateformat => 'iso' })
147 $perl_path .= $Config{_exe} unless $perl_path =~ m/$Config{_exe}$/i;
150 my $zebraVersion = `zebraidx -V`;
152 # Check running PSGI env
153 if ( C4::Context->psgi_env ) {
156 psgi_server => ($ENV{ PLACK_ENV }) ? "Plack ($ENV{PLACK_ENV})" :
157 ($ENV{ MOD_PERL }) ? "mod_perl ($ENV{MOD_PERL})" :
162 # Memcached configuration
163 my $memcached_servers = $ENV{MEMCACHED_SERVERS} || C4::Context->config('memcached_servers');
164 my $memcached_namespace = $ENV{MEMCACHED_NAMESPACE} || C4::Context->config('memcached_namespace') // 'koha';
166 my $cache = Koha::Caches->get_instance;
167 my $effective_caching_method = ref($cache->cache);
168 # Memcached may have been running when plack has been initialized but could have been stopped since
169 # FIXME What are the consequences of that??
170 my $is_memcached_still_active = $cache->set_in_cache('test_for_about_page', "just a simple value");
172 my $where_is_memcached_config = 'nowhere';
173 if ( $ENV{MEMCACHED_SERVERS} and C4::Context->config('memcached_servers') ) {
174 $where_is_memcached_config = 'both';
175 } elsif ( $ENV{MEMCACHED_SERVERS} and not C4::Context->config('memcached_servers') ) {
176 $where_is_memcached_config = 'ENV_only';
177 } elsif ( C4::Context->config('memcached_servers') ) {
178 $where_is_memcached_config = 'config_only';
182 effective_caching_method => $effective_caching_method,
183 memcached_servers => $memcached_servers,
184 memcached_namespace => $memcached_namespace,
185 is_memcached_still_active => $is_memcached_still_active,
186 where_is_memcached_config => $where_is_memcached_config,
187 perlPath => $perl_path,
188 zebraVersion => $zebraVersion,
189 kohaVersion => $versions{'kohaVersion'},
190 osVersion => $versions{'osVersion'},
191 perlVersion => $versions{'perlVersion'},
192 perlIncPath => [ map { perlinc => $_ }, @INC ],
193 mysqlVersion => $versions{'mysqlVersion'},
194 apacheVersion => $versions{'apacheVersion'},
195 memcached_running => Koha::Caches->get_instance->memcached_cache,
200 if($tab eq 'sysinfo') {
202 # Additional system information for warnings
204 my $warnStatisticsFieldsError;
205 my $prefStatisticsFields = C4::Context->preference('StatisticsFields');
206 if ($prefStatisticsFields) {
207 $warnStatisticsFieldsError = $prefStatisticsFields
208 unless ( $prefStatisticsFields eq C4::Members::Statistics->get_fields() );
211 my $prefAutoCreateAuthorities = C4::Context->preference('AutoCreateAuthorities');
212 my $prefRequireChoosingExistingAuthority = C4::Context->preference('RequireChoosingExistingAuthority');
213 my $warnPrefRequireChoosingExistingAuthority = ( !$prefAutoCreateAuthorities && ( !$prefRequireChoosingExistingAuthority) );
215 my $prefEasyAnalyticalRecords = C4::Context->preference('EasyAnalyticalRecords');
216 my $prefUseControlNumber = C4::Context->preference('UseControlNumber');
217 my $warnPrefEasyAnalyticalRecords = ( $prefEasyAnalyticalRecords && $prefUseControlNumber );
219 my $AnonymousPatron = C4::Context->preference('AnonymousPatron');
220 my $warnPrefAnonymousPatronOPACPrivacy = (
221 C4::Context->preference('OPACPrivacy')
222 and not $AnonymousPatron
224 my $warnPrefAnonymousPatronAnonSuggestions = (
225 C4::Context->preference('AnonSuggestions')
226 and not $AnonymousPatron
229 my $anonymous_patron = Koha::Patrons->find( $AnonymousPatron );
230 my $warnPrefAnonymousPatronAnonSuggestions_PatronDoesNotExist = ( $AnonymousPatron && C4::Context->preference('AnonSuggestions') && not $anonymous_patron );
232 my $warnPrefAnonymousPatronOPACPrivacy_PatronDoesNotExist = ( not $anonymous_patron and Koha::Patrons->search({ privacy => 2 })->count );
234 my $warnPrefKohaAdminEmailAddress = !Koha::Email->is_valid(C4::Context->preference('KohaAdminEmailAddress'));
236 my $c = Koha::Items->filter_by_visible_in_opac->count;
237 my @warnings = C4::Context->dbh->selectrow_array('SHOW WARNINGS');
238 my $warnPrefOpacHiddenItems = $warnings[2];
240 my $invalid_yesno = Koha::Config::SysPrefs->search(
243 value => { -or => { 'is' => undef, -not_in => [ "1", "0" ] } }
246 $template->param( invalid_yesno => $invalid_yesno );
248 my $errZebraConnection = C4::Context->Zconn("biblioserver",0)->errcode();
250 my $warnIsRootUser = (! $loggedinuser);
252 my $warnNoActiveCurrency = (! defined Koha::Acquisition::Currencies->get_active);
254 my @xml_config_warnings;
256 if ( C4::Context->config('zebra_bib_index_mode')
257 and C4::Context->config('zebra_bib_index_mode') eq 'grs1' )
259 push @xml_config_warnings, { error => 'zebra_bib_index_mode_is_grs1' };
262 if ( C4::Context->config('zebra_auth_index_mode')
263 and C4::Context->config('zebra_auth_index_mode') eq 'grs1' )
265 push @xml_config_warnings, { error => 'zebra_auth_index_mode_is_grs1' };
268 my $authorityserver = C4::Context->zebraconfig('authorityserver');
269 if( ( C4::Context->config('zebra_auth_index_mode')
270 and C4::Context->config('zebra_auth_index_mode') eq 'dom' )
271 && ( $authorityserver->{config} !~ /zebra-authorities-dom.cfg/ ) )
273 push @xml_config_warnings, {
274 error => 'zebra_auth_index_mode_mismatch_warn'
278 if ( ! defined C4::Context->config('log4perl_conf') ) {
279 push @xml_config_warnings, {
280 error => 'log4perl_entry_missing'
284 if ( ! defined C4::Context->config('lockdir') ) {
285 push @xml_config_warnings, {
286 error => 'lockdir_entry_missing'
290 unless ( -w C4::Context->config('lockdir') ) {
291 push @xml_config_warnings, {
292 error => 'lockdir_not_writable',
293 lockdir => C4::Context->config('lockdir')
298 if ( ! defined C4::Context->config('upload_path') ) {
299 if ( Koha::Config::SysPrefs->find('OPACBaseURL')->value ) {
300 # OPACBaseURL seems to be set
301 push @xml_config_warnings, {
302 error => 'uploadpath_entry_missing'
305 push @xml_config_warnings, {
306 error => 'uploadpath_and_opacbaseurl_entry_missing'
311 if ( ! C4::Context->config('tmp_path') ) {
312 my $temporary_directory = C4::Context::temporary_directory;
313 push @xml_config_warnings, {
314 error => 'tmp_path_missing',
315 effective_tmp_dir => $temporary_directory,
319 my $encryption_key = C4::Context->config('encryption_key');
320 if ( !$encryption_key || $encryption_key eq '__ENCRYPTION_KEY__') {
321 push @xml_config_warnings, { error => 'encryption_key_missing' };
324 # Test Zebra facets configuration
325 if ( !defined C4::Context->config('use_zebra_facets') ) {
326 push @xml_config_warnings, { error => 'use_zebra_facets_entry_missing' };
329 unless ( Koha::I18N->_base_directory ) {
330 $template->param( warnI18nMissing => 1 );
334 if ( C4::Context->preference('ILLModule') ) {
335 my $warnILLConfiguration = 0;
336 my $ill_config_from_file = C4::Context->config("interlibrary_loans");
337 my $ill_config = Koha::ILL::Request::Config->new;
339 my $available_ill_backends =
340 ( scalar @{ $ill_config->available_backends } > 0 );
343 if ( !$available_ill_backends ) {
344 $template->param( no_ill_backends => 1 );
345 $warnILLConfiguration = 1;
348 # Check ILLPartnerCode sys pref
349 if ( !Koha::Patron::Categories->find( C4::Context->preference('ILLPartnerCode') ) ) {
350 $template->param( ill_partner_code_doesnt_exist => C4::Context->preference('ILLPartnerCode') );
351 $warnILLConfiguration = 1;
352 } elsif ( !Koha::Patrons->search( { categorycode => C4::Context->preference('ILLPartnerCode') } )->count ) {
353 $template->param( ill_partner_code_no_patrons => C4::Context->preference('ILLPartnerCode') );
354 $warnILLConfiguration = 1;
357 if ( !C4::Context->preference('ILLPartnerCode') ) {
358 # partner code not defined
359 $template->param( ill_partner_code_not_defined => 1 );
360 $warnILLConfiguration = 1;
364 if ( !$ill_config_from_file->{branch} ) {
366 $template->param( ill_branch_not_defined => 1 );
367 $warnILLConfiguration = 1;
370 $template->param( warnILLConfiguration => $warnILLConfiguration );
372 unless ( can_run('weasyprint') ) {
373 $template->param( weasyprint_missing => 1 );
379 OPACXSLTDetailsDisplay
381 OPACXSLTResultsDisplay
387 for my $p ( @xslt_prefs ) {
388 my $xsl_filename = C4::XSLT::get_xsl_filename( $p );
389 next if -e $xsl_filename;
393 value => C4::Context->preference("$p"),
394 filename => $xsl_filename
398 $template->param( warnXSLT => \@warnXSLT ) if @warnXSLT;
401 if ( C4::Context->preference('SearchEngine') eq 'Elasticsearch' ) {
402 # Check ES configuration health and runtime status
407 my $es_has_missing = 0;
411 $es_conf = Koha::SearchEngine::Elasticsearch::_read_configuration();
414 if ( ref($_) eq 'Koha::Exceptions::Config::MissingEntry' ) {
415 $template->param( elasticsearch_fatal_config_error => $_->message );
416 $es_config_error = 1;
419 if ( !$es_config_error ) {
421 my $biblios_index_name = $es_conf->{index_name} . "_" . $Koha::SearchEngine::BIBLIOS_INDEX;
422 my $authorities_index_name = $es_conf->{index_name} . "_" . $Koha::SearchEngine::AUTHORITIES_INDEX;
424 my @indexes = ($biblios_index_name, $authorities_index_name);
425 # TODO: When new indexes get added, we could have other ways to
426 # fetch the list of available indexes (e.g. plugins, etc)
427 $es_status->{nodes} = $es_conf->{nodes};
428 my $es = Search::Elasticsearch->new( $es_conf );
429 my $es_status->{version} = $es->info->{version}->{number};
431 foreach my $index ( @indexes ) {
434 $index_count = $es->indices->stats( index => $index )
435 ->{_all}{primaries}{docs}{count};
438 if ( ref($_) eq 'Search::Elasticsearch::Error::Missing' ) {
439 push @{ $es_status->{errors} }, "Index not found ($index)";
442 elsif ( ref($_) eq 'Search::Elasticsearch::Error::NoNodes' ) {
446 # TODO: when time comes, we will cover more use cases
452 my $missing_count = 0;
453 if ( $index eq $biblios_index_name ) {
454 $db_count = Koha::Biblios->search->count;
455 } elsif ( $index eq $authorities_index_name ) {
456 $db_count = Koha::Authorities->search->count;
458 if ( $db_count != -1 && $index_count != -1 ) {
459 $missing_count = $db_count - $index_count;
460 $es_has_missing = 1 if $missing_count > 0;
462 push @{ $es_status->{indexes} },
464 index_name => $index,
465 index_count => $index_count,
466 db_count => $db_count,
467 missing_count => $missing_count,
470 $es_status->{running} = $es_running;
473 elasticsearch_status => $es_status,
474 elasticsearch_has_missing => $es_has_missing,
479 if ( C4::Context->preference('RESTOAuth2ClientCredentials') ) {
480 # Do we have the required deps?
481 unless ( can_load( modules => { 'Net::OAuth2::AuthorizationServer' => undef }) ) {
482 $template->param( oauth2_missing_deps => 1 );
486 # Sco Patron should not contain any other perms than circulate => self_checkout
487 if ( C4::Context->preference('WebBasedSelfCheck')
488 and C4::Context->preference('AutoSelfCheckAllowed')
490 my $userid = C4::Context->preference('AutoSelfCheckID');
491 my $all_permissions = C4::Auth::get_user_subpermissions( $userid );
492 my ( $has_self_checkout_perm, $has_other_permissions );
493 while ( my ( $module, $permissions ) = each %$all_permissions ) {
494 if ( $module eq 'self_check' ) {
495 while ( my ( $permission, $flag ) = each %$permissions ) {
496 if ( $permission eq 'self_checkout_module' ) {
497 $has_self_checkout_perm = 1;
499 $has_other_permissions = 1;
503 $has_other_permissions = 1;
507 AutoSelfCheckPatronDoesNotHaveSelfCheckPerm => not ( $has_self_checkout_perm ),
508 AutoSelfCheckPatronHasTooManyPerm => $has_other_permissions,
512 if ( C4::Context->preference('PatronSelfRegistration') ) {
513 $template->param( warnPrefPatronSelfRegistrationDefaultCategory => 1 )
514 unless Koha::Patron::Categories->find(C4::Context->preference('PatronSelfRegistrationDefaultCategory'));
517 # Test YAML system preferences
518 # FIXME: This is list of current YAML formatted prefs, should by type of preference
520 "BibtexExportAdditionalFields",
521 "ItemsDeniedRenewal",
523 "MarcItemFieldsToOrder",
525 "RisExportAdditionalFields",
526 "UpdateitemLocationOnCheckin",
527 "UpdateItemWhenLostFromHoldList",
528 "UpdateNotForLoanStatusOnCheckin",
529 "UpdateNotForLoanStatusOnCheckout",
532 foreach my $syspref (@yaml_prefs) {
533 my $yaml = C4::Context->preference( $syspref );
535 eval { YAML::XS::Load( Encode::encode_utf8("$yaml\n\n") ); };
537 push @bad_yaml_prefs, $syspref;
541 $template->param( 'bad_yaml_prefs' => \@bad_yaml_prefs ) if @bad_yaml_prefs;
544 my $dbh = C4::Context->dbh;
545 my $patrons = $dbh->selectall_arrayref(
546 q|select b.borrowernumber from borrowers b join deletedborrowers db on b.borrowernumber=db.borrowernumber|,
549 my $biblios = $dbh->selectall_arrayref(
550 q|select b.biblionumber from biblio b join deletedbiblio db on b.biblionumber=db.biblionumber|,
553 my $biblioitems = $dbh->selectall_arrayref(
554 q|select bi.biblioitemnumber from biblioitems bi join deletedbiblioitems dbi on bi.biblionumber=dbi.biblionumber|,
557 my $items = $dbh->selectall_arrayref(
558 q|select i.itemnumber from items i join deleteditems di on i.itemnumber=di.itemnumber|,
561 my $checkouts = $dbh->selectall_arrayref(
562 q|select i.issue_id from issues i join old_issues oi on i.issue_id=oi.issue_id|,
565 my $holds = $dbh->selectall_arrayref(
566 q|select r.reserve_id from reserves r join old_reserves o on r.reserve_id=o.reserve_id|,
569 if ( @$patrons or @$biblios or @$biblioitems or @$items or @$checkouts or @$holds ) {
572 ai_patrons => $patrons,
573 ai_biblios => $biblios,
574 ai_biblioitems => $biblioitems,
576 ai_checkouts => $checkouts,
584 my $dbh = C4::Context->dbh;
585 my $units = Koha::CirculationRules->search({ rule_name => 'lengthunit', rule_value => { -not_in => ['days', 'hours'] } });
587 if ( $units->count ) {
589 warnIssuingRules => 1,
595 # Guarantor relationships warnings
597 my $dbh = C4::Context->dbh;
598 my ($bad_relationships_count) = $dbh->selectall_arrayref(q{
601 SELECT relationship FROM borrower_relationships WHERE relationship='_bad_data'
603 SELECT relationship FROM borrowers WHERE relationship='_bad_data') a
606 $bad_relationships_count = $bad_relationships_count->[0]->[0];
608 my $existing_relationships = $dbh->selectall_arrayref(q{
609 SELECT DISTINCT(relationship)
611 SELECT relationship FROM borrower_relationships WHERE relationship IS NOT NULL
613 SELECT relationship FROM borrowers WHERE relationship IS NOT NULL) a
616 my %valid_relationships = map { $_ => 1 } split( /,|\|/, C4::Context->preference('borrowerRelationship') );
617 $valid_relationships{ _bad_data } = 1; # we handle this case in another way
619 my $wrong_relationships = [ grep { !$valid_relationships{ $_->[0] } } @{$existing_relationships} ];
620 if ( @$wrong_relationships or $bad_relationships_count ) {
623 warnRelationships => 1,
626 if ( $wrong_relationships ) {
628 wrong_relationships => $wrong_relationships
631 if ($bad_relationships_count) {
633 bad_relationships_count => $bad_relationships_count,
640 # Test 'bcrypt_settings' config for Pseudonymization
641 $template->param( config_bcrypt_settings_no_set => 1 )
642 if C4::Context->preference('Pseudonymization')
643 and not C4::Context->config('bcrypt_settings');
647 my @frameworkcodes = Koha::BiblioFrameworks->search->get_column('frameworkcode');
648 my @hidden_biblionumbers;
649 push @frameworkcodes, ""; # it's not in the biblio_frameworks table!
650 my $no_FA_framework = 1;
651 for my $frameworkcode ( @frameworkcodes ) {
652 $no_FA_framework = 0 if $frameworkcode eq 'FA';
653 my $shouldhidemarc_opac = Koha::Filter::MARC::ViewPolicy->should_hide_marc(
655 frameworkcode => $frameworkcode,
659 push @hidden_biblionumbers, { frameworkcode => $frameworkcode, interface => 'opac' }
660 if $shouldhidemarc_opac->{biblionumber};
662 my $shouldhidemarc_intranet = Koha::Filter::MARC::ViewPolicy->should_hide_marc(
664 frameworkcode => $frameworkcode,
665 interface => "intranet"
668 push @hidden_biblionumbers, { frameworkcode => $frameworkcode, interface => 'intranet' }
669 if $shouldhidemarc_intranet->{biblionumber};
671 $template->param( warnHiddenBiblionumbers => \@hidden_biblionumbers );
672 $template->param( warnFastCataloging => $no_FA_framework );
676 # BackgroundJob - test connection to message broker
678 Koha::BackgroundJob->connect;
682 $template->param( warnConnectBroker => $@ );
686 #BZ 28267: Warn administrators if there are database rows with a format other than 'DYNAMIC'
688 $template->param( warnDbRowFormat => C4::Installer->has_non_dynamic_row_format );
692 prefRequireChoosingExistingAuthority => $prefRequireChoosingExistingAuthority,
693 prefAutoCreateAuthorities => $prefAutoCreateAuthorities,
694 warnPrefRequireChoosingExistingAuthority => $warnPrefRequireChoosingExistingAuthority,
695 warnPrefEasyAnalyticalRecords => $warnPrefEasyAnalyticalRecords,
696 warnPrefAnonymousPatronOPACPrivacy => $warnPrefAnonymousPatronOPACPrivacy,
697 warnPrefAnonymousPatronAnonSuggestions => $warnPrefAnonymousPatronAnonSuggestions,
698 warnPrefAnonymousPatronOPACPrivacy_PatronDoesNotExist => $warnPrefAnonymousPatronOPACPrivacy_PatronDoesNotExist,
699 warnPrefAnonymousPatronAnonSuggestions_PatronDoesNotExist => $warnPrefAnonymousPatronAnonSuggestions_PatronDoesNotExist,
700 warnPrefKohaAdminEmailAddress => $warnPrefKohaAdminEmailAddress,
701 warnPrefOpacHiddenItems => $warnPrefOpacHiddenItems,
702 errZebraConnection => $errZebraConnection,
703 warnIsRootUser => $warnIsRootUser,
704 warnNoActiveCurrency => $warnNoActiveCurrency,
705 warnNoTemplateCaching => ( C4::Context->config('template_cache_dir') ? 0 : 1 ),
706 xml_config_warnings => \@xml_config_warnings,
707 warnStatisticsFieldsError => $warnStatisticsFieldsError,
711 if ( $tab eq 'perl' ) {
715 my $perl_modules = C4::Installer::PerlModules->new;
716 $perl_modules->versions_info;
718 my @pm_types = qw(missing_pm upgrade_pm current_pm);
720 foreach my $pm_type(@pm_types) {
721 my $modules = $perl_modules->get_attr($pm_type);
722 foreach (@$modules) {
723 my ($module, $stats) = each %$_;
728 version => $stats->{'cur_ver'},
729 missing => ($pm_type eq 'missing_pm' ? 1 : 0),
730 upgrade => ($pm_type eq 'upgrade_pm' ? 1 : 0),
731 current => ($pm_type eq 'current_pm' ? 1 : 0),
732 require => $stats->{'required'},
733 reqversion => $stats->{'min_ver'},
734 maxversion => $stats->{'max_ver'},
735 excversion => $stats->{'exc_ver'}
741 @components = sort {$a->{'name'} cmp $b->{'name'}} @components;
746 foreach (@components) {
748 unless (++$counter % 4) {
749 push (@$table, {row => $row});
753 # Processing the last line (if there are any modules left)
754 if (scalar(@$row) > 0) {
755 # Extending $row to the table size
757 # Pushing the last line
758 push (@$table, {row => $row});
762 $template->param( table => $table );
766 if ( $tab eq 'team' ) {
768 ## ------------------------------------------
769 ## Koha contributions
773 -e "$docdir" . "/teams.yaml"
774 ? YAML::XS::LoadFile( "$docdir" . "/teams.yaml" )
776 my $dev_team = (sort {$b <=> $a} (keys %{$teams->{team}}))[0];
777 my $short_version = substr($versions{'kohaVersion'},0,5);
778 my $minor = substr($versions{'kohaVersion'},3,2);
779 my $development_version = ( $minor eq '05' || $minor eq '11' ) ? 0 : 1;
781 $template->param( short_version => $short_version );
782 $template->param( development_version => $development_version );
786 -e "$docdir" . "/contributors.yaml"
787 ? YAML::XS::LoadFile( "$docdir" . "/contributors.yaml" )
789 delete $contributors->{_others_};
790 for my $version ( sort { $a <=> $b } keys %{$teams->{team}} ) {
791 for my $role ( keys %{ $teams->{team}->{$version} } ) {
792 my $detail = $teams->{team}->{$version}->{$role};
793 my $normalized_role = "$role";
794 $normalized_role =~ s/s$//;
795 if ( ref( $detail ) eq 'ARRAY' ) {
796 for my $contributor ( @{ $detail } ) {
798 my $localized_role = $normalized_role;
799 if ( my $subversion = $contributor->{version} ) {
800 $localized_role .= ':' . $subversion;
802 if ( my $area = $contributor->{area} ) {
803 $localized_role .= ':' . $area;
806 my $name = $contributor->{name};
807 # Add role to contributors
808 push @{ $contributors->{$name}->{roles}->{$localized_role} },
810 # Add openhub to teams
811 if ( exists( $contributors->{$name}->{openhub} ) ) {
812 $contributor->{openhub} = $contributors->{$name}->{openhub};
816 elsif ( $role eq 'release_date' ) {
817 $teams->{team}->{$version}->{$role} = DateTime->from_epoch( epoch => $teams->{team}->{$version}->{$role} );
819 elsif ( $role eq 'codename' ) {
820 if ( $version == $short_version ) {
826 if ( my $subversion = $detail->{version} ) {
827 $normalized_role .= ':' . $subversion;
829 if ( my $area = $detail->{area} ) {
830 $normalized_role .= ':' . $area;
833 my $name = $detail->{name};
834 # Add role to contributors
835 push @{ $contributors->{$name}->{roles}->{$normalized_role} },
837 # Add openhub to teams
838 if ( exists( $contributors->{$name}->{openhub} ) ) {
840 $contributors->{$name}->{openhub};
846 ## Create last name ordered array of people from contributors
848 { name => $_, ( $contributors->{$_} ? %{ $contributors->{$_} } : () ) }
850 my ($alast) = $a =~ /(\S+)$/;
851 my ($blast) = $b =~ /(\S+)$/;
852 my $cmp = lc($alast||"") cmp lc($blast||"");
855 my ($a2last) = $a =~ /(\S+)\s\S+$/;
856 my ($b2last) = $b =~ /(\S+)\s\S+$/;
857 lc($a2last||"") cmp lc($b2last||"");
858 } keys %$contributors;
860 $template->param( kohaCodename => $codename);
861 $template->param( contributors => \@people );
862 $template->param( maintenance_team => $teams->{team}->{$dev_team} );
863 $template->param( release_team => $teams->{team}->{$short_version} );
866 if ( $tab eq 'history' ) {
869 if ( open( my $file, "<:encoding(UTF-8)", "$docdir" . "/history.txt" ) ) {
879 shift @lines; #remove header row
882 my ( $epoch, $date, $desc, $tag ) = split(/\t/);
883 if(!$desc && $date=~ /(?<=\d{4})\s+/) {
884 ($date, $desc)= ($`, $');
896 #foreach my $row2 (@rows2) {
899 push( @$table2, { row2 => $row2 } );
903 $template->param( table2 => $table2 );
905 $template->param( timeline_read_error => 1 );
909 output_html_with_http_headers $query, $cookie, $template->output;