You are here

recommender.module in Recommender API 7.6

File

recommender.module
View source
<?php

require_once 'classes/Matrix.php';
require_once 'classes/Recommender.php';
define('RECOMMENDER_ADMIN_PATH', COMPUTING_MODULE_ADMIN_PATH . '/recommender');

/**
 * Implements hook_help().
 */
function recommender_help($path, $args) {
  $output = '';
  switch ($path) {
    case 'admin/help#recommender':
    case RECOMMENDER_ADMIN_PATH:
      $output = '<p>' . t("Please install Recommender API enabled sub-modules to make content recommendations.") . '</p>';
      break;
  }
  return $output;
}

/**
 * Implements hook_menu().
 */
function recommender_menu() {
  $items = array();
  $items[RECOMMENDER_ADMIN_PATH] = array(
    'type' => MENU_LOCAL_TASK,
    'title' => 'Recommender API',
    'description' => t('The admin interface to interact with Recommender API module and sub-modules.'),
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'recommender_overview',
    ),
    'access arguments' => array(
      'administer recommender',
    ),
  );
  $items[RECOMMENDER_ADMIN_PATH . '/overview'] = array(
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'title' => 'Overview',
    'weight' => -10,
  );
  return $items;
}
function recommender_overview($form, &$form_state) {
  $form['info'] = array(
    '#markup' => '<p>' . t("After you configure any settings of Recommender API modules, please go to !link to submit a Recommender command. And then use either 'drush recommender-run' or the Java agent to compute recommendations offline.", array(
      '!link' => l('Command', COMPUTING_MODULE_ADMIN_PATH . '/list'),
    )) . '</p>',
  );
  $form['recommender_cron_strategy'] = array(
    '#type' => 'radios',
    '#title' => t('How to run cron tasks?'),
    '#options' => array(
      'drupal' => t('Together with Drupal Cron'),
      'drush' => t('Separately with Drush'),
    ),
    '#default_value' => variable_get('recommender_cron_strategy', 'drupal'),
  );
  $form['recommender_cron_strategy']['drupal']['#description'] = t('Run recommender cron tasks whenever Drupal Cron runs.');
  $form['recommender_cron_strategy']['drush']['#description'] = t('Setup to run "drush recommender-cron" separately instead of running recommender cron with Drupal Cron.');
  $form['warning'] = array(
    '#markup' => '<p><em>' . t("WARNING: Do not use the 'Run Recommender' button below unless your dataset is small (such as the dataset in Recommender Example). Otherwise you might see PHP 'Out of Memory' error or 'Timeout' error.") . '</em></p>',
  );

  // append to 'actions' for system_settings_form().
  $form['actions'] = array(
    'run' => array(
      '#type' => 'submit',
      '#name' => 'run',
      '#value' => t('Run Recommender'),
    ),
  );
  $form['#submit'][] = 'recommender_overview_submit';
  return system_settings_form($form);
}
function recommender_overview_submit($form, &$form_state) {
  if ($form_state['triggering_element']['#name'] == 'run') {
    recommender_run();
  }
}
function recommender_run() {
  $count = 0;
  while ($record = computing_claim('recommender')) {
    drupal_set_message(t("Processing recommender computation: !id", array(
      '!id' => $record->id,
    )));
    recommender_process_record($record);
    computing_update($record);
    $count++;
  }
  if ($count > 0) {
    drupal_set_message(t('Successfully run recommender.'));
  }
  else {
    drupal_set_message(t('No recommender command to run.'), 'warning');
  }
}

/**
 * Implements hook_computing_data().
 * Expose recommender commands to computing.module.
 */
