File: | C4/Members/Attributes.pm |
Coverage: | 44.1% |
line | stmt | bran | cond | sub | time | code |
---|---|---|---|---|---|---|
1 | package C4::Members::Attributes; | |||||
2 | ||||||
3 | # Copyright (C) 2008 LibLime | |||||
4 | # | |||||
5 | # This file is part of Koha. | |||||
6 | # | |||||
7 | # Koha is free software; you can redistribute it and/or modify it under the | |||||
8 | # terms of the GNU General Public License as published by the Free Software | |||||
9 | # Foundation; either version 2 of the License, or (at your option) any later | |||||
10 | # version. | |||||
11 | # | |||||
12 | # Koha is distributed in the hope that it will be useful, but WITHOUT ANY | |||||
13 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR | |||||
14 | # A PARTICULAR PURPOSE. See the GNU General Public License for more details. | |||||
15 | # | |||||
16 | # You should have received a copy of the GNU General Public License along | |||||
17 | # with Koha; if not, write to the Free Software Foundation, Inc., | |||||
18 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. | |||||
19 | ||||||
20 | 15 15 15 | 574 138 1295 | use strict; | |||
21 | 15 15 15 | 173 199 675 | use warnings; | |||
22 | ||||||
23 | 15 15 15 | 31978 200089 803 | use Text::CSV; # Don't be tempted to use Text::CSV::Unicode -- even in binary mode it fails. | |||
24 | 15 15 15 | 1095 250 422 | use C4::Context; | |||
25 | 15 15 15 | 2410 440 731 | use C4::Members::AttributeTypes; | |||
26 | ||||||
27 | 15 15 15 | 207 92 2273 | use vars qw($VERSION @ISA @EXPORT_OK @EXPORT %EXPORT_TAGS); | |||
28 | our ($csv, $AttributeTypes); | |||||
29 | ||||||
30 | BEGIN { | |||||
31 | # set the version for version checking | |||||
32 | 15 | 90 | $VERSION = 3.01; | |||
33 | 15 | 373 | @ISA = qw(Exporter); | |||
34 | 15 | 128 | @EXPORT_OK = qw(GetBorrowerAttributes GetBorrowerAttributeValue CheckUniqueness SetBorrowerAttributes | |||
35 | extended_attributes_code_value_arrayref extended_attributes_merge | |||||
36 | SearchIdMatchingAttribute); | |||||
37 | 15 | 24297 | %EXPORT_TAGS = ( all => \@EXPORT_OK ); | |||
38 | } | |||||
39 | ||||||
40 - 68 | =head1 NAME C4::Members::Attributes - manage extend patron attributes =head1 SYNOPSIS use C4::Members::Attributes; my $attributes = C4::Members::Attributes::GetBorrowerAttributes($borrowernumber); =head1 FUNCTIONS =head2 GetBorrowerAttributes my $attributes = C4::Members::Attributes::GetBorrowerAttributes($borrowernumber[, $opac_only]); 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 | |||||
69 | ||||||
70 | sub GetBorrowerAttributes { | |||||
71 | 0 | 0 | my $borrowernumber = shift; | |||
72 | 0 | 0 | my $opac_only = @_ ? shift : 0; | |||
73 | ||||||
74 | 0 | 0 | my $dbh = C4::Context->dbh(); | |||
75 | 0 | 0 | my $query = "SELECT code, description, attribute, lib, password, display_checkout, category_code, class | |||
76 | FROM borrower_attributes | |||||
77 | JOIN borrower_attribute_types USING (code) | |||||
78 | LEFT JOIN authorised_values ON (category = authorised_value_category AND attribute = authorised_value) | |||||
79 | WHERE borrowernumber = ?"; | |||||
80 | 0 | 0 | $query .= "\nAND opac_display = 1" if $opac_only; | |||
81 | 0 | 0 | $query .= "\nORDER BY code, attribute"; | |||
82 | 0 | 0 | my $sth = $dbh->prepare_cached($query); | |||
83 | 0 | 0 | $sth->execute($borrowernumber); | |||
84 | 0 | 0 | my @results = (); | |||
85 | 0 | 0 | while (my $row = $sth->fetchrow_hashref()) { | |||
86 | 0 | 0 | push @results, { | |||
87 | code => $row->{'code'}, | |||||
88 | description => $row->{'description'}, | |||||
89 | value => $row->{'attribute'}, | |||||
90 | value_description => $row->{'lib'}, | |||||
91 | password => $row->{'password'}, | |||||
92 | display_checkout => $row->{'display_checkout'}, | |||||
93 | category_code => $row->{'category_code'}, | |||||
94 | class => $row->{'class'}, | |||||
95 | } | |||||
96 | } | |||||
97 | 0 | 0 | return \@results; | |||
98 | } | |||||
99 | ||||||
100 - 106 | =head2 GetAttributes my $attributes = C4::Members::Attributes::GetAttributes([$opac_only]); Retrieve an arrayref of extended attribute codes =cut | |||||
107 | ||||||
108 | sub GetAttributes { | |||||
109 | 0 | 0 | my ($opac_only) = @_; | |||
110 | ||||||
111 | 0 | 0 | my $dbh = C4::Context->dbh(); | |||
112 | 0 | 0 | my $query = "SELECT code FROM borrower_attribute_types"; | |||
113 | 0 | 0 | $query .= "\nWHERE opac_display = 1" if $opac_only; | |||
114 | 0 | 0 | $query .= "\nORDER BY code"; | |||
115 | 0 | 0 | return $dbh->selectcol_arrayref($query); | |||
116 | } | |||||
117 | ||||||
118 - 125 | =head2 GetBorrowerAttributeValue my $value = C4::Members::Attributes::GetBorrowerAttributeValue($borrowernumber, $attribute_code); Retrieve the value of an extended attribute C<$attribute_code> associated with the patron specified by C<$borrowernumber>. =cut | |||||
126 | ||||||
127 | sub GetBorrowerAttributeValue { | |||||
128 | 0 | 0 | my $borrowernumber = shift; | |||
129 | 0 | 0 | my $code = shift; | |||
130 | ||||||
131 | 0 | 0 | my $dbh = C4::Context->dbh(); | |||
132 | 0 | 0 | my $query = "SELECT attribute | |||
133 | FROM borrower_attributes | |||||
134 | WHERE borrowernumber = ? | |||||
135 | AND code = ?"; | |||||
136 | 0 | 0 | my $value = $dbh->selectrow_array($query, undef, $borrowernumber, $code); | |||
137 | 0 | 0 | return $value; | |||
138 | } | |||||
139 | ||||||
140 - 144 | =head2 SearchIdMatchingAttribute my $matching_borrowernumbers = C4::Members::Attributes::SearchIdMatchingAttribute($filter); =cut | |||||
145 | ||||||
146 | sub SearchIdMatchingAttribute{ | |||||
147 | 0 | 0 | my $filter = shift; | |||
148 | 0 | 0 | $filter = [$filter] unless ref $filter; | |||
149 | ||||||
150 | 0 | 0 | my $dbh = C4::Context->dbh(); | |||
151 | 0 | 0 | my $query = qq{ | |||
152 | SELECT DISTINCT borrowernumber | |||||
153 | FROM borrower_attributes | |||||
154 | JOIN borrower_attribute_types USING (code) | |||||
155 | WHERE staff_searchable = 1 | |||||
156 | AND (} . join (" OR ", map "attribute like ?", @$filter) .qq{)}; | |||||
157 | 0 | 0 | my $sth = $dbh->prepare_cached($query); | |||
158 | 0 | 0 | $sth->execute(map "%$_%", @$filter); | |||
159 | 0 0 | 0 0 | return [map $_->[0], @{ $sth->fetchall_arrayref }]; | |||
160 | } | |||||
161 | ||||||
162 - 174 | =head2 CheckUniqueness my $ok = CheckUniqueness($code, $value[, $borrowernumber]); 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 | |||||
175 | ||||||
176 | sub CheckUniqueness { | |||||
177 | 0 | 0 | my $code = shift; | |||
178 | 0 | 0 | my $value = shift; | |||
179 | 0 | 0 | my $borrowernumber = @_ ? shift : undef; | |||
180 | ||||||
181 | 0 | 0 | my $attr_type = C4::Members::AttributeTypes->fetch($code); | |||
182 | ||||||
183 | 0 | 0 | return 0 unless defined $attr_type; | |||
184 | 0 | 0 | return 1 unless $attr_type->unique_id(); | |||
185 | ||||||
186 | 0 | 0 | my $dbh = C4::Context->dbh; | |||
187 | 0 | 0 | my $sth; | |||
188 | 0 | 0 | if (defined($borrowernumber)) { | |||
189 | 0 | 0 | $sth = $dbh->prepare("SELECT COUNT(*) | |||
190 | FROM borrower_attributes | |||||
191 | WHERE code = ? | |||||
192 | AND attribute = ? | |||||
193 | AND borrowernumber <> ?"); | |||||
194 | 0 | 0 | $sth->execute($code, $value, $borrowernumber); | |||
195 | } else { | |||||
196 | 0 | 0 | $sth = $dbh->prepare("SELECT COUNT(*) | |||
197 | FROM borrower_attributes | |||||
198 | WHERE code = ? | |||||
199 | AND attribute = ?"); | |||||
200 | 0 | 0 | $sth->execute($code, $value); | |||
201 | } | |||||
202 | 0 | 0 | my ($count) = $sth->fetchrow_array; | |||
203 | 0 | 0 | return ($count == 0); | |||
204 | } | |||||
205 | ||||||
206 - 213 | =head2 SetBorrowerAttributes SetBorrowerAttributes($borrowernumber, [ { code => 'CODE', value => 'value', password => 'password' }, ... ] ); Set patron attributes for the patron identified by C<$borrowernumber>, replacing any that existed previously. =cut | |||||
214 | ||||||
215 | sub SetBorrowerAttributes { | |||||
216 | 0 | 0 | my $borrowernumber = shift; | |||
217 | 0 | 0 | my $attr_list = shift; | |||
218 | ||||||
219 | 0 | 0 | my $dbh = C4::Context->dbh; | |||
220 | 0 | 0 | my $delsth = $dbh->prepare("DELETE FROM borrower_attributes WHERE borrowernumber = ?"); | |||
221 | 0 | 0 | $delsth->execute($borrowernumber); | |||
222 | ||||||
223 | 0 | 0 | my $sth = $dbh->prepare("INSERT INTO borrower_attributes (borrowernumber, code, attribute, password) | |||
224 | VALUES (?, ?, ?, ?)"); | |||||
225 | 0 | 0 | foreach my $attr (@$attr_list) { | |||
226 | 0 | 0 | $attr->{password} = undef unless exists $attr->{password}; | |||
227 | 0 | 0 | $sth->execute($borrowernumber, $attr->{code}, $attr->{value}, $attr->{password}); | |||
228 | 0 | 0 | if ($sth->err) { | |||
229 | 0 | 0 | warn sprintf('Database returned the following error: %s', $sth->errstr); | |||
230 | 0 | 0 | return; # bail immediately on errors | |||
231 | } | |||||
232 | } | |||||
233 | 0 | 0 | return 1; # borower attributes successfully set | |||
234 | } | |||||
235 | ||||||
236 - 247 | =head2 extended_attributes_code_value_arrayref my $patron_attributes = "homeroom:1150605,grade:01,extradata:foobar"; my $aref = extended_attributes_code_value_arrayref($patron_attributes); Takes a comma-delimited CSV-style string argument and returns the kind of data structure that SetBorrowerAttributes wants, namely a reference to array of hashrefs like: [ { code => 'CODE', value => 'value' }, { code => 'CODE2', value => 'othervalue' } ... ] Caches Text::CSV parser object for efficiency. =cut | |||||
248 | ||||||
249 | sub extended_attributes_code_value_arrayref { | |||||
250 | 4 | 15 | my $string = shift or return; | |||
251 | 4 | 19 | $csv or $csv = Text::CSV->new({binary => 1}); # binary needed for non-ASCII Unicode | |||
252 | 4 | 135 | my $ok = $csv->parse($string); # parse field again to get subfields! | |||
253 | 4 | 202 | my @list = $csv->fields(); | |||
254 | # TODO: error handling (check $ok) | |||||
255 | return [ | |||||
256 | 12 11 | 13 27 | sort {&_sort_by_code($a,$b)} | |||
257 | 4 11 11 | 43 15 66 | map { map { my @arr = split /:/, $_, 2; { code => $arr[0], value => $arr[1] } } $_ } | |||
258 | @list | |||||
259 | ]; | |||||
260 | # nested map because of split | |||||
261 | } | |||||
262 | ||||||
263 - 281 | =head2 extended_attributes_merge my $old_attributes = extended_attributes_code_value_arrayref("homeroom:224,grade:04,deanslist:2007,deanslist:2008,somedata:xxx"); my $new_attributes = extended_attributes_code_value_arrayref("homeroom:115,grade:05,deanslist:2009,extradata:foobar"); my $merged = extended_attributes_merge($patron_attributes, $new_attributes, 1); # assuming deanslist is a repeatable code, value same as: # $merged = extended_attributes_code_value_arrayref("homeroom:115,grade:05,deanslist:2007,deanslist:2008,deanslist:2009,extradata:foobar,somedata:xxx"); Takes three arguments. The first two are references to array of hashrefs, each like: [ { code => 'CODE', value => 'value' }, { code => 'CODE2', value => 'othervalue' } ... ] The third option specifies whether repeatable codes are clobbered or collected. True for non-clobber. Returns one reference to (merged) array of hashref. Caches results of C4::Members::AttributeTypes::GetAttributeTypes_hashref(1) for efficiency. =cut | |||||
282 | ||||||
283 | sub extended_attributes_merge { | |||||
284 | 4 | 12 | my $old = shift or return; | |||
285 | 4 | 11 | my $new = shift or return $old; | |||
286 | 4 | 8 | my $keep = @_ ? shift : 0; | |||
287 | 4 | 10 | $AttributeTypes or $AttributeTypes = C4::Members::AttributeTypes::GetAttributeTypes_hashref(1); | |||
288 | 4 | 8 | my @merged = @$old; | |||
289 | 4 | 8 | foreach my $att (@$new) { | |||
290 | 10 | 20 | unless ($att->{code}) { | |||
291 | 0 | 0 | warn "Cannot merge element: no 'code' defined"; | |||
292 | 0 | 0 | next; | |||
293 | } | |||||
294 | 10 | 25 | unless ($AttributeTypes->{$att->{code}}) { | |||
295 | 0 | 0 | warn "Cannot merge element: unrecognized code = '$att->{code}'"; | |||
296 | 0 | 0 | next; | |||
297 | } | |||||
298 | 10 | 47 | unless ($AttributeTypes->{$att->{code}}->{repeatable} and $keep) { | |||
299 | 9 41 | 11 83 | @merged = grep {$att->{code} ne $_->{code}} @merged; # filter out any existing attributes of the same code | |||
300 | } | |||||
301 | 10 | 20 | push @merged, $att; | |||
302 | } | |||||
303 | 4 24 | 24 31 | return [( sort {&_sort_by_code($a,$b)} @merged )]; | |||
304 | } | |||||
305 | ||||||
306 | sub _sort_by_code { | |||||
307 | 36 | 40 | my ($x, $y) = @_; | |||
308 | 36 | 62 | defined ($x->{code}) or return -1; | |||
309 | 36 | 62 | defined ($y->{code}) or return 1; | |||
310 | 36 | 157 | return $x->{code} cmp $y->{code} || $x->{value} cmp $y->{value}; | |||
311 | } | |||||
312 | ||||||
313 - 319 | =head1 AUTHOR Koha Development Team <http://koha-community.org/> Galen Charlton <galen.charlton@liblime.com> =cut | |||||
320 | ||||||
321 | 1; |