Bug 29407: Make the pickup locations dropdown JS reusable
[koha.git] / C4 / Tags.pm
1 package C4::Tags;
2
3 # Copyright Liblime 2008
4 # Parts Copyright ACPL 2011
5 #
6 # This file is part of Koha.
7 #
8 # Koha is free software; you can redistribute it and/or modify it
9 # under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # Koha is distributed in the hope that it will be useful, but
14 # WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with Koha; if not, see <http://www.gnu.org/licenses>.
20
21 use strict;
22 use warnings;
23 use Carp qw( carp );
24 use Exporter;
25
26 use C4::Context;
27 use Module::Load::Conditional qw( check_install );
28 #use Data::Dumper;
29 use constant TAG_FIELDS => qw(tag_id borrowernumber biblionumber term language date_created);
30 use constant TAG_SELECT => "SELECT " . join(',', TAG_FIELDS) . "\n FROM   tags_all\n";
31
32 our (@ISA, @EXPORT_OK);
33 BEGIN {
34     @ISA       = qw(Exporter);
35     @EXPORT_OK = qw(
36       get_tag get_tags get_tag_rows
37       add_tags
38       add_tag
39       add_tag_approval
40       add_tag_index
41       delete_tag_row_by_id
42       remove_tag
43       delete_tag_rows_by_ids
44       get_approval_rows
45       blacklist
46       whitelist
47       is_approved
48       approval_counts
49       get_count_by_tag_status
50       get_filters
51       stratify_tags
52     );
53     my $ext_dict = C4::Context->preference('TagsExternalDictionary');
54     if ( $ext_dict && ! check_install( module => 'Lingua::Ispell' ) ) {
55         warn "Ignoring TagsExternalDictionary, because Lingua::Ispell is not installed.";
56         $ext_dict = q{};
57     }
58         if ($ext_dict) {
59                 require Lingua::Ispell;
60         import Lingua::Ispell qw(spellcheck add_word_lc);
61         $Lingua::Ispell::path = $ext_dict;
62         }
63 }
64
65 =head1 C4::Tags.pm - Support for user tagging of biblios.
66
67 =cut
68
69 sub get_filters {
70         my $query = "SELECT * FROM tags_filters ";
71         my ($sth);
72         if (@_) {
73                 $sth = C4::Context->dbh->prepare($query . " WHERE filter_id = ? ");
74                 $sth->execute(shift);
75         } else {
76                 $sth = C4::Context->dbh->prepare($query);
77                 $sth->execute;
78         }
79         return $sth->fetchall_arrayref({});
80 }
81
82 #       (SELECT count(*) FROM tags_all     ) as tags_all,
83 #       (SELECT count(*) FROM tags_index   ) as tags_index,
84
85 sub approval_counts {
86         my $query = "SELECT
87                 (SELECT count(*) FROM tags_approval WHERE approved= 1) as approved_count,
88                 (SELECT count(*) FROM tags_approval WHERE approved=-1) as rejected_count,
89                 (SELECT count(*) FROM tags_approval WHERE approved= 0) as unapproved_count
90         ";
91         my $sth = C4::Context->dbh->prepare($query);
92         $sth->execute;
93         my $result = $sth->fetchrow_hashref();
94         $result->{approved_total} = $result->{approved_count} + $result->{rejected_count} + $result->{unapproved_count};
95         return $result;
96 }
97
98 =head2 get_count_by_tag_status
99
100   get_count_by_tag_status($status);
101
102 Takes a status and gets a count of tags with that status
103
104 =cut
105
106 sub get_count_by_tag_status  {
107     my ($status) = @_;
108     my $dbh            = C4::Context->dbh;
109     my $query          =
110       "SELECT count(*) FROM tags_approval WHERE approved=?";
111     my $sth = $dbh->prepare($query);
112     $sth->execute( $status );
113   return $sth->fetchrow;
114 }
115
116 sub remove_tag {
117         my $tag_id  = shift or return;
118         my $user_id = (@_) ? shift : undef;
119         my $rows = (defined $user_id) ?
120                         get_tag_rows({tag_id=>$tag_id, borrowernumber=>$user_id}) :
121                         get_tag_rows({tag_id=>$tag_id}) ;
122         $rows or return 0;
123         (scalar(@$rows) == 1) or return;        # should never happen (duplicate ids)
124         my $row = shift(@$rows);
125         ($tag_id == $row->{tag_id}) or return 0;
126         my $tags = get_tags({term=>$row->{term}, biblionumber=>$row->{biblionumber}});
127         my $index = shift(@$tags);
128         if ($index->{weight} <= 1) {
129                 delete_tag_index($row->{term},$row->{biblionumber});
130         } else {
131                 decrement_weight($row->{term},$row->{biblionumber});
132         }
133         if ($index->{weight_total} <= 1) {
134                 delete_tag_approval($row->{term});
135         } else {
136                 decrement_weight_total($row->{term});
137         }
138         delete_tag_row_by_id($tag_id);
139 }
140
141 sub delete_tag_index {
142         (@_) or return;
143         my $sth = C4::Context->dbh->prepare("DELETE FROM tags_index WHERE term = ? AND biblionumber = ? LIMIT 1");
144         $sth->execute(@_);
145         return $sth->rows || 0;
146 }
147 sub delete_tag_approval {
148         (@_) or return;
149         my $sth = C4::Context->dbh->prepare("DELETE FROM tags_approval WHERE term = ? LIMIT 1");
150         $sth->execute(shift);
151         return $sth->rows || 0;
152 }
153 sub delete_tag_row_by_id {
154         (@_) or return;
155         my $sth = C4::Context->dbh->prepare("DELETE FROM tags_all WHERE tag_id = ? LIMIT 1");
156         $sth->execute(shift);
157         return $sth->rows || 0;
158 }
159 sub delete_tag_rows_by_ids {
160         (@_) or return;
161         my $i=0;
162         foreach(@_) {
163                 $i += delete_tag_row_by_id($_);
164         }
165         ($i == scalar(@_)) or
166                 warn sprintf "delete_tag_rows_by_ids tried %s tag_ids, only succeeded on $i", scalar(@_);
167         return $i;
168 }
169
170 sub get_tag_rows {
171         my $hash = shift || {};
172     my @ok_fields = TAG_FIELDS;
173         push @ok_fields, 'limit';       # push the limit! :)
174         my $wheres;
175         my $limit  = "";
176         my @exe_args = ();
177         foreach my $key (keys %$hash) {
178                 unless (length $key) {
179                         carp "Empty argument key to get_tag_rows: ignoring!";
180                         next;
181                 }
182                 unless (1 == scalar grep { $_ eq $key } @ok_fields) {
183                         carp "get_tag_rows received unreconized argument key '$key'.";
184                         next;
185                 }
186                 if ($key eq 'limit') {
187                         my $val = $hash->{$key};
188                         unless ($val =~ /^(\d+,)?\d+$/) {
189                                 carp "Non-nuerical limit value '$val' ignored!";
190                                 next;
191                         }
192                         $limit = " LIMIT $val\n";
193                 } else {
194                         $wheres .= ($wheres) ? " AND    $key = ?\n" : " WHERE  $key = ?\n";
195                         push @exe_args, $hash->{$key};
196                 }
197         }
198     my $query = TAG_SELECT . ($wheres||'') . $limit;
199         my $sth = C4::Context->dbh->prepare($query);
200         if (@exe_args) {
201                 $sth->execute(@exe_args);
202         } else {
203                 $sth->execute;
204         }
205         return $sth->fetchall_arrayref({});
206 }
207
208 sub get_tags {          # i.e., from tags_index
209         my $hash = shift || {};
210         my @ok_fields = qw(term biblionumber weight limit sort approved);
211         my $wheres;
212         my $limit  = "";
213         my $order  = "";
214         my @exe_args = ();
215         foreach my $key (keys %$hash) {
216                 unless (length $key) {
217                         carp "Empty argument key to get_tags: ignoring!";
218                         next;
219                 }
220                 unless (1 == scalar grep { $_ eq $key } @ok_fields) {
221                         carp "get_tags received unreconized argument key '$key'.";
222                         next;
223                 }
224                 if ($key eq 'limit') {
225                         my $val = $hash->{$key};
226                         unless ($val =~ /^(\d+,)?\d+$/) {
227                                 carp "Non-nuerical limit value '$val' ignored!";
228                                 next;
229                         }
230                         $limit = " LIMIT $val\n";
231                 } elsif ($key eq 'sort') {
232                         foreach my $by (split /\,/, $hash->{$key}) {
233                                 unless (
234                                         $by =~ /^([-+])?(term)/ or
235                                         $by =~ /^([-+])?(biblionumber)/ or
236                                         $by =~ /^([-+])?(weight)/
237                                 ) {
238                                         carp "get_tags received illegal sort order '$by'";
239                                         next;
240                                 }
241                                 if ($order) {
242                                         $order .= ", ";
243                                 } else {
244                                         $order = " ORDER BY ";
245                                 }
246                                 $order .= $2 . " " . ((!$1) ? '' : $1 eq '-' ? 'DESC' : $1 eq '+' ? 'ASC' : '') . "\n";
247                         }
248                         
249                 } else {
250                         my $whereval = $hash->{$key};
251                         my $longkey = ($key eq 'term'    ) ? 'tags_index.term'        :
252                                                   ($key eq 'approved') ? 'tags_approval.approved' : $key;
253                         my $op = ($whereval =~ s/^(>=|<=)// or
254                                           $whereval =~ s/^(>|=|<)//   ) ? $1 : '=';
255                         $wheres .= ($wheres) ? " AND    $longkey $op ?\n" : " WHERE  $longkey $op ?\n";
256                         push @exe_args, $whereval;
257                 }
258         }
259         my $query = "
260         SELECT    tags_index.term as term,biblionumber,weight,weight_total
261         FROM      tags_index
262         LEFT JOIN tags_approval 
263         ON        tags_index.term = tags_approval.term
264         " . ($wheres||'') . $order . $limit;
265         my $sth = C4::Context->dbh->prepare($query);
266         if (@exe_args) {
267                 $sth->execute(@exe_args);
268         } else {
269                 $sth->execute;
270         }
271         return $sth->fetchall_arrayref({});
272 }
273
274 sub get_approval_rows {         # i.e., from tags_approval
275         my $hash = shift || {};
276         my @ok_fields = qw(term approved date_approved approved_by weight_total limit sort borrowernumber);
277         my $wheres;
278         my $limit  = "";
279         my $order  = "";
280         my @exe_args = ();
281         foreach my $key (keys %$hash) {
282                 unless (length $key) {
283                         carp "Empty argument key to get_approval_rows: ignoring!";
284                         next;
285                 }
286                 unless (1 == scalar grep { $_ eq $key } @ok_fields) {
287                         carp "get_approval_rows received unreconized argument key '$key'.";
288                         next;
289                 }
290                 if ($key eq 'limit') {
291                         my $val = $hash->{$key};
292                         unless ($val =~ /^(\d+,)?\d+$/) {
293                                 carp "Non-numerical limit value '$val' ignored!";
294                                 next;
295                         }
296                         $limit = " LIMIT $val\n";
297                 } elsif ($key eq 'sort') {
298                         foreach my $by (split /\,/, $hash->{$key}) {
299                                 unless (
300                                         $by =~ /^([-+])?(term)/            or
301                                         $by =~ /^([-+])?(biblionumber)/    or
302                     $by =~ /^([-+])?(borrowernumber)/  or
303                                         $by =~ /^([-+])?(weight_total)/    or
304                                         $by =~ /^([-+])?(approved(_by)?)/  or
305                                         $by =~ /^([-+])?(date_approved)/
306                                 ) {
307                                         carp "get_approval_rows received illegal sort order '$by'";
308                                         next;
309                                 }
310                                 if ($order) {
311                                         $order .= ", ";
312                                 } else {
313                                         $order = " ORDER BY " unless $order;
314                                 }
315                                 $order .= $2 . " " . ((!$1) ? '' : $1 eq '-' ? 'DESC' : $1 eq '+' ? 'ASC' : '') . "\n";
316                         }
317                         
318                 } else {
319                         my $whereval = $hash->{$key};
320                         my $op = ($whereval =~ s/^(>=|<=)// or
321                                           $whereval =~ s/^(>|=|<)//   ) ? $1 : '=';
322                         $wheres .= ($wheres) ? " AND    $key $op ?\n" : " WHERE  $key $op ?\n";
323                         push @exe_args, $whereval;
324                 }
325         }
326         my $query = "
327         SELECT  tags_approval.term          AS term,
328                         tags_approval.approved      AS approved,
329                         tags_approval.date_approved AS date_approved,
330                         tags_approval.approved_by   AS approved_by,
331                         tags_approval.weight_total  AS weight_total,
332                         CONCAT(borrowers.surname, ', ', borrowers.firstname) AS approved_by_name
333         FROM    tags_approval
334         LEFT JOIN borrowers
335         ON      tags_approval.approved_by = borrowers.borrowernumber ";
336         $query .= ($wheres||'') . $order . $limit;
337         my $sth = C4::Context->dbh->prepare($query);
338         if (@exe_args) {
339                 $sth->execute(@exe_args);
340         } else {
341                 $sth->execute;
342         }
343         return $sth->fetchall_arrayref({});
344 }
345
346 sub is_approved {
347         my $term = shift or return;
348         my $sth = C4::Context->dbh->prepare("SELECT approved FROM tags_approval WHERE term = ?");
349         $sth->execute($term);
350     my $ext_dict = C4::Context->preference('TagsExternalDictionary');
351         unless ($sth->rows) {
352                 $ext_dict and return (spellcheck($term) ? 0 : 1);       # spellcheck returns empty on OK word
353                 return 0;
354         }
355         return $sth->fetchrow;
356 }
357
358 sub get_tag_index {
359         my $term = shift or return;
360         my $sth;
361         if (@_) {
362                 $sth = C4::Context->dbh->prepare("SELECT * FROM tags_index WHERE term = ? AND biblionumber = ?");
363                 $sth->execute($term,shift);
364         } else {
365                 $sth = C4::Context->dbh->prepare("SELECT * FROM tags_index WHERE term = ?");
366                 $sth->execute($term);
367         }
368         return $sth->fetchrow_hashref;
369 }
370
371 sub whitelist {
372         my $operator = shift;
373         defined $operator or return; # have to test defined to allow =0 (kohaadmin)
374     my $ext_dict = C4::Context->preference('TagsExternalDictionary');
375         if ($ext_dict) {
376                 foreach (@_) {
377                         spellcheck($_) or next;
378                         add_word_lc($_);
379                 }
380         }
381         foreach (@_) {
382                 my $aref = get_approval_rows({term=>$_});
383                 if ($aref and scalar @$aref) {
384                         mod_tag_approval($operator,$_,1);
385                 } else {
386                         add_tag_approval($_,$operator);
387                 }
388         }
389         return scalar @_;
390 }
391 # note: there is no "unwhitelist" operation because there is no remove for Ispell.
392 # The blacklist regexps should operate "in front of" the whitelist, so if you approve
393 # a term mistakenly, you can still reverse it. But there is no going back to "neutral".
394 sub blacklist {
395         my $operator = shift;
396         defined $operator or return; # have to test defined to allow =0 (kohaadmin)
397         foreach (@_) {
398                 my $aref = get_approval_rows({term=>$_});
399                 if ($aref and scalar @$aref) {
400                         mod_tag_approval($operator,$_,-1);
401                 } else {
402                         add_tag_approval($_,$operator,-1);
403                 }
404         }
405         return scalar @_;
406 }
407 sub add_filter {
408         my $operator = shift;
409         defined $operator or return; # have to test defined to allow =0 (kohaadmin)
410         my $query = "INSERT INTO tags_blacklist (regexp,y,z) VALUES (?,?,?)";
411         # my $sth = C4::Context->dbh->prepare($query);
412         return scalar @_;
413 }
414 sub remove_filter {
415         my $operator = shift;
416         defined $operator or return; # have to test defined to allow =0 (kohaadmin)
417         my $query = "REMOVE FROM tags_blacklist WHERE blacklist_id = ?";
418         # my $sth = C4::Context->dbh->prepare($query);
419         # $sth->execute($term);
420         return scalar @_;
421 }
422
423 sub add_tag_approval {  # or disapproval
424         my $term = shift or return;
425         my $query = "SELECT * FROM tags_approval WHERE term = ?";
426         my $sth = C4::Context->dbh->prepare($query);
427         $sth->execute($term);
428         ($sth->rows) and return increment_weight_total($term);
429         my $operator = shift || 0;
430         my $approval = (@_ ? shift : 0);        # default is unapproved
431         my @exe_args = ($term);         # all 3 queries will use this argument
432         if ($operator) {
433                 $query = "INSERT INTO tags_approval (term,approved_by,approved,date_approved) VALUES (?,?,?,NOW())";
434                 push @exe_args, $operator, $approval;
435         } elsif ($approval) {
436                 $query = "INSERT INTO tags_approval (term,approved,date_approved) VALUES (?,?,NOW())";
437                 push @exe_args, $approval;
438         } else {
439                 $query = "INSERT INTO tags_approval (term,date_approved) VALUES (?,NOW())";
440         }
441         $sth = C4::Context->dbh->prepare($query);
442         $sth->execute(@exe_args);
443         return $sth->rows;
444 }
445
446 sub mod_tag_approval {
447         my $operator = shift;
448         defined $operator or return; # have to test defined to allow =0 (kohaadmin)
449         my $term     = shift or return;
450         my $approval = (scalar @_ ? shift : 1); # default is to approve
451         my $query = "UPDATE tags_approval SET approved_by=?, approved=?, date_approved=NOW() WHERE term = ?";
452         my $sth = C4::Context->dbh->prepare($query);
453         $sth->execute($operator,$approval,$term);
454 }
455
456 sub add_tag_index {
457         my $term         = shift or return;
458         my $biblionumber = shift or return;
459         my $query = "SELECT * FROM tags_index WHERE term = ? AND biblionumber = ?";
460         my $sth = C4::Context->dbh->prepare($query);
461         $sth->execute($term,$biblionumber);
462         ($sth->rows) and return increment_weight($term,$biblionumber);
463         $query = "INSERT INTO tags_index (term,biblionumber) VALUES (?,?)";
464         $sth = C4::Context->dbh->prepare($query);
465         $sth->execute($term,$biblionumber);
466         return $sth->rows;
467 }
468
469 sub get_tag {           # by tag_id
470         (@_) or return;
471     my $sth = C4::Context->dbh->prepare(TAG_SELECT . "WHERE tag_id = ?");
472         $sth->execute(shift);
473         return $sth->fetchrow_hashref;
474 }
475
476 sub increment_weights {
477         increment_weight(@_);
478         increment_weight_total(shift);
479 }
480 sub decrement_weights {
481         decrement_weight(@_);
482         decrement_weight_total(shift);
483 }
484 sub increment_weight_total {
485         _set_weight_total('weight_total+1',shift);
486 }
487 sub increment_weight {
488         _set_weight('weight+1',shift,shift);
489 }
490 sub decrement_weight_total {
491         _set_weight_total('weight_total-1',shift);
492 }
493 sub decrement_weight {
494         _set_weight('weight-1',shift,shift);
495 }
496 sub _set_weight_total {
497         my $sth = C4::Context->dbh->prepare("
498         UPDATE tags_approval
499         SET    weight_total=" . (shift) . "
500         WHERE  term=?
501         ");                                             # note: CANNOT use "?" for weight_total (see the args above).
502         $sth->execute(shift);   # just the term
503 }
504 sub _set_weight {
505         my $dbh = C4::Context->dbh;
506         my $sth = $dbh->prepare("
507         UPDATE tags_index
508         SET    weight=" . (shift) . "
509         WHERE  term=?
510         AND    biblionumber=?
511         ");
512         $sth->execute(@_);
513 }
514
515 sub add_tag {   # biblionumber,term,[borrowernumber,approvernumber]
516         my $biblionumber = shift or return;
517         my $term         = shift or return;
518         my $borrowernumber = (@_) ? shift : 0;          # the user, default to kohaadmin
519         $term =~ s/^\s+//;
520         $term =~ s/\s+$//;
521         ($term) or return;      # must be more than whitespace
522         my $rows = get_tag_rows({biblionumber=>$biblionumber, borrowernumber=>$borrowernumber, term=>$term, limit=>1});
523         my $query = "INSERT INTO tags_all
524         (borrowernumber,biblionumber,term,date_created)
525         VALUES (?,?,?,NOW())";
526         if (scalar @$rows) {
527                 return;
528         }
529         # add to tags_all regardless of approaval
530         my $sth = C4::Context->dbh->prepare($query);
531         $sth->execute($borrowernumber,$biblionumber,$term);
532
533         # then 
534         if (scalar @_) {        # if arg remains, it is the borrowernumber of the approver: tag is pre-approved.
535                 my $approver = shift;
536                 add_tag_approval($term,$approver,1);
537                 add_tag_index($term,$biblionumber,$approver);
538         } elsif (is_approved($term) >= 1) {
539                 add_tag_approval($term,0,1);
540                 add_tag_index($term,$biblionumber,1);
541         } else {
542                 add_tag_approval($term);
543                 add_tag_index($term,$biblionumber);
544         }
545 }
546
547 # This takes a set of tags, as returned by C<get_approval_rows> and divides
548 # them up into a number of "strata" based on their weight. This is useful
549 # to display them in a number of different sizes.
550 #
551 # Usage:
552 #   ($min, $max) = stratify_tags($strata, $tags);
553 # $stratum: the number of divisions you want
554 # $tags: the tags, as provided by get_approval_rows
555 # $min: the minimum stratum value
556 # $max: the maximum stratum value. This may be the same as $min if there
557 # is only one weight. Beware of divide by zeros.
558 # This will add a field to the tag called "stratum" containing the calculated
559 # value.
560 sub stratify_tags {
561     my ( $strata, $tags ) = @_;
562     return (0,0) if !@$tags;
563     my ( $min, $max );
564     foreach (@$tags) {
565         my $w = $_->{weight_total};
566         $min = $w if ( !defined($min) || $min > $w );
567         $max = $w if ( !defined($max) || $max < $w );
568     }
569
570     # normalise min to zero
571     $max = $max - $min;
572     my $orig_min = $min;
573     $min = 0;
574
575     # if min and max are the same, just make it 1
576     my $span = ( $strata - 1 ) / ( $max || 1 );
577     foreach (@$tags) {
578         my $w = $_->{weight_total};
579         $_->{stratum} = int( ( $w - $orig_min ) * $span );
580     }
581     return ( $min, $max );
582 }
583
584 1;
585 __END__
586
587 =head2 add_tag(biblionumber,term[,borrowernumber])
588
589 =head3 TO DO: Add real perldoc
590
591 =cut
592
593 =head2 External Dictionary (Ispell) [Recommended]
594
595 An external dictionary can be used as a means of "pre-populating" and tracking
596 allowed terms based on the widely available Ispell dictionary.  This can be the system
597 dictionary or a personal version, but in order to support whitelisting, it must be
598 editable to the process running Koha.  
599
600 To enable, enter the absolute path to the ispell dictionary in the system
601 preference "TagsExternalDictionary".
602
603 Using external Ispell is recommended for both ease of use and performance.  Note that any
604 language version of Ispell can be installed.  It is also possible to modify the dictionary 
605 at the command line to affect the desired content.
606
607 WARNING: The default Ispell dictionary includes (properly spelled) obscenities!  Users 
608 should build their own wordlist and recompile Ispell based on it.  See man ispell for 
609 instructions.
610
611 =head2 Table Structure
612
613 The tables used by tags are:
614         tags_all
615         tags_index
616         tags_approval
617         tags_blacklist
618
619 Your first thought may be that this looks a little complicated.  It is, but only because
620 it has to be.  I'll try to explain.
621
622 tags_all - This table would be all we really need if we didn't care about moderation or
623 performance or tags disappearing when borrowers are removed.  Too bad, we do.  Otherwise
624 though, it contains all the relevant info about a given tag:
625         tag_id         - unique id number for it
626         borrowernumber - user that entered it
627         biblionumber   - book record it is attached to
628         term           - tag "term" itself
629         language       - perhaps used later to influence weighting
630         date_created   - date and time it was created
631
632 tags_approval - Since we need to provide moderation, this table is used to track it.  If no
633 external dictionary is used, this table is the sole reference for approval and rejection.
634 With an external dictionary, it tracks pending terms and past whitelist/blacklist actions.
635 This could be called an "approved terms" table.  See above regarding the External Dictionary.
636         term           - tag "term" itself 
637         approved       - Negative, 0 or positive if tag is rejected, pending or approved.
638         date_approved  - date of last action
639         approved_by    - staffer performing the last action
640     weight_total   - total occurrence of term in any biblio by any users
641
642 tags_index - This table is for performance, because by far the most common operation will 
643 be fetching tags for a list of search results.  We will have a set of biblios, and we will
644 want ONLY their approved tags and overall weighting.  While we could implement a query that
645 would traverse tags_all filtered against tags_approval, the performance implications of
646 trying to calculate that and the "weight" (number of times a tag appears) on the fly are drastic.
647         term           - approved term as it appears in tags_approval
648         biblionumber   - book record it is attached to
649         weight         - number of times tag applied by any user
650
651 tags_blacklist - A set of regular expression filters.  Unsurprisingly, these should be perl-
652 compatible (PCRE) for your version of perl.  Since this is a blacklist, a term will be
653 blocked if it matches any of the given patterns.  WARNING: do not add blacklist regexps
654 if you do not understand their operation and interaction.  It is quite easy to define too
655 simple or too complex a regexp and effectively block all terms.  The blacklist operation is 
656 fairly resource intensive, since every line of tags_blacklist will need to be read and compared.
657 It is recommended that tags_blacklist be used minimally, and only by an administrator with an
658 understanding of regular expression syntax and performance.
659
660 So the best way to think about the different tables is that they are each tailored to a certain
661 use.  Note that tags_approval and tags_index do not rely on the user's borrower mapping, so
662 the tag population can continue to grow even if a user (along with their corresponding
663 rows in tags_all) is removed.  
664
665 =head2 Tricks
666
667 If you want to auto-populate some tags for debugging, do something like this:
668
669 mysql> select biblionumber from biblio where title LIKE "%Health%";
670 +--------------+
671 | biblionumber |
672 +--------------+
673 |           18 | 
674 |           22 | 
675 |           24 | 
676 |           30 | 
677 |           44 | 
678 |           45 | 
679 |           46 | 
680 |           49 | 
681 |          111 | 
682 |          113 | 
683 |          128 | 
684 |          146 | 
685 |          155 | 
686 |          518 | 
687 |          522 | 
688 |          524 | 
689 |          530 | 
690 |          544 | 
691 |          545 | 
692 |          546 | 
693 |          549 | 
694 |          611 | 
695 |          613 | 
696 |          628 | 
697 |          646 | 
698 |          655 | 
699 +--------------+
700 26 rows in set (0.00 sec)
701
702 Then, take those numbers and type/pipe them into this perl command line:
703 perl -ne 'use C4::Tags qw(get_tags add_tag); use Data::Dumper;chomp; add_tag($_,"health",51,1); print Dumper get_tags({limit=>5,term=>"health",});'
704
705 Note, the borrowernumber in this example is 51.  Use your own or any arbitrary valid borrowernumber.
706
707 =cut
708