You are here

oa_notifications.module in Open Atrium Notifications 7.2

File

oa_notifications.module
View source
<?php

/**
 * @file
 * Code for the Open Atrium Notifications feature.
 */
include_once 'oa_notifications.features.inc';

/**
 * Name of session key for saving "do not notify" option
 */
define('SKIP_FLAG', 'oa_skip_notify');

/**
 * Name of session key for saving "expand all" option
 */
define('DETAIL_FLAG', 'oa_notify_detail');

/**
 * Name of form element containing notification settings
 */
define('OA_NOTIFY_FORM', 'oa_notifications');

/**
 * Implements hook_ctools_plugin_directory().
 */
function oa_notifications_ctools_plugin_directory($owner, $plugin_type) {
  if ($owner == 'ctools' && $plugin_type == 'content_types') {
    return 'plugins/content_types';
  }
}

/**
 * Implements hook_menu().
 */
function oa_notifications_menu() {
  $items['oa_notifications/autocomplete/%'] = array(
    'title' => 'Autocomplete for notifications',
    'page callback' => 'oa_notifications_autocomplete_callback',
    'page arguments' => array(
      2,
    ),
    'access arguments' => array(
      'access content',
    ),
    'type' => MENU_CALLBACK,
  );
  $items['oa_notifications/%ctools_js/remove/%node/%/%'] = array(
    'title' => 'Autocomplete for notifications',
    'page callback' => 'oa_notifications_remove_callback',
    'page arguments' => array(
      1,
      3,
      4,
      5,
    ),
    'access arguments' => array(
      'access content',
    ),
    'type' => MENU_CALLBACK,
  );
  return $items;
}

/**
 * Implements hook_contextual_tabs_alter().
 *
 * Adds a "Subscribe" or "Unsubscribe" flag.
 */
function oa_notifications_contextual_tabs_alter(&$data) {
  if ($node = menu_get_object()) {
    drupal_add_js(drupal_get_path('module', 'oa_notifications') . '/oa_notifications.js');
    array_push($data['links'], array(
      'data' => flag_create_link('subscribe_section_content', $node->nid),
    ));
  }
}

/**
 * Implements hook_theme().
 */
function oa_notifications_theme() {
  return array(
    'oa_notifications_view' => array(
      'template' => 'oa-notifications-view',
      'variables' => array(
        'node' => NULL,
        'notifications' => array(),
      ),
      'path' => drupal_get_path('module', 'oa_notifications') . '/templates',
    ),
  );
}

/**
 * Return the resolved list of all users to be notified on a piece of content.
 *
 * @param object $node
 *   The node to return notifications.
 *
 * @return array
 *   An array of users keyed by uid.
 */
function oa_notifications_get_notifications($node, $notifications = NULL) {
  $cache =& drupal_static(__FUNCTION__);

  // See if notifications should be skipped.
  if (oa_notification_skip()) {
    return array();
  }
  $nid = isset($node) ? $node->nid : 0;
  if (!isset($cache[$nid])) {
    $bucket = array();
    $gid = isset($node) ? oa_core_get_group_from_node($node) : oa_core_get_space_context();
    if (!isset($notifications)) {
      $notifications = oa_notifications_load_multiple($node);
    }
    if (isset($notifications['group'])) {
      foreach (array_keys($notifications['group']) as $group_id) {
        $users = oa_core_get_group_users_for_space($gid, $group_id);
        $bucket += $users;
      }
    }
    if (isset($notifications['team'])) {
      foreach (array_keys($notifications['team']) as $team_id) {
        $results = oa_teams_get_team_members($team_id);
        $users = user_load_multiple(array_keys($results));
        $bucket += $users;
      }
    }
    if (isset($notifications['user'])) {
      $users = user_load_multiple(array_keys($notifications['user']));
      $bucket += $users;
    }
    if (isset($node)) {

      // Also grab users who flagged the content.
      $flags = flag_get_entity_flags('node', $node->nid, 'subscribe_section_content');
      if (!empty($flags) && is_array($flags)) {
        $bucket += user_load_multiple(array_keys($flags));
      }

      // Get users who subscribed to entire section.
      if (isset($node->{OA_SECTION_FIELD}[LANGUAGE_NONE][0]['target_id'])) {
        $flags = flag_get_entity_flags('node', $node->{OA_SECTION_FIELD}[LANGUAGE_NONE][0]['target_id'], 'subscribe_section_content');
        if (!empty($flags) && is_array($flags)) {
          $bucket += user_load_multiple(array_keys($flags));
        }
      }

      // Get users who subscribed to entire space.
      if (isset($node->{OA_SPACE_FIELD}[LANGUAGE_NONE][0]['target_id'])) {
        $flags = flag_get_entity_flags('node', $node->{OA_SPACE_FIELD}[LANGUAGE_NONE][0]['target_id'], 'subscribe_section_content');
        if (!empty($flags) && is_array($flags)) {
          $bucket += user_load_multiple(array_keys($flags));
        }
      }

      // Only notify users who have access to this node
      foreach ($bucket as $uid => $user) {
        if (!node_access('view', $node, $user)) {
          unset($bucket[$uid]);
        }
      }

      // Allow modules to alter users.
      drupal_alter('notifications_users', $bucket, $node);
    }
    $cache[$nid] = $bucket;
  }
  return $cache[$nid];
}

/**
 * Some utility functions...
 */

/**
 * Gets the users for a particular node.
 *
 * Get the listing of users that are in the specified space, or the current
 * space if no space is specified that also have access to the provided node.
 *
 * @param int $gid
 *   The space ID.
 */
function oa_notifications_get_users_for_node($node, $gid = NULL) {
  if (!isset($gid)) {
    $gid = oa_core_get_space_context();
  }

  // @TODO: This won't scale. We need users that have access to the given node
  // which is controlled by node_access grants, etc.  Not easily queried.
  // The move to an autocomplete might make this potential performance problem
  // disappear.
  $users = oa_core_get_inherited_users_for_space($gid);

  // The loop below is currently a performance issue that needs more work
  // Lets just return the list of users that have access to space without
  // actually calling full node_access. Unfortunately, og_subgroups_node_grants
  // will cause all subspaces to be entity-loaded!

  /*
  if (isset($node->nid)) {
    foreach ($users as $uid => $user) {
      if (!node_access('view', $node, $user)) {
        unset($users[$uid]);
      }
    }
  }
  */
  return $users;
}

