You are here

radioactivity.module in Radioactivity 5

File

radioactivity.module
View source
<?php

/*
 * Advanced node popularity by radioactivity model
 */
DEFINE('RADIOACTIVITY_PERM_ADMIN', 'administer radioactivity');
require_once 'radioactivity.inc';
function radioactivity_perm() {
  return array(
    RADIOACTIVITY_PERM_ADMIN,
  );
}
function radioactivity_help($section = '') {
  $output = '';
  switch ($section) {
    case "admin/help#radioactivity":
      $output = '<p>' . t("This module makes nodes radioactive! User activity increases radioactivity of a node " . "while time decays the radioactivity. The radioactivity is halved after a half-life period. The radioactivity is suitable for " . "better node activity metrics. Views support is built-in.") . '</p>';
      break;
  }
  return $output;
}
function radioactivity_menu($may_cache) {
  $items = array();
  if ($may_cache) {
    $items[] = array(
      'path' => 'admin/settings/radioactivity',
      'title' => t('Radioactivity'),
      'description' => t('Configure settings for radioactivity.'),
      'callback' => 'drupal_get_form',
      'callback arguments' => array(
        'radioactivity_admin_general_form',
      ),
      'access' => user_access(RADIOACTIVITY_PERM_ADMIN),
      'type' => MENU_NORMAL_ITEM,
    );
    $items[] = array(
      'path' => 'admin/settings/radioactivity/general',
      'title' => t('General'),
      'description' => t('Configure settings for radioactivity.'),
      'callback' => 'drupal_get_form',
      'callback arguments' => array(
        'radioactivity_admin_general_form',
      ),
      'access' => user_access(RADIOACTIVITY_PERM_ADMIN),
      'weight' => 0,
      'type' => MENU_DEFAULT_LOCAL_TASK,
    );
    $items[] = array(
      'path' => 'admin/settings/radioactivity/list_profiles',
      'title' => t('Decay profiles'),
      'description' => t('List of decay profiles.'),
      'callback' => 'drupal_get_form',
      'callback arguments' => array(
        'radioactivity_admin_profile_list',
      ),
      'access' => user_access(RADIOACTIVITY_PERM_ADMIN),
      'weight' => 1,
      'type' => MENU_LOCAL_TASK,
    );
    $items[] = array(
      'path' => 'admin/settings/radioactivity/profile_new',
      'title' => t('New profile'),
      'description' => t('Add new decay profile.'),
      'callback' => 'drupal_get_form',
      'callback arguments' => array(
        'radioactivity_admin_profile_form',
        0,
      ),
      'access' => user_access(RADIOACTIVITY_PERM_ADMIN),
      'weight' => 2,
      'type' => MENU_LOCAL_TASK,
    );
  }
  else {

    // decay profile edit pages, cannot be cached
    if (arg(0) == 'admin' && arg(1) == 'settings' && arg(2) == 'radioactivity' && substr(arg(3), 0, 8) == 'profile_' && is_numeric(substr(arg(3), 8))) {
      $decay_profile_id = substr(arg(3), 8);
      $decay_profiles = _radioactivity_get_decay_profiles();
      $decay_profile = $decay_profiles[$decay_profile_id];
      $items[] = array(
        'path' => 'admin/settings/radioactivity/profile_' . $decay_profile_id,
        'title' => t('Edit decay profile @label', array(
          '@label' => $decay_profile["label"],
        )),
        'description' => t('Configure settings for decay profile @label.', array(
          '@label' => $decay_profile["label"],
        )),
        'callback' => 'drupal_get_form',
        'callback arguments' => array(
          'radioactivity_admin_profile_form',
          $decay_profile_id,
        ),
        'access' => user_access(RADIOACTIVITY_PERM_ADMIN),
        'weight' => 3,
        'type' => MENU_LOCAL_TASK,
      );
      $items[] = array(
        'path' => 'admin/settings/radioactivity/profile_' . $decay_profile_id . '/delete',
        'title' => t('Delete profile'),
        'callback' => 'drupal_get_form',
        'callback arguments' => array(
          'radioactivity_admin_delete_profile_form',
          $decay_profile_id,
        ),
        'access' => user_access(RADIOACTIVITY_PERM_ADMIN),
        'type' => MENU_CALLBACK,
      );
    }
  }
  return $items;
}
function _radioactivity_set_decay_profiles($decay_profiles) {

  // invalidate views cache (if views-module exists on this site)
  if (function_exists('views_invalidate_cache')) {
    views_invalidate_cache();
  }
  return variable_set("radioactivity_profiles", $decay_profiles);
}
function _radioactivity_get_decay_granularity() {
  $granularity = (int) variable_get('radioactivity_decay_granularity', 0);
  if ($granularity <= 0) {
    $granularity = 600;
  }
  return $granularity;
}
function radioactivity_admin_general_form() {
  $form = array();
  $form['radioactivity_decay_granularity'] = array(
    '#type' => 'textfield',
    '#title' => t('Decay granularity (in seconds)'),
    '#description' => t('This setting determines how often at most the radioactivity is decreased by the decay formula. ' . 'The shorter the time, the more accurate the modeling will be, but the more database ' . 'activity is required. The default (10 minutes) should be good starting point.'),
    '#size' => 10,
    '#required' => TRUE,
    '#default_value' => _radioactivity_get_decay_granularity(),
  );
  $form['memcached'] = array(
    '#type' => 'fieldset',
    '#tree' => FALSE,
    '#title' => t('Memcached acceleration'),
  );
  $memcached_ok = FALSE;
  switch (radioactivity_determine_memcached_availability()) {
    case RADIOACTIVITY_MEMCACHE_OK:
      $memcached_availability_text = t('Ok');
      $memcached_ok = TRUE;
      break;
    case RADIOACTIVITY_MEMCACHE_NO_BIN:
      $memcached_availability_text = t('Cannot obtain memcache bin %bin', array(
        '%bin' => 'radioactivity',
      ));
      break;
    case RADIOACTIVITY_MEMCACHE_NO_MODULE:
      $memcached_availability_text = t('Memcache not enabled');
      break;
  }
  $form['memcached']['availability'] = array(
    '#type' => 'item',
    '#title' => t('Memcached configuration status'),
    '#value' => $memcached_availability_text,
  );
  $form['memcached']['radioactivity_memcached_enable'] = array(
    '#type' => 'checkbox',
    '#title' => t('Memcached acceleration for node views'),
    '#description' => t('If this option is enabled, node views do not update radioactivity energies directly. Instead, ' . 'entry with minimal information is written to memcached. These entries are processed during cron runs.'),
    '#default_value' => radioactivity_get_memcached_enable(),
    '#disabled' => !$memcached_ok,
  );
  $form['memcached']['radioactivity_memcached_expiration'] = array(
    '#type' => 'textfield',
    '#title' => t('Memcached entry expiration time (in seconds)'),
    '#description' => t('Expiration time for memcached entries used by radioactivity. This should be at least twice as long as your maximum ' . 'cron interval.'),
    '#size' => 10,
    '#required' => TRUE,
    '#disabled' => !$memcached_ok,
    '#default_value' => radioactivity_get_memcached_expiration(),
  );
  return system_settings_form($form);
}
function radioactivity_admin_profile_list() {
  $form = array();
  $decay_profiles = _radioactivity_get_decay_profiles();
  $profile_rows = array();
  foreach ($decay_profiles as $dpid => $decay_profile) {
    $profile_rows[] = array(
      'data' => array(
        $dpid,
        check_plain($decay_profile["label"]),
        '<a href="' . url('admin/settings/radioactivity/profile_' . $dpid) . '">' . t("Edit") . '</a>',
      ),
    );
  }
  $profiles_table = theme('table', array(
    t('Id'),
    t('Label'),
    t('Actions'),
  ), $profile_rows);
  $form['profiles_table'] = array(
    '#value' => $profiles_table,
  );
  return $form;
}
function _radioactivity_oclassdef_to_form($oclass, $name, $def, $sources, $energy, $level = 0) {
  $form = array(
    '#type' => 'fieldset',
    '#tree' => TRUE,
    '#collapsible' => TRUE,
    '#title' => t('Energy settings for @oclass', array(
      '@oclass' => $name,
    )),
  );
  $collapsed = TRUE;
  if (count($sources) == 0) {
    $form['no_sources'] = array(
      '#type' => 'item',
      '#value' => t('You must enable at least one plug-in that provides an energy source for this target class.'),
    );
  }
  if ($level == 0 && @is_array($def['subclasses'])) {
    $form[] = array(
      '#type' => 'item',
      '#value' => t('The default settings for #type.', array(
        '#type' => $name,
      )),
    );
  }
  elseif ($level > 0) {
    $form[] = array(
      '#type' => 'item',
      '#value' => t('Specific settings for #type. Empty field uses setting from parent.', array(
        '#type' => $name,
      )),
    );
  }
  foreach ($sources as $source => $sdef) {
    @($energy_value = $energy[$source]);
    $form[$source] = array(
      '#type' => 'textfield',
      '#title' => t('Incident energy from %s', array(
        '%s' => $sdef['title_placeholder'],
      )),
      '#default_value' => $energy_value,
    );
    if (strlen($energy_value) > 0) {
      $collapsed = FALSE;
    }
  }
  if (@is_array($def['subclasses'])) {
    foreach ($def['subclasses'] as $subclass => $subclassdef) {
      $form['subclasses'][$subclass] = _radioactivity_oclassdef_to_form($subclass, $name . ' / ' . $subclass, $subclassdef, $sources, @$energy['subclasses'][$subclass], $level + 1);
      if (!$form['subclasses'][$subclass]['#collapsed']) {
        $collapsed = FALSE;
      }
    }
  }
  $form['#collapsed'] = $collapsed;
  return $form;
}
function radioactivity_admin_profile_form($dpid) {
  $form = array();
  if (!(int) $dpid) {
    $dpid = -1;
  }
  $form[] = array(
    '#type' => 'item',
    '#title' => t('Profile id'),
    '#value' => $dpid > 0 ? $dpid : t('Unassigned'),
  );
  $form['decay_profile_id'] = array(
    '#type' => 'hidden',
    '#value' => $dpid,
  );
  if ($dpid > 0) {
    $decay_profiles = _radioactivity_get_decay_profiles();
    $decay_profile = $decay_profiles[$dpid];
    unset($decay_profiles);
  }
  else {

    // defaults for new
    $decay_profile = array(
      'half_life' => 6 * 3600,
      'cut_off_energy' => 0.5,
      'energy' => array(
        'node' => array(
          'view' => 1,
        ),
      ),
      'label' => '',
      'description' => '',
    );
  }
  $form['label'] = array(
    '#type' => 'textfield',
    '#title' => t('Profile label'),
    '#required' => TRUE,
    '#description' => t('The profile label. Used in views, links, etc'),
    '#default_value' => $decay_profile['label'],
  );
  $form['description'] = array(
    '#type' => 'textarea',
    '#title' => t('Description'),
    '#description' => t('The description of the profile.'),
    '#default_value' => $decay_profile['description'],
  );
  $form['half_life'] = array(
    '#type' => 'textfield',
    '#title' => t('Half-life of the radioactivity in seconds'),
    '#required' => TRUE,
    '#description' => t('Determines the decay rate of the radioactivity. For exaple, if the decay rate is ' . '3600 (one hour), the radioactivity halves once an hour. If it is now 1000, it will ' . 'be 500 after an hour, 250 after two hours, and so on. The default is 6 hours.'),
    '#default_value' => $decay_profile['half_life'],
  );
  $form['cut_off_energy'] = array(
    '#type' => 'textfield',
    '#title' => t('Cut-off energy'),
    '#required' => TRUE,
    '#description' => t('The cut-off energy. Below this energy level, the node is considered non-radioactive and ' . 'the radioactivity information will be deleted from the database. Leave 0 disable cut-off.'),
    '#default_value' => $decay_profile['cut_off_energy'],
  );
  $radioactivity_info = radioactivity_get_radioactivity_info();

  //  $form['debug']=
  //    array('#value' => print_r($radioactivity_info, TRUE));
  $form['energy'] = array(
    '#type' => 'fieldset',
    '#tree' => TRUE,
    '#title' => t('Energy settings'),
  );
  if (count($radioactivity_info["targets"]) == 0) {

    // no energy target classes
    $form['energy']['no_targets'] = array(
      '#type' => 'item',
      '#value' => t('You must enable at least one plug-in that provides an energy target class. ' . 'Try <em>radioactivity_node</em>.'),
    );
  }
  else {
    foreach ($radioactivity_info['targets'] as $oclass => $def) {
      $form['energy'][$oclass] = _radioactivity_oclassdef_to_form($oclass, $oclass, $def, $radioactivity_info['sources'][$oclass], @$decay_profile['energy'][$oclass]);
      $form['energy'][$oclass]['#collapsed'] = FALSE;
    }
  }
  $form['buttons']['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Save decay profile'),
  );
  $form['buttons']['delete'] = array(
    '#value' => l(t('Delete profile'), 'admin/settings/radioactivity/profile_' . $dpid . '/delete'),
  );
  if (!empty($_POST) && form_get_errors()) {
    drupal_set_message(t('The settings have not been saved because of the errors.'), 'error');
  }
  return $form;
}

