You are here

bat_options.module in Booking and Availability Management Tools for Drupal 7

Same filename and directory in other branches
  1. 8 modules/bat_options/bat_options.module

File

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

/**
 * @file
 */
define('BAT_OPTIONS_ADD', 'add');
define('BAT_OPTIONS_ADD_DAILY', 'add-daily');
define('BAT_OPTIONS_SUB', 'sub');
define('BAT_OPTIONS_SUB_DAILY', 'sub-daily');
define('BAT_OPTIONS_ADD_PERSON', 'add_person');
define('BAT_OPTIONS_ADD_DAILY_PERSON', 'add-daily_person');
define('BAT_OPTIONS_SUB_PERSON', 'sub_person');
define('BAT_OPTIONS_SUB_DAILY_PERSON', 'sub-daily_person');
define('BAT_OPTIONS_REPLACE', 'replace');
define('BAT_OPTIONS_INCREASE', 'increase');
define('BAT_OPTIONS_DECREASE', 'decrease');
define('BAT_OPTIONS_NOCHARGE', 'no_charge');
define('BAT_OPTIONS_OPTIONAL', 'optional');
define('BAT_OPTIONS_MANDATORY', 'mandatory');
define('BAT_OPTIONS_ONREQUEST', 'on_request');

/**
 * Implements hook_menu().
 */
function bat_options_menu() {
  $items['bat_options/ajax'] = array(
    'title' => 'Remove item callback',
    'page callback' => 'bat_options_remove_js',
    'delivery callback' => 'ajax_deliver',
    'access callback' => TRUE,
    'theme callback' => 'ajax_base_page_theme',
    'type' => MENU_CALLBACK,
    'file path' => 'includes',
    'file' => 'form.inc',
  );
  return $items;
}

/**
 * Implements hook_field_info().
 */
function bat_options_field_info() {
  return array(
    'bat_options' => array(
      'label' => t('BAT Type Options'),
      'description' => t('BAT Type Options.'),
      'settings' => array(),
      'default_widget' => 'bat_options_combined',
      'default_formatter' => 'bat_options_default',
      'property_type' => 'bat_options',
      'property_callbacks' => array(
        'bat_options_property_info_callback',
      ),
    ),
  );
}

/**
 * Property callback for the Entity Metadata framework.
 */
function bat_options_property_info_callback(&$info, $entity_type, $field, $instance, $field_type) {

  // Apply the default.
  entity_metadata_field_default_property_callback($info, $entity_type, $field, $instance, $field_type);

  // Finally add in instance specific property info.
  $name = $field['field_name'];
  $property =& $info[$entity_type]['bundles'][$instance['bundle']]['properties'][$name];
  $property['type'] = $field['cardinality'] != 1 ? 'list<bat_options>' : 'bat_options';
  $property['property info'] = bat_options_data_property_info('Type options');
  $property['getter callback'] = 'entity_metadata_field_verbatim_get';
  $property['setter callback'] = 'entity_metadata_field_verbatim_set';
}

/**
 * Defines info for the properties of the bat_options data structure.
 */
function bat_options_data_property_info($name = NULL) {

  // Build an array of basic property information for bat_options.
  $properties = array(
    'name' => array(
      'label' => 'Name',
      'type' => 'text',
      'getter callback' => 'entity_property_verbatim_get',
      'setter callback' => 'entity_property_verbatim_set',
    ),
    'quantity' => array(
      'label' => 'Quantity',
      'type' => 'integer',
      'getter callback' => 'entity_property_verbatim_get',
      'setter callback' => 'entity_property_verbatim_set',
    ),
    'operation' => array(
      'label' => 'Operation',
      'type' => 'text',
      'getter callback' => 'entity_property_verbatim_get',
      'setter callback' => 'entity_property_verbatim_set',
    ),
    'value' => array(
      'label' => 'Value',
      'type' => 'integer',
      'getter callback' => 'entity_property_verbatim_get',
      'setter callback' => 'entity_property_verbatim_set',
    ),
    'type' => array(
      'label' => 'Type',
      'type' => 'text',
      'getter callback' => 'entity_property_verbatim_get',
      'setter callback' => 'entity_property_verbatim_set',
    ),
  );

  // Add the default values for each of the bat_options properties.
  foreach ($properties as &$value) {
    $value += array(
      'description' => !empty($name) ? t('!label of field %name', array(
        '!label' => $value['label'],
        '%name' => $name,
      )) : '',
    );
  }
  return $properties;
}