/**
 * Return all notifications for given source.  Optionally filtered by type.
 *
 * @param object $source_entity
 *   The loaded source entity.
 * @param string $source_type
 *   The entity type of the source (defaults to 'node').
 * @param string $target_type
 *   An optional parameter that allows to filter the notification based on
 *   target type (group, team, user).
 *
 * @return array
 *   Returns an associative array of notifications first keyed by target type,
 *   then beneath that keyed by the target_id.
 */
function oa_notifications_load_multiple($source_entity, $source_type = 'node', $target_type = NULL) {
  if (is_numeric($source_entity)) {
    $source_entity = entity_load($source_type, array(
      $source_entity,
    ));
    $source_entity = current($source_entity);
  }
  $source_id = current(entity_extract_ids($source_type, $source_entity));
  $query = db_select('oa_notifications', 'n')
    ->fields('n')
    ->condition('n.source_id', $source_id)
    ->condition('n.source_type', $source_type);
  if (isset($target_type)) {
    $query
      ->condition('n.target_type', $target_type);
  }
  $notifications = array();
  $results = $query
    ->execute()
    ->fetchAllAssoc('notification_id');
  foreach ($results as $row) {
    $notifications[$row->target_type][$row->target_id] = $row;
  }
  drupal_alter('oa_notifications_load', $notifications, $source_entity, $source_type, $target_type);
  return $notifications;
}

/**
 * Create a notification instance
 * @param $type
 * @param $id
 * @param $source_type
 * @param $source_id
 * @return \stdClass
 */
function oa_notifications_make($type, $id, $source_type = 'node', $source_id = 0) {
  $n = new stdClass();
  $n->source_id = $source_id;
  $n->source_type = $source_type;
  $n->target_id = $id;
  $n->target_type = $type;
  return $n;
}

/**
 * Helper function to parse notifications from the input field value
 * @param $values
 * @param $source_type type of source entity, usually 'node'
 *   if empty, then remove the notifications instead of adding them
 * @param $source_id nid of node to add notifications
 * @param $notifications
 * @param $do_save bool optional to write each record to db as parsed
 */
function oa_notification_parse($value, $source_type, $source_id, &$notifications, $do_save = FALSE) {
  $items = explode(',', $value);
  if (!empty($items)) {
    foreach ($items as $item) {
      if (strpos($item, ':') !== FALSE) {
        list($type, $id) = explode(':', $item);
        if (!empty($id) && !empty($type)) {
          if (empty($source_type)) {
            if ($do_save && $notifications[$type][$id]) {
              oa_notifications_delete_for_target($id, $type);
            }
            unset($notifications[$type][$id]);
          }
          else {
            $n = oa_notifications_make($type, $id, $source_type, $source_id);
            if ($do_save && !isset($notifications[$type][$id])) {

              // only save if its a new notification
              drupal_write_record('oa_notifications', $n);
            }
            $notifications[$type][$id] = $n;
          }
        }
      }
    }
  }
}

/**
 * Saves notifications.
 *
 * @param array $values
 *   The values to save.
 */
function oa_notifications_save_notifications(&$values, $notifications = NULL) {
  if (isset($values['skip_notify'])) {
    oa_notification_skip($values['skip_notify']);
  }
  if (isset($values['source_id']) && isset($values['source_type'])) {
    drupal_alter('oa_notifications_save', $values);
    $source_type = $values['source_type'];
    $source_id = $values['source_id'];
    if (!$source_id) {

      // Node add form, so just update the form values.
      oa_notification_parse($values['notify_list']['combined'], $source_type, $source_id, $notifications);
      $values['notify_list']['combined'] = '';
      return $notifications;
    }

    // Otherwise, update the database directly.
    if (isset($values['override'])) {
      oa_notifications_save_override($source_type, $source_id, $values['override']);
    }
    if (!isset($values['override']) || $values['override']) {

      // Only adding a single notification.
      if (!empty($values['notify_list']['combined'])) {
        $notifications = oa_notifications_load_multiple($source_id, $source_type);
        oa_notification_parse($values['notify_list']['combined'], $source_type, $source_id, $notifications, TRUE);
        $values['notify_list']['combined'] = '';
        return $notifications;
      }
      else {

        // Adding/overwriting all notifications.
        foreach ($notifications as $type => $list) {
          foreach ($list as $id => $item) {

            // Basically updating source_id here
            $n = oa_notifications_make($type, $id, $source_type, $source_id);
            $notifications[$type][$id] = $n;
          }
        }
        oa_notifications_save_for_source($source_id, $source_type, $notifications);
      }
    }
  }
  return $notifications;
}

/**
 * Save a collection of Notifications for a particular source item.
 *
 * @param int $source_id
 *   The ID of the source (typically a nid).
 * @param string $source_type
 *   The entity type of the source (defaults to 'node').
 * @param array $notifications
 *   A collection of Notification object for this source.
 */
function oa_notifications_save_for_source($source_id, $source_type, $notifications) {
  if (!empty($source_id)) {
    db_delete('oa_notifications')
      ->condition('source_id', $source_id)
      ->condition('source_type', $source_type)
      ->execute();
    foreach ($notifications as $key => $value) {
      if (is_numeric($key)) {

        // Support a flat list of notification records
        unset($value->notification_id);
        drupal_write_record('oa_notifications', $value);
      }
      else {
        foreach ($value as $id => $n) {

          // Also support list keyed by notification type.
          unset($n->notification_id);
          drupal_write_record('oa_notifications', $n);
        }
      }
    }
  }
}

/**
 * Add a new notification to a node
 * @param $nid
 * @param $type
 * @param $id
 */
