You are here

editableviews.module in Editable Views 7

editableviews.module Contain module code. @todo:

  • some way to mark cells for a NEW entity? at least put a class on them!
  • ditto, put class on relationship cells.

File

editableviews.module
View source
<?php

/**
 * @file editableviews.module
 * Contain module code.
 * @todo:
 *  - some way to mark cells for a NEW entity? at least put a class on them!
 *  - ditto, put class on relationship cells.
 */

/**
 * Implements hook_views_api().
 */
function editableviews_views_api() {
  return array(
    'api' => '3.0-alpha1',
    'path' => drupal_get_path('module', 'editableviews'),
  );
}

/**
 * Implements hook_forms().
 *
 * Handle our dynamic form IDs.
 *
 * @see editableviews_plugin_style_row_edit_table::get_form()
 */
function editableviews_forms($form_id, $args) {

  // We only care about the first piece of the form ID here. Everything after
  // that is only for the benefit of hook_form_alter().
  $length = strlen('editableviews_entity_form');
  if (substr($form_id, 0, $length) == 'editableviews_entity_form') {
    $forms[$form_id] = array(
      'callback' => 'editableviews_entity_form',
    );
    return $forms;
  }
}

/**
 * Form builder for an editable view.
 *
 * @see editableviews_forms()
 *
 * @param $entities
 *  An array of entities to get the form for, keyed first by entity type and
 *  then by entity id. The ids may be fake in the case of new, unsaved entities.
 *  Furthermore, new entities should have a property
 *  'editableviews_exposed_fields'. This should be an array of field names
 *  corresponding to editable field handlers on the view. A value in at least
 *  one of these fields on the entity causes it to be saved by
 *  editableviews_entity_form_submit_save(). This allows the user
 *  to leave a blank part of the result empty. However, it does not account for
 *  the fact that there may be default values in the field widgets!
 *  TODO: consider this problem!
 * @param $results_coordinates
 *  An array containing coordinates lookups for getting entity type and entity
 *  from row id and relationship, and the same the other way round. Contains:
 *  - 'entities_to_results': A nested array keyed by entity type then entity
 *    id. The final values are flat arrays of the relationship id and result
 *    index.
 *  - 'results_to_entities': A nested array keyed by relationship id then
 *    result index. The final values are flat arrays of the entity type and
 *    entity id.
 *  Conceptually, the pair (relationship id, result index) can be visualized as
 *  the coordinates that specify which cells in the table the entity occupies:
 *  the index gives the row and the relationship id one or more columns.
 * @param $field_handlers
 *  An array of the editable Views field handlers to show the form elements for.
 *  This should be grouped by relationship ID, with the base table for the
 *  view's base.
 * @param $view
 *  The view object.
 */
