[Jifty-commit] r3560 - in apps/CASPlus/trunk: lib/CASPlus lib/CASPlus/Model lib/CASPlus/Util t

jifty-commit at lists.jifty.org jifty-commit at lists.jifty.org
Tue Jun 26 09:15:37 EDT 2007


Author: sterling
Date: Tue Jun 26 09:15:36 2007
New Revision: 3560

Added:
   apps/CASPlus/trunk/lib/CASPlus/Util/
   apps/CASPlus/trunk/lib/CASPlus/Util/Relationship.pm
   apps/CASPlus/trunk/t/40-many-to-many_relationships-explicit.t
Modified:
   apps/CASPlus/trunk/   (props changed)
   apps/CASPlus/trunk/lib/CASPlus/Model/Profile.pm
   apps/CASPlus/trunk/lib/CASPlus/Model/RoleMember.pm
   apps/CASPlus/trunk/lib/CASPlus/Model/User.pm
   apps/CASPlus/trunk/lib/CASPlus/ProfileBase.pm
   apps/CASPlus/trunk/lib/CASPlus/ProfileRelationshipMixin.pm

Log:
 r7787 at dynpc145:  andrew | 2007-06-25 21:45:44 -0500
  * Added a relationships() method to Profile models to find all relationships
    for the profile.
  * Added cascade delete triggers to RoleMember models to automatically delete
    all cached paths related to a removed role member.
  * Removed the recalculate_role_cache() method from User.
  * Added the recalculate_role_cache() method to Profile objects.
  * Started extracted role cache manipulation methods into the new utility class,
    CASPlus::Util::Relationship.
  * Added the 40-many-to-many-relationships-explicit.t test to confirm basic
    role caching algorithms against my hand-predicted outcomes.


Modified: apps/CASPlus/trunk/lib/CASPlus/Model/Profile.pm
==============================================================================
--- apps/CASPlus/trunk/lib/CASPlus/Model/Profile.pm	(original)
+++ apps/CASPlus/trunk/lib/CASPlus/Model/Profile.pm	Tue Jun 26 09:15:36 2007
@@ -315,6 +315,31 @@
     return $relationships;
 }
 
+=head2 relationships
+
+  my $relationships = $profile->relationships;
+
+Returns a collection containing all relationships the profile belongs to, whether as parent or child or both.
+
+=cut
+
+sub relationships {
+    my $self = shift;
+
+    my $relationships = CASPlus::Model::ProfileRelationshipCollection->new;
+    $relationships->limit(
+        column => 'relation_parent',
+        value  => $self,
+    );
+    $relationships->limit(
+        column           => 'relation_child',
+        value            => $self,
+        entry_aggregator => 'OR',
+    );
+
+    return $relationships;
+}
+
 =head1 AUTHOR
 
 Andrew Sterling Hanenkamp C<<hanenkamp at cpan.org>>

Modified: apps/CASPlus/trunk/lib/CASPlus/Model/RoleMember.pm
==============================================================================
--- apps/CASPlus/trunk/lib/CASPlus/Model/RoleMember.pm	(original)
+++ apps/CASPlus/trunk/lib/CASPlus/Model/RoleMember.pm	Tue Jun 26 09:15:36 2007
@@ -44,7 +44,49 @@
 
 =head1 METHODS
 
-=head1 current_user_can
+=head2 before_delete
+
+Helper hook handler used with L</after_delete>.
+
+=cut
+
+sub before_delete {
+    my $self = shift;
+
+    $self->{__delete_old_id} = $self->id;
+
+    return 1;
+}
+
+=head2 after_delete
+
+Cascade deletes all path cache objects associated with this role membership.
+
+=cut
+
+sub after_delete {
+    my ($self, $result) = @_;
+
+    my $id = delete $self->{__delete_old_id};
+
+    # Only continue if the delete was carried out
+    if ($result) {
+        my $cache_paths = CASPlus::Model::MemberRolePathCacheCollection->new(
+            current_user => CASPlus::CurrentUser->superuser,
+        );
+        $cache_paths->limit(
+            column => 'role_member',
+            value  => $id,
+        );
+        while (my $cache_path = $cache_paths->next) {
+            $cache_path->delete;
+        }
+    }
+
+    return 1;
+}
+
+=head2 current_user_can
 
   my $access = $membership->current_user_can($right, %args);
 

Modified: apps/CASPlus/trunk/lib/CASPlus/Model/User.pm
==============================================================================
--- apps/CASPlus/trunk/lib/CASPlus/Model/User.pm	(original)
+++ apps/CASPlus/trunk/lib/CASPlus/Model/User.pm	Tue Jun 26 09:15:36 2007
@@ -230,200 +230,6 @@
     return ${ $str->string_ref };
 }
 
