diff --git a/core/modules/field/modules/entity_reference/entity_reference.module b/core/modules/field/modules/entity_reference/entity_reference.module index 4540cc9..bd93686 100644 --- a/core/modules/field/modules/entity_reference/entity_reference.module +++ b/core/modules/field/modules/entity_reference/entity_reference.module @@ -75,16 +75,56 @@ function entity_reference_get_selection_handler($field, $instance, EntityInterfa * Implements hook_field_is_empty(). */ function entity_reference_field_is_empty($item, $field) { + if (!empty($item['target_id']) && $item['target_id'] == 'auto_create') { + // Allow auto-create entities. + return FALSE; + } return !isset($item['target_id']) || !is_numeric($item['target_id']); } /** + * Implements hook_field_presave(). + * + * Create an entity on the fly. + */ +function entity_reference_field_presave($entity_type, $entity, $field, $instance, $langcode, &$items) { + global $user; + $target_type = $field['settings']['target_type']; + $entity_info = entity_get_info($target_type); + + // Get the bundle. + if (count($instance['settings']['handler_settings']['target_bundles']) == 1) { + $bundle = reset($instance['settings']['handler_settings']['target_bundles']); + } + else { + $bundle = reset($entity_info['bundles']); + } + + foreach ($items as $delta => $item) { + if ($item['target_id'] == 'auto_create') { + $bundle_key = $entity_info['entity_keys']['bundle']; + $label_key = $entity_info['entity_keys']['label']; + $values = array( + $label_key => $item['label'], + $bundle_key => $bundle, + // @todo: Use wrapper to get the user if exists or needed. + 'uid' => !empty($entity->uid) ? $entity->uid : $user->uid, + ); + $target_entity = entity_create($target_type, $values); + $target_entity->save(); + $items[$delta]['target_id'] = $target_entity->id(); + } + } +} + + +/** * Implements hook_field_validate(). */ function entity_reference_field_validate($entity_type, $entity, $field, $instance, $langcode, $items, &$errors) { $ids = array(); foreach ($items as $delta => $item) { - if (!entity_reference_field_is_empty($item, $field) && $item['target_id'] !== NULL) { + if (!entity_reference_field_is_empty($item, $field) && $item['target_id'] !== 'auto_create') { $ids[$item['target_id']] = $delta; } } diff --git a/core/modules/field/modules/entity_reference/lib/Drupal/entity_reference/Plugin/Type/Selection/SelectionBroken.php b/core/modules/field/modules/entity_reference/lib/Drupal/entity_reference/Plugin/Type/Selection/SelectionBroken.php index 68172d7..a246866 100644 --- a/core/modules/field/modules/entity_reference/lib/Drupal/entity_reference/Plugin/Type/Selection/SelectionBroken.php +++ b/core/modules/field/modules/entity_reference/lib/Drupal/entity_reference/Plugin/Type/Selection/SelectionBroken.php @@ -56,7 +56,7 @@ public function validateReferencableEntities(array $ids) { /** * Implements SelectionInterface::validateAutocompleteInput(). */ - public function validateAutocompleteInput($input, &$element, &$form_state, $form) { } + public function validateAutocompleteInput($input, &$element, &$form_state, $form, $strict = TRUE) { } /** * Implements SelectionInterface::entityQueryAlter(). diff --git a/core/modules/field/modules/entity_reference/lib/Drupal/entity_reference/Plugin/Type/Selection/SelectionInterface.php b/core/modules/field/modules/entity_reference/lib/Drupal/entity_reference/Plugin/Type/Selection/SelectionInterface.php index 9430a90..2a27c37 100644 --- a/core/modules/field/modules/entity_reference/lib/Drupal/entity_reference/Plugin/Type/Selection/SelectionInterface.php +++ b/core/modules/field/modules/entity_reference/lib/Drupal/entity_reference/Plugin/Type/Selection/SelectionInterface.php @@ -48,13 +48,15 @@ public function validateReferencableEntities(array $ids); * Single string from autocomplete widget. * @param array $element * The form element to set a form error. + * @param boolean $strict + * If TRUE, and an element is not found issue a form error. * * @return integer|null * Value of a matching entity ID, or NULL if none. * * @see \Drupal\entity_reference\Plugin\field\widget::elementValidate() */ - public function validateAutocompleteInput($input, &$element, &$form_state, $form); + public function validateAutocompleteInput($input, &$element, &$form_state, $form, $strict = TRUE); /** * Allows the selection to alter the SelectQuery generated by EntityFieldQuery. diff --git a/core/modules/field/modules/entity_reference/lib/Drupal/entity_reference/Plugin/entity_reference/selection/SelectionBase.php b/core/modules/field/modules/entity_reference/lib/Drupal/entity_reference/Plugin/entity_reference/selection/SelectionBase.php index 4b8ccb5..2fb8b93 100644 --- a/core/modules/field/modules/entity_reference/lib/Drupal/entity_reference/Plugin/entity_reference/selection/SelectionBase.php +++ b/core/modules/field/modules/entity_reference/lib/Drupal/entity_reference/Plugin/entity_reference/selection/SelectionBase.php @@ -12,6 +12,7 @@ use Drupal\Core\Database\Query\AlterableInterface; use Drupal\Core\Database\Query\SelectInterface; use Drupal\Core\Entity\EntityInterface; +use Drupal\Component\Utility\NestedArray; use Drupal\entity_reference\Plugin\Type\Selection\SelectionInterface; /** @@ -64,16 +65,18 @@ public function __construct($field, $instance, EntityInterface $entity = NULL) { public static function settingsForm($field, $instance) { $entity_info = entity_get_info($field['settings']['target_type']); - // Merge-in default values. - $instance['settings'] += array( + $default_values = array( 'handler_settings' => array( 'target_bundles' => array(), 'sort' => array( 'type' => 'none', ), + 'auto_create' => FALSE, ), ); + $instance['settings'] = NestedArray::mergeDeep($default_values, $instance['settings']); + if (!empty($entity_info['entity_keys']['bundle'])) { $bundles = array(); foreach ($entity_info['bundles'] as $bundle_name => $bundle_info) { @@ -161,6 +164,13 @@ public static function settingsForm($field, $instance) { ); } + $form['auto_create'] = array( + '#type' => 'checkbox', + '#title' => t('Auto-create'), + '#description' => t('Allow creating new entities in autocomplete.'), + '#default_value' => $instance['settings']['handler_settings']['auto_create'], + ); + return $form; } @@ -221,15 +231,17 @@ public function validateReferencableEntities(array $ids) { /** * Implements SelectionInterface::validateAutocompleteInput(). */ - public function validateAutocompleteInput($input, &$element, &$form_state, $form) { + public function validateAutocompleteInput($input, &$element, &$form_state, $form, $strict = TRUE) { $entities = $this->getReferencableEntities($input, '=', 6); $params = array( '%value' => $input, '@value' => $input, ); if (empty($entities)) { - // Error if there are no entities available for a required field. - form_error($element, t('There are no entities matching "%value".', $params)); + if ($strict) { + // Error if there are no entities available for a required field. + form_error($element, t('There are no entities matching "%value".', $params)); + } } elseif (count($entities) > 5) { $params['@id'] = key($entities); @@ -270,7 +282,10 @@ public function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS') { $query = entity_query($target_type); if (!empty($this->instance['settings']['handler_settings']['target_bundles'])) { - $bundle_key = $entity_info['entity_keys']['bundle']; + // @todo: Taxonomy term's bundle key is vocabulary_machine_name, but + // entity_query() fails with it, so for now hardcode the vid, until + // http://drupal.org/node/1552396 is fixed. + $bundle_key = $target_type != 'taxonomy_term' ? $entity_info['entity_keys']['bundle'] : 'vid'; $query->condition($bundle_key, $this->instance['settings']['handler_settings']['target_bundles'], 'IN'); } diff --git a/core/modules/field/modules/entity_reference/lib/Drupal/entity_reference/Plugin/field/widget/AutocompleteTagsWidget.php b/core/modules/field/modules/entity_reference/lib/Drupal/entity_reference/Plugin/field/widget/AutocompleteTagsWidget.php index c347bc9..053f16a 100644 --- a/core/modules/field/modules/entity_reference/lib/Drupal/entity_reference/Plugin/field/widget/AutocompleteTagsWidget.php +++ b/core/modules/field/modules/entity_reference/lib/Drupal/entity_reference/Plugin/field/widget/AutocompleteTagsWidget.php @@ -50,23 +50,32 @@ public function formElement(array $items, $delta, array $element, $langcode, arr public function elementValidate($element, &$form_state, $form) { $value = array(); // If a value was entered into the autocomplete. + $handler = entity_reference_get_selection_handler($this->field, $this->instance); + $entity_info = entity_get_info($this->field['settings']['target_type']); + $auto_create = isset($this->instance['settings']['handler_settings']['auto_create']) ? $this->instance['settings']['handler_settings']['auto_create'] : FALSE; + if (!empty($element['#value'])) { $entities = drupal_explode_tags($element['#value']); $value = array(); foreach ($entities as $entity) { + $match = FALSE; + // Take "label (entity id)', match the id from parenthesis. if (preg_match("/.+\((\d+)\)/", $entity, $matches)) { - $value[] = array( - 'target_id' => $matches[1], - ); + $match = $matches[1]; } else { // Try to get a match from the input string when the user didn't use the // autocomplete but filled in a value manually. - $handler = entity_reference_get_selection_handler($this->field, $this->instance); - $value[] = array( - 'target_id' => $handler->validateAutocompleteInput($entity, $element, $form_state, $form), - ); + $match = $handler->validateAutocompleteInput($entity, $element, $form_state, $form, !$auto_create); + } + + if ($match) { + $value[] = array('target_id' => $match); + } + elseif ($auto_create && (count($this->instance['settings']['handler_settings']['target_bundles']) == 1 || count($entity_info['bundles']) == 1)) { + // Auto-create item. see entity_reference_field_presave(). + $value[] = array('target_id' => 'auto_create', 'label' => $entity); } } } diff --git a/core/modules/field/modules/entity_reference/lib/Drupal/entity_reference/Plugin/field/widget/AutocompleteWidget.php b/core/modules/field/modules/entity_reference/lib/Drupal/entity_reference/Plugin/field/widget/AutocompleteWidget.php index b62dc50..c0343bd 100644 --- a/core/modules/field/modules/entity_reference/lib/Drupal/entity_reference/Plugin/field/widget/AutocompleteWidget.php +++ b/core/modules/field/modules/entity_reference/lib/Drupal/entity_reference/Plugin/field/widget/AutocompleteWidget.php @@ -57,6 +57,8 @@ public function formElement(array $items, $delta, array $element, $langcode, arr * Overrides Drupal\entity_reference\Plugin\field\widget\AutocompleteWidgetBase::elementValidate() */ public function elementValidate($element, &$form_state, $form) { + $auto_create = isset($this->instance['settings']['handler_settings']['auto_create']) ? $this->instance['settings']['handler_settings']['auto_create'] : FALSE; + // If a value was entered into the autocomplete. $value = ''; if (!empty($element['#value'])) { @@ -68,7 +70,20 @@ public function elementValidate($element, &$form_state, $form) { // Try to get a match from the input string when the user didn't use the // autocomplete but filled in a value manually. $handler = entity_reference_get_selection_handler($this->field, $this->instance); - $value = $handler->validateAutocompleteInput($element['#value'], $element, $form_state, $form); + $value = $handler->validateAutocompleteInput($element['#value'], $element, $form_state, $form, !$auto_create); + } + + if (!$value && $auto_create && (count($this->instance['settings']['handler_settings']['target_bundles']) == 1 || count($entity_info['bundles']) == 1)) { + // Auto-create item. see entity_reference_field_presave(). + $value = array( + 'target_id' => 'auto_create', + 'label' => $element['#value'], + // Keep the weight property. + '_weight' => $element['#weight'], + ); + // Change the element['#parents'], so in form_set_value() we + // popualte the correct key. + array_pop($element['#parents']); } } form_set_value($element, $value, $form_state); diff --git a/core/modules/field/modules/entity_reference/lib/Drupal/entity_reference/Tests/EntityReferenceAutoCreateTest.php b/core/modules/field/modules/entity_reference/lib/Drupal/entity_reference/Tests/EntityReferenceAutoCreateTest.php new file mode 100644 index 0000000..87d53a9 --- /dev/null +++ b/core/modules/field/modules/entity_reference/lib/Drupal/entity_reference/Tests/EntityReferenceAutoCreateTest.php @@ -0,0 +1,111 @@ + 'Entity Reference auto-create', + 'description' => 'Tests creating new entity (e.g. taxonomy-term) from an autocomplete widget.', + 'group' => 'Entity Reference', + ); + } + + public static $modules = array('entity_reference', 'node'); + + function setUp() { + parent::setUp(); + + // Create a "referecning" and "referenced" node types. + $referencing = $this->drupalCreateContentType(); + $this->referencing_type = $referencing->type; + + $referenced = $this->drupalCreateContentType(); + $this->referenced_type = $referenced->type; + + $field = array( + 'translatable' => FALSE, + 'entity_types' => array(), + 'settings' => array( + 'target_type' => 'node', + ), + 'field_name' => 'test_field', + 'type' => 'entity_reference', + 'cardinality' => FIELD_CARDINALITY_UNLIMITED, + ); + + field_create_field($field); + + $instance = array( + 'label' => 'Entity reference field', + 'field_name' => 'test_field', + 'entity_type' => 'node', + 'bundle' => $referencing->type, + 'settings' => array( + 'handler' => 'base', + 'handler_settings' => array( + // Reference a single vocabulary. + 'target_bundles' => array( + $referenced->type, + ), + // Enable auto-create. + 'auto_create' => TRUE, + ), + ), + ); + + field_create_instance($instance); + } + + /** + * Assert creation on a new entity. + */ + public function testAutoCreate() { + $user1 = $this->drupalCreateUser(array('access content', "create $this->referencing_type content")); + $this->drupalLogin($user1); + + $new_title = $this->randomName(); + + // Assert referenced node does not exist. + $base_query = entity_query('node'); + $base_query + ->condition('type', $this->referenced_type) + ->condition('title', $new_title); + + $query = clone $base_query; + $result = $query->execute(); + $this->assertFalse($result, 'Referenced node does not exist yet.'); + + $edit = array( + 'title' => $this->randomName(), + 'test_field[und][0][target_id]' => $new_title, + ); + $this->drupalPost("node/add/$this->referencing_type", $edit, 'Save'); + + // Assert referenced node was created. + $query = clone $base_query; + $result = $query->execute(); + $this->assertTrue($result, 'Referenced node was created.'); + $referenced_nid = key($result); + + // Assert the referenced node is associated with referencing node. + $result = entity_query('node') + ->condition('type', $this->referencing_type) + ->execute(); + + $referencing_nid = key($result); + $referencing_node = node_load($referencing_nid); + $this->assertEqual($referenced_nid, $referencing_node->test_field[LANGUAGE_NOT_SPECIFIED][0]['target_id'], 'Term is referenced from the node.'); + } +} diff --git a/core/modules/field/modules/entity_reference/lib/Drupal/entity_reference/Tests/EntityReferenceSelectionSortTest.php b/core/modules/field/modules/entity_reference/lib/Drupal/entity_reference/Tests/EntityReferenceSelectionSortTest.php index 7b87a46..7d48215 100644 --- a/core/modules/field/modules/entity_reference/lib/Drupal/entity_reference/Tests/EntityReferenceSelectionSortTest.php +++ b/core/modules/field/modules/entity_reference/lib/Drupal/entity_reference/Tests/EntityReferenceSelectionSortTest.php @@ -62,8 +62,8 @@ public function testSort() { 'target_type' => 'node', ), 'field_name' => 'test_field', - 'type' => 'entityreference', - 'cardinality' => '1', + 'type' => 'entity_reference', + 'cardinality' => 1, ); $instance = array( diff --git a/core/modules/views/lib/Drupal/views/Plugin/entity_reference/selection/ViewsSelection.php b/core/modules/views/lib/Drupal/views/Plugin/entity_reference/selection/ViewsSelection.php index 1fdbaf1..ea6bd2e 100644 --- a/core/modules/views/lib/Drupal/views/Plugin/entity_reference/selection/ViewsSelection.php +++ b/core/modules/views/lib/Drupal/views/Plugin/entity_reference/selection/ViewsSelection.php @@ -189,7 +189,7 @@ public function validateReferencableEntities(array $ids) { /** * Implements Drupal\entity_reference\Plugin\Type\Selection\SelectionInterface::validateAutocompleteInput(). */ - public function validateAutocompleteInput($input, &$element, &$form_state, $form) { + public function validateAutocompleteInput($input, &$element, &$form_state, $form, $strict = TRUE) { return NULL; }