/**
 * Implements hook_field_is_empty().
 */
function bat_options_field_is_empty($item, $field) {
  $per_person = isset($field['settings']['per_person']) ? $field['settings']['per_person'] : FALSE;
  return empty($item['name']) || empty($item['quantity']) || !(is_numeric($item['quantity']) && is_int((int) $item['quantity'])) || (empty($item['value']) || !is_numeric($item['value'])) && $item['operation'] != 'no_charge' || empty($item['operation']) || !in_array($item['operation'], array_keys(bat_options_price_options($per_person)));
}

/**
 * Implements hook_field_widget_info().
 */
function bat_options_field_widget_info() {
  return array(
    'bat_options_combined' => array(
      'label' => t('Combined text field'),
      'field types' => array(
        'bat_options',
      ),
      'settings' => array(),
    ),
  );
}

/**
 * Implements hook_field_formatter_info().
 */
function bat_options_field_formatter_info() {
  return array(
    'bat_options_default' => array(
      'label' => t('BAT Options Default'),
      'field types' => array(
        'bat_options',
      ),
    ),
    'bat_options_price' => array(
      'label' => t('BAT Options Price'),
      'field types' => array(
        'bat_options',
      ),
    ),
    'bat_options_admin' => array(
      'label' => t('BAT Options Administrator'),
      'field types' => array(
        'bat_options',
      ),
    ),
  );
}

/**
 * Implements hook_field_formatter_view().
 */
function bat_options_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
  $element = array();
  switch ($display['type']) {
    case 'bat_options_default':
      foreach ($items as $delta => $item) {
        $element[$delta] = array(
          '#markup' => "{$item['quantity']} x {$item['name']}",
        );
      }
      break;
    case 'bat_options_price':
      $currency_setting = commerce_currency_load(commerce_default_currency());
      $currency_symbol = $currency_setting['symbol'];
      foreach ($items as $delta => $item) {
        $price = t('@currency_symbol@amount', array(
          '@currency_symbol' => $currency_symbol,
          '@amount' => number_format($item['value'], 2, '.', ''),
        ));
        if ($item['value'] > 0) {
          $element[$delta] = array(
            '#markup' => "{$item['quantity']} x {$item['name']} - {$price}",
          );
        }
        else {
          $element[$delta] = array(
            '#markup' => "{$item['quantity']} x {$item['name']}",
          );
        }
      }
      break;
    case 'bat_options_admin':
      foreach ($items as $delta => $item) {
        $element[$delta] = array(
          '#markup' => "{$item['quantity']} x {$item['name']} - {$item['operation']} {$item['value']}",
        );
      }
      break;
  }
  return $element;
}

/**
 * Implements hook_field_settings_form().
 */
function bat_options_field_settings_form($field, $instance, $has_data) {
  $settings = $field['settings'];
  $form = array();
  $form['per_person'] = array(
    '#type' => 'checkbox',
    '#title' => t('Enable "Per person" options'),
    '#default_value' => isset($settings['per_person']) ? $settings['per_person'] : 0,
  );
  return $form;
}

/**
 * Implements hook_field_widget_form().
 */