// removes empty subclass leafs
function _radioactivity_prune_array($energy) {
  $ret = array();
  foreach ($energy as $key => $value) {
    if (is_array($value)) {
      $value = _radioactivity_prune_array($value);
      if (count($value)) {
        $ret[$key] = $value;
      }
    }
    elseif (strlen(trim($value))) {
      $ret[$key] = trim($value);
    }
  }
  return $ret;
}
function radioactivity_admin_profile_form_submit($form_id, $form) {
  $dpid = (int) $form['decay_profile_id'];
  if ($dpid == 0) {

    // internal error
    drupal_set_message(t('Internal error: decay_profile_id=@dpid', array(
      '@dpid' => $dpid,
    )), 'error');
    return FALSE;
  }
  $decay_profiles = _radioactivity_get_decay_profiles();
  if ($dpid < 0) {

    // get new $dpid
    $dpids = array_keys($decay_profiles);
    if (count($dpids) > 0) {
      $dpid = 1 + max($dpids);
    }
    else {
      $dpid = 1;
    }
  }

  // build profile array
  $decay_profile = array();
  $decay_profile['label'] = $form['label'];
  $decay_profile['description'] = $form['description'];
  $half_life = (int) $form['half_life'];
  if ($half_life <= 0) {
    $half_life = 6 * 3600;
  }
  $decay_profile['half_life'] = $half_life;
  $decay_profile['cut_off_energy'] = (double) $form['cut_off_energy'];
  $decay_profile['energy'] = _radioactivity_prune_array($form['energy']);
  $decay_profiles[$dpid] = $decay_profile;
  _radioactivity_set_decay_profiles($decay_profiles);
  drupal_set_message(t('Profile @dpid saved.', array(
    '@dpid' => $dpid,
  )));
}
function radioactivity_admin_delete_profile_form($dpid) {
  $decay_profiles = _radioactivity_get_decay_profiles();
  return confirm_form(array(
    'decay_profile_id' => array(
      '#type' => 'hidden',
      '#value' => $dpid,
    ),
  ), t('Are you sure you want to delete radiation decay profile @label (@id)?', array(
    '@label' => $decay_profiles[$dpid]['label'],
    '@id' => $dpid,
  )), "admin/settings/radioactivity/profile_" . $dpid, NULL, t('Delete'));
}
function radioactivity_admin_delete_profile_form_submit($form_id, $form) {
  $dpid = $form['decay_profile_id'];
  drupal_set_message(t("Deleted profile @id", array(
    '@id' => $dpid,
  )));
  $decay_profiles = _radioactivity_get_decay_profiles();
  unset($decay_profiles[$dpid]);
  _radioactivity_set_decay_profiles($decay_profiles);
  db_query("DELETE FROM {radioactivity} WHERE decay_profile=%d", $dpid);
  drupal_goto("admin/settings/radioactivity");
}
function radioactivity_process_memcached_entries() {
  $combined = array();

  // get newest memcache entry
  $entry_id_top = (int) dmemcache_get('entry_id_seq', 'radioactivity');
  $entry_id = dmemcache_get('entry_id_processed', 'radioactivity');
  if ($entry_id === FALSE) {
    $entry_id = $entry_id_top;
  }

  // check if entry_id_top has gotten flushed
  if ($entry_id_top < $entry_id) {
    $entry_id = 0;
  }
  while ($entry_id < $entry_id_top) {
    ++$entry_id;
    $entry = dmemcache_get('entry-' . $entry_id, 'radioactivity');
    if (!$entry) {
      continue;
    }

    // probably expired, try next
    switch ($entry['type']) {
      case 'add-energy':
        ++$combined[$entry['oid']][$entry['oclass']][$entry['source']];
        break;
      default:
    }
  }

  // execute combined
  foreach ($combined as $oid => $rest1) {
    foreach ($rest1 as $oclass => $rest2) {
      foreach ($rest2 as $source => $times) {
        _radioactivity_add_energy_internal($oid, $oclass, $source, $times);
      }
    }
  }
  dmemcache_set('entry_id_processed', $entry_id, 0, 'radioactivity');
}
function radioactivity_cron() {
  if (radioactivity_get_memcached_enable()) {
    radioactivity_process_memcached_entries();
  }
  $timestamp = time();

  // last cron
  $last_cron_timestamp = (int) variable_get('radioactivity_last_cron_timestamp', 0);
  $granularity = (int) _radioactivity_get_decay_granularity();
  $threshold_timestamp = $last_cron_timestamp - $last_cron_timestamp % $granularity + $granularity;
  if ($timestamp < $threshold_timestamp) {
    return;
  }

  // don't update yet
  foreach (_radioactivity_get_decay_profiles() as $dpid => $decay_profile) {
    $half_life = (double) $decay_profile["half_life"];
    $cut_off_energy = (double) $decay_profile["cut_off_energy"];

    // the formula is:
    // E=E_0 * 2^(- delta_time / half_life)
    db_query("UPDATE {radioactivity} SET energy=energy * pow(2, (last_emission_timestamp-%d)*1.0/%f), last_emission_timestamp=%d " . "WHERE decay_profile=%d AND last_emission_timestamp<%d", $timestamp, $half_life, $timestamp, $dpid, $timestamp);

    // delete clean (l. non-radioactive) nodes
    db_query("DELETE FROM {radioactivity} WHERE decay_profile=%d AND energy < %f", $dpid, $cut_off_energy);
  }
  variable_set('radioactivity_last_cron_timestamp', $timestamp);
}

/**
 * Reads energies for a node. Returns array of $dpid => $energy
 */
function radioactivity_get_energy($oid, $oclass) {
  $ret = array();

  // remap id if necessary
  $oid = _radioactivity_possibly_remap_id($oid, $oclass);
  $result = db_query("SELECT decay_profile, energy FROM {radioactivity} WHERE id=%d AND class='%s'", $oid, $oclass);
  while ($row = db_fetch_object($result)) {
    $ret[$row->decay_profile] = $row->energy;
  }
  return $ret;
}
function radioactivity_delete_energy($oid, $oclass) {

  // remap id if necessary
  $oid = _radioactivity_possibly_remap_id($oid, $oclass);
  db_query("DELETE FROM {radioactivity} WHERE id=%d AND class='%s'", $oid, $oclass);
  return TRUE;
}
function radioactivity_get_radioactivity_array($oid, $oclass) {
  $ret = array();
  $energies = radioactivity_get_energy($oid, $oclass);
  foreach ($energies as $dpid => $energy) {
    $ret['energy'][$dpid] = (double) $energies[$dpid];
  }
  return $ret;
}