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 ( $template, $loggedinuser, $cookie ) = get_template_and_user(
66 template_name => "about.tt",
69 flagsrequired => { catalogue => 1 },
74 if ( defined C4::Context->config('docdir') ) {
75 $docdir = C4::Context->config('docdir');
78 # if no <docdir> is defined in koha-conf.xml, use the default location
79 # this is a work-around to stop breakage on upgraded Kohas, bug 8911
80 $docdir = C4::Context->config('intranetdir') . '/docs';
83 my %versions = C4::Context::get_versions();
84 my $config_timezone = C4::Context->config('timezone') // '';
85 my $config_invalid = !DateTime::TimeZone->is_valid_name( $config_timezone );
86 my $env_timezone = $ENV{TZ} // '';
87 my $env_invalid = !DateTime::TimeZone->is_valid_name( $env_timezone );
88 my $actual_bad_tz_fallback = 0;
90 if ( $config_timezone ne '' &&
93 $actual_bad_tz_fallback = 1;
95 elsif ( $config_timezone eq '' &&
96 $env_timezone ne '' &&
98 # No config, but bad ENV{TZ}
99 $actual_bad_tz_fallback = 1;
103 actual => C4::Context->tz->name,
104 actual_bad_tz_fallback => $actual_bad_tz_fallback,
105 config => $config_timezone,
106 config_invalid => $config_invalid,
107 environment => $env_timezone,
108 environment_invalid => $env_invalid
112 my $log4perl_config = C4::Context->config("log4perl_conf");
114 if ( ! $log4perl_config ) {
115 push @log4perl_errors, 'missing_config_entry'
118 my @lines = read_file($log4perl_config) or push @log4perl_errors, 'cannot_read_config_file';
119 for my $line ( @lines ) {
120 next unless $line =~ m|log4perl.appender.\w+.filename=(.*)|;
121 push @log4perl_errors, 'logfile_not_writable' unless -w $1;
124 eval {Koha::Logger->get};
125 push @log4perl_errors, 'cannot_init_module' and warn $@ if $@;
126 $template->param( log4perl_errors => @log4perl_errors );
130 time_zone => $time_zone,
131 current_date_and_time => output_pref({ dt => dt_from_string(), dateformat => 'iso' })
136 $perl_path .= $Config{_exe} unless $perl_path =~ m/$Config{_exe}$/i;
139 my $zebraVersion = `zebraidx -V`;
141 # Check running PSGI env
142 if ( C4::Context->psgi_env ) {
145 psgi_server => ($ENV{ PLACK_ENV }) ? "Plack ($ENV{PLACK_ENV})" :
146 ($ENV{ MOD_PERL }) ? "mod_perl ($ENV{MOD_PERL})" :
151 # Memcached configuration
152 my $memcached_servers = $ENV{MEMCACHED_SERVERS} || C4::Context->config('memcached_servers');
153 my $memcached_namespace = $ENV{MEMCACHED_NAMESPACE} || C4::Context->config('memcached_namespace') // 'koha';
155 my $cache = Koha::Caches->get_instance;
156 my $effective_caching_method = ref($cache->cache);
157 # Memcached may have been running when plack has been initialized but could have been stopped since
158 # FIXME What are the consequences of that??
159 my $is_memcached_still_active = $cache->set_in_cache('test_for_about_page', "just a simple value");
161 my $where_is_memcached_config = 'nowhere';
162 if ( $ENV{MEMCACHED_SERVERS} and C4::Context->config('memcached_servers') ) {
163 $where_is_memcached_config = 'both';
164 } elsif ( $ENV{MEMCACHED_SERVERS} and not C4::Context->config('memcached_servers') ) {
165 $where_is_memcached_config = 'ENV_only';
166 } elsif ( C4::Context->config('memcached_servers') ) {
167 $where_is_memcached_config = 'config_only';
171 effective_caching_method => $effective_caching_method,
172 memcached_servers => $memcached_servers,
173 memcached_namespace => $memcached_namespace,
174 is_memcached_still_active => $is_memcached_still_active,
175 where_is_memcached_config => $where_is_memcached_config,
176 perlPath => $perl_path,
177 zebraVersion => $zebraVersion,
178 kohaVersion => $versions{'kohaVersion'},
179 osVersion => $versions{'osVersion'},
180 perlVersion => $versions{'perlVersion'},
181 perlIncPath => [ map { perlinc => $_ }, @INC ],
182 mysqlVersion => $versions{'mysqlVersion'},
183 apacheVersion => $versions{'apacheVersion'},
184 memcached_running => Koha::Caches->get_instance->memcached_cache,
187 # Additional system information for warnings
189 my $warnStatisticsFieldsError;
190 my $prefStatisticsFields = C4::Context->preference('StatisticsFields');
191 if ($prefStatisticsFields) {
192 $warnStatisticsFieldsError = $prefStatisticsFields
193 unless ( $prefStatisticsFields eq C4::Members::Statistics->get_fields() );
196 my $prefAutoCreateAuthorities = C4::Context->preference('AutoCreateAuthorities');
197 my $prefRequireChoosingExistingAuthority = C4::Context->preference('RequireChoosingExistingAuthority');
198 my $warnPrefRequireChoosingExistingAuthority = ( !$prefAutoCreateAuthorities && ( !$prefRequireChoosingExistingAuthority) );
200 my $prefEasyAnalyticalRecords = C4::Context->preference('EasyAnalyticalRecords');
201 my $prefUseControlNumber = C4::Context->preference('UseControlNumber');
202 my $warnPrefEasyAnalyticalRecords = ( $prefEasyAnalyticalRecords && $prefUseControlNumber );
204 my $AnonymousPatron = C4::Context->preference('AnonymousPatron');
205 my $warnPrefAnonymousPatronOPACPrivacy = (
206 C4::Context->preference('OPACPrivacy')
207 and not $AnonymousPatron
209 my $warnPrefAnonymousPatronAnonSuggestions = (
210 C4::Context->preference('AnonSuggestions')
211 and not $AnonymousPatron
214 my $anonymous_patron = Koha::Patrons->find( $AnonymousPatron );
215 my $warnPrefAnonymousPatronAnonSuggestions_PatronDoesNotExist = ( $AnonymousPatron && C4::Context->preference('AnonSuggestions') && not $anonymous_patron );
217 my $warnPrefAnonymousPatronOPACPrivacy_PatronDoesNotExist = ( not $anonymous_patron and Koha::Patrons->search({ privacy => 2 })->count );
219 my $warnPrefKohaAdminEmailAddress = !Koha::Email->is_valid(C4::Context->preference('KohaAdminEmailAddress'));
221 my $c = Koha::Items->filter_by_visible_in_opac->count;
222 my @warnings = C4::Context->dbh->selectrow_array('SHOW WARNINGS');
223 my $warnPrefOpacHiddenItems = $warnings[2];
225 my $invalid_yesno = Koha::Config::SysPrefs->search(
228 value => { -or => { 'is' => undef, -not_in => [ "1", "0" ] } }
231 $template->param( invalid_yesno => $invalid_yesno );
233 my $errZebraConnection = C4::Context->Zconn("biblioserver",0)->errcode();
235 my $warnIsRootUser = (! $loggedinuser);
237 my $warnNoActiveCurrency = (! defined Koha::Acquisition::Currencies->get_active);
239 my @xml_config_warnings;
241 if ( C4::Context->config('zebra_bib_index_mode')
242 and C4::Context->config('zebra_bib_index_mode') eq 'grs1' )
244 push @xml_config_warnings, { error => 'zebra_bib_index_mode_is_grs1' };
247 if ( C4::Context->config('zebra_auth_index_mode')
248 and C4::Context->config('zebra_auth_index_mode') eq 'grs1' )
250 push @xml_config_warnings, { error => 'zebra_auth_index_mode_is_grs1' };
253 my $authorityserver = C4::Context->zebraconfig('authorityserver');
254 if( ( C4::Context->config('zebra_auth_index_mode')
255 and C4::Context->config('zebra_auth_index_mode') eq 'dom' )
256 && ( $authorityserver->{config} !~ /zebra-authorities-dom.cfg/ ) )
258 push @xml_config_warnings, {
259 error => 'zebra_auth_index_mode_mismatch_warn'
263 if ( ! defined C4::Context->config('log4perl_conf') ) {
264 push @xml_config_warnings, {
265 error => 'log4perl_entry_missing'
269 if ( ! defined C4::Context->config('lockdir') ) {
270 push @xml_config_warnings, {
271 error => 'lockdir_entry_missing'
275 unless ( -w C4::Context->config('lockdir') ) {
276 push @xml_config_warnings, {
277 error => 'lockdir_not_writable',
278 lockdir => C4::Context->config('lockdir')
283 if ( ! defined C4::Context->config('upload_path') ) {
284 if ( Koha::Config::SysPrefs->find('OPACBaseURL')->value ) {
285 # OPACBaseURL seems to be set
286 push @xml_config_warnings, {
287 error => 'uploadpath_entry_missing'
290 push @xml_config_warnings, {
291 error => 'uploadpath_and_opacbaseurl_entry_missing'
296 if ( ! C4::Context->config('tmp_path') ) {
297 my $temporary_directory = C4::Context::temporary_directory;
298 push @xml_config_warnings, {
299 error => 'tmp_path_missing',
300 effective_tmp_dir => $temporary_directory,
304 my $encryption_key = C4::Context->config('encryption_key');
305 if ( !$encryption_key || $encryption_key eq '__ENCRYPTION_KEY__') {
306 push @xml_config_warnings, { error => 'encryption_key_missing' };
309 # Test Zebra facets configuration
310 if ( !defined C4::Context->config('use_zebra_facets') ) {
311 push @xml_config_warnings, { error => 'use_zebra_facets_entry_missing' };
314 unless ( Koha::I18N->_base_directory ) {
315 $template->param( warnI18nMissing => 1 );
319 if ( C4::Context->preference('ILLModule') ) {
320 my $warnILLConfiguration = 0;
321 my $ill_config_from_file = C4::Context->config("interlibrary_loans");
322 my $ill_config = Koha::ILL::Request::Config->new;
324 my $available_ill_backends =
325 ( scalar @{ $ill_config->available_backends } > 0 );
328 if ( !$available_ill_backends ) {
329 $template->param( no_ill_backends => 1 );
330 $warnILLConfiguration = 1;
333 # Check ILLPartnerCode sys pref
334 if ( !Koha::Patron::Categories->find( C4::Context->preference('ILLPartnerCode') ) ) {
335 $template->param( ill_partner_code_doesnt_exist => C4::Context->preference('ILLPartnerCode') );
336 $warnILLConfiguration = 1;
337 } elsif ( !Koha::Patrons->search( { categorycode => C4::Context->preference('ILLPartnerCode') } )->count ) {
338 $template->param( ill_partner_code_no_patrons => C4::Context->preference('ILLPartnerCode') );
339 $warnILLConfiguration = 1;
342 if ( !C4::Context->preference('ILLPartnerCode') ) {
343 # partner code not defined
344 $template->param( ill_partner_code_not_defined => 1 );
345 $warnILLConfiguration = 1;
349 if ( !$ill_config_from_file->{branch} ) {
351 $template->param( ill_branch_not_defined => 1 );
352 $warnILLConfiguration = 1;
355 $template->param( warnILLConfiguration => $warnILLConfiguration );
357 unless ( can_run('weasyprint') ) {
358 $template->param( weasyprint_missing => 1 );
364 OPACXSLTDetailsDisplay
366 OPACXSLTResultsDisplay
372 for my $p ( @xslt_prefs ) {
373 my $xsl_filename = C4::XSLT::get_xsl_filename( $p );
374 next if -e $xsl_filename;
378 value => C4::Context->preference("$p"),
379 filename => $xsl_filename
383 $template->param( warnXSLT => \@warnXSLT ) if @warnXSLT;
386 if ( C4::Context->preference('SearchEngine') eq 'Elasticsearch' ) {
387 # Check ES configuration health and runtime status
392 my $es_has_missing = 0;
396 $es_conf = Koha::SearchEngine::Elasticsearch::_read_configuration();
399 if ( ref($_) eq 'Koha::Exceptions::Config::MissingEntry' ) {
400 $template->param( elasticsearch_fatal_config_error => $_->message );
401 $es_config_error = 1;
404 if ( !$es_config_error ) {
406 my $biblios_index_name = $es_conf->{index_name} . "_" . $Koha::SearchEngine::BIBLIOS_INDEX;
407 my $authorities_index_name = $es_conf->{index_name} . "_" . $Koha::SearchEngine::AUTHORITIES_INDEX;
409 my @indexes = ($biblios_index_name, $authorities_index_name);
410 # TODO: When new indexes get added, we could have other ways to
411 # fetch the list of available indexes (e.g. plugins, etc)
412 $es_status->{nodes} = $es_conf->{nodes};
413 my $es = Search::Elasticsearch->new( $es_conf );
414 my $es_status->{version} = $es->info->{version}->{number};
416 foreach my $index ( @indexes ) {
419 $index_count = $es->indices->stats( index => $index )
420 ->{_all}{primaries}{docs}{count};
423 if ( ref($_) eq 'Search::Elasticsearch::Error::Missing' ) {
424 push @{ $es_status->{errors} }, "Index not found ($index)";
427 elsif ( ref($_) eq 'Search::Elasticsearch::Error::NoNodes' ) {
431 # TODO: when time comes, we will cover more use cases
437 my $missing_count = 0;
438 if ( $index eq $biblios_index_name ) {
439 $db_count = Koha::Biblios->search->count;
440 } elsif ( $index eq $authorities_index_name ) {
441 $db_count = Koha::Authorities->search->count;
443 if ( $db_count != -1 && $index_count != -1 ) {
444 $missing_count = $db_count - $index_count;
445 $es_has_missing = 1 if $missing_count > 0;
447 push @{ $es_status->{indexes} },
449 index_name => $index,
450 index_count => $index_count,
451 db_count => $db_count,
452 missing_count => $missing_count,
455 $es_status->{running} = $es_running;
458 elasticsearch_status => $es_status,
459 elasticsearch_has_missing => $es_has_missing,
464 if ( C4::Context->preference('RESTOAuth2ClientCredentials') ) {
465 # Do we have the required deps?
466 unless ( can_load( modules => { 'Net::OAuth2::AuthorizationServer' => undef }) ) {
467 $template->param( oauth2_missing_deps => 1 );
471 # Sco Patron should not contain any other perms than circulate => self_checkout
472 if ( C4::Context->preference('WebBasedSelfCheck')
473 and C4::Context->preference('AutoSelfCheckAllowed')
475 my $userid = C4::Context->preference('AutoSelfCheckID');
476 my $all_permissions = C4::Auth::get_user_subpermissions( $userid );
477 my ( $has_self_checkout_perm, $has_other_permissions );
478 while ( my ( $module, $permissions ) = each %$all_permissions ) {
479 if ( $module eq 'self_check' ) {
480 while ( my ( $permission, $flag ) = each %$permissions ) {
481 if ( $permission eq 'self_checkout_module' ) {
482 $has_self_checkout_perm = 1;
484 $has_other_permissions = 1;
488 $has_other_permissions = 1;
492 AutoSelfCheckPatronDoesNotHaveSelfCheckPerm => not ( $has_self_checkout_perm ),
493 AutoSelfCheckPatronHasTooManyPerm => $has_other_permissions,
497 if ( C4::Context->preference('PatronSelfRegistration') ) {
498 $template->param( warnPrefPatronSelfRegistrationDefaultCategory => 1 )
499 unless Koha::Patron::Categories->find(C4::Context->preference('PatronSelfRegistrationDefaultCategory'));
502 # Test YAML system preferences
503 # FIXME: This is list of current YAML formatted prefs, should by type of preference
505 "BibtexExportAdditionalFields",
506 "ItemsDeniedRenewal",
508 "MarcItemFieldsToOrder",
510 "RisExportAdditionalFields",
511 "UpdateitemLocationOnCheckin",
512 "UpdateItemWhenLostFromHoldList",
513 "UpdateNotForLoanStatusOnCheckin",
514 "UpdateNotForLoanStatusOnCheckout",
517 foreach my $syspref (@yaml_prefs) {
518 my $yaml = C4::Context->preference( $syspref );
520 eval { YAML::XS::Load( Encode::encode_utf8("$yaml\n\n") ); };
522 push @bad_yaml_prefs, $syspref;
526 $template->param( 'bad_yaml_prefs' => \@bad_yaml_prefs ) if @bad_yaml_prefs;
529 my $dbh = C4::Context->dbh;
530 my $patrons = $dbh->selectall_arrayref(
531 q|select b.borrowernumber from borrowers b join deletedborrowers db on b.borrowernumber=db.borrowernumber|,
534 my $biblios = $dbh->selectall_arrayref(
535 q|select b.biblionumber from biblio b join deletedbiblio db on b.biblionumber=db.biblionumber|,
538 my $biblioitems = $dbh->selectall_arrayref(
539 q|select bi.biblioitemnumber from biblioitems bi join deletedbiblioitems dbi on bi.biblionumber=dbi.biblionumber|,
542 my $items = $dbh->selectall_arrayref(
543 q|select i.itemnumber from items i join deleteditems di on i.itemnumber=di.itemnumber|,
546 my $checkouts = $dbh->selectall_arrayref(
547 q|select i.issue_id from issues i join old_issues oi on i.issue_id=oi.issue_id|,
550 my $holds = $dbh->selectall_arrayref(
551 q|select r.reserve_id from reserves r join old_reserves o on r.reserve_id=o.reserve_id|,
554 if ( @$patrons or @$biblios or @$biblioitems or @$items or @$checkouts or @$holds ) {
557 ai_patrons => $patrons,
558 ai_biblios => $biblios,
559 ai_biblioitems => $biblioitems,
561 ai_checkouts => $checkouts,
569 my $dbh = C4::Context->dbh;
570 my $units = Koha::CirculationRules->search({ rule_name => 'lengthunit', rule_value => { -not_in => ['days', 'hours'] } });
572 if ( $units->count ) {
574 warnIssuingRules => 1,
580 # Guarantor relationships warnings
582 my $dbh = C4::Context->dbh;
583 my ($bad_relationships_count) = $dbh->selectall_arrayref(q{
586 SELECT relationship FROM borrower_relationships WHERE relationship='_bad_data'
588 SELECT relationship FROM borrowers WHERE relationship='_bad_data') a
591 $bad_relationships_count = $bad_relationships_count->[0]->[0];
593 my $existing_relationships = $dbh->selectall_arrayref(q{
594 SELECT DISTINCT(relationship)
596 SELECT relationship FROM borrower_relationships WHERE relationship IS NOT NULL
598 SELECT relationship FROM borrowers WHERE relationship IS NOT NULL) a
601 my %valid_relationships = map { $_ => 1 } split( /,|\|/, C4::Context->preference('borrowerRelationship') );
602 $valid_relationships{ _bad_data } = 1; # we handle this case in another way
604 my $wrong_relationships = [ grep { !$valid_relationships{ $_->[0] } } @{$existing_relationships} ];
605 if ( @$wrong_relationships or $bad_relationships_count ) {
608 warnRelationships => 1,
611 if ( $wrong_relationships ) {
613 wrong_relationships => $wrong_relationships
616 if ($bad_relationships_count) {
618 bad_relationships_count => $bad_relationships_count,
625 # Test 'bcrypt_settings' config for Pseudonymization
626 $template->param( config_bcrypt_settings_no_set => 1 )
627 if C4::Context->preference('Pseudonymization')
628 and not C4::Context->config('bcrypt_settings');
632 my @frameworkcodes = Koha::BiblioFrameworks->search->get_column('frameworkcode');
633 my @hidden_biblionumbers;
634 push @frameworkcodes, ""; # it's not in the biblio_frameworks table!
635 my $no_FA_framework = 1;
636 for my $frameworkcode ( @frameworkcodes ) {
637 $no_FA_framework = 0 if $frameworkcode eq 'FA';
638 my $shouldhidemarc_opac = Koha::Filter::MARC::ViewPolicy->should_hide_marc(
640 frameworkcode => $frameworkcode,
644 push @hidden_biblionumbers, { frameworkcode => $frameworkcode, interface => 'opac' }
645 if $shouldhidemarc_opac->{biblionumber};
647 my $shouldhidemarc_intranet = Koha::Filter::MARC::ViewPolicy->should_hide_marc(
649 frameworkcode => $frameworkcode,
650 interface => "intranet"
653 push @hidden_biblionumbers, { frameworkcode => $frameworkcode, interface => 'intranet' }
654 if $shouldhidemarc_intranet->{biblionumber};
656 $template->param( warnHiddenBiblionumbers => \@hidden_biblionumbers );
657 $template->param( warnFastCataloging => $no_FA_framework );
661 # BackgroundJob - test connection to message broker
663 Koha::BackgroundJob->connect;
667 $template->param( warnConnectBroker => $@ );
671 #BZ 28267: Warn administrators if there are database rows with a format other than 'DYNAMIC'
673 $template->param( warnDbRowFormat => C4::Installer->has_non_dynamic_row_format );
677 prefRequireChoosingExistingAuthority => $prefRequireChoosingExistingAuthority,
678 prefAutoCreateAuthorities => $prefAutoCreateAuthorities,
679 warnPrefRequireChoosingExistingAuthority => $warnPrefRequireChoosingExistingAuthority,
680 warnPrefEasyAnalyticalRecords => $warnPrefEasyAnalyticalRecords,
681 warnPrefAnonymousPatronOPACPrivacy => $warnPrefAnonymousPatronOPACPrivacy,
682 warnPrefAnonymousPatronAnonSuggestions => $warnPrefAnonymousPatronAnonSuggestions,
683 warnPrefAnonymousPatronOPACPrivacy_PatronDoesNotExist => $warnPrefAnonymousPatronOPACPrivacy_PatronDoesNotExist,
684 warnPrefAnonymousPatronAnonSuggestions_PatronDoesNotExist => $warnPrefAnonymousPatronAnonSuggestions_PatronDoesNotExist,
685 warnPrefKohaAdminEmailAddress => $warnPrefKohaAdminEmailAddress,
686 warnPrefOpacHiddenItems => $warnPrefOpacHiddenItems,
687 errZebraConnection => $errZebraConnection,
688 warnIsRootUser => $warnIsRootUser,
689 warnNoActiveCurrency => $warnNoActiveCurrency,
690 warnNoTemplateCaching => ( C4::Context->config('template_cache_dir') ? 0 : 1 ),
691 xml_config_warnings => \@xml_config_warnings,
692 warnStatisticsFieldsError => $warnStatisticsFieldsError,
697 my $perl_modules = C4::Installer::PerlModules->new;
698 $perl_modules->versions_info;
700 my @pm_types = qw(missing_pm upgrade_pm current_pm);
702 foreach my $pm_type(@pm_types) {
703 my $modules = $perl_modules->get_attr($pm_type);
704 foreach (@$modules) {
705 my ($module, $stats) = each %$_;
710 version => $stats->{'cur_ver'},
711 missing => ($pm_type eq 'missing_pm' ? 1 : 0),
712 upgrade => ($pm_type eq 'upgrade_pm' ? 1 : 0),
713 current => ($pm_type eq 'current_pm' ? 1 : 0),
714 require => $stats->{'required'},
715 reqversion => $stats->{'min_ver'},
716 maxversion => $stats->{'max_ver'},
717 excversion => $stats->{'exc_ver'}
723 @components = sort {$a->{'name'} cmp $b->{'name'}} @components;
728 foreach (@components) {
730 unless (++$counter % 4) {
731 push (@$table, {row => $row});
735 # Processing the last line (if there are any modules left)
736 if (scalar(@$row) > 0) {
737 # Extending $row to the table size
739 # Pushing the last line
740 push (@$table, {row => $row});
744 $template->param( table => $table );
747 ## ------------------------------------------
748 ## Koha contributions
752 -e "$docdir" . "/teams.yaml"
753 ? YAML::XS::LoadFile( "$docdir" . "/teams.yaml" )
755 my $dev_team = (sort {$b <=> $a} (keys %{$teams->{team}}))[0];
756 my $short_version = substr($versions{'kohaVersion'},0,5);
757 my $minor = substr($versions{'kohaVersion'},3,2);
758 my $development_version = ( $minor eq '05' || $minor eq '11' ) ? 0 : 1;
760 $template->param( short_version => $short_version );
761 $template->param( development_version => $development_version );
765 -e "$docdir" . "/contributors.yaml"
766 ? YAML::XS::LoadFile( "$docdir" . "/contributors.yaml" )
768 delete $contributors->{_others_};
769 for my $version ( sort { $a <=> $b } keys %{$teams->{team}} ) {
770 for my $role ( keys %{ $teams->{team}->{$version} } ) {
771 my $detail = $teams->{team}->{$version}->{$role};
772 my $normalized_role = "$role";
773 $normalized_role =~ s/s$//;
774 if ( ref( $detail ) eq 'ARRAY' ) {
775 for my $contributor ( @{ $detail } ) {
777 my $localized_role = $normalized_role;
778 if ( my $subversion = $contributor->{version} ) {
779 $localized_role .= ':' . $subversion;
781 if ( my $area = $contributor->{area} ) {
782 $localized_role .= ':' . $area;
785 my $name = $contributor->{name};
786 # Add role to contributors
787 push @{ $contributors->{$name}->{roles}->{$localized_role} },
789 # Add openhub to teams
790 if ( exists( $contributors->{$name}->{openhub} ) ) {
791 $contributor->{openhub} = $contributors->{$name}->{openhub};
795 elsif ( $role eq 'release_date' ) {
796 $teams->{team}->{$version}->{$role} = DateTime->from_epoch( epoch => $teams->{team}->{$version}->{$role} );
798 elsif ( $role eq 'codename' ) {
799 if ( $version == $short_version ) {
805 if ( my $subversion = $detail->{version} ) {
806 $normalized_role .= ':' . $subversion;
808 if ( my $area = $detail->{area} ) {
809 $normalized_role .= ':' . $area;
812 my $name = $detail->{name};
813 # Add role to contributors
814 push @{ $contributors->{$name}->{roles}->{$normalized_role} },
816 # Add openhub to teams
817 if ( exists( $contributors->{$name}->{openhub} ) ) {
819 $contributors->{$name}->{openhub};
825 ## Create last name ordered array of people from contributors
827 { name => $_, ( $contributors->{$_} ? %{ $contributors->{$_} } : () ) }
829 my ($alast) = $a =~ /(\S+)$/;
830 my ($blast) = $b =~ /(\S+)$/;
831 my $cmp = lc($alast||"") cmp lc($blast||"");
834 my ($a2last) = $a =~ /(\S+)\s\S+$/;
835 my ($b2last) = $b =~ /(\S+)\s\S+$/;
836 lc($a2last||"") cmp lc($b2last||"");
837 } keys %$contributors;
839 $template->param( kohaCodename => $codename);
840 $template->param( contributors => \@people );
841 $template->param( maintenance_team => $teams->{team}->{$dev_team} );
842 $template->param( release_team => $teams->{team}->{$short_version} );
845 if ( open( my $file, "<:encoding(UTF-8)", "$docdir" . "/history.txt" ) ) {
855 shift @lines; #remove header row
858 my ( $epoch, $date, $desc, $tag ) = split(/\t/);
859 if(!$desc && $date=~ /(?<=\d{4})\s+/) {
860 ($date, $desc)= ($`, $');
872 #foreach my $row2 (@rows2) {
875 push( @$table2, { row2 => $row2 } );
879 $template->param( table2 => $table2 );
881 $template->param( timeline_read_error => 1 );
884 output_html_with_http_headers $query, $cookie, $template->output;