-=head2 recalculate_role_cache
-
-  $user->recalculate_role_cache;
-
-This is a brute force method for rebuilding the L</member_roles> collection. This causes role memberships to be replaced by a complete recalculation for the current user.
-
-This is provided in case an external utility will be manipulating profiles without using the API that makes sure that roles are correct or if they become somehow corrupted. In general, however, this method is costly and does not need to be called regularly.
-
-=cut
-
-sub recalculate_role_cache {
-    my $self         = shift;
-    my $self_obj     = $self->profile;
-    my $self_profile = $self_obj->profile_definition;
-
-    # Queue used for breadth-first search
-    my @open_list = ([ $self_obj, $self_profile, $self_obj->unique_id ]);
-
-    # The roles that have been discovered
-    my %object_paths;
-
-    # Breadth-first search of parents
-    PARENT: while (my $parent = shift @open_list) {
-        my ($parent_obj, $parent_profile, $parent_path) = @$parent;
-        my $unique_id = $parent_obj->unique_id;
-        
-        # Don't continue if this object has already been added, the children
-        # can be ignored as well on the assumption that the object's children
-        # were already listed in @open_list
-        if ($object_paths{ $unique_id }) {
-            push @{ $object_paths{ $unique_id } }, $parent_path;
-            next PARENT;
-        }
-
-        # Create a new object path list
-        $object_paths{ $unique_id } 
-            = [ $parent_obj, $parent_profile, $parent_path ];
-
-        # Find all the parents of the current object that might also contribute
-        # roles to the user
-        my $parent_relationships = $parent_profile->parent_relationships;
-        $parent_relationships->limit(
-            column => 'roles_propagate_to_children',
-            value  => 1,
-        );
-
-        # Iterate through the parents
-        while (my $parent_relationship = $parent_relationships->next) {
-            my $method = $parent_relationship->child_column->name;
-
-            # A one-to-many relationship
-            if ($parent_relationship->many_parents 
-                    and not $parent_relationship->many_children) {
-
-                # Iterate throught the grandparents
-                my $grandparents = $parent->$method;
-                while (my $grandparent_obj = $grandparents->next) {
-                    push @open_list, [ 
-                        $grandparent_obj, 
-                        $parent_relationship->relation_parent,
-                        $parent_path 
-                            . ':@' . $parent_relationship->id 
-                            . ':'  . $grandparent_obj->unique_id,
-                    ];
-                }
-            }
-
-            # A one-to-one or many-to-one relationship
-            elsif (!$parent_relationship->many_parents) {
-                my $grandparent_obj = $parent_obj->$method;
-
-                # Only add it to the list if the link exists
-                if ($grandparent_obj->id) {
-                    push @open_list, [
-                        $grandparent_obj,
-                        $parent_relationship->relation_parent,
-                        $parent_path 
-                            . ':@' . $parent_relationship->id 
-                            . ':'  . $grandparent_obj->unique_id,
-                    ];
-                }
-            }
-
-            # A many-to-many relationship
-            else {
-                my $grandparent_links = $parent_obj->$method;
-
-                # Similar to one-to-many, but an extra jump
-                while (my $grandparent_link = $grandparent_links->next) {
-                    my $grandparent_obj = $grandparent_link->parent;
-                    push @open_list, [ 
-                        $grandparent_obj, 
-                        $parent_relationship->relation_parent,
-                        $parent_path 
-                            . ':@' . $parent_relationship->id 
-                            . ':'  . $grandparent_obj->unique_id,
-                    ];
-                }
-            }
-        }
-    }
-
-    # Find the list of orphaned cache_paths that have this user at
-    # the root, we need to delete all of these before continuing. The list will
-    # be completely rebuilt.
-    my $cache_paths = CASPlus::Model::RoleMemberPathCacheCollection->new;
-    $cache_paths->limit(
-        column   => 'role_member',
-        operator => 'IS',
-        value    => 'NULL',
-
-    );
-    $cache_paths->limit(
-        column   => 'cache_path',
-        operator => 'STARTSWITH',
-        value    => ':'.$self_obj->unique_id.':',
-    );
-
-    # Delete all the orphaned cache paths belonging to this user
-    while (my $cache_path = $cache_paths->next) {
-        $cache_path->delete;
-    }
-
-    # Delete any removed roles
-    my $member_roles = $self->member_roles;
-    while (my $member_role = $member_roles->next) {
-
-        my $role_profile = $member_role->the_role->profile;
-
-        # Only delete the removed roles
-        unless ($object_paths{ $role_profile->unique_id }) {
-            my $cache_paths = $member_role->cache_paths;
-            while (my $path_cache = $cache_paths->next) {
-                $path_cache->delete;
-            }
-            $member_role->delete;
-        }
-    }
-
-    # Update/add existing/new roles
-    my $role_member = CASPlus::Model::RoleMember->new;
-    for my $object_info (values %object_paths) {
-        my $object      = shift @$object_info;
-        my $obj_profile = shift @$object_info;
-        my %cache_paths = map { (":$_:" => 1) } @$object_info;
-
-        # Handle roles specially by adding them to the cache
-        if ($obj_profile->profile_type eq 'role') {
-            my $role = $object->role_object;
-
-            # Try to load it first
-            $role_member->load_by_cols( the_role => $role );
-
-            # If not found, create it
-            unless ($role_member->id) {
-                $role_member->create(
-                    the_user => $self,
-                    the_role => $role,
-                );
-            }
-
-            # Delete removed cache paths
-            my $cache_paths = $role_member->cache_paths;
-            while (my $path_cache = $cache_paths->next) {
-                # Keep it if it exists, but remove it from the add list
-                $path_cache->delete 
-                    unless delete $cache_paths{ $path_cache->cache_path };
-            }
-
-            # All that should be left in %cache_paths are those to add, add'em
-            my $path_cache = CASPlus::Model::RoleMemberPathCache->new;
-            for my $cache_path (values %cache_paths) {
-                $path_cache->create(
-                    role_member => $role_member,
-                    cache_path  => $cache_path,
-                );
-            }
-        }
-
-        # Not a role, but we remember role propagating paths in a way that can
-        # speed up adding roles in the future
-        else {
-
-            # Add all the ophaned cache paths from user to object back in
-            my $path_cache = CASPlus::Model::RoleMemberPathCache->new;
-            for my $cache_path (values %cache_paths) {
-                $path_cache->create(
-                    cache_path  => $cache_path,
-                );
-            }
-        }
-    }
-}
-
 =head1 AUTHOR
 
 Andrew Sterling Hanenkamp, C<<hanenkamp at cpan.org>>>

Modified: apps/CASPlus/trunk/lib/CASPlus/ProfileBase.pm
==============================================================================
--- apps/CASPlus/trunk/lib/CASPlus/ProfileBase.pm	(original)
+++ apps/CASPlus/trunk/lib/CASPlus/ProfileBase.pm	Tue Jun 26 09:15:36 2007
@@ -4,6 +4,7 @@
 package CASPlus::ProfileBase;
 use base qw/ CASPlus::Record /;
 
+use CASPlus::Util::Relationship;
 use Jifty::Util;
 use List::MoreUtils qw/ any /;
 
@@ -162,6 +163,22 @@
     return $unique_profile_link->id;
 }
 
+=head2 recalculate_role_cache
+
+  $profile_obj->recalculate_role_cache;
+
+This is a brute force method for rebuilding all L<CASPlus::Model::RoleMember> and L<CASPlus::Model::RoleMemberPathCache> objects associated with this profile object.
+
+Basically, this completely rebuilds the cache for any profile object related to the current profile object. On a user, this will result in role cache for the current user being recalculated. On any object (including users), this also results in the partial recalculation of any users that have a descendant relationship with the current object.
+
+=cut
+
+sub recalculate_role_cache {
+    my $self_obj     = shift;
+
+    CASPlus::Util::Relationship->recalculate_role_cache_for_object($self_obj);
+}
+
 =head1 AUTHOR
 
 Andrew Sterling Hanenkamp C<<hanenkamp at cpan.org>>

