root/branches/release-35/lib/MT/Category.pm @ 1931

Revision 1931, 15.1 kB (checked in by bchoate, 20 months ago)

Adding meta declaration to MT::Category.

  • 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::Category;
8
9use strict;
10use base qw( MT::Object );
11
12use MT::Blog;
13
14__PACKAGE__->install_properties({
15    column_defs => {
16        'id' => 'integer not null auto_increment',
17        'blog_id' => 'integer not null',
18        'label' => 'string(100) not null',
19        'author_id' => 'integer',
20        'ping_urls' => 'text',
21        'description' => 'text',
22        'parent' => 'integer',
23        'allow_pings' => 'boolean',
24        'basename' => 'string(255)',
25    },
26    indexes => {
27        blog_id => 1,
28        label => 1,
29        parent => 1,
30        basename => 1,
31        blog_class => {
32            columns => [ 'blog_id', 'class' ],
33        },
34    },
35    defaults => {
36        parent => 0,
37        allow_pings => 0,
38    },
39    class_type => 'category',
40    child_of => 'MT::Blog',
41    audit => 1,
42    meta => 1,
43    child_classes => ['MT::Placement', 'MT::Trackback', 'MT::FileInfo'],
44    datasource => 'category',
45    primary_key => 'id',
46});
47
48sub class_label {
49    MT->translate("Category");
50}
51
52sub class_label_plural {
53    MT->translate("Categories");
54}
55
56sub basename_prefix {
57    my $this = shift;
58    my ($dash) = @_;
59    my $prefix = 'cat';
60    if ($dash) {
61        $prefix .= MT->instance->config('CategoryNameNodash') ? '' : '-';
62    }
63    $prefix;
64}
65
66sub ping_url_list {
67    my $cat = shift;
68    return [] unless $cat->ping_urls && $cat->ping_urls =~ /\S/;
69    [ split /\r?\n/, $cat->ping_urls ];
70}
71
72sub publish_path {
73    my $cat = shift;
74    return $cat->{__path} if exists $cat->{__path};
75    my $result = $cat->basename;
76    do {
77        # TODO: uh, does this not mean we cache the resulting path on the
78        # root category object instead?
79        $cat = $cat->parent ? __PACKAGE__->load($cat->parent) : undef;
80        $result = join "/", $cat->basename, $result if $cat;
81    } while ($cat);
82    # caching this information may be problematic IF
83    # parent category basenames are changed.
84    $cat->{__path} = $result;
85}
86*category_path = \&publish_path;
87
88sub category_label_path {
89    my $cat = shift;
90    return $cat->{__label_path} if exists $cat->{__label_path};
91    my $result = $cat->label =~ m!/! ? '[' . $cat->label . ']' : $cat->label;
92    do {
93        $cat = $cat->parent ? __PACKAGE__->load($cat->parent) : undef;
94        $result = join "/", ($cat->label =~ m!/! ? '[' . $cat->label . ']' : $cat->label),
95            $result if $cat;
96    } while ($cat);
97    # caching this information may be problematic IF
98    # parent category labels are changed.
99    $cat->{__label_path} = $result;
100}
101
102sub cache_obj {
103    my $pkg = shift;
104    my (%param) = @_;
105    my $blog_id = $param{blog_id};
106    my $sess_id = 'blog:' . $blog_id;
107    require MT::Session;
108    my $cat_cache = MT::Session::get_unexpired_value(60 * 60, {
109        kind => 'CC',  # category cache
110        id => $sess_id
111    });
112    if (!$cat_cache) {
113        $cat_cache = new MT::Session;
114        $cat_cache->kind('CC');
115        $cat_cache->id($sess_id);
116        $cat_cache->start(time);
117    }
118    $cat_cache;
119}
120
121sub clear_cache {
122    my $pkg = shift;
123    my (%param) = @_;
124    my $cat_cache = $pkg->cache_obj(@_);
125    $cat_cache->remove;
126}
127
128sub cache {
129    my $pkg = shift;
130    my (%param) = @_;
131    my $blog_id = $param{blog_id};
132    my $sess_id = 'blog:' . $blog_id;
133    my $cat_cache = $pkg->cache_obj(@_);
134    my $data = $cat_cache->get('category_cache');
135    if (!$data) {
136        my $cat_iter = $pkg->load_iter({blog_id => $blog_id});
137        $data = [];
138        while (my $cat = $cat_iter->()) {
139            push @$data, [ $cat->id, $cat->label, $cat->parent ];
140        }
141        $cat_cache->set('category_cache', $data);
142        $cat_cache->save;
143    }
144    $data || [];
145}
146
147sub save {
148    my $cat = shift;
149    my $pkg = ref($cat);
150
151    my $clear_cache;
152    if ($cat->id) {
153        my $orig_cat = $pkg->load($cat->id);
154        if (!$orig_cat || ($orig_cat->label ne $cat->label) || ($orig_cat->parent != $cat->parent)) {
155            $clear_cache = 1;
156        }
157    } else {
158        # new category-- invalidate any cache
159        $clear_cache = 1;
160    }
161
162    # check that the parent is legit.
163    if ($cat->parent && $cat->parent ne '0') {
164        my $parent = $pkg->load($cat->parent);
165        $cat->parent(0) unless $parent;
166    }
167
168    if ($cat->parent && $cat->parent ne '0') {
169        my $parent = $pkg->load($cat->parent);
170        if (!$parent) {
171            return $cat->error(MT->translate("Categories must exist within the same blog"))
172                if ($cat->blog_id != $parent->blog_id);
173            return $cat->error(MT->translate("Category loop detected"))
174                if ($cat->id && $cat->is_ancestor($parent));
175        }
176    }
177
178    $cat->SUPER::save(@_) or return;
179
180    # set category basename after save, because of cat_id needed.
181    if (!defined($cat->basename) || ($cat->basename eq '')) {
182        require MT::Util;
183        my $name = MT::Util::make_unique_category_basename($cat);
184        $cat->basename($name);
185        $cat->SUPER::save(@_) or return;
186    }
187
188    ## If pings are allowed on this entry, create or update
189    ## the corresponding Trackback object for this entry.
190    require MT::Trackback;
191    if ($cat->allow_pings) {
192        my $tb;
193        unless ($tb = MT::Trackback->load({
194                                 category_id => $cat->id })) {
195            $tb = MT::Trackback->new;
196            $tb->blog_id($cat->blog_id);
197            $tb->category_id($cat->id);
198            $tb->entry_id(0);   ## entry_id can't be NULL
199        }
200        if (defined(my $pass = $cat->{__tb_passphrase})) {
201            $tb->passphrase($pass);
202        }
203        $tb->title($cat->label);
204        $tb->description($cat->description);
205        my $blog = MT::Blog->load($cat->blog_id)
206            or return;
207        my $url = $blog->archive_url;
208        $url .= '/' unless $url =~ m!/$!;
209        $url .= MT::Util::archive_file_for(undef, $blog,
210            'Category', $cat);
211        $tb->url($url);
212        $tb->is_disabled(0);
213        $tb->save
214            or return $cat->error($tb->errstr);
215    } else {
216        ## If there is a TrackBack item for this category, but
217        ## pings are now disabled, make sure that we mark the
218        ## object as disabled.
219        if (my $tb = MT::Trackback->load({
220                                  category_id => $cat->id })) {
221            $tb->is_disabled(1);
222            $tb->save
223                or return $cat->error($tb->errstr);
224        }
225    }
226    if ($clear_cache) {
227        $pkg->clear_cache('blog_id' => $cat->blog_id);
228    }
229    1;
230}
231
232sub remove {
233    my $cat = shift;
234    $cat->remove_children({ key => 'category_id' });
235    if (ref $cat) {
236        my $pkg = ref($cat);
237        # orphan my children up to the root level
238        my @children = $cat->children_categories;
239        if (scalar @children) {
240            foreach my $child (@children) {
241                $child->parent(($cat->parent) ? $cat->parent : '0');
242                $child->save or return $cat->error($child->save);
243            }
244        } else {
245            $pkg->clear_cache('blog_id' => $cat->blog_id);
246        }
247    }
248    $cat->SUPER::remove(@_);
249}
250
251
252sub _flattened_category_hierarchy {
253    # Either the class name or a MT::Category object
254    my $cat = shift;
255    my $class = ref($cat) || $cat;
256    my @cats = ();
257    my @flattened_cats = ();
258
259    if (!ref ($cat)) {
260        # If it is the class name (i.e. called "statically")
261        # Grab the blog_id from the parameters list and get the top level categories
262        my $blog_id = shift or return ();
263
264        my @cats = $class->load({ blog_id => $blog_id }, { 'sort' => 'label' });
265        my $children = {};
266        foreach my $cat (@cats) {
267            if ($cat->parent) {
268                my $list = $children->{$cat->parent} ||= [];
269                push @$list, $cat;
270            }
271        }
272        sub __pusher {
273            my ($children, $id) = @_;
274            my $list = $children->{$id};
275            return () unless $list && @$list;
276            my @flat;
277            push @flat, 'BEGIN_SUBCATS';
278            foreach (@$list) {
279                push @flat, $_;
280                if ($children->{$_->id}) {
281                    push @flat, __pusher($children, $_->id);
282                }
283            }
284            push @flat, 'END_SUBCATS';
285            @flat;
286        }
287        foreach my $cat (@cats) {
288            if (!$cat->parent) {
289                push @flattened_cats, $cat;
290                push @flattened_cats, __pusher($children, $cat->id)
291                        if $children->{$cat->id};
292            }
293        }
294        return @flattened_cats;
295    }
296
297    # Otherwise, the starting point is the category itself
298    @cats = ($cat);
299
300    # Depth-first search time
301    foreach my $c (@cats) {
302        # Push the current category onto the list
303        push @flattened_cats, $c;
304
305        # If it has any children
306        my @children = $c->children_categories;
307        if (scalar @children) {
308
309            # Indicate the start of the children
310     
311            push @flattened_cats, "BEGIN_SUBCATS";
312
313            # Add all the kids (and their associated subcategories)
314            foreach my $kid (@children) {
315                push @flattened_cats, ($kid->_flattened_category_hierarchy);
316            }
317
318            # Indicate the end of the children
319            push @flattened_cats, "END_SUBCATS";
320        }
321    }
322
323    @flattened_cats;
324}
325
326# Deprecated routine -- also assumes MT::Category class, so it won't
327# work with folders for instance.
328sub _buildCatHier {
329    my ($blog_id) = @_;
330 
331    require MT::Request;
332
333    my %children;
334
335    my $r = MT::Request->instance;
336    my $all_cats = $r->cache('sub_cats_cats');
337    unless ($all_cats) {
338        $r->cache('sub_cats_cats', $all_cats = {});
339    }
340    my $cats;
341    if (defined $all_cats->{$blog_id}) {
342        my $children = $all_cats->{$blog_id}{'children'};
343        return ($children);
344    }
345
346    # Start by loading all the categories for the given blog
347    # and default to setting all of their parents to '0'
348 
349    my @cats = MT::Category->load({ blog_id => $blog_id });
350    foreach my $cat (@cats) {
351        push @{$children{($cat->parent) ? $cat->parent : '0'}}, $cat;
352    }
353
354    foreach my $i (keys %children) {
355        @{$children{$i}} = sort { $a->label cmp $b->label } @{$children{$i}};
356    }
357
358    $all_cats->{$blog_id}{'children'} = \%children;
359    $r->cache('sub_cats_cats', $all_cats);
360 
361    (\%children);
362}
363
364sub top_level_categories {
365    my ($class, $blog_id) = @_;
366    my @cats = $class->load({ blog_id => $blog_id, parent => '0' }, { 'sort' => 'label' });
367}
368
369sub copy_cat {
370    my $class = shift;
371    my $cat = $class->new;
372    my $old_cat = shift;
373    $cat->set_values($old_cat->column_values);
374    $cat;
375}
376
377sub parent_categories {
378    my $cat = shift;
379
380    return () if (!$cat->parent_category);
381    ($cat->parent_category, $cat->parent_category->parent_categories);
382}
383
384sub parent_category {
385    my $cat = shift;
386    my $class = ref($cat);
387    unless ($cat->{__parent_category}) {
388        $cat->{__parent_category} = ($cat->parent) ? $class->load($cat->parent) : undef;
389    }
390    $cat->{__parent_category};
391}
392
393sub children_categories {
394    my $cat = shift;
395    my $class = ref($cat);
396    unless ($cat->{__children}) {
397        @{$cat->{__children}} = sort { $a->label cmp $b->label }
398        $class->load({ blog_id => $cat->blog_id,
399            parent => $cat->id });
400    }
401    @{$cat->{__children}};
402}
403
404sub is_ancestor {
405    my $cat = shift;
406    my ($possible_child) = @_;
407
408    # Catch the different blog edge case
409    return 0 if $cat->blog_id != $possible_child->blog_id;
410
411    return 1 if $cat->id == $possible_child->id;
412
413    # Keep having the child bump up one level in the hierarchy
414    # to see if it ever reaches the current category
415    # (more efficient then descending from the current category
416    # as the children lists do not need to be calculated
417
418    my $class = ref($cat);
419    while (my $id = $possible_child->parent) {
420        $possible_child = $class->load($id);
421        return 1 if $cat->id == $possible_child->id;
422    }
423 
424    # Looks like we didn't find it
425    0;
426}
427
428sub is_descendant {
429    my $cat = shift;
430    my ($possible_parent) = @_;
431    $possible_parent->is_ancestor($cat);
432}
433
4341;
435__END__
436
437=head1 NAME
438
439MT::Category - Movable Type category record
440
441=head1 SYNOPSIS
442
443    use MT::Category;
444    my $cat = MT::Category->new;
445    $cat->blog_id($blog->id);
446    $cat->label('My Category');
447    my @children = $cat->children;
448    $cat->save
449        or die $cat->errstr;
450
451=head1 DESCRIPTION
452
453An I<MT::Category> object represents a category in the Movable Type system.
454It is essentially a wrapper around the category label; by wrapping the label
455in an object with a numeric ID, we can use the ID as a "foreign key" when
456mapping entries into categories. Thus, if the category label changes, the
457mappings don't break. This object does not contain any information about the
458category-entry mappings--for those, look at the I<MT::Placement> object.
459
460=head1 USAGE
461
462As a subclass of I<MT::Object>, I<MT::Category> inherits all of the
463data-management and -storage methods from that class; thus you should look
464at the I<MT::Object> documentation for details about creating a new object,
465loading an existing object, saving an object, etc.
466
467=head1 DATA ACCESS METHODS
468
469The I<MT::Category> object holds the following pieces of data. These fields
470can be accessed and set using the standard data access methods described in
471the I<MT::Object> documentation.
472
473=over 4
474
475=item * id
476
477The numeric ID of the category.
478
479=item * blog_id
480
481The numeric ID of the blog to which this category belongs.
482
483=item * label
484
485The label of the category.
486
487=item * author_id
488
489The numeric ID of the author you created this category.
490
491=item * parent_category
492
493Returns a I<MT::Category> object representing the immediate parent category.
494Returns undef if there is none.
495
496=item * parent_categories
497
498Returns an array of I<MT::Category> objects representing the path from the
499category to the top level of categories, with the first member of the array
500being the immediate parent.  Returns an empty array if the category is already
501at the top level.
502
503=item * children_categories
504
505Returns an array of I<MT::Category> objects representing all of the
506immediate children of the category.
507
508=item * $subcat->is_descendant($parent)
509
510Returns a true value if the category is a descendant of $parent.
511
512=item * $subcat->is_ancestor($child)
513
514Returns a true value if the category is an ancestor of $child.
515
516=back
517
518=head1 DATA LOOKUP
519
520In addition to numeric ID lookup, you can look up or sort records by any
521combination of the following fields. See the I<load> documentation in
522I<MT::Object> for more information.
523
524=over 4
525
526=item * blog_id
527
528=item * label
529
530=back
531
532=head1 NOTES
533
534=over 4
535
536=item *
537
538When you remove a category using I<MT::Category::remove>, in addition to
539removing the category record, all of the entry-category mappings
540(I<MT::Placement> objects) will be removed.
541
542=back
543
544=head1 CLASS METHODS
545
546=over 4
547
548=item * MT::Category->top_level_categories($blog_id)
549
550
551Returns an array of I<MT::Category> objects representing the top level of
552the category hierarchy in the blog identified by $blog_id.
553
554=back
555
556=head1 AUTHOR & COPYRIGHTS
557
558Please see the I<MT> manpage for author, copyright, and license information.
559
560=cut
Note: See TracBrowser for help on using the browser.