View | Details | Raw Unified | Return to bug 31503
Collapse All | Expand All

(-)a/Koha/Patron.pm (+24 lines)
Lines 46-51 use Koha::Old::Checkouts; Link Here
46
use Koha::OverdueRules;
46
use Koha::OverdueRules;
47
use Koha::Patron::Attributes;
47
use Koha::Patron::Attributes;
48
use Koha::Patron::Categories;
48
use Koha::Patron::Categories;
49
use Koha::Patron::Consents;
49
use Koha::Patron::Debarments;
50
use Koha::Patron::Debarments;
50
use Koha::Patron::HouseboundProfile;
51
use Koha::Patron::HouseboundProfile;
51
use Koha::Patron::HouseboundRole;
52
use Koha::Patron::HouseboundRole;
Lines 2716-2721 sub alert_subscriptions { Link Here
2716
    return Koha::Subscriptions->search( { subscriptionid => \@subscription_ids } );
2717
    return Koha::Subscriptions->search( { subscriptionid => \@subscription_ids } );
2717
}
2718
}
2718
2719
2720
=head3 consent
2721
2722
    my $consent = $patron->consent(TYPE);
2723
2724
    Returns the first consent of type TYPE (there should be only one) or a new instance
2725
    of Koha::Patron::Consent.
2726
2727
=cut
2728
2729
sub consent {
2730
    my ( $self, $type ) = @_;
2731
    Koha::Exceptions::MissingParameter->throw('Missing consent type') if !$type;
2732
    my $consents = Koha::Patron::Consents->search(
2733
        {
2734
            borrowernumber => $self->borrowernumber,
2735
            type           => $type,
2736
        }
2737
    );
2738
    return $consents && $consents->count
2739
        ? $consents->next
2740
        : Koha::Patron::Consent->new( { borrowernumber => $self->borrowernumber, type => $type } );
2741
}
2742
2719
=head2 Internal methods
2743
=head2 Internal methods
2720
2744
2721
=head3 _type
2745
=head3 _type
(-)a/Koha/Patron/Consents.pm (+27 lines)
Lines 20-26 package Koha::Patron::Consents; Link Here
20
use Modern::Perl;
20
use Modern::Perl;
21
21
22
use base qw(Koha::Objects);
22
use base qw(Koha::Objects);
23
24
use C4::Context;
23
use Koha::Patron::Consent;
25
use Koha::Patron::Consent;
26
use Koha::Plugins;
24
27
25
=head1 NAME
28
=head1 NAME
26
29
Lines 34-41 Koha::Objects class for handling patron consents Link Here
34
37
35
=head2 Class Methods
38
=head2 Class Methods
36
39
40
=head3 available_types
41
42
    Returns an HASHref of available consent types like:
43
        { type1 => {}, type2 => {}, .. }
44
45
    Checks preferences OPACCustomConsentTypes and PrivacyPolicyConsent.
46
    Calls patron_consent_type plugins (if pref enabled).
47
48
    Note: The plugins return an ARRAYref with type, title and description like:
49
        [ my_type => { title => { lang => 1, .. }, description => { lang => 2, .. } } ]
50
37
=cut
51
=cut
38
52
53
sub available_types {
54
    my ($self) = shift;
55
    my $response = {};
56
    $response->{GDPR_PROCESSING} = 1 if C4::Context->preference('PrivacyPolicyConsent');
57
    if ( C4::Context->preference('OPACCustomConsentTypes') ) {
58
        foreach my $return ( Koha::Plugins->call('patron_consent_type') ) {
59
            next if ref($return) ne 'ARRAY' or @$return != 2;    # ignoring bad input
60
            $response->{ $return->[0] } = $return->[1];
61
        }
62
    }
63
    return $response;
64
}
65
39
=head3 _type
66
=head3 _type
40
67
41
=cut
68
=cut
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-patron-consent.tt (-44 / +50 lines)
Lines 36-73 Link Here
36
            </div>
36
            </div>
37
            <div class="col-lg-10 order-first order-md-first order-lg-2">
37
            <div class="col-lg-10 order-first order-md-first order-lg-2">
38
                <div id="patronconsents" class="maincontent">
38
                <div id="patronconsents" class="maincontent">
39
40
                    [% IF Koha.Preference('PrivacyPolicyConsent') %]
41
                    <div class="alert alert-warning">
42
                            <p>In order to keep you logged in, we need your consent to process personal data as specified in the privacy policy linked below.</p>
43
                            <p>Please save your consent below or log out. Thank you!</p>
44
                    </div>
45
                    [% END %]
46
47
                    <h1>Your consents</h1>
