diff options
Diffstat (limited to 'includes/form.inc')
-rw-r--r-- | includes/form.inc | 285 |
1 files changed, 276 insertions, 9 deletions
diff --git a/includes/form.inc b/includes/form.inc index dc843d286..5de5ded31 100644 --- a/includes/form.inc +++ b/includes/form.inc @@ -255,6 +255,12 @@ function drupal_process_form($form_id, &$form) { // In that case we accept a submission without button values. if ((($form['#programmed']) || $form_submitted || (!$form_button_counter[0] && $form_button_counter[1])) && !form_get_errors()) { $redirect = drupal_submit_form($form_id, $form); + if ($batch =& batch_get()) { + $batch['progressive'] = !$form['#programmed']; + batch_process(); + // Progressive batch processing redirects to the progress page. + // Execution continues only if programmed form. + } if (!$form['#programmed']) { drupal_redirect_form($form, $redirect); } @@ -420,7 +426,19 @@ function drupal_submit_form($form_id, $form) { $args = array_merge($default_args, (array) $args); // Since we can only redirect to one page, only the last redirect // will work. - $redirect = call_user_func_array($function, $args); + if ($batch =& batch_get()) { + // Some previous _submit callback has set a batch. + // We store the call in a special 'control' batch set for execution + // at the correct time during the batch processing workflow. + $batch['sets'][] = array('form submit' => array($function, $args)); + } + else { + $redirect = call_user_func_array($function, $args); + if ($batch =& batch_get()) { + // The _submit callback has opened a batch: store the needed form info. + $batch['form_redirect'] = isset($form['#redirect']) ? $form['#redirect'] : NULL; + } + } $submitted = TRUE; if (isset($redirect)) { $goto = $redirect; @@ -1491,14 +1509,14 @@ function theme_markup($element) { } /** -* Format a password field. -* -* @param $element -* An associative array containing the properties of the element. -* Properties used: title, value, description, size, maxlength, required, attributes -* @return -* A themed HTML string representing the form. -*/ + * Format a password field. + * + * @param $element + * An associative array containing the properties of the element. + * Properties used: title, value, description, size, maxlength, required, attributes + * @return + * A themed HTML string representing the form. + */ function theme_password($element) { $size = $element['#size'] ? ' size="'. $element['#size'] .'" ' : ''; $maxlength = $element['#maxlength'] ? ' maxlength="'. $element['#maxlength'] .'" ' : ''; @@ -1625,3 +1643,252 @@ function form_clean_id($id = NULL) { /** * @} End of "defgroup form". */ + +/** + * @defgroup batch Batch operations + * @{ + * Functions allowing forms processing to be spread out over several page + * requests, thus ensuring that the processing does not get interrupted + * because of a PHP timeout, while allowing the user to receive feedback + * on the progress of the ongoing operations. + * + * The API is primarily designed to integrate nicely with the Form API + * workflow, but can also be used by non-FAPI scripts (like update.php) + * or even simple page callbacks (which should probably be used sparingly). + * + * Example: + * @code + * $batch = array( + * 'title' => t('Exporting'), + * 'operations' => array( + * array('my_function_1', array($account->uid, 'story')), + * array('my_function_2', array()), + * ), + * 'finished' => 'my_finished_callback', + * ); + * batch_set($batch); + * // only needed if not inside a form _submit callback : + * batch_process(); + * @endcode + * + * Sample batch operations: + * @code + * // Simple and artificial: load a node of a given type for a given user + * function my_function_1($uid, $type, &$context) { + * // The $context array gathers batch context information about the execution (read), + * // as well as 'return values' for the current operation (write) + * // The following keys are provided : + * // 'results' (read / write): The array of results gathered so far by + * // the batch processing, for the curent operation to append its own. + * // 'message' (write): A text message displayed in the progress page. + * // The following keys allow for multi-step operations : + * // 'sandbox' (read / write): An array that can be freely used to + * // store persistent data between iterations. It is recommended to + * // use this instead of $_SESSION, which is unsafe if the user + * // continues browsing in a separate window while the batch is processing. + * // 'finished' (write): A float number between 0 and 1 informing + * // the processing engine of the completion level for the operation. + * // 1 means the operation is finished and processing can continue + * // to the next operation. This value always be 1 if not specified + * // by the batch operation (a single-step operation), so that will + * // be considered as finished. + * $node = node_load(array('uid' => $uid, 'type' => $type)); + * $context['results'][] = $node->nid .' : '. $node->title; + * $context['message'] = $node->title; + * } + * + * // More advanced example: mutli-step operation - load all nodes, five by five + * function my_function_2(&$context) { + * if (empty($context['sandbox'])) { + * $context['sandbox']['progress'] = 0; + * $context['sandbox']['current_node'] = 0; + * $context['sandbox']['max'] = db_result(db_query('SELECT COUNT(DISTINCT nid) FROM {node}')); + * } + * $limit = 5; + * $result = db_query_range("SELECT nid FROM {node} WHERE nid > %d ORDER BY nid ASC", $context['sandbox']['current_node'], 0, $limit); + * while ($row = db_fetch_array($result)) { + * $node = node_load($row['nid'], NULL, TRUE); + * $context['results'][] = $node->nid .' : '. $node->title; + * $context['sandbox']['progress']++; + * $context['sandbox']['current_node'] = $node->nid; + * $context['message'] = $node->title; + * } + * if ($context['sandbox']['progress'] != $context['sandbox']['max']) { + * $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max']; + * } + * } + * @endcode + * + * Sample 'finished' callback: + * @code + * function batch_test_finished($success, $results, $operations) { + * if ($success) { + * $message = format_plural(count($results), 'One node processed.', '@count nodes processed.'); + * } + * else { + * $message = t('Finished with an error.'); + * } + * drupal_set_message($message); + * // Provinding data for the redirected page is done through $_SESSION. + * foreach ($results as $result) { + * $items[] = t('Loaded node %title.', array('%title' => $result)); + * } + * $_SESSION['my_batch_results'] = $items; + * } + * @endcode + */ + +/** + * Open a new batch. + * + * @param $batch + * An array defining the batch. The following keys can be used: + * 'operations': an array of function calls to be performed. + * Example: + * @code + * array( + * array('my_function_1', array($arg1)), + * array('my_function_2', array($arg2_1, $arg2_2)), + * ) + * @endcode + * All the other values below are optional. + * batch_init() provides default values for the messages. + * 'title': title for the progress page. + * Defaults to t('Processing'). + * 'init_message': message displayed while the processing is initialized. + * Defaults to t('Initializing.'). + * 'progress_message': message displayed while processing the batch. + * Available placeholders are @current, @remaining, @total and @percent. + * Defaults to t('Remaining @remaining of @total.'). + * 'error_message': message displayed if an error occurred while processing the batch. + * Defaults to t('An error has occurred.'). + * 'finished': the name of a function to be executed after the batch has completed. + * This should be used to perform any result massaging that may be needed, + * and possibly save data in $_SESSION for display after final page redirection. + * + * Operations are added as new batch sets. Batch sets are used to ensure + * clean code independency, ensuring that several batches submitted by + * different parts of the code (core / contrib modules) can be processed + * correctly while not interfering or having to cope with each other. Each + * batch set gets to specify his own UI messages, operates on it's own set + * of operations and results, and triggers it's own 'finished' callback. + * Batch sets are processed sequentially, with the progress bar starting + * fresh for every new set. + */ +function batch_set($batch_definition) { + if ($batch_definition) { + $batch =& batch_get(); + // Initialize the batch + if (empty($batch)) { + $batch = array( + 'id' => db_next_id('{batch}_bid'), + 'sets' => array(), + ); + } + + $init = array( + 'sandbox' => array(), + 'results' => array(), + 'success' => FALSE, + ); + // Use get_t() to allow batches at install time. + $t = get_t(); + $defaults = array( + 'title' => $t('Processing'), + 'init_message' => $t('Initializing.'), + 'progress_message' => $t('Remaining @remaining of @total.'), + 'error_message' => $t('An error has occurred.'), + ); + $batch_set = $init + $batch_definition + $defaults; + + // Tweak init_message to avoid the bottom of the page flickering down after init phase. + $batch_set['init_message'] .= '<br/> '; + $batch_set['total'] = count($batch_set['operations']); + + // If the batch is being processed (meaning we are executing a stored submit callback), + // insert the new set after the current one. + if (isset($batch['current_set'])) { + // array_insert does not exist... + $slice1 = array_slice($batch['sets'], 0, $batch['current_set'] + 1); + $slice2 = array_slice($batch['sets'], $batch['current_set'] + 1); + $batch['sets'] = array_merge($slice1, array($batch_set), $slice2); + } + else { + $batch['sets'][] = $batch_set; + } + } +} + +/** + * Process the batch. + * + * Unless the batch has been markes with 'progressive' = FALSE, the function + * isses a drupal_goto and thus ends page execution. + * + * This function is not needed in form submit callbacks; Form API takes care + * of batches issued during form submission. + * + * @param $redirect + * (optional) Path to redirect to when the batch has finished processing. + * @param $url + * (optional - should ony be used for separate scripts like update.php) + * URL of the batch processing page. + */ +function batch_process($redirect = NULL, $url = NULL) { + global $form_values, $user; + $batch =& batch_get(); + + // batch_process should not be called inside form _submit callbacks, or while a + // batch is already running. Neutralize the call if it is the case. + if (isset($batch['current_set']) || (isset($form_values) && !isset($batch['progressive']))) { + return; + } + + if (isset($batch)) { + // Add process information + $t = get_t(); + $url = isset($url) ? $url : 'batch'; + $process_info = array( + 'current_set' => 0, + 'progressive' => TRUE, + 'url' => isset($url) ? $url : 'batch', + 'source_page' => $_GET['q'], + 'redirect' => $redirect, + 'error_message' => $t('Please continue to <a href="!error_url">the error page</a>', array('!error_url' => url($url, array('query' => array('id' => $batch['id'], 'op' => 'error'))))), + ); + $batch += $process_info; + + if ($batch['progressive']) { + // Save and unset the destination if any. drupal_goto looks for redirection + // in $_REQUEST['destination'] and $_REQUEST['edit']['destination']. + if (isset($_REQUEST['destination'])) { + $batch['destination'] = $_REQUEST['destination']; + unset($_REQUEST['destination']); + } + elseif (isset($_REQUEST['edit']['destination'])) { + $batch['destination'] = $_REQUEST['edit']['destination']; + unset($_REQUEST['edit']['destination']); + } + db_query("INSERT INTO {batch} (bid, sid, timestamp, batch) VALUES (%d, %d, %d, '%s')", $batch['id'], $user->sid, time(), serialize($batch)); + drupal_goto($batch['url'], 'op=start&id='. $batch['id']); + } + else { + // Non-progressive execution: bypass the whole progressbar workflow + // and execute the batch in one pass. + require_once './includes/batch.inc'; + _batch_process(); + } + } +} + +/** + * Retrive the current batch. + */ +function &batch_get() { + static $batch = array(); + return $batch; +} + +/** + * @} End of "defgroup batch". + */ |