root/branches/release-38/lib/MT/Entry.pm @ 2275

Revision 2275, 30.0 kB (checked in by bchoate, 19 months ago)

Changed basename index to be blog_id+basename since that is how we search for existing basenames.

  • 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();
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        my $iter = $class->load_iter({ object_id => $entry->id, object_ds => $entry->class_type });
744        while (my $o = $iter->()) {
745            $o->remove;
746        }
747    }
748
749
750    $entry->SUPER::remove(@_);
751}
752
753sub blog {
754    my ($entry) = @_;
755    $entry->cache_property('blog', sub {
756        my $blog_id = $entry->blog_id;
757        require MT::Blog;
758        MT::Blog->load($blog_id) or
759            $entry->error(MT->translate(
760            "Load of blog '[_1]' failed: [_2]", $blog_id, MT::Blog->errstr));
761    });
762}
763
764sub to_hash {
765    my $entry = shift;
766    my $hash = $entry->SUPER::to_hash(@_);
767
768    $hash->{'entry.text_html'} = sub { MT->apply_text_filters($entry->text, $entry->text_filters) };
769    $hash->{'entry.text_more_html'} = sub { MT->apply_text_filters($entry->text_more, $entry->text_filters) };
770    $hash->{'entry.permalink'} = $entry->permalink;
771    $hash->{'entry.status_text'} = $entry->status_text;
772    $hash->{'entry.status_is_' . $entry->status} = 1;
773    $hash->{'entry.created_on_iso'} = sub { MT::Util::ts2iso($entry->blog_id, $entry->created_on) };
774    $hash->{'entry.modified_on_iso'} = sub { MT::Util::ts2iso($entry->blog_id, $entry->modified_on) };
775    $hash->{'entry.authored_on_iso'} = sub { MT::Util::ts2iso($entry->blog_id, $entry->authored_on) };
776
777    # Populate author info
778    my $auth = $entry->author or return $hash;
779    my $auth_hash = $auth->to_hash;
780    $hash->{"entry.$_"} = $auth_hash->{$_} foreach keys %$auth_hash;
781
782    $hash;
783}
784
785#trans('Draft')
786#trans('Review')
787#trans('Future')
788
7891;
790__END__
791
792=head1 NAME
793
794MT::Entry - Movable Type entry record
795
796=head1 SYNOPSIS
797
798    use MT::Entry;
799    my $entry = MT::Entry->new;
800    $entry->blog_id($blog->id);
801    $entry->status(MT::Entry::RELEASE());
802    $entry->author_id($author->id);
803    $entry->title('My title');
804    $entry->text('Some text');
805    $entry->save
806        or die $entry->errstr;
807
808=head1 DESCRIPTION
809
810An I<MT::Entry> object represents an entry in the Movable Type system. It
811contains all of the metadata about the entry (author, status, category, etc.),
812as well as the actual body (and extended body) of the entry.
813
814=head1 USAGE
815
816As a subclass of I<MT::Object>, I<MT::Entry> inherits all of the
817data-management and -storage methods from that class; thus you should look
818at the I<MT::Object> documentation for details about creating a new object,
819loading an existing object, saving an object, etc.
820
821The following methods are unique to the I<MT::Entry> interface:
822
823=head2 $entry->next
824
825Loads and returns the next entry, where "next" is defined as the next record
826in ascending chronological order (the entry posted after the current entry).
827entry I<$entry>).
828
829Returns an I<MT::Entry> object representing this next entry; if there is not
830a next entry, returns C<undef>.
831
832Caches the return value internally so that subsequent calls will not have to
833re-query the database.
834
835=head2 $entry->previous
836
837Loads and returns the previous entry, where "previous" is defined as the
838previous record in ascending chronological order (the entry posted before the
839current entry I<$entry>).
840
841Returns an I<MT::Entry> object representing this previous entry; if there is
842not a next entry, returns C<undef>.
843
844Caches the return value internally so that subsequent calls will not have to
845re-query the database.
846
847=head2 $entry->author
848
849Returns an I<MT::Author> object representing the author of the entry
850I<$entry>. If the author record has been removed, returns C<undef>.
851
852Caches the return value internally so that subsequent calls will not have to
853re-query the database.
854
855=head2 $entry->category
856
857Returns an I<MT::Category> object representing the primary category of the
858entry I<$entry>. If a primary category has not been assigned, returns
859C<undef>.
860
861Caches the return value internally so that subsequent calls will not have to
862re-query the database.
863
864=head2 $entry->categories
865
866Returns a reference to an array of I<MT::Category> objects representing the
867categories to which the entry I<$entry> has been assigned (both primary and
868secondary categories). If the entry has not been assigned to any categories,
869returns a reference to an empty array.
870
871Caches the return value internally so that subsequent calls will not have to
872re-query the database.
873
874=head2 $entry->is_in_category($cat)
875
876Returns true if the entry I<$entry> has been assigned to entry I<$cat>, false
877otherwise.
878
879=head2 $entry->comments
880
881Returns a reference to an array of I<MT::Comment> objects representing the
882comments made on the entry I<$entry>. If no comments have been made on the
883entry, returns a reference to an empty array.
884
885Caches the return value internally so that subsequent calls will not have to
886re-query the database.
887
888=head2 $entry->archive_file([ $archive_type ])
889
890Returns the name of/path to the archive file for the entry I<$entry>. If
891I<$archive_type> is not specified, and you are using multiple archive types
892for your blog, the path is created from the preferred archive type that you
893have selected. If I<$archive_type> is specified, it should be one of the
894following values: C<Individual>, C<Daily>, C<Weekly>, C<Monthly>, and
895C<Category>.
896
897=head2 $entry->archive_url([ $archive_type ])
898
899Returns the absolute URL to the archive page for the entry I<$entry>. This
900calls I<archive_file> internally, so if I<$archive_type> is specified, it
901is merely passed through to that method. In other words, this is the
902blog Archive URL plus the results of I<archive_file>.
903
904=head2 $entry->permalink([ $archive_type ])
905
906Returns the (smart) permalink for the entry I<$entry>. Internally this calls
907I<archive_url>, which calls I<archive_file>, so I<$archive_type> (if
908specified) is merely passed through to that method. The result of this
909method is the same as I<archive_url> plus the URI fragment
910(C<#entry_id>), unless the preferred archive type is Individual, in which
911case the two methods give exactly the same results.
912
913=head2 $entry->text_filters
914
915Returns a reference to an array of text filter keynames (the short names
916that are the first argument to I<MT::add_text_filter>. This list can be
917passed directly in as the second argument to I<MT::apply_text_filters>.
918
919=head1 DATA ACCESS METHODS
920
921The I<MT::Entry> object holds the following pieces of data. These fields can
922be accessed and set using the standard data access methods described in the
923I<MT::Object> documentation.
924
925=over 4
926
927=item * id
928
929The numeric ID of the entry.
930
931=item * blog_id
932
933The numeric ID of the blog in which this entry has been posted.
934
935=item * author_id
936
937The numeric ID of the author who posted this entry.
938
939=item * status
940
941The status of the entry, either Publish (C<2>) or Draft (C<1>).
942
943=item * allow_comments
944
945An integer flag specifying whether comments are allowed on this entry. This
946setting determines whether C<E<lt>MTEntryIfAllowCommentsE<gt>> containers are
947displayed for this entry. Possible values are 0 for no comments, 1 for open
948comments and 2 for closed comments (that is, display the comments on this
949entry but do not allow new comments to be added).
950
951=item * convert_breaks
952
953A boolean flag specifying whether line and paragraph breaks should be converted
954when rebuilding this entry.
955
956=item * title
957
958The title of the entry.
959
960=item * excerpt
961
962The excerpt of the entry.
963
964=item * text
965
966The main body text of the entry.
967
968=item * text_more
969
970The extended body text of the entry.
971
972=item * created_on
973
974The timestamp denoting when the entry record was created, in the format
975C<YYYYMMDDHHMMSS>. Note that the timestamp has already been adjusted for the
976selected timezone.
977
978=item * modified_on
979
980The timestamp denoting when the entry record was last modified, in the
981format C<YYYYMMDDHHMMSS>. Note that the timestamp has already been adjusted
982for the selected timezone.
983
984=back
985
986=head1 DATA LOOKUP
987
988In addition to numeric ID lookup, you can look up or sort records by any
989combination of the following fields. See the I<load> documentation in
990I<MT::Object> for more information.
991
992=over 4
993
994=item * blog_id
995
996=item * status
997
998=item * author_id
999
1000=item * created_on
1001
1002=item * modified_on
1003
1004=back
1005
1006=head1 NOTES
1007
1008=over 4
1009
1010=item *
1011
1012When you remove an entry using I<MT::Entry::remove>, in addition to removing
1013the entry record, all of the comments and placements (I<MT::Comment> and
1014I<MT::Placement> records, respectively) for this entry will also be removed.
1015
1016=back
1017
1018=head1 AUTHOR & COPYRIGHTS
1019
1020Please see the I<MT> manpage for author, copyright, and license information.
1021
1022=cut
Note: See TracBrowser for help on using the browser.