Modified: apps/CASPlus/trunk/lib/CASPlus/ProfileRelationshipMixin.pm
==============================================================================
--- apps/CASPlus/trunk/lib/CASPlus/ProfileRelationshipMixin.pm	(original)
+++ apps/CASPlus/trunk/lib/CASPlus/ProfileRelationshipMixin.pm	Tue Jun 26 09:15:36 2007
@@ -8,6 +8,8 @@
 use CASPlus::Record schema {
 };
 
+use CASPlus::Util::Relationship;
+
 =head1 NAME
 
 CASPlus::ProfileRelationshipMixin - mixin class adding triggers to relationships
@@ -85,91 +87,11 @@
     my ($self, $result) = @_;
 
     my $args = delete $self->{__create_relationship};
+    $args->{relationship} = $self->profile_relationship_definition;
 
-    if ($result) {
-
-        # Get the relationship definition
-        my $relationship = $self->profile_relationship_definition;
-
-        # Base case: Add the initial path of the child is a user
-        if ($relationship->relation_child->profile_type eq 'user') {
-            my $base_role_member = undef;
-            
-            # Create a role membership if this is a user-role relationship
-            if ($relationship->relation_child->profile_type eq 'role') {
-                $base_role_member = CASPlus::Model::RoleMember->new;
-                $base_role_member->create(
-                    the_user => $args->{child}->user_object,
-                    the_role => $args->{parent}->role_object,
-                );
-            }
-
-            # Create abbreviated names for the IDs in the cache path
-            my $cid = $args->{child}->unique_id;
-            my $rid = $relationship->id;
-            my $pid = $args->{parent}->unique_id;
-
-            # Build the cache path
-            my $cache_path = ":$cid:\@$rid:$pid:";
-
-            # Create the cache path record
-            my $path_cache = CASPlus::Model::RoleMemberPathCache->new;
-            $path_cache->create(
-                (defined $base_role_member 
-                    ? (role_member => $base_role_member) 
-                    : ()),
-                cache_path  => $cache_path,
-            );
-        }
-
-        # Find all cache paths ending with the child
-        my $cache_paths = CASPlus::Model::RoleMemberPathCacheCollection->new;
-        $cache_paths->limit(
-            column   => 'cache_path',
-            operator => 'ENDSWITH',
-            value    => ':'.$args->{child}->id.':',
-        );
-
-        # If this adds a role to the affected users, load that role
-        my $role
-            = $relationship->relation_parent->profile_type eq 'role'
-                ? $args->{parent}->role_object 
-                : undef;
-
-        # Add new paths that add the parent to the end of the paths with the
-        # child.
-        my $new_path_cache = CASPlus::Model::RoleMemberPathCache->new;
-        while (my $path_cache = $cache_paths->next) {
-            my $cache_path = $path_cache->cache_path;
-            my $role_member = undef;
-
-            # Has a role, we need to create a role member
-            if ($role) {
+    CASPlus::Util::Relationship->add_relationship(%$args) if $result;
 
-                # Figure out which user to add the role to
-                my ($user_unique_id) = $cache_path =~ /^:(\d+):/;
-                my $user_profile_obj 
-                    = CASPlus->get_profile_object_by_unique_id($user_unique_id);
-
-                die "No profile for unique ID #($user_unique_id) taken from cache path ($cache_path)" unless defined $user_profile_obj;
-
-                # Create the role membership
-                $role_member = CASPlus::Model::RoleMember->new;
-                $role_member->create(
-                    the_user => $user_profile_obj->user_object,
-                    the_role => $role,
-                );
-            }
-
-            # Create the appended cache path record
-            my $rel_id = $relationship->id;
-            my $par_id = $args->{parent}->unique_id;
-            $new_path_cache->create(
-                (defined $role_member ? (role_member => $role_member) : ()),
-                cache_path  => $cache_path.'@'.$rel_id.':'.$par_id.':',
-            );
-        }
-    }
+    return 1;
 }
 
 =head2 before_delete_relationship

