root/branches/feature-narrow-tables/lib/MT/Entry.pm @ 1763

Revision 1763, 29.6 kB (checked in by mpaschal, 20 months ago)

Declare these meta objects as objects, until the upgrader can know them automatically
BugzID: 68749

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