function oa_notifications_add($nid, $type, $id) {

  // See if record already exists
  $query = db_select('oa_notifications', 'n')
    ->fields('n', array(
    'notification_id',
  ))
    ->condition('n.source_id', $nid)
    ->condition('n.source_type', 'node')
    ->condition('n.target_type', $type)
    ->condition('n.target_id', $id);
  $results = $query
    ->execute()
    ->fetchCol(0);
  if (empty($results)) {

    // Add new notification record
    $n = oa_notifications_make($type, $id, 'node', $nid);
    drupal_write_record('oa_notifications', $n);
  }
}

/**
 * Implements hook_node_insert().
 */
function oa_notifications_node_insert($node) {
  if (oa_notifications_is_notification_type($node)) {
    if ($node->status == NODE_NOT_PUBLISHED) {

      // Skip notifications for draft content.
      oa_notification_skip(TRUE);
      $node->{OA_NOTIFY_FORM}['skip_notify'] = TRUE;
    }
    if (isset($node->notification_data)) {
      $node->{OA_NOTIFY_FORM}['source_id'] = $node->nid;
      $node->{OA_NOTIFY_FORM}['source_type'] = 'node';
      oa_notifications_save_notifications($node->{OA_NOTIFY_FORM}, $node->notification_data);
    }
  }
}

/**
 * Implements hook_node_update().
 */
function oa_notifications_node_update($node) {
  if (oa_notifications_is_notification_type($node)) {
    if ($node->status == NODE_NOT_PUBLISHED) {

      // Skip notifications for draft content.
      oa_notification_skip(TRUE);
    }
  }
}

/**
 * Determine if this is an entity type which we need to provide notifications.
 *
 * @param object $node
 *   The node object.
 *
 * @return bool
 *   Returns TRUE if entity type requires providing notifications.
 */
function oa_notifications_is_notification_type($node) {

  // @TODO: Implement this as configurable. For now, add
  // notifications to any group-content type, and groups themselves.
  $types = oa_core_list_content_types(TRUE);

  // Default notifications on spaces.
  $types[OA_SPACE_TYPE] = TRUE;

  // No notifications on teams.
  unset($types[OA_TEAM_TYPE]);
  drupal_alter('is_notification_type', $types);
  return array_key_exists($node->type, $types);
}

/**
 * Implements hook_node_delete().
 *
 * Cleanup the notifications for teams and groups as they are removed.
 */
function oa_notifications_node_delete($node) {
  if ($node->type == OA_TEAM_TYPE) {
    $target_type = 'team';
  }
  elseif ($node->type == OA_GROUP_TYPE) {
    $target_type = 'group';
  }
  if (isset($target_type)) {
    oa_notifications_delete_for_target($node->nid, $target_type);
  }
}

/**
 * Delete notifications for a target.
 *
 * @param int $id
 *   The target id (e.g. node id).
 * @param string $type
 *   The target type (e.g. 'group', 'team').
 */
function oa_notifications_delete_for_target($id, $type) {
  db_delete('oa_notifications')
    ->condition('target_id', $id)
    ->condition('target_type', $type)
    ->execute();
}

/**
 * Delete specific notification
 *
 * @param int $source_id
 *   The source id (node id)
 * @param int $id
 *   The target id (e.g. node id).
 * @param string $type
 *   The target type (e.g. 'group', 'team').
 */
function oa_notifications_delete_specific($source_id, $id, $type) {
  db_delete('oa_notifications')
    ->condition('source_id', $source_id)
    ->condition('target_id', $id)
    ->condition('target_type', $type)
    ->execute();
}

/**
 * AJAX callback saves the quick reply notification configuration.
 */
function oa_notifications_ajax_callback($form, $form_state) {
  $notifications = $form_state['storage']['notification_data'];
  if (isset($form_state['values'][OA_NOTIFY_FORM])) {
    $notifications = oa_notifications_save_notifications($form_state['values'][OA_NOTIFY_FORM], $notifications);
  }
  $form[OA_NOTIFY_FORM]['notify_list']['combined']['#value'] = $form_state['values'][OA_NOTIFY_FORM]['notify_list']['combined'];
  $form[OA_NOTIFY_FORM]['notify_list']['data'] = oa_notifications_render_view($form['#node'], true, $notifications);
  return $form[OA_NOTIFY_FORM]['notify_list'];
}

/**
 * Callback for AJAX saving of the skip-notification option.
 */
function oa_notifications_skip_ajax_callback($form, $form_state) {
  if (isset($form_state['values'][OA_NOTIFY_FORM]['skip_notify'])) {
    oa_notification_skip($form_state['values'][OA_NOTIFY_FORM]['skip_notify']);
    $form[OA_NOTIFY_FORM]['skip_notify']['#value'] = oa_notification_skip();
  }
  return $form[OA_NOTIFY_FORM]['skip_notify'];
}

/**
 * Define the fields that are used for configuring notifications.
 */