function editableviews_entity_form($form, &$form_state, $entities, $results_coordinates, $field_handlers, $view) {

  // Include this here as it's fairly likely we have handlers that need it;
  // rather than let the handler class include it over and over again.
  ctools_include('fields');

  //dsm(func_get_args(), 'form builder params');

  //dsm($entities);

  //dsm($field_handlers);
  $form['#tree'] = TRUE;
  $form['#entity_ids'] = array();

  //$form['#field_names'] = $fields;
  $form['#field_handlers'] = $field_handlers;

  // Put these in the form in case custom form handlers need to look at them.
  $form['#results_coordinates'] = $results_coordinates;

  // Put the view name and display name on the form, for hook_form_alter().
  $form['#view_name'] = $view->name;
  $form['#view_display_name'] = $view->current_display;

  // Get the message option from the view style plugin.
  $form['#save_messages'] = $view->style_plugin->options['save_messages'];

  // Get the batch option from the view style plugin.
  $form['#batch'] = $view->style_plugin->options['batch'];

  // Get the batch_size option from the view style plugin.
  $form['#batch_size'] = $view->style_plugin->options['batch_size'];
  foreach (array_keys($entities) as $entity_type) {
    foreach ($entities[$entity_type] as $entity_id => $entity) {
      if (isset($entity->language)) {
        $langcode = field_valid_language($entity->language);
      }
      else {
        $langcode = field_valid_language(NULL);
      }

      // Note we have to explicitly use the array key for the entity id rather
      // than extract it here, because it might not actually be a real id for
      // the case of an entity being created on a relationship.
      list(, , $bundle) = entity_extract_ids($entity_type, $entity);

      // Stash the entity type and id.
      $form['#entity_ids'][$entity_type][] = $entity_id;

      // Get the relationship that this entity is on. We only want to get form
      // elements from the field handlers that are on this relationship.
      list($relationship, $index) = $results_coordinates['entities_to_results'][$entity_type][$entity_id];

      // Build up the per-entity subform.
      $form[$entity_type][$entity_id] = array(
        // This allows FieldAPI to have multiple field form elements attached.
        '#parents' => array(
          $entity_type,
          $entity_id,
        ),
        '#entity_type' => $entity_type,
        '#entity' => $entity,
        '#bundle' => $bundle,
        // Stash the relationship this entity is on, so this form's validate and
        // submit handlers can get the relevant field handler for it.
        '#views_relationship' => $relationship,
      );
      foreach ($field_handlers[$relationship] as $field_handler) {

        // Pass the form to each field handler for it to add its element.
        $field_handler
          ->edit_form($entity_type, $entity, $form[$entity_type][$entity_id], $form_state);
      }
    }
  }
  $form['actions'] = array(
    '#type' => 'actions',
  );
  $form['actions']['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Save'),
  );
  $form['#submit'] = array(
    // We split the submit process up into the building of form values and the
    // actual saving, to allow implementations of this to add extra processing
    // steps in between, using hook_form_alter().
    // For easier inserting of submit handlers, we key this array, which will
    // not bother FormAPI at all (see form_execute_handlers()).
    'build_values' => 'editableviews_entity_form_submit_build_values',
    'save' => 'editableviews_entity_form_submit_save',
  );

  //dsm($form, 'end of form builder');
  return $form;
}

/**
 * Form validate handler.
 *
 * Adapted from fape_field_edit_field_form_validate().
 */
function editableviews_entity_form_validate($form, &$form_state) {
  ctools_include('fields');
  $field_handlers = $form['#field_handlers'];

  // Act on each entity in turn.
  foreach ($form['#entity_ids'] as $entity_type => $entity_ids) {
    foreach ($entity_ids as $entity_id) {
      $bundle = $form[$entity_type][$entity_id]['#bundle'];
      $entity = $form[$entity_type][$entity_id]['#entity'];
      $relationship = $form[$entity_type][$entity_id]['#views_relationship'];

      // Start with a fresh errors array for each entity. FieldAPI field
      // validation will add to this array.
      $errors = array();

      // Invoke the field handlers on the relationship the entity is on.
      foreach ($field_handlers[$relationship] as $field_handler) {
        $field_handler
          ->edit_form_validate($entity_type, $entity, $form[$entity_type][$entity_id], $form_state, $errors);
      }

      // Invoke hook_field_attach_validate() to let other modules validate the
      // entity. This should be done once only for each entity.
      // Avoid module_invoke_all() to let $errors be taken by reference.
      foreach (module_implements('field_attach_validate') as $module) {
        $function = $module . '_field_attach_validate';
        $function($entity_type, $entity, $errors);
      }
      if ($errors) {

        // Pass FieldAPI validation errors back to widgets for accurate error
        // flagging.
        foreach ($field_handlers[$relationship] as $field_handler) {
          $real_field_name = $field_handler->definition['field_name'];
          if (isset($errors[$real_field_name])) {
            $field_errors = array(
              $real_field_name => $errors[$real_field_name],
            );

            // This is ugly, but only FieldAPI field handlers need this. The
            // alternative would be for the editableviews_handler_field_field_edit
            // handler's edit_form_validate() method to detect whether it's the
            // last FieldAPI field handler for the current entity, which would be
            // messier.
            if (method_exists($field_handler, 'edit_form_validate_errors')) {
              $field_handler
                ->edit_form_validate_errors($entity_type, $entity, $form[$entity_type][$entity_id], $form_state, $field_errors);
            }
          }
        }
      }
    }
  }
}

