You are here

opening_hours.module in Opening hours 7

Same filename and directory in other branches
  1. 6 opening_hours.module

Opening hours module.

File

opening_hours.module
View source
<?php

/**
 * @file
 * Opening hours module.
 */

/**
 * Implements hook_menu().
 */
function opening_hours_menu() {
  $include_path = drupal_get_path('module', 'opening_hours') . '/includes';
  $items = array();
  $items['node/%node/opening_hours'] = array(
    'title' => 'Opening hours',
    'page callback' => 'opening_hours_node_edit_page',
    'page arguments' => array(
      1,
    ),
    'access callback' => 'opening_hours_node_edit_access',
    'access arguments' => array(
      1,
    ),
    'weight' => 3,
    'file' => 'opening_hours.pages.inc',
    'file path' => $include_path,
    'type' => MENU_LOCAL_TASK,
  );
  $items['opening_hours/instances'] = array(
    'page callback' => 'opening_hours_crud_api_page',
    'access callback' => TRUE,
    'file' => 'opening_hours.pages.inc',
    'file path' => $include_path,
    'type' => MENU_CALLBACK,
  );
  $items['opening_hours/instances/%opening_hours_instance'] = array(
    'page callback' => 'opening_hours_instance_id_api_page',
    'page arguments' => array(
      2,
    ),
    'access callback' => 'user_access',
    'access arguments' => array(
      'edit opening hours for content',
    ),
    'file' => 'opening_hours.pages.inc',
    'file path' => $include_path,
    'type' => MENU_CALLBACK,
  );
  $items['admin/structure/opening_hours'] = array(
    'title' => 'Opening hours',
    'description' => 'Managed blocked days for the opening hours module.',
    'page callback' => 'opening_hours_admin_settings_page',
    'access arguments' => array(
      'administer opening hours configuration',
    ),
    'file' => 'opening_hours.admin.inc',
    'file path' => $include_path,
  );
  $items['admin/structure/opening_hours/blocked_day/add'] = array(
    'title' => 'Add blocked day',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'opening_hours_admin_blocked_day_add_form',
    ),
    'access arguments' => array(
      'administer opening hours configuration',
    ),
    'file' => 'opening_hours.admin.inc',
    'file path' => $include_path,
  );
  $items['admin/structure/opening_hours/blocked_day/%/delete'] = array(
    'title' => 'Delete blocked day',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'opening_hours_admin_blocked_day_delete_form',
      4,
    ),
    'access arguments' => array(
      'administer opening hours configuration',
    ),
    'file' => 'opening_hours.admin.inc',
    'file path' => $include_path,
  );
  return $items;
}

/**
 * Implements hook_block_info().
 */
function opening_hours_block_info() {
  return array(
    'week' => array(
      'info' => t('Opening hours for node by week'),
      'cache' => DRUPAL_NO_CACHE,
    ),
  );
}

/**
 * Implements hook_block_view().
 */
function opening_hours_block_view($delta = '') {
  $block = array();
  if ($node = menu_get_object()) {
    $block['subject'] = t('Opening hours');
    $block['content'] = array(
      '#theme' => 'opening_hours_' . $delta,
      '#node' => $node,
    );
  }
  return $block;
}

/**
 * Implements hook_form_FORM_ID_alter().
 *
 * We add our own fieldset to the node settings form, so the user can
 * enable opening hours for a node type there.
 */