Added: apps/CASPlus/trunk/lib/CASPlus/Util/Relationship.pm
==============================================================================
--- (empty file)
+++ apps/CASPlus/trunk/lib/CASPlus/Util/Relationship.pm	Tue Jun 26 09:15:36 2007
@@ -0,0 +1,452 @@
+use strict;
+use warnings;
+
+package CASPlus::Util::Relationship;
+
+=head1 NAME
+
+CASPlus::Util::Relationship - utilties for relationships and role caching
+
+=head1 DESCRIPTION
+
+B<WARNING:> Unless you are working on the core of CAS+, you probably don't want to mess with any of these methods.
+
+The utilities in this class are documented for internal use by CAS+ and are not intended for any other use than to provide helpers for writing to and reading from the role cache. This is a delicate process that requires some specialized code that has been deposited here.
+
+=head1 METHODS
+
+=head2 add_relationship
+
+  CASPlus::Util::Relationship->add_relationship({
+      child        => $child, 
+      relationship => $relationship, 
+      parent       => $parent
+  );
+
+This method handles the details of modifying the role cache after adding the relationship specified by the arguments.
+
+=cut
+
+sub add_relationship {
+    my $class = shift;
+    my %args  = @_;
+
+    $class->add_immediate_relationship(%args);
+    $class->add_append_relationships(%args);
+    $class->add_prepend_relationships(%args);
+}
+
+=head2 add_immediate_relationship
+
+  CASPlus::Util::Relationship->add_immediate_relationship(
+      child        => $child, 
+      relationship => $relationship, 
+      parent       => $parent,
+  );
+
+Updates the path cache for and creates any role memberships required to make
+handle the relationship directly between C<$child> and C<$parent>.
+
+=cut
+
+sub add_immediate_relationship {
+    my $class = shift;
+    my %args  = @_;
+
+    # Create abbreviated names for the IDs in the cache path
+    my $cid = $args{child}->unique_id;
+    my $rid = $args{relationship}->id;
+    my $pid = $args{parent}->unique_id;
+
+    # Build the cache path
+    my $cache_path = ":$cid:\@$rid:$pid:";
+
+    # Create the cache path and possibly the role membership
+    $args{cache_path} = $cache_path;
+    $class->add_cache_path(%args);
+}
+
+=head2 add_cache_path
+
+  CASPlus::Util::Relationship->add_role_member_cache_path(
+      child          => $child, 
+      parent         => $parent, 
+      cache_path     => $cache_path,
+  );
+
+Adds a cache path to the role cache. If the given C<$child> is a user profile object and the given C<$parent> is a role profile object, this will associate the cache path with the correct L<CASPlus::Model::RoleMember> (or create it if it does not yet exist).
+
+=cut
+
+sub add_cache_path {
+    my $class = shift;
+    my %args  = @_;
+
+    my $child_profile  = $args{child}->profile_definition;
+    my $parent_profile = $args{parent}->profile_definition;
+
+    # Create a role membership if this is a user-role relationship
+    my $role_member;
+    if ($child_profile->profile_type eq 'user'
+            and $parent_profile->profile_type eq 'role') {
+        $role_member = CASPlus::Model::RoleMember->new;
+        $role_member->load_or_create(
+            the_user => $args{child}->user_object,
+            the_role => $args{parent}->role_object,
+        );
+    }
+
+    # The arguments to create
+    my %path_cache = (
+        cache_path => $args{cache_path},
+    );
+    $path_cache{role_member} = $role_member if defined $role_member;
+
+    # Create the cache path record
+    my $path_cache = CASPlus::Model::RoleMemberPathCache->new;
+    $path_cache->load_or_create(%path_cache);
+}
+
+=head2 add_append_relationships
+
+  CASPlus::Util::Relationship->add_append_relationships(
+      child        => $child,
+      relationship => $relationship,
+      parent       => $parent,
+  );
+
+Finds all cached paths ending with the unique ID of C<$child> and appends this relationship to those paths. This will update any role memberships and cached paths as required.
+
+=cut
+
+sub add_append_relationships {
+    my $class = shift;
+    my %args  = @_;
+
+    # Find all cache paths ending with the child
+    my $cache_paths = CASPlus::Model::RoleMemberPathCacheCollection->new;
+    $cache_paths->limit(
+        column   => 'cache_path',
+        operator => 'ENDSWITH',
+        value    => ':'.$args{child}->unique_id.':',
+    );
+
+    # Add new paths that add the parent to the end of the paths with the
+    # child.
+    my $new_path_cache = CASPlus::Model::RoleMemberPathCache->new;
+    while (my $path_cache = $cache_paths->next) {
+        my $cache_path = $path_cache->cache_path;
+
+        # Figure out which user to add the role to
+        my ($user_unique_id) = $cache_path =~ /^:(\d+):/;
+        my $user_profile_obj 
+            = CASPlus->get_profile_object_by_unique_id($user_unique_id);
+
+        # Create the appended cache path record
+        my $rid = $args{relationship}->id;
+        my $pid = $args{parent}->unique_id;
+
+        # Create the new cache path
+        $class->add_cache_path(
+            child      => $user_profile_obj,
+            parent     => $args{parent},
+            cache_path => $cache_path . "\@$rid:$pid:",
+        );
+    }
+}
+
+=head2 add_prepend_relationships
+
+  CASPlus::Util::Relationship->add_prepend_relationships(
+      child        => $child,
+      relationship => $relationship,
+      parent       => $parent,
+  );
+
+Finds all cached paths starting with the unique ID of C<$parent> and prepends this relationship to those paths. This will update any role memberships and cached paths as required.
+
+=cut
+
+sub add_prepend_relationships {
+    my $class = shift;
+    my %args  = @_;
+
+    # Find all cache paths starting with the parent
+    my $cache_paths = CASPlus::Model::RoleMemberPathCacheCollection->new;
+    $cache_paths->limit(
+        column   => 'cache_path',
+        operator => 'STARTSWITH',
+        value    => ':'.$args{parent}->unique_id.':',
+    );
+
+    # Add new paths that add the child to the start of the paths with the
+    # parent.
+    while (my $path_cache = $cache_paths->next) {
+        my $cache_path = $path_cache->cache_path;
+
+        # Load the last unique ID and see if it is a role
+        my ($last_id) = $cache_path =~ /:(\d+):$/;
+        my $last_profile_obj 
+            = CASPlus->get_profile_object_by_unique_id($last_id);
+
+        # Create the appended cache path record
+        my $cid = $args{child}->unique_id;
+        my $rid = $args{relationship}->id;
+
+        # Create the cache path
+        $class->add_cache_path(
+            child      => $args{child},
+            parent     => $last_profile_obj,
+            cache_path => ":$cid:\@$rid" . $cache_path,
+        );
+    }
+}
+
+=head2 recalculate_role_cache_for_object
+
+  CASPlus::Util::Relationship->recalculate_role_cache_for_object($object);
+
+Performs a role cache recalculation for every path related to C<$object>. This deletes all such paths and then traces the relationships in both directions, rebuilding adding any missing role memberships in the process. At the end, the database is checked for any role memberships with no related cached paths and deletes those found.
+
+=cut
+
+sub recalculate_role_cache_for_object {
+    my $class = shift;
+    my $obj   = shift;
+
+    $class->clear_all_cache_paths_for_object($obj);
+
+    # Remember Unique IDs we've already hit. Count any relationship we find
+    # reaching one, but don't traverse farther in case of loops.
+    my %unique_ids;
+
+    # Iterate through all relationships
+    my @open_list = (
+        [ child  => $obj ],
+        [ parent => $obj ],
+    );
+    while (my $item = shift @open_list) {
+        
+        # Search upward, $item->[1] is previously a parent, now a child
+        if ($item->[0] eq 'child') {
+            push @open_list,
+                map  { $unique_ids{$_->[1]}++; [ child => $_->[0] ] }
+                grep { !$unique_ids{$_->[1]} }
+                map  { [ $_, $_->unique_id ] }
+                $class->add_parent_relationships($item->[1]);
+        }
+
+        # Search downward, $item->[1] is previously a child, now a parent
+        else {
+            push @open_list, 
+                map  { $unique_ids{$_->[1]}++; [ parent => $_->[0] ] }
+                grep { !$unique_ids{$_->[1]} }
+                map  { [ $_, $_->unique_id ] }
+                $class->add_child_relationships($item->[1]);
+        }
+    }
+
+    $class->clear_abandoned_role_members;
+}
+
+=head2 clear_all_cache_paths_for_object
+
+  CASPlus::Util::Relationship->clear_all_cache_paths_for_object($object);
+
+All records stored in L<CASPlus::Model::RoleMemberPathCache> that name the given object's unique ID are deleted. This is performed prior to recalculation.
+
+=cut
+
+sub clear_all_cache_paths_for_object {
+    my $class  = shift;
+    my $object = shift;
+
+    # Find the list of cache paths that contain this object
+    my $cache_paths = CASPlus::Model::RoleMemberPathCacheCollection->new;
+    $cache_paths->limit(
+        column   => 'cache_path',
+        operator => 'MATCHES',
+        value    => ':'.$object->unique_id.':',
+    );
+
+    # Delete all those paths
+    while (my $path_cache = $cache_paths->next) {
+        $path_cache->delete;
+    }
+}
+
+=head2 add_parent_relationships
+
+  my @parents = CASPlus::Util::Relationship->add_parent_relationships($child);
+
+Finds all the parents of C<$child>. It adds each relationship to the role cache and returns a list of parents.
+
+=cut
+
+sub add_parent_relationships {
+    my ($class, $child) = @_;
+
+    my $profile = $child->profile_definition;
+    my $relationships = $profile->parent_relationships;
+
+    my @parents;
+
+    while (my $relationship = $relationships->next) {
+        my $method = $relationship->child_column->name;
+
+        # A one-to-many relationship
+        if ($relationship->many_parents and not $relationship->many_children) {
+
+            # Iterator through the parents
+            my $parents = $child->method;
+            while (my $parent = $parents->next) {
+                push @parents, $parent;
+                $class->add_relationship(
+                    child        => $child,
+                    relationship => $relationship,
+                    parent       => $parent,
+                );
+            }
+
+        }
+
+        # A one-to-one or many-to-one relationship
+        elsif (not $relationship->many_parents) {
+            my $parent = $child->$method;
+
+            # Only add it if it's set
+            if ($parent->id) {
+                push @parents, $parent;
+                $class->add_relationship(
+                    child        => $child,
+                    relationship => $relationship,
+                    parent       => $parent,
+                );
+            }
+        }
+
+        # A many-to-many relationship
+        else {
+            my $parent_links = $child->$method;
+
+            # Similar to one-to-many, but an extra jump
+            while (my $parent_link = $parent_links->next) {
+                my $parent = $parent_link->parent;
+                push @parents, $parent;
+                $class->add_relationship(
+                    child        => $child,
+                    relationship => $relationship,
+                    parent       => $parent,
+                );
+            }
+        }
+    }
+
+    return @parents;
+}
+
+=head2 add_child_relationships
+
+  my @children = CASPlus::Util::Relationship->add_child_relationships($parent);
+
+Finds all the children of C<$parent>. It adds each relationship to the role cache and returns a list of children.
+
+=cut
+
+sub add_child_relationships {
+    my ($class, $parent) = @_;
+
+    my $profile = $parent->profile_definition;
+    my $relationships = $profile->child_relationships;
+
+    my @children;
+
+    while (my $relationship = $relationships->next) {
+        my $method = $relationship->parent_column->name;
+
+        # A one-to-many relationship
+        if ($relationship->many_children and not $relationship->many_parents) {
+
+            # Iterator through the children
+            my $children = $parent->method;
+            while (my $child = $children->next) {
+                push @children, $child;
+                $class->add_relationship(
+                    child        => $child,
+                    relationship => $relationship,
+                    parent       => $parent,
+                );
+            }
+
+        }
+
+        # A one-to-one or many-to-one relationship
+        elsif (not $relationship->many_children) {
+            my $child = $parent->$method;
+
+            # Only add it if it's set
+            if ($child->id) {
+                push @children, $child;
+                $class->add_relationship(
+                    child        => $child,
+                    relationship => $relationship,
+                    parent       => $parent,
+                );
+            }
+        }
+
+        # A many-to-many relationship
+        else {
+            my $child_links = $parent->$method;
+
+            # Similar to one-to-many, but an extra jump
+            while (my $child_link = $child_links->next) {
+                my $child = $child_link->child;
+                push @children, $child;
+                $class->add_relationship(
+                    child        => $child,
+                    relationship => $relationship,
+                    parent       => $parent,
+                );
+            }
+        }
+    }
+
+    return @children;
+}
+
+=head2 clear_abandoned_role_members
+
+  CASPlus::Util::Relationship->clear_abandoned_role_members;
+
+Searches the role cache for any role memberships that no longer have any cached paths associated with them. These role memberships are removed.
+
+=cut
+
+sub clear_abandoned_role_members {
+    my $class = shift;
+
+    # Load the role memberships that now have an empty cache path
+    my $role_members = CASPlus::Model::RoleMemberCollection->new;
+    my $cache_paths_alias = $role_members->join(
+        column1 => 'id',
+        table2  => CASPlus::Model::RoleMemberPathCache->table,
+        column2 => 'role_member',
+    );
+    $role_members->group_by({
+        column => 'id',
+    });
+    $role_members->limit(
+        'alias'    => $cache_paths_alias,
+        'column'   => 'cache_path',
+        'function' => 'COUNT',
+        'value'    => 0,
+    );
+
+    # Delete these role memberships, they no longer hold
+    while (my $role_member = $role_members->next) {
+        $role_member->delete;
+    }
+}
+
+1;

Added: apps/CASPlus/trunk/t/40-many-to-many_relationships-explicit.t
==============================================================================
--- (empty file)
+++ apps/CASPlus/trunk/t/40-many-to-many_relationships-explicit.t	Tue Jun 26 09:15:36 2007
@@ -0,0 +1,604 @@
+#!/usr/bin/env perl
+use strict;
+use warnings;
+
+use List::Util qw/ sum /;
+use Jifty::Test tests => 1;
+
+my $system_user = CASPlus::CurrentUser->superuser;
+
+my $role_profile = CASPlus::Model::Profile->new(current_user => $system_user);
+$role_profile->create(
+    name         => 'generic role',
+    profile_type => 'role',
+);
+ok($role_profile->id, 'created a role profile');
+
+my $other_profile = CASPlus::Model::Profile->new(current_user => $system_user);
+$other_profile->create(
+    name         => 'generic other',
+    profile_type => 'other',
+);
+ok($other_profile->id, 'created an other profile');
+
+my $user_profile = CASPlus::Model::Profile->new(current_user => $system_user);
+$user_profile->create(
+    name         => 'generic user',
+    profile_type => 'user',
+);
+ok($user_profile->id, 'created a user profile');
+
+my $ur_relationship = CASPlus::Model::ProfileRelationship->new(current_user => $system_user);
+$ur_relationship->create(
+    name                        => 'user-role',
+    parent_name                 => 'my_roles',
+    child_name                  => 'my_users',
+    relation_parent             => $role_profile,
+    relation_child              => $user_profile,
+    many_parents                => 1,
+    many_children               => 1,
+    roles_propagate_to_children => 1,
+);
+ok($ur_relationship->id, 'created a user-role relationship');
+
+my $uo_relationship = CASPlus::Model::ProfileRelationship->new(current_user => $system_user);
+$uo_relationship->create(
+    name                        => 'user-other',
+    parent_name                 => 'my_others',
+    child_name                  => 'my_users',
+    relation_parent             => $other_profile,
+    relation_child              => $user_profile,
+    many_parents                => 1,
+    many_children               => 1,
+    roles_propagate_to_children => 1,
+);
+ok($ur_relationship->id, 'created a user-other relationship');
+
+my $rr_relationship = CASPlus::Model::ProfileRelationship->new(current_user => $system_user);
+$rr_relationship->create(
+    name                        => 'role-role',
+    parent_name                 => 'my_parent_roles',
+    child_name                  => 'my_child_roles',
+    relation_parent             => $role_profile,
+    relation_child              => $role_profile,
+    many_parents                => 1,
+    many_children               => 1,
+    roles_propagate_to_children => 1,
+);
+ok($rr_relationship->id, 'created a role-role relationship');
+
+my $or_relationship = CASPlus::Model::ProfileRelationship->new(current_user => $system_user);
+$or_relationship->create(
+    name                        => 'other-role',
+    parent_name                 => 'my_roles',
+    child_name                  => 'my_others',
+    relation_parent             => $role_profile,
+    relation_child              => $other_profile,
+    many_parents                => 1,
+    many_children               => 1,
+    roles_propagate_to_children => 1,
+);
+ok($or_relationship->id, 'created a other-role relationship');
+
+my $user = CASPlus::Model::User->new(current_user => $system_user);
+my $role = CASPlus::Model::Role->new(current_user => $system_user);
+my $permission = CASPlus::Model::ProfilePermission->new(current_user => $system_user);
+
+my $user_class  = $user_profile->record_class;
+my $other_class = $other_profile->record_class;
+my $role_class  = $role_profile->record_class;
+
+my $ur_class = $ur_relationship->record_class;
+my $uo_class = $uo_relationship->record_class;
+my $or_class = $or_relationship->record_class;
+my $rr_class = $rr_relationship->record_class;
+
+# Create some test objects
+
+{ # user-1 : unique ID 1
+    $user->create(
+        username => 'user-1',
+        password => 'test',
+    );
+    ok($user->id, 'created user-1');
+    my $user_obj = $user_class->new(current_user => $system_user);
+    $user_obj->create(
+        user_object => $user,
+    );
+    ok($user->id, 'created user-1 profile');
+    ok($user_obj->unique_id, 'created user-1 unique ID');
+}
+
+{ # role-1 : unique ID 2
+    $role->create(
+        name => 'role-1',
+    );
+    ok($role->id, 'created role-1');
+    my $role_obj = $role_class->new(current_user => $system_user);
+    $role_obj->create(
+        role_object => $role,
+    );
+    ok($role_obj->id, 'created role-1 profile');
+    ok($role_obj->unique_id, 'created role-1 unique ID');
+}
+
+{ # role-2 : unique ID 3
+    $role->create(
+        name => 'role-2',
+    );
+    ok($role->id, 'created role-2');
+    my $role_obj = $role_class->new(current_user => $system_user);
+    $role_obj->create(
+        role_object => $role,
+    );
+    ok($role_obj->id, 'created role-2 profile');
+    ok($role_obj->unique_id, 'created role-2 unique ID');
+}
+
+{ # <other> : unique ID 4
+    my $other_obj = $other_class->new(current_user => $system_user);
+    $other_obj->create();
+    ok($other_obj->id, 'created first <other> profile');
+    ok($other_obj->unique_id, 'created first <other> unique ID');
+}
+
+{ # user-2 : unique ID 5
+    $user->create(
+        username => 'user-2',
+        password => 'test',
+    );
+    ok($user->id, 'created user-2');
+    my $user_obj = $user_class->new(current_user => $system_user);
+    $user_obj->create(
+        user_object => $user,
+    );
+    ok($user_obj->id, 'created user-2 profile');
+    ok($user_obj->unique_id, 'created user-2 unique ID');
+}
+
+{ # <other> : unique ID 6
+    my $other_obj = $other_class->new(current_user => $system_user);
+    $other_obj->create();
+    ok($other_obj->id, 'created second <other> profile');
+    ok($other_obj->unique_id, 'created second <other> unique ID');
+}
+
+{ # role-3 : unique ID 7
+    $role->create(
+        name => 'role-3',
+    );
+    ok($role->id, 'created role-3');
+    my $role_obj = $role_class->new(current_user => $system_user);
+    $role_obj->create(
+        role_object => $role,
+    );
+    ok($role_obj->id, 'created role-3 profile');
+    ok($role_obj->unique_id, 'created role-3 unique ID');
+}
+
+{ # user-3 : unique ID 8
+    $user->create(
+        username => 'user-3',
+        password => 'test',
+    );
+    ok($user->id, 'created user-3');
+    my $user_obj = $user_class->new(current_user => $system_user);
+    $user_obj->create(
+        user_object => $user,
+    );
+    ok($user_obj->id, 'created user-3 profile');
+    ok($user_obj->unique_id, 'created user-3 unique ID');
+}
+
+{ # role-4 : unique ID 9
+    $role->create(
+        name => 'role-4',
+    );
+    ok($role->id, 'created role-4');
+    my $role_obj = $role_class->new(current_user => $system_user);
+    $role_obj->create(
+        role_object => $role,
+    );
+    ok($role_obj->id, 'created role-4 profile');
+    ok($role_obj->unique_id, 'created role-4 unique ID');
+}
+
+{ # user-4 : unique ID 10
+    $user->create(
+        username => 'user-4',
+        password => 'test',
+    );
+    ok($user->id, 'created user-4');
+    my $user_obj = $user_class->new(current_user => $system_user);
+    $user_obj->create(
+        user_object => $user,
+    );
+    ok($user_obj->id, 'created user-4 profile');
+    ok($user_obj->unique_id, 'created user-4 unique ID');
+}
+
+{ # role-5 : unique ID 11
+    $role->create(
+        name => 'role-5',
+    );
+    ok($role->id, 'created role-5');
+    my $role_obj = $role_class->new(current_user => $system_user);
+    $role_obj->create(
+        role_object => $role,
+    );
+    ok($role_obj->id, 'created role-5 profile');
+    ok($role_obj->unique_id, 'created role-5 unique ID');
+}
+
+# Create the relationships
+
+{ # user-2 - first <other> relationship
+    my $user_obj = $user_class->new(current_user => $system_user);
+    $user_obj->load(2);
+    ok($user_obj->id, 'loaded user-2');
+    
+    my $other_obj = $other_class->new(current_user => $system_user);
+    $other_obj->load(1);
+    ok($other_obj->id, 'loaded first <other>');
+
+    my $uo_relationship = $uo_class->new(current_user => $system_user);
+    $uo_relationship->create(
+        parent => $other_obj,
+        child  => $user_obj,
+    );
+    ok($uo_relationship->id, 'created user-2 - first <other> relationship');
+}
+
+{ # first <other> - role-2 relationship
+    my $role_obj = $role_class->new(current_user => $system_user);
+    $role_obj->load(2);
+    ok($role_obj->id, 'loaded role-2');
+    
+    my $other_obj = $other_class->new(current_user => $system_user);
+    $other_obj->load(1);
+    ok($other_obj->id, 'loaded first <other>');
+
+    my $or_relationship = $or_class->new(current_user => $system_user);
+    $or_relationship->create(
+        parent => $role_obj,
+        child  => $other_obj,
+    );
+    ok($or_relationship->id, 'created first <other> - role-2 relationship');
+}
+
+{ # user-4 - second <other> relationship
+    my $user_obj = $user_class->new(current_user => $system_user);
+    $user_obj->load(4);
+    ok($user_obj->id, 'loaded user-4');
+    
+    my $other_obj = $other_class->new(current_user => $system_user);
+    $other_obj->load(2);
+    ok($other_obj->id, 'loaded second <other>');
+
+    my $uo_relationship = $uo_class->new(current_user => $system_user);
+    $uo_relationship->create(
+        parent => $other_obj,
+        child  => $user_obj,
+    );
+    ok($uo_relationship->id, 'created user-4 - second <other> relationship');
+}
+
+{ # role-1 - role-3 relationship
+    my $parent_role_obj = $role_class->new(current_user => $system_user);
+    $parent_role_obj->load(3);
+    ok($parent_role_obj->id, 'loaded role-3');
+    
+    my $child_role_obj = $role_class->new(current_user => $system_user);
+    $child_role_obj->load(1);
+    ok($child_role_obj->id, 'loaded role-1');
+
+    my $rr_relationship = $rr_class->new(current_user => $system_user);
+    $rr_relationship->create(
+        parent => $parent_role_obj,
+        child  => $child_role_obj,
+    );
+    ok($rr_relationship->id, 'created role-1 - role-3 relationship');
+}
+
+{ # user-3 - role-1 relationship
+    my $user_obj = $user_class->new(current_user => $system_user);
+    $user_obj->load(3);
+    ok($user_obj->id, 'loaded user-3');
+    
+    my $role_obj = $role_class->new(current_user => $system_user);
+    $role_obj->load(1);
+    ok($role_obj->id, 'loaded role-1');
+
+    my $ur_relationship = $ur_class->new(current_user => $system_user);
+    $ur_relationship->create(
+        parent => $role_obj,
+        child  => $user_obj,
+    );
+    ok($ur_relationship->id, 'created user-3 - role-1 relationship');
+}
+
+{ # first <other> - role-4 relationship
+    my $role_obj = $role_class->new(current_user => $system_user);
+    $role_obj->load(4);
+    ok($role_obj->id, 'loaded role-4');
+    
+    my $other_obj = $other_class->new(current_user => $system_user);
+    $other_obj->load(1);
+    ok($other_obj->id, 'loaded first <other>');
+
+    my $or_relationship = $or_class->new(current_user => $system_user);
+    $or_relationship->create(
+        parent => $role_obj,
+        child  => $other_obj,
+    );
+    ok($or_relationship->id, 'created first <other> - role-4 relationship');
+}
+
+{ # user-1 - role-1 relationship
+    my $user_obj = $user_class->new(current_user => $system_user);
+    $user_obj->load(1);
+    ok($user_obj->id, 'loaded user-1');
+    
+    my $role_obj = $role_class->new(current_user => $system_user);
+    $role_obj->load(1);
+    ok($role_obj->id, 'loaded role-1');
+
+    my $ur_relationship = $ur_class->new(current_user => $system_user);
+    $ur_relationship->create(
+        parent => $role_obj,
+        child  => $user_obj,
+    );
+    ok($ur_relationship->id, 'created user-1 - role-1 relationship');
+}
+
+{ # user-3 - first <other> relationship
+    my $user_obj = $user_class->new(current_user => $system_user);
+    $user_obj->load(3);
+    ok($user_obj->id, 'loaded user-3');
+    
+    my $other_obj = $other_class->new(current_user => $system_user);
+    $other_obj->load(1);
+    ok($other_obj->id, 'loaded first <other>');
+
+    my $uo_relationship = $uo_class->new(current_user => $system_user);
+    $uo_relationship->create(
+        parent => $other_obj,
+        child  => $user_obj,
+    );
+    ok($uo_relationship->id, 'created user-3 - first <other> relationship');
+}
+
+{ # role-1 - role-2 relationship
+    my $parent_role_obj = $role_class->new(current_user => $system_user);
+    $parent_role_obj->load(2);
+    ok($parent_role_obj->id, 'loaded role-2');
+    
+    my $child_role_obj = $role_class->new(current_user => $system_user);
+    $child_role_obj->load(1);
+    ok($child_role_obj->id, 'loaded role-1');
+
+    my $rr_relationship = $rr_class->new(current_user => $system_user);
+    $rr_relationship->create(
+        parent => $parent_role_obj,
+        child  => $child_role_obj,
+    );
+    ok($rr_relationship->id, 'created role-1 - role-2 relationship');
+}
+
+# Create the permissions
+
+{ # permissions for role-1
+    $role->load(1);
+    is($role->id, 1, 'loaded role-1');
+
+    my $profile_obj = CASPlus->get_profile_object_by_unique_id(int(rand(11)) + 1);
+    ok($profile_obj->id, 'loaded a random profile object');
+
+    $permission->create(
+        the_role          => $role,
+        profile           => $profile_obj,
+        may_create        => int(rand(1)),
+        may_create_my_own => int(rand(1)),
+        may_read          => int(rand(1)),
+        may_read_my_own   => int(rand(1)),
+        may_write         => int(rand(1)),
+        may_write_my_own  => int(rand(1)),
+        may_delete        => int(rand(1)),
+        may_delete_my_own => int(rand(1)),
+    );
+    ok($permission->id, "created a permission for role-1");
+}
+
+{ # permissions for role-2
+    $role->load(2);
+    is($role->id, 2, 'loaded role-2');
+
+    for ( 1 .. 3 ) {
+        my $profile_obj = CASPlus->get_profile_object_by_unique_id(int(rand(11)) + 1);
+        ok($profile_obj->id, 'loaded a random profile object');
+
+        $permission->create(
+            the_role          => $role,
+            profile           => $profile_obj,
+            may_create        => int(rand(1)),
+            may_create_my_own => int(rand(1)),
+            may_read          => int(rand(1)),
+            may_read_my_own   => int(rand(1)),
+            may_write         => int(rand(1)),
+            may_write_my_own  => int(rand(1)),
+            may_delete        => int(rand(1)),
+            may_delete_my_own => int(rand(1)),
+        );
+        ok($permission->id, "created a permission for role-2");
+    }
+}
+
+{ # permissions for role-3
+    $role->load(3);
+    is($role->id, 3, 'loaded role-3');
+
+    for ( 1 .. 3 ) {
+        my $profile_obj = CASPlus->get_profile_object_by_unique_id(int(rand(11)) + 1);
+        ok($profile_obj->id, 'loaded a random profile object');
+
+        $permission->create(
+            the_role          => $role,
+            profile           => $profile_obj,
+            may_create        => int(rand(1)),
+            may_create_my_own => int(rand(1)),
+            may_read          => int(rand(1)),
+            may_read_my_own   => int(rand(1)),
+            may_write         => int(rand(1)),
+            may_write_my_own  => int(rand(1)),
+            may_delete        => int(rand(1)),
+            may_delete_my_own => int(rand(1)),
+        );
+        ok($permission->id, "created a permission for role-3");
+    }
+}
+
+{ # permissions for role-4
+    $role->load(4);
+    is($role->id, 4, 'loaded role-4');
+
+    for ( 1 .. 3 ) {
+        my $profile_obj = CASPlus->get_profile_object_by_unique_id(int(rand(11)) + 1);
+        ok($profile_obj->id, 'loaded a random profile object');
+
+        $permission->create(
+            the_role          => $role,
+            profile           => $profile_obj,
+            may_create        => int(rand(1)),
+            may_create_my_own => int(rand(1)),
+            may_read          => int(rand(1)),
+            may_read_my_own   => int(rand(1)),
+            may_write         => int(rand(1)),
+            may_write_my_own  => int(rand(1)),
+            may_delete        => int(rand(1)),
+            may_delete_my_own => int(rand(1)),
+        );
+        ok($permission->id, "created a permission for role-4");
+    }
+}
+
+{ # permissions for role-5
+    $role->load(5);
+    is($role->id, 5, 'loaded role-4');
+
+    for ( 1 .. 3 ) {
+        my $profile_obj = CASPlus->get_profile_object_by_unique_id(int(rand(11) + 1));
+        ok($profile_obj->id, 'loaded a random profile object');
+
+        $permission->create(
+            the_role          => $role,
+            profile           => $profile_obj,
+            may_create        => int(rand(1)),
+            may_create_my_own => int(rand(1)),
+            may_read          => int(rand(1)),
+            may_read_my_own   => int(rand(1)),
+            may_write         => int(rand(1)),
+            may_write_my_own  => int(rand(1)),
+            may_delete        => int(rand(1)),
+            may_delete_my_own => int(rand(1)),
+        );
+        ok($permission->id, "created a permission for role-5");
+    }
+}
+
+my %user_results = (
+    1 => {
+        roles => [ 1, 2, 3 ],
+        perms => [ 1, 2, 3, 4, 5, 6, 7 ],
+    },
+    2 => {
+        roles => [ 2, 4 ],
+        perms => [ 2, 3, 4, 8, 9, 10 ],
+    },
+    3 => {
+        roles => [ 1, 2, 3, 4 ],
+        perms => [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ],
+    },
+    4 => {
+        roles => [ ],
+        perms => [ ],
+    },
+);
+
+my %role_members = (
+    '' => [
+        ':5:@2:4:',
+        ':4:@4:3:',
+        ':10:@2:6:',
+        ':2:@3:7:',
+        ':4:@4:9:',
+        ':8:@2:4:',
+        ':2:@3:3:',
+    ],
+    1 => [
+        ':5:@2:4:@4:3:',
+    ],
+    2 => [
+        ':8:@1:2:',
+    ],
+    3 => [
+        ':8:@1:2:@3:7:',
+    ],
+    4 => [
+        ':5:@2:4:@4:9:',
+    ],
+    5 => [
+        ':1:@1:2:',
+    ],
+    6 => [
+        ':1:@1:2:@3:7:',
+    ],
+    7 => [
+        ':8:@2:4:@4:3:',
+        ':8:@1:2:@3:3:',
+    ],
+    8 => [
+        ':8:@2:4:@4:9:',
+    ],
+    9 => [
+        ':1:@1:2:@3:3:',
+    ],
+);
+
+# Run the tests on the role cache built on-the-fly
+my $role_members = CASPlus::Model::RoleMemberCollection->new(current_user => $system_user);
+$role_members->unlimit;
+is($role_members->count_all, (scalar keys %role_members) - 1, 'there are the right number of role members');
+
+my $cache_paths = CASPlus::Model::RoleMemberPathCacheCollection->new(current_user => $system_user);
+$cache_paths->unlimit;
+is($cache_paths->count_all, (sum map { scalar @$_ } values %role_members), 'there are the right number of cache paths');
+
+#while (my $path_cache = $cache_paths->next) {
+#    diag($path_cache->cache_path);
+#}
+
+my $profiles = CASPlus::Model::ProfileCollection->new(current_user => $system_user);
+$profiles->unlimit;
+while (my $profile = $profiles->next) {
+    my $collection_class = $profile->record_class->collection_class;
+    my $objects = $collection_class->new(current_user => $system_user);
+    $objects->unlimit;
+    while (my $object = $objects->next) {
+        $object->recalculate_role_cache;
+
+        # Run the tests on the role cache built by total recalculation
+        $role_members = CASPlus::Model::RoleMemberCollection->new(current_user => $system_user);
+        $role_members->unlimit;
+        is($role_members->count_all, (scalar keys %role_members) - 1, 'there are the right number of role members');
+
+        $cache_paths = CASPlus::Model::RoleMemberPathCacheCollection->new(current_user => $system_user);
+        $cache_paths->unlimit;
+        is($cache_paths->count_all, (sum map { scalar @$_ } values %role_members), 'there are the right number of cache paths');
+    }
+
+}
+
+
+#while (my $path_cache = $cache_paths->next) {
+#    diag($path_cache->cache_path);
+#}


More information about the Jifty-commit mailing list