/**
 * Form submit handler the first: build entities from field values.
 *
 * Entities are flagged as needing to be saved by our other submit handler,
 * editableviews_entity_form_submit_save().
 *
 * Existing entities are flagged if they have editable fields on them.
 *
 * New entities are checked to see whether they need to be saved or not. This
 * allows the user to leave a new entity's form elements empty to indicate they
 * do not wish to create an entity in that space.
 * Note however that this means if FieldAPI fields have default values, and
 * these are left in the form, the entity will be created! There is no real
 * way that I can envisage to account for this, short of adding a form element
 * with a checkbox for the user to explicitly say a new entity should be saved.
 * TODO: consider implementing this.
 */
function editableviews_entity_form_submit_build_values($form, &$form_state) {
  ctools_include('fields');

  //dsm($form);

  //dsm($form_state, 'fs');
  $field_handlers = $form['#field_handlers'];

  // Act on each entity in turn.
  foreach ($form['#entity_ids'] as $entity_type => $entity_ids) {
    foreach ($entity_ids as $entity_id) {
      $bundle = $form[$entity_type][$entity_id]['#bundle'];
      $entity = $form[$entity_type][$entity_id]['#entity'];
      $relationship = $form[$entity_type][$entity_id]['#views_relationship'];

      // Invoke the field handlers on the relationship the entity is on, if any.
      foreach ($field_handlers[$relationship] as $field_handler) {
        $field_handler
          ->edit_form_submit($entity_type, $entity, $form[$entity_type][$entity_id], $form_state);
      }

      // Set our flag on the entity if it needs to be saved.
      if (is_numeric($entity_id)) {

        // For existing entities, we save if the entity had editable fields
        // in the view. This does mean that an entity could be saved needlessly,
        // as we don't compare old values with incoming form values.
        if (count($field_handlers[$relationship])) {
          $entity->editableviews_needs_save = TRUE;
        }
      }
      else {

        // For new entities, check they have had values set on them by the user
        // that justify saving them.
        $wrapper = entity_metadata_wrapper($entity_type, $entity);
        foreach ($entity->editableviews_exposed_fields as $expected_field_name) {

          // We have to use the wrapper, as FieldAPI fields will contain a
          // nested array with the langcode even if left empty.
          $value = $wrapper->{$expected_field_name}
            ->raw();
          if (!empty($value)) {

            // If there is a value, mark this entity as needing to be saved by
            // editableviews_entity_form_submit_save(). Obviously, custom
            // form submit handlers that come after us are free to remove or
            // force this.
            $entity->editableviews_needs_save = TRUE;

            // No point going any further.
            break;
          }
        }
      }

      // Put the entities in the form state for the subsequent submit handlers to
      // find. Key by entity type to make it easy to get hold of that.
      $form_state['entities'][$entity_type][$entity_id] = $entity;
    }
  }
}

/**
 * Form submit handler the second: save entities.
 *
 * Saves all entities in $form_state['entities'], with the exception of new
 * entities which have their 'editableviews_needs_save' set to FALSE.
 * If batch support is enabled, batch is triggered from here.
 */
function editableviews_entity_form_submit_save($form, &$form_state) {
  if (!$form['#batch']) {
    editableviews_entity_save($form_state['entities'], $form['#save_messages']);
  }
  else {
    $operations = array();
    $entities = $form_state['entities'];
    foreach (array_keys($entities) as $entity_type) {
      $entity_array = $entities[$entity_type];
      while (!empty($entity_array)) {
        $list = array_splice($entity_array, 0, $form['#batch_size']);
        $operations[] = array(
          'editableviews_entity_save',
          array(
            array(
              $entity_type => $list,
            ),
            $form['#save_messages'],
          ),
        );
      }
    }
    $batch = array(
      'operations' => $operations,
      'finished' => 'editableviews_save_batch_finished',
    );
    batch_set($batch);
  }
}

/**
 * Implements callback_batch_finished().
 */
function editableviews_save_batch_finished($success, $results, $operations) {
  if ($success) {
    drupal_set_message(t('Entities saved successfully'));
  }
  else {
    drupal_set_message(t('An error occurred while saving entities'), 'error');
  }
}

/**
 * Saves the entities in $entities array.
 *
 * @param array $entities
 *   Entities to save.
 * @param string $save_messages
 *   Configuration setting about messages to show to end users. It must be one
 *   of the values below:
 *     - 'none': Show no messages.
 *     - 'summary': Show a single message summarizing the changes.
 *     - 'individual': Show a message for each saved entity.
 */