function opening_hours_form_node_type_form_alter(&$form, &$form_state, $form_id) {
  $form['opening_hours'] = array(
    '#type' => 'fieldset',
    '#title' => t('Opening hours'),
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
    '#group' => 'additional_settings',
    '#attributes' => array(
      'class' => array(
        'opening-hours-node-type-settings-form',
      ),
    ),
    '#attached' => array(
      'js' => array(
        drupal_get_path('module', 'opening_hours') . '/js/opening_hours.node-type-form.js',
      ),
    ),
  );
  $form['opening_hours']['opening_hours_enabled'] = array(
    '#type' => 'checkbox',
    '#title' => t('Enable opening hours for this content type'),
    '#default_value' => variable_get('opening_hours_enabled_' . $form['#node_type']->type, FALSE),
  );
  $form['opening_hours']['opening_hours_hide_on_empty'] = [
    '#type' => 'checkbox',
    '#title' => t('Hide opening hours widget for nodes with no opening hours data'),
    '#default_value' => variable_get('opening_hours_hide_on_empty_' . $form['#node_type']->type, TRUE),
  ];

  // If taxonomy.module is enabled, allow the user to pick a category
  // vocabulary for categorization of opening hours periods.
  if (function_exists('taxonomy_vocabulary_get_names')) {

    // Generate a list of form API options for selecting a vocabulary.
    $options = array(
      t('- None -'),
    );
    foreach (taxonomy_vocabulary_get_names() as $vocab) {
      $options[$vocab->machine_name] = $vocab->name;
    }
    $form['opening_hours']['opening_hours_category_vocabulary'] = array(
      '#type' => 'select',
      '#title' => t('Category vocabulary'),
      '#description' => t('You can select a taxonomy vocabulary to act as categories for opening hours here. If enabled, you will be able to select one of the terms in that vocabulary as category when creating or editing an opening hours time interval.'),
      '#options' => $options,
      '#default_value' => variable_get('opening_hours_category_vocabulary_' . $form['#node_type']->type, 0),
      '#states' => array(
        'visible' => array(
          ':input[name="' . 'opening_hours_enabled' . '"]' => array(
            'checked' => TRUE,
          ),
        ),
      ),
    );
  }
}

/**
 * Implements hook_permission().
 */
function opening_hours_permission() {
  return array(
    'edit opening hours for content' => array(
      'title' => t('Edit opening hours for content'),
      'description' => t('Allows the user to configure opening hours for any node type that has opening hours enabled.'),
    ),
    'administer opening hours configuration' => array(
      'title' => t('Administer opening hours configuration'),
      'description' => t('Allows the user to configure global closing days for opening hours and other settings.'),
    ),
  );
}

/**
 * Implements hook_cron().
 */
function opening_hours_cron() {
  $last_monthly = variable_get('opening_hours_last_monthly_cron', 0);

  // If more than 30 days has passed since last cron, run it again.
  if ($last_monthly < $_SERVER['REQUEST_TIME'] - 30 * 86400) {

    // Delete repeating instance copies more than a week in the past.
    db_query("\n      DELETE FROM {opening_hours}\n      WHERE original_instance_id IS NOT NULL\n      AND customised = 0\n      AND date < :date\n    ", array(
      ':date' => date('Y-m-d', $_SERVER['REQUEST_TIME'] - 7 * 86400),
    ));

    // Propagate instances that are still repeating.
    $propagate_query = db_query("\n      SELECT * FROM {opening_hours}\n      WHERE repeat_end_date > :date\n      AND repeat_rule IS NOT NULL AND repeat_rule != ''\n    ", array(
      ':date' => date('Y-m-d', $_SERVER['REQUEST_TIME']),
    ));
    foreach ($propagate_query as $instance) {
      opening_hours_repeat_instance_propagate($instance);
    }
  }
  variable_set('opening_hours_last_monthly_cron', $_SERVER['REQUEST_TIME']);
}

/**
 * Implements hook_theme().
 */
function opening_hours_theme($existing, $type, $theme, $path) {
  return array(
    'opening_hours_admin' => array(
      'variables' => array(),
      'path' => $path . '/templates',
      'template' => 'opening_hours_admin',
    ),
    'opening_hours_presentation' => array(
      'variables' => array(),
      'path' => $path . '/templates',
      'template' => 'opening_hours_presentation',
    ),
    'opening_hours_week' => array(
      'variables' => array(
        'node' => NULL,
      ),
      'path' => $path . '/templates',
      'template' => 'opening_hours_week',
    ),
  );
}

/**
 * Implements hook_admin_paths().
 */
function opening_hours_admin_paths() {
  return array(
    'node/*/opening_hours' => TRUE,
  );
}

/**
 * Implements hook_init().
 */
function opening_hours_init() {

  // If overlay.module is active it breaks our JavaScript event handling.
  // This hook disables overlay on opening hours edit page (but doesn't
  // enable it back as side-effect).
  if (function_exists('overlay_set_mode') && arg(0) == 'node' && arg(2) == 'opening_hours') {
    overlay_set_mode('none');
  }
}

/**
 * Implements hook_ctools_plugin_directory().
 */