function oa_notifications_form_fields(&$form, &$form_state, $node) {
  $nid = isset($node->nid) ? $node->nid : 0;
  $form['#node'] = $node;
  $form['source_type'] = array(
    '#type' => 'value',
    '#value' => 'node',
  );
  $form['source_id'] = array(
    '#type' => 'value',
    '#value' => $nid,
  );
  $default = array(
    'group' => array(),
    'team' => array(),
    'user' => array(),
  );
  $triggering = isset($form_state['triggering_element']) ? $form_state['triggering_element']['#name'] : '';

  // Get from form state, if available, for AJAX.
  if ($triggering == 'oa_notifications[override]') {
    $override = !empty($form_state['input'][OA_NOTIFY_FORM]['override']);
    if (!empty($node->nid)) {
      oa_notifications_save_override('node', $node->nid, $override);
    }
  }
  else {
    $override = isset($form_state['input'][OA_NOTIFY_FORM]['override']) ? $form_state['input'][OA_NOTIFY_FORM]['override'] : ($node->type == 'oa_section' ? oa_notifications_is_overriding('node', $nid) : TRUE);
  }
  if ($node->type == 'oa_section') {

    // Sections are handled differently due to space inheritance and overriding.
    $form['override'] = array(
      '#type' => 'checkbox',
      '#title' => t('Override default space notifications'),
      '#default_value' => $override,
      '#ajax' => array(
        'wrapper' => 'notify-list',
        'callback' => 'oa_notifications_form_fields_override_ajax',
      ),
    );
    $notifications = $override ? oa_notifications_load_multiple($node) : oa_notifications_get_default_notifications(0, 0, FALSE);
  }
  elseif (!empty($triggering)) {

    // Load current notifications if we are in an ajax rebuild.
    $notifications = $form_state['storage']['notification_data'];
  }
  else {

    // content node or Space node, check for parent space in URL for defaults.
    $parent = 0;
    $section = 0;
    if (!empty($_GET[OA_PARENT_SPACE]) && is_numeric($_GET[OA_PARENT_SPACE])) {
      $parent = $_GET[OA_PARENT_SPACE];
      $section = FALSE;
    }
    if (!empty($_GET[OA_SECTION_FIELD]) && is_numeric($_GET[OA_SECTION_FIELD])) {
      $section = $_GET[OA_SECTION_FIELD];
    }
    $notifications = $nid ? oa_notifications_load_multiple($node) : oa_notifications_get_default_notifications($section, $parent, $node->type != OA_SPACE_TYPE);
  }
  if ($triggering == OA_NOTIFY_FORM . '[notify_list][combined]') {
    oa_notifications_process_remove($notifications, $form_state);
    if (isset($form_state['input'][OA_NOTIFY_FORM]['notify_list']['combined'])) {
      oa_notification_parse($form_state['input'][OA_NOTIFY_FORM]['notify_list']['combined'], 'node', $nid, $notifications);
    }
  }
  else {
    $notifications = array_merge($default, $notifications);
  }
  $form['subscribe'] = isset($form_state['want form']) ? array() : oa_notifications_subscribe_button($node);
  $form['notify_list'] = oa_notifications_build_list_form($node, $override, !empty($form_state['want form']), $notifications);
  $form_state['storage']['notification_data'] = $notifications;
  oa_notifications_reset();
  $using_defaults = !empty($form_state['want form']) && in_array($node->type, array(
    OA_SPACE_TYPE,
    OA_GROUP_TYPE,
    OA_SECTION_TYPE,
  ));
  $form['skip_notify'] = array(
    '#type' => 'checkbox',
    '#title' => t('Do not send notifications for this update.'),
    '#ajax' => array(
      'callback' => 'oa_notifications_skip_ajax_callback',
      'wrapper' => 'skip-notify-div',
      'method' => 'replace',
    ),
    '#prefix' => '<div id="skip-notify-div">',
    '#suffix' => '</div>',
    '#access' => !empty($form_state['want form']) || _oa_notification_show_skip($node),
    '#default_value' => $using_defaults,
  );
}
function oa_notifications_build_list_form($node, $override = FALSE, $editmode = FALSE, $notifications = NULL) {
  $nid = !empty($node->nid) ? $node->nid : 0;
  $element = array(
    '#type' => 'markup',
    '#tree' => TRUE,
    '#prefix' => '<div id="notify-list">',
    '#suffix' => '</div>',
  );
  $allow_edit = $editmode || _oa_notifications_allow_edit($node);
  $element['combined'] = array(
    '#type' => 'textfield',
    '#title' => t('Add groups, teams, or members to notifications'),
    '#multiple' => TRUE,
    '#default_value' => '',
    '#disabled' => empty($override),
    '#access' => $allow_edit,
    '#autocomplete_path' => 'oa_notifications/autocomplete/' . $nid,
    '#ajax' => array(
      'callback' => 'oa_notifications_ajax_callback',
      'wrapper' => 'notify-list',
    ),
    '#limit_validation_errors' => array(),
  );
  $element['data'] = oa_notifications_render_view($node, $editmode, $notifications);
  $element['show_details'] = isset($node->nid) ? oa_notifications_show_details_button() : array();
  $element['notification_remove'] = array(
    '#type' => 'hidden',
    '#value' => '',
    '#attributes' => array(
      'class' => array(
        'oa-notifications-remove',
      ),
    ),
  );
  $element['notification_add'] = array(
    '#type' => 'hidden',
    '#value' => '',
    '#attributes' => array(
      'class' => array(
        'oa-notifications-add',
      ),
    ),
  );
  return $element;
}

/**
 * Helper function to determine if notifications can be added to page
 * @param $node
 * @return bool
 */
function _oa_notifications_allow_edit($node) {
  if (arg(0) == 'node' && (arg(2) == 'edit' || arg(1) == 'add')) {
    return TRUE;
  }
  else {
    return empty($node->nid) || _oa_notifications_can_comment($node);
  }
}

/**
 * Helper function to return true if comment form is enabled for the node
 * @param $node
 */
function _oa_notifications_can_comment($node) {
  $result = FALSE;
  if (oa_notifications_is_notification_type($node)) {
    $comment_type = variable_get('comment_' . $node->type, COMMENT_NODE_OPEN);
    $result = user_access('post comments') && $comment_type == COMMENT_NODE_OPEN && isset($node->comment) && $node->comment == COMMENT_NODE_OPEN;
  }
  return $result;
}

/**
 * Helper to determine if the skip notification checkbox should be shown
 * @param $node
 * @return bool
 */
function _oa_notification_show_skip($node) {
  if (arg(0) == 'node' && arg(2) == 'edit') {
    return TRUE;
  }
  else {
    return _oa_notifications_can_comment($node);
  }
}

/**
 * Sets form value
 *
 * @param $element
 * @param $form_state
 * @param $form
 */