48
49
                    <form action="/cgi-bin/koha/opac-patron-consent.pl" method="post">
39
                    <form action="/cgi-bin/koha/opac-patron-consent.pl" method="post">
50
                        [% IF Koha.Preference('PrivacyPolicyConsent') %]
40
                        <input type="hidden" name="op" value="save"/>
51
                        <legend><h2 id="GDPR_consents">Privacy policy consents</h2></legend>
41
                        <input type="hidden" name="borrowernumber" value="[% patron.borrowernumber | html %]"/>
52
                            <input type="hidden" name="op" value="gdpr_proc_save"/>
42
                        <h1>Your consents</h1>
53
                            <input type="hidden" name="borrowernumber" value="[% patron.borrowernumber | html %]"/>
43
                        <br>
44
                        [% FOREACH consent IN consents %]
45
                            [% SET consent_type = consent.type %]
46
                            [% IF consent_type == 'GDPR_PROCESSING' %]
47
                                <legend><h2>Privacy policy consent</h2></legend>
48
                                <p>Please read the <a target="_blank" href="[% Koha.Preference('PrivacyPolicyURL') | url %]">privacy policy</a>.</p>
49
                                <p>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 <strong>remove</strong> your account within a reasonable time.</p>
50
                                <p>Do you agree with our processing of your personal data as outlined in the policy?</p>
51
                            [% ELSIF consent_types.$consent_type %]
52
                                [% SET consent_title = ( consent_types.$consent_type.title.$lang || consent_types.$consent_type.title.en ) %]
53
                                [% SET consent_desc  = ( consent_types.$consent_type.description.$lang || consent_types.$consent_type.description.en )  %]
54
                                <legend><h2>[% consent_title | html %]</h2></legend>
55
                                <p>[% consent_desc | html %]</p>
56
                                <p>Do you agree?</p>
57
                            [% ELSE %]
58
                                <legend><h2>Consent for [% consent_type | html %]</h2></legend>
59
                                <p>Do you agree?</p>
60
                            [% END %]
54
                            <fieldset>
61
                            <fieldset>
55
                                <ul><li>
62
                                <input type="radio" name="check_[% consent_type | html %]" value="1"> Yes<br>
56
                                    <p>I have read the <a target="_blank" href="[% Koha.Preference('PrivacyPolicyURL') | url %]">privacy policy</a> and agree with your processing of my personal data as outlined therein.</p>
63
                                <input type="radio" name="check_[% consent_type | html %]" value="0"> No
57
                                    <p><input type="radio" name="gdpr_processing" value="agreed"> Yes, I agree.<br>
58
                                    <input type="radio" name="gdpr_processing" value="disagreed"> No, I do not agree. Please remove my account within a reasonable time.</p>
59
                                    [% IF gdpr_proc_consent %]
60
                                        <p>Your consent was registered on [% gdpr_proc_consent | $KohaDates with_hours => 1 %].</p>
61
                                    [% ELSIF gdpr_proc_refusal %]
62
                                        <p>You indicated recently that you do not consent, and we will process your request soon.</p>
63
                                    [% END %]
64
                                </li></ul>
65
                            </fieldset>
66
                            <fieldset class="action">
67
                                <input id="saveconsent" type="submit" value="Save" class="btn btn-primary" />
68
                            </fieldset>
64
                            </fieldset>
65
                            [% IF consent.given_on %]
66
                                <p class="consent_info">Your consent was registered on [% consent.given_on | html %].</p>
67
                            [% ELSIF consent.refused_on %]
68
                                <p class="dissent_info">We registered that you did not consent on [% consent.refused_on | html %].</p>
69
                            [% END %]
70
                            <br>
69
                        [% END %]
71
                        [% END %]
70
72
                        <fieldset class="action">
73
                            <input id="saveconsent" type="submit" value="Save" class="btn btn-primary" />
74
                        </fieldset>
71
                    </form>
75
                    </form>
72
76
73
                    [% IF Koha.Preference('CookieConsent') %]
77
                    [% IF Koha.Preference('CookieConsent') %]
Lines 75-81 Link Here
75
                        <button id="viewCookieConsents" type="button" class="btn btn-success">View your cookie consents</button>
79
                        <button id="viewCookieConsents" type="button" class="btn btn-success">View your cookie consents</button>
76
                    [% END %]
80
                    [% END %]
77
81
78
                </div> <!-- / #userpasswd -->
82
                </div> <!-- / #patronconsents -->
79
            </div> <!-- / .col-lg-10 -->
83
            </div> <!-- / .col-lg-10 -->
