diff --git a/og.module b/og.module index cc6228f..8dd03a1 100644 --- a/og.module +++ b/og.module @@ -898,7 +898,7 @@ function og_entity_delete($entity, $entity_type) { list($id, , $bundle) = entity_extract_ids($entity_type, $entity); if (og_is_group($entity_type, $entity)) { og_delete_user_roles_by_group($entity_type, $entity); - og_membership_delete_by_group($entity_type, $id); + og_membership_delete_by_group($entity_type, $entity); } if (og_is_group_content_type($entity_type, $bundle)) { // As the field attachers are called after hook_entity_presave() we @@ -1531,14 +1531,172 @@ function og_membership_delete_multiple($ids = array()) { } /** - * Delete all OG memberships by group. + * Implements hook_advanced_queue_info(). */ -function og_membership_delete_by_group($group_type, $gid) { +function og_advanced_queue_info() { + $items['og_membership_orphans'] = array( + 'worker callback' => 'og_membership_orphans_worker', + // Message-subscribe will deal itself with deleting claimed items. + 'delete when completed' => FALSE, + ); + return $items; +} + +/** + * Advanced queue worker; Process a queue item. + * + * Delete memberships, and if needed all related group-content. + */ +function og_membership_orphans_worker($item, $end_time = FALSE) { + $queue = DrupalQueue::get('og_membership_orphans'); + $data = $item->data; + + $group_type = $data['group_type']; + $gid = $data['gid']; + $query = new EntityFieldQuery(); $result = $query ->entityCondition('entity_type', 'og_membership') ->propertyCondition('group_type', $group_type, '=') ->propertyCondition('gid', $gid, '=') + ->propertyOrderBy('id') + ->range(0, 10) + ->execute(); + + if (empty($result['og_membership'])) { + // We can delete the item. + $queue->deleteItem($item); + return TRUE; + } + + $ids = array_keys($result['og_membership']); + if ($data['orphans']['move']) { + _og_orphans_move($ids, $data['orphans']['move']['group_type'], $data['orphans']['move']['gid']); + // Delete the item. + $queue->deleteItem($item); + } + elseif ($data['orphans']['delete']) { + _og_orphans_delete($ids); + // Release the item. + $queue->releaseItem($item); + } + return TRUE; +} + +/** + * Helper function to delete orphan group-content. + * + * @param $ids + * Array of OG membership IDs. + * + * @see og_membership_delete_by_group_worker() + */ +function _og_orphans_delete($ids) { + // Get all the group-content that is now orphan. + $orphans = array(); + $og_memberships = og_membership_load_multiple($ids); + + foreach ($og_memberships as $og_membership) { + $entity_type = $og_membership->entity_type; + $id = $og_membership->etid; + // Don't delete users. + if ($entity_type == 'user') { + continue; + } + $entity_groups = og_get_entity_groups($entity_type, $id); + // Orphan node can be relate to only one type of entity group. + if (count($entity_groups) == 1) { + $gids = reset($entity_groups); + // Orphan node can be relate to only one node. + if (count($gids) > 1) { + continue; + } + } + $orphans[$entity_type][] = $id; + } + + if ($orphans) { + foreach ($orphans as $entity_type => $ids) { + entity_delete_multiple($entity_type, $ids); + } + } + + // Delete the OG memberships. + og_membership_delete_multiple($ids); +} + +/** + * Helper function to move orphan group-content to another group. + * + * @param $ids + * Array of OG membership IDs. + * + * @see og_membership_delete_by_group_worker() + */ +function _og_orphans_move($ids, $group_type, $gid) { + if (!og_is_group($group_type, $gid)) { + $params = array( + '@group-type' => $group_type, + '@gid' => $gid, + ); + throw new OgException(format_string('Cannot move orphan group-content to @group-type - @gid, as it is not a valid group.', $params)); + } + + $og_memberships = og_membership_load_multiple($ids); + foreach ($og_memberships as $og_membership) { + $entity_type = $og_membership->entity_type; + $id = $og_membership->etid; + if (count(og_get_entity_groups($entity_type, $id)) > 1) { + continue; + } + $og_membership->group_type = $group_type; + $og_membership->gid = $gid; + $og_membership->save(); + } +} + +/** + * Register memberships for deletion. + * + * if the property "skip_og_membership_delete_by_group" exists on the + * entity, this function will return early, and allow other implementing + * modules to deal with the deletion logic. + * + * @param $entity_type + * The group type. + * @param $entity + * The group entity object. + */ +function og_membership_delete_by_group($entity_type, $entity) { + if (!empty($entity->skip_og_membership_delete_by_group)) { + return; + } + + list($gid) = entity_extract_ids($entity_type, $entity); + if (variable_get('og_use_queue', FALSE) && module_exists('advancedqueue')) { + $queue = DrupalQueue::get('og_membership_orphans'); + // Add item to the queue. + $task = array( + 'group_type' => $entity_type, + 'gid' => $gid, + // Allow implementing modules to determine the disposition (e.g. delete + // orphan group content). + 'orphans' => array( + 'delete' => isset($entity->og_orphans['delete']) ? $entity->og_orphans['delete'] : variable_get('og_orphans_delete', FALSE), + 'move' => isset($entity->og_orphans['move']) ? $entity->og_orphans['move'] : array(), + ), + ); + + // Exit now, as the task will be processed via advanced-queue. + return $queue->createItem($task); + } + + // No scalable solution was chosen, so just delete OG memberships. + $query = new EntityFieldQuery(); + $result = $query + ->entityCondition('entity_type', 'og_membership') + ->propertyCondition('group_type', $entity_type, '=') + ->propertyCondition('gid', $gid, '=') ->execute(); if (!empty($result['og_membership'])) { diff --git a/og.test b/og.test index 6c9ffd0..c2c68f4 100644 --- a/og.test +++ b/og.test @@ -1568,3 +1568,105 @@ class OgBehaviorHandlerTestCase extends DrupalWebTestCase { $this->assertEqual(array_values($gids['entity_test']), array($entity1->pid), 'Widget behavior was skipped and removed group association as expected.'); } } + +/** + * Testing for deleting orphans group content. + */ +class OgDeleteOrphansTestCase extends DrupalWebTestCase { + + public $group_type; + public $node_type; + + public static function getInfo() { + return array( + 'name' => 'OG orphan delete', + 'description' => 'Verifying for deleting orphan group content.', + 'group' => 'Organic groups', + 'dependencies' => array('advancedqueue'), + ); + } + + function setUp() { + parent::setUp('og_test', 'advancedqueue'); + + // Create a group content type. + $group = $this->drupalCreateContentType(); + og_create_field(OG_GROUP_FIELD, 'node', $group->type); + $this->group_type = $group->type; + + // Create group audience content type. + $type = $this->drupalCreateContentType(); + $this->node_type = $type->type; + + // Add OG audience field to the audience content type. + $og_field = og_fields_info(OG_AUDIENCE_FIELD); + $og_field['field']['settings']['target_type'] = 'node'; + og_create_field(OG_AUDIENCE_FIELD, 'node', $type->type, $og_field); + + // Set the setting for delete a group content when deleting group. + variable_set('og_orphans_delete', TRUE); + variable_set('og_use_queue', TRUE); + } + + /** + * Testing two things: + * When deleting a group, the node of the group will be deleted. + * A binded node with the deleted group and another group won't be deleted. + */ + function testDeleteGroup() { + // Creating two groups. + $first_group = $this->drupalCreateNode(array('type' => $this->group_type)); + $second_group = $this->drupalCreateNode(array('type' => $this->group_type)); + + // Create two nodes. + $first_node = $this->drupalCreateNode(array('type' => $this->node_type)); + og_group('node', $first_group, array('entity_type' => 'node', 'entity' => $first_node)); + og_group('node', $second_group, array('entity_type' => 'node', 'entity' => $first_node)); + + $second_node = $this->drupalCreateNode(array('type' => $this->node_type)); + og_group('node', $first_group, array('entity_type' => 'node', 'entity' => $second_node)); + + // Delete the group. + node_delete($first_group->nid); + + // Execute manually the queue worker. + $queue = DrupalQueue::get('og_membership_orphans'); + $item = $queue->claimItem(); + og_membership_orphans_worker($item); + + // Load the nodes we used during the test. + $first_node = node_load($first_node->nid); + $second_node = node_load($second_node->nid); + + // Verify the none orphan node wasn't deleted. + $this->assertTrue($first_node, "The second node is realted to another group and deleted."); + // Verify the orphan node deleted. + $this->assertFalse($second_node, "The orphan node deleted."); + } + + /** + * Testing the moving of the node to another group when deleting a group. + */ + function testMoveOrphans() { + // Creating two groups. + $first_group = $this->drupalCreateNode(array('type' => $this->group_type, 'title' => 'move')); + $second_group = $this->drupalCreateNode(array('type' => $this->group_type)); + + // Create a group and relate it to one group. + $first_node = $this->drupalCreateNode(array('type' => $this->node_type)); + og_group('node', $first_group, array('entity_type' => 'node', 'entity' => $first_node)); + + // Delete the group. + node_delete($first_group->nid); + + // Execute manually the queue worker. + $queue = DrupalQueue::get('og_membership_orphans'); + $item = $queue->claimItem(); + og_membership_orphans_worker($item); + + // Load the node from the DB and verify he moved to the other group. + $wrapper = entity_metadata_wrapper('node', $first_node->nid); + + $this->assertTrue($wrapper->{OG_AUDIENCE_FIELD}->get(0)->getIdentifier() == $second_group->nid, "The node of the group moved to another group."); + } +} diff --git a/og_ui/og_ui.admin.inc b/og_ui/og_ui.admin.inc index 4db7cfe..fa75f17 100644 --- a/og_ui/og_ui.admin.inc +++ b/og_ui/og_ui.admin.inc @@ -73,6 +73,32 @@ function og_ui_admin_settings($form_state) { '#access' => module_exists('features'), ); + $form['og_use_queue'] = array( + '#type' => 'checkbox', + '#title' => t('Use queue'), + '#description' => t('Use "Advacned-Queue" module to process operations such as deleting memberships when groups are deleted.', array('@url' => 'http://drupal.org/project/advancedqueue')), + '#default_value' => variable_get('og_use_queue', FALSE), + '#disabled' => !module_exists('advancedqueue'), + ); + + $form['og_orphans_delete'] = array( + '#type' => 'checkbox', + '#title' => t('Delete orphans'), + '#description' => t('Delete "Orphan" group-content (not including useres), when the group is deleted.'), + '#default_value' => variable_get('og_orphans_delete', FALSE), + '#disabled' => !module_exists('advancedqueue'), + '#states' => array( + 'visible' => array( + ':input[name="og_use_queue"]' => array('checked' => TRUE), + ), + ), + '#attributes' => array( + 'class' => array('entityreference-settings'), + ), + ); + + // Re-use Entity-reference CSS for indentation. + $form['#attached']['css'][] = drupal_get_path('module', 'entityreference') . '/entityreference.admin.css'; return system_settings_form($form); } diff --git a/tests/og_test.module b/tests/og_test.module index 2908aad..95c3d4c 100644 --- a/tests/og_test.module +++ b/tests/og_test.module @@ -8,9 +8,55 @@ /** * Implements hook_node_presave(). */ -function og_node_presave($node) { +function og_test_node_presave($node) { if (!empty($node->nid) && !empty($node->og_group_on_save)) { $values = $node->og_group_on_save; og_group($values['group_type'], $values['gid'], array('entity_type' => 'node', 'entity' => $node)); } } + +/** + * Implements hook_module_implements_alter(). + */ +function og_test_module_implements_alter(&$implementations, $hook) { + if ($hook != 'entity_delete') { + return; + } + + // Switch the orders of the implementations. + $og = $implementations['og']; + $og_test = $implementations['og_test']; + + unset($implementations['og'], $implementations['og_test']); + + $implementations['og_test'] = $og_test; + $implementations['og'] = $og; +} + +/** + * Implements hook_entity_delete(). + */ +function og_test_entity_delete($entity, $type) { + if (!og_is_group($type, $entity) || $entity->title != 'move' ) { + return; + } + + $query = new EntityFieldQuery(); + $result = $query + ->entityCondition('entity_type', 'node') + ->propertyCondition('nid', $entity->nid, '<>') + ->execute(); + + if (empty($result['node'])) { + return; + } + + $nid = reset(array_keys($result['node'])); + + $entity->og_orphans = array( + 'move' => array( + 'group_type' => 'node', + 'gid' => $nid, + ), + ); +}