Skip to content
admin.inc 184 KiB
Newer Older
<?php
/**
 * @file admin.inc
 * Provides the Views' administrative interface.
 */

/**
 * Create an array of Views admin CSS for adding or attaching.
 *
 * This returns an array of arrays. Each array represents a single
 * file. The array format is:
 * - file: The fully qualified name of the file to send to drupal_add_css
 * - options: An array of options to pass to drupal_add_css.
function views_ui_get_admin_css() {
  $module_path = drupal_get_path('module', 'views_ui');
  $list = array();
  $list[$module_path . '/css/views-admin.css'] = array();

  $list[$module_path . '/css/ie/views-admin.ie7.css'] = array(
    'browsers' => array(
      'IE' => 'lte IE 7',
      '!IE' => FALSE
    ),
    'preprocess' => FALSE,
  $list[$module_path . '/css/views-admin.theme.css'] = array();

  // Add in any theme specific CSS files we have
  $themes = list_themes();
  $theme_key = $GLOBALS['theme'];
  while ($theme_key) {
    $list[$module_path . "/css/views-admin.$theme_key.css"] = array(
      'group' => CSS_THEME,
    $theme_key = isset($themes[$theme_key]->base_theme) ? $themes[$theme_key]->base_theme : '';
Jesse Beach's avatar
Jesse Beach committed
  // Views contains style overrides for the following modules
  $module_list = array('contextual', 'advanced_help', 'ctools');
Jesse Beach's avatar
Jesse Beach committed
  foreach ($module_list as $module) {
    if (module_exists($module)) {
      $list[$module_path . '/css/views-admin.' . $module . '.css'] = array();
    }

  return $list;
}

/**
 * Adds standard Views administration CSS to the current page.
 */
