From 5e9e96fe491e4eed3adcfd8d03514411515a6461 Mon Sep 17 00:00:00 2001 From: Marcel de Rooy Date: Thu, 31 Aug 2023 08:40:29 +0000 Subject: [PATCH] Bug 31503: Make opac-patron-consent more generic Adds $patron->consent and $consents->available_types. Incorporates them into script/template. Provides two unit tests. Note: A follow-up patch helps you test this with an example plugin. Test plan: Run t/db_dependent/Koha/Patron.t Run t/db_dependent/Koha/Patron/Consents.t Toggle the value of pref PrivacyPolicyConsent and look at OPAC account, tab Consents. Signed-off-by: Marcel de Rooy Signed-off-by: David Nind --- Koha/Patron.pm | 24 +++++ Koha/Patron/Consents.pm | 27 ++++++ .../en/modules/opac-patron-consent.tt | 94 ++++++++++--------- opac/opac-patron-consent.pl | 57 +++++------ t/db_dependent/Koha/Patron.t | 22 ++++- t/db_dependent/Koha/Patron/Consents.t | 45 ++++++++- 6 files changed, 189 insertions(+), 80 deletions(-) diff --git a/Koha/Patron.pm b/Koha/Patron.pm index 6937ea904a..8c16da639b 100644 --- a/Koha/Patron.pm +++ b/Koha/Patron.pm @@ -44,6 +44,7 @@ use Koha::CurbsidePickups; use Koha::Old::Checkouts; use Koha::Patron::Attributes; use Koha::Patron::Categories; +use Koha::Patron::Consents; use Koha::Patron::Debarments; use Koha::Patron::HouseboundProfile; use Koha::Patron::HouseboundRole; @@ -2490,6 +2491,29 @@ sub get_savings { )->next->get_column('total_savings') // 0; } +=head3 consent + + my $consent = $patron->consent(TYPE); + + Returns the first consent of type TYPE (there should be only one) or a new instance + of Koha::Patron::Consent. + +=cut + +sub consent { + my ( $self, $type ) = @_; + Koha::Exceptions::MissingParameter->throw('Missing consent type') if !$type; + my $consents = Koha::Patron::Consents->search( + { + borrowernumber => $self->borrowernumber, + type => $type, + } + ); + return $consents && $consents->count + ? $consents->next + : Koha::Patron::Consent->new( { borrowernumber => $self->borrowernumber, type => $type } ); +} + =head2 Internal methods =head3 _type diff --git a/Koha/Patron/Consents.pm b/Koha/Patron/Consents.pm index fc75a55d41..7dcb14d6a9 100644 --- a/Koha/Patron/Consents.pm +++ b/Koha/Patron/Consents.pm @@ -20,7 +20,10 @@ package Koha::Patron::Consents; use Modern::Perl; use base qw(Koha::Objects); + +use C4::Context; use Koha::Patron::Consent; +use Koha::Plugins; =head1 NAME @@ -34,8 +37,32 @@ Koha::Objects class for handling patron consents =head2 Class Methods +=head3 available_types + + Returns an HASHref of available consent types like: + { type1 => {}, type2 => {}, .. } + + Checks preferences OPACCustomConsentTypes and PrivacyPolicyConsent. + Calls patron_consent_type plugins (if pref enabled). + + Note: The plugins return an ARRAYref with type, title and description like: + [ my_type => { title => { lang => 1, .. }, description => { lang => 2, .. } } ] + =cut +sub available_types { + my ($self) = shift; + my $response = {}; + $response->{GDPR_PROCESSING} = 1 if C4::Context->preference('PrivacyPolicyConsent'); + if ( C4::Context->preference('OPACCustomConsentTypes') ) { + foreach my $return ( Koha::Plugins->call('patron_consent_type') ) { + next if ref($return) ne 'ARRAY' or @$return != 2; # ignoring bad input + $response->{ $return->[0] } = $return->[1]; + } + } + return $response; +} + =head3 _type =cut diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-patron-consent.tt b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-patron-consent.tt index d6fe1dcafe..6069cd2ac0 100644 --- a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-patron-consent.tt +++ b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-patron-consent.tt @@ -36,41 +36,45 @@
- - [% IF Koha.Preference('PrivacyPolicyConsent') %] -
-

In order to keep you logged in, we need your consent to process personal data as specified in the privacy policy linked below.

-

Please save your consent below or log out. Thank you!

-
- [% END %] - -

Your consents

-
- [% IF Koha.Preference('PrivacyPolicyConsent') %] -

Privacy policy consents

- - + + +

Your consents

+
+ [% FOREACH consent IN consents %] + [% SET consent_type = consent.type %] + [% IF consent_type == 'GDPR_PROCESSING' %] +

Privacy policy consent

+

Please read the privacy policy.

+

In order to keep you logged in, we need your consent to process personal data as specified in the EU General Data Protection Regulation of May 25, 2018. If you would not agree, we will need to remove your account within a reasonable time.

+

Do you agree with our processing of your personal data as outlined in the policy?

