add call to doc-head-open.inc and doc-head-close.inc
[koha.git] / C4 / Shelf.pm
1 package Shelf;
2
3 =head1 NAME
4
5 Shelf - Perl extension for Virtual Bookshelves
6
7 =cut
8
9 use strict;
10 use C4::Context;
11 use Cache::FileCache;
12
13 =head1 VERSION
14
15   $Id$
16
17 =cut
18
19 =head1 DESCRIPTION
20
21 Module for querying and stocking Virtual Bookshelves
22
23    1. can contain a list of items, a list of biblioitems, or a list of biblios
24    2. can have an arbitrary name, and will have a unique numerical identifier
25    3. will have arbitrary metadata (properties) associated with it
26           * Sharing information (private, only visible by the owner of the
27             shelf; shared with a group of patrons; public, viewable by anybody)
28           * Special circulation rules - Do not return to home branch, do not
29             circulate, reduced loan time (ie 3 day loan)
30           * Search query term - if the shelf is the result of a query, the
31             query itself can be stored with the list of books that resulted
32           * Creation date - useful for 'retiring' a stale cached query result
33           * Access information - who has "write" or "read" access to the shelf.
34           * Searchable - If a patron can perform a search query on the contents
35             of this shelf
36
37
38 Patrons typically will only use "biblioitem" bookshelves, and will not need to
39 be presented with the differences between biblioitem and item bookshelves.
40
41
42 Some uses for VirtualBookshelves
43
44    1. Cache search results for faster response on popular searches
45    2. Name search results so that patrons can pull up saved searches
46    3. Creation of sub-collections within a library or branch
47    4. replacing "itemtypes" field... this would allow an individual item to be
48         a member of more than one itemtype
49    5. store a patron's reading record (if he chooses to store such data)
50    6. store a patron's "To be read" list
51    7. A teacher of a course could add a list of books to a shelf for his course
52         and ask that those items be marked non-circulating so students always
53         have access to them at the library.
54           * The teacher creates the list of materials that she wants to be
55             non-circulating (or reduced to 3-day loan) and marks them as such
56           * A librarian receives a notice that a shelf requires her attention.
57             He can pull up a list of the contents of the shelf, the owner of
58             the shelf, and the reason the owner is requesting this change in
59             circulation rules. The librarian can approve or deny the request.
60           * Optionally, create an access flag that grants teachers the right to
61             put items on modified circulation shelves without librarian
62             intervention.
63
64
65 =cut
66
67 =head1 METHODS
68
69 =head2 C<new()>
70
71 Base constructor for the class.
72
73   my $shelf=Shelf->new(56);
74       will load bookshelf 56.
75   my $shelf=Shelf->new(-name => 'Fiction');
76   my $shelf=Shelf->new('Fiction');
77       will load the internal 'Fiction' shelf
78   my $shelf=Shelf->new('Favourite Books', 'sjohnson');
79   my $shelf=Shelf->new(-name => 'Favourite Books', -owner => 'sjohnson');
80       will load sjohnson's "Favourite Books" bookshelf
81   
82   Any of the last four invocations will create a new shelf with the name and
83   owner given if one doesn't already exist.
84
85
86 =cut
87
88 sub new {
89     my $self = {};
90     $self->{ID} = undef;
91     $self->{NAME}=undef;
92     $self->{OWNDER}=undef;
93     $self->{BIBLIOCONTENTS}={};
94     $self->{BIBLIOITEMCONTENTS}={};
95     $self->{ITEMCONTENTS}={};
96     $self->{ATTRIBUTES}={};
97     $self->{CACHE}=new Cache::FileCache( { 'namespace' => 'KohaShelves' } );
98
99     if (@_) {
100         my $dbh=C4::Context->dbh();
101         shift;
102         if ($#_ == 0) {
103             $self->{ID}=shift;
104             # load attributes of shelf #ID
105             my $sth;
106             $sth=$dbh->prepare("select bookshelfname,bookshelfowner from bookshelves where bookshelfid=?");
107             $sth->execute($self->{ID});
108             ($self->{NAME},$self->{OWNER}) = $sth->fetchrow;
109             $sth=$dbh->prepare("select attribute,value from bookshelfattributes where bookshelfid=?");
110             $sth->execute($self->{ID});
111             while (my ($attribute,$value) = $sth->fetchrow) {
112                 $self->{ATTRIBUTES}->{$attribute}=$value;
113             }
114         } elsif ($#_) {
115             my ($name,$owner,$attributes);
116             if ($_[0] =~/^-/) {
117                 my %params=@_;
118                 $name=$params{name};
119                 $owner=$params{owner};
120                 $attributes=$params{attributes};
121             } else {
122                 $name=shift;
123                 $owner=shift;
124                 $attributes=shift;
125             }
126             my $sth=$dbh->prepare("select bookshelfid from bookshelves where bookshelfname=? and bookshelfowner=?");
127             $sth->execute($name, $owner);
128             if ($sth->rows) {
129                 ($self->{ID})=$sth->fetchrow;
130                 $sth=$dbh->prepare("select attribute,value from bookshelfattributes where bookshelfid=?");
131                 $sth->execute($self->{ID});
132                 while (my ($attribute,$value) = $sth->fetchrow) {
133                     $self->{ATTRIBUTES}->{$attribute}=$value;
134                 }
135             } else {
136                 $sth=$dbh->prepare("insert into bookshelves (bookshelfname, bookshelfowner) values (?, ?)");
137                 $sth->execute($name,$owner);
138                 $sth=$dbh->prepare("select bookshelfid from bookshelves where bookshelfname=? and bookshelfowner=?");
139                 $sth->execute($name,$owner);
140                 ($self->{ID})=$sth->fetchrow();
141                 foreach my $attribute (keys %$attributes) {
142                     my $value=$attributes->{$attribute};
143                     $self->attribute($attribute,$value);
144                 }
145             }
146         }
147     }
148     bless($self);
149     return $self;
150 }
151
152
153 =head2 C<itemcontents()>
154
155 retrieve a slice of itemnumbers from a shelf.
156
157     my $arrayref = $shelf->itemcontents(-orderby=>'title', 
158                                         -startat=>50,
159                                         -number=>10     );
160
161 =cut
162
163 sub itemcontents {
164     my $self=shift;
165     my ($orderby,$startat,$number);
166     if ($_[0]=~/^\-/) {
167         my %params=@_;
168         $orderby=$params{'-orderby'};
169         $startat=$params{'-startat'};
170         $number=$params{'-number'};
171     } else {
172         ($orderby,$startat,$number)=@_;
173     }
174     $number--;
175     unless ($self->{ITEMCONTENTS}->{orderby}->{$orderby}) {
176         $self->loadcontents(-orderby=>$orderby, -startat=>$startat, -number=>$number);
177     }
178     my $endat=$startat+$number;
179     my @return;
180     foreach (@{$self->{ITEMCONTENTS}->{orderby}->{$orderby}}[$startat..$endat]) {
181         push @return,$_;
182     }
183     return \@return;
184 }
185
186 =head2 C<biblioitemcontents()>
187
188 retrieve a slice of biblioitemnumbers from a shelf.
189
190     my $arrayref = $shelf->biblioitemcontents(-orderby=>'title', 
191                                               -startat=>50,
192                                               -number=>10       );
193
194 =cut
195
196 sub biblioitemcontents {
197     my $self=shift;
198     my ($orderby,$startat,$number);
199     if ($_[0]=~/^\-/) {
200         my %params=@_;
201         $orderby=$params{'-orderby'};
202         $startat=$params{'-startat'};
203         $number=$params{'-number'};
204     } else {
205         ($orderby,$startat,$number)=@_;
206     }
207     unless ($self->{BIBLIOITEMCONTENTS}->{orderby}->{$orderby}) {
208         $self->loadcontents(-orderby=>$orderby, -startat=>$startat, -number=>$number);
209     }
210     my $endat=$startat+$number;
211     my @return;
212     foreach (@{$self->{BIBLIOITEMCONTENTS}->{orderby}->{$orderby}}[$startat..$endat]) {
213         push @return,$_;
214     }
215     return \@return;
216 }
217
218 =head2 C<biblioitemcontents()>
219
220 retrieve a slice of biblionumbers from a shelf.
221
222     my $arrayref = $shelf->bibliocontents(-orderby=>'title', 
223                                           -startat=>50,
224                                           -number=>10   );
225
226 =cut
227
228 sub bibliocontents {
229     my $self=shift;
230     my ($orderby,$startat,$number);
231     if ($_[0]=~/^\-/) {
232         my %params=@_;
233         $orderby=$params{'-orderby'};
234         $startat=$params{'-startat'};
235         $number=$params{'-number'};
236     } else {
237         ($orderby,$startat,$number)=@_;
238     }
239     unless ($self->{BIBLIOCONTENTS}->{orderby}->{$orderby}) {
240         $self->loadcontents(-orderby=>$orderby, -startat=>$startat, -number=>$number);
241     }
242     my $endat=$startat+$number;
243     my @return;
244     foreach (@{$self->{BIBLIOCONTENTS}->{orderby}->{$orderby}}[$startat..$endat]) {
245         push @return,$_;
246     }
247     return \@return;
248 }
249
250
251 =head2 C<itemcounter()>
252
253 returns the number of items on the shelf
254
255     my $itemcount=$shelf->itemcounter();
256
257 =cut
258 sub itemcounter {
259     my $self=shift;
260     unless ($self->{ITEMCONTENTS}->{orderby}->{'natural'}) {
261         $self->loadcontents();
262     }
263     my @temparray=@{$self->{ITEMCONTENTS}->{orderby}->{'natural'}};
264     return $#temparray+1;
265 }
266
267 sub shelfcontents {
268     my $self=shift;
269 }
270
271
272 =head2 C<clearcontents()>
273
274 Removes all contents from the shelf.
275
276     $shelf->clearcontents();
277
278 =cut
279
280 sub clearcontents {
281     my $self=shift;
282     my $dbh=C4::Context->dbh();
283     my $sth=$dbh->prepare("delete from bookshelfcontents where bookshelfid=?");
284     $sth->execute($self->{ID});
285     foreach my $level ('ITEM', 'BIBLIOITEM', 'BIBLIO') {
286         delete $self->{$level."CONTENTS"};
287         $self->{$level."CONTENTS"}={};
288     }
289     $self->clearcache();
290
291 }
292
293
294
295 =head2 C<addtoshelf()>
296
297 adds an array of items to a shelf.  If any modifications are actually made to
298 the shelf then the per process caches and the FileCache for that shelf are
299 cleared.
300
301   $shelf->addtoshelf(-add => [[ 45, 54, 67], [69, 87, 143]]);
302
303 =cut
304
305 sub addtoshelf {
306     my $self=shift;
307     my $add;
308     if ($_[0]=~/^\-/) {
309         my %params=@_;
310         $add=$params{'-add'};
311     } else {
312         ($add)=@_;
313     }
314     my $dbh=C4::Context->dbh();
315     my $sth;
316     my $bookshelfid=$self->{ID};
317     my $clearcache=0;
318     foreach (@$add) {
319         my ($biblionumber,$biblioitemnumber,$itemnumber) = @$_;
320         $sth=$dbh->prepare("select count(*) from bookshelfcontents where bookshelfid=? and itemnumber=? and biblioitemnumber=? and biblionumber=?");
321         $sth->execute($bookshelfid,$itemnumber,$biblioitemnumber,$biblionumber);
322         my $rows=$sth->fetchrow();
323         if ($rows==0) {
324             $sth=$dbh->prepare("insert into bookshelfcontents (bookshelfid,biblionumber,biblioitemnumber,itemnumber) values (?,?,?,?)");
325             $sth->execute($bookshelfid,$biblionumber,$biblioitemnumber,$itemnumber);
326             $clearcache=1;
327         }
328     }
329     ($clearcache) && ($self->clearcache());
330 }
331
332
333 sub removefromshelf {
334     my $self=shift;
335 }
336
337 =head2 C<attribute()>
338
339 Returns or sets the value of a given attribute for the shelf.
340
341   my $loanlength=$shelf->attribute('loanlength');
342   $shelf->attribute('loanlength', '21 days');
343
344
345 =cut
346
347 sub attribute {
348     my $self=shift;
349     my ($attribute, $value);
350     $attribute=shift;
351     $value=shift;
352     if ($value) {
353         $self->{ATTRIBUTES}->{$attribute}=$value;
354         my $dbh=C4::Context->dbh();
355         my $sth=$dbh->prepare("select value from bookshelfattributes where bookshelfid=? and attribute=?");
356         $sth->execute($self->{ID}, $attribute);
357         if ($sth->rows) {
358             my $sti=$dbh->prepare("update bookshelfattributes set value=? where bookshelfid=? and attribute=?");
359             $sti->execute($value, $self->{ID}, $attribute);
360         } else {
361             my $sti=$dbh->prepare("insert into bookshelfattributes (bookshelfid, attribute, value) values (?, ?, ?)");
362             $sti->execute($self->{ID}, $attribute, $value);
363         }
364     }
365     return $self->{ATTRIBUTES}->{$attribute};
366 }
367
368
369 =head2 C<attributes()>
370
371 Returns a hash reference of the shelf attributes
372
373     my $attributes=$shelf->attributes();
374     my $loanlength=$attributes->{loanlength};
375
376 =cut
377
378 sub attributes {
379     my $self=shift;
380     return $self->{ATTRIBUTES};
381 }
382
383 =head2 C<clearcache()>
384
385 Clears the per process in-memory cache and the FileCache if any changes are
386 made to a shelf.
387
388   $shelf->clearshelf();
389
390 =cut
391
392 sub clearcache {
393     my $self=shift;
394     foreach my $level ('ITEM','BIBLIOITEM','BIBLIO') {
395         delete $self->{$level."CONTENTS"};
396         foreach my $sorttype (('author', 'title')) {
397             $self->{CACHE}->remove($self->{ID}."_".$level."CONTENTS_".$sorttype);
398         }
399     }
400 }
401
402
403 =head2 C<loadcontents()>
404
405 loads the contents of a particular shelf and loads into a per process memory
406 cache as well as a shared Cache::FileCache.
407
408 This subroutine is normally only used internally (called by itemcontents,
409 biblioitemcontents, or bibliocontents).
410
411   $shelf->loadcontents(-orderby => 'author', -startat => 30, -number => 10);
412
413
414 =cut
415
416 sub loadcontents {
417     my $self=shift;
418     my ($orderby,$startat,$number);
419     if ($_[0]=~/^\-/) {
420         my %params=@_;
421         $orderby=$params{'-orderby'};
422         $startat=$params{'-startat'};
423         $number=$params{'-number'};
424     } else {
425         ($orderby,$startat,$number)=@_;
426     }
427     my $bookshelfid=$self->{ID};
428     ($orderby) || ($orderby='natural');
429     $self->{ITEMCONTENTS}->{orderby}->{$orderby}=$self->{CACHE}->get( "$bookshelfid\_ITEMCONTENTS_$orderby" );
430     $self->{BIBLIOITEMCONTENTS}->{orderby}->{$orderby}=$self->{CACHE}->get( "$bookshelfid\_BIBLIOITEMCONTENTS_$orderby" );
431     $self->{BIBLIOCONTENTS}->{orderby}->{$orderby}=$self->{CACHE}->get( "$bookshelfid\_BIBLIOCONTENTS_$orderby" );
432     if ( defined $self->{ITEMCONTENTS}->{orderby}->{$orderby}) {
433         return;
434     }
435     my $dbh=C4::Context->dbh();
436     my $sth;
437     my $limit='';
438     if ($startat && $number) {
439         $limit="limit $startat,$number";
440     }
441     $limit='';
442     my $biblionumbers;
443     my $biblioitemnumbers;
444     if ($orderby eq 'author') {
445         $sth=$dbh->prepare("select itemnumber,BSC.biblionumber,BSC.biblioitemnumber from bookshelfcontents BSC, biblio B where BSC.biblionumber=B.biblionumber and bookshelfid=? order by B.author $limit");
446     } elsif ($orderby eq 'title') {
447         $sth=$dbh->prepare("select itemnumber,BSC.biblionumber,BSC.biblioitemnumber from bookshelfcontents BSC, biblio B where BSC.biblionumber=B.biblionumber and bookshelfid=? order by B.title $limit");
448     } else {
449         $sth=$dbh->prepare("select itemnumber,biblionumber,biblioitemnumber from bookshelfcontents where bookshelfid=? $limit");
450     }
451     $sth->execute($bookshelfid);
452     my @results;
453     my @biblioresults;
454     my @biblioitemresults;
455     while (my ($itemnumber,$biblionumber,$biblioitemnumber) = $sth->fetchrow) {
456         unless ($biblionumbers->{$biblionumber}) {
457             $biblionumbers->{$biblionumber}=1;
458             push @biblioresults, $biblionumber;
459         }
460         unless ($biblioitemnumbers->{$biblioitemnumber}) {
461             $biblioitemnumbers->{$biblioitemnumber}=1;
462             push @biblioitemresults, $biblioitemnumber;
463         }
464         push @results, $itemnumber;
465     }
466     $self->{CACHE}->set("$bookshelfid\_ITEMCONTENTS_$orderby", \@results, "3 hours");
467     $self->{CACHE}->set("$bookshelfid\_BIBLIOITEMCONTENTS_$orderby", \@results, "3 hours");
468     $self->{CACHE}->set("$bookshelfid\_BIBLIOCONTENTS_$orderby", \@results, "3 hours");
469     $self->{ITEMCONTENTS}->{orderby}->{$orderby}=\@results;
470     $self->{BIBLIOOCONTENTS}->{orderby}->{$orderby}=\@biblioresults;
471     $self->{BIBLIOITEMCONTENTS}->{orderby}->{$orderby}=\@biblioitemresults;
472 }
473
474
475
476 1;