diff --git a/entityreference.info b/entityreference.info index 9772c69..24852f3 100644 --- a/entityreference.info +++ b/entityreference.info @@ -22,3 +22,4 @@ files[] = tests/entityreference.handlers.test files[] = tests/entityreference.taxonomy.test files[] = tests/entityreference.admin.test files[] = tests/entityreference.feeds.test +files[] = tests/entityreference.revisions.test diff --git a/entityreference.install b/entityreference.install index 4e248ef..2f79947 100644 --- a/entityreference.install +++ b/entityreference.install @@ -18,11 +18,17 @@ function entityreference_field_schema($field) { $schema = array( 'columns' => array( 'target_id' => array( - 'description' => 'The id of the target entity.', + 'description' => 'The ID of the target entity.', 'type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, ), + 'revision_id' => array( + 'description' => 'The revision ID of the target entity.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => FALSE, + ), ), 'indexes' => array( 'target_id' => array('target_id'), @@ -161,4 +167,34 @@ function entityreference_update_7002() { 'not null' => TRUE, )); } -} \ No newline at end of file +} + +/** + * Add "revision-id" to the field schema. + */ +function entityreference_update_7003() { + if (!module_exists('field_sql_storage')) { + return; + } + foreach (field_info_fields() as $field_name => $field) { + if ($field['type'] != 'entityreference' || $field['storage']['type'] !== 'field_sql_storage') { + // Not an entity reference field. + continue; + } + + // Add the new column. + $field = field_info_field($field_name); + $table_name = _field_sql_storage_tablename($field); + $revision_name = _field_sql_storage_revision_tablename($field); + + $spec = array( + 'description' => 'The revision ID of the target entity.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => FALSE, + ); + + db_add_field($table_name, $field_name . '_revision_id', $spec); + db_add_field($revision_name, $field_name . '_revision_id', $spec); + } +} diff --git a/entityreference.module b/entityreference.module index bdcb562..328cd26 100644 --- a/entityreference.module +++ b/entityreference.module @@ -243,6 +243,40 @@ function entityreference_field_validate($entity_type, $entity, $field, $instance * Adds the target type to the field data structure when saving. */ function entityreference_field_presave($entity_type, $entity, $field, $instance, $langcode, &$items) { + // Reference the right revision(s) if revision locking is enabled. + if (!empty($field['settings']['handler_settings']['lock_revision'])) { + $target_type = $field['settings']['target_type']; + + // If the entity is being updated, retrieve the original field items. + $original_items = isset($entity->original) ? field_get_items($entity_type, $entity->original, $field['field_name']) : array(); + + foreach ($items as $key => $item) { + // Load the referenced entity revision if it was explicitly set. + $referenced_entity_revision = FALSE; + if (isset($item['revision_id']) && $item['revision_id']) { + $referenced_entity_revision = entity_revision_load($target_type, $item['revision_id']); + } + // Load the entity from the database if it wasn't set. + if (!$referenced_entity_revision) { + $referenced_entity_revision = entity_load_single($target_type, $item['target_id']); + } + list(, $referenced_entity_revision_id) = entity_extract_ids($target_type, $referenced_entity_revision); + // Find the original item, which does not have the same delta per se. + $original_revision_id = FALSE; + foreach ($original_items as $original_item) { + if ($original_item['target_id'] == $item['target_id']) { + $original_revision_id = isset($original_item['revision_id']) ? $original_item['revision_id'] : FALSE; + } + } + if (isset($referenced_entity_revision_id) && is_numeric($referenced_entity_revision_id) && !$original_revision_id) { + $items[$key]['revision_id'] = $referenced_entity_revision_id; + } + elseif ($original_revision_id) { + $items[$key]['revision_id'] = $original_revision_id; + } + } + } + // Invoke the behaviors. foreach (entityreference_get_behavior_handlers($field, $instance) as $handler) { $handler->presave($entity_type, $entity, $field, $instance, $langcode, $items); @@ -672,6 +706,15 @@ function entityreference_field_property_callback(&$info, $entity_type, $field, $ // Then apply the default. entity_metadata_field_default_property_callback($info, $entity_type, $field, $instance, $field_type); + // If the entity reference is locked to a revision, load that revision, not + // the current one. + if (!empty($field['settings']['handler_settings']['lock_revision'])) { + $name = $field['field_name']; + $property = &$info[$entity_type]['bundles'][$instance['bundle']]['properties'][$name]; + $property['getter callback'] = 'entityreference_metadata_field_get_revision_data'; + $property['setter callback'] = 'entityreference_metadata_field_set_revision_data'; + } + // Invoke the behaviors to allow them to change the properties. foreach (entityreference_get_behavior_handlers($field, $instance) as $handler) { $handler->property_info_alter($info, $entity_type, $field, $instance, $field_type); @@ -679,6 +722,52 @@ function entityreference_field_property_callback(&$info, $entity_type, $field, $ } /** + * Entity metadata getter callback. Returns the referenced revision. + */ +function entityreference_metadata_field_get_revision_data($entity, array $options, $name, $entity_type, $info){ + $field = field_info_field($name); + $langcode = isset($options['language']) ? $options['language']->language : LANGUAGE_NONE; + $langcode = entity_metadata_field_get_language($entity_type, $entity, $field, $langcode, TRUE); + $values = array(); + if (isset($entity->{$name}[$langcode])) { + foreach ($entity->{$name}[$langcode] as $delta => $data) { + if (isset($data['revision_id'])) { + $values[$delta] = array('id' => $data['target_id'], 'vid' => $data['revision_id']); + } + else { + $values[$delta] = $data['target_id']; + } + } + } + // For an empty single-valued field, we have to return NULL. + return $field['cardinality'] == 1 ? ($values ? reset($values) : NULL) : $values; +} + +/** + * Entity metadata setter callback. Sets the referenced revision. + */ +function entityreference_metadata_field_set_revision_data($entity, $name, $value, $langcode, $entity_type, $info) { + $field = field_info_field($name); + $langcode = entity_metadata_field_get_language($entity_type, $entity, $field, $langcode); + $values = $field['cardinality'] == 1 ? array($value) : (array) $value; + + $items = array(); + foreach ($values as $delta => $value) { + if (isset($value) && is_array($value)) { + $items[$delta]['target_id'] = $value['id']; + $items[$delta]['revision_id'] = $value['vid']; + } + else { + $items[$delta]['target_id'] = $value; + } + } + $entity->{$name}[$langcode] = $items; + // Empty the static field language cache, so the field system picks up any + // possible new languages. + drupal_static_reset('field_language'); +} + +/** * Implements hook_field_widget_info(). */ function entityreference_field_widget_info() { @@ -1172,42 +1261,67 @@ function entityreference_field_formatter_settings_summary($field, $instance, $vi */ function entityreference_field_formatter_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items, $displays) { $target_ids = array(); + $revision_ids = array(); // Collect every possible entity attached to any of the entities. foreach ($entities as $id => $entity) { foreach ($items[$id] as $delta => $item) { - if (isset($item['target_id'])) { + if (!empty($item['revision_id'])) { + $revision_ids[] = $item['revision_id']; + } + elseif (!empty($item['target_id'])) { $target_ids[] = $item['target_id']; } } } + $target_type = $field['settings']['target_type']; + + $target_entities = array(); + if ($target_ids) { - $target_entities = entity_load($field['settings']['target_type'], $target_ids); + $target_entities = entity_load($target_type, $target_ids); } - else { - $target_entities = array(); + + if ($revision_ids) { + // We need to load the revisions one by-one. + foreach ($revision_ids as $revision_id) { + $entity = entity_revision_load($target_type, $revision_id); + if ($entity) { + list($id) = entity_extract_ids($target_type, $entity); + // Use the revision-ID in the key. + $identifier = $id . ':' . $revision_id; + $target_entities[$identifier] = $entity; + } + } } - // Iterate through the fieldable entities again to attach the loaded data. + // Iterate through the fieldable entities again to attach the loaded + // data. foreach ($entities as $id => $entity) { $rekey = FALSE; - foreach ($items[$id] as $delta => $item) { // Check whether the referenced entity could be loaded. - if (isset($target_entities[$item['target_id']])) { + $identifier = !empty($item['revision_id']) ? $item['target_id'] . ':' . $item['revision_id'] : $item['target_id']; + if (isset($target_entities[$identifier])) { // Replace the instance value with the term data. - $items[$id][$delta]['entity'] = $target_entities[$item['target_id']]; + $items[$id][$delta]['entity'] = $target_entities[$identifier]; // Check whether the user has access to the referenced entity. - $has_view_access = (entity_access('view', $field['settings']['target_type'], $target_entities[$item['target_id']]) !== FALSE); - $has_update_access = (entity_access('update', $field['settings']['target_type'], $target_entities[$item['target_id']]) !== FALSE); + $has_view_access = (entity_access('view', $field['settings']['target_type'], $target_entities[$identifier]) !== FALSE); + $has_update_access = (entity_access('update', $field['settings']['target_type'], $target_entities[$identifier]) !== FALSE); $items[$id][$delta]['access'] = ($has_view_access || $has_update_access); } // Otherwise, unset the instance value, since the entity does not exist. else { unset($items[$id][$delta]); $rekey = TRUE; + unset($items[$id][$delta]); + continue; } + + $entity = $target_entities[$identifier]; + $items[$id][$delta]['entity'] = $entity; + $items[$id][$delta]['access'] = entity_access('view', $target_type, $entity); } if ($rekey) { diff --git a/plugins/selection/EntityReference_SelectionHandler_Generic.class.php b/plugins/selection/EntityReference_SelectionHandler_Generic.class.php index 57a3a37..8afbc8a 100644 --- a/plugins/selection/EntityReference_SelectionHandler_Generic.class.php +++ b/plugins/selection/EntityReference_SelectionHandler_Generic.class.php @@ -149,6 +149,23 @@ class EntityReference_SelectionHandler_Generic implements EntityReference_Select ); } + // Provide options to reference revisions if the entity supports it. + if (!empty($entity_info['revision table'])) { + $form['reference_revisions'] = array( + '#type' => 'checkbox', + '#title' => t('Reference revisions'), + '#default_value' => !empty($field['settings']['handler_settings']['reference_revisions']), + '#description' => t('When this is enabled, the reference will track the current revision at the time it is referenced. When disabled the reference will always point to the newest revision of the entity.'), + ); + $form['lock_revision'] = array( + '#type' => 'checkbox', + '#title' => t('Lock the revision.'), + '#default_value' => !empty($field['settings']['handler_settings']['lock_revision']), + '#description' => t('Locks the field to the revision of the entity at the time it was referenced. If this is disabled the revision will be updated each time the referencing entity is saved.'), + '#states' => array('visible' => array(':input[name="field[settings][handler_settings][reference_revisions]"]' => array('checked' => TRUE))), + ); + } + return $form; } diff --git a/plugins/selection/EntityReference_SelectionHandler_Views.class.php b/plugins/selection/EntityReference_SelectionHandler_Views.class.php index 1b036a7..607531e 100644 --- a/plugins/selection/EntityReference_SelectionHandler_Views.class.php +++ b/plugins/selection/EntityReference_SelectionHandler_Views.class.php @@ -68,6 +68,24 @@ class EntityReference_SelectionHandler_Views implements EntityReference_Selectio )) . '

', ); } + + // Provide options to reference revisions if the entity supports it. + if (!empty($entity_info['revision table'])) { + $form['reference_revisions'] = array( + '#type' => 'checkbox', + '#title' => t('Reference revisions'), + '#default_value' => !empty($field['settings']['handler_settings']['reference_revisions']), + '#description' => t('When this is enabled, the reference will track the current revision at the time it is referenced. When disabled the reference will always point to the newest revision of the entity.'), + ); + $form['lock_revision'] = array( + '#type' => 'checkbox', + '#title' => t('Lock the revision.'), + '#default_value' => !empty($field['settings']['handler_settings']['lock_revision']), + '#description' => t('Locks the field to the revision of the entity at the time it was referenced. If this is disabled the revision will be updated each time the referencing entity is saved.'), + '#states' => array('visible' => array(':input[name="field[settings][handler_settings][reference_revisions]"]' => array('checked' => TRUE))), + ); + } + return $form; } diff --git a/tests/entityreference.admin.test b/tests/entityreference.admin.test index 9a12119..7868a42 100644 --- a/tests/entityreference.admin.test +++ b/tests/entityreference.admin.test @@ -74,13 +74,17 @@ class EntityReferenceAdminTestCase extends DrupalWebTestCase { // The base handler should be selected by default. $this->assertFieldByName('field[settings][handler]', 'base'); - // The base handler settings should be diplayed. + // The base handler settings should be displayed. $entity_type = 'node'; $entity_info = entity_get_info($entity_type); foreach ($entity_info['bundles'] as $bundle_name => $bundle_info) { $this->assertFieldByName('field[settings][handler_settings][target_bundles][' . $bundle_name . ']'); } + // The revision settings should be displayed since nodes support revisions. + $this->assertFieldByName('field[settings][handler_settings][reference_revisions]', 1); + $this->assertFieldByName('field[settings][handler_settings][lock_revision]', 1); + // Test the sort settings. $options = array('none', 'property', 'field'); $this->assertFieldSelectOptions('field[settings][handler_settings][sort][type]', $options); @@ -109,6 +113,19 @@ class EntityReferenceAdminTestCase extends DrupalWebTestCase { $this->drupalPost(NULL, array(), t('Save settings')); // Check that the field appears in the overview form. - $this->assertFieldByXPath('//table[@id="field-overview"]//td[1]', 'Test label', t('Field was created and appears in the overview page.')); + $elements = $this->xpath('//table[@id="field-overview"]//td[text()="Test label"]'); + $this->assertTrue($elements, 'Field was created and appears in the overview page.'); + + // Test that the revision options are not shown for an entity type that does + // not support revisions. + $this->drupalPost($bundle_path . '/fields', array( + 'fields[_add_new_field][label]' => 'User', + 'fields[_add_new_field][field_name]' => 'test_user', + 'fields[_add_new_field][type]' => 'entityreference', + 'fields[_add_new_field][widget_type]' => 'entityreference_autocomplete', + ), t('Save')); + $this->drupalPostAJAX(NULL, array('field[settings][target_type]' => 'user'), 'field[settings][target_type]'); + $this->assertNoFieldByName('field[settings][handler_settings][reference_revisions]', 1); + $this->assertNoFieldByName('field[settings][handler_settings][lock_revision]', 1); } } diff --git a/tests/entityreference.revisions.test b/tests/entityreference.revisions.test new file mode 100644 index 0000000..4a4e2a6 --- /dev/null +++ b/tests/entityreference.revisions.test @@ -0,0 +1,162 @@ + 'Entity Reference Revisions', + 'description' => 'Tests referencing of revisions.', + 'group' => 'Entity Reference', + ); + } + + /** + * {@inheritdoc} + */ + public function setUp() { + parent::setUp('entityreference'); + + // Create an entity reference field. + $field = array( + 'entity_types' => array('node'), + 'settings' => array( + 'handler' => 'base', + 'target_type' => 'node', + 'handler_settings' => array( + 'target_bundles' => array(), + ), + ), + 'field_name' => 'field_entityreference', + 'type' => 'entityreference', + ); + $field = field_create_field($field); + + // Create a field instance. + $instance = array( + 'field_name' => 'field_entityreference', + 'bundle' => 'article', + 'entity_type' => 'node', + ); + $this->instance = field_create_instance($instance); + + // Create two nodes. The first will be referenced in the second. + $node = array( + 'type' => 'article', + 'status' => 1, + 'title' => $this->randomString(), + 'uid' => 1, + ); + $this->referenced_node = (object) $node; + node_save($this->referenced_node); + + $node['field_entityreference'][LANGUAGE_NONE][]['target_id'] = $this->referenced_node->nid; + $this->referencing_node = (object) $node; + node_save($this->referencing_node); + } + + /** + * Test referencing a revision of an entity. + */ + public function testReferencingRevisions() { + // Check that the referencing of specific revisions is disabled by default. + $this->assertFalse($this->isReferencingRevision(), 'Referencing of a specific revision is disabled by default.'); + + // Check that it is still disabled after updating the referencing node. + node_save($this->referencing_node); + $this->assertFalse($this->isReferencingRevision(), 'Referencing of a specific revision is still disabled after the referencing node is updated.'); + + // Enable revision referencing and update the referencing node so it picks + // up the change. + $this->instance['settings']['handler_settings']['reference_revisions'] = TRUE; + field_update_instance($this->instance); + node_save($this->referencing_node); + + // Check that the field is now referencing revisions. Retrieve the revision + // ID for comparison. + $this->assertTrue($this->isReferencingRevision(), 'The field is referencing revisions after this has been enabled.'); + $vid = $this->getReferencedRevisionId(); + + // Update the referencing node. The revision is not locked, so it should + // change to the current revision. + $this->assertNotEqual($vid, $vid = $this->getReferencedRevisionId(), 'When revision referencing is enabled and the revision is not locked, the current revision is referenced when the referencing entity is updated.'); + + // Enable revision locking and update the referencing node so it picks up + // the change. + $this->instance['settings']['handler_settings']['lock_revision'] = TRUE; + field_update_instance($this->instance); + node_save($this->referencing_node); + + $this->createNewRevision(); + $this->assertEqual($vid, $this->getReferencedRevisionId(), 'When revision locking is enabled the original revision is still referenced.'); + + // Update the referencing node. The revision is now locked, so it should + // still reference the original revision. + node_save($this->referencing_node); + + } + + /** + * Creates a new revision of the referenced node. + */ + protected function createNewRevision() { + $this->referenced_node->revision = TRUE; + node_save($this->referenced_node); + } + + /** + * Returns the revision of the referenced node. + * + * @return int + * The referenced revision. + */ + protected function getReferencedRevisionId() { + $items = field_get_items('node', $this->referencing_node, 'field_entityreference'); + if (empty($items[0]['revision_id'])) { + throw new Exception('The field is not referencing a revision.'); + } + return $items[0]['revision_id']; + } + + /** + * Returns whether or not the field is referencing a revision. + * + * @return bool + * TRUE if the field is referencing a revision, FALSE otherwise. + */ + protected function isReferencingRevision() { + $items = field_get_items('node', $this->referencing_node, 'field_entityreference'); + return !empty($items[0]['revision_id']); + } +} diff --git a/views/entityreference.views.inc b/views/entityreference.views.inc index baa7034..f79be4b 100644 --- a/views/entityreference.views.inc +++ b/views/entityreference.views.inc @@ -30,6 +30,26 @@ function entityreference_field_views_data($field) { 'help' => t('A bridge to the @entity entity that is referenced via !field_name', $parameters), ); } + + if (isset($field['settings']['handler_settings']['lock_revision']) && $field['settings']['handler_settings']['lock_revision'] && isset($entity_info['revision table'])) { + $entity = $entity_info['label']; + if ($entity == t('Node')) { + $entity = t('Content'); + } + $entity .= ' ' . t('Revision'); + + $field_name = $field['field_name'] . '_revision_id'; + $parameters = array('@entity' => $entity, '!field_name' => $field['field_name']); + $data[$table_name][$field_name]['relationship'] = array( + 'handler' => 'views_handler_relationship', + 'base' => $entity_info['revision table'], + 'base field' => $entity_info['entity keys']['revision'], + 'label' => t('@entity entity revision referenced from !field_name', $parameters), + 'group' => t('Entity Reference'), + 'title' => t('Referenced Entity Revision'), + 'help' => t('A bridge to the @entity entity revision that is referenced via !field_name', $parameters), + ); + } } // Invoke the behaviors to allow them to change the properties.