You are here

rooms_availability_reference.module in Rooms - Drupal Booking for Hotels, B&Bs and Vacation Rentals 7

Defines a field type for referencing availability information

File

modules/rooms_availability_reference/rooms_availability_reference.module
View source
<?php

/**
 * @file
 * Defines a field type for referencing availability information
 */

/**
 * Implements hook_permission().
 */
function rooms_availability_reference_permission() {
  $permissions = array(
    'reference booking unit availability' => array(
      'title' => t('Reference unit availability'),
      'description' => t('Allows users to embed availability information in other nodes.'),
    ),
  );
  return $permissions;
}

/**
 * Implements hook_menu().
 */
function rooms_availability_reference_menu() {
  $items['rooms_availability_reference/autocomplete/%/%/%'] = array(
    'page callback' => 'rooms_availability_reference_autocomplete',
    'page arguments' => array(
      2,
      3,
      4,
    ),
    'access arguments' => array(
      'reference booking unit availability',
    ),
    'type' => MENU_CALLBACK,
  );
  return $items;
}

/**
 * Implements hook_field_info().
 */
function rooms_availability_reference_field_info() {
  return array(
    'rooms_availability_reference' => array(
      'label' => t('Rooms Availability Reference'),
      'description' => t('Display availability information embedded from other fieldable content.'),
      'settings' => array(
        'referenceable_unit_types' => array(),
      ),
      'default_widget' => 'rooms_availability_reference_autocomplete',
      'default_formatter' => 'rooms_availability_reference_default',
    ),
  );
}

/**
 * Implements hook_field_is_empty().
 */
function rooms_availability_reference_field_is_empty($item, $field) {
  return empty($item['unit_id']);
}

/**
 * Implements hook_field_formatter_info().
 */
function rooms_availability_reference_field_formatter_info() {
  $ret = array(
    'rooms_availability_reference_default' => array(
      'label' => t('Rooms Availability Calendar'),
      'description' => t('Displays availability information on a calendar.'),
      'field types' => array(
        'rooms_availability_reference',
        'entityreference',
      ),
    ),
  );
  return $ret;
}

/**
 * Implements hook_field_formatter_prepare_view().
 */
function rooms_availability_reference_field_formatter_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items, $displays) {
  if ($field['type'] == 'entityreference') {
    entityreference_field_formatter_prepare_view($entity_type, $entities, $field, $instances, $langcode, $items, $displays);
  }
}

/**
 * Implements hook_field_formatter_view().
 *
 * @todo parametrize date range
 */
function rooms_availability_reference_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
  $result = array();
  $element = array();
  $js_settings = array();
  switch ($display['type']) {
    case 'rooms_availability_reference_default':
      if ($field['type'] == 'entityreference' && $field['settings']['target_type'] != 'rooms_unit') {
        return $element;
      }
      rooms_fullcalendar_loaded();

      // Full day events on calendar.
      if (variable_get('rooms_calendar_events_view', '0') == '1') {
        drupal_add_js(drupal_get_path('module', 'rooms_availability_reference') . '/js/rooms_availability_reference_full_day.js');
      }
      else {
        drupal_add_js(drupal_get_path('module', 'rooms_availability_reference') . '/js/rooms_availability_reference.js');
      }
      drupal_add_css(drupal_get_path('module', 'rooms_availability') . '/css/fullcalendar.theme.css');
      drupal_add_css(drupal_get_path('module', 'rooms_availability_reference') . '/css/rooms_availability_reference_calendar.css');
      $unit_names = array();
      $unit_ids = array();
      $id = drupal_html_id($field['field_name'] . '-availability-formatter');
      foreach ($items as $delta => $item) {
        if ($field['type'] == 'entityreference') {
          if (empty($item['access'])) {
            continue;
          }
          $unit = $item['entity'];
        }
        else {
          $unit = rooms_unit_load($item['unit_id']);
        }
        if ($unit) {
          $unit_names[] = $unit->name;
          $unit_ids[] = $unit->unit_id;
        }
      }
      if (!empty($unit_ids)) {
        $result[] = array(
          '#prefix' => '<div id="' . $id . '" class="availability-title">',
          '#markup' => '<h2>' . implode(', ', $unit_names) . '</h2>',
          '#suffix' => '</div>',
        );

        // Inject settings in javascript that we will use.
        $js_settings[$id] = array(
          'unitID' => $unit_ids,
          'style' => ROOMS_AVAILABILITY_GENERIC_STYLE,
          'firstDay' => intval(variable_get('date_first_day', 0)),
        );
      }
      break;
  }
  if (!empty($result)) {
    $element[] = array(
      '#theme' => 'rooms_availability_field_calendar',
      'calendar' => $result,
      '#attached' => array(
        'js' => array(
          array(
            'data' => array(
              'roomsAvailabilityRef' => $js_settings,
            ),
            'type' => 'setting',
          ),
        ),
      ),
    );
  }
  return $element;
}