function recommender_computing_data() {
  $computing_data = array(
    'recommender' => recommender_fetch_data(),
  );
  foreach ($computing_data['recommender'] as $name => &$data) {
    if (!isset($data['form callback'])) {
      $data['form callback'] = 'recommender_command_form';
    }
    $data['command'] = $data['algorithm'];
  }
  return $computing_data;
}
function recommender_command_form($form, &$form_state, $data) {

  // checking data should be deferred to sub modules to provide detailed info.
  //  if (@$data['data structure']['preference']['type'] == 'table') {
  //    $preference_table = @$data['data structure']['preference']['name'];
  //    $count = @db_query("SELECT COUNT(*) FROM $preference_table")->fetchField();
  //    if ($count < 10000) {
  //      drupal_set_message(t('Not enough preference data to compute recommendations. Refer to the module\'s documentation for more details.'), 'warning');
  //    }
  //  }
  // 1. process $form data. make them into $form['input']
  $form['options'] = array(
    '#type' => 'fieldset',
    '#title' => t('Configure additional parameters for %command', array(
      '%command' => $data['title'],
    )),
    //'#weight' => 5,
    '#collapsible' => FALSE,
    '#collapsed' => FALSE,
    '#tree' => TRUE,
  );
  if (isset($data['form elements callback']) && function_exists($data['form elements callback'])) {
    $form['options'] = $form['options'] + call_user_func($data['form elements callback']);
  }
  else {
    $form['options']['no_options_message'] = array(
      '#markup' => t('No extra parameters defined.'),
    );
  }

  // 2. add other command data.
  $form['common'] = array(
    '#type' => 'fieldset',
    '#title' => t('Configure generic parameters'),
    //'#weight' => 5,
    '#collapsible' => TRUE,
    '#collapsed' => FALSE,
    '#tree' => TRUE,
  );
  $form['common']['data'] = array(
    '#type' => 'value',
    '#value' => $data,
  );
  $form['common']['label'] = array(
    '#type' => 'textfield',
    '#title' => 'Label',
    '#description' => 'The human-readable name of this command.',
    '#default_value' => $data['title'],
    '#required' => TRUE,
  );
  if (variable_get('recommender_show_database_option', FALSE)) {
    $form['common']['database'] = array(
      '#type' => 'select',
      '#title' => t('Include database access info?'),
      '#description' => t('Specifies whether to add database access info defined in settings.php into the computing record. Use this option only for development purpose when you use a remote agent to compute recommendations. Remove this option from admin interface by setting "recommender_show_database_option" to FALSE. See more in documentations.'),
      '#options' => _recommender_database_options(),
      '#empty_value' => '',
      '#required' => FALSE,
    );
  }

  //  $form['common']['weight'] = array(
  //    '#type' => 'textfield',
  //    '#title' => 'Weight',
  //    '#description' => 'Command execution weight. Low number has higher priority.',
  //    '#default_value' => 0,
  //    '#element_validation' => 'element_validate_integer',
  //    '#required' => TRUE,
  //  );
  // 3. add code from system_settings_form().
  $form['actions'] = array(
    '#type' => 'actions',
    'submit' => array(
      '#type' => 'submit',
      '#value' => t('Add Command'),
    ),
  );
  return $form;
}
function recommender_command_form_submit($form, &$form_state) {
  $data = $form_state['values']['common']['data'];
  $default_options = isset($data['options']) ? $data['options'] : array();
  $input = array(
    'algorithm' => $data['algorithm'],
    'data structure' => recommender_prepare_data_structure($data['data structure']),
    'options' => $default_options + (isset($form_state['values']['options']) ? $form_state['values']['options'] : array()),
  );
  if (variable_get('recommender_show_database_option', FALSE) && !empty($form_state['values']['common']['database'])) {
    global $databases;
    list($db_name, $db_target) = explode('-', $form_state['values']['common']['database']);
    $input['database'] = $databases[$db_name][$db_target];
  }
  $command = isset($data['command']) ? $data['command'] : $data['algorithm'];
  $result = computing_create($data['application'], $command, $form_state['values']['common']['label'], $input);
  if ($result) {
    drupal_set_message(t('Adding new recommender command successfully. Please run "drush recommender-run" or use Java agent program to execute the command.'));
  }
  else {
    drupal_set_message(t('Cannot add new recommender command. Please inform site administrators.'), 'error');
  }
  $form_state['redirect'] = COMPUTING_MODULE_ADMIN_PATH;
}
function _recommender_database_options() {
  global $databases;
  $options = array();
  foreach ($databases as $name => $target_data) {
    foreach ($target_data as $target => $def) {
      $options["{$name}-{$target}"] = "{$name} - {$target}";
    }
  }
  return $options;
}

/**
 * Fill in the missing 'data structure' data from hook_recommender_data().
 *
 * @param $data_structure_override
 * @return mixed
 */
