diff --git a/field_collection.info b/field_collection.info index 4909157..62dc3cd 100644 --- a/field_collection.info +++ b/field_collection.info @@ -4,6 +4,7 @@ core = 7.x dependencies[] = entity files[] = field_collection.test 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 91edfbb..bb82014 100644 --- a/field_collection.install +++ b/field_collection.install @@ -281,3 +281,30 @@ function field_collection_update_7004() { } } } + +/** + * Update any fields inside field collections that were already set up to use + * Entity Translation before the patch for it was applied. + */ +function field_collection_update_7005() { + $fc_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); + $result = $query->execute(); + if (isset($result['field_collection_item'])) { + $fc_results = $fc_results + $result['field_collection_item']; + } + } + } + if (count($fc_results)) { + $ids = array_keys($fc_results); + $field_collection_items = entity_load('field_collection_item', $ids); + foreach ($field_collection_items as $item) { + $item->copy_translations(LANGUAGE_NONE); + $item->save(); + } + } +} diff --git a/field_collection.module b/field_collection.module index 6462014..009bd24 100644 --- a/field_collection.module +++ b/field_collection.module @@ -56,7 +56,12 @@ function field_collection_entity_info() { ), ), 'access callback' => 'field_collection_item_access', - '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 @@ -72,12 +77,49 @@ 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, + ); + } } return $return; } /** + * 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) { @@ -326,6 +368,32 @@ class FieldCollectionItemEntity extends Entity { } /** + * Updates the wrapped host entity object. + */ + public function updateHostEntity($entity) { + $this->fetchHostDetails(); + list($recieved_id) = entity_extract_ids($this->hostEntityType, $entity); + + if ($this->isInUse()) { + $current_id = $this->hostEntityId; + } else { + $current_host = entity_revision_load($this->hostEntityType, $this->hostEntityRevisionId); + list($current_id) = entity_extract_ids($this->hostEntityType, $current_host); + } + + 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'] = $entity; + } + } + else { + throw new Exception('The host entity cannot be changed.'); + } + } + + /** * Returns the host entity, which embeds this field collection item. */ public function hostEntity() { @@ -429,7 +497,7 @@ class FieldCollectionItemEntity extends Entity { * Determines the language code under which the item is stored. */ public function langcode() { - if ($this->delta() != NULL) { + if ($this->delta() !== NULL) { return $this->langcode; } } @@ -472,7 +540,12 @@ class FieldCollectionItemEntity extends Entity { if (!empty($this->is_new) && !(isset($this->hostEntityId) || isset($this->hostEntity) || isset($this->hostEntityRevisionId))) { 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->copy_translations(); + } + // 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). @@ -504,6 +577,29 @@ class FieldCollectionItemEntity extends Entity { } } + public function copy_translations($source_language = NULL) { + $host_et_handler = entity_translation_get_handler($this->hostEntityType(), $this->hostEntity()); + $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)); + $et_handler = entity_translation_get_handler($this->entityType(), $this); + $fields = array_keys(field_info_instances('field_collection_item', $this->field_name)); + + foreach ($fields as $t_field) { + foreach ($target_languages as $lang_code) { + if (isset($this->{$t_field}[$source_language])) { + $this->{$t_field}[$lang_code] = $this->{$t_field}[$source_language]; + } + } + if ($source_language == LANGUAGE_NONE && count($this->{$t_field}) > 1) { + $this->{$t_field}[$source_language] = NULL; + } + } + } + /** * Deletes the field collection item and the reference in the host entity. */ @@ -916,6 +1012,13 @@ function field_collection_field_insert($host_entity_type, $host_entity, $field, * 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_langcode = field_collection_entity_language($host_entity_type, $host_entity->original); + $langcode = $langcode == $original_langcode ? $entity_langcode : $langcode; + $items_original = !empty($host_entity->original->{$field['field_name']}[$langcode]) ? $host_entity->original->{$field['field_name']}[$langcode] : array(); $original_by_id = array_flip(field_collection_field_item_to_ids($items_original)); @@ -927,12 +1030,8 @@ function field_collection_field_update($host_entity_type, $host_entity, $field, if ($entity = field_collection_field_get_entity($item)) { - if (!empty($entity->is_new)) { - $entity->setHostEntity($host_entity_type, $host_entity, LANGUAGE_NONE, FALSE); - } - // 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; $is_default = entity_revision_is_default($host_entity_type, $host_entity); // If an entity type does not support saving non-default entities, @@ -942,6 +1041,14 @@ function field_collection_field_update($host_entity_type, $host_entity, $field, $entity->archived = 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( @@ -978,9 +1085,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. @@ -1096,6 +1202,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, @@ -1121,22 +1228,29 @@ 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.'), ); - $elements['delete'] = array( + $elements['translate'] = array( '#type' => 'textfield', - '#title' => t('Delete link title'), - '#default_value' => $settings['delete'], + '#title' => t('Translate link title'), + '#default_value' => $settings['translate'], '#description' => t('Leave the title empty, to hide the link.'), + '#access' => field_collection_item_is_translatable(), ); - $elements['add'] = array( + $elements['delete'] = array( '#type' => 'textfield', - '#title' => t('Add link title'), - '#default_value' => $settings['add'], + '#title' => t('Delete link title'), + '#default_value' => $settings['delete'], '#description' => t('Leave the title empty, to hide the link.'), ); $elements['description'] = array( @@ -1177,7 +1291,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)))); } @@ -1210,7 +1324,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())); @@ -1241,7 +1355,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]), @@ -1271,6 +1385,37 @@ 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)); + uksort($operations, "_field_collection_compare_operation_keys_by_name"); + + return $operations; +} + +/** + * Comparison function used in field_collection_get_operations + */ +function _field_collection_compare_operation_keys_by_name($a, $b) { + global $field_collection_operation_keys; + return $field_collection_operation_keys[$a] - $field_collection_operation_keys[$b]; +} + +/** * 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) { @@ -1279,7 +1424,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)) { $path = field_collection_field_get_path($field); @@ -1406,8 +1551,21 @@ function field_collection_field_widget_form(&$form, &$form_state, $field, $insta $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']; + $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'; + } + 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); + $entity_langcode = entity_language('field_collection_item', $field_collection_item); + field_attach_form('field_collection_item', $field_collection_item, $element, $form_state, $entity_langcode); if (empty($element['#required'])) { $element['#after_build'][] = 'field_collection_field_widget_embed_delay_required_validation'; @@ -1436,6 +1594,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. @@ -1607,7 +1774,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']); } @@ -1667,7 +1834,28 @@ 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. + if (isset($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); @@ -1677,13 +1865,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']))); } @@ -1710,6 +1896,10 @@ 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() @@ -1762,12 +1952,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'], @@ -1858,7 +2052,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); } /** @@ -1911,3 +2108,44 @@ function field_collection_devel_generate($object, $field, $instance, $bundle) { return array('value' => $field_collection->item_id); } + +/** + * 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); + } + } + } + } + } + } +} diff --git a/field_collection.pages.inc b/field_collection.pages.inc index 6e69266..a70beb4 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); $title = ($field['cardinality'] == 1) ? $instance['label'] : t('Add new !instance_label', array('!instance_label' => $field_collection_item->translatedInstanceLabel())); drupal_set_title($title); diff --git a/includes/translation.handler.field_collection_item.inc b/includes/translation.handler.field_collection_item.inc new file mode 100644 index 0000000..2eaea26 --- /dev/null +++ b/includes/translation.handler.field_collection_item.inc @@ -0,0 +1,49 @@ +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() { + $langcode = $this->entity->langcode() ? $this->entity->langcode() : LANGUAGE_NONE; + // Use the field language as entity language. If the current field is + // untranslatable inherit the host entity language. + if ($langcode == LANGUAGE_NONE && ($host_entity_type = $this->entity->hostEntityType()) && ($host_entity = $this->entity->hostEntity())) { + $handler = $this->factory->getHandler($host_entity_type, $host_entity); + $langcode = $handler->getLanguage(); + } + return $langcode; + } + +}