/**
 * Implements hook_field_settings_form().
 */
function rooms_availability_reference_field_settings_form($field, $instance, $has_data) {
  $settings = $field['settings'];
  $form = array();
  $form['referenceable_unit_types'] = array(
    '#type' => 'checkboxes',
    '#title' => t('Unit types that can be referenced'),
    '#multiple' => TRUE,
    '#default_value' => isset($settings['referenceable_unit_types']) ? $settings['referenceable_unit_types'] : array(),
    '#options' => array_map('check_plain', rooms_unit_types_ids()),
  );
  return $form;
}

/**
 * Implements hook_field_validate().
 */
function rooms_availability_reference_field_validate($entity_type, $entity, $field, $instance, $langcode, $items, &$errors) {

  // Extract unit ids to check.
  $ids = array();

  // Check for non-numeric values.
  // First check non-numeric "nid's to avoid losing time with them.
  foreach ($items as $delta => $item) {
    if (is_array($item) && !empty($item['unit_id'])) {
      if (is_numeric($item['unit_id'])) {
        $ids[] = $item['unit_id'];
      }
      else {
        $errors[$field['field_name']][$langcode][$delta][] = array(
          'error' => 'invalid_unit_id',
          'message' => t('%name: invalid input.', array(
            '%name' => $instance['label'],
          )),
        );
      }
    }
  }
}

/**
 * Retrieves an array of candidate referenceable booking units.
 *
 * This info is used in various places (allowed values, autocomplete
 * results, input validation...). Some of them only need the nids,
 * others nid + titles, others yet nid + titles + rendered row (for
 * display in widgets).
 *
 * The array we return contains all the potentially needed information,
 * and lets consumers use the parts they actually need.
 *
 * @param array $field
 *   The field definition.
 * @param array $options
 *   An array of options to limit the scope of the returned list. The following
 *   key/value pairs are accepted:
 *   - string: string to filter unit names on (used by autocomplete).
 *   - match: operator to match the above string against, can be any of:
 *     'contains', 'equals', 'starts_with'. Defaults to 'contains'.
 *   - ids: array of specific unit ids to lookup.
 *   - limit: maximum size of the the result set. Defaults to 0 (no limit).
 *
 * @return array
 *   An array of valid units in the form:
 *   array(
 *     unit_id => array(
 *       'name' => The unit title,
 *       'rendered' => The text to display in widgets (can be HTML)
 *     ),
 *     ...
 *   )
 */
function rooms_availability_reference_potential_references($field, $options = array()) {

  // Fill in default options.
  $options += array(
    'string' => '',
    'match' => 'contains',
    'ids' => array(),
    'limit' => 0,
  );
  $results =& drupal_static(__FUNCTION__, array());

  // Create unique id for static cache.
  $cid = $field['field_name'] . ':' . $options['match'] . ':' . ($options['string'] !== '' ? $options['string'] : implode('-', $options['ids'])) . ':' . $options['limit'];
  if (!isset($results[$cid])) {
    $references = FALSE;
    if ($references === FALSE) {
      $references = _rooms_availability_reference_potential_references($field, $options);
    }

    // Store the results.
    $results[$cid] = !empty($references) ? $references : array();
  }
  return $results[$cid];
}

/**
 * Helper function for node_reference_potential_references().
 *
 * List of referenceable nodes defined by content types.
 */
