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