From 0a471bfeda152207452884466d690794ce3672fc 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 Signed-off-by: Martin Renvoize Signed-off-by: Tomas Cohen Arazi --- 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 | 21 +++++ t/db_dependent/Koha/Patron/Consents.t | 45 ++++++++- 6 files changed, 189 insertions(+), 79 deletions(-) diff --git a/Koha/Patron.pm b/Koha/Patron.pm index 9c268051a4..902c6995d3 100644 --- a/Koha/Patron.pm +++ b/Koha/Patron.pm @@ -47,6 +47,7 @@ use Koha::Old::Checkouts; use Koha::OverdueRules; use Koha::Patron::Attributes; use Koha::Patron::Categories; +use Koha::Patron::Consents; use Koha::Patron::Debarments; use Koha::Patron::HouseboundProfile; use Koha::Patron::HouseboundRole; @@ -2704,6 +2705,29 @@ sub alert_subscriptions { return Koha::Subscriptions->search( { subscriptionid => \@subscription_ids } ); } +=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 b3759f0f37..c1b32ede80 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 @@ -32,38 +32,42 @@
- - [% 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 %] - +
+ +
[% IF Koha.Preference('CookieConsent') %] @@ -71,7 +75,7 @@ [% END %] -
+
@@ -80,27 +84,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 8863bc5be0..de97129318 100755 --- a/t/db_dependent/Koha/Patron.t +++ b/t/db_dependent/Koha/Patron.t @@ -1859,6 +1859,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 } }); @@ -1892,6 +1893,7 @@ subtest 'update privacy tests' => sub { subtest 'alert_subscriptions tests' => sub { plan tests => 3; + $schema->storage->txn_begin; my $patron = $builder->build_object( { class => 'Koha::Patrons' } ); @@ -1908,6 +1910,25 @@ subtest 'alert_subscriptions tests' => sub { is( $subscriptions[1]->subscriptionid, $subscription2->subscriptionid, "Second subscribed alert is correct" ); $patron->discard_changes; + $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; }; subtest 'update_lastseen tests' => sub { 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.39.5