root/branches/release-38/lib/MT/Category.pm @ 2275

Revision 2275, 15.1 kB (checked in by bchoate, 19 months ago)

Changed basename index to be blog_id+basename since that is how we search for existing basenames.

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