You are here

resource_conflict.module in Resource Conflict 7.3

Provides general functionality for the resource conflict module.

File

resource_conflict.module
View source
<?php

/**
 * @file
 * Provides general functionality for the resource conflict module.
 */

/**
 * Implements hook_node_validate().
 *
 * Fire a rule event if this node is enabled for RC.
 * If the Rule executed our action "resource_conflict_form_error"
 * Then a session var would have been set that indicates there was a conflict
 * and we should set a form error.
 */
function resource_conflict_node_validate($node, $form, &$form_state) {

  // Don't validate on delete.
  if (isset($form_state['values']['op']) && $form_state['values']['op'] == t('Delete')) {
    return;
  }

  // Conflict handling is not enabled for this content type.
  if (!variable_get('rc_type_' . $node->type, FALSE)) {
    return;
  }
  rules_invoke_event('resource_conflict_node_validation', $node, $form);
  if (isset($_SESSION['resource_conflict_message'])) {

    // Find the date field that handles conflicts.
    $message = implode('<br>', $_SESSION['resource_conflict_message']);
    form_set_error('', $message);
    unset($_SESSION['resource_conflict_message']);
  }
}

/**
 * Check a node for conflicts.
 *
 * @return array
 *   A list of nodes.
 */
function _resource_conflict_get_conflicting_nids($node) {

  // Check the content type to make sure conflict handling is enabled.
  if (!variable_get('rc_type_' . $node->type, FALSE)) {
    return array();
  }
  $time_spans = _resource_conflict_get_timespans($node);

  // Back out if we don't have date data to compare with.
  if (empty($time_spans)) {
    return array();
  }
  return _resource_conflict_query_for_conflicts($time_spans, $node->nid);
}

/**
 * Load an array of start/end times for the RC enabled date field on the node.
 */
function _resource_conflict_get_timespans($node) {
  $time_spans = array();
  $date_field = variable_get('rc_date_field_' . $node->type, FALSE);
  if (!$date_field) {
    return array();
  }
  $start_buffer = variable_get('rc_conflict_buffer_start_' . $node->type, '');
  $end_buffer = variable_get('rc_conflict_buffer_end_' . $node->type, '');
  $date_field_info = field_info_field($date_field);
  $date_format = date_type_format($date_field_info['type']);
  $date_items = field_get_items('node', $node, $date_field);
  if (!$date_items) {
    return array();
  }

  // Date repeat fields may have multiple values, so we iterate over each.
  foreach ($date_items as $single_repetition_date) {

    // Avoid the "Add another item" element that is added for date repeat
    // fields.
    if (!is_array($single_repetition_date)) {
      continue;
    }
    $start = $single_repetition_date['value'];
    $end = $single_repetition_date['value2'];

    // Skip unless the user filled in a start and end date.
    if (empty($start) || empty($end)) {
      continue;
    }
    if (!empty($start_buffer)) {
      $dateobj = DateObject::createFromFormat($date_format, $start);
      if ($dateobj
        ->modify($start_buffer)) {
        $start = $dateobj
          ->format($date_format);
      }
    }
    if (!empty($end_buffer)) {
      $dateobj = DateObject::createFromFormat($date_format, $end);
      if ($dateobj
        ->modify($end_buffer)) {
        $end = $dateobj
          ->format($date_format);
      }
    }

    // Unix stamps should be interpreted as integers, but field_get_items pulls
    // them out as strings. We need to cast back to int or our database
    // comparisons fail.
    if ($date_format == DATE_FORMAT_UNIX) {
      $start = (int) $start;
      $end = (int) $end;
    }
    $time_spans[] = array(
      'start' => $start,
      'end' => $end,
    );
  }
  return $time_spans;
}

/**
 * Rules Action: Poor mans way of passing a form error to form validation.
 *
 * Used for a resource conflict node form. See resource_conflict_node_validate()
 * for how this session var is used to set a form error.
 */
function resource_conflict_form_error($message) {
  $_SESSION['resource_conflict_message'][] = $message;
}