function _rooms_availability_reference_potential_references($field, $options) {

  // Avoid useless work.
  if (!isset($field['settings']['referenceable_unit_types'])) {
    return array();
  }
  if (!count($field['settings']['referenceable_unit_types'])) {
    return array();
  }
  $query = db_select('rooms_units', 'u');
  $unit_unit_id_alias = $query
    ->addField('u', 'unit_id');
  $unit_name_alias = $query
    ->addField('u', 'name', 'name');
  $unit_type_alias = $query
    ->addField('u', 'type', 'type');
  if (is_array($field['settings']['referenceable_unit_types'])) {
    $query
      ->condition('u.type', $field['settings']['referenceable_unit_types'], 'IN');
  }
  if ($options['string'] !== '') {
    switch ($options['match']) {
      case 'contains':
        $query
          ->condition('u.name', '%' . $options['string'] . '%', 'LIKE');
        break;
      case 'starts_with':
        $query
          ->condition('u.name', $options['string'] . '%', 'LIKE');
        break;
      case 'equals':
      default:

        // No match type or incorrect match type: use "=".
        $query
          ->condition('u.name', $options['string']);
        break;
    }
  }
  if ($options['ids']) {
    $query
      ->condition('u.unit_id', $options['ids'], 'IN');
  }
  if ($options['limit']) {
    $query
      ->range(0, $options['limit']);
  }
  $query
    ->orderBy($unit_name_alias)
    ->orderBy($unit_type_alias);
  $result = $query
    ->execute()
    ->fetchAll();
  $references = array();
  foreach ($result as $unit) {
    $references[$unit->unit_id] = array(
      'title' => $unit->name,
      'rendered' => check_plain($unit->name),
    );
  }
  return $references;
}

/**
 * Menu callback for the autocomplete results.
 */
function rooms_availability_reference_autocomplete($entity_type, $bundle, $field_name, $string = '') {
  $field = field_info_field($field_name);
  $instance = field_info_instance($entity_type, $field_name, $bundle);
  $options = array(
    'string' => $string,
    'match' => $instance['widget']['settings']['autocomplete_match'],
    'limit' => 10,
  );
  $references = rooms_availability_reference_potential_references($field, $options);
  $matches = array();
  foreach ($references as $id => $row) {

    // Markup is fine in autocompletion results (might happen when rendered
    // through Views) but we want to remove hyperlinks.
    $suggestion = preg_replace('/<a href="([^<]*)">([^<]*)<\\/a>/', '$2', $row['rendered']);

    // Add a class wrapper for a few required CSS overrides.
    $matches[$row['title'] . " [unit_id:{$id}]"] = '<div class="reference-autocomplete">' . $suggestion . '</div>';
  }
  drupal_json_output($matches);
}

/**
 * Implements hook_field_widget_info().
 */
function rooms_availability_reference_field_widget_info() {
  return array(
    'rooms_availability_reference_autocomplete' => array(
      'label' => t('Autocomplete text field'),
      'description' => t('Display the list of referenceable units as a textfield with autocomplete behaviour.'),
      'field types' => array(
        'rooms_availability_reference',
      ),
      'settings' => array(
        'autocomplete_match' => 'contains',
        'size' => 60,
        'autocomplete_path' => 'rooms_availability_reference/autocomplete',
      ),
    ),
  );
}

/**
 * Implements hook_field_widget_form().
 */
function rooms_availability_reference_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
  switch ($instance['widget']['type']) {
    case 'rooms_availability_reference_autocomplete':
      $element += array(
        '#type' => 'textfield',
        '#default_value' => isset($items[$delta]['unit_id']) ? $items[$delta]['unit_id'] : NULL,
        '#autocomplete_path' => $instance['widget']['settings']['autocomplete_path'] . '/' . $instance['entity_type'] . '/' . $instance['bundle'] . '/' . $field['field_name'],
        '#size' => $instance['widget']['settings']['size'],
        '#maxlength' => 255,
        '#element_validate' => array(
          'rooms_availability_reference_autocomplete_validate',
        ),
        '#value_callback' => 'rooms_availability_reference_autocomplete_value',
      );
      break;
  }
  return array(
    'unit_id' => $element,
  );
}

/**
 * Value callback for a rooms_availability_reference autocomplete element.
 *
 * Replace the unit id with a unit name.
 */
