From f33c08ee3a56af07756de425aca82e5f81e61da1 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 --- 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 | 20 ++++ t/db_dependent/Koha/Patron/Consents.t | 45 ++++++++- 6 files changed, 188 insertions(+), 79 deletions(-) diff --git a/Koha/Patron.pm b/Koha/Patron.pm index 18e9d234e0..c675e8d5c2 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; @@ -2546,6 +2547,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 94f4f2fb67..af7644d784 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,38 +36,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') %] @@ -75,7 +79,7 @@ [% END %] -
+
@@ -84,27 +88,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 b2c82dfb9c..52d62962fe 100755 --- a/t/db_dependent/Koha/Patron.t +++ b/t/db_dependent/Koha/Patron.t @@ -1522,6 +1522,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 } }); @@ -1548,4 +1549,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.41.0