/**
 * Rules Condition: check a node object for conflicts.
 */
function resource_conflict_contains_conflict($node) {
  $conflicting_node_ids = _resource_conflict_get_conflicting_nids($node);
  if (empty($conflicting_node_ids)) {
    return FALSE;
  }

  // Fire Rules event for each conflict detected.
  foreach ($conflicting_node_ids as $conflicting_node_id) {
    $conflicting_node = node_load($conflicting_node_id);
    rules_invoke_event('resource_conflict_conflict_detected', $node, $conflicting_node);
  }
  return TRUE;
}

/**
 * Rules Action: load list of conflicting nodes, if any.
 */
function resource_conflict_load_conflict_list($node) {
  $conflicting_node_ids = _resource_conflict_get_conflicting_nids($node);
  if (empty($conflicting_node_ids)) {
    return array(
      'conflict_list' => array(),
    );
  }
  $conflicting_nodes = array();
  foreach ($conflicting_node_ids as $conflicting_node_id) {
    $conflicting_nodes[] = node_load($conflicting_node_id);
  }
  return array(
    'conflict_list' => $conflicting_nodes,
  );
}

/**
 * Determine if any conflict enabled nodes overlap the specified times.
 *
 * 1. $start is within the event time
 * 2. $end is within the event time
 * 3. The event encompasses $start and $end
 * 4. Allow the end of one event to occur at the start of the next.
 *
 * @param array $time_spans
 *   An array of 'time span' arrays with the following keys:
 *   - start: Start of an event
 *   - end: End of an event
 *   The date format of each pair is that of the unsaved node's date format
 *   All date fields that are compared must be of the same format.
 * @param int $nid
 *   The node ID of the resource that we're conflict checking for.
 *
 * @return array
 *   An array of node IDs.
 */
function _resource_conflict_query_for_conflicts(array $time_spans, $nid) {
  $nids = array();
  $conflict_types = variable_get('rc_types', array());
  foreach ($conflict_types as $type) {
    $date_field = variable_get('rc_date_field_' . $type, FALSE);
    $date_table = '{field_data_' . $date_field . '}';
    $start_field = 'date_table.' . $date_field . '_value';
    $end_field = 'date_table.' . $date_field . '_value2';
    foreach ($time_spans as $time_span) {
      $query = db_select('node', 'n');
      $query
        ->join($date_table, 'date_table', 'n.vid = date_table.revision_id');
      $query
        ->fields('n', array(
        'nid',
      ))
        ->condition(db_or()
        ->condition(db_and()
        ->condition($start_field, $time_span['start'], '<=')
        ->condition($end_field, $time_span['start'], '>'))
        ->condition(db_and()
        ->condition($start_field, $time_span['end'], '<')
        ->condition($end_field, $time_span['end'], '>='))
        ->condition(db_and()
        ->condition($start_field, $time_span['start'], '>=')
        ->condition($end_field, $time_span['end'], '<=')))
        ->addTag('resource_conflict')
        ->distinct();

      // $nid may be null if the unsaved node was not yet created.
      if ($nid) {
        $query
          ->condition('n.nid', $nid, '<>');
      }
      $result = $query
        ->execute();
      $nids = array_merge($nids, $result
        ->fetchCol());
    }
  }
  return array_unique($nids);
}

/**
 * Implements hook_form_alter().
 */
