root/branches/release-36/lib/MT/Entry.pm @ 2043

Revision 2043, 29.9 kB (checked in by auno, 19 months ago)

Set zero for some inconsistent comment/ping count just in case. BugzID:79433

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