From d75cc041f6be14314dd6233671bce9d0861894ad Mon Sep 17 00:00:00 2001 From: Galen Charlton Date: Mon, 12 May 2008 08:32:49 -0500 Subject: [PATCH] new C4 modules for patron attributes Two new modules to support patron attributes: - C4::Members::AttributeTypes OO-module for managing patron attribute types. - C4::Members::Attributes Procedural module for retrieving and setting extended attributes belonging to a patron. Signed-off-by: Joshua Ferraro --- C4/Members/AttributeTypes.pm | 442 +++++++++++++++++++++++ C4/Members/Attributes.pm | 181 ++++++++++ t/lib/KohaTest/Members/AttributeTypes.pm | 120 ++++++ 3 files changed, 743 insertions(+) create mode 100644 C4/Members/AttributeTypes.pm create mode 100644 C4/Members/Attributes.pm create mode 100644 t/lib/KohaTest/Members/AttributeTypes.pm diff --git a/C4/Members/AttributeTypes.pm b/C4/Members/AttributeTypes.pm new file mode 100644 index 0000000000..4eb69902ff --- /dev/null +++ b/C4/Members/AttributeTypes.pm @@ -0,0 +1,442 @@ +package C4::Members::AttributeTypes; + +# Copyright (C) 2008 LibLime +# +# 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 2 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, write to the Free Software Foundation, Inc., 59 Temple Place, +# Suite 330, Boston, MA 02111-1307 USA + +use strict; +use C4::Context; + +use vars qw($VERSION); + +BEGIN { + # set the version for version checking + $VERSION = 3.00; +} + +=head1 NAME + +C4::Members::AttributeTypes - mananage extended patron attribute types + +=head1 SYNOPSIS + +=over 4 + +my @attribute_types = C4::Members::AttributeTypes::GetAttributeTypes(); + +my $attr_type = C4::Members::AttributeTypes->new($code, $description); +$attr_type->code($code); +$attr_type->description($description); +$attr_type->repeatable($repeatable); +$attr_type->unique_id($unique_id); +$attr_type->opac_display($opac_display); +$attr_type->password_allowed($password_allowed); +$attr_type->staff_searchable($staff_searchable); +$attr_type->authorised_value_category($authorised_value_category); +$attr_type->store(); +$attr_type->delete(); + +my $attr_type = C4::Members::AttributeTypes->fetch($code); +$attr_type = C4::Members::AttributeTypes->delete($code); + +=back + +=head1 FUNCTIONS + +=head2 GetAttributeTypes + +=over 4 + +my @attribute_types = C4::Members::AttributeTypes::GetAttributeTypes(); + +=back + +Returns an array of hashrefs of each attribute type defined +in the database. The array is sorted by code. Each hashref contains +the following fields: + +code +description + +=cut + +sub GetAttributeTypes { + my $dbh = C4::Context->dbh; + my $sth = $dbh->prepare('SELECT code, description FROM borrower_attribute_types ORDER by code'); + $sth->execute(); + my @results = (); + while (my $row = $sth->fetchrow_hashref) { + push @results, $row; + } + return @results; +} + +=head1 METHODS + +=over 4 + +my $attr_type = C4::Members::AttributeTypes->new($code, $description); + +=back + +Create a new attribute type. + +=cut + +sub new { + my $class = shift; + my $self = {}; + + $self->{'code'} = shift; + $self->{'description'} = shift; + $self->{'repeatable'} = 0; + $self->{'unique_id'} = 0; + $self->{'opac_display'} = 0; + $self->{'password_allowed'} = 0; + $self->{'staff_searchable'} = 0; + $self->{'authorised_value_category'} = ''; + + bless $self, $class; + return $self; +} + +=head2 fetch + +=over 4 + +my $attr_type = C4::Members::AttributeTypes->fetch($code); + +=back + +Fetches an attribute type from the database. If no +type with the given C<$code> exists, returns undef. + +=cut + +sub fetch { + my $class = shift; + my $code = shift; + my $self = {}; + my $dbh = C4::Context->dbh(); + + my $sth = $dbh->prepare_cached("SELECT * FROM borrower_attribute_types WHERE code = ?"); + $sth->execute($code); + my $row = $sth->fetchrow_hashref; + $sth->finish(); + return undef unless defined $row; + + $self->{'code'} = $row->{'code'}; + $self->{'description'} = $row->{'description'}; + $self->{'repeatable'} = $row->{'repeatable'}; + $self->{'unique_id'} = $row->{'unique_id'}; + $self->{'opac_display'} = $row->{'opac_display'}; + $self->{'password_allowed'} = $row->{'password_allowed'}; + $self->{'staff_searchable'} = $row->{'staff_searchable'}; + $self->{'authorised_value_category'} = $row->{'authorised_value_category'}; + + bless $self, $class; + return $self; +} + +=head2 store + +=over 4 + +$attr_type->store(); + +=back + +Stores attribute type in the database. If the type +previously retrieved from the database via the fetch() +method, the DB representation of the type is replaced. + +=cut + +sub store { + my $self = shift; + + my $dbh = C4::Context->dbh; + my $sth; + my $existing = __PACKAGE__->fetch($self->{'code'}); + if (defined $existing) { + $sth = $dbh->prepare_cached("UPDATE borrower_attribute_types + SET description = ?, + repeatable = ?, + unique_id = ?, + opac_display = ?, + password_allowed = ?, + staff_searchable = ?, + authorised_value_category = ? + WHERE code = ?"); + } else { + $sth = $dbh->prepare_cached("INSERT INTO borrower_attribute_types + (description, repeatable, unique_id, opac_display, password_allowed, + staff_searchable, authorised_value_category, code) + VALUES (?, ?, ?, ?, ?, + ?, ?, ?)"); + } + $sth->bind_param(1, $self->{'description'}); + $sth->bind_param(2, $self->{'repeatable'}); + $sth->bind_param(3, $self->{'unique_id'}); + $sth->bind_param(4, $self->{'opac_display'}); + $sth->bind_param(5, $self->{'password_allowed'}); + $sth->bind_param(6, $self->{'staff_searchable'}); + $sth->bind_param(7, $self->{'authorised_value_category'}); + $sth->bind_param(8, $self->{'code'}); + $sth->execute; + +} + +=head2 code + +=over 4 + +my $code = $attr_type->code(); +$attr_type->code($code); + +=back + +Accessor. Note that the code is immutable once +a type is created or fetched from the database. + +=cut + +sub code { + my $self = shift; + return $self->{'code'}; +} + +=head2 description + +=over 4 + +my $description = $attr_type->description(); +$attr_type->description($description); + +=back + +Accessor. + +=cut + +sub description { + my $self = shift; + @_ ? $self->{'description'} = shift : $self->{'description'}; +} + +=head2 repeatable + +=over 4 + +my $repeatable = $attr_type->repeatable(); +$attr_type->repeatable($repeatable); + +=back + +Accessor. The C<$repeatable> argument +is interpreted as a Perl boolean. + +=cut + +sub repeatable { + my $self = shift; + @_ ? $self->{'repeatable'} = ((shift) ? 1 : 0) : $self->{'repeatable'}; +} + +=head2 unique_id + +=over 4 + +my $unique_id = $attr_type->unique_id(); +$attr_type->unique_id($unique_id); + +=back + +Accessor. The C<$unique_id> argument +is interpreted as a Perl boolean. + +=cut + +sub unique_id { + my $self = shift; + @_ ? $self->{'unique_id'} = ((shift) ? 1 : 0) : $self->{'unique_id'}; +} +=head2 opac_display + +=over 4 + +my $opac_display = $attr_type->opac_display(); +$attr_type->opac_display($opac_display); + +=back + +Accessor. The C<$opac_display> argument +is interpreted as a Perl boolean. + +=cut + +sub opac_display { + my $self = shift; + @_ ? $self->{'opac_display'} = ((shift) ? 1 : 0) : $self->{'opac_display'}; +} +=head2 password_allowed + +=over 4 + +my $password_allowed = $attr_type->password_allowed(); +$attr_type->password_allowed($password_allowed); + +=back + +Accessor. The C<$password_allowed> argument +is interpreted as a Perl boolean. + +=cut + +sub password_allowed { + my $self = shift; + @_ ? $self->{'password_allowed'} = ((shift) ? 1 : 0) : $self->{'password_allowed'}; +} +=head2 staff_searchable + +=over 4 + +my $staff_searchable = $attr_type->staff_searchable(); +$attr_type->staff_searchable($staff_searchable); + +=back + +Accessor. The C<$staff_searchable> argument +is interpreted as a Perl boolean. + +=cut + +sub staff_searchable { + my $self = shift; + @_ ? $self->{'staff_searchable'} = ((shift) ? 1 : 0) : $self->{'staff_searchable'}; +} + +=head2 authorised_value_category + +=over 4 + +my $authorised_value_category = $attr_type->authorised_value_category(); +$attr_type->authorised_value_category($authorised_value_category); + +=back + +Accessor. + +=cut + +sub authorised_value_category { + my $self = shift; + @_ ? $self->{'authorised_value_category'} = shift : $self->{'authorised_value_category'}; +} + +=head2 delete + +=over 4 + +$attr_type->delete(); +C4::Members::AttributeTypes->delete($code); + +=back + +Delete an attribute type from the database. The attribute +type may be specified either by an object or by a code. + +=cut + +sub delete { + my $arg = shift; + my $code; + if (ref($arg) eq __PACKAGE__) { + $code = $arg->{'code'}; + } else { + $code = shift; + } + + my $dbh = C4::Context->dbh; + my $sth = $dbh->prepare_cached("DELETE FROM borrower_attribute_types WHERE code = ?"); + $sth->execute($code); +} + +=head2 num_patrons + +=over 4 + +my $count = $attr_type->num_patrons(); + +=back + +Returns the number of patron records that use +this attribute type. + +=cut + +sub num_patrons { + my $self = shift; + + my $dbh = C4::Context->dbh; + my $sth = $dbh->prepare_cached("SELECT COUNT(DISTINCT borrowernumber) + FROM borrower_attributes + WHERE code = ?"); + $sth->execute($self->{code}); + my ($count) = $sth->fetchrow_array; + $sth->finish; + return $count; +} + +=head2 get_patrons + +=over 4 + +my @borrowernumbers = $attr_type->get_patrons($attribute); + +=back + +Returns the borrowernumber of the patron records that +have an attribute with the specifie value. + +=cut + +sub get_patrons { + my $self = shift; + my $value = shift; + + my $dbh = C4::Context->dbh; + my $sth = $dbh->prepare_cached("SELECT DISTINCT borrowernumber + FROM borrower_attributes + WHERE code = ? + AND attribute = ?"); + $sth->execute($self->{code}, $value); + my @results; + while (my ($borrowernumber) = $sth->fetchrow_array) { + push @results, $borrowernumber; + } + return @results; +} + +=head1 AUTHOR + +Koha Development Team + +Galen Charlton + +=cut + +1; diff --git a/C4/Members/Attributes.pm b/C4/Members/Attributes.pm new file mode 100644 index 0000000000..e012320240 --- /dev/null +++ b/C4/Members/Attributes.pm @@ -0,0 +1,181 @@ +package C4::Members::Attributes; + +# Copyright (C) 2008 LibLime +# +# 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 2 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, write to the Free Software Foundation, Inc., 59 Temple Place, +# Suite 330, Boston, MA 02111-1307 USA + +use strict; +use C4::Context; +use C4::Members::AttributeTypes; + +use vars qw($VERSION); + +BEGIN { + # set the version for version checking + $VERSION = 3.00; +} + +=head1 NAME + +C4::Members::Attribute - manage extend patron attributes + +=head1 SYNOPSIS + +=over 4 + +my $attributes = C4::Members::Attributes::GetBorrowerAttributes($borrowernumber); + +=back + +=head1 FUNCTIONS + +=head2 GetBorrowerAttributes + +=over 4 + +my $attributes = C4::Members::Attributes::GetBorrowerAttributes($borrowernumber[, $opac_only]); + +=back + +Retrieve an arrayref of extended attributes associated with the +patron specified by C<$borrowernumber>. Each entry in the arrayref +is a hashref containing the following keys: + +code (attribute type code) +description (attribute type description) +value (attribute value) +value_description (attribute value description (if associated with an authorised value)) +password (password, if any, associated with attribute + +If the C<$opac_only> parameter is present and has a true value, only the attributes +marked for OPAC display are returned. + +=cut + +sub GetBorrowerAttributes { + my $borrowernumber = shift; + my $opac_only = @_ ? shift : 0; + + my $dbh = C4::Context->dbh(); + my $query = "SELECT code, description, attribute, lib, password + FROM borrower_attributes + JOIN borrower_attribute_types USING (code) + LEFT JOIN authorised_values ON (category = authorised_value_category AND attribute = authorised_value) + WHERE borrowernumber = ?"; + $query .= "\nAND opac_display = 1" if $opac_only; + $query .= "\nORDER BY code, attribute"; + my $sth = $dbh->prepare_cached($query); + $sth->execute($borrowernumber); + my @results = (); + while (my $row = $sth->fetchrow_hashref()) { + push @results, { + code => $row->{'code'}, + description => $row->{'description'}, + value => $row->{'attribute'}, + value_description => $row->{'lib'}, + password => $row->{'password'}, + } + } + return \@results; +} + +=head2 CheckUniqueness + +=over 4 + +my $ok = CheckUniqueness($code, $value[, $borrowernumber]); + +=back + +Given an attribute type and value, verify if would violate +a unique_id restriction if added to the patron. The +optional C<$borrowernumber> is the patron that the attribute +value would be added to, if known. + +Returns false if the C<$code> is not valid or the +value would violate the uniqueness constraint. + +=cut + +sub CheckUniqueness { + my $code = shift; + my $value = shift; + my $borrowernumber = @_ ? shift : undef; + + my $attr_type = C4::Members::AttributeTypes->fetch($code); + + return 0 unless defined $attr_type; + return 1 unless $attr_type->unique_id(); + + my $dbh = C4::Context->dbh; + my $sth; + if (defined($borrowernumber)) { + $sth = $dbh->prepare("SELECT COUNT(*) + FROM borrower_attributes + WHERE code = ? + AND attribute = ? + AND borrowernumber <> ?"); + $sth->execute($code, $value, $borrowernumber); + } else { + $sth = $dbh->prepare("SELECT COUNT(*) + FROM borrower_attributes + WHERE code = ? + AND attribute = ?"); + $sth->execute($code, $value); + } + my ($count) = $sth->fetchrow_array; + $sth->finish(); + return ($count == 0); +} + +=head2 SetBorrowerAttributes + +=over 4 + +SetBorrowerAttributes($borrowernumber, [ { code => 'CODE', value => 'value', password => 'password' }, ... ] ); + +=back + +Set patron attributes for the patron identified by C<$borrowernumber>, +replacing any that existed previously. + +=cut + +sub SetBorrowerAttributes { + my $borrowernumber = shift; + my $attr_list = shift; + + my $dbh = C4::Context->dbh; + my $delsth = $dbh->prepare("DELETE FROM borrower_attributes WHERE borrowernumber = ?"); + $delsth->execute($borrowernumber); + + my $sth = $dbh->prepare("INSERT INTO borrower_attributes (borrowernumber, code, attribute, password) + VALUES (?, ?, ?, ?)"); + foreach my $attr (@$attr_list) { + $attr->{password} = undef unless exists $attr->{password}; + $sth->execute($borrowernumber, $attr->{code}, $attr->{value}, $attr->{password}); + } +} + +=head1 AUTHOR + +Koha Development Team + +Galen Charlton + +=cut + +1; diff --git a/t/lib/KohaTest/Members/AttributeTypes.pm b/t/lib/KohaTest/Members/AttributeTypes.pm new file mode 100644 index 0000000000..c306e3ed7f --- /dev/null +++ b/t/lib/KohaTest/Members/AttributeTypes.pm @@ -0,0 +1,120 @@ +package KohaTest::Members::AttributeTypes; +#use base qw( KohaTest ); +use base qw( Test::Class ); + +use strict; +use warnings; + +use Test::More; + +use C4::Members::AttributeTypes; +sub testing_class { 'C4::Members::AttributeTypes' }; + +sub methods : Test( 1 ) { + my $self = shift; + my @methods = qw( + new + fetch + GetAttributeTypes + code + description + repeatable + unique_id + opac_display + password_allowed + staff_searchable + authorised_value_category + store + delete + ); + + can_ok( $self->testing_class, @methods ); +} + +sub startup_50_create_types : Test( startup => 28 ) { + my $self = shift; + + my $type1 = C4::Members::AttributeTypes->new('CAMPUSID', 'institution ID'); + isa_ok($type1, 'C4::Members::AttributeTypes'); + is($type1->code(), 'CAMPUSID', "set code in constructor"); + is($type1->description(), 'institution ID', "set description in constructor"); + ok(!$type1->repeatable(), "repeatable defaults to false"); + ok(!$type1->unique_id(), "unique_id defaults to false"); + ok(!$type1->opac_display(), "opac_display defaults to false"); + ok(!$type1->password_allowed(), "password_allowed defaults to false"); + ok(!$type1->staff_searchable(), "staff_searchable defaults to false"); + is($type1->authorised_value_category(), '', "authorised_value_category defaults to ''"); + + $type1->repeatable('foobar'); + ok($type1->repeatable(), "repeatable now true"); + cmp_ok($type1->repeatable(), '==', 1, "repeatable not set to 'foobar'"); + $type1->repeatable(0); + ok(!$type1->repeatable(), "repeatable now false"); + + $type1->unique_id('foobar'); + ok($type1->unique_id(), "unique_id now true"); + cmp_ok($type1->unique_id(), '==', 1, "unique_id not set to 'foobar'"); + $type1->unique_id(0); + ok(!$type1->unique_id(), "unique_id now false"); + + $type1->opac_display('foobar'); + ok($type1->opac_display(), "opac_display now true"); + cmp_ok($type1->opac_display(), '==', 1, "opac_display not set to 'foobar'"); + $type1->opac_display(0); + ok(!$type1->opac_display(), "opac_display now false"); + + $type1->password_allowed('foobar'); + ok($type1->password_allowed(), "password_allowed now true"); + cmp_ok($type1->password_allowed(), '==', 1, "password_allowed not set to 'foobar'"); + $type1->password_allowed(0); + ok(!$type1->password_allowed(), "password_allowed now false"); + + $type1->staff_searchable('foobar'); + ok($type1->staff_searchable(), "staff_searchable now true"); + cmp_ok($type1->staff_searchable(), '==', 1, "staff_searchable not set to 'foobar'"); + $type1->staff_searchable(0); + ok(!$type1->staff_searchable(), "staff_searchable now false"); + + $type1->code('INSTID'); + is($type1->code(), 'CAMPUSID', 'code() allows retrieving but not setting'); + $type1->description('student ID'); + is($type1->description(), 'student ID', 'set description'); + $type1->authorised_value_category('CAT'); + is($type1->authorised_value_category(), 'CAT', 'set authorised_value_category'); + + $type1->repeatable(1); + $type1->staff_searchable(1); + $type1->store(); + is($type1->num_patrons(), 0, 'no patrons using the new attribute type yet'); + + my $type2 = C4::Members::AttributeTypes->new('ABC', 'ABC ID'); + $type2->store(); +} + +sub shutdown_50_list_and_remove_types : Test( shutdown => 11 ) { + my $self = shift; + + my @list = C4::Members::AttributeTypes::GetAttributeTypes(); + is_deeply(\@list, [ { code => 'ABC', description => 'ABC ID' }, + { code => 'CAMPUSID', description => 'student ID' } ], "retrieved list of types"); + + my $type1 = C4::Members::AttributeTypes->fetch($list[1]->{code}); + isa_ok($type1, 'C4::Members::AttributeTypes'); + is($type1->code(), 'CAMPUSID', 'fetched code'); + is($type1->description(), 'student ID', 'fetched description'); + is($type1->authorised_value_category(), 'CAT', 'fetched authorised_value_category'); + ok($type1->repeatable(), "fetched repeatable"); + ok(!$type1->unique_id(), "fetched unique_id"); + ok(!$type1->opac_display(), "fetched opac_display"); + ok(!$type1->password_allowed(), "fetched password_allowed"); + ok($type1->staff_searchable(), "fetched staff_searchable"); + + $type1->delete(); + C4::Members::AttributeTypes->delete('ABC'); + + my @newlist = C4::Members::AttributeTypes::GetAttributeTypes(); + is(scalar(@newlist), 0, "no types left after deletion"); + +} + +1; -- 2.39.5