root/branches/release-34/lib/MT/Entry.pm @ 1856

Revision 1856, 29.7 kB (checked in by bchoate, 20 months ago)

Removing class_author index since we don't select (system-wide) entries by author. It is also confusing the MySQL optimizer in some cases, ignoring the class+authored_on index.

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