root/branches/release-26/lib/MT/Category.pm @ 1174

Revision 1174, 14.8 kB (checked in by bchoate, 23 months ago)

Updated copyright year for source.

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