Index: relatedlinks.module =================================================================== --- relatedlinks.module (.../trunk/newashoka/public/modules/relatedlinks/relatedlinks.module) (revision 1232) +++ relatedlinks.module (.../sandboxes/cschwartz/newashoka/public/modules/relatedlinks/relatedlinks.module) (working copy) @@ -3,31 +3,70 @@ /** * @file - * Related links are defined in 2 ways: - * 1) For certain content types, HTML links are automatically discovered and included. - * 2) The author may manually add HTML links that will appear at the top of the - * 'Related links' block. - * 3) An optional block can also list related taxonomy nodes as related links. - * - * When a node is viewed alone, a block is provided to authorized users that - * displays a complete list of links. If no links are defined, the block will - * disappear. + * Provides a block with related links. + * + * Implemented Drupal Hooks: relatedlinks_help() + * relatedlinks_menu() + * relatedlinks_perm() + * relatedlinks_nodeapi() + * relatedlinks_form_alter() + * relatedlinks_block() + * + * Theme Functions: theme_relatedlinks() + * theme_relatedlinks_types_table() + * + * Private Functions: _relatedlinks_settings_form() + * _relatedlinks_settings_form_submit() + * _relatedlinks_add_links() + * _relatedlinks_delete_links() + * _relatedlinks_get_db_links() + * _relatedlinks_get_taxonomy_links() + * _relatedlinks_get_type_property() + * _relatedlinks_get_type_defaults() + * _relatedlinks_sort() + * _relatedlinks_get_link_url() + * _relatedlinks_get_link_text() */ // Define mnemonics to indicate the types of links stored in the relatedlinks // table [type field]. -define('RELATEDLINKS_PARSED', 1); -define('RELATEDLINKS_MANUAL', 2); +define('RELATEDLINKS_PARSED', 1); +define('RELATEDLINKS_MANUAL', 2); +define('RELATEDLINKS_TAXONOMY', 3); /** * Implementation of hook_help(). */ function relatedlinks_help($section) { + + // Add some basic help text. + $helptext = t( + 'Related links are defined in 3 ways: ' . + '
' . + '
Parsed Links
' . + '
For certain content types, HTML links ' . + 'are automatically discovered and included.
' . + '
Manual Links
' . + '
The author may manually add HTML links.
' . + '
Taxonomy Links
' . + '
Related taxonomy nodes can also be listed.
' . + '
'); + + // Add extra information for the help page. + $helptext_extra = t( + '

When a node is viewed alone, a block is provided to authorized ' . + 'users that displays a list of related links. If no links are ' . + 'defined, the block will disappear.

' . + '

Parsed and manual links are updated when the node is edited. ' . + 'Taxonomy links are determined on the fly.