function oa_notifications_select2widget_entity_validate_field(&$element, &$form_state, $form) {
  $value = array();
  $entity_labels = array();
  $settings = $element['#settings']['select2widgetajax'];
  $field = field_widget_field($element, $form_state);
  $instance = field_info_instance($element['#entity_type'], $element['#field_name'], $element['#bundle']);
  $target_entities = explode(',', $element['#value']);
  foreach ($target_entities as &$target_entity) {
    if (is_numeric($target_entity)) {
      $value[$target_entity] = $target_entity;
      $entity = entity_load_single($field['settings']['target_type'], $target_entity);
      if ($entity !== FALSE) {
        $label = entity_label($field['settings']['target_type'], $entity);
        $key = "{$label} ({$target_entity})";
      }
      else {
        $key = "Anonymous ({$target_entity})";
      }

      // Labels containing commas or quotes must be wrapped in quotes.
      if (strpos($key, ',') !== FALSE || strpos($key, '"') !== FALSE) {
        $key = '"' . str_replace('"', '""', $key) . '"';
      }
      $entity_labels[$target_entity] = $key;
    }
    elseif (preg_match("/.+\\((\\d+)\\)/", $target_entity, $matches)) {
      $entity_labels[$matches[1]] = $target_entity;
      $value[$matches[1]] = $matches[1];
    }
  }
  $element['#attached']['js'][0]['data']['select2widgetajax']['elements'][$element['#id']]['init'] = $entity_labels;

  //Update default values
  form_set_value($element, $value, $form_state);
}

/**
 * AJAX callback for oa_notifications_form_fields override checkbox.
 */
function oa_notifications_form_fields_override_ajax($form, &$form_state) {
  $notifications = $form_state['storage']['notification_data'];
  $form[OA_NOTIFY_FORM]['notify_list']['data'] = oa_notifications_render_view($form['#node'], true, $notifications);
  return $form[OA_NOTIFY_FORM]['notify_list'];
}

/**
 * Implements hook_form_node_form_alter().
 */
function oa_notifications_form_node_form_alter(&$form, &$form_state, $form_id) {

  // We are on a node/edit/add form.
  // Check content type.
  if (isset($form['#node']) && oa_notifications_is_notification_type($form['#node'])) {
    $form[OA_NOTIFY_FORM] = array(
      '#type' => 'fieldset',
      '#title' => t('Notifications'),
      '#collapsible' => TRUE,
      '#collapsed' => in_array($form['#node']->type, array(
        OA_GROUP_TYPE,
        OA_SECTION_TYPE,
      )) ? TRUE : FALSE,
      '#weight' => 9,
      '#tree' => TRUE,
    );
    oa_notifications_form_fields($form[OA_NOTIFY_FORM], $form_state, $form['#node']);
    if (!empty($form_state['want form'])) {

      // Only executes on node ADD form (new nodes)
      $form['#submit'][] = 'oa_notifications_form_submit';
    }
  }
}

/**
 * Submit handler for node edit form to save notification values
 * Only executes on node ADD form (new nodes)
 * @param $form
 * @param $form_state
 */
function oa_notifications_form_submit($form, &$form_state) {
  if (empty($form[OA_NOTIFY_FORM]['skip_notify']['#access'])) {

    // skip notifications if notify checkbox is not accessible
    oa_notification_skip(TRUE);
    $form_state['values'][OA_NOTIFY_FORM]['skip_notify'] = TRUE;
  }
  elseif (isset($form_state['values'][OA_NOTIFY_FORM]['skip_notify'])) {
    oa_notification_skip($form_state['values'][OA_NOTIFY_FORM]['skip_notify']);
  }
  $node = $form_state['node'];
  if (empty($node->nid)) {

    // Only save from form data if on node/add page (nid is undefined)
    // Otherwise data is already saved in DB via ajax.
    $notifications = $form_state['storage']['notification_data'];
    oa_notifications_process_add($notifications, $form_state);
    oa_notifications_process_remove($notifications, $form_state);
    $node->notification_data = $notifications;
  }
}

/**
 * Remove any notifications added by javascript
 * @param $notifications
 * @param $form_state
 */
function oa_notifications_process_remove(&$notifications, &$form_state) {
  if (!empty($form_state['input'][OA_NOTIFY_FORM]['notify_list']['notification_remove'])) {
    oa_notification_parse($form_state['input'][OA_NOTIFY_FORM]['notify_list']['notification_remove'], '', 0, $notifications);
    $form_state['input'][OA_NOTIFY_FORM]['notify_list']['notification_remove'] = '';
    $form_state['storage']['notification_data'] = $notifications;
  }
}

/**
 * Adds any notifications added by javascript
 * @param $notifications
 * @param $form_state
 */
function oa_notifications_process_add(&$notifications, &$form_state) {
  if (!empty($form_state['input'][OA_NOTIFY_FORM]['notify_list']['notification_add'])) {
    oa_notification_parse($form_state['input'][OA_NOTIFY_FORM]['notify_list']['notification_add'], 'node', 0, $notifications);
    $form_state['input'][OA_NOTIFY_FORM]['notify_list']['notification_add'] = '';
    $form_state['storage']['notification_data'] = $notifications;
  }
}

/**
 * Gets a piece of section content's default notifications.
 *
 * @param int $section_id
 *   Node ID of the content's section. Optional.
 * @param int $space_id
 *   Node ID of the content's space. Optional.
 */
function oa_notifications_get_default_notifications($section_id = 0, $space_id = 0, $allow_override = TRUE) {
  $notifications = array();

  // Space / section default to what's in the context.
  // Don't override section if is set to FALSE (like when creating subspaces).
  $section_id = $section_id || $section_id === FALSE ? $section_id : oa_core_get_section_context();
  $space_id = $space_id ? $space_id : oa_core_get_space_context();
  if ($section_id && ($section = node_load($section_id))) {
    if ($allow_override && oa_notifications_is_overriding('node', $section_id)) {
      return oa_notifications_load_multiple($section);
    }
    else {

      // Switch to the section's space, if available.
      $space_id = empty($section->{OA_SPACE_FIELD}[LANGUAGE_NONE][0]['target_id']) ? $space_id : $section->{OA_SPACE_FIELD}[LANGUAGE_NONE][0]['target_id'];
    }
  }

  // If the section isn't overriding notifications OR section ID wasn't set AND
  // the space ID is set, use the space's notifications instead.
  if ($space_id && ($space = node_load($space_id))) {
    return oa_notifications_load_multiple($space);
  }
  return array();
}
function oa_notification_render_details(&$form, $form_state) {
  $notifications = NULL;
  if (empty($form['#node']->nid) || isset($form[OA_NOTIFY_FORM]['override'])) {
    $notifications = $form_state['storage']['notification_data'];
  }
  $node = isset($form['#node']) ? $form['#node'] : NULL;
  $show_detail = _oa_notifications_session_detail();
  $data = oa_notifications_render_view($node, FALSE, $notifications, !$show_detail);
  return drupal_render($data);
}

