| 1 |
<?php
|
| 2 |
/*
|
| 3 |
Copyright (C) 2008 by Phase2 Technology.
|
| 4 |
Author(s): Frank Febbraro
|
| 5 |
|
| 6 |
This program is free software; you can redistribute it and/or modify
|
| 7 |
it under the terms of the GNU General Public License.
|
| 8 |
This program is distributed in the hope that it will be useful,
|
| 9 |
but WITHOUT ANY WARRANTY. See the LICENSE.txt file for more details.
|
| 10 |
|
| 11 |
$Id$
|
| 12 |
*/
|
| 13 |
/**
|
| 14 |
* @file morelikethis.module
|
| 15 |
* TODO: - Caching for performance improvements of internal search.
|
| 16 |
* - Add pluggable more like this mechanism that supports:
|
| 17 |
* - the current taxonomy search
|
| 18 |
* - an internal calais search
|
| 19 |
* - yahoo boss search
|
| 20 |
* - internal drupal search
|
| 21 |
* - apache solr search
|
| 22 |
* - any other mechanism that could be imagined
|
| 23 |
*/
|
| 24 |
|
| 25 |
/**
|
| 26 |
* Implementation of hook_perm().
|
| 27 |
*/
|
| 28 |
function morelikethis_perm() {
|
| 29 |
return array('administer morelikethis');
|
| 30 |
}
|
| 31 |
|
| 32 |
/**
|
| 33 |
* Implementation of hook_theme().
|
| 34 |
*/
|
| 35 |
function morelikethis_theme() {
|
| 36 |
return array(
|
| 37 |
'morelikethis_block' => array(
|
| 38 |
'arguments' => array('items' => NULL),
|
| 39 |
),
|
| 40 |
'morelikethis_item' => array(
|
| 41 |
'arguments' => array('item' => NULL),
|
| 42 |
),
|
| 43 |
);
|
| 44 |
}
|
| 45 |
|
| 46 |
/**
|
| 47 |
* Implementation of hook_menu().
|
| 48 |
*/
|
| 49 |
function morelikethis_menu() {
|
| 50 |
|
| 51 |
$items['admin/settings/morelikethis'] = array(
|
| 52 |
'title' => 'More Like This settings',
|
| 53 |
'description' => 'Configuration for More Like This.',
|
| 54 |
'page callback' => 'drupal_get_form',
|
| 55 |
'page arguments' => array('morelikethis_settings_form'),
|
| 56 |
'access arguments' => array('administer morelikethis'),
|
| 57 |
'file' => 'morelikethis.admin.inc',
|
| 58 |
);
|
| 59 |
|
| 60 |
return $items;
|
| 61 |
}
|
| 62 |
|
| 63 |
/**
|
| 64 |
* Implementation of hook_block().
|
| 65 |
*/
|
| 66 |
function morelikethis_block($op = 'list', $delta = 0, $edit = array()) {
|
| 67 |
|
| 68 |
switch ($op) {
|
| 69 |
case 'list':
|
| 70 |
return _mlt_block_list();
|
| 71 |
break;
|
| 72 |
case 'view':
|
| 73 |
return _mlt_block_view($delta);
|
| 74 |
break;
|
| 75 |
}
|
| 76 |
}
|
| 77 |
|
| 78 |
/**
|
| 79 |
* Provide block listing.
|
| 80 |
*/
|
| 81 |
function _mlt_block_list(){
|
| 82 |
$blocks = array();
|
| 83 |
$blocks[0] = array(
|
| 84 |
'info' => t('More Like This Block'),
|
| 85 |
);
|
| 86 |
return $blocks;
|
| 87 |
}
|
| 88 |
|
| 89 |
/**
|
| 90 |
* Display More Like This block.
|
| 91 |
*/
|
| 92 |
function _mlt_block_view($delta){
|
| 93 |
switch ($delta) {
|
| 94 |
case 0:
|
| 95 |
if(arg(0) == 'node' && is_numeric(arg(1))) {
|
| 96 |
$block['subject'] = t('More Like This');
|
| 97 |
$more = morelikethis_find(arg(1));
|
| 98 |
$block['content'] = theme('morelikethis_block', $more);
|
| 99 |
}
|
| 100 |
break;
|
| 101 |
}
|
| 102 |
return $block;
|
| 103 |
}
|
| 104 |
|
| 105 |
/**
|
| 106 |
* Load term data for a node
|
| 107 |
*/
|
| 108 |
function morelikethis_load($vid) {
|
| 109 |
$result = db_query("SELECT term FROM {morelikethis} WHERE vid = %d", $vid);
|
| 110 |
$terms = array();
|
| 111 |
while($term = db_result($result)) {
|
| 112 |
$terms[] = $term;
|
| 113 |
}
|
| 114 |
return $terms;
|
| 115 |
}
|
| 116 |
|
| 117 |
|
| 118 |
/**
|
| 119 |
* Save the data used to render the morelikethis terms.
|
| 120 |
*/
|
| 121 |
function morelikethis_save($node, $data) {
|
| 122 |
$terms = drupal_explode_tags($data['terms']);
|
| 123 |
foreach($terms as $term) {
|
| 124 |
db_query("INSERT INTO {morelikethis} (nid, vid, term) VALUES (%d, %d, '%s')", $node->nid, $node->vid, $term);
|
| 125 |
}
|
| 126 |
}
|
| 127 |
|
| 128 |
/**
|
| 129 |
* Attempt to find internal nodes that are like the provided node. Use the list of terms
|
| 130 |
* provided in the morelikethis DNA to search the taxonomy for other nodes matching these
|
| 131 |
* terms. This is the guts behind the internal More Like This search.
|
| 132 |
*
|
| 133 |
* @param nid
|
| 134 |
* The node id to find related content
|
| 135 |
*/
|
| 136 |
function morelikethis_find($nid) {
|
| 137 |
$node = node_load($nid);
|
| 138 |
$key = drupal_strtolower($node->type);
|
| 139 |
$types = variable_get("morelikethis_target_types_{$key}", FALSE);
|
| 140 |
|
| 141 |
if(empty($types))
|
| 142 |
return FALSE;
|
| 143 |
|
| 144 |
$terms = morelikethis_load($node->vid);
|
| 145 |
$term_count = count($terms);
|
| 146 |
|
| 147 |
if($term_count == 0)
|
| 148 |
return FALSE;
|
| 149 |
|
| 150 |
$types = array_keys($types);
|
| 151 |
$count = intval(variable_get("morelikethis_count_{$key}", 10));
|
| 152 |
$threshold = floatval(variable_get("morelikethis_threshold_{$key}", 0.0));
|
| 153 |
|
| 154 |
$sql = "SELECT n.nid, n.title, count(*) as hits, count(*)/$term_count as relevance";
|
| 155 |
$sql .= " FROM {node} n";
|
| 156 |
$sql .= " JOIN {term_node} tn ON n.nid = tn.nid";
|
| 157 |
$sql .= " JOIN {term_data} td ON tn.tid = td.tid";
|
| 158 |
$sql .= " WHERE n.nid <> %d";
|
| 159 |
$sql .= " AND n.type IN (" . db_placeholders($types, 'varchar') . ")";
|
| 160 |
$sql .= " AND td.name IN (" . db_placeholders($terms, 'varchar') . ")";
|
| 161 |
$sql .= " GROUP BY n.nid";
|
| 162 |
$sql .= " HAVING relevance >= %f";
|
| 163 |
$sql .= " ORDER BY hits DESC, n.nid DESC";
|
| 164 |
$args = array_merge(array($node->nid), $types, $terms, array($threshold));
|
| 165 |
|
| 166 |
$result = db_query_range($sql, $args, 0, $count);
|
| 167 |
|
| 168 |
$likeness = array();
|
| 169 |
while($likenode = db_fetch_object($result)) {
|
| 170 |
$likeness[] = $likenode;
|
| 171 |
}
|
| 172 |
return $likeness;
|
| 173 |
}
|
| 174 |
|
| 175 |
/**
|
| 176 |
* Implementation of hook_form_alter().
|
| 177 |
*
|
| 178 |
* Add the More Like This fields to the node edit form.
|
| 179 |
*/
|
| 180 |
function morelikethis_form_alter(&$form, $form_state, $form_id) {
|
| 181 |
|
| 182 |
if (isset($form['type']) && isset($form['#node']) && $form['type']['#value'] .'_node_form' == $form_id) {
|
| 183 |
$node = $form['#node'];
|
| 184 |
$key = drupal_strtolower($node->type);
|
| 185 |
|
| 186 |
if(!variable_get("morelikethis_enabled_{$key}", FALSE))
|
| 187 |
return;
|
| 188 |
|
| 189 |
if($node->taxonomy) {
|
| 190 |
$options = array();
|
| 191 |
foreach ($node->taxonomy as $term) {
|
| 192 |
$vocab = taxonomy_vocabulary_load($term->vid);
|
| 193 |
$key = $vocab->name;
|
| 194 |
|
| 195 |
if(!array_key_exists($key, $options)) {
|
| 196 |
$options[$key] = array();
|
| 197 |
}
|
| 198 |
// Remove quotes surrounding terms
|
| 199 |
$term_name = preg_replace('/^"(.*)"$/', '\1', $term->name);
|
| 200 |
$options[$key][$term_name] = $term_name;
|
| 201 |
}
|
| 202 |
}
|
| 203 |
|
| 204 |
$terms = drupal_implode_tags(morelikethis_load($node->vid));
|
| 205 |
|
| 206 |
$form['morelikethis'] = array(
|
| 207 |
'#type' => 'fieldset',
|
| 208 |
'#title' => t('More Like This'),
|
| 209 |
'#tree' => TRUE,
|
| 210 |
'#collapsible' => TRUE,
|
| 211 |
'#collapsed' => FALSE,
|
| 212 |
);
|
| 213 |
|
| 214 |
$form['morelikethis']['terms'] = array(
|
| 215 |
'#type' => 'textfield',
|
| 216 |
'#title' => t('More Like This Terms'),
|
| 217 |
'#description' => t('A comma-separated list of terms that will be used as the "identity" of this content item to find more content that is like this. You can select existing taxonomy terms form the list below, or enter in your own manually. Example: election, stock market, "Company, Inc.".'),
|
| 218 |
'#maxlength' => 255,
|
| 219 |
'#default_value' => $terms,
|
| 220 |
);
|
| 221 |
|
| 222 |
if(!empty($options)) {
|
| 223 |
$form['morelikethis']['taxonomy-terms'] = array(
|
| 224 |
'#type' => 'select',
|
| 225 |
'#title' => t('Choose existing terms'),
|
| 226 |
'#description' => t('Selecting terms will put them in the above textfield.'),
|
| 227 |
'#size' => 5,
|
| 228 |
'#options' => $options,
|
| 229 |
);
|
| 230 |
}
|
| 231 |
|
| 232 |
$path = drupal_get_path('module', 'morelikethis');
|
| 233 |
drupal_add_js("$path/morelikethis.js", 'module');
|
| 234 |
}
|
| 235 |
}
|
| 236 |
|
| 237 |
/**
|
| 238 |
* Implementation of hook_nodeapi().
|
| 239 |
*/
|
| 240 |
function morelikethis_nodeapi(&$node, $op) {
|
| 241 |
switch ($op) {
|
| 242 |
case 'insert':
|
| 243 |
case 'update':
|
| 244 |
if(property_exists($node, 'morelikethis')) {
|
| 245 |
morelikethis_save($node, $node->morelikethis);
|
| 246 |
}
|
| 247 |
break;
|
| 248 |
case 'delete':
|
| 249 |
db_query('DELETE FROM {morelikethis} WHERE nid = %d', $node->nid);
|
| 250 |
break;
|
| 251 |
case 'view':
|
| 252 |
$more = morelikethis_find($node->nid);
|
| 253 |
if($more) {
|
| 254 |
_morelikethis_build_node_property($node, $more);
|
| 255 |
}
|
| 256 |
break;
|
| 257 |
}
|
| 258 |
}
|
| 259 |
|
| 260 |
/**
|
| 261 |
* Builds the morelikethis property for a node.
|
| 262 |
*
|
| 263 |
* @param $node
|
| 264 |
* The node to add a property that contains More Like This data.
|
| 265 |
* @param $more
|
| 266 |
* An array of More Like This data for the provided node.
|
| 267 |
*/
|
| 268 |
function _morelikethis_build_node_property(&$node, $more) {
|
| 269 |
$node->morelikethis = array();
|
| 270 |
|
| 271 |
foreach($more as $item) {
|
| 272 |
$entry = array(
|
| 273 |
'#item' => $item,
|
| 274 |
'#view' => theme('morelikethis_item', $item),
|
| 275 |
);
|
| 276 |
$node->morelikethis[] = $entry;
|
| 277 |
}
|
| 278 |
}
|
| 279 |
|
| 280 |
/**
|
| 281 |
* Theme the body of the More Like This block.
|
| 282 |
*
|
| 283 |
* @param $items
|
| 284 |
* An array of More Like This objects for the provided node.
|
| 285 |
*/
|
| 286 |
function theme_morelikethis_block($items){
|
| 287 |
if(!$items)
|
| 288 |
return t('No related items were found.');
|
| 289 |
|
| 290 |
$links = array();
|
| 291 |
foreach($items as $item){
|
| 292 |
$links[] = theme('morelikethis_item', $item);
|
| 293 |
}
|
| 294 |
return theme('item_list', $links);
|
| 295 |
}
|
| 296 |
|
| 297 |
/**
|
| 298 |
* Theme an individual morelikethis item.
|
| 299 |
*
|
| 300 |
* @param $item
|
| 301 |
* An More Like This object for the provided node.
|
| 302 |
*/
|
| 303 |
function theme_morelikethis_item($item) {
|
| 304 |
$percentage = sprintf('%.1f%%', $item->relevance * 100);
|
| 305 |
return l("$item->title ($percentage)", "node/$item->nid");
|
| 306 |
}
|