function bat_options_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
  if ($instance['widget']['type'] == 'bat_options_combined') {
    $field_parents = $element['#field_parents'];
    $field_name = $element['#field_name'];
    $language = $element['#language'];
    $parents = array_merge($field_parents, array(
      $field_name,
      $language,
      $delta,
    ));
    $element['name'] = array(
      '#type' => 'textfield',
      '#title' => t('Name'),
      '#default_value' => isset($items[$delta]['name']) ? $items[$delta]['name'] : NULL,
      '#attributes' => array(
        'class' => array(
          'bat_options-option--name',
        ),
      ),
    );
    $element['quantity'] = array(
      '#type' => 'select',
      '#title' => t('Quantity'),
      '#options' => drupal_map_assoc(range(1, 10, 1)),
      '#default_value' => isset($items[$delta]['quantity']) ? $items[$delta]['quantity'] : NULL,
      '#description' => t('How many of this add-on should be available'),
      '#attributes' => array(
        'class' => array(
          'bat_options-option--quantity',
        ),
      ),
    );
    $per_person = isset($field['settings']['per_person']) ? $field['settings']['per_person'] : FALSE;
    $price_options = bat_options_price_options($per_person);
    $element['operation'] = array(
      '#type' => 'select',
      '#title' => t('Operation'),
      '#options' => $price_options,
      '#default_value' => isset($items[$delta]['operation']) ? $items[$delta]['operation'] : NULL,
      '#attributes' => array(
        'class' => array(
          'bat_options-option--operation',
        ),
      ),
    );
    $element['value'] = array(
      '#type' => 'textfield',
      '#title' => t('Value'),
      '#size' => 10,
      '#default_value' => isset($items[$delta]['value']) && $items[$delta]['value'] != 0 ? $items[$delta]['value'] : NULL,
      '#element_validate' => array(
        'bat_options_element_value_validate',
      ),
      '#attributes' => array(
        'class' => array(
          'bat_options-option--value',
        ),
      ),
      '#states' => array(
        'disabled' => array(
          ':input[name="field_addons[en][' . $delta . '][operation]"]' => array(
            'value' => 'no_charge',
          ),
        ),
      ),
    );
    $type_options = array(
      BAT_OPTIONS_OPTIONAL => t('Optional'),
      BAT_OPTIONS_MANDATORY => t('Mandatory'),
      BAT_OPTIONS_ONREQUEST => t('On Request'),
    );
    $element['type'] = array(
      '#type' => 'select',
      '#title' => t('Type'),
      '#options' => $type_options,
      '#default_value' => isset($items[$delta]['type']) ? $items[$delta]['type'] : 'optional',
      '#attributes' => array(
        'class' => array(
          'bat_options-option--type',
        ),
      ),
    );
    $element['remove'] = array(
      '#delta' => $delta,
      '#name' => implode('_', $parents) . '_remove_button',
      '#type' => 'submit',
      '#value' => t('Remove'),
      '#validate' => array(),
      '#submit' => array(
        'bat_options_remove_submit',
      ),
      '#limit_validation_errors' => array(),
      '#ajax' => array(
        'path' => 'bat_options/ajax',
        'effect' => 'fade',
      ),
      '#attributes' => array(
        'class' => array(
          'bat_options-option--remove-button',
        ),
      ),
    );
    $element['#attached']['css'] = array(
      drupal_get_path('module', 'bat_options') . '/css/bat_options_widget.css',
    );
    return $element;
  }
}

/**
 * Form element validation handler for numeric elements that must be positive.
 */
function bat_options_element_value_validate($element, &$form_state) {
  $value = $element['#value'];
  if ($value == '') {
    form_set_value($element, 0, $form_state);
  }
  elseif (!is_numeric($value) || $value <= 0) {
    form_error($element, t('%name must be a positive number.', array(
      '%name' => $element['#title'],
    )));
  }
}

/**
 * Page callback to handle AJAX for removing a bat options item.
 *
 * This is a direct page callback. The actual job of deleting the item is
 * done in the submit handler for the button, so all we really need to
 * do is process the form and then generate output. We generate this
 * output by doing a replace command on the id of the entire form element.
 */