/**
 * Node add modal callback.
 */
function oa_notifications_details_callback($form, $form_state) {
  return oa_notification_render_details($form, $form_state);
}

/**
 * Remove notification callback.
 */
function oa_notifications_remove_callback($js = FALSE, $node, $type, $id) {
  oa_notifications_delete_specific($node->nid, $id, $type);
  if ($js) {
    $commands = array();
    $data = oa_notifications_render_view($node, true);
    $commands[] = ajax_command_replace('#notify-data', drupal_render($data));
    ajax_deliver(array(
      '#type' => 'ajax',
      '#commands' => $commands,
    ));
  }
  else {
    drupal_set_message(t('Removed notification'));
    drupal_goto();
  }
}

/**
 * Menu callback for autocomplete results
 * @param $node
 */
function oa_notifications_autocomplete_callback($nid, $string) {
  if ($nid) {
    $node = node_load($nid);
    $space_id = oa_core_get_group_from_node($node, array(
      OA_SPACE_TYPE,
    ));
  }
  else {
    $space_id = oa_core_get_space_context();
  }
  $lower = strtolower($string);
  $matches = array();
  $category = $lower == 'space' || $lower == 'group' || $lower == 'team' || $lower == 'user' || $lower == 'member' ? $lower : '';
  $wildcard = $lower == '%' || $lower == '*' || !empty($category);
  $type = $category == 'group' ? OA_GROUP_TYPE : ($category == 'space' ? OA_SPACE_TYPE : NULL);
  $parents = oa_core_get_parents($space_id, $type, NODE_PUBLISHED, FALSE, TRUE);

  // Include current space.
  if ($category !== 'group') {
    $parents[] = $space_id;
  }

  // Match parent groups and spaces
  if (empty($category) || $category == 'space' || $category == 'group') {
    $query = db_select('node', 'n');
    $query
      ->fields('n', array(
      'nid',
      'title',
      'type',
    ))
      ->condition('n.nid', $parents, 'IN')
      ->addTag('node_access');
    if (!$wildcard) {
      $query
        ->condition('n.title', '%' . db_like($string) . '%', 'LIKE');
    }
    $query
      ->range(0, 100);
    $result = $query
      ->execute();
    foreach ($result as $row) {
      $title = check_plain($row->title) . ' (';
      $title .= $row->type == OA_SPACE_TYPE ? t('Space') : t('Group');
      $title .= ')';
      $matches['group:' . $row->nid] = $title;
    }
  }

  // Next, match teams
  if (empty($category) || $category == 'team') {
    $query = db_select('node', 'n');
    $query
      ->rightJoin('og_membership', 'og', 'n.nid = og.etid');
    $query
      ->fields('n', array(
      'nid',
      'title',
    ))
      ->condition('n.type', OA_TEAM_TYPE)
      ->condition('og.entity_type', 'node')
      ->condition('og.gid', $space_id)
      ->condition('og.state', OG_STATE_ACTIVE, '=')
      ->addTag('node_access');
    if (!$wildcard) {
      $query
        ->condition('n.title', '%' . db_like($string) . '%', 'LIKE');
    }
    $query
      ->range(0, 100);
    $result = $query
      ->execute();
    foreach ($result as $row) {
      $matches['team:' . $row->nid] = check_plain($row->title) . ' (' . t('Team') . ')';
    }
  }

  // Finally, match users in the space
  if (empty($category) || $category == 'user' || $category == 'member') {
    if ($category == 'member' || oa_core_get_group_privacy($space_id)) {
      $query = db_select('og_membership', 'og');
      $query
        ->rightJoin('realname', 'u', 'u.uid = og.etid');
      $query
        ->fields('u', array(
        'uid',
        'realname',
      ))
        ->condition('og.gid', $parents, 'IN')
        ->condition('og.group_type', 'node', '=')
        ->condition('og.entity_type', 'user', '=')
        ->condition('og.state', OG_STATE_ACTIVE, '=');
      if ($lower == 'member') {
        $query
          ->condition('og.gid', $space_id, '=');
      }
      else {
        $query
          ->condition('og.gid', $parents, 'IN');
      }
    }
    else {

      // For public spaces, allow any user to match
      $query = db_select('realname', 'u');
      $query
        ->fields('u', array(
        'uid',
        'realname',
      ));
    }
    if (!$wildcard) {
      $query
        ->condition('u.realname', '%' . db_like($string) . '%', 'LIKE');
    }
    $query
      ->range(0, 100);
    $result = $query
      ->execute();

    // save the query to matches
    foreach ($result as $row) {
      $name = trim($row->realname);
      if (!empty($name)) {
        $matches['user:' . $row->uid] = check_plain($name) . ' (' . t('User') . ')';
      }
    }
  }

  // Return the result to the form in json
  drupal_json_output($matches);
}

/**
 * Returns whether or a node is overriding default notifications or not.
 *
 * @param string $entity_type
 *   Entity type to check
 * @param int $entity_id
 *   Entity ID to check.
 *
 * @return int
 *   Whether or not it's overriding. Default is not overriding.
 */
function oa_notifications_is_overriding($entity_type, $entity_id) {
  $override = db_select('oa_notifications_override', 'oa_no')
    ->fields('oa_no')
    ->condition('entity_type', $entity_type)
    ->condition('entity_id', $entity_id)
    ->execute()
    ->fetch();
  return $override ? $override->override : 0;
}

/**
 * Saves an entity's override settings.
 *
 * @param string $entity_type
 *   The entity type.
 * @param int $entity_id
 *   The entity id.
 * @param mixed $override
 *   Whether or not they're overriding. Bool or int.
 */