function opening_hours_ctools_plugin_directory($module, $plugin) {
  if ($module == 'ctools' && !empty($plugin)) {
    return "plugins/{$plugin}";
  }
}

/**
 * Access callback for the opening hours node edit page.
 */
function opening_hours_node_edit_access($node) {
  return user_access('edit opening hours for content') && variable_get('opening_hours_enabled_' . $node->type);
}

/**
 * Check if any opening hours has been input on a node.
 *
 * Used for hiding the opening_hours block on pages where it will never
 * display any data.
 *
 * @param int $nid
 *   Node ID to check.
 *
 * @return bool
 *   TRUE if opening hours are present, FALSE if not.
 */
function opening_hours_present_on_node($nid, $reset = FALSE) {
  static $presence = array();

  // Load Drupal’s cache if $presence array is empty.
  if (!$reset && empty($presence) && ($cache = cache_get('opening_hours_present_on_node'))) {
    if (is_array($cache->data)) {
      $presence = $cache->data;
    }
  }

  // Check the static cache.
  if (!isset($presence[$nid]) || $reset) {
    $presence[$nid] = (bool) db_query("\n      SELECT instance_id FROM {opening_hours} WHERE nid = :nid LIMIT 1\n    ", array(
      ':nid' => $nid,
    ))
      ->fetchField();
    cache_set('opening_hours_present_on_node', $presence);
  }
  return $presence[$nid];
}

/**
 * Load opening hours instance by id.
 */
function opening_hours_instance_load($instance_id) {
  $query = db_query("SELECT * FROM {opening_hours} WHERE instance_id = :id LIMIT 1", array(
    ':id' => $instance_id,
  ));
  if ($row = $query
    ->fetchObject()) {
    return opening_hours_instance_prepare($row);
  }
  return FALSE;
}

/**
 * Load opening hours instances by nid and date.
 */
function opening_hours_instance_load_multiple($nids, $from_date, $to_date) {

  // Make sure nids is an array.
  if (!is_array($nids)) {
    $nids = array(
      $nids,
    );
  }

  // Filter nids, so we don't pass nasty things to the database.
  $nids = array_filter($nids, 'is_numeric');
  $nids = array_filter($nids);
  $query = db_query("\n    SELECT * FROM {opening_hours} WHERE nid IN (:nids)\n    AND date BETWEEN :from_date AND :to_date\n    ORDER BY start_time\n  ", array(
    ':nids' => $nids,
    ':from_date' => $from_date,
    ':to_date' => $to_date,
  ));
  $results = array();
  foreach ($query as $row) {
    $results[] = opening_hours_instance_prepare($row);
  }

  // Alter instances if is needed.
  drupal_alter('opening_hours_instance_load_multiple', $results);
  return $results;
}

/**
 * Prepare an instance object loaded from the database for use with Backbone.
 */
function opening_hours_instance_prepare($instance) {

  // Cast integers to the correct type.
  $instance->instance_id = (int) $instance->instance_id;
  $instance->nid = (int) $instance->nid;
  $instance->category_tid = !empty($instance->category_tid) ? (int) $instance->category_tid : NULL;
  $instance->original_instance_id = !empty($instance->original_instance_id) ? (int) $instance->original_instance_id : NULL;

  // Backbone expects the primary key to be named `id`. Let's not disappoint.
  $instance->id = $instance->instance_id;
  $instance->start_time = opening_hours_format_time($instance->start_time);
  $instance->end_time = opening_hours_format_time($instance->end_time);
  return $instance;
}

/**
 * Format a time value from the database, stripping the seconds.
 */
function opening_hours_format_time($time) {
  $matches = array();
  $time_formatted = NULL;
  preg_match('/^([0-2]?\\d):([0-5]?\\d)/', $time, $matches);
  if (!empty($matches[1]) && !empty($matches[2])) {
    $time_formatted = $matches[1] . ':' . $matches[2];
  }
  drupal_alter('opening_hours_format_time', $time_formatted);
  return $time_formatted;
}

/**
 * Propagates a repeating instance.
 *
 * Makes copies of the event each time it repeats until either the
 * repeat rule ends or two years have passed.
 */
