root/branches/release-40/lib/MT/Entry.pm @ 2645

Revision 2645, 29.9 kB (checked in by breese, 17 months ago)

fixed bug 80329 - just a documentation bug in the API docs about the use of allow_comments in MT::Entry

  • 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        blog_basename => {
61            columns => [ 'blog_id', 'basename' ],
62        },
63        # Page listings are published in order by title
64        title => 1,
65        blog_author => {
66            columns => [ 'blog_id', 'class', 'author_id', 'authored_on' ],
67        },
68        # For optimizing weekly archives, selected by blog, class,
69        # status.
70        blog_week => {
71            columns => [ 'blog_id', 'class', 'status', 'week_number' ],
72        },
73        # For system-overview listings where we list all entries of
74        # a particular class by authored on date
75        class_authored => {
76            columns => [ 'class', 'authored_on' ],
77        },
78        # For most blog-level listings, where we list all entries
79        # in a blog with a particular class by authored on date.
80        blog_authored => {
81            columns => ['blog_id', 'class', 'authored_on'],
82        },
83        # For most publishing listings, where we list entries in a blog
84        # with a particular class, publish status (2) and authored on date
85        blog_stat_date => {
86            columns => ['blog_id', 'class', 'status', 'authored_on', 'id'],
87        },
88        # for tag count
89        tag_count => {
90            columns => ['status', 'class', 'blog_id', 'id'],
91        },
92    },
93    defaults => {
94        comment_count => 0,
95        ping_count => 0,
96    },
97    child_of => 'MT::Blog',
98    child_classes => ['MT::Comment','MT::Placement','MT::Trackback','MT::FileInfo'],
99    audit => 1,
100    meta => 1,
101    datasource => 'entry',
102    primary_key => 'id',
103    class_type => 'entry',
104});
105
106sub HOLD ()    { 1 }
107sub RELEASE () { 2 }
108sub REVIEW ()  { 3 }
109sub FUTURE ()  { 4 }
110
111use Exporter;
112*import = \&Exporter::import;
113use vars qw( @EXPORT_OK %EXPORT_TAGS);
114@EXPORT_OK = qw( HOLD RELEASE FUTURE );
115%EXPORT_TAGS = (constants => [ qw(HOLD RELEASE FUTURE) ]);
116
117sub class_label {
118    MT->translate("Entry");
119}
120
121sub class_label_plural {
122    MT->translate("Entries");
123}
124
125sub container_type {
126    return "category";
127}
128
129sub container_label {
130    MT->translate("Category");
131}
132
133sub cache_key {
134    my($entry_id, $key);
135    if (@_ == 3) {
136        ($entry_id, $key) = @_[1, 2];
137    } else {
138        ($entry_id, $key) = ($_[0]->id, $_[1]);
139    }
140    return sprintf "entry%s-%d", $key, $entry_id;
141}
142
143sub status_text {
144    my $s = $_[0];
145    $s == HOLD ? "Draft" :
146        $s == RELEASE ? "Publish" :
147            $s == REVIEW ? "Review" : 
148            $s == FUTURE ? "Future" : '';
149}
150
151sub status_int {
152    my $s = lc $_[0];   ## Lower-case it so that it's case-insensitive
153    $s eq 'draft' ? HOLD :
154        $s eq 'publish' ? RELEASE :
155            $s eq 'review' ? REVIEW :
156                $s eq 'future' ? FUTURE : undef;
157}
158
159sub authored_on_obj {
160    my $obj = shift;
161    return $obj->column_as_datetime('authored_on');
162}
163
164sub next {
165    my $entry = shift;
166    my($opt) = @_;
167    my $terms;
168    if (ref $opt) {
169        $terms = $opt;
170    }
171    else {
172        $terms = $opt ? { status => RELEASE } : {};
173    }
174    $entry->_nextprev('next', $terms);
175}
176
177sub previous {
178    my $entry = shift;
179    my($opt) = @_;
180    my $terms;
181    if (ref $opt) {
182        $terms = $opt;
183    }
184    else {
185        $terms = $opt ? { status => RELEASE } : {};
186    }
187    $entry->_nextprev('previous', $terms);
188}
189
190sub _nextprev {
191    my $obj = shift;
192    my $class = ref($obj);
193    my ($direction, $terms) = @_;
194    return undef unless ($direction eq 'next' || $direction eq 'previous');
195    my $next = $direction eq 'next';
196
197    $terms->{author_id} = $obj->author_id if delete $terms->{by_author};
198    if (delete $terms->{by_category}) {
199        if (my $c = $obj->category) {
200            $terms->{category_id} = $c->id;
201        }
202        else {
203            return undef;
204        }
205    }
206
207    my $label = '__' . $direction;
208    $label .= ':author='. $terms->{author_id} if exists $terms->{author_id};
209    $label .= ':category='. $terms->{category_id} if exists $terms->{category_id};
210    return $obj->{$label} if $obj->{$label};
211
212    my $args = {};
213    if (my $cat_id = delete $terms->{category_id}) {
214        my $join = MT::Placement->join_on('entry_id',
215            { category_id => $cat_id }
216        );
217        $args->{join} = $join;
218    }
219
220    my $o = $obj->nextprev(
221        direction => $direction,
222        terms     => { blog_id => $obj->blog_id, class => $obj->class, %$terms },
223        args      => $args,
224        by        => 'authored_on',
225    );
226    weaken($obj->{$label} = $o) if $o;
227    return $o;
228}
229
230sub trackback {
231    my $entry = shift;
232    $entry->cache_property('trackback', sub {
233        require MT::Trackback;
234        if ($entry->id) {
235            return scalar MT::Trackback->load({ entry_id => $entry->id });
236        }
237    }, @_);
238}
239
240sub author {
241    my $entry = shift;
242    $entry->cache_property('author', sub {
243        return undef unless $entry->author_id;
244        my $req = MT::Request->instance();
245        my $author_cache = $req->stash('author_cache');
246        my $author = $author_cache->{$entry->author_id};
247        unless ($author) {
248            require MT::Author;
249            $author = MT::Author->load($entry->author_id)
250                or return undef;
251            $author_cache->{$entry->author_id} = $author;
252            $req->stash('author_cache', $author_cache);
253        }
254        $author;
255    });
256}
257
258sub __load_category_data {
259    my $entry = shift;
260    my $t = MT->get_timer;
261    $t->pause_partial if $t;
262    my $cache = MT::Memcached->instance;
263    my $memkey = $entry->cache_key('categories');
264    my $rows;
265    unless ($rows = $cache->get($memkey)) {
266        require MT::Placement;
267        my @maps = MT::Placement->search({ entry_id => $entry->id });
268        $rows = [ map { [ $_->category_id, $_->is_primary ] } @maps ];
269        $cache->set($memkey, $rows, CATEGORY_CACHE_TIME);
270    }
271    $t->mark('MT::Entry::__load_category_data') if $t;
272    return $rows;
273}
274
275sub flush_category_cache {
276    my($copy, $place) = @_;
277    MT::Memcached->instance->delete(
278        MT::Entry->cache_key($place->entry_id, 'categories')
279    );
280}
281
282MT::Placement->add_trigger(
283    post_save   => \&flush_category_cache,
284    post_remove => \&flush_category_cache
285);
286
287sub category {
288    my $entry = shift;
289    $entry->cache_property('category', sub {
290        my $rows = $entry->__load_category_data or return;
291        my @rows = grep { $_->[1] } @$rows or return;
292        require MT::Category;
293        return MT::Category->lookup( $rows[0] );
294    });
295}
296
297sub categories {
298    my $entry = shift;
299    $entry->cache_property('categories', sub {
300        my $rows = $entry->__load_category_data or return;
301        my $cats = MT::Category->lookup_multi([ map { $_->[0] } @$rows ]);
302        my @cats = sort { $a->label cmp $b->label } @$cats;
303        return \@cats;
304    });
305}
306
307sub is_in_category {
308    my $entry = shift;
309    my($cat) = @_;
310    my $cats = $entry->categories;
311    for my $c (@$cats) {
312        return 1 if $c->id == $cat->id;
313    }
314    0;
315}
316
317sub comments {
318    my $entry = shift;
319    my ($terms, $args) = @_;
320    require MT::Comment;
321    if ($terms || $args) {
322        $terms ||= {};
323        $terms->{entry_id} = $entry->id;
324        return [ MT::Comment->load( $terms, $args ) ];
325    } else {
326        $entry->cache_property('comments', sub {
327            [ MT::Comment->load({ entry_id => $entry->id }) ];
328        });
329    }
330}
331
332sub comment_latest {
333    my $entry = shift;
334    $entry->cache_property('comment_latest', sub {
335        require MT::Comment;
336        MT::Comment->load({
337            entry_id => $entry->id,
338            visible => 1
339        }, {
340            'sort' => 'created_on',
341            direction => 'descend',
342            limit => 1,
343        });
344    });
345}
346
347MT::Comment->add_trigger(
348    post_save => sub {
349        my $comment = shift;
350        my $entry   = MT::Entry->load( $comment->entry_id )
351            or return;
352        my $count   = MT::Comment->count(
353            {
354                entry_id => $comment->entry_id,
355                visible  => 1,
356            }
357        );
358        $entry->comment_count($count);
359        $entry->save;
360    }
361);
362
363MT::Comment->add_trigger(
364    post_remove => sub {
365        my $comment = shift;
366        my $entry   = MT::Entry->load( $comment->entry_id )
367            or return;
368        if ( $comment->visible ) {
369            my $count = $entry->comment_count > 0 ? $entry->comment_count - 1 : 0;
370            $entry->comment_count($count);
371            $entry->save;
372        }
373    }
374);
375
376sub pings {
377    my $entry = shift;
378    my ($terms, $args) = @_;
379    my $tb = $entry->trackback;
380    return undef unless $tb;
381    if ($terms || $args) {
382        $terms ||= {};
383        $terms->{tb_id} = $tb->id;
384        return [ MT::TBPing->load( $terms, $args ) ];
385    } else {
386        $entry->cache_property('pings', sub {
387            [ MT::TBPing->load({ tb_id => $tb->id }) ];
388        });
389    }
390}
391
392MT::TBPing->add_trigger(
393    post_save => sub {
394        my $ping = shift;
395        require MT::Trackback;
396        if ( my $tb = MT::Trackback->load( $ping->tb_id ) ) {
397            if ( $tb->entry_id ) {
398                my $entry = MT::Entry->load( $tb->entry_id )
399                    or return;
400                my $count = MT::TBPing->count(
401                    {
402                        tb_id   => $tb->id,
403                        visible => 1,
404                    }
405                );
406                $entry->ping_count($count);
407                $entry->save;
408            }
409        }
410    }
411);
412
413MT::TBPing->add_trigger(
414    post_remove => sub {
415        my $ping = shift;
416        require MT::Trackback;
417        if ( my $tb = MT::Trackback->load( $ping->tb_id ) ) {
418            if ( $tb->entry_id && $ping->visible ) {
419                my $entry = MT::Entry->load( $tb->entry_id )
420                    or return;
421                my $count = $entry->ping_count > 0 ? $entry->ping_count - 1 : 0;
422                $entry->ping_count($count);
423                $entry->save;
424            }
425        }
426    }
427);
428
429sub archive_file {
430    my $entry = shift;
431    my($at) = @_;
432    my $blog = $entry->blog() || return $entry->error(MT->translate(
433                                                     "Load of blog failed: [_1]",
434                                                     MT::Blog->errstr));
435    unless ($at) {
436        $at = $blog->archive_type_preferred || $blog->archive_type;
437        return '' if !$at || $at eq 'None';
438        return '' if $at eq 'Page';
439        my %at = map { $_ => 1 } split /,/, $at;
440        # FIXME: should draw from list of registered archive types
441        for my $tat (qw( Individual Daily Weekly Author-Monthly Category-Monthly Monthly Category )) {
442            $at = $tat if $at{$tat};
443            last;
444        }
445    }
446    archive_file_for($entry, $blog, $at);
447}
448
449sub archive_url {
450    my $entry = shift;
451    my $blog = $entry->blog() || return $entry->error(MT->translate(
452                                                     "Load of blog failed: [_1]",
453                                                     MT::Blog->errstr));
454    my $url = $blog->archive_url || "";
455    $url .= '/' unless $url =~ m!/$!;
456    $url . $entry->archive_file(@_);
457}
458
459sub permalink {
460    my $entry = shift;
461    my $blog = $entry->blog() || return $entry->error(MT->translate(
462                                                     "Load of blog failed: [_1]",
463                                                     MT::Blog->errstr));
464    my $url = $entry->archive_url($_[0]);
465    my $effective_archive_type = ($_[0]
466        || $blog->archive_type_preferred
467        || $blog->archive_type);
468    $url .= '#' . ($_[1]->{valid_html} ? 'a' : '') . 
469        sprintf("%06d", $entry->id)
470        unless ($effective_archive_type eq 'Individual' 
471        || $_[1]->{no_anchor});
472    $url;
473}
474
475sub all_permalinks {
476    my $entry = shift;
477    my $blog = $entry->blog || return $entry->error(MT->translate(
478                                                    "Load of blog failed: [_1]",
479                                                    MT::Blog->errstr));
480    my @at = split /,/, $blog->archive_type;
481    return unless @at;
482    my @urls;
483    for my $at (@at) {
484        push @urls, $entry->permalink($at);
485    }
486    @urls;
487}
488
489sub text_filters {
490    my $entry = shift;
491    my $filters = $entry->convert_breaks;
492    if (!defined $filters) {
493        my $blog = $entry->blog() || return [];
494        $filters = $blog->convert_paras;
495    }
496    return [] unless $filters;
497    if ($filters eq '1') {
498        return [ '__default__' ];
499    } else {
500        return [ split /\s*,\s*/, $filters ];
501    }
502}
503
504sub get_excerpt {
505    my $entry = shift;
506    my($words) = @_;
507    return $entry->excerpt if $entry->excerpt;
508    my $excerpt = MT->apply_text_filters($entry->text, $entry->text_filters);
509    my $blog = $entry->blog() || return $entry->error(MT->translate(
510                                                     "Load of blog failed: [_1]",
511                                                     MT::Blog->errstr));
512    MT::I18N::first_n_text($excerpt, $words || $blog->words_in_excerpt || MT::I18N::const('DEFAULT_LENGTH_ENTRY_EXCERPT')) . '...';
513}
514
515sub pinged_url_list {
516    my $entry = shift;
517    my (%param) = @_;
518    my $include_failures = $param{Failures} || $param{OnlyFailures};
519    my $exclude_successes = $param{OnlyFailures};
520    my $urls = $entry->pinged_urls;
521    return [] unless $urls && $urls =~ /\S/;
522    my %urls = map { $_ => 1 } split /\r?\n/, $urls;
523    my %to_ping = map { $_ => 1 } @{ $entry->to_ping_url_list };
524    foreach (keys %to_ping) {
525        delete $urls{$_} if exists $urls{$_};
526    }
527    my @urls = keys %urls;
528    foreach (@urls) {
529        if (m/^([^ ]+) /) {
530            delete $urls{$_}; # remove ones with error messages
531            $urls{$1} = 1 if $include_failures;
532        } else {
533            delete $urls{$_} if $exclude_successes;
534        }
535    }
536    [ keys %urls ];
537}
538
539sub to_ping_url_list {
540    my $entry = shift;
541    my $urls = $entry->to_ping_urls;
542    return [] unless $urls && $urls =~ /\S/;
543    [ split /\r?\n/, $urls ];
544}
545
546# TBD: Write a test for this routine
547sub make_atom_id {
548    my $entry = shift;
549
550    my $blog = $entry->blog;
551    my ($host, $year, $path, $blog_id, $entry_id);
552    $blog_id = $blog->id;
553    $entry_id = $entry->id;
554    my $url = $blog->site_url || '';
555    return unless $url;
556    $url .= '/' unless $url =~ m!/$!;
557    if ($url && ($url =~ m!^https?://([^/:]+)(?::\d+)?(/.*)$!)) {
558        $host = $1;
559        $path = $2;
560    }
561    if ($entry->authored_on && ($entry->authored_on =~ m/^(\d{4})/)) {
562        $year = $1;
563    }
564    return unless $host && $year && $path && $blog_id && $entry_id;
565    qq{tag:$host,$year:$path/$blog_id.$entry_id};
566}
567
568sub discover_tb_from_entry {
569    my $entry = shift;
570    ## If we need to auto-discover TrackBack ping URLs, do that here.
571    my $cfg = MT->config;
572    my $blog = $entry->blog();
573    my $send_tb = $cfg->OutboundTrackbackLimit;
574    if ($send_tb ne 'off' && 
575        $blog && ($blog->autodiscover_links
576                  || $blog->internal_autodiscovery)) {
577        my @tb_domains;
578        if ($send_tb eq 'selected') {
579            @tb_domains = $cfg->OutboundTrackbackDomains;
580        } elsif ($send_tb eq 'local') {
581            my $iter = MT::Blog->load_iter(undef, { fetchonly => ['site_url'], no_triggers => 1 });
582            while (my $b = $iter->()) {
583                next if $b->id == $blog->id;
584                push @tb_domains, extract_domain($b->site_url);
585            }
586        }
587        my $tb_domains;
588        if (@tb_domains) {
589            $tb_domains = '';
590            my %seen;
591            foreach (@tb_domains) {
592                next unless $_;
593                $_ = lc($_);
594                next if $seen{$_};
595                $tb_domains .= '|' if $tb_domains ne '';
596                $tb_domains .= quotemeta($_);
597                $seen{$_} = 1;
598            }
599            $tb_domains = '(' . $tb_domains . ')' if $tb_domains;
600        }
601        my $archive_domain;
602        ($archive_domain) = extract_domains($blog->archive_url);
603        my %to_ping = map { $_ => 1 } @{ $entry->to_ping_url_list };
604        my %pinged = map { $_ => 1 } @{ $entry->pinged_url_list(IncludeFailures => 1) };
605        my $body = $entry->text . ($entry->text_more || "");
606        $body = MT->apply_text_filters($body, $entry->text_filters);
607        while ($body =~ m!<a\s.*?\bhref\s*=\s*(["']?)([^'">]+)\1!gsi) {
608            my $url = $2;
609            my $url_domain;
610            ($url_domain) = extract_domains($url);
611            if ($url_domain =~ m/\Q$archive_domain\E$/i) {
612                next if !$blog->internal_autodiscovery;
613            } else {
614                next if !$blog->autodiscover_links;
615            }
616            next if $tb_domains && lc($url_domain) !~ m/$tb_domains$/;
617            if (my $item = discover_tb($url)) {
618                $to_ping{ $item->{ping_url} } = 1
619                    unless $pinged{$item->{ping_url}};
620            }
621        }
622        $entry->to_ping_urls(join "\n", keys %to_ping);
623    }
624}
625
626sub sync_assets {
627    my $entry = shift;
628    my $text = ($entry->text || '') . "\n" . ($entry->text_more || '');
629
630    require MT::ObjectAsset;
631    my @assets = MT::ObjectAsset->load({
632        object_id => $entry->id,
633        blog_id => $entry->blog_id,
634        object_ds => $entry->datasource,
635        embedded => 1,
636    });
637    my %assets = map { $_->asset_id => $_->id } @assets;
638    while ($text =~ m!<form[^>]*?\smt:asset-id=["'](\d+)["'][^>]*?>(.+?)</form>!gis) {
639        my $id = $1;
640        my $innards = $2;
641
642        # reference to an existing asset...
643        if (exists $assets{$id}) {
644            $assets{$id} = 0;
645        } else {
646            # is asset exists?
647            my $asset = MT->model('asset')->load({ id => $id }) or next;
648
649            my $map = new MT::ObjectAsset;
650            $map->blog_id($entry->blog_id);
651            $map->asset_id($id);
652            $map->object_ds($entry->datasource);
653            $map->object_id($entry->id);
654            $map->embedded(1);
655            $map->save;
656            $assets{$id} = 0;
657        }
658    }
659    if (my @old_maps = grep { $assets{$_->asset_id} } @assets) {
660        my @old_ids = map { $_->id } grep { $_->embedded } @old_maps;
661        MT::ObjectAsset->remove( { id => \@old_ids })
662            if @old_ids;
663    }
664    return 1;
665}
666
667sub save {
668    my $entry = shift;
669    my $is_new = $entry->id ? 0 : 1;
670
671    ## If there's no basename specified, create a unique basename.
672    if (!defined($entry->basename) || ($entry->basename eq '')) {
673        my $name = MT::Util::make_unique_basename($entry);
674        $entry->basename($name);
675    }
676    if (!$entry->id && !$entry->authored_on) {
677        my @ts = MT::Util::offset_time_list(time, $entry->blog_id);
678        my $ts = sprintf '%04d%02d%02d%02d%02d%02d',
679            $ts[5]+1900, $ts[4]+1, @ts[3,2,1,0];
680        $entry->authored_on($ts);
681    }
682    if (my $dt = $entry->authored_on_obj) {
683        my ($yr, $w) = $dt->week;
684        $entry->week_number($yr * 100 + $w);
685    }
686
687    my $sync_assets = $entry->is_changed('text')
688        || $entry->is_changed('text_more');
689
690    unless ($entry->SUPER::save(@_)) {
691        print STDERR "error during save: " . $entry->errstr . "\n";
692        die $entry->errstr;
693    }
694
695    $entry->sync_assets() if $sync_assets;
696
697    if (!$entry->atom_id && (($entry->status || 0) != HOLD)) {
698        $entry->atom_id($entry->make_atom_id());
699        $entry->SUPER::save(@_) if $entry->atom_id;
700    }
701
702    ## If pings are allowed on this entry, create or update
703    ## the corresponding TrackBack object for this entry.
704    require MT::Trackback;
705    if ($entry->allow_pings) {
706        my $tb;
707        unless ($tb = $entry->trackback) {
708            $tb = MT::Trackback->new;
709            $tb->blog_id($entry->blog_id);
710            $tb->entry_id($entry->id);
711            $tb->category_id(0);   ## category_id can't be NULL
712        }
713        $tb->title($entry->title);
714        $tb->description($entry->get_excerpt);
715        $tb->url($entry->permalink);
716        $tb->is_disabled(0);
717        $tb->save
718            or return $entry->error($tb->errstr);
719        $entry->trackback($tb);
720    } else {
721        ## If there is a TrackBack item for this entry, but
722        ## pings are now disabled, make sure that we mark the
723        ## object as disabled.
724        if (my $tb = $entry->trackback) {
725            $tb->is_disabled(1);
726            $tb->save
727                or return $entry->error($tb->errstr);
728        }
729    }
730
731    $entry->clear_cache() if $is_new;
732
733    1;
734}
735
736sub remove {
737    my $entry = shift;
738    if (ref $entry) {
739        $entry->remove_children({ key => 'entry_id' }) or return;
740
741        # Remove MT::ObjectAsset records
742        my $class = MT->model('objectasset');
743        $class->remove({ object_id => $entry->id, object_ds => $entry->class_type });
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 not allowing any additional
944comments and 1 for allowing new comments to be made on the entry.
945
946=item * convert_breaks
947
948A boolean flag specifying whether line and paragraph breaks should be converted
949when rebuilding this entry.
950
951=item * title
952
953The title of the entry.
954
955=item * excerpt
956
957The excerpt of the entry.
958
959=item * text
960
961The main body text of the entry.
962
963=item * text_more
964
965The extended body text of the entry.
966
967=item * created_on
968
969The timestamp denoting when the entry record was created, in the format
970C<YYYYMMDDHHMMSS>. Note that the timestamp has already been adjusted for the
971selected timezone.
972
973=item * modified_on
974
975The timestamp denoting when the entry record was last modified, in the
976format C<YYYYMMDDHHMMSS>. Note that the timestamp has already been adjusted
977for the selected timezone.
978
979=back
980
981=head1 DATA LOOKUP
982
983In addition to numeric ID lookup, you can look up or sort records by any
984combination of the following fields. See the I<load> documentation in
985I<MT::Object> for more information.
986
987=over 4
988
989=item * blog_id
990
991=item * status
992
993=item * author_id
994
995=item * created_on
996
997=item * modified_on
998
999=back
1000
1001=head1 NOTES
1002
1003=over 4
1004
1005=item *
1006
1007When you remove an entry using I<MT::Entry::remove>, in addition to removing
1008the entry record, all of the comments and placements (I<MT::Comment> and
1009I<MT::Placement> records, respectively) for this entry will also be removed.
1010
1011=back
1012
1013=head1 AUTHOR & COPYRIGHTS
1014
1015Please see the I<MT> manpage for author, copyright, and license information.
1016
1017=cut
Note: See TracBrowser for help on using the browser.