function oa_notifications_save_override($entity_type, $entity_id, $override) {
  db_merge('oa_notifications_override')
    ->key(array(
    'entity_type' => $entity_type,
    'entity_id' => $entity_id,
  ))
    ->fields(array(
    'entity_type' => $entity_type,
    'entity_id' => $entity_id,
    'override' => $override ? 1 : 0,
  ))
    ->execute();
}

/**
 * Implements hook_preprocess_flag().
 */
function oa_notifications_preprocess_flag(&$vars) {
  if ($vars['flag']->name == 'subscribe_section_content') {
    if ($vars['status'] == 'unflagged') {
      $vars['link_text'] = '<i class="icon-flag"></i> ' . $vars['link_text'];
    }
    else {
      $vars['link_text'] = '<i class="icon-ban-circle"></i> ' . $vars['link_text'];
    }
    $vars['after_flagging'] = TRUE;
    if ($vars['action'] == 'unflag') {
      $description = t('You are receiving notifications.');
    }
    else {
      $description = t('Follow to receive notifications.');
    }
    $vars['message_text'] = $description;
  }
}

/**
 * Reset any notification flag saved in session.
 */
function oa_notifications_reset() {
  oa_notification_skip(FALSE);
}

/**
 * Return whether notification should be skipped.
 *
 * Notifications can be disabled for the session or for a single page load.
 * Set the session value to disable it for a session (directly) or pass
 * TRUE to this function for just this page load.
 *
 * @param $value
 *   (optional) Set whether notification should be skipped for this page load.
 *
 * @return
 *  TRUE if notifications should skipped for this page load
 *  FALSE if notifications are enabled.
 */
function oa_notification_skip($value = NULL) {
  $skip =& drupal_static(__FUNCTION__, FALSE);
  if (isset($value)) {
    $skip = $value;
    if (drupal_session_started()) {
      if ($value) {
        $_SESSION[SKIP_FLAG] = $value;
      }
      elseif (isset($_SESSION[SKIP_FLAG])) {
        unset($_SESSION[SKIP_FLAG]);
      }
    }
  }
  return $skip || !empty($_SESSION[SKIP_FLAG]);
}
function _oa_notifications_session_detail($value = NULL) {
  $result = false;
  if (isset($_SESSION)) {
    if (!is_null($value)) {
      if (empty($value)) {
        unset($_SESSION[DETAIL_FLAG]);
      }
      else {
        $_SESSION[DETAIL_FLAG] = $value;
      }
    }
    $result = !empty($_SESSION[DETAIL_FLAG]);
  }
  return $result;
}

/**
 * Render the read only version of the notifications listing.
 *
 * @param object $node
 *    The node being viewed/edited
 *
 * @return
 *    A render array of the notification configuration
 */
function oa_notifications_render_view($node, $editmode = FALSE, $notifications = NULL, $show_detail = FALSE) {
  $nid = !empty($node->nid) ? $node->nid : 0;
  $space_id = !empty($nid) ? oa_core_get_group_from_node($node) : oa_core_get_space_context();
  _oa_notifications_session_detail($show_detail);
  $render = array(
    '#theme' => "oa_notifications_view",
    '#node' => $node,
    '#prefix' => '<div id="notify-data">',
    '#suffix' => '</div>',
  );
  if (!isset($notifications)) {
    $notifications = oa_notifications_load_multiple($node);
  }
  $remove_icon = '<i class="fa fa-times"></i>';

  // Turn off ajax when on node/add form (nid = 0).
  // This will be handled in javascript instead.
  $remove_class = $nid ? 'use-ajax' : '';
  if (array_key_exists('group', $notifications)) {
    $groups = node_load_multiple(array_keys($notifications['group']));
    foreach ($groups as $group) {
      $name = $group->title;
      $token = array(
        'tok' => drupal_get_token('remove_group' . $group->nid),
      ) + drupal_get_destination();
      $remove_link = l($remove_icon, 'oa_notifications/nojs/remove/' . $nid . '/group/' . $group->nid, array(
        'attributes' => array(
          'class' => $remove_class,
          'title' => t('Remove ' . $name),
        ),
        'query' => $token,
        'html' => TRUE,
      ));
      $render['#notifications']['group'][] = array(
        'name' => $name,
        'data' => 'group:' . $group->nid,
        'url' => 'node/' . $group->nid,
        'remove_link' => $remove_link,
        'icon' => $group->type == OA_GROUP_TYPE ? 'fa fa-users' : 'fa fa-sitemap',
        'title' => $group->type == OA_GROUP_TYPE ? t('Group: ') . $name : t('Space: ') . $name,
      );
      if ($show_detail) {
        $users = oa_core_get_group_users_for_space($space_id, $group->nid);
        foreach ($users as $user) {
          $name = oa_core_realname($user);
          $render['#notifications']['group'][] = array(
            'name' => $name,
            'url' => 'user/' . $user->uid,
            'picture' => oa_users_picture($user),
            'class' => 'oa-notify-detail',
            'blocked' => _oa_notification_user_blocked($user, $space_id),
          );
        }
      }
    }
  }
  if (array_key_exists('team', $notifications)) {
    $teams = node_load_multiple(array_keys($notifications['team']));
    foreach ($teams as $team) {
      $name = $team->title;
      $token = array(
        'tok' => drupal_get_token('remove_user' . $team->nid),
      ) + drupal_get_destination();
      $remove_link = l($remove_icon, 'oa_notifications/nojs/remove/' . $nid . '/team/' . $team->nid, array(
        'attributes' => array(
          'class' => $remove_class,
          'title' => t('Remove ' . $name),
        ),
        'query' => $token,
        'html' => TRUE,
      ));
      $render['#notifications']['team'][] = array(
        'name' => $name,
        'data' => 'team:' . $team->nid,
        'url' => 'node/' . $team->nid,
        'remove_link' => $remove_link,
        'icon' => 'fa fa-user-plus',
        'title' => t('Team: ') . $name,
      );
      if ($show_detail) {
        $users = oa_teams_get_team_members($team->nid);
        $users = user_load_multiple(array_keys($users));
        foreach ($users as $user) {
          $name = oa_core_realname($user);
          $render['#notifications']['team'][] = array(
            'name' => $name,
            'url' => 'user/' . $user->uid,
            'picture' => oa_users_picture($user),
            'class' => 'oa-notify-detail',
            'blocked' => _oa_notification_user_blocked($user, $space_id),
          );
        }
      }
    }
  }
  if (array_key_exists('user', $notifications)) {
    $users = user_load_multiple(array_keys($notifications['user']));
    foreach ($users as $user) {
      $name = oa_core_realname($user);
      $token = array(
        'tok' => drupal_get_token('remove_user' . $user->uid),
      ) + drupal_get_destination();
      $remove_link = l($remove_icon, 'oa_notifications/nojs/remove/' . $nid . '/user/' . $user->uid, array(
        'attributes' => array(
          'class' => $remove_class,
          'title' => t('Remove ' . $name),
        ),
        'query' => $token,
        'html' => TRUE,
      ));
      $render['#notifications']['user'][] = array(
        'name' => $name,
        'data' => 'user:' . $user->uid,
        'url' => 'user/' . $user->uid,
        'picture' => oa_users_picture($user),
        'access' => $nid ? node_access('view', $node, $user) : TRUE,
        'blocked' => _oa_notification_user_blocked($user, $space_id),
        'remove_link' => $remove_link,
        'title' => t('Member: ') . $name,
      );
    }
  }
  if (!empty($groups) || !empty($teams)) {
    $render['#prefix'] = '<div id="notify-data" class="has-details">';
  }

  // Attach the bootstrap tooltip in case there is an access denied user.
  drupal_add_js('jQuery(document).ready(function () {
    jQuery("span.label-important").tooltip();
  });', 'inline');
  return $render;
}