function opening_hours_repeat_instance_propagate(&$instance) {

  // Maximum limit is about two years in the future.
  $limit = $_SERVER['REQUEST_TIME'] + 365 * 86400;

  // Set up the increment for the repeat rule.
  if ($instance->repeat_rule == 'weekly') {
    $increment = 7 * 86400;
  }
  if (!empty($instance->repeat_end_date)) {

    // Use noon on the date when converting to timestamp to dodge
    // daylight savings issues.
    $end_date = strtotime($instance->repeat_end_date . 'T12:00:00');

    // If the end date is before the limit, it becomes the new limit.
    if ($end_date && $end_date < $limit) {
      $limit = $end_date;
    }
  }

  // Bail if we don't have an increment.
  if (empty($increment) || $increment < 2) {
    return;
  }
  $current_date = strtotime($instance->date . 'T12:00:00');

  // Figure out how far the instance has already been propagated, and
  // start there.
  $start_point_date = db_query('
    SELECT MAX(date) FROM {opening_hours} WHERE original_instance_id = :id
  ', array(
    ':id' => $instance->instance_id,
  ))
    ->fetchField();
  if ($start_point_date) {
    $start_point_date = strtotime($start_point_date . 'T12:00:00');

    // If our start point is later than the current date, use that when
    // iterating, so we don't generate duplicate entries.
    if ($start_point_date > $current_date) {
      $current_date = $start_point_date;
    }
  }
  for ($current_date += $increment; $current_date < $limit; $current_date += $increment) {

    // Generate the new propagated instance.
    $propagated = (object) array(
      'nid' => $instance->nid,
      'date' => date('Y-m-d', $current_date),
      'start_time' => $instance->start_time,
      'end_time' => $instance->end_time,
      'original_instance_id' => $instance->instance_id,
      'customised' => 0,
    );

    // Propagate the category_tid, if set.
    if (!empty($instance->category_tid)) {
      $propagated->category_tid = $instance->category_tid;
    }

    // Propagate the notice, if set.
    if (!empty($instance->notice)) {
      $propagated->notice = $instance->notice;
    }
    drupal_write_record('opening_hours', $propagated);
  }
}

/**
 * Prevent additional propagation of instance.
 *
 * Sets the repeat end date of an instance to the date of the latest
 * existing instance to prevent additional propagation.
 */
function opening_hours_repeat_stop_propagation($instance_id) {

  // Get the date of the last non-deleted instance.
  $max_date = db_query('
    SELECT MAX(date) FROM {opening_hours} WHERE original_instance_id = :id
  ', array(
    ':id' => $instance_id,
  ))
    ->fetchField();
  db_query("\n    UPDATE {opening_hours}\n    SET repeat_end_date = :date\n    WHERE instance_id = :id\n  ", array(
    ':date' => $max_date,
    ':id' => $instance_id,
  ));
}

/**
 * Generate the JavaScript settings array shared between presentation and admin.
 */
function opening_hours_js_settings($nid = NULL) {
  $settings = array(
    'blockedDays' => variable_get('opening_hours_blocked_days', array()),
    'firstDayOfWeek' => (int) variable_get('date_first_day', 1),
    // Options for the jQuery UI datepicker date formatter.
    'formatDate' => array(
      'monthNames' => array(
        t('January'),
        t('February'),
        t('March'),
        t('April'),
        t('May'),
        t('June'),
        t('July'),
        t('August'),
        t('September'),
        t('October'),
        t('November'),
        t('December'),
      ),
      'dayNames' => array(
        t('Sunday'),
        t('Monday'),
        t('Tuesday'),
        t('Wednesday'),
        t('Thursday'),
        t('Friday'),
        t('Saturday'),
      ),
    ),
  );
  $settings['categories'] = array();

  // If we have a category vocabulary selected for this node
  // type, get the category terms and export them as a setting.
  if (function_exists('taxonomy_vocabulary_machine_name_load')) {
    if ($nid) {
      $node = node_load($nid);
    }
    else {
      $node = menu_get_object();
    }
    if (!empty($node->type)) {
      $machine_name = variable_get('opening_hours_category_vocabulary_' . $node->type, 0);
      if (!empty($machine_name)) {
        $vocabulary = taxonomy_vocabulary_machine_name_load($machine_name);
        if ($vocabulary) {
          foreach (taxonomy_get_tree($vocabulary->vid, 0, 1) as $term) {
            $settings['categories'][$term->tid] = $term->name;
          }
        }
      }
    }
  }
  return $settings;
}

