root/branches/release-30/lib/MT/Tag.pm @ 1372

Revision 1372, 17.8 kB (checked in by bchoate, 22 months ago)

Initial work for performance logging.

  • Property svn:keywords set to Id Revision
Line 
1# Movable Type (r) Open Source (C) 2001-2008 Six Apart, Ltd.
2# This program is distributed under the terms of the
3# GNU General Public License, version 2.
4#
5# $Id$
6
7package MT::Tag;
8
9use strict;
10use base qw( MT::Object );
11
12__PACKAGE__->install_properties({
13    column_defs => {
14        'id' => 'integer not null auto_increment primary key',
15        'name' => 'string(255) not null',
16        'n8d_id' => 'integer',
17        'is_private' => 'boolean'
18    },
19    indexes => {
20        name => 1,
21        is_private => 1,
22        n8d_id => 1,
23    },
24    defaults => {
25        n8d_id => 0,
26        is_private => 0,
27    },
28    child_classes => ['MT::ObjectTag'],
29    datasource => 'tag',
30    primary_key => 'id',
31});
32
33sub class_label {
34    return MT->translate('Tag');
35}
36
37sub class_label_plural {
38    return MT->translate('Tags');
39}
40
41sub save {
42    my $tag = shift;
43    my $name = $tag->name;
44    return $tag->error(MT->translate("Tag must have a valid name"))
45        unless defined($name) && length($name);
46    my $n8d = $tag->normalize;
47    return $tag->error(MT->translate("Tag must have a valid name"))
48        unless defined($n8d) && length($n8d);
49    if ($n8d ne $name) {
50        my $n8d_tag = MT::Tag->load({ name => $n8d });
51        if (!$n8d_tag) {
52            $n8d_tag = new MT::Tag;
53            $n8d_tag->name($n8d);
54            $n8d_tag->save;
55        }
56        if (!$tag->n8d_id || ($tag->n8d_id != $n8d_tag->id)) {
57            $tag->n8d_id($n8d_tag->id);
58        }
59    } else {
60        $tag->n8d_id(0) if $tag->n8d_id;
61    }
62    # maintain the private flag...
63    $tag->is_private( $name =~ m/^@/ ? 1 : 0 );
64    $tag->SUPER::save();
65}
66
67sub normalize {
68    my $tag = shift;
69    my ($str) = @_;
70    if (!@_ && !(ref $tag)) {
71        $str = $tag;
72    } elsif (!$str && (ref $tag)) {
73        $str = $tag->name;
74    }
75
76    my $private = $str =~ m/^@/;
77    $str = MT::I18N::encode_text( $str, MT->instance->config->PublishCharset, 'utf-8' );
78    $str =~ s/[@!`\\<>\*&#\/~\?'"\.\,=\(\)\${}\[\];:\ \+\-\r\n]+//gs;
79    $str = lc $str;
80    $str = '@' . $str if $private;
81    $str = MT::I18N::encode_text( $str, 'utf-8', MT->instance->config->PublishCharset );
82    $str;
83}
84
85sub remove {
86    my $tag = shift;
87    my $n8d_tag;
88    if (ref $tag) {
89        if (!$tag->n8d_id) {
90            # normalized tag! we can't delete if others reference us
91            my $child_tags = MT::Tag->count({n8d_id => $tag->id});
92            return $tag->error(MT->translate("This tag is referenced by others."))
93                if $child_tags;
94        } else {
95            $n8d_tag = MT::Tag->load($tag->n8d_id);
96        }
97    }
98    $tag->remove_children({key => 'tag_id'});
99    $tag->SUPER::remove(@_)
100        or return $tag->error($tag->errstr);
101    # check for an orphaned normalized tag and delete if necessary
102    if ($n8d_tag) {
103        # Normalized tag, no longer referenced by other tags...
104        if (!MT::Tag->count({n8d_id => $n8d_tag->id})) {
105            # Noramlized tag that no longer has any object tag associations
106            require MT::ObjectTag;
107            if (!MT::ObjectTag->count({tag_id => $n8d_tag->id})) {
108                $n8d_tag->remove
109                    or return $tag->error($n8d_tag->errstr);
110            }
111        }
112    }
113    1;
114}
115
116sub split {
117    my $pkg = shift;
118    my ($delim, $str) = @_;
119    $delim = quotemeta($delim);
120    my @tags;
121    $str =~ s/(^\s+|\s+$)//gs;
122    while (length($str) && ($str =~ m/^(((['"])(.*?)\3[^$delim]*?|.*?)($delim\s*|$))/s)) {
123        $str = substr($str, length($1));
124        my $tag = defined $4 ? $4 : $2;
125        #$tag =~ s/(^[\s,]+|[\s,]+$)//gs;
126        $tag =~ s/(^\s+|\s+$)//gs;
127        $tag =~ s/\s+/ /gs;
128        my $n8d_tag = MT::Tag->normalize($tag);
129        next if $n8d_tag eq '';
130        push @tags, $tag if $tag ne '';
131    }
132    @tags;
133}
134
135sub join {
136    my $obj = shift;
137    my ($delim, @tags) = @_;
138    my $tags = '';
139    foreach (@tags) {
140        $tags .= $delim . ($delim eq ' ' ? '' : ' ') if $tags ne '';
141        if (m/\Q$delim\E/) {
142            if (m/"/) {
143                $tags .= "'" . $_ . "'";
144            } else {
145                $tags .= '"' . $_ . '"';
146            }
147        } else {
148            $tags .= $_;
149        }
150    }
151    $tags;
152}
153
154sub load_by_datasource {
155    my $pkg = shift;
156    my ($datasource, $terms, $args) = @_;
157    $args ||= {};
158    $args->{'sort'} ||= 'name';
159    my $blog_id;
160    my %jargs;
161    if ($terms->{blog_id}) {
162        $blog_id = $terms->{blog_id};
163        delete $terms->{blog_id};
164        if ($args->{not} && $args->{not}{blog_id}) {
165            $jargs{not}{blog_id} = 1;
166        }
167    }
168    $args->{'join'} ||= MT::ObjectTag->join_on('tag_id', {
169        $blog_id ? (blog_id => $blog_id) : (),
170        object_datasource => $datasource
171    }, { unique => 1, %jargs });
172    my @tags = MT::Tag->load($terms, $args);
173    @tags;
174}
175
176# static method for tag cache control
177sub cache_obj {
178    my $pkg = shift;
179    my (%param) = @_;
180    my $blog_id = $param{blog_id};
181    my $ds = $param{datasource};
182
183    require MT::Session;
184    # Clear any tag cache if tags were modified upon saving
185    my $sess_id = ($blog_id ? 'blog:' . $blog_id . ';' : '') . 'datasource:' . $ds . ($param{private} ? ';private' : '');
186    my $tag_cache = MT::Session::get_unexpired_value(60 * 60, {
187        kind => 'TC',  # tag cache
188        id => $sess_id
189    });
190    if (!$tag_cache) {
191        $tag_cache = new MT::Session;
192        $tag_cache->kind('TC');
193        $tag_cache->id($sess_id);
194        $tag_cache->start(time);
195    }
196    $tag_cache;
197}
198
199sub clear_cache {
200    my $pkg = shift;
201    my (%param) = @_;
202    my $blog_id = $param{blog_id};
203    my $ds = $param{datasource};
204
205    my $tag_cache;
206    my $sess_id = ($blog_id ? 'blog:' . $blog_id . ';' : '') . 'datasource:' . $ds;
207    require MT::Session;
208    $tag_cache = MT::Session->load({
209        kind => 'TC',
210        id => $sess_id});
211    $tag_cache->remove if $tag_cache;
212
213    $sess_id = ($blog_id ? 'blog:' . $blog_id . ';' : '') . 'datasource:' . $ds . ';private';
214    $tag_cache = MT::Session->load({
215        kind => 'TC',
216        id => $sess_id});
217    $tag_cache->remove if $tag_cache;
218}
219
220sub cache {
221    my $pkg = shift;
222    my (%param) = @_;
223    my $blog_id = $param{blog_id};
224    my $class = $param{class};
225    if (ref($class) eq 'SCALAR') {
226        $class = eval "use $class;";
227        if (my $err = $@) {
228            $class = eval 'use MT::Entry;';
229        }
230    }
231    my $ds = $class->datasource;
232    $param{datasource} = $ds;
233
234    my $tag_cache = $pkg->cache_obj(%param);
235    my $data = $tag_cache->get('tag_cache');
236    if (!$data) {
237        my $tag_count;
238        require MT::ObjectTag;
239        $tag_count = {};
240        my $tag_count_iter =
241            MT::ObjectTag->count_group_by({
242                ($blog_id ? (blog_id => $blog_id) : ()),
243                object_datasource => $ds
244            }, { group => ['tag_id']});
245        while (my ($count, $tag_id) = $tag_count_iter->()) {
246            $tag_count->{$tag_id} = $count;
247        }
248        my $iter = $class->load_iter(undef,
249            { 'join' => ['MT::ObjectTag', 'object_id',
250            { ($blog_id ? (blog_id => $blog_id) : ()), object_datasource => $ds },
251            { unique => 1 } ],
252              sort => 'modified_on', direction => 'descend' 
253        });
254        my @tags;
255        my %tags_seen;
256        my $limit = MT->config->MaxTagAutoCompletionItems;
257        while (my $entry = $iter->()) {
258            my @etags = $entry->tags;
259            @etags = grep /^[^@]/, @etags unless $param{private};
260            @etags = grep { !exists($tags_seen{$_}) } @etags;
261            next if 0 == scalar(@etags);
262
263            $tags_seen{$_} = 1 for @etags;
264
265            my @ttags = MT::Tag->load({ 'name' => \@etags },
266                { 'join' => ['MT::ObjectTag', 'tag_id',
267                { ($blog_id ? (blog_id => $blog_id) : ()), object_datasource => $ds },
268                { unique => 1 } ]
269            });
270            if (scalar(@tags) + scalar(@ttags) <= $limit) {
271                push @tags, @ttags;
272            } else {
273                for (0..($limit - scalar(@tags)) - 1) {
274                    push @tags, $ttags[$_];
275                }
276            }
277            last if ($limit <= scalar(@tags));
278        }
279        $data = {};
280        if (@tags) {
281            foreach my $tag (@tags) {
282                $data->{$tag->name} = $tag_count->{$tag->id} || 1;
283            }
284            $tag_cache->set('tag_cache', $data);
285            $tag_cache->save;
286        }
287    }
288    $data || {};
289}
290
291# An interface for any MT::Object that wishes to utilize tags themselves
292
293package MT::Taggable;
294
295use constant TAG_CACHE_TIME => 7 * 24 * 60 * 60;  ## 1 week
296
297sub install_properties {
298    my $pkg = shift;
299    my ($class) = @_;
300
301    # synchronize tags if necessary
302    $class->add_trigger( post_save => \&post_save_tags );
303    $class->add_trigger( pre_remove => \&pre_remove_tags );
304}
305
306# post_save trigger for MT::Taggable objects to synchronize tags upon save.
307sub post_save_tags {
308    my $class = shift;
309    my ($obj) = @_;
310    $obj->save_tags;
311}
312
313sub pre_remove_tags {
314    my $class = shift;
315    my ($obj) = @_;
316    $obj->remove_tags if ref $obj;
317}
318
319sub tag_cache_key {
320    my $obj = shift;
321    return undef unless $obj->id;
322    return sprintf "%stags-%d", $obj->datasource, $obj->id;
323}
324
325sub __load_tags {
326    my $obj = shift;
327    my $t = MT->get_timer;
328    $t->pause_partial if $t;
329
330    if (!$obj->id) {
331        $obj->{__tags} = [];
332        return $obj->{__tag_objects} = [];
333    }
334    return if exists $obj->{__tag_objects};
335
336    require MT::Memcached;
337    my $cache = MT::Memcached->instance;
338    my $memkey = $obj->tag_cache_key;
339    my @tags;
340    if (my $tag_ids = $cache->get($memkey)) {
341        @tags = grep { defined } @{ MT::Tag->lookup_multi($tag_ids) };
342    } else {
343        require MT::ObjectTag;
344        @tags = MT::Tag->search(undef, { 
345            sort => 'name', 
346            join => [ 'MT::ObjectTag', 'tag_id', { object_id => $obj->id,
347                object_datasource => $obj->datasource }, { unique => 1 } ],       
348        });
349        $cache->set($memkey, [ map { $_->id } @tags ], TAG_CACHE_TIME);
350    }
351    $obj->{__tags} = [ map { $_->name } @tags ];
352    $t->mark('MT::Tag::__load_tags') if $t;
353    $obj->{__tag_objects} = \@tags;
354}
355
356sub get_tags {
357    my $obj = shift;
358    $obj->__load_tags unless $obj->{__tags} && @{ $obj->{__tags} };
359    return @{ $obj->{__tags} };
360}
361
362sub get_tag_objects {
363    my $obj = shift;
364    $obj->__load_tags;
365    return $obj->{__tag_objects};
366}
367
368sub set_tags {
369    my $obj = shift;
370    $obj->{__tags} = [ sort @_ ];
371    $obj->{__save_tags} = 1;
372}
373
374sub save_tags {
375    my $obj = shift;
376    return 1 unless $obj->{__save_tags};
377    require MT::ObjectTag;
378    my $clear_cache = 0;
379    my @tags = @{ $obj->{__tags} };
380    return 1 unless @tags;
381
382    my $t = MT->get_timer;
383    $t->pause_partial if $t;
384
385    $obj->{__tag_objects} = [];
386    my $blog_id = $obj->has_column('blog_id') ? $obj->blog_id : 0;
387    my @existing_tags = MT::ObjectTag->load({object_id => $obj->id,
388        object_datasource => $obj->datasource });
389    my %existing_tags = map { $_->tag_id => $_ } @existing_tags;
390    foreach my $tag_name (@tags) {
391        my $tag = MT::Tag->load({ name => $tag_name },
392            { binary => { name => 1 } } );
393        if ($tag) {
394            if (exists $existing_tags{$tag->id}) {
395                $existing_tags{$tag->id} = 0;
396                push @{ $obj->{__tag_objects} }, $tag;
397                next;
398            }
399        } else {
400            # new tag
401            $tag = new MT::Tag;
402            $tag->name($tag_name);
403            $tag->save or next;
404            $clear_cache = 1;
405        }
406        my $otag = new MT::ObjectTag;
407        $otag->blog_id($blog_id);
408        $otag->tag_id($tag->id);
409        $otag->object_id($obj->id);
410        $otag->object_datasource($obj->datasource);
411        $otag->save or return $obj->error($otag->errstr);
412        $existing_tags{$tag->id} = 0;
413        $clear_cache = 1;
414
415        push @{ $obj->{__tag_objects} }, $tag;
416    }
417
418    foreach my $otag (values %existing_tags) {
419        next unless ref $otag;
420        my $this_tag_id = $otag->tag_id;
421        $otag->remove;
422        if (! MT::ObjectTag->count({tag_id => $this_tag_id})) {
423            # no more references to this tag... just delete it now
424            if (my $tag = MT::Tag->load($this_tag_id)) {
425                $tag->remove;
426            }
427        }
428        $clear_cache = 1;
429    }
430    delete $obj->{__save_tags};
431    if ($clear_cache) {
432        MT::Tag->clear_cache(datasource => $obj->datasource, ($blog_id ? (blog_id => $blog_id) : ()));
433
434        require MT::Memcached;
435        MT::Memcached->instance->delete( $obj->tag_cache_key );
436    }
437    $t->mark('MT::Tag::save_tags') if $t;
438    1;
439}
440
441sub tags {
442    my $obj = shift;
443    $obj->set_tags(@_) if @_;
444    $obj->get_tags;
445}
446
447sub add_tags {
448    my $obj = shift;
449    my (@tags) = @_;
450    my @etags = $obj->tags;
451    push @tags, @etags;
452    my %uniq;
453    @uniq{@tags} = ();
454    $obj->set_tags(keys %uniq);
455}
456
457sub remove_tags {
458    my $obj = shift;
459    my (@tags) = @_;
460    if (@tags) {
461        my @etags = $obj->tags;
462        my %uniq;
463        @uniq{@etags} = ();
464        delete $uniq{$_} for @tags;
465        if (keys %uniq) {
466            $obj->set_tags(keys %uniq);
467            return;
468        }
469    }
470    require MT::ObjectTag;
471    my @et = MT::ObjectTag->load({ object_id => $obj->id,
472                                   object_datasource => $obj->datasource });
473    $_->remove for @et;
474    $obj->{__tags} = [];
475    delete $obj->{__save_tags};
476    MT::Tag->clear_cache(datasource => $obj->datasource,
477        ($obj->blog_id ? (blog_id => $obj->blog_id) : ())) if @et;
478
479    require MT::Memcached;
480    MT::Memcached->instance->delete( $obj->tag_cache_key );
481}
482
483sub has_tag {
484    my $obj = shift;
485    my ($tag) = @_;
486    # this should also check normalized versions
487    $tag = $tag->name if ref $tag;
488    my $n8d_tag = MT::Tag->normalize($tag);
489    my @tags = $obj->tags;
490    foreach (@tags) {
491        return 1 if $tag eq $_;
492        return 1 if ($tag ne $n8d_tag) && ($n8d_tag eq MT::Tag->normalize($_));
493    }
494    0;
495}
496
497# counts number of tags
498sub tag_count {
499    my $obj = shift;
500    my ($terms) = @_;
501    my $pkg = ref $obj ? ref $obj : $obj;
502    $terms ||= {};
503    my $jterms = {};
504    if (ref $obj) {
505        $terms->{object_id} = $obj->id if $obj->id;
506        $jterms->{blog_id} = $obj->blog_id if $obj->column('blog_id');
507    }
508    if ($terms->{blog_id}) {
509        $jterms->{blog_id} = $terms->{blog_id};
510        delete $terms->{blog_id};
511    }
512    $jterms->{object_datasource} = $obj->datasource;
513    my $pkg_terms = {};
514    $pkg_terms->{id} = \'=objecttag_object_id';
515    if ( $pkg->class_type eq 'entry' or $pkg->class_type eq 'page' ) {
516        $pkg_terms->{class} = $pkg->class_type;
517    }
518    require MT::ObjectTag;
519    MT::Tag->count(
520        undef,
521        {
522            join => MT::ObjectTag->join_on(
523                'tag_id', $jterms,
524                { unique => 1, join => $pkg->join_on( undef, $pkg_terms ) }
525            )
526        }
527    );
528}
529
530# counts number of objects tagged with a given tag
531sub tagged_count {
532    my $obj = shift;
533    my ($tag_id, $terms) = @_;
534    $terms ||= {};
535    my $jterms = {};
536    my $pkg = ref $obj ? ref $obj : $obj;
537    if (defined $tag_id && ($tag_id =~ m/\D/)) {
538        my $n8d_tag = MT::Tag->normalize($tag_id);
539        my $tag = MT::Tag->load({ name => [ $tag_id, $n8d_tag ] },
540            { binary => { name => 1 } });
541        return 0 unless $tag;
542        $tag_id = $tag->id;
543    }
544    if (ref $obj) {
545        $terms->{object_id} = $obj->id if $obj->id;
546        $jterms->{blog_id} = $obj->blog_id if $obj->column('blog_id');
547    } else {
548        $jterms->{blog_id} = $terms->{blog_id} if $terms->{blog_id};
549    }
550    $jterms->{object_datasource} = $pkg->datasource;
551    $jterms->{tag_id} = $tag_id if $tag_id;
552    my $args = { join => ['MT::ObjectTag', 'object_id', $jterms, { unique => 1 }] };
553    require MT::ObjectTag;
554    $pkg->count($terms, $args);
555}
556
5571;
558__END__
559
560=head1 NAME
561
562MT::Tag - Movable Type tag record and methods
563
564=head1 SYNOPSIS
565
566    use MT::Tag;
567    my $tag = MT::Tag->new;
568    $tag->name('favorite');
569    $tag->save
570        or die $tag->errstr;
571
572=head1 DATA ACCESS METHODS
573
574The I<MT::Tag> object holds the following pieces of data. These fields
575can be accessed and set using the standard data access methods described in
576the L<MT::Object> documentation.
577
578=over 4
579
580=item * id
581
582The numeric ID of the tag.
583
584=item * name
585
586The name of the tag.
587
588=item * n8d_id
589
590The ID of a "normalized" version of this tag. If undef or 0, it would
591signifiy this tag is a normalized tag name.
592
593=back
594
595=head1 DATA LOOKUP
596
597In addition to numeric ID lookup, you can look up or sort records by any
598combination of the following fields. See the I<load> documentation in
599I<MT::Object> for more information.
600
601=over 4
602
603=item * name
604
605=back
606
607=head1 OTHER METHODS
608
609=head2 cache
610
611Return the entry tags. If there are no cached tags, they are loaded
612first. If the tags have already been loaded with this method, that
613data is returned instead.
614
615=head2 cache_obj
616
617Cache the session tags.
618
619=head2 clear_cache
620
621Remove the tag cache.
622
623=head2 join($seperator, @tags)
624
625Return the given I<tags> as a string with the defined I<seperator>.
626
627=head2 split($seperator, $tags)
628
629Split-up the given I<tags> string by the given I<seperator>.
630
631=head2 normalize($tag)
632
633Sanitize the text (remove potentially characters) and lower-case the
634I<tag>. The I<tag> may be given as a string or as a tag object. In the
635case of the latter, C<$tag-E<gt>name> attribute is used.
636
637=head2 load_by_datasource($datasource, $terms, $args)
638
639Return a list of tags given by an object I<datasource> type, selection
640I<terms> and I<arguments>.
641
642=head2 $tag->save()
643
644Save the literal as well as a normalized copy if one does not exist.
645
646=head2 $tag->remove()
647
648Remove the tag and all its children unless it is referenced by another
649entry.
650
651=head1 NOTES
652
653=over 4
654
655=item *
656
657When you remove a tag using I<MT::Tag::remove>, in addition to
658removing the tag record, all of the related I<MT::ObjectTag> records are
659removed as well.
660
661=back
662
663=head1 AUTHOR & COPYRIGHT
664
665Please see L<MT/AUTHOR & COPYRIGHT>.
666
667=cut
Note: See TracBrowser for help on using the browser.