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

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

Initial work for performance logging.

  • Property svn:keywords set to Author Date 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::Entry;
8
9use strict;
10
11use MT::Tag; # Holds MT::Taggable
12use base qw( MT::Object MT::Taggable MT::Scorable );
13
14use MT::Blog;
15use MT::Author;
16use MT::Category;
17use MT::Memcached;
18use MT::Placement;
19use MT::Comment;
20use MT::TBPing;
21use MT::Util qw( archive_file_for discover_tb start_end_period extract_domain
22                 extract_domains );
23
24use constant CATEGORY_CACHE_TIME => 7 * 24 * 60 * 60;  ## 1 week
25
26__PACKAGE__->install_properties({
27    column_defs => {
28        'id' => 'integer not null auto_increment',
29        'blog_id' => 'integer not null',
30        'status' => 'smallint not null',
31        'author_id' => 'integer not null',
32        'allow_comments' => 'boolean',
33        'title' => 'string(255)',
34        'excerpt' => 'text',
35        'text' => 'text',
36        'text_more' => 'text',
37        'convert_breaks' => 'string(30)',
38        'to_ping_urls' => 'text',
39        'pinged_urls' => 'text',
40        'allow_pings' => 'boolean',
41        'keywords' => 'text',
42        'tangent_cache' => 'text',
43        'basename' => 'string(255)',
44        'atom_id' => 'string(255)',
45        'authored_on' => 'datetime',
46        'week_number' => 'integer',
47        'template_id' => 'integer',
48## Have to keep this around for use in mt-upgrade.cgi.
49        'category_id' => 'integer',
50    },
51    indexes => {
52        blog_id => 1,
53        status => 1,
54        author_id => 1,
55        created_on => 1,
56        modified_on => 1,
57        authored_on => 1,
58        week_number => 1,
59        basename => 1,
60        blog_authored => {
61            columns => ['blog_id', 'authored_on'],
62        },
63    },
64    child_of => 'MT::Blog',
65    child_classes => ['MT::Comment','MT::Placement','MT::Trackback','MT::FileInfo'],
66    audit => 1,
67    meta => 1,
68    datasource => 'entry',
69    primary_key => 'id',
70    class_type => 'entry',
71});
72
73use constant HOLD    => 1;
74use constant RELEASE => 2;
75use constant REVIEW  => 3;
76use constant FUTURE  => 4;
77
78use Exporter;
79*import = \&Exporter::import;
80use vars qw( @EXPORT_OK %EXPORT_TAGS);
81@EXPORT_OK = qw( HOLD RELEASE FUTURE );
82%EXPORT_TAGS = (constants => [ qw(HOLD RELEASE FUTURE) ]);
83
84sub class_label {
85    MT->translate("Entry");
86}
87
88sub class_label_plural {
89    MT->translate("Entries");
90}
91
92sub container_type {
93    return "category";
94}
95
96sub container_label {
97    MT->translate("Category");
98}
99
100sub cache_key {
101    my($entry_id, $key);
102    if (@_ == 3) {
103        ($entry_id, $key) = @_[1, 2];
104    } else {
105        ($entry_id, $key) = ($_[0]->id, $_[1]);
106    }
107    return sprintf "entry%s-%d", $key, $entry_id;
108}
109
110sub status_text {
111    my $s = $_[0];
112    $s == HOLD ? "Draft" :
113        $s == RELEASE ? "Publish" :
114            $s == REVIEW ? "Review" : 
115            $s == FUTURE ? "Future" : '';
116}
117
118sub status_int {
119    my $s = lc $_[0];   ## Lower-case it so that it's case-insensitive
120    $s eq 'draft' ? HOLD :
121        $s eq 'publish' ? RELEASE :
122            $s eq 'review' ? REVIEW :
123                $s eq 'future' ? FUTURE : undef;
124}
125
126sub authored_on_obj {
127    my $obj = shift;
128    return $obj->column_as_datetime('authored_on');
129}
130
131sub next {
132    my $entry = shift;
133    my($opt) = @_;
134    my $terms;
135    if (ref $opt) {
136        $terms = $opt;
137    }
138    else {
139        $terms = $opt ? { status => RELEASE } : {};
140    }
141    $entry->_nextprev('next', $terms);
142}
143
144sub previous {
145    my $entry = shift;
146    my($opt) = @_;
147    my $terms;
148    if (ref $opt) {
149        $terms = $opt;
150    }
151    else {
152        $terms = $opt ? { status => RELEASE } : {};
153    }
154    $entry->_nextprev('previous', $terms);
155}
156
157sub _nextprev {
158    my $obj = shift;
159    my $class = ref($obj);
160    my ($direction, $terms) = @_;
161    return undef unless ($direction eq 'next' || $direction eq 'previous');
162    my $next = $direction eq 'next';
163
164    $terms->{author_id} = $obj->author_id if delete $terms->{by_author};
165    if (delete $terms->{by_category}) {
166        if (my $c = $obj->category) {
167            $terms->{category_id} = $c->id;
168        }
169        else {
170            return undef;
171        }
172    }
173
174    my $label = '__' . $direction;
175    $label .= ':author='. $terms->{author_id} if exists $terms->{author_id};
176    $label .= ':category='. $terms->{category_id} if exists $terms->{category_id};
177    return $obj->{$label} if $obj->{$label};
178
179    my $args = {};
180    if (my $cat_id = delete $terms->{category_id}) {
181        my $join = MT::Placement->join_on('entry_id',
182            { category_id => $cat_id }
183        );
184        $args->{join} = $join;
185    }
186
187    return $obj->{$label} = $obj->nextprev(
188        direction => $direction,
189        terms     => { blog_id => $obj->blog_id, class => $obj->class, %$terms },
190        args      => $args,
191        by        => 'authored_on',
192    );
193}
194
195sub trackback {
196    my $entry = shift;
197    $entry->cache_property('trackback', sub {
198        require MT::Trackback;
199        if ($entry->id) {
200            return scalar MT::Trackback->load({ entry_id => $entry->id });
201        }
202    }, @_);
203}
204
205sub author {
206    my $entry = shift;
207    $entry->cache_property('author', sub {
208        return undef unless $entry->author_id;
209        my $req = MT::Request->instance();
210        my $author_cache = $req->stash('author_cache');
211        my $author = $author_cache->{$entry->author_id};
212        unless ($author) {
213            require MT::Author;
214            $author = MT::Author->load($entry->author_id);
215            $author_cache->{$entry->author_id} = $author;
216            $req->stash('author_cache', $author_cache);
217        }
218        $author;
219    });
220}
221
222sub __load_category_data {
223    my $entry = shift;
224    my $t = MT->get_timer;
225    $t->pause_partial if $t;
226    my $cache = MT::Memcached->instance;
227    my $memkey = $entry->cache_key('categories');
228    my $rows;
229    unless ($rows = $cache->get($memkey)) {
230        require MT::Placement;
231        my @maps = MT::Placement->search({ entry_id => $entry->id });
232        $rows = [ map { [ $_->category_id, $_->is_primary ] } @maps ];
233        $cache->set($memkey, $rows, CATEGORY_CACHE_TIME);
234    }
235    $t->mark('MT::Entry::__load_category_data') if $t;
236    return $rows;
237}
238
239sub flush_category_cache {
240    my($copy, $place) = @_;
241    MT::Memcached->instance->delete(
242        MT::Entry->cache_key($place->entry_id, 'categories')
243    );
244}
245
246MT::Placement->add_trigger(
247    post_save   => \&flush_category_cache,
248    post_remove => \&flush_category_cache
249);
250
251sub category {
252    my $entry = shift;
253    $entry->cache_property('category', sub {
254        my $rows = $entry->__load_category_data or return;
255        my @rows = grep { $_->[1] } @$rows or return;
256        require MT::Category;
257        return MT::Category->lookup( $rows[0] );
258    });
259}
260
261sub categories {
262    my $entry = shift;
263    $entry->cache_property('categories', sub {
264        my $rows = $entry->__load_category_data or return;
265        my $cats = MT::Category->lookup_multi([ map { $_->[0] } @$rows ]);
266        my @cats = sort { $a->label cmp $b->label } @$cats;
267        return \@cats;
268    });
269}
270
271sub is_in_category {
272    my $entry = shift;
273    my($cat) = @_;
274    my $cats = $entry->categories;
275    for my $c (@$cats) {
276        return 1 if $c->id == $cat->id;
277    }
278    0;
279}
280
281sub comments {
282    my $entry = shift;
283    my ($terms, $args) = @_;
284    require MT::Comment;
285    if ($terms || $args) {
286        $terms ||= {};
287        $terms->{entry_id} = $entry->id;
288        return [ MT::Comment->load( $terms, $args ) ];
289    } else {
290        $entry->cache_property('comments', sub {
291            [ MT::Comment->load({ entry_id => $entry->id }) ];
292        });
293    }
294}
295
296sub comment_latest {
297    my $entry = shift;
298    $entry->cache_property('comment_latest', sub {
299        require MT::Comment;
300        MT::Comment->load({
301            entry_id => $entry->id,
302            visible => 1
303        }, {
304            'sort' => 'created_on',
305            direction => 'descend',
306            limit => 1,
307        });
308    });
309}
310
311MT::Comment->add_trigger( post_save => sub {
312    my($clone, $comment) = @_;
313
314    ## Note: This flag is set in MT::Comment::visible().
315    if (my $delta = $comment->{__changed}{visibility}) {
316        my $memkey = MT::Entry->cache_key($comment->entry_id, 'commentcount');
317        my $cache = MT::Memcached->instance;
318        if ($delta > 0) {
319            $cache->incr($memkey, $delta);
320        } elsif ($delta < 0) {
321            $cache->decr($memkey, abs $delta);
322        }
323    }
324} );
325
326MT::Comment->add_trigger( post_remove => sub {
327    my($comment) = @_;
328
329    ## If this comment was published, decrement the cached count of
330    ## visible comments on the entry.
331    if ($comment->visible && !$comment->is_changed('visible')) {
332        my $memkey = MT::Entry->cache_key($comment->entry_id, 'commentcount');
333        MT::Memcached->instance->decr($memkey, 1);
334    }
335} );
336
337sub comment_count {
338    my $entry = shift;
339    $entry->cache_property('comment_count', sub {
340        my $cache = MT::Memcached->instance;
341        my $memkey = $entry->cache_key('commentcount');
342        if (defined( my $count = $cache->get($memkey) )) {
343            return $count;
344        } else { 
345            require MT::Comment;
346            my $count = MT::Comment->count({
347                entry_id => $entry->id,
348                visible => 1
349            });
350            $cache->add($memkey, $count, 7 * 24 * 60 * 60);  ## 1 week.
351            return $count;
352        }
353    });
354}
355
356sub pings {
357    my $entry = shift;
358    my ($terms, $args) = @_;
359    if ($terms || $args) {
360        $terms ||= {};
361        $terms->{entry_id} = $entry->id;
362        return [ MT::TBPing->load( $terms, $args ) ];
363    } else {
364        $entry->cache_property('pings', sub {
365            [ MT::TBPing->load({ entry_id => $entry->id }) ];
366        });
367    }
368}
369
370MT::TBPing->add_trigger( post_save => sub {
371    my($clone, $ping) = @_;
372
373    ## Note: This flag is set in MT::TBPing::visible().
374    if (my $delta = $ping->{__changed}{visibility}) {
375        my($class, $entry_id) = $ping->parent_id;
376        return unless $entry_id;
377        my $memkey = MT::Entry->cache_key($entry_id, 'pingcount');
378        my $cache = MT::Memcached->instance;
379        if ($delta > 0) {
380            $cache->incr($memkey, $delta);
381        } elsif ($delta < 0) {
382            $cache->decr($memkey, abs $delta);
383        }
384    }
385} );
386
387MT::TBPing->add_trigger( post_remove => sub {
388    my($ping) = @_;
389
390    ## If this ping was published, decrement the cached count of
391    ## visible pings on the entry.
392    if ($ping->visible && !$ping->is_changed('visible')) {
393        my($class, $entry_id) = $ping->parent_id;
394        return unless $entry_id;
395        my $memkey = MT::Entry->cache_key($entry_id, 'pingcount');
396        MT::Memcached->instance->decr($memkey, 1);
397    }
398} );
399
400sub ping_count {
401    my $entry = shift;
402    $entry->cache_property('ping_count', sub {
403        my $cache = MT::Memcached->instance;
404        my $memkey = 'entrypingcount-' . $entry->id;
405        if (defined( my $count = $cache->get($memkey) )) {
406            return $count;
407        } else { 
408            require MT::TBPing;
409            my $tb = $entry->trackback;
410            my $count =
411                $tb ? MT::TBPing->count({ tb_id => $tb->id, visible => 1 }) : 0;
412            $cache->add($memkey, $count, 7 * 24 * 60 * 60);  ## 1 week.
413            return $count;
414        }
415    });
416}
417
418sub archive_file {
419    my $entry = shift;
420    my($at) = @_;
421    my $blog = $entry->blog() || return $entry->error(MT->translate(
422                                                     "Load of blog failed: [_1]",
423                                                     MT::Blog->errstr));
424    unless ($at) {
425        $at = $blog->archive_type_preferred || $blog->archive_type;
426        return '' if !$at || $at eq 'None';
427        return '' if $at eq 'Page';
428        my %at = map { $_ => 1 } split /,/, $at;
429        # FIXME: should draw from list of registered archive types
430        for my $tat (qw( Individual Daily Weekly Author-Monthly Category-Monthly Monthly Category )) {
431            $at = $tat if $at{$tat};
432            last;
433        }
434    }
435    archive_file_for($entry, $blog, $at);
436}
437
438sub archive_url {
439    my $entry = shift;
440    my $blog = $entry->blog() || return $entry->error(MT->translate(
441                                                     "Load of blog failed: [_1]",
442                                                     MT::Blog->errstr));
443    my $url = $blog->archive_url || "";
444    $url .= '/' unless $url =~ m!/$!;
445    $url . $entry->archive_file(@_);
446}
447
448sub permalink {
449    my $entry = shift;
450    my $blog = $entry->blog() || return $entry->error(MT->translate(
451                                                     "Load of blog failed: [_1]",
452                                                     MT::Blog->errstr));
453    my $url = $entry->archive_url($_[0]);
454    my $effective_archive_type = ($_[0]
455        || $blog->archive_type_preferred
456        || $blog->archive_type);
457    $url .= '#' . ($_[1]->{valid_html} ? 'a' : '') . 
458        sprintf("%06d", $entry->id)
459        unless ($effective_archive_type eq 'Individual' 
460        || $_[1]->{no_anchor});
461    $url;
462}
463
464sub all_permalinks {
465    my $entry = shift;
466    my $blog = $entry->blog || return $entry->error(MT->translate(
467                                                    "Load of blog failed: [_1]",
468                                                    MT::Blog->errstr));
469    my @at = split /,/, $blog->archive_type;
470    return unless @at;
471    my @urls;
472    for my $at (@at) {
473        push @urls, $entry->permalink($at);
474    }
475    @urls;
476}
477
478sub text_filters {
479    my $entry = shift;
480    my $filters = $entry->convert_breaks;
481    if (!defined $filters) {
482        my $blog = $entry->blog() || return [];
483        $filters = $blog->convert_paras;
484    }
485    return [] unless $filters;
486    if ($filters eq '1') {
487        return [ '__default__' ];
488    } else {
489        return [ split /\s*,\s*/, $filters ];
490    }
491}
492
493sub get_excerpt {
494    my $entry = shift;
495    my($words) = @_;
496    return $entry->excerpt if $entry->excerpt;
497    my $excerpt = MT->apply_text_filters($entry->text, $entry->text_filters);
498    my $blog = $entry->blog() || return $entry->error(MT->translate(
499                                                     "Load of blog failed: [_1]",
500                                                     MT::Blog->errstr));
501    MT::I18N::first_n_text($excerpt, $words || $blog->words_in_excerpt || MT::I18N::const('DEFAULT_LENGTH_ENTRY_EXCERPT')) . '...';
502}
503
504sub pinged_url_list {
505    my $entry = shift;
506    my (%param) = @_;
507    my $include_failures = $param{Failures} || $param{OnlyFailures};
508    my $exclude_successes = $param{OnlyFailures};
509    my $urls = $entry->pinged_urls;
510    return [] unless $urls && $urls =~ /\S/;
511    my %urls = map { $_ => 1 } split /\r?\n/, $urls;
512    my %to_ping = map { $_ => 1 } @{ $entry->to_ping_url_list };
513    foreach (keys %to_ping) {
514        delete $urls{$_} if exists $urls{$_};
515    }
516    my @urls = keys %urls;
517    foreach (@urls) {
518        if (m/^([^ ]+) /) {
519            delete $urls{$_}; # remove ones with error messages
520            $urls{$1} = 1 if $include_failures;
521        } else {
522            delete $urls{$_} if $exclude_successes;
523        }
524    }
525    [ keys %urls ];
526}
527
528sub to_ping_url_list {
529    my $entry = shift;
530    my $urls = $entry->to_ping_urls;
531    return [] unless $urls && $urls =~ /\S/;
532    [ split /\r?\n/, $urls ];
533}
534
535# TBD: Write a test for this routine
536sub make_atom_id {
537    my $entry = shift;
538
539    my $blog = $entry->blog;
540    my ($host, $year, $path, $blog_id, $entry_id);
541    $blog_id = $blog->id;
542    $entry_id = $entry->id;
543    my $url = $blog->site_url || '';
544    return unless $url;
545    $url .= '/' unless $url =~ m!/$!;
546    if ($url && ($url =~ m!^https?://([^/:]+)(?::\d+)?(/.*)$!)) {
547        $host = $1;
548        $path = $2;
549    }
550    if ($entry->authored_on && ($entry->authored_on =~ m/^(\d{4})/)) {
551        $year = $1;
552    }
553    return unless $host && $year && $path && $blog_id && $entry_id;
554    qq{tag:$host,$year:$path/$blog_id.$entry_id};
555}
556
557sub discover_tb_from_entry {
558    my $entry = shift;
559    ## If we need to auto-discover TrackBack ping URLs, do that here.
560    my $cfg = MT->config;
561    my $blog = $entry->blog();
562    my $send_tb = $cfg->OutboundTrackbackLimit;
563    if ($send_tb ne 'off' && 
564        $blog && ($blog->autodiscover_links
565                  || $blog->internal_autodiscovery)) {
566        my @tb_domains;
567        if ($send_tb eq 'selected') {
568            @tb_domains = $cfg->OutboundTrackbackDomains;
569        } elsif ($send_tb eq 'local') {
570            my $iter = MT::Blog->load_iter();
571            while (my $b = $iter->()) {
572                next if $b->id == $blog->id;
573                push @tb_domains, extract_domain($b->site_url);
574            }
575        }
576        my $tb_domains;
577        if (@tb_domains) {
578            $tb_domains = '';
579            my %seen;
580            foreach (@tb_domains) {
581                next unless $_;
582                $_ = lc($_);
583                next if $seen{$_};
584                $tb_domains .= '|' if $tb_domains ne '';
585                $tb_domains .= quotemeta($_);
586                $seen{$_} = 1;
587            }
588            $tb_domains = '(' . $tb_domains . ')' if $tb_domains;
589        }
590        my $archive_domain;
591        ($archive_domain) = extract_domains($blog->archive_url);
592        my %to_ping = map { $_ => 1 } @{ $entry->to_ping_url_list };
593        my %pinged = map { $_ => 1 } @{ $entry->pinged_url_list(IncludeFailures => 1) };
594        my $body = $entry->text . ($entry->text_more || "");
595        $body = MT->apply_text_filters($body, $entry->text_filters);
596        while ($body =~ m!<a\s.*?\bhref\s*=\s*(["']?)([^'">]+)\1!gsi) {
597            my $url = $2;
598            my $url_domain;
599            ($url_domain) = extract_domains($url);
600            if ($url_domain =~ m/\Q$archive_domain\E$/i) {
601                next if !$blog->internal_autodiscovery;
602            } else {
603                next if !$blog->autodiscover_links;
604            }
605            next if $tb_domains && lc($url_domain) !~ m/$tb_domains$/;
606            if (my $item = discover_tb($url)) {
607                $to_ping{ $item->{ping_url} } = 1
608                    unless $pinged{$item->{ping_url}};
609            }
610        }
611        $entry->to_ping_urls(join "\n", keys %to_ping);
612    }
613}
614
615sub sync_assets {
616    my $entry = shift;
617    my $text = ($entry->text || '') . "\n" . ($entry->text_more || '');
618
619    require MT::ObjectAsset;
620    my @assets = MT::ObjectAsset->load({
621        object_id => $entry->id,
622        blog_id => $entry->blog_id,
623        object_ds => $entry->datasource
624    });
625    my %assets = map { $_->asset_id => $_->id } @assets;
626    while ($text =~ m!<form[^>]*?\smt:asset-id=["'](\d+)["'][^>]*?>(.+?)</form>!gis) {
627        my $id = $1;
628        my $innards = $2;
629
630        # is asset exists?
631        my $asset = MT->model('asset')->load({ id => $id }) or next;
632
633        # reference to an existing asset...
634        if (exists $assets{$id}) {
635            $assets{$id} = 0;
636        } else {
637            my $map = new MT::ObjectAsset;
638            $map->blog_id($entry->blog_id);
639            $map->asset_id($id);
640            $map->object_ds($entry->datasource);
641            $map->object_id($entry->id);
642            $map->save;
643            $assets{$id} = 0;
644        }
645    }
646    if (my @old_maps = grep { $assets{$_->asset_id} } @assets) {
647        my @old_ids = map { $_->id } @old_maps;
648        MT::ObjectAsset->remove( { id => \@old_ids });
649    }
650    return 1;
651}
652
653sub save {
654    my $entry = shift;
655    my $is_new = $entry->id ? 0 : 1;
656
657    ## If there's no basename specified, create a unique basename.
658    if (!defined($entry->basename) || ($entry->basename eq '')) {
659        my $name = MT::Util::make_unique_basename($entry);
660        $entry->basename($name);
661    }
662    if (!$entry->id && !$entry->authored_on) {
663        my @ts = MT::Util::offset_time_list(time, $entry->blog_id);
664        my $ts = sprintf '%04d%02d%02d%02d%02d%02d',
665            $ts[5]+1900, $ts[4]+1, @ts[3,2,1,0];
666        $entry->authored_on($ts);
667    }
668    if (my $dt = $entry->authored_on_obj) {
669        my ($yr, $w) = $dt->week;
670        $entry->week_number($yr * 100 + $w);
671    }
672
673    my $sync_assets = $entry->is_changed('text')
674        || $entry->is_changed('text_more');
675
676    unless ($entry->SUPER::save(@_)) {
677        print STDERR "error during save: " . $entry->errstr . "\n";
678        die $entry->errstr;
679    }
680
681    $entry->sync_assets() if $sync_assets;
682
683    if (!$entry->atom_id && (($entry->status || 0) != HOLD)) {
684        $entry->atom_id($entry->make_atom_id());
685        $entry->SUPER::save(@_) if $entry->atom_id;
686    }
687
688    ## If pings are allowed on this entry, create or update
689    ## the corresponding TrackBack object for this entry.
690    require MT::Trackback;
691    if ($entry->allow_pings) {
692        my $tb;
693        unless ($tb = $entry->trackback) {
694            $tb = MT::Trackback->new;
695            $tb->blog_id($entry->blog_id);
696            $tb->entry_id($entry->id);
697            $tb->category_id(0);   ## category_id can't be NULL
698        }
699        $tb->title($entry->title);
700        $tb->description($entry->get_excerpt);
701        $tb->url($entry->permalink);
702        $tb->is_disabled(0);
703        $tb->save
704            or return $entry->error($tb->errstr);
705        $entry->trackback($tb);
706    } else {
707        ## If there is a TrackBack item for this entry, but
708        ## pings are now disabled, make sure that we mark the
709        ## object as disabled.
710        if (my $tb = $entry->trackback) {
711            $tb->is_disabled(1);
712            $tb->save
713                or return $entry->error($tb->errstr);
714        }
715    }
716
717    delete $entry->{__next} if exists $entry->{__next};
718    delete $entry->{__previous} if exists $entry->{__previous};
719    delete $entry->{__cache}{category}
720        if $is_new && exists $entry->{__cache}{category};
721    1;
722}
723
724sub remove {
725    my $entry = shift;
726    if (ref $entry) {
727        $entry->remove_children({ key => 'entry_id' }) or return;
728
729        # Remove MT::ObjectAsset records
730        my $class = MT->model('objectasset');
731        my $iter = $class->load_iter({ object_id => $entry->id, object_ds => $entry->class_type });
732        while (my $o = $iter->()) {
733            $o->remove;
734        }
735    }
736
737
738    $entry->SUPER::remove(@_);
739}
740
741sub blog {
742    my ($entry) = @_;
743    $entry->cache_property('blog', sub {
744        my $blog_id = $entry->blog_id;
745        require MT::Blog;
746        MT::Blog->load($blog_id) or
747            $entry->error(MT->translate(
748            "Load of blog '[_1]' failed: [_2]", $blog_id, MT::Blog->errstr));
749    });
750}
751
752sub to_hash {
753    my $entry = shift;
754    my $hash = $entry->SUPER::to_hash(@_);
755
756    $hash->{'entry.text_html'} = sub { MT->apply_text_filters($entry->text, $entry->text_filters) };
757    $hash->{'entry.text_more_html'} = sub { MT->apply_text_filters($entry->text_more, $entry->text_filters) };
758    $hash->{'entry.permalink'} = $entry->permalink;
759    $hash->{'entry.status_text'} = $entry->status_text;
760    $hash->{'entry.status_is_' . $entry->status} = 1;
761    $hash->{'entry.created_on_iso'} = sub { MT::Util::ts2iso($entry->blog_id, $entry->created_on) };
762    $hash->{'entry.modified_on_iso'} = sub { MT::Util::ts2iso($entry->blog_id, $entry->modified_on) };
763    $hash->{'entry.authored_on_iso'} = sub { MT::Util::ts2iso($entry->blog_id, $entry->authored_on) };
764
765    # Populate author info
766    my $auth = $entry->author or return $hash;
767    my $auth_hash = $auth->to_hash;
768    $hash->{"entry.$_"} = $auth_hash->{$_} foreach keys %$auth_hash;
769
770    $hash;
771}
772
773#trans('Draft')
774#trans('Review')
775#trans('Future')
776
7771;
778__END__
779
780=head1 NAME
781
782MT::Entry - Movable Type entry record
783
784=head1 SYNOPSIS
785
786    use MT::Entry;
787    my $entry = MT::Entry->new;
788    $entry->blog_id($blog->id);
789    $entry->status(MT::Entry::RELEASE());
790    $entry->author_id($author->id);
791    $entry->title('My title');
792    $entry->text('Some text');
793    $entry->save
794        or die $entry->errstr;
795
796=head1 DESCRIPTION
797
798An I<MT::Entry> object represents an entry in the Movable Type system. It
799contains all of the metadata about the entry (author, status, category, etc.),
800as well as the actual body (and extended body) of the entry.
801
802=head1 USAGE
803
804As a subclass of I<MT::Object>, I<MT::Entry> inherits all of the
805data-management and -storage methods from that class; thus you should look
806at the I<MT::Object> documentation for details about creating a new object,
807loading an existing object, saving an object, etc.
808
809The following methods are unique to the I<MT::Entry> interface:
810
811=head2 $entry->next
812
813Loads and returns the next entry, where "next" is defined as the next record
814in ascending chronological order (the entry posted after the current entry).
815entry I<$entry>).
816
817Returns an I<MT::Entry> object representing this next entry; if there is not
818a next entry, returns C<undef>.
819
820Caches the return value internally so that subsequent calls will not have to
821re-query the database.
822
823=head2 $entry->previous
824
825Loads and returns the previous entry, where "previous" is defined as the
826previous record in ascending chronological order (the entry posted before the
827current entry I<$entry>).
828
829Returns an I<MT::Entry> object representing this previous entry; if there is
830not a next entry, returns C<undef>.
831
832Caches the return value internally so that subsequent calls will not have to
833re-query the database.
834
835=head2 $entry->author
836
837Returns an I<MT::Author> object representing the author of the entry
838I<$entry>. If the author record has been removed, returns C<undef>.
839
840Caches the return value internally so that subsequent calls will not have to
841re-query the database.
842
843=head2 $entry->category
844
845Returns an I<MT::Category> object representing the primary category of the
846entry I<$entry>. If a primary category has not been assigned, returns
847C<undef>.
848
849Caches the return value internally so that subsequent calls will not have to
850re-query the database.
851
852=head2 $entry->categories
853
854Returns a reference to an array of I<MT::Category> objects representing the
855categories to which the entry I<$entry> has been assigned (both primary and
856secondary categories). If the entry has not been assigned to any categories,
857returns a reference to an empty array.
858
859Caches the return value internally so that subsequent calls will not have to
860re-query the database.
861
862=head2 $entry->is_in_category($cat)
863
864Returns true if the entry I<$entry> has been assigned to entry I<$cat>, false
865otherwise.
866
867=head2 $entry->comments
868
869Returns a reference to an array of I<MT::Comment> objects representing the
870comments made on the entry I<$entry>. If no comments have been made on the
871entry, returns a reference to an empty array.
872
873Caches the return value internally so that subsequent calls will not have to
874re-query the database.
875
876=head2 $entry->comment_count
877
878Returns the number of comments made on this entry.
879
880Caches the return value internally so that subsequent calls will not have to
881re-query the database.
882
883=head2 $entry->ping_count
884
885Returns the number of TrackBack pings made on this entry.
886
887Caches the return value internally so that subsequent calls will not have to
888re-query the database.
889
890=head2 $entry->archive_file([ $archive_type ])
891
892Returns the name of/path to the archive file for the entry I<$entry>. If
893I<$archive_type> is not specified, and you are using multiple archive types
894for your blog, the path is created from the preferred archive type that you
895have selected. If I<$archive_type> is specified, it should be one of the
896following values: C<Individual>, C<Daily>, C<Weekly>, C<Monthly>, and
897C<Category>.
898
899=head2 $entry->archive_url([ $archive_type ])
900
901Returns the absolute URL to the archive page for the entry I<$entry>. This
902calls I<archive_file> internally, so if I<$archive_type> is specified, it
903is merely passed through to that method. In other words, this is the
904blog Archive URL plus the results of I<archive_file>.
905
906=head2 $entry->permalink([ $archive_type ])
907
908Returns the (smart) permalink for the entry I<$entry>. Internally this calls
909I<archive_url>, which calls I<archive_file>, so I<$archive_type> (if
910specified) is merely passed through to that method. The result of this
911method is the same as I<archive_url> plus the URI fragment
912(C<#entry_id>), unless the preferred archive type is Individual, in which
913case the two methods give exactly the same results.
914
915=head2 $entry->text_filters
916
917Returns a reference to an array of text filter keynames (the short names
918that are the first argument to I<MT::add_text_filter>. This list can be
919passed directly in as the second argument to I<MT::apply_text_filters>.
920
921=head1 DATA ACCESS METHODS
922
923The I<MT::Entry> object holds the following pieces of data. These fields can
924be accessed and set using the standard data access methods described in the
925I<MT::Object> documentation.
926
927=over 4
928
929=item * id
930
931The numeric ID of the entry.
932
933=item * blog_id
934
935The numeric ID of the blog in which this entry has been posted.
936
937=item * author_id
938
939The numeric ID of the author who posted this entry.
940
941=item * status
942
943The status of the entry, either Publish (C<2>) or Draft (C<1>).
944
945=item * allow_comments
946
947An integer flag specifying whether comments are allowed on this entry. This
948setting determines whether C<E<lt>MTEntryIfAllowCommentsE<gt>> containers are
949displayed for this entry. Possible values are 0 for no comments, 1 for open
950comments and 2 for closed comments (that is, display the comments on this
951entry but do not allow new comments to be added).
952
953=item * convert_breaks
954
955A boolean flag specifying whether line and paragraph breaks should be converted
956when rebuilding this entry.
957
958=item * title
959
960The title of the entry.
961
962=item * excerpt
963
964The excerpt of the entry.
965
966=item * text
967
968The main body text of the entry.
969
970=item * text_more
971
972The extended body text of the entry.
973
974=item * created_on
975
976The timestamp denoting when the entry record was created, in the format
977C<YYYYMMDDHHMMSS>. Note that the timestamp has already been adjusted for the
978selected timezone.
979
980=item * modified_on
981
982The timestamp denoting when the entry record was last modified, in the
983format C<YYYYMMDDHHMMSS>. Note that the timestamp has already been adjusted
984for the selected timezone.
985
986=back
987
988=head1 DATA LOOKUP
989
990In addition to numeric ID lookup, you can look up or sort records by any
991combination of the following fields. See the I<load> documentation in
992I<MT::Object> for more information.
993
994=over 4
995
996=item * blog_id
997
998=item * status
999
1000=item * author_id
1001
1002=item * created_on
1003
1004=item * modified_on
1005
1006=back
1007
1008=head1 NOTES
1009
1010=over 4
1011
1012=item *
1013
1014When you remove an entry using I<MT::Entry::remove>, in addition to removing
1015the entry record, all of the comments and placements (I<MT::Comment> and
1016I<MT::Placement> records, respectively) for this entry will also be removed.
1017
1018=back
1019
1020=head1 AUTHOR & COPYRIGHTS
1021
1022Please see the I<MT> manpage for author, copyright, and license information.
1023
1024=cut
Note: See TracBrowser for help on using the browser.