/**
 * Common JavaScript attachments.
 *
 * Generates an array suitable for use with #attached.
 * See http://shomeya.com/articles/getting-used-to-attached for details.
 */
function opening_hours_js_attach_common($nid = NULL) {
  $attachments =& drupal_static(__FUNCTION__);
  if (empty($attachments)) {

    // Provide common JavaScript settings.
    $attachments['js'][] = array(
      'data' => array(
        'OpeningHours' => opening_hours_js_settings($nid),
      ),
      'type' => 'setting',
    );
    $path = drupal_get_path('module', 'opening_hours') . '/js/';
    $attachments['js'][] = $path . 'opening_hours.prototype.js';
    $attachments['js'][] = $path . 'opening_hours.core.js';
    $attachments['js'][] = array(
      // Use the minified version of Underscore when JS aggregation is enabled.
      'data' => variable_get('preprocess_js', FALSE) ? $path . 'underscore-min.js' : $path . 'underscore.js',
      'type' => 'file',
      'group' => JS_LIBRARY,
    );

    // We need the datepicker plugin for formatting and selecting dates.
    // TODO: Figure out if this can be loaded via attachments.
    date_popup_add();

    // Only return the attachments the first time, to avoid these files
    // being added to the page twice, messing up the order.
    return $attachments;
  }
}

/**
 * Admin-specific attachments.
 */
function opening_hours_js_attach_admin($nid) {
  $attachments =& drupal_static(__FUNCTION__ . ':' . $nid);
  if (empty($attachments)) {
    $path = drupal_get_path('module', 'opening_hours') . '/js/';
    $attachments = opening_hours_js_attach_common();

    // A bit of extra styling.
    drupal_add_library('system', 'ui.datepicker');
    drupal_add_library('system', 'ui.dialog');
    $attachments['css'][] = drupal_get_path('module', 'opening_hours') . '/css/opening_hours.admin.css';

    // We need the dialog library.
    $attachments['library'][] = array(
      'system',
      'ui.dialog',
    );
    $attachments['js'][] = array(
      // Use the minified version of Backbone when JS aggregation is enabled.
      'data' => variable_get('preprocess_js', FALSE) ? $path . 'backbone-min.js' : $path . 'backbone.js',
      'type' => 'file',
      'group' => JS_LIBRARY,
    );
    $attachments['js'][] = $path . 'opening_hours.models.js';
    $attachments['js'][] = $path . 'opening_hours.collections.js';
    $attachments['js'][] = $path . 'opening_hours.views.js';
    $attachments['js'][] = $path . 'opening_hours.routers.js';
    $attachments['js'][] = $path . 'opening_hours.admin.js';

    // For the admin page, we need the node ID, passed from the page callback.
    $attachments['js'][0]['data']['OpeningHours'] += array(
      'nid' => $nid,
      'path' => base_path() . drupal_get_path('module', 'opening_hours'),
    );
  }
  return $attachments;
}

/**
 * Attachments for the presentation page.
 */
function opening_hours_js_attach_presentation($nid = NULL) {
  $attachments = opening_hours_js_attach_common($nid);
  $path = drupal_get_path('module', 'opening_hours');
  $attachments['css'][] = $path . '/css/opening_hours.theme.css';
  $attachments['js'][] = $path . '/js/opening_hours.presentation.js';
  return $attachments;
}

/**
 * Helper function to load our JavaScript dependencies.
 *
 * Kept around for backwards compatibilty. You should not call this, if
 * possible, add opening_hours_js_attach_presentation or
 * opening_hours_js_attach_admin to your render array instead.
 *
 * It is still called in template_preprocess_opening_hours_week(), since
 * we would need to change the API of the module if we didn't.
 * So far, it's been possible to embed the opening hours with a single
 * theme('opening_hours_week', $node'). If we want that, we'll have to
 * add the JavaScript in the preprocessor, rather than the render array.
 */
function opening_hours_add_js($type = 'presentation', $nid = FALSE) {
  if ($type == 'admin') {
    $attachments = opening_hours_js_attach_admin($nid);
  }
  elseif ($type == 'presentation') {
    $attachments = opening_hours_js_attach_presentation($nid);
  }
  if (!empty($attachments)) {

    // Create a fake empty element, since that is what
    // drupal_process_attached expects.
    $fakeelement = array(
      '#attached' => $attachments,
    );
    drupal_process_attached($fakeelement);
  }
}

