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