function views_ui_add_admin_css() {
  foreach (views_ui_get_admin_css() as $file => $options) {
    drupal_add_css($file, $options);
}

/**
 * Check to see if the advanced help module is installed, and if not put up
 * a message.
 *
 * Only call this function if the user is already in a position for this to
 * be useful.
 */
function views_ui_check_advanced_help() {
Daniel Wehner's avatar
Daniel Wehner committed
  if (!variable_get('views_ui_show_advanced_help_warning', TRUE)) {
    return;
  }

  if (!module_exists('advanced_help')) {
    $filename = db_query_range("SELECT filename FROM {system} WHERE type = 'module' AND name = 'advanced_help'", 0, 1)
      ->fetchField();
    if ($filename && file_exists($filename)) {
Daniel Wehner's avatar
Daniel Wehner committed
      drupal_set_message(t('If you <a href="@modules">enable the advanced help module</a>, Views will provide more and better help. <a href="@hide">Hide this message.</a>', array('@modules' => url('admin/modules'),'@hide' => url('admin/structure/views/settings'))));
Daniel Wehner's avatar
Daniel Wehner committed
      drupal_set_message(t('If you install the advanced help module from !href, Views will provide more and better help. <a href="@hide">Hide this message.</a>', array('!href' => l('http://drupal.org/project/advanced_help', 'http://drupal.org/project/advanced_help'), '@hide' => url('admin/structure/views/settings'))));
 * #pre_render callback for the live preview element.
 *
 * Delaying the preview generation until the rendering phase ensures that time
 * isn't wasted generating a preview if it won't be displayed (for example, if
 * after processing the Edit form, a redirect is issued).
function views_ui_pre_render_preview($element) {
  $preview = views_ui_preview($element['#view'], $element['#display_id'], $element['#preview_args']);
  // @todo HTML does not support a FORM inside a FORM. Remove this hack after
  //   fixing Views UI to display the preview outside of the edit form, so that
  //   we don't have to mess with the exposed widget form in this way.
  $preview = preg_replace('@</?form.*?>@', '', $preview);
  $element['#markup'] = $preview;
  return $element;
 * Returns the results of the live preview.
function views_ui_preview($view, $display_id, $args = array()) {
  // When this function is invoked as a page callback, each Views argument is
  // passed separately.
  if (!is_array($args)) {
    $args = array_slice(func_get_args(), 2);
  // Save $_GET['q'] so it can be restored before returning from this function.
  $q = $_GET['q'];
  // Determine where the query and performance statistics should be output.
  $show_query = variable_get('views_ui_show_sql_query', FALSE);
  $show_info = variable_get('views_ui_show_preview_information', FALSE);
  $show_location = variable_get('views_ui_show_sql_query_where', 'above');

  $show_stats = variable_get('views_ui_show_performance_statistics', FALSE);
  if ($show_stats) {
    $show_stats = variable_get('views_ui_show_sql_query_where', 'above');
  $rows = array('query' => array(), 'statistics' => array());
  $output = '';

  $errors = $view->validate();
  if ($errors === TRUE) {
    $view->ajax = TRUE;
    $view->live_preview = TRUE;
Earl Miles's avatar
Earl Miles committed

    // AJAX happens via $_POST but everything expects exposed data to
    // be in GET. Copy stuff but remove ajax-framework specific keys.
    $exposed_input = $_POST;
Earl Miles's avatar
Earl Miles committed
    foreach (array('view_name', 'view_display_id', 'view_args', 'view_path', 'view_dom_id', 'pager_element', 'view_base_path', 'ajax_html_ids', 'ajax_page_state') as $key) {
Earl Miles's avatar
Earl Miles committed
      if (isset($exposed_input[$key])) {
        unset($exposed_input[$key]);
      }
    }
    // @todo Remove this hack after fixing Views UI to display the preview
    //   outside of the edit form, so that the edit form doesn't conflict with
    //   the exposed widget form.
    $exposed_input['form_id'] = 'views_exposed_form';
    unset($exposed_input['form_build_id'], $exposed_input['form_token']);
Earl Miles's avatar
Earl Miles committed

    $view->set_exposed_input($exposed_input);

    // Store the current view URL for later use:
    $view->set_display($display_id);
    $view->set_arguments($args);

    if ($view->display_handler->get_option('path')) {
      $path = $view->get_url();
    }

    // Make view links come back to preview.
    $view->override_path = 'admin/structure/views/nojs/preview/' . $view->name . '/' . $display_id;
    // Also override $_GET['q'] so we get the pager.
    $_GET['q'] = $view->override_path;
    if ($args) {
      $_GET['q'] .= '/' . implode('/', $args);
    // Suppress contextual links of entities within the result set during a
    // Preview.
    // @todo We'll want to add contextual links specific to editing the View, so
    //   the suppression may need to be moved deeper into the Preview pipeline.
    views_ui_contextual_links_suppress_push();
    try {
      $preview = $view->preview($display_id, $args);
    }
    catch (Exception $e) {
      drupal_set_message($e->getMessage(), 'error');
    }
    views_ui_contextual_links_suppress_pop();
    // Prepare the query information and statistics to show either above or
    // below the view preview.
    if ($show_info || $show_query || $show_stats) {
      // Get information from the preview for display.
      if (!empty($view->build_info['query'])) {
        if ($show_query) {
          $query = $view->build_info['query'];
          // Only the sql default class has a method getArguments.
          $quoted = array();

          if (get_class($view->query) == 'views_plugin_query_default') {
            $arguments = $query->getArguments();
            $connection = Database::getConnection();
            foreach ((array)$query->arguments() as $key => $val) {
              $quoted[$key] = $connection->quote($val);
          $rows['query'][] = array('<strong>' . t('Query') . '</strong>', '<pre>' . check_plain(strtr($query, $quoted)) . '</pre>');
          if (!empty($view->additional_queries)) {
            $queries = '<strong>' . t('These queries were run during view rendering:') . '</strong>';
            foreach ($view->additional_queries as $query) {
              if ($queries) {
                $queries .= "\n";
              }
              $queries .= t('[@time ms]', array('@time' => intval($query[1] * 100000) / 100)) . ' ' . $query[0];
            }
            $rows['query'][] = array('<strong>' . t('Other queries') . '</strong>', '<pre>' . $queries . '</pre>');
          $rows['query'][] = array('<strong>' . t('Title') . '</strong>', filter_xss_admin($view->get_title()));
          if (isset($path)) {
            $path = l($path, $path);
          }
          else {
            $path = t('This display has no path.');
          }
          $rows['query'][] = array('<strong>' . t('Path') . '</strong>', $path);
        if ($show_stats) {
          $rows['statistics'][] = array('<strong>' . t('Query build time') . '</strong>', t('@time ms', array('@time' => intval($view->build_time * 100000) / 100)));
          $rows['statistics'][] = array('<strong>' . t('Query execute time') . '</strong>', t('@time ms', array('@time' => intval($view->execute_time * 100000) / 100)));
          $rows['statistics'][] = array('<strong>' . t('View render time') . '</strong>', t('@time ms', array('@time' => intval($view->render_time * 100000) / 100)));
        drupal_alter('views_preview_info', $rows, $view);
        // No query was run. Display that information in place of either the
        // query or the performance statistics, whichever comes first.
        if ($combined || ($show_location === 'above')) {
          $rows['query'] = array(array('<strong>' . t('Query') . '</strong>', t('No query was run')));
        }
        else {
          $rows['statistics'] = array(array('<strong>' . t('Query') . '</strong>', t('No query was run')));
        }
      }
    }
  }
  else {
    foreach ($errors as $error) {
      drupal_set_message($error, 'error');
    }
    $preview = t('Unable to preview due to validation errors.');
  }

  // Assemble the preview, the query info, and the query statistics in the
  // requested order.
  if ($show_location === 'above') {
    if ($combined) {
      $output .= '<div class="views-query-info">' . theme('table', array('rows' => array_merge($rows['query'], $rows['statistics']))) . '</div>';
    }
    else {
      $output .= '<div class="views-query-info">' . theme('table', array('rows' => $rows['query'])) . '</div>';
    }
  elseif ($show_stats === 'above') {
    $output .= '<div class="views-query-info">' . theme('table', array('rows' => $rows['statistics'])) . '</div>';
  if ($show_location === 'below') {
    if ($combined) {
      $output .= '<div class="views-query-info">' . theme('table', array('rows' => array_merge($rows['query'], $rows['statistics']))) . '</div>';
    else {
      $output .= '<div class="views-query-info">' . theme('table', array('rows' => $rows['query'])) . '</div>';
  elseif ($show_stats === 'below') {
    $output .= '<div class="views-query-info">' . theme('table', array('rows' => $rows['statistics'])) . '</div>';
  return $output;
 * AJAX callback that returns the preview section of the edit form.
function views_ui_preview_callback($form, $form_state) {
  return $form['displays']['preview'];
}

/**
 * Page callback to add a new view.
 */
function views_ui_add_page() {
  return drupal_get_form('views_ui_add_form');
 * Form builder for the "add new view" page.
 */
function views_ui_add_form($form, &$form_state) {
  ctools_include('dependent');
  $form['#attached']['js'][] = drupal_get_path('module', 'views_ui') . '/js/views-admin.js';
  $form['#attributes']['class'] = array('views-admin');
    '#type' => 'textfield',
    '#title' => t('View name'),
    '#required' => TRUE,
    '#default_value' => !empty($form_state['view']) ? $form_state['view']->human_name : '',
  $form['name'] = array(
    '#type' => 'machine_name',
    '#maxlength' => 32,
    '#machine_name' => array(
      'exists' => 'views_get_view',
      'source' => array('human_name'),
    ),
    '#description' => t('A unique machine-readable name for this View. It must only contain lowercase letters, numbers, and underscores.'),
  $form['description_enable'] = array(
    '#type' => 'checkbox',
    '#title' => t('Description'),
  );
  $form['description'] = array(
    '#type' => 'textfield',
    '#title' => t('Provide description'),
    '#title_display' => 'invisible',
    '#default_value' => !empty($form_state['view']) ? $form_state['view']->description : '',
    '#dependency' => array(
      'edit-description-enable' => array(1),
  // Create a wrapper for the entire dynamic portion of the form. Everything
  // that can be updated by AJAX goes somewhere inside here. For example, this
  // is needed by "Show" dropdown (below); it changes the base table of the
  // view and therefore potentially requires all options on the form to be
  // dynamically updated.
  $form['displays'] = array();

  // Create the part of the form that allows the user to select the basic
  // properties of what the view will display.
    '#type' => 'fieldset',
    '#tree' => TRUE,
    '#attributes' => array('class' => array('container-inline')),
  // Create the "Show" dropdown, which allows the base table of the view to be
  // selected.
  $wizard_plugins = views_ui_get_wizards();
  foreach ($wizard_plugins as $key => $w) {
    $options[$key] = $w['title'];
  $form['displays']['show']['wizard_key'] = array(
    '#type' => 'select',
    '#title' => t('Show'),
    '#options' => $options,
  $show_form = &$form['displays']['show'];
  $show_form['wizard_key']['#default_value'] = views_ui_get_selected($form_state, array('show', 'wizard_key'), 'node', $show_form['wizard_key']);
  // Changing this dropdown updates the entire content of $form['displays'] via
  // AJAX.
  views_ui_add_ajax_trigger($show_form, 'wizard_key', array('displays'));

  // Build the rest of the form based on the currently selected wizard plugin.
  $wizard_key = $show_form['wizard_key']['#default_value'];
  $get_instance = $wizard_plugins[$wizard_key]['get_instance'];
  $wizard_instance = $get_instance($wizard_plugins[$wizard_key]);
  $form = $wizard_instance->build_form($form, $form_state);
  $form['save'] = array(
    '#type' => 'submit',
    '#value' => t('Save & exit'),
    '#validate' => array('views_ui_wizard_form_validate'),
    '#submit' => array('views_ui_add_form_save_submit'),
  );
    '#type' => 'submit',
    '#value' => t('Continue & edit'),
    '#validate' => array('views_ui_wizard_form_validate'),
    '#submit' => array('views_ui_add_form_store_edit_submit'),
    '#process' => array_merge(array('views_ui_default_button'), element_info_property('submit', '#process', array())),
  $form['cancel'] = array(
    '#type' => 'submit',
    '#value' => t('Cancel'),
    '#submit' => array('views_ui_add_form_cancel_submit'),
    '#limit_validation_errors' => array(),
  return $form;
}
 * Gets the current value of a #select element, from within a form constructor function.
 *
 * This function is intended for use in highly dynamic forms (in particular the
 * add view wizard) which are rebuilt in different ways depending on which
 * triggering element (AJAX or otherwise) was most recently fired. For example,
 * sometimes it is necessary to decide how to build one dynamic form element
 * based on the value of a different dynamic form element that may not have
 * even been present on the form the last time it was submitted. This function
 * takes care of resolving those conflicts and gives you the proper current
 * value of the requested #select element.
 *
 * By necessity, this function sometimes uses non-validated user input from
 * $form_state['input'] in making its determination. Although it performs some
 * minor validation of its own, it is not complete. The intention is that the
 * return value of this function should only be used to help decide how to
 * build the current form the next time it is reloaded, not to be saved as if
 * it had gone through the normal, final form validation process. Do NOT use
 * the results of this function for any other purpose besides deciding how to
 * build the next version of the form.
 *
 * @param $form_state
 *   The  standard associative array containing the current state of the form.
 * @param $parents
 *   An array of parent keys that point to the part of the submitted form
 *   values that are expected to contain the element's value (in the case where
 *   this form element was actually submitted). In a simple case (assuming
 *   #tree is TRUE throughout the form), if the select element is located in
 *   $form['wrapper']['select'], so that the submitted form values would
 *   normally be found in $form_state['values']['wrapper']['select'], you would
 *   pass array('wrapper', 'select') for this parameter.
 * @param $default_value
 *   The default value to return if the #select element does not currently have
 *   a proper value set based on the submitted input.
 * @param $element
 *   An array representing the current version of the #select element within
 *   the form.
 *
 * @return
 *   The current value of the #select element. A common use for this is to feed
 *   it back into $element['#default_value'] so that the form will be rendered
 *   with the correct value selected.
function views_ui_get_selected($form_state, $parents, $default_value, $element) {
  // For now, don't trust this to work on anything but a #select element.
  if (!isset($element['#type']) || $element['#type'] != 'select' || !isset($element['#options'])) {
    return $default_value;
  }

  // If there is a user-submitted value for this element that matches one of
  // the currently available options attached to it, use that. We need to check
  // $form_state['input'] rather than $form_state['values'] here because the
  // triggering element often has the #limit_validation_errors property set to
  // prevent unwanted errors elsewhere on the form. This means that the
  // $form_state['values'] array won't be complete. We could make it complete
  // by adding each required part of the form to the #limit_validation_errors
  // property individually as the form is being built, but this is difficult to
  // do for a highly dynamic and extensible form. This method is much simpler.
  if (!empty($form_state['input'])) {
    $key_exists = NULL;
    $submitted = drupal_array_get_nested_value($form_state['input'], $parents, $key_exists);
    // Check that the user-submitted value is one of the allowed options before
    // returning it. This is not a substitute for actual form validation;
    // rather it is necessary because, for example, the same select element
    // might have #options A, B, and C under one set of conditions but #options
    // D, E, F under a different set of conditions. So the form submission
    // might have occurred with option A selected, but when the form is rebuilt
    // option A is no longer one of the choices. In that case, we don't want to
    // use the value that was submitted anymore but rather fall back to the
    // default value.
    if ($key_exists && in_array($submitted, array_keys($element['#options']))) {
      return $submitted;
    }
  // Fall back on returning the default value if nothing was returned above.
  return $default_value;
 * Converts a form element in the add view wizard to be AJAX-enabled.
 * This function takes a form element and adds AJAX behaviors to it such that
 * changing it triggers another part of the form to update automatically. It
 * also adds a submit button to the form that appears next to the triggering
 * element and that duplicates its functionality for users who do not have
 * JavaScript enabled (the button is automatically hidden for users who do have
 * JavaScript).
 *
 * To use this function, call it directly from your form builder function
 * immediately after you have defined the form element that will serve as the
 * JavaScript trigger. Calling it elsewhere (such as in hook_form_alter()) may
 * mean that the non-JavaScript fallback button does not appear in the correct
 * place in the form.
 *
 * @param $wrapping_element
 *   The element whose child will server as the AJAX trigger. For example, if
 *   $form['some_wrapper']['triggering_element'] represents the element which
 *   will trigger the AJAX behavior, you would pass $form['some_wrapper'] for
 *   this parameter.
 * @param $trigger_key
 *   The key within the wrapping element that identifies which of its children
 *   serves as the AJAX trigger. In the above example, you would pass
 *   'triggering_element' for this parameter.
 * @param $refresh_parents
 *   An array of parent keys that point to the part of the form that will be
 *   refreshed by AJAX. For example, if triggering the AJAX behavior should
 *   cause $form['dynamic_content']['section'] to be refreshed, you would pass
 *   array('dynamic_content', 'section') for this parameter.
function views_ui_add_ajax_trigger(&$wrapping_element, $trigger_key, $refresh_parents) {
  $seen_ids = &drupal_static(__FUNCTION__ . ':seen_ids', array());
  $seen_buttons = &drupal_static(__FUNCTION__ . ':seen_buttons', array());

  // Add the AJAX behavior to the triggering element.
  $triggering_element = &$wrapping_element[$trigger_key];
  $triggering_element['#ajax']['callback'] = 'views_ui_ajax_update_form';
  // We do not use drupal_html_id() to get an ID for the AJAX wrapper, because
  // it remembers IDs across AJAX requests (and won't reuse them), but in our
  // case we need to use the same ID from request to request so that the
  // wrapper can be recognized by the AJAX system and its content can be
  // dynamically updated. So instead, we will keep track of duplicate IDs
  // (within a single request) on our own, later in this function.
  $triggering_element['#ajax']['wrapper'] = 'edit-view-' . implode('-', $refresh_parents) . '-wrapper';

  // Add a submit button for users who do not have JavaScript enabled. It
  // should be displayed next to the triggering element on the form.
  $button_key = $trigger_key . '_trigger_update';
  $wrapping_element[$button_key] = array(
    '#type' => 'submit',
    // Hide this button when JavaScript is enabled.
    '#attributes' => array('class' => array('js-hide')),
    '#submit' => array('views_ui_nojs_submit'),
    // Add a process function to limit this button's validation errors to the
    // triggering element only. We have to do this in #process since until the
    // form API has added the #parents property to the triggering element for
    // us, we don't have any (easy) way to find out where its submitted values
    // will eventually appear in $form_state['values'].
    '#process' => array_merge(array('views_ui_add_limited_validation'), element_info_property('submit', '#process', array())),
    // Add an after-build function that inserts a wrapper around the region of
    // the form that needs to be refreshed by AJAX (so that the AJAX system can
    // detect and dynamically update it). This is done in #after_build because
    // it's a convenient place where we have automatic access to the complete
    // form array, but also to minimize the chance that the HTML we add will
    // get clobbered by code that runs after we have added it.
    '#after_build' => array_merge(element_info_property('submit', '#after_build', array()), array('views_ui_add_ajax_wrapper')),
  );
  // Copy #weight and #access from the triggering element to the button, so
  // that the two elements will be displayed together.
  foreach (array('#weight', '#access') as $property) {
    if (isset($triggering_element[$property])) {
      $wrapping_element[$button_key][$property] = $triggering_element[$property];
    }
  }
  // For easiest integration with the form API and the testing framework, we
  // always give the button a unique #value, rather than playing around with
  // #name.
  $button_title = !empty($triggering_element['#title']) ? $triggering_element['#title'] : $trigger_key;
  if (empty($seen_buttons[$button_title])) {
    $wrapping_element[$button_key]['#value'] = t('Update "@title" choice', array(
      '@title' => $button_title,
    ));
    $seen_buttons[$button_title] = 1;
  }
  else {
    $wrapping_element[$button_key]['#value'] = t('Update "@title" choice (@number)', array(
      '@title' => $button_title,
      '@number' => ++$seen_buttons[$button_title],
   ));
  }

  // Attach custom data to the triggering element and submit button, so we can
  // use it in both the process function and AJAX callback.
  $ajax_data = array(
    'wrapper' => $triggering_element['#ajax']['wrapper'],
    'trigger_key' => $trigger_key,
    'refresh_parents' => $refresh_parents,
    // Keep track of duplicate wrappers so we don't add the same wrapper to the
    // page more than once.
    'duplicate_wrapper' => !empty($seen_ids[$triggering_element['#ajax']['wrapper']]),
  $seen_ids[$triggering_element['#ajax']['wrapper']] = TRUE;
  $triggering_element['#views_ui_ajax_data'] = $ajax_data;
  $wrapping_element[$button_key]['#views_ui_ajax_data'] = $ajax_data;
Daniel Wehner's avatar
Daniel Wehner committed
/**
 * Processes a non-JavaScript fallback submit button to limit its validation errors.
Daniel Wehner's avatar
Daniel Wehner committed
 */
function views_ui_add_limited_validation($element, &$form_state) {
  // Retrieve the AJAX triggering element so we can determine its parents. (We
  // know it's at the same level of the complete form array as the submit
  // button, so all we have to do to find it is swap out the submit button's
  // last array parent.)
  $array_parents = $element['#array_parents'];
  array_pop($array_parents);
  $array_parents[] = $element['#views_ui_ajax_data']['trigger_key'];
  $ajax_triggering_element = drupal_array_get_nested_value($form_state['complete form'], $array_parents);

  // Limit this button's validation to the AJAX triggering element, so it can
  // update the form for that change without requiring that the rest of the
  // form be filled out properly yet.
  $element['#limit_validation_errors'] = array($ajax_triggering_element['#parents']);

  // If we are in the process of a form submission and this is the button that
  // was clicked, the form API workflow in form_builder() will have already
  // copied it to $form_state['triggering_element'] before our #process
  // function is run. So we need to make the same modifications in $form_state
  // as we did to the element itself, to ensure that #limit_validation_errors
  // will actually be set in the correct place.
  if (!empty($form_state['triggering_element'])) {
    $clicked_button = &$form_state['triggering_element'];
    if ($clicked_button['#name'] == $element['#name'] && $clicked_button['#value'] == $element['#value']) {
      $clicked_button['#limit_validation_errors'] = $element['#limit_validation_errors'];
    }
  }

  return $element;
 * After-build function that adds a wrapper to a form region (for AJAX refreshes).
Daniel Wehner's avatar
Daniel Wehner committed
 *
 * This function inserts a wrapper around the region of the form that needs to
 * be refreshed by AJAX, based on information stored in the corresponding
 * submit button form element.
function views_ui_add_ajax_wrapper($element, &$form_state) {
  // Don't add the wrapper <div> if the same one was already inserted on this
  // form.
  if (empty($element['#views_ui_ajax_data']['duplicate_wrapper'])) {
    // Find the region of the complete form that needs to be refreshed by AJAX.
    // This was earlier stored in a property on the element.
    $complete_form = &$form_state['complete form'];
    $refresh_parents = $element['#views_ui_ajax_data']['refresh_parents'];
    $refresh_element = drupal_array_get_nested_value($complete_form, $refresh_parents);

    // The HTML ID that AJAX expects was also stored in a property on the
    // element, so use that information to insert the wrapper <div> here.
    $id = $element['#views_ui_ajax_data']['wrapper'];
    $refresh_element += array(
      '#prefix' => '',
      '#suffix' => '',
    );
    $refresh_element['#prefix'] = '<div id="' . $id . '" class="views-ui-ajax-wrapper">' . $refresh_element['#prefix'];
    $refresh_element['#suffix'] .= '</div>';
    // Copy the element that needs to be refreshed back into the form, with our
    // modifications to it.
    drupal_array_set_nested_value($complete_form, $refresh_parents, $refresh_element);
 * Updates a part of the add view form via AJAX.
 *
 * @return
 *   The part of the form that has changed.
 */
function views_ui_ajax_update_form($form, $form_state) {
  // The region that needs to be updated was stored in a property of the
  // triggering element by views_ui_add_ajax_trigger(), so all we have to do is
  // retrieve that here.
  return drupal_array_get_nested_value($form, $form_state['triggering_element']['#views_ui_ajax_data']['refresh_parents']);
 * Non-Javascript fallback for updating the add view form.
function views_ui_nojs_submit($form, &$form_state) {
  $form_state['rebuild'] = TRUE;
/**
 * Validate the add view form.
 */
function views_ui_wizard_form_validate($form, &$form_state) {
  $w = views_ui_get_wizard($form_state['values']['show']['wizard_key']);
  $form_state['wizard'] = $w;
  $get_instance = $w['get_instance'];
  $form_state['wizard_instance'] = $get_instance($w);
  $errors = $form_state['wizard_instance']->validate($form, $form_state);
  foreach ($errors as $name => $message) {
    form_set_error($name, $message);
 * Process the add view form, 'save'.
function views_ui_add_form_save_submit($form, &$form_state) {
  try {
    $view = $form_state['wizard_instance']->create_view($form, $form_state);
  catch (ViewsWizardException $e) {
    drupal_set_message($e->getMessage(), 'error');
    $form_state['redirect'] = 'admin/structure/views';
  }
  $view->save();
  menu_rebuild();
  cache_clear_all('*', 'cache_views', TRUE);
  cache_clear_all();
  $form_state['redirect'] = 'admin/structure/views';
  if (!empty($view->display['page'])) {
    $display = $view->display['page'];
    if ($display->handler->has_path()) {
      $one_path = $display->handler->get_option('path');
      if (strpos($one_path, '%') === FALSE) {
        $form_state['redirect'] = $one_path;  // PATH TO THE VIEW IF IT HAS ONE
Peter Wolanin's avatar
Peter Wolanin committed
        return;
Peter Wolanin's avatar
Peter Wolanin committed
  drupal_set_message(t('Your view was saved. You may edit it from the list below.'));
 * Process the add view form, 'continue'.
function views_ui_add_form_store_edit_submit($form, &$form_state) {
  try {
    $view = $form_state['wizard_instance']->create_view($form, $form_state);
  }
  catch (ViewsWizardException $e) {
    drupal_set_message($e->getMessage(), 'error');
    $form_state['redirect'] = 'admin/structure/views';
  }
  // Just cache it temporarily to edit it.
  views_ui_cache_set($view);
  $form_state['redirect'] = 'admin/structure/views/view/' . $view->name;
}

/**
 * Cancel the add view form.
 */
function views_ui_add_form_cancel_submit($form, &$form_state) {
  $form_state['redirect'] = 'admin/structure/views';
}

/**
 * Form element validation handler for a taxonomy autocomplete field.
 *
 * This allows a taxonomy autocomplete field to be validated outside the
 * standard Field API workflow, without passing in a complete field widget.
 * Instead, all that is required is that $element['#field_name'] contain the
 * name of the taxonomy autocomplete field that is being validated.
 *
 * This function is currently not used for validation directly, although it
 * could be. Instead, it is only used to store the term IDs and vocabulary name
 * in the element value, based on the tags that the user typed in.
 *
 * @see taxonomy_autocomplete_validate()
 */
function views_ui_taxonomy_autocomplete_validate($element, &$form_state) {
  $value = array();
  if ($tags = $element['#value']) {
    // Get the machine names of the vocabularies we will search, keyed by the
    // vocabulary IDs.
    $field = field_info_field($element['#field_name']);
    $vocabularies = array();
    if (!empty($field['settings']['allowed_values'])) {
      foreach ($field['settings']['allowed_values'] as $tree) {
        if ($vocabulary = taxonomy_vocabulary_machine_name_load($tree['vocabulary'])) {
          $vocabularies[$vocabulary->vid] = $tree['vocabulary'];
        }
      }
    }
    // Store the term ID of each (valid) tag that the user typed.
    $typed_terms = drupal_explode_tags($tags);
    foreach ($typed_terms as $typed_term) {
      if ($terms = taxonomy_term_load_multiple(array(), array('name' => trim($typed_term), 'vid' => array_keys($vocabularies)))) {
        $term = array_pop($terms);
        $value['tids'][] = $term->tid;
      }
    }
    // Store the term IDs along with the name of the vocabulary. Currently
    // Views (as well as the Field UI) assumes that there will only be one
    // vocabulary, although technically the API allows there to be more than
    // one.
    if (!empty($value['tids'])) {
      $value['tids'] = array_unique($value['tids']);
      $value['vocabulary'] = array_pop($vocabularies);
    }
  }
  form_set_value($element, $value, $form_state);
}

 * Theme function; returns basic administrative information about a view.
function theme_views_ui_view_info($variables) {
  $view = $variables['view'];
  $title = $view->get_human_name();
  $displays = _views_ui_get_displays_list($view);
  $displays = empty($displays) ? t('None') : format_plural(count($displays), 'Display', 'Displays') . ': ' . '<em>' . implode(', ', $displays) . '</em>';
  switch ($view->type) {
    case t('Default'):
    default:
      $type = t('In code');
      break;
    case t('Normal'):
      $type = t('In database');
      break;
    case t('Overridden'):
      $type = t('Database overriding code');
  }

  $output = '';
  $output .= '<div class="views-ui-view-title">' . $title . "</div>\n";
  $output .= '<div class="views-ui-view-displays">' . $displays . "</div>\n";
Earl Miles's avatar
Earl Miles committed
  $output .= '<div class="views-ui-view-storage">' . $type . "</div>\n";
  $output .= '<div class="views-ui-view-base">' . t('Type') . ': ' . $variables['base']. "</div>\n";
}

/**
 * Page to delete a view.
 */
function views_ui_break_lock_confirm($form, &$form_state, $view) {
  $form_state['view'] = &$view;
  $form = array();

  if (empty($view->locked)) {
    return t('There is no lock on view %view to break.', array('%name' => $view->name));
  }

  $cancel = 'admin/structure/views/view/' . $view->name . '/edit';
  if (!empty($_REQUEST['cancel'])) {
    $cancel = $_REQUEST['cancel'];
  }

  $account = user_load($view->locked->uid);
  return confirm_form($form,
                  t('Are you sure you want to break the lock on view %name?',
                  array('%name' => $view->name)),
                  $cancel,
                  t('By breaking this lock, any unsaved changes made by !user will be lost!', array('!user' => theme('username', array('account' => $account)))),
                  t('Break lock'),
                  t('Cancel'));
}

/**
 * Submit handler to break_lock a view.
 */
function views_ui_break_lock_confirm_submit(&$form, &$form_state) {
  ctools_object_cache_clear_all('view', $form_state['view']->name);
  $form_state['redirect'] = 'admin/structure/views/view/' . $form_state['view']->name . '/edit';
  drupal_set_message(t('The lock has been broken and you may now edit this view.'));
}

Alex Bronstein's avatar
Alex Bronstein committed
/**
 * Page title callback for the Edit View page.
 */
function views_ui_edit_page_title($view) {
  $bases = views_fetch_base_tables();
  $name = $view->get_human_name();
  if (isset($bases[$view->base_table])) {
    $name .= ' (' . $bases[$view->base_table]['title'] . ')';
  }

  return $name;
 * Form builder callback for editing a View.
 *
 * @todo Remove as many #prefix/#suffix lines as possible. Use #theme_wrappers
 *   instead.
Alex Bronstein's avatar
Alex Bronstein committed
 *
 * @todo Rename to views_ui_edit_view_form(). See that function for the "old"
 *   version.
function views_ui_edit_form($form, &$form_state, $view, $display_id = NULL) {
  // Do not allow the form to be cached, because $form_state['view'] can become
  // stale between page requests.
  // @see views_ui_ajax_get_form() for how this affects #ajax.
  // @todo To remove this and allow the form to be cacheable:
  //   - Change $form_state['view'] to $form_state['temporary']['view'].
  //   - Add a #process function to initialize $form_state['temporary']['view']
  //     on cached form submissions.
  //   - Update ctools_include() to support cached forms, or else use
  //     form_load_include().
  $form['#attached']['js'][] = ctools_attach_js('dependent');
  $form['#attached']['js'][] = ctools_attach_js('collapsible-div');
  $form['#tree'] = TRUE;
  // @todo When more functionality is added to this form, cloning here may be
  //   too soon. But some of what we do with $view later in this function
  //   results in making it unserializable due to PDO limitations.
  $form_state['view'] = clone($view);
  $form['#attached']['library'][] = array('system', 'ui.tabs');
  $form['#attached']['library'][] = array('system', 'ui.dialog');
  $form['#attached']['library'][] = array('system', 'drupal.ajax');
  $form['#attached']['library'][] = array('system', 'jquery.form');
  // TODO: This should be getting added to the page when an ajax popup calls
  // for it, instead of having to add it manually here.
  $form['#attached']['js'][] = 'misc/tabledrag.js';
  $form['#attached']['css'] = views_ui_get_admin_css();
  $module_path = drupal_get_path('module', 'views_ui');
  $form['#attached']['js'][] = $module_path . '/js/views-admin.js';
  $form['#attached']['js'][] = array(
    'data' => array('views' => array('ajax' => array(
      'id' => '#views-ajax-body',
      'title' => '#views-ajax-title',
      'popup' => '#views-ajax-popup',
      'defaultForm' => views_ui_get_default_ajax_message(),
    ))),
    'type' => 'setting',
  );

  $form += array(
    '#prefix' => '',
    '#suffix' => '',
  $form['#prefix'] = $form['#prefix'] . '<div class="views-edit-view views-admin clearfix">';
  $form['#suffix'] = '</div>' . $form['#suffix'];

  if (isset($view->locked) && is_object($view->locked)) {
    $form['locked'] = array(
      '#theme_wrappers' => array('container'),
      '#attributes' => array('class' => array('view-locked', 'messages', 'warning')),
      '#markup' => t('This view is being edited by user !user, and is therefore locked from editing by others. This lock is !age old. Click here to <a href="!break">break this lock</a>.', array('!user' => theme('username', array('account' => user_load($view->locked->uid))), '!age' => format_interval(REQUEST_TIME - $view->locked->updated), '!break' => url('admin/structure/views/view/' . $view->name . '/break-lock'))),
  if (isset($view->vid) && $view->vid == 'new') {
    $message = t('* All changes are stored temporarily. Click Save to make your changes permanent. Click Cancel to discard the view.');
    $message = t('* All changes are stored temporarily. Click Save to make your changes permanent. Click Cancel to discard your changes.');
  $form['changed'] = array(
    '#theme_wrappers' => array('container'),
    '#attributes' => array('class' => array('view-changed', 'messages', 'warning')),
    '#markup' => $message,
  );
  if (empty($view->changed)) {
    $form['changed']['#attributes']['class'][] = 'js-hide';
    '#prefix' => '<div>',
    '#suffix' => '</div>',
Daniel Wehner's avatar
Daniel Wehner committed
    '#markup' => t('Modify the display(s) of your view below or add new displays.'),
  $form['actions'] = array(
    '#type' => 'actions',
    '#weight' => 0,
  );

  if (empty($view->changed)) {
    $form['actions']['#attributes'] = array(
      'class' => array(
        'js-hide',
      ),
    );
  }

Alex Bronstein's avatar
Alex Bronstein committed
  $form['actions']['save'] = array(
    '#type' => 'submit',
    '#value' => t('Save'),
    // Taken from the "old" UI. @TODO: Review and rename.
Alex Bronstein's avatar
Alex Bronstein committed
    '#validate' => array('views_ui_edit_view_form_validate'),
    '#submit' => array('views_ui_edit_view_form_submit'),
  );
  $form['actions']['cancel'] = array(
    '#type' => 'submit',
    '#submit' => array('views_ui_edit_view_form_cancel'),
  );
  $form['displays'] = array(
    '#prefix' => '<h1 class="unit-title clearfix">' . t('Displays') . '</h1>' . "\n" . '<div class="views-displays">',
    '#suffix' => '</div>',
  );

  $form['displays']['top']['#theme_wrappers'] = array('views_container');
  $form['displays']['top']['#attributes']['class'] = array('views-display-top');

  // Extra actions for the display
  $form['displays']['top']['extra_actions'] = array(
    '#theme' => 'links__ctools_dropbutton',
    '#attributes' => array(
        'id' => 'views-display-extra-actions',
        'class' => array(
          'horizontal', 'right', 'links', 'actions',
        ),