root/branches/release-26/lib/MT/Entry.pm @ 1174

Revision 1174, 30.1 kB (checked in by bchoate, 23 months ago)

Updated copyright year for source.

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