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 f01ef7b..8fb237e 100644 --- a/entityreference.module +++ b/entityreference.module @@ -69,6 +69,44 @@ function entityreference_field_info() { } /** + * Implements hook_element_info_alter(). + * + * Adds a new process callback to handle the #entityreference property. + */ +function entityreference_element_info_alter(&$type) { + array_unshift($type['textfield']['#process'], 'entityreference_textfield_element_process'); +} + +/** + * Adds entityrerefence functionality to elements with a valid + * #autocomplete_path. Also attach the entityreference validation callback on + * the element. + * + * The items bellow described the list of accepted parameters for + * #entityreference property: + * - type: (optional, default: tags) [single|tags] + * - handler: (optional, default: base) the entity_reference handler. + * - target_type: the target entity type. + * - handler_settings: (optional) An array of handler settings such as + * target_bundles, sort and auto_create. + * + * @param array $element + * Form API element. + * @param array $form_state + * Form state API element. + * + * @return array + * Processed element. + */ +function entityreference_textfield_element_process($element, &$form_state) { + if (!empty($element['#entityreference'])) { + $element['#autocomplete_path'] = 'entityreference/form_element/autocomplete/' . base64_encode(serialize($element['#entityreference'])); + $element['#element_validate'] = array('_entityreference_form_element_autocomplete_validate'); + } + return $element; +} + +/** * Implements hook_flush_caches(). */ function entityreference_flush_caches() { @@ -109,6 +147,14 @@ function entityreference_menu() { 'access arguments' => array(2, 3, 4, 5), 'type' => MENU_CALLBACK, ); + $items['entityreference/form_element/autocomplete/%'] = array( + 'title' => 'Form element Autocomplete', + 'page callback' => 'entityreference_form_element_autocomplete_callback', + 'page arguments' => array(3), + 'access callback' => 'entityreference_form_element_autocomplete_access_callback', + 'access arguments' => array(3), + 'type' => MENU_CALLBACK, + ); return $items; } @@ -243,6 +289,47 @@ 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) { + if (!empty($field['settings']['handler_settings']['lock_revision'])) { + $dest_entity_type = $field['settings']['target_type']; + $original_items = array(); + if (isset($entity->original)) { + $original_items = field_get_items($entity_type, $entity->original, $field['field_name']); + } + else { + $ids = entity_extract_ids($entity_type, $entity); + if ($ids[0]) { + $original = entity_load_unchanged($entity_type, $ids[0]); + if ($original) { + $original_items = field_get_items($entity_type, $original, $field['field_name']); + } + } + } + foreach ($items as $key => $val) { + // If the revision id is explicitly set, load the revision. + $ref_entity = FALSE; + if (isset($val['revision_id']) && $val['revision_id']) { + $ref_entity = entity_revision_load($dest_entity_type, $val['revision_id']); + } + // If the was not set or couldn't be loaded. + if (!$ref_entity) { + $ref_entity = entity_load_single($dest_entity_type, $val['target_id']); + } + $ref_ids = entity_extract_ids($dest_entity_type, $ref_entity); + // find the original item, which does not have the same delta per se. + $original_revision = FALSE; + foreach ($original_items as $original_item) { + if ($original_item['target_id'] == $val['target_id']) { + $original_revision = isset($original_item['revision_id']) ? $original_item['revision_id'] : FALSE; + } + } + if (isset($ref_ids[1]) && is_numeric($ref_ids[1]) && !$original_revision) { + $items[$key]['revision_id'] = $ref_ids[1]; + } + else if ($original_revision) { + $items[$key]['revision_id'] = $original_revision; + } + } + } // Invoke the behaviors. foreach (entityreference_get_behavior_handlers($field, $instance) as $handler) { $handler->presave($entity_type, $entity, $field, $instance, $langcode, $items); @@ -672,12 +759,61 @@ 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); } } +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; +} + +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(). */ @@ -931,6 +1067,28 @@ function _entityreference_autocomplete_tags_validate($element, &$form_state, $fo form_set_value($element, $value, $form_state); } + +/** + * Validation callback for autocomplete form element + * + * @param array $element + * The form element. + * @param array $form + * The form array. + * @param array $form_state + * The form state array. + */ +function _entityreference_form_element_autocomplete_validate($element, &$form_state, $form) { + $type = isset($element['#entityreference']['type']) ? $element['#entityreference']['type'] : FALSE; + + if (!$type || $type == 'tags') { + _entityreference_autocomplete_tags_validate($element, $form_state, $form); + } + elseif ($type == 'single') { + _entityreference_autocomplete_validate($element, $form_state, $form); + } +} + /** * Implements hook_field_widget_error(). */ @@ -963,6 +1121,25 @@ function entityreference_autocomplete_access_callback($type, $field_name, $entit } /** + * Menu access callback for the form element autocomplete widget. + * + * @param string $settings + * The entityreference settings from the element. + * + * @return bool + * True if user can access this menu item. + */ +function entityreference_form_element_autocomplete_access_callback($settings) { + $settings = unserialize(base64_decode($settings)); + + if ($settings || isset($settings['target_type'])) { + return TRUE; + } + + return FALSE; +} + +/** * Menu callback: autocomplete the label of an entity. * * @param $type @@ -999,6 +1176,40 @@ function entityreference_autocomplete_callback($type, $field_name, $entity_type, } /** + * Menu callback: autocomplete the label of a entityreference form element. + * + * @param string $settings + * The entity_reference settings from the element. + * @param string $string + * The label of the entity to query by. + * + * @return string + * Matches found. + */ +function entityreference_form_element_autocomplete_callback($settings, $string = '') { + $settings = unserialize(base64_decode($settings)); + + // Defines both mock field and instance array. + $field = array( + 'settings' => array( + 'target_type' => $settings['target_type'], + 'handler' => isset($settings['handler']) ? $settings['handler'] : 'base', + 'handler_settings' => isset($settings['handler_settings']) ? $settings['handler_settings'] : array('target_bundles' => array()), + ) + ); + $instance = array( + 'widget' => array( + 'settings' => array( + 'match_operator' => 'CONTAINS' + ) + ) + ); + $type = isset($settings['type']) ? $settings['type'] : 'tags'; + + return entityreference_autocomplete_callback_get_matches($type, $field, $instance, array(), 'NULL', $string); +} + +/** * Return JSON based on given field, instance and string. * * This function can be used by other modules that wish to pass a mocked @@ -1170,40 +1381,58 @@ 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']])) { - // Replace the instance value with the term data. - $items[$id][$delta]['entity'] = $target_entities[$item['target_id']]; - // Check whether the user has access to the referenced entity. - $items[$id][$delta]['access'] = entity_access('view', $field['settings']['target_type'], $target_entities[$item['target_id']]); - } - // Otherwise, unset the instance value, since the entity does not exist. - else { - unset($items[$id][$delta]); + // If we have a revision-ID, the key uses it as-well. + $identifier = !empty($item['revision_id']) ? $item['target_id'] . ':' . $item['revision_id'] : $item['target_id']; + if (!isset($target_entities[$identifier])) { + // The entity no longer exists, so remove the key. $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 97d0174..a384f8b 100644 --- a/plugins/selection/EntityReference_SelectionHandler_Generic.class.php +++ b/plugins/selection/EntityReference_SelectionHandler_Generic.class.php @@ -149,6 +149,17 @@ class EntityReference_SelectionHandler_Generic implements EntityReference_Select ); } + // Provide an option to lock the entity reference to the current revision if + // the entity supports it. + if (!empty($entity_info['revision table'])) { + $form['lock_revision'] = array( + '#type' => 'checkbox', + '#title' => t('Lock the field to the revision of the entity at the time it was referenced.'), + '#default_value' => !empty($field['settings']['handler_settings']['lock_revision']) ? TRUE : FALSE, + '#description' => t('When this is enabled, the reference will track the latest revision to that entity when this field is saved. This, combined with e.g. the Workbench Moderation module, can be used to provide limited workflow functionality around the referenced content.', array('!url' => 'http://drupal.org/project/workbench_moderation')) + ); + } + return $form; } diff --git a/plugins/selection/EntityReference_SelectionHandler_Views.class.php b/plugins/selection/EntityReference_SelectionHandler_Views.class.php index 1b036a7..60e569c 100644 --- a/plugins/selection/EntityReference_SelectionHandler_Views.class.php +++ b/plugins/selection/EntityReference_SelectionHandler_Views.class.php @@ -68,6 +68,18 @@ class EntityReference_SelectionHandler_Views implements EntityReference_Selectio )) . '

', ); } + + // Provide an option to lock the entity reference to the latest revision + // if the entity supports it. + if (!empty($entity_info['revision table'])) { + $form['lock_revision'] = array( + '#type' => 'checkbox', + '#title' => t('Lock the field to the revision of the entity at the time it was referenced.'), + '#default_value' => !empty($field['settings']['handler_settings']['lock_revision']) ? TRUE : FALSE, + '#description' => t('When this is enabled, the reference will track the latest revision to that entity when this field is saved. This, combined with e.g. the Workbench Moderation module, can be used to provide limited workflow functionality around the referenced content.', array('!url' => 'http://drupal.org/project/workbench_moderation')) + ); + } + return $form; }