function resource_conflict_form_alter(&$form, $form_state, $form_id) {
  if ($form_id != 'node_type_form') {
    return;
  }
  $type = isset($form['old_type']) && isset($form['old_type']['#value']) ? $form['old_type']['#value'] : NULL;
  $form['resource_conflict_set'] = array(
    '#type' => 'fieldset',
    '#title' => t('Resource Conflict'),
    '#collapsible' => TRUE,
    '#group' => 'additional_settings',
  );
  $disabled_msg = t('To set up this content type for conflict checking, first add a Date field with a required end date (repeate dates are supported). When the conditions have been met, this section will be enabled for configuration.');

  // The user is adding a new content type.
  if ($type == NULL) {
    $form['resource_conflict_set']['rc_info'] = array(
      '#prefix' => '<p>',
      '#suffix' => '</p>',
      '#markup' => $disabled_msg,
    );
    return;
  }
  $date_fields = array();
  $fields = field_info_instances('node', $type);
  foreach ($fields as $machine_name => $field_instance) {
    if (_resource_conflict_check_field_compatibility($field_instance)) {
      $date_fields[$machine_name] = $field_instance['label'];
    }
  }
  if (empty($date_fields)) {
    $form['resource_conflict_set']['requirements'] = array(
      '#prefix' => '<p>',
      '#suffix' => '</p>',
      '#weight' => -10,
      '#markup' => $disabled_msg,
    );
  }
  else {
    $form['resource_conflict_set']['rc_type'] = array(
      '#type' => 'checkbox',
      '#title' => t('Enable resource conflict checking for this content type'),
      '#default_value' => variable_get('rc_type_' . $type, 0),
      '#weight' => -8,
    );
    $form['resource_conflict_set']['rc_date_field'] = array(
      '#type' => 'select',
      '#title' => t('Field to use as the date for conflict checks'),
      '#options' => $date_fields,
      '#multiple' => FALSE,
      '#default_value' => variable_get('rc_date_field_' . $type, FALSE),
      '#description' => t("Select the date field to use to check for resource conflicts."),
    );
    $form['resource_conflict_set']['rc_conflict_buffer_start'] = array(
      '#type' => 'textfield',
      '#title' => t('Conflict Buffer - Start'),
      '#description' => t('Added to the start date of bookings to extend the conflict zone. <a href="@strtotime">strtotime</a> format. Example: "-5 minutes".', array(
        '@strtotime' => 'http://us1.php.net/strtotime',
      )),
      '#default_value' => variable_get('rc_conflict_buffer_start_' . $type, ''),
    );
    $form['resource_conflict_set']['rc_conflict_buffer_end'] = array(
      '#type' => 'textfield',
      '#title' => t('Conflict Buffer - End'),
      '#description' => t('Added to the end date of bookings to extend the conflict zone. <a href="@strtotime">strtotime</a> format. Example: "+5 minutes".', array(
        '@strtotime' => 'http://us1.php.net/strtotime',
      )),
      '#default_value' => variable_get('rc_conflict_buffer_end_' . $type, ''),
    );
  }

  // Add our custom submission handler so we can manage the RC settings.
  $form['#validate'][] = 'resource_conflict_form_validate';
  $form['#submit'][] = 'resource_conflict_form_submit';
}

/**
 * Our custom validate handler for when a content type is saved.
 */
function resource_conflict_form_validate($form, &$form_state) {
  if (!empty($form_state['values']['rc_conflict_buffer_start'])) {
    if (strtotime($form_state['values']['rc_conflict_buffer_start']) === FALSE) {
      form_set_error('rc_conflict_buffer_start', t('Start buffer not valid. Check PHP documentation for strtotime().'));
    }
  }
  if (!empty($form_state['values']['rc_conflict_buffer_end'])) {
    if (strtotime($form_state['values']['rc_conflict_buffer_end']) === FALSE) {
      form_set_error('rc_conflict_buffer_end', t('End buffer not valid. Check PHP documentation for strtotime().'));
    }
  }
}

/**
 * Our custom submit handler for when a content type is saved.
 */
function resource_conflict_form_submit($form, &$form_state) {
  $type = $form_state['values']['type'];
  if (isset($form_state['values']['rc_type']) && $form_state['values']['rc_type'] == 1) {
    _resource_conflict_add_type($type);
  }
  else {
    _resource_conflict_remove_type($type);
  }
}

/**
 * Implements hook_field_delete_instance().
 *
 * If this field was assigned as an RC date field, we want to delete it's data
 * from our registry and possibly warn the user that RC was disabled for this
 * content type.
 */
