root/branches/release-32/lib/MT/Entry.pm @ 1597

Revision 1597, 29.5 kB (checked in by takayama, 20 months ago)

Fixed bug:71136
* Added more 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        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($o->{$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    });
626    my %assets = map { $_->asset_id => $_->id } @assets;
627    while ($text =~ m!<form[^>]*?\smt:asset-id=["'](\d+)["'][^>]*?>(.+?)</form>!gis) {
628        my $id = $1;
629        my $innards = $2;
630
631        # is asset exists?
632        my $asset = MT->model('asset')->load({ id => $id }) or next;
633
634        # reference to an existing asset...
635        if (exists $assets{$id}) {
636            $assets{$id} = 0;
637        } else {
638            my $map = new MT::ObjectAsset;
639            $map->blog_id($entry->blog_id);
640            $map->asset_id($id);
641            $map->object_ds($entry->datasource);
642            $map->object_id($entry->id);
643            $map->save;
644            $assets{$id} = 0;
645        }
646    }
647    if (my @old_maps = grep { $assets{$_->asset_id} } @assets) {
648        my @old_ids = map { $_->id } @old_maps;
649        MT::ObjectAsset->remove( { id => \@old_ids });
650    }
651    return 1;
652}
653
654sub save {
655    my $entry = shift;
656    my $is_new = $entry->id ? 0 : 1;
657
658    ## If there's no basename specified, create a unique basename.
659    if (!defined($entry->basename) || ($entry->basename eq '')) {
660        my $name = MT::Util::make_unique_basename($entry);
661        $entry->basename($name);
662    }
663    if (!$entry->id && !$entry->authored_on) {
664        my @ts = MT::Util::offset_time_list(time, $entry->blog_id);
665        my $ts = sprintf '%04d%02d%02d%02d%02d%02d',
666            $ts[5]+1900, $ts[4]+1, @ts[3,2,1,0];
667        $entry->authored_on($ts);
668    }
669    if (my $dt = $entry->authored_on_obj) {
670        my ($yr, $w) = $dt->week;
671        $entry->week_number($yr * 100 + $w);
672    }
673
674    my $sync_assets = $entry->is_changed('text')
675        || $entry->is_changed('text_more');
676
677    unless ($entry->SUPER::save(@_)) {
678        print STDERR "error during save: " . $entry->errstr . "\n";
679        die $entry->errstr;
680    }
681
682    $entry->sync_assets() if $sync_assets;
683
684    if (!$entry->atom_id && (($entry->status || 0) != HOLD)) {
685        $entry->atom_id($entry->make_atom_id());
686        $entry->SUPER::save(@_) if $entry->atom_id;
687    }
688
689    ## If pings are allowed on this entry, create or update
690    ## the corresponding TrackBack object for this entry.
691    require MT::Trackback;
692    if ($entry->allow_pings) {
693        my $tb;
694        unless ($tb = $entry->trackback) {
695            $tb = MT::Trackback->new;
696            $tb->blog_id($entry->blog_id);
697            $tb->entry_id($entry->id);
698            $tb->category_id(0);   ## category_id can't be NULL
699        }
700        $tb->title($entry->title);
701        $tb->description($entry->get_excerpt);
702        $tb->url($entry->permalink);
703        $tb->is_disabled(0);
704        $tb->save
705            or return $entry->error($tb->errstr);
706        $entry->trackback($tb);
707    } else {
708        ## If there is a TrackBack item for this entry, but
709        ## pings are now disabled, make sure that we mark the
710        ## object as disabled.
711        if (my $tb = $entry->trackback) {
712            $tb->is_disabled(1);
713            $tb->save
714                or return $entry->error($tb->errstr);
715        }
716    }
717
718    $entry->clear_cache() if $is_new;
719
720    1;
721}
722
723sub remove {
724    my $entry = shift;
725    if (ref $entry) {
726        $entry->remove_children({ key => 'entry_id' }) or return;
727
728        # Remove MT::ObjectAsset records
729        my $class = MT->model('objectasset');
730        my $iter = $class->load_iter({ object_id => $entry->id, object_ds => $entry->class_type });
731        while (my $o = $iter->()) {
732            $o->remove;
733        }
734    }
735
736
737    $entry->SUPER::remove(@_);
738}
739
740sub blog {
741    my ($entry) = @_;
742    $entry->cache_property('blog', sub {
743        my $blog_id = $entry->blog_id;
744        require MT::Blog;
745        MT::Blog->load($blog_id) or
746            $entry->error(MT->translate(
747            "Load of blog '[_1]' failed: [_2]", $blog_id, MT::Blog->errstr));
748    });
749}
750
751sub to_hash {
752    my $entry = shift;
753    my $hash = $entry->SUPER::to_hash(@_);
754
755    $hash->{'entry.text_html'} = sub { MT->apply_text_filters($entry->text, $entry->text_filters) };
756    $hash->{'entry.text_more_html'} = sub { MT->apply_text_filters($entry->text_more, $entry->text_filters) };
757    $hash->{'entry.permalink'} = $entry->permalink;
758    $hash->{'entry.status_text'} = $entry->status_text;
759    $hash->{'entry.status_is_' . $entry->status} = 1;
760    $hash->{'entry.created_on_iso'} = sub { MT::Util::ts2iso($entry->blog_id, $entry->created_on) };
761    $hash->{'entry.modified_on_iso'} = sub { MT::Util::ts2iso($entry->blog_id, $entry->modified_on) };
762    $hash->{'entry.authored_on_iso'} = sub { MT::Util::ts2iso($entry->blog_id, $entry->authored_on) };
763
764    # Populate author info
765    my $auth = $entry->author or return $hash;
766    my $auth_hash = $auth->to_hash;
767    $hash->{"entry.$_"} = $auth_hash->{$_} foreach keys %$auth_hash;
768
769    $hash;
770}
771
772#trans('Draft')
773#trans('Review')
774#trans('Future')
775
7761;
777__END__
778
779=head1 NAME
780
781MT::Entry - Movable Type entry record
782
783=head1 SYNOPSIS
784
785    use MT::Entry;
786    my $entry = MT::Entry->new;
787    $entry->blog_id($blog->id);
788    $entry->status(MT::Entry::RELEASE());
789    $entry->author_id($author->id);
790    $entry->title('My title');
791    $entry->text('Some text');
792    $entry->save
793        or die $entry->errstr;
794
795=head1 DESCRIPTION
796
797An I<MT::Entry> object represents an entry in the Movable Type system. It
798contains all of the metadata about the entry (author, status, category, etc.),
799as well as the actual body (and extended body) of the entry.
800
801=head1 USAGE
802
803As a subclass of I<MT::Object>, I<MT::Entry> inherits all of the
804data-management and -storage methods from that class; thus you should look
805at the I<MT::Object> documentation for details about creating a new object,
806loading an existing object, saving an object, etc.
807
808The following methods are unique to the I<MT::Entry> interface:
809
810=head2 $entry->next
811
812Loads and returns the next entry, where "next" is defined as the next record
813in ascending chronological order (the entry posted after the current entry).
814entry I<$entry>).
815
816Returns an I<MT::Entry> object representing this next entry; if there is not
817a next entry, returns C<undef>.
818
819Caches the return value internally so that subsequent calls will not have to
820re-query the database.
821
822=head2 $entry->previous
823
824Loads and returns the previous entry, where "previous" is defined as the
825previous record in ascending chronological order (the entry posted before the
826current entry I<$entry>).
827
828Returns an I<MT::Entry> object representing this previous entry; if there is
829not a next entry, returns C<undef>.
830
831Caches the return value internally so that subsequent calls will not have to
832re-query the database.
833
834=head2 $entry->author
835
836Returns an I<MT::Author> object representing the author of the entry
837I<$entry>. If the author record has been removed, returns C<undef>.
838
839Caches the return value internally so that subsequent calls will not have to
840re-query the database.
841
842=head2 $entry->category
843
844Returns an I<MT::Category> object representing the primary category of the
845entry I<$entry>. If a primary category has not been assigned, returns
846C<undef>.
847
848Caches the return value internally so that subsequent calls will not have to
849re-query the database.
850
851=head2 $entry->categories
852
853Returns a reference to an array of I<MT::Category> objects representing the
854categories to which the entry I<$entry> has been assigned (both primary and
855secondary categories). If the entry has not been assigned to any categories,
856returns a reference to an empty array.
857
858Caches the return value internally so that subsequent calls will not have to
859re-query the database.
860
861=head2 $entry->is_in_category($cat)
862
863Returns true if the entry I<$entry> has been assigned to entry I<$cat>, false
864otherwise.
865
866=head2 $entry->comments
867
868Returns a reference to an array of I<MT::Comment> objects representing the
869comments made on the entry I<$entry>. If no comments have been made on the
870entry, returns a reference to an empty array.
871
872Caches the return value internally so that subsequent calls will not have to
873re-query the database.
874
875=head2 $entry->archive_file([ $archive_type ])
876
877Returns the name of/path to the archive file for the entry I<$entry>. If
878I<$archive_type> is not specified, and you are using multiple archive types
879for your blog, the path is created from the preferred archive type that you
880have selected. If I<$archive_type> is specified, it should be one of the
881following values: C<Individual>, C<Daily>, C<Weekly>, C<Monthly>, and
882C<Category>.
883
884=head2 $entry->archive_url([ $archive_type ])
885
886Returns the absolute URL to the archive page for the entry I<$entry>. This
887calls I<archive_file> internally, so if I<$archive_type> is specified, it
888is merely passed through to that method. In other words, this is the
889blog Archive URL plus the results of I<archive_file>.
890
891=head2 $entry->permalink([ $archive_type ])
892
893Returns the (smart) permalink for the entry I<$entry>. Internally this calls
894I<archive_url>, which calls I<archive_file>, so I<$archive_type> (if
895specified) is merely passed through to that method. The result of this
896method is the same as I<archive_url> plus the URI fragment
897(C<#entry_id>), unless the preferred archive type is Individual, in which
898case the two methods give exactly the same results.
899
900=head2 $entry->text_filters
901
902Returns a reference to an array of text filter keynames (the short names
903that are the first argument to I<MT::add_text_filter>. This list can be
904passed directly in as the second argument to I<MT::apply_text_filters>.
905
906=head1 DATA ACCESS METHODS
907
908The I<MT::Entry> object holds the following pieces of data. These fields can
909be accessed and set using the standard data access methods described in the
910I<MT::Object> documentation.
911
912=over 4
913
914=item * id
915
916The numeric ID of the entry.
917
918=item * blog_id
919
920The numeric ID of the blog in which this entry has been posted.
921
922=item * author_id
923
924The numeric ID of the author who posted this entry.
925
926=item * status
927
928The status of the entry, either Publish (C<2>) or Draft (C<1>).
929
930=item * allow_comments
931
932An integer flag specifying whether comments are allowed on this entry. This
933setting determines whether C<E<lt>MTEntryIfAllowCommentsE<gt>> containers are
934displayed for this entry. Possible values are 0 for no comments, 1 for open
935comments and 2 for closed comments (that is, display the comments on this
936entry but do not allow new comments to be added).
937
938=item * convert_breaks
939
940A boolean flag specifying whether line and paragraph breaks should be converted
941when rebuilding this entry.
942
943=item * title
944
945The title of the entry.
946
947=item * excerpt
948
949The excerpt of the entry.
950
951=item * text
952
953The main body text of the entry.
954
955=item * text_more
956
957The extended body text of the entry.
958
959=item * created_on
960
961The timestamp denoting when the entry record was created, in the format
962C<YYYYMMDDHHMMSS>. Note that the timestamp has already been adjusted for the
963selected timezone.
964
965=item * modified_on
966
967The timestamp denoting when the entry record was last modified, in the
968format C<YYYYMMDDHHMMSS>. Note that the timestamp has already been adjusted
969for the selected timezone.
970
971=back
972
973=head1 DATA LOOKUP
974
975In addition to numeric ID lookup, you can look up or sort records by any
976combination of the following fields. See the I<load> documentation in
977I<MT::Object> for more information.
978
979=over 4
980
981=item * blog_id
982
983=item * status
984
985=item * author_id
986
987=item * created_on
988
989=item * modified_on
990
991=back
992
993=head1 NOTES
994
995=over 4
996
997=item *
998
999When you remove an entry using I<MT::Entry::remove>, in addition to removing
1000the entry record, all of the comments and placements (I<MT::Comment> and
1001I<MT::Placement> records, respectively) for this entry will also be removed.
1002
1003=back
1004
1005=head1 AUTHOR & COPYRIGHTS
1006
1007Please see the I<MT> manpage for author, copyright, and license information.
1008
1009=cut
Note: See TracBrowser for help on using the browser.