/**
 * Preprocess variables for the week template.
 */
function template_preprocess_opening_hours_week(&$vars) {
  static $once;
  $nid = NULL;
  if ($vars['node']) {
    $nid = $vars['node']->nid;
  }

  // Only add JavaScript and templates the first time this is run on a page.
  if (!$once) {
    opening_hours_add_js('presentation', $nid);

    // Add our client-side templates to the page.
    $vars['preface'] = theme('opening_hours_presentation');
    $once = TRUE;
  }
}

/**
 * Implements hook_field_extra_fields().
 *
 * Define opening hours as an extra field for display in view modes.
 */
function opening_hours_field_extra_fields() {
  $extra = array();
  $node_types = node_type_get_types();
  foreach ($node_types as $node_type) {

    // We only provide the extra fields on the enabled node types.
    if (variable_get('opening_hours_enabled_' . $node_type->type, FALSE)) {
      $extra['node'][$node_type->type] = array(
        'display' => array(
          'opening_hours_week' => array(
            'label' => t('Opening hours'),
            'description' => t('Opening hours'),
            'weight' => 0,
            'visible' => FALSE,
          ),
        ),
      );
    }
  }
  return $extra;
}

/**
 * Implements hook_node_view().
 *
 * Add opening hours as a field on node views.
 */
function opening_hours_node_view($node, $view_mode, $langcode) {
  if (!variable_get('opening_hours_hide_on_empty_' . $node->type, TRUE) || opening_hours_present_on_node($node->nid)) {
    $extrafields = field_extra_fields_get_display('node', $node->type, $view_mode);
    if (isset($extrafields['opening_hours_week']) && isset($extrafields['opening_hours_week']['visible']) && $extrafields['opening_hours_week']['visible']) {

      // We can't control label position on extra fields in UI. We'll store our
      // choice in a variable instead.
      $label_position = variable_get('opening_hours_week_label_position__' . $node->type . '__' . $view_mode, 'above');
      $node->content['opening_hours_week'] = array(
        '#entity_type' => 'node',
        '#theme' => array(
          'field',
        ),
        '#label_display' => $label_position,
        '#items' => array(
          0,
        ),
        '#title' => t('Opening hours'),
        '#language' => $langcode,
        '#field_name' => 'opening_hours_week',
        '#field_type' => 'text',
        '#bundle' => $node->type,
        '#view_mode' => $view_mode,
        0 => array(
          '#theme' => 'opening_hours_week',
          '#node' => $node,
        ),
      );
    }
  }
}

Functions

Namesort descending Description
opening_hours_add_js Helper function to load our JavaScript dependencies.
opening_hours_admin_paths Implements hook_admin_paths().
opening_hours_block_info Implements hook_block_info().
opening_hours_block_view Implements hook_block_view().
opening_hours_cron Implements hook_cron().
opening_hours_ctools_plugin_directory Implements hook_ctools_plugin_directory().
opening_hours_field_extra_fields Implements hook_field_extra_fields().
opening_hours_format_time Format a time value from the database, stripping the seconds.
opening_hours_form_node_type_form_alter Implements hook_form_FORM_ID_alter().
opening_hours_init Implements hook_init().
opening_hours_instance_load Load opening hours instance by id.
opening_hours_instance_load_multiple Load opening hours instances by nid and date.
opening_hours_instance_prepare Prepare an instance object loaded from the database for use with Backbone.
opening_hours_js_attach_admin Admin-specific attachments.
opening_hours_js_attach_common Common JavaScript attachments.
opening_hours_js_attach_presentation Attachments for the presentation page.
opening_hours_js_settings Generate the JavaScript settings array shared between presentation and admin.
opening_hours_menu Implements hook_menu().
opening_hours_node_edit_access Access callback for the opening hours node edit page.
opening_hours_node_view Implements hook_node_view().
opening_hours_permission Implements hook_permission().
opening_hours_present_on_node Check if any opening hours has been input on a node.
opening_hours_repeat_instance_propagate Propagates a repeating instance.
opening_hours_repeat_stop_propagation Prevent additional propagation of instance.
opening_hours_theme Implements hook_theme().
template_preprocess_opening_hours_week Preprocess variables for the week template.