function rooms_availability_reference_autocomplete_value($element, $input = FALSE, $form_state) {
  if ($input === FALSE) {

    // We're building the displayed 'default value': expand the raw unit id into
    // "unit name [unit_id:n]".
    $unit_id = $element['#default_value'];
    if (!empty($unit_id)) {
      $unit = rooms_unit_load($unit_id);
      $value = $unit->name;
      $value .= ' [unit_id:' . $unit_id . ']';
      return $value;
    }
  }
}

/**
 * Validation callback for a node_reference autocomplete element.
 */
function rooms_availability_reference_autocomplete_validate($element, &$form_state, $form) {
  $field = field_widget_field($element, $form_state);
  $instance = field_widget_instance($element, $form_state);
  $value = $element['#value'];
  $unit_id = NULL;
  if (!empty($value)) {

    // Check whether we have an explicit "[unit_id:n]" input.
    preg_match('/^(?:\\s*|(.*) )?\\[\\s*unit_id\\s*:\\s*(\\d+)\\s*\\]$/', $value, $matches);
    if (!empty($matches)) {

      // Explicit nid. Check that the 'title' part matches the actual title for
      // the nid.
      list(, $title, $unit_id) = $matches;
      if (!empty($title)) {
        $unit = rooms_unit_load($unit_id);
        $real_title = $unit->name;
        if (trim($title) != trim($real_title)) {
          form_error($element, t('%name: title mismatch. Please check your selection.', array(
            '%name' => $instance['label'],
          )));
        }
      }
    }
    else {

      // No explicit unit_id (the submitted value was not populated by
      // autocomplete selection). Get the unit_id of a referenceable node from
      // the entered title.
      $options = array(
        'string' => $value,
        'match' => 'equals',
        'limit' => 1,
      );
      $references = rooms_availability_reference_potential_references($field, $options);
      if ($references) {

        // @todo The best thing would be to present the user with an
        // additional form, allowing the user to choose between valid
        // candidates with the same title. ATM, we pick the first
        // matching candidate...
        $unit_id = key($references);
      }
      else {
        form_error($element, t('%name: unable to find a unit with that title.', array(
          '%name' => $instance['label'],
        )));
      }
    }
  }

  // Set the element's value as the node id that was extracted from the entered
  // input.
  form_set_value($element, $unit_id, $form_state);
}

/**
 * Implements hook_field_widget_error().
 */
function rooms_availability_reference_field_widget_error($element, $error, $form, &$form_state) {
  form_error($element['unit_id'], $error['message']);
}

/**
 * Implements hook_theme().
 */
function rooms_availability_reference_theme() {
  return array(
    'rooms_availability_field_calendar' => array(
      'render element' => 'rooms_availability_field_calendar',
      'template' => 'rooms_availability_field_calendar',
    ),
  );
}

Functions

Namesort descending Description
rooms_availability_reference_autocomplete Menu callback for the autocomplete results.
rooms_availability_reference_autocomplete_validate Validation callback for a node_reference autocomplete element.
rooms_availability_reference_autocomplete_value Value callback for a rooms_availability_reference autocomplete element.
rooms_availability_reference_field_formatter_info Implements hook_field_formatter_info().
rooms_availability_reference_field_formatter_prepare_view Implements hook_field_formatter_prepare_view().
rooms_availability_reference_field_formatter_view Implements hook_field_formatter_view().
rooms_availability_reference_field_info Implements hook_field_info().
rooms_availability_reference_field_is_empty Implements hook_field_is_empty().
rooms_availability_reference_field_settings_form Implements hook_field_settings_form().
rooms_availability_reference_field_validate Implements hook_field_validate().
rooms_availability_reference_field_widget_error Implements hook_field_widget_error().
rooms_availability_reference_field_widget_form Implements hook_field_widget_form().
rooms_availability_reference_field_widget_info Implements hook_field_widget_info().
rooms_availability_reference_menu Implements hook_menu().
rooms_availability_reference_permission Implements hook_permission().
rooms_availability_reference_potential_references Retrieves an array of candidate referenceable booking units.
rooms_availability_reference_theme Implements hook_theme().
_rooms_availability_reference_potential_references Helper function for node_reference_potential_references().