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