80
        </div> <!-- / .row -->
84
        </div> <!-- / .row -->
81
    </div> <!-- / .container-fluid -->
85
    </div> <!-- / .container-fluid -->
Lines 84-110 Link Here
84
[% INCLUDE 'opac-bottom.inc' %]
88
[% INCLUDE 'opac-bottom.inc' %]
85
[% BLOCK jsinclude %]
89
[% BLOCK jsinclude %]
86
    <script>
90
    <script>
87
        var consent = null;
88
        $(document).ready(function() {
91
        $(document).ready(function() {
89
            [% IF gdpr_proc_consent %]
92
            [% FOREACH consent IN consents %]
90
                consent=1;
93
                [% IF consent.given_on %]
91
                $("input[type='radio'][value='agreed']").prop('checked',true);
94
                    $("input[type='radio'][name='check_[% consent.type | html %]'][value='1']").prop('checked',true);
92
                $(".alert").hide();
95
                [% ELSIF consent.refused_on %]
93
            [% ELSIF gdpr_proc_refusal %]
96
                    $("input[type='radio'][name='check_[% consent.type | html %]'][value='0']").prop('checked',true);
94
                consent=0;
97
                 [% END %]
95
                $("input[type='radio'][value='disagreed']").prop('checked',true);
96
            [% ELSE %]
97
            [% END %]
98
            [% END %]
99
            // Initially no choice is made or no change, so disable button
98
            $("#saveconsent").prop('disabled', true);
100
            $("#saveconsent").prop('disabled', true);
99
101
100
            $("input[type='radio']").click(function() {
102
            $("input[type='radio']").click(function() {
101
                var radio = $(this).val();
103
                $("#saveconsent").prop('disabled', false);
102
                if(radio=='agreed' && (consent==null || consent==0)) $("#saveconsent").prop('disabled', false);
104
                var v = $(this).val();
103
                if(radio=='disagreed' && (consent==null || consent==1)) $("#saveconsent").prop('disabled', false);
105
                // If former registration info present, toggle
104
                if(radio=='agreed') $(".alert").hide();
106
                if( v == 1 ) {
105
                if(radio=='disagreed') $(".alert").show();
107
                    $(this).parent().siblings('.consent_info').show();
108
                    $(this).parent().siblings('.dissent_info').hide();
109
                } else {
110
                    $(this).parent().siblings('.consent_info').hide();
111
                    $(this).parent().siblings('.dissent_info').show();
112
                }
106
            });
113
            });
107
108
        });
114
        });
109
    </script>
115
    </script>
