diff --git a/README.txt b/README.txt index e027040..51aafb3 100644 --- a/README.txt +++ b/README.txt @@ -28,9 +28,20 @@ field collection item may also be viewed and edited separately. module might provide such widgets itself too. -Restrictions -------------- + Using field collection with entity translation + ----------------------------------------------- - * As of now, the field collection field does not properly respect field - translation. Thus, for now it is suggested to only use the field for - entities that are not translatable. + * Field collection items must be selected as a translatable entity type at + Admin -> Config -> Regional -> Entity Translation. + + * The common use case is to leave the field collection field untranslatable + and set the necessary fields inside it to translatable. There is currently + a known issue where a host can not be translated unless it has at least + one other field that is translatable, even if some fields inside one of + its field collections are translatable. + + * The alternate use case is to make the field collection field in the host + translatable. If this is done it does not matter whether the inner fields + are set to translatable or not, they will all be translatable as every + language for the host will have a completely separate copy of the field + collection item(s). diff --git a/field_collection.entity.inc b/field_collection.entity.inc index 2e8a11b..2707c49 100644 --- a/field_collection.entity.inc +++ b/field_collection.entity.inc @@ -231,8 +231,8 @@ class FieldCollectionItemEntity extends Entity { if ($current_id == $recieved_id) { $this->hostEntity = $entity; $delta = $this->delta(); - if (isset($entity->{$this->field_name}[$this->langcode][$delta]['entity'])) { - $entity->{$this->field_name}[$this->langcode][$delta]['entity'] = $this; + if (isset($entity->{$this->field_name}[$this->langcode()][$delta]['entity'])) { + $entity->{$this->field_name}[$this->langcode()][$delta]['entity'] = $this; } } else { @@ -326,14 +326,34 @@ class FieldCollectionItemEntity extends Entity { public function delta() { if (($entity = $this->hostEntity()) && isset($entity->{$this->field_name})) { foreach ($entity->{$this->field_name} as $langcode => &$data) { - foreach ($data as $delta => $item) { - if (isset($item['value']) && $item['value'] == $this->item_id) { - $this->langcode = $langcode; - return $delta; + if (!empty($data)) { + foreach ($data as $delta => $item) { + if (isset($item['value']) && $item['value'] == $this->item_id) { + $this->langcode = $langcode; + return $delta; + } + elseif (isset($item['entity']) && $item['entity'] === $this) { + $this->langcode = $langcode; + return $delta; + } } - elseif (isset($item['entity']) && $item['entity'] === $this) { - $this->langcode = $langcode; - return $delta; + } + } + // If we don't find the delta in the current values (cause the item + // is being deleted, for example), we search the delta in the originalcontent. + if (!empty($entity->original)) { + foreach ($entity->original->{$this->field_name} as $langcode => &$data) { + if (!empty($data)) { + foreach ($data as $delta => $item) { + if (isset($item['value']) && $item['value'] == $this->item_id) { + $this->langcode = $langcode; + return $delta; + } + elseif (isset($item['entity']) && $item['entity'] === $this) { + $this->langcode = $langcode; + return $delta; + } + } } } } @@ -344,9 +364,15 @@ class FieldCollectionItemEntity extends Entity { * Determines the language code under which the item is stored. */ public function langcode() { - if ($this->delta() !== NULL) { - return $this->langcode; + if ($this->delta() === NULL || empty($this->langcode)) { + $this->langcode = field_collection_entity_language('field_collection_item', $this); } + + if (empty($this->langcode) || ($this->langcode != LANGUAGE_NONE && (!module_exists('entity_translation') || !entity_translation_enabled('field_collection_item')))) { + $this->langcode = LANGUAGE_NONE; + } + + return $this->langcode; } /** @@ -388,6 +414,11 @@ class FieldCollectionItemEntity extends Entity { throw new Exception("Unable to create a field collection item without a given host entity."); } + // Copy the values of translatable fields for a new field collection item. + if (field_collection_item_is_translatable() && !empty($this->is_new) && $this->langcode() == LANGUAGE_NONE) { + $this->copyTranslations(); + } + // Only save directly if we are told to skip saving the host entity. Else, // we always save via the host as saving the host might trigger saving // field collection items anyway (e.g. if a new revision is created). @@ -410,11 +441,12 @@ class FieldCollectionItemEntity extends Entity { // @see field_collection_field_presave() $delta = $this->delta(); if (isset($delta)) { - $host_entity->{$this->field_name}[$this->langcode][$delta] = array('entity' => $this); + $host_entity->{$this->field_name}[$this->langcode()][$delta] = array('entity' => $this); } else { - $host_entity->{$this->field_name}[$this->langcode][] = array('entity' => $this); + $host_entity->{$this->field_name}[$this->langcode()][] = array('entity' => $this); } + return entity_save($this->hostEntityType, $host_entity); } } @@ -428,16 +460,59 @@ class FieldCollectionItemEntity extends Entity { } /** + * Copies text to all languages the collection item has a translation for. + * + * @param $source_language + * Language code to copy the text from. + */ + public function copyTranslations($source_language = NULL) { + // Get a handler for Entity Translation if there is one. + $host_et_handler = NULL; + if (module_exists('entity_translation')) { + $host_et_handler = entity_translation_get_handler($this->hostEntityType(), $this->hostEntity()); + } + if (is_null($host_et_handler)) { + return; + } + + $host_languages = array_keys($host_et_handler->getTranslations()->data); + if (empty($host_languages)) { + $host_languages = array(entity_language($this->hostEntityType(), $this->hostEntity())); + } + $source_language = isset($source_language) ? $source_language : $host_et_handler->getLanguage(); + $target_languages = array_diff($host_languages, array($source_language)); + $fields_instances = array_keys(field_info_instances('field_collection_item', $this->field_name)); + $fields = field_info_fields(); + + foreach ($fields_instances as $translatable_field) { + if ($fields[$translatable_field]['translatable'] == 1) { + foreach ($target_languages as $langcode) { + if (isset($this->{$translatable_field}[$source_language])) { + //Source (translatable_field) is set, therefore continue processing. + if(!isset($this->{$translatable_field}[$langcode])) { + //Destination (translatable_field) is not set, therefore safe to copy the translation. + $this->{$translatable_field}[$langcode] = $this->{$translatable_field}[$source_language]; + } + } + } + if ($source_language == LANGUAGE_NONE && count($this->{$translatable_field}) > 1) { + $this->{$translatable_field}[$source_language] = NULL; + } + } + } + } + + /** * Deletes the host entity's reference of the field collection item. */ protected function deleteHostEntityReference() { $delta = $this->delta(); if ($this->item_id && isset($delta)) { - unset($this->hostEntity->{$this->field_name}[$this->langcode][$delta]); + unset($this->hostEntity->{$this->field_name}[$this->langcode()][$delta]); // Do not save when the host entity is being deleted. See // field_collection_field_delete(). if (empty($this->hostEntity->field_collection_deleting)) { - entity_save($this->hostEntityType, $this->hostEntity); + entity_save($this->hostEntityType(), $this->hostEntity()); } } } @@ -449,7 +524,6 @@ class FieldCollectionItemEntity extends Entity { * a field collection item on the default revision of the host should not * delete the collection item from archived revisions too. Instead, we delete * the current default revision and archive the field collection. - * */ public function deleteRevision($skip_host_update = FALSE) { if (!$this->revision_id) { @@ -473,11 +547,8 @@ class FieldCollectionItemEntity extends Entity { ->condition('revision_id', $this->revision_id, '<>') ->execute() ->fetchAssoc(); - // If no other revision is left, delete. Else archive the item. - if (!$row) { - $this->delete(); - } - else { + + if ($row) { // Make the other revision the default revision and archive the item. db_update('field_collection_item') ->fields(array('archived' => 1, 'revision_id' => $row['revision_id'])) @@ -486,6 +557,10 @@ class FieldCollectionItemEntity extends Entity { entity_get_controller('field_collection_item')->resetCache(array($this->item_id)); entity_revision_delete('field_collection_item', $this->revision_id); } + if (!$row && !isset($this->hostEntity()->{$this->field_name}[$this->langcode()][$this->delta()])) { + // Delete if there is no existing revision or translation to be saved. + $this->delete(); + } } } diff --git a/field_collection.info b/field_collection.info index 1e57aa1..d3d408b 100644 --- a/field_collection.info +++ b/field_collection.info @@ -2,9 +2,11 @@ name = Field collection description = Provides a field collection field, to which any number of fields can be attached. core = 7.x dependencies[] = entity +test_dependencies[] = entity_translation files[] = field_collection.test files[] = field_collection.entity.inc files[] = field_collection.info.inc +files[] = includes/translation.handler.field_collection_item.inc files[] = views/field_collection_handler_relationship.inc files[] = field_collection.migrate.inc configure = admin/structure/field-collections diff --git a/field_collection.install b/field_collection.install index d2b1892..e253e95 100644 --- a/field_collection.install +++ b/field_collection.install @@ -328,3 +328,32 @@ function field_collection_update_7006() { } } } + +/** + * Update fields in field collections already set to use Entity Translation. + */ +function field_collection_update_7007() { + // Include FieldCollectionItemEntity class. + module_load_include('inc', 'field_collection', 'field_collection.entity'); + + $results = array(); + foreach (field_info_fields() as $f_name => $field) { + if ($field['translatable'] == 1 && isset($field['bundles']['field_collection_item'])) { + $query = new EntityFieldQuery(); + $query->entityCondition('entity_type', 'field_collection_item') + ->fieldLanguageCondition($f_name, LANGUAGE_NONE); + $query_result = $query->execute(); + if (isset($query_result['field_collection_item'])) { + $results = $results + $query_result['field_collection_item']; + } + } + } + if (count($results)) { + $ids = array_keys($results); + $field_collection_items = entity_load('field_collection_item', $ids); + foreach ($field_collection_items as $item) { + $item->copyTranslations(LANGUAGE_NONE); + $item->save(); + } + } +} diff --git a/field_collection.module b/field_collection.module index db7b859..01a412d 100644 --- a/field_collection.module +++ b/field_collection.module @@ -19,6 +19,20 @@ function field_collection_help($path, $arg) { } /** + * Implements hook_form_alter(). + * + * Checks for a value set by the embedded widget so fields are not displayed + * with the 'all languages' hint incorrectly. + */ +function field_collection_form_alter(&$form, &$form_state) { + if (!empty($form['#field_collection_translation_fields'])) { + foreach ($form['#field_collection_translation_fields'] as $address) { + drupal_array_set_nested_value($form, array_merge($address, array('#multilingual')), TRUE); + } + } +} + +/** * Implements hook_ctools_plugin_directory(). */ function field_collection_ctools_plugin_directory($module, $plugin) { @@ -57,7 +71,12 @@ function field_collection_entity_info() { ), 'access callback' => 'field_collection_item_access', 'deletion callback' => 'field_collection_item_delete', - 'metadata controller class' => 'FieldCollectionItemMetadataController' + 'metadata controller class' => 'FieldCollectionItemMetadataController', + 'translation' => array( + 'entity_translation' => array( + 'class' => 'EntityTranslationFieldCollectionItemHandler', + ), + ), ); // Add info about the bundles. We do not use field_info_fields() but directly @@ -73,6 +92,19 @@ function field_collection_entity_info() { 'access arguments' => array('administer field collections'), ), ); + + $path = field_collection_field_get_path($field) . '/%field_collection_item'; + // Enable the first available path scheme as default one. + if (!isset($return['field_collection_item']['translation']['entity_translation']['base path'])) { + $return['field_collection_item']['translation']['entity_translation']['base path'] = $path; + $return['field_collection_item']['translation']['entity_translation']['path wildcard'] = '%field_collection_item'; + $return['field_collection_item']['translation']['entity_translation']['default_scheme'] = $field_name; + } + else { + $return['field_collection_item']['translation']['entity_translation']['path schemes'][$field_name] = array( + 'base path' => $path, + ); + } } if (module_exists('entitycache')) { @@ -84,6 +116,30 @@ function field_collection_entity_info() { } /** + * Provide the original entity language. + * + * If a language property is defined for the current entity we synchronize the + * field value using the entity language, otherwise we fall back to + * LANGUAGE_NONE. + * + * @param $entity_type + * @param $entity + * + * @return + * A language code + */ +function field_collection_entity_language($entity_type, $entity) { + if (module_exists('entity_translation') && entity_translation_enabled($entity_type)) { + $handler = entity_translation_get_handler($entity_type, $entity); + $langcode = $handler->getLanguage(); + } + else { + $langcode = entity_language($entity_type, $entity); + } + return !empty($langcode) ? $langcode : LANGUAGE_NONE; +} + +/** * Menu callback for loading the bundle names. */ function field_collection_field_name_load($arg) { @@ -291,7 +347,7 @@ function field_collection_item_access($op, FieldCollectionItemEntity $item = NUL */ function field_collection_item_delete($id) { $fci = field_collection_item_load($id); - if ($fci) { + if (!empty($fci)) { $fci->delete(); } } @@ -375,7 +431,6 @@ function field_collection_field_get_path($field) { * Implements hook_field_settings_form(). */ function field_collection_field_settings_form($field, $instance) { - $form['hide_blank_items'] = array( '#type' => 'checkbox', '#title' => t('Hide blank items'), @@ -409,7 +464,7 @@ function field_collection_field_insert($host_entity_type, $host_entity, $field, $entity = $new_entity; } if (!empty($entity->is_new)) { - $entity->setHostEntity($host_entity_type, $host_entity, LANGUAGE_NONE, FALSE); + $entity->setHostEntity($host_entity_type, $host_entity, field_collection_entity_language($host_entity_type, $host_entity), FALSE); } $entity->save(TRUE); $item = array( @@ -424,37 +479,38 @@ function field_collection_field_insert($host_entity_type, $host_entity, $field, * Implements hook_field_update(). * * Care about removed field collection items. + * * Support saving field collection items in @code $item['entity'] @endcode. This * may be used to seamlessly create field collection items during host-entity * creation or to save changes to the host entity and its collections at once. */ function field_collection_field_update($host_entity_type, $host_entity, $field, $instance, $langcode, &$items) { + // When entity language is changed field values are moved to the new language + // and old values are marked as removed. We need to avoid processing them in + // this case. + $entity_langcode = field_collection_entity_language($host_entity_type, $host_entity); + $original = isset($host_entity->original) ? $host_entity->original : $host_entity; + $original_langcode = field_collection_entity_language($host_entity_type, $original); + $langcode = $langcode == $original_langcode ? $entity_langcode : $langcode; + // Prevent workbench moderation from deleting field collections on node_save() // during workbench_moderation_store(), when $host_entity->revision == 0. if (!empty($host_entity->workbench_moderation['updating_live_revision'])) { return; } - $items_original = !empty($host_entity->original->{$field['field_name']}[$langcode]) ? $host_entity->original->{$field['field_name']}[$langcode] : array(); + // Load items from the original entity. + $items_original = !empty($original->{$field['field_name']}[$langcode]) ? $original->{$field['field_name']}[$langcode] : array(); $original_by_id = array_flip(field_collection_field_item_to_ids($items_original)); - foreach ($items as &$item) { + foreach ($items as $delta => &$item) { // In case the entity has been changed / created, save it and set the id. // If the host entity creates a new revision, save new item-revisions as // well. if (isset($item['entity']) || !empty($host_entity->revision)) { - if ($entity = field_collection_field_get_entity($item)) { - - if (!empty($entity->is_new)) { - $entity->setHostEntity($host_entity_type, $host_entity, LANGUAGE_NONE, FALSE); - } - else { - $entity->updateHostEntity($host_entity); - } - // If the host entity is saved as new revision, do the same for the item. - if (!empty($host_entity->revision)) { + if (!empty($host_entity->revision) || !empty($host_entity->is_new_revision)) { $entity->revision = TRUE; // Without this cache clear entity_revision_is_default will // incorrectly return false here when creating a new published revision @@ -474,6 +530,14 @@ function field_collection_field_update($host_entity_type, $host_entity, $field, $entity->default_revision = FALSE; } } + + if (!empty($entity->is_new)) { + $entity->setHostEntity($host_entity_type, $host_entity, $langcode, FALSE); + } + else { + $entity->updateHostEntity($host_entity); + } + $entity->save(TRUE); $item = array( @@ -482,6 +546,7 @@ function field_collection_field_update($host_entity_type, $host_entity, $field, ); } } + unset($original_by_id[$item['value']]); } @@ -498,8 +563,17 @@ function field_collection_field_update($host_entity_type, $host_entity, $field, ->execute(); } else { + // Load items from the original entity from all languages checking which + // are the unused items. + $current_items = array(); + $languages = language_list(); + foreach ($languages as $langcode_value) { + $current_items += !empty($host_entity->{$field['field_name']}[$langcode_value->language]) ? $host_entity->{$field['field_name']}[$langcode_value->language] : array(); + $current_by_id = field_collection_field_item_to_ids($current_items); + } + $items_to_remove = array_diff($ids, $current_by_id); // Delete unused field collection items now. - foreach (field_collection_item_load_multiple($ids) as $un_item) { + foreach (field_collection_item_load_multiple($items_to_remove) as $un_item) { $un_item->updateHostEntity($host_entity); $un_item->deleteRevision(TRUE); } @@ -511,9 +585,8 @@ function field_collection_field_update($host_entity_type, $host_entity, $field, * Implements hook_field_delete(). */ function field_collection_field_delete($entity_type, $entity, $field, $instance, $langcode, &$items) { - $ids = field_collection_field_item_to_ids($items); // Also delete all embedded entities. - if ($ids && field_info_field($field['field_name'])) { + if ($ids = field_collection_field_item_to_ids($items)) { // We filter out entities that are still being referenced by other // host-entities. This should never be the case, but it might happened e.g. // when modules cloned a node without knowing about field-collection. @@ -649,6 +722,7 @@ function field_collection_field_formatter_info() { 'field types' => array('field_collection'), 'settings' => array( 'edit' => t('Edit'), + 'translate' => t('Translate'), 'delete' => t('Delete'), 'add' => t('Add'), 'description' => TRUE, @@ -674,11 +748,24 @@ function field_collection_field_formatter_settings_form($field, $instance, $view $elements = array(); if ($display['type'] != 'field_collection_fields') { + $elements['add'] = array( + '#type' => 'textfield', + '#title' => t('Add link title'), + '#default_value' => $settings['add'], + '#description' => t('Leave the title empty, to hide the link.'), + ); $elements['edit'] = array( '#type' => 'textfield', '#title' => t('Edit link title'), '#default_value' => $settings['edit'], '#description' => t('Leave the title empty, to hide the link.'), + '#access' => field_collection_item_is_translatable(), + ); + $elements['translate'] = array( + '#type' => 'textfield', + '#title' => t('Translate link title'), + '#default_value' => $settings['translate'], + '#description' => t('Leave the title empty, to hide the link.'), ); $elements['delete'] = array( '#type' => 'textfield', @@ -686,12 +773,6 @@ function field_collection_field_formatter_settings_form($field, $instance, $view '#default_value' => $settings['delete'], '#description' => t('Leave the title empty, to hide the link.'), ); - $elements['add'] = array( - '#type' => 'textfield', - '#title' => t('Add link title'), - '#default_value' => $settings['add'], - '#description' => t('Leave the title empty, to hide the link.'), - ); $elements['description'] = array( '#type' => 'checkbox', '#title' => t('Show the field description beside the add link.'), @@ -730,7 +811,7 @@ function field_collection_field_formatter_settings_summary($field, $instance, $v $output = array(); if ($display['type'] !== 'field_collection_fields') { - $links = array_filter(array_intersect_key($settings, array_flip(array('add', 'edit', 'delete')))); + $links = field_collection_get_operations($settings, TRUE); if ($links) { $output[] = t('Links: @links', array('@links' => check_plain(implode(', ', $links)))); } @@ -763,7 +844,7 @@ function field_collection_field_formatter_view($entity_type, $entity, $field, $i if ($field_collection = field_collection_field_get_entity($item)) { $output = l($field_collection->label(), $field_collection->path()); $links = array(); - foreach (array('edit', 'delete') as $op) { + foreach (field_collection_get_operations($settings) as $op => $label) { if ($settings[$op] && field_collection_item_access($op == 'edit' ? 'update' : $op, $field_collection)) { $title = entity_i18n_string("field:{$field['field_name']}:{$instance['bundle']}:setting_$op", $settings[$op]); $links[] = l($title, $field_collection->path() . '/' . $op, array('query' => drupal_get_destination())); @@ -793,7 +874,7 @@ function field_collection_field_formatter_view($entity_type, $entity, $field, $i '#theme' => 'links__field_collection_view', ); $links['#attributes']['class'][] = 'field-collection-view-links'; - foreach (array('edit', 'delete') as $op) { + foreach (field_collection_get_operations($settings) as $op => $label) { if ($settings[$op] && field_collection_item_access($op == 'edit' ? 'update' : $op, $field_collection)) { $links['#links'][$op] = array( 'title' => entity_i18n_string("field:{$field['field_name']}:{$instance['bundle']}:setting_$op", $settings[$op]), @@ -826,6 +907,29 @@ function field_collection_field_formatter_view($entity_type, $entity, $field, $i } /** + * Returns an array of enabled operations. + */ +function field_collection_get_operations($settings, $add = FALSE) { + $operations = array(); + + if ($add) { + $operations[] = 'add'; + } + $operations[] = 'edit'; + if (field_collection_item_is_translatable()) { + $operations[] = 'translate'; + } + $operations[] = 'delete'; + + global $field_collection_operation_keys; + $field_collection_operation_keys = array_flip($operations); + $operations = array_filter(array_intersect_key($settings, $field_collection_operation_keys)); + asort($operations); + + return $operations; +} + +/** * Helper function to add links to a field collection field. */ function field_collection_field_formatter_links(&$element, $entity_type, $entity, $field, $instance, $langcode, $items, $display) { @@ -835,7 +939,7 @@ function field_collection_field_formatter_links(&$element, $entity_type, $entity if ($settings['add'] && ($field['cardinality'] == FIELD_CARDINALITY_UNLIMITED || count($items) < $field['cardinality'])) { // Check whether the current is allowed to create a new item. $field_collection_item = entity_create('field_collection_item', array('field_name' => $field['field_name'])); - $field_collection_item->setHostEntity($entity_type, $entity, LANGUAGE_NONE, FALSE); + $field_collection_item->setHostEntity($entity_type, $entity, $langcode, FALSE); if (field_collection_item_access('create', $field_collection_item)) { $allow_create_item = TRUE; @@ -959,15 +1063,56 @@ function field_collection_field_widget_form(&$form, &$form_state, $field, $insta // load. if (empty($field_collection_item)) { $field_collection_item = entity_create('field_collection_item', array('field_name' => $field_name)); - $field_collection_item->setHostEntity($element['#entity_type'], $element['#entity']); + $field_collection_item->setHostEntity($element['#entity_type'], $element['#entity'], $langcode); } // Put our entity in the form state, so FAPI callbacks can access it. $field_state['entity'][$delta] = $field_collection_item; } + // Register a child entity translation handler to properly deal with the + // entity form language. + if (field_collection_item_is_translatable()) { + $element['#host_entity_type'] = $element['#entity_type']; + $element['#host_entity'] = $element['#entity']; + // Give each field collection item a unique entity translation handler + // ID, otherwise an infinite loop occurs when adding values to nested + // field collection items. + if (!isset($field_collection_item->entity_translation_handler_id)) { + list($id, $revision_id) = entity_extract_ids('field_collection_item', $field_collection_item); + $revision_id = isset($revision_id) ? $revision_id : 0; + $field_collection_item->entity_translation_handler_id = 'field_collection_item' . '-' . (!empty($id) ? 'eid-' . $id . '-' . $revision_id : 'new-' . rand()); + } + $element['#field_collection_item'] = $field_collection_item; + field_collection_add_child_translation_handler($element); + // Ensure this is executed even with cached forms. This is mainly useful + // when dealing with AJAX calls. + $element['#process'][] = 'field_collection_add_child_translation_handler'; + // Flag the field to be processed in field_collection_form_alter to + // avoid adding incorrect translation hints. + $address = array_slice($element['#parents'], 0, -2); + if (empty($form['#field_collection_translation_fields']) || !in_array($address, $form['#field_collection_translation_fields'])) { + $form['#field_collection_translation_fields'][] = $address; + } + } + + // Add the subform field_form_set_state($field_parents, $field_name, $language, $form_state, $field_state); - field_attach_form('field_collection_item', $field_collection_item, $element, $form_state, $language); + // Set the language to to parent entity language, because + // field_content_languages() will always set $language to LANGUAGE_NONE. + if (field_collection_item_is_translatable()) { + field_attach_form('field_collection_item', $field_collection_item, $element, $form_state, entity_language($element['#host_entity_type'], $element['#host_entity'])); + } + else { + field_attach_form('field_collection_item', $field_collection_item, $element, $form_state, $language); + } + + // Make sure subfields get translatable clues (like 'all languages') + if (field_collection_item_is_translatable() && variable_get('entity_translation_shared_labels', TRUE)) { + foreach (element_children($element) as $key) { + $element[$key]['#process'][] = 'entity_translation_element_translatability_clue'; + } + } if (empty($element['#required'])) { $element['#after_build'][] = 'field_collection_field_widget_embed_delay_required_validation'; @@ -997,6 +1142,15 @@ function field_collection_field_widget_form(&$form, &$form_state, $field, $insta } /** + * Registers a child entity translation handler for the given element. + */ +function field_collection_add_child_translation_handler($element) { + $handler = entity_translation_get_handler($element['#host_entity_type'], $element['#host_entity']); + $handler->addChild('field_collection_item', $element['#field_collection_item']); + return $element; +} + +/** * Implements hook_field_attach_form(). * * Corrects #max_delta when we hide the blank field collection item. @@ -1033,6 +1187,19 @@ function field_collection_field_attach_form($entity_type, $entity, &$form, &$for } } } + + // If FCs are translatable, make sure we mark any necessary sub-fields in the + // FC widget as translatable as well. + if ($entity_type == 'field_collection_item' + && field_collection_item_is_translatable() + ) { + foreach (field_info_instances($entity_type, $form['#bundle']) as $field_name => $instance) { + $field = field_info_field($field_name); + if (isset($field['translatable'])) { + $form[$field_name]['#multilingual'] = (boolean) $field['translatable']; + } + } + } } /** @@ -1170,7 +1337,7 @@ function field_collection_field_get_entity(&$item, $field_name = NULL) { elseif (isset($item['value'])) { // By default always load the default revision, so caches get used. $entity = field_collection_item_load($item['value']); - if ($entity->revision_id != $item['revision_id']) { + if ($entity && $entity->revision_id != $item['revision_id']) { // A non-default revision is a referenced, so load this one. $entity = field_collection_item_revision_load($item['revision_id']); } @@ -1230,7 +1397,30 @@ function field_collection_field_widget_embed_validate($element, &$form_state, $c $language = $element['#language']; $field_state = field_form_get_state($field_parents, $field_name, $language, $form_state); - $field_collection_item = $field_state['entity'][$element['#delta']]; + + // We have to populate the field_collection_item before we can attach it to + // the form. + // We need to check that the item is not empty because it could be just the + // empty field collection added to the form by default. + if (isset($field_state['entity'][$element['#delta']]) && !field_collection_is_empty($field_state['entity'][$element['#delta']])) { + $field_collection_item = $field_state['entity'][$element['#delta']]; + } + elseif ($form_state['values'][$field_state['array_parents'][0]][$field_state['array_parents'][1]][$element['#delta']]) { + $field_collection_item = clone $field_state['entity'][0]; + foreach ($form_state['values'][$field_state['array_parents'][0]][$field_state['array_parents'][1]][$element['#delta']] as $key => $value) { + if (property_exists($field_collection_item, $key)) { + $field_collection_item->{$key} = $value; + } + } + } + + // Handle a possible language change. + if (field_collection_item_is_translatable()) { + $handler = entity_translation_get_handler('field_collection_item', $field_collection_item); + $element_values = &drupal_array_get_nested_value($form_state['values'], $field_state['array_parents']); + $element_form_state = array('values' => &$element_values[$element['#delta']]); + $handler->entityFormLanguageWidgetSubmit($element, $element_form_state); + } // Attach field API validation of the embedded form. field_attach_form_validate('field_collection_item', $field_collection_item, $element, $form_state); @@ -1240,13 +1430,11 @@ function field_collection_field_widget_embed_validate($element, &$form_state, $c foreach ($element['#field_collection_required_elements'] as &$elements) { // Copied from _form_validate(). - // #1676206: Modified to support options widget. if (isset($elements['#needs_validation'])) { $is_empty_multiple = (!count($elements['#value'])); $is_empty_string = (is_string($elements['#value']) && drupal_strlen(trim($elements['#value'])) == 0); $is_empty_value = ($elements['#value'] === 0); - $is_empty_option = (isset($elements['#options']['_none']) && $elements['#value'] == '_none'); - if ($is_empty_multiple || $is_empty_string || $is_empty_value || $is_empty_option) { + if ($is_empty_multiple || $is_empty_string || $is_empty_value) { if (isset($elements['#title'])) { form_error($elements, t('@name field is required.', array('@name' => $elements['#title']))); } @@ -1258,6 +1446,9 @@ function field_collection_field_widget_embed_validate($element, &$form_state, $c } } + + + // Only if the form is being submitted, finish the collection entity and // prepare it for saving. if ($form_state['submitted'] && !form_get_errors()) { @@ -1273,12 +1464,17 @@ function field_collection_field_widget_embed_validate($element, &$form_state, $c $item['_weight'] = $element['_weight']['#value']; } + // Ensure field columns are poroperly populated. + $item['value'] = $field_collection_item->item_id; + $item['revision_id'] = $field_collection_item->revision_id; + // Put the field collection item in $item['entity'], so it is saved with // the host entity via hook_field_presave() / field API if it is not empty. // @see field_collection_field_presave() $item['entity'] = $field_collection_item; form_set_value($element, $item, $form_state); } + } /** @@ -1325,12 +1521,16 @@ function field_collection_i18n_string_list_field_alter(&$properties, $type, $ins foreach ($instance['display'] as $view_mode => $display) { if ($display['type'] != 'field_collection_fields') { - $display['settings'] += array('edit' => 'edit', 'delete' => 'delete', 'add' => 'add'); + $display['settings'] += array('edit' => 'edit', 'translate' => 'translate', 'delete' => 'delete', 'add' => 'add'); $properties['field'][$instance['field_name']][$instance['bundle']]['setting_edit'] = array( 'title' => t('Edit link title'), 'string' => $display['settings']['edit'], ); + $properties['field'][$instance['field_name']][$instance['bundle']]['setting_translate'] = array( + 'title' => t('Edit translate title'), + 'string' => $display['settings']['translate'], + ); $properties['field'][$instance['field_name']][$instance['bundle']]['setting_delete'] = array( 'title' => t('Delete link title'), 'string' => $display['settings']['delete'], @@ -1421,7 +1621,10 @@ function field_collection_item_set_host_entity($item, $property_name, $wrapper) if (!isset($wrapper->{$item->field_name})) { throw new EntityMetadataWrapperException('The specified entity has no such field collection field.'); } - $item->setHostEntity($wrapper->type(), $wrapper->value()); + $entity_type = $wrapper->type(); + $field = field_info_field($item->field_name); + $langcode = field_is_translatable($entity_type, $field) ? field_collection_entity_language($entity_type, $wrapper->value()) : LANGUAGE_NONE; + $item->setHostEntity($wrapper->type(), $wrapper->value(), $langcode); } /** @@ -1479,6 +1682,47 @@ function field_collection_devel_generate($object, $field, $instance, $bundle) { } /** + * Determine if field collection items can be translated. + * + * @return + * Boolean indicating whether field collection items can be translated. + */ +function field_collection_item_is_translatable() { + return (bool) module_invoke('entity_translation', 'enabled', 'field_collection_item'); +} + +/** + * Implements hook_entity_translation_delete(). + */ +function field_collection_entity_translation_delete($entity_type, $entity, $langcode) { + if (field_collection_item_is_translatable()) { + list(, , $bundle) = entity_extract_ids($entity_type, $entity); + + foreach (field_info_instances($entity_type, $bundle) as $instance) { + $field_name = $instance['field_name']; + $field = field_info_field($field_name); + + if ($field['type'] == 'field_collection') { + $field_langcode = field_is_translatable($entity_type, $field) ? $langcode : LANGUAGE_NONE; + + if (!empty($entity->{$field_name}[$field_langcode])) { + foreach ($entity->{$field_name}[$field_langcode] as $delta => $item) { + $field_collection_item = field_collection_field_get_entity($item); + $handler = entity_translation_get_handler('field_collection_item', $field_collection_item); + $translations = $handler->getTranslations(); + + if (isset($translations->data[$langcode])) { + $handler->removeTranslation($langcode); + $field_collection_item->save(TRUE); + } + } + } + } + } + } +} + +/** * Determines if the additional blank items should be displayed or not. * * @param array $field @@ -1505,3 +1749,42 @@ function field_collection_admin_menu_map() { return $map; } } + +/** + * implements hook_entity_translation_insert + */ +function field_collection_entity_translation_insert($entity_type, $entity, $translation, $values = array()) { + // Check if some of the values inserted are of a field_collection field + if (!empty($values)) { + foreach ($values as $field_name => $value) { + $field = field_info_field($field_name); + + if ($field['type'] == 'field_collection') { + // We have found a field collection + $language = $translation['language']; + $source_language = $translation['source']; + + if (!empty($value[$language])) { + $source_items = !empty($entity->{$field_name}[$source_language]) ? field_collection_field_item_to_ids($entity->{$field_name}[$source_language]) : array(); + foreach ($value[$language] as $delta => $field_value) { + if (!isset($field_value['entity'])) { + if ($fc_entity = field_collection_field_get_entity($field_value)) { + // Check if this field collection item belongs to the source language + if (in_array($fc_entity->item_id, $source_items)) { + // Clone the field collection item + $new_fc_entity = clone $fc_entity; + $new_fc_entity->item_id = NULL; + $new_fc_entity->revision_id = NULL; + $new_fc_entity->is_new = TRUE; + + // Set the new entity for saving it later + $entity->{$field_name}[$language][$delta]['entity'] = $new_fc_entity; + } + } + } + } + } + } + } + } +} diff --git a/field_collection.pages.inc b/field_collection.pages.inc index b88e9ed..4d12151 100644 --- a/field_collection.pages.inc +++ b/field_collection.pages.inc @@ -30,7 +30,8 @@ function field_collection_item_form($form, &$form_state, $field_collection_item) // @todo: Fix core and remove the hack. $form['field_name'] = array('#type' => 'value', '#value' => $field_collection_item->field_name); - field_attach_form('field_collection_item', $field_collection_item, $form, $form_state); + $langcode = entity_language('field_collection_item', $field_collection_item); + field_attach_form('field_collection_item', $field_collection_item, $form, $form_state, $langcode); $form['actions'] = array('#type' => 'actions', '#weight' => 50); $form['actions']['submit'] = array( @@ -114,7 +115,8 @@ function field_collection_item_add($field_name, $entity_type, $entity_id, $revis // Check field cardinality. $field = field_info_field($field_name); - $langcode = LANGUAGE_NONE; + $langcode = !empty($field['translatable']) ? entity_language($entity_type, $entity) : LANGUAGE_NONE; + if (!($field['cardinality'] == FIELD_CARDINALITY_UNLIMITED || !isset($entity->{$field_name}[$langcode]) || count($entity->{$field_name}[$langcode]) < $field['cardinality'])) { drupal_set_message(t('Too many items.'), 'error'); return ''; @@ -125,7 +127,7 @@ function field_collection_item_add($field_name, $entity_type, $entity_id, $revis // as during the form-workflow we have multiple field collection item entity // instances, which we don't want link all with the host. // That way the link is going to be created when the item is saved. - $field_collection_item->setHostEntity($entity_type, $entity, LANGUAGE_NONE, FALSE); + $field_collection_item->setHostEntity($entity_type, $entity, $langcode, FALSE); $label = $field_collection_item->translatedInstanceLabel(); $title = ($field['cardinality'] == 1) ? $label : t('Add new !instance_label', array('!instance_label' => $label)); diff --git a/field_collection.test b/field_collection.test index b39b1e2..bbf60e6 100644 --- a/field_collection.test +++ b/field_collection.test @@ -641,3 +641,640 @@ class FieldCollectionContentTranslationTestCase extends DrupalWebTestCase { } } + +/** + * Test using field collection with content that gets translated with Entity Translation. + */ +class FieldCollectionEntityTranslationTestCase extends DrupalWebTestCase { + const TRANS_FIELD_EN = 'Translatable EN'; + const TRANS_FIELD_DE = 'Translatable DE'; + const TRANS_FIELD_DE_MOD = 'Translatable DE Mod'; + const UNTRANS_FIELD_EN = 'Untranslatable EN'; + const UNTRANS_FIELD_DE = 'Untranslatable DE'; + const UNTRANS_FIELD_DE_MOD = 'Untranslatable DE Mod'; + const NUM_VALUES = 4; + + public static function getInfo() { + return array( + 'name' => 'Field collection entity translation', + 'description' => 'Tests using content under translation with Entity Translation.', + 'group' => 'Field types', + 'dependencies' => array('entity_translation'), + ); + } + + /** + * Login the given user only if she has not changed. + */ + function login($user) { + if (!isset($this->current_user) || $this->current_user->uid != $user->uid) { + $this->current_user = $user; + $this->drupalLogin($user); + } + } + + /** + * Returns a user with administration rights. + * + * @param $permissions + * Additional permissions for administrative user. + */ + function getAdminUser(array $permissions = array()) { + if (!isset($this->admin_user)) { + $this->admin_user = $this->drupalCreateUser(array_merge(array( + 'bypass node access', + 'administer nodes', + 'administer languages', + 'administer content types', + 'administer blocks', + 'access administration pages', + 'administer site configuration', + 'administer entity translation', + ), $permissions)); + } + return $this->admin_user; + } + + /** + * Returns a user with minimal translation rights. + * + * @param $permissions + * Additional permissions for administrative user. + */ + function getTranslatorUser(array $permissions = array()) { + if (!isset($this->translator_user)) { + $this->translator_user = $this->drupalCreateUser(array_merge(array( + 'create page content', + 'edit own page content', + 'delete own page content', + 'translate any entity', + ), $permissions)); + } + return $this->translator_user; + } + + /** + * Install a specified language if it has not been already, otherwise make sure that the language is enabled. + * + * @param $langcode + * The language code to check. + */ + function addLanguage($langcode) { + // Check to make sure that language has not already been installed. + $this->drupalGet('admin/config/regional/language'); + + if (strpos($this->drupalGetContent(), 'enabled[' . $langcode . ']') === FALSE) { + // Doesn't have language installed so add it. + $edit = array(); + $edit['langcode'] = $langcode; + $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language')); + + // Make sure we are not using a stale list. + drupal_static_reset('language_list'); + $languages = language_list('language'); + $this->assertTrue(array_key_exists($langcode, $languages), t('Language was installed successfully.')); + + if (array_key_exists($langcode, $languages)) { + $this->assertRaw(t('The language %language has been created and can now be used. More information is available on the help screen.', array('%language' => $languages[$langcode]->name, '@locale-help' => url('admin/help/locale'))), t('Language has been created.')); + } + } + elseif ($this->xpath('//input[@type="checkbox" and @name=:name and @checked="checked"]', array(':name' => 'enabled[' . $langcode . ']'))) { + // It is installed and enabled. No need to do anything. + $this->assertTrue(TRUE, 'Language [' . $langcode . '] already installed and enabled.'); + } + else { + // It is installed but not enabled. Enable it. + $this->assertTrue(TRUE, 'Language [' . $langcode . '] already installed.'); + $this->drupalPost(NULL, array('enabled[' . $langcode . ']' => TRUE), t('Save configuration')); + $this->assertRaw(t('Configuration saved.'), t('Language successfully enabled.')); + } + } + + public function setUp() { + parent::setUp(array('field_collection', 'entity_translation')); + $language_none = LANGUAGE_NONE; + // Login with an admin user + $this->login($this->getAdminUser()); + // Add English and German languages + $this->addLanguage('en'); + $this->addLanguage('de'); + + // Set "Article" content type to use multilingual support with translation. + $edit = array(); + $edit['language_content_type'] = ENTITY_TRANSLATION_ENABLED; + $this->drupalPost('admin/structure/types/manage/page', $edit, t('Save content type')); + $this->assertRaw(t('The content type %type has been updated.', array('%type' => 'Basic page')), t('Basic page content type has been updated.')); + + // Create a field collection field to use for the tests. + $this->field_name = 'field_test_collection'; + $this->field_base = "{$this->field_name}[$language_none]"; + $this->field = array( + 'field_name' => $this->field_name, + 'type' => 'field_collection', + 'cardinality' => -1, + 'translatable' => TRUE, + ); + $this->field = field_create_field($this->field); + $this->field_id = $this->field['id']; + + $this->instance = array( + 'field_name' => $this->field_name, + 'entity_type' => 'node', + 'bundle' => 'page', + 'label' => $this->randomName() . '_label', + 'description' => $this->randomName() . '_description', + 'weight' => mt_rand(0, 127), + 'settings' => array(), + 'widget' => array( + 'type' => 'field_collection_embed', + 'label' => 'Test', + 'settings' => array(), + ), + ); + $this->instance = field_create_instance($this->instance); + + // Enable entity translation of field collections + $this->drupalGet('admin/config/regional/entity_translation'); + $this->drupalPost('admin/config/regional/entity_translation', array('entity_translation_entity_types[field_collection_item]' => TRUE), t('Save configuration')); + $this->assertRaw(t('The configuration options have been saved.'), t('Entity translation of field collections enabled.')); + + // Add an untraslatable field to the collection + $this->field_untrans_name = 'field_text_untrans'; + $this->field_untrans_base = "[{$this->field_untrans_name}][$language_none][0][value]"; + $field = array( + 'field_name' => $this->field_untrans_name, + 'type' => 'text', + 'cardinality' => 1, + 'translatable' => FALSE, + ); + field_create_field($field); + $instance = array( + 'entity_type' => 'field_collection_item', + 'field_name' => $this->field_untrans_name, + 'bundle' => $this->field_name, + 'label' => 'Test untranslatable text field', + 'widget' => array( + 'type' => 'text_textfield', + ), + ); + field_create_instance($instance); + + // Add a translatable field to the collection + $this->field_trans_name = 'field_text_trans'; + $this->field_trans_base = "[{$this->field_trans_name}][$language_none][0][value]"; + $this->field_trans_dest = "[{$this->field_trans_name}][de][0][value]"; + $field = array( + 'field_name' => $this->field_trans_name, + 'type' => 'text', + 'cardinality' => 1, + 'translatable' => TRUE, + ); + field_create_field($field); + $instance = array( + 'entity_type' => 'field_collection_item', + 'field_name' => $this->field_trans_name, + 'bundle' => $this->field_name, + 'label' => 'Test translatable text field', + 'widget' => array( + 'type' => 'text_textfield', + ), + ); + field_create_instance($instance); + + $this->login($this->getTranslatorUser()); + } + + /** + * Creates a basic page in the specified language with only a value in the field collection + * + * @param integer $num_values + * The number of values to include in the field collection + */ + protected function createPage($num_values, $langcode = 'en') { + // Check if num_values is greater than the field cardinality + if ($num_values > self::NUM_VALUES) { + $num_values = self::NUM_VALUES; + } + + $title = $this->randomName(); + + $this->drupalGet('node/add/page'); + + $edit = array(); + $edit['title'] = $title; + for ($i = 0; $i < $num_values; $i++) { + if ($i != 0) { + $this->drupalPost(NULL, array(), t('Add another item')); + } + $edit[$this->field_base . '[' . $i . ']' . $this->field_untrans_base] = self::UNTRANS_FIELD_EN . '_' . $i; + $edit[$this->field_base . '[' . $i . ']' . $this->field_trans_base] = self::TRANS_FIELD_EN . '_' . $i; + } + + $edit['language'] = $langcode; + $this->drupalPost(NULL, $edit, t('Save')); + $this->assertRaw(t('Basic page %title has been created.', array('%title' => $title)), t('Basic page created.')); + + // Check to make sure the node was created. + $node = $this->drupalGetNodeByTitle($title); + $this->assertTrue($node, t('Node found in database.')); + + return $node; + + } + + /** + * Create a translation using the Entity Translation Form + * + * @param $node + * Node of the basic page to create translation for. + * @param $langcode + * The language code of the translation. + * @param $source_langcode + * The original language code. + */ + protected function createTranslationForm($node, $langcode, $source_langcode = 'en') { + $language_none = LANGUAGE_NONE; + $edit = array(); + + $this->drupalGet('node/' . $node->nid . '/edit/add/' . $source_langcode . '/' .$langcode); + + // Get the field collection in the original language + $fc_values = $node->{$this->field_name}[$source_langcode]; + + // Check if all the fields were well populated and fill it later with the new value + foreach ($fc_values as $delta => $fc_value) { + // Load the field collection item + $fc_item_array = entity_load('field_collection_item', array($fc_value['value'])); + $fc_item = reset($fc_item_array); + $fc_untrans_key = "{$this->field_name}[$langcode][$delta]{$this->field_untrans_base}"; + $fc_trans_key = "{$this->field_name}[$langcode][$delta]{$this->field_trans_dest}"; + $this->assertFieldByXPath( + "//input[@name='$fc_untrans_key']", + $fc_item->{$this->field_untrans_name}[LANGUAGE_NONE][0]['value'], + 'Original value of untranslatable field correctly populated' + ); + $this->assertFieldByXPath( + "//input[@name='$fc_trans_key']", + $fc_item->{$this->field_trans_name}['en'][0]['value'], + 'Original value of translatable field correctly populated' + ); + + $edit[$fc_untrans_key] = self::UNTRANS_FIELD_DE . '_' . $delta; + $edit[$fc_trans_key] = self::TRANS_FIELD_DE . '_' . $delta; + } + + // Save the translation + $this->drupalPost(NULL, $edit, t('Save')); + $this->drupalGet('node/' . $node->nid . '/translate'); + $this->assertLinkByHref('node/' . $node->nid . '/edit/' . $langcode, 0, t('Translation edit link found. Translation created.')); + + // Reload the node + $node = node_load($node->nid, NULL, TRUE); + + // Check the values of the translated field + $this->checkFieldCollectionContent($node, $langcode); + + // Check the values of the field in the original language + $this->checkFieldCollectionContent($node, $source_langcode); + + return $node; + } + + /** + * Removes a translation using the entity translation form + * + * @param mixed $node + * The node to remove the translation from + * @param unknown $langcode + * The language of the translation to remove + * @param unknown $source_langcode + * The source language of the node + */ + protected function removeTranslationForm($node, $langcode, $source_langcode) { + // Number of field collection items in the source language + $num_original_fc_items = count($node->{$this->field_name}[$source_langcode]); + // Get the field_collection items of the translation + $fc_item_ids = array(); + foreach ($node->{$this->field_name}[$langcode] as $delta => $value) { + $fc_item_ids[] = $value['value']; + } + + // Fetch the translation edition form + $this->drupalGet('node/' . $node->nid . '/edit/' . $langcode); + + // Remove the translation + $this->drupalPost(NULL, array(), t('Delete translation')); + // Confirm deletion + $this->drupalPost(NULL, array(), t('Delete')); + + // Reload the node + $node = node_load($node->nid, NULL, TRUE); + + // Check that the translation is removed + $this->drupalGet('node/' . $node->nid . '/translate'); + $this->assertLinkByHref('node/' . $node->nid . '/edit/add/' . $source_langcode . '/' . $langcode, 0, 'The add translation link appears'); + $this->assert(empty($node->{$this->field_name}[$langcode])); + + // Check that the field collection items of the translation are removed + // If this test fails it is maybe not clear if it'a problem of field_collection or of + // Entity Translation that does not fires field_attach_update + $fc_items = entity_load('field_collection_item', $fc_item_ids); + $this->assert(empty($fc_items), t('The field collection item has been removed from the database.')); + + // Check that the field collection in the original language has not changed + $num_fc_items = count($node->{$this->field_name}[$source_langcode]); + $this->assertEqual($num_original_fc_items, $num_fc_items, 'The number of field collection items in the original language has not changed.'); + $this->checkFieldCollectionContent($node, $source_langcode); + } + + /** + * Creates a translation programmatically using Entity Translation + * + * @param $node + * Node of the basic page to create translation for. + * @param $langcode + * The language code of the translation. + * @param $source_langcode + * The source language code. + */ + protected function createTranslation($node, $langcode) { + $source_langcode = $node->language; + + // Get the Entity Translation Handler + $handler = entity_translation_get_handler('node', $node, TRUE); + // Variable to hold the fields values + $values = array(); + // Translation settings + $translation = array( + 'translate' => 0, + 'status' => 1, + 'language' => $langcode, + 'source' => $source_langcode, + 'uid' => $node->uid, + ); + // Copy field values + foreach (field_info_instances('node', $node->type) as $instance) { + $field_name = $instance['field_name']; + $field = field_info_field($field_name); + $field_value = array(); + // Copy the value of the translated field if it's translatable + if ($field['translatable']) { + if (isset($node->{$field_name}[$node->language])) { + $field_value = $node->{$field_name}[$source_langcode]; + $values[$field_name][$langcode] = $field_value; + $node->{$field_name}[$langcode] = $field_value; + } + } + } + + $handler->setTranslation($translation, $values); + $handler->saveTranslations(); + field_attach_update('node', $node); + + // Reload an return the node + $node = node_load($node->nid, null, TRUE); + return $node; + } + + /** + * Removes a translation programmatically using the entity translation api + * + * @param mixed $node + * The node to remove the translation from + * @param unknown $langcode + * The language of the translation to remove + */ + protected function removeTranslation($node, $langcode) { + // Get a translation entity handler + $handler = entity_translation_get_handler('node', $node, TRUE); + + // Remove the translation + $handler->removeTranslation($langcode); + node_save($node); + + // Reload and return the node + $node = node_load($node->nid, null, TRUE); + + return $node; + } + + /** + * Creates a new revision of the node and checks the result + * + * @param $node + * @param $langcode + * @param $source_langcode + * @return + * The new revision of the node + */ + protected function createRevision($node, $langcode, $source_langcode) { + $node_original_revision = $node->vid; + // The original entries of the translated field + $original_fc_item_ids = $node->{$this->field_name}[$langcode]; + + // Create the revision + $node->revision = TRUE; + node_save($node); + + // The new entries of the translated field + $new_fc_item_ids = $node->{$this->field_name}[$langcode]; + + // Check that the field collection items are the same and a new revision of each one has been created + foreach ($original_fc_item_ids as $delta => $value) { + $this->assertEqual($value['value'], $new_fc_item_ids[$delta]['value'], t('We have the same field collection item')); + $this->assertNotEqual($value['revision_id'], $new_fc_item_ids[$delta]['revision_id'], t('We have a new revision of the field collection item')); + } + + return $node; + } + + /** + * Check the content of the field collection for a specified language + * + * @param mixed $node + * The node to check + * @param string $langcode + * The language to check + */ + protected function checkFieldCollectionContent($node, $langcode) { + switch($langcode) { + case 'en': + $untrans_field = self::UNTRANS_FIELD_EN; + $trans_field = self::TRANS_FIELD_EN; + break; + case 'de': + $untrans_field = self::UNTRANS_FIELD_DE; + $trans_field = self::TRANS_FIELD_DE; + break; + } + // Get the field collection in the specified language + $fc_values = $node->{$this->field_name}[$langcode]; + + foreach ($fc_values as $delta => $fc_value) { + // Load the field collection item + $fc_item_array = entity_load('field_collection_item', array($fc_value['value'])); + $fc_item = reset($fc_item_array); + $fc_untrans_key = "{$this->field_name}[$langcode][$delta]{$this->field_untrans_base}"; + $fc_trans_key = "{$this->field_name}[$langcode][$delta]{$this->field_trans_base}"; + + $this->assertEqual($untrans_field . '_' . $delta, $fc_item->{$this->field_untrans_name}[LANGUAGE_NONE][0]['value']); + $this->assertEqual($trans_field . '_' . $delta, $fc_item->{$this->field_trans_name}[$langcode][0]['value']); + } + } + + /** + * Returns the text field values of an specified node, language and delta + * + * @param mixed $node + * @param string $langcode + * @param integer $delta + * @return array + */ + protected function getFieldValues($node, $langcode, $delta) { + $return = array(); + + if (isset($node->{$this->field_name}[$langcode][$delta]['value'])) { + $fc_item_id = $node->{$this->field_name}[$langcode][$delta]['value']; + // Load the field collection + $fc_item_array = entity_load('field_collection_item', array($fc_item_id)); + $fc_item = reset($fc_item_array); + + $return = array( + 'field_untrans' => $fc_item->{$this->field_untrans_name}[LANGUAGE_NONE][0]['value'], + 'field_trans' => $fc_item->{$this->field_trans_name}[$langcode][0]['value'], + ); + } + + return $return; + } + + /** + * Ensures that field collections has the right behaviour in all Entity Translation use cases + */ + public function testEntityTranslation() { + $source_langcode = 'en'; + $translation_langcode = 'de'; + + /* + * Tests with a page with only one value in the field collection + */ + // Create an article in the original language with only one field collection value + $node = $this->createPage(1, $source_langcode); + + // Create a traslation of the page through the entity translation form + $node = $this->createTranslationForm($node, $translation_langcode, $source_langcode); + + /* + * Test with a page with multiple values in the field collection + */ + $num_values = 4; + // Create a page in the original language with multiple field collection values + $node = $this->createPage($num_values, $source_langcode); + + // Create a traslation of the page through the entity translation form + $node = $this->createTranslationForm($node, $translation_langcode, $source_langcode); + + // Assign a new field collection item to an existing node + $values = array(); + $values['field_name'] = $this->field_name; + $fc_entity = entity_create('field_collection_item', $values); + $fc_entity->setHostEntity('node', $node, $translation_langcode); + $fc_wrapper = entity_metadata_wrapper('field_collection_item', $fc_entity); + $fc_wrapper->{$this->field_untrans_name}->set(self::UNTRANS_FIELD_DE_MOD); + $fc_wrapper->{$this->field_trans_name}->set(self::TRANS_FIELD_DE_MOD); + $fc_wrapper->save(TRUE); + node_save($node); + + // Reload the node to check it + $node = node_load($node->nid, NULL, TRUE); + // Check that there is a new element in the translation + $this->assertEqual($num_values + 1, count($node->{$this->field_name}[$translation_langcode]), t('We have one item more in translation.')); + // Check that the new element is correctly saved + $fc_item_values = $this->getFieldValues($node, $translation_langcode, $num_values); + $this->assertEqual($fc_item_values['field_untrans'], self::UNTRANS_FIELD_DE_MOD); + $this->assertEqual($fc_item_values['field_trans'], self::TRANS_FIELD_DE_MOD); + // Check that we have the same items in the original language + $this->assertEqual($num_values, count($node->{$this->field_name}[$source_langcode]), t('We have same items in the original language.')); + + // Remove a field collection item from the translation + $fc_item_id = $node->{$this->field_name}[$translation_langcode][0]['value']; + unset($node->{$this->field_name}[$translation_langcode][0]); + node_save($node); + // Reload the node + $node = node_load($node->nid, NULL, TRUE); + // Check that we have one item less in the translation + // We should take into account that we added a field one step before + $this->assertEqual($num_values, count($node->{$this->field_name}[$translation_langcode]), t('We have one item less in translation.')); + // Check that we have the same items in the original language + $this->assertEqual($num_values, count($node->{$this->field_name}[$source_langcode]), t('We have same items in the original language.')); + // Check that the field collection is removed from the database + $fc_items = entity_load('field_collection_item', array($fc_item_id)); + $this->assert(empty($fc_items), t('The field collection item has been removed from the database.')); + + // Delete the translation + $this->removeTranslationForm($node, $translation_langcode, $source_langcode); + + /* + * Check the revisioning of an entity with translations + */ + $num_values = 4; + // Create a page in the original language with multiple field collection values + $node_rev = $this->createPage($num_values, $source_langcode); + + // Create a traslation of the page + $node_rev = $this->createTranslationForm($node_rev, $translation_langcode, $source_langcode); + + $original_revision = $node_rev->vid; + + // Create a new revision of the node + $node_rev = $this->createRevision($node_rev, $translation_langcode, $source_langcode); + + /* + * Test creating programmatically + */ + $num_values = 4; + // Create a page in the original language + $node_prog = $this->createPage($num_values, $source_langcode); + + // Create programmatically a translation of the page + $node_prog = $this->createTranslation($node_prog, $translation_langcode); + + $orig_fc_items = $node_prog->{$this->field_name}[$source_langcode]; + $trans_fc_items = $node_prog->{$this->field_name}[$translation_langcode]; + + $orig_fc_item_ids = array(); + $trans_fc_item_ids = array(); + + // Check each item + foreach ($orig_fc_items as $delta => $value) { + $orig_fc_item_ids[] = $value['value']; + $trans_fc_item_ids[] = $trans_fc_items[$delta]['value']; + + // Check if we have new items for the translation + $this->assertNotEqual($value['value'], $trans_fc_items[$delta]['value'], t('New item generated for translation.')); + } + + // Check that the original item still exists in the database + $fc_items = entity_load('field_collection_item', $orig_fc_item_ids); + $this->assert(!empty($fc_items), t('Field Collections in the source language still exist.')); + // Check that the translated item exists in the database + $fc_items = entity_load('field_collection_item', $trans_fc_item_ids); + $this->assert(!empty($fc_items), t('Translations for the Field Collection exist.')); + + // Remove the translation and check that the original field collection items are still there + $node_prog = $this->removeTranslation($node, $translation_langcode); + + // Check the content in the source language + $this->checkFieldCollectionContent($node_prog, $source_langcode); + + // Check that the field translated content has been removed + $this->assert(empty($node->{$this->field_name}[$translation_langcode]), t('Translated content removed.')); + + // Check that the field collection items have been removed from the database + // This test fails and it not really clear if it'a problem of field_collection or of + // Entity Translation that does not fires field_attach_update +// $fc_items = entity_load('field_collection_item', $trans_fc_item_ids); +// $this->assert(empty($fc_items), t('Translations for the Field Collection removed from the database.')); + } + +} diff --git a/includes/translation.handler.field_collection_item.inc b/includes/translation.handler.field_collection_item.inc new file mode 100644 index 0000000..2599013 --- /dev/null +++ b/includes/translation.handler.field_collection_item.inc @@ -0,0 +1,55 @@ +bundle != $entity_info['translation']['entity_translation']['default_scheme']) { + $this->setPathScheme($this->bundle); + } + } + + /** + * {@inheritdoc} + */ + public function getAccess($op) { + return field_collection_item_access($op, $this->entity); + } + + /** + * {@inheritdoc} + */ + public function getLanguage() { + // Do not use $this->entity->langcode() as this will finally call + // field_collection_entity_language() which again calls us! + // If the current field is untranslatable, try inherit the host entity + // language. + if (($host_entity_type = $this->entity->hostEntityType()) && entity_translation_enabled($host_entity_type) && ($host_entity = $this->entity->hostEntity())) { + $handler = $this->factory->getHandler($host_entity_type, $host_entity); + $langcode = $handler->getFormLanguage(); + } + // If the host entity is not translatable, use the default language + // fallback. + else { + $langcode = parent::getLanguage(); + } + return $langcode; + } + +}