function bat_options_remove_js() {

  // drupal_html_id() very helpfully ensures that all html IDS are unique
  // on a page. Unfortunately what it doesn't realize is that the IDs
  // we are generating are going to replace IDs that already exist, so
  // this actually works against us.
  if (isset($_POST['ajax_html_ids'])) {
    unset($_POST['ajax_html_ids']);
  }
  list($form, $form_state, $form_id, $form_build_id, $commands) = ajax_get_form();
  drupal_process_form($form['#form_id'], $form, $form_state);

  // Get the information on what we're removing.
  $button = $form_state['triggering_element'];

  // Go two levels up in the form, to the whole widget.
  $element = drupal_array_get_nested_value($form, array_slice($button['#array_parents'], 0, -3));

  // Now send back the proper AJAX command to replace it.
  $commands[] = ajax_command_replace('#' . $element['#id'], drupal_render($element));
  $return = array(
    '#type' => 'ajax',
    '#commands' => $commands,
  );

  // Because we're doing this ourselves, messages aren't automatic. We have
  // to add them.
  $messages = theme('status_messages');
  if ($messages) {
    $return['#commands'][] = ajax_command_prepend('#' . $element['#id'], $messages);
  }
  return $return;
}

/**
 * Submit callback to remove an item from the field UI multiple wrapper.
 *
 * When a remove button is submitted, we need to find the item that it
 * referenced and delete it. Since field UI has the deltas as a straight
 * unbroken array key, we have to renumber everything down. Since we do this
 * we *also* need to move all the deltas around in the $form_state['values'],
 * $form_state['input'], and $form_state['field'] so that user changed values
 * follow. This is a bit of a complicated process.
 */
function bat_options_remove_submit($form, &$form_state) {
  $button = $form_state['triggering_element'];
  $delta = $button['#delta'];

  // Where in the form we'll find the parent element.
  $address = array_slice($button['#array_parents'], 0, -2);

  // Go one level up in the form, to the widgets container.
  $parent_element = drupal_array_get_nested_value($form, $address);
  $field_name = $parent_element['#field_name'];
  $langcode = $parent_element['#language'];
  $parents = $parent_element['#field_parents'];
  $field_state = field_form_get_state($parents, $field_name, $langcode, $form_state);

  // Go ahead and renumber everything from our delta to the last
  // item down one. This will overwrite the item being removed.
  for ($i = $delta; $i <= $field_state['items_count']; $i++) {
    $old_element_address = array_merge($address, array(
      $i + 1,
    ));
    $new_element_address = array_merge($address, array(
      $i,
    ));
    $moving_element = drupal_array_get_nested_value($form, $old_element_address);
    $moving_element_value = drupal_array_get_nested_value($form_state['values'], $old_element_address);
    $moving_element_input = drupal_array_get_nested_value($form_state['input'], $old_element_address);
    $moving_element_field = drupal_array_get_nested_value($form_state['field'], $old_element_address);

    // Tell the element where it's being moved to.
    $moving_element['#parents'] = $new_element_address;

    // Move the element around.
    form_set_value($moving_element, $moving_element_value, $form_state);
    drupal_array_set_nested_value($form_state['input'], $moving_element['#parents'], $moving_element_input);
    drupal_array_set_nested_value($form_state['field'], $moving_element['#parents'], $moving_element_field);
  }

  // Then remove the last item. But we must not go negative.
  if ($field_state['items_count'] > 0) {
    $field_state['items_count']--;
  }

  // Fix the weights. Field UI lets the weights be in a range of
  // (-1 * item_count) to (item_count). This means that when we remove one,
  // the range shrinks; weights outside of that range then get set to
  // the first item in the select by the browser, floating them to the top.
  // We use a brute force method because we lost weights on both ends
  // and if the user has moved things around, we have to cascade because
  // if I have items weight weights 3 and 4, and I change 4 to 3 but leave
  // the 3, the order of the two 3s now is undefined and may not match what
  // the user had selected.
  $input = drupal_array_get_nested_value($form_state['input'], $address);

  // Sort by weight.
  uasort($input, '_field_sort_items_helper');

  // Reweight everything in the correct order.
  $weight = -1 * $field_state['items_count'];
  foreach ($input as $key => $item) {
    if ($item) {
      $input[$key]['_weight'] = $weight++;
    }
  }
  drupal_array_set_nested_value($form_state['input'], $address, $input);
  field_form_set_state($parents, $field_name, $langcode, $form_state, $field_state);
  $form_state['rebuild'] = TRUE;
}