110
[% END %]
116
[% END %]
(-)a/opac/opac-patron-consent.pl (-33 / +24 lines)
Lines 23-36 use CGI qw/-utf8/; Link Here
23
use C4::Auth qw( get_template_and_user );
23
use C4::Auth qw( get_template_and_user );
24
use C4::Output qw( output_html_with_http_headers );
24
use C4::Output qw( output_html_with_http_headers );
25
use Koha::DateUtils qw( dt_from_string );
25
use Koha::DateUtils qw( dt_from_string );
26
use Koha::Patron::Consents;
26
use Koha::Exceptions::Patron;
27
use Koha::Patrons;
27
use Koha::Patrons;
28
28
29
use constant GDPR_PROCESSING => 'GDPR_PROCESSING';
30
31
my $query = CGI->new;
29
my $query = CGI->new;
32
my $op = $query->param('op') // q{};
30
my $op = $query->param('op') // q{};
33
my $gdpr_check = $query->param('gdpr_processing') // q{};
31
my $vars = $query->Vars;
34
32
35
my ( $template, $borrowernumber, $cookie ) = get_template_and_user({
33
my ( $template, $borrowernumber, $cookie ) = get_template_and_user({
36
    template_name   => "opac-patron-consent.tt",
34
    template_name   => "opac-patron-consent.tt",
Lines 38-81 my ( $template, $borrowernumber, $cookie ) = get_template_and_user({ Link Here
38
    type            => "opac",
36
    type            => "opac",
39
});
37
});
40
38
41
my $patron = Koha::Patrons->find($borrowernumber);
39
my $patron = Koha::Patrons->find($borrowernumber)
42
my $gdpr_proc_consent;
40
    or Koha::Exceptions::Patron->throw("Patron id $borrowernumber not found");
43
if( C4::Context->preference('PrivacyPolicyConsent') ) {
41
44
    $gdpr_proc_consent = Koha::Patron::Consents->search({
42
# Get consent types and values
45
        borrowernumber => $borrowernumber,
43
my @consents;
46
        type => GDPR_PROCESSING,
44
my $consent_types = Koha::Patron::Consents->available_types;
47
    })->next;
45
foreach my $consent_type ( sort keys %$consent_types) {
48
    $gdpr_proc_consent //= Koha::Patron::Consent->new({
46
    push @consents, $patron->consent($consent_type);
49
        borrowernumber => $borrowernumber,
50
        type => GDPR_PROCESSING,
51
    });
52
}
47
}
53
48
54
# Handle saves here
49
# Handle saves here
55
if( $op eq 'gdpr_proc_save' && $gdpr_proc_consent ) {
50
my $needs_redirect;
56
    if( $gdpr_check eq 'agreed' ) {
51
foreach my $consent ( @consents ) {
57
        $gdpr_proc_consent->given_on( dt_from_string() );
52
    my $check = $vars->{ "check_".$consent->type };
58
        $gdpr_proc_consent->refused_on( undef );
53
    next if !defined($check);    # no choice made
59
    } elsif( $gdpr_check eq 'disagreed' ) {
54
    $needs_redirect = 1
60
        $gdpr_proc_consent->given_on( undef );
55
        if $consent->type eq q/GDPR_PROCESSING/ && !$check && C4::Context->preference('PrivacyPolicyConsent') eq 'Enforced';
61
        $gdpr_proc_consent->refused_on( dt_from_string() );
56
    next if $consent->given_on && $check || $consent->refused_on && !$check;
62
    }
57
        # No update if no consent change
63
    $gdpr_proc_consent->store;
58
    $consent->set({
59
        given_on => $check ? dt_from_string() : undef,
60
        refused_on => $check ? undef : dt_from_string(),
61
    })->store;
64
}
62
}
65
63
66
# If user refused GDPR consent and we enforce GDPR, logout (when saving)
64
# If user refused GDPR consent and we enforce GDPR, logout (when saving)
67
if( $op =~ /save/ && C4::Context->preference('PrivacyPolicyConsent') eq 'Enforced' && $gdpr_proc_consent->refused_on )
65
if( $needs_redirect ) {
68
{
69
    print $query->redirect('/cgi-bin/koha/opac-main.pl?logout.x=1');
66
    print $query->redirect('/cgi-bin/koha/opac-main.pl?logout.x=1');
70
    exit;
67
    exit;
71
}
68
}
72
69
73
$template->param( patron => $patron );
70
$template->param( patron => $patron, consents => \@consents, consent_types => $consent_types );
74
if( $gdpr_proc_consent ) {
75
    $template->param(
76
        gdpr_proc_consent => $gdpr_proc_consent->given_on // q{},
77
        gdpr_proc_refusal => $gdpr_proc_consent->refused_on // q{},
78
    );
79
}
80
71
81
output_html_with_http_headers $query, $cookie, $template->output, undef, { force_no_caching => 1 };
72
output_html_with_http_headers $query, $cookie, $template->output, undef, { force_no_caching => 1 };
(-)a/t/db_dependent/Koha/Patron.t (-1 / +22 lines)
Lines 19-25 Link Here
19
19
20
use Modern::Perl;
20
use Modern::Perl;
21
21
22
use Test::More tests => 28;
22
use Test::More tests => 29;
23
use Test::Exception;
23
use Test::Exception;
24
use Test::Warn;
24
use Test::Warn;
25
25
Lines 1900-1905 subtest 'update privacy tests' => sub { Link Here
1900
1900
1901
    plan tests => 5;
1901
    plan tests => 5;
1902
1902
1903
    $schema->storage->txn_begin;
1903
    my $patron = $builder->build_object({ class => 'Koha::Patrons', value => { privacy => 1 } });
1904
    my $patron = $builder->build_object({ class => 'Koha::Patrons', value => { privacy => 1 } });
1904
1905
1905
    my $old_checkout = $builder->build_object({ class => 'Koha::Old::Checkouts', value => { borrowernumber => $patron->id } });
1906
    my $old_checkout = $builder->build_object({ class => 'Koha::Old::Checkouts', value => { borrowernumber => $patron->id } });
Lines 1933-1938 subtest 'update privacy tests' => sub { Link Here
1933
subtest 'alert_subscriptions tests' => sub {
1934
subtest 'alert_subscriptions tests' => sub {
1934
1935
1935
    plan tests => 3;
1936
    plan tests => 3;
1937
    $schema->storage->txn_begin;
1936
1938
1937
    my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
1939
    my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
1938
1940
Lines 1949-1952 subtest 'alert_subscriptions tests' => sub { Link Here
1949
    is( $subscriptions[1]->subscriptionid, $subscription2->subscriptionid, "Second subscribed alert is correct" );
1951
    is( $subscriptions[1]->subscriptionid, $subscription2->subscriptionid, "Second subscribed alert is correct" );
1950
1952
1951
    $patron->discard_changes;
1953
    $patron->discard_changes;
1954
    $schema->storage->txn_rollback;
1955
};
1956
1957
subtest 'test patron_consent' => sub {
1958
    plan tests => 4;
1959
    $schema->storage->txn_begin;
1960
1961
    my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
1962
    throws_ok { $patron->consent } 'Koha::Exceptions::MissingParameter', 'missing type';
1963
1964
    my $consent = $patron->consent('GDPR_PROCESSING');
1965
    is( ref $consent, 'Koha::Patron::Consent', 'return type check' );
1966
    $consent->given_on('2021-02-03')->store;
1967
    undef $consent;
1968
    is( $patron->consent('GDPR_PROCESSING')->given_on, '2021-02-03 00:00:00', 'check date' );
1969
1970
    is( $patron->consent('NOT_EXIST')->refused_on, undef, 'New empty object for new type' );
1971
1972
    $schema->storage->txn_rollback;
1952
};
1973
};
(-)a/t/db_dependent/Koha/Patron/Consents.t (-3 / +43 lines)
Lines 18-26 Link Here
18
# along with Koha; if not, see <http://www.gnu.org/licenses>.
18
# along with Koha; if not, see <http://www.gnu.org/licenses>.
19
19
20
use Modern::Perl;
20
use Modern::Perl;
21
use Data::Dumper qw/Dumper/;
22
use Test::More tests => 2;
23
use Test::MockModule;
24
use Test::MockObject;
21
25
22
use Test::More tests => 1;
26
use t::lib::Mocks;
23
24
use t::lib::TestBuilder;
27
use t::lib::TestBuilder;
25
28
26
use Koha::Database;
29
use Koha::Database;
Lines 46-48 subtest 'Basic tests for Koha::Patron::Consent' => sub { Link Here
46
49
47
    $schema->storage->txn_rollback;
50
    $schema->storage->txn_rollback;
48
};
51
};
49
- 
52
53
subtest 'Method available_types' => sub {
54
    plan tests => 7;
55
    $schema->storage->txn_begin;
56
57
    # Mock get_enabled_plugins
58
    my $plugins        = [];
59
    my $plugins_module = Test::MockModule->new('Koha::Plugins');
60
    $plugins_module->mock( 'get_enabled_plugins', sub { return @$plugins } );
61
    my $plugin_1 = Test::MockObject->new;
62
    my $data_1   = [ 'plugin_1', { title => { en => 'Title1' }, description => { en => 'Desc1' } } ];
63
    $plugin_1->mock( 'patron_consent_type', sub { return $data_1; } );
64
    my $plugin_2 = Test::MockObject->new;
65
    my $data_2   = [ 'plugin_2', { title => { en => 'Title2' }, description => { en => 'Desc2' } } ];
66
    $plugin_2->mock( 'patron_consent_type', sub { return $data_2; } );
67
    $plugins = [ $plugin_1, $plugin_2 ];
68
69
    t::lib::Mocks::mock_preference( 'PrivacyPolicyConsent',   'Enforced' );
70
    t::lib::Mocks::mock_preference( 'OPACCustomConsentTypes', 0 );
71
    my $types = Koha::Patron::Consents->available_types;
72
    is( keys %$types, 1, 'Expect one plugin for privacy policy' );
73
    t::lib::Mocks::mock_preference( 'OPACCustomConsentTypes', 1 );
74
    $types = Koha::Patron::Consents->available_types;
75
    is( keys %$types, 3, 'Expect three plugins when allowing custom consents' );
76
    t::lib::Mocks::mock_preference( 'PrivacyPolicyConsent', '' );
77
    $types = Koha::Patron::Consents->available_types;
78
    is( keys %$types,              2,     'Expect two plugins, when pref disabled' );
79
    is( $types->{GDPR_PROCESSING}, undef, 'GDPR key should not be found' );
80
    is_deeply( $types->{plugin_2}, $data_2->[1], 'Check type hash' );
81
82
    # Let plugin_2 return bad data (hashref)
83
    $data_2 = { not_expected => 1 };
84
    $types  = Koha::Patron::Consents->available_types;
85
    is( keys %$types, 1, 'Expect one plugin, when plugin_2 fails' );
86
    is_deeply( $types->{plugin_1}, $data_1->[1], 'Check type hash' );
87
88
    $schema->storage->txn_rollback;
89
};

Return to bug 31503