/**
 * Helper function to return if user is blocking notifications of a space
 * @param $account
 * @param $space_id
 */
function _oa_notification_user_blocked($account, $space_id) {
  $result = FALSE;
  if (isset($account->data['oa_messages']['message_notifications'][$space_id])) {
    $result = empty($account->data['oa_messages']['message_notifications'][$space_id]['methods']);
  }
  return $result;
}

/**
 * Builds a Flags powered subscribe button. Only if user isn't on notifications.
 */
function oa_notifications_subscribe_button($node) {
  return array(
    '#type' => 'markup',
    '#markup' => flag_create_link('subscribe_section_content', $node->nid),
  );
}

/**
 * Builds the show details button.
 */
function oa_notifications_show_details_button() {
  return array(
    '#type' => 'submit',
    '#value' => t('Expand all'),
    '#name' => 'oa_notifications_detail',
    '#ajax' => array(
      'callback' => 'oa_notifications_details_callback',
      'wrapper' => 'notify-data',
      'prevent' => 'submit',
    ),
    '#limit_validation_errors' => array(),
    '#prefix' => '<div class="oa-notify-details">',
    '#suffix' => '</div>',
  );
}

Functions

Namesort descending Description
oa_notifications_add Add a new notification to a node
oa_notifications_ajax_callback AJAX callback saves the quick reply notification configuration.
oa_notifications_autocomplete_callback Menu callback for autocomplete results
oa_notifications_build_list_form
oa_notifications_contextual_tabs_alter Implements hook_contextual_tabs_alter().
oa_notifications_ctools_plugin_directory Implements hook_ctools_plugin_directory().
oa_notifications_delete_for_target Delete notifications for a target.
oa_notifications_delete_specific Delete specific notification
oa_notifications_details_callback Node add modal callback.
oa_notifications_form_fields Define the fields that are used for configuring notifications.
oa_notifications_form_fields_override_ajax AJAX callback for oa_notifications_form_fields override checkbox.
oa_notifications_form_node_form_alter Implements hook_form_node_form_alter().
oa_notifications_form_submit Submit handler for node edit form to save notification values Only executes on node ADD form (new nodes)
oa_notifications_get_default_notifications Gets a piece of section content's default notifications.
oa_notifications_get_notifications Return the resolved list of all users to be notified on a piece of content.
oa_notifications_get_users_for_node Gets the users for a particular node.
oa_notifications_is_notification_type Determine if this is an entity type which we need to provide notifications.
oa_notifications_is_overriding Returns whether or a node is overriding default notifications or not.
oa_notifications_load_multiple Return all notifications for given source. Optionally filtered by type.
oa_notifications_make Create a notification instance
oa_notifications_menu Implements hook_menu().
oa_notifications_node_delete Implements hook_node_delete().
oa_notifications_node_insert Implements hook_node_insert().
oa_notifications_node_update Implements hook_node_update().
oa_notifications_preprocess_flag Implements hook_preprocess_flag().
oa_notifications_process_add Adds any notifications added by javascript
oa_notifications_process_remove Remove any notifications added by javascript
oa_notifications_remove_callback Remove notification callback.
oa_notifications_render_view Render the read only version of the notifications listing.
oa_notifications_reset Reset any notification flag saved in session.
oa_notifications_save_for_source Save a collection of Notifications for a particular source item.
oa_notifications_save_notifications Saves notifications.
oa_notifications_save_override Saves an entity's override settings.
oa_notifications_select2widget_entity_validate_field Sets form value
oa_notifications_show_details_button Builds the show details button.
oa_notifications_skip_ajax_callback Callback for AJAX saving of the skip-notification option.
oa_notifications_subscribe_button Builds a Flags powered subscribe button. Only if user isn't on notifications.
oa_notifications_theme Implements hook_theme().
oa_notification_parse Helper function to parse notifications from the input field value
oa_notification_render_details
oa_notification_skip Return whether notification should be skipped.
_oa_notifications_allow_edit Helper function to determine if notifications can be added to page
_oa_notifications_can_comment Helper function to return true if comment form is enabled for the node
_oa_notifications_session_detail
_oa_notification_show_skip Helper to determine if the skip notification checkbox should be shown
_oa_notification_user_blocked Helper function to return if user is blocking notifications of a space

Constants

Namesort descending Description
DETAIL_FLAG Name of session key for saving "expand all" option
OA_NOTIFY_FORM Name of form element containing notification settings
SKIP_FLAG Name of session key for saving "do not notify" option