root/branches/release-35/lib/MT/Entry.pm @ 1920

Revision 1920, 29.7 kB (checked in by auno, 20 months ago)

Need more condition to decrease the count when comments or trackbacks are deleted. BugzID:79271

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