From 3ab40366363cd77446d208163ec9951bf687c22a Mon Sep 17 00:00:00 2001 From: Pedro Amorim Date: Tue, 29 Aug 2023 12:52:00 +0000 Subject: [PATCH] Bug 34587: New sushi counter class Moves a lot of the logic for creating a counter file and parsing a SUSHI response to a new class to simplify logic Signed-off-by: Jessica Zairo Signed-off-by: Michaela Sieber Signed-off-by: Nick Clemens Signed-off-by: Tomas Cohen Arazi --- Koha/ERM/EUsage/CounterFile.pm | 7 +- Koha/ERM/EUsage/SushiCounter.pm | 656 +++++++++++++++++++++++ Koha/ERM/EUsage/UsageDataProvider.pm | 751 ++------------------------- 3 files changed, 708 insertions(+), 706 deletions(-) create mode 100644 Koha/ERM/EUsage/SushiCounter.pm diff --git a/Koha/ERM/EUsage/CounterFile.pm b/Koha/ERM/EUsage/CounterFile.pm index 406104d76b..6534b1749d 100644 --- a/Koha/ERM/EUsage/CounterFile.pm +++ b/Koha/ERM/EUsage/CounterFile.pm @@ -31,6 +31,7 @@ use Koha::ERM::EUsage::UsageItems; use Koha::ERM::EUsage::UsageTitle; use Koha::ERM::EUsage::UsageTitles; use Koha::ERM::EUsage::UsageDataProvider; +use Koha::ERM::EUsage::SushiCounter; use Koha::Exceptions::ERM::EUsage::CounterFile; use base qw(Koha::Object); @@ -214,7 +215,7 @@ sub _add_monthly_usage_entries { my $usage_data_provider = $self->get_usage_data_provider; my $usage_object_info = $self->_get_usage_object_id_hash($usage_object); - my $specific_fields = $usage_data_provider->get_report_type_specific_fields( $self->type ); + my $specific_fields = Koha::ERM::EUsage::SushiCounter->get_report_type_specific_fields( $self->type ); $usage_object->monthly_usages( [ @@ -246,7 +247,7 @@ sub _add_yearly_usage_entries { my $usage_data_provider = $self->get_usage_data_provider; my $usage_object_info = $self->_get_usage_object_id_hash($usage_object); - my $specific_fields = $usage_data_provider->get_report_type_specific_fields( $self->type ); + my $specific_fields = Koha::ERM::EUsage::SushiCounter->get_report_type_specific_fields( $self->type ); while ( my ( $year, $usage ) = each( %{$yearly_usages} ) ) { @@ -529,7 +530,7 @@ sub _add_usage_object_entry { my ( $self, $row ) = @_; my $usage_data_provider = $self->get_usage_data_provider; - my $specific_fields = $usage_data_provider->get_report_type_specific_fields( $self->type ); + my $specific_fields = Koha::ERM::EUsage::SushiCounter->get_report_type_specific_fields( $self->type ); if ( $self->type =~ /PR/i ) { return Koha::ERM::EUsage::UsagePlatform->new( diff --git a/Koha/ERM/EUsage/SushiCounter.pm b/Koha/ERM/EUsage/SushiCounter.pm new file mode 100644 index 0000000000..a685297eb0 --- /dev/null +++ b/Koha/ERM/EUsage/SushiCounter.pm @@ -0,0 +1,656 @@ +package Koha::ERM::EUsage::SushiCounter; + +# Copyright 2023 PTFS Europe + +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use Modern::Perl; + +use Text::CSV_XS qw( csv ); + +use base qw(Koha::Object); + +=head1 NAME + +Koha::ERM::EUsage::SushiCounter - Koha SushiCounter Object class + +=head1 API + +=head2 Class Methods + +=head3 new + + my $sushi_counter = + Koha::ERM::EUsage::SushiCounter->new( { response => decode_json( $response->decoded_content ) } ); + +=cut + +sub new { + my ( $class, $params ) = @_; + + my $self = $class->SUPER::new; + $self->{sushi} = { + header => $params->{response}->{Report_Header}, + body => $params->{response}->{Report_Items} + }; + + #TODO: Handle empty $self->{sushi}->{body} here! + + return $self; +} + +=head3 get_COUNTER_from_SUSHI + + $self->get_COUNTER_from_SUSHI; + +Get the COUNTER file generated from the SUSHI response + +=cut + +sub get_COUNTER_from_SUSHI { + my ($self) = @_; + + # Get ready to build COUNTER file + my @report_header = $self->_COUNTER_report_header; + my @report_column_headings = $self->_COUNTER_report_column_headings; + my @report_body = $self->_COUNTER_report_body; + + return $self->_build_COUNTER_report_file( \@report_header, \@report_column_headings, \@report_body ); +} + +=head2 Internal methods + +=head3 _build_COUNTER_report_file + +Build the COUNTER file +https://cop5.projectcounter.org/en/5.0.2/03-specifications/02-formats-for-counter-reports.html#report-header + +=cut + +sub _build_COUNTER_report_file { + my ( $self, $header, $column_headings, $body ) = @_; + + my @report = ( @{$header}, @{$column_headings}, @{$body} ); + + #TODO: change this to tab instead of comma + csv( in => \@report, out => \my $counter_file, encoding => "utf-8" ); + + return $counter_file; + +} + +=head3 _COUNTER_report_header + +Return a COUNTER report header +https://cop5.projectcounter.org/en/5.0.2/04-reports/03-title-reports.html + +=cut + +sub _COUNTER_report_header { + my ($self) = @_; + + my $header = $self->{sushi}->{header}; + + my @metric_types_string = $self->_get_SUSHI_Name_Value( $header->{Report_Filters}, "Metric_Type" ); + + my $begin_date = $self->_get_SUSHI_Name_Value( $header->{Report_Filters}, "Begin_Date" ); + my $end_date = $self->_get_SUSHI_Name_Value( $header->{Report_Filters}, "End_Date" ); + + return ( + [ Report_Name => $header->{Report_Name} || "" ], + [ Report_ID => $header->{Report_ID} || "" ], + [ Release => $header->{Release} || "" ], + [ Institution_Name => $header->{Institution_Name} || "" ], + [ Institution_ID => join( "; ", map( $_->{Type} . ":" . $_->{Value}, @{ $header->{Institution_ID} } ) ) || "" ], + [ Metric_Types => join( "; ", split( /\|/, $metric_types_string[0] ) ) || "" ], + [ Report_Filters => join( "; ", map( $_->{Name} . ":" . $_->{Value}, @{ $header->{Report_Filters} } ) ) || "" ], + + #TODO: Report_Attributes may need parsing, test this with a SUSHI response that provides it + [ Report_Attributes => $header->{Report_Attributes} || "" ], + [ + Exceptions => join( + "; ", map( $_->{Code} . ": " . $_->{Message} . " (" . $_->{Data} . ")", @{ $header->{Exceptions} } ) + ) + || "" + ], + [ Reporting_Period => "Begin_Date=" . $begin_date . "; End_Date=" . $end_date ], + [ Created => $header->{Created} || "" ], + [ Created_By => $header->{Created_By} || "" ], + [""] #empty 13th line (COUNTER 5) + ); +} + +=head3 _COUNTER_item_report_row + +Return a COUNTER item for the COUNTER items report body +https://cop5.projectcounter.org/en/5.0.2/04-reports/04-item-reports.html#column-headings-elements + +=cut + +sub _COUNTER_item_report_row { + my ( $self, $item_row, $metric_type, $total_usage, $monthly_usages ) = @_; + + return ( + [ + $item_row->{Item} || "", + $item_row->{Publisher} || "", + $self->_get_SUSHI_Type_Value( $item_row->{Publisher_ID}, "ISNI" ) || "", + $item_row->{Platform} || "", + $self->_get_SUSHI_Type_Value( $item_row->{Item_ID}, "DOI" ) || "", + $item_row->{Proprietary_ID} || "", + "", #FIXME: What goes in URI? + $metric_type, + $total_usage, + @{$monthly_usages} + ] + ); +} + +=head3 _COUNTER_database_report_row + +Return a COUNTER database for the COUNTER databases report body +https://cop5.projectcounter.org/en/5.0.2/04-reports/02-database-reports.html#column-headings-elements + +=cut + +sub _COUNTER_database_report_row { + my ( $self, $database_row, $metric_type, $total_usage, $monthly_usages ) = @_; + + return ( + [ + $database_row->{Database} || "", + $database_row->{Publisher} || "", + $self->_get_SUSHI_Type_Value( $database_row->{Publisher_ID}, "ISNI" ) || "", + $database_row->{Platform} || "", + $database_row->{Proprietary_ID} || "", + $metric_type, + $total_usage, + @{$monthly_usages} + ] + ); +} + +=head3 _COUNTER_platform_report_row + +Return a COUNTER platform for the COUNTER platforms report body +https://cop5.projectcounter.org/en/5.0.2/04-reports/01-platform-reports.html#column-headings-elements + +=cut + +sub _COUNTER_platform_report_row { + my ( $self, $platform_row, $metric_type, $total_usage, $monthly_usages ) = @_; + + return ( + [ + $platform_row->{Platform} || "", $metric_type, + $total_usage, @{$monthly_usages} + ] + ); +} + +=head3 _COUNTER_title_report_row + +Return a COUNTER title for the COUNTER titles report body +https://cop5.projectcounter.org/en/5.0.2/04-reports/03-title-reports.html#column-headings-elements + +=cut + +sub _COUNTER_title_report_row { + my ( $self, $title_row, $metric_type, $total_usage, $monthly_usages ) = @_; + + my $header = $self->{sushi}->{header}; + my $specific_fields = $self->get_report_type_specific_fields( $header->{Report_ID} ); + + return ( + [ + # Title + $title_row->{Title} || "", + + # Publisher + $title_row->{Publisher} || "", + + # Publisher_ID + $self->_get_SUSHI_Type_Value( $title_row->{Publisher_ID}, "ISNI" ) || "", + + # Platform + $title_row->{Platform} || "", + + # DOI + $self->_get_SUSHI_Type_Value( $title_row->{Item_ID}, "DOI" ) || "", + + # Proprietary_ID + $self->_get_SUSHI_Type_Value( $title_row->{Item_ID}, "Proprietary" ) || "", + + # ISBN + grep ( /ISBN/, @{$specific_fields} ) + ? ( $self->_get_SUSHI_Type_Value( $title_row->{Item_ID}, "ISBN" ) || "" ) + : (), + + # Print_ISSN + $self->_get_SUSHI_Type_Value( $title_row->{Item_ID}, "Print_ISSN" ) || "", + + # Online_ISSN + $self->_get_SUSHI_Type_Value( $title_row->{Item_ID}, "Online_ISSN" ) || "", + + # URI - FIXME: What goes in URI? + "", + + # YOP + grep ( /YOP/, @{$specific_fields} ) ? ( $title_row->{YOP} || "" ) : (), + + # Access_Type + grep ( /Access_Type/, @{$specific_fields} ) ? ( $title_row->{Access_Type} || "" ) : (), + + # Metric_Type + $metric_type, + + # Report_Period_Total + $total_usage, + + # Monthly usage entries + @{$monthly_usages} + ] + ); +} + +=head3 _COUNTER_report_row + +Return a COUNTER row for the COUNTER report body + +=cut + +sub _COUNTER_report_row { + my ( $self, $report_row, $metric_type ) = @_; + + my $header = $self->{sushi}->{header}; + + my ( $total_usage, @monthly_usages ) = $self->_get_COUNTER_row_usages( $report_row, $metric_type ); + + if ( $header->{Report_ID} =~ /PR/i ) { + return $self->_COUNTER_platform_report_row( + $report_row, $metric_type, + $total_usage, \@monthly_usages + ); + } elsif ( $header->{Report_ID} =~ /DR/i ) { + return $self->_COUNTER_database_report_row( $report_row, $metric_type, $total_usage, \@monthly_usages ); + } elsif ( $header->{Report_ID} =~ /IR/i ) { + return $self->_COUNTER_item_report_row( $report_row, $metric_type, $total_usage, \@monthly_usages ); + } elsif ( $header->{Report_ID} =~ /TR/i ) { + return $self->_COUNTER_title_report_row( $report_row, $metric_type, $total_usage, \@monthly_usages ); + } +} + +=head3 _get_COUNTER_row_usages + +Returns the total and monthly usages for a row + +=cut + +sub _get_COUNTER_row_usages { + my ( $self, $row, $metric_type ) = @_; + + my @usage_months = $self->_get_usage_months( $self->{sushi}->{header} ); + + my @usage_months_fields = (); + my $count_total = 0; + + foreach my $usage_month (@usage_months) { + my $month_is_empty = 1; + + foreach my $performance ( @{ $row->{Performance} } ) { + my $period = $performance->{Period}; + my $period_usage_month = substr( $period->{Begin_Date}, 0, 7 ); + + my $instances = $performance->{Instance}; + my @metric_type_count = + map( $_->{Metric_Type} eq $metric_type ? $_->{Count} : (), + @{$instances} ); + + if ( $period_usage_month eq $usage_month && $metric_type_count[0] ) { + push( @usage_months_fields, $metric_type_count[0] ); + $count_total += $metric_type_count[0]; + $month_is_empty = 0; + } + } + + if ($month_is_empty) { + push( @usage_months_fields, 0 ); + } + } + return ( $count_total, @usage_months_fields ); +} + +=head3 _COUNTER_report_body + +Return the COUNTER report body as an array + +=cut + +sub _COUNTER_report_body { + my ($self) = @_; + + my $header = $self->{sushi}->{header}; + my $body = $self->{sushi}->{body}; + + my @report_body = (); + + my $total_records = 0; + foreach my $report_row ( @{$body} ) { + + my @metric_types = (); + + # Grab all metric_types this SUSHI result has statistics for + foreach my $performance ( @{ $report_row->{Performance} } ) { + my @SUSHI_metric_types = + map( $_->{Metric_Type}, @{ $performance->{Instance} } ); + + foreach my $sushi_metric_type (@SUSHI_metric_types) { + push( @metric_types, $sushi_metric_type ) unless grep { $_ eq $sushi_metric_type } @metric_types; + } + } + + # Add one report row for each metric_type we're working with + foreach my $metric_type (@metric_types) { + push( @report_body, $self->_COUNTER_report_row( $report_row, $metric_type ) ); + } + $self->{total_records} = ++$total_records; + } + + return @report_body; +} + +=head3 _get_SUSHI_Name_Value + +Returns "Value" of a given "Name" + +=cut + +sub _get_SUSHI_Name_Value { + my ( $self, $item, $name ) = @_; + + my @value = map( $_->{Name} eq $name ? $_->{Value} : (), @{$item} ); + + return $value[0]; +} + +=head3 _get_SUSHI_Type_Value + +Returns "Value" of a given "Type" + +=cut + +sub _get_SUSHI_Type_Value { + my ( $self, $item, $type ) = @_; + + my @value = map( $_->{Type} eq $type ? $_->{Value} : (), @{$item} ); + + return $value[0]; +} + +=head3 _COUNTER_report_column_headings + +Returns column headings by report type + Check the report type from the COUNTER header + and return column headings accordingly + +=cut + +sub _COUNTER_report_column_headings { + my ($self) = @_; + + my $header = $self->{sushi}->{header}; + + if ( $header->{Report_ID} =~ /PR/i ) { + return $self->_COUNTER_platforms_report_column_headings; + } elsif ( $header->{Report_ID} =~ /DR/i ) { + return $self->_COUNTER_databases_report_column_headings; + } elsif ( $header->{Report_ID} =~ /IR/i ) { + return $self->_COUNTER_items_report_column_headings; + } elsif ( $header->{Report_ID} =~ /TR/i ) { + return $self->_COUNTER_titles_report_column_headings; + } + + return; +} + +=head3 _COUNTER_items_report_column_headings + +Return items report column headings + +=cut + +sub _COUNTER_items_report_column_headings { + my ($self) = @_; + + my $header = $self->{sushi}->{header}; + my @month_headings = $self->_get_usage_months( $header, 1 ); + + return ( + [ + "Item", + "Publisher", + "Publisher_ID", + "Platform", + + # "Authors", #IR_A1 only + # "Publication_Date", #IR_A1 only + # "Article_Version", #IR_A1 only + "DOI", + "Proprietary_ID", + + # "ISBN", #IR only + # "Print_ISSN", #IR_A1 only + # "Online_ISSN", #IR_A1 only + "URI", + + # "Parent_Title", #IR_A1 only + # "Parent_Authors", #IR_A1 only + # "Parent_Publication_Date", #IR only + # "Parent_Article_Version", #IR_A1 only + # "Parent_Data_Type", #IR only + # "Parent_DOI", #IR_A1 only + # "Parent_Proprietary_ID", #IR_A1 only + # "Parent_ISBN", #IR only + # "Parent_Print_ISSN", #IR_A1 only + # "Parent_Online_ISSN", #IR_A1 only + # "Parent_URI", #IR_A1 only + # "Component_Title", #IR only + # "Component_Authors", #IR only + # "Component_Publication_Date", #IR only + # "Component_Data_Type", #IR only + # "Component_DOI", #IR only + # "Component_Proprietary_ID", #IR only + # "Component_ISBN", #IR only + # "Component_Print_ISSN", #IR only + # "Component_Online_ISSN", #IR only + # "Component_URI", #IR only + # "Data_Type", #IR only + # "YOP", #IR only + # "Access_Type", #IR_A1 only + # "Access_Method", #IR only + "Metric_Type", + "Reporting_Period_Total", + + # @month_headings in "Mmm-yyyy" format. TODO: Show unless Exclude_Monthly_Details=true + @month_headings + ] + ); +} + +=head3 _COUNTER_databases_report_column_headings + +Return databases report column headings + +=cut + +sub _COUNTER_databases_report_column_headings { + my ($self) = @_; + + my $header = $self->{sushi}->{header}; + my @month_headings = $self->_get_usage_months( $header, 1 ); + + return ( + [ + "Database", + "Publisher", + "Publisher_ID", + "Platform", + "Proprietary_ID", + "Metric_Type", + "Reporting_Period_Total", + + # @month_headings in "Mmm-yyyy" format. TODO: Show unless Exclude_Monthly_Details=true + @month_headings + ] + ); +} + +=head3 _COUNTER_platforms_report_column_headings + +Return platforms report column headings + +=cut + +sub _COUNTER_platforms_report_column_headings { + my ($self) = @_; + + my $header = $self->{sushi}->{header}; + my @month_headings = $self->_get_usage_months( $header, 1 ); + + return ( + [ + "Platform", + "Metric_Type", + "Reporting_Period_Total", + + # @month_headings in "Mmm-yyyy" format. TODO: Show unless Exclude_Monthly_Details=true + @month_headings + ] + ); +} + +=head3 _COUNTER_titles_report_column_headings + +Return titles report column headings + +=cut + +sub _COUNTER_titles_report_column_headings { + my ($self) = @_; + + my $header = $self->{sushi}->{header}; + my @month_headings = $self->_get_usage_months( $header, 1 ); + my $specific_fields = $self->get_report_type_specific_fields( $header->{Report_ID} ); + + return ( + [ + "Title", + "Publisher", + "Publisher_ID", + "Platform", + "DOI", + "Proprietary_ID", + grep ( /ISBN/, @{$specific_fields} ) ? ("ISBN") : (), + "Print_ISSN", + "Online_ISSN", + "URI", + + #"Data_Type", #TODO: Only if requested (?) + #"Section_Type", #TODO: Only if requested (?) + grep ( /YOP/, @{$specific_fields} ) ? ("YOP") : (), + grep ( /Access_Type/, @{$specific_fields} ) ? ("Access_Type") : (), + + #"Access_Method", #TODO: Only if requested (?) + "Metric_Type", + "Reporting_Period_Total", + + # @month_headings in "Mmm-yyyy" format. TODO: Show unless Exclude_Monthly_Details=true + @month_headings + ] + ); +} + +=head3 _get_usage_months + +Return report usage months. Formatted for column headings if $column_headings_formatting + +=cut + +sub _get_usage_months { + my ( $self, $header, $column_headings_formatting ) = @_; + + my @months = ( + "Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" + ); + + my @begin_date = map( $_->{Name} eq "Begin_Date" ? $_->{Value} : (), @{ $header->{Report_Filters} } ); + my $begin_month = substr( $begin_date[0], 5, 2 ); + my $begin_year = substr( $begin_date[0], 0, 4 ); + + my @end_date = map( $_->{Name} eq "End_Date" ? $_->{Value} : (), @{ $header->{Report_Filters} } ); + my $end_month = substr( $end_date[0], 5, 2 ); + my $end_year = substr( $end_date[0], 0, 4 ); + + my @month_headings = (); + while ( $begin_month <= $end_month || $begin_year < $end_year ) { + push( + @month_headings, + $column_headings_formatting + ? $months[ $begin_month - 1 ] . " " . $begin_year + : $begin_year . "-" . $begin_month + ); + $begin_month++; + if ( $begin_month > 12 ) { + $begin_month = 1; + $begin_year++; + } + $begin_month = "0" . $begin_month if length($begin_month) == 1; + } + + return @month_headings; +} + +=head3 get_report_type_specific_fields + +Returns the specific fields for a given report_type + +=cut + +sub get_report_type_specific_fields { + my ( $self, $report_type ) = @_; + + my %report_type_map = ( + "TR_B1" => [ 'YOP', 'ISBN' ], + "TR_B2" => [ 'YOP', 'ISBN' ], + "TR_B3" => [ 'YOP', 'Access_Type', 'ISBN' ], + "TR_J3" => ['Access_Type'], + "TR_J4" => ['YOP'], + ); + + return $report_type_map{$report_type}; + +} + +=head3 _type + +=cut + +sub _type { + return 'ErmSushiCounter'; +} + +1; diff --git a/Koha/ERM/EUsage/UsageDataProvider.pm b/Koha/ERM/EUsage/UsageDataProvider.pm index c62bb1d006..00d3e8f7e6 100644 --- a/Koha/ERM/EUsage/UsageDataProvider.pm +++ b/Koha/ERM/EUsage/UsageDataProvider.pm @@ -22,7 +22,6 @@ use Modern::Perl; use HTTP::Request; use JSON qw( decode_json ); use LWP::UserAgent; -use Text::CSV_XS qw( csv ); use Koha::Exceptions; @@ -35,6 +34,7 @@ use Koha::ERM::EUsage::UsageItems; use Koha::ERM::EUsage::UsagePlatforms; use Koha::ERM::EUsage::UsageDatabases; use Koha::ERM::EUsage::MonthlyUsages; +use Koha::ERM::EUsage::SushiCounter; use Koha::BackgroundJob::ErmSushiHarvester; =head1 NAME @@ -216,120 +216,13 @@ sub harvest_sushi { } # Parse the SUSHI response - $self->parse_SUSHI_response( decode_json( $response->decoded_content ) ); -} - -=head3 set_background_job_callbacks - - $self->set_background_job_callbacks($background_job_callbacks); - -Sets the background job callbacks - -=over - -=item background_job_callbacks - -Background job callbacks - -=back - -=cut - -sub set_background_job_callbacks { - my ( $self, $background_job_callbacks ) = @_; - - $self->{job_callbacks} = $background_job_callbacks; -} - -=head3 parse_SUSHI_response - - $self->parse_SUSHI_response( decode_json( $response->decoded_content ) ); - -Parse the SUSHI response, prepare the COUNTER report file header, -column headings and body - -=over - -=item result - -The result of the SUSHI response after json decoded - -=back + my $sushi_counter = + Koha::ERM::EUsage::SushiCounter->new( { response => decode_json( $response->decoded_content ) } ); + my $counter_file = $sushi_counter->get_COUNTER_from_SUSHI; -=cut - -sub parse_SUSHI_response { - my ( $self, $result ) = @_; - - # Set class wide sushi response content - $self->{sushi} = { - header => $result->{Report_Header}, - body => $result->{Report_Items} - }; - - #TODO: Handle empty $self->{sushi}->{body} here! - - # Get ready to build COUNTER file - my @report_header = $self->_COUNTER_report_header; - my @report_column_headings = $self->_COUNTER_report_column_headings; - my @report_body = $self->_COUNTER_report_body; - - $self->_build_COUNTER_report_file( - \@report_header, - \@report_column_headings, \@report_body - ); -} - -=head2 Internal methods - -=head3 _build_url_query - -Build the URL query params for COUNTER 5 SUSHI request - -=cut - -sub _build_url_query { - my ($self) = @_; - - unless ( $self->service_url && $self->customer_id ) { - die sprintf - "SUSHI Harvesting config for usage data provider %d is missing service_url or customer_id\n", - $self->erm_usage_data_provider_id; - } - - # FIXME: service_url needs to end in 'reports/' - # below concat will result in a badly formed URL otherwise - # Either validate this on UI form, here, or both - my $url = $self->service_url; - - $url .= $self->{report_type}; - $url .= '?customer_id=' . $self->customer_id; - $url .= '&requestor_id=' . $self->requestor_id if $self->requestor_id; - $url .= '&api_key=' . $self->api_key if $self->api_key; - $url .= '&begin_date=' . $self->{begin_date} if $self->{begin_date}; - $url .= '&end_date=' . $self->{end_date} if $self->{end_date}; - - return $url; -} - -=head3 _build_COUNTER_report_file - -Build the COUNTER file -https://cop5.projectcounter.org/en/5.0.2/03-specifications/02-formats-for-counter-reports.html#report-header - -=cut - -sub _build_COUNTER_report_file { - my ( $self, $header, $column_headings, $body ) = @_; - - my @report = ( @{$header}, @{$column_headings}, @{$body} ); - - #TODO: change this to tab instead of comma - csv( in => \@report, out => \my $counter_file, encoding => "utf-8" ); - - $self->counter_files( + $self->counter_files( [ - { + { usage_data_provider_id => $self->erm_usage_data_provider_id, file_content => $counter_file, date_uploaded => POSIX::strftime( "%Y%m%d%H%M%S", localtime ), @@ -339,609 +232,29 @@ sub _build_COUNTER_report_file { } ] ); -} - -=head3 _COUNTER_report_header - -Return a COUNTER report header -https://cop5.projectcounter.org/en/5.0.2/04-reports/03-title-reports.html - -=cut - -sub _COUNTER_report_header { - my ($self) = @_; - - my $header = $self->{sushi}->{header}; - - my @metric_types_string = $self->_get_SUSHI_Name_Value( $header->{Report_Filters}, "Metric_Type" ); - - my $begin_date = $self->_get_SUSHI_Name_Value( $header->{Report_Filters}, "Begin_Date" ); - my $end_date = $self->_get_SUSHI_Name_Value( $header->{Report_Filters}, "End_Date" ); - - return ( - [ Report_Name => $header->{Report_Name} || "" ], - [ Report_ID => $header->{Report_ID} || "" ], - [ Release => $header->{Release} || "" ], - [ Institution_Name => $header->{Institution_Name} || "" ], - [ - Institution_ID => join( - "; ", - map( $_->{Type} . ":" . $_->{Value}, - @{ $header->{Institution_ID} } ) - ) - || "" - ], - [ Metric_Types => join( "; ", split( /\|/, $metric_types_string[0] ) ) || "" ], - [ - Report_Filters => join( - "; ", - map( $_->{Name} . ":" . $_->{Value}, - @{ $header->{Report_Filters} } ) - ) - || "" - ], - - #TODO: Report_Attributes may need parsing, test this with a SUSHI response that provides it - [ Report_Attributes => $header->{Report_Attributes} || "" ], - [ - Exceptions => join( - "; ", - map( $_->{Code} . ": " . $_->{Message} . " (" . $_->{Data} . ")", - @{ $header->{Exceptions} } ) - ) - || "" - ], - [ Reporting_Period => "Begin_Date=" . $begin_date . "; End_Date=" . $end_date ], - [ Created => $header->{Created} || "" ], - [ Created_By => $header->{Created_By} || "" ], - [""] #empty 13th line - ); -} - -=head3 _COUNTER_item_report_row - -Return a COUNTER item for the COUNTER items report body -https://cop5.projectcounter.org/en/5.0.2/04-reports/04-item-reports.html#column-headings-elements - -=cut - -sub _COUNTER_item_report_row { - my ( $self, $item_row, $metric_type, $total_usage, $monthly_usages ) = @_; - - return ( - [ - $item_row->{Item} - || "", - $item_row->{Publisher} - || "", - $self->_get_SUSHI_Type_Value( $item_row->{Publisher_ID}, "ISNI" ) - || "", - $item_row->{Platform} - || "", - $self->_get_SUSHI_Type_Value( $item_row->{Item_ID}, "DOI" ) - || "", - $item_row->{Proprietary_ID} - || "", - "", #FIXME: What goes in URI? - $metric_type, - $total_usage, - @{$monthly_usages} - ] - ); -} - -=head3 _COUNTER_database_report_row - -Return a COUNTER database for the COUNTER databases report body -https://cop5.projectcounter.org/en/5.0.2/04-reports/02-database-reports.html#column-headings-elements - -=cut - -sub _COUNTER_database_report_row { - my ( $self, $database_row, $metric_type, $total_usage, $monthly_usages ) = @_; - - return ( - [ - $database_row->{Database} || "", - $database_row->{Publisher} || "", - $self->_get_SUSHI_Type_Value( - $database_row->{Publisher_ID}, - "ISNI" - ) - || "", - $database_row->{Platform} || "", - $database_row->{Proprietary_ID} || "", - $metric_type, - $total_usage, - @{$monthly_usages} - ] - ); -} - -=head3 _COUNTER_platform_report_row - -Return a COUNTER platform for the COUNTER platforms report body -https://cop5.projectcounter.org/en/5.0.2/04-reports/01-platform-reports.html#column-headings-elements - -=cut - -sub _COUNTER_platform_report_row { - my ( $self, $platform_row, $metric_type, $total_usage, $monthly_usages ) = @_; - - return ( - [ - $platform_row->{Platform} || "", $metric_type, - $total_usage, @{$monthly_usages} - ] - ); -} - -=head3 _COUNTER_title_report_row - -Return a COUNTER title for the COUNTER titles report body -https://cop5.projectcounter.org/en/5.0.2/04-reports/03-title-reports.html#column-headings-elements - -=cut - -sub _COUNTER_title_report_row { - my ( $self, $title_row, $metric_type, $total_usage, $monthly_usages ) = @_; - - my $header = $self->{sushi}->{header}; - my $specific_fields = $self->get_report_type_specific_fields( $header->{Report_ID} ); - - return ( - [ - # Title - $title_row->{Title} || "", - - # Publisher - $title_row->{Publisher} || "", - - # Publisher_ID - $self->_get_SUSHI_Type_Value( $title_row->{Publisher_ID}, "ISNI" ) - || "", - - # Platform - $title_row->{Platform} || "", - - # DOI - $self->_get_SUSHI_Type_Value( $title_row->{Item_ID}, "DOI" ) || "", - - # Proprietary_ID - $self->_get_SUSHI_Type_Value( $title_row->{Item_ID}, "Proprietary" ) - || "", - - # ISBN - grep ( /ISBN/, @{$specific_fields} ) - ? ( $self->_get_SUSHI_Type_Value( $title_row->{Item_ID}, "ISBN" ) - || "" ) - : (), - - # Print_ISSN - $self->_get_SUSHI_Type_Value( $title_row->{Item_ID}, "Print_ISSN" ) - || "", - - # Online_ISSN - $self->_get_SUSHI_Type_Value( $title_row->{Item_ID}, "Online_ISSN" ) - || "", - - # URI - FIXME: What goes in URI? - "", - - # YOP - grep ( /YOP/, @{$specific_fields} ) - ? ( $title_row->{YOP} || "" ) - : (), - - # Access_Type - grep ( /Access_Type/, @{$specific_fields} ) - ? ( $title_row->{Access_Type} || "" ) - : (), - - # Metric_Type - $metric_type, - - # Report_Period_Total - $total_usage, - - # Monthly usage entries - @{$monthly_usages} - ] - ); -} - -=head3 _COUNTER_report_row - -Return a COUNTER row for the COUNTER report body - -=cut - -sub _COUNTER_report_row { - my ( $self, $report_row, $metric_type ) = @_; - - my $header = $self->{sushi}->{header}; - - my ( $total_usage, @monthly_usages ) = $self->_get_row_usages( $report_row, $metric_type ); - - if ( $header->{Report_ID} =~ /PR/i ) { - return $self->_COUNTER_platform_report_row( - $report_row, $metric_type, - $total_usage, \@monthly_usages - ); - } elsif ( $header->{Report_ID} =~ /DR/i ) { - return $self->_COUNTER_database_report_row( - $report_row, $metric_type, - $total_usage, \@monthly_usages - ); - } elsif ( $header->{Report_ID} =~ /IR/i ) { - return $self->_COUNTER_item_report_row( - $report_row, $metric_type, - $total_usage, \@monthly_usages - ); - } elsif ( $header->{Report_ID} =~ /TR/i ) { - return $self->_COUNTER_title_report_row( - $report_row, $metric_type, - $total_usage, \@monthly_usages - ); - } -} - -=head3 _get_row_usages - -Returns the total and monthly usages for a row - -=cut - -sub _get_row_usages { - my ( $self, $row, $metric_type ) = @_; - - my @usage_months = $self->_get_usage_months( $self->{sushi}->{header} ); - - my @usage_months_fields = (); - my $count_total = 0; - - foreach my $usage_month (@usage_months) { - my $month_is_empty = 1; - - foreach my $performance ( @{ $row->{Performance} } ) { - my $period = $performance->{Period}; - my $period_usage_month = substr( $period->{Begin_Date}, 0, 7 ); - - my $instances = $performance->{Instance}; - my @metric_type_count = - map( $_->{Metric_Type} eq $metric_type ? $_->{Count} : (), - @{$instances} ); - - if ( $period_usage_month eq $usage_month && $metric_type_count[0] ) { - push( @usage_months_fields, $metric_type_count[0] ); - $count_total += $metric_type_count[0]; - $month_is_empty = 0; - } - } - - if ($month_is_empty) { - push( @usage_months_fields, 0 ); - } - } - return ( $count_total, @usage_months_fields ); -} - -=head3 _COUNTER_report_body - -Return the COUNTER report body as an array - -=cut - -sub _COUNTER_report_body { - my ($self) = @_; - - my $header = $self->{sushi}->{header}; - my $body = $self->{sushi}->{body}; - - my @report_body = (); - - my $total_records = 0; - foreach my $report_row ( @{$body} ) { - - my @metric_types = (); - - # Grab all metric_types this SUSHI result has statistics for - foreach my $performance ( @{ $report_row->{Performance} } ) { - my @SUSHI_metric_types = - map( $_->{Metric_Type}, @{ $performance->{Instance} } ); - - foreach my $sushi_metric_type (@SUSHI_metric_types) { - push( @metric_types, $sushi_metric_type ) - unless grep { $_ eq $sushi_metric_type } @metric_types; - } - } - - # Add one report row for each metric_type we're working with - foreach my $metric_type (@metric_types) { - push( - @report_body, - $self->_COUNTER_report_row( $report_row, $metric_type ) - ); - } - $self->{total_records} = ++$total_records; - } - - return @report_body; -} - -=head3 _get_SUSHI_Name_Value - -Returns "Value" of a given "Name" - -=cut - -sub _get_SUSHI_Name_Value { - my ( $self, $item, $name ) = @_; - - my @value = map( $_->{Name} eq $name ? $_->{Value} : (), @{$item} ); - - return $value[0]; -} - -=head3 _get_SUSHI_Type_Value - -Returns "Value" of a given "Type" - -=cut - -sub _get_SUSHI_Type_Value { - my ( $self, $item, $type ) = @_; - - my @value = map( $_->{Type} eq $type ? $_->{Value} : (), @{$item} ); - - return $value[0]; -} - -=head3 _COUNTER_report_column_headings - -Returns column headings by report type - Check the report type from the COUNTER header - and return column headings accordingly - -=cut - -sub _COUNTER_report_column_headings { - my ($self) = @_; - - my $header = $self->{sushi}->{header}; - - if ( $header->{Report_ID} =~ /PR/i ) { - return $self->_COUNTER_platforms_report_column_headings; - } elsif ( $header->{Report_ID} =~ /DR/i ) { - return $self->_COUNTER_databases_report_column_headings; - } elsif ( $header->{Report_ID} =~ /IR/i ) { - return $self->_COUNTER_items_report_column_headings; - } elsif ( $header->{Report_ID} =~ /TR/i ) { - return $self->_COUNTER_titles_report_column_headings; - } - - return; -} - -=head3 _COUNTER_items_report_column_headings - -Return items report column headings - -=cut - -sub _COUNTER_items_report_column_headings { - my ($self) = @_; - - my $header = $self->{sushi}->{header}; - my @month_headings = $self->_get_usage_months( $header, 1 ); - - return ( - [ - "Item", - "Publisher", - "Publisher_ID", - "Platform", - - # "Authors", #IR_A1 only - # "Publication_Date", #IR_A1 only - # "Article_Version", #IR_A1 only - "DOI", - "Proprietary_ID", - - # "ISBN", #IR only - # "Print_ISSN", #IR_A1 only - # "Online_ISSN", #IR_A1 only - "URI", - - # "Parent_Title", #IR_A1 only - # "Parent_Authors", #IR_A1 only - # "Parent_Publication_Date", #IR only - # "Parent_Article_Version", #IR_A1 only - # "Parent_Data_Type", #IR only - # "Parent_DOI", #IR_A1 only - # "Parent_Proprietary_ID", #IR_A1 only - # "Parent_ISBN", #IR only - # "Parent_Print_ISSN", #IR_A1 only - # "Parent_Online_ISSN", #IR_A1 only - # "Parent_URI", #IR_A1 only - # "Component_Title", #IR only - # "Component_Authors", #IR only - # "Component_Publication_Date", #IR only - # "Component_Data_Type", #IR only - # "Component_DOI", #IR only - # "Component_Proprietary_ID", #IR only - # "Component_ISBN", #IR only - # "Component_Print_ISSN", #IR only - # "Component_Online_ISSN", #IR only - # "Component_URI", #IR only - # "Data_Type", #IR only - # "YOP", #IR only - # "Access_Type", #IR_A1 only - # "Access_Method", #IR only - "Metric_Type", - "Reporting_Period_Total", - - # @month_headings in "Mmm-yyyy" format. TODO: Show unless Exclude_Monthly_Details=true - @month_headings - ] - ); -} - -=head3 _COUNTER_databases_report_column_headings - -Return databases report column headings - -=cut - -sub _COUNTER_databases_report_column_headings { - my ($self) = @_; - - my $header = $self->{sushi}->{header}; - my @month_headings = $self->_get_usage_months( $header, 1 ); - - return ( - [ - "Database", - "Publisher", - "Publisher_ID", - "Platform", - "Proprietary_ID", - "Metric_Type", - "Reporting_Period_Total", - - # @month_headings in "Mmm-yyyy" format. TODO: Show unless Exclude_Monthly_Details=true - @month_headings - ] - ); -} - -=head3 _COUNTER_platforms_report_column_headings - -Return platforms report column headings - -=cut - -sub _COUNTER_platforms_report_column_headings { - my ($self) = @_; - - my $header = $self->{sushi}->{header}; - my @month_headings = $self->_get_usage_months( $header, 1 ); - - return ( - [ - "Platform", - "Metric_Type", - "Reporting_Period_Total", - - # @month_headings in "Mmm-yyyy" format. TODO: Show unless Exclude_Monthly_Details=true - @month_headings - ] - ); -} - -=head3 _COUNTER_titles_report_column_headings - -Return titles report column headings - -=cut -sub _COUNTER_titles_report_column_headings { - my ($self) = @_; - - my $header = $self->{sushi}->{header}; - my @month_headings = $self->_get_usage_months( $header, 1 ); - my $specific_fields = $self->get_report_type_specific_fields( $header->{Report_ID} ); - - return ( - [ - "Title", - "Publisher", - "Publisher_ID", - "Platform", - "DOI", - "Proprietary_ID", - grep ( /ISBN/, @{$specific_fields} ) ? ("ISBN") : (), - "Print_ISSN", - "Online_ISSN", - "URI", - - #"Data_Type", #TODO: Only if requested (?) - #"Section_Type", #TODO: Only if requested (?) - grep ( /YOP/, @{$specific_fields} ) ? ("YOP") : (), - grep ( /Access_Type/, @{$specific_fields} ) ? ("Access_Type") : (), - - #"Access_Method", #TODO: Only if requested (?) - "Metric_Type", - "Reporting_Period_Total", - - # @month_headings in "Mmm-yyyy" format. TODO: Show unless Exclude_Monthly_Details=true - @month_headings - ] - ); } -=head3 _get_usage_months - -Return report usage months. Formatted for column headings if $column_headings_formatting - -=cut - -sub _get_usage_months { - my ( $self, $header, $column_headings_formatting ) = @_; - - my @months = ( - "Jan", "Feb", "Mar", "Apr", "May", "Jun", - "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" - ); +=head3 set_background_job_callbacks - my @begin_date = map( $_->{Name} eq "Begin_Date" ? $_->{Value} : (), - @{ $header->{Report_Filters} } ); - my $begin_month = substr( $begin_date[0], 5, 2 ); - my $begin_year = substr( $begin_date[0], 0, 4 ); + $self->set_background_job_callbacks($background_job_callbacks); - my @end_date = map( $_->{Name} eq "End_Date" ? $_->{Value} : (), - @{ $header->{Report_Filters} } ); - my $end_month = substr( $end_date[0], 5, 2 ); - my $end_year = substr( $end_date[0], 0, 4 ); +Sets the background job callbacks - my @month_headings = (); - while ( $begin_month <= $end_month || $begin_year < $end_year ) { - push( - @month_headings, - $column_headings_formatting - ? $months[ $begin_month - 1 ] . " " . $begin_year - : $begin_year . "-" . $begin_month - ); - $begin_month++; - if ( $begin_month > 12 ) { - $begin_month = 1; - $begin_year++; - } - $begin_month = "0" . $begin_month if length($begin_month) == 1; - } +=over - return @month_headings; -} +=item background_job_callbacks -=head3 get_report_type_specific_fields +Background job callbacks -Returns the specific fields for a given report_type +=back =cut -sub get_report_type_specific_fields { - my ( $self, $report_type ) = @_; - - my %report_type_map = ( - "TR_B1" => [ 'YOP', 'ISBN' ], - "TR_B2" => [ 'YOP', 'ISBN' ], - "TR_B3" => [ 'YOP', 'Access_Type', 'ISBN' ], - "TR_J3" => ['Access_Type'], - "TR_J4" => ['YOP'], - ); - - return $report_type_map{$report_type}; +sub set_background_job_callbacks { + my ( $self, $background_job_callbacks ) = @_; + $self->{job_callbacks} = $background_job_callbacks; } =head3 test_connection @@ -1053,6 +366,38 @@ sub erm_usage_databases { return Koha::ERM::EUsage::UsageDatabases->_new_from_dbic($usage_database_rs); } +=head2 Internal methods + +=head3 _build_url_query + +Build the URL query params for COUNTER 5 SUSHI request + +=cut + +sub _build_url_query { + my ($self) = @_; + + unless ( $self->service_url && $self->customer_id ) { + die sprintf + "SUSHI Harvesting config for usage data provider %d is missing service_url or customer_id\n", + $self->erm_usage_data_provider_id; + } + + # FIXME: service_url needs to end in 'reports/' + # below concat will result in a badly formed URL otherwise + # Either validate this on UI form, here, or both + my $url = $self->service_url; + + $url .= $self->{report_type}; + $url .= '?customer_id=' . $self->customer_id; + $url .= '&requestor_id=' . $self->requestor_id if $self->requestor_id; + $url .= '&api_key=' . $self->api_key if $self->api_key; + $url .= '&begin_date=' . $self->{begin_date} if $self->{begin_date}; + $url .= '&end_date=' . $self->{end_date} if $self->{end_date}; + + return $url; +} + =head3 _type =cut -- 2.39.5