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

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

Revised weakref handling for categories. BugId:79687

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