Commit 7cf3518b authored by Dries's avatar Dries
Browse files

- Patch #926016 by effulgentsia, chrisshattuck: several bugs when trying to...

- Patch #926016 by effulgentsia, chrisshattuck: several bugs when trying to remove files from a multivalued file/image field.
parent af2b5e7d
......@@ -649,7 +649,8 @@ function file_field_widget_process($element, &$form_state, $form) {
// file, the entire group of file fields is updated together.
if ($field['cardinality'] != 1) {
$new_path = preg_replace('/\/\d+\//', '/', $element['remove_button']['#ajax']['path'], 1);
$new_wrapper = preg_replace('/-\d+-/', '-', $element['remove_button']['#ajax']['wrapper'], 1);
$field_element = drupal_array_get_nested_value($form, array_slice($element['#array_parents'], 0, -1));
$new_wrapper = $field_element['#id'] . '-ajax-wrapper';
foreach (element_children($element) as $key) {
if (isset($element[$key]['#ajax'])) {
$element[$key]['#ajax']['path'] = $new_path;
......@@ -659,6 +660,15 @@ function file_field_widget_process($element, &$form_state, $form) {
unset($element['#prefix'], $element['#suffix']);
}
// Add another submit handler to the upload and remove buttons, to implement
// functionality needed by the field widget. This submit handler, along with
// the rebuild logic in file_field_widget_form() requires the entire field,
// not just the individual item, to be valid.
foreach (array('upload_button', 'remove_button') as $key) {
$element[$key]['#submit'][] = 'file_field_widget_submit';
$element[$key]['#limit_validation_errors'] = array(array_slice($element['#parents'], 0, -1));
}
return $element;
}
......@@ -722,6 +732,25 @@ function _file_field_get_description_from_element($element) {
return FALSE;
}
/**
* Submit handler for upload and remove buttons of file_generic fields.
*
* This runs in addition to and after file_managed_file_submit().
*
* @see file_managed_file_submit()
* @see file_field_widget_form()
* @see file_field_widget_process()
*/
function file_field_widget_submit($form, &$form_state) {
// During the form rebuild, file_field_widget_form() will create field item
// widget elements using re-indexed deltas, so clear out $form_state['input']
// to avoid a mismatch between old and new deltas. The rebuilt elements will
// have #default_value set appropriately for the current state of the field,
// so nothing is lost in doing this.
list($field_name, $langcode) = $form_state['triggering_element']['#parents'];
unset($form_state['input'][$field_name][$langcode]);
}
/**
* Returns HTML for an individual file upload widget.
*
......
......@@ -376,6 +376,7 @@ function file_managed_file_process($element, &$form_state, $form) {
'#value' => t('Upload'),
'#validate' => array(),
'#submit' => array('file_managed_file_submit'),
'#limit_validation_errors' => array($element['#parents']),
'#ajax' => $ajax_settings,
'#weight' => -5,
);
......@@ -389,16 +390,11 @@ function file_managed_file_process($element, &$form_state, $form) {
'#value' => t('Remove'),
'#validate' => array(),
'#submit' => array('file_managed_file_submit'),
'#limit_validation_errors' => array($element['#parents']),
'#ajax' => $ajax_settings,
'#weight' => -5,
);
// Limit validation errors to the file field only. The entire field is needed later
// by file_field_widget_form(), so the last element is sliced off the #parents array
// to avoid removing too much from $form_state['values'].
$element['upload_button']['#limit_validation_errors'] = array(array_slice($element['#parents'], 0, -1));
$element['remove_button']['#limit_validation_errors'] = array(array_slice($element['#parents'], 0, -1));
$element['fid'] = array(
'#type' => 'hidden',
'#value' => $fid,
......
......@@ -203,29 +203,21 @@ class FileFieldTestCase extends DrupalWebTestCase {
/**
* Test class to test file field upload and remove buttons, with and without AJAX.
* Test class to test file field widget, single and multi-valued, with and without AJAX, with public and private files.
*/
class FileFieldWidgetTestCase extends FileFieldTestCase {
public static function getInfo() {
return array(
'name' => 'File field widget test',
'description' => 'Test upload and remove buttons, with and without AJAX.',
'description' => 'Tests the file field widget, single and multi-valued, with and without AJAX, with public and private files.',
'group' => 'File',
);
}
/**
* Tests upload and remove buttons, with and without AJAX.
*
* @todo This function currently only tests the "remove" button of a single-
* valued field. Tests should be added for the "upload" button and for each
* button of a multi-valued field. Tests involving multiple AJAX steps on
* the same page will become easier after http://drupal.org/node/789186
* lands. Testing the "upload" button in AJAX context requires more
* investigation into how jQuery uploads files, so that drupalPostAJAX() can
* emulate that correctly.
* Tests upload and remove buttons, with and without AJAX, for a single-valued File field.
*/
function testWidget() {
function testSingleValuedWidget() {
// Use 'page' instead of 'article', so that the 'article' image field does
// not conflict with this test. If in the future the 'page' type gets its
// own default file or image field, this test can be made more robust by
......@@ -241,11 +233,14 @@ class FileFieldWidgetTestCase extends FileFieldTestCase {
foreach (array('nojs', 'js') as $type) {
// Create a new node with the uploaded file and ensure it got uploaded
// successfully.
// @todo This only tests a 'nojs' submission, because drupalPostAJAX()
// does not yet support file uploads.
$nid = $this->uploadNodeFile($test_file, $field_name, $type_name);
$node = node_load($nid, NULL, TRUE);
$node_file = (object) $node->{$field_name}[LANGUAGE_NONE][0];
$this->assertFileExists($node_file, t('New file saved to disk on node creation.'));
// Test file download.
// Ensure the file can be downloaded.
$this->drupalGet(file_create_url($node_file->uri));
$this->assertResponse(200, t('Confirmed that the generated URL is correct by downloading the shipped file.'));
......@@ -260,13 +255,8 @@ class FileFieldWidgetTestCase extends FileFieldTestCase {
$this->drupalPost(NULL, array(), t('Remove'));
break;
case 'js':
// @todo This can be simplified after http://drupal.org/node/789186
// lands.
preg_match('/jQuery\.extend\(Drupal\.settings, (.*?)\);/', $this->content, $matches);
$settings = drupal_json_decode($matches[1]);
$button = $this->xpath('//input[@type="submit" and @value="' . t('Remove') . '"]');
$button_id = (string) $button[0]['id'];
$this->drupalPostAJAX(NULL, array(), array((string) $button[0]['name'] => (string) $button[0]['value']), $settings['ajax'][$button_id]['url'], array(), array(), NULL, $settings['ajax'][$button_id]);
$this->drupalPostAJAX(NULL, array(), array((string) $button[0]['name'] => (string) $button[0]['value']));
break;
}
......@@ -279,34 +269,133 @@ class FileFieldWidgetTestCase extends FileFieldTestCase {
$node = node_load($nid, NULL, TRUE);
$this->assertTrue(empty($node->{$field_name}[LANGUAGE_NONE][0]['fid']), t('File was successfully removed from the node.'));
}
}
// Test partial form submissions using the Upload button on a multivalue field.
field_delete_field($field_name);
/**
* Tests upload and remove buttons, with and without AJAX, for a multi-valued File field.
*/
function testMultiValuedWidget() {
// Use 'page' instead of 'article', so that the 'article' image field does
// not conflict with this test. If in the future the 'page' type gets its
// own default file or image field, this test can be made more robust by
// using a custom node type.
$type_name = 'page';
$field_name = strtolower($this->randomName());
$this->createFileField($field_name, $type_name, array('cardinality' => 3));
$field = field_info_field($field_name);
$instance = field_info_instance('node', $field_name, $type_name);
$test_file = $this->getTestFile('text');
foreach (array('nojs', 'js') as $type) {
// Visit the node creation form, and upload 3 files. Since the field has
// cardinality of 3, ensure the "Upload" button is displayed until after
// the 3rd file, and after that, isn't displayed.
// @todo This is only testing a non-AJAX upload, because drupalPostAJAX()
// does not yet emulate jQuery's file upload.
$this->drupalGet("node/add/$type_name");
for ($delta = 0; $delta < 3; $delta++) {
$edit = array('files[' . $field_name . '_' . LANGUAGE_NONE . '_' . $delta . ']' => drupal_realpath($test_file->uri));
// If the Upload button doesn't exist, drupalPost() will automatically
// fail with an assertion message.
$this->drupalPost(NULL, $edit, t('Upload'));
}
$this->assertNoFieldByXpath('//input[@type="submit"]', t('Upload'), t('After uploading 3 files, the "Upload" button is no longer displayed.'));
// Test clicking each "Remove" button. For extra robustness, test them out
// of sequential order. They are 0-indexed, and get renumbered after each
// iteration, so array(1, 1, 0) means:
// - First remove the 2nd file.
// - Then remove what is then the 2nd file (was originally the 3rd file).
// - Then remove the first file.
$num_expected_remove_buttons = 3;
foreach (array(1, 1, 0) as $delta) {
// Ensure we have the expected number of Remove buttons, and that they
// are numbered sequentially.
$buttons = $this->xpath('//input[@type="submit" and @value="Remove"]');
$this->assertTrue(is_array($buttons) && count($buttons) === $num_expected_remove_buttons, t('There are %n "Remove" buttons displayed (JSMode=%type).', array('%n' => $num_expected_remove_buttons, '%type' => $type)));
foreach ($buttons as $i => $button) {
$this->assertIdentical((string) $button['name'], $field_name . '_' . LANGUAGE_NONE . '_' . $i . '_remove_button');
}
// "Click" the remove button (emulating either a nojs or js submission).
$button_name = $field_name . '_' . LANGUAGE_NONE . '_' . $delta . '_remove_button';
switch ($type) {
case 'nojs':
// drupalPost() takes a $submit parameter that is the value of the
// button whose click we want to emulate. Since we have multiple
// buttons with the value "Remove", and want to control which one we
// use, we change the value of the other ones to something else.
// Since non-clicked buttons aren't included in the submitted POST
// data, and since drupalPost() will result in $this being updated
// with a newly rebuilt form, this doesn't cause problems.
foreach ($buttons as $button) {
if ($button['name'] != $button_name) {
$button['value'] = 'DUMMY';
}
}
$this->drupalPost(NULL, array(), t('Remove'));
break;
case 'js':
// drupalPostAJAX() lets us target the button precisely, so we don't
// require the workaround used above for nojs.
$this->drupalPostAJAX(NULL, array(), array($button_name => t('Remove')));
break;
}
$num_expected_remove_buttons--;
// Ensure we have a single Upload button, and that it is numbered
// sequentially after the Remove buttons.
$buttons = $this->xpath('//input[@type="submit" and @value="Upload"]');
$this->assertTrue(is_array($buttons) && count($buttons) == 1 && ((string) $buttons[0]['name'] === ($field_name . '_' . LANGUAGE_NONE . '_' . $num_expected_remove_buttons . '_upload_button')), t('After removing a file, an "Upload" button is displayed (JSMode=%type).'));
}
$this->drupalGet("node/add/$type_name");
for ($delta = 0; $delta < 3; $delta++) {
$edit = array('files[' . $field_name . '_' . LANGUAGE_NONE . '_' . $delta . ']' => drupal_realpath($test_file->uri));
$this->drupalPost(NULL, $edit, t('Upload'));
// Ensure the page now has no Remove buttons.
$this->assertNoFieldByXPath('//input[@type="submit"]', t('Remove'), t('After removing all files, there is no "Remove" button displayed.', array('%n' => $num_expected_remove_buttons, '%type' => $type)));
// Save the node and ensure it does not have any files.
$this->drupalPost(NULL, array('title' => $this->randomName()), t('Save'));
$matches = array();
preg_match('/node\/([0-9]+)/', $this->getUrl(), $matches);
$nid = $matches[1];
$node = node_load($nid, NULL, TRUE);
$this->assertTrue(empty($node->{$field_name}[LANGUAGE_NONE][0]['fid']), t('Node was successfully saved without any files.'));
}
$this->assertNoFieldByXpath('//input[@type="submit"]', t('Upload'), t('After uploading 3 files, the "Upload" button is no longer displayed.'));
}
/**
* Tests a file field with a "Private files" upload destination setting.
*/
function testPrivateFileSetting() {
// Use 'page' instead of 'article', so that the 'article' image field does
// not conflict with this test. If in the future the 'page' type gets its
// own default file or image field, this test can be made more robust by
// using a custom node type.
$type_name = 'page';
$field_name = strtolower($this->randomName());
$this->createFileField($field_name, $type_name);
$field = field_info_field($field_name);
$instance = field_info_instance('node', $field_name, $type_name);
// Test private download method.
$test_file = $this->getTestFile('text');
// Change the field setting to make its files private, and upload a file.
$edit = array('field[settings][uri_scheme]' => 'private');
$this->drupalPost("admin/structure/types/manage/$type_name/fields/$field_name", $edit, t('Save settings'));
// Create a new node with the uploaded file and ensure it got uploaded
// successfully.
$nid = $this->uploadNodeFile($test_file, $field_name, $type_name);
$node = node_load($nid, NULL, TRUE);
$node_file = (object) $node->{$field_name}[LANGUAGE_NONE][0];
$this->assertFileExists($node_file, t('New file saved to disk on node creation.'));
// Test file download.
// Ensure the private file is available to the user who uploaded it.
$this->drupalGet(file_create_url($node_file->uri));
$this->assertResponse(200, t('Confirmed that the generated URL is correct by downloading the shipped file.'));
// Ensure we can't change 'uri_scheme' field settings while there are some
// entities with uploaded files.
$this->drupalGet("admin/structure/types/manage/$type_name/fields/$field_name");
$this->assertFieldByXpath('//input[@id="edit-field-settings-uri-scheme-public" and @disabled="disabled"]', 'public', t('Upload destination setting disabled.'));
// Delete node and confirm that setting could be changed.
node_delete($nid);
$this->drupalGet("admin/structure/types/manage/$type_name/fields/$field_name");
......
......@@ -1801,7 +1801,7 @@ protected function drupalPost($path, $edit, $submit, array $options = array(), a
*
* @see ajax.js
*/
protected function drupalPostAJAX($path, $edit, $triggering_element, $ajax_path = 'system/ajax', array $options = array(), array $headers = array(), $form_html_id = NULL, $ajax_settings = NULL) {
protected function drupalPostAJAX($path, $edit, $triggering_element, $ajax_path = NULL, array $options = array(), array $headers = array(), $form_html_id = NULL, $ajax_settings = NULL) {
// Get the content of the initial page prior to calling drupalPost(), since
// drupalPost() replaces $this->content.
if (isset($path)) {
......@@ -1838,6 +1838,12 @@ protected function drupalPostAJAX($path, $edit, $triggering_element, $ajax_path
$extra_post .= '&' . urlencode('ajax_html_ids[]') . '=' . urlencode($id);
}
// Unless a particular path is specified, use the one specified by the
// AJAX settings, or else 'system/ajax'.
if (!isset($ajax_path)) {
$ajax_path = isset($ajax_settings['url']) ? $ajax_settings['url'] : 'system/ajax';
}
// Submit the POST request.
$return = drupal_json_decode($this->drupalPost(NULL, $edit, array('path' => $ajax_path, 'triggering_element' => $triggering_element), $options, $headers, $form_html_id, $extra_post));
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment