| 1 | # TagSupplementals - Supplemental features for MT 3.3 tags. |
|---|
| 2 | # |
|---|
| 3 | # $Id$ |
|---|
| 4 | # This software is provided as-is. You may use it for commercial or |
|---|
| 5 | # personal use. If you distribute it, please keep this notice intact. |
|---|
| 6 | # |
|---|
| 7 | # Copyright (c) 2006-2008 Hirotaka Ogawa |
|---|
| 8 | |
|---|
| 9 | package MT::Plugin::TagSupplementals; |
|---|
| 10 | use strict; |
|---|
| 11 | use warnings; |
|---|
| 12 | |
|---|
| 13 | use MT 4; |
|---|
| 14 | use base qw(MT::Plugin); |
|---|
| 15 | |
|---|
| 16 | our $VERSION = '0.10'; |
|---|
| 17 | our $HAVE_MT_XSEARCH = 0; |
|---|
| 18 | { |
|---|
| 19 | eval { require MT::XSearch; $HAVE_MT_XSEARCH = 1 }; |
|---|
| 20 | if ($HAVE_MT_XSEARCH) { |
|---|
| 21 | MT::XSearch->add_search_plugin('TagSupplementals', { |
|---|
| 22 | label => 'Tag Search', |
|---|
| 23 | description => 'Tag Search plugin for MT-XSearch', |
|---|
| 24 | on_execute => \&xsearch_on_execute, |
|---|
| 25 | on_stash => \&xsearch_on_stash, |
|---|
| 26 | }); |
|---|
| 27 | } |
|---|
| 28 | } |
|---|
| 29 | |
|---|
| 30 | my $plugin = __PACKAGE__->new({ |
|---|
| 31 | name => 'TagSupplementals', |
|---|
| 32 | description => 'A plugin for providing supplemental "tag" features for MT4', |
|---|
| 33 | doc_link => 'http://code.as-is.net/public/wiki/TagSupplementals_Plugin', |
|---|
| 34 | author_name => 'Hirotaka Ogawa', |
|---|
| 35 | author_link => 'http://profile.typekey.com/ogawa/', |
|---|
| 36 | version => $VERSION, |
|---|
| 37 | registry => { |
|---|
| 38 | tags => { |
|---|
| 39 | block => { |
|---|
| 40 | RelatedEntries => \&related_entries, |
|---|
| 41 | RelatedTags => \&related_tags, |
|---|
| 42 | ArchiveTags => \&archive_tags, |
|---|
| 43 | SearchTags => \&search_tags, |
|---|
| 44 | $HAVE_MT_XSEARCH ? (XSearchTags => \&xsearch_tags) : (), |
|---|
| 45 | }, |
|---|
| 46 | function => { |
|---|
| 47 | EntryTagsCount => \&entry_tags_count, |
|---|
| 48 | TagLastUpdated => \&tag_last_updated, |
|---|
| 49 | $HAVE_MT_XSEARCH ? (TagXSearchLink => \&tag_xsearch_link) : (), |
|---|
| 50 | }, |
|---|
| 51 | modifier => { |
|---|
| 52 | encode_urlplus => \&encode_urlplus, |
|---|
| 53 | }, |
|---|
| 54 | }, |
|---|
| 55 | } |
|---|
| 56 | }); |
|---|
| 57 | MT->add_plugin($plugin); |
|---|
| 58 | |
|---|
| 59 | use MT::Template::Context; |
|---|
| 60 | use MT::Entry; |
|---|
| 61 | use MT::Tag; |
|---|
| 62 | use MT::ObjectTag; |
|---|
| 63 | use MT::Promise qw(force); |
|---|
| 64 | |
|---|
| 65 | sub entry_tags_count { |
|---|
| 66 | my $ctx = shift; |
|---|
| 67 | my $entry = $ctx->stash('entry') |
|---|
| 68 | or return $ctx->_no_entry_error('MT' . $ctx->stash('tag')); |
|---|
| 69 | my @tags = $entry->get_tags; |
|---|
| 70 | scalar @tags; |
|---|
| 71 | } |
|---|
| 72 | |
|---|
| 73 | sub tag_last_updated { |
|---|
| 74 | my ($ctx, $args) = @_; |
|---|
| 75 | my $tag = $ctx->stash('Tag') or return ''; |
|---|
| 76 | my (%blog_terms, %blog_args); |
|---|
| 77 | $ctx->set_blog_load_context($args, \%blog_terms, \%blog_args) |
|---|
| 78 | or return $ctx->error($ctx->errstr); |
|---|
| 79 | |
|---|
| 80 | my ($e) = MT::Entry->load(undef, { |
|---|
| 81 | sort => 'created_on', |
|---|
| 82 | direction => 'descend', |
|---|
| 83 | limit => 1, |
|---|
| 84 | join => [ 'MT::ObjectTag', 'object_id', { |
|---|
| 85 | %blog_terms, |
|---|
| 86 | tag_id => $tag->id, |
|---|
| 87 | object_datasource => MT::Entry->datasource, |
|---|
| 88 | }, { |
|---|
| 89 | %blog_args, |
|---|
| 90 | unique => 1, |
|---|
| 91 | } ] }) |
|---|
| 92 | or return ''; |
|---|
| 93 | |
|---|
| 94 | $args->{ts} = $e->created_on; |
|---|
| 95 | MT::Template::Context::_hdlr_date($ctx, $args); |
|---|
| 96 | } |
|---|
| 97 | |
|---|
| 98 | #sub _object_tags { |
|---|
| 99 | # my ($blog_id, $tag_id) = @_; |
|---|
| 100 | # my $r = MT::Request->instance; |
|---|
| 101 | # my $otag_cache = $r->stash('object_tags_cache:' . $blog_id) || {}; |
|---|
| 102 | # if (!$otag_cache->{$tag_id}) { |
|---|
| 103 | # my @otags = MT::ObjectTag->load({ |
|---|
| 104 | # blog_id => $blog_id, |
|---|
| 105 | # tag_id => $tag_id, |
|---|
| 106 | # object_datasource => MT::Entry->datasource, |
|---|
| 107 | # }); |
|---|
| 108 | # $otag_cache->{$tag_id} = \@otags; |
|---|
| 109 | # $r->stash('object_tags_cache:' . $blog_id, $otag_cache); |
|---|
| 110 | # } |
|---|
| 111 | # $otag_cache->{$tag_id}; |
|---|
| 112 | #} |
|---|
| 113 | |
|---|
| 114 | sub related_entries { |
|---|
| 115 | my ($ctx, $args, $cond) = @_; |
|---|
| 116 | my $entry = $ctx->stash('entry') |
|---|
| 117 | or return $ctx->_no_entry_error('MT' . $ctx->stash('tag')); |
|---|
| 118 | |
|---|
| 119 | my $weight = $args->{weight} || 'constant'; |
|---|
| 120 | my $lastn = $args->{lastn} || 0; |
|---|
| 121 | my $offset = $args->{offset} || 0; |
|---|
| 122 | $lastn += $offset; |
|---|
| 123 | |
|---|
| 124 | my $entry_id = $entry->id; |
|---|
| 125 | my (%blog_terms, %blog_args); |
|---|
| 126 | $ctx->set_blog_load_context($args, \%blog_terms, \%blog_args) |
|---|
| 127 | or return $ctx->error($ctx->errstr); |
|---|
| 128 | |
|---|
| 129 | my @tags = MT::Tag->load(undef, { |
|---|
| 130 | sort => 'name', |
|---|
| 131 | join => [ 'MT::ObjectTag', 'tag_id', { |
|---|
| 132 | %blog_terms, |
|---|
| 133 | object_id => $entry_id, |
|---|
| 134 | object_datasource => MT::Entry->datasource, |
|---|
| 135 | }, { |
|---|
| 136 | %blog_args, |
|---|
| 137 | unique => 1, |
|---|
| 138 | } ] }) |
|---|
| 139 | or return ''; |
|---|
| 140 | my %tag_ids; |
|---|
| 141 | foreach (@tags) { |
|---|
| 142 | $tag_ids{$_->id} = 1; |
|---|
| 143 | my @more = MT::Tag->load({ n8d_id => $_->n8d_id ? $_->n8d_id : $_->id }); |
|---|
| 144 | $tag_ids{$_->id} = 1 foreach @more; |
|---|
| 145 | } |
|---|
| 146 | my @tag_ids = keys %tag_ids; |
|---|
| 147 | |
|---|
| 148 | my %rank; |
|---|
| 149 | if ($weight eq 'constant') { |
|---|
| 150 | if (MT::Object->driver->can('count_group_by')) { |
|---|
| 151 | my $iter = MT::ObjectTag->count_group_by({ |
|---|
| 152 | %blog_terms, |
|---|
| 153 | tag_id => \@tag_ids, |
|---|
| 154 | object_datasource => MT::Entry->datasource, |
|---|
| 155 | }, { |
|---|
| 156 | %blog_args, |
|---|
| 157 | group => ['object_id'], |
|---|
| 158 | }); |
|---|
| 159 | while (my ($count, $object_id) = $iter->()) { |
|---|
| 160 | $rank{$object_id} = $count; |
|---|
| 161 | } |
|---|
| 162 | } else { |
|---|
| 163 | my $iter = MT::ObjectTag->load_iter({ |
|---|
| 164 | %blog_terms, |
|---|
| 165 | tag_id => \@tag_ids, |
|---|
| 166 | object_datasource => MT::Entry->datasource, |
|---|
| 167 | }, { |
|---|
| 168 | %blog_args, |
|---|
| 169 | }); |
|---|
| 170 | while (my $otag = $iter->()) { |
|---|
| 171 | $rank{$otag->object_id}++; |
|---|
| 172 | } |
|---|
| 173 | } |
|---|
| 174 | } elsif ($weight eq 'idf') { |
|---|
| 175 | for my $tag_id (@tag_ids) { |
|---|
| 176 | my @otags = MT::ObjectTag->load({ |
|---|
| 177 | %blog_terms, |
|---|
| 178 | tag_id => $tag_id, |
|---|
| 179 | object_datasource => MT::Entry->datasource, |
|---|
| 180 | }, { |
|---|
| 181 | %blog_args, |
|---|
| 182 | }); |
|---|
| 183 | next if scalar @otags == 1; |
|---|
| 184 | my $rank = 1 / (scalar @otags - 1); |
|---|
| 185 | for my $otag (@otags) { |
|---|
| 186 | $rank{$otag->object_id} += $rank; |
|---|
| 187 | } |
|---|
| 188 | } |
|---|
| 189 | } |
|---|
| 190 | delete $rank{$entry_id}; |
|---|
| 191 | |
|---|
| 192 | # sort by entry_id, and then sort by rank |
|---|
| 193 | my @eids = sort { $b <=> $a } keys %rank; |
|---|
| 194 | @eids = sort { $rank{$b} <=> $rank{$a} } @eids; |
|---|
| 195 | |
|---|
| 196 | my @entries; |
|---|
| 197 | my $i = 0; |
|---|
| 198 | foreach (@eids) { |
|---|
| 199 | my $e = MT::Entry->load($_); |
|---|
| 200 | if ($e->status == MT::Entry::RELEASE()) { |
|---|
| 201 | next if $i < $offset; |
|---|
| 202 | push @entries, $e; |
|---|
| 203 | $i++; |
|---|
| 204 | last if $lastn && $i >= $lastn; |
|---|
| 205 | } |
|---|
| 206 | } |
|---|
| 207 | |
|---|
| 208 | my $res = ''; |
|---|
| 209 | my $tokens = $ctx->stash('tokens'); |
|---|
| 210 | my $builder = $ctx->stash('builder'); |
|---|
| 211 | $i = 0; |
|---|
| 212 | for my $e (@entries) { |
|---|
| 213 | local $ctx->{__stash}{entry} = $e; |
|---|
| 214 | local $ctx->{current_timestamp} = $e->created_on; |
|---|
| 215 | local $ctx->{modification_timestamp} = $e->modified_on; |
|---|
| 216 | my $out = $builder->build($ctx, $tokens, { |
|---|
| 217 | %$cond, |
|---|
| 218 | EntriesHeader => !$i, |
|---|
| 219 | EntriesFooter => !defined $entries[$i+1], |
|---|
| 220 | }); |
|---|
| 221 | return $ctx->error($ctx->errstr) unless defined $out; |
|---|
| 222 | $res .= $out; |
|---|
| 223 | $i++; |
|---|
| 224 | } |
|---|
| 225 | $res; |
|---|
| 226 | } |
|---|
| 227 | |
|---|
| 228 | sub related_tags { |
|---|
| 229 | my ($ctx, $args, $cond) = @_; |
|---|
| 230 | my $tag = $ctx->stash('Tag') or return ''; |
|---|
| 231 | my (%blog_terms, %blog_args); |
|---|
| 232 | $ctx->set_blog_load_context($args, \%blog_terms, \%blog_args) |
|---|
| 233 | or return $ctx->error($ctx->errstr); |
|---|
| 234 | |
|---|
| 235 | my @otags = MT::ObjectTag->load({ |
|---|
| 236 | %blog_terms, |
|---|
| 237 | tag_id => $tag->id, |
|---|
| 238 | object_datasource => MT::Entry->datasource, |
|---|
| 239 | }, { |
|---|
| 240 | %blog_args, |
|---|
| 241 | }); |
|---|
| 242 | my @eids = map { $_->object_id } @otags; |
|---|
| 243 | |
|---|
| 244 | my $iter = MT::Tag->load_iter(undef, { |
|---|
| 245 | sort => 'name', |
|---|
| 246 | join => ['MT::ObjectTag', 'tag_id', { |
|---|
| 247 | %blog_terms, |
|---|
| 248 | object_id => \@eids, |
|---|
| 249 | object_datasource => MT::Entry->datasource, |
|---|
| 250 | }, { |
|---|
| 251 | %blog_args, |
|---|
| 252 | unique => 1, |
|---|
| 253 | } ] }); |
|---|
| 254 | |
|---|
| 255 | my @res; |
|---|
| 256 | my $builder = $ctx->stash('builder'); |
|---|
| 257 | my $tokens = $ctx->stash('tokens'); |
|---|
| 258 | while (my $t = $iter->()) { |
|---|
| 259 | next if $t->is_private || ($t->id == $tag->id); |
|---|
| 260 | local $ctx->{__stash}{Tag} = $t; |
|---|
| 261 | local $ctx->{__stash}{tag_count} = undef; |
|---|
| 262 | local $ctx->{__stash}{tag_entry_count} = undef; |
|---|
| 263 | defined(my $out = $builder->build($ctx, $tokens)) |
|---|
| 264 | or return $ctx->error($ctx->errstr); |
|---|
| 265 | push @res, $out; |
|---|
| 266 | } |
|---|
| 267 | my $glue = $args->{glue} || ''; |
|---|
| 268 | join $glue, @res; |
|---|
| 269 | } |
|---|
| 270 | |
|---|
| 271 | sub archive_tags { |
|---|
| 272 | my ($ctx, $args, $cond) = @_; |
|---|
| 273 | my $entries = force($ctx->stash('entries')) or return ''; |
|---|
| 274 | my (%blog_terms, %blog_args); |
|---|
| 275 | $ctx->set_blog_load_context($args, \%blog_terms, \%blog_args) |
|---|
| 276 | or return $ctx->error($ctx->errstr); |
|---|
| 277 | |
|---|
| 278 | my @eids = map { $_->id } grep { $_->status == MT::Entry::RELEASE() } @$entries; |
|---|
| 279 | |
|---|
| 280 | my $iter = MT::Tag->load_iter(undef, { |
|---|
| 281 | sort => 'name', |
|---|
| 282 | join => ['MT::ObjectTag', 'tag_id', { |
|---|
| 283 | %blog_terms, |
|---|
| 284 | object_id => \@eids, |
|---|
| 285 | object_datasource => MT::Entry->datasource, |
|---|
| 286 | }, { |
|---|
| 287 | %blog_args, |
|---|
| 288 | unique => 1, |
|---|
| 289 | } ] }); |
|---|
| 290 | |
|---|
| 291 | my @res; |
|---|
| 292 | my $builder = $ctx->stash('builder'); |
|---|
| 293 | my $tokens = $ctx->stash('tokens'); |
|---|
| 294 | while (my $t = $iter->()) { |
|---|
| 295 | next if $t->is_private; |
|---|
| 296 | local $ctx->{__stash}{Tag} = $t; |
|---|
| 297 | local $ctx->{__stash}{tag_count} = undef; |
|---|
| 298 | local $ctx->{__stash}{tag_entry_count} = undef; |
|---|
| 299 | defined(my $out = $builder->build($ctx, $tokens)) |
|---|
| 300 | or return $ctx->error($ctx->errstr); |
|---|
| 301 | push @res, $out; |
|---|
| 302 | } |
|---|
| 303 | my $glue = $args->{glue} || ''; |
|---|
| 304 | join $glue, @res; |
|---|
| 305 | } |
|---|
| 306 | |
|---|
| 307 | sub encode_urlplus { |
|---|
| 308 | my $s = $_[0]; |
|---|
| 309 | return $s unless $_[1]; |
|---|
| 310 | $s =~ s!([^ a-zA-Z0-9_.~-])!uc sprintf "%%%02x", ord($1)!eg; |
|---|
| 311 | $s =~ tr/ /+/; |
|---|
| 312 | $s; |
|---|
| 313 | } |
|---|
| 314 | |
|---|
| 315 | sub search_tags { |
|---|
| 316 | my ($ctx, $args, $cond) = @_; |
|---|
| 317 | |
|---|
| 318 | return '' unless $ctx->stash('search_string') =~ /\S/; |
|---|
| 319 | my $tags = $ctx->stash('search_string'); |
|---|
| 320 | my @tag_names = MT::Tag->split(',', $tags); |
|---|
| 321 | # my %tags = map { $_ => 1, MT::Tag->normalize($_) => 1 } @tag_names; |
|---|
| 322 | # my @tags = MT::Tag->load({ name => [ keys %tags ] }); |
|---|
| 323 | my @tags = MT::Tag->load({ name => @tag_names }); |
|---|
| 324 | return '' unless scalar @tags; |
|---|
| 325 | |
|---|
| 326 | my @res; |
|---|
| 327 | my $builder = $ctx->stash('builder'); |
|---|
| 328 | my $tokens = $ctx->stash('tokens'); |
|---|
| 329 | foreach (@tags) { |
|---|
| 330 | local $ctx->{__stash}{'Tag'} = $_; |
|---|
| 331 | local $ctx->{__stash}{tag_count} = undef; |
|---|
| 332 | defined(my $out = $builder->build($ctx, $tokens, $cond)) |
|---|
| 333 | or return $ctx->error($ctx->errstr); |
|---|
| 334 | push @res, $out; |
|---|
| 335 | } |
|---|
| 336 | my $glue = $args->{glue} || ''; |
|---|
| 337 | join $glue, @res; |
|---|
| 338 | } |
|---|
| 339 | |
|---|
| 340 | sub tag_xsearch_link { |
|---|
| 341 | my ($ctx, $args, $cond) = @_; |
|---|
| 342 | my $tag = $ctx->stash('Tag') or return ''; |
|---|
| 343 | my $delimiter = $args->{delimiter} || ''; |
|---|
| 344 | my $path = MT::Template::Context->_hdlr_cgi_path($ctx); |
|---|
| 345 | |
|---|
| 346 | $path . 'mt-xsearch.cgi' . '?blog_id=' . $ctx->stash('blog_id') . |
|---|
| 347 | '&search_key=TagSupplementals' . |
|---|
| 348 | ($delimiter ? '&delimiter=' . MT::Util::encode_url($delimiter) : '') . |
|---|
| 349 | '&search=' . MT::Util::encode_url($tag->name); |
|---|
| 350 | } |
|---|
| 351 | |
|---|
| 352 | sub xsearch_tags { |
|---|
| 353 | my ($ctx, $args, $cond) = @_; |
|---|
| 354 | |
|---|
| 355 | return '' unless defined $ctx->stash('xsearch_tags'); |
|---|
| 356 | my $tags = $ctx->stash('xsearch_tags'); |
|---|
| 357 | return '' unless scalar @$tags; |
|---|
| 358 | |
|---|
| 359 | my @res; |
|---|
| 360 | my $builder = $ctx->stash('builder'); |
|---|
| 361 | my $tokens = $ctx->stash('tokens'); |
|---|
| 362 | foreach (@$tags) { |
|---|
| 363 | local $ctx->{__stash}{'Tag'} = $_; |
|---|
| 364 | local $ctx->{__stash}{tag_count} = undef; |
|---|
| 365 | defined(my $out = $builder->build($ctx, $tokens, $cond)) |
|---|
| 366 | or return $ctx->error($ctx->errstr); |
|---|
| 367 | push @res, $out; |
|---|
| 368 | } |
|---|
| 369 | my $glue = $args->{glue} || ''; |
|---|
| 370 | join $glue, @res; |
|---|
| 371 | } |
|---|
| 372 | |
|---|
| 373 | sub xsearch_on_stash { |
|---|
| 374 | my ($ctx, $val, $self) = @_; |
|---|
| 375 | $ctx->stash('entry', $val); |
|---|
| 376 | $ctx->{current_timestamp} = $val->created_on; |
|---|
| 377 | $ctx->{modification_timestamp} = $val->modified_on; |
|---|
| 378 | $ctx->stash('xsearch_tags', $self->{xsearch_tags}); |
|---|
| 379 | } |
|---|
| 380 | |
|---|
| 381 | sub xsearch_on_execute { |
|---|
| 382 | my ($args, $self) = @_; |
|---|
| 383 | |
|---|
| 384 | my $blog_id = $args->{blog_id} or MT->error('Blog ID is required.'); |
|---|
| 385 | my $delimiter = $args->{delimiter} || ','; |
|---|
| 386 | my $sort_by = $args->{sort_by} || 'created_on'; |
|---|
| 387 | my $sort_order = $args->{sort_order} || 'descend'; |
|---|
| 388 | my $lastn = $args->{lastn} || 0; |
|---|
| 389 | |
|---|
| 390 | my $tags = $args->{search} or MT->error('Search string is required.'); |
|---|
| 391 | my @tag_names = MT::Tag->split($delimiter, $tags) |
|---|
| 392 | or return []; |
|---|
| 393 | my $tag_count = scalar @tag_names; |
|---|
| 394 | |
|---|
| 395 | my @tags = MT::Tag->load_by_datasource(MT::Entry->datasource, { |
|---|
| 396 | is_private => 0, |
|---|
| 397 | $blog_id ? (blog_id => $blog_id) : (), |
|---|
| 398 | name => \@tag_names, |
|---|
| 399 | }); |
|---|
| 400 | $self->{xsearch_tags} = \@tags; |
|---|
| 401 | my @tag_ids = map { $_->id } @tags; |
|---|
| 402 | |
|---|
| 403 | my @eids; |
|---|
| 404 | if (MT::Object->driver->can('count_group_by')) { |
|---|
| 405 | my $iter = MT::ObjectTag->count_group_by({ |
|---|
| 406 | blog_id => $blog_id, |
|---|
| 407 | tag_id => \@tag_ids, |
|---|
| 408 | object_datasource => MT::Entry->datasource, |
|---|
| 409 | }, { |
|---|
| 410 | group => ['object_id'], |
|---|
| 411 | }); |
|---|
| 412 | while (my ($count, $object_id) = $iter->()) { |
|---|
| 413 | push @eids, $object_id if $count == $tag_count; |
|---|
| 414 | } |
|---|
| 415 | } else { |
|---|
| 416 | my $iter = MT::ObjectTag->load_iter({ |
|---|
| 417 | blog_id => $blog_id, |
|---|
| 418 | tag_id => \@tag_ids, |
|---|
| 419 | object_datasource => MT::Entry->datasource, |
|---|
| 420 | }); |
|---|
| 421 | my %count; |
|---|
| 422 | while (my $otag = $iter->()) { |
|---|
| 423 | $count{$otag->object_id}++; |
|---|
| 424 | } |
|---|
| 425 | foreach (keys %count) { |
|---|
| 426 | push @eids, $_ if $count{$_} == $tag_count; |
|---|
| 427 | } |
|---|
| 428 | } |
|---|
| 429 | return [] unless scalar @eids; |
|---|
| 430 | |
|---|
| 431 | my @entries; |
|---|
| 432 | map { push @entries, MT::Entry->load($_) } @eids; |
|---|
| 433 | @entries = $sort_order eq 'descend' ? |
|---|
| 434 | sort { $b->created_on <=> $a->created_on } @entries : |
|---|
| 435 | sort { $a->created_on <=> $b->created_on } @entries; |
|---|
| 436 | splice(@entries, $lastn) if $lastn && (scalar @entries > $lastn); |
|---|
| 437 | |
|---|
| 438 | \@entries; |
|---|
| 439 | } |
|---|
| 440 | |
|---|
| 441 | 1; |
|---|