+ [% ELSIF consent_types.$consent_type %] + [% SET consent_title = ( consent_types.$consent_type.title.$lang || consent_types.$consent_type.title.en ) %] + [% SET consent_desc = ( consent_types.$consent_type.description.$lang || consent_types.$consent_type.description.en ) %] +

[% consent_title | html %]

+

[% consent_desc | html %]

+

Do you agree?

+ [% ELSE %] +

Consent for [% consent_type | html %]

+

Do you agree?

+ [% END %]
-
  • -

    I have read the privacy policy and agree with your processing of my personal data as outlined therein.

    -

    Yes, I agree.
    - No, I do not agree. Please remove my account within a reasonable time.

    - [% IF gdpr_proc_consent %] -

    Your consent was registered on [% gdpr_proc_consent | $KohaDates with_hours => 1 %].

    - [% ELSIF gdpr_proc_refusal %] -

    You indicated recently that you do not consent, and we will process your request soon.

    - [% END %] -
-
-
- + Yes
+ No
+ [% IF consent.given_on %] + + [% ELSIF consent.refused_on %] +

We registered that you did not consent on [% consent.refused_on | html %].

+ [% END %] +
[% END %] - +
+ +
-
+
@@ -79,27 +83,29 @@ [% INCLUDE 'opac-bottom.inc' %] [% BLOCK jsinclude %] [% END %] diff --git a/opac/opac-patron-consent.pl b/opac/opac-patron-consent.pl index 49c121250e..565417d6fa 100755 --- a/opac/opac-patron-consent.pl +++ b/opac/opac-patron-consent.pl @@ -23,14 +23,12 @@ use CGI qw/-utf8/; use C4::Auth qw( get_template_and_user ); use C4::Output qw( output_html_with_http_headers ); use Koha::DateUtils qw( dt_from_string ); -use Koha::Patron::Consents; +use Koha::Exceptions::Patron; use Koha::Patrons; -use constant GDPR_PROCESSING => 'GDPR_PROCESSING'; - my $query = CGI->new; my $op = $query->param('op') // q{}; -my $gdpr_check = $query->param('gdpr_processing') // q{}; +my $vars = $query->Vars; my ( $template, $borrowernumber, $cookie ) = get_template_and_user({ template_name => "opac-patron-consent.tt", @@ -38,44 +36,37 @@ my ( $template, $borrowernumber, $cookie ) = get_template_and_user({ type => "opac", }); -my $patron = Koha::Patrons->find($borrowernumber); -my $gdpr_proc_consent; -if( C4::Context->preference('PrivacyPolicyConsent') ) { - $gdpr_proc_consent = Koha::Patron::Consents->search({ - borrowernumber => $borrowernumber, - type => GDPR_PROCESSING, - })->next; - $gdpr_proc_consent //= Koha::Patron::Consent->new({ - borrowernumber => $borrowernumber, - type => GDPR_PROCESSING, - }); +my $patron = Koha::Patrons->find($borrowernumber) + or Koha::Exceptions::Patron->throw("Patron id $borrowernumber not found"); + +# Get consent types and values +my @consents; +my $consent_types = Koha::Patron::Consents->available_types; +foreach my $consent_type ( sort keys %$consent_types) { + push @consents, $patron->consent($consent_type); } # Handle saves here -if( $op eq 'gdpr_proc_save' && $gdpr_proc_consent ) { - if( $gdpr_check eq 'agreed' ) { - $gdpr_proc_consent->given_on( dt_from_string() ); - $gdpr_proc_consent->refused_on( undef ); - } elsif( $gdpr_check eq 'disagreed' ) { - $gdpr_proc_consent->given_on( undef ); - $gdpr_proc_consent->refused_on( dt_from_string() ); - } - $gdpr_proc_consent->store; +my $needs_redirect; +foreach my $consent ( @consents ) { + my $check = $vars->{ "check_".$consent->type }; + next if !defined($check); # no choice made + $needs_redirect = 1 + if $consent->type eq q/GDPR_PROCESSING/ && !$check && C4::Context->preference('PrivacyPolicyConsent') eq 'Enforced'; + next if $consent->given_on && $check || $consent->refused_on && !$check; + # No update if no consent change + $consent->set({ + given_on => $check ? dt_from_string() : undef, + refused_on => $check ? undef : dt_from_string(), + })->store; } # If user refused GDPR consent and we enforce GDPR, logout (when saving) -if( $op =~ /save/ && C4::Context->preference('PrivacyPolicyConsent') eq 'Enforced' && $gdpr_proc_consent->refused_on ) -{ +if( $needs_redirect ) { print $query->redirect('/cgi-bin/koha/opac-main.pl?logout.x=1'); exit; } -$template->param( patron => $patron ); -if( $gdpr_proc_consent ) { - $template->param( - gdpr_proc_consent => $gdpr_proc_consent->given_on // q{}, - gdpr_proc_refusal => $gdpr_proc_consent->refused_on // q{}, - ); -} +$template->param( patron => $patron, consents => \@consents, consent_types => $consent_types ); output_html_with_http_headers $query, $cookie, $template->output, undef, { force_no_caching => 1 }; diff --git a/t/db_dependent/Koha/Patron.t b/t/db_dependent/Koha/Patron.t index 5d05a16efd..92ca508ac6 100755 --- a/t/db_dependent/Koha/Patron.t +++ b/t/db_dependent/Koha/Patron.t @@ -19,7 +19,7 @@ use Modern::Perl; -use Test::More tests => 24; +use Test::More tests => 25; use Test::Exception; use Test::Warn; @@ -1471,6 +1471,7 @@ subtest 'update privacy tests' => sub { plan tests => 5; + $schema->storage->txn_begin; my $patron = $builder->build_object({ class => 'Koha::Patrons', value => { privacy => 1 } }); my $old_checkout = $builder->build_object({ class => 'Koha::Old::Checkouts', value => { borrowernumber => $patron->id } }); @@ -1497,4 +1498,23 @@ subtest 'update privacy tests' => sub { is( $old_checkout->borrowernumber, $anon_patron->id, "Checkout is successfully anonymized"); is( $patron->privacy(), 2, "Patron privacy is successfully updated"); + $schema->storage->txn_rollback; +}; + +subtest 'test patron_consent' => sub { + plan tests => 4; + $schema->storage->txn_begin; + + my $patron = $builder->build_object( { class => 'Koha::Patrons' } ); + throws_ok { $patron->consent } 'Koha::Exceptions::MissingParameter', 'missing type'; + + my $consent = $patron->consent('GDPR_PROCESSING'); + is( ref $consent, 'Koha::Patron::Consent', 'return type check' ); + $consent->given_on('2021-02-03')->store; + undef $consent; + is( $patron->consent('GDPR_PROCESSING')->given_on, '2021-02-03 00:00:00', 'check date' ); + + is( $patron->consent('NOT_EXIST')->refused_on, undef, 'New empty object for new type' ); + + $schema->storage->txn_rollback; }; diff --git a/t/db_dependent/Koha/Patron/Consents.t b/t/db_dependent/Koha/Patron/Consents.t index ab9ed669d4..4d2bf54d8f 100755 --- a/t/db_dependent/Koha/Patron/Consents.t +++ b/t/db_dependent/Koha/Patron/Consents.t @@ -18,9 +18,12 @@ # along with Koha; if not, see . use Modern::Perl; +use Data::Dumper qw/Dumper/; +use Test::More tests => 2; +use Test::MockModule; +use Test::MockObject; -use Test::More tests => 1; - +use t::lib::Mocks; use t::lib::TestBuilder; use Koha::Database; @@ -46,3 +49,41 @@ subtest 'Basic tests for Koha::Patron::Consent' => sub { $schema->storage->txn_rollback; }; + +subtest 'Method available_types' => sub { + plan tests => 7; + $schema->storage->txn_begin; + + # Mock get_enabled_plugins + my $plugins = []; + my $plugins_module = Test::MockModule->new('Koha::Plugins'); + $plugins_module->mock( 'get_enabled_plugins', sub { return @$plugins } ); + my $plugin_1 = Test::MockObject->new; + my $data_1 = [ 'plugin_1', { title => { en => 'Title1' }, description => { en => 'Desc1' } } ]; + $plugin_1->mock( 'patron_consent_type', sub { return $data_1; } ); + my $plugin_2 = Test::MockObject->new; + my $data_2 = [ 'plugin_2', { title => { en => 'Title2' }, description => { en => 'Desc2' } } ]; + $plugin_2->mock( 'patron_consent_type', sub { return $data_2; } ); + $plugins = [ $plugin_1, $plugin_2 ]; + + t::lib::Mocks::mock_preference( 'PrivacyPolicyConsent', 'Enforced' ); + t::lib::Mocks::mock_preference( 'OPACCustomConsentTypes', 0 ); + my $types = Koha::Patron::Consents->available_types; + is( keys %$types, 1, 'Expect one plugin for privacy policy' ); + t::lib::Mocks::mock_preference( 'OPACCustomConsentTypes', 1 ); + $types = Koha::Patron::Consents->available_types; + is( keys %$types, 3, 'Expect three plugins when allowing custom consents' ); + t::lib::Mocks::mock_preference( 'PrivacyPolicyConsent', '' ); + $types = Koha::Patron::Consents->available_types; + is( keys %$types, 2, 'Expect two plugins, when pref disabled' ); + is( $types->{GDPR_PROCESSING}, undef, 'GDPR key should not be found' ); + is_deeply( $types->{plugin_2}, $data_2->[1], 'Check type hash' ); + + # Let plugin_2 return bad data (hashref) + $data_2 = { not_expected => 1 }; + $types = Koha::Patron::Consents->available_types; + is( keys %$types, 1, 'Expect one plugin, when plugin_2 fails' ); + is_deeply( $types->{plugin_1}, $data_1->[1], 'Check type hash' ); + + $schema->storage->txn_rollback; +}; -- 2.30.2