'); + switch ($section) { case 'admin/modules#description': return t('Provides a block with related links.'); case 'admin/help#relatedlinks': - return t('Related links are defined in 2 ways: in certain input formats, HTML links are automatically discovered and included; when publishing certain content types, the author may manually add HTML links that will appear at the top of the Related links block. When a node is viewed alone, a block is provided to authorized users that displays a complete list of links. If no links are defined, the block will disappear.'); + return $helptext . $helptext_extra; + case 'admin/relatedlinks': + return $helptext; } } @@ -67,10 +106,28 @@ * */ function relatedlinks_nodeapi(&$node, $op, $arg) { - if ((user_access('add related links') || user_access('administer related links') && in_array($node->type, variable_get('relatedlinks_node_types', array())))) { + if (user_access('add related links') || + user_access('administer related links') && + in_array($node->type, variable_get('relatedlinks_nodes', array()))) { switch ($op) { case 'load': - $links = _relatedlinks_get_links($node->nid, RELATEDLINKS_MANUAL); + $links = _relatedlinks_get_db_links($node->nid, RELATEDLINKS_MANUAL); + + // The links that come in from the database are HTML-formatted. + // This isn't very user friendly, so we're splitting each into + // the URL and the title, separated by a space. Or if there isn't + // a separate title, just list the URL. + foreach ($links as $index => $link) { + $url = _relatedlinks_get_link_url($link); + $title = _relatedlinks_get_link_text($link); + + if ($url == $title) { + $links[$index] = $url; + } else { + $links[$index] = $url . " " . $title; + } + } + $node->relatedlinks = !empty($links) ? implode("\n", $links) : ''; break; case 'delete': @@ -80,10 +137,16 @@ _relatedlinks_delete_links($node->nid); // Fall through. case 'insert': - if (in_array('manual', variable_get('relatedlinks_types', array('parsed')))) { - _relatedlinks_add_links($node->nid, explode("\n", $node->relatedlinks), RELATEDLINKS_MANUAL); + + if (_relatedlinks_get_type_property('Manual', 'enabled')) { + // Reverse the array to maintain the original ordering + // across multiple node updates. + _relatedlinks_add_links($node->nid, + array_reverse(explode("\n", $node->relatedlinks)), + RELATEDLINKS_MANUAL); } - if (in_array('parsed', variable_get('relatedlinks_types', array('parsed')))) { + + if (_relatedlinks_get_type_property('Parsed', 'enabled')) { // Rather than parsing out only the URI + link text, an attempt is // made to retain any other attributes present. preg_match_all('#(]+>[^<]+)#', $node->body, $matches); @@ -101,7 +164,8 @@ unset($matches[1][$index]); } } - _relatedlinks_add_links($node->nid, $matches[1], RELATEDLINKS_PARSED); + _relatedlinks_add_links($node->nid, $matches[1], + RELATEDLINKS_PARSED); } } } @@ -112,10 +176,16 @@ * Implementation of hook_form_alter(). */ function relatedlinks_form_alter($form_id, &$form) { - if (isset($form['type']) && $form['type']['#value'] .'_node_form' == $form_id && in_array('manual', variable_get('relatedlinks_types', array('parsed')))) { + if (isset($form['type']) && + ($form['type']['#value'] .'_node_form' == $form_id) && + _relatedlinks_get_type_property('Manual', 'enabled') && + (user_access('add related links') || + user_access('administer related links'))) { + $node = $form['#node']; - if (!in_array($node->type, variable_get('relatedlinks_node_types', array()))) { + if (!in_array($node->type, + variable_get('relatedlinks_nodes', array()))) { return; } @@ -132,7 +202,15 @@ '#type' => 'textarea', '#default_value' => $relatedlinks, '#rows' => 3, - '#description' => t('To manually define links to related material, enter 1 HTML-formatted link per line. Example: <a href="http://www.example.com">Clickable text</a>.'), + '#notinymce' => TRUE, + '#description' => t('To manually define links to related material, ' . + 'enter one URL and title (separated by a space) ' . + 'per line. Example: "http://www.example.com ' . + 'Clickable Text". Alternately, you can '. + 'enter an internal site address. Example: ' . + '"about About Us" '. + 'If no title is submitted, ' . + 'the URL itself will become the title.'), ); } } @@ -144,9 +222,9 @@ * @todo Add caching support. */ function relatedlinks_block($op = 'list', $delta = 0) { + if ($op == 'list') { - $blocks[0]['info'] = t('Related links'); - $blocks[1]['info'] = t('Related taxonomy terms'); + $blocks[0]['info'] = t('Related Links'); return $blocks; } if ($op == 'view') { @@ -155,22 +233,65 @@ if ($node = node_load(arg(1))) { switch ($delta) { case 0: - if (in_array($node->type, variable_get('relatedlinks_node_types', array()))) { - // _relatedlinks_get_links also takes care of links filtering and validation. - $links = _relatedlinks_get_links($node->nid); - - if (!empty($links)) { - $block['subject'] = t('Related links'); - $block['content'] = theme('relatedlinks', $links); + // Only display the block if the current node type has related + // links enabled. + if (in_array($node->type, + variable_get('relatedlinks_nodes', array()))) { + + // We need to gather all of the taxonomies that we'll need + // in order to find taxonomy related links. The reason we're + // not gathering them all (i.e. array_keys($node->taxonomy)) + // is because we may be restricted to certain vocabularies. + $taxonomies = array(); + foreach ($node->taxonomy as $key => $taxonomy) { + if (in_array($taxonomy->vid, + variable_get('relatedlinks_vocabularies', + array($taxonomy->vid)))) { + $taxonomies[] = $key; + } } + + // Combine links arrays from all sources. + $links_data = array_merge( + _relatedlinks_get_db_links($node->nid), + _relatedlinks_get_taxonomy_links($taxonomies, $node->nid)); + + // Sort them. + uasort($links_data, '_relatedlinks_sort'); + + // If there are any links, display them. + if (count($links_data)) { + $block['subject'] = t('Related Links'); + + // Create an array that'll tell us how many of each link + // type we have. Each key is a link type, and each value + // is the number of that type in the $links_data array. + // While we're there, create a list containing only the + // links, with no other data. + $types = array(); + $links_only = array(); + foreach ($links_data as $key => $value) { + + // Skip any elements that exceed the link type limit. + $limit =_relatedlinks_get_type_property($value['type'],'max'); + if ((!$limit) || ($types[$value['type']] < $limit)) { + $types[$value['type']]++; + $links_only[] = $value['link']; + } + } + + // Theme each sublist with a title. + $offset = 0; + $block['content'] = ''; + foreach ($types as $link_type => $number_of_this_type) { + $block['content'] .= theme('relatedlinks', + array_slice($links_only, $offset, $number_of_this_type), + _relatedlinks_get_type_property($link_type, 'title')); + $offset += $number_of_this_type; + } + } } break; - case 1: - $links = _relatedlinks_get_terms(array_keys($node->taxonomy), $node->nid); - if (!empty($links)) { - $block['subject'] = t('Related terms'); - $block['content'] = theme('relatedlinks_terms', $links); - } } } } @@ -181,15 +302,37 @@ /** * Theme the relatedlinks block output. */ -function theme_relatedlinks($links = array()) { - return theme('item_list', $links); +function theme_relatedlinks($links = array(), $title) { + return theme('item_list', $links, $title); } /** - * Theme the relatedlinks block output. + * Theme the relatedlinks settings form. */ -function theme_relatedlinks_terms($links = array()) { - return theme('item_list', $links); +function theme_relatedlinks_types_table($form) { + + // Create an link type index for weight & then sort it. + $weights = array(); + foreach (element_children($form) as $index){ + $weights[$index] = $form[$index]['weight']['#value']; + } + asort($weights); + + // Render the link type data in a table sorted by weight. + foreach (array_keys($weights) as $type) { + $rows[] = array( + form_render($form[$type]['name']), + form_render($form[$type]['enabled']), + form_render($form[$type]['weight']), + form_render($form[$type]['title']), + form_render($form[$type]['max']), + ); + } + $header = array(t('Link Type'), t('Enabled'), t('Weight'), t('Title'), + t('Limit')); + $output = theme('table', $header, $rows, array('style' => 'width: 100%', + 'class' => 'form-item')); + return $output; } /** @@ -197,29 +340,105 @@ * due to the checkboxes element. */ function _relatedlinks_settings_form() { - $form['relatedlinks'] = array( + + // Create the Link Types section. + $linktypes_data = variable_get('relatedlinks_types', array()); + $form['relatedlinks_types'] = array( '#type' => 'fieldset', - '#title' => t('Related links'), + '#title' => t('Link Types'), '#collapsible' => TRUE, '#collapsed' => FALSE, + '#theme' => 'relatedlinks_types_table', + '#tree' => TRUE, + '#weight' => 1, + '#description' => t('These are the controls for specific link types. ' . + 'Heavier weighted link types will sink to the ' . + 'bottom. To remove titles for each type, simply ' . + 'leave those fields blank. For an unlimited number ' . + 'of links, leave the Limit field blank.'), ); - $form['relatedlinks']['relatedlinks_node_types'] = array( + $defaults = _relatedlinks_get_type_defaults(); + foreach ($defaults as $typename => $typedefaults) { + $form['relatedlinks_types'][$typename]['name'] = array( + '#value' => $typename, + ); + $form['relatedlinks_types'][$typename]['enabled'] = array( + '#type' => 'checkbox', + '#return_value' => TRUE, + '#default_value' => $linktypes_data ? + $linktypes_data[$typename]['enabled'] : $typedefaults['enabled'], + ); + $form['relatedlinks_types'][$typename]['weight'] = array( + '#type' => 'weight', + '#delta' => 10, + '#default_value' => $linktypes_data ? + $linktypes_data[$typename]['weight'] : $typedefaults['weight'], + ); + $form['relatedlinks_types'][$typename]['title'] = array( + '#type' => 'textfield', + '#size' => 20, + '#maxlength' => 40, + '#default_value' => $linktypes_data ? + $linktypes_data[$typename]['title'] : $typedefaults['title'], + ); + $form['relatedlinks_types'][$typename]['max'] = array( + '#type' => 'textfield', + '#size' => 3, + '#maxlength' => 3, + '#default_value' => $linktypes_data ? + $linktypes_data[$typename]['max'] : $typedefaults['max'], + ); + } + + // Create the Activated Content Types section. + $form['activated_content_types'] = array( + '#type' => 'fieldset', + '#title' => t('Activated Content Types'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#weight' => 2, + ); + $form['activated_content_types']['relatedlinks_nodes'] = array( '#type' => 'checkboxes', - '#title' => t('Node associations'), - '#description' => t('Select the node types to associate with the related links module [parsed and manually added links]. This is not applicable to the taxonomy terms block.'), + '#title' => t('Content Types'), + '#description' => t('Select the content types to enable for parsed and ' . + 'manual links.'), '#options' => node_get_types(), - '#default_value' => variable_get('relatedlinks_node_types', array()), + '#default_value' => variable_get('relatedlinks_nodes', array()), ); - $form['relatedlinks']['relatedlinks_types'] = array( - '#type' => 'checkboxes', - '#title' => t('Link types'), - '#description' => t('Select the link types to enable.'), - '#options' => array('parsed' => t('Parsed links'), - 'manual' => t('Manually added links')), - '#default_value' => variable_get('relatedlinks_types', array('parsed')), - ); - $form['submit'] = array('#type' => 'submit', '#value' => t('Save configuration')); + // Create the Activated Taxonomy Vocabularies section. + if (module_exist('taxonomy')) { + $voptions = array(); + $vocabularies = taxonomy_get_vocabularies(); + foreach ($vocabularies as $vid => $vname) { + $voptions[$vid] = t($vname->name); + } + $form['activated_taxonomy_vocabularies'] = array( + '#type' => 'fieldset', + '#title' => t('Activated Taxonomy Vocabularies'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#weight' => 3, + ); + $form['activated_taxonomy_vocabularies']['relatedlinks_vocabularies']=array( + '#type' => 'checkboxes', + '#title' => t('Taxonomy Vocabularies'), + '#description' => t('Select the taxonomy vocabularies to enable for ' . + 'taxonomy links.'), + '#options' => $voptions, + '#default_value' => variable_get('relatedlinks_vocabularies', + array_keys($vocabularies)), + ); + } + + // Set the form submissino information. + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Save configuration'), + '#weight' => 4); + + // Return the completed from. return drupal_get_form('_relatedlinks_settings_form', $form); } @@ -227,12 +446,14 @@ * Process relatedlinks settings form submissions. */ function _relatedlinks_settings_form_submit($form_id, $form_values) { - $types = array_filter($form_values['relatedlinks_types']); - variable_set('relatedlinks_types', array_keys($types)); + variable_set('relatedlinks_types', $form_values['relatedlinks_types']); - $node_types = array_filter($form_values['relatedlinks_node_types']); - variable_set('relatedlinks_node_types', array_keys($node_types)); + $nodes = array_filter($form_values['relatedlinks_nodes']); + variable_set('relatedlinks_nodes', array_keys($nodes)); + $vocabularies = array_filter($form_values['relatedlinks_vocabularies']); + variable_set('relatedlinks_vocabularies', array_keys($vocabularies)); + drupal_set_message(t('Configuration settings saved.')); cache_clear_all(); } @@ -244,7 +465,27 @@ foreach ($links as $link) { $link = trim($link); if (!empty($link)) { - db_query("INSERT INTO {relatedlinks} (nid, link, type) VALUES (%d, '%s', %d)", $nid, $link, $type); + + // Manual links need to be converted from a user-friendly format + // to HTML format because they are entered by the user. + if ($type == RELATEDLINKS_MANUAL) { + // Get the URL and the title from the text. + $url_and_title = explode(" ", $link); + $url = array_shift($url_and_title); + $title = implode(" ", $url_and_title); + + // If there is no title, the title will become the URL itself. + if (!$title) { + $title = $url; + } + + // Construct the HTML-formatted link. + $link = "$title"; + } + + // Insert it into the database. + db_query("INSERT INTO {relatedlinks} (nid, link, type) " . + "VALUES (%d, '%s', %d)", $nid, $link, $type); } } } @@ -257,41 +498,219 @@ } /** - * Retrieve related links from the database. + * Retrieve related links from the database. Currently, these include + * parsed links and manual links. This function also takes care + * of links filtering and validation. + * + * Returns: If $type is specified, then a one-dimensional array is returned + * filled with links of that type. + * + * If $type is not specified, a two-dimensional array is returned. + * In this case, the 'link' field will contain the link, the + * 'type' field will contain the link type, and the 'weight' + * field will contain the link type's weight.. */ -function _relatedlinks_get_links($nid, $type = NULL) { +function _relatedlinks_get_db_links($nid, $type = NULL) { if (!isset($type)) { - // The ORDER BY clause ensures that manually added links are given preference. - $result = db_query('SELECT link FROM {relatedlinks} WHERE nid = %d ORDER BY TYPE DESC', $nid); + // The ORDER BY clause ensures that manually added links are given + // preference. + $result = db_query('SELECT link, type FROM {relatedlinks} WHERE nid = %d ' . + 'ORDER BY TYPE DESC', $nid); } else { - $result = db_query('SELECT link FROM {relatedlinks} WHERE nid = %d AND type = %d ORDER BY TYPE DESC', $nid, $type); + $result = db_query('SELECT link FROM {relatedlinks} ' . + 'WHERE nid = %d AND type = %d ORDER BY TYPE DESC', + $nid, $type); } + // Fetch the data into the an array. $links = array(); - + $link_number = 0; while ($link = db_fetch_array($result)) { - $links[] = filter_xss($link['link'], array('a')); + if (!isset($type)) { + // We need to get the link type information as well. + $links[$link_number]['link'] = filter_xss($link['link'], array('a')); + $links[$link_number]['type'] = $link['type']; + $links[$link_number]['weight'] = + _relatedlinks_get_type_property($link['type'], 'weight'); + } + else { + $links[$link_number] = filter_xss($link['link'], array('a')); + } + $link_number++; } + // Return it. return $links; } /** - * Retrieve related taxonomy terms. + * Retrieve taxonomy links. Rather than fetching these from the database, + * they are collected on the fly. They are dependant on the current taxonomy + * state, independant of the current node. */ -function _relatedlinks_get_terms($tids, $nid) { - // The following query is likely to prove expensive when the numbers involved are large. - // pgSQL compliant? - $result = module_invoke('taxonomy', 'select_nodes', $tids, 'or', 0, FALSE, 'RAND()'); +function _relatedlinks_get_taxonomy_links($tids, $nid) { + $links = array(); - $links = array(); - while ($node = db_fetch_array($result)) { - // Exclude the current nid. - if ($node['nid'] != $nid) { - $links[] = l($node['title'], 'node/'. $node['nid']); + // Only get the terms if this link type is enabled. + if (_relatedlinks_get_type_property('Taxonomy', 'enabled')) { + + // The following query is likely to prove expensive when the numbers + // involved are large. + // pgSQL compliant? + $result = module_invoke('taxonomy', 'select_nodes', $tids, 'or', 0, + FALSE, 'RAND()'); + + // Fetch the data into an array. + $link_number = 0; + while ($node = db_fetch_array($result)) { + // Exclude the current nid. + if ($node['nid'] != $nid) { + $links[$link_number]['link'] = l($node['title'], 'node/'. $node['nid']); + $links[$link_number]['type'] = RELATEDLINKS_TAXONOMY; + $links[$link_number]['weight'] = + _relatedlinks_get_type_property(RELATEDLINKS_TAXONOMY, 'weight'); + } + $link_number++; } } + // Return it. return $links; } + +/** + * Retrieve a link type property. + * + * @param $type + * The link type. + * @param $property + * The property to return. + * @return + * The property value. + */ +function _relatedlinks_get_type_property($type, $property) { + + // If the type is an integer, convert it to a string. + if ($type == 1) $type = 'Parsed'; + if ($type == 2) $type = 'Manual'; + if ($type == 3) $type = 'Taxonomy'; + + // If the link types data structure has been set, get the property from there. + if ($linktypes_data = variable_get('relatedlinks_types', array())) { + return $linktypes_data[$type][$property]; + } + + // Otherwise, we have to look at the default values. + $defaults = _relatedlinks_get_type_defaults(); + + // And set them up for next time. + variable_set('relatedlinks_types', $defaults); + + return $defaults[$type][$property]; +} // _relatedlinks_get_type_property + +/** + * Retrieve the link type defaults. + * This should probably be done with globals somehow, instead of in a function. + * + * @return + * An array containing defaults for each link type. + */ +function _relatedlinks_get_type_defaults() { + + /* + * Create a data structure for link types data. + * To add a new link type, simply add a new array to the structure below. + */ + $defaults = array(); + + // Parsed Links + $defaults['Parsed'] = array('enabled' => TRUE, + 'weight' => 2, + 'title' => t('In Content'), + 'max' => 5); + // Manual Links + $defaults['Manual'] = array('enabled' => FALSE, + 'weight' => 1, + 'title' => t('By Recommendation'), + 'max' => 5); + // Taxonomy Link + $defaults['Taxonomy'] = array('enabled' => FALSE, + 'weight' => 3, + 'title' => t('By Category'), + 'max' => 5); + + return $defaults; +} // _relatedlinks_get_type_defaults + +/** + * Compare two links for sorting. + * Links are sorted by weight, type, and then alphanumerically. + * + * @param $link_one + * The first link + * @param $link_two + * The second link + * @return + * +1 if $link_one is greater than $link_two + * 0 if $link_one is equal to $link_two + * -1 if $link_one is less than $link_two + */ +function _relatedlinks_sort($link_one, $link_two) { + + /* + * w => weight + * t => type + * n => name + */ + $w1 = $link_one['weight']; + $w2 = $link_two['weight']; + $t1 = $link_one['type']; + $t2 = $link_two['type']; + $n1 = _relatedlinks_get_link_text($link_one['link']); + $n2 = _relatedlinks_get_link_text($link_two['link']); + + // Sort first by weight, then by type, and then finally alphabetically. + switch (TRUE) { + case ($w1 > $w2): + return 1; + case ($w1 < $w2): + return -1; + case ($w1 == $w2): + switch (TRUE) { + case ($t1 > $t2): + return 1; + case ($t1 < $t2): + return -1; + case ($t1 == $t2): + return strcasecmp($n1, $n2); + } + } +} // _relatedlinks_sort + +/** + * Get the URL from a link. + * + * @param $link + * The link as stored in the DB, text surrounded by tags. + * @return + * The URL. + */ +function _relatedlinks_get_link_url($link) { + preg_match('#href="([^"]*)"#',$link,$matches); + return ($matches[1]); +} + +/** + * Get the text from a link. + * + * @param $link + * The link as stored in the DB, text surrounded by tags. + * @return + * The link text. + */ +function _relatedlinks_get_link_text($link) { + preg_match('#>([^<]*)<#',$link,$matches); + return ($matches[1]); +}