/**
 * Returns the available price options for booking_unit options field.
 *
 * @param $per_person
 *
 * @return array
 */
function bat_options_price_options($per_person = FALSE) {
  $options = array(
    BAT_OPTIONS_ADD => t('Add to price'),
    BAT_OPTIONS_ADD_DAILY => t('Add to price per night'),
    BAT_OPTIONS_SUB => t('Subtract from price'),
    BAT_OPTIONS_SUB_DAILY => t('Subtract from price per night'),
    BAT_OPTIONS_REPLACE => t('Replace price'),
    BAT_OPTIONS_INCREASE => t('Increase price by % amount'),
    BAT_OPTIONS_DECREASE => t('Decrease price by % amount'),
    BAT_OPTIONS_NOCHARGE => t('No charge'),
  );
  if ($per_person) {
    $options[BAT_OPTIONS_ADD_PERSON] = t('Add to price per person');
    $options[BAT_OPTIONS_ADD_DAILY_PERSON] = t('Add to price per night per person');
    $options[BAT_OPTIONS_SUB_PERSON] = t('Subtract from price per person');
    $options[BAT_OPTIONS_SUB_DAILY_PERSON] = t('Subtract from price per night per person');
  }
  drupal_alter('bat_options_price_options', $options);
  return $options;
}

/**
 * Returns available options given a Bat type.
 *
 * @param BatType $type
 *   The type from which to retrieve options.
 *
 * @return array
 *   The available options for the given type.
 */
function bat_options_get_type_options(BatType $type) {
  $options =& drupal_static(__FUNCTION__);
  if (isset($options['types'][$type->type_id])) {
    return $options['types'][$type->type_id];
  }
  $type_options = is_array(field_get_items('bat_type', $type, 'field_addons')) ? field_get_items('bat_type', $type, 'field_addons') : array();
  $options['types'][$type->type_id] = $type_options;
  return $options['types'][$type->type_id];
}

/**
 * Converts option human name to its machine name.
 *
 * @param string $option_name
 *   The human option name.
 * @param string $pattern
 *   The pattern used to convert. By default "/[^a-z0-9_]+/".
 * @param string $replacement
 *   The replacement string. By default "_".
 *
 * @return string
 *   The option machine name.
 */
function bat_options_get_machine_name($option_name, $pattern = '/[^a-z0-9_]+/', $replacement = '_') {
  return preg_replace($pattern, $replacement, drupal_strtolower($option_name));
}

/**
 * Given an option, return a string that explains the operation.
 *
 * @param $option
 *
 * @return string
 */