function editableviews_entity_save($entities, $save_messages) {
  $entity_info = entity_get_info();
  $save_message_labels = array();
  foreach (array_keys($entities) as $entity_type) {
    foreach ($entities[$entity_type] as $entity_id => $entity) {

      // Check whether a save has been requested.
      if (empty($entity->editableviews_needs_save)) {
        continue;
      }

      // Save the entity using Entity API.
      entity_save($entity_type, $entity);

      // If the entity was a new one, and on a forward relationship, we need
      // to set the referring entity to point to it. We can do this now that the
      // new entity has been saved and has an id.
      if (isset($entity->editableviews_future_reference)) {
        list($new_entity_id, ) = entity_extract_ids($entity_type, $entity);
        $referring_entity = $entities[$entity->editableviews_future_reference['entity_type']][$entity->editableviews_future_reference['entity_id']];
        $wrapper = entity_metadata_wrapper($entity->editableviews_future_reference['entity_type'], $referring_entity);

        // Make the existing entity point to the new entity.
        $relationship_field_name = $entity->editableviews_future_reference['field_name'];
        $wrapper->{$relationship_field_name}
          ->set($new_entity_id);

        // This needs to be saved a second time, as there's no simple way we can
        // guarantee the order we come to these in: they are in the order we
        // encounter them going through the field handlers.
        entity_save($entity->editableviews_future_reference['entity_type'], $referring_entity);
      }
      if ($save_messages == 'individual') {

        // Show a confirmation message. This could get pretty long on a big View!
        drupal_set_message(t("Saved %entity-type entity %label.", array(
          '%entity-type' => $entity_info[$entity_type]['label'],
          '%label' => entity_label($entity_type, $entity),
        )));
      }
      elseif ($save_messages == 'summary') {

        // Use drupal_placeholder() to format this the same as %label above.
        $save_message_labels[] = drupal_placeholder(entity_label($entity_type, $entity));
      }
    }
  }
  if ($save_messages == 'summary' && count($save_message_labels)) {
    drupal_set_message(t("Saved entities !labels.", array(
      // These have been sanitized already.
      '!labels' => implode(', ', $save_message_labels),
    )));
  }
}

/**
 * Implements hook_module_implements_alter().
 */
function editableviews_module_implements_alter(&$implementations, $hook) {
  if ($hook == 'views_data_alter') {

    // Move our hook_views_data_alter() to the end of the list, so we can
    // doctor relationships provided by other modules.
    $group = $implementations['editableviews'];
    unset($implementations['editableviews']);
    $implementations['editableviews'] = $group;
  }
}

// ================================================= Sample form alteration.

/**
 * Implements hook_form_FORM_ID_alter(): editableviews_entity_form_test_vre_fields
 */

/*
function editableviews_form_editableviews_entity_form_test_vre_fields_alter(&$form, &$form_state, $form_id) {
  // Faffily insert our submit handler after the 'build_values' one.
  $new_submit = array();
  foreach ($form['#submit'] as $key => $submit) {
    $new_submit[$key] = $submit;
    if ($key == 'build_values') {
      $new_submit['ours'] = 'mymodule_editableviews_form_submit';
    }
  }
  $form['#submit'] = $new_submit;
}
*/

/**
 * Custom form submit handler.
 */

/*
function mymodule_editableviews_form_submit($form, &$form_state) {
  foreach (array_keys($form_state['entities']) as $entity_type) {
    foreach ($form_state['entities'][$entity_type] as $entity_id => $entity) {
      // Change something on the entity before it is saved.
      $entity->uid = 1;
      // Remove an entity from saving entirely.
      unset($form_state['entities'][$entity_type][115]);
    }
  }
}
*/

Functions

Namesort descending Description
editableviews_entity_form Form builder for an editable view.
editableviews_entity_form_submit_build_values Form submit handler the first: build entities from field values.
editableviews_entity_form_submit_save Form submit handler the second: save entities.
editableviews_entity_form_validate Form validate handler.
editableviews_entity_save Saves the entities in $entities array.
editableviews_forms Implements hook_forms().
editableviews_module_implements_alter Implements hook_module_implements_alter().
editableviews_save_batch_finished Implements callback_batch_finished().
editableviews_views_api Implements hook_views_api().