function resource_conflict_field_delete_instance($instance) {
  $rc_date_field = variable_get('rc_date_field_' . $instance['bundle'], FALSE);
  if ($rc_date_field == $instance['field_name']) {
    variable_del('rc_date_field_' . $instance['bundle']);

    // Msg user if the content type was enabled for RC.
    $content_type_enabled = variable_get('rc_type_' . $instance['bundle'], FALSE);
    if ($content_type_enabled == 1) {
      $msg = t('Resource Conflict has been disabled for the %type content type as the date field has been deleted.', array(
        '%type' => $instance['bundle'],
      ));
      drupal_set_message($msg, 'warning');
      watchdog('resource conflict', $msg, WATCHDOG_WARNING);
    }

    // Delete the reg values for this content type whether it was enabled or
    // not.
    _resource_conflict_remove_type($instance['bundle']);
  }
}

/**
 * Implements hook_field_update_instance().
 *
 * Notice when an RC-enabled field is modified, and make sure it still meets
 * requirements.
 */
function resource_conflict_field_update_instance($instance, $prior_instance) {
  if (variable_get('rc_type_' . $instance['bundle'], FALSE) && variable_get('rc_date_field_' . $instance['bundle'], FALSE) == $instance['field_name']) {
    if (!_resource_conflict_check_field_compatibility($instance)) {
      variable_del('rc_date_field_' . $instance['bundle']);
      _resource_conflict_remove_type($instance['bundle']);
      $msg = t('Resource Conflict has been disabled for the %type content type as the date field no longer meets requirements', array(
        '%type' => $instance['bundle'],
      ));
      drupal_set_message($msg, 'warning');
      watchdog('resource conflict', $msg, WATCHDOG_WARNING);
    }
  }
}

/**
 * Adds the provided content type to our list of enabled RC types.
 */
function _resource_conflict_add_type($content_type) {
  $conflict_types = variable_get("rc_types", array());
  if (!in_array($content_type, $conflict_types)) {
    $conflict_types[] = $content_type;
    variable_set("rc_types", $conflict_types);
  }
}

/**
 * Removes the provided content type from our list of enabled RC types.
 */
function _resource_conflict_remove_type($content_type) {
  $conflict_types = variable_get("rc_types", array());
  if (($key = array_search($content_type, $conflict_types)) !== FALSE) {
    unset($conflict_types[$key]);
    variable_set("rc_types", $conflict_types);
  }
  variable_del('rc_type_' . $content_type);
}

/**
 * Checks if the provided field instance is compatible with our module.
 */
function _resource_conflict_check_field_compatibility($field_instance) {
  $field_info = field_info_field($field_instance['field_name']);
  if ($field_info['module'] == 'date' && $field_info['settings']['todate'] == 'required') {
    return TRUE;
  }
  return FALSE;
}

Functions

Namesort descending Description
resource_conflict_contains_conflict Rules Condition: check a node object for conflicts.
resource_conflict_field_delete_instance Implements hook_field_delete_instance().
resource_conflict_field_update_instance Implements hook_field_update_instance().
resource_conflict_form_alter Implements hook_form_alter().
resource_conflict_form_error Rules Action: Poor mans way of passing a form error to form validation.
resource_conflict_form_submit Our custom submit handler for when a content type is saved.
resource_conflict_form_validate Our custom validate handler for when a content type is saved.
resource_conflict_load_conflict_list Rules Action: load list of conflicting nodes, if any.
resource_conflict_node_validate Implements hook_node_validate().
_resource_conflict_add_type Adds the provided content type to our list of enabled RC types.
_resource_conflict_check_field_compatibility Checks if the provided field instance is compatible with our module.
_resource_conflict_get_conflicting_nids Check a node for conflicts.
_resource_conflict_get_timespans Load an array of start/end times for the RC enabled date field on the node.
_resource_conflict_query_for_conflicts Determine if any conflict enabled nodes overlap the specified times.
_resource_conflict_remove_type Removes the provided content type from our list of enabled RC types.