function bat_options_get_operation_label($option) {
  $label = '';
  $currency_setting = commerce_currency_load(commerce_default_currency());
  $currency_symbol = $currency_setting['symbol'];
  switch ($option['operation']) {
    case 'add':
      $label = t('+@amount@currency_symbol to price', array(
        '@amount' => $option['value'],
        '@currency_symbol' => $currency_symbol,
      ));
      break;
    case 'add-daily':
      $label = t('+@amount@currency_symbol per night to price', array(
        '@amount' => $option['value'],
        '@currency_symbol' => $currency_symbol,
      ));
      break;
    case 'sub-daily':
      $label = t('-@amount@currency_symbol per night from price', array(
        '@amount' => $option['value'],
        '@currency_symbol' => $currency_symbol,
      ));
      break;
    case 'replace':
      $label = t('Replace price with @amount@currency_symbol', array(
        '@amount' => $option['value'],
        '@currency_symbol' => $currency_symbol,
      ));
      break;
    case 'increase':
      $label = t('Increase price by @amount%', array(
        '@amount' => $option['value'],
      ));
      break;
    case 'decrease':
      $label = t('Decrease price by @amount%', array(
        '@amount' => $option['value'],
      ));
      break;
    case 'sub':
      $label = t('-@amount@currency_symbol from price', array(
        '@amount' => $option['value'],
        '@currency_symbol' => $currency_symbol,
      ));
      break;
    case 'add_person':
      $label = t('+@amount@currency_symbol per person to price', array(
        '@amount' => $option['value'],
        '@currency_symbol' => $currency_symbol,
      ));
      break;
    case 'add-daily_person':
      $label = t('+@amount@currency_symbol per night per person to price', array(
        '@amount' => $option['value'],
        '@currency_symbol' => $currency_symbol,
      ));
      break;
    case 'sub_person':
      $label = t('-@amount@currency_symbol per person from price', array(
        '@amount' => $option['value'],
        '@currency_symbol' => $currency_symbol,
      ));
      break;
    case 'sub-daily_person':
      $label = t('-@amount@currency_symbol per night per person from price', array(
        '@amount' => $option['value'],
        '@currency_symbol' => $currency_symbol,
      ));
      break;
  }
  return $label;
}

/**
 * Calculate the price for an option.
 *
 * @param $booking_price
 * @param $option
 * @param $quantity
 * @param $nights
 * @param $persons
 *
 * @return float
 */
function bat_options_get_option_price($booking_price, $option, $quantity, $nights, $persons = 0) {
  $price = 0;
  switch ($option['operation']) {
    case BAT_OPTIONS_ADD:
      $price += $option['value'];
      break;
    case BAT_OPTIONS_ADD_DAILY:
      $price += $option['value'] * $nights;
      break;
    case BAT_OPTIONS_SUB:
      $price -= $option['value'];
      break;
    case BAT_OPTIONS_SUB_DAILY:
      $price -= $option['value'] * $nights;
      break;

    // NB: This will only be correctly calculated for a single add-on with the replace price operation per order.
    case BAT_OPTIONS_REPLACE:
      $price = $option['value'] - $booking_price;
      break;
    case BAT_OPTIONS_INCREASE:
      $price += $booking_price * ($option['value'] / 100);
      break;
    case BAT_OPTIONS_DECREASE:
      $price -= $booking_price * ($option['value'] / 100);
      break;
    case BAT_OPTIONS_ADD_PERSON:
      $price += $option['value'] * $persons;
      break;
    case BAT_OPTIONS_ADD_DAILY_PERSON:
      $price += $option['value'] * $nights * $persons;
      break;
    case BAT_OPTIONS_SUB_PERSON:
      $price -= $option['value'] * $persons;
      break;
    case BAT_OPTIONS_SUB_DAILY_PERSON:
      $price -= $option['value'] * $nights * $persons;
      break;
  }
  return $price * 100;
}

Functions

Namesort descending Description
bat_options_data_property_info Defines info for the properties of the bat_options data structure.
bat_options_element_value_validate Form element validation handler for numeric elements that must be positive.
bat_options_field_formatter_info Implements hook_field_formatter_info().
bat_options_field_formatter_view Implements hook_field_formatter_view().
bat_options_field_info Implements hook_field_info().
bat_options_field_is_empty Implements hook_field_is_empty().
bat_options_field_settings_form Implements hook_field_settings_form().
bat_options_field_widget_form Implements hook_field_widget_form().
bat_options_field_widget_info Implements hook_field_widget_info().
bat_options_get_machine_name Converts option human name to its machine name.
bat_options_get_operation_label Given an option, return a string that explains the operation.
bat_options_get_option_price Calculate the price for an option.
bat_options_get_type_options Returns available options given a Bat type.
bat_options_menu Implements hook_menu().
bat_options_price_options Returns the available price options for booking_unit options field.
bat_options_property_info_callback Property callback for the Entity Metadata framework.
bat_options_remove_js Page callback to handle AJAX for removing a bat options item.
bat_options_remove_submit Submit callback to remove an item from the field UI multiple wrapper.

Constants