function recommender_prepare_data_structure($data_structure_override) {
  $default_data_structure = array(
    'preference' => array(
      'name' => 'recommender_preference',
      'user field' => 'uid',
      'item field' => 'eid',
      'score field' => 'score',
      'score type' => 'number',
      // or could be 'boolean'
      'timestamp field' => 'updated',
    ),
    'user similarity' => array(
      'name' => 'recommender_user_similarity',
      'user1 field' => 'uid1',
      'user2 field' => 'uid2',
      'score field' => 'score',
      'timestamp field' => 'updated',
    ),
    'item similarity' => array(
      'name' => 'recommender_item_similarity',
      'item1 field' => 'eid1',
      'item2 field' => 'eid2',
      'score field' => 'score',
      'timestamp field' => 'updated',
    ),
    'prediction' => array(
      'name' => 'recommender_prediction',
      'user field' => 'uid',
      'item field' => 'eid',
      'score field' => 'score',
      'timestamp field' => 'updated',
    ),
    'item entity type' => 'node',
    'user entity type' => 'user',
  );

  // note: here we add all the 4 tables even though they are not defined in $data_structure_override.
  return drupal_array_merge_deep($default_data_structure, $data_structure_override);
}

/**
 * This function process the command, and saves result back to $record. Will not save record.
 *
 * @param $record
 * @return mixed
 */
function recommender_process_record($record) {
  $mapping = array(
    'user2user' => 'UserBasedRecommender',
    'user2user_boolean' => 'UserBasedBooleanRecommender',
    'item2item' => 'ItemBasedRecommender',
    'item2item_boolean' => 'ItemBasedBooleanRecommender',
  );
  if (!array_key_exists($record->command, $mapping)) {
    $record->status = 'FLD';
    $record->message = 'Cannot identify recommender command';
    $record->output = NULL;
    return;
  }
  $recommender_class = $mapping[$record->command];
  $recommender = new $recommender_class();

  // compute, could be time-consuming.
  try {
    $recommender
      ->initialize($record->input);
    $recommender
      ->execute();
    $result = $recommender
      ->finalize();
  } catch (Exception $e) {
    $record->status = 'FLD';
    $record->message = 'Unexpected exception caught: ' . $e
      ->getMessage();
    $record->output = NULL;
    return;
  }
  $record->status = 'SCF';
  $record->message = 'Successfully computed recommendations.';
  $record->output = $result;
}

/**
 * Implements hook_permission().
 */
function recommender_permission() {
  $permissions = array(
    'administer recommender' => array(
      'title' => t('Administer recommender'),
      'description' => t('Administer Recommender API related modules'),
    ),
  );
  return $permissions;
}

/**
 * Implements hook_views_api().
 */
function recommender_views_api() {
  return array(
    'api' => 3,
    'path' => drupal_get_path('module', 'recommender'),
  );
}
function recommender_fetch_data($name = NULL) {
  $recommender_data = module_invoke_all('recommender_data');
  drupal_alter('recommender_data', $recommender_data);
  if ($name != NULL) {
    if (isset($recommender_data[$name]) && is_array($recommender_data[$name])) {
      $recommender_definition = $recommender_data[$name];

      // perhaps do other default filling.
      return $recommender_definition;
    }
    return FALSE;
  }
  else {
    return $recommender_data;
  }
}

/**
 * Implements hook_cron().
 */
function recommender_cron() {
  if (variable_get('recommender_cron_strategy', 'drupal') == 'drupal') {
    $queue = DrupalQueue::get('recommender_cron_process');
    $queue
      ->createItem(time());
  }
}

/**
 * Implements hook_cron_queue_info().
 */
function recommender_cron_queue_info() {
  return array(
    'recommender_cron_process' => array(
      'worker callback' => 'recommender_cron_process',
      'time' => 600,
    ),
  );
}
function _recommender_process_periodic_callback($timestamp, $hook_callback) {
  $data = recommender_fetch_data();
  $callbacks = array();

  // add these to remove duplicates.
  foreach ($data as $rec => $def) {
    if (@$def[$hook_callback] && function_exists($def[$hook_callback])) {
      $callbacks[$def[$hook_callback]] = $def[$hook_callback];
    }
  }

  // shuffle them
  shuffle($callbacks);
  foreach ($callbacks as $callback) {
    $callback($timestamp);
  }
}
function recommender_cron_process($timestamp) {
  _recommender_process_periodic_callback($timestamp, 'cron callback');
}
function recommender_upkeep_process($timestamp) {
  _recommender_process_periodic_callback($timestamp, 'upkeep callback');
}