root/branches/release-33/lib/MT/Entry.pm @ 1731

Revision 1731, 29.6 kB (checked in by bchoate, 20 months ago)

Adding embedded column to MT::ObjectAsset. BugId:71500. Thanks, Tim!

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