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

Revision 2314, 15.2 kB (checked in by bchoate, 19 months ago)

Weaken category parent/children references cached in a category object. 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        weaken( $_ ) foreach @{ $cat->{__children} };
405    }
406    @{$cat->{__children}};
407}
408
409sub is_ancestor {
410    my $cat = shift;
411    my ($possible_child) = @_;
412
413    # Catch the different blog edge case
414    return 0 if $cat->blog_id != $possible_child->blog_id;
415
416    return 1 if $cat->id == $possible_child->id;
417
418    # Keep having the child bump up one level in the hierarchy
419    # to see if it ever reaches the current category
420    # (more efficient then descending from the current category
421    # as the children lists do not need to be calculated
422
423    my $class = ref($cat);
424    while (my $id = $possible_child->parent) {
425        $possible_child = $class->load($id);
426        return 1 if $cat->id == $possible_child->id;
427    }
428 
429    # Looks like we didn't find it
430    0;
431}
432
433sub is_descendant {
434    my $cat = shift;
435    my ($possible_parent) = @_;
436    $possible_parent->is_ancestor($cat);
437}
438
4391;
440__END__
441
442=head1 NAME
443
444MT::Category - Movable Type category record
445
446=head1 SYNOPSIS
447
448    use MT::Category;
449    my $cat = MT::Category->new;
450    $cat->blog_id($blog->id);
451    $cat->label('My Category');
452    my @children = $cat->children;
453    $cat->save
454        or die $cat->errstr;
455
456=head1 DESCRIPTION
457
458An I<MT::Category> object represents a category in the Movable Type system.
459It is essentially a wrapper around the category label; by wrapping the label
460in an object with a numeric ID, we can use the ID as a "foreign key" when
461mapping entries into categories. Thus, if the category label changes, the
462mappings don't break. This object does not contain any information about the
463category-entry mappings--for those, look at the I<MT::Placement> object.
464
465=head1 USAGE
466
467As a subclass of I<MT::Object>, I<MT::Category> inherits all of the
468data-management and -storage methods from that class; thus you should look
469at the I<MT::Object> documentation for details about creating a new object,
470loading an existing object, saving an object, etc.
471
472=head1 DATA ACCESS METHODS
473
474The I<MT::Category> object holds the following pieces of data. These fields
475can be accessed and set using the standard data access methods described in
476the I<MT::Object> documentation.
477
478=over 4
479
480=item * id
481
482The numeric ID of the category.
483
484=item * blog_id
485
486The numeric ID of the blog to which this category belongs.
487
488=item * label
489
490The label of the category.
491
492=item * author_id
493
494The numeric ID of the author you created this category.
495
496=item * parent_category
497
498Returns a I<MT::Category> object representing the immediate parent category.
499Returns undef if there is none.
500
501=item * parent_categories
502
503Returns an array of I<MT::Category> objects representing the path from the
504category to the top level of categories, with the first member of the array
505being the immediate parent.  Returns an empty array if the category is already
506at the top level.
507
508=item * children_categories
509
510Returns an array of I<MT::Category> objects representing all of the
511immediate children of the category.
512
513=item * $subcat->is_descendant($parent)
514
515Returns a true value if the category is a descendant of $parent.
516
517=item * $subcat->is_ancestor($child)
518
519Returns a true value if the category is an ancestor of $child.
520
521=back
522
523=head1 DATA LOOKUP
524
525In addition to numeric ID lookup, you can look up or sort records by any
526combination of the following fields. See the I<load> documentation in
527I<MT::Object> for more information.
528
529=over 4
530
531=item * blog_id
532
533=item * label
534
535=back
536
537=head1 NOTES
538
539=over 4
540
541=item *
542
543When you remove a category using I<MT::Category::remove>, in addition to
544removing the category record, all of the entry-category mappings
545(I<MT::Placement> objects) will be removed.
546
547=back
548
549=head1 CLASS METHODS
550
551=over 4
552
553=item * MT::Category->top_level_categories($blog_id)
554
555
556Returns an array of I<MT::Category> objects representing the top level of
557the category hierarchy in the blog identified by $blog_id.
558
559=back
560
561=head1 AUTHOR & COPYRIGHTS
562
563Please see the I<MT> manpage for author, copyright, and license information.
564
